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>
310 lines
13 KiB
YAML
310 lines
13 KiB
YAML
---
|
|
apiVersion: apps/v1
|
|
kind: StatefulSet
|
|
metadata:
|
|
name: cheeztv
|
|
namespace: cheeztv
|
|
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 (cheeztv-0/1) are the lease owner identity, hence StatefulSet.
|
|
replicas: 2
|
|
serviceName: cheeztv
|
|
podManagementPolicy: Parallel
|
|
updateStrategy:
|
|
type: RollingUpdate
|
|
selector:
|
|
matchLabels:
|
|
app: cheeztv
|
|
template:
|
|
metadata:
|
|
labels:
|
|
app: cheeztv
|
|
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: cheeztv
|
|
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: cheeztv
|
|
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 (cheeztv-ha additions).
|
|
- name: Jellyfin__TranscodeStore__RedisConnectionString
|
|
value: "valkey-cheeztv-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: cheeztv-postgres-app
|
|
key: username
|
|
- name: PGPASSWORD
|
|
valueFrom:
|
|
secretKeyRef:
|
|
name: cheeztv-postgres-app
|
|
key: password
|
|
- name: PGDB
|
|
valueFrom:
|
|
secretKeyRef:
|
|
name: cheeztv-postgres-app
|
|
key: dbname
|
|
- name: POSTGRES_CONNECTION_STRING
|
|
value: "postgresql://$(PGUSER):$(PGPASSWORD)@cheeztv-postgres-pooler:5432/$(PGDB)"
|
|
- name: DATABASE_URL
|
|
value: "postgresql://$(PGUSER):$(PGPASSWORD)@cheeztv-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
|
|
# Kids-only instance: mount just the tvshows/kids subtree of the
|
|
# shared TV subvolume (subPath kids) so cheeztv's library never
|
|
# exposes the adult tree. Path matches fafflix's /media/tv-kids
|
|
# mount so the same episode resolves identically across instances.
|
|
mountPath: /media/tv
|
|
subPath: kids
|
|
readOnly: true
|
|
- name: media-movies
|
|
# Kids-only instance: mount just the movies/kids subtree of the
|
|
# shared movies subvolume (subPath kids).
|
|
mountPath: /media/movies
|
|
subPath: kids
|
|
readOnly: true
|
|
volumes:
|
|
- name: plugin-config
|
|
configMap:
|
|
name: cheeztv-plugin-config
|
|
- name: config
|
|
persistentVolumeClaim:
|
|
claimName: cheeztv-config
|
|
- name: transcode
|
|
persistentVolumeClaim:
|
|
claimName: cheeztv-transcode
|
|
- name: media-tv
|
|
persistentVolumeClaim:
|
|
claimName: cheeztv-media-tv
|
|
- name: media-movies
|
|
persistentVolumeClaim:
|
|
claimName: cheeztv-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 cheeztv-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
|