Commit Graph
4354 Commits
Author SHA1 Message Date
k.rennerandClaude Opus 4.7 a9b460ac33 fix(velero): bump dataMoverPrepareTimeout 30m → 4h for large PVCs
Pipeline 758 hit `Operation Error: timeout on preparing data upload`
30 min after Backup creation. The CLI flag --item-operation-timeout
(from commit 68bba53) was the wrong knob — it controls the per-item
operation timeout client-side, not the server-side prepare phase.

The actual flag is the Velero server's --data-mover-prepare-timeout
(default 30 min), exposed via the helm chart as
configuration.dataMoverPrepareTimeout. CSI VolumeSnapshotContent
provisioning + Kopia repo init on the immich library (~250 GiB,
millions of small files) takes longer than 30 min on this cluster's
ceph-cluster-csi pace.

Also keep configuration.defaultItemOperationTimeout: 4h for safety.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 17:44:18 +02:00
k.rennerandClaude Opus 4.7 77aeb038d7 fix(migrate-immich): tolerate rsync exit 23 (partial-transfer warnings)
ci/woodpecker/manual/migrate-immich Pipeline failed
Pipeline 757 hit rsync exit 23 after processing all 189k entries —
"some files/attrs were not transferred". For a live immich library
that's a handful of files with weird perms or that vanished mid-sync,
not a real failure. rsync had already moved everything else. With
`set -eu` the script aborts instead of proceeding to the velero backup
create step.

Treat 23 as a warning, fail on any other non-zero code.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 16:03:24 +02:00
k.rennerandClaude Opus 4.7 43a239f25b fix(migrate-immich): switch from tar+find-delete to rsync --delete
ci/woodpecker/manual/migrate-immich Pipeline failed
Pipeline 756 was stuck for 22 minutes in `find /dst/data -mindepth 1
-delete` and barely made progress (deleted ~4 GiB out of 250). CephFS
bulk-delete on millions of small files is metadata-bound; at observed
rate it would have taken 20+ hours before tar could even start.

immich VM (immich.srv.renner.uno) ships rsync — same as paperless +
audiobookshelf — so use the same `rsync -aH --delete` pattern. rsync
deletes per-file as it syncs and is incremental on re-runs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 15:08:31 +02:00
k.rennerandClaude Opus 4.7 68bba536d2 fix(migrate-*): bump item-operation-timeout to 4h for large PVCs
ci/woodpecker/manual/migrate-immich Pipeline failed
Pipeline 754 hit `Operation Error: timeout on preparing data upload`
on immich's 250 GB library PVC. Velero's CSI plugin defaults to 30 min
for the prepare phase (CSI VolumeSnapshotContent provisioning + Kopia
DataUpload setup). Fast Ceph snapshot creation is plenty for paperless
or music-assistant but slow for big PVCs.

Add --item-operation-timeout=4h to every velero backup create call so
the prepare phase has room to finish on the largest PVCs without
falsely failing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 14:30:17 +02:00
k.rennerandClaude Opus 4.7 664f136221 fix(migrate-*): trap-based cleanup + heartbeats (paperless, abs, MA, influxdb)
Apply the same shape that landed in migrate-immich (commit e9792f9) to
the other four migrate pipelines:

  - Whole step in one shell block so trap EXIT covers any termination
    (success / error / SIGTERM-on-cancel) — pipeline cancellation no
    longer leaves Flux suspended, the Deployment scaled to 0, or the
    staging Job + SSH Secret stranded.
  - Heartbeat every ~3 min during Job-wait: elapsed, pod phase,
    extracted size. Stops Woodpecker's idle-output watchdog from
    killing silent steps.
  - velero backup create runs without --wait — poll separately so the
    long upload phase (especially for audiobookshelf 70 GB and immich
    250 GB) keeps emitting heartbeats.
  - Job manifests get ttlSecondsAfterFinished: 600 as a self-cleaning
    artefact in case trap doesn't run (SIGKILL etc.).
  - migrate-influxdb additionally scales the StatefulSet back to 1 in
    the cleanup function (Helm controller doesn't notice live replica
    drift, so otherwise the pod stays at replicas=0).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 14:20:57 +02:00
k.rennerandClaude Opus 4.7 e9792f9afa fix(migrate-immich): trap-based cleanup + heartbeat in long polls
ci/woodpecker/cron/e2e Pipeline failed
ci/woodpecker/cron/security-scan Pipeline was successful
ci/woodpecker/manual/migrate-immich Pipeline failed
Pipeline 751 was killed by Woodpecker's idle-output watchdog after
exactly 32 minutes — the wait loop polled the Job's Complete/Failed
conditions every 30 s but produced no output between polls. With
silent stdout, the agent classifies the step as stuck.

Pipeline 753 had the same pattern.

Refactor:
  - Whole step is now ONE shell script in a single `- |` block. A
    cleanup() function runs on EXIT (any reason: success, error,
    SIGTERM-on-cancel) so a cancelled pipeline doesn't leave Flux
    suspended, the Deployment at replicas=0, or the staging Job +
    SSH Secret stranded.
  - Heartbeat every 6 polls (~3 min) during Job-wait: prints elapsed,
    pod phase, and `du -sh /dst/data` so a 4 h transfer has visible
    progress.
  - velero backup create runs without --wait — poll separately with
    the same heartbeat shape, summing DataUpload bytes_done/total.
  - Job manifest gets ttlSecondsAfterFinished: 600 so even SIGKILL
    on the Woodpecker runner leaves a self-cleaning artefact.

