Capi-2-Argo Cluster Operator (CACO) converts ClusterAPI cluster credentials into ArgoCD cluster definitions and keeps them synchronized. It bridges the automation gap for teams that use ClusterAPI to provision Kubernetes clusters and ArgoCD to manage workloads on them.
ClusterAPI provides declarative APIs for provisioning, upgrading, and operating multiple Kubernetes clusters. ArgoCD is a GitOps continuous delivery tool that deploys applications to target Kubernetes clusters.
A typical pipeline looks like this:
- Git holds Kubernetes cluster definitions as CRDs
- ArgoCD watches these resources from Git
- ArgoCD deploys definitions on a Management Cluster
- ClusterAPI reconciles the definitions
- ClusterAPI provisions clusters on the cloud provider
- ClusterAPI stores provisioned cluster credentials as Kubernetes Secrets
- ❌ ArgoCD has no way to discover and authenticate to the new clusters
CACO watches for CAPI-managed kubeconfig secrets, converts them into ArgoCD-compatible cluster secrets, and keeps them in sync. This closes the loop:
- CACO detects CAPI cluster secrets
- CACO converts them to ArgoCD cluster definitions
- CACO creates/updates them in the ArgoCD namespace
- ArgoCD discovers the new clusters
- ✔️ ArgoCD deploys workloads to CAPI-provisioned clusters
CACO transforms a CAPI kubeconfig secret:
kind: Secret
apiVersion: v1
type: cluster.x-k8s.io/secret
metadata:
labels:
cluster.x-k8s.io/cluster-name: my-cluster
name: my-cluster-kubeconfig
data:
value: << base64-encoded kubeconfig >>Into an ArgoCD cluster secret:
kind: Secret
apiVersion: v1
type: Opaque
metadata:
labels:
argocd.argoproj.io/secret-type: cluster
capi-to-argocd/owned: "true"
name: cluster-my-cluster
namespace: argocd
stringData:
name: my-cluster
server: https://my-cluster.example.com:6443
config: |
{
"tlsClientConfig": {
"caData": "<base64-ca>",
"certData": "<base64-cert>",
"keyData": "<base64-key>"
}
}helm repo add capi2argo https://dntosas.github.io/capi2argo-cluster-operator/
helm repo update
helm upgrade -i capi2argo capi2argo/capi2argo-cluster-operatorSee the chart values for all available configuration options.
CACO is configured through environment variables (set via Helm values):
| Environment Variable | Helm Value | Default | Description |
|---|---|---|---|
ARGOCD_NAMESPACE |
argoCDNamespace |
argocd |
Namespace where ArgoCD cluster secrets are created |
ALLOWED_NAMESPACES |
allowedNamespaces |
"" (all) |
Comma-separated list of namespaces to watch. Empty means all namespaces |
ENABLE_GARBAGE_COLLECTION |
garbageCollectionEnabled |
false |
Delete ArgoCD secrets when the corresponding CAPI secret is deleted |
ENABLE_NAMESPACED_NAMES |
namespacedNamesEnabled |
false |
Prepend cluster namespace to ArgoCD secret names to avoid collisions |
ENABLE_AUTO_LABEL_COPY |
(via extraEnvVars) |
false |
Automatically copy all non-system labels from CAPI Cluster to ArgoCD secret |
| Flag | Default | Description |
|---|---|---|
--sync-duration |
45s |
How often to re-sync cluster secrets |
--metrics-bind-address |
:8080 |
Address for the Prometheus metrics endpoint |
--health-probe-bind-address |
:8081 |
Address for health/readiness probes |
--leader-elect |
false |
Enable leader election for HA deployments |
--debug |
false |
Enable debug logging |
--dry-run |
false |
Run without making changes |
By default CACO watches all namespaces for CAPI secrets. To limit it to specific namespaces, set a comma-separated list:
# values.yaml
allowedNamespaces: "team-a,team-b,production"Or via environment variable:
ALLOWED_NAMESPACES=team-a,team-b,productionSecrets in namespaces not in the list are ignored entirely (they don't even trigger reconciliation).
When enabled, CACO deletes the corresponding ArgoCD secret when a CAPI kubeconfig secret is deleted:
# values.yaml
garbageCollectionEnabled: trueTo exclude a specific cluster from being synced to ArgoCD, add the ignore label to the Cluster resource:
apiVersion: cluster.x-k8s.io/v1beta1
kind: Cluster
metadata:
name: my-cluster
labels:
ignore-cluster.capi-to-argocd: ""CACO can copy labels from a Cluster resource to the generated ArgoCD secret. This is useful for ArgoCD ApplicationSet generators that select clusters by label.
Add a label with the format take-along-label.capi-to-argocd.<label-key>: "":
apiVersion: cluster.x-k8s.io/v1beta1
kind: Cluster
metadata:
name: my-cluster
namespace: default
labels:
env: production
team: platform
take-along-label.capi-to-argocd.env: ""
take-along-label.capi-to-argocd.team: ""The resulting ArgoCD secret will include:
metadata:
labels:
env: production
team: platform
taken-from-cluster-label.capi-to-argocd.env: ""
taken-from-cluster-label.capi-to-argocd.team: ""As an alternative to take-along labels, enable automatic copying of all non-system labels:
ENABLE_AUTO_LABEL_COPY=trueThis copies every label from the Cluster resource to the ArgoCD secret, except:
kubernetes.io/*(system labels)cluster.x-k8s.io/*(CAPI internal labels)capi-to-argocd/*(controller internal labels)
When managing clusters across multiple namespaces, name collisions can occur (e.g., two namespaces both have a cluster named prod). Enable namespaced names to prepend the namespace:
# values.yaml
namespacedNamesEnabled: trueThis changes the ArgoCD secret name from cluster-prod to cluster-<namespace>-prod.
CACO supports Rancher-managed clusters that use Opaque secret types instead of the standard CAPI type. Opaque secrets are accepted if they carry the cluster.x-k8s.io/cluster-name label.
CACO exposes custom metrics on the /metrics endpoint:
| Metric | Type | Description |
|---|---|---|
caco_argocd_secrets_created_total |
Counter | Total ArgoCD cluster secrets created |
caco_argocd_secrets_updated_total |
Counter | Total ArgoCD cluster secrets updated |
caco_argocd_secrets_deleted_total |
Counter | Total ArgoCD cluster secrets deleted |
Enable the ServiceMonitor in the Helm chart:
metrics:
enabled: true
serviceMonitor:
enabled: true- DRY Production Pipelines - Everything as testable, version-controlled code
- Automated Credential Management - No manual steps, UI clicks, or cron scripts
- End-to-End Infrastructure Testing - Bundle cluster provisioning and workload deployment
- Dynamic Environments - Automatically register new clusters with ArgoCD as they are provisioned
- Go 1.25+
- A running Kubernetes cluster (or kind for local development)
- envtest binaries (installed automatically via
make envtest)
make fmt # Format code
make vet # Run go vet
make lint # Run golangci-lint
make test # Run tests (unit + integration via envtest)
make ci # Run fmt + vet + lint + test
make build # Build the binary (linux/amd64)
make build-darwin # Build the binary (darwin/arm64)
make run # Run the controller locally against current kubeconfig
make modsync # Run go mod tidy + vendormake docker-build-dev # Build dev Docker image
make helm-deploy-dev # Deploy to current cluster via HelmContributions are welcome! Feel free to open issues or pull requests.

