Merge pull request 'bazarr, tautulli: read the app's own API key through its container' (#14) from fix/bazarr-tautulli-read-config-through-their-container into main
This commit was merged in pull request #14.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
// bazarr's own API key, found where bazarr keeps it. Shared by the client (tools, events) and the
|
||||
// Servarr step, and kept apart from client.ts so the step and its test load it without the client.
|
||||
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { readFileSync } from "node:fs";
|
||||
|
||||
/**
|
||||
@@ -10,12 +11,21 @@ import { readFileSync } from "node:fs";
|
||||
* stays right if the operator regenerates the key in bazarr's settings screen. The file is read, never
|
||||
* written. Undefined when the file or the key is not there.
|
||||
*/
|
||||
export function apiKeyFromConfigDir(configDir?: string): string | undefined {
|
||||
if (!configDir) return undefined;
|
||||
let text: string;
|
||||
try { text = readFileSync(`${configDir.replace(/\/$/, "")}/config/config.yaml`, "utf8"); }
|
||||
catch { return undefined; }
|
||||
return apiKeyFromConfigYaml(text);
|
||||
export function apiKeyFromConfigDir(configDir?: string, container?: string): string | undefined {
|
||||
if (!configDir && !container) return undefined;
|
||||
let text: string | undefined;
|
||||
if (configDir) {
|
||||
try { text = readFileSync(`${configDir.replace(/\/$/, "")}/config/config.yaml`, "utf8"); }
|
||||
catch { text = undefined; }
|
||||
}
|
||||
// **Through bazarr's own container when this machine's account may not read its directory.** The
|
||||
// directory belongs to the account bazarr runs as here, mode 0700; the node's runtime is another
|
||||
// account. The container is bazarr, and reads its own file.
|
||||
if (text === undefined && container) {
|
||||
try { text = execFileSync("docker", ["exec", container, "cat", "/config/config/config.yaml"], { encoding: "utf8", timeout: 10_000 }); }
|
||||
catch { return undefined; }
|
||||
}
|
||||
return text === undefined ? undefined : apiKeyFromConfigYaml(text);
|
||||
}
|
||||
|
||||
/** `auth.apikey` from the text of bazarr's config.yaml — a top-level `auth:` mapping, one level deep. */
|
||||
|
||||
@@ -70,7 +70,7 @@ export class BazarrClient {
|
||||
const url = cfg.url ?? env.MESH_BAZARR_URL;
|
||||
const apiKey =
|
||||
cfg.apiKey ??
|
||||
apiKeyFromConfigDir(env.MESH_BAZARR_CONFIG_DIR) ??
|
||||
apiKeyFromConfigDir(env.MESH_BAZARR_CONFIG_DIR, env.MESH_BAZARR_CONTAINER) ??
|
||||
readSecret(env.MESH_BAZARR_API_KEY_FILE) ??
|
||||
env.MESH_BAZARR_API_KEY;
|
||||
if (!url) throw new Error("no Bazarr URL — set MESH_BAZARR_URL");
|
||||
|
||||
@@ -104,7 +104,8 @@
|
||||
"env": {
|
||||
"MESH_BAZARR_URL": "http://127.0.0.1:${port:6767}",
|
||||
"MESH_BAZARR_CONFIG_DIR": "${dir:config}",
|
||||
"MESH_SERVARR_DIR": "${dir:state}"
|
||||
"MESH_SERVARR_DIR": "${dir:state}",
|
||||
"MESH_BAZARR_CONTAINER": "bazarr"
|
||||
},
|
||||
"restart-on": [
|
||||
"bound-sonarr-api",
|
||||
@@ -152,7 +153,8 @@
|
||||
"env": {
|
||||
"MESH_BAZARR_URL": "http://127.0.0.1:${port:6767}",
|
||||
"MESH_BAZARR_CONFIG_FILE": "${dir:state}/config.json",
|
||||
"MESH_BAZARR_CONFIG_DIR": "${dir:config}"
|
||||
"MESH_BAZARR_CONFIG_DIR": "${dir:config}",
|
||||
"MESH_BAZARR_CONTAINER": "bazarr"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -31,7 +31,7 @@ if (!bazarrUp) {
|
||||
}
|
||||
|
||||
// Read after bazarr answers: on a first start bazarr writes its config.yaml, key included, as it boots.
|
||||
const apiKey = apiKeyFromConfigDir(process.env.MESH_BAZARR_CONFIG_DIR);
|
||||
const apiKey = apiKeyFromConfigDir(process.env.MESH_BAZARR_CONFIG_DIR, process.env.MESH_BAZARR_CONTAINER);
|
||||
if (!apiKey) {
|
||||
console.error("[bazarr-servarr] no bazarr API key in bazarr's config/config.yaml under MESH_BAZARR_CONFIG_DIR");
|
||||
process.exit(1);
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
// { response: { result: "success" | "error", message, data } }. This client unwraps that envelope
|
||||
// and hands back only the data.
|
||||
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { readFileSync } from "node:fs";
|
||||
|
||||
export interface TautulliSession {
|
||||
@@ -50,11 +51,20 @@ function meshConfig(file?: string): Record<string, string> {
|
||||
* again on the next start. Undefined when there is no file or no key yet (a fresh install whose
|
||||
* setup wizard has not run).
|
||||
*/
|
||||
export function keyOfTautulli(dir?: string): string | undefined {
|
||||
if (!dir) return undefined;
|
||||
let ini: string;
|
||||
try { ini = readFileSync(`${dir.replace(/\/$/, "")}/config.ini`, "utf8"); }
|
||||
catch { return undefined; }
|
||||
export function keyOfTautulli(dir?: string, container?: string): string | undefined {
|
||||
if (!dir && !container) return undefined;
|
||||
let ini: string | undefined;
|
||||
if (dir) {
|
||||
try { ini = readFileSync(`${dir.replace(/\/$/, "")}/config.ini`, "utf8"); }
|
||||
catch { ini = undefined; }
|
||||
}
|
||||
// Through Tautulli's own container when this machine's account may not read its directory: it
|
||||
// belongs to the account Tautulli runs as here, mode 0700, and the node's runtime is another.
|
||||
if (ini === undefined && container) {
|
||||
try { ini = execFileSync("docker", ["exec", container, "cat", "/config/config.ini"], { encoding: "utf8", timeout: 10_000 }); }
|
||||
catch { return undefined; }
|
||||
}
|
||||
if (ini === undefined) return undefined;
|
||||
let section = "";
|
||||
for (const raw of ini.split(/\r?\n/)) {
|
||||
const line = raw.trim();
|
||||
@@ -86,7 +96,7 @@ export class TautulliClient {
|
||||
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 ?? keyOfTautulli(env.MESH_TAUTULLI_CONFIG_DIR);
|
||||
const apiKey = cfg.apiKey ?? env.MESH_TAUTULLI_APIKEY ?? keyOfTautulli(env.MESH_TAUTULLI_CONFIG_DIR, env.MESH_TAUTULLI_CONTAINER);
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -106,7 +106,8 @@
|
||||
"env": {
|
||||
"MESH_TAUTULLI_URL": "http://127.0.0.1:${port:8181}",
|
||||
"MESH_TAUTULLI_CONFIG_DIR": "${dir:config}",
|
||||
"MESH_PLEX_DIR": "${dir:state}"
|
||||
"MESH_PLEX_DIR": "${dir:state}",
|
||||
"MESH_TAUTULLI_CONTAINER": "tautulli"
|
||||
},
|
||||
"restart-on": [
|
||||
"server",
|
||||
@@ -150,7 +151,8 @@
|
||||
"env": {
|
||||
"MESH_TAUTULLI_URL": "http://127.0.0.1:${port:8181}",
|
||||
"MESH_TAUTULLI_CONFIG_FILE": "${dir:mesh-state}/config.json",
|
||||
"MESH_TAUTULLI_CONFIG_DIR": "${dir:config}"
|
||||
"MESH_TAUTULLI_CONFIG_DIR": "${dir:config}",
|
||||
"MESH_TAUTULLI_CONTAINER": "tautulli"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -19,7 +19,7 @@ const waitSeconds = Number(process.env.MESH_TAUTULLI_WAIT_SECONDS ?? "180");
|
||||
const http: Http = { fetch: (u, init) => fetch(u, init) };
|
||||
const read = (path: string) => readFile(path, "utf8").catch(() => undefined);
|
||||
|
||||
const apiKey = keyOfTautulli(process.env.MESH_TAUTULLI_CONFIG_DIR);
|
||||
const apiKey = keyOfTautulli(process.env.MESH_TAUTULLI_CONFIG_DIR, process.env.MESH_TAUTULLI_CONTAINER);
|
||||
if (!apiKey) {
|
||||
console.error("[tautulli-plex] Tautulli's config.ini holds no API key yet — it writes one on its first start");
|
||||
process.exit(1);
|
||||
|
||||
Reference in New Issue
Block a user