feat(ytdlp-web-player): Gateway-API as an alternative to the Ingress

Group 3 of the #114 sweep, last chart - and the one that carried the group-2
pattern this sweep was told to look for. It reads ingress.enabled OUTSIDE its
Ingress template, in ytdlp-web-player.validate, and in Gateway mode
ingress.enabled is false by construction, so that guard would have gone
silent. Fixed with ytdlp-web-player.published = ingress.enabled OR
httpRoute.enabled - the freesailarr.published / sure.publishedOverTls shape,
identical to ingress.enabled while httpRoute.enabled is false, which is why it
changes no existing render.

AND THE GUARD IS NEEDED MORE IN GATEWAY MODE, NOT LESS. It refuses to publish
the application and the oauth2-proxy on the same host:

  - In INGRESS mode the consequence of ignoring it is loud: ingress-nginx's
    admission webhook rejects the second Ingress and the Fleet bundle goes to
    ErrApplied.
  - 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 and 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
    that no Kubernetes object reports.

So the Gateway-mode abort has its own message saying exactly that. Both
messages exercised, plus the collision reached through httpRoute.hostnames
rather than through the ingress.domain fallback, and the guard still firing
when only the Deployment is rendered.

THE SUBCHART HAS NO GATEWAY SUPPORT, and this chart cannot give it one -
upstream's chart is consumed as it comes (README, "Chart dependencies"). It
renders an Ingress or nothing. So in the RECOMMENDED configuration, where the
proxy sits in the data path and owns the host, the publishing object is the
SUBCHART'S INGRESS in both of this chart's modes, and turning httpRoute on
buys nothing. The route is for the proxy-disabled case, or for the application
on a host of its own - which is the documented unauthenticated bypass. Said in
the template header, in values.yaml, in the schema and in the proxy branch of
NOTES.txt, because someone migrating hosts will otherwise set the value and
wonder why nothing moved.

The 16k proxy-buffer-size this chart recommends is NOT lost by this switch,
and that needed stating rather than assuming: it belongs on the PROXY's
Ingress (oauth2-proxy.ingress.annotations) as the second half of the 4kB
session-cookie trap, and the subchart keeps rendering an Ingress. This chart's
own ingress.annotations default is {} and it sets nothing but the cert-manager
annotation, so there is no chart-side annotation loss to name.

CORRECTED CLAIM: values.yaml and the schema both said that everything the
Ingress needs is read only inside its template, so switching it off freed BOTH
ingress.domain and ingress.clusterIssuer. clusterIssuer is still freed; domain
is now read by the HTTPRoute, so it is freed only with both modes off. The
reason the claim was made still holds and is kept - the application has no
base-URL setting and builds no absolute links.

NOTHING WAS MEASURED ON A LIVE INGRESS OF THIS CHART, because there is none.
Measured 2026-10-12: the release runs in namespace jlab-player with both
Deployments Ready, 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: this workspace's identity may not list httproutes or
secrets, so neither a hand-written HTTPRoute nor the release's Helm values are
readable. Recorded as the open question it is, in values.yaml and in the
overlay, rather than guessed at.

Minor: additive, defaults unchanged. Empty helm template diff against
origin/main with ci/ytdlp-web-player/test-values.yaml.

Part of #114
This commit is contained in:
jklein committed 2026-10-12 21:22:49 +00:00
1 parent d860704172
commit f647597a69
7 files changed
+504 -36

No files matched your search

+87 -12
View File
@@ -343,19 +343,72 @@ http://{{ .Release.Name }}-{{ .Chart.Name }}-service:80
{{- end -}}
{{/*
Refuse to render two Ingresses for the same host (issue #214).
Does this release publish the application itself, in either mode?
With the proxy enabled, this chart's Ingress and the subchart's Ingress can
both be switched on, and both then claim the same host. That is not a merge and
not a warning: ingress-nginx's admission webhook rejects whichever arrives
second with "host ... and path ... is already defined in ingress ...". Under
Fleet the bundle goes to ErrApplied and Fleet does not retry a rejected apply
by itself - getting out costs a forceSyncGeneration bump on the GitRepo.
Failing here costs a re-render.
ytdlp-web-player.published = ingress.enabled OR httpRoute.enabled
Added with the Gateway-API support (Gitea #114), and it is the same shape as
freesailarr.published and sure.publishedOverTls. It exists because
httpRoute.enabled is an ALTERNATIVE to ingress.enabled, so in Gateway mode
ingress.enabled is false BY CONSTRUCTION - and every reader of
ingress.enabled outside the Ingress template would silently get the wrong
answer in that mode. In this chart there was exactly one such reader, the host
guard below, and it was the guard that is needed MOST in Gateway mode. See its
comment.
Identical to ingress.enabled while httpRoute.enabled is false, which is why
introducing it changes no existing release's render.
*/}}
{{- define "ytdlp-web-player.published" -}}
{{- $httpRoute := .Values.httpRoute | default dict -}}
{{- if or .Values.ingress.enabled $httpRoute.enabled -}}
true
{{- else -}}
false
{{- end -}}
{{- end -}}
{{/*
Refuse to publish the application and the proxy on the same host (issue #214,
extended for Gitea #114).
With the proxy enabled, this chart's own routing object and the SUBCHART's
Ingress can both be switched on, and both then claim the same host. What
happens next depends on the mode, and the Gateway-mode half is the dangerous
one:
INGRESS MODE - this chart's Ingress against the subchart's Ingress. Not a
merge and not a warning: ingress-nginx's admission webhook rejects
whichever arrives second with "host ... and path ... is already defined in
ingress ...". Under Fleet the bundle goes to ErrApplied and Fleet does not
retry a rejected apply by itself - getting out costs a forceSyncGeneration
bump on the GitRepo. Loud, at least.
GATEWAY MODE - this chart's HTTPRoute against the subchart's Ingress. THE
WEBHOOK DOES NOT FIRE AT ALL: it compares Ingress objects with Ingress
objects, and these are two different kinds. So nothing rejects anything,
both objects are valid, and one hostname is served over two data paths.
And the two are not equivalent - the proxy's Ingress goes through
AUTHENTICATION and this chart's HTTPRoute goes STRAIGHT TO THE
APPLICATION. Which one a client reaches is a question of what DNS resolves
to, so the failure mode is an authentication bypass that appears and
disappears with a DNS record. Nothing in Kubernetes reports it.
That is why this guard now asks ytdlp-web-player.published rather than
ingress.enabled: in Gateway mode the old condition was false and the guard was
silent, exactly where it mattered more.
THE SUBCHART HAS NO GATEWAY SUPPORT, and that is not something this chart can
fix - upstream's chart is consumed as it comes (README, "Chart dependencies").
So in the RECOMMENDED configuration, where the proxy sits in the data path and
owns the host, the publishing object is the subchart's INGRESS whatever this
chart's mode is. Switching this chart to httpRoute there buys nothing; the
chart's own routing object is for the other case, the app on a host of its own
or the proxy switched off.
Host-based rather than "both switched on", because a proxy on a host of its own
collides with nothing. That case is not silently blessed either: NOTES.txt says
plainly that the app's own Ingress is then a way past the proxy.
plainly that the app's own route is then a way past the proxy.
Called from the Deployment - the one template that always renders - so the
guard cannot be skipped by scaling to zero or by rendering a single file with
@@ -364,9 +417,31 @@ guard cannot be skipped by scaling to zero or by rendering a single file with
{{- define "ytdlp-web-player.validate" -}}
{{- $proxy := (index .Values "oauth2-proxy") | default dict -}}
{{- $proxyIngress := $proxy.ingress | default dict -}}
{{- if and .Values.ingress.enabled $proxy.enabled $proxyIngress.enabled -}}
{{- if has .Values.ingress.domain ($proxyIngress.hosts | default list) -}}
{{- fail (printf "ingress.enabled and oauth2-proxy.ingress.enabled both claim the host %q. ingress-nginx allows only one Ingress per host and path; its admission webhook rejects the second one, which leaves a Fleet bundle in ErrApplied until forceSyncGeneration is bumped. Decide who owns the host: with the proxy in the data path set ingress.enabled=false (the app is then reached through the proxy and its Service), or drop the proxy's Ingress with oauth2-proxy.ingress.enabled=false." .Values.ingress.domain) -}}
{{- $httpRoute := .Values.httpRoute | default dict -}}
{{- if and (eq (include "ytdlp-web-player.published" .) "true") $proxy.enabled $proxyIngress.enabled -}}
{{- /*
The hostnames this chart publishes on: ingress.domain in Ingress mode, and in
Gateway mode httpRoute.hostnames or - when that is empty - ingress.domain,
which is the same single source of truth the route itself uses.
*/ -}}
{{- $ownHosts := list -}}
{{- if $httpRoute.enabled -}}
{{- $ownHosts = $httpRoute.hostnames | default list -}}
{{- if not $ownHosts -}}
{{- $ownHosts = list .Values.ingress.domain -}}
{{- end -}}
{{- else -}}
{{- $ownHosts = list .Values.ingress.domain -}}
{{- end -}}
{{- $proxyHosts := $proxyIngress.hosts | default list -}}
{{- range $host := $ownHosts -}}
{{- if has $host $proxyHosts -}}
{{- if $httpRoute.enabled -}}
{{- fail (printf "httpRoute.enabled and oauth2-proxy.ingress.enabled both claim the host %q, and in this mode NOTHING REJECTS IT. ingress-nginx's admission webhook compares Ingress objects with Ingress objects; this chart renders an HTTPRoute, so the two kinds never meet and both objects stay valid. One hostname is then 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, so this is an authentication bypass that comes and goes with a DNS record and that no Kubernetes object reports. Decide who owns the host: with the proxy in the data path set httpRoute.enabled=false (the app is then reached through the proxy and its Service - note the subchart has no Gateway support, so the proxy publishes over an Ingress either way), or drop the proxy's Ingress with oauth2-proxy.ingress.enabled=false, or give this chart a host of its own via httpRoute.hostnames and accept that it bypasses the proxy." $host) -}}
{{- else -}}
{{- fail (printf "ingress.enabled and oauth2-proxy.ingress.enabled both claim the host %q. ingress-nginx allows only one Ingress per host and path; its admission webhook rejects the second one, which leaves a Fleet bundle in ErrApplied until forceSyncGeneration is bumped. Decide who owns the host: with the proxy in the data path set ingress.enabled=false (the app is then reached through the proxy and its Service), or drop the proxy's Ingress with oauth2-proxy.ingress.enabled=false." $host) -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- end -}}