62 Commits

Author SHA1 Message Date
unkinben c0c3eb4f66 Force Replace sync for the Recreate puppet master to clear stale rollingUpdate (#349)
## Why

ArgoCD fails to sync the puppet app with:

    Deployment.apps "puppetserver-master" is invalid:
    spec.strategy.rollingUpdate: Forbidden: may not be specified when strategy type is 'Recreate'

The manifest is already correct: #341 changed the master to `spec.strategy.type: Recreate` with no rollingUpdate block. The failure is a live-object artifact. When the master ran RollingUpdate the API server defaulted `spec.strategy.rollingUpdate` (maxSurge/maxUnavailable) onto the object. That defaulted field is owned by no applier, so neither a client-side merge nor server-side apply drops it when the desired manifest omits it. The live object therefore keeps `rollingUpdate` while gaining `type: Recreate`, which the API server rejects — blocking every sync.

k8s forbids any `rollingUpdate` field when `strategy.type` is `Recreate`; the two are mutually exclusive, so the sync cannot converge until the stale field is removed from the live object.

## Changes

- Annotate the `puppetserver-master` Deployment with `argocd.argoproj.io/sync-options: Replace=true`. Replace performs a full PUT that overwrites the whole object, dropping the stale `rollingUpdate` field and letting the Recreate strategy apply cleanly. The annotation is scoped to this one resource, so puppetdb/puppetboard/compiler keep the app-wide ServerSideApply behaviour.

## Validation

- `kustomize build --enable-helm apps/overlays/au-syd1/puppet` renders `puppetserver-master` with `strategy: { type: Recreate }` (no rollingUpdate) and the new sync-options annotation.
- `make kubeconform`: puppet overlay 34/34 valid; only the known cattle-system rancher kubeVersion incompatibility fails.
- `pre-commit`: all hooks pass.

---------

Co-authored-by: Ben Vincent <neotheo@gmail.com>
Reviewed-on: #349
Co-authored-by: Ben Vincent <ben@unkin.net>
Co-committed-by: Ben Vincent <ben@unkin.net>
2026-08-09 20:55:00 +10:00
unkinben 4c2f275f04 puppet: reduce privilege in namespace workloads (#307) (#319)
Why: shrink the blast radius of the Puppet control-plane pods (CA/eyaml keys, compiled catalogs) per the security sweep in #307 — remove root where it is not required and strip cargo-culted capabilities.

How:
- puppetboard cert-generator init: root+APE:true -> uid 1000, drop:[all], APE:false; pod fsGroup 1000; removed trailing `chown -R 1000:1000` (PVC now group-owned).
- puppetdb create-log-dir init: root -> uid 999, drop:[all], APE:false; pod fsGroup 999; removed `chown 999:999`.
- All OpenVox capability add-lists: removed the duplicate CAP_-prefixed spellings (k8s normalises both to the same kernel cap) and dropped the unused AUDIT_WRITE.
- Added allowPrivilegeEscalation:false and seccompProfile RuntimeDefault across the workloads.

Stays root (evidence-backed, class-B fallback): the puppetserver master/compiler and puppetdb main containers, plus the perms-and-dirs and generate-types root containers. The OpenVox image entrypoint runs `chown -R puppet:puppet` over root-owned baked-in dirs and drops the JVM to the puppet user via `runuser` (needs CHOWN/SETUID/SETGID); a non-root start crashloops. Their cap sets are reduced to the minimum justified (CHOWN/DAC_OVERRIDE/FOWNER[/SETUID/SETGID]).

Validation: `kustomize build --enable-helm` clean; kubeconform 0 invalid / 0 errors; pre-commit (yamllint etc.) green. Confirmed against live pods: puppetserver/puppetdb JVMs already run as puppet/puppetdb via `runuser`; `pam_loginuid` is absent from the su/runuser PAM stacks and loginuid is unset, so dropping AUDIT_WRITE is safe.

Post-merge smoke test (puppet had an outage this session — watch closely): after argocd sync, confirm puppetserver master + a compiler reach `running` at /status/v1/simple, puppetdb reaches `running`, puppetboard serves 200, and the generate-types + g10k CronJobs complete — i.e. catalogs still compile and reports still ingest.

Closes #307

https://claude.ai/code/session_015ur3i7D2azsMAWTSVABApv
Reviewed-on: #319
Co-authored-by: Ben Vincent <ben@unkin.net>
Co-committed-by: Ben Vincent <ben@unkin.net>
2026-08-08 22:36:44 +10:00
unkinben c722df415a puppetdb: isolate only the stockpile queue per pod (fix #340 crashloop) (#342) 2026-08-08 20:16:11 +10:00
unkinben 4c7c97ab80 puppetdb: unique per-pod command-queue directory (#340) 2026-08-08 20:04:17 +10:00
unkinben 5d1cc10588 Pin puppet master to a single Recreate replica (#341)
## Why

The puppet MASTER is the singleton CA/master. A second master, even transiently during a rolling update, races on CA/cert signing and shared state (the CA lives on a shared PVC mounted by every master pod). The master was previously driven by an HPA with `minReplicas: 2`, `maxReplicas: 5` and a `RollingUpdate` strategy, so 2-5 masters could coexist normally and a rollout would briefly run old+new masters against the same CA data — a latent CA-corruption/split-brain bug. Recreate guarantees the old pod terminates before the new one starts, so two masters never coexist.

## Changes

- Set `puppetserver-master` `spec.replicas: 1` and `spec.strategy.type: Recreate` (drops RollingUpdate).
- Remove the `puppetserver-masters-autoscaler` HPA and its kustomization entry, which forced 2-5 master replicas and would otherwise override `replicas: 1`.
- Refresh the `puppetserver-master-vpa` note to reflect the pinned-singleton, no-HPA state (VPA stays `updateMode: Off`, recommendation-only).

The compiler (`puppetserver-compiler`) remains the horizontally-scalable tier with its own HPA — untouched. puppetdb/puppetboard untouched.

https://claude.ai/code/session_01JUoARVdmhxKQHyyyp1pxeT
---------

Co-authored-by: Ben Vincent <neotheo@gmail.com>
Reviewed-on: #341
Co-authored-by: Ben Vincent <ben@unkin.net>
Co-committed-by: Ben Vincent <ben@unkin.net>
2026-08-08 20:02:38 +10:00
unkinben ba7a1a9509 Enable Reloader secret watching, scope existing auto to configmap-only (#326) (#339)
## Why

The re-keyed internal `unkin.net` intermediate broke CA consumers (CNPG->RGW backups, subPath/startup-cached CA mounts) and needed manual pod restarts, because Reloader was deployed with `ignoreSecrets: true` and could not restart on the `vault-ca-cert` Secret. Enabling secret watching naively is unsafe: many workloads carry the generic `reloader.stakater.com/auto`, and the estate rotates numerous Secrets via Vault/VSO — those would restart on every rotation. This enables secret watching but scopes existing `auto` to ConfigMaps, making secret-reload opt-in per Secret.

## Changes

- Set `reloader.ignoreSecrets: false` (au-syd1 reloader-system values) so Secrets are watched.
- Convert every generic `reloader.stakater.com/auto: "true"` to the ConfigMap-only `configmap.reloader.stakater.com/auto: "true"` — 22 annotations across 19 files. Existing ConfigMap-reload behaviour is preserved; Vault/VSO Secret rotations no longer restart these workloads.
- Add explicit `secret.reloader.stakater.com/reload: "vault-ca-cert"` to the CA consumers that mount the CA and carry a Reloader annotation: `artifactapi/api`, `cephrgw-operator`, `puppetserver-master`, `puppetserver-compiler`, `litellm`, `logarchiver`.
- Add `secret.reloader.stakater.com/reload: "kanidm-tls"` so kanidm rolls when cert-manager renews its leaf.
- Add `docs/ca-rotation.md` runbook (indexed in `docs/README.md`).

## Safety review (secret-only / CA workloads)

`vault-ca-cert` is a plain reflected Secret that bootstraps Vault trust (not VSO-rotated; changes only on intermediate re-key). `kanidm-tls` is a cert-manager leaf. Everything else mounted (`environment`, `*-credentials`, `eyaml-keys`, `puppetboard-secrets`, `s3-credentials`, `nats-auth`, `clickhouse-credentials`, `woodpecker-*`) is VSO/CNPG Vault-rotated and deliberately excluded.

- `cephrgw-operator` — mounts only Secrets (`cephrgw-credentials` VSO + `vault-ca-cert`), no ConfigMap. Its old comment said "restart when the credentials Secret rotates"; `cephrgw-credentials` is VSO so that is now excluded, and reload is scoped to `vault-ca-cert` only. Comment updated.
- `nats` (logging) — old comment "Roll the StatefulSet when nats-auth changes"; `nats-auth` is VSO, so this is now ConfigMap-only (deliberately no roll on rotation). Comment updated. Same for the vector agent/aggregator/vm-ingest (VSO `nats-auth`/`clickhouse-credentials`).
- `artifactapi/ui` — mounts neither a ConfigMap nor a Secret; its `auto` was already a no-op. Left as ConfigMap-only.
- `puppetdb` / `puppetboard` — mount a ConfigMap plus VSO Secrets (postgres creds / puppetboard-secrets); ConfigMap-only is correct, no secret reload added.

CA consumers that mount `vault-ca-cert` but have **no** Reloader annotation (CRD-managed or startup-cached) are documented in `docs/ca-rotation.md` for manual restart rather than annotated here: `grafana`, `observability/vmagent`, `paperclip`, `argocd-repo-server`, plus CNPG clusters (`kubectl cnpg restart`).

## Notes / coordination

- Annotations left in their existing location (some sit on the pod template, e.g. `litellm`, `puppetdb`; Reloader reads controller-level metadata — placement unchanged from before, no regression).
- Touches `apps/overlays/au-syd1/logging/values-vector-*.yaml`, which overlap open PR #320 (Tier-2 Vector pipelines) — only the one-line reloader annotation is changed here.

## Validation

- `make kubeconform` — touched overlays (reloader-system, logging, woodpecker, authentik) valid; only the known-unrelated cattle-system rancher chart kubeVersion failure remains.
- `uvx pre-commit run --all-files` — all hooks pass.

Closes #326

---------

Co-authored-by: Ben Vincent <neotheo@gmail.com>
Reviewed-on: #339
Co-authored-by: Ben Vincent <ben@unkin.net>
Co-committed-by: Ben Vincent <ben@unkin.net>
2026-08-08 19:35:31 +10:00
unkinben 6a13ca758a cephrgw: recreate cnpg backup buckets on ec placement (step 3) (#316)
Final step of the ec migration: the old buckets were purged in #315, so the operator will now create fresh ones on the ec placement target. Restores the nine Bucket CRs with placementTarget: ec and retainOnDelete: true (purge disabled again for safety).

- re-add the nine cnpg backup Bucket resources on ec, retainOnDelete: true

Reviewed-on: #316
Co-authored-by: Ben Vincent <ben@unkin.net>
Co-committed-by: Ben Vincent <ben@unkin.net>
2026-07-30 23:44:50 +10:00
unkinben 7daeb4af65 cephrgw: remove cnpg backup buckets (ec migration step 2) (#315)
Step 2 of the ec placement migration: with purge-on-delete now allowed (#313), removing the Bucket CRs makes the operator delete the underlying RGW buckets and their objects, freeing the names to be recreated on ec in step 3.

- remove the nine cnpg backup Bucket resources
- keep ObjectStoreUser and ScheduledBackup so the backup-s3 secrets and schedules survive

Reviewed-on: #315
Co-authored-by: Ben Vincent <ben@unkin.net>
Co-committed-by: Ben Vincent <ben@unkin.net>
2026-07-30 23:35:55 +10:00
unkinben 6e8a061b94 cephrgw: allow purge-on-delete for cnpg backup buckets (#313)
Step 1 of moving the CNPG backup buckets to ec placement: RGW can't move an existing bucket, so they must be dropped and recreated. This lets the operator actually delete the buckets (with their objects) when the CRs are removed in step 2.

- set retainOnDelete: false and purgeOnDelete: true on all nine cnpg backup Bucket CRs
- leave ObjectStoreUser/BucketAccess untouched so the backup-s3 secrets persist

Reviewed-on: #313
Co-authored-by: Ben Vincent <ben@unkin.net>
Co-committed-by: Ben Vincent <ben@unkin.net>
2026-07-30 22:16:18 +10:00
unkinben 23c26e8cc2 cephrgw v0.4.0; move CNPG backup buckets to ec placement (#312)
cephrgw-operator v0.4.0 adds immutable placementTarget selection on Buckets; the nine CNPG backup buckets should live on the ec (4/1) placement instead of 3-replica. Existing buckets cannot change placement, so after this merges the buckets get deleted and recreated on ec and fresh base backups are triggered (day-old backups are accepted losses, per Ben).

- bump cephrgw-operator image and CRD ref to v0.4.0
- add placementTarget: ec to all nine cnpg backup Bucket CRs

Reviewed-on: #312
Co-authored-by: Ben Vincent <ben@unkin.net>
Co-committed-by: Ben Vincent <ben@unkin.net>
2026-07-30 21:08:25 +10:00
unkinben e0eeeb6b04 Use full postgres image on minimal CNPG clusters so WAL archiving works (#304)
## Why

The CNPG buckets are empty after PR #298. Live diagnosis found **two** independent
causes; this PR fixes one of them.

`litellm`, `puppet` and `paperclip` run the CNPG `17-minimal-trixie` image, which
omits the `barman-cloud` CLI. In-tree `barmanObjectStore` archiving shells out to
`barman-cloud-wal-archive`, so their archiver dies immediately:

```
ContinuousArchiving=False :: unexpected failure invoking barman-cloud-wal-archive:
exec: "barman-cloud-wal-archive": executable file not found in $PATH
```

(verified on the live `puppet-postgres` primary: `which barman-cloud-wal-archive` →
not found; on a `-system` cluster it resolves to `/usr/local/bin/barman-cloud-wal-archive`).

## How

Switch those three clusters from `17-minimal-trixie` to `17-system-trixie` — the
`-system` variant already used by the other six clusters, which bundles the
barman-cloud tools. Tag confirmed present upstream (ghcr manifest HTTP 200).

```
- imageName: ghcr.io/cloudnative-pg/postgresql:17-minimal-trixie
+ imageName: ghcr.io/cloudnative-pg/postgresql:17-system-trixie
```

CNPG applies this as a rolling image update (switchover, no data change).

## Not fixed here (separate, primary blocker)

The other six clusters (full image, barman present) fail with a **TLS trust**
error — the reflected `vault-ca-cert` bundle carries a **stale intermediate CA**,
so barman can't verify `s3.ceph.unkin.net`:

```
SSL: CERTIFICATE_VERIFY_FAILED ... certificate signature failure
```

That is a shared trust-anchor refresh (likely owned by terraform-k8s /
`config/certificates/secret.yaml`, `managed-by: terragrunt`), handled separately —
it also gates litellm/puppet once they have barman. See the investigation report.

## Validation

- `kustomize build --enable-helm` + `kubeconform` pass on `litellm`, `puppet`
  overlays and the `paperclip` base (paperclip has no overlay yet).
- `pre-commit run` passes on all changed files.

## Follow-ups

- Longer term, the Barman Cloud Plugin (sidecar) would let minimal images keep
  their size while still archiving — track with the plugin migration.

https://claude.ai/code/session_015ur3i7D2azsMAWTSVABApv
Reviewed-on: #304
Co-authored-by: Ben Vincent <ben@unkin.net>
Co-committed-by: Ben Vincent <ben@unkin.net>
2026-07-28 23:11:30 +10:00
unkinben 3c2bdf307a Add S3 backups to all CNPG Postgres clusters (#298)
## Why

None of the 8 CNPG Postgres clusters in this repo had **any** backup configured. A
lost PVC, a fat-fingered migration, or a bad app deploy meant permanent, unrecoverable
data loss for authentik, litellm, artifactapi, woodpecker, puppet, encapi, paperclip
and grafana. This adds continuous WAL archiving + a nightly base backup to Ceph RGW for
every cluster, plus code-forward restore docs.

## What

- **`spec.backup.barmanObjectStore`** on each `cnpg_cluster.yaml` — turns on continuous
  WAL archiving to `s3://cnpg-<app>`, WAL compressed with zstd, base backups with bzip2,
  30-day retention. TLS to `s3.ceph.unkin.net` is trusted via the reflected
  `vault-ca-cert` (`endpointCA`).
- **`cnpg_backup.yaml`** per app — a cephrgw `ObjectStoreUser` + `Bucket` (operator
  provisions the bucket and mints the S3 key into `cnpg-<app>-backup-s3`; **nothing is
  hardcoded**) and a staggered nightly `ScheduledBackup`.
- **`schemas/ceph.unkin.net/*.json`** — the three cephrgw CRD schemas so kubeconform can
  validate the new CRs.
- **`docs/`** — new docs folder (README index + `cnpg-backups.md` + `cnpg-restore.md`).

## Design decisions (answers to the open questions)

**One bucket for all, or per-database?** → **Per-database (one bucket + owner user per
cluster).** The cephrgw CRDs are namespace-scoped (`BucketRef`/`OwnerRef` resolve only
*within the same namespace*), and CNPG reads its S3 credential Secret from its *own*
namespace. A single shared bucket would require either cross-namespace bucket refs
(unsupported) or hand-copying the S3 secret into all 8 namespaces (defeats "operator
mints the keys"). Per-namespace `s3://cnpg-<app>` with a dedicated owner user is the
simplest correct topology and needs zero manual seeding. Each user owns exactly one
bucket, so owner-level (full) access is already tightly scoped — no extra `BucketAccess`
grant needed.

**Backup mechanism.** The deployed CNPG operator is **v1.28** (helm chart
`cloudnative-pg-0.27.0`, appVersion 1.28.0). 1.26+ deprecates the in-tree
`barmanObjectStore` in favour of the Barman Cloud Plugin, but the plugin is **not
deployed**, and `barmanObjectStore` is still fully functional on 1.28. So this uses the
in-tree mechanism. Migrating to the plugin is a follow-up (noted in `docs/cnpg-backups.md`).

## Schedule / retention (defaults — Ben to adjust)

| App | Cluster | Bucket | Nightly base backup |
| --- | --- | --- | --- |
| authentik | postgres | cnpg-authentik | 01:00 |
| litellm | litellm-postgres | cnpg-litellm | 01:20 |
| artifactapi | postgres | cnpg-artifactapi | 01:40 |
| woodpecker | woodpecker-postgres | cnpg-woodpecker | 02:00 |
| puppet | puppet-postgres | cnpg-puppet | 02:20 |
| encapi | postgres | cnpg-encapi | 02:40 |
| paperclip | paperclip-postgres | cnpg-paperclip | 03:00 |
| grafana | postgres | cnpg-grafana | 03:20 |

Retention is **30d** across the board — flagged as a default to tune per cluster.
Schedules are staggered 20 min apart so 8 base backups don't hit RGW at once.

## Validation

- `kustomize build --enable-helm` + `kubeconform` (repo CI args, incl. the new ceph
  schemas) pass on all 8 affected overlays (paperclip validated at base — it has no
  overlay yet). ceph CRs resolve their schemas (`Skipped: 0`).
- `pre-commit run` passes on all changed files (yamllint, no-plain-secrets, etc.).
- Note: a full `ci/validate-apps.sh` run aborts locally on the unrelated
  `cattle-system` overlay (`chart requires kubeVersion < 1.35 vs host helm v1.36.0`) —
  pre-existing, reproduces on `origin/main`, unrelated to this change.

## Notes / caveats

- No overlap with the woodpecker chart-bump PR (#297, overlay files only) or the logging
  PR (#296) beyond the three **identical** generated `schemas/ceph.unkin.net/*.json`
  files, which merge cleanly whichever lands first.
- Credentials: no manual seeding — the cephrgw-operator mints the RGW user + keys. The
  only prerequisite is the operator being healthy (it is, in `cephrgw-system`).

## Follow-ups

- Barman Cloud Plugin migration (deploy plugin, move clusters to `ObjectStore` CRs).
- Tune per-cluster retention / schedule if the defaults don't fit.

https://claude.ai/code/session_015ur3i7D2azsMAWTSVABApv
Reviewed-on: #298
Co-authored-by: Ben Vincent <ben@unkin.net>
Co-committed-by: Ben Vincent <ben@unkin.net>
2026-07-27 23:57:08 +10:00
unkinben 1d87c42fc1 Add advise-mode VPA for every workload (#283)
## Why

Turns on right-sizing telemetry for the whole estate. Adds a `VerticalPodAutoscaler` with `updateMode: "Off"` (recommendation-only, advise mode) for every Deployment and StatefulSet in `apps/base`. Off mode never evicts or mutates pods, so this is purely observational: the VPA recommender (added in the vpa-system PR) publishes suggested requests/limits in each VPA's status, and nothing acts on them until someone deliberately flips a mode.

## Changes

- Add one `vpa.yaml` per app under `apps/base/<app>/` containing a `<workload>-vpa` VerticalPodAutoscaler for each workload, and register it in that app's `kustomization.yaml`.
- Coverage: 17 workloads across 11 apps.
  - age-api (age-api), artifactapi (api, redis, ui), authentik (redis), bind-system (bind-operator), cephrgw-system (cephrgw-operator), encapi (encapi), kanidm (kanidm StatefulSet), litellm (litellm, redis), paperclip (paperclip), pdbmux (pdbmux), puppet (puppetboard, puppetdb, puppetserver-compiler, puppetserver-master).

## Skipped (intentionally)

- **CNPG `Cluster` objects** (artifactapi, authentik, encapi, grafana, litellm, paperclip, puppet, woodpecker) — Postgres is managed by CloudNativePG, not a VPA target.
- **CronJobs** (puppet g10k/generate-types, reposync x4) — not VPA-able.

## HPA / VPA caveat

api, ui (artifactapi), litellm, and all four puppet deployments also carry an HPA. With `updateMode: "Off"` there is no conflict today (VPA only recommends). VPA objects targeting these carry an inline comment: do **not** flip to `Auto`/`Initial` while the HPA still autoscales on CPU/memory, or the two controllers will fight over the same resource. Move the HPA to a custom/non-resource metric first.

## Verification

- `kubectl kustomize` over every touched overlay: 9/11 overlays PASS rendering all their VPAs; 2 failures are pre-existing and unrelated to this change — `authentik` fails on a local helm-tooling flag error identically on origin/main, and `paperclip` has no `apps/overlays/au-syd1/paperclip` directory yet. Both apps' base kustomizations build clean and render their VPA.

## Merge gate

- **Requires the vpa-system CRDs PR (argocd-apps #281) to merge first.** These manifests use `autoscaling.k8s.io/v1 VerticalPodAutoscaler`; without the CRDs installed, ArgoCD sync fails on an unknown kind.

Reviewed-on: #283
Co-authored-by: Ben Vincent <ben@unkin.net>
Co-committed-by: Ben Vincent <ben@unkin.net>
2026-07-25 18:30:23 +10:00
unkinben 05318553d6 Swap puppet compiler ENC to the encapic Go binary (#277)
## Why

The puppet-on-k8s compilers classify nodes with a uv/python ENC script (`encapi-enc`). Each fresh compiler pod resolves the script's python dependencies on first invocation, and that resolution fails on cold pods (observed exits 135/2), breaking puppet agent catalog compilation. `encapic` (git.unkin.net/unkin/encapic) is a stdlib-only Go replacement with no runtime dependency resolution — a behavioural drop-in whose output matches the python script byte-for-byte.

## Changes

- Points the compiler `external_nodes` at `/opt/bin/encapic`.
- Reworks the `setup-shared-bins` init container to `curl` the encapic `v0.1.0` `encapic_linux_amd64` release binary (sha256-verified against the published `.sha256`, installed mode 0755) into the shared bins dir, instead of copying the python script and installing uv.
- Removes the `puppet-encapi-enc` configmap generator, its volume and mount, and the `resources/encapi-enc` script. uv was consumed solely by that script (grep of `apps/base/puppet` confirms no other consumer), so its installation is removed too.

`kubectl kustomize apps/overlays/au-syd1/puppet` builds clean.

## Merge gate

Do not merge until the encapic `v0.1.0` release assets exist:
`https://git.unkin.net/unkin/encapic/releases/download/v0.1.0/encapic_linux_amd64` (+ `.sha256`). The init container pulls them at pod start.

## Rollback

Revert this PR to restore the `encapi-enc` configmap script + uv install and repoint `external_nodes`.

---------

Co-authored-by: benvin <neotheo@gmail.com>
Reviewed-on: #277
Co-authored-by: Ben Vincent <ben@unkin.net>
Co-committed-by: Ben Vincent <ben@unkin.net>
2026-07-25 12:40:42 +10:00
unkinben 3af12180fd Switch puppet compiler ENC from Cobbler to encapi (#272)
## Why

The k8s puppetserver compilers classify nodes via an exec ENC that today queries legacy Cobbler (`https://cobbler.main.unkin.net`) over TLS. `encapi` now runs in-cluster and exposes a cobbler-wire-compatible endpoint (`GET /cblr/svc/op/puppet/hostname/<certname>`), a drop-in for the Cobbler URL. This cuts the puppet-on-k8s ENC over from Cobbler to encapi — a prerequisite for migrating VM agents onto puppet-on-k8s.

## Changes

- Rename the ENC script `resources/cobbler-enc` -> `resources/encapi-enc`, and its configmap `puppet-cobbler-enc` -> `puppet-encapi-enc` (kustomization configMapGenerator + deployment volume, initContainer copy path, and volumeMount subPath).
- Point `external_nodes` in the compiler `puppet.conf` at `/opt/bin/encapi-enc`.
- Target the in-cluster encapi service `http://encapi.encapi.svc.cluster.local` (plain HTTP), overridable via the `ENCAPI_URL` env var.
- Drop the `/opt/vault-ca-cert.crt` verify for the ENC request (no TLS needed in-cluster).
- Leave the response normalization identical: classes coerced to a list, `enc_role`/`enc_env` params set, `environment` stripped when it equals `testing`.

Verified with `kubectl kustomize apps/overlays/au-syd1/puppet` (builds clean, exit 0); the generated `puppet-encapi-enc` configmap contains the new URL and env var.

## 🚨 Merge gate

**Do not merge until encapi is seeded** (terraform-incus `benvin/encapi-seed` PR applied). An empty encapi means every node resolves to a 404. On 404 the ENC script exits non-zero, so puppet fails the compile rather than classifying the node with zero classes — nodes will fail to run until they exist in encapi. Seed encapi first so real nodes classify correctly; only unknown nodes should 404.

Reviewed-on: #272
Co-authored-by: Ben Vincent <ben@unkin.net>
Co-committed-by: Ben Vincent <ben@unkin.net>
2026-07-24 23:42:23 +10:00
benvin 646d76da16 chore: reduce resource requirements of puppet (#246)
- lower reserved CPU for puppet related services

---------

Co-authored-by: Ben Vincent <ben@unkin.net>
Reviewed-on: #246
2026-07-12 18:04:50 +10:00
unkinben 0b7819bda3 chore: bump almalinux9 image tags (#188)
Bump almalinux9 image tags to 20260606

Reviewed-on: #188
Co-authored-by: Ben Vincent <ben@unkin.net>
Co-committed-by: Ben Vincent <ben@unkin.net>
2026-06-07 00:35:12 +10:00
unkinben 6497dab25e fix(puppet): remove explicit clusterIP: null from puppetdb Service (#166)
## Summary

- Removes `clusterIP: null` from the `puppetdb` Service spec

## Why

Setting `clusterIP: null` makes ArgoCD's desired state explicit about the field being null. Kubernetes assigns a real IP on creation and the field is immutable afterward. The null vs assigned-IP mismatch causes permanent OutOfSync on the puppetdb Service. Removing the field means ArgoCD no longer claims ownership of `clusterIP`, so the API server's value is authoritative.

Reviewed-on: #166
2026-05-25 22:44:24 +10:00
unkinben 1890dd4bda fix(gateways): add explicit group/kind/weight to all HTTPRoute refs (#162)
## Summary

- Adds `group: gateway.networking.k8s.io` and `kind: Gateway` to all `parentRefs` entries
- Adds `group: ""`, `kind: Service`, and `weight: 1` to all `backendRefs` entries
- Affects 9 HTTPRoute files across artifactapi, cattle-system, consul, kanidm, litellm, paperclip, puppet, and vault

## Why

ArgoCD diffs the desired manifest against the live Kubernetes object. The Gateway API controller defaults these fields when creating/updating objects, so the live state always has them — causing persistent OutOfSync for every HTTPRoute. Same root cause as #153 (certificateRefs).

## Test plan

- [ ] All affected ArgoCD applications show Synced after merge

Reviewed-on: #162
2026-05-24 20:32:37 +10:00
unkinben cbc2c1cb9f fix(gateways): add explicit group: "" to all certificateRefs entries (#153)
The Gateway API admission server defaults certificateRefs[].group to ""
when it is omitted. ArgoCD diffed the desired state (no group field) against
the live state (group: "") and flagged every gateway as out of sync.

Fix: explicitly set group: "" in all certificateRefs entries so the
rendered manifest matches the API server's canonical form exactly.

Affected: artifactapi, cattle-system, consul, litellm, paperclip,
puppet (puppetboard + puppetdb), vault.

Reviewed-on: #153
2026-05-23 23:47:24 +10:00
unkinben 445d8b6e7e feat: add HTTP→HTTPS redirect to Gateway API services (#145)
Add port 80 HTTP listener and redirect HTTPRoute to artifactapi,
cattle-system (rancher), litellm, paperclip, and puppetboard — restoring
the redirect behaviour that existed on the previous nginx/traefik Ingress
resources.

Reviewed-on: #145
2026-05-23 17:34:07 +10:00
unkinben 90ddd932fe feat(puppet): migrate puppetdb Ingress to Gateway API (#131)
## Summary

- Replace `Ingress` (nginx) with `Gateway` + `HTTPRoute` using `traefik-internal` GatewayClass
- TLS terminated at the Gateway listener; cert-manager provisions the certificate via `vault-issuer`
- external-dns annotations moved to the Gateway
- `ingress_puppetboard.yaml` is unchanged in this PR (separate PR)

## Test plan

- [ ] ArgoCD syncs the puppet app cleanly
- [ ] cert-manager issues the `puppetdb-tls` certificate
- [ ] external-dns creates the DNS record
- [ ] `https://puppetdb.k8s.syd1.au.unkin.net` is reachable

Reviewed-on: #131
2026-05-23 16:05:26 +10:00
unkinben 2c6d88aa6b feat(puppet): migrate puppetboard Ingress to Gateway API (#130)
## Summary

- Replace `Ingress` (nginx) with `Gateway` + `HTTPRoute` using `traefik-internal` GatewayClass
- TLS terminated at the Gateway listener; cert-manager provisions the certificate via `vault-issuer`
- external-dns annotations moved to the Gateway
- `ingress_puppetdb.yaml` is unchanged in this PR (separate PR)

## Test plan

- [ ] ArgoCD syncs the puppet app cleanly
- [ ] cert-manager issues the `puppetboard-tls` certificate
- [ ] external-dns creates the DNS record
- [ ] `https://puppetboard.k8s.syd1.au.unkin.net` is reachable

Reviewed-on: #130
2026-05-23 01:31:28 +10:00
unkinben e269220228 fix: clone r10k config to /tmp/r10k-config instead of /shared (#76)
The g10k-code cronjob was failing with "Permission denied" because the
container (running as uid 999, non-root) attempted to create /shared in
the container root filesystem, which is not writable. Clone to /tmp
which is always writable by unprivileged users.

Reviewed-on: #76
2026-03-24 19:25:06 +11:00
unkinben 1388875685 fix: remove shared-config PVC from g10k cronjob, clone r10k config directly (#75)
The RWO puppetserver-shared-config PVC caused multi-attach errors when
the cronjob pod was scheduled on a different node than the previous run,
stalling the init container indefinitely. Since the config only needs to
exist for the duration of the job, remove the init container and PVC
entirely and clone the r10k config directly into /shared within the main
container before running g10k.

Reviewed-on: #75
2026-03-24 18:54:58 +11:00
unkinben 49224d4a1b fix: increase generate-types memory limit and remove invalid JVM env var (#74)
The container was OOMKilled on every run because the 256Mi limit was far
too low for `puppet generate types`. Remove PUPPETSERVER_JAVA_ARGS (only
relevant to the puppetserver JVM, not the puppet CLI) and raise the
memory limit to 1Gi / request 512Mi.

Reviewed-on: #74
2026-03-24 18:51:46 +11:00
unkinben 28dc8dc238 feat: update gems for puppet (#73)
- add deep_merge, ipaddr, and hiera-eyaml gems
- pin intel-device-plugins to 0.35.0

Reviewed-on: #73
2026-03-24 18:33:03 +11:00
unkinben 33420e1286 revert: remove filemapper gem install (#72)
filemapper is not available on RubyGems under that name and was causing
puppetserver-compiler to crash loop. The interfaces provider that
requires puppetx/filemapper is Debian-specific and should not be loaded
on RedHat-based puppetservers.

Reviewed-on: #72
2026-03-24 18:22:23 +11:00
unkinben 0fc1268c51 fix: install filemapper gem and deploy generate-types cronjob (#71)
The network module's interfaces provider requires puppetx/filemapper
which was not installed, causing catalog compilation failures with
"no such file to load -- puppetx/filemapper".

Adds filemapper to additional-ruby-gems.sh for puppetserver/compiler
pods, installs it directly in the generate-types cronjob (which has no
access to that script), and adds cronjob_generate-types.yaml to the
kustomization so the CronJob is actually deployed.

💘 Generated with Crush

Assisted-by: Claude Sonnet 4.6 via Crush <crush@charm.land>

Reviewed-on: #71
2026-03-22 00:03:33 +11:00
unkinben c0d95b71a7 fix: connect puppetboard to puppetdb over SSL on port 8081 (#70)
Puppetboard was connecting to PuppetDB on port 8080 (plain HTTP), causing
403 Forbidden errors on the /metrics/v2 Jolokia endpoint which requires
HTTPS with a Puppet certificate. Also replaced the invalid
PUPPETDB_SSL_SKIP_VERIFY var with the correct PUPPETDB_SSL_VERIFY,
PUPPETDB_CERT, and PUPPETDB_KEY pointing to the certs already generated
by the cert-generator init container.

💘 Generated with Crush

Assisted-by: Claude Sonnet 4.6 via Crush <crush@charm.land>

Reviewed-on: #70
2026-03-22 00:01:54 +11:00
unkinben 2a96d9e948 feat: add PuppetDB read-only database user and pooler (#69)
PuppetDB requires a separate read-only database user for its read pool.
Without it, it refuses to use the write user for read queries and all
/pdb/query/v4 calls fail with a 500.

- Add puppetdb_read role via CNPG managed.roles with password sourced
  from a new postgres-read-credentials Vault secret
- Grant CONNECT, USAGE, SELECT and default privileges to puppetdb_read
  via postInitApplicationSQL (must also be run manually on existing cluster)
- Add puppet-postgres-pooler-ro Pooler (type: ro) routing to replicas
- Add puppetdb-read-database-conf ConfigMap with read-database.conf
  mounted into /etc/puppetlabs/puppetdb/conf.d/ in the PuppetDB deployment
- Wire OPENVOXDB_READ_POSTGRES_* env vars from the new secret

💘 Generated with Crush

Assisted-by: Claude Sonnet 4.6 via Crush <crush@charm.land>

Reviewed-on: #69
2026-03-21 23:31:01 +11:00
unkinben b49e8d3647 chore: change back to puppetdb:8081 (#68)
- puppetdb requires access via 8081 from puppetservers
- puppetservers do not trust the certificate via ingress

Reviewed-on: #68
2026-03-21 22:50:46 +11:00
unkinben 5f227939bc feat: add CronJob to generate Puppet types for all environments (#67)
- add kubernetes CronJob that runs every 5 minutes to automaticall generate Puppet types for all environments in the code directory.

Reviewed-on: #67
2026-03-21 17:39:03 +11:00
unkinben ffc861daa7 fix: update puppet.conf with main/server/user (#66)
- master config section is not used
- server containes all setting specifically for a server (puppet, puppet ca)
- user is for all puppet <command> tooling, like 'puppet generate'

Reviewed-on: #66
2026-03-21 17:16:15 +11:00
unkinben 47bd341371 chore: tidy initContainers (#65)
- make initcontainers easier to read/follow

Reviewed-on: #65
2026-03-21 17:16:07 +11:00
unkinben ee9ec23f6f chore: use docker not container (#64)
was referencing the main branch of upstream container, not the one I am
actually using. s/container/docker/

Reviewed-on: #64
2026-03-21 16:47:02 +11:00
unkinben 3f355bbfd3 feat: add custom entrypoint script for additional Ruby gems (#63)
Add support for installing additional Ruby gems via custom entrypoint script.
The script is mounted as a ConfigMap into /container-custom-entrypoint.d/
and will be executed during Puppetserver container startup.

Reviewed-on: #63
2026-03-21 16:01:46 +11:00
unkinben 00cbb6a817 fix: update ENC script CA certificate path (#62)
- Mount vault-ca-cert secret at /opt/vault-ca-cert.crt in both deployments
- Update cobbler-enc script to use correct CA certificate path
- Resolves OSError about missing TLS CA certificate bundle

Reviewed-on: #62
2026-03-20 23:05:35 +11:00
unkinben f474c5c530 feat: add shared bins volume for uv and cobbler-enc (#61)
- Add puppet-shared-bins PVC (10GB) for shared binaries
- Mount /opt/bin in both compiler and master deployments
- Add init container to install uv binary and cobbler script to shared volume
- Update cobbler-enc to use absolute path and uv cache directory
- Configure puppet.conf to reference cobbler-enc from /opt/bin

Reviewed-on: #61
2026-03-20 22:49:31 +11:00
unkinben c1ea6e1e81 fix: update puppet.conf to point to enc (#60)
enc script is in /etc/puppetlabs/puppet to ensure its copied during the init container phase

Reviewed-on: #60
2026-03-20 21:34:40 +11:00
unkinben 3553e9f6dd refactor: simplify DNS alt names for puppetserver compiler (#59)
Remove individual compiler pod DNS names and use generic puppetserver-compiler name instead.

Reviewed-on: #59
2026-03-20 21:27:04 +11:00
unkinben 6decc45e65 fix: use http port for puppetdb (#58)
DEBUG:urllib3.connectionpool:Starting new HTTP connection (1): puppetdb:8081
ERROR:pypuppetdb.api.base:Could not reach PuppetDB on puppetdb:8081 over HTTP.

- puppetdb_host assumes HTTP when not verifying ssl

Reviewed-on: #58
2026-03-20 21:26:52 +11:00
unkinben c2d23aaeae refactor: convert puppetserver compilers to deployment with configmap integration (#57)
- Convert StatefulSet to Deployment for better scaling flexibility
- Add initContainer to copy configmaps to shared RWX volume (10GB)
- Integrate puppetserver-compiler-config configmap for environment variables
- Configure configMapGenerator with stable names (disableNameSuffixHash)
- Update HPA to target Deployment instead of StatefulSet
- Simplify puppetboard SSL config to skip verification for internal connections

Reviewed-on: #57
2026-03-20 20:47:36 +11:00
unkinben f25117ab7f testing via ingress for puppetdb (#56)
Reviewed-on: #56
2026-03-20 00:00:41 +11:00
unkinben 47b894c450 enable debugging for puppetboard (#55)
Reviewed-on: #55
2026-03-19 23:56:49 +11:00
unkinben 059992f6a3 fix: external access to puppetdb (#53) (#54)
- use vault cert for puppetdb ingress

Reviewed-on: #53

Reviewed-on: #54
2026-03-19 23:32:27 +11:00
unkinben 6ffb0898a4 fix: external access to puppetdb (#53)
- use vault cert for puppetdb ingress

Reviewed-on: #53
2026-03-19 23:26:02 +11:00
unkinben 30d56030b5 fix: increase number of cnpg_pooler_connections (#52)
in previous puppet installs, the puppetdb api service opens MANY
connections. we need to increase the number to greater than 300.

Reviewed-on: #52
2026-03-19 18:37:03 +11:00
unkinben 504d4ae7c9 fix: enable PuppetDB HTTPS support with automatic SSL certificate generation (#51)
This enables secure HTTPS communication to PuppetDB, required for other puppet related services

- make use of USE_OPENVOXSERVER flag

Reviewed-on: #51
2026-03-19 17:06:49 +11:00
unkinben 24d09744e3 git commit -m "fix: configure PuppetDB HTTPS connections and add Puppetboard SSL support (#50)
- Update PuppetDB connections from HTTP (8080) to HTTPS (8081)
- Add automatic certificate generation for Puppetboard using Puppet CA
- Implement initContainers for proper certificate provisioning before app start
- Add dedicated PVC for Puppetboard certificates with RWX access
- Configure SSL verification and client authentication for secure PuppetDB access

Reviewed-on: #50
2026-03-19 16:34:41 +11:00