Gateway API as the second routing mode (pilot on cyberchef)
Documents Gitea #114's pilot, which is in helm-charts PR #115. Chart-Style-Guide §4 becomes "Ingress and Gateway API" and carries the binding form: HTTPRoute with parentRefs only - no Gateway, no gatewayClassName, no certificate, no implementation-specific policy object, because that subset is identical on nginx-gateway-fabric, Cilium, Envoy Gateway and Istio. Plus the four guards (a value instead of auto-detection, mutual exclusion with ingress.enabled, mandatory parentRefs, one source of truth for the hostname) and what has no counterpart at all: proxy-body-size and auth annotations. The §4 anchor changed, so the reference on the homeassistant page moved with it. CI-CD gains the httproute-match job as #5 (install-test renumbered to #7) and a section on the Gateway-API CRD fixture - why it is real rather than a stub, why --server-side (the CRD is 244792 bytes as JSON against a 262144 byte annotation limit), and why it is not in ci/common/. cyberchef gains the httpRoute values, a "Routing modes" section, the attachment check, and the honest version of its CI section: its install-test now runs in Gateway mode, which gives up installing the Ingress and buys a route validated against a real schema. What stays unprovable is named - nothing routes in k3d. template-chart carries the reference implementation; New-Chart gains the overlay obligation; Renovate replaces the blanket "ci/** is not managed" with the distinction that was always meant - a fixture image stays unmanaged, a pinned tool is tracked wherever it lives. Lessons-Learned: the stub CRD that would have proved nothing.
9 files changed
+194
-23
No files matched your search
+45
-6
@@ -74,7 +74,7 @@ Three things were listed here as only that run could prove, and they were the wh
|
||||
|
||||
## The PR pipeline — `validate-charts.yml`
|
||||
|
||||
Five jobs. Everything except the render test follows the **changed** chart directories.
|
||||
Six jobs. Everything except the three render tests follows the **changed** chart directories.
|
||||
|
||||
### 1. `detect-changed-charts`
|
||||
|
||||
@@ -129,7 +129,28 @@ Three things to know before touching it:
|
||||
|
||||
Measured: 30 charts, **5 selectors verified in ~14 s**, no cluster. The fail path was exercised rather than assumed — renaming `node-group` to `nodegroup` in a hand-written copy of the PDB selector produced two `FAIL` lines naming both the selector and the labels actually present, plus a non-zero exit; the same was done for the spread constraint's selector, and the `matchExpressions`-only and overlay-without-selector paths were probed too.
|
||||
|
||||
### 5. `trivy-config` (reporting only)
|
||||
### 5. `httproute-match` (every chart, every PR)
|
||||
|
||||
The third job that ignores the changed-chart matrix, and for the same reason as the two above: the mistake arrives by copy into a chart whose own diff nobody reads for it — and this convention is going to be copied into 20 more charts when the Gateway-API sweep runs (Gitea #114). `ci/run-httproute-match-test.sh` renders **every** chart with the fixtures the install-test uses and holds each `HTTPRoute` against the `Service` objects of that **same** render.
|
||||
|
||||
| Finding | What it does | Who notices today |
|
||||
|---|---|---|
|
||||
| `parentRefs` empty | the route attaches to **no** Gateway: created, schema-valid, rejected by nobody, routing nothing | nobody — no event, no condition, the only symptom is a hostname that does not answer |
|
||||
| `backendRefs` naming a Service, or a port, that does not exist | the reference is resolved **by name** at runtime and validated by nothing | nobody in CI — `ResolvedRefs: False` lands in a route status that only a Gateway controller writes, and none runs here |
|
||||
|
||||
**The real Gateway-API CRDs in the install-test do not close either gap**, which is the whole reason this job exists next to them: the Gateway-API schema accepts an empty `parentRefs` list and a `backendRefs` entry pointing at a Service that was never created. The CRDs prove the object is *well-formed*; this test proves it *points somewhere*.
|
||||
|
||||
The port half is a genuinely new exposure rather than a hypothetical. An Ingress backend references its Service port **by name**; `backendRefs` takes a `PortNumber` and cannot. So from now on the number in the route and the port in the Service are kept in step by hand, and this test is what holds that pair.
|
||||
|
||||
Three things to know before touching it:
|
||||
|
||||
- **Helm is the YAML parser**, through `ci/yaml2json`, for exactly the same reason as in `selector-match` above — no pyyaml, no `yq`, and a hand-written parser fails by reading nothing and reporting nothing.
|
||||
- **What is deliberately not checked.** *That traffic flows* — no Gateway controller runs in CI and none will, so `Accepted: True` is not obtainable and that claim is made nowhere in this pipeline. *Mutual exclusion with the Ingress* — each chart aborts its own render when `ingress.enabled` and `httpRoute.enabled` are both true, which is where it belongs: this test renders one configuration per chart and a rule here would have to assume the switch is chart-global, which may not survive the multi-app charts of the sweep. *Cross-namespace and non-Service backends*, and a rule carrying no `backendRefs` at all (legal for a filter-only rule such as a redirect) — all three are **reported as "not checked", never counted as passing**.
|
||||
- **Two anti-vacuum guards**, because the convention ships `httpRoute.enabled: false` and this test would otherwise be green over nothing. `ci/httproute/<chart>.yaml` is a render-only overlay applied **last** whose one job is to put a chart into Gateway mode — and a chart that *has* one and still renders no route **fails**. On top of that, a run in which **no assertion at all** held fails. Today `cyberchef` is exercised through its ordinary install-test fixture (which installs in Gateway mode) and `template-chart` through its overlay.
|
||||
|
||||
Measured: 27 charts, **4 assertions held in ~3 s**, no cluster. The fail path was exercised rather than assumed: a Service renamed in the route, a port the Service does not expose, a port left out entirely, an emptied `parentRefs` with the chart's own guard disabled, an overlay that renders no route, and a run with zero assertions — each produced a `FAIL` naming both sides and a non-zero exit.
|
||||
|
||||
### 6. `trivy-config` (reporting only)
|
||||
|
||||
Renders each chart in the scan matrix and lists what `trivy config` finds. **It never fails the job** (`TRIVY_CONFIG_MODE: report`). Deliberately not a gate yet: the repo has several documented deviations, and a check that goes red on those every time is one people learn to scroll past. Switching the mode to `gate` is the intended step once the seccomp gap is closed.
|
||||
|
||||
@@ -137,7 +158,7 @@ Trivy is installed from its release tarball, not via `aquasecurity/setup-trivy`
|
||||
|
||||
Exceptions live in `ci/trivy/trivyignore.yaml`, each with a mandatory `statement` — the script refuses to run without one and prints every suppressed finding together with its reason. **The trusted-registry list for AVD-KSV-0125 is a separate file on purpose**, `ci/trivy/data/ksv0125.yaml`: it is *configured* rather than suppressed, so the check keeps being able to say something about an image from a genuinely foreign source. A new registry therefore means an entry there, not an ignore — `quay.io` joined it with [keycloak](keycloak), the first chart in the repo using it. Note what the check does *not* forgive: an unqualified Docker Hub image puts the organisation in the registry position and is still reported, which is why every chart writes its image fully qualified.
|
||||
|
||||
### 6. `install-test` (per changed, non-excluded chart)
|
||||
### 7. `install-test` (per changed, non-excluded chart)
|
||||
|
||||
`ci/run-install-test.sh <chart>` against a throwaway [k3d](https://k3d.io) cluster: apply the stub `OnePasswordItem` CRD plus the chart's fixtures, `helm install` with the CI values, and wait for every Deployment and StatefulSet to roll out. Since probes and hardening are active, "Ready" means the app actually answers. On failure the job dumps pod descriptions, events and container logs.
|
||||
|
||||
@@ -153,7 +174,9 @@ Fixtures live at the **repo root** under `ci/`, deliberately *not* inside the ch
|
||||
|
||||
| Path | Purpose |
|
||||
|---|---|
|
||||
| `ci/common/` | applied for every chart: the schema-less stub CRD for `OnePasswordItem` |
|
||||
| `ci/common/` | applied for every chart (client-side): the schema-less stub CRD for `OnePasswordItem` |
|
||||
| `ci/gateway-api-crds.sh` | the **real** Gateway-API CRDs, applied `--server-side` and only for a chart whose render actually contains an `HTTPRoute` — see below |
|
||||
| `ci/httproute/<chart>.yaml` | render-only overlay for the `httproute-match` job, never used at install time |
|
||||
| `ci/<chart>/test-values.yaml` | values for the CI install — **required for every installable chart** |
|
||||
| `ci/<chart>/fixtures.yaml` | dummy secrets, throwaway infrastructure (postgres/redis/dex), static hostPath PVs and storage classes, seed jobs |
|
||||
| `ci/<chart>/fixtures.env.yaml` | fixtures needing a value that must not be committed: `${VARIABLE}` placeholders only, rendered with `envsubst` and piped straight into `kubectl`, never printed |
|
||||
@@ -166,6 +189,22 @@ Substitution is restricted to the names in `REQUIRED_ENV`, so an undeclared vari
|
||||
|
||||
Fixture images avoid Docker Hub where possible (`public.ecr.aws`, `ghcr.io`) to dodge runner rate limits.
|
||||
|
||||
### The Gateway-API CRDs are real, pinned and applied server-side
|
||||
|
||||
Added with the Gateway-API pilot (Gitea #114). `ci/gateway-api-crds.sh` fetches the **standard channel at a pinned version** — tracked by Renovate through a `customManager` for `ci/*.sh`, the same `# renovate:` comment convention the workflows use for Trivy and `kubectl` — and applies it. `ci/run-install-test.sh` calls it only when the chart's render actually contains an `HTTPRoute`, detected from the render rather than declared per chart, so there is no list to maintain and the sweep enrols a chart simply by switching its fixture into Gateway mode. A false negative there is loud, not silent: without the CRDs `helm install` fails with `no matches for kind HTTPRoute in version gateway.networking.k8s.io/v1`.
|
||||
|
||||
Three decisions, each with the measurement behind it:
|
||||
|
||||
- **Not a stub, unlike `OnePasswordItem`.** A schema-less stub is right for that CR — nobody is testing the 1Password operator's API, only that the chart's object applies. For an `HTTPRoute` the **schema is the entire point of the exercise**: with a stub the API server accepts a malformed `parentRefs`, a port written as a string, a path type that does not exist, the install goes green, and the only thing demonstrated is "something was created". With the real CRDs the same apply is validated field by field.
|
||||
- **`kubectl apply --server-side`, and not out of taste.** Client-side apply stores the whole applied object in the `kubectl.kubernetes.io/last-applied-configuration` annotation, and an object's annotations may total at most **262144** bytes. The HTTPRoute CRD of the standard channel is **429367** bytes of YAML, which comes to **244792** bytes as JSON (measured on v1.6.3, re-read through `ci/yaml2json`) — 93 % of the limit, with every upstream release adding fields. Client-side apply therefore *happens to fit today* and is one release away from failing with `metadata.annotations: Too long` on a Renovate PR that has nothing to do with it. Server-side apply writes no such annotation and has no such limit.
|
||||
- **Not in `ci/common/`**, which is applied **client-side for every chart**: putting the CRDs there would hand that landmine to all twenty-odd charts and spend the download on charts that render no route at all.
|
||||
|
||||
It waits for `condition=Established` rather than for the objects to exist. `kubectl apply` returns as soon as the CRD object is created, while the API server needs a moment to serve the new endpoint — and a `helm install` in that window fails with `no matches for kind HTTPRoute`, which reads like a missing CRD rather than like a race. Same lesson as the Ready-node loop in `install-test.yml`: wait for the condition that means *usable*, not for the one that means *present*.
|
||||
|
||||
**Downloaded rather than vendored**, like Trivy's tarball, k3d's install script and `kubectl`. 1.1 MB of generated YAML in the repository would go stale invisibly and would bury every diff touching `ci/`. The price is named instead of hidden: one more network fetch per affected leg, and if the release asset ever disappears an unrelated pull request goes red — loudly, which is the acceptable half.
|
||||
|
||||
**What this still does not prove: that traffic flows.** No Gateway controller runs in k3d, so no route is ever reconciled and no status is ever written. Provable here: the object is schema-valid and the pod becomes `Ready`.
|
||||
|
||||
### Excluded charts are dropped from the matrix, not skipped inside it
|
||||
|
||||
An excluded chart used to get a job that ran the script, printed `SKIP` and exited 0 — and the forge knows only the exit code, so the check list showed `Install-test satisfactory ✓` for a chart whose image was never pulled, indistinguishable from one that really came up. `detect-changed-charts` now removes the excluded charts **before** the matrix is built, so no job exists and nothing claims they passed. The all-charts run does the same in its `select-charts` job.
|
||||
@@ -309,13 +348,13 @@ All three are recorded on [`Gemessenes-Verhalten`](https://git.jk-dev.de/workflo
|
||||
|
||||
Not a workflow but the prerequisite of every helm command in this repo: it fetches a chart's declared dependencies into `charts/`, and returns silently for the charts that declare none (all but one).
|
||||
|
||||
It runs in **six** places, because every helm command exits 1 while a declared dependency is missing: before lint and package in `validate-charts.yml`, before package in the shared publish block, and inside `ci/run-install-test.sh`, `ci/run-quantity-render-test.sh`, `ci/run-selector-match-test.sh` and `ci/run-trivy-config.sh`. The second of those is now across a repository boundary: the block calls this script from the calling repository's checkout, which makes the script part of the contract — renaming or deleting it breaks publishing from a place that no longer reads our diffs. Two of those call sites are the render tests that cover **every** chart on **every** PR — so a dependency-carrying chart can turn a third-party Helm repository's bad day into a red pipeline on an unrelated PR, now from either of them. Measured while writing the selector-match test: the registry answered `503` for several minutes and both tests went red on `stalwart`, `ytdlp-web-player` and `paperless-ngx` with nothing in the diff to blame.
|
||||
It runs in **seven** places, because every helm command exits 1 while a declared dependency is missing: before lint and package in `validate-charts.yml`, before package in the shared publish block, and inside `ci/run-install-test.sh`, `ci/run-quantity-render-test.sh`, `ci/run-selector-match-test.sh`, `ci/run-httproute-match-test.sh` and `ci/run-trivy-config.sh`. The second of those is now across a repository boundary: the block calls this script from the calling repository's checkout, which makes the script part of the contract — renaming or deleting it breaks publishing from a place that no longer reads our diffs. Three of those call sites are the render tests that cover **every** chart on **every** PR — so a dependency-carrying chart can turn a third-party Helm repository's bad day into a red pipeline on an unrelated PR, now from either of them. Measured while writing the selector-match test: the registry answered `503` for several minutes and both tests went red on `stalwart`, `ytdlp-web-player` and `paperless-ngx` with nothing in the diff to blame.
|
||||
|
||||
It runs `helm dependency update`, not `build`: `Chart.lock` is not committed, because Renovate rewrites `Chart.yaml` and cannot regenerate the lock, so a committed one would arrive stale.
|
||||
|
||||
## A PR that touches only `ci/`
|
||||
|
||||
Both matrices follow the changed **chart directories**, so a PR that only changes the harness exercises nothing by itself. The Trivy scan has a fixed-sample fallback for that case; the install-test has none — bump the affected chart's patch version to pull it into the matrix, or run the [all-charts install-test](#the-all-charts-install-test-on-demand) once the change is on `main`. The quantity render test and the selector-match test are the two exceptions to the rule: both always run over everything.
|
||||
Both matrices follow the changed **chart directories**, so a PR that only changes the harness exercises nothing by itself. The Trivy scan has a fixed-sample fallback for that case; the install-test has none — bump the affected chart's patch version to pull it into the matrix, or run the [all-charts install-test](#the-all-charts-install-test-on-demand) once the change is on `main`. The quantity render test, the selector-match test and the HTTPRoute render test are the three exceptions to the rule: all of them always run over everything.
|
||||
|
||||
## What a Gitea runner does differently
|
||||
|
||||
|
||||
+31
-2
@@ -26,9 +26,9 @@ See [Versioning](Versioning) for the full rules. Short form: `version: <chart-se
|
||||
|
||||
**Documented exception — minecraft-server.** The only chart whose main image tag can be overridden, through `image.tagOverride` (empty by default). `appVersion` is the tag and therefore fixes the JVM, and only the newest Java line is maintained out of the directory; the older lines stay published as frozen chart versions. Without the override, running an older JVM would mean installing a frozen chart and giving up every later fix — so the line is a decision at rollout time rather than a property of the chart. It is named `tagOverride`, not `tag`, because it decouples the running image from the chart version, and it is deliberately outside Renovate's reach. The chart checks the pairing itself: the Java line of the **effective** tag against `minecraft.version`, failing the render with both values named.
|
||||
|
||||
## 4. Ingress
|
||||
## 4. Ingress and Gateway API
|
||||
|
||||
- `ingress.domain` and `ingress.clusterIssuer` are `required` **without defaults** — every installation sets both explicitly; they are the only values a default install needs.
|
||||
- `ingress.domain` and `ingress.clusterIssuer` are `required` **without defaults** — every installation sets both explicitly; they are the only values a default install needs. `clusterIssuer` is required **only while `ingress.enabled`**: in Gateway mode the chart renders no certificate, and a value that is demanded and does nothing is exactly the defect [§12](#12-values-that-must-not-lie) exists to remove.
|
||||
- `ingress.className` defaults to `nginx`.
|
||||
- `ingress.annotations` takes optional extra annotations, merged with the cert-manager annotation the chart always sets.
|
||||
- The TLS secret is named `tls-<release>-<chart>-ingress`.
|
||||
@@ -41,6 +41,35 @@ See [Versioning](Versioning) for the full rules. Short form: `version: <chart-se
|
||||
|
||||
**Documented exception — freesailarr** has no `ingress.domain` at all and offers both shapes; see [its page](freesailarr).
|
||||
|
||||
### The Gateway API is the second routing mode
|
||||
|
||||
Added with the pilot on `cyberchef` (Gitea #114). A chart offers it through one values block, **off by default**, so adding it to an existing chart is additive — a **minor** bump:
|
||||
|
||||
```yaml
|
||||
httpRoute:
|
||||
enabled: false # counterpart to ingress.enabled
|
||||
parentRefs: [] # the Gateway (name, namespace, sectionName) — a platform object
|
||||
hostnames: [] # empty = [ingress.domain]
|
||||
```
|
||||
|
||||
**Implementation-neutral, and that is the load-bearing decision.** A chart renders **only** `HTTPRoute` with `parentRefs`: no `Gateway`, no `gatewayClassName`, no certificate, and **no** implementation-specific policy object (`ClientSettingsPolicy`, `ClientTrafficPolicy` and their equivalents). That subset looks identical on nginx-gateway-fabric, Cilium, Envoy Gateway and Istio — which is precisely why the implementation decision can still fall later without the charts being touched again. A policy object would bind a chart to one implementation *and* to a CRD whose presence it cannot check. Class and certificate therefore move to the **platform**: they sit on the Gateway and its listeners, in `cluster-config`.
|
||||
|
||||
The rest of the rules, each against a failure that reports nothing:
|
||||
|
||||
| Rule | What it prevents |
|
||||
|---|---|
|
||||
| **A value, never auto-detection** | `.Capabilities.APIVersions` is unreliable at template time, and an `HTTPRoute` without the Gateway-API CRDs fails at *install* time. A chart that guessed would sometimes render an object the cluster rejects and sometimes silently render nothing. |
|
||||
| **Mutually exclusive with `ingress.enabled`**, render abort naming **both** value paths | two data paths onto one Service under one hostname is a conflict at DNS level already. A migration runs over a **second hostname** (`httpRoute.hostnames` set explicitly while the Ingress keeps `ingress.domain`), never in parallel. |
|
||||
| **`parentRefs` mandatory in Gateway mode**, render abort on the empty list | a route without a parent attaches to nothing: created, schema-valid, rejected by nobody, routing nothing — no event, no condition, the only symptom a hostname that does not answer. |
|
||||
| **One source of truth for the hostname** — empty `hostnames` means `[ingress.domain]` | two values that have to agree drift apart. `ingress.domain` is read in Gateway mode too: it names the release's public hostname and is not an Ingress switch. |
|
||||
| **`NOTES.txt` names the active mode** and what it costs, as `keycloak` already does for its cases | a mode nobody can see from the outside |
|
||||
|
||||
**What has no counterpart, and must be said rather than rebuilt.** The Gateway API has no annotation channel for controller behaviour. `rewrite-target` has a real equivalent (the `URLRewrite` filter in a rule); **`proxy-body-size` has none** — an upload limit is implementation-specific policy. The five charts that set one (`bulwark`, `calibre-web-automated`, `outline`, `paperless-ngx`, `zipline`) must print in their Gateway-mode `NOTES.txt` that the limit is **not enforced** and has to be set at the Gateway, because whoever overlooks that silently inherits the Gateway's default and meets it as a proxy error instead of an application error. The same holds for auth annotations in front of an app with no login of its own: in Gateway mode that escape hatch does not exist in the chart at all — `cyberchef` is the live example.
|
||||
|
||||
**And the CI side is part of the convention, not an afterthought.** Either the chart's install-test fixture installs in Gateway mode, or the chart gets a render-only overlay at `ci/httproute/<chart>.yaml`. Without one of the two, `ci/run-httproute-match-test.sh` is green over nothing for that chart — which that test fails on. See [CI/CD](CI-CD#5-httproute-match-every-chart-every-pr).
|
||||
|
||||
Reference implementation: `template-chart/templates/xxx.httproute.yaml`. The remaining 20 charts follow in their own PRs, with the five special cases (`matrix-synapse` renders in a loop, `keycloak` and `bulwark` have two Ingresses each, `homeassistant` is the documented ingress exception, `garage` ships `ingress.enabled: false`, `stalwart` renders per node group) argued one at a time.
|
||||
|
||||
## 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 (`/tmp`, nginx cache dirs, s6-overlay's `/run`) are emptyDirs so the root filesystem stays read-only.
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@ grep -E '^(version|appVersion):' <chart>/Chart.yaml
|
||||
| [bulwark](bulwark) | Bulwark Webmail (JMAP client for [stalwart](stalwart)) | StatefulSet + **one** PVC | yes, at a host **root** only | **the volume beats the chart, for every value, silently** — `admin override > env var > default`, the admin dashboard may write any key, and a `helm upgrade` can succeed and change nothing. Only three values escape it. The way out of an OIDC lockout is therefore **not** `helm upgrade` with `oauth.only: false` but removing the override in the dashboard — which is why `ADMIN_PASSWORD` is set. Both secrets use the **file** form, mounted without `subPath` so rotation needs no restart. Probes carry **nothing** about the mail server: with Stalwart gone the pod is green |
|
||||
| [calibre-web-automated](calibre-web-automated) | Calibre-Web Automated | StatefulSet + extra consume PVC | yes | keeps `readOnlyRootFilesystem: false` on purpose — see [§12.4](Chart-Style-Guide#12-values-that-must-not-lie) |
|
||||
| [convertx](convertx) | ConvertX | StatefulSet + data PVC | yes | **the only chart that overrides `command` to bypass its image entrypoint** — that is what keeps the hardening baseline intact on a rootful image, at the price of re-reading the entrypoint at every app-version bump; probes use `/healthcheck` because `/` is a 302 |
|
||||
| [cyberchef](cyberchef) | CyberChef | Deployment | yes | stateless; the simplest chart in the repo and a good first read |
|
||||
| [cyberchef](cyberchef) | CyberChef | Deployment | yes, **or** a Gateway-API `HTTPRoute` | stateless; the simplest chart in the repo and a good first read — and the **Gateway-API pilot** (Gitea #114): the first chart offering `httpRoute.enabled` as the exclusive alternative to `ingress.enabled`, `HTTPRoute` with `parentRefs` only, no Gateway and no certificate. Also the one chart whose install-test runs in Gateway mode, because that test installs one configuration per chart |
|
||||
| [freesailarr](freesailarr) | gluetun VPN media stack, ten containers | single-pod StatefulSet | per app | the exception to many rules: no icon, no app-version-driven tags, two ingress shapes |
|
||||
| [garage](garage) | Garage (S3-compatible object store) | StatefulSet + **two** PVCs | off by default | **one** node at `replication_factor = 1` — which applies to the metadata too, so `metadata_fsync`, the metadata snapshots and a second PVC for them are mandatory rather than optional; `FROM scratch` image, so no shell and no exec probes ever; the RPC secret has to be an env var; readiness is `/health`, liveness and startup deliberately are not |
|
||||
| [gotenberg](gotenberg) | Gotenberg | Deployment + Service | no | PDF conversion for [paperless-ngx](paperless-ngx); sets every limit explicitly |
|
||||
|
||||
@@ -488,6 +488,19 @@ The second is the one that matters here, because a dropped `labelSelector` is ex
|
||||
|
||||
**Now:** where a chart renders an entry into a list the user can also append to, the chart **aborts the render on a collision and names both values** — the switch that produced its own entry and the list the user wrote. The abort fires only while the chart's entry is actually rendered, so it cannot trip a configuration that has no duplicate. The generalisation beyond this field: **a list merged by a key is a namespace, and two writers into one namespace need a referee at render time** — the API's own answer is either an index or a silent merge, and neither names a value anybody set.
|
||||
|
||||
## The stub CRD that would have proved nothing
|
||||
|
||||
The Gateway-API pilot ([cyberchef](cyberchef), Gitea #114) needed an `HTTPRoute` to be installed somewhere, and this repo already had a pattern for a CRD the CI cluster does not carry: `ci/common/` holds a **schema-less** stub for `OnePasswordItem` (`x-kubernetes-preserve-unknown-fields`). Reaching for it a second time was the obvious move and the wrong one.
|
||||
|
||||
The stub is right for `OnePasswordItem`. Nobody is testing the 1Password operator's API there — only that the chart's CR applies at all, so the absence of a schema costs nothing. For an `HTTPRoute` the schema **is** the whole exercise. Against a schema-less stub the API server cheerfully accepts a malformed `parentRefs`, a port written as a string, a `path.type` that does not exist; the install goes green, and the only thing demonstrated is *"something was created"* — which is the class of signal this repository removes rather than adds.
|
||||
|
||||
**Now:** the real CRDs (standard channel, pinned, Renovate-tracked, applied `--server-side`) are fetched by `ci/gateway-api-crds.sh` for any chart whose render contains an `HTTPRoute`. Two further findings came out of doing it properly, both measured rather than assumed:
|
||||
|
||||
- **The schema still cannot catch the two failures that matter.** It accepts an **empty** `parentRefs` list — a route attached to no Gateway, created, valid, rejected by nobody, routing nothing — and it accepts a `backendRefs` entry naming a Service that was never created, because that reference is resolved **by name** at runtime. Real CRDs prove an object is *well-formed*; they say nothing about whether it *points somewhere*. That half needed a render test of its own ([`httproute-match`](CI-CD#5-httproute-match-every-chart-every-pr)), and the generalisation is worth keeping: **validating against a real schema and validating against the rest of the render are two different checks, and the first one is the one that looks sufficient.**
|
||||
- **Client-side `kubectl apply` was 93 % of the way to a failure nobody would have attributed correctly.** It stores the whole applied object in the `last-applied-configuration` annotation, annotations total at most **262144** bytes, and that one CRD is **244792** bytes as JSON — fitting today, one upstream release away from `metadata.annotations: Too long` on a Renovate PR that touches nothing related. Hence `--server-side`, which writes no such annotation. **A limit one release away is not headroom**, and the apply mode is part of a fixture's definition rather than a detail of how it happens to be installed.
|
||||
|
||||
And the part that stayed honest: **none of this proves that traffic flows.** No Gateway controller runs in k3d, so no route is ever reconciled and no status is ever written. What the pipeline claims is exactly what it checks — schema-valid object, `Ready` pod, a route pointing at a Service and a port the same render creates.
|
||||
|
||||
## The recurring shape
|
||||
|
||||
- **Green is a claim about the assertions that ran, not about the system.**
|
||||
@@ -525,4 +538,5 @@ The second is the one that matters here, because a dropped `labelSelector` is ex
|
||||
- **A protection that rests on a field can be lost by the field being absent.** Beta fields are pruned without a word on an older API server, so the question is not only "is the value right" but "does this version keep it at all" — with the minimum version written at the value.
|
||||
- **A documented symptom that does not occur is worse than no documentation**, because somebody will wait for it instead of looking.
|
||||
- **A list merged by a key is a namespace, and two writers into one namespace need a referee at render time.** The API's own answers are an error naming a list index and a silent merge that drops one half; neither names a value anybody set.
|
||||
- **A fixture is defined by what it validates, not by what it stands in for.** The same trick — a schema-less stub CRD — is correct for one API and worthless for the next, and the question that separates them is whether the schema is the thing under test. Reusing a pattern because the shape matches is how a green run comes to mean "something was created".
|
||||
- **When the fix needs a privilege, ask whether two cheap steps can replace one privileged one.** Splitting a pre-build into a seeding container plus a build container kept the hardening baseline intact across the whole pod, where the shorter single-container version would have bought a documented deviation *and* a suppression that silences the container that actually serves traffic.
|
||||
+2
-1
@@ -35,7 +35,7 @@ The schema and the null-safe fallback helpers come with the template, but their
|
||||
|
||||
1. **`.github/workflows/publish-<chart-name>.yml`** — copy an existing one (e.g. `publish-outline.yml`). It triggers on pushes to `main` touching `<chart-name>/**` and calls `publish-helm-chart.yml`, which lives in `workflow-util/pipelines` and is bound to a tag. **Copy, do not write from memory:** the `uses:` path, the tag, `registry-owner: ${{ github.repository_owner }}` and `secrets: inherit` all have to be right, and a hull missing the owner input fails only when the publish actually runs. Without the file at all the chart is never published, and nothing complains. See [CI/CD](CI-CD#what-a-caller-here-supplies).
|
||||
2. **`.github/workflows/change-app-version.yml`** — add the chart to the `chart-name` choice list.
|
||||
3. **`ci/<chart-name>/`** — install-test fixtures: `test-values.yaml` (required), plus `fixtures.yaml`, `fixtures.env.yaml` and `settings.env` where the chart needs them. Alternatively an entry **with justification** in `ci/excluded-charts.txt`. The install-test fails if both are missing. See [CI/CD](CI-CD#install-test-fixtures).
|
||||
3. **`ci/<chart-name>/`** — install-test fixtures: `test-values.yaml` (required), plus `fixtures.yaml`, `fixtures.env.yaml` and `settings.env` where the chart needs them. Alternatively an entry **with justification** in `ci/excluded-charts.txt`. The install-test fails if both are missing. See [CI/CD](CI-CD#install-test-fixtures). If the chart offers Gateway mode ([§4](Chart-Style-Guide#4-ingress-and-gateway-api)), either the fixture installs in that mode or the chart gets a render-only overlay at `ci/httproute/<chart-name>.yaml` — without one of the two the `httproute-match` job is green over nothing for it, which that job fails on.
|
||||
4. **`renovate.json5`** — a `customManagers` block for the chart's main image. Without it the app version is never updated. Check the image's tag list first: a flavour, date or prerelease suffix needs a `versioningTemplate` **with a comment saying why**, and floating tags like `latest`/`stable` belong outside the pattern. See [Renovate](Renovate#a-capture-group-is-a-claim-about-how-the-value-is-compared).
|
||||
|
||||
## 4. Only if the chart needs a dependency
|
||||
@@ -70,6 +70,7 @@ A new chart needs a [wiki page](Workflow#the-wiki-is-part-of-the-work) in the sa
|
||||
- [ ] publish workflow added
|
||||
- [ ] `change-app-version.yml` choice list extended
|
||||
- [ ] CI fixtures or a justified exclusion
|
||||
- [ ] if the chart offers Gateway mode: its fixture installs in that mode, or `ci/httproute/<chart>.yaml` exists
|
||||
- [ ] `renovate.json5` block added
|
||||
- [ ] `helm lint --strict` clean, render verified, k3d install verified
|
||||
- [ ] wiki page, chart index row, sidebar entry
|
||||
+5
-2
@@ -19,9 +19,12 @@ The `jlab-renovate` GitHub App that used to do this is switched off along with t
|
||||
| **All** images of `freesailarr`, its main one included | built-in `helm-values` manager — the chart pins every tag in `values.yaml` and has no app-version block. Exception: `mediathekarr` is pinned by **digest** and has its own `customManagers` block |
|
||||
| The oauth2-proxy subchart of `ytdlp-web-player` | built-in `helmv3` manager — repository URL and version sit together in `Chart.yaml`, so no custom manager is needed |
|
||||
| GitHub Actions | built-in `github-actions` manager |
|
||||
| Tool versions pinned as workflow env vars (`TRIVY_VERSION`) | a `# renovate: datasource=… depName=…` comment above the line |
|
||||
| Tool versions pinned as workflow env vars (`TRIVY_VERSION`, `KUBECTL_VERSION`) | a `# renovate: datasource=… depName=…` comment above the line, through a `customManagers` entry for `.github/workflows/*.yml` |
|
||||
| Tool versions pinned in a **`ci/` shell script** (`GATEWAY_API_VERSION` in `ci/gateway-api-crds.sh`) | the same comment convention, through a second `customManagers` entry for `ci/*.sh` — the assignment there is `VAR=value` rather than the workflow's `VAR: value`, which is why it is a separate pattern |
|
||||
|
||||
**Not managed:** `ci/**` (install-test fixtures pin throwaway infrastructure to major lines on purpose) and `template-chart/**` (placeholder image). A consequence worth knowing: `ci/matrix-synapse/fixtures.yaml` pins a Synapse image Renovate will not follow — bump it by hand when the chart moves a major.
|
||||
**Not managed:** the **fixture images** under `ci/<chart>/` (they pin throwaway infrastructure to major lines on purpose) and `template-chart/**` (placeholder image). A consequence worth knowing: `ci/matrix-synapse/fixtures.yaml` pins a Synapse image Renovate will not follow — bump it by hand when the chart moves a major.
|
||||
|
||||
The blanket "`ci/**` is not managed" was true until the Gateway-API pilot (Gitea #114) put a pinned **tool** version in `ci/gateway-api-crds.sh`. The distinction that replaced it is the one that was always meant: a fixture's *image* is pinned deliberately and stays unmanaged, while a *tool* fetched at a pinned version is tracked wherever it lives — in a workflow or in a `ci/` script. The second `customManagers` entry is deliberately a separate block rather than a widened `managerFilePatterns` on the first: that one is dry-run verified and covers every workflow, and broadening it would put the verification back in question for a second file shape (see [A capture group is a claim](#a-capture-group-is-a-claim-about-how-the-value-is-compared) — a valid rule is not a matching rule).
|
||||
|
||||
## Why the mapping lives in `renovate.json5`
|
||||
|
||||
|
||||
+77
-8
@@ -1,6 +1,6 @@
|
||||
# cyberchef
|
||||
|
||||
Deploys [CyberChef](https://github.com/gchq/CyberChef), the "Cyber Swiss Army Knife", as a stateless Deployment behind an Ingress. Everything the app does happens in the visitor's browser; the pod only serves static files. This is the smallest chart in the repo and the shortest path to seeing every convention of the [chart style guide](Chart-Style-Guide) in one place.
|
||||
Deploys [CyberChef](https://github.com/gchq/CyberChef), the "Cyber Swiss Army Knife", as a stateless Deployment behind an Ingress **or** a Gateway-API `HTTPRoute`. Everything the app does happens in the visitor's browser; the pod only serves static files. This is the smallest chart in the repo and the shortest path to seeing every convention of the [chart style guide](Chart-Style-Guide) in one place.
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
@@ -8,16 +8,25 @@ Deploys [CyberChef](https://github.com/gchq/CyberChef), the "Cyber Swiss Army Kn
|
||||
| Versions | not listed on purpose — read the current pair off `cyberchef/Chart.yaml`, see [Chart index](Charts) |
|
||||
| Workloads | Deployment (1 replica) |
|
||||
| Storage | none — one emptyDir on `/tmp` for nginx |
|
||||
| Ingress | yes, `ingress.domain` + `ingress.clusterIssuer` required while `ingress.enabled` |
|
||||
| Routing | **two mutually exclusive modes.** Ingress (default): `ingress.domain` + `ingress.clusterIssuer` required while `ingress.enabled`. Gateway API: `httpRoute.enabled` with `httpRoute.parentRefs` — see [Routing modes](#routing-modes) |
|
||||
| External dependencies | none |
|
||||
| Secrets | none |
|
||||
| Install-test | `ci/cyberchef/` (test values only, no fixtures) |
|
||||
| Install-test | `ci/cyberchef/` (test values only, no fixtures) — **installed in Gateway mode**, see [CI](#ci) |
|
||||
|
||||
## Prerequisites
|
||||
|
||||
**In Ingress mode (the default):**
|
||||
|
||||
- **cert-manager** with the `ClusterIssuer` named in `ingress.clusterIssuer`, and DNS for `ingress.domain` pointing at the ingress controller. With `ingress.enabled: false` neither is needed.
|
||||
- Nothing else. No database, no vault item, no volume — the two ingress values are the whole input of an installation.
|
||||
|
||||
**In Gateway mode (`httpRoute.enabled: true`):**
|
||||
|
||||
- The **Gateway-API CRDs**, standard channel. The chart does not and cannot detect them, so without them the install fails with `no matches for kind HTTPRoute`.
|
||||
- A **Gateway** whose listener accepts routes from this namespace (`allowedRoutes`), named in `httpRoute.parentRefs`. The chart renders no Gateway.
|
||||
- The **certificate on that Gateway's listener**. `ingress.clusterIssuer` is unused and unnecessary here — the chart renders no `Certificate` in this mode.
|
||||
- `ingress.domain` is still needed (it is the hostname, not an Ingress switch) unless `httpRoute.hostnames` is set explicitly.
|
||||
|
||||
## Installation
|
||||
|
||||
The registry URL and its credentials live in 1Password (item `jlab-registry-access`: `username`, `password`, `charts-url`) and never in a repository or a shell history file.
|
||||
@@ -39,6 +48,20 @@ ingress:
|
||||
clusterIssuer: letsencrypt
|
||||
```
|
||||
|
||||
The same installation in Gateway mode:
|
||||
|
||||
```yaml
|
||||
ingress:
|
||||
enabled: false
|
||||
domain: cyberchef.example.com
|
||||
httpRoute:
|
||||
enabled: true
|
||||
parentRefs:
|
||||
- name: public
|
||||
namespace: gateway
|
||||
sectionName: https
|
||||
```
|
||||
|
||||
`helm install` prints the chart's `NOTES.txt`, which renders the same information against the values actually set. Read it — it is the per-release version of this page.
|
||||
|
||||
## Configurable values
|
||||
@@ -71,24 +94,59 @@ An erased block (`cyberchef.securityContext:` with no children, a `null` probe,
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
|---|---|---|---|
|
||||
| `ingress.enabled` | bool | `true` | `false` omits the Ingress entirely. Nothing outside the Ingress template reads `domain` or `clusterIssuer`, so both become unnecessary with it — the rare chart where the gate frees *both* values. |
|
||||
| `ingress.enabled` | bool | `true` | `false` omits the Ingress entirely. **Mutually exclusive with `httpRoute.enabled`** — with both `true` the render aborts naming both values. Nothing outside the routing templates reads `clusterIssuer`, so it becomes unnecessary with the gate; `domain` is also read by the `HTTPRoute` when `httpRoute.hostnames` is empty. |
|
||||
| `ingress.domain` | string | `null` | **Required while `ingress.enabled` is true.** Ingress host and TLS SAN. |
|
||||
| `ingress.clusterIssuer` | string | `null` | **Required while `ingress.enabled` is true.** cert-manager `ClusterIssuer`. |
|
||||
| `ingress.className` | string | `nginx` | Ingress class. |
|
||||
| `ingress.annotations` | object | `{}` | Merged with the cert-manager annotation the chart always sets. The place to bolt authentication on — see below. |
|
||||
| `ingress.annotations` | object | `{}` | Merged with the cert-manager annotation the chart always sets. The place to bolt authentication on — see below. **No counterpart in Gateway mode**: the Gateway API has no annotation channel, so everything set here is gone in that mode. |
|
||||
|
||||
TLS is stored in `tls-<release>-cyberchef-ingress`.
|
||||
|
||||
### `httpRoute`
|
||||
|
||||
The Gateway-API alternative, off by default. Added with the Gateway-API pilot (Gitea #114) — this is the first chart in the repo to carry it, and the form is now binding for all of them ([style guide §4](Chart-Style-Guide#4-ingress-and-gateway-api)).
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
|---|---|---|---|
|
||||
| `httpRoute.enabled` | bool | `false` | Renders `<release>-cyberchef-httproute` instead of the Ingress. **Mutually exclusive with `ingress.enabled`.** |
|
||||
| `httpRoute.parentRefs` | list | `[]` | The Gateway(s) the route attaches to — Gateway API `ParentReference` entries (`name`, and optionally `namespace`, `sectionName`, `port`, `group`, `kind`), passed through unchanged. **Required while `httpRoute.enabled` is true**: the render aborts on an empty list. The schema requires `name` on every entry and is otherwise left open, because Kubernetes owns that schema. |
|
||||
| `httpRoute.hostnames` | list | `[]` | Empty means `[ingress.domain]`, so the hostname has a single source of truth. Set it explicitly only to publish under a different or an additional name — which is how a migration off the Ingress runs. |
|
||||
|
||||
What the chart renders in this mode is **only** the `HTTPRoute`: one rule, `PathPrefix: /`, one `backendRefs` entry pointing at `<release>-cyberchef-service` on port **80**.
|
||||
|
||||
## Routing modes
|
||||
|
||||
The two modes are exclusive by design, and three of the four rules exist because of a failure that reports nothing:
|
||||
|
||||
| Rule | What it prevents |
|
||||
|---|---|
|
||||
| `ingress.enabled` and `httpRoute.enabled` cannot both be true — the render aborts naming **both** value paths | two data paths onto one Service under one hostname. That is a conflict at DNS level already: one name, two addresses, and which one a client reaches is luck. **A migration runs over a second hostname** (`httpRoute.hostnames` set explicitly while the Ingress keeps `ingress.domain`), never in parallel. |
|
||||
| `httpRoute.parentRefs` must not be empty — the render aborts | a route without a parent attaches to **no** Gateway. The object is created, it is schema-valid, no controller rejects it, and it routes nothing: no event, no condition, and the only symptom is a hostname that does not answer. |
|
||||
| the mode is a **value**, never auto-detected | `.Capabilities.APIVersions` is unreliable at template time, and an `HTTPRoute` without the CRDs fails at install time. A chart that guessed would sometimes render an object the cluster rejects and sometimes silently render nothing. |
|
||||
| empty `hostnames` means `[ingress.domain]` | two values that have to agree drift apart. |
|
||||
|
||||
**What moves to the platform, and what is simply gone.** In Gateway mode the chart renders no `Gateway`, no `gatewayClassName`, no `Certificate` and — deliberately — no implementation-specific policy object (`ClientSettingsPolicy`, `ClientTrafficPolicy` and their equivalents). Only `HTTPRoute` with `parentRefs` looks identical on nginx-gateway-fabric, Cilium, Envoy Gateway and Istio, and keeping to that subset is what lets the cluster's implementation be chosen, or replaced, without touching the chart again. Class and certificate therefore belong to the Gateway and its listeners. `ingress.annotations` has **no** counterpart at all — relevant for this chart in particular, see the next section.
|
||||
|
||||
**Checking an attachment.** A refused attachment is visible only in the route's own status, which is written by the implementation:
|
||||
|
||||
```bash
|
||||
kubectl -n <namespace> get httproute <release>-cyberchef-httproute -o yaml
|
||||
```
|
||||
|
||||
`Accepted: True` and `ResolvedRefs: True` under `status.parents[]` is a working attachment. An **empty** status means no controller has looked at the route at all — usually a `parentRefs` pointing at a Gateway that does not exist, or a listener whose `allowedRoutes` does not admit this namespace.
|
||||
|
||||
## Operating notes
|
||||
|
||||
### Anyone who reaches the URL can use it
|
||||
|
||||
CyberChef has no login and this chart adds none. If the instance must not be open to the internet, put authentication in front of it through `ingress.annotations` (ingress-nginx external auth or basic auth), or leave `ingress.enabled: false` and reach it with a port-forward:
|
||||
CyberChef has no login and this chart adds none. **In Ingress mode** the escape hatch is `ingress.annotations` (ingress-nginx external auth or basic auth), or leaving `ingress.enabled: false` and reaching it with a port-forward:
|
||||
|
||||
```bash
|
||||
kubectl -n <namespace> port-forward svc/<release>-cyberchef-service 8080:80
|
||||
```
|
||||
|
||||
**In Gateway mode that escape hatch does not exist in the chart**, and this is the sharpest consequence of the routing switch for this particular chart. The Gateway API has no annotation channel, so the external-auth annotations have no counterpart, and the chart renders no implementation-specific policy object on purpose. Protection then has to come from the Gateway itself: a listener that is not publicly reachable, or that implementation's own auth filter/policy, configured next to the Gateway. The `NOTES.txt` of a Gateway-mode release says so, because "put auth in front of it via `ingress.annotations`" is advice that silently stops applying.
|
||||
|
||||
### `cyberchef.env` reaches nginx, not CyberChef
|
||||
|
||||
The served bundle is static JavaScript: recipes and inputs are processed in the visitor's browser and never reach the server. Environment variables land in the nginx process that serves the files and are read by nothing else. The `NOTES.txt` says so explicitly whenever the list is non-empty, because the setting looks like it should work.
|
||||
@@ -99,7 +157,7 @@ Since app version 11.0.0 the upstream image is built on `nginxinc/nginx-unprivil
|
||||
|
||||
### Scaled to zero
|
||||
|
||||
With `scaleDown: true` (or `cyberchef.replicas: 0`) the Deployment runs at zero and the URL stops answering, while Service and Ingress remain — the certificate is not re-issued when you scale back up.
|
||||
With `scaleDown: true` (or `cyberchef.replicas: 0`) the Deployment runs at zero and the URL stops answering, while Service and Ingress remain — the certificate is not re-issued when you scale back up. In Gateway mode the Service and the `HTTPRoute` remain in the same way, so the route keeps pointing at a Service with no endpoints and the Gateway answers with an error of its own rather than with nothing.
|
||||
|
||||
## Upgrades and backup
|
||||
|
||||
@@ -107,10 +165,21 @@ There is nothing to back up and nothing to migrate. The pod holds no state, so a
|
||||
|
||||
## CI
|
||||
|
||||
`ci/cyberchef/test-values.yaml` sets only `ingress.domain: cyberchef.ci.local` and `ingress.clusterIssuer: ci`; the Ingress merely has to apply, since the CI cluster runs neither ingress-nginx nor cert-manager. No `fixtures.yaml` exists — there is nothing to fake. Because it installs fast and depends on nothing, cyberchef is also the chart the README's measured install-test runtimes are taken against. See [CI/CD](CI-CD).
|
||||
**This chart's install-test runs in Gateway mode, and that is a deliberate trade** (Gitea #114). The install-test installs exactly one configuration per chart and the two modes are exclusive, so one of them had to be chosen.
|
||||
|
||||
`ci/cyberchef/test-values.yaml` therefore sets `ingress.enabled: false`, `httpRoute.enabled: true` with a `parentRefs` entry, and keeps `ingress.domain` as the hostname. No `fixtures.yaml` exists — there is nothing to fake.
|
||||
|
||||
What is given up: the Ingress of this chart is no longer *installed* anywhere. That costs little. An `Ingress` is a built-in GA type the API server has validated for a decade, the CI cluster's own ingress controller is traefik while the chart asks for class `nginx`, so nothing ever routed through it there either — the install only proved "the API server accepted an Ingress", and that keeps being proved by twenty other charts plus `helm lint --strict`, which renders this chart's **defaults** (Ingress mode) on every PR.
|
||||
|
||||
What is gained: a real `HTTPRoute` applied against the **real** Gateway-API CRDs, fetched at a pinned version by `ci/gateway-api-crds.sh` and applied `--server-side`. A schema-less stub CRD would have accepted a malformed route and the test would have proved only that something was created. See [CI/CD](CI-CD) for the CRD fixture and for the `httproute-match` render test that holds the half the CRDs cannot.
|
||||
|
||||
**Not proved, and it cannot be: that traffic flows.** No Gateway controller runs in the CI cluster, so nothing routes. What is proved is that the object is schema-valid and the pod becomes `Ready`.
|
||||
|
||||
Because it installs fast and depends on nothing, cyberchef is also the chart the README's measured install-test runtimes are taken against.
|
||||
|
||||
## See also
|
||||
|
||||
- [Chart style guide](Chart-Style-Guide) — the rules this chart implements, in their plainest form
|
||||
- [Versioning](Versioning) — what a bump of `9.0.1+up11.4.0` means
|
||||
- [CI/CD](CI-CD) — the `httproute-match` render test and the Gateway-API CRD fixture
|
||||
- [Chart index](Charts)
|
||||
+1
-1
@@ -112,7 +112,7 @@ All three are `subPath` mounts from a Secret volume, which makes them **read-onl
|
||||
|
||||
### `ingress`
|
||||
|
||||
Documented exception to [style guide §4](Chart-Style-Guide#4-ingress): this chart keeps the upstream-style flexible block. There is no `ingress.domain`, no `ingress.clusterIssuer`, and **no required value here at all** — only `className` follows the repo standard.
|
||||
Documented exception to [style guide §4](Chart-Style-Guide#4-ingress-and-gateway-api): this chart keeps the upstream-style flexible block. There is no `ingress.domain`, no `ingress.clusterIssuer`, and **no required value here at all** — only `className` follows the repo standard.
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
|---|---|---|---|
|
||||
|
||||
+18
-2
@@ -11,6 +11,7 @@ This page is the practical companion to [Creating a new chart](New-Chart): what
|
||||
| Workloads | a Deployment **and** a StatefulSet — the two shapes to choose between |
|
||||
| Storage | one RWO PVC `<release>-xxx-pvc` (optional for a copy) |
|
||||
| Ingress | yes, gated by `ingress.enabled`; `domain` + `clusterIssuer` required |
|
||||
| Gateway API | yes, `httpRoute.enabled` (off by default) — the reference implementation of [style guide §4](Chart-Style-Guide#4-ingress-and-gateway-api) |
|
||||
| External dependencies | none |
|
||||
| Secrets | 1 × `OnePasswordItem` (placeholder field `secretKey`) |
|
||||
| Install-test | **excluded** (`ci/excluded-charts.txt`) |
|
||||
@@ -37,7 +38,7 @@ helm template template-chart \
|
||||
## What to replace, and in what order
|
||||
|
||||
1. **Copy the directory**: `cp -r template-chart <chart-name>`. Everything below happens inside the copy.
|
||||
2. **`Chart.yaml`** — `name` (this is the `xxx` in every rendered object name), `description`, `icon` (mandatory in every chart), `version` and `appVersion`. `appVersion` is the **exact image tag**; `version` is `<chart-semver>+up<appVersion>` with dashes normalized to dots ([Versioning](Versioning)). The scaffold's `4.1.0+upxxx` is its own version and carries nothing into the copy.
|
||||
2. **`Chart.yaml`** — `name` (this is the `xxx` in every rendered object name), `description`, `icon` (mandatory in every chart), `version` and `appVersion`. `appVersion` is the **exact image tag**; `version` is `<chart-semver>+up<appVersion>` with dashes normalized to dots ([Versioning](Versioning)). The scaffold's `4.2.0+upxxx` is its own version and carries nothing into the copy.
|
||||
3. **File names**: `templates/xxx.<kind>.yaml` → `templates/<app>.<kind>.yaml`. The `<app>` part is the chart name; the `<kind>` part is what the object is (`deployment`, `sfs`, `service`, `secret`, `configmap`, `ingress`, `pvc`).
|
||||
4. **Decide the workload shape** and delete the other file. Stateful apps take `<app>.sfs.yaml`; if you keep it, move the Service selector from `…-deployment` to `…-sfs`. Delete `xxx.pvc.yaml` if the app needs no volume, and `xxx.secret.yaml` plus the `secrets` block if it needs no 1Password item.
|
||||
5. **`values.yaml`**: rename the `xxx:` top-level key to the app, rename `secrets.xxxSecretRef`, and replace the placeholder `env`, `ports` and probe content with the app's real ones.
|
||||
@@ -47,7 +48,8 @@ helm template template-chart \
|
||||
9. **The image** in the workload: replace host/org/image, keep the registry host — fully qualified on purpose, because an unqualified `registry/image` resolves to Docker Hub. Keep the tag as `{{ .Chart.AppVersion }}`.
|
||||
10. **The probes**: point them at a real health endpoint, and carry the probe's *context* into the fallback defaults in `_helpers.tpl`, not just its timings. A rebuilt default that loses a port name, a `Host` header or an exec command leaves the workload with a probe that cannot succeed.
|
||||
11. **`templates/NOTES.txt`**: delete the leading `{{/* … */}}` block (it explains the bar and renders nothing) and replace the body with what an operator of *your* app needs — the rendered access URL, **which fields the 1Password item must carry**, the options actually active in this release, and the chart's traps, all conditional on the values really set. No real domain anywhere; `example.com` where an example is unavoidable.
|
||||
12. **Check where `ingress.domain` is read.** In the scaffold it is read once outside the Ingress template (the `NORMAL_ENV` entry), which is why that read carries a `required()` of its own — otherwise switching the Ingress off would render an empty address silently. Decide this per chart instead of assuming either way.
|
||||
12. **Check where `ingress.domain` is read.** In the scaffold it is read outside the Ingress template twice — the `NORMAL_ENV` entry and the `HTTPRoute`'s hostname fallback — which is why the `env` read carries a `required()` of its own; otherwise switching the Ingress off would render an empty address silently. Decide this per chart instead of assuming either way.
|
||||
13. **Decide the routing modes.** `httpRoute` ships off, so a copy works unchanged in Ingress mode. If the app offers Gateway mode, say in its `NOTES.txt` what that mode costs it — an upload limit set through `proxy-body-size` has **no** Gateway-API equivalent, and an auth annotation has none either.
|
||||
|
||||
Then wire up CI — the four steps that live outside the chart directory: a `publish-<chart>.yml` workflow, the chart name in the `change-app-version.yml` choice list, `ci/<chart>/` fixtures (or a justified line in `ci/excluded-charts.txt`), and a `customManagers` block in `renovate.json5`. Details in [Creating a new chart](New-Chart) and [Renovate](Renovate).
|
||||
|
||||
@@ -118,6 +120,20 @@ Probe, `securityContext` and `resources` blocks may be erased (`null`) — the c
|
||||
|
||||
TLS is stored in `tls-<release>-xxx-ingress`.
|
||||
|
||||
### `httpRoute`
|
||||
|
||||
The Gateway-API alternative, off by default and **mutually exclusive** with `ingress.enabled` — with both true the render aborts naming both values. `templates/xxx.httproute.yaml` is the reference implementation of the convention; the whole argument is in [style guide §4](Chart-Style-Guide#4-ingress-and-gateway-api).
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
|---|---|---|---|
|
||||
| `httpRoute.enabled` | bool | `false` | Renders `<release>-xxx-httproute` instead of the Ingress. |
|
||||
| `httpRoute.parentRefs` | list | `[]` | Gateway API `ParentReference` entries, passed through unchanged. **Required while enabled**: the render aborts on an empty list, because a route without a parent attaches to nothing and reports nothing. The schema requires `name` per entry and stays otherwise open — Kubernetes owns that schema. |
|
||||
| `httpRoute.hostnames` | list | `[]` | Empty means `[ingress.domain]`, one source of truth for the hostname. |
|
||||
|
||||
The only thing to adapt when copying the file is the **backend**: the Service name follows the naming scheme, but `backendRefs` takes a port **number** and cannot reference a port by name the way the Ingress backend does — so the number has to match `<app>.service.yaml` by hand, and `ci/run-httproute-match-test.sh` is what holds that pair on every PR.
|
||||
|
||||
Note also what the scaffold demonstrates on purpose: `ingress.domain` **stays required in Gateway mode here**, because the `env` block reads it for `NORMAL_ENV`. That is a property of this chart, not of the convention — check it per chart ([§12.1](Chart-Style-Guide#12-values-that-must-not-lie)).
|
||||
|
||||
## Traps the scaffold carries into the copy
|
||||
|
||||
### Why 8080 and not 80
|
||||
|
||||
Reference in new issue
Block a user