Skip to content

Commit 900756d

Browse files
sebastianiv21claude
andcommitted
feat(helm): add Redis auth support with password bootstrap
Enable authenticated Redis in the Helm chart. A pre-install/pre-upgrade hook Job generates the Redis password Secret (referenced by redis.auth.existingSecret) when it doesn't already exist, keeping the password stable across upgrades and ArgoCD re-syncs. - Add redisMasterHost/redisSecretName/redisPasswordInitImage helpers - Assemble an authenticated APPSMITH_REDIS_URL from the Secret, injected by reference so cleartext never lands in the ConfigMap; skipped when the user supplies their own APPSMITH_REDIS_URL - Gate the init-container ping and ConfigMap URL on redis.auth.enabled - Add redisAuth.passwordInit image config and values.schema.json entries - Add redis_auth_test snapshot/test coverage Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent f0b5b2c commit 900756d

8 files changed

Lines changed: 578 additions & 7 deletions

File tree

deploy/helm/templates/_helpers.tpl

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,33 @@ Uses existing secret if provided, otherwise derives from the CR name
229229
{{- end -}}
230230
{{- end -}}
231231

232+
{{/*
233+
Redis: password secret name
234+
Uses existing secret if provided, otherwise derives "{release}-redis-secret"
235+
*/}}
236+
{{- define "appsmith.redisSecretName" -}}
237+
{{- .Values.redis.auth.existingSecret | default (printf "%s-redis-secret" .Release.Name) -}}
238+
{{- end -}}
239+
240+
{{/*
241+
Redis: master service hostname (FQDN inside the cluster).
242+
Derived from the release name to stay uniform with the chart's other components.
243+
Assumes the release name does not contain "redis" (otherwise the Bitnami subchart
244+
collapses its fullname to just the release name and this host would not match).
245+
*/}}
246+
{{- define "appsmith.redisMasterHost" -}}
247+
{{- printf "%s-redis-master.%s.svc.cluster.local" .Release.Name (include "appsmith.namespace" .) -}}
248+
{{- end -}}
249+
250+
{{/*
251+
Redis: kubectl image used by the password-init Job.
252+
Kept independent from the MongoDB equivalent so the two bootstraps don't share config.
253+
*/}}
254+
{{- define "appsmith.redisPasswordInitImage" -}}
255+
{{- $img := .Values.redisAuth.passwordInit.image -}}
256+
{{- printf "%s/%s:%s" $img.registry $img.repository $img.tag -}}
257+
{{- end -}}
258+
232259
{{/*
233260
Renders a value that contains template.
234261
*/}}

deploy/helm/templates/configMap.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,8 @@ data:
3737
{{- end }}
3838

3939
{{- if and (eq "APPSMITH_REDIS_URL" $key) ( not $value) }}
40-
{{- if $.Values.redis.enabled }}
41-
{{ $key }}: redis://{{ $releaseName }}-redis-master.{{ $nameSpace }}.svc.cluster.local:6379
40+
{{- if and $.Values.redis.enabled (not $.Values.redis.auth.enabled) }}
41+
{{ $key }}: redis://{{ include "appsmith.redisMasterHost" $ }}:6379
4242
{{- end }}
4343
{{- end }}
4444

