diff --git a/Makefile b/Makefile index 8f3fda4..668cf9d 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,5 @@ helm-docs: - cd cmd/helm-docs && go build - mv cmd/helm-docs/helm-docs . + go build github.com/norwoodj/helm-docs/cmd/helm-docs .PHONY: fmt fmt: diff --git a/cmd/helm-docs/command_line.go b/cmd/helm-docs/command_line.go index cc950c8..f3a55db 100644 --- a/cmd/helm-docs/command_line.go +++ b/cmd/helm-docs/command_line.go @@ -5,6 +5,7 @@ import ( "os" "strings" + "github.com/norwoodj/helm-docs/pkg/document" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/spf13/viper" @@ -48,7 +49,13 @@ func newHelmDocsCommand(run func(cmd *cobra.Command, args []string)) (*cobra.Com command.PersistentFlags().StringP("ignore-file", "i", ".helmdocsignore", "The filename to use as an ignore file to exclude chart directories") command.PersistentFlags().StringP("log-level", "l", "info", logLevelUsage) command.PersistentFlags().StringP("output-file", "o", "README.md", "markdown file path relative to each chart directory to which rendered documentation will be written") +<<<<<<< HEAD + command.PersistentFlags().StringP("sort-values-order", "s", document.AlphaNumSortOrder, fmt.Sprintf("order in which to sort the values table (\"%s\" or \"%s\")", document.AlphaNumSortOrder, document.FileSortOrder)) command.PersistentFlags().StringSliceP("template-files", "t", []string{"README.md.gotmpl"}, "gotemplate file paths relative to each chart directory from which documentation will be generated") +======= + command.PersistentFlags().StringP("template-file", "t", "README.md.gotmpl", "gotemplate file path relative to each chart directory from which documentation will be generated") + command.PersistentFlags().StringP("sort-values-order", "s", "alpha", "order in which to print the values (alpha or file)") +>>>>>>> 18b313e... change flag viper.AutomaticEnv() viper.SetEnvPrefix("HELM_DOCS") diff --git a/pkg/document/model.go b/pkg/document/model.go index f14e0ea..e96e5f9 100644 --- a/pkg/document/model.go +++ b/pkg/document/model.go @@ -2,9 +2,12 @@ package document import ( "fmt" + "sort" "strconv" + log "github.com/sirupsen/logrus" "github.com/norwoodj/helm-docs/pkg/helm" + "github.com/spf13/viper" "gopkg.in/yaml.v3" ) @@ -15,6 +18,8 @@ type valueRow struct { Default string AutoDescription string Description string + Column int + LineNumber int } type chartTemplateData struct { @@ -40,7 +45,7 @@ func getChartTemplateData(chartDocumentationInfo helm.ChartDocumentationInfo, he return chartTemplateData{}, fmt.Errorf("values file must resolve to a map, not %s", strconv.Itoa(int(chartDocumentationInfo.ChartValues.Kind))) } - valuesTableRows, err := createValueRowsFromObject( + valuesTableRows, err := createValueRowsFromField( "", nil, chartDocumentationInfo.ChartValues.Content[0], @@ -48,6 +53,28 @@ func getChartTemplateData(chartDocumentationInfo helm.ChartDocumentationInfo, he true, ) + sortOrder := viper.GetString("sort-values-order") + if sortOrder == FileSortOrder { + sort.Slice(valuesTableRows[:], func(i, j int) bool { + if valuesTableRows[i].LineNumber < valuesTableRows[j].LineNumber { + return true + } else if valuesTableRows[i].Column < valuesTableRows[j].Column { + return true + } + + return false + }) + } else if sortOrder == AlphaNumSortOrder { + sort.Slice(valuesTableRows[:], func(i, j int) bool { + return valuesTableRows[i].Key < valuesTableRows[j].Key + }) + } else { + log.Warnf("Invalid sort order provided %s, defaulting to %s", sortOrder, AlphaNumSortOrder) + sort.Slice(valuesTableRows[:], func(i, j int) bool { + return valuesTableRows[i].Key < valuesTableRows[j].Key + }) + } + if err != nil { return chartTemplateData{}, err } diff --git a/pkg/document/util.go b/pkg/document/util.go index c8503eb..f5b62ce 100644 --- a/pkg/document/util.go +++ b/pkg/document/util.go @@ -5,14 +5,17 @@ import ( "gopkg.in/yaml.v3" ) -type jsonableMap map[string]interface{} +const ( + AlphaNumSortOrder = "alphanum" + FileSortOrder = "file" +) // 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 *yaml.Node) interface{} { switch values.Kind { case yaml.MappingNode: - convertedMap := make(jsonableMap) + convertedMap := make(map[string]interface{}) for i := 0; i < len(values.Content); i += 2 { k := values.Content[i] diff --git a/pkg/document/values.go b/pkg/document/values.go index 863d3d4..74dd4c0 100644 --- a/pkg/document/values.go +++ b/pkg/document/values.go @@ -5,7 +5,6 @@ import ( "encoding/json" "fmt" "regexp" - "sort" "strings" "github.com/norwoodj/helm-docs/pkg/helm" @@ -69,14 +68,14 @@ func getTypeName(value interface{}) string { return stringType case []interface{}: return listType - case jsonableMap: + case map[string]interface{}: return objectType } return "" } -func parseNilValueType(key string, description helm.ChartValueDescription, autoDescription helm.ChartValueDescription) valueRow { +func parseNilValueType(key string, description helm.ChartValueDescription, autoDescription helm.ChartValueDescription, column int, lineNumber int) valueRow { if len(description.Description) == 0 { description.Description = autoDescription.Description } @@ -101,6 +100,8 @@ func parseNilValueType(key string, description helm.ChartValueDescription, autoD Default: description.Default, AutoDescription: autoDescription.Description, Description: description.Description, + Column: column, + LineNumber: lineNumber, } } @@ -117,14 +118,40 @@ func jsonMarshalNoEscape(key string, value interface{}) (string, error) { return strings.TrimRight(outputBuffer.String(), "\n"), nil } +func getDescriptionFromNode(node *yaml.Node) helm.ChartValueDescription { + if node == nil { + return helm.ChartValueDescription{} + } + + if node.HeadComment == "" { + return helm.ChartValueDescription{} + } + + commentLines := strings.Split(node.HeadComment, "\n") + match := autoDocCommentRegex.FindStringSubmatch(commentLines[0]) + + if len(match) < 2 { + return helm.ChartValueDescription{} + } + + keyFromComment, c := helm.ParseComment(commentLines) + if keyFromComment != "" { + return helm.ChartValueDescription{} + } + + return c +} + func createValueRow( key string, value interface{}, description helm.ChartValueDescription, autoDescription helm.ChartValueDescription, + column int, + lineNumber int, ) (valueRow, error) { if value == nil { - return parseNilValueType(key, description, autoDescription), nil + return parseNilValueType(key, description, autoDescription, column, lineNumber), nil } autoDefaultValue := autoDescription.Default @@ -145,94 +172,11 @@ func createValueRow( Default: defaultValue, AutoDescription: autoDescription.Description, Description: description.Description, + Column: column, + LineNumber: lineNumber, }, nil } -func createValueRowsFromField( - nextPrefix string, - key *yaml.Node, - value *yaml.Node, - keysToDescriptions map[string]helm.ChartValueDescription, - documentLeafNodes bool, -) ([]valueRow, error) { - switch value.Kind { - case yaml.MappingNode: - return createValueRowsFromObject(nextPrefix, key, value, keysToDescriptions, documentLeafNodes) - case yaml.SequenceNode: - return createValueRowsFromList(nextPrefix, key, value, keysToDescriptions, documentLeafNodes) - case yaml.AliasNode: - return createValueRowsFromField(nextPrefix, key, value.Alias, keysToDescriptions, documentLeafNodes) - case yaml.ScalarNode: - autoDescription := getDescriptionFromNode(key) - description, hasDescription := keysToDescriptions[nextPrefix] - if !(documentLeafNodes || hasDescription || autoDescription.Description != "") { - return []valueRow{}, nil - } - - switch value.Tag { - case nullTag: - leafValueRow, err := createValueRow(nextPrefix, nil, description, autoDescription) - return []valueRow{leafValueRow}, err - case strTag: - fallthrough - case timestampTag: - leafValueRow, err := createValueRow(nextPrefix, value.Value, description, autoDescription) - return []valueRow{leafValueRow}, err - case intTag: - var decodedValue int - err := value.Decode(&decodedValue) - if err != nil { - return []valueRow{}, err - } - - leafValueRow, err := createValueRow(nextPrefix, decodedValue, description, autoDescription) - return []valueRow{leafValueRow}, err - case floatTag: - var decodedValue float64 - err := value.Decode(&decodedValue) - if err != nil { - return []valueRow{}, err - } - leafValueRow, err := createValueRow(nextPrefix, decodedValue, description, autoDescription) - return []valueRow{leafValueRow}, err - - case boolTag: - var decodedValue bool - err := value.Decode(&decodedValue) - if err != nil { - return []valueRow{}, err - } - leafValueRow, err := createValueRow(nextPrefix, decodedValue, description, autoDescription) - return []valueRow{leafValueRow}, err - } - } - - return []valueRow{}, fmt.Errorf("invalid node type %d received", value.Kind) -} - -func getDescriptionFromNode(node *yaml.Node) helm.ChartValueDescription { - if node == nil { - return helm.ChartValueDescription{} - } - - if node.HeadComment == "" { - return helm.ChartValueDescription{} - } - - commentLines := strings.Split(node.HeadComment, "\n") - match := autoDocCommentRegex.FindStringSubmatch(commentLines[0]) - if len(match) < 2 { - return helm.ChartValueDescription{} - } - - keyFromComment, c := helm.ParseComment(commentLines) - if keyFromComment != "" { - return helm.ChartValueDescription{} - } - - return c -} - func createValueRowsFromList( prefix string, key *yaml.Node, @@ -250,7 +194,7 @@ func createValueRowsFromList( return []valueRow{}, nil } - emptyListRow, err := createValueRow(prefix, make([]interface{}, 0), description, autoDescription) + emptyListRow, err := createValueRow(prefix, make([]interface{}, 0), description, autoDescription, key.Column, key.Line) if err != nil { return nil, err } @@ -264,7 +208,7 @@ func createValueRowsFromList( // documented without descriptions if hasDescription || autoDescription.Description != "" { jsonableObject := convertHelmValuesToJsonable(values) - listRow, err := createValueRow(prefix, jsonableObject, description, autoDescription) + listRow, err := createValueRow(prefix, jsonableObject, description, autoDescription, key.Column, key.Line) if err != nil { return nil, err @@ -290,18 +234,18 @@ func createValueRowsFromList( } func createValueRowsFromObject( - prefix string, + nextPrefix string, key *yaml.Node, values *yaml.Node, keysToDescriptions map[string]helm.ChartValueDescription, documentLeafNodes bool, ) ([]valueRow, error) { - description, hasDescription := keysToDescriptions[prefix] + description, hasDescription := keysToDescriptions[nextPrefix] autoDescription := getDescriptionFromNode(key) if len(values.Content) == 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 == "" { + if nextPrefix == "" { return []valueRow{}, nil } @@ -311,7 +255,7 @@ func createValueRowsFromObject( return []valueRow{}, nil } - documentedRow, err := createValueRow(prefix, jsonableMap{}, description, autoDescription) + documentedRow, err := createValueRow(nextPrefix, make(map[string]interface{}), description, autoDescription, key.Column, key.Line) return []valueRow{documentedRow}, err } @@ -321,7 +265,7 @@ func createValueRowsFromObject( // documented without descriptions if hasDescription || autoDescription.Description != "" { jsonableObject := convertHelmValuesToJsonable(values) - objectRow, err := createValueRow(prefix, jsonableObject, description, autoDescription) + objectRow, err := createValueRow(nextPrefix, jsonableObject, description, autoDescription, key.Column, key.Line) if err != nil { return nil, err @@ -334,7 +278,7 @@ func createValueRowsFromObject( for i := 0; i < len(values.Content); i += 2 { k := values.Content[i] v := values.Content[i+1] - nextPrefix := formatNextObjectKeyPrefix(prefix, k.Value) + nextPrefix := formatNextObjectKeyPrefix(nextPrefix, k.Value) valueRowsForObjectField, err := createValueRowsFromField(nextPrefix, k, v, keysToDescriptions, documentLeafNodes) if err != nil { @@ -344,12 +288,67 @@ func createValueRowsFromObject( valueRows = append(valueRows, valueRowsForObjectField...) } - // 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, nil +} + +func createValueRowsFromField( + prefix string, + key *yaml.Node, + value *yaml.Node, + keysToDescriptions map[string]helm.ChartValueDescription, + documentLeafNodes bool, +) ([]valueRow, error) { + switch value.Kind { + case yaml.MappingNode: + return createValueRowsFromObject(prefix, key, value, keysToDescriptions, documentLeafNodes) + case yaml.SequenceNode: + return createValueRowsFromList(prefix, key, value, keysToDescriptions, documentLeafNodes) + case yaml.AliasNode: + return createValueRowsFromField(prefix, key, value.Alias, keysToDescriptions, documentLeafNodes) + case yaml.ScalarNode: + autoDescription := getDescriptionFromNode(key) + description, hasDescription := keysToDescriptions[prefix] + if !(documentLeafNodes || hasDescription || autoDescription.Description != "") { + return []valueRow{}, nil + } + + switch value.Tag { + case nullTag: + leafValueRow, err := createValueRow(prefix, nil, description, autoDescription, key.Column, key.Line) + return []valueRow{leafValueRow}, err + case strTag: + fallthrough + case timestampTag: + leafValueRow, err := createValueRow(prefix, value.Value, description, autoDescription, key.Column, key.Line) + return []valueRow{leafValueRow}, err + case intTag: + var decodedValue int + err := value.Decode(&decodedValue) + if err != nil { + return []valueRow{}, err + } + + leafValueRow, err := createValueRow(prefix, decodedValue, description, autoDescription, key.Column, key.Line) + return []valueRow{leafValueRow}, err + case floatTag: + var decodedValue float64 + err := value.Decode(&decodedValue) + if err != nil { + return []valueRow{}, err + } + leafValueRow, err := createValueRow(prefix, decodedValue, description, autoDescription, key.Column, key.Line) + return []valueRow{leafValueRow}, err + + case boolTag: + var decodedValue bool + err := value.Decode(&decodedValue) + if err != nil { + return []valueRow{}, err + } + leafValueRow, err := createValueRow(prefix, decodedValue, description, autoDescription, key.Column, key.Line) + return []valueRow{leafValueRow}, err + } } - return valueRows, nil + return []valueRow{}, fmt.Errorf("invalid node type %d received", value.Kind) } diff --git a/pkg/helm/chart_info.go b/pkg/helm/chart_info.go index 9fef30a..fdeeaa9 100644 --- a/pkg/helm/chart_info.go +++ b/pkg/helm/chart_info.go @@ -49,6 +49,7 @@ type ChartRequirements struct { } type ChartValueDescription struct { + Order int Description string Default string }