Skip to content

Declarative *arr wiring

The *arr apps split their config into two layers. config.xml (API key, auth, port) is seeded declaratively; everything relational (download clients, root folders, indexers, app links) lives in SQLite and is applied through the REST API by an idempotent Job.

Vault ── arr-apikeys (generated) ─▶ media-arr-apikeys Secret ─▶ initContainers seed config.xml
└─ wiring (you write) ───────▶ media-wiring-secrets Secret ─▶ wiring Job ─▶ *arr REST APIs
PieceWhat
vault-credential-setup JobGenerates 32-char API keys for prowlarr/sonarr/radarr/bazarr into Vault (prevent_override → stable)
media-arr-apikeys (VaultStaticSecret)Projects those keys into the namespace
seed-config initContainersWrite /config/config.xml with the pinned key on first boot (idempotent — skip if present)
media-wiring JobGET-before-POST against the APIs: NZBGet clients, root folders, Prowlarr→Sonarr/Radarr, indexers from indexers.json
media-wiring-secrets (VaultStaticSecret)NZBGet password + indexer API keys — you write these to Vault

Everything is reconciled by Flux; the Jobs carry kustomize.toolkit.fluxcd.io/force: enabled so they re-run and re-assert state each reconcile.

1. Create the Vault policies + k8s-auth roles (must exist before the setup Job can authenticate). They’re Terraform-managed alongside the other apps:

Terminal window
cd tf/application-parameters/hl-cluster-vault
terraform init -upgrade
terraform apply # adds the media-setup / media-secrets roles + policies

2. Let Flux reconcile — the vault-credential-setup Job logs into Vault as media-setup and writes the four API keys to secrets/cluster/internal/ns/media/arr-apikeys. vault-secrets-operator then projects them into the media-arr-apikeys Secret, and the apps’ initContainers seed config.xml. Force a run if you don’t want to wait:

Terminal window
flux -n tenant-0-k8s-services-homelab-muehlena-de reconcile kustomization applications-media
kubectl -n media get secret media-arr-apikeys # should have prowlarr/sonarr/radarr/bazarr keys

3. Write the wiring secrets you own — the NZBGet control password (the same value as the VM’s media_nas_nzbget_control_password) and any Usenet-indexer API keys. Use the OpenBao CLI (bao, or vault):

Terminal window
bao kv put secrets/cluster/internal/ns/media/wiring \
nzbget-password='<same as media_nas_nzbget_control_password>' \
INDEXER_NZBGEEK_APIKEY='<your indexer key>' # optional, per indexer

Key names for indexer secrets must be valid env-var names (INDEXER_*) — they’re exposed to the wiring Job for envsubst into indexers.json. nzbget-password is read from a file mount, so its hyphen is fine.

k8s/manifests/base/appliations/media-wiring/config/indexers.json is a JSON array of full Prowlarr /api/v1/indexer bodies (empty by default). Reference secret keys as ${INDEXER_*}:

[
{
"name": "NZBgeek",
"implementation": "Newznab",
"configContract": "NewznabSettings",
"enable": true,
"protocol": "usenet",
"fields": [
{ "name": "baseUrl", "value": "https://api.nzbgeek.info" },
{ "name": "apiKey", "value": "${INDEXER_NZBGEEK_APIKEY}" },
{ "name": "categories", "value": [5000, 5040] }
]
}
]

Once an indexer exists in Prowlarr, the Prowlarr→Sonarr/Radarr app links (set by the Job) auto-push it to the *arr apps — no per-app indexer config.

Covered idempotently: Sonarr/Radarr NZBGet download client + root folders (/data/media/tv, /data/media/movies), and Prowlarr applications for Sonarr + Radarr. Tunables are env on the Job (SONARR_URL, NZBGET_HOST, SONARR_ROOT, …).

Not covered: Bazarr (its config isn’t RESTful — add Sonarr/Radarr in its UI with the keys from media-arr-apikeys), and quality profiles / custom formats (use Configarr / Recyclarr). Re-running the Job is always safe.