From f0f2600bed4f30a48df58b29fe8fc4fe1ef12d3f Mon Sep 17 00:00:00 2001 From: mrhid6 Date: Mon, 7 Sep 2026 14:17:42 +0000 Subject: [PATCH] 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. --- .gitea/workflows/chart-release.yml | 13 +++++ deploy/chart/vantage/templates/NOTES.txt | 10 ++++ deploy/chart/vantage/templates/_helpers.tpl | 15 +++++ .../vantage/templates/backup-cronjob.yaml | 56 +++++++++++++++++++ deploy/chart/vantage/values.yaml | 22 ++++++++ 5 files changed, 116 insertions(+) create mode 100644 deploy/chart/vantage/templates/backup-cronjob.yaml diff --git a/.gitea/workflows/chart-release.yml b/.gitea/workflows/chart-release.yml index 3c8bbe0..a94810e 100644 --- a/.gitea/workflows/chart-release.yml +++ b/.gitea/workflows/chart-release.yml @@ -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 diff --git a/deploy/chart/vantage/templates/NOTES.txt b/deploy/chart/vantage/templates/NOTES.txt index 09c39fe..617aefa 100644 --- a/deploy/chart/vantage/templates/NOTES.txt +++ b/deploy/chart/vantage/templates/NOTES.txt @@ -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 }} diff --git a/deploy/chart/vantage/templates/_helpers.tpl b/deploy/chart/vantage/templates/_helpers.tpl index 842cf5b..2ada665 100644 --- a/deploy/chart/vantage/templates/_helpers.tpl +++ b/deploy/chart/vantage/templates/_helpers.tpl @@ -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 -}} diff --git a/deploy/chart/vantage/templates/backup-cronjob.yaml b/deploy/chart/vantage/templates/backup-cronjob.yaml new file mode 100644 index 0000000..0a43656 --- /dev/null +++ b/deploy/chart/vantage/templates/backup-cronjob.yaml @@ -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 }} diff --git a/deploy/chart/vantage/values.yaml b/deploy/chart/vantage/values.yaml index 4ba7f6a..32a1e0f 100644 --- a/deploy/chart/vantage/values.yaml +++ b/deploy/chart/vantage/values.yaml @@ -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: {}