deploy/helm/templates/deployment.yaml

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,15 @@ spec:
7979
{{- else }}
8080
image: "{{ .Values.redis.image.registry }}/{{ .Values.redis.image.repository }}:{{ .Values.redis.image.tag }}"
8181
{{- end }}
82-
command: ['sh', '-c', "until redis-cli -h {{.Release.Name}}-redis-master.{{.Release.Namespace}}.svc.cluster.local ping ; do echo waiting for redis; sleep 2; done"]
82+
command: ['sh', '-c', "until redis-cli -h {{ include "appsmith.redisMasterHost" . }} ping ; do echo waiting for redis; sleep 2; done"]
83+
{{- if .Values.redis.auth.enabled }}
84+
env:
85+
- name: REDISCLI_AUTH
86+
valueFrom:
87+
secretKeyRef:
88+
name: {{ include "appsmith.redisSecretName" . }}
89+
key: {{ .Values.redis.auth.existingSecretPasswordKey }}
90+
{{- end }}
8391
{{- end }}
8492
{{- if .Values.mongodb.enabled }}
8593
- name: mongo-init-container
@@ -175,6 +183,21 @@ spec:
175183
name: {{ include "appsmith.mongoOperatorSecretName" . }}
176184
key: connectionString.standardSrv
177185
{{- end }}
186+
{{- if and .Values.redis.enabled .Values.redis.auth.enabled (not .Values.applicationConfig.APPSMITH_REDIS_URL) }}
187+
# Assemble the authenticated Redis URL for the bundled subchart. Skipped when the
188+
# user supplied applicationConfig.APPSMITH_REDIS_URL (which is honored via the
189+
# ConfigMap) — an explicit env entry here would otherwise take precedence over
190+
# envFrom and silently override it. The password is pulled from the Secret and
191+
# injected by reference, so the cleartext never lands in the ConfigMap; order
192+
# matters — the password var must precede the URL that references it via $(VAR).
193+
- name: APPSMITH_REDIS_PASSWORD
194+
valueFrom:
195+
secretKeyRef:
196+
name: {{ include "appsmith.redisSecretName" . }}
197+
key: {{ .Values.redis.auth.existingSecretPasswordKey }}
198+
- name: APPSMITH_REDIS_URL
199+
value: "redis://:$(APPSMITH_REDIS_PASSWORD)@{{ include "appsmith.redisMasterHost" . }}:6379"
200+
{{- end }}
178201
envFrom:
179202
- configMapRef:
180203
name: {{ include "appsmith.fullname" . }}
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
{{/*
2+
Creates the Redis password secret via a pre-install/pre-upgrade Job when the
3+
bundled redis subchart is enabled with auth on.
4+
5+
The Job is idempotent: if the secret already exists, it exits without change.
6+
This keeps the password stable across Helm upgrades and ArgoCD re-syncs —
7+
unlike a template-based `randAlphaNum` approach, which changes on every render
8+
and would either lock clients out or force an ArgoCD ignoreDifferences rule.
9+
10+
The runtime "secret already exists -> exit 0" check is also the migration and
11+
bring-your-own-secret guard: an upgrade of an install that already has the
12+
secret (or a user who pre-created their own) is left untouched.
13+
14+
The resulting Secret has no Helm release labels/annotations and no
15+
ownerReferences, so ArgoCD does not track or diff it.
16+
*/}}
17+
{{- if and .Values.redis.enabled .Values.redis.auth.enabled }}
18+
{{- $secretName := include "appsmith.redisSecretName" . -}}
19+
{{- $passwordKey := .Values.redis.auth.existingSecretPasswordKey -}}
20+
{{- $jobName := printf "%s-redis-password-init" (include "appsmith.fullname" .) | trunc 63 | trimSuffix "-" -}}
21+
{{- $namespace := include "appsmith.namespace" . -}}
22+
---
23+
apiVersion: v1
24+
kind: ServiceAccount
25+
metadata:
26+
name: {{ $jobName }}
27+
namespace: {{ $namespace }}
28+
labels:
29+
{{- include "appsmith.labels" . | nindent 4 }}
30+
annotations:
31+
helm.sh/hook: pre-install,pre-upgrade
32+
helm.sh/hook-weight: "-5"
33+
helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded
34+
---
35+
apiVersion: rbac.authorization.k8s.io/v1
36+
kind: Role
37+
metadata:
38+
name: {{ $jobName }}
39+
namespace: {{ $namespace }}
40+
labels:
41+
{{- include "appsmith.labels" . | nindent 4 }}
42+
annotations:
43+
helm.sh/hook: pre-install,pre-upgrade
44+
helm.sh/hook-weight: "-5"
45+
helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded
46+
rules:
47+
# Check whether the password secret already exists (scoped to one name).
48+
- apiGroups: [""]
49+
resources: ["secrets"]
50+
resourceNames: [{{ $secretName | quote }}]
51+
verbs: ["get"]
52+
# Create it if it doesn't. resourceNames can't scope `create` (the name
53+
# is in the request body, not the URL), so this is namespace-scoped.
54+
- apiGroups: [""]
55+
resources: ["secrets"]
56+
verbs: ["create"]
57+
---
58+
apiVersion: rbac.authorization.k8s.io/v1
59+
kind: RoleBinding
60+
metadata:
61+
name: {{ $jobName }}
62+
namespace: {{ $namespace }}
63+
labels:
64+
{{- include "appsmith.labels" . | nindent 4 }}
65+
annotations:
66+
helm.sh/hook: pre-install,pre-upgrade
67+
helm.sh/hook-weight: "-5"
68+
helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded
69+
subjects:
70+
- kind: ServiceAccount
71+
name: {{ $jobName }}
72+
namespace: {{ $namespace }}
73+
roleRef:
74+
kind: Role
75+
name: {{ $jobName }}
76+
apiGroup: rbac.authorization.k8s.io
77+
---
78+
apiVersion: batch/v1
79+
kind: Job
80+
metadata:
81+
name: {{ $jobName }}
82+
namespace: {{ $namespace }}
83+
labels:
84+
{{- include "appsmith.labels" . | nindent 4 }}
85+
annotations:
86+
helm.sh/hook: pre-install,pre-upgrade
87+
helm.sh/hook-weight: "0"
88+
helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded
89+
spec:
90+
ttlSecondsAfterFinished: 60
91+
backoffLimit: 3
92+
template:
93+
metadata:
94+
labels:
95+
{{- include "appsmith.labels" . | nindent 8 }}
96+
spec:
97+
serviceAccountName: {{ $jobName }}
98+
restartPolicy: Never
99+
{{- if .Values.image.pullSecrets }}
100+
imagePullSecrets:
101+
- name: {{ .Values.image.pullSecrets }}
102+
{{- end }}
103+
containers:
104+
- name: create-password
105+
image: {{ include "appsmith.redisPasswordInitImage" . | quote }}
106+
imagePullPolicy: {{ .Values.redisAuth.passwordInit.image.pullPolicy }}
107+
command:
108+
- /bin/sh
109+
- -c
110+
- |
111+
set -eu
112+
SECRET_NAME={{ $secretName | quote }}
113+
NAMESPACE={{ $namespace | quote }}
114+
PASSWORD_KEY={{ $passwordKey | quote }}
115+
if kubectl -n "$NAMESPACE" get secret "$SECRET_NAME" >/dev/null 2>&1; then
116+
echo "Secret $SECRET_NAME already exists; leaving it untouched."
117+
exit 0
118+
fi
119+
PASSWORD=$(tr -dc 'A-Za-z0-9' </dev/urandom | head -c 24)
120+
kubectl -n "$NAMESPACE" create secret generic "$SECRET_NAME" \
121+
--from-literal="$PASSWORD_KEY"="$PASSWORD"
122+
echo "Created secret $SECRET_NAME."
123+
securityContext:
124+
allowPrivilegeEscalation: false
125+
readOnlyRootFilesystem: true
126+
runAsNonRoot: true
127+
runAsUser: 1000
128+
capabilities:
129+
drop: ["ALL"]
130+
{{- end }}

