A kcp-native composition controller: one declarative blueprint → a bindable API → cross-workspace resource graphs. No Go controller per offering.
Status: alpha · License: Apache-2.0 · Go 1.26 · Runtime: kcp · Image: ghcr.io/opendefense/krop-controller
krop-controller lets a provider author one declarative
ResourceGraphDefinition (RGD) blueprint. krop publishes it as a bindable kcp
APIExport, and for every consumer instance it materializes a graph of child
resources split across the consumer and provider workspaces — with CEL
dependencies allowed to cross between them. The provider writes no Go controller
per offering: the blueprint is the offering. krop reuses
kro's graph builder, CEL engine, and
client-free runtime as a library, and replaces kro's single-cluster machinery
with kcp-native publication, dual-target apply, and cross-workspace garbage
collection.
flowchart LR
BP["Provider authors ONE<br/>ResourceGraphDefinition"] -->|Registrar compiles + publishes| EX["APIExport<br/>(+ auto-derived permissionClaims)"]
EX -->|consumer binds + accepts claims| BIND["APIBinding"]
BIND -->|consumer creates| INST["Instance"]
INST -->|Engine materializes graph| CHILD["consumer-target children<br/>(consumer ws, via vw + claims)"]
INST -->|Engine materializes graph| PROV["provider-target children<br/>(provider ws, controller identity)"]
PROV -.->|cross-target CEL: status feeds| CHILD
A single provider process watches blueprints, publishes each as an APIExport,
and fans in all bound consumers behind one virtual-workspace manager per
blueprint. See docs/architecture.md for the full picture,
workspace topology, and Mermaid flows.
The fastest way to see krop work end-to-end, with zero manual setup:
make test-e2eThis spins up a kind cluster, installs cert-manager + kcp-operator, provisions a
real kcp (RootShard + FrontProxy + etcd), mints the controller's
workspace-scoped kubeconfig, deploys krop via its own Helm chart as a pod, then
runs the full provider→consumer flow (publish a blueprint, bind a consumer, create
an instance, watch children materialize across workspaces, GC) — 5/5 specs
including a negative least-privilege case. Set E2E_SKIP_CLEANUP=1 to keep the
cluster and poke around.
For the real hands-on provider/consumer walkthrough (deploy the chart, mint the identity, author a blueprint, bind, create an instance), follow docs/getting-started.md.
A blueprint is just a kro RGD served under krop's group, plus one routing
target per resource. Abbreviated (full example:
config/kcp/examples/blueprint-hosteddatabase.yaml):
apiVersion: krop.opendefense.cloud/v1alpha1
kind: ResourceGraphDefinition
metadata:
name: hosteddatabase
spec:
schema: # the generated instance API
apiVersion: v1alpha1
kind: HostedDatabase
group: krop.opendefense.cloud
spec:
name: string
engine: string | default="postgres"
status:
endpoint: ${connection.data.endpoint}
vpcID: ${vpc.status.vpcID}
resources:
- id: vpc # CONSUMER-target read-only externalRef
target: consumer
externalRef:
apiVersion: ec2.services.k8s.aws/v1alpha1
kind: VPC
metadata:
name: ${schema.spec.name}-vpc
- id: db # HOST-target child (physical host cluster)
target: host
template:
apiVersion: apps/v1
kind: Deployment
metadata:
# prefix the child name with the consumer's kcp logical-cluster name
# (globally unique + immutable) so two tenants never collide on the host.
name: ${schema.metadata.annotations["krop.opendefense.cloud/consumer-cluster"]}-${schema.spec.name}
namespace: databases
spec:
replicas: 1
selector: { matchLabels: { app: ${schema.spec.name} } }
template:
metadata: { labels: { app: ${schema.spec.name} } }
spec:
containers:
- name: db
image: ${schema.spec.engine}:16
env:
- name: VPC_ID
value: ${vpc.status.vpcID} # consumer read → host write
- id: connection # CONSUMER-target child (default)
target: consumer
template:
apiVersion: v1
kind: ConfigMap
metadata:
name: ${schema.spec.name}-connection
data:
endpoint: ${db.metadata.name}.databases.svc.cluster.local # host → consumer CEL
vpcID: ${vpc.status.vpcID}New to blueprints? Start with docs/guides/writing-your-first-blueprint.md (a simpler single-child tutorial), then read the full authoring reference.
- Declarative blueprints. Author one kro
ResourceGraphDefinition; krop serves the generated instance kind. No per-offering Go controller. - Three-target materialization. Each resource is routed by a single
per-resource
targetfield (defaultconsumer):consumerchildren land in the tenant workspace (through the APIExport virtual workspace + the consumer's accepted permissionClaims);providerchildren land in the provider workspace (written with the controller's own kcp identity);hostchildren land in the physical host cluster the controller runs in. - External references (read-only). A resource can be an
externalRefinstead of atemplate— an existing object krop reads but never creates or GCs (single bymetadata.name, or a collection bymetadata.selector). Routed bytargetlike any resource, its observedstatus/datafunnels into other resources via${id.status.x}CEL — e.g. read a VPC in the consumer plane and feed${vpc.status.vpcId}into a VM written to the host cluster. - Cross-target CEL dependencies. A consumer child can read a provider child's
live
status.*via${...}CEL; it pends until that status is set, then materializes. - Consumer workspace info in CEL. The reconciler stamps the consumer's kcp
logical-cluster name (globally unique + immutable) onto each instance as the
krop.opendefense.cloud/consumer-clusterannotation, so a blueprint can derive collision-free host/provider child names by prefixing with${schema.metadata.annotations["krop.opendefense.cloud/consumer-cluster"]}. - Automatic APIExport publication. The Registrar compiles each blueprint,
mints an
APIResourceSchema, auto-derives thepermissionClaimsfrom the consumer-target node GVRs, and upserts theAPIExport. - Dynamic per-blueprint serving. A Supervisor runs one multicluster manager per published APIExport, fanning in every bound consumer; it restarts on spec change and self-heals a crashed manager.
- Cross-workspace GC + orphan sweep. Finalizer-driven, label-based garbage collection across both workspaces, plus a liveness-record orphan sweep for the mid-life-unbind case owner references cannot cover.
- Least-privilege by construction. The chart renders no host-cluster ClusterRole/Role — only a ServiceAccount + Deployment. All authority lives in kcp RBAC + accepted claims.
- HA & production-ready. Leader election, Helm chart, health/metrics, and a full CI/CD pipeline.
| Doc | What it covers |
|---|---|
| Getting Started | The hands-on provider+consumer walkthrough: deploy, publish, bind, create an instance. |
| Guides | Task-oriented how-tos (below). |
| — Writing your first blueprint | A from-scratch tutorial building a minimal single-child blueprint. |
| — Cross-target dependencies | The provider-status → consumer-child CEL recipe (pend-until-ready). |
| — Deploying in production | Identity, RBAC, helm install with HA + metrics. |
| — Troubleshooting | Problem → cause → fix, with concrete kubectl checks. |
| Blueprint authoring | The RGD reference: schema, resources, CEL, target routing, claims, naming, pruning. |
| Architecture | How krop is built and why; workspace topology, components, key flows, Mermaid diagrams. |
| Decisions (ADRs) | The 12 significant design decisions. |
| Permissions | The least-privilege authorization model and checklist. |
| Operations | Production ops: flags, RBAC, observability, GC/sweep, upgrades, troubleshooting. |
helm install krop charts/krop-controller \
--namespace krop-system --create-namespace \
--set image.tag=<your-tag> \
--set kcp.kubeconfigSecret.name=krop-kubeconfigThe chart does not template the kcp kubeconfig Secret — you provide it out-of-band, and separately grant the controller's kcp identity least-privilege RBAC. See getting-started.md and permissions.md for those prerequisites, and the production deploy guide for a concise recipe.
Image: ghcr.io/opendefense/krop-controller. Build artifacts locally:
make build # build the controller binary
make docker-build # build the container image
make helm-package # package the Helm chartgit clone https://github.com/opendefensecloud/krop-controller && cd krop-controller
make build # build the binary
make test # unit + envtest (downloads the pinned kcp test binary via the Makefile)
make test-e2e # full stack: kind + kcp-operator + helm-deployed pod
make lint # license headers, shellcheck, golangci-lintmake testruns unit tests plus an envtest tier against a real in-process kcp (the Makefile downloads the pinned kcp test binary —KCP_VERSION, currently0.30.0— intobin/).make test-e2erequires Docker (kind) and takes several minutes.- A Nix flake devshell provides the pinned toolchain (Go 1.26.4 via
dev-kit): runnix develop(or usedirenvwith the checked-in.envrc).
Module: go.opendefense.cloud/krop-controller (Go 1.26).
- Conventional commits are enforced in CI (
feat,fix,docs,chore,refactor,test,ci,perf,revert). - Run
make lintandmake testbefore opening a PR. - CI covers Go build/test/lint, Helm lint, conventional-commit checks, codegen drift, container build, and OSV vulnerability scanning.
Apache-2.0.