Skip to content

feat(operator): SPIFFE ClusterSPIFFEID provisioning (proposal 011 PR-2) - #84

Merged
flg77 merged 1 commit into
mainfrom
spiffe/operator-controller
May 15, 2026
Merged

feat(operator): SPIFFE ClusterSPIFFEID provisioning (proposal 011 PR-2)#84
flg77 merged 1 commit into
mainfrom
spiffe/operator-controller

Conversation

@flg77

@flg77 flg77 commented May 15, 2026

Copy link
Copy Markdown
Owner

Summary

Second PR of proposal 011. When an AgentCollective carries spec.spiffe.enabled: true, the operator issues a matching ClusterSPIFFEID custom resource so spire-controller-manager attests the collective's agent pods.

Inert by design — no AgentCollective carries spec.spiffe until an operator opts in. Existing deployments see zero behaviour change.

What's new

CRD (agentcollective_types.go + hand-updated CRD bases + zz_generated.deepcopy.go):

  • SpiffeSpec on AgentCollectiveSpecenabled, trustDomain. (Edge fields land in 012 PR-2.)
  • Status fields spiffeID, spiffeIssued, spiffeError on AgentCollectiveStatus.

Prerequisite detection:

  • New PrerequisiteStatus.SpireInstalled, set by PrerequisiteReconciler via the new APIGroupChecker.SpireInstalled() (detects the spire.spiffe.io API group).

Reconciler (operator/internal/reconcilers/collective/spiffe.go):

  • SpiffeReconciler.ReconcileCollective issues / updates one ClusterSPIFFEID per SPIFFE-enabled collective.
  • SPIFFE ID: spiffe://<trust-domain>/role/<collective-name>; trust domain defaults to <corpus>.acc.local when blank.
  • podSelector targets the collective's pods by the acc.io/collective label.
  • Strict no-op when spec.spiffe absent/disabled, or when spire-controller-manager is not installed — the latter reports an operator-readable reason via status.spiffeError, never failing reconciliation.
  • ClusterSPIFFEID is cluster-scoped → no namespaced owner ref; created with ACC labels, explicit cleanup via DeleteClusterSPIFFEID (wired in a follow-up).

Wired into CollectiveReconciler after KServe; the result is patched onto the AgentCollective status sub-resource (no-op when unchanged).

RBAC: operator ClusterRole gains spire.spiffe.io/clusterspiffeids (full verbs) — kubebuilder marker + hand-updated config/rbac/role.yaml.

Test plan

  • go build ./... clean — validated on lighthouse (Linux, Go 1.23.4)
  • 7 unit tests in operator/test/unit/spiffe_reconciler_test.go — disabled / enabled-false / SPIRE-absent no-op paths, ClusterSPIFFEID issuance + idempotency, trust-domain derivation, deletion. All pass.
  • Full operator suite green (go test ./...)
  • Reviewer note: generated artefacts (zz_generated.deepcopy.go, CRD bases) were hand-edited since controller-gen could not run in this environment. make manifests generate should produce an identical diff — worth a confirming run.

Proposal 011 reference

