Commit Graph
4354 Commits
Author SHA1 Message Date
k.rennerandClaude Opus 4.7 233a6a3cd0 fix(rook-ceph): Bump mgr memory to 1Gi/2Gi to stop OOMKill loop
ci/woodpecker/manual/rebuild-k8s Pipeline failed
Both rook-ceph-mgr-a and -b were CrashLoopBackOff with exit 137
(OOMKilled) at the previous 512Mi/1Gi sizing, which stalled Ceph
CSI provisioner calls and left PVCs stuck in FailedMount. Dashboard
+ volumes + rbd_support modules need more headroom than the initial
default.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 06:50:51 +02:00
k.rennerandClaude Opus 4.7 8eae7cd85e fix(cnpg): Wire explicit region to barman-cloud ObjectStore
ci/woodpecker/manual/migrate-audiobookshelf Pipeline was successful
ci/woodpecker/manual/migrate-paperless Pipeline was successful
ci/woodpecker/manual/migrate-authentik-db Pipeline was successful
Garage rejects HeadBucket calls with 400 Bad Request when boto3 sends
them without a signed region, which makes CNPG's bootstrap.recovery
fail at the barman-cloud-check-wal-archive step. Add REGION=garage to
the barman-cloud-creds Secret and reference it from the ObjectStore's
s3Credentials.region so barman-cloud-* invocations sign with the
correct region.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 23:03:31 +02:00
k.rennerandClaude Opus 4.7 e12b8d9ab2 fix(cnpg-barman-plugin): Split into its own Flux Kustomization
ci/woodpecker/manual/migrate-authentik-db Pipeline was successful
ci/woodpecker/manual/migrate-paperless Pipeline was successful
The barman-cloud Deployment mounts barman-cloud-{client,server}-tls
Secrets that are created by cert-manager Certificates in post-crds. In
a single-Kustomization infrastructure reconcile this is circular: the
Deployment can't start without the Secrets, and post-crds can't run
until infrastructure reports ready. Move the plugin into its own
Kustomization that dependsOn infrastructure-crds so the Secrets exist
before the plugin Pod schedules.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 22:38:00 +02:00
k.rennerandClaude Opus 4.7 fd453558f3 fix(velero): Move Schedules to post-crds
Same CRD-ordering fix as the barman Certificates: Velero Schedule CRs
need velero.io CRDs, which don't exist until the Velero HelmRelease
finishes installing. Moving them into post-crds/ lets the dry-run
succeed on a fresh bootstrap.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 22:27:40 +02:00
k.rennerandClaude Opus 4.7 52ac217ac5 fix(cnpg-barman-plugin): Move Certificates to post-crds
On a fresh cluster bootstrap the `infrastructure` Kustomization
reconciles before cert-manager's HelmRelease finishes installing its
CRDs, so Flux's dry-run fails on the barman-cloud Certificate /
Issuer with `no matches for kind Certificate in version
cert-manager.io/v1`. Move those three resources into post-crds/ which
runs after infrastructure and after cert-manager's CRDs are present.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 22:25:53 +02:00
rebuild-k8s pipeline 9ae0c4ee4a ci: Export cluster access files from rebuild 2026-04-20 20:08:46 +00:00
k.rennerandClaude Opus 4.7 413ba990e9 fix(migrate): Run ephemeral Postgres in-step for barman-cloud-backup
ci/woodpecker/manual/migrate-paperless Pipeline was successful
ci/woodpecker/manual/migrate-authentik-db Pipeline was successful
ci/woodpecker/manual/migrate-audiobookshelf Pipeline was successful
ci/woodpecker/manual/rebuild-k8s Pipeline failed
barman-cloud-backup reads PGDATA from the local filesystem — it is not
a pure network client — so connecting to a `services:` Postgres leaves
PGDATA unreachable and the backup fails on pg_control. Drop the
`services:` block and boot a local Postgres inside the seed-barman step
(initdb + pg_ctl start) so PGDATA is co-located with the barman
process. Loopback connection is trust-auth, ephemeral, step-scoped.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 21:35:14 +02:00
k.rennerandClaude Opus 4.7 b95501097e feat(migration): Cluster-independent seed path via k8s-migration bucket
ci/woodpecker/manual/migrate-authentik-db Pipeline failed
ci/woodpecker/manual/migrate-paperless Pipeline failed
ci/woodpecker/manual/migrate-audiobookshelf Pipeline was successful
The migrate-{paperless,audiobookshelf} pipelines now push legacy
tarballs directly to Garage (s3://k8s-migration/{paperless,audiobookshelf}/...)
using Woodpecker-scoped garage_migration_* secrets, so the seed
producer has no cluster dependency. Runs as a standalone `woodpecker
pipeline create` — no trigger-backup-job required.

In-cluster seed Jobs:
- Read the same tarballs from Garage via a new SOPS-encrypted
  `garage-migration-creds` Secret in each seed namespace.
- paperless-data-seed-v2 and audiobookshelf-data-seed-v2 replace the
  v1 Jobs (Jobs are immutable, so a name bump is needed).
- audiobookshelf-seed no longer owns the migration-bucket OBC; it
  moves back to the audiobookshelf namespace where its legacy
  trigger-backup-job still references it (retirement deferred).

Flux Kustomizations for *-seed gain decryption.provider=sops so the
new Secret can be applied.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 21:22:17 +02:00
k.rennerandClaude Opus 4.7 c08afd6195 feat(garage): Add k8s-migration bucket + migration-key
ci/woodpecker/manual/install-garage-synology Pipeline was successful
ci/woodpecker/manual/migrate-paperless Pipeline failed
Cluster-independent migration seed flow needs a dedicated Garage bucket
that the standalone migrate-* pipelines write to (and the seed Jobs
read from at bootstrap). Separate from k8s-cnpg (DB-only) and k8s-velero
(ongoing PVC backups).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 21:13:59 +02:00
k.rennerandClaude Opus 4.7 dbf4febace fix(migrate): Use python3-psycopg2 + system-site-packages for barman
ci/woodpecker/manual/migrate-paperless Pipeline failed
The postgres:16 image lacks gcc / libpq-dev, so pip build of barman's
psycopg2 dep fails. Install the compiled binary via apt and point the
venv at system site-packages so barman[cloud] reuses it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 21:05:47 +02:00
k.rennerandClaude Opus 4.7 2512481709 feat(k8s): symmetric bootstrap-restore via CNPG Barman + Velero
ci/woodpecker/manual/migrate-audiobookshelf Pipeline failed
ci/woodpecker/manual/migrate-authentik-db Pipeline failed
ci/woodpecker/manual/migrate-paperless Pipeline failed
Every cluster bootstrap now restores from the latest backup in Garage;
migration becomes a one-off seed that writes to the same paths the live
cluster uses.

DB path (ak-pg, pprl-pg):
- Cluster.bootstrap switched from initdb to recovery, reading the live
  Barman ObjectStore via externalClusters.
- migrate-{authentik-db,paperless} pipelines stand up an ephemeral
  Postgres, pg_restore the legacy dump into it, purge the Barman path,
  then barman-cloud-backup against that instance so the seed matches
  the format the live cluster writes.
- Obsolete post-bootstrap restore-job manifests removed.

App PVC path (paperless, audiobookshelf):
- defaultSnapshotMoveData=true on the Velero HelmRelease so CSI snapshot
  contents are moved to Garage (survive Ceph loss).
- Daily Velero Schedules for paperless, audiobookshelf, authentik.
- velero-bootstrap-restore Kustomization: Job creates one Restore per
  Schedule from latest Backup at rebuild; skips schedules with zero
  Backups so first-ever bootstrap is a no-op.
- Transitional *-seed Kustomizations pull legacy tarballs into fresh
  PVCs on first bootstrap (sentinel-idempotent). Velero has no
  external-producer API so this bridge is unavoidable during migration;
  retire once Velero has a verified backup.

Requires Woodpecker secrets garage_cnpg_key_id + garage_cnpg_secret_key
(cnpg-key.creds from /volume1/docker/garage/config/keys/ on nas-1).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 20:55:22 +02:00
k.rennerandClaude Opus 4.7 79d2198185 test(e2e): Add CNPG Barman backup-wiring checks
ci/woodpecker/manual/migrate-paperless Pipeline was successful
ci/woodpecker/manual/migrate-audiobookshelf Pipeline was successful
ci/woodpecker/manual/migrate-authentik-db Pipeline was successful
Verifies the full Barman plugin chain after the Velero block:

- barman-cloud plugin pod ready (must be in database ns, not
  cnpg-system — see wiki/cnpg-plugin-same-namespace-constraint)
- ObjectStore CRs exist for ak-pg and pprl-pg
- Cluster conditions report ContinuousArchiving=True
- ScheduledBackup 'daily' jobs have produced at least one
  completed Backup each

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 18:46:41 +02:00
k.rennerandClaude Opus 4.7 ab66c76306 feat(cnpg): Schedule daily base backup for ak-pg and pprl-pg
ci/woodpecker/manual/migrate-authentik-db Pipeline was successful
ScheduledBackup CRs trigger a base backup via the Barman Cloud
plugin daily at 02:30 UTC (after the PBS client's 02:00 NAS
backup, so the S3-resident backup data is picked up by PBS on
the following day's 02:00 run via Garage's underlying /volume1
filesystem). `immediate: true` forces the first run at apply time
so we can verify the backup path end-to-end; `backupOwnerReference:
self` keeps historical Backups tied to the schedule lifecycle.

WAL archiving is already continuous via postgres.yaml
spec.plugins[].isWALArchiver=true.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 18:41:33 +02:00
k.rennerandClaude Opus 4.7 e178b9858c fix(cnpg): Move barman-cloud plugin into database namespace
ci/woodpecker/manual/migrate-paperless Pipeline was successful
CNPG 1.29's plugin controller ONLY discovers plugin Services and
their mTLS secrets in the operator's own namespace, per
internal/controller/plugin_controller.go:

    // We only consider the secrets that are installed in the
    // operator namespace because plugins need to be deployed
    // in the same namespace as the operator.
    if obj.GetNamespace() != r.OperatorNamespace {
        return nil
    }
    [...]
    client.InNamespace(r.OperatorNamespace),

The vendored manifest installed barman-cloud into cnpg-system, which
the operator (running in database) was blind to — hence the
phaseReason "Unknown plugin: 'barman-cloud.cloudnative-pg.io'. This
may be caused by the plugin not being loaded correctly by the operator."

Rewrote the manifest to target `database` (12 namespace fields
flipped). Removed the now-orphan cnpg-system namespace definition.
Reverted the temporary --log-level=debug arg on the operator.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 18:33:59 +02:00
k.renner 92d99da83e debug(cnpg): Use additionalArgs --log-level=debug (chart-correct key) 2026-04-20 18:22:42 +02:00
k.rennerandClaude Opus 4.7 f4b3abdea8 debug(cnpg): Enable operator debug logging
Temporary — needed to see why the plugin controller is silent on the
barman-cloud Service. Revert once discovered.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 18:21:10 +02:00
k.rennerandClaude Opus 4.7 d234c3302f fix(cnpg): Include barman-cloud plugin in operator config
ci/woodpecker/manual/migrate-paperless Pipeline was successful
ci/woodpecker/manual/migrate-authentik-db Pipeline was successful
ci/woodpecker/manual/migrate-audiobookshelf Pipeline was successful
The CNPG operator refused to reconcile Clusters that reference the
Barman Cloud plugin with:
  "Cluster cannot proceed to reconciliation due to an unknown plugin
   being required"

Its loaded config showed includePlugins="" — the operator's plugin
allowlist was empty. Setting INCLUDE_PLUGINS in the controller-
manager ConfigMap (via Helm values.config.data) lets the operator
recognize and wire up the plugin's Service (cnpg-system/barman-cloud)
discovered via its cnpg.io/pluginName label.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 16:13:10 +02:00
k.rennerandClaude Opus 4.7 00b2ae2a58 feat(cnpg): Wire Barman Cloud WAL archiving for ak-pg and pprl-pg
Adds per-cluster ObjectStore CRs pointing at Garage's k8s-cnpg bucket
(subpaths ak-pg/ and pprl-pg/) with gzip+AES256 for both WAL and data,
and a 30-day retention policy. Credentials stored as SOPS-encrypted
Secrets (barman-cloud-creds) in each namespace.

Each Cluster adds spec.plugins referencing barman-cloud.cloudnative-pg.io
as the WAL archiver pointing at its ObjectStore. Bootstrap remains
initdb for now (existing clusters ignore bootstrap changes) — switching
to bootstrap.recovery for disaster-recovery bootstrap is a follow-up
once the first base backup is confirmed in Garage.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 15:30:28 +02:00
k.rennerandClaude Opus 4.7 aea40809ec feat(cnpg): Install Barman Cloud CNPG-I plugin v0.12.0
ci/woodpecker/manual/migrate-paperless Pipeline was successful
Vendored upstream manifest from
github.com/cloudnative-pg/plugin-barman-cloud releases/download/v0.12.0/manifest.yaml.
Installs the plugin controller into cnpg-system namespace with its
ObjectStore CRD (barmancloud.cnpg.io/v1) and cert-manager-issued
self-signed client/server TLS. cnpg-system namespace added to the
namespaces manifest.

Paired ObjectStore CRs + Cluster spec wiring for ak-pg / pprl-pg
come in a follow-up commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 15:22:54 +02:00
k.rennerandClaude Opus 4.7 b2d6d84f9f fix(e2e): Disambiguate velero backup/restore vs CNPG backup CRDs
ci/woodpecker/manual/e2e-test Pipeline failed
`kubectl get backup` was resolving to `backups.postgresql.cnpg.io`
(CNPG) instead of `backups.velero.io`, so the backup-phase and
item-count checks timed out against a non-existent CNPG Backup
even though Velero's own backup Completed (restore worked, marker
survived). Fully-qualifying to backup.velero.io / restore.velero.io.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 15:15:52 +02:00
k.rennerandClaude Opus 4.7 9e44ada89c test(e2e): Add Velero backup+restore roundtrip checks
ci/woodpecker/manual/e2e-test Pipeline failed
ci/woodpecker/manual/migrate-authentik-db Pipeline was successful
ci/woodpecker/manual/migrate-audiobookshelf Pipeline was successful
Adds a CSI snapshot data movement roundtrip to e2e-test.sh:

- BSL default reports Available
- node-agent DaemonSet has 3 pods ready
- VolumeSnapshotClass ceph-block-snapclass labelled for Velero
- Disposable velero-e2e namespace writes a marker into a ceph-block PVC
- Backup with snapshotMoveData=true reaches Completed with >=1 item
- Namespace deleted, Restore reaches Completed
- Restored pod comes back and the marker file matches

Left-behind state on pass is cleaned up in-line; on failure the Backup
and Restore YAML are dumped into check.out for inspection.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 14:55:09 +02:00
k.rennerandClaude Opus 4.7 475553e979 fix(velero): Privileged PSS + install snapshot-controller
Two blockers surfaced when the Velero HelmRelease reconciled:

1. velero namespace defaulted to `baseline` PSS — node-agent
   DaemonSet needs hostPath mounts for /var/lib/kubelet/{pods,plugins}
   and was rejected with PodSecurity violations. Labelled the
   namespace `privileged` to match the rook-ceph/crowdsec/ingress
   pattern.

2. The cluster had no `snapshot.storage.k8s.io` CRDs — Rook-Ceph
   CSI does not ship them. Added the piraeus snapshot-controller
   chart in kube-system; it provides the VolumeSnapshot,
   VolumeSnapshotContent, and VolumeSnapshotClass CRDs plus the
   controller that reconciles them. Required for Velero CSI
   snapshot data movement and for our rook-ceph VolumeSnapshotClass
   manifest to apply.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 14:34:21 +02:00
k.rennerandClaude Opus 4.7 561eb61309 feat(velero): Install Velero + Rook-Ceph VolumeSnapshotClass
Adds Velero via HelmRelease in a new `velero` namespace, configured
for CSI snapshot data movement to the Garage S3 endpoint on nas-1
(k8s-velero bucket). Credentials SOPS-encrypted via the repo's age
recipient; AWS plugin loaded as an initContainer; node-agent enabled
for data movement and FSB fallback; native VSL snapshots disabled in
favour of the CSI path.

Paired with a Rook-Ceph VolumeSnapshotClass for RBD (default) and
CephFS — labelled for Velero auto-discovery. Adds a `velero`
namespace definition so Flux has somewhere to put everything.

No checksum gymnastics required on Garage today, but keeps the
aws-sdk-go-v2 `checksumAlgorithm=""` workaround documented in-place
against regressions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 14:27:32 +02:00
k.rennerandClaude Opus 4.7 3b71367dcc fix(garage): Split smoke test into diagnostic + roundtrip
ci/woodpecker/manual/test-garage-smoke Pipeline was successful
ci/woodpecker/manual/migrate-audiobookshelf Pipeline was successful
ci/woodpecker/manual/migrate-authentik-db Pipeline was successful
ci/woodpecker/cron/security-scan Pipeline was successful
ci/woodpecker/manual/migrate-paperless Pipeline was successful
no_log: true hid the roundtrip's actual error. Now a separate
network-probe task (DNS/TLS/cert, no secrets) runs first with full
logging, then the secret-bearing aws-cli roundtrip runs with no_log
but a debug task prints only its stdout/stderr lines so errors
surface without leaking env keys.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 11:43:30 +02:00
k.rennerandClaude Opus 4.7 52feb6529c ci(garage): Add test-garage-smoke pipeline
ci/woodpecker/manual/test-garage-smoke Pipeline failed
Reads the velero-key creds from nas-1, then delegates a full S3
roundtrip (list/put/list/get+diff/delete) to the CI runner against
https://s3.nas-1.renner.uno. Exercises DNS, reverse proxy, TLS cert,
and Garage's path-style S3 API end-to-end. Secrets kept out of the
log via no_log.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 11:40:56 +02:00
k.rennerandClaude Opus 4.7 d864c70647 feat(garage): Bootstrap buckets, keys, and grants in pipeline
ci/woodpecker/manual/install-garage-synology Pipeline was successful
Extends install-garage-synology with an idempotent step that creates
three buckets (k8s-velero, k8s-cnpg, k8s-secrets), a dedicated key
per bucket (velero-key, cnpg-key, secrets-key), and the matching
bucket allow grants. Garage does not expose an access-key secret
after creation, so the secrets are captured at create time and
persisted to chmod-600 files under /volume1/docker/garage/config/keys/
on nas-1 for later retrieval via sudo cat. no_log: true keeps them
out of the pipeline log.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 11:19:48 +02:00
k.rennerandClaude Opus 4.7 6f6a748a07 ci(garage): Add install-garage-synology pipeline
ci/woodpecker/manual/migrate-audiobookshelf Pipeline failed
ci/woodpecker/manual/install-garage-synology Pipeline was successful
Dedicated manual pipeline gated on PIPELINE=install-garage-synology.
Adds sshpass to apk deps — required for Synology password SSH auth.
Mirrors the existing per-playbook pipeline pattern (one .woodpecker
file per play, hardcoded ansible-playbook invocation).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 10:59:46 +02:00
k.rennerandClaude Opus 4.7 df4a1c842e chore(ci): Remove rogue run-ansible pipeline
Created by an unsanctioned agent session; not the intended deployment
path for Ansible playbooks in this repo.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 10:55:12 +02:00
k.rennerandClaude Opus 4.7 7abd426e6b fix(ci): Install sshpass in run-ansible pipeline
Needed for Synology playbooks — DSM does not support SSH key auth for
the ladmin user, so nas-1 host_vars sets ansible_ssh_pass. Without
sshpass in the CI container, those plays fail before the first task.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 10:52:36 +02:00
k.rennerandClaude Opus 4.7 69b8ffb618 feat(nas-1): Add Garage S3 service for K8s backups
ci/woodpecker/manual/run-ansible Pipeline failed
Single-node Garage in DSM Container Manager as the on-prem S3 target
for Velero and the CNPG Barman Cloud Plugin. SQLite engine over LMDB
avoids unclean-shutdown corruption risk given replication_factor=1
with no peers to rebuild from. Ports bound to loopback; DSM reverse
proxy terminates TLS. Bucket + key bootstrap is a one-shot manual
step post-install.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 10:49:25 +02:00
rebuild-k8s pipeline 08ef77937d ci: Export cluster access files from rebuild
ci/woodpecker/manual/migrate-authentik-db Pipeline was successful
ci/woodpecker/manual/migrate-paperless Pipeline was successful
ci/woodpecker/manual/migrate-audiobookshelf Pipeline failed
2026-04-20 06:47:23 +00:00
k.rennerandClaude Opus 4.7 ada1fdf954 fix(flux): Gate apps on real Ceph readiness, not manifest apply
ci/woodpecker/manual/rebuild-k8s Pipeline failed
infrastructure-ceph-cluster was reporting Ready as soon as the
CephCluster/CephBlockPool/CephFilesystem manifests applied, because
kstatus can't interpret Rook CR conditions by default. Downstream apps
(which `dependsOn: infrastructure-ceph-cluster`) then reconciled too
early, scheduled pods, and crash-looped init containers against storage
that wasn't yet serving I/O — resolving eventually, but noisily.

Add healthCheckExprs that block the Kustomization until each Rook CR's
status.phase == 'Ready'. Apps now wait for a truly-healthy Ceph.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 07:58:50 +02:00
rebuild-k8s pipeline e72486ba4b ci: Export cluster access files from rebuild
ci/woodpecker/manual/migrate-authentik-db Pipeline was successful
ci/woodpecker/manual/migrate-paperless Pipeline was successful
ci/woodpecker/manual/migrate-audiobookshelf Pipeline was successful
2026-04-20 05:32:11 +00:00
k.rennerandClaude Opus 4.7 0c21e4c2a3 fix(k8s): Hard-stop Talos VMs on -replace instead of graceful shutdown
ci/woodpecker/push/deploy-infrastructure Pipeline was successful
ci/woodpecker/push/security-scan Pipeline was successful
ci/woodpecker/manual/rebuild-k8s Pipeline was successful
Rebuild-k8s pipeline was hanging on the destroy phase of `tofu apply
-replace` because BPG issues a graceful shutdown and waits up to 1800s
(provider default `timeout_shutdown_vm`) for Talos guests that don't
respond. Setting `stop_on_destroy = true` on all 4 K8s VMs switches the
provider to `qm stop` (hard power-off), so destroy is immediate.

- Module gains `stop_on_destroy` variable (default false preserves
  existing behavior for every other VM).
