// ombi's connections step — run once by the host after ombi's server starts, and run again whenever // a binding or pair credential it reads changes (the container's `restart-on`, novox/hq ADR 0099). // It brings ombi's connections to Sonarr, Radarr, Lidarr (servarr/settings.ts) and Plex // (plex/settings.ts) in line with what the mesh bound. // // **A step, not a loop**, for the reason route-adapter gives: everything it does is a function of // files the mesh writes, and the host already knows when they change. It connects to no broker. // // Exits non-zero when any app could not be put right — a refused credential, an unreachable app, an // ombi that cannot reach it — so the node reports the step failed and the host runs it again on the // next apply. It is declared last in the manifest, so its failing gates nothing else of ombi's // (novox/hq ADR 0136). One app failing does not stop the others being put right. // // Reads, per provision, `/.json` (the binding) and `/.secret` (the // pair credential), where is MESH_CONNECTIONS_DIR. Never prints a key or a token. import { join } from "node:path"; import { PLEX_PROVISION, reconcilePlex } from "../plex/settings.js"; import { APPS, ombiReady, readBinding, readIfThere, reconcileApp, type Http, type Outcome } from "../servarr/settings.js"; const dir = process.env.MESH_CONNECTIONS_DIR ?? "/run/connections"; const url = process.env.MESH_OMBI_URL ?? "http://127.0.0.1:3579"; const apiKey = (await readIfThere(process.env.MESH_OMBI_API_KEY_FILE))?.trim() ?? process.env.MESH_OMBI_API_KEY ?? ""; const waitSeconds = Number(process.env.MESH_OMBI_WAIT_SECONDS ?? "180"); // Every call bounded: an entry ombi keeps may name a host that no longer answers, and a step that // hangs on it holds the apply. const http: Http = { fetch: (u, init) => fetch(u, { ...init, signal: AbortSignal.timeout(20_000) }) }; if (!apiKey) { console.error("[ombi-connections] no ombi API key — ombi's own `api-key` secret has not been accepted"); process.exit(1); } const ombi = { url, apiKey }; if (!(await ombiReady(http, ombi, waitSeconds * 1000))) { console.error(`[ombi-connections] ombi did not answer at ${url} within ${waitSeconds}s`); process.exit(1); } const inputs = async (provision: string) => [await readBinding(join(dir, `${provision}.json`)), await readIfThere(join(dir, `${provision}.secret`))] as const; const outcomes: Outcome[] = []; for (const spec of APPS) { outcomes.push(await reconcileApp(http, ombi, spec, ...(await inputs(spec.provision)))); } outcomes.push(await reconcilePlex(http, ombi, ...(await inputs(PLEX_PROVISION)))); let failed = 0; for (const outcome of outcomes) { switch (outcome.result) { case "unchanged": console.log(`[ombi-connections] ${outcome.app}: already as the mesh says; connection tested`); break; case "written": console.log(`[ombi-connections] ${outcome.app}: wrote ${outcome.fields.join(", ")}; connection tested`); break; case "refused": failed++; console.error(`[ombi-connections] ${outcome.app}: ${outcome.problem}`); break; } } process.exitCode = failed > 0 ? 1 : 0;