Validate Helm Charts / Detect changed charts (pull_request) Successful in 3s
Validate Helm Charts / Resource quantities (pull_request) Successful in 8s
Validate Helm Charts / Build ${{ fromJSON(needs.detect-changed-charts.outputs.charts) }} (pull_request) Successful in 4s
Validate Helm Charts / Trivy config ${{ fromJSON(needs.detect-changed-charts.outputs.scan-charts) }} (pull_request) Successful in 10s
Validate Helm Charts / Install-test ${{ fromJSON(needs.detect-changed-charts.outputs.install-charts) }} (pull_request) Failing after 15m54s
One StatefulSet with two containers - the Puma web process and a Sidekiq worker - sharing one ReadWriteOnce PVC at /rails/storage. That shared volume is the reason they live in one pod: splitting them would need an RWX volume. Only the web container migrates, because the image entrypoint runs db:prepare solely for the './bin/rails server' command; the worker is started through args: so the entrypoint (and its jemalloc preload) stays in place and the two can never race. Probes are chosen for what they prove: liveness and startup use Rails' /up, which is a boot check and touches no database, while readiness uses '/', which runs User.count through authenticate_user! in self-hosted mode and therefore fails while Postgres is unreachable. The worker speaks no HTTP, so all three of its probes ask sidekiqmon whether this pod still holds a Sidekiq heartbeat in Redis. Three OnePasswordItem CRs (app, database, redis), ingress.domain kept required outside the Ingress because APP_DOMAIN and the WebAuthn relying party read it, strict values.schema.json, CI fixtures with throwaway Postgres 16 and Redis, and a Renovate manager on semver so the -alpha/-rc/-hotfix channel and the moving tags are never selected.
166 lines
9.3 KiB
Plaintext
166 lines
9.3 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) -}}
|
|
Sure {{ .Chart.AppVersion }} — release {{ .Release.Name }} in namespace {{ .Release.Namespace }}.
|
|
|
|
{{- if .Values.ingress.enabled }}
|
|
|
|
ACCESS
|
|
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 }}
|
|
|
|
ACCESS — NO INGRESS
|
|
ingress.enabled is false, so this release has no Ingress 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 the Ingress off 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 }} 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 }}
|