Vault: 011 - SPIFFE workload identity for ACC agents.md. PR-1 merged (#81). PR-3 (spiffe-helper sidecar) is next.

🤖 Generated with Claude Code

Second PR of proposal 011 — when an AgentCollective carries
spec.spiffe.enabled=true, the operator issues a matching
ClusterSPIFFEID custom resource so spire-controller-manager
attests the collective's agent pods.

Inert by design: no AgentCollective carries spec.spiffe until an
operator opts in.  Default behaviour for every existing
deployment is unchanged.

CRD changes (agentcollective_types.go + hand-updated CRD bases +
zz_generated.deepcopy.go):

* New SpiffeSpec struct on AgentCollectiveSpec — Enabled +
  TrustDomain.  Edge fields land in proposal 012 PR-2.
* New status fields SpiffeID / SpiffeIssued / SpiffeError on
  AgentCollectiveStatus.

Prerequisite detection:

* New PrerequisiteStatus.SpireInstalled, set by
  PrerequisiteReconciler from APIGroupChecker.SpireInstalled()
  (detects the spire.spiffe.io API group).

New reconciler (operator/internal/reconcilers/collective/spiffe.go):

* SpiffeReconciler.ReconcileCollective issues / updates one
  ClusterSPIFFEID per SPIFFE-enabled collective.
* SPIFFE ID: spiffe://<trust-domain>/role/<collective-name>.
  Trust domain defaults to <corpus>.acc.local when blank.
* podSelector targets the collective's pods by acc.io/collective.
* Strict no-op when spiffe disabled OR SPIRE absent — the latter
  reports an operator-readable reason via SpiffeResult.Err →
  status.spiffeError.  Never fails reconciliation.
* ClusterSPIFFEID is cluster-scoped so no namespaced owner ref;
  created with ACC labels, explicit cleanup via
  DeleteClusterSPIFFEID (wired in a follow-up).

Wired into CollectiveReconciler — runs after KServe; the
SpiffeResult is patched onto the AgentCollective status
sub-resource via patchSpiffeStatus (no-op when unchanged).

RBAC: operator ClusterRole gains spire.spiffe.io/clusterspiffeids
(get;list;watch;create;update;patch;delete) — kubebuilder marker
on the reconciler + hand-updated config/rbac/role.yaml.

Test coverage: 7 unit tests in spiffe_reconciler_test.go covering
disabled / enabled-false / SPIRE-absent no-op paths, ClusterSPIFFEID
issuance + idempotency, trust-domain derivation, deletion.  Fake
client registers the ClusterSPIFFEID GVK as unstructured.

Generated artefacts (deepcopy + CRD bases) hand-edited since
controller-gen could not be run in this environment; `make
manifests generate` should produce an identical diff.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@flg77
flg77 merged commit 8d43cb3 into main May 15, 2026
@flg77
flg77 deleted the spiffe/operator-controller branch May 15, 2026 15:51
flg77 added a commit that referenced this pull request May 15, 2026
Third PR of proposal 011 — when an AgentCollective has
spec.spiffe.enabled=true, every agent pod gains a spiffe-helper
sidecar that materialises the pod's X.509-SVID + JWT-SVID into a
shared emptyDir.  The agent container reads SVID files from there.

Inert by design: pods are unchanged until an operator opts in.

New operator/internal/reconcilers/collective/spiffe_sidecar.go:

* ApplySpiffeSidecar(deploy, collective, helperCMName) mutates a
  built agent Deployment in place — adds the spiffe-helper
  container, three volumes (spiffe-svids emptyDir shared with the
  agent; spiffe-workload-api CSI volume exposing the SPIRE Agent
  Workload API socket; spiffe-helper-config ConfigMap), the
  spiffe.io/spire-managed-identity pod annotation, and the agent
  container's read mount + ACC_SVID_* env vars.  No-op when SPIFFE
  is disabled.
* RenderSpiffeHelperConfig() produces the helper.conf HCL that
  drives the sidecar (agent_address, cert_dir, jwt_svids with
  audience acc-role-update).
* SpiffeEnabled() + SpiffeHelperConfigMapName() helpers, shared
  with the wiring in agent_deployment.go.

agent_deployment.go changes:

* ReconcileCollective upserts a per-collective spiffe-helper
  ConfigMap (<collective>-spiffe-helper) when SPIFFE is enabled.
* reconcileRoleDeployment calls ApplySpiffeSidecar after building
  the base Deployment, before the Upsert.

Test coverage: 6 unit tests in spiffe_sidecar_test.go covering
SpiffeEnabled, helper.conf rendering, disabled-is-no-op, sidecar
injection (containers/volumes/annotation/agent-env), helper
container shape, and ConfigMap naming.  contains() reused from
role_catalogue_test.go (same package).

Validated on lighthouse: go build ./... clean, full operator
suite green.

Stacked on spiffe/operator-controller (011 PR-2, PR #84).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
flg77 added a commit that referenced this pull request May 15, 2026
Third PR of proposal 011 — when an AgentCollective has
spec.spiffe.enabled=true, every agent pod gains a spiffe-helper
sidecar that materialises the pod's X.509-SVID + JWT-SVID into a
shared emptyDir.  The agent container reads SVID files from there.

Inert by design: pods are unchanged until an operator opts in.

New operator/internal/reconcilers/collective/spiffe_sidecar.go:

* ApplySpiffeSidecar(deploy, collective, helperCMName) mutates a
  built agent Deployment in place — adds the spiffe-helper
  container, three volumes (spiffe-svids emptyDir shared with the
  agent; spiffe-workload-api CSI volume exposing the SPIRE Agent
  Workload API socket; spiffe-helper-config ConfigMap), the
  spiffe.io/spire-managed-identity pod annotation, and the agent
  container's read mount + ACC_SVID_* env vars.  No-op when SPIFFE
  is disabled.
* RenderSpiffeHelperConfig() produces the helper.conf HCL that
  drives the sidecar (agent_address, cert_dir, jwt_svids with
  audience acc-role-update).
* SpiffeEnabled() + SpiffeHelperConfigMapName() helpers, shared
  with the wiring in agent_deployment.go.

agent_deployment.go changes:

* ReconcileCollective upserts a per-collective spiffe-helper
  ConfigMap (<collective>-spiffe-helper) when SPIFFE is enabled.
* reconcileRoleDeployment calls ApplySpiffeSidecar after building
  the base Deployment, before the Upsert.

Test coverage: 6 unit tests in spiffe_sidecar_test.go covering
SpiffeEnabled, helper.conf rendering, disabled-is-no-op, sidecar
injection (containers/volumes/annotation/agent-env), helper
container shape, and ConfigMap naming.  contains() reused from
role_catalogue_test.go (same package).

Validated on lighthouse: go build ./... clean, full operator
suite green.

Stacked on spiffe/operator-controller (011 PR-2, PR #84).

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
flg77 added a commit that referenced this pull request May 15, 2026
Second PR of proposal 012 — extends the operator-side SPIFFE
provisioning (011 PR-2) to edge deployments.

Inert by design: no behaviour change until an operator sets
deployMode=edge + spec.spiffe.

CRD changes (agentcollective_types.go + hand-updated CRD bases):

* SpiffeSpec gains EdgeTopology (nested | federated | ed25519,
  default nested) + EdgeSiteID.
* AgentCollectiveStatus gains EdgeSiteID — echoes the site
  qualifier baked into SpiffeID.

Reconciler (collective/spiffe.go):

* New computeSpiffeID() — when the owning AgentCorpus has
  deployMode=edge AND spiffe.edgeTopology=nested, the SPIFFE ID
  is site-qualified:
    spiffe://<trust-domain>/edge/<site-id>/role/<collective>
  Federated / ed25519 topologies + every non-edge deploy mode
  keep the flat spiffe://<trust-domain>/role/<collective> form.
* nested topology without an edgeSiteID reports a config error
  via SpiffeResult.Err -> status.spiffeError; never hard-fails
  reconciliation.
* CollectiveReconciler.patchSpiffeStatus also patches EdgeSiteID.

Deploy manifests (deploy/edge-spire/):

* nested-spire-server.values.yaml — Helm values overlay for the
  upstream spiffe/spire chart configured for nested topology
  (edge SPIRE downstream of an rhoai parent).  ACC does not
  vendor SPIRE — operators helm-install it.
* edge-bundle-fetcher.yaml — PVC + CronJob caching the parent
  SPIRE trust bundle every 6h for offline survival.
* README.md — install runbook + the edgeSiteID-uniqueness
  requirement + the no-RWX fallback.

Test coverage: 5 unit tests in spiffe_edge_test.go covering the
site-qualified ID, the missing-edgeSiteID error path, federated
plain ID, non-edge topology-ignored, and ed25519 topology.

Validated on lighthouse: go build ./... clean, full operator
suite green.

Stacked on spiffe/agent-sidecar (011 PR-3, PR #85), which is
itself stacked on 011 PR-2 (#84).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
flg77 added a commit that referenced this pull request May 15, 2026
Second PR of proposal 012 — extends the operator-side SPIFFE
provisioning (011 PR-2) to edge deployments.

Inert by design: no behaviour change until an operator sets
deployMode=edge + spec.spiffe.

CRD changes (agentcollective_types.go + hand-updated CRD bases):

* SpiffeSpec gains EdgeTopology (nested | federated | ed25519,
  default nested) + EdgeSiteID.
* AgentCollectiveStatus gains EdgeSiteID — echoes the site
  qualifier baked into SpiffeID.

Reconciler (collective/spiffe.go):

* New computeSpiffeID() — when the owning AgentCorpus has
  deployMode=edge AND spiffe.edgeTopology=nested, the SPIFFE ID
  is site-qualified:
    spiffe://<trust-domain>/edge/<site-id>/role/<collective>
  Federated / ed25519 topologies + every non-edge deploy mode
  keep the flat spiffe://<trust-domain>/role/<collective> form.
* nested topology without an edgeSiteID reports a config error
  via SpiffeResult.Err -> status.spiffeError; never hard-fails
  reconciliation.
* CollectiveReconciler.patchSpiffeStatus also patches EdgeSiteID.

Deploy manifests (deploy/edge-spire/):

* nested-spire-server.values.yaml — Helm values overlay for the
  upstream spiffe/spire chart configured for nested topology
  (edge SPIRE downstream of an rhoai parent).  ACC does not
  vendor SPIRE — operators helm-install it.
* edge-bundle-fetcher.yaml — PVC + CronJob caching the parent
  SPIRE trust bundle every 6h for offline survival.
* README.md — install runbook + the edgeSiteID-uniqueness
  requirement + the no-RWX fallback.

Test coverage: 5 unit tests in spiffe_edge_test.go covering the
site-qualified ID, the missing-edgeSiteID error path, federated
plain ID, non-edge topology-ignored, and ed25519 topology.

Validated on lighthouse: go build ./... clean, full operator
suite green.

Stacked on spiffe/agent-sidecar (011 PR-3, PR #85), which is
itself stacked on 011 PR-2 (#84).

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
flg77 added a commit that referenced this pull request Jun 15, 2026
feat(agent): pack-role boot-and-wait (AgentRole=str + boot-dormant/self-promote)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant