diff --git a/CLAUDE.md b/CLAUDE.md index 90fe5c3..6d379ea 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -47,19 +47,23 @@ Copy `template-chart/` and replace every `xxx` placeholder — in file names (`t Semver conventions for the chart part: -- **Patch**: bug fixes without behavior or values-schema change. +- **Patch**: bug fixes without behavior or values-schema change; plain app-version updates. - **Minor**: additive, backward-compatible changes (new optional values with unchanged defaults). -- **Major**: runtime behavior changes or breaking values changes (e.g. probes, security hardening, computed limits). +- **Major**: runtime behavior changes or breaking values changes (e.g. probes, security hardening, computed limits, values that become required). App-version updates go through PRs (so the validate and install-test pipelines run), not through the `change-app-version.yml` dispatch workflow (which commits directly to main and bypasses both). For app **major** jumps, update stepwise: first to the last release of the old major line (own PR, merge, publish), then to the newest major. -## Chart conventions (from template-chart) +## Chart conventions -- All resources are named `{{ .Release.Name }}-{{ .Chart.Name }}-` (e.g. `...-deployment`, `...-service`, `...-secret`). -- Image tags come from `{{ .Chart.AppVersion }}`. +The binding style guide for all current and future charts lives in **README.md ("Chart style guidelines")**; `template-chart/` is the reference implementation. Compact summary: + +- Resources named `{{ .Release.Name }}-{{ .Chart.Name }}-`; template files `..yaml`; stateful apps use a StatefulSet (`.sfs.yaml`). Documented exception: historical PVC names stay (renames would orphan data); StatefulSet renames only with a `--cascade=orphan` migration note in a major PR. +- 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. -- Ingress standard: `ingress.domain` and `ingress.clusterIssuer` (cert-manager) are `required` values; `ingress.className` defaults to `nginx`; optional `ingress.annotations` are merged with the cert-manager annotation; TLS secret named `tls---ingress`. -- `env` entries in values support Helm templating — the deployment renders them through `tpl`, and supports `value`, `secretKeyRef`, and `configMapKeyRef` forms. -- Hardened defaults: `automountServiceAccountToken: false`, non-root user, `readOnlyRootFilesystem: true`, all capabilities dropped; probes and resource requests/limits always set. Security contexts are values-configurable (`securityContext.pod` / `securityContext.container`); image-forced deviations are documented with a comment in the chart's `values.yaml`. -- Stateful apps use a StatefulSet in a `.sfs.yaml` template instead of a deployment. -- No chart has dependencies; `charts/` directories are empty. +- 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. +- Resources: requests as values defaults; limits auto-computed by the `_helpers.tpl` helper (`limitFactor` default 3; no CPU limits unless set explicitly). +- Replicas per workload (explicit `0` allowed) plus a chart-global `scaleDown` flag that overrides them. +- `icon:` mandatory in every `Chart.yaml`; `helm lint --strict` without findings; CI fixtures under `ci//` or a justified exclusion; no chart dependencies (`charts/` directories are empty). diff --git a/README.md b/README.md index 36b816d..a538d72 100644 --- a/README.md +++ b/README.md @@ -26,10 +26,10 @@ Collection of some self-made helm-charts. Each top-level directory is a standalo ## Versioning -`Chart.yaml` uses the scheme `version: +up` (e.g. `1.4.0+up1.9.2`), where `appVersion` matches the `+up` suffix (dashes in the app version are normalized to dots in the suffix, e.g. `0.0.36-beta` → `+up0.0.36.beta`). +`Chart.yaml` uses the scheme `version: +up` (e.g. `1.4.0+up1.9.2`) — see the [chart style guidelines](#chart-style-guidelines) for the full rules (suffix normalization, semver bump levels). -- Bump the **semver part** when the chart itself (templates/values) changes. -- The **app version** is bumped via the `Change App-Version` workflow, which rewrites `version:`/`appVersion:` and commits directly to `main`. +- Bump the **chart version** in every change that touches a chart directory — a push to `main` publishes the chart. +- **App-version updates go through PRs** (so the validate and install-test pipelines run). The manually dispatched `Change App-Version` workflow (rewrites `version:`/`appVersion:` and commits directly to `main`) exists but bypasses both pipelines. For app **major** jumps, update stepwise: first to the last release of the old major line (own PR, merge, publish), then to the newest major. ## CI/CD (GitHub Actions) @@ -69,16 +69,75 @@ helm template --set ingress.domain=example.com --set ingress.cluste For runtime verification (do the workloads actually start, do probes/security settings hold?), use a throwaway local [k3d](https://k3d.io) cluster: `k3d cluster create `, apply a stub CRD for `OnePasswordItem` plus dummy secrets (and throwaway postgres/redis pods where the app needs them), `helm install` the chart, watch pod readiness/events, then `k3d cluster delete `. -## Chart conventions +## Chart style guidelines + +These guidelines are **binding for every current and future chart** in this repo. They are the consolidated end state of the conventions overhaul (#32, batches 0–4); `template-chart/` is the reference implementation. + +### 1. Naming + +- Resources are named `{{ .Release.Name }}-{{ .Chart.Name }}-` (e.g. `...-deployment`, `...-sfs`, `...-service`, `...-secret`, `...-configmap`, `...-ingress`, `...-pvc`). +- Template files are named `..yaml`; stateful apps use a StatefulSet in a `.sfs.yaml` template instead of a deployment. +- **Documented exception — existing PVC names**: PVCs of already-published charts keep their historical names even where they deviate from the scheme. Renaming a PVC makes Kubernetes provision a new, empty volume, so a rename would orphan the existing data. New charts name PVCs `{{ .Release.Name }}-{{ .Chart.Name }}-pvc` per the scheme. +- StatefulSet renames are possible (data survives because the charts use standalone PVC templates, not `volumeClaimTemplates`) but only ship in a major-bump PR with a one-time migration note: `kubectl delete statefulset --cascade=orphan` (and delete the leftover old pod) before upgrading. + +### 2. Versioning + +- `Chart.yaml` uses `version: +up`; the `+up` suffix matches `appVersion`, with dashes normalized to dots (e.g. `0.0.36-beta` → `+up0.0.36.beta`). `appVersion` is the **exact image tag** of the main app image. +- Semver rules for the chart part: + - **Patch**: bug fixes without behavior or values-schema change; plain app-version updates. + - **Minor**: additive, backward-compatible changes (new optional values with unchanged defaults). + - **Major**: runtime behavior changes or breaking values changes (e.g. resource renames, probes, security hardening, values that become required). +- Any push to `main` touching a chart directory publishes that chart, so the version must be bumped in the same change. + +### 3. Images + +- The main app image tag comes from `{{ .Chart.AppVersion }}` — never hardcoded. +- Auxiliary images (sidecars, init containers, secondary workloads — e.g. the delegation nginx in matrix-synapse, guacd in patchmon, busybox init/consume images in paperless-ngx) are pinned via values instead of being hardcoded in templates, and their pinned versions are reviewed alongside every app-version update round. + +### 4. Ingress + +- `ingress.domain` and `ingress.clusterIssuer` (cert-manager) are `required` values **without defaults** — every installation sets both explicitly; they are the only values needed for a default install. +- `ingress.className` selects the ingress class and defaults to `nginx`. +- `ingress.annotations` takes optional extra annotations that are merged with the cert-manager `cluster-issuer` annotation the chart always sets. +- The TLS secret is named `tls---ingress`. +- **Documented exception — homeassistant**: keeps its upstream-style flexible ingress values (host/tls lists, no required values); only its `ingress.className` default follows the standard (`nginx`, clearable to omit the field). + +### 5. Security hardening + +- Defaults on every workload: `automountServiceAccountToken: false`, non-root user, `readOnlyRootFilesystem: true`, all capabilities dropped, `allowPrivilegeEscalation: false`. +- Pod and container contexts are values-configurable via `securityContext.pod` / `securityContext.container`. +- Writable paths (e.g. `/tmp`, nginx cache dirs, s6-overlay's `/run`) are emptyDirs so the root filesystem stays read-only. +- Where an image cannot satisfy the full standard (e.g. Home Assistant must start as root for s6-overlay, paperless needs a root init container to hand over `/run`, samba needs its capabilities), the deviation is minimal and **must** be justified with a comment in that chart's `values.yaml`. + +### 6. Probes + +- Every workload has liveness, readiness and startup probes (documented exception: the paperless-ngx consume CronJob). +- Probes are values-configurable with calibrated defaults (the startup probe gates liveness/readiness; its budget is documented in a values comment per chart). + +### 7. Resources (auto-limits) + +- Resource requests ship as chart defaults in `values.yaml` and are user-overridable. +- Limits are computed by a per-chart `_helpers.tpl` helper (reference implementation in `template-chart/templates/_helpers.tpl`): an explicitly set limit always wins, missing memory and ephemeral-storage limits default to `resources.limitFactor` × the request (factor default `3`), and a CPU limit is only rendered when set explicitly — otherwise CPU stays unlimited (it is compressible, throttling instead of OOM-killing). The helper is duplicated into every chart because charts have no dependencies. + +### 8. Secrets -- Resources are named `{{ .Release.Name }}-{{ .Chart.Name }}-`; template files are named `..yaml` (StatefulSets use `.sfs.yaml`). - - **Deliberate exception — existing PVC names**: PVCs of already-published charts keep their historical names even where they deviate from the scheme. Renaming a PVC makes Kubernetes provision a new, empty volume, so a rename would orphan the existing data. New charts name PVCs `{{ .Release.Name }}-{{ .Chart.Name }}-pvc` per the scheme. StatefulSet names *are* being aligned (data survives because the charts use standalone PVC templates, not `volumeClaimTemplates`); each rename ships in its own major-bump PR with a one-time migration note (`kubectl delete statefulset --cascade=orphan` before upgrading). -- Image tags come from `{{ .Chart.AppVersion }}`. -- Auxiliary/sidecar images (images other than the app image, currently only the delegation nginx in matrix-synapse) are pinned via values (`.image.repository` / `.image.tag`) instead of being hardcoded in templates, and their pinned versions are reviewed alongside every app-version update round. - Secrets are **1Password `OnePasswordItem` CRs**: values expose `secrets.SecretRef` holding an `op://` item path; the 1Password operator materializes the Kubernetes Secret in-cluster. -- Ingress standard: `ingress.domain` and `ingress.clusterIssuer` (cert-manager) are the only required values. `ingress.className` selects the ingress class and defaults to `nginx`; `ingress.annotations` takes optional extra annotations that are merged with the cert-manager `cluster-issuer` annotation the chart always sets. The TLS secret is named `tls---ingress`. (homeassistant deliberately keeps its upstream-style flexible ingress values instead.) -- `env` entries in `values.yaml` are rendered through `tpl`, so values may contain Helm templating; `value`, `secretKeyRef` and `configMapKeyRef` forms are supported. -- Hardened defaults: `automountServiceAccountToken: false`, non-root user, `readOnlyRootFilesystem: true`, all capabilities dropped, probes and resource requests always set. Pod and container contexts are configurable via `securityContext.pod` / `securityContext.container` in each chart's values; writable paths (e.g. `/tmp`, nginx cache dirs, s6-overlay's `/run`) are emptyDirs. Where an image cannot satisfy the full standard (e.g. Home Assistant must start as root for s6-overlay, paperless needs a root init container to hand over `/run`), the deviation is minimal and justified with a comment in that chart's `values.yaml`. -- Resource requests ship as chart defaults and are user-overridable; limits are computed by a per-chart `_helpers.tpl` helper (reference implementation in `template-chart/templates/_helpers.tpl`): an explicitly set limit always wins, missing memory and ephemeral-storage limits default to `resources.limitFactor` × the request (factor default `3`), and a CPU limit is only rendered when set explicitly — otherwise CPU stays unlimited (it is compressible, throttling instead of OOM-killing). The helper is duplicated into every chart because charts have no dependencies. -- Replica counts and scale-to-zero: every Deployment/StatefulSet has a `replicas` value (default `1`, the values path per workload matches its section, e.g. `patchmon.frontend.replicas`) that also accepts an explicit `0` for per-workload fine control. In addition, 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 (paperless-ngx consume job). `scaleDown` **stops** the app but does not uninstall it — PVCs, Secrets, Services and Ingress stay in place, and 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`. + +### 9. 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 + +- 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 + +- `icon:` is mandatory in every `Chart.yaml`. +- `helm lint --strict` must pass without findings. +- `NOTES.txt` for every chart is planned (#43); currently only homeassistant ships one. +- CI: every installable chart has fixtures under `ci//` (`test-values.yaml`, optionally `fixtures.yaml`/`settings.env`) or a justified entry in `ci/excluded-charts.txt` — the install-test fails if both are missing. - Charts have no dependencies (`charts/` directories are empty).