Is it possible to filter the results of a range in helm?
For example the values file contains a list of maps like:
clients:
- name: clientA
id: id001
user: usernameA
pass: passwordA
- name: clientB
id: id002
user: usernameB
pass: passwordB
- name: clientA
id: id003
user: usernameA
pass: passwordA
In my template I need to extract only the unique values of user and pass.
I have something like this:
{{- range .Values.clients }}
- name: {{ .name | printf "user_%s" }}
valueFrom:
secretKeyRef:
name: somesecret
key: {{ .user | quote }}
- name: {{ .name | printf "pass_%s" }}
valueFrom:
secretKeyRef:
name: somesecret
key: {{ .pass | quote }}
{{- end }}
The end result should be something like:
- name: user_clientA
valueFrom:
secretKeyRef:
name: somesecret
key: usernameA
- name: pass_clientA
valueFrom:
secretKeyRef:
name: somesecret
key: passwordA
- name: user_clientB
valueFrom:
secretKeyRef:
name: somesecret
key: usernameB
- name: pass_clientB
valueFrom:
secretKeyRef:
name: somesecret
key: passwordB
I tried {{- range .Values.clients | pick "user" "pass" | uniq }} but doesn't seem to work. I don't know how to write this so i can filter just "user" and "pass" keys and discard the duplicates.
Even with its extensions, Helm doesn't support deep filtering of lists like you propose. There's not a great way to say "filter this list to the unique values of this map item", or "given an item, is it the first one in the list with some property". In a more functional language I could imagine using constructs like map or filter here, and Helm has no equivalents to these.
In practice the thing I'd be most likely to do is to declare this an invalid configuration. It would be lazy, but not unreasonable, to just execute the template as you've shown it. (If this is in a pod spec's env: block, I believe this would produce a valid object and the last setting of each environment variable takes effect.)
It's possible to do this checking in Helm templates; but it's a lot of code, in an unfamiliar language, that's hard to test. Also consider whether something like an operator could be a better implementation choice for you, since this can be written in a more standard language.
If you believe the length of the list will be reasonably short, you can write a recursive template to process this. It would receive the list of remaining items and the list of names it's already seen. If the list of items is empty, stop; if the current item has been seen, call ourselves with the rest of the list and the same seen list; otherwise emit the item, add it to the "seen" list, and call ourselves. In Python pseudocode this could look like:
def helper(remaining, seen):
if len(remaining) == 0:
# do nothing if the list is empty
return
else:
first = remaining[0]
name = first['name']
rest = remaining[1:]
if name in seen:
# we have already seen this item; skip it and go to the next one
return helper(rest, seen)
else:
# this is a new item; emit it and remember its name
emit(first)
new_seen = seen + [name]
return helper(rest, new_seen)
def process(the_list):
helper(the_list, [])
You can translate that logic to Helm templates, with the additional trick of packing the multiple parameters into a single list, since the Go text/template templates only take a single parameter. (If you need the top-level Helm object for .Values, .Release, .Files, etc. you need to pass that explicitly as well.)
{{- define "helper" -}}
{{- $remaining = index . 0 -}}
{{- $seen := index . 1 -}}
{{- if empty $remaining -}}
{{-/* do nothing */-}}
{{- else -}}
{{- $first := first $remaining -}}
{{- $name := $first.name -}}
{{- $rest := rest $remaining -}}
{{- if has $name $seen -}}
{{-/* recurse without emitting anything */-}}
{{- template "helper" (list $rest $seen) -}}
{{- else -}}
{{-/* emit the item */-}}
- name: user_{{ $first.name }}
valueFrom:
secretKeyRef:
name: somesecret
key: {{ quote $first.user }}
- name: pass_{{ $first.name }}
valueFrom:
secretKeyRef:
name: somesecret
key: {{ quote $first.pass }}
{{/* ...then move on to the next item */-}}
{{- $new_seen := append $seen $first.name -}}
{{- template "helper" (list $first $new_seen) -}}
{{- end -}}
{{- end -}}
{{- end -}}
env:
{{ include "helper" (list .Values.clients list) | indent 2 }}
Related
Assuming I have the following values.yaml for a subchart:
global:
ingress:
annotations:
nginx.ingress.kubernetes.io/configuration-snippet: |
add_header yyy "yyy";
tag: 123
port: 1234
ingress:
enabled: true
annotations:
nginx.ingress.kubernetes.io/configuration-snippet: |
add_header xxx "xxx";
...
how can I merge both annotation blocks in the ingress.yaml template together so that it results in:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: app-ingress
annotations: |
add_header xxx "xxx";
add_header yyy "yyy";
...
Has anyone a hint where to start here?
Both values are just strings, and you can either write them one after another or concatenate them at the template layer.
So, for example, a minimal thing that should come close to working, for this specific example, could be
{{- $key := "nginx.ingress.kubernetes.io/configuration-snippet" -}}
metadata:
annotations: |
{{ index .Values.ingress.annotations $key | trim | indent 4 }}
{{ index .Values.global.ingress.annotations $key | trim | indent 4 }}
This takes advantage of the fact that Helm doesn't really "understand" YAML at the templating layer; instead, a template writes out an arbitrary string and them Helm tries to parse it afterwards. So you can use the YAML | block scalar marker, and then write out arbitrary content under it, and so long as you get the indentation right it will work.
The question sounds like it's reaching for a more general question of how to merge the two annotation lists, combining individual values by concatenating the strings. Helm includes a set of dictionary template functions, which somewhat unusually work by mutating a dictionary in-place. You can combine this with the underdocumented toYaml function to write the dictionary in valid YAML syntax.
In pseudocode, I might write:
Create a new dictionary that's a copy of the local annotations.
Loop through the global annotations. For each, if the key does not exist, save its value, but if it does, append the global value.
Write the result as YAML.
You could translate this into Helm template code:
{{- $annotations := deepCopy .Values.ingress.annotations -}}
{{- range $key, $value := .Values.global.ingress.annotations -}}
{{- $existing := get $annotations $key -}}
{{- $new := cat $existing $value -}}
{{- $_ := set $annotations $key $new -}}
{{- end -}}
metadata:
annotations:
{{ $annotations | toYaml | indent 4 }}
In particular here I've taken advantage of the property that get returns an empty string if the key doesn't exist, which happens to be what you want in this particular case. For other "reduce values" type operations you might need to check if the value exists using hasKey, or use a default value.
I want to use the direct translation from k8s secret-keys to SpringBoot properties.
Therefore I have a helm chart (but similar with plain k8s):
apiVersion: v1
data:
app.entry[0].name: {{.Values.firstEntry.name | b64enc }}
kind: Secret
metadata:
name: my-secret
type: Opaque
With that my intention is that this behaves as if I'd set the spring property file:
app.entry[0].name: "someName"
But when I do this I get an error:
Invalid value: "[app.entry[0].name]": a valid config key must consist of alphanumeric characters, '-', '_' or '.' (e.g. 'key.name', or 'KEY_NAME', or 'key-name', regex used for validation is '[-._a-zA-Z0-9]+'),
So, [0] seems not to be allowed as a key name for the secrets.
Any idea how I can inject an array entry into spring directly from a k8s secret name?
Shooting around wildly I tried these that all failed:
app.entry[0].name: ... -- k8s rejects '['
app.entry__0.name: ... -- k8s ok, but Spring does not recognize this as array (I think)
"app.entry[0].name": ... -- k8s rejects '['
'app.entry[0].name': ... -- k8s rejects '['
You should be able to use environnment variables like described in sprint-boot-env.
app.entry[0].name property will be set using APP_ENTRY_0_NAME environment variable. This could be set in your deployment.
Using secret like:
apiVersion: v1
data:
value: {{.Values.firstEntry.name | b64enc }}
kind: Secret
metadata:
name: my-secret
type: Opaque
and then use it with
env:
- name: APP_ENTRY_0_NAME
valueFrom:
secretKeyRef:
name: my-secret
key: value
What you can do is passing the application.properties file specified within a k8s Secret to your Spring Boot application.
For instance, define your k8s Opaque Secret this way:
apiVersion: v1
kind: Secret
type: Opaque
metadata:
name: my-secret
data:
application.properties: "app.entry[0].name={{ .Values.firstEntry.name }}"
Of course you will have more properties that you want to set in your application.properties file, so just see this as an example with the type of entry that you need to specify, as stated in your question. I'm not a Spring Boot specialist, but an idea could be (if possible) to tell the Spring Boot application to look for more than a single application.properties file so that you would only need to pass some of the configuration parameters from the outside in instead of all of the parameters.
When using kubernetes secrets as files in pods, as specified within the official kubernetes documentation, each key in the secret data map becomes a filename under a volume mountpath (See point 4).
Hence, you can just mount the application.properties file defined within your k8s secret into your container in which your Spring Boot application is running. Assuming that you make use of a deployment template in your helm chart, here is a sample deployment.yaml template would do the job (please focus on the part where the volumes and volumeMount are specified):
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "sample.fullname" . }}
labels:
{{- include "sample.labels" . | nindent 4 }}
spec:
{{- if not .Values.autoscaling.enabled }}
replicas: {{ .Values.replicaCount }}
{{- end }}
selector:
matchLabels:
{{- include "sample.selectorLabels" . | nindent 6 }}
template:
metadata:
{{- with .Values.podAnnotations }}
annotations:
{{- toYaml . | nindent 8 }}
{{- end }}
labels:
{{- include "sample.selectorLabels" . | nindent 8 }}
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
serviceAccountName: {{ include "sample.serviceAccountName" . }}
securityContext:
{{- toYaml .Values.podSecurityContext | nindent 8 }}
containers:
- name: {{ .Chart.Name }}
securityContext:
{{- toYaml .Values.securityContext | nindent 12 }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- name: http
containerPort: 80
protocol: TCP
livenessProbe:
httpGet:
path: /
port: http
readinessProbe:
httpGet:
path: /
port: http
resources:
{{- toYaml .Values.resources | nindent 12 }}
volumeMounts:
- name: my-awesome-volume
mountPath: /path/where/springboot/app/expects/application.properties
subPath: application.properties
volumes:
- name: my-awesome-volume
secret:
secretName: my-secret
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}
As desired, this gives you a solution with no necessity of changing any of your application code. I hope that this gets you going in the intended way.
You can do saving json file as a secret
Step 1:
Create json file which needs to be stored as secret example : secret-data.json
{
"entry": [
{
"name1": "data1",
"key1": "dataX"
},
{
"name2": "data2",
"key2": "dataY"
}
]
}
Step2 : Create a secret from a file
kubectl create secret generic data-1 --from-file=secret-data.json
Step 3: Attach secret to pod
env:
- name: APP_DATA
valueFrom:
secretKeyRef:
name: data-1
key: secret-data.json
You can verify the same by exec into container and checking env
I have Following go Template code
custom_listeners: {{ range $cl := $.Vars.CustomListeners }}
{{ $cl.ListenerType }}:
{{ range $k,$v := $cl.Values }}{{ $k }}: "{{ $v }}"
{{ end }}{{ end }}
Which creates the following YAML file . However the looping through the services add extra line between the service. I have tried different method to remove this line but it messes the yml formatting.
custom_listeners:
service1:
name: "service1"
port: "8091"
ssl_enabled: "false"
<-------------------------- Extra Line
service2:
name: "service2"
port: "8092"
ssl_enabled: "false"
<-------------------------- Extra Line
service3:
name: "service3"
port: "9093"
ssl_enabled: "false"
Just wondering what would be the best to way to get Desired result below :
custom_listeners:
service1:
name: "service1"
port: "8091"
ssl_enabled: "false"
service2:
name: "service2"
port: "8092"
ssl_enabled: "false"
service3:
name: "service3"
port: "7093"
ssl_enabled: "false"
https://pkg.go.dev/text/template#hdr-Text_and_spaces
... if an action's left delimiter (by default "{{") is followed
immediately by a minus sign and white space, all trailing white space
is trimmed from the immediately preceding text. Similarly, if the
right delimiter ("}}") is preceded by white space and a minus sign,
all leading white space is trimmed from the immediately following
text. ...
custom_listeners:
{{- range $cl := $.Vars.CustomListeners }}
{{ $cl.ListenerType }}:
{{- range $k,$v := $cl.Values }}
{{ $k }}: "{{ $v }}"
{{- end }}
{{- end }}
https://go.dev/play/p/aZ7tNqV2Phq
I'm new to helm and I want to be able to write gitlab project variables to files using config maps and shared environment variables.
I have a set of environment variables defined as gitlab project variables (the gitlab runner exposes them as environment variables) for each environment (where <ENV> is DEV/TEST/PROD for the sake of brevity):
MYSQL_USER_<ENV> = "user"
MYSQL_PASSWORD_<ENV> = "pass"
In the helm chart every environment has a map of its variables. For example, values.<ENV>.yaml contains:
envVars:
MYSQL_USER: $MYSQL_USER_<ENV>
MYSQL_PASSWORD: $MYSQL_PASSWORD_<ENV>
values.yaml contains a Ruby file which will consume those variables:
files:
config.rb: |
mysql['username'] = ENV["MYSQL_USER"]
mysql['password'] = ENV["MYSQL_PASSWORD"]
configmap.env.yaml defines:
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "mychart.fullname" . }}-config-env
data:
{{- range $config_key, $config_value := .Values.envVars }}
{{ $config_key }}: {{ $config_value | quote | nindent 4 }}
{{- end }}
configmap.files.yaml defines:
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "mychart.fullname" . }}-config-volume
data:
{{- range $file_path, $file_content := .Values.files }}
{{ $file_path }}: |
{{ $file_content | indent 4 -}}
{{- end }}
Finally, the deployment of the config map (only the config map part is shown here and I'm not using secrets here because this question is long enough as it is):
volumes:
- name: {{ include "mychart.fullname" . }}-config-volume
configMap:
name: {{ include "mychart.fullname" . }}-config-volume
containers:
- name: {{ .Chart.Name }}
volumeMounts:
- name: {{ include "mychart.fullname" . }}-config-volume
mountPath: /etc/my-config-dir
envFrom:
- configMapRef:
name: {{ include "mychart.fullname" . }}-config-env
So, in one sentence the workflow should be:
MYSQL_USER/PASSWORD_<ENV> saved into MYSQL_USER/PASSWORD, which are then written to /etc/my-config-dir/config.rb
I can't seem to make the environment variables of values.yaml (MYSQL_USER, MYSQL_PASSWORD) get the value of the project variables (MYSQL_USER_<ENV>, MYSQL_PASSWORD_<ENV>).
I use helm 3, but {{ env "MYSQL_USER_<ENV>" }} fails.
I could use string interpolation with the environment variable's name it the Ruby file, but then I would have to know what environment variables should be created for every container.
I'm trying to avoid having multiple --set arguments. Also I'm not sure how envsubst can be used here...
Any help will be greatly appreciated, thanks!
So eventually I used envsubst:
script:
- VALUES_FILE=values.${ENV}.yaml
- envsubst < ${VALUES_FILE}.env > ${VALUES_FILE}
- helm upgrade ... -f ${VALUES_FILE}
I am new to Kubernetes, Helm, and Golang. Basically, I'm trying to concat strings in my values.yaml file to populate another value in my values.yaml file. I'm using _helpers.tpl to do this (in an attempt to understand Helm and Golang better). I could just write "users: "datafeed:password:::incoming" " in my values.yaml file and be done with it; but would like to avoid that.
I have the following in my values.yaml file:
sftp:
users: ""
username: "datafeed"
password: "password"
incoming: "incoming"
And want the final values.yaml file to read:
sftp:
users: "datafeed:password:::incoming"
username: "datafeed"
password: "password"
incoming: "incoming"
To do this, I am trying to edit the _helpers.tpl file. I have tried
{{- define "sftp.users" -}}
{{- .Values.sftp.users: .Values.sftp.username+":"+.Values.sftp.password+":::"+.Values.sftp.incoming -}}
{{- end -}}
and
{{- define "sftp.users" -}}
{{- .Values.sftp.users:= .Values.sftp.username+":"+.Values.sftp.password+":::"+.Values.sftp.incoming -}}
{{- end -}}
Then I tried making each segment a variable (and deleted the explicit values in the values.yaml file):
{{- define "sftp.users" -}}
{{ $username:= "datafeed" }}
{{ $password:= "password" }}
{{ $incoming:= "incoming" }}
{{- .Values.sftp.users= {{$username}}+":"+{{$password}}":::"+{{$incoming}} -}}
and then setting the fields/keys explicitly:
username: {{ .Values.sftp.username | default "datafeed" }}
password: {{ .Values.sftp.password | default "password" }}
incoming: {{ .Values.sftp.incoming | default "incoming" }}
{{- .Values.sftp.users:= username+":"+password+":::"+incoming -}}
and:
{{define "username"}}datafeed{{end}}
{{define "password"}}password{{end}}
{{define "incoming"}}incoming{{end}}
{{define "users"}}{{template "username"}}:{{template "password"}}:::{{template "incoming"}}{{end}}
{{- printf "users" -}}
{{- .Values.sftp.users: users -}}
I have also looked at previous posts:
Helm _helpers.tpl: Calling defined templates in other template definitions
Kubernetes Helm, combine two variables with a string in the middle
How to get values from values.yaml to _helpers.tpl in helm charts
None of this seems to work. I can't tell if it's my approach or my syntax. Probably both.