arrstack,fafflix,cheeztv: provision shared mediastore CephFS volume (#428)

## Why

The media estate is currently split across two 1Ti CephFS subvolumes (`media-tv`
and `media-movies`), so a download that lands in one and imports to the other is
a cross-filesystem copy, not a hardlink move. A single 10Ti `mediastore`
subvolume (already created on `cephfs_csi_ssd_ec_4_1`) collapses that: one
filesystem holding `/{fafflix,cheeztv}/{tvseries,movies}` plus
`/nzbget/downloads/complete`, so every arr import is an atomic same-fs hardlink.

This PR only provisions the volume and seeds its tree. No existing PV, PVC or
workload is touched, and nothing mounts the new claims yet — the cutover of the
arrs and the two jellyfins is a separate change.

## How

- Add static PVs `arrstack-mediastore`, `fafflix-mediastore` and
  `cheeztv-mediastore`, all pointing at the same rootPath
  (`/volumes/csi_ssd_ec_4_1/mediastore/a0152dac-…`) with unique names and
  volumeHandles pinned by `claimRef` — the established pattern for the shared
  media subvolumes.
- Add the matching RWX 10Ti PVCs (`mediastore` in arrstack,
  `fafflix-mediastore`, `cheeztv-mediastore`), annotated
  `k8up.io/backup: "false"` and statically bound via `volumeName` +
  `storageClassName: ""`.
- Add `mediastore-bootstrap`, a one-shot ArgoCD Sync-hook Job
  (`hook-delete-policy: BeforeHookCreation`, no sync-wave needed) that mounts
  the arrstack claim and `mkdir -p`s the directory tree as uid/gid 1000 —
  the uid the arrstack media pods run as. Idempotent, so it self-heals on
  every sync.
- Wire the new manifests into the arrstack, fafflix and cheeztv bases.

## Validation

- `kustomize build` clean on `apps/overlays/au-syd1/{arrstack,fafflix,cheeztv}`
- `kubeconform` clean on all three overlays (91 / 29 / 32 resources valid)
- pre-commit (yamllint, check-yaml, no-plain-secrets) passed

Reviewed-on: #428
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
This commit was merged in pull request #428.
This commit is contained in:
2026-08-29 21:18:09 +10:00
committed by BenVincent
parent 9616e0e2b3
commit e10f0c09c9
10 changed files with 245 additions and 0 deletions
+3
View File
@@ -8,9 +8,12 @@ resources:
- pv-media-tv.yaml - pv-media-tv.yaml
- pv-media-movies.yaml - pv-media-movies.yaml
- pv-mediafs.yaml - pv-mediafs.yaml
- pv-mediastore.yaml
- pvc-media-tv.yaml - pvc-media-tv.yaml
- pvc-media-movies.yaml - pvc-media-movies.yaml
- pvc-mediafs.yaml - pvc-mediafs.yaml
- pvc-mediastore.yaml
- mediastore-bootstrap-job.yaml
- media-bucket.yaml - media-bucket.yaml
- backups-bucket.yaml - backups-bucket.yaml
- postgres - postgres
@@ -0,0 +1,74 @@
---
# Seeds the directory skeleton on the freshly created mediastore subvolume so
# the arrs, nzbget and both jellyfins mount subPaths that already exist and are
# owned by uid/gid 1000 (the uid every arrstack media pod runs as). mkdir -p is
# idempotent, so re-running it on every sync is harmless and self-heals a tree
# someone deleted by hand.
#
# Sync hook with BeforeHookCreation delete: ArgoCD replaces the completed Job
# each sync instead of failing on the immutable pod template. No sync-wave is
# needed -- the PVC applies in the same wave and the pod simply stays Pending
# until it binds.
apiVersion: batch/v1
kind: Job
metadata:
name: mediastore-bootstrap
namespace: arrstack
annotations:
argocd.argoproj.io/hook: Sync
argocd.argoproj.io/hook-delete-policy: BeforeHookCreation
spec:
backoffLimit: 6
ttlSecondsAfterFinished: 600
template:
metadata:
labels:
app: mediastore-bootstrap
spec:
serviceAccountName: default
automountServiceAccountToken: false
restartPolicy: Never
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
fsGroupChangePolicy: OnRootMismatch
seccompProfile:
type: RuntimeDefault
containers:
- name: mkdir
image: docker.io/library/alpine:3
imagePullPolicy: IfNotPresent
command:
- sh
- -c
- |
set -eu
mkdir -p \
/media/fafflix/tvseries \
/media/fafflix/movies \
/media/cheeztv/tvseries \
/media/cheeztv/movies \
/media/nzbget/downloads/complete
ls -la /media
volumeMounts:
- name: mediastore
mountPath: /media
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
resources:
requests:
cpu: 10m
memory: 32Mi
limits:
cpu: 200m
memory: 128Mi
volumes:
- name: mediastore
persistentVolumeClaim:
claimName: mediastore
+32
View File
@@ -0,0 +1,32 @@
---
# Static PV for the shared MEDIASTORE CephFS subvolume: one 10Ti filesystem
# holding every library plus the nzbget download tree, so arr imports are
# same-filesystem hardlink moves across tv AND movies. Same rootPath as the
# fafflix/cheeztv mediastore PVs; each namespace gets its own PV (unique name +
# volumeHandle) pinned by claimRef.
apiVersion: v1
kind: PersistentVolume
metadata:
name: arrstack-mediastore
spec:
capacity:
storage: 10Ti
accessModes:
- ReadWriteMany
persistentVolumeReclaimPolicy: Retain
storageClassName: ""
volumeMode: Filesystem
claimRef:
namespace: arrstack
name: mediastore
csi:
driver: cephfs.csi.ceph.com
volumeHandle: arrstack-mediastore-static
nodeStageSecretRef:
name: csi-cephfs-secret
namespace: csi-cephfs
volumeAttributes:
staticVolume: "true"
clusterID: cephfs_csi_ssd_ec_4_1
fsName: cephfs
rootPath: /volumes/csi_ssd_ec_4_1/mediastore/a0152dac-a51b-4b95-ac5e-ecdd99bfe3f1
+22
View File
@@ -0,0 +1,22 @@
---
# Whole media tree (/fafflix, /cheeztv, /nzbget) on one RWX filesystem, shared
# across the sonarr/radarr/nzbget pods. Statically bound to the
# arrstack-mediastore PV (the same CephFS subvolume fafflix and cheeztv mount).
# storageClassName "" + volumeName disables dynamic provisioning and binds the
# pre-created static PV.
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: mediastore
namespace: arrstack
annotations:
k8up.io/backup: "false"
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 10Ti
storageClassName: ""
volumeName: arrstack-mediastore
volumeMode: Filesystem
+2
View File
@@ -15,8 +15,10 @@ resources:
- pvc-transcode.yaml - pvc-transcode.yaml
- pv-media-tv.yaml - pv-media-tv.yaml
- pv-media-movies.yaml - pv-media-movies.yaml
- pv-mediastore.yaml
- pvc-media-tv.yaml - pvc-media-tv.yaml
- pvc-media-movies.yaml - pvc-media-movies.yaml
- pvc-mediastore.yaml
- statefulset.yaml - statefulset.yaml
- plugin-configmap.yaml - plugin-configmap.yaml
- pdb.yaml - pdb.yaml
+31
View File
@@ -0,0 +1,31 @@
---
# Static PV for the shared MEDIASTORE CephFS subvolume. Same rootPath as
# arrstack's mediastore PV so the arrs write and cheeztv reads the identical
# library tree (cheeztv scans /cheeztv/{tvseries,movies}); each namespace gets
# its own PV (unique name + volumeHandle) pinned by claimRef.
apiVersion: v1
kind: PersistentVolume
metadata:
name: cheeztv-mediastore
spec:
capacity:
storage: 10Ti
accessModes:
- ReadWriteMany
persistentVolumeReclaimPolicy: Retain
storageClassName: ""
volumeMode: Filesystem
claimRef:
namespace: cheeztv
name: cheeztv-mediastore
csi:
driver: cephfs.csi.ceph.com
volumeHandle: cheeztv-mediastore-static
nodeStageSecretRef:
name: csi-cephfs-secret
namespace: csi-cephfs
volumeAttributes:
staticVolume: "true"
clusterID: cephfs_csi_ssd_ec_4_1
fsName: cephfs
rootPath: /volumes/csi_ssd_ec_4_1/mediastore/a0152dac-a51b-4b95-ac5e-ecdd99bfe3f1
+24
View File
@@ -0,0 +1,24 @@
---
# Shared media tree, read-many across replicas. Statically bound to the
# cheeztv-mediastore PV (the CephFS subvolume also used by arrstack and
# fafflix). storageClassName "" + volumeName disables dynamic provisioning and
# binds the pre-created static PV.
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: cheeztv-mediastore
namespace: cheeztv
annotations:
# Exclude from the cheeztv-config k8up Schedule (skipWithoutAnnotation is
# false cluster-wide, so unannotated PVCs are swept in). Only cheeztv-config
# is backed up; the media library is not restic-backup material.
k8up.io/backup: "false"
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 10Ti
storageClassName: ""
volumeName: cheeztv-mediastore
volumeMode: Filesystem
+2
View File
@@ -15,8 +15,10 @@ resources:
- pvc-transcode.yaml - pvc-transcode.yaml
- pv-media-tv.yaml - pv-media-tv.yaml
- pv-media-movies.yaml - pv-media-movies.yaml
- pv-mediastore.yaml
- pvc-media-tv.yaml - pvc-media-tv.yaml
- pvc-media-movies.yaml - pvc-media-movies.yaml
- pvc-mediastore.yaml
- statefulset.yaml - statefulset.yaml
- plugin-configmap.yaml - plugin-configmap.yaml
- pdb.yaml - pdb.yaml
+31
View File
@@ -0,0 +1,31 @@
---
# Static PV for the shared MEDIASTORE CephFS subvolume. Same rootPath as
# arrstack's mediastore PV so the arrs write and fafflix reads the identical
# library tree (fafflix scans /fafflix/{tvseries,movies}); each namespace gets
# its own PV (unique name + volumeHandle) pinned by claimRef.
apiVersion: v1
kind: PersistentVolume
metadata:
name: fafflix-mediastore
spec:
capacity:
storage: 10Ti
accessModes:
- ReadWriteMany
persistentVolumeReclaimPolicy: Retain
storageClassName: ""
volumeMode: Filesystem
claimRef:
namespace: fafflix
name: fafflix-mediastore
csi:
driver: cephfs.csi.ceph.com
volumeHandle: fafflix-mediastore-static
nodeStageSecretRef:
name: csi-cephfs-secret
namespace: csi-cephfs
volumeAttributes:
staticVolume: "true"
clusterID: cephfs_csi_ssd_ec_4_1
fsName: cephfs
rootPath: /volumes/csi_ssd_ec_4_1/mediastore/a0152dac-a51b-4b95-ac5e-ecdd99bfe3f1
+24
View File
@@ -0,0 +1,24 @@
---
# Shared media tree, read-many across replicas. Statically bound to the
# fafflix-mediastore PV (the CephFS subvolume also used by arrstack and
# cheeztv). storageClassName "" + volumeName disables dynamic provisioning and
# binds the pre-created static PV.
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: fafflix-mediastore
namespace: fafflix
annotations:
# Exclude from the fafflix-config k8up Schedule (skipWithoutAnnotation is
# false cluster-wide, so unannotated PVCs are swept in). Only fafflix-config
# is backed up; the media library is not restic-backup material.
k8up.io/backup: "false"
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 10Ti
storageClassName: ""
volumeName: fafflix-mediastore
volumeMode: Filesystem