Helm template - how to use "if exists at least one of" in array? - kubernetes-helm

I'm trying to make a list of env vars from a values.yaml become a single secret.yaml file containing the list of envs of the type "secret". The idea is to only create this secret file if at least one of the types equals "secret".
Eg:
values.yaml
env:
- name: PLAIN_TEXT_ENV_VAR1
type: plain
value: text value
- name: PLAIN_TEXT_ENV_VAR2
type: plain
value: text value
- name: TOP_SECRET_ENV_VAR_1
type: secret
- name: TOP_SECRET_ENV_VAR_2
type: secret
- name: TOP_SECRET_ENV_VAR_3
type: secret
resulting secret.yaml
kind: Secret
metadata:
name: test
data:
TOP_SECRET_ENV_VAR_1: change_me
TOP_SECRET_ENV_VAR_2: change_me
TOP_SECRET_ENV_VAR_3: change_me
I've already tried to create some flow control using range to iterate, boolean variables and if statements, but go template seems to ignore my ifs after I change it to another value.
Now my secret template is like the one below.
{{ $flowcontrol := true -}}
{{ if $flowcontrol -}}
{{ range $env := $.Values.env -}}
{{ if eq $env.type "secret" -}}
apiVersion: v1
kind: Secret
metadata:
name: testsecret
data:
{{- range $env := $.Values.env }}
{{- if eq $env.type "secret" }}
{{ $env.name }}: "change_me"
{{- end }}
{{- end }}
{{ $flowcontrol := false }}
{{ end -}}
{{ end -}}
{{ end -}}
It results in three replicated secret.yaml files with 3 variables:
$ helm template .
---
# Source: teste/templates/secret.yaml
apiVersion: v1
kind: Secret
metadata:
name: testsecret
data:
TOP_SECRET_ENV_VAR_1: "change_me"
TOP_SECRET_ENV_VAR_2: "change_me"
TOP_SECRET_ENV_VAR_3: "change_me"
apiVersion: v1
kind: Secret
metadata:
name: testsecret
data:
TOP_SECRET_ENV_VAR_1: "change_me"
TOP_SECRET_ENV_VAR_2: "change_me"
TOP_SECRET_ENV_VAR_3: "change_me"
apiVersion: v1
kind: Secret
metadata:
name: testsecret
data:
TOP_SECRET_ENV_VAR_1: "change_me"
TOP_SECRET_ENV_VAR_2: "change_me"
TOP_SECRET_ENV_VAR_3: "change_me"
How can one control the flow as: if the first one item of a list satisfies the condition, iterate through the rest of the same list only in another section after?

I've managed to create this feature! :D
Basicaly i've created a "template function" using define which iterates the env list from values.yaml and writes a string containing only the envs which the type property matches the word "secret".
Then I call that function using include and assign it's output to a variable.
If the variable length is greater than 0 (meaning it's not an empty string), the secret file is created and then I use the same string to fill the data property.
Here's the code containing the function and the secret template:
{{- define "get-secrets-from-env-list" -}}
{{- $allenv := index . 0 -}}
{{- range $i, $scrts := $allenv -}}
{{- if eq $scrts.type "secret" -}}
{{- nindent 0 $scrts.name -}}: {{ "change_me" | b64enc -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{ $secrets := include "get-secrets-from-env-list" (list .Values.env ) }}
{{- if gt (len $secrets) 0 -}}
apiVersion: v1
kind: Secret
type: Opaque
metadata:
name: {{ include "awesome-chart.fullname" $ }}
labels:
app.kubernetes.io/name: {{ include "awesome-chart.name" $ }}
helm.sh/chart: {{ include "awesome-chart.chart" $ }}
app.kubernetes.io/instance: {{ $.Release.Name }}
app.kubernetes.io/managed-by: {{ $.Release.Service }}
data:
{{- nindent 2 $secrets -}}
{{- end -}}%
Here's a sample output:
$ helm template .
---
# Source: awesome-chart/templates/secret.yaml
apiVersion: v1
kind: Secret
type: Opaque
metadata:
name: my-app
labels:
app.kubernetes.io/name: awesome-chart
helm.sh/chart: awesome-chart
app.kubernetes.io/instance: release-name
app.kubernetes.io/managed-by: Tiller
data:
TOP_SECRET_ENV_VAR_1: Y2hhbmdlX21l
TOP_SECRET_ENV_VAR_2: Y2hhbmdlX21l
TOP_SECRET_ENV_VAR_3: Y2hhbmdlX21l
Works like a charm! ;)

Related

helm to create single configmap with different labels

can anyone help me creating configmaps in helm with different labels.
I have 6 configmaps each have their own labels which will be used by pods. I would like to have the same labels in helm as well. Is it possible in Helm?
These are my values for configmap. I want to render these values and create separate configmap.
values.yaml/
configmaptestingforhelm:
configmapref1:
confname: serviceaccount-config
key: serviceaccount.test.path
value: /root/test_key.json
configmapref2:
confname: host-credentials-config
key: secret.credentials.host.path
value: /root/host_key.json
I am trying to create separate configmap for each configmappref* as below
configmap1
apiVersion: v1
kind: ConfigMap
{{- range $value := .Values.configmap.configmapref1 }}
metadata:
name: {{ $value.confname }}
data:
{{ $value.key }} : {{ $value.value }}
{{- end}}
configmap2
apiVersion: v1
kind: ConfigMap
{{- range $value := .Values.configmap.configmapref2 }}
metadata:
name: {{ $value.confname }}
data:
{{ $value.key }} : {{ $value.value }}
{{- end}}
My expected output is as below ,but I am not getting the desired output
apiVersion: v1
data:
secret.credentials.host.path: /root/host_key.json
kind: ConfigMap
metadata:
name: host-credentials-config
apiVersion: v1
data:
serviceaccount.test.path: /root/test_key.json
kind: ConfigMap
metadata:
name: serviceaccount-config
values.yaml
testLables:
cm0:
a: b
c: d
cm1:
e: f
g: k
templates/configmap.yaml
---
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "test.fullname" . }}-0
labels:
{{- include "test.labels" . | nindent 4 }}
{{- range $k, $v := .Values.testLables.cm0 }}
{{ $k }}: {{ $v }}
{{- end }}
data:
data: |-
k: v
---
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "test.fullname" . }}-1
labels:
{{- include "test.labels" . | nindent 4 }}
{{- range $k, $v := .Values.testLables.cm1 }}
{{ $k }}: {{ $v }}
{{- end }}
data:
data: |-
k: v
cmd:
helm template --debug test .
output
---
# Source: test/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: test-0
labels:
helm.sh/chart: test-0.1.0
app.kubernetes.io/name: test
app.kubernetes.io/instance: test
app.kubernetes.io/version: "1.18.0"
app.kubernetes.io/managed-by: Helm
a: b
c: d
data:
data: |-
k: v
---
# Source: test/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: test-1
labels:
helm.sh/chart: test-0.1.0
app.kubernetes.io/name: test
app.kubernetes.io/instance: test
app.kubernetes.io/version: "1.18.0"
app.kubernetes.io/managed-by: Helm
e: f
g: k
data:
data: |-
k: v
update:
values.yaml
configmaptestingforhelm:
configmapref1:
confname: serviceaccount-config
key: serviceaccount.test.path
value: /root/test_key.json
configmapref2:
confname: host-credentials-config
key: secret.credentials.host.path
value: /root/host_key.json
templates/configmap.yaml
{{- range $k, $cm := .Values.configmaptestingforhelm }}
---
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ $cm.confname }}
data:
{{ $cm.key }}: {{ $cm.value }}
{{- end }}
cmd
helm template --debug test .
output:
---
apiVersion: v1
kind: ConfigMap
metadata:
name: serviceaccount-config
data:
serviceaccount.test.path: /root/test_key.json
---
apiVersion: v1
kind: ConfigMap
metadata:
name: host-credentials-config
data:
secret.credentials.host.path: /root/host_key.json

Helm - Check if value not exists OR part of list

