Skip to content

feat: Predictable Helmfile template #932

@mumoshu

Description

@mumoshu

TL;DR;

I want to add a new helmfile.yaml field to make templating helmfile configs easier.

Problem

Helmfile's double-rendering has opened a wide variety of use-cases that requires you to write a dynamic Helmfile configs.

The fact it works by recursively rendering the template twice to avoid a chicken-and-egg problem.

The problem is that Helmfile needs to load environment values to render the template but to load the values you need to render the template at first. It breaks in many ways and something results in behaviors that are hard to understand. See the incomplete list of those issues to get more context.

Solution

I'd like to propose an alternative format for Helmfile config that doesn't suffer from the chicken-and-egg problem.

Let's see.

Option 1

In the first option, we add the new template.releases field to helmfile.yaml, and a new template function nindenrt.

helmfile.yaml:

values:
- foo: bar

environments:
  prod:
    value:
    - prod.yaml

template:
  # RELEASES: This is the alternative version of `releases` that is rendered by the template
  # You can write a template in the same way as I have been to.
  releases: |
  - {{ tpl (readFile "release.tpl") (dict "name" "myapp1") | nindent 2}}
  # You can add more fields to be merged
  - {{ tpl (readFile "release.tpl") (dict "name" "myapp1") | nindent 2}}
    needs:
    - myapp1
  {{ tpl (readFile "comon-releases.tpl") . }}

Option 2

In the second option, we add a new field named template.helpers in addition to all the things in the first option.

helmfile.yaml:

values:
- foo: bar

environments:
  prod:
    value:
    - prod.yaml

template:
  # HELPERS: Template snippets that are reusable from multiple Helmfile configs
  #
  # Any files listed here can be called with {{ tempalte "BASENAME" }}
  # See https://golang.org/pkg/html/template/#ParseFiles
  helpers:
  - helpers/release.tpl
  - helpers/common-releases.tpl
  # RELEASES: This is the alternative version of `releases` that is rendered by the template
  # In addition to `.Environment.Values`, you can acess template snippets declared above.
  releases: |
  - {{ template "release.tpl" (dict "name" "myapp1") | nindent 2}}
  # You can add more fields to be merged
  - {{ template "release.tpl" (dict "name" "myapp1") | nindent 2}}
    needs:
    - myapp1
  {{ template "common-releases.tpl" . }}

Note that the nindent template function is recently added one that is becoming widely used across many official Helm charts. I'd like to adopt the pattern so that anyone has some experience in reading and writing charts is able to get started with the new way of writing Helmfile configs easily.

helpers/release.tpl:

name: {{ .Name }}
chart: ...
{{ if .Namespace }}
namespace: {{ .Namespace }}
{{ end }}

helpers/common-releases.tpl:

- name: logging
  chart: stable/fluent-bit
- name: servicemesh
  charts: istio/istio

Notice that the new template, template.helpers and template.releases fields are just plain YAML primitives.

This means that Helmfile can load the whole file as a valid YAML. It makes Helmfile possible to load environment values from it, and then render the template. We don't need the double-rendering anymore, which makes the result more predictable.

Option 3

In the third option, we add a new field named releasesTemplate as a alternative to the template.releases in the first option.

This is more consistent with existing fields like installedTemplate and waitTemplate than other options

helmfile.yaml:

values:
- foo: bar

environments:
  prod:
    value:
    - prod.yaml

releasesTemplate: |
  - {{ template "release.tpl" (dict "name" "myapp1") | nindent 2}}
  # You can add more fields to be merged
  - {{ template "release.tpl" (dict "name" "myapp1") | nindent 2}}
    needs:
    - myapp1
  {{ template "common-releases.tpl" . }}

As similar to the option 2, we may add something like templateContext to allow declaraing template helpers to be loaded into the context.

Notes

  • template.helpers is optional. Even without that, something like {{ tpl (readFile "helpers/foo.tpl") (dict "bar "baz") }} can be employed to address the same use-case.
  • This doesn't replace sub-helmfiles. Sub-helmfiles will continue to be used for creating a module which contains a sets of environments, repositories, releases and its template, where template.releases are used for making each sub-helmfile DRY.
  • Implementation-wise, this will still leverage the double-rendering. Helmfile would run the first render as before, and if and only if template.releases is found it proceeds with rendering only the template.releases rather than the whole file. The resulting YAML array is then merged into the top-level releases. Everything after that remains the same as today's Helmfile.
  • If this turned to work, I'd like to remove the double-rendering in Helmfile v1 (or v2?)

Acknowledgement

Double Rendering was added in #308 a year ago. It has been a great way to make your configs dynamic and DRY. It did serve many happy users.

I'm happy and impressed about that this is the feature requested and added by the community.

Thanks a lot for all the people involved in the feature, and especially the original author of it, @davidovich. Without it, I couldn't have come up with this idea, and more importantly Helmfile wouldn't have been popular so much ☺️

I hope this proposal makes the existing users of Helmfile and Double Rendering even more happier, while making on-boarding experiences for new-comers easier as well.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions