diff --git a/README.md b/README.md index 91bfd8f..d7c5bb3 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/backup-job.yaml b/backup-job.yaml new file mode 100644 index 0000000..4327316 --- /dev/null +++ b/backup-job.yaml @@ -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 diff --git a/backup.sh b/backup.sh new file mode 100755 index 0000000..8539f63 --- /dev/null +++ b/backup.sh @@ -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 diff --git a/install.sh b/install.sh index 0a0e3ab..b5f0750 100755 --- a/install.sh +++ b/install.sh @@ -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}" diff --git a/jellyfin.yaml b/jellyfin.yaml index 41ccd05..38613fb 100644 --- a/jellyfin.yaml +++ b/jellyfin.yaml @@ -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 diff --git a/migration-job.yaml b/migration-job.yaml index dc8c42e..ae7a126 100644 --- a/migration-job.yaml +++ b/migration-job.yaml @@ -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 diff --git a/plugin-upgrade-job.yaml b/plugin-upgrade-job.yaml new file mode 100644 index 0000000..5f4fbea --- /dev/null +++ b/plugin-upgrade-job.yaml @@ -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: {} diff --git a/test.sh b/test.sh index a10412e..63d085a 100755 --- a/test.sh +++ b/test.sh @@ -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