Retire Plex direct deploy bootstrap and one-time migration files
Validate, publish and promote Plex chart / validate (push) Successful in 1s
Validate, publish and promote Plex chart / publish (push) Successful in 2s

This commit is contained in:
2026-10-23 09:53:30 +02:00
parent 1d84fb0bc4
commit 110548cdf8
7 changed files with 5 additions and 298 deletions
+5 -19
View File
@@ -20,26 +20,12 @@ live on the retained 160 GiB `plex-config` RBD PVC. Transcodes use node-local
ephemeral storage. The official Plex image is pinned to the same version as the
migrated Arch installation.
## One-time migration
## Historical migration
`migrate.sh` stops the Arch service, confirms no Plex process remains, checks
both source SQLite databases, creates the PVC, and starts a one-shot copy Job:
```sh
./migrate.sh
kubectl -n plex logs -f job/plex-archlinux-migration
kubectl -n plex wait --for=condition=complete \
job/plex-archlinux-migration --timeout=12h
```
The Job refuses to overwrite a non-empty destination and compares regular-file
counts and byte totals before writing its completion marker. Copy I/O is
deliberately duty-cycle throttled so the migration cannot monopolize bcachefs
and Ceph. Do not delete the source `/var/lib/plex` tree until the Kubernetes
server has been verified and a separate backup exists.
The historical migration scripts remain for recovery; routine deployment is
owned by Argo CD. Plex remains available at
The one-time Arch-to-Kubernetes copy tooling is preserved in Git history, not
as an active deployment path. The existing `plex-config` PVC must not be
deleted or recreated during upgrades. Routine deployment is owned by Argo CD.
Plex remains available at
`https://plex.brunner.ninja` and directly on `192.168.0.2:32400`.
## Monitoring
-60
View File
@@ -1,60 +0,0 @@
apiVersion: v1
kind: ServiceAccount
metadata:
name: plex-deployer
namespace: plex
automountServiceAccountToken: false
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: plex-deployer
namespace: plex
rules:
- apiGroups: [""]
resources: ["configmaps"]
resourceNames: ["plex-scripts"]
verbs: ["get", "patch", "update"]
- apiGroups: [""]
resources: ["persistentvolumeclaims"]
resourceNames: ["plex-config"]
verbs: ["get", "patch", "update"]
- apiGroups: [""]
resources: ["services"]
resourceNames: ["plex"]
verbs: ["get", "patch", "update"]
- apiGroups: ["apps"]
resources: ["deployments"]
resourceNames: ["plex"]
verbs: ["get", "patch", "update"]
- apiGroups: ["traefik.io"]
resources: ["ingressroutes"]
resourceNames: ["plex-brunner-ninja"]
verbs: ["get", "patch", "update"]
- apiGroups: ["monitoring.coreos.com"]
resources: ["servicemonitors", "prometheusrules"]
resourceNames: ["plex"]
verbs: ["get", "patch", "update"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: plex-deployer
namespace: plex
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: plex-deployer
subjects:
- kind: ServiceAccount
name: plex-deployer
namespace: plex
---
apiVersion: v1
kind: Secret
metadata:
name: plex-deployer-token
namespace: plex
annotations:
kubernetes.io/service-account.name: plex-deployer
type: kubernetes.io/service-account-token
-48
View File
@@ -1,48 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
project_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
namespace=plex
service_account=plex-deployer
secret=plex-deployer-token
if [[ "$(kubectl config current-context)" != kubernetes-admin@kubernetes ]]; then
echo 'Run this only with the kubernetes-admin@kubernetes homelab context.' >&2
exit 1
fi
kubectl apply --filename "${project_dir}/namespace.yaml" >&2
kubectl apply --filename "${project_dir}/ci-deployer.yaml" >&2
for attempt in {1..30}; do
if kubectl --namespace "${namespace}" get secret "${secret}" \
--output=jsonpath='{.data.token}' 2>/dev/null | grep -q .; then
break
fi
if [[ "${attempt}" == 30 ]]; then
echo 'Timed out waiting for the service-account token.' >&2
exit 1
fi
sleep 1
done
workdir=$(mktemp --directory)
trap 'rm -rf "${workdir}"' EXIT
server=$(kubectl config view --minify --output=jsonpath='{.clusters[0].cluster.server}')
kubectl --namespace "${namespace}" get secret "${secret}" \
--output=jsonpath='{.data.ca\.crt}' | base64 --decode > "${workdir}/ca.crt"
token=$(kubectl --namespace "${namespace}" get secret "${secret}" \
--output=jsonpath='{.data.token}' | base64 --decode)
export KUBECONFIG="${workdir}/config"
kubectl config set-cluster cluster --server="${server}" \
--certificate-authority="${workdir}/ca.crt" --embed-certs=true >/dev/null
kubectl config set-credentials "${service_account}" --token="${token}" >/dev/null
kubectl config set-context plex --cluster=cluster --user="${service_account}" \
--namespace="${namespace}" >/dev/null
kubectl config use-context plex >/dev/null
echo 'Store this single line as the Gitea Actions secret KUBE_CONFIG_BASE64.' >&2
echo 'Treat it as a password; do not commit it.' >&2
base64 --wrap=0 "${KUBECONFIG}"
printf '\n'
-50
View File
@@ -1,50 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
project_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[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
sudo systemctl stop plexmediaserver.service
if sudo systemctl is-active --quiet plexmediaserver.service; then
echo 'plexmediaserver.service is still active; refusing to copy its database.' >&2
exit 1
fi
if pgrep -u plex >/dev/null; then
echo 'A process owned by plex is still running; refusing to copy its database.' >&2
exit 1
fi
database_root='/var/lib/plex/Plex Media Server/Plug-in Support/Databases'
for database in \
"${database_root}/com.plexapp.plugins.library.db" \
"${database_root}/com.plexapp.plugins.library.blobs.db"; do
result=$(sudo -u plex '/usr/lib/plexmediaserver/Plex SQLite' "${database}" 'PRAGMA quick_check;')
if [[ "${result}" != ok ]]; then
echo "SQLite quick_check failed for ${database}: ${result}" >&2
exit 1
fi
done
kubectl apply --filename "${project_dir}/namespace.yaml"
kubectl apply --filename "${project_dir}/plex.yaml" \
--selector=app.kubernetes.io/component=state
kubectl --namespace plex wait \
--for=jsonpath='{.status.phase}'=Bound \
persistentvolumeclaim/plex-config \
--timeout=10m
if kubectl --namespace plex get job plex-archlinux-migration >/dev/null 2>&1; then
echo 'Job plex-archlinux-migration already exists; inspect it instead of overwriting it.' >&2
exit 1
fi
kubectl apply --filename "${project_dir}/migration-job.yaml"
echo 'Migration started. Follow it with:'
echo ' kubectl -n plex logs -f job/plex-archlinux-migration'
echo ' kubectl -n plex wait --for=condition=complete job/plex-archlinux-migration --timeout=12h'
-86
View File
@@ -1,86 +0,0 @@
apiVersion: batch/v1
kind: Job
metadata:
name: plex-archlinux-migration
namespace: plex
labels:
app.kubernetes.io/name: plex
app.kubernetes.io/component: migration
spec:
backoffLimit: 0
template:
metadata:
labels:
app.kubernetes.io/name: plex
app.kubernetes.io/component: migration
spec:
restartPolicy: Never
nodeSelector:
kubernetes.io/hostname: arschrock
tolerations:
- key: node-role.kubernetes.io/control-plane
operator: Exists
effect: NoSchedule
containers:
- name: copy
image: docker.io/library/alpine:3.24.2
imagePullPolicy: IfNotPresent
command:
- /bin/sh
- -ec
- |
marker=/target/.archlinux-migration-complete
source='/source/Plex Media Server'
destination='/target/Library/Application Support/Plex Media Server'
test -d "${source}"
if [ -e "${marker}" ]; then
echo 'Migration marker already exists; refusing to overwrite the PVC.' >&2
exit 1
fi
if find /target -mindepth 1 -maxdepth 1 ! -name lost+found -print -quit | grep -q .; then
echo 'The destination PVC is not empty; refusing to overwrite it.' >&2
exit 1
fi
mkdir -p '/target/Library/Application Support'
cp -a "${source}" '/target/Library/Application Support/' &
copy_pid=$!
while kill -0 "${copy_pid}" 2>/dev/null; do
kill -CONT "${copy_pid}" 2>/dev/null || true
sleep 0.2
kill -STOP "${copy_pid}" 2>/dev/null || true
sleep 3.8
done
kill -CONT "${copy_pid}" 2>/dev/null || true
wait "${copy_pid}"
sync -f /target
source_files=$(find "${source}" -type f | wc -l)
destination_files=$(find "${destination}" -type f | wc -l)
source_bytes=$(find "${source}" -type f -exec stat -c '%s' {} + | awk '{ total += $1 } END { print total + 0 }')
destination_bytes=$(find "${destination}" -type f -exec stat -c '%s' {} + | awk '{ total += $1 } END { print total + 0 }')
test "${source_files}" = "${destination_files}"
test "${source_bytes}" = "${destination_bytes}"
printf 'source_files=%s\nsource_bytes=%s\n' "${source_files}" "${source_bytes}" > "${marker}"
chown 964:964 /target "${marker}"
echo "Copied ${source_files} files (${source_bytes} bytes)."
resources:
requests:
cpu: 250m
memory: 256Mi
securityContext:
allowPrivilegeEscalation: false
runAsUser: 0
runAsGroup: 0
volumeMounts:
- name: source
mountPath: /source
readOnly: true
- name: target
mountPath: /target
volumes:
- name: source
hostPath:
path: /var/lib/plex
type: Directory
- name: target
persistentVolumeClaim:
claimName: plex-config
-6
View File
@@ -1,6 +0,0 @@
apiVersion: v1
kind: Namespace
metadata:
name: plex
labels:
app.kubernetes.io/name: plex
-29
View File
@@ -1,29 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
project_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
sh -n "${project_dir}/install.sh"
bash -n "${project_dir}/migrate.sh"
bash -n "${project_dir}/create-ci-kubeconfig.sh"
if grep -q '/komposthaufen' "${project_dir}/install.sh"; then
echo 'install.sh must not inspect node-local media paths on the admin machine.' >&2
exit 1
fi
if grep -q 'plex-archlinux-migration' "${project_dir}/install.sh"; then
echo 'install.sh must not depend on the one-time migration Job.' >&2
exit 1
fi
if command -v docker >/dev/null 2>&1 && docker info >/dev/null 2>&1; then
for manifest in namespace.yaml plex.yaml migration-job.yaml; do
docker run --rm --interactive \
ghcr.io/yannh/kubeconform:v0.7.0 \
-strict -ignore-missing-schemas -summary < "${project_dir}/${manifest}"
done
docker run --rm --interactive \
ghcr.io/yannh/kubeconform:v0.7.0 \
-strict -summary < "${project_dir}/ci-deployer.yaml"
fi