fix(freesailarr): redirect the bare /qbittorrent prefix in Gateway mode
Validate Helm Charts / Detect changed charts (pull_request) Successful in 5s
Validate Helm Charts / Resource quantities (pull_request) Successful in 15s
Validate Helm Charts / Selector match (pull_request) Successful in 14s
Validate Helm Charts / HTTPRoute match (pull_request) Successful in 14s
Validate Helm Charts / Build freesailarr (pull_request) Successful in 8s
Validate Helm Charts / Trivy config freesailarr (pull_request) Successful in 22s
Install-test / Install-test freesailarr (pull_request) Successful in 4m19s
Validate Helm Charts / Install-test (pull_request) Successful in 4m19s
Publish Helm Chart with patched Application Version / publish-helm-chart (push) Successful in 12s
Publish Freesailarr Helm Chart / Publish Freesailarr Helm Chart (push) Successful in 12s
SonarQube Analysis / SonarQube Analysis (push) Successful in 48s
Validate Helm Charts / Detect changed charts (pull_request) Successful in 5s
Validate Helm Charts / Resource quantities (pull_request) Successful in 15s
Validate Helm Charts / Selector match (pull_request) Successful in 14s
Validate Helm Charts / HTTPRoute match (pull_request) Successful in 14s
Validate Helm Charts / Build freesailarr (pull_request) Successful in 8s
Validate Helm Charts / Trivy config freesailarr (pull_request) Successful in 22s
Install-test / Install-test freesailarr (pull_request) Successful in 4m19s
Validate Helm Charts / Install-test (pull_request) Successful in 4m19s
Publish Helm Chart with patched Application Version / publish-helm-chart (push) Successful in 12s
Publish Freesailarr Helm Chart / Publish Freesailarr Helm Chart (push) Successful in 12s
SonarQube Analysis / SonarQube Analysis (push) Successful in 48s
qBittorrent has no base-path setting, so the chart serves it under a prefix and strips that prefix before the request reaches it. For the slashed form that is correct. For the BARE form - /qbittorrent, no trailing slash - the request is rewritten to "/" and qBittorrent answers 200 with its login page, while the browser's document URL keeps the prefixless form. Every relative link in that page therefore resolves against the host root, and on the shared host the root is seerr: the login form comes up unstyled and does nothing while a different application answers its assets and XHRs. Nothing reports an error at any level. The shared-domain HTTPRoute now carries a second rule for qBittorrent: an Exact match on qbittorrent.ingress.path with a RequestRedirect filter that replaces the full path with the slashed form, statusCode 301. The Gateway API ranks an Exact path match above every prefix match, so it wins over both qBittorrent's own PathPrefix rule and seerr's "/" wherever it is emitted - the rules' length-descending order stays a rendering convention. 301 rather than 308 because only 301 and 302 are Core in that API; an Extended value an implementation declines makes it set Accepted=False on the WHOLE route, which would take the four neighbouring apps' routing down with it, and that would also contradict decision 1 of Gitea #114. statusCode is written out because it defaults to 302. The rule is guarded on the app being qbittorrent and its path not being "/", and it reads the path from the value, never a literal. The INGRESS BRANCH IS UNCHANGED, deliberately. The remedy there is an nginx configuration-snippet annotation, and the chart cannot ship it: snippets are ignored unless the controller runs with allow-snippet-annotations=true, off by default since ingress-nginx 1.9 and only switched on by accepting annotation risk level "Critical". A shipped annotation would be a silent no-op on every cluster keeping the default. The working snippet, that price, and why a third Ingress object with pathType: Exact is not a substitute - buildLocation in ingress-nginx controller-v1.14.5 returns a regex location in its enforceRegex branch before reaching the PathTypeExact branch, and enforceRegex is computed once per nginx server over every location of the hostname, one of which carries rewrite-target - are all written out in values.yaml instead. ci/run-httproute-match-test.sh gains a third assertion, per HTTPRoute object: a rule stripping a prefix with ReplacePrefixMatch requires a rule in the same route matching that path Exactly and carrying a RequestRedirect filter. This is what stops the gap reappearing; bulwark is the second rewrite-target chart and meets the same check when group 3 of Gitea #114 lands. 37 assertions over 15 charts, up from 36. Both fail paths were exercised: the rule deleted, and the redirect pointed at a path other than the one stripped. Minor and not major: with the Ingress branch untouched and httpRoute.enabled defaulting to false, no existing release's render changes by a byte and no value becomes required. Shown by an empty helm template diff against origin/main in both Ingress shapes, shared-domain and per-app hosts. Fixes #118
This commit was merged in pull request #119.
This commit is contained in:
6 files changed
+345
-23
No files matched your search
@@ -12,6 +12,14 @@
|
||||
{{- range $shared }}{{ $sharedNames = append $sharedNames .name }}{{ end -}}
|
||||
{{- $httpRoute := .Values.httpRoute | default dict -}}
|
||||
{{- $published := include "freesailarr.published" . -}}
|
||||
{{- /* NOTHING IN CI READS THIS FILE, so every claim below is unverified by any
|
||||
pipeline stage. `helm template` does not emit NOTES.txt at all, and no
|
||||
workflow or script under ci/ reads it (measured for Gitea #118: the
|
||||
three hits for "NOTES" under ci/ are comments in fixtures, not reads).
|
||||
The render tests therefore cannot catch a statement here that has gone
|
||||
stale, and the install-test never sees it either — it is prose checked
|
||||
by review only. Treat an edit to it with the same care as the templates
|
||||
it describes, because nothing else will. */ -}}
|
||||
freesailarr — release {{ .Release.Name }} in namespace {{ .Release.Namespace }}.
|
||||
{{ if .Values.mediathekarr.enabled }}Ten{{ else }}Nine{{ end }} containers in ONE pod, sharing gluetun's network namespace.
|
||||
|
||||
@@ -87,17 +95,39 @@ ACCESS
|
||||
same host, because nginx applies rewrite-target per object and the other
|
||||
apps must not be rewritten.
|
||||
{{- else }} In Gateway mode that second object is gone: a
|
||||
URLRewrite filter belongs to a RULE, so qBittorrent is one rule of
|
||||
URLRewrite filter belongs to a RULE, so qBittorrent is a rule of
|
||||
{{ $name }}-httproute with the filter on it and its neighbours are
|
||||
untouched by construction — no extra object, no regex path.
|
||||
{{- end }}
|
||||
Two things here look exactly
|
||||
like a login failure and send you looking in the wrong place: its session
|
||||
cookie is marked "Secure" (over plain HTTP the browser never sends it back
|
||||
and every click bounces to the login form), and it bans the client IP for a
|
||||
while after a few failed attempts. If it does not work at all, set
|
||||
qbittorrent.ingress.host and it is back on a host of its own - no chart
|
||||
change needed.
|
||||
|
||||
OPEN IT WITH THE TRAILING SLASH:
|
||||
|
||||
https://{{ $sharedHost }}{{ include "freesailarr.appPath" (dict "root" $ "app" "qbittorrent") }}/
|
||||
|
||||
{{ if .Values.ingress.enabled }} Without the slash the prefix is stripped to "/", qBittorrent answers 200,
|
||||
and every RELATIVE link in that page resolves against the ROOT of this
|
||||
host — which is {{ if has "seerr" $sharedNames }}seerr{{ else }}whatever serves "/" here{{ end }}, not qBittorrent. The login form then looks
|
||||
unstyled and does nothing while another application answers its requests.
|
||||
This chart does NOT render that redirect in Ingress mode: nginx offers only
|
||||
a configuration-snippet annotation for it, and snippets are ignored unless
|
||||
the controller runs with allow-snippet-annotations=true, which has been off
|
||||
by default since ingress-nginx 1.9 and means accepting annotation risk
|
||||
level "Critical". The working snippet, and its price, are in values.yaml at
|
||||
qbittorrent.ingress (Gitea #118).
|
||||
|
||||
{{ else }} The bare, slashless path is redirected 301 to the slashed form by a rule of
|
||||
{{ $name }}-httproute — an Exact path match, which the Gateway API ranks
|
||||
above every prefix match, so it wins wherever it sits in the list. Without
|
||||
it qBittorrent would answer 200 under a URL whose relative links resolve to
|
||||
the root of this host, which is a different application (Gitea #118). This
|
||||
is the one thing the Gateway mode does here that the Ingress mode cannot.
|
||||
|
||||
{{ end }} Two further things look exactly like a login failure and send you
|
||||
looking in the wrong place: its session cookie is marked "Secure" (over
|
||||
plain HTTP the browser never sends it back and every click bounces to the
|
||||
login form), and it bans the client IP for a while after a few failed
|
||||
attempts. If it does not work at all, set qbittorrent.ingress.host and it
|
||||
is back on a host of its own - no chart change needed.
|
||||
{{- end }}
|
||||
{{- if not (has "seerr" $sharedNames) }}
|
||||
|
||||
|
||||
@@ -60,6 +60,16 @@ equivalent"; in this chart the equivalent is strictly less machinery than the
|
||||
original, and that is the reason this chart was worth converting rather than
|
||||
merely translating.
|
||||
|
||||
qBittorrent is the one app that contributes a SECOND rule, and it is not part
|
||||
of the rewrite: a 301 from the bare, slashless prefix to the slashed form
|
||||
(Gitea #118). Without it qBittorrent answers 200 under a URL whose relative
|
||||
links resolve to the host root, which on the shared host is a DIFFERENT
|
||||
application. In Ingress mode the chart does not render that redirect - it
|
||||
cannot, as the only mechanism nginx offers for it is a snippet annotation that
|
||||
most clusters refuse (values.yaml, qbittorrent.ingress, has the full price).
|
||||
This is therefore the one place where the Gateway mode is not merely less
|
||||
machinery but closes a gap the Ingress mode leaves open.
|
||||
|
||||
WHAT IS NOT BETTER: ANYTHING THAT WAS AN ANNOTATION AND IS NOT A FILTER. The
|
||||
per-app and global ingress.annotations have no counterpart at all here. This
|
||||
chart sets no body-size annotation of its own, so nothing it NEEDS is lost -
|
||||
@@ -136,14 +146,21 @@ switching every app off while leaving ingress.host set produces no object
|
||||
rather than an empty one without rules.
|
||||
|
||||
qBittorrent is in here with the others, which is the difference to the Ingress:
|
||||
its rule carries the URLRewrite filter that strips the prefix, and a filter in
|
||||
the Gateway API belongs to a RULE. No second object, no repeated TLS secret, no
|
||||
conditional cert-manager annotation, no regex path - see the header.
|
||||
a filter in the Gateway API belongs to a RULE, so it contributes TWO rules of
|
||||
its own and the apps around it stay untouched by construction - a PathPrefix
|
||||
rule with the URLRewrite filter that strips the prefix, and an Exact rule on
|
||||
the bare, slashless prefix that redirects 301 to the slashed form (Gitea #118;
|
||||
the long reasoning is on that rule). No second object, no repeated TLS secret,
|
||||
no conditional cert-manager annotation, no regex path - see the header.
|
||||
|
||||
The rules come sorted by path length descending, as the Ingress paths do. The
|
||||
Gateway API specifies matching precedence by longest path match independent of
|
||||
order (and so does nginx), so this is about a deterministic, reviewable
|
||||
rendering, not about matching behaviour.
|
||||
The rules come sorted by path length descending, as the Ingress paths do, and
|
||||
qBittorrent's redirect rule follows the rule it corrects. Neither placement
|
||||
carries meaning: the Gateway API specifies matching precedence by criteria
|
||||
before order - an Exact match outranks every prefix match, and among prefix
|
||||
matches the longest wins - and list order is consulted only when all of those
|
||||
tie. So the ordering here is about a deterministic, reviewable rendering, not
|
||||
about matching behaviour. That it is ALSO the order nginx would need is a
|
||||
coincidence of this chart's history and not a property to rely on.
|
||||
*/}}
|
||||
---
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
@@ -183,6 +200,87 @@ spec:
|
||||
backendRefs:
|
||||
- name: {{ $.Release.Name }}-{{ $.Chart.Name }}-{{ $app.name }}-service
|
||||
port: {{ int $app.port }}
|
||||
{{- if and (eq $app.name "qbittorrent") (ne $app.path "/") }}
|
||||
{{- /* THE BARE PREFIX, WITHOUT THE TRAILING SLASH (Gitea #118)
|
||||
|
||||
A request for the bare qbittorrent.ingress.path - "/qbittorrent"
|
||||
by default, no trailing slash - is matched by the PathPrefix rule
|
||||
above and the URLRewrite turns it into "/". That is a valid
|
||||
request to qBittorrent, which answers 200 with its login
|
||||
page, so nothing anywhere reports a problem. But the browser's
|
||||
document URL stays prefixless, and a relative link in that page
|
||||
resolves against the PARENT of that path - which is the host root.
|
||||
Every asset, every XHR and every form target therefore goes to "/",
|
||||
and "/" on this shared host is SEERR. The symptom is qBittorrent's
|
||||
login page that is unstyled and does nothing, while the requests it
|
||||
makes are answered by a different application entirely - and seerr
|
||||
answering a qBittorrent request is not a clue anyone follows
|
||||
quickly. One redirect to the slashed form fixes all of it, because
|
||||
relative links then resolve against the slashed prefix.
|
||||
|
||||
PRECEDENCE IS NOT POSITION. This rule is matched by the Gateway API
|
||||
on an Exact path, and the specification ranks "Exact path match"
|
||||
ABOVE "Prefix path match with largest number of characters", with
|
||||
list order consulted only when every earlier criterion ties
|
||||
(gateway-api v1.6.3, apis/v1/httproute_types.go). So this rule wins
|
||||
over the PathPrefix rule above AND over seerr's PathPrefix "/" no
|
||||
matter where it is emitted. The length-descending sort of the rules
|
||||
in this route therefore stays what the header says it is - a
|
||||
deterministic, reviewable rendering - and emitting this rule
|
||||
directly after the one it corrects is a readability choice, not a
|
||||
correctness one.
|
||||
|
||||
301 AND NOT 308, deliberately, and the reason is not the HTTP
|
||||
semantics but which values an implementation is obliged to support.
|
||||
The Gateway API marks 301 and 302 as CORE and everything else -
|
||||
308 included - as EXTENDED. An implementation that does not support
|
||||
the value given here must set the Accepted condition of the ROUTE to
|
||||
status False with reason UnsupportedValue: not of this rule, of the
|
||||
whole object. On the shared host that is one route carrying every
|
||||
app under it, so a 308 that one implementation declines takes
|
||||
prowlarr, radarr, sonarr and seerr down with qBittorrent - and it
|
||||
does so on a route that is schema-valid, in a status field no
|
||||
controller writes in CI. It would also contradict decision 1 of
|
||||
Gitea #114, which is that this chart renders only the subset
|
||||
that looks identical on nginx-gateway-fabric, Cilium, Envoy
|
||||
Gateway and Istio, so that the cluster's implementation can be
|
||||
swapped without touching the chart.
|
||||
|
||||
What 308 would buy is method and body preservation, and this rule
|
||||
does not need it: it fires on exactly one path, the prefixless entry
|
||||
to the WebUI, which a browser reaches with GET. qBittorrent's API
|
||||
lives under "<path>/api/..." - slashed, longer, and therefore
|
||||
served by the PathPrefix rule above, which this rule never sees. A
|
||||
POST to the API is not redirected at all, so there is no body to
|
||||
lose.
|
||||
|
||||
statusCode MUST BE WRITTEN OUT: it defaults to 302 in the API, and
|
||||
a temporary redirect here would be re-fetched by every client on
|
||||
every visit forever, for a prefix that is a permanent property of
|
||||
this release's layout.
|
||||
|
||||
THE ONE ASYMMETRY TO THE nginx SNIPPET (values.yaml, qbittorrent):
|
||||
HTTPRequestRedirectFilter has exactly five fields - scheme,
|
||||
hostname, path, port, statusCode - and says nothing about the query
|
||||
string, while ReplaceFullPath is specified as replacing the full
|
||||
PATH. The nginx remedy in Ingress mode appends "$is_args$args" and
|
||||
so carries the query over explicitly; here the query's fate is left
|
||||
to the implementation and is not something this chart can state.
|
||||
It costs nothing on the path this rule actually serves - the UI
|
||||
entry point takes no query parameters - but it is a real difference
|
||||
and not a translation. */}}
|
||||
- matches:
|
||||
- path:
|
||||
type: Exact
|
||||
value: {{ $app.path | quote }}
|
||||
filters:
|
||||
- type: RequestRedirect
|
||||
requestRedirect:
|
||||
path:
|
||||
type: ReplaceFullPath
|
||||
replaceFullPath: {{ printf "%s/" $app.path | quote }}
|
||||
statusCode: 301
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
Reference in new issue
Block a user