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).