Commit Graph

8 Commits

Author SHA1 Message Date
unkin-agent 538830a864 Bump arrstack app images to -unkin3
ci/woodpecker/pr/vector-test Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/pr/kubeconform Pipeline was successful
Deploy the merged active-active work (process lifecycle, Redis rate
limiter, radarr MediaCover->S3) now on each fork's main; the cluster
still runs -unkin2 so the merged code is undeployed.

- sonarr: v5.0.0-unkin2 -> v5.0.0-unkin3
- radarr: v6.4.2-unkin2 -> v6.4.2-unkin3
- prowlarr: v2.6.2-unkin2 -> v2.6.2-unkin3
2026-08-22 17:53:26 +10:00
unkin-agent 5155cee5e7 arrstack: use waitfordb initContainer instead of inline psql loop (#386)
## Why

The sonarr/radarr/prowlarr deployments gated app startup on their Postgres
database being reachable using a hand-written `psql`-in-a-shell loop running
the `postgres:17-alpine` image. Now that `unkin/waitfordb` is released
(v0.1.0), replace that inline probe with the purpose-built tool so the wait
logic lives in one tested place instead of being copy-pasted per workload.

`waitfordb` reads the same `PG*` env as a libpq fallback and exits `0` once
`SELECT 1` succeeds under those credentials/database, so the retrofit is a
drop-in — the password still never lands in argv.

## Changes

- Swap the `wait-for-db` initContainer image from `dockerhub/library/postgres:17-alpine`
  to `docker-internal/waitfordb:v0.1.0` (pinned, not `latest`) in all three
  arrstack deployments.
- Drop the `sh -c` psql loop `command` — `waitfordb` is the image entrypoint.
- Keep the existing `PGHOST/PGPORT/PGDATABASE/PGUSER/PGPASSWORD` env, the
  `wait-for-db` name, and the resource requests/limits unchanged.
- Add `WAITFORDB_TIMEOUT=5m` so a stuck database fails the pod fast, and
  `WAITFORDB_SSLMODE=disable` to match the in-cluster Postgres.

Validated with `kubectl kustomize apps/overlays/au-syd1/arrstack` (builds clean;
3 waitfordb refs, no remaining postgres init image).

Reviewed-on: #386
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-08-22 16:15:45 +10:00
unkin-agent 8446c31414 arrstack: fix sync-wave deadlock + config multi-attach (#385)
## Why
The active-active arrstack deploy (#383) is stuck in-cluster: the app Deployments (sync-wave 0) can never become Healthy without their Postgres DB, so ArgoCD never advances to create the shared `arrstack-postgres` cluster (wave 1). Separately, `/config` is an immutable RWO cephrbd PVC, so 3 replicas fail with Multi-Attach.

## How
- Order DB infra before the apps: `<app>-db` VaultStaticSecrets + backup ObjectStoreUser/Bucket at sync-wave -3, CNPG `Cluster` -2, `Database` CRDs -1; app Deployments stay at default 0.
- Switch each app's `/config` to an emptyDir per-pod volume (library DB is in Postgres, config is env-driven; MediaCover regenerates). Old RWO config PVCs orphan.

Validated with kustomize build (62 resources). arrproxy/nzbget untouched.

Reviewed-on: #385
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-08-22 12:47:38 +10:00
unkin-agent 187fabb72b arrstack: run sonarr/radarr/prowlarr active-active on shared Postgres (#383)
## Why

The arrstack `sonarr`/`radarr`/`prowlarr` apps currently run as single-replica upstream LinuxServer images on SQLite (RWO config PVC, `Recreate` strategy) — no HA, no horizontal scale, and a hard one-pod ceiling because SQLite can't be shared. Our `-unkin2` forks move all app state into PostgreSQL and use Postgres advisory locks for cross-replica coordination (migrations, scheduled tasks, command execution), which makes an **active-active multi-replica** deployment safe.

This is a fresh Postgres backend — **no SQLite data migration**. The current single-replica pods are throwaway baselines; the new pods start on an empty per-app database and the fork's provider migrates the schema on first start. `arrproxy` (its own `arrproxy-db`) and `nzbget` are untouched.

## What

New `apps/base/arrstack/postgres/`:
- `cnpg_cluster.yaml` — CNPG `Cluster` `arrstack-postgres` (3 instances, `cephrbd-fast-delete`), throwaway `app` initdb owner, and 3 `managed.roles` (`sonarr`/`radarr`/`prowlarr`, login non-superuser) each with `passwordSecret: <app>-db`.
- `database-{sonarr,radarr,prowlarr}.yaml` — CNPG `Database` `<app>-main` owned by `<app>`, `databaseReclaimPolicy: retain`.
- `cnpg_backup.yaml` — cephrgw `ObjectStoreUser` + `Bucket cnpg-arrstack` + nightly `ScheduledBackup` (mirrors jellyfin).
- `vaultstaticsecret.yaml` — 3 VSO `VaultStaticSecret`s syncing `<app>-db` (keys `username`/`password`) from `kv/kubernetes/namespace/arrstack/default/<app>-db` (already seeded); these are both the CNPG role `passwordSecret` and the Deployment's Postgres creds.
- sync-waves: `<app>-db` Secrets (0) → Cluster (1) → Databases (2).
- added `postgres` to `apps/base/arrstack/kustomization.yaml`.

Per app (`sonarr`/`radarr`/`prowlarr`):
- `deployment.yaml`: image → `docker-internal/<app>:v<base>-unkin2` (sonarr `v5.0.0`, radarr `v6.4.2`, prowlarr `v2.6.2`); `replicas: 3`; `RollingUpdate`. Removed the `apikey-init` initContainer and `PUID`/`PGID`/`TZ`. Runs the binary directly (`command: [/app/<Bin>]`, `args: [-nobrowser, -data=/config, -nosingleinstancecheck]`). Env from the new ConfigMap + `secretKeyRef`: `<App>__Postgres__User`/`__Password` (Secret `<app>-db`) and `<App>__Auth__ApiKey` (existing `<app>-apikey`, key `apitoken`). `/<app>/ping` probes kept. `securityContext` runAsUser/runAsGroup/fsGroup 1000 for CephFS group-write.
- new `configmap.yaml`: non-secret `<App>__…` env (Postgres Host/Port/MainDb, `Log__DbEnabled=false`, `Auth__Method=External`, `Auth__Required=DisabledForLocalAddresses`, `App__InstanceName`, `Server__Port`, `Server__UrlBase=/<app>`, `Update__Mechanism=External`).
- `pvc-config.yaml` → RWX `cephfs-raid5-retain` (shared `/config` for `config.xml`/MediaCover).
- unchanged: `service.yaml`, `gateway.yaml`, `httproute.yaml`, apikey `vaultstaticsecret.yaml` — arrproxy still addresses the Service VIP, so N replicas are transparent.

## Validation

`kustomize build apps/overlays/au-syd1/arrstack` clean; `kubeconform` **64/64 valid**; `yamllint` and the repo pre-commit (incl. no-plain-secrets) pass. Runtime validation (pods Ready, single migrate under advisory lock, scheduled-task/command single-owner, UI via `https://arrstack.unkin.net/<app>/`, 3→1→3 rescale) is post-merge once ArgoCD syncs.

## Decisions / unknowns

- **Fork image UID/fsGroup**: the `-unkin2` images have no `USER` (run as root); pinned `runAsUser`/`runAsGroup`/`fsGroup: 1000` with `fsGroupChangePolicy: OnRootMismatch`. Verify group-write on the shared CephFS `/config` on first deploy.
- **Config storage**: chose RWX `cephfs-raid5-retain` shared `/config` (per spec preference) over per-pod `emptyDir`.
- **Postgres storage/version/resources**: mirrored jellyfin/arrproxy CNPG conventions — `cephrbd-fast-delete` 10Gi, PG17, 3 instances.

Reviewed-on: #383
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-08-18 21:29:59 +10:00
unkin-agent 108d1cb213 arrstack: set *arr AuthenticationMethod=External (proxy-trusted) (#382)
## Why

The *arr UIs reached through **arrproxy** were prompting **"Authentication Required"**. Sonarr v4 (and Radarr/Prowlarr on the same Servarr auth code) refuses remote access when `AuthenticationMethod=None`, so the web UI kept demanding a login even though **arrproxy + oauth2-proxy already authenticate every user at the front door**. That is a double prompt with no purpose.

## Change

Extend the existing `apikey-init` container for **sonarr / radarr / prowlarr** to also idempotently enforce, in `/config/config.xml`:

- `<AuthenticationMethod>External</AuthenticationMethod>`
- `<AuthenticationRequired>Enabled</AuthenticationRequired>`

Create-or-replace both elements (same idempotent sed/printf pattern already used for `<ApiKey>`/`<UrlBase>`), without disturbing ApiKey or UrlBase. Pods use `Recreate`, so they roll and re-run the init container on apply.

## Why this is the correct, header-less fix

In the Servarr v4 source, `AddExternal()` registers the **identical `NoAuthenticationHandler` as `AddNone()`** — `External` requires **no** username header (no `X-Forwarded-User` / `Remote-User`). It differs from `None` only in that it is **exempt from the None remote-access block**, so the UI stops prompting while remote access is permitted.

This matters because arrproxy's `trustBoundary` deliberately **strips all inbound identity headers** and forwards only the real `X-Api-Key` to the upstream *arr — it forwards no username header. Because `External` needs none, that stripping is irrelevant and **no arrproxy change is required**. The API path is unaffected (arrproxy injects the real key; *arr API auth is key-based regardless of AuthenticationMethod).

## Validation

- `kustomize build --enable-helm apps/overlays/au-syd1/arrstack` → OK (rendered init carries the auth logic for all 3 apps)
- pre-commit (yamllint + all hooks) → Passed

Versions in scope: sonarr 4.0.19, radarr 6.3.0, prowlarr 2.5.2 (all share the Servarr v4 auth handler).

---------

Co-authored-by: unkin-agent <unkin-agent@git.unkin.net>
Reviewed-on: #382
Co-authored-by: Unkin Agent <unkin-agent@unkin.net>
Co-committed-by: Unkin Agent <unkin-agent@unkin.net>
2026-08-18 20:03:18 +10:00
unkin-agent 76da30d2f7 arrstack: set *arr UrlBase=/<app> for path-based proxy hosting (#378)
## Why
arrproxy forwards `arrstack.unkin.net/<app>/...` to each *arr Service **preserving** the `/<app>` prefix. Without a matching `<UrlBase>` the apps 307-redirect to `/`, breaking path-based hosting. Prerequisite for arrproxy path routing (PR #377... arrproxy service account added in #376).

## Change
Extend the existing idempotent `apikey-init` init container in sonarr/radarr/prowlarr to also enforce `<UrlBase>/<app></UrlBase>` in `/config/config.xml` (sonarr=/sonarr, radarr=/radarr, prowlarr=/prowlarr):

- Set/replace `<UrlBase>` if present, insert after `<Config>` if absent, or write both `<ApiKey>` + `<UrlBase>` when creating a fresh config. `<ApiKey>` handling is unchanged.
- Because Servarr moves all routes (including `/ping`) under the URL base, update liveness/readiness probes from `/ping` to `/<app>/ping` so they don't 404 once UrlBase is set.

nzbget is not fronted by arrproxy and is left untouched.

## Validation
- `kustomize build --enable-helm apps/overlays/au-syd1/arrstack` succeeds
- `pre-commit` (yamllint etc.) passes

---------

Co-authored-by: BenVincent <benvin@main.unkin.net>
Co-authored-by: unkin-agent <unkin-agent@git.unkin.net>
Reviewed-on: #378
Co-authored-by: Unkin Agent <unkin-agent@unkin.net>
Co-committed-by: Unkin Agent <unkin-agent@unkin.net>
2026-08-17 07:59:48 +10:00
unkin-agent d387301d6b arrstack: manage sonarr/radarr/prowlarr API keys via Vault (#369)
## Why

The sonarr/radarr/prowlarr images self-generate an API key into
/config/config.xml on first boot, so the key is unmanaged and differs per
volume reset. This makes Vault the source of truth for those keys (override
bootstrap, chosen by Ben): the key is minted in Vault and enforced into
config.xml before each app starts.

## Changes

- Add a `VaultAuth` `default` in the `arrstack` namespace (kubernetes auth,
  mount `k8s/au/syd1`, role `default`, SA `default`), mirroring jellyfin.
- Add a per-app `VaultStaticSecret` that syncs
  `kv/kubernetes/namespace/arrstack/default/<app>` (key `apitoken`) into the
  `<app>-apikey` Secret. The `default` k8s role's templated policy already
  grants read on that path for the `arrstack/default` SA, so no
  terraform-vault change is needed.
- Add an `apikey-init` initContainer to each of the three deployments that
  reads `API_KEY` from the VSO-created Secret, fails closed on a missing or
  non-hex value, and writes/updates only the `<ApiKey>` element in
  `/config/config.xml` (then fixes ownership 1000:1000, mode 600). Image is a
  pinned busybox via artifactapi to keep this PR atomic (no new image
  dependency).
- Wire the new manifests into the base and per-app kustomizations.

## Notes

- Keys already seeded in Vault at `kv/kubernetes/namespace/arrstack/default/<app>`.
- nzbget is out of scope: it has no config.xml `<ApiKey>` (uses ControlPassword),
  a separate follow-up.
- Downstream consumers (proxy, terraform) currently read
  `kv/service/media-apps/<app>`; the authoritative key now lives at the path
  above. Reconciliation is deferred.

---------

Co-authored-by: unkin-agent <unkin-agent@git.unkin.net>
Reviewed-on: #369
Co-authored-by: Unkin Agent <unkin-agent@unkin.net>
Co-committed-by: Unkin Agent <unkin-agent@unkin.net>
2026-08-15 16:43:02 +10:00
unkin-agent fc08f1fecc arrstack: sonarr/radarr/prowlarr/nzbget on shared CephFS media (#366)
## Why
Stands up the download-automation stack (sonarr, radarr, prowlarr, nzbget) in a new `arrstack` namespace, sharing the same CephFS TV/movies subvolumes that jellyfin serves read-only (jellyfin static-PV rework is on the base branch, PR #237). Downloads and libraries live on one filesystem per media type so the arrs import with atomic hardlink moves.

## Changes
- Add `apps/base/arrstack/` namespace with its own static CephFS PVs (`arrstack-media-tv`, `arrstack-media-movies`) + RWX PVCs (`media-tv`, `media-movies`) bound to the same rootPaths jellyfin uses; each namespace gets a unique PV name/volumeHandle pinned by claimRef.
- Deploy sonarr (mounts media-tv RW), radarr (media-movies RW), prowlarr (config only), nzbget (both media PVCs RW); single replica, Recreate strategy, LinuxServer images via artifactapi dockerhub remote, PUID/PGID/TZ, probes, fsGroup for CephFS writability.
- Per-app config PVCs on `cephrbd-fast-retain` (RWO, 5Gi) for the SQLite state; internal-only Gateway + HTTPRoute per app (traefik-internal, vault-issuer, external-dns `<app>.k8s.syd1.au.unkin.net`), no oauth yet.
- nzbget: initContainer seeds `/config/nzbget.conf` (copy image template if absent, append managed path/category block once) and creates the media dirs; category `tv` -> `/media/tv/downloads`, `movies` -> `/media/movies/downloads`.
- Wire ArgoCD: add `apps/overlays/*/arrstack` to the media ApplicationSet. (media AppProject already gains the arrstack destination + PersistentVolume whitelist on the base branch.)

## Depends on
terraform-artifactapi PR #31 (allowlist `^linuxserver/` on the dockerhub remote) must be merged + applied before these pods can pull.

## Follow-up (terraform)
Root folders (/media/tv/series, /media/movies/films), download-client wiring (host `nzbget`, port 6789, categories tv/movies) and API keys are configured later via terraform-arr.

---------

Co-authored-by: Ben Vincent <neotheo@gmail.com>
Co-authored-by: Ben Vin <neotheo@gmail.com>
Reviewed-on: #366
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-08-15 16:22:06 +10:00