helm-docs icon indicating copy to clipboard operation
helm-docs copied to clipboard

Optional custom template (rendering) per chart

Open sagikazarmark opened this issue 4 years ago • 2 comments

Use case

When maintaining multiple charts in a single repository, it's inevitable that some charts require custom README templates.

Currently, this is possible using the --template-files option: if a template is not found, a builtin default template is loaded.

This works flawlessly, until you want to pair it with an other use case: define a custom, common template for all the other charts. In other words: chart a should use the template README.md.gotmpl, chart b and chart c should use ../../README.md.gotmpl (template files are relative to chart directories).

Since chart b and chart c don't have README.md.gotmpl, the builtin default template is loaded.

This issue can be worked around by symlinking the common template into every chart directory as needed, but that's not very elegant, is it? (You can see it working here though

One would think that passing both templates to the command would work (like this: --template-files ../../README.md.gotmpl --template-files README.md.gotmpl). Passing the following would however result in the default template being loaded (see #77 for more details). Even after fixing #77, the result of this command will be a readme with the content rendered twice (more precisely: two similar readmes rendered in one file).

This is because internally, all templates are loaded into a single template instance and the entire template is rendered, resulting in an output containing the custom readme template and the builtin default as well.

Solution#1

A simple solution to this problem would be introducing a convention for loading a README.md.gotmpl from the charts root directory as well. In this scenario, if no template files are defined, the default README.md.gotmpl should be checked in the chart directory first, then one directory above.

This is far from perfect though: it doesn't solve more advanced use cases (ie. multiple template files)

Solution#2

A more elegant solution would be using named templates:

package main

import (
	"os"
	"text/template"
)

func main() {
	// Load default template
	tpl := template.Must(template.New("main").Parse("default template"))
	
	// Define builtin templates
	template.Must(tpl.Parse(`{{ define "header" }}header{{ end }}`))
	
	// Load custom main templates
	template.Must(tpl.New("main").Parse(`custom template: {{ template "header" .}}`))
	
	// Load custom templates
	
	tpl.ExecuteTemplate(os.Stdout, "main", nil)
}

(See it in action: https://play.golang.org/p/uizMUHwK4sF)

In this case, templates rendering the output and templates providing "blocks" are kept separate. There is always one main template (that renders the output), but there can be any number of additional template providing building blocks (eg. common header for all chart README templates, etc)

There is one pitfall: named templates are already used here, so we have to make sure they don't overlap.

In terms of implementation, we could introduce a new flag for main templates in addition to the existing --template-files:

  • --main-template-files
  • --output-template-files

The new flag would work similarly to the solution outlined in #77: it would try to load all files, but instead of manually falling back to the default, the template system will take care of it for us, by each new template overriding the old one.

We can probably make this a backward compatible change: if no --main-template-files are passed to the command, the old behavior is kept.

Alternatively, we can keep using a single flag and prepend main template file names with main:: --template-files helpers.gotmpl --template-files main:../README.md.gotmpl --template-files main:README.md.gotmpl. I find it less elegant, but resolves the ambiguity if having two template file flags doing different things.

Solution#3

All of the solutions above presume applying the same settings to each chart. Solution#2 relies on defining multiple templates, falling back in the list if one is not found.

A more direct approach to chart specific changes is chart specific configuration in the chart directory.

For example, a docs.yaml:

templates:
   - README.md.gotmpl

This would also need some sort of fallback though (eg. in the repo root).

Alternatively, we could use annotations in Chart.yaml:

annotations:
    helm-docs-templates: ../helpers.gotmpl;README.md.gotmpl

Conclusion

The above use case uncovers multiple problems:

  • Every template passed to the renderer generates an output
  • There is no way to specify chart specific values

Solution#2 provides a solution for the first problem without a need to solve the second one. It feels like it doesn't really need solving at the moment. We can always implement it later.

sagikazarmark avatar Jan 13 '21 00:01 sagikazarmark

@norwoodj what do you think?

sagikazarmark avatar Jan 29 '21 23:01 sagikazarmark

I think I like #3, the docs.yaml approach best. This way we could retain old behavior when this file is not found, and read from that file when it is.

norwoodj avatar Feb 10 '21 15:02 norwoodj