deploy/helm/tests/__snapshot__/defaults_snapshot_test.yaml.snap

Lines changed: 139 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
APPSMITH_KEYCLOAK_DB_PASSWORD: password
1919
APPSMITH_KEYCLOAK_DB_URL: RELEASE-NAME-postgresql.NAMESPACE.svc.cluster.local:5432/keycloak
2020
APPSMITH_KEYCLOAK_DB_USERNAME: root
21-
APPSMITH_REDIS_URL: redis://RELEASE-NAME-redis-master.NAMESPACE.svc.cluster.local:6379
2221
kind: ConfigMap
2322
metadata:
2423
labels:
@@ -60,6 +59,13 @@
6059
value: kubernetes.KUBE_PING
6160
- name: APPSMITH_HEADLESS_SVC
6261
value: RELEASE-NAME-appsmith-headless
62+
- name: APPSMITH_REDIS_PASSWORD
63+
valueFrom:
64+
secretKeyRef:
65+
key: redis-password
66+
name: appsmith-redis-secret
67+
- name: APPSMITH_REDIS_URL
68+
value: redis://:$(APPSMITH_REDIS_PASSWORD)@RELEASE-NAME-redis-master.NAMESPACE.svc.cluster.local:6379
6369
envFrom:
6470
- configMapRef:
6571
name: RELEASE-NAME-appsmith
@@ -108,6 +114,12 @@
108114
- sh
109115
- -c
110116
- until redis-cli -h RELEASE-NAME-redis-master.NAMESPACE.svc.cluster.local ping ; do echo waiting for redis; sleep 2; done
117+
env:
118+
- name: REDISCLI_AUTH
119+
valueFrom:
120+
secretKeyRef:
121+
key: redis-password
122+
name: appsmith-redis-secret
111123
image: docker.io/redis:7.4.9
112124
name: redis-init-container
113125
- command:
@@ -162,6 +174,130 @@
162174
app.kubernetes.io/name: appsmith
163175
type: ClusterIP
164176
5: |
177+
apiVersion: v1
178+
kind: ServiceAccount
179+
metadata:
180+
annotations:
181+
helm.sh/hook: pre-install,pre-upgrade
182+
helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded
183+
helm.sh/hook-weight: "-5"
184+
labels:
185+
app.kubernetes.io/instance: RELEASE-NAME
186+
app.kubernetes.io/managed-by: Helm
187+
app.kubernetes.io/name: appsmith
188+
appsmith.sh/chart: appsmith-3.8.1
189+
name: RELEASE-NAME-appsmith-redis-password-init
190+
namespace: NAMESPACE
191+
6: |
192+
apiVersion: rbac.authorization.k8s.io/v1
193+
kind: Role
194+
metadata:
195+
annotations:
196+
helm.sh/hook: pre-install,pre-upgrade
197+
helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded
198+
helm.sh/hook-weight: "-5"
199+
labels:
200+
app.kubernetes.io/instance: RELEASE-NAME
201+
app.kubernetes.io/managed-by: Helm
202+
app.kubernetes.io/name: appsmith
203+
appsmith.sh/chart: appsmith-3.8.1
204+
name: RELEASE-NAME-appsmith-redis-password-init
205+
namespace: NAMESPACE
206+
rules:
207+
- apiGroups:
208+
- ""
209+
resourceNames:
210+
- appsmith-redis-secret
211+
resources:
212+
- secrets
213+
verbs:
214+
- get
215+
- apiGroups:
216+
- ""
217+
resources:
218+
- secrets
219+
verbs:
220+
- create
221+
7: |
222+
apiVersion: rbac.authorization.k8s.io/v1
223+
kind: RoleBinding
224+
metadata:
225+
annotations:
226+
helm.sh/hook: pre-install,pre-upgrade
227+
helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded
228+
helm.sh/hook-weight: "-5"
229+
labels:
230+
app.kubernetes.io/instance: RELEASE-NAME
231+
app.kubernetes.io/managed-by: Helm
232+
app.kubernetes.io/name: appsmith
233+
appsmith.sh/chart: appsmith-3.8.1
234+
name: RELEASE-NAME-appsmith-redis-password-init
235+
namespace: NAMESPACE
236+
roleRef:
237+
apiGroup: rbac.authorization.k8s.io
238+
kind: Role
239+
name: RELEASE-NAME-appsmith-redis-password-init
240+
subjects:
241+
- kind: ServiceAccount
242+
name: RELEASE-NAME-appsmith-redis-password-init
243+
namespace: NAMESPACE
244+
8: |
245+
apiVersion: batch/v1
246+
kind: Job
247+
metadata:
248+
annotations:
249+
helm.sh/hook: pre-install,pre-upgrade
250+
helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded
251+
helm.sh/hook-weight: "0"
252+
labels:
253+
app.kubernetes.io/instance: RELEASE-NAME
254+
app.kubernetes.io/managed-by: Helm
255+
app.kubernetes.io/name: appsmith
256+
appsmith.sh/chart: appsmith-3.8.1
257+
name: RELEASE-NAME-appsmith-redis-password-init
258+
namespace: NAMESPACE
259+
spec:
260+
backoffLimit: 3
261+
template:
262+
metadata:
263+
labels:
264+
app.kubernetes.io/instance: RELEASE-NAME
265+
app.kubernetes.io/managed-by: Helm
266+
app.kubernetes.io/name: appsmith
267+
appsmith.sh/chart: appsmith-3.8.1
268+
spec:
269+
containers:
270+
- command:
271+
- /bin/sh
272+
- -c
273+
- |
274+
set -eu
275+
SECRET_NAME="appsmith-redis-secret"
276+
NAMESPACE="NAMESPACE"
277+
PASSWORD_KEY="redis-password"
278+
if kubectl -n "$NAMESPACE" get secret "$SECRET_NAME" >/dev/null 2>&1; then
279+
echo "Secret $SECRET_NAME already exists; leaving it untouched."
280+
exit 0
281+
fi
282+
PASSWORD=$(tr -dc 'A-Za-z0-9' </dev/urandom | head -c 24)
283+
kubectl -n "$NAMESPACE" create secret generic "$SECRET_NAME" \
284+
--from-literal="$PASSWORD_KEY"="$PASSWORD"
285+
echo "Created secret $SECRET_NAME."
286+
image: docker.io/alpine/kubectl:latest
287+
imagePullPolicy: IfNotPresent
288+
name: create-password
289+
securityContext:
290+
allowPrivilegeEscalation: false
291+
capabilities:
292+
drop:
293+
- ALL
294+
readOnlyRootFilesystem: true
295+
runAsNonRoot: true
296+
runAsUser: 1000
297+
restartPolicy: Never
298+
serviceAccountName: RELEASE-NAME-appsmith-redis-password-init
299+
ttlSecondsAfterFinished: 60
300+
9: |
165301
apiVersion: policy/v1beta1
166302
kind: PodDisruptionBudget
167303
metadata:
@@ -173,7 +309,7 @@
173309
matchLabels:
174310
app.kubernetes.io/instance: RELEASE-NAME
175311
app.kubernetes.io/name: appsmith
176-
6: |
312+
10: |
177313
apiVersion: v1
178314
kind: Service
179315
metadata:
@@ -194,7 +330,7 @@
194330
app.kubernetes.io/instance: RELEASE-NAME
195331
app.kubernetes.io/name: appsmith
196332
type: ClusterIP
197-
7: |
333+
11: |
198334
apiVersion: v1
199335
kind: ServiceAccount
200336
metadata:

0 commit comments

Comments
 (0)