Assuming I have this values.yaml under my helm chart -
tasks:
- name: test-production-dev
env:
- production
- dev
- name: test-dev
env:
- dev
- name: test-all
environment_variables:
STAGE: dev
I would like to run my cronjob based on these values -
if .env doesn't exist - run any time.
if .env exists - run only if environment_variables.STAGE is in the .env list.
This is what I've done so far ( with no luck ) -
{{- range $.Values.tasks}}
# check if $value.env not exists OR contains stage
{{if or .env (hasKey .env "$.Values.environment_variables.STAGE") }}
apiVersion: batch/v1
kind: CronJob
...
{{- end}}
---
{{- end}}
values.yaml
tasks:
- name: test-production-dev
env:
- production
- dev
- name: test-dev
env:
- dev
- name: test-all
- name: test-production
env:
- production
environment_variables:
STAGE: dev
template/xxx.yaml
plan a
...
{{- range $.Values.tasks }}
{{- $flag := false }}
{{- if .env }}
{{- range .env }}
{{- if eq . $.Values.environment_variables.STAGE }}
{{- $flag = true }}
{{- end }}
{{- end }}
{{- else }}
{{- $flag = true }}
{{- end }}
{{- if $flag }}
apiVersion: batch/v1
kind: CronJob
meta:
name: {{ .name }}
{{- end }}
{{- end }}
...
plan b
...
{{- range $.Values.tasks }}
{{- if or (not .env) (has $.Values.environment_variables.STAGE .env) }}
apiVersion: batch/v1
kind: CronJob
meta:
name: {{ .name }}
{{- end }}
{{- end }}
...
output
...
apiVersion: batch/v1
kind: CronJob
meta:
name: test-production-dev
apiVersion: batch/v1
kind: CronJob
meta:
name: test-dev
apiVersion: batch/v1
kind: CronJob
meta:
name: test-all
...

helm helpers file can't evaluate field type interface array/string

I am rather new to helm, and I am trying to create a chart, but running into values not transforming from the values.yaml file into my generated chart.
here are my values.yaml
apiVersion: security.istio.io/v1alpha2
kind: RequestAuthentication
metadata:
name: name01
namespace: ns-01
spec:
selector:
matchLabels:
app: app-label
jwtRules:
- issuer: foo
jwksUri: bar
forwardOriginalToken: true
audiences:
- user1
- user2
then with my helm template:
apiVersion: networking.istio.io/v1alpha2
kind: RequestAuthentication
metadata:
name: name01
spec:
selector:
matchLabels:
app: {{ .Values.spec.selector.matchLabels.app }}
jwtRules:
- issuer: foo
jwksUri: bar
forwardOriginalToken: true
audiences: |-
{{- range .Values.spec.jwtRules.audiences }}
- {{ . | title | quote }}
{{ end }}
---
I also have a helpers file.
_helpers.tpl
{{/* vim: set filetype=mustache: */}}
{{- define "jwtRules.audiences" -}}
{{- range $.Values.spec.jwtRules.audiences }}
audiences:
- {{ . | quote }}
{{- end }}
{{- end }}
the error its producing: at <.Values.spec.jwtRules.audiences>: can't evaluate field audiences in type interface {}
This one is simple - you don't have a spec.jwtRules.audiences in your values file! jwtRules contains an array, so you'll have to use some index or iterate over it. Also, i don't think that neither your indentation, nor using of |- for audiences is correct, per docs it should be an array of strings.
So i came up with this example (your values are unchanged):
apiVersion: networking.istio.io/v1alpha2
kind: RequestAuthentication
metadata:
name: name01
spec:
selector:
matchLabels:
app: {{ .Values.spec.selector.matchLabels.app }}
jwtRules:
- issuer: foo
jwksUri: bar
forwardOriginalToken: true
audiences:
{{- with (first .Values.spec.jwtRules) }}
{{- range .audiences }}
- {{ . | title | quote -}}
{{- end }}
{{- end }}
renders into:
apiVersion: networking.istio.io/v1alpha2
kind: RequestAuthentication
metadata:
name: name01
spec:
selector:
matchLabels:
app: app-label
jwtRules:
- issuer: foo
jwksUri: bar
forwardOriginalToken: true
audiences:
- "User1"
- "User2"
In this case it uses a first element of array
Thank you #andrew. I also came to a simple solution but would like feedback on it.
I removed the helpers file then modified my helm chart with the following.
values.yaml (is kept the same as above)
helmchart:
apiVersion: networking.istio.io/v1alpha2
kind: RequestAuthentication
metadata:
name: name01
spec:
selector:
matchLabels:
app: {{ .Values.spec.selector.matchLabels.app }}
jwtRules:
- issuer: foo
jwksUri: bar
forwardOriginalToken: true
{{- with index .Values.spec.jwtRules 0 }}
audiences:
{{- range $a := .audiences }}
- {{ $a -}}
{{ end }}
{{ end }}
---
This produces the following:
apiVersion: networking.istio.io/v1alpha2
kind: RequestAuthentication
metadata:
name: name01
spec:
selector:
matchLabels:
app: app-label
jwtRules:
- issuer: foo
jwksUri: bar
forwardOriginalToken: true
audiences:
- user1
- user2
Should I continue to use a helpers file?
Thanks again for all the help.

