From 3e378170dfa57827d3598ad4e12433186e2a3b52 Mon Sep 17 00:00:00 2001 From: jochens Date: Tue, 29 Sep 2026 23:41:47 +0200 Subject: [PATCH] tautulli: its config is placed, it runs the build in use, and its runtime finds its own key 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". --- modules/tautulli/client.ts | 35 +++++++++++++++++++++++++++++++---- modules/tautulli/module.json | 19 ++++++++++++------- 2 files changed, 43 insertions(+), 11 deletions(-) diff --git a/modules/tautulli/client.ts b/modules/tautulli/client.ts index 69c71d5..5a6c48c 100644 --- a/modules/tautulli/client.ts +++ b/modules/tautulli/client.ts @@ -41,6 +41,32 @@ function meshConfig(file?: string): Record { catch { return {}; } } +/** + * The API key Tautulli minted for itself, read from its own config.ini (mounted read-only). + * + * Tautulli owns this key: it writes it on first run and every client of its API — this runtime + * included — must present the same one. So the mesh does not mint or hold it; the one place it + * lives is the file Tautulli keeps, and a key regenerated in Tautulli's settings is simply read + * again on the next start. Undefined when there is no file or no key yet (a fresh install whose + * setup wizard has not run). + */ +function keyOfTautulli(dir?: string): string | undefined { + if (!dir) return undefined; + let ini: string; + try { ini = readFileSync(`${dir.replace(/\/$/, "")}/config.ini`, "utf8"); } + catch { return undefined; } + let section = ""; + for (const raw of ini.split(/\r?\n/)) { + const line = raw.trim(); + const header = /^\[(.+)\]$/.exec(line); + if (header) { section = header[1]; continue; } + if (section !== "General") continue; + const kv = /^api_key\s*=\s*"?([^"]*)"?$/.exec(line); + if (kv && kv[1]) return kv[1]; + } + return undefined; +} + export class TautulliClient { readonly baseUrl: string; @@ -52,15 +78,16 @@ export class TautulliClient { } /** - * Build from the module's resolved environment. The API key is read from MESH_TAUTULLI_APIKEY - * (Tautulli mints it in Settings → Web Interface); the base URL defaults to the local container. + * Build from the module's resolved environment. The API key is the one Tautulli minted for + * itself (Settings → Web Interface), read from its config.ini under MESH_TAUTULLI_CONFIG_DIR; + * MESH_TAUTULLI_APIKEY still wins where it is set. The base URL defaults to the local container. * Throws when no key is configured — the module then contributes nothing rather than failing. */ static fromEnv(env: NodeJS.ProcessEnv = process.env): TautulliClient { const cfg = meshConfig(env.MESH_TAUTULLI_CONFIG_FILE); const url = cfg.url ?? (env.MESH_TAUTULLI_URL ?? `http://127.0.0.1:${env.TAUTULLI_PORT ?? "8181"}`); - const apiKey = cfg.apiKey ?? env.MESH_TAUTULLI_APIKEY; - if (!apiKey) throw new Error("no Tautulli API key — set MESH_TAUTULLI_APIKEY"); + const apiKey = cfg.apiKey ?? env.MESH_TAUTULLI_APIKEY ?? keyOfTautulli(env.MESH_TAUTULLI_CONFIG_DIR); + if (!apiKey) throw new Error("no Tautulli API key — Tautulli's config.ini has none yet (finish its setup and enable the API)"); return new TautulliClient(url, apiKey); } diff --git a/modules/tautulli/module.json b/modules/tautulli/module.json index f59f22a..715afc0 100644 --- a/modules/tautulli/module.json +++ b/modules/tautulli/module.json @@ -16,7 +16,7 @@ "port": 8181, "protocol": "tcp", "from": "mesh", - "why": "watch statistics" + "why": "the watch statistics pages and API" } ], "resources": [ @@ -26,10 +26,15 @@ "path": "/var/lib/mesh/tautulli", "mode": "0700" }, + { + "id": "state", + "type": "directory", + "mode": "0700", + "place": "." + }, { "id": "config", "type": "directory", - "path": "/services/tautulli/config", "mode": "0700", "owner": "1000:1000" }, @@ -37,7 +42,7 @@ "id": "server", "type": "container", "name": "tautulli", - "image": "lscr.io/linuxserver/tautulli@sha256:13f03ecfc61a7af89d492677389771ac29682a72153ce08a5cd4faffbc0197e8", + "image": "lscr.io/linuxserver/tautulli@sha256:e35570d636471f8aabfac7044057712679f954844a763ba735baa9659ea142b5", "env": { "PUID": "1000", "PGID": "1000", @@ -47,7 +52,7 @@ "8181" ], "volumes": [ - "/services/tautulli/config:/config" + "${dir:config}:/config" ] }, { @@ -66,11 +71,11 @@ "volumes": [ "/var/lib/mesh/tautulli/broker:/run/secrets/broker:ro", "/var/lib/mesh/tautulli/config.json:/run/config/config.json:ro", - "/services/tautulli/config:/var/lib/tautulli/config:ro" + "${dir:config}:/var/lib/tautulli/config:ro" ], "env": { "MESH_BROKER_FILE": "/run/secrets/broker", - "MESH_TAUTULLI_URL": "http://127.0.0.1:8181", + "MESH_TAUTULLI_URL": "http://127.0.0.1:${port:8181}", "MESH_TAUTULLI_CONFIG_FILE": "/run/config/config.json", "MESH_TAUTULLI_CONFIG_DIR": "/var/lib/tautulli/config" }, @@ -90,7 +95,7 @@ } }, "binds": { - "route": "/var/lib/mesh/tautulli/route.json" + "route": "${dir:state}/route.json" }, "build": { "on": [