This guide covers development on the OCM Kubernetes controller in kubernetes/controller/. For the general
contribution process, see the central contributing guide.
The controller reconciles OCM component versions into Kubernetes clusters. For the full architecture, reconciliation chain, and concept overview, see the Kubernetes Controllers page on the project website.
From a contributor's perspective, the controller consists of four reconcilers that form a pipeline - each custom resource
depends on the previous one becoming Ready:
- Repository validates that an OCM repository is reachable at the configured interval.
- Component resolves a component version using semver constraints and optionally verifies its signature.
- Resource resolves a specific resource from the component and publishes access metadata in its status. This metadata is what downstream consumers (like the Deployer) use to locate and fetch the resource content.
- Deployer downloads resource content and applies it to the cluster (the resource must contain valid Kubernetes manifests). It uses the ApplySet implementation for server-side apply and pruning.
The codebase uses controller-runtime and is deployed via a Helm chart.
kubernetes/controller/
├── cmd/main.go # Manager bootstrap, plugin registration
├── api/v1alpha1/ # CRD type definitions (kubebuilder markers)
├── internal/
│ ├── controller/ # Reconcilers (one per CRD above)
│ ├── ocm/ # Base reconciler, shared OCM utilities
│ └── resolution/ # Component resolution worker pool + cache
├── chart/ # Helm chart
└── hack/ # Generation scripts (CRD/RBAC)
All reconcilers embed a shared base reconciler from internal/ocm/reconciler.go that provides ctrl.Client,
runtime.Scheme, and record.EventRecorder.
The controller uses the same OCM plugin system as the CLI. At
startup, cmd/main.go registers plugins for OCI component repositories, RSA signing, OCI credentials, resource
fetching, digest processing, and blob transformation. These plugins handle all communication with OCM repositories.
Component descriptor resolution runs through a worker pool with an in-memory LRU cache
(internal/resolution/workerpool/). The worker pool is added as a controller-runtime Runnable so the manager handles
its lifecycle.
CRDs and RBAC rules are generated from kubebuilder markers in the Go
source files under api/v1alpha1/. The generation pipeline has three steps:
task manifests- Runscontroller-gento produce raw CRD YAML intoconfig/crd/basesand raw RBAC intobin/gen/rbac/.task generate- Generates Go deepcopy andruntime.Objectimplementations from type definitions.task helm/generate- Wraps the raw CRDs and RBAC from step 1 into Helm chart templates (adding conditionals, cert-manager annotations, etc.) via scripts inhack/. The output goes intochart/templates/crd/andchart/templates/rbac/.
To validate that everything is in sync:
task kubernetes/controller:helm/validateThis regenerates all artifacts, lints the chart, renders templates, and checks that the working tree is clean. CI
enforces this - if you modify api/v1alpha1/ types or RBAC markers and forget to regenerate, CI will fail.
In addition to the general prerequisites, controller development requires:
- Docker - for building container images and running Kind clusters
- Helm - for chart linting, templating, and local installs
- kubectl - for interacting with test clusters
- FluxCD CLI - required for E2E tests
- Kind - required for E2E tests
- kro - required for E2E tests
All other tools (controller-gen, envtest, helm-docs, yq) are installed automatically by the Taskfile into
kubernetes/controller/bin/. Their versions are pinned in kubernetes/controller/.env.
# Build the controller binary
task kubernetes/controller:build
# Build the Docker image (host architecture)
task kubernetes/controller:docker-build
# Run the controller locally (connects to your current kubeconfig context)
task kubernetes/controller:runUnit tests run against a local Kubernetes API server provided by envtest. The Taskfile handles downloading the correct envtest binaries.
task kubernetes/controller:testE2E tests run against a real Kubernetes cluster using Kind:
# Set up a local Kind cluster with the controller loaded
task kubernetes/controller:test/e2e/setup/local
# Run the E2E test suite
task kubernetes/controller:test/e2eThe E2E setup creates a Kind cluster, installs FluxCD and kro, loads the locally built controller image, and installs the Helm chart.
The controller uses Ginkgo v2 with Gomega matchers.
This is different from the Go bindings and CLI, which use testify. Each controller package has a suite_test.go that
bootstraps the envtest environment - see any of the internal/controller/*/suite_test.go files for the pattern.
Use -ginkgo.focus to run specific specs (not -run, which only matches the top-level test function).
For controller-specific testing patterns (reconciler structure, condition handling, resource references), see the controller idioms section in the coding patterns guide.
The chart lives in chart/ and is the primary deployment mechanism.
# Lint the chart
task kubernetes/controller:helm/lint
# Render templates locally
task kubernetes/controller:helm/template
# Generate values JSON schema and chart docs
task kubernetes/controller:helm/schema
task kubernetes/controller:helm/docs
# Install into / uninstall from current cluster
task kubernetes/controller:helm/install
task kubernetes/controller:helm/uninstallA typical change to the controller follows this flow:
- Modify API types in
api/v1alpha1/or controller logic ininternal/controller/. - Run
task kubernetes/controller:testto verify unit tests pass (this also regenerates code and manifests). - Run
task kubernetes/controller:helm/validateto ensure CRDs, RBAC, and the chart are consistent. - Set up a local Kind cluster with
task kubernetes/controller:test/e2e/setup/localand run E2E tests withtask kubernetes/controller:test/e2e.