diff --git a/kubernetes/app/media/configmap.yaml b/kubernetes/app/media/configmap.yaml new file mode 100644 index 0000000..e0d9bef --- /dev/null +++ b/kubernetes/app/media/configmap.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: media-common-env + namespace: media +data: + PUID: "1027" + PGID: "100" + TZ: Europe/Kyiv + UMASK: "002" diff --git a/kubernetes/app/media/cronjob-backup.yaml b/kubernetes/app/media/cronjob-backup.yaml new file mode 100644 index 0000000..8e8515e --- /dev/null +++ b/kubernetes/app/media/cronjob-backup.yaml @@ -0,0 +1,135 @@ +apiVersion: batch/v1 +kind: CronJob +metadata: + name: sonarr-db-backup + namespace: media + labels: + app: sonarr-db-backup +spec: + schedule: "0 3 * * *" + concurrencyPolicy: Forbid + successfulJobsHistoryLimit: 3 + failedJobsHistoryLimit: 3 + jobTemplate: + spec: + template: + metadata: + labels: + app: sonarr-db-backup + spec: + restartPolicy: OnFailure + initContainers: + - name: pg-dump + image: postgres:14.21 + env: + - name: PGHOST + value: sonarr-db + - name: PGUSER + valueFrom: + secretKeyRef: + name: media-db-credentials + key: SONARR_DB_USER + - name: PGPASSWORD + valueFrom: + secretKeyRef: + name: media-db-credentials + key: SONARR_DB_PASSWORD + - name: PGDATABASE + valueFrom: + secretKeyRef: + name: media-db-credentials + key: SONARR_DB_NAME + command: + - sh + - -c + - pg_dump --clean --if-exists > /backup/dump.sql + volumeMounts: + - name: backup + mountPath: /backup + containers: + - name: rclone-upload + image: rclone/rclone:1.69 + command: + - sh + - -c + - rclone copy /backup/dump.sql r2crypt:sonarr/ --config /config/rclone/rclone.conf + volumeMounts: + - name: backup + mountPath: /backup + - name: rclone-config + mountPath: /config/rclone + readOnly: true + volumes: + - name: backup + emptyDir: {} + - name: rclone-config + secret: + secretName: rclone-config +--- +apiVersion: batch/v1 +kind: CronJob +metadata: + name: radarr-db-backup + namespace: media + labels: + app: radarr-db-backup +spec: + schedule: "0 3 * * *" + concurrencyPolicy: Forbid + successfulJobsHistoryLimit: 3 + failedJobsHistoryLimit: 3 + jobTemplate: + spec: + template: + metadata: + labels: + app: radarr-db-backup + spec: + restartPolicy: OnFailure + initContainers: + - name: pg-dump + image: postgres:14.21 + env: + - name: PGHOST + value: radarr-db + - name: PGUSER + valueFrom: + secretKeyRef: + name: media-db-credentials + key: RADARR_DB_USER + - name: PGPASSWORD + valueFrom: + secretKeyRef: + name: media-db-credentials + key: RADARR_DB_PASSWORD + - name: PGDATABASE + valueFrom: + secretKeyRef: + name: media-db-credentials + key: RADARR_DB_NAME + command: + - sh + - -c + - pg_dump --clean --if-exists > /backup/dump.sql + volumeMounts: + - name: backup + mountPath: /backup + containers: + - name: rclone-upload + image: rclone/rclone:1.69 + command: + - sh + - -c + - rclone copy /backup/dump.sql r2crypt:radarr/ --config /config/rclone/rclone.conf + volumeMounts: + - name: backup + mountPath: /backup + - name: rclone-config + mountPath: /config/rclone + readOnly: true + volumes: + - name: backup + emptyDir: {} + - name: rclone-config + secret: + secretName: rclone-config diff --git a/kubernetes/app/media/deployment-prowlarr.yaml b/kubernetes/app/media/deployment-prowlarr.yaml new file mode 100644 index 0000000..09d8132 --- /dev/null +++ b/kubernetes/app/media/deployment-prowlarr.yaml @@ -0,0 +1,59 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: prowlarr + namespace: media + labels: + app: prowlarr +spec: + replicas: 0 + strategy: + type: Recreate + selector: + matchLabels: + app: prowlarr + template: + metadata: + labels: + app: prowlarr + spec: + initContainers: + - name: wait-for-config + image: busybox:1.37 + command: + - sh + - -c + - until ls /config > /dev/null 2>&1; do echo "Waiting for config volume..."; sleep 5; done + volumeMounts: + - name: config + mountPath: /config + containers: + - name: prowlarr + image: lscr.io/linuxserver/prowlarr:2.3.0.5236-ls137 + envFrom: + - configMapRef: + name: media-common-env + ports: + - containerPort: 9696 + name: http + protocol: TCP + livenessProbe: + httpGet: + port: 9696 + path: /ping + initialDelaySeconds: 30 + periodSeconds: 30 + failureThreshold: 5 + readinessProbe: + httpGet: + port: 9696 + path: /ping + initialDelaySeconds: 10 + periodSeconds: 10 + volumeMounts: + - name: config + mountPath: /config + volumes: + - name: config + persistentVolumeClaim: + claimName: prowlarr-config diff --git a/kubernetes/app/media/deployment-qbittorrent.yaml b/kubernetes/app/media/deployment-qbittorrent.yaml new file mode 100644 index 0000000..c602ff1 --- /dev/null +++ b/kubernetes/app/media/deployment-qbittorrent.yaml @@ -0,0 +1,75 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: qbittorrent + namespace: media + labels: + app: qbittorrent +spec: + replicas: 0 + strategy: + type: Recreate + selector: + matchLabels: + app: qbittorrent + template: + metadata: + labels: + app: qbittorrent + spec: + initContainers: + - name: wait-for-nfs + image: busybox:1.37 + command: + - sh + - -c + - until ls /media > /dev/null 2>&1; do echo "Waiting for NFS..."; sleep 5; done + volumeMounts: + - name: media + mountPath: /media + containers: + - name: qbittorrent + image: lscr.io/linuxserver/qbittorrent:5.1.4-r2-ls441 + envFrom: + - configMapRef: + name: media-common-env + env: + - name: WEBUI_PORT + value: "8114" + ports: + - containerPort: 8114 + name: webui + protocol: TCP + - containerPort: 23312 + name: bt-tcp + protocol: TCP + - containerPort: 23312 + name: bt-udp + protocol: UDP + livenessProbe: + httpGet: + port: 8114 + path: / + initialDelaySeconds: 30 + periodSeconds: 30 + timeoutSeconds: 5 + failureThreshold: 5 + readinessProbe: + httpGet: + port: 8114 + path: / + initialDelaySeconds: 10 + periodSeconds: 10 + timeoutSeconds: 5 + volumeMounts: + - name: config + mountPath: /config + - name: media + mountPath: /media + volumes: + - name: config + persistentVolumeClaim: + claimName: qbittorrent-config + - name: media + persistentVolumeClaim: + claimName: media-nfs diff --git a/kubernetes/app/media/deployment-radarr.yaml b/kubernetes/app/media/deployment-radarr.yaml new file mode 100644 index 0000000..c3f0087 --- /dev/null +++ b/kubernetes/app/media/deployment-radarr.yaml @@ -0,0 +1,89 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: radarr + namespace: media + labels: + app: radarr +spec: + replicas: 0 + strategy: + type: Recreate + selector: + matchLabels: + app: radarr + template: + metadata: + labels: + app: radarr + spec: + initContainers: + - name: wait-for-nfs + image: busybox:1.37 + command: + - sh + - -c + - until ls /media > /dev/null 2>&1; do echo "Waiting for NFS..."; sleep 5; done + volumeMounts: + - name: media + mountPath: /media + - name: wait-for-db + image: busybox:1.37 + command: + - sh + - -c + - until nc -z radarr-db 5432; do echo "Waiting for database..."; sleep 2; done + - name: init-log-db + image: postgres:14.21 + env: + - name: PGHOST + value: radarr-db + - name: PGUSER + valueFrom: + secretKeyRef: + name: media-db-credentials + key: RADARR_DB_USER + - name: PGPASSWORD + valueFrom: + secretKeyRef: + name: media-db-credentials + key: RADARR_DB_PASSWORD + command: + - sh + - -c + - psql -d postgres -c 'CREATE DATABASE "radarr-log"' || true + containers: + - name: radarr + image: lscr.io/linuxserver/radarr:6.0.4.10291-ls293 + envFrom: + - configMapRef: + name: media-common-env + ports: + - containerPort: 7878 + name: http + protocol: TCP + livenessProbe: + httpGet: + port: 7878 + path: /ping + initialDelaySeconds: 30 + periodSeconds: 30 + failureThreshold: 5 + readinessProbe: + httpGet: + port: 7878 + path: /ping + initialDelaySeconds: 10 + periodSeconds: 10 + volumeMounts: + - name: config + mountPath: /config + - name: media + mountPath: /media + volumes: + - name: config + persistentVolumeClaim: + claimName: radarr-config + - name: media + persistentVolumeClaim: + claimName: media-nfs diff --git a/kubernetes/app/media/deployment-sonarr.yaml b/kubernetes/app/media/deployment-sonarr.yaml new file mode 100644 index 0000000..a873f23 --- /dev/null +++ b/kubernetes/app/media/deployment-sonarr.yaml @@ -0,0 +1,89 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: sonarr + namespace: media + labels: + app: sonarr +spec: + replicas: 0 + strategy: + type: Recreate + selector: + matchLabels: + app: sonarr + template: + metadata: + labels: + app: sonarr + spec: + initContainers: + - name: wait-for-nfs + image: busybox:1.37 + command: + - sh + - -c + - until ls /media > /dev/null 2>&1; do echo "Waiting for NFS..."; sleep 5; done + volumeMounts: + - name: media + mountPath: /media + - name: wait-for-db + image: busybox:1.37 + command: + - sh + - -c + - until nc -z sonarr-db 5432; do echo "Waiting for database..."; sleep 2; done + - name: init-log-db + image: postgres:14.21 + env: + - name: PGHOST + value: sonarr-db + - name: PGUSER + valueFrom: + secretKeyRef: + name: media-db-credentials + key: SONARR_DB_USER + - name: PGPASSWORD + valueFrom: + secretKeyRef: + name: media-db-credentials + key: SONARR_DB_PASSWORD + command: + - sh + - -c + - psql -d postgres -c 'CREATE DATABASE "sonarr-log"' || true + containers: + - name: sonarr + image: lscr.io/linuxserver/sonarr:4.0.16.2944-ls303 + envFrom: + - configMapRef: + name: media-common-env + ports: + - containerPort: 8989 + name: http + protocol: TCP + livenessProbe: + httpGet: + port: 8989 + path: /ping + initialDelaySeconds: 30 + periodSeconds: 30 + failureThreshold: 5 + readinessProbe: + httpGet: + port: 8989 + path: /ping + initialDelaySeconds: 10 + periodSeconds: 10 + volumeMounts: + - name: config + mountPath: /config + - name: media + mountPath: /media + volumes: + - name: config + persistentVolumeClaim: + claimName: sonarr-config + - name: media + persistentVolumeClaim: + claimName: media-nfs diff --git a/kubernetes/app/media/ingress.yaml b/kubernetes/app/media/ingress.yaml new file mode 100644 index 0000000..6eb105f --- /dev/null +++ b/kubernetes/app/media/ingress.yaml @@ -0,0 +1,159 @@ +# Middleware for API clients (NZB360 etc.) that use HTTP basic auth. +# Uses Authelia's legacy verify endpoint which responds with 401 + +# WWW-Authenticate instead of redirecting to the login page. +apiVersion: traefik.io/v1alpha1 +kind: Middleware +metadata: + name: authelia-basic + namespace: media +spec: + forwardAuth: + address: http://authelia-authelia.authelia.svc.cluster.local/api/verify?auth=basic + trustForwardHeader: true + authResponseHeaders: + - Remote-User + - Remote-Groups + - Remote-Email + - Remote-Name +--- +# qBittorrent - browser access via Authelia SSO +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: qbittorrent + namespace: media + annotations: + cert-manager.io/cluster-issuer: letsencrypt + traefik.ingress.kubernetes.io/router.middlewares: authelia-chain-authelia-authelia-auth@kubernetescrd +spec: + tls: + - hosts: + - ${QBITTORRENT_HOST} + secretName: qbittorrent-tls + rules: + - host: ${QBITTORRENT_HOST} + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: qbittorrent + port: + number: 8114 +--- +# qBittorrent API - basic auth for NZB360. +# Uses IngressRoute with HeaderRegexp so only requests carrying an +# Authorization: Basic header are matched; browser XHR/fetch calls +# (which rely on the Authelia session cookie) fall through to the +# standard SSO Ingress above. +apiVersion: traefik.io/v1alpha1 +kind: IngressRoute +metadata: + name: qbittorrent-api + namespace: media +spec: + entryPoints: + - websecure + routes: + - match: Host(`${QBITTORRENT_HOST}`) && PathPrefix(`/api/v2`) && HeaderRegexp(`Authorization`, `^Basic .+$`) + kind: Rule + middlewares: + - name: authelia-basic + services: + - name: qbittorrent + port: 8114 + tls: + secretName: qbittorrent-tls +--- +# Sonarr - browser access via Authelia SSO +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: sonarr + namespace: media + annotations: + cert-manager.io/cluster-issuer: letsencrypt + traefik.ingress.kubernetes.io/router.middlewares: authelia-chain-authelia-authelia-auth@kubernetescrd +spec: + tls: + - hosts: + - ${SONARR_HOST} + secretName: sonarr-tls + rules: + - host: ${SONARR_HOST} + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: sonarr + port: + number: 8989 +--- +# Sonarr API - basic auth for NZB360 +apiVersion: traefik.io/v1alpha1 +kind: IngressRoute +metadata: + name: sonarr-api + namespace: media +spec: + entryPoints: + - websecure + routes: + - match: Host(`${SONARR_HOST}`) && PathPrefix(`/api/v3`) && HeaderRegexp(`Authorization`, `^Basic .+$`) + kind: Rule + middlewares: + - name: authelia-basic + services: + - name: sonarr + port: 8989 + tls: + secretName: sonarr-tls +--- +# Radarr - browser access via Authelia SSO +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: radarr + namespace: media + annotations: + cert-manager.io/cluster-issuer: letsencrypt + traefik.ingress.kubernetes.io/router.middlewares: authelia-chain-authelia-authelia-auth@kubernetescrd +spec: + tls: + - hosts: + - ${RADARR_HOST} + secretName: radarr-tls + rules: + - host: ${RADARR_HOST} + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: radarr + port: + number: 7878 +--- +# Radarr API - basic auth for NZB360 +apiVersion: traefik.io/v1alpha1 +kind: IngressRoute +metadata: + name: radarr-api + namespace: media +spec: + entryPoints: + - websecure + routes: + - match: Host(`${RADARR_HOST}`) && PathPrefix(`/api/v3`) && HeaderRegexp(`Authorization`, `^Basic .+$`) + kind: Rule + middlewares: + - name: authelia-basic + services: + - name: radarr + port: 7878 + tls: + secretName: radarr-tls diff --git a/kubernetes/app/media/namespace.yaml b/kubernetes/app/media/namespace.yaml new file mode 100644 index 0000000..d592f0d --- /dev/null +++ b/kubernetes/app/media/namespace.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: media diff --git a/kubernetes/app/media/networkpolicy.yaml b/kubernetes/app/media/networkpolicy.yaml new file mode 100644 index 0000000..0122090 --- /dev/null +++ b/kubernetes/app/media/networkpolicy.yaml @@ -0,0 +1,145 @@ +# Default deny all ingress in the media namespace +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: default-deny-ingress + namespace: media +spec: + podSelector: {} + policyTypes: + - Ingress +--- +# Allow ingress controller to reach qbittorrent, sonarr, radarr +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: allow-ingress-controller + namespace: media +spec: + podSelector: + matchExpressions: + - key: app + operator: In + values: + - qbittorrent + - sonarr + - radarr + policyTypes: + - Ingress + ingress: + - from: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: traefik +--- +# sonarr-db: only reachable from sonarr and backup jobs +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: sonarr-db + namespace: media +spec: + podSelector: + matchLabels: + app: sonarr-db + policyTypes: + - Ingress + ingress: + - from: + - podSelector: + matchLabels: + app: sonarr + - podSelector: + matchLabels: + app: sonarr-db-backup +--- +# radarr-db: only reachable from radarr and backup jobs +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: radarr-db + namespace: media +spec: + podSelector: + matchLabels: + app: radarr-db + policyTypes: + - Ingress + ingress: + - from: + - podSelector: + matchLabels: + app: radarr + - podSelector: + matchLabels: + app: radarr-db-backup +--- +# Allow prowlarr to receive connections from sonarr and radarr +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: allow-internal-comms + namespace: media +spec: + podSelector: + matchLabels: + app: prowlarr + policyTypes: + - Ingress + ingress: + - from: + - podSelector: + matchLabels: + app: sonarr + - podSelector: + matchLabels: + app: radarr +--- +# Allow prowlarr to reach sonarr, radarr, and qbittorrent +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: allow-prowlarr-to-apps + namespace: media +spec: + podSelector: + matchExpressions: + - key: app + operator: In + values: + - sonarr + - radarr + - qbittorrent + policyTypes: + - Ingress + ingress: + - from: + - podSelector: + matchLabels: + app: prowlarr +--- +# Allow qbittorrent to receive connections from sonarr, radarr, and external BT traffic +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: allow-qbittorrent + namespace: media +spec: + podSelector: + matchLabels: + app: qbittorrent + policyTypes: + - Ingress + ingress: + - from: + - podSelector: + matchLabels: + app: sonarr + - podSelector: + matchLabels: + app: radarr + - ports: + - port: 23312 + protocol: TCP + - port: 23312 + protocol: UDP diff --git a/kubernetes/app/media/pv-media.yaml b/kubernetes/app/media/pv-media.yaml new file mode 100644 index 0000000..caa7505 --- /dev/null +++ b/kubernetes/app/media/pv-media.yaml @@ -0,0 +1,17 @@ +apiVersion: v1 +kind: PersistentVolume +metadata: + name: media-nfs +spec: + capacity: + storage: 1Ti + accessModes: + - ReadWriteMany + storageClassName: "" + persistentVolumeReclaimPolicy: Retain + mountOptions: + - hard + - nointr + nfs: + server: synology.storage.lviv + path: ${MEDIA_NFS_PATH} diff --git a/kubernetes/app/media/secret-rclone.sops.yaml b/kubernetes/app/media/secret-rclone.sops.yaml new file mode 100644 index 0000000..2bc2e37 --- /dev/null +++ b/kubernetes/app/media/secret-rclone.sops.yaml @@ -0,0 +1,22 @@ +apiVersion: v1 +kind: Secret +metadata: + name: rclone-config + namespace: media +stringData: + rclone.conf: ENC[AES256_GCM,data:aTthn7P4ESDNtqRDW+R7RI1e0B2ftgHg6406vqHtsUPW25SEaNUiGuWlSY/BoiCuagBBM6TM+Gow2XrtTQauPND1irb5D5xyhcphFM5uF+8dE4qNJV4J09NjBd7Lzdp1udej2BJoRAeCwgAEwq8857log4TRwrbGzNrfRKOPrz7D3okBQP2hyxfZ85dlQH0ojUEodPi4U3mpwgXc8Kb0JhziZc1KjHbIzZ1/GkaKcYLI8Rl8q7esdqxnrA51sssWVkmSC4zoiPjwPwUWifSNMb7KC8L8bXRIVdpP8/f4rDyYJoa9uC7GW44nYIBVfLJrM015ZtpSzh05/IS9ev6N2REkjudLmOwwvOw52s8dKh1IRXixcV1JPk3knam2jnezYcnvmLYgyReryZFRFFP0xgaVOmZa5is=,iv:zrFW+ssUTt8T14TZz5n30rVr792FiDYxr89BdIIxn1c=,tag:XVtMvVbuo1vQU3hp8VTnHQ==,type:str] +sops: + age: + - recipient: age1zffnskvuezntkk703a0pyxsd5m8vx2hm33dr47wdfy8mn4fdw4sqgw0jgc + enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBlR1BZak5zZWdmZ2g3YktL + Q1lqZzVTWEtpVGhqSk9WamxmRm1WSFNnNkN3CjNzMEtyMnh6bEtjbW9BY0NGZm1G + dnMyOW5DVFpZT3JMVGNxVW9raWZkSzAKLS0tIHNHMXdZTlhXYWFRYmYvS21tQUps + MXB5QVkyZDZlQWlMd1NqdEl1V0g0d00KMSyMsWeN5oEx3s5Zh3x9MHiRywFvRuZm + lZEdl0ho90lJ8m+rPHIT+pI7vBMHLB3mJiBfIVR4KJQRUkhPjGSMRQ== + -----END AGE ENCRYPTED FILE----- + lastmodified: "2026-02-22T16:12:51Z" + mac: ENC[AES256_GCM,data:gUyVh53pNCUHfW9+pww9sQ10kL/U92N0AV1Ys+fKt3W/wq4msyXLErQVVJBrHoPQo/ncKUMbCUGvMNHsUnJT7H8g8LuqMUxLbZhFju6rLHrgg+1hIKeA1cStOlTgBTQLbXA2gfsclQQ+nAf9zeAsgz3MfuK8PEHs3U2O8cSrtP4=,iv:fQd6lggTw3OI+8lZ1BOZvD+lLt7p5wlelf59sFsQHZE=,tag:4V8do9D3IiNPDDR5GRLXOg==,type:str] + encrypted_regex: ^(data|stringData|email)$ + version: 3.11.0 diff --git a/kubernetes/app/media/secret.sops.yaml b/kubernetes/app/media/secret.sops.yaml new file mode 100644 index 0000000..6659e4d --- /dev/null +++ b/kubernetes/app/media/secret.sops.yaml @@ -0,0 +1,27 @@ +apiVersion: v1 +kind: Secret +metadata: + name: media-db-credentials + namespace: media +stringData: + SONARR_DB_USER: ENC[AES256_GCM,data:4hElKqu/,iv:FxKGg5gBSVi6wwp7Evz/BeU1HXgkBSjlk5RqLm65qws=,tag:DeCX+CgbNUJVkvTLu7uBoQ==,type:str] + SONARR_DB_NAME: ENC[AES256_GCM,data:idbioCQ81Vd8oz4=,iv:uD2fJ/4ZANiEB/V1zH1nfLF20LVOCuh6Zzrb1KQeekc=,tag:/Cp0K2U488VBQdtJX3lvmQ==,type:str] + SONARR_DB_PASSWORD: ENC[AES256_GCM,data:Ut+XErV4XN60lgiLGZ6XTWEXWgLF8NHkrUUXv4M2XFNsL0/DvL4qfCTKigCP8FR5ayBNhXAoF3cQ467WUFpvUA==,iv:hfN6h0lLqWd7uSc/EHsZwPT2sjSyF0Oyr1HpCDfWkV8=,tag:Idb2bMtM/4OezqL2ttN2CA==,type:str] + RADARR_DB_USER: ENC[AES256_GCM,data:q8IuzrfO,iv:MVretLwHLhd5fsYYRa9jyq59ebcYdZB1flYWoBwrTII=,tag:+LV02NiQyd+T6mwqLzET5A==,type:str] + RADARR_DB_NAME: ENC[AES256_GCM,data:VxtGKMV+LO/exOo=,iv:6RUjbk+/tw1GQYK4nqElhELo5lqgvYhR7gYZIaLDOlM=,tag:FGexxymrlwncNcVMOLZ8/Q==,type:str] + RADARR_DB_PASSWORD: ENC[AES256_GCM,data:SB2AGVPdlhGGreSKUQ9fxJn1O6wyPMucy+knwWhQrxqyijzMgCJIYFRv8Gr3gpns3Zo1EvX1faxc/EfN19XSxw==,iv:SeqSpGuCTmN4V3V9AGWSD664Z2mMdZU9tWM2uTZYusM=,tag:xm9J4s5F8x8ulMualpJVtQ==,type:str] +sops: + age: + - recipient: age1zffnskvuezntkk703a0pyxsd5m8vx2hm33dr47wdfy8mn4fdw4sqgw0jgc + enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBINHB0V21XRklkY2p5WGZE + a0UxaDVRODZoZUtlYm1nR1lnLzdITlZrZG44CnhwRDVkS1lhSWsrNU5ObE5kSWl5 + bVZhZUt3NFIzdUdWYVZZTXV4d09mVGcKLS0tIHhrTUFvdDRKZ1NyZ2llaHZQTWhK + ZVZmNzRlOVRnK1FBdTc1ZmU1bzNidU0KBP4sIZzuVn7PU17e09p6Td0sMG7K+NsQ + AcvdVNFr6mOfivGn86Ao1R4xPE4ANqZfrNQCgIoKxsOQPcc13vOJbA== + -----END AGE ENCRYPTED FILE----- + lastmodified: "2026-02-22T10:20:21Z" + mac: ENC[AES256_GCM,data:36OEPmj2rFjEAWU9RG8DvvwYJGuMmLKZ9e2fW48N8SBajaGiSGLBfRkRKHy5ncTtNaGmlfBuRtM5CoA9/qaxNQJ/q7DefSx0E0KuS9zTamfRcf7oTZEqmaK3v8r8AXmCTi4od5eiISPT5d9oN48V+aNLd3iFLwt9fSUQ8dPOQ3A=,iv:yP145UUDKBU9NSPlIZ+wAeKB/TDBv4OUMlCvayzKTmU=,tag:Qt383JbWxzB3/GxF32NezA==,type:str] + encrypted_regex: ^(data|stringData|email)$ + version: 3.11.0 diff --git a/kubernetes/app/media/service.yaml b/kubernetes/app/media/service.yaml new file mode 100644 index 0000000..f91b086 --- /dev/null +++ b/kubernetes/app/media/service.yaml @@ -0,0 +1,124 @@ +apiVersion: v1 +kind: Service +metadata: + name: qbittorrent + namespace: media + labels: + app: qbittorrent +spec: + type: ClusterIP + ports: + - name: 8114-8114 + port: 8114 + targetPort: 8114 + protocol: TCP + selector: + app: qbittorrent +--- +apiVersion: v1 +kind: Service +metadata: + name: qbittorrent-bt + namespace: media + labels: + app: qbittorrent +spec: + type: NodePort + ports: + - name: bt-tcp + port: 23312 + targetPort: 23312 + nodePort: 30312 + protocol: TCP + - name: bt-udp + port: 23312 + targetPort: 23312 + nodePort: 30312 + protocol: UDP + selector: + app: qbittorrent +--- +apiVersion: v1 +kind: Service +metadata: + name: prowlarr + namespace: media + labels: + app: prowlarr +spec: + type: ClusterIP + ports: + - name: 9696-9696 + port: 9696 + targetPort: 9696 + protocol: TCP + selector: + app: prowlarr +--- +apiVersion: v1 +kind: Service +metadata: + name: sonarr + namespace: media + labels: + app: sonarr +spec: + type: ClusterIP + ports: + - name: 8989-8989 + port: 8989 + targetPort: 8989 + protocol: TCP + selector: + app: sonarr +--- +apiVersion: v1 +kind: Service +metadata: + name: radarr + namespace: media + labels: + app: radarr +spec: + type: ClusterIP + ports: + - name: 7878-7878 + port: 7878 + targetPort: 7878 + protocol: TCP + selector: + app: radarr +--- +apiVersion: v1 +kind: Service +metadata: + name: sonarr-db + namespace: media + labels: + app: sonarr-db +spec: + type: ClusterIP + ports: + - name: 5432-5432 + port: 5432 + targetPort: 5432 + protocol: TCP + selector: + app: sonarr-db +--- +apiVersion: v1 +kind: Service +metadata: + name: radarr-db + namespace: media + labels: + app: radarr-db +spec: + type: ClusterIP + ports: + - name: 5432-5432 + port: 5432 + targetPort: 5432 + protocol: TCP + selector: + app: radarr-db diff --git a/kubernetes/app/media/statefulset-radarr-db.yaml b/kubernetes/app/media/statefulset-radarr-db.yaml new file mode 100644 index 0000000..f968836 --- /dev/null +++ b/kubernetes/app/media/statefulset-radarr-db.yaml @@ -0,0 +1,65 @@ +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: radarr-db + namespace: media + labels: + app: radarr-db +spec: + replicas: 1 + serviceName: radarr-db + selector: + matchLabels: + app: radarr-db + template: + metadata: + labels: + app: radarr-db + spec: + securityContext: + runAsUser: 1027 + runAsGroup: 100 + fsGroup: 100 + containers: + - name: postgres + image: postgres:14.21 + env: + - name: POSTGRES_DB + valueFrom: + secretKeyRef: + name: media-db-credentials + key: RADARR_DB_NAME + - name: POSTGRES_USER + valueFrom: + secretKeyRef: + name: media-db-credentials + key: RADARR_DB_USER + - name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + name: media-db-credentials + key: RADARR_DB_PASSWORD + - name: PGDATA + value: /var/lib/postgresql/data/pgdata + ports: + - containerPort: 5432 + name: postgres + protocol: TCP + livenessProbe: + tcpSocket: + port: 5432 + initialDelaySeconds: 30 + periodSeconds: 30 + failureThreshold: 5 + readinessProbe: + tcpSocket: + port: 5432 + initialDelaySeconds: 5 + periodSeconds: 10 + volumeMounts: + - name: data + mountPath: /var/lib/postgresql/data + volumes: + - name: data + persistentVolumeClaim: + claimName: radarr-db diff --git a/kubernetes/app/media/statefulset-sonarr-db.yaml b/kubernetes/app/media/statefulset-sonarr-db.yaml new file mode 100644 index 0000000..0aec81e --- /dev/null +++ b/kubernetes/app/media/statefulset-sonarr-db.yaml @@ -0,0 +1,65 @@ +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: sonarr-db + namespace: media + labels: + app: sonarr-db +spec: + replicas: 1 + serviceName: sonarr-db + selector: + matchLabels: + app: sonarr-db + template: + metadata: + labels: + app: sonarr-db + spec: + securityContext: + runAsUser: 1027 + runAsGroup: 100 + fsGroup: 100 + containers: + - name: postgres + image: postgres:14.21 + env: + - name: POSTGRES_DB + valueFrom: + secretKeyRef: + name: media-db-credentials + key: SONARR_DB_NAME + - name: POSTGRES_USER + valueFrom: + secretKeyRef: + name: media-db-credentials + key: SONARR_DB_USER + - name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + name: media-db-credentials + key: SONARR_DB_PASSWORD + - name: PGDATA + value: /var/lib/postgresql/data/pgdata + ports: + - containerPort: 5432 + name: postgres + protocol: TCP + livenessProbe: + tcpSocket: + port: 5432 + initialDelaySeconds: 30 + periodSeconds: 30 + failureThreshold: 5 + readinessProbe: + tcpSocket: + port: 5432 + initialDelaySeconds: 5 + periodSeconds: 10 + volumeMounts: + - name: data + mountPath: /var/lib/postgresql/data + volumes: + - name: data + persistentVolumeClaim: + claimName: sonarr-db diff --git a/kubernetes/app/media/volumeclaim.yaml b/kubernetes/app/media/volumeclaim.yaml new file mode 100644 index 0000000..7493bb5 --- /dev/null +++ b/kubernetes/app/media/volumeclaim.yaml @@ -0,0 +1,91 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: media-nfs + namespace: media +spec: + accessModes: + - ReadWriteMany + storageClassName: "" + volumeName: media-nfs + resources: + requests: + storage: 1Ti +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: qbittorrent-config + namespace: media +spec: + accessModes: + - ReadWriteOnce + storageClassName: nfs-synology-ssd + resources: + requests: + storage: 1Gi +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: prowlarr-config + namespace: media +spec: + accessModes: + - ReadWriteOnce + storageClassName: nfs-synology-ssd + resources: + requests: + storage: 1Gi +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: sonarr-config + namespace: media +spec: + accessModes: + - ReadWriteOnce + storageClassName: nfs-synology-ssd + resources: + requests: + storage: 5Gi +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: radarr-config + namespace: media +spec: + accessModes: + - ReadWriteOnce + storageClassName: nfs-synology-ssd + resources: + requests: + storage: 5Gi +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: sonarr-db + namespace: media +spec: + accessModes: + - ReadWriteOnce + storageClassName: nfs-synology-ssd + resources: + requests: + storage: 5Gi +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: radarr-db + namespace: media +spec: + accessModes: + - ReadWriteOnce + storageClassName: nfs-synology-ssd + resources: + requests: + storage: 5Gi diff --git a/kubernetes/config/cluster-vars.sops.yaml b/kubernetes/config/cluster-vars.sops.yaml index 1bc2acf..9a63894 100644 --- a/kubernetes/config/cluster-vars.sops.yaml +++ b/kubernetes/config/cluster-vars.sops.yaml @@ -6,6 +6,10 @@ metadata: stringData: LUBELOGGER_HOST: ENC[AES256_GCM,data:OvDY/XIE/YW8lSDJmhHYI63r4eLQOojsMjjkUIge,iv:v1JafZB4cmVFjX+yA7FjjoXfx7jPpZQaq1HyXvNXvsY=,tag:+h5Gg/q3bKP3l7xCNLaBqA==,type:str] AUTHELIA_DOMAIN: ENC[AES256_GCM,data:mioy6n5AqiN8jPU9cMTO,iv:HFjyN0UCohNxuJkmt9dgcvnjHSTSAr7svin9Fjjykk8=,tag:2aknDljwRFGJBTzzmQ2UFA==,type:str] + QBITTORRENT_HOST: ENC[AES256_GCM,data:OxaHKav3STl9FKV0qtJ0FpfTHgSrvCXrkP25,iv:wzfie/2YkZH0A6R3wF3BJV8U5aKhewjQ7c9AfstFQMI=,tag:rxHw0XhzqBm/rN4OytclgQ==,type:str] + SONARR_HOST: ENC[AES256_GCM,data:dMEgQYHxa0tIilY8HPkppFgOWzH52Q==,iv:yCUF8ZVOSY2IfULa9B44ALGjgFyZyQrQWPZsyQzqwbM=,tag:kfhsZhPE1rZNYUJ3X2rRSQ==,type:str] + RADARR_HOST: ENC[AES256_GCM,data:lsWSI+D/qzru3qHGvJjKRepYEGHe5Q==,iv:cq9yNVCMuon+ZUpFwfkwBvVg9oIT0MXagjDi6l/29YA=,tag:koGF34SrtHe0brRiNKP8rg==,type:str] + MEDIA_NFS_PATH: ENC[AES256_GCM,data:BSDMu0n2Vx4koYBIMF8=,iv:c9kGdcTxObNaaaTzEhSRkyHvo+dxSN+o+96n9UqJieU=,tag:W9+MbyAuK85xajjwntRi0Q==,type:str] sops: age: - recipient: age1zffnskvuezntkk703a0pyxsd5m8vx2hm33dr47wdfy8mn4fdw4sqgw0jgc @@ -17,7 +21,7 @@ sops: LzhUN3Z4cExIL1IyS3ZCNWh5aWpLbDgKQ7c3MmLykA00NaLoctKVDfJvPqTqh3Ia cDZJUc6jYJXOJYM6YYyZOYcCL2z8V2RpIfA9sPg8PB2eiipZxjk+Cg== -----END AGE ENCRYPTED FILE----- - lastmodified: "2026-02-21T22:09:11Z" - mac: ENC[AES256_GCM,data:KXaSGwX9gmEgFvmVuSblrmGqNKS5nooAO1GJsCTP/QkAvTMzSsVZZcoUreQY3HU0bLxWABzb8ZPgOGVBNvL05sB0uwGwvUvpsUZp5ryYWVdKxsdCGeZpMdsXFbwIprdI/SerEnHzDvXCV7XKes22N8A5wsVYZXm6s+grkJhAR68=,iv:yLQR5N19/cThHRH4cqiut0GEXBDuo4CEzuKlC6G4N3I=,tag:j9lEXa/b3Kkf9+Ph2f1rkw==,type:str] + lastmodified: "2026-02-22T10:48:28Z" + mac: ENC[AES256_GCM,data:qw4wqmlHntZiybI2VPKcQEUJg0AG8kgWn7hEw6WlCltf7STbRWLLedR5TTRmKpcCQofE4T02ZZPLbzVVP7tSer1S4nAYSMwkfQhgkZ8DVvn/E2O/Yxzja/DXLV9tNMYpYexSkfVX8zYRS1zdd48VBgA4P4N0WR65kvoUnIScrgA=,iv:+brh3Gehjes1G6wNxPVDgHltI80Ih5d21Va/ADJxxCI=,tag:pay3NKWyLYF4BPZnCszW0Q==,type:str] encrypted_regex: ^(data|stringData|email)$ version: 3.11.0 diff --git a/kubernetes/infrastructure/controllers/traefik/release.yaml b/kubernetes/infrastructure/controllers/traefik/release.yaml index 7c48ead..b0a5326 100644 --- a/kubernetes/infrastructure/controllers/traefik/release.yaml +++ b/kubernetes/infrastructure/controllers/traefik/release.yaml @@ -16,6 +16,9 @@ spec: namespace: flux-system interval: 1m0s values: + providers: + kubernetesCRD: + allowCrossNamespace: true service: type: ClusterIP ports: