Deploy read-only Copyparty with guarded arschrock media storage
This commit is contained in:
1 parent
6d24ca7921
commit
d6d3e013cf
8 files changed
+374
-1
No files matched your search
@@ -0,0 +1,78 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
readonly expected_context="kubernetes-admin@kubernetes"
|
||||
readonly node="arschrock"
|
||||
readonly dynmap_ready_label="minecraft.brunner.ninja/dynmap-ready"
|
||||
readonly library_services=(lidarr.service readarr.service radarr.service sonarr.service bazarr.service)
|
||||
readonly whisparr_service="whisparr.service"
|
||||
|
||||
current_context="$(kubectl config current-context)"
|
||||
if [[ "${current_context}" != "${expected_context}" ]]; then
|
||||
echo "Refusing to modify ${node}: context is ${current_context}, expected ${expected_context}." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# These host-native library managers must never inspect the empty Btrfs
|
||||
# directory underneath /komposthaufen when its Bcachefs mount is offline.
|
||||
sudo -n systemctl stop "${library_services[@]}" "${whisparr_service}"
|
||||
for service in "${library_services[@]}" "${whisparr_service}"; do
|
||||
if sudo -n systemctl is-active --quiet "${service}"; then
|
||||
echo "${service} is still active; do not unmount /komposthaufen." >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
kubectl get node "${node}" >/dev/null
|
||||
if ! kubectl label node "${node}" media.brunner.ninja/multimedia-ready- 2>/dev/null; then
|
||||
labels=$(kubectl get node "${node}" --output=jsonpath='{.metadata.labels}')
|
||||
if [[ "${labels}" == *"media.brunner.ninja/multimedia-ready"* ]]; then
|
||||
echo "Could not remove the media-ready label from ${node}; do not unmount." >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
for namespace in plex jellyfin copyparty; do
|
||||
scheduled_pods=$(kubectl --namespace "${namespace}" get pods \
|
||||
--selector="app.kubernetes.io/component=server" \
|
||||
--field-selector="spec.nodeName=${node}" \
|
||||
--output=name)
|
||||
if [[ -n "${scheduled_pods}" ]]; then
|
||||
mapfile -t pod_names <<< "${scheduled_pods}"
|
||||
kubectl --namespace "${namespace}" delete "${pod_names[@]}" --wait=false
|
||||
for pod in "${pod_names[@]}"; do
|
||||
kubectl --namespace "${namespace}" wait --for=delete "${pod}" --timeout=5m
|
||||
done
|
||||
fi
|
||||
done
|
||||
|
||||
if ! kubectl label node "${node}" "${dynmap_ready_label}-" 2>/dev/null; then
|
||||
labels=$(kubectl get node "${node}" --output=jsonpath='{.metadata.labels}')
|
||||
if [[ "${labels}" == *"${dynmap_ready_label}"* ]]; then
|
||||
echo "Could not remove the Dynmap-ready label from ${node}; do not unmount." >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
mapfile -t paper_pods < <(kubectl --namespace mcserver get pods \
|
||||
--selector="app.kubernetes.io/name=minecraft-paper,app.kubernetes.io/instance=paper" \
|
||||
--field-selector="spec.nodeName=${node}" --output=name)
|
||||
if ((${#paper_pods[@]})); then
|
||||
kubectl --namespace mcserver delete "${paper_pods[@]}" --wait=false
|
||||
for pod in "${paper_pods[@]}"; do
|
||||
kubectl --namespace mcserver wait --for=delete "${pod}" --timeout=15m
|
||||
done
|
||||
fi
|
||||
|
||||
for namespace in plex jellyfin copyparty; do
|
||||
remaining_pods=$(kubectl --namespace "${namespace}" get pods \
|
||||
--selector="app.kubernetes.io/component=server" \
|
||||
--field-selector="spec.nodeName=${node}" \
|
||||
--output=name)
|
||||
if [[ -n "${remaining_pods}" ]]; then
|
||||
echo "A ${namespace} Pod is still assigned to ${node}; do not unmount." >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
echo "Plex, Jellyfin, Copyparty and Paper are stopped on ${node}. Replacements stay Pending until their storage-ready labels return."
|
||||
@@ -0,0 +1,89 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
readonly expected_context="kubernetes-admin@kubernetes"
|
||||
readonly node="arschrock"
|
||||
readonly media_path="/komposthaufen/multimedia"
|
||||
readonly dynmap_tiles_path="/home/feedc0de/mcserver/plugins/dynmap/web/tiles"
|
||||
readonly dynmap_ready_label="minecraft.brunner.ninja/dynmap-ready"
|
||||
readonly library_services=(lidarr.service readarr.service radarr.service sonarr.service bazarr.service)
|
||||
readonly whisparr_service="whisparr.service"
|
||||
readonly whisparr_library_path="/mnt/msdownld.tmp/_hardware/whisparr"
|
||||
|
||||
current_context="$(kubectl config current-context)"
|
||||
if [[ "${current_context}" != "${expected_context}" ]]; then
|
||||
echo "Refusing to modify ${node}: context is ${current_context}, expected ${expected_context}." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! mountpoint -q /komposthaufen; then
|
||||
echo "/komposthaufen is not a mountpoint; storage remains offline." >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ "$(findmnt -T "${media_path}" -n -o FSTYPE)" != bcachefs ]]; then
|
||||
echo "${media_path} is not backed by bcachefs; storage remains offline." >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ ! -d "${media_path}/Videos" ]]; then
|
||||
echo "Required directory ${media_path}/Videos is missing; storage remains offline." >&2
|
||||
exit 1
|
||||
fi
|
||||
if ! mountpoint -q "${dynmap_tiles_path}" || [[ "$(findmnt -T "${dynmap_tiles_path}" -n -o FSTYPE)" != bcachefs ]]; then
|
||||
echo "${dynmap_tiles_path} is not mounted from bcachefs; Paper remains stopped." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
for attempt in $(seq 1 60); do
|
||||
if kubectl get node "${node}" >/dev/null 2>&1; then
|
||||
# Stale readiness after a reboot may have mounted the empty system-disk
|
||||
# directory. Drop both gates and recreate every hostPath consumer first.
|
||||
kubectl label node "${node}" media.brunner.ninja/multimedia-ready- 2>/dev/null || true
|
||||
if ! kubectl label node "${node}" "${dynmap_ready_label}-" 2>/dev/null; then
|
||||
labels="$(kubectl get node "${node}" --output=jsonpath='{.metadata.labels}')"
|
||||
if [[ "${labels}" == *"${dynmap_ready_label}"* ]]; then
|
||||
echo "Could not clear the stale Dynmap-ready label on ${node}." >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
for namespace in plex jellyfin copyparty; do
|
||||
mapfile -t pods < <(
|
||||
kubectl --namespace "${namespace}" get pods \
|
||||
--selector="app.kubernetes.io/component=server" \
|
||||
--output=name 2>/dev/null || true
|
||||
)
|
||||
if ((${#pods[@]})); then
|
||||
kubectl --namespace "${namespace}" delete "${pods[@]}" --wait=false
|
||||
for pod in "${pods[@]}"; do
|
||||
kubectl --namespace "${namespace}" wait --for=delete "${pod}" --timeout=5m
|
||||
done
|
||||
fi
|
||||
done
|
||||
mapfile -t paper_pods < <(
|
||||
kubectl --namespace mcserver get pods \
|
||||
--selector="app.kubernetes.io/name=minecraft-paper,app.kubernetes.io/instance=paper" \
|
||||
--output=name 2>/dev/null || true
|
||||
)
|
||||
if ((${#paper_pods[@]})); then
|
||||
kubectl --namespace mcserver delete "${paper_pods[@]}" --wait=false
|
||||
for pod in "${paper_pods[@]}"; do
|
||||
kubectl --namespace mcserver wait --for=delete "${pod}" --timeout=15m
|
||||
done
|
||||
fi
|
||||
kubectl label node "${node}" media.brunner.ninja/multimedia-ready=true --overwrite
|
||||
kubectl label node "${node}" "${dynmap_ready_label}=true" --overwrite
|
||||
sudo -n systemctl start "${library_services[@]}"
|
||||
if mountpoint -q /mnt \
|
||||
&& [[ "$(findmnt -T /mnt -n -o FSTYPE)" == bcachefs ]] \
|
||||
&& [[ -d "${whisparr_library_path}" ]]; then
|
||||
sudo -n systemctl start "${whisparr_service}"
|
||||
else
|
||||
echo "Whisparr remains stopped; its /mnt Bcachefs library is not ready."
|
||||
fi
|
||||
echo "${node} is media-ready; Plex, Jellyfin, Copyparty and Paper may now be scheduled."
|
||||
exit 0
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
|
||||
echo "Kubernetes API did not become available; ${node} remains media-offline." >&2
|
||||
exit 1
|
||||
Reference in new issue
Block a user