diff --git a/cmd/timoni/testdata/module/README.md b/cmd/timoni/testdata/module/README.md index 8672510b..18c15bb5 100644 --- a/cmd/timoni/testdata/module/README.md +++ b/cmd/timoni/testdata/module/README.md @@ -41,15 +41,15 @@ timoni -n module delete module ## Configuration -| KEY | TYPE | DEFAULT | DESCRIPTION | -|------------------------------|----------|------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `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. | -| `client: enabled:` | `bool` | `true` | | -| `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. | -| `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. | -| `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. | -| `server: enabled:` | `bool` | `true` | | -| `domain:` | `string` | `"example.internal"` | | -| `ns: enabled:` | `bool` | `false` | | -| `team:` | `string` | `"test"` | | +| KEY | TYPE | DEFAULT | DESCRIPTION | +|------------------------------|----------|-----------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `metadata: annotations:` | `struct` | `{}` | Annotations is an unstructured key value map stored with a resource that may be set to store and retrieve arbitrary metadata. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations | +| `client: enabled:` | `bool` | `true` | | +| `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. | +| `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. | +| `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. | +| `server: enabled:` | `bool` | `true` | | +| `domain:` | `string` | `"example.internal"` | | +| `ns: enabled:` | `bool` | `false` | | +| `team:` | `string` | `"test"` | | diff --git a/internal/engine/module_builder.go b/internal/engine/module_builder.go index cf0ce1aa..f710af45 100644 --- a/internal/engine/module_builder.go +++ b/internal/engine/module_builder.go @@ -315,52 +315,91 @@ func (b *ModuleBuilder) GetConfigDoc(value cue.Value) ([][]string, error) { return nil, fmt.Errorf("lookup %s failed: %w", apiv1.ConfigValuesSelector, cfgValues.Err()) } - labelDomain := regexp.MustCompile(`^([a-zA-Z0-9-_.]+)?(".+")?$`) + rows, err := iterateFields(cfgValues) + if err != nil { + return nil, err + } + + return rows, nil +} +func iterateFields(v cue.Value) ([][]string, error) { var rows [][]string - configDataInfo := func(v cue.Value) bool { - var row []string - var noDoc bool - var doc string - - for _, d := range v.Doc() { - if line := len(d.List) - 1; line >= 0 { - switch d.List[line].Text { - case "// +nodoc": - noDoc = true - break - } + + fields, err := v.Fields( + cue.Optional(true), + cue.Concrete(true), + cue.Docs(true), + ) + if err != nil { + return nil, fmt.Errorf("Cue Fields Error: %w", err) + } + + for fields.Next() { + v := fields.Value() + fmt.Println(fields.Label(), v.IncompleteKind(), v.IsConcrete()) + + // We are chekcing if the field is a struct and not optional and is concrete before we iterate through it + // this allows for definition of default values as full structs without generating output for each + // field in the struct where it doesn't make sense e.g. + // + // - annotations?: {[string]: string} + // - affinity: corev1.Affinity | *{nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms: [...]} + if v.IncompleteKind() == cue.StructKind && !fields.IsOptional() && v.IsConcrete() { + iRows, err := iterateFields(v) + if err != nil { + return nil, err } - doc += d.Text() - doc = strings.ReplaceAll(doc, "\n", " ") - doc = strings.ReplaceAll(doc, "+required", "") - doc = strings.ReplaceAll(doc, "+optional", "") + rows = append(rows, iRows...) + } else { + rows = append(rows, getField(v)) } + } - if !noDoc { - defaultVal, _ := v.Default() - valueBytes, _ := defaultVal.MarshalJSON() - valueType := strings.ReplaceAll(v.IncompleteKind().String(), "|", "\\|") + return rows, nil +} - value := strings.ReplaceAll(string(valueBytes), "\":", "\": ") - value = strings.ReplaceAll(value, "\":[", "\": [") - value = strings.ReplaceAll(value, "},", "}, ") - value = strings.ReplaceAll(value, "|", "\\|") +func getField(v cue.Value) []string { + var row []string + labelDomain := regexp.MustCompile(`^([a-zA-Z0-9-_.]+)?(".+")?$`) - field := strings.Replace(v.Path().String(), "timoni.instance.config.", "", 1) - match := labelDomain.FindStringSubmatch(field) + var noDoc bool + var doc string - row = append(row, fmt.Sprintf("`%s:`", strings.ReplaceAll(match[1], ".", ": ")+match[2])) - row = append(row, fmt.Sprintf("`%s`", valueType)) - row = append(row, fmt.Sprintf("`%s`", value)) - row = append(row, fmt.Sprintf("%s", doc)) - rows = append(rows, row) + for _, d := range v.Doc() { + if line := len(d.List) - 1; line >= 0 { + switch d.List[line].Text { + case "// +nodoc": + noDoc = true + break + } } - return true + doc += d.Text() + doc = strings.ReplaceAll(doc, "\n", " ") + doc = strings.ReplaceAll(doc, "+required", "") + doc = strings.ReplaceAll(doc, "+optional", "") } - cfgValues.Walk(configDataInfo, nil) - return rows, nil + if !noDoc { + defaultVal, _ := v.Default() + valueBytes, _ := defaultVal.MarshalJSON() + valueType := strings.ReplaceAll(v.IncompleteKind().String(), "|", "\\|") + + value := strings.ReplaceAll(string(valueBytes), "\":", "\": ") + value = strings.ReplaceAll(value, "\":[", "\": [") + value = strings.ReplaceAll(value, "},", "}, ") + value = strings.ReplaceAll(value, "|", "\\|") + + field := strings.Replace(v.Path().String(), "timoni.instance.config.", "", 1) + match := labelDomain.FindStringSubmatch(field) + + row = append(row, fmt.Sprintf("`%s:`", strings.ReplaceAll(match[1], ".", ": ")+match[2])) + row = append(row, fmt.Sprintf("`%s`", valueType)) + row = append(row, fmt.Sprintf("`%s`", value)) + row = append(row, fmt.Sprintf("%s", doc)) + } + + return row }