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/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/nginx-ingress/values.yaml b/example-charts/nginx-ingress/values.yaml index 7da3c04..6cbad49 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 -- Add 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/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..303ca7a 100644 --- a/pkg/document/generate.go +++ b/pkg/document/generate.go @@ -38,14 +38,19 @@ func PrintDocumentation(chartDocumentationInfo helm.ChartDocumentationInfo, dryR chartDocumentationTemplate, err := newChartDocumentationTemplate(chartDocumentationInfo) if err != nil { - log.Warnf("Error generating templates for chart %s: %s", chartDocumentationInfo.ChartDirectory, err) + log.Warnf("Error generating gotemplates for chart %s: %s", chartDocumentationInfo.ChartDirectory, err) return } - chartTemplateDataObject := getChartTemplateData(chartDocumentationInfo) - err = chartDocumentationTemplate.Execute(outputFile, chartTemplateDataObject) + chartTemplateDataObject, err := getChartTemplateData(chartDocumentationInfo) + if err != nil { + log.Warnf("Error generating template data for chart %s: %s", chartDocumentationInfo.ChartDirectory, err) + return + } + err = chartDocumentationTemplate.Execute(outputFile, chartTemplateDataObject) if err != nil { log.Warnf("Error generating documentation for chart %s: %s", chartDocumentationInfo.ChartDirectory, err) + return } } diff --git a/pkg/document/model.go b/pkg/document/model.go index a03f591..391da0b 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.ChartValuesObject, + 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..633eb58 --- /dev/null +++ b/pkg/document/util.go @@ -0,0 +1,50 @@ +package document + +import ( + "fmt" + "github.com/norwoodj/helm-docs/pkg/helm" +) + +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 helm.ChartValuesObject: + convertedMap := make(jsonableMap) + + for key, value := range values.(helm.ChartValuesObject) { + convertedMap[convertMapKeyToString(key)] = convertHelmValuesToJsonable(value) + } + + return convertedMap + + case helm.ChartValuesList: + convertedList := make(helm.ChartValuesList, 0) + + for _, value := range values.(helm.ChartValuesList) { + convertedList = append(convertedList, convertHelmValuesToJsonable(value)) + } + + return convertedList + + default: + return values + } +} diff --git a/pkg/document/values.go b/pkg/document/values.go index ffdccea..02832e1 100644 --- a/pkg/document/values.go +++ b/pkg/document/values.go @@ -1,10 +1,10 @@ package document import ( + "encoding/json" "fmt" "regexp" "sort" - "strconv" "strings" "github.com/norwoodj/helm-docs/pkg/helm" @@ -19,60 +19,49 @@ const ( stringType = "string" ) -func createAtomRow(prefix string, value interface{}, keysToDescriptions map[string]string) valueRow { - description := keysToDescriptions[prefix] +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, ".") { + 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, - } - 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 stringType + case helm.ChartValuesList: + 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) @@ -85,97 +74,194 @@ func parseNilValueType(prefix string, description string) valueRow { } return valueRow{ - Key: prefix, + Key: key, Type: t, Default: "\\", 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 createSubListOrObjectRows( + nextPrefix string, + value interface{}, + keysToDescriptions map[string]string, + documentLeafNodes bool, +) ([]valueRow, error) { + valueRows := make([]valueRow, 0) + + switch value.(type) { + case helm.ChartValuesObject: + subObjectValuesRows, err := createValueRowsFromObject(nextPrefix, value.(helm.ChartValuesObject), keysToDescriptions, documentLeafNodes) + if err != nil { + return nil, err + } + + valueRows = append(valueRows, subObjectValuesRows...) + + case helm.ChartValuesList: + subListValuesRows, err := createValueRowsFromList(nextPrefix, value.(helm.ChartValuesList), 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 helm.ChartValuesList, + 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) + + // 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 { + listRow, err := createValueRow(prefix, values, description) - for i, v := range values { - var nextPrefix string - if prefix != "" { - nextPrefix = fmt.Sprintf("%s[%d]", prefix, i) - } else { - nextPrefix = fmt.Sprintf("[%d]", i) + 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 := createSubListOrObjectRows(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 helm.ChartValuesObject, + 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, k.(string)) + valueRowsForObjectField, err := createSubListOrObjectRows(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 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..87d77a5 --- /dev/null +++ b/pkg/document/values_test.go @@ -0,0 +1,381 @@ +package document + +import ( + "github.com/norwoodj/helm-docs/pkg/helm" + "golang.org/x/net/html/atom" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestEmptyValues(t *testing.T) { + valuesRows, err := createValueRowsFromObject("", make(helm.ChartValuesObject), make(map[string]string), true) + assert.Nil(t, err) + assert.Len(t, valuesRows, 0) +} + +func TestSimpleValues(t *testing.T) { + helmValues := helm.ChartValuesObject{ + "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, valuesRows[0].Key, "echo") + assert.Equal(t, valuesRows[0].Type, intType) + assert.Equal(t, valuesRows[0].Default, "`0`") + assert.Equal(t, valuesRows[0].Description, "") + + assert.Equal(t, valuesRows[1].Key, "foxtrot") + assert.Equal(t, valuesRows[1].Type, boolType) + assert.Equal(t, valuesRows[1].Default, "`true`") + assert.Equal(t, valuesRows[1].Description, "") + + assert.Equal(t, valuesRows[2].Key, "hello") + assert.Equal(t, valuesRows[2].Type, stringType) + assert.Equal(t, valuesRows[2].Default, "`\"world\"`") + assert.Equal(t, valuesRows[2].Description, "") + + assert.Equal(t, valuesRows[3].Key, "oscar") + assert.Equal(t, valuesRows[3].Type, floatType) + assert.Equal(t, valuesRows[3].Default, "`3.14159`") + assert.Equal(t, valuesRows[3].Description, "") +} + +func TestSimpleValuesWithDescriptions(t *testing.T) { + helmValues := helm.ChartValuesObject{ + "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, valuesRows[0].Key, "echo") + assert.Equal(t, valuesRows[0].Type, intType) + assert.Equal(t, valuesRows[0].Default, "`0`") + assert.Equal(t, valuesRows[0].Description, "echo") + + assert.Equal(t, valuesRows[1].Key, "foxtrot") + assert.Equal(t, valuesRows[1].Type, boolType) + assert.Equal(t, valuesRows[1].Default, "`true`") + assert.Equal(t, valuesRows[1].Description, "foxtrot") + + assert.Equal(t, valuesRows[2].Key, "hello") + assert.Equal(t, valuesRows[2].Type, stringType) + assert.Equal(t, valuesRows[2].Default, "`\"world\"`") + assert.Equal(t, valuesRows[2].Description, "hello") + + assert.Equal(t, valuesRows[3].Key, "oscar") + assert.Equal(t, valuesRows[3].Type, floatType) + assert.Equal(t, valuesRows[3].Default, "`3.14159`") + assert.Equal(t, valuesRows[3].Description, "oscar") +} + +func TestRecursiveValues(t *testing.T) { + helmValues := helm.ChartValuesObject{ + "recursive": helm.ChartValuesObject{ + "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, valuesRows[0].Key, "oscar") + assert.Equal(t, valuesRows[0].Type, stringType) + assert.Equal(t, valuesRows[0].Default, "`\"dog\"`") + assert.Equal(t, valuesRows[0].Description, "") + + assert.Equal(t, valuesRows[1].Key, "recursive.echo") + assert.Equal(t, valuesRows[1].Type, stringType) + assert.Equal(t, valuesRows[1].Default, "`\"cat\"`") + assert.Equal(t, valuesRows[1].Description, "") +} + +func TestRecursiveValuesWithDescriptions(t *testing.T) { + helmValues := helm.ChartValuesObject{ + "recursive": helm.ChartValuesObject{ + "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, valuesRows[0].Key, "oscar") + assert.Equal(t, valuesRows[0].Type, stringType) + assert.Equal(t, valuesRows[0].Default, "`\"dog\"`") + assert.Equal(t, valuesRows[0].Description, "oscar") + + assert.Equal(t, valuesRows[1].Key, "recursive.echo") + assert.Equal(t, valuesRows[1].Type, stringType) + assert.Equal(t, valuesRows[1].Default, "`\"cat\"`") + assert.Equal(t, valuesRows[1].Description, "echo") +} + +func TestEmptyObject(t *testing.T) { + helmValues := helm.ChartValuesObject{ + "recursive": helm.ChartValuesObject{}, + "oscar": "dog", + } + + valuesRows, err := createValueRowsFromObject("", helmValues, make(map[string]string), true) + + assert.Nil(t, err) + assert.Len(t, valuesRows, 2) + + assert.Equal(t, valuesRows[0].Key, "oscar") + assert.Equal(t, valuesRows[0].Type, stringType) + assert.Equal(t, valuesRows[0].Default, "`\"dog\"`") + assert.Equal(t, valuesRows[0].Description, "") + + assert.Equal(t, valuesRows[1].Key, "recursive") + assert.Equal(t, valuesRows[1].Type, objectType) + assert.Equal(t, valuesRows[1].Default, "`{}`") + assert.Equal(t, valuesRows[1].Description, "") +} + +func TestEmptyObjectWithDescription(t *testing.T) { + helmValues := helm.ChartValuesObject{ + "recursive": helm.ChartValuesObject{}, + "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, valuesRows[0].Key, "oscar") + assert.Equal(t, valuesRows[0].Type, stringType) + assert.Equal(t, valuesRows[0].Default, "`\"dog\"`") + assert.Equal(t, valuesRows[0].Description, "") + + assert.Equal(t, valuesRows[1].Key, "recursive") + assert.Equal(t, valuesRows[1].Type, objectType) + assert.Equal(t, valuesRows[1].Default, "`{}`") + assert.Equal(t, valuesRows[1].Description, "an empty object") +} + +func TestEmptyList(t *testing.T) { + helmValues := helm.ChartValuesObject{ + "birds": helm.ChartValuesList{}, + "echo": "cat", + } + + valuesRows, err := createValueRowsFromObject("", helmValues, make(map[string]string), true) + + assert.Nil(t, err) + assert.Len(t, valuesRows, 2) + + assert.Equal(t, valuesRows[0].Key, "birds") + assert.Equal(t, valuesRows[0].Type, listType) + assert.Equal(t, valuesRows[0].Default, "`[]`") + assert.Equal(t, valuesRows[0].Description, "") + + assert.Equal(t, valuesRows[1].Key, "echo") + assert.Equal(t, valuesRows[1].Type, stringType) + assert.Equal(t, valuesRows[1].Default, "`\"cat\"`") + assert.Equal(t, valuesRows[1].Description, "") +} + +func TestEmptyListWithDescriptions(t *testing.T) { + helmValues := helm.ChartValuesObject{ + "birds": helm.ChartValuesList{}, + "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, valuesRows[0].Key, "birds") + assert.Equal(t, valuesRows[0].Type, listType) + assert.Equal(t, valuesRows[0].Default, "`[]`") + assert.Equal(t, valuesRows[0].Description, "birds") + + assert.Equal(t, valuesRows[1].Key, "echo") + assert.Equal(t, valuesRows[1].Type, stringType) + assert.Equal(t, valuesRows[1].Default, "`\"cat\"`") + assert.Equal(t, valuesRows[1].Description, "echo") +} + +func TestListOfStrings(t *testing.T) { + helmValues := helm.ChartValuesObject{ + "cats": helm.ChartValuesList{"echo", "foxtrot"}, + } + + valuesRows, err := createValueRowsFromObject("", helmValues, make(map[string]string), true) + + assert.Nil(t, err) + assert.Len(t, valuesRows, 2) + + assert.Equal(t, valuesRows[0].Key, "cats[0]") + assert.Equal(t, valuesRows[0].Type, stringType) + assert.Equal(t, valuesRows[0].Default, "`\"echo\"`") + assert.Equal(t, valuesRows[0].Description, "") + + assert.Equal(t, valuesRows[1].Key, "cats[1]") + assert.Equal(t, valuesRows[1].Type, stringType) + assert.Equal(t, valuesRows[1].Default, "`\"foxtrot\"`") + assert.Equal(t, valuesRows[1].Description, "") + +} + +func TestListOfStringsWithDescriptions(t *testing.T) { + helmValues := helm.ChartValuesObject{ + "cats": helm.ChartValuesList{"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, valuesRows[0].Key, "cats[0]") + assert.Equal(t, valuesRows[0].Type, stringType) + assert.Equal(t, valuesRows[0].Default, "`\"echo\"`") + assert.Equal(t, valuesRows[0].Description, "the black one") + + assert.Equal(t, valuesRows[1].Key, "cats[1]") + assert.Equal(t, valuesRows[1].Type, stringType) + assert.Equal(t, valuesRows[1].Default, "`\"foxtrot\"`") + assert.Equal(t, valuesRows[1].Description, "the friendly one") + +} + +func TestListOfObjects(t *testing.T) { + helmValues := helm.ChartValuesObject{ + "animals": helm.ChartValuesList{ + helm.ChartValuesObject{ + "elements": helm.ChartValuesList{"echo", "foxtrot"}, + "type": "cat", + }, + helm.ChartValuesObject{ + "elements": helm.ChartValuesList{"oscar"}, + "type": "dog", + }, + }, + } + + valuesRows, err := createValueRowsFromObject("", helmValues, make(map[string]string), true) + + assert.Nil(t, err) + assert.Len(t, valuesRows, 5) + + assert.Equal(t, valuesRows[0].Key, "animals[0].elements[0]") + assert.Equal(t, valuesRows[0].Type, stringType) + assert.Equal(t, valuesRows[0].Default, "`\"echo\"`") + assert.Equal(t, valuesRows[0].Description, "") + + assert.Equal(t, valuesRows[1].Key, "animals[0].elements[1]") + assert.Equal(t, valuesRows[1].Type, stringType) + assert.Equal(t, valuesRows[1].Default, "`\"foxtrot\"`") + assert.Equal(t, valuesRows[1].Description, "") + + assert.Equal(t, valuesRows[2].Key, "animals[0].type") + assert.Equal(t, valuesRows[2].Type, stringType) + assert.Equal(t, valuesRows[2].Default, "`\"cat\"`") + assert.Equal(t, valuesRows[2].Description, "") + + assert.Equal(t, valuesRows[3].Key, "animals[1].elements[0]") + assert.Equal(t, valuesRows[3].Type, stringType) + assert.Equal(t, valuesRows[3].Default, "`\"oscar\"`") + assert.Equal(t, valuesRows[3].Description, "") + + assert.Equal(t, valuesRows[4].Key, "animals[1].type") + assert.Equal(t, valuesRows[4].Type, stringType) + assert.Equal(t, valuesRows[4].Default, "`\"dog\"`") + assert.Equal(t, valuesRows[4].Description, "") +} + +func TestListOfObjectsWithDescriptions(t *testing.T) { + helmValues := helm.ChartValuesObject{ + "animals": helm.ChartValuesList{ + helm.ChartValuesObject{ + "elements": helm.ChartValuesList{"echo", "foxtrot"}, + "type": "cat", + }, + helm.ChartValuesObject{ + "elements": helm.ChartValuesList{"oscar"}, + "type": "dog", + }, + }, + } + + descriptions := map[string]string{ + + } + + valuesRows, err := createValueRowsFromObject("", helmValues, make(map[string]string), true) + + assert.Nil(t, err) + assert.Len(t, valuesRows, 5) + + assert.Equal(t, valuesRows[0].Key, "animals[0].elements[0]") + assert.Equal(t, valuesRows[0].Type, stringType) + assert.Equal(t, valuesRows[0].Default, "`\"echo\"`") + assert.Equal(t, valuesRows[0].Description, "") + + assert.Equal(t, valuesRows[1].Key, "animals[0].elements[1]") + assert.Equal(t, valuesRows[1].Type, stringType) + assert.Equal(t, valuesRows[1].Default, "`\"foxtrot\"`") + assert.Equal(t, valuesRows[1].Description, "") + + assert.Equal(t, valuesRows[2].Key, "animals[0].type") + assert.Equal(t, valuesRows[2].Type, stringType) + assert.Equal(t, valuesRows[2].Default, "`\"cat\"`") + assert.Equal(t, valuesRows[2].Description, "") + + assert.Equal(t, valuesRows[3].Key, "animals[1].elements[0]") + assert.Equal(t, valuesRows[3].Type, stringType) + assert.Equal(t, valuesRows[3].Default, "`\"oscar\"`") + assert.Equal(t, valuesRows[3].Description, "") + + assert.Equal(t, valuesRows[4].Key, "animals[1].type") + assert.Equal(t, valuesRows[4].Type, stringType) + assert.Equal(t, valuesRows[4].Default, "`\"dog\"`") + assert.Equal(t, valuesRows[4].Description, "") +} diff --git a/pkg/helm/chart_info.go b/pkg/helm/chart_info.go index b1908ed..7b17d89 100644 --- a/pkg/helm/chart_info.go +++ b/pkg/helm/chart_info.go @@ -41,12 +41,13 @@ type ChartRequirements struct { Dependencies []ChartRequirementsItem } -type ChartValues map[interface{}]interface{} +type ChartValuesObject map[interface{}]interface{} +type ChartValuesList []interface{} type ChartDocumentationInfo struct { ChartMeta ChartRequirements - ChartValues + ChartValuesObject ChartDirectory string ChartValuesDescriptions map[string]string @@ -127,9 +128,9 @@ func parseChartRequirementsFile(chartDirectory string) (ChartRequirements, error return chartRequirements, nil } -func parseChartValuesFile(chartDirectory string) (ChartValues, error) { +func parseChartValuesFile(chartDirectory string) (ChartValuesObject, error) { valuesPath := path.Join(chartDirectory, "values.yaml") - values := ChartValues{} + values := ChartValuesObject{} yamlFileContents, err := getYamlFileContents(valuesPath) if isErrorInReadingNecessaryFile(valuesPath, err) { @@ -179,7 +180,7 @@ func ParseChartInformation(chartDirectory string) (ChartDocumentationInfo, error return chartDocInfo, err } - chartDocInfo.ChartValues, err = parseChartValuesFile(chartDirectory) + chartDocInfo.ChartValuesObject, err = parseChartValuesFile(chartDirectory) if err != nil { return chartDocInfo, err }