tautulli: placed config, the build ace runs, and a runtime that reads its own key #151

Closed
mesh-admin wants to merge 2 commits from feat/tautulli-for-ace into main
Contributor

Prepares tautulli for ace's HAL → nox-mesh move (preparation only, nothing deployed).

  • config is a placed directory (${dir:config}); the route binds into the placed state (${dir:state}/route.json). No /services/... path left (ADR 0112).
  • Image pin: catalogue had v2.18.1-ls242; ace runs ls244 (sha256:e35570d6…). Tautulli migrates its own DB schema, so the pin follows what ace runs.
  • The runtime used http://127.0.0.1:8181 (the software port); it now uses ${port:8181}, as gitea does.
  • The runtime's API key could only come from env or the settings-merged config.json (so a secret in settings). Tautulli mints and owns the key in its config.ini, which the runtime already mounted read-only but never read. client.ts now reads [General] api_key from there. Nothing to mint or accept.

Verified: go test ./internal/catalogue/ -run Catalogue with MESH_CATALOGUE set (not skipped). The pinned image in a throwaway container answers /status 200, and on start it re-owns a 1001:2000 /config to PUID/PGID 1000. client.ts run under node read the key from that instance's config.ini, and get_activity and get_history returned success.

Prepares tautulli for ace's HAL → nox-mesh move (preparation only, nothing deployed). - `config` is a placed directory (`${dir:config}`); the route binds into the placed state (`${dir:state}/route.json`). No `/services/...` path left (ADR 0112). - Image pin: catalogue had v2.18.1-ls242; ace runs **ls244** (`sha256:e35570d6…`). Tautulli migrates its own DB schema, so the pin follows what ace runs. - The runtime used `http://127.0.0.1:8181` (the software port); it now uses `${port:8181}`, as gitea does. - The runtime's API key could only come from env or the settings-merged config.json (so a secret in settings). Tautulli mints and owns the key in its `config.ini`, which the runtime already mounted read-only but never read. `client.ts` now reads `[General] api_key` from there. Nothing to mint or accept. Verified: `go test ./internal/catalogue/ -run Catalogue` with MESH_CATALOGUE set (not skipped). The pinned image in a throwaway container answers `/status` 200, and on start it re-owns a 1001:2000 `/config` to PUID/PGID 1000. `client.ts` run under node read the key from that instance's config.ini, and get_activity and get_history returned success.
mesh-admin added 1 commit 2026-09-29 21:41:58 +00:00
The module stated /services/tautulli/config and /var/lib/mesh/tautulli/route.json,
novox's layout, which no definition may carry (ADR 0112). Tautulli's config dir is
now a placed directory (${dir:config}) and the route binds into the placed state
(${dir:state}), as searxng and mosquitto do.

The image was pinned to v2.18.1-ls242; ace runs ls244 (2026-09-11), and Tautulli
migrates its own database schema, so the pin moves to the digest ace runs.

The runtime called http://127.0.0.1:8181, which is the software's port, not the
machine port the mesh assigns; it now asks with ${port:8181}, as gitea does.

The runtime mounted Tautulli's config dir read-only but never read it: its API key
could only come from MESH_TAUTULLI_APIKEY or the settings-merged config.json, i.e.
a secret in settings. Tautulli mints and owns that key in its config.ini, so the
runtime now reads it from there. Nothing for the mesh to mint or accept.

Verified: catalogue tests with MESH_CATALOGUE pointing here (not skipped); the
pinned image started in a throwaway container on a dir owned 1001:2000 with
PUID/PGID 1000 answers /status 200 and re-owns /config to 1000:1000 on start;
client.ts, run under node, read the key from that instance's config.ini and got
success from get_activity and get_history; without a config.ini it throws, which
the tools and events entrypoints already treat as "not configured".
jschoubben added 1 commit 2026-09-30 11:14:29 +00:00
Tautulli reached plex at 172.18.0.1, the gateway of a HAL network that goes
away with HAL, and the plan was to retype it by hand in the window. Tautulli
now requires plex-api, and where plex is comes from the binding.

Tautulli keeps the connection only in config.ini, reads it at start and
writes its whole config back on every shutdown; its API cannot set it and
its settings form needs an admin login. So a step after start would be
overwritten the moment the container is recreated. The write is made where
nothing can overwrite it: the linuxserver image's custom-init runs
plex/mesh-plex.py as root before Tautulli starts, and the server restarts on
its binding and credential, so a moved plex or an accepted token lands.

It writes only [PMS] keys, only when they differ, every other line byte for
byte: pms_ip, pms_port, pms_ssl and pms_url from the binding; pms_identifier
from plex's /identity; pms_token only when plex takes it. A minted value -
before the operator accepts the server's X-Plex-Token for this pair - is
never written, while the address still is, so Tautulli's own working token
keeps working at plex's new address.

A failure in custom-init is a log line nobody reads, so a run-once `plex`
step, declared last so it gates nothing (ADR 0136), checks what the mesh can
report: plex takes the credential (else it names the secret accept), Tautulli
holds the bound URL, and Tautulli says it is connected. It writes nothing.

