Validate Helm Charts / Detect changed charts (pull_request) Successful in 5s
Validate Helm Charts / Resource quantities (pull_request) Successful in 15s
Validate Helm Charts / Selector match (pull_request) Successful in 14s
Validate Helm Charts / Build bentopdf (pull_request) Successful in 9s
Validate Helm Charts / HTTPRoute match (pull_request) Successful in 17s
Validate Helm Charts / Trivy config bentopdf (pull_request) Successful in 19s
Validate Helm Charts / Build garage (pull_request) Successful in 8s
Validate Helm Charts / Build patchmon (pull_request) Successful in 7s
Validate Helm Charts / Build rallly (pull_request) Successful in 7s
Validate Helm Charts / Build searxng (pull_request) Successful in 6s
Validate Helm Charts / Build sure (pull_request) Successful in 7s
Validate Helm Charts / Build urlaubsverwaltung (pull_request) Successful in 6s
Validate Helm Charts / Trivy config garage (pull_request) Successful in 18s
Validate Helm Charts / Build voucher-vault (pull_request) Successful in 7s
Validate Helm Charts / Trivy config patchmon (pull_request) Successful in 16s
Validate Helm Charts / Trivy config rallly (pull_request) Successful in 18s
Install-test / Install-test bentopdf (pull_request) Successful in 1m10s
Validate Helm Charts / Trivy config searxng (pull_request) Successful in 22s
Validate Helm Charts / Trivy config sure (pull_request) Successful in 18s
Validate Helm Charts / Trivy config urlaubsverwaltung (pull_request) Successful in 17s
Validate Helm Charts / Trivy config voucher-vault (pull_request) Successful in 19s
Install-test / Install-test garage (pull_request) Successful in 1m17s
Install-test / Install-test patchmon (pull_request) Successful in 1m24s
Install-test / Install-test rallly (pull_request) Successful in 1m39s
Install-test / Install-test searxng (pull_request) Successful in 1m7s
Install-test / Install-test sure (pull_request) Successful in 2m39s
Install-test / Install-test voucher-vault (pull_request) Successful in 2m9s
Validate Helm Charts / Install-test (pull_request) Successful in 13m15s
Install-test / Install-test urlaubsverwaltung (pull_request) Successful in 1m50s
Publish BentoPDF Helm Chart / Publish BentoPDF Helm Chart (push) Successful in 10s
Publish Helm Chart with patched Application Version / publish-helm-chart (push) Successful in 9s
Publish Garage Helm Chart / Publish Garage Helm Chart (push) Successful in 9s
Publish Helm Chart with patched Application Version / publish-helm-chart (push) Successful in 9s
Publish Rallly Helm Chart / Publish Rallly Helm Chart (push) Successful in 10s
Publish Helm Chart with patched Application Version / publish-helm-chart (push) Successful in 10s
Publish SearXNG Helm Chart / Publish SearXNG Helm Chart (push) Successful in 10s
Publish Helm Chart with patched Application Version / publish-helm-chart (push) Successful in 9s
Publish Sure Helm Chart / Publish Sure Helm Chart (push) Successful in 10s
Publish Helm Chart with patched Application Version / publish-helm-chart (push) Successful in 10s
Publish Urlaubsverwaltung Helm Chart / Publish Urlaubsverwaltung Helm Chart (push) Successful in 10s
Publish Helm Chart with patched Application Version / publish-helm-chart (push) Successful in 10s
Publish Patchmon Helm Chart / Publish Patchmon Helm Chart (push) Successful in 9s
Publish Helm Chart with patched Application Version / publish-helm-chart (push) Successful in 10s
Publish VoucherVault Helm Chart / Publish VoucherVault Helm Chart (push) Successful in 10s
Publish Helm Chart with patched Application Version / publish-helm-chart (push) Successful in 10s
SonarQube Analysis / SonarQube Analysis (push) Successful in 43s
Group 1 of the #114 sweep: the charts that render exactly one Ingress object and set no behaviour-bearing nginx annotation - bentopdf, garage, patchmon, rallly, searxng, sure, urlaubsverwaltung, voucher-vault. Each gets the form the pilot (cyberchef, PR #115) fixed and template-chart carries: an httpRoute block (enabled/parentRefs/hostnames, off by default), a template rendering ONLY an HTTPRoute with parentRefs - no Gateway, no gatewayClassName, no Certificate, no implementation-specific policy object - render aborts on both modes at once (naming both value paths), on an empty parentRefs and on a missing hostname, empty hostnames meaning [ingress.domain], a schema entry closed against unknown keys with an open parentRef definition, and a NOTES.txt that names the active mode and what the mode costs. Minor, additive, defaults unchanged: `helm template` with each chart's CI values is byte-identical to origin/main for seven of the eight, and for sure identical in every rendered value (the diff is two YAML comment lines). Three findings that were not in the plan, each argued in the file it changes: sure derived RAILS_ASSUME_SSL/RAILS_FORCE_SSL from ingress.enabled alone. Gateway mode sets that false by construction while TLS is still terminated at the Gateway, so both flags would have gone false: insecure cookies, no HSTS, http:// links, with a Ready pod and a working UI as the only symptom. They now read sure.publishedOverTls (ingress.enabled OR httpRoute.enabled), which is ingress.enabled while httpRoute.enabled is false - hence still Minor. garage claimed, in its Ingress template and its schema, that nothing outside that template reads ingress.domain. The HTTPRoute reads it, so the claim now holds only while both modes are off. Corrected rather than left standing. garage also depends on an upload limit it does not set: its annotations comment tells the operator to set proxy-body-size: "0" because ingress-nginx truncates S3 uploads at 1 MB. proxy-body-size has no Gateway-API counterpart, so its Gateway-mode NOTES prints that the limit is the Gateway's. README's list of affected charts named five and now names garage as a sixth. clusterIssuer needed no change in any of the eight: its only required() sits inside the Ingress template behind ingress.enabled, the same no-op the pilot found. Annotation guidance that stops applying in Gateway mode moved into the Gateway branch of NOTES.txt for bentopdf (X-Robots-Tag) and searxng (forward-auth). CI: one render overlay per chart under ci/httproute/, so the assertion is not vacuum-green - ci/run-httproute-match-test.sh goes from 4 held assertions over 2 charts to 20 over 10. The install-test fixtures stay in Ingress mode: the install runs one configuration per chart, cyberchef already pays that price against the real Gateway-API CRDs, and for garage the Ingress is the only place its Service port is referenced by NAME. Not proven, by construction: that traffic flows. No Gateway controller runs in CI. Not run locally either: k3d, Docker, Trivy. Part of #114
222 lines
12 KiB
Plaintext
222 lines
12 KiB
Plaintext
{{- $replicas := include "voucher-vault.replicas" (dict "scaleDown" .Values.scaleDown "replicas" .Values.voucherVault.replicas) -}}
|
|
{{- $startup := fromYaml (include "voucher-vault.startupProbe" .Values.voucherVault.startupProbe) -}}
|
|
{{- $pod := printf "%s-%s-sfs-0" .Release.Name .Chart.Name -}}
|
|
{{- $env := dict -}}
|
|
{{- $names := list -}}
|
|
{{- range $entry := .Values.voucherVault.env }}
|
|
{{- $names = append $names $entry.name }}
|
|
{{- if not (kindIs "invalid" $entry.value) }}{{- $_ := set $env $entry.name $entry.value }}{{- end }}
|
|
{{- end }}
|
|
{{- $missing := list -}}
|
|
{{- range $required := list "DOMAIN" "SECRET_KEY" "REDIS_URL" "DB_ENGINE" "POSTGRES_HOST" "POSTGRES_DB" }}
|
|
{{- if not (has $required $names) }}{{- $missing = append $missing $required }}{{- 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 }}.
|
|
|
|
{{- if .Values.ingress.enabled }}
|
|
|
|
ACCESS — INGRESS MODE (ingress.enabled: true, httpRoute.enabled: false)
|
|
https://{{ .Values.ingress.domain }}
|
|
|
|
The certificate is requested by cert-manager through the "{{ .Values.ingress.clusterIssuer }}"
|
|
cluster issuer. Until it has been issued the browser shows a warning; follow it with
|
|
|
|
kubectl -n {{ .Release.Namespace }} get certificate tls-{{ .Release.Name }}-{{ .Chart.Name }}-ingress
|
|
|
|
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
|
|
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 }}
|
|
|
|
ACCESS — NEITHER INGRESS NOR HTTPROUTE
|
|
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
|
|
ALLOWED_HOSTS entry ("{{ .Values.ingress.domain }}"), not just an Ingress host.
|
|
|
|
Forward the service, and keep 8000 as the LOCAL port:
|
|
|
|
kubectl -n {{ .Release.Namespace }} port-forward svc/{{ .Release.Name }}-{{ .Chart.Name }}-service 8000:80
|
|
|
|
Then open http://127.0.0.1:8000/ — not http://localhost:8000/. Only 127.0.0.1
|
|
and the domain above are in ALLOWED_HOSTS, so "localhost" answers 400; and
|
|
CSRF_TRUSTED_ORIGINS holds exactly http://127.0.0.1:8000, so any other local
|
|
port fails every POST including the login form.
|
|
{{- end }}
|
|
|
|
{{- if eq (lower (printf "%v" (index $env "SECURE_COOKIES" | default ""))) "true" }}
|
|
|
|
SECURE_COOKIES is True in voucherVault.env, so the session and CSRF cookies
|
|
carry the Secure flag. Browsers accept those from 127.0.0.1, but from no other
|
|
plain-http host: a login that silently bounces back to the form when the app
|
|
is reached over http is this, not a wrong password.
|
|
{{- end }}
|
|
|
|
Sessions expire after {{ index $env "SESSION_COOKIE_AGE" | default 30 }} minutes and at browser close: the app
|
|
multiplies SESSION_COOKIE_AGE by 60, so that value is minutes, not seconds.
|
|
{{ if eq $replicas "0" }}
|
|
SCALED TO ZERO
|
|
{{ 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
|
|
and Service{{ if .Values.ingress.enabled }} and 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.{{ else }} stay in place.{{ end }}
|
|
{{ end }}
|
|
SECRETS — THE FIELDS EACH 1PASSWORD ITEM MUST CARRY
|
|
All three OnePasswordItem CRs are created unconditionally and carry an item
|
|
path only. The field names below are decided here and appear nowhere in
|
|
1Password, so they are what you would otherwise read out of the templates. A
|
|
field missing from the item does not fail this install: the operator writes a
|
|
Secret without that key, and the pod stays in CreateContainerConfigError.
|
|
|
|
{{ .Values.secrets.voucherVaultSecretRef }}
|
|
→ Secret {{ .Release.Name }}-{{ .Chart.Name }}-secret
|
|
secretKey Django's SECRET_KEY. Changing it logs everyone out.
|
|
redis-url Celery broker, e.g. redis://:PASSWORD@redis:6379/0
|
|
|
|
{{ .Values.secrets.databaseSecretRef }}
|
|
→ Secret {{ .Release.Name }}-{{ .Chart.Name }}-db-secret
|
|
host port user password database
|
|
|
|
{{ .Values.secrets.oidcSecretRef }}
|
|
→ Secret {{ .Release.Name }}-{{ .Chart.Name }}-oidc-secret
|
|
client-id client-secret
|
|
authorization-endpoint token-endpoint user-endpoint
|
|
oidc-jwks-endpoint
|
|
|
|
The OIDC item is required even if you turn OIDC_ENABLED off in voucherVault.env,
|
|
because the CR is not gated. Sign algorithm is RS256, which is why the JWKS
|
|
endpoint is a field and not an optional extra. The redirect URI to register at
|
|
the provider is
|
|
|
|
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.
|
|
{{- if $missing }}
|
|
|
|
voucherVault.env HAS REPLACED THE CHART'S LIST — {{ len $missing }} REQUIRED ENTRIES ARE GONE
|
|
For this chart voucherVault.env is the container's COMPLETE environment, not
|
|
an additive block: a values file that sets it overwrites the whole list,
|
|
including the entries that wire up the secrets. Missing right now:
|
|
{{ join ", " $missing }}.
|
|
Copy the full list out of the chart's values.yaml and edit that copy.
|
|
{{- end }}
|
|
|
|
THE FIRST START PRINTS THE ONLY ADMIN PASSWORD IT WILL EVER PRINT
|
|
Against an empty database the entrypoint creates the superuser "admin" with a
|
|
randomly generated password and writes it to stdout — once, never again, and
|
|
into no Secret:
|
|
|
|
kubectl -n {{ .Release.Namespace }} logs {{ $pod }} | grep -A3 "Creating superuser"
|
|
|
|
If that line has already scrolled out of the log, reset it in place:
|
|
|
|
kubectl -n {{ .Release.Namespace }} exec -it {{ $pod }} -- python manage.py changepassword admin
|
|
|
|
With OIDC enabled the app drops its password-change pages and the login form
|
|
redirects to the provider on its own (OIDC_AUTOLOGIN). Django's own admin
|
|
login at /admin/ is not covered by that redirect — that is the way back in
|
|
when the provider is down or its item is wrong.
|
|
|
|
A POD THAT NEVER STARTS IS USUALLY WAITING FOR POSTGRES
|
|
Before it opens the HTTP port, the entrypoint loops on
|
|
"nc -z $POSTGRES_HOST $POSTGRES_PORT" with no timeout. A wrong host or port in
|
|
the database item therefore produces no error at all — the log simply stops at
|
|
|
|
[TASK] Waiting for PostgreSQL db...
|
|
|
|
until the startup probe kills the container. That budget is
|
|
{{ $startup.periodSeconds }}s x {{ $startup.failureThreshold }} failures = {{ mul $startup.periodSeconds $startup.failureThreshold }}s, and it surfaces as CrashLoopBackOff.
|
|
|
|
The other candidate is one line earlier: the entrypoint exits 1 with "[FAIL]
|
|
Insufficient permissions on bind mount volume" when /opt/app/database is not
|
|
writable for uid/gid 33. fsGroup 33 covers that on
|
|
{{ if .Values.storage.storageClass }}storage class "{{ .Values.storage.storageClass }}"{{ else }}the cluster's default storage class{{ end }}, as long as it honours fsGroup.
|
|
|
|
WHAT A READY POD DOES NOT PROVE
|
|
The probes send "Host: 127.0.0.1" on purpose: that host is always in
|
|
ALLOWED_HOSTS, so probing survives every domain change. The price is that a
|
|
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
|
|
domain instead:
|
|
{{ if or .Values.ingress.enabled $httpRoute.enabled }}
|
|
curl -sI https://{{ first $hostnames }}/ | head -1
|
|
{{- else }}
|
|
curl -sI http://127.0.0.1:8000/ | head -1 # through the port-forward above
|
|
{{- end }}
|
|
|
|
A 302 to a language-prefixed path is the healthy answer; 400 means the Host
|
|
does not match. Celery is the second blind spot: worker and beat are started
|
|
detached from the same entrypoint, so uwsgi answers whether or not they came
|
|
up. A wrong redis-url costs the expiry notifications and shows nothing else.
|
|
|
|
THIS RELEASE IS SINGLE-INSTANCE BY CONSTRUCTION
|
|
Uploaded files land in /opt/app/database, on the one ReadWriteOnce PVC
|
|
{{ .Release.Name }}-{{ .Chart.Name }}-pvc ({{ .Values.storage.amount }}),
|
|
which every pod of the StatefulSet mounts at the same path — and each pod runs
|
|
its own Celery beat. Raising voucherVault.replicas above 1 therefore buys
|
|
nothing: further pods stay Pending on a node that does not hold the volume,
|
|
and where they do start they send every expiry notification a second time.
|
|
|
|
UPGRADES MIGRATE, ROLLBACKS DO NOT UNMIGRATE
|
|
Every single start runs makemigrations and migrate before the server comes up,
|
|
so an app-version bump changes the schema the moment the new pod boots.
|
|
"helm rollback" puts the old image back but leaves the migrated schema in
|
|
place, and the old code then meets columns it does not know. Back up before
|
|
bumping the app version, and back up both halves: the rows live in Postgres,
|
|
the uploads on the PVC.
|
|
|
|
EXPIRY NOTIFICATIONS STAY SILENT UNTIL A USER ADDS AN APPRISE URL
|
|
EXPIRY_THRESHOLD_DAYS ({{ index $env "EXPIRY_THRESHOLD_DAYS" | default 30 }}) and EXPIRY_LAST_NOTIFICATION_DAYS ({{ index $env "EXPIRY_LAST_NOTIFICATION_DAYS" | default 7 }})
|
|
decide when an item counts as expiring, not whether anyone hears about it:
|
|
delivery runs over the Apprise URLs each user stores in their own profile, and
|
|
a profile without one is skipped without a word. The "Periodic Expiry Check"
|
|
schedule runs daily at 09:00 in the container's time zone
|
|
(TZ={{ index $env "TZ" | default "Europe/Berlin" }}) and is created ONLY while an empty database is initialized —
|
|
delete it in the admin and no later start brings it back.
|