Retire Plex direct deploy bootstrap and one-time migration files
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -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'
|
||||
@@ -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
|
||||
@@ -1,6 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: plex
|
||||
labels:
|
||||
app.kubernetes.io/name: plex
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user