Final stage of the seed-woodpecker -> dedicated woodpecker KS move. The
commonAnnotations prune:disabled guard on apps/woodpecker/ has done its job: the
ownership handoff completed (verified — wp-pg + all app objects adopted by the
woodpecker KS, RESTARTS=0, nothing pruned), so the app objects no longer need
blanket prune protection and should be normally prunable by their owning KS again.
Removes only the commonAnnotations block. The 4 CNPG DB objects keep their
per-file prune: disabled (permanent data-guard on Cluster/wp-pg etc.) — verified
kubectl kustomize apps/woodpecker now annotates exactly those 4, not the 13 app
objects.
Rolls woodpecker-server + agent once more as the annotation leaves their pod
templates — the same accepted CI restart as stage 1.
Co-Authored-By: Claude Bot <claude-bot@renner.uno>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Restructures woodpecker into its own Flux Kustomization instead of splitting its
app across the catch-all apps KS and its DB across seed-woodpecker.
- Add clusters/homelab/woodpecker.yaml: dedicated KS over apps/woodpecker with
the infrastructure-cnpg-barman dep + CNPG Cluster healthCheckExprs (moved off
seed-woodpecker) so the CNPG concerns live here, not in the shared apps KS.
- Remove woodpecker/ from apps/kustomization.yaml (catch-all apps KS no longer
owns the woodpecker app objects).
- git mv seed/woodpecker/{cluster,objectstore,scheduled-backup,
hetzner-creds-externalsecret}.yaml + MIGRATION.md -> apps/woodpecker/, and add
the four DB manifests to apps/woodpecker/kustomization.yaml.
- Delete clusters/homelab/seed-woodpecker.yaml and the now-empty
kubernetes/seed/woodpecker/.
- bootstrap.sh: add a wait_kustomization woodpecker entry (it used to be covered
by 'wait apps'); e2e-test.sh: add woodpecker to the KS readiness checks.
Safe dual ownership transfer (nothing pruned): every object carries prune:
disabled from stage 1 (#394) — the app objects via commonAnnotations, the DB
objects per-file — so when the apps KS drops woodpecker/ and seed-woodpecker is
deleted, both GC sweeps skip these live objects and the new woodpecker KS adopts
them in place. No CNPG restart (Cluster is not a Deployment); no app restart
(manifests unchanged, only the owning KS label flips).
Verified: kubectl kustomize apps/woodpecker builds 17 objects (13 app + 4 DB
incl. wp-pg Cluster), all prune-protected; bash -n bootstrap.sh + e2e-test.sh
OK; wp-pg is the sole CNPG cluster in apps ns and hetzner-s3-creds is used only
by wp-pg-store; seed/woodpecker fully removed.
Stage 3 will drop the transient commonAnnotations (one more accepted app roll).
Co-Authored-By: Claude Bot <claude-bot@renner.uno>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Folds the app-object protection into stage 1 now that a woodpecker CI restart is
acceptable. commonAnnotations adds kustomize.toolkit.fluxcd.io/prune: disabled to
every object under apps/woodpecker/ (the woodpecker-ci Namespace, server/agent
Deployments, Service, Ingress, PDB, NetworkPolicy, ResourceQuota/LimitRange,
RBAC, ExternalSecret), so when 'woodpecker/' is later removed from
apps/kustomization.yaml the catch-all apps KS can't prune them mid-handoff.
Trade-off accepted: kustomize propagates commonAnnotations into the two
Deployments' pod templates, so this rolls woodpecker-server + agent once on
apply (and once more when removed in the final stage). Confirmed harmless — the
DB (wp-pg) is a separate CNPG resource, untouched.
Co-Authored-By: Claude Bot <claude-bot@renner.uno>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Stage 1 of moving woodpecker off seed-woodpecker into a dedicated woodpecker KS.
Adds kustomize.toolkit.fluxcd.io/prune: disabled, in place, to the four
data-bearing DB objects in seed/woodpecker/ so the later ownership handoff can
never prune them:
- Cluster/wp-pg (pruning deletes the DB's data PVCs)
- ObjectStore/wp-pg-store (barman backup target)
- ScheduledBackup/wp-pg-daily
- ExternalSecret/hetzner-s3-creds (barman S3 creds)
Landing the annotation via the existing seed-woodpecker KS guarantees it is live
on the objects before any move. Metadata-only; no spec change, so CNPG/the
running wp-pg pod are untouched. The Cluster keeps the annotation permanently as
a data-guard; the others are incidental to the same handoff.
The woodpecker *app* objects (server/agent/Namespace/etc., currently under the
catch-all apps KS) are protected separately in the next stage — they need
per-object top-level annotations rather than commonAnnotations, which would
propagate into the Deployments' pod templates and needlessly restart live CI.
Co-Authored-By: Claude Bot <claude-bot@renner.uno>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Addresses the follow-up review on #390: three more comments still described the
removed influxdb-seed Job, some in the present tense (contradicting the note the
prior commit added).
- helmrelease.yaml adminUser comment: drop the 'sibling influxdb-seed Job reads
the secret / deletes the bucket' description; state the admin token comes from
the influxdb-bootstrap Secret (OpenBao via ESO).
- helmrelease.yaml resource/timeout rationale: reword 'after a partial seed' and
'batches from the seed Job' to the seed-independent conditions (large dataset /
large write batches) that still justify the 6Gi limit + 60s write timeout.
- ha-token-job.yaml: drop 'Same wait pattern as the seed Job's wait-for-influxd';
describe the two-stage wait directly.
Verified: kubectl kustomize apps/influxdb builds; the only remaining 'seed'
strings are accurate migration-rationale comments and the OpenBao KV path.
Co-Authored-By: Claude Bot <claude-bot@renner.uno>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Without the suffix the app resolves devices only via its 2-minute mDNS
cache; the authoritative-DNS leg of MdnsConnectCallback is disabled and
the presence sweep falsely marks online always-on devices missing
(5 devices confirmed online but flagged, 2026-07-24).
Fixes#389
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Addresses the doc-only suggestion on #390: explain in the new influxdb KS that
dropping seed-influxdb's infrastructure-cnpg-barman dependency is intentional
(copy-paste boilerplate; InfluxDB has no CNPG cluster, only a ceph-block PVC).
Co-Authored-By: Claude Bot <claude-bot@renner.uno>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Step 2 of the InfluxDB seed->apps handoff. #388 already landed
prune: disabled on the live HelmRelease/HelmRepository/ExternalSecret, so
retiring seed-influxdb here cannot trigger a helm uninstall — GC skips the
annotated objects and the new influxdb KS adopts them in place.
- git mv kubernetes/seed/influxdb/ -> kubernetes/apps/influxdb/ (annotations
from #388 travel with the files).
- Add kubernetes/clusters/homelab/influxdb.yaml (KS 'influxdb', path
apps/influxdb, deps infrastructure/crds/ceph). Delete seed-influxdb.yaml.
Review fixes from the closed atomic PR (#387):
- bootstrap.sh: wait on 'influxdb' instead of the deleted 'seed-influxdb'
(would otherwise spin the full 600s timeout on cold bootstrap).
- e2e-test.sh: swap the seed-influxdb readiness check for 'influxdb'.
- Fix stale comment paths kubernetes/seed/influxdb/... -> apps/influxdb/... in
network-policies/home-assistant-ns.yaml and apps/monitoring/
helmrelease-prometheus.yaml.
- Trim the helmrelease.yaml comment that described the removed seed-job.yaml,
and the ha-token-job.yaml 'at cutover moves to apps/influxdb' note (this is
that cutover).
Left intentionally: externalsecret.yaml key 'seed/influxdb/bootstrap' is an
OpenBao KV path, not a repo path — moving it needs a vault-side secret
migration, out of scope here.
Verified: kubectl kustomize apps/influxdb builds all objects with the right
prune/force annotations; bash -n passes on bootstrap.sh + e2e-test.sh; no
remaining seed-influxdb references in the repo (only the OpenBao KV path).
Co-Authored-By: Claude Bot <claude-bot@renner.uno>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Step 1 of a two-step seed->apps handoff for InfluxDB (the move itself follows
in a separate PR). Adds kustomize.toolkit.fluxcd.io/prune: disabled to the
HelmRelease, HelmRepository, and ExternalSecret *in place* under
kubernetes/seed/influxdb/, with no move and no KS changes.
Why split: doing the annotate + move + delete-old-KS in one commit races the
new KS's first reconcile (which stamps the annotation onto the live objects)
against the old KS's deletion-GC. If GC wins, it prunes the un-annotated
HelmRelease -> helm uninstall -> the running InfluxDB StatefulSet is torn down.
Landing the annotation via the existing seed-influxdb KS first guarantees it is
live on the objects before any prune can run, per Flux's documented pattern.
No behavioural change: only a kustomize metadata annotation is added, so
helm-controller performs no upgrade and the server is untouched. After merge,
verify the annotation is live on all three objects, then land the move PR.
Co-Authored-By: Claude Bot <claude-bot@renner.uno>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Second step of dissolving the transitional 'seed' layer (after
music-assistant). Move the audiobookshelf config/metadata/books PVCs from
seed-audiobookshelf into the app's own Kustomization and retire the seed KS.
- git mv kubernetes/seed/audiobookshelf/pvcs.yaml -> kubernetes/apps/audiobookshelf/
and add it to the app kustomization.
- Drop 'dependsOn: seed-audiobookshelf' from the audiobookshelf KS (it now owns
the 3 PVCs directly; ceph dep already present, no CNPG involved).
- Delete kubernetes/clusters/homelab/seed-audiobookshelf.yaml and the now-empty
kubernetes/seed/audiobookshelf/.
- Remove seed-audiobookshelf from the e2e-test.sh Kustomization readiness checks
(it no longer exists post-merge) — same fix the review flagged on the
music-assistant PR, done proactively here.
Safe ownership transfer: all 3 PVCs carry kustomize.toolkit.fluxcd.io/prune:
disabled, so Flux GC skips them when seed-audiobookshelf is pruned; the
audiobookshelf KS adopts them on reconcile. Verified: PVCs Bound + prune-disabled
live; kubectl kustomize apps/audiobookshelf builds all 3.
Co-Authored-By: Claude Bot <claude-bot@renner.uno>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Addresses review on #385.
Blocking: e2e-test.sh asserted kustomization/seed-music-assistant Ready, but
this PR deletes that KS — the test would fail post-merge with 'not found'.
Remove it from the checked list (line 110).
Also add a clarifying note in MIGRATION-sops-openbao.md that
seed-music-assistant has since been retired (the 2026-07-14 log line is left
factually intact as a historical record).
Co-Authored-By: Claude Bot <claude-bot@renner.uno>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
First step of dissolving the transitional 'seed' layer: move the
music-assistant-data PVC from seed-music-assistant into the app's own
Kustomization and retire the seed KS.
- git mv kubernetes/seed/music-assistant/pvcs.yaml -> kubernetes/apps/music-assistant/
and add it to the app kustomization.
- Drop 'dependsOn: seed-music-assistant' from the music-assistant KS (it now
owns the PVC directly; ceph dep already present, no CNPG involved).
- Delete kubernetes/clusters/homelab/seed-music-assistant.yaml and the now-empty
kubernetes/seed/music-assistant/.
Safe ownership transfer: the PVC already carries
kustomize.toolkit.fluxcd.io/prune: disabled, so Flux GC skips it even when the
seed-music-assistant KS is pruned — the object is never torn down, and the
music-assistant KS adopts it on next reconcile. Verified: PVC is Bound and
prune-disabled live; kubectl kustomize apps/music-assistant builds with the PVC.
Pilot for the per-app seed->apps relocation; remaining apps (audiobookshelf,
influxdb, woodpecker, paperless, immich, authentik) follow the same pattern.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The monolithic `Kustomization/seed` was split into per-app `seed-<app>`
KSes; it was left suspended + prune-disabled only so its inventory could
be pruned by a follow-up commit. That inventory is now empty (ownership
fully migrated to the per-app KSes), so the object owns nothing and can
be removed cleanly — flux-system (prune: true) will garbage-collect the
now-absent Kustomization/seed, and its own prune: false + empty inventory
means zero live resources are torn down.
Also drop kubernetes/seed/kustomization.yaml, the aggregator that only the
monolith referenced (path ./kubernetes/seed); it was already stale (missing
woodpecker). The per-app KSes target ./kubernetes/seed/<app> subdirs and are
unaffected.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Gitea 1.21.4 logs a deprecation warning for [server] LFS_CONTENT_PATH
(superseded by [lfs] PATH) and recommends DISABLE_QUERY_AUTH_TOKEN
(default from 1.23). Both surface as startup notices on the admin
dashboard.
Add the replacements as GITEA__ env overrides so they are tracked in
git. environment-to-ini can only add keys, so the stale
[server] LFS_CONTENT_PATH was also removed from the on-PVC app.ini by
hand (same path value -> existing data/lfs objects untouched).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The push trigger matched any branch, so pushing this feature branch ran the
refresh pipeline from the branch: it built and pushed ci-claude:2.1.218 and
opened a bump PR (#379) prematurely. Restrict the push event to branch: master
so only master ever builds/pushes images or opens bump PRs; cron and manual
are unaffected.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The bump credential is a claude-bot PAT scoped write:repository only (a
dedicated Gitea user needs admin to create, which claude-bot lacks; the
scoped-down PAT is the same least-privilege at the token level). Token minted
and stored as the ci_claude_bump_bot_token Woodpecker secret (events:
push/cron/manual). Commit author set to claude-bot accordingly. Only the cron
registration remains, to be done after this merges to master.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The ci-claude image installed a fixed, pinned Claude Code version and every
consumer pipeline pinned a stale dated image tag. Two problems:
- The npm install layer changed on every build (install-time mtimes + the
npm cache), so it never deduped in the Gitea registry — the single largest
orphan pattern behind #336 (~260 MB orphaned per no-op rebuild).
- A frozen version silently rots: old Claude Code releases eventually stop
working against the API.
Containerfile: default to @latest (npm auto-selects the musl native binary on
this alpine base) and make the install layer byte-stable — clean the npm cache
and normalize install-time mtimes on the installed tree and the directories the
RUN touches. Two independent --no-cache builds now produce a byte-identical npm
layer, so a rebuild of the same version dedups instead of orphaning a copy.
New ci-claude-refresh.yml owns the image lifecycle: weekly (and on-demand) it
resolves the latest published version, and only when it differs from what the
consumers pin does it build ci-claude:<version> (+:latest) and open a PR
rewiring the 5 consumer pipelines. Version tags make "did it change?" a
race-free string compare and keep the cron from opening no-op PRs. Nothing
auto-merges. build-ci-images.yml no longer builds ci-claude so the two never
fight over tags.
Requires owner setup before it does anything (documented in the pipeline
header): a dedicated ci-claude-bump-bot identity (write:repository only) as the
ci_claude_bump_bot_token secret, and a cron named "ci-claude-refresh".
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Review blocker: my previous filename fix used bash substring syntax
${TALOS_SCHEMATIC:0:12}, but the apply step runs under busybox ash
(Alpine /bin/sh) which errors 'Bad substitution' — the repo already uses
cut for this exact reason. Replaced with $(echo "$TALOS_SCHEMATIC" |
cut -c1-12). The HCL substr() in talos_image.tf is unaffected (not shell).
Also finishes the comment sweep the prior commit claimed but didn't: 4
more stale 'eth1' host-NIC references in the matter/otbr/music-assistant
NAD headers are now ens19. Grep confirms zero eth1 left in the v30iot
NAD/deployment files.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014S5CLfizEtzHS51kxJFQgc
The step exports REVIEW_TOKEN (write:repository PAT) for its own curl/gitea_post
calls, but that also placed it in the environment of the claude triage+review
agents, which process attacker-controlled PR content — a prompt-injected diff
could read /proc/self/environ and exfiltrate it into the public report. The
agents never call Gitea (the shell does, via $AUTH), so run them under
'env -u REVIEW_TOKEN'. Defence in depth alongside the settings.json /proc deny.
Caught by the review bot; matches the fix applied to the other fleet repos.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Review follow-up (optional): the shelly/matter/otbr NAD header comments
still described the old eth1 probe-order naming, contradicting this PR's
own root-cause writeup. Updated to the stable ens19 predictable name and
dropped the stale k8s-edge/probe-order narration.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014S5CLfizEtzHS51kxJFQgc
Review blocker: the Proxmox image-exists check derived TALOS_FILENAME
from version only, so bumping the schematic left the cached filename
unchanged — a future VM (re)provision would find the stale old-schematic
image and skip the download, silently reintroducing net.ifnames=0. Embed
the schematic hash in the filename (pipeline + opentofu/talos_image.tf,
kept in sync) so a schematic bump forces a fresh download.
Also reconciles stale comments the rename left behind: the MA reconcile
docstring (hostNetwork-era eth1 explanation), cluster.yaml kubelet nodeIP
(eth1->ens19), and the matter NAD's k8s-edge claim (edge isn't migrated
and isn't a current node; the pod is pinned off it anyway).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014S5CLfizEtzHS51kxJFQgc
Root cause of the IoT NIC name shuffle: the nocloud platform driver
unconditionally injects net.ifnames=0, disabling systemd predictable
naming, so eth0/1/2 followed driver-probe order and renamed across
reboots (a k8s-3 reboot put the v30iot NIC on eth2 and mgmt on eth1,
silently mis-targeting the v6-disable sysctl onto mgmt and attaching
macvlan children to the Server VLAN). LinkAliasConfig cannot fix this
(it only sets a cosmetic IFLA_IFALIAS, invisible to sysctl/CNI).
Fix: re-enable predictable naming via a new Image Factory schematic
(extraKernelArgs: -net.ifnames=0, net.ifnames=1). Because the cluster
boots sd-boot/UKI, the cmdline is baked into the image — machine.install
extraKernelArgs is silently ignored — so this MUST be delivered as a
schematic + talosctl upgrade, not a config patch. The busPath-derived
names are topology-stable and identical across the three identical VMs:
0000:06:13.0 (v30iot) -> ens19
0000:06:12.0 (mgmt) -> ens18
0000:02:00.0 (ceph) -> enp2s0np1
Re-keys everything that referenced the volatile eth1 onto the stable
ens19: the four v30iot macvlan NADs (matter/otbr/shelly/music-assistant)
and the per-node v6-disable sysctl. Bumps the schematic id in the deploy
pipeline. Applied live via rolling talosctl upgrade (one node at a time,
full Ceph recovery between each); verified ens19 identical on all three,
v30iot NIC addressless v4+v6, macvlan children retain their own v6.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014S5CLfizEtzHS51kxJFQgc
The earlier edit that removed the stale suspend-comment block also dropped the
'spec:' line, leaving suspend/schedule/jobTemplate nested under metadata: — an
invalid CronJob that would be rejected by the API server (kustomize build does
not validate against the k8s schema, so it passed). Re-add spec: and sync the
top-of-file doc comment to the reconciled infrastructure/claude#setup-token
path. Caught by the review bot on this PR; verified with kubectl --dry-run=server.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Gate zigbee2mqtt.home.renner.uno behind the same passkey-step-up Authentik
proxy-outpost pattern used for esphome/shelly-manager/vscode. The Z2M frontend
has no auth of its own (anyone on :8080 controls the whole Zigbee network), so
Traefik only ever routes the hostname to a dedicated outpost that validates the
session before forwarding.
New:
- apps/home-assistant/zigbee2mqtt-gate-service.yaml — ExternalName mirror to
ak-outpost-zigbee2mqtt-gate in the authentik ns (:9000).
- apps/home-assistant/zigbee2mqtt-ingress.yaml — zigbee2mqtt.home.renner.uno →
the outpost; cert-manager ingress-shim issues zigbee2mqtt-tls.
- apps/authentik/zigbee2mqtt-gate-egress.yaml — CNP letting the outpost egress
to the zigbee2mqtt pod on :8080.
Edited:
- apps/authentik/blueprints-configmap.yaml — 5-entry blueprint block (group /
proxyprovider / application / policybinding / outpost) named zigbee2mqtt-gate;
internal_host carries the explicit :8080 (Service listens on 8080, unlike
esphome's :80). Authentik spawns the ak-outpost-zigbee2mqtt-gate Deployment
+Service from the outpost entry.
- infrastructure/network-policies/home-assistant-ns.yaml — fromEndpoints rule
admitting the zigbee2mqtt-gate outpost to :8080 (shared 8080 with vscode/
shelly-manager; the fromEndpoints selector + the authentik-side egress scope
it to the zigbee2mqtt pod).
- the two kustomizations registering the new files.
DNS (k8s-gateway watches Ingress) and TLS (cert-manager) are automatic. Access
limited to the "Zigbee2MQTT Managers" group (k.renner). Verified: kustomize
builds, dry-run OK, blueprint YAML parses with the new block.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Pin both CronJobs to the built secret-rotator image that carries woodpecker-secret
mode (sha256:828bf486…), flip suspend:false, and reconcile the claude-oauth
BAO path to homelab/infrastructure/claude#setup-token (where the OAuth value is
actually seeded) — with the matching policy update (applied out-of-band).
Bootstraps + policies/roles are already applied and the org secrets are live, so
the rotators now maintain them on schedule: review-bot mints+syncs review_bot_token
quarterly; claude-oauth syncs claude_code_oauth_token daily.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>