diff --git a/apps/kustomization.yaml b/apps/kustomization.yaml index e991ff0..f0eeb12 100644 --- a/apps/kustomization.yaml +++ b/apps/kustomization.yaml @@ -8,3 +8,4 @@ kind: Kustomization resources: - pihole.yaml - gitea.yaml + - plex.yaml diff --git a/apps/plex.yaml b/apps/plex.yaml new file mode 100644 index 0000000..0eb3bff --- /dev/null +++ b/apps/plex.yaml @@ -0,0 +1,188 @@ +--- +# Namespace (trust zone; default-deny + policies apply) +apiVersion: v1 +kind: Namespace +metadata: + name: plex + 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: plex + 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 plex-config (R-STATE) +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: plex-config + namespace: plex + labels: + app.kubernetes.io/name: plex +spec: + accessModes: + - ReadWriteOnce + storageClassName: longhorn + resources: + requests: + storage: 20Gi +--- +# Deployment (R-PLAT-02) +apiVersion: apps/v1 +kind: Deployment +metadata: + name: plex + namespace: plex + labels: + app.kubernetes.io/name: plex +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: plex + template: + metadata: + labels: + app.kubernetes.io/name: plex + homelab.keithjackson.dev/egress-class: internet + homelab.keithjackson.dev/auth-mode: native + spec: + containers: + - name: plex + image: plexinc/pms-docker:1.41.3.9314-a0bfa8370 + ports: + - containerPort: 32400 + volumeMounts: + - name: config + mountPath: /data/config + volumes: + - name: config + persistentVolumeClaim: + claimName: plex-config +--- +# Service +apiVersion: v1 +kind: Service +metadata: + name: plex + namespace: plex + labels: + app.kubernetes.io/name: plex +spec: + selector: + app.kubernetes.io/name: plex + ports: + - name: port-32400 + port: 32400 + targetPort: 32400 +--- +# CiliumNetworkPolicy gateway-ingress allow (R-NET-01/02): plex-allow-gateway-ingress +apiVersion: cilium.io/v2 +kind: CiliumNetworkPolicy +metadata: + name: plex-allow-gateway-ingress + namespace: plex + labels: + app.kubernetes.io/name: plex +spec: + endpointSelector: + matchLabels: + app.kubernetes.io/name: plex + ingress: + - fromEntities: + - ingress + toPorts: + - ports: + - port: '32400' + protocol: TCP +--- +# HTTPRoute plex-32400 (R-TLS/R-DNS) +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: plex-32400 + namespace: plex + labels: + app.kubernetes.io/name: plex +spec: + parentRefs: + - name: shared + namespace: gateway + sectionName: https + hostnames: + - plex.home.keithjackson.dev + rules: + - matches: + - path: + type: PathPrefix + value: / + backendRefs: + - name: plex + port: 32400 +--- +# ServiceMonitor (R-OBS-01) +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + name: plex + namespace: plex + labels: + release: kube-prometheus-stack + app.kubernetes.io/name: plex +spec: + selector: + matchLabels: + app.kubernetes.io/name: plex + namespaceSelector: + matchNames: + - plex + endpoints: + - port: metrics + interval: 30s +--- +# PrometheusRule SLI alerts (R-OBS-01) +apiVersion: monitoring.coreos.com/v1 +kind: PrometheusRule +metadata: + name: plex-slis + namespace: plex + labels: + release: kube-prometheus-stack + app.kubernetes.io/name: plex +spec: + groups: + - name: plex.slis + rules: + - alert: plex-down + expr: up{job="plex"} == 0 + for: 5m + labels: + severity: critical + service: plex + annotations: + summary: plex is down + description: 'plex has been unreachable for 5m (SLI: up).' diff --git a/catalog/plex.yaml b/catalog/plex.yaml new file mode 100644 index 0000000..fccbf17 --- /dev/null +++ b/catalog/plex.yaml @@ -0,0 +1,53 @@ +# ServiceDefinition: plex +# Source of truth: spec docs/13-service-catalog.md, row 1 (the catalog's largest service). +# public | egress internet | config on Longhorn (metadata DB, DR-noncritical/reacquirable) | +# auth: native (Plex accounts, NOT Keycloak — docs/13 + docs/15 documented exception). +# Validates against schemas/servicedefinition.schema.json. +# +# BULK MEDIA (spec bulk-tier requirement, docs/04) — OPEN PREREQUISITE, intentionally NOT declared +# here yet (the requirement ID is cited in the WS-APPS report, not as a token here: this change does +# NOT exercise the bulk tier, so it must not be selected as a "touched" requirement whose in-cluster +# conformance test is still a Phase-1 gap). +# The Plex library belongs on the `bulk` tier (docs/04 / docs/13 cross-cutting note: metadata DB +# on replicated Longhorn, large library on bulk). The `bulk` StorageClass exists +# (platform/longhorn/storageclasses.yaml) and the schema+renderer both support `class: bulk`, but +# that class is a documented STUB: OQ-2 (external NFS/SMB backing) is UNRESOLVED and the NFS CSI +# driver is not installed, so a `bulk` PVC will not bind and would leave the Plex pod Pending. +# To onboard the running Plex shell now (compute + config + public exposure), the media volume is +# omitted. Once OQ-2 is resolved and `bulk` provisions, attach the library with a one-line add: +# storage: +# - name: media +# class: bulk # external mount; size omitted (sized outside the cluster) +# then re-render. See the WS-APPS report for the full bulk-media finding. +name: plex +kind: container +source: + image: plexinc/pms-docker:1.41.3.9314-a0bfa8370 # pinned recent stable build (verify tag at deploy) +exposure: public # edge port-forward + public DNS; clients + Seerr reach it (docs/09; R-DNS/R-NET) +egress: internet # normal WAN egress (docs/03; R-NET-05) +storage: + - name: config + size: 20Gi + class: longhorn # Plex DB/metadata: replicated app state on the 3-replica tier (docs/04; R-STATE-01/02) +dependencies: [] # default-deny: reaches only DNS; clients/Seerr reach IT (docs/03; R-NET-01/02) +ingressRoutes: + - host: plex.home.keithjackson.dev + port: 32400 # Plex web UI + API served on 32400 (wildcard TLS at the Gateway; docs/08/09) +auth: + mode: native # Plex account auth — documented exception, NOT Keycloak (docs/13/15; R-IDP-02) +update: + channel: stable # R-UPD-01 (never pre-release) + range: ">=1.40.0" # acceptable versions (advisory; image-automation off during onboarding) + cadence: "0 4 * * 0" # weekly stable check/apply + autoRollbackOnFailedHealth: true # R-UPD-03 +observability: + metrics: true + slis: + - up # R-OBS-01: service-down alert +backup: + # The config volume is on the longhorn (replicated, "irreplaceable") tier, so the L0 Kyverno + # backup policy (R-STATE-03) forbids `reacquirable` here — it must reference an off-cluster + # schedule. Plex's metadata DB (library index, thumbnails, watch state) is expensive to rebuild, + # so it is backed up off-cluster. The bulk library itself, once attached, is the `reacquirable` + # part (docs/04) and is NOT covered by this schedule. + policy: daily-offcluster # off-cluster backup of the longhorn config volume (docs/04; R-STATE-03)