- Opted in on k8s-1/2/3 (controlplane) and k8s-edge (worker).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 07:19:17 +02:00
k.rennerandClaude Opus 4.7 da76201f1c fix(k8s-edge): Apply taint via kubelet register-with-taints
Reverts the bootstrap.sh kubectl-taint hack and fixes it properly in
the Talos machine config. Talos machine.nodeTaints is silently
ineffective on workers — NodeRestriction admission blocks post-join
taint writes by worker kubelets. Kubelets CAN register with taints via
the --register-with-taints flag, so pass it through kubelet.extraArgs
and the taint lands at node registration instead of post-hoc reconcile.

Also drops the workload-class nodeLabel (same NodeRestriction issue:
arbitrary-namespace labels aren't kubelet-settable); only the
kubernetes.io zone label stays since that one is on the allow-list.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 07:02:38 +02:00
k.rennerandClaude Opus 4.7 816651db39 fix(bootstrap): Reinforce worker taints via kubectl after node join
Talos nodeTaints in the per-node config patch don't always take effect
on first kubelet registration (pipeline 508 saw k8s-edge come up with
zero taints despite talosctl apply-config succeeding). Result: every
stateful workload the scheduler cared about landed on the edge node —
Grafana, Prometheus, Loki, Authentik pods, etc.

Apply taints + labels via kubectl after the node is Ready, which
survives future reconciliation regardless of how Talos applies the
config patch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 06:57:37 +02:00
rebuild-k8s pipeline a3361a62fa ci: Export cluster access files from rebuild
ci/woodpecker/manual/migrate-authentik-db Pipeline was successful
ci/woodpecker/manual/migrate-paperless Pipeline was successful
ci/woodpecker/manual/migrate-audiobookshelf Pipeline was successful
2026-04-19 19:25:51 +00:00
rebuild-k8s pipeline 925b123740 ci: Export cluster access files from rebuild
ci/woodpecker/manual/migrate-authentik-db Pipeline was successful
ci/woodpecker/manual/migrate-paperless Pipeline was successful
ci/woodpecker/manual/migrate-audiobookshelf Pipeline was successful
ci/woodpecker/manual/rebuild-k8s Pipeline failed
2026-04-19 18:07:28 +00:00
k.rennerandClaude Opus 4.7 36b8096777 chore(audiobookshelf): Extend wait-apps budget to 80 min
ci/woodpecker/manual/rebuild-k8s Pipeline failed
First measurement on a 70 GB library took ~70 min end-to-end: 17 min
LXC tar + upload, 52 min S3 download + extract into PVCs. 30 min was
too tight — e2e would fire before the restore job wrote its sentinel.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 19:56:56 +02:00
k.rennerandClaude Opus 4.7 7bcabf7d33 fix(audiobookshelf): Add Cilium network policies
ci/woodpecker/manual/migrate-authentik-db Pipeline was successful
The namespace had no CNP, but ingress-nginx's egress policy is an
allow-list that only permits specific target namespaces. Requests to
audiobooks.k.renner.uno hit 504 because Cilium dropped ingress-nginx →
audiobookshelf-pod connects.

Adds the same default-deny-ingress pattern used by paperless/authentik
and extends ingress-nginx-egress with the new namespace.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 19:54:26 +02:00
k.rennerandClaude Opus 4.7 c34591e7ba feat: Add dedicated K8s edge node for pinned/USB workloads
ci/woodpecker/push/deploy-infrastructure Pipeline was successful
ci/woodpecker/push/security-scan Pipeline was successful
ci/woodpecker/manual/migrate-authentik-db Pipeline was successful
ci/woodpecker/manual/migrate-paperless Pipeline was successful
Introduces a 4th Talos node (VMID 153, k8s-edge) outside the Ceph quorum
and general scheduling pool. Tainted workload-class=pinned:NoSchedule so
only tolerating pods schedule here; intended for Home Assistant (Z-Wave +
EDI21 USB sticks) and Music Assistant (latency-sensitive; moved out of
the HA VM originally because in-VM scheduling caused audio stutters).

Rationale: NoSchedule-tainting one of the existing 3 nodes would shrink
general+Ceph capacity from 3 to 2 and compromise redundancy for every
other workload. A dedicated node keeps the symmetric 3-node layout
intact, and USB passthrough pins this node to a physical host anyway so
non-HA is the correct model.

target_node is a placeholder until the Proxmox host carrying the USB
dongles is chosen; module already ignores node_name. USB passthrough
(usb = [...]) and the workload manifests get added in follow-up commits.

bootstrap.sh WORKERS array and e2e-test.sh 4-node check already landed
with the Audiobookshelf commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 18:38:18 +02:00
k.rennerandClaude Opus 4.7 adfdc7e756 fix(audiobookshelf): Hardcode Debian config/metadata paths
ci/woodpecker/manual/migrate-audiobookshelf Pipeline was successful
regex_search(..., multiline=True) returns a list, so the tar command
was expanding to tar -C ['/usr/share/audiobookshelf/config']. Drop the
auto-detection and just pin the Debian defaults — the LXC's
/etc/default/audiobookshelf confirmed those are the values in use.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 18:36:25 +02:00
k.rennerandClaude Opus 4.7 904bcac70a feat: Migrate Audiobookshelf from LXC to K8s
ci/woodpecker/manual/migrate-audiobookshelf Pipeline failed
Mirrors the Paperless pattern: new namespace, RWX PVCs, OBC-based S3
migration bucket, trigger-backup-job that kicks a Woodpecker pipeline,
restore-data-job that streams tarballs directly from S3 into PVCs.

The Deployment uses an initContainer + sentinel file so the app never
boots on an empty /config while the SQLite tarball is still extracting,
and the restore job is idempotent (skipped when the sentinel exists).

Also extends bootstrap.sh wait-apps + e2e-test.sh to cover the new
Kustomization, OBC, TLS cert, and /healthcheck on audiobooks.k.renner.uno.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 18:31:46 +02:00
rebuild-k8s pipeline 15b1574efd ci: Export cluster access files from rebuild
ci/woodpecker/manual/migrate-authentik-db Pipeline was successful
ci/woodpecker/manual/migrate-paperless Pipeline was successful
2026-04-19 15:28:20 +00:00
k.rennerandClaude Opus 4.7 6cf74eb15d feat: Switch ingresses to LE staging and chain e2e onto rebuild-k8s
ci/woodpecker/manual/e2e-test Pipeline was successful
ci/woodpecker/manual/migrate-paperless Pipeline was successful
ci/woodpecker/manual/migrate-authentik-db Pipeline was successful
ci/woodpecker/cron/security-scan Pipeline was successful
ci/woodpecker/manual/rebuild-k8s Pipeline was successful
- Flip all ingress cert-manager.io/cluster-issuer annotations from
  letsencrypt-prod to letsencrypt-staging. Five rebuilds in a week
  tripped LE's per-identifier rate limit (5 certs / 168h) and left 3
  hostnames with the ingress-nginx fake cert. Staging has ~30k/week,
  so rebuilds stop being rate-limited. ClusterIssuer definitions for
  both remain in post-crds; flip back per-ingress when ready for prod.
- Rework e2e-test.sh TLS check to validate the cert *issuer* contains
  "Let's Encrypt" rather than chain-validating against the public CA
  bundle. Same check works for both staging and prod, and still
  catches the real failure mode (fake cert = cert-manager broken).
- HTTP checks now use -k since staging certs aren't publicly trusted;
  the separate TLS check is what asserts cert-manager worked.
- Chain the e2e step onto .woodpecker/rebuild-k8s.yml so every rebuild
  validates the resulting cluster end-to-end before reporting success.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 10:09:07 +02:00
k.rennerandClaude Opus 4.7 f4b191367a feat: Add end-to-end test pipeline for the k8s cluster
ci/woodpecker/manual/e2e-test Pipeline failed
Add a manual Woodpecker pipeline (`PIPELINE=e2e-test`) that drives a
real test of the cluster end-to-end, beyond what `rebuild-k8s`'s
wait-for-reconciliation covers:

- All nodes Ready; all Flux Kustomizations and HelmReleases Ready; zero
  failing pods cluster-wide.
- Ceph: CephCluster Ready, 3 OSDs Running, CephObjectStore Ready.
- Storage: provisions a `ceph-block` PVC, mounts it in a short-lived
  pod, writes+reads a file to prove the CSI path works, then cleans up.
- OBCs: authentik + paperless migration buckets Bound.
- Databases: CNPG ak-pg and pprl-pg both have 2 ready instances.
- TLS: validates each ingress hostname's cert with OpenSSL against the
  public CA bundle (no `-k`) so we actually catch fake certs / LE
  failures.
- App health endpoints: Authentik live/ready probes, Grafana /api/health
  reports database=ok, paperless/docs reachable (not 5xx).
- External DNS: verifies public resolution via 1.1.1.1.

Script lives at kubernetes/e2e-test.sh so it can also be run locally
once kubeconfig is loaded. The pipeline decrypts kubeconfig.sops from
kubernetes/access/ (committed by rebuild-k8s) and runs it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 09:59:04 +02:00
rebuild-k8s pipeline aeee204bff ci: Export cluster access files from rebuild
ci/woodpecker/cron/update-all Pipeline failed
ci/woodpecker/manual/migrate-paperless Pipeline was successful
ci/woodpecker/manual/migrate-authentik-db Pipeline was successful
2026-04-18 23:48:08 +00:00
k.rennerandClaude Opus 4.7 b80cef6007 fix: Tighten k8s bootstrap dependency chain
ci/woodpecker/manual/rebuild-k8s Pipeline was successful
- Drop duplicate HelmRepository external-dns in apps namespace (owned by
  infrastructure/external-dns-unifi) to end SSA ownership flapping.
- Infrastructure Kustomization timeout 5m -> 15m; first-boot Rook +
  cert-manager + cnpg + cilium do not fit in 5m on a cold cluster.
- authentik-db timeout 10m -> 30m; the restore job polls S3 for up to
  10min and needs margin against the trigger-backup job.
- Remove orphaned sealed-secrets / external-secrets / metallb dirs that
  were never referenced (Flux decrypts SOPS natively; Cilium handles L2).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 01:31:23 +02:00
k.rennerandClaude Opus 4.7 80945a38d7 fix: Scope OBC health check to each app's own bucket
ci/woodpecker/manual/migrate-authentik-db Pipeline was successful
ci/woodpecker/manual/migrate-paperless Pipeline failed
Previously the OBC CEL health check lived on infrastructure-ceph-cluster,
which holds OBCs for every app. A slow bind in one namespace stalled
every other app that depends on infrastructure-ceph-cluster. Move each
OBC into its own app Kustomization (paperless-db, authentik-db) and
apply the health check there so each app only waits on its own bucket.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 00:39:52 +02:00
rebuild-k8s pipeline 2844dfdaae ci: Export cluster access files from rebuild
ci/woodpecker/manual/migrate-authentik-db Pipeline was successful
ci/woodpecker/manual/migrate-paperless Pipeline was successful
2026-04-18 22:08:17 +00:00