diff --git a/example-charts/sections/Chart.yaml b/example-charts/sections/Chart.yaml new file mode 100644 index 0000000..85b0f13 --- /dev/null +++ b/example-charts/sections/Chart.yaml @@ -0,0 +1,12 @@ +apiVersion: v2 +name: sections +version: "1.0.0" +type: application +appVersion: "13.0.0" +description: A chart for showing how to use sections +home: "https://github.com/norwoodj/helm-docs/tree/master/example-charts/sections" +maintainers: + - email: rohdeconstantin@gmail.com + name: Constantin Rohde +sources: ["https://github.com/norwoodj/helm-docs/tree/master/example-charts/sections"] +engine: gotpl \ No newline at end of file diff --git a/example-charts/sections/README.md b/example-charts/sections/README.md new file mode 100644 index 0000000..020843a --- /dev/null +++ b/example-charts/sections/README.md @@ -0,0 +1,179 @@ +# Sections + +This creates values, but sectioned into own section tables if a section comment is provided. + +## Values + +### Some Section + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| controller.extraVolumes[0].configMap.name | string | `"nginx-ingress-config"` | Uses the name of the configmap created by this chart | +| controller.persistentVolumeClaims | list | the chart will construct this list internally unless specified | List of persistent volume claims to create. | +| controller.podLabels | object | `{}` | The labels to be applied to instances of the controller pod | + +### Special Attention + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| controller.ingressClass | string | `"nginx"` | You can also specify value comments like this | +| controller.publishService | object | `{"enabled":false}` | This is a publishService | +| controller.replicas | int | `nil` | Number of nginx-ingress pods to load balance between | + +### Other Values + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| controller.extraVolumes[0].name | string | `"config-volume"` | | +| controller.image.repository | string | `"nginx-ingress-controller"` | | +| controller.image.tag | string | `"18.0831"` | | +| controller.name | string | `"controller"` | | +| 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"` | | + +## Values + +

Some Section

+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
KeyTypeDefaultDescription
controller.extraVolumes[0].configMap.namestring
+"nginx-ingress-config"
+
+
Uses the name of the configmap created by this chart
controller.persistentVolumeClaimslist
+the chart will construct this list internally unless specified
+
+
List of persistent volume claims to create.
controller.podLabelsobject
+{}
+
+
The labels to be applied to instances of the controller pod
+

Special Attention

+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
KeyTypeDefaultDescription
controller.ingressClassstring
+"nginx"
+
+
You can also specify value comments like this
controller.publishServiceobject
+{
+  "enabled": false
+}
+
+
This is a publishService
controller.replicasint
+null
+
+
Number of nginx-ingress pods to load balance between
+ +

Other Values

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
KeyTypeDefaultDescription
controller.extraVolumes[0].namestring
+"config-volume"
+
+
controller.image.repositorystring
+"nginx-ingress-controller"
+
+
controller.image.tagstring
+"18.0831"
+
+
controller.namestring
+"controller"
+
+
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.typestring
+"LoadBalancer"
+
+
diff --git a/example-charts/sections/README.md.gotmpl b/example-charts/sections/README.md.gotmpl new file mode 100644 index 0000000..3078ad2 --- /dev/null +++ b/example-charts/sections/README.md.gotmpl @@ -0,0 +1,7 @@ +# Sections + +This creates values, but sectioned into own section tables if a seciton comment is provided. + +{{ template "chart.valuesSection" . }} + +{{ template "chart.valuesSectionHtml" . }} diff --git a/example-charts/sections/values.yaml b/example-charts/sections/values.yaml new file mode 100644 index 0000000..1adb6e3 --- /dev/null +++ b/example-charts/sections/values.yaml @@ -0,0 +1,43 @@ +controller: + name: controller + image: + repository: nginx-ingress-controller + tag: "18.0831" + + # controller.persistentVolumeClaims -- List of persistent volume claims to create. + # @default -- the chart will construct this list internally unless specified + # @section -- Some Section + persistentVolumeClaims: [] + + extraVolumes: + - name: config-volume + configMap: + # controller.extraVolumes[0].configMap.name -- Uses the name of the configmap created by this chart + # @section -- Some Section + name: nginx-ingress-config + + # -- You can also specify value comments like this + # @section -- Special Attention + ingressClass: nginx + + + # controller.podLabels -- The labels to be applied to instances of the controller pod + # @section -- Some Section + podLabels: {} + + # controller.publishService -- This is a publishService + # @section -- Special Attention + publishService: + enabled: false + + # -- (int) Number of nginx-ingress pods to load balance between + # @raw + # @section -- Special Attention + replicas: + + service: + annotations: + # controller.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 + + type: LoadBalancer diff --git a/pkg/document/model.go b/pkg/document/model.go index 026cb6c..5a1ec12 100644 --- a/pkg/document/model.go +++ b/pkg/document/model.go @@ -20,6 +20,7 @@ type valueRow struct { Default string AutoDescription string Description string + Section string Column int LineNumber int Dependency string @@ -30,17 +31,21 @@ type chartTemplateData struct { helm.ChartDocumentationInfo HelmDocsVersion string Values []valueRow + Sections sections Files files } -func sortValueRows(valueRows []valueRow) { - sortOrder := viper.GetString("sort-values-order") +type sections struct { + DefaultSection section + Sections []section +} - if sortOrder != FileSortOrder && sortOrder != AlphaNumSortOrder { - log.Warnf("Invalid sort order provided %s, defaulting to %s", sortOrder, AlphaNumSortOrder) - sortOrder = AlphaNumSortOrder - } +type section struct { + SectionName string + SectionItems []valueRow +} +func sortValueRowsByOrder(valueRows []valueRow, sortOrder string) { sort.Slice(valueRows, func(i, j int) bool { // Globals sort above non-globals. if valueRows[i].IsGlobal != valueRows[j].IsGlobal { @@ -75,6 +80,32 @@ func sortValueRows(valueRows []valueRow) { }) } +func sortValueRows(valueRows []valueRow) { + sortOrder := viper.GetString("sort-values-order") + + if sortOrder != FileSortOrder && sortOrder != AlphaNumSortOrder { + log.Warnf("Invalid sort order provided %s, defaulting to %s", sortOrder, AlphaNumSortOrder) + sortOrder = AlphaNumSortOrder + } + + sortValueRowsByOrder(valueRows, sortOrder) +} + +func sortSectionedValueRows(sectionedValueRows sections) { + sortOrder := viper.GetString("sort-values-order") + + if sortOrder != FileSortOrder && sortOrder != AlphaNumSortOrder { + log.Warnf("Invalid sort order provided %s, defaulting to %s", sortOrder, AlphaNumSortOrder) + sortOrder = AlphaNumSortOrder + } + + sortValueRowsByOrder(sectionedValueRows.DefaultSection.SectionItems, sortOrder) + + for _, section := range sectionedValueRows.Sections { + sortValueRowsByOrder(section.SectionItems, sortOrder) + } +} + func getUnsortedValueRows(document *yaml.Node, descriptions map[string]helm.ChartValueDescription) ([]valueRow, error) { // Handle empty values file case. if document.Kind == 0 { @@ -92,6 +123,39 @@ func getUnsortedValueRows(document *yaml.Node, descriptions map[string]helm.Char return createValueRowsFromField("", nil, document.Content[0], descriptions, true) } +func getSectionedValueRows(valueRows []valueRow) sections { + var valueRowsSectionSorted sections + valueRowsSectionSorted.DefaultSection = section{ + SectionName: "Other Values", + SectionItems: []valueRow{}, + } + + for _, row := range valueRows { + if row.Section == "" { + valueRowsSectionSorted.DefaultSection.SectionItems = append(valueRowsSectionSorted.DefaultSection.SectionItems, row) + continue + } + + containsSection := false + for i, section := range valueRowsSectionSorted.Sections { + if section.SectionName == row.Section { + containsSection = true + valueRowsSectionSorted.Sections[i].SectionItems = append(valueRowsSectionSorted.Sections[i].SectionItems, row) + break + } + } + + if !containsSection { + valueRowsSectionSorted.Sections = append(valueRowsSectionSorted.Sections, section{ + SectionName: row.Section, + SectionItems: []valueRow{row}, + }) + } + } + + return valueRowsSectionSorted +} + func getChartTemplateData(info helm.ChartDocumentationInfo, helmDocsVersion string, dependencyValues []DependencyValues) (chartTemplateData, error) { valuesTableRows, err := getUnsortedValueRows(info.ChartValues, info.ChartValuesDescriptions) if err != nil { @@ -135,6 +199,8 @@ func getChartTemplateData(info helm.ChartDocumentationInfo, helmDocsVersion stri } sortValueRows(valuesTableRows) + valueRowsSectionSorted := getSectionedValueRows(valuesTableRows) + sortSectionedValueRows(valueRowsSectionSorted) files, err := getFiles(info.ChartDirectory) if err != nil { @@ -145,6 +211,7 @@ func getChartTemplateData(info helm.ChartDocumentationInfo, helmDocsVersion stri ChartDocumentationInfo: info, HelmDocsVersion: helmDocsVersion, Values: valuesTableRows, + Sections: valueRowsSectionSorted, Files: files, }, nil } diff --git a/pkg/document/template.go b/pkg/document/template.go index ee0dbd9..88f5e60 100644 --- a/pkg/document/template.go +++ b/pkg/document/template.go @@ -208,12 +208,35 @@ func getValuesTableTemplates() string { valuesSectionBuilder.WriteString(`{{ define "chart.valuesHeader" }}## Values{{ end }}`) valuesSectionBuilder.WriteString(`{{ define "chart.valuesTable" }}`) + valuesSectionBuilder.WriteString("{{ if .Sections.Sections }}") + valuesSectionBuilder.WriteString("{{ range .Sections.Sections }}") + valuesSectionBuilder.WriteString("\n") + valuesSectionBuilder.WriteString("\n### {{ .SectionName }}\n") + valuesSectionBuilder.WriteString("\n") + valuesSectionBuilder.WriteString("| Key | Type | Default | Description |\n") + valuesSectionBuilder.WriteString("|-----|------|---------|-------------|\n") + valuesSectionBuilder.WriteString(" {{- range .SectionItems }}") + valuesSectionBuilder.WriteString("\n| {{ .Key }} | {{ .Type }} | {{ if .Default }}{{ .Default }}{{ else }}{{ .AutoDefault }}{{ end }} | {{ if .Description }}{{ .Description }}{{ else }}{{ .AutoDescription }}{{ end }} |") + valuesSectionBuilder.WriteString(" {{- end }}") + valuesSectionBuilder.WriteString("{{- end }}") + valuesSectionBuilder.WriteString("{{ if .Sections.DefaultSection.SectionItems}}") + valuesSectionBuilder.WriteString("\n") + valuesSectionBuilder.WriteString("\n### {{ .Sections.DefaultSection.SectionName }}\n") + valuesSectionBuilder.WriteString("\n") + valuesSectionBuilder.WriteString("| Key | Type | Default | Description |\n") + valuesSectionBuilder.WriteString("|-----|------|---------|-------------|\n") + valuesSectionBuilder.WriteString(" {{- range .Sections.DefaultSection.SectionItems }}") + valuesSectionBuilder.WriteString("\n| {{ .Key }} | {{ .Type }} | {{ if .Default }}{{ .Default }}{{ else }}{{ .AutoDefault }}{{ end }} | {{ if .Description }}{{ .Description }}{{ else }}{{ .AutoDescription }}{{ end }} |") + valuesSectionBuilder.WriteString(" {{- end }}") + valuesSectionBuilder.WriteString("{{ end }}") + valuesSectionBuilder.WriteString("{{ else }}") valuesSectionBuilder.WriteString("| Key | Type | Default | Description |\n") valuesSectionBuilder.WriteString("|-----|------|---------|-------------|\n") valuesSectionBuilder.WriteString(" {{- range .Values }}") valuesSectionBuilder.WriteString("\n| {{ .Key }} | {{ .Type }} | {{ if .Default }}{{ .Default }}{{ else }}{{ .AutoDefault }}{{ end }} | {{ if .Description }}{{ .Description }}{{ else }}{{ .AutoDescription }}{{ end }} |") valuesSectionBuilder.WriteString(" {{- end }}") valuesSectionBuilder.WriteString("{{ end }}") + valuesSectionBuilder.WriteString("{{ end }}") valuesSectionBuilder.WriteString(`{{ define "chart.valuesSection" }}`) valuesSectionBuilder.WriteString("{{ if .Values }}") @@ -243,6 +266,50 @@ func getValuesTableTemplates() string { {{ end }} {{ define "chart.valuesTableHtml" }} +{{ if .Sections.Sections }} +{{- range .Sections.Sections }} +

{{- .SectionName }}

+ + + + + + + + + {{- range .SectionItems }} + + + + + + + {{- end }} + +
KeyTypeDefaultDescription
{{ .Key }}{{ .Type }}{{ template "chart.valueDefaultColumnRender" . }}{{ if .Description }}{{ .Description }}{{ else }}{{ .AutoDescription }}{{ end }}
+{{- end }} +{{ if .Sections.DefaultSection.SectionItems }} +

{{- .Sections.DefaultSection.SectionName }}

+ + + + + + + + + {{- range .Sections.DefaultSection.SectionItems }} + + + + + + + {{- end }} + +
KeyTypeDefaultDescription
{{ .Key }}{{ .Type }}{{ template "chart.valueDefaultColumnRender" . }}{{ if .Description }}{{ .Description }}{{ else }}{{ .AutoDescription }}{{ end }}
+{{ end }} +{{ else }} @@ -262,9 +329,10 @@ func getValuesTableTemplates() string {
Key
{{ end }} +{{ end }} {{ define "chart.valuesSectionHtml" }} -{{ if .Values }} +{{ if .Sections }} {{ template "chart.valuesHeader" . }} {{ template "chart.valuesTableHtml" . }} {{ end }} diff --git a/pkg/document/values.go b/pkg/document/values.go index 072d3ba..05c7436 100644 --- a/pkg/document/values.go +++ b/pkg/document/values.go @@ -93,6 +93,11 @@ func parseNilValueType(key string, description helm.ChartValueDescription, autoD description.Default = "`nil`" } + section := description.Section + if section == "" && autoDescription.Section != "" { + section = autoDescription.Section + } + return valueRow{ Key: key, Type: t, @@ -101,6 +106,7 @@ func parseNilValueType(key string, description helm.ChartValueDescription, autoD Default: description.Default, AutoDescription: autoDescription.Description, Description: description.Description, + Section: section, Column: column, LineNumber: lineNumber, } @@ -180,6 +186,11 @@ func createValueRow( defaultValue = fmt.Sprintf("%s", value) } + section := description.Section + if section == "" && autoDescription.Section != "" { + section = autoDescription.Section + } + return valueRow{ Key: key, Type: defaultType, @@ -188,6 +199,7 @@ func createValueRow( Default: defaultValue, AutoDescription: autoDescription.Description, Description: description.Description, + Section: section, Column: column, LineNumber: lineNumber, }, nil diff --git a/pkg/document/values_test.go b/pkg/document/values_test.go index 32265a8..fc7796a 100644 --- a/pkg/document/values_test.go +++ b/pkg/document/values_test.go @@ -1561,3 +1561,105 @@ owner: assert.Equal(t, "owner@home.org", valuesRows[1].Default) assert.Equal(t, "This has to be email address", valuesRows[1].AutoDescription) } + +func TestSection(t *testing.T) { + helmValues := parseYamlValues(` +animals: + # -- This describes a lion + # @section -- Feline Section + lion: + + # -- This describes a cat + # @section -- Feline Section + cat: +`) + + valuesRows, err := getSortedValuesTableRows(helmValues, make(map[string]helm.ChartValueDescription)) + + assert.Nil(t, err) + assert.Len(t, valuesRows, 2) + assert.Equal(t, "animals.cat", valuesRows[0].Key) + assert.Equal(t, "This describes a cat", valuesRows[0].AutoDescription) + assert.Equal(t, "Feline Section", valuesRows[0].Section) + assert.Equal(t, "animals.lion", valuesRows[1].Key) + assert.Equal(t, "This describes a lion", valuesRows[1].AutoDescription) + assert.Equal(t, "Feline Section", valuesRows[1].Section) +} + +func TestSectionWithAnnotations(t *testing.T) { + helmValues := parseYamlValues(` +animals: + # -- This describes a lion + # @default -- Rawr + # @section -- Feline Section + lion: + + # -- This describes a cat + # @raw + # -Rawr + # @section -- Feline Section + cat: + + # -- (int) This describes a leopard + # @section -- Feline Section + leopard: +`) + + valuesRows, err := getSortedValuesTableRows(helmValues, make(map[string]helm.ChartValueDescription)) + + assert.Nil(t, err) + assert.Len(t, valuesRows, 3) + assert.Equal(t, "animals.cat", valuesRows[0].Key) + assert.Equal(t, "This describes a cat\n-Rawr", valuesRows[0].AutoDescription) + assert.Equal(t, "Feline Section", valuesRows[0].Section) + assert.Equal(t, "animals.leopard", valuesRows[1].Key) + assert.Equal(t, "int", valuesRows[1].Type) + assert.Equal(t, "This describes a leopard", valuesRows[1].AutoDescription) + assert.Equal(t, "Feline Section", valuesRows[1].Section) + assert.Equal(t, "animals.lion", valuesRows[2].Key) + assert.Equal(t, "This describes a lion", valuesRows[2].AutoDescription) + assert.Equal(t, "Rawr", valuesRows[2].AutoDefault) + assert.Equal(t, "Feline Section", valuesRows[2].Section) +} + +func TestDifferentSections(t *testing.T) { + helmValues := parseYamlValues(` +animals: + # -- This describes a lion + # @default -- Rawr + # @section -- Feline Section + lion: + + # -- This describes a cow + # @raw + # - Moooe + # @section -- Cow Section + cow: + + # -- (int) This describes a leopard + # @section -- Feline Section + leopard: + + # -- This describes a cougar + # @section -- Feline Section + cougar: +`) + valuesRows, err := getSortedValuesTableRows(helmValues, make(map[string]helm.ChartValueDescription)) + + assert.Nil(t, err) + assert.Len(t, valuesRows, 4) + assert.Equal(t, "animals.cougar", valuesRows[0].Key) + assert.Equal(t, "This describes a cougar", valuesRows[0].AutoDescription) + assert.Equal(t, "Feline Section", valuesRows[0].Section) + assert.Equal(t, "animals.cow", valuesRows[1].Key) + assert.Equal(t, "This describes a cow\n- Moooe", valuesRows[1].AutoDescription) + assert.Equal(t, "Cow Section", valuesRows[1].Section) + assert.Equal(t, "animals.leopard", valuesRows[2].Key) + assert.Equal(t, "int", valuesRows[2].Type) + assert.Equal(t, "This describes a leopard", valuesRows[2].AutoDescription) + assert.Equal(t, "Feline Section", valuesRows[3].Section) + assert.Equal(t, "animals.lion", valuesRows[3].Key) + assert.Equal(t, "This describes a lion", valuesRows[3].AutoDescription) + assert.Equal(t, "Rawr", valuesRows[3].AutoDefault) + assert.Equal(t, "Feline Section", valuesRows[3].Section) +} diff --git a/pkg/helm/chart_info.go b/pkg/helm/chart_info.go index be9a489..7c4e2a7 100644 --- a/pkg/helm/chart_info.go +++ b/pkg/helm/chart_info.go @@ -21,6 +21,7 @@ var commentContinuationRegex = regexp.MustCompile("^\\s*#(\\s?)(.*)$") var defaultValueRegex = regexp.MustCompile("^\\s*# @default -- (.*)$") var valueTypeRegex = regexp.MustCompile("^\\((.*?)\\)\\s*(.*)$") var valueNotationTypeRegex = regexp.MustCompile("^\\s*#\\s+@notationType\\s+--\\s+(.*)$") +var sectionRegex = regexp.MustCompile("^\\s*# @section -- (.*)$") type ChartMetaMaintainer struct { Email string @@ -57,6 +58,7 @@ type ChartRequirements struct { type ChartValueDescription struct { Description string Default string + Section string ValueType string NotationType string } @@ -265,12 +267,18 @@ func parseChartValuesFileComments(chartDirectory string, values *yaml.Node, lint continue } - // If we've already found a values comment, on the next line try and parse a custom default value. If we find one - // that completes parsing for this key, add it to the list and reset to searching for a new key + // If we've already found a values comment, on the next line try and parse a comment continuation, a custom default value, or a section comment. + // If we find continuations we can add them to the list and continue to the next line until we find a section comment or default value. + // If we find a default value, we can add it to the list and continue to the next line. In the case we don't find one, we continue looking for a section comment. + // When we eventually find a section comment, we add it to the list and conclude matching for the current key. If we don't find one, matching is also concluded. + // + // NOTE: This isn't readily enforced yet, because we can match the section comment and custom default value more than once and in another order, although this is just overwriting it. + // Values comment, possible continuation, default value once or none then section comment once or none should be the preferred order. defaultCommentMatch := defaultValueRegex.FindStringSubmatch(currentLine) + sectionCommentMatch := sectionRegex.FindStringSubmatch(currentLine) commentContinuationMatch := commentContinuationRegex.FindStringSubmatch(currentLine) - if len(defaultCommentMatch) > 1 || len(commentContinuationMatch) > 1 { + if len(defaultCommentMatch) > 1 || len(sectionCommentMatch) > 1 || len(commentContinuationMatch) > 1 { commentLines = append(commentLines, currentLine) continue } diff --git a/pkg/helm/comment.go b/pkg/helm/comment.go index b8803c8..f8b17b4 100644 --- a/pkg/helm/comment.go +++ b/pkg/helm/comment.go @@ -50,6 +50,7 @@ func ParseComment(commentLines []string) (string, ChartValueDescription) { rawFlagMatch := rawDescriptionRegex.FindStringSubmatch(line) defaultCommentMatch := defaultValueRegex.FindStringSubmatch(line) notationTypeCommentMatch := valueNotationTypeRegex.FindStringSubmatch(line) + sectionCommentMatch := sectionRegex.FindStringSubmatch(line) if !isRaw && len(rawFlagMatch) == 1 { isRaw = true @@ -66,6 +67,11 @@ func ParseComment(commentLines []string) (string, ChartValueDescription) { continue } + if len(sectionCommentMatch) > 1 { + c.Section = sectionCommentMatch[1] + continue + } + commentContinuationMatch := commentContinuationRegex.FindStringSubmatch(line) if isRaw {