Host Cluster Controller
Component: host-cluster-controller (Kubernetes operator) · Namespace: kubeopera-system
host-cluster-controller does the work behind Cluster Management. For every HostCluster resource it validates access to a connected cluster or has a new one provisioned, installs Flux, has a complete GitOps environment generated for the cluster, and points Flux at it — until the cluster is running KubeOpera and managed from Git.
It follows the same pattern as tenant-controller: the resource's status is a state machine, and long-running work is delegated to dedicated services.
The HostCluster resource
apiVersion: kubeopera.io/v1alpha1
kind: HostCluster
metadata:
name: 7e9d3a1b-...
namespace: kubeopera-system
spec:
clusterID: "7e9d3a1b-..."
mode: connect_existing # connect_existing | provision_vanilla | provision_eks | provision_gke
provider: aws # aws | gcp | …
region: eu-north-1
connectKubeconfigSecretRef: hostcluster-7e9d3a1b-input-kubeconfig # connect_existing only
provisioning: # provision_* only
topology: vanilla_kubeadm # vanilla_kubeadm | eks | gke
controlPlaneCount: 1 # vanilla only
nodeCount: 2
instanceType: t3.medium
kubernetesVersion: "1.31.0"
credentialRef: <cloud credential ID>
gitOpsRepoURL: https://github.com/acme/gitops-iac.git
gitOpsPath: fluxcd/clusters/my-cluster
gitCredentialsSecretRef: gitops-iac-credentials
domain: my-cluster.acme.com # required: the base domain for every service's hostname
registryPullSecretRef: hostcluster-7e9d3a1b-registry-pull # optional
adminEmail: admin@acme.com # optional: create a first administrator
adminUsername: admin
status:
phase: Ready
kubeconfigSecret: hostcluster-7e9d3a1b-kubeconfig
fluxHealthy: true
kubeOperaInstalled: true
adminPasswordSecretRef: hostcluster-7e9d3a1b-admin-password
message: "Flux healthy and reconciling fluxcd/clusters/my-cluster. Administrator admin@acme.com created."
lastReconciledAt: "2026-09-26T00:35:00Z"
spec.provider is a free-form string, so new providers can be added without changing the resource definition.
The lifecycle
| Phase | What the controller does |
|---|---|
ValidatingAccess | Connects with the supplied kubeconfig and checks the API server responds. Temporary failures are retried with backoff for up to five minutes. |
Provisioning | Asks cluster-provisioner to build the cluster, then polls until it's done and fetches the kubeconfig. |
BootstrappingFlux | Installs Flux with its Helm chart, waits until healthy, and gives it Git credentials. |
GeneratingGitOpsTree | Asks gitops-scaffolder to generate and push a complete environment at spec.gitOpsPath. |
InstallingKubeOpera | Creates a Flux GitRepository and Kustomization pointing at the new environment, and waits for its apps layer to be ready. Then creates the first administrator, if requested. |
Ready | Flux is healthy, reconciling the cluster's own complete environment, and the platform's services have converged. |
Failed | The message explains what went wrong; the controller retries automatically with backoff. |
Long-running work survives restarts
Provisioning and scaffolding take minutes, but reconcile loops should be quick. So the controller starts a job in the delegate service, records the job ID as an annotation on the resource (kubeopera.io/provision-job-id, kubeopera.io/scaffold-job-id) and polls it on later reconciles. If the controller restarts, it picks up exactly where it left off.
Provisioning
- Start:
POST /api/v1/provisionsto cluster-provisioner with the resource'sspec.provisioning. - Poll:
GET /api/v1/provisions/{id}on each reconcile, reflecting progress in the resource's message. - Fetch the kubeconfig once, when the job succeeds, and store it.
- Continue to
BootstrappingFlux— from here, provisioned and connected clusters follow identical steps.
Generating the GitOps environment
gitops-scaffolder generates the environment in two passes — infrastructure first, then everything that needs Sealed Secrets once the cluster's own Sealed Secrets controller is running. See gitops-scaffolder and Setup: creating more environments.
The first administrator
When adminEmail and adminUsername are set, and once the cluster's apps layer is ready, the controller creates the platform administrator on the new cluster with a generated password. It stores the password in hostcluster-<clusterID>-admin-password on the management cluster (named in status.adminPasswordSecretRef) and does this exactly once, so an existing password is never silently replaced.
Kubeconfigs
Every cluster's kubeconfig is stored the same way:
kubectl get secret hostcluster-<cluster-id>-kubeconfig -n kubeopera-system \
-o jsonpath='{.data.kubeconfig}' | base64 -d
It's labelled kubeopera.io/host-cluster-id and kubeopera.io/managed-by=host-cluster-controller, and its server: address is kept exactly as the cluster exposes it.
Managed clusters authenticate with short-lived cloud tokens instead of static credentials: EKS kubeconfigs use the bundled /eks-token-helper, and GKE kubeconfigs use the bundled /gke-token-helper, both shipped in the controller's image.
Deletion
Deleting a HostCluster (disconnect) removes the Flux resources KubeOpera created on the target and the stored kubeconfig, and leaves the cluster's workloads running. For clusters KubeOpera provisioned, Delete in the dashboard also asks cluster-provisioner to destroy the infrastructure.
Permissions
The controller only needs access to its own resources and Secrets in kubeopera-system, so it uses a namespace-scoped Role:
rules:
- apiGroups: [kubeopera.io]
resources: [hostclusters, hostclusters/status, hostclusters/finalizers]
verbs: [get, list, watch, update, patch]
- apiGroups: [""]
resources: [secrets]
verbs: [get, list, watch, create, update, patch, delete]
Everything it does to a target cluster uses that cluster's own kubeconfig. The manager's cache is scoped to kubeopera-system to match the Role.
Operating it
kubectl get hostclusters -n kubeopera-system -w
kubectl describe hostcluster <cluster-id> -n kubeopera-system
kubectl logs -n kubeopera-system -l app=host-cluster-controller -f
Probes are on :8081/healthz and /readyz; metrics on :8080/metrics.
Configuration
| Variable | Default | Description |
|---|---|---|
CLUSTER_PROVISIONER_BASE_URL | http://cluster-provisioner | cluster-provisioner. |
GITOPS_SCAFFOLDER_BASE_URL | http://gitops-scaffolder | gitops-scaffolder. |
SERVICE_CLIENT_ID / SERVICE_CLIENT_SECRET | — | The controller's service identity for calling them. |
ENVIRONMENT | production | Logging mode. |