feat(platform): empty the composition, hand the OpenBao CRs to Flux #686

Open
sean wants to merge 1 commit from feat/retire-openbao-composition into trunk
Owner

What

Empties the platform-instance composition. resources: [], 549 lines down to 75. This is the step that actually hands the last ten Crossplane-managed resources to Flux.

Sequencing -- do not land this before both of these:

  1. seanfarm #755 (wait: false on platform-seanfarm-instances) -- otherwise ten Kustomizations gate on an XR that is about to compose nothing
  2. seanfarm #756 (the ported Retain guard) -- this PR deletes the guard that currently protects the signing keys

What it hands over

The composition owned the cluster's secrets and TLS authority: the shared OpenBaoConnection, the internal-CA PKI (pki_root, pki_internal, and the cnpg-server / cnpg-client / bundle-export issuing roles), and the transit backend holding the cosign image-signing keys, with its policy. Ten resources.

All ten are already declared in seanfarm kubernetes/flux/infrastructure/openbao-platform (#750), applied alongside Crossplane and co-managed by kustomize-controller with no field-ownership conflict. This hands over resources Flux already writes rather than moving them.

The OpenBao server left Crossplane on 2026-06-10. This is the same move for what the server holds, and the last one: this XR tracks exactly ten composed resources, and there is one composition left in the cluster.

What happens on landing

The nine direct composed resources carry ownerReferences to the XR with blockOwnerDeletion: true, so ordinary Kubernetes GC removes them and openbao-platform recreates them on its next reconcile.

The OpenBao-side state is not affected. Every one sets spec.deletionPolicy: Retain, which is enforced rather than advisory -- libseanfarm-operator v0.4.13 (pinned by the deployed openbao-operator v0.12.32) reconcile/harness.go:401:

if policy != DeletionPolicyRetain && !dryRun {
    ... h.Delete(ctx, obj, c) ...
}

The UnmountSecrets call that would destroy a mount and every key in it is inside that block (openbaotransitbackend_impl.go:152), so Retain never reaches it. Both PKI mounts, the intermediate CA, the three issuing roles, the transit mount and its three keys survive untouched. The window is one with no drift correction, not an outage.

The tenth, the shared OpenBaoConnection, is not collected at all -- its wrapping Object was set to Orphan in #684. It is referenced by name, over two hops, by five live OpenBaoDynamicSecrets in four tenant namespaces, so it must survive continuously rather than be recreated.

Guard changes

check-binjovi-forgejo-key.sh removed. It asserts one binjovi-forgejo-jwt ECDSA P-256 key by reading this composition, so emptying the file makes it fail -- which it did, and which is how the gap surfaced. It is ported and widened in seanfarm #756, which must land first so the invariant is never unguarded.

check-composition-fmt-args.sh canary taught to tell empty from broken. Its n == 0 check guards against the yq extraction silently breaking, and an empty composition legitimately yields zero patches. It now counts composed resources to distinguish the two: resources present with no patches found still fails; composing nothing passes.

That fix had a bug worth naming. The first version used add, which this yq build does not have, with stderr suppressed and an unset-to-zero default -- so the probe returned 0 and the canary silently passed when it should have fired. Exactly the failure class the canary exists to catch. It now uses a flat count, does not suppress yq errors, and rejects a non-numeric count.

Both behaviours verified: with a composed resource present and no string-combine patches, the canary fires (exit 1); with the composition empty, it passes.

What is left

The XRD, this composition, the Platform/seanfarm claim, and Crossplane itself -- removed in the next change, once this has soaked. Keeping the claim alive here means the Flux Kustomization applying it still has a resource to apply, and the handover can be verified on its own.

tests/check.sh exits 0.

https://claude.ai/code/session_01GfkEuwuvGSqGyXVuxwT7PA

## What Empties the `platform-instance` composition. `resources: []`, 549 lines down to 75. This is the step that actually hands the last ten Crossplane-managed resources to Flux. **Sequencing -- do not land this before both of these:** 1. seanfarm #755 (`wait: false` on `platform-seanfarm-instances`) -- otherwise ten Kustomizations gate on an XR that is about to compose nothing 2. seanfarm #756 (the ported `Retain` guard) -- this PR deletes the guard that currently protects the signing keys ## What it hands over The composition owned the cluster's secrets and TLS authority: the shared `OpenBaoConnection`, the internal-CA PKI (`pki_root`, `pki_internal`, and the `cnpg-server` / `cnpg-client` / `bundle-export` issuing roles), and the transit backend holding the cosign image-signing keys, with its policy. Ten resources. All ten are already declared in seanfarm `kubernetes/flux/infrastructure/openbao-platform` (#750), applied alongside Crossplane and co-managed by `kustomize-controller` with no field-ownership conflict. This hands over resources Flux already writes rather than moving them. The OpenBao **server** left Crossplane on 2026-06-10. This is the same move for what the server holds, and the last one: this XR tracks exactly ten composed resources, and there is one composition left in the cluster. ## What happens on landing The nine direct composed resources carry `ownerReferences` to the XR with `blockOwnerDeletion: true`, so ordinary Kubernetes GC removes them and `openbao-platform` recreates them on its next reconcile. **The OpenBao-side state is not affected.** Every one sets `spec.deletionPolicy: Retain`, which is enforced rather than advisory -- `libseanfarm-operator` v0.4.13 (pinned by the deployed `openbao-operator` v0.12.32) `reconcile/harness.go:401`: ```go if policy != DeletionPolicyRetain && !dryRun { ... h.Delete(ctx, obj, c) ... } ``` The `UnmountSecrets` call that would destroy a mount and every key in it is inside that block (`openbaotransitbackend_impl.go:152`), so `Retain` never reaches it. Both PKI mounts, the intermediate CA, the three issuing roles, the transit mount and its three keys survive untouched. The window is one with no drift correction, not an outage. The tenth, the shared `OpenBaoConnection`, is not collected at all -- its wrapping `Object` was set to `Orphan` in #684. It is referenced by name, over two hops, by five live `OpenBaoDynamicSecret`s in four tenant namespaces, so it must survive continuously rather than be recreated. ## Guard changes **`check-binjovi-forgejo-key.sh` removed.** It asserts one `binjovi-forgejo-jwt` ECDSA P-256 key by reading this composition, so emptying the file makes it fail -- which it did, and which is how the gap surfaced. It is **ported and widened** in seanfarm #756, which must land first so the invariant is never unguarded. **`check-composition-fmt-args.sh` canary taught to tell empty from broken.** Its `n == 0` check guards against the `yq` extraction silently breaking, and an empty composition legitimately yields zero patches. It now counts composed resources to distinguish the two: resources present with no patches found still fails; composing nothing passes. That fix had a bug worth naming. The first version used `add`, which this `yq` build does not have, with stderr suppressed and an unset-to-zero default -- so the probe returned 0 and the canary silently passed when it should have fired. Exactly the failure class the canary exists to catch. It now uses a flat count, does not suppress `yq` errors, and rejects a non-numeric count. Both behaviours verified: with a composed resource present and no string-combine patches, the canary fires (exit 1); with the composition empty, it passes. ## What is left The XRD, this composition, the `Platform/seanfarm` claim, and Crossplane itself -- removed in the next change, once this has soaked. Keeping the claim alive here means the Flux Kustomization applying it still has a resource to apply, and the handover can be verified on its own. `tests/check.sh` exits 0. https://claude.ai/code/session_01GfkEuwuvGSqGyXVuxwT7PA
feat(platform): empty the composition, hand the OpenBao CRs to Flux
Some checks failed
binjovi/ci Binjovi failed the frozen plan
0cc230f1d2
Set resources: [] on platform-instance. This hands the last ten
Crossplane-managed resources to Flux; 549 lines become 75.

Land seanfarm #755 and #756 first. #755 sets wait: false on
platform-seanfarm-instances, so ten Kustomizations stop gating on an XR that
is about to compose nothing. #756 ports the guard this change deletes.

The composition owned the shared OpenBaoConnection, the internal-CA PKI and
the transit backend holding the cosign signing keys. All ten resources are
already declared in seanfarm kubernetes/flux/infrastructure/openbao-platform
and co-managed there with no field-ownership conflict, so this hands over
resources Flux already writes.

On landing, the nine direct composed resources are garbage-collected through
their ownerReferences and recreated by Flux. The OpenBao-side state is not
affected: each sets deletionPolicy: Retain, and libseanfarm-operator v0.4.13,
pinned by the deployed openbao-operator v0.12.32, skips the whole remote-delete
block under Retain. The UnmountSecrets call is inside that block. Both PKI
mounts, the intermediate CA, the three roles, the transit mount and its three
keys survive. The tenth resource, the connection record, is orphaned rather
than collected, because five live OpenBaoDynamicSecrets reference it by name.

Two guard changes come with it.

check-binjovi-forgejo-key.sh is removed: it reads this composition for the
binjovi-forgejo-jwt key, so an empty file fails it. It is ported and widened in
seanfarm #756, which lands first so the invariant is never unguarded.

check-composition-fmt-args.sh now tells an empty composition apart from a
broken extraction. Its n == 0 canary guards against yq silently failing, and
composing nothing legitimately yields zero patches, so it counts composed
resources to decide. The first attempt at this used add, which this yq build
lacks, with stderr suppressed and an unset-to-zero default, so the probe
returned zero and the canary passed when it should have fired - the same
failure class it exists to catch. It now uses a flat count, lets yq errors
surface, and rejects a non-numeric count. Both directions were verified.

Claude-Session: https://claude.ai/code/session_01GfkEuwuvGSqGyXVuxwT7PA
Some checks failed
binjovi/ci Binjovi failed the frozen plan
Required
Details
Some required checks were not successful.
You are not authorized to merge this pull request.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin feat/retire-openbao-composition:feat/retire-openbao-composition
git switch feat/retire-openbao-composition
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
sean/platform-seanfarm!686
No description provided.