diff --git a/apps/base/jellyfin/deployment.yaml b/apps/base/jellyfin/deployment.yaml new file mode 100644 index 0000000..e139b0b --- /dev/null +++ b/apps/base/jellyfin/deployment.yaml @@ -0,0 +1,97 @@ +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: jellyfin + namespace: jellyfin +spec: + # Single-replica for now. The jellyfin-ha fork adds the Redis transcode store + # and RWX transcode scratch that make scaling to true HA a follow-up. + replicas: 1 + strategy: + # Config PVC is RWO; Recreate avoids two pods contending for it. + type: Recreate + selector: + matchLabels: + app: jellyfin + template: + metadata: + labels: + app: jellyfin + spec: + securityContext: + fsGroup: 1000 + nodeSelector: + feature.node.kubernetes.io/pci-0300_8086.present: "true" + containers: + - name: jellyfin + image: git.unkin.net/unkin/jellyfin-ha:v0.1.0 + imagePullPolicy: IfNotPresent + ports: + - name: http + containerPort: 8096 + protocol: TCP + env: + - name: TZ + value: Australia/Sydney + - name: PUID + value: "1000" + - name: PGID + value: "1000" + - name: JELLYFIN_PublishedServerUrl + value: https://jellyfin.k8s.syd1.au.unkin.net + # Distributed transcode session store (jellyfin-ha additions). + - name: Jellyfin__TranscodeStore__RedisConnectionString + value: "jellyfin-redis:6379,abortConnect=false" + - name: Jellyfin__TranscodeStore__LeaseDurationSeconds + value: "30" + resources: + requests: + cpu: 100m + memory: 1Gi + limits: + cpu: "4" + memory: 8Gi + gpu.intel.com/i915: "1" + livenessProbe: + httpGet: + path: /health + port: http + initialDelaySeconds: 30 + periodSeconds: 30 + timeoutSeconds: 5 + failureThreshold: 3 + readinessProbe: + httpGet: + path: /health + port: http + initialDelaySeconds: 10 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 3 + volumeMounts: + - name: config + mountPath: /config + - name: cache + mountPath: /cache + - name: transcode + mountPath: /transcode + - name: media-library + mountPath: /mnt/movies + subPath: movies + - name: media-library + mountPath: /mnt/tvseries + subPath: tvseries + volumes: + - name: config + persistentVolumeClaim: + claimName: jellyfin-config + - name: cache + persistentVolumeClaim: + claimName: jellyfin-cache + - name: transcode + persistentVolumeClaim: + claimName: jellyfin-transcode + - name: media-library + persistentVolumeClaim: + claimName: media-library diff --git a/apps/base/jellyfin/gateway.yaml b/apps/base/jellyfin/gateway.yaml new file mode 100644 index 0000000..a491519 --- /dev/null +++ b/apps/base/jellyfin/gateway.yaml @@ -0,0 +1,37 @@ +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: Gateway +metadata: + labels: + traefik.io/instance: internal + annotations: + cert-manager.io/cluster-issuer: vault-issuer + cert-manager.io/common-name: jellyfin.k8s.syd1.au.unkin.net + cert-manager.io/private-key-size: "4096" + external-dns.alpha.kubernetes.io/hostname: jellyfin.k8s.syd1.au.unkin.net + external-dns.alpha.kubernetes.io/target: 198.18.200.4 + name: jellyfin + namespace: jellyfin +spec: + gatewayClassName: traefik-internal + listeners: + - allowedRoutes: + namespaces: + from: Same + hostname: jellyfin.k8s.syd1.au.unkin.net + name: http + port: 80 + protocol: HTTP + - allowedRoutes: + namespaces: + from: Same + hostname: jellyfin.k8s.syd1.au.unkin.net + name: https + port: 443 + protocol: HTTPS + tls: + certificateRefs: + - group: "" + kind: Secret + name: jellyfin-tls + mode: Terminate diff --git a/apps/base/jellyfin/httproute.yaml b/apps/base/jellyfin/httproute.yaml new file mode 100644 index 0000000..538947d --- /dev/null +++ b/apps/base/jellyfin/httproute.yaml @@ -0,0 +1,49 @@ +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: http-redirect + namespace: jellyfin +spec: + hostnames: + - jellyfin.k8s.syd1.au.unkin.net + parentRefs: + - group: gateway.networking.k8s.io + kind: Gateway + name: jellyfin + sectionName: http + rules: + - filters: + - type: RequestRedirect + requestRedirect: + scheme: https + statusCode: 301 + matches: + - path: + type: PathPrefix + value: / +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: jellyfin + namespace: jellyfin +spec: + hostnames: + - jellyfin.k8s.syd1.au.unkin.net + parentRefs: + - group: gateway.networking.k8s.io + kind: Gateway + name: jellyfin + sectionName: https + rules: + - backendRefs: + - group: "" + kind: Service + name: jellyfin + port: 8096 + weight: 1 + matches: + - path: + type: PathPrefix + value: / diff --git a/apps/base/jellyfin/kustomization.yaml b/apps/base/jellyfin/kustomization.yaml new file mode 100644 index 0000000..ba9b0f8 --- /dev/null +++ b/apps/base/jellyfin/kustomization.yaml @@ -0,0 +1,18 @@ +--- +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: + - namespace.yaml + - pv_media-library.yaml + - pvc_media-library.yaml + - pvc_config.yaml + - pvc_cache.yaml + - pvc_transcode.yaml + - deployment.yaml + - service.yaml + - redis-deployment.yaml + - redis-service.yaml + - redis-pvc.yaml + - gateway.yaml + - httproute.yaml diff --git a/apps/base/jellyfin/namespace.yaml b/apps/base/jellyfin/namespace.yaml new file mode 100644 index 0000000..3122e11 --- /dev/null +++ b/apps/base/jellyfin/namespace.yaml @@ -0,0 +1,5 @@ +--- +apiVersion: v1 +kind: Namespace +metadata: + name: jellyfin diff --git a/apps/base/jellyfin/pv_media-library.yaml b/apps/base/jellyfin/pv_media-library.yaml new file mode 100644 index 0000000..463e5ab --- /dev/null +++ b/apps/base/jellyfin/pv_media-library.yaml @@ -0,0 +1,41 @@ +--- +# Static CephFS PersistentVolume bound to the pre-existing, ACTIVELY-USED +# puppet media library (ceph filesystem "mediafs", mounted by the VM/incus +# instances at /shared/media). ceph-csi only mounts this volume; staticVolume +# tells it the storage pre-exists and it must never provision or delete it. +# +# reclaimPolicy MUST stay Retain: deleting this PV or its PVC must NEVER be able +# to reclaim or destroy the underlying CephFS data that the VM instances use. +apiVersion: v1 +kind: PersistentVolume +metadata: + name: jellyfin-media-library +spec: + accessModes: + - ReadWriteMany + capacity: + storage: 10Ti + # Load-bearing safety control. Do not change to Delete. + persistentVolumeReclaimPolicy: Retain + storageClassName: "" + volumeMode: Filesystem + # Pre-bind to the media-library claim so nothing else can grab this PV. + claimRef: + apiVersion: v1 + kind: PersistentVolumeClaim + name: media-library + namespace: jellyfin + csi: + driver: cephfs.csi.ceph.com + volumeHandle: jellyfin-media-library-static + nodeStageSecretRef: + name: csi-cephfs-secret + namespace: csi-cephfs + volumeAttributes: + # clusterID maps (in the csi-cephfs ceph-csi-config) to the mon set that + # also serves mediafs; for a static volume only the mon lookup is used. + clusterID: cephfs_csi_ssd_ec_4_1 + fsName: mediafs + staticVolume: "true" + # Filesystem-internal root of the library (mediafs root == /shared/media). + rootPath: / diff --git a/apps/base/jellyfin/pvc_cache.yaml b/apps/base/jellyfin/pvc_cache.yaml new file mode 100644 index 0000000..26a78b8 --- /dev/null +++ b/apps/base/jellyfin/pvc_cache.yaml @@ -0,0 +1,15 @@ +--- +# Local transcode/image cache. Scratch, delete reclaim policy. +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: jellyfin-cache + namespace: jellyfin +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 200Gi + storageClassName: cephrbd-fast-delete + volumeMode: Filesystem diff --git a/apps/base/jellyfin/pvc_config.yaml b/apps/base/jellyfin/pvc_config.yaml new file mode 100644 index 0000000..71467d0 --- /dev/null +++ b/apps/base/jellyfin/pvc_config.yaml @@ -0,0 +1,15 @@ +--- +# Jellyfin config + SQLite library database. Single-writer, block storage. +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: jellyfin-config + namespace: jellyfin +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 10Gi + storageClassName: cephrbd-fast-retain + volumeMode: Filesystem diff --git a/apps/base/jellyfin/pvc_media-library.yaml b/apps/base/jellyfin/pvc_media-library.yaml new file mode 100644 index 0000000..2e100ce --- /dev/null +++ b/apps/base/jellyfin/pvc_media-library.yaml @@ -0,0 +1,19 @@ +--- +# Claim bound to the static mediafs PV. RWX so every app in the stack shares the +# one library. storageClassName "" + volumeName pin it to the static PV (no +# dynamic provisioning). Deleting this claim cannot reclaim the data (PV is +# Retain). +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: media-library + namespace: jellyfin +spec: + accessModes: + - ReadWriteMany + storageClassName: "" + volumeName: jellyfin-media-library + resources: + requests: + storage: 10Ti + volumeMode: Filesystem diff --git a/apps/base/jellyfin/pvc_transcode.yaml b/apps/base/jellyfin/pvc_transcode.yaml new file mode 100644 index 0000000..4f8c891 --- /dev/null +++ b/apps/base/jellyfin/pvc_transcode.yaml @@ -0,0 +1,17 @@ +--- +# Shared transcode scratch. ReadWriteMany is the hard requirement for the HA +# fork: a taking-over pod must read the in-flight HLS segments written by the +# pod it replaces. Scratch data, so delete reclaim policy. +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: jellyfin-transcode + namespace: jellyfin +spec: + accessModes: + - ReadWriteMany + resources: + requests: + storage: 100Gi + storageClassName: cephfs-raid5-delete + volumeMode: Filesystem diff --git a/apps/base/jellyfin/redis-deployment.yaml b/apps/base/jellyfin/redis-deployment.yaml new file mode 100644 index 0000000..4a61e3c --- /dev/null +++ b/apps/base/jellyfin/redis-deployment.yaml @@ -0,0 +1,64 @@ +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: jellyfin-redis + namespace: jellyfin +spec: + replicas: 1 + strategy: + type: Recreate + selector: + matchLabels: + app: jellyfin-redis + template: + metadata: + labels: + app: jellyfin-redis + spec: + restartPolicy: Always + containers: + - name: redis + image: redis:7-alpine + imagePullPolicy: IfNotPresent + command: + - redis-server + - --save + - "20" + - "1" + ports: + - name: redis + containerPort: 6379 + protocol: TCP + livenessProbe: + exec: + command: + - redis-cli + - ping + initialDelaySeconds: 30 + periodSeconds: 30 + timeoutSeconds: 5 + failureThreshold: 3 + readinessProbe: + exec: + command: + - redis-cli + - ping + initialDelaySeconds: 5 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 3 + resources: + requests: + cpu: 50m + memory: 128Mi + limits: + cpu: 500m + memory: 512Mi + volumeMounts: + - name: data + mountPath: /data + volumes: + - name: data + persistentVolumeClaim: + claimName: jellyfin-redis-data diff --git a/apps/base/jellyfin/redis-pvc.yaml b/apps/base/jellyfin/redis-pvc.yaml new file mode 100644 index 0000000..36a4315 --- /dev/null +++ b/apps/base/jellyfin/redis-pvc.yaml @@ -0,0 +1,14 @@ +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: jellyfin-redis-data + namespace: jellyfin +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 5Gi + storageClassName: cephrbd-fast-delete + volumeMode: Filesystem diff --git a/apps/base/jellyfin/redis-service.yaml b/apps/base/jellyfin/redis-service.yaml new file mode 100644 index 0000000..2867750 --- /dev/null +++ b/apps/base/jellyfin/redis-service.yaml @@ -0,0 +1,17 @@ +--- +apiVersion: v1 +kind: Service +metadata: + name: jellyfin-redis + namespace: jellyfin +spec: + type: ClusterIP + internalTrafficPolicy: Cluster + sessionAffinity: None + selector: + app: jellyfin-redis + ports: + - name: redis + port: 6379 + targetPort: redis + protocol: TCP diff --git a/apps/base/jellyfin/service.yaml b/apps/base/jellyfin/service.yaml new file mode 100644 index 0000000..65b86d4 --- /dev/null +++ b/apps/base/jellyfin/service.yaml @@ -0,0 +1,17 @@ +--- +apiVersion: v1 +kind: Service +metadata: + name: jellyfin + namespace: jellyfin +spec: + type: ClusterIP + internalTrafficPolicy: Cluster + sessionAffinity: None + selector: + app: jellyfin + ports: + - name: http + port: 8096 + targetPort: http + protocol: TCP diff --git a/apps/overlays/au-syd1/jellyfin/kustomization.yaml b/apps/overlays/au-syd1/jellyfin/kustomization.yaml new file mode 100644 index 0000000..d8d5df6 --- /dev/null +++ b/apps/overlays/au-syd1/jellyfin/kustomization.yaml @@ -0,0 +1,6 @@ +--- +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: + - ../../../base/jellyfin diff --git a/argocd/applicationsets/kustomization.yaml b/argocd/applicationsets/kustomization.yaml index b54cc35..c95dedd 100644 --- a/argocd/applicationsets/kustomization.yaml +++ b/argocd/applicationsets/kustomization.yaml @@ -5,6 +5,7 @@ kind: Kustomization resources: - aitooling.yaml - logging.yaml + - media.yaml - observability.yaml - platform.yaml - storage.yaml diff --git a/argocd/applicationsets/media.yaml b/argocd/applicationsets/media.yaml new file mode 100644 index 0000000..f600938 --- /dev/null +++ b/argocd/applicationsets/media.yaml @@ -0,0 +1,32 @@ +--- +apiVersion: argoproj.io/v1alpha1 +kind: ApplicationSet +metadata: + name: media-apps + namespace: argocd +spec: + generators: + - git: + repoURL: https://git.unkin.net/unkin/argocd-apps + revision: HEAD + directories: + # jellyfin only for now; downloads/managers apps join in later PRs. + - path: apps/overlays/*/jellyfin + template: + metadata: + name: 'media-{{path[3]}}' + spec: + project: media + source: + repoURL: https://git.unkin.net/unkin/argocd-apps + targetRevision: HEAD + path: '{{path}}' + destination: + server: https://kubernetes.default.svc + namespace: jellyfin + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - ServerSideApply=true diff --git a/argocd/projects/kustomization.yaml b/argocd/projects/kustomization.yaml index b54cc35..c95dedd 100644 --- a/argocd/projects/kustomization.yaml +++ b/argocd/projects/kustomization.yaml @@ -5,6 +5,7 @@ kind: Kustomization resources: - aitooling.yaml - logging.yaml + - media.yaml - observability.yaml - platform.yaml - storage.yaml diff --git a/argocd/projects/media.yaml b/argocd/projects/media.yaml new file mode 100644 index 0000000..4bb0421 --- /dev/null +++ b/argocd/projects/media.yaml @@ -0,0 +1,21 @@ +--- +apiVersion: argoproj.io/v1alpha1 +kind: AppProject +metadata: + name: media + namespace: argocd +spec: + description: Media services (jellyfin; downloads/managers namespaces to follow) + sourceRepos: + - https://git.unkin.net/unkin/argocd-apps + destinations: + - namespace: jellyfin + server: https://kubernetes.default.svc + clusterResourceWhitelist: + - group: '' + kind: Namespace + - group: '' + kind: PersistentVolume + namespaceResourceWhitelist: + - group: '*' + kind: '*'