From 2591e91e253a47d76347eb913ebcce345f72970b Mon Sep 17 00:00:00 2001 From: Justin Klein Date: Mon, 12 Oct 2026 21:37:24 +0000 Subject: [PATCH] ytdlp-web-player: the Gateway API as an alternative to the Ingress (#114) The chart that carried the pattern this sweep was looking for: it read ingress.enabled OUTSIDE its Ingress template, in the host guard, and ingress.enabled is false by construction in Gateway mode - so the guard would have gone silent. Helper ytdlp-web-player.published now backs it. And the guard is needed MORE in that mode, which is the section worth reading. In Ingress mode a host claimed twice fails loudly: the ingress-nginx admission webhook rejects the second Ingress. In Gateway mode the webhook NEVER FIRES - it compares Ingress objects with Ingress objects, and an HTTPRoute is a different kind - so two valid objects serve one hostname over two data paths that are not equivalent: the proxy's Ingress through authentication, this chart's route straight to the application. Which one a client reaches depends on DNS. An authentication bypass that comes and goes with a record, reported by nothing. Also written down, because an operator migrating hosts will otherwise set the value and wonder why nothing moved: the oauth2-proxy SUBCHART HAS NO GATEWAY SUPPORT and this chart cannot give it one, so with the proxy owning the host the publishing object is the subchart's Ingress in BOTH modes. The chart's own route is for the proxy-disabled case or for the app on a host of its own. Corrected claim: this page said gating the Ingress off frees both domain and clusterIssuer. clusterIssuer still, domain only with both modes off. And the open question, recorded rather than guessed: the release runs in jlab-player with both Deployments Ready and NOT ONE of the cluster's 23 Ingress objects is in that namespace. What publishes it could not be determined - the workspace's read-only identity may not list httproutes or secrets - and the answer decides whether a mode switch here is a no-op, a change of publisher, or a bypass. --- ytdlp-web-player.md | 65 ++++++++++++++++++++++++++++++++++++++------- 1 file changed, 55 insertions(+), 10 deletions(-) diff --git a/ytdlp-web-player.md b/ytdlp-web-player.md index 7a85568..6684a1f 100644 --- a/ytdlp-web-player.md +++ b/ytdlp-web-player.md @@ -8,7 +8,7 @@ Deploys [ytdlp_web_player](https://github.com/matszwe02/ytdlp_web_player), a web | Versions | not listed on purpose — read the current pair off `ytdlp-web-player/Chart.yaml`, see [Chart index](Charts) | | Workloads | Deployment (1 replica); plus the oauth2-proxy subchart's own when enabled | | Storage | none — two emptyDirs (`/data`, `/cache`) | -| Ingress | yes, `ingress.domain` + `ingress.clusterIssuer` required **only while the Ingress is on** | +| Routing | **two mutually exclusive modes**, Ingress (default) or Gateway API (`httpRoute.enabled`); `ingress.clusterIssuer` required only while the Ingress is on, `ingress.domain` while **either** mode is. **The subchart has no Gateway support**, so with the proxy owning the host the publishing object is its Ingress in both modes — see [Routing](#routing-ingress-or-gateway-api) | | Dependencies | `oauth2-proxy` `10.7.0` (https://oauth2-proxy.github.io/manifests), `condition: oauth2-proxy.enabled`, off by default | | Secrets | 1 × `OnePasswordItem`, and only with the proxy enabled | | Install-test | `ci/ytdlp-web-player/` (test values only — no fixtures needed) | @@ -135,13 +135,23 @@ The CR is rendered **only** while the proxy is enabled *and* `oauth2-proxy.confi | Key | Type | Default | Description | |---|---|---|---| -| `ingress.enabled` | bool | `true` | Gates this chart's whole Ingress template. **Unlike `searxng`, it frees both `domain` and `clusterIssuer`** — nothing outside the Ingress template reads either: the app builds no absolute URLs and has no base-URL setting. | -| `ingress.domain` | string | `null` | **Required while the Ingress is enabled.** Host. | -| `ingress.clusterIssuer` | string | `null` | **Required while the Ingress is enabled.** cert-manager `ClusterIssuer`. | -| `ingress.className` | string | `nginx` | Ingress class. | -| `ingress.annotations` | object | `{}` | Merged with the cert-manager annotation the chart always sets. | +| `ingress.enabled` | bool | `true` | Gates this chart's whole Ingress template. **Mutually exclusive with `httpRoute.enabled`.** It frees `clusterIssuer`; it frees `domain` only if `httpRoute` is off as well — *this page used to say it frees both, which was true until the Gateway support landed and the route started reading the domain*. What still holds is the reason: the app builds no absolute URLs and has no base-URL setting, so unlike `searxng` nothing **else** reads either value. | +| `ingress.domain` | string | `null` | **Required while either mode is enabled.** Host — the Ingress's, and the `HTTPRoute`'s while `httpRoute.hostnames` is empty. | +| `ingress.clusterIssuer` | string | `null` | **Required while the Ingress is enabled.** cert-manager `ClusterIssuer`. Unused *and* unnecessary in Gateway mode. | +| `ingress.className` | string | `nginx` | Ingress class. No counterpart in Gateway mode — the class sits on the Gateway. | +| `ingress.annotations` | object | `{}` | Merged with the cert-manager annotation the chart always sets. **No counterpart in Gateway mode.** Note which annotation this is *not* about: the 16k `proxy-buffer-size` this chart recommends belongs on the **proxy's** Ingress (`oauth2-proxy.ingress.annotations`), and the subchart renders an Ingress in either mode, so that one is unaffected. | -TLS is stored in `tls--ytdlp-web-player-ingress`. +TLS is stored in `tls--ytdlp-web-player-ingress`. In Gateway mode the chart renders neither the secret nor a certificate. + +### `httpRoute` + +The Gateway-API alternative to the Ingress, **off by default**. Added with group 3 of the Gateway-API sweep (Gitea #114); the form is the pilot's and is binding for every chart ([style guide §4](Chart-Style-Guide#4-ingress-and-gateway-api)). **Read [Routing](#routing-ingress-or-gateway-api) before switching it on — in the recommended configuration it changes nothing.** + +| Key | Type | Default | Description | +|---|---|---|---| +| `httpRoute.enabled` | bool | `false` | Renders `-ytdlp-web-player-httproute` instead of the Ingress. **Mutually exclusive with `ingress.enabled`** — with both `true` the render aborts naming both values. | +| `httpRoute.parentRefs` | list | `[]` | The Gateway(s) the route attaches to — Gateway API `ParentReference` entries (`name`, and optionally `namespace`, `sectionName`, `port`, `group`, `kind`), passed through unchanged. **Required while `httpRoute.enabled` is true**: the render aborts on an empty list. The schema requires `name` on every entry and is otherwise left open, because Kubernetes owns that schema and adds fields to it across releases. | +| `httpRoute.hostnames` | list | `[]` | Empty means `[ingress.domain]`, so the hostname has a single source of truth. Set it to publish under a different or additional name — which is also how the application is given a host of **its own** beside the proxy's. **The host guard reads this list too**, so a hostname set here that the proxy's Ingress also claims is caught rather than silently published twice. | ### `oauth2-proxy` (subchart) @@ -167,11 +177,40 @@ Reaching the key from a template needs `index .Values "oauth2-proxy"` — `.Valu The application has **no authentication of any kind** and the chart adds none while `oauth2-proxy.enabled` is false. An anonymous visitor does not just read a page: they can make this pod fetch and transcode arbitrary videos from the internet — your egress, your CPU, your IP address in the logs of whatever site is being pulled from, and your ephemeral storage. Assume it is found: hosts get scanned, and certificate transparency logs publish every name cert-manager requests, so "nobody knows the URL" has a half-life of hours. `NOTES.txt` prints this warning whenever an Ingress is on and the proxy is off. -### Who owns the host +### Routing: Ingress or Gateway API -The proxy sits in the data path, so with `oauth2-proxy.enabled: true` the Ingress belongs to the **proxy** and this chart's `ingress.enabled` should be `false`; the app is then reachable only through its Service. +Two mutually exclusive modes, and exactly one of them may be on. `ingress.enabled` ships `true`, `httpRoute.enabled` ships `false`. -Leaving both on for the **same host** does not merge the routes — ingress-nginx's admission webhook rejects the second Ingress, which leaves a Fleet bundle in `ErrApplied` until `forceSyncGeneration` is bumped. The chart therefore **refuses to render that combination** and names the host in the failure. Leaving this chart's Ingress on for a **different** host is allowed and means exactly what it says: an unauthenticated way past the proxy. `NOTES.txt` warns about it. +**Read this before switching `httpRoute` on, because in the recommended configuration it does nothing.** The oauth2-proxy subchart has **no Gateway-API support** and this chart cannot give it one — upstream's chart is consumed as it comes ([what the dependency costs](#what-the-dependency-costs)), and it renders an Ingress or nothing. So with the proxy in the data path and owning the host, the object that publishes the release is **the subchart's Ingress in both of this chart's modes**. Turning `httpRoute.enabled` on there changes nothing about how the host is served. + +This chart's own route is for the other two cases: + +- the proxy switched off entirely (`oauth2-proxy.enabled: false`), so the chart publishes the application itself; +- the application on a host of **its own** beside the proxy's host — allowed, and meaning exactly what it says: an unauthenticated way past the proxy. + +| Rule | Why | +|---|---| +| `ingress.enabled` and `httpRoute.enabled` cannot both be true — the render aborts naming **both** value paths | two data paths onto one Service under one hostname is a conflict at DNS level already. `ingress.enabled` defaults to **true** here, so turning Gateway mode on means setting `ingress.enabled: false` in the same values file. **A migration runs over a second hostname** | +| `httpRoute.parentRefs` must not be empty — the render aborts | a route without a parent attaches to **no** Gateway. The object is created, it is schema-valid, no controller rejects it, and it routes nothing: no event, no condition, and the only symptom is a hostname that does not answer | + +### Who owns the host — and in Gateway mode nothing else will tell you + +The proxy sits in the data path, so with `oauth2-proxy.enabled: true` the Ingress belongs to the **proxy** and this chart's own routing object should be off; the app is then reachable only through its Service. + +Leaving both on for the **same host** is refused by the chart, and **the reason to refuse it is much stronger in Gateway mode**: + +- **In Ingress mode** ignoring it would fail loudly anyway: ingress-nginx's admission webhook rejects the second Ingress, which leaves a Fleet bundle in `ErrApplied` until `forceSyncGeneration` is bumped. Annoying, but reported. +- **In Gateway mode the webhook never fires.** It compares Ingress objects with Ingress objects, and an `HTTPRoute` is a different **kind** — so nothing rejects anything, both objects stay valid, and one hostname is served over two data paths that are **not equivalent**: the proxy's Ingress goes through **authentication**, this chart's `HTTPRoute` goes **straight to the application**. Which one a client reaches depends on what DNS resolves to. That is an **authentication bypass that comes and goes with a DNS record**, and no Kubernetes object reports it. + +The guard is the helper `ytdlp-web-player.published` (`ingress.enabled OR httpRoute.enabled`), and it exists in that form because of exactly this: before the Gateway support it asked `ingress.enabled`, which is `false` *by construction* in Gateway mode — so the guard would have gone silent precisely where it matters most. It reads `httpRoute.hostnames` as well as `ingress.domain`, and the Gateway-mode abort carries its own message naming the bypass. Called from the Deployment, the one template that always renders, so it cannot be skipped by scaling to zero or by `helm template -s`. + +Leaving this chart's routing object on for a **different** host stays allowed and still means an unauthenticated way past the proxy; `NOTES.txt` warns about it in both modes. **And in Gateway mode that is the *safe* half** — it is the same-host case that is dangerous, which is why the chart refuses it rather than warning. + +### Nothing was measured on a live Ingress of this chart, because there is none + +Checked 2026-10-12: the release runs in namespace `jlab-player` with both Deployments Ready (`jlab-player-ytdlp-web-player-service` and `jlab-player-oauth2-proxy`), and **not one of the cluster's 23 Ingress objects is in that namespace** — neither this chart's nor the subchart's. + +**What publishes it instead could not be determined.** The workspace's read-only identity may not list `httproutes` or `secrets`, so neither a hand-written `HTTPRoute` beside the release nor the release's own Helm values are readable from here. Recorded as the open question it is rather than guessed at — and worth resolving before anybody plans a mode switch for this host, since the answer decides whether the switch is a no-op, a change of publisher, or a bypass. ### What the dependency costs @@ -278,6 +317,12 @@ No `cookies.txt`: videos that require a login on the source site do not work, an Because the chart declares a dependency, `ci/helm-dependencies.sh` runs before lint, package, render and install — see [CI/CD](CI-CD). +**The Gateway mode is held at render level**, by `ci/httproute/ytdlp-web-player.yaml` plus `ci/run-httproute-match-test.sh`, which runs over every chart on every PR ([CI/CD](CI-CD#httproute-render-test)). The overlay switches this chart into Gateway mode; **2 assertions** — `parentRefs` not empty, and the `backendRefs` entry resolving to the application Service of the same render **by port number**, because `backendRefs` takes a `PortNumber` and cannot name the port `http` the way the Ingress does. That number also has to agree with `ytdlp-web-player.appServiceAddress`, which is what the proxy gets as its upstream — three places, and the overlay holds two of them together. + +**The subchart stays disabled in the overlay too**, and deliberately rather than lazily: it has no Gateway-API support, so switching it on would add a second object this test does not look at, while the thing worth exercising in Gateway mode is this chart's own route. The interesting interaction — one hostname claimed by the proxy's Ingress *and* by this chart's `HTTPRoute`, which the admission webhook cannot catch because the kinds differ — is a render-**failure** path: the chart refuses it. This test only runs renders it expects to succeed, so that guard was exercised by hand instead, in both modes, including the collision reached through `httpRoute.hostnames` rather than through the `ingress.domain` fallback, and with only the Deployment rendered. + +**What that does not cover**: that the hostname answers (no Gateway controller runs in CI, so no route reaches `Accepted: True`); that authentication is in front of the application — with the proxy off, that route **is** the unauthenticated path, which is correct for a fixture and is exactly what `NOTES.txt` warns about for a real installation; and anything about how this release is published in the real cluster, since it has no live Ingress to compare against. **Nothing in CI reads `NOTES.txt`** either: `helm template` does not emit it and no script under `ci/` reads it, so its text carries review-only assurance. + ## Renovate Two blocks watch this chart. The built-in `helmv3` manager reads the `dependencies:` block and opens the subchart PR (labelled `subchart`); no `customManagers` entry is needed or would help, because the repository URL and the version sit in one file. A `customManagers` block tracks `appVersion` against `docker.io/matszwe02/ytdlp_web_player` with an anchored `versioning` regex: the registry carries four versioned tags and the two floating ones, `latest` and `stable`, **both of which move** — `stable` was republished at the same minute as `v1.2.0`. The anchors requiring a `v` prefix and three numeric parts are what keeps them out of the value that selects the image. The `v` itself stays outside every capture group: it carries no ordering, and a capture group is a claim about how a value is compared. See [Renovate](Renovate).