How not to overwrite randomly generated secrets in Helm templates - kubernetes

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

Related

Helm named template explodes with 'unsupported value: encountered a cycle via map[string]interface' if called more than once

I am trying to refactor a Helm chart for a large enterprise application with numerous deployments, services, ingresses etc and am trying to reduce copy-and-paste. Since I've not found any generally accepted DRY design patterns for Helm charts that have enterprise applications in mind, I am simply working from this guide as starting point: https://faun.pub/dry-helm-charts-for-micro-services-db3a1d6ecb80
I would like to define a named template for each high level resource, such as a deployment and call it as needed for each component in the application that I'm deploying. Using a simple ConfigMap as an example, I have come up with the following example that works perfectly as expected if I invoke the named template only once:
{{- define "mergeproblem.configmap" -}}
{{- $ := index . 0 -}}
{{- $name := index . 2 -}}
{{- with index . 1 -}}
{{- $this := dict "Values" (get .Values $name) -}}
{{- $defaultRoot := dict "Values" (omit .Values $name) -}}
{{- $noValues := omit . "Values" -}}
{{- with merge $noValues $this $defaultRoot -}}
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "mergeproblem.fullname" . }}-{{ $name }}
labels:
{{- include "mergeproblem.labels" . | nindent 4 }}
data:
MergeProblemExample: |
Values: {{ .Values | toYaml | nindent 6 }}
Template: {{ .Template | toYaml | nindent 6 }}
Chart: {{ .Chart | toYaml | nindent 6 }}
Release: {{ .Release | toYaml | nindent 6 }}
{{- end -}}
{{- end -}}
{{- end -}}
The idea here is that I can call this named template, giving it the name of the resource I want to create. It will pull the defaults from the top-level .Values but merge in the .Values.$name over the top with a higher precedence so that the template contents can be relatively simple without having to perform individual merges or ternaries on every variable interpolation -- instead the main scoped input will already have the merging done on .Values.
An example of the working output if I make only a single call: https://pastebin.com/ZCJ4LTpV
I am invoking it with the following:
{{- template "mergeproblem.configmap" (list $ . "myfoo") -}}
However if I invoke it more than once like so:
{{- template "mergeproblem.configmap" (list $ . "myfoo") -}}
{{- template "mergeproblem.configmap" (list $ . "yourbar") -}}
I receive the following error from Helm:
$ helm upgrade --install --namespace mergeproblem --debug mergeproblem ./mergeproblem/
history.go:56: [debug] getting history for release mergeproblem
upgrade.go:123: [debug] preparing upgrade for mergeproblem
upgrade.go:131: [debug] performing update for mergeproblem
upgrade.go:303: [debug] creating upgraded release for mergeproblem
Error: UPGRADE FAILED: create: failed to encode release "mergeproblem": json: unsupported value: encountered a cycle via map[string]interface {}
helm.go:81: [debug] json: unsupported value: encountered a cycle via map[string]interface {}
create: failed to encode release "mergeproblem"
helm.sh/helm/v3/pkg/storage/driver.(*Secrets).Create
/private/tmp/helm-20210414-93729-197z3ms/pkg/storage/driver/secrets.go:156
helm.sh/helm/v3/pkg/storage.(*Storage).Create
/private/tmp/helm-20210414-93729-197z3ms/pkg/storage/storage.go:69
helm.sh/helm/v3/pkg/action.(*Upgrade).performUpgrade
/private/tmp/helm-20210414-93729-197z3ms/pkg/action/upgrade.go:304
helm.sh/helm/v3/pkg/action.(*Upgrade).Run
/private/tmp/helm-20210414-93729-197z3ms/pkg/action/upgrade.go:132
main.newUpgradeCmd.func2
/private/tmp/helm-20210414-93729-197z3ms/cmd/helm/upgrade.go:155
github.com/spf13/cobra.(*Command).execute
/Users/brew/Library/Caches/Homebrew/go_mod_cache/pkg/mod/github.com/spf13/cobra#v1.1.1/command.go:850
github.com/spf13/cobra.(*Command).ExecuteC
/Users/brew/Library/Caches/Homebrew/go_mod_cache/pkg/mod/github.com/spf13/cobra#v1.1.1/command.go:958
github.com/spf13/cobra.(*Command).Execute
/Users/brew/Library/Caches/Homebrew/go_mod_cache/pkg/mod/github.com/spf13/cobra#v1.1.1/command.go:895
main.main
/private/tmp/helm-20210414-93729-197z3ms/cmd/helm/helm.go:80
runtime.main
/usr/local/Cellar/go/1.16.3/libexec/src/runtime/proc.go:225
runtime.goexit
/usr/local/Cellar/go/1.16.3/libexec/src/runtime/asm_amd64.s:1371
UPGRADE FAILED
main.newUpgradeCmd.func2
/private/tmp/helm-20210414-93729-197z3ms/cmd/helm/upgrade.go:157
github.com/spf13/cobra.(*Command).execute
/Users/brew/Library/Caches/Homebrew/go_mod_cache/pkg/mod/github.com/spf13/cobra#v1.1.1/command.go:850
github.com/spf13/cobra.(*Command).ExecuteC
/Users/brew/Library/Caches/Homebrew/go_mod_cache/pkg/mod/github.com/spf13/cobra#v1.1.1/command.go:958
github.com/spf13/cobra.(*Command).Execute
/Users/brew/Library/Caches/Homebrew/go_mod_cache/pkg/mod/github.com/spf13/cobra#v1.1.1/command.go:895
main.main
/private/tmp/helm-20210414-93729-197z3ms/cmd/helm/helm.go:80
runtime.main
/usr/local/Cellar/go/1.16.3/libexec/src/runtime/proc.go:225
runtime.goexit
/usr/local/Cellar/go/1.16.3/libexec/src/runtime/asm_amd64.s:1371
My values.yaml looks like this:
########
#
# Component specifics
#
myfoo:
image:
repository: REPOSITORY-FROM-MYFOO-BLOCK
tag: TAG-FROM-MYFOO-BLOCK
autoscaling:
minReplicas: 33
yourbar:
image:
repository: Repository-From-Yourbar-Block
tag: Tag-From-Yourbar-Block
########
#
# Example defaults
#
replicaCount: 1
image:
repository: default-repository-from-top-level-image-block
pullPolicy: IfNotPresent
# Overrides the image tag whose default is the chart appVersion.
tag: "default-tag-from-top-level-image-block"
imagePullSecrets: []
nameOverride: ""
fullnameOverride: ""
autoscaling:
enabled: false
minReplicas: 1
maxReplicas: 100
targetCPUUtilizationPercentage: 80
# targetMemoryUtilizationPercentage: 80

How to merge 2 lists together (of containers) in helm?

I'm trying to create my own chart library from tutorial: https://helm.sh/docs/topics/library_charts/ . In the tutorial, there is really nice function, which merges 2 yamls together:
{{- define "libchart.util.merge" -}}
{{- $top := first . -}}
{{- $overrides := fromYaml (include (index . 1) $top) | default (dict ) -}}
{{- $tpl := fromYaml (include (index . 2) $top) | default (dict ) -}}
{{- toYaml (merge $overrides $tpl) -}}
{{- end -}}
I define web deployment template as:
{{- define "libchart.web.tpl" -}}
(...)
containers:
- env:
envFrom:
- configMapRef:
name: app-configmap
- secretRef:
name: app-secrets
image: {{ include "libchart.image" . }}
imagePullPolicy: Always
name: web
resources: {}
(...)
{{- end -}}
{{- define "libchart.web" -}}
{{- include "libchart.util.merge" (append . "libchart.web.tpl") -}}
{{- end -}}
When I want to override a dictionary it's ok:
{{- include "libchart.web" (list . "web") -}}
{{- define "web" -}}
metadata:
annotations:
test1: "test1"
{{- end -}}
And now I want to override resources:
{{- include "libchart.web" (list . "web") -}}
{{- define "web" -}}
spec:
template:
spec:
containers:
- resources:
requests:
cpu: '0.01'
memory: 109.0Mi
{{- end -}}
And the error which I get is:
Error: unable to build kubernetes objects from release manifest: error validating "": error validating data: ValidationError(Deployment.spec.template.spec.containers[0]): missing required field "name" in io.k8s.api.core.v1.Container
Which means that it tries to create 2 elements on the container list.
I found closed issue on githug: https://github.com/helm/charts/issues/19855.

Helm template doesnt work when binded to configmap

I have a file which will be binded to a configmap. Once I put a tpl function in it, it stops working when other lines are included in the file.
I use this helper tpl:
{{- define "call-nested" }}
{{- $dot := index . 0 }}
{{- $subchart := index . 1 | splitList "." }}
{{- $template := index . 2 }}
{{- $values := $dot.Values }}
{{- range $subchart }}
{{- $values = index $values . }}
{{- end }}
{{- include $template (dict "Chart" (dict "Name" (last $subchart)) "Values" $values "Release" $dot.Release "Capabilities" $dot.Capabilities) }}
{{- end }}
testing in some.yaml:
psqlhost: {{include "call-nested" (list . "postgresql" "postgresql.fullname")}}
newlinekey: value
It works well if some.yaml is a standalone file. But once I bind it a configmap, it gives this error:
executing "mytestchart/templates/my-configmap.yaml" at <tpl (.Files.Glob "config/*").AsConfig .>: error calling tpl: Error during tpl function execution for "some.yaml:<br>\"name: {{include \\\"call-nested\\\" (list . \\\"postgresql\\\" \\\"postgresql.fullname\\\")}}\\r\\nnewlinekey:\n value\"\n": parse error in "mytestchart/templates/my-configmap.yaml": template: mytestchart/templates/my-configmap.yam:1: unexpected "\\" in operand
Once I remove the new line it will also work well.
edit: Configmap:
apiVersion: v1
kind: ConfigMap
metadata:
name: somename
data:
{{ tpl (.Files.Glob "config/*").AsConfig .| indent 2 }}
Your template is not rendering correctly. In order to make it work you need to change the following:
Add a .tmpl suffix to the file you'd like to put to your ConfigMap, for example: some.yaml.tmpl.
Set your ConfigMap's data: to: {{- tpl ((.Files.Glob "config/*.tmpl").AsConfig) . | indent 2 }}. Add a indentation to that line also.
Your ConfigMap would than look something like this:
apiVersion: v1
kind: ConfigMap
metadata:
name: somename
data:
{{- tpl ((.Files.Glob "config/*.tmpl").AsConfig) . | indent 2 }}
You can find more info with some examples here.
Please let me know if that helped.

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 }}

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

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! ;)