Skip to content

feat(cli): butleradm idp create/update + test/validate (P1 #6) - #55

Merged
atbagan merged 2 commits into
mainfrom
feat/idp-create-update-test
Jun 10, 2026
Merged

atbagan merged 2 commits into
mainfrom
feat/idp-create-update-test

Conversation

@atbagan

@atbagan atbagan commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

P1 #6 — IdentityProvider create/update + discovery test

Maps to audit P1 #6. Closes the SSO-standup gap: today butleradm idp is list/get/delete only, so an operator can't create or update an identity provider from the CLI. Adds the lifecycle verbs and completes console parity for the IdP surface.

Verbs

Verb Mechanism What it does
idp create NAME --from-file f.yaml CRD-direct Create an IdentityProvider from a manifest
idp update NAME --from-file f.yaml CRD-direct Update the spec, preserving status + resourceVersion
idp test --issuer-url URL [-o table|json|yaml] serverhttp Probe an issuer's OIDC discovery before creating a provider
idp validate NAME [-o ...] serverhttp Probe an existing provider's configured issuer

The split (mechanism)

  • create / update are CRD-direct and cluster-scoped (IdentityProvider is cluster-scoped; no -n), mirroring provider create/provider update. update does Get-then-merge-spec-then-Update, preserving the existing status conditions and resourceVersion so a spec edit never clobbers the controller-managed status.
  • test / validate go through butler-server (serverhttp). Both are read-only OIDC discovery probes (testOIDCDiscovery fetches the issuer's .well-known/openid-configuration), so neither mutates anything and neither needs a confirmation guard. test takes an issuer URL (pre-create); validate uses an existing provider's configured issuer (POST .../{name}/validate).

Secret handling (chosen v1 limitation)

The client secret is supplied by reference: the manifest's spec.oidc.clientSecretRef points at a Secret created separately (for example kubectl create secret), consistent with provider create --from-file. The console instead takes the secret inline and creates it server-side. Documented as a known v1 choice; a --client-secret-from-file convenience is queued for later. In exchange, the CRD-direct --from-file can set any spec field, including spec.oidc.insecureSkipVerify and spec.oidc.googleWorkspace, which neither client exposed before (audit Section 7.3 gap).

Tests

  • mergeIdPSpec preserves status conditions + resourceVersion (mutation-checked: a clobbering merge fails).
  • printDiscovery rendering for valid + invalid results (non-vacuous).
  • translateDiscoveryError 403/404/400 mapping.
  • build (both binaries) + vet + idp pkg tests green.

Cross-track

No collision with the portal auth track: test/validate ride serverhttp on the device-flow session JWT, not header impersonation.

Follows P1 #5 (provider get/update + discovery) in the CLI/console parity effort.

atbagan added 2 commits June 10, 2026 15:56
Add four verbs to the butleradm idp group, closing audit P1 #6 so an
operator can stand up SSO from the CLI (previously list/get/delete only):

- idp create NAME --from-file: create an IdentityProvider from a manifest.
- idp update NAME --from-file: update the spec, preserving the existing
  status conditions and resourceVersion so a spec edit never clobbers the
  controller-managed status.
- idp test --issuer-url URL: probe an OIDC issuer's discovery document
  before creating a provider.
- idp validate NAME: probe an existing provider's configured issuer.

create and update are CRD-direct and cluster-scoped, like the rest of the
idp group and mirroring provider create/update. test and validate go through
butler-server; both are read-only OIDC discovery probes (a fetch of the
issuer's well-known document), so neither mutates anything and neither needs
a confirmation guard.

The client secret is supplied by reference: the manifest's
spec.oidc.clientSecretRef points at a Secret created separately, consistent
with provider create. This is a chosen v1 limitation; the console takes the
secret inline and creates it server-side. In exchange, the manifest can set
any spec field, including spec.oidc.insecureSkipVerify and
spec.oidc.googleWorkspace, which neither client exposed before.
The idp get detail view and list ISSUER column read the pre-nesting paths
spec.issuerURL/clientID/scopes/claims, which are always empty on the current
CRD (fields live under spec.oidc). Read spec.oidc.* so issuer, client ID,
scopes, and claim mappings render. Surfaced by the P1 #6 lifecycle E2E.
@atbagan

atbagan commented Jun 10, 2026

Copy link
Copy Markdown
Contributor Author

Live E2E validation (butler-beta, console.beta.butlerlabs.dev)

Full IdP lifecycle exercised against the real butler-server + management apiserver, with a throwaway IdP (e2e-p16-throwaway, public Google issuer) and cleaned up to pre-state. The real butlerlabs IdP was untouched.

Step Verb Result
1 pre-state list PASS (1 real IdP captured, not touched)
2a test --issuer-url https://accounts.google.com PASS (valid=true, returned authorization + token endpoints)
2b test negative (non-OIDC URL) PASS (clean valid=false with message, no crash)
3 create --from-file PASS (CRD created; also correctly surfaced an apiserver validation error when spec.oidc.redirectURL was missing)
4 get + -o json PASS (json correct; detail view bug found — see below)
5 update --from-file PASS (load-bearing): spec changed, status condition preserved, resourceVersion incremented cleanly 118591345 -> 118591346 with no optimistic-concurrency conflict, against the real apiserver
6 validate NAME PASS (probed the existing IdP's issuer, valid=true)
7 delete PASS (removed)
8 cleanup list PASS (back to pre-state; throwaway IdP + Secret deleted)

Bug found and fixed in-branch

The E2E surfaced a pre-existing display bug: idp get (detail view) and idp list (ISSUER column) read spec.issuerURL/clientID/scopes/claims, but the CRD nests those under spec.oidc.*, so they always rendered blank. Fixed to read spec.oidc.* (commit on this branch); re-verified live that get/list now populate issuer, client ID, scopes, and claim mappings. (The STATUS/Phase column stays blank because no controller populates IdP status today; that is a separate concern, left out of scope.)

@atbagan
atbagan merged commit 35a6bb1 into main Jun 10, 2026
1 check passed
@atbagan
atbagan deleted the feat/idp-create-update-test branch June 10, 2026 23:38
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