// 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;