{{/* The Gateway-API alternative to freesailarr.ingress.yaml (Gitea #114). Form and reasoning: template-chart/templates/xxx.httproute.yaml and README, "Ingress and Gateway API". Only HTTPRoutes with parentRefs. No Gateway, no gatewayClassName, no Certificate, and no implementation-specific policy object - that subset looks identical on nginx-gateway-fabric, Cilium, Envoy Gateway and Istio, which is the whole reason the cluster's implementation can be swapped without touching this chart. Class and certificate therefore belong to the PLATFORM, on the Gateway and its listeners. ingress.clusterIssuer, ingress.className, ingress.annotations and their per-app overrides are all inert in this mode. THE SAME TWO SHAPES AS THE INGRESS, AND THEY STILL MIX 1. One host per app (the default): every published app gets its own HTTPRoute with its own hostname, pointing at its own app's Service. 2. One shared domain with sub-paths (ingress.host): the apps that carry no host of their own move into ONE route with one rule each. THIS CHART HAS NO ingress.domain, SO httpRoute HAS NO hostnames It is the documented exception on that point (README, guideline 4): the chart bundles ten independently exposed apps, so there is no single hostname to put in a chart-global list - a hostnames entry could not say which app it belonged to. The hostnames therefore come from the values that already hold them, ingress.host and .ingress.host, which keeps the single source of truth the convention is actually about. Those two are not Ingress switches: they name the public addresses of this release, and both modes read them. Consequence for a migration: the "second hostname" the convention offers elsewhere is done here by changing .ingress.host, which in Gateway mode feeds only the route - there is no Ingress rendered to disagree with it. WHERE THE GATEWAY API IS BETTER THAN WHAT IT REPLACES, AND IT IS NOT A TRANSLATION qBittorrent has no base-path setting: it serves its WebUI at the root and the proxy has to strip the prefix. nginx applies rewrite-target per INGRESS OBJECT and not per path, and the other apps must NOT be rewritten - their UrlBase contains the prefix, so they expect the full path. That is why freesailarr.ingress.yaml splits qBittorrent into a SECOND Ingress object on the same host, which then has to - repeat the same TLS secret name as the main object, so that neither object depends on an assumption about whether nginx applies the other one's TLS configuration to its paths, - OMIT the cert-manager annotation so only one Certificate is requested for that one secret - except when no main object renders at all, which is a conditional inside the annotation block, and - match on an ImplementationSpecific path with a regex ("(/|$)(.*)") whose capture group 2 the rewrite refers to. The Gateway API's URLRewrite filter is per RULE. All of that disappears: one route, one rule per app, and qBittorrent's rule carries the filter while its neighbours do not. The path match goes back to a plain PathPrefix, there is no second object, no duplicated TLS block, no conditional annotation and no regex. The convention's own summary says rewrite-target "has a real 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. 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 - but an operator who added one through ingress.annotations (qBittorrent takes torrent file uploads) loses it silently in this mode. NOTES.txt says so. */}} {{- /* Null-safe like every other block this chart dereferences (#99): an override written without children - "httpRoute:" with the keys below it deleted - is YAML null, and `.Values.httpRoute.enabled` would abort the render with a nil pointer. An empty block means "chart defaults apply", and both defaults are empty or false, so the whole block collapses to "no HTTPRoute". */ -}} {{- $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 Ingresses and the HTTPRoutes would publish the same Services under the same hostnames 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 is this chart's MASTER switch and defaults to true, so turning the Gateway mode on means setting ingress.enabled: false in the same values file. A migration runs over different hostnames (change ingress.host or .ingress.host), never over parallel operation of both modes." -}} {{- end -}} {{- if $httpRoute.enabled -}} {{- $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 -}} {{- $sharedHost := .Values.ingress.host | default "" }} {{- /* The same validated list the Ingress uses, and the same list the UrlBase env vars read: it is gated on freesailarr.published, so it is populated in this mode as well. Its two guards - seerr/mediathekarr only at "/", no duplicate paths on the shared host - therefore apply here unchanged, which is the whole reason they live in the helper. */}} {{- $shared := include "freesailarr.sharedIngressApps" $ | fromJsonArray }} {{- range $app := include "freesailarr.ingressApps" $ | fromJsonArray }} {{- $config := (index $.Values $app.name).ingress }} {{- /* Skip the apps that live on the shared domain; they are one rule each in the single route below. */}} {{- if or (not $sharedHost) ($config.host | default "") }} {{- $host := required (printf "%s.ingress.host must be provided while %s.ingress.enabled is true (or set the shared ingress.host to serve the app under a sub-path instead). The value is the app's public hostname and is read in Gateway mode as well - it is not an Ingress switch." $app.name $app.name) $config.host }} --- apiVersion: gateway.networking.k8s.io/v1 kind: HTTPRoute metadata: name: {{ $.Release.Name }}-{{ $.Chart.Name }}-{{ $app.name }}-httproute spec: parentRefs: {{- toYaml $parentRefs | nindent 4 }} hostnames: - {{ $host | quote }} rules: {{- /* The Ingress counterpart is `path: / , pathType: Prefix` on this app's own Service. An app on a host of its own is served at the root, so no rewrite is involved in either mode - that question only arises under the shared host below. The port is the Service's port NUMBER and not its name (the Ingress addresses it by name, which is the app name): backendRefs takes a PortNumber, so this reference cannot be made by name and the number has to track freesailarr.service.yaml. Both come from freesailarr.portApps, which is also what the Service renders from, so they cannot drift - and ci/run-httproute-match-test.sh fails when a backendRef names a Service, or a port, the same render does not create. */}} - matches: - path: type: PathPrefix value: / backendRefs: - name: {{ $.Release.Name }}-{{ $.Chart.Name }}-{{ $app.name }}-service port: {{ int $app.port }} {{- end }} {{- end }} {{- if $shared }} {{/* The shared-domain route: ONE object for every app under ingress.host, one rule each, rendered only when at least one app actually contributes a path - so 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. 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. */}} --- apiVersion: gateway.networking.k8s.io/v1 kind: HTTPRoute metadata: name: {{ .Release.Name }}-{{ .Chart.Name }}-httproute spec: parentRefs: {{- toYaml $parentRefs | nindent 4 }} hostnames: - {{ $sharedHost | quote }} rules: {{- range $app := $shared }} - matches: - path: type: PathPrefix value: {{ $app.path | quote }} {{- if eq $app.name "qbittorrent" }} {{- /* qBittorrent has no base-path setting - it serves its WebUI at the root, so the prefix has to be stripped (https://github.com/qbittorrent/wiki/blob/master/NGINX-Reverse-Proxy-for-Web-UI.md). ReplacePrefixMatch replaces exactly the PathPrefix matched above, which is why the match must be a PathPrefix and not a regex. The apps around it must NOT be rewritten: their UrlBase carries the prefix, so they expect the full path. Under an Ingress that made a second object unavoidable, because rewrite-target applies per object; here the filter is scoped to this rule and the others are untouched by construction. */}} filters: - type: URLRewrite urlRewrite: path: type: ReplacePrefixMatch replacePrefixMatch: / {{- end }} backendRefs: - name: {{ $.Release.Name }}-{{ $.Chart.Name }}-{{ $app.name }}-service port: {{ int $app.port }} {{- end }} {{- end }} {{- end }}