Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
76322e4d1a
|
||
|
|
33d7cedbab
|
No files matched your search
@@ -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
@@ -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:
|
||||||
|
|||||||
@@ -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 }}
|
||||||
@@ -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"],
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
@@ -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:
|
||||||
|
|||||||
@@ -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 }}
|
||||||
|
|||||||
@@ -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 }}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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:
|
||||||
|
|||||||
@@ -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 }}
|
||||||
@@ -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
@@ -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
@@ -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:
|
||||||
|
|||||||
@@ -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 }}
|
||||||
@@ -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 }}
|
||||||
@@ -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"],
|
||||||
|
|||||||
@@ -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
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 }}
|
||||||
@@ -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"],
|
||||||
|
|||||||
@@ -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
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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: []
|
||||||
@@ -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
|
||||||
@@ -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,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"],
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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 }}
|
||||||
@@ -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"],
|
||||||
|
|||||||
@@ -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: []
|
||||||
Reference in new issue
Block a user