The first install-test run failed with both containers crash-looping
during boot:
system temporary path is world-writable: /tmp
/tmp is world-writable: /tmp
. is not writable: /rails
ArgumentError: could not find a temporary directory
raised from MiniMagick, which Active Storage loads while Rails boots.
Ruby's Dir.tmpdir skips a candidate that is world-writable without a
sticky bit, and a Kubernetes emptyDir is created mode 0777 with no sticky
bit - so with a read-only root filesystem there was no candidate left.
A tiny init container, the app's own image running as the same non-root
uid and with no capability added, now creates one private subdirectory
(mode 0700) inside each tmp emptyDir, and both containers run with
TMPDIR pointing at it. Creating an entry in a 0777 directory and chmod'ing
what one just created needs no privilege, so the hardening standard holds
unchanged.
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.
Walks back the one part of the previous commit that changed rendered output:
storage was pulled into the server fallback, so an erased size fell back to
20Gi instead of rendering a PVC without one.
values.schema.json had documented the opposite decision - "an erased value
yields an invalid PVC rather than a silently degraded workload" - and that is
someone's reasoned trade, not collateral of the nil-pointer bug next to it.
Overturning it inside a patch would have made this the only release where
valid values render differently, and it would have settled an open design
question in a PR nobody opened for that. It goes to #39 instead.
So storage is out of the defaults again and the schema note is restored
verbatim, extended only by where the exception is argued. values.yaml marks
it as the one key of the block that is deliberately not null-safe. The
README rule that had leaned on this example now says the thing that actually
holds: an exception is fine, an unmarked one is not.
Everything else stands - dev and server null-safe, the single-key findings on
the ports, formatNumber against exponential notation, 7.0.1.
Re-measured after the walk-back: rendered output against the chart defaults
and against a full override set is byte-identical to 7.0.0, NOTES.txt
included, and "--set server=null" now renders exactly the sizeless PVC that
"--set server.storage=null" rendered on 7.0.0.
--set dev=null aborted the render at .Values.dev.debug and --set server=null
at .Values.server.replicas, while the third block, game, has fallen back to
the chart defaults since 7.0.0. Half-safe is the worse state of the two:
whoever sees game=null work assumes server=null does too.
Both blocks now run through satisfactory.defaultedDict like game, via two new
helpers next to it. server.resources is deliberately NOT re-defaulted there -
it already runs through satisfactory.effectiveResources, which carries those
defaults and is null-safe on its own, and a second copy of limitFactor: 2 /
4Gi / 1 / 5Gi would be a third place for them to drift.
server is read in four files, not one, and the fourth is NOTES.txt, which
helm template never renders - a fix confined to the manifests would have
looked green and still aborted on a real helm install. All four go through
the helpers.
The single erased key was the quieter half of the bug and is fixed with it:
a nulled server.gamePort rendered "port:" and "nodePort:" empty into the
Service and an empty containerPort into the StatefulSet, and a nulled
server.storage a PVC asking for no size - manifests rejected by nothing until
the API server saw them. server.storage was documented in values.schema.json
as deliberately not null-safe; that reasoning does not survive the block-level
case, because it made "server: null" render a sizeless PVC instead of falling
back like everything else. The schema note is replaced rather than left lying.
Rendered output for valid values is unchanged: helm template against the
chart defaults and against a full override set is byte-identical to 7.0.0,
and so is NOTES.txt. Hence a patch bump, appVersion untouched.
Fixes#16
The shared jlab-renovate-bot instance serves this repo and cluster-config,
and two repos behaving differently under one bot is a source of surprises
rather than of safety. This adopts cluster-config's configuration:
config:best-practices in extends, labels ["renovate"], no PR caps
(prHourlyLimit 0, prConcurrentLimit 100), separateMinorPatch, an explicit
dependencyDashboard, and global automerge with platformAutomerge.
Two of those choices were previously wrong here and are now right, because
the branch protection on main has caught up: a review requirement and
validate-charts as a required check are both active. platformAutomerge
therefore arms the PR on the forge and Gitea merges it when the check
passes AND the review exists - the review is the gate, which is what makes
automerging majors something other than unattended merging.
Comments that asserted the opposite are rewritten rather than left to rot:
the platformAutomerge: false comment ("not (yet) a required check on either
side"), the "Majors stay manual" rule, the README PR policy bullets and the
required-check paragraph, and the CLAUDE.md forge notes.
config:best-practices pulls in docker:pinDigests, which is switched off
again for custom.regex and helm-values: an appVersion: is a tag by
construction and has nowhere to put a digest. Kept deliberately:
minimumReleaseAge of three days on the routine updates (every merge here
publishes a chart), the ignorePaths and ci/** opt-outs, every per-chart
manager, the nginx and qbittorrent constraints, and postUpgradeTasks.
Fixes#34
Work on this repo happens in a Coder workspace that has no 1Password CLI.
The rule that tea may only be called as 'op plugin run -- tea' therefore
described an environment that no longer exists here, and following it meant
either waiting for a tool that is not coming or ending with a pushed branch
and no PR.
tea is now called directly. It carries the workspace's stored login, so what
it opens is authored by the bot account - accepted, and named in the report
when it happens. The older instruction is kept for machines that do have op.
Signing needed the same correction: it runs through the workspace's own SSH
signer, not op-ssh-sign, and the check is 'git log --format=%G?' printing G.
The wiki now carries one page per chart (prerequisites, installation, the
required 1Password fields, the complete values reference and the chart's
traps) plus the workflow, the pipelines, versioning, Renovate, the style
guide and the incidents behind each rule.
README points at it and keeps its own role: the overview for someone reading
the code, while anything an operator needs lives in the wiki. CLAUDE.md gains
the wiki edit as step 7 of the mandatory workflow, with a table of what
obliges an edit - and the one thing that does not, a plain app-version bump,
since Renovate automerges those and never touches the wiki.
Also corrects the patchmon row: the 2.x server serves frontend and /api/* from
one process, so the chart renders one Deployment plus an optional guacd
sidecar, not two Deployments.