Skip to content

Component Configuration

Individual components are not configured directly. The kMetal release pins each component's version and settings, and the operator applies them — so there is no per-component values block to override.

What exists instead is a small number of deliberate escape hatches, plus one control that applies to every component at once. This page is the map of what you can actually reach.

For the field-by-field surface, see Platform Configuration Reference.

Why there is no per-component override

The operator's whole purpose is that a release pins the component set, the versions and the order. A site that could change a component's values would no longer be running a combination anyone tested — and the failures that produces are the expensive kind: a CNI priority that silently makes Kube-OVN the default delegate, a CDI capability order that provisions raw-block volumes, a feature gate the rest of the platform assumes.

So the API carries values with no sane default, and nothing else. When a component genuinely needs to be configured differently, that belongs in a kMetal release — not in a local values file.

Placement: the one control over every component

spec.placement decides where the platform's controllers run. Omitted, it is exactly this:

spec:
  placement:
    nodeSelector:
      node-role.kubernetes.io/control-plane: ""
    tolerations:
      - key: node-role.kubernetes.io/control-plane
        operator: Exists
        effect: NoSchedule

It applies to every component that is control-plane workload — cert-manager and its cainjector, webhook and startupapicheck, Kamaji and its etcd, Capsule, the CAPI operator, Sveltos, the kMetal operators, MetalLB's controller. The default keeps them off tenant nodes; the default tolerations are a no-op on an untainted control plane and are what make the selector work on a tainted one.

It deliberately does not reach the DaemonSets and per-node plugins. MetalLB's speaker has to answer for a VIP from any node that can carry it, and Kube-OVN's per-node workloads are placed by node label instead, because they need the overlay interface present.

Change it when the platform should run on a dedicated class of node rather than the control plane — an infra pool, say. Set the label on those nodes first: a selector matching nothing leaves every platform Pod Pending with no other symptom.

It also does not place tenant VMs, and must not. KubeVirt's virt-handler — and with it every tenant machine and every image-builder installer VM — is pinned by the platform to nodes labelled node-role.kubernetes.io/compute, which is not a field:

# What kMetal patches onto the KubeVirt object, for reference
spec:
  workloads:
    nodePlacement:
      nodeSelector:
        node-role.kubernetes.io/compute: ""

spec.placement puts the platform's own controllers on the control plane; a tenant VM belongs on the opposite class of node, beside the CDI importer pods that fill its disk. Label those nodes, or nothing starts a machine — see Under Cluster Setup.

The ClusterClass passthrough

spec.clusterClass.values is the one true passthrough in the API. It is merged over the values kMetal computes for the ClusterClass chart, and wins on conflict:

spec:
  clusterClass:
    values:
      tiers:
        kubeadm:
          enabled: true
        standard:
          enabled: false
      dataVolume:
        volumeSnapshotClass: platform-storage-snapshot
        storageTiers:
          - 30Gi
      kubeadm:
        containerDisk:
          image: quay.io/capk/ubuntu-2404-container-disk
          tag: v1.34.1

kMetal computes only what it alone can know: where it installed the image builder, that the golden images and the machine root disks cloned from them both belong on storage.underclusterClassName, and that tenant machines attach to the Kube-OVN overlay. Everything else — which tiers to offer, the worker image and its tag, DNS servers, disk sizes, the VolumeSnapshotClass used to clone boot volumes — is yours.

The chart is not mirrored into the KMetal API because its knobs move between versions, and mirroring them would break on every bump. Read the accepted surface off the pinned chart:

helm show values oci://ghcr.io/clastix/cloud-clusterclass/kmetal --version <pinned>

Two chart values are yours to set, and tenant machines will not boot without them

Neither is among the values kMetal computes:

  • dataVolume.volumeSnapshotClass — must serve the same driver as storage.underclusterClassName. From chart v1.10.0 there is no default at all, and the component will not render without it.
  • the kubeadm tier's containerDisk.tag — must match the Kubernetes version tenant Clusters ask for.

And because these values win on conflict, this hatch can also override the computed ones that hold the platform together. Change what you mean to change.

Storage capabilities

spec.storage.tenantClaimPropertySets pins how CDI provisions volumes on the tenant StorageClass, as a CDI StorageProfile:

spec:
  storage:
    tenantClaimPropertySets:
      - accessModes:
          - ReadWriteOnce
        volumeMode: Filesystem

Order is significant — CDI takes the first entry for a DataVolume that does not ask for anything specific. Leave it empty on a single-capability backend, where auto-derivation can only produce one answer. Set it on anything advertising more than one combination: a backend that advertises ReadWriteMany + Block first has CDI provision raw-block volumes, and the importer pod then fails on a permission error naming neither the class nor the volume mode.

See Tenant Storage (CSI).

Turning a component off

There is no enabled flag per component. Two components are installed only when their spec block is present, and omitting the block is how you decline them:

Block Omitted means
console No web console. Nothing else depends on it.
storage.tenantClaimPropertySets No CDI StorageProfile is pinned; CDI derives its own order.

Everything else in the component set is part of the platform. There is no supported configuration of kMetal without Kube-OVN, Kamaji, or the Cluster API providers, because tenant clusters are defined in terms of them.

Checking what a change produces

kubectl apply --dry-run=server -f kmetal.yaml     # the API server carries the validation
kmetal render -f kmetal.yaml --from-cluster       # the objects the operator would apply

After applying, the per-component status is where a rejected or unsatisfiable change surfaces:

kubectl get km kmetal -o jsonpath='{range .status.components[*]}{.name}{"\t"}{.phase}{"\t"}{.message}{"\n"}{end}'

See Also: Platform Configuration · Configuration Best Practices · Platform Configuration Reference