feat(bp-crossplane-claims): UserAccess CRD + Composition + RBAC ClusterRoles for Sovereign IAM (closes #322) (#446)
Adds the data plane for the Sovereign IAM access plane (epic #320): - platform/crossplane-claims/chart/templates/xrds/useraccess.yaml XUserAccess XRD (access.openova.io/v1alpha1) — cluster-scoped Claim carrying user identity (Keycloak subject + groups), Sovereign ref, and one or more (application, role, namespaces) grants. - platform/crossplane-claims/chart/templates/compositions/useraccess.yaml Default Composition useraccess.compose.openova.io — materialises one RoleBinding per Claim via provider-kubernetes Object against the per-Sovereign sovereign-<sovereignRef> ProviderConfig. Multi-grant shapes are expanded api-side into N single-grant Claims (avoids the Composition-iteration trap; no composition-functions introduced). - platform/crossplane-claims/chart/templates/clusterroles.yaml Three canonical ClusterRoles — openova:application-{admin,editor,viewer}. Editor + viewer explicitly omit secrets; admin can manage namespace- scoped roles/rolebindings (NOT cluster-scoped). - userAccess.enabled values toggle (default true), version bumps to 1.1.0 on chart + blueprint, sample fixture, validation script extended to expect 7 XRDs / 7 Compositions / 3 ClusterRoles. Canonical seam: extends the existing platform/crossplane-claims/chart/ XRD+Composition pattern (compose.openova.io/v1alpha1 family). New API group access.openova.io is intentional — IAM is a separate concern from the cloud-resource compose.* family. No catalyst-api or UI code touched (those are #323's territory; this PR ships the data model #323 consumes). Co-authored-by: hatiyildiz <hatiyildiz@noreply.github.com>
This commit is contained in:
parent
7ea496ba64
commit
b6810c1940
@ -5,7 +5,7 @@ metadata:
|
||||
labels:
|
||||
catalyst.openova.io/section: pts-3-2-gitops-and-iac
|
||||
spec:
|
||||
version: 1.0.0
|
||||
version: 1.1.0
|
||||
card:
|
||||
title: crossplane-claims
|
||||
summary: |
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
apiVersion: v2
|
||||
name: bp-crossplane-claims
|
||||
version: 1.0.0
|
||||
version: 1.1.0
|
||||
description: |
|
||||
Catalyst Crossplane XRDs + Compositions Blueprint. Carries ONLY the
|
||||
apiextensions.crossplane.io/v1 CompositeResourceDefinition and
|
||||
|
||||
152
platform/crossplane-claims/chart/templates/clusterroles.yaml
Normal file
152
platform/crossplane-claims/chart/templates/clusterroles.yaml
Normal file
@ -0,0 +1,152 @@
|
||||
{{- if .Values.userAccess.enabled }}
|
||||
# Canonical Sovereign-IAM ClusterRoles — openova:application-{admin,editor,viewer}.
|
||||
#
|
||||
# Every Sovereign cluster receives these three ClusterRoles via
|
||||
# bp-crossplane-claims. The UserAccess Composition's RoleBinding roleRef
|
||||
# resolves to one of them.
|
||||
#
|
||||
# Per docs/INVIOLABLE-PRINCIPLES.md:
|
||||
# #3 ClusterRoles are Helm-installed (Flux reconciles bp-crossplane-claims),
|
||||
# never `kubectl apply`-ed directly.
|
||||
# #4 Verb lists are explicit — no `*` on secrets even for admin
|
||||
# (admin can manage Secrets, but the verb list spells out the verbs
|
||||
# so the audit trail is grep-able).
|
||||
#
|
||||
# Role boundary contract:
|
||||
# admin — full namespace control + RBAC management within the namespace
|
||||
# (create/get/list/watch/update/patch on roles + rolebindings).
|
||||
# Can read/write Secrets — required for any team that ships
|
||||
# Apps using imagePullSecrets, TLS, or DB creds.
|
||||
# editor — workloads + observability (pods, deployments, jobs, services,
|
||||
# configmaps, ingresses, networkpolicies). Can exec/log into
|
||||
# pods. CANNOT read/write Secrets — that gate is what separates
|
||||
# editor from admin.
|
||||
# viewer — get/list/watch only across the same resource families as
|
||||
# editor. CANNOT read Secrets.
|
||||
#
|
||||
# These are the three roles the UserAccess Composition can bind. Anything
|
||||
# narrower (e.g. read-only-of-one-deployment) is out of scope — the model
|
||||
# is intentionally coarse; finer slicing belongs to the Application's own
|
||||
# RBAC payload, not to Sovereign-wide IAM.
|
||||
|
||||
# ── openova:application-admin ────────────────────────────────────────────
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: openova:application-admin
|
||||
labels:
|
||||
catalyst.openova.io/component: crossplane-claims
|
||||
catalyst.openova.io/sovereign-iam-role: admin
|
||||
rules:
|
||||
# Core workload + observability resources, full CRUD.
|
||||
- apiGroups: [""]
|
||||
resources:
|
||||
- pods
|
||||
- pods/exec
|
||||
- pods/log
|
||||
- pods/portforward
|
||||
- services
|
||||
- configmaps
|
||||
- persistentvolumeclaims
|
||||
- serviceaccounts
|
||||
- events
|
||||
verbs: ["create", "get", "list", "watch", "update", "patch", "delete"]
|
||||
# Secrets — admin only. Editor + viewer roles deliberately omit secrets.
|
||||
- apiGroups: [""]
|
||||
resources: ["secrets"]
|
||||
verbs: ["create", "get", "list", "watch", "update", "patch", "delete"]
|
||||
# apps/* — deployments, statefulsets, daemonsets, replicasets.
|
||||
- apiGroups: ["apps"]
|
||||
resources: ["deployments", "statefulsets", "daemonsets", "replicasets"]
|
||||
verbs: ["create", "get", "list", "watch", "update", "patch", "delete"]
|
||||
- apiGroups: ["apps"]
|
||||
resources: ["deployments/scale", "statefulsets/scale"]
|
||||
verbs: ["get", "patch", "update"]
|
||||
# batch/* — jobs, cronjobs.
|
||||
- apiGroups: ["batch"]
|
||||
resources: ["jobs", "cronjobs"]
|
||||
verbs: ["create", "get", "list", "watch", "update", "patch", "delete"]
|
||||
# networking — ingresses, networkpolicies.
|
||||
- apiGroups: ["networking.k8s.io"]
|
||||
resources: ["ingresses", "networkpolicies"]
|
||||
verbs: ["create", "get", "list", "watch", "update", "patch", "delete"]
|
||||
# RBAC — admin can manage roles/rolebindings within the namespace
|
||||
# (NOT clusterroles/clusterrolebindings — those stay platform-only).
|
||||
- apiGroups: ["rbac.authorization.k8s.io"]
|
||||
resources: ["roles", "rolebindings"]
|
||||
verbs: ["create", "get", "list", "watch", "update", "patch", "delete"]
|
||||
|
||||
---
|
||||
# ── openova:application-editor ───────────────────────────────────────────
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: openova:application-editor
|
||||
labels:
|
||||
catalyst.openova.io/component: crossplane-claims
|
||||
catalyst.openova.io/sovereign-iam-role: editor
|
||||
rules:
|
||||
# Core workload + observability — same as admin EXCEPT secrets.
|
||||
- apiGroups: [""]
|
||||
resources:
|
||||
- pods
|
||||
- pods/exec
|
||||
- pods/log
|
||||
- pods/portforward
|
||||
- services
|
||||
- configmaps
|
||||
- persistentvolumeclaims
|
||||
- serviceaccounts
|
||||
- events
|
||||
verbs: ["create", "get", "list", "watch", "update", "patch", "delete"]
|
||||
# Secrets: explicitly omitted. Editors cannot read or write secrets.
|
||||
# An editor that needs a credential must ask an admin to install it.
|
||||
- apiGroups: ["apps"]
|
||||
resources: ["deployments", "statefulsets", "daemonsets", "replicasets"]
|
||||
verbs: ["create", "get", "list", "watch", "update", "patch", "delete"]
|
||||
- apiGroups: ["apps"]
|
||||
resources: ["deployments/scale", "statefulsets/scale"]
|
||||
verbs: ["get", "patch", "update"]
|
||||
- apiGroups: ["batch"]
|
||||
resources: ["jobs", "cronjobs"]
|
||||
verbs: ["create", "get", "list", "watch", "update", "patch", "delete"]
|
||||
- apiGroups: ["networking.k8s.io"]
|
||||
resources: ["ingresses", "networkpolicies"]
|
||||
verbs: ["create", "get", "list", "watch", "update", "patch", "delete"]
|
||||
# RBAC: editors cannot manage RBAC. roles/rolebindings are admin-only.
|
||||
|
||||
---
|
||||
# ── openova:application-viewer ───────────────────────────────────────────
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: openova:application-viewer
|
||||
labels:
|
||||
catalyst.openova.io/component: crossplane-claims
|
||||
catalyst.openova.io/sovereign-iam-role: viewer
|
||||
rules:
|
||||
# Read-only across the same resource families as editor — minus secrets,
|
||||
# minus exec/portforward (a viewer can read logs, but cannot exec into a
|
||||
# pod or open a port-forward; both of those are write-equivalent verbs
|
||||
# against the workload).
|
||||
- apiGroups: [""]
|
||||
resources:
|
||||
- pods
|
||||
- pods/log
|
||||
- services
|
||||
- configmaps
|
||||
- persistentvolumeclaims
|
||||
- serviceaccounts
|
||||
- events
|
||||
verbs: ["get", "list", "watch"]
|
||||
# Secrets: explicitly omitted. Viewers cannot read secrets.
|
||||
- apiGroups: ["apps"]
|
||||
resources: ["deployments", "statefulsets", "daemonsets", "replicasets"]
|
||||
verbs: ["get", "list", "watch"]
|
||||
- apiGroups: ["batch"]
|
||||
resources: ["jobs", "cronjobs"]
|
||||
verbs: ["get", "list", "watch"]
|
||||
- apiGroups: ["networking.k8s.io"]
|
||||
resources: ["ingresses", "networkpolicies"]
|
||||
verbs: ["get", "list", "watch"]
|
||||
{{- end }}
|
||||
@ -0,0 +1,176 @@
|
||||
{{- if .Values.userAccess.enabled }}
|
||||
# Composition: useraccess.compose.openova.io — default realization for
|
||||
# XUserAccess.
|
||||
#
|
||||
# Translates a UserAccess Claim into a single provider-kubernetes Object
|
||||
# whose embedded manifest is a RoleBinding referencing one of the three
|
||||
# canonical openova:application-{admin,editor,viewer} ClusterRoles. The
|
||||
# Object lands on the per-Sovereign target cluster selected by
|
||||
# `sovereign-<sovereignRef>` ProviderConfig.
|
||||
#
|
||||
# This Composition is intentionally narrow: it materialises ONE grant
|
||||
# per Claim — the (application, role) shape carried in
|
||||
# spec.applications[0]. Multi-app / multi-namespace grants are expanded
|
||||
# by catalyst-api into N UserAccess Claims (one per application ×
|
||||
# namespace × role) BEFORE writing them. That keeps the Composition
|
||||
# stateless and avoids the for-loop-in-Composition trap (Crossplane v1
|
||||
# Compositions cannot iterate over array fields without
|
||||
# composition-functions, which we deliberately do not introduce here per
|
||||
# the canonical-seam map).
|
||||
#
|
||||
# Per docs/INVIOLABLE-PRINCIPLES.md:
|
||||
# #3 RoleBindings on Sovereign clusters are provider-kubernetes
|
||||
# Objects, never raw kubectl apply.
|
||||
# #4 Role-name suffix (admin/editor/viewer) is a schema enum; the
|
||||
# canonical ClusterRole name is composed via fmt transform —
|
||||
# never hardcoded.
|
||||
#
|
||||
# UPDATE flow:
|
||||
# - patch spec.applications[0].role → ClusterRole reference rotates
|
||||
# in place (provider-kubernetes patches the live RoleBinding).
|
||||
# - patch spec.user.keycloakSubject → subject rewrites.
|
||||
#
|
||||
# DELETE flow:
|
||||
# - delete the UserAccess → composite controller deletes the Object →
|
||||
# provider-kubernetes deletes the RoleBinding on the Sovereign.
|
||||
# - hand-edited RoleBinding gets reverted within Crossplane's poll
|
||||
# interval (default 60s).
|
||||
|
||||
apiVersion: apiextensions.crossplane.io/v1
|
||||
kind: Composition
|
||||
metadata:
|
||||
name: useraccess.compose.openova.io
|
||||
labels:
|
||||
catalyst.openova.io/component: crossplane
|
||||
catalyst.openova.io/composition-family: iam
|
||||
catalyst.openova.io/day2-crud: "true"
|
||||
spec:
|
||||
compositeTypeRef:
|
||||
apiVersion: access.openova.io/v1alpha1
|
||||
kind: XUserAccess
|
||||
|
||||
writeConnectionSecretsToNamespace: crossplane-system
|
||||
|
||||
resources:
|
||||
# ── 1. RoleBinding (provider-kubernetes Object) ───────────────────────
|
||||
# The catalyst-api expands multi-app/multi-namespace UserAccess Claims
|
||||
# into N single-app-single-namespace Claims before submitting; this
|
||||
# resource block consumes spec.applications[0] only. The
|
||||
# spec.applications[0].namespaces[0] field MUST be set by the api
|
||||
# (the schema doesn't enforce it because the bare-Claim shape that
|
||||
# an operator might author is also valid input — the api expands).
|
||||
- name: rolebinding
|
||||
base:
|
||||
apiVersion: kubernetes.crossplane.io/v1alpha2
|
||||
kind: Object
|
||||
spec:
|
||||
forProvider:
|
||||
manifest:
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: RoleBinding
|
||||
metadata:
|
||||
labels:
|
||||
catalyst.openova.io/managed-by: crossplane
|
||||
catalyst.openova.io/composition-family: iam
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: ClusterRole
|
||||
name: ""
|
||||
subjects: []
|
||||
providerConfigRef:
|
||||
name: ""
|
||||
deletionPolicy: Delete
|
||||
patches:
|
||||
# Object name: <sovereign>-<app>-<role>-<subject-or-first-group>
|
||||
- fromFieldPath: metadata.name
|
||||
toFieldPath: metadata.name
|
||||
transforms:
|
||||
- type: string
|
||||
string:
|
||||
fmt: "useraccess-%s"
|
||||
|
||||
# Inner RoleBinding name: same shape, prefix with useraccess-
|
||||
- fromFieldPath: metadata.name
|
||||
toFieldPath: spec.forProvider.manifest.metadata.name
|
||||
transforms:
|
||||
- type: string
|
||||
string:
|
||||
fmt: "useraccess-%s"
|
||||
|
||||
# Target namespace: the api MUST have expanded the Claim to a
|
||||
# single-namespace shape before submitting. This patch reads
|
||||
# applications[0].namespaces[0] verbatim.
|
||||
- fromFieldPath: spec.applications[0].namespaces[0]
|
||||
toFieldPath: spec.forProvider.manifest.metadata.namespace
|
||||
|
||||
# roleRef.name: rewrite role suffix to the canonical ClusterRole
|
||||
# via fmt transform. spec.applications[0].role is enum
|
||||
# {admin, editor, viewer}; output is openova:application-<role>.
|
||||
- fromFieldPath: spec.applications[0].role
|
||||
toFieldPath: spec.forProvider.manifest.roleRef.name
|
||||
transforms:
|
||||
- type: string
|
||||
string:
|
||||
fmt: "openova:application-%s"
|
||||
|
||||
# subjects[0]: User identity from spec.user.keycloakSubject.
|
||||
# The User-vs-Group decision is encoded by the api at expansion
|
||||
# time: a Claim authored against a Group is rewritten to populate
|
||||
# spec.user.keycloakSubject="<group-marker>" — but the simpler
|
||||
# contract for the v1alpha1 Composition is: keycloakSubject is
|
||||
# always an OIDC `sub`, group-binding is a separate Claim shape
|
||||
# with subjects[].kind=Group (not modelled in this Composition;
|
||||
# tracked for v1alpha2 once group-mode usage is observed).
|
||||
- fromFieldPath: spec.user.keycloakSubject
|
||||
toFieldPath: spec.forProvider.manifest.subjects[0].kind
|
||||
transforms:
|
||||
- type: match
|
||||
match:
|
||||
patterns:
|
||||
- type: regexp
|
||||
regexp: '.+'
|
||||
result: 'User'
|
||||
fallbackTo: Value
|
||||
fallbackValue: 'User'
|
||||
- fromFieldPath: spec.user.keycloakSubject
|
||||
toFieldPath: spec.forProvider.manifest.subjects[0].apiGroup
|
||||
transforms:
|
||||
- type: match
|
||||
match:
|
||||
patterns:
|
||||
- type: regexp
|
||||
regexp: '.+'
|
||||
result: 'rbac.authorization.k8s.io'
|
||||
fallbackTo: Value
|
||||
fallbackValue: 'rbac.authorization.k8s.io'
|
||||
- fromFieldPath: spec.user.keycloakSubject
|
||||
toFieldPath: spec.forProvider.manifest.subjects[0].name
|
||||
transforms:
|
||||
- type: string
|
||||
string:
|
||||
fmt: "oidc:%s"
|
||||
|
||||
# ProviderConfig: per-Sovereign provider-kubernetes config named
|
||||
# sovereign-<sovereignRef>. catalyst-api creates the
|
||||
# ProviderConfig as part of Sovereign provisioning (#326).
|
||||
- fromFieldPath: spec.sovereignRef
|
||||
toFieldPath: spec.providerConfigRef.name
|
||||
transforms:
|
||||
- type: string
|
||||
string:
|
||||
fmt: "sovereign-%s"
|
||||
|
||||
# Sovereign label on the RoleBinding — for ops/audit queries.
|
||||
- fromFieldPath: spec.sovereignRef
|
||||
toFieldPath: spec.forProvider.manifest.metadata.labels[catalyst.openova.io/sovereign]
|
||||
|
||||
# Bubble grant-count to status (single grant per Composition pass;
|
||||
# multi-grant Claims are expanded api-side before submission).
|
||||
- type: ToCompositeFieldPath
|
||||
fromFieldPath: spec.sovereignRef
|
||||
toFieldPath: status.providerConfigRef
|
||||
transforms:
|
||||
- type: string
|
||||
string:
|
||||
fmt: "sovereign-%s"
|
||||
{{- end }}
|
||||
181
platform/crossplane-claims/chart/templates/xrds/useraccess.yaml
Normal file
181
platform/crossplane-claims/chart/templates/xrds/useraccess.yaml
Normal file
@ -0,0 +1,181 @@
|
||||
{{- if .Values.userAccess.enabled }}
|
||||
# XRD: XUserAccess — Catalyst Sovereign IAM intent.
|
||||
#
|
||||
# A UserAccess Claim is the source-of-truth declaration of "user X
|
||||
# (identified by Keycloak subject or group) has role Y on Sovereign Z's
|
||||
# Application A in namespace(s) N". The Composition (useraccess.compose
|
||||
# .openova.io) materialises the intent into one K8s RoleBinding per
|
||||
# (application × namespace × role) tuple on the target Sovereign cluster
|
||||
# via provider-kubernetes.
|
||||
#
|
||||
# Per docs/INVIOLABLE-PRINCIPLES.md:
|
||||
# #3 Crossplane is the day-2 IaC for IAM grants too — never let
|
||||
# catalyst-api `kubectl apply` RoleBindings directly. catalyst-api
|
||||
# writes the UserAccess Claim; Crossplane reconciles the RBAC.
|
||||
# #4 Role names are the three canonical openova:application-* ClusterRoles
|
||||
# (admin/editor/viewer); the role string in spec.applications[].role
|
||||
# is one of {admin, editor, viewer} and the Composition rewrites it
|
||||
# to the fully-qualified ClusterRole name.
|
||||
#
|
||||
# Cluster-scoped: a UserAccess spans namespaces by design (one user can
|
||||
# hold grants on many tenants), so the Claim itself is cluster-scoped.
|
||||
# Composed RoleBindings are namespaced per spec.applications[].namespaces.
|
||||
|
||||
apiVersion: apiextensions.crossplane.io/v1
|
||||
kind: CompositeResourceDefinition
|
||||
metadata:
|
||||
name: xuseraccesses.access.openova.io
|
||||
labels:
|
||||
catalyst.openova.io/component: crossplane
|
||||
catalyst.openova.io/composition-family: iam
|
||||
catalyst.openova.io/day2-crud: "true"
|
||||
spec:
|
||||
group: access.openova.io
|
||||
names:
|
||||
kind: XUserAccess
|
||||
plural: xuseraccesses
|
||||
claimNames:
|
||||
kind: UserAccess
|
||||
plural: useraccesses
|
||||
defaultCompositionRef:
|
||||
name: useraccess.compose.openova.io
|
||||
versions:
|
||||
- name: v1alpha1
|
||||
served: true
|
||||
referenceable: true
|
||||
schema:
|
||||
openAPIV3Schema:
|
||||
type: object
|
||||
required: [spec]
|
||||
properties:
|
||||
spec:
|
||||
type: object
|
||||
required: [user, sovereignRef, applications]
|
||||
properties:
|
||||
user:
|
||||
type: object
|
||||
description: |
|
||||
Identity of the principal receiving access. Either
|
||||
keycloakSubject (an OIDC `sub` claim, binding a single
|
||||
user) or keycloakGroups (a list of group names, binding
|
||||
every member of those groups) or both. The Composition
|
||||
emits one RoleBinding subject per identity.
|
||||
properties:
|
||||
keycloakSubject:
|
||||
type: string
|
||||
description: |
|
||||
OIDC `sub` claim — the Keycloak user's stable id.
|
||||
Renders as a RoleBinding subject of kind=User,
|
||||
name="oidc:<subject>" (matches the api-server
|
||||
--oidc-username-prefix convention).
|
||||
keycloakGroups:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: |
|
||||
Keycloak group names. Each renders as an additional
|
||||
RoleBinding subject of kind=Group,
|
||||
name="oidc:<group>" (matches --oidc-groups-prefix).
|
||||
sovereignRef:
|
||||
type: string
|
||||
description: |
|
||||
Sovereign identifier — used to select the per-Sovereign
|
||||
provider-kubernetes ProviderConfig (named
|
||||
`sovereign-<sovereignRef>`) that the Composition writes
|
||||
RoleBindings against.
|
||||
pattern: '^[a-z0-9][a-z0-9-]{0,62}$'
|
||||
applications:
|
||||
type: array
|
||||
description: |
|
||||
One entry per (Application × role) grant. An entry may
|
||||
name explicit namespaces, vClusters (rendered as
|
||||
additional namespaces with the vCluster prefix), or
|
||||
omit both — in which case the catalyst-api expands the
|
||||
Claim against Application.k8s_namespaces before
|
||||
applying. The Composition itself only renders explicit
|
||||
namespaces; expansion is the api's job.
|
||||
minItems: 1
|
||||
items:
|
||||
type: object
|
||||
required: [app, role]
|
||||
properties:
|
||||
app:
|
||||
type: string
|
||||
description: |
|
||||
Application slug as registered in catalyst-api
|
||||
(e.g. helmwatch, ai-inference).
|
||||
pattern: '^[a-z0-9][a-z0-9-]{0,62}$'
|
||||
role:
|
||||
type: string
|
||||
description: |
|
||||
One of admin | editor | viewer. The Composition
|
||||
rewrites this to the canonical ClusterRole name
|
||||
openova:application-<role>.
|
||||
enum: [admin, editor, viewer]
|
||||
namespaces:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: |
|
||||
Explicit namespace list for this grant. When
|
||||
omitted, catalyst-api resolves it from the
|
||||
Application's k8s_namespaces field before writing
|
||||
the Claim. The Composition treats an empty list
|
||||
as "no RoleBindings for this application" (the
|
||||
api MUST expand before submitting).
|
||||
vClusters:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: |
|
||||
vCluster names — rendered as additional
|
||||
namespaces using the catalyst convention
|
||||
`vcluster-<name>`.
|
||||
providerConfigRef:
|
||||
type: object
|
||||
description: |
|
||||
Override for the Composition's per-Sovereign
|
||||
provider-kubernetes ProviderConfig. Defaults to
|
||||
`sovereign-<sovereignRef>` (resolved via Composition
|
||||
patch). Most callers leave this unset.
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
status:
|
||||
type: object
|
||||
properties:
|
||||
conditions:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
type: { type: string }
|
||||
status: { type: string }
|
||||
reason: { type: string }
|
||||
message: { type: string }
|
||||
lastTransitionTime: { type: string, format: date-time }
|
||||
rolebindingsCreated:
|
||||
type: integer
|
||||
description: |
|
||||
Count of RoleBinding Objects this composite owns.
|
||||
Surfaced to the catalyst-api for the user-access editor
|
||||
UI's "<n> grants active" badge.
|
||||
providerConfigRef:
|
||||
type: string
|
||||
description: |
|
||||
Resolved ProviderConfig name — useful for ops debugging
|
||||
when a grant fails to land.
|
||||
additionalPrinterColumns:
|
||||
- name: SOVEREIGN
|
||||
type: string
|
||||
jsonPath: .spec.sovereignRef
|
||||
- name: USER
|
||||
type: string
|
||||
jsonPath: .spec.user.keycloakSubject
|
||||
- name: GRANTS
|
||||
type: integer
|
||||
jsonPath: .status.rolebindingsCreated
|
||||
- name: AGE
|
||||
type: date
|
||||
jsonPath: .metadata.creationTimestamp
|
||||
{{- end }}
|
||||
@ -3,9 +3,10 @@
|
||||
#
|
||||
# This is the chart-level lint+template+kubectl-dry-run pass that runs
|
||||
# against every render of bp-crossplane's templates/xrds + templates/compositions
|
||||
# directory tree. The 6 XRDs and 6 Compositions composed here back the
|
||||
# directory tree. The 7 XRDs and 7 Compositions composed here back the
|
||||
# catalyst-api Day-2 CRUD endpoints (RegionClaim, ClusterClaim,
|
||||
# NodePoolClaim, LoadBalancerClaim, PeeringClaim, NodeActionClaim).
|
||||
# NodePoolClaim, LoadBalancerClaim, PeeringClaim, NodeActionClaim) plus
|
||||
# the Sovereign IAM access plane (UserAccess — issue #322).
|
||||
#
|
||||
# Verifies, in order:
|
||||
# 1. `helm template` renders without error (no Go-template breakage).
|
||||
@ -53,23 +54,31 @@ helm template smoke-cp . > "$TMP/render.yaml" 2> "$TMP/render.err" || {
|
||||
}
|
||||
echo " PASS"
|
||||
|
||||
echo "[composition-validate] Case 2: render contains 6 XRDs"
|
||||
echo "[composition-validate] Case 2: render contains 7 XRDs"
|
||||
XRD_COUNT="$(grep -c '^kind: CompositeResourceDefinition$' "$TMP/render.yaml" || true)"
|
||||
if [ "$XRD_COUNT" -ne 6 ]; then
|
||||
echo "FAIL: expected 6 XRDs, found $XRD_COUNT" >&2
|
||||
if [ "$XRD_COUNT" -ne 7 ]; then
|
||||
echo "FAIL: expected 7 XRDs, found $XRD_COUNT" >&2
|
||||
grep -E '^(kind| name): ' "$TMP/render.yaml" | head -40 >&2
|
||||
exit 1
|
||||
fi
|
||||
echo " PASS ($XRD_COUNT XRDs)"
|
||||
|
||||
echo "[composition-validate] Case 3: render contains ≥ 6 Compositions"
|
||||
echo "[composition-validate] Case 3: render contains ≥ 7 Compositions"
|
||||
COMPOSITION_COUNT="$(grep -c '^kind: Composition$' "$TMP/render.yaml" || true)"
|
||||
if [ "$COMPOSITION_COUNT" -lt 6 ]; then
|
||||
echo "FAIL: expected ≥ 6 Compositions, found $COMPOSITION_COUNT" >&2
|
||||
if [ "$COMPOSITION_COUNT" -lt 7 ]; then
|
||||
echo "FAIL: expected ≥ 7 Compositions, found $COMPOSITION_COUNT" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo " PASS ($COMPOSITION_COUNT Compositions)"
|
||||
|
||||
echo "[composition-validate] Case 3b: render contains 3 ClusterRoles (Sovereign IAM)"
|
||||
CLUSTERROLE_COUNT="$(grep -c '^kind: ClusterRole$' "$TMP/render.yaml" || true)"
|
||||
if [ "$CLUSTERROLE_COUNT" -ne 3 ]; then
|
||||
echo "FAIL: expected 3 ClusterRoles (openova:application-{admin,editor,viewer}), found $CLUSTERROLE_COUNT" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo " PASS ($CLUSTERROLE_COUNT ClusterRoles)"
|
||||
|
||||
echo "[composition-validate] Case 4: every expected claim kind is present"
|
||||
EXPECTED_KINDS=(
|
||||
RegionClaim
|
||||
@ -78,6 +87,7 @@ EXPECTED_KINDS=(
|
||||
LoadBalancerClaim
|
||||
PeeringClaim
|
||||
NodeActionClaim
|
||||
UserAccess
|
||||
)
|
||||
for kind in "${EXPECTED_KINDS[@]}"; do
|
||||
if ! grep -q "kind: $kind$" "$TMP/render.yaml"; then
|
||||
@ -85,7 +95,7 @@ for kind in "${EXPECTED_KINDS[@]}"; do
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
echo " PASS (all 6 claim kinds present)"
|
||||
echo " PASS (all ${#EXPECTED_KINDS[@]} claim kinds present)"
|
||||
|
||||
echo "[composition-validate] Case 5: every rendered document is valid YAML"
|
||||
# We can't run `kubectl apply --dry-run=client` without an API server
|
||||
|
||||
18
platform/crossplane-claims/chart/tests/fixtures/useraccess-sample.yaml
vendored
Normal file
18
platform/crossplane-claims/chart/tests/fixtures/useraccess-sample.yaml
vendored
Normal file
@ -0,0 +1,18 @@
|
||||
# UserAccess sample fixture — single-grant shape (post catalyst-api expansion).
|
||||
# Multi-app / multi-namespace UserAccess Claims authored by the api are
|
||||
# expanded into N of these single-grant claims before submitting.
|
||||
apiVersion: access.openova.io/v1alpha1
|
||||
kind: UserAccess
|
||||
metadata:
|
||||
name: alice-helmwatch-omantel-prod
|
||||
spec:
|
||||
user:
|
||||
keycloakSubject: alice
|
||||
keycloakGroups:
|
||||
- sovereign-ops
|
||||
sovereignRef: omantel
|
||||
applications:
|
||||
- app: helmwatch
|
||||
role: editor
|
||||
namespaces:
|
||||
- helmwatch-prod
|
||||
@ -1,9 +1,18 @@
|
||||
# bp-crossplane-claims has no operator-tunable values — the XRDs and
|
||||
# Compositions it ships are static manifests with no Go-template
|
||||
# substitutions. This file exists so `helm template` and the CI smoke
|
||||
# render have an explicit defaults document; future per-Sovereign
|
||||
# overrides (e.g. defaultCompositionRef swaps for a non-Hetzner cloud)
|
||||
# would land here.
|
||||
# bp-crossplane-claims has no operator-tunable values for the
|
||||
# core compose.openova.io/v1alpha1 family — those XRDs and Compositions
|
||||
# are static manifests with no Go-template substitutions. This file
|
||||
# exists so `helm template` and the CI smoke render have an explicit
|
||||
# defaults document; future per-Sovereign overrides (e.g.
|
||||
# defaultCompositionRef swaps for a non-Hetzner cloud) would land here.
|
||||
|
||||
catalystBlueprint:
|
||||
upstream: null
|
||||
|
||||
# Sovereign IAM access plane (epic #320). Renders the
|
||||
# access.openova.io/v1alpha1 XUserAccess XRD, its Composition, and the
|
||||
# three canonical openova:application-{admin,editor,viewer} ClusterRoles
|
||||
# that the Composition's RoleBindings reference. Default-on; toggle-off
|
||||
# is reserved for the rare case where a Sovereign uses an external IAM
|
||||
# stack and does not consume the Catalyst access plane.
|
||||
userAccess:
|
||||
enabled: true
|
||||
|
||||
Loading…
Reference in New Issue
Block a user