I'm looking for a solution to control the input values(defined in values.yaml). I would like to check if the input value is authorized.
Example:
values.yaml
provider: aws
services:
- nginx
- varnish
- php
And in another file(maybe _helpers.tpl?)
authorized_providers:
- aws
- azure
authorized_services:
- nginx
- php
And raise an error(custom message if it's possible) to indicate that the input values are not supported/authorized.
My goal is to avoid to generate a Kubernetes configmap with unsupported values(helm install works but this configuration will generate container errors).
EDIT:
I finally found a solution using "required" with some tricks.
Following my example with my values.yaml config file.
I define in _helpers.tpl:
{{/*
Define the authorized Value for the parameter: .Values.provider
*/}}
{{- define "authorized.provider" }}
{{- printf "aws,azure" }}
{{- end }}
{{/*
Define the error message if the .Values.provider doesn't respect the authorized.provider condition.
*/}}
{{- define "error.provider" }}
{{- $provider := include "authorized.provider" . }}
{{- printf "Invalid value set for .Values.provider - Must be one of %s" $provider }}
{{- end }}
{{/*
Define the authorized Value for the parameter: .Values.services
*/}}
{{- define "authorized.services" }}
{{- printf "nginx,php" }}
{{- end }}
{{/*
Define the error message if the .Values.services doesn't respect the authorized.services condition.
*/}}
{{- define "error.services" }}
{{- $services := include "authorized.services" . }}
{{- printf "Invalid value set for .Values.services - Authorized values are %s" $services }}
{{- end }}
And next, I've created another file: input-values-validation.yaml
{{- $provider := include "authorized.provider" . }}
{{- $errorProvider := include "error.provider" . }}
{{- if not (has .Values.provider (splitList "," $provider)) }}
{{ required $errorProvider .Values.foo }}
{{- end }}
{{- $services := include "authorized.services" . }}
{{- $errorServices := include "error.Services" . }}
{{- $root := . -}}
{{- range .Values.services }}
{{- if not (has . (splitList "," $services)) }}
{{ required $errorServices $root.Values.foo }}
{{- end }}
{{- end }}
Output if bad input value:
==> Linting
[ERROR] templates/: render error in "templates/input-values-validation.yaml": template: templates/input-values-validation.yaml:12:3: executing "templates/input-values-validation.yaml" at<required $errorServ...>: error calling required: Invalid value set for .Values.services - Authorized values are nginx,php
Infos:
".Values.foo" must never be set in the values.yaml file. I used it to fail the "required" check and raise the error.
I've tried to put the content of "input-values-validation.yaml" in the _helpers.tpl file but this generate an error "[ERROR] templates/: rendering template failed: runtime error: invalid memory address or nil pointer dereference". It seems that the "required" function must be used only in yaml files.
So with this solution, I'm able to define the authorized values in the _helpers.tpl file and generate a "custom" error message. And if in the futur I support more providers/services(my example), I'll only need to modify the value in "authorized.provider" and "authorized.services".
I've not seen it done with helm2, at least not in a scan of the official charts, the attempt to define common functions in an incubator chart.
The trickiest bit is being able to give a good error - the closest I've seen is the sprig fail function
But helm3 should provide for this kind of validation, either with schemas or lua
Otherwise perhaps you could do it like:
aws:
nginx: false
varnish: false
php: true
So that the chart user chooses which services they want with a true/false
Related
In Helm's v3 documentation: Accessing Files Inside Templates, the author gives an example of 3 properties (toml) files; where each file has only one key/value pair.
The configmap.yaml looks like this. I'm only adding one config.toml for simplicity.
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-config
data:
{{- $files := .Files }}
{{- range tuple "config.toml" }}
{{ . }}: |-
{{ $files.Get . }}
{{- end }}
This works fine, until I add a second line to the config.toml file.
config.toml
replicaCount=1
foo=bar
Then I get an Error: INSTALLATION FAILED: YAML parse error on deploy/templates/configmap.yaml: error converting YAML to JSON: yaml: line 9: could not find expected ':'
Any thoughts will be appreciated.
Thanks
Helm will read in that file, but it is (for good or bad) a text templating engine. It does not understand that you are trying to compose a YAML file and thus it will not help you. That's actually why you will see so many, many templates in the wild with {{ .thing | indent 8 }} or {{ .otherThing | toYaml }} -- because you need to help Helm know in what context it is emitting the text
Thus, in your specific case, you'll want the indent filter with a value of 4 because your current template has two spaces for the key indent level, and two more spaces for the value block scalar
data:
{{- $files := .Files }}
{{- range tuple "config.toml" }}
{{ . }}: |-
{{ $files.Get . | indent 4 }}
{{/* notice this ^^^ template expression is flush left,
because the 'indent' is handling whitespace, not the golang template itself */}}
{{- end }}
Also, while this is the specific answer to your question, don't overlook the .AsConfig section on that page which seems much more likely to be what you really want to happen, and requires less indent math
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.
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.
I have the following snippet in a helm library which is supposed to add in all the files in a folder into a ConfigMap except for those ending in .tz.
{{- /*
add the contents of every file in the config folder of this IOC helm chart
into the config map - this must include start.sh the startup script.
The files must be text only. Any files ending in .tz are explicitly ommitted
*/ -}}
{{ (.Files.Glob "config/*[!.tz]").AsConfig | indent 2 }}
version.txt: |
IOC {{ .Release.Name }} version {{ .Chart.AppVersion }}
This does not quite work in that it filters out all files ending in . or t or z.
I cant see how to do this with Go globbing. I also cannot work out the syntax for using 'without' for the list created by .Files.Glob.
Can anyone enlighten me? Thanks!
Thanks #David Maze. This almost worked but the reference to .Files.Get failed and I think that is because the . context becomes the range counter within the range loop.
Adding $ to .Files within the loop got it working.
Also to fully replicate AsConfig I needed to extract the basename from
the path which I did by adding regexReplaceAll.
data:
{{- range $path, $_ := .Files.Glob "config/*" }}
{{- if not (hasSuffix ".tz" $path) }}
{{ regexReplaceAll "(.*)/" $path "" }}: |
{{ $.Files.Get $path | indent 4 }}
{{- end }}
{{- end }}
The Helm documentation references the Go "github.com/gobwas/glob" package for the supported glob syntax; that doesn't support any sort of "except" cases, except for single characters.
What you can do instead is iterate over all of the files, and then use a normal conditional to exclude the ones you don't want. You have to construct the ConfigMap structure yourself rather than relying on the AsConfig helper.
data:
{{- range $path, $_ := .Files.Glob "config/*" }}
{{- if not (hasSuffix ".tz" $path) }}
{{ $path }}: |
{{ .Files.Get $path | indent 4 }}
{{- end }}
{{- end }}
im currently going through the docs of helm, and there have been at least 3 different uses for the dot (.), is there any specific definition for it? and does it have something in common with the bash use (actual folder/files)?
some cases in the documentation
This print the accesed files in the range called before?
{{- $files := .Files }}
{{- range tuple "config1.toml" "config2.toml" "config3.toml" }}
{{ . }}: |-
{{ $files.Get . }}
{{- end }}
This tells "mychart.app" to use the files in the current folder (bash-like behaviour)
{{ include "mychart.app" . | indent 4 }}
and this, i guess it takes the values from the whole folder??? i guess this is not correct since is not working (it has been made by another employee back then and i have to fix it)
{{- define "read.select-annot" -}}
{{- range $key, $value := . }}
{{ $key }}: {{ $value }}
{{- end }}
{{- end }}
thanks for the help
In general, . in Helm templates has nothing to do with files or directories.
The Helm templating language uses Go's text/template system. There are a couple of different ways a period character can appear there.
First of all, . can be a character in a string:
{{- range tuple "config1.toml" "config2.toml" "config3.toml" }}
{{/* ^^^^^^^^^^^^
this is a literal string "config1.toml" */}}
...
{{- end }}
Secondly, . can be a lookup operator. There aren't any solid examples in your question, but a typical use is looking up in values. If your values.yaml file has
root:
key: value
then you can expand
{{ .Values.root.key }}
and the . before root and key navigates one level down in the dictionary structure.
The third use, and possibly the one that's confusing you, is that . on its own is a variable.
{{ . }}
You can do field lookups on it, and you have some examples of that: .Files is the same as index . "Files", and looks up the field "Files" on the object ..
You use . as a variable in several places:
{{- $files := .Files }} {{/* Get "Files" from . */}}
{{ . }} {{/* Write . as a value */}}
{{ include "mychart.app" . }} {{/* Pass . as the template parameter */}}
. is tricky in that it has somewhat contextual meaning:
At the top level, Helm initializes . to an object with keys Files, Release, Values, and Chart.
In a defined template, . is the parameter to the template. (So when you include or template, you need to pass . down as that parameter.)
In a range loop, . is the current item being iterated on.
In a with block, . is the matched item if it exists.
In particular, the interaction with range can be tricky. Let's look at a simplified version of your loop:
# {{ . }}
{{- range tuple "config1.toml" "config2.toml" "config3.toml" }}
- {{ . }}
{{- end }}
Outside the range loop, . is probably the top-level Helm object. But inside the range loop, . is the file name (each value from the tuple in turn). That's where you need to save values from . into local variables:
{{/* We're about to invalidate ., so save .Files into a variable. */}}
{{- $files := .Files }}
{{- range tuple "config1.toml" "config2.toml" "config3.toml" }}
{{/* This . is the filename from the "tuple" call */}}
{{ . }}: |-
{{/* Call .Get, from the saved $files, passing the filename .
as the parameter */}}
{{ $files.Get . }}
{{- end }}
I am new to helm and helm templating language. I have the following in my _helper.tpl:
{{/*
Get couchdb password
*/}}
{{- define "couchdb.password" -}}
{{- $name := default .Chart.Name .Values.nameOverride -}}
{{- printf "'#refsec/couchdb-%s/adminPassword'" $name -}}
{{- end -}}
{{/*
Get couchdb username
*/}}
{{- define "couchdb.username" -}}
{{- $name := default .Chart.Name .Values.nameOverride -}}
{{- printf "'#refsec/couchdb-%s/adminUsername'" $name -}}
{{- end -}}
But there is an issue with this piece of code. REPETTION!! As you see this line is repeated 2 times: {{- $name := default .Chart.Name .Values.nameOverride -}}
Can I define a global variable for these casesa and use it over and over without repetition?
In principle you can define a template just for that one line
{{- define "couchdb.chart.name" -}}
{{- default .Chart.Name .Values.nameOverride -}}
{{- end -}}
But the syntax to invoke it isn't actually much shorter
{{- define "couchdb.password" -}}
{{- $name := include "couchdb.chart.name" . -}}
{{- printf "'#refsec/couchdb-%s/adminPassword'" $name -}}
{{- end -}}
{{- define "couchdb.username" -}}
{{- printf "'#refsec/couchdb-%s/adminUsername'" (include "couchdb.chart.name" .) -}}
{{- end -}}
I'm pretty sure the Go text/template language doesn't have global variables in the form you're suggesting here. The documentation describes variable syntax and invocation, but contains the slightly cryptic note
A template invocation does not inherit variables from the point of its invocation.
and I think from reading other context there's not "global scope" so much as "a main template".
In any case, I've never seen a global variable in a Helm chart, and even variables in the form you show in the question are a little bit unusual.