Preserve Jellyfin upgrade and cold-backup tooling
This commit is contained in:
1 parent
9a28f4fefc
commit
c2238fd538
8 files changed
+181
-16
No files matched your search
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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,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
|
||||
Reference in new issue
Block a user