Add checksum/config annotation to template-chart and make it binding

A changed ConfigMap did not restart any pod in this repo: no chart carries
a checksum/config annotation, so the pod template stays identical and the
running container keeps its old config. With subPath mounts (the usual form
here) the kubelet never refreshes the file either, so the change is silently
ineffective - see #82, where this caused a real outage while every bundle was
green and the pod Ready.

template-chart is the reference implementation, so it gets the annotation on
both pod templates (Deployment and StatefulSet), and the README style guide
gets a new binding section 9 covering the rule, the reason, and the limit for
1Password-backed Secrets.

Major bump: runtime behavior changes - a config change now costs a restart.
This commit is contained in:
jklein committed 2026-08-23 18:49:10 +02:00
1 parent b41cc6eccc
commit 1bca98b9ef
5 files changed
+36 -4

No files matched your search

+1
View File
@@ -61,6 +61,7 @@ The binding style guide for all current and future charts lives in **README.md (
- Main image tag from `{{ .Chart.AppVersion }}`; auxiliary/sidecar/init images pinned via values and reviewed on app-update rounds.
- Ingress: `ingress.domain` and `ingress.clusterIssuer` `required` **without defaults**; `ingress.className` defaults to `nginx`; optional `ingress.annotations` merged with the cert-manager annotation; TLS secret `tls-<release>-<chart>-ingress`. Documented exception: homeassistant keeps its flexible upstream ingress style (className default `nginx`).
- Secrets are 1Password `OnePasswordItem` CRs: values expose `secrets.<name>SecretRef` holding an `op://` item path; the operator materializes the Kubernetes Secret.
- Config changes must restart the workload (#82): every pod template consuming a ConfigMap carries `checksum/config: {{ include (print $.Template.BasePath "/<chart>.configmap.yaml") . | sha256sum }}` (one line per ConfigMap). Without it a changed ConfigMap restarts nothing — `subPath` mounts are never refreshed and apps read config only at start. Adding it to an existing chart is a **major** bump. Secrets from `OnePasswordItem` cannot be hashed (Helm never sees their content); charts mounting them document the limitation in `values.yaml`.
- Every container offers an additive `env` block rendered through `tpl` with `value`, `secretKeyRef`, and `configMapKeyRef` forms.
- Hardening: `automountServiceAccountToken: false`, non-root, `readOnlyRootFilesystem: true`, caps dropped, `allowPrivilegeEscalation: false`; contexts values-configurable (`securityContext.pod` / `securityContext.container`); writable paths as emptyDirs; image-forced deviations MUST be documented with a comment in the chart's `values.yaml`.
- Probes: liveness/readiness/startup on every workload (exception: paperless consume CronJob), values-configurable with calibrated defaults.
+20 -3
View File
@@ -125,18 +125,35 @@ These guidelines are **binding for every current and future chart** in this repo
- Secrets are **1Password `OnePasswordItem` CRs**: values expose `secrets.<name>SecretRef` holding an `op://` item path; the 1Password operator materializes the Kubernetes Secret in-cluster.
### 9. Environment variables
### 9. Config changes must restart the workload (#82)
- **Every workload that consumes a ConfigMap carries a `checksum/config` annotation on its pod template** — one line per ConfigMap it consumes:
```yaml
template:
metadata:
annotations:
checksum/config: {{ include (print $.Template.BasePath "/<chart>.configmap.yaml") . | sha256sum }}
```
Reference implementation: `template-chart/templates/xxx.deployment.yaml` and `xxx.sfs.yaml`. Where a workload consumes several ConfigMaps, use one annotation per file with a distinguishing suffix (`checksum/config-<name>`).
- **Why it is mandatory, not optional**: without it a changed ConfigMap does not change the pod template, so nothing restarts and the running container keeps the old config — silently. `subPath` mounts (the usual form here) are never refreshed by the kubelet at all, and most apps read their config only at start. In #82 this produced a real outage: the router kept routing to Services that no longer existed while every bundle was green and the pod was Ready.
- **The annotation is content-based, not time-based**: the hash is taken over the rendered ConfigMap, so an unchanged config renders an identical hash and `helm upgrade` is a no-op. It never degenerates into a restart-on-every-upgrade.
- Adding the annotation to an existing chart is a **major** bump (§2): runtime behavior changes — a config change now costs a restart. Name that cost in the PR (some workloads take minutes to come back).
- **Secrets from `OnePasswordItem` cannot use this mechanism.** Helm never sees their content — the 1Password operator fills the Secret in-cluster after rendering, so there is nothing to hash. A chart that mounts such a Secret as a file documents the limitation in its `values.yaml` where it cannot close the gap.
### 10. Environment variables
- Every workload container offers an additive `env` value block, appended to the env entries the chart always sets.
- Entries are rendered through `tpl` (so values may contain Helm templating) and support the `value`, `secretKeyRef` and `configMapKeyRef` forms (pattern: `template-chart/templates/xxx.deployment.yaml`).
### 10. Scaling
### 11. Scaling
- Every Deployment/StatefulSet has a `replicas` value (default `1`, the values path per workload matches its section) that also accepts an explicit `0` for per-workload fine control.
- Every chart has a global `scaleDown` flag (default `false`): `scaleDown: true` takes precedence over all `replicas` values, renders every Deployment/StatefulSet with `replicas: 0` and suspends CronJobs. It **stops** the app but does not uninstall it — PVCs, Secrets, Services and Ingress stay in place; setting it back to `false` brings the workloads back up.
- Implemented via a per-chart `<chart>.replicas` helper in `_helpers.tpl` (reference in `template-chart`); the helper uses an explicit nil check (`kindIs "invalid"`) because Helm's `default` would swallow an explicit `0`.
### 11. Everything else
### 12. Everything else
- `icon:` is mandatory in every `Chart.yaml`.
- `helm lint --strict` must pass without findings.
+1 -1
View File
@@ -5,7 +5,7 @@ icon: https://helm.sh/img/helm.svg
type: application
version: 1.0.0+upxxx
version: 2.0.0+upxxx
appVersion: "xxx"
maintainers:
@@ -14,6 +14,13 @@ spec:
name: {{ .Release.Name }}-{{ .Chart.Name }}-deployment
labels:
app: {{ .Release.Name }}-{{ .Chart.Name }}-deployment
annotations:
# Hash of the rendered ConfigMap: a changed ConfigMap changes the pod
# template and thus rolls the pods. Without it a config change stays
# invisible to the running container - subPath mounts are never
# updated by the kubelet, and most apps read their config only at
# start. Add one line per ConfigMap the workload consumes.
checksum/config: {{ include (print $.Template.BasePath "/xxx.configmap.yaml") . | sha256sum }}
spec:
automountServiceAccountToken: false
securityContext:
+7
View File
@@ -15,6 +15,13 @@ spec:
name: {{ .Release.Name }}-{{ .Chart.Name }}-sfs
labels:
app: {{ .Release.Name }}-{{ .Chart.Name }}-sfs
annotations:
# Hash of the rendered ConfigMap: a changed ConfigMap changes the pod
# template and thus restarts the pods. Without it a config change stays
# invisible to the running container - subPath mounts are never
# updated by the kubelet, and most apps read their config only at
# start. Add one line per ConfigMap the workload consumes.
checksum/config: {{ include (print $.Template.BasePath "/xxx.configmap.yaml") . | sha256sum }}
spec:
automountServiceAccountToken: false
securityContext: