2 Commits
Author SHA1 Message Date
jlab-renovate-bot 76322e4d1a Update l4rm4nd/vouchervault Docker tag to v1.30.3
renovate/stability-days Updates have met minimum release age requirement
Validate Helm Charts / Detect changed charts (pull_request) Successful in 4s
Validate Helm Charts / Resource quantities (pull_request) Successful in 15s
Validate Helm Charts / Selector match (pull_request) Successful in 13s
Validate Helm Charts / HTTPRoute match (pull_request) Successful in 13s
Validate Helm Charts / Build voucher-vault (pull_request) Successful in 6s
Validate Helm Charts / Trivy config voucher-vault (pull_request) Successful in 23s
Install-test / Install-test voucher-vault (pull_request) Successful in 2m18s
Validate Helm Charts / Install-test (pull_request) Successful in 2m18s
2026-10-12 18:05:47 +00:00
jklein 33d7cedbab feat: Gateway-API as an alternative to the Ingress in eight charts
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 / Build bentopdf (pull_request) Successful in 9s
Validate Helm Charts / HTTPRoute match (pull_request) Successful in 17s
Validate Helm Charts / Trivy config bentopdf (pull_request) Successful in 19s
Validate Helm Charts / Build garage (pull_request) Successful in 8s
Validate Helm Charts / Build patchmon (pull_request) Successful in 7s
Validate Helm Charts / Build rallly (pull_request) Successful in 7s
Validate Helm Charts / Build searxng (pull_request) Successful in 6s
Validate Helm Charts / Build sure (pull_request) Successful in 7s
Validate Helm Charts / Build urlaubsverwaltung (pull_request) Successful in 6s
Validate Helm Charts / Trivy config garage (pull_request) Successful in 18s
Validate Helm Charts / Build voucher-vault (pull_request) Successful in 7s
Validate Helm Charts / Trivy config patchmon (pull_request) Successful in 16s
Validate Helm Charts / Trivy config rallly (pull_request) Successful in 18s
Install-test / Install-test bentopdf (pull_request) Successful in 1m10s
Validate Helm Charts / Trivy config searxng (pull_request) Successful in 22s
Validate Helm Charts / Trivy config sure (pull_request) Successful in 18s
Validate Helm Charts / Trivy config urlaubsverwaltung (pull_request) Successful in 17s
Validate Helm Charts / Trivy config voucher-vault (pull_request) Successful in 19s
Install-test / Install-test garage (pull_request) Successful in 1m17s
Install-test / Install-test patchmon (pull_request) Successful in 1m24s
Install-test / Install-test rallly (pull_request) Successful in 1m39s
Install-test / Install-test searxng (pull_request) Successful in 1m7s
Install-test / Install-test sure (pull_request) Successful in 2m39s
Install-test / Install-test voucher-vault (pull_request) Successful in 2m9s
Validate Helm Charts / Install-test (pull_request) Successful in 13m15s
Install-test / Install-test urlaubsverwaltung (pull_request) Successful in 1m50s
Publish BentoPDF Helm Chart / Publish BentoPDF Helm Chart (push) Successful in 10s
Publish Helm Chart with patched Application Version / publish-helm-chart (push) Successful in 9s
Publish Garage Helm Chart / Publish Garage Helm Chart (push) Successful in 9s
Publish Helm Chart with patched Application Version / publish-helm-chart (push) Successful in 9s
Publish Rallly Helm Chart / Publish Rallly Helm Chart (push) Successful in 10s
Publish Helm Chart with patched Application Version / publish-helm-chart (push) Successful in 10s
Publish SearXNG Helm Chart / Publish SearXNG Helm Chart (push) Successful in 10s
Publish Helm Chart with patched Application Version / publish-helm-chart (push) Successful in 9s
Publish Sure Helm Chart / Publish Sure Helm Chart (push) Successful in 10s
Publish Helm Chart with patched Application Version / publish-helm-chart (push) Successful in 10s
Publish Urlaubsverwaltung Helm Chart / Publish Urlaubsverwaltung Helm Chart (push) Successful in 10s
Publish Helm Chart with patched Application Version / publish-helm-chart (push) Successful in 10s
Publish Patchmon Helm Chart / Publish Patchmon Helm Chart (push) Successful in 9s
Publish Helm Chart with patched Application Version / publish-helm-chart (push) Successful in 10s
Publish VoucherVault Helm Chart / Publish VoucherVault Helm Chart (push) Successful in 10s
Publish Helm Chart with patched Application Version / publish-helm-chart (push) Successful in 10s
SonarQube Analysis / SonarQube Analysis (push) Successful in 43s
Group 1 of the #114 sweep: the charts that render exactly one Ingress object
and set no behaviour-bearing nginx annotation - bentopdf, garage, patchmon,
rallly, searxng, sure, urlaubsverwaltung, voucher-vault.

