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>
This commit was merged in pull request #414.
This commit is contained in:
2026-08-25 20:06:39 +10:00
committed by BenVincent
parent 94da740a9d
commit 9db52c5e26
37 changed files with 319 additions and 638 deletions
+3 -3
View File
@@ -15,12 +15,12 @@ resources:
- backups-bucket.yaml
- postgres
- valkey
- sonarr
- radarr
- prowlarr
- nzbget
- sonarr-kids
- radarr-kids
- nzbget-kids
- sonarr-adult
- radarr-adult
- nzbget-adult
- arrproxy
- mediamover
@@ -2,7 +2,7 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: nzbget
name: nzbget-adult
namespace: arrstack
spec:
replicas: 1
@@ -11,25 +11,25 @@ spec:
type: Recreate
selector:
matchLabels:
app: nzbget
app: nzbget-adult
template:
metadata:
labels:
app: nzbget
app: nzbget-adult
spec:
securityContext:
fsGroup: 1000
fsGroupChangePolicy: OnRootMismatch
initContainers:
# Seed download layout onto the shared media PVCs (not /config or an
# emptyDir) so completed downloads land beside the arr libraries and
# imports are same-filesystem hardlink moves. Reuses the image's own
# template (inherits correct WebDir/ConfigTemplate) and appends the
# path/category overrides once; nzbget honours the last value for a
# repeated option, and the grep guard keeps re-runs idempotent so admin
# UI edits to the persisted /config/nzbget.conf survive restarts.
# Seed download layout onto the adult media subtrees (not /config or an
# emptyDir) so completed adult downloads land beside the adult arr libraries
# and imports are same-filesystem hardlink moves. The media mounts use the
# tvshows/adult and movies/adult subPaths, so the in-container paths match
# the kids nzbget while the data stays scoped to the adult subtree. Reuses
# the image's own template and appends the path/category overrides once;
# the grep guard keeps re-runs idempotent so admin UI edits survive.
- name: seed-config
image: artifactapi.k8s.syd1.au.unkin.net/dockerhub/linuxserver/nzbget:version-v26.2
image: docker.io/linuxserver/nzbget:version-v26.2
command:
- sh
- -c
@@ -42,11 +42,12 @@ spec:
cat >> /config/nzbget.conf << 'CONF'
# arrstack-managed download layout (appended once; last value wins).
# Downloads land on the shared media PVCs by category so sonarr/radarr
# import with atomic hardlink moves (download dir + library share one
# filesystem per media type). InterDir is empty: nzbget writes each
# download straight into its category DestDir, so BOTH tv and movies
# stay on their own PVC with no cross-filesystem intermediate copy.
# Downloads land on the shared media PVCs by category so sonarr-adult/
# radarr-adult import with atomic hardlink moves (download dir +
# library share one filesystem per media type). InterDir is empty:
# nzbget writes each download straight into its category DestDir, so
# BOTH tv and movies stay on their own PVC with no cross-filesystem
# intermediate copy.
MainDir=/media/tv
InterDir=
DestDir=/media/tv/downloads
@@ -77,11 +78,13 @@ spec:
mountPath: /config
- name: media-tv
mountPath: /media/tv
subPath: tvshows/adult
- name: media-movies
mountPath: /media/movies
subPath: movies/adult
containers:
- name: nzbget
image: artifactapi.k8s.syd1.au.unkin.net/dockerhub/linuxserver/nzbget:version-v26.2
image: docker.io/linuxserver/nzbget:version-v26.2
imagePullPolicy: IfNotPresent
ports:
- name: http
@@ -123,12 +126,14 @@ spec:
mountPath: /config
- name: media-tv
mountPath: /media/tv
subPath: tvshows/adult
- name: media-movies
mountPath: /media/movies
subPath: movies/adult
volumes:
- name: config
persistentVolumeClaim:
claimName: nzbget-config
claimName: nzbget-adult-config
- name: media-tv
persistentVolumeClaim:
claimName: media-tv
@@ -6,5 +6,3 @@ resources:
- pvc-config.yaml
- deployment.yaml
- service.yaml
- gateway.yaml
- httproute.yaml
@@ -0,0 +1,16 @@
---
# NZBGet (adult) config + queue/temp state. RWO on cephrbd (block, fast-delete).
# The download data itself lives on the shared media PVCs, not here.
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: nzbget-adult-config
namespace: arrstack
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 5Gi
storageClassName: cephrbd-fast-delete
volumeMode: Filesystem
@@ -2,7 +2,7 @@
apiVersion: v1
kind: Service
metadata:
name: nzbget
name: nzbget-adult
namespace: arrstack
spec:
ports:
@@ -11,5 +11,5 @@ spec:
protocol: TCP
targetPort: http
selector:
app: nzbget
app: nzbget-adult
type: ClusterIP
-37
View File
@@ -1,37 +0,0 @@
---
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
labels:
traefik.io/instance: internal
annotations:
cert-manager.io/cluster-issuer: vault-issuer
cert-manager.io/common-name: nzbget.k8s.syd1.au.unkin.net
cert-manager.io/private-key-size: "4096"
external-dns.alpha.kubernetes.io/hostname: nzbget.k8s.syd1.au.unkin.net
external-dns.alpha.kubernetes.io/target: 198.18.200.4
name: nzbget
namespace: arrstack
spec:
gatewayClassName: traefik-internal
listeners:
- allowedRoutes:
namespaces:
from: Same
hostname: nzbget.k8s.syd1.au.unkin.net
name: http
port: 80
protocol: HTTP
- allowedRoutes:
namespaces:
from: Same
hostname: nzbget.k8s.syd1.au.unkin.net
name: https
port: 443
protocol: HTTPS
tls:
certificateRefs:
- group: ""
kind: Secret
name: nzbget-tls
mode: Terminate
-49
View File
@@ -1,49 +0,0 @@
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: nzbget-http-redirect
namespace: arrstack
spec:
hostnames:
- nzbget.k8s.syd1.au.unkin.net
parentRefs:
- group: gateway.networking.k8s.io
kind: Gateway
name: nzbget
sectionName: http
rules:
- filters:
- type: RequestRedirect
requestRedirect:
scheme: https
statusCode: 301
matches:
- path:
type: PathPrefix
value: /
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: nzbget-route
namespace: arrstack
spec:
hostnames:
- nzbget.k8s.syd1.au.unkin.net
parentRefs:
- group: gateway.networking.k8s.io
kind: Gateway
name: nzbget
sectionName: https
rules:
- backendRefs:
- group: ""
kind: Service
name: nzbget
port: 6789
weight: 1
matches:
- path:
type: PathPrefix
value: /
-16
View File
@@ -1,16 +0,0 @@
---
# NZBGet config + queue/temp state. RWO on cephrbd (block). Retain: this is
# state. The download data itself lives on the shared media PVCs, not here.
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: nzbget-config
namespace: arrstack
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 5Gi
storageClassName: cephrbd-fast-retain
volumeMode: Filesystem
+24 -24
View File
@@ -50,30 +50,6 @@ spec:
owner: app
managed:
roles:
- name: sonarr
ensure: present
comment: Sonarr application role (owns sonarr-main)
login: true
superuser: false
createdb: false
createrole: false
inherit: true
replication: false
connectionLimit: -1
passwordSecret:
name: sonarr-db
- name: radarr
ensure: present
comment: Radarr application role (owns radarr-main)
login: true
superuser: false
createdb: false
createrole: false
inherit: true
replication: false
connectionLimit: -1
passwordSecret:
name: radarr-db
- name: prowlarr
ensure: present
comment: Prowlarr application role (owns prowlarr-main)
@@ -110,6 +86,30 @@ spec:
connectionLimit: -1
passwordSecret:
name: radarr-kids-db
- name: sonarr-adult
ensure: present
comment: Sonarr (adult) application role (owns sonarr-adult-main)
login: true
superuser: false
createdb: false
createrole: false
inherit: true
replication: false
connectionLimit: -1
passwordSecret:
name: sonarr-adult-db
- name: radarr-adult
ensure: present
comment: Radarr (adult) application role (owns radarr-adult-main)
login: true
superuser: false
createdb: false
createrole: false
inherit: true
replication: false
connectionLimit: -1
passwordSecret:
name: radarr-adult-db
enablePDB: true
enableSuperuserAccess: false
failoverDelay: 0
@@ -0,0 +1,15 @@
---
# Per-app database owned by the radarr-adult managed role. The fork's provider
# runs its own schema migrations on first start (advisory-locked, so only one
# replica migrates). retain: the database survives a Database CRD delete.
apiVersion: postgresql.cnpg.io/v1
kind: Database
metadata:
name: radarr-adult-main
namespace: arrstack
spec:
cluster:
name: arrstack-postgres
name: radarr-adult-main
owner: radarr-adult
databaseReclaimPolicy: retain
@@ -1,15 +0,0 @@
---
# Per-app database owned by the radarr managed role. The fork's provider runs its
# own schema migrations on first start (advisory-locked, so only one replica
# migrates). retain: the database survives a Database CRD delete.
apiVersion: postgresql.cnpg.io/v1
kind: Database
metadata:
name: radarr-main
namespace: arrstack
spec:
cluster:
name: arrstack-postgres
name: radarr-main
owner: radarr
databaseReclaimPolicy: retain
@@ -0,0 +1,15 @@
---
# Per-app database owned by the sonarr-adult managed role. The fork's provider
# runs its own schema migrations on first start (advisory-locked, so only one
# replica migrates). retain: the database survives a Database CRD delete.
apiVersion: postgresql.cnpg.io/v1
kind: Database
metadata:
name: sonarr-adult-main
namespace: arrstack
spec:
cluster:
name: arrstack-postgres
name: sonarr-adult-main
owner: sonarr-adult
databaseReclaimPolicy: retain
@@ -1,15 +0,0 @@
---
# Per-app database owned by the sonarr managed role. The fork's provider runs its
# own schema migrations on first start (advisory-locked, so only one replica
# migrates). retain: the database survives a Database CRD delete.
apiVersion: postgresql.cnpg.io/v1
kind: Database
metadata:
name: sonarr-main
namespace: arrstack
spec:
cluster:
name: arrstack-postgres
name: sonarr-main
owner: sonarr
databaseReclaimPolicy: retain
@@ -6,8 +6,8 @@ resources:
- vaultstaticsecret.yaml
- cnpg_cluster.yaml
- cnpg_backup.yaml
- database-sonarr.yaml
- database-radarr.yaml
- database-prowlarr.yaml
- database-sonarr-kids.yaml
- database-radarr-kids.yaml
- database-sonarr-adult.yaml
- database-radarr-adult.yaml
@@ -10,40 +10,6 @@
# (wave 1) reconciles the roles.
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultStaticSecret
metadata:
name: sonarr-db
namespace: arrstack
spec:
destination:
create: true
name: sonarr-db
overwrite: true
hmacSecretData: true
mount: kv
path: kubernetes/namespace/arrstack/default/sonarr-db
refreshAfter: 5m
type: kv-v2
vaultAuthRef: default
---
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultStaticSecret
metadata:
name: radarr-db
namespace: arrstack
spec:
destination:
create: true
name: radarr-db
overwrite: true
hmacSecretData: true
mount: kv
path: kubernetes/namespace/arrstack/default/radarr-db
refreshAfter: 5m
type: kv-v2
vaultAuthRef: default
---
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultStaticSecret
metadata:
name: prowlarr-db
namespace: arrstack
@@ -92,3 +58,37 @@ spec:
refreshAfter: 5m
type: kv-v2
vaultAuthRef: default
---
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultStaticSecret
metadata:
name: sonarr-adult-db
namespace: arrstack
spec:
destination:
create: true
name: sonarr-adult-db
overwrite: true
hmacSecretData: true
mount: kv
path: kubernetes/namespace/arrstack/default/sonarr-adult-db
refreshAfter: 5m
type: kv-v2
vaultAuthRef: default
---
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultStaticSecret
metadata:
name: radarr-adult-db
namespace: arrstack
spec:
destination:
create: true
name: radarr-adult-db
overwrite: true
hmacSecretData: true
mount: kv
path: kubernetes/namespace/arrstack/default/radarr-adult-db
refreshAfter: 5m
type: kv-v2
vaultAuthRef: default
@@ -0,0 +1,25 @@
---
# Non-secret env for the -unkin2 fork (adult tier). Identical mechanism to the
# kids radarr, pointed at its own shared-Postgres database (radarr-adult-main)
# and its own UrlBase (/3aa168/radarr) so arrproxy path-routing reaches the adult
# instance separately. Shares the one arrstack Valkey (keys namespaced by the
# fork's radarr:ratelimit: prefix). User/Password/ApiKey come from Secrets (see
# deployment.yaml), not here.
apiVersion: v1
kind: ConfigMap
metadata:
name: radarr-adult-env
namespace: arrstack
data:
Radarr__Postgres__Host: arrstack-postgres-rw.arrstack.svc.cluster.local
Radarr__Postgres__Port: "5432"
Radarr__Postgres__MainDb: radarr-adult-main
Radarr__Log__DbEnabled: "false"
Radarr__Auth__Method: External
Radarr__Auth__Required: DisabledForLocalAddresses
Radarr__App__InstanceName: Radarr
Radarr__Server__Port: "7878"
Radarr__Server__UrlBase: /3aa168/radarr
Radarr__Update__Mechanism: External
Radarr__Redis__Host: valkey-arrstack-valkey.arrstack.svc.cluster.local
Radarr__Redis__Port: "6379"
@@ -2,43 +2,40 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: radarr
name: radarr-adult
namespace: arrstack
annotations:
# radarr-env is a plain (unhashed) ConfigMap consumed by fixed-name envFrom,
# so editing it does not roll the Deployment on its own. Reloader watches the
# referenced ConfigMap and triggers a rolling restart on change, so adding the
# Redis env activates the #14 features on the next ArgoCD sync without a manual
# `rollout restart`.
# Reloader rolls the Deployment when radarr-adult-env changes (plain envFrom
# ConfigMap does not trigger a rollout on its own).
configmap.reloader.stakater.com/auto: "true"
spec:
# Active-active: the -unkin2 fork keeps all state in the shared Postgres
# (arrstack-postgres) and coordinates via Postgres advisory locks, so N
# replicas run concurrently behind the radarr Service. RollingUpdate is safe —
# no SQLite, no RWO lock.
# replicas run concurrently behind the radarr-adult Service. RollingUpdate is
# safe — no SQLite, no RWO lock.
replicas: 3
strategy:
type: RollingUpdate
selector:
matchLabels:
app: radarr
app: radarr-adult
template:
metadata:
labels:
app: radarr
app: radarr-adult
spec:
securityContext:
# Fork image has no USER (runs as root by default); pin it to a non-root
# UID and group-write the shared RWX CephFS /config (MediaCover etc.).
# OnRootMismatch avoids a recursive chown of the whole media tree.
# Fork image has no USER; pin it to a non-root UID and group-write the
# shared RWX CephFS media subtree. OnRootMismatch avoids a recursive
# chown of the whole media tree.
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
fsGroupChangePolicy: OnRootMismatch
initContainers:
# Gate the app on its own Postgres database+role being reachable, instead
# of relying on ArgoCD sync-waves (which deadlock if apps aren't Healthy).
# waitfordb reads the PG* env as a libpq fallback, so the password never lands in argv.
# Gate the app on its own Postgres database+role being reachable.
# waitfordb reads the PG* env as a libpq fallback, so the password never
# lands in argv.
- name: wait-for-db
image: artifactapi.k8s.syd1.au.unkin.net/docker-internal/waitfordb:v0.1.0
env:
@@ -51,16 +48,16 @@ spec:
- name: PGPORT
value: "5432"
- name: PGDATABASE
value: radarr-main
value: radarr-adult-main
- name: PGUSER
valueFrom:
secretKeyRef:
name: radarr-db
name: radarr-adult-db
key: username
- name: PGPASSWORD
valueFrom:
secretKeyRef:
name: radarr-db
name: radarr-adult-db
key: password
resources:
requests:
@@ -78,9 +75,8 @@ spec:
args:
- -nobrowser
- -data=/config
# Required: bypass the single-instance guard so multiple replicas
# can share one /config. Cross-replica safety is the Postgres layer,
# not a local lock file.
# Bypass the single-instance guard so multiple replicas can share one
# /config. Cross-replica safety is the Postgres layer, not a lock file.
- -nosingleinstancecheck
ports:
- name: http
@@ -88,27 +84,25 @@ spec:
protocol: TCP
envFrom:
- configMapRef:
name: radarr-env
name: radarr-adult-env
env:
- name: Radarr__Postgres__User
valueFrom:
secretKeyRef:
name: radarr-db
name: radarr-adult-db
key: username
- name: Radarr__Postgres__Password
valueFrom:
secretKeyRef:
name: radarr-db
name: radarr-adult-db
key: password
- name: Radarr__Auth__ApiKey
valueFrom:
secretKeyRef:
name: radarr-apikey
name: radarr-adult-apikey
key: apitoken
# MediaCover object store (shared Ceph RGW bucket). Serves posters/fanart
# from S3 so any replica can render them instead of the leader-local
# emptyDir /config. Consumed by the -unkin3+ image; older images ignore
# these unknown config keys. Creds Secret is minted by cephrgw-operator.
# MediaCover object store (shared arrstack-media Ceph RGW bucket,
# partitioned by the radarr-adult key prefix).
- name: Radarr__MediaCover__S3__Endpoint
valueFrom:
secretKeyRef:
@@ -127,14 +121,13 @@ spec:
- name: Radarr__MediaCover__S3__Bucket
value: arrstack-media
- name: Radarr__MediaCover__S3__Prefix
value: radarr
value: radarr-adult
- name: Radarr__MediaCover__S3__ForcePathStyle
value: "true"
- name: Radarr__MediaCover__S3__CaCertPath
value: /etc/ssl/vault-ca/ca.crt
# Backup object store (shared arrstack-backups Ceph RGW bucket,
# per-app key prefix). Routes the periodic config+DB zip backups off
# the ephemeral /config so any replica can write and restore them.
# partitioned by the radarr-adult key prefix).
- name: Radarr__BackupS3__Endpoint
valueFrom:
secretKeyRef:
@@ -153,14 +146,14 @@ spec:
- name: Radarr__BackupS3__Bucket
value: arrstack-backups
- name: Radarr__BackupS3__Prefix
value: radarr
value: radarr-adult
- name: Radarr__BackupS3__ForcePathStyle
value: "true"
- name: Radarr__BackupS3__CaCertPath
value: /etc/ssl/vault-ca/ca.crt
livenessProbe:
httpGet:
path: /radarr/ping
path: /3aa168/radarr/ping
port: http
initialDelaySeconds: 30
periodSeconds: 30
@@ -168,7 +161,7 @@ spec:
failureThreshold: 3
readinessProbe:
httpGet:
path: /radarr/ping
path: /3aa168/radarr/ping
port: http
initialDelaySeconds: 10
periodSeconds: 10
@@ -184,28 +177,31 @@ spec:
volumeMounts:
- name: config
mountPath: /config
# Adult movies subtree of the shared media-movies PVC (same CephFS
# subvolume the kids radarr writes and jellyfin reads).
- name: media-movies
mountPath: /media/movies
subPath: movies/adult
- name: vault-ca
mountPath: /etc/ssl/vault-ca
readOnly: true
# exportarr sidecar: polls the local replica's API and exposes Prometheus
# metrics on :9708 (scraped by the radarr-exportarr VMPodScrape).
# metrics on :9708 (scraped by the radarr-adult-exportarr VMPodScrape).
- name: exportarr
image: artifactapi.k8s.syd1.au.unkin.net/ghcr/onedr0p/exportarr:v2.3.0
image: ghcr.io/onedr0p/exportarr:v2.3.0
imagePullPolicy: IfNotPresent
args:
- radarr
env:
- name: PORT
value: "9708"
# URL includes the /radarr UrlBase (Radarr__Server__UrlBase).
# URL includes the /3aa168/radarr UrlBase (Radarr__Server__UrlBase).
- name: URL
value: http://localhost:7878/radarr
value: http://localhost:7878/3aa168/radarr
- name: APIKEY
valueFrom:
secretKeyRef:
name: radarr-apikey
name: radarr-adult-apikey
key: apitoken
ports:
- name: metrics
@@ -7,6 +7,4 @@ resources:
- configmap.yaml
- deployment.yaml
- service.yaml
- gateway.yaml
- httproute.yaml
- vmpodscrape.yaml
@@ -2,7 +2,7 @@
apiVersion: v1
kind: Service
metadata:
name: radarr
name: radarr-adult
namespace: arrstack
spec:
ports:
@@ -11,5 +11,5 @@ spec:
protocol: TCP
targetPort: http
selector:
app: radarr
app: radarr-adult
type: ClusterIP
@@ -0,0 +1,25 @@
---
# radarr-adult API key. Seeded out-of-band at
# kv/kubernetes/namespace/arrstack/default/radarr-adult (key: apitoken); the
# default k8s role's templated policy already grants read on
# kv/data/kubernetes/namespace/{{sa_namespace}}/{{sa_name}}/* for the
# arrstack/default ServiceAccount, so no terraform-vault change is needed. VSO
# syncs it into the radarr-adult-apikey Secret consumed by the Deployment.
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultStaticSecret
metadata:
name: radarr-adult-apikey
namespace: arrstack
annotations:
argocd.argoproj.io/sync-wave: "0"
spec:
destination:
create: true
name: radarr-adult-apikey
overwrite: true
hmacSecretData: true
mount: kv
path: kubernetes/namespace/arrstack/default/radarr-adult
refreshAfter: 5m
type: kv-v2
vaultAuthRef: default
@@ -1,16 +1,16 @@
---
# Scrape the exportarr sidecar (:9708) on every radarr pod. Picked up by the
# Scrape the exportarr sidecar (:9708) on every radarr-adult pod. Picked up by the
# observability VMAgent (selectAllByDefault). Pod-level rather than
# VMServiceScrape because the radarr Service doesn't expose the metrics port.
# VMServiceScrape because the radarr-adult Service doesn't expose the metrics port.
apiVersion: operator.victoriametrics.com/v1beta1
kind: VMPodScrape
metadata:
name: radarr-exportarr
name: radarr-adult-exportarr
namespace: arrstack
spec:
selector:
matchLabels:
app: radarr
app: radarr-adult
podMetricsEndpoints:
- port: metrics
path: /metrics
-33
View File
@@ -1,33 +0,0 @@
---
# Non-secret env for the -unkin2 fork. The fork reads Servarr config from
# Radarr__<Section>__<Key> env (no config.xml edits, no s6/PUID). Postgres wiring
# points every replica at the same shared DB (arrstack-postgres-rw / radarr-main);
# Auth__Method=External defers UI auth to arrproxy/oauth2-proxy; Server__UrlBase
# keeps the /radarr prefix so arrproxy path-routing works; App__InstanceName is
# identical across replicas (shared session-cookie name). User/Password/ApiKey
# come from Secrets (see deployment.yaml), not here.
apiVersion: v1
kind: ConfigMap
metadata:
name: radarr-env
namespace: arrstack
data:
Radarr__Postgres__Host: arrstack-postgres-rw.arrstack.svc.cluster.local
Radarr__Postgres__Port: "5432"
Radarr__Postgres__MainDb: radarr-main
Radarr__Log__DbEnabled: "false"
Radarr__Auth__Method: External
Radarr__Auth__Required: DisabledForLocalAddresses
Radarr__App__InstanceName: Radarr
Radarr__Server__Port: "7878"
Radarr__Server__UrlBase: /radarr
Radarr__Update__Mechanism: External
# Shared arrstack Valkey (valkey-operator). Setting Host is what activates the
# fork's #14 Redis features (SignalR backplane, cross-replica cache-invalidation
# bus, distributed rate limiter): RedisOptions.IsConfigured gates purely on a
# non-empty Host, so there is no separate Enabled flag. The operator leaves the
# default user passwordless (jellyfin parity), so no Password/Ssl is wired.
# Channels/keys are namespaced by this fork's radarr:ratelimit: prefix, so the
# one cluster is safe to share with sonarr/prowlarr.
Radarr__Redis__Host: valkey-arrstack-valkey.arrstack.svc.cluster.local
Radarr__Redis__Port: "6379"
-37
View File
@@ -1,37 +0,0 @@
---
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
labels:
traefik.io/instance: internal
annotations:
cert-manager.io/cluster-issuer: vault-issuer
cert-manager.io/common-name: radarr.k8s.syd1.au.unkin.net
cert-manager.io/private-key-size: "4096"
external-dns.alpha.kubernetes.io/hostname: radarr.k8s.syd1.au.unkin.net
external-dns.alpha.kubernetes.io/target: 198.18.200.4
name: radarr
namespace: arrstack
spec:
gatewayClassName: traefik-internal
listeners:
- allowedRoutes:
namespaces:
from: Same
hostname: radarr.k8s.syd1.au.unkin.net
name: http
port: 80
protocol: HTTP
- allowedRoutes:
namespaces:
from: Same
hostname: radarr.k8s.syd1.au.unkin.net
name: https
port: 443
protocol: HTTPS
tls:
certificateRefs:
- group: ""
kind: Secret
name: radarr-tls
mode: Terminate
-49
View File
@@ -1,49 +0,0 @@
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: radarr-http-redirect
namespace: arrstack
spec:
hostnames:
- radarr.k8s.syd1.au.unkin.net
parentRefs:
- group: gateway.networking.k8s.io
kind: Gateway
name: radarr
sectionName: http
rules:
- filters:
- type: RequestRedirect
requestRedirect:
scheme: https
statusCode: 301
matches:
- path:
type: PathPrefix
value: /
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: radarr-route
namespace: arrstack
spec:
hostnames:
- radarr.k8s.syd1.au.unkin.net
parentRefs:
- group: gateway.networking.k8s.io
kind: Gateway
name: radarr
sectionName: https
rules:
- backendRefs:
- group: ""
kind: Service
name: radarr
port: 7878
weight: 1
matches:
- path:
type: PathPrefix
value: /
-17
View File
@@ -1,17 +0,0 @@
---
# Radarr /config. RWX on CephFS so all replicas share it (the -unkin2 fork keeps
# the database in Postgres; /config now holds only config.xml + MediaCover, which
# tolerate — and want — shared access). Retain: this is state.
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: radarr-config
namespace: arrstack
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 5Gi
storageClassName: cephfs-raid5-retain
volumeMode: Filesystem
@@ -1,25 +0,0 @@
---
# radarr API key. Seeded at kv/kubernetes/namespace/arrstack/default/radarr
# (key: apitoken); the default k8s role's templated policy already grants read
# on kv/data/kubernetes/namespace/{{sa_namespace}}/{{sa_name}}/* for the
# arrstack/default ServiceAccount, so no terraform-vault change is needed. VSO
# syncs it into the radarr-apikey Secret that the apikey-init initContainer reads
# to enforce <ApiKey> in /config/config.xml (Vault is source of truth).
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultStaticSecret
metadata:
name: radarr-apikey
namespace: arrstack
annotations:
argocd.argoproj.io/sync-wave: "0"
spec:
destination:
create: true
name: radarr-apikey
overwrite: true
hmacSecretData: true
mount: kv
path: kubernetes/namespace/arrstack/default/radarr
refreshAfter: 5m
type: kv-v2
vaultAuthRef: default
@@ -0,0 +1,25 @@
---
# Non-secret env for the -unkin2 fork (adult tier). Identical mechanism to the
# kids sonarr, pointed at its own shared-Postgres database (sonarr-adult-main)
# and its own UrlBase (/3aa168/sonarr) so arrproxy path-routing reaches the adult
# instance separately. Shares the one arrstack Valkey (keys namespaced by the
# fork's sonarr:ratelimit: prefix). User/Password/ApiKey come from Secrets (see
# deployment.yaml), not here.
apiVersion: v1
kind: ConfigMap
metadata:
name: sonarr-adult-env
namespace: arrstack
data:
Sonarr__Postgres__Host: arrstack-postgres-rw.arrstack.svc.cluster.local
Sonarr__Postgres__Port: "5432"
Sonarr__Postgres__MainDb: sonarr-adult-main
Sonarr__Log__DbEnabled: "false"
Sonarr__Auth__Method: External
Sonarr__Auth__Required: DisabledForLocalAddresses
Sonarr__App__InstanceName: Sonarr
Sonarr__Server__Port: "8989"
Sonarr__Server__UrlBase: /3aa168/sonarr
Sonarr__Update__Mechanism: External
Sonarr__Redis__Host: valkey-arrstack-valkey.arrstack.svc.cluster.local
Sonarr__Redis__Port: "6379"
@@ -2,43 +2,40 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: sonarr
name: sonarr-adult
namespace: arrstack
annotations:
# sonarr-env is a plain (unhashed) ConfigMap consumed by fixed-name envFrom,
# so editing it does not roll the Deployment on its own. Reloader watches the
# referenced ConfigMap and triggers a rolling restart on change, so adding the
# Redis env activates the #14 features on the next ArgoCD sync without a manual
# `rollout restart`.
# Reloader rolls the Deployment when sonarr-adult-env changes (plain envFrom
# ConfigMap does not trigger a rollout on its own).
configmap.reloader.stakater.com/auto: "true"
spec:
# Active-active: the -unkin2 fork keeps all state in the shared Postgres
# (arrstack-postgres) and coordinates via Postgres advisory locks, so N
# replicas run concurrently behind the sonarr Service. RollingUpdate is safe —
# no SQLite, no RWO lock.
# replicas run concurrently behind the sonarr-adult Service. RollingUpdate is
# safe — no SQLite, no RWO lock.
replicas: 3
strategy:
type: RollingUpdate
selector:
matchLabels:
app: sonarr
app: sonarr-adult
template:
metadata:
labels:
app: sonarr
app: sonarr-adult
spec:
securityContext:
# Fork image has no USER (runs as root by default); pin it to a non-root
# UID and group-write the shared RWX CephFS /config (MediaCover etc.).
# OnRootMismatch avoids a recursive chown of the whole media tree.
# Fork image has no USER; pin it to a non-root UID and group-write the
# shared RWX CephFS media subtree. OnRootMismatch avoids a recursive
# chown of the whole media tree.
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
fsGroupChangePolicy: OnRootMismatch
initContainers:
# Gate the app on its own Postgres database+role being reachable, instead
# of relying on ArgoCD sync-waves (which deadlock if apps aren't Healthy).
# waitfordb reads the PG* env as a libpq fallback, so the password never lands in argv.
# Gate the app on its own Postgres database+role being reachable.
# waitfordb reads the PG* env as a libpq fallback, so the password never
# lands in argv.
- name: wait-for-db
image: artifactapi.k8s.syd1.au.unkin.net/docker-internal/waitfordb:v0.1.0
env:
@@ -51,16 +48,16 @@ spec:
- name: PGPORT
value: "5432"
- name: PGDATABASE
value: sonarr-main
value: sonarr-adult-main
- name: PGUSER
valueFrom:
secretKeyRef:
name: sonarr-db
name: sonarr-adult-db
key: username
- name: PGPASSWORD
valueFrom:
secretKeyRef:
name: sonarr-db
name: sonarr-adult-db
key: password
resources:
requests:
@@ -78,9 +75,8 @@ spec:
args:
- -nobrowser
- -data=/config
# Required: bypass the single-instance guard so multiple replicas
# can share one /config. Cross-replica safety is the Postgres layer,
# not a local lock file.
# Bypass the single-instance guard so multiple replicas can share one
# /config. Cross-replica safety is the Postgres layer, not a lock file.
- -nosingleinstancecheck
ports:
- name: http
@@ -88,29 +84,25 @@ spec:
protocol: TCP
envFrom:
- configMapRef:
name: sonarr-env
name: sonarr-adult-env
env:
- name: Sonarr__Postgres__User
valueFrom:
secretKeyRef:
name: sonarr-db
name: sonarr-adult-db
key: username
- name: Sonarr__Postgres__Password
valueFrom:
secretKeyRef:
name: sonarr-db
name: sonarr-adult-db
key: password
- name: Sonarr__Auth__ApiKey
valueFrom:
secretKeyRef:
name: sonarr-apikey
name: sonarr-adult-apikey
key: apitoken
# MediaCover object store (shared arrstack-media Ceph RGW bucket,
# partitioned from radarr by the sonarr key prefix). Serves
# posters/fanart from S3 so any replica renders them instead of the
# leader-local emptyDir /config. Consumed by the -unkin3+ image;
# older images ignore these unknown config keys. Creds Secret minted
# by cephrgw-operator.
# partitioned by the sonarr-adult key prefix).
- name: Sonarr__MediaCoverS3__Endpoint
valueFrom:
secretKeyRef:
@@ -129,14 +121,13 @@ spec:
- name: Sonarr__MediaCoverS3__Bucket
value: arrstack-media
- name: Sonarr__MediaCoverS3__Prefix
value: sonarr
value: sonarr-adult
- name: Sonarr__MediaCoverS3__ForcePathStyle
value: "true"
- name: Sonarr__MediaCoverS3__CaCertPath
value: /etc/ssl/vault-ca/ca.crt
# Backup object store (shared arrstack-backups Ceph RGW bucket,
# per-app key prefix). Routes the periodic config+DB zip backups off
# the ephemeral /config so any replica can write and restore them.
# partitioned by the sonarr-adult key prefix).
- name: Sonarr__BackupS3__Endpoint
valueFrom:
secretKeyRef:
@@ -155,14 +146,14 @@ spec:
- name: Sonarr__BackupS3__Bucket
value: arrstack-backups
- name: Sonarr__BackupS3__Prefix
value: sonarr
value: sonarr-adult
- name: Sonarr__BackupS3__ForcePathStyle
value: "true"
- name: Sonarr__BackupS3__CaCertPath
value: /etc/ssl/vault-ca/ca.crt
livenessProbe:
httpGet:
path: /sonarr/ping
path: /3aa168/sonarr/ping
port: http
initialDelaySeconds: 30
periodSeconds: 30
@@ -170,7 +161,7 @@ spec:
failureThreshold: 3
readinessProbe:
httpGet:
path: /sonarr/ping
path: /3aa168/sonarr/ping
port: http
initialDelaySeconds: 10
periodSeconds: 10
@@ -186,28 +177,31 @@ spec:
volumeMounts:
- name: config
mountPath: /config
# Adult TV subtree of the shared media-tv PVC (same CephFS subvolume
# the kids sonarr writes and jellyfin reads).
- name: media-tv
mountPath: /media/tv
subPath: tvshows/adult
- name: vault-ca
mountPath: /etc/ssl/vault-ca
readOnly: true
# exportarr sidecar: polls the local replica's API and exposes Prometheus
# metrics on :9707 (scraped by the sonarr-exportarr VMPodScrape).
# metrics on :9707 (scraped by the sonarr-adult-exportarr VMPodScrape).
- name: exportarr
image: artifactapi.k8s.syd1.au.unkin.net/ghcr/onedr0p/exportarr:v2.3.0
image: ghcr.io/onedr0p/exportarr:v2.3.0
imagePullPolicy: IfNotPresent
args:
- sonarr
env:
- name: PORT
value: "9707"
# URL includes the /sonarr UrlBase (Sonarr__Server__UrlBase).
# URL includes the /3aa168/sonarr UrlBase (Sonarr__Server__UrlBase).
- name: URL
value: http://localhost:8989/sonarr
value: http://localhost:8989/3aa168/sonarr
- name: APIKEY
valueFrom:
secretKeyRef:
name: sonarr-apikey
name: sonarr-adult-apikey
key: apitoken
ports:
- name: metrics
@@ -7,6 +7,4 @@ resources:
- configmap.yaml
- deployment.yaml
- service.yaml
- gateway.yaml
- httproute.yaml
- vmpodscrape.yaml
@@ -2,7 +2,7 @@
apiVersion: v1
kind: Service
metadata:
name: sonarr
name: sonarr-adult
namespace: arrstack
spec:
ports:
@@ -11,5 +11,5 @@ spec:
protocol: TCP
targetPort: http
selector:
app: sonarr
app: sonarr-adult
type: ClusterIP
@@ -0,0 +1,25 @@
---
# sonarr-adult API key. Seeded out-of-band at
# kv/kubernetes/namespace/arrstack/default/sonarr-adult (key: apitoken); the
# default k8s role's templated policy already grants read on
# kv/data/kubernetes/namespace/{{sa_namespace}}/{{sa_name}}/* for the
# arrstack/default ServiceAccount, so no terraform-vault change is needed. VSO
# syncs it into the sonarr-adult-apikey Secret consumed by the Deployment.
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultStaticSecret
metadata:
name: sonarr-adult-apikey
namespace: arrstack
annotations:
argocd.argoproj.io/sync-wave: "0"
spec:
destination:
create: true
name: sonarr-adult-apikey
overwrite: true
hmacSecretData: true
mount: kv
path: kubernetes/namespace/arrstack/default/sonarr-adult
refreshAfter: 5m
type: kv-v2
vaultAuthRef: default
@@ -1,16 +1,16 @@
---
# Scrape the exportarr sidecar (:9707) on every sonarr pod. Picked up by the
# Scrape the exportarr sidecar (:9707) on every sonarr-adult pod. Picked up by the
# observability VMAgent (selectAllByDefault). Pod-level rather than
# VMServiceScrape because the sonarr Service doesn't expose the metrics port.
# VMServiceScrape because the sonarr-adult Service doesn't expose the metrics port.
apiVersion: operator.victoriametrics.com/v1beta1
kind: VMPodScrape
metadata:
name: sonarr-exportarr
name: sonarr-adult-exportarr
namespace: arrstack
spec:
selector:
matchLabels:
app: sonarr
app: sonarr-adult
podMetricsEndpoints:
- port: metrics
path: /metrics
-33
View File
@@ -1,33 +0,0 @@
---
# Non-secret env for the -unkin2 fork. The fork reads Servarr config from
# Sonarr__<Section>__<Key> env (no config.xml edits, no s6/PUID). Postgres wiring
# points every replica at the same shared DB (arrstack-postgres-rw / sonarr-main);
# Auth__Method=External defers UI auth to arrproxy/oauth2-proxy; Server__UrlBase
# keeps the /sonarr prefix so arrproxy path-routing works; App__InstanceName is
# identical across replicas (shared session-cookie name). User/Password/ApiKey
# come from Secrets (see deployment.yaml), not here.
apiVersion: v1
kind: ConfigMap
metadata:
name: sonarr-env
namespace: arrstack
data:
Sonarr__Postgres__Host: arrstack-postgres-rw.arrstack.svc.cluster.local
Sonarr__Postgres__Port: "5432"
Sonarr__Postgres__MainDb: sonarr-main
Sonarr__Log__DbEnabled: "false"
Sonarr__Auth__Method: External
Sonarr__Auth__Required: DisabledForLocalAddresses
Sonarr__App__InstanceName: Sonarr
Sonarr__Server__Port: "8989"
Sonarr__Server__UrlBase: /sonarr
Sonarr__Update__Mechanism: External
# Shared arrstack Valkey (valkey-operator). Setting Host is what activates the
# fork's #14 Redis features (SignalR backplane, cross-replica cache-invalidation
# bus, distributed rate limiter): RedisOptions.IsConfigured gates purely on a
# non-empty Host, so there is no separate Enabled flag. The operator leaves the
# default user passwordless (jellyfin parity), so no Password/Ssl is wired.
# Channels/keys are namespaced by this fork's sonarr:ratelimit: prefix, so the
# one cluster is safe to share with radarr/prowlarr.
Sonarr__Redis__Host: valkey-arrstack-valkey.arrstack.svc.cluster.local
Sonarr__Redis__Port: "6379"
-37
View File
@@ -1,37 +0,0 @@
---
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
labels:
traefik.io/instance: internal
annotations:
cert-manager.io/cluster-issuer: vault-issuer
cert-manager.io/common-name: sonarr.k8s.syd1.au.unkin.net
cert-manager.io/private-key-size: "4096"
external-dns.alpha.kubernetes.io/hostname: sonarr.k8s.syd1.au.unkin.net
external-dns.alpha.kubernetes.io/target: 198.18.200.4
name: sonarr
namespace: arrstack
spec:
gatewayClassName: traefik-internal
listeners:
- allowedRoutes:
namespaces:
from: Same
hostname: sonarr.k8s.syd1.au.unkin.net
name: http
port: 80
protocol: HTTP
- allowedRoutes:
namespaces:
from: Same
hostname: sonarr.k8s.syd1.au.unkin.net
name: https
port: 443
protocol: HTTPS
tls:
certificateRefs:
- group: ""
kind: Secret
name: sonarr-tls
mode: Terminate
-49
View File
@@ -1,49 +0,0 @@
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: sonarr-http-redirect
namespace: arrstack
spec:
hostnames:
- sonarr.k8s.syd1.au.unkin.net
parentRefs:
- group: gateway.networking.k8s.io
kind: Gateway
name: sonarr
sectionName: http
rules:
- filters:
- type: RequestRedirect
requestRedirect:
scheme: https
statusCode: 301
matches:
- path:
type: PathPrefix
value: /
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: sonarr-route
namespace: arrstack
spec:
hostnames:
- sonarr.k8s.syd1.au.unkin.net
parentRefs:
- group: gateway.networking.k8s.io
kind: Gateway
name: sonarr
sectionName: https
rules:
- backendRefs:
- group: ""
kind: Service
name: sonarr
port: 8989
weight: 1
matches:
- path:
type: PathPrefix
value: /
-17
View File
@@ -1,17 +0,0 @@
---
# Sonarr /config. RWX on CephFS so all replicas share it (the -unkin2 fork keeps
# the database in Postgres; /config now holds only config.xml + MediaCover, which
# tolerate — and want — shared access). Retain: this is state.
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: sonarr-config
namespace: arrstack
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 5Gi
storageClassName: cephfs-raid5-retain
volumeMode: Filesystem
@@ -1,25 +0,0 @@
---
# sonarr API key. Seeded at kv/kubernetes/namespace/arrstack/default/sonarr
# (key: apitoken); the default k8s role's templated policy already grants read
# on kv/data/kubernetes/namespace/{{sa_namespace}}/{{sa_name}}/* for the
# arrstack/default ServiceAccount, so no terraform-vault change is needed. VSO
# syncs it into the sonarr-apikey Secret that the apikey-init initContainer reads
# to enforce <ApiKey> in /config/config.xml (Vault is source of truth).
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultStaticSecret
metadata:
name: sonarr-apikey
namespace: arrstack
annotations:
argocd.argoproj.io/sync-wave: "0"
spec:
destination:
create: true
name: sonarr-apikey
overwrite: true
hmacSecretData: true
mount: kv
path: kubernetes/namespace/arrstack/default/sonarr
refreshAfter: 5m
type: kv-v2
vaultAuthRef: default