From 3ccb4ed1682e8c0c6db04c7a294e95f72060e0b7 Mon Sep 17 00:00:00 2001 From: John Norwood Date: Sun, 28 Jul 2019 21:00:23 +0000 Subject: [PATCH] feat: updates values table generation allowing for non-empty lists/maps to be documented Fixes https://github.com/norwoodj/helm-docs/issues/9 --- .goreleaser.yml | 1 + Makefile | 15 + README.md | 67 ++- cmd/helm-docs/main.go | 13 +- example-charts/custom-template/README.md | 24 +- example-charts/nginx-ingress/README.md | 25 +- example-charts/nginx-ingress/values.yaml | 9 +- example-charts/no-requirements/README.md | 8 +- go.mod | 1 + pkg/document/generate.go | 20 +- pkg/document/model.go | 15 +- pkg/document/util.go | 49 ++ pkg/document/values.go | 306 +++++++---- pkg/document/values_test.go | 622 +++++++++++++++++++++++ pkg/helm/chart_info.go | 8 +- 15 files changed, 1018 insertions(+), 165 deletions(-) create mode 100644 Makefile create mode 100644 pkg/document/util.go create mode 100644 pkg/document/values_test.go diff --git a/.goreleaser.yml b/.goreleaser.yml index 2b5a5dc..1de5122 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -1,6 +1,7 @@ before: hooks: - go mod download + - go test ./... builds: - main: ./cmd/helm-docs env: diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..6aec8b2 --- /dev/null +++ b/Makefile @@ -0,0 +1,15 @@ +helm-docs: + cd cmd/helm-docs && go build + mv cmd/helm-docs/helm-docs . + +.PHONY: fmt +fmt: + go fmt ./... + +.PHONY: test +test: + go test -v ./... + +.PHONY: clean +clean: + rm helm-docs diff --git a/README.md b/README.md index a97f2e3..cb97e78 100644 --- a/README.md +++ b/README.md @@ -99,17 +99,55 @@ controller: replicas: 2 ``` -The descriptions will be picked up and put in the table in the README. The comment need not be near the parameter it -explains, although this is probably preferable. +The following rules are used to determine which values will be added to the values table in the README: + +* By default, only _leaf nodes_, that is, fields of type `int`, `string`, `float`, `bool`, empty lists, and empty maps + are added as rows in the values table. These fields will be added even if they do not have a description comment +* Lists and maps which contain elements will not be added as rows in the values table _unless_ they have a description + comment which refers to them +* Adding a description comment for a non-empty list or map in this way makes it so that leaf nodes underneath the + described field will _not_ be automatically added to the values table. In order to document both a non-empty list/map + _and_ a leaf node within that field, description comments must be added for both + +e.g. In this case, both `controller.livenessProbe` and `controller.livenessProbe.httpGet.path` will be added as rows in +the values table, but `controller.livenessProbe.httpGet.port` will not +```yaml +controller: + # controller.livenessProbe -- Configure the healthcheck for the ingress controller + livenessProbe: + httpGet: + # controller.livenessProbe.httpGet.path -- This is the liveness check endpoint + path: /healthz + port: http +``` + +Results in: + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| controller.livenessProbe | object | `{"httpGet":{"path":"/healthz","port":8080}}` | Configure the healthcheck for the ingress controller | +| controller.livenessProbe.httpGet.path | string | `"/healthz"` | This is the liveness check endpoint | + +If we remove the comment for `controller.livenessProbe` however, both leaf nodes `controller.livenessProbe.httpGet.path` +and `controller.livenessProbe.httpGet.port` will be added to the table, with our without description comments: -_Note:_ if the value in question contains any `.` characters, that section of the path must be quoted e.g. ```yaml -service: - annotations: - # ingress.annotations."external-dns.alpha.kubernetes.io/hostname" -- Hostname to be assigned to the ELB for the service - external-dns.alpha.kubernetes.io/hostname: stupidchess.jmn23.com +controller: + livenessProbe: + httpGet: + # controller.livenessProbe.httpGet.path -- This is the liveness check endpoint + path: /healthz + port: http ``` +Results in: + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| controller.livenessProbe.httpGet.path | string | `"/healthz"` | This is the liveness check endpoint | +| controller.livenessProbe.httpGet.port | string | `"http"` | | + + ### nil values If you would like to define a key for a value, but leave the default empty, you can still specify a description for it as well as a type. Like so: @@ -119,3 +157,18 @@ controller: replicas: ``` This could be useful when wanting to enforce user-defined values for the chart, where there are no sensible defaults. + +### Spaces and Dots in keys +If a key name contains any "." or " " characters, that section of the path must be quoted in description comments e.g. + +```yaml +service: + annotations: + # service.annotations."external-dns.alpha.kubernetes.io/hostname" -- Hostname to be assigned to the ELB for the service + external-dns.alpha.kubernetes.io/hostname: stupidchess.jmn23.com + +configMap: + # configMap."not real config param" -- A completely fake config parameter for a useful example + not real config param: value +``` + diff --git a/cmd/helm-docs/main.go b/cmd/helm-docs/main.go index a326b3f..a2fa52e 100644 --- a/cmd/helm-docs/main.go +++ b/cmd/helm-docs/main.go @@ -12,7 +12,7 @@ import ( "github.com/spf13/viper" ) -func retrieveInfoAndPrintDocumentation(chartDirectory string, waitGroup *sync.WaitGroup) { +func retrieveInfoAndPrintDocumentation(chartDirectory string, waitGroup *sync.WaitGroup, dryRun bool) { defer waitGroup.Done() chartDocumentationInfo, err := helm.ParseChartInformation(chartDirectory) @@ -21,7 +21,7 @@ func retrieveInfoAndPrintDocumentation(chartDirectory string, waitGroup *sync.Wa return } - document.PrintDocumentation(chartDocumentationInfo, viper.GetBool("dry-run")) + document.PrintDocumentation(chartDocumentationInfo, dryRun) } @@ -35,11 +35,18 @@ func helmDocs(_ *cobra.Command, _ []string) { } log.Infof("Found Chart directories [%s]", strings.Join(chartDirs, ", ")) + dryRun := viper.GetBool("dry-run") waitGroup := sync.WaitGroup{} for _, c := range chartDirs { waitGroup.Add(1) - go retrieveInfoAndPrintDocumentation(c, &waitGroup) + + // On dry runs all output goes to stdout, and so as to not jumble things, generate serially + if dryRun { + retrieveInfoAndPrintDocumentation(c, &waitGroup, dryRun) + } else { + go retrieveInfoAndPrintDocumentation(c, &waitGroup, dryRun) + } } waitGroup.Wait() diff --git a/example-charts/custom-template/README.md b/example-charts/custom-template/README.md index 259ce4a..27d2684 100644 --- a/example-charts/custom-template/README.md +++ b/example-charts/custom-template/README.md @@ -23,15 +23,15 @@ culpa qui officia deserunt mollit anim id est laborum. | Key | Type | Default | Description | |-----|------|---------|-------------| -| controller.extraVolumes[0].configMap.name | string | "nginx-ingress-config" | Uses the name of the configmap created by this chart | -| controller.extraVolumes[0].name | string | "config-volume" | | -| controller.image.repository | string | "nginx-ingress-controller" | | -| controller.image.tag | string | "18.0831" | | -| controller.ingressClass | string | "nginx" | Name of the ingress class to route through this controller | -| controller.name | string | "controller" | | -| controller.persistentVolumeClaims | list | [] | List of persistent volume claims to create | -| controller.podLabels | object | {} | The labels to be applied to instances of the controller pod | -| controller.publishService.enabled | bool | false | Whether to expose the ingress controller to the public world | -| controller.replicas | int | \ | Number of nginx-ingress pods to load balance between | -| controller.service.annotations."external-dns.alpha.kubernetes.io/hostname" | string | "stupidchess.jmn23.com" | Hostname to be assigned to the ELB for the service | -| controller.service.type | string | "LoadBalancer" | | +| controller.extraVolumes[0].configMap.name | string | `"nginx-ingress-config"` | Uses the name of the configmap created by this chart | +| controller.extraVolumes[0].name | string | `"config-volume"` | | +| controller.image.repository | string | `"nginx-ingress-controller"` | | +| controller.image.tag | string | `"18.0831"` | | +| controller.ingressClass | string | `"nginx"` | Name of the ingress class to route through this controller | +| controller.name | string | `"controller"` | | +| controller.persistentVolumeClaims | list | `[]` | List of persistent volume claims to create | +| controller.podLabels | object | `{}` | The labels to be applied to instances of the controller pod | +| controller.publishService.enabled | bool | `false` | Whether to expose the ingress controller to the public world | +| controller.replicas | int | `nil` | Number of nginx-ingress pods to load balance between | +| controller.service.annotations."external-dns.alpha.kubernetes.io/hostname" | string | `"stupidchess.jmn23.com"` | Hostname to be assigned to the ELB for the service | +| controller.service.type | string | `"LoadBalancer"` | | diff --git a/example-charts/nginx-ingress/README.md b/example-charts/nginx-ingress/README.md index c968191..2a8db99 100644 --- a/example-charts/nginx-ingress/README.md +++ b/example-charts/nginx-ingress/README.md @@ -16,15 +16,16 @@ Source code can be found [here](https://github.com/norwoodj/helm-docs/example-ch | Key | Type | Default | Description | |-----|------|---------|-------------| -| controller.extraVolumes[0].configMap.name | string | "nginx-ingress-config" | Uses the name of the configmap created by this chart | -| controller.extraVolumes[0].name | string | "config-volume" | | -| controller.image.repository | string | "nginx-ingress-controller" | | -| controller.image.tag | string | "18.0831" | | -| controller.ingressClass | string | "nginx" | Name of the ingress class to route through this controller | -| controller.name | string | "controller" | | -| controller.persistentVolumeClaims | list | [] | List of persistent volume claims to create | -| controller.podLabels | object | {} | The labels to be applied to instances of the controller pod | -| controller.publishService.enabled | bool | false | Whether to expose the ingress controller to the public world | -| controller.replicas | int | \ | Number of nginx-ingress pods to load balance between | -| controller.service.annotations."external-dns.alpha.kubernetes.io/hostname" | string | "stupidchess.jmn23.com" | Hostname to be assigned to the ELB for the service | -| controller.service.type | string | "LoadBalancer" | | +| controller.extraVolumes | list | `[{"configMap":{"name":"nginx-ingress-config"},"name":"config-volume"}]` | Additional volumes to be mounted into the ingress controller container | +| controller.image.repository | string | `"nginx-ingress-controller"` | | +| controller.image.tag | string | `"18.0831"` | | +| controller.ingressClass | string | `"nginx"` | Name of the ingress class to route through this controller | +| controller.livenessProbe | object | `{"httpGet":{"path":"/healthz","port":8080}}` | Configure the healthcheck for the ingress controller | +| controller.livenessProbe.httpGet.path | string | `"/healthz"` | This is the liveness check endpoint | +| controller.name | string | `"controller"` | | +| controller.persistentVolumeClaims | list | `[]` | List of persistent volume claims to create | +| controller.podLabels | object | `{}` | The labels to be applied to instances of the controller pod | +| controller.publishService.enabled | bool | `false` | Whether to expose the ingress controller to the public world | +| controller.replicas | int | `nil` | Number of nginx-ingress pods to load balance between | +| controller.service.annotations."external-dns.alpha.kubernetes.io/hostname" | string | `"stupidchess.jmn23.com"` | Hostname to be assigned to the ELB for the service | +| controller.service.type | string | `"LoadBalancer"` | | diff --git a/example-charts/nginx-ingress/values.yaml b/example-charts/nginx-ingress/values.yaml index 7da3c04..06d22c4 100644 --- a/example-charts/nginx-ingress/values.yaml +++ b/example-charts/nginx-ingress/values.yaml @@ -7,12 +7,19 @@ controller: # controller.persistentVolumeClaims -- List of persistent volume claims to create persistentVolumeClaims: [] + # controller.extraVolumes -- Additional volumes to be mounted into the ingress controller container extraVolumes: - name: config-volume configMap: - # controller.extraVolumes[0].configMap.name -- Uses the name of the configmap created by this chart name: nginx-ingress-config + # controller.livenessProbe -- Configure the healthcheck for the ingress controller + livenessProbe: + httpGet: + # controller.livenessProbe.httpGet.path -- This is the liveness check endpoint + path: /healthz + port: 8080 + # controller.ingressClass -- Name of the ingress class to route through this controller ingressClass: nginx diff --git a/example-charts/no-requirements/README.md b/example-charts/no-requirements/README.md index 67789e6..167450e 100644 --- a/example-charts/no-requirements/README.md +++ b/example-charts/no-requirements/README.md @@ -12,7 +12,7 @@ Source code can be found [here](https://github.com/norwoodj/helm-docs/example-ch | Key | Type | Default | Description | |-----|------|---------|-------------| -| rules.latency.percentiles.99.duration | string | "5m" | Duration for which the 99th percentile must be above the threshold to alert | -| rules.latency.percentiles.99.threshold | float | 1.5 | Threshold in seconds for our 99th percentile latency above which the alert will fire | -| rules.statusCodes.codes.5xx.duration | string | "5m" | Duration for which the percent of 5xx responses must be above the threshold to alert | -| rules.statusCodes.codes.5xx.threshold | float | 1.5 | Threshold percentage of 5xx responses above which the alert will fire | +| rules.latency.percentiles.99.duration | string | `"5m"` | Duration for which the 99th percentile must be above the threshold to alert | +| rules.latency.percentiles.99.threshold | float | `1.5` | Threshold in seconds for our 99th percentile latency above which the alert will fire | +| rules.statusCodes.codes.5xx.duration | string | `"5m"` | Duration for which the percent of 5xx responses must be above the threshold to alert | +| rules.statusCodes.codes.5xx.threshold | float | `1.5` | Threshold percentage of 5xx responses above which the alert will fire | diff --git a/go.mod b/go.mod index 82b9209..52b3818 100644 --- a/go.mod +++ b/go.mod @@ -11,5 +11,6 @@ require ( github.com/spf13/cobra v0.0.5 github.com/spf13/pflag v1.0.3 github.com/spf13/viper v1.4.0 + github.com/stretchr/testify v1.2.2 gopkg.in/yaml.v2 v2.2.2 ) diff --git a/pkg/document/generate.go b/pkg/document/generate.go index df45a6f..8422c10 100644 --- a/pkg/document/generate.go +++ b/pkg/document/generate.go @@ -26,25 +26,29 @@ func getOutputFile(chartDirectory string, dryRun bool) (*os.File, error) { func PrintDocumentation(chartDocumentationInfo helm.ChartDocumentationInfo, dryRun bool) { log.Infof("Generating README Documentation for chart %s", chartDocumentationInfo.ChartDirectory) - outputFile, err := getOutputFile(chartDocumentationInfo.ChartDirectory, dryRun) + chartDocumentationTemplate, err := newChartDocumentationTemplate(chartDocumentationInfo) if err != nil { - log.Warnf("Could not open chart README file %s, skipping chart", filepath.Join(chartDocumentationInfo.ChartDirectory, "README.md")) + log.Warnf("Error generating gotemplates for chart %s: %s", chartDocumentationInfo.ChartDirectory, err) return } - if !dryRun { - defer outputFile.Close() + chartTemplateDataObject, err := getChartTemplateData(chartDocumentationInfo) + if err != nil { + log.Warnf("Error generating template data for chart %s: %s", chartDocumentationInfo.ChartDirectory, err) + return } - chartDocumentationTemplate, err := newChartDocumentationTemplate(chartDocumentationInfo) + outputFile, err := getOutputFile(chartDocumentationInfo.ChartDirectory, dryRun) if err != nil { - log.Warnf("Error generating templates for chart %s: %s", chartDocumentationInfo.ChartDirectory, err) + log.Warnf("Could not open chart README file %s, skipping chart", filepath.Join(chartDocumentationInfo.ChartDirectory, "README.md")) return } - chartTemplateDataObject := getChartTemplateData(chartDocumentationInfo) - err = chartDocumentationTemplate.Execute(outputFile, chartTemplateDataObject) + if !dryRun { + defer outputFile.Close() + } + err = chartDocumentationTemplate.Execute(outputFile, chartTemplateDataObject) if err != nil { log.Warnf("Error generating documentation for chart %s: %s", chartDocumentationInfo.ChartDirectory, err) } diff --git a/pkg/document/model.go b/pkg/document/model.go index a03f591..56950d6 100644 --- a/pkg/document/model.go +++ b/pkg/document/model.go @@ -16,11 +16,20 @@ type chartTemplateData struct { Values []valueRow } -func getChartTemplateData(chartDocumentationInfo helm.ChartDocumentationInfo) chartTemplateData { - valuesTableRows := createValueRows("", chartDocumentationInfo.ChartValues, chartDocumentationInfo.ChartValuesDescriptions) +func getChartTemplateData(chartDocumentationInfo helm.ChartDocumentationInfo) (chartTemplateData, error) { + valuesTableRows, err := createValueRowsFromObject( + "", + chartDocumentationInfo.ChartValues, + chartDocumentationInfo.ChartValuesDescriptions, + true, + ) + + if err != nil { + return chartTemplateData{}, err + } return chartTemplateData{ ChartDocumentationInfo: chartDocumentationInfo, Values: valuesTableRows, - } + }, nil } diff --git a/pkg/document/util.go b/pkg/document/util.go new file mode 100644 index 0000000..29d1a58 --- /dev/null +++ b/pkg/document/util.go @@ -0,0 +1,49 @@ +package document + +import ( + "fmt" +) + +type jsonableMap map[string]interface{} + +func convertMapKeyToString(key interface{}) string { + switch key.(type) { + case string: + return key.(string) + case int: + return fmt.Sprintf("int(%d)", key) + case float64: + return fmt.Sprintf("float(%f)", key) + case bool: + return fmt.Sprintf("bool(%t)", key) + } + + return fmt.Sprintf("?(%+v)", key) +} + +// The json library can only marshal maps with string keys, and so all of our lists and maps that go into documentation +// must be converted to have only string keys before marshalling +func convertHelmValuesToJsonable(values interface{}) interface{} { + switch values.(type) { + case map[interface{}]interface{}: + convertedMap := make(jsonableMap) + + for key, value := range values.(map[interface{}]interface{}) { + convertedMap[convertMapKeyToString(key)] = convertHelmValuesToJsonable(value) + } + + return convertedMap + + case []interface{}: + convertedList := make([]interface{}, 0) + + for _, value := range values.([]interface{}) { + convertedList = append(convertedList, convertHelmValuesToJsonable(value)) + } + + return convertedList + + default: + return values + } +} diff --git a/pkg/document/values.go b/pkg/document/values.go index ffdccea..d9af984 100644 --- a/pkg/document/values.go +++ b/pkg/document/values.go @@ -1,13 +1,11 @@ package document import ( + "encoding/json" "fmt" "regexp" "sort" - "strconv" "strings" - - "github.com/norwoodj/helm-docs/pkg/helm" ) const ( @@ -19,63 +17,53 @@ const ( stringType = "string" ) -func createAtomRow(prefix string, value interface{}, keysToDescriptions map[string]string) valueRow { - description := keysToDescriptions[prefix] +var nilValueTypeRegex, _ = regexp.Compile("^\\(.*?\\)") + +func formatNextListKeyPrefix(prefix string, index int) string { + return fmt.Sprintf("%s[%d]", prefix, index) +} + +func formatNextObjectKeyPrefix(prefix string, key string) string { + var escapedKey string + var nextPrefix string + + if strings.Contains(key, ".") || strings.Contains(key, " ") { + escapedKey = fmt.Sprintf(`"%s"`, key) + } else { + escapedKey = key + } + + if prefix != "" { + nextPrefix = fmt.Sprintf("%s.%s", prefix, escapedKey) + } else { + nextPrefix = fmt.Sprintf("%s", escapedKey) + } + + return nextPrefix +} +func getTypeName(value interface{}) string { switch value.(type) { case bool: - return valueRow{ - Key: prefix, - Type: boolType, - Default: fmt.Sprintf("%t", value), - Description: description, - } + return boolType case float64: - return valueRow{ - Key: prefix, - Type: floatType, - Default: strconv.FormatFloat(value.(float64), 'f', -1, 64), - Description: description, - } + return floatType case int: - return valueRow{ - Key: prefix, - Type: intType, - Default: fmt.Sprintf("%d", value), - Description: description, - } + return intType case string: - return valueRow{ - Key: prefix, - Type: stringType, - Default: fmt.Sprintf("\"%s\"", value), - Description: description, - } + return stringType case []interface{}: - return valueRow{ - Key: prefix, - Type: listType, - Default: "[]", - Description: description, - } - case helm.ChartValues: - return valueRow{ - Key: prefix, - Type: objectType, - Default: "{}", - Description: description, - } - case nil: - return parseNilValueType(prefix, description) + return listType + case jsonableMap: + return objectType } - return valueRow{} + return "" } -func parseNilValueType(prefix string, description string) valueRow { +func parseNilValueType(key string, description string) valueRow { // Grab whatever's in between the parentheses of the description and treat it as the type - r, _ := regexp.Compile("^\\(.*?\\)") - t := r.FindString(description) + t := nilValueTypeRegex.FindString(description) if len(t) > 0 { t = t[1 : len(t)-1] @@ -85,97 +73,195 @@ func parseNilValueType(prefix string, description string) valueRow { } return valueRow{ - Key: prefix, + Key: key, Type: t, - Default: "\\", + Default: "`nil`", Description: description, } } -func createListRows(prefix string, values []interface{}, keysToDescriptions map[string]string) []valueRow { +func createValueRow( + key string, + value interface{}, + description string, +) (valueRow, error) { + if value == nil { + return parseNilValueType(key, description), nil + } + + jsonEncodedValue, err := json.Marshal(value) + if err != nil { + return valueRow{}, fmt.Errorf("failed to marshal default value for %s to json: %s", key, err) + } + + defaultValue := fmt.Sprintf("`%s`", jsonEncodedValue) + return valueRow{ + Key: key, + Type: getTypeName(value), + Default: defaultValue, + Description: description, + }, nil +} + +func createRowsFromField( + nextPrefix string, + value interface{}, + keysToDescriptions map[string]string, + documentLeafNodes bool, +) ([]valueRow, error) { + valueRows := make([]valueRow, 0) + + switch value.(type) { + case map[interface{}]interface{}: + subObjectValuesRows, err := createValueRowsFromObject(nextPrefix, value.(map[interface{}]interface{}), keysToDescriptions, documentLeafNodes) + if err != nil { + return nil, err + } + + valueRows = append(valueRows, subObjectValuesRows...) + + case []interface{}: + subListValuesRows, err := createValueRowsFromList(nextPrefix, value.([]interface{}), keysToDescriptions, documentLeafNodes) + if err != nil { + return nil, err + } + + valueRows = append(valueRows, subListValuesRows...) + + default: + description, hasDescription := keysToDescriptions[nextPrefix] + if !(documentLeafNodes || hasDescription) { + return []valueRow{}, nil + } + + leafValueRow, err := createValueRow(nextPrefix, value, description) + if err != nil { + return nil, err + } + + valueRows = append(valueRows, leafValueRow) + } + + return valueRows, nil +} + +func createValueRowsFromList( + prefix string, + values []interface{}, + keysToDescriptions map[string]string, + documentLeafNodes bool, +) ([]valueRow, error) { + description, hasDescription := keysToDescriptions[prefix] + + // If we encounter an empty list, it should be documented if no parent object or list had a description or if this + // list has a description if len(values) == 0 { - return []valueRow{createAtomRow(prefix, values, keysToDescriptions)} + + if !(documentLeafNodes || hasDescription) { + return []valueRow{}, nil + } + + emptyListRow, err := createValueRow(prefix, values, description) + if err != nil { + return nil, err + } + + return []valueRow{emptyListRow}, nil } - valueRows := []valueRow{} + valueRows := make([]valueRow, 0) - for i, v := range values { - var nextPrefix string - if prefix != "" { - nextPrefix = fmt.Sprintf("%s[%d]", prefix, i) - } else { - nextPrefix = fmt.Sprintf("[%d]", i) + // We have a nonempty list with a description, document it, and mark that leaf nodes underneath it should not be + // documented without descriptions + if hasDescription { + jsonableObject := convertHelmValuesToJsonable(values) + listRow, err := createValueRow(prefix, jsonableObject, description) + + if err != nil { + return nil, err } - switch v.(type) { - case helm.ChartValues: - valueRows = append(valueRows, createValueRows(nextPrefix, v.(helm.ChartValues), keysToDescriptions)...) - case []interface{}: - valueRows = append(valueRows, createListRows(nextPrefix, v.([]interface{}), keysToDescriptions)...) - case bool: - valueRows = append(valueRows, createAtomRow(nextPrefix, v, keysToDescriptions)) - case float64: - valueRows = append(valueRows, createAtomRow(nextPrefix, v, keysToDescriptions)) - case int: - valueRows = append(valueRows, createAtomRow(nextPrefix, v, keysToDescriptions)) - case string: - valueRows = append(valueRows, createAtomRow(nextPrefix, v, keysToDescriptions)) - break + valueRows = append(valueRows, listRow) + documentLeafNodes = false + } + + // Generate documentation rows for all list items and their potential sub-fields + for i, v := range values { + nextPrefix := formatNextListKeyPrefix(prefix, i) + valueRowsForListField, err := createRowsFromField(nextPrefix, v, keysToDescriptions, documentLeafNodes) + + if err != nil { + return nil, err } + + valueRows = append(valueRows, valueRowsForListField...) } - return valueRows + return valueRows, nil } -func createValueRows(prefix string, values helm.ChartValues, keysToDescriptions map[string]string) []valueRow { +func createValueRowsFromObject( + prefix string, + values map[interface{}]interface{}, + keysToDescriptions map[string]string, + documentLeafNodes bool, +) ([]valueRow, error) { + description, hasDescription := keysToDescriptions[prefix] + if len(values) == 0 { + // if the first level of recursion has no values, then there are no values at all, and so we return zero rows of documentation if prefix == "" { - return []valueRow{} + return []valueRow{}, nil + } + + // Otherwise, we have a leaf empty object node that should be documented if no object up the recursion chain had + // a description or if this object has a description + if !(documentLeafNodes || hasDescription) { + return []valueRow{}, nil + } + + documentedRow, err := createValueRow(prefix, jsonableMap{}, description) + + if err != nil { + return nil, err } - return []valueRow{createAtomRow(prefix, values, keysToDescriptions)} + return []valueRow{documentedRow}, nil } valueRows := make([]valueRow, 0) - for k, v := range values { - var escapedKey string - var nextPrefix string - - key := k.(string) - if strings.Contains(key, ".") { - escapedKey = fmt.Sprintf("\"%s\"", k) - } else { - escapedKey = key - } + // We have a nonempty object with a description, document it, and mark that leaf nodes underneath it should not be + // documented without descriptions + if hasDescription { + jsonableObject := convertHelmValuesToJsonable(values) + objectRow, err := createValueRow(prefix, jsonableObject, description) - if prefix != "" { - nextPrefix = fmt.Sprintf("%s.%s", prefix, escapedKey) - } else { - nextPrefix = fmt.Sprintf("%s", escapedKey) + if err != nil { + return nil, err } - switch v.(type) { - case helm.ChartValues: - valueRows = append(valueRows, createValueRows(nextPrefix, v.(helm.ChartValues), keysToDescriptions)...) - case []interface{}: - valueRows = append(valueRows, createListRows(nextPrefix, v.([]interface{}), keysToDescriptions)...) - case bool: - valueRows = append(valueRows, createAtomRow(nextPrefix, v, keysToDescriptions)) - case float64: - valueRows = append(valueRows, createAtomRow(nextPrefix, v, keysToDescriptions)) - case int: - valueRows = append(valueRows, createAtomRow(nextPrefix, v, keysToDescriptions)) - case string: - valueRows = append(valueRows, createAtomRow(nextPrefix, v, keysToDescriptions)) - default: - valueRows = append(valueRows, createAtomRow(nextPrefix, v, keysToDescriptions)) + valueRows = append(valueRows, objectRow) + documentLeafNodes = false + } + + for k, v := range values { + nextPrefix := formatNextObjectKeyPrefix(prefix, convertMapKeyToString(k)) + valueRowsForObjectField, err := createRowsFromField(nextPrefix, v, keysToDescriptions, documentLeafNodes) + + if err != nil { + return nil, err } + + valueRows = append(valueRows, valueRowsForObjectField...) } - sort.Slice(valueRows[:], func(i, j int) bool { - return valueRows[i].Key < valueRows[j].Key - }) + // At the top level of recursion, sort value rows by key + if prefix == "" { + sort.Slice(valueRows[:], func(i, j int) bool { + return valueRows[i].Key < valueRows[j].Key + }) + } - return valueRows + return valueRows, nil } diff --git a/pkg/document/values_test.go b/pkg/document/values_test.go new file mode 100644 index 0000000..7e4aa3a --- /dev/null +++ b/pkg/document/values_test.go @@ -0,0 +1,622 @@ +package document + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "gopkg.in/yaml.v2" +) + +func parseYamlValues(yamlValues string) map[interface{}]interface{} { + var chartValues map[interface{}]interface{} + err := yaml.Unmarshal([]byte(strings.TrimSpace(yamlValues)), &chartValues) + + if err != nil { + panic(err) + } + + return chartValues +} + +func TestEmptyValues(t *testing.T) { + valuesRows, err := createValueRowsFromObject("", make(map[interface{}]interface{}), make(map[string]string), true) + assert.Nil(t, err) + assert.Len(t, valuesRows, 0) +} + +func TestSimpleValues(t *testing.T) { + helmValues := parseYamlValues(` +echo: 0 +foxtrot: true +hello: "world" +oscar: 3.14159 + `) + + valuesRows, err := createValueRowsFromObject("", helmValues, make(map[string]string), true) + + assert.Nil(t, err) + assert.Len(t, valuesRows, 4) + + assert.Equal(t, "echo", valuesRows[0].Key) + assert.Equal(t, intType, valuesRows[0].Type, intType) + assert.Equal(t, "`0`", valuesRows[0].Default) + assert.Equal(t, "", valuesRows[0].Description) + + assert.Equal(t, "foxtrot", valuesRows[1].Key) + assert.Equal(t, boolType, valuesRows[1].Type) + assert.Equal(t, "`true`", valuesRows[1].Default) + assert.Equal(t, "", valuesRows[1].Description) + + assert.Equal(t, "hello", valuesRows[2].Key) + assert.Equal(t, stringType, valuesRows[2].Type) + assert.Equal(t, "`\"world\"`", valuesRows[2].Default) + assert.Equal(t, "", valuesRows[2].Description) + + assert.Equal(t, "oscar", valuesRows[3].Key) + assert.Equal(t, floatType, valuesRows[3].Type) + assert.Equal(t, "`3.14159`", valuesRows[3].Default) + assert.Equal(t, "", valuesRows[3].Description) +} + +func TestSimpleValuesWithDescriptions(t *testing.T) { + helmValues := parseYamlValues(` +echo: 0 +foxtrot: true +hello: "world" +oscar: 3.14159 + `) + + descriptions := map[string]string{ + "echo": "echo", + "foxtrot": "foxtrot", + "hello": "hello", + "oscar": "oscar", + } + + valuesRows, err := createValueRowsFromObject("", helmValues, descriptions, true) + + assert.Nil(t, err) + assert.Len(t, valuesRows, 4) + + assert.Equal(t, "echo", valuesRows[0].Key) + assert.Equal(t, intType, valuesRows[0].Type, intType) + assert.Equal(t, "`0`", valuesRows[0].Default) + assert.Equal(t, "echo", valuesRows[0].Description) + + assert.Equal(t, "foxtrot", valuesRows[1].Key) + assert.Equal(t, boolType, valuesRows[1].Type) + assert.Equal(t, "`true`", valuesRows[1].Default) + assert.Equal(t, "foxtrot", valuesRows[1].Description) + + assert.Equal(t, "hello", valuesRows[2].Key) + assert.Equal(t, stringType, valuesRows[2].Type) + assert.Equal(t, "`\"world\"`", valuesRows[2].Default) + assert.Equal(t, "hello", valuesRows[2].Description) + + assert.Equal(t, "oscar", valuesRows[3].Key) + assert.Equal(t, floatType, valuesRows[3].Type) + assert.Equal(t, "`3.14159`", valuesRows[3].Default) + assert.Equal(t, "oscar", valuesRows[3].Description) +} + +func TestRecursiveValues(t *testing.T) { + helmValues := parseYamlValues(` +recursive: + echo: cat +oscar: dog + `) + + valuesRows, err := createValueRowsFromObject("", helmValues, make(map[string]string), true) + + assert.Nil(t, err) + assert.Len(t, valuesRows, 2) + + assert.Equal(t, "oscar", valuesRows[0].Key) + assert.Equal(t, stringType, valuesRows[0].Type) + assert.Equal(t, "`\"dog\"`", valuesRows[0].Default) + assert.Equal(t, "", valuesRows[0].Description) + + assert.Equal(t, "recursive.echo", valuesRows[1].Key) + assert.Equal(t, stringType, valuesRows[1].Type) + assert.Equal(t, "`\"cat\"`", valuesRows[1].Default) + assert.Equal(t, "", valuesRows[1].Description) +} + +func TestRecursiveValuesWithDescriptions(t *testing.T) { + helmValues := parseYamlValues(` +recursive: + echo: cat +oscar: dog + `) + + descriptions := map[string]string{ + "recursive.echo": "echo", + "oscar": "oscar", + } + + valuesRows, err := createValueRowsFromObject("", helmValues, descriptions, true) + + assert.Nil(t, err) + assert.Len(t, valuesRows, 2) + + assert.Equal(t, "oscar", valuesRows[0].Key) + assert.Equal(t, stringType, valuesRows[0].Type) + assert.Equal(t, "`\"dog\"`", valuesRows[0].Default) + assert.Equal(t, "oscar", valuesRows[0].Description) + + assert.Equal(t, "recursive.echo", valuesRows[1].Key) + assert.Equal(t, stringType, valuesRows[1].Type) + assert.Equal(t, "`\"cat\"`", valuesRows[1].Default) + assert.Equal(t, "echo", valuesRows[1].Description) +} + +func TestEmptyObject(t *testing.T) { + helmValues := parseYamlValues(` +recursive: {} +oscar: dog + `) + + valuesRows, err := createValueRowsFromObject("", helmValues, make(map[string]string), true) + + assert.Nil(t, err) + assert.Len(t, valuesRows, 2) + + assert.Equal(t, "oscar", valuesRows[0].Key, "oscar") + assert.Equal(t, stringType, valuesRows[0].Type) + assert.Equal(t, "`\"dog\"`", valuesRows[0].Default) + assert.Equal(t, "", valuesRows[0].Description) + + assert.Equal(t, "recursive", valuesRows[1].Key) + assert.Equal(t, objectType, valuesRows[1].Type) + assert.Equal(t, "`{}`", valuesRows[1].Default) + assert.Equal(t, "", valuesRows[1].Description) +} + +func TestEmptyObjectWithDescription(t *testing.T) { + helmValues := parseYamlValues(` +recursive: {} +oscar: dog + `) + + descriptions := map[string]string{"recursive": "an empty object"} + + valuesRows, err := createValueRowsFromObject("", helmValues, descriptions, true) + + assert.Nil(t, err) + assert.Len(t, valuesRows, 2) + + assert.Equal(t, "oscar", valuesRows[0].Key) + assert.Equal(t, stringType, valuesRows[0].Type) + assert.Equal(t, "`\"dog\"`", valuesRows[0].Default) + assert.Equal(t, "", valuesRows[0].Description) + + assert.Equal(t, "recursive", valuesRows[1].Key) + assert.Equal(t, objectType, valuesRows[1].Type) + assert.Equal(t, "`{}`", valuesRows[1].Default) + assert.Equal(t, "an empty object", valuesRows[1].Description) +} + +func TestEmptyList(t *testing.T) { + helmValues := parseYamlValues(` +birds: [] +echo: cat + `) + + valuesRows, err := createValueRowsFromObject("", helmValues, make(map[string]string), true) + + assert.Nil(t, err) + assert.Len(t, valuesRows, 2) + + assert.Equal(t, "birds", valuesRows[0].Key) + assert.Equal(t, listType, valuesRows[0].Type) + assert.Equal(t, "`[]`", valuesRows[0].Default) + assert.Equal(t, "", valuesRows[0].Description) + + assert.Equal(t, "echo", valuesRows[1].Key) + assert.Equal(t, stringType, valuesRows[1].Type) + assert.Equal(t, "`\"cat\"`", valuesRows[1].Default) + assert.Equal(t, "", valuesRows[1].Description) +} + +func TestEmptyListWithDescriptions(t *testing.T) { + helmValues := parseYamlValues(` +birds: [] +echo: cat + `) + + descriptions := map[string]string{ + "birds": "birds", + "echo": "echo", + } + + valuesRows, err := createValueRowsFromObject("", helmValues, descriptions, true) + + assert.Nil(t, err) + assert.Len(t, valuesRows, 2) + + assert.Equal(t, "birds", valuesRows[0].Key) + assert.Equal(t, listType, valuesRows[0].Type) + assert.Equal(t, "`[]`", valuesRows[0].Default) + assert.Equal(t, "birds", valuesRows[0].Description) + + assert.Equal(t, "echo", valuesRows[1].Key) + assert.Equal(t, stringType, valuesRows[1].Type) + assert.Equal(t, "`\"cat\"`", valuesRows[1].Default) + assert.Equal(t, "echo", valuesRows[1].Description) +} + +func TestListOfStrings(t *testing.T) { + helmValues := parseYamlValues(` +cats: [echo, foxtrot] + `) + + valuesRows, err := createValueRowsFromObject("", helmValues, make(map[string]string), true) + + assert.Nil(t, err) + assert.Len(t, valuesRows, 2) + + assert.Equal(t, "cats[0]", valuesRows[0].Key) + assert.Equal(t, stringType, valuesRows[0].Type) + assert.Equal(t, "`\"echo\"`", valuesRows[0].Default) + assert.Equal(t, "", valuesRows[0].Description) + + assert.Equal(t, "cats[1]", valuesRows[1].Key) + assert.Equal(t, stringType, valuesRows[1].Type) + assert.Equal(t, "`\"foxtrot\"`", valuesRows[1].Default) + assert.Equal(t, "", valuesRows[1].Description) + +} + +func TestListOfStringsWithDescriptions(t *testing.T) { + helmValues := parseYamlValues(` +cats: [echo, foxtrot] + `) + + descriptions := map[string]string{ + "cats[0]": "the black one", + "cats[1]": "the friendly one", + } + + valuesRows, err := createValueRowsFromObject("", helmValues, descriptions, true) + + assert.Nil(t, err) + assert.Len(t, valuesRows, 2) + + assert.Equal(t, "cats[0]", valuesRows[0].Key) + assert.Equal(t, stringType, valuesRows[0].Type) + assert.Equal(t, "`\"echo\"`", valuesRows[0].Default) + assert.Equal(t, "the black one", valuesRows[0].Description) + + assert.Equal(t, "cats[1]", valuesRows[1].Key) + assert.Equal(t, stringType, valuesRows[1].Type) + assert.Equal(t, "`\"foxtrot\"`", valuesRows[1].Default) + assert.Equal(t, "the friendly one", valuesRows[1].Description) + +} + +func TestListOfObjects(t *testing.T) { + helmValues := parseYamlValues(` +animals: + - elements: [echo, foxtrot] + type: cat + - elements: [oscar] + type: dog + `) + + valuesRows, err := createValueRowsFromObject("", helmValues, make(map[string]string), true) + + assert.Nil(t, err) + assert.Len(t, valuesRows, 5) + + assert.Equal(t, "animals[0].elements[0]", valuesRows[0].Key) + assert.Equal(t, stringType, valuesRows[0].Type) + assert.Equal(t, "`\"echo\"`", valuesRows[0].Default) + assert.Equal(t, "", valuesRows[0].Description) + + assert.Equal(t, "animals[0].elements[1]", valuesRows[1].Key) + assert.Equal(t, stringType, valuesRows[1].Type) + assert.Equal(t, "`\"foxtrot\"`", valuesRows[1].Default) + assert.Equal(t, "", valuesRows[1].Description) + + assert.Equal(t, "animals[0].type", valuesRows[2].Key) + assert.Equal(t, stringType, valuesRows[2].Type) + assert.Equal(t, "`\"cat\"`", valuesRows[2].Default) + assert.Equal(t, "", valuesRows[2].Description) + + assert.Equal(t, "animals[1].elements[0]", valuesRows[3].Key) + assert.Equal(t, stringType, valuesRows[3].Type) + assert.Equal(t, "`\"oscar\"`", valuesRows[3].Default) + assert.Equal(t, "", valuesRows[3].Description) + + assert.Equal(t, "animals[1].type", valuesRows[4].Key) + assert.Equal(t, stringType, valuesRows[4].Type) + assert.Equal(t, "`\"dog\"`", valuesRows[4].Default) + assert.Equal(t, "", valuesRows[4].Description) +} + +func TestListOfObjectsWithDescriptions(t *testing.T) { + helmValues := parseYamlValues(` +animals: + - elements: [echo, foxtrot] + type: cat + - elements: [oscar] + type: dog + `) + + descriptions := map[string]string{ + "animals[0].elements[0]": "the black one", + "animals[0].elements[1]": "the friendly one", + "animals[1].elements[0]": "the sleepy one", + } + + valuesRows, err := createValueRowsFromObject("", helmValues, descriptions, true) + + assert.Nil(t, err) + assert.Len(t, valuesRows, 5) + + assert.Equal(t, "animals[0].elements[0]", valuesRows[0].Key) + assert.Equal(t, stringType, valuesRows[0].Type) + assert.Equal(t, "`\"echo\"`", valuesRows[0].Default) + assert.Equal(t, "the black one", valuesRows[0].Description) + + assert.Equal(t, "animals[0].elements[1]", valuesRows[1].Key) + assert.Equal(t, stringType, valuesRows[1].Type) + assert.Equal(t, "`\"foxtrot\"`", valuesRows[1].Default) + assert.Equal(t, "the friendly one", valuesRows[1].Description) + + assert.Equal(t, "animals[0].type", valuesRows[2].Key) + assert.Equal(t, stringType, valuesRows[2].Type) + assert.Equal(t, "`\"cat\"`", valuesRows[2].Default) + assert.Equal(t, "", valuesRows[2].Description) + + assert.Equal(t, "animals[1].elements[0]", valuesRows[3].Key) + assert.Equal(t, stringType, valuesRows[3].Type) + assert.Equal(t, "`\"oscar\"`", valuesRows[3].Default) + assert.Equal(t, "the sleepy one", valuesRows[3].Description) + + assert.Equal(t, "animals[1].type", valuesRows[4].Key) + assert.Equal(t, stringType, valuesRows[4].Type) + assert.Equal(t, "`\"dog\"`", valuesRows[4].Default) + assert.Equal(t, "", valuesRows[4].Description) +} + +func TestDescriptionOnList(t *testing.T) { + helmValues := parseYamlValues(` +animals: + - elements: [echo, foxtrot] + type: cat + - elements: [oscar] + type: dog + `) + + descriptions := map[string]string{ + "animals": "all the animals of the house", + } + + valuesRows, err := createValueRowsFromObject("", helmValues, descriptions, true) + + assert.Nil(t, err) + assert.Len(t, valuesRows, 1) + + assert.Equal(t, "animals", valuesRows[0].Key) + assert.Equal(t, listType, valuesRows[0].Type) + assert.Equal(t, "`[{\"elements\":[\"echo\",\"foxtrot\"],\"type\":\"cat\"},{\"elements\":[\"oscar\"],\"type\":\"dog\"}]`", valuesRows[0].Default) + assert.Equal(t, "all the animals of the house", valuesRows[0].Description) +} + +func TestDescriptionOnObjectUnderList(t *testing.T) { + helmValues := parseYamlValues(` +animals: + - elements: [echo, foxtrot] + type: cat + - elements: [oscar] + type: dog + `) + + descriptions := map[string]string{ + "animals[0]": "all the cats of the house", + } + + valuesRows, err := createValueRowsFromObject("", helmValues, descriptions, true) + + assert.Nil(t, err) + assert.Len(t, valuesRows, 3) + + assert.Equal(t, "animals[0]", valuesRows[0].Key) + assert.Equal(t, objectType, valuesRows[0].Type) + assert.Equal(t, "`{\"elements\":[\"echo\",\"foxtrot\"],\"type\":\"cat\"}`", valuesRows[0].Default) + assert.Equal(t, "all the cats of the house", valuesRows[0].Description) + + assert.Equal(t, "animals[1].elements[0]", valuesRows[1].Key) + assert.Equal(t, stringType, valuesRows[1].Type) + assert.Equal(t, "`\"oscar\"`", valuesRows[1].Default) + assert.Equal(t, "", valuesRows[1].Description) + + assert.Equal(t, "animals[1].type", valuesRows[2].Key) + assert.Equal(t, stringType, valuesRows[2].Type) + assert.Equal(t, "`\"dog\"`", valuesRows[2].Default) + assert.Equal(t, "", valuesRows[2].Description) +} + +func TestDescriptionOnObjectUnderObject(t *testing.T) { + helmValues := parseYamlValues(` +animals: + byTrait: + friendly: [foxtrot, oscar] + mean: [echo] + sleepy: [oscar] + `) + + descriptions := map[string]string{ + "animals.byTrait": "animals listed by their various characteristics", + } + + valuesRows, err := createValueRowsFromObject("", helmValues, descriptions, true) + + assert.Nil(t, err) + assert.Len(t, valuesRows, 1) + + assert.Equal(t, "animals.byTrait", valuesRows[0].Key) + assert.Equal(t, objectType, valuesRows[0].Type) + assert.Equal(t, "`{\"friendly\":[\"foxtrot\",\"oscar\"],\"mean\":[\"echo\"],\"sleepy\":[\"oscar\"]}`", valuesRows[0].Default) + assert.Equal(t, "animals listed by their various characteristics", valuesRows[0].Description) +} + +func TestDescriptionsDownChain(t *testing.T) { + helmValues := parseYamlValues(` +animals: + byTrait: + friendly: [foxtrot, oscar] + mean: [echo] + sleepy: [oscar] + `) + + descriptions := map[string]string{ + "animals": "animal stuff", + "animals.byTrait": "animals listed by their various characteristics", + "animals.byTrait.friendly": "the friendly animals of the house", + "animals.byTrait.friendly[0]": "best cat ever", + } + + valuesRows, err := createValueRowsFromObject("", helmValues, descriptions, true) + + assert.Nil(t, err) + assert.Len(t, valuesRows, 4) + + assert.Equal(t, "animals", valuesRows[0].Key) + assert.Equal(t, objectType, valuesRows[0].Type) + assert.Equal(t, "`{\"byTrait\":{\"friendly\":[\"foxtrot\",\"oscar\"],\"mean\":[\"echo\"],\"sleepy\":[\"oscar\"]}}`", valuesRows[0].Default) + assert.Equal(t, "animal stuff", valuesRows[0].Description) + + assert.Equal(t, "animals.byTrait", valuesRows[1].Key) + assert.Equal(t, objectType, valuesRows[1].Type) + assert.Equal(t, "`{\"friendly\":[\"foxtrot\",\"oscar\"],\"mean\":[\"echo\"],\"sleepy\":[\"oscar\"]}`", valuesRows[1].Default) + assert.Equal(t, "animals listed by their various characteristics", valuesRows[1].Description) + + assert.Equal(t, "animals.byTrait.friendly", valuesRows[2].Key) + assert.Equal(t, listType, valuesRows[2].Type) + assert.Equal(t, "`[\"foxtrot\",\"oscar\"]`", valuesRows[2].Default) + assert.Equal(t, "the friendly animals of the house", valuesRows[2].Description) + + assert.Equal(t, "animals.byTrait.friendly[0]", valuesRows[3].Key) + assert.Equal(t, stringType, valuesRows[3].Type) + assert.Equal(t, "`\"foxtrot\"`", valuesRows[3].Default) + assert.Equal(t, "best cat ever", valuesRows[3].Description) +} + +func TestNilValues(t *testing.T) { + helmValues := parseYamlValues(` +animals: + birds: + birdCount: + nonWeirdCats: + `) + + descriptions := map[string]string{ + "animals.birdCount": "(int) the number of birds we have", + "animals.birds": "(list) the list of birds we have", + "animals.nonWeirdCats": "the cats that we have that are not weird", + } + + valuesRows, err := createValueRowsFromObject("", helmValues, descriptions, true) + + assert.Nil(t, err) + assert.Len(t, valuesRows, 3) + + assert.Equal(t, "animals.birdCount", valuesRows[0].Key) + assert.Equal(t, intType, valuesRows[0].Type) + assert.Equal(t, "`nil`", valuesRows[0].Default) + assert.Equal(t, "the number of birds we have", valuesRows[0].Description) + + assert.Equal(t, "animals.birds", valuesRows[1].Key) + assert.Equal(t, listType, valuesRows[1].Type) + assert.Equal(t, "`nil`", valuesRows[1].Default) + assert.Equal(t, "the list of birds we have", valuesRows[1].Description) + + assert.Equal(t, "animals.nonWeirdCats", valuesRows[2].Key) + assert.Equal(t, stringType, valuesRows[2].Type) + assert.Equal(t, "`nil`", valuesRows[2].Default) + assert.Equal(t, "the cats that we have that are not weird", valuesRows[2].Description) +} + +func TestKeysWithSpecialCharacters(t *testing.T) { + helmValues := parseYamlValues(` +websites: + stupidchess.jmn23.com: defunct +fullNames: + John Norwood: me +`) + + valuesRows, err := createValueRowsFromObject("", helmValues, make(map[string]string), true) + + assert.Nil(t, err) + assert.Len(t, valuesRows, 2) + + assert.Equal(t, `fullNames."John Norwood"`, valuesRows[0].Key) + assert.Equal(t, stringType, valuesRows[0].Type) + assert.Equal(t, "`\"me\"`", valuesRows[0].Default) + assert.Equal(t, "", valuesRows[0].Description) + + assert.Equal(t, `websites."stupidchess.jmn23.com"`, valuesRows[1].Key) + assert.Equal(t, stringType, valuesRows[1].Type) + assert.Equal(t, "`\"defunct\"`", valuesRows[1].Default) + assert.Equal(t, "", valuesRows[1].Description) +} + +func TestKeysWithSpecialCharactersWithDescriptions(t *testing.T) { + helmValues := parseYamlValues(` +websites: + stupidchess.jmn23.com: defunct +fullNames: + John Norwood: me +`) + + descriptions := map[string]string{ + `fullNames."John Norwood"`: "who am I", + `websites."stupidchess.jmn23.com"`: "status of the stupidchess website", + } + + valuesRows, err := createValueRowsFromObject("", helmValues, descriptions, true) + + assert.Nil(t, err) + assert.Len(t, valuesRows, 2) + + assert.Equal(t, `fullNames."John Norwood"`, valuesRows[0].Key) + assert.Equal(t, stringType, valuesRows[0].Type) + assert.Equal(t, "`\"me\"`", valuesRows[0].Default) + assert.Equal(t, "who am I", valuesRows[0].Description) + + assert.Equal(t, `websites."stupidchess.jmn23.com"`, valuesRows[1].Key) + assert.Equal(t, stringType, valuesRows[1].Type) + assert.Equal(t, "`\"defunct\"`", valuesRows[1].Default) + assert.Equal(t, "status of the stupidchess website", valuesRows[1].Description) +} + +func TestNonStringKeys(t *testing.T) { + helmValues := parseYamlValues(` +3: three +3.14159: pi +true: "true" +`) + + valuesRows, err := createValueRowsFromObject("", helmValues, make(map[string]string), true) + + assert.Nil(t, err) + assert.Len(t, valuesRows, 3) + + assert.Equal(t, `"float(3.141590)"`, valuesRows[0].Key) + assert.Equal(t, stringType, valuesRows[0].Type) + assert.Equal(t, "`\"pi\"`", valuesRows[0].Default) + assert.Equal(t, "", valuesRows[0].Description) + + assert.Equal(t, "bool(true)", valuesRows[1].Key) + assert.Equal(t, stringType, valuesRows[1].Type) + assert.Equal(t, "`\"true\"`", valuesRows[1].Default) + assert.Equal(t, "", valuesRows[1].Description) + + assert.Equal(t, "int(3)", valuesRows[2].Key) + assert.Equal(t, stringType, valuesRows[2].Type) + assert.Equal(t, "`\"three\"`", valuesRows[2].Default) + assert.Equal(t, "", valuesRows[2].Description) +} diff --git a/pkg/helm/chart_info.go b/pkg/helm/chart_info.go index b1908ed..9e37ff2 100644 --- a/pkg/helm/chart_info.go +++ b/pkg/helm/chart_info.go @@ -41,13 +41,11 @@ type ChartRequirements struct { Dependencies []ChartRequirementsItem } -type ChartValues map[interface{}]interface{} - type ChartDocumentationInfo struct { ChartMeta ChartRequirements - ChartValues + ChartValues map[interface{}]interface{} ChartDirectory string ChartValuesDescriptions map[string]string } @@ -127,9 +125,9 @@ func parseChartRequirementsFile(chartDirectory string) (ChartRequirements, error return chartRequirements, nil } -func parseChartValuesFile(chartDirectory string) (ChartValues, error) { +func parseChartValuesFile(chartDirectory string) (map[interface{}]interface{}, error) { valuesPath := path.Join(chartDirectory, "values.yaml") - values := ChartValues{} + values := make(map[interface{}]interface{}) yamlFileContents, err := getYamlFileContents(valuesPath) if isErrorInReadingNecessaryFile(valuesPath, err) {