feat(keycloak): one hostname per realm, in both routing modes
Validate Helm Charts / Detect changed charts (pull_request) Successful in 4s
Validate Helm Charts / Resource quantities (pull_request) Successful in 14s
Validate Helm Charts / Selector match (pull_request) Successful in 14s
Validate Helm Charts / HTTPRoute match (pull_request) Successful in 14s
Validate Helm Charts / Build keycloak (pull_request) Successful in 8s
Validate Helm Charts / Trivy config keycloak (pull_request) Successful in 24s
Install-test / Install-test keycloak (pull_request) Successful in 3m29s
Validate Helm Charts / Install-test (pull_request) Successful in 3m29s
Validate Helm Charts / Detect changed charts (pull_request) Successful in 4s
Validate Helm Charts / Resource quantities (pull_request) Successful in 14s
Validate Helm Charts / Selector match (pull_request) Successful in 14s
Validate Helm Charts / HTTPRoute match (pull_request) Successful in 14s
Validate Helm Charts / Build keycloak (pull_request) Successful in 8s
Validate Helm Charts / Trivy config keycloak (pull_request) Successful in 24s
Install-test / Install-test keycloak (pull_request) Successful in 3m29s
Validate Helm Charts / Install-test (pull_request) Successful in 3m29s
keycloak.realmHosts gives a realm a hostname of its own: this release's Service published under a further name, with that name's bare / sent to /realms/<realm>/account/. It renders in BOTH modes - one Ingress per entry carrying nginx.ingress.kubernetes.io/app-root, or one HTTPRoute per entry whose Exact match on / carries a RequestRedirect filter - because a routing value in this repository is mode-independent by design - ingress.domain is read in Gateway mode too - so a feature rendering in one mode only would silently disappear when an installation switched. Default []: nothing renders, and the render is byte-identical to 1.2.0 for the fixture's values in both modes. Minor, additive, no value becomes required. ONE OBJECT PER ENTRY, not one with several hostnames. spec.hostnames applies to the whole object and a rule's matches are path-based, so inside a shared route every entry would match Exact: / and the realm could not be told apart. Names therefore carry the LIST INDEX, as homeassistant's per-host routes do and for the same reason - a realm name is not a dependable object-name fragment. The consequence is written down rather than left to be discovered: reordering the list renames and recreates the objects. THE WORK IS IN THE INGRESS HALF, NOT THE ROUTE. Two convention rules do not survive these objects, and both deviations are required rather than tolerated: tlsSecretName is a per-entry value with NO default, because these hostnames already have certificates and several entries typically share one, so a derived name would ask cert-manager for a second certificate for a name a wildcard already covers; and the cert-manager annotation is OPTIONAL per entry, because cert-manager keys a Certificate by the secret it writes and only one object per secret may request it - the same deliberate omission the qBittorrent object in freesailarr makes. Annotations are per entry too: of the four hostnames this was written for, three carry the proxy-buffer pair and one does not, which a chart-global block could not express. statusCode: 302 is written out at the field's own default. 301 and 302 are both Support: Core, so the portability trap of #119 - an implementation declining an Extended code sets Accepted: False on the WHOLE route - does not apply here; ingress-nginx emits 302 for app-root, and a value that is only accidentally right documents nothing. Five render aborts, each against a failure that reports nothing: an entry without a host (ingress-nginx reads an empty host as a catch-all for the whole controller), without a realm (/realms//account/, a 404 behind a healthy hostname and certificate), without a tlsSecretName in Ingress mode (the controller's default certificate, a browser-only failure), a repeated host or one the release already publishes (webhook rejection in Ingress mode, two merged routes decided by object age in Gateway mode), and a filled list with both modes off (no object at all, and nothing in the release that reveals the list was ignored). The entry-shape checks sit in a shared helper so they exist in both modes, and the Ingress template skips them while httpRoute.enabled is also true so that the specific message cannot hide the mutual-exclusion one. CI: the two entries live in ci/keycloak/test-values.yaml, which ci/httproute/keycloak.yaml is applied on top of, so one definition covers both modes - two Ingress objects in the install-test, two HTTPRoutes in the render test. They cover both Ingress forms, with and without a clusterIssuer, and the render test holds their inertness in Gateway mode. The httproute match test goes from 56 assertions over 21 charts to 60, keycloak's share from 4 to 8 plus two "not checked" lines for the backend-less redirect rules. The #118 prefix/redirect pairing correctly does not fire: it is scoped to rules that strip a prefix, and a realm route strips nothing. NOT IN THIS CHANGE: the migration. Nothing in jk-cluster-config is touched. With the default [] no running system changes, so the order a takeover has to run in is documented instead - remove the objects in the owning repository first, then install with the list filled, because the reverse or both at once is rejected by ingress-nginx's admission webhook and leaves a Fleet bundle in ErrApplied until forceSyncGeneration is raised. Whether anything there depends on those objects BY NAME is unchecked and named as unchecked. Fixes #122
This commit is contained in:
10 files changed
+629
-16
No files matched your search
@@ -23,7 +23,7 @@ Every chart has a wiki page of its own — `https://git.jk-dev.de/jlab-cluster/h
|
||||
| `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) |
|
||||
| `homeassistant` | Home Assistant | StatefulSet; the only chart carrying Helm tests (`templates/tests/`) |
|
||||
| `keycloak` | Keycloak (OpenID Connect / SAML identity provider) | **Two** replicas with `cache=ispn`, clustered through Keycloak's default `jdbc-ping` stack — discovery runs over the PostgreSQL that is there anyway, so no headless Service and no JGroups wiring. Deployment, **no PVC** (Keycloak 26 keeps everything in the database, user sessions included). **The chart has two init containers and needs both** (Gitea #83): `db`, `health-enabled` and `metrics-enabled` are Keycloak *build* options, the published image is built with their defaults, and a runtime change makes Keycloak rewrite `/opt/keycloak/lib/quarkus` at start — which `readOnlyRootFilesystem: true` forbids and which upstream describes as a start that "creates an image that is mutable". An empty `emptyDir` over that path does not work either, because `QuarkusEntryPoint` loads `quarkus/quarkus-application.dat` before it dispatches any command, `build` included. So one init container **seeds** the directory into a volume, the next runs `kc.sh build` into it, and the serving container runs `start --optimized` — every container keeping the full hardening baseline. Build options therefore exist in exactly **one** place and a build option in `keycloak.env` fails the render. First chart with a `PodDisruptionBudget` (§11) and first `quay.io` image (hence the entry in `ci/trivy/data/ksv0125.yaml`). Two Services (8080, and the management port 9000 **never** published — it serves `/health/*` and optionally `/metrics` unauthenticated) and two Ingresses. **The admin hostname is not an access control**: `hostname-admin` only decides which URLs the server generates, and upstream states that it "does not prevent accessing the Administration REST API endpoints via the frontend URL" — restricting that API is reverse-proxy work, and the chart renders no `/admin` deny rule. `ingress.domain` is read as `KC_HOSTNAME` (as a full URL, which upstream requires alongside `hostname-admin`), so it stays required with the Ingress off. Liveness is `/health/live` and deliberately not `/health` or `/health/ready` — those include the database, and a liveness probe on them restarts the whole cluster for as long as the database is away. **`keycloak.metrics.enabled` defaults to `true` for the readiness probe, not for the metrics**: upstream's table of health checks carries a "Requires Metrics" column and the *database* check is the row that says yes, so with it off `/health/ready` answers 200 while the database is gone — the `urlaubsverwaltung` shape (#218) with the dependency hidden behind an option whose name says observability. Two traps at the values: upstream's `db-pool-max-size` default of 100 would ask a default PostgreSQL for 200 connections across two replicas (the chart ships 20), and two releases sharing one database **merge into one Infinispan cluster** unless the cluster names differ |
|
||||
| `keycloak` | Keycloak (OpenID Connect / SAML identity provider) | **Two** replicas with `cache=ispn`, clustered through Keycloak's default `jdbc-ping` stack — discovery runs over the PostgreSQL that is there anyway, so no headless Service and no JGroups wiring. Deployment, **no PVC** (Keycloak 26 keeps everything in the database, user sessions included). **The chart has two init containers and needs both** (Gitea #83): `db`, `health-enabled` and `metrics-enabled` are Keycloak *build* options, the published image is built with their defaults, and a runtime change makes Keycloak rewrite `/opt/keycloak/lib/quarkus` at start — which `readOnlyRootFilesystem: true` forbids and which upstream describes as a start that "creates an image that is mutable". An empty `emptyDir` over that path does not work either, because `QuarkusEntryPoint` loads `quarkus/quarkus-application.dat` before it dispatches any command, `build` included. So one init container **seeds** the directory into a volume, the next runs `kc.sh build` into it, and the serving container runs `start --optimized` — every container keeping the full hardening baseline. Build options therefore exist in exactly **one** place and a build option in `keycloak.env` fails the render. First chart with a `PodDisruptionBudget` (§11) and first `quay.io` image (hence the entry in `ci/trivy/data/ksv0125.yaml`). Two Services (8080, and the management port 9000 **never** published — it serves `/health/*` and optionally `/metrics` unauthenticated) and two Ingresses — plus one routing object per `keycloak.realmHosts` entry (Gitea #122), empty by default, which gives a realm a hostname of its own by sending that hostname's bare `/` to `/realms/<realm>/account/`: `app-root` in Ingress mode, an `Exact` match with a `RequestRedirect` filter in Gateway mode. Those entries carry a **required** per-entry TLS secret name and an **optional** per-entry issuer, the documented deviation in §4, because the hostnames they publish already have certificates and often share one. **The admin hostname is not an access control**: `hostname-admin` only decides which URLs the server generates, and upstream states that it "does not prevent accessing the Administration REST API endpoints via the frontend URL" — restricting that API is reverse-proxy work, and the chart renders no `/admin` deny rule. `ingress.domain` is read as `KC_HOSTNAME` (as a full URL, which upstream requires alongside `hostname-admin`), so it stays required with the Ingress off. Liveness is `/health/live` and deliberately not `/health` or `/health/ready` — those include the database, and a liveness probe on them restarts the whole cluster for as long as the database is away. **`keycloak.metrics.enabled` defaults to `true` for the readiness probe, not for the metrics**: upstream's table of health checks carries a "Requires Metrics" column and the *database* check is the row that says yes, so with it off `/health/ready` answers 200 while the database is gone — the `urlaubsverwaltung` shape (#218) with the dependency hidden behind an option whose name says observability. Two traps at the values: upstream's `db-pool-max-size` default of 100 would ask a default PostgreSQL for 200 connections across two replicas (the chart ships 20), and two releases sharing one database **merge into one Infinispan cluster** unless the cluster names differ |
|
||||
| `matrix-synapse` | Matrix Synapse | StatefulSet plus an nginx delegation Deployment/Service |
|
||||
| `minecraft-router` | itzg/mc-router | Deployment + routing ConfigMap + NodePort Service (TCP only, no ingress); `mappings` is a required value |
|
||||
| `minecraft-server` | itzg/minecraft-server | StatefulSet + PVC + Service + init-script ConfigMap (no ingress); service type is a value (`ClusterIP` by default, routed via `minecraft-router`). **Only the newest Java line is maintained** (chart 5.0.0, #158): the directory carries one line, older ones stay published as frozen artifacts (java8/java17 at `4.0.0`, java21 at `4.1.0`) and receive no further chart changes — publishing once per line out of one directory is what made the lines drift. To run an older JVM with the current chart, set `image.tagOverride`; the chart then checks that `minecraft.version` fits the Java line of the **effective** tag and fails the render with both values named. Two cases pass unchecked on purpose: a tag without a `java<N>` part and a `minecraft.version` that is not a plain numeric `x[.y[.z]]`. `image.tagOverride` is deliberately outside Renovate's reach — `renovate.json5` tracks `appVersion` only. Since chart 2.0.0 the tag is an **immutable dated upstream release** (`appVersion: <release>-java8` / `-java17` / `-java21` / `-java25`, e.g. `2026.8.2-java8`) instead of the continuously rebuilt rolling line tag. Pilot for the strict `values.schema.json` (#68), which is now binding for every chart |
|
||||
@@ -181,7 +181,7 @@ Three things to know before touching it:
|
||||
- **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. *That any redirect is ever served* — the pairing check proves the rendered **object** carries a `RequestRedirect` rule on the bare prefix and nothing more: whether an implementation honours the `statusCode` or declines it and fails the whole route is outside what a render can see. *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**. The redirect rule the pairing check demands is itself backend-less, so a chart that satisfies that check also earns one "not checked" line; both verdicts are correct at once.
|
||||
- **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. `cyberchef` is exercised through its ordinary install-test fixture (which installs in Gateway mode, see below), every other converted chart through its overlay. For a multi-app chart the overlay also has to enter every shape the template has: `ci/httproute/freesailarr.yaml` moves one app onto a host of its own so that the shared-host route *and* the per-app route are both rendered.
|
||||
|
||||
Measured in CI (run 719, the pilot): **30 charts, 4 assertions held**, whole job 13 s including checkout and the Helm setup, no cluster. Group 1 took that to 20 assertions over 10 charts and group 2 to 36 over 15; the prefix/redirect pairing of Gitea #118 adds the first check since the pilot rather than a chart, which brought it to 37 over 15 — one assertion for `freesailarr`'s `/qbittorrent`, plus one more "not checked" line for the backend-less redirect rule it demands. **Group 3 takes it to `56` assertions over `21` charts**, which is the sweep finished except for the frozen `rss-bridge`. Group 3's 19 come from six charts and three of them contribute more than one route: `keycloak` 4 (public and admin routes), `matrix-synapse` 4 (homeserver and delegation routes), `homeassistant` 5 (two routes, three rules — its overlay is the only one in the repo that has to *supply* hostnames, since the chart has no `ingress.domain`), `bulwark` 2, `stalwart` 2, `ytdlp-web-player` 2. 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, a run with zero assertions, and — for the pairing — both the redirect rule deleted outright and the redirect pointed at a path other than the one stripped; each produced a `FAIL` naming both sides and a non-zero exit.
|
||||
Measured in CI (run 719, the pilot): **30 charts, 4 assertions held**, whole job 13 s including checkout and the Helm setup, no cluster. Group 1 took that to 20 assertions over 10 charts and group 2 to 36 over 15; the prefix/redirect pairing of Gitea #118 adds the first check since the pilot rather than a chart, which brought it to 37 over 15 — one assertion for `freesailarr`'s `/qbittorrent`, plus one more "not checked" line for the backend-less redirect rule it demands. **Group 3 takes it to `56` assertions over `21` charts**, which is the sweep finished except for the frozen `rss-bridge`. Group 3's 19 come from six charts and three of them contribute more than one route: `keycloak` 4 (public and admin routes), `matrix-synapse` 4 (homeserver and delegation routes), `homeassistant` 5 (two routes, three rules — its overlay is the only one in the repo that has to *supply* hostnames, since the chart has no `ingress.domain`), `bulwark` 2, `stalwart` 2, `ytdlp-web-player` 2. **The realm hostnames of Gitea #122 add `4`**: `keycloak`'s share goes from 4 to 8, because its two realm routes each earn one `parentRefs` assertion and one `backendRefs` assertion on their prefix rule, plus one "not checked" line each for their backend-less redirect rule (`keycloak: ok (8 assertion(s) held, 2 not checked)`). Measured as `60` over `21` charts on the branch that added them, which did **not** contain the `rss-bridge` conversion — the two landed in parallel, so the **delta** is the durable figure here and the running total is whatever the first CI run on `main` prints after both. No new *check*: the two realm routes are objects, and the prefix/redirect pairing of #118 deliberately does **not** fire on them — it is scoped to rules that strip a prefix with a `ReplacePrefixMatch` rewrite, and a realm route strips nothing, it redirects the bare root of a hostname that serves one realm. 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, a run with zero assertions, and — for the pairing — both the redirect rule deleted outright and the redirect pointed at a path other than the one stripped; each produced a `FAIL` naming both sides and a non-zero exit.
|
||||
|
||||
### Install-test (PR pipeline stage 2)
|
||||
|
||||
@@ -386,6 +386,7 @@ These guidelines are **binding for every current and future chart** in this repo
|
||||
- `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`.
|
||||
- **A set of objects publishing ONE backend under hostnames the chart does not own is the third admissible shape, and it buys its secret name and its issuer per entry (Gitea #122).** Where a chart's app serves several tenants from one deployment and each wants a hostname of its own — `keycloak`'s realms are the case this was written for — the hostnames come as a values list and the chart renders one object per entry. Two convention rules do not survive that, and both deviations are **required**, not merely tolerated: the TLS secret name is a per-entry value with **no** default, because these hostnames already have certificates and several entries typically share one (a wildcard for a zone), so a derived `tls-<release>-<chart>-...` name per entry would ask cert-manager for a second certificate for a name that is already covered — one ACME order per hostname, for nothing; and the cert-manager annotation becomes **optional per entry**, because cert-manager keys a Certificate by the secret it writes, so of several objects naming one secret exactly one may carry it. That is the same deliberate omission the qBittorrent object in `freesailarr` makes, for the same reason, and it is why the secret name is required rather than defaulted: a chart that invents the name cannot also leave the issuing to somebody else. Annotations are per entry too, since the set is not uniform across the hostnames. Object names carry the **list index**, for the reason spelled out in the homeassistant bullet below — a hostname is not a dependable object-name fragment, so reordering the list renames and recreates the objects — and the entries render in **both** routing modes — a mode-asymmetric feature is not admissible, which is also why the per-entry `parentRefs` exists as the counterpart of the per-entry class.
|
||||
- **Documented exception — homeassistant, and it extends to the Gateway mode**: 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). Because it has **no `ingress.domain`**, it has **no `httpRoute.hostnames`** either — the second chart without that key after `freesailarr`, and the schema rejects it outright so the absence is enforced rather than merely intended. The hostnames and the paths come from `ingress.hosts`, which is therefore read in **both** modes and is not an Ingress-only value, and the chart renders **one `HTTPRoute` per entry** of that list with one rule per path of the entry. One object carrying several hostnames would be wrong rather than merely different: an `HTTPRoute`'s rules apply to *every* hostname of the object, and per-host paths are this chart's whole point. `ingress.className`, `ingress.tls` and `ingress.annotations` are not read in Gateway mode. Route names carry the entry's **index**, not its hostname (a hostname is not a dependable object-name fragment), so reordering `ingress.hosts` renames routes and therefore recreates rather than edits them — harmless, since a route holds no state and no certificate, but worth knowing before it looks like a bug. Its `pathType` default is the awkward one; see the `ImplementationSpecific` rule in the Gateway-API bullets below.
|
||||
- **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:
|
||||
|
||||
@@ -30,11 +30,29 @@
|
||||
# parentRefs fall back to httpRoute.parentRefs below - the empty-admin
|
||||
# branch, which is the one an installation hits unless it wants an
|
||||
# internal Gateway.
|
||||
# - both routes' backendRef is checked against the serving Service of the
|
||||
# same render. Both point at the SAME Service, by PORT NUMBER
|
||||
# - every route's backendRef is checked against the serving Service of the
|
||||
# same render. They all point at the SAME Service, by PORT NUMBER
|
||||
# (keycloak.ports.http) rather than by the name "http" the Ingress uses,
|
||||
# because backendRefs takes a PortNumber - so this overlay is what holds
|
||||
# that number against the Service's.
|
||||
# - THE TWO REALM ROUTES (Gitea #122), which come from keycloak.realmHosts in
|
||||
# the fixture rather than from this file. Nothing is repeated here on
|
||||
# purpose: the fixture installs them as Ingress objects and this overlay
|
||||
# turns the same two entries into HTTPRoutes, so one definition covers both
|
||||
# modes and the two halves cannot drift into describing different hostnames.
|
||||
# What the routes add to the two above is a SECOND RULE SHAPE - an Exact
|
||||
# match on / carrying a RequestRedirect filter and no backendRefs at all.
|
||||
# That rule is reported as "not checked" by this test, correctly and by
|
||||
# design: a filter-only rule has no backend to resolve. The assertion it
|
||||
# does earn is on its sibling rule, the PathPrefix one that points at the
|
||||
# Service. The prefix/redirect pairing check of #118 does NOT fire for these
|
||||
# routes and must not: it is scoped to rules that STRIP a prefix with a
|
||||
# ReplacePrefixMatch rewrite, and these strip nothing - they redirect the
|
||||
# bare root of a hostname that serves one realm.
|
||||
# The entries also cover the two Ingress-mode forms (one with a
|
||||
# clusterIssuer, one without) and what this test holds about them is their
|
||||
# INERTNESS: neither tlsSecretName nor clusterIssuer nor the per-entry
|
||||
# annotation may appear anywhere in a rendered route.
|
||||
#
|
||||
# NOT covered here, and it cannot be: that either hostname answers. No Gateway
|
||||
# controller runs in CI, no route reaches "Accepted: True", and the
|
||||
|
||||
@@ -203,6 +203,43 @@ keycloak:
|
||||
mountPath: /opt/keycloak/themes/ci-theme
|
||||
readOnly: true
|
||||
|
||||
# TWO REALM HOSTNAMES (Gitea #122), and they are set HERE rather than in the
|
||||
# Gateway overlay on purpose: ci/httproute/keycloak.yaml is applied on top of
|
||||
# this file, so one definition covers BOTH modes - two Ingress objects in this
|
||||
# install, two HTTPRoutes in the render-only test, from the same two entries.
|
||||
# Defining them in the overlay instead would leave the Ingress half of the
|
||||
# feature with no coverage at all, and it is the half that carries the
|
||||
# required value and the convention deviation.
|
||||
#
|
||||
# WHAT THIS INSTALL ADDS TO THE RENDER TEST, which is the part a cluster-less
|
||||
# check cannot reach: the API server accepts both objects, including the
|
||||
# Service port named BY NAME ("http") - a typo there is invisible until
|
||||
# somebody opens the URL - and the second entry's cert-manager annotation does
|
||||
# not make the apply fail in a cluster without cert-manager. No ingress
|
||||
# controller answers in CI, so neither app-root nor the proxy buffers are
|
||||
# exercised; that is the same gap the two objects above have.
|
||||
#
|
||||
# THE TWO ENTRIES ARE THE TWO FORMS, deliberately:
|
||||
# [0] NO clusterIssuer - the normal case, where the certificate comes from
|
||||
# elsewhere (a wildcard, another release) and this object only consumes
|
||||
# the secret. It also carries a per-entry annotation, which is what
|
||||
# makes the per-entry (rather than chart-global) annotations block a
|
||||
# tested shape rather than an intended one.
|
||||
# [1] WITH clusterIssuer - the case where the certificate is issued for this
|
||||
# entry's own secret.
|
||||
# Both are inert in Gateway mode, and the render test is what holds that: the
|
||||
# routes it renders from these entries must show no trace of either field.
|
||||
realmHosts:
|
||||
- host: realm-one.ci.example.com
|
||||
realm: ci-realm-one
|
||||
tlsSecretName: tls-ci-realm-shared-ingress
|
||||
annotations:
|
||||
nginx.ingress.kubernetes.io/proxy-buffer-size: 16k
|
||||
- host: realm-two.ci.example.com
|
||||
realm: ci-realm-two
|
||||
tlsSecretName: tls-ci-realm-two-ingress
|
||||
clusterIssuer: ci-selfsigned
|
||||
|
||||
ingress:
|
||||
# Left ON although no ingress controller answers in CI: the point is that
|
||||
# both objects are accepted and that the Service port they name BY NAME
|
||||
|
||||
+1
-1
@@ -43,7 +43,7 @@ type: application
|
||||
# procedure lives in NOTES.txt and on the wiki page instead of in a
|
||||
# packageRules entry. Read the "MINOR UPGRADES" section of NOTES.txt before
|
||||
# merging an app-version PR that changes the minor.
|
||||
version: 1.2.0+up26.8.0
|
||||
version: 1.3.0+up26.8.0
|
||||
appVersion: "26.8.0"
|
||||
|
||||
maintainers:
|
||||
|
||||
@@ -18,6 +18,7 @@ see of it.
|
||||
{{- if not $adminHostnames -}}
|
||||
{{- $adminHostnames = list (.Values.ingress.admin | default dict).domain -}}
|
||||
{{- end -}}
|
||||
{{- $realmHosts := (fromYaml (include "keycloak.settings" .)).realmHosts | default list -}}
|
||||
{{- $deploy := printf "%s-%s-deployment" .Release.Name .Chart.Name -}}
|
||||
{{- $svc := printf "%s-%s-service" .Release.Name .Chart.Name -}}
|
||||
Keycloak {{ .Chart.AppVersion }} installed as release {{ .Release.Name }} in namespace {{ .Release.Namespace }}.
|
||||
@@ -45,16 +46,40 @@ ENDPOINTS - {{ if .Values.ingress.enabled }}INGRESS MODE (ingress.enabled: true,
|
||||
/health, /health/live, /health/ready, /health/started{{ if $settings.metrics.enabled }} and /metrics{{ end }}
|
||||
WITHOUT authentication. It has a ClusterIP Service
|
||||
({{ .Release.Name }}-{{ .Chart.Name }}-management-service) for in-cluster scraping only.
|
||||
{{ if $realmHosts }}
|
||||
REALM HOSTNAMES - {{ len $realmHosts }} further name(s), each landing in ONE realm
|
||||
|
||||
The object names carry the LIST INDEX and not the realm, so this is the only
|
||||
place that says which is which. Reordering keycloak.realmHosts therefore
|
||||
renames these objects and recreates them.
|
||||
{{ range $i, $entry := $realmHosts }}
|
||||
https://{{ $entry.host }}
|
||||
realm {{ $entry.realm }} (bare / -> /realms/{{ $entry.realm }}/account/)
|
||||
object {{ $.Release.Name }}-{{ $.Chart.Name }}-realm-{{ if $httpRoute.enabled }}httproute{{ else }}ingress{{ end }}{{ if $i }}-{{ $i }}{{ end }}
|
||||
{{- if not $httpRoute.enabled }}
|
||||
cert {{ $entry.tlsSecretName }}{{ if $entry.clusterIssuer }}, requested by THIS object ({{ $entry.clusterIssuer }}){{ else }}, NOT requested by this object - it must already exist or be issued elsewhere{{ end }}
|
||||
{{- end }}
|
||||
{{ end }}
|
||||
{{- /*
|
||||
No "and nothing publishes them" branch here, deliberately: a filled
|
||||
keycloak.realmHosts with both modes off ABORTS the render in
|
||||
keycloak.ingress.yaml, so this block can only ever be reached with a mode on.
|
||||
A branch for an impossible state reads like a state somebody has seen.
|
||||
*/ -}}
|
||||
{{- end }}
|
||||
{{ if $httpRoute.enabled }}
|
||||
GATEWAY MODE - WHAT THIS CHART DOES NOT RENDER, AND WHAT YOU MUST SET YOURSELF
|
||||
|
||||
This release renders {{ if $adminHostname }}TWO HTTPRoutes{{ else }}ONE HTTPRoute{{ end }} and NOTHING else of the routing path -
|
||||
This release renders {{ add (ternary 2 1 $adminHostname) (len $realmHosts) }} HTTPRoute(s) and NOTHING else of the routing path -
|
||||
no Gateway, no class, no Certificate, no policy object.
|
||||
|
||||
{{ .Release.Name }}-{{ .Chart.Name }}-httproute {{ join ", " $hostnames }}
|
||||
{{- if $adminHostname }}
|
||||
{{ .Release.Name }}-{{ .Chart.Name }}-admin-httproute {{ join ", " $adminHostnames }}
|
||||
{{- end }}
|
||||
{{- range $i, $entry := $realmHosts }}
|
||||
{{ $.Release.Name }}-{{ $.Chart.Name }}-realm-httproute{{ if $i }}-{{ $i }}{{ end }} {{ $entry.host }} (realm {{ $entry.realm }})
|
||||
{{- end }}
|
||||
|
||||
Three things therefore belong to the cluster, and if any is missing the
|
||||
hostnames above simply do not answer:
|
||||
@@ -69,6 +94,11 @@ GATEWAY MODE - WHAT THIS CHART DOES NOT RENDER, AND WHAT YOU MUST SET YOURSELF
|
||||
{{- range ($httpRouteAdmin.parentRefs | default $httpRoute.parentRefs) }}
|
||||
{{ .name }}{{ if .namespace }} in namespace {{ .namespace }}{{ end }}{{ if .sectionName }}, listener {{ .sectionName }}{{ end }} (admin)
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- range $i, $entry := $realmHosts }}
|
||||
{{- range ($entry.parentRefs | default $httpRoute.parentRefs) }}
|
||||
{{ .name }}{{ if .namespace }} in namespace {{ .namespace }}{{ end }}{{ if .sectionName }}, listener {{ .sectionName }}{{ end }} (realm {{ $entry.realm }})
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
must exist AND their listeners must allow routes from this namespace
|
||||
({{ .Release.Namespace }}) in allowedRoutes. A rejected attachment is visible only
|
||||
@@ -84,6 +114,12 @@ GATEWAY MODE - WHAT THIS CHART DOES NOT RENDER, AND WHAT YOU MUST SET YOURSELF
|
||||
chart renders no Certificate here, so there is no
|
||||
tls-{{ .Release.Name }}-{{ .Chart.Name }}-ingress secret and nothing for cert-manager to do
|
||||
for this release.
|
||||
{{- if $realmHosts }}
|
||||
The realm hostnames above are part of that: their tlsSecretName values
|
||||
are not read in this mode either. Where several of them share a DNS zone,
|
||||
ONE wildcard listener for that zone replaces one certificate per name -
|
||||
which is the one thing this mode makes cheaper here rather than dearer.
|
||||
{{- end }}
|
||||
|
||||
AND THE ONE YOU WILL NOT NOTICE UNTIL A LOGIN FAILS: THE PROXY BUFFERS.
|
||||
|
||||
@@ -109,6 +145,28 @@ GATEWAY MODE - WHAT THIS CHART DOES NOT RENDER, AND WHAT YOU MUST SET YOURSELF
|
||||
the Gateway API has no annotation channel for controller behaviour. An IP
|
||||
allow-list for the admin host is the other thing that has to move - and read
|
||||
the first trap below before relying on it in either mode.
|
||||
{{ if $realmHosts }}
|
||||
AND THE SAME LOSS ONCE MORE, ON THE REALM HOSTNAMES - where it is likelier to
|
||||
be met rather than merely possible.
|
||||
|
||||
Each entry of keycloak.realmHosts carries its own annotations block in Ingress
|
||||
mode, and in this mode every one of them is gone: {{ len $realmHosts }} hostname(s) here, none
|
||||
of which can configure a proxy buffer through this chart any more. What the
|
||||
entries CAN still express is the routing - the redirect into the realm is a
|
||||
rule of the route and survives the mode change intact.
|
||||
|
||||
Why these hostnames are the awkward ones: they exist to put somebody in an
|
||||
ACCOUNT CONSOLE. That is a logged-in session from the first request after the
|
||||
redirect, so the large Authorization and Set-Cookie headers are the normal
|
||||
traffic on these names rather than an edge case the way they are on a login
|
||||
host that mostly serves anonymous requests. A buffer sized for the default
|
||||
therefore fails here FIRST, and it fails as a 502 from the Gateway on a
|
||||
hostname whose Ingress never had a chart value to point at - so there is
|
||||
nothing to grep for and nothing to un-set.
|
||||
|
||||
Set the buffer equivalent for these hostnames too, in the Gateway
|
||||
implementation's own object, and set it before the names resolve there.
|
||||
{{- end }}
|
||||
|
||||
THE ADMIN HOSTNAME'S "INTERNAL CLASS" MOVES TO THE PARENT. What
|
||||
ingress.admin.className does in the other mode, httpRoute.admin.parentRefs
|
||||
|
||||
@@ -184,7 +184,8 @@ Usage: {{ $settings := fromYaml (include "keycloak.settings" .) }}
|
||||
"features" (list)
|
||||
"cache" (dict "clusterName" nil)
|
||||
"ports" (dict "http" 8080 "management" 9000 "jgroups" 7800)
|
||||
"podDisruptionBudget" (dict "enabled" true "minAvailable" 1) -}}
|
||||
"podDisruptionBudget" (dict "enabled" true "minAvailable" 1)
|
||||
"realmHosts" (list) -}}
|
||||
{{- include "keycloak.defaultedDict" (dict "defaults" $defaults "given" $given "path" "keycloak") -}}
|
||||
{{- end -}}
|
||||
|
||||
@@ -342,6 +343,104 @@ true
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
THE REALM HOSTNAMES (Gitea #122) - validated here once, rendered twice.
|
||||
|
||||
WHAT THEY ARE. Keycloak serves every realm from one deployment, and a realm's
|
||||
account console lives at /realms/<realm>/account/. A realm therefore gets a
|
||||
hostname of its own by publishing THIS release's Service under that name and
|
||||
sending the bare / to that path. The list is keycloak.realmHosts, it defaults
|
||||
to empty, and an empty list renders nothing in either mode - the feature is
|
||||
additive and the default render is byte-identical without it.
|
||||
|
||||
WHY THE VALIDATION IS SHARED AND NOT PER TEMPLATE. The same list feeds the
|
||||
Ingress and the HTTPRoute template, so a check written in one of them is a check
|
||||
that does not exist in the other mode - and the failure these checks are about
|
||||
is a hostname that resolves, answers, and serves the wrong thing or nothing at
|
||||
all. None of it is loud: an entry without a realm would render an Ingress whose
|
||||
app-root annotation reads "/realms//account/", an entry without a host would
|
||||
render an object with an empty host that ingress-nginx treats as a CATCH-ALL for
|
||||
the whole controller, and an entry without a tlsSecretName would render a TLS
|
||||
block with no secret, which serves the controller's default certificate and
|
||||
fails in the browser only.
|
||||
|
||||
THE MODE DECIDES WHICH FIELDS ARE LOAD-BEARING, which is why it is a parameter
|
||||
rather than read from the values here: tlsSecretName and annotations are Ingress
|
||||
fields with no counterpart in the Gateway API (see keycloak.httproute.yaml), and
|
||||
parentRefs is the other way round. A field demanded in a mode that ignores it is
|
||||
the defect guideline 12 exists to remove, so each is required only where it is
|
||||
read.
|
||||
|
||||
Input: dict "root" <the root context> "mode" "ingress"|"gateway"
|
||||
Output: YAML of dict "entries" <the entries, as given, after validation>
|
||||
*/}}
|
||||
{{- define "keycloak.realmHosts" -}}
|
||||
{{- $root := .root -}}
|
||||
{{- $mode := .mode -}}
|
||||
{{- $entries := (fromYaml (include "keycloak.settings" $root)).realmHosts | default list -}}
|
||||
{{- if not (kindIs "slice" $entries) -}}
|
||||
{{- fail (printf "keycloak.realmHosts must be a list or null, got %s (%v). One entry per realm hostname, each a mapping of host and realm plus the fields its routing mode reads - see values.yaml." (kindOf $entries) $entries) -}}
|
||||
{{- end -}}
|
||||
{{- $ingress := $root.Values.ingress | default dict -}}
|
||||
{{- $ingressAdmin := $ingress.admin | default dict -}}
|
||||
{{- $httpRoute := $root.Values.httpRoute | default dict -}}
|
||||
{{- /*
|
||||
THE HOSTNAMES THIS RELEASE ALREADY CLAIMS, collected so that a realm entry
|
||||
cannot claim one of them a second time. Both modes punish it, differently and
|
||||
neither of them clearly: under ingress-nginx the admission webhook rejects the
|
||||
SECOND Ingress on the same host and path - which under Fleet leaves the bundle
|
||||
in ErrApplied until forceSyncGeneration is raised - while two HTTPRoutes on one
|
||||
listener and one hostname are MERGED by the implementation, so the duplicate is
|
||||
accepted and which of the two identical rules serves a request is decided by
|
||||
object age. The first is a deployment that stops, the second is a coin toss that
|
||||
looks healthy.
|
||||
*/ -}}
|
||||
{{- $claimed := dict -}}
|
||||
{{- if $ingress.domain -}}
|
||||
{{- $_ := set $claimed (toString $ingress.domain) "ingress.domain, this release's public hostname" -}}
|
||||
{{- end -}}
|
||||
{{- if and (eq (include "keycloak.adminHostnameEnabled" $root) "true") $ingressAdmin.domain -}}
|
||||
{{- $_ := set $claimed (toString $ingressAdmin.domain) "ingress.admin.domain, the administration console's hostname" -}}
|
||||
{{- end -}}
|
||||
{{- if eq $mode "gateway" -}}
|
||||
{{- range ($httpRoute.hostnames | default list) -}}
|
||||
{{- $_ := set $claimed (toString .) "httpRoute.hostnames, the public route's explicit hostname list" -}}
|
||||
{{- end -}}
|
||||
{{- range (($httpRoute.admin | default dict).hostnames | default list) -}}
|
||||
{{- $_ := set $claimed (toString .) "httpRoute.admin.hostnames, the admin route's explicit hostname list" -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- $seen := dict -}}
|
||||
{{- range $i, $entry := $entries -}}
|
||||
{{- $at := printf "keycloak.realmHosts[%d]" $i -}}
|
||||
{{- if not (kindIs "map" $entry) -}}
|
||||
{{- fail (printf "%s must be a mapping with at least host and realm, got %s (%v). The list is a list of entries, not of hostnames - the realm cannot be derived from the hostname, and this installation is the proof: id.klein.pub serves the realm klein-family." $at (kindOf $entry) $entry) -}}
|
||||
{{- end -}}
|
||||
{{- $host := toString ($entry.host | default "") -}}
|
||||
{{- $realm := toString ($entry.realm | default "") -}}
|
||||
{{- if not $host -}}
|
||||
{{- fail (printf "%s has no host (its realm is %q). An Ingress rule or an HTTPRoute without a hostname is not an error in either API: ingress-nginx treats the empty host as a CATCH-ALL and the object then answers every name the controller serves that nothing else claims, while an HTTPRoute with an empty hostnames list attaches to every hostname of its listener. Both publish this realm's account console across hostnames nobody chose. Set %s.host." $at $realm $at) -}}
|
||||
{{- end -}}
|
||||
{{- if not $realm -}}
|
||||
{{- fail (printf "%s (host %s) has no realm. The realm is the whole point of the entry - it is what the bare / redirects to - and without it the redirect target would read /realms//account/, which Keycloak answers 404 while the hostname, the certificate and the route are all healthy. Set %s.realm, and read it off the realm in Keycloak rather than off the hostname: the two need not match, and in this installation they do not." $at $host $at) -}}
|
||||
{{- end -}}
|
||||
{{- if contains "/" $realm -}}
|
||||
{{- fail (printf "%s has realm %q, which contains a slash. The value is interpolated into the URL path /realms/<realm>/account/, so a slash does not escape it - it silently retargets the redirect at a different path. Use the realm's name as Keycloak shows it." $at $realm) -}}
|
||||
{{- end -}}
|
||||
{{- if hasKey $seen $host -}}
|
||||
{{- fail (printf "%s repeats the host %s, already used by keycloak.realmHosts[%v]. Two entries on one hostname render two objects with the same hostname and the same path: in Ingress mode the admission webhook rejects the second, in Gateway mode both are accepted and merged and the redirect a client gets - so the realm it lands in - depends on which object is older. Give each realm its own hostname, or drop the duplicate entry." $at $host (index $seen $host)) -}}
|
||||
{{- end -}}
|
||||
{{- $_ := set $seen $host $i -}}
|
||||
{{- if hasKey $claimed $host -}}
|
||||
{{- fail (printf "%s claims the host %s, which this release already publishes through %s. One hostname cannot carry both: a realm entry sends the bare / to /realms/%s/account/, and the hostname it collides with serves the login endpoints or the administration console at / instead. Pick a hostname of its own for the realm - that is what the feature is for - or publish the realm through the existing host without this entry." $at $host (index $claimed $host) $realm) -}}
|
||||
{{- end -}}
|
||||
{{- if and (eq $mode "ingress") (not (toString ($entry.tlsSecretName | default ""))) -}}
|
||||
{{- fail (printf "%s (host %s) has no tlsSecretName, and in Ingress mode it is required. Unlike this chart's own two Ingresses the realm objects do NOT get the conventional tls-<release>-keycloak-ingress name: these hostnames live on certificates that already exist - a wildcard covering several of them, or one per name - and letting the chart invent a secret name per entry would ask cert-manager for a SECOND certificate for a name that is already covered, which costs an ACME order per host and buys nothing. So the name is operator data and has to be given. If the certificate is to be issued for this entry, name a secret of its own here and set %s.clusterIssuer as well." $at $host $at) -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- toYaml (dict "entries" $entries) -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
The effective security contexts, probes and resources (issue #99).
|
||||
|
||||
|
||||
@@ -46,16 +46,54 @@ policy. So the Gateway branch of NOTES.txt NAMES it instead of rebuilding it
|
||||
(decision 2 of #114). Whoever migrates this host sets the equivalent next to
|
||||
the Gateway, in that implementation's own object.
|
||||
|
||||
A NEIGHBOUR ON THE SAME ZONE, WHICH THIS CHART DOES NOT OWN
|
||||
THE REALM ROUTES, AND WHY THERE IS ONE OBJECT PER ENTRY (Gitea #122)
|
||||
|
||||
The same namespace carries four more Ingress objects - keycloak-realm-*-ingress
|
||||
with nginx.ingress.kubernetes.io/app-root - from the Helm release
|
||||
jk-cluster-config-fleet-cd-keycloak-realm-ingress, i.e. from ANOTHER
|
||||
repository. They point at THIS release's Service under four further hostnames
|
||||
(bk.id..., id.mail..., muth-event.id..., id.klein.pub). They are not rendered
|
||||
here and must not be: this chart renders exactly the two routes its own two
|
||||
Ingresses cover. Migrating the host is therefore not finished by switching this
|
||||
chart - see the wiki page for the list.
|
||||
keycloak.realmHosts publishes further hostnames onto the SAME Service, each
|
||||
sending the bare / to one realm's account console. Empty by default, so this
|
||||
block renders nothing unless an installation fills it.
|
||||
|
||||
ONE OBJECT PER ENTRY, never one object with several hostnames. spec.hostnames
|
||||
applies to the WHOLE object and a rule's matches are path-based, so the realm
|
||||
could not be told apart inside a shared route: all entries would match
|
||||
`Exact: /` and whichever rule the implementation picked would send every
|
||||
hostname to the same realm. The count is therefore data-driven, which is also
|
||||
why the object names are INDEX-based (style guide §4, as homeassistant does it)
|
||||
- a realm name is not a dependable object-name fragment and a sanitised one can
|
||||
collide. The consequence, written down rather than discovered: REORDERING THE
|
||||
LIST RENAMES ROUTES, so they are recreated rather than edited. A route holds no
|
||||
state and no certificate, which is what makes that acceptable; the TLS secret
|
||||
the Ingress mode references is not the chart's and survives untouched either
|
||||
way.
|
||||
|
||||
TWO RULES PER OBJECT, AND THE REDIRECT IS AN EXACT MATCH. The Ingress
|
||||
counterpart is nginx.ingress.kubernetes.io/app-root, which ingress-nginx renders
|
||||
as `if ($uri = /) { return 302 $scheme://$http_host<app-root>; }`
|
||||
(rootfs/etc/nginx/template/nginx.tmpl:1000, controller-v1.14.5) - a 302 on
|
||||
exactly /, and everything else proxied unchanged. So: an Exact match on / with a
|
||||
RequestRedirect to /realms/<realm>/account/, plus the ordinary PathPrefix rule
|
||||
on the Service. The Exact rule wins on / by the specification's precedence
|
||||
("Exact" path match ahead of prefix matches) INDEPENDENTLY of its position in
|
||||
the list, so the order below is a rendering convention and not a promise anyone
|
||||
has to rely on.
|
||||
|
||||
statusCode IS WRITTEN OUT, at the value the field already defaults to. 301 and
|
||||
302 are both Support: Core here, so the portability trap of #119 - an
|
||||
implementation declining an Extended status code sets Accepted: False on the
|
||||
WHOLE route, taking its other hostnames down with it - does not apply. The
|
||||
reason to write it anyway: ingress-nginx emits 302 and a value that is merely
|
||||
accidentally right documents nothing.
|
||||
|
||||
WHAT THE REALM ENTRIES LOSE IN THIS MODE is the per-entry annotations block,
|
||||
and for three of the four hostnames this installation runs today that means the
|
||||
proxy buffers - see the paragraph above, which is about the same loss on this
|
||||
chart's own two objects. It is worse here than there in one respect only: these
|
||||
hostnames serve ACCOUNT CONSOLE sessions, so the large-header case is the
|
||||
normal case rather than the edge, and the 502 arrives from the proxy on a
|
||||
hostname whose Ingress never had a chart value to point at.
|
||||
|
||||
ingress.clusterIssuer and the entries' tlsSecretName are not read in this mode:
|
||||
the certificates for these hostnames sit on the Gateway's listeners, where a
|
||||
wildcard listener covers whole zones at once.
|
||||
*/}}
|
||||
{{- /*
|
||||
Null-safe like every other block a template here dereferences (#99): an
|
||||
@@ -74,6 +112,7 @@ YAML null, and .Values.httpRoute.enabled would abort with a nil pointer.
|
||||
{{- 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 -}}
|
||||
{{- $realmHosts := (fromYaml (include "keycloak.realmHosts" (dict "root" . "mode" "gateway"))).entries -}}
|
||||
{{- /*
|
||||
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
|
||||
@@ -161,4 +200,74 @@ spec:
|
||||
- name: {{ .Release.Name }}-{{ .Chart.Name }}-service
|
||||
port: {{ $settings.ports.http | int64 }}
|
||||
{{- end }}
|
||||
{{- range $i, $entry := $realmHosts }}
|
||||
---
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
kind: HTTPRoute
|
||||
metadata:
|
||||
{{- /*
|
||||
INDEX-BASED, like homeassistant's per-host routes and for the same reason
|
||||
(style guide §4): the count is data-driven and a realm name is not a
|
||||
dependable object-name fragment - the 63-character label limit plus the
|
||||
release prefix, and a sanitised name can collide with another entry's. The
|
||||
price is that reordering keycloak.realmHosts RENAMES these routes, so they are
|
||||
recreated rather than edited. Which hostname and which realm each index
|
||||
carries is printed by NOTES.txt, because the name does not say.
|
||||
*/}}
|
||||
name: "{{ $.Release.Name }}-{{ $.Chart.Name }}-realm-httproute{{ if $i }}-{{ $i }}{{ end }}"
|
||||
labels:
|
||||
app: {{ $.Release.Name }}-{{ $.Chart.Name }}-deployment
|
||||
spec:
|
||||
{{- /*
|
||||
Its own parent, falling back to the chart-global one - the same shape as
|
||||
httpRoute.admin.parentRefs. It is not decoration here: these hostnames sit in
|
||||
several DNS zones, so a Gateway that terminates them needs either a wildcard
|
||||
listener per zone or a named listener per host, and a chart-global parentRef
|
||||
pinning one sectionName would attach every realm route to one certificate's
|
||||
listener. A route whose listener does not match its hostname is not an error
|
||||
either - it is simply not accepted, and the hostname does not answer.
|
||||
*/}}
|
||||
parentRefs:
|
||||
{{- toYaml ($entry.parentRefs | default $parentRefs) | nindent 4 }}
|
||||
hostnames:
|
||||
- {{ $entry.host | quote }}
|
||||
rules:
|
||||
{{- /*
|
||||
RULE ONE, the counterpart of nginx.ingress.kubernetes.io/app-root. Exact
|
||||
on / so that only the bare entry point is redirected and every other path
|
||||
- the realm's own endpoints, its assets, the token endpoints - continues
|
||||
to be proxied by rule two. Exact outranks every prefix match in the
|
||||
specification's precedence, so this rule wins on / wherever it is emitted.
|
||||
*/}}
|
||||
- matches:
|
||||
- path:
|
||||
type: Exact
|
||||
value: /
|
||||
filters:
|
||||
- type: RequestRedirect
|
||||
requestRedirect:
|
||||
path:
|
||||
type: ReplaceFullPath
|
||||
replaceFullPath: {{ printf "/realms/%s/account/" $entry.realm | quote }}
|
||||
{{- /*
|
||||
Written out at the field's own default. 301 and 302 are both
|
||||
Support: Core, so the trap of #119 - an implementation declining an
|
||||
Extended code sets Accepted: False on the WHOLE route - is not in
|
||||
play; ingress-nginx emits 302 for app-root, and a value that is only
|
||||
accidentally right documents nothing.
|
||||
*/}}
|
||||
statusCode: 302
|
||||
{{- /*
|
||||
RULE TWO: the same Service, the same port and the same prefix as the
|
||||
public route. These hostnames are not a second application - they are the
|
||||
same Keycloak under a name that lands in one realm.
|
||||
*/}}
|
||||
- matches:
|
||||
- path:
|
||||
type: PathPrefix
|
||||
value: /
|
||||
backendRefs:
|
||||
- name: {{ $.Release.Name }}-{{ $.Chart.Name }}-service
|
||||
port: {{ $settings.ports.http | int64 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -46,6 +46,41 @@ exclusion exists to prevent. The default render is unchanged (ingress.enabled
|
||||
defaults to true); what changed is a combination that could not be coherent.
|
||||
*/ -}}
|
||||
{{- $adminEnabled := eq (include "keycloak.adminHostnameEnabled" .) "true" }}
|
||||
{{- /*
|
||||
THE REALM HOSTNAMES WITH NO MODE TO CARRY THEM (Gitea #122), checked here
|
||||
because this is the template that owns ingress.enabled and because the check
|
||||
has to run when NEITHER routing template emits anything. Both modes off is a
|
||||
legitimate configuration - the chart then publishes nothing and NOTES.txt says
|
||||
so - but a filled keycloak.realmHosts next to it is not: the list would be
|
||||
accepted, validated, and rendered into no object at all, which is the one
|
||||
outcome nobody can tell from a working install by looking at the release.
|
||||
Emitting nothing is only correct when nothing was asked for.
|
||||
*/ -}}
|
||||
{{- if and (not .Values.ingress.enabled) (not (.Values.httpRoute | default dict).enabled) }}
|
||||
{{- if (fromYaml (include "keycloak.settings" .)).realmHosts }}
|
||||
{{- fail "keycloak.realmHosts is set while BOTH routing modes are off (ingress.enabled: false and httpRoute.enabled: false). The entries name hostnames and realms and there is no object to put them in: no Ingress, no HTTPRoute, no DNS answer, and nothing in the rendered release that reveals the list was ignored. Switch on the mode these hostnames are meant to be served by, or remove the entries - both are coherent, this combination is not." }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- /*
|
||||
Validated and bound OUTSIDE the ingress.enabled block, because a variable
|
||||
declared inside an if-block does not exist after it and the realm objects below
|
||||
need the same list. The helper is only CALLED in Ingress mode: it demands a
|
||||
tlsSecretName per entry, which the Gateway mode neither reads nor should ask
|
||||
for.
|
||||
|
||||
AND NOT WHEN httpRoute.enabled IS ALSO TRUE, which looks redundant next to
|
||||
ingress.enabled and is not. That combination is the one keycloak.httproute.yaml
|
||||
aborts over, with a message about the two data paths; template execution order
|
||||
is not something a chart can rely on, so a realm check that fired here would
|
||||
sometimes answer "this entry needs a tlsSecretName" to a values file whose
|
||||
actual defect is that both modes are on. The specific message must not be able
|
||||
to hide the structural one. Measured while writing this: without the second
|
||||
condition, exactly that happened.
|
||||
*/ -}}
|
||||
{{- $realmHosts := list }}
|
||||
{{- if and .Values.ingress.enabled (not (.Values.httpRoute | default dict).enabled) }}
|
||||
{{- $realmHosts = (fromYaml (include "keycloak.realmHosts" (dict "root" . "mode" "ingress"))).entries }}
|
||||
{{- end }}
|
||||
{{- if .Values.ingress.enabled }}
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
@@ -119,3 +154,103 @@ spec:
|
||||
- {{ required "An admin Domain/Host must be provided (ingress.admin.domain) while ingress.admin.enabled is true" ((.Values.ingress.admin | default dict).domain) }}
|
||||
secretName: tls-{{ .Release.Name }}-{{ .Chart.Name }}-admin-ingress
|
||||
{{- end }}
|
||||
{{- range $i, $entry := $realmHosts }}
|
||||
---
|
||||
{{/*
|
||||
A REALM HOSTNAME (Gitea #122), one Ingress object per keycloak.realmHosts entry.
|
||||
|
||||
Keycloak serves every realm from this one deployment, and a realm's account
|
||||
console lives at /realms/<realm>/account/. An entry here gives a realm a
|
||||
hostname of its own: the same Service as the two objects above, under a further
|
||||
name, with the bare / sent to that realm's console by app-root. Gateway mode's
|
||||
counterpart is an Exact match plus a RequestRedirect filter - see
|
||||
keycloak.httproute.yaml.
|
||||
|
||||
THIS IS WHERE THE CHART DEVIATES FROM THE REPO'S INGRESS CONVENTION, and the
|
||||
deviation is the expensive half of the feature rather than an oversight. The
|
||||
convention (style guide §4) is one TLS secret per object named
|
||||
tls-<release>-<chart>-ingress, with the cert-manager annotation ALWAYS set.
|
||||
Neither holds here:
|
||||
|
||||
- THE SECRET NAME IS OPERATOR DATA, required per entry and never derived.
|
||||
These hostnames live in zones whose certificates already exist, and several
|
||||
of them typically share one: a wildcard for a zone covers every realm host
|
||||
under it. A chart-derived name per entry would ask cert-manager for a fresh
|
||||
certificate for a name a wildcard already covers - one ACME order per
|
||||
hostname, for nothing.
|
||||
- THE ISSUER IS OPTIONAL, and omitting it is the normal case rather than the
|
||||
exception. cert-manager keys a Certificate by the secret it writes, so two
|
||||
objects naming one secret and both carrying the annotation request the same
|
||||
certificate twice. Exactly one object per secret may carry it - which is the
|
||||
same deliberate omission the qBittorrent object in freesailarr makes, for
|
||||
the same reason. Where an entry's certificate is to be issued FOR it, it
|
||||
names a secret of its own and sets clusterIssuer; where the certificate
|
||||
comes from elsewhere (a wildcard, a Certificate in cluster-config, another
|
||||
release), clusterIssuer stays empty and this object only consumes the
|
||||
secret.
|
||||
|
||||
The entry's annotations are merged last and per entry, not chart-globally,
|
||||
because this is not a uniform set: of the four hostnames this installation runs
|
||||
today, three carry the proxy-buffer pair and one does not. A chart-global
|
||||
annotation block could not express that difference.
|
||||
*/}}
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
{{- /*
|
||||
INDEX-BASED, like homeassistant's per-host routes and for the same reason
|
||||
(style guide §4): the count is data-driven, and a realm name is not a
|
||||
dependable object-name fragment - the 63-character label limit plus the
|
||||
release prefix, and a sanitised name can collide with another entry's. The
|
||||
price, written down rather than discovered: reordering keycloak.realmHosts
|
||||
RENAMES these objects, so they are recreated rather than edited, and the
|
||||
hostname is briefly unserved while that happens. The TLS secret is not the
|
||||
chart's and survives it. NOTES.txt prints which index carries which hostname
|
||||
and realm, because the name does not say.
|
||||
*/}}
|
||||
name: "{{ $.Release.Name }}-{{ $.Chart.Name }}-realm-ingress{{ if $i }}-{{ $i }}{{ end }}"
|
||||
labels:
|
||||
app: {{ $.Release.Name }}-{{ $.Chart.Name }}-deployment
|
||||
annotations:
|
||||
{{- /*
|
||||
The counterpart of the Gateway mode's Exact-match RequestRedirect.
|
||||
ingress-nginx renders this annotation as
|
||||
if ($uri = /) { return 302 $scheme://$http_host<app-root>; }
|
||||
(rootfs/etc/nginx/template/nginx.tmpl:1000, controller-v1.14.5) - a 302 on
|
||||
exactly /, with every other path proxied unchanged.
|
||||
*/}}
|
||||
nginx.ingress.kubernetes.io/app-root: {{ printf "/realms/%s/account/" $entry.realm | quote }}
|
||||
{{- with $entry.clusterIssuer }}
|
||||
cert-manager.io/cluster-issuer: {{ . | quote }}
|
||||
{{- end }}
|
||||
{{- with $entry.annotations }}
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
{{- /*
|
||||
The entry's own class, falling back to ingress.className - the same shape as
|
||||
ingress.admin.className. A realm hostname can perfectly well belong to a zone
|
||||
served by a different controller than the login host.
|
||||
*/}}
|
||||
ingressClassName: {{ $entry.className | default $.Values.ingress.className }}
|
||||
rules:
|
||||
- host: {{ $entry.host | quote }}
|
||||
http:
|
||||
paths:
|
||||
{{- /*
|
||||
The SAME backend as the two objects above, by port NAME as they do.
|
||||
Everything but the bare / is proxied straight through; app-root
|
||||
handles /.
|
||||
*/}}
|
||||
- backend:
|
||||
service:
|
||||
name: {{ $.Release.Name }}-{{ $.Chart.Name }}-service
|
||||
port:
|
||||
name: http
|
||||
path: /
|
||||
pathType: Prefix
|
||||
tls:
|
||||
- hosts:
|
||||
- {{ $entry.host | quote }}
|
||||
secretName: {{ $entry.tlsSecretName | quote }}
|
||||
{{- end }}
|
||||
@@ -151,6 +151,48 @@
|
||||
"extraVolumeMounts": {
|
||||
"description": "Additional volume mounts, rendered verbatim and TWICE - on the build init container as well as on the serving container (Gitea #88). That is deliberate: a login theme is read at runtime, but an SPI provider JAR must be present while `kc.sh build` writes the provider registry, or it is never registered and nothing reports it. Items loosely typed for the same reason as extraVolumes. Never mount on /opt/keycloak/themes itself - it would hide the image's base, keycloak and keycloak.v2 themes; use a subpath /opt/keycloak/themes/<name>.",
|
||||
"type": ["array", "null"]
|
||||
},
|
||||
"realmHosts": {
|
||||
"description": "One hostname per realm (Gitea #122), rendered in BOTH routing modes: one Ingress per entry with nginx.ingress.kubernetes.io/app-root, or one HTTPRoute per entry whose Exact match on / carries a RequestRedirect filter to /realms/<realm>/account/. Empty by default and then renders nothing at all. One object per entry, named by list INDEX - so reordering the list renames and recreates them. host and realm are required and the realm is NOT derived from the hostname; the render also aborts on a repeated host and on a host this release already publishes, because one hostname cannot serve two different things at /. tlsSecretName is required in Ingress mode and never derived, and clusterIssuer is optional there - the documented deviation from the one-secret/always-annotate convention, so that a certificate already covering these zones is consumed rather than requested a second time. See values.yaml for the full reasoning and for the order a takeover of existing hostnames has to run in.",
|
||||
"type": ["array", "null"],
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["host", "realm"],
|
||||
"properties": {
|
||||
"host": {
|
||||
"description": "The hostname serving this realm. Must differ from every other entry's and from the hostnames this release already publishes.",
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"realm": {
|
||||
"description": "The realm as Keycloak names it, interpolated into the redirect target /realms/<realm>/account/. Not derivable from the hostname - the two need not correspond and in the installation this was written for they do not. A slash in it aborts the render, because it would retarget the redirect instead of being escaped.",
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"tlsSecretName": {
|
||||
"description": "The TLS secret this hostname's certificate lives in. REQUIRED IN INGRESS MODE and deliberately not derived from release and chart name: these zones already have certificates, often one wildcard for several entries, and a derived name would mean one further ACME order per hostname for a name that is already covered. Not read in Gateway mode - there the certificate sits on the Gateway's listener.",
|
||||
"type": ["string", "null"]
|
||||
},
|
||||
"clusterIssuer": {
|
||||
"description": "Optional cert-manager issuer for THIS object. Empty or absent renders no cert-manager annotation, which is the normal case: cert-manager keys a Certificate by the secret it writes, so of several objects naming one secret exactly one may carry the annotation. Same deliberate omission as the qBittorrent object in freesailarr. Not read in Gateway mode.",
|
||||
"type": ["string", "null"]
|
||||
},
|
||||
"className": {
|
||||
"description": "Optional ingress class for this object, falling back to ingress.className - the same shape as ingress.admin.className, because a realm hostname may belong to a zone served by another controller. Not read in Gateway mode, where the class is a property of the Gateway.",
|
||||
"type": ["string", "null"]
|
||||
},
|
||||
"annotations": {
|
||||
"description": "Extra annotations for THIS entry's Ingress, merged after the app-root and cert-manager annotations. Per entry rather than chart-global on purpose: the set is not uniform across hostnames - of the four this was written for, three carry the proxy-buffer pair and one does not. Free-form keys by nature, so this object stays open. INERT in Gateway mode, and for an account console that loses the proxy buffers in the case where large headers are the norm rather than the edge.",
|
||||
"type": "object"
|
||||
},
|
||||
"parentRefs": {
|
||||
"description": "Optional Gateway(s) for this entry's HTTPRoute, falling back to httpRoute.parentRefs - the counterpart of className above, because \"which data path\" is a property of the Gateway in this API. Not decoration here: realm hostnames sit in several DNS zones, so a chart-global parentRef pinning one sectionName would attach every realm route to one certificate's listener, and a route whose listener does not match its hostname is not accepted and simply does not answer. Not read in Ingress mode.",
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/definitions/parentRef" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -614,6 +614,120 @@ keycloak:
|
||||
extraVolumes: []
|
||||
extraVolumeMounts: []
|
||||
|
||||
# ONE HOSTNAME PER REALM (Gitea #122). Empty by default, and an empty list
|
||||
# renders NOTHING in either mode - the default render is byte-identical to a
|
||||
# release without this feature.
|
||||
#
|
||||
# WHAT IT IS FOR. Keycloak serves every realm from this one deployment, and a
|
||||
# realm's account console lives at /realms/<realm>/account/. An entry here
|
||||
# gives one realm a hostname of its own: this release's Service published
|
||||
# under a further name, with the bare / sent to that realm's console and
|
||||
# everything else proxied through unchanged. It renders in BOTH routing modes
|
||||
# - one Ingress per entry with nginx.ingress.kubernetes.io/app-root, or one
|
||||
# HTTPRoute per entry whose Exact match on / carries a RequestRedirect filter.
|
||||
#
|
||||
# ONE OBJECT PER ENTRY, never one object with several hostnames: an
|
||||
# HTTPRoute's hostnames apply to the whole object and a rule's matches are
|
||||
# path-based, so inside a shared route every entry would match `Exact: /` and
|
||||
# the realm could not be told apart. The objects are named by LIST INDEX
|
||||
# (<release>-keycloak-realm-ingress, -realm-ingress-1, ... and the same for
|
||||
# -realm-httproute), which means REORDERING THIS LIST RENAMES THEM and they
|
||||
# are recreated rather than edited. NOTES.txt prints the index-to-hostname
|
||||
# mapping, because the object name does not carry it.
|
||||
#
|
||||
# THE REALM IS NOT DERIVABLE FROM THE HOSTNAME and the chart does not try:
|
||||
# both are given per entry. In the installation this feature was written for,
|
||||
# one of the four hostnames serves a realm whose name shares nothing with it.
|
||||
#
|
||||
# Fields, with the mode that reads each one:
|
||||
#
|
||||
# host (both) REQUIRED. The hostname. Must not repeat
|
||||
# another entry's, and must not be a hostname
|
||||
# this release already publishes
|
||||
# (ingress.domain, ingress.admin.domain, an
|
||||
# httpRoute hostname) - the render aborts on
|
||||
# either, because one hostname cannot serve two
|
||||
# different things at /.
|
||||
# realm (both) REQUIRED. The realm as Keycloak names it. It
|
||||
# is interpolated into /realms/<realm>/account/.
|
||||
# tlsSecretName (Ingress) REQUIRED in Ingress mode, see below.
|
||||
# clusterIssuer (Ingress) OPTIONAL, see below. Empty or absent = no
|
||||
# cert-manager annotation on this object.
|
||||
# annotations (Ingress) OPTIONAL, per entry. Merged last.
|
||||
# className (Ingress) OPTIONAL. Falls back to ingress.className.
|
||||
# parentRefs (Gateway) OPTIONAL. Falls back to httpRoute.parentRefs.
|
||||
#
|
||||
# realmHosts:
|
||||
# - host: realm-one.id.example.com
|
||||
# realm: realm-one
|
||||
# tlsSecretName: tls-id-wildcard-ingress # a certificate that exists
|
||||
# clusterIssuer: "" # issued elsewhere, see below
|
||||
# annotations:
|
||||
# nginx.ingress.kubernetes.io/proxy-buffer-size: 16k
|
||||
# nginx.ingress.kubernetes.io/proxy-buffers-number: "4"
|
||||
# - host: id.other.example.com
|
||||
# realm: realm-two
|
||||
# tlsSecretName: tls-id-other-ingress
|
||||
# clusterIssuer: letsencrypt-issuer # issued for THIS object
|
||||
#
|
||||
# (Neutral examples on purpose. Realm names, hostnames and secret names are
|
||||
# operator data and are not in this chart - they belong in the values file of
|
||||
# the installation.)
|
||||
#
|
||||
# WHY THIS DEVIATES FROM THE REPO'S INGRESS CONVENTION, which is one TLS
|
||||
# secret named tls-<release>-<chart>-ingress and the cert-manager annotation
|
||||
# ALWAYS set (style guide §4). Neither holds for these objects, and both
|
||||
# deviations are deliberate:
|
||||
#
|
||||
# - tlsSecretName is REQUIRED AND NEVER DERIVED. These hostnames live in
|
||||
# zones whose certificates already exist, and several entries typically
|
||||
# share one - a wildcard for a zone covers every realm host under it. A
|
||||
# chart-derived name per entry would ask cert-manager for a fresh
|
||||
# certificate for a name a wildcard already covers: one ACME order per
|
||||
# hostname, for nothing. So the name is operator data, and an entry
|
||||
# without one aborts the render in Ingress mode rather than rendering a
|
||||
# TLS block that silently serves the controller's default certificate.
|
||||
# - clusterIssuer is OPTIONAL, and leaving it empty is the normal case.
|
||||
# cert-manager keys a Certificate by the secret it writes, so two objects
|
||||
# naming one secret and both carrying the annotation request the same
|
||||
# certificate twice. Exactly one object per secret may carry it - the same
|
||||
# deliberate omission the qBittorrent object in freesailarr makes, for the
|
||||
# same reason. Set it only where the certificate is to be issued FOR this
|
||||
# entry's own secret.
|
||||
#
|
||||
# ANNOTATIONS ARE PER ENTRY and not chart-global, because the set is not
|
||||
# uniform: of the four hostnames the installation this was written for runs
|
||||
# today, three carry the proxy-buffer pair and one does not. One global block
|
||||
# could not express that difference. In GATEWAY MODE this field is inert, like
|
||||
# ingress.annotations - and for these hostnames that loss bites harder than
|
||||
# for the chart's own two, because an account console session is exactly the
|
||||
# large-header case: see the httpRoute block below and NOTES.txt.
|
||||
#
|
||||
# TAKING OVER HOSTNAMES THAT ANOTHER RELEASE SERVES TODAY - THE ORDER MATTERS,
|
||||
# AND IT IS NOT REVERSIBLE WITHIN A DEPLOY. These objects may already exist in
|
||||
# the cluster, created by a different repository and owned by a different Helm
|
||||
# release. Filling this list then does not edit them, it adds a SECOND object
|
||||
# for the same hostname and path, and the two orders are not equally bad:
|
||||
#
|
||||
# 1. Remove the objects in the owning repository FIRST and let that removal
|
||||
# reach the cluster.
|
||||
# 2. THEN install this chart with the list filled.
|
||||
#
|
||||
# The window between the two is a short unreachability of those hostnames.
|
||||
# The other order, or both at once, is worse: ingress-nginx's admission
|
||||
# webhook rejects the second claim on a host-and-path pair, and under Fleet
|
||||
# that leaves the bundle in ErrApplied until forceSyncGeneration is raised -
|
||||
# measured, and written down in ytdlp-web-player's collision fail. In Gateway
|
||||
# mode the webhook never fires (it compares Ingresses with Ingresses), and the
|
||||
# duplicate is accepted instead: two merged routes on one hostname, with
|
||||
# object age deciding which redirect a client gets.
|
||||
#
|
||||
# AND ONE QUESTION THIS CHART CANNOT ANSWER, UNCHECKED AS OF THIS WRITING:
|
||||
# whether anything in the owning repository depends on those objects BY NAME -
|
||||
# a Fleet bundle, a monitor, an objectset reference. Clear that before step 1,
|
||||
# not after.
|
||||
realmHosts: []
|
||||
|
||||
ingress:
|
||||
# The PUBLIC ingress - the login endpoint, the token endpoints, the account
|
||||
# console. On by default, because an identity provider nothing can reach is
|
||||
|
||||
Reference in new issue
Block a user