Wires PLEX_CLAIM into the plex Deployment from a SOPS-encrypted Secret so the
server can be claimed to a plex.tv account. Plex will not create libraries over
the mounted 8.33T NFS bulk share while the server is unclaimed.
The committed value is the documented placeholder, not a credential. The
Secret exists so the valueFrom.secretKeyRef is always resolvable in the Flux
graph (the ServiceDefinition schema's secretKeyRef shape has no optional, so an
absent Secret is a hard CreateContainerConfigError). The live token is injected
separately by the operator.
Adds runbooks/plex-claim.md with the claim procedure.
Gate: ./ci/gate.sh green locally in the pinned toolbox — lint, l0,
render-golden and l1 all green in 74s. The only non-green stage is
spec-immutability, which cannot run outside CI's spec checkout and which gate.yml tolerates explicitly.
This PR is also being used to measure the real end-to-end wall time of the
branch -> PR -> gate -> merge -> reconcile -> pod-roll loop, so the claim-token
step that follows can be budgeted against a measured number instead of an
assumed one.
Wires `PLEX_CLAIM` into the plex Deployment from a SOPS-encrypted Secret so the
server can be claimed to a plex.tv account. Plex will not create libraries over
the mounted 8.33T NFS bulk share while the server is unclaimed.
**The committed value is the documented placeholder, not a credential.** The
Secret exists so the `valueFrom.secretKeyRef` is always resolvable in the Flux
graph (the ServiceDefinition schema's secretKeyRef shape has no `optional`, so an
absent Secret is a hard CreateContainerConfigError). The live token is injected
separately by the operator.
Adds `runbooks/plex-claim.md` with the claim procedure.
Gate: `./ci/gate.sh` green locally in the pinned toolbox — lint, l0,
render-golden and l1 all green in 74s. The only non-green stage is
spec-immutability, which cannot run outside CI's spec checkout and which
`gate.yml` tolerates explicitly.
This PR is also being used to measure the real end-to-end wall time of the
branch -> PR -> gate -> merge -> reconcile -> pod-roll loop, so the claim-token
step that follows can be budgeted against a measured number instead of an
assumed one.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
🤖 Generated with [Claude Code](https://claude.com/claude-code)
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]>
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Wires
PLEX_CLAIMinto the plex Deployment from a SOPS-encrypted Secret so theserver can be claimed to a plex.tv account. Plex will not create libraries over
the mounted 8.33T NFS bulk share while the server is unclaimed.
The committed value is the documented placeholder, not a credential. The
Secret exists so the
valueFrom.secretKeyRefis always resolvable in the Fluxgraph (the ServiceDefinition schema's secretKeyRef shape has no
optional, so anabsent Secret is a hard CreateContainerConfigError). The live token is injected
separately by the operator.
Adds
runbooks/plex-claim.mdwith the claim procedure.Gate:
./ci/gate.shgreen locally in the pinned toolbox — lint, l0,render-golden and l1 all green in 74s. The only non-green stage is
spec-immutability, which cannot run outside CI's spec checkout and which
gate.ymltolerates explicitly.This PR is also being used to measure the real end-to-end wall time of the
branch -> PR -> gate -> merge -> reconcile -> pod-roll loop, so the claim-token
step that follows can be budgeted against a measured number instead of an
assumed one.
Co-Authored-By: Claude Opus 5 (1M context) [email protected]
🤖 Generated with Claude Code