feat: Add an optional scheduled backup CronJob to the chart

Off by default: a backup with nowhere durable to land is a false sense of
safety and the chart cannot know where that is. NOTES.txt says so when it
is off.

No restore manifest ships: a restore must never be something a helm
upgrade can trigger.
This commit is contained in:
2026-09-07 14:17:42 +00:00
parent 884fd189fb
commit f0f2600bed
5 changed files with 116 additions and 0 deletions
+13
View File
@@ -76,6 +76,13 @@ jobs:
fi
echo "ok: reaper configured in cloud mode only"
- name: Render with backups enabled
run: |
helm template test "$CHART_DIR" \
--set backup.enabled=true \
--set backup.image=gitea.hostxtra.co.uk/mrhid6/vantage/vantagectl:latest \
--set backup.pvcName=vantage-backups > /dev/null
- name: Render against external Redis and MongoDB
run: |
helm template test "$CHART_DIR" \
@@ -149,6 +156,12 @@ jobs:
--set ingress.enabled=true \
--set ingress.web.host=vantage.example.com \
--set ingress.grpc.host=agents.example.com
refuses "backup enabled with no pvcName" \
--set backup.enabled=true \
--set backup.image=gitea.hostxtra.co.uk/mrhid6/vantage/vantagectl:latest
refuses "backup enabled with no image" \
--set backup.enabled=true \
--set backup.pvcName=vantage-backups
- name: Read the chart version
id: chart
+10
View File
@@ -67,3 +67,13 @@ or add an Ingress on top of the -web and -server services.
Quick access via port-forward, e.g.:
kubectl port-forward svc/{{ .Release.Name }}-web {{ .Values.web.service.port }}:{{ .Values.web.service.port }}
kubectl port-forward svc/{{ .Release.Name }}-server {{ .Values.server.service.httpPort }}:{{ .Values.server.service.httpPort }}
{{- if not .Values.backup.enabled }}
No backups are scheduled. Vantage encrypts SSH private keys, vault secrets and
SSO client secrets with KEY_ENCRYPTION_KEY, and that key is not stored anywhere
but your own configuration — a database restored without it is permanently
unreadable.
Set backup.enabled, backup.image and backup.pvcName, and store
KEY_ENCRYPTION_KEY somewhere that survives this cluster.
{{- end }}
@@ -85,3 +85,18 @@ both read it.
fieldRef:
fieldPath: status.podIP
{{- end -}}
{{/*
vantage.backup.env renders the environment vantagectl needs.
It reads the SAME values the server does rather than taking its own, because a
backup that connected to a different database, or stamped a fingerprint of a
different key, than the deployment it is backing up would be worse than no
backup: it would look like one.
*/}}
{{- define "vantage.backup.env" -}}
- name: MONGO_URI
value: {{ tpl .Values.server.env.mongoUri . | quote }}
- name: KEY_ENCRYPTION_KEY
value: {{ .Values.server.env.keyEncryptionKey | quote }}
{{- end -}}
@@ -0,0 +1,56 @@
{{- if .Values.backup.enabled }}
{{- if not .Values.backup.pvcName }}
{{- fail "backup.enabled requires backup.pvcName: a backup needs somewhere durable to land, and the chart cannot guess where that is" }}
{{- end }}
{{- if not .Values.backup.image }}
{{- fail "backup.enabled requires backup.image: the vantagectl image to run" }}
{{- end }}
apiVersion: batch/v1
kind: CronJob
metadata:
name: {{ include "vantage.fullname" . }}-backup
labels:
{{- include "vantage.labels" . | nindent 4 }}
app.kubernetes.io/component: backup
spec:
schedule: {{ .Values.backup.schedule | quote }}
concurrencyPolicy: Forbid
successfulJobsHistoryLimit: {{ .Values.backup.successfulJobsHistoryLimit }}
failedJobsHistoryLimit: {{ .Values.backup.failedJobsHistoryLimit }}
jobTemplate:
spec:
backoffLimit: 2
template:
metadata:
labels:
{{- include "vantage.labels" . | nindent 12 }}
app.kubernetes.io/component: backup
spec:
restartPolicy: Never
containers:
- name: vantagectl
image: {{ .Values.backup.image | quote }}
args:
- backup
- --out
- /backups
{{- with .Values.backup.exclude }}
- --exclude
- {{ join "," . | quote }}
{{- end }}
env:
# Referenced, never redeclared. A backup job holding its own
# copy of KEY_ENCRYPTION_KEY is a second place for it to be
# wrong, and the fingerprint it stamps would then be a
# fingerprint of the wrong key.
{{- include "vantage.backup.env" . | nindent 16 }}
volumeMounts:
- name: backups
mountPath: /backups
resources:
{{- toYaml .Values.backup.resources | nindent 16 }}
volumes:
- name: backups
persistentVolumeClaim:
claimName: {{ .Values.backup.pvcName | quote }}
{{- end }}
+22
View File
@@ -111,3 +111,25 @@ ingress:
certResolver: ""
imagePullSecrets: []
# Scheduled backups.
#
# Off by default, deliberately. A backup with nowhere durable to land is a
# false sense of safety, and the chart cannot know where that is — pvcName
# must name a volume you have decided will outlive the cluster.
#
# There is no restore manifest here on purpose: a restore is an operator
# decision with a confirmation attached, and must never be something a
# `helm upgrade` can trigger. Run one as a `kubectl run` Job with
# --confirm-db.
backup:
enabled: false
schedule: "0 2 * * *"
image: ""
pvcName: ""
# Collections to leave out. Recorded in each archive's manifest, so an
# archive can never claim to be complete when it is not.
exclude: []
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 3
resources: {}