Set up Controller Environments

This guide helps you set up a local Kubernetes environment for testing OCM controller-based deployments. You’ll install the OCM Controllers, an orchestrator (kro or Crossplane), and a deployer (FluxCD or ArgoCD) to enable GitOps workflows with OCM component versions.

Not all components are always required

Depending on your use case, you may not need the full setup. For example, if you’re only deploying a raw k8s deployment from an ocm resource, you may be able to skip the orchestrator and a deployer like FluxCD or ArgoCD.

Check the prerequisites of the tutorial or how-to you’re following to see what’s actually needed. This guide installs everything so you’re covered for any scenario.

You’ll end up with

  • A local or remote Kubernetes cluster with OCM Controllers, an orchestrator (kro or Crossplane), and a deployer like FluxCD or ArgoCD installed

Estimated time

~15 minutes

Prerequisites

Setup Workflow

  1. Create a Local Kubernetes Cluster

    Skip this step if you’re using a remote Kubernetes cluster.

    Create a local kind cluster:

    kind create cluster
    You should see this output
    Creating cluster "kind" ...
     βœ“ Ensuring node image (kindest/node:v1.35.0) πŸ–Ό
     βœ“ Preparing nodes πŸ“¦
     βœ“ Writing configuration πŸ“œ
     βœ“ Starting control-plane πŸ•ΉοΈ
     βœ“ Installing CNI 
     βœ“ Installing StorageClass πŸ’Ύ
    Set kubectl context to "kind-kind"
    You can now use your cluster with:
    
    kubectl cluster-info --context kind-kind
    Have a nice day! πŸ‘‹

    Verify the cluster is running:

    kubectl cluster-info
    You should see this output
    Kubernetes control plane is running at https://127.0.0.1:53348
    CoreDNS is running at https://127.0.0.1:53348/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy
    
    To further debug and diagnose cluster problems, use 'kubectl cluster-info dump'
  2. Install the OCM Controllers

    Use Helm to install the OCM controllers:

    helm install ocm-k8s-toolkit "oci://ghcr.io/open-component-model/kubernetes/controller/chart" \
      --namespace ocm-k8s-toolkit-system \
      --create-namespace
    Resource names follow the release name

    The release name ocm-k8s-toolkit used above gives the controller’s resources predictable names, such as the service account ocm-k8s-toolkit-controller-manager. If you install under a different release name, or via a GitOps tool such as Flux that alters the effective release name, add --set fullnameOverride=ocm-k8s-toolkit to keep these names stable. This matters when you configure custom RBAC, which binds to the service account by name.

    You should see this output
    Pulled: ghcr.io/open-component-model/kubernetes/controller/chart:0.4.0
    Digest: sha256:eac0dc587a1d288f36ef1961bb69f0ffb2791e0153f86d1fdbe54ae2f36f1194
    NAME: ocm-k8s-toolkit
    LAST DEPLOYED: Tue Apr 28 17:42:51 2026
    NAMESPACE: ocm-k8s-toolkit-system
    STATUS: deployed
    REVISION: 1
    DESCRIPTION: Install complete
    TEST SUITE: None

    Verify the OCM controller is running:

    kubectl get pods -n ocm-k8s-toolkit-system
    You should see this output
    NAME                                                  READY   STATUS    RESTARTS   AGE
    ocm-k8s-toolkit-controller-manager-79b7975755-vxtqt   1/1     Running   0          59s
  3. Install an Orchestrator

    An orchestrator reconciles the deployment graph that an OCM component carries. Install the one the tutorial or how-to you’re following asks for. kro reconciles a ResourceGraphDefinition, generates a CRD from it, and reconciles the instances of that CRD. Crossplane reconciles a CompositeResourceDefinition (XRD) plus a Composition, and reconciles the composite resources the XRD defines.

    Install kro following the official installation guide. The easiest way is via Helm:

    helm install kro oci://registry.k8s.io/kro/charts/kro \
      --namespace kro-system \
      --create-namespace \
      --set rbac.mode=aggregation
    kro access control

    rbac.mode=aggregation limits kro to its own resources and to the resources that a labeled ClusterRole allows. See Access Control in the kro documentation. kro’s default mode, unrestricted, is not recommended for production. The roles below are not production-hardened either: they grant all verbs so the guides work. For production, limit them to what your RGDs create.

    You should see this output
    Pulled: registry.k8s.io/kro/charts/kro:0.8.5
    Digest: sha256:c9a9dc0133f43a25711f4bdbce1eeb4b6448015958f901c6fad61a049e54415e
    NAME: kro
    LAST DEPLOYED: Wed Feb 25 12:02:15 2026
    NAMESPACE: kro-system
    STATUS: deployed
    REVISION: 1
    DESCRIPTION: Install complete
    TEST SUITE: None

    Verify kro is running:

    kubectl get pods -n kro-system
    You should see this output
    NAME                   READY   STATUS    RESTARTS   AGE
    kro-5644d5759f-82nsx   1/1     Running   0          2m22s

    Allow kro to manage OCM resources:

    kubectl apply -f - << 'EOF'
    apiVersion: rbac.authorization.k8s.io/v1
    kind: ClusterRole
    metadata:
      name: kro:controller:ocm
      labels:
        # kro picks up every ClusterRole with this label, see https://kro.run/docs/advanced/access-control
        rbac.kro.run/aggregate-to-controller: "true"
    rules:
      - apiGroups:
          - delivery.ocm.software
        resources:
          - repositories
          - components
          - resources
          - deployers
        verbs:
          - create
          - delete
          - get
          - list
          - patch
          - update
          - watch
    EOF

    Install Crossplane v2 core via Helm, following the official installation guide. The 2.x range installs the latest Crossplane v2 release, which the bootstrap tutorial requires:

    helm install crossplane crossplane \
      --repo https://charts.crossplane.io/stable \
      --version '2.x' \
      --namespace crossplane-system \
      --create-namespace
    Security consideration

    This installs Crossplane with cluster-wide permissions, which is suitable for local development but not recommended for production environments. See the Crossplane documentation for guidance on hardening RBAC.

    Verify Crossplane is running:

    kubectl get pods -n crossplane-system

    Crossplane reconciles graphs through composition functions. For OCM bootstrap tutorials, install the patch-and-transform function, which the Composition runs as its pipeline step:

    kubectl apply -f - << 'EOF'
    apiVersion: pkg.crossplane.io/v1
    kind: Function
    metadata:
      name: function-patch-and-transform
    spec:
      package: xpkg.crossplane.io/crossplane-contrib/function-patch-and-transform:v0.11.0
    EOF
    RBAC on a hardened cluster

    Crossplane composes the OCM Resource and the deployer objects (Flux or Argo CD), so on a hardened cluster you must grant Crossplane’s service account access to those composed kinds. See Configure Custom RBAC for Deployers.

  4. Install a Deployer

    The following examples of Flux and Argo CD are demonstrating how they can be used as deployers. In theory, you could use any other deployer that is able to apply a deployable resource to a Kubernetes cluster.

    Choose the one you’re already using or prefer.

    Install the controllers using the Flux CLI:

    flux install
    You should see this output
    ✚ generating manifests
    βœ” manifests build completed
    β–Ί installing components in flux-system namespace
    CustomResourceDefinition/alerts.notification.toolkit.fluxcd.io created
    CustomResourceDefinition/buckets.source.toolkit.fluxcd.io created
    CustomResourceDefinition/externalartifacts.source.toolkit.fluxcd.io created
    CustomResourceDefinition/gitrepositories.source.toolkit.fluxcd.io created
    CustomResourceDefinition/helmcharts.source.toolkit.fluxcd.io created
    CustomResourceDefinition/helmreleases.helm.toolkit.fluxcd.io created
    CustomResourceDefinition/helmrepositories.source.toolkit.fluxcd.io created
    CustomResourceDefinition/kustomizations.kustomize.toolkit.fluxcd.io created
    CustomResourceDefinition/ocirepositories.source.toolkit.fluxcd.io created
    CustomResourceDefinition/providers.notification.toolkit.fluxcd.io created
    CustomResourceDefinition/receivers.notification.toolkit.fluxcd.io created
    Namespace/flux-system created
    ClusterRole/crd-controller-flux-system created
    ClusterRole/flux-edit-flux-system created
    ClusterRole/flux-view-flux-system created
    ClusterRoleBinding/cluster-reconciler-flux-system created
    ClusterRoleBinding/crd-controller-flux-system created
    ResourceQuota/flux-system/critical-pods-flux-system created
    ServiceAccount/flux-system/helm-controller created
    ServiceAccount/flux-system/kustomize-controller created
    ServiceAccount/flux-system/notification-controller created
    ServiceAccount/flux-system/source-controller created
    Service/flux-system/notification-controller created
    Service/flux-system/source-controller created
    Service/flux-system/webhook-receiver created
    Deployment/flux-system/helm-controller created
    Deployment/flux-system/kustomize-controller created
    Deployment/flux-system/notification-controller created
    Deployment/flux-system/source-controller created
    NetworkPolicy/flux-system/allow-egress created
    NetworkPolicy/flux-system/allow-scraping created
    NetworkPolicy/flux-system/allow-webhooks created
    β—Ž verifying installation
    βœ” helm-controller: deployment ready
    βœ” kustomize-controller: deployment ready
    βœ” notification-controller: deployment ready
    βœ” source-controller: deployment ready
    βœ” install finished

    Verify Flux is running:

    kubectl get pods -n flux-system
    You should see this output
    NAME                                         READY   STATUS      RESTARTS        AGE
    helm-controller-b6767d66-zbwws               1/1     Running     0               3h29m
    kustomize-controller-57c7ff5596-v6fvr        1/1     Running     0               3h29m
    notification-controller-58ffd586f7-pr65t     1/1     Running     0               3h29m
    source-controller-6ff87cb475-2h2lv           1/1     Running     0               3h29m

    Allow kro to manage the Flux resources that the guides use. Add more kinds when your ResourceGraphDefinitions create them:

    kubectl apply -f - << 'EOF'
    apiVersion: rbac.authorization.k8s.io/v1
    kind: ClusterRole
    metadata:
      name: kro:controller:flux
      labels:
        # kro picks up every ClusterRole with this label, see https://kro.run/docs/advanced/access-control
        rbac.kro.run/aggregate-to-controller: "true"
    rules:
      - apiGroups:
          - source.toolkit.fluxcd.io
        resources:
          - ocirepositories
        verbs:
          - create
          - delete
          - get
          - list
          - patch
          - update
          - watch
      - apiGroups:
          - helm.toolkit.fluxcd.io
        resources:
          - helmreleases
        verbs:
          - create
          - delete
          - get
          - list
          - patch
          - update
          - watch
    EOF

    Install Argo CD via Helm Chart:

    helm repo add argo https://argoproj.github.io/argo-helm
    # ... "argo" has been added to your repositories
    
    helm repo update argo
    
    helm upgrade --install argocd argo/argo-cd \
      --namespace argocd \
      --create-namespace \
      --wait \
      --timeout 5m
    # ...

    Wait for all pods to become ready:

    kubectl wait --for=condition=Ready pods --all -n argocd \
      --field-selector='status.phase!=Succeeded' --timeout=120s
    You should see this output
    pod/argocd-application-controller-0 condition met
    pod/argocd-applicationset-controller-68fd97ccb6-nkcbg condition met
    pod/argocd-dex-server-99ff57675-9qk2l condition met
    pod/argocd-notifications-controller-8596549fb6-bldjz condition met
    pod/argocd-redis-6f6867546c-vs6c6 condition met
    pod/argocd-repo-server-59444f4bbb-gbzxn condition met
    pod/argocd-server-765575f778-j8krk condition met

    Verify Argo CD is running:

    kubectl get pods -n argocd
    You should see this output
    NAME                                                READY   STATUS    RESTARTS   AGE
    argocd-application-controller-0                     1/1     Running   0          42s
    argocd-applicationset-controller-68fd97ccb6-nkcbg   1/1     Running   0          43s
    argocd-dex-server-99ff57675-9qk2l                   1/1     Running   0          43s
    argocd-notifications-controller-8596549fb6-bldjz    1/1     Running   0          43s
    argocd-redis-6f6867546c-vs6c6                       1/1     Running   0          42s
    argocd-repo-server-59444f4bbb-gbzxn                 1/1     Running   0          42s
    argocd-server-765575f778-j8krk                      1/1     Running   0          42s

    Allow kro to manage the Argo CD resources that the guides use. Add more kinds when your ResourceGraphDefinitions create them:

    kubectl apply -f - << 'EOF'
    apiVersion: rbac.authorization.k8s.io/v1
    kind: ClusterRole
    metadata:
      name: kro:controller:argocd
      labels:
        # kro picks up every ClusterRole with this label, see https://kro.run/docs/advanced/access-control
        rbac.kro.run/aggregate-to-controller: "true"
    rules:
      - apiGroups:
          - argoproj.io
        resources:
          - applications
        verbs:
          - create
          - delete
          - get
          - list
          - patch
          - update
          - watch
    EOF
    OCI registry credentials

    To deploy Helm charts from a private OCI registry (e.g. ghcr.io), create an Argo CD repository Secret with enableOCI: "true":

    apiVersion: v1
    kind: Secret
    metadata:
      name: ghcr-helm
      namespace: argocd
      labels:
        argocd.argoproj.io/secret-type: repository
    stringData:
      name: ghcr-helm
      type: oci
      url: oci://<path-to-helm-chart>
      enableOCI: "true"
      username: <your-username>
      password: <your-token>

    Public OCI registries require no configuration. Read more about ArgoCD OCI Support here.

  5. Verify Complete Setup

    Check all components are running:

    kubectl get pods --all-namespaces | grep -E '(kro-system|crossplane-system|flux-system|argocd|ocm-k8s-toolkit-system)'
    You should see this output

    Depending on what deployer you installed the output could differ:

    NAMESPACE                NAME                                                 READY    STATUS             RESTARTS        AGE
    argocd-application-controller-0                                                1/1     Running            0               24m
    argocd-applicationset-controller-85f58f44f4-g85xv                              1/1     Running            0               24m
    argocd-dex-server-69884b6f8b-vf5s9                                             1/1     Running            0               24m
    argocd-notifications-controller-8669567fb-47fcj                                1/1     Running            0               24m
    argocd-redis-5d9668fdff-v2pbp                                                  1/1     Running            0               24m
    argocd-repo-server-95465d997-mbmcp                                             1/1     Running            0               24m
    argocd-server-767b9d54cf-q7bbz                                                 1/1     Running            0               24m
    crossplane-system        crossplane-6b8c9d7f54-nq8rk                           1/1     Running            0               12m
    crossplane-system        crossplane-rbac-manager-7d9f6c84b5-2xk7p              1/1     Running            0               12m
    crossplane-system        function-patch-and-transform-9d4f2a1c7b-5f8d7c9b64-ltzqd  1/1   Running            0               10m
    flux-system              helm-controller-b6767d66-zbwws                        1/1     Running            0               3h39m
    flux-system              kustomize-controller-57c7ff5596-v6fvr                 1/1     Running            0               3h39m
    flux-system              notification-controller-58ffd586f7-pr65t              1/1     Running            0               3h39m
    flux-system              source-controller-6ff87cb475-2h2lv                    1/1     Running            0               3h39m
    kro-system               kro-86d5b5b5bd-6gmvr                                  1/1     Running            0               3h38m
    ocm-k8s-toolkit-system   ocm-k8s-toolkit-controller-manager-788f58d4bd-ntbx8   1/1     Running            0               57s

Registry Access

The OCM Controllers need access to an OCI registry to fetch component versions.

Tip

We recommend using a publicly accessible registry like ghcr.io. Using a local registry requires additional configuration to ensure it’s accessible both from your CLI and from within the cluster.

For private registries, you’ll need to configure credentials. See Configure Credentials for Private Registries for details.

Cleanup

To remove the local kind cluster after testing, run the following command. If you plan to continue with the next tutorial steps, you can keep the cluster.

kind delete cluster
You should see this output
Deleting cluster "kind" ...
Deleted nodes: ["kind-control-plane"]

Next Steps