How to compare a value to a string with go templating

I want to loop through a values file to create a namespace and a networkpolicy in/for that namespace, except for default. I only want to create the policy and not the namespace for default since it is there by default.
values file:
namespaces:
- name: default
- name: test1
- name: test2
template file:
# Loop through the namespace names and create the namespaces
{{- range $namespaces := .Values.namespaces }}
{{- if ne "default" }}
apiVersion: v1
kind: Namespace
metadata:
name: {{ $namespaces.name }}
---
{{- end }}
{{- end }}
# Loop through the namespace names and create a network policy for those namespace
{{- range $namespaces := .Values.namespaces }}
---
kind: NetworkPolicy
apiVersion: networking.k8s.io/v1
metadata:
name: {{ $namespaces.name }}-networkpolicy
namespace: {{ $namespaces.name }}
spec:
podSelector: {}
ingress:
- from:
- namespaceSelector:
matchLabels:
name: {{ $namespaces.name }}
---
{{- end }}
The error I get is:
Error: UPGRADE FAILED: template: namespaces/templates/namespaces.yaml:3:7: executing "namespaces/templates/namespaces.yaml" at <ne>: wrong number of args for ne: want 2 got 1
It's probably something simple, but not seeing it. Hope someone can help.
This worked for me:
# Loop through the namespace names and create the namespaces
{{- range $namespaces := .Values.namespaces }}
{{- if ne $namespaces.name "default" }}
apiVersion: v1
kind: Namespace
metadata:
name: {{ $namespaces.name }}
---
{{- end }}
{{- end }}
# Loop through the namespace names and create a network policy for those namespace
{{- range $namespaces := .Values.namespaces }}
---
kind: NetworkPolicy
apiVersion: networking.k8s.io/v1
metadata:
name: {{ $namespaces.name }}-networkpolicy
namespace: {{ $namespaces.name }}
spec:
podSelector: {}
ingress:
- from:
- namespaceSelector:
matchLabels:
name: {{ $namespaces.name }}
---
{{- end }}

How not to overwrite randomly generated secrets in Helm templates

