Repository navigation
feat(server): apply ClusterCreationPolicy to provider option-list handlers - #74
Merged
Merged
Conversation
…dlers Wires ADR-018 modal-time policy enforcement into the four provider option-list handlers (ListImages, ListNetworks, ListClusters, ListStorageContainers). - New package internal/api/policy with generic Apply that wraps the butler-api/pkg/policy resolver. Apply lists ClusterCreationPolicy via the dynamic client, runs the ADR-018 section 6 resolution algorithm, and either filters (pin/allowList), reorders (recommended), or passes the items through with metadata (default). - New helpers in providers_policy.go (resolutionContext plus four applyXxxPolicy methods). The four ImageInfo, NetworkInfo, ClusterInfo, StorageContainerInfo response types get GetID methods so the generic policy.Apply filter can compare them against rule values without per-type code. - Each of the four list handlers now appends an optional policy block to the response envelope. Existing API consumers ignore the new field; the console (PR 5) reads it. - New ClusterCreationPolicyGVR constant in internal/k8s/client.go for symmetry with the other Butler CRDs. - Resolution context comes from UserSession.SelectedTeam and UserSession.SelectedEnvironment, already populated by the existing X-Butler-Environment middleware. No new request headers. Errors fetching policies are non-fatal at list time. The unfiltered provider response is served and the policy block is omitted, so a transient List failure does not block the modal entirely. go.mod carries a replace directive to ../butler-api for the implementation chain. Removed at merge phase. Depends on butlerdotdev/butler-api#45.
Merged
8 tasks done
added 2 commits
May 18, 2026 13:46
Extends PR #74 scope per ADR-018 amendment promoting UI authoring to v1 (butler-controller#106). Five new endpoints under /admin/policies: - GET /admin/policies — list (platform-viewer + above) - GET /admin/policies/{name} — fetch (platform-viewer + above) - POST /admin/policies — create (platform-admin only) - PUT /admin/policies/{name} — update (platform-admin only) - DELETE /admin/policies/{name} — delete (platform-admin only) Thin dynamic-client wrappers around ClusterCreationPolicyGVR. The admission webhook in butler-controller validates structure on Create and Update; webhook denials are unwrapped via the existing writeWebhookError helper into a structured 403 that the console keys on for inline field-level rendering (see ADR-010). Update path preserves metadata.resourceVersion for optimistic concurrency: a concurrent edit returns 409 with a reload-and-retry hint rather than silently overwriting. Read routes mounted under the platform-viewer admin block (visible in the admin UI for viewers); mutation routes mounted under the admin-only group (RequireAdmin). Matches the existing identity provider admin endpoint gating. UI consumer lands in butler-console#76 (PolicyForm + admin pages).
Remove local replace directive now that butler-api v0.21.0 is published with ClusterCreationPolicy types and resolver.
atbagan
added a commit
to butlerdotdev/butler-console
that referenced
this pull request
May 19, 2026
…76) * feat(create-cluster): render ClusterCreationPolicy curation in modal Consumes the optional policy block butler-server now appends to the four provider option-list responses. The console does not re-resolve policy; the server already filtered for pin and allowList and reordered for recommended, so the modal renders the server-applied state and adds the operator-facing affordances. API typings: - New PolicyMode union and PolicyMetadata interface in src/api/providers.ts - Optional policy field on ImageListResponse, NetworkListResponse, ClusterListResponse, StorageContainerListResponse - Both types re-exported from src/api/index.ts CreateClusterPage: - Four new policy state hooks (imagePolicy, networkPolicy, clusterPolicy, storagePolicy) capture the policy block from each list response - Auto-select honors policy.default when the default ID is in the filtered list, falling back to the existing heuristic otherwise - New PolicyBadge component renders below affected dropdowns, distinguishing pin ("Pinned by policy"), allowList ("Filtered by policy"), and recommended ("Recommended by policy" with the recommendedReason text). default mode stays silent since the pre-selection conveys intent without extra UI. - HarvesterFields and NutanixFields accept the new policy props and render the badge under each dropdown they own (Harvester network and image; Nutanix cluster, subnet, image, and storage container). Depends on butlerdotdev/butler-server#74 for the response envelope change. Existing API consumers without the new field continue to work because the policy block is optional. * feat(console): add admin pages for ClusterCreationPolicy authoring Extends PR #76 scope per ADR-018 amendment promoting UI authoring to v1 (butler-controller#106). Companion to butler-server admin endpoints landed in PR #74 commit 326c539. API client (src/api/policies.ts, re-exported from src/api/index.ts): - ClusterCreationPolicy, PolicyOptionType, PolicyOptionMode, PolicyOptionRule, PolicyScope, PolicyListResponse types - WebhookError type matching butler-server's writeWebhookError shape - policiesApi with list, get, create, update, delete Shared form (src/components/policy/PolicyForm.tsx): - Scope picker with three radios (clusterWide, team, teamAndEnv) and dependent team / environment dropdowns - Provider multi-select from the ProviderType enum - Per-rule editor with Option Type, Mode, Values (textarea), Default (Input), Recommended Reason (Input) - Local-validation messaging plus webhook denial inline rendering keyed on WebhookError.field Three pages under /admin/policies: - PoliciesListPage — list view with name, scope, providers, option types, age columns; empty state with CTA; New Policy button - PolicyCreatePage — wraps PolicyForm for create flow - PolicyDetailPage — view mode with three read-only cards (scope, providers, option rules); Edit toggles to PolicyForm; Delete with confirmation modal App.tsx wires three routes under RequireAdmin. Sidebar gains a "Creation Policies" link under the Platform section. Webhook denials surface inline against the offending field path (spec.options[<option-type>] for rule conflicts; other field paths as butler-server forwards them). * feat(console): pick providers and option values from live cluster state PolicyForm now fetches the cluster's configured ProviderConfigs at mount and drives both the targetProviders selection and the value pickers from live data instead of hardcoded type lists and free-text ID entry. targetProviders: - Filtered to provider types that have at least one ProviderConfig on the cluster (queried via providersApi.list). Avoids offering aws/azure/gcp when no such ProviderConfig exists. - Empty-state messaging when no ProviderConfigs are configured. Discovery provider: - New section in the form. Picks a specific ProviderConfig that the value pickers use as their data source. Defaults to the first configured provider matching the selected targetProviders. - Choice does not change what the policy targets; it sources the dropdowns. Targeting is the targetProviders list. Value pickers per option rule: - pin, allowList, recommended: chip list of selected entries plus a SearchableSelect of remaining entries. Entries labelled by name with the ID truncated as a suffix. - default: SearchableSelect with the discovery provider's entries. - "Edit raw IDs" per-rule toggle falls back to the original textarea or text input. Used for GitOps imports that reference IDs the picker can't resolve, or for entries the provider has removed. Per-option-type provider support matrix lives in PROVIDER_OPTION_SUPPORT in PolicyForm. Nutanix supports all four; Harvester supports image and network. Cluster and storageContainer on Harvester fall back to raw IDs with an inline notice. Pages drop the now-internal PROVIDER_TYPES constant; the form owns the configured-types lookup. * fix(console): keep policy value picker open during search PolicyForm defined ValuePicker and DefaultPicker as inner functions inside the PolicyForm component body. Inner-component definitions create a new function reference on every parent render, so React treats them as new component types and remounts on every PolicyForm re-render. The SearchableSelect's internal open state resets to false on remount, closing the dropdown the instant a state change rerendered the form (such as the click that opened it). Fix: inline the picker JSX directly inside the rule loop. No more inner components. Entry fetching moves to a single useEffect on PolicyForm keyed on discoveryKey and rules.length, so the form still lazy-loads option lists when the discovery provider or rule set changes. Same behavior as the previous implementation for the user; the dropdown now stays open and supports filter and keyboard nav as the underlying SearchableSelect intended. * fix(console): pin mode is exactly one value PolicyForm previously rendered pin with the same multi-value chip list as allowList and recommended. The name "pin" implies a single fixed value; the form should match. Pin now renders as a single-select identical in UX to default mode, storing the chosen ID as Values[0]. Local form validation rejects pin rules with zero or more-than-one values. The policy admission webhook in butler-controller enforces the same constraint at the schema level (companion fix in PR #105). allowList and recommended continue to accept multiple values as before. The orthogonal mode grid is now clean: pin: single value, enforcing default: single value, advisory allowList: multi value, enforcing recommended: multi value, advisory * chore(sidebar): rename Creation Policies to Policies, dedicated icon Two small UX improvements on the new admin policies tab. Name: "Creation Policies" pigeonholes the surface to the single ClusterCreationPolicy CRD that exists today. The URL is already /admin/policies (forward-friendly); aligning the label keeps the sidebar future-proof when other policy CRDs land alongside. The tab already lives under the Platform section, so prefixing with "Platform" would be redundant. Icon: the previous AccessControlIcon (shield + check) is also used by the Access Control tab right below in the same section. New PoliciesIcon is a clipboard with a check, the common policy/audit metaphor, distinct from the shield motif. Page H1 stays "Cluster Creation Policies" for now since the page specifically lists that one CRD. Generalizes when other policy kinds arrive. * feat(console): /admin/policies is a hub of policy kinds Previously /admin/policies jumped straight to the ClusterCreationPolicy list, which pigeonholes the surface. /admin/policies now renders a hub of cards, one per policy kind, with each card linking into a kind- specific list. ClusterCreationPolicies is the one card today; future policy CRDs (upgrade windows, addon governance, network pool allocation) slot in as additional cards without restructuring routes. Route restructure: /admin/policies -> PoliciesHubPage (new) /admin/policies/cluster-creation -> PoliciesListPage /admin/policies/cluster-creation/new -> PolicyCreatePage /admin/policies/cluster-creation/:name -> PolicyDetailPage Hub card shows: kind title, description, live policy count (fetched on mount), "Manage" CTA, and the ADR reference tag. Cards are clickable. Disabled future-kind cards can be added later as "coming soon" placeholders when work begins. Sidebar entry unchanged at /admin/policies (now lands on the hub). Server-side API endpoints unchanged. Internal navigation in PolicyCreatePage and PolicyDetailPage updated to redirect to /admin/policies/cluster-creation after create / delete / cancel. PoliciesListPage create button and detail link updated likewise. * chore(policies): add back buttons to nested pages, drop ADR tag Two UI polish items. Back buttons: the deeper policy pages now have a back affordance that returns one level up the hierarchy rather than relying on browser history. Cluster Creation list -> back to Policies hub New Cluster Creation -> back to Cluster Creation list Detail view -> back to Cluster Creation list Detail edit mode -> back to Detail view (cancels edit) Each back button is the standard chevron-left icon with the parent page name as the label, matching the AdminTeamDetailPage pattern. ADR tag: hub card no longer shows "ADR-018" in a chip. The ADR reference belongs in the design doc, not as user-facing UI metadata on the policy authoring surface. * feat(policies): resolve provider IDs to names on detail view The detail page previously rendered every option rule as "values: <uuid>, <uuid>" regardless of mode. Two improvements. Name resolution. The detail page now fetches the configured ProviderConfigs and the per-option-type entry lists from the first eligible discovery provider, then displays each ID alongside the provider-returned name. Fetch is best-effort and per-option-type cached in state; on failure the page falls back to bare IDs so the view still functions when the provider is unreachable. Mode-specific display: - pin: rendered as "Value: <name> (<id prefix>)" singular - allowList and recommended: bulleted list of names with id prefix per entry, count in the section heading - default: rendered as "Default: <name> (<id prefix>)" - recommended reason: free text on its own line ID prefix (first 8 chars) shows alongside each name for disambiguation when multiple entries share a label and for operator-facing kubectl correlation. Stored IDs are unchanged. * feat(policies): render scope as parsed fields, not JSON The Scope card on the detail page rendered the raw spec.scope object as a JSON pre block. Readable to anyone living in YAML, but unfriendly to an admin who just wants to see "this policy targets team observability-engineering". Replaced with a definition list keyed on the scope kind: Cluster-wide: Type: Cluster-wide Applies to: Every team on this cluster Team: Type: Team Team: <linked team name -> /admin/teams/:name> Team and environment: Type: Team and environment Team: <linked team name> Environment: <env name> Team name links to the admin team detail page. Environment shown as plain monospace text since there's no per-env admin view to link to today. ID prefix not relevant here since teams and envs are named identifiers, not UUIDs. * refactor(console): rename clusterWide scope to platformWide Companion to butler-api commit 76b3e2b and butler-controller commit ae79e68. The schema rename replaces "Cluster-wide" with "Platform-wide" since the policy applies to every team and every environment on the Butler platform, not to a specific Kubernetes cluster. - ScopeKind union in PolicyForm: 'clusterWide' -> 'platformWide' - PolicyScope interface in src/api/policies.ts: field rename - Form radio label "Cluster-wide" -> "Platform-wide" - Detail page rendering branches on the new field name with the updated user-facing label and "Applies to: Every team and environment on this Butler platform" - List page scope summary: "cluster-wide" -> "platform-wide" No behavior change. UI strings now match the schema and the hub copy. * fix(console): remove em-dash from PoliciesHubPage empty-state Three-pass review caught an em-dash on PoliciesHubPage as the empty-state placeholder for the policy count. Butler discipline disallows em-dashes in shipped code. Replaced the single em-dash with an ASCII hyphen. PR #76 diff is now em-dash free; pre-existing em-dashes elsewhere in the codebase are not in this PR's scope. --------- Co-authored-by: Andrew Bagan <andrew.bagan-2@corteva.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fourth PR in the five-repo ADR-018 implementation chain.
What this adds
Modal-time policy filtering
internal/api/policy/package wrappingbutler-api/pkg/policy.Resolvewith the response-shape transformation: filter for pin/allowList, reorder for recommended, pass-through for default.internal/api/handlers/providers_policy.gowithGetIDmethods on the four response types plusapplyXxxPolicyhelpers per option type.internal/api/handlers/providers.go. Each now appends an optionalpolicyblock to the response envelope.ClusterCreationPolicyGVRadded tointernal/k8s/client.go.Admin authoring endpoints (added in commit 326c539 per ADR-018 amendment butlerdotdev/butler-controller#106)
Five new endpoints under
/admin/policiesenabling UI authoring:GET /admin/policies(platform-viewer + above)GET /admin/policies/{name}(platform-viewer + above)POST /admin/policies(platform-admin only)PUT /admin/policies/{name}(platform-admin only; preserves resourceVersion for optimistic concurrency)DELETE /admin/policies/{name}(platform-admin only)Webhook denials are unwrapped via the existing
writeWebhookErrorhelper into a structured 403 the console renders inline against the offending field (ADR-010).Depends on
pkg/policyresolver, sourced via local replace)Replace directives in flight
go.modcarriesreplace github.com/butlerdotdev/butler-api => ../butler-api. Removed at merge phase.Backward compatibility
Existing API consumers ignore the optional
policyblock on the four list responses. New admin endpoints have no existing consumers.Implementation chain status
Verification
go build ./...cleango vet ./...cleango test ./internal/api/...passes