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
216 lines
12 KiB
Plaintext
216 lines
12 KiB
Plaintext
{{- $replicas := include "sure.replicas" (dict "scaleDown" .Values.scaleDown "replicas" .Values.sure.replicas) -}}
|
|
{{- $webStartup := fromYaml (include "sure.webStartupProbe" .Values.sure.web.startupProbe) -}}
|
|
{{- $workerStartup := fromYaml (include "sure.workerStartupProbe" .Values.sure.worker.startupProbe) -}}
|
|
{{- $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 }}.
|
|
|
|
{{- 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
|
|
|
|
The first account created on a fresh instance becomes the super admin — sign
|
|
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 }}
|
|
|
|
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.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
|
|
the instance inside the cluster with
|
|
|
|
kubectl -n {{ .Release.Namespace }} port-forward svc/{{ .Release.Name }}-{{ .Chart.Name }}-service 8080:80
|
|
|
|
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
|
|
plain-HTTP session able to log in at all; mail links still point at
|
|
https://{{ .Values.ingress.domain }}/.
|
|
{{- end }}
|
|
{{ if eq $replicas "0" }}
|
|
SCALED TO ZERO
|
|
{{ 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
|
|
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 }}
|
|
WHAT THE THREE 1PASSWORD ITEMS MUST CONTAIN
|
|
The chart creates OnePasswordItem CRs; the operator turns each into a
|
|
Kubernetes Secret whose keys are the FIELD NAMES of the 1Password item. A
|
|
missing field does not fail the install — the release goes through and the
|
|
pod then hangs in CreateContainerConfigError naming the key it could not
|
|
find (kubectl -n {{ .Release.Namespace }} describe pod).
|
|
|
|
secrets.appSecretRef → {{ .Values.secrets.appSecretRef }}
|
|
Secret {{ .Release.Name }}-{{ .Chart.Name }}-secret, field:
|
|
secret-key-base SECRET_KEY_BASE, 64 random bytes hex
|
|
(openssl rand -hex 64)
|
|
|
|
secrets.dbSecretRef → {{ .Values.secrets.dbSecretRef }}
|
|
Secret {{ .Release.Name }}-{{ .Chart.Name }}-db-secret, fields:
|
|
host DB_HOST, the PostgreSQL hostname
|
|
port DB_PORT, usually 5432
|
|
database POSTGRES_DB
|
|
username POSTGRES_USER
|
|
password POSTGRES_PASSWORD
|
|
|
|
secrets.redisSecretRef → {{ .Values.secrets.redisSecretRef }}
|
|
Secret {{ .Release.Name }}-{{ .Chart.Name }}-redis-secret, field:
|
|
connection-url REDIS_URL, e.g. redis://:password@host:6379/0
|
|
|
|
ROTATING SECRET_KEY_BASE DESTROYS MORE THAN SESSIONS
|
|
In self-hosted mode Sure derives its Active Record encryption keys from
|
|
SECRET_KEY_BASE (config/initializers/active_record_encryption.rb: three
|
|
SHA-256 digests over that one value) unless
|
|
ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY and its two companions are set. Every
|
|
encrypted column in the database — provider tokens, OAuth credentials, MFA
|
|
secrets — is therefore readable only with the SECRET_KEY_BASE it was written
|
|
under. Replacing that value does not just log everyone out: it makes the
|
|
existing encrypted data undecryptable, and no rotation of the item brings it
|
|
back. Treat this field as permanent for the life of the database, or pin the
|
|
three encryption keys explicitly through sure.web.env / sure.worker.env
|
|
before the first start.
|
|
|
|
CHANGING A SECRET'S CONTENT RESTARTS NOTHING
|
|
Helm never sees the content of a OnePasswordItem, so it cannot hash it. The
|
|
pod template carries checksum/secret-ref annotations over the rendered
|
|
OnePasswordItems, so pointing a *SecretRef at a DIFFERENT item rolls the
|
|
pod — but editing a value INSIDE the same item does not: the operator
|
|
updates the Kubernetes Secret while both containers keep the old value in
|
|
their environment. After such a change:
|
|
|
|
kubectl -n {{ .Release.Namespace }} rollout restart statefulset/{{ .Release.Name }}-{{ .Chart.Name }}-sfs
|
|
|
|
The operator's auto-restart annotation would not help here and is
|
|
deliberately not set: it restarts Deployments only, and this is a
|
|
StatefulSet.
|
|
|
|
POSTGRES AND REDIS ARE NOT PART OF THIS CHART
|
|
Both must exist and be reachable before the first start. Redis is not a
|
|
cache the app can do without — it holds the Sidekiq queues, the Rails cache
|
|
store and the rate limiter.
|
|
|
|
What the probes do with that, per container:
|
|
web liveness and startup use /up, which is Rails' boot check and
|
|
touches no database — a Postgres outage therefore does not restart
|
|
the pod. Readiness uses "/", which runs a User.count query, so the
|
|
pod leaves the Service while the database is gone and returns on
|
|
its own afterwards.
|
|
worker all three probes run "sidekiqmon processes" and look for this
|
|
pod's own entry, i.e. a live Sidekiq heartbeat in Redis. There is
|
|
no HTTP and no dependency-free alternative: with Redis gone the
|
|
worker has nothing to do, and liveness only restarts after
|
|
{{ $workerLiveness.failureThreshold }} x {{ $workerLiveness.periodSeconds }}s = {{ mul $workerLiveness.failureThreshold $workerLiveness.periodSeconds }}s of missing heartbeat.
|
|
|
|
A pod that never turns Ready is usually a connection problem, not a chart
|
|
problem: read the container logs, not the pod events.
|
|
|
|
kubectl -n {{ .Release.Namespace }} logs statefulset/{{ .Release.Name }}-{{ .Chart.Name }}-sfs -c {{ .Release.Name }}-{{ .Chart.Name }}-web
|
|
|
|
THE FIRST START MIGRATES, AND THE STARTUP PROBE IS THE CLOCK
|
|
The image entrypoint runs "./bin/rails db:prepare" — but only when the
|
|
command is "./bin/rails server". That is the web container, and only the web
|
|
container: the worker runs "bundle exec sidekiq" and never migrates, so the
|
|
two can never race. Puma binds the port after the migration, which means the
|
|
whole schema creation happens inside the startup probe's budget:
|
|
|
|
web {{ $webStartup.failureThreshold }} x {{ $webStartup.periodSeconds }}s = {{ mul $webStartup.failureThreshold $webStartup.periodSeconds }}s
|
|
worker {{ $workerStartup.failureThreshold }} x {{ $workerStartup.periodSeconds }}s = {{ mul $workerStartup.failureThreshold $workerStartup.periodSeconds }}s (it waits for the schema the web container creates)
|
|
|
|
Raise sure.web.startupProbe.failureThreshold BEFORE a large app-version jump
|
|
or a first import, not after the crash loop has started — a restart lands in
|
|
the middle of a migration. Back up the database before any app-version bump:
|
|
migrations are one-way, and a helm rollback returns the old image to a
|
|
database the new one has already rewritten.
|
|
|
|
ONE POD, ONE VOLUME, TWO CONTAINERS
|
|
Uploads, imports and exports live on the ReadWriteOnce PVC
|
|
{{ .Release.Name }}-{{ .Chart.Name }}-pvc, mounted at /rails/storage in both containers. That is
|
|
the reason web and worker share a pod at all: a background job writes the
|
|
same files the web process serves, and one RWO volume cannot be attached to
|
|
two pods.
|
|
{{- if gt (int $replicas) 1 }}
|
|
|
|
sure.replicas is {{ $replicas }}. Expect the extra pods to stay Pending on
|
|
"Multi-Attach error for volume" unless the storage class is genuinely RWX.
|
|
{{- end }}
|
|
|
|
Backup is two things at the same time: the database holds the accounts and
|
|
one row per attachment, the PVC holds the bytes. Restoring one without the
|
|
other gives an app full of broken references. Because the PVC is rendered by
|
|
the chart, helm uninstall deletes it — the external database survives that,
|
|
which makes an accidental uninstall look recoverable while the files are
|
|
already gone.
|
|
|
|
THE CHART CONFIGURES NO MAIL AND NO AI
|
|
Sure sends invitations and notifications over SMTP and can run an assistant
|
|
against an OpenAI-compatible endpoint. This chart sets neither: no SMTP_*
|
|
variable, no OPENAI_* variable. Mails are therefore quietly never sent, and
|
|
the AI features stay off — nothing in the UI says so.
|
|
|
|
Both are plain environment variables, so they go into sure.web.env and, for
|
|
anything a background job needs (the assistant runs in the worker), into
|
|
sure.worker.env as well. The secretKeyRef form is available for the API key
|
|
and the SMTP password. Note that upstream sizes AI usage at 1 GB of memory
|
|
per process; the chart's computed memory limit is 3x its request.
|
|
{{- if or .Values.sure.web.env .Values.sure.worker.env }}
|
|
|
|
Currently set: {{ len .Values.sure.web.env }} extra variable(s) on the web container,
|
|
{{ len .Values.sure.worker.env }} on the worker, rendered after the ones the chart sets itself.
|
|
An entry repeating a name the chart already sets wins at install time and is
|
|
silently lost on the next helm upgrade — read it back off the StatefulSet.
|
|
{{- end }}
|