Preserve Jellyfin upgrade and cold-backup tooling

This commit is contained in:
feedc0de committed 2026-10-23 09:41:16 +02:00
1 parent 9a28f4fefc
commit c2238fd538
8 files changed
+181 -16

No files matched your search

+3 -1
View File
@@ -17,7 +17,9 @@ the label.
The retained 80 GiB `jellyfin-config` RBD PVC contains the migrated Arch data,
configuration, metadata, preview images, databases, and logs. Cache and
transcode activity use node-local ephemeral storage. The official Jellyfin
image is pinned to version 10.11.11, matching the source installation.
image is pinned to version 12.2. Before this major-version migration, the
complete PVC was backed up cold to the bcachefs-backed
`/komposthaufen/backups/jellyfin` directory on `arschrock`.
## One-time migration
+70
View File
@@ -0,0 +1,70 @@
apiVersion: batch/v1
kind: Job
metadata:
name: jellyfin-cold-backup-pre-12-2
namespace: jellyfin
labels:
app.kubernetes.io/name: jellyfin
app.kubernetes.io/component: backup
spec:
backoffLimit: 0
template:
metadata:
labels:
app.kubernetes.io/name: jellyfin
app.kubernetes.io/component: backup
spec:
restartPolicy: Never
automountServiceAccountToken: false
nodeSelector:
kubernetes.io/hostname: arschrock
media.brunner.ninja/multimedia-ready: "true"
tolerations:
- key: node-role.kubernetes.io/control-plane
operator: Exists
effect: NoSchedule
securityContext:
seccompProfile:
type: RuntimeDefault
containers:
- name: backup
image: docker.io/library/alpine:3.24.2
imagePullPolicy: IfNotPresent
command:
- /bin/sh
- -ec
- |
archive=/backup/jellyfin-pvc-pre-12.2.tar
test "$(stat -f -c '%t' /backup)" = ca451a4e
if [ ! -f "${archive}" ]; then
tar -C /state -cpf "${archive}.partial" .
mv "${archive}.partial" "${archive}"
fi
tar -tf "${archive}" >/dev/null
sha256sum "${archive}" >"${archive}.sha256"
find /state -xdev -type f | wc -l >"${archive}.source-file-count"
du -sb /state >"${archive}.source-bytes"
ls -lh "${archive}" "${archive}.sha256"
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
add:
- DAC_READ_SEARCH
readOnlyRootFilesystem: true
volumeMounts:
- name: state
mountPath: /state
readOnly: true
- name: backup
mountPath: /backup
volumes:
- name: state
persistentVolumeClaim:
claimName: jellyfin-config
readOnly: true
- name: backup
hostPath:
path: /komposthaufen/backups/jellyfin
type: DirectoryOrCreate
Executable
+22
View File
@@ -0,0 +1,22 @@
#!/bin/sh
set -eu
project_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
expected_context=kubernetes-admin@kubernetes
context=$(kubectl config current-context)
if [ "${context}" != "${expected_context}" ]; then
echo "Expected Kubernetes context ${expected_context}, found ${context}." >&2
exit 1
fi
kubectl --namespace jellyfin scale deployment/jellyfin --replicas=0
kubectl --namespace jellyfin wait --for=delete pod \
--selector=app.kubernetes.io/name=jellyfin,app.kubernetes.io/component=server \
--timeout=10m
kubectl apply --dry-run=server --filename "${project_dir}/backup-job.yaml"
kubectl apply --filename "${project_dir}/backup-job.yaml"
kubectl --namespace jellyfin wait --for=condition=complete \
job/jellyfin-cold-backup-pre-12-2 --timeout=12h
kubectl --namespace jellyfin logs job/jellyfin-cold-backup-pre-12-2
+6 -8
View File
@@ -10,17 +10,14 @@ if [ "${context}" != "${expected_context}" ]; then
echo "Expected Kubernetes context ${expected_context}, found ${context}." >&2
exit 1
fi
if [ "$(findmnt -T /komposthaufen/multimedia -n -o FSTYPE)" != bcachefs ]; then
echo '/komposthaufen/multimedia is not backed by bcachefs.' >&2
exit 1
fi
test -d /komposthaufen/multimedia/Videos
kubectl apply --filename "${project_dir}/namespace.yaml"
kubectl --namespace jellyfin wait --for=condition=complete \
job/jellyfin-archlinux-migration --timeout=5s
if kubectl --namespace jellyfin get job/jellyfin-archlinux-migration >/dev/null 2>&1; then
kubectl --namespace jellyfin wait --for=condition=complete \
job/jellyfin-archlinux-migration --timeout=5s
fi
kubectl apply --dry-run=server --filename "${project_dir}/jellyfin.yaml"
kubectl apply --filename "${project_dir}/jellyfin.yaml"
kubectl --namespace jellyfin scale deployment/jellyfin --replicas=1
kubectl --namespace jellyfin rollout status deployment/jellyfin --timeout=30m
pod=$(kubectl --namespace jellyfin get pod \
@@ -29,5 +26,6 @@ pod=$(kubectl --namespace jellyfin get pod \
node=$(kubectl --namespace jellyfin get pod "${pod}" --output=jsonpath='{.spec.nodeName}')
image=$(kubectl --namespace jellyfin get deployment jellyfin --output=jsonpath='{.spec.template.spec.containers[0].image}')
test "${node}" = arschrock
test "${image}" = docker.io/jellyfin/jellyfin:12.2
kubectl --namespace jellyfin get pod "${pod}" --output=wide
printf 'Jellyfin is ready on %s with image %s.\n' "${node}" "${image}"
+2 -2
View File
@@ -79,7 +79,7 @@ spec:
type: RuntimeDefault
initContainers:
- name: prepare
image: docker.io/library/alpine:3.24.1
image: docker.io/library/alpine:3.24.2
imagePullPolicy: IfNotPresent
command:
- /tools/prepare.sh
@@ -106,7 +106,7 @@ spec:
readOnly: true
containers:
- name: jellyfin
image: docker.io/jellyfin/jellyfin:10.11.11
image: docker.io/jellyfin/jellyfin:12.2
imagePullPolicy: IfNotPresent
env:
- name: TZ
+1 -1
View File
@@ -23,7 +23,7 @@ spec:
effect: NoSchedule
containers:
- name: copy
image: docker.io/library/alpine:3.24.1
image: docker.io/library/alpine:3.24.2
imagePullPolicy: IfNotPresent
command:
- /bin/sh
+72
View File
@@ -0,0 +1,72 @@
apiVersion: batch/v1
kind: Job
metadata:
name: jellyfin-plugins-for-12-2
namespace: jellyfin
labels:
app.kubernetes.io/name: jellyfin
app.kubernetes.io/component: plugin-upgrade
spec:
backoffLimit: 0
template:
metadata:
labels:
app.kubernetes.io/name: jellyfin
app.kubernetes.io/component: plugin-upgrade
spec:
restartPolicy: Never
automountServiceAccountToken: false
nodeSelector:
kubernetes.io/hostname: arschrock
tolerations:
- key: node-role.kubernetes.io/control-plane
operator: Exists
effect: NoSchedule
securityContext:
seccompProfile:
type: RuntimeDefault
containers:
- name: upgrade
image: docker.io/library/alpine:3.24.2
imagePullPolicy: IfNotPresent
command:
- /bin/sh
- -ec
- |
plugins=/state/data/plugins
saved=/state/plugin-backups/pre-12.2
mkdir -p "${saved}"
for old in "${plugins}"/LDAP\ Authentication_* "${plugins}"/Jellyfin\ Ignore_*; do
[ -e "${old}" ] || continue
name=${old##*/}
[ -e "${saved}/${name}" ] || mv "${old}" "${saved}/${name}"
done
mkdir -p "${plugins}/LDAP Authentication_24.0.0.0"
wget -qO /tmp/ldap.zip https://github.com/jellyfin/jellyfin-plugin-ldapauth/releases/download/v24/ldap-authentication_24.0.0.0.zip
unzip -q /tmp/ldap.zip -d "${plugins}/LDAP Authentication_24.0.0.0"
mkdir -p "${plugins}/Jellyfin Ignore_0.6.3.0"
wget -qO /tmp/ignore.zip https://github.com/fdett/jellyfin-ignore/releases/download/v0.6.3/jellyfin-ignore_0.6.3.0.zip
unzip -q /tmp/ignore.zip -d "${plugins}/Jellyfin Ignore_0.6.3.0"
grep -q '"targetAbi": "12.0.0.0"' "${plugins}/LDAP Authentication_24.0.0.0/meta.json"
grep -q '"targetAbi": "12.0.0.0"' "${plugins}/Jellyfin Ignore_0.6.3.0/meta.json"
find "${saved}" -maxdepth 1 -mindepth 1 -type d -print
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
readOnlyRootFilesystem: true
runAsNonRoot: true
runAsUser: 930
runAsGroup: 930
volumeMounts:
- name: state
mountPath: /state
- name: tmp
mountPath: /tmp
volumes:
- name: state
persistentVolumeClaim:
claimName: jellyfin-config
- name: tmp
emptyDir: {}
+5 -4
View File
@@ -5,16 +5,17 @@ set -euo pipefail
project_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
sh -n "${project_dir}/install.sh"
sh -n "${project_dir}/backup.sh"
bash -n "${project_dir}/migrate.sh"
bash -n "${project_dir}/create-ci-kubeconfig.sh"
if command -v docker >/dev/null 2>&1 && docker info >/dev/null 2>&1; then
for manifest in namespace.yaml jellyfin.yaml migration-job.yaml; do
docker run --rm --interactive \
if command -v podman >/dev/null 2>&1; then
for manifest in namespace.yaml jellyfin.yaml migration-job.yaml backup-job.yaml plugin-upgrade-job.yaml; do
podman run --rm --interactive \
ghcr.io/yannh/kubeconform:v0.7.0 \
-strict -ignore-missing-schemas -summary < "${project_dir}/${manifest}"
done
docker run --rm --interactive \
podman run --rm --interactive \
ghcr.io/yannh/kubeconform:v0.7.0 \
-strict -summary < "${project_dir}/ci-deployer.yaml"
fi