Files
helm-charts/sure/values.yaml
T
jklein 33d7cedbab
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
feat: Gateway-API as an alternative to the Ingress in eight charts
Group 1 of the #114 sweep: the charts that render exactly one Ingress object
and set no behaviour-bearing nginx annotation - bentopdf, garage, patchmon,
rallly, searxng, sure, urlaubsverwaltung, voucher-vault.

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

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

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

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

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

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

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

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

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

Part of #114
2026-10-12 16:06:18 +00:00

377 lines
19 KiB
YAML

# Scale the whole chart down without uninstalling it: scaleDown: true renders
# every Deployment/StatefulSet with replicas: 0 (and suspends CronJobs) and
# takes precedence over any per-workload replicas value. PVCs, Secrets and
# Services stay in place; scaleDown: false brings the workloads back up.
scaleDown: false
# 1Password item paths (op://<vault>/<item>). The 1Password operator turns each
# OnePasswordItem into a Kubernetes Secret whose keys are the FIELD NAMES of the
# item; the chart references those field names, so a renamed field is a pod in
# CreateContainerConfigError, not a render error. NOTES.txt lists every field.
#
# Helm never sees the CONTENT of these secrets, so no checksum/config-style
# annotation can cover them (README, guideline 9). The pod template does carry a
# checksum/secret-ref over the rendered OnePasswordItems, so REPOINTING one of
# the *SecretRef values at a different item restarts the pod; editing a value
# INSIDE the same item does not, and needs a manual
# kubectl rollout restart statefulset/<release>-sure-sfs
# The operator's operator.1password.io/auto-restart annotation is deliberately
# NOT set: it only ever restarts Deployments, so on this StatefulSet it would
# advertise a guarantee that does not exist (README, guideline 9).
secrets:
# SECRET_KEY_BASE (field: secret-key-base)
appSecretRef: null
# PostgreSQL connection (fields: host, port, database, username, password)
dbSecretRef: null
# Redis connection URL (field: connection-url)
redisSecretRef: null
sure:
# Replica count for the StatefulSet; 0 is allowed and scales it down. The
# chart-global scaleDown flag takes precedence.
#
# Both containers live in the SAME pod, so this value scales web and worker
# together. That is a consequence of the single ReadWriteOnce PVC they share
# at /rails/storage (Active Storage): splitting them into two workloads would
# need a ReadWriteMany volume. Above 1 the extra pods normally stay Pending on
# "Multi-Attach error for volume" - see NOTES.txt.
replicas: 1
# Pod-level hardening, shared by both containers and by the init container.
# The upstream image declares USER 1000:1000 (group and user "rails", created
# in the Dockerfile), so the chart needs no deviation from the repo hardening
# standard; fsGroup 1000 makes the storage PVC writable for that uid.
#
# readOnlyRootFilesystem: true holds, and the writable paths are emptyDirs:
# /rails/tmp, /rails/log and /tmp, one set per container. /tmp needs one extra
# step that is easy to mistake for decoration - a tiny init container creates
# two private subdirectories in it, and the containers run with TMPDIR and
# HOME pointing there. Ruby refuses a world-writable directory without a
# sticky bit as its temporary directory and an emptyDir is exactly that, so
# without this the app aborts during boot; HOME follows because the image's
# /home/rails is read-only and Bundler otherwise builds a throwaway home per
# invocation. The StatefulSet carries the measured evidence for both.
securityContext:
pod:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
fsGroupChangePolicy: Always
# RuntimeDefault applies the container runtime's default seccomp filter,
# which blocks the syscalls a normal workload never makes. It sits in the
# pod context on purpose: from here it covers every container in the pod
# - web and worker alike - including init containers, while a
# container-level seccompProfile would have to be repeated for each one
# (and silently missed on the next one added). A container that genuinely
# needs a different filter can still override it under
# <container>.securityContext.container, which wins over this value.
seccompProfile:
type: RuntimeDefault
# ---------------------------------------------------------------------------
# web - Puma, the container that also runs the database migrations
# ---------------------------------------------------------------------------
web:
securityContext:
container:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
runAsNonRoot: true
capabilities:
drop:
- ALL
resources:
# Limits are optional: memory and ephemeral-storage limits default to
# limitFactor x the request, an explicitly set limit always wins, and a
# CPU limit is only set when configured explicitly under "limits".
#
# The request follows upstream's own sizing note (docs/hosting/docker.md
# for 0.7.4): steady-state memory "around 352 MB", comfortable under a
# 512 MB limit, while "if you enable AI, run at least 1 GB". 512Mi request
# x limitFactor 3 = 1536Mi limit, which holds both the everyday figure and
# the AI recommendation without reserving the larger number on the node.
# Read from upstream's documentation, not measured here.
#
# ephemeral-storage is NOT the template default of 10Mi, and the reason is
# measured: with 10Mi (limit 30Mi) per container the kubelet evicted the
# pod within two minutes of every start - "Pod ephemeral local storage
# usage exceeds the total limit of containers 60Mi". Both emptyDirs and
# the container's stdout log count against that limit, and this image
# rebuilds its Bootsnap cache under /rails/tmp on every pod start because
# the emptyDir hides the one baked into the image. 256Mi request (768Mi
# limit) is headroom for that, not a measurement of it - the steady-state
# figure is still unknown.
limitFactor: 3
requests:
cpu: 0.1
memory: 512Mi
ephemeral-storage: 256Mi
# PROBES - what each one actually proves (README, guideline 12.3)
#
# /up is Rails' own health route (config/routes.rb: get "up" =>
# "rails/health#show"). READ FROM THE RAILS SOURCE, NOT MEASURED HERE: that
# controller renders 200 as soon as the app has booted without raising and
# touches NO database and NO Redis. It is a boot check, and the widespread
# belief that it tests the database is exactly what guideline 12.3 exists to
# catch - upstream's own Helm chart (charts/sure in the app repository) uses
# /up for readiness, which is the mistake this chart does not copy.
#
# Liveness therefore uses /up: a Postgres outage must not restart the pod.
livenessProbe:
httpGet:
path: /up
port: http
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
# Readiness asks for "/" instead, because that path provably reaches the
# database - and it does so through curl rather than an httpGet probe, for
# a reason that was measured rather than guessed (see below).
#
# Why "/" reaches the database: every request runs ApplicationController's
# authenticate_user! before_action (app/controllers/concerns/
# authentication.rb); without a session cookie - which a probe never has -
# it falls into self_hosted_first_login?, i.e.
# "config.app_mode.self_hosted? && User.count.zero?". User.count is an SQL
# query. Measured in the install-test: against an empty database the app
# answered 302 to /registration/new, which is the branch that query decides,
# so the path demonstrably touches Postgres. With the database down the
# query raises and Rails answers 500.
#
# Why exec + curl and not httpGet: the kubelet's HTTP prober FOLLOWS
# redirects to the same host, and with RAILS_ASSUME_SSL=true (which the
# chart sets whenever the Ingress is on) Rails builds that redirect as
# https://<pod-ip>:3000/... - a scheme Puma does not speak. The install-test
# failed with "http: server gave HTTP response to HTTPS client" on a pod
# that was answering correctly. curl without -L asks once and reports the
# status code; the test accepts 2xx and 3xx exactly as the kubelet would,
# and a 500 from a missing database still fails. curl is part of the image
# (the Dockerfile installs it), so nothing extra is pulled in for this.
#
# One condition this depends on: SELF_HOSTED=true, which the chart always
# sets. In managed mode the same code short-circuits before User.count and
# "/" would redirect without touching Postgres at all.
readinessProbe:
exec:
command:
- /bin/bash
- -c
- "code=\"$(curl -sS -o /dev/null -w '%{http_code}' --max-time 5 http://127.0.0.1:3000/)\"; test \"$code\" -ge 200 -a \"$code\" -lt 400"
periodSeconds: 10
timeoutSeconds: 10
failureThreshold: 3
# The startup probe gates liveness and readiness, so neither needs an
# initialDelaySeconds. Its budget has to cover the first start, which is the
# expensive one: the image entrypoint (/rails/bin/docker-entrypoint) runs
# "./bin/rails db:prepare" whenever the command is "./bin/rails server", so
# the web container creates and migrates the schema before Puma binds the
# port. Budget 10s x 60 = 600s. Raise it before a large app-version jump;
# a restart in the middle of a migration is the failure mode it prevents.
startupProbe:
httpGet:
path: /up
port: http
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 60
# Extra environment variables, appended to the ones the chart always sets.
# Values are rendered through tpl; value, secretKeyRef and configMapKeyRef
# forms are supported.
#
# This is also where the AI features go - the chart models none of them as
# values on purpose. OPENAI_ACCESS_TOKEN (secretKeyRef form), OPENAI_MODEL,
# OPENAI_URI_BASE, LLM_CONTEXT_WINDOW, OPENAI_REQUEST_TIMEOUT,
# AI_RESPONSE_TIMEOUT, ASSISTANT_MAX_TOOL_CALL_ITERATIONS and the SMTP_*
# block are plain environment variables to this image. Set them on BOTH
# containers where a background job needs them as well (the assistant runs
# in the worker).
#
# Additive means additive: an entry repeating a name the chart already sets
# reaches the pod twice at install time (the later one, yours, wins) but is
# LOST on a later helm upgrade, because Kubernetes merges env by name and
# the pair collapses to the chart's own value - with a green upgrade either
# way (README, guideline 10).
env: []
# ---------------------------------------------------------------------------
# worker - Sidekiq: the same image, started with "bundle exec sidekiq" as
# its args (the image entrypoint stays in place; see the StatefulSet)
# ---------------------------------------------------------------------------
worker:
securityContext:
container:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
runAsNonRoot: true
capabilities:
drop:
- ALL
resources:
# Same sizing as the web container, for a documented reason: upstream
# notes that the sample/demo-data generator runs in the WORKER and is the
# one operation that deterministically exceeds 512 MB (observed flat at
# ~680 MB on a 2 GB container). The computed limit of 3 x 512Mi = 1536Mi
# covers that; the request stays at the everyday figure.
#
# ephemeral-storage: same 256Mi as the web container and for the same
# measured reason - see the comment there.
limitFactor: 3
requests:
cpu: 0.1
memory: 512Mi
ephemeral-storage: 256Mi
# PROBES - the worker speaks no HTTP at all, so all three are exec probes.
#
# The command asks Sidekiq's own monitor whether THIS process is in the
# process set: sidekiqmon prints one line per registered process, each
# starting with the identity "<hostname>:<pid>:<nonce>" (sidekiq 8.0.5,
# lib/sidekiq/component.rb), and in a pod the hostname is the pod name that
# bash exposes as $HOSTNAME. Sidekiq keeps that registration alive with a
# heartbeat whose Redis key expires after about 60s, so a process that hangs
# disappears from the set while its PID still exists - which is precisely
# what a process check like "pgrep sidekiq" could not tell you (and would
# not tell you anything at all: Sidekiq is PID 1 here, so if it dies the
# container dies with it and the check can never fail - a check a broken
# system passes, README).
#
# READ FROM THE SIDEKIQ 8.0.5 SOURCE, NOT MEASURED: sidekiqmon is a plain
# Ruby entry point that requires sidekiq/api only - it does NOT boot Rails -
# and it reads REDIS_URL from the environment, which the chart sets on this
# container. Cost per check is therefore one Ruby start, not an application
# boot; the periods below are chosen on the assumption that this takes a few
# seconds, and the PR install-test is the first place it actually runs.
#
# Liveness hangs on Redis, which guideline 12.3 allows only where no
# dependency-free path proves anything - that is the case here (see above) -
# and it is given a deliberately generous budget of 60s x 5 = 300s of
# missing heartbeat, so a short Redis blip does not restart the pod.
livenessProbe:
exec:
command:
- /bin/bash
- -c
- bundle exec sidekiqmon processes | grep -q "^${HOSTNAME}:"
periodSeconds: 60
timeoutSeconds: 15
failureThreshold: 5
# Readiness uses the same check with a tighter budget: a worker that is not
# registered in Redis is not doing any work, whatever its process state.
readinessProbe:
exec:
command:
- /bin/bash
- -c
- bundle exec sidekiqmon processes | grep -q "^${HOSTNAME}:"
periodSeconds: 30
timeoutSeconds: 15
failureThreshold: 3
# The startup probe gates the other two. Sidekiq boots the full Rails app
# before it registers, and on a first install it waits for the web container
# to finish db:prepare, so the budget matches the web container's: 20s x 30
# = 600s.
startupProbe:
exec:
command:
- /bin/bash
- -c
- bundle exec sidekiqmon processes | grep -q "^${HOSTNAME}:"
periodSeconds: 20
timeoutSeconds: 15
failureThreshold: 30
# Extra environment variables for the worker, appended to the ones the chart
# always sets. Same rules as sure.web.env above.
env: []
# The Active Storage volume, mounted at /rails/storage in BOTH containers.
# Deliberately not null-safe: an erased block yields an invalid PVC, which fails
# loudly (README, guideline 12).
storage:
amount: 10Gi
storageClass: null
ingress:
# Gates the whole Ingress template. Note that ingress.domain is read three
# more times outside it - APP_DOMAIN (the host Action Mailer builds link URLs
# from), WEBAUTHN_RP_ID and WEBAUTHN_ALLOWED_ORIGINS (the relying party a
# passkey is bound to) - so it keeps its own required() there and stays
# mandatory with the Ingress switched off; only clusterIssuer becomes
# unnecessary (README, guideline 12.1). Without that guard the app would come
# up with WEBAUTHN_RP_ID="" and register passkeys against the wrong relying
# party - rendered cleanly, pod green.
#
# Mutually exclusive with httpRoute.enabled below (Gitea #114): with both
# true the render aborts, naming both values.
#
# 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
clusterIssuer: null
domain: null
className: nginx
# Extra annotations for the Ingress; merged with the cert-manager
# 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: {}
# 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: []