Commit Graph

421 Commits

Author SHA1 Message Date
unkin-agent 8e508c231e argocd: drop internal-CA rootCA pin from Authentik oidc.config
ci/woodpecker/pr/vector-test Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/pr/kubeconform Pipeline was successful
identity.unkin.net moved from the internal unkin.net CA to the LetsEncrypt
*.unkin.net wildcard, so pinning the internal root made argocd-server reject
the OIDC discovery handshake (x509: certificate signed by unknown authority).
The stock image trust store already carries the public roots.
2026-08-29 21:26:39 +10:00
unkin-agent 74ad2c8773 woodpecker: add mediamark-ci service account (#438)
The mediamark Woodpecker docker step needs a dedicated ServiceAccount so it can push to the trusted in-cluster registry, mirroring the existing arrproxy-ci setup.

- Add bare ServiceAccount `mediamark-ci` in namespace `woodpecker` and register it in the woodpecker base kustomization.

Reviewed-on: #438
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-08-29 21:21:21 +10:00
unkin-agent 6b5b129ad6 clickhouse-system: use canonical upstream image names (#436)
rke2's `registries.yaml` already rewrites upstream image names to the artifactapi mirror, so manifests must carry canonical upstream names. Only in-house `artifactapi.k8s.syd1.au.unkin.net/docker-internal/...` images stay explicit.

Changes:
- Point the Altinity chart crdHook at `docker.io/bitnami/kubectl`.
- Point the operator at `docker.io/altinity/clickhouse-operator`.
- Point the metrics exporter at `docker.io/altinity/metrics-exporter`.
- Reword the header comment that claimed all images are pulled through the artifactapi dockerhub remote.

Only the `repository` keys change; the chart still supplies the tags (0.27.2 / latest), so rendered tags are identical. `kustomize build --enable-helm apps/overlays/au-syd1/clickhouse-system` differs from main only in those three image strings. No other proxied refs in the file.

Reviewed-on: #436
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-08-29 21:20:10 +10:00
unkin-agent 38a41bd44b watchstate: use canonical upstream image name for alpine (#435)
rke2's `registries.yaml` already rewrites upstream image names to the artifactapi mirror, so manifests must carry canonical upstream names. Only in-house `artifactapi.k8s.syd1.au.unkin.net/docker-internal/...` images stay explicit.

Changes:
- Point the watchstate oauth2-proxy cert-combine init container at `docker.io/library/alpine:3`.

Tag unchanged. `kustomize build --enable-helm apps/overlays/au-syd1/watchstate` differs from main only in that image string. No extra proxied refs in the file (the oauth2-proxy image itself is already canonical `quay.io/...`).

Reviewed-on: #435
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-08-29 21:19:36 +10:00
unkin-agent da1d812eec netbox: use canonical upstream image name for redis_exporter (#434)
rke2's `registries.yaml` already rewrites upstream image names to the artifactapi mirror, so manifests must carry canonical upstream names. Only in-house `artifactapi.k8s.syd1.au.unkin.net/docker-internal/...` images stay explicit.

Changes:
- Point the netbox valkey metrics sidecar at `docker.io/oliver006/redis_exporter:v1.89.0`.

Tag unchanged. `kustomize build --enable-helm apps/overlays/au-syd1/netbox` differs from main only in that image string. No extra proxied refs in the file (the `valkey/valkey:8-alpine` container is already a bare upstream name).

Reviewed-on: #434
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-08-29 21:19:27 +10:00
unkin-agent df89947f47 litellm: use canonical upstream image name for redis_exporter (#432)
rke2's `registries.yaml` already rewrites upstream image names to the artifactapi mirror, so manifests must carry canonical upstream names. Only in-house `artifactapi.k8s.syd1.au.unkin.net/docker-internal/...` images stay explicit.

Changes:
- Point the litellm redis metrics sidecar at `docker.io/oliver006/redis_exporter:v1.89.0`.

Tag unchanged. `kustomize build --enable-helm apps/overlays/au-syd1/litellm` differs from main only in that image string. No extra proxied refs in the file (the `redis:7-alpine` container is already a bare upstream name).

Reviewed-on: #432
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-08-29 21:19:01 +10:00
unkin-agent 7f928dddfc gitea: use canonical upstream image name for redis_exporter (#431)
rke2's `registries.yaml` already rewrites upstream image names to the artifactapi mirror, so manifests must carry canonical upstream names. Only in-house `artifactapi.k8s.syd1.au.unkin.net/docker-internal/...` images stay explicit.

Changes:
- Point the gitea valkey metrics sidecar at `docker.io/oliver006/redis_exporter:v1.89.0`.

Tag unchanged. `kustomize build --enable-helm apps/overlays/au-syd1/gitea` differs from main only in that image string. No extra proxied refs in the file (the `valkey/valkey:8-alpine` container is already a bare upstream name).

Reviewed-on: #431
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-08-29 21:18:44 +10:00
unkin-agent b459e9a90a authentik: use canonical upstream image name for redis_exporter (#430)
rke2's `registries.yaml` already rewrites upstream image names to the artifactapi mirror, so manifests must carry canonical upstream names. Only in-house `artifactapi.k8s.syd1.au.unkin.net/docker-internal/...` images stay explicit.

Changes:
- Point the authentik redis metrics sidecar at `docker.io/oliver006/redis_exporter:v1.89.0`.

Tag unchanged. `kustomize build --enable-helm apps/overlays/au-syd1/authentik` differs from main only in that image string. No extra proxied refs in the file (the `redis:7-alpine` container is already a bare upstream name).

Reviewed-on: #430
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-08-29 21:18:33 +10:00
unkin-agent c8355967d4 artifactapi: use canonical upstream image name for redis_exporter (#429)
rke2's `registries.yaml` already rewrites upstream image names to the artifactapi mirror, so manifests must carry canonical upstream names. Only in-house `artifactapi.k8s.syd1.au.unkin.net/docker-internal/...` images stay explicit.

Changes:
- Point the artifactapi redis metrics sidecar at `docker.io/oliver006/redis_exporter:v1.89.0`.

Tag unchanged. `kustomize build --enable-helm apps/overlays/au-syd1/artifactapi` differs from main only in that image string. No extra proxied refs in the file (the `redis:7-alpine` container is already a bare upstream name).

Reviewed-on: #429
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-08-29 21:18:27 +10:00
unkin-agent e10f0c09c9 arrstack,fafflix,cheeztv: provision shared mediastore CephFS volume (#428)
## Why

The media estate is currently split across two 1Ti CephFS subvolumes (`media-tv`
and `media-movies`), so a download that lands in one and imports to the other is
a cross-filesystem copy, not a hardlink move. A single 10Ti `mediastore`
subvolume (already created on `cephfs_csi_ssd_ec_4_1`) collapses that: one
filesystem holding `/{fafflix,cheeztv}/{tvseries,movies}` plus
`/nzbget/downloads/complete`, so every arr import is an atomic same-fs hardlink.

This PR only provisions the volume and seeds its tree. No existing PV, PVC or
workload is touched, and nothing mounts the new claims yet — the cutover of the
arrs and the two jellyfins is a separate change.

## How

- Add static PVs `arrstack-mediastore`, `fafflix-mediastore` and
  `cheeztv-mediastore`, all pointing at the same rootPath
  (`/volumes/csi_ssd_ec_4_1/mediastore/a0152dac-…`) with unique names and
  volumeHandles pinned by `claimRef` — the established pattern for the shared
  media subvolumes.
- Add the matching RWX 10Ti PVCs (`mediastore` in arrstack,
  `fafflix-mediastore`, `cheeztv-mediastore`), annotated
  `k8up.io/backup: "false"` and statically bound via `volumeName` +
  `storageClassName: ""`.
- Add `mediastore-bootstrap`, a one-shot ArgoCD Sync-hook Job
  (`hook-delete-policy: BeforeHookCreation`, no sync-wave needed) that mounts
  the arrstack claim and `mkdir -p`s the directory tree as uid/gid 1000 —
  the uid the arrstack media pods run as. Idempotent, so it self-heals on
  every sync.
- Wire the new manifests into the arrstack, fafflix and cheeztv bases.

## Validation

- `kustomize build` clean on `apps/overlays/au-syd1/{arrstack,fafflix,cheeztv}`
- `kubeconform` clean on all three overlays (91 / 29 / 32 resources valid)
- pre-commit (yamllint, check-yaml, no-plain-secrets) passed

Reviewed-on: #428
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-08-29 21:18:09 +10:00
unkin-agent 9616e0e2b3 arrstack: use canonical upstream image names (#427)
rke2's `registries.yaml` already rewrites upstream image names to the artifactapi mirror, so manifests must carry canonical upstream names. Only in-house `artifactapi.k8s.syd1.au.unkin.net/docker-internal/...` images stay explicit.

Changes:
- Point the arrproxy migrate job at `docker.io/library/postgres:18-alpine`.
- Point the arrproxy oauth2-proxy cert-combine init container at `docker.io/library/alpine:3`.
- Point the arrstack ValkeyCluster at `docker.io/valkey/valkey:9.0.0`.

Tags are unchanged. `kustomize build --enable-helm apps/overlays/au-syd1/arrstack` differs from main only in those three image strings. No extra proxied refs found in these files (the oauth2-proxy image itself is already canonical `quay.io/...`).

Reviewed-on: #427
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-08-29 21:16:59 +10:00
unkin-agent c313c7cc3f fafflix,cheeztv: declarative SSO + LDAP plugin auth config (#424)
## Summary

- Adds SSO-Auth.xml, LDAP-Auth.xml, and branding.xml plugin config (rendered by an initContainer into /config) to fafflix and cheeztv
- Adds a per-namespace oauth-credentials VaultStaticSecret to each instance
- Adds a "Sign in with SSO" login link via jellyfin branding config

## Why

Phase-1 jellyfin auth: browser SSO via the 9p4 SSO plugin (Authentik OIDC, ak_groups claim) and phone/TV app-passwords via the LDAP plugin against the Authentik LDAP outpost. Config is declarative — restarts overwrite UI edits. Activates when the plugin-baked jellyfin-ha image lands (separate PR).

Reviewed-on: #424
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-08-29 20:47:54 +10:00
unkin-agent 0322fc6989 jellyfin: bump fafflix+cheeztv to v0.2.0 (#426)
## Summary

- Bumps the jellyfin-ha image to v0.2.0 on fafflix and cheeztv

## Why

v0.2.0 bakes the LDAP and SSO auth plugins into the image; with the plugin config PR this enables browser SSO and app-password logins.

Reviewed-on: #426
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-08-29 13:22:40 +10:00
unkin-agent f8716dd16a kea-operator: bump to v0.1.5 (#425)
## Summary

- Bumps kea-operator image pin to v0.1.5

## Why

v0.1.4 fixed HA peer URLs but the HA hook then bound its dedicated listener to the ClusterIP and crashed; v0.1.5 disables the dedicated listener so HA flows via the ctrl-agent.

Reviewed-on: #425
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-08-29 12:55:13 +10:00
unkin-agent 74eb09b638 kea-operator: bump to v0.1.4 (#423)
why: production kea-dhcp4 is in a permanent crashloop — Kea's HA hook rejects DNS hostnames in peer URLs and v0.1.3 renders headless-DNS peer URLs; v0.1.4 ships the merged fix (per-peer ClusterIP Services + IP-literal URLs).

- bump kea-operator image pin to v0.1.4

Reviewed-on: #423
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-08-26 23:19:41 +10:00
unkin-agent 8eb57f9c19 authentik: deploy LDAP outpost for in-cluster app-password binds (#422)
## Why

Jellyfin authenticates users against Authentik over LDAP (app-password binds). The `authentik-ldap` Service, Gateway, and TLSRoute already exist in the authentik base, but nothing backed the Service — no LDAP outpost was ever deployed. This adds the missing Deployment so in-cluster clients (Jellyfin) can bind.

Deployed as a separate Deployment inside the existing `authentik` namespace rather than a new app/namespace: the authentik overlay is already wired into the `platform` ApplicationSet and the LDAP Service/Gateway/TLSRoute already live here, so this needs no new AppProject/ApplicationSet wiring and keeps the outpost next to the core it serves.

## How

- `ldap-outpost-deployment.yaml`: 2-replica (stateless) Deployment, image `ghcr.io/goauthentik/ldap:2026.5.3` (canonical upstream name; matches the deployed authentik chart version; containerd mirrors route via artifactapi). Pod labels match the existing `authentik-ldap` Service selector.
  - `AUTHENTIK_HOST=https://identity.k8s.syd1.au.unkin.net`, `AUTHENTIK_INSECURE=false`.
  - Trusts the internal CA via the established combine-certs initContainer pattern (concatenates the base roots with the reflected `vault-ca-cert`) + `SSL_CERT_FILE`.
  - `AUTHENTIK_TOKEN` sourced from secret `authentik-ldap-outpost-token`.
  - Resources set; TCP probes on the LDAP port; reloader annotation for token + CA rotation.
- `ldap-outpost-vaultstaticsecret.yaml`: VSS (reuses the namespace `default` VaultAuth) materialising the token from KV `kubernetes/namespace/authentik/default/outpost-token` (key `token`).
- `ldap-service.yaml`: adds the plaintext `ldap` port 3389 alongside the existing `ldaps` 6636 (ClusterIP only, no public exposure — external reach is via the pre-existing internal Gateway/TLSRoute).
- `ldap-outpost-vmpodscrape.yaml`: VMPodScrape for the outpost metrics endpoint (:9300).
- No sync-wave annotations.

## Dependency — token seed (blocking)

The outpost pods CrashLoop until the token exists. After merge, the Authentik LDAP outpost token must be seeded into Vault KV at `kv/kubernetes/namespace/authentik/default/outpost-token` with key `token`. This path is provided by the corresponding terraform-authentik PR; the value must match the outpost token authentik issues.

## Validation

`kustomize build --enable-helm apps/overlays/au-syd1/authentik` + repo kubeconform: 38 resources, Valid: 38, Invalid: 0, Errors: 0.

Reviewed-on: #422
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-08-26 23:19:24 +10:00
unkin-agent b9f727bc97 watchstate: expose externally at watchstate.unkin.net (#420)
## Why

WatchState currently only has an internal front door (watchstate.k8s.syd1.au.unkin.net, PR #419, merged). This adds a public front door at **watchstate.unkin.net** via the external (DMZ) Traefik so the admin UI is reachable off-cluster, still fully gated by oauth2-proxy/Authentik. The internal gateway and routes are untouched; oauth2-proxy now fronts BOTH hostnames.

Stacked base note: the intended base (benvin/watchstate-deploy, PR #419) merged into main before this PR opened, so this targets `main`.

## How

- **DNS**: `watchstate-dns-internal` DNSRecord in `apps/base/bind-internal/authoritative/records.yaml` (unkin.net zone, bind-internal) -> `A 198.18.199.0`, the traefik-external VIP. Mirrors the `arrstack-dns-internal` precedent (external front door via bind, not external-dns). Kept in the central bind-internal location, so no AppProject destination widening is needed.
- **Gateway**: new `watchstate-external` Gateway (`gatewayClassName: traefik-external`), http + https listeners on hostname `watchstate.unkin.net`. TLS terminated with the Let's Encrypt `*.unkin.net` wildcard secret `wildcard-unkin-net-tls` — so **no cert-manager and no external-dns annotations**.
- **Routes**: `watchstate-external-http-redirect` (http->https 301) and `watchstate-external` (https -> `watchstate-oauth2:80`), mirroring the arrproxy/cheeztv external patterns.
- **oauth2-proxy dual-host**: `OAUTH2_PROXY_REDIRECT_URL` changed from the pinned cluster callback to the **relative** `/oauth2/callback`. With `OAUTH2_PROXY_REVERSE_PROXY=true` (already set), oauth2-proxy derives scheme+host per request from `X-Forwarded-Proto/Host` (verified in oauth2-proxy v7.15.3 `getOAuthRedirectURI`: host-less redirectURL falls through to request-derived), so the callback works on both `watchstate.unkin.net` and `watchstate.k8s.syd1.au.unkin.net`. `COOKIE_DOMAINS` and `WHITELIST_DOMAINS` now list both hosts (one cookie per host; a single parent-domain cookie can't cleanly span unkin.net vs k8s.syd1.au.unkin.net).
- Dropped the no-op `argocd.argoproj.io/sync-wave: "0"` annotation on the `default` VaultAuth. New resources intentionally carry no sync-wave annotations.

## Validation

- `kustomize build --enable-helm` on `apps/overlays/au-syd1/watchstate` and `.../bind-internal`: OK.
- kubeconform (repo CI config, k8s 1.33.7): watchstate overlay 15/15 valid (2 Gateways, 4 HTTPRoutes); bind-internal overlay 67/67 valid (incl. `watchstate-dns-internal`).

## Dependencies

- **Reflector allow-list (argocd-apps PR #418)**: reflects `wildcard-unkin-net-tls` into the `watchstate` namespace. This PR references that secret as if present.
- **Authentik (terraform-authentik, separate PR)**: register both `https://watchstate.unkin.net/oauth2/callback` and `https://watchstate.k8s.syd1.au.unkin.net/oauth2/callback` as redirect URIs on the watchstate provider.

Reviewed-on: #420
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-08-26 21:47:03 +10:00
unkin-agent 062d217ea8 bind-internal: add cheeztv.unkin.net record (#421)
why: cheeztv.unkin.net never resolved — external-dns ignores unkin.net (domain-filter is k8s.syd1.au.unkin.net only); *.unkin.net names are DNSRecord CRs.

- add cheeztv-dns-internal A record → 198.18.200.4 (internal traefik VIP serving the cheeztv gateway)

Reviewed-on: #421
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-08-26 20:09:08 +10:00
unkin-agent 5da12e398d Extend LE *.unkin.net wildcard to arrstack + authentik (reflect into gitea) (#418)
## Why

#417 stood up the central Let's Encrypt `*.unkin.net` wildcard (Certificate `wildcard-unkin-net` in `cert-manager`, secret `wildcard-unkin-net-tls`, emberstack-reflected). This wires the remaining single-label public hosts onto that publicly-trusted leaf instead of the internal Vault-PKI CA.

A `*.unkin.net` wildcard matches **single-label** subdomains only, so cluster hostnames under `*.k8s.syd1.au.unkin.net` are deliberately left on `vault-issuer`.

> Note: PR #417 already merged to `main`, so this is no longer stacked — it targets `main` directly.

## How

- **cert-manager**: reflect `wildcard-unkin-net-tls` into `arrstack`, `authentik`, `gitea` (appended to both reflector namespace lists → `cheeztv,arrstack,authentik,gitea`).
- **arrstack**: repoint the public `arrproxy` gateway listener (`arrstack.unkin.net`) `certificateRefs` → `wildcard-unkin-net-tls`; drop its `cert-manager.io/*` vault-issuer annotations.
- **authentik**: repoint the `identity.unkin.net` https listener → `wildcard-unkin-net-tls`. The `identity.k8s.syd1.au.unkin.net` listeners keep `authentik-tls` (vault-issuer), so the gateway `common-name` is retargeted to that cluster host. `authentik-ldap` gateway untouched.
- **gitea**: secret **reflected only**; `git.unkin.net` cutover deferred (no manifest change), same deferred pattern as fafflix.

## Validation

- `kustomize build` OK for cert-manager / arrstack / authentik / gitea overlays.
- `kubeconform` all valid (0 invalid, 0 errors) across the four overlays.
- Verified no `*.k8s.syd1.au.unkin.net` listener lost its vault-issuer wiring.

Reviewed-on: #418
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-08-25 21:48:54 +10:00
unkin-agent 48632eb9f9 watchstate: deploy admin-gated jellyfin watch-state sync tool (#419)
## Why

Deploy WatchState (arabcoders/watchstate), the Jellyfin/Plex/Emby watch-state
sync tool, as an internal admin tool. It gets an admin-only web UI/API gated the
same way as logviewer: an oauth2-proxy front backed by Authentik OIDC.

## What

- New `watchstate` namespace + media-project app; base at `apps/base/watchstate`,
  overlay at `apps/overlays/au-syd1/watchstate`.
- Image `ghcr.io/arabcoders/watchstate:v1.10.3` (current release; canonical
  upstream name, containerd mirrors route ghcr via artifactapi).
- `replicas: 1`, `strategy: Recreate`, single `5Gi` `cephrbd-fast-delete` RWO PVC
  at `/config` — sqlite + the in-container cron/redis are single-writer.
- `runAsUser/runAsGroup/fsGroup: 1000` (image's rootless user); liveness/readiness
  `GET /v1/api/system/healthcheck` on 8080 (route confirmed in upstream
  `src/API/System/HealthCheck.php`, no auth guard).
- oauth2-proxy (mirrors logviewer 1:1) fronts every path. Authentik issuer
  `identity.k8s.syd1.au.unkin.net`, redirect
  `https://watchstate.k8s.syd1.au.unkin.net/oauth2/callback`. Authorization is
  enforced Authentik-side (akR-global-admin only), so no oauth2-proxy group
  allowlist is configured.
- Internal-only Gateway (`traefik-internal`) for
  `watchstate.k8s.syd1.au.unkin.net`, `vault-issuer` TLS leaf, external-dns to
  `198.18.200.4`. HTTP -> HTTPS redirect.
- `VaultStaticSecret` pulls OIDC creds from
  `kv/kubernetes/namespace/watchstate/default/oauth-credentials`; `vault-ca-cert`
  auto-reflects into the namespace.
- Registered in the media `ApplicationSet` + `AppProject`.

## Scrape decision

No `VMPodScrape`: WatchState exposes no Prometheus/`/metrics` endpoint.

## Follow-ups

- **Seed check:** the VaultStaticSecret expects `client_id`, `client_secret`, and
  `cookie_secret` keys at the kv path. Only `client_secret` was confirmed seeded;
  `client_id` and a generated `cookie_secret` must also be present or the
  oauth2-proxy pod will not start.
- **Webhook ingestion:** the Jellyfin webhook endpoint (`/v1/api/webhook`)
  currently sits behind oauth2-proxy like everything else. When sync is wired up,
  the jellyfins pushing webhooks will need an auth-bypass or an apikey route for
  that path.

Reviewed-on: #419
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-08-25 21:39:07 +10:00
unkin-agent d42d013541 cheeztv: terminate public cheeztv.unkin.net TLS with the LE *.unkin.net wildcard (#417)
why: cheeztv.unkin.net needs a publicly-trusted cert; DNS-01 via the delegated acme.unkin.net zone is now working (staging wildcard verified end-to-end in ~70s). A single centrally-managed wildcard avoids per-host CNAMEs and LE duplicate-order rate limits.

- add Certificate wildcard-unkin-net (cert-manager ns, ClusterIssuer letsencrypt prod, secret wildcard-unkin-net-tls) with emberstack reflector secretTemplate annotations
- reflect the secret into the cheeztv namespace (append future consumers to the two reflector namespace lists)
- point the public cheeztv gateway listener at wildcard-unkin-net-tls and drop its vault-issuer annotations (cheeztv-internal stays on vault-issuer)

Reviewed-on: #417
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-08-25 20:58:02 +10:00
unkin-agent 1ecba543aa arrstack: wire arrproxy v0.4.0 tier routing (fafflix/cheeztv) (#416)
## Why

The old bare `sonarr`/`radarr` Services are gone, but arrproxy still ran the v0.3.x built-in default topology pointing at non-existent `sonarr`/`radarr` upstreams and mounted dead `sonarr-apikey`/`radarr-apikey` Secrets. This cuts the front door over to arrproxy v0.4.0 tier routing so the adult (fafflix, `/3aa168`) and kids (cheeztv, `/3df803`) *arr instances are reachable again, with per-tier group authorization.

## Changes

- Bump `arrproxy-api` and `arrproxy-ui` images to `v0.4.0`.
- Add `arrproxy-tiers` ConfigMap (`ARRPROXY_TIERS_FILE`): `fafflix` + `cheeztv` tiers, each with sonarr/radarr upstreams, route hash, key subdir and group grants. Adults (`akP-media-fafflix`) reach both tiers all methods; kids (`akP-media-cheeztv`) reach only cheeztv, GET/HEAD. `legacyRoutes:false` retires the unprefixed routes.
- Because arrproxy strips `/<hash>` and proxies `/<app>/...`, each upstream URL carries the `/<hash>` path segment so the joined upstream path (`/3aa168/sonarr/...`) matches the *arr's own UrlBase.
- Mount the tiers file and rework the projected keys volume to the real per-instance `{sonarr,radarr}-{adult,kids}-apikey` Secrets under `<keyDir>/<app>` (`adult/*`, `kids/*`); refresh reloader annotations.
- oauth2-proxy: route `/3aa168` and `/3df803` to arrproxy-api (drop dead `/sonarr` `/radarr` `/prowlarr`; prowlarr is served directly), and widen `SKIP_AUTH_REGEX` to `^/[^/]+/[^/]+/api` for the hash-prefixed tier api paths.

Group headers keep flowing via the existing Authentik `ak_groups` claim -> oauth2-proxy `X-Forwarded-Groups` mechanism. Validated with `kustomize build` + repo kubeconform (88/88 valid). No new Vault seeds required — the per-instance apikey Secrets already exist.

Reviewed-on: #416
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-08-25 20:49:04 +10:00
unkin-agent fe689dc08e Replace legacy jellyfin app with fafflix (adult, cheeztv pattern) (#415)
## Why

Rebuild the adult media instance as `fafflix`, mirroring the kids instance (`cheeztv`) HA pattern (Postgres-backed jellyfin-ha fork, Valkey transcode-lease store, shared-RWX transcode, CNPG + k8up/restic backups, static CephFS media PVs). Ben: "replace the adult jellyfin with the same pattern as kids." The adult instance holds no data currently, so the wipe/replace is sanctioned.

Stacked on top of `benvin/cheeztv` (#413) — base is that branch, not main.

## How

- Add `apps/base/fafflix/` — cheeztv pattern with `s/cheeztv/fafflix` on names/namespace/labels/secrets/buckets and the Vault kv path `kubernetes/namespace/fafflix/default/k8up-restic`.
- Media: mount the shared movies/tv CephFS subvolumes' `adult` subtree at `/media/movies` and `/media/tv`, plus the `kids` subtree at `/media/movies-kids` and `/media/tv-kids` (same two static PVs, new unique volumeHandles `fafflix-media-{movies,tv}-static`) so fafflix can resume kids content started on cheeztv. All media mounts readOnly, matching cheeztv.
- **Hostname preserved:** fafflix keeps serving the legacy jellyfin host `jellyfin.k8s.syd1.au.unkin.net` (TLS secret `fafflix-tls`, cert-manager re-mints). The dedicated fafflix domain switch is explicitly deferred — no `fafflix.unkin.net` created.
- config PVC on `cephfs-raid5-delete` (delete variant).
- Remove `apps/base/jellyfin/` + its overlay; swap `jellyfin`->`fafflix` in the `media-apps` ApplicationSet directory glob and the `media` AppProject namespace destination.
- Third-party images stay canonical upstream; the in-house `jellyfin-ha` image ref is unchanged.

## Verification

- `kustomize build` + `kubeconform -strict` clean on the fafflix overlay and the whole media appset scope (fafflix/cheeztv/arrstack): 0 invalid, 0 errors.
- No dangling `apps/base/jellyfin` references; `jellyfin.k8s.syd1.au.unkin.net` now served by exactly one app (fafflix); no `fafflix.unkin.net` anywhere.

Reviewed-on: #415
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-08-25 20:06:53 +10:00
unkin-agent 9db52c5e26 arrstack: rebuild adult tier on the shared-Postgres pattern (#414)
## Why
Rebuild the ADULT arr tier on the same shared-Postgres, active-active pattern the kids tier now uses, replacing the legacy adult sonarr/radarr/nzbget. The old adult apps hold **no data**, so the wipe is sanctioned.

**Stacked on** `benvin/arrstack-kids` (#412) — review/merge that first.

## How
- **Add** `sonarr-adult` / `radarr-adult` / `nzbget-adult` (copies of the `*-kids` dirs, `s/kids/adult`):
  - own DBs `sonarr-adult-main` / `radarr-adult-main`, Vault paths `.../default/{sonarr,radarr}-adult(+-db)`
  - S3 prefixes `sonarr-adult` / `radarr-adult`; UrlBase + probe + exportarr URLs `/3aa168/{sonarr,radarr}` (adult hash)
  - media subPaths `tvshows/adult` + `movies/adult`; nzbget config PVC on `cephrbd-fast-delete`
- **postgres**: add managed roles `sonarr-adult`/`radarr-adult`, Database CRDs `{sonarr,radarr}-adult-main`, VaultStaticSecrets `{sonarr,radarr}-adult-db`
- **Remove** legacy `sonarr`/`radarr`/`nzbget` dirs (incl. their gateway/httproute), managed roles `sonarr`/`radarr`, databases `{sonarr,radarr}-main`, VSS `{sonarr,radarr}-db`, and all kustomization entries
- Third-party images stay canonical upstream; in-house images stay explicit `docker-internal`

`prowlarr`, `arrproxy`, and everything else untouched. arrproxy path re-wiring is a later PR.

Verified: `kustomize build` + `kubeconform` (87/87 valid), no dangling refs to removed apps, no `3df803` in adult files.
Reviewed-on: #414
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-08-25 20:06:39 +10:00
unkin-agent 94da740a9d Add cheeztv kids Jellyfin instance + fafflix kids mounts (#413)
## Why
Kids need their own safe Jellyfin front (cheeztv.unkin.net) with a library that only ever exposes kids titles, while those same titles stay resumable in the existing adult instance (fafflix). The fafflix.unkin.net domain switch is deliberately out of scope.

## How
- **New `cheeztv` instance** (`apps/base/cheeztv`): mirrors the fafflix HA Jellyfin stack 1:1 - same `jellyfin-ha:v0.1.3` fork image, 2-replica StatefulSet, RWX transcode PVC + HA takeover, Intel iGPU transcode, CNPG Postgres + PgBouncer, Valkey transcode store, k8up config backup, VMPodScrape.
- **Kids-only media**: its own static PVs on the *same* shared movies/tv CephFS subvolumes, mounted with `subPath: kids` so it sees only `movies/kids` and `tvshows/kids`.
- **Fully separate state**: own namespace, config PVC, `cheeztv-postgres` cluster, `cheeztv-valkey`, and `cnpg-cheeztv` / `cheeztv-config-backup` RGW buckets - nothing shared with fafflix.
- **DNS/cert**: `cheeztv.unkin.net` via internal Traefik + external-dns (198.18.200.4) + cert-manager `cheeztv-tls`, matching logviewer.unkin.net; plus the cluster hostname `cheeztv.k8s.syd1.au.unkin.net` mirroring fafflix.
- **fafflix change**: ADDS `movies/kids` + `tvshows/kids` subPath mounts alongside its current media mounts so kids libraries are resumable there. Existing mounts, hostname and ingress untouched.
- Registers cheeztv in the media ApplicationSet generator and AppProject destinations.

## Validation
kubeconform clean: cheeztv 28/28, jellyfin 25/25 valid. No arr/watchstate/domain changes.

Note: the k8up restic repo password must be seeded at `kv/kubernetes/namespace/cheeztv/default/k8up-restic` (same manual step as fafflix); templated Vault policy already grants read, no terraform-vault change needed.
Reviewed-on: #413
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-08-25 19:59:08 +10:00
unkin-agent b419635fc2 arrstack: add kids backend tier (sonarr/radarr/nzbget-kids) (#412)
Splits the arrstack media pipeline into an adult and a kids tier so kids content lives in its own libraries, databases, and downloader while reusing the shared prowlarr, CNPG cluster, Valkey, media PVCs, and S3 buckets. Backend workloads only — no routing/ingress, no arrproxy, no adult-instance changes in this PR.

How:
- sonarr-kids / radarr-kids: mirror the adult -unkin fork Deployments (same images/tags, 3 active-active replicas) on their own Postgres DBs (sonarr-kids-main / radarr-kids-main), UrlBase /3df803/sonarr and /3df803/radarr, and kids MediaCover/Backup S3 prefixes.
- Media mounts use the tvshows/kids and movies/kids subPaths of the shared media-tv / media-movies PVCs, so kids libraries are isolated on the same CephFS subvolumes.
- nzbget-kids: second downloader on its own RWO config PVC, completed downloads land in the kids media subtrees for same-filesystem hardlink imports.
- postgres: two managed roles + Database CRDs and their VSO-synced <app>-kids-db credential secrets.
- per-app VMPodScrape for each kids exportarr sidecar; new apps registered in the base kustomization.

Validated: kustomize build + kubeconform (96/96 valid), yamllint + pre-commit clean.
Reviewed-on: #412
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-08-25 08:03:21 +10:00
unkin-agent a064d9a3c4 arrproxy: bump images to v0.3.1 (#411)
Deploy the new space-pirate UI background released in arrproxy v0.3.1.

- Bump arrproxy-api image to v0.3.1
- Bump arrproxy-ui image to v0.3.1

Reviewed-on: #411
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-08-24 21:42:28 +10:00
unkin-agent aea319cec0 authentik: ClientIP session affinity on server service (#410)
## Why

Tofu applies against authentik hit a cross-replica read-back race: a create lands on one of the 3 server pods and the provider's immediate read-back hits another pod that returns stale data, failing the run with "inconsistent result after apply" (upstream bug goauthentik/terraform-provider-authentik#954, still open). ClientIP session affinity keeps a whole apply run on a single pod, sidestepping the race until the provider is fixed.

## Changes

- Sets `server.service.sessionAffinity: ClientIP` with `sessionAffinityConfig.clientIP.timeoutSeconds: 60` in the au-syd1 authentik overlay values (rendered by the vendored chart's `templates/server/service.yaml`)

Reviewed-on: #410
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-08-23 23:13:51 +10:00
unkin-agent 5a23146513 logging: set clickhouse readonly profile to readonly=2 (#409)
ci/woodpecker/pr/vector-test Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/pr/kubeconform Pipeline was successful
## Why

The `readonly` profile used by `logreader` sets `readonly=1`, which rejects any per-query settings change. HTTP clients (logviewer, chlog) send settings params like `max_execution_time` with their queries, so every query fails with `Code: 164 DB::Exception: Cannot modify 'max_execution_time' setting in readonly mode` — logviewer healthz returns 503 and CrashLoops, chlog queries fail. `readonly=2` keeps data access read-only while permitting settings changes, which is what HTTP clients need.

## Changes

- Changes the `readonly` profile in `apps/base/logging/clickhouseinstallation.yaml` from `readonly: "1"` to `readonly: "2"`

Reviewed-on: #409
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-08-23 22:36:52 +10:00
unkin-agent 03c56c0ff0 arrstack: raise arrstack-postgres resources after OOMKills (#408)
## Why

Both `arrstack-postgres` replicas were OOMKilled at the 1Gi memory limit on 2026-08-23 07:19, causing a failover to timeline 14, leaving replica-3 unrecoverable via pg_rewind, and blocking the argocd sync wave on Cluster Ready=False.

## Changes

- Raises the CNPG Cluster resources to requests 1Gi memory / 250m CPU and limits 2Gi memory / 1 CPU

**NOTE:** applying this restarts the postgres instances (CNPG rolling restart).

Reviewed-on: #408
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-08-23 22:01:25 +10:00
unkin-agent 61b5a7f695 Add Traefik Grafana dashboard (#407)
## Why

Traefik metrics (traefik_entrypoint_requests_total, traefik_service_*, etc.) are now flowing into VictoriaMetrics with the relabeled `traefik_instance` label, but there is no dashboard to view them.

## Changes

- Add `apps/base/grafana/dashboards/traefik.yaml`: GrafanaDashboard CR mirroring the redis.yaml sibling (gzipJson embed, `instanceSelector dashboards: grafana`, `resyncPeriod: 5m`, `allowCrossNamespaceImport: false`)
- Embed the official Traefik Standalone dashboard (grafana.com ID 17346, revision 9), adapted:
  - strip `__inputs`/`__requires`/`__elements` and the `DS_PROMETHEUS` datasource variable
  - rewrite all prometheus datasource uids to the estate VictoriaMetrics uid `det2y55dac4jkc`
  - add an `instance` template variable over `traefik_instance` and filter every query with `traefik_instance=~"$instance"` so traefik-internal / traefik-external are switchable
  - chain the entrypoint/service variable queries through the instance filter
- Register the dashboard in `apps/base/grafana/kustomization.yaml`

Validated: gzipJson decode roundtrip, `kustomize build`, kubeconform.
Reviewed-on: #407
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-08-23 21:56:45 +10:00
unkin-agent 8da505ceb4 arrproxy: bump image versions to v0.3.0 (#406)
Ship icon grid + jellyfin tile features by upgrading arrproxy UI and API to v0.3.0.

- Bump arrproxy-api image from v0.2.0 to v0.3.0
- Bump arrproxy-ui image from v0.2.0 to v0.3.0
- Verified images built and pushed to artifactapi

Reviewed-on: #406
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-08-23 21:38:43 +10:00
unkin-agent f6c297dcb5 Add logviewer.unkin.net internal DNS record (#405)
## Why
logviewer.unkin.net does not resolve, so the log viewer UI is unreachable even though its Gateway and certificate already exist (from #404).

## Changes
- Adds DNSRecord `logviewer-dns-internal` (A, ttl 600 → 198.18.200.4, the internal traefik gateway VIP) to bind-internal authoritative records, mirroring the existing ghp/identity records

Reviewed-on: #405
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-08-23 21:38:27 +10:00
unkin-agent e608dadae7 observability: fix dropped kubelet/cadvisor metrics, add kube-state-metrics + VMAlert k8s.rules (#399)
## Why

Kubernetes container metrics are entirely missing from VictoriaMetrics, leaving the CNPG dashboard CPU panels empty. The `kubernetes-nodes`/`kubernetes-nodes-cadvisor` scrape jobs use a blanket `labelmap` on `__meta_kubernetes_node_label_(.+)`, copying ~50+ node-feature-discovery labels onto every series; every kubelet/cadvisor series then exceeds vminsert's 40-label default and 100% are silently dropped (`vm_rows_ignored_total{reason="too_many_labels"}` ~38B). On top of that, kube-state-metrics, VMAlert and the kube-prometheus `k8s.rules` recording rules were never deployed, so `node_namespace_pod_container:container_cpu_usage_seconds_total:sum_irate` (and companions) have no producer.

## Changes

- replaces the blanket node-label `labelmap` in both node scrape jobs with a single `__meta_kubernetes_node_name -> node` relabel
- renames the node jobs so series carry `job="kubelet"` plus `metrics_path` (`/metrics` and `/metrics/cadvisor`), matching kube-prometheus selectors; the cadvisor job keeps a distinct `job_name` (uniqueness requirement) and forces `job=kubelet` via relabel
- adds kube-state-metrics v2.20.0 (Deployment, standard ClusterRole/Binding, headless Service, VMServiceScrape with `jobLabel` yielding `job="kube-state-metrics"`), image routed through the artifactapi `k8s-registry` remote
- adds a VMAlert CR (datasource vmselect-main, remoteWrite/remoteRead vminsert/vmselect-main, `notifier.blackhole` since it only evaluates recording rules)
- adds a VMRule with the kube-prometheus `k8s.rules` recording group (cpu sum_irate, memory working_set/rss/cache/swap, namespace requests/limits sums, `kube_pod_owner` workload relabel rules)

## Prerequisite

The artifactapi `k8s-registry` remote exists but its `patterns` allowlist (`^autoscaling/vpa-`, `^external-dns/`, `^sig-storage/`) does not cover `kube-state-metrics/` — terraform-artifactapi needs `^kube-state-metrics/` added to `config/remote_docker/k8s-registry.yaml` before/with this merge, or the KSM pod will ImagePullBackOff.

## Notes

- validated with `kustomize build` (base + au-syd1 overlay) and kubeconform (VM CRD schemas skipped)
- the grafana CNPG dashboard also has stale datasource uids — separate follow-up, deliberately not touched here

Reviewed-on: #399
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-08-23 21:38:06 +10:00
unkin-agent 13a04a6ebc Export and scrape metrics from all redis/valkey instances (#398)
## Why

No redis/valkey instance managed in this repo exports metrics today — the six caches (artifactapi, authentik, litellm, gitea, netbox, jellyfin) are blind spots in VictoriaMetrics.

## What

- Enables the valkey-operator exporter sidecar on the jellyfin ValkeyCluster (`exporter.enabled: true`); overrides the operator's default bare-dockerhub image (`oliver006/redis_exporter:v1.80.0`) with the artifactapi-proxied pin. The operator manages a dedicated `_exporter` ACL user for it.
- Adds a `metrics-exporter` sidecar (`artifactapi.k8s.syd1.au.unkin.net/dockerhub/oliver006/redis_exporter:v1.89.0`, :9121, port name `metrics`, req 32Mi/25m lim 64Mi/100m) to the five raw Deployments: artifactapi/authentik/litellm redis and gitea/netbox valkey. gitea/netbox sidecars get the same restrictive securityContext as their valkey container.
- Adds the `monitoring.unkin.net/redis-exporter: "true"` opt-in pod label to all five pod templates.
- Adds `apps/base/observability/vmpodscrape-redis.yaml` with two VMPodScrapes (any namespace, port `metrics`): `redis-exporters` selecting the opt-in label, and `valkey-operator-clusters` selecting `app.kubernetes.io/managed-by: valkey-operator` + `app.kubernetes.io/component: valkey-node` — the ValkeyCluster CR has no pod-label passthrough (verified against the v0.5.0 CRD/source), so operator pods cannot carry the opt-in label.

## Notes

- No instance runs with `requirepass`/auth, so no REDIS_PASSWORD wiring is needed on the raw Deployments; the jellyfin exporter auth is operator-managed.
- Validated: `kustomize build` on every touched base + au-syd1 overlay (helm overlays with `--enable-helm`) and `kubeconform -strict -ignore-missing-schemas` (68 valid, 0 invalid).

Reviewed-on: #398
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-08-23 19:19:16 +10:00
unkin-agent 08a3493097 logging: deploy logviewer v0.1.0 behind oauth2-proxy (#404)
## Why

The logviewer UI gives browser access to the ClickHouse log store (`logreader` user). It must only be reachable by authorized users, so it sits behind an Authentik-gated oauth2-proxy front at https://logviewer.unkin.net, mirroring the arrstack front-door and mediamover/pdbmux gateway patterns.

## Changes

- Adds the logviewer Deployment (1 replica, `docker-internal/logviewer:v0.1.0`) reading ClickHouse at `clickhouse-logs` via the existing `clickhouse-logreader` Secret, `/healthz` probes, nonroot securityContext
- Adds an oauth2-proxy (quay.io/oauth2-proxy/oauth2-proxy:v7.15.3) front with issuer `https://identity.unkin.net/application/o/logviewer/`, `ak_groups` claim, allowed group `akP-logviewer-admin`, internal-CA trust via the combine-certs initContainer
- Syncs kv `kubernetes/namespace/logging/default/oauth-credentials` into the `logviewer-oauth-credentials` Secret via the existing `default` VaultAuth (kv path already seeded; no terraform-vault change needed)
- Adds ClusterIP Services for the app and the oauth2-proxy front door
- Adds internal Gateway + HTTPRoutes for `logviewer.unkin.net` (vault-issuer cert, external-dns -> 198.18.200.4, http->https redirect)
- Registers `logviewer/` in the logging base kustomization

## Dependency

Login requires the Authentik `logviewer` provider/application from terraform-authentik PR #21. This PR is safe to merge first: the VSO secret sync and the deployment come up regardless; OIDC login only works once #21 is applied.

Validated with `kustomize build` (base + au-syd1 overlay) and kubeconform (49/49 valid).

Reviewed-on: #404
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-08-23 18:51:15 +10:00
unkin-agent 4e3c8288e2 Bump radarr to v6.4.2-unkin7 (#403)
Ship Commands-index migration (PR #13) which adds partial indexes on Commands for queue claim polling.

- Bump radarr image to v6.4.2-unkin7 in apps/base/arrstack/radarr/deployment.yaml

Reviewed-on: #403
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-08-23 17:15:54 +10:00
unkin-agent c649cc7ee3 Bump prowlarr to v2.6.2-unkin6 (#402)
Ship Commands-index migration.

- Update prowlarr image tag to v2.6.2-unkin6 in apps/base/arrstack/prowlarr/deployment.yaml

Reviewed-on: #402
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-08-23 17:13:54 +10:00
unkin-agent cd51078d79 logging: add read-only clickhouse logreader user + Vault-synced secret (#400)
## Why
CLI tools and the logviewer UI need ClickHouse access without write or DDL rights. The only existing app user (`vector`) has `access_management` and write access; this adds a properly scoped read-only login using the same secret-sourcing mechanism.

## Changes
- Adds a `logreader` user to the `logs` ClickHouseInstallation: password hash via `secretKeyRef` to the Vault-synced `clickhouse-logreader` Secret (same mechanism as `vector`), networks `::/0`, `readonly` profile, scoped to the `logs` database only
- Adds a `readonly` profile (`readonly: 1`, same memory/time limits as default)
- Adds a `clickhouse-logreader` VaultStaticSecret mirroring the existing `clickhouse-credentials` object (kv-v2, 5m refresh, VSO-created destination Secret)

## Vault kv
Seeded at `kv/kubernetes/namespace/logging/default/clickhouse-logreader` (version 1) with keys: `username`, `password`, `password_sha256_hex`. No secret material lands in git; the CHI only ever sees the sha256 hash via the synced Secret.

Reviewed-on: #400
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-08-23 17:00:07 +10:00
unkin-agent b5acb19ac0 Bump arrstack image pins: radarr v6.4.2-unkin6, sonarr v5.0.0-unkin6 (#401)
Ship ffprobe exec bit fix (radarr) and Commands-index migration (sonarr).

- Update radarr image tag v6.4.2-unkin5 → v6.4.2-unkin6 (ffprobe fix)
- Update sonarr image tag v5.0.0-unkin5 → v5.0.0-unkin6 (Commands-index)

Reviewed-on: #401
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-08-23 16:57:27 +10:00
unkin-agent ba71bd1a35 Scrape traefik gateway metrics into VictoriaMetrics (#397)
## Why

Both traefik instances (traefik-internal/traefik-external, chart 40.0.0, image v3.7.0) already expose prometheus metrics on container port `metrics` (9100), but nothing scrapes them — VictoriaMetrics has no traefik series (verified empty). The observability VMAgent runs with selectAllByDefault and consumes VM*Scrape CRs only, so a VMPodScrape is all that is needed.

## Changes

- Adds `apps/base/observability/vmpodscrape-traefik.yaml`: scrapes port `metrics` /metrics on `app.kubernetes.io/name: traefik` pods in traefik-system, relabeling `app.kubernetes.io/instance` to a stable `traefik_instance` label to split internal vs external series
- Registers the new CR in the observability base kustomization
- Enables `metrics.prometheus.addEntryPointsLabels` and `addRoutersLabels` in both au-syd1 traefik values files for per-entrypoint/per-route dashboards (pods restart on rollout — acceptable)

Validated with `kustomize build` (observability base + overlay, traefik-system overlay with --enable-helm) and kubeconform (unknown CRDs skipped); rendered deployments carry the new metric flags and the VMPodScrape renders in the overlay.

Reviewed-on: #397
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-08-23 12:41:58 +10:00
unkin-agent 8fd1d83b1b Add Redis/Valkey Grafana dashboard (#396)
## Why

The grafana-operator estate has no dashboard for Redis/Valkey; redis_exporter instances are being added in a parallel PR and need visibility.

## Changes

- Adds `apps/base/grafana/dashboards/redis.yaml`, a GrafanaDashboard CR embedding grafana.com dashboard 763 ("Redis Dashboard for Prometheus Redis Exporter 1.x") as gzipJson like the sibling dashboards, with namespace/instance template variables over `redis_up` for multi-instance use and all datasource refs pinned to the VictoriaMetrics datasource uid (`det2y55dac4jkc`), registered in the grafana kustomization

Reviewed-on: #396
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-08-23 12:38:55 +10:00
unkin-agent 19f7afac92 Add shared arrstack Valkey and wire arr apps' Redis features (#394)
Activates the arr forks (sonarr/radarr/prowlarr) #14 Redis features — SignalR backplane, cross-replica cache-invalidation bus, and distributed rate limiter — which ship deployed but inert because no Valkey exists in arrstack and nothing is wired to it. Ben wants ONE Valkey shared by all three apps.

## Changes
- Adds a single shared `ValkeyCluster` (`arrstack-valkey`) under `apps/base/arrstack/valkey/`, modeled on `jellyfin-valkey`: `shards:1`/`replicas:2` HA (one primary + two replicas, automatic failover), image via artifactapi (`artifactapi.k8s.syd1.au.unkin.net/dockerhub/valkey/valkey:9.0.0`), operator-default passwordless `default` user, node spread across hosts, cluster-aware PDB, ephemeral `/data`.
- Registers the `valkey` component in the arrstack base kustomization.
- Wires `<App>__Redis__Host` + `<App>__Redis__Port` into the `sonarr-env` / `radarr-env` / `prowlarr-env` ConfigMaps, all pointing at the shared service `valkey-arrstack-valkey.arrstack.svc.cluster.local:6379`.

## Notes
- Setting `Host` is the activation switch: the fork's `RedisOptions.IsConfigured` gates purely on a non-empty `Host`, so there is no separate Enabled flag.
- Passwordless (jellyfin parity — the operator leaves the `default` user without auth), so no `Password`/`Ssl` is wired and there is no secret to reference.
- One cluster is safe for all three: each fork namespaces its keys and pub/sub channels by a per-app prefix (`sonarr:ratelimit:` / `radarr:ratelimit:` / `prowlarr:ratelimit:`), so their state never collides.
- App `image:` (-unkin5), the waitfordb initContainer, and S3 buckets are untouched.
- Validated: `kubectl kustomize` renders clean for both `apps/base/arrstack` and `apps/overlays/au-syd1/arrstack`.

Follow-up: after merge, Valkey must come up and the three apps must roll (pick up the new env) before the #14 features can be validated live.
---------

Co-authored-by: Ben Vincent <ben@unkin.net>
Reviewed-on: #394
Co-authored-by: Unkin Agent <unkin-agent@unkin.net>
Co-committed-by: Unkin Agent <unkin-agent@unkin.net>
2026-08-23 12:37:42 +10:00
unkin-agent b3d202b8fa arrstack: add exportarr metrics sidecars to sonarr and radarr (#395)
Sonarr and radarr currently expose no Prometheus metrics, so queue depth, indexer health, and library stats are invisible to the observability stack. This adds the standard exportarr exporter as a sidecar to both Deployments.

- Adds an exportarr sidecar (ghcr.io/onedr0p/exportarr v2.3.0, pulled through the artifactapi `ghcr` remote, which already whitelists `^onedr0p/`) to the sonarr and radarr Deployments
- Points each exporter at its local replica via env (`URL=http://localhost:8989/sonarr` / `:7878/radarr`, matching the apps' UrlBase) and reuses the existing VSO-synced API-key Secrets (`sonarr-apikey`/`radarr-apikey`, key `apitoken`) via `APIKEY` secretKeyRef — no key in argv
- Serves metrics on 9707 (sonarr) / 9708 (radarr) with `/healthz` liveness+readiness probes and small resources (25m/32Mi requests, 100m/128Mi limits)
- Adds per-app VMPodScrape objects (picked up by the observability VMAgent, selectAllByDefault); pod-level rather than VMServiceScrape because the app Services don't expose the metrics port

Validated with `kustomize build apps/base/arrstack` + kubeconform (0 invalid).

Note: with 3 replicas per app, each pod's exporter reports the same shared-Postgres-backed stats, so series are duplicated across the `pod` label; dashboards should aggregate with `max` or filter to one pod.
Reviewed-on: #395
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-08-23 12:35:56 +10:00
unkin-agent 32611bfd16 Deploy mediamover v0.1.0 into arrstack (#393)
Deploys the released mediamover v0.1.0 into the arrstack namespace: a single-replica server (API + UI on :8080) that browses the mediafs source, queues per-file copy/move operations into the media PVCs, and spawns one worker Job per file using its own image — its ServiceAccount/RBAC grants the Job and Pod access that requires. All PVs/PVCs it mounts are already live.

- Adds `apps/base/arrstack/mediamover/` with SA, Role/RoleBinding (batch jobs create/get/list/watch/delete; pods get/list/watch; pods/log get/list), Deployment, Service, Gateway, HTTPRoute
- Runs `mediamover server` with `--src-root /srv/src --src-pvc mediafs`, `--dst-roots movies=/srv/dst/movies,tv=/srv/dst/tv`, `--dst-pvc movies=media-movies,tv=media-tv`, `--namespace arrstack`, `--image ...mediamover:v0.1.0`
- Mounts mediafs RW at /srv/src (move deletes the source) plus media-movies and media-tv destinations; keeps 1 replica with Recreate strategy since the queue is in-memory
- Names the Service `mediamover` on 8080 to match the worker callback default `http://mediamover.arrstack.svc:8080`
- Exposes the UI at https://mediamover.k8s.syd1.au.unkin.net via a dedicated internal Gateway (pdbmux pattern); the existing arrproxy Gateway is external and hostname-locked to arrstack.unkin.net so it cannot carry this route
- Probes hit `GET /api/limit` (the server has no dedicated health endpoint); registers `mediamover` in the arrstack base kustomization

Reviewed-on: #393
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-08-23 00:09:40 +10:00
unkin-agent 7c49a9f3ad Deploy arrstack S3 TLS-trust fix (arrstack images -unkin4 -> -unkin5) (#392)
## Why

Ben merged the S3 TLS-trust fix into each Servarr fork's `main` (sonarr#10, radarr#11, prowlarr#10, branch `benvin/s3-tls-trust-fix`). The fix corrects TLS trust for the leaf-only certificate presentation of the Ceph RGW endpoint, so backups and MediaCover uploads to S3 succeed across all replicas instead of failing the TLS handshake.

The `-unkin5` images carrying the fix are built and pushed to `artifactapi.k8s.syd1.au.unkin.net/docker-internal/<app>`. This PR rolls them out.

## Changes

- Bump `apps/base/arrstack/sonarr/deployment.yaml` app image `v5.0.0-unkin4` -> `v5.0.0-unkin5`
- Bump `apps/base/arrstack/radarr/deployment.yaml` app image `v6.4.2-unkin4` -> `v6.4.2-unkin5`
- Bump `apps/base/arrstack/prowlarr/deployment.yaml` app image `v2.6.2-unkin4` -> `v2.6.2-unkin5`

Only the application container `image:` line changes in each file; the `waitfordb` initContainer and bucket YAML are untouched.

Reviewed-on: #392
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-08-22 23:15:11 +10:00
unkin-agent 671c075a08 Mount mediafs CephFS into arrstack (#391)
Exposes the legacy mediafs CephFS filesystem to the arrstack namespace so the upcoming mediamover tool can migrate media and run bandwidth tests against the old filesystem; the ceph-mediafs client secret is already seeded in Vault. The mover deployment itself lands in a follow-up PR.

- Adds a `ceph-mediafs-secret` VaultStaticSecret in csi-cephfs syncing `kv/service/kubernetes/au/syd1/csi/ceph-mediafs-secret`
- Adds a 10Ti static RWX PV `arrstack-mediafs` (fsName `mediafs`, rootPath `/`, staged with the mediafs secret) pinned by claimRef
- Adds the matching `mediafs` PVC in arrstack (storageClassName "", volumeName-bound, backups excluded)
- Registers all three in their base kustomizations

Reviewed-on: #391
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-08-22 23:05:43 +10:00
unkin-agent 06fe261661 arrstack: deploy -unkin4 (Redis SignalR backplane + S3 storage) (#390)
## Why

The cluster runs the arrstack forks at `-unkin3`. Ben merged two workstreams into each fork's `main`:

- **Redis SignalR backplane + cross-replica cache invalidation** — SignalR messages and cache-invalidation events fan out across replicas via Redis, so any replica serves consistent live UI/state.
- **MediaCover -> S3 + backups -> S3** — MediaCover (sonarr/radarr) and backups (all three) use an optional S3 store, so any stateless replica can serve covers and write/restore backups.

Fresh `-unkin4` tags were cut on each fork's `main` HEAD, building `docker-internal/<app>:<base>-unkin4` images. This bumps the deployment images so ArgoCD rolls the merged work into the cluster.

## Changes

- Bump sonarr app image `v5.0.0-unkin3` -> `v5.0.0-unkin4`
- Bump radarr app image `v6.4.2-unkin3` -> `v6.4.2-unkin4`
- Bump prowlarr app image `v2.6.2-unkin3` -> `v2.6.2-unkin4`

The waitfordb initContainer and bucket resources are unchanged; only the app-container `image:` tags move.

Reviewed-on: #390
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-08-22 20:46:48 +10:00
unkin-agent bdf6114794 arrstack: S3 buckets + wiring for app backups and sonarr MediaCover (#389)
## Why

Servarr backups (periodic config+DB zips) and sonarr MediaCover posters still live on each replica's local `/config` emptyDir: backups are leader-local and lost on reschedule, and sonarr covers render inconsistently across the 3 active-active replicas. #387 moved radarr covers to a shared Ceph RGW bucket; this extends that pattern to backups for all three apps and to sonarr's covers.

## Changes

- Add an `arrstack-backups` ObjectStoreUser + Bucket (cephrgw-operator); the operator mints the `arrstack-backups-s3` credential Secret in-namespace (AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY / S3_ENDPOINT), so no Vault KV seeding is required. Backups get their own bucket (not `arrstack-media`) because they carry secrets (Config.xml API keys, the DB) and warrant separate credentials/lifecycle from poster art; the three apps share it, isolated by a per-app key prefix.
- Wire sonarr to the shared `arrstack-media` bucket via `Sonarr__MediaCoverS3__*` (prefix `sonarr`, partitioned from radarr's `radarr` prefix to avoid movie/series id collisions), plus the estate CA mount at `/etc/ssl/vault-ca/ca.crt` for RGW TLS validation.
- Wire radarr/sonarr/prowlarr backups via `<App>__BackupS3__*` to `arrstack-backups` (per-app prefix); add the CA mount to sonarr and prowlarr.

## Notes

- Credentials come only from the operator-minted Secret via env; no secrets in manifests.
- The env keys are consumed by the -unkin3+ images (separate app PRs) and ignored by older images, so this is safe to apply ahead of the image bump.
- `kubectl kustomize apps/base/arrstack` builds clean.

Reviewed-on: #389
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-08-22 18:39:40 +10:00
unkin-agent eb37beb7e4 Deploy arrstack -unkin3 (active-active code) (#388)
Deploy the merged active-active work now on each Servarr fork's main. The cluster still runs the -unkin2 images, so the merged code (K8s process lifecycle, Redis-backed distributed rate limiting, and radarr MediaCover->S3) is not yet deployed. The -unkin3 tag pipelines have built and pushed new images to docker-internal.

Changes:
- Bump sonarr app image v5.0.0-unkin2 -> v5.0.0-unkin3
- Bump radarr app image v6.4.2-unkin2 -> v6.4.2-unkin3
- Bump prowlarr app image v2.6.2-unkin2 -> v2.6.2-unkin3

Touches only the app container image line in each apps/base/arrstack/<app>/deployment.yaml; the waitfordb initContainer is unchanged.

Reviewed-on: #388
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-08-22 18:28:14 +10:00