diff --git a/apps/kustomization.yaml b/apps/kustomization.yaml index f0eeb12..1c870e4 100644 --- a/apps/kustomization.yaml +++ b/apps/kustomization.yaml @@ -8,4 +8,6 @@ kind: Kustomization resources: - pihole.yaml - gitea.yaml + - sonarr.yaml + - radarr.yaml - plex.yaml diff --git a/apps/radarr.yaml b/apps/radarr.yaml new file mode 100644 index 0000000..ac62986 --- /dev/null +++ b/apps/radarr.yaml @@ -0,0 +1,236 @@ +--- +# Namespace (trust zone; default-deny + policies apply) +apiVersion: v1 +kind: Namespace +metadata: + name: radarr + labels: + homelab.keithjackson.dev/app-namespace: 'true' + gateway-access: shared + annotations: + homelab.keithjackson.dev/backup-policy: daily-offcluster +--- +# Default-deny baseline (R-NET-01): deny all, allow only DNS +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: default-deny + namespace: radarr + labels: + homelab.keithjackson.dev/baseline: default-deny +spec: + podSelector: {} + policyTypes: + - Ingress + - Egress + ingress: [] + egress: + - to: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: kube-system + ports: + - protocol: UDP + port: 53 + - protocol: TCP + port: 53 +--- +# PVC radarr-config (R-STATE) +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: radarr-config + namespace: radarr + labels: + app.kubernetes.io/name: radarr +spec: + accessModes: + - ReadWriteOnce + storageClassName: longhorn + resources: + requests: + storage: 5Gi +--- +# Deployment (R-PLAT-02) +apiVersion: apps/v1 +kind: Deployment +metadata: + name: radarr + namespace: radarr + labels: + app.kubernetes.io/name: radarr +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: radarr + template: + metadata: + labels: + app.kubernetes.io/name: radarr + homelab.keithjackson.dev/egress-class: vpn + homelab.keithjackson.dev/auth-mode: forward-auth + annotations: + homelab.keithjackson.dev/auth-mode: forward-auth + homelab.keithjackson.dev/auth-groups: media-users + spec: + containers: + - name: radarr + image: lscr.io/linuxserver/radarr:5.21.1 + ports: + - containerPort: 7878 + volumeMounts: + - name: config + mountPath: /data/config + volumes: + - name: config + persistentVolumeClaim: + claimName: radarr-config +--- +# Service +apiVersion: v1 +kind: Service +metadata: + name: radarr + namespace: radarr + labels: + app.kubernetes.io/name: radarr +spec: + selector: + app.kubernetes.io/name: radarr + ports: + - name: port-7878 + port: 7878 + targetPort: 7878 +--- +# CiliumNetworkPolicy egress allow (R-NET-02): radarr-allow-qbittorrent-8080 +apiVersion: cilium.io/v2 +kind: CiliumNetworkPolicy +metadata: + name: radarr-allow-qbittorrent-8080 + namespace: radarr + labels: + app.kubernetes.io/name: radarr +spec: + endpointSelector: + matchLabels: + app.kubernetes.io/name: radarr + egress: + - toEndpoints: + - matchLabels: + app.kubernetes.io/name: qbittorrent + toPorts: + - ports: + - port: '8080' + protocol: TCP +--- +# CiliumNetworkPolicy egress allow (R-NET-02): radarr-allow-jackett-9117 +apiVersion: cilium.io/v2 +kind: CiliumNetworkPolicy +metadata: + name: radarr-allow-jackett-9117 + namespace: radarr + labels: + app.kubernetes.io/name: radarr +spec: + endpointSelector: + matchLabels: + app.kubernetes.io/name: radarr + egress: + - toEndpoints: + - matchLabels: + app.kubernetes.io/name: jackett + toPorts: + - ports: + - port: '9117' + protocol: TCP +--- +# CiliumNetworkPolicy gateway-ingress allow (R-NET-01/02): radarr-allow-gateway-ingress +apiVersion: cilium.io/v2 +kind: CiliumNetworkPolicy +metadata: + name: radarr-allow-gateway-ingress + namespace: radarr + labels: + app.kubernetes.io/name: radarr +spec: + endpointSelector: + matchLabels: + app.kubernetes.io/name: radarr + ingress: + - fromEntities: + - ingress + toPorts: + - ports: + - port: '7878' + protocol: TCP +--- +# HTTPRoute radarr-7878 (R-TLS/R-DNS) +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: radarr-7878 + namespace: radarr + labels: + app.kubernetes.io/name: radarr + annotations: + homelab.keithjackson.dev/auth-mode: forward-auth + homelab.keithjackson.dev/auth-groups: media-users +spec: + parentRefs: + - name: shared + namespace: gateway + sectionName: https + hostnames: + - radarr.home.keithjackson.dev + rules: + - matches: + - path: + type: PathPrefix + value: / + backendRefs: + - name: radarr + port: 7878 +--- +# ServiceMonitor (R-OBS-01) +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + name: radarr + namespace: radarr + labels: + release: kube-prometheus-stack + app.kubernetes.io/name: radarr +spec: + selector: + matchLabels: + app.kubernetes.io/name: radarr + namespaceSelector: + matchNames: + - radarr + endpoints: + - port: metrics + interval: 30s +--- +# PrometheusRule SLI alerts (R-OBS-01) +apiVersion: monitoring.coreos.com/v1 +kind: PrometheusRule +metadata: + name: radarr-slis + namespace: radarr + labels: + release: kube-prometheus-stack + app.kubernetes.io/name: radarr +spec: + groups: + - name: radarr.slis + rules: + - alert: radarr-down + expr: up{job="radarr"} == 0 + for: 5m + labels: + severity: critical + service: radarr + annotations: + summary: radarr is down + description: 'radarr has been unreachable for 5m (SLI: up).' diff --git a/apps/sonarr.yaml b/apps/sonarr.yaml new file mode 100644 index 0000000..d1a3862 --- /dev/null +++ b/apps/sonarr.yaml @@ -0,0 +1,236 @@ +--- +# Namespace (trust zone; default-deny + policies apply) +apiVersion: v1 +kind: Namespace +metadata: + name: sonarr + labels: + homelab.keithjackson.dev/app-namespace: 'true' + gateway-access: shared + annotations: + homelab.keithjackson.dev/backup-policy: daily-offcluster +--- +# Default-deny baseline (R-NET-01): deny all, allow only DNS +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: default-deny + namespace: sonarr + labels: + homelab.keithjackson.dev/baseline: default-deny +spec: + podSelector: {} + policyTypes: + - Ingress + - Egress + ingress: [] + egress: + - to: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: kube-system + ports: + - protocol: UDP + port: 53 + - protocol: TCP + port: 53 +--- +# PVC sonarr-config (R-STATE) +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: sonarr-config + namespace: sonarr + labels: + app.kubernetes.io/name: sonarr +spec: + accessModes: + - ReadWriteOnce + storageClassName: longhorn + resources: + requests: + storage: 5Gi +--- +# Deployment (R-PLAT-02) +apiVersion: apps/v1 +kind: Deployment +metadata: + name: sonarr + namespace: sonarr + labels: + app.kubernetes.io/name: sonarr +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: sonarr + template: + metadata: + labels: + app.kubernetes.io/name: sonarr + homelab.keithjackson.dev/egress-class: vpn + homelab.keithjackson.dev/auth-mode: forward-auth + annotations: + homelab.keithjackson.dev/auth-mode: forward-auth + homelab.keithjackson.dev/auth-groups: media-users + spec: + containers: + - name: sonarr + image: lscr.io/linuxserver/sonarr:4.0.15 + ports: + - containerPort: 8989 + volumeMounts: + - name: config + mountPath: /data/config + volumes: + - name: config + persistentVolumeClaim: + claimName: sonarr-config +--- +# Service +apiVersion: v1 +kind: Service +metadata: + name: sonarr + namespace: sonarr + labels: + app.kubernetes.io/name: sonarr +spec: + selector: + app.kubernetes.io/name: sonarr + ports: + - name: port-8989 + port: 8989 + targetPort: 8989 +--- +# CiliumNetworkPolicy egress allow (R-NET-02): sonarr-allow-qbittorrent-8080 +apiVersion: cilium.io/v2 +kind: CiliumNetworkPolicy +metadata: + name: sonarr-allow-qbittorrent-8080 + namespace: sonarr + labels: + app.kubernetes.io/name: sonarr +spec: + endpointSelector: + matchLabels: + app.kubernetes.io/name: sonarr + egress: + - toEndpoints: + - matchLabels: + app.kubernetes.io/name: qbittorrent + toPorts: + - ports: + - port: '8080' + protocol: TCP +--- +# CiliumNetworkPolicy egress allow (R-NET-02): sonarr-allow-jackett-9117 +apiVersion: cilium.io/v2 +kind: CiliumNetworkPolicy +metadata: + name: sonarr-allow-jackett-9117 + namespace: sonarr + labels: + app.kubernetes.io/name: sonarr +spec: + endpointSelector: + matchLabels: + app.kubernetes.io/name: sonarr + egress: + - toEndpoints: + - matchLabels: + app.kubernetes.io/name: jackett + toPorts: + - ports: + - port: '9117' + protocol: TCP +--- +# CiliumNetworkPolicy gateway-ingress allow (R-NET-01/02): sonarr-allow-gateway-ingress +apiVersion: cilium.io/v2 +kind: CiliumNetworkPolicy +metadata: + name: sonarr-allow-gateway-ingress + namespace: sonarr + labels: + app.kubernetes.io/name: sonarr +spec: + endpointSelector: + matchLabels: + app.kubernetes.io/name: sonarr + ingress: + - fromEntities: + - ingress + toPorts: + - ports: + - port: '8989' + protocol: TCP +--- +# HTTPRoute sonarr-8989 (R-TLS/R-DNS) +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: sonarr-8989 + namespace: sonarr + labels: + app.kubernetes.io/name: sonarr + annotations: + homelab.keithjackson.dev/auth-mode: forward-auth + homelab.keithjackson.dev/auth-groups: media-users +spec: + parentRefs: + - name: shared + namespace: gateway + sectionName: https + hostnames: + - sonarr.home.keithjackson.dev + rules: + - matches: + - path: + type: PathPrefix + value: / + backendRefs: + - name: sonarr + port: 8989 +--- +# ServiceMonitor (R-OBS-01) +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + name: sonarr + namespace: sonarr + labels: + release: kube-prometheus-stack + app.kubernetes.io/name: sonarr +spec: + selector: + matchLabels: + app.kubernetes.io/name: sonarr + namespaceSelector: + matchNames: + - sonarr + endpoints: + - port: metrics + interval: 30s +--- +# PrometheusRule SLI alerts (R-OBS-01) +apiVersion: monitoring.coreos.com/v1 +kind: PrometheusRule +metadata: + name: sonarr-slis + namespace: sonarr + labels: + release: kube-prometheus-stack + app.kubernetes.io/name: sonarr +spec: + groups: + - name: sonarr.slis + rules: + - alert: sonarr-down + expr: up{job="sonarr"} == 0 + for: 5m + labels: + severity: critical + service: sonarr + annotations: + summary: sonarr is down + description: 'sonarr has been unreachable for 5m (SLI: up).' diff --git a/catalog/radarr.yaml b/catalog/radarr.yaml new file mode 100644 index 0000000..1ff1b2a --- /dev/null +++ b/catalog/radarr.yaml @@ -0,0 +1,48 @@ +# ServiceDefinition: radarr +# Source of truth: spec docs/13-service-catalog.md row 3; docs/15-identity-and-access.md; docs/03. +# internal | egress VPN (+ kill-switch) | config on Longhorn | auth forward-auth | arr→downloaders. +# Radarr (movies) scrapes indexers and hands releases to the download client; both of those hops leave +# the cluster over the shared PIA/WireGuard egress gateway (egress: vpn, ADR-0015; R-NET-05/06), and +# the intra-cluster hops to qbittorrent/jackett are EXPLICIT east-west allows on top of default-deny +# (R-NET-01/02). Validates against schemas/servicedefinition.schema.json. +name: radarr +kind: container +source: + image: lscr.io/linuxserver/radarr:5.21.1 # pinned stable v5 tag (R-UPD-01); update.range governs advancement +exposure: internal # LAN-only web UI at radarr.home.keithjackson.dev (docs/13 row 3; R-DNS/R-NET) +egress: vpn # indexer scrape + downloader handoff steered via the shared VPN egress gateway, + # kill-switch enforced OUTSIDE the workload (docs/03; ADR-0015; R-NET-05/06) +storage: + - name: config + size: 5Gi + class: longhorn # app DB / config on the 3-replica tier; DR-critical (docs/04; R-STATE-02) + # NOTE (bulk-media PREREQUISITE): docs/13 row 3 places the media library / downloads volume on the + # `bulk` tier (R-STATE-04). The `bulk` StorageClass exists (platform/longhorn/storageclasses.yaml) + # but is a NON-provisioning STUB pending OQ-2 (NFS/SMB backing = REPLACE-ME). Adding a `class: bulk` + # volume now would leave the PVC (and therefore the pod) Pending. The media/downloads volume is + # therefore DEFERRED until OQ-2 is resolved; config-only on longhorn keeps Radarr deployable today. +dependencies: + - to: qbittorrent # arr → download client (docs/13 dependency graph; R-NET-02 explicit allow) + port: 8080 + protocol: tcp + - to: jackett # arr → indexer proxy (docs/13 dependency graph; R-NET-02 explicit allow) + port: 9117 + protocol: tcp +ingressRoutes: + - host: radarr.home.keithjackson.dev + port: 7878 # Radarr web/API; TLS terminates at the shared Gateway (docs/08, docs/09) +auth: + mode: forward-auth # bare UI, no usable OIDC: oauth2-proxy → Keycloak at the Gateway (docs/15; R-IDP-02) + groups: + - media-users # authenticated AND in the media group (R-IDP-06) +update: + channel: stable # R-UPD-01 + range: ">=5.0.0 <6.0.0" # stay on the Radarr v5 stable line + cadence: "0 4 * * *" + autoRollbackOnFailedHealth: true # R-UPD-03 +observability: + metrics: true + slis: + - up # arr down => alert (docs/10; R-OBS-01) +backup: + policy: daily-offcluster # Radarr config/DB is irreplaceable, backed up off-cluster (docs/04; R-STATE-03) diff --git a/catalog/sonarr.yaml b/catalog/sonarr.yaml new file mode 100644 index 0000000..acb7495 --- /dev/null +++ b/catalog/sonarr.yaml @@ -0,0 +1,48 @@ +# ServiceDefinition: sonarr +# Source of truth: spec docs/13-service-catalog.md row 2; docs/15-identity-and-access.md; docs/03. +# internal | egress VPN (+ kill-switch) | config on Longhorn | auth forward-auth | arr→downloaders. +# Sonarr (TV) scrapes indexers and hands releases to the download client; both of those hops leave +# the cluster over the shared PIA/WireGuard egress gateway (egress: vpn, ADR-0015; R-NET-05/06), and +# the intra-cluster hops to qbittorrent/jackett are EXPLICIT east-west allows on top of default-deny +# (R-NET-01/02). Validates against schemas/servicedefinition.schema.json. +name: sonarr +kind: container +source: + image: lscr.io/linuxserver/sonarr:4.0.15 # pinned stable v4 tag (R-UPD-01); update.range governs advancement +exposure: internal # LAN-only web UI at sonarr.home.keithjackson.dev (docs/13 row 2; R-DNS/R-NET) +egress: vpn # indexer scrape + downloader handoff steered via the shared VPN egress gateway, + # kill-switch enforced OUTSIDE the workload (docs/03; ADR-0015; R-NET-05/06) +storage: + - name: config + size: 5Gi + class: longhorn # app DB / config on the 3-replica tier; DR-critical (docs/04; R-STATE-02) + # NOTE (bulk-media PREREQUISITE): docs/13 row 2 places the media library / downloads volume on the + # `bulk` tier (R-STATE-04). The `bulk` StorageClass exists (platform/longhorn/storageclasses.yaml) + # but is a NON-provisioning STUB pending OQ-2 (NFS/SMB backing = REPLACE-ME). Adding a `class: bulk` + # volume now would leave the PVC (and therefore the pod) Pending. The media/downloads volume is + # therefore DEFERRED until OQ-2 is resolved; config-only on longhorn keeps Sonarr deployable today. +dependencies: + - to: qbittorrent # arr → download client (docs/13 dependency graph; R-NET-02 explicit allow) + port: 8080 + protocol: tcp + - to: jackett # arr → indexer proxy (docs/13 dependency graph; R-NET-02 explicit allow) + port: 9117 + protocol: tcp +ingressRoutes: + - host: sonarr.home.keithjackson.dev + port: 8989 # Sonarr web/API; TLS terminates at the shared Gateway (docs/08, docs/09) +auth: + mode: forward-auth # bare UI, no usable OIDC: oauth2-proxy → Keycloak at the Gateway (docs/15; R-IDP-02) + groups: + - media-users # authenticated AND in the media group (R-IDP-06) +update: + channel: stable # R-UPD-01 + range: ">=4.0.0 <5.0.0" # stay on the Sonarr v4 stable line + cadence: "0 4 * * *" + autoRollbackOnFailedHealth: true # R-UPD-03 +observability: + metrics: true + slis: + - up # arr down => alert (docs/10; R-OBS-01) +backup: + policy: daily-offcluster # Sonarr config/DB is irreplaceable, backed up off-cluster (docs/04; R-STATE-03)