go-bananas/deploy/helm/go-bananas/templates/pvc.yaml
Daniel Noulet 951dd8d26b
Some checks failed
ci / go (push) Has been cancelled
ci / admin-ui (push) Has been cancelled
ci / runtime-sdk (push) Has been cancelled
ci / helm (push) Has been cancelled
ci / openapi (push) Has been cancelled
ci / container (push) Has been cancelled
ci / release-config (push) Has been cancelled
ci / shared styling (push) Has been cancelled
ci / security (push) Has been cancelled
ci / all green (push) Has been cancelled
initial commit
2026-10-07 15:11:35 +02:00

161 lines
No EOL
6.8 KiB
YAML

{{- if .Values.persistence.enabled }}
{{- if eq .Values.persistence.type "persistentVolumeClaim" }}
{{- if not .Values.persistence.existingClaim }}
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: {{ include "go-bananas.dataVolumeName" . }}
namespace: {{ .Release.Namespace }}
labels:
{{- include "go-bananas.labels" . | nindent 4 }}
annotations:
{{- /*
Retain the volume when the release is uninstalled. The themes in it are the
only copy of an operator's work: deleting a volume on uninstall would make
`helm uninstall` destructive in a way that is very easy to do by accident.
*/}}
helm.sh/resource-policy: keep
{{- with .Values.global.extraAnnotations }}
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
accessModes:
- {{ .Values.persistence.accessMode }}
resources:
requests:
storage: {{ .Values.persistence.size }}
{{- with .Values.persistence.storageClassName }}
storageClassName: {{ . | quote }}
{{- end }}
{{- end }}
{{- end }}
{{- if and .Values.persistence.snapshot.enabled (eq .Values.persistence.type "persistentVolumeClaim") }}
---
{{/*
A scheduled SQLite snapshot.
Three things this gets right that the obvious implementation does not:
1. *It is a CronJob, not a Helm hook.* Combining `schedule` with
`helm.sh/hook: pre-upgrade` is a contradiction: a hook runs on every upgrade
regardless of the schedule, and with `hook-succeeded` the resource is then
deleted. As a hook it would both ignore its own schedule and never produce
anything to restore.
2. *The snapshots land on the same volume.* An `emptyDir` destination would be
discarded the moment the pod exits, which makes the whole exercise pointless.
3. *It runs the service's own `backup` command.* Copying a database file while a
write is in flight produces a torn copy. `go-bananas backup` uses SQLite's
VACUUM INTO, which reads through the database's own backup machinery and is
consistent without blocking writers. Using the application binary rather than a
`sqlite3` from some other image also guarantees the SQLite library is the same
version the schema was written for, and means no third-party image and no
package installation into a read-only root filesystem.
The known limitation is stated rather than hidden: the job mounts the same
ReadWriteOnce volume as the Deployment, so on a cluster whose CSI driver enforces
single-node attachment strictly the job can fail to mount while the application is
running. If your cluster reports MultiAttach errors, give the job a node affinity
that pins it alongside the running pod, or take snapshots from outside the cluster.
A release using `emptyDir` data has nothing to snapshot and is unaffected.
Restoring is manual, because a restore should be a deliberate act:
kubectl -n <namespace> exec <pod> -- /usr/local/bin/go-bananas backup --to /tmp/check.db
# then stop the service, replace the database file, and start it again.
*/}}
apiVersion: batch/v1
kind: CronJob
metadata:
name: {{ include "go-bananas.fullname" . }}-snapshot
namespace: {{ .Release.Namespace }}
labels:
{{- include "go-bananas.labels" . | nindent 4 }}
annotations:
{{- with .Values.global.extraAnnotations }}
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
schedule: {{ .Values.persistence.snapshot.schedule | quote }}
concurrencyPolicy: Forbid
# Snapshots are named with a timestamp, so two runs cannot collide, and an
# overlap is a sign the volume is too slow rather than a problem to solve by
# running them at once.
startingDeadlineSeconds: 600
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 3
jobTemplate:
metadata:
labels:
{{- include "go-bananas.labels" . | nindent 8 }}
spec:
backoffLimit: 2
template:
metadata:
labels:
{{- include "go-bananas.selectorLabels" . | nindent 12 }}
app.kubernetes.io/component: snapshot
spec:
restartPolicy: OnFailure
securityContext:
runAsNonRoot: true
runAsUser: 65532
runAsGroup: 65532
fsGroup: 65532
containers:
- name: snapshot
# The service image itself: the snapshot command is part of the
# binary, so there is no second image to keep in step with this one.
image: {{ include "go-bananas.image" . }}
imagePullPolicy: {{ .Values.image.pullPolicy }}
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
env:
- name: DATABASE_PATH
value: {{ include "go-bananas.databasePath" . | quote }}
- name: SNAPSHOT_RETENTION_DAYS
value: {{ .Values.persistence.snapshot.retention | quote }}
# A shell, because the snapshot filename needs a timestamp and an argv
# list cannot expand one.
command: ["/bin/sh", "-euc"]
args:
- |
dest="/snapshot/go-bananas-$(date -u +%Y%m%dT%H%M%SZ).db"
/usr/local/bin/go-bananas backup --to "$dest"
# Retention by age, run here rather than as a lifecycle hook so it
# happens on the same schedule as the snapshot and cannot be skipped
# by a container that failed to start. An old snapshot is removed even
# though the Job that produced it has long since been garbage
# collected.
find /snapshot -name 'go-bananas-*.db' \
-mtime "+$SNAPSHOT_RETENTION_DAYS" -delete
ls -la /snapshot
volumeMounts:
- name: data
mountPath: {{ .Values.persistence.mountPath }}
readOnly: true
- name: snapshot
# The writable half of the same volume. Only the snapshot
# directory is writable; the database file itself is mounted
# read-only above.
mountPath: /snapshot
subPath: snapshots
volumes:
- name: data
persistentVolumeClaim:
claimName: {{ .Values.persistence.existingClaim | default (include "go-bananas.dataVolumeName" .) }}
readOnly: true
- name: snapshot
persistentVolumeClaim:
claimName: {{ .Values.persistence.existingClaim | default (include "go-bananas.dataVolumeName" .) }}
# Writable, and only for the subPath above. The pod as a whole can
# still reach the database, which is why the read-only mount and the
# `runAsNonRoot` security context are both kept.
readOnly: false
{{- end }}
{{- end }}