Skip to content

feat(controller): add ClusterCreationPolicy webhook + TenantCluster enforcement - #105

Merged
atbagan merged 5 commits into
mainfrom
feat/clustercreationpolicy-webhooks-and-resolver
May 19, 2026
Merged

atbagan merged 5 commits into
mainfrom
feat/clustercreationpolicy-webhooks-and-resolver

Conversation

@atbagan

@atbagan atbagan commented May 18, 2026

Copy link
Copy Markdown
Contributor

Third PR in the five-repo ADR-018 implementation chain.

What this adds

  • ClusterCreationPolicyValidator (new webhook). Validates the CRD itself: referenced Team and environment exist, mode-specific value requirements, intra-tier conflict detection scoped to overlapping providers AND shared option-type keys.
  • TenantCluster webhook extension. New validatePolicy method resolves applicable policies via butler-api/pkg/policy, uses the fieldmap to read tc.Spec.InfrastructureOverride.{Nutanix,Harvester}.*, rejects on pin or allowList violation with the policy name and field path.
  • Manager registration. New webhook wired alongside the four existing validators in cmd/main.go.

Depends on

Replace directives in flight

go.mod carries replace github.com/butlerdotdev/butler-api => ../butler-api during the implementation chain. The directive is removed at merge phase once butler-api ships a tagged release containing pkg/policy.

Implementation chain status

Verification

  • go build ./... clean
  • go vet ./... clean
  • go test ./internal/webhook/... passes (existing webhook tests unaffected)

…enforcement

Implements ADR-018 enforcement on butler-controller. Two surfaces:

ClusterCreationPolicyValidator (new file
internal/webhook/clustercreationpolicy_webhook.go):
- Referenced Team exists when scope is team or teamAndEnvironment
- Referenced environment exists on the Team when scope is
  teamAndEnvironment
- Mode-specific value requirements with operator-facing error messages
  (pin/allowList require non-empty Values; default requires non-empty
  Default; recommended requires non-empty Values)
- Intra-tier conflict detection scoped to overlapping targetProviders
  AND shared option-type keys, naming the conflicting policy

Provider-entry existence is intentionally not validated at admission
time. A provider outage would block legitimate policy authoring; stale
references surface via the deferred status reconciler.

TenantClusterValidator extended (internal/webhook/tenantcluster_webhook.go):
- validatePolicy method calls into butler-api/pkg/policy to resolve
  applicable rules for (team, environment, provider) and enforce
  pin/allowList rejections via the fieldmap. Runs after ProviderConfig
  fetch because provider type drives field dispatch.
- Rejections name the policy via ResolveWithSources and include the
  TenantCluster spec field path.

Webhook registered in cmd/main.go alongside the four existing
validators.

go.mod carries a replace directive to ../butler-api during the
implementation chain. The directive will be removed at merge phase
once butler-api ships a tagged release containing pkg/policy.

Depends on butlerdotdev/butler-api#45 (types + pkg/policy resolver).
Original validateOptionRules treated pin and allowList identically:
non-empty Values required. By the ADR-018 name "pin" means a single
fixed value, not a set; the webhook now enforces len(Values) == 1
for pin and continues to require non-empty Values for allowList.

Companion to the form-side fix in butler-console PR #76. ADR
amendment to clarify the semantics will land separately.
@atbagan atbagan mentioned this pull request May 19, 2026
2 tasks done
atbagan added a commit that referenced this pull request May 19, 2026
The original §5 mode-semantics text allowed pin to carry one or
many Values, with the modal switching between a read-only single
entry and a list. That made pin and allowList only differ by the
optional Default field, which is a weak distinction the form
implementation surfaced as a real UX problem (operators added
multiple values to a "pin" rule when they expected a singleton).

Update §5 pin definition: Values must have length 1; the modal
renders the entry as a single read-only selection. Use allowList
when the value count is unbounded; use pin only when the single-
value semantics are deliberate.

Mode-behavior table row for pin updated to "Yes (single value)"
and "Renders read-only" so the orthogonal grid reads cleanly:

  pin:         single value, enforcing
  default:     single value, advisory
  allowList:   multi value,  enforcing
  recommended: multi value,  advisory

butler-controller webhook (PR #105) enforces len(Values) == 1 for
pin. butler-console form (PR #76) renders pin as a single-select.
No design change beyond clarifying intent that was already
present in the name "pin".
atbagan added 3 commits May 18, 2026 20:07
Companion to butler-api PR #45 commit 76b3e2b which renames the
scope variant across the API surface. The webhook references the
renamed field and tier name.

- scope.ClusterWide -> scope.PlatformWide on type access
- stClusterWide -> stPlatformWide internal tier enum
- Comment text in intra-tier conflict detection

Conflict detection logic, mode-specific validation, and referential
integrity checks are unchanged.
The CRD's OpenAPI v3 schema enforces enum validation on map values
but not on map keys. A policy with an unknown OptionType key (for
example "unknownOptionType") passes CRD admission silently.

Extending ClusterCreationPolicyValidator.validateOptionRules to
reject unknown OptionType keys closes the admission gap. The
runtime resolver already ignores unknown keys (iterates Options by
typed key), so this fix is admission-only, not affecting runtime
correctness.

Three-pass review on this PR caught that the fix promised in
ADR-018 e2e testing did not make it into the implementation. Now
landed.

Added clustercreationpolicy_webhook_test.go with table-driven cases
covering the unknown-key rejection, a known-key happy path, and the
existing pin-singleton rule for symmetry.

Platform finding worth capturing: kubebuilder enum markers on map
keys are advisory only in CRD OpenAPI v3 schema. Webhook validation
is required for enforcement of map-key sets.
Remove local replace directive now that butler-api v0.21.0 is
published with ClusterCreationPolicy types and resolver.
@atbagan
atbagan merged commit 366d67a into main May 19, 2026
8 checks passed
@atbagan
atbagan deleted the feat/clustercreationpolicy-webhooks-and-resolver branch May 19, 2026 13:54
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