Commit Graph
4354 Commits
Author SHA1 Message Date
k.rennerandClaude Opus 4.7 6b8990015c fix(immich-seed): pre-CREATE non-trusted extensions before pg_restore
ci/woodpecker/manual/rebuild-k8s Pipeline failed
Pipeline 846 cold-start was clean end-to-end EXCEPT immich-server
crash-looped at runtime with:

  PostgresError: permission denied to create extension "vector"
  HINT: Must be superuser to create this extension.

Newer immich (>= the version we're on) issues `CREATE EXTENSION
IF NOT EXISTS vector` (pgvector) at startup as part of the vchord
migration path. The legacy dump only ships pgvecto.rs (`vectors`) so
pg_restore doesn't create pgvector. The immich role isn't a
superuser, and pgvector is not a trusted extension, so the runtime
CREATE fails.

Pre-create the extensions immich uses, as postgres, in the seed Job
right after CREATE DATABASE. The dump's own `CREATE EXTENSION
IF NOT EXISTS` calls then short-circuit (extension already exists),
and so does immich's runtime CREATE — no privilege check on the
non-trusted extension because it's already installed.

List: cube, earthdistance (legacy geo helpers), vector (pgvector,
runtime), vectors (pgvecto.rs, dump), vchord (VectorChord, runtime
migration target).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 10:30:06 +02:00
k.rennerandClaude Opus 4.7 e912dea9e5 fix(security-scan): drop --bare so OAuth token is read
ci/woodpecker/manual/security-scan Pipeline failed
Pipeline 849's agent step failed at "Not logged in · Please run
/login" despite CLAUDE_CODE_OAUTH_TOKEN being correctly injected
from the Woodpecker secret. Per Claude Code's authentication.md:

  "Bare mode does not read CLAUDE_CODE_OAUTH_TOKEN. If your script
   passes --bare, authenticate with ANTHROPIC_API_KEY or an
   apiKeyHelper instead."

The headless guide had recommended --bare for CI, but didn't flag
this incompatibility with subscription auth. We use the
subscription-backed OAuth token, not an API key, so --bare is the
wrong choice here.

Trade-off: without --bare, claude loads its default config
(CLAUDE.md, hooks, MCP, skills) on startup. None of those are
present in the ci-claude image's HOME so the loss is theoretical
for now, but if any of them get added we'd need to revisit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 10:21:32 +02:00
k.rennerandClaude Opus 4.7 9ed03dd9ea fix(security-scan): move jq summary out of trivy steps
aquasec/trivy:latest doesn't ship jq, so the per-finding summary
lines in iac-scan and image-scans were failing on "command not found"
and (because they ran as the last command in their respective `- |`
blocks despite the `|| true`) tripping the step's exit code.

Trivy steps now produce JSON only — no jq dependency on that image.
The human-readable per-finding summary moves to security-fix-agent,
which runs on ci-claude (jq baked in) and prints both IaC misconfigs
and image vulns to the log before the gate check, so the log is
still useful even when the agent exits early on no findings.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 09:57:19 +02:00
k.rennerandClaude Opus 4.7 1fa74955e3 ci(security-scan): cron + manual only, drop push trigger
Push-triggered scans were causing every code change to launch the
agent loop, which is both noisy (mostly nothing-changed runs) and
fork-bombs the bot's own PR branches into rerunning the full
workflow on every commit. The agent's branch=master step gate
prevented an actual fork-bomb, but the scan steps themselves were
still wasting CI on every push.

Schedule + manual is enough: the cron is the primary cadence, and
manual covers ad-hoc triggers when something specific needs scanning.
The agent's branch=master gate stays in place as defense-in-depth.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 09:53:03 +02:00
k.rennerandClaude Opus 4.7 13414df1b3 ci(security-scan): auto-fix agent that opens PRs for Trivy findings
ci/woodpecker/manual/security-scan Pipeline failed
The cron security-scan now ends in an autonomous fix loop:

  iac-scan       → trivy-iac.json
  image-scans    → trivy-image-N.json (per scanned image)
  security-fix-agent (master only):
    1. Gates on >=1 CRITICAL/HIGH finding across all JSON outputs.
    2. Creates branch security-fix/<date>-<short-sha>.
    3. Hands the workspace to Claude Code in headless --bare mode
       (subscription-backed via CLAUDE_CODE_OAUTH_TOKEN, no API key)
       with a narrow tool allowlist (Read/Edit/Write/Glob/Grep —
       no Bash). The agent reads the JSON, applies surgical fixes
       under ansible/, opentofu/, kubernetes/, ci/images/, and writes
       a Markdown summary to /tmp/security-fix-summary.md.
    4. If the agent produced a diff, the step commits as
       "Security Fix Agent <security-bot@renner.uno>" and pushes to
       Gitea, then opens a PR via the pulls API using a dedicated
       security_bot_token (NOT gitea_admin) so the PR is visibly
       machine-authored.

The step is gated to branch=master so it doesn't fire on its own
PR branches — those still trigger the scan workflow as a re-validation
gate, but skip the agent (no fork-bombing).

Trivy now writes JSON output (--format json -o), and findings appear
both in the JSON and as one-line summaries in the step log via jq.
The legacy --exit-code 1 was kept off so the workflow always reaches
the agent step on master; on PR branches the scans are informational
and a human reviews the agent's PR.

Required Woodpecker secrets (added by user, not by this commit):
  - claude_code_oauth_token (claude setup-token output)
  - security_bot_token      (Gitea PAT for the security-bot user)

Required Gitea setup (also out of band):
  - User "security-bot" with email security-bot@renner.uno
  - Write collaborator on gitea_admin/homelab

Agent constraints live in ci/agents/security-fix/prompt.md so they're
diffable + reviewable separately from the pipeline plumbing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 09:28:59 +02:00
k.rennerandClaude Opus 4.7 c4d8df7a92 ci: add prebuilt ci-claude image
ci/woodpecker/push/build-ci-images Pipeline was successful
Fifth image in the ci-* family. Bakes Node.js 22 + the Claude Code
CLI (@anthropic-ai/claude-code) plus git, openssh-client, curl, jq.
Used by the upcoming security-fix-agent step in security-scan.yml,
which runs Claude headlessly inside CI to propose fixes for Trivy
findings and open a PR.

Auth pattern (deferred to the consumer commit): subscription-backed
CLAUDE_CODE_OAUTH_TOKEN secret, injected as an env var, no API key.

build-ci-images.yml gains a build-claude step that mirrors the
existing four (kaniko, parallel, registry-mirror cache, date + latest
tags pushed to gitea.renner.uno/gitea_admin/).

Pinning: Containerfile uses CLAUDE_CODE_VERSION=latest at image-build
time. The resulting date-tagged image freezes whatever was current
when build-ci-images ran. Bumping = trigger build-ci-images, get a
new date tag, bump the consumer pin.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 09:23:36 +02:00
rebuild-k8s pipeline 810f1bb51e ci: Export cluster access files from rebuild 2026-05-01 07:07:38 +00:00
k.rennerandClaude Opus 4.7 ed9d06651e infra(gitea): bump LXC rootfs 4G → 16G
ci/woodpecker/push/deploy-infrastructure Pipeline was successful
ci/woodpecker/manual/deploy-infrastructure Pipeline was successful
ci/woodpecker/manual/build-ci-images Pipeline was successful
ci/woodpecker/push/security-scan Pipeline was successful
ci/woodpecker/manual/rebuild-k8s Pipeline failed
Container registry blob storage filled the gitea LXC's rootfs.
build-ci-images pipeline 841 had three of four kaniko image pushes
fail with HTTP 500 on the gitea v2 registry endpoint; gitea host
journal showed "no space left on device" across alloy + tmp writes
at the same instant.

4G is way too tight for a registry with the ci-{ansible,k8s,tofu,
caddy-builder} family at ~200-500MB each, especially with kaniko
keeping the previous date-tagged copy until GC. 16G gives headroom
for several months of date-tagged image churn before the next bump.

bpg's proxmox_virtual_environment_container does in-place rootfs
resize for LXC (`pct resize`), no restart needed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 08:40:41 +02:00
k.rennerandClaude Opus 4.7 ff33f03ebd infra(woodpecker): bump CI agent memory 2 → 4 GiB
ci/woodpecker/push/deploy-infrastructure Pipeline was successful
ci/woodpecker/push/security-scan Pipeline failed
ci/woodpecker/manual/deploy-infrastructure Pipeline was successful
ci/woodpecker/manual/build-caddy Pipeline failed
ci/woodpecker/manual/build-ci-images Pipeline is running
Pipeline 832 (build-ci-images) ran 4 kaniko builds in parallel
across 2 agents (woodpecker_agent_count=2). build-ansible got OOM-
killed — 2 GiB per agent split across 2 concurrent kaniko jobs is
~1 GiB per build, and ansible + collections + pip deps under
kaniko's tmpfs layer cache exceeded that.

4 GiB per agent gives ~2 GiB per concurrent build, which has
enough headroom for the largest of the four images. If we ever
hit the same wall again the next bump is doubling the agent
count, not the memory — kaniko parallelism doesn't help much
beyond ~2 GiB.

BPG's proxmox_virtual_environment_container does in-place memory
updates for LXC (cgroup-level, no restart), so the apply is safe
to run while the agents are servicing other jobs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 07:56:50 +02:00
k.rennerandClaude Opus 4.7 6810b6dc5e ci(build-caddy): swap to prebuilt ci-caddy-builder + ci-k8s images
ci/woodpecker/push/build-caddy Pipeline failed
The build step pulled golang:1.26 from Docker Hub on every run and
then `go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest`,
so the xcaddy version was whatever HEAD shipped that day. The
release step pulled alpine:latest and `apk add curl jq` for nothing
more than two API calls against gitea.

  - build:   golang:1.26 + go install xcaddy@latest
           → ci-caddy-builder:2026-05-01 (xcaddy v0.4.5 baked in)
  - release: alpine:latest + apk add curl jq
           → ci-k8s:2026-04-29 (curl + jq baked in)

ci-caddy-builder was just produced by build-ci-images pipeline 832
(commit 60b9698). xcaddy version bumps now happen by editing
ci/images/caddy-builder/Containerfile and bumping the date pin
here, same flow as the other ci-* consumers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 07:55:04 +02:00
k.rennerandClaude Opus 4.7 60b969853b ci: add prebuilt ci-caddy-builder image
ci/woodpecker/push/build-ci-images Pipeline failed
Fourth image in the ci-* family (alongside ci-ansible, ci-k8s,
ci-tofu). Bakes the Go toolchain (golang:1.26 base) plus xcaddy
pinned to v0.4.5 so the build-caddy pipeline doesn't have to
`go install ...@latest` on every run.

build-ci-images.yml gains a build-caddy-builder step that mirrors
the existing three (kaniko, parallel, pull-through-cached registry,
date + latest tags pushed to gitea.renner.uno/gitea_admin/).

The build-caddy.yml consumer swap is a separate commit so it can
land after the first build-ci-images run produces the image.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 07:48:21 +02:00
k.rennerandClaude Opus 4.7 8bfbfe3b5b ci(rebuild-k8s): swap recreate-vms to prebuilt ci-tofu image
The recreate-vms step still pulled ghcr.io/opentofu/opentofu:1.11
and ran `apk add --no-cache git openssh-client curl` on every run —
the only step in this pipeline that hadn't been migrated to a
prebuilt ci-* image. Each invocation was an apk roundtrip plus a
GHCR pull, and the resulting tool versions were whatever Alpine
edge happened to ship that day.

Swap to gitea.renner.uno/gitea_admin/ci-tofu:2026-04-29, which
already bakes git, openssh-client, curl, and tofu 1.11.0. The
inline `apk add` line goes away; everything else in the step
keeps working unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 07:48:07 +02:00
k.rennerandClaude Opus 4.7 68b8c86fe1 ci: drop completed migrate-tfstate one-shot pipeline
The RustFS → Garage tfstate cutover is done (commit fb82f5d). The
pipeline was always documented as one-shot and the source bucket on
RustFS is read-only / wedged anyway, so re-running it accomplishes
nothing.

Removing it also takes out two more inline `apk add` calls
(aws-cli, ca-certificates, jq) that nudged us toward upstream Alpine
on every run.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 07:47:52 +02:00
k.rennerandClaude Opus 4.7 3c416f4291 fix(ci/k8s,rebuild-k8s): bake aws-cli into ci-k8s, drop runtime apk add
ci/woodpecker/push/build-ci-images Pipeline failed
ci/woodpecker/manual/build-ci-images Pipeline failed
Two fixes after pipeline 829's wipe-backups step blew up:

  - aws-cli is not in ci-k8s:2026-04-29 — wipe ran "aws s3 rm ..." and
    got "/bin/sh: aws: not found", then exited 0 because set -eu doesn't
    catch failures inside pipes / command substitutions.

ci/images/k8s/Containerfile gains aws-cli; pushing this triggers the
build-ci-images pipeline, which builds gitea.renner.uno/gitea_admin/
ci-k8s:2026-05-01 (date tag).

The wipe-backups step is bumped to that tag and switches to
set -euo pipefail with an explicit "remaining objects must be 0"
check after each rm — so a failed wipe halts the rebuild before the
new cluster's seed Jobs run against stale state.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 07:30:12 +02:00
k.rennerandClaude Opus 4.7 15461102de feat(rebuild-k8s): optional wipe-backups step gated on WIPE_BACKUPS=true
ci/woodpecker/manual/rebuild-k8s Pipeline failed
Cold-start rebuilds need s3://k8s-cnpg/* and s3://k8s-velero/* empty
before the new cluster's seed Jobs run, otherwise the seed gate skips
seeding (sees a stale base backup) and CNPG bootstrap.recovery tries
to recover from data that doesn't match the new cluster, sticks in
"Setting up primary" until backoffLimit exhausts.

Wiping by hand from a workstation has a race: between `aws s3 rm` and
the next pipeline triggering, the still-alive old cluster's barman
plugin can lay down a fresh scheduled-backup base, re-poisoning the
bucket.

Wiping inside the pipeline AFTER recreate-vms closes the race —
recreate-vms destroys the old VMs (and therefore its barman + Velero
pods), so by the time we run `aws s3 rm` there is no writer left.

Step is opt-in via WIPE_BACKUPS=true to avoid clobbering the bucket
on routine warm-restart rebuilds:

  woodpecker pipeline create gitea_admin/homelab \
    --branch master --var PIPELINE=rebuild-k8s \
    --var WIPE_BACKUPS=true

Requires Woodpecker secrets: garage_cnpg_key_id, garage_cnpg_secret_key,
garage_velero_key_id, garage_velero_secret_key (RW perms on the
respective buckets via the cnpg-key / velero-key Garage keys).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 07:15:00 +02:00
rebuild-k8s pipeline c047f1099c ci: Export cluster access files from rebuild 2026-04-30 18:58:58 +00:00
k.rennerandClaude Opus 4.7 e40c73301b fix(seed): drop --no-owner/--no-privileges, let dump set ownership
ci/woodpecker/manual/rebuild-k8s Pipeline failed
The previous "drop --role + REASSIGN" approach broke ownership: with
--no-owner --no-privileges the dump's "ALTER TABLE ... OWNER TO X" /
"GRANT ... TO X" statements are stripped, so user tables end up
owned by postgres in PGDATA. REASSIGN OWNED BY postgres TO <app>
then errors with "cannot reassign ownership of objects ... required
by the database system" — the whole REASSIGN rolls back and tables
stay postgres-owned. Recovered cluster has tables the app role
can't write to.

Cleaner: keep pg_restore running as postgres (no --role) AND keep
the dump's ownership/ACL statements. As postgres (superuser),
ALTER TABLE OWNER TO <app> and GRANT ... TO <app> just succeed,
regardless of extension schemas. The resulting PGDATA matches the
legacy DB's ownership/ACL state exactly. CNPG bootstrap.recovery
streams it back unchanged.

Applied to authentik / paperless / immich seed Jobs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 20:53:52 +02:00
rebuild-k8s pipeline ba9f1ea6c5 ci: Export cluster access files from rebuild 2026-04-30 18:40:29 +00:00
k.rennerandClaude Opus 4.7 e7fdfc4c66 refactor(seed): pg_restore as postgres + REASSIGN, drop --role workaround
ci/woodpecker/manual/rebuild-k8s Pipeline failed
Replace the schema-permission workarounds (pre-create extensions,
GRANT USAGE on vectors/vchord) with the cleaner shape: drop --role
from pg_restore so it runs as postgres (superuser), then REASSIGN
ownership to the app role.

Why the workarounds existed:

  pg_restore --role=immich → SET ROLE immich after connecting →
  every CREATE EXTENSION / CREATE TABLE referencing extension-owned
  schemas (vectors.vector, ll_to_earth, …) hit "permission denied".
  Patched per-failure with pre-create + GRANT USAGE — fragile, dump-
  specific, fumbles around the actual issue.

Why dropping --role works:

  Without --role pg_restore stays as postgres for the whole replay —
  superuser owns nothing of consequence in the schemas, so every
  CREATE/COPY/ALTER inside the dump just works. After the load,
  REASSIGN OWNED BY postgres TO <app> + GRANT ALL ON SCHEMA public
  hands ownership back to the app role for runtime.

This is what the seed job is supposed to do: produce a functional
PGDATA snapshot. The barman base backup of that PGDATA is then
self-contained — CNPG bootstrap.recovery streams it back as-is,
no schema fumbling on the recovery side.

Applied symmetrically to authentik / paperless / immich seed Jobs.
The immich-specific pre-create/GRANT blocks added in earlier commits
are removed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 20:35:40 +02:00
rebuild-k8s pipeline 71ae7ef52d ci: Export cluster access files from rebuild 2026-04-30 16:12:24 +00:00
k.rennerandClaude Opus 4.7 ae76682fc8 fix(immich-seed,bootstrap): grant schema usage + bump MA timeout
ci/woodpecker/manual/rebuild-k8s Pipeline failed
immich seed Job: pre-creating extensions wasn't enough — vectors and
vchord create their own schemas (`vectors`, `vchord`) owned by postgres.
pg_restore under SET ROLE immich then hits "permission denied for
schema vectors" when CREATE TABLE face_search references
vectors.vector(512). Add GRANT USAGE, CREATE on those schemas to
TARGET_OWNER right after CREATE EXTENSION.

bootstrap.sh wait-apps: pipeline 825 timed out at music-assistant (10m).
On cold-start the wait timer for music-assistant starts while
velero-bootstrap-restore is still reconciling (~15min), eating most of
the 10m budget on the dependency. Bump to 30m (180 iters × 10s).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 18:07:26 +02:00
rebuild-k8s pipeline de7bca0355 ci: Export cluster access files from rebuild 2026-04-30 15:32:05 +00:00
k.rennerandClaude Opus 4.7 410496d9d8 fix(immich-seed): pre-create extensions as postgres before pg_restore
ci/woodpecker/manual/rebuild-k8s Pipeline failed
pg_restore runs with --role=immich which issues SET ROLE immich after
connecting, so every CREATE EXTENSION in the legacy dump fails with
"permission denied to create extension" (immich is not superuser).
Dependent vector tables (face_search, smart_search) then fail to
restore, and CNPG bootstrap.recovery on the live cluster ends up with
a backup that has the schema but no vector tables.

Per the wiki pattern (pg-restore-no-superuser-if-not-exists): the dump
emits `CREATE EXTENSION IF NOT EXISTS`, which short-circuits the
privilege check when the extension already exists. So pre-create the
required extensions as postgres before pg_restore:

  cube           (dependency of earthdistance)
  earthdistance  (geo helpers)
  vectors        (pgvecto.rs — legacy vector extension)
  vchord         (VectorChord — current vector extension)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 17:17:44 +02:00
rebuild-k8s pipeline c24dd42117 ci: Export cluster access files from rebuild 2026-04-30 14:28:19 +00:00
k.rennerandClaude Opus 4.7 41f8364292 fix(immich-seed): runAsUser=0 — dual-ext image defaults to postgres user
ci/woodpecker/manual/rebuild-k8s Pipeline failed
ghcr.io/corentingiraud/cnpg-pgvector-vectorchord:16-migration inherits
the CNPG postgresql base image which sets the postgres user as the
default container user (security default). The seed script needs root
for `apt-get install` (barman, awscli), `chown postgres:postgres
$PGDATA`, and `su postgres -c initdb`. Without runAsUser=0 it fails
immediately with "Permission denied" on /var/lib/apt/lists/lock.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 16:23:02 +02:00
rebuild-k8s pipeline 59ac0d99b3 ci: Export cluster access files from rebuild 2026-04-30 13:11:55 +00:00
k.rennerandClaude Opus 4.7 2508f73c7d fix(immich-seed): use dual-extension image + preload libs
ci/woodpecker/manual/rebuild-k8s Pipeline failed
The im-pg seed Job's ephemeral Postgres ran the standard postgres:16
image, which doesn't ship pgvecto.rs or vchord. The legacy dump's
CREATE EXTENSION vectors / CREATE EXTENSION vchord silently failed
during pg_restore (only "warnings"), leaving the face_search /
smart_search vector tables un-restored. The barman-cloud-backup
captured this incomplete schema; the live im-pg Cluster recovered
from it; immich-server then crash-looped trying to CREATE EXTENSION
vector itself as the non-superuser immich role
("permission denied to create extension").

Match the seed Job's image and shared_preload_libraries to the live
im-pg Cluster spec (postgres.yaml):

  image: ghcr.io/corentingiraud/cnpg-pgvector-vectorchord:16-migration
  shared_preload_libraries = 'vectors.so,vchord.so'

Now the seed's ephemeral PG can load pgvecto.rs catalog objects from
the dump and execute the dump's CREATE EXTENSION calls, so the backup
written to Garage actually contains the vector tables.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 15:00:40 +02:00
rebuild-k8s pipeline b90fad4a61 ci: Export cluster access files from rebuild
ci/woodpecker/cron/security-scan Pipeline was successful
ci/woodpecker/cron/e2e Pipeline failed
2026-04-30 11:06:49 +00:00
k.rennerandClaude Opus 4.7 1c6addb1d8 fix(seed): wire barman-cloud-backup to local pg + tighten gate
ci/woodpecker/manual/rebuild-k8s Pipeline failed
Three bugs in the cold-start seed Jobs found by pipeline 819's first
authentik attempt:

1. barman-cloud-backup connects to PG via SQL to start/stop the backup;
   without --host/--port/--user it falls back to the OS user (root),
   which has no PG role → FATAL: role "root" does not exist. Add
   --host 127.0.0.1 --port 5432 --user postgres.

2. Idempotency gate "any object under <BARMAN_DEST>/" is too loose:
   a failed prior attempt left a single marker object in Garage; the
   retry pod's gate said "1 object — seed already done" and exited 0,
   marking the Job Complete with no real backup. Tighten to check
   <BARMAN_DEST>/<BARMAN_SERVER>/base/ — only a successful backup
   writes under base/.

3. archive_timeout was unset; archive_command fires async after
   pg_switch_wal so end_wal of the base backup may not upload before
   we stop PG. Added archive_timeout='60s' and a post-pg_switch_wal
   poll on PGDATA/pg_wal/archive_status/*.ready draining (matches the
   old migrate-* pipeline pattern).

Also added a final barman-cloud-backup-list call as a sanity check
that a backup actually landed before declaring Seed complete.

Applied symmetrically to authentik / paperless / immich seed-job.yaml.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 13:00:47 +02:00
rebuild-k8s pipeline bf7e4725e2 ci: Export cluster access files from rebuild 2026-04-30 10:44:55 +00:00
k.rennerandClaude Opus 4.7 2e6f7dbb3f feat(seed): per-app cold-start Seed Job populates Garage for CNPG bootstrap.recovery
ci/woodpecker/manual/rebuild-k8s Pipeline failed
Adds kubernetes/apps/{authentik,paperless,immich}/seed-job.yaml — a
single k8s Job per app that:

  1. Idempotency-gates on `aws s3 ls s3://k8s-cnpg/<cluster>/` being
     empty (any object means a prior seed or live cluster archived).
  2. legacy-dump init container: ssh + pg_dump (or docker exec
     pg_dump for paperless/immich which run their legacy DB in
     docker-compose) → emptyDir.
  3. main container: ephemeral Postgres (initdb + WAL archive_command
     wired to barman-cloud-wal-archive) + pg_restore the dump +
     barman-cloud-backup writes a base backup to s3://k8s-cnpg/<cluster>/.

The Cluster's bootstrap.recovery now has a producer: the seed Job
populates Garage; CNPG's recovery Job retries on empty bucket, then
succeeds once the seed lands the base backup + end_wal. No more
chicken-and-egg where the in-pod init container needed the cluster
already up.

In-pod init containers stripped of DB seed:
  - authentik: removed entirely (no PVC seed needed). HelmRelease loses
    serviceAccountName, initContainers, ssh volume.
  - paperless / immich: rewritten to PVC rsync only, sentinel-guarded
    (`/data/.seed-done`) so subsequent restarts skip the rsync. No
    kubectl, no Backup CR creation, no serviceAccountName.

seed-rbac.yaml deleted from authentik / paperless / immich (the only
RBAC consumer was the in-pod init container's kubectl calls). influxdb
keeps its seed-rbac (its init container still creates Velero Backup CRs).

bootstrap.sh wait-apps timeouts bumped for cold-start reality:
  apps      5m → 15m  (Ceph mon/mgr/OSD bring-up on fresh cluster)
  authentik 5m → 20m  (seed Job + CNPG recovery from the seed)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 11:28:55 +02:00
k.rennerandClaude Opus 4.7 23764eca2d refactor(apps): merge *-db Flux KS into the app KS
authentik-db, paperless-db, immich-db are no longer separate Flux
Kustomizations — their CNPG Cluster, ObjectStore, ObjectBucketClaim,
Barman creds, and ScheduledBackup move into the app's Kustomization
(kubernetes/apps/<app>/) and the *-db Flux KS in
kubernetes/clusters/homelab/<app>.yaml is dropped.

Why: the prior split made the app KS dep-block on the *-db KS reaching
Ready, which kstatus evaluated against the CNPG Cluster's Ready
condition. On a cold-start with empty Garage, the Cluster never
reaches Ready (bootstrap.recovery has nothing to recover), so the app
KS never deploys and the seed initContainer that's supposed to
populate the DB never runs — chicken-and-egg. Merging removes the
inter-KS gate; the app Pod (with seed initContainer) and the Cluster
come up in the same KS unit.

Bootstrap mode change to make the seed actually viable lands in the
follow-on commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 11:17:05 +02:00
rebuild-k8s pipeline 474d582e22 ci: Export cluster access files from rebuild 2026-04-30 08:19:11 +00:00
k.rennerandClaude Opus 4.7 69dbee5140 fix(bootstrap): drop || true on wait_kustomization + wait_certificates
ci/woodpecker/manual/rebuild-k8s Pipeline failed
wait-apps was burning the full ~5h timeout chain (sum of per-KS
timeouts up to 1440x10s for immich) before reaching e2e even when
authentik/paperless/immich were doomed by an empty CNPG bucket.
Removing || true lets set -euo pipefail halt at the first KS
timeout, surfacing the cold-start failure in minutes instead of
hours.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 09:57:44 +02:00
k.renner c47a2dfef3 feat(garage): expose RPC 3901 for remote garage CLI
ci/woodpecker/manual/install-garage-synology Pipeline was successful
DSM reverse proxy is HTTP-only and breaks the Noise handshake; expose
RPC directly so the CLI can connect from a workstation. rpc_secret
remains the auth boundary.
2026-04-30 09:24:05 +02:00
rebuild-k8s pipeline 1d21e9a6ea ci: Export cluster access files from rebuild 2026-04-30 06:10:01 +00:00
k.rennerandClaude Opus 4.7 1715fdee0a fix(authentik): disableWait on Helm install/upgrade
ci/woodpecker/manual/rebuild-k8s Pipeline failed
The seed initContainer's first failure (no rsync on legacy authentik-
db-1) wedged the Deployment rollout: failing pod kept the RS at 1
replica, Helm's --wait timed out, Helm failed and didn't apply later
fixes. The Deployment is stuck at the original broken template, the
broken RS is recreated by the controller.

disableWait makes Helm just apply the rendered manifests and exit.
The Deployment controller rolls out asynchronously; the seed fixes
land without Helm getting blocked.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 07:43:30 +02:00
k.rennerandClaude Opus 4.7 ae643c619f fix(seed): drop cnpg.io/cluster label selector from gate query
The seed initContainer creates Backup CRs with no labels (chart
operator doesn't auto-label them either). The gate's
"-l cnpg.io/cluster=\$CLUSTER" selector therefore returned 0 every
time, so every pod restart re-ran the full seed (pg_restore against
the live cluster, app-session impact, etc.).

Each app namespace owns at most one CNPG cluster, so just count any
completed Backup in the namespace. Fixes the re-seed loop the cold-
start test exposed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 07:38:19 +02:00
k.rennerandClaude Opus 4.7 6e97bcdc30 fix(authentik): scp the dump instead of rsync — legacy lacks rsync
authentik-db-1 doesn't have rsync installed, so the seed initContainer's
"rsync \$LEGACY:\$LEGACY_DUMP /tmp/legacy.dump" spawned a remote rsync
that failed with "command not found". Switch this single-file dump
transfer to scp (always available via ssh).

The other seeds (paperless, immich, audiobookshelf, influxdb) keep
rsync because their legacy hosts have it; music-assistant already
uses ssh+tar for the same reason.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 07:30:26 +02:00
k.rennerandClaude Opus 4.7 940f5555ef fix: exclude paperless-redis + music-assistant-data PVCs from Velero
Both are RWO ceph-block PVCs whose running app pods hold them RW on
the same node where Velero's CSI snapshot-exposer tries to mount the
snapshot read-only. The kernel rejects the second RBD mount with
"cannot mount /dev/rbdN read-only", leaving daily Backup pods stuck
in ContainerCreating indefinitely.

paperless-redis is a cache so it's safe to skip; music-assistant-data
is repopulatable from the legacy LXC via the seed initContainer.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 07:21:03 +02:00
k.rennerandClaude Opus 4.7 8f7d5825da fix(migrations): correct Helm values keys for authentik + influxdb seed
Verified against the upstream chart values.yaml / templates:

- authentik chart exposes server.serviceAccountName (string, references
  an existing SA), not the bitnami-style top-level serviceAccount.{create,
  name}. server.initContainers and server.volumes were already correct.

- influxdb2 chart has NO .Values.initContainers hook in its StatefulSet
  template. Add the seed initContainer via spec.postRenderers (Kustomize
  patch on the rendered StatefulSet "influxdb-influxdb2"). The SSH
  Secret volume goes through the chart's supported top-level `volumes:`
  value (not `extraVolumes`).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 07:09:57 +02:00
k.rennerandClaude Opus 4.7 176d763a73 refactor(migrations): seed becomes initContainer for the remaining five apps
Replicates the paperless pattern (49a1e75) for immich, audiobookshelf,
music-assistant, authentik, and influxdb:

- Seed work moves into an initContainer in each app's Pod template
  (Deployment for immich/audiobookshelf/music-assistant; HelmRelease
  values for authentik server + influxdb StatefulSet).
- Same Pod owns the PVC → no RWO/RWX contention regardless of access
  mode. Main container only starts after seed completes (initContainer
  ordering enforces "app starts after seed").
- Idempotency gate: completed CNPG Backup (DB apps) or completed
  Velero Backup (PVC apps); skip-fast on warm starts.
- SOPS-encrypted SSH secrets and seed-RBAC moved into kubernetes/apps/
  <app>/. PVC-only apps' RBAC is cross-namespace (Role+Binding in
  velero ns; SA in app ns).
- Drops kubernetes/migrations/ entirely and the five
  kubernetes/clusters/homelab/<app>-seed.yaml Flux Kustomizations.
- Removes <app>-seed from each app Kustomization's dependsOn.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 22:54:25 +02:00
k.rennerandClaude Opus 4.7 49a1e75d08 refactor(paperless): seed becomes initContainer in the app Pod
Drops the standalone seed-Job pattern (kubernetes/migrations/paperless/
+ kubernetes/clusters/homelab/paperless-seed.yaml) for paperless. The
seed work moves into an initContainer in the paperless Deployment:
- idempotency-gated against completed CNPG Backups
- rsyncs data + media PVCs from the legacy VM
- pg_dump on legacy + rsync the file + pg_restore into pprl-pg
- creates a fresh CNPG Backup (fire-and-forget)

Same Pod owns the PVCs, so RWO/RWX access mode is irrelevant — no
contention with the existing app pod. initContainer ordering enforces
"main container starts after seed completes" by Pod semantics, not
Flux dependsOn timing.

SOPS-encrypted SSH secret moves to apps/paperless/. RBAC for the seed
SA moves into apps/paperless/seed-rbac.yaml.

This is the pattern; immich/audiobookshelf/music-assistant/authentik/
influxdb to follow once the shape is reviewed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 22:46:17 +02:00
k.rennerandClaude Opus 4.7 85a11380d9 fix(migrations): influxdb seed uses rsync, not ssh+tar streaming
Same fix as the pg_dump rewrite: stream-through-ssh of a multi-GB
data dir has no compression/parallelism/resume. rsync handles it
properly. Adds rsync to the apk install (ci-k8s doesn't ship it).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 22:34:20 +02:00
k.rennerandClaude Opus 4.7 4f22b6bf1b fix(migrations): pg_dump locally on legacy, rsync the file
The previous "ssh ... pg_dump > /tmp/legacy.dump" piped the entire
dump through an SSH stdout tunnel — no compression, no parallelism,
no resume. For multi-GB dumps that's the bottleneck.

Run pg_dump on the legacy host writing to /tmp/<db>-seed-dump there,
then rsync the file. rsync handles the transfer efficiently and we
get progress + (in principle) resume on retry. Cleanup ssh removes
the legacy temp file. Authentik also gains rsync in its apk install.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 22:30:40 +02:00
k.rennerandClaude Opus 4.7 8e32482c76 fix(migrations): two-phase rsync in audiobookshelf seed
Phase 1 rsyncs books while the legacy audiobookshelf systemd unit
stays up — no --delete so the running app's reads don't race. Books
are append-mostly so phase 1 carries the bulk of the data.

Phase 2 stops the unit briefly for a consistent snapshot of config +
metadata (SQLite, can't be rsynced live) and a delta of books with
--delete. Legacy downtime drops from hours to minutes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 22:15:45 +02:00
k.rennerandClaude Opus 4.7 3e9f6f8c20 chore(migrations): encrypted seed-ssh secrets for the new three apps
audiobookshelf, music-assistant, influxdb seed-ssh.sops.yaml now hold
the SOPS-encrypted SSH key, so the Flux Kustomizations can reconcile
the seed Jobs end-to-end without operator intervention.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 22:11:18 +02:00
k.rennerandClaude Opus 4.7 3306c34b4a fix(migrations): pre-check Backup before quiescing the app
Pipelines previously scaled the app to 0 + suspended Flux *before*
checking whether the Job would no-op. If a completed Backup already
exists, the Job's idempotency gate trips and the seed exits 0 — but
we'd already taken the app down for it.

Move the same gate query to the very top of each pipeline (right after
kubeconfig setup) and exit 0 if a Backup exists. The app stays up
when the seed isn't needed; only the cold path quiesces.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 22:09:41 +02:00
k.rennerandClaude Opus 4.7 08c4a78148 refactor(migrations): seed Jobs use prebuilt ci-k8s image
ci-k8s already ships kubectl/sops/openssh/curl/bash/tar. Replaces
postgres:16 + apt-get install + curl-install kubectl on DB seeds
(paperless/immich/authentik) and alpine:3.21 + apk add + curl-install
kubectl on PVC seeds (audiobookshelf/music-assistant/influxdb).

Net per-Job startup: a few seconds of apk for postgresql16-client
(DB seeds) or rsync (audiobookshelf), nothing for music-assistant /
influxdb. Same registry-mirror cache hit either way; far less network
on cold start.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 22:02:03 +02:00
k.rennerandClaude Opus 4.7 06083226dc feat(migrations): port audiobookshelf, music-assistant, influxdb to seed-Job template
Same shape as paperless/immich/authentik: idempotency-gated in-cluster
Job that rsyncs/streams legacy data into the live PVCs and creates a
Velero Backup CR labelled with the schedule. Pipelines collapse to
trigger-and-wait. Flux Kustomizations gate the app on the seed.

PVC-only apps gate on Velero Backup CRs (which live in the velero
namespace and survive an app-namespace wipe), which is more robust than
the CNPG-Backup gate used for DB apps. Cross-namespace RBAC: SA in
<app> namespace, Role+Binding in velero namespace.

music-assistant inherits the edge-zone nodeSelector + workload-class
toleration from its app pod (RWO PVC pinned to k8s-edge). influxdb
quiesces the StatefulSet rather than a Deployment.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 21:57:19 +02:00