kubernetes-helmgo-templatessprig-template-functions

Query Values in helm template


I am trying to generate a TLS certificate for all of the ingress resources in my Helm chart. My helm chart contains an application with multiple backends so my Values.yaml is structed like this:

backend1:
  ingress:
    host: testing.app.com
    tls:
      - secretName: my-tls-cert
        hosts:
          - testing.app.com
backend2:
  ingress:
    host: testing.app.com
    tls:
      - secretName: idp-cts-cert
        hosts:
          - idp-cts
db
  creds: ""

serviceName: ""

Notice there is a mixture of maps and string values. My goal is to use a utility template I wrote to call genSignedCert and generate one TLS cert that has the hosts listed as a CN or alternate name:

{{/*
Return a self-signed TLS certificate
{{ include "common.certs.ingress-tls" .hosts  }}
*/}}
{{- define "common.certs.gen-cert" -}}
{{- $hostlist := toStrings . -}}
{{- $cn := (first $hostlist) -}}
{{- $altnames := uniq (rest $hostlist) -}}
{{- $ca := genCA "idp-ca" 365 -}}
{{- $cert := genSignedCert $cn nil $altnames 365 $ca -}}
tls.crt: {{ $cert.Cert | b64enc }}
tls.key: {{ $cert.Key | b64enc }}
{{- end -}} 

I have tried iterating over the Values and but I cannot come up with workable code to do this.

Edit1: I am aware of the security implications of using self-signed certificates. The bad values.yaml structure is inherited from the fact that this is an umbrella chart and each backed is also it's own chart. A refactor of the charts structure may be required, but I wanted to exhaust all options first.


Solution

  • Consider generating the TLS certificate outside Helm, and injecting it via values (or storing its components in a Secret directly). This avoids some complicated code here. There is a more serious problem, though: every time you call genCA and genSignedCert it creates a new certificate, so every time you upgrade you'll get a different certificate, and for that matter if you call this template once per Ingress object, each will have a different certificate.


    It'd help this problem to restructure the values.yaml slightly. It's hard for code to tell that backend1 is a backend specification, but serviceName isn't. If you just have a list of backends this gets easier:

    backends:
      - ingress:
          host: testing.app.com
          ...
      - ingress:
          host: testing.app.com
          ...
    

    You'll then hit a couple of limitations of Helm templates as a full-featured programming language. Templates only ever return strings, so you can't write a template that returns a list. You can't pass a function as a parameter to a template, so you can't write a general-purpose map (in limited cases you can pass a template name and include it).

    What you can do is write a recursive function that passes the partial list forward to the next iteration, and then invokes the final generator when it's done. In Python, we might write:

    def generateCertificate(backends, tls, hosts):
      # If `tls` is non-empty, take the first item from it and add its
      # hosts to the `hosts` list; then recurse with the same backend
      # list, the remaining `tls` items, and the updated `hosts`:
      if len(tls) > 0:
        return generateCertificate(backends, tls[1:], hosts + tls[0].hosts)
      # If `tls` is empty but `backends` is non-empty, take the first
      # backend, and recurse with the remaining `backends`, the `tls` items
      # from the selected backend, and the same `hosts`:
      else if len(backends) > 0:
        return generateCertificate(backends[1:], backends[0].tls, hosts)
      # If `tls` and `backends` are both empty, we're done
      else:
        return buildTheCertificate(hosts)
    
    certificate = generateCertificate(values.backends, [], [])
    

    We can convert this logic into Go templates:

    {{/* Emit a TLS certificate given the list of backends.  The
    parameter is a dictionary with keys `backends`, `tls`, and `hosts`. */}}
    {{- define "common.certs.gen-cert" -}}
    {{- if .tls -}}
      {{- include "common.certs.gen-cert" (dict "backends" .backend "tls" (last .tls) "hosts" (concat .hosts (head .tls).hosts)) -}}
    {{- else if .backends -}}
      {{- include "common.certs.gen-cert" (dict "backends" (tail .backends) "tls" (head .backends).tls "hosts" .hosts) -}}
    {{- else -}}
      {{- include "common.certs.gen-cert-hosts" .hosts -}}
    {{- end -}}
    {{- end -}}
    
    {{/* Actually generate a TLS certificate from a list of host names.
    Note, the certificate will be regenerated on every call.  The
    single parameter is a list of names. */}}
    {{- define "common.certs.gen-cert-hosts" -}}
    {{- $cn := first . -}}
    {{- $altnames := rest . | uniq -}}
    {{- $ca := genCA "idp-ca" 365 -}}
    {{- $cert := genSignedCert $cn nil $altnames 365 $ca -}}
    tls.crt: {{ $cert.Cert | b64enc }}
    tls.key: {{ $cert.Key | b64enc }}
    {{- end -}}
    
    {{- include "common.certs.gen-cert" (dict "backends" .Values.backends) -}}
    

    This is enough intricate code that it's probably worth unit-testing it. Setting this up is left as an exercise; Helm does not have any sort of native support here.