openova/core
e3mrah 82ec096f4d
feat(rbac): Keycloak Identity Provider CRUD + Org-controller federation wire-up (slice F1+F2, #1098) (#1150)
Slice F of EPIC-3: per-Organization Azure SSO / Okta / generic-OIDC
federation reconciled into the per-Sovereign Keycloak realm.

F1 — catalyst-api keycloak client extension:
  products/catalyst/bootstrap/api/internal/keycloak/admin_idp.go
  - IdentityProvider + IdentityProviderMapper struct types
  - GET/POST/PUT/DELETE on /identity-provider/instances/{alias}
  - GET/POST/PUT on /identity-provider/instances/{alias}/mappers
  - EnsureIdentityProvider — find-or-create + drift-correct via byte-equal
    short-circuit on the catalyst-tracked field set; idempotent re-runs
  - EnsureIdentityProviderMapper — same idempotency anchor by mapper Name
  - 409 race path re-finds and reconciles drift after the sibling create
  - Drift detection ignores unknown server-side Config keys (Keycloak
    defaults like pkceEnabled) so we don't fight the admin UI
  - 9 unit tests covering clean-create / steady-state-no-write /
    drift-PUT / 409-race / not-found / list / mapper variants

F2 — organization-controller Reconcile extension:
  core/controllers/organization/internal/controller/
  - KeycloakClient interface gains EnsureIdentityProvider /
    EnsureIdentityProviderMapper / DeleteIdentityProvider
  - LiveKeycloak implementation mirrors the F1 admin_idp.go pattern
    (no cross-module Go dep on catalyst-api — out-of-process callers
    re-implement the narrow surface, like cert-manager-dynadot-webhook)
  - Reconciler resolves clientSecretRef from a K8s Secret in the
    controller's namespace (default catalyst-controllers) and passes
    the value to Keycloak in-memory only (Inviolable Principle #5)
  - Federation alias is deterministic: <provider>-<slug> (e.g.
    azure-sso-acme) so two Orgs federating to the same upstream IdP
    stay isolated
  - Empty-federation path best-effort deletes any stray IdP under any
    of the supported provider aliases
  - Two new status conditions surfaced on every reconcile so the
    access-matrix UI can render the federation column unconditionally:
      IdentityProviderConfigured   (True/AzureSSOConfigured|OktaConfigured|OIDCConfigured
                                    or False/NoFederation|SecretMissing|KCUnreachable)
      IdentityProviderClaimMappersConfigured
  - 5 new unit tests: AzureSSO happy-path / Secret-missing requeue /
    federation idempotent / cleanup-on-drop / Okta provider
  - Existing TestReconcile_HappyPath updated for 3-condition assertion

CRD extension — products/catalyst/chart/crds/organization.yaml:
  spec.identity.federationConfig already had {issuer, clientId,
  clientSecretRef}; this PR adds {tenantId, authorizationUrl, tokenUrl,
  jwksUrl, claimMappers[{src,dest}]}. No oneOf branches, no default
  inside arrays — passes structural-schema admission. Sample fixture
  (organization-sample-valid.yaml) extended.

RBAC — chart + kubebuilder source:
  Adds secrets:get/list/watch to organization-controller ClusterRole
  so the reconciler can read the federation client-secret K8s Secret.

Test coverage:
  go test -count=1 -race ./internal/keycloak/...                       OK
  go test -count=1 -race ./core/controllers/organization/...           OK
  go vet ./... clean across both modules
  Pre-existing flake confirmed: TestPinIssue_ConcurrentRapidFireRateLimit
  (canon §7 — CI-runner timing flake)

Refs: docs/EPICS-1-6-unified-design.md §6.4
      docs/INVIOLABLE-PRINCIPLES.md §4 (no hardcoded values), §5 (secrets)
      ADR-0001 §2.7 (Org CR is source of truth, KC is reconciliation target)

Co-authored-by: hatiyildiz <hati.yildiz@openova.io>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 04:26:12 +04:00
..
admin fix(admin,billing): drop unsafe state-write in snippet — spinner stays forever (#1000) (#1001) 2026-05-05 23:36:50 +04:00
cmd/cert-manager-dynadot-webhook fix(tls): DNS-01 wildcard TLS chain — solverName pdns, NodePort 30053, dynadot test fix (#582) 2026-05-02 13:49:58 +04:00
console feat(consolidation): Phase 1 — move Catalyst-Zero apps + CI + manifests into public monorepo 2026-04-28 12:08:09 +02:00
controllers feat(rbac): Keycloak Identity Provider CRUD + Org-controller federation wire-up (slice F1+F2, #1098) (#1150) 2026-05-09 04:26:12 +04:00
marketplace fix(marketplace,checkout): drop Google sign-in, port Sovereign-style PinInput6 (#1010) (#1011) 2026-05-06 00:08:42 +04:00
marketplace-api feat(consolidation): Phase 1 — move Catalyst-Zero apps + CI + manifests into public monorepo 2026-04-28 12:08:09 +02:00
pkg/dynadot-client fix(pdm/dynadot): remove fictional ResponseHeader wrapper from api3.json adapter (#939) (#948) 2026-05-05 15:11:39 +04:00
pool-domain-manager fix(pdm/dynadot): remove fictional ResponseHeader wrapper from api3.json adapter (#939) (#948) 2026-05-05 15:11:39 +04:00
services feat(catalog): catalog-svc HTTP REST service + chart wiring (slice L1+L2, #1097) (#1148) 2026-05-09 04:04:52 +04:00
README.md feat(consolidation): Phase 1 — move Catalyst-Zero apps + CI + manifests into public monorepo 2026-04-28 12:08:09 +02:00

Catalyst Control Plane (core/)

The user-facing Catalyst control plane modules. Status: Consolidated and deployed on Catalyst-Zero (Contabo k3s) as of Pass 105 (2026-04-28).

Read first: docs/PROVISIONING-PLAN.md, docs/GLOSSARY.md, docs/ARCHITECTURE.md, docs/IMPLEMENTATION-STATUS.md.


What this is

The four modules that constitute the Catalyst control plane's user-facing surface, plus the Go backend they share. Each is its own Containerfile-built workload, deployed on every Catalyst Sovereign (starting with Catalyst-Zero on Contabo, and on every franchised Sovereign provisioned thereafter).

Module Stack Purpose Deployed image
console/ Astro + Svelte Primary user-facing UI. Form / Advanced / IaC editor depths. The Sovereign-provisioning wizard at /sovereign (Phase 3) lives here. ghcr.io/openova-io/openova/console:<sha>
admin/ Astro + Svelte Sovereign-admin operations UI. Includes the canonical voucher / billing / catalog / orders / tenants admin surface that sovereign-admin uses to issue vouchers to franchised tenants. ghcr.io/openova-io/openova/admin:<sha>
marketplace/ Astro + Svelte Public-facing Blueprint card grid (the "App Store"). 5-step Plan → Apps → Addons → Checkout → Review flow. ghcr.io/openova-io/openova/marketplace:<sha>
marketplace-api/ Go Backend API for marketplace and console. Handlers (handlers/), provisioner (provisioner/), store (store/). Phase 4 extends this with full Hetzner provisioning. ghcr.io/openova-io/openova/marketplace-api:<sha>

The Helm chart that deploys all four (plus catalyst-ui, catalyst-api, and the legacy SME backend services) lives at products/catalyst/chart/.


CI / Build

Each module has a corresponding GitHub Actions workflow:

Each workflow watches its module path, builds the Containerfile, pushes to GHCR with a SHA tag, and pins the SHA into the corresponding manifest in products/catalyst/chart/templates/ (so Flux on Catalyst-Zero picks up the new image on the next reconciliation).


Migration history

  • Pass 105 (2026-04-28): console/, admin/, marketplace/ consolidated from openova-private/apps/{console,admin,marketplace}/ into this directory. marketplace-api/ consolidated from openova-private/website/marketplace-api/. Six CI workflows migrated to .github/workflows/ of the public repo. Catalyst-Zero K8s manifests migrated from openova-private/clusters/contabo-mkt/apps/{catalyst,sme/services,marketplace-api}/ into products/catalyst/chart/templates/. Image references updated from ghcr.io/openova-io/openova-private/sme-{admin,console,marketplace} to ghcr.io/openova-io/openova/{admin,console,marketplace}. The 8 legacy SME backend services (auth, billing, catalog, domain, gateway, notification, provisioning, tenant) keep their openova-private/sme-* image refs until their source code migrates in a follow-up phase.

Part of OpenOva