The script is kept as plex/mesh-plex.py and plex/50-mesh-plex; module.json
carries copies, and a test fails when they differ. Tests run the script with
python3 against a fake plex (skipped where there is none) and the step
against fakes; `npm test` builds first.
Author
Contributor

tautulli now reaches plex through plex-api (commit 991e33f). This replaces the plan's hand edit of pms_ip in the window.

Why the write happens before Tautulli starts. Tautulli keeps its Plex connection only in config.ini. It reads that file at start and writes its whole in-memory config back on every shutdown. Its API has no command that sets the connection, and its settings form needs an admin login. So a step that edits the file after start is overwritten as soon as the container is recreated.

The write therefore runs in the one place nothing can overwrite it. The linuxserver image's custom-init runs plex/mesh-plex.py as root, after the old container has stopped and before Tautulli reads the file. The server container restarts on bound-plex-api and secret-plex-api, so a moved plex or a newly accepted token lands.

What the write changes. Only [PMS] keys, only when they differ, and every other line stays byte for byte:

  • pms_ip, pms_port, pms_ssl and pms_url come from the binding.
  • pms_identifier comes from plex's /identity.
  • pms_token is written only when plex accepts it.

A minted value is never written, but the address still is. So Tautulli's existing working token keeps working at plex's new address even before the accept.

The check step. A custom-init failure is only a log line, so a run-once plex step checks what the mesh can report. It is declared last, so it gates nothing. It fails in three cases:

  • plex refuses the credential; the message names secret accept ace tautulli plex-api --provider ace --from <file>;
  • Tautulli does not hold the bound URL;
  • Tautulli's own server_status is not connected.

The step writes nothing.

Checks run:

  • Tests: 11 in npm test. They cover the script under python3 against a fake plex, the step against fakes, and a check that module.json carries exactly plex/mesh-plex.py and plex/50-mesh-plex.
  • Build: typecheck and the Dockerfile build pass. go test ./internal/catalogue/ passes.
  • End to end: a throwaway Tautulli (the pinned image) against a throwaway Plex, in three runs.
    1. Minted credential: the address was written, the token was not, and the step failed naming the accept.
    2. Accepted token: pms_token was written, Tautulli reported itself connected, and the step passed.
    3. Container recreated again: nothing was written ("already as the mesh says"), and no other config.ini line changed.
  • ace, read-only: today's pms_identifier equals plex's machineIdentifier, so on ace the window changes only ip, url and token.
**tautulli now reaches plex through `plex-api`** (commit 991e33f). This replaces the plan's hand edit of `pms_ip` in the window. **Why the write happens before Tautulli starts.** Tautulli keeps its Plex connection only in config.ini. It reads that file at start and writes its whole in-memory config back on every shutdown. Its API has no command that sets the connection, and its settings form needs an admin login. So a step that edits the file after start is overwritten as soon as the container is recreated. The write therefore runs in the one place nothing can overwrite it. The linuxserver image's custom-init runs `plex/mesh-plex.py` as root, after the old container has stopped and before Tautulli reads the file. The server container restarts on `bound-plex-api` and `secret-plex-api`, so a moved plex or a newly accepted token lands. **What the write changes.** Only `[PMS]` keys, only when they differ, and every other line stays byte for byte: - `pms_ip`, `pms_port`, `pms_ssl` and `pms_url` come from the binding. - `pms_identifier` comes from plex's `/identity`. - `pms_token` is written only when plex accepts it. A minted value is never written, but the address still is. So Tautulli's existing working token keeps working at plex's new address even before the accept. **The check step.** A custom-init failure is only a log line, so a run-once `plex` step checks what the mesh can report. It is declared last, so it gates nothing. It fails in three cases: - plex refuses the credential; the message names `secret accept ace tautulli plex-api --provider ace --from <file>`; - Tautulli does not hold the bound URL; - Tautulli's own `server_status` is not connected. The step writes nothing. **Checks run:** - **Tests:** 11 in `npm test`. They cover the script under python3 against a fake plex, the step against fakes, and a check that module.json carries exactly `plex/mesh-plex.py` and `plex/50-mesh-plex`. - **Build:** typecheck and the Dockerfile build pass. `go test ./internal/catalogue/` passes. - **End to end:** a throwaway Tautulli (the pinned image) against a throwaway Plex, in three runs. 1. Minted credential: the address was written, the token was not, and the step failed naming the accept. 2. Accepted token: `pms_token` was written, Tautulli reported itself connected, and the step passed. 3. Container recreated again: nothing was written ("already as the mesh says"), and no other config.ini line changed. - **ace, read-only:** today's `pms_identifier` equals plex's `machineIdentifier`, so on ace the window changes only ip, url and token.
Author
Contributor

Superseded: this module now lives in novox/mesh-media-catalog (the media chain, consolidated from #145–#168 in stack order; its non-media parts merged via #195). Closing.

Superseded: this module now lives in novox/mesh-media-catalog (the media chain, consolidated from #145–#168 in stack order; its non-media parts merged via #195). Closing.
mesh-admin closed this pull request 2026-09-30 19:45:17 +00:00

Pull request closed

Please reopen this pull request to perform a merge.
Sign in to join this conversation.
No Reviewers
No labels
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: novox/mesh-catalog#151