I want to generate a password in a Helm template, this is easy to do using the randAlphaNum function. However the password will be changed when the release is upgraded. Is there a way to check if a password was previously generated and then use the existing value? Something like this:
apiVersion: v1
kind: Secret
metadata:
name: db-details
data:
{{ if .Secrets.db-details.db-password }}
db-password: {{ .Secrets.db-details.db-password | b64enc }}
{{ else }}
db-password: {{ randAlphaNum 20 | b64enc }}
{{ end }}
You can build on shaunc's idea to use the lookup function to fix the original poster's code like this:
apiVersion: v1
kind: Secret
metadata:
name: db-details
data:
{{- if .Release.IsInstall }}
db-password: {{ randAlphaNum 20 | b64enc }}
{{ else }}
# `index` function is necessary because the property name contains a dash.
# Otherwise (...).data.db_password would have worked too.
db-password: {{ index (lookup "v1" "Secret" .Release.Namespace "db-details").data "db-password" }}
{{ end }}
Only creating the Secret when it doesn't yet exist won't work because Helm will delete objects that are no longer defined during the upgrade.
Using an annotation to keep the object around has the disadvantage that it will not be deleted when you delete the release with helm delete ....
I've got a lot of trouble with the answers from Jan Dubois and shaunc. So I built a combined solution.
The downside of Jan's answer: It leads to errors, when it is used with --dry-run.
The downside of shaunc's answer: It won't work, because the resources will be deleted on helm upgrade.
Here is my code:
# store the secret-name as var
# in my case, the name was very long and containing a lot of fields
# so it helps me a lot
{{- $secret_name := "your-secret-name" -}}
apiVersion: v1
kind: Secret
metadata:
name: {{ $secret_name }}
data:
# try to get the old secret
# keep in mind, that a dry-run only returns an empty map
{{- $old_sec := lookup "v1" "Secret" .Release.Namespace $secret_name }}
# check, if a secret is already set
{{- if or (not $old_sec) (not $old_sec.data) }}
# if not set, then generate a new password
db-password: {{ randAlphaNum 20 | b64enc }}
{{ else }}
# if set, then use the old value
db-password: {{ index $old_sec.data "db-password" }}
{{ end }}
It's still one of the biggest issues of Helm. As far as I understand no good solution is available yet (see https://github.com/helm/charts/issues/5167).
One dirty workaround is to create secret as pre-install hook. Obvious downside of this approach is that secret will not be deleted on helm delete.
apiVersion: v1
kind: Secret
metadata:
name: {{ template "helm-random-secret.fullname" . }}
annotations:
"helm.sh/hook": "pre-install"
"helm.sh/hook-delete-policy": "before-hook-creation"
labels:
app: {{ template "helm-random-secret.name" . }}
chart: {{ template "helm-random-secret.chart" . }}
release: {{ .Release.Name }}
heritage: {{ .Release.Service }}
data:
some-password: {{ default (randAlphaNum 10) .Values.somePassword | b64enc | quote }}
Building on shaunc's idea to use the lookup function, I've created the following template:
{{/*
Returns a secret if it already in Kubernetes, otherwise it creates
it randomly.
*/}}
{{- define "getOrGeneratePass" }}
{{- $len := (default 16 .Length) | int -}}
{{- $obj := (lookup "v1" .Kind .Namespace .Name).data -}}
{{- if $obj }}
{{- index $obj .Key -}}
{{- else if (eq (lower .Kind) "secret") -}}
{{- randAlphaNum $len | b64enc -}}
{{- else -}}
{{- randAlphaNum $len -}}
{{- end -}}
{{- end }}
Then you can simply configure secrets like:
---
apiVersion: v1
kind: Secret
metadata:
name: my-secret
type: Opaque
data:
PASSWORD: "{{ include "getOrGeneratePass" (dict "Namespace" .Release.Namespace "Kind" "Secret" "Name" "my-secret" "Key" "PASSWORD") }}"
You can use the lookup function and skip generation if secret already exists:
{{- if not (lookup "v1" "secret" .Release.Namespace "db-details") -}}
<create secret here>
{{- end -}}
The actual tools are all here. My workaround is just another combination of suggested tools
{{- if not (lookup "v1" "Secret" .Release.Namespace "mysecret") }}
apiVersion: v1
kind: Secret
metadata:
name: mysecret
annotations:
"helm.sh/resource-policy": "keep"
type: Opaque
stringData:
password: {{ randAlphaNum 24 }}
{{- end }}
So if there is no such secret, it will be created. If the secret is present, it will be removed from the chart, but not from the cluster, the "helm.sh/resource-policy": "keep" will prevent it.
You may ask (as someone already did above) why lookup, not .Release.IsUpdate. Imagine the situation: your secret is a password to a database. You keep the data in the persistent volume, the claim for which is also annotated by "helm.sh/resource-policy": "keep", so if you even uninstall and reinstall the chart, the data would persist. If you do so with .Release.IsUpdate as condition, then you password will be recreated, the old password will be lost and you will loose the access to your data. If you query for the secret existence, it won't happen.
I've rewritten kubernetes replicator and added some annotations to deal with this kind of problems: https://github.com/olli-ai/k8s-replicator#use-random-password-generated-by-an-helm-chart
Now can generate a random password with helm, and replicate it only once to another secret thus it won't be change by helm in the future.
apiVersion: v1
kind: Secret
type: Opaque
metadata:
name: admin-password-source
annotations:
k8s-replicator/replicate-to: "admin-password"
k8s-replicator/replicate-once: "true"
stringData:
password: {{ randAlphaNum 64 | quote }}
Hope it will help people.
You can leverage definitions in the _helpers.tpl
_helpers.tpl
{{/*
Create the secret name
*/}}
{{- define "mssql-server.secretName" -}}
{{- include "mssql-server.name" . }}-mssql-secret
{{- end }}
{{/*
Get sa password value
*/}}
{{- define "mssql-server.sapassword" -}}
{{- if .Release.IsInstall -}}
{{ .Values.sa_password | default (randAlphaNum 20) | b64enc | quote }}
{{- else -}}
{{ index (lookup "v1" "Secret" .Release.Namespace (include "mssql-server.secretName" .)).data "sa_password" }}
{{- end }}
{{- end }}
secret.yaml
apiVersion: v1
kind: Secret
metadata:
name: {{ include "mssql-server.secretName" . }}
labels:
{{- include "mssql-server.labels" . | nindent 4 }}
type: Opaque
data:
sa_password: {{ include "mssql-server.sapassword" . }}
A bit late here, and most people may just catch it in the documentation:
helm does this for you with the annotation "helm.sh/resource-policy": keep
see:
https://helm.sh/docs/howto/charts_tips_and_tricks/#tell-helm-not-to-uninstall-a-resource