Plex cannot create libraries over the already-mounted 8.33T NFS bulk share while
the server is UNCLAIMED. catalog/plex.yaml carried a long argument for leaving
PLEX_CLAIM unwired; the operator has decided to override it. This implements the
wiring in the way that respects the half of that argument which is still true.
WHAT WAS TRUE AND STILL IS: a https://plex.tv/claim token has a ~4-minute TTL,
far shorter than commit -> push -> Flux reconcile -> pod-roll on its own timers.
So the committed value is a documented PLACEHOLDER and the live token is injected
by the operator inside that window, with the reconcile FORCED rather than waited
for. runbooks/plex-claim.md is that timed procedure.
WHAT WAS NOT TRUE: the old comment claimed "a placeholder/absent claim Secret
would crashloop the pod". Half of that is wrong, and the difference is the whole
design. Established from plexinc/pms-docker's own first-run script (branch
`master`, root/etc/cont-init.d/40-plex-first-run, s6-overlay v2), cross-checked
byte-identical against the same file inside the running :latest image:
* the claim exchange (line 106) is gated on PLEX_CLAIM being non-empty AND the
PlexOnlineToken pref in /config being empty. Once the server is claimed the
block is skipped entirely, so the env is a NO-OP on every later start
whatever it holds ("If server is already logged in, this parameter is
ignored" -- upstream README).
* with an invalid/expired token on a still-unclaimed /config, plex.tv answers
403 with an empty body; curl's status is never checked, the token sed yields
empty, the success branch at line 120 does not fire, there is no else branch
and no non-zero exit, and the script (no `set -e`) exits 0. The pod comes up
UNCLAIMED AND SERVING. It does not crashloop.
* the image never sets S6_BEHAVIOUR_IF_STAGE2_FAILS (default 0 = continue even
if a cont-init script failed), so even a non-zero exit would not halt
startup.
An ABSENT Secret is the real hard failure (secretKeyRef has no `optional`, so it
is CreateContainerConfigError), which is why the Secret is committed, encrypted
and listed in the wave kustomization rather than hand-created at claim time.
NO SCHEMA CHANGE. `optional` on secretKeyRef would only be needed if the pod had
to tolerate a missing Secret or could not start without a VALID token. Neither
holds: the Secret always exists in the Flux graph, and an invalid token is
non-fatal. schemas/servicedefinition.schema.json is untouched, and so are
catalog/render.py's env emission and the renderer contract.
Changes:
* apps/wave-0/plex-claim-secret.sops.yaml -- new Opaque Secret plex-claim/plex,
key PLEX_CLAIM, SOPS-encrypted to the committed age recipient. Its header
carries NO dollar-brace expressions on purpose: kustomize preserves body
comments into the stream envsubst reads (per
conformance/tests/l0-static/flux-postbuild-substitution), so quoting the
upstream shell conditions verbatim would have been a live substitution
hazard on apps-wave-0, not prose.
* catalog/plex.yaml -- the PLEX_CLAIM env via valueFrom.secretKeyRef, the
"deliberately NOT set" block rewritten around the TTL + one-shot design, and
the stale crashloop claim in the /config storage comment corrected.
* apps/wave-0/plex.yaml -- re-rendered with the canonical command
(`--all catalog -o apps --wave-layout --no-image-automation`). No L1 golden
under conformance/tests/l1-rendering covers plex, so this is the only
rendered surface that moves.
* catalog/render.py -- plan_apps_waves places `<svc>-claim-secret.sops.yaml`
in the app's OWN wave, like the bootstrap secret, plus a selftest assertion.
Without this, the next `--emit-waves` would have silently dropped the Secret
from apps/wave-0/kustomization.yaml and put the pod into
CreateContainerConfigError. apps/wave-0/kustomization.yaml is the hand-pruned
subset (vm-demo/keycloak are rendered but deliberately unlisted), so its one
new line was applied without re-emitting the rest.
* runbooks/plex-claim.md + its kustomization.yaml generator entry + README
index -- the operator procedure that fits in 4 minutes, including the single
forced-reconcile command and the mandatory pod roll (a changed Secret does
NOT restart anything: PLEX_CLAIM is read only at container start and the pod
template is byte-identical across this change).
Touched requirements: R-SEC-01 (secrets encrypted at rest), R-GITOPS-02
(ServiceDefinition onboarding contract).
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>