fix(audiobookshelf): collapse seed Job into pod initContainer
ci/woodpecker/manual/install-garage-synology Pipeline was successful

Same shape as the music-assistant fix (commit c9ea0af): the standalone
seed Job had ttlSecondsAfterFinished: 3600, so Flux churned a fresh seed
pod every hour with no purpose (sentinel-shortcut would just re-exit
within 1s, but each cycle still added kubelet, image-pull, and PVC-mount
load). Move the seed body verbatim into a `seed` initContainer in the
app pod.

Audiobookshelf uses RWX (ceph-filesystem) for all three of its data
volumes, so the Multi-Attach race that broke music-assistant didn't
surface here — but the refactor still applies for consistency, kills
the hourly Job churn, and removes one more Flux Kustomization.

Refactor:
- replace `wait-for-restore` initContainer with the self-contained
  `seed` initContainer (preserves runAsRoot + CHOWN/DAC_OVERRIDE/FOWNER
  needed for the recursive 1000:1000 chown over /audiobooks)
- move garage-migration-creds.sops.yaml + pvcs.yaml from
  kubernetes/apps/audiobookshelf-seed/ into kubernetes/apps/audiobookshelf/
- delete kubernetes/apps/audiobookshelf-seed/
- collapse the two Flux Kustomizations into one; replace
  `dependsOn: audiobookshelf-seed` with the standard infrastructure +
  velero-bootstrap-restore deps

`fetch-le-staging-roots` initContainer is unchanged. Init order is
seed → fetch-le-staging-roots → app container.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-26 16:41:09 +02:00
co-authored by Claude Opus 4.7
parent 32354a45b0
commit b9717e929e
7 changed files with 86 additions and 161 deletions
@@ -1,6 +0,0 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- garage-migration-creds.sops.yaml
- pvcs.yaml
- seed-job.yaml
@@ -1,112 +0,0 @@
---
# Dev-phase seed Job for Audiobookshelf PVCs. Reads legacy tarballs
# from Garage (s3://k8s-migration/audiobookshelf/...) written by the
# standalone `migrate-audiobookshelf` Woodpecker pipeline. Sentinel is
# `/config/.restore-done`; legacy `.restore-v3-done` is still honored
# on previously-seeded PVCs and normalized forward on first re-run.
apiVersion: batch/v1
kind: Job
metadata:
name: audiobookshelf-data-seed-v3
namespace: audiobookshelf
spec:
ttlSecondsAfterFinished: 3600
template:
spec:
restartPolicy: Never
securityContext:
fsGroup: 1000
seccompProfile:
type: RuntimeDefault
containers:
- name: seed
image: alpine:3.21
securityContext:
runAsNonRoot: false
allowPrivilegeEscalation: true
capabilities:
drop: ["ALL"]
add: ["CHOWN", "DAC_OVERRIDE", "FOWNER"]
command:
- sh
- -c
- |
set -eu
if [ -f /config/.restore-done ] || [ -f /config/.restore-v3-done ]; then
echo "Seed already applied (sentinel present); normalizing and exiting."
touch /config/.restore-done
chown 1000:1000 /config/.restore-done 2>/dev/null || true
exit 0
fi
apk add --no-cache aws-cli tar ca-certificates >/dev/null
export HOME=/tmp AWS_ENDPOINT_URL="$S3_ENDPOINT" AWS_DEFAULT_REGION=garage
# TLS cert is for s3.nas-1.renner.uno only, not wildcarded, so
# force path-style addressing (bucket in path, not subdomain).
mkdir -p "$HOME/.aws"
printf '[default]\ns3 =\n addressing_style = path\n' > "$HOME/.aws/config"
echo "Waiting for tarballs at s3://$S3_BUCKET/$S3_PREFIX/ ..."
for i in $(seq 1 1080); do
if aws s3api head-object --bucket "$S3_BUCKET" --key "$S3_PREFIX/audiobookshelf-config.tar.gz" >/dev/null 2>&1 \
&& aws s3api head-object --bucket "$S3_BUCKET" --key "$S3_PREFIX/audiobookshelf-metadata.tar.gz" >/dev/null 2>&1 \
&& aws s3api head-object --bucket "$S3_BUCKET" --key "$S3_PREFIX/audiobookshelf-books.tar" >/dev/null 2>&1; then
echo "All three tarballs found."; break
fi
echo " not ready yet ($i/1080) — sleeping 10s"; sleep 10
if [ "$i" = "1080" ]; then
echo "ERROR: tarballs not found after 3 hours"; exit 1
fi
done
aws s3 ls "s3://$S3_BUCKET/$S3_PREFIX/"
echo "Streaming config tarball → /config ..."
aws s3 cp "s3://$S3_BUCKET/$S3_PREFIX/audiobookshelf-config.tar.gz" - | tar -xz -C /config
echo "Streaming metadata tarball → /metadata ..."
aws s3 cp "s3://$S3_BUCKET/$S3_PREFIX/audiobookshelf-metadata.tar.gz" - | tar -xz -C /metadata
echo "Streaming books tarball → /audiobooks ..."
aws s3 cp "s3://$S3_BUCKET/$S3_PREFIX/audiobookshelf-books.tar" - | tar -x -C /audiobooks
echo "Normalizing ownership to 1000:1000 ..."
chown -R 1000:1000 /config /metadata /audiobooks
echo "Writing seed sentinel ..."
touch /config/.restore-done
chown 1000:1000 /config/.restore-done
echo "Done. Summary:"
du -sh /config /metadata /audiobooks
env:
- name: S3_ENDPOINT
value: "https://s3.nas-1.renner.uno"
- name: S3_BUCKET
value: "k8s-migration"
- name: S3_PREFIX
value: "audiobookshelf"
- name: AWS_ACCESS_KEY_ID
valueFrom:
secretKeyRef:
name: garage-migration-creds
key: AWS_ACCESS_KEY_ID
- name: AWS_SECRET_ACCESS_KEY
valueFrom:
secretKeyRef:
name: garage-migration-creds
key: AWS_SECRET_ACCESS_KEY
volumeMounts:
- name: config
mountPath: /config
- name: metadata
mountPath: /metadata
- name: books
mountPath: /audiobooks
volumes:
- name: config
persistentVolumeClaim:
claimName: audiobookshelf-config
- name: metadata
persistentVolumeClaim:
claimName: audiobookshelf-metadata
- name: books
persistentVolumeClaim:
claimName: audiobookshelf-books
+77 -10
View File
@@ -40,28 +40,95 @@ spec:
seccompProfile:
type: RuntimeDefault
initContainers:
# Block until the restore job writes its sentinel. Prevents the app from
# initializing a fresh SQLite under /config while restore is still extracting.
- name: wait-for-restore
# Restore the three audiobookshelf PVCs from the Garage tarballs
# written by the `migrate-audiobookshelf` Woodpecker pipeline.
# Sentinel-guarded so subsequent pod restarts short-circuit in <1s.
# Lives in the app pod (not a separate Job) so volume attachments
# are shared via the same kubelet mounts and Flux can't churn an
# orphan Job after `ttlSecondsAfterFinished`. On a fresh cluster
# rebuild the volumes are empty, so this runs the full ~70 GB
# extract before the app container starts.
- name: seed
image: alpine:3.21
command:
- sh
- -c
- |
until [ -f /config/.restore-done ]; do
echo "waiting for /config/.restore-done ..."
sleep 10
set -eu
if [ -f /config/.restore-done ] || [ -f /config/.restore-v3-done ]; then
echo "Seed already applied (sentinel present); normalizing and exiting."
touch /config/.restore-done
chown 1000:1000 /config/.restore-done 2>/dev/null || true
exit 0
fi
apk add --no-cache aws-cli tar ca-certificates >/dev/null
export HOME=/tmp AWS_ENDPOINT_URL="$S3_ENDPOINT" AWS_DEFAULT_REGION=garage
# TLS cert is for s3.nas-1.renner.uno only, not wildcarded, so
# force path-style addressing (bucket in path, not subdomain).
mkdir -p "$HOME/.aws"
printf '[default]\ns3 =\n addressing_style = path\n' > "$HOME/.aws/config"
echo "Waiting for tarballs at s3://$S3_BUCKET/$S3_PREFIX/ ..."
for i in $(seq 1 1080); do
if aws s3api head-object --bucket "$S3_BUCKET" --key "$S3_PREFIX/audiobookshelf-config.tar.gz" >/dev/null 2>&1 \
&& aws s3api head-object --bucket "$S3_BUCKET" --key "$S3_PREFIX/audiobookshelf-metadata.tar.gz" >/dev/null 2>&1 \
&& aws s3api head-object --bucket "$S3_BUCKET" --key "$S3_PREFIX/audiobookshelf-books.tar" >/dev/null 2>&1; then
echo "All three tarballs found."; break
fi
echo " not ready yet ($i/1080) — sleeping 10s"; sleep 10
if [ "$i" = "1080" ]; then
echo "ERROR: tarballs not found after 3 hours"; exit 1
fi
done
echo "restore sentinel present — continuing."
aws s3 ls "s3://$S3_BUCKET/$S3_PREFIX/"
echo "Streaming config tarball → /config ..."
aws s3 cp "s3://$S3_BUCKET/$S3_PREFIX/audiobookshelf-config.tar.gz" - | tar -xz -C /config
echo "Streaming metadata tarball → /metadata ..."
aws s3 cp "s3://$S3_BUCKET/$S3_PREFIX/audiobookshelf-metadata.tar.gz" - | tar -xz -C /metadata
echo "Streaming books tarball → /audiobooks ..."
aws s3 cp "s3://$S3_BUCKET/$S3_PREFIX/audiobookshelf-books.tar" - | tar -x -C /audiobooks
echo "Normalizing ownership to 1000:1000 ..."
chown -R 1000:1000 /config /metadata /audiobooks
echo "Writing seed sentinel ..."
touch /config/.restore-done
chown 1000:1000 /config/.restore-done
echo "Done. Summary:"
du -sh /config /metadata /audiobooks
env:
- name: S3_ENDPOINT
value: "https://s3.nas-1.renner.uno"
- name: S3_BUCKET
value: "k8s-migration"
- name: S3_PREFIX
value: "audiobookshelf"
- name: AWS_ACCESS_KEY_ID
valueFrom:
secretKeyRef:
name: garage-migration-creds
key: AWS_ACCESS_KEY_ID
- name: AWS_SECRET_ACCESS_KEY
valueFrom:
secretKeyRef:
name: garage-migration-creds
key: AWS_SECRET_ACCESS_KEY
securityContext:
runAsNonRoot: true
runAsUser: 1000
allowPrivilegeEscalation: false
runAsNonRoot: false
allowPrivilegeEscalation: true
capabilities:
drop: ["ALL"]
add: ["CHOWN", "DAC_OVERRIDE", "FOWNER"]
volumeMounts:
- name: config
mountPath: /config
- name: metadata
mountPath: /metadata
- name: books
mountPath: /audiobooks
# Fetch the Let's Encrypt staging root CAs at pod start so
# Node (openid-client) trusts the staging-issued auth.renner.uno
# cert when doing server-side OIDC token exchange. Bundle both
@@ -1,6 +1,8 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- garage-migration-creds.sops.yaml
- pvcs.yaml
- migration-bucket.yaml
- deployment.yaml
- ingress.yaml
@@ -1,41 +1,12 @@
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: audiobookshelf-seed
namespace: flux-system
spec:
interval: 10m
retryInterval: 1m
# Library tar (~70 GB) is the slow path; matches the old 4 h ceiling.
timeout: 4h
sourceRef:
kind: GitRepository
name: flux-system
path: ./kubernetes/apps/audiobookshelf-seed
prune: true
wait: true
decryption:
provider: sops
secretRef:
name: sops-age
healthCheckExprs:
- apiVersion: batch/v1
kind: Job
current: "status.succeeded == 1"
inProgress: "!has(status.succeeded) || status.succeeded == 0"
failed: "has(status.failed) && status.failed >= 1"
dependsOn:
- name: infrastructure
- name: infrastructure-crds
- name: infrastructure-ceph-cluster
- name: velero-bootstrap-restore
---
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: audiobookshelf
namespace: flux-system
spec:
# Seed Job collapsed into the app pod's `seed` initContainer (same RWX
# mounts, no separate Job to churn after ttlSecondsAfterFinished).
# Library tar (~70 GB) is the slow path; matches the prior 4 h ceiling.
interval: 10m
retryInterval: 1m
timeout: 4h
@@ -50,4 +21,7 @@ spec:
secretRef:
name: sops-age
dependsOn:
- name: audiobookshelf-seed
- name: infrastructure
- name: infrastructure-crds
- name: infrastructure-ceph-cluster
- name: velero-bootstrap-restore