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

+64
View File
@@ -0,0 +1,64 @@
# Render-only overlay for ci/run-httproute-match-test.sh, applied LAST.
#
# Its one job is to put this chart into Gateway mode, because the convention
# ships with httpRoute.enabled: false and the test would otherwise be green
# over nothing. A chart that has an overlay here and still renders no
# HTTPRoute FAILS the test - that is the whole point of the file existing.
#
# An overlay and not a change to ci/ytdlp-web-player/test-values.yaml: the
# install-test runs exactly ONE configuration per chart, and this chart's
# fixture is the one that resolves and installs the oauth2-proxy SUBCHART.
# Trading its Ingress for a CRD schema check that cyberchef's fixture already
# performs against the real Gateway-API CRDs would be a bad swap. The install
# keeps the Ingress; the route is held here, at render level.
#
# ingress.enabled: false is not optional here - ingress.enabled defaults to
# TRUE in this chart, so without this line the chart aborts its own render with
# both modes on.
#
# THE SUBCHART IS LEFT DISABLED, as the fixture has it, and that is deliberate
# rather than lazy. The oauth2-proxy chart HAS NO GATEWAY-API SUPPORT - it
# renders an Ingress or nothing - so switching it on here 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 between
# the two (one hostname claimed by the proxy's Ingress AND by this chart's
# HTTPRoute, which the ingress-nginx 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 - see the PR.
#
# WHAT THIS OVERLAY COVERS:
#
# - ingress.domain comes from the fixture (ytdlp-web-player.example.com), so
# the route renders with its hostname from that single source of truth.
# httpRoute.hostnames stays empty on purpose: the fallback is the path an
# installation will actually use.
# - the backendRef is checked against the application Service of the same
# render, by PORT NUMBER rather than by the name "http" the Ingress uses,
# because backendRefs takes a PortNumber. That number also has to agree
# with ytdlp-web-player.appServiceAddress, which is what the proxy gets as
# its upstream - three places, and this overlay holds two of them together.
#
# No URLRewrite filter is rendered by this chart in either mode, so the #118
# prefix/redirect assurance has no precondition to meet here.
#
# NOT covered here, and it cannot be:
#
# - that the hostname answers. No Gateway controller runs in CI and no route
# reaches "Accepted: True".
# - that authentication is in front of the application. With the proxy off
# this route IS the unauthenticated path, which is correct for a fixture
# and is exactly what NOTES.txt warns about for a real installation.
# - anything about how this release is published in the real cluster. There
# is no live Ingress of this chart to compare against: 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. What
# publishes it could not be determined from the workspace.
ingress:
enabled: false
httpRoute:
enabled: true
parentRefs:
- name: public
namespace: gateway
sectionName: https
+1 -1
View File
@@ -5,7 +5,7 @@ icon: https://raw.githubusercontent.com/matszwe02/ytdlp_web_player/main/src/stat
type: application
version: 1.0.1+upv1.2.0
version: 1.1.0+upv1.2.0
appVersion: "v1.2.0"
maintainers:
+98 -15
View File
@@ -1,4 +1,23 @@
{{- /*
NOTHING IN CI READS THIS FILE. `helm template` does not emit notes and no
workflow or script under ci/ reads it, so every claim below carries
review-only assurance - it is checked by being read, not by a stage. A
rendering ERROR here does fail `helm template`, which is all the pipeline can
see of it.
*/ -}}
{{- $replicas := include "ytdlp-web-player.replicas" (dict "scaleDown" .Values.scaleDown "replicas" .Values.ytdlpWebPlayer.replicas) -}}
{{- $httpRoute := .Values.httpRoute | default dict -}}
{{- /*
"Published" rather than "has an Ingress": the two modes are alternatives, so
every paragraph that asked ingress.enabled was really asking whether this
chart publishes the application (Gitea #114). Same helper the host guard uses.
*/ -}}
{{- $published := eq (include "ytdlp-web-player.published" .) "true" -}}
{{- $hostnames := $httpRoute.hostnames | default list -}}
{{- if not $hostnames -}}
{{- $hostnames = list .Values.ingress.domain -}}
{{- end -}}
{{- $ownHost := first $hostnames -}}
{{- $res := fromYaml (include "ytdlp-web-player.effectiveResources" .Values.ytdlpWebPlayer.resources) -}}
{{- $eph := index ($res.limits | default dict) "ephemeral-storage" | default "the computed ephemeral-storage limit" -}}
{{- $startup := fromYaml (include "ytdlp-web-player.startupProbe" .Values.ytdlpWebPlayer.startupProbe) -}}
@@ -22,23 +41,66 @@ ACCESS — THROUGH THE PROXY
way into the application. Certificate and TLS secret are the subchart's too —
this chart's ingress.clusterIssuer does not reach it; set the cert-manager
annotation under oauth2-proxy.ingress.annotations.
AN INGRESS, AND IT STAYS ONE IN BOTH OF THIS CHART'S MODES. The subchart has
no Gateway-API support and this chart cannot give it one (upstream's chart is
consumed as it comes), so httpRoute.enabled on THIS chart does not move the
proxy off its Ingress. In this configuration — the recommended one — turning
it on buys nothing.
{{- else if .Values.ingress.enabled }}
ACCESS
ACCESS — INGRESS MODE (ingress.enabled: true, httpRoute.enabled: false)
https://{{ .Values.ingress.domain }}
The certificate is requested by cert-manager through the "{{ .Values.ingress.clusterIssuer }}"
cluster issuer. Until it has been issued the browser shows a warning; follow it with
kubectl -n {{ .Release.Namespace }} get certificate tls-{{ .Release.Name }}-{{ .Chart.Name }}-ingress
{{- else if $httpRoute.enabled }}
ACCESS — GATEWAY MODE (httpRoute.enabled: true, ingress.enabled: false)
{{- range $hostnames }}
https://{{ . }}
{{- end }}
This release renders ONE HTTPRoute ({{ .Release.Name }}-{{ .Chart.Name }}-httproute) and nothing
else of the routing path — no Gateway, no class, no Certificate, no policy
object. Three things therefore belong to the cluster:
1. The Gateway-API CRDs. Without them this install would have failed, so
they are present — nothing to check.
2. The Gateway named in httpRoute.parentRefs
{{- range $httpRoute.parentRefs }}
{{ .name }}{{ if .namespace }} in namespace {{ .namespace }}{{ end }}{{ if .sectionName }}, listener {{ .sectionName }}{{ end }}
{{- end }}
must exist AND its listener must allow routes from this namespace
({{ .Release.Namespace }}) in allowedRoutes. A rejected attachment is visible only
in the route's status:
kubectl -n {{ .Release.Namespace }} get httproute {{ .Release.Name }}-{{ .Chart.Name }}-httproute -o yaml
Read status.parents[].conditions — "Accepted: True" and
"ResolvedRefs: True" is what a working attachment looks like. An empty
status means no controller has looked at the route at all.
3. The TLS certificate for the hostname, on the Gateway's listener.
ingress.clusterIssuer is NOT used in this mode and is not required: the
chart renders no Certificate here, so there is no
tls-{{ .Release.Name }}-{{ .Chart.Name }}-ingress secret.
ingress.annotations is inert in this mode — no annotation channel in the
Gateway API. Note which annotation that is NOT about: the 16k
proxy-buffer-size this chart recommends belongs on the PROXY's Ingress, and
the subchart renders an Ingress whatever this chart does, so it is
unaffected.
{{- else }}
ACCESS — NO INGRESS
ingress.enabled is false, so this release has no Ingress and no certificate.
ACCESS — NEITHER INGRESS NOR HTTPROUTE
ingress.enabled and httpRoute.enabled are both false, so this release has no
Ingress, no HTTPRoute and no certificate.
Both ingress.domain and ingress.clusterIssuer are unnecessary here — unlike
in some other charts in this repo, nothing outside the Ingress template reads
the domain: this application builds no absolute URLs and has no base-URL
setting. Reach it inside the cluster:
in some other charts in this repo, nothing but the two routing templates
reads the domain: this application builds no absolute URLs and has no
base-URL setting. Reach it inside the cluster:
kubectl -n {{ .Release.Namespace }} port-forward svc/{{ .Release.Name }}-{{ .Chart.Name }}-service 8080:80
@@ -48,14 +110,16 @@ ACCESS — NO INGRESS
SCALED TO ZERO
{{ if .Values.scaleDown }}scaleDown: true{{ else }}ytdlpWebPlayer.replicas: 0{{ end }} — the Deployment runs with replicas: 0, so nothing answers
until you set it back and upgrade again. Service{{ if .Values.ingress.enabled }} and Ingress stay in place,
so the certificate is not re-issued.{{ else }} stays in place.{{ end }} Nothing is lost that was not
so the certificate is not re-issued.{{ else if $httpRoute.enabled }} and HTTPRoute stay in place, so the
route keeps pointing at a Service with no endpoints — the Gateway answers
with an error of its own rather than with nothing.{{ else }} stays in place.{{ end }} Nothing is lost that was not
going to be lost anyway: the video cache is an emptyDir (see below).
{{- if $proxy.enabled }}
Note that scaleDown does NOT reach the subchart — the proxy keeps running and
will answer with a gateway error. Set oauth2-proxy.replicaCount: 0 as well.
{{- end }}
{{ end }}
{{- if and .Values.ingress.enabled (not $proxy.enabled) }}
{{- if and $published (not $proxy.enabled) }}
THIS RELEASE IS OPEN TO EVERYONE WHO KNOWS THE URL
The application has NO authentication of any kind, and this chart adds none
while oauth2-proxy.enabled is false. What an anonymous visitor can do here is
@@ -73,22 +137,41 @@ THIS RELEASE IS OPEN TO EVERYONE WHO KNOWS THE URL
enabled: true
ingress:
enabled: true
hosts: ["{{ .Values.ingress.domain }}"]
hosts: ["{{ $ownHost }}"]
ingress:
enabled: false # the proxy owns the host from then on
{{- if $httpRoute.enabled }}
httpRoute:
enabled: false # and so does this one: the SUBCHART HAS NO
# GATEWAY SUPPORT, so the proxy publishes over
# an Ingress either way. Leaving this on for the
# same host is refused by the chart; leaving it
# on for a different host is the bypass below.
{{- end }}
Anything else in front of it — an ingress-nginx auth annotation, a network
policy, a VPN-only ingress class — has to come from ingress.annotations or
from outside this chart.
from outside this chart{{ if $httpRoute.enabled }}, and NONE of it is available in Gateway mode:
there is no annotation channel, so an auth annotation has no counterpart.
Authentication would have to come from the Gateway itself or from the proxy{{ end }}.
{{- end }}
{{- if and $proxy.enabled .Values.ingress.enabled }}
{{- if and $proxy.enabled $published }}
WARNING — THE APP'S OWN INGRESS IS A WAY PAST THE PROXY
Both oauth2-proxy.enabled and ingress.enabled are true. The chart only
WARNING — THE APP'S OWN {{ if $httpRoute.enabled }}ROUTE{{ else }}INGRESS{{ end }} IS A WAY PAST THE PROXY
Both oauth2-proxy.enabled and {{ if $httpRoute.enabled }}httpRoute.enabled{{ else }}ingress.enabled{{ end }} are true. The chart only
refuses this when both claim the SAME host; here they do not, so it renders —
but https://{{ .Values.ingress.domain }} reaches the application directly, with no
but https://{{ $ownHost }} reaches the application directly, with no
authentication in front of it, whatever the proxy does on its own host.
Unless that is deliberate, set ingress.enabled=false.
Unless that is deliberate, set {{ if $httpRoute.enabled }}httpRoute.enabled=false{{ else }}ingress.enabled=false{{ end }}.
{{- if $httpRoute.enabled }}
AND IN THIS MODE THE SAME-HOST CASE IS THE ONE WORTH FEARING, which is why
the chart refuses it: ingress-nginx's admission webhook compares Ingress
objects with Ingress objects, so it never sees an HTTPRoute. Two valid
objects would then serve one hostname — the proxy's Ingress through
authentication, this chart's route straight to the application — and which
one a client reaches would depend on DNS alone, with nothing reporting it.
{{- end }}
{{- end }}
{{- if $proxy.enabled }}
+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 -}}
@@ -0,0 +1,134 @@
{{/*
HTTPRoute - the Gateway-API alternative to ytdlp-web-player.ingress.yaml
(Gitea #114).
READ THE SUBCHART PARAGRAPH FIRST, because it decides whether this mode is
useful at all for a given installation.
THE oauth2-proxy SUBCHART HAS NO GATEWAY SUPPORT, AND THIS CHART CANNOT GIVE
IT ONE. Upstream's chart is consumed as it comes - that is the standing rule
for dependencies (README, "Chart dependencies"): where its defaults fall short
of this repository's conventions, the shortfall is DOCUMENTED and never
patched. It renders an Ingress or nothing.
So in the RECOMMENDED configuration - oauth2-proxy.enabled true, the proxy in
the data path, the proxy owning the host and this chart's own routing object
switched off - the object that publishes the release is the SUBCHART'S
INGRESS, in both of this chart's modes. Setting httpRoute.enabled there buys
nothing and changes nothing about how the host is served. This route is for
the other two cases:
- the proxy switched off entirely (oauth2-proxy.enabled false), the chart
publishing the application itself;
- the application on a host of ITS OWN, next to the proxy's host - which is
allowed and means exactly what it says: an unauthenticated way past the
proxy. NOTES.txt says so in both modes.
AND THAT SECOND CASE IS WHY THE HOST GUARD CHANGED. ytdlp-web-player.validate
used to ask ingress.enabled, which is false by construction in this mode, so
the guard went silent exactly where it matters most. It now asks
ytdlp-web-player.published (ingress.enabled OR httpRoute.enabled). The
difference is not cosmetic: in Ingress mode a host claimed twice is rejected
LOUDLY by ingress-nginx's admission webhook, while in Gateway mode the webhook
never fires - it compares Ingress objects with Ingress objects, and an
HTTPRoute is a different kind. Two valid objects then serve one hostname over
two data paths, one through authentication and one straight to the
application, and which one a client reaches depends on DNS. See the helper's
comment for the full argument.
WHAT THIS RENDERS, AND WHAT IT MUST NOT
Only an HTTPRoute with parentRefs. No Gateway, no gatewayClassName, no
Certificate, no implementation-specific policy object - the subset that looks
identical on nginx-gateway-fabric, Cilium, Envoy Gateway and Istio (decision 1
of #114). Class and certificate belong to the PLATFORM, on the Gateway and its
listeners, in cluster-config.
WHAT THIS MODE DROPS, AND WHAT IT DOES NOT
ingress.annotations is inert here - no annotation channel in the Gateway API.
For this chart the measured picture is unusual, so it is worth being exact
about which annotation lives where:
- The 16k proxy-buffer-size this chart's values recommend is NOT on this
chart's Ingress. It belongs on the PROXY's Ingress
(oauth2-proxy.ingress.annotations), because it is the second half of the
4kB session-cookie trap: the response carrying the proxy's session cookie
does not fit ingress-nginx's default 4k buffer and the browser gets a 502
AFTER a successful login. The subchart still renders an Ingress in this
mode, so that annotation is UNAFFECTED by this switch. It would only be
lost if the proxy itself ever moved behind a Gateway, which this chart
cannot do.
- This chart's own ingress.annotations default is {} and the chart sets
nothing but the cert-manager annotation, so there is no chart-side loss to
name.
NOTHING WAS MEASURED ON A LIVE INGRESS OF THIS CHART, because there is none.
Checked on 2026-10-12: the release runs in namespace jlab-player (Services
jlab-player-ytdlp-web-player-service and jlab-player-oauth2-proxy, both
Deployments Ready) and NOT ONE of the 23 Ingress objects in the cluster 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
own Helm values are readable. Stated as the open question it is, rather than
guessed at.
*/}}
{{- /*
Null-safe like every other block a template here dereferences (#99): an
override written without children - "httpRoute:" with its keys deleted - is
YAML null, and .Values.httpRoute.enabled would abort with a nil pointer.
*/ -}}
{{- $httpRoute := .Values.httpRoute | default dict -}}
{{- if and $httpRoute.enabled .Values.ingress.enabled -}}
{{- fail "ingress.enabled and httpRoute.enabled are both true - set exactly one of them. The Ingress and the HTTPRoute would publish the same Service under the same hostname over two different data paths, which is already a conflict at DNS level: one name, two addresses, and which one a client reaches is luck. ingress.enabled defaults to TRUE in this chart, so turning the Gateway mode on means setting ingress.enabled: false in the same values file. A migration runs over a SECOND hostname (set httpRoute.hostnames explicitly while the Ingress keeps ingress.domain), never over parallel operation of both - and if the oauth2-proxy subchart is in the data path, mind that it has no Gateway support and publishes over an Ingress regardless." -}}
{{- end -}}
{{- if $httpRoute.enabled -}}
{{- include "ytdlp-web-player.validate" . -}}
{{- $parentRefs := $httpRoute.parentRefs | default list -}}
{{- if not $parentRefs -}}
{{- fail "httpRoute.enabled is true but httpRoute.parentRefs is empty - an HTTPRoute without a parent attaches to no Gateway. It is created, it is schema-valid, and it routes NOTHING, with no event and no condition that says so; the only symptom is a hostname that does not answer. Name the cluster's Gateway, e.g. httpRoute.parentRefs: [{name: public, namespace: gateway}]." -}}
{{- end -}}
{{- /*
One source of truth for the hostname: an empty httpRoute.hostnames means
[ingress.domain]. Two values that have to agree drift apart.
AND THIS READ CHANGES THE CHART'S OWN ACCOUNT OF ITSELF. values.yaml and the
schema both stated that everything the Ingress needs is read only inside the
Ingress template, which is why switching it off freed BOTH ingress.domain and
ingress.clusterIssuer. The second half still holds - the application has no
base-URL setting and builds no absolute links, so it needs no hostname of its
own - but ingress.domain is now read HERE as well. Both have been corrected
rather than left standing. clusterIssuer is still freed by this mode: no
Certificate is rendered.
*/ -}}
{{- $hostnames := $httpRoute.hostnames | default list -}}
{{- if not $hostnames -}}
{{- $hostnames = list (required "A Domain/Host must be provided: set ingress.domain (used as the HTTPRoute's single hostname when httpRoute.hostnames is empty) or set httpRoute.hostnames explicitly" .Values.ingress.domain) -}}
{{- end -}}
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: {{ .Release.Name }}-{{ .Chart.Name }}-httproute
spec:
parentRefs:
{{- toYaml $parentRefs | nindent 4 }}
hostnames:
{{- toYaml $hostnames | nindent 4 }}
rules:
{{- /*
The Ingress counterpart is `path: / , pathType: Prefix` on the
application Service. The port is the Service's port NUMBER and not its
name "http": backendRefs takes a PortNumber, so unlike the Ingress this
reference cannot be made by name. 80 is what the Service publishes (and
what ytdlp-web-player.appServiceAddress hands the proxy as its
upstream), so the three agree; ci/run-httproute-match-test.sh holds the
pair for the case where they stop agreeing.
*/}}
- matches:
- path:
type: PathPrefix
value: /
backendRefs:
- name: {{ .Release.Name }}-{{ .Chart.Name }}-service
port: 80
{{- end }}
+27 -3
View File
@@ -72,30 +72,54 @@
"additionalProperties": false,
"properties": {
"enabled": {
"description": "Gates this chart's whole Ingress template (#93). Here it frees BOTH domain and clusterIssuer: nothing outside the Ingress template reads either of them - the application needs no hostname of its own, so the searxng/matrix-synapse case of a domain read from an env block does not arise (guideline 1, checked in templates/ and in values.yaml).",
"description": "Gates this chart's whole Ingress template (#93). Mutually exclusive with httpRoute.enabled - the render aborts when both are true. CORRECTED with Gitea #114: it used to say this frees BOTH domain and clusterIssuer because nothing outside the Ingress template reads either. clusterIssuer is still freed (no Certificate is rendered in either mode by anything but the Ingress), but ingress.domain is read by the HTTPRoute as well, so it is freed only when BOTH modes are off. What still holds is the reason the claim was made: the application needs no hostname of its own, so the searxng/matrix-synapse case of a domain read from an env block does not arise.",
"type": "boolean"
},
"clusterIssuer": {
"type": ["string", "null"]
},
"domain": {
"description": "The public hostname. Read by the Ingress and by the HTTPRoute (when httpRoute.hostnames is empty), and by nothing else - the application takes no base URL.",
"type": ["string", "null"]
},
"className": {
"type": ["string", "null"]
},
"annotations": {
"description": "Extra Ingress annotations, merged with the cert-manager annotation the chart always sets. Free-form keys by nature, so this object stays open.",
"description": "Extra Ingress annotations, merged with the cert-manager annotation the chart always sets. Free-form keys by nature, so this object stays open. Inert in Gateway mode. Note this is NOT where the 16k proxy-buffer-size belongs: that one goes on the PROXY's Ingress (oauth2-proxy.ingress.annotations), and the subchart renders an Ingress in either mode, so it is unaffected by the switch.",
"type": "object"
}
}
},
"httpRoute": {
"description": "Gateway-API alternative to the Ingress (Gitea #114). Mutually exclusive with ingress.enabled - the render aborts when both are true, naming both value paths. The chart renders only an HTTPRoute with parentRefs; class and certificate sit on the Gateway and belong to the platform. WHAT IT DOES NOT DO: the oauth2-proxy subchart has NO Gateway support and this chart cannot give it one (upstream's chart is consumed as it comes), so in the recommended configuration - proxy in the data path, proxy owning the host - the publishing object is the SUBCHART'S INGRESS whatever this value says. This route is for the proxy-disabled case or for the application on a host of its own, which is an unauthenticated way past the proxy. THE HOST GUARD IS SHARPER HERE, NOT WEAKER: if this chart and the proxy's Ingress claim one host, the ingress-nginx admission webhook never fires (it compares Ingress with Ingress, and an HTTPRoute is another kind), so two valid objects serve one hostname over two data paths - one authenticated, one not - and which a client reaches depends on DNS. The chart refuses that combination in both modes via the ytdlp-web-player.published helper. The Gateway-API CRDs must be present in the cluster, because the chart cannot detect them.",
"type": ["object", "null"],
"additionalProperties": false,
"properties": {
"enabled": { "type": "boolean" },
"parentRefs": {
"description": "The Gateway(s) the route attaches to. Required while httpRoute.enabled is true, enforced by required()-equivalent in the template (an empty list aborts the render) - a JSON Schema \"required\" cannot do it, because the key ships as an empty list and is therefore present. Entries are the Gateway API's ParentReference and are passed through unchanged.",
"type": "array",
"items": { "$ref": "#/definitions/parentRef" }
},
"hostnames": {
"description": "Empty means [ingress.domain], so the hostname has a single source of truth. 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.",
"type": "array",
"items": { "type": "string" }
}
}
},
"oauth2-proxy": {
"description": "Values of the oauth2-proxy subchart, passed straight through. Open on purpose and at every level: this chart owns neither the keys nor their meaning, and a closed copy would reject whatever the subchart adds in its next release - the exact failure mode a schema is supposed to prevent, only pointed at the wrong chart. This chart reads exactly three of them: enabled (the dependency condition), ingress.enabled and ingress.hosts (the two-Ingress guard).",
"description": "Values of the oauth2-proxy subchart, passed straight through. Open on purpose and at every level: this chart owns neither the keys nor their meaning, and a closed copy would reject whatever the subchart adds in its next release - the exact failure mode a schema is supposed to prevent, only pointed at the wrong chart. This chart reads exactly three of them: enabled (the dependency condition), ingress.enabled and ingress.hosts (the host guard, which since Gitea #114 compares them against this chart's hostnames in EITHER mode). The subchart has no Gateway-API support and renders an Ingress or nothing, which is why httpRoute.enabled on this chart does not move the proxy off its Ingress.",
"type": "object"
}
},
"definitions": {
"parentRef": {
"description": "A Gateway API ParentReference, passed verbatim into HTTPRoute.spec.parentRefs. Deliberately left OPEN (no additionalProperties: false), for the same reason probes and security contexts are: Kubernetes owns this schema, the Gateway API adds fields to it across releases, and a closed list here would reject a field only upstream knows about. What the chart can assert is that \"name\" is there - it is required by the API and a reference without it attaches to nothing, which is the silent failure this chart aborts the render over.",
"type": "object",
"required": ["name"]
},
"resources": {
"description": "May be null / empty: the chart then falls back to its default requests and computed limits (#99) instead of rendering a container without resource accounting.",
"type": ["object", "null"],
+93 -5
View File
@@ -232,11 +232,18 @@ storage:
cacheSizeLimit: null
ingress:
# Gates this chart's own Ingress. Everything the Ingress needs is read only
# inside its template - the application itself needs no hostname of its own
# (no base-URL setting, no absolute links, no url_for(_external=True)), so
# switching the Ingress off frees BOTH ingress.domain and
# ingress.clusterIssuer instead of only the issuer.
# Gates this chart's own Ingress. MUTUALLY EXCLUSIVE WITH httpRoute.enabled -
# the render aborts when both are true, and the message names both value
# paths.
#
# CORRECTED WITH THE GATEWAY-API SUPPORT (Gitea #114): this used to say that
# everything the Ingress needs is read only inside its template, and that
# switching the Ingress off therefore freed BOTH ingress.domain and
# ingress.clusterIssuer. The second half still holds - the application needs
# no hostname of its own (no base-URL setting, no absolute links, no
# url_for(_external=True)) and this mode renders no Certificate, so
# clusterIssuer is still freed. But ingress.domain is now read by the
# HTTPRoute as well, so it is freed only when BOTH modes are off.
#
# WHO OWNS THE HOST, WITH THE PROXY ENABLED: the proxy sits in the data path,
# so with oauth2-proxy.enabled=true the Ingress belongs to the PROXY
@@ -252,8 +259,89 @@ ingress:
className: nginx
# Extra annotations for the Ingress; merged with the cert-manager
# cluster-issuer annotation the chart always sets.
#
# INERT IN GATEWAY MODE - the Gateway API has no annotation channel for
# controller behaviour. 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 keeps rendering an
# Ingress in either mode, so that one is unaffected by the switch.
annotations: {}
# THE GATEWAY-API ALTERNATIVE TO THE INGRESS (Gitea #114).
#
# Mutually exclusive with ingress.enabled - the render aborts when both are
# true. The chart renders ONLY an HTTPRoute with parentRefs: no Gateway, no
# class, no Certificate, and no implementation-specific policy object. Class
# and certificate sit on the Gateway and its listeners and belong to the
# platform (cluster-config).
#
# BEFORE SWITCHING THIS ON, KNOW WHAT IT DOES NOT DO. The oauth2-proxy
# SUBCHART HAS NO GATEWAY SUPPORT and this chart cannot give it one - upstream's
# chart is consumed as it comes (README, "Chart dependencies"), so it renders an
# Ingress or nothing. In the RECOMMENDED configuration, where the proxy sits in
# the data path and owns the host while this chart's own routing object is off,
# the object that publishes the release is THE SUBCHART'S INGRESS whatever this
# value says. Turning httpRoute on there buys nothing.
#
# This route is for the other two cases:
# - the proxy switched off (oauth2-proxy.enabled: false), so this chart
# publishes the application itself;
# - the application on a host of ITS OWN beside the proxy's host, which is
# allowed and means exactly what it says: an unauthenticated way past the
# proxy.
#
# AND THE HOST GUARD IS SHARPER IN THIS MODE, NOT WEAKER. If this chart and the
# proxy's Ingress claim the SAME host:
# - in Ingress mode ingress-nginx's admission webhook rejects the second
# object loudly (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. 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 DNS, so it is an authentication bypass that comes and goes
# with a record and that nothing in Kubernetes reports.
# The chart therefore refuses to render that combination in BOTH modes, via the
# ytdlp-web-player.published helper (ingress.enabled OR httpRoute.enabled).
# Before #114 that guard asked ingress.enabled, which is false by construction
# in Gateway mode - it would have gone silent exactly where it matters most.
#
# 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, 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 from this workspace (its identity may not
# list httproutes or secrets). An open question, not a guess.
#
# THE GATEWAY-API CRDs MUST BE PRESENT. The chart cannot detect them
# (.Capabilities.APIVersions is unreliable at template time) and an HTTPRoute
# without them fails at install, so this is a value and not a guess.
httpRoute:
enabled: false
# The Gateway(s) the route attaches to - a platform object, usually in
# another namespace, whose listener must allow this namespace in
# allowedRoutes. Entries are the Gateway API's ParentReference (name, and
# optionally namespace, sectionName, port, group, kind), passed through
# unchanged:
#
# parentRefs:
# - name: public
# namespace: gateway
# sectionName: https
#
# REQUIRED while httpRoute.enabled is true - the render aborts on an empty
# list, because a route without a parent is created, is valid and routes
# nothing, with no event to say so.
parentRefs: []
# Empty means [ingress.domain], so the hostname has one source of truth. Set
# explicitly only to publish under a different or an additional name - which
# is how a migration from the Ingress runs, and 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.
hostnames: []
# --- oauth2-proxy subchart -------------------------------------------------
#
# Everything below this line is passed to the oauth2-proxy chart