1 Commits
Author SHA1 Message Date
jlab-renovate-bot b183b3f4fe Update urlaubsverwaltung/urlaubsverwaltung Docker tag to v6.17.0
renovate/stability-days Updates have met minimum release age requirement
Validate Helm Charts / Detect changed charts (pull_request) Successful in 3s
Validate Helm Charts / Resource quantities (pull_request) Successful in 11s
Validate Helm Charts / Selector match (pull_request) Successful in 11s
Validate Helm Charts / Build urlaubsverwaltung (pull_request) Successful in 6s
Validate Helm Charts / Trivy config urlaubsverwaltung (pull_request) Successful in 14s
Install-test / Install-test urlaubsverwaltung (pull_request) Successful in 1m40s
Validate Helm Charts / Install-test (pull_request) Successful in 1m39s
2026-10-12 09:36:04 +00:00
18 changed files with 37 additions and 1225 deletions

No files matched your search

-34
View File
@@ -203,40 +203,6 @@ jobs:
- name: "Check every rendered selector against the rendered pod labels"
run: ci/run-selector-match-test.sh
httproute-match:
name: "HTTPRoute match"
runs-on: ubuntu-latest
# The third job that ignores the changed-chart matrix and runs over *every*
# chart (Gitea #114), 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 about to be copied into 20 more charts when the sweep runs.
#
# What it asserts: every HTTPRoute a chart renders names a Service, on a
# port, that the SAME render creates, and its parentRefs is not empty. Both
# failures are SILENT and neither is reachable from the other checks:
# parentRefs: [] attaches the route to no Gateway (created, schema-valid,
# routes nothing, no event and no condition), and a backendRef is resolved
# by name with nothing validating that the name exists. The real Gateway-API
# CRDs in the install-test prove the object is WELL-FORMED; they accept both
# of these, because an empty list and a dangling name are valid YAML
# against that schema.
#
# What is NOT provable anywhere, and is written down rather than implied: no
# Gateway controller runs in CI, so no route is ever reconciled. That
# traffic flows is not part of this pipeline's claims.
#
# No cluster and no Trivy; ~3 seconds for the whole repository, depends on
# no other job. The YAML is parsed by Helm through ci/yaml2json, because the
# runners have neither pyyaml nor yq and a hand-written parser could read
# nothing and report nothing - see the script's header.
steps:
- name: "Checkout Repository"
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- name: "Install Helm"
uses: azure/setup-helm@9bc31f4ebc9c6b171d7bfbaa5d006ae7abdb4310 # v5
- name: "Check every rendered HTTPRoute against the rendered Services"
run: ci/run-httproute-match-test.sh
trivy-config:
name: "Trivy config ${{ matrix.chart }}"
needs: detect-changed-charts
+5 -46
View File
@@ -18,7 +18,7 @@ Every chart has a wiki page of its own — `https://git.jk-dev.de/jlab-cluster/h
| `bentopdf` | BentoPDF | Stateless Deployment + `config.json` ConfigMap, `subPath`-mounted into the web root and always rendered (empty renders as `{}`) |
| `bulwark` | Bulwark Webmail (JMAP client for Stalwart) | **The companion to `stalwart`, and a pure client**: no mail, no database, no cache, no broker — the messages belong to the Stalwart release named by `bulwark.jmapServerUrl`. StatefulSet with **one** PVC at `/app/data`, carrying all four of Bulwark's directories (admin config, admin state with the audit log, encrypted user settings, telemetry) as subdirectories — one volume because snapshots are atomic per volume and never across them (the `garage` lesson, #81). No ConfigMap, so no `checksum/config` (§9 has nothing to attach to); the `op://` paths are covered by `checksum/secret-ref`. **The defining property, and it governs every value: the VOLUME BEATS THE CHART.** `configManager.get` resolves `admin override > env var > default`, the admin dashboard may write *any* key, and nothing says it just disconnected the chart — so a `helm upgrade` with a changed value can succeed and change nothing. Upstream documents this only for the server URL; it is true of everything. Exactly three values escape it (`SESSION_SECRET(_FILE)`, `ADMIN_PASSWORD`, `BULWARK_TELEMETRY`), and `bulwark.adminConfigReadOnly` closes it at a price that is **threefold and measured**: it freezes the settings policy (and with it the only way to move Web Push off the vendor relay), it freezes an override that *already* exists rather than removing it, and on a **fresh** install it stops `admin.json` from being written at all — so the admin dashboard never comes up and answers HTTP 500 while the pod stays Ready. Hence the default `false`. Sign-in is **OIDC-only** (`oauth.only`), and the way back out of a lockout is **not** `helm upgrade` with it `false` — `oauthOnly` is read through the same precedence chain, so the repair is removing the override in the **admin dashboard**, which is why `ADMIN_PASSWORD` is set at all (it generates no random password and logs nothing — the `#65` analogy does not hold here). Its **redirect URI carries the UI language** (`/<locale>/auth/callback`, 27 locales, and the language is not in the visible URL), so a single registered URI is the likeliest cause of a failed first sign-in. Both `SESSION_SECRET_FILE` and `OAUTH_CLIENT_SECRET_FILE` exist and are used — the better path `garage` and `keycloak` did not have — mounted **without `subPath`** so that a rotation takes effect without a restart, since both are re-read on every use. Two backup statements, both sharp: without the session secret the synchronized settings are unreadable **out of an intact volume**, and if the dashboard's OAuth setup was ever used the client secret sits in `config.json` on that volume in **cleartext**. All three probes are a plain GET on `/api/health` — **not** HEAD, which a `HTTPGetAction` cannot send — and the limit is written at the value: it carries process, HTTP acceptance, a free event loop and Next.js routing, and **nothing** about Stalwart, so with the mail server gone the pod is green. Served only at a host **root** (`NEXT_PUBLIC_BASE_PATH` is compiled in), one replica (single RWO PVC), two emptyDirs for `readOnlyRootFilesystem` (`/tmp`, `/app/.next/cache` — image optimization is active because `images.unoptimized` is in the lite branch only) |
| `calibre-web-automated` | Calibre-Web Automated | StatefulSet, extra consume-PVC |
| `cyberchef` | CyberChef | Stateless Deployment. **The Gateway-API pilot (#114)**: the first chart offering `httpRoute.enabled` as the alternative to `ingress.enabled` — `HTTPRoute` with `parentRefs` only, no Gateway and no certificate, the two modes mutually exclusive. It is also the only chart whose install-test runs in Gateway mode, because that test installs one configuration per chart |
| `cyberchef` | CyberChef | Stateless Deployment |
| `freesailarr` | gluetun VPN media stack (qbittorrent, prowlarr, flaresolverr, radarr, recyclarr, sonarr, seerr, bazarr, and mediathekarr off by default) | Single-pod StatefulSet: all apps share gluetun's network namespace; config volumes via volumeClaimTemplates, media PVCs as existingClaims (optional per-category `subPath`, so one claim can serve all three categories). One ClusterIP Service per app (mediathekarr's carries both of its ports); no global `ingress.domain`. Ingress in two shapes that mix: one host per app (default, each app's host required), or one shared `ingress.host` with the apps as sub-paths (documented ingress exception below) |
| `garage` | Garage (S3-compatible object store) | **One** node, `replication_factor = 1` — a deliberate trade with its price named (Gitea #81): the volume layer underneath already replicates and encrypts, so Garage replication on top would be nine times the raw data across two layers that know nothing about each other. What that costs is that the factor applies to the **metadata** too, where a corrupted database is upstream-documented as "generally not recoverable" and a replicated volume replicates the corruption. Hence three settings that are mandatory rather than optional: `metadata_fsync = true` (**off** in Garage's defaults), the automatic metadata snapshots, and those snapshots on a **second PVC** — a file-system snapshot of a live metadata database is not a restore point, so the volume backup covers the blocks and not the index. The first PVC carries `metadata_dir` **and** `data_dir` as two subdirectories, because volume snapshots are atomic per volume: that is what keeps index and blocks consistent with each other. Two further properties shape the whole chart: the image is **`FROM scratch`** and contains exactly one file, `/garage` — no shell, so **exec probes are impossible here** and an init container would need a second image — and the RPC secret has to arrive as an **environment variable**, because `rpc_secret_file` rejects any file with a group bit set while the kubelet sets one (the reasoning sits at the value). Started with `garage server --single-node`, which creates the cluster layout in-process before any listener opens and is idempotent on the layout version, and with `--default-access-key`/`--default-bucket`, which **import** key and bucket from the 1Password item — so the vault is the source of truth instead of a copy of what the server generated. Readiness is the admin API's `/health`, liveness and startup deliberately are **not**: `/health` answers 503 until a layout exists, so a node waiting for one would be restarted forever |
| `gotenberg` | Gotenberg | Deployment + Service only (no ingress) |
@@ -68,8 +68,8 @@ Every chart has a wiki page of its own — `https://git.jk-dev.de/jlab-cluster/h
Note: the registry **accepts a re-upload of an already published version** (HTTP 201) and replaces the stored artifact, so a republish of an existing version is still silent — see #160. That is also what makes a manual `workflow_dispatch` of any `publish-<chart>.yml` a harmless way to prove the shared block works.
- `change-app-version.yml` — manually dispatched app-version bump; the chart must be listed in its `chart-name` choice list.
- `ci/helm-dependencies.sh <chart-dir>` — not a workflow but the step they all share: fetches a chart's declared dependencies into `charts/`, and returns silently for the charts that declare none (all but three: `ytdlp-web-player`, `stalwart`, `paperless-ngx`). It runs in **seven** places, because every helm command this repo builds on exits 1 while a declared dependency is missing: before lint and package in `validate-charts.yml`, before package in the shared publish block (which is why that block names this script as a prerequisite of its callers — see above), 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`. See "Chart dependencies" in the style guidelines for what that costs.
- `validate-charts.yml` — PR pipeline: detects which charts a PR touches and, for exactly those, runs `helm lint --strict`, `helm package`, and a chart-version bump check (`template-chart` is exempt from the version check). PRs without chart changes skip the build matrix. After the build stage, an **install-test** job (the same changed-charts matrix, minus the charts listed in `ci/excluded-charts.txt`) installs each changed chart into an ephemeral [k3d](https://k3d.io) cluster on the runner and requires every workload to become Ready — see below. Both matrices follow the changed chart directories, so a PR that only changes the harness under `ci/` 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 after the merge. The **resource-quantity render test**, the **selector-match render test** and the **HTTPRoute render test** are the three exceptions to that rule: all of them run over every chart on every PR — see below.
- `ci/helm-dependencies.sh <chart-dir>` — not a workflow but the step they all share: fetches a chart's declared dependencies into `charts/`, and returns silently for the charts that declare none (all but three: `ytdlp-web-player`, `stalwart`, `paperless-ngx`). It runs in **six** places, because every helm command this repo builds on exits 1 while a declared dependency is missing: before lint and package in `validate-charts.yml`, before package in the shared publish block (which is why that block names this script as a prerequisite of its callers — see above), 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`. See "Chart dependencies" in the style guidelines for what that costs.
- `validate-charts.yml` — PR pipeline: detects which charts a PR touches and, for exactly those, runs `helm lint --strict`, `helm package`, and a chart-version bump check (`template-chart` is exempt from the version check). PRs without chart changes skip the build matrix. After the build stage, an **install-test** job (the same changed-charts matrix, minus the charts listed in `ci/excluded-charts.txt`) installs each changed chart into an ephemeral [k3d](https://k3d.io) cluster on the runner and requires every workload to become Ready — see below. Both matrices follow the changed chart directories, so a PR that only changes the harness under `ci/` 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 after the merge. The **resource-quantity render test** and the **selector-match render test** are the two exceptions to that rule: both run over every chart on every PR — see below.
- `install-test.yml` — reusable workflow (`workflow_call`) holding the install-test **job** itself: the runner-label expression, the pinned `kubectl` with its skew check, `envsubst`, the containerised-job detection, the per-leg k3d cluster and the `freesailarr` VPN credentials. Its one input is a JSON array of chart names; its callers decide *which* charts are tested and whether the job runs at all, never *how*. Two callers: `validate-charts.yml` and `install-test-all-charts.yml`. **It is not copied for the second caller** — a second copy is a second chance to test differently, unnoticed, the same argument that gives `ci/excluded-charts.txt` exactly one parser.
**It stays in this repository, and that is a decision, not an oversight** (Gitea #66, decision 4). It is reusable in form but not in substance: it is welded to the `ci/` fixtures, to the `ci/excluded-charts.txt` parser and to this repository's chart matrix. A building block that only one repository can use is not shared, it is merely moved somewhere else — and moving it would buy nothing while putting a tag between us and a file we change often. `publish-helm-chart.yml` was the opposite case: it takes a chart name and a registry owner and knows nothing about this repository, which is exactly why it could go. This note exists so the question is not asked again in half a year.
@@ -161,25 +161,6 @@ 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, and a non-zero exit; the same was done for the spread constraint's selector.
### HTTPRoute render test (all charts, every PR)
`ci/run-httproute-match-test.sh [chart …]` (no argument = all charts) renders every chart with the same fixtures the install-test uses and checks every `HTTPRoute` in the result against the `Service` objects of that **same** render. Introduced with the Gateway-API pilot (Gitea #114); two failure modes, both silent:
| Finding | What it does | Who notices today |
|---|---|---|
| `parentRefs` empty | the route attaches to **no** Gateway: created, schema-valid, rejected by nobody, and it routes 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 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 happily 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 genuine new exposure rather than a hypothetical: an Ingress backend references its Service port **by name**, while `backendRefs` takes a `PortNumber` and cannot, so the number in the route and the port in the Service are kept in step by hand from now on.
Three things to know before touching it:
- **Helm is the YAML parser**, through `ci/yaml2json`, for the same reason as the selector-match test above — the runners have neither `pyyaml` nor `yq`, and a parser written in `awk` fails by reading nothing and reporting nothing.
- **What is deliberately not checked**, named so the gaps are not mistaken for coverage. *That traffic flows* — no Gateway controller runs in CI and none will, so `Accepted: True` is not obtainable; that claim is simply not made anywhere 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 with no `backendRefs` at all (legal for a filter-only rule) — 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 the run fails if **no assertion at all** held anywhere. Today `cyberchef` is exercised through its ordinary install-test fixture (which installs in Gateway mode, see below) and `template-chart` through its overlay.
Measured in CI (run 719): **30 charts, 4 assertions held**, whole job 13 s including checkout and the Helm setup, 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.
### Install-test (PR pipeline stage 2)
For every chart a PR changes, `ci/run-install-test.sh <chart>` runs against a throwaway k3d cluster: it applies the stub `OnePasswordItem` CRD plus the chart's fixtures, `helm install`s the chart with its CI values, and waits for all Deployments/StatefulSets to roll out (per-chart timeout). Since probes and security hardening are active on all workloads, "Ready" means the app actually answers. On failure the job dumps pod descriptions, events and container logs.
@@ -189,7 +170,6 @@ The job itself is the reusable workflow **`install-test.yml`**; `validate-charts
Fixtures live at the **repo root** under `ci/` (deliberately *not* inside the chart directories, so they are neither packaged into the chart tgz nor trigger the publish workflows on merge):
- `ci/common/` — fixtures applied for every chart (schema-less stub CRD for `OnePasswordItem`)
- `ci/gateway-api-crds.sh` — the **real** Gateway-API CRDs (standard channel, pinned, Renovate-tracked), applied only for a chart whose render actually contains an `HTTPRoute`. **Deliberately not a stub and deliberately not in `ci/common/`.** A schema-less stub would make the exercise worthless here: the API server would accept a malformed `parentRefs`, a port written as a string, a path type that does not exist, and the install-test would prove only that *something was created* — for `OnePasswordItem` that is the right trade (nobody is testing the operator's API), for an `HTTPRoute` the schema **is** the point. Applied with `kubectl apply --server-side`, and the reason is measured: client-side apply stores the whole object in the `last-applied-configuration` annotation, annotations total at most 262144 bytes, and the HTTPRoute CRD of v1.6.3 is 244792 bytes as JSON — 93 % of the limit, with every release adding fields. Client-side happens to fit today and is one upstream release away from failing with `metadata.annotations: Too long` on a Renovate PR that has nothing to do with it. Downloaded rather than vendored, like Trivy's tarball and `kubectl`: 1.1 MB of generated YAML in the repo would go stale invisibly, and the price of the fetch is named — if the asset disappears, an unrelated PR goes red, loudly
- `ci/<chart>/test-values.yaml` — values for the CI install (required for every installable chart)
- `ci/<chart>/fixtures.yaml` — dummy secrets with in-cluster connection data, throwaway infrastructure (postgres/redis/dex), static hostPath PVs/StorageClasses where the chart pins a storage class or needs RWX, and seed jobs (e.g. `synapse generate`)
- `ci/<chart>/fixtures.env.yaml` — fixtures that need a value which must not be committed. The file holds `${VARIABLE}` placeholders and nothing else; `ci/run-install-test.sh` renders it with `envsubst` and pipes the result straight into `kubectl` (never printed). Substitution is restricted to the names from `REQUIRED_ENV`, so an undeclared variable is never filled in and no unrelated `$…` in the manifest is touched
@@ -358,35 +338,14 @@ These guidelines are **binding for every current and future chart** in this repo
- **Documented exception — minecraft-server**: the only chart whose main image tag can be overridden, through `image.tagOverride` (#158, empty by default — the tag still comes from `appVersion`). `appVersion` is the tag and therefore fixes the JVM, and since chart 5.0.0 only the **newest** Java line is maintained out of this one directory; the older lines stay published as frozen chart versions (java8 and java17 at 4.0.0, java21 at 4.1.0) and never receive another change. 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. The override is named `tagOverride`, not `tag`, because it decouples the running image from the chart version, and it is deliberately outside Renovate's reach (`renovate.json5` tracks the `appVersion` in `Chart.yaml` and nothing else). The chart checks the pairing itself: the Java line of the **effective** tag against `minecraft.version`, failing the render with both values named — a mismatch used to install silently and only break the container afterwards.
- 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 and Gateway API
### 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. `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 precisely the defect guideline 12 exists to remove.
- `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-<release>-<chart>-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).
- **A shared domain with sub-paths is the second admissible ingress shape.** Where a chart exposes several apps, they may either each take a host of their own or share one host and be told apart by path (`domain.tld/sonarr`, `domain.tld/radarr`, …) — one DNS name and one certificate for the whole bundle. The rules for the shared shape: the domain is a single value (`ingress.host`), each app's path is its own value defaulting to `/<app>`, the paths render in one Ingress object `<release>-<chart>-ingress` with the standard TLS secret `tls-<release>-<chart>-ingress`, and an app that carries a host of its own always wins and keeps its own object — so both shapes mix without extra values. An app that needs a per-path proxy annotation (`rewrite-target` is per **object**, not per path) gets its own object on the same host, repeating the same TLS secret name and **omitting** the cert-manager annotation, so that exactly one certificate is requested. Apps that cannot serve a base path must be rendered at `/` or refused with a `fail` — never rendered somewhere they provably cannot work.
- **The Gateway API is the second routing mode, and it is implementation-neutral by construction (Gitea #114).** A chart offers it through one values block, off by default:
```yaml
httpRoute:
enabled: false # counterpart to ingress.enabled
parentRefs: [] # the Gateway (name, namespace, sectionName) — a platform object
hostnames: [] # empty = [ingress.domain]
```
The rules, each of which is load-bearing:
- **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, and keeping to it is the whole reason the cluster's implementation can be chosen — or replaced — without touching the charts again. A policy object binds 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`.
- **A value, not auto-detection.** `.Capabilities.APIVersions` is unreliable at template time, and an `HTTPRoute` without the Gateway-API CRDs fails at install time — so a chart that guessed would sometimes render an object the cluster rejects and sometimes silently render nothing.
- **Mutually exclusive with `ingress.enabled`**, enforced by a `fail` that names **both** value paths. Two paths onto the same 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 over parallel operation.
- **`parentRefs` is mandatory in Gateway mode**, enforced by a `fail` on the empty list. A route without a parent attaches to nothing and is simply inert — created, schema-valid, rejected by nobody, reported by nothing.
- **One source of truth for the hostname.** Empty `hostnames` means `[ingress.domain]`; two values that have to agree drift apart. `ingress.domain` is therefore read in Gateway mode too — it names the release's public hostname and is not an Ingress switch.
- **`NOTES.txt` says which mode is active**, as `keycloak` already does for its cases, and names what the mode costs.
- **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, so a chart that depends on one (`bulwark`, `calibre-web-automated`, `outline`, `paperless-ngx`, `zipline`) must print in its Gateway-mode `NOTES.txt` that the limit is **not enforced** and has to be set at the Gateway. The same applies to 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.
- **CI**: either the chart's install-test fixture installs in Gateway mode, or it gets a render-only overlay at `ci/httproute/<chart>.yaml` — otherwise `ci/run-httproute-match-test.sh` is green over nothing for it, which that test fails on.
Reference implementation: `template-chart/templates/xxx.httproute.yaml`. Pilot: `cyberchef`. 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.
- **Documented exception — freesailarr**: the chart bundles several independently exposed apps in one pod, so it has **no** `ingress.domain`. It offers both shapes. By default each app carries its own `<app>.ingress.enabled` plus a required `<app>.ingress.host` (required only while that app's ingress is enabled and it is not on the shared domain) and renders its own Ingress object `<release>-<chart>-<app>-ingress` with its own TLS secret `tls-<release>-<chart>-<app>-ingress`. Setting the global `ingress.host` switches every app without a host of its own to the shared-domain shape under `<app>.ingress.path`; qBittorrent (no base path, needs a prefix-stripping rewrite) gets the extra object described above, and seerr as well as mediathekarr are pinned to `/` — mediathekarr because its UI links assets with root-absolute paths, which a rewrite cannot fix; with both of them on one shared host the duplicate-path guard fires and one of them needs a host of its own (its ingress is therefore off by default). `clusterIssuer`, `className` and `annotations` stay global defaults under `ingress.` and are overridable per app.
### 5. Security hardening
+4 -38
View File
@@ -1,40 +1,6 @@
# cyberchef: stateless, no secrets.
#
# THIS CHART IS INSTALLED IN GATEWAY MODE, NOT IN INGRESS MODE (Gitea #114),
# and that is a deliberate trade, because the install-test runs exactly ONE
# configuration per chart and the two modes are mutually exclusive.
#
# What is given up: the Ingress of this one chart is no longer installed
# anywhere. That costs little. An Ingress is a built-in, GA type whose schema
# the API server has validated for a decade, k3d's own ingress controller is
# traefik while this chart asks for the class "nginx", so nothing ever routed
# through it here either - the install only ever proved "the API server
# accepted an Ingress". And it keeps being proved, twenty times over, by every
# other chart in the repo plus `helm lint --strict`, which renders this chart's
# DEFAULTS, i.e. Ingress mode, on every pull request.
#
# What is gained: a real HTTPRoute, applied against the real Gateway-API CRDs
# (see ci/gateway-api-crds.sh), which is a CRD whose schema nothing else in
# this repository has ever exercised. That is where a malformed parentRefs or
# backendRefs is actually caught.
#
# Still NOT proved by this, and it cannot be: that traffic flows. No Gateway
# controller runs in k3d, so nothing routes. The install-test proves the object
# is schema-valid and the pod becomes Ready; ci/run-httproute-match-test.sh
# proves the route points at a Service the same render creates.
# cyberchef: stateless, no secrets. Only the required ingress values are set.
# The Ingress just has to apply - there is no ingress-nginx/cert-manager in
# the CI cluster.
ingress:
# The hostname is read by the HTTPRoute too (httpRoute.hostnames is empty, so
# it falls back to ingress.domain) - it is the release's public name, not an
# Ingress switch.
domain: cyberchef.ci.local
enabled: false
httpRoute:
enabled: true
# The Gateway does not exist in the CI cluster, and it does not need to: a
# parentRef is resolved by the Gateway controller, of which there is none
# here, so the reference is stored as written. What the real CRDs DO validate
# is its shape.
parentRefs:
- name: ci-gateway
namespace: default
sectionName: https
clusterIssuer: ci
-99
View File
@@ -1,99 +0,0 @@
#!/usr/bin/env bash
# Install the REAL Gateway-API CRDs (standard channel) into the cluster of the
# current kubectl context, for the install-test of a chart that renders an
# HTTPRoute (Gitea #114).
#
# Usage: ci/gateway-api-crds.sh (called by ci/run-install-test.sh)
#
# WHY NOT A STUB CRD, WHICH IS WHAT ci/common/ DOES FOR OnePasswordItem
#
# Because a stub would make the install-test prove nothing. ci/common's
# OnePasswordItem CRD is schema-less on purpose (x-kubernetes-preserve-unknown-
# fields): nobody is testing the 1Password operator's API, only that the chart's
# CR applies. For an HTTPRoute the situation is the opposite - the SCHEMA is the
# entire value of the exercise. With a schema-less stub the API server accepts a
# malformed parentRefs, a backendRef with 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, and a malformed route fails the install-test loudly.
#
# This is the half that CANNOT be moved into a render test: a render cannot know
# the Gateway API's schema. And the half the CRDs cannot cover is held by
# ci/run-httproute-match-test.sh - schema validation accepts an empty
# parentRefs list and a backendRef naming a Service that does not exist.
#
# What neither proves, and nothing here can: that traffic flows. No Gateway
# controller runs in k3d, so no route is ever reconciled and no status is ever
# written. Provable: the object is schema-valid and the pod becomes Ready.
#
# WHY --server-side, AND THE MEASUREMENT BEHIND IT
#
# Client-side `kubectl 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) - i.e. 93% of the limit,
# with every release adding fields. So client-side apply happens to fit today
# and is one upstream 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.
#
# This is also why the CRDs are NOT in ci/common/, which ci/run-install-test.sh
# applies client-side for EVERY chart: putting them there would hand that
# landmine to all 20-odd charts and spend the download on charts that render no
# route at all.
#
# WHY DOWNLOADED AND NOT VENDORED
#
# 1.1 MB of generated YAML in the repository would go stale invisibly and would
# bury every diff that touches ci/. Downloading a PINNED artifact is what this
# repo already does for every other piece of CI tooling - Trivy's tarball, k3d's
# install script, kubectl - each with a `# renovate:` comment keeping the pin
# current. The cost is named rather than hidden: one more network fetch, and if
# the release asset ever disappears this goes red on a pull request that changed
# nothing related. It fails loudly, which is the acceptable half.
set -euo pipefail
# The pinned standard channel. Renovate keeps this current through the
# customManager for ci/*.sh in renovate.json5; prereleases are excluded there,
# so the -rc.N tags upstream publishes are never offered.
# renovate: datasource=github-releases depName=kubernetes-sigs/gateway-api
GATEWAY_API_VERSION=v1.6.3
# The standard channel, as published - the whole set and not a hand-picked
# subset. The charts render only HTTPRoute today, so HTTPRoute's CRD is the only
# one strictly needed; taking the published set instead means the sweep of #114
# needs no change here, and a fixture that wants a real Gateway object later
# finds its CRD already installed.
URL="https://github.com/kubernetes-sigs/gateway-api/releases/download/${GATEWAY_API_VERSION}/standard-install.yaml"
MANIFEST="$(mktemp)"
trap 'rm -f "$MANIFEST"' EXIT
echo "==> Gateway API ${GATEWAY_API_VERSION} (standard channel): $URL"
curl -sSfL -o "$MANIFEST" "$URL"
# A truncated or redirected download would otherwise be applied as whatever it
# happens to contain. Asserting the one object the charts actually need is
# cheaper than a checksum nobody can regenerate, and it names what is wrong.
if ! grep -q '^ name: httproutes.gateway.networking.k8s.io$' "$MANIFEST"; then
echo "ERROR: the downloaded manifest does not contain the httproutes CRD." >&2
echo "That is not a chart failure: either the release asset changed shape or the" >&2
echo "download was truncated. Got $(wc -c <"$MANIFEST") bytes from $URL" >&2
exit 1
fi
kubectl apply --server-side -f "$MANIFEST"
# Established, not just created. `kubectl apply` returns as soon as the CRD
# object exists, 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 exactly 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.
for CRD in $(kubectl get crd -o name | grep '\.gateway\.networking\.k8s\.io$'); do
kubectl wait --for=condition=Established "$CRD" --timeout=60s
done
kubectl get crd -o name | grep '\.gateway\.networking\.k8s\.io$' | sed 's/^/ /'
echo "==> Gateway API CRDs established."
-23
View File
@@ -1,23 +0,0 @@
# Render-only overlay for ci/run-httproute-match-test.sh, applied LAST.
#
# Its one job is to put this chart into Gateway mode, because the convention
# ships with httpRoute.enabled: false and the test would otherwise be green
# over nothing. A chart that has an overlay here and still renders no
# HTTPRoute FAILS the test - that is the whole point of the file existing.
#
# template-chart has no ci/<chart>/test-values.yaml (it is excluded from the
# install-test for its placeholder image), so the test reaches it through
# ci/trivy/values/template-chart.yaml, which this overlay sits on top of.
#
# ingress.enabled: false is not optional here. The two modes are mutually
# exclusive and the chart aborts its own render when both are true, so an
# overlay that only switched httpRoute on would turn this test red on the
# charts that implement the convention correctly.
ingress:
enabled: false
httpRoute:
enabled: true
parentRefs:
- name: public
namespace: gateway
sectionName: https
-467
View File
@@ -1,467 +0,0 @@
#!/usr/bin/env bash
# HTTPRoute render test: every HTTPRoute a chart renders is held against the
# Services of the SAME render, and against the one condition without which the
# route attaches to nothing.
#
# Usage: ci/run-httproute-match-test.sh [chart ...] (no argument = all charts)
#
# WHAT IT GUARDS AGAINST, AND WHY NEITHER A RENDER NOR AN INSTALL DOES (#114)
#
# An HTTPRoute is the Gateway-API alternative to the Ingress (README, "Ingress
# and Gateway API"), and it has two silent failure modes that look exactly like
# a working route from the outside:
#
# parentRefs EMPTY - 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. There is no event and no condition; the only symptom is a
# hostname that does not answer. The charts abort the render over this, and
# this test is the half that catches a chart which forgot to.
# backendRefs pointing at NOTHING - the reference is by name, in the release
# namespace, and nothing validates that the name exists. A typo, a renamed
# Service, or a port number that no longer matches the Service's port
# yields a route that resolves to no endpoint. ResolvedRefs goes False in
# the route's STATUS, which only exists once a controller has looked at it
# - so in CI, where no Gateway controller runs, nothing reports it at all.
#
# And the install-test cannot close either gap, even with the real CRDs in the
# cluster: schema validation accepts an empty parentRefs list and accepts a
# backendRef to a Service that does not exist. The CRDs prove the object is
# WELL-FORMED; this test is what proves it POINTS SOMEWHERE.
#
# Over ALL charts and not the changed ones, for the same reason as
# ci/run-quantity-render-test.sh and ci/run-selector-match-test.sh: the mistake
# arrives by copy into a chart whose own diff nobody reads for it - and this
# convention is going to be copied 20 more times when the sweep of #114 runs.
# Running over everything also means there is no list to maintain: adding a
# chart enrols it.
#
# HOW THE YAML IS PARSED
#
# By Helm, through ci/yaml2json, exactly as ci/run-selector-match-test.sh does
# it - the runners have neither pyyaml nor yq, and a parser written in awk
# would be able to read nothing, find no route and report no finding, leaving
# this test green for a chart it never looked at. The only textual step is the
# document split on a line that is exactly "---" in column one, and getting
# that wrong is not a false pass: the fragment stops being valid YAML and helm
# exits 1. See that script's header for the long form.
#
# WHAT IS DELIBERATELY NOT CHECKED, so the gaps are not mistaken for coverage
#
# - THAT TRAFFIC FLOWS. No Gateway controller runs in CI and none will; a
# route's status is written by the implementation, so "Accepted: True" is
# not obtainable here. What is obtainable: the object is schema-valid
# (install-test, against the real CRDs) and it names a Service and a port
# that exist (this test). Routing itself is unproven, by construction.
# - MUTUAL EXCLUSION with the Ingress. Every chart aborts its own render when
# ingress.enabled and httpRoute.enabled are both true, which is where that
# check belongs: this test renders ONE configuration per chart, and a
# render-level rule here would have to assume the switch is chart-global.
# That holds for the pilot and may not hold for the multi-app charts of the
# sweep (matrix-synapse renders in a loop, keycloak and bulwark have two
# Ingresses each), so the rule is not written down here where it would have
# to be undone.
# - CROSS-NAMESPACE backendRefs and non-Service backends. A backendRef into
# another namespace needs a ReferenceGrant on the other side and its target
# is by definition not in this render; a backend that is not a Service is
# some implementation's own kind. Both are REPORTED as "not checked" and
# are never counted as passing, because a green tick for a code path nobody
# entered is worse than an honest gap (README, "Two rules for every check
# added here").
# - A RULE WITHOUT backendRefs. Legal - a rule carrying only a
# RequestRedirect filter needs no backend - so it is reported, not failed.
#
# AND THE ANTI-VACUUM GUARDS, because this test can be green over nothing. The
# convention ships with httpRoute.enabled: false, so a chart renders no route
# at all unless its CI values or an overlay switch the mode on. Two rules:
#
# 1. ci/httproute/<chart>.yaml is an overlay applied last, whose only job is
# to put a chart into Gateway mode for this test. A chart that HAS one and
# still renders no HTTPRoute FAILS - the overlay exists for exactly that.
# 2. If no HTTPRoute at all was checked across the whole run, the run fails.
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
CI_DIR="$ROOT/ci"
YAML2JSON="$CI_DIR/yaml2json"
OVERLAY_DIR="$CI_DIR/httproute"
RELEASE="httproute"
WORK="$(mktemp -d)"
trap 'rm -rf "$WORK"' EXIT
# --- Which charts -----------------------------------------------------------
CHARTS=()
if [ "$#" -gt 0 ]; then
CHARTS=("$@")
else
for DIR in "$ROOT"/*/; do
[ -f "$DIR/Chart.yaml" ] || continue
CHARTS+=("$(basename "$DIR")")
done
fi
if [ "${#CHARTS[@]}" -eq 0 ]; then
echo "ERROR: no chart directories found under $ROOT" >&2
exit 1
fi
FAILED=""
CHECKED=""
NOT_EXERCISED=""
WITH_GAPS=""
TOTAL_ASSERTIONS=0
SUMMARY_ROWS=""
for CHART in "${CHARTS[@]}"; do
if [ ! -f "$ROOT/$CHART/Chart.yaml" ]; then
echo "ERROR: '$CHART' is not a chart directory" >&2
exit 1
fi
# Same reason as in the other two render tests: a chart with a declared
# dependency cannot be rendered at all while charts/ is empty, and this job
# runs over EVERY chart on EVERY pull request - so without this line a PR
# touching nothing but a README would go red on the charts that have one.
"$ROOT/ci/helm-dependencies.sh" "$ROOT/$CHART"
# Values: the same precedence the other render tests use, so a chart's CI
# values are maintained in one place and satisfy its required() values.
VALUES_ARGS=()
VALUES_LABEL="chart defaults"
if [ -f "$CI_DIR/$CHART/test-values.yaml" ]; then
VALUES_ARGS=(-f "$CI_DIR/$CHART/test-values.yaml")
VALUES_LABEL="ci/$CHART/test-values.yaml"
elif [ -f "$CI_DIR/trivy/values/$CHART.yaml" ]; then
VALUES_ARGS=(-f "$CI_DIR/trivy/values/$CHART.yaml")
VALUES_LABEL="ci/trivy/values/$CHART.yaml"
fi
HAS_OVERLAY=""
if [ -f "$OVERLAY_DIR/$CHART.yaml" ]; then
# Last, so it wins over the install-test values.
VALUES_ARGS+=(-f "$OVERLAY_DIR/$CHART.yaml")
VALUES_LABEL="$VALUES_LABEL + ci/httproute/$CHART.yaml"
HAS_OVERLAY=1
fi
echo "::group::$CHART ($VALUES_LABEL)"
RENDER="$WORK/$CHART.manifest.yaml"
if ! helm template "$RELEASE" "$ROOT/$CHART" \
${VALUES_ARGS[@]+"${VALUES_ARGS[@]}"} >"$RENDER" 2>"$WORK/err"; then
sed 's/^/ /' "$WORK/err"
echo "::endgroup::"
echo "::error title=httproute $CHART::$CHART does not render with its CI values, so no HTTPRoute could be checked. Reproduce: helm template $RELEASE $CHART $(printf '%s ' "${VALUES_ARGS[@]+${VALUES_ARGS[@]}}")"
FAILED="$FAILED $CHART"
continue
fi
# --- The document split, and the only textual step in this script ----------
# One YAML sequence item per rendered document, content indented by four and
# the item marker on a line of its own - so a document whose first line is a
# comment ("# Source: ...") needs no special case. A wrong split does not
# produce a false pass: the result stops being parsable and helm exits 1.
DOCS="$WORK/$CHART.docs.yaml"
awk '
BEGIN { print "docs:"; started = 0 }
/^---[ \t]*$/ { print " -"; started = 1; next }
{
if (!started) { print " -"; started = 1 }
if (length($0) == 0) print ""; else print " " $0
}
' "$RENDER" >"$DOCS"
JSON="$WORK/$CHART.json"
if ! helm template y2j "$YAML2JSON" -f "$DOCS" 2>"$WORK/err" \
| awk '/^\{/ { print; exit }' >"$JSON"; then
sed 's/^/ /' "$WORK/err"
echo "::endgroup::"
echo "::error title=httproute $CHART::the rendered manifests of $CHART could not be re-read as YAML by ci/yaml2json. That is a parse failure, not an HTTPRoute finding: either the render contains a document this script's '---' split cut in the wrong place, or the manifest is not valid YAML. Helm's own message is in the job log."
FAILED="$FAILED $CHART"
continue
fi
if [ ! -s "$JSON" ]; then
echo "::endgroup::"
echo "::error title=httproute $CHART::ci/yaml2json produced no JSON for $CHART - the manifests were not re-read, so nothing was checked."
FAILED="$FAILED $CHART"
continue
fi
# --- The comparison --------------------------------------------------------
# Inline and on the standard library only, as in the two sibling tests. It
# writes one "VERDICT|detail" line per assertion and never decides the exit
# code itself: the loop below does, so a verdict can never be lost to a
# swallowed status.
set +e
python3 - "$CHART" "$JSON" >"$WORK/$CHART.out" 2>"$WORK/$CHART.pyerr" <<'PY'
import json, sys
chart, path = sys.argv[1], sys.argv[2]
docs = json.load(open(path)).get("docs") or []
docs = [d for d in docs if isinstance(d, dict)]
def name_of(doc):
meta = doc.get("metadata")
if isinstance(meta, dict) and meta.get("name"):
return str(meta["name"])
return "<unnamed %s>" % doc.get("kind", "object")
# Every Service of the render, as name -> the set of port NUMBERS it exposes.
# Port numbers and not names on purpose: an HTTPRoute backendRef carries a
# PortNumber and cannot reference a port by name, which is exactly where it
# differs from the Ingress backend this convention replaces - and exactly why
# a chart has to keep the number in the route in step with its Service by
# hand.
services = {}
for doc in docs:
if doc.get("kind") != "Service":
continue
ports = set()
spec = doc.get("spec") if isinstance(doc.get("spec"), dict) else {}
for port in (spec.get("ports") or []):
if isinstance(port, dict) and port.get("port") is not None:
ports.add(str(port["port"]))
services[name_of(doc)] = ports
routes = [d for d in docs if d.get("kind") == "HTTPRoute"]
lines = []
for doc in routes:
route = "HTTPRoute %s" % name_of(doc)
spec = doc.get("spec") if isinstance(doc.get("spec"), dict) else {}
# 1. parentRefs must be a non-empty list. The whole point: a route without
# a parent is valid, created, and inert.
parents = spec.get("parentRefs")
if not isinstance(parents, list) or not parents:
lines.append("FAIL|%s: %s has %s parentRefs. 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, the only symptom is a "
"hostname that does not answer. The chart must abort its "
"own render on an empty httpRoute.parentRefs"
% (chart, route,
"no" if parents is None else "an empty list of"))
else:
bad = [p for p in parents
if not isinstance(p, dict) or not str(p.get("name") or "").strip()]
if bad:
lines.append("FAIL|%s: %s has %d parentRef(s) without a usable "
"'name' (%r). ParentReference.name is required by the "
"API; a reference without it attaches to nothing"
% (chart, route, len(bad), bad))
else:
lines.append("OK|%s: %s has %d parentRef(s): %s"
% (chart, route, len(parents),
", ".join("%s%s" % (p["name"],
"/%s" % p["namespace"] if p.get("namespace") else "")
for p in parents)))
# 2. Every backendRefs entry, against the Services of the SAME render.
rules = spec.get("rules")
if not isinstance(rules, list) or not rules:
lines.append("FAIL|%s: %s renders no rules at all, so it matches no "
"request and forwards nothing" % (chart, route))
continue
for i, rule in enumerate(rules):
tag = "%s rules[%d]" % (route, i)
if not isinstance(rule, dict):
lines.append("FAIL|%s: %s is not a mapping (%r)" % (chart, tag, rule))
continue
backends = rule.get("backendRefs")
if not backends:
# Legal: a rule carrying only a RequestRedirect or a
# RequestMirror filter needs no backend. Reported, never counted.
lines.append("SKIP|%s: %s has no backendRefs. That is legal for a "
"filter-only rule (a redirect, for instance) and is "
"not evaluated here - but a rule meant to forward "
"traffic and carrying no backend answers 500 by "
"specification" % (chart, tag))
continue
if not isinstance(backends, list):
lines.append("FAIL|%s: %s backendRefs is not a list (%r)"
% (chart, tag, backends))
continue
for j, backend in enumerate(backends):
where = "%s.backendRefs[%d]" % (tag, j)
if not isinstance(backend, dict):
lines.append("FAIL|%s: %s is not a mapping (%r)"
% (chart, where, backend))
continue
# kind and group default to Service and the core group.
kind = str(backend.get("kind") or "Service")
group = str(backend.get("group") or "")
if kind != "Service" or group not in ("", "core"):
lines.append("SKIP|%s: %s references %s.%s, which is not a core "
"Service - some implementation's own backend kind. "
"This test cannot resolve it and does not count it "
"as passing" % (chart, where, group or "core", kind))
continue
namespace = backend.get("namespace")
if namespace:
lines.append("SKIP|%s: %s references Service %s in namespace %s. A "
"cross-namespace backend needs a ReferenceGrant on "
"the other side and its target is by definition not "
"in this render, so it is reported rather than "
"checked" % (chart, where, backend.get("name"), namespace))
continue
target = str(backend.get("name") or "")
if not target:
lines.append("FAIL|%s: %s has no 'name' - it references no "
"backend at all" % (chart, where))
continue
if target not in services:
lines.append("FAIL|%s: %s references Service %r, which the SAME "
"render does not create. A backendRef is resolved "
"by name at runtime and validated by nothing: the "
"route is accepted, ResolvedRefs goes False in a "
"status no controller writes in CI, and the "
"hostname answers with the Gateway's error instead "
"of the app. Services this render does create: %s"
% (chart, where, target,
", ".join(sorted(services)) or "none at all"))
continue
port = backend.get("port")
if port is None:
lines.append("FAIL|%s: %s references Service %r without a port. "
"A backendRef to a Service must carry one - the "
"CRD schema cannot require it conditionally, so an "
"omitted port is accepted at apply time and "
"resolves to nothing. The Service exposes: %s"
% (chart, where, target,
", ".join(sorted(services[target])) or "no ports"))
continue
if str(port) not in services[target]:
lines.append("FAIL|%s: %s references Service %r on port %s, which "
"that Service does not expose (it serves %s). "
"backendRefs takes a port NUMBER and cannot name a "
"port, so this pair is kept in step by hand and "
"drifts the moment one side is edited alone"
% (chart, where, target, port,
", ".join(sorted(services[target])) or "no ports"))
continue
lines.append("OK|%s: %s resolves to Service %s:%s, created by the same "
"render" % (chart, where, target, port))
for line in lines:
print(line)
PY
RC=$?
set -e
if [ "$RC" -gt 1 ]; then
sed 's/^/ /' "$WORK/$CHART.pyerr"
echo "::endgroup::"
echo "::error title=httproute $CHART::the HTTPRoute comparison itself failed for $CHART (python exit $RC). Treated as a failure, never as a pass."
FAILED="$FAILED $CHART"
continue
fi
CHART_FAIL=""
CHART_OK=0
CHART_SKIP=0
while IFS='|' read -r VERDICT DETAIL; do
case "$VERDICT" in
OK)
CHART_OK=$((CHART_OK + 1))
echo " ok $DETAIL"
;;
SKIP)
CHART_SKIP=$((CHART_SKIP + 1))
echo " gap $DETAIL"
;;
FAIL)
CHART_FAIL=1
echo " FAIL $DETAIL"
echo "::error title=httproute $CHART::$DETAIL"
;;
*)
[ -n "$VERDICT" ] && echo " $VERDICT $DETAIL"
;;
esac
done <"$WORK/$CHART.out"
echo "::endgroup::"
TOTAL_ASSERTIONS=$((TOTAL_ASSERTIONS + CHART_OK))
if [ -n "$CHART_FAIL" ]; then
FAILED="$FAILED $CHART"
SUMMARY_ROWS="${SUMMARY_ROWS}| \`$CHART\` | **failed** | $CHART_OK | $CHART_SKIP |"$'\n'
echo "$CHART: FAILED"
elif [ "$CHART_OK" -gt 0 ]; then
CHECKED="$CHECKED $CHART"
[ "$CHART_SKIP" -gt 0 ] && WITH_GAPS="$WITH_GAPS $CHART"
SUMMARY_ROWS="${SUMMARY_ROWS}| \`$CHART\` | ok | $CHART_OK | $CHART_SKIP |"$'\n'
echo "$CHART: ok ($CHART_OK assertion(s) held, $CHART_SKIP not checked)"
elif [ -n "$HAS_OVERLAY" ]; then
# The overlay's ONE job is to put this chart into Gateway mode. If no
# HTTPRoute came out, this test is vacuum-green for the chart the overlay
# was written for, which is the failure the overlay prevents.
FAILED="$FAILED $CHART"
SUMMARY_ROWS="${SUMMARY_ROWS}| \`$CHART\` | **failed** | 0 | $CHART_SKIP |"$'\n'
echo "::error title=httproute $CHART::$CHART has an overlay at ci/httproute/$CHART.yaml but rendered no HTTPRoute, so nothing was checked. That overlay exists only to switch the chart into Gateway mode - either it no longer does (a renamed value, a changed gate) or the chart stopped rendering a route. A green tick here would be a tick for a check that ran over nothing."
echo "$CHART: FAILED (overlay present, no HTTPRoute rendered)"
else
NOT_EXERCISED="$NOT_EXERCISED $CHART"
[ "$CHART_SKIP" -gt 0 ] && WITH_GAPS="$WITH_GAPS $CHART"
echo "$CHART: no HTTPRoute rendered - nothing to check."
fi
done
# --- Report ------------------------------------------------------------------
count() { set -- $1; echo "$#"; }
listing() { set -- $1; [ "$#" -eq 0 ] && { echo "–"; return; }; printf '`%s`' "$1"; shift; for C in "$@"; do printf ', `%s`' "$C"; done; echo; }
SUMMARY="${GITHUB_STEP_SUMMARY:-/dev/null}"
{
echo "### HTTPRoute render test (Gitea #114)"
echo
echo "Every \`HTTPRoute\` a chart renders, held against the \`Service\` objects of the"
echo "**same** render. An empty \`parentRefs\` attaches the route to no Gateway, and a"
echo "\`backendRefs\` entry naming a Service or a port that does not exist resolves to"
echo "nothing — both are schema-valid, so neither the real CRDs in the install-test nor"
echo "any render error catches them, and the only report would be a route status that no"
echo "controller writes in CI."
echo
echo "| Result | Count | Charts |"
echo "|---|---|---|"
echo "| HTTPRoutes checked | $(count "$CHECKED") | $(listing "$CHECKED") |"
echo "| No HTTPRoute rendered | $(count "$NOT_EXERCISED") | $(listing "$NOT_EXERCISED") |"
echo "| Failed | $(count "$FAILED") | $(listing "$FAILED") |"
echo
echo "$TOTAL_ASSERTIONS assertion(s) held in total."
echo
echo "**Not provable here, by construction:** that traffic flows. No Gateway controller"
echo "runs in CI, so no route ever reaches \`Accepted: True\`."
if [ -n "$WITH_GAPS" ]; then
echo
echo "Charts with references this test does **not** resolve (cross-namespace or non-Service backends, filter-only rules — reported, never counted as passing): $(listing "$WITH_GAPS")."
fi
if [ -n "$SUMMARY_ROWS" ]; then
echo
echo "| Chart | Result | Checked | Not checked |"
echo "| --- | --- | --- | --- |"
printf '%s' "$SUMMARY_ROWS"
fi
} >>"$SUMMARY"
echo "====================== httproute render test ======================"
echo "checked: $(count "$CHECKED")${CHECKED:+ -$CHECKED}"
echo "not exercised: $(count "$NOT_EXERCISED")${NOT_EXERCISED:+ -$NOT_EXERCISED}"
echo "failed: $(count "$FAILED")${FAILED:+ -$FAILED}"
echo "assertions held: $TOTAL_ASSERTIONS"
echo "=================================================================="
if [ -n "$FAILED" ]; then
echo "ERROR: HTTPRoute findings in:$FAILED" >&2
exit 1
fi
# The second anti-vacuum guard. Reaching zero means every chart fell into "no
# HTTPRoute rendered", which is indistinguishable from a broken harness - and
# it is the state this test would silently decay into if the overlays were ever
# dropped, or if the convention were reverted in the charts without anyone
# removing this job.
if [ "$TOTAL_ASSERTIONS" -eq 0 ]; then
echo "ERROR: not a single HTTPRoute assertion was checked in this run. Either no chart renders an HTTPRoute any more, or the overlays under ci/httproute/ no longer switch one into Gateway mode. A pass over zero assertions is not a pass." >&2
exit 1
fi
-24
View File
@@ -8,8 +8,6 @@
#
# Layout (repo root):
# ci/common/ fixtures applied for every chart (OnePasswordItem stub CRD)
# ci/gateway-api-crds.sh the REAL Gateway-API CRDs, applied only for a
# chart whose render contains an HTTPRoute
# ci/<chart>/test-values.yaml values for the CI install (required)
# ci/<chart>/fixtures.yaml dummy secrets / throwaway infra / static PVs (optional)
# ci/<chart>/fixtures.env.yaml fixtures with ${VARS} filled in from the environment (optional)
@@ -201,28 +199,6 @@ echo "::endgroup::"
# chart whose declared dependencies are not in charts/. No-op for a chart
# without dependencies; see ci/helm-dependencies.sh.
"$ROOT/ci/helm-dependencies.sh" "$ROOT/$CHART"
# --- 3a. Gateway-API CRDs, for a chart whose CI values render a route --------
# Detected from the render rather than declared per chart in settings.env, for
# the same reason the render tests run over every chart: there is then no list
# to maintain, and the sweep of Gitea #114 enrols a chart by switching its
# fixture into Gateway mode. The CRDs are REAL and not a stub - with a
# schema-less stub the API server would accept a malformed HTTPRoute and the
# install-test would prove only that something was created; see
# ci/gateway-api-crds.sh for the whole argument, and for why they are not in
# ci/common/ (which is applied client-side for every chart).
#
# A grep on the rendered kind, and a false negative here is loud rather than
# silent: without the CRDs the helm install below fails with "no matches for
# kind HTTPRoute in version gateway.networking.k8s.io/v1", which names exactly
# what is missing.
if helm template "$RELEASE" "$ROOT/$CHART" -f "$CI_DIR/$CHART/test-values.yaml" \
| grep -q '^kind: HTTPRoute$'; then
echo "::group::Gateway API CRDs"
"$ROOT/ci/gateway-api-crds.sh"
echo "::endgroup::"
fi
echo "::group::helm install"
helm install "$RELEASE" "$ROOT/$CHART" \
--namespace "$NAMESPACE" \
+1 -1
View File
@@ -5,7 +5,7 @@ icon: https://raw.githubusercontent.com/gchq/CyberChef/master/src/web/static/ima
type: application
version: 9.1.0+up11.4.0
version: 9.0.2+up11.4.0
appVersion: "11.4.0"
maintainers:
+10 -67
View File
@@ -1,14 +1,9 @@
{{- $replicas := include "cyberchef.replicas" (dict "scaleDown" .Values.scaleDown "replicas" .Values.cyberchef.replicas) -}}
{{- $httpRoute := .Values.httpRoute | default dict -}}
{{- $hostnames := $httpRoute.hostnames | default list -}}
{{- if not $hostnames -}}
{{- $hostnames = list .Values.ingress.domain -}}
{{- end -}}
CyberChef {{ .Chart.AppVersion }} — release {{ .Release.Name }} in namespace {{ .Release.Namespace }}.
{{- if .Values.ingress.enabled }}
ACCESS — INGRESS MODE (ingress.enabled: true, httpRoute.enabled: false)
ACCESS
https://{{ .Values.ingress.domain }}
The certificate is requested by cert-manager through the "{{ .Values.ingress.clusterIssuer }}"
@@ -18,83 +13,31 @@ ACCESS — INGRESS MODE (ingress.enabled: true, httpRoute.enabled: false)
This chart has no secrets and no volumes — the two ingress values above are
everything an installation needs.
{{- else if $httpRoute.enabled }}
ACCESS — GATEWAY MODE (httpRoute.enabled: true, ingress.enabled: false)
{{- range $hostnames }}
https://{{ . }}
{{- end }}
This release renders an HTTPRoute ({{ .Release.Name }}-{{ .Chart.Name }}-httproute) and NOTHING else
of the routing path — no Gateway, no class, no Certificate. Three things
therefore belong to the cluster and not to this chart, and if any of them is
missing the hostname above simply does not answer:
1. The Gateway-API CRDs. Without them this install would have failed, so
they are present — nothing to check.
2. The Gateway named in httpRoute.parentRefs
{{- range $httpRoute.parentRefs }}
{{ .name }}{{ if .namespace }} in namespace {{ .namespace }}{{ end }}{{ if .sectionName }}, listener {{ .sectionName }}{{ end }}
{{- end }}
must exist AND its listener must allow routes from this namespace
({{ .Release.Namespace }}) in allowedRoutes. A rejected attachment is
visible only in the route's status:
kubectl -n {{ .Release.Namespace }} get httproute {{ .Release.Name }}-{{ .Chart.Name }}-httproute -o yaml
Read status.parents[].conditions — "Accepted: True" and
"ResolvedRefs: True" is what a working attachment looks like. An empty
status means no controller has looked at the route at all.
3. The TLS certificate for the hostname, on the Gateway's listener.
ingress.clusterIssuer is NOT used in this mode and is not required —
the chart renders no Certificate here, so there is no
tls-{{ .Release.Name }}-{{ .Chart.Name }}-ingress secret and nothing for cert-manager to do
on behalf of this release.
ingress.annotations is also inert in this mode: the Gateway API has no
annotation channel for controller behaviour. See the note below on who can
use this instance.
{{- else }}
ACCESS — NEITHER INGRESS NOR HTTPROUTE
ingress.enabled and httpRoute.enabled are both false, so this release has no
Ingress, no HTTPRoute and no certificate; ingress.domain and
ingress.clusterIssuer are not needed. The app is only reachable inside the
cluster, as {{ .Release.Name }}-{{ .Chart.Name }}-service:80. To open it locally:
ACCESS — NO INGRESS
ingress.enabled is false, so this release has no Ingress and no certificate;
ingress.domain and ingress.clusterIssuer are not needed. The app is only
reachable inside the cluster, as
{{ .Release.Name }}-{{ .Chart.Name }}-service:80. To open it locally:
kubectl -n {{ .Release.Namespace }} port-forward svc/{{ .Release.Name }}-{{ .Chart.Name }}-service 8080:80
Then browse to http://localhost:8080. To publish it instead, set either
ingress.enabled=true with ingress.domain and ingress.clusterIssuer, or
httpRoute.enabled=true with ingress.domain and httpRoute.parentRefs.
Then browse to http://localhost:8080. Set ingress.enabled=true together with
ingress.domain and ingress.clusterIssuer to publish it instead.
{{- end }}
{{ if eq $replicas "0" }}
SCALED TO ZERO
{{ if .Values.scaleDown }}scaleDown: true{{ else }}cyberchef.replicas: 0{{ end }} — the deployment runs with replicas: 0, so the endpoint
above does not answer until you set it back and upgrade again. Service{{ if .Values.ingress.enabled }} and
Ingress stay in place, so the certificate is not re-issued.{{ else if $httpRoute.enabled }} and
HTTPRoute stay in place, so the route keeps pointing at a Service with no
endpoints — the Gateway answers with an error of its own rather than with
nothing.{{ else }} stays in
Ingress stay in place, so the certificate is not re-issued.{{ else }} stays in
place.{{ end }}
{{ end }}
ANYONE WHO REACHES THE URL CAN USE IT
{{- if $httpRoute.enabled }}
CyberChef has no login, and this chart adds none. In GATEWAY MODE there is no
place in this chart to put one either: the Gateway API has no annotation
channel, so the ingress-nginx external-auth or basic-auth annotations that
cover this in Ingress mode have NO counterpart here, and the chart renders no
implementation-specific policy object on purpose (it would bind the chart to
one Gateway implementation). Protection 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 and not here.
{{- else }}
CyberChef has no login, and this chart adds none. If it must not be open to
everyone, put authentication in front of it via ingress.annotations — the
chart merges them into the Ingress, e.g. ingress-nginx external auth or
basic auth annotations. Note that this escape hatch exists in Ingress mode
only: httpRoute.enabled has no annotation channel at all.
{{- end }}
basic auth annotations.
NOTHING TO BACK UP, NOTHING TO MIGRATE
CyberChef is a static web app: recipes and inputs are processed in the
@@ -1,82 +0,0 @@
{{/*
The Gateway-API alternative to cyberchef.ingress.yaml (Gitea #114).
WHAT THIS RENDERS, AND WHAT IT DELIBERATELY DOES NOT
Only an HTTPRoute with parentRefs. No Gateway, no gatewayClassName, no
Certificate, and no implementation-specific policy object (nginx-gateway-
fabric's ClientSettingsPolicy, Envoy Gateway's ClientTrafficPolicy or
whatever the next one calls it). That subset is the part which looks IDENTICAL
on nginx-gateway-fabric, Cilium, Envoy Gateway and Istio - and keeping to it is
what lets the cluster's implementation be chosen, or changed, without touching
this chart again. A policy object here would bind the chart to one
implementation and to a CRD whose presence it cannot check.
So the class and the certificate move from this chart to the PLATFORM: they sit
on the Gateway and its listeners, which belong in cluster-config, not here.
ingress.clusterIssuer is therefore meaningless in this mode - the chart renders
no Certificate at all - and it is not required either, because both reads of it
live inside cyberchef.ingress.yaml, behind ingress.enabled.
WHY A VALUE AND NOT AUTO-DETECTION
.Capabilities.APIVersions is unreliable at template time (it answers from the
cluster helm happens to be pointed at, and not at all for `helm template`),
while an HTTPRoute without the Gateway-API CRDs fails at INSTALL time. A chart
that guesses would therefore sometimes render an object the cluster rejects and
sometimes silently render nothing. So the mode is a value.
*/}}
{{- /*
Null-safe like every other block this chart dereferences (#99): an override
written without children - "httpRoute:" with the keys below it deleted - is
YAML null, and `.Values.httpRoute.enabled` would abort the render with a nil
pointer. An empty block means "chart defaults apply", and all three defaults
here are empty or false, so the whole block collapses to "no HTTPRoute".
*/ -}}
{{- $httpRoute := .Values.httpRoute | default dict -}}
{{- if and $httpRoute.enabled .Values.ingress.enabled -}}
{{- fail "ingress.enabled and httpRoute.enabled are both true - set exactly one of them. The Ingress and the HTTPRoute would publish the same Service under the same hostname over two different data paths, which is already a conflict at DNS level: one name, two addresses, and which one a client reaches is luck. A migration runs over a SECOND hostname (set httpRoute.hostnames explicitly while the Ingress keeps ingress.domain), never over parallel operation of both." -}}
{{- end -}}
{{- if $httpRoute.enabled -}}
{{- $parentRefs := $httpRoute.parentRefs | default list -}}
{{- if not $parentRefs -}}
{{- fail "httpRoute.enabled is true but httpRoute.parentRefs is empty - an HTTPRoute without a parent attaches to no Gateway. It is created, it is schema-valid, and it routes NOTHING, with no event and no condition that says so; the only symptom is a hostname that does not answer. Name the cluster's Gateway, e.g. httpRoute.parentRefs: [{name: public, namespace: gateway}]." -}}
{{- end -}}
{{- /*
One source of truth for the hostname: an empty httpRoute.hostnames means
[ingress.domain], so the two cannot drift apart. ingress.domain is read here
even though no Ingress is rendered in this mode - the value names the public
hostname of this release, which is what both modes need; it is not an Ingress
switch. Setting httpRoute.hostnames explicitly is what a migration over a
second hostname needs, and it then wins over ingress.domain.
*/ -}}
{{- $hostnames := $httpRoute.hostnames | default list -}}
{{- if not $hostnames -}}
{{- $hostnames = list (required "A Domain/Host must be provided: set ingress.domain (used as the HTTPRoute's single hostname when httpRoute.hostnames is empty) or set httpRoute.hostnames explicitly" .Values.ingress.domain) -}}
{{- end -}}
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: {{ .Release.Name }}-{{ .Chart.Name }}-httproute
spec:
parentRefs:
{{- toYaml $parentRefs | nindent 4 }}
hostnames:
{{- toYaml $hostnames | nindent 4 }}
rules:
# The Ingress counterpart is `path: / , pathType: Prefix` on the one
# backend Service - CyberChef is a static bundle served at the root and has
# no second path. The port is the Service's port NUMBER and not its name:
# backendRefs takes a PortNumber, so unlike the Ingress this reference
# cannot be made by name and the 80 below has to track
# cyberchef.service.yaml by hand. ci/run-httproute-match-test.sh is what
# holds that pair - it fails when a backendRef names a Service, or a port,
# the same render does not create.
- matches:
- path:
type: PathPrefix
value: /
backendRefs:
- name: {{ .Release.Name }}-{{ .Chart.Name }}-service
port: 80
{{- end }}
+1 -30
View File
@@ -57,42 +57,13 @@
"type": ["string", "null"]
},
"annotations": {
"description": "Extra Ingress annotations, merged with the cert-manager annotation the chart always sets. Free-form keys by nature. No counterpart in Gateway mode: the Gateway API has no annotation channel, so these are gone when httpRoute.enabled is used instead.",
"description": "Extra Ingress annotations, merged with the cert-manager annotation the chart always sets. Free-form keys by nature.",
"type": "object"
}
}
},
"httpRoute": {
"description": "Gateway-API alternative to the Ingress (Gitea #114). Mutually exclusive with ingress.enabled - the render aborts when both are true. The chart renders only an HTTPRoute with parentRefs; class and certificate sit on the Gateway and belong to the platform. The Gateway-API CRDs must be present in the cluster, because the chart cannot detect them.",
"type": "object",
"additionalProperties": false,
"properties": {
"enabled": {
"type": "boolean"
},
"parentRefs": {
"description": "The Gateway(s) the route attaches to. Required while httpRoute.enabled is true, enforced by required()-equivalent in the template (an empty list aborts the render) - a JSON Schema \"required\" cannot do it, because the key ships as an empty list and is therefore present. Entries are the Gateway API's ParentReference and are passed through unchanged.",
"type": "array",
"items": {
"$ref": "#/definitions/parentRef"
}
},
"hostnames": {
"description": "Empty means [ingress.domain], so the hostname has a single source of truth.",
"type": "array",
"items": {
"type": "string"
}
}
}
}
},
"definitions": {
"parentRef": {
"description": "A Gateway API ParentReference, passed verbatim into HTTPRoute.spec.parentRefs. Deliberately left OPEN (no additionalProperties: false), for the same reason probes and security contexts are: Kubernetes owns this schema, the Gateway API adds fields to it across releases, and a closed list here would reject a field only upstream knows about. What the chart can assert is that \"name\" is there - it is required by the API and a reference without it attaches to nothing, which is the silent failure this chart aborts the render over.",
"type": "object",
"required": ["name"]
},
"resources": {
"description": "May be null / empty: the chart then falls back to its default requests and computed limits (issue #99) instead of rendering a container without resource accounting.",
"type": ["object", "null"],
+3 -50
View File
@@ -68,60 +68,13 @@ cyberchef:
ephemeralStorage: 10Mi
ingress:
# Set to false to omit the Ingress entirely (the required clusterIssuer value
# is then not needed either, because it is only read inside the Ingress
# template). Mutually exclusive with httpRoute.enabled below: with both true
# the render aborts, naming both values.
# Set to false to omit the Ingress entirely (the required domain and
# clusterIssuer values are then not needed either, because they are only
# read inside the Ingress template).
enabled: true
clusterIssuer: null
# The public hostname of this release. Read by the Ingress AND - when
# httpRoute.hostnames is empty - by the HTTPRoute, so it stays the single
# source of truth for the hostname in both modes.
domain: null
className: nginx
# Extra annotations for the Ingress; merged with the cert-manager
# cluster-issuer annotation the chart always sets.
#
# THESE HAVE NO COUNTERPART IN GATEWAY MODE. The Gateway API has no
# annotation channel for controller behaviour, so everything configured here
# - external auth in front of an app that has no login of its own, for
# instance - is simply gone when httpRoute.enabled is used instead. It is
# then the Gateway's business, or that of an implementation-specific policy
# object this chart deliberately does not render.
annotations: {}
# Gateway-API alternative to the Ingress above (Gitea #114). Off by default, so
# an existing release is unaffected; exactly one of ingress.enabled and
# httpRoute.enabled may be true.
#
# The chart renders ONLY an HTTPRoute with parentRefs - no Gateway, no
# gatewayClassName, no Certificate and no implementation-specific policy
# object. Class and certificate belong to the Gateway and therefore to the
# platform, which is what keeps this chart independent of whether the cluster
# runs nginx-gateway-fabric, Cilium, Envoy Gateway or Istio.
#
# The Gateway-API CRDs must be installed in the cluster: the chart does not and
# cannot detect them (.Capabilities.APIVersions is unreliable at template
# time), so with httpRoute.enabled: true and no CRDs the INSTALL fails.
httpRoute:
enabled: false
# The Gateway(s) this route attaches to - a platform object, usually in
# another namespace, which must allow this namespace in its listener's
# allowedRoutes. One entry per parent, the fields are the Gateway API's
# ParentReference (name, and optionally namespace, sectionName, port, group,
# kind), passed through unchanged:
#
# parentRefs:
# - name: public
# namespace: gateway
# sectionName: https
#
# REQUIRED while httpRoute.enabled is true - the render aborts on an empty
# list, because a route without a parent is created, is valid and routes
# nothing, with no event to say so.
parentRefs: []
# Empty means [ingress.domain], so the hostname has one source of truth. Set
# explicitly only to publish under a different or an additional name - which
# is how a migration from the Ingress runs: the HTTPRoute takes a second
# hostname while the Ingress keeps ingress.domain.
hostnames: []
-27
View File
@@ -245,33 +245,6 @@
versioningTemplate: "{{#if versioning}}{{{versioning}}}{{else}}semver{{/if}}",
},
{
description: [
"The same `# renovate:` comment convention for tool versions pinned in a ci/ SHELL",
"script, where the assignment is `VAR=value` instead of the workflow's `VAR: value`.",
"Today that is GATEWAY_API_VERSION in ci/gateway-api-crds.sh — the Gateway-API CRDs the",
"install-test applies for a chart rendering an HTTPRoute (Gitea #114). They are fetched",
"at a pinned version rather than vendored, exactly as Trivy's tarball and kubectl are,",
"so without this manager the pin would simply age in place.",
"",
"A SEPARATE block and not a second matchString on the one above, deliberately: that",
"rule was verified by dry run and covers every workflow in the repo, and widening its",
"managerFilePatterns would put that verification back in question for a second file",
"shape. The cost of the split is one more block; the cost of being wrong is an update",
"path that silently stops matching (#158: a valid rule is not a matching rule).",
"",
"Prereleases are excluded repo-wide, which matters here: upstream publishes -rc.N tags",
"as GitHub releases, and the standard channel of a release candidate is not what a",
"pipeline should validate against.",
],
customType: "regex",
managerFilePatterns: ["ci/*.sh"],
matchStrings: [
"# renovate: datasource=(?<datasource>\\S+) depName=(?<depName>\\S+)(?: versioning=(?<versioning>\\S+))?\\s+\\w+=[\"']?(?<currentValue>[^\"'\\s]+)",
],
versioningTemplate: "{{#if versioning}}{{{versioning}}}{{else}}semver{{/if}}",
},
// --- Chart app versions -------------------------------------------------
//
// Every chart renders its main image as `<repo>:{{ .Chart.AppVersion }}`,
+1 -1
View File
@@ -5,7 +5,7 @@ icon: https://helm.sh/img/helm.svg
type: application
version: 4.2.0+upxxx
version: 4.1.0+upxxx
appVersion: "xxx"
maintainers:
+8 -58
View File
@@ -22,72 +22,25 @@ Note for the copier: a NOTES.txt renders no Kubernetes object, so adding one
is purely additive - a MINOR bump, and safe to roll out at any time.
*/}}
{{- $replicas := include "xxx.replicas" (dict "scaleDown" .Values.scaleDown "replicas" .Values.xxx.replicas) -}}
{{- $httpRoute := .Values.httpRoute | default dict -}}
{{- $hostnames := $httpRoute.hostnames | default list -}}
{{- if not $hostnames -}}
{{- $hostnames = list .Values.ingress.domain -}}
{{- end -}}
xxx {{ .Chart.AppVersion }} — release {{ .Release.Name }} in namespace {{ .Release.Namespace }}.
{{- if .Values.ingress.enabled }}
ACCESS — INGRESS MODE (ingress.enabled: true, httpRoute.enabled: false)
ACCESS
https://{{ .Values.ingress.domain }}
The certificate is requested by cert-manager through the "{{ .Values.ingress.clusterIssuer }}"
cluster issuer. Until it has been issued the browser shows a warning; follow it with
kubectl -n {{ .Release.Namespace }} get certificate tls-{{ .Release.Name }}-{{ .Chart.Name }}-ingress
{{- else if $httpRoute.enabled }}
ACCESS — GATEWAY MODE (httpRoute.enabled: true, ingress.enabled: false)
{{- range $hostnames }}
https://{{ . }}
{{- end }}
This release renders an HTTPRoute ({{ .Release.Name }}-{{ .Chart.Name }}-httproute) and nothing else of
the routing path — no Gateway, no class, no Certificate. Three things
therefore belong to the cluster rather than to this chart, and the hostname
above does not answer while any of them is missing:
1. The Gateway-API CRDs — present, or this install would have failed.
2. The Gateway named in httpRoute.parentRefs
{{- range $httpRoute.parentRefs }}
{{ .name }}{{ if .namespace }} in namespace {{ .namespace }}{{ end }}{{ if .sectionName }}, listener {{ .sectionName }}{{ end }}
{{- end }}
must exist AND allow routes from namespace {{ .Release.Namespace }} in its
listener's allowedRoutes. A refused attachment shows up only in the
route's own status:
kubectl -n {{ .Release.Namespace }} get httproute {{ .Release.Name }}-{{ .Chart.Name }}-httproute -o yaml
"Accepted: True" and "ResolvedRefs: True" under status.parents[] is
what a working attachment looks like; an empty status means no
controller has looked at the route at all.
3. The certificate for the hostname, on the Gateway's listener.
ingress.clusterIssuer is NOT used in this mode and is not required —
this chart renders no Certificate here.
ingress.annotations is inert in this mode as well: the Gateway API has no
annotation channel. When you copy this chart, say here what that costs YOUR
app — an upload limit set through proxy-body-size has no Gateway-API
equivalent at all, so silence on this point means the operator gets the
Gateway's default and finds out from a proxy error message.
ingress.domain STAYS REQUIRED in this mode, and not only as the hostname
fallback: the env block in values.yaml reads it for NORMAL_ENV, which is why
that read carries a required() of its own. Check that per chart (README,
guideline 12, point 1) — in a chart where nothing outside the routing
templates reads it, httpRoute.hostnames can replace it entirely.
{{- else }}
ACCESS — NEITHER INGRESS NOR HTTPROUTE
ingress.enabled and httpRoute.enabled are both false, so this release has no
Ingress, no HTTPRoute and no certificate, and ingress.clusterIssuer is not
needed. ingress.domain STAYS REQUIRED even so: the env block in values.yaml
reads it for NORMAL_ENV, which is why that read carries a required() of its
own. Without it the workload would start with an empty address and nothing
would point at the mistake.
ACCESS — NO INGRESS
ingress.enabled is false, so this release has no Ingress and no certificate,
and ingress.clusterIssuer is not needed. ingress.domain STAYS REQUIRED even
so: the env block in values.yaml reads it for NORMAL_ENV, which is why that
read carries a required() of its own. Without it the workload would start
with an empty address and nothing would point at the mistake.
Reach the release inside the cluster with
@@ -99,10 +52,7 @@ ACCESS — NEITHER INGRESS NOR HTTPROUTE
SCALED TO ZERO
{{ if .Values.scaleDown }}scaleDown: true{{ else }}xxx.replicas: 0{{ end }} — the workload runs with replicas: 0, so the endpoint
above does not answer until you set it back and upgrade again. PVC, Secret{{ if .Values.ingress.enabled }},
Service and Ingress stay in place, so the certificate is not re-issued.{{ else if $httpRoute.enabled }},
Service and HTTPRoute stay in place, so the route keeps pointing at a Service
with no endpoints — the Gateway then answers with an error of its own rather
than with nothing.{{ else }} and
Service and Ingress stay in place, so the certificate is not re-issued.{{ else }} and
Service stay in place.{{ end }}
{{ end }}
THE 1PASSWORD ITEM MUST CARRY THE FIELDS THE CHART READS
@@ -1,99 +0,0 @@
{{/*
HTTPRoute — the Gateway-API alternative to xxx.ingress.yaml (Gitea #114), and
the reference implementation of that convention (README, "Ingress and
Gateway API").
Copy this file with the rest of template-chart and rename it to
<app>.httproute.yaml. There is nothing chart-specific to replace in the body
except the BACKEND: the Service name follows the naming scheme and the port
below must be the port number your <app>.service.yaml actually exposes.
WHAT THIS RENDERS, AND WHAT IT MUST NOT
Only an HTTPRoute with parentRefs. No Gateway, no gatewayClassName, no
Certificate, and no implementation-specific policy object
(ClientSettingsPolicy, ClientTrafficPolicy and their equivalents). That subset
is the part which looks identical on nginx-gateway-fabric, Cilium, Envoy
Gateway and Istio, and keeping to it is the whole reason the cluster's
implementation can be chosen - or replaced - without touching the charts
again. Adding a policy object here binds the chart to one implementation and
to a CRD whose presence it cannot check.
The consequence is that class and certificate belong to the PLATFORM: they sit
on the Gateway and its listeners, in cluster-config. A chart in this mode
renders no Certificate, so ingress.clusterIssuer does nothing - which is why
it may only be `required` under ingress.enabled.
WHAT HAS NO COUNTERPART AT ALL
The Gateway API has no annotation channel for controller behaviour. Everything
a chart does through ingress.annotations today - proxy-body-size,
rewrite-target, external auth - is gone in this mode. rewrite-target has a real
equivalent (the URLRewrite filter in a rule, not added here because this
scaffold has no base path to strip); an upload limit does NOT, and a chart that
depends on one must SAY SO in its NOTES.txt for this mode rather than quietly
inherit the Gateway's default.
WHY A VALUE AND NOT 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 at all. So the mode is a value, and the CRDs are the cluster's
business.
*/}}
{{- /*
Null-safe like every other block a template here dereferences (#99): an
override written without children - "httpRoute:" with its keys deleted - is
YAML null, and `.Values.httpRoute.enabled` would abort with a nil pointer. An
empty block means "chart defaults apply", and all three defaults are empty or
false, so it collapses to "no HTTPRoute".
*/ -}}
{{- $httpRoute := .Values.httpRoute | default dict -}}
{{- if and $httpRoute.enabled .Values.ingress.enabled -}}
{{- fail "ingress.enabled and httpRoute.enabled are both true - set exactly one of them. The Ingress and the HTTPRoute would publish the same Service under the same hostname over two different data paths, which is already a conflict at DNS level: one name, two addresses, and which one a client reaches is luck. A migration runs over a SECOND hostname (set httpRoute.hostnames explicitly while the Ingress keeps ingress.domain), never over parallel operation of both." -}}
{{- end -}}
{{- if $httpRoute.enabled -}}
{{- $parentRefs := $httpRoute.parentRefs | default list -}}
{{- if not $parentRefs -}}
{{- fail "httpRoute.enabled is true but httpRoute.parentRefs is empty - an HTTPRoute without a parent attaches to no Gateway. It is created, it is schema-valid, and it routes NOTHING, with no event and no condition that says so; the only symptom is a hostname that does not answer. Name the cluster's Gateway, e.g. httpRoute.parentRefs: [{name: public, namespace: gateway}]." -}}
{{- end -}}
{{- /*
One source of truth for the hostname: an empty httpRoute.hostnames means
[ingress.domain]. Two values that have to agree drift apart. ingress.domain is
read here although no Ingress is rendered in this mode - it names the public
hostname of the release, which both modes need, and it is not an Ingress
switch. In THIS chart that read is not even the only one: the env block in
values.yaml reads ingress.domain too, which is why it carries a required() of
its own and stays mandatory with the Ingress switched off (README, guideline
12, point 1). Check that per chart; do not copy the conclusion.
*/ -}}
{{- $hostnames := $httpRoute.hostnames | default list -}}
{{- if not $hostnames -}}
{{- $hostnames = list (required "A Domain/Host must be provided: set ingress.domain (used as the HTTPRoute's single hostname when httpRoute.hostnames is empty) or set httpRoute.hostnames explicitly" .Values.ingress.domain) -}}
{{- end -}}
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: {{ .Release.Name }}-{{ .Chart.Name }}-httproute
spec:
parentRefs:
{{- toYaml $parentRefs | nindent 4 }}
hostnames:
{{- toYaml $hostnames | nindent 4 }}
rules:
# The Ingress counterpart is `path: / , pathType: Prefix` on the backend
# Service. The port is the Service's port NUMBER, not its name:
# backendRefs takes a PortNumber, so unlike the Ingress this reference
# cannot be made by name, and the number here has to track
# <app>.service.yaml by hand. ci/run-httproute-match-test.sh is what holds
# that pair - it fails when a backendRef names a Service, or a port, that
# the same render does not create.
- matches:
- path:
type: PathPrefix
value: /
backendRefs:
- name: {{ .Release.Name }}-{{ .Chart.Name }}-service
port: 80
{{- end }}
+1 -30
View File
@@ -85,42 +85,13 @@
"type": ["string", "null"]
},
"annotations": {
"description": "Extra Ingress annotations, merged with the cert-manager annotation the chart always sets. Free-form keys by nature, so this object stays open. No counterpart in Gateway mode: the Gateway API has no annotation channel, so these are gone when httpRoute.enabled is used instead.",
"description": "Extra Ingress annotations, merged with the cert-manager annotation the chart always sets. Free-form keys by nature, so this object stays open.",
"type": "object"
}
}
},
"httpRoute": {
"description": "Gateway-API alternative to the Ingress (Gitea #114). Mutually exclusive with ingress.enabled - the render aborts when both are true. The chart renders only an HTTPRoute with parentRefs; class and certificate sit on the Gateway and belong to the platform. The Gateway-API CRDs must be present in the cluster, because the chart cannot detect them.",
"type": "object",
"additionalProperties": false,
"properties": {
"enabled": {
"type": "boolean"
},
"parentRefs": {
"description": "The Gateway(s) the route attaches to. Required while httpRoute.enabled is true, and enforced in the template (an empty list aborts the render) - a JSON Schema \"required\" cannot do it, because the key ships as an empty list and is therefore present. Entries are the Gateway API's ParentReference and are passed through unchanged.",
"type": "array",
"items": {
"$ref": "#/definitions/parentRef"
}
},
"hostnames": {
"description": "Empty means [ingress.domain], so the hostname has a single source of truth.",
"type": "array",
"items": {
"type": "string"
}
}
}
}
},
"definitions": {
"parentRef": {
"description": "A Gateway API ParentReference, passed verbatim into HTTPRoute.spec.parentRefs. Deliberately left OPEN (no additionalProperties: false), for the same reason probes and security contexts are: Kubernetes owns this schema, the Gateway API adds fields to it across releases, and a closed list here would reject a field only upstream knows about. What a chart can assert is that \"name\" is there - it is required by the API, and a reference without it attaches to nothing, which is the silent failure the render aborts over.",
"type": "object",
"required": ["name"]
},
"resources": {
"description": "May be null / empty: the chart then falls back to its default requests and computed limits (issue #99) instead of rendering a container without resource accounting.",
"type": ["object", "null"],
+3 -49
View File
@@ -118,59 +118,13 @@ storage:
ingress:
# Gates the whole Ingress template. Note that ingress.domain is ALSO read by
# the env block above AND by the HTTPRoute below, so it stays required even
# with the Ingress switched off; only clusterIssuer becomes unnecessary.
# Check this per chart instead of assuming either way (guideline 1).
#
# Mutually exclusive with httpRoute.enabled below: with both true the render
# aborts, naming both values.
# the env block above, so it stays required even with the Ingress switched
# off; only clusterIssuer becomes unnecessary. Check this per chart instead
# of assuming either way (guideline 1).
enabled: true
clusterIssuer: null
domain: null
className: nginx
# Extra annotations for the Ingress; merged with the cert-manager
# cluster-issuer annotation the chart always sets.
#
# NO COUNTERPART IN GATEWAY MODE. The Gateway API has no annotation channel
# for controller behaviour, so everything set here is gone when
# httpRoute.enabled is used instead. rewrite-target has an equivalent (the
# URLRewrite filter in an HTTPRoute rule); an upload limit
# (proxy-body-size) has none at all, and a chart that depends on one must say
# so in its NOTES.txt for that mode.
annotations: {}
# Gateway-API alternative to the Ingress above (Gitea #114). Off by default, so
# adding it to an existing chart is additive; exactly one of ingress.enabled
# and httpRoute.enabled may be true.
#
# The chart renders ONLY an HTTPRoute with parentRefs - no Gateway, no
# gatewayClassName, no Certificate and no implementation-specific policy
# object. Class and certificate belong to the Gateway and therefore to the
# platform, which is what keeps a chart independent of whether the cluster runs
# nginx-gateway-fabric, Cilium, Envoy Gateway or Istio.
#
# The Gateway-API CRDs must be installed in the cluster: the chart does not and
# cannot detect them (.Capabilities.APIVersions is unreliable at template
# time), so with httpRoute.enabled: true and no CRDs the INSTALL fails.
httpRoute:
enabled: false
# The Gateway(s) this route attaches to - a platform object, usually in
# another namespace, which must allow this namespace in its listener's
# allowedRoutes. One entry per parent; the fields are the Gateway API's
# ParentReference (name, and optionally namespace, sectionName, port, group,
# kind), passed through unchanged:
#
# parentRefs:
# - name: public
# namespace: gateway
# sectionName: https
#
# REQUIRED while httpRoute.enabled is true - the render aborts on an empty
# list, because a route without a parent is created, is valid and routes
# nothing, with no event to say so.
parentRefs: []
# Empty means [ingress.domain], so the hostname has one source of truth. Set
# explicitly only to publish under a different or an additional name - which
# is how a migration from the Ingress runs: the HTTPRoute takes a second
# hostname while the Ingress keeps ingress.domain.
hostnames: []