Same pattern still needs to land on paperless / audiobookshelf /
music-assistant / influxdb pipelines.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 12:30:29 +02:00
k.rennerandClaude Opus 4.7 eefc0606e5 fix(migrate-*): poll Job conditions instead of kubectl logs -f
ci/woodpecker/manual/migrate-audiobookshelf Pipeline was successful
ci/woodpecker/manual/migrate-immich Pipeline failed
`kubectl logs -f --pod-running-timeout=10m` is unreliable as a wait
barrier when the pod spends time in ContainerCreating (volume attach
on RWX PVCs especially for the 300 GiB books PVC). It can return
early without streaming, leaving the subsequent `kubectl wait
--for=condition=complete --timeout=30s` to hit a Job that's still
running and exit 1.

Pipeline 750 (migrate-audiobookshelf) hit this: rsync started fine
in the staging Job but the pipeline reported failure after 88 s —
even though the Job kept running and the velero backup create that
followed actually got submitted (Backup ended up in
WaitingForPluginOperations).

Replace the logs-and-wait dance with a 4 h-capped poll loop that
reads the Job's Complete / Failed conditions directly. On Failed,
print describe + last 100 log lines for diagnosis. On Complete,
print the last 200 log lines for visibility.

Same change applied to all 5 migrate-* pipelines.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 10:52:29 +02:00
rebuild-k8s pipeline 0e41f977ed ci: Export cluster access files from rebuild
ci/woodpecker/manual/migrate-paperless Pipeline was successful
ci/woodpecker/manual/migrate-music-assistant Pipeline was successful
ci/woodpecker/manual/migrate-influxdb Pipeline was successful
ci/woodpecker/manual/migrate-audiobookshelf Pipeline failed
2026-04-28 07:03:32 +00:00
k.rennerandClaude Opus 4.7 16f86e2266 fix(velero,k8s-cp): wait for BSL sync + 48 GB CP nodes
ci/woodpecker/push/deploy-infrastructure Pipeline was successful
ci/woodpecker/push/security-scan Pipeline was successful
ci/woodpecker/manual/deploy-infrastructure Pipeline failed
ci/woodpecker/manual/rebuild-k8s Pipeline failed
bootstrap-restore Job:
  - Wait for BackupStorageLocation default to reach phase=Available
    before iterating schedules so the BSL controller has had a chance
    to sync Backup CRs from Garage.
  - Per-schedule retry: if the visible Backup count is 0 on first read,
    sleep 10 s × 10 retries before declaring "no Backups produced yet".
    This unblocks the BSL-sync-race regression where rebuild 741 skipped
    authentik-daily, music-assistant-daily, and immich-daily.
  - Use wc -l | tr -d ' ' for backup count instead of grep -c which can
    double-emit on empty input (matches the migrate-* pipeline pattern).

opentofu k8s control plane:
  - Bump VM memory 24 GB → 48 GB on k8s-1/k8s-2/k8s-3. Rebuild 741
    showed sustained API-server timeouts under Velero data-mover
    coordination load (DataDownload list calls timing out with
    "the server was unable to return a response in the time allotted"),
    causing audiobookshelf-books restore to PartiallyFail and
    migrate-immich seed Backup to also PartiallyFail. Hosts have
    plenty of free RAM, so increasing CP headroom is the cheap fix.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 08:05:40 +02:00
rebuild-k8s pipeline 36fc925064 ci: Export cluster access files from rebuild 2026-04-28 05:10:24 +00:00
k.rennerandClaude Opus 4.7 3a2130391e fix(migrate-influxdb): scale StatefulSet back up at end
ci/woodpecker/manual/rebuild-k8s Pipeline failed
Helm controller doesn't notice live replica drift on a deployed
HelmRelease, so resuming the Flux Kustomization alone left
influxdb-influxdb2 at replicas=0 after the seed-pvcs step's scale-down.
Pod had to be brought back manually via kubectl scale during pipeline
740 verification. Add explicit scale --replicas=1 at the end of the
pipeline so subsequent runs come back automatically.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 06:36:30 +02:00
k.rennerandClaude Opus 4.7 04d7c55aff refactor(influxdb): align with Velero, drop API-restore seed-Job model
ci/woodpecker/manual/migrate-influxdb Pipeline was successful
Replace the influxdb-seed Flux Kustomization (which ran a one-shot Job
calling `influx restore` against an empty bucket on bootstrap) with
the same producer-equals-consumer pattern used by paperless / abs /
immich / music-assistant. Velero captures the file-level data PVC
directly; on cluster bootstrap the StatefulSet starts against a
populated /var/lib/influxdb2 and the helm chart's empty-bucket setup
is skipped.

migrate-influxdb rewritten:
  - SSH stops the legacy influxdb.service on VM 108 (consistency for
    BoltDB + WAL + TSM file-level snapshot), tar-streams /var/lib/
    influxdb into the influxdb-influxdb2 PVC, restarts the service via
    a trap so observability comes back even on rsync failure.
  - Wipe-and-write into k8s-velero with label
    velero.io/schedule-name=influxdb-daily.
  - Drop kubernetes/apps/influxdb-seed/ entirely + the legacy backup-
    influxdb Ansible playbook. influxdb Flux Kustomization now depends
    on velero-bootstrap-restore.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 14:15:51 +02:00
k.rennerandClaude Opus 4.7 56f9dbcb7d refactor(immich,music-assistant): align with Velero, switch to ssh+tar streaming
ci/woodpecker/manual/migrate-music-assistant Pipeline was successful
ci/woodpecker/manual/migrate-immich Pipeline was successful
migrate-immich:
  - Replace backup-library Ansible step with seed-pvcs that ssh-tar-
    streams /mnt/media/immich from immich.srv.renner.uno into the
    library PVC (~250 GiB) and writes a Velero Backup labelled
    velero.io/schedule-name=immich-daily. Library mount path is /data
    inside the immich-server pod (same PVC).
  - DB seed (backup-legacy-db + seed-barman) untouched — already
    aligned via Barman producer-equals-consumer in s3://k8s-cnpg/im-pg.
  - Drop seed initContainer + garage-migration-creds Secret + the
    legacy backup-immich Ansible playbook. Reduce immich Flux
    Kustomization timeout 4h → 30m.

migrate-music-assistant fix:
  - Pipeline 736 failed because the legacy MA LXC has no rsync binary
    (`bash: line 1: rsync: command not found`). Switch to ssh+tar
    streaming with explicit `find /dst/data -mindepth 1 -delete` to
    mimic rsync --delete. Universal tar + ssh dependency only.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 14:08:04 +02:00
k.rennerandClaude Opus 4.7 25408fecf2 refactor(music-assistant): align PVC seed with Velero, drop dual-source path
ci/woodpecker/manual/migrate-music-assistant Pipeline failed
ci/woodpecker/cron/e2e Pipeline failed
ci/woodpecker/cron/security-scan Pipeline was successful
migrate-music-assistant now rsyncs /opt/music-assistant/data from the
legacy LXC (music-assistant.iot.renner.uno) into the RWO data PVC and
writes a Velero Backup labelled velero.io/schedule-name=music-assistant-
daily so velero-bootstrap-restore picks it up identically to a daily
Schedule artefact.

Staging Job inherits the edge-zone nodeSelector + workload-class
toleration so it lands on k8s-edge alongside the RWO PVC (Multi-Attach
impossible by construction). SSHs to the legacy LXC, stops the docker-
compose stack, rsyncs, restarts it via a trap so the service comes
back even if the rsync fails midway.

Drop seed initContainer + garage-migration-creds Secret + the legacy
backup-music-assistant Ansible playbook. Reduce Flux Kustomization
timeout 4h → 30m and add velero-bootstrap-restore as a dependency.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 14:02:34 +02:00
k.rennerandClaude Opus 4.7 f157f5399c feat(velero): add Schedules + bootstrap-restore for music-assistant, immich, influxdb
Three new daily Velero Schedules covering the namespaces still riding
the legacy seed-initContainer-from-k8s-migration path. Each restricts
to PVCs only (immich + authentik exclude CNPG cluster PVCs since the
DB is covered by Barman in the *-db namespace). Schedules stagger
through 04:45 → 05:15 → 05:45 local to spread node-agent load.

Also extends the velero-bootstrap-restore Job's SCHEDULES env so it
iterates over the new entries on cluster boot. With the schedule
labels in place, the migrate-* pipelines can write seed Backups
labelled velero.io/schedule-name=<app>-daily and bootstrap-restore
will pick them up identically to a daily Schedule artefact.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 13:59:23 +02:00
k.rennerandClaude Opus 4.7 a03f1257ed refactor(audiobookshelf): align PVC seed with Velero, drop dual-source path
ci/woodpecker/manual/migrate-audiobookshelf Pipeline was successful
migrate-audiobookshelf now writes the PVC seed as a Velero Backup
labelled velero.io/schedule-name=audiobookshelf-daily. velero-bootstrap-
restore picks it up identically to a daily Schedule Backup.

The seed-pvcs step suspends Flux, scales audiobookshelf to 0, applies
an in-cluster Job that SSHs to the legacy VM, stops the audiobookshelf
systemd unit (so the SQLite DB in /usr/share/audiobookshelf/config is
consistent), rsyncs config + metadata + the ~70 GB books library into
the real PVCs, then restarts the unit. Each migrate run wipes prior
audiobookshelf-daily Backups before writing the fresh seed — symmetric
with paperless.

Drop the seed initContainer from the audiobookshelf Deployment, the
now-unused garage-migration-creds Secret, and the backup-audiobookshelf
Ansible playbook. The k8s-migration bucket is no longer consulted on
bootstrap. Reduce Flux Kustomization timeout from 4h → 30m (no in-pod
extract anymore).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 13:18:42 +02:00
rebuild-k8s pipeline 9019dcdeed ci: Export cluster access files from rebuild 2026-04-27 09:43:07 +00:00
rebuild-k8s pipeline d979c81b6b ci: Export cluster access files from rebuild
ci/woodpecker/manual/rebuild-k8s Pipeline failed
2026-04-27 09:24:45 +00:00
rebuild-k8s pipeline 5e0c7f830d ci: Export cluster access files from rebuild
ci/woodpecker/manual/rebuild-k8s Pipeline failed
2026-04-27 08:55:14 +00:00
k.rennerandClaude Opus 4.7 da550cf5e3 fix: surface 3 distinct failures #731 surfaced
ci/woodpecker/manual/rebuild-k8s Pipeline failed
The instrumented wait_kustomization helper from #731 finally let us see
*why* paperless / monitoring / home-assistant Kustomizations were stalling.
Three independent bugs:

