cdaab736b5
The deployed v0.3.3 build returns 500 from /Shows/NextUp on PostgreSQL, breaking the home screen, and lets replicas diverge: library-visibility and shared-config changes never propagate, user data (resume, played state, favourites, ratings) is overwritten between pods, and eight scheduled tasks run on every replica instead of only the scan leader. v0.4.0 carries the fixes. - Pin cheeztv and fafflix to jellyfin-ha:v0.4.0. No config change needed: cross-pod invalidation reuses the transcode-store Redis connection string both apps already set. Reviewed-on: #483 Co-authored-by: unkin-agent <unkin-agent@unkin.net> Co-committed-by: unkin-agent <unkin-agent@unkin.net>
324 lines
13 KiB
YAML
324 lines
13 KiB
YAML
---
|
|
apiVersion: apps/v1
|
|
kind: StatefulSet
|
|
metadata:
|
|
name: fafflix
|
|
namespace: fafflix
|
|
annotations:
|
|
configmap.reloader.stakater.com/auto: "true"
|
|
spec:
|
|
# HA: two replicas coordinate transcode session ownership through Valkey and
|
|
# resume each other's HLS segments off the shared RWX transcode PVC. Stable
|
|
# pod names (fafflix-0/1) are the lease owner identity, hence StatefulSet.
|
|
replicas: 2
|
|
serviceName: fafflix
|
|
podManagementPolicy: Parallel
|
|
updateStrategy:
|
|
type: RollingUpdate
|
|
selector:
|
|
matchLabels:
|
|
app: fafflix
|
|
template:
|
|
metadata:
|
|
labels:
|
|
app: fafflix
|
|
spec:
|
|
securityContext:
|
|
# Group-write the shared RWX volumes and grant the render/video groups so
|
|
# the runAsUser 1000 process can open the Intel DRI render node injected
|
|
# by the device plugin.
|
|
fsGroup: 1000
|
|
supplementalGroups:
|
|
- 44
|
|
- 105
|
|
- 109
|
|
seccompProfile:
|
|
type: RuntimeDefault
|
|
affinity:
|
|
# Spread the two replicas across nodes for node-level HA. Soft so a
|
|
# single-GPU-node cluster still schedules both (i915 has 4 shared slots).
|
|
podAntiAffinity:
|
|
preferredDuringSchedulingIgnoredDuringExecution:
|
|
- weight: 100
|
|
podAffinityTerm:
|
|
labelSelector:
|
|
matchLabels:
|
|
app: fafflix
|
|
topologyKey: kubernetes.io/hostname
|
|
initContainers:
|
|
# Seed the fork's PostgreSQL provider (database.xml) and Intel iGPU
|
|
# hardware transcode settings (encoding.xml) before Jellyfin starts.
|
|
# Runs as root to chown into the shared config volume; mirrors the fork
|
|
# Helm chart's inject-db-config. Each file is written only when absent so
|
|
# admin changes persisted to the shared RWX /config survive pod restarts.
|
|
- name: inject-config
|
|
image: busybox:1.37.0
|
|
command:
|
|
- sh
|
|
- -c
|
|
- |
|
|
mkdir -p /config/config
|
|
chown 1000:1000 /config/config
|
|
chmod 775 /config/config
|
|
if [ ! -e /config/config/database.xml ]; then
|
|
cat > /config/config/database.xml << 'DBEOF'
|
|
<?xml version="1.0" encoding="utf-8"?>
|
|
<DatabaseConfigurationOptions>
|
|
<DatabaseType>Jellyfin-PostgreSQL</DatabaseType>
|
|
<LockingBehavior>NoLock</LockingBehavior>
|
|
</DatabaseConfigurationOptions>
|
|
DBEOF
|
|
chown 1000:1000 /config/config/database.xml
|
|
chmod 664 /config/config/database.xml
|
|
fi
|
|
# VAAPI on the Intel render node the device plugin injects
|
|
# (/dev/dri/renderD128 — ffmpeg's default DRM node, reachable via
|
|
# the render/video supplementalGroups). Without this the attached
|
|
# iGPU is idle and every transcode runs in software. Omitted
|
|
# elements fall back to the fork's EncodingOptions defaults.
|
|
if [ ! -e /config/config/encoding.xml ]; then
|
|
cat > /config/config/encoding.xml << 'ENCEOF'
|
|
<?xml version="1.0" encoding="utf-8"?>
|
|
<EncodingOptions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
|
|
<EncodingThreadCount>-1</EncodingThreadCount>
|
|
<HardwareAccelerationType>vaapi</HardwareAccelerationType>
|
|
<VaapiDevice>/dev/dri/renderD128</VaapiDevice>
|
|
<EnableHardwareEncoding>true</EnableHardwareEncoding>
|
|
<AllowHevcEncoding>true</AllowHevcEncoding>
|
|
<AllowAv1Encoding>false</AllowAv1Encoding>
|
|
<EnableIntelLowPowerH264HwEncoder>false</EnableIntelLowPowerH264HwEncoder>
|
|
<EnableIntelLowPowerHevcHwEncoder>false</EnableIntelLowPowerHevcHwEncoder>
|
|
<EnableTonemapping>false</EnableTonemapping>
|
|
<EnableVppTonemapping>false</EnableVppTonemapping>
|
|
<HardwareDecodingCodecs>
|
|
<string>h264</string>
|
|
<string>hevc</string>
|
|
<string>vc1</string>
|
|
<string>vp9</string>
|
|
</HardwareDecodingCodecs>
|
|
</EncodingOptions>
|
|
ENCEOF
|
|
chown 1000:1000 /config/config/encoding.xml
|
|
chmod 664 /config/config/encoding.xml
|
|
fi
|
|
resources:
|
|
requests:
|
|
cpu: 10m
|
|
memory: 32Mi
|
|
limits:
|
|
cpu: 100m
|
|
memory: 64Mi
|
|
volumeMounts:
|
|
- name: config
|
|
mountPath: /config
|
|
# Render the SSO/LDAP plugin configs into the shared config volume,
|
|
# substituting the client secret and LDAP bind password from the
|
|
# VSO-synced oauth-credentials Secret (never committed). Plugin configs
|
|
# are fully managed here so they are overwritten every start; the login
|
|
# button branding is written only when absent so admin edits survive.
|
|
- name: inject-plugin-config
|
|
image: busybox:1.37.0
|
|
command:
|
|
- sh
|
|
- -c
|
|
- |
|
|
mkdir -p /config/plugins/configurations /config/config
|
|
chown 1000:1000 /config/plugins /config/plugins/configurations /config/config
|
|
esc() { printf '%s' "$1" | sed -e 's/[&|\\]/\\&/g'; }
|
|
cs=$(esc "${CLIENT_SECRET}")
|
|
lp=$(esc "${LDAP_BIND_PASSWORD}")
|
|
sed "s|@@CLIENT_SECRET@@|${cs}|" /templates/SSO-Auth.xml > /config/plugins/configurations/SSO-Auth.xml
|
|
sed "s|@@LDAP_BIND_PASSWORD@@|${lp}|" /templates/LDAP-Auth.xml > /config/plugins/configurations/LDAP-Auth.xml
|
|
chown 1000:1000 /config/plugins/configurations/SSO-Auth.xml /config/plugins/configurations/LDAP-Auth.xml
|
|
chmod 600 /config/plugins/configurations/SSO-Auth.xml /config/plugins/configurations/LDAP-Auth.xml
|
|
if [ ! -e /config/config/branding.xml ]; then
|
|
cp /templates/branding.xml /config/config/branding.xml
|
|
chown 1000:1000 /config/config/branding.xml
|
|
chmod 664 /config/config/branding.xml
|
|
fi
|
|
env:
|
|
- name: CLIENT_SECRET
|
|
valueFrom:
|
|
secretKeyRef:
|
|
name: oauth-credentials
|
|
key: client_secret
|
|
optional: true
|
|
- name: LDAP_BIND_PASSWORD
|
|
valueFrom:
|
|
secretKeyRef:
|
|
name: oauth-credentials
|
|
key: ldap_bind_password
|
|
optional: true
|
|
resources:
|
|
requests:
|
|
cpu: 10m
|
|
memory: 32Mi
|
|
limits:
|
|
cpu: 100m
|
|
memory: 64Mi
|
|
volumeMounts:
|
|
- name: config
|
|
mountPath: /config
|
|
- name: plugin-config
|
|
mountPath: /templates
|
|
readOnly: true
|
|
containers:
|
|
- name: fafflix
|
|
image: artifactapi.k8s.syd1.au.unkin.net/docker-internal/jellyfin-ha:v0.4.0
|
|
imagePullPolicy: IfNotPresent
|
|
ports:
|
|
- name: http
|
|
containerPort: 8096
|
|
protocol: TCP
|
|
env:
|
|
# Pod identity for the Valkey transcode lease owner. The fork reads
|
|
# JELLYFIN_INSTANCE_ID (falling back to MachineName); the stable
|
|
# StatefulSet pod name gives each replica a unique lease identity so
|
|
# takeover can target a dead replica. JELLYFIN_HA_POD_NAME is set for
|
|
# parity with the fork Helm chart (nothing currently reads it).
|
|
- name: JELLYFIN_INSTANCE_ID
|
|
valueFrom:
|
|
fieldRef:
|
|
fieldPath: metadata.name
|
|
- name: JELLYFIN_HA_POD_NAME
|
|
valueFrom:
|
|
fieldRef:
|
|
fieldPath: metadata.name
|
|
# Multiple replicas must not each answer UDP auto-discovery.
|
|
- name: JELLYFIN_Network__AutoDiscovery
|
|
value: "false"
|
|
# Config dir must differ from the data root (Jellyfin sanity check).
|
|
- name: JELLYFIN_CONFIG_DIR
|
|
value: /config/config
|
|
# Distributed transcode session store (fafflix-ha additions).
|
|
- name: Jellyfin__TranscodeStore__RedisConnectionString
|
|
value: "valkey-fafflix-valkey:6379,abortConnect=false"
|
|
- name: Jellyfin__TranscodeStore__LeaseDurationSeconds
|
|
value: "30"
|
|
# PostgreSQL main DB via the CNPG-generated app secret, routed through
|
|
# the PgBouncer pooler. Composed with $(VAR) expansion so the password
|
|
# is never rendered into the manifest; CNPG passwords are URL-safe.
|
|
- name: PGUSER
|
|
valueFrom:
|
|
secretKeyRef:
|
|
name: fafflix-postgres-app
|
|
key: username
|
|
- name: PGPASSWORD
|
|
valueFrom:
|
|
secretKeyRef:
|
|
name: fafflix-postgres-app
|
|
key: password
|
|
- name: PGDB
|
|
valueFrom:
|
|
secretKeyRef:
|
|
name: fafflix-postgres-app
|
|
key: dbname
|
|
- name: POSTGRES_CONNECTION_STRING
|
|
value: "postgresql://$(PGUSER):$(PGPASSWORD)@fafflix-postgres-pooler:5432/$(PGDB)"
|
|
- name: DATABASE_URL
|
|
value: "postgresql://$(PGUSER):$(PGPASSWORD)@fafflix-postgres-pooler:5432/$(PGDB)"
|
|
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
|
|
resources:
|
|
requests:
|
|
cpu: "1"
|
|
memory: 1Gi
|
|
gpu.intel.com/i915: "1"
|
|
limits:
|
|
cpu: "4"
|
|
memory: 6Gi
|
|
# Intel iGPU (QSV/VA-API) slot. Requesting it pins the pod to a
|
|
# GPU-labelled node and injects /dev/dri/renderD* automatically, so
|
|
# no /dev/dri hostPath or privileged container is needed. VA-API is
|
|
# pre-enabled via the seeded encoding.xml (see inject-config), so
|
|
# transcodes use the iGPU on first boot with no manual UI step.
|
|
gpu.intel.com/i915: "1"
|
|
securityContext:
|
|
runAsUser: 1000
|
|
runAsGroup: 1000
|
|
volumeMounts:
|
|
- name: config
|
|
mountPath: /config
|
|
- name: transcode
|
|
# Fork's real transcode temp path. RWX so a surviving pod reads the
|
|
# in-flight .ts/.m3u8 segments of the pod it takes over. A per-pod
|
|
# volume here silently breaks HA takeover.
|
|
mountPath: /config/transcodes
|
|
- name: cache
|
|
mountPath: /cache
|
|
- name: media-tv
|
|
# Adult instance: mount the tvshows/adult subtree of the shared TV
|
|
# subvolume (subPath adult) as fafflix's primary TV library. Same
|
|
# rootPath the cheeztv instance reads under subPath kids, so an
|
|
# episode resolves identically across instances.
|
|
mountPath: /media/tv
|
|
subPath: adult
|
|
readOnly: true
|
|
- name: media-tv
|
|
# Also mount the kids TV subtree (subPath kids) so fafflix can
|
|
# resume playback of kids content started on cheeztv — same
|
|
# underlying subvolume, different subtree, distinct mount path.
|
|
mountPath: /media/tv-kids
|
|
subPath: kids
|
|
readOnly: true
|
|
- name: media-movies
|
|
# Adult instance: mount the movies/adult subtree of the shared
|
|
# movies subvolume (subPath adult) as fafflix's primary movie
|
|
# library.
|
|
mountPath: /media/movies
|
|
subPath: adult
|
|
readOnly: true
|
|
- name: media-movies
|
|
# Also mount the kids movies subtree (subPath kids) for cross-resume
|
|
# of kids content started on cheeztv.
|
|
mountPath: /media/movies-kids
|
|
subPath: kids
|
|
readOnly: true
|
|
volumes:
|
|
- name: plugin-config
|
|
configMap:
|
|
name: fafflix-plugin-config
|
|
- name: config
|
|
persistentVolumeClaim:
|
|
claimName: fafflix-config
|
|
- name: transcode
|
|
persistentVolumeClaim:
|
|
claimName: fafflix-transcode
|
|
- name: media-tv
|
|
persistentVolumeClaim:
|
|
claimName: fafflix-media-tv
|
|
- name: media-movies
|
|
persistentVolumeClaim:
|
|
claimName: fafflix-media-movies
|
|
volumeClaimTemplates:
|
|
# Per-pod scratch cache — RWO, disposable, one PVC per replica.
|
|
- metadata:
|
|
name: cache
|
|
annotations:
|
|
# Exclude the per-pod cache PVCs from the fafflix-config k8up Schedule
|
|
# (skipWithoutAnnotation is false cluster-wide). Cache is disposable and
|
|
# RWO — it would also fail to mount into the backup pod while in use.
|
|
k8up.io/backup: "false"
|
|
spec:
|
|
accessModes:
|
|
- ReadWriteOnce
|
|
resources:
|
|
requests:
|
|
storage: 30Gi
|
|
storageClassName: cephrbd-fast-delete
|
|
volumeMode: Filesystem
|