KCM Proof of Concept
| Repository metadata | Value |
|---|---|
| Hosting | Forgejo |
| Release Tag | v0.0.6 |
| Last update | 2026-09-22 on main |
| Owner | DevFW-CICD |
| Repository | Open repository |
Kinds
| Resource | API group | Owner | Scope | Role |
|---|---|---|---|---|
KubernetesCluster | kubernetes.edge-connect.eu/v1alpha1 | tenant | Namespaced | desired state, node pools by capability |
KubernetesCluster | provider.kubernetes.edge-connect.eu/v1alpha1 | scheduler | Namespaced | resolved projection, nodePools[].type |
KubernetesClusterOffering | provider.kubernetes.edge-connect.eu/v1alpha1 | platform | Cluster | what one provider supports |
VirtualMachineType | provider.edge-connect.eu/v1alpha1 | platform | Cluster | machine-type catalog |
Provider | provider.edge-connect.eu/v1alpha1 | tenant | Cluster | provider identity + the tenant's own credentials reference |
provider.edge-connect.eu holds provider infrastructure primitives that are
not Kubernetes-specific; provider.kubernetes.edge-connect.eu holds the
Kubernetes-service provider layer.
None of Provider, KubernetesClusterOffering and VirtualMachineType has a
status: all three are operator- or refresh-controller-authored inventory, never
reconciled against a provider API by this repository.
Provider.metadata.name is the provider name — the value the scheduler writes
into spec.providerRef.name and the value a provider controller filters its
own work by. An offering points at a Provider through its own
spec.providerRef; the offering's metadata.name is just an object name
(ADR-0002).
A tenant authors its own Provider, in its own workspace, supplying its own
cloud account (ADR-0004). The offering and the Provider therefore
live in different workspaces, so the provider name is a vocabulary the two
sides share rather than a reference resolvable in one place: the catalog says
what otc can serve, a tenant's Provider/otc says that tenant can pay for
it. Neither is complete alone, and nothing validates the pairing — a catalog
naming a provider nobody supplies shows up as clusters reporting
ProviderNotConfigured.
A VirtualMachineType's metadata.name is likewise a platform-chosen
Kubernetes name. The provider's own machine-type string lives in
spec.providerMachineType (an OTC ECS flavor such as s3.xlarge.2), because a
provider flavor is not guaranteed to be a valid Kubernetes object name.
Scheduling contract
- Read the tenant
KubernetesCluster. - Candidates are the
KubernetesClusterOfferings whoseavailableRegionscontainspec.region, whoseavailableVersionscontainspec.version, and whose provider the workspace holds aProviderfor. Only names are read here — never a credentials Secret. - Per node pool, pick a
VirtualMachineTypefrom the candidate'savailableVirtualMachineTypeswhosecpuandmemorysatisfy the pool'scapabilities. A candidate that cannot satisfy every pool is dropped whole. - Write the winning offering's
spec.providerRef.name— aProvider's name — onto the tenant resource'sspec.providerRef.name. - Emit the provider-facing
KubernetesClusterwith a concretenodePools[].typeper pool.
Two policy choices make the outcome reproducible, so re-running the scheduler over unchanged inputs never moves a live pool onto a different machine type:
- among candidates, the lowest provider name wins, and the lowest offering name breaks a tie between two offerings of one provider;
- within a candidate, a pool takes the smallest fitting type, ordered by
cpu, thenmemory, then name.
A machine type must be available in the requested region and the offering must serve that region: the offering declares where the provider serves Kubernetes, the machine type declares where that shape exists.
Once spec.providerRef.name is set the cluster is pinned. A cheaper
offering appearing elsewhere never migrates it, and a pinned provider losing
its offering is reported rather than rescheduled — the nodes live at that
provider.
Unschedulable requests
An unschedulable request surfaces as a Scheduled=False condition, never a
silent retry. The reasons reuse the offering-resolution error contract of
platform ADR-0015:
| Reason | Meaning |
|---|---|
OfferingNotFound | no offering serves the requested region and version (or the pinned provider has none) |
ProviderNotConfigured | an offering serves the request but this workspace supplies credentials for no provider that could run it; the message names the providers that would |
CapabilityMismatch | a candidate exists but no machine type it offers satisfies a pool; the message names the pool |
CatalogUnavailable | the offerings or the catalog could not be read at all — transient, and the only one of the three that retries fast |
The condition stands for as long as the request cannot be served. The catalog
lives in another logical cluster this controller does not watch, so an
unschedulable cluster is re-checked every --unschedulable-requeue
(default 5m) — that way a provider adding a region or a machine type unsticks
existing clusters without an operator touching them.
The same requeue covers ProviderNotConfigured: there is no watch on
Provider, so a tenant that supplies credentials waits up to
--unschedulable-requeue before anything reschedules. Nothing is wrong during
that window, but it is long enough to be reported as "it did not work" by
someone who created the Provider and refreshed immediately. The condition and
its message stand throughout, so the cluster says what it is waiting for.
Tenant conditions
| Condition | Owner | Meaning |
|---|---|---|
Scheduled | cluster-scheduler | provider selected, every pool resolved |
Projected | cluster-scheduler | provider-facing cluster is in sync |
ProviderClusterReady | cluster-status | the projection reports itself ready |
Ready | cluster-status | roll-up; a False Ready names the pending phases |
status.ready follows Ready. The two reconcilers own disjoint conditions, so
neither overwrites the other's diagnosis.
Deleting a tenant cluster deletes its projection first: the
kubernetes.edge-connect.eu/cluster-projection finalizer is released only
afterwards, so provider state is never orphaned behind a vanished tenant
resource.