1. monitoring stalled forever with
     PrometheusRule/monitoring/garage: dry-run failed: no matches for
     kind "PrometheusRule" in version "monitoring.coreos.com..."
   Chicken-and-egg: the rule's CRD comes from kube-prometheus-stack's
   HelmRelease in the same Kustomization, so Flux's server-side dry-run
   on the rule fails before the chart can install. Move the rule into a
   new `monitoring-rules` Kustomization that dependsOn `monitoring` —
   chart installs (CRDs and all) first, then the rule applies cleanly.

2. home-assistant stalled with
     Service/home-assistant-iot: namespace not specified
   iot-service.yaml was missing `metadata.namespace: home-assistant`
   while every other manifest in the dir has it. Added.

3. paperless stalled with
     Deployment/paperless/paperless status: 'F[ailed early due to ...]
   The truncated message was cut off at 120 chars in the per-iteration
   progress line so we couldn't see the actual reason. Bump truncation
   to 240 chars and dump full `flux get kustomization` + recent warning
   events on TIMEOUT so the next run gives a definitive answer instead
   of needing a second cluster trip.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 10:42:29 +02:00
rebuild-k8s pipeline ba7e2412ab ci: Export cluster access files from rebuild 2026-04-27 07:01:14 +00:00
k.rennerandClaude Opus 4.7 1209d1fa12 fix(bootstrap): wait for all cert-manager Certificates Ready in wait-apps
ci/woodpecker/manual/rebuild-k8s Pipeline failed
A Flux Kustomization that owns an Ingress with a cert-manager.io/cluster-
issuer annotation is marked Ready as soon as the Ingress + Certificate
manifests are applied. The actual ACME DNS-01 challenge runs asynchronously
afterward (~30s-2min per Certificate via Cloudflare), so e2e's TLS check
can hit the ingress-nginx fake default cert ("issuer=Acme Co, CN=Kubernetes
Ingress Controller Fake Certificate") for any Certificate still Issuing.

Add a `wait_certificates` poll at the end of wait-apps that blocks until
every Certificate cluster-wide reports Ready=True. Same progress-printing
pattern as wait_kustomization (state-change + 60s heartbeat). Budget is
15m, well above realistic Cloudflare ACME issuance times even with several
Certs in flight.

Closes the gap on #730's `dash.k.renner.uno fake cert` and grafana
`/api/health returns nginx HTML` failures (the Service backend was likely
fine but the Ingress was being served the default cert because the
Certificate hadn't finished issuing yet).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 08:55:21 +02:00
k.rennerandClaude Opus 4.7 49061f3320 test(e2e): instrument observability checks so failures aren't silent
#730 dropped 'Failed: 8' with three observability checks reporting empty
bodies (loki/prom/am). Root cause was opacity in the harness, not the
cluster: kubectl exec was being run with 2>/dev/null, so any failure (DNS
glitch in the helper pod, transient connection refused against the in-
cluster Service, missing endpoint, ...) was silenced. The check function
captured an empty $out, the grep failed, and the failure summary tail
was '-- ' with no diagnostic. Prometheus had 86+ min to come up before
e2e ran, so the data is up — the test is broken.

Fix:
- Hard-fail (exit 1) if e2e-helper doesn't reach Ready in 180s, with a
  full kubectl describe output, instead of continuing silently into
  obs checks against a missing pod.
- Sanity-check `kubectl exec` works against e2e-helper *before* trusting
  it for obs queries.
- Bump the helper pod's sleep 600 → sleep 1800 so it can't expire mid-e2e.
- Replace the inline kubectl-exec curls with a `helper_curl` wrapper that
  - captures stderr (no more 2>/dev/null swallowing the actual error)
  - retries 5x with 5s backoff for transient DNS/connection failures
  - prints `attempt=N code=XXX body=...` per attempt so the failure tail
    shows exactly what happened
  - is exported (export -f) so the `bash -c` subshells the check helper
    uses can see it.

Loki/Prom/AM checks now read the response body from /tmp/helper_body
written by the wrapper, and the per-attempt diagnostic lands in the check
output regardless of pass/fail.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 08:45:22 +02:00
k.rennerandClaude Opus 4.7 9fadd552a0 fix(bootstrap): bump wait-apps timeouts for monitoring/paperless/home-assistant
Pipeline #730's wait-apps still produced an incomplete cluster for e2e:
the new wait_kustomization helper warned-and-continued on monitoring
(10m) and paperless (15m) before they finished reconciling under the
load of immich's parallel 250GB extract. e2e then caught the half-
built state via observability + grafana TLS + paperless 503 failures.

Bumping timeouts costs nothing on the happy path — Flux reconciles
all Kustomizations in parallel, so the wait just observes; longer
budgets only matter when something's actually slow.

- monitoring: 60 → 180 (10m → 30m). kube-prometheus-stack pulls many
  images and stands up Prom + AM + Grafana + Loki + node-exporter +
  state-metrics; first install on a contended cluster routinely
  takes 15-25 min.
- paperless: 90 → 180 (15m → 30m). Velero CSI snapshot restore for
  the data + media PVCs runs concurrently with all the other restores.
- home-assistant: 90 → 180 (15m → 30m). Same Velero contention plus
  HA Core's native restore on first start.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 08:33:24 +02:00
rebuild-k8s pipeline 6ebb05fbf0 ci: Export cluster access files from rebuild 2026-04-27 04:41:23 +00:00
k.rennerandClaude Opus 4.7 7009b7d03d fix(bootstrap): extend wait-apps coverage + add per-Kustomization progress
ci/woodpecker/manual/rebuild-k8s Pipeline failed
The wait-apps phase only blocked on `apps`, `authentik`, `paperless`, and
`audiobookshelf`. After moving the music-assistant / immich / home-assistant
seed bodies into pod initContainers (and given monitoring is what serves the
Loki/Prom/AM/Grafana endpoints the e2e suite hits), wait-apps was returning
success while immich was still mid-extract and kube-prometheus-stack hadn't
issued certs yet — so e2e ran against a half-built cluster and flaked on
observability + grafana TLS + immich pod still in Init:0/2.

Refactor:
- factor the per-Kustomization wait into a `wait_kustomization` helper that
  prints the elapsed time, current Ready/Message status on every state
  change, and a 60-s heartbeat so a long silent reconcile is still visible
- add waits for: monitoring, music-assistant, home-assistant, immich
- give immich the longest budget (1440 × 10s = 240 min) — matches the prior
  immich-seed Kustomization's 4h ceiling for the ~250GB library extract

Order is cosmetic (Flux reconciles in parallel), so the call sequence is
fast → slow to surface progress earlier in CI logs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 06:33:01 +02:00
k.rennerandClaude Opus 4.7 71a8128e58 fix(migrate-paperless): wipe loop emits "0 remaining" forever on empty
ci/woodpecker/manual/migrate-paperless Pipeline was successful
`grep -c . || echo 0` double-emits when stdin is empty: grep -c returns
"0" *and* exits 1, then `|| echo 0` runs and emits another "0", making
$count = "0\n0". The `[ "$count" -eq 0 ]` check then fails (non-numeric)
and the loop runs to its 60-iteration limit (5 min wasted) before the
script proceeds to create the new seed.

Switch to a `count_backups()` helper using `wc -l | tr -d ' '` which
yields a clean integer for empty *and* populated input. Velero
deletion finished cluster-side correctly during pipeline 728 — the
stuck loop was purely the count parsing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 21:59:19 +02:00
k.rennerandClaude Opus 4.7 7b7f9c46a4 fix(migrate-paperless): velero backup get -o name is invalid
ci/woodpecker/manual/migrate-paperless Pipeline was successful
Velero CLI only supports -o table|json|yaml. The wipe step's earlier
`velero backup get -l ... -o name` errored on stderr and returned an
empty stdout, so `grep -c .` counted 0 lines and the wipe was skipped.
Discovered after pipeline 727 left three pre-existing stale paperless-
daily Backups in place alongside the new seed.

Switch to the same counting pattern velero-bootstrap-restore already
uses: default table output, skip the header row, count remaining lines.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 21:44:39 +02:00
k.rennerandClaude Opus 4.7 e40211dad6 fix(migrate-paperless): avoid Woodpecker templating eating ${VAR}
ci/woodpecker/manual/migrate-paperless Pipeline was successful
Woodpecker substitutes ${var} during YAML parsing, replacing references
that aren't pipeline-level variables with empty strings. Pipeline 726
failed at velero CLI download because ${VELERO_VERSION} resolved to ""
and the URL became velero--linux-amd64.tar.gz.

- Use $VAR (no braces) for env-var refs in seed-pvcs commands, matching
  the seed-barman convention already in this file.
- Hardcode paperless.srv.renner.uno in the in-cluster Job spec instead
  of templating it via ${LEGACY_HOST} (Woodpecker would template that
  too, even inside a kubectl-apply heredoc).
- Replace ${var:-0} shell-default forms with `|| echo 0` at assignment
  time so the script never relies on default-expansion syntax that
  could clash with Woodpecker templating.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 21:24:33 +02:00
k.rennerandClaude Opus 4.7 3ff1f28163 refactor(paperless): align PVC seed with Velero, drop dual-source path
ci/woodpecker/manual/migrate-paperless Pipeline failed
migrate-paperless now writes the PVC seed as a Velero Backup labelled
velero.io/schedule-name=paperless-daily, so velero-bootstrap-restore
picks it up identically to a daily Schedule Backup. Single source per
data class on cluster bootstrap: k8s-velero for PVCs, k8s-cnpg for DB.

The seed-pvcs step suspends Flux, scales paperless to 0, applies an
in-cluster Job that rsyncs paperless_data + paperless_media from the
legacy VM, deletes prior paperless-daily Backups (Velero's deletion
controller cleans the Kopia data in Garage), then velero backup create
--snapshot-move-data --wait. Mirrors the wipe-then-write pattern that
seed-barman already uses for the CNPG bucket, so each migrate run is
authoritative for both halves.

Drop the seed initContainer from the paperless Deployment, the now-
unused garage-migration-creds Secret, and the backup-paperless Ansible
playbook. The k8s-migration bucket is no longer consulted on bootstrap.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 21:13:18 +02:00
rebuild-k8s pipeline 9cc5f90540 ci: Export cluster access files from rebuild 2026-04-26 17:12:37 +00:00
k.rennerandClaude Opus 4.7 cc751fcc1f perf(garage): 64M block_size
ci/woodpecker/manual/install-garage-synology Pipeline was successful
ci/woodpecker/manual/rebuild-k8s Pipeline failed
Bumps block_size from 20M to 64M. Bench (2026-04-26) on the live
20M deployment showed end-to-end cold reads at 57.6 MiB/s vs disk-
buffered floor at 67.1 MiB/s — software overhead is small (~14%),
HDD seeks dominate. Larger blocks reduce the seek count per object
proportionally; expected gain is ~15-25% on cold restores.

Only affects NEW uploads — existing objects retain their 20M (and
older 1M) chunking. CNPG WAL alignment is unchanged: WAL segments
are 16 MiB, block_size is a max, so each WAL still uploads as one
block.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 18:19:07 +02:00
rebuild-k8s pipeline 71f2228f40 ci: Export cluster access files from rebuild 2026-04-26 15:16:31 +00:00
k.rennerandClaude Opus 4.7 3c93054fa8 fix(home-assistant): collapse seed Job into HA pod initContainer
ci/woodpecker/manual/rebuild-k8s Pipeline failed
Same pattern as the other seed-Job → initContainer refactors today
(c9ea0af / b9717e9 / 74994aa / e44d86b), with the home-assistant-
specific twist that the new initContainer doesn't *do* the restore —
it just stages /config/backups/<file>.tar and /config/.HA_RESTORE
manifest, and HA Core's native backup_restore.py performs the actual
decrypt + restore on its own startup. The trusted-proxies sidecar
still gates on /config/.HA_RESTORE_RESULT (HA's post-restore signal)
exactly as before.

Why this matters: home-assistant-config is RWO ceph-block. The previous
seed Job worked only because both the Job and the app pod were pinned
to `topology.kubernetes.io/zone: edge` AND there was a single edge node,
guaranteeing co-located RBD attach. Production may have multiple edge
nodes, at which point the seed and app could land on different ones and
hit the same Multi-Attach race that broke music-assistant. Moving the
seed into the HA pod itself makes Multi-Attach impossible by
construction (one pod, one kubelet, one RBD attach).

The sentinel `/config/backups/.k8s-seed-applied` survives HA's restore
(which preserves /config/backups/), so subsequent pod restarts short-
circuit in <1s. Cluster rebuild → empty PVC → no sentinel → full
download + manifest staging → HA Core does the restore on first boot.

Refactor:
- add `seed` initContainer to deployment.yaml (the HA pod previously
  had no initContainers — it relied solely on Flux dependsOn ordering
  for the standalone seed Job)
- move backup-password.sops.yaml + garage-migration-creds.sops.yaml +
  pvcs.yaml from kubernetes/apps/home-assistant-seed/ into
  kubernetes/apps/home-assistant/
- delete kubernetes/apps/home-assistant-seed/
- collapse the standalone home-assistant-seed Flux Kustomization;
  replace `dependsOn: home-assistant-seed` on the app Kustomization
  with the standard infrastructure deps the seed Kustomization had,
  and bump timeout 30m → 4h to absorb the 3h backup-wait window

The trusted-proxies sidecar, deployment-level nodeSelector/tolerations,
and HA Core privileged container security context are unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 17:09:43 +02:00
k.rennerandClaude Opus 4.7 e34e0cdea7 fix(influxdb-seed): make seed truly run-once, not every TTL cycle
The seed had `ttlSecondsAfterFinished: 3600` and no idempotency check —
so every hour Flux re-applied a fresh Job that downloaded the multi-GiB
backup tarball, *deleted the populated bucket*, and ran `influx restore`
again. "Idempotent" only because the second restore wrote the same data,
but with a window of empty bucket between delete and restore-finish.

Unlike the file-based seeds (music-assistant / audiobookshelf / immich /
paperless), this one operates against the live InfluxDB API after the
chart is up — so the initContainer-in-app-pod pattern doesn't fit. The
fix is API-state idempotency plus persistent Job lifecycle:

- drop `ttlSecondsAfterFinished: 3600` so the Job stays in the cluster
  after first success; Flux's wait/healthCheckExprs already gate on
  status.succeeded == 1, and a persistent Job won't be re-applied on
  subsequent reconciles since the manifest matches actual state
- add a query at the top of the seed body: if the bucket already has
  any data point, exit 0 before touching anything destructive. This is
  defense-in-depth in case the Job is ever manually re-run on a
  populated cluster

Re-running the restore after a fresh backup: kubectl delete the Job
manually; Flux recreates it on next reconcile.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 17:03:02 +02:00
k.rennerandClaude Opus 4.7 8a67c11dec fix(monitoring): label Garage PrometheusRule for kube-prometheus-stack
kube-prometheus-stack's Prometheus resource has ruleSelector
matchLabels: release=kube-prometheus-stack. Without this label the
PrometheusRule was created in-cluster but never loaded into Prometheus
— dead silent alerts. Verified post-fix that the rule appears in
/api/v1/rules.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 16:54:11 +02:00
k.rennerandClaude Opus 4.7 e44d86b42f fix(paperless): collapse seed Job into paperless pod initContainer
Same shape as the prior seed-Job → initContainer refactors:
music-assistant (c9ea0af), audiobookshelf (b9717e9), immich (74994aa).
Add a `seed` initContainer to the paperless Deployment that owns the
S3-wait + extract + chown for the data and media RWX PVCs. paperless-
consume is the live drop-folder (not seeded); paperless-redis is mounted
by the redis pod (not paperless), so the seed never touched it.

Paperless used RWX (ceph-filesystem) for its data PVCs, so the
Multi-Attach race that broke music-assistant didn't surface here either —
but the refactor removes one more standalone Flux Kustomization, kills
the hourly Job churn, and matches the pattern across all migrated apps.

Refactor:
- paperless.yaml: add `seed` initContainer (the deployment had no
  initContainers before — the seed Job's sentinel was relied on purely
  via Flux dependsOn ordering). Mount paths /data + /media match the
  legacy Job's; the app container's /usr/src/paperless/{data,media}
  mounts are unchanged
- move garage-migration-creds.sops.yaml + pvcs.yaml from
  kubernetes/apps/paperless-seed/ into kubernetes/apps/paperless/
- delete kubernetes/apps/paperless-seed/
- collapse the Flux Kustomizations: drop paperless-seed, replace
  `dependsOn: paperless-seed` with velero-bootstrap-restore on the app
  Kustomization (paperless-db dep stays — separate concern)
- raise app Kustomization timeout 1h → 2h to absorb the worst-case
  tarball wait + extract

Also drop the now-stale "during the migration window" comments on the
audiobookshelf and music-assistant pvcs.yaml — the seed Kustomizations
those comments referenced are gone.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 16:52:23 +02:00
k.rennerandClaude Opus 4.7 74994aa397 fix(immich): collapse seed Job into immich-server pod initContainer
Same shape as music-assistant (c9ea0af) and audiobookshelf (b9717e9):
the standalone seed Job had ttlSecondsAfterFinished: 3600 and was being
recreated by Flux every hour for no purpose (sentinel-shortcut would
re-exit within 1s, but the kubelet/image-pull/PVC-mount cycle still
spent resources). Move the seed body into a `seed` initContainer in the
immich-server pod.

Immich uses RWX (ceph-filesystem) for the library volume, 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.

Path note: the legacy seed Job mounted the library at /library; the
immich-server pod mounts it at /data. The new initContainer uses /data
to match the rest of the pod (it's the same PVC, just a different
in-container mount path).

Refactor:
- replace `wait-for-restore` initContainer in server.yaml with the
  self-contained `seed` initContainer (preserves runAsRoot +
  CHOWN/DAC_OVERRIDE/FOWNER for the recursive 1000:1000 chown)
- move garage-migration-creds.sops.yaml + pvcs.yaml from
  kubernetes/apps/immich-seed/ into kubernetes/apps/immich/
- delete kubernetes/apps/immich-seed/
- collapse the standalone immich-seed Flux Kustomization; replace
  `dependsOn: immich-seed` on the app Kustomization with
  velero-bootstrap-restore (immich-db dep stays — it's a separate
  CNPG bootstrap-recovery concern)
- raise app Kustomization timeout 1h → 4h to absorb the worst-case
  tarball wait

immich-machine-learning is unchanged — it doesn't touch the library.
fetch-le-staging-roots initContainer is unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 16:46:32 +02:00
k.rennerandClaude Opus 4.7 b9717e929e 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>
2026-04-26 16:41:09 +02:00
k.rennerandClaude Opus 4.7 32354a45b0 feat(monitoring): scrape Garage S3 metrics + fix token rotation
Prometheus now scrapes Garage's admin endpoint on nas-1
(s3.nas-1.renner.uno:39033 via DSM reverse proxy, plain HTTP) with
bearer-token auth from a SOPS-encrypted Secret mounted into the
kube-prometheus-stack pod. Adds GarageDown / GarageScrapeFailing
alerts; richer rules to follow once the metric surface is wiki-documented.

install-garage-synology.yml: secret writes are now compare-then-write
and the force-recreate task fires on any rotated token, not just
garage.toml changes. Without this, a new metrics_token would never
reach the running Garage process.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 16:40:19 +02:00
rebuild-k8s pipeline c0ee6ece1a ci: Export cluster access files from rebuild 2026-04-26 13:11:39 +00:00
k.rennerandClaude Opus 4.7 c9ea0af308 fix(music-assistant): move seed into pod initContainer (kill Multi-Attach race)
ci/woodpecker/manual/rebuild-k8s Pipeline was successful
The standalone seed Job had `ttlSecondsAfterFinished: 3600`, so Flux
recreated it ~1h after each successful run. The recreated pod had no
node affinity and got scheduled to k8s-3, while the music-assistant app
is pinned to k8s-edge (`topology.kubernetes.io/zone: edge` for hostNetwork
on the IoT VLAN). The shared PVC is RWO Ceph-RBD — cross-node attach is
refused, so the seed pod stuck in Multi-Attach forever and the
music-assistant-seed Kustomization stayed Reconciling.

Move the seed body verbatim into a `seed` initContainer in the app
Deployment. Same pod = same kubelet RBD mount, no Multi-Attach is even
possible. The existing `[ -f /data/.restore-done ]` sentinel keeps
ordinary pod restarts to <1s; on a fresh cluster rebuild the
initContainer does the full extract before the app container starts —
exactly the per-rebuild semantics we want.

Refactor:
- replace `wait-for-restore` initContainer with a self-contained `seed`
  initContainer that owns the s3 wait/extract/sentinel logic
- move garage-migration-creds.sops.yaml + pvcs.yaml from
  kubernetes/apps/music-assistant-seed/ into kubernetes/apps/music-assistant/
- delete kubernetes/apps/music-assistant-seed/
- collapse the two Flux Kustomizations into one; replace the
  `dependsOn: music-assistant-seed` chain with the standard
  infrastructure deps it inherits
- raise the app Kustomization timeout to 4h to cover the worst-case
  3h-tarball-wait window the seed Job had

The same latent race exists in audiobookshelf-seed and paperless-seed
(they happen to co-locate on k8s-3 with their app pods today, so they
work by luck). Worth a follow-up to apply the same pattern there.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 15:02:59 +02:00
k.rennerandClaude Opus 4.7 92a1733959 test(e2e): retry+log http_body so transient runner blips don't fail silently
The previous http_body piped curl through `tee | grep -q`. grep -q has no
stdout, so when curl had a transient TLS/connection failure (or returned
an unexpected body), the failure summary line was a bare `-- ` with no
code, no body, no diagnostic.

Investigated after grafana /api/health flaked twice in CI today while
working fine when reprobed seconds later. Confirmed via ingress access
log that every request was 200 with the expected body — the fail was
runner-side opacity, not a real DB issue.

Now: 3x retry with 2s backoff, --max-time bumped 10s -> 15s, each attempt
logs `attempt=N code=XXX body=...` to /tmp/check.out so on a real failure
the actual response is in the summary tail.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 14:51:53 +02:00
k.rennerandClaude Opus 4.7 d3f6d9e0ac test(e2e): add functional checks (data presence, observability, OIDC, RGW)
ci/woodpecker/manual/e2e Pipeline failed
Existing e2e was readiness-only — every probe could pass on a cluster that
had restored zero data, lost OIDC config, or stopped shipping logs. Add
sections that exercise actual behavior:

- Data presence (post-restore): paperless documents_document, authentik
  users + applications, audiobookshelf library entries — threshold-based
  (`>= N`) so adds/removes don't flake the test.
- Observability data flow: Loki query for K8s log volume in last 5m,
  Prometheus count(up == 1) >= 25, Alertmanager has no active critical
  alerts. Run from a short-lived helper pod (curlimages/curl) so we don't
  exec into product pods.
- Authentik OIDC discovery: per-app .well-known/openid-configuration must
  return JSON with issuer + authorization_endpoint. Catches Authentik
  config drift that wouldn't show up on the live/ready probes.
- Ceph RGW S3 round-trip: PUT/GET/DELETE via the existing
  paperless/migration-bucket OBC, run as a Job so credentials come from
  envFrom and never leak to argv.

Also add .woodpecker/e2e.yml so the same script runs on cron between
rebuilds (configure schedule via Woodpecker Cron tab; suggested daily).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 14:24:16 +02:00
rebuild-k8s pipeline 80b6cdbe57 ci: Export cluster access files from rebuild
ci/woodpecker/cron/security-scan Pipeline was successful
2026-04-26 10:28:53 +00:00
k.rennerandClaude Opus 4.7 c0dd0cc359 ci(rebuild-k8s): wipe OSD disks (nvme0n1) before flux-bootstrap
ci/woodpecker/manual/rebuild-k8s Pipeline failed
PCI-passthrough OSD disks survive VM destroy/recreate but the Rook mon DB
on Talos EPHEMERAL does not. Without wiping, the new cluster's fresh FSID
mismatches the BlueStore signature on /dev/nvme0n1 and ceph-volume skips
each OSD with "belonging to a different ceph cluster", leaving 0 OSDs.

Add a wipe-osd-disks step between talos-bootstrap and export-access that
runs `talosctl wipe disk nvme0n1 --method FAST` against k8s-1/2/3
(k8s-edge has no OSD).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 12:23:41 +02:00
rebuild-k8s pipeline 8804a83f13 ci: Export cluster access files from rebuild 2026-04-26 09:50:03 +00:00
rebuild-k8s pipeline 0cdae5495c ci: Export cluster access files from rebuild
ci/woodpecker/manual/rebuild-k8s Pipeline failed
2026-04-26 09:35:26 +00:00
k.rennerandClaude Opus 4.7 1a8dc7c072 ci(tofu): wire ansible env (roles_path, host-key, vault) for local-exec
ci/woodpecker/push/deploy-infrastructure Pipeline failed
ci/woodpecker/manual/deploy-infrastructure Pipeline was successful
ci/woodpecker/manual/rebuild-k8s Pipeline failed
The ansible.cfg in the repo is ignored at runtime because the working
dir is world-writable in the CI container, so its roles_path,
host_key_checking and vault_password_file settings don't apply. The
local-exec provisioner that runs the per-LXC install playbook needs
all three configured via env vars instead.

Matches the env pattern used by update-all.yml / deploy-caddy.yml:

- ANSIBLE_ROLES_PATH=../roles:./roles  → finds the common, docker-install
  and per-service roles (otherwise it only searches plays/roles/)
- ANSIBLE_HOST_KEY_CHECKING=false      → first-contact SSH to a freshly
  created LXC won't have the key fingerprint cached
- VAULT_PASS + /tmp/.vault_pass +
  ANSIBLE_VAULT_PASSWORD_FILE         → group_vars/all/vault.yml is
  encrypted; common role fails to load vars without it
- ANSIBLE_FORCE_COLOR=true             → pretty logs, matches house style

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 10:50:53 +02:00
k.rennerandClaude Opus 4.7 12e886aa87 ci(tofu): export PROXMOX_TOKEN_{ID,SECRET} for ansible inventory
ci/woodpecker/push/deploy-infrastructure Pipeline was successful
ci/woodpecker/manual/deploy-infrastructure Pipeline failed
community.proxmox.proxmox dynamic inventory plugin reads token_id and
token_secret from PROXMOX_TOKEN_* env vars (the inventory yaml has
those keys commented out, expecting envs). Without them the plugin
fails: \"You must specify either a password or both token_id and
token_secret.\" Matches the pattern used in update-all.yml.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 10:44:11 +02:00