diff --git a/.gitignore b/.gitignore index e72bfb5..23d7330 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,5 @@ dist .idea/ *.iml +.vscode/ +vendor/ \ No newline at end of file diff --git a/Makefile b/Makefile index 668cf9d..45ccd76 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,9 @@ helm-docs: go build github.com/norwoodj/helm-docs/cmd/helm-docs +install: + go install github.com/norwoodj/helm-docs/cmd/helm-docs + .PHONY: fmt fmt: go fmt ./... diff --git a/README.md b/README.md index 94b6b91..f61b6ea 100644 --- a/README.md +++ b/README.md @@ -177,6 +177,9 @@ can be used as well: | chart.valuesHeader | The heading for the chart values section | | chart.valuesTable | A table of the chart's values parsed from the `values.yaml` file (see below) | | chart.valuesSection | A section headed by the valuesHeader from above containing the valuesTable from above or "" if there are no values | +| chart.valuesTableHtml | Like `chart.valuesTable` but it is rendered as (X)HTML tags to allow further rendering customization, instead of markdown tables format. | +| chart.valuesSectionHtml | Like `chart.valuesSection` but uses `chart.valuesTableHtml` | +| chart.valueDefaultColumnRender | This is a hook template if you want to redefine how helm-docs render the default values in `chart.valuesTableHtml` mode. This is especially useful when combined with (X)HTML tags, so that you can nicely format multiline default values, like YAML/JSON object tree snippet with codeblock syntax highlighter, which is not possible or difficult when using the markdown table format. It can be redefined in your template file. | The default internal template mentioned above uses many of these and looks like this: ``` @@ -333,3 +336,81 @@ configMap: # configMap."not real config param" -- A completely fake config parameter for a useful example not real config param: value ``` + +### Advanced table rendering +Some helm chart `values.yaml` uses complicated structure for the key/value +pairs. For example, it may uses a multiline string of Go template text instead +of plain strings. Some values might also refer to a certain YAML/JSON object +structure, like internal k8s value type, or an enum. For these use case, +a standard markdown table format might be inadequate and you want to use HTML +tags to render the table. + +Some example use case on why you need advanced table rendering: + + - Hyperlinking the value type to an anchor or HTML link somewhere for reference + - Collapsible value description using `` tags to save space + - Multiline default values as codeblocks, instead of one line JSON structure for readability + - Custom rendering, for colors, actions, bookmarking, cross-reference, etc + - Cascading the markdown file generated by helm-docs to be post-processed by Jamstack into a static HTML docs site. + +In order to accomodate this, `helm-docs` provides an extensible and flexible way to customize rendering. + +1. Use the HTML value renderer instead of the default markdown format + +You can use `chart.valuesSectionHtml` to render the values table as HTML tags, +instead of using `chart.valuesSection`. Using HTML tables provides more +flexibility because it can be processed by markdown viewer as a nested blocks, +instead of one row per line. This allows you to customize how each columns in a +row are rendered. + +2. Overriding built-in templates + +You can always overrides or redefine built-in templates in your own `_templates. +gotmpl` file. The built-in templates can be thought of as a template hook. +For example, if you need to change the HTML table, for example to add a new +column, or define maximum width/height, you can override `chart.valuesTableHtml`. Your overrides will then be called by `chart.valuesSectionHtml`. + +You can add your own rendering logic for each column. For example, we have `chart.valueDefaultColumnRender` that is used to render "default value" column for each rows. If you want to override how helm-docs render the +"type" column, just define your own rendering template and call it from +`chart.valuesTableHtml` for each of the rows. + +3. Using the metadata of each rows of values + +Custom styling and rendering can be done as flexible as you want, but you +still need a metadata that describes each rows of values. You can access +this information from the templates. + +When you override `chart.valuesTableHtml`, as you can see in the original +definition in `func getValuesTableTemplates()` [pkg/document/template.go](pkg/document/template.go), we iterates each row of values. +For each "Value", it is modeled as a struct defined in `valueRow` struct +in [pkg/document/model.go](pkg/document/model.go). You can then use the +fields in your template. + +Some fields here are directly referenced from `values.yaml`: +- `Key`: the full name of the key referenced in `values.yaml` +- `Type`: the type of the value of the key in `values.yaml`. Can be automatically inferred from YAML structure, or annotated using `# -- (mytype)` where `mytype` can be any string that you refer as the type of the value. +- `NotationType`: the notation of the type used to render the default value. If `Type` refers to the data type of the value, then `NotationType` refers to **how** this value should be written/rendered by helm-docs. Generally helm-docs only remembers the notation type, but it was the writer's responsibility to make a template tag to render a specific notation type. Annotate the key with `# @notationType -- (mynotation)` where `mynotation` is an identifier to tell the renderer how to write the value. +- `Default`: this is the default value of the key, found from `values.yaml`. It is either inferred from the YAML structure or defined using `# @default -- my default value` annotation, in case you need to show other example values. +- `Description`: this is the description of the key/value, taken from the comments found in the `values.yaml` for the referred key. +- `LineNumber`: this is the line number associated with where the key is declared. You can use this to construct an anchor to the actual `values.yaml` file. + +Note that helm-docs only provides these information, but the default behaviour is to always render it in plain Markdown file to be viewed locally. + +4. Use markdown files generated by helm-docs as intermediary files to be processed further + +Public helm charts sometimes needs to be published as static content +instead of just stored in a repository. This is needed for helm users to +be able to view or browse the chart options and dependencies. + +It is often more than enough to just browse the chart values options on +git hosting that is able to render markdown files as a nice HTML page, like GitHub or GitLab. +However, for a certain use case, you may want to use your own +documentation generator to host or publish the output of helm-docs. + +If you use some kind of Jamstack like Gatsby or Hugo, you can use the +output of helm-docs as an input for these doc generator. A typical use +case is to override helm-docs built-in template so that it renders a +markdown or markdownX files to be processed by Gatsby or Hugo into +a static Web/Javascript page. + +For a more concrete examples on how to do these custom rendering, see [example here](./example-charts/custom-value-notation-type/README.md) \ No newline at end of file diff --git a/example-charts/custom-value-notation-type/Chart.yaml b/example-charts/custom-value-notation-type/Chart.yaml new file mode 100644 index 0000000..ff6525b --- /dev/null +++ b/example-charts/custom-value-notation-type/Chart.yaml @@ -0,0 +1,27 @@ +apiVersion: v2 +name: django +version: 0.2.1 +appVersion: 3.1 +description: Generic chart for basic Django-based web app +keywords: + - Django + - Web +home: https://www.djangoproject.com/ +sources: + - https://github.com/django/django +maintainers: + - name: Rizky Maulana Nugraha + email: lana.pcfre@gmail.com +icon: https://raw.githubusercontent.com/kartoza/charts/master/assets/logo/django.png +engine: gotpl +dependencies: + - name: postgis + version: 0.2.1 + repository: "file://../../postgis/v0.2.1" + condition: postgis.enabled + tags: + - database-backend + - postgis + - name: common + version: 1.0.0 + repository: "file://../../common/v1.0.0" diff --git a/example-charts/custom-value-notation-type/README.md b/example-charts/custom-value-notation-type/README.md new file mode 100644 index 0000000..1dcff2c --- /dev/null +++ b/example-charts/custom-value-notation-type/README.md @@ -0,0 +1,1106 @@ +# django + +![Version: 0.2.1](https://img.shields.io/badge/Version-0.2.1-informational?style=flat-square) ![AppVersion: 3.1](https://img.shields.io/badge/AppVersion-3.1-informational?style=flat-square) + +Generic chart for basic Django-based web app + +**Homepage:** + +## Maintainers + +| Name | Email | Url | +| ---- | ------ | --- | +| Rizky Maulana Nugraha | | | + +## Source Code + +* + +## Requirements + +| Repository | Name | Version | +|------------|------|---------| +| file://../../common/v1.0.0 | common | 1.0.0 | +| file://../../postgis/v0.2.1 | postgis | 0.2.1 | + +# Some Long Description + +This is a sample README with custom overrides. +Check the template in [README.md.gotmpl](README.md.gotmpl). + +In that file, we redefine the template definition of `chart.valueDefaultColumnRender` +for some custom `@notationType` such as `string/email`. + +This chart README uses `chart.valuesSectionHtml` instead of `chart.valuesSection`. +Using HTML table directly instead of using Markdown table allows us to control the table +presentation, such as the height. This is especially useful for very long `values.yaml` file, +and you need to scroll both horizontally and vertically to navigate the values. + +In the template file, we redefine `chart.valuesTableHtml` so that we use table height of +400px at most. Github can understand that attribute. The more sophisticated use case is if you +want to combine helm-docs with a Jamstack static generator where you can have your own page generated +from this README. + +The customization can goes even further. Normally, you can't define anchor in markdown unless it is a heading. But you can do so easily using HTML tags. +You can override the column key renderer by adding an `id` attribute so that it can be referred. +This way, when you write markdown links like [ingress.tls.secretName](#ingress--tls--secretName), clicking the link +will take you to the value description row. + +## Value Types + +One of the benefit of using HTML table is we can make a simple tooltip and anchor. +For example, the value [global.adminEmail](#global--adminEmail) is annotated as type `string/email`. We create +the definition of the value type here and can be anchored by links with `#stringemail` hyperlinks. + +We can also create custom type column renderer, where we can assign a tooltip for each type. +Try this out. Go navigate to [global.adminEmail](#global--adminEmail) value, hover on the value type `string/email`, you will then see +some tooltip. Clicking the type link will direct you back to it's relevant value type section below. + +Other useful case is If the type is a known type, like +Kubernetes service type, you can anchor the type to redirect user to k8s documentation page to learn more. +Check the value [persistence.staticDir.accessModes](#persistence--staticDir--accessModes) + +### string/email + +This value type is for a valid email address format. Such as owner@somedomain.org. + +## Notation Type + +Another reason to use HTML table is because in some cases we want to custom-render the default value. + +In helm chart templates, sometimes author designs the template to accept a go template string value. +That means, the template string can be processed by helm chart and be replaced with dynamic computed values, before it was +rendered to the chart. Although it is very useful and flexible to make the default value be dynamic, +it is not entirely obvious for the chart users to see a go template as value in a `values.yml`. +It would then be helpful to custom-render these default values in the helm README, so that it is not +treated as a pure JSON object (because the syntax highlighter would be incorrect). +Instead we can custom render the presentation so it would make sense to the user. + +In our example here, any key with a type `tpl/xxx` would be rendered as `
`
+HTML tag, in which we both put the key name and the YAML multiline modifier `|` to make
+it really clear that the key accept a multiline string as value, because it would be rendered as
+YAML object by helm after the values are interpolated/substituted.
+
+Take a look at [extraPodEnv](#extraPodEnv). The `Default` column shows the key name `extraPodEnv`, the multiline YAML
+modifier `|`, and the template string which contains some go string template syntax `{{ }}`.
+
+You can also control the HTML styling directly. In some markdown viewer, the HTML tag and inline styles
+are respected, so the custom styles can be seen. Combined with a Jamstack approach, you can
+design your template to also incorporate some custom React styles or simple CSS.
+
+In our example here, [global.adminEmail](#global--adminEmail) is annotated with `email` notationType.
+This allows you to insert custom rendering code for email. For supported markdown viewer, like Visual Studio Code,
+the default value will have `green` color, and if clicked will direct you to your default email composer.
+
+The reason we have two separate annotation, value type and notation type, is because several different types
+can have the same type renderer. For example, any type `tpl/xxx` is a go template string, so it will be rendered the same
+in our docs if we annotate it with `@notationType -- tpl`.
+
+## Customized Rendering
+
+This README also shows some possible customization with helm-docs. In the [README.md.gotmpl](README.md.gotmpl)
+file, you can see that we modified the column `Key` to also be hyperlinked with the definition in `values.yaml`.
+If you view this README.md files in GitHub and click the value's key, you will be directed to the
+key location in the `values.yaml` file.
+
+You can also render a raw string into the comments using `@section` annotations.
+You can jump to [sampleYaml](#sampleYaml) key and check it's description where it
+uses HTML `` tag to collapse some part of the comments.
+
+## Values
+
+
+	
+		
+		
+		
+		
+	
+	
+		
+			
+			
+			
+			
+		
+		
+			
+			
+			
+			
+		
+		
+			
+			
+			
+			
+		
+		
+			
+			
+			
+			
+		
+		
+			
+			
+			
+			
+		
+		
+			
+			
+			
+			
+		
+		
+			
+			
+			
+			
+		
+		
+			
+			
+			
+			
+		
+		
+			
+			
+			
+			
+		
+		
+			
+			
+			
+			
+		
+		
+			
+			
+			
+			
+		
+		
+			
+			
+			
+			
+		
+		
+			
+			
+			
+			
+		
+		
+			
+			
+			
+			
+		
+		
+			
+			
+			
+			
+		
+		
+			
+			
+			
+			
+		
+		
+			
+			
+			
+			
+		
+		
+			
+			
+			
+			
+		
+		
+			
+			
+			
+			
+		
+		
+			
+			
+			
+			
+		
+		
+			
+			
+			
+			
+		
+		
+			
+			
+			
+			
+		
+		
+			
+			
+			
+			
+		
+		
+			
+			
+			
+			
+		
+		
+			
+			
+			
+			
+		
+		
+			
+			
+			
+			
+		
+		
+			
+			
+			
+			
+		
+		
+			
+			
+			
+			
+		
+		
+			
+			
+			
+			
+		
+		
+			
+			
+			
+			
+		
+		
+			
+			
+			
+			
+		
+		
+			
+			
+			
+			
+		
+		
+			
+			
+			
+			
+		
+		
+			
+			
+			
+			
+		
+		
+			
+			
+			
+			
+		
+		
+			
+			
+			
+			
+		
+		
+			
+			
+			
+			
+		
+		
+			
+			
+			
+			
+		
+		
+			
+			
+			
+			
+		
+		
+			
+			
+			
+			
+		
+		
+			
+			
+			
+			
+		
+		
+			
+			
+			
+			
+		
+		
+			
+			
+			
+			
+		
+		
+			
+			
+			
+			
+		
+		
+			
+			
+			
+			
+		
+		
+			
+			
+			
+			
+		
+		
+			
+			
+			
+			
+		
+		
+			
+			
+			
+			
+		
+		
+			
+			
+			
+			
+		
+		
+			
+			
+			
+			
+		
+		
+			
+			
+			
+			
+		
+		
+			
+			
+			
+			
+		
+		
+			
+			
+			
+			
+		
+		
+			
+			
+			
+			
+		
+		
+			
+			
+			
+			
+		
+		
+			
+			
+			
+			
+		
+		
+			
+			
+			
+			
+		
+		
+			
+			
+			
+			
+		
+		
+			
+			
+			
+			
+		
+		
+			
+			
+			
+			
+		
+		
+			
+			
+			
+			
+		
+		
+			
+			
+			
+			
+		
+		
+			
+			
+			
+			
+		
+	
+
KeyTypeDefaultDescription
extraConfigMap +tpl/dict + +
+
+extraConfigMap: |
+ 
+
+
+
Define this for extra config map to be included in django-shared-config
extraPodEnv +tpl/array + +
+
+extraPodEnv: |
+  - name: DJANGO_SETTINGS_MODULE
+    value: "django.settings"
+  - name: DEBUG
+    value: {{ .Values.global.debug | quote }}
+  - name: ROOT_URLCONF
+    value: {{ .Values.global.rootURLConf | quote }}
+  - name: MAIN_APP_NAME
+    value: {{ .Values.global.mainAppName | quote }}
+ 
+
+
+
Define this for extra Django environment variables
extraPodSpec +tpl/object + +
+
+extraPodSpec: |
+ 
+
+
+
This will be evaluated as pod spec
extraSecret +tpl/dict + +
+
+extraSecret: |
+ 
+
+
+
Define this for extra secrets to be included in django-shared-secret secret
extraVolume +tpl/array + +
+
+extraVolume: |
+ 
+
+
+
Define this for extra volume (in pair with extraVolumeMounts)
extraVolumeMounts +tpl/array + +
+
+extraVolumeMounts: |
+ 
+
+
+
Define this for extra volume mounts in the pod
global +object + +
+
+{
+  "adminEmail": "admin@localhost",
+  "adminPassword": {
+    "value": null,
+    "valueFrom": {
+      "secretKeyRef": {
+        "key": "admin-password",
+        "name": null
+      }
+    }
+  },
+  "adminUser": "admin",
+  "databaseHost": "postgis",
+  "databaseName": "django",
+  "databasePassword": {
+    "value": null,
+    "valueFrom": {
+      "secretKeyRef": {
+        "key": "database-password",
+        "name": null
+      }
+    }
+  },
+  "databasePort": 5432,
+  "databaseUsername": "django_db_user",
+  "debug": "False",
+  "djangoArgs": "[\"uwsgi\",\"--chdir=${REPO_ROOT}\",\"--module=${MAIN_APP_NAME}.wsgi\",\"--socket=:8000\",\"--http=0.0.0.0:8080\",\"--processes=5\",\"--buffer-size=8192\"]\n",
+  "djangoCommand": "[\"/opt/django/scripts/docker-entrypoint.sh\"]\n",
+  "djangoSecretKey": {
+    "value": null,
+    "valueFrom": {
+      "secretKeyRef": {
+        "key": "django-secret",
+        "name": null
+      }
+    }
+  },
+  "djangoSettingsModule": "django.settings",
+  "existingSecret": "",
+  "mainAppName": "django",
+  "mediaRoot": "/opt/django/media",
+  "nameOverride": "django",
+  "rootURLConf": "django.urls",
+  "sharedSecretName": "django-shared-secret",
+  "siteName": "django",
+  "staticRoot": "/opt/django/static"
+}
+
+
+
This key name is used for service interconnection between subcharts and parent charts.
global.adminEmail +string/email + + + Default admin email sender
global.adminPassword.value +string + +
+
+null
+
+
+
Specify this password value. If not, it will be autogenerated everytime chart upgraded
global.adminUser +string + +
+
+"admin"
+
+
+
Default super user admin username
global.databaseHost +string + +
+
+"postgis"
+
+
+
Django database host location. By default this chart can generate standard postgres chart. So you can leave it as default. If you use external backend, you must provide the value
global.databaseName +string + +
+
+"django"
+
+
+
Django database name
global.databasePassword.value +string + +
+
+null
+
+
+
Specify this password value. If not, it will be autogenerated everytime chart upgraded. If you use external backend, you must provide the value
global.databasePort +int + +
+
+5432
+
+
+
Django database port. By default this chart can generate standard postgres chart. So you can leave it as default. If you use external backend, you must provide the value
global.databaseUsername +string + +
+
+"django_db_user"
+
+
+
Database username backend to connect to. If you use external backend, provide the value
global.debug +string + +
+
+"False"
+
+
+
Python boolean literal, this will correspond to `DEBUG` environment variable inside the Django container. Useful as a debug switch.
global.djangoArgs +tpl/array + +
+
+global.djangoArgs: |
+  ["uwsgi","--chdir=${REPO_ROOT}","--module=${MAIN_APP_NAME}.wsgi","--socket=:8000","--http=0.0.0.0:8080","--processes=5","--buffer-size=8192"]
+ 
+
+
+
The django command args to be passed to entrypoint command
global.djangoCommand +tpl/array + +
+
+global.djangoCommand: |
+  ["/opt/django/scripts/docker-entrypoint.sh"]
+ 
+
+
+
The django entrypoint command to execute
global.djangoSecretKey.value +string + +
+
+null
+
+
+
Specify this Django Secret string value. If not, it will be autogenerated everytime chart upgraded
global.djangoSettingsModule +string + +
+
+"django.settings"
+
+
+
Django settings module to be used
global.existingSecret +tpl/string + +
+
+global.existingSecret: |
+ 
+
+
+
Name of existing secret
global.mainAppName +string + +
+
+"django"
+
+
+
The main app name to execute. Affects which settings, wsgi, and rootURL to use.
global.mediaRoot +path + +
+
+"/opt/django/media"
+
+
+
Location to the media directory
global.rootURLConf +string + +
+
+"django.urls"
+
+
+
Django root URL conf to be used
global.sharedSecretName +string + +
+
+"django-shared-secret"
+
+
+
Name of shared secret store that will be generated
global.siteName +string + +
+
+"django"
+
+
+
The site name. It will be used to construct url such as http://django/
global.staticRoot +path + +
+
+"/opt/django/static"
+
+
+
Location to the static directory
image +object + +
+
+{
+  "pullPolicy": "IfNotPresent",
+  "registry": "docker.io",
+  "repository": "lucernae/django-sample",
+  "tag": "3.1"
+}
+
+
+
Image map
image.pullPolicy +string + +
+
+"IfNotPresent"
+
+
+
Image pullPolicy
image.registry +string + +
+
+"docker.io"
+
+
+
Image registry
image.repository +string + +
+
+"lucernae/django-sample"
+
+
+
Image repository
image.tag +string + +
+
+"3.1"
+
+
+
Image tag
ingress.annotations +dict + +
+
+{}
+
+
+
Custom Ingress annotations
ingress.enabled +bool + +
+
+false
+
+
+
Set to true to generate Ingress resource
ingress.host +tpl/string + +
+
+ingress.host: |
+ 
+
+
+
Set custom host name. (DNS name convention)
ingress.labels +dict + +
+
+{}
+
+
+
Custom Ingress labels
ingress.tls.enabled +bool + +
+
+false
+
+
+
Set to true to enable HTTPS
ingress.tls.secretName +string + +
+
+"django-tls"
+
+
+
You must provide a secret name where the TLS cert is stored
labels +map + +
+
+user/workload: "true"
+client-name: "my-boss"
+project-name: "awesome-project"
+
+
+
+
The deployment label
persistence.mediaDir.accessModes[0] +string + +
+
+"ReadWriteOnce"
+
+
+
persistence.mediaDir.annotations +object + +
+
+{}
+
+
+
persistence.mediaDir.enabled +bool + +
+
+true
+
+
+
Allow persistence
persistence.mediaDir.existingClaim +bool + +
+
+false
+
+
+
persistence.mediaDir.mountPath +string + +
+
+"/opt/django/media"
+
+
+
persistence.mediaDir.size +string + +
+
+"8Gi"
+
+
+
persistence.mediaDir.subPath +string + +
+
+"media"
+
+
+
persistence.staticDir.accessModes +k8s/storage/persistent-volume/access-modes + +
+
+- ReadWriteOnce
+
+
+
+
Static Dir access modes
persistence.staticDir.annotations +object + +
+
+{}
+
+
+
persistence.staticDir.enabled +bool + +
+
+true
+
+
+
Allow persistence
persistence.staticDir.existingClaim +bool + +
+
+false
+
+
+
persistence.staticDir.mountPath +string + +
+
+"/opt/django/static"
+
+
+
persistence.staticDir.size +string + +
+
+"8Gi"
+
+
+
persistence.staticDir.subPath +string + +
+
+"static"
+
+
+
postgis.enabled +bool + +
+
+true
+
+
+
Enable postgis as database backend by default. Set to false if using different external backend.
postgis.existingSecret +tpl/string + +
+
+postgis.existingSecret: |
+  {{ include "common.sharedSecretName" . | quote -}}
+ 
+
+
+
Existing secret to be used
probe +tpl/object + +
+
+probe: |
+ 
+
+
+
Probe can be overridden
sampleYaml +dict + +
+
+{}
+
+
+
Values with long description +Sometimes you need a very long description +for your values. + +Any comment section for a given key with **@section** attribute +will be treated as raw string and stored as is. +Since it generates in Markdown format, you can do something like this: + +```yaml +hello: + bar: true +``` + +Markdown also accept subset of HTML tags. So you can also do this: + +
++Expand + +```bash +execute some command +``` + +
service.annotations +dict + +
+
+{}
+
+
+
Extra service annotations
service.clusterIP +string + +
+
+""
+
+
+
Specify `None` for headless service. Otherwise, leave them be.
service.externalIPs +tpl/array + +
+
+service.externalIPs: |
+ 
+
+
+
Specify for LoadBalancer service type
service.nodePort +int + +
+
+null
+
+
+
Specify node port, for NodePort service type
service.port +int + +
+
+80
+
+
+
Specify service port
service.type +string + +
+
+"ClusterIP"
+
+
+
Define k8s service for Django.
+ diff --git a/example-charts/custom-value-notation-type/README.md.gotmpl b/example-charts/custom-value-notation-type/README.md.gotmpl new file mode 100644 index 0000000..be2274c --- /dev/null +++ b/example-charts/custom-value-notation-type/README.md.gotmpl @@ -0,0 +1,162 @@ +{{ template "chart.header" . }} + +{{ template "chart.deprecationWarning" . }} + +{{ template "chart.badgesSection" . }} + +{{ template "chart.description" . }} + +{{ template "chart.homepageLine" . }} + +{{ template "chart.maintainersSection" . }} + +{{ template "chart.sourcesSection" . }} + +{{ template "chart.requirementsSection" . }} + +# Some Long Description + +This is a sample README with custom overrides. +Check the template in [README.md.gotmpl](README.md.gotmpl). + +In that file, we redefine the template definition of `chart.valueDefaultColumnRender` +for some custom `@notationType` such as `string/email`. + +This chart README uses `chart.valuesSectionHtml` instead of `chart.valuesSection`. +Using HTML table directly instead of using Markdown table allows us to control the table +presentation, such as the height. This is especially useful for very long `values.yaml` file, +and you need to scroll both horizontally and vertically to navigate the values. + +In the template file, we redefine `chart.valuesTableHtml` so that we use table height of +400px at most. Github can understand that attribute. The more sophisticated use case is if you +want to combine helm-docs with a Jamstack static generator where you can have your own page generated +from this README. + +The customization can goes even further. Normally, you can't define anchor in markdown unless it is a heading. But you can do so easily using HTML tags. +You can override the column key renderer by adding an `id` attribute so that it can be referred. +This way, when you write markdown links like [ingress.tls.secretName](#ingress--tls--secretName), clicking the link +will take you to the value description row. + +## Value Types + +One of the benefit of using HTML table is we can make a simple tooltip and anchor. +For example, the value [global.adminEmail](#global--adminEmail) is annotated as type `string/email`. We create +the definition of the value type here and can be anchored by links with `#stringemail` hyperlinks. + +We can also create custom type column renderer, where we can assign a tooltip for each type. +Try this out. Go navigate to [global.adminEmail](#global--adminEmail) value, hover on the value type `string/email`, you will then see +some tooltip. Clicking the type link will direct you back to it's relevant value type section below. + +Other useful case is If the type is a known type, like +Kubernetes service type, you can anchor the type to redirect user to k8s documentation page to learn more. +Check the value [persistence.staticDir.accessModes](#persistence--staticDir--accessModes) + +### string/email + +{{- define "chart.valuetypes.email" }} +This value type is for a valid email address format. Such as owner@somedomain.org. +{{- end }} +{{ template "chart.valuetypes.email" . }} + +## Notation Type + +Another reason to use HTML table is because in some cases we want to custom-render the default value. + +In helm chart templates, sometimes author designs the template to accept a go template string value. +That means, the template string can be processed by helm chart and be replaced with dynamic computed values, before it was +rendered to the chart. Although it is very useful and flexible to make the default value be dynamic, +it is not entirely obvious for the chart users to see a go template as value in a `values.yml`. +It would then be helpful to custom-render these default values in the helm README, so that it is not +treated as a pure JSON object (because the syntax highlighter would be incorrect). +Instead we can custom render the presentation so it would make sense to the user. + +In our example here, any key with a type `tpl/xxx` would be rendered as `
` 
+HTML tag, in which we both put the key name and the YAML multiline modifier `|` to make 
+it really clear that the key accept a multiline string as value, because it would be rendered as 
+YAML object by helm after the values are interpolated/substituted.
+
+Take a look at [extraPodEnv](#extraPodEnv). The `Default` column shows the key name `extraPodEnv`, the multiline YAML 
+modifier `|`, and the template string which contains some go string template syntax `{{"{{ }}"}}`.
+
+You can also control the HTML styling directly. In some markdown viewer, the HTML tag and inline styles 
+are respected, so the custom styles can be seen. Combined with a Jamstack approach, you can 
+design your template to also incorporate some custom React styles or simple CSS.
+
+In our example here, [global.adminEmail](#global--adminEmail) is annotated with `email` notationType.
+This allows you to insert custom rendering code for email. For supported markdown viewer, like Visual Studio Code, 
+the default value will have `green` color, and if clicked will direct you to your default email composer.
+
+The reason we have two separate annotation, value type and notation type, is because several different types 
+can have the same type renderer. For example, any type `tpl/xxx` is a go template string, so it will be rendered the same 
+in our docs if we annotate it with `@notationType -- tpl`.
+
+## Customized Rendering
+
+This README also shows some possible customization with helm-docs. In the [README.md.gotmpl](README.md.gotmpl) 
+file, you can see that we modified the column `Key` to also be hyperlinked with the definition in `values.yaml`.
+If you view this README.md files in GitHub and click the value's key, you will be directed to the 
+key location in the `values.yaml` file.
+
+You can also render a raw string into the comments using `@section` annotations.
+You can jump to [sampleYaml](#sampleYaml) key and check it's description where it
+uses HTML `` tag to collapse some part of the comments.
+
+{{ define "chart.valueDefaultColumnRender" }}
+{{- $defaultValue := (default .Default .AutoDefault)  -}}
+{{- $notationType := .NotationType }}
+{{- if (and (hasPrefix "`" $defaultValue) (hasSuffix "`" $defaultValue) ) -}}
+{{- $defaultValue = (toPrettyJson (fromJson (trimAll "`" (default .Default .AutoDefault) ) ) ) -}}
+{{- $notationType = "json" }}
+{{- end -}}
+{{- if (eq $notationType "tpl" ) }}
+
+{{ .Key }}: |
+{{- $defaultValue | nindent 2 }}
+
+{{- else if (eq $notationType "email") }} +"{{ $defaultValue }}" +{{- else }} +
+{{ $defaultValue }}
+
+{{- end }} +{{ end }} + +{{ define "chart.typeColumnRender" }} +{{- if (eq .Type "string/email") }} +{{.Type}} +{{- else if (eq .Type "k8s/storage/persistent-volume/access-modes" )}} +{{- .Type }} +{{- else }} +{{ .Type }} +{{- end }} +{{ end }} + +{{ define "chart.valuesTableHtml" }} + + + + + + + + + {{- range .Values }} + + + + + + + {{- end }} + +
KeyTypeDefaultDescription
{{ .Key }}{{- template "chart.typeColumnRender" . -}} +
{{ template "chart.valueDefaultColumnRender" . }}
+
{{ if .Description }}{{ .Description }}{{ else }}{{ .AutoDescription }}{{ end }}
+{{ end }} + +{{ template "chart.valuesSectionHtml" . }} + +{{ template "helm-docs.versionFooter" . }} diff --git a/example-charts/custom-value-notation-type/values.yaml b/example-charts/custom-value-notation-type/values.yaml new file mode 100644 index 0000000..bf08e89 --- /dev/null +++ b/example-charts/custom-value-notation-type/values.yaml @@ -0,0 +1,227 @@ +# -- Image map +image: + # -- Image registry + registry: docker.io + # -- Image repository + repository: lucernae/django-sample + # -- Image tag + tag: "3.1" + # -- Image pullPolicy + pullPolicy: IfNotPresent + + +# -- This key name is used for service interconnection between subcharts and parent charts. +global: + nameOverride: django + # -- (tpl/string) Name of existing secret + # @notationType -- tpl + existingSecret: | + # -- (string) Name of shared secret store that will be generated + sharedSecretName: django-shared-secret + # generic values + # -- (string) The site name. It will be used to construct url such as http://django/ + siteName: django + # -- (tpl/array) The django entrypoint command to execute + # @notationType -- tpl + djangoCommand: | + ["/opt/django/scripts/docker-entrypoint.sh"] + # -- (tpl/array) The django command args to be passed to entrypoint command + # @notationType -- tpl + djangoArgs: | + ["uwsgi","--chdir=${REPO_ROOT}","--module=${MAIN_APP_NAME}.wsgi","--socket=:8000","--http=0.0.0.0:8080","--processes=5","--buffer-size=8192"] + # -- (string) Default super user admin username + adminUser: admin + adminPassword: + # -- (string) Specify this password value. If not, it will be autogenerated everytime chart upgraded + value: + valueFrom: + secretKeyRef: + name: + key: admin-password + # -- (string/email) Default admin email sender + # @notationType -- email + adminEmail: admin@localhost + djangoSecretKey: + # -- (string) Specify this Django Secret string value. If not, it will be autogenerated everytime chart upgraded + value: + valueFrom: + secretKeyRef: + name: + key: django-secret + # -- (string) Database username backend to connect to. If you use external backend, provide the value + databaseUsername: django_db_user + databasePassword: + # -- (string) Specify this password value. If not, it will be autogenerated everytime chart upgraded. If you use external backend, you must provide the value + value: + valueFrom: + secretKeyRef: + name: + key: database-password + # -- (string) Django database name + databaseName: django + # -- (string) Django database host location. By default this chart can generate standard postgres chart. So you can leave it as default. If you use external backend, you must provide the value + databaseHost: postgis + # -- (int) Django database port. By default this chart can generate standard postgres chart. So you can leave it as default. If you use external backend, you must provide the value + databasePort: 5432 + # -- (string) Python boolean literal, this will correspond to `DEBUG` environment variable inside the Django container. Useful as a debug switch. + debug: "False" + # -- (string) The main app name to execute. Affects which settings, wsgi, and rootURL to use. + mainAppName: django + # -- (string) Django settings module to be used + djangoSettingsModule: django.settings + # -- (string) Django root URL conf to be used + rootURLConf: django.urls + # -- (path) Location to the static directory + staticRoot: /opt/django/static + # -- (path) Location to the media directory + mediaRoot: /opt/django/media + +# -- (map) The deployment label +# @notationType -- yaml +labels: + user/workload: "true" + client-name: "my-boss" + project-name: "awesome-project" + +# -- (tpl/array) Define this for extra Django environment variables +# @notationType -- tpl +extraPodEnv: | + - name: DJANGO_SETTINGS_MODULE + value: "django.settings" + - name: DEBUG + value: {{ .Values.global.debug | quote }} + - name: ROOT_URLCONF + value: {{ .Values.global.rootURLConf | quote }} + - name: MAIN_APP_NAME + value: {{ .Values.global.mainAppName | quote }} + +# -- (tpl/object) This will be evaluated as pod spec +# @notationType -- tpl +extraPodSpec: | +# nodeSelector: +# a.label: value + +# -- (tpl/dict) Define this for extra secrets to be included in django-shared-secret secret +# @notationType -- tpl +extraSecret: | +# key_1: value_1 + +# -- (tpl/dict) Define this for extra config map to be included in django-shared-config +# @notationType -- tpl +extraConfigMap: | +# file_1: conf content + +# -- (tpl/array) Define this for extra volume mounts in the pod +# @notationType -- tpl +extraVolumeMounts: | +# You may potentially mount a config map/secret +# - name: custom-config +# mountPath: /docker-entrypoint.sh +# subPath: docker-entrypoint.sh +# readOnly: true + +# -- (tpl/array) Define this for extra volume (in pair with extraVolumeMounts) +# @notationType -- tpl +extraVolume: | +# You may potentially mount a config map/secret +# - name: custom-config +# configMap: +# name: geonode-config + +service: + # -- (string) Define k8s service for Django. + type: ClusterIP + # -- (string) Specify `None` for headless service. Otherwise, leave them be. + clusterIP: "" + # -- (tpl/array) Specify for LoadBalancer service type + # @notationType -- tpl + externalIPs: | + # -- (int) Specify service port + port: 80 + + # -- (int) Specify node port, for NodePort service type + nodePort: + + # -- (dict) Extra service annotations + annotations: {} + +ingress: + # -- (bool) Set to true to generate Ingress resource + enabled: false + # -- (tpl/string) Set custom host name. (DNS name convention) + # @notationType -- tpl + host: | + # -- (dict) Custom Ingress annotations + annotations: {} + # -- (dict) Custom Ingress labels + labels: {} + tls: + # -- (bool) Set to true to enable HTTPS + enabled: false + # -- (string) You must provide a secret name where the TLS cert is stored + secretName: django-tls + +# -- (tpl/object) Probe can be overridden +# @notationType -- tpl +probe: | + +postgis: + # -- (bool) Enable postgis as database backend by default. Set to false if using different external backend. + enabled: true + + # -- (tpl/string) Existing secret to be used + # @notationType -- tpl + existingSecret: | + {{ include "common.sharedSecretName" . | quote -}} + + +persistence: + staticDir: + # -- (bool) Allow persistence + enabled: true + existingClaim: false + mountPath: /opt/django/static + subPath: "static" + size: 8Gi + # -- (k8s/storage/persistent-volume/access-modes) Static Dir access modes + # @notationType -- yaml + accessModes: + - ReadWriteOnce + annotations: {} + mediaDir: + # -- (bool) Allow persistence + enabled: true + existingClaim: false + mountPath: /opt/django/media + subPath: "media" + size: 8Gi + accessModes: + - ReadWriteOnce + annotations: {} + + +# -- (dict) Values with long description +# @section +# Sometimes you need a very long description +# for your values. +# +# Any comment section for a given key with **@section** attribute +# will be treated as raw string and stored as is. +# Since it generates in Markdown format, you can do something like this: +# +# ```yaml +# hello: +# bar: true +# ``` +# +# Markdown also accept subset of HTML tags. So you can also do this: +# +#
+# +Expand +# +# ```bash +# execute some command +# ``` +# +#
+sampleYaml: {} \ No newline at end of file diff --git a/pkg/document/model.go b/pkg/document/model.go index a09c10d..763dd8b 100644 --- a/pkg/document/model.go +++ b/pkg/document/model.go @@ -15,6 +15,7 @@ import ( type valueRow struct { Key string Type string + NotationType string AutoDefault string Default string AutoDescription string diff --git a/pkg/document/template.go b/pkg/document/template.go index 7de0613..ee0dbd9 100644 --- a/pkg/document/template.go +++ b/pkg/document/template.go @@ -223,6 +223,54 @@ func getValuesTableTemplates() string { valuesSectionBuilder.WriteString("{{ end }}") valuesSectionBuilder.WriteString("{{ end }}") + // For HTML tables + valuesSectionBuilder.WriteString(` +{{ define "chart.valueDefaultColumnRender" }} +{{- $defaultValue := (default .Default .AutoDefault) -}} +{{- $notationType := .NotationType }} +{{- if (and (hasPrefix "` + "`" + `" $defaultValue) (hasSuffix "` + "`" + `" $defaultValue) ) -}} +{{- $defaultValue = (toPrettyJson (fromJson (trimAll "` + "`" + `" (default .Default .AutoDefault) ) ) ) -}} +{{- $notationType = "json" }} +{{- end -}} +
+{{- if (eq $notationType "tpl" ) }}
+{{ .Key }}: |
+{{- $defaultValue | nindent 2 }}
+{{- else }}
+{{ $defaultValue }}
+{{- end }}
+
+{{ end }} + +{{ define "chart.valuesTableHtml" }} + + + + + + + + + {{- range .Values }} + + + + + + + {{- end }} + +
KeyTypeDefaultDescription
{{ .Key }}{{ .Type }}{{ template "chart.valueDefaultColumnRender" . }}{{ if .Description }}{{ .Description }}{{ else }}{{ .AutoDescription }}{{ end }}
+{{ end }} + +{{ define "chart.valuesSectionHtml" }} +{{ if .Values }} +{{ template "chart.valuesHeader" . }} +{{ template "chart.valuesTableHtml" . }} +{{ end }} +{{ end }} + `) + return valuesSectionBuilder.String() } diff --git a/pkg/document/values.go b/pkg/document/values.go index a6df424..da0112f 100644 --- a/pkg/document/values.go +++ b/pkg/document/values.go @@ -18,6 +18,8 @@ const ( listType = "list" objectType = "object" stringType = "string" + yamlType = "yaml" + tplType = "tpl" ) // Yaml tags that differentiate the type of scalar object in the node @@ -89,6 +91,9 @@ func parseNilValueType(key string, description helm.ChartValueDescription, autoD } else { description.Description = "" } + } else if autoDescription.ValueType != "" { + // Use whatever the type recognized by autoDescription parser + t = autoDescription.ValueType } else { t = stringType } @@ -158,7 +163,18 @@ func createValueRow( autoDefaultValue := autoDescription.Default defaultValue := description.Default - if defaultValue == "" && autoDefaultValue == "" { + notationType := autoDescription.NotationType + defaultType := getTypeName(value) + if description.ValueType != "" { + defaultType = description.ValueType + } else if autoDescription.ValueType != "" { + defaultType = autoDescription.ValueType + } else if notationType != "" { + // If nothing can be inferred then infer from notationType + defaultType = notationType + } + + if defaultValue == "" && autoDefaultValue == "" && notationType == "" { jsonEncodedValue, err := jsonMarshalNoEscape(key, value) if err != nil { return valueRow{}, fmt.Errorf("failed to marshal default value for %s to json: %s", key, err) @@ -167,9 +183,16 @@ func createValueRow( defaultValue = fmt.Sprintf("`%s`", jsonEncodedValue) } + if defaultValue == "" && autoDefaultValue == "" && notationType != "" { + // We want to render custom styles for custom NotationType + // So, output a raw default value for this and let the template handle it + defaultValue = fmt.Sprintf("%s", value) + } + return valueRow{ Key: key, - Type: getTypeName(value), + Type: defaultType, + NotationType: notationType, AutoDefault: autoDescription.Default, Default: defaultValue, AutoDescription: autoDescription.Description, @@ -208,7 +231,7 @@ func createValueRowsFromList( // 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 || autoDescription.Description != "" { + if hasDescription || (autoDescription.Description != "" && autoDescription.NotationType == "") { jsonableObject := convertHelmValuesToJsonable(values) listRow, err := createValueRow(prefix, jsonableObject, description, autoDescription, key.Column, key.Line) @@ -216,6 +239,38 @@ func createValueRowsFromList( return nil, err } + valueRows = append(valueRows, listRow) + documentLeafNodes = false + } else if hasDescription || (autoDescription.Description != "" && autoDescription.NotationType != "") { + // If it has NotationType described, then use that + var notationValue interface{} + var err error + var listRow valueRow + switch autoDescription.NotationType { + case yamlType: + notationValue, err = yaml.Marshal(values) + if err != nil { + return nil, err + } + + listRow, err = createValueRow(prefix, notationValue, description, autoDescription, key.Column, key.Line) + + if err != nil { + return nil, err + } + default: + // Any other case means we let the template renderer to decide how to + // format the default value. But the value are stored as raw string + fallthrough + case tplType: + notationValue = values.Value + listRow, err = createValueRow(prefix, notationValue, description, autoDescription, key.Column, key.Line) + + if err != nil { + return nil, err + } + } + valueRows = append(valueRows, listRow) documentLeafNodes = false } @@ -265,7 +320,7 @@ func createValueRowsFromObject( // 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 || autoDescription.Description != "" { + if hasDescription || (autoDescription.Description != "" && autoDescription.NotationType == "") { jsonableObject := convertHelmValuesToJsonable(values) objectRow, err := createValueRow(nextPrefix, jsonableObject, description, autoDescription, key.Column, key.Line) @@ -273,6 +328,40 @@ func createValueRowsFromObject( return nil, err } + valueRows = append(valueRows, objectRow) + documentLeafNodes = false + } else if hasDescription || (autoDescription.Description != "" && autoDescription.NotationType != "") { + + // If it has NotationType described, then use that + var notationValue interface{} + var err error + var objectRow valueRow + switch autoDescription.NotationType { + case yamlType: + notationValue, err = yaml.Marshal(values) + if err != nil { + return nil, err + } + + objectRow, err = createValueRow(nextPrefix, notationValue, description, autoDescription, key.Column, key.Line) + + if err != nil { + return nil, err + } + + default: + // Any other case means we let the template renderer to decide how to + // format the default value. But the value are stored as raw string + fallthrough + case tplType: + notationValue = values.Value + objectRow, err = createValueRow(nextPrefix, notationValue, description, autoDescription, key.Column, key.Line) + + if err != nil { + return nil, err + } + } + valueRows = append(valueRows, objectRow) documentLeafNodes = false } @@ -319,6 +408,40 @@ func createValueRowsFromField( leafValueRow, err := createValueRow(prefix, nil, description, autoDescription, key.Column, key.Line) return []valueRow{leafValueRow}, err case strTag: + // extra check to see if the node is a string, but @notationType was declared + if autoDescription.NotationType != "" { + var notationValue interface{} + var err error + var leafValueRow valueRow + switch autoDescription.NotationType { + case yamlType: + notationValue, err = yaml.Marshal(value) + if err != nil { + return nil, err + } + + leafValueRow, err = createValueRow(prefix, notationValue, description, autoDescription, key.Column, key.Line) + + if err != nil { + return nil, err + } + + return []valueRow{leafValueRow}, err + default: + // Any other case means we let the template renderer to decide how to + // format the default value. But the value are stored as raw string + fallthrough + case tplType: + notationValue = value.Value + leafValueRow, err = createValueRow(prefix, notationValue, description, autoDescription, key.Column, key.Line) + + if err != nil { + return nil, err + } + + return []valueRow{leafValueRow}, err + } + } fallthrough case timestampTag: leafValueRow, err := createValueRow(prefix, value.Value, description, autoDescription, key.Column, key.Line) diff --git a/pkg/document/values_test.go b/pkg/document/values_test.go index e0e12cf..5730b1b 100644 --- a/pkg/document/values_test.go +++ b/pkg/document/values_test.go @@ -1439,3 +1439,117 @@ foo: assert.Equal(t, "", valuesRows[0].Description) assert.Equal(t, "Bar!", valuesRows[0].AutoDescription) } + +func TestMultilineDescriptionSection(t *testing.T) { + helmValues := parseYamlValues(` +animals: + # -- (list) I mean, dogs are quite nice too... + # @section + # + # List of default dogs: + # - Umbra + # - Penumbra + # - Somnus + # + # @default -- The list of dogs that _I_ own + dogs: +`) + + valuesRows, err := getSortedValuesTableRows(helmValues, make(map[string]helm.ChartValueDescription)) + + assert.Nil(t, err) + assert.Len(t, valuesRows, 1) + + assert.Equal(t, "animals.dogs", valuesRows[0].Key) + assert.Equal(t, listType, valuesRows[0].Type) + assert.Equal(t, "The list of dogs that _I_ own", valuesRows[0].AutoDefault) + assert.Equal(t, "", valuesRows[0].Default) + assert.Equal(t, "I mean, dogs are quite nice too...\n\nList of default dogs:\n - Umbra\n - Penumbra\n - Somnus\n", valuesRows[0].Description) +} + +func TestExtractValueNotationType(t *testing.T) { + helmValues := parseYamlValues(` +animals: + # -- (list) My animals lists + # @notationType -- yaml + cats: + - mike + - ralph + # -- (list) My animal lists, but in tpl string + # @notationType -- tpl + catsInTpl: | + {{- .Values.animals.cats }} + + # -- (object) Declaring object as tpl (to be cascaded with tpl function) + # @notationType -- tpl + dinosaur: | + name: hockney + dynamicVar: {{ .Values.fromOtherProperty }} + + # -- (object) Declaring object as yaml + # @notationType -- yaml + fish: + name: nomoby +`) + + valuesRows, err := getSortedValuesTableRows(helmValues, make(map[string]helm.ChartValueDescription)) + + assert.Nil(t, err) + assert.Len(t, valuesRows, 4) + + assert.Equal(t, "animals.cats", valuesRows[0].Key) + assert.Equal(t, listType, valuesRows[0].Type) + assert.Equal(t, yamlType, valuesRows[0].NotationType) + assert.Equal(t, "- mike\n- ralph\n", valuesRows[0].Default) + assert.Equal(t, "My animals lists", valuesRows[0].AutoDescription) + + assert.Equal(t, "animals.catsInTpl", valuesRows[1].Key) + assert.Equal(t, listType, valuesRows[1].Type) + assert.Equal(t, tplType, valuesRows[1].NotationType) + assert.Equal(t, "{{- .Values.animals.cats }}\n", valuesRows[1].Default) + assert.Equal(t, "My animal lists, but in tpl string", valuesRows[1].AutoDescription) + + assert.Equal(t, "animals.dinosaur", valuesRows[2].Key) + assert.Equal(t, objectType, valuesRows[2].Type) + assert.Equal(t, tplType, valuesRows[2].NotationType) + assert.Equal(t, "name: hockney\ndynamicVar: {{ .Values.fromOtherProperty }}\n", valuesRows[2].Default) + assert.Equal(t, "Declaring object as tpl (to be cascaded with tpl function)", valuesRows[2].AutoDescription) + + assert.Equal(t, "animals.fish", valuesRows[3].Key) + assert.Equal(t, objectType, valuesRows[3].Type) + assert.Equal(t, yamlType, valuesRows[3].NotationType) + assert.Equal(t, "name: nomoby\n", valuesRows[3].Default) + assert.Equal(t, "My animals lists", valuesRows[0].AutoDescription) +} + +func TestExtractCustomDeclaredType(t *testing.T) { + helmValues := parseYamlValues(` +animals: + # -- (list/csv) My animals lists but annotated as csv field + cats: mike,ralph + +owner: + # -- (string/email) This has to be email address + # @notationType -- email + email: "owner@home.org" + `) + + valuesRows, err := getSortedValuesTableRows(helmValues, make(map[string]helm.ChartValueDescription)) + + assert.Nil(t, err) + assert.Len(t, valuesRows, 2) + assert.Equal(t, "animals.cats", valuesRows[0].Key) + // With custom value type, we can convey to the reader that this value is a list, but in a csv format + assert.Equal(t, "list/csv", valuesRows[0].Type) + assert.Equal(t, "`\"mike,ralph\"`", valuesRows[0].Default) + assert.Equal(t, "My animals lists but annotated as csv field", valuesRows[0].AutoDescription) + + assert.Equal(t, "owner.email", valuesRows[1].Key) + assert.Equal(t, "string/email", valuesRows[1].Type) + assert.Equal(t, "email", valuesRows[1].NotationType) + // In case of custom notation type, value in Default must be raw string + // So that template can handle the formatting. + // In this case, email might be reformatted as owner@home.org + assert.Equal(t, "owner@home.org", valuesRows[1].Default) + assert.Equal(t, "This has to be email address", valuesRows[1].AutoDescription) +} diff --git a/pkg/helm/chart_info.go b/pkg/helm/chart_info.go index 969c7c6..01737c8 100644 --- a/pkg/helm/chart_info.go +++ b/pkg/helm/chart_info.go @@ -16,8 +16,11 @@ import ( ) var valuesDescriptionRegex = regexp.MustCompile("^\\s*#\\s*(.*)\\s+--\\s*(.*)$") -var commentContinuationRegex = regexp.MustCompile("^\\s*# (.*)$") +var sectionDescriptionRegex = regexp.MustCompile("^\\s*#\\s+@section") +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+(.*)$") type ChartMetaMaintainer struct { Email string @@ -52,8 +55,10 @@ type ChartRequirements struct { } type ChartValueDescription struct { - Description string - Default string + Description string + Default string + ValueType string + NotationType string } type ChartDocumentationInfo struct { diff --git a/pkg/helm/comment.go b/pkg/helm/comment.go index de93d0e..bf27f4c 100644 --- a/pkg/helm/comment.go +++ b/pkg/helm/comment.go @@ -34,18 +34,46 @@ func ParseComment(commentLines []string) (string, ChartValueDescription) { break } + valueTypeMatch := valueTypeRegex.FindStringSubmatch(c.Description) + if len(valueTypeMatch) > 0 && valueTypeMatch[1] != "" { + c.ValueType = valueTypeMatch[1] + c.Description = valueTypeMatch[2] + } + + var isSection = false + for _, line := range commentLines[docStartIdx+1:] { + sectionFlagMatch := sectionDescriptionRegex.FindStringSubmatch(line) defaultCommentMatch := defaultValueRegex.FindStringSubmatch(line) + notationTypeCommentMatch := valueNotationTypeRegex.FindStringSubmatch(line) + + if !isSection && len(sectionFlagMatch) == 1 { + isSection = true + continue + } if len(defaultCommentMatch) > 1 { c.Default = defaultCommentMatch[1] continue } + if len(notationTypeCommentMatch) > 1 { + c.NotationType = notationTypeCommentMatch[1] + continue + } + commentContinuationMatch := commentContinuationRegex.FindStringSubmatch(line) - if len(commentContinuationMatch) > 1 { - c.Description += " " + commentContinuationMatch[1] + if isSection { + + if len(commentContinuationMatch) > 1 { + c.Description += "\n" + commentContinuationMatch[2] + } + continue + } else { + if len(commentContinuationMatch) > 1 { + c.Description += " " + commentContinuationMatch[2] + } continue } }