From 0eddf060c2ec3b975676742d29744d6733f03655 Mon Sep 17 00:00:00 2001 From: unkin-agent Date: Mon, 24 Aug 2026 22:06:22 +1000 Subject: [PATCH] arrstack: add kids backend tier (sonarr/radarr/nzbget-kids) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Split 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 or arrproxy changes in this PR; the adult instances are untouched. - sonarr-kids/radarr-kids: mirror the adult -unkin fork Deployments (same images/tags, 3 active-active replicas), pointed at 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. - nzbget-kids: second downloader on its own RWO config PVC, writing completed downloads into the kids media subtrees for same-filesystem hardlink imports. - postgres: two managed roles + Database CRDs (sonarr-kids/radarr-kids) and their VSO-synced -kids-db credential secrets. - per-app VMPodScrape for each kids exportarr sidecar; register the three new apps in the base kustomization. --- apps/base/arrstack/kustomization.yaml | 3 + .../base/arrstack/nzbget-kids/deployment.yaml | 142 ++++++++++ .../arrstack/nzbget-kids/kustomization.yaml | 8 + .../base/arrstack/nzbget-kids/pvc-config.yaml | 16 ++ apps/base/arrstack/nzbget-kids/service.yaml | 15 ++ apps/base/arrstack/postgres/cnpg_cluster.yaml | 24 ++ .../postgres/database-radarr-kids.yaml | 15 ++ .../postgres/database-sonarr-kids.yaml | 15 ++ .../base/arrstack/postgres/kustomization.yaml | 2 + .../arrstack/postgres/vaultstaticsecret.yaml | 34 +++ apps/base/arrstack/radarr-kids/configmap.yaml | 25 ++ .../base/arrstack/radarr-kids/deployment.yaml | 245 ++++++++++++++++++ .../arrstack/radarr-kids/kustomization.yaml | 10 + apps/base/arrstack/radarr-kids/service.yaml | 15 ++ .../radarr-kids/vaultstaticsecret.yaml | 25 ++ .../arrstack/radarr-kids/vmpodscrape.yaml | 16 ++ apps/base/arrstack/sonarr-kids/configmap.yaml | 25 ++ .../base/arrstack/sonarr-kids/deployment.yaml | 245 ++++++++++++++++++ .../arrstack/sonarr-kids/kustomization.yaml | 10 + apps/base/arrstack/sonarr-kids/service.yaml | 15 ++ .../sonarr-kids/vaultstaticsecret.yaml | 25 ++ .../arrstack/sonarr-kids/vmpodscrape.yaml | 16 ++ 22 files changed, 946 insertions(+) create mode 100644 apps/base/arrstack/nzbget-kids/deployment.yaml create mode 100644 apps/base/arrstack/nzbget-kids/kustomization.yaml create mode 100644 apps/base/arrstack/nzbget-kids/pvc-config.yaml create mode 100644 apps/base/arrstack/nzbget-kids/service.yaml create mode 100644 apps/base/arrstack/postgres/database-radarr-kids.yaml create mode 100644 apps/base/arrstack/postgres/database-sonarr-kids.yaml create mode 100644 apps/base/arrstack/radarr-kids/configmap.yaml create mode 100644 apps/base/arrstack/radarr-kids/deployment.yaml create mode 100644 apps/base/arrstack/radarr-kids/kustomization.yaml create mode 100644 apps/base/arrstack/radarr-kids/service.yaml create mode 100644 apps/base/arrstack/radarr-kids/vaultstaticsecret.yaml create mode 100644 apps/base/arrstack/radarr-kids/vmpodscrape.yaml create mode 100644 apps/base/arrstack/sonarr-kids/configmap.yaml create mode 100644 apps/base/arrstack/sonarr-kids/deployment.yaml create mode 100644 apps/base/arrstack/sonarr-kids/kustomization.yaml create mode 100644 apps/base/arrstack/sonarr-kids/service.yaml create mode 100644 apps/base/arrstack/sonarr-kids/vaultstaticsecret.yaml create mode 100644 apps/base/arrstack/sonarr-kids/vmpodscrape.yaml diff --git a/apps/base/arrstack/kustomization.yaml b/apps/base/arrstack/kustomization.yaml index 2798a6b..e14ec46 100644 --- a/apps/base/arrstack/kustomization.yaml +++ b/apps/base/arrstack/kustomization.yaml @@ -19,5 +19,8 @@ resources: - radarr - prowlarr - nzbget + - sonarr-kids + - radarr-kids + - nzbget-kids - arrproxy - mediamover diff --git a/apps/base/arrstack/nzbget-kids/deployment.yaml b/apps/base/arrstack/nzbget-kids/deployment.yaml new file mode 100644 index 0000000..ba232c3 --- /dev/null +++ b/apps/base/arrstack/nzbget-kids/deployment.yaml @@ -0,0 +1,142 @@ +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nzbget-kids + namespace: arrstack +spec: + replicas: 1 + strategy: + # RWO config PVC + single queue state: never run two pods at once. + type: Recreate + selector: + matchLabels: + app: nzbget-kids + template: + metadata: + labels: + app: nzbget-kids + spec: + securityContext: + fsGroup: 1000 + fsGroupChangePolicy: OnRootMismatch + initContainers: + # Seed download layout onto the kids media subtrees (not /config or an + # emptyDir) so completed kids downloads land beside the kids arr libraries + # and imports are same-filesystem hardlink moves. The media mounts use the + # tvshows/kids and movies/kids subPaths, so the in-container paths match + # the adult nzbget while the data stays scoped to the kids 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 + command: + - sh + - -c + - | + set -e + if [ ! -f /config/nzbget.conf ]; then + cp /app/nzbget/share/nzbget/nzbget.conf /config/nzbget.conf + fi + if ! grep -q '# arrstack-managed' /config/nzbget.conf; then + 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-kids/ + # radarr-kids 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 + NzbDir=/config/nzb + QueueDir=/config/queue + TempDir=/config/tmp + ControlIP=0.0.0.0 + ControlPort=6789 + Category1.Name=tv + Category1.DestDir=/media/tv/downloads + Category2.Name=movies + Category2.DestDir=/media/movies/downloads + CONF + fi + mkdir -p /media/tv/series /media/tv/downloads /media/movies/films /media/movies/downloads + chown 1000:1000 /config/nzbget.conf \ + /media/tv /media/tv/series /media/tv/downloads \ + /media/movies /media/movies/films /media/movies/downloads + resources: + requests: + cpu: 10m + memory: 32Mi + limits: + cpu: 200m + memory: 128Mi + volumeMounts: + - name: config + mountPath: /config + - name: media-tv + mountPath: /media/tv + subPath: tvshows/kids + - name: media-movies + mountPath: /media/movies + subPath: movies/kids + containers: + - name: nzbget + image: artifactapi.k8s.syd1.au.unkin.net/dockerhub/linuxserver/nzbget:version-v26.2 + imagePullPolicy: IfNotPresent + ports: + - name: http + containerPort: 6789 + protocol: TCP + env: + - name: PUID + value: "1000" + - name: PGID + value: "1000" + - name: TZ + value: Australia/Sydney + livenessProbe: + # nzbget's root path requires auth (401); a TCP check is the + # dependency-free liveness signal for the web/JSON-RPC server. + tcpSocket: + port: http + initialDelaySeconds: 30 + periodSeconds: 30 + timeoutSeconds: 5 + failureThreshold: 3 + readinessProbe: + tcpSocket: + port: http + initialDelaySeconds: 10 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 3 + resources: + requests: + cpu: 200m + memory: 256Mi + limits: + # Headroom for par2 repair + unpack of large downloads. + cpu: "2" + memory: 2Gi + volumeMounts: + - name: config + mountPath: /config + - name: media-tv + mountPath: /media/tv + subPath: tvshows/kids + - name: media-movies + mountPath: /media/movies + subPath: movies/kids + volumes: + - name: config + persistentVolumeClaim: + claimName: nzbget-kids-config + - name: media-tv + persistentVolumeClaim: + claimName: media-tv + - name: media-movies + persistentVolumeClaim: + claimName: media-movies diff --git a/apps/base/arrstack/nzbget-kids/kustomization.yaml b/apps/base/arrstack/nzbget-kids/kustomization.yaml new file mode 100644 index 0000000..ed46412 --- /dev/null +++ b/apps/base/arrstack/nzbget-kids/kustomization.yaml @@ -0,0 +1,8 @@ +--- +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: + - pvc-config.yaml + - deployment.yaml + - service.yaml diff --git a/apps/base/arrstack/nzbget-kids/pvc-config.yaml b/apps/base/arrstack/nzbget-kids/pvc-config.yaml new file mode 100644 index 0000000..c154c18 --- /dev/null +++ b/apps/base/arrstack/nzbget-kids/pvc-config.yaml @@ -0,0 +1,16 @@ +--- +# NZBGet (kids) 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-kids-config + namespace: arrstack +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 5Gi + storageClassName: cephrbd-fast-retain + volumeMode: Filesystem diff --git a/apps/base/arrstack/nzbget-kids/service.yaml b/apps/base/arrstack/nzbget-kids/service.yaml new file mode 100644 index 0000000..ae52da4 --- /dev/null +++ b/apps/base/arrstack/nzbget-kids/service.yaml @@ -0,0 +1,15 @@ +--- +apiVersion: v1 +kind: Service +metadata: + name: nzbget-kids + namespace: arrstack +spec: + ports: + - name: http + port: 6789 + protocol: TCP + targetPort: http + selector: + app: nzbget-kids + type: ClusterIP diff --git a/apps/base/arrstack/postgres/cnpg_cluster.yaml b/apps/base/arrstack/postgres/cnpg_cluster.yaml index 5d9fd62..0240dbe 100644 --- a/apps/base/arrstack/postgres/cnpg_cluster.yaml +++ b/apps/base/arrstack/postgres/cnpg_cluster.yaml @@ -86,6 +86,30 @@ spec: connectionLimit: -1 passwordSecret: name: prowlarr-db + - name: sonarr-kids + ensure: present + comment: Sonarr (kids) application role (owns sonarr-kids-main) + login: true + superuser: false + createdb: false + createrole: false + inherit: true + replication: false + connectionLimit: -1 + passwordSecret: + name: sonarr-kids-db + - name: radarr-kids + ensure: present + comment: Radarr (kids) application role (owns radarr-kids-main) + login: true + superuser: false + createdb: false + createrole: false + inherit: true + replication: false + connectionLimit: -1 + passwordSecret: + name: radarr-kids-db enablePDB: true enableSuperuserAccess: false failoverDelay: 0 diff --git a/apps/base/arrstack/postgres/database-radarr-kids.yaml b/apps/base/arrstack/postgres/database-radarr-kids.yaml new file mode 100644 index 0000000..3312d19 --- /dev/null +++ b/apps/base/arrstack/postgres/database-radarr-kids.yaml @@ -0,0 +1,15 @@ +--- +# Per-app database owned by the radarr-kids 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-kids-main + namespace: arrstack +spec: + cluster: + name: arrstack-postgres + name: radarr-kids-main + owner: radarr-kids + databaseReclaimPolicy: retain diff --git a/apps/base/arrstack/postgres/database-sonarr-kids.yaml b/apps/base/arrstack/postgres/database-sonarr-kids.yaml new file mode 100644 index 0000000..d421322 --- /dev/null +++ b/apps/base/arrstack/postgres/database-sonarr-kids.yaml @@ -0,0 +1,15 @@ +--- +# Per-app database owned by the sonarr-kids 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-kids-main + namespace: arrstack +spec: + cluster: + name: arrstack-postgres + name: sonarr-kids-main + owner: sonarr-kids + databaseReclaimPolicy: retain diff --git a/apps/base/arrstack/postgres/kustomization.yaml b/apps/base/arrstack/postgres/kustomization.yaml index d9a56ee..13dbd95 100644 --- a/apps/base/arrstack/postgres/kustomization.yaml +++ b/apps/base/arrstack/postgres/kustomization.yaml @@ -9,3 +9,5 @@ resources: - database-sonarr.yaml - database-radarr.yaml - database-prowlarr.yaml + - database-sonarr-kids.yaml + - database-radarr-kids.yaml diff --git a/apps/base/arrstack/postgres/vaultstaticsecret.yaml b/apps/base/arrstack/postgres/vaultstaticsecret.yaml index 101a6ad..bfab34b 100644 --- a/apps/base/arrstack/postgres/vaultstaticsecret.yaml +++ b/apps/base/arrstack/postgres/vaultstaticsecret.yaml @@ -58,3 +58,37 @@ spec: refreshAfter: 5m type: kv-v2 vaultAuthRef: default +--- +apiVersion: secrets.hashicorp.com/v1beta1 +kind: VaultStaticSecret +metadata: + name: sonarr-kids-db + namespace: arrstack +spec: + destination: + create: true + name: sonarr-kids-db + overwrite: true + hmacSecretData: true + mount: kv + path: kubernetes/namespace/arrstack/default/sonarr-kids-db + refreshAfter: 5m + type: kv-v2 + vaultAuthRef: default +--- +apiVersion: secrets.hashicorp.com/v1beta1 +kind: VaultStaticSecret +metadata: + name: radarr-kids-db + namespace: arrstack +spec: + destination: + create: true + name: radarr-kids-db + overwrite: true + hmacSecretData: true + mount: kv + path: kubernetes/namespace/arrstack/default/radarr-kids-db + refreshAfter: 5m + type: kv-v2 + vaultAuthRef: default diff --git a/apps/base/arrstack/radarr-kids/configmap.yaml b/apps/base/arrstack/radarr-kids/configmap.yaml new file mode 100644 index 0000000..74fcff9 --- /dev/null +++ b/apps/base/arrstack/radarr-kids/configmap.yaml @@ -0,0 +1,25 @@ +--- +# Non-secret env for the -unkin2 fork (kids tier). Identical mechanism to the +# adult radarr, pointed at its own shared-Postgres database (radarr-kids-main) +# and its own UrlBase (/3df803/radarr) so arrproxy path-routing reaches the kids +# 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-kids-env + namespace: arrstack +data: + Radarr__Postgres__Host: arrstack-postgres-rw.arrstack.svc.cluster.local + Radarr__Postgres__Port: "5432" + Radarr__Postgres__MainDb: radarr-kids-main + Radarr__Log__DbEnabled: "false" + Radarr__Auth__Method: External + Radarr__Auth__Required: DisabledForLocalAddresses + Radarr__App__InstanceName: Radarr + Radarr__Server__Port: "7878" + Radarr__Server__UrlBase: /3df803/radarr + Radarr__Update__Mechanism: External + Radarr__Redis__Host: valkey-arrstack-valkey.arrstack.svc.cluster.local + Radarr__Redis__Port: "6379" diff --git a/apps/base/arrstack/radarr-kids/deployment.yaml b/apps/base/arrstack/radarr-kids/deployment.yaml new file mode 100644 index 0000000..a1bb00f --- /dev/null +++ b/apps/base/arrstack/radarr-kids/deployment.yaml @@ -0,0 +1,245 @@ +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: radarr-kids + namespace: arrstack + annotations: + # Reloader rolls the Deployment when radarr-kids-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-kids Service. RollingUpdate is + # safe — no SQLite, no RWO lock. + replicas: 3 + strategy: + type: RollingUpdate + selector: + matchLabels: + app: radarr-kids + template: + metadata: + labels: + app: radarr-kids + spec: + securityContext: + # 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. + # 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: + - name: WAITFORDB_TIMEOUT + value: 5m + - name: WAITFORDB_SSLMODE + value: disable + - name: PGHOST + value: arrstack-postgres-rw.arrstack.svc.cluster.local + - name: PGPORT + value: "5432" + - name: PGDATABASE + value: radarr-kids-main + - name: PGUSER + valueFrom: + secretKeyRef: + name: radarr-kids-db + key: username + - name: PGPASSWORD + valueFrom: + secretKeyRef: + name: radarr-kids-db + key: password + resources: + requests: + cpu: 10m + memory: 32Mi + limits: + cpu: 100m + memory: 64Mi + containers: + - name: radarr + image: artifactapi.k8s.syd1.au.unkin.net/docker-internal/radarr:v6.4.2-unkin7 + imagePullPolicy: IfNotPresent + command: + - /app/Radarr + args: + - -nobrowser + - -data=/config + # 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 + containerPort: 7878 + protocol: TCP + envFrom: + - configMapRef: + name: radarr-kids-env + env: + - name: Radarr__Postgres__User + valueFrom: + secretKeyRef: + name: radarr-kids-db + key: username + - name: Radarr__Postgres__Password + valueFrom: + secretKeyRef: + name: radarr-kids-db + key: password + - name: Radarr__Auth__ApiKey + valueFrom: + secretKeyRef: + name: radarr-kids-apikey + key: apitoken + # MediaCover object store (shared arrstack-media Ceph RGW bucket, + # partitioned by the radarr-kids key prefix). + - name: Radarr__MediaCover__S3__Endpoint + valueFrom: + secretKeyRef: + name: arrstack-media-s3 + key: S3_ENDPOINT + - name: Radarr__MediaCover__S3__AccessKey + valueFrom: + secretKeyRef: + name: arrstack-media-s3 + key: AWS_ACCESS_KEY_ID + - name: Radarr__MediaCover__S3__SecretKey + valueFrom: + secretKeyRef: + name: arrstack-media-s3 + key: AWS_SECRET_ACCESS_KEY + - name: Radarr__MediaCover__S3__Bucket + value: arrstack-media + - name: Radarr__MediaCover__S3__Prefix + value: radarr-kids + - 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, + # partitioned by the radarr-kids key prefix). + - name: Radarr__BackupS3__Endpoint + valueFrom: + secretKeyRef: + name: arrstack-backups-s3 + key: S3_ENDPOINT + - name: Radarr__BackupS3__AccessKey + valueFrom: + secretKeyRef: + name: arrstack-backups-s3 + key: AWS_ACCESS_KEY_ID + - name: Radarr__BackupS3__SecretKey + valueFrom: + secretKeyRef: + name: arrstack-backups-s3 + key: AWS_SECRET_ACCESS_KEY + - name: Radarr__BackupS3__Bucket + value: arrstack-backups + - name: Radarr__BackupS3__Prefix + value: radarr-kids + - name: Radarr__BackupS3__ForcePathStyle + value: "true" + - name: Radarr__BackupS3__CaCertPath + value: /etc/ssl/vault-ca/ca.crt + livenessProbe: + httpGet: + path: /3df803/radarr/ping + port: http + initialDelaySeconds: 30 + periodSeconds: 30 + timeoutSeconds: 5 + failureThreshold: 3 + readinessProbe: + httpGet: + path: /3df803/radarr/ping + port: http + initialDelaySeconds: 10 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 3 + resources: + requests: + cpu: 100m + memory: 256Mi + limits: + cpu: "1" + memory: 1Gi + volumeMounts: + - name: config + mountPath: /config + # Kids movies subtree of the shared media-movies PVC (same CephFS + # subvolume the adult radarr writes and jellyfin reads). + - name: media-movies + mountPath: /media/movies + subPath: movies/kids + - 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-kids-exportarr VMPodScrape). + - name: exportarr + image: artifactapi.k8s.syd1.au.unkin.net/ghcr/onedr0p/exportarr:v2.3.0 + imagePullPolicy: IfNotPresent + args: + - radarr + env: + - name: PORT + value: "9708" + # URL includes the /3df803/radarr UrlBase (Radarr__Server__UrlBase). + - name: URL + value: http://localhost:7878/3df803/radarr + - name: APIKEY + valueFrom: + secretKeyRef: + name: radarr-kids-apikey + key: apitoken + ports: + - name: metrics + containerPort: 9708 + protocol: TCP + livenessProbe: + httpGet: + path: /healthz + port: metrics + initialDelaySeconds: 15 + periodSeconds: 30 + timeoutSeconds: 5 + failureThreshold: 3 + readinessProbe: + httpGet: + path: /healthz + port: metrics + initialDelaySeconds: 5 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 3 + resources: + requests: + cpu: 25m + memory: 32Mi + limits: + cpu: 100m + memory: 128Mi + volumes: + - name: config + emptyDir: {} + - name: media-movies + persistentVolumeClaim: + claimName: media-movies + # Estate CA for validating the Ceph RGW (s3.ceph.unkin.net) TLS cert. + - name: vault-ca + secret: + secretName: vault-ca-cert + items: + - key: ca.crt + path: ca.crt diff --git a/apps/base/arrstack/radarr-kids/kustomization.yaml b/apps/base/arrstack/radarr-kids/kustomization.yaml new file mode 100644 index 0000000..5ab2110 --- /dev/null +++ b/apps/base/arrstack/radarr-kids/kustomization.yaml @@ -0,0 +1,10 @@ +--- +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: + - vaultstaticsecret.yaml + - configmap.yaml + - deployment.yaml + - service.yaml + - vmpodscrape.yaml diff --git a/apps/base/arrstack/radarr-kids/service.yaml b/apps/base/arrstack/radarr-kids/service.yaml new file mode 100644 index 0000000..a10a8de --- /dev/null +++ b/apps/base/arrstack/radarr-kids/service.yaml @@ -0,0 +1,15 @@ +--- +apiVersion: v1 +kind: Service +metadata: + name: radarr-kids + namespace: arrstack +spec: + ports: + - name: http + port: 7878 + protocol: TCP + targetPort: http + selector: + app: radarr-kids + type: ClusterIP diff --git a/apps/base/arrstack/radarr-kids/vaultstaticsecret.yaml b/apps/base/arrstack/radarr-kids/vaultstaticsecret.yaml new file mode 100644 index 0000000..d016839 --- /dev/null +++ b/apps/base/arrstack/radarr-kids/vaultstaticsecret.yaml @@ -0,0 +1,25 @@ +--- +# radarr-kids API key. Seeded out-of-band at +# kv/kubernetes/namespace/arrstack/default/radarr-kids (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-kids-apikey Secret consumed by the Deployment. +apiVersion: secrets.hashicorp.com/v1beta1 +kind: VaultStaticSecret +metadata: + name: radarr-kids-apikey + namespace: arrstack + annotations: + argocd.argoproj.io/sync-wave: "0" +spec: + destination: + create: true + name: radarr-kids-apikey + overwrite: true + hmacSecretData: true + mount: kv + path: kubernetes/namespace/arrstack/default/radarr-kids + refreshAfter: 5m + type: kv-v2 + vaultAuthRef: default diff --git a/apps/base/arrstack/radarr-kids/vmpodscrape.yaml b/apps/base/arrstack/radarr-kids/vmpodscrape.yaml new file mode 100644 index 0000000..5a1c07c --- /dev/null +++ b/apps/base/arrstack/radarr-kids/vmpodscrape.yaml @@ -0,0 +1,16 @@ +--- +# Scrape the exportarr sidecar (:9708) on every radarr-kids pod. Picked up by the +# observability VMAgent (selectAllByDefault). Pod-level rather than +# VMServiceScrape because the radarr-kids Service doesn't expose the metrics port. +apiVersion: operator.victoriametrics.com/v1beta1 +kind: VMPodScrape +metadata: + name: radarr-kids-exportarr + namespace: arrstack +spec: + selector: + matchLabels: + app: radarr-kids + podMetricsEndpoints: + - port: metrics + path: /metrics diff --git a/apps/base/arrstack/sonarr-kids/configmap.yaml b/apps/base/arrstack/sonarr-kids/configmap.yaml new file mode 100644 index 0000000..00286a1 --- /dev/null +++ b/apps/base/arrstack/sonarr-kids/configmap.yaml @@ -0,0 +1,25 @@ +--- +# Non-secret env for the -unkin2 fork (kids tier). Identical mechanism to the +# adult sonarr, pointed at its own shared-Postgres database (sonarr-kids-main) +# and its own UrlBase (/3df803/sonarr) so arrproxy path-routing reaches the kids +# 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-kids-env + namespace: arrstack +data: + Sonarr__Postgres__Host: arrstack-postgres-rw.arrstack.svc.cluster.local + Sonarr__Postgres__Port: "5432" + Sonarr__Postgres__MainDb: sonarr-kids-main + Sonarr__Log__DbEnabled: "false" + Sonarr__Auth__Method: External + Sonarr__Auth__Required: DisabledForLocalAddresses + Sonarr__App__InstanceName: Sonarr + Sonarr__Server__Port: "8989" + Sonarr__Server__UrlBase: /3df803/sonarr + Sonarr__Update__Mechanism: External + Sonarr__Redis__Host: valkey-arrstack-valkey.arrstack.svc.cluster.local + Sonarr__Redis__Port: "6379" diff --git a/apps/base/arrstack/sonarr-kids/deployment.yaml b/apps/base/arrstack/sonarr-kids/deployment.yaml new file mode 100644 index 0000000..e979c32 --- /dev/null +++ b/apps/base/arrstack/sonarr-kids/deployment.yaml @@ -0,0 +1,245 @@ +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: sonarr-kids + namespace: arrstack + annotations: + # Reloader rolls the Deployment when sonarr-kids-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-kids Service. RollingUpdate is + # safe — no SQLite, no RWO lock. + replicas: 3 + strategy: + type: RollingUpdate + selector: + matchLabels: + app: sonarr-kids + template: + metadata: + labels: + app: sonarr-kids + spec: + securityContext: + # 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. + # 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: + - name: WAITFORDB_TIMEOUT + value: 5m + - name: WAITFORDB_SSLMODE + value: disable + - name: PGHOST + value: arrstack-postgres-rw.arrstack.svc.cluster.local + - name: PGPORT + value: "5432" + - name: PGDATABASE + value: sonarr-kids-main + - name: PGUSER + valueFrom: + secretKeyRef: + name: sonarr-kids-db + key: username + - name: PGPASSWORD + valueFrom: + secretKeyRef: + name: sonarr-kids-db + key: password + resources: + requests: + cpu: 10m + memory: 32Mi + limits: + cpu: 100m + memory: 64Mi + containers: + - name: sonarr + image: artifactapi.k8s.syd1.au.unkin.net/docker-internal/sonarr:v5.0.0-unkin6 + imagePullPolicy: IfNotPresent + command: + - /app/Sonarr + args: + - -nobrowser + - -data=/config + # 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 + containerPort: 8989 + protocol: TCP + envFrom: + - configMapRef: + name: sonarr-kids-env + env: + - name: Sonarr__Postgres__User + valueFrom: + secretKeyRef: + name: sonarr-kids-db + key: username + - name: Sonarr__Postgres__Password + valueFrom: + secretKeyRef: + name: sonarr-kids-db + key: password + - name: Sonarr__Auth__ApiKey + valueFrom: + secretKeyRef: + name: sonarr-kids-apikey + key: apitoken + # MediaCover object store (shared arrstack-media Ceph RGW bucket, + # partitioned by the sonarr-kids key prefix). + - name: Sonarr__MediaCoverS3__Endpoint + valueFrom: + secretKeyRef: + name: arrstack-media-s3 + key: S3_ENDPOINT + - name: Sonarr__MediaCoverS3__AccessKey + valueFrom: + secretKeyRef: + name: arrstack-media-s3 + key: AWS_ACCESS_KEY_ID + - name: Sonarr__MediaCoverS3__SecretKey + valueFrom: + secretKeyRef: + name: arrstack-media-s3 + key: AWS_SECRET_ACCESS_KEY + - name: Sonarr__MediaCoverS3__Bucket + value: arrstack-media + - name: Sonarr__MediaCoverS3__Prefix + value: sonarr-kids + - 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, + # partitioned by the sonarr-kids key prefix). + - name: Sonarr__BackupS3__Endpoint + valueFrom: + secretKeyRef: + name: arrstack-backups-s3 + key: S3_ENDPOINT + - name: Sonarr__BackupS3__AccessKey + valueFrom: + secretKeyRef: + name: arrstack-backups-s3 + key: AWS_ACCESS_KEY_ID + - name: Sonarr__BackupS3__SecretKey + valueFrom: + secretKeyRef: + name: arrstack-backups-s3 + key: AWS_SECRET_ACCESS_KEY + - name: Sonarr__BackupS3__Bucket + value: arrstack-backups + - name: Sonarr__BackupS3__Prefix + value: sonarr-kids + - name: Sonarr__BackupS3__ForcePathStyle + value: "true" + - name: Sonarr__BackupS3__CaCertPath + value: /etc/ssl/vault-ca/ca.crt + livenessProbe: + httpGet: + path: /3df803/sonarr/ping + port: http + initialDelaySeconds: 30 + periodSeconds: 30 + timeoutSeconds: 5 + failureThreshold: 3 + readinessProbe: + httpGet: + path: /3df803/sonarr/ping + port: http + initialDelaySeconds: 10 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 3 + resources: + requests: + cpu: 100m + memory: 256Mi + limits: + cpu: "1" + memory: 1Gi + volumeMounts: + - name: config + mountPath: /config + # Kids TV subtree of the shared media-tv PVC (same CephFS subvolume + # the adult sonarr writes and jellyfin reads). + - name: media-tv + mountPath: /media/tv + subPath: tvshows/kids + - 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-kids-exportarr VMPodScrape). + - name: exportarr + image: artifactapi.k8s.syd1.au.unkin.net/ghcr/onedr0p/exportarr:v2.3.0 + imagePullPolicy: IfNotPresent + args: + - sonarr + env: + - name: PORT + value: "9707" + # URL includes the /3df803/sonarr UrlBase (Sonarr__Server__UrlBase). + - name: URL + value: http://localhost:8989/3df803/sonarr + - name: APIKEY + valueFrom: + secretKeyRef: + name: sonarr-kids-apikey + key: apitoken + ports: + - name: metrics + containerPort: 9707 + protocol: TCP + livenessProbe: + httpGet: + path: /healthz + port: metrics + initialDelaySeconds: 15 + periodSeconds: 30 + timeoutSeconds: 5 + failureThreshold: 3 + readinessProbe: + httpGet: + path: /healthz + port: metrics + initialDelaySeconds: 5 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 3 + resources: + requests: + cpu: 25m + memory: 32Mi + limits: + cpu: 100m + memory: 128Mi + volumes: + - name: config + emptyDir: {} + - name: media-tv + persistentVolumeClaim: + claimName: media-tv + # Estate CA for validating the Ceph RGW (s3.ceph.unkin.net) TLS cert. + - name: vault-ca + secret: + secretName: vault-ca-cert + items: + - key: ca.crt + path: ca.crt diff --git a/apps/base/arrstack/sonarr-kids/kustomization.yaml b/apps/base/arrstack/sonarr-kids/kustomization.yaml new file mode 100644 index 0000000..5ab2110 --- /dev/null +++ b/apps/base/arrstack/sonarr-kids/kustomization.yaml @@ -0,0 +1,10 @@ +--- +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: + - vaultstaticsecret.yaml + - configmap.yaml + - deployment.yaml + - service.yaml + - vmpodscrape.yaml diff --git a/apps/base/arrstack/sonarr-kids/service.yaml b/apps/base/arrstack/sonarr-kids/service.yaml new file mode 100644 index 0000000..a016e1d --- /dev/null +++ b/apps/base/arrstack/sonarr-kids/service.yaml @@ -0,0 +1,15 @@ +--- +apiVersion: v1 +kind: Service +metadata: + name: sonarr-kids + namespace: arrstack +spec: + ports: + - name: http + port: 8989 + protocol: TCP + targetPort: http + selector: + app: sonarr-kids + type: ClusterIP diff --git a/apps/base/arrstack/sonarr-kids/vaultstaticsecret.yaml b/apps/base/arrstack/sonarr-kids/vaultstaticsecret.yaml new file mode 100644 index 0000000..e9cd5f2 --- /dev/null +++ b/apps/base/arrstack/sonarr-kids/vaultstaticsecret.yaml @@ -0,0 +1,25 @@ +--- +# sonarr-kids API key. Seeded out-of-band at +# kv/kubernetes/namespace/arrstack/default/sonarr-kids (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-kids-apikey Secret consumed by the Deployment. +apiVersion: secrets.hashicorp.com/v1beta1 +kind: VaultStaticSecret +metadata: + name: sonarr-kids-apikey + namespace: arrstack + annotations: + argocd.argoproj.io/sync-wave: "0" +spec: + destination: + create: true + name: sonarr-kids-apikey + overwrite: true + hmacSecretData: true + mount: kv + path: kubernetes/namespace/arrstack/default/sonarr-kids + refreshAfter: 5m + type: kv-v2 + vaultAuthRef: default diff --git a/apps/base/arrstack/sonarr-kids/vmpodscrape.yaml b/apps/base/arrstack/sonarr-kids/vmpodscrape.yaml new file mode 100644 index 0000000..0e88fa8 --- /dev/null +++ b/apps/base/arrstack/sonarr-kids/vmpodscrape.yaml @@ -0,0 +1,16 @@ +--- +# Scrape the exportarr sidecar (:9707) on every sonarr-kids pod. Picked up by the +# observability VMAgent (selectAllByDefault). Pod-level rather than +# VMServiceScrape because the sonarr-kids Service doesn't expose the metrics port. +apiVersion: operator.victoriametrics.com/v1beta1 +kind: VMPodScrape +metadata: + name: sonarr-kids-exportarr + namespace: arrstack +spec: + selector: + matchLabels: + app: sonarr-kids + podMetricsEndpoints: + - port: metrics + path: /metrics