Skip to content

Commit f44efe5

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. 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 Update nodoc behaviour Update structure to accomodate nodoc Signed-off-by: Luke Mallon (Nalum) <[email protected]>
1 parent cf22fe3 commit f44efe5

File tree

15 files changed

+285
-108
lines changed

15 files changed

+285
-108
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: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -124,17 +124,13 @@ func runConfigShowModCmd(cmd *cobra.Command, args []string) error {
124124
return fmt.Errorf("build failed: %w", err)
125125
}
126126

127-
buildResult, err := builder.Build()
128-
if err != nil {
129-
return describeErr(f.GetModuleRoot(), "validation failed", err)
130-
}
127+
rows, err := builder.GetConfigDoc()
131128

132-
rows, err := builder.GetConfigDoc(buildResult)
133129
if err != nil {
134130
return describeErr(f.GetModuleRoot(), "failed to get config structure", err)
135131
}
136132

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

139135
if configShowModArgs.output == "" {
140136
printMarkDownTable(rootCmd.OutOrStdout(), header, rows)

cmd/timoni/testdata/module/README.md

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

4242
## Configuration
4343

44-
| KEY | TYPE | DEFAULT | DESCRIPTION |
45-
|------------------------------|----------|------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
46-
| `metadata: labels:` | `struct` | `{"app.kubernetes.io/name": "module-name","app.kubernetes.io/kube": "1.27.5","app.kubernetes.io/version": "0.0.0-devel","app.kubernetes.io/team": "test"}` | Map of string keys and values that can be used to organize and categorize (scope and select) objects. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels Standard Kubernetes labels: app name and version. |
47-
| `client: enabled:` | `bool` | `true` | |
48-
| `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. |
49-
| `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. |
50-
| `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. |
51-
| `server: enabled:` | `bool` | `true` | |
52-
| `domain:` | `string` | `"example.internal"` | |
53-
| `ns: 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+
| `server: enabled:` | `*true \| bool` | |
51+
| `domain:` | `*"example.internal" \| string` | |
52+
| `ns: enabled:` | `*false \| bool` | |
53+
| `team:` | `"test"` | |
5554

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

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,25 +17,21 @@ import (
1717
"app.kubernetes.io/team": team
1818
}
1919

20-
// +nodoc
2120
client: {
2221
enabled: *true | bool
2322

24-
// +nodoc
2523
image: timoniv1.#Image & {
2624
repository: *"cgr.dev/chainguard/timoni" | string
2725
tag: *"latest-dev" | string
2826
digest: *"sha256:b49fbaac0eedc22c1cfcd26684707179cccbed0df205171bae3e1bae61326a10" | string
2927
}
3028
}
3129

32-
// +nodoc
3330
server: {
3431
enabled: *true | bool
3532
}
3633
domain: *"example.internal" | string
3734

38-
// +nodoc
3935
ns: {
4036
enabled: *false | bool
4137
}

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)