sonarr: its config dir is placed, its image is the one ace runs #157

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

Converts sonarr for ace's HAL → nox-mesh cutover. Conversion only — nothing is assigned.

Depends on #156 (feat/servarr-api-provision is merged into this branch: sonarr provides sonarr-api). Merge #156 first; until then this diff also shows #156's commits.

Changes

  • config is a pathless ${dir:config} (0700, 1000:1000); the route binding moves to a placed state dir (${dir:state}/route.json). /var/lib/mesh/sonarr stays for the broker secret. No host path of the module's own remains (ADR 0112).
  • Image pinned to 4.0.20.3014-ls325, the digest ace runs. The old pin (4.0.19.2979-ls322) was older than the running version — never safe against data Sonarr has already migrated forward.
  • Container mount points stay /series, /anime, /downloads: the database stores root folders and download paths under those names and has no remote path mappings.
  • Generic access paths unchanged; ace's /storage/... paths and the media owner 1001:2000 wait on hq 153.

Verified

  • MESH_CATALOGUE=<worktree> go test -count=1 ./internal/catalogue/ — 73 manifests parsed (not skipped).
  • Throwaway container of the pinned image on a fresh 0700 1000:1000 config dir: /ping OK, v3 API answers with its generated key, a wrong key gets 401, the root folder is writable; the runtime's client (client.ts) discovers the key from that config.xml and reads the queue. Removed afterwards.

Not in this PR (needs a decision, reported to the operator)

Sonarr reaches its download clients (NZBGet, qBittorrent) and Jackett's Torznab feeds by container name on HAL's network (nzbget:6789, qbittorrent:8112, jackett:9117). Expressing that as provisioning needs providers in nzbget, qbittorrent and jackett (outside this change) and a consumer step writing host/port/key into Sonarr's own database through its API, like ombi's in #156. Until then those connections are repointed by hand in the migration window.

Converts **sonarr** for ace's HAL → nox-mesh cutover. Conversion only — nothing is assigned. **Depends on #156** (feat/servarr-api-provision is merged into this branch: sonarr provides `sonarr-api`). Merge #156 first; until then this diff also shows #156's commits. ## Changes - `config` is a pathless `${dir:config}` (0700, 1000:1000); the route binding moves to a placed `state` dir (`${dir:state}/route.json`). `/var/lib/mesh/sonarr` stays for the broker secret. No host path of the module's own remains (ADR 0112). - Image pinned to **4.0.20.3014-ls325**, the digest ace runs. The old pin (4.0.19.2979-ls322) was older than the running version — never safe against data Sonarr has already migrated forward. - Container mount points stay `/series, /anime, /downloads`: the database stores root folders and download paths under those names and has no remote path mappings. - Generic access paths unchanged; ace's `/storage/...` paths and the media owner 1001:2000 wait on hq 153. ## Verified - `MESH_CATALOGUE=<worktree> go test -count=1 ./internal/catalogue/` — 73 manifests parsed (not skipped). - Throwaway container of the pinned image on a fresh 0700 1000:1000 config dir: `/ping` OK, v3 API answers with its generated key, a wrong key gets 401, the root folder is writable; the runtime's client (`client.ts`) discovers the key from that `config.xml` and reads the queue. Removed afterwards. ## Not in this PR (needs a decision, reported to the operator) Sonarr reaches its download clients (NZBGet, qBittorrent) and Jackett's Torznab feeds by container name on HAL's network (`nzbget:6789`, `qbittorrent:8112`, `jackett:9117`). Expressing that as provisioning needs providers in nzbget, qbittorrent and jackett (outside this change) and a consumer step writing host/port/key into Sonarr's own database through its API, like ombi's in #156. Until then those connections are repointed by hand in the migration window.
mesh-admin added 6 commits 2026-09-30 09:53:58 +00:00
ombi's definition named /services/ombi/config (a HAL machine path) in three
places and pinned an image older than the one ace runs. Ombi migrates its
own SQLite schema, so a take onto the older pin (v4.53.10-ls267) would start
it on a database the newer build (ls269) already touched.

- config is a pathless placed directory, mounted as ${dir:config}
- a state directory placed at the assignment root carries route.json
- image pinned to the digest ace runs today (v4.53.10-ls269)
- the sidecar reaches ombi on the machine port the mesh assigns
  (${port:3579}) rather than assuming 3579 is free
- the sidecar no longer mounts ombi's data directory: MESH_OMBI_CONFIG_DIR
  is read by no code, and the mount exposed the databases for nothing

Verified: catalogue tests (MESH_CATALOGUE set, 6 pass, none skipped); the
pinned image starts as PUID 1000 in a 0700 dir and answers /api/v1/Status
200; data owned 1001:2000 (ace's media ids) under a 1000:1000 dir is
re-owned by the image's init and serves 200; a minted ApiKey is refused
(401) - the api-key secret must be accepted from ombi's own settings.
A host-network sidecar reaches its service over the machine's loopback, and
the mesh publishes that service on a machine port it assigns (ADR 0038) —
so dialling the software's port reaches whatever else holds it. On ace,
searxng's sidecar dialled 127.0.0.1:8080 and got unifi's inform port. The
same shape in bazarr, bookshelf, lidarr, nzbget, qbittorrent, radarr and
sonarr; each now asks with ${port:N} (hq 088). Found in review of ace's
module preparation.
A consumer on another machine (ombi first; jackett, bazarr and home-assistant
later) reached these by container name on HAL's shared network, which the mesh
does not have. Each app now provides <app>-api at mesh scope and serves the
software port, so the mesh tells a consumer where it is and redirects the
port to where the machine published it.

Named per app, not one servarr-api: a requirement is matched by name and
answered by exactly one provider per node, so a consumer cannot require one
name from three providers - and ombi's code is written against each app's
own API version (ADR 0027).

No grants and no provisioner: a Servarr instance has one API key, which the
mesh cannot mint. The operator accepts it as the pair credential for each
consumer (ADR 0092).
ombi keeps its Servarr connections in its own database, so the mesh has no
file to write them into. A run-once step reads the three bindings and pair
credentials and writes host, port, TLS, base path and key into ombi through
ombi's own API - only when they differ, and nothing else ombi keeps.

Until the operator accepts an app's key for this pair the mesh delivers a
value it minted, which no Servarr app accepts. The step tries the key against
the app first and, refused, writes nothing and fails naming the secret accept
that fixes it, so the old working key in ombi is never replaced by a dead one.

Declared last so its failing gates nothing else of ombi (ADR 0136), and
restart-on its six inputs so it runs again when a provider moves (ADR 0099).
The manifest named /services/sonarr/config and /var/lib/mesh/sonarr/route.json, host
paths ADR 0112 takes out of definitions. The config dir is now a pathless ${dir:config}
(0700, 1000:1000) mounted into the server and, read-only, into the runtime that reads the
API key from config.xml; the route binding lives in a placed state dir, as jackett and
searxng do. /var/lib/mesh/sonarr stays for the broker secret.

The image is pinned to 4.0.20.3014-ls325, the digest ace runs today. The old pin
(4.0.19.2979-ls322) was older than the running version, and Sonarr migrates its database
forward on start, so pointing the older build at ace's data is not safe.

The container mount points stay /series, /anime and /downloads: Sonarr's database stores
its root folders and the download clients' reported paths under exactly those names, and
there are no remote path mappings to absorb a change. The generic access paths stay;
ace's (/storage/media/*, /storage/downloads) and its media owner 1001:2000 wait on hq 153.

Based on feat/servarr-api-provision (#156), which makes sonarr provide sonarr-api.

Verified: catalogue key tests with MESH_CATALOGUE set (73 manifests parsed, not skipped);
a throwaway container of the pinned image on a fresh 0700 1000:1000 config dir answers
/ping, serves the v3 API with the key it generated and refuses a wrong one (401), accepts
/series as a writable root folder; the runtime's client discovers the key from that
config.xml and reads the queue.
jschoubben added 1 commit 2026-09-30 11:08:06 +00:00
sonarr reached its download clients as nzbget:6789 and qbittorrent:8112 and its indexers
through jackett:9117 or indexers.zurag.be - HAL container names and a public route,
typed into its database by hand. Nothing on the mesh answers those names.

It now requires nzbget-api, qbittorrent-api and jackett-api. A run-once step
(downloads/, declared last, restart-on its bindings, credentials and settings) writes
host, port, TLS, base path, user and credential into sonarr through sonarr's own API:

- A download client is the mesh's when its name is downloads.<provision>.name and its
  kind the provider's; it is registered when missing. A jackett feed is the mesh's when
  its host is the bound one, one the step bound before, or one in
  downloads.jackett-api.adopt-hosts; the jackett indexers in
  downloads.jackett-api.indexers are registered when missing. Every other entry is left
  alone, and nothing is ever deleted.
- Only connection fields, only when they differ. The stored password is masked, so the
  app tests the entry with the credential it holds; only if that fails is the delivered
  one written. Categories, priorities and "enabled" are never touched.
- Each credential is tried against its provider first. A minted value (nothing accepted
  yet) is never written; the step exits 1 naming the exact secret accept.

The step is byte-identical in sonarr, radarr, lidarr and bookshelf (the same Servarr
API, v3 or v1): each module builds from its own directory, so each carries a copy, and
test/downloads.test.ts fails if a sibling's copy differs.

Verified: strict typecheck, the Dockerfile build, 14 unit tests; and the compiled step
against fresh pinned sonarr/radarr/lidarr/bookshelf with throwaway nzbget, qBittorrent
and jackett - minted credentials refused with nothing written, accepted ones registered
and tested by the app, a migration-shaped radarr repointed, reruns unchanged.
Author
Contributor

New commit 28b756f: sonarr reaches nzbget, qbittorrent and jackett through the mesh.

Manifest

  • Now requires nzbget-api (#167), qbittorrent-api (#168) and jackett-api (#150).
  • The bindings and pair credentials land in ${dir:state}.
  • New resources:
    • downloads-config: ${dir:state}/downloads.json, merge json, carrying node: ${machine:name} and the downloads.* settings.
    • downloads-memory: a directory.
    • downloads: a run-once step on the host network, declared last. It restarts when the settings, any of the three bindings or any of the three secrets change.

What the step does, through sonarr's own API:

  • It writes the connection fields only (host, port, TLS, base path, user, credential) into the entries the mesh manages:
    • Download clients: the entry whose name is downloads.<provision>.name and whose kind matches that provider. If there is none, the step registers one.
    • jackett feeds: a Torznab feed of jackett's shape whose host is one of: the bound at, a host the step pointed feeds at before (kept in downloads-memory), or a host listed in downloads.jackett-api.adopt-hosts. A jackett indexer listed in downloads.jackett-api.indexers that the app lacks is registered.
  • Everything else is left alone: other entries, categories, priorities and "enabled". Nothing is ever deleted.
  • The stored password is masked, so the step asks sonarr to test the entry with the password it already holds. The delivered credential is written only if that test fails.
  • Each credential is tried against its provider first. A minted value is never written: the step exits 1 with secret accept <node> sonarr <provision> --provider <node> --from <file>.
  • Shared code: the step is byte-identical in sonarr, radarr, lidarr and bookshelf, which use the same Servarr API (v3 or v1). Each module builds from its own directory, so each carries a copy, and test/downloads.test.ts fails if a sibling's copy differs.

On ace (the draft ace-assignments/sonarr.json adds this): downloads: {nzbget-api: {name: "NZBGet"}, qbittorrent-api: {name: "qBitTorrent"}, jackett-api: {adopt-hosts: ["jackett"]}}. That adopts the two clients and the disabled "Jackett - RARBG" feed the migration plan names. RARBG's jackett indexer no longer exists, so that feed is repointed and reported as untested. Accept the three pair credentials (nzbget ControlPassword, qBittorrent WebUI password, jackett APIKey) before the first apply.

Verified

  • Strict typecheck and the Dockerfile build pass, and 14 unit tests pass.
  • Catalogue tests pass with MESH_CATALOGUE set to all seven downloads branches merged. A scratch resolve on a fake ace filled every ${bound}, ${port}, ${machine}, ${dir} and each consumer's own sealed credential, and the step is the module's last container.
  • End-to-end run of the compiled step against the fresh pinned app, with throwaway nzbget, qBittorrent and jackett:
    • Minted credentials were refused, with the exact accept command, and nothing was written.
    • Accepted credentials registered the clients and a feed, and the app's own test passed.
    • A radarr shaped like ace's (NZBGet at nzbget:6789, qBitTorrent with an old password, disabled feeds at jackett:9117 including one dead one) was repointed.
    • A person's seedbox feed was left untouched, and reruns reported "already as the mesh says".
**New commit 28b756f: sonarr reaches nzbget, qbittorrent and jackett through the mesh.** **Manifest** - Now requires `nzbget-api` (#167), `qbittorrent-api` (#168) and `jackett-api` (#150). - The bindings and pair credentials land in `${dir:state}`. - New resources: - `downloads-config`: `${dir:state}/downloads.json`, merge json, carrying `node: ${machine:name}` and the `downloads.*` settings. - `downloads-memory`: a directory. - `downloads`: a run-once step on the host network, declared last. It restarts when the settings, any of the three bindings or any of the three secrets change. **What the step does**, through sonarr's own API: - It writes the connection fields only (host, port, TLS, base path, user, credential) into the entries the mesh manages: - **Download clients:** the entry whose name is `downloads.<provision>.name` and whose kind matches that provider. If there is none, the step registers one. - **jackett feeds:** a Torznab feed of jackett's shape whose host is one of: the bound `at`, a host the step pointed feeds at before (kept in `downloads-memory`), or a host listed in `downloads.jackett-api.adopt-hosts`. A jackett indexer listed in `downloads.jackett-api.indexers` that the app lacks is registered. - **Everything else is left alone:** other entries, categories, priorities and "enabled". **Nothing is ever deleted.** - **The stored password is masked,** so the step asks sonarr to test the entry with the password it already holds. The delivered credential is written only if that test fails. - **Each credential is tried against its provider first.** A minted value is never written: the step exits 1 with `secret accept <node> sonarr <provision> --provider <node> --from <file>`. - **Shared code:** the step is byte-identical in sonarr, radarr, lidarr and bookshelf, which use the same Servarr API (v3 or v1). Each module builds from its own directory, so each carries a copy, and `test/downloads.test.ts` fails if a sibling's copy differs. **On ace** (the draft `ace-assignments/sonarr.json` adds this): `downloads: {nzbget-api: {name: "NZBGet"}, qbittorrent-api: {name: "qBitTorrent"}, jackett-api: {adopt-hosts: ["jackett"]}}`. That adopts the two clients and the disabled "Jackett - RARBG" feed the migration plan names. RARBG's jackett indexer no longer exists, so that feed is repointed and reported as untested. Accept the three pair credentials (nzbget ControlPassword, qBittorrent WebUI password, jackett APIKey) before the first apply. **Verified** - Strict typecheck and the Dockerfile build pass, and 14 unit tests pass. - Catalogue tests pass with `MESH_CATALOGUE` set to all seven downloads branches merged. A scratch resolve on a fake ace filled every `${bound}`, `${port}`, `${machine}`, `${dir}` and each consumer's own sealed credential, and the step is the module's last container. - End-to-end run of the compiled step against the fresh pinned app, with throwaway nzbget, qBittorrent and jackett: - Minted credentials were refused, with the exact accept command, and nothing was written. - Accepted credentials registered the clients and a feed, and the app's own test passed. - A radarr shaped like ace's (`NZBGet` at `nzbget:6789`, `qBitTorrent` with an old password, disabled feeds at `jackett:9117` including one dead one) was repointed. - A person's seedbox feed was left untouched, and reruns reported "already as the mesh says".
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:20 +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#157