Skip to content

Commit

Permalink
Refactor code to take into account optional fields
Browse files Browse the repository at this point in the history
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) <[email protected]>
  • Loading branch information
Nalum committed Oct 3, 2024
1 parent cf22fe3 commit b0e99db
Show file tree
Hide file tree
Showing 4 changed files with 184 additions and 64 deletions.
6 changes: 1 addition & 5 deletions cmd/timoni/mod_show_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,12 +124,8 @@ func runConfigShowModCmd(cmd *cobra.Command, args []string) error {
return fmt.Errorf("build failed: %w", err)
}

buildResult, err := builder.Build()
if err != nil {
return describeErr(f.GetModuleRoot(), "validation failed", err)
}
rows, err := builder.GetConfigDoc()

rows, err := builder.GetConfigDoc(buildResult)
if err != nil {
return describeErr(f.GetModuleRoot(), "failed to get config structure", err)
}
Expand Down
1 change: 1 addition & 0 deletions cmd/timoni/testdata/module/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ timoni -n module delete module

| 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 |
| `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. |
Expand Down
182 changes: 182 additions & 0 deletions internal/engine/get_config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
/*
Copyright 2023 Stefan Prodan
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package engine

import (
"errors"
"fmt"
"regexp"
"strings"

"cuelang.org/go/cue"
"cuelang.org/go/cue/ast"
"cuelang.org/go/cue/load"

apiv1 "github.com/stefanprodan/timoni/api/v1alpha1"
)

// GetConfigDoc extracts the config structure from the module.
func (b *ModuleBuilder) GetConfigDoc() ([][]string, error) {
var value cue.Value

cfg := &load.Config{
ModuleRoot: b.moduleRoot,
Package: b.pkgName,
Dir: b.pkgPath,
DataFiles: true,
Tags: []string{
"name=" + b.name,
"namespace=" + b.namespace,
},
TagVars: map[string]load.TagVar{
"moduleVersion": {
Func: func() (ast.Expr, error) {
return ast.NewString(b.moduleVersion), nil
},
},
"kubeVersion": {
Func: func() (ast.Expr, error) {
return ast.NewString(b.kubeVersion), nil
},
},
},
}

modInstances := load.Instances([]string{}, cfg)
if len(modInstances) == 0 {
return nil, errors.New("no instances found")
}

modInstance := modInstances[0]
if modInstance.Err != nil {
return nil, fmt.Errorf("instance error: %w", modInstance.Err)
}

value = b.ctx.BuildInstance(modInstance)
if value.Err() != nil {
return nil, value.Err()
}

cfgValues := value.LookupPath(cue.ParsePath(apiv1.ConfigValuesSelector.String()))
if cfgValues.Err() != nil {
return nil, fmt.Errorf("lookup %s failed: %w", apiv1.ConfigValuesSelector, cfgValues.Err())
}

rows, err := iterateFields(cfgValues)
if err != nil {
return nil, err
}

return rows, nil
}

func iterateFields(v cue.Value) ([][]string, error) {
var rows [][]string

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()

// 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() {
// Assume we want to use the field
useField := true
iRows, err := iterateFields(v)

if err != nil {
return nil, err
}

for _, row := range iRows {
if len(row) > 0 {
// 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
useField = false
rows = append(rows, row)
}
}

if useField {
rows = append(rows, getField(v))
}
} else {
rows = append(rows, getField(v))
}
}

return rows, nil
}

func getField(v cue.Value) []string {
var row []string
labelDomain := regexp.MustCompile(`^([a-zA-Z0-9-_.]+)?(".+")?$`)

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
}
}

doc += d.Text()
doc = strings.ReplaceAll(doc, "\n", " ")
doc = strings.ReplaceAll(doc, "+required", "")
doc = strings.ReplaceAll(doc, "+optional", "")
}

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, "|", "\\|")

if len(value) == 0 {
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
}
59 changes: 0 additions & 59 deletions internal/engine/module_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,7 @@ import (
"os"
"path/filepath"
"reflect"
"regexp"
"slices"
"strings"

"cuelang.org/go/cue"
"cuelang.org/go/cue/ast"
Expand Down Expand Up @@ -316,60 +314,3 @@ func (b *ModuleBuilder) GetContainerImages(value cue.Value) ([]string, error) {

return images, nil
}

// GetConfigDoc extracts the config structure from the module.
func (b *ModuleBuilder) GetConfigDoc(value cue.Value) ([][]string, error) {
cfgValues := value.LookupPath(cue.ParsePath(apiv1.ConfigValuesSelector.String()))
if cfgValues.Err() != nil {
return nil, fmt.Errorf("lookup %s failed: %w", apiv1.ConfigValuesSelector, cfgValues.Err())
}

labelDomain := regexp.MustCompile(`^([a-zA-Z0-9-_.]+)?(".+")?$`)

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
}
}

doc += d.Text()
doc = strings.ReplaceAll(doc, "\n", " ")
doc = strings.ReplaceAll(doc, "+required", "")
doc = strings.ReplaceAll(doc, "+optional", "")
}

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))
rows = append(rows, row)
}

return true
}

cfgValues.Walk(configDataInfo, nil)
return rows, nil
}

0 comments on commit b0e99db

Please sign in to comment.