Skip to content

Commit 06f143d

Browse files
committed
Refactor code to take into account optional fields
Refactor code so we are not passing in values and only using the defaults from the module config Show parent field if there are no child fields to show We want to show the labels field (among others) in the markdown table if so that it is part of the documentation rather than showing all the preset labels as individual fields in the docs. Signed-off-by: Luke Mallon (Nalum) <luke@mallon.ie> Update schema with +nodoc tags and copy to blueprint starter module Hide certain fields This change brings the output more inline with the definition Update output to remove default column Commit updated test data Signed-off-by: Luke Mallon (Nalum) <luke@mallon.ie> Update nodoc behaviour Signed-off-by: Luke Mallon (Nalum) <luke@mallon.ie> Update structure to accomodate nodoc Signed-off-by: Luke Mallon (Nalum) <luke@mallon.ie> Merge branch 'main' into module-md-optional-fields
1 parent 9fba645 commit 06f143d

File tree

15 files changed

+287
-107
lines changed

15 files changed

+287
-107
lines changed

blueprints/starter/README.md

Lines changed: 33 additions & 16 deletions
Large diffs are not rendered by default.

blueprints/starter/cue.mod/pkg/timoni.sh/core/v1alpha1/image.cue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import (
3030

3131
// Reference is the image address computed from repository, tag and digest
3232
// in the format [REPOSITORY]:[TAG]@[DIGEST].
33+
// +nodoc
3334
reference: string
3435

3536
if digest != "" && tag != "" {

blueprints/starter/cue.mod/pkg/timoni.sh/core/v1alpha1/metadata.cue

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,13 +43,17 @@ import "strings"
4343

4444
// Standard Kubernetes labels: app name, version and managed-by.
4545
labels: {
46-
(#StdLabelName): name
47-
(#StdLabelVersion): #Version
46+
// +nodoc
47+
(#StdLabelName): name
48+
// +nodoc
49+
(#StdLabelVersion): #Version
50+
// +nodoc
4851
(#StdLabelManagedBy): "timoni"
4952
}
5053

5154
// LabelSelector selects Pods based on the app.kubernetes.io/name label.
5255
#LabelSelector: #Labels & {
56+
// +nodoc
5357
(#StdLabelName): name
5458
}
5559

@@ -74,6 +78,7 @@ import "strings"
7478
namespace: #Meta.namespace
7579

7680
labels: #Meta.labels
81+
// +nodoc
7782
labels: (#StdLabelComponent): #Component
7883

7984
annotations?: #Annotations
@@ -84,8 +89,10 @@ import "strings"
8489
// LabelSelector selects Pods based on the app.kubernetes.io/name
8590
// and app.kubernetes.io/component labels.
8691
#LabelSelector: #Labels & {
92+
// +nodoc
8793
(#StdLabelComponent): #Component
88-
(#StdLabelName): #Meta.name
94+
// +nodoc
95+
(#StdLabelName): #Meta.name
8996
}
9097
}
9198

@@ -104,6 +111,7 @@ import "strings"
104111
name: #Meta.name + "-" + #Component
105112

106113
labels: #Meta.labels
114+
// +nodoc
107115
labels: (#StdLabelComponent): #Component
108116

109117
annotations?: #Annotations
@@ -114,7 +122,9 @@ import "strings"
114122
// LabelSelector selects Pods based on the app.kubernetes.io/name
115123
// and app.kubernetes.io/component labels.
116124
#LabelSelector: #Labels & {
125+
// +nodoc
117126
(#StdLabelComponent): #Component
118-
(#StdLabelName): #Meta.name
127+
// +nodoc
128+
(#StdLabelName): #Meta.name
119129
}
120130
}

blueprints/starter/cue.mod/pkg/timoni.sh/core/v1alpha1/selector.cue

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,8 @@ package v1alpha1
1515
labels: #Labels
1616

1717
// Standard Kubernetes label: app name.
18-
labels: (#StdLabelName): #Name
18+
labels: {
19+
// +nodoc
20+
(#StdLabelName): #Name
21+
}
1922
}

blueprints/starter/cue.mod/pkg/timoni.sh/core/v1alpha1/semver.cue

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,11 @@ import (
2121
let minMajor = strconv.Atoi(strings.Split(#Minimum, ".")[0])
2222
let minMinor = strconv.Atoi(strings.Split(#Minimum, ".")[1])
2323

24+
// +nodoc
2425
major: int & >=minMajor
2526
major: strconv.Atoi(strings.Split(#Version, ".")[0])
2627

28+
// +nodoc
2729
minor: int & >=minMinor
2830
minor: strconv.Atoi(strings.Split(#Version, ".")[1])
2931
}

blueprints/starter/templates/config.cue

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,16 @@ import (
99
#Config: {
1010
// The kubeVersion is a required field, set at apply-time
1111
// via timoni.cue by querying the user's Kubernetes API.
12+
// +nodoc
1213
kubeVersion!: string
1314
// Using the kubeVersion you can enforce a minimum Kubernetes minor version.
1415
// By default, the minimum Kubernetes version is set to 1.20.
16+
// +nodoc
1517
clusterVersion: timoniv1.#SemVer & {#Version: kubeVersion, #Minimum: "1.20.0"}
1618

1719
// The moduleVersion is set from the user-supplied module version.
1820
// This field is used for the `app.kubernetes.io/version` label.
21+
// +nodoc
1922
moduleVersion!: string
2023

2124
// The Kubernetes metadata common to all resources.

cmd/timoni/mod_show_config.go

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -124,17 +124,16 @@ func runConfigShowModCmd(cmd *cobra.Command, args []string) error {
124124
return fmt.Errorf("build failed: %w", err)
125125
}
126126

127-
buildResult, err := builder.Build()
127+
_, err = builder.Build()
128128
if err != nil {
129129
return describeErr(f.GetModuleRoot(), "validation failed", err)
130130
}
131-
132-
rows, err := builder.GetConfigDoc(buildResult)
131+
rows, err := builder.GetConfigDoc()
133132
if err != nil {
134133
return describeErr(f.GetModuleRoot(), "failed to get config structure", err)
135134
}
136135

137-
header := []string{"Key", "Type", "Default", "Description"}
136+
header := []string{"Key", "Type", "Description"}
138137

139138
if configShowModArgs.output == "" {
140139
printMarkDownTable(rootCmd.OutOrStdout(), header, rows)
@@ -166,7 +165,6 @@ func writeFile(readFile string, header []string, rows [][]string, f fetcher.Fetc
166165
if err != nil {
167166
if errors.Is(err, fs.ErrNotExist) {
168167
inputFile, err = os.Create(readFile)
169-
170168
if err != nil {
171169
return "", describeErr(f.GetModuleRoot(), "Unable to create the temporary output file", err)
172170
}

cmd/timoni/testdata/module/README.md

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -41,15 +41,15 @@ timoni -n module delete module
4141

4242
## Configuration
4343

44-
| KEY | TYPE | DEFAULT | DESCRIPTION |
45-
|------------------------------|----------|-----------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
46-
| `client: enabled:` | `bool` | `true` | |
47-
| `client: image: repository:` | `string` | `"cgr.dev/chainguard/timoni"` | Repository is the address of a container registry repository. An image repository is made up of slash-separated name components, optionally prefixed by a registry hostname and port in the format [HOST[:PORT_NUMBER]/]PATH. |
48-
| `client: image: tag:` | `string` | `"latest-dev"` | Tag identifies an image in the repository. A tag name may contain lowercase and uppercase characters, digits, underscores, periods and dashes. A tag name may not start with a period or a dash and may contain a maximum of 128 characters. |
49-
| `client: image: digest:` | `string` | `"sha256:b49fbaac0eedc22c1cfcd26684707179cccbed0df205171bae3e1bae61326a10"` | Digest uniquely and immutably identifies an image in the repository. Spec: https://github.com/opencontainers/image-spec/blob/main/descriptor.md#digests. |
50-
| `client: image: pullPolicy:` | `string` | `"IfNotPresent"` | PullPolicy defines the pull policy for the image. By default, it is set to IfNotPresent. |
51-
| `server: enabled:` | `bool` | `true` | |
52-
| `domain:` | `string` | `"example.internal"` | |
53-
| `globals: enabled:` | `bool` | `false` | |
54-
| `team:` | `string` | `"test"` | |
44+
| KEY | TYPE | DESCRIPTION |
45+
|------------------------------|----------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
46+
| `client: enabled:` | `*true \| bool` | |
47+
| `client: image: repository:` | `*"cgr.dev/chainguard/timoni" \| string` | Repository is the address of a container registry repository. An image repository is made up of slash-separated name components, optionally prefixed by a registry hostname and port in the format [HOST[:PORT_NUMBER]/]PATH. |
48+
| `client: image: tag:` | `*"latest-dev" \| strings.MaxRunes(128)` | Tag identifies an image in the repository. A tag name may contain lowercase and uppercase characters, digits, underscores, periods and dashes. A tag name may not start with a period or a dash and may contain a maximum of 128 characters. |
49+
| `client: image: digest:` | `*"sha256:b49fbaac0eedc22c1cfcd26684707179cccbed0df205171bae3e1bae61326a10" \| string` | Digest uniquely and immutably identifies an image in the repository. Spec: https://github.com/opencontainers/image-spec/blob/main/descriptor.md#digests. |
50+
| `client: image: pullPolicy:` | `*"IfNotPresent" \| "Always" \| "Never"` | PullPolicy defines the pull policy for the image. By default, it is set to IfNotPresent. |
51+
| `server: enabled:` | `*true \| bool` | |
52+
| `domain:` | `*"example.internal" \| string` | |
53+
| `globals: enabled:` | `*false \| bool` | |
54+
| `team:` | `"test"` | |
5555

cmd/timoni/testdata/module/templates/config.cue

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,25 +23,21 @@ import (
2323
"app.kubernetes.io/team": team
2424
}
2525

26-
// +nodoc
2726
client: {
2827
enabled: *true | bool
2928

30-
// +nodoc
3129
image: timoniv1.#Image & {
3230
repository: *"cgr.dev/chainguard/timoni" | string
3331
tag: *"latest-dev" | string
3432
digest: *"sha256:b49fbaac0eedc22c1cfcd26684707179cccbed0df205171bae3e1bae61326a10" | string
3533
}
3634
}
3735

38-
// +nodoc
3936
server: {
4037
enabled: *true | bool
4138
}
4239
domain: *"example.internal" | string
4340

44-
// +nodoc
4541
globals: {
4642
enabled: *false | bool
4743
}

internal/engine/get_config.go

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
/*
2+
Copyright 2023 Stefan Prodan
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package engine
18+
19+
import (
20+
"errors"
21+
"fmt"
22+
"regexp"
23+
"strings"
24+
25+
"cuelang.org/go/cue"
26+
"cuelang.org/go/cue/ast"
27+
"cuelang.org/go/cue/load"
28+
29+
apiv1 "github.com/stefanprodan/timoni/api/v1alpha1"
30+
)
31+
32+
// GetConfigDoc extracts the config structure from the module.
33+
func (b *ModuleBuilder) GetConfigDoc() ([][]string, error) {
34+
var value cue.Value
35+
36+
cfg := &load.Config{
37+
ModuleRoot: b.moduleRoot,
38+
Package: b.pkgName,
39+
Dir: b.pkgPath,
40+
DataFiles: true,
41+
Tags: []string{
42+
"name=" + b.name,
43+
"namespace=" + b.namespace,
44+
},
45+
TagVars: map[string]load.TagVar{
46+
"moduleVersion": {
47+
Func: func() (ast.Expr, error) {
48+
return ast.NewString(b.moduleVersion), nil
49+
},
50+
},
51+
"kubeVersion": {
52+
Func: func() (ast.Expr, error) {
53+
return ast.NewString(b.kubeVersion), nil
54+
},
55+
},
56+
},
57+
}
58+
59+
modInstances := load.Instances([]string{}, cfg)
60+
if len(modInstances) == 0 {
61+
return nil, errors.New("no instances found")
62+
}
63+
64+
modInstance := modInstances[0]
65+
if modInstance.Err != nil {
66+
return nil, fmt.Errorf("instance error: %w", modInstance.Err)
67+
}
68+
69+
value = b.ctx.BuildInstance(modInstance)
70+
if value.Err() != nil {
71+
return nil, value.Err()
72+
}
73+
74+
cfgValues := value.LookupPath(cue.ParsePath(apiv1.ConfigValuesSelector.String()))
75+
if cfgValues.Err() != nil {
76+
return nil, fmt.Errorf("lookup %s failed: %w", apiv1.ConfigValuesSelector, cfgValues.Err())
77+
}
78+
79+
rows, err := iterateFields(cfgValues)
80+
if err != nil {
81+
return nil, err
82+
}
83+
84+
return rows, nil
85+
}
86+
87+
func iterateFields(v cue.Value) ([][]string, error) {
88+
var rows [][]string
89+
90+
fields, err := v.Fields(
91+
cue.Optional(true),
92+
cue.Concrete(true),
93+
cue.Docs(true),
94+
)
95+
if err != nil {
96+
return nil, fmt.Errorf("Cue Fields Error: %w", err)
97+
}
98+
99+
for fields.Next() {
100+
v := fields.Value()
101+
_, noDoc := hasNoDoc(v)
102+
103+
if noDoc {
104+
continue
105+
}
106+
107+
// We are chekcing if the field is a struct and not optional and is concrete before we iterate through it
108+
// this allows for definition of default values as full structs without generating output for each
109+
// field in the struct where it doesn't make sense e.g.
110+
//
111+
// - annotations?: {[string]: string}
112+
// - affinity: corev1.Affinity | *{nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms: [...]}
113+
if v.IncompleteKind() == cue.StructKind && !fields.IsOptional() && v.IsConcrete() {
114+
//if _, ok := v.Default(); v.IncompleteKind() == cue.StructKind && !fields.IsOptional() && ok {
115+
// Assume we want to use the field
116+
useField := true
117+
iRows, err := iterateFields(v)
118+
119+
if err != nil {
120+
return nil, err
121+
}
122+
123+
for _, row := range iRows {
124+
if len(row) > 0 {
125+
// If we have a row with more than 0 elements, we don't want to use the field and should use the child rows instead
126+
useField = false
127+
rows = append(rows, row)
128+
}
129+
}
130+
131+
if useField {
132+
rows = append(rows, getField(v))
133+
}
134+
} else {
135+
rows = append(rows, getField(v))
136+
}
137+
}
138+
139+
return rows, nil
140+
}
141+
142+
func hasNoDoc(v cue.Value) (string, bool) {
143+
var noDoc bool
144+
var doc string
145+
146+
for _, d := range v.Doc() {
147+
if line := len(d.List) - 1; line >= 0 {
148+
switch d.List[line].Text {
149+
case "// +nodoc":
150+
noDoc = true
151+
break
152+
}
153+
}
154+
155+
doc += d.Text()
156+
doc = strings.ReplaceAll(doc, "\n", " ")
157+
doc = strings.ReplaceAll(doc, "+required", "")
158+
doc = strings.ReplaceAll(doc, "+optional", "")
159+
}
160+
161+
return doc, noDoc
162+
}
163+
164+
func getField(v cue.Value) []string {
165+
var row []string
166+
labelDomain := regexp.MustCompile(`^([a-zA-Z0-9-_.]+)?(".+")?$`)
167+
doc, noDoc := hasNoDoc(v)
168+
169+
if !noDoc {
170+
fieldType := strings.ReplaceAll(fmt.Sprintf("%v", v), "\n", "")
171+
fieldType = strings.ReplaceAll(fieldType, "|", "\\|")
172+
fieldType = strings.ReplaceAll(fieldType, "\":", "\": ")
173+
fieldType = strings.ReplaceAll(fieldType, "\":[", "\": [")
174+
fieldType = strings.ReplaceAll(fieldType, "},", "}, ")
175+
176+
if len(fieldType) == 0 {
177+
fieldType = " "
178+
}
179+
180+
field := strings.Replace(v.Path().String(), "timoni.instance.config.", "", 1)
181+
match := labelDomain.FindStringSubmatch(field)
182+
183+
row = append(row, fmt.Sprintf("`%s:`", strings.ReplaceAll(match[1], ".", ": ")+match[2]))
184+
row = append(row, fmt.Sprintf("`%s`", fieldType))
185+
row = append(row, fmt.Sprintf("%s", doc))
186+
}
187+
188+
return row
189+
}

0 commit comments

Comments
 (0)