Each gets the form the pilot (cyberchef, PR #115) fixed and template-chart
carries: an httpRoute block (enabled/parentRefs/hostnames, off by default), a
template rendering ONLY an HTTPRoute with parentRefs - no Gateway, no
gatewayClassName, no Certificate, no implementation-specific policy object -
render aborts on both modes at once (naming both value paths), on an empty
parentRefs and on a missing hostname, empty hostnames meaning [ingress.domain],
a schema entry closed against unknown keys with an open parentRef definition,
and a NOTES.txt that names the active mode and what the mode costs.

Minor, additive, defaults unchanged: `helm template` with each chart's CI
values is byte-identical to origin/main for seven of the eight, and for sure
identical in every rendered value (the diff is two YAML comment lines).

Three findings that were not in the plan, each argued in the file it changes:

  sure derived RAILS_ASSUME_SSL/RAILS_FORCE_SSL from ingress.enabled alone.
  Gateway mode sets that false by construction while TLS is still terminated at
  the Gateway, so both flags would have gone false: insecure cookies, no HSTS,
  http:// links, with a Ready pod and a working UI as the only symptom. They now
  read sure.publishedOverTls (ingress.enabled OR httpRoute.enabled), which is
  ingress.enabled while httpRoute.enabled is false - hence still Minor.

  garage claimed, in its Ingress template and its schema, that nothing outside
  that template reads ingress.domain. The HTTPRoute reads it, so the claim now
  holds only while both modes are off. Corrected rather than left standing.

  garage also depends on an upload limit it does not set: its annotations
  comment tells the operator to set proxy-body-size: "0" because ingress-nginx
  truncates S3 uploads at 1 MB. proxy-body-size has no Gateway-API counterpart,
  so its Gateway-mode NOTES prints that the limit is the Gateway's. README's
  list of affected charts named five and now names garage as a sixth.

clusterIssuer needed no change in any of the eight: its only required() sits
inside the Ingress template behind ingress.enabled, the same no-op the pilot
found. Annotation guidance that stops applying in Gateway mode moved into the
Gateway branch of NOTES.txt for bentopdf (X-Robots-Tag) and searxng
(forward-auth).

CI: one render overlay per chart under ci/httproute/, so the assertion is not
vacuum-green - ci/run-httproute-match-test.sh goes from 4 held assertions over
2 charts to 20 over 10. The install-test fixtures stay in Ingress mode: the
install runs one configuration per chart, cyberchef already pays that price
against the real Gateway-API CRDs, and for garage the Ingress is the only place
its Service port is referenced by NAME.

Not proven, by construction: that traffic flows. No Gateway controller runs in
CI. Not run locally either: k3d, Docker, Trivy.

Part of #114
2026-10-12 16:06:18 +00:00
51 changed files with 2303 additions and 107 deletions

No files matched your search

+3 -2
View File
@@ -383,10 +383,11 @@ These guidelines are **binding for every current and future chart** in this repo
- **`parentRefs` is mandatory in Gateway mode**, enforced by a `fail` on the empty list. A route without a parent attaches to nothing and is simply inert — created, schema-valid, rejected by nobody, reported by nothing. - **`parentRefs` is mandatory in Gateway mode**, enforced by a `fail` on the empty list. A route without a parent attaches to nothing and is simply inert — created, schema-valid, rejected by nobody, reported by nothing.
- **One source of truth for the hostname.** Empty `hostnames` means `[ingress.domain]`; two values that have to agree drift apart. `ingress.domain` is therefore read in Gateway mode too — it names the release's public hostname and is not an Ingress switch. - **One source of truth for the hostname.** Empty `hostnames` means `[ingress.domain]`; two values that have to agree drift apart. `ingress.domain` is therefore read in Gateway mode too — it names the release's public hostname and is not an Ingress switch.
- **`NOTES.txt` says which mode is active**, as `keycloak` already does for its cases, and names what the mode costs. - **`NOTES.txt` says which mode is active**, as `keycloak` already does for its cases, and names what the mode costs.
- **What has no counterpart, and must be said rather than rebuilt.** The Gateway API has no annotation channel for controller behaviour. `rewrite-target` has a real equivalent (the `URLRewrite` filter in a rule); **`proxy-body-size` has none** — an upload limit is implementation-specific policy, so a chart that depends on one (`bulwark`, `calibre-web-automated`, `outline`, `paperless-ngx`, `zipline`) must print in its Gateway-mode `NOTES.txt` that the limit is **not enforced** and has to be set at the Gateway. The same applies to auth annotations in front of an app with no login of its own: in Gateway mode that escape hatch does not exist in the chart at all. - **What has no counterpart, and must be said rather than rebuilt.** The Gateway API has no annotation channel for controller behaviour. `rewrite-target` has a real equivalent (the `URLRewrite` filter in a rule); **`proxy-body-size` has none** — an upload limit is implementation-specific policy, so a chart that depends on one must print in its Gateway-mode `NOTES.txt` that the limit is **not enforced** and has to be set at the Gateway. Five charts *set* one by default (`bulwark`, `calibre-web-automated`, `outline`, `paperless-ngx`, `zipline`), and `garage` is a sixth that *depends* on one without setting it: its `ingress.annotations` comment tells the operator to set `proxy-body-size: "0"` because ingress-nginx's 1 MB default truncates S3 uploads. Measured while sweeping group 1 — "sets no behaviour-bearing annotation" is not the same question as "needs none", and the sweep screens on the second one. The same applies to auth annotations in front of an app with no login of its own: in Gateway mode that escape hatch does not exist in the chart at all (`cyberchef`, `searxng`), and so does any annotation a chart documents as its only lever for a response header (`bentopdf`: `X-Robots-Tag`).
- **A value derived from `ingress.enabled` is a second thing the sweep has to look for, and it is not visible in the Ingress template.** Gateway mode sets `ingress.enabled: false` *by construction*, so any value computed from it flips — and where it stood for "is there a TLS-terminating proxy in front", the flipped value is wrong while nothing fails. Found in `sure`, whose `RAILS_ASSUME_SSL`/`RAILS_FORCE_SSL` would have gone `false` with TLS terminated at the Gateway: insecure cookies, no HSTS, `http://` links, green pod. The fix is one helper reading `ingress.enabled OR httpRoute.enabled`, not a new value. Still open on this count: `freesailarr` and `ytdlp-web-player` both read `.Values.ingress.enabled` outside their Ingress templates.
- **CI**: either the chart's install-test fixture installs in Gateway mode, or it gets a render-only overlay at `ci/httproute/<chart>.yaml` — otherwise `ci/run-httproute-match-test.sh` is green over nothing for it, which that test fails on. - **CI**: either the chart's install-test fixture installs in Gateway mode, or it gets a render-only overlay at `ci/httproute/<chart>.yaml` — otherwise `ci/run-httproute-match-test.sh` is green over nothing for it, which that test fails on.
Reference implementation: `template-chart/templates/xxx.httproute.yaml`. Pilot: `cyberchef`. The remaining 20 charts follow in their own PRs, with the five special cases (`matrix-synapse` renders in a loop, `keycloak` and `bulwark` have two Ingresses each, `homeassistant` is the documented ingress exception, `garage` ships `ingress.enabled: false`, `stalwart` renders per node group) argued one at a time. Reference implementation: `template-chart/templates/xxx.httproute.yaml`. Pilot: `cyberchef`. Group 1 — the charts rendering one Ingress with no behaviour-bearing annotation — followed: `bentopdf`, `garage`, `patchmon`, `rallly`, `searxng`, `sure`, `urlaubsverwaltung`, `voucher-vault`. That leaves **twelve** charts plus `rss-bridge`, which is frozen for changes (#51) and is why #114 cannot be closed while #51 is open: group 2 is the annotation cases (`calibre-web-automated`, `outline`, `paperless-ngx`, `zipline`, `freesailarr`) and group 3 the special shapes (`homeassistant` is the documented ingress exception, `keycloak` and `bulwark` have two Ingresses each, `matrix-synapse` renders in a loop, `stalwart` renders per node group, `ytdlp-web-player` hangs off the oauth2-proxy subchart). `garage` was one of the named special cases and is done: it ships `ingress.enabled: false`, so both of its modes are off by default and its route is exercised by a render overlay rather than by the install-test, which keeps its Ingress.
- **Documented exception — freesailarr**: the chart bundles several independently exposed apps in one pod, so it has **no** `ingress.domain`. It offers both shapes. By default each app carries its own `<app>.ingress.enabled` plus a required `<app>.ingress.host` (required only while that app's ingress is enabled and it is not on the shared domain) and renders its own Ingress object `<release>-<chart>-<app>-ingress` with its own TLS secret `tls-<release>-<chart>-<app>-ingress`. Setting the global `ingress.host` switches every app without a host of its own to the shared-domain shape under `<app>.ingress.path`; qBittorrent (no base path, needs a prefix-stripping rewrite) gets the extra object described above, and seerr as well as mediathekarr are pinned to `/` — mediathekarr because its UI links assets with root-absolute paths, which a rewrite cannot fix; with both of them on one shared host the duplicate-path guard fires and one of them needs a host of its own (its ingress is therefore off by default). `clusterIssuer`, `className` and `annotations` stay global defaults under `ingress.` and are overridable per app. - **Documented exception — freesailarr**: the chart bundles several independently exposed apps in one pod, so it has **no** `ingress.domain`. It offers both shapes. By default each app carries its own `<app>.ingress.enabled` plus a required `<app>.ingress.host` (required only while that app's ingress is enabled and it is not on the shared domain) and renders its own Ingress object `<release>-<chart>-<app>-ingress` with its own TLS secret `tls-<release>-<chart>-<app>-ingress`. Setting the global `ingress.host` switches every app without a host of its own to the shared-domain shape under `<app>.ingress.path`; qBittorrent (no base path, needs a prefix-stripping rewrite) gets the extra object described above, and seerr as well as mediathekarr are pinned to `/` — mediathekarr because its UI links assets with root-absolute paths, which a rewrite cannot fix; with both of them on one shared host the duplicate-path guard fires and one of them needs a host of its own (its ingress is therefore off by default). `clusterIssuer`, `className` and `annotations` stay global defaults under `ingress.` and are overridable per app.
### 5. Security hardening ### 5. Security hardening
+1 -1
View File
@@ -5,7 +5,7 @@ icon: https://raw.githubusercontent.com/alam00000/bentopdf/main/public/images/fa
type: application type: application
version: 1.0.3+up2.8.8 version: 1.1.0+up2.8.8
appVersion: "2.8.8" appVersion: "2.8.8"
maintainers: maintainers:
+86 -12
View File
@@ -1,9 +1,14 @@
{{- $replicas := include "bentopdf.replicas" (dict "scaleDown" .Values.scaleDown "replicas" .Values.bentopdf.replicas) -}} {{- $replicas := include "bentopdf.replicas" (dict "scaleDown" .Values.scaleDown "replicas" .Values.bentopdf.replicas) -}}
{{- $httpRoute := .Values.httpRoute | default dict -}}
{{- $hostnames := $httpRoute.hostnames | default list -}}
{{- if not $hostnames -}}
{{- $hostnames = list .Values.ingress.domain -}}
{{- end -}}
BentoPDF {{ .Chart.AppVersion }} — release {{ .Release.Name }} in namespace {{ .Release.Namespace }}. BentoPDF {{ .Chart.AppVersion }} — release {{ .Release.Name }} in namespace {{ .Release.Namespace }}.
{{- if .Values.ingress.enabled }} {{- if .Values.ingress.enabled }}
ACCESS ACCESS — INGRESS MODE (ingress.enabled: true, httpRoute.enabled: false)
https://{{ .Values.ingress.domain }} https://{{ .Values.ingress.domain }}
The certificate is requested by cert-manager through the "{{ .Values.ingress.clusterIssuer }}" The certificate is requested by cert-manager through the "{{ .Values.ingress.clusterIssuer }}"
@@ -13,24 +18,74 @@ ACCESS
This chart has no secrets and no volumes — the two ingress values above are This chart has no secrets and no volumes — the two ingress values above are
everything an installation needs. everything an installation needs.
{{- else if $httpRoute.enabled }}
ACCESS — GATEWAY MODE (httpRoute.enabled: true, ingress.enabled: false)
{{- range $hostnames }}
https://{{ . }}
{{- end }}
This release renders an HTTPRoute ({{ .Release.Name }}-{{ .Chart.Name }}-httproute) and NOTHING else
of the routing path — no Gateway, no class, no Certificate. Three things
therefore belong to the cluster and not to this chart, and if any of them is
missing the hostname above simply does not answer:
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 and nothing for cert-manager to do
on behalf of this release.
AND TWO THINGS THIS MODE TAKES AWAY FROM YOU, both because the Gateway API
has no annotation channel and this chart renders no implementation-specific
policy object on purpose:
* THE X-Robots-Tag HEADER. A publicly reachable instance gets indexed, and
its pages carry a canonical link to the vendor's site. In Ingress mode
ingress.annotations is the lever (see values.yaml); here there is none in
the chart at all — the header has to be added at the Gateway, or the
instance has to not be public.
* THE REPAIR FOR A PROXY THAT MANGLES RESPONSES. See the two notes below on
the cross-origin headers and on .mjs; in this mode neither can be fixed
from this chart.
{{- else }} {{- else }}
ACCESS — NO INGRESS ACCESS — NEITHER INGRESS NOR HTTPROUTE
ingress.enabled is false, so this release has no Ingress and no certificate; ingress.enabled and httpRoute.enabled are both false, so this release has no
ingress.domain and ingress.clusterIssuer are not needed. The app is only Ingress, no HTTPRoute and no certificate; ingress.domain and
reachable inside the cluster, as ingress.clusterIssuer are not needed. The app is only reachable inside the
cluster, as
{{ .Release.Name }}-{{ .Chart.Name }}-service:80. To open it locally: {{ .Release.Name }}-{{ .Chart.Name }}-service:80. To open it locally:
kubectl -n {{ .Release.Namespace }} port-forward svc/{{ .Release.Name }}-{{ .Chart.Name }}-service 8080:80 kubectl -n {{ .Release.Namespace }} port-forward svc/{{ .Release.Name }}-{{ .Chart.Name }}-service 8080:80
Then browse to http://localhost:8080. Set ingress.enabled=true together with Then browse to http://localhost:8080. To publish it instead, set either
ingress.domain and ingress.clusterIssuer to publish it instead. ingress.enabled=true with ingress.domain and ingress.clusterIssuer, or
httpRoute.enabled=true with ingress.domain and httpRoute.parentRefs.
{{- end }} {{- end }}
{{ if eq $replicas "0" }} {{ if eq $replicas "0" }}
SCALED TO ZERO SCALED TO ZERO
{{ if .Values.scaleDown }}scaleDown: true{{ else }}bentopdf.replicas: 0{{ end }} — the deployment runs with replicas: 0, so the endpoint {{ if .Values.scaleDown }}scaleDown: true{{ else }}bentopdf.replicas: 0{{ end }} — the deployment runs with replicas: 0, so the endpoint
above does not answer until you set it back and upgrade again. Service{{ if .Values.ingress.enabled }} and above does not answer 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 Ingress stay in place, 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 }} place.{{ end }}
{{ end }} {{ end }}
A GREEN POD ONLY PROVES DELIVERY A GREEN POD ONLY PROVES DELIVERY
@@ -40,21 +95,40 @@ A GREEN POD ONLY PROVES DELIVERY
handing out index.html — no more. Whether PDF processing works is a handing out index.html — no more. Whether PDF processing works is a
question about the browser, not about this release. question about the browser, not about this release.
TWO HEADERS HAVE TO SURVIVE YOUR INGRESS CONTROLLER TWO HEADERS HAVE TO SURVIVE YOUR PROXY
The image sends Cross-Origin-Opener-Policy: same-origin and The image sends Cross-Origin-Opener-Policy: same-origin and
Cross-Origin-Embedder-Policy: credentialless. Without both arriving at the Cross-Origin-Embedder-Policy: credentialless. Without both arriving at the
browser there is no SharedArrayBuffer, and the LibreOffice-backed browser there is no SharedArrayBuffer, and the LibreOffice-backed
conversions (Word, Excel, PowerPoint) fail while everything else keeps conversions (Word, Excel, PowerPoint) fail while everything else keeps
working. Check them through the ingress, not at the pod: working. Check them through the published URL, not at the pod:
curl -sI https://{{ if .Values.ingress.enabled }}{{ .Values.ingress.domain }}{{ else }}<host>{{ end }}/ | grep -i cross-origin curl -sI https://{{ if .Values.ingress.enabled }}{{ .Values.ingress.domain }}{{ else if $httpRoute.enabled }}{{ first $hostnames }}{{ else }}<host>{{ end }}/ | grep -i cross-origin
The image sends them itself, so they arrive in both modes unless something
in the path strips or rewrites them. The difference is the repair: in Ingress
mode ingress.annotations can put them back, in Gateway mode that channel does
not exist and it is the Gateway's business. Same for the .mjs MIME type (see
values.yaml).
ANYONE WHO REACHES THE URL CAN USE IT ANYONE WHO REACHES THE URL CAN USE IT
{{- if $httpRoute.enabled }}
BentoPDF has no login, and this chart adds none. That is defensible here —
the server takes nothing in, stores nothing and has no session — but a
public instance also gets indexed, and its pages carry a canonical link to
the vendor's site. In GATEWAY MODE there is no place in this chart for either
fix: the Gateway API has no annotation channel, so the X-Robots-Tag snippet
documented in values.yaml has NO counterpart here, and the chart renders no
implementation-specific policy object on purpose (it would bind the chart to
one Gateway implementation). Both have to happen at the Gateway — or the
instance must not be publicly reachable.
{{- else }}
BentoPDF has no login, and this chart adds none. That is defensible here — BentoPDF has no login, and this chart adds none. That is defensible here —
the server takes nothing in, stores nothing and has no session — but a the server takes nothing in, stores nothing and has no session — but a
public instance also gets indexed, and its pages carry a canonical link to public instance also gets indexed, and its pages carry a canonical link to
the vendor's site. Both are addressed through ingress.annotations; see the vendor's site. Both are addressed through ingress.annotations; see
values.yaml. values.yaml. Note that this escape hatch exists in Ingress mode only:
httpRoute.enabled has no annotation channel at all.
{{- end }}
NOTHING TO BACK UP, NOTHING TO MIGRATE NOTHING TO BACK UP, NOTHING TO MIGRATE
No database, no volumes, no uploads: documents never leave the visitor's No database, no volumes, no uploads: documents never leave the visitor's
@@ -0,0 +1,90 @@
{{/*
The Gateway-API alternative to bentopdf.ingress.yaml (Gitea #114). Form and
reasoning: template-chart/templates/xxx.httproute.yaml and README, "Ingress and
Gateway API".
Only an HTTPRoute 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.
WHAT THIS CHART IN PARTICULAR LOSES IN THIS MODE, AND IT IS NOT NOTHING
ingress.annotations is BentoPDF's only lever for two things it cannot do
itself, and the Gateway API has no annotation channel at all:
* the X-Robots-Tag header that keeps a public instance out of search
indexes (ROBOTS_NOINDEX cannot run on a read-only root filesystem, and
every page carries a canonical link to the vendor's domain), and
* the repair for controllers that override the MIME type of .mjs files or
strip Cross-Origin-Opener-Policy / Cross-Origin-Embedder-Policy.
None of that is rebuilt here - a policy object would bind the chart to one
implementation. NOTES.txt says so in this mode instead of leaving it to be
discovered on a search-results page. The two cross-origin headers are sent by
the image itself and still arrive unless something strips them, so they are a
thing to CHECK (see NOTES.txt) rather than a thing to configure.
ingress.clusterIssuer is meaningless here - this chart renders no Certificate
in this mode - and it is not demanded either: its only required() sits inside
bentopdf.ingress.yaml, behind ingress.enabled.
*/}}
{{- /*
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 all three 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 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. A migration runs over a SECOND hostname (set httpRoute.hostnames explicitly while the Ingress keeps ingress.domain), never over parallel operation of both." -}}
{{- 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 -}}
{{- /*
One source of truth for the hostname: an empty httpRoute.hostnames means
[ingress.domain], so the two cannot drift apart. ingress.domain is read here
even though no Ingress is rendered in this mode - the value names the public
hostname of this release, which both modes need; it is not an Ingress switch.
Checked for THIS chart rather than copied: nothing outside bentopdf.ingress.yaml
and this file reads ingress.domain - BentoPDF is a static bundle and is handed
no base URL - so with both routing modes off, domain is genuinely free. In
Gateway mode this required() is the one that fires.
*/ -}}
{{- $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 one
# backend Service - BentoPDF is served at the root and has no second path.
# 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 and the 80 below has to track
# bentopdf.service.yaml by hand. ci/run-httproute-match-test.sh is what
# holds that pair - it 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 }}-service
port: 80
{{- end }}
+31 -1
View File
@@ -49,6 +49,7 @@
"additionalProperties": false, "additionalProperties": false,
"properties": { "properties": {
"enabled": { "enabled": {
"description": "Gates the whole Ingress template. Mutually exclusive with httpRoute.enabled - the render aborts when both are true. It does not free ingress.domain by itself: the HTTPRoute reads that value too, so domain is only unnecessary while BOTH routing modes are off. clusterIssuer is read by the Ingress alone.",
"type": "boolean" "type": "boolean"
}, },
"clusterIssuer": { "clusterIssuer": {
@@ -61,13 +62,42 @@
"type": ["string", "null"] "type": ["string", "null"]
}, },
"annotations": { "annotations": {
"description": "Extra Ingress annotations, merged with the cert-manager annotation the chart always sets. Free-form keys by nature.", "description": "Extra Ingress annotations, merged with the cert-manager annotation the chart always sets. Free-form keys by nature. No counterpart in Gateway mode: the Gateway API has no annotation channel, so these are gone when httpRoute.enabled is used instead - including the X-Robots-Tag snippet that is this chart's only lever against being indexed.",
"type": "object" "type": "object"
} }
} }
},
"httpRoute": {
"description": "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; class and certificate sit on the Gateway and belong to the platform. The Gateway-API CRDs must be present in the cluster, because the chart cannot detect them.",
"type": "object",
"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.",
"type": "array",
"items": {
"type": "string"
}
}
}
} }
}, },
"definitions": { "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": { "resources": {
"description": "May be null / empty: the chart then falls back to its default requests and computed limits (issue #99) instead of rendering a container without resource accounting.", "description": "May be null / empty: the chart then falls back to its default requests and computed limits (issue #99) instead of rendering a container without resource accounting.",
"type": ["object", "null"], "type": ["object", "null"],
+59
View File
@@ -174,13 +174,30 @@ ingress:
# read inside the Ingress template - unlike searxng, this chart has no # read inside the Ingress template - unlike searxng, this chart has no
# second reader of ingress.domain: the upstream counterpart BASE_URL is a # second reader of ingress.domain: the upstream counterpart BASE_URL is a
# build-time argument and empty in the published image). # build-time argument and empty in the published image).
#
# Mutually exclusive with httpRoute.enabled below (Gitea #114): with both
# true the render aborts, naming both values. In Gateway mode ingress.domain
# is read again - by the HTTPRoute, as its single hostname - so "both modes
# off" is what frees it, not "ingress off".
enabled: true enabled: true
clusterIssuer: null clusterIssuer: null
# The public hostname of this release. Read by the Ingress AND - when
# httpRoute.hostnames is empty - by the HTTPRoute, so it stays the single
# source of truth for the hostname in both modes.
domain: null domain: null
className: nginx className: nginx
# Extra annotations for the Ingress; merged with the cert-manager # Extra annotations for the Ingress; merged with the cert-manager
# cluster-issuer annotation the chart always sets. # cluster-issuer annotation the chart always sets.
# #
# THESE HAVE NO COUNTERPART IN GATEWAY MODE, AND FOR THIS CHART THAT COSTS
# SOMETHING CONCRETE. The Gateway API has no annotation channel for
# controller behaviour, so both uses documented below - the X-Robots-Tag
# header and any repair for a controller that mangles the headers or the
# .mjs MIME type - are simply gone when httpRoute.enabled is used instead.
# The chart does not rebuild them: a policy object would bind it to one
# Gateway implementation. It is then the Gateway's business. NOTES.txt says
# so in that mode.
#
# TWO HEADERS MUST SURVIVE THE PROXY, OR OFFICE CONVERSION BREAKS. The image # TWO HEADERS MUST SURVIVE THE PROXY, OR OFFICE CONVERSION BREAKS. The image
# sends Cross-Origin-Opener-Policy: same-origin and # sends Cross-Origin-Opener-Policy: same-origin and
# Cross-Origin-Embedder-Policy: credentialless. Together they make the page # Cross-Origin-Embedder-Policy: credentialless. Together they make the page
@@ -212,6 +229,48 @@ ingress:
# add-headers ConfigMap is the alternative. # add-headers ConfigMap is the alternative.
annotations: {} annotations: {}
# Gateway-API alternative to the Ingress above (Gitea #114). Off by default, so
# an existing release is unaffected; exactly one of ingress.enabled and
# httpRoute.enabled may be true.
#
# The chart renders ONLY an HTTPRoute with parentRefs - no Gateway, no
# gatewayClassName, no Certificate and no implementation-specific policy
# object. Class and certificate belong to the Gateway and therefore to the
# platform, which is what keeps this chart independent of whether the cluster
# runs nginx-gateway-fabric, Cilium, Envoy Gateway or Istio.
#
# The Gateway-API CRDs must be installed in the cluster: the chart does not and
# cannot detect them (.Capabilities.APIVersions is unreliable at template
# time), so with httpRoute.enabled: true and no CRDs the INSTALL fails.
#
# WHAT THIS MODE COSTS HERE: the whole ingress.annotations escape hatch above,
# including the X-Robots-Tag header - a public instance in Gateway mode gets
# indexed unless the Gateway adds the header. The two cross-origin headers are
# a different case: the image sends them itself, so they still arrive in this
# mode unless something in the path strips them (NOTES.txt has the curl).
httpRoute:
enabled: false
# The Gateway(s) this route attaches to - a platform object, usually in
# another namespace, which must allow this namespace in its listener's
# allowedRoutes. One entry per parent, the fields 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: the HTTPRoute takes a second
# hostname while the Ingress keeps ingress.domain.
hostnames: []
# WHAT THIS CHART CANNOT DO # WHAT THIS CHART CANNOT DO
# #
# Almost everything else about BentoPDF is a BUILD-TIME argument and therefore # Almost everything else about BentoPDF is a BUILD-TIME argument and therefore
+31
View File
@@ -0,0 +1,31 @@
# 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.
#
# WHY AN OVERLAY AND NOT A CHANGE TO ci/bentopdf/test-values.yaml, which is
# what the pilot did for cyberchef: the install-test runs exactly ONE
# configuration per chart, and the two modes are mutually exclusive. cyberchef
# already pays that price and gives the install-test its HTTPRoute against the
# real Gateway-API CRDs. Spending it a second time here would drop this chart's
# Ingress from the install-test for a schema this repository now exercises
# anyway. So the install keeps testing the Ingress; the route is held at render
# level, which is where its two silent failure modes live.
#
# ingress.enabled: false is not optional here. The two modes are mutually
# exclusive and the chart aborts its own render when both are true, so an
# overlay that only switched httpRoute on would turn this test red.
#
# ingress.domain comes from ci/bentopdf/test-values.yaml, which this overlay
# sits on top of - httpRoute.hostnames is left empty on purpose, so the
# fallback to ingress.domain is what the test exercises.
ingress:
enabled: false
httpRoute:
enabled: true
parentRefs:
- name: public
namespace: gateway
sectionName: https
+35
View File
@@ -0,0 +1,35 @@
# 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.
#
# THIS CHART IS THE ONE WHERE THE OVERLAY EARNS ITS KEEP TWICE OVER, and the
# reason is in ci/garage/test-values.yaml: that fixture keeps the Ingress ON
# deliberately, so the install-test proves the Service port it names BY NAME
# ("s3") exists. An HTTPRoute cannot make that reference - backendRefs takes a
# PortNumber - so garage is the one chart in the sweep whose route renders a
# port NUMBER out of a VALUE (garage.ports.s3), not a literal. Switching the
# install fixture into Gateway mode would have traded a check that catches a
# renamed port for one that does not, and would have given up garage's only
# Ingress coverage at the same time. Here the number is held instead by this
# test, which fails when a backendRef names a port the same render's Service
# does not expose - exactly the drift the value introduces.
#
# ingress.enabled: false is not optional here. The two modes are mutually
# exclusive and the chart aborts its own render when both are true, and this
# chart's fixture has the Ingress on, so the overlay must switch it off.
#
# ingress.domain comes from ci/garage/test-values.yaml. Note that garage ships
# ingress.enabled: false AND httpRoute.enabled: false, so without this overlay
# the chart renders no route at all - which is correct and is why the overlay
# exists rather than a changed default.
ingress:
enabled: false
httpRoute:
enabled: true
parentRefs:
- name: public
namespace: gateway
sectionName: https
+29
View File
@@ -0,0 +1,29 @@
# 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/patchmon/test-values.yaml: the install-test
# runs exactly ONE configuration per chart and the two modes are mutually
# exclusive, so switching the fixture would drop this chart's Ingress from the
# install for a CRD schema cyberchef's fixture already exercises against the
# real Gateway-API CRDs. The install keeps the Ingress; the route is held here,
# at render level, which is where its two silent failure modes live.
#
# ingress.enabled: false is not optional here - the chart aborts its own render
# when both modes are true.
#
# ingress.domain comes from ci/patchmon/test-values.yaml and keeps feeding
# CORS_ORIGIN and the probes' Host header in this mode too; httpRoute.hostnames
# is left empty on purpose, so the fallback to ingress.domain is what the test
# exercises - and the two therefore cannot disagree here.
ingress:
enabled: false
httpRoute:
enabled: true
parentRefs:
- name: public
namespace: gateway
sectionName: https
+27
View File
@@ -0,0 +1,27 @@
# 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/rallly/test-values.yaml: the install-test
# runs exactly ONE configuration per chart and the two modes are mutually
# exclusive, so switching the fixture would drop this chart's Ingress from the
# install for a CRD schema cyberchef's fixture already exercises against the
# real Gateway-API CRDs.
#
# ingress.enabled: false is not optional here - the chart aborts its own render
# when both modes are true.
#
# ingress.domain comes from ci/rallly/test-values.yaml and keeps feeding
# NEXT_PUBLIC_BASE_URL in this mode too; httpRoute.hostnames is left empty on
# purpose, so the fallback to ingress.domain is what the test exercises.
ingress:
enabled: false
httpRoute:
enabled: true
parentRefs:
- name: public
namespace: gateway
sectionName: https
+27
View File
@@ -0,0 +1,27 @@
# 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/searxng/test-values.yaml: the install-test
# runs exactly ONE configuration per chart and the two modes are mutually
# exclusive, so switching the fixture would drop this chart's Ingress from the
# install for a CRD schema cyberchef's fixture already exercises against the
# real Gateway-API CRDs.
#
# ingress.enabled: false is not optional here - the chart aborts its own render
# when both modes are true.
#
# ingress.domain comes from ci/searxng/test-values.yaml and keeps feeding
# SEARXNG_BASE_URL in this mode too; httpRoute.hostnames is left empty on
# purpose, so the fallback to ingress.domain is what the test exercises.
ingress:
enabled: false
httpRoute:
enabled: true
parentRefs:
- name: public
namespace: gateway
sectionName: https
+33
View File
@@ -0,0 +1,33 @@
# 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/sure/test-values.yaml: the install-test
# runs exactly ONE configuration per chart and the two modes are mutually
# exclusive, so switching the fixture would drop this chart's Ingress from the
# install for a CRD schema cyberchef's fixture already exercises against the
# real Gateway-API CRDs.
#
# ingress.enabled: false is not optional here - the chart aborts its own render
# when both modes are true. For THIS chart the combination also exercises
# sure.publishedOverTls: with the Ingress off and the route on, the render must
# still produce RAILS_ASSUME_SSL/RAILS_FORCE_SSL as "true", because TLS is
# terminated at the Gateway. That is not asserted by this test - it only looks
# at HTTPRoutes - but it is rendered here, so a regression shows up in the
# manifest this job parses rather than only in a cluster.
#
# ingress.domain comes from ci/sure/test-values.yaml and keeps feeding
# APP_DOMAIN, WEBAUTHN_RP_ID and WEBAUTHN_ALLOWED_ORIGINS in this mode too;
# httpRoute.hostnames is left empty on purpose, so the fallback to
# ingress.domain is what the test exercises.
ingress:
enabled: false
httpRoute:
enabled: true
parentRefs:
- name: public
namespace: gateway
sectionName: https
+28
View File
@@ -0,0 +1,28 @@
# 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/urlaubsverwaltung/test-values.yaml: the
# install-test runs exactly ONE configuration per chart and the two modes are
# mutually exclusive, so switching the fixture would drop this chart's Ingress
# from the install for a CRD schema cyberchef's fixture already exercises
# against the real Gateway-API CRDs.
#
# ingress.enabled: false is not optional here - the chart aborts its own render
# when both modes are true.
#
# ingress.domain comes from ci/urlaubsverwaltung/test-values.yaml. This is the
# one chart in the group where nothing but the routing templates reads it, so
# with the overlay applied the HTTPRoute's required() is the only thing
# demanding it - which is exactly the path worth rendering.
ingress:
enabled: false
httpRoute:
enabled: true
parentRefs:
- name: public
namespace: gateway
sectionName: https
+29
View File
@@ -0,0 +1,29 @@
# 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/voucher-vault/test-values.yaml: the
# install-test runs exactly ONE configuration per chart and the two modes are
# mutually exclusive, so switching the fixture would drop this chart's Ingress
# from the install for a CRD schema cyberchef's fixture already exercises
# against the real Gateway-API CRDs.
#
# ingress.enabled: false is not optional here - the chart aborts its own render
# when both modes are true.
#
# ingress.domain comes from ci/voucher-vault/test-values.yaml and keeps feeding
# Django's DOMAIN (and through it ALLOWED_HOSTS) in this mode too;
# httpRoute.hostnames is left empty on purpose, so the fallback to
# ingress.domain is what the test exercises - and the two therefore cannot
# disagree here, which is the one way this chart fails invisibly.
ingress:
enabled: false
httpRoute:
enabled: true
parentRefs:
- name: public
namespace: gateway
sectionName: https
+1 -1
View File
@@ -19,7 +19,7 @@ type: application
# offer those as "updates", which is why the garage block in renovate.json5 # offer those as "updates", which is why the garage block in renovate.json5
# carries a versioningTemplate anchored to ^v<major>.<minor>.<patch>$ - same # carries a versioningTemplate anchored to ^v<major>.<minor>.<patch>$ - same
# shape as the stalwart block, for the same reason. # shape as the stalwart block, for the same reason.
version: 1.0.0+upv2.4.1 version: 1.1.0+upv2.4.1
appVersion: "v2.4.1" appVersion: "v2.4.1"
maintainers: maintainers:
+63 -1
View File
@@ -1,6 +1,11 @@
{{- $settings := fromYaml (include "garage.settings" .) -}} {{- $settings := fromYaml (include "garage.settings" .) -}}
{{- $pod := printf "%s-%s-sfs-0" .Release.Name .Chart.Name -}} {{- $pod := printf "%s-%s-sfs-0" .Release.Name .Chart.Name -}}
{{- $svc := printf "%s-%s-service" .Release.Name .Chart.Name -}} {{- $svc := printf "%s-%s-service" .Release.Name .Chart.Name -}}
{{- $httpRoute := .Values.httpRoute | default dict -}}
{{- $hostnames := $httpRoute.hostnames | default list -}}
{{- if not $hostnames -}}
{{- $hostnames = list .Values.ingress.domain -}}
{{- end -}}
Garage {{ .Chart.AppVersion }} installed as release {{ .Release.Name }} in namespace {{ .Release.Namespace }}. Garage {{ .Chart.AppVersion }} installed as release {{ .Release.Name }} in namespace {{ .Release.Namespace }}.
ONE node, replication_factor = {{ $settings.replicationFactor | int64 }}, metadata engine {{ $settings.dbEngine }}. ONE node, replication_factor = {{ $settings.replicationFactor | int64 }}, metadata engine {{ $settings.dbEngine }}.
@@ -9,7 +14,64 @@ S3 ENDPOINT (in-cluster, path-style addressing):
http://{{ $svc }}.{{ .Release.Namespace }}.svc.cluster.local:{{ $settings.ports.s3 | int64 }} http://{{ $svc }}.{{ .Release.Namespace }}.svc.cluster.local:{{ $settings.ports.s3 | int64 }}
region: {{ $settings.s3.region }} region: {{ $settings.s3.region }}
{{- if .Values.ingress.enabled }} {{- if .Values.ingress.enabled }}
published at: https://{{ .Values.ingress.domain }} published at: https://{{ .Values.ingress.domain }} (INGRESS MODE)
{{- else if $httpRoute.enabled }}
{{- range $hostnames }}
published at: https://{{ . }} (GATEWAY MODE)
{{- end }}
{{- end }}
{{- if .Values.ingress.enabled }}
PUBLISHED THROUGH AN INGRESS (ingress.enabled: true, httpRoute.enabled: false)
The certificate is requested by cert-manager through the "{{ .Values.ingress.clusterIssuer }}"
cluster issuer:
kubectl -n {{ .Release.Namespace }} get certificate tls-{{ .Release.Name }}-{{ .Chart.Name }}-ingress
The Ingress references the Service's s3 port BY NAME, so the port number
never has to be kept in step by hand here.
{{- else if $httpRoute.enabled }}
PUBLISHED THROUGH AN HTTPROUTE (httpRoute.enabled: true, ingress.enabled: false)
This release renders an HTTPRoute ({{ .Release.Name }}-{{ .Chart.Name }}-httproute) pointing at the
Service's s3 port ({{ $settings.ports.s3 | int64 }}) and NOTHING else of the routing path — no
Gateway, no class, no Certificate. So three things belong to the cluster and
not to this chart, and if any is missing the hostname above does not answer:
1. The Gateway-API CRDs. Without them this install would have failed.
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 shows
up 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 a working attachment. 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, so there is no
tls-{{ .Release.Name }}-{{ .Chart.Name }}-ingress secret and nothing for cert-manager to do.
YOUR UPLOAD LIMIT IS NOW THE GATEWAY'S, AND THIS IS AN OBJECT STORE.
In Ingress mode the advice is nginx.ingress.kubernetes.io/proxy-body-size: "0"
via ingress.annotations, because the controller's 1m default truncates
uploads. The Gateway API has NO counterpart — it knows no body size at all,
that is implementation-specific policy — and this chart deliberately renders
no policy object, which would bind it to one implementation. So whatever
limit the Gateway has is the limit your S3 PUTs get, and the symptom is
uploads failing by size rather than by permission. Raise it at the Gateway
or at its implementation's own policy object, next to the Gateway and not
here.
Mind also that the route carries the port NUMBER ({{ $settings.ports.s3 | int64 }}), not the name
"s3": an HTTPRoute backendRef takes a PortNumber. The chart renders it from
the same garage.ports.s3 value the Service uses, so the two cannot drift.
{{- end }} {{- end }}
{{- if $settings.web.enabled }} {{- if $settings.web.enabled }}
static website endpoint on port {{ $settings.ports.web | int64 }}{{ with $settings.web.rootDomain }} (root domain {{ . }}){{ else }} - WITHOUT a rootDomain it serves nothing; set garage.web.rootDomain{{ end }} static website endpoint on port {{ $settings.ports.web | int64 }}{{ with $settings.web.rootDomain }} (root domain {{ . }}){{ else }} - WITHOUT a rootDomain it serves nothing; set garage.web.rootDomain{{ end }}
+105
View File
@@ -0,0 +1,105 @@
{{/*
The Gateway-API alternative to garage.ingress.yaml (Gitea #114). Form and
reasoning: template-chart/templates/xxx.httproute.yaml and README, "Ingress and
Gateway API".
Only an HTTPRoute 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.
OFF BY DEFAULT, AND SO IS THE INGRESS. This chart publishes nothing unless
asked (ingress.enabled: false, httpRoute.enabled: false), because the consumer
it was built for speaks S3 inside the cluster. Both routing modes are therefore
a values change, and both expose THE S3 API - not a web UI. Garage's admin API
is JSON and its authenticated half is closed in this chart, and the project's
web interface is a separate program that is not in this image. Whoever turns
either mode on is putting an S3 endpoint on the internet; it authenticates
every request with a signature, and the credentials are the two fields of the
s3 vault item.
WHAT CHANGES FOR ingress.domain, AND IT IS THE ONE CLAIM THIS FILE INVALIDATES
garage.ingress.yaml used to be able to say that NOTHING outside it reads
ingress.domain, so that switching the Ingress off freed domain as well as
clusterIssuer. This file is now a second reader (when httpRoute.hostnames is
empty), so the sentence holds only while BOTH modes are off. That is corrected
in that template's header and in values.schema.json rather than left standing.
Unchanged: garage.s3.rootDomain is still its own value and still not coupled to
ingress.domain. A wildcard vhost suffix for bucket addressing and the single
host a route answers on are different things, and the Gateway API does not make
them the same.
ingress.clusterIssuer is meaningless here - this chart renders no Certificate
in this mode - and it is not demanded either: its only required() sits inside
garage.ingress.yaml, behind ingress.enabled.
*/}}
{{- /*
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 all three 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 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. A migration runs over a SECOND hostname (set httpRoute.hostnames explicitly while the Ingress keeps ingress.domain), never over parallel operation of both." -}}
{{- end -}}
{{- if $httpRoute.enabled -}}
{{- $settings := fromYaml (include "garage.settings" .) -}}
{{- $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], so the two cannot drift apart. ingress.domain is read here
even though no Ingress is rendered in this mode - the value names the public
hostname of this release, which both modes need; it is not an Ingress switch.
*/ -}}
{{- $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
labels:
app: {{ .Release.Name }}-{{ .Chart.Name }}-sfs
spec:
parentRefs:
{{- toYaml $parentRefs | nindent 4 }}
hostnames:
{{- toYaml $hostnames | nindent 4 }}
rules:
# The Ingress counterpart is `path: / , pathType: Prefix` on the S3 port of
# the one Service.
#
# THE PORT IS WHERE THIS CHART DIFFERS FROM EVERY OTHER ONE IN THE SWEEP.
# backendRefs takes a PortNumber and cannot name a port, while
# garage.ingress.yaml references it BY NAME ("s3") and is therefore immune
# to the number changing. Here the number has to be rendered - and
# garage.ports.s3 is a value, so a literal 3900 would be a lie the moment
# anyone changes it. It comes out of the same garage.settings helper that
# garage.service.yaml renders its port from, which is what keeps the two
# in step by construction instead of by hand.
#
# The admin port is deliberately NOT routed: with this chart's config it
# serves /health and /metrics without authentication and answers everything
# else 401/403. That is a monitoring surface for the cluster, not something
# to publish.
#
# ci/run-httproute-match-test.sh holds the pair either way - it 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 }}-service
port: {{ $settings.ports.s3 | int64 }}
{{- end }}
+11 -5
View File
@@ -5,11 +5,17 @@ values exist anyway, built to this repo's ingress convention, so that
publishing the endpoint later is a values change rather than a chart change. publishing the endpoint later is a values change rather than a chart change.
GUIDELINE 1, CHECKED IN BOTH DIRECTIONS (README, "Values that must not lie"): GUIDELINE 1, CHECKED IN BOTH DIRECTIONS (README, "Values that must not lie"):
nothing outside this template reads ingress.domain. The S3 API's vhost-style the S3 API's vhost-style suffix is its own value, garage.s3.rootDomain, because
suffix is its own value, garage.s3.rootDomain, because the two are not the same the two are not the same thing - a wildcard vhost domain and the single host a
thing - a wildcard vhost domain and the single host an Ingress answers on. So route answers on.
with the Ingress switched off, BOTH domain and clusterIssuer are free, and
their required() lives here where it can actually fire. This template used to be able to add "and nothing outside it reads
ingress.domain", so that switching the Ingress off freed domain as well as
clusterIssuer. SINCE #114 THAT IS NO LONGER TRUE: garage.httproute.yaml reads
ingress.domain too, as its single hostname when httpRoute.hostnames is empty.
So domain is free only while BOTH routing modes are off. clusterIssuer is
unchanged - it is read nowhere but here, Gateway mode renders no Certificate,
and its required() below still lives where it can actually fire.
WHAT IT PUBLISHES: the S3 API. There is no web UI to expose - Garage's admin WHAT IT PUBLISHES: the S3 API. There is no web UI to expose - Garage's admin
API is JSON and the authenticated half is closed in this chart (no token), and API is JSON and the authenticated half is closed in this chart (no token), and
+32 -3
View File
@@ -114,7 +114,7 @@
"minLength": 1 "minLength": 1
}, },
"rootDomain": { "rootDomain": {
"description": "Optional suffix for vhost-style bucket access. Deliberately not coupled to ingress.domain - the two are different things.", "description": "Optional suffix for vhost-style bucket access. Deliberately not coupled to ingress.domain or httpRoute.hostnames - a wildcard vhost domain and the single host a route answers on are different things.",
"type": ["string", "null"] "type": ["string", "null"]
} }
} }
@@ -182,7 +182,7 @@
"additionalProperties": false, "additionalProperties": false,
"properties": { "properties": {
"enabled": { "enabled": {
"description": "Gates the whole Ingress template (issue #93). Checked in both directions for this chart: nothing outside that template reads ingress.domain, so switching the Ingress off frees domain as well as clusterIssuer.", "description": "Gates the whole Ingress template (issue #93), and ships false. Mutually exclusive with httpRoute.enabled - the render aborts when both are true. Checked in both directions for this chart, and the answer moved with Gitea #114: the HTTPRoute reads ingress.domain too (as its single hostname), so domain is free only while BOTH routing modes are off; clusterIssuer is read by the Ingress alone and ingress.enabled: false still frees it.",
"type": "boolean" "type": "boolean"
}, },
"clusterIssuer": { "clusterIssuer": {
@@ -195,13 +195,42 @@
"type": ["string", "null"] "type": ["string", "null"]
}, },
"annotations": { "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. No counterpart in Gateway mode: the Gateway API has no annotation channel and no notion of a body size, so the proxy-body-size an S3 endpoint needs cannot be set here when httpRoute.enabled is used - it has to be raised at the Gateway.",
"type": "object" "type": "object"
} }
} }
},
"httpRoute": {
"description": "Gateway-API alternative to the Ingress (Gitea #114), and off by default like the Ingress here. Mutually exclusive with ingress.enabled - the render aborts when both are true. The chart renders only an HTTPRoute with parentRefs, pointing at the Service's s3 port; class and certificate sit on the Gateway and belong to the platform. The Gateway-API CRDs must be present in the cluster, because the chart cannot detect them.",
"type": "object",
"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. A wildcard for vhost-style bucket access goes here, alongside garage.s3.rootDomain.",
"type": "array",
"items": {
"type": "string"
}
}
}
} }
}, },
"definitions": { "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"]
},
"port": { "port": {
"type": ["integer", "null"], "type": ["integer", "null"],
"minimum": 1, "minimum": 1,
+71 -5
View File
@@ -421,11 +421,21 @@ ingress:
# there is nothing to publish. The values exist, built to this repo's # there is nothing to publish. The values exist, built to this repo's
# convention, so that publishing the endpoint later is a values change. # convention, so that publishing the endpoint later is a values change.
# #
# Guideline 1, checked in both directions: nothing outside the Ingress # Mutually exclusive with httpRoute.enabled below (Gitea #114): with both
# template reads ingress.domain (the vhost suffix is its own value, # true the render aborts, naming both values. Both ship off, so this chart
# garage.s3.rootDomain), so switching the Ingress off frees BOTH domain and # publishes nothing until asked - in either mode.
# clusterIssuer. With it on, both are required and the required() fires in #
# the template. # Guideline 1, checked in both directions, and the answer MOVED with #114:
# the Ingress template is no longer the only reader of ingress.domain - the
# HTTPRoute reads it too, as its single hostname, when httpRoute.hostnames is
# empty. So domain is free only while BOTH routing modes are off; with either
# one on it is required, and the required() fires in whichever template is
# rendering. clusterIssuer is unchanged: it is read only by the Ingress, so
# ingress.enabled: false frees it in every case - Gateway mode renders no
# Certificate at all.
#
# Unchanged as well: the vhost suffix is its own value, garage.s3.rootDomain,
# and is still deliberately not coupled to ingress.domain.
enabled: false enabled: false
clusterIssuer: null clusterIssuer: null
domain: null domain: null
@@ -434,4 +444,60 @@ ingress:
# cluster-issuer annotation the chart always sets. An S3 endpoint behind # cluster-issuer annotation the chart always sets. An S3 endpoint behind
# nginx usually wants nginx.ingress.kubernetes.io/proxy-body-size: "0" - # nginx usually wants nginx.ingress.kubernetes.io/proxy-body-size: "0" -
# without it the controller's 1m default truncates uploads. # without it the controller's 1m default truncates uploads.
#
# THAT ONE HAS NO COUNTERPART IN GATEWAY MODE. The Gateway API has no
# annotation channel and no notion of a body size at all - an upload limit is
# implementation-specific policy - so an S3 endpoint published through an
# HTTPRoute silently inherits the Gateway's limit, whatever it is. For an
# object store that is not a detail: it is where PUTs start failing by size.
# The chart does not rebuild it (a policy object would bind it to one
# implementation); NOTES.txt prints the warning in that mode and the limit
# has to be raised at the Gateway.
annotations: {} annotations: {}
# Gateway-API alternative to the Ingress above (Gitea #114). Off by default -
# like the Ingress here, so this chart still publishes nothing unless asked.
# Exactly one of ingress.enabled and httpRoute.enabled may be true.
#
# The chart renders ONLY an HTTPRoute with parentRefs - no Gateway, no
# gatewayClassName, no Certificate and no implementation-specific policy
# object. Class and certificate belong to the Gateway and therefore to the
# platform, which is what keeps this chart independent of whether the cluster
# runs nginx-gateway-fabric, Cilium, Envoy Gateway or Istio.
#
# The Gateway-API CRDs must be installed in the cluster: the chart does not and
# cannot detect them (.Capabilities.APIVersions is unreliable at template
# time), so with httpRoute.enabled: true and no CRDs the INSTALL fails.
#
# WHAT GETS PUBLISHED IS THE S3 API, in this mode exactly as in Ingress mode:
# the route points at the Service's s3 port and nothing else. The admin port is
# deliberately not routed. Read the Ingress comment above before turning either
# mode on - and read the upload-limit note, which is the one real regression of
# this mode for an object store.
httpRoute:
enabled: false
# The Gateway(s) this route attaches to - a platform object, usually in
# another namespace, which must allow this namespace in its listener's
# allowedRoutes. One entry per parent, the fields 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: the HTTPRoute takes a second
# hostname while the Ingress keeps ingress.domain.
#
# For vhost-style bucket access this is where the wildcard goes
# ("*.s3.example.com" alongside the plain host), together with
# garage.s3.rootDomain - the two still have to be set in step, as they do for
# the Ingress.
hostnames: []
+1 -1
View File
@@ -5,7 +5,7 @@ icon: https://raw.githubusercontent.com/PatchMon/PatchMon/main/frontend/public/a
type: application type: application
version: 7.0.2+up2.1.3 version: 7.1.0+up2.1.3
appVersion: "2.1.3" appVersion: "2.1.3"
maintainers: maintainers:
+55 -6
View File
@@ -4,21 +4,65 @@
{{- $startup := fromYaml (include "patchmon.server.startupProbe" (dict "given" .Values.patchmon.server.startupProbe "root" $)) -}} {{- $startup := fromYaml (include "patchmon.server.startupProbe" (dict "given" .Values.patchmon.server.startupProbe "root" $)) -}}
{{- $budget := mul (int $startup.periodSeconds) (int $startup.failureThreshold) -}} {{- $budget := mul (int $startup.periodSeconds) (int $startup.failureThreshold) -}}
{{- $name := printf "%s-%s" .Release.Name .Chart.Name -}} {{- $name := printf "%s-%s" .Release.Name .Chart.Name -}}
{{- $httpRoute := .Values.httpRoute | default dict -}}
{{- $hostnames := $httpRoute.hostnames | default list -}}
{{- if not $hostnames -}}
{{- $hostnames = list .Values.ingress.domain -}}
{{- end -}}
PatchMon {{ .Chart.AppVersion }} — release {{ .Release.Name }} in namespace {{ .Release.Namespace }}. PatchMon {{ .Chart.AppVersion }} — release {{ .Release.Name }} in namespace {{ .Release.Namespace }}.
ACCESS ACCESS
{{- if .Values.ingress.enabled }} {{- if .Values.ingress.enabled }}
INGRESS MODE (ingress.enabled: true, httpRoute.enabled: false)
https://{{ .Values.ingress.domain }} https://{{ .Values.ingress.domain }}
While cert-manager is still issuing the certificate the browser warns: While cert-manager is still issuing the certificate the browser warns:
kubectl -n {{ .Release.Namespace }} get certificate tls-{{ $name }}-ingress kubectl -n {{ .Release.Namespace }} get certificate tls-{{ $name }}-ingress
{{- else if $httpRoute.enabled }}
GATEWAY MODE (httpRoute.enabled: true, ingress.enabled: false)
{{- range $hostnames }}
https://{{ . }}
{{- end }}
This release renders an HTTPRoute ({{ $name }}-httproute) and NOTHING else of
the routing path — no Gateway, no class, no Certificate. Three things
therefore belong to the cluster and not to this chart:
1. The Gateway-API CRDs. Without them this install would have failed.
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 shows
up only in the route's status:
kubectl -n {{ .Release.Namespace }} get httproute {{ $name }}-httproute -o yaml
Read status.parents[].conditions — "Accepted: True" and
"ResolvedRefs: True" is a working attachment. 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, so there is no tls-{{ $name }}-ingress
secret and nothing for cert-manager to do.
ingress.domain is still required in this mode and is {{ .Values.ingress.domain }}: it builds
CORS_ORIGIN and the probes' Host header, neither of which the route replaces.
If you set httpRoute.hostnames to a name ingress.domain does not carry, the
server answers that name with a rejection — set both together.
ingress.annotations is inert in this mode: the Gateway API has no annotation
channel. Nothing this chart needs lives there, though — the WebSocket upgrade
works on a plain route, and TRUST_PROXY stays correct behind a Gateway.
{{- else }} {{- else }}
ingress.enabled is false, so there is no Ingress. Reach it directly with NEITHER INGRESS NOR HTTPROUTE — ingress.enabled and httpRoute.enabled are
both false, so this release is not published. Reach it directly with
kubectl -n {{ .Release.Namespace }} port-forward svc/{{ $name }}-service 3000:80 kubectl -n {{ .Release.Namespace }} port-forward svc/{{ $name }}-service 3000:80
ingress.domain stays required even without an Ingress — the server's ingress.domain stays required even without either — the server's
CORS_ORIGIN is built from it — and it is still {{ .Values.ingress.domain }} here. The server CORS_ORIGIN is built from it — and it is still {{ .Values.ingress.domain }} here. The server
answers 403 to any Host header it does not know, so address it under that answers 403 to any Host header it does not know, so address it under that
name rather than as plain localhost: name rather than as plain localhost:
@@ -54,10 +98,10 @@ SIGN-IN IS OIDC-ONLY — THERE IS NO LOCAL ACCOUNT TO FALL BACK ON
there is no built-in admin. If the OIDC item is wrong, nobody gets in — there is no built-in admin. If the OIDC item is wrong, nobody gets in —
including you. Users are created on first login with the "readonly" role; including you. Users are created on first login with the "readonly" role;
admin rights follow the group named in the item's admin-group-name field. admin rights follow the group named in the item's admin-group-name field.
The item's redirect-uri has to point back at this installation{{ if .Values.ingress.enabled }} under The item's redirect-uri has to point back at this installation{{ if or .Values.ingress.enabled $httpRoute.enabled }} under
https://{{ .Values.ingress.domain }}{{ end }}, and it must match what is registered at the https://{{ first $hostnames }}{{ end }}, and it must match what is registered at the
identity provider exactly. identity provider exactly.
{{- if .Values.ingress.enabled }} {{- if or .Values.ingress.enabled $httpRoute.enabled }}
A WRONG ingress.domain KEEPS THE POD OUT OF READY FOREVER A WRONG ingress.domain KEEPS THE POD OUT OF READY FOREVER
The 2.x server answers 403 to any request whose Host header is neither The 2.x server answers 403 to any request whose Host header is neither
@@ -66,6 +110,11 @@ A WRONG ingress.domain KEEPS THE POD OUT OF READY FOREVER
If that value is not the hostname the server is actually reached under, the If that value is not the hostname the server is actually reached under, the
probes get 403, the pod never turns Ready and nothing in the log looks like probes get 403, the pod never turns Ready and nothing in the log looks like
a configuration error — just repeated 403s. a configuration error — just repeated 403s.
{{- if $httpRoute.enabled }}
In GATEWAY MODE that applies to httpRoute.hostnames as well: the route
publishes {{ join ", " $hostnames }}, while CORS_ORIGIN and the probes still
follow ingress.domain ({{ .Values.ingress.domain }}). The two have to agree.
{{- end }}
{{- end }} {{- end }}
GIVE THE FIRST START TIME GIVE THE FIRST START TIME
@@ -94,7 +143,7 @@ GUACD SIDECAR IS ON
SCALED TO ZERO SCALED TO ZERO
{{ if .Values.scaleDown }}scaleDown: true{{ else }}patchmon.server.replicas: 0{{ end }} — the deployment runs with replicas: 0. Secrets, {{ if .Values.scaleDown }}scaleDown: true{{ else }}patchmon.server.replicas: 0{{ end }} — the deployment runs with replicas: 0. Secrets,
Service{{ if .Values.ingress.enabled }} and Ingress{{ end }} stay in place, and the agents on your hosts keep Service{{ if .Values.ingress.enabled }} and Ingress{{ else if $httpRoute.enabled }} and HTTPRoute{{ end }} stay in place, and the agents on your hosts keep
retrying, so nothing is lost; they simply report nothing until you set it retrying, so nothing is lost; they simply report nothing until you set it
back and upgrade again. back and upgrade again.
{{- end }} {{- end }}
@@ -0,0 +1,93 @@
{{/*
The Gateway-API alternative to patchmon.ingress.yaml (Gitea #114). Form and
reasoning: template-chart/templates/xxx.httproute.yaml and README, "Ingress and
Gateway API".
Only an HTTPRoute 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 is meaningless here - this chart renders no Certificate
in this mode - and it is not demanded either: its only required() sits inside
patchmon.ingress.yaml, behind ingress.enabled.
ingress.domain IS STILL REQUIRED, AND NOT BECAUSE OF THIS FILE
Checked for this chart rather than copied: ingress.domain is read twice more
outside both routing templates. CORS_ORIGIN in patchmon.deployment.yaml carries
its own required() and is therefore what makes domain mandatory in EVERY mode
of this chart, Gateway mode included - the fallback below is never the only
reader. The second read is the explicit "Host" header the probes send
(patchmon.server.httpGet in _helpers.tpl), which deliberately uses
`| default ""` and demands nothing itself, because the required() above has
already fired by the time it runs.
The consequence that matters in this mode is the one NOTES.txt already spells
out for the Ingress: the 2.x server rejects requests whose Host is neither
loopback nor a CORS_ORIGIN host, so a domain the app does not answer on keeps
the pod out of Ready forever. Nothing about that changes with a Gateway in
front instead of an Ingress - and TRUST_PROXY stays right either way, since a
Gateway is a reverse proxy too.
WEBSOCKETS: nothing to configure, in either mode. The 2.x server serves the
frontend, /api/* and its WebSocket upgrade from one process on one port, and
the Gateway API passes an upgrade through on a plain HTTPRoute - there is no
annotation involved in Ingress mode either.
*/}}
{{- /*
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 all three 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 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. A migration runs over a SECOND hostname (set httpRoute.hostnames explicitly while the Ingress keeps ingress.domain), never over parallel operation of both." -}}
{{- 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 -}}
{{- /*
One source of truth for the hostname: an empty httpRoute.hostnames means
[ingress.domain], so the two cannot drift apart. Setting httpRoute.hostnames
explicitly is what a migration over a second hostname needs - but mind that it
does NOT move CORS_ORIGIN or the probes' Host header, which keep following
ingress.domain. For this chart the two therefore have to be set together, or
the app rejects the browser's requests from the new name.
*/ -}}
{{- $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 one
# backend Service - the 2.x server serves the frontend and /api/* from one
# process, so a single path covers everything. 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 and the 80
# below has to track patchmon.service.yaml by hand.
# ci/run-httproute-match-test.sh is what holds that pair - it 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 }}-service
port: 80
{{- end }}
+31 -2
View File
@@ -91,7 +91,7 @@
"additionalProperties": false, "additionalProperties": false,
"properties": { "properties": {
"enabled": { "enabled": {
"description": "Gates the whole Ingress template (issue #93). false omits the Ingress, and the required domain/clusterIssuer values are then not needed either.", "description": "Gates the whole Ingress template (issue #93). Mutually exclusive with httpRoute.enabled - the render aborts when both are true. false omits the Ingress and makes clusterIssuer unnecessary - but NOT domain: that value also builds CORS_ORIGIN and the probes' Host header, keeps its own required() there and therefore stays mandatory in every mode (the earlier wording lumped the two together, which was wrong).",
"type": "boolean" "type": "boolean"
}, },
"clusterIssuer": { "clusterIssuer": {
@@ -104,11 +104,35 @@
"type": ["string", "null"] "type": ["string", "null"]
}, },
"annotations": { "annotations": {
"description": "Extra Ingress annotations, merged with the cert-manager annotation the chart always sets. Free-form keys by nature.", "description": "Extra Ingress annotations, merged with the cert-manager annotation the chart always sets. Free-form keys by nature. No counterpart in Gateway mode: the Gateway API has no annotation channel, so these are gone when httpRoute.enabled is used instead. Nothing the chart needs lives here - the WebSocket upgrade works on a plain route and TRUST_PROXY is an environment variable.",
"type": "object" "type": "object"
} }
} }
}, },
"httpRoute": {
"description": "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; class and certificate sit on the Gateway and belong to the platform. The Gateway-API CRDs must be present in the cluster, because the chart cannot detect them.",
"type": "object",
"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. A hostname set here does not move CORS_ORIGIN or the probes' Host header, which keep following ingress.domain.",
"type": "array",
"items": {
"type": "string"
}
}
}
},
"secrets": { "secrets": {
"type": "object", "type": "object",
"additionalProperties": false, "additionalProperties": false,
@@ -129,6 +153,11 @@
} }
}, },
"definitions": { "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": { "resources": {
"description": "May be null / empty: the chart then falls back to its default requests and computed limits (issue #99) instead of rendering a container without resource accounting.", "description": "May be null / empty: the chart then falls back to its default requests and computed limits (issue #99) instead of rendering a container without resource accounting.",
"type": ["object", "null"], "type": ["object", "null"],
+64 -3
View File
@@ -147,17 +147,78 @@ patchmon:
env: [] env: []
ingress: ingress:
# Set to false to omit the Ingress entirely (the required domain and # Set to false to omit the Ingress entirely. clusterIssuer is then not needed
# clusterIssuer values are then not needed either, because they are only # either, because it is only read inside the Ingress template.
# read inside the Ingress template). #
# ingress.domain is a different case and the comment here used to lump the
# two together, which was wrong: it is read twice MORE outside the Ingress -
# CORS_ORIGIN in the Deployment (with its own required()) and the explicit
# Host header the probes send - so it stays mandatory with the Ingress off.
# The 2.x server rejects requests whose Host is neither loopback nor a
# CORS_ORIGIN host, so an empty value keeps the pod out of Ready forever.
#
# Mutually exclusive with httpRoute.enabled below (Gitea #114): with both
# true the render aborts, naming both values.
enabled: true enabled: true
clusterIssuer: null clusterIssuer: null
# The public hostname of this release. Read by the Ingress, by CORS_ORIGIN,
# by the probes' Host header, and - when httpRoute.hostnames is empty - by
# the HTTPRoute. Required in every mode (see above).
domain: null domain: null
className: nginx className: nginx
# Extra annotations for the Ingress; merged with the cert-manager # Extra annotations for the Ingress; merged with the cert-manager
# cluster-issuer annotation the chart always sets. # cluster-issuer annotation the chart always sets.
#
# THESE HAVE NO COUNTERPART IN GATEWAY MODE. The Gateway API has no
# annotation channel for controller behaviour, so anything configured here is
# simply gone when httpRoute.enabled is used instead - it is then the
# Gateway's business, or that of an implementation-specific policy object
# this chart deliberately does not render. Nothing this chart NEEDS lives
# here: the server's WebSocket upgrade works on a plain route, and TRUST_PROXY
# is an environment variable, not an annotation.
annotations: {} annotations: {}
# Gateway-API alternative to the Ingress above (Gitea #114). Off by default, so
# an existing release is unaffected; exactly one of ingress.enabled and
# httpRoute.enabled may be true.
#
# The chart renders ONLY an HTTPRoute with parentRefs - no Gateway, no
# gatewayClassName, no Certificate and no implementation-specific policy
# object. Class and certificate belong to the Gateway and therefore to the
# platform, which is what keeps this chart independent of whether the cluster
# runs nginx-gateway-fabric, Cilium, Envoy Gateway or Istio.
#
# The Gateway-API CRDs must be installed in the cluster: the chart does not and
# cannot detect them (.Capabilities.APIVersions is unreliable at template
# time), so with httpRoute.enabled: true and no CRDs the INSTALL fails.
httpRoute:
enabled: false
# The Gateway(s) this route attaches to - a platform object, usually in
# another namespace, which must allow this namespace in its listener's
# allowedRoutes. One entry per parent, the fields 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: the HTTPRoute takes a second
# hostname while the Ingress keeps ingress.domain.
#
# For THIS chart, mind that a second hostname does not move CORS_ORIGIN or
# the probes' Host header with it: both keep following ingress.domain, and
# the server answers the new name with a rejection until ingress.domain names
# it too.
hostnames: []
secrets: secrets:
redisSecretRef: null redisSecretRef: null
postgresSecretRef: null postgresSecretRef: null
+1 -1
View File
@@ -5,7 +5,7 @@ icon: https://raw.githubusercontent.com/lukevella/rallly/main/apps/landing/publi
type: application type: application
version: 8.1.8+up4.15.4 version: 8.2.0+up4.15.4
appVersion: "4.15.4" appVersion: "4.15.4"
maintainers: maintainers:
+60 -9
View File
@@ -6,11 +6,16 @@
{{- $service := printf "%s-%s-service" .Release.Name .Chart.Name -}} {{- $service := printf "%s-%s-service" .Release.Name .Chart.Name -}}
{{- $envNames := list -}} {{- $envNames := list -}}
{{- range $entry := .Values.rallly.env }}{{- $envNames = append $envNames $entry.name }}{{- end }} {{- range $entry := .Values.rallly.env }}{{- $envNames = append $envNames $entry.name }}{{- end }}
{{- $httpRoute := .Values.httpRoute | default dict -}}
{{- $hostnames := $httpRoute.hostnames | default list -}}
{{- if not $hostnames -}}
{{- $hostnames = list .Values.ingress.domain -}}
{{- end -}}
Rallly {{ .Chart.AppVersion }} — release {{ .Release.Name }} in namespace {{ .Release.Namespace }}. Rallly {{ .Chart.AppVersion }} — release {{ .Release.Name }} in namespace {{ .Release.Namespace }}.
{{- if .Values.ingress.enabled }} {{- if .Values.ingress.enabled }}
ACCESS ACCESS — INGRESS MODE (ingress.enabled: true, httpRoute.enabled: false)
https://{{ .Values.ingress.domain }} https://{{ .Values.ingress.domain }}
The certificate is requested by cert-manager through the "{{ .Values.ingress.clusterIssuer }}" The certificate is requested by cert-manager through the "{{ .Values.ingress.clusterIssuer }}"
@@ -18,11 +23,50 @@ ACCESS
it with it with
kubectl -n {{ .Release.Namespace }} get certificate tls-{{ .Release.Name }}-{{ .Chart.Name }}-ingress 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 an HTTPRoute ({{ .Release.Name }}-{{ .Chart.Name }}-httproute) and NOTHING else
of the routing path — no Gateway, no class, no Certificate. Three things
therefore belong to the cluster and not to this chart:
1. The Gateway-API CRDs. Without them this install would have failed.
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 shows
up 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 a working attachment. 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, so there is no
tls-{{ .Release.Name }}-{{ .Chart.Name }}-ingress secret and nothing for cert-manager to do.
ingress.annotations is inert in this mode — the Gateway API has no annotation
channel — and nothing this chart needs lives there.
MIND THE HOSTNAME IN THIS MODE. NEXT_PUBLIC_BASE_URL still follows
ingress.domain ("https://{{ .Values.ingress.domain }}"), not httpRoute.hostnames, and the OIDC
redirect URI registered at the provider is built from it. Publishing under a
name that ingress.domain does not carry gives you an instance whose links and
sign-in all leave for the other host.
{{- else }} {{- else }}
ACCESS — NO INGRESS ACCESS — NEITHER INGRESS NOR HTTPROUTE
ingress.enabled is false, so this release has no Ingress and no certificate; ingress.enabled and httpRoute.enabled are both false, so this release has no
ingress.clusterIssuer is not needed. ingress.domain still is — see below. Ingress, no HTTPRoute and no certificate; ingress.clusterIssuer is not
needed. ingress.domain still is — see below.
kubectl -n {{ .Release.Namespace }} port-forward svc/{{ $service }} 3000:80 kubectl -n {{ .Release.Namespace }} port-forward svc/{{ $service }} 3000:80
@@ -34,7 +78,8 @@ ACCESS — NO INGRESS
ingress.domain is not only the Ingress host. The chart passes it to the app as ingress.domain is not only the Ingress host. The chart passes it to the app as
NEXT_PUBLIC_BASE_URL — upstream's "fully qualified public URL of your NEXT_PUBLIC_BASE_URL — upstream's "fully qualified public URL of your
instance" — which is why it stays required with the Ingress switched off. instance" — which is why it stays required in every mode, with the Ingress
switched off and in Gateway mode alike.
SIGN-IN GOES THROUGH THE PROVIDER, AND ONLY THROUGH THE PROVIDER SIGN-IN GOES THROUGH THE PROVIDER, AND ONLY THROUGH THE PROVIDER
This chart sets EMAIL_LOGIN_ENABLED and REGISTRATION_ENABLED to "false" in the This chart sets EMAIL_LOGIN_ENABLED and REGISTRATION_ENABLED to "false" in the
@@ -172,12 +217,18 @@ THE CACHE VOLUME COUNTS AGAINST THE EPHEMERAL-STORAGE LIMIT
is rebuilt on demand, at the cost of a slower first request. is rebuilt on demand, at the cost of a slower first request.
CHANGING THE DOMAIN LATER IS A CHANGE ON TWO SIDES CHANGING THE DOMAIN LATER IS A CHANGE ON TWO SIDES
ingress.domain is the Ingress host, the app's NEXT_PUBLIC_BASE_URL and ingress.domain is the published host, the app's NEXT_PUBLIC_BASE_URL and
therefore the OIDC redirect URI in one value. Editing it here alone breaks the therefore the OIDC redirect URI in one value. Editing it here alone breaks the
login: the provider refuses a callback it was never told about, and it is the login: the provider refuses a callback it was never told about, and it is the
provider that reports it — Kubernetes reports a healthy pod{{ if .Values.ingress.enabled }} and a valid provider that reports it — Kubernetes reports a healthy pod{{ if .Values.ingress.enabled }} and a valid
certificate for the new name{{ end }}. Register the new redirect URI first, then certificate for the new name{{ else if $httpRoute.enabled }} and an accepted
upgrade. route{{ end }}. Register the new redirect URI first, then upgrade.
{{- if $httpRoute.enabled }}
In GATEWAY MODE there is a third side: httpRoute.hostnames, which is what the
route actually publishes. It does not follow ingress.domain once set
explicitly, so a migration over a second hostname means the provider, the
route and ingress.domain all have to end up agreeing.
{{- end }}
{{- if $envNames }} {{- if $envNames }}
rallly.env ADDS {{ len $envNames }} VARIABLE(S): {{ join ", " $envNames }} rallly.env ADDS {{ len $envNames }} VARIABLE(S): {{ join ", " $envNames }}
@@ -191,5 +242,5 @@ rallly.env ADDS {{ len $envNames }} VARIABLE(S): {{ join ", " $envNames }}
SCALED TO ZERO SCALED TO ZERO
{{ if .Values.scaleDown }}scaleDown: true{{ else }}rallly.replicas: 0{{ end }} — the Deployment runs with replicas: 0, so nothing answers {{ if .Values.scaleDown }}scaleDown: true{{ else }}rallly.replicas: 0{{ end }} — the Deployment runs with replicas: 0, so nothing answers
until you set it back and upgrade again. Secrets and Service{{ if .Values.ingress.enabled }}, and the Ingress until you set it back and upgrade again. Secrets and Service{{ if .Values.ingress.enabled }}, and the Ingress
with its certificate,{{ end }} stay in place. with its certificate,{{ else if $httpRoute.enabled }}, and the HTTPRoute,{{ end }} stay in place.
{{- end }} {{- end }}
+82
View File
@@ -0,0 +1,82 @@
{{/*
The Gateway-API alternative to rallly.ingress.yaml (Gitea #114). Form and
reasoning: template-chart/templates/xxx.httproute.yaml and README, "Ingress and
Gateway API".
Only an HTTPRoute 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 is meaningless here - this chart renders no Certificate
in this mode - and it is not demanded either: its only required() sits inside
rallly.ingress.yaml, behind ingress.enabled.
ingress.domain IS STILL REQUIRED, AND NOT BECAUSE OF THIS FILE
Checked for this chart rather than copied: rallly.deployment.yaml reads
ingress.domain with its own required() to build NEXT_PUBLIC_BASE_URL, which
Rallly bakes into every invitation mail and every OIDC redirect. So domain is
mandatory in every mode of this chart and the fallback below is never the only
reader.
That also means the hostname is not a routing detail here: it is registered at
the identity provider as the callback URL. Changing it - including by setting
httpRoute.hostnames to a second name for a migration - needs the provider
updated too, or logins fail with a redirect-uri mismatch while the pod stays
green. NOTES.txt says this for both modes.
*/}}
{{- /*
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 all three 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 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. A migration runs over a SECOND hostname (set httpRoute.hostnames explicitly while the Ingress keeps ingress.domain), never over parallel operation of both." -}}
{{- 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 -}}
{{- /*
One source of truth for the hostname: an empty httpRoute.hostnames means
[ingress.domain], so the two cannot drift apart. Setting httpRoute.hostnames
explicitly does NOT move NEXT_PUBLIC_BASE_URL, which keeps following
ingress.domain - see the header: for this chart the two belong together.
*/ -}}
{{- $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 one
# backend Service - the Next.js server serves the app, its API routes and
# /api/auth/* from one process, so a single path covers everything. 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 and the 80 below has to track rallly.service.yaml by hand.
# ci/run-httproute-match-test.sh is what holds that pair - it 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 }}-service
port: 80
{{- end }}
+31 -2
View File
@@ -113,7 +113,7 @@
"additionalProperties": false, "additionalProperties": false,
"properties": { "properties": {
"enabled": { "enabled": {
"description": "Gates the whole Ingress template (issue #93). false omits the Ingress and makes clusterIssuer unnecessary - but NOT domain, which is also the app's public base URL.", "description": "Gates the whole Ingress template (issue #93). Mutually exclusive with httpRoute.enabled - the render aborts when both are true. false omits the Ingress and makes clusterIssuer unnecessary - but NOT domain, which is also the app's public base URL.",
"type": "boolean" "type": "boolean"
}, },
"clusterIssuer": { "clusterIssuer": {
@@ -126,13 +126,42 @@
"type": ["string", "null"] "type": ["string", "null"]
}, },
"annotations": { "annotations": {
"description": "Extra Ingress annotations, merged with the cert-manager annotation the chart always sets. Free-form keys by nature.", "description": "Extra Ingress annotations, merged with the cert-manager annotation the chart always sets. Free-form keys by nature. No counterpart in Gateway mode: the Gateway API has no annotation channel, so these are gone when httpRoute.enabled is used instead.",
"type": "object" "type": "object"
} }
} }
},
"httpRoute": {
"description": "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; class and certificate sit on the Gateway and belong to the platform. The Gateway-API CRDs must be present in the cluster, because the chart cannot detect them.",
"type": "object",
"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. A hostname set here does not move NEXT_PUBLIC_BASE_URL or the OIDC redirect URI, which keep following ingress.domain.",
"type": "array",
"items": {
"type": "string"
}
}
}
} }
}, },
"definitions": { "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": { "resources": {
"description": "May be null / empty: the chart then falls back to its default requests and computed limits (issue #99) instead of rendering a container without resource accounting.", "description": "May be null / empty: the chart then falls back to its default requests and computed limits (issue #99) instead of rendering a container without resource accounting.",
"type": ["object", "null"], "type": ["object", "null"],
+54
View File
@@ -125,10 +125,64 @@ ingress:
# required even then: it is also the app's public base URL # required even then: it is also the app's public base URL
# (NEXT_PUBLIC_BASE_URL), not just the Ingress host. Only clusterIssuer # (NEXT_PUBLIC_BASE_URL), not just the Ingress host. Only clusterIssuer
# becomes unnecessary. # becomes unnecessary.
#
# Mutually exclusive with httpRoute.enabled below (Gitea #114): with both
# true the render aborts, naming both values.
enabled: true enabled: true
clusterIssuer: null clusterIssuer: null
# The public hostname of this release. Read by the Ingress, by
# NEXT_PUBLIC_BASE_URL, and - when httpRoute.hostnames is empty - by the
# HTTPRoute. Required in every mode (see above).
domain: null domain: null
className: nginx className: nginx
# Extra annotations for the Ingress; merged with the cert-manager # Extra annotations for the Ingress; merged with the cert-manager
# cluster-issuer annotation the chart always sets. # cluster-issuer annotation the chart always sets.
#
# THESE HAVE NO COUNTERPART IN GATEWAY MODE. The Gateway API has no
# annotation channel for controller behaviour, so anything configured here is
# simply gone when httpRoute.enabled is used instead - it is then the
# Gateway's business, or that of an implementation-specific policy object
# this chart deliberately does not render. Nothing this chart needs lives
# here by default.
annotations: {} annotations: {}
# Gateway-API alternative to the Ingress above (Gitea #114). Off by default, so
# an existing release is unaffected; exactly one of ingress.enabled and
# httpRoute.enabled may be true.
#
# The chart renders ONLY an HTTPRoute with parentRefs - no Gateway, no
# gatewayClassName, no Certificate and no implementation-specific policy
# object. Class and certificate belong to the Gateway and therefore to the
# platform, which is what keeps this chart independent of whether the cluster
# runs nginx-gateway-fabric, Cilium, Envoy Gateway or Istio.
#
# The Gateway-API CRDs must be installed in the cluster: the chart does not and
# cannot detect them (.Capabilities.APIVersions is unreliable at template
# time), so with httpRoute.enabled: true and no CRDs the INSTALL fails.
httpRoute:
enabled: false
# The Gateway(s) this route attaches to - a platform object, usually in
# another namespace, which must allow this namespace in its listener's
# allowedRoutes. One entry per parent, the fields 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: the HTTPRoute takes a second
# hostname while the Ingress keeps ingress.domain.
#
# For THIS chart, mind that a second hostname does not move
# NEXT_PUBLIC_BASE_URL with it, and that the OIDC redirect URI registered at
# the provider is built from ingress.domain - a new name needs both sides
# updated or logins fail on a redirect-uri mismatch while the pod stays green.
hostnames: []
+1 -1
View File
@@ -9,7 +9,7 @@ type: application
# (2026.8.22-9fea41204) and "latest" moves daily. appVersion is therefore the # (2026.8.22-9fea41204) and "latest" moves daily. appVersion is therefore the
# exact tag, and the +up suffix normalizes the dash to a dot as the versioning # exact tag, and the +up suffix normalizes the dash to a dot as the versioning
# scheme requires - the same shape rss-bridge and minecraft-server use. # scheme requires - the same shape rss-bridge and minecraft-server use.
version: 1.0.6+up2026.9.13.3b8009065 version: 1.1.0+up2026.9.13.3b8009065
appVersion: "2026.9.13-3b8009065" appVersion: "2026.9.13-3b8009065"
maintainers: maintainers:
+69 -7
View File
@@ -1,20 +1,68 @@
{{- $replicas := include "searxng.replicas" (dict "scaleDown" .Values.scaleDown "replicas" .Values.searxng.replicas) -}} {{- $replicas := include "searxng.replicas" (dict "scaleDown" .Values.scaleDown "replicas" .Values.searxng.replicas) -}}
{{- $settings := fromYaml (include "searxng.effectiveSettings" .Values.settings) -}} {{- $settings := fromYaml (include "searxng.effectiveSettings" .Values.settings) -}}
{{- $httpRoute := .Values.httpRoute | default dict -}}
{{- $hostnames := $httpRoute.hostnames | default list -}}
{{- if not $hostnames -}}
{{- $hostnames = list .Values.ingress.domain -}}
{{- end -}}
SearXNG {{ .Chart.AppVersion }} — release {{ .Release.Name }} in namespace {{ .Release.Namespace }}. SearXNG {{ .Chart.AppVersion }} — release {{ .Release.Name }} in namespace {{ .Release.Namespace }}.
{{- if .Values.ingress.enabled }} {{- if .Values.ingress.enabled }}
ACCESS ACCESS — INGRESS MODE (ingress.enabled: true, httpRoute.enabled: false)
https://{{ .Values.ingress.domain }} https://{{ .Values.ingress.domain }}
The certificate is requested by cert-manager through the "{{ .Values.ingress.clusterIssuer }}" 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 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 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 an HTTPRoute ({{ .Release.Name }}-{{ .Chart.Name }}-httproute) and NOTHING else
of the routing path — no Gateway, no class, no Certificate. Three things
therefore belong to the cluster and not to this chart:
1. The Gateway-API CRDs. Without them this install would have failed.
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 shows
up 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 a working attachment. 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, so there is no
tls-{{ .Release.Name }}-{{ .Chart.Name }}-ingress secret and nothing for cert-manager to do.
SEARXNG_BASE_URL still follows ingress.domain ("https://{{ .Values.ingress.domain }}/"), not
httpRoute.hostnames: signed image-proxy URLs and the OpenSearch description
keep naming that host whatever the route publishes.
AND THE THING THIS MODE TAKES AWAY: the forward-auth escape hatch. The
Gateway API has no annotation channel, so the Authentik-style auth
annotations documented under ingress.annotations in values.yaml have NO
counterpart here, and the chart renders no implementation-specific policy
object on purpose. SearXNG has no login of its own, so "who may use this
instance" is entirely the Gateway's question in this mode — a listener that
is not publicly reachable, or that implementation's own auth filter,
configured next to the Gateway and not here.
{{- else }} {{- else }}
ACCESS — NO INGRESS ACCESS — NEITHER INGRESS NOR HTTPROUTE
ingress.enabled is false, so this release has no Ingress and no certificate. ingress.enabled and httpRoute.enabled are both false, so this release has no
Ingress, no HTTPRoute and no certificate.
ingress.domain is still required: it builds SEARXNG_BASE_URL, which SearXNG ingress.domain is still required: it builds SEARXNG_BASE_URL, which SearXNG
puts into its OpenSearch description, into the signed image-proxy URLs and puts into its OpenSearch description, into the signed image-proxy URLs and
into every absolute link. Reach the instance inside the cluster with into every absolute link. Reach the instance inside the cluster with
@@ -28,7 +76,10 @@ ACCESS — NO INGRESS
SCALED TO ZERO SCALED TO ZERO
{{ if .Values.scaleDown }}scaleDown: true{{ else }}searxng.replicas: 0{{ end }} — the deployment runs with replicas: 0, so the endpoint {{ if .Values.scaleDown }}scaleDown: true{{ else }}searxng.replicas: 0{{ end }} — the deployment runs with replicas: 0, so the endpoint
above does not answer until you set it back and upgrade again. Service{{ if .Values.ingress.enabled }} and above does not answer 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 Ingress stay in place, 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 }} place.{{ end }}
{{ end }} {{ end }}
CONFIGURATION CONFIGURATION
@@ -48,14 +99,25 @@ WHAT THE GREEN CHECKMARK DOES AND DOES NOT MEAN
All three probes hit /healthz, the only path SearXNG's bot detection lets All three probes hit /healthz, the only path SearXNG's bot detection lets
through unconditionally. Its handler returns a fixed "OK". So a Ready pod through unconditionally. Its handler returns a fixed "OK". So a Ready pod
means the worker answers — not that a search works, not that any engine is means the worker answers — not that a search works, not that any engine is
reachable, and not that a login placed in front of the Ingress is up (probes reachable, and not that a login placed in front of it is up (probes go
go straight to the pod and never pass the Ingress). straight to the pod and never pass the {{ if $httpRoute.enabled }}Gateway{{ else }}Ingress{{ end }}).
{{- if not $settings.limiter }} {{- if not $settings.limiter }}
BOT DETECTION IS OFF BOT DETECTION IS OFF
{{- if $httpRoute.enabled }}
settings.limiter is false and no Valkey is configured. The instance relies on
whatever sits in front of it — and in GATEWAY MODE that cannot be configured
from this chart: ingress.annotations has no counterpart, so a login has to be
put on the Gateway itself (a listener that is not publicly reachable, or that
implementation's own auth filter). The limiter itself is unaffected by the
mode; it runs inside SearXNG, not at the proxy.
{{- else }}
settings.limiter is false and no Valkey is configured. The instance relies on settings.limiter is false and no Valkey is configured. The instance relies on
whatever sits in front of it — put authentication there via whatever sits in front of it — put authentication there via
ingress.annotations if it must not be open to everyone. ingress.annotations if it must not be open to everyone. Mind that this
escape hatch exists in Ingress mode only: httpRoute.enabled has no
annotation channel at all.
{{- end }}
{{- else if not .Values.secrets.valkeySecretRef }} {{- else if not .Values.secrets.valkeySecretRef }}
BOT DETECTION IS ON WITHOUT A VALKEY BOT DETECTION IS ON WITHOUT A VALKEY
+93
View File
@@ -0,0 +1,93 @@
{{/*
The Gateway-API alternative to searxng.ingress.yaml (Gitea #114). Form and
reasoning: template-chart/templates/xxx.httproute.yaml and README, "Ingress and
Gateway API".
Only an HTTPRoute 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.
WHAT THIS CHART IN PARTICULAR LOSES IN THIS MODE, AND IT IS THE LOUD ONE
values.yaml documents ingress.annotations as THE place authentication in front
of SearXNG belongs - the forward-auth annotations for Authentik and friends,
which need no chart change because they are entirely an ingress-nginx matter.
The Gateway API has no annotation channel for controller behaviour, so in this
mode that escape hatch does not exist in this chart at all. It is not rebuilt
here: a policy object would bind the chart to one implementation and to a CRD
whose presence it cannot check. Protection has to come from the Gateway - a
listener that is not publicly reachable, or that implementation's own auth
filter. NOTES.txt says so in this mode rather than leaving an operator to find
out from their access log.
Note that the rate limiter is a separate question and unaffected: it runs
inside SearXNG against a Valkey (settings.limiter), not at the proxy.
ingress.clusterIssuer is meaningless here - this chart renders no Certificate
in this mode - and it is not demanded either: its only required() sits inside
searxng.ingress.yaml, behind ingress.enabled.
ingress.domain IS STILL REQUIRED, AND NOT BECAUSE OF THIS FILE
Checked for this chart rather than copied: searxng.deployment.yaml reads
ingress.domain with its own required() to build SEARXNG_BASE_URL, which is what
SearXNG puts into its OpenSearch description, into the image-proxy URLs it
signs and into every absolute link it generates. So domain is mandatory in
every mode of this chart and the fallback below is never the only reader -
which also means httpRoute.hostnames set for a migration does NOT move
server.base_url with it.
*/}}
{{- /*
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 all three 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 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. A migration runs over a SECOND hostname (set httpRoute.hostnames explicitly while the Ingress keeps ingress.domain), never over parallel operation of both." -}}
{{- 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 -}}
{{- /*
One source of truth for the hostname: an empty httpRoute.hostnames means
[ingress.domain], so the two cannot drift apart. See the header for what
setting it explicitly does not move.
*/ -}}
{{- $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 one
# backend Service - SearXNG serves its UI, /search and /healthz from one
# worker and has no second path. 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 and the 80 below has to
# track searxng.service.yaml by hand.
# ci/run-httproute-match-test.sh is what holds that pair - it 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 }}-service
port: 80
{{- end }}
+31 -2
View File
@@ -95,7 +95,7 @@
"additionalProperties": false, "additionalProperties": false,
"properties": { "properties": {
"enabled": { "enabled": {
"description": "Gates the Ingress template only. ingress.domain is read a second time in the Deployment (SEARXNG_BASE_URL) and keeps its own required() there, so switching the Ingress off frees clusterIssuer but NOT domain.", "description": "Gates the Ingress template only. Mutually exclusive with httpRoute.enabled - the render aborts when both are true. ingress.domain is read a second time in the Deployment (SEARXNG_BASE_URL) and keeps its own required() there, so switching the Ingress off frees clusterIssuer but NOT domain.",
"type": "boolean" "type": "boolean"
}, },
"clusterIssuer": { "clusterIssuer": {
@@ -108,13 +108,42 @@
"type": ["string", "null"] "type": ["string", "null"]
}, },
"annotations": { "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. No counterpart in Gateway mode: the Gateway API has no annotation channel, so the forward-auth setup documented in values.yaml - the way to put a login in front of an app that has none - does not exist when httpRoute.enabled is used instead.",
"type": "object" "type": "object"
} }
} }
},
"httpRoute": {
"description": "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; class and certificate sit on the Gateway and belong to the platform. The Gateway-API CRDs must be present in the cluster, because the chart cannot detect them.",
"type": "object",
"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. A hostname set here does not move SEARXNG_BASE_URL, which keeps following ingress.domain.",
"type": "array",
"items": {
"type": "string"
}
}
}
} }
}, },
"definitions": { "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": { "resources": {
"description": "May be null / empty: the chart then falls back to its default requests and computed limits (issue #99) instead of rendering a container without resource accounting.", "description": "May be null / empty: the chart then falls back to its default requests and computed limits (issue #99) instead of rendering a container without resource accounting.",
"type": ["object", "null"], "type": ["object", "null"],
+62
View File
@@ -301,10 +301,15 @@ ingress:
# every absolute link it generates. Left empty, the instance renders and the # every absolute link it generates. Left empty, the instance renders and the
# pod goes green while browsers get sent to a broken address - exactly the # pod goes green while browsers get sent to a broken address - exactly the
# silent failure the guideline exists for. # silent failure the guideline exists for.
#
# Mutually exclusive with httpRoute.enabled below (Gitea #114): with both
# true the render aborts, naming both values.
enabled: true enabled: true
clusterIssuer: null clusterIssuer: null
# The hostname this instance is served under. Required (see above), with no # The hostname this instance is served under. Required (see above), with no
# default - hostnames are installation data and have no business in a chart. # default - hostnames are installation data and have no business in a chart.
# Read by the Ingress, by SEARXNG_BASE_URL, and - when httpRoute.hostnames is
# empty - by the HTTPRoute.
domain: null domain: null
className: nginx className: nginx
# Extra annotations for the Ingress; merged with the cert-manager # Extra annotations for the Ingress; merged with the cert-manager
@@ -336,4 +341,61 @@ ingress:
# queries through the same login. That is not a chart concern, but it is # queries through the same login. That is not a chart concern, but it is
# the reason such a setup "suddenly stops working" after a session # the reason such a setup "suddenly stops working" after a session
# expires. # expires.
#
# AND THE THIRD THING TO KNOW: NONE OF THIS EXISTS IN GATEWAY MODE. The
# Gateway API has no annotation channel for controller behaviour, so the
# forward-auth block above - the documented way to put a login in front of
# this instance - has no counterpart at all when httpRoute.enabled is used
# instead. The chart does not rebuild it: a policy object would bind it to
# one Gateway implementation and to a CRD whose presence it cannot check.
# Protection then has to come from the Gateway itself - a listener that is
# not publicly reachable, or that implementation's own auth filter,
# configured next to the Gateway. NOTES.txt says so in that mode.
#
# The rate limiter is unaffected either way: it runs inside SearXNG against a
# Valkey (settings.limiter), not at the proxy.
annotations: {} annotations: {}
# Gateway-API alternative to the Ingress above (Gitea #114). Off by default, so
# an existing release is unaffected; exactly one of ingress.enabled and
# httpRoute.enabled may be true.
#
# The chart renders ONLY an HTTPRoute with parentRefs - no Gateway, no
# gatewayClassName, no Certificate and no implementation-specific policy
# object. Class and certificate belong to the Gateway and therefore to the
# platform, which is what keeps this chart independent of whether the cluster
# runs nginx-gateway-fabric, Cilium, Envoy Gateway or Istio.
#
# The Gateway-API CRDs must be installed in the cluster: the chart does not and
# cannot detect them (.Capabilities.APIVersions is unreliable at template
# time), so with httpRoute.enabled: true and no CRDs the INSTALL fails.
#
# WHAT THIS MODE COSTS HERE: the forward-auth escape hatch documented under
# ingress.annotations above. A SearXNG instance has no login of its own, so in
# Gateway mode "who may use this" is entirely the Gateway's question.
httpRoute:
enabled: false
# The Gateway(s) this route attaches to - a platform object, usually in
# another namespace, which must allow this namespace in its listener's
# allowedRoutes. One entry per parent, the fields 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: the HTTPRoute takes a second
# hostname while the Ingress keeps ingress.domain.
#
# For THIS chart, mind that a second hostname does not move SEARXNG_BASE_URL
# with it: signed image-proxy URLs and the OpenSearch description keep naming
# ingress.domain.
hostnames: []
+1 -1
View File
@@ -5,7 +5,7 @@ icon: https://raw.githubusercontent.com/we-promise/sure/main/public/android-chro
type: application type: application
version: 1.0.0+up0.7.4 version: 1.1.0+up0.7.4
appVersion: "0.7.4" appVersion: "0.7.4"
maintainers: maintainers:
+55 -5
View File
@@ -2,11 +2,16 @@
{{- $webStartup := fromYaml (include "sure.webStartupProbe" .Values.sure.web.startupProbe) -}} {{- $webStartup := fromYaml (include "sure.webStartupProbe" .Values.sure.web.startupProbe) -}}
{{- $workerStartup := fromYaml (include "sure.workerStartupProbe" .Values.sure.worker.startupProbe) -}} {{- $workerStartup := fromYaml (include "sure.workerStartupProbe" .Values.sure.worker.startupProbe) -}}
{{- $workerLiveness := fromYaml (include "sure.workerLivenessProbe" .Values.sure.worker.livenessProbe) -}} {{- $workerLiveness := fromYaml (include "sure.workerLivenessProbe" .Values.sure.worker.livenessProbe) -}}
{{- $httpRoute := .Values.httpRoute | default dict -}}
{{- $hostnames := $httpRoute.hostnames | default list -}}
{{- if not $hostnames -}}
{{- $hostnames = list .Values.ingress.domain -}}
{{- end -}}
Sure {{ .Chart.AppVersion }} — release {{ .Release.Name }} in namespace {{ .Release.Namespace }}. Sure {{ .Chart.AppVersion }} — release {{ .Release.Name }} in namespace {{ .Release.Namespace }}.
{{- if .Values.ingress.enabled }} {{- if .Values.ingress.enabled }}
ACCESS ACCESS — INGRESS MODE (ingress.enabled: true, httpRoute.enabled: false)
https://{{ .Values.ingress.domain }} https://{{ .Values.ingress.domain }}
The certificate is requested by cert-manager through the "{{ .Values.ingress.clusterIssuer }}" The certificate is requested by cert-manager through the "{{ .Values.ingress.clusterIssuer }}"
@@ -16,17 +21,62 @@ ACCESS
The first account created on a fresh instance becomes the super admin — sign The first account created on a fresh instance becomes the super admin — sign
in yourself before handing the URL to anyone. in yourself before handing the URL to anyone.
{{- else if $httpRoute.enabled }}
ACCESS — GATEWAY MODE (httpRoute.enabled: true, ingress.enabled: false)
{{- range $hostnames }}
https://{{ . }}
{{- end }}
The first account created on a fresh instance becomes the super admin — sign
in yourself before handing the URL to anyone.
This release renders an HTTPRoute ({{ .Release.Name }}-{{ .Chart.Name }}-httproute) and NOTHING else
of the routing path — no Gateway, no class, no Certificate. Three things
therefore belong to the cluster and not to this chart:
1. The Gateway-API CRDs. Without them this install would have failed.
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 shows
up 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 a working attachment. 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. This is
the one to get right here: the chart renders RAILS_ASSUME_SSL and
RAILS_FORCE_SSL as "true" in this mode, i.e. Rails TRUSTS the forwarded
scheme, sets secure cookies and sends HSTS. A Gateway listener that is
plain HTTP would therefore hand out cookies the browser never returns.
ingress.clusterIssuer is NOT used and not required — the chart renders
no Certificate, so there is no tls-{{ .Release.Name }}-{{ .Chart.Name }}-ingress secret.
APP_DOMAIN, WEBAUTHN_RP_ID and WEBAUTHN_ALLOWED_ORIGINS still follow
ingress.domain ({{ .Values.ingress.domain }}), not httpRoute.hostnames. Passkeys are bound to
the relying party id, so publishing under a name that ingress.domain does not
carry gives you passkeys registered against the wrong party — and changing
ingress.domain afterwards invalidates every credential already registered.
ingress.annotations is inert in this mode: the Gateway API has no annotation
channel. Nothing this chart needs lives there.
{{- else }} {{- else }}
ACCESS — NO INGRESS ACCESS — NEITHER INGRESS NOR HTTPROUTE
ingress.enabled is false, so this release has no Ingress and no certificate. ingress.enabled and httpRoute.enabled are both false, so this release has no
Ingress, no HTTPRoute and no certificate.
ingress.domain is still required: it becomes APP_DOMAIN (the host in every ingress.domain is still required: it becomes APP_DOMAIN (the host in every
mail link) and the WebAuthn relying party the app binds passkeys to. Reach mail link) and the WebAuthn relying party the app binds passkeys to. Reach
the instance inside the cluster with the instance inside the cluster with
kubectl -n {{ .Release.Namespace }} port-forward svc/{{ .Release.Name }}-{{ .Chart.Name }}-service 8080:80 kubectl -n {{ .Release.Namespace }} port-forward svc/{{ .Release.Name }}-{{ .Chart.Name }}-service 8080:80
and open http://localhost:8080. With the Ingress off the chart also renders and open http://localhost:8080. With nothing published the chart also renders
RAILS_ASSUME_SSL and RAILS_FORCE_SSL as "false", which is what makes that RAILS_ASSUME_SSL and RAILS_FORCE_SSL as "false", which is what makes that
plain-HTTP session able to log in at all; mail links still point at plain-HTTP session able to log in at all; mail links still point at
https://{{ .Values.ingress.domain }}/. https://{{ .Values.ingress.domain }}/.
@@ -35,7 +85,7 @@ ACCESS — NO INGRESS
SCALED TO ZERO SCALED TO ZERO
{{ if .Values.scaleDown }}scaleDown: true{{ else }}sure.replicas: 0{{ end }} — the StatefulSet runs with replicas: 0, so neither the web nor {{ if .Values.scaleDown }}scaleDown: true{{ else }}sure.replicas: 0{{ end }} — the StatefulSet runs with replicas: 0, so neither the web nor
the worker process is running. PVC, Secrets, Service{{ if .Values.ingress.enabled }}, Ingress and the the worker process is running. PVC, Secrets, Service{{ if .Values.ingress.enabled }}, Ingress and the
certificate stay{{ else }} stay{{ end }} in place; set it back and upgrade to bring both containers up. certificate stay{{ else if $httpRoute.enabled }}, and the HTTPRoute stay{{ else }} stay{{ end }} in place; set it back and upgrade to bring both containers up.
{{ end }} {{ end }}
WHAT THE THREE 1PASSWORD ITEMS MUST CONTAIN WHAT THE THREE 1PASSWORD ITEMS MUST CONTAIN
The chart creates OnePasswordItem CRs; the operator turns each into a The chart creates OnePasswordItem CRs; the operator turns each into a
+62 -8
View File
@@ -318,6 +318,46 @@ comes from.
{{- include "sure.resources" $merged -}} {{- include "sure.resources" $merged -}}
{{- end -}} {{- end -}}
{{/*
Is this release published through a TLS-terminating proxy at all? Returns the
string "true" or "false", for use as an environment variable value.
Input: the root context.
ONE PLACE, BECAUSE THE WRONG ANSWER IS INVISIBLE (Gitea #114)
RAILS_ASSUME_SSL and RAILS_FORCE_SSL used to be rendered from ingress.enabled
directly. That was correct while an Ingress was the only way this chart could
publish anything. With the Gateway-API mode it stopped being correct in exactly
one configuration - and that configuration is the whole point of the new mode:
httpRoute.enabled: true forces ingress.enabled: false (the two are mutually
exclusive, the render aborts otherwise), while TLS is still terminated in front
of the app, now at the Gateway's listener. Read from ingress.enabled alone, both
flags would have come out "false" there, and Rails would then
- not trust the X-Forwarded-Proto the Gateway sends, so it builds absolute
URLs - mail links, OIDC redirects - as http://,
- hand out session cookies without the Secure attribute, over a connection
that is in fact TLS, and
- send no HSTS header.
None of that fails. The pod goes Ready, the UI works, and the only way to
notice is to look at a Set-Cookie header. So the question is asked once, here,
and both flags read the answer.
With httpRoute.enabled at its default of false this expression IS
ingress.enabled, which is why adding it changes nothing for an existing
release: `helm template` with chart defaults renders byte-identically.
*/}}
{{- define "sure.publishedOverTls" -}}
{{- $httpRoute := .Values.httpRoute | default dict -}}
{{- if or .Values.ingress.enabled $httpRoute.enabled -}}
true
{{- else -}}
false
{{- end -}}
{{- end -}}
{{/* {{/*
The environment both containers share: the same image, the same database, the The environment both containers share: the same image, the same database, the
same Redis, the same secrets. Only the command differs. same Redis, the same secrets. Only the command differs.
@@ -358,16 +398,30 @@ probe on "/" reach the database at all (see values.yaml).
value: "/tmp/home" value: "/tmp/home"
- name: PORT - name: PORT
value: "3000" value: "3000"
# Sure's production.rb defaults BOTH of these to true. The chart renders them {{- /*
# explicitly from ingress.enabled instead: with the Ingress terminating TLS, Sure's production.rb defaults BOTH of these to true. The chart renders them
# assume_ssl makes Rails treat the forwarded request as secure (so force_ssl explicitly from whether this release is published through a TLS-terminating
# adds HSTS and secure cookies without redirecting), while without an Ingress proxy at all - ingress.enabled OR httpRoute.enabled, see
# both have to be off - a port-forwarded plain-HTTP session would otherwise "sure.publishedOverTls" above for why that OR is load-bearing and why reading
# either be redirected to https or hand out cookies the browser never returns. ingress.enabled alone was a silent defect in Gateway mode.
With a proxy in front, assume_ssl makes Rails treat the forwarded request as
secure (so force_ssl adds HSTS and secure cookies without redirecting); with
nothing in front both have to be off, or a port-forwarded plain-HTTP session is
either redirected to https or handed cookies the browser never returns.
A template comment and not a YAML one on purpose: this block is inside the
define, so a "#" comment here is rendered INTO every manifest and every edit to
it shows up as a diff in the deployed object. The emitted comment below is kept
to the one line an operator reading `helm get manifest` needs.
*/ -}}
{{- $tls := include "sure.publishedOverTls" . }}
# Rendered from ingress.enabled OR httpRoute.enabled (TLS is terminated by
# whichever proxy publishes this release); both off means plain HTTP.
- name: RAILS_ASSUME_SSL - name: RAILS_ASSUME_SSL
value: "{{ .Values.ingress.enabled }}" value: "{{ $tls }}"
- name: RAILS_FORCE_SSL - name: RAILS_FORCE_SSL
value: "{{ .Values.ingress.enabled }}" value: "{{ $tls }}"
# Action Mailer builds absolute URLs from this host, and it is the fallback the # Action Mailer builds absolute URLs from this host, and it is the fallback the
# WebAuthn initializer uses when WEBAUTHN_RP_ID is unset. # WebAuthn initializer uses when WEBAUTHN_RP_ID is unset.
- name: APP_DOMAIN - name: APP_DOMAIN
+99
View File
@@ -0,0 +1,99 @@
{{/*
The Gateway-API alternative to sure.ingress.yaml (Gitea #114). Form and
reasoning: template-chart/templates/xxx.httproute.yaml and README, "Ingress and
Gateway API".
Only an HTTPRoute 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 is meaningless here - this chart renders no Certificate
in this mode - and it is not demanded either: its only required() sits inside
sure.ingress.yaml, behind ingress.enabled.
THE ONE THING THIS CHART NEEDED BEYOND THE ROUTE: HOW RAILS IS TOLD ABOUT TLS
sure.commonEnv used to render RAILS_ASSUME_SSL and RAILS_FORCE_SSL from
ingress.enabled alone, which was exactly right while an Ingress was the only
way this release could be published. In Gateway mode ingress.enabled is false
BY CONSTRUCTION - the two modes are mutually exclusive - while TLS is still
terminated in front of the app, now at the Gateway's listener. Left as it was,
a Gateway-mode install would have come up with both flags false: Rails would
not trust the forwarded scheme, would hand out cookies without the Secure
attribute, would send no HSTS and would build absolute links as http://. The
pod goes Ready and the UI works, so nothing reports it - which is the class of
failure this repository treats as a defect rather than a default.
The flags therefore follow "is this release published through a TLS-terminating
proxy at all" (sure.publishedOverTls in _helpers.tpl), which is
ingress.enabled OR httpRoute.enabled. With httpRoute.enabled at its default of
false that expression IS ingress.enabled, so nothing changes for an existing
release - the behaviour only differs for a configuration that did not exist
before. A port-forwarded plain-HTTP session still gets both flags false and can
still log in.
ingress.domain IS STILL REQUIRED, AND NOT BECAUSE OF THIS FILE
Checked for this chart rather than copied: sure.commonEnv reads ingress.domain
with its own required(), three times over - APP_DOMAIN, WEBAUTHN_RP_ID and
WEBAUTHN_ALLOWED_ORIGINS. So domain is mandatory in every mode and the fallback
below is never the only reader. Mind WEBAUTHN_RP_ID in particular when
publishing under a new name: passkeys are bound to the relying party id, and
changing it invalidates every registered credential. That is a reason to be
careful with httpRoute.hostnames, which does NOT move the relying party id
with it.
*/}}
{{- /*
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 all three 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 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. A migration runs over a SECOND hostname (set httpRoute.hostnames explicitly while the Ingress keeps ingress.domain), never over parallel operation of both." -}}
{{- 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 -}}
{{- /*
One source of truth for the hostname: an empty httpRoute.hostnames means
[ingress.domain], so the two cannot drift apart. See the header for what
setting it explicitly does not move.
*/ -}}
{{- $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 one
# backend Service - the web container serves everything; the worker has no
# HTTP port and is never routed to. 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 and the 80 below has to
# track sure.service.yaml by hand.
# ci/run-httproute-match-test.sh is what holds that pair - it 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 }}-service
port: 80
{{- end }}
+31 -2
View File
@@ -118,7 +118,7 @@
"additionalProperties": false, "additionalProperties": false,
"properties": { "properties": {
"enabled": { "enabled": {
"description": "Gates the Ingress template (issue #93) and decides whether the chart renders RAILS_ASSUME_SSL/RAILS_FORCE_SSL as true or false. It does NOT free ingress.domain: that value is also read for APP_DOMAIN, WEBAUTHN_RP_ID and WEBAUTHN_ALLOWED_ORIGINS and keeps its own required() there (guideline 12.1).", "description": "Gates the Ingress template (issue #93). Mutually exclusive with httpRoute.enabled - the render aborts when both are true. It does NOT free ingress.domain: that value is also read for APP_DOMAIN, WEBAUTHN_RP_ID and WEBAUTHN_ALLOWED_ORIGINS and keeps its own required() there (guideline 12.1). Since Gitea #114 it no longer decides the TLS flags on its own either: RAILS_ASSUME_SSL/RAILS_FORCE_SSL follow ingress.enabled OR httpRoute.enabled, because in Gateway mode TLS is terminated at the Gateway while this value is false by construction.",
"type": "boolean" "type": "boolean"
}, },
"clusterIssuer": { "clusterIssuer": {
@@ -131,13 +131,42 @@
"type": ["string", "null"] "type": ["string", "null"]
}, },
"annotations": { "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. No counterpart in Gateway mode: the Gateway API has no annotation channel, so these are gone when httpRoute.enabled is used instead.",
"type": "object" "type": "object"
} }
} }
},
"httpRoute": {
"description": "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; class and certificate sit on the Gateway and belong to the platform. The Gateway-API CRDs must be present in the cluster, because the chart cannot detect them. This value also feeds RAILS_ASSUME_SSL/RAILS_FORCE_SSL - see ingress.enabled.",
"type": "object",
"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. A hostname set here does not move WEBAUTHN_RP_ID, APP_DOMAIN or WEBAUTHN_ALLOWED_ORIGINS, which keep following ingress.domain - and changing the relying party id invalidates every registered passkey.",
"type": "array",
"items": {
"type": "string"
}
}
}
} }
}, },
"definitions": { "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": { "resources": {
"description": "May be null / empty: the chart then falls back to its default requests and computed limits (issue #99) instead of rendering a container without resource accounting.", "description": "May be null / empty: the chart then falls back to its default requests and computed limits (issue #99) instead of rendering a container without resource accounting.",
"type": ["object", "null"], "type": ["object", "null"],
+68 -4
View File
@@ -299,14 +299,78 @@ ingress:
# up with WEBAUTHN_RP_ID="" and register passkeys against the wrong relying # up with WEBAUTHN_RP_ID="" and register passkeys against the wrong relying
# party - rendered cleanly, pod green. # party - rendered cleanly, pod green.
# #
# ingress.enabled also decides how the app is told about TLS: enabled renders # Mutually exclusive with httpRoute.enabled below (Gitea #114): with both
# RAILS_ASSUME_SSL=true and RAILS_FORCE_SSL=true (the Ingress terminates TLS, # true the render aborts, naming both values.
# so Rails must trust that and may set secure cookies and HSTS), disabled #
# renders both as "false" so a plain-HTTP port-forward can log in at all. # HOW THE APP IS TOLD ABOUT TLS NO LONGER HANGS ON THIS VALUE ALONE (#114).
# RAILS_ASSUME_SSL and RAILS_FORCE_SSL follow "is this release published
# through a TLS-terminating proxy at all", i.e. ingress.enabled OR
# httpRoute.enabled: with either on they render "true" (the proxy terminates
# TLS, so Rails must trust the forwarded scheme and may set secure cookies
# and HSTS), with both off they render "false" so a plain-HTTP port-forward
# can log in at all. Deriving them from ingress.enabled alone was right while
# an Ingress was the only way to publish; in Gateway mode ingress.enabled is
# false by construction, and the flags would have gone false with TLS still
# terminated at the Gateway - insecure cookies and http:// links, with a
# green pod and a working UI as the only symptom.
enabled: true enabled: true
clusterIssuer: null clusterIssuer: null
domain: null domain: null
className: nginx className: nginx
# Extra annotations for the Ingress; merged with the cert-manager # Extra annotations for the Ingress; merged with the cert-manager
# cluster-issuer annotation the chart always sets. # cluster-issuer annotation the chart always sets.
#
# THESE HAVE NO COUNTERPART IN GATEWAY MODE. The Gateway API has no
# annotation channel for controller behaviour, so anything configured here is
# simply gone when httpRoute.enabled is used instead - it is then the
# Gateway's business, or that of an implementation-specific policy object
# this chart deliberately does not render. Nothing this chart needs lives
# here by default; the TLS flags above are environment variables and are
# handled in both modes.
annotations: {} annotations: {}
# Gateway-API alternative to the Ingress above (Gitea #114). Off by default, so
# an existing release is unaffected; exactly one of ingress.enabled and
# httpRoute.enabled may be true.
#
# The chart renders ONLY an HTTPRoute with parentRefs - no Gateway, no
# gatewayClassName, no Certificate and no implementation-specific policy
# object. Class and certificate belong to the Gateway and therefore to the
# platform, which is what keeps this chart independent of whether the cluster
# runs nginx-gateway-fabric, Cilium, Envoy Gateway or Istio.
#
# The Gateway-API CRDs must be installed in the cluster: the chart does not and
# cannot detect them (.Capabilities.APIVersions is unreliable at template
# time), so with httpRoute.enabled: true and no CRDs the INSTALL fails.
#
# This value also feeds RAILS_ASSUME_SSL / RAILS_FORCE_SSL - see the note under
# ingress.enabled above.
httpRoute:
enabled: false
# The Gateway(s) this route attaches to - a platform object, usually in
# another namespace, which must allow this namespace in its listener's
# allowedRoutes. One entry per parent, the fields 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: the HTTPRoute takes a second
# hostname while the Ingress keeps ingress.domain.
#
# BE CAREFUL WITH THIS ONE HERE. A second hostname does not move
# WEBAUTHN_RP_ID, which keeps following ingress.domain - and passkeys are
# bound to the relying party id, so changing ingress.domain to match
# invalidates every registered credential (upstream
# docs/hosting/webauthn.md). APP_DOMAIN and WEBAUTHN_ALLOWED_ORIGINS follow
# ingress.domain as well.
hostnames: []
+1 -1
View File
@@ -4,5 +4,5 @@ description: A Helm chart to deploy urlaubsverwaltung on Kubernetes
icon: https://raw.githubusercontent.com/urlaubsverwaltung/urlaubsverwaltung/main/src/main/resources/static/favicons/apple-touch-icon.png icon: https://raw.githubusercontent.com/urlaubsverwaltung/urlaubsverwaltung/main/src/main/resources/static/favicons/apple-touch-icon.png
type: application type: application
version: 10.0.3+up6.15.0 version: 10.1.0+up6.15.0
appVersion: 6.15.0 appVersion: 6.15.0
+65 -10
View File
@@ -44,11 +44,16 @@ force. Mirror the defaulting of the resources helper on the raw value instead.
{{- range $entry := .Values.urlaubsverwaltung.env }} {{- range $entry := .Values.urlaubsverwaltung.env }}
{{- $envNames = append $envNames $entry.name }} {{- $envNames = append $envNames $entry.name }}
{{- end }} {{- end }}
{{- $httpRoute := .Values.httpRoute | default dict -}}
{{- $hostnames := $httpRoute.hostnames | default list -}}
{{- if not $hostnames -}}
{{- $hostnames = list .Values.ingress.domain -}}
{{- end -}}
Urlaubsverwaltung {{ .Chart.AppVersion }} — release {{ .Release.Name }} in namespace {{ .Release.Namespace }}. Urlaubsverwaltung {{ .Chart.AppVersion }} — release {{ .Release.Name }} in namespace {{ .Release.Namespace }}.
{{- if .Values.ingress.enabled }} {{- if .Values.ingress.enabled }}
ACCESS ACCESS — INGRESS MODE (ingress.enabled: true, httpRoute.enabled: false)
https://{{ .Values.ingress.domain }} https://{{ .Values.ingress.domain }}
The certificate is requested by cert-manager through the "{{ .Values.ingress.clusterIssuer }}" The certificate is requested by cert-manager through the "{{ .Values.ingress.clusterIssuer }}"
@@ -64,15 +69,63 @@ ACCESS
API description, not of data. Turning them off is a API description, not of data. Turning them off is a
springdoc.api-docs.enabled / springdoc.swagger-ui.enabled property; the chart springdoc.api-docs.enabled / springdoc.swagger-ui.enabled property; the chart
sets neither, so it can be added through urlaubsverwaltung.env. sets neither, so it can be added through urlaubsverwaltung.env.
{{- else if $httpRoute.enabled }}
ACCESS — GATEWAY MODE (httpRoute.enabled: true, ingress.enabled: false)
{{- range $hostnames }}
https://{{ . }}
{{- end }}
The route publishes the application at "/" with no path restriction, exactly
as the Ingress does — including /swagger-ui.html and /v3/api-docs, which this
app version enables by default and this chart does not turn off. Both are
behind the login, so this is exposure of an API description, not of data;
springdoc.api-docs.enabled / springdoc.swagger-ui.enabled through
urlaubsverwaltung.env is how to turn them off.
This release renders an HTTPRoute ({{ .Release.Name }}-{{ .Chart.Name }}-httproute) and NOTHING else
of the routing path — no Gateway, no class, no Certificate. Three things
therefore belong to the cluster and not to this chart:
1. The Gateway-API CRDs. Without them this install would have failed.
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 shows
up 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 a working attachment. 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, so there is no
tls-{{ .Release.Name }}-{{ .Chart.Name }}-ingress secret and nothing for cert-manager to do.
THE HOSTNAME AND THE APPLICATION'S IDEA OF ITSELF STILL DO NOT AGREE BY
THEMSELVES. No environment variable of the container derives from
ingress.domain or from httpRoute.hostnames; the address the application
considers its own is the OIDC redirect URI, out of the 1Password item below.
The two can drift apart without any complaint — the route answers, the login
bounces.
ingress.annotations is inert in this mode: the Gateway API has no annotation
channel. Nothing this chart needs lives there.
{{- else }} {{- else }}
ACCESS — NO INGRESS ACCESS — NEITHER INGRESS NOR HTTPROUTE
ingress.enabled is false, so this release has no Ingress and no certificate; ingress.enabled and httpRoute.enabled are both false, so this release has no
ingress.clusterIssuer is not needed. Unlike other charts in this repo, Ingress, no HTTPRoute and no certificate;
ingress.domain is read by NOTHING else here — no environment variable of the ingress.clusterIssuer is not needed, and neither is ingress.domain. Unlike
container derives from it. The address the application considers its own is other charts in this repo, ingress.domain is read by NOTHING here except the
the OIDC redirect URI, and that comes out of the 1Password item below, not two routing templates — no environment variable of the container derives from
out of this value. The two can drift apart without any complaint. it. The address the application considers its own is the OIDC redirect URI,
and that comes out of the 1Password item below, not out of this value. The
two can drift apart without any complaint.
Reach the release inside the cluster with Reach the release inside the cluster with
@@ -87,7 +140,9 @@ SCALED TO ZERO
{{ if .Values.scaleDown }}scaleDown: true{{ else }}urlaubsverwaltung.replicas: 0{{ end }} — the Deployment runs with replicas: 0, so the endpoint {{ if .Values.scaleDown }}scaleDown: true{{ else }}urlaubsverwaltung.replicas: 0{{ end }} — the Deployment runs with replicas: 0, so the endpoint
above does not answer until you set it back and upgrade again. Both above does not answer until you set it back and upgrade again. Both
OnePasswordItem CRs and the Service{{ if .Values.ingress.enabled }} and Ingress stay in place, so the OnePasswordItem CRs and the Service{{ if .Values.ingress.enabled }} and Ingress stay in place, so the
certificate is not re-issued.{{ else }} stay in place.{{ end }} This chart owns no volume: all 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 }} stay in place.{{ end }} This chart owns no volume: all
state lives in the external database, so scaling to zero loses nothing. state lives in the external database, so scaling to zero loses nothing.
{{ end }} {{ end }}
SECRETS — THE FIELDS EACH 1PASSWORD ITEM MUST CARRY SECRETS — THE FIELDS EACH 1PASSWORD ITEM MUST CARRY
@@ -114,7 +169,7 @@ SECRETS — THE FIELDS EACH 1PASSWORD ITEM MUST CARRY
redirect-uri the full callback URL. This chart registers the client redirect-uri the full callback URL. This chart registers the client
under the id "default", so the path is under the id "default", so the path is
/login/oauth2/code/default — for example /login/oauth2/code/default — for example
https://{{ if .Values.ingress.enabled }}{{ .Values.ingress.domain }}{{ else }}example.com{{ end }}/login/oauth2/code/default https://{{ if or .Values.ingress.enabled $httpRoute.enabled }}{{ first $hostnames }}{{ else }}example.com{{ end }}/login/oauth2/code/default
and the same URL has to be registered at the provider. and the same URL has to be registered at the provider.
This chart deploys NO database and NO identity provider. Both have to exist This chart deploys NO database and NO identity provider. Both have to exist
@@ -0,0 +1,84 @@
{{/*
The Gateway-API alternative to urlaubsverwaltung.ingress.yaml (Gitea #114).
Form and reasoning: template-chart/templates/xxx.httproute.yaml and README,
"Ingress and Gateway API".
Only an HTTPRoute 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 is meaningless here - this chart renders no Certificate
in this mode - and it is not demanded either: its only required() sits inside
urlaubsverwaltung.ingress.yaml, behind ingress.enabled.
ingress.domain, CHECKED FOR THIS CHART AND NOT COPIED
Unusually for this repository, nothing outside the routing templates reads
ingress.domain here: no environment variable of the application is derived from
it. UV_MAIL_APPLICATIONURL - the host the app puts into the links inside its
notification mails - is a hardcoded placeholder (http://localhost:8080) and is
deliberately NOT derived from ingress.domain, which NOTES.txt already says and
which this mode does not change. So with both routing modes off, domain is
genuinely free, and in Gateway mode the required() below is the one that fires.
What does NOT follow the hostname automatically is the OIDC redirect URI: it is
a field of the 1Password item and is registered at the identity provider.
Publishing under a different name - including via httpRoute.hostnames - means
updating both, or login fails with a redirect-uri mismatch while the pod stays
green. NOTES.txt says this for both modes.
*/}}
{{- /*
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 all three 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 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. A migration runs over a SECOND hostname (set httpRoute.hostnames explicitly while the Ingress keeps ingress.domain), never over parallel operation of both." -}}
{{- 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 -}}
{{- /*
One source of truth for the hostname: an empty httpRoute.hostnames means
[ingress.domain], so the two cannot drift apart. ingress.domain is read here
even though no Ingress is rendered in this mode - the value names the public
hostname of this release, which both modes need; it is not an Ingress switch.
*/ -}}
{{- $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 one
# backend Service - the Spring Boot application serves the UI, /login and
# /actuator from one port. 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 and the 80 below has to track
# urlaubsverwaltung.service.yaml by hand.
# ci/run-httproute-match-test.sh is what holds that pair - it 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 }}-service
port: 80
{{- end }}
+31 -1
View File
@@ -31,6 +31,7 @@
"additionalProperties": false, "additionalProperties": false,
"properties": { "properties": {
"enabled": { "enabled": {
"description": "Gates the whole Ingress template. Mutually exclusive with httpRoute.enabled - the render aborts when both are true. Checked in both directions for this chart: nothing outside the two routing templates reads ingress.domain (UV_MAIL_APPLICATIONURL is a hardcoded placeholder and deliberately not derived from it), so domain is unnecessary while BOTH routing modes are off; clusterIssuer is read by the Ingress alone.",
"type": "boolean" "type": "boolean"
}, },
"clusterIssuer": { "clusterIssuer": {
@@ -43,11 +44,35 @@
"type": ["string", "null"] "type": ["string", "null"]
}, },
"annotations": { "annotations": {
"description": "Extra Ingress annotations, merged with the cert-manager annotation the chart always sets. Free-form keys by nature.", "description": "Extra Ingress annotations, merged with the cert-manager annotation the chart always sets. Free-form keys by nature. No counterpart in Gateway mode: the Gateway API has no annotation channel, so these are gone when httpRoute.enabled is used instead.",
"type": "object" "type": "object"
} }
} }
}, },
"httpRoute": {
"description": "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; class and certificate sit on the Gateway and belong to the platform. The Gateway-API CRDs must be present in the cluster, because the chart cannot detect them.",
"type": "object",
"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 OIDC redirect URI lives in the 1Password item and does not follow it - publishing under a new name means updating both.",
"type": "array",
"items": {
"type": "string"
}
}
}
},
"urlaubsverwaltung": { "urlaubsverwaltung": {
"type": "object", "type": "object",
"additionalProperties": false, "additionalProperties": false,
@@ -78,6 +103,11 @@
} }
}, },
"definitions": { "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": { "resources": {
"description": "May be null / empty: the chart then falls back to its default requests and computed limits (issue #99) instead of rendering a container without resource accounting.", "description": "May be null / empty: the chart then falls back to its default requests and computed limits (issue #99) instead of rendering a container without resource accounting.",
"type": ["object", "null"], "type": ["object", "null"],
+62
View File
@@ -23,14 +23,76 @@ secrets:
oidcSecretRef: null oidcSecretRef: null
ingress: ingress:
# Gates the whole Ingress template. Mutually exclusive with
# httpRoute.enabled below (Gitea #114): with both true the render aborts,
# naming both values.
#
# Guideline 1, checked in both directions for this chart: NOTHING outside the
# two routing templates reads ingress.domain - no environment variable of the
# application is derived from it, and UV_MAIL_APPLICATIONURL (the host in the
# links inside notification mails) is a hardcoded placeholder, deliberately
# not derived from it. So clusterIssuer AND domain are both free while both
# routing modes are off; with either one on, domain is required and its
# required() fires in whichever template renders. clusterIssuer is only ever
# read by the Ingress - Gateway mode renders no Certificate.
enabled: true enabled: true
clusterIssuer: null clusterIssuer: null
# The public hostname of this release. Read by the Ingress and - when
# httpRoute.hostnames is empty - by the HTTPRoute.
domain: null domain: null
className: nginx className: nginx
# Extra annotations for the Ingress; merged with the cert-manager # Extra annotations for the Ingress; merged with the cert-manager
# cluster-issuer annotation the chart always sets. # cluster-issuer annotation the chart always sets.
#
# THESE HAVE NO COUNTERPART IN GATEWAY MODE. The Gateway API has no
# annotation channel for controller behaviour, so anything configured here is
# simply gone when httpRoute.enabled is used instead - it is then the
# Gateway's business, or that of an implementation-specific policy object
# this chart deliberately does not render. Nothing this chart needs lives
# here by default.
annotations: {} annotations: {}
# Gateway-API alternative to the Ingress above (Gitea #114). Off by default, so
# an existing release is unaffected; exactly one of ingress.enabled and
# httpRoute.enabled may be true.
#
# The chart renders ONLY an HTTPRoute with parentRefs - no Gateway, no
# gatewayClassName, no Certificate and no implementation-specific policy
# object. Class and certificate belong to the Gateway and therefore to the
# platform, which is what keeps this chart independent of whether the cluster
# runs nginx-gateway-fabric, Cilium, Envoy Gateway or Istio.
#
# The Gateway-API CRDs must be installed in the cluster: the chart does not and
# cannot detect them (.Capabilities.APIVersions is unreliable at template
# time), so with httpRoute.enabled: true and no CRDs the INSTALL fails.
httpRoute:
enabled: false
# The Gateway(s) this route attaches to - a platform object, usually in
# another namespace, which must allow this namespace in its listener's
# allowedRoutes. One entry per parent, the fields 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: the HTTPRoute takes a second
# hostname while the Ingress keeps ingress.domain.
#
# For THIS chart, mind that the OIDC redirect URI is a field of the 1Password
# item and is registered at the identity provider - it does not follow the
# hostname by itself, so publishing under a new name means updating both or
# login fails on a redirect-uri mismatch while the pod stays green.
hostnames: []
urlaubsverwaltung: urlaubsverwaltung:
# Replica count for this workload; 0 is allowed and scales it down. # Replica count for this workload; 0 is allowed and scales it down.
# The chart-global scaleDown flag takes precedence. # The chart-global scaleDown flag takes precedence.
+1 -1
View File
@@ -5,7 +5,7 @@ icon: https://raw.githubusercontent.com/l4rm4nd/VoucherVault/main/myapp/static/a
type: application type: application
version: 6.1.3+up1.30.3 version: 6.2.1+up1.30.3
appVersion: "1.30.3" appVersion: "1.30.3"
maintainers: maintainers:
+54 -7
View File
@@ -11,11 +11,16 @@
{{- range $required := list "DOMAIN" "SECRET_KEY" "REDIS_URL" "DB_ENGINE" "POSTGRES_HOST" "POSTGRES_DB" }} {{- range $required := list "DOMAIN" "SECRET_KEY" "REDIS_URL" "DB_ENGINE" "POSTGRES_HOST" "POSTGRES_DB" }}
{{- if not (has $required $names) }}{{- $missing = append $missing $required }}{{- end }} {{- if not (has $required $names) }}{{- $missing = append $missing $required }}{{- end }}
{{- end }} {{- end }}
{{- $httpRoute := .Values.httpRoute | default dict -}}
{{- $hostnames := $httpRoute.hostnames | default list -}}
{{- if not $hostnames -}}
{{- $hostnames = list .Values.ingress.domain -}}
{{- end -}}
VoucherVault {{ .Chart.AppVersion }} — release {{ .Release.Name }} in namespace {{ .Release.Namespace }}. VoucherVault {{ .Chart.AppVersion }} — release {{ .Release.Name }} in namespace {{ .Release.Namespace }}.
{{- if .Values.ingress.enabled }} {{- if .Values.ingress.enabled }}
ACCESS ACCESS — INGRESS MODE (ingress.enabled: true, httpRoute.enabled: false)
https://{{ .Values.ingress.domain }} https://{{ .Values.ingress.domain }}
The certificate is requested by cert-manager through the "{{ .Values.ingress.clusterIssuer }}" The certificate is requested by cert-manager through the "{{ .Values.ingress.clusterIssuer }}"
@@ -26,10 +31,51 @@ ACCESS
ingress.domain is more than the Ingress host: it is the only entry Django adds ingress.domain is more than the Ingress host: it is the only entry Django adds
to ALLOWED_HOSTS besides 127.0.0.1, and it builds CSRF_TRUSTED_ORIGINS. Under to ALLOWED_HOSTS besides 127.0.0.1, and it builds CSRF_TRUSTED_ORIGINS. Under
any other name the app answers 400 before a single view runs. any other name the app answers 400 before a single view runs.
{{- else if $httpRoute.enabled }}
ACCESS — GATEWAY MODE (httpRoute.enabled: true, ingress.enabled: false)
{{- range $hostnames }}
https://{{ . }}
{{- end }}
This release renders an HTTPRoute ({{ .Release.Name }}-{{ .Chart.Name }}-httproute) and NOTHING else
of the routing path — no Gateway, no class, no Certificate. Three things
therefore belong to the cluster and not to this chart:
1. The Gateway-API CRDs. Without them this install would have failed.
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 shows
up 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 a working attachment. 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, so there is no
tls-{{ .Release.Name }}-{{ .Chart.Name }}-ingress secret and nothing for cert-manager to do.
AND THE ONE THAT WILL ACTUALLY BITE: ALLOWED_HOSTS DOES NOT FOLLOW THE ROUTE.
Django's DOMAIN is built from ingress.domain ("{{ .Values.ingress.domain }}"), and that is the
only name besides 127.0.0.1 it accepts. A hostname this route publishes which
ingress.domain does not name is answered with 400 before a single view runs —
and all three probes stay green while it happens, because they deliberately
carry a Host header of 127.0.0.1 so that probing survives every domain
change. So set httpRoute.hostnames and ingress.domain together.
ingress.annotations is inert in this mode: the Gateway API has no annotation
channel. Nothing this chart needs lives there.
{{- else }} {{- else }}
ACCESS — NO INGRESS ACCESS — NEITHER INGRESS NOR HTTPROUTE
ingress.enabled is false, so this release has no Ingress and no certificate; ingress.enabled and httpRoute.enabled are both false, so this release has no
Ingress, no HTTPRoute and no certificate;
ingress.clusterIssuer is not needed. ingress.domain still is — it is Django's ingress.clusterIssuer is not needed. ingress.domain still is — it is Django's
ALLOWED_HOSTS entry ("{{ .Values.ingress.domain }}"), not just an Ingress host. ALLOWED_HOSTS entry ("{{ .Values.ingress.domain }}"), not just an Ingress host.
@@ -58,7 +104,8 @@ SCALED TO ZERO
{{ if .Values.scaleDown }}scaleDown: true{{ else }}voucherVault.replicas: 0{{ end }} — the StatefulSet runs with replicas: 0, so the endpoint {{ if .Values.scaleDown }}scaleDown: true{{ else }}voucherVault.replicas: 0{{ end }} — the StatefulSet runs with replicas: 0, so the endpoint
above does not answer until you set it back and upgrade again. PVC, Secrets above does not answer until you set it back and upgrade again. PVC, Secrets
and Service{{ if .Values.ingress.enabled }} and Ingress stay in place, so the certificate is not and Service{{ if .Values.ingress.enabled }} and Ingress stay in place, so the certificate is not
re-issued.{{ else }} stay in place.{{ end }} 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.{{ else }} stay in place.{{ end }}
{{ end }} {{ end }}
SECRETS — THE FIELDS EACH 1PASSWORD ITEM MUST CARRY SECRETS — THE FIELDS EACH 1PASSWORD ITEM MUST CARRY
All three OnePasswordItem CRs are created unconditionally and carry an item All three OnePasswordItem CRs are created unconditionally and carry an item
@@ -87,7 +134,7 @@ SECRETS — THE FIELDS EACH 1PASSWORD ITEM MUST CARRY
endpoint is a field and not an optional extra. The redirect URI to register at endpoint is a field and not an optional extra. The redirect URI to register at
the provider is the provider is
https://{{ if .Values.ingress.enabled }}{{ .Values.ingress.domain }}{{ else }}example.com{{ end }}/oidc/callback/ https://{{ if or .Values.ingress.enabled $httpRoute.enabled }}{{ first $hostnames }}{{ else }}example.com{{ end }}/oidc/callback/
and it carries no language prefix, unlike every other path of the app. and it carries no language prefix, unlike every other path of the app.
{{- if $missing }} {{- if $missing }}
@@ -137,8 +184,8 @@ WHAT A READY POD DOES NOT PROVE
wrong ingress.domain is invisible to Kubernetes — all three probes pass, the wrong ingress.domain is invisible to Kubernetes — all three probes pass, the
rollout goes green, and every real request gets 400. Check it against the rollout goes green, and every real request gets 400. Check it against the
domain instead: domain instead:
{{ if .Values.ingress.enabled }} {{ if or .Values.ingress.enabled $httpRoute.enabled }}
curl -sI https://{{ .Values.ingress.domain }}/ | head -1 curl -sI https://{{ first $hostnames }}/ | head -1
{{- else }} {{- else }}
curl -sI http://127.0.0.1:8000/ | head -1 # through the port-forward above curl -sI http://127.0.0.1:8000/ | head -1 # through the port-forward above
{{- end }} {{- end }}
@@ -0,0 +1,83 @@
{{/*
The Gateway-API alternative to voucher-vault.ingress.yaml (Gitea #114). Form
and reasoning: template-chart/templates/xxx.httproute.yaml and README, "Ingress
and Gateway API".
Only an HTTPRoute 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 is meaningless here - this chart renders no Certificate
in this mode - and it is not demanded either: its only required() sits inside
voucher-vault.ingress.yaml, behind ingress.enabled.
ingress.domain IS STILL REQUIRED, AND NOT BECAUSE OF THIS FILE
Checked for this chart rather than copied: the DOMAIN entry of the env block in
values.yaml reads ingress.domain with its own required() (rendered through
tpl). Django's ALLOWED_HOSTS accepts only 127.0.0.1 and that DOMAIN, and
CSRF_TRUSTED_ORIGINS is built from it. So domain is mandatory in every mode of
this chart and the fallback below is never the only reader.
That coupling is the trap worth knowing in this mode: a hostname the route
publishes but DOMAIN does not name gets answered with 400 by Django, and all
three probes stay green while it happens - they carry a Host header of
127.0.0.1 on purpose, so probing survives every domain change and therefore
cannot report this one. Setting httpRoute.hostnames for a migration does NOT
move DOMAIN with it; a second hostname needs ingress.domain changed in the same
step, or added to ALLOWED_HOSTS by the app's own configuration.
*/}}
{{- /*
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 all three 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 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. A migration runs over a SECOND hostname (set httpRoute.hostnames explicitly while the Ingress keeps ingress.domain), never over parallel operation of both." -}}
{{- 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 -}}
{{- /*
One source of truth for the hostname: an empty httpRoute.hostnames means
[ingress.domain], so the two cannot drift apart. See the header for what
setting it explicitly does not move.
*/ -}}
{{- $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 one
# backend Service - Django serves the UI, /oidc/* and the admin from one
# process. 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 and the 80 below has to track
# voucher-vault.service.yaml by hand.
# ci/run-httproute-match-test.sh is what holds that pair - it 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 }}-service
port: 80
{{- end }}
+31 -2
View File
@@ -91,7 +91,7 @@
"additionalProperties": false, "additionalProperties": false,
"properties": { "properties": {
"enabled": { "enabled": {
"description": "Gates the whole Ingress template. false omits the Ingress and makes clusterIssuer unnecessary - but NOT domain, which is also Django's ALLOWED_HOSTS entry.", "description": "Gates the whole Ingress template. Mutually exclusive with httpRoute.enabled - the render aborts when both are true. false omits the Ingress and makes clusterIssuer unnecessary - but NOT domain, which is also Django's ALLOWED_HOSTS entry.",
"type": "boolean" "type": "boolean"
}, },
"clusterIssuer": { "clusterIssuer": {
@@ -104,13 +104,42 @@
"type": ["string", "null"] "type": ["string", "null"]
}, },
"annotations": { "annotations": {
"description": "Extra Ingress annotations, merged with the cert-manager annotation the chart always sets. Free-form keys by nature.", "description": "Extra Ingress annotations, merged with the cert-manager annotation the chart always sets. Free-form keys by nature. No counterpart in Gateway mode: the Gateway API has no annotation channel, so these are gone when httpRoute.enabled is used instead.",
"type": "object" "type": "object"
} }
} }
},
"httpRoute": {
"description": "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; class and certificate sit on the Gateway and belong to the platform. The Gateway-API CRDs must be present in the cluster, because the chart cannot detect them.",
"type": "object",
"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. A hostname set here does not move Django's DOMAIN, so ALLOWED_HOSTS answers it with 400 while all three probes stay green - they carry a Host header of 127.0.0.1 on purpose.",
"type": "array",
"items": {
"type": "string"
}
}
}
} }
}, },
"definitions": { "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": { "resources": {
"description": "May be null / empty: the chart then falls back to its default requests and computed limits (issue #99) instead of rendering a container without resource accounting.", "description": "May be null / empty: the chart then falls back to its default requests and computed limits (issue #99) instead of rendering a container without resource accounting.",
"type": ["object", "null"], "type": ["object", "null"],
+55
View File
@@ -212,10 +212,65 @@ ingress:
# Set to false to omit the Ingress entirely. Note that ingress.domain stays # Set to false to omit the Ingress entirely. Note that ingress.domain stays
# required even then: it is also Django's ALLOWED_HOSTS entry (the DOMAIN env # required even then: it is also Django's ALLOWED_HOSTS entry (the DOMAIN env
# above), not just the Ingress host. Only clusterIssuer becomes unnecessary. # above), not just the Ingress host. Only clusterIssuer becomes unnecessary.
#
# Mutually exclusive with httpRoute.enabled below (Gitea #114): with both
# true the render aborts, naming both values.
enabled: true enabled: true
clusterIssuer: null clusterIssuer: null
# The public hostname of this release. Read by the Ingress, by Django's
# DOMAIN (and through it ALLOWED_HOSTS and CSRF_TRUSTED_ORIGINS), and - when
# httpRoute.hostnames is empty - by the HTTPRoute. Required in every mode.
domain: null domain: null
className: nginx className: nginx
# Extra annotations for the Ingress; merged with the cert-manager # Extra annotations for the Ingress; merged with the cert-manager
# cluster-issuer annotation the chart always sets. # cluster-issuer annotation the chart always sets.
#
# THESE HAVE NO COUNTERPART IN GATEWAY MODE. The Gateway API has no
# annotation channel for controller behaviour, so anything configured here is
# simply gone when httpRoute.enabled is used instead - it is then the
# Gateway's business, or that of an implementation-specific policy object
# this chart deliberately does not render. Nothing this chart needs lives
# here by default.
annotations: {} annotations: {}
# Gateway-API alternative to the Ingress above (Gitea #114). Off by default, so
# an existing release is unaffected; exactly one of ingress.enabled and
# httpRoute.enabled may be true.
#
# The chart renders ONLY an HTTPRoute with parentRefs - no Gateway, no
# gatewayClassName, no Certificate and no implementation-specific policy
# object. Class and certificate belong to the Gateway and therefore to the
# platform, which is what keeps this chart independent of whether the cluster
# runs nginx-gateway-fabric, Cilium, Envoy Gateway or Istio.
#
# The Gateway-API CRDs must be installed in the cluster: the chart does not and
# cannot detect them (.Capabilities.APIVersions is unreliable at template
# time), so with httpRoute.enabled: true and no CRDs the INSTALL fails.
httpRoute:
enabled: false
# The Gateway(s) this route attaches to - a platform object, usually in
# another namespace, which must allow this namespace in its listener's
# allowedRoutes. One entry per parent, the fields 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: the HTTPRoute takes a second
# hostname while the Ingress keeps ingress.domain.
#
# FOR THIS CHART THAT IS A SHARP EDGE. A hostname the route publishes but
# DOMAIN does not name is answered with 400 by Django (ALLOWED_HOSTS), and
# all three probes stay green while it happens - they carry a Host header of
# 127.0.0.1 on purpose. So a second hostname needs ingress.domain changed in
# the same step.
hostnames: []