Skip to content

Hosted Control Plane Tasks

How tenant control planes are reached, and where their state lives.

For the model, see Hosted Control Planes.

Make a tenant control plane reachable

Each tenant control plane is published as a LoadBalancer Service taking an address from tenant-cp-pool, which MetalLB serves. Your part is that the pool exists, has room, and is routed — see Expose tenant control planes for the pool and its constraints, and Pin or auto-assign a control-plane address for how a cluster ends up on a particular address.

# What each tenant control plane is published on
kubectl get svc -A -l kamaji.clastix.io/component=service \
  -o custom-columns='NS:.metadata.namespace,NAME:.metadata.name,IP:.status.loadBalancer.ingress[0].ip'

Pin the address rather than letting it be assigned: it ends up in every kubeconfig and in the api-server certificate's SANs, so it is not something to move later.

For reaching control planes from outside the data centre, see Control Plane External Access.

Hand a tenant a working kubeconfig

The kubeconfig in the cluster's Secret points at the control plane's internal address, which a tenant cannot reach. The address to use is the one the LoadBalancer Service publishes.

Three ways to produce a usable one, in order of convenience:

# 1. The utility: namespace first, cluster second
kmetal-kubeconfig dynamo-prod solar > solar.kubeconfig
  1. The web console serves the same kubeconfig with the address already substituted.
  2. By hand — read the address off the Service and rewrite the server field.

See Create First Cluster.

Decide where control-plane state lives

Every hosted control plane keeps its etcd data on a Kamaji datastore, and that data sits on the class named by storage.etcdClassName:

spec:
  storage:
    underclusterClassName: platform-storage   # golden images and machine disks
    etcdClassName: local-path                 # this control-plane state; optional

etcdClassName is optional and defaults to underclusterClassName, so on a cluster with one platform class there is nothing to set. Name it separately when the class the golden images need — a snapshotting one — is the wrong class for etcd, which on most backends it is.

This is the platform's most consequential storage decision. A node-local class ties the durability of every tenant control plane to the node holding the data; a network-attached class survives losing it. Choosing it here rather than inheriting it means the trade is deliberate instead of a side effect of what the image pipeline needed.

Changing it after tenants exist means migrating etcd data, so settle it before the first tenant — see Configuration Best Practices.

kubectl get datastores.kamaji.clastix.io
kubectl get pvc -A | grep -i etcd

Control which Kubernetes versions tenants may ask for

A tenant sets spec.topology.version on their Cluster. What makes a version usable is that the worker image matching it exists — the kubeadm tier's containerDisk.tag must match the version tenant clusters request:

spec:
  clusterClass:
    values:
      kubeadm:
        containerDisk:
          image: quay.io/capk/ubuntu-2404-container-disk
          tag: v1.34.1

A tenant asking for a version with no matching worker image gets a control plane that comes up and workers that never join.

See MultiCluster Management Tasks for the version and machine-shape surface.