diff --git a/apps/base/arrstack/kustomization.yaml b/apps/base/arrstack/kustomization.yaml index b60d6f2..5015628 100644 --- a/apps/base/arrstack/kustomization.yaml +++ b/apps/base/arrstack/kustomization.yaml @@ -9,6 +9,7 @@ resources: - pv-media-movies.yaml - pvc-media-tv.yaml - pvc-media-movies.yaml + - postgres - sonarr - radarr - prowlarr diff --git a/apps/base/arrstack/postgres/cnpg_backup.yaml b/apps/base/arrstack/postgres/cnpg_backup.yaml new file mode 100644 index 0000000..fce9a69 --- /dev/null +++ b/apps/base/arrstack/postgres/cnpg_backup.yaml @@ -0,0 +1,45 @@ +--- +# Ceph RGW (S3) backup target for the shared arrstack CNPG cluster, provisioned +# by the in-estate cephrgw-operator: one dedicated bucket + owner user. CNPG +# reads the S3 credential Secret (cnpg-arrstack-backup-s3) from this namespace. +apiVersion: ceph.unkin.net/v1alpha1 +kind: ObjectStoreUser +metadata: + name: cnpg-arrstack-backup + namespace: arrstack +spec: + displayName: "CNPG backup owner (arrstack)" + uid: cnpg-arrstack-backup + maxBuckets: 5 + secretName: cnpg-arrstack-backup-s3 + retainOnDelete: true +--- +apiVersion: ceph.unkin.net/v1alpha1 +kind: Bucket +metadata: + name: cnpg-arrstack + namespace: arrstack +spec: + placementTarget: ec + bucketName: cnpg-arrstack + ownerRef: cnpg-arrstack-backup + versioning: false + tags: + app: arrstack + purpose: cnpg-backup + retainOnDelete: true +--- +# Nightly base backup on top of always-on WAL archiving. Staggered from the +# other CNPG clusters (6-field cron, seconds first). +apiVersion: postgresql.cnpg.io/v1 +kind: ScheduledBackup +metadata: + name: cnpg-arrstack-nightly + namespace: arrstack +spec: + schedule: "0 45 3 * * *" + immediate: false + backupOwnerReference: self + method: barmanObjectStore + cluster: + name: arrstack-postgres diff --git a/apps/base/arrstack/postgres/cnpg_cluster.yaml b/apps/base/arrstack/postgres/cnpg_cluster.yaml new file mode 100644 index 0000000..ebe748c --- /dev/null +++ b/apps/base/arrstack/postgres/cnpg_cluster.yaml @@ -0,0 +1,166 @@ +--- +# Shared PostgreSQL backend for the -unkin2 fork sonarr/radarr/prowlarr, whose +# Npgsql/EF Core provider moves each *arr off SQLite into Postgres and makes the +# shared-nothing, active-active multi-replica deployment possible. One cluster, +# one throwaway initdb owner ("app"), and three managed login roles — one per +# app — each with its own per-app database (see database-*.yaml). Role passwords +# come from the VSO-synced -db Secrets (vaultstaticsecret.yaml), so no +# credential is rendered into git. +apiVersion: postgresql.cnpg.io/v1 +kind: Cluster +metadata: + name: arrstack-postgres + namespace: arrstack + annotations: + # Wave 1: the per-app -db Secrets (wave 0) must exist first — CNPG reads + # them as the managed roles' passwordSecret. ArgoCD gates dependents on the + # Cluster's health status. + argocd.argoproj.io/sync-wave: "1" +spec: + inheritedMetadata: + annotations: + k8up.io/backup: "false" + affinity: + podAntiAffinityType: preferred + backup: + retentionPolicy: 30d + barmanObjectStore: + destinationPath: s3://cnpg-arrstack + endpointURL: https://s3.ceph.unkin.net + endpointCA: + name: vault-ca-cert + key: ca.crt + s3Credentials: + accessKeyId: + name: cnpg-arrstack-backup-s3 + key: AWS_ACCESS_KEY_ID + secretAccessKey: + name: cnpg-arrstack-backup-s3 + key: AWS_SECRET_ACCESS_KEY + serverName: arrstack + data: + compression: bzip2 + jobs: 2 + wal: + compression: zstd + maxParallel: 2 + bootstrap: + initdb: + # Throwaway owner + database: the real per-app databases are provisioned by + # the CNPG Database CRDs, owned by the managed roles below. + database: app + encoding: UTF8 + localeCType: C + localeCollate: C + 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) + login: true + superuser: false + createdb: false + createrole: false + inherit: true + replication: false + connectionLimit: -1 + passwordSecret: + name: prowlarr-db + enablePDB: true + enableSuperuserAccess: false + failoverDelay: 0 + imageName: ghcr.io/cloudnative-pg/postgresql:17-system-trixie + instances: 3 + logLevel: info + maxSyncReplicas: 0 + minSyncReplicas: 0 + monitoring: + customQueriesConfigMap: + - key: queries + name: cnpg-default-monitoring + disableDefaultQueries: false + enablePodMonitor: false + postgresql: + parameters: + archive_mode: "on" + archive_timeout: 5min + dynamic_shared_memory_type: posix + effective_cache_size: 256MB + full_page_writes: "on" + log_destination: csvlog + log_directory: /controller/log + log_filename: postgres + log_rotation_age: "0" + log_rotation_size: "0" + log_truncate_on_rotation: "false" + logging_collector: "on" + max_connections: "200" + max_parallel_workers: "16" + max_replication_slots: "16" + max_worker_processes: "16" + shared_buffers: 128MB + shared_memory_type: mmap + ssl_max_protocol_version: TLSv1.3 + ssl_min_protocol_version: TLSv1.3 + wal_keep_size: 256MB + wal_level: logical + wal_log_hints: "on" + wal_receiver_timeout: 5s + wal_sender_timeout: 5s + syncReplicaElectionConstraint: + enabled: false + primaryUpdateMethod: restart + primaryUpdateStrategy: unsupervised + probes: + liveness: + isolationCheck: + connectionTimeout: 1000 + enabled: true + requestTimeout: 1000 + replicationSlots: + highAvailability: + enabled: true + slotPrefix: _cnpg_ + synchronizeReplicas: + enabled: true + updateInterval: 30 + resources: + limits: + cpu: "1" + memory: 1Gi + requests: + cpu: 50m + memory: 512Mi + smartShutdownTimeout: 180 + startDelay: 3600 + stopDelay: 1800 + storage: + resizeInUseVolumes: true + size: 10Gi + storageClass: cephrbd-fast-delete + switchoverDelay: 3600 diff --git a/apps/base/arrstack/postgres/database-prowlarr.yaml b/apps/base/arrstack/postgres/database-prowlarr.yaml new file mode 100644 index 0000000..2dbd313 --- /dev/null +++ b/apps/base/arrstack/postgres/database-prowlarr.yaml @@ -0,0 +1,17 @@ +--- +# Per-app database owned by the prowlarr 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: prowlarr-main + namespace: arrstack + annotations: + argocd.argoproj.io/sync-wave: "2" +spec: + cluster: + name: arrstack-postgres + name: prowlarr-main + owner: prowlarr + databaseReclaimPolicy: retain diff --git a/apps/base/arrstack/postgres/database-radarr.yaml b/apps/base/arrstack/postgres/database-radarr.yaml new file mode 100644 index 0000000..56164fd --- /dev/null +++ b/apps/base/arrstack/postgres/database-radarr.yaml @@ -0,0 +1,17 @@ +--- +# 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 + annotations: + argocd.argoproj.io/sync-wave: "2" +spec: + cluster: + name: arrstack-postgres + name: radarr-main + owner: radarr + databaseReclaimPolicy: retain diff --git a/apps/base/arrstack/postgres/database-sonarr.yaml b/apps/base/arrstack/postgres/database-sonarr.yaml new file mode 100644 index 0000000..57a7258 --- /dev/null +++ b/apps/base/arrstack/postgres/database-sonarr.yaml @@ -0,0 +1,17 @@ +--- +# 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 + annotations: + argocd.argoproj.io/sync-wave: "2" +spec: + cluster: + name: arrstack-postgres + name: sonarr-main + owner: sonarr + databaseReclaimPolicy: retain diff --git a/apps/base/arrstack/postgres/kustomization.yaml b/apps/base/arrstack/postgres/kustomization.yaml new file mode 100644 index 0000000..d9a56ee --- /dev/null +++ b/apps/base/arrstack/postgres/kustomization.yaml @@ -0,0 +1,11 @@ +--- +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: + - vaultstaticsecret.yaml + - cnpg_cluster.yaml + - cnpg_backup.yaml + - database-sonarr.yaml + - database-radarr.yaml + - database-prowlarr.yaml diff --git a/apps/base/arrstack/postgres/vaultstaticsecret.yaml b/apps/base/arrstack/postgres/vaultstaticsecret.yaml new file mode 100644 index 0000000..4f5c147 --- /dev/null +++ b/apps/base/arrstack/postgres/vaultstaticsecret.yaml @@ -0,0 +1,66 @@ +--- +# Per-app Postgres role credentials. Each is seeded out-of-band at +# kv/kubernetes/namespace/arrstack/default/-db (keys: username, password); +# 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 each into the -db Secret, which is both the CNPG managed role's +# passwordSecret (cnpg_cluster.yaml) and the source of the app Deployment's +# __Postgres__User/__Password env. Wave 0: must exist before the Cluster +# (wave 1) reconciles the roles. +apiVersion: secrets.hashicorp.com/v1beta1 +kind: VaultStaticSecret +metadata: + name: sonarr-db + namespace: arrstack + annotations: + argocd.argoproj.io/sync-wave: "0" +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 + annotations: + argocd.argoproj.io/sync-wave: "0" +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 + annotations: + argocd.argoproj.io/sync-wave: "0" +spec: + destination: + create: true + name: prowlarr-db + overwrite: true + hmacSecretData: true + mount: kv + path: kubernetes/namespace/arrstack/default/prowlarr-db + refreshAfter: 5m + type: kv-v2 + vaultAuthRef: default diff --git a/apps/base/arrstack/prowlarr/configmap.yaml b/apps/base/arrstack/prowlarr/configmap.yaml new file mode 100644 index 0000000..353c02b --- /dev/null +++ b/apps/base/arrstack/prowlarr/configmap.yaml @@ -0,0 +1,24 @@ +--- +# Non-secret env for the -unkin2 fork. The fork reads Servarr config from +# Prowlarr__
__ env (no config.xml edits, no s6/PUID). Postgres +# wiring points every replica at the same shared DB (arrstack-postgres-rw / +# prowlarr-main); Auth__Method=External defers UI auth to arrproxy/oauth2-proxy; +# Server__UrlBase keeps the /prowlarr 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: prowlarr-env + namespace: arrstack +data: + Prowlarr__Postgres__Host: arrstack-postgres-rw.arrstack.svc.cluster.local + Prowlarr__Postgres__Port: "5432" + Prowlarr__Postgres__MainDb: prowlarr-main + Prowlarr__Log__DbEnabled: "false" + Prowlarr__Auth__Method: External + Prowlarr__Auth__Required: DisabledForLocalAddresses + Prowlarr__App__InstanceName: Prowlarr + Prowlarr__Server__Port: "9696" + Prowlarr__Server__UrlBase: /prowlarr + Prowlarr__Update__Mechanism: External diff --git a/apps/base/arrstack/prowlarr/deployment.yaml b/apps/base/arrstack/prowlarr/deployment.yaml index 0987f90..5f24632 100644 --- a/apps/base/arrstack/prowlarr/deployment.yaml +++ b/apps/base/arrstack/prowlarr/deployment.yaml @@ -5,10 +5,13 @@ metadata: name: prowlarr namespace: arrstack spec: - replicas: 1 + # 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 prowlarr Service. RollingUpdate is safe + # — no SQLite, no RWO lock. + replicas: 3 strategy: - # RWO config PVC + single stateful SQLite DB: never run two pods at once. - type: Recreate + type: RollingUpdate selector: matchLabels: app: prowlarr @@ -18,96 +21,49 @@ spec: app: prowlarr 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. OnRootMismatch + # avoids a recursive chown of the whole volume. + runAsUser: 1000 + runAsGroup: 1000 fsGroup: 1000 fsGroupChangePolicy: OnRootMismatch - initContainers: - # Enforce the Vault-sourced API key and the reverse-proxy URL base in - # /config/config.xml before the app starts. Vault is source of truth - # (override bootstrap): the key is minted in Vault, synced by VSO into the - # prowlarr-apikey Secret, and written here. UrlBase=/prowlarr lets arrproxy - # forward arrstack.unkin.net/prowlarr/... with the prefix preserved (no 307). - # Runs as root to fix ownership; touches only , , and - # =External + . External makes - # the *arr defer UI login to arrproxy/oauth2-proxy (same no-auth request - # handler as None, but permits remote access without prompting). - - name: apikey-init - image: artifactapi.k8s.syd1.au.unkin.net/dockerhub/library/busybox:1.37.0 - imagePullPolicy: IfNotPresent - securityContext: - runAsUser: 0 - env: - - name: API_KEY - valueFrom: - secretKeyRef: - name: prowlarr-apikey - key: apitoken - command: - - sh - - -c - - | - set -eu - case "$API_KEY" in - "" | *[!0-9a-fA-F]*) - echo "config-init: API_KEY missing or not hex; refusing" >&2 - exit 1 - ;; - esac - URL_BASE=/prowlarr - AUTH_METHOD=External - AUTH_REQUIRED=Enabled - CFG=/config/config.xml - if [ ! -f "$CFG" ]; then - printf '\n %s\n %s\n %s\n %s\n\n' "$API_KEY" "$URL_BASE" "$AUTH_METHOD" "$AUTH_REQUIRED" > "$CFG" - else - if grep -q '' "$CFG"; then - sed -i "s|[^<]*|${API_KEY}|" "$CFG" - else - sed -i "s||\n ${API_KEY}|" "$CFG" - fi - if grep -q '' "$CFG"; then - sed -i "s|[^<]*|${URL_BASE}|" "$CFG" - else - sed -i "s||\n ${URL_BASE}|" "$CFG" - fi - if grep -q '' "$CFG"; then - sed -i "s|[^<]*|${AUTH_METHOD}|" "$CFG" - else - sed -i "s||\n ${AUTH_METHOD}|" "$CFG" - fi - if grep -q '' "$CFG"; then - sed -i "s|[^<]*|${AUTH_REQUIRED}|" "$CFG" - else - sed -i "s||\n ${AUTH_REQUIRED}|" "$CFG" - fi - fi - chown 1000:1000 "$CFG" - chmod 600 "$CFG" - echo "config-init: , =${URL_BASE}, =${AUTH_METHOD}, =${AUTH_REQUIRED} enforced from Vault" - resources: - requests: - cpu: 50m - memory: 32Mi - limits: - cpu: 200m - memory: 64Mi - volumeMounts: - - name: config - mountPath: /config containers: - name: prowlarr - image: artifactapi.k8s.syd1.au.unkin.net/dockerhub/linuxserver/prowlarr:2.5.2 + image: artifactapi.k8s.syd1.au.unkin.net/docker-internal/prowlarr:v2.6.2-unkin2 imagePullPolicy: IfNotPresent + command: + - /app/Prowlarr + 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. + - -nosingleinstancecheck ports: - name: http containerPort: 9696 protocol: TCP + envFrom: + - configMapRef: + name: prowlarr-env env: - - name: PUID - value: "1000" - - name: PGID - value: "1000" - - name: TZ - value: Australia/Sydney + - name: Prowlarr__Postgres__User + valueFrom: + secretKeyRef: + name: prowlarr-db + key: username + - name: Prowlarr__Postgres__Password + valueFrom: + secretKeyRef: + name: prowlarr-db + key: password + - name: Prowlarr__Auth__ApiKey + valueFrom: + secretKeyRef: + name: prowlarr-apikey + key: apitoken livenessProbe: httpGet: path: /prowlarr/ping @@ -130,7 +86,7 @@ spec: memory: 256Mi limits: cpu: "1" - memory: 512Mi + memory: 1Gi volumeMounts: - name: config mountPath: /config diff --git a/apps/base/arrstack/prowlarr/kustomization.yaml b/apps/base/arrstack/prowlarr/kustomization.yaml index 887f44d..b1d64b3 100644 --- a/apps/base/arrstack/prowlarr/kustomization.yaml +++ b/apps/base/arrstack/prowlarr/kustomization.yaml @@ -5,6 +5,7 @@ kind: Kustomization resources: - pvc-config.yaml - vaultstaticsecret.yaml + - configmap.yaml - deployment.yaml - service.yaml - gateway.yaml diff --git a/apps/base/arrstack/prowlarr/pvc-config.yaml b/apps/base/arrstack/prowlarr/pvc-config.yaml index 208bd7c..58f9d46 100644 --- a/apps/base/arrstack/prowlarr/pvc-config.yaml +++ b/apps/base/arrstack/prowlarr/pvc-config.yaml @@ -1,6 +1,7 @@ --- -# Prowlarr config + SQLite DB. RWO on cephrbd (block) — the arr apps' SQLite -# does not tolerate CephFS locking. Retain: this is state. +# Prowlarr /config. RWX on CephFS so all replicas share it (the -unkin2 fork +# keeps the database in Postgres; /config now holds only config.xml + assets, +# which tolerate — and want — shared access). Retain: this is state. apiVersion: v1 kind: PersistentVolumeClaim metadata: @@ -8,9 +9,9 @@ metadata: namespace: arrstack spec: accessModes: - - ReadWriteOnce + - ReadWriteMany resources: requests: storage: 5Gi - storageClassName: cephrbd-fast-retain + storageClassName: cephfs-raid5-retain volumeMode: Filesystem diff --git a/apps/base/arrstack/radarr/configmap.yaml b/apps/base/arrstack/radarr/configmap.yaml new file mode 100644 index 0000000..8678ced --- /dev/null +++ b/apps/base/arrstack/radarr/configmap.yaml @@ -0,0 +1,24 @@ +--- +# Non-secret env for the -unkin2 fork. The fork reads Servarr config from +# Radarr__
__ 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 diff --git a/apps/base/arrstack/radarr/deployment.yaml b/apps/base/arrstack/radarr/deployment.yaml index 47bd03a..5a8f133 100644 --- a/apps/base/arrstack/radarr/deployment.yaml +++ b/apps/base/arrstack/radarr/deployment.yaml @@ -5,10 +5,13 @@ metadata: name: radarr namespace: arrstack spec: - replicas: 1 + # 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: 3 strategy: - # RWO config PVC + single stateful SQLite DB: never run two pods at once. - type: Recreate + type: RollingUpdate selector: matchLabels: app: radarr @@ -18,96 +21,49 @@ spec: app: radarr 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. + runAsUser: 1000 + runAsGroup: 1000 fsGroup: 1000 fsGroupChangePolicy: OnRootMismatch - initContainers: - # Enforce the Vault-sourced API key and the reverse-proxy URL base in - # /config/config.xml before the app starts. Vault is source of truth - # (override bootstrap): the key is minted in Vault, synced by VSO into the - # radarr-apikey Secret, and written here. UrlBase=/radarr lets arrproxy - # forward arrstack.unkin.net/radarr/... with the prefix preserved (no 307). - # Runs as root to fix ownership; touches only , , and - # =External + . External makes - # the *arr defer UI login to arrproxy/oauth2-proxy (same no-auth request - # handler as None, but permits remote access without prompting). - - name: apikey-init - image: artifactapi.k8s.syd1.au.unkin.net/dockerhub/library/busybox:1.37.0 - imagePullPolicy: IfNotPresent - securityContext: - runAsUser: 0 - env: - - name: API_KEY - valueFrom: - secretKeyRef: - name: radarr-apikey - key: apitoken - command: - - sh - - -c - - | - set -eu - case "$API_KEY" in - "" | *[!0-9a-fA-F]*) - echo "config-init: API_KEY missing or not hex; refusing" >&2 - exit 1 - ;; - esac - URL_BASE=/radarr - AUTH_METHOD=External - AUTH_REQUIRED=Enabled - CFG=/config/config.xml - if [ ! -f "$CFG" ]; then - printf '\n %s\n %s\n %s\n %s\n\n' "$API_KEY" "$URL_BASE" "$AUTH_METHOD" "$AUTH_REQUIRED" > "$CFG" - else - if grep -q '' "$CFG"; then - sed -i "s|[^<]*|${API_KEY}|" "$CFG" - else - sed -i "s||\n ${API_KEY}|" "$CFG" - fi - if grep -q '' "$CFG"; then - sed -i "s|[^<]*|${URL_BASE}|" "$CFG" - else - sed -i "s||\n ${URL_BASE}|" "$CFG" - fi - if grep -q '' "$CFG"; then - sed -i "s|[^<]*|${AUTH_METHOD}|" "$CFG" - else - sed -i "s||\n ${AUTH_METHOD}|" "$CFG" - fi - if grep -q '' "$CFG"; then - sed -i "s|[^<]*|${AUTH_REQUIRED}|" "$CFG" - else - sed -i "s||\n ${AUTH_REQUIRED}|" "$CFG" - fi - fi - chown 1000:1000 "$CFG" - chmod 600 "$CFG" - echo "config-init: , =${URL_BASE}, =${AUTH_METHOD}, =${AUTH_REQUIRED} enforced from Vault" - resources: - requests: - cpu: 50m - memory: 32Mi - limits: - cpu: 200m - memory: 64Mi - volumeMounts: - - name: config - mountPath: /config containers: - name: radarr - image: artifactapi.k8s.syd1.au.unkin.net/dockerhub/linuxserver/radarr:6.3.0 + image: artifactapi.k8s.syd1.au.unkin.net/docker-internal/radarr:v6.4.2-unkin2 imagePullPolicy: IfNotPresent + command: + - /app/Radarr + 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. + - -nosingleinstancecheck ports: - name: http containerPort: 7878 protocol: TCP + envFrom: + - configMapRef: + name: radarr-env env: - - name: PUID - value: "1000" - - name: PGID - value: "1000" - - name: TZ - value: Australia/Sydney + - name: Radarr__Postgres__User + valueFrom: + secretKeyRef: + name: radarr-db + key: username + - name: Radarr__Postgres__Password + valueFrom: + secretKeyRef: + name: radarr-db + key: password + - name: Radarr__Auth__ApiKey + valueFrom: + secretKeyRef: + name: radarr-apikey + key: apitoken livenessProbe: httpGet: path: /radarr/ping diff --git a/apps/base/arrstack/radarr/kustomization.yaml b/apps/base/arrstack/radarr/kustomization.yaml index 887f44d..b1d64b3 100644 --- a/apps/base/arrstack/radarr/kustomization.yaml +++ b/apps/base/arrstack/radarr/kustomization.yaml @@ -5,6 +5,7 @@ kind: Kustomization resources: - pvc-config.yaml - vaultstaticsecret.yaml + - configmap.yaml - deployment.yaml - service.yaml - gateway.yaml diff --git a/apps/base/arrstack/radarr/pvc-config.yaml b/apps/base/arrstack/radarr/pvc-config.yaml index 25fadad..b5a2b1e 100644 --- a/apps/base/arrstack/radarr/pvc-config.yaml +++ b/apps/base/arrstack/radarr/pvc-config.yaml @@ -1,6 +1,7 @@ --- -# Radarr config + SQLite DB. RWO on cephrbd (block) — the arr apps' SQLite -# does not tolerate CephFS locking. Retain: this is state. +# 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: @@ -8,9 +9,9 @@ metadata: namespace: arrstack spec: accessModes: - - ReadWriteOnce + - ReadWriteMany resources: requests: storage: 5Gi - storageClassName: cephrbd-fast-retain + storageClassName: cephfs-raid5-retain volumeMode: Filesystem diff --git a/apps/base/arrstack/sonarr/configmap.yaml b/apps/base/arrstack/sonarr/configmap.yaml new file mode 100644 index 0000000..e837b0e --- /dev/null +++ b/apps/base/arrstack/sonarr/configmap.yaml @@ -0,0 +1,24 @@ +--- +# Non-secret env for the -unkin2 fork. The fork reads Servarr config from +# Sonarr__
__ 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 diff --git a/apps/base/arrstack/sonarr/deployment.yaml b/apps/base/arrstack/sonarr/deployment.yaml index 9ce95a6..4be6748 100644 --- a/apps/base/arrstack/sonarr/deployment.yaml +++ b/apps/base/arrstack/sonarr/deployment.yaml @@ -5,10 +5,13 @@ metadata: name: sonarr namespace: arrstack spec: - replicas: 1 + # 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: 3 strategy: - # RWO config PVC + single stateful SQLite DB: never run two pods at once. - type: Recreate + type: RollingUpdate selector: matchLabels: app: sonarr @@ -18,99 +21,49 @@ spec: app: sonarr spec: securityContext: - # LinuxServer images init as root via s6 then step down to PUID/PGID. - # fsGroup makes the shared CephFS group-writable for that user; + # 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. + runAsUser: 1000 + runAsGroup: 1000 fsGroup: 1000 fsGroupChangePolicy: OnRootMismatch - initContainers: - # Enforce the Vault-sourced API key and the reverse-proxy URL base in - # /config/config.xml before the app starts. Vault is source of truth - # (override bootstrap): the key is minted in Vault, synced by VSO into the - # sonarr-apikey Secret, and written here. UrlBase=/sonarr lets arrproxy - # forward arrstack.unkin.net/sonarr/... with the prefix preserved (no 307). - # Runs as root to fix ownership; touches only , , and - # =External + . External makes - # the *arr defer UI login to arrproxy/oauth2-proxy (same no-auth request - # handler as None, but permits remote access without prompting). - - name: apikey-init - image: artifactapi.k8s.syd1.au.unkin.net/dockerhub/library/busybox:1.37.0 - imagePullPolicy: IfNotPresent - securityContext: - runAsUser: 0 - env: - - name: API_KEY - valueFrom: - secretKeyRef: - name: sonarr-apikey - key: apitoken - command: - - sh - - -c - - | - set -eu - case "$API_KEY" in - "" | *[!0-9a-fA-F]*) - echo "config-init: API_KEY missing or not hex; refusing" >&2 - exit 1 - ;; - esac - URL_BASE=/sonarr - AUTH_METHOD=External - AUTH_REQUIRED=Enabled - CFG=/config/config.xml - if [ ! -f "$CFG" ]; then - printf '\n %s\n %s\n %s\n %s\n\n' "$API_KEY" "$URL_BASE" "$AUTH_METHOD" "$AUTH_REQUIRED" > "$CFG" - else - if grep -q '' "$CFG"; then - sed -i "s|[^<]*|${API_KEY}|" "$CFG" - else - sed -i "s||\n ${API_KEY}|" "$CFG" - fi - if grep -q '' "$CFG"; then - sed -i "s|[^<]*|${URL_BASE}|" "$CFG" - else - sed -i "s||\n ${URL_BASE}|" "$CFG" - fi - if grep -q '' "$CFG"; then - sed -i "s|[^<]*|${AUTH_METHOD}|" "$CFG" - else - sed -i "s||\n ${AUTH_METHOD}|" "$CFG" - fi - if grep -q '' "$CFG"; then - sed -i "s|[^<]*|${AUTH_REQUIRED}|" "$CFG" - else - sed -i "s||\n ${AUTH_REQUIRED}|" "$CFG" - fi - fi - chown 1000:1000 "$CFG" - chmod 600 "$CFG" - echo "config-init: , =${URL_BASE}, =${AUTH_METHOD}, =${AUTH_REQUIRED} enforced from Vault" - resources: - requests: - cpu: 50m - memory: 32Mi - limits: - cpu: 200m - memory: 64Mi - volumeMounts: - - name: config - mountPath: /config containers: - name: sonarr - image: artifactapi.k8s.syd1.au.unkin.net/dockerhub/linuxserver/sonarr:4.0.19 + image: artifactapi.k8s.syd1.au.unkin.net/docker-internal/sonarr:v5.0.0-unkin2 imagePullPolicy: IfNotPresent + command: + - /app/Sonarr + 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. + - -nosingleinstancecheck ports: - name: http containerPort: 8989 protocol: TCP + envFrom: + - configMapRef: + name: sonarr-env env: - - name: PUID - value: "1000" - - name: PGID - value: "1000" - - name: TZ - value: Australia/Sydney + - name: Sonarr__Postgres__User + valueFrom: + secretKeyRef: + name: sonarr-db + key: username + - name: Sonarr__Postgres__Password + valueFrom: + secretKeyRef: + name: sonarr-db + key: password + - name: Sonarr__Auth__ApiKey + valueFrom: + secretKeyRef: + name: sonarr-apikey + key: apitoken livenessProbe: httpGet: path: /sonarr/ping diff --git a/apps/base/arrstack/sonarr/kustomization.yaml b/apps/base/arrstack/sonarr/kustomization.yaml index 887f44d..b1d64b3 100644 --- a/apps/base/arrstack/sonarr/kustomization.yaml +++ b/apps/base/arrstack/sonarr/kustomization.yaml @@ -5,6 +5,7 @@ kind: Kustomization resources: - pvc-config.yaml - vaultstaticsecret.yaml + - configmap.yaml - deployment.yaml - service.yaml - gateway.yaml diff --git a/apps/base/arrstack/sonarr/pvc-config.yaml b/apps/base/arrstack/sonarr/pvc-config.yaml index 8bc132c..baa87c7 100644 --- a/apps/base/arrstack/sonarr/pvc-config.yaml +++ b/apps/base/arrstack/sonarr/pvc-config.yaml @@ -1,6 +1,7 @@ --- -# Sonarr config + SQLite DB. RWO on cephrbd (block) — the arr apps' SQLite -# does not tolerate CephFS locking. Retain: this is state. +# 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: @@ -8,9 +9,9 @@ metadata: namespace: arrstack spec: accessModes: - - ReadWriteOnce + - ReadWriteMany resources: requests: storage: 5Gi - storageClassName: cephrbd-fast-retain + storageClassName: cephfs-raid5-retain volumeMode: Filesystem