diff --git a/CLAUDE.md b/CLAUDE.md index 6d379ea..a866165 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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---ingress`. Documented exception: homeassistant keeps its flexible upstream ingress style (className default `nginx`). - Secrets are 1Password `OnePasswordItem` CRs: values expose `secrets.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 "/.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. diff --git a/README.md b/README.md index 0cfba09..f883ae9 100644 --- a/README.md +++ b/README.md @@ -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.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 "/.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-`). +- **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 `.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. diff --git a/template-chart/Chart.yaml b/template-chart/Chart.yaml index f47363a..30d46e3 100644 --- a/template-chart/Chart.yaml +++ b/template-chart/Chart.yaml @@ -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: diff --git a/template-chart/templates/xxx.deployment.yaml b/template-chart/templates/xxx.deployment.yaml index 0108b2e..016ab6d 100644 --- a/template-chart/templates/xxx.deployment.yaml +++ b/template-chart/templates/xxx.deployment.yaml @@ -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: diff --git a/template-chart/templates/xxx.sfs.yaml b/template-chart/templates/xxx.sfs.yaml index 231cb2f..689f4b6 100644 --- a/template-chart/templates/xxx.sfs.yaml +++ b/template-chart/templates/xxx.sfs.yaml @@ -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: