From 9fa9c85d4c14aea1a5fb7b610d7da36db5a969e5 Mon Sep 17 00:00:00 2001 From: Dhairya Arora Date: Wed, 25 Feb 2026 19:52:57 +0530 Subject: [PATCH 01/35] enable multiple control plane classes - ClusterClass now supports multiple classes for control-plane - similar to workers. - Cluster topology now includes a field "class" for control-plane which references to the control. Signed-off-by: Dhairya Arora --- api/v1beta1/cluster_types.go | 7 + api/v1beta1/clusterclass_types.go | 46 ++- api/v1beta1/zz_generated.deepcopy.go | 48 +++ api/v1beta1/zz_generated.openapi.go | 172 +++++++- .../cluster.x-k8s.io_clusterclasses.yaml | 387 ++++++++++++++++++ .../crd/bases/cluster.x-k8s.io_clusters.yaml | 7 + exp/topology/scope/blueprint.go | 31 +- .../core/v1alpha4/zz_generated.conversion.go | 6 +- .../clusterclass/clusterclass_controller.go | 15 +- .../controllers/topology/cluster/blueprint.go | 38 +- .../topology/cluster/reconcile_state_test.go | 8 +- internal/webhooks/patch_validation_test.go | 384 +++++++++-------- 12 files changed, 971 insertions(+), 178 deletions(-) diff --git a/api/v1beta1/cluster_types.go b/api/v1beta1/cluster_types.go index be27ac6d1300..9f9281b8ace6 100644 --- a/api/v1beta1/cluster_types.go +++ b/api/v1beta1/cluster_types.go @@ -599,6 +599,13 @@ type ControlPlaneTopology struct { // +optional Metadata ObjectMeta `json:"metadata,omitempty"` + // class is the name of the ControlPlaneClass used to create the set of control plane nodes. + // This should match one of the control plane classes defined in the ClusterClass object. + // +optional + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=256 + Class string `json:"class,omitempty"` + // replicas is the number of control plane nodes. // If the value is nil, the ControlPlane object is created without the number of Replicas // and it's assumed that the control plane controller does not implement support for this field. diff --git a/api/v1beta1/clusterclass_types.go b/api/v1beta1/clusterclass_types.go index 4d62dbf7246b..f9e9f06df2b2 100644 --- a/api/v1beta1/clusterclass_types.go +++ b/api/v1beta1/clusterclass_types.go @@ -114,7 +114,7 @@ type ClusterClassSpec struct { // controlPlane is a reference to a local struct that holds the details // for provisioning the Control Plane for the Cluster. // +optional - ControlPlane ControlPlaneClass `json:"controlPlane,omitempty"` + ControlPlane ControlPlaneTopologyClass `json:"controlPlane,omitempty"` // workers describes the worker nodes for the cluster. // It is a collection of node types which can be used to create @@ -136,6 +136,26 @@ type ClusterClassSpec struct { Patches []ClusterClassPatch `json:"patches,omitempty"` } +// ControlPlaneTopologyClass wraps the control plane configuration, supporting +// both the original single control plane definition and multiple control plane classes. +type ControlPlaneTopologyClass struct { + // ControlPlaneClass contains the default/single control plane definition. + // This is the original upstream field, preserved for backward compatibility. + // +optional + ControlPlaneClass `json:",inline"` + + // classes is a list of named control plane classes that can be referenced + // from the Cluster topology. Each class defines a distinct control plane + // configuration. The class name MUST be unique within this list. + // When classes is defined, the Cluster topology can reference a specific + // control plane class by name. + // +optional + // +listType=map + // +listMapKey=class + // +kubebuilder:validation:MaxItems=100 + Classes []ControlPlaneClass `json:"classes,omitempty"` +} + // ControlPlaneClass defines the class for the control plane. type ControlPlaneClass struct { // metadata is the metadata applied to the ControlPlane and the Machines of the ControlPlane @@ -148,6 +168,14 @@ type ControlPlaneClass struct { // +optional Metadata ObjectMeta `json:"metadata,omitempty"` + // class denotes a type of control-plane node present in the cluster. + // When used in ControlPlaneTopologyClass.Classes, this name MUST be unique + // within the list and can be referenced from the Cluster topology. + // +optional + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=256 + Class string `json:"class,omitempty"` + // LocalObjectTemplate contains the reference to the control plane provider. LocalObjectTemplate `json:",inline"` @@ -1013,6 +1041,11 @@ type PatchSelectorMatch struct { // +optional InfrastructureCluster bool `json:"infrastructureCluster,omitempty"` + // controlPlaneClass selects templates referenced in specific ControlPlaneClasses in + // .spec.controlPlane.classes. + // +optional + ControlPlaneClass *PatchSelectorMatchControlPlaneClass `json:"controlPlaneClass,omitempty"` + // machineDeploymentClass selects templates referenced in specific MachineDeploymentClasses in // .spec.workers.machineDeployments. // +optional @@ -1024,6 +1057,17 @@ type PatchSelectorMatch struct { MachinePoolClass *PatchSelectorMatchMachinePoolClass `json:"machinePoolClass,omitempty"` } +// PatchSelectorMatchControlPlaneClass selects templates referenced +// in specific ControlPlaneClasses in .spec.controlPlane.classes. +type PatchSelectorMatchControlPlaneClass struct { + // names selects templates by class names. + // +optional + // +kubebuilder:validation:MaxItems=100 + // +kubebuilder:validation:items:MinLength=1 + // +kubebuilder:validation:items:MaxLength=256 + Names []string `json:"names,omitempty"` +} + // PatchSelectorMatchMachineDeploymentClass selects templates referenced // in specific MachineDeploymentClasses in .spec.workers.machineDeployments. type PatchSelectorMatchMachineDeploymentClass struct { diff --git a/api/v1beta1/zz_generated.deepcopy.go b/api/v1beta1/zz_generated.deepcopy.go index ec25c1dfb000..9e9b6466334a 100644 --- a/api/v1beta1/zz_generated.deepcopy.go +++ b/api/v1beta1/zz_generated.deepcopy.go @@ -780,6 +780,29 @@ func (in *ControlPlaneTopology) DeepCopy() *ControlPlaneTopology { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ControlPlaneTopologyClass) DeepCopyInto(out *ControlPlaneTopologyClass) { + *out = *in + in.ControlPlaneClass.DeepCopyInto(&out.ControlPlaneClass) + if in.Classes != nil { + in, out := &in.Classes, &out.Classes + *out = make([]ControlPlaneClass, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ControlPlaneTopologyClass. +func (in *ControlPlaneTopologyClass) DeepCopy() *ControlPlaneTopologyClass { + if in == nil { + return nil + } + out := new(ControlPlaneTopologyClass) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ControlPlaneVariables) DeepCopyInto(out *ControlPlaneVariables) { *out = *in @@ -2601,6 +2624,11 @@ func (in *PatchSelector) DeepCopy() *PatchSelector { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *PatchSelectorMatch) DeepCopyInto(out *PatchSelectorMatch) { *out = *in + if in.ControlPlaneClass != nil { + in, out := &in.ControlPlaneClass, &out.ControlPlaneClass + *out = new(PatchSelectorMatchControlPlaneClass) + (*in).DeepCopyInto(*out) + } if in.MachineDeploymentClass != nil { in, out := &in.MachineDeploymentClass, &out.MachineDeploymentClass *out = new(PatchSelectorMatchMachineDeploymentClass) @@ -2623,6 +2651,26 @@ func (in *PatchSelectorMatch) DeepCopy() *PatchSelectorMatch { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PatchSelectorMatchControlPlaneClass) DeepCopyInto(out *PatchSelectorMatchControlPlaneClass) { + *out = *in + if in.Names != nil { + in, out := &in.Names, &out.Names + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PatchSelectorMatchControlPlaneClass. +func (in *PatchSelectorMatchControlPlaneClass) DeepCopy() *PatchSelectorMatchControlPlaneClass { + if in == nil { + return nil + } + out := new(PatchSelectorMatchControlPlaneClass) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *PatchSelectorMatchMachineDeploymentClass) DeepCopyInto(out *PatchSelectorMatchMachineDeploymentClass) { *out = *in diff --git a/api/v1beta1/zz_generated.openapi.go b/api/v1beta1/zz_generated.openapi.go index a3f0529dc3ca..a30ccb9f66ef 100644 --- a/api/v1beta1/zz_generated.openapi.go +++ b/api/v1beta1/zz_generated.openapi.go @@ -53,6 +53,7 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA "sigs.k8s.io/cluster-api/api/v1beta1.ControlPlaneClass": schema_sigsk8sio_cluster_api_api_v1beta1_ControlPlaneClass(ref), "sigs.k8s.io/cluster-api/api/v1beta1.ControlPlaneClassNamingStrategy": schema_sigsk8sio_cluster_api_api_v1beta1_ControlPlaneClassNamingStrategy(ref), "sigs.k8s.io/cluster-api/api/v1beta1.ControlPlaneTopology": schema_sigsk8sio_cluster_api_api_v1beta1_ControlPlaneTopology(ref), + "sigs.k8s.io/cluster-api/api/v1beta1.ControlPlaneTopologyClass": schema_sigsk8sio_cluster_api_api_v1beta1_ControlPlaneTopologyClass(ref), "sigs.k8s.io/cluster-api/api/v1beta1.ControlPlaneVariables": schema_sigsk8sio_cluster_api_api_v1beta1_ControlPlaneVariables(ref), "sigs.k8s.io/cluster-api/api/v1beta1.ExternalPatchDefinition": schema_sigsk8sio_cluster_api_api_v1beta1_ExternalPatchDefinition(ref), "sigs.k8s.io/cluster-api/api/v1beta1.FailureDomainSpec": schema_sigsk8sio_cluster_api_api_v1beta1_FailureDomainSpec(ref), @@ -111,6 +112,7 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA "sigs.k8s.io/cluster-api/api/v1beta1.PatchDefinition": schema_sigsk8sio_cluster_api_api_v1beta1_PatchDefinition(ref), "sigs.k8s.io/cluster-api/api/v1beta1.PatchSelector": schema_sigsk8sio_cluster_api_api_v1beta1_PatchSelector(ref), "sigs.k8s.io/cluster-api/api/v1beta1.PatchSelectorMatch": schema_sigsk8sio_cluster_api_api_v1beta1_PatchSelectorMatch(ref), + "sigs.k8s.io/cluster-api/api/v1beta1.PatchSelectorMatchControlPlaneClass": schema_sigsk8sio_cluster_api_api_v1beta1_PatchSelectorMatchControlPlaneClass(ref), "sigs.k8s.io/cluster-api/api/v1beta1.PatchSelectorMatchMachineDeploymentClass": schema_sigsk8sio_cluster_api_api_v1beta1_PatchSelectorMatchMachineDeploymentClass(ref), "sigs.k8s.io/cluster-api/api/v1beta1.PatchSelectorMatchMachinePoolClass": schema_sigsk8sio_cluster_api_api_v1beta1_PatchSelectorMatchMachinePoolClass(ref), "sigs.k8s.io/cluster-api/api/v1beta1.RemediationStrategy": schema_sigsk8sio_cluster_api_api_v1beta1_RemediationStrategy(ref), @@ -467,7 +469,7 @@ func schema_sigsk8sio_cluster_api_api_v1beta1_ClusterClassSpec(ref common.Refere SchemaProps: spec.SchemaProps{ Description: "controlPlane is a reference to a local struct that holds the details for provisioning the Control Plane for the Cluster.", Default: map[string]interface{}{}, - Ref: ref("sigs.k8s.io/cluster-api/api/v1beta1.ControlPlaneClass"), + Ref: ref("sigs.k8s.io/cluster-api/api/v1beta1.ControlPlaneTopologyClass"), }, }, "workers": { @@ -509,7 +511,7 @@ func schema_sigsk8sio_cluster_api_api_v1beta1_ClusterClassSpec(ref common.Refere }, }, Dependencies: []string{ - "sigs.k8s.io/cluster-api/api/v1beta1.ClusterAvailabilityGate", "sigs.k8s.io/cluster-api/api/v1beta1.ClusterClassPatch", "sigs.k8s.io/cluster-api/api/v1beta1.ClusterClassVariable", "sigs.k8s.io/cluster-api/api/v1beta1.ControlPlaneClass", "sigs.k8s.io/cluster-api/api/v1beta1.InfrastructureNamingStrategy", "sigs.k8s.io/cluster-api/api/v1beta1.LocalObjectTemplate", "sigs.k8s.io/cluster-api/api/v1beta1.WorkersClass"}, + "sigs.k8s.io/cluster-api/api/v1beta1.ClusterAvailabilityGate", "sigs.k8s.io/cluster-api/api/v1beta1.ClusterClassPatch", "sigs.k8s.io/cluster-api/api/v1beta1.ClusterClassVariable", "sigs.k8s.io/cluster-api/api/v1beta1.ControlPlaneTopologyClass", "sigs.k8s.io/cluster-api/api/v1beta1.InfrastructureNamingStrategy", "sigs.k8s.io/cluster-api/api/v1beta1.LocalObjectTemplate", "sigs.k8s.io/cluster-api/api/v1beta1.WorkersClass"}, } } @@ -1257,6 +1259,13 @@ func schema_sigsk8sio_cluster_api_api_v1beta1_ControlPlaneClass(ref common.Refer Ref: ref("sigs.k8s.io/cluster-api/api/v1beta1.ObjectMeta"), }, }, + "class": { + SchemaProps: spec.SchemaProps{ + Description: "class denotes a type of control-plane node present in the cluster. When used in ControlPlaneTopologyClass.Classes, this name MUST be unique within the list and can be referenced from the Cluster topology.", + Type: []string{"string"}, + Format: "", + }, + }, "ref": { SchemaProps: spec.SchemaProps{ Description: "ref is a required reference to a custom resource offered by a provider.", @@ -1364,6 +1373,13 @@ func schema_sigsk8sio_cluster_api_api_v1beta1_ControlPlaneTopology(ref common.Re Ref: ref("sigs.k8s.io/cluster-api/api/v1beta1.ObjectMeta"), }, }, + "class": { + SchemaProps: spec.SchemaProps{ + Description: "class is the name of the ControlPlaneClass used to create the set of control plane nodes. This should match one of the control plane classes defined in the ClusterClass object.", + Type: []string{"string"}, + Format: "", + }, + }, "replicas": { SchemaProps: spec.SchemaProps{ Description: "replicas is the number of control plane nodes. If the value is nil, the ControlPlane object is created without the number of Replicas and it's assumed that the control plane controller does not implement support for this field. When specified against a control plane provider that lacks support for this field, this value will be ignored.", @@ -1431,6 +1447,122 @@ func schema_sigsk8sio_cluster_api_api_v1beta1_ControlPlaneTopology(ref common.Re } } +func schema_sigsk8sio_cluster_api_api_v1beta1_ControlPlaneTopologyClass(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "ControlPlaneTopologyClass wraps the control plane configuration, supporting both the original single control plane definition and multiple control plane classes.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "metadata": { + SchemaProps: spec.SchemaProps{ + Description: "metadata is the metadata applied to the ControlPlane and the Machines of the ControlPlane if the ControlPlaneTemplate referenced is machine based. If not, it is applied only to the ControlPlane. At runtime this metadata is merged with the corresponding metadata from the topology.\n\nThis field is supported if and only if the control plane provider template referenced is Machine based.", + Default: map[string]interface{}{}, + Ref: ref("sigs.k8s.io/cluster-api/api/v1beta1.ObjectMeta"), + }, + }, + "class": { + SchemaProps: spec.SchemaProps{ + Description: "class denotes a type of control-plane node present in the cluster. When used in ControlPlaneTopologyClass.Classes, this name MUST be unique within the list and can be referenced from the Cluster topology.", + Type: []string{"string"}, + Format: "", + }, + }, + "ref": { + SchemaProps: spec.SchemaProps{ + Description: "ref is a required reference to a custom resource offered by a provider.", + Ref: ref("k8s.io/api/core/v1.ObjectReference"), + }, + }, + "machineInfrastructure": { + SchemaProps: spec.SchemaProps{ + Description: "machineInfrastructure defines the metadata and infrastructure information for control plane machines.\n\nThis field is supported if and only if the control plane provider template referenced above is Machine based and supports setting replicas.", + Ref: ref("sigs.k8s.io/cluster-api/api/v1beta1.LocalObjectTemplate"), + }, + }, + "machineHealthCheck": { + SchemaProps: spec.SchemaProps{ + Description: "machineHealthCheck defines a MachineHealthCheck for this ControlPlaneClass. This field is supported if and only if the ControlPlane provider template referenced above is Machine based and supports setting replicas.", + Ref: ref("sigs.k8s.io/cluster-api/api/v1beta1.MachineHealthCheckClass"), + }, + }, + "namingStrategy": { + SchemaProps: spec.SchemaProps{ + Description: "namingStrategy allows changing the naming pattern used when creating the control plane provider object.", + Ref: ref("sigs.k8s.io/cluster-api/api/v1beta1.ControlPlaneClassNamingStrategy"), + }, + }, + "nodeDrainTimeout": { + SchemaProps: spec.SchemaProps{ + Description: "nodeDrainTimeout is the total amount of time that the controller will spend on draining a node. The default value is 0, meaning that the node can be drained without any time limitations. NOTE: NodeDrainTimeout is different from `kubectl drain --timeout` NOTE: This value can be overridden while defining a Cluster.Topology.", + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.Duration"), + }, + }, + "nodeVolumeDetachTimeout": { + SchemaProps: spec.SchemaProps{ + Description: "nodeVolumeDetachTimeout is the total amount of time that the controller will spend on waiting for all volumes to be detached. The default value is 0, meaning that the volumes can be detached without any time limitations. NOTE: This value can be overridden while defining a Cluster.Topology.", + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.Duration"), + }, + }, + "nodeDeletionTimeout": { + SchemaProps: spec.SchemaProps{ + Description: "nodeDeletionTimeout defines how long the controller will attempt to delete the Node that the Machine hosts after the Machine is marked for deletion. A duration of 0 will retry deletion indefinitely. Defaults to 10 seconds. NOTE: This value can be overridden while defining a Cluster.Topology.", + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.Duration"), + }, + }, + "readinessGates": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-list-map-keys": []interface{}{ + "conditionType", + }, + "x-kubernetes-list-type": "map", + }, + }, + SchemaProps: spec.SchemaProps{ + Description: "readinessGates specifies additional conditions to include when evaluating Machine Ready condition.\n\nThis field can be used e.g. to instruct the machine controller to include in the computation for Machine's ready computation a condition, managed by an external controllers, reporting the status of special software/hardware installed on the Machine.\n\nNOTE: This field is considered only for computing v1beta2 conditions. NOTE: If a Cluster defines a custom list of readinessGates for the control plane, such list overrides readinessGates defined in this field. NOTE: Specific control plane provider implementations might automatically extend the list of readinessGates; e.g. the kubeadm control provider adds ReadinessGates for the APIServerPodHealthy, SchedulerPodHealthy conditions, etc.", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("sigs.k8s.io/cluster-api/api/v1beta1.MachineReadinessGate"), + }, + }, + }, + }, + }, + "classes": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-list-map-keys": []interface{}{ + "class", + }, + "x-kubernetes-list-type": "map", + }, + }, + SchemaProps: spec.SchemaProps{ + Description: "classes is a list of named control plane classes that can be referenced from the Cluster topology. Each class defines a distinct control plane configuration. The class name MUST be unique within this list. When classes is defined, the Cluster topology can reference a specific control plane class by name.", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("sigs.k8s.io/cluster-api/api/v1beta1.ControlPlaneClass"), + }, + }, + }, + }, + }, + }, + Required: []string{"ref"}, + }, + }, + Dependencies: []string{ + "k8s.io/api/core/v1.ObjectReference", "k8s.io/apimachinery/pkg/apis/meta/v1.Duration", "sigs.k8s.io/cluster-api/api/v1beta1.ControlPlaneClass", "sigs.k8s.io/cluster-api/api/v1beta1.ControlPlaneClassNamingStrategy", "sigs.k8s.io/cluster-api/api/v1beta1.LocalObjectTemplate", "sigs.k8s.io/cluster-api/api/v1beta1.MachineHealthCheckClass", "sigs.k8s.io/cluster-api/api/v1beta1.MachineReadinessGate", "sigs.k8s.io/cluster-api/api/v1beta1.ObjectMeta"}, + } +} + func schema_sigsk8sio_cluster_api_api_v1beta1_ControlPlaneVariables(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ @@ -4564,6 +4696,12 @@ func schema_sigsk8sio_cluster_api_api_v1beta1_PatchSelectorMatch(ref common.Refe Format: "", }, }, + "controlPlaneClass": { + SchemaProps: spec.SchemaProps{ + Description: "controlPlaneClass selects templates referenced in specific ControlPlaneClasses in .spec.controlPlane.classes.", + Ref: ref("sigs.k8s.io/cluster-api/api/v1beta1.PatchSelectorMatchControlPlaneClass"), + }, + }, "machineDeploymentClass": { SchemaProps: spec.SchemaProps{ Description: "machineDeploymentClass selects templates referenced in specific MachineDeploymentClasses in .spec.workers.machineDeployments.", @@ -4580,7 +4718,35 @@ func schema_sigsk8sio_cluster_api_api_v1beta1_PatchSelectorMatch(ref common.Refe }, }, Dependencies: []string{ - "sigs.k8s.io/cluster-api/api/v1beta1.PatchSelectorMatchMachineDeploymentClass", "sigs.k8s.io/cluster-api/api/v1beta1.PatchSelectorMatchMachinePoolClass"}, + "sigs.k8s.io/cluster-api/api/v1beta1.PatchSelectorMatchControlPlaneClass", "sigs.k8s.io/cluster-api/api/v1beta1.PatchSelectorMatchMachineDeploymentClass", "sigs.k8s.io/cluster-api/api/v1beta1.PatchSelectorMatchMachinePoolClass"}, + } +} + +func schema_sigsk8sio_cluster_api_api_v1beta1_PatchSelectorMatchControlPlaneClass(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "PatchSelectorMatchControlPlaneClass selects templates referenced in specific ControlPlaneClasses in .spec.controlPlane.classes.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "names": { + SchemaProps: spec.SchemaProps{ + Description: "names selects templates by class names.", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + }, + }, + }, } } diff --git a/config/crd/bases/cluster.x-k8s.io_clusterclasses.yaml b/config/crd/bases/cluster.x-k8s.io_clusterclasses.yaml index 7d8f42572c73..f5e7a68cd9b6 100644 --- a/config/crd/bases/cluster.x-k8s.io_clusterclasses.yaml +++ b/config/crd/bases/cluster.x-k8s.io_clusterclasses.yaml @@ -490,6 +490,378 @@ spec: controlPlane is a reference to a local struct that holds the details for provisioning the Control Plane for the Cluster. properties: + class: + description: |- + class denotes a type of control-plane node present in the cluster. + When used in ControlPlaneTopologyClass.Classes, this name MUST be unique + within the list and can be referenced from the Cluster topology. + maxLength: 256 + minLength: 1 + type: string + classes: + description: |- + classes is a list of named control plane classes that can be referenced + from the Cluster topology. Each class defines a distinct control plane + configuration. The class name MUST be unique within this list. + When classes is defined, the Cluster topology can reference a specific + control plane class by name. + items: + description: ControlPlaneClass defines the class for the control + plane. + properties: + class: + description: |- + class denotes a type of control-plane node present in the cluster. + When used in ControlPlaneTopologyClass.Classes, this name MUST be unique + within the list and can be referenced from the Cluster topology. + maxLength: 256 + minLength: 1 + type: string + machineHealthCheck: + description: |- + machineHealthCheck defines a MachineHealthCheck for this ControlPlaneClass. + This field is supported if and only if the ControlPlane provider template + referenced above is Machine based and supports setting replicas. + properties: + maxUnhealthy: + anyOf: + - type: integer + - type: string + description: |- + maxUnhealthy specifies the maximum number of unhealthy machines allowed. + Any further remediation is only allowed if at most "maxUnhealthy" machines selected by + "selector" are not healthy. + x-kubernetes-int-or-string: true + nodeStartupTimeout: + description: |- + nodeStartupTimeout allows to set the maximum time for MachineHealthCheck + to consider a Machine unhealthy if a corresponding Node isn't associated + through a `Spec.ProviderID` field. + + The duration set in this field is compared to the greatest of: + - Cluster's infrastructure ready condition timestamp (if and when available) + - Control Plane's initialized condition timestamp (if and when available) + - Machine's infrastructure ready condition timestamp (if and when available) + - Machine's metadata creation timestamp + + Defaults to 10 minutes. + If you wish to disable this feature, set the value explicitly to 0. + type: string + remediationTemplate: + description: |- + remediationTemplate is a reference to a remediation template + provided by an infrastructure provider. + + This field is completely optional, when filled, the MachineHealthCheck controller + creates a new object from the template referenced and hands off remediation of the machine to + a controller that lives outside of Cluster API. + properties: + apiVersion: + description: API version of the referent. + type: string + fieldPath: + description: |- + If referring to a piece of an object instead of an entire object, this string + should contain a valid JSON/Go field access statement, such as desiredState.manifest.containers[2]. + For example, if the object reference is to a container within a pod, this would take on a value like: + "spec.containers{name}" (where "name" refers to the name of the container that triggered + the event) or if no container name is specified "spec.containers[2]" (container with + index 2 in this pod). This syntax is chosen only to have some well-defined way of + referencing a part of an object. + type: string + kind: + description: |- + Kind of the referent. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + namespace: + description: |- + Namespace of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/ + type: string + resourceVersion: + description: |- + Specific resourceVersion to which this reference is made, if any. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency + type: string + uid: + description: |- + UID of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids + type: string + type: object + x-kubernetes-map-type: atomic + unhealthyConditions: + description: |- + unhealthyConditions contains a list of the conditions that determine + whether a node is considered unhealthy. The conditions are combined in a + logical OR, i.e. if any of the conditions is met, the node is unhealthy. + items: + description: |- + UnhealthyCondition represents a Node condition type and value with a timeout + specified as a duration. When the named condition has been in the given + status for at least the timeout value, a node is considered unhealthy. + properties: + status: + description: status of the condition, one of True, + False, Unknown. + minLength: 1 + type: string + timeout: + description: |- + timeout is the duration that a node must be in a given status for, + after which the node is considered unhealthy. + For example, with a value of "1h", the node must match the status + for at least 1 hour before being considered unhealthy. + type: string + type: + description: type of Node condition + minLength: 1 + type: string + required: + - status + - timeout + - type + type: object + maxItems: 100 + type: array + unhealthyRange: + description: |- + unhealthyRange specifies the range of unhealthy machines allowed. + Any further remediation is only allowed if the number of machines selected by "selector" as not healthy + is within the range of "unhealthyRange". Takes precedence over maxUnhealthy. + Eg. "[3-5]" - This means that remediation will be allowed only when: + (a) there are at least 3 unhealthy machines (and) + (b) there are at most 5 unhealthy machines + maxLength: 32 + minLength: 1 + pattern: ^\[[0-9]+-[0-9]+\]$ + type: string + type: object + machineInfrastructure: + description: |- + machineInfrastructure defines the metadata and infrastructure information + for control plane machines. + + This field is supported if and only if the control plane provider template + referenced above is Machine based and supports setting replicas. + properties: + ref: + description: |- + ref is a required reference to a custom resource + offered by a provider. + properties: + apiVersion: + description: API version of the referent. + type: string + fieldPath: + description: |- + If referring to a piece of an object instead of an entire object, this string + should contain a valid JSON/Go field access statement, such as desiredState.manifest.containers[2]. + For example, if the object reference is to a container within a pod, this would take on a value like: + "spec.containers{name}" (where "name" refers to the name of the container that triggered + the event) or if no container name is specified "spec.containers[2]" (container with + index 2 in this pod). This syntax is chosen only to have some well-defined way of + referencing a part of an object. + type: string + kind: + description: |- + Kind of the referent. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + namespace: + description: |- + Namespace of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/ + type: string + resourceVersion: + description: |- + Specific resourceVersion to which this reference is made, if any. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency + type: string + uid: + description: |- + UID of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids + type: string + type: object + x-kubernetes-map-type: atomic + required: + - ref + type: object + metadata: + description: |- + metadata is the metadata applied to the ControlPlane and the Machines of the ControlPlane + if the ControlPlaneTemplate referenced is machine based. If not, it is applied only to the + ControlPlane. + At runtime this metadata is merged with the corresponding metadata from the topology. + + This field is supported if and only if the control plane provider template + referenced is Machine based. + properties: + annotations: + additionalProperties: + type: string + description: |- + annotations is an unstructured key value map stored with a resource that may be + set by external tools to store and retrieve arbitrary metadata. They are not + queryable and should be preserved when modifying objects. + More info: http://kubernetes.io/docs/user-guide/annotations + type: object + labels: + additionalProperties: + type: string + description: |- + labels is a map of string keys and values that can be used to organize and categorize + (scope and select) objects. May match selectors of replication controllers + and services. + More info: http://kubernetes.io/docs/user-guide/labels + type: object + type: object + namingStrategy: + description: namingStrategy allows changing the naming pattern + used when creating the control plane provider object. + properties: + template: + description: |- + template defines the template to use for generating the name of the ControlPlane object. + If not defined, it will fallback to `{{ .cluster.name }}-{{ .random }}`. + If the templated string exceeds 63 characters, it will be trimmed to 58 characters and will + get concatenated with a random suffix of length 5. + The templating mechanism provides the following arguments: + * `.cluster.name`: The name of the cluster object. + * `.random`: A random alphanumeric string, without vowels, of length 5. + maxLength: 1024 + minLength: 1 + type: string + type: object + nodeDeletionTimeout: + description: |- + nodeDeletionTimeout defines how long the controller will attempt to delete the Node that the Machine + hosts after the Machine is marked for deletion. A duration of 0 will retry deletion indefinitely. + Defaults to 10 seconds. + NOTE: This value can be overridden while defining a Cluster.Topology. + type: string + nodeDrainTimeout: + description: |- + nodeDrainTimeout is the total amount of time that the controller will spend on draining a node. + The default value is 0, meaning that the node can be drained without any time limitations. + NOTE: NodeDrainTimeout is different from `kubectl drain --timeout` + NOTE: This value can be overridden while defining a Cluster.Topology. + type: string + nodeVolumeDetachTimeout: + description: |- + nodeVolumeDetachTimeout is the total amount of time that the controller will spend on waiting for all volumes + to be detached. The default value is 0, meaning that the volumes can be detached without any time limitations. + NOTE: This value can be overridden while defining a Cluster.Topology. + type: string + readinessGates: + description: |- + readinessGates specifies additional conditions to include when evaluating Machine Ready condition. + + This field can be used e.g. to instruct the machine controller to include in the computation for Machine's ready + computation a condition, managed by an external controllers, reporting the status of special software/hardware installed on the Machine. + + NOTE: This field is considered only for computing v1beta2 conditions. + NOTE: If a Cluster defines a custom list of readinessGates for the control plane, + such list overrides readinessGates defined in this field. + NOTE: Specific control plane provider implementations might automatically extend the list of readinessGates; + e.g. the kubeadm control provider adds ReadinessGates for the APIServerPodHealthy, SchedulerPodHealthy conditions, etc. + items: + description: MachineReadinessGate contains the type of + a Machine condition to be used as a readiness gate. + properties: + conditionType: + description: |- + conditionType refers to a condition with matching type in the Machine's condition list. + If the conditions doesn't exist, it will be treated as unknown. + Note: Both Cluster API conditions or conditions added by 3rd party controllers can be used as readiness gates. + maxLength: 316 + minLength: 1 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + polarity: + description: |- + polarity of the conditionType specified in this readinessGate. + Valid values are Positive, Negative and omitted. + When omitted, the default behaviour will be Positive. + A positive polarity means that the condition should report a true status under normal conditions. + A negative polarity means that the condition should report a false status under normal conditions. + enum: + - Positive + - Negative + type: string + required: + - conditionType + type: object + maxItems: 32 + type: array + x-kubernetes-list-map-keys: + - conditionType + x-kubernetes-list-type: map + ref: + description: |- + ref is a required reference to a custom resource + offered by a provider. + properties: + apiVersion: + description: API version of the referent. + type: string + fieldPath: + description: |- + If referring to a piece of an object instead of an entire object, this string + should contain a valid JSON/Go field access statement, such as desiredState.manifest.containers[2]. + For example, if the object reference is to a container within a pod, this would take on a value like: + "spec.containers{name}" (where "name" refers to the name of the container that triggered + the event) or if no container name is specified "spec.containers[2]" (container with + index 2 in this pod). This syntax is chosen only to have some well-defined way of + referencing a part of an object. + type: string + kind: + description: |- + Kind of the referent. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + namespace: + description: |- + Namespace of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/ + type: string + resourceVersion: + description: |- + Specific resourceVersion to which this reference is made, if any. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency + type: string + uid: + description: |- + UID of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids + type: string + type: object + x-kubernetes-map-type: atomic + required: + - ref + type: object + maxItems: 100 + type: array + x-kubernetes-list-map-keys: + - class + x-kubernetes-list-type: map machineHealthCheck: description: |- machineHealthCheck defines a MachineHealthCheck for this ControlPlaneClass. @@ -1010,6 +1382,21 @@ spec: Note: this will match the controlPlane and also the controlPlane machineInfrastructure (depending on the kind and apiVersion). type: boolean + controlPlaneClass: + description: |- + controlPlaneClass selects templates referenced in specific ControlPlaneClasses in + .spec.controlPlane.classes. + properties: + names: + description: names selects templates by class + names. + items: + maxLength: 256 + minLength: 1 + type: string + maxItems: 100 + type: array + type: object infrastructureCluster: description: infrastructureCluster selects templates referenced in .spec.infrastructure. diff --git a/config/crd/bases/cluster.x-k8s.io_clusters.yaml b/config/crd/bases/cluster.x-k8s.io_clusters.yaml index 2694d431cf02..43044564a2bc 100644 --- a/config/crd/bases/cluster.x-k8s.io_clusters.yaml +++ b/config/crd/bases/cluster.x-k8s.io_clusters.yaml @@ -979,6 +979,13 @@ spec: controlPlane: description: controlPlane describes the cluster control plane. properties: + class: + description: |- + class is the name of the ControlPlaneClass used to create the set of control plane nodes. + This should match one of the control plane classes defined in the ClusterClass object. + maxLength: 256 + minLength: 1 + type: string machineHealthCheck: description: |- machineHealthCheck allows to enable, disable and override diff --git a/exp/topology/scope/blueprint.go b/exp/topology/scope/blueprint.go index 08e734505f80..f83af0aee18a 100644 --- a/exp/topology/scope/blueprint.go +++ b/exp/topology/scope/blueprint.go @@ -31,6 +31,11 @@ type ClusterBlueprint struct { // ClusterClass holds the ClusterClass object referenced from Cluster.Spec.Topology. ClusterClass *clusterv1.ClusterClass + // ControlPlaneClass holds the resolved ControlPlaneClass from the ClusterClass. + // This is the ControlPlaneClass selected based on the Cluster topology's control plane class field. + // If the topology does not specify a class, this is the inline ControlPlaneClass from ClusterClass.Spec.ControlPlane. + ControlPlaneClass *clusterv1.ControlPlaneClass + // InfrastructureClusterTemplate holds the InfrastructureClusterTemplate referenced from ClusterClass. InfrastructureClusterTemplate *unstructured.Unstructured @@ -93,7 +98,11 @@ type MachinePoolBlueprint struct { // HasControlPlaneInfrastructureMachine checks whether the clusterClass mandates the controlPlane has infrastructureMachines. func (b *ClusterBlueprint) HasControlPlaneInfrastructureMachine() bool { - return b.ClusterClass.Spec.ControlPlane.MachineInfrastructure != nil && b.ClusterClass.Spec.ControlPlane.MachineInfrastructure.Ref != nil + if b.ControlPlaneClass == nil { + return false + } + + return b.ControlPlaneClass.MachineInfrastructure != nil && b.ControlPlaneClass.MachineInfrastructure.Ref != nil } // IsControlPlaneMachineHealthCheckEnabled returns true if a MachineHealthCheck should be created for the control plane. @@ -102,19 +111,31 @@ func (b *ClusterBlueprint) IsControlPlaneMachineHealthCheckEnabled() bool { if !b.HasControlPlaneInfrastructureMachine() { return false } - // If no MachineHealthCheck is defined in the ClusterClass or in the Cluster Topology then return false. - if b.ClusterClass.Spec.ControlPlane.MachineHealthCheck == nil && + + // If no MachineHealthCheck is defined in the resolved ControlPlaneClass or in the Cluster Topology then return false. + cpClassMHC := b.controlPlaneClassMachineHealthCheck() + if cpClassMHC == nil && (b.Topology.ControlPlane.MachineHealthCheck == nil || b.Topology.ControlPlane.MachineHealthCheck.MachineHealthCheckClass.IsZero()) { return false } + // If `enable` is not set then consider it as true. A MachineHealthCheck will be created from either ClusterClass or Cluster Topology. if b.Topology.ControlPlane.MachineHealthCheck == nil || b.Topology.ControlPlane.MachineHealthCheck.Enable == nil { return true } + // If `enable` is explicitly set, use the value. return *b.Topology.ControlPlane.MachineHealthCheck.Enable } +// controlPlaneClassMachineHealthCheck returns the MachineHealthCheck from the resolved ControlPlaneClass. +func (b *ClusterBlueprint) controlPlaneClassMachineHealthCheck() *clusterv1.MachineHealthCheckClass { + if b.ControlPlaneClass == nil { + return nil + } + return b.ControlPlaneClass.MachineHealthCheck +} + // ControlPlaneMachineHealthCheckClass returns the MachineHealthCheckClass that should be used to create the MachineHealthCheck object. func (b *ClusterBlueprint) ControlPlaneMachineHealthCheckClass() *clusterv1.MachineHealthCheckClass { if b.Topology.ControlPlane.MachineHealthCheck != nil && !b.Topology.ControlPlane.MachineHealthCheck.MachineHealthCheckClass.IsZero() { @@ -123,9 +144,9 @@ func (b *ClusterBlueprint) ControlPlaneMachineHealthCheckClass() *clusterv1.Mach return b.ControlPlane.MachineHealthCheck } -// HasControlPlaneMachineHealthCheck returns true if the ControlPlaneClass has both MachineInfrastructure and a MachineHealthCheck defined. +// HasControlPlaneMachineHealthCheck returns true if the resolved ControlPlaneClass has both MachineInfrastructure and a MachineHealthCheck defined. func (b *ClusterBlueprint) HasControlPlaneMachineHealthCheck() bool { - return b.HasControlPlaneInfrastructureMachine() && b.ClusterClass.Spec.ControlPlane.MachineHealthCheck != nil + return b.HasControlPlaneInfrastructureMachine() && b.controlPlaneClassMachineHealthCheck() != nil } // IsMachineDeploymentMachineHealthCheckEnabled returns true if a MachineHealthCheck should be created for the MachineDeployment. diff --git a/internal/apis/core/v1alpha4/zz_generated.conversion.go b/internal/apis/core/v1alpha4/zz_generated.conversion.go index c5bbfcfd5d90..60940d560467 100644 --- a/internal/apis/core/v1alpha4/zz_generated.conversion.go +++ b/internal/apis/core/v1alpha4/zz_generated.conversion.go @@ -627,7 +627,7 @@ func autoConvert_v1alpha4_ClusterClassSpec_To_v1beta1_ClusterClassSpec(in *Clust if err := Convert_v1alpha4_LocalObjectTemplate_To_v1beta1_LocalObjectTemplate(&in.Infrastructure, &out.Infrastructure, s); err != nil { return err } - if err := Convert_v1alpha4_ControlPlaneClass_To_v1beta1_ControlPlaneClass(&in.ControlPlane, &out.ControlPlane, s); err != nil { + if err := Convert_v1alpha4_ControlPlaneClass_To_v1beta1_ControlPlaneTopologyClass(&in.ControlPlane, &out.ControlPlane, s); err != nil { return err } if err := Convert_v1alpha4_WorkersClass_To_v1beta1_WorkersClass(&in.Workers, &out.Workers, s); err != nil { @@ -647,7 +647,7 @@ func autoConvert_v1beta1_ClusterClassSpec_To_v1alpha4_ClusterClassSpec(in *v1bet return err } // WARNING: in.InfrastructureNamingStrategy requires manual conversion: does not exist in peer-type - if err := Convert_v1beta1_ControlPlaneClass_To_v1alpha4_ControlPlaneClass(&in.ControlPlane, &out.ControlPlane, s); err != nil { + if err := Convert_v1beta1_ControlPlaneTopologyClass_To_v1alpha4_ControlPlaneClass(&in.ControlPlane, &out.ControlPlane, s); err != nil { return err } if err := Convert_v1beta1_WorkersClass_To_v1alpha4_WorkersClass(&in.Workers, &out.Workers, s); err != nil { @@ -852,6 +852,7 @@ func autoConvert_v1beta1_ControlPlaneClass_To_v1alpha4_ControlPlaneClass(in *v1b if err := Convert_v1beta1_ObjectMeta_To_v1alpha4_ObjectMeta(&in.Metadata, &out.Metadata, s); err != nil { return err } + // WARNING: in.Class requires manual conversion: does not exist in peer-type if err := Convert_v1beta1_LocalObjectTemplate_To_v1alpha4_LocalObjectTemplate(&in.LocalObjectTemplate, &out.LocalObjectTemplate, s); err != nil { return err } @@ -882,6 +883,7 @@ func autoConvert_v1beta1_ControlPlaneTopology_To_v1alpha4_ControlPlaneTopology(i if err := Convert_v1beta1_ObjectMeta_To_v1alpha4_ObjectMeta(&in.Metadata, &out.Metadata, s); err != nil { return err } + // WARNING: in.Class requires manual conversion: does not exist in peer-type out.Replicas = (*int32)(unsafe.Pointer(in.Replicas)) // WARNING: in.MachineHealthCheck requires manual conversion: does not exist in peer-type // WARNING: in.NodeDrainTimeout requires manual conversion: does not exist in peer-type diff --git a/internal/controllers/clusterclass/clusterclass_controller.go b/internal/controllers/clusterclass/clusterclass_controller.go index 2b0f1cd994fe..1091b0b8fba4 100644 --- a/internal/controllers/clusterclass/clusterclass_controller.go +++ b/internal/controllers/clusterclass/clusterclass_controller.go @@ -95,7 +95,6 @@ func (r *Reconciler) SetupWithManager(ctx context.Context, mgr ctrl.Manager, opt ). WithEventFilter(predicates.ResourceHasFilterLabel(mgr.GetScheme(), predicateLog, r.WatchFilterValue)). Complete(r) - if err != nil { return errors.Wrap(err, "failed setting up with a controller manager") } @@ -225,6 +224,17 @@ func (r *Reconciler) reconcileExternalReferences(ctx context.Context, s *scope) refs = append(refs, clusterClass.Spec.ControlPlane.MachineInfrastructure.Ref) } + // collect the same for control-plane classes as well. + for _, cpClass := range clusterClass.Spec.ControlPlane.Classes { + if cpClass.Ref != nil { + refs = append(refs, cpClass.Ref) + } + + if cpClass.MachineInfrastructure != nil && cpClass.MachineInfrastructure.Ref != nil { + refs = append(refs, cpClass.MachineInfrastructure.Ref) + } + } + for _, mdClass := range clusterClass.Spec.Workers.MachineDeployments { if mdClass.Template.Bootstrap.Ref != nil { refs = append(refs, mdClass.Template.Bootstrap.Ref) @@ -394,7 +404,8 @@ func addNewStatusVariable(variable clusterv1.ClusterClassVariable, from string) Metadata: variable.Metadata, Schema: variable.Schema, }, - }} + }, + } } func addDefinitionToExistingStatusVariable(variable clusterv1.ClusterClassVariable, from string, existingVariable *clusterv1.ClusterClassStatusVariable) *clusterv1.ClusterClassStatusVariable { diff --git a/internal/controllers/topology/cluster/blueprint.go b/internal/controllers/topology/cluster/blueprint.go index dfa8ddbbddbf..b7d5fbda1eb0 100644 --- a/internal/controllers/topology/cluster/blueprint.go +++ b/internal/controllers/topology/cluster/blueprint.go @@ -44,9 +44,18 @@ func (r *Reconciler) getBlueprint(ctx context.Context, cluster *clusterv1.Cluste return nil, errors.Wrapf(err, "failed to get infrastructure cluster template for ClusterClass %s", klog.KObj(blueprint.ClusterClass)) } - // Get ClusterClass.spec.controlPlane. + // Resolve the ControlPlaneClass to use. + // If the Cluster topology specifies a control plane class, look it up from ClusterClass.spec.controlPlane.classes. + // Otherwise, fall back to the inline ClusterClass.spec.controlPlane definition. + controlPlaneClass, err := resolveControlPlaneClass(cluster, clusterClass) + if err != nil { + return nil, errors.Wrapf(err, "failed to resolve control plane class for ClusterClass %s", klog.KObj(blueprint.ClusterClass)) + } + + blueprint.ControlPlaneClass = controlPlaneClass + blueprint.ControlPlane = &scope.ControlPlaneBlueprint{} - blueprint.ControlPlane.Template, err = r.getReference(ctx, blueprint.ClusterClass.Spec.ControlPlane.Ref) + blueprint.ControlPlane.Template, err = r.getReference(ctx, controlPlaneClass.Ref) if err != nil { return nil, errors.Wrapf(err, "failed to get control plane template for ClusterClass %s", klog.KObj(blueprint.ClusterClass)) } @@ -120,3 +129,28 @@ func (r *Reconciler) getBlueprint(ctx context.Context, cluster *clusterv1.Cluste return blueprint, nil } + +// resolveControlPlaneClass determines which ControlPlaneClass to use based on the Cluster topology. +// If the Cluster topology specifies a control plane class name, it is looked up from +// ClusterClass.spec.controlPlane.classes. +// Otherwise, the inline ClusterClass.spec.controlPlane is used. +func resolveControlPlaneClass(cluster *clusterv1.Cluster, clusterClass *clusterv1.ClusterClass) (*clusterv1.ControlPlaneClass, error) { + // If the topology doesn't specify a class, use the inline definition. + if cluster.Spec.Topology.ControlPlane.Class == "" { + return &clusterClass.Spec.ControlPlane.ControlPlaneClass, nil + } + + // Look up the class from spec.controlPlane.classes. + for i := range clusterClass.Spec.ControlPlane.Classes { + if clusterClass.Spec.ControlPlane.Classes[i].Class == cluster.Spec.Topology.ControlPlane.Class { + return &clusterClass.Spec.ControlPlane.Classes[i], nil + } + } + + return nil, errors.Errorf( + "control plane class %q not found in ClusterClass %s/%s", + cluster.Spec.Topology.ControlPlane.Class, + clusterClass.Namespace, + clusterClass.Name, + ) +} diff --git a/internal/controllers/topology/cluster/reconcile_state_test.go b/internal/controllers/topology/cluster/reconcile_state_test.go index 87dfd0bad3a6..9aa37913e63d 100644 --- a/internal/controllers/topology/cluster/reconcile_state_test.go +++ b/internal/controllers/topology/cluster/reconcile_state_test.go @@ -1707,9 +1707,11 @@ func TestReconcileControlPlaneCleanup(t *testing.T) { s.Blueprint = &scope.ClusterBlueprint{ ClusterClass: &clusterv1.ClusterClass{ Spec: clusterv1.ClusterClassSpec{ - ControlPlane: clusterv1.ControlPlaneClass{ - MachineInfrastructure: &clusterv1.LocalObjectTemplate{ - Ref: contract.ObjToRef(infrastructureMachineTemplate), + ControlPlane: clusterv1.ControlPlaneTopologyClass{ + ControlPlaneClass: clusterv1.ControlPlaneClass{ + MachineInfrastructure: &clusterv1.LocalObjectTemplate{ + Ref: contract.ObjToRef(infrastructureMachineTemplate), + }, }, }, }, diff --git a/internal/webhooks/patch_validation_test.go b/internal/webhooks/patch_validation_test.go index 365e8dab5d76..f13a1449168b 100644 --- a/internal/webhooks/patch_validation_test.go +++ b/internal/webhooks/patch_validation_test.go @@ -43,11 +43,13 @@ func TestValidatePatches(t *testing.T) { name: "pass multiple patches that are correctly formatted", clusterClass: clusterv1.ClusterClass{ Spec: clusterv1.ClusterClassSpec{ - ControlPlane: clusterv1.ControlPlaneClass{ - LocalObjectTemplate: clusterv1.LocalObjectTemplate{ - Ref: &corev1.ObjectReference{ - APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", - Kind: "ControlPlaneTemplate", + ControlPlane: clusterv1.ControlPlaneTopologyClass{ + ControlPlaneClass: clusterv1.ControlPlaneClass{ + LocalObjectTemplate: clusterv1.LocalObjectTemplate{ + Ref: &corev1.ObjectReference{ + APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", + Kind: "ControlPlaneTemplate", + }, }, }, }, @@ -130,11 +132,13 @@ func TestValidatePatches(t *testing.T) { name: "error if patch name is empty", clusterClass: clusterv1.ClusterClass{ Spec: clusterv1.ClusterClassSpec{ - ControlPlane: clusterv1.ControlPlaneClass{ - LocalObjectTemplate: clusterv1.LocalObjectTemplate{ - Ref: &corev1.ObjectReference{ - APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", - Kind: "ControlPlaneTemplate", + ControlPlane: clusterv1.ControlPlaneTopologyClass{ + ControlPlaneClass: clusterv1.ControlPlaneClass{ + LocalObjectTemplate: clusterv1.LocalObjectTemplate{ + Ref: &corev1.ObjectReference{ + APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", + Kind: "ControlPlaneTemplate", + }, }, }, }, @@ -183,11 +187,13 @@ func TestValidatePatches(t *testing.T) { name: "error if patches name is not unique", clusterClass: clusterv1.ClusterClass{ Spec: clusterv1.ClusterClassSpec{ - ControlPlane: clusterv1.ControlPlaneClass{ - LocalObjectTemplate: clusterv1.LocalObjectTemplate{ - Ref: &corev1.ObjectReference{ - APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", - Kind: "ControlPlaneTemplate", + ControlPlane: clusterv1.ControlPlaneTopologyClass{ + ControlPlaneClass: clusterv1.ControlPlaneClass{ + LocalObjectTemplate: clusterv1.LocalObjectTemplate{ + Ref: &corev1.ObjectReference{ + APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", + Kind: "ControlPlaneTemplate", + }, }, }, }, @@ -270,11 +276,13 @@ func TestValidatePatches(t *testing.T) { name: "pass if enabledIf is a valid Go template", clusterClass: clusterv1.ClusterClass{ Spec: clusterv1.ClusterClassSpec{ - ControlPlane: clusterv1.ControlPlaneClass{ - LocalObjectTemplate: clusterv1.LocalObjectTemplate{ - Ref: &corev1.ObjectReference{ - APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", - Kind: "ControlPlaneTemplate", + ControlPlane: clusterv1.ControlPlaneTopologyClass{ + ControlPlaneClass: clusterv1.ControlPlaneClass{ + LocalObjectTemplate: clusterv1.LocalObjectTemplate{ + Ref: &corev1.ObjectReference{ + APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", + Kind: "ControlPlaneTemplate", + }, }, }, }, @@ -293,11 +301,13 @@ func TestValidatePatches(t *testing.T) { name: "error if enabledIf is an invalid Go template", clusterClass: clusterv1.ClusterClass{ Spec: clusterv1.ClusterClassSpec{ - ControlPlane: clusterv1.ControlPlaneClass{ - LocalObjectTemplate: clusterv1.LocalObjectTemplate{ - Ref: &corev1.ObjectReference{ - APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", - Kind: "ControlPlaneTemplate", + ControlPlane: clusterv1.ControlPlaneTopologyClass{ + ControlPlaneClass: clusterv1.ControlPlaneClass{ + LocalObjectTemplate: clusterv1.LocalObjectTemplate{ + Ref: &corev1.ObjectReference{ + APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", + Kind: "ControlPlaneTemplate", + }, }, }, }, @@ -316,11 +326,13 @@ func TestValidatePatches(t *testing.T) { name: "error if patch op is not \"add\" \"remove\" or \"replace\"", clusterClass: clusterv1.ClusterClass{ Spec: clusterv1.ClusterClassSpec{ - ControlPlane: clusterv1.ControlPlaneClass{ - LocalObjectTemplate: clusterv1.LocalObjectTemplate{ - Ref: &corev1.ObjectReference{ - APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", - Kind: "ControlPlaneTemplate", + ControlPlane: clusterv1.ControlPlaneTopologyClass{ + ControlPlaneClass: clusterv1.ControlPlaneClass{ + LocalObjectTemplate: clusterv1.LocalObjectTemplate{ + Ref: &corev1.ObjectReference{ + APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", + Kind: "ControlPlaneTemplate", + }, }, }, }, @@ -358,11 +370,13 @@ func TestValidatePatches(t *testing.T) { name: "error if jsonPath does not begin with \"/spec/\"", clusterClass: clusterv1.ClusterClass{ Spec: clusterv1.ClusterClassSpec{ - ControlPlane: clusterv1.ControlPlaneClass{ - LocalObjectTemplate: clusterv1.LocalObjectTemplate{ - Ref: &corev1.ObjectReference{ - APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", - Kind: "ControlPlaneTemplate", + ControlPlane: clusterv1.ControlPlaneTopologyClass{ + ControlPlaneClass: clusterv1.ControlPlaneClass{ + LocalObjectTemplate: clusterv1.LocalObjectTemplate{ + Ref: &corev1.ObjectReference{ + APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", + Kind: "ControlPlaneTemplate", + }, }, }, }, @@ -398,11 +412,13 @@ func TestValidatePatches(t *testing.T) { name: "pass if jsonPatch path uses a valid index for add i.e. 0", clusterClass: clusterv1.ClusterClass{ Spec: clusterv1.ClusterClassSpec{ - ControlPlane: clusterv1.ControlPlaneClass{ - LocalObjectTemplate: clusterv1.LocalObjectTemplate{ - Ref: &corev1.ObjectReference{ - APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", - Kind: "ControlPlaneTemplate", + ControlPlane: clusterv1.ControlPlaneTopologyClass{ + ControlPlaneClass: clusterv1.ControlPlaneClass{ + LocalObjectTemplate: clusterv1.LocalObjectTemplate{ + Ref: &corev1.ObjectReference{ + APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", + Kind: "ControlPlaneTemplate", + }, }, }, }, @@ -450,11 +466,13 @@ func TestValidatePatches(t *testing.T) { name: "error if jsonPatch path uses an invalid index for add i.e. a number greater than 0.", clusterClass: clusterv1.ClusterClass{ Spec: clusterv1.ClusterClassSpec{ - ControlPlane: clusterv1.ControlPlaneClass{ - LocalObjectTemplate: clusterv1.LocalObjectTemplate{ - Ref: &corev1.ObjectReference{ - APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", - Kind: "ControlPlaneTemplate", + ControlPlane: clusterv1.ControlPlaneTopologyClass{ + ControlPlaneClass: clusterv1.ControlPlaneClass{ + LocalObjectTemplate: clusterv1.LocalObjectTemplate{ + Ref: &corev1.ObjectReference{ + APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", + Kind: "ControlPlaneTemplate", + }, }, }, }, @@ -503,11 +521,13 @@ func TestValidatePatches(t *testing.T) { name: "error if jsonPatch path uses an invalid index for add i.e. 01", clusterClass: clusterv1.ClusterClass{ Spec: clusterv1.ClusterClassSpec{ - ControlPlane: clusterv1.ControlPlaneClass{ - LocalObjectTemplate: clusterv1.LocalObjectTemplate{ - Ref: &corev1.ObjectReference{ - APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", - Kind: "ControlPlaneTemplate", + ControlPlane: clusterv1.ControlPlaneTopologyClass{ + ControlPlaneClass: clusterv1.ControlPlaneClass{ + LocalObjectTemplate: clusterv1.LocalObjectTemplate{ + Ref: &corev1.ObjectReference{ + APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", + Kind: "ControlPlaneTemplate", + }, }, }, }, @@ -556,11 +576,13 @@ func TestValidatePatches(t *testing.T) { name: "error if jsonPatch path uses any index for remove i.e. 0 or -.", clusterClass: clusterv1.ClusterClass{ Spec: clusterv1.ClusterClassSpec{ - ControlPlane: clusterv1.ControlPlaneClass{ - LocalObjectTemplate: clusterv1.LocalObjectTemplate{ - Ref: &corev1.ObjectReference{ - APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", - Kind: "ControlPlaneTemplate", + ControlPlane: clusterv1.ControlPlaneTopologyClass{ + ControlPlaneClass: clusterv1.ControlPlaneClass{ + LocalObjectTemplate: clusterv1.LocalObjectTemplate{ + Ref: &corev1.ObjectReference{ + APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", + Kind: "ControlPlaneTemplate", + }, }, }, }, @@ -609,11 +631,13 @@ func TestValidatePatches(t *testing.T) { name: "error if jsonPatch path uses any index for replace i.e. 0", clusterClass: clusterv1.ClusterClass{ Spec: clusterv1.ClusterClassSpec{ - ControlPlane: clusterv1.ControlPlaneClass{ - LocalObjectTemplate: clusterv1.LocalObjectTemplate{ - Ref: &corev1.ObjectReference{ - APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", - Kind: "ControlPlaneTemplate", + ControlPlane: clusterv1.ControlPlaneTopologyClass{ + ControlPlaneClass: clusterv1.ControlPlaneClass{ + LocalObjectTemplate: clusterv1.LocalObjectTemplate{ + Ref: &corev1.ObjectReference{ + APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", + Kind: "ControlPlaneTemplate", + }, }, }, }, @@ -664,11 +688,13 @@ func TestValidatePatches(t *testing.T) { name: "error if jsonPatch has neither Value nor ValueFrom", clusterClass: clusterv1.ClusterClass{ Spec: clusterv1.ClusterClassSpec{ - ControlPlane: clusterv1.ControlPlaneClass{ - LocalObjectTemplate: clusterv1.LocalObjectTemplate{ - Ref: &corev1.ObjectReference{ - APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", - Kind: "ControlPlaneTemplate", + ControlPlane: clusterv1.ControlPlaneTopologyClass{ + ControlPlaneClass: clusterv1.ControlPlaneClass{ + LocalObjectTemplate: clusterv1.LocalObjectTemplate{ + Ref: &corev1.ObjectReference{ + APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", + Kind: "ControlPlaneTemplate", + }, }, }, }, @@ -704,11 +730,13 @@ func TestValidatePatches(t *testing.T) { name: "error if jsonPatch has both Value and ValueFrom", clusterClass: clusterv1.ClusterClass{ Spec: clusterv1.ClusterClassSpec{ - ControlPlane: clusterv1.ControlPlaneClass{ - LocalObjectTemplate: clusterv1.LocalObjectTemplate{ - Ref: &corev1.ObjectReference{ - APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", - Kind: "ControlPlaneTemplate", + ControlPlane: clusterv1.ControlPlaneTopologyClass{ + ControlPlaneClass: clusterv1.ControlPlaneClass{ + LocalObjectTemplate: clusterv1.LocalObjectTemplate{ + Ref: &corev1.ObjectReference{ + APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", + Kind: "ControlPlaneTemplate", + }, }, }, }, @@ -759,11 +787,13 @@ func TestValidatePatches(t *testing.T) { name: "pass if jsonPatch value is valid json literal", clusterClass: clusterv1.ClusterClass{ Spec: clusterv1.ClusterClassSpec{ - ControlPlane: clusterv1.ControlPlaneClass{ - LocalObjectTemplate: clusterv1.LocalObjectTemplate{ - Ref: &corev1.ObjectReference{ - APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", - Kind: "ControlPlaneTemplate", + ControlPlane: clusterv1.ControlPlaneTopologyClass{ + ControlPlaneClass: clusterv1.ControlPlaneClass{ + LocalObjectTemplate: clusterv1.LocalObjectTemplate{ + Ref: &corev1.ObjectReference{ + APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", + Kind: "ControlPlaneTemplate", + }, }, }, }, @@ -798,11 +828,13 @@ func TestValidatePatches(t *testing.T) { name: "pass if jsonPatch value is valid json", clusterClass: clusterv1.ClusterClass{ Spec: clusterv1.ClusterClassSpec{ - ControlPlane: clusterv1.ControlPlaneClass{ - LocalObjectTemplate: clusterv1.LocalObjectTemplate{ - Ref: &corev1.ObjectReference{ - APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", - Kind: "ControlPlaneTemplate", + ControlPlane: clusterv1.ControlPlaneTopologyClass{ + ControlPlaneClass: clusterv1.ControlPlaneClass{ + LocalObjectTemplate: clusterv1.LocalObjectTemplate{ + Ref: &corev1.ObjectReference{ + APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", + Kind: "ControlPlaneTemplate", + }, }, }, }, @@ -840,11 +872,13 @@ func TestValidatePatches(t *testing.T) { name: "pass if jsonPatch value is nil", clusterClass: clusterv1.ClusterClass{ Spec: clusterv1.ClusterClassSpec{ - ControlPlane: clusterv1.ControlPlaneClass{ - LocalObjectTemplate: clusterv1.LocalObjectTemplate{ - Ref: &corev1.ObjectReference{ - APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", - Kind: "ControlPlaneTemplate", + ControlPlane: clusterv1.ControlPlaneTopologyClass{ + ControlPlaneClass: clusterv1.ControlPlaneClass{ + LocalObjectTemplate: clusterv1.LocalObjectTemplate{ + Ref: &corev1.ObjectReference{ + APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", + Kind: "ControlPlaneTemplate", + }, }, }, }, @@ -881,11 +915,13 @@ func TestValidatePatches(t *testing.T) { name: "error if jsonPatch value is invalid json", clusterClass: clusterv1.ClusterClass{ Spec: clusterv1.ClusterClassSpec{ - ControlPlane: clusterv1.ControlPlaneClass{ - LocalObjectTemplate: clusterv1.LocalObjectTemplate{ - Ref: &corev1.ObjectReference{ - APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", - Kind: "ControlPlaneTemplate", + ControlPlane: clusterv1.ControlPlaneTopologyClass{ + ControlPlaneClass: clusterv1.ControlPlaneClass{ + LocalObjectTemplate: clusterv1.LocalObjectTemplate{ + Ref: &corev1.ObjectReference{ + APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", + Kind: "ControlPlaneTemplate", + }, }, }, }, @@ -926,11 +962,13 @@ func TestValidatePatches(t *testing.T) { name: "error if jsonPatch defines neither ValueFrom.Template nor ValueFrom.Variable", clusterClass: clusterv1.ClusterClass{ Spec: clusterv1.ClusterClassSpec{ - ControlPlane: clusterv1.ControlPlaneClass{ - LocalObjectTemplate: clusterv1.LocalObjectTemplate{ - Ref: &corev1.ObjectReference{ - APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", - Kind: "ControlPlaneTemplate", + ControlPlane: clusterv1.ControlPlaneTopologyClass{ + ControlPlaneClass: clusterv1.ControlPlaneClass{ + LocalObjectTemplate: clusterv1.LocalObjectTemplate{ + Ref: &corev1.ObjectReference{ + APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", + Kind: "ControlPlaneTemplate", + }, }, }, }, @@ -965,11 +1003,13 @@ func TestValidatePatches(t *testing.T) { name: "error if jsonPatch has both ValueFrom.Template and ValueFrom.Variable", clusterClass: clusterv1.ClusterClass{ Spec: clusterv1.ClusterClassSpec{ - ControlPlane: clusterv1.ControlPlaneClass{ - LocalObjectTemplate: clusterv1.LocalObjectTemplate{ - Ref: &corev1.ObjectReference{ - APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", - Kind: "ControlPlaneTemplate", + ControlPlane: clusterv1.ControlPlaneTopologyClass{ + ControlPlaneClass: clusterv1.ControlPlaneClass{ + LocalObjectTemplate: clusterv1.LocalObjectTemplate{ + Ref: &corev1.ObjectReference{ + APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", + Kind: "ControlPlaneTemplate", + }, }, }, }, @@ -1020,11 +1060,13 @@ func TestValidatePatches(t *testing.T) { name: "pass if jsonPatch defines a valid ValueFrom.Template", clusterClass: clusterv1.ClusterClass{ Spec: clusterv1.ClusterClassSpec{ - ControlPlane: clusterv1.ControlPlaneClass{ - LocalObjectTemplate: clusterv1.LocalObjectTemplate{ - Ref: &corev1.ObjectReference{ - APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", - Kind: "ControlPlaneTemplate", + ControlPlane: clusterv1.ControlPlaneTopologyClass{ + ControlPlaneClass: clusterv1.ControlPlaneClass{ + LocalObjectTemplate: clusterv1.LocalObjectTemplate{ + Ref: &corev1.ObjectReference{ + APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", + Kind: "ControlPlaneTemplate", + }, }, }, }, @@ -1072,11 +1114,13 @@ func TestValidatePatches(t *testing.T) { name: "error if jsonPatch defines an invalid ValueFrom.Template", clusterClass: clusterv1.ClusterClass{ Spec: clusterv1.ClusterClassSpec{ - ControlPlane: clusterv1.ControlPlaneClass{ - LocalObjectTemplate: clusterv1.LocalObjectTemplate{ - Ref: &corev1.ObjectReference{ - APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", - Kind: "ControlPlaneTemplate", + ControlPlane: clusterv1.ControlPlaneTopologyClass{ + ControlPlaneClass: clusterv1.ControlPlaneClass{ + LocalObjectTemplate: clusterv1.LocalObjectTemplate{ + Ref: &corev1.ObjectReference{ + APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", + Kind: "ControlPlaneTemplate", + }, }, }, }, @@ -1127,11 +1171,13 @@ func TestValidatePatches(t *testing.T) { name: "error if jsonPatch valueFrom uses a variable which is not defined", clusterClass: clusterv1.ClusterClass{ Spec: clusterv1.ClusterClassSpec{ - ControlPlane: clusterv1.ControlPlaneClass{ - LocalObjectTemplate: clusterv1.LocalObjectTemplate{ - Ref: &corev1.ObjectReference{ - APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", - Kind: "ControlPlaneTemplate", + ControlPlane: clusterv1.ControlPlaneTopologyClass{ + ControlPlaneClass: clusterv1.ControlPlaneClass{ + LocalObjectTemplate: clusterv1.LocalObjectTemplate{ + Ref: &corev1.ObjectReference{ + APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", + Kind: "ControlPlaneTemplate", + }, }, }, }, @@ -1179,11 +1225,13 @@ func TestValidatePatches(t *testing.T) { name: "pass if jsonPatch uses a user-defined variable which is defined", clusterClass: clusterv1.ClusterClass{ Spec: clusterv1.ClusterClassSpec{ - ControlPlane: clusterv1.ControlPlaneClass{ - LocalObjectTemplate: clusterv1.LocalObjectTemplate{ - Ref: &corev1.ObjectReference{ - APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", - Kind: "ControlPlaneTemplate", + ControlPlane: clusterv1.ControlPlaneTopologyClass{ + ControlPlaneClass: clusterv1.ControlPlaneClass{ + LocalObjectTemplate: clusterv1.LocalObjectTemplate{ + Ref: &corev1.ObjectReference{ + APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", + Kind: "ControlPlaneTemplate", + }, }, }, }, @@ -1231,11 +1279,13 @@ func TestValidatePatches(t *testing.T) { name: "pass if jsonPatch uses a nested user-defined variable which is defined", clusterClass: clusterv1.ClusterClass{ Spec: clusterv1.ClusterClassSpec{ - ControlPlane: clusterv1.ControlPlaneClass{ - LocalObjectTemplate: clusterv1.LocalObjectTemplate{ - Ref: &corev1.ObjectReference{ - APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", - Kind: "ControlPlaneTemplate", + ControlPlane: clusterv1.ControlPlaneTopologyClass{ + ControlPlaneClass: clusterv1.ControlPlaneClass{ + LocalObjectTemplate: clusterv1.LocalObjectTemplate{ + Ref: &corev1.ObjectReference{ + APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", + Kind: "ControlPlaneTemplate", + }, }, }, }, @@ -1288,11 +1338,13 @@ func TestValidatePatches(t *testing.T) { name: "error if jsonPatch uses a builtin variable which is not defined", clusterClass: clusterv1.ClusterClass{ Spec: clusterv1.ClusterClassSpec{ - ControlPlane: clusterv1.ControlPlaneClass{ - LocalObjectTemplate: clusterv1.LocalObjectTemplate{ - Ref: &corev1.ObjectReference{ - APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", - Kind: "ControlPlaneTemplate", + ControlPlane: clusterv1.ControlPlaneTopologyClass{ + ControlPlaneClass: clusterv1.ControlPlaneClass{ + LocalObjectTemplate: clusterv1.LocalObjectTemplate{ + Ref: &corev1.ObjectReference{ + APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", + Kind: "ControlPlaneTemplate", + }, }, }, }, @@ -1329,11 +1381,13 @@ func TestValidatePatches(t *testing.T) { name: "pass if jsonPatch uses a builtin variable which is defined", clusterClass: clusterv1.ClusterClass{ Spec: clusterv1.ClusterClassSpec{ - ControlPlane: clusterv1.ControlPlaneClass{ - LocalObjectTemplate: clusterv1.LocalObjectTemplate{ - Ref: &corev1.ObjectReference{ - APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", - Kind: "ControlPlaneTemplate", + ControlPlane: clusterv1.ControlPlaneTopologyClass{ + ControlPlaneClass: clusterv1.ControlPlaneClass{ + LocalObjectTemplate: clusterv1.LocalObjectTemplate{ + Ref: &corev1.ObjectReference{ + APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", + Kind: "ControlPlaneTemplate", + }, }, }, }, @@ -1373,11 +1427,13 @@ func TestValidatePatches(t *testing.T) { name: "pass if patch defines both external.generateExtension and external.validateExtension", clusterClass: clusterv1.ClusterClass{ Spec: clusterv1.ClusterClassSpec{ - ControlPlane: clusterv1.ControlPlaneClass{ - LocalObjectTemplate: clusterv1.LocalObjectTemplate{ - Ref: &corev1.ObjectReference{ - APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", - Kind: "ControlPlaneTemplate", + ControlPlane: clusterv1.ControlPlaneTopologyClass{ + ControlPlaneClass: clusterv1.ControlPlaneClass{ + LocalObjectTemplate: clusterv1.LocalObjectTemplate{ + Ref: &corev1.ObjectReference{ + APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", + Kind: "ControlPlaneTemplate", + }, }, }, }, @@ -1400,11 +1456,13 @@ func TestValidatePatches(t *testing.T) { name: "error if patch defines both external and RuntimeSDK is not enabled", clusterClass: clusterv1.ClusterClass{ Spec: clusterv1.ClusterClassSpec{ - ControlPlane: clusterv1.ControlPlaneClass{ - LocalObjectTemplate: clusterv1.LocalObjectTemplate{ - Ref: &corev1.ObjectReference{ - APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", - Kind: "ControlPlaneTemplate", + ControlPlane: clusterv1.ControlPlaneTopologyClass{ + ControlPlaneClass: clusterv1.ControlPlaneClass{ + LocalObjectTemplate: clusterv1.LocalObjectTemplate{ + Ref: &corev1.ObjectReference{ + APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", + Kind: "ControlPlaneTemplate", + }, }, }, }, @@ -1427,11 +1485,13 @@ func TestValidatePatches(t *testing.T) { name: "error if patch defines neither external.generateExtension nor external.validateExtension", clusterClass: clusterv1.ClusterClass{ Spec: clusterv1.ClusterClassSpec{ - ControlPlane: clusterv1.ControlPlaneClass{ - LocalObjectTemplate: clusterv1.LocalObjectTemplate{ - Ref: &corev1.ObjectReference{ - APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", - Kind: "ControlPlaneTemplate", + ControlPlane: clusterv1.ControlPlaneTopologyClass{ + ControlPlaneClass: clusterv1.ControlPlaneClass{ + LocalObjectTemplate: clusterv1.LocalObjectTemplate{ + Ref: &corev1.ObjectReference{ + APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", + Kind: "ControlPlaneTemplate", + }, }, }, }, @@ -1451,11 +1511,13 @@ func TestValidatePatches(t *testing.T) { name: "error if patch defines both external and definitions", clusterClass: clusterv1.ClusterClass{ Spec: clusterv1.ClusterClassSpec{ - ControlPlane: clusterv1.ControlPlaneClass{ - LocalObjectTemplate: clusterv1.LocalObjectTemplate{ - Ref: &corev1.ObjectReference{ - APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", - Kind: "ControlPlaneTemplate", + ControlPlane: clusterv1.ControlPlaneTopologyClass{ + ControlPlaneClass: clusterv1.ControlPlaneClass{ + LocalObjectTemplate: clusterv1.LocalObjectTemplate{ + Ref: &corev1.ObjectReference{ + APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", + Kind: "ControlPlaneTemplate", + }, }, }, }, @@ -1479,11 +1541,13 @@ func TestValidatePatches(t *testing.T) { name: "error if neither external nor definitions is defined", clusterClass: clusterv1.ClusterClass{ Spec: clusterv1.ClusterClassSpec{ - ControlPlane: clusterv1.ControlPlaneClass{ - LocalObjectTemplate: clusterv1.LocalObjectTemplate{ - Ref: &corev1.ObjectReference{ - APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", - Kind: "ControlPlaneTemplate", + ControlPlane: clusterv1.ControlPlaneTopologyClass{ + ControlPlaneClass: clusterv1.ControlPlaneClass{ + LocalObjectTemplate: clusterv1.LocalObjectTemplate{ + Ref: &corev1.ObjectReference{ + APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", + Kind: "ControlPlaneTemplate", + }, }, }, }, From dfca86cd50f2e344cc376e788348cd282c2618fe Mon Sep 17 00:00:00 2001 From: Dhairya Arora Date: Wed, 25 Feb 2026 23:15:13 +0530 Subject: [PATCH 02/35] fixup! enable multiple control plane classes --- api/v1beta1/cluster_types.go | 1 + api/v1beta1/clusterclass_types.go | 34 +- api/v1beta1/zz_generated.deepcopy.go | 30 +- api/v1beta1/zz_generated.openapi.go | 145 +--- .../cluster.x-k8s.io_clusterclasses.yaml | 729 +++++++++--------- .../crd/bases/cluster.x-k8s.io_clusters.yaml | 1 + .../core/v1alpha4/zz_generated.conversion.go | 5 +- .../clusterclass/clusterclass_controller.go | 2 +- .../controllers/topology/cluster/blueprint.go | 10 +- .../topology/cluster/reconcile_state_test.go | 8 +- internal/topology/check/compatibility.go | 39 +- internal/topology/check/compatibility_test.go | 67 ++ internal/webhooks/patch_validation_test.go | 384 ++++----- 13 files changed, 688 insertions(+), 767 deletions(-) diff --git a/api/v1beta1/cluster_types.go b/api/v1beta1/cluster_types.go index 9f9281b8ace6..c1badd42a8ba 100644 --- a/api/v1beta1/cluster_types.go +++ b/api/v1beta1/cluster_types.go @@ -601,6 +601,7 @@ type ControlPlaneTopology struct { // class is the name of the ControlPlaneClass used to create the set of control plane nodes. // This should match one of the control plane classes defined in the ClusterClass object. + // syself new field. // +optional // +kubebuilder:validation:MinLength=1 // +kubebuilder:validation:MaxLength=256 diff --git a/api/v1beta1/clusterclass_types.go b/api/v1beta1/clusterclass_types.go index f9e9f06df2b2..ae81610c0499 100644 --- a/api/v1beta1/clusterclass_types.go +++ b/api/v1beta1/clusterclass_types.go @@ -114,7 +114,19 @@ type ClusterClassSpec struct { // controlPlane is a reference to a local struct that holds the details // for provisioning the Control Plane for the Cluster. // +optional - ControlPlane ControlPlaneTopologyClass `json:"controlPlane,omitempty"` + ControlPlane ControlPlaneClass `json:"controlPlane,omitempty"` + + // controlPlaneClasses is a list of named control plane classes that can be referenced + // from the Cluster topology. Each class defines a distinct control plane + // configuration. The class name MUST be unique within this list. + // When classes is defined, the Cluster topology can reference a specific + // control plane class by name. + // syself new field. + // +optional + // +listType=map + // +listMapKey=class + // +kubebuilder:validation:MaxItems=100 + ControlPlaneClasses []ControlPlaneClass `json:"controlPlaneClasses,omitempty"` // workers describes the worker nodes for the cluster. // It is a collection of node types which can be used to create @@ -136,26 +148,6 @@ type ClusterClassSpec struct { Patches []ClusterClassPatch `json:"patches,omitempty"` } -// ControlPlaneTopologyClass wraps the control plane configuration, supporting -// both the original single control plane definition and multiple control plane classes. -type ControlPlaneTopologyClass struct { - // ControlPlaneClass contains the default/single control plane definition. - // This is the original upstream field, preserved for backward compatibility. - // +optional - ControlPlaneClass `json:",inline"` - - // classes is a list of named control plane classes that can be referenced - // from the Cluster topology. Each class defines a distinct control plane - // configuration. The class name MUST be unique within this list. - // When classes is defined, the Cluster topology can reference a specific - // control plane class by name. - // +optional - // +listType=map - // +listMapKey=class - // +kubebuilder:validation:MaxItems=100 - Classes []ControlPlaneClass `json:"classes,omitempty"` -} - // ControlPlaneClass defines the class for the control plane. type ControlPlaneClass struct { // metadata is the metadata applied to the ControlPlane and the Machines of the ControlPlane diff --git a/api/v1beta1/zz_generated.deepcopy.go b/api/v1beta1/zz_generated.deepcopy.go index 9e9b6466334a..08766f1950c2 100644 --- a/api/v1beta1/zz_generated.deepcopy.go +++ b/api/v1beta1/zz_generated.deepcopy.go @@ -217,6 +217,13 @@ func (in *ClusterClassSpec) DeepCopyInto(out *ClusterClassSpec) { (*in).DeepCopyInto(*out) } in.ControlPlane.DeepCopyInto(&out.ControlPlane) + if in.ControlPlaneClasses != nil { + in, out := &in.ControlPlaneClasses, &out.ControlPlaneClasses + *out = make([]ControlPlaneClass, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } in.Workers.DeepCopyInto(&out.Workers) if in.Variables != nil { in, out := &in.Variables, &out.Variables @@ -780,29 +787,6 @@ func (in *ControlPlaneTopology) DeepCopy() *ControlPlaneTopology { return out } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *ControlPlaneTopologyClass) DeepCopyInto(out *ControlPlaneTopologyClass) { - *out = *in - in.ControlPlaneClass.DeepCopyInto(&out.ControlPlaneClass) - if in.Classes != nil { - in, out := &in.Classes, &out.Classes - *out = make([]ControlPlaneClass, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ControlPlaneTopologyClass. -func (in *ControlPlaneTopologyClass) DeepCopy() *ControlPlaneTopologyClass { - if in == nil { - return nil - } - out := new(ControlPlaneTopologyClass) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ControlPlaneVariables) DeepCopyInto(out *ControlPlaneVariables) { *out = *in diff --git a/api/v1beta1/zz_generated.openapi.go b/api/v1beta1/zz_generated.openapi.go index a30ccb9f66ef..11f46abe5e70 100644 --- a/api/v1beta1/zz_generated.openapi.go +++ b/api/v1beta1/zz_generated.openapi.go @@ -53,7 +53,6 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA "sigs.k8s.io/cluster-api/api/v1beta1.ControlPlaneClass": schema_sigsk8sio_cluster_api_api_v1beta1_ControlPlaneClass(ref), "sigs.k8s.io/cluster-api/api/v1beta1.ControlPlaneClassNamingStrategy": schema_sigsk8sio_cluster_api_api_v1beta1_ControlPlaneClassNamingStrategy(ref), "sigs.k8s.io/cluster-api/api/v1beta1.ControlPlaneTopology": schema_sigsk8sio_cluster_api_api_v1beta1_ControlPlaneTopology(ref), - "sigs.k8s.io/cluster-api/api/v1beta1.ControlPlaneTopologyClass": schema_sigsk8sio_cluster_api_api_v1beta1_ControlPlaneTopologyClass(ref), "sigs.k8s.io/cluster-api/api/v1beta1.ControlPlaneVariables": schema_sigsk8sio_cluster_api_api_v1beta1_ControlPlaneVariables(ref), "sigs.k8s.io/cluster-api/api/v1beta1.ExternalPatchDefinition": schema_sigsk8sio_cluster_api_api_v1beta1_ExternalPatchDefinition(ref), "sigs.k8s.io/cluster-api/api/v1beta1.FailureDomainSpec": schema_sigsk8sio_cluster_api_api_v1beta1_FailureDomainSpec(ref), @@ -469,7 +468,29 @@ func schema_sigsk8sio_cluster_api_api_v1beta1_ClusterClassSpec(ref common.Refere SchemaProps: spec.SchemaProps{ Description: "controlPlane is a reference to a local struct that holds the details for provisioning the Control Plane for the Cluster.", Default: map[string]interface{}{}, - Ref: ref("sigs.k8s.io/cluster-api/api/v1beta1.ControlPlaneTopologyClass"), + Ref: ref("sigs.k8s.io/cluster-api/api/v1beta1.ControlPlaneClass"), + }, + }, + "controlPlaneClasses": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-list-map-keys": []interface{}{ + "class", + }, + "x-kubernetes-list-type": "map", + }, + }, + SchemaProps: spec.SchemaProps{ + Description: "controlPlaneClasses is a list of named control plane classes that can be referenced from the Cluster topology. Each class defines a distinct control plane configuration. The class name MUST be unique within this list. When classes is defined, the Cluster topology can reference a specific control plane class by name. syself new field.", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("sigs.k8s.io/cluster-api/api/v1beta1.ControlPlaneClass"), + }, + }, + }, }, }, "workers": { @@ -511,7 +532,7 @@ func schema_sigsk8sio_cluster_api_api_v1beta1_ClusterClassSpec(ref common.Refere }, }, Dependencies: []string{ - "sigs.k8s.io/cluster-api/api/v1beta1.ClusterAvailabilityGate", "sigs.k8s.io/cluster-api/api/v1beta1.ClusterClassPatch", "sigs.k8s.io/cluster-api/api/v1beta1.ClusterClassVariable", "sigs.k8s.io/cluster-api/api/v1beta1.ControlPlaneTopologyClass", "sigs.k8s.io/cluster-api/api/v1beta1.InfrastructureNamingStrategy", "sigs.k8s.io/cluster-api/api/v1beta1.LocalObjectTemplate", "sigs.k8s.io/cluster-api/api/v1beta1.WorkersClass"}, + "sigs.k8s.io/cluster-api/api/v1beta1.ClusterAvailabilityGate", "sigs.k8s.io/cluster-api/api/v1beta1.ClusterClassPatch", "sigs.k8s.io/cluster-api/api/v1beta1.ClusterClassVariable", "sigs.k8s.io/cluster-api/api/v1beta1.ControlPlaneClass", "sigs.k8s.io/cluster-api/api/v1beta1.InfrastructureNamingStrategy", "sigs.k8s.io/cluster-api/api/v1beta1.LocalObjectTemplate", "sigs.k8s.io/cluster-api/api/v1beta1.WorkersClass"}, } } @@ -1375,7 +1396,7 @@ func schema_sigsk8sio_cluster_api_api_v1beta1_ControlPlaneTopology(ref common.Re }, "class": { SchemaProps: spec.SchemaProps{ - Description: "class is the name of the ControlPlaneClass used to create the set of control plane nodes. This should match one of the control plane classes defined in the ClusterClass object.", + Description: "class is the name of the ControlPlaneClass used to create the set of control plane nodes. This should match one of the control plane classes defined in the ClusterClass object. syself new field.", Type: []string{"string"}, Format: "", }, @@ -1447,122 +1468,6 @@ func schema_sigsk8sio_cluster_api_api_v1beta1_ControlPlaneTopology(ref common.Re } } -func schema_sigsk8sio_cluster_api_api_v1beta1_ControlPlaneTopologyClass(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Description: "ControlPlaneTopologyClass wraps the control plane configuration, supporting both the original single control plane definition and multiple control plane classes.", - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "metadata": { - SchemaProps: spec.SchemaProps{ - Description: "metadata is the metadata applied to the ControlPlane and the Machines of the ControlPlane if the ControlPlaneTemplate referenced is machine based. If not, it is applied only to the ControlPlane. At runtime this metadata is merged with the corresponding metadata from the topology.\n\nThis field is supported if and only if the control plane provider template referenced is Machine based.", - Default: map[string]interface{}{}, - Ref: ref("sigs.k8s.io/cluster-api/api/v1beta1.ObjectMeta"), - }, - }, - "class": { - SchemaProps: spec.SchemaProps{ - Description: "class denotes a type of control-plane node present in the cluster. When used in ControlPlaneTopologyClass.Classes, this name MUST be unique within the list and can be referenced from the Cluster topology.", - Type: []string{"string"}, - Format: "", - }, - }, - "ref": { - SchemaProps: spec.SchemaProps{ - Description: "ref is a required reference to a custom resource offered by a provider.", - Ref: ref("k8s.io/api/core/v1.ObjectReference"), - }, - }, - "machineInfrastructure": { - SchemaProps: spec.SchemaProps{ - Description: "machineInfrastructure defines the metadata and infrastructure information for control plane machines.\n\nThis field is supported if and only if the control plane provider template referenced above is Machine based and supports setting replicas.", - Ref: ref("sigs.k8s.io/cluster-api/api/v1beta1.LocalObjectTemplate"), - }, - }, - "machineHealthCheck": { - SchemaProps: spec.SchemaProps{ - Description: "machineHealthCheck defines a MachineHealthCheck for this ControlPlaneClass. This field is supported if and only if the ControlPlane provider template referenced above is Machine based and supports setting replicas.", - Ref: ref("sigs.k8s.io/cluster-api/api/v1beta1.MachineHealthCheckClass"), - }, - }, - "namingStrategy": { - SchemaProps: spec.SchemaProps{ - Description: "namingStrategy allows changing the naming pattern used when creating the control plane provider object.", - Ref: ref("sigs.k8s.io/cluster-api/api/v1beta1.ControlPlaneClassNamingStrategy"), - }, - }, - "nodeDrainTimeout": { - SchemaProps: spec.SchemaProps{ - Description: "nodeDrainTimeout is the total amount of time that the controller will spend on draining a node. The default value is 0, meaning that the node can be drained without any time limitations. NOTE: NodeDrainTimeout is different from `kubectl drain --timeout` NOTE: This value can be overridden while defining a Cluster.Topology.", - Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.Duration"), - }, - }, - "nodeVolumeDetachTimeout": { - SchemaProps: spec.SchemaProps{ - Description: "nodeVolumeDetachTimeout is the total amount of time that the controller will spend on waiting for all volumes to be detached. The default value is 0, meaning that the volumes can be detached without any time limitations. NOTE: This value can be overridden while defining a Cluster.Topology.", - Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.Duration"), - }, - }, - "nodeDeletionTimeout": { - SchemaProps: spec.SchemaProps{ - Description: "nodeDeletionTimeout defines how long the controller will attempt to delete the Node that the Machine hosts after the Machine is marked for deletion. A duration of 0 will retry deletion indefinitely. Defaults to 10 seconds. NOTE: This value can be overridden while defining a Cluster.Topology.", - Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.Duration"), - }, - }, - "readinessGates": { - VendorExtensible: spec.VendorExtensible{ - Extensions: spec.Extensions{ - "x-kubernetes-list-map-keys": []interface{}{ - "conditionType", - }, - "x-kubernetes-list-type": "map", - }, - }, - SchemaProps: spec.SchemaProps{ - Description: "readinessGates specifies additional conditions to include when evaluating Machine Ready condition.\n\nThis field can be used e.g. to instruct the machine controller to include in the computation for Machine's ready computation a condition, managed by an external controllers, reporting the status of special software/hardware installed on the Machine.\n\nNOTE: This field is considered only for computing v1beta2 conditions. NOTE: If a Cluster defines a custom list of readinessGates for the control plane, such list overrides readinessGates defined in this field. NOTE: Specific control plane provider implementations might automatically extend the list of readinessGates; e.g. the kubeadm control provider adds ReadinessGates for the APIServerPodHealthy, SchedulerPodHealthy conditions, etc.", - Type: []string{"array"}, - Items: &spec.SchemaOrArray{ - Schema: &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("sigs.k8s.io/cluster-api/api/v1beta1.MachineReadinessGate"), - }, - }, - }, - }, - }, - "classes": { - VendorExtensible: spec.VendorExtensible{ - Extensions: spec.Extensions{ - "x-kubernetes-list-map-keys": []interface{}{ - "class", - }, - "x-kubernetes-list-type": "map", - }, - }, - SchemaProps: spec.SchemaProps{ - Description: "classes is a list of named control plane classes that can be referenced from the Cluster topology. Each class defines a distinct control plane configuration. The class name MUST be unique within this list. When classes is defined, the Cluster topology can reference a specific control plane class by name.", - Type: []string{"array"}, - Items: &spec.SchemaOrArray{ - Schema: &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("sigs.k8s.io/cluster-api/api/v1beta1.ControlPlaneClass"), - }, - }, - }, - }, - }, - }, - Required: []string{"ref"}, - }, - }, - Dependencies: []string{ - "k8s.io/api/core/v1.ObjectReference", "k8s.io/apimachinery/pkg/apis/meta/v1.Duration", "sigs.k8s.io/cluster-api/api/v1beta1.ControlPlaneClass", "sigs.k8s.io/cluster-api/api/v1beta1.ControlPlaneClassNamingStrategy", "sigs.k8s.io/cluster-api/api/v1beta1.LocalObjectTemplate", "sigs.k8s.io/cluster-api/api/v1beta1.MachineHealthCheckClass", "sigs.k8s.io/cluster-api/api/v1beta1.MachineReadinessGate", "sigs.k8s.io/cluster-api/api/v1beta1.ObjectMeta"}, - } -} - func schema_sigsk8sio_cluster_api_api_v1beta1_ControlPlaneVariables(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ diff --git a/config/crd/bases/cluster.x-k8s.io_clusterclasses.yaml b/config/crd/bases/cluster.x-k8s.io_clusterclasses.yaml index f5e7a68cd9b6..8b869b64e6e3 100644 --- a/config/crd/bases/cluster.x-k8s.io_clusterclasses.yaml +++ b/config/crd/bases/cluster.x-k8s.io_clusterclasses.yaml @@ -498,370 +498,6 @@ spec: maxLength: 256 minLength: 1 type: string - classes: - description: |- - classes is a list of named control plane classes that can be referenced - from the Cluster topology. Each class defines a distinct control plane - configuration. The class name MUST be unique within this list. - When classes is defined, the Cluster topology can reference a specific - control plane class by name. - items: - description: ControlPlaneClass defines the class for the control - plane. - properties: - class: - description: |- - class denotes a type of control-plane node present in the cluster. - When used in ControlPlaneTopologyClass.Classes, this name MUST be unique - within the list and can be referenced from the Cluster topology. - maxLength: 256 - minLength: 1 - type: string - machineHealthCheck: - description: |- - machineHealthCheck defines a MachineHealthCheck for this ControlPlaneClass. - This field is supported if and only if the ControlPlane provider template - referenced above is Machine based and supports setting replicas. - properties: - maxUnhealthy: - anyOf: - - type: integer - - type: string - description: |- - maxUnhealthy specifies the maximum number of unhealthy machines allowed. - Any further remediation is only allowed if at most "maxUnhealthy" machines selected by - "selector" are not healthy. - x-kubernetes-int-or-string: true - nodeStartupTimeout: - description: |- - nodeStartupTimeout allows to set the maximum time for MachineHealthCheck - to consider a Machine unhealthy if a corresponding Node isn't associated - through a `Spec.ProviderID` field. - - The duration set in this field is compared to the greatest of: - - Cluster's infrastructure ready condition timestamp (if and when available) - - Control Plane's initialized condition timestamp (if and when available) - - Machine's infrastructure ready condition timestamp (if and when available) - - Machine's metadata creation timestamp - - Defaults to 10 minutes. - If you wish to disable this feature, set the value explicitly to 0. - type: string - remediationTemplate: - description: |- - remediationTemplate is a reference to a remediation template - provided by an infrastructure provider. - - This field is completely optional, when filled, the MachineHealthCheck controller - creates a new object from the template referenced and hands off remediation of the machine to - a controller that lives outside of Cluster API. - properties: - apiVersion: - description: API version of the referent. - type: string - fieldPath: - description: |- - If referring to a piece of an object instead of an entire object, this string - should contain a valid JSON/Go field access statement, such as desiredState.manifest.containers[2]. - For example, if the object reference is to a container within a pod, this would take on a value like: - "spec.containers{name}" (where "name" refers to the name of the container that triggered - the event) or if no container name is specified "spec.containers[2]" (container with - index 2 in this pod). This syntax is chosen only to have some well-defined way of - referencing a part of an object. - type: string - kind: - description: |- - Kind of the referent. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds - type: string - name: - description: |- - Name of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - type: string - namespace: - description: |- - Namespace of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/ - type: string - resourceVersion: - description: |- - Specific resourceVersion to which this reference is made, if any. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency - type: string - uid: - description: |- - UID of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids - type: string - type: object - x-kubernetes-map-type: atomic - unhealthyConditions: - description: |- - unhealthyConditions contains a list of the conditions that determine - whether a node is considered unhealthy. The conditions are combined in a - logical OR, i.e. if any of the conditions is met, the node is unhealthy. - items: - description: |- - UnhealthyCondition represents a Node condition type and value with a timeout - specified as a duration. When the named condition has been in the given - status for at least the timeout value, a node is considered unhealthy. - properties: - status: - description: status of the condition, one of True, - False, Unknown. - minLength: 1 - type: string - timeout: - description: |- - timeout is the duration that a node must be in a given status for, - after which the node is considered unhealthy. - For example, with a value of "1h", the node must match the status - for at least 1 hour before being considered unhealthy. - type: string - type: - description: type of Node condition - minLength: 1 - type: string - required: - - status - - timeout - - type - type: object - maxItems: 100 - type: array - unhealthyRange: - description: |- - unhealthyRange specifies the range of unhealthy machines allowed. - Any further remediation is only allowed if the number of machines selected by "selector" as not healthy - is within the range of "unhealthyRange". Takes precedence over maxUnhealthy. - Eg. "[3-5]" - This means that remediation will be allowed only when: - (a) there are at least 3 unhealthy machines (and) - (b) there are at most 5 unhealthy machines - maxLength: 32 - minLength: 1 - pattern: ^\[[0-9]+-[0-9]+\]$ - type: string - type: object - machineInfrastructure: - description: |- - machineInfrastructure defines the metadata and infrastructure information - for control plane machines. - - This field is supported if and only if the control plane provider template - referenced above is Machine based and supports setting replicas. - properties: - ref: - description: |- - ref is a required reference to a custom resource - offered by a provider. - properties: - apiVersion: - description: API version of the referent. - type: string - fieldPath: - description: |- - If referring to a piece of an object instead of an entire object, this string - should contain a valid JSON/Go field access statement, such as desiredState.manifest.containers[2]. - For example, if the object reference is to a container within a pod, this would take on a value like: - "spec.containers{name}" (where "name" refers to the name of the container that triggered - the event) or if no container name is specified "spec.containers[2]" (container with - index 2 in this pod). This syntax is chosen only to have some well-defined way of - referencing a part of an object. - type: string - kind: - description: |- - Kind of the referent. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds - type: string - name: - description: |- - Name of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - type: string - namespace: - description: |- - Namespace of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/ - type: string - resourceVersion: - description: |- - Specific resourceVersion to which this reference is made, if any. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency - type: string - uid: - description: |- - UID of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids - type: string - type: object - x-kubernetes-map-type: atomic - required: - - ref - type: object - metadata: - description: |- - metadata is the metadata applied to the ControlPlane and the Machines of the ControlPlane - if the ControlPlaneTemplate referenced is machine based. If not, it is applied only to the - ControlPlane. - At runtime this metadata is merged with the corresponding metadata from the topology. - - This field is supported if and only if the control plane provider template - referenced is Machine based. - properties: - annotations: - additionalProperties: - type: string - description: |- - annotations is an unstructured key value map stored with a resource that may be - set by external tools to store and retrieve arbitrary metadata. They are not - queryable and should be preserved when modifying objects. - More info: http://kubernetes.io/docs/user-guide/annotations - type: object - labels: - additionalProperties: - type: string - description: |- - labels is a map of string keys and values that can be used to organize and categorize - (scope and select) objects. May match selectors of replication controllers - and services. - More info: http://kubernetes.io/docs/user-guide/labels - type: object - type: object - namingStrategy: - description: namingStrategy allows changing the naming pattern - used when creating the control plane provider object. - properties: - template: - description: |- - template defines the template to use for generating the name of the ControlPlane object. - If not defined, it will fallback to `{{ .cluster.name }}-{{ .random }}`. - If the templated string exceeds 63 characters, it will be trimmed to 58 characters and will - get concatenated with a random suffix of length 5. - The templating mechanism provides the following arguments: - * `.cluster.name`: The name of the cluster object. - * `.random`: A random alphanumeric string, without vowels, of length 5. - maxLength: 1024 - minLength: 1 - type: string - type: object - nodeDeletionTimeout: - description: |- - nodeDeletionTimeout defines how long the controller will attempt to delete the Node that the Machine - hosts after the Machine is marked for deletion. A duration of 0 will retry deletion indefinitely. - Defaults to 10 seconds. - NOTE: This value can be overridden while defining a Cluster.Topology. - type: string - nodeDrainTimeout: - description: |- - nodeDrainTimeout is the total amount of time that the controller will spend on draining a node. - The default value is 0, meaning that the node can be drained without any time limitations. - NOTE: NodeDrainTimeout is different from `kubectl drain --timeout` - NOTE: This value can be overridden while defining a Cluster.Topology. - type: string - nodeVolumeDetachTimeout: - description: |- - nodeVolumeDetachTimeout is the total amount of time that the controller will spend on waiting for all volumes - to be detached. The default value is 0, meaning that the volumes can be detached without any time limitations. - NOTE: This value can be overridden while defining a Cluster.Topology. - type: string - readinessGates: - description: |- - readinessGates specifies additional conditions to include when evaluating Machine Ready condition. - - This field can be used e.g. to instruct the machine controller to include in the computation for Machine's ready - computation a condition, managed by an external controllers, reporting the status of special software/hardware installed on the Machine. - - NOTE: This field is considered only for computing v1beta2 conditions. - NOTE: If a Cluster defines a custom list of readinessGates for the control plane, - such list overrides readinessGates defined in this field. - NOTE: Specific control plane provider implementations might automatically extend the list of readinessGates; - e.g. the kubeadm control provider adds ReadinessGates for the APIServerPodHealthy, SchedulerPodHealthy conditions, etc. - items: - description: MachineReadinessGate contains the type of - a Machine condition to be used as a readiness gate. - properties: - conditionType: - description: |- - conditionType refers to a condition with matching type in the Machine's condition list. - If the conditions doesn't exist, it will be treated as unknown. - Note: Both Cluster API conditions or conditions added by 3rd party controllers can be used as readiness gates. - maxLength: 316 - minLength: 1 - pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ - type: string - polarity: - description: |- - polarity of the conditionType specified in this readinessGate. - Valid values are Positive, Negative and omitted. - When omitted, the default behaviour will be Positive. - A positive polarity means that the condition should report a true status under normal conditions. - A negative polarity means that the condition should report a false status under normal conditions. - enum: - - Positive - - Negative - type: string - required: - - conditionType - type: object - maxItems: 32 - type: array - x-kubernetes-list-map-keys: - - conditionType - x-kubernetes-list-type: map - ref: - description: |- - ref is a required reference to a custom resource - offered by a provider. - properties: - apiVersion: - description: API version of the referent. - type: string - fieldPath: - description: |- - If referring to a piece of an object instead of an entire object, this string - should contain a valid JSON/Go field access statement, such as desiredState.manifest.containers[2]. - For example, if the object reference is to a container within a pod, this would take on a value like: - "spec.containers{name}" (where "name" refers to the name of the container that triggered - the event) or if no container name is specified "spec.containers[2]" (container with - index 2 in this pod). This syntax is chosen only to have some well-defined way of - referencing a part of an object. - type: string - kind: - description: |- - Kind of the referent. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds - type: string - name: - description: |- - Name of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - type: string - namespace: - description: |- - Namespace of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/ - type: string - resourceVersion: - description: |- - Specific resourceVersion to which this reference is made, if any. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency - type: string - uid: - description: |- - UID of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids - type: string - type: object - x-kubernetes-map-type: atomic - required: - - ref - type: object - maxItems: 100 - type: array - x-kubernetes-list-map-keys: - - class - x-kubernetes-list-type: map machineHealthCheck: description: |- machineHealthCheck defines a MachineHealthCheck for this ControlPlaneClass. @@ -1202,6 +838,371 @@ spec: required: - ref type: object + controlPlaneClasses: + description: |- + controlPlaneClasses is a list of named control plane classes that can be referenced + from the Cluster topology. Each class defines a distinct control plane + configuration. The class name MUST be unique within this list. + When classes is defined, the Cluster topology can reference a specific + control plane class by name. + syself new field. + items: + description: ControlPlaneClass defines the class for the control + plane. + properties: + class: + description: |- + class denotes a type of control-plane node present in the cluster. + When used in ControlPlaneTopologyClass.Classes, this name MUST be unique + within the list and can be referenced from the Cluster topology. + maxLength: 256 + minLength: 1 + type: string + machineHealthCheck: + description: |- + machineHealthCheck defines a MachineHealthCheck for this ControlPlaneClass. + This field is supported if and only if the ControlPlane provider template + referenced above is Machine based and supports setting replicas. + properties: + maxUnhealthy: + anyOf: + - type: integer + - type: string + description: |- + maxUnhealthy specifies the maximum number of unhealthy machines allowed. + Any further remediation is only allowed if at most "maxUnhealthy" machines selected by + "selector" are not healthy. + x-kubernetes-int-or-string: true + nodeStartupTimeout: + description: |- + nodeStartupTimeout allows to set the maximum time for MachineHealthCheck + to consider a Machine unhealthy if a corresponding Node isn't associated + through a `Spec.ProviderID` field. + + The duration set in this field is compared to the greatest of: + - Cluster's infrastructure ready condition timestamp (if and when available) + - Control Plane's initialized condition timestamp (if and when available) + - Machine's infrastructure ready condition timestamp (if and when available) + - Machine's metadata creation timestamp + + Defaults to 10 minutes. + If you wish to disable this feature, set the value explicitly to 0. + type: string + remediationTemplate: + description: |- + remediationTemplate is a reference to a remediation template + provided by an infrastructure provider. + + This field is completely optional, when filled, the MachineHealthCheck controller + creates a new object from the template referenced and hands off remediation of the machine to + a controller that lives outside of Cluster API. + properties: + apiVersion: + description: API version of the referent. + type: string + fieldPath: + description: |- + If referring to a piece of an object instead of an entire object, this string + should contain a valid JSON/Go field access statement, such as desiredState.manifest.containers[2]. + For example, if the object reference is to a container within a pod, this would take on a value like: + "spec.containers{name}" (where "name" refers to the name of the container that triggered + the event) or if no container name is specified "spec.containers[2]" (container with + index 2 in this pod). This syntax is chosen only to have some well-defined way of + referencing a part of an object. + type: string + kind: + description: |- + Kind of the referent. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + namespace: + description: |- + Namespace of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/ + type: string + resourceVersion: + description: |- + Specific resourceVersion to which this reference is made, if any. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency + type: string + uid: + description: |- + UID of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids + type: string + type: object + x-kubernetes-map-type: atomic + unhealthyConditions: + description: |- + unhealthyConditions contains a list of the conditions that determine + whether a node is considered unhealthy. The conditions are combined in a + logical OR, i.e. if any of the conditions is met, the node is unhealthy. + items: + description: |- + UnhealthyCondition represents a Node condition type and value with a timeout + specified as a duration. When the named condition has been in the given + status for at least the timeout value, a node is considered unhealthy. + properties: + status: + description: status of the condition, one of True, + False, Unknown. + minLength: 1 + type: string + timeout: + description: |- + timeout is the duration that a node must be in a given status for, + after which the node is considered unhealthy. + For example, with a value of "1h", the node must match the status + for at least 1 hour before being considered unhealthy. + type: string + type: + description: type of Node condition + minLength: 1 + type: string + required: + - status + - timeout + - type + type: object + maxItems: 100 + type: array + unhealthyRange: + description: |- + unhealthyRange specifies the range of unhealthy machines allowed. + Any further remediation is only allowed if the number of machines selected by "selector" as not healthy + is within the range of "unhealthyRange". Takes precedence over maxUnhealthy. + Eg. "[3-5]" - This means that remediation will be allowed only when: + (a) there are at least 3 unhealthy machines (and) + (b) there are at most 5 unhealthy machines + maxLength: 32 + minLength: 1 + pattern: ^\[[0-9]+-[0-9]+\]$ + type: string + type: object + machineInfrastructure: + description: |- + machineInfrastructure defines the metadata and infrastructure information + for control plane machines. + + This field is supported if and only if the control plane provider template + referenced above is Machine based and supports setting replicas. + properties: + ref: + description: |- + ref is a required reference to a custom resource + offered by a provider. + properties: + apiVersion: + description: API version of the referent. + type: string + fieldPath: + description: |- + If referring to a piece of an object instead of an entire object, this string + should contain a valid JSON/Go field access statement, such as desiredState.manifest.containers[2]. + For example, if the object reference is to a container within a pod, this would take on a value like: + "spec.containers{name}" (where "name" refers to the name of the container that triggered + the event) or if no container name is specified "spec.containers[2]" (container with + index 2 in this pod). This syntax is chosen only to have some well-defined way of + referencing a part of an object. + type: string + kind: + description: |- + Kind of the referent. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + namespace: + description: |- + Namespace of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/ + type: string + resourceVersion: + description: |- + Specific resourceVersion to which this reference is made, if any. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency + type: string + uid: + description: |- + UID of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids + type: string + type: object + x-kubernetes-map-type: atomic + required: + - ref + type: object + metadata: + description: |- + metadata is the metadata applied to the ControlPlane and the Machines of the ControlPlane + if the ControlPlaneTemplate referenced is machine based. If not, it is applied only to the + ControlPlane. + At runtime this metadata is merged with the corresponding metadata from the topology. + + This field is supported if and only if the control plane provider template + referenced is Machine based. + properties: + annotations: + additionalProperties: + type: string + description: |- + annotations is an unstructured key value map stored with a resource that may be + set by external tools to store and retrieve arbitrary metadata. They are not + queryable and should be preserved when modifying objects. + More info: http://kubernetes.io/docs/user-guide/annotations + type: object + labels: + additionalProperties: + type: string + description: |- + labels is a map of string keys and values that can be used to organize and categorize + (scope and select) objects. May match selectors of replication controllers + and services. + More info: http://kubernetes.io/docs/user-guide/labels + type: object + type: object + namingStrategy: + description: namingStrategy allows changing the naming pattern + used when creating the control plane provider object. + properties: + template: + description: |- + template defines the template to use for generating the name of the ControlPlane object. + If not defined, it will fallback to `{{ .cluster.name }}-{{ .random }}`. + If the templated string exceeds 63 characters, it will be trimmed to 58 characters and will + get concatenated with a random suffix of length 5. + The templating mechanism provides the following arguments: + * `.cluster.name`: The name of the cluster object. + * `.random`: A random alphanumeric string, without vowels, of length 5. + maxLength: 1024 + minLength: 1 + type: string + type: object + nodeDeletionTimeout: + description: |- + nodeDeletionTimeout defines how long the controller will attempt to delete the Node that the Machine + hosts after the Machine is marked for deletion. A duration of 0 will retry deletion indefinitely. + Defaults to 10 seconds. + NOTE: This value can be overridden while defining a Cluster.Topology. + type: string + nodeDrainTimeout: + description: |- + nodeDrainTimeout is the total amount of time that the controller will spend on draining a node. + The default value is 0, meaning that the node can be drained without any time limitations. + NOTE: NodeDrainTimeout is different from `kubectl drain --timeout` + NOTE: This value can be overridden while defining a Cluster.Topology. + type: string + nodeVolumeDetachTimeout: + description: |- + nodeVolumeDetachTimeout is the total amount of time that the controller will spend on waiting for all volumes + to be detached. The default value is 0, meaning that the volumes can be detached without any time limitations. + NOTE: This value can be overridden while defining a Cluster.Topology. + type: string + readinessGates: + description: |- + readinessGates specifies additional conditions to include when evaluating Machine Ready condition. + + This field can be used e.g. to instruct the machine controller to include in the computation for Machine's ready + computation a condition, managed by an external controllers, reporting the status of special software/hardware installed on the Machine. + + NOTE: This field is considered only for computing v1beta2 conditions. + NOTE: If a Cluster defines a custom list of readinessGates for the control plane, + such list overrides readinessGates defined in this field. + NOTE: Specific control plane provider implementations might automatically extend the list of readinessGates; + e.g. the kubeadm control provider adds ReadinessGates for the APIServerPodHealthy, SchedulerPodHealthy conditions, etc. + items: + description: MachineReadinessGate contains the type of a Machine + condition to be used as a readiness gate. + properties: + conditionType: + description: |- + conditionType refers to a condition with matching type in the Machine's condition list. + If the conditions doesn't exist, it will be treated as unknown. + Note: Both Cluster API conditions or conditions added by 3rd party controllers can be used as readiness gates. + maxLength: 316 + minLength: 1 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + polarity: + description: |- + polarity of the conditionType specified in this readinessGate. + Valid values are Positive, Negative and omitted. + When omitted, the default behaviour will be Positive. + A positive polarity means that the condition should report a true status under normal conditions. + A negative polarity means that the condition should report a false status under normal conditions. + enum: + - Positive + - Negative + type: string + required: + - conditionType + type: object + maxItems: 32 + type: array + x-kubernetes-list-map-keys: + - conditionType + x-kubernetes-list-type: map + ref: + description: |- + ref is a required reference to a custom resource + offered by a provider. + properties: + apiVersion: + description: API version of the referent. + type: string + fieldPath: + description: |- + If referring to a piece of an object instead of an entire object, this string + should contain a valid JSON/Go field access statement, such as desiredState.manifest.containers[2]. + For example, if the object reference is to a container within a pod, this would take on a value like: + "spec.containers{name}" (where "name" refers to the name of the container that triggered + the event) or if no container name is specified "spec.containers[2]" (container with + index 2 in this pod). This syntax is chosen only to have some well-defined way of + referencing a part of an object. + type: string + kind: + description: |- + Kind of the referent. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + namespace: + description: |- + Namespace of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/ + type: string + resourceVersion: + description: |- + Specific resourceVersion to which this reference is made, if any. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency + type: string + uid: + description: |- + UID of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids + type: string + type: object + x-kubernetes-map-type: atomic + required: + - ref + type: object + maxItems: 100 + type: array + x-kubernetes-list-map-keys: + - class + x-kubernetes-list-type: map infrastructure: description: |- infrastructure is a reference to a provider-specific template that holds diff --git a/config/crd/bases/cluster.x-k8s.io_clusters.yaml b/config/crd/bases/cluster.x-k8s.io_clusters.yaml index 43044564a2bc..a843104071c4 100644 --- a/config/crd/bases/cluster.x-k8s.io_clusters.yaml +++ b/config/crd/bases/cluster.x-k8s.io_clusters.yaml @@ -983,6 +983,7 @@ spec: description: |- class is the name of the ControlPlaneClass used to create the set of control plane nodes. This should match one of the control plane classes defined in the ClusterClass object. + syself new field. maxLength: 256 minLength: 1 type: string diff --git a/internal/apis/core/v1alpha4/zz_generated.conversion.go b/internal/apis/core/v1alpha4/zz_generated.conversion.go index 60940d560467..a9ab2137ec6b 100644 --- a/internal/apis/core/v1alpha4/zz_generated.conversion.go +++ b/internal/apis/core/v1alpha4/zz_generated.conversion.go @@ -627,7 +627,7 @@ func autoConvert_v1alpha4_ClusterClassSpec_To_v1beta1_ClusterClassSpec(in *Clust if err := Convert_v1alpha4_LocalObjectTemplate_To_v1beta1_LocalObjectTemplate(&in.Infrastructure, &out.Infrastructure, s); err != nil { return err } - if err := Convert_v1alpha4_ControlPlaneClass_To_v1beta1_ControlPlaneTopologyClass(&in.ControlPlane, &out.ControlPlane, s); err != nil { + if err := Convert_v1alpha4_ControlPlaneClass_To_v1beta1_ControlPlaneClass(&in.ControlPlane, &out.ControlPlane, s); err != nil { return err } if err := Convert_v1alpha4_WorkersClass_To_v1beta1_WorkersClass(&in.Workers, &out.Workers, s); err != nil { @@ -647,9 +647,10 @@ func autoConvert_v1beta1_ClusterClassSpec_To_v1alpha4_ClusterClassSpec(in *v1bet return err } // WARNING: in.InfrastructureNamingStrategy requires manual conversion: does not exist in peer-type - if err := Convert_v1beta1_ControlPlaneTopologyClass_To_v1alpha4_ControlPlaneClass(&in.ControlPlane, &out.ControlPlane, s); err != nil { + if err := Convert_v1beta1_ControlPlaneClass_To_v1alpha4_ControlPlaneClass(&in.ControlPlane, &out.ControlPlane, s); err != nil { return err } + // WARNING: in.ControlPlaneClasses requires manual conversion: does not exist in peer-type if err := Convert_v1beta1_WorkersClass_To_v1alpha4_WorkersClass(&in.Workers, &out.Workers, s); err != nil { return err } diff --git a/internal/controllers/clusterclass/clusterclass_controller.go b/internal/controllers/clusterclass/clusterclass_controller.go index 1091b0b8fba4..21c62efb4a14 100644 --- a/internal/controllers/clusterclass/clusterclass_controller.go +++ b/internal/controllers/clusterclass/clusterclass_controller.go @@ -225,7 +225,7 @@ func (r *Reconciler) reconcileExternalReferences(ctx context.Context, s *scope) } // collect the same for control-plane classes as well. - for _, cpClass := range clusterClass.Spec.ControlPlane.Classes { + for _, cpClass := range clusterClass.Spec.ControlPlaneClasses { if cpClass.Ref != nil { refs = append(refs, cpClass.Ref) } diff --git a/internal/controllers/topology/cluster/blueprint.go b/internal/controllers/topology/cluster/blueprint.go index b7d5fbda1eb0..96a588b195f0 100644 --- a/internal/controllers/topology/cluster/blueprint.go +++ b/internal/controllers/topology/cluster/blueprint.go @@ -137,13 +137,13 @@ func (r *Reconciler) getBlueprint(ctx context.Context, cluster *clusterv1.Cluste func resolveControlPlaneClass(cluster *clusterv1.Cluster, clusterClass *clusterv1.ClusterClass) (*clusterv1.ControlPlaneClass, error) { // If the topology doesn't specify a class, use the inline definition. if cluster.Spec.Topology.ControlPlane.Class == "" { - return &clusterClass.Spec.ControlPlane.ControlPlaneClass, nil + return &clusterClass.Spec.ControlPlane, nil } - // Look up the class from spec.controlPlane.classes. - for i := range clusterClass.Spec.ControlPlane.Classes { - if clusterClass.Spec.ControlPlane.Classes[i].Class == cluster.Spec.Topology.ControlPlane.Class { - return &clusterClass.Spec.ControlPlane.Classes[i], nil + // Look up the class from spec.controlPlaneClasses. + for i := range clusterClass.Spec.ControlPlaneClasses { + if clusterClass.Spec.ControlPlaneClasses[i].Class == cluster.Spec.Topology.ControlPlane.Class { + return &clusterClass.Spec.ControlPlaneClasses[i], nil } } diff --git a/internal/controllers/topology/cluster/reconcile_state_test.go b/internal/controllers/topology/cluster/reconcile_state_test.go index 9aa37913e63d..87dfd0bad3a6 100644 --- a/internal/controllers/topology/cluster/reconcile_state_test.go +++ b/internal/controllers/topology/cluster/reconcile_state_test.go @@ -1707,11 +1707,9 @@ func TestReconcileControlPlaneCleanup(t *testing.T) { s.Blueprint = &scope.ClusterBlueprint{ ClusterClass: &clusterv1.ClusterClass{ Spec: clusterv1.ClusterClassSpec{ - ControlPlane: clusterv1.ControlPlaneTopologyClass{ - ControlPlaneClass: clusterv1.ControlPlaneClass{ - MachineInfrastructure: &clusterv1.LocalObjectTemplate{ - Ref: contract.ObjToRef(infrastructureMachineTemplate), - }, + ControlPlane: clusterv1.ControlPlaneClass{ + MachineInfrastructure: &clusterv1.LocalObjectTemplate{ + Ref: contract.ObjToRef(infrastructureMachineTemplate), }, }, }, diff --git a/internal/topology/check/compatibility.go b/internal/topology/check/compatibility.go index ead9f4b130a0..08d36b696226 100644 --- a/internal/topology/check/compatibility.go +++ b/internal/topology/check/compatibility.go @@ -236,6 +236,25 @@ func MachineDeploymentClassesAreCompatible(current, desired *clusterv1.ClusterCl return allErrs } +// ControlPlaneClassesAreUnique checks that no two ControlPlaneClasses in a ClusterClass share a name. +func ControlPlaneClassesAreUnique(clusterClass *clusterv1.ClusterClass) field.ErrorList { + var allErrs field.ErrorList + classes := sets.Set[string]{} + for i, class := range clusterClass.Spec.ControlPlaneClasses { + if classes.Has(class.Class) { + allErrs = append(allErrs, + field.Invalid( + field.NewPath("spec", "controlplane", "classes").Index(i).Child("class"), + class.Class, + fmt.Sprintf("ControlPlane class must be unique. ControlPlane with class %q is defined more than once", class.Class), + ), + ) + } + classes.Insert(class.Class) + } + return allErrs +} + // MachineDeploymentClassesAreUnique checks that no two MachineDeploymentClasses in a ClusterClass share a name. func MachineDeploymentClassesAreUnique(clusterClass *clusterv1.ClusterClass) field.ErrorList { var allErrs field.ErrorList @@ -432,8 +451,24 @@ func ClusterClassReferencesAreValid(clusterClass *clusterv1.ClusterClass) field. field.NewPath("spec", "infrastructure"))...) allErrs = append(allErrs, LocalObjectTemplateIsValid(&clusterClass.Spec.ControlPlane.LocalObjectTemplate, clusterClass.Namespace, field.NewPath("spec", "controlPlane"))...) - if clusterClass.Spec.ControlPlane.MachineInfrastructure != nil { - allErrs = append(allErrs, LocalObjectTemplateIsValid(clusterClass.Spec.ControlPlane.MachineInfrastructure, clusterClass.Namespace, field.NewPath("spec", "controlPlane", "machineInfrastructure"))...) + + // validate the inline control plane definition. + cpPath := field.NewPath("spec", "controlPlane") + if clusterClass.Spec.ControlPlane.LocalObjectTemplate.Ref != nil { + allErrs = append(allErrs, LocalObjectTemplateIsValid(&clusterClass.Spec.ControlPlane.LocalObjectTemplate, clusterClass.Namespace, cpPath)...) + if clusterClass.Spec.ControlPlane.MachineInfrastructure != nil { + allErrs = append(allErrs, LocalObjectTemplateIsValid(clusterClass.Spec.ControlPlane.MachineInfrastructure, clusterClass.Namespace, cpPath.Child("machineInfrastructure"))...) + } + } + + // validate each named control plane class. + for i := range clusterClass.Spec.ControlPlaneClasses { + cpc := clusterClass.Spec.ControlPlaneClasses[i] + classPath := cpPath.Child("controlPlaneClasses").Index(i) + allErrs = append(allErrs, LocalObjectTemplateIsValid(&cpc.LocalObjectTemplate, clusterClass.Namespace, classPath)...) + if cpc.MachineInfrastructure != nil { + allErrs = append(allErrs, LocalObjectTemplateIsValid(cpc.MachineInfrastructure, clusterClass.Namespace, classPath.Child("machineInfrastructure"))...) + } } for i := range clusterClass.Spec.Workers.MachineDeployments { diff --git a/internal/topology/check/compatibility_test.go b/internal/topology/check/compatibility_test.go index 9b22f7c0aba3..96e9c1cee41d 100644 --- a/internal/topology/check/compatibility_test.go +++ b/internal/topology/check/compatibility_test.go @@ -993,6 +993,73 @@ func TestMachinePoolClassesAreCompatible(t *testing.T) { } } +func TestControlPlaneClassesAreUnique(t *testing.T) { + tests := []struct { + name string + clusterClass *clusterv1.ClusterClass + wantErr bool + }{ + { + name: "pass if ControlPlaneClasses are unique", + clusterClass: &clusterv1.ClusterClass{ + Spec: clusterv1.ClusterClassSpec{ + ControlPlaneClasses: []clusterv1.ControlPlaneClass{ + {Class: "aa"}, + {Class: "bb"}, + }, + }, + }, + wantErr: false, + }, + { + name: "pass if no ControlPlaneClasses are defined", + clusterClass: &clusterv1.ClusterClass{ + Spec: clusterv1.ClusterClassSpec{ + ControlPlane: clusterv1.ControlPlaneClass{}, + }, + }, + wantErr: false, + }, + { + name: "fail if ControlPlaneClasses are duplicated", + clusterClass: &clusterv1.ClusterClass{ + Spec: clusterv1.ClusterClassSpec{ + ControlPlaneClasses: []clusterv1.ControlPlaneClass{ + {Class: "aa"}, + {Class: "aa"}, + }, + }, + }, + wantErr: true, + }, + { + name: "fail if multiple ControlPlaneClasses are identical", + clusterClass: &clusterv1.ClusterClass{ + Spec: clusterv1.ClusterClassSpec{ + ControlPlaneClasses: []clusterv1.ControlPlaneClass{ + {Class: "aa"}, + {Class: "aa"}, + {Class: "aa"}, + {Class: "aa"}, + }, + }, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + allErrs := ControlPlaneClassesAreUnique(tt.clusterClass) + if tt.wantErr { + g.Expect(allErrs).ToNot(BeEmpty()) + return + } + g.Expect(allErrs).To(BeEmpty()) + }) + } +} + func TestMachineDeploymentClassesAreUnique(t *testing.T) { tests := []struct { name string diff --git a/internal/webhooks/patch_validation_test.go b/internal/webhooks/patch_validation_test.go index f13a1449168b..365e8dab5d76 100644 --- a/internal/webhooks/patch_validation_test.go +++ b/internal/webhooks/patch_validation_test.go @@ -43,13 +43,11 @@ func TestValidatePatches(t *testing.T) { name: "pass multiple patches that are correctly formatted", clusterClass: clusterv1.ClusterClass{ Spec: clusterv1.ClusterClassSpec{ - ControlPlane: clusterv1.ControlPlaneTopologyClass{ - ControlPlaneClass: clusterv1.ControlPlaneClass{ - LocalObjectTemplate: clusterv1.LocalObjectTemplate{ - Ref: &corev1.ObjectReference{ - APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", - Kind: "ControlPlaneTemplate", - }, + ControlPlane: clusterv1.ControlPlaneClass{ + LocalObjectTemplate: clusterv1.LocalObjectTemplate{ + Ref: &corev1.ObjectReference{ + APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", + Kind: "ControlPlaneTemplate", }, }, }, @@ -132,13 +130,11 @@ func TestValidatePatches(t *testing.T) { name: "error if patch name is empty", clusterClass: clusterv1.ClusterClass{ Spec: clusterv1.ClusterClassSpec{ - ControlPlane: clusterv1.ControlPlaneTopologyClass{ - ControlPlaneClass: clusterv1.ControlPlaneClass{ - LocalObjectTemplate: clusterv1.LocalObjectTemplate{ - Ref: &corev1.ObjectReference{ - APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", - Kind: "ControlPlaneTemplate", - }, + ControlPlane: clusterv1.ControlPlaneClass{ + LocalObjectTemplate: clusterv1.LocalObjectTemplate{ + Ref: &corev1.ObjectReference{ + APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", + Kind: "ControlPlaneTemplate", }, }, }, @@ -187,13 +183,11 @@ func TestValidatePatches(t *testing.T) { name: "error if patches name is not unique", clusterClass: clusterv1.ClusterClass{ Spec: clusterv1.ClusterClassSpec{ - ControlPlane: clusterv1.ControlPlaneTopologyClass{ - ControlPlaneClass: clusterv1.ControlPlaneClass{ - LocalObjectTemplate: clusterv1.LocalObjectTemplate{ - Ref: &corev1.ObjectReference{ - APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", - Kind: "ControlPlaneTemplate", - }, + ControlPlane: clusterv1.ControlPlaneClass{ + LocalObjectTemplate: clusterv1.LocalObjectTemplate{ + Ref: &corev1.ObjectReference{ + APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", + Kind: "ControlPlaneTemplate", }, }, }, @@ -276,13 +270,11 @@ func TestValidatePatches(t *testing.T) { name: "pass if enabledIf is a valid Go template", clusterClass: clusterv1.ClusterClass{ Spec: clusterv1.ClusterClassSpec{ - ControlPlane: clusterv1.ControlPlaneTopologyClass{ - ControlPlaneClass: clusterv1.ControlPlaneClass{ - LocalObjectTemplate: clusterv1.LocalObjectTemplate{ - Ref: &corev1.ObjectReference{ - APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", - Kind: "ControlPlaneTemplate", - }, + ControlPlane: clusterv1.ControlPlaneClass{ + LocalObjectTemplate: clusterv1.LocalObjectTemplate{ + Ref: &corev1.ObjectReference{ + APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", + Kind: "ControlPlaneTemplate", }, }, }, @@ -301,13 +293,11 @@ func TestValidatePatches(t *testing.T) { name: "error if enabledIf is an invalid Go template", clusterClass: clusterv1.ClusterClass{ Spec: clusterv1.ClusterClassSpec{ - ControlPlane: clusterv1.ControlPlaneTopologyClass{ - ControlPlaneClass: clusterv1.ControlPlaneClass{ - LocalObjectTemplate: clusterv1.LocalObjectTemplate{ - Ref: &corev1.ObjectReference{ - APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", - Kind: "ControlPlaneTemplate", - }, + ControlPlane: clusterv1.ControlPlaneClass{ + LocalObjectTemplate: clusterv1.LocalObjectTemplate{ + Ref: &corev1.ObjectReference{ + APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", + Kind: "ControlPlaneTemplate", }, }, }, @@ -326,13 +316,11 @@ func TestValidatePatches(t *testing.T) { name: "error if patch op is not \"add\" \"remove\" or \"replace\"", clusterClass: clusterv1.ClusterClass{ Spec: clusterv1.ClusterClassSpec{ - ControlPlane: clusterv1.ControlPlaneTopologyClass{ - ControlPlaneClass: clusterv1.ControlPlaneClass{ - LocalObjectTemplate: clusterv1.LocalObjectTemplate{ - Ref: &corev1.ObjectReference{ - APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", - Kind: "ControlPlaneTemplate", - }, + ControlPlane: clusterv1.ControlPlaneClass{ + LocalObjectTemplate: clusterv1.LocalObjectTemplate{ + Ref: &corev1.ObjectReference{ + APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", + Kind: "ControlPlaneTemplate", }, }, }, @@ -370,13 +358,11 @@ func TestValidatePatches(t *testing.T) { name: "error if jsonPath does not begin with \"/spec/\"", clusterClass: clusterv1.ClusterClass{ Spec: clusterv1.ClusterClassSpec{ - ControlPlane: clusterv1.ControlPlaneTopologyClass{ - ControlPlaneClass: clusterv1.ControlPlaneClass{ - LocalObjectTemplate: clusterv1.LocalObjectTemplate{ - Ref: &corev1.ObjectReference{ - APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", - Kind: "ControlPlaneTemplate", - }, + ControlPlane: clusterv1.ControlPlaneClass{ + LocalObjectTemplate: clusterv1.LocalObjectTemplate{ + Ref: &corev1.ObjectReference{ + APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", + Kind: "ControlPlaneTemplate", }, }, }, @@ -412,13 +398,11 @@ func TestValidatePatches(t *testing.T) { name: "pass if jsonPatch path uses a valid index for add i.e. 0", clusterClass: clusterv1.ClusterClass{ Spec: clusterv1.ClusterClassSpec{ - ControlPlane: clusterv1.ControlPlaneTopologyClass{ - ControlPlaneClass: clusterv1.ControlPlaneClass{ - LocalObjectTemplate: clusterv1.LocalObjectTemplate{ - Ref: &corev1.ObjectReference{ - APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", - Kind: "ControlPlaneTemplate", - }, + ControlPlane: clusterv1.ControlPlaneClass{ + LocalObjectTemplate: clusterv1.LocalObjectTemplate{ + Ref: &corev1.ObjectReference{ + APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", + Kind: "ControlPlaneTemplate", }, }, }, @@ -466,13 +450,11 @@ func TestValidatePatches(t *testing.T) { name: "error if jsonPatch path uses an invalid index for add i.e. a number greater than 0.", clusterClass: clusterv1.ClusterClass{ Spec: clusterv1.ClusterClassSpec{ - ControlPlane: clusterv1.ControlPlaneTopologyClass{ - ControlPlaneClass: clusterv1.ControlPlaneClass{ - LocalObjectTemplate: clusterv1.LocalObjectTemplate{ - Ref: &corev1.ObjectReference{ - APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", - Kind: "ControlPlaneTemplate", - }, + ControlPlane: clusterv1.ControlPlaneClass{ + LocalObjectTemplate: clusterv1.LocalObjectTemplate{ + Ref: &corev1.ObjectReference{ + APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", + Kind: "ControlPlaneTemplate", }, }, }, @@ -521,13 +503,11 @@ func TestValidatePatches(t *testing.T) { name: "error if jsonPatch path uses an invalid index for add i.e. 01", clusterClass: clusterv1.ClusterClass{ Spec: clusterv1.ClusterClassSpec{ - ControlPlane: clusterv1.ControlPlaneTopologyClass{ - ControlPlaneClass: clusterv1.ControlPlaneClass{ - LocalObjectTemplate: clusterv1.LocalObjectTemplate{ - Ref: &corev1.ObjectReference{ - APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", - Kind: "ControlPlaneTemplate", - }, + ControlPlane: clusterv1.ControlPlaneClass{ + LocalObjectTemplate: clusterv1.LocalObjectTemplate{ + Ref: &corev1.ObjectReference{ + APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", + Kind: "ControlPlaneTemplate", }, }, }, @@ -576,13 +556,11 @@ func TestValidatePatches(t *testing.T) { name: "error if jsonPatch path uses any index for remove i.e. 0 or -.", clusterClass: clusterv1.ClusterClass{ Spec: clusterv1.ClusterClassSpec{ - ControlPlane: clusterv1.ControlPlaneTopologyClass{ - ControlPlaneClass: clusterv1.ControlPlaneClass{ - LocalObjectTemplate: clusterv1.LocalObjectTemplate{ - Ref: &corev1.ObjectReference{ - APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", - Kind: "ControlPlaneTemplate", - }, + ControlPlane: clusterv1.ControlPlaneClass{ + LocalObjectTemplate: clusterv1.LocalObjectTemplate{ + Ref: &corev1.ObjectReference{ + APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", + Kind: "ControlPlaneTemplate", }, }, }, @@ -631,13 +609,11 @@ func TestValidatePatches(t *testing.T) { name: "error if jsonPatch path uses any index for replace i.e. 0", clusterClass: clusterv1.ClusterClass{ Spec: clusterv1.ClusterClassSpec{ - ControlPlane: clusterv1.ControlPlaneTopologyClass{ - ControlPlaneClass: clusterv1.ControlPlaneClass{ - LocalObjectTemplate: clusterv1.LocalObjectTemplate{ - Ref: &corev1.ObjectReference{ - APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", - Kind: "ControlPlaneTemplate", - }, + ControlPlane: clusterv1.ControlPlaneClass{ + LocalObjectTemplate: clusterv1.LocalObjectTemplate{ + Ref: &corev1.ObjectReference{ + APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", + Kind: "ControlPlaneTemplate", }, }, }, @@ -688,13 +664,11 @@ func TestValidatePatches(t *testing.T) { name: "error if jsonPatch has neither Value nor ValueFrom", clusterClass: clusterv1.ClusterClass{ Spec: clusterv1.ClusterClassSpec{ - ControlPlane: clusterv1.ControlPlaneTopologyClass{ - ControlPlaneClass: clusterv1.ControlPlaneClass{ - LocalObjectTemplate: clusterv1.LocalObjectTemplate{ - Ref: &corev1.ObjectReference{ - APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", - Kind: "ControlPlaneTemplate", - }, + ControlPlane: clusterv1.ControlPlaneClass{ + LocalObjectTemplate: clusterv1.LocalObjectTemplate{ + Ref: &corev1.ObjectReference{ + APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", + Kind: "ControlPlaneTemplate", }, }, }, @@ -730,13 +704,11 @@ func TestValidatePatches(t *testing.T) { name: "error if jsonPatch has both Value and ValueFrom", clusterClass: clusterv1.ClusterClass{ Spec: clusterv1.ClusterClassSpec{ - ControlPlane: clusterv1.ControlPlaneTopologyClass{ - ControlPlaneClass: clusterv1.ControlPlaneClass{ - LocalObjectTemplate: clusterv1.LocalObjectTemplate{ - Ref: &corev1.ObjectReference{ - APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", - Kind: "ControlPlaneTemplate", - }, + ControlPlane: clusterv1.ControlPlaneClass{ + LocalObjectTemplate: clusterv1.LocalObjectTemplate{ + Ref: &corev1.ObjectReference{ + APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", + Kind: "ControlPlaneTemplate", }, }, }, @@ -787,13 +759,11 @@ func TestValidatePatches(t *testing.T) { name: "pass if jsonPatch value is valid json literal", clusterClass: clusterv1.ClusterClass{ Spec: clusterv1.ClusterClassSpec{ - ControlPlane: clusterv1.ControlPlaneTopologyClass{ - ControlPlaneClass: clusterv1.ControlPlaneClass{ - LocalObjectTemplate: clusterv1.LocalObjectTemplate{ - Ref: &corev1.ObjectReference{ - APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", - Kind: "ControlPlaneTemplate", - }, + ControlPlane: clusterv1.ControlPlaneClass{ + LocalObjectTemplate: clusterv1.LocalObjectTemplate{ + Ref: &corev1.ObjectReference{ + APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", + Kind: "ControlPlaneTemplate", }, }, }, @@ -828,13 +798,11 @@ func TestValidatePatches(t *testing.T) { name: "pass if jsonPatch value is valid json", clusterClass: clusterv1.ClusterClass{ Spec: clusterv1.ClusterClassSpec{ - ControlPlane: clusterv1.ControlPlaneTopologyClass{ - ControlPlaneClass: clusterv1.ControlPlaneClass{ - LocalObjectTemplate: clusterv1.LocalObjectTemplate{ - Ref: &corev1.ObjectReference{ - APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", - Kind: "ControlPlaneTemplate", - }, + ControlPlane: clusterv1.ControlPlaneClass{ + LocalObjectTemplate: clusterv1.LocalObjectTemplate{ + Ref: &corev1.ObjectReference{ + APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", + Kind: "ControlPlaneTemplate", }, }, }, @@ -872,13 +840,11 @@ func TestValidatePatches(t *testing.T) { name: "pass if jsonPatch value is nil", clusterClass: clusterv1.ClusterClass{ Spec: clusterv1.ClusterClassSpec{ - ControlPlane: clusterv1.ControlPlaneTopologyClass{ - ControlPlaneClass: clusterv1.ControlPlaneClass{ - LocalObjectTemplate: clusterv1.LocalObjectTemplate{ - Ref: &corev1.ObjectReference{ - APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", - Kind: "ControlPlaneTemplate", - }, + ControlPlane: clusterv1.ControlPlaneClass{ + LocalObjectTemplate: clusterv1.LocalObjectTemplate{ + Ref: &corev1.ObjectReference{ + APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", + Kind: "ControlPlaneTemplate", }, }, }, @@ -915,13 +881,11 @@ func TestValidatePatches(t *testing.T) { name: "error if jsonPatch value is invalid json", clusterClass: clusterv1.ClusterClass{ Spec: clusterv1.ClusterClassSpec{ - ControlPlane: clusterv1.ControlPlaneTopologyClass{ - ControlPlaneClass: clusterv1.ControlPlaneClass{ - LocalObjectTemplate: clusterv1.LocalObjectTemplate{ - Ref: &corev1.ObjectReference{ - APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", - Kind: "ControlPlaneTemplate", - }, + ControlPlane: clusterv1.ControlPlaneClass{ + LocalObjectTemplate: clusterv1.LocalObjectTemplate{ + Ref: &corev1.ObjectReference{ + APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", + Kind: "ControlPlaneTemplate", }, }, }, @@ -962,13 +926,11 @@ func TestValidatePatches(t *testing.T) { name: "error if jsonPatch defines neither ValueFrom.Template nor ValueFrom.Variable", clusterClass: clusterv1.ClusterClass{ Spec: clusterv1.ClusterClassSpec{ - ControlPlane: clusterv1.ControlPlaneTopologyClass{ - ControlPlaneClass: clusterv1.ControlPlaneClass{ - LocalObjectTemplate: clusterv1.LocalObjectTemplate{ - Ref: &corev1.ObjectReference{ - APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", - Kind: "ControlPlaneTemplate", - }, + ControlPlane: clusterv1.ControlPlaneClass{ + LocalObjectTemplate: clusterv1.LocalObjectTemplate{ + Ref: &corev1.ObjectReference{ + APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", + Kind: "ControlPlaneTemplate", }, }, }, @@ -1003,13 +965,11 @@ func TestValidatePatches(t *testing.T) { name: "error if jsonPatch has both ValueFrom.Template and ValueFrom.Variable", clusterClass: clusterv1.ClusterClass{ Spec: clusterv1.ClusterClassSpec{ - ControlPlane: clusterv1.ControlPlaneTopologyClass{ - ControlPlaneClass: clusterv1.ControlPlaneClass{ - LocalObjectTemplate: clusterv1.LocalObjectTemplate{ - Ref: &corev1.ObjectReference{ - APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", - Kind: "ControlPlaneTemplate", - }, + ControlPlane: clusterv1.ControlPlaneClass{ + LocalObjectTemplate: clusterv1.LocalObjectTemplate{ + Ref: &corev1.ObjectReference{ + APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", + Kind: "ControlPlaneTemplate", }, }, }, @@ -1060,13 +1020,11 @@ func TestValidatePatches(t *testing.T) { name: "pass if jsonPatch defines a valid ValueFrom.Template", clusterClass: clusterv1.ClusterClass{ Spec: clusterv1.ClusterClassSpec{ - ControlPlane: clusterv1.ControlPlaneTopologyClass{ - ControlPlaneClass: clusterv1.ControlPlaneClass{ - LocalObjectTemplate: clusterv1.LocalObjectTemplate{ - Ref: &corev1.ObjectReference{ - APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", - Kind: "ControlPlaneTemplate", - }, + ControlPlane: clusterv1.ControlPlaneClass{ + LocalObjectTemplate: clusterv1.LocalObjectTemplate{ + Ref: &corev1.ObjectReference{ + APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", + Kind: "ControlPlaneTemplate", }, }, }, @@ -1114,13 +1072,11 @@ func TestValidatePatches(t *testing.T) { name: "error if jsonPatch defines an invalid ValueFrom.Template", clusterClass: clusterv1.ClusterClass{ Spec: clusterv1.ClusterClassSpec{ - ControlPlane: clusterv1.ControlPlaneTopologyClass{ - ControlPlaneClass: clusterv1.ControlPlaneClass{ - LocalObjectTemplate: clusterv1.LocalObjectTemplate{ - Ref: &corev1.ObjectReference{ - APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", - Kind: "ControlPlaneTemplate", - }, + ControlPlane: clusterv1.ControlPlaneClass{ + LocalObjectTemplate: clusterv1.LocalObjectTemplate{ + Ref: &corev1.ObjectReference{ + APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", + Kind: "ControlPlaneTemplate", }, }, }, @@ -1171,13 +1127,11 @@ func TestValidatePatches(t *testing.T) { name: "error if jsonPatch valueFrom uses a variable which is not defined", clusterClass: clusterv1.ClusterClass{ Spec: clusterv1.ClusterClassSpec{ - ControlPlane: clusterv1.ControlPlaneTopologyClass{ - ControlPlaneClass: clusterv1.ControlPlaneClass{ - LocalObjectTemplate: clusterv1.LocalObjectTemplate{ - Ref: &corev1.ObjectReference{ - APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", - Kind: "ControlPlaneTemplate", - }, + ControlPlane: clusterv1.ControlPlaneClass{ + LocalObjectTemplate: clusterv1.LocalObjectTemplate{ + Ref: &corev1.ObjectReference{ + APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", + Kind: "ControlPlaneTemplate", }, }, }, @@ -1225,13 +1179,11 @@ func TestValidatePatches(t *testing.T) { name: "pass if jsonPatch uses a user-defined variable which is defined", clusterClass: clusterv1.ClusterClass{ Spec: clusterv1.ClusterClassSpec{ - ControlPlane: clusterv1.ControlPlaneTopologyClass{ - ControlPlaneClass: clusterv1.ControlPlaneClass{ - LocalObjectTemplate: clusterv1.LocalObjectTemplate{ - Ref: &corev1.ObjectReference{ - APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", - Kind: "ControlPlaneTemplate", - }, + ControlPlane: clusterv1.ControlPlaneClass{ + LocalObjectTemplate: clusterv1.LocalObjectTemplate{ + Ref: &corev1.ObjectReference{ + APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", + Kind: "ControlPlaneTemplate", }, }, }, @@ -1279,13 +1231,11 @@ func TestValidatePatches(t *testing.T) { name: "pass if jsonPatch uses a nested user-defined variable which is defined", clusterClass: clusterv1.ClusterClass{ Spec: clusterv1.ClusterClassSpec{ - ControlPlane: clusterv1.ControlPlaneTopologyClass{ - ControlPlaneClass: clusterv1.ControlPlaneClass{ - LocalObjectTemplate: clusterv1.LocalObjectTemplate{ - Ref: &corev1.ObjectReference{ - APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", - Kind: "ControlPlaneTemplate", - }, + ControlPlane: clusterv1.ControlPlaneClass{ + LocalObjectTemplate: clusterv1.LocalObjectTemplate{ + Ref: &corev1.ObjectReference{ + APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", + Kind: "ControlPlaneTemplate", }, }, }, @@ -1338,13 +1288,11 @@ func TestValidatePatches(t *testing.T) { name: "error if jsonPatch uses a builtin variable which is not defined", clusterClass: clusterv1.ClusterClass{ Spec: clusterv1.ClusterClassSpec{ - ControlPlane: clusterv1.ControlPlaneTopologyClass{ - ControlPlaneClass: clusterv1.ControlPlaneClass{ - LocalObjectTemplate: clusterv1.LocalObjectTemplate{ - Ref: &corev1.ObjectReference{ - APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", - Kind: "ControlPlaneTemplate", - }, + ControlPlane: clusterv1.ControlPlaneClass{ + LocalObjectTemplate: clusterv1.LocalObjectTemplate{ + Ref: &corev1.ObjectReference{ + APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", + Kind: "ControlPlaneTemplate", }, }, }, @@ -1381,13 +1329,11 @@ func TestValidatePatches(t *testing.T) { name: "pass if jsonPatch uses a builtin variable which is defined", clusterClass: clusterv1.ClusterClass{ Spec: clusterv1.ClusterClassSpec{ - ControlPlane: clusterv1.ControlPlaneTopologyClass{ - ControlPlaneClass: clusterv1.ControlPlaneClass{ - LocalObjectTemplate: clusterv1.LocalObjectTemplate{ - Ref: &corev1.ObjectReference{ - APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", - Kind: "ControlPlaneTemplate", - }, + ControlPlane: clusterv1.ControlPlaneClass{ + LocalObjectTemplate: clusterv1.LocalObjectTemplate{ + Ref: &corev1.ObjectReference{ + APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", + Kind: "ControlPlaneTemplate", }, }, }, @@ -1427,13 +1373,11 @@ func TestValidatePatches(t *testing.T) { name: "pass if patch defines both external.generateExtension and external.validateExtension", clusterClass: clusterv1.ClusterClass{ Spec: clusterv1.ClusterClassSpec{ - ControlPlane: clusterv1.ControlPlaneTopologyClass{ - ControlPlaneClass: clusterv1.ControlPlaneClass{ - LocalObjectTemplate: clusterv1.LocalObjectTemplate{ - Ref: &corev1.ObjectReference{ - APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", - Kind: "ControlPlaneTemplate", - }, + ControlPlane: clusterv1.ControlPlaneClass{ + LocalObjectTemplate: clusterv1.LocalObjectTemplate{ + Ref: &corev1.ObjectReference{ + APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", + Kind: "ControlPlaneTemplate", }, }, }, @@ -1456,13 +1400,11 @@ func TestValidatePatches(t *testing.T) { name: "error if patch defines both external and RuntimeSDK is not enabled", clusterClass: clusterv1.ClusterClass{ Spec: clusterv1.ClusterClassSpec{ - ControlPlane: clusterv1.ControlPlaneTopologyClass{ - ControlPlaneClass: clusterv1.ControlPlaneClass{ - LocalObjectTemplate: clusterv1.LocalObjectTemplate{ - Ref: &corev1.ObjectReference{ - APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", - Kind: "ControlPlaneTemplate", - }, + ControlPlane: clusterv1.ControlPlaneClass{ + LocalObjectTemplate: clusterv1.LocalObjectTemplate{ + Ref: &corev1.ObjectReference{ + APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", + Kind: "ControlPlaneTemplate", }, }, }, @@ -1485,13 +1427,11 @@ func TestValidatePatches(t *testing.T) { name: "error if patch defines neither external.generateExtension nor external.validateExtension", clusterClass: clusterv1.ClusterClass{ Spec: clusterv1.ClusterClassSpec{ - ControlPlane: clusterv1.ControlPlaneTopologyClass{ - ControlPlaneClass: clusterv1.ControlPlaneClass{ - LocalObjectTemplate: clusterv1.LocalObjectTemplate{ - Ref: &corev1.ObjectReference{ - APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", - Kind: "ControlPlaneTemplate", - }, + ControlPlane: clusterv1.ControlPlaneClass{ + LocalObjectTemplate: clusterv1.LocalObjectTemplate{ + Ref: &corev1.ObjectReference{ + APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", + Kind: "ControlPlaneTemplate", }, }, }, @@ -1511,13 +1451,11 @@ func TestValidatePatches(t *testing.T) { name: "error if patch defines both external and definitions", clusterClass: clusterv1.ClusterClass{ Spec: clusterv1.ClusterClassSpec{ - ControlPlane: clusterv1.ControlPlaneTopologyClass{ - ControlPlaneClass: clusterv1.ControlPlaneClass{ - LocalObjectTemplate: clusterv1.LocalObjectTemplate{ - Ref: &corev1.ObjectReference{ - APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", - Kind: "ControlPlaneTemplate", - }, + ControlPlane: clusterv1.ControlPlaneClass{ + LocalObjectTemplate: clusterv1.LocalObjectTemplate{ + Ref: &corev1.ObjectReference{ + APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", + Kind: "ControlPlaneTemplate", }, }, }, @@ -1541,13 +1479,11 @@ func TestValidatePatches(t *testing.T) { name: "error if neither external nor definitions is defined", clusterClass: clusterv1.ClusterClass{ Spec: clusterv1.ClusterClassSpec{ - ControlPlane: clusterv1.ControlPlaneTopologyClass{ - ControlPlaneClass: clusterv1.ControlPlaneClass{ - LocalObjectTemplate: clusterv1.LocalObjectTemplate{ - Ref: &corev1.ObjectReference{ - APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", - Kind: "ControlPlaneTemplate", - }, + ControlPlane: clusterv1.ControlPlaneClass{ + LocalObjectTemplate: clusterv1.LocalObjectTemplate{ + Ref: &corev1.ObjectReference{ + APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", + Kind: "ControlPlaneTemplate", }, }, }, From a9f7b13e499c86bc1061efd26a739a7ecdc54bac Mon Sep 17 00:00:00 2001 From: Dhairya Arora Date: Thu, 26 Feb 2026 00:36:34 +0530 Subject: [PATCH 03/35] fixup! fixup! enable multiple control plane classes --- cmd/clusterctl/client/cluster/objectgraph.go | 29 ++++- cmd/clusterctl/client/cluster/topology.go | 22 +++- exp/topology/desiredstate/desired_state.go | 25 ++-- exp/topology/scope/blueprint.go | 1 + .../controllers/topology/cluster/blueprint.go | 5 +- internal/topology/check/compatibility.go | 54 ++++++++- internal/webhooks/cluster.go | 58 ++++++++-- internal/webhooks/clusterclass.go | 107 ++++++++++++++++-- internal/webhooks/patch_validation.go | 38 +++++++ 9 files changed, 295 insertions(+), 44 deletions(-) diff --git a/cmd/clusterctl/client/cluster/objectgraph.go b/cmd/clusterctl/client/cluster/objectgraph.go index 8e6b7d76cc41..1f3373918d86 100644 --- a/cmd/clusterctl/client/cluster/objectgraph.go +++ b/cmd/clusterctl/client/cluster/objectgraph.go @@ -43,9 +43,11 @@ import ( secretutil "sigs.k8s.io/cluster-api/util/secret" ) -const clusterTopologyNameKey = "cluster.spec.topology.class" -const clusterTopologyNamespaceKey = "cluster.spec.topology.classNamespace" -const clusterResourceSetBindingClusterNameKey = "clusterresourcesetbinding.spec.clustername" +const ( + clusterTopologyNameKey = "cluster.spec.topology.class" + clusterTopologyNamespaceKey = "cluster.spec.topology.classNamespace" + clusterResourceSetBindingClusterNameKey = "clusterresourcesetbinding.spec.clustername" +) type empty struct{} @@ -523,14 +525,31 @@ func (o *objectGraph) Discovery(ctx context.Context, namespace string) error { errs := []error{} _, err = o.fetchRef(ctx, discoveryBackoff, cc.Spec.Infrastructure.Ref) errs = append(errs, err) - _, err = o.fetchRef(ctx, discoveryBackoff, cc.Spec.ControlPlane.Ref) - errs = append(errs, err) + + // syself change. + // Fetch inline control plane refs (if defined). + if cc.Spec.ControlPlane.Ref != nil { + _, err = o.fetchRef(ctx, discoveryBackoff, cc.Spec.ControlPlane.Ref) + errs = append(errs, err) + } if cc.Spec.ControlPlane.MachineInfrastructure != nil { _, err = o.fetchRef(ctx, discoveryBackoff, cc.Spec.ControlPlane.MachineInfrastructure.Ref) errs = append(errs, err) } + // Fetch refs from named control plane classes. + for _, cpClass := range cc.Spec.ControlPlaneClasses { + if cpClass.Ref != nil { + _, err = o.fetchRef(ctx, discoveryBackoff, cpClass.Ref) + errs = append(errs, err) + } + if cpClass.MachineInfrastructure != nil { + _, err = o.fetchRef(ctx, discoveryBackoff, cpClass.MachineInfrastructure.Ref) + errs = append(errs, err) + } + } + for _, mdClass := range cc.Spec.Workers.MachineDeployments { _, err = o.fetchRef(ctx, discoveryBackoff, mdClass.Template.Infrastructure.Ref) errs = append(errs, err) diff --git a/cmd/clusterctl/client/cluster/topology.go b/cmd/clusterctl/client/cluster/topology.go index 2c1c329fce51..74804cf7b3c9 100644 --- a/cmd/clusterctl/client/cluster/topology.go +++ b/cmd/clusterctl/client/cluster/topology.go @@ -782,21 +782,37 @@ func equalRef(a, b *corev1.ObjectReference) bool { } func clusterClassUsesTemplate(cc *clusterv1.ClusterClass, templateRef *corev1.ObjectReference) bool { + // syself change. // Check infrastructure ref. if equalRef(cc.Spec.Infrastructure.Ref, templateRef) { return true } - // Check control plane ref. - if equalRef(cc.Spec.ControlPlane.Ref, templateRef) { + + // Check inline control plane ref. + if cc.Spec.ControlPlane.Ref != nil && equalRef(cc.Spec.ControlPlane.Ref, templateRef) { return true } - // If control plane uses machine, check it. + + // If inline control plane uses machine, check it. if cc.Spec.ControlPlane.MachineInfrastructure != nil && cc.Spec.ControlPlane.MachineInfrastructure.Ref != nil { if equalRef(cc.Spec.ControlPlane.MachineInfrastructure.Ref, templateRef) { return true } } + // Check named control plane classes. + for _, cpClass := range cc.Spec.ControlPlaneClasses { + if cpClass.Ref != nil && equalRef(cpClass.Ref, templateRef) { + return true + } + + if cpClass.MachineInfrastructure != nil && cpClass.MachineInfrastructure.Ref != nil { + if equalRef(cpClass.MachineInfrastructure.Ref, templateRef) { + return true + } + } + } + for _, mdClass := range cc.Spec.Workers.MachineDeployments { // Check bootstrap template ref. if equalRef(mdClass.Template.Bootstrap.Ref, templateRef) { diff --git a/exp/topology/desiredstate/desired_state.go b/exp/topology/desiredstate/desired_state.go index 082f6e826635..e5c6a67cf0e0 100644 --- a/exp/topology/desiredstate/desired_state.go +++ b/exp/topology/desiredstate/desired_state.go @@ -229,7 +229,9 @@ func computeInfrastructureCluster(_ context.Context, s *scope.Scope) (*unstructu // that should be referenced by the ControlPlane object. func computeControlPlaneInfrastructureMachineTemplate(_ context.Context, s *scope.Scope) (*unstructured.Unstructured, error) { template := s.Blueprint.ControlPlane.InfrastructureMachineTemplate - templateClonedFromRef := s.Blueprint.ClusterClass.Spec.ControlPlane.MachineInfrastructure.Ref + + // syself change + templateClonedFromRef := s.Blueprint.ControlPlaneClass.MachineInfrastructure.Ref cluster := s.Current.Cluster // Check if the current control plane object has a machineTemplate.infrastructureRef already defined. @@ -259,7 +261,9 @@ func computeControlPlaneInfrastructureMachineTemplate(_ context.Context, s *scop // corresponding template defined in the blueprint. func (g *generator) computeControlPlane(ctx context.Context, s *scope.Scope, infrastructureMachineTemplate *unstructured.Unstructured) (*unstructured.Unstructured, error) { template := s.Blueprint.ControlPlane.Template - templateClonedFromRef := s.Blueprint.ClusterClass.Spec.ControlPlane.Ref + + // syself change + templateClonedFromRef := s.Blueprint.ControlPlaneClass.Ref cluster := s.Current.Cluster currentRef := cluster.Spec.ControlPlaneRef @@ -267,7 +271,7 @@ func (g *generator) computeControlPlane(ctx context.Context, s *scope.Scope, inf // We merge the labels and annotations from topology and ClusterClass. // We also add the cluster-name and the topology owned labels, so they are propagated down. topologyMetadata := s.Blueprint.Topology.ControlPlane.Metadata - clusterClassMetadata := s.Blueprint.ClusterClass.Spec.ControlPlane.Metadata + clusterClassMetadata := s.Blueprint.ControlPlaneClass.Metadata controlPlaneLabels := util.MergeMap(topologyMetadata.Labels, clusterClassMetadata.Labels) if controlPlaneLabels == nil { @@ -279,8 +283,8 @@ func (g *generator) computeControlPlane(ctx context.Context, s *scope.Scope, inf controlPlaneAnnotations := util.MergeMap(topologyMetadata.Annotations, clusterClassMetadata.Annotations) nameTemplate := "{{ .cluster.name }}-{{ .random }}" - if s.Blueprint.ClusterClass.Spec.ControlPlane.NamingStrategy != nil && s.Blueprint.ClusterClass.Spec.ControlPlane.NamingStrategy.Template != nil { - nameTemplate = *s.Blueprint.ClusterClass.Spec.ControlPlane.NamingStrategy.Template + if s.Blueprint.ControlPlaneClass.NamingStrategy != nil && s.Blueprint.ControlPlaneClass.NamingStrategy.Template != nil { + nameTemplate = *s.Blueprint.ControlPlaneClass.NamingStrategy.Template } controlPlane, err := templateToObject(templateToInput{ @@ -365,14 +369,15 @@ func (g *generator) computeControlPlane(ctx context.Context, s *scope.Scope, inf if err := contract.ControlPlane().MachineTemplate().ReadinessGates().Set(controlPlane, s.Blueprint.Topology.ControlPlane.ReadinessGates); err != nil { return nil, errors.Wrapf(err, "failed to set %s in the ControlPlane object", contract.ControlPlane().MachineTemplate().ReadinessGates().Path()) } - } else if s.Blueprint.ClusterClass.Spec.ControlPlane.ReadinessGates != nil { - if err := contract.ControlPlane().MachineTemplate().ReadinessGates().Set(controlPlane, s.Blueprint.ClusterClass.Spec.ControlPlane.ReadinessGates); err != nil { + // syself change + } else if s.Blueprint.ControlPlaneClass.ReadinessGates != nil { + if err := contract.ControlPlane().MachineTemplate().ReadinessGates().Set(controlPlane, s.Blueprint.ControlPlaneClass.ReadinessGates); err != nil { return nil, errors.Wrapf(err, "failed to set %s in the ControlPlane object", contract.ControlPlane().MachineTemplate().ReadinessGates().Path()) } } // If it is required to manage the NodeDrainTimeout for the control plane, set the corresponding field. - nodeDrainTimeout := s.Blueprint.ClusterClass.Spec.ControlPlane.NodeDrainTimeout + nodeDrainTimeout := s.Blueprint.ControlPlaneClass.NodeDrainTimeout if s.Blueprint.Topology.ControlPlane.NodeDrainTimeout != nil { nodeDrainTimeout = s.Blueprint.Topology.ControlPlane.NodeDrainTimeout } @@ -383,7 +388,7 @@ func (g *generator) computeControlPlane(ctx context.Context, s *scope.Scope, inf } // If it is required to manage the NodeVolumeDetachTimeout for the control plane, set the corresponding field. - nodeVolumeDetachTimeout := s.Blueprint.ClusterClass.Spec.ControlPlane.NodeVolumeDetachTimeout + nodeVolumeDetachTimeout := s.Blueprint.ControlPlaneClass.NodeVolumeDetachTimeout if s.Blueprint.Topology.ControlPlane.NodeVolumeDetachTimeout != nil { nodeVolumeDetachTimeout = s.Blueprint.Topology.ControlPlane.NodeVolumeDetachTimeout } @@ -394,7 +399,7 @@ func (g *generator) computeControlPlane(ctx context.Context, s *scope.Scope, inf } // If it is required to manage the NodeDeletionTimeout for the control plane, set the corresponding field. - nodeDeletionTimeout := s.Blueprint.ClusterClass.Spec.ControlPlane.NodeDeletionTimeout + nodeDeletionTimeout := s.Blueprint.ControlPlaneClass.NodeDeletionTimeout if s.Blueprint.Topology.ControlPlane.NodeDeletionTimeout != nil { nodeDeletionTimeout = s.Blueprint.Topology.ControlPlane.NodeDeletionTimeout } diff --git a/exp/topology/scope/blueprint.go b/exp/topology/scope/blueprint.go index f83af0aee18a..60802a44458b 100644 --- a/exp/topology/scope/blueprint.go +++ b/exp/topology/scope/blueprint.go @@ -31,6 +31,7 @@ type ClusterBlueprint struct { // ClusterClass holds the ClusterClass object referenced from Cluster.Spec.Topology. ClusterClass *clusterv1.ClusterClass + // syself change // ControlPlaneClass holds the resolved ControlPlaneClass from the ClusterClass. // This is the ControlPlaneClass selected based on the Cluster topology's control plane class field. // If the topology does not specify a class, this is the inline ControlPlaneClass from ClusterClass.Spec.ControlPlane. diff --git a/internal/controllers/topology/cluster/blueprint.go b/internal/controllers/topology/cluster/blueprint.go index 96a588b195f0..7a5ab0a18c94 100644 --- a/internal/controllers/topology/cluster/blueprint.go +++ b/internal/controllers/topology/cluster/blueprint.go @@ -44,6 +44,7 @@ func (r *Reconciler) getBlueprint(ctx context.Context, cluster *clusterv1.Cluste return nil, errors.Wrapf(err, "failed to get infrastructure cluster template for ClusterClass %s", klog.KObj(blueprint.ClusterClass)) } + // syself change // Resolve the ControlPlaneClass to use. // If the Cluster topology specifies a control plane class, look it up from ClusterClass.spec.controlPlane.classes. // Otherwise, fall back to the inline ClusterClass.spec.controlPlane definition. @@ -62,7 +63,7 @@ func (r *Reconciler) getBlueprint(ctx context.Context, cluster *clusterv1.Cluste // If the clusterClass mandates the controlPlane has infrastructureMachines, read it. if blueprint.HasControlPlaneInfrastructureMachine() { - blueprint.ControlPlane.InfrastructureMachineTemplate, err = r.getReference(ctx, blueprint.ClusterClass.Spec.ControlPlane.MachineInfrastructure.Ref) + blueprint.ControlPlane.InfrastructureMachineTemplate, err = r.getReference(ctx, blueprint.ControlPlaneClass.MachineInfrastructure.Ref) if err != nil { return nil, errors.Wrapf(err, "failed to get control plane's machine template for ClusterClass %s", klog.KObj(blueprint.ClusterClass)) } @@ -70,7 +71,7 @@ func (r *Reconciler) getBlueprint(ctx context.Context, cluster *clusterv1.Cluste // If the clusterClass defines a valid MachineHealthCheck (including a defined MachineInfrastructure) set the blueprint MachineHealthCheck. if blueprint.HasControlPlaneMachineHealthCheck() { - blueprint.ControlPlane.MachineHealthCheck = blueprint.ClusterClass.Spec.ControlPlane.MachineHealthCheck + blueprint.ControlPlane.MachineHealthCheck = blueprint.ControlPlaneClass.MachineHealthCheck } // Loop over the machine deployments classes in ClusterClass diff --git a/internal/topology/check/compatibility.go b/internal/topology/check/compatibility.go index 08d36b696226..4445623edc36 100644 --- a/internal/topology/check/compatibility.go +++ b/internal/topology/check/compatibility.go @@ -197,14 +197,29 @@ func ClusterClassesAreCompatible(current, desired *clusterv1.ClusterClass) field allErrs = append(allErrs, LocalObjectTemplatesAreCompatible(current.Spec.Infrastructure, desired.Spec.Infrastructure, field.NewPath("spec", "infrastructure"))...) - // Validate control plane changes desired a compatible way. - allErrs = append(allErrs, LocalObjectTemplatesAreCompatible(current.Spec.ControlPlane.LocalObjectTemplate, desired.Spec.ControlPlane.LocalObjectTemplate, - field.NewPath("spec", "controlPlane"))...) - if desired.Spec.ControlPlane.MachineInfrastructure != nil && current.Spec.ControlPlane.MachineInfrastructure != nil { - allErrs = append(allErrs, LocalObjectTemplatesAreCompatible(*current.Spec.ControlPlane.MachineInfrastructure, *desired.Spec.ControlPlane.MachineInfrastructure, - field.NewPath("spec", "controlPlane", "machineInfrastructure"))...) + // Validate inline control plane changes in a compatible way (only if both have refs set). + if current.Spec.ControlPlane.LocalObjectTemplate.Ref != nil && desired.Spec.ControlPlane.LocalObjectTemplate.Ref != nil { + allErrs = append(allErrs, LocalObjectTemplatesAreCompatible(current.Spec.ControlPlane.LocalObjectTemplate, desired.Spec.ControlPlane.LocalObjectTemplate, + field.NewPath("spec", "controlPlane"))...) + if desired.Spec.ControlPlane.MachineInfrastructure != nil && current.Spec.ControlPlane.MachineInfrastructure != nil { + allErrs = append(allErrs, LocalObjectTemplatesAreCompatible(*current.Spec.ControlPlane.MachineInfrastructure, *desired.Spec.ControlPlane.MachineInfrastructure, + field.NewPath("spec", "controlPlane", "machineInfrastructure"))...) + } } + // Validate named control plane class changes in a compatible way. + for _, desiredClass := range desired.Spec.ControlPlaneClasses { + for i, currentClass := range current.Spec.ControlPlaneClasses { + if desiredClass.Class == currentClass.Class { + classPath := field.NewPath("spec", "controlPlaneClasses").Index(i) + allErrs = append(allErrs, LocalObjectTemplatesAreCompatible(currentClass.LocalObjectTemplate, desiredClass.LocalObjectTemplate, classPath)...) + if desiredClass.MachineInfrastructure != nil && currentClass.MachineInfrastructure != nil { + allErrs = append(allErrs, LocalObjectTemplatesAreCompatible(*currentClass.MachineInfrastructure, *desiredClass.MachineInfrastructure, + classPath.Child("machineInfrastructure"))...) + } + } + } + } // Validate changes to MachineDeployments. allErrs = append(allErrs, MachineDeploymentClassesAreCompatible(current, desired)...) @@ -443,6 +458,33 @@ func MachinePoolTopologiesAreValidAndDefinedInClusterClass(desired *clusterv1.Cl return allErrs } +// ControlPlaneTopologyClassIsDefinedInClusterClass checks that the control plane class referenced +// in the Cluster topology (if set) is defined in the ClusterClass. +// syself change. +func ControlPlaneTopologyClassIsDefinedInClusterClass(desired *clusterv1.Cluster, clusterClass *clusterv1.ClusterClass) field.ErrorList { + var allErrs field.ErrorList + cpClass := desired.Spec.Topology.ControlPlane.Class + if cpClass == "" { + return nil + } + + for _, class := range clusterClass.Spec.ControlPlaneClasses { + if class.Class == cpClass { + return nil + } + } + + allErrs = append(allErrs, + field.Invalid( + field.NewPath("spec", "topology", "controlPlane", "class"), + cpClass, + fmt.Sprintf("ControlPlaneClass with name %q does not exist in ClusterClass %q", + cpClass, clusterClass.Name), + ), + ) + return allErrs +} + // ClusterClassReferencesAreValid checks that each template reference in the ClusterClass is valid . func ClusterClassReferencesAreValid(clusterClass *clusterv1.ClusterClass) field.ErrorList { var allErrs field.ErrorList diff --git a/internal/webhooks/cluster.go b/internal/webhooks/cluster.go index 627411c0f659..30879baa34a3 100644 --- a/internal/webhooks/cluster.go +++ b/internal/webhooks/cluster.go @@ -77,8 +77,10 @@ type Cluster struct { decoder admission.Decoder } -var _ webhook.CustomDefaulter = &Cluster{} -var _ webhook.CustomValidator = &Cluster{} +var ( + _ webhook.CustomDefaulter = &Cluster{} + _ webhook.CustomValidator = &Cluster{} +) var errClusterClassNotReconciled = errors.New("ClusterClass is not successfully reconciled") @@ -673,10 +675,18 @@ func validateMachineHealthChecks(cluster *clusterv1.Cluster, clusterClass *clust if cluster.Spec.Topology.ControlPlane.MachineHealthCheck != nil { fldPath := field.NewPath("spec", "topology", "controlPlane", "machineHealthCheck") + // syself change + // Resolve the control plane class to use for validation. + cpClass, err := resolveControlPlaneClassForValidation(cluster, clusterClass) + if err != nil { + allErrs = append(allErrs, field.InternalError(fldPath, err)) + return allErrs + } + // Validate ControlPlane MachineHealthCheck if defined. if !cluster.Spec.Topology.ControlPlane.MachineHealthCheck.MachineHealthCheckClass.IsZero() { - // Ensure ControlPlane does not define a MachineHealthCheck if the ClusterClass does not define MachineInfrastructure. - if clusterClass.Spec.ControlPlane.MachineInfrastructure == nil { + // Ensure ControlPlane does not define a MachineHealthCheck if the ControlPlaneClass does not define MachineInfrastructure. + if cpClass.MachineInfrastructure == nil { allErrs = append(allErrs, field.Forbidden( fldPath, "can be set only if spec.controlPlane.machineInfrastructure is set in ClusterClass", @@ -688,12 +698,8 @@ func validateMachineHealthChecks(cluster *clusterv1.Cluster, clusterClass *clust // If MachineHealthCheck is explicitly enabled then make sure that a MachineHealthCheck definition is // available either in the Cluster topology or in the ClusterClass. - // (One of these definitions will be used in the controller to create the MachineHealthCheck) - - // Check if the machineHealthCheck is explicitly enabled in the ControlPlaneTopology. if cluster.Spec.Topology.ControlPlane.MachineHealthCheck.Enable != nil && *cluster.Spec.Topology.ControlPlane.MachineHealthCheck.Enable { - // Ensure the MHC is defined in at least one of the ControlPlaneTopology of the Cluster or the ControlPlaneClass of the ClusterClass. - if cluster.Spec.Topology.ControlPlane.MachineHealthCheck.MachineHealthCheckClass.IsZero() && clusterClass.Spec.ControlPlane.MachineHealthCheck == nil { + if cluster.Spec.Topology.ControlPlane.MachineHealthCheck.MachineHealthCheckClass.IsZero() && cpClass.MachineHealthCheck == nil { allErrs = append(allErrs, field.Forbidden( fldPath.Child("enable"), fmt.Sprintf("cannot be set to %t as MachineHealthCheck definition is not available in the Cluster topology or the ClusterClass", *cluster.Spec.Topology.ControlPlane.MachineHealthCheck.Enable), @@ -749,6 +755,15 @@ func machineDeploymentClassOfName(clusterClass *clusterv1.ClusterClass, name str return nil } +func controlPlaneClassOfName(clusterClass *clusterv1.ClusterClass, name string) *clusterv1.ControlPlaneClass { + for _, cpClass := range clusterClass.Spec.ControlPlaneClasses { + if cpClass.Class == name { + return &cpClass + } + } + return nil +} + // validateCIDRBlocks ensures the passed CIDR is valid. func validateCIDRBlocks(fldPath *field.Path, cidrs []string) field.ErrorList { var allErrs field.ErrorList @@ -928,6 +943,10 @@ func ValidateClusterForClusterClass(cluster *clusterv1.Cluster, clusterClass *cl if clusterClass == nil { return field.ErrorList{field.InternalError(field.NewPath(""), errors.New("ClusterClass can not be nil"))} } + + // syself change + allErrs = append(allErrs, check.ControlPlaneTopologyClassIsDefinedInClusterClass(cluster, clusterClass)...) + allErrs = append(allErrs, check.MachineDeploymentTopologiesAreValidAndDefinedInClusterClass(cluster, clusterClass)...) allErrs = append(allErrs, check.MachinePoolTopologiesAreValidAndDefinedInClusterClass(cluster, clusterClass)...) @@ -1138,3 +1157,24 @@ func validateAutoscalerAnnotationsForCluster(cluster *clusterv1.Cluster, cluster } return allErrs } + +// resolveControlPlaneClassForValidation returns the ControlPlaneClass to use for validation. +// If the Cluster topology specifies a control plane class, it is looked up from ClusterClass.spec.controlPlane.classes. +// Otherwise, the inline ClusterClass.spec.controlPlane definition is used. +func resolveControlPlaneClassForValidation(cluster *clusterv1.Cluster, clusterClass *clusterv1.ClusterClass) (*clusterv1.ControlPlaneClass, error) { + if cluster.Spec.Topology.ControlPlane.Class == "" { + return &clusterClass.Spec.ControlPlane, nil + } + + for i := range clusterClass.Spec.ControlPlaneClasses { + if clusterClass.Spec.ControlPlaneClasses[i].Class == cluster.Spec.Topology.ControlPlane.Class { + return &clusterClass.Spec.ControlPlaneClasses[i], nil + } + } + + return nil, fmt.Errorf("control plane class %q not found in ClusterClass %s/%s", + cluster.Spec.Topology.ControlPlane.Class, + clusterClass.Namespace, + clusterClass.Name, + ) +} diff --git a/internal/webhooks/clusterclass.go b/internal/webhooks/clusterclass.go index 1b190c9425a7..d8739d3ea745 100644 --- a/internal/webhooks/clusterclass.go +++ b/internal/webhooks/clusterclass.go @@ -60,8 +60,10 @@ type ClusterClass struct { Client client.Reader } -var _ webhook.CustomDefaulter = &ClusterClass{} -var _ webhook.CustomValidator = &ClusterClass{} +var ( + _ webhook.CustomDefaulter = &ClusterClass{} + _ webhook.CustomValidator = &ClusterClass{} +) // Default implements defaulting for ClusterClass create and update. func (webhook *ClusterClass) Default(_ context.Context, obj runtime.Object) error { @@ -71,16 +73,28 @@ func (webhook *ClusterClass) Default(_ context.Context, obj runtime.Object) erro } // Default all namespaces in the references to the object namespace. defaultNamespace(in.Spec.Infrastructure.Ref, in.Namespace) - defaultNamespace(in.Spec.ControlPlane.Ref, in.Namespace) + // syself change + // Default inline control plane refs. + defaultNamespace(in.Spec.ControlPlane.Ref, in.Namespace) if in.Spec.ControlPlane.MachineInfrastructure != nil { defaultNamespace(in.Spec.ControlPlane.MachineInfrastructure.Ref, in.Namespace) } - if in.Spec.ControlPlane.MachineHealthCheck != nil { defaultNamespace(in.Spec.ControlPlane.MachineHealthCheck.RemediationTemplate, in.Namespace) } + // Default named control plane class refs. + for i := range in.Spec.ControlPlaneClasses { + defaultNamespace(in.Spec.ControlPlaneClasses[i].Ref, in.Namespace) + if in.Spec.ControlPlaneClasses[i].MachineInfrastructure != nil { + defaultNamespace(in.Spec.ControlPlaneClasses[i].MachineInfrastructure.Ref, in.Namespace) + } + if in.Spec.ControlPlaneClasses[i].MachineHealthCheck != nil { + defaultNamespace(in.Spec.ControlPlaneClasses[i].MachineHealthCheck.RemediationTemplate, in.Namespace) + } + } + for i := range in.Spec.Workers.MachineDeployments { defaultNamespace(in.Spec.Workers.MachineDeployments[i].Template.Bootstrap.Ref, in.Namespace) defaultNamespace(in.Spec.Workers.MachineDeployments[i].Template.Infrastructure.Ref, in.Namespace) @@ -252,6 +266,49 @@ func validateUpdatesToMachineHealthCheckClasses(clusters []clusterv1.Cluster, ol } } + // syself change + // For each ControlPlaneClass check if the MachineHealthCheck definition is dropped. + // For each ControlPlaneClass check if the MachineHealthCheck definition is dropped. + for _, newCPClass := range newClusterClass.Spec.ControlPlaneClasses { + oldCPClass := controlPlaneClassOfName(oldClusterClass, newCPClass.Class) + if oldCPClass == nil { + // New ControlPlaneClass. Nothing to validate. + continue + } + + // If the MachineHealthCheck was dropped then check that no cluster is using it. + if oldCPClass.MachineHealthCheck != nil && newCPClass.MachineHealthCheck == nil { + clustersUsingMHC := []string{} + + for _, cluster := range clusters { + if cluster.Spec.Topology == nil { + continue + } + if cluster.Spec.Topology.ControlPlane.Class != newCPClass.Class { + continue + } + + if cluster.Spec.Topology.ControlPlane.MachineHealthCheck != nil && + cluster.Spec.Topology.ControlPlane.MachineHealthCheck.Enable != nil && + *cluster.Spec.Topology.ControlPlane.MachineHealthCheck.Enable && + cluster.Spec.Topology.ControlPlane.MachineHealthCheck.MachineHealthCheckClass.IsZero() { + + clustersUsingMHC = append(clustersUsingMHC, cluster.Name) + } + } + + if len(clustersUsingMHC) != 0 { + allErrs = append(allErrs, field.Forbidden( + field.NewPath("spec", "controlPlaneClasses").Key(newCPClass.Class).Child("machineHealthCheck"), + fmt.Sprintf( + "MachineHealthCheck cannot be deleted because it is used by Cluster(s) %q", + strings.Join(clustersUsingMHC, ","), + ), + )) + } + } + } + // For each MachineDeploymentClass check if the MachineHealthCheck definition is dropped. for _, newMdClass := range newClusterClass.Spec.Workers.MachineDeployments { oldMdClass := machineDeploymentClassOfName(oldClusterClass, newMdClass.Class) @@ -473,12 +530,34 @@ func validateNamingStrategies(clusterClass *clusterv1.ClusterClass) field.ErrorL } } - for _, md := range clusterClass.Spec.Workers.MachineDeployments { + // syself change + // Validate naming strategies for each control plane class + for i, cp := range clusterClass.Spec.ControlPlaneClasses { + if cp.NamingStrategy == nil || cp.NamingStrategy.Template == nil { + continue + } + name, err := topologynames.ControlPlaneNameGenerator(*cp.NamingStrategy.Template, "cluster").GenerateName() + templateFldPath := field.NewPath("spec", "controlPlaneClasses").Index(i).Child("namingStrategy", "template") + if err != nil { + allErrs = append(allErrs, + field.Invalid( + templateFldPath, + *cp.NamingStrategy.Template, + fmt.Sprintf("invalid ControlPlaneClass name template: %v", err), + )) + } else { + for _, err := range validation.IsDNS1123Subdomain(name) { + allErrs = append(allErrs, field.Invalid(templateFldPath, *cp.NamingStrategy.Template, err)) + } + } + } + + for i, md := range clusterClass.Spec.Workers.MachineDeployments { if md.NamingStrategy == nil || md.NamingStrategy.Template == nil { continue } name, err := topologynames.MachineDeploymentNameGenerator(*md.NamingStrategy.Template, "cluster", "mdtopology").GenerateName() - templateFldPath := field.NewPath("spec", "workers", "machineDeployments").Key(md.Class).Child("namingStrategy", "template") + templateFldPath := field.NewPath("spec", "workers", "machineDeployments").Index(i).Child("namingStrategy", "template") if err != nil { allErrs = append(allErrs, field.Invalid( @@ -493,12 +572,12 @@ func validateNamingStrategies(clusterClass *clusterv1.ClusterClass) field.ErrorL } } - for _, mp := range clusterClass.Spec.Workers.MachinePools { + for i, mp := range clusterClass.Spec.Workers.MachinePools { if mp.NamingStrategy == nil || mp.NamingStrategy.Template == nil { continue } name, err := topologynames.MachinePoolNameGenerator(*mp.NamingStrategy.Template, "cluster", "mptopology").GenerateName() - templateFldPath := field.NewPath("spec", "workers", "machinePools").Key(mp.Class).Child("namingStrategy", "template") + templateFldPath := field.NewPath("spec", "workers", "machinePools").Index(i).Child("namingStrategy", "template") if err != nil { allErrs = append(allErrs, field.Invalid( @@ -528,7 +607,8 @@ func validateMachineHealthCheckClass(fldPath *field.Path, namepace string, m *cl UnhealthyConditions: m.UnhealthyConditions, UnhealthyRange: m.UnhealthyRange, RemediationTemplate: m.RemediationTemplate, - }} + }, + } return (&MachineHealthCheck{}).validateCommonFields(&mhc, fldPath) } @@ -539,6 +619,15 @@ func validateClusterClassMetadata(clusterClass *clusterv1.ClusterClass) field.Er for _, m := range clusterClass.Spec.Workers.MachineDeployments { allErrs = append(allErrs, m.Template.Metadata.Validate(field.NewPath("spec", "workers", "machineDeployments").Key(m.Class).Child("template", "metadata"))...) } + + // syself change + // Validate metadata for each control plane class + for i, cp := range clusterClass.Spec.ControlPlaneClasses { + allErrs = append(allErrs, + cp.Metadata.Validate( + field.NewPath("spec", "controlPlaneClasses").Index(i).Child("metadata"))...) + } + for _, m := range clusterClass.Spec.Workers.MachinePools { allErrs = append(allErrs, m.Template.Metadata.Validate(field.NewPath("spec", "workers", "machinePools").Key(m.Class).Child("template", "metadata"))...) } diff --git a/internal/webhooks/patch_validation.go b/internal/webhooks/patch_validation.go index ee1fc561da0e..3a0f6659b2e8 100644 --- a/internal/webhooks/patch_validation.go +++ b/internal/webhooks/patch_validation.go @@ -167,6 +167,7 @@ func validateSelectors(selector clusterv1.PatchSelector, class *clusterv1.Cluste // Return an error if none of the possible selectors are enabled. if !(selector.MatchResources.InfrastructureCluster || selector.MatchResources.ControlPlane || + (selector.MatchResources.ControlPlaneClass != nil && len(selector.MatchResources.ControlPlaneClass.Names) > 0) || (selector.MatchResources.MachineDeploymentClass != nil && len(selector.MatchResources.MachineDeploymentClass.Names) > 0) || (selector.MatchResources.MachinePoolClass != nil && len(selector.MatchResources.MachinePoolClass.Names) > 0)) { return append(allErrs, @@ -205,6 +206,43 @@ func validateSelectors(selector clusterv1.PatchSelector, class *clusterv1.Cluste } } + // Validate selectors for control plane classes + if selector.MatchResources.ControlPlaneClass != nil && len(selector.MatchResources.ControlPlaneClass.Names) > 0 { + for i, name := range selector.MatchResources.ControlPlaneClass.Names { + match := false + err := validateSelectorName(name, path, "controlPlaneClass", i) + if err != nil { + allErrs = append(allErrs, err) + break + } + for _, cp := range class.Spec.ControlPlaneClasses { + var matches bool + if cp.Class == name || name == "*" { + matches = true + } else if strings.HasPrefix(name, "*") && strings.HasSuffix(cp.Class, strings.TrimPrefix(name, "*")) { + matches = true + } else if strings.HasSuffix(name, "*") && strings.HasPrefix(cp.Class, strings.TrimSuffix(name, "*")) { + matches = true + } + + if matches { + if selectorMatchTemplate(selector, cp.Ref) || + (cp.MachineInfrastructure != nil && selectorMatchTemplate(selector, cp.MachineInfrastructure.Ref)) { + match = true + break + } + } + } + if !match { + allErrs = append(allErrs, field.Invalid( + path.Child("matchResources", "controlPlaneClass", "names").Index(i), + name, + "selector is enabled but matches neither the controlPlane ref nor the controlPlane machineInfrastructure ref of a ControlPlane class", + )) + } + } + } + if selector.MatchResources.MachineDeploymentClass != nil && len(selector.MatchResources.MachineDeploymentClass.Names) > 0 { for i, name := range selector.MatchResources.MachineDeploymentClass.Names { match := false From 075173969c21738ffeb9e61164e658e1a7bc88f5 Mon Sep 17 00:00:00 2001 From: Dhairya Arora Date: Thu, 26 Feb 2026 08:40:13 +0530 Subject: [PATCH 04/35] add the class name for control plane that should be patched --- .../topologymutation_variable_types.go | 5 ++++ .../topology/cluster/patches/engine.go | 3 ++- .../patches/inline/json_patch_generator.go | 27 +++++++++++++++++++ .../cluster/patches/variables/variables.go | 6 +++-- .../patches/variables/variables_test.go | 2 +- 5 files changed, 39 insertions(+), 4 deletions(-) diff --git a/exp/runtime/hooks/api/v1alpha1/topologymutation_variable_types.go b/exp/runtime/hooks/api/v1alpha1/topologymutation_variable_types.go index edc7f7b29cfb..e459a0080a99 100644 --- a/exp/runtime/hooks/api/v1alpha1/topologymutation_variable_types.go +++ b/exp/runtime/hooks/api/v1alpha1/topologymutation_variable_types.go @@ -123,6 +123,11 @@ type ControlPlaneBuiltins struct { // +optional Name string `json:"name,omitempty"` + // class is the class name of the ControlPlane, + // to which the current template belongs to. + // +optional + Class string `json:"class,omitempty"` + // replicas is the value of the replicas field of the ControlPlane object. // +optional Replicas *int64 `json:"replicas,omitempty"` diff --git a/internal/controllers/topology/cluster/patches/engine.go b/internal/controllers/topology/cluster/patches/engine.go index ed78569e9aba..b7f4eca11e9d 100644 --- a/internal/controllers/topology/cluster/patches/engine.go +++ b/internal/controllers/topology/cluster/patches/engine.go @@ -166,8 +166,9 @@ func addVariablesForPatch(blueprint *scope.ClusterBlueprint, desired *scope.Clus } req.Variables = globalVariables + // syself change // Calculate the Control Plane variables. - controlPlaneVariables, err := variables.ControlPlane(&blueprint.Topology.ControlPlane, desired.ControlPlane.Object, desired.ControlPlane.InfrastructureMachineTemplate, patchVariableDefinitions) + controlPlaneVariables, err := variables.ControlPlane(&blueprint.Topology.ControlPlane, desired.ControlPlane.Object, desired.ControlPlane.InfrastructureMachineTemplate, blueprint.Topology.ControlPlane.Class, patchVariableDefinitions) if err != nil { return errors.Wrapf(err, "failed to calculate ControlPlane variables") } diff --git a/internal/controllers/topology/cluster/patches/inline/json_patch_generator.go b/internal/controllers/topology/cluster/patches/inline/json_patch_generator.go index fe09c91100de..af8e69ab9d00 100644 --- a/internal/controllers/topology/cluster/patches/inline/json_patch_generator.go +++ b/internal/controllers/topology/cluster/patches/inline/json_patch_generator.go @@ -155,6 +155,33 @@ func matchesSelector(req *runtimehooksv1.GeneratePatchesRequestItem, templateVar } } + // syself change + // Check if the request is for a ControlPlane or the InfrastructureMachineTemplate of a ControlPlaneClass. + if selector.MatchResources.ControlPlaneClass != nil { + if (req.HolderReference.Kind == "Cluster" && req.HolderReference.FieldPath == "spec.controlPlaneRef") || + req.HolderReference.FieldPath == strings.Join(contract.ControlPlane().MachineTemplate().InfrastructureRef().Path(), ".") { + // Read the builtin.controlPlane.class variable. + templateCPClassJSON, err := patchvariables.GetVariableValue(templateVariables, "builtin.controlPlane.class") + + // If the builtin variable could be read. + if err == nil { + // If templateCPClass matches one of the configured ControlPlaneClasses. + for _, cpClass := range selector.MatchResources.ControlPlaneClass.Names { + if cpClass == "*" || string(templateCPClassJSON.Raw) == strconv.Quote(cpClass) { + return true + } + unquoted, _ := strconv.Unquote(string(templateCPClassJSON.Raw)) + if strings.HasPrefix(cpClass, "*") && strings.HasSuffix(unquoted, strings.TrimPrefix(cpClass, "*")) { + return true + } + if strings.HasSuffix(cpClass, "*") && strings.HasPrefix(unquoted, strings.TrimSuffix(cpClass, "*")) { + return true + } + } + } + } + } + // Check if the request is for a BootstrapConfigTemplate or an InfrastructureMachineTemplate // of one of the configured MachineDeploymentClasses. if selector.MatchResources.MachineDeploymentClass != nil { diff --git a/internal/controllers/topology/cluster/patches/variables/variables.go b/internal/controllers/topology/cluster/patches/variables/variables.go index 5f27c0f53cab..3c4bd43773f1 100644 --- a/internal/controllers/topology/cluster/patches/variables/variables.go +++ b/internal/controllers/topology/cluster/patches/variables/variables.go @@ -94,7 +94,7 @@ func Global(clusterTopology *clusterv1.Topology, cluster *clusterv1.Cluster, pat } // ControlPlane returns variables that apply to templates belonging to the ControlPlane. -func ControlPlane(cpTopology *clusterv1.ControlPlaneTopology, cp, cpInfrastructureMachineTemplate *unstructured.Unstructured, patchVariableDefinitions map[string]bool) ([]runtimehooksv1.Variable, error) { +func ControlPlane(cpTopology *clusterv1.ControlPlaneTopology, cp, cpInfrastructureMachineTemplate *unstructured.Unstructured, controlPlaneClass string, patchVariableDefinitions map[string]bool) ([]runtimehooksv1.Variable, error) { variables := []runtimehooksv1.Variable{} // Add variables overrides for the ControlPlane. @@ -107,10 +107,12 @@ func ControlPlane(cpTopology *clusterv1.ControlPlaneTopology, cp, cpInfrastructu } } + // syself change // Construct builtin variable. builtin := runtimehooksv1.Builtins{ ControlPlane: &runtimehooksv1.ControlPlaneBuiltins{ - Name: cp.GetName(), + Name: cp.GetName(), + Class: controlPlaneClass, }, } diff --git a/internal/controllers/topology/cluster/patches/variables/variables_test.go b/internal/controllers/topology/cluster/patches/variables/variables_test.go index ffbf79d65034..d50088ea4315 100644 --- a/internal/controllers/topology/cluster/patches/variables/variables_test.go +++ b/internal/controllers/topology/cluster/patches/variables/variables_test.go @@ -623,7 +623,7 @@ func TestControlPlane(t *testing.T) { t.Run(tt.name, func(t *testing.T) { g := NewWithT(t) - got, err := ControlPlane(tt.controlPlaneTopology, tt.controlPlane, tt.controlPlaneInfrastructureMachineTemplate, tt.variableDefinitionsForPatch) + got, err := ControlPlane(tt.controlPlaneTopology, tt.controlPlane, tt.controlPlaneInfrastructureMachineTemplate, "", tt.variableDefinitionsForPatch) g.Expect(err).ToNot(HaveOccurred()) g.Expect(got).To(BeComparableTo(tt.want)) }) From 06378fba84563d85fcf2cdfbc0d066de3752e027 Mon Sep 17 00:00:00 2001 From: Dhairya Arora Date: Thu, 26 Feb 2026 08:42:26 +0530 Subject: [PATCH 05/35] fixup! add the class name for control plane that should be patched --- exp/runtime/hooks/api/v1alpha1/zz_generated.openapi.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/exp/runtime/hooks/api/v1alpha1/zz_generated.openapi.go b/exp/runtime/hooks/api/v1alpha1/zz_generated.openapi.go index 627c45b105eb..d4416dbf8971 100644 --- a/exp/runtime/hooks/api/v1alpha1/zz_generated.openapi.go +++ b/exp/runtime/hooks/api/v1alpha1/zz_generated.openapi.go @@ -1027,6 +1027,13 @@ func schema_runtime_hooks_api_v1alpha1_ControlPlaneBuiltins(ref common.Reference Format: "", }, }, + "class": { + SchemaProps: spec.SchemaProps{ + Description: "class is the class name of the ControlPlane, to which the current template belongs to.", + Type: []string{"string"}, + Format: "", + }, + }, "replicas": { SchemaProps: spec.SchemaProps{ Description: "replicas is the value of the replicas field of the ControlPlane object.", From 7fdfc6ef0695b2075acd67ebcc1d30d908321df6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=BCttler?= Date: Thu, 26 Feb 2026 08:04:57 +0100 Subject: [PATCH 06/35] added GOMODCACHE so packages are not downloaded again for each ARCH --- Makefile | 1 + 1 file changed, 1 insertion(+) diff --git a/Makefile b/Makefile index 7e7547e3b69a..831232d8475e 100644 --- a/Makefile +++ b/Makefile @@ -1105,6 +1105,7 @@ release-binary: $(RELEASE_DIR) -e GOOS=$(GOOS) \ -e GOARCH=$(GOARCH) \ -e GOCACHE=/tmp/ \ + -e GOMODCACHE \ --user $$(id -u):$$(id -g) \ -v "$$(pwd):/workspace$(DOCKER_VOL_OPTS)" \ -w /workspace \ From 9b64c2a9372c4c2d1f6cecd10d1e43a89334098b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=BCttler?= Date: Thu, 26 Feb 2026 08:20:07 +0100 Subject: [PATCH 07/35] ... --- hack/release.sh | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100755 hack/release.sh diff --git a/hack/release.sh b/hack/release.sh new file mode 100755 index 000000000000..9740459a6bff --- /dev/null +++ b/hack/release.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +# Bash Strict Mode: https://github.com/guettli/bash-strict-mode +trap 'echo -e "\n🤷 🚨 šŸ”„ Warning: A command has failed. Exiting the script. Line was ($0:$LINENO): $(sed -n "${LINENO}p" "$0" 2>/dev/null || true) šŸ”„ 🚨 🤷 "; exit 3' ERR +set -Eeuo pipefail + +if [[ -n "$(git status --porcelain --untracked-files=normal)" ]]; then + echo "failed: git working tree is dirty (modified, staged, or untracked files present)" + git status --short --untracked-files=normal + exit 1 +fi + +if [[ -z ${RELEASE_TAG:-} ]]; then + echo "failed: RELEASE_TAG is not set. Use v1.10.7-syself.XX" + exit 1 +fi + +if ! git rev-parse --verify --quiet "refs/tags/${RELEASE_TAG}^{commit}" >/dev/null; then + echo "failed: RELEASE_TAG '${RELEASE_TAG}' does not exist as a git tag" + exit 1 +fi + +release_tag_commit="$(git rev-parse "refs/tags/${RELEASE_TAG}^{commit}")" +head_commit="$(git rev-parse HEAD)" +if [[ "${release_tag_commit}" != "${head_commit}" ]]; then + echo "failed: RELEASE_TAG '${RELEASE_TAG}' points to ${release_tag_commit}, but HEAD is ${head_commit}" + exit 1 +fi + +export PROD_REGISTRY=ghcr.io/syself/cluster-api + +make release From 210d0ea59f6e372ecedb22ba3590c58a674a41e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=BCttler?= Date: Thu, 26 Feb 2026 08:22:30 +0100 Subject: [PATCH 08/35] ... --- hack/release.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/hack/release.sh b/hack/release.sh index 9740459a6bff..a889c03146c0 100755 --- a/hack/release.sh +++ b/hack/release.sh @@ -16,6 +16,9 @@ fi if ! git rev-parse --verify --quiet "refs/tags/${RELEASE_TAG}^{commit}" >/dev/null; then echo "failed: RELEASE_TAG '${RELEASE_TAG}' does not exist as a git tag" + echo + echo "use: git tag $RELEASE_TAG" + echo exit 1 fi From 2a66dbfc3cfde542c3370b1b3879152c7c49b39c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=BCttler?= Date: Thu, 26 Feb 2026 08:23:49 +0100 Subject: [PATCH 09/35] ... --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 831232d8475e..8a47b410805e 100644 --- a/Makefile +++ b/Makefile @@ -1105,7 +1105,7 @@ release-binary: $(RELEASE_DIR) -e GOOS=$(GOOS) \ -e GOARCH=$(GOARCH) \ -e GOCACHE=/tmp/ \ - -e GOMODCACHE \ + -e GOMODCACHE=$$(go env GOMODCACHE) \ --user $$(id -u):$$(id -g) \ -v "$$(pwd):/workspace$(DOCKER_VOL_OPTS)" \ -w /workspace \ From 2e8b08f7258f7881d81e9fbf4d4fd98fd1998d85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=BCttler?= Date: Thu, 26 Feb 2026 08:28:27 +0100 Subject: [PATCH 10/35] pass in GOMODCACHE --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 8a47b410805e..c133346d33f5 100644 --- a/Makefile +++ b/Makefile @@ -1105,9 +1105,9 @@ release-binary: $(RELEASE_DIR) -e GOOS=$(GOOS) \ -e GOARCH=$(GOARCH) \ -e GOCACHE=/tmp/ \ - -e GOMODCACHE=$$(go env GOMODCACHE) \ --user $$(id -u):$$(id -g) \ -v "$$(pwd):/workspace$(DOCKER_VOL_OPTS)" \ + -v "$$(go env GOMODCACHE):/go/pkg/mod" \ -w /workspace \ golang:$(GO_VERSION) \ go build -a -trimpath -ldflags "$(LDFLAGS) -extldflags '-static'" \ From e2a5dae84fb981186cc353f942198ba86aa2a6ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=BCttler?= Date: Thu, 26 Feb 2026 08:35:51 +0100 Subject: [PATCH 11/35] only amd64 --- Makefile | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Makefile b/Makefile index c133346d33f5..2ced9168381f 100644 --- a/Makefile +++ b/Makefile @@ -242,7 +242,7 @@ CAPI_KIND_CLUSTER_NAME ?= capi-test TAG ?= dev ARCH ?= $(shell go env GOARCH) -ALL_ARCH ?= amd64 arm arm64 ppc64le s390x +ALL_ARCH ?= amd64 # Allow overriding the imagePullPolicy PULL_POLICY ?= Always @@ -1091,11 +1091,11 @@ release-manifests-dev: $(RELEASE_DIR) $(KUSTOMIZE) ## Build the development mani .PHONY: release-binaries release-binaries: ## Build the binaries to publish with a release RELEASE_BINARY=clusterctl-linux-amd64 BUILD_PATH=./cmd/clusterctl GOOS=linux GOARCH=amd64 $(MAKE) release-binary - RELEASE_BINARY=clusterctl-linux-arm64 BUILD_PATH=./cmd/clusterctl GOOS=linux GOARCH=arm64 $(MAKE) release-binary - RELEASE_BINARY=clusterctl-darwin-amd64 BUILD_PATH=./cmd/clusterctl GOOS=darwin GOARCH=amd64 $(MAKE) release-binary - RELEASE_BINARY=clusterctl-darwin-arm64 BUILD_PATH=./cmd/clusterctl GOOS=darwin GOARCH=arm64 $(MAKE) release-binary - RELEASE_BINARY=clusterctl-windows-amd64.exe BUILD_PATH=./cmd/clusterctl GOOS=windows GOARCH=amd64 $(MAKE) release-binary - RELEASE_BINARY=clusterctl-linux-ppc64le BUILD_PATH=./cmd/clusterctl GOOS=linux GOARCH=ppc64le $(MAKE) release-binary +# RELEASE_BINARY=clusterctl-linux-arm64 BUILD_PATH=./cmd/clusterctl GOOS=linux GOARCH=arm64 $(MAKE) release-binary +# RELEASE_BINARY=clusterctl-darwin-amd64 BUILD_PATH=./cmd/clusterctl GOOS=darwin GOARCH=amd64 $(MAKE) release-binary +# RELEASE_BINARY=clusterctl-darwin-arm64 BUILD_PATH=./cmd/clusterctl GOOS=darwin GOARCH=arm64 $(MAKE) release-binary +# RELEASE_BINARY=clusterctl-windows-amd64.exe BUILD_PATH=./cmd/clusterctl GOOS=windows GOARCH=amd64 $(MAKE) release-binary +# RELEASE_BINARY=clusterctl-linux-ppc64le BUILD_PATH=./cmd/clusterctl GOOS=linux GOARCH=ppc64le $(MAKE) release-binary .PHONY: release-binary release-binary: $(RELEASE_DIR) From 96448a86cd47200778e7251e4ceab653e0ee656b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=BCttler?= Date: Thu, 26 Feb 2026 08:39:22 +0100 Subject: [PATCH 12/35] handle git dirty in our script. --- Makefile | 6 +++--- hack/release.sh | 37 +++++++++++++++++++------------------ 2 files changed, 22 insertions(+), 21 deletions(-) diff --git a/Makefile b/Makefile index 2ced9168381f..95f2a37c1c43 100644 --- a/Makefile +++ b/Makefile @@ -1013,9 +1013,9 @@ $(RELEASE_NOTES_DIR): .PHONY: release release: clean-release ## Build and push container images using the latest git tag for the commit - @if [ -z "${RELEASE_TAG}" ]; then echo "RELEASE_TAG is not set"; exit 1; fi - @if ! [ -z "$$(git status --porcelain)" ]; then echo "Your local git repository contains uncommitted changes, use git clean before proceeding."; exit 1; fi - git checkout "${RELEASE_TAG}" + #@if [ -z "${RELEASE_TAG}" ]; then echo "RELEASE_TAG is not set"; exit 1; fi + #@if ! [ -z "$$(git status --porcelain)" ]; then echo "Your local git repository contains uncommitted changes, use git clean before proceeding."; exit 1; fi + #git checkout "${RELEASE_TAG}" # Build binaries first. GIT_VERSION=$(RELEASE_TAG) $(MAKE) release-binaries # Set the manifest images to the staging/production bucket and Builds the manifests to publish with a release. diff --git a/hack/release.sh b/hack/release.sh index a889c03146c0..9749319da8f2 100755 --- a/hack/release.sh +++ b/hack/release.sh @@ -3,30 +3,31 @@ trap 'echo -e "\n🤷 🚨 šŸ”„ Warning: A command has failed. Exiting the script. Line was ($0:$LINENO): $(sed -n "${LINENO}p" "$0" 2>/dev/null || true) šŸ”„ 🚨 🤷 "; exit 3' ERR set -Eeuo pipefail -if [[ -n "$(git status --porcelain --untracked-files=normal)" ]]; then - echo "failed: git working tree is dirty (modified, staged, or untracked files present)" - git status --short --untracked-files=normal - exit 1 -fi - if [[ -z ${RELEASE_TAG:-} ]]; then echo "failed: RELEASE_TAG is not set. Use v1.10.7-syself.XX" exit 1 fi -if ! git rev-parse --verify --quiet "refs/tags/${RELEASE_TAG}^{commit}" >/dev/null; then - echo "failed: RELEASE_TAG '${RELEASE_TAG}' does not exist as a git tag" - echo - echo "use: git tag $RELEASE_TAG" - echo - exit 1 -fi +if [[ -z $IGNORE_GIT_DIRTY ]]; then + if [[ -n "$(git status --porcelain --untracked-files=normal)" ]]; then + echo "failed: git working tree is dirty (modified, staged, or untracked files present)" + git status --short --untracked-files=normal + exit 1 + fi + if ! git rev-parse --verify --quiet "refs/tags/${RELEASE_TAG}^{commit}" >/dev/null; then + echo "failed: RELEASE_TAG '${RELEASE_TAG}' does not exist as a git tag" + echo + echo "use: git tag $RELEASE_TAG" + echo + exit 1 + fi -release_tag_commit="$(git rev-parse "refs/tags/${RELEASE_TAG}^{commit}")" -head_commit="$(git rev-parse HEAD)" -if [[ "${release_tag_commit}" != "${head_commit}" ]]; then - echo "failed: RELEASE_TAG '${RELEASE_TAG}' points to ${release_tag_commit}, but HEAD is ${head_commit}" - exit 1 + release_tag_commit="$(git rev-parse "refs/tags/${RELEASE_TAG}^{commit}")" + head_commit="$(git rev-parse HEAD)" + if [[ "${release_tag_commit}" != "${head_commit}" ]]; then + echo "failed: RELEASE_TAG '${RELEASE_TAG}' points to ${release_tag_commit}, but HEAD is ${head_commit}" + exit 1 + fi fi export PROD_REGISTRY=ghcr.io/syself/cluster-api From 9f4fa4d69ca13818cdcdf827c19889233176203c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=BCttler?= Date: Thu, 26 Feb 2026 08:47:34 +0100 Subject: [PATCH 13/35] set STAGING_REGISTRY, too. --- hack/release.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/hack/release.sh b/hack/release.sh index 9749319da8f2..aaad4a2f3032 100755 --- a/hack/release.sh +++ b/hack/release.sh @@ -30,6 +30,7 @@ if [[ -z $IGNORE_GIT_DIRTY ]]; then fi fi -export PROD_REGISTRY=ghcr.io/syself/cluster-api +export PROD_REGISTRY=ghcr.io/syself/cluster-api-prod +export STAGING_REGISTRY=ghcr.io/syself/cluster-api-staging make release From 5adff1b338314947b3b9fdad706cf06b8afa8229 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=BCttler?= Date: Thu, 26 Feb 2026 08:48:19 +0100 Subject: [PATCH 14/35] ... --- hack/release.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hack/release.sh b/hack/release.sh index aaad4a2f3032..c5a852afc743 100755 --- a/hack/release.sh +++ b/hack/release.sh @@ -8,7 +8,7 @@ if [[ -z ${RELEASE_TAG:-} ]]; then exit 1 fi -if [[ -z $IGNORE_GIT_DIRTY ]]; then +if [[ -z ${IGNORE_GIT_DIRTY:-} ]]; then if [[ -n "$(git status --porcelain --untracked-files=normal)" ]]; then echo "failed: git working tree is dirty (modified, staged, or untracked files present)" git status --short --untracked-files=normal From 611ec9f442c5193e5f121d5ea4969b60f3d32a9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=BCttler?= Date: Thu, 26 Feb 2026 08:50:29 +0100 Subject: [PATCH 15/35] .. --- Makefile | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 95f2a37c1c43..3a564fc06d7e 100644 --- a/Makefile +++ b/Makefile @@ -208,7 +208,7 @@ REGISTRY ?= gcr.io/$(shell gcloud config get-value project) PROD_REGISTRY ?= registry.k8s.io/cluster-api STAGING_REGISTRY ?= gcr.io/k8s-staging-cluster-api -STAGING_BUCKET ?= k8s-staging-cluster-api +#STAGING_BUCKET ?= k8s-staging-cluster-api # core IMAGE_NAME ?= cluster-api-controller @@ -1129,7 +1129,8 @@ release-staging: ## Build and push container images to the staging bucket $(MAKE) release-manifests-dev # Example manifest location: https://storage.googleapis.com/k8s-staging-cluster-api/components/main/core-components.yaml # Please note that these files are deleted after a certain period, at the time of this writing 60 days after file creation. - gsutil cp $(RELEASE_DIR)/* gs://$(STAGING_BUCKET)/components/$(RELEASE_ALIAS_TAG) + + ##gsutil cp $(RELEASE_DIR)/* gs://$(STAGING_BUCKET)/components/$(RELEASE_ALIAS_TAG) .PHONY: release-staging-nightly release-staging-nightly: ## Tag and push container images to the staging bucket. Example image tag: cluster-api-controller:nightly_main_20210121 @@ -1146,7 +1147,7 @@ release-staging-nightly: ## Tag and push container images to the staging bucket. $(MAKE) release-manifests-dev # Example manifest location: https://storage.googleapis.com/k8s-staging-cluster-api/components/nightly_main_20240425/core-components.yaml # Please note that these files are deleted after a certain period, at the time of this writing 60 days after file creation. - gsutil cp $(RELEASE_DIR)/* gs://$(STAGING_BUCKET)/components/$(NEW_RELEASE_ALIAS_TAG) + #gsutil cp $(RELEASE_DIR)/* gs://$(STAGING_BUCKET)/components/$(NEW_RELEASE_ALIAS_TAG) .PHONY: release-alias-tag release-alias-tag: ## Add the release alias tag to the last build tag From d66cefaa9cd2fee6280d5bc149c3bef7cc96b876 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=BCttler?= Date: Thu, 26 Feb 2026 08:59:41 +0100 Subject: [PATCH 16/35] set REGISTRY, too. --- Makefile | 3 +++ hack/release.sh | 1 + 2 files changed, 4 insertions(+) diff --git a/Makefile b/Makefile index 3a564fc06d7e..b5d541abc3bf 100644 --- a/Makefile +++ b/Makefile @@ -205,8 +205,11 @@ TILT_PREPARE := $(abspath $(TOOLS_BIN_DIR)/$(TILT_PREPARE_BIN)) # Define Docker related variables. Releases should modify and double check these vars. REGISTRY ?= gcr.io/$(shell gcloud config get-value project) + +# For string inside YAML files (in "out" directory) PROD_REGISTRY ?= registry.k8s.io/cluster-api +# For string inside YAML files (in "out" directory) STAGING_REGISTRY ?= gcr.io/k8s-staging-cluster-api #STAGING_BUCKET ?= k8s-staging-cluster-api diff --git a/hack/release.sh b/hack/release.sh index c5a852afc743..8fff751ac9a7 100755 --- a/hack/release.sh +++ b/hack/release.sh @@ -32,5 +32,6 @@ fi export PROD_REGISTRY=ghcr.io/syself/cluster-api-prod export STAGING_REGISTRY=ghcr.io/syself/cluster-api-staging +export REGISTRY=$PROD_REGISTRY make release From d48d38684da3a6b3c091c27059a98cd58fe9c886 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=BCttler?= Date: Thu, 26 Feb 2026 09:03:42 +0100 Subject: [PATCH 17/35] call docker push. --- Makefile | 15 ++++++++------- hack/release.sh | 2 ++ 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/Makefile b/Makefile index b5d541abc3bf..516ad02e13c5 100644 --- a/Makefile +++ b/Makefile @@ -204,7 +204,7 @@ TILT_PREPARE_BIN := tilt-prepare TILT_PREPARE := $(abspath $(TOOLS_BIN_DIR)/$(TILT_PREPARE_BIN)) # Define Docker related variables. Releases should modify and double check these vars. -REGISTRY ?= gcr.io/$(shell gcloud config get-value project) +REGISTRY ?= gcr.io/xxxxxx # For string inside YAML files (in "out" directory) PROD_REGISTRY ?= registry.k8s.io/cluster-api @@ -1154,12 +1154,13 @@ release-staging-nightly: ## Tag and push container images to the staging bucket. .PHONY: release-alias-tag release-alias-tag: ## Add the release alias tag to the last build tag - gcloud container images add-tag $(CONTROLLER_IMG):$(TAG) $(CONTROLLER_IMG):$(RELEASE_ALIAS_TAG) - gcloud container images add-tag $(KUBEADM_BOOTSTRAP_CONTROLLER_IMG):$(TAG) $(KUBEADM_BOOTSTRAP_CONTROLLER_IMG):$(RELEASE_ALIAS_TAG) - gcloud container images add-tag $(KUBEADM_CONTROL_PLANE_CONTROLLER_IMG):$(TAG) $(KUBEADM_CONTROL_PLANE_CONTROLLER_IMG):$(RELEASE_ALIAS_TAG) - gcloud container images add-tag $(CLUSTERCTL_IMG):$(TAG) $(CLUSTERCTL_IMG):$(RELEASE_ALIAS_TAG) - gcloud container images add-tag $(CAPD_CONTROLLER_IMG):$(TAG) $(CAPD_CONTROLLER_IMG):$(RELEASE_ALIAS_TAG) - gcloud container images add-tag $(TEST_EXTENSION_IMG):$(TAG) $(TEST_EXTENSION_IMG):$(RELEASE_ALIAS_TAG) + echo "Syself: skipping" +# gcloud container images add-tag $(CONTROLLER_IMG):$(TAG) $(CONTROLLER_IMG):$(RELEASE_ALIAS_TAG) +# gcloud container images add-tag $(KUBEADM_BOOTSTRAP_CONTROLLER_IMG):$(TAG) $(KUBEADM_BOOTSTRAP_CONTROLLER_IMG):$(RELEASE_ALIAS_TAG) +# gcloud container images add-tag $(KUBEADM_CONTROL_PLANE_CONTROLLER_IMG):$(TAG) $(KUBEADM_CONTROL_PLANE_CONTROLLER_IMG):$(RELEASE_ALIAS_TAG) +# gcloud container images add-tag $(CLUSTERCTL_IMG):$(TAG) $(CLUSTERCTL_IMG):$(RELEASE_ALIAS_TAG) +# gcloud container images add-tag $(CAPD_CONTROLLER_IMG):$(TAG) $(CAPD_CONTROLLER_IMG):$(RELEASE_ALIAS_TAG) +# gcloud container images add-tag $(TEST_EXTENSION_IMG):$(TAG) $(TEST_EXTENSION_IMG):$(RELEASE_ALIAS_TAG) .PHONY: release-notes-tool release-notes-tool: diff --git a/hack/release.sh b/hack/release.sh index 8fff751ac9a7..a4650dc94e07 100755 --- a/hack/release.sh +++ b/hack/release.sh @@ -35,3 +35,5 @@ export STAGING_REGISTRY=ghcr.io/syself/cluster-api-staging export REGISTRY=$PROD_REGISTRY make release + +make docker-push-all From a9db246659590ba0b0b1dd914dbe183e9399cfc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=BCttler?= Date: Thu, 26 Feb 2026 09:11:09 +0100 Subject: [PATCH 18/35] build before push. --- Makefile | 3 ++- hack/release.sh | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 516ad02e13c5..0adfa0028072 100644 --- a/Makefile +++ b/Makefile @@ -1107,10 +1107,11 @@ release-binary: $(RELEASE_DIR) -e CGO_ENABLED=0 \ -e GOOS=$(GOOS) \ -e GOARCH=$(GOARCH) \ - -e GOCACHE=/tmp/ \ + -e GOCACHE=/go/build-cache/ \ --user $$(id -u):$$(id -g) \ -v "$$(pwd):/workspace$(DOCKER_VOL_OPTS)" \ -v "$$(go env GOMODCACHE):/go/pkg/mod" \ + -v "$$(go env GOCACHE):/go/build-cache" \ -w /workspace \ golang:$(GO_VERSION) \ go build -a -trimpath -ldflags "$(LDFLAGS) -extldflags '-static'" \ diff --git a/hack/release.sh b/hack/release.sh index a4650dc94e07..bfc45edc649d 100755 --- a/hack/release.sh +++ b/hack/release.sh @@ -35,5 +35,5 @@ export STAGING_REGISTRY=ghcr.io/syself/cluster-api-staging export REGISTRY=$PROD_REGISTRY make release - +make docker-build-all make docker-push-all From 7d7c63f04e39257369a3917cdbf6042e6aaba446 Mon Sep 17 00:00:00 2001 From: Dhairya Arora Date: Thu, 26 Feb 2026 16:37:47 +0530 Subject: [PATCH 19/35] add syself new field comment on the cpclass patch selector match --- api/v1beta1/clusterclass_types.go | 1 + 1 file changed, 1 insertion(+) diff --git a/api/v1beta1/clusterclass_types.go b/api/v1beta1/clusterclass_types.go index ae81610c0499..897d48166554 100644 --- a/api/v1beta1/clusterclass_types.go +++ b/api/v1beta1/clusterclass_types.go @@ -1035,6 +1035,7 @@ type PatchSelectorMatch struct { // controlPlaneClass selects templates referenced in specific ControlPlaneClasses in // .spec.controlPlane.classes. + // syself new field. // +optional ControlPlaneClass *PatchSelectorMatchControlPlaneClass `json:"controlPlaneClass,omitempty"` From 973f37e94b191335e03dcec4ce21e8bfe53f99cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=BCttler?= Date: Thu, 26 Feb 2026 12:25:51 +0100 Subject: [PATCH 20/35] add: hack/create-capi-op-yaml.sh to create yaml for capi-operator. --- hack/create-capi-op-yaml.sh | 38 +++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100755 hack/create-capi-op-yaml.sh diff --git a/hack/create-capi-op-yaml.sh b/hack/create-capi-op-yaml.sh new file mode 100755 index 000000000000..fc58f46dd129 --- /dev/null +++ b/hack/create-capi-op-yaml.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +# Bash Strict Mode: https://github.com/guettli/bash-strict-mode +trap 'echo -e "\n🤷 🚨 šŸ”„ Warning: A command has failed. Exiting the script. Line was ($0:$LINENO): $(sed -n "${LINENO}p" "$0" 2>/dev/null || true) šŸ”„ 🚨 🤷 "; exit 3' ERR +set -Eeuo pipefail + +tmpdir="$(mktemp -d)" +gzip -c out/core-components.yaml >"${tmpdir}/core-components.yaml.gz" + +kubectl create configmap capi-core-custom-v1.10.7 -n mgt-system \ + --from-file=metadata=out/metadata.yaml \ + --from-file=components="${tmpdir}/core-components.yaml.gz" \ + --dry-run=client -o yaml | + kubectl label --local -f - \ + provider-components=core-custom \ + provider.cluster.x-k8s.io/version=v1.10.7 \ + -o yaml | + kubectl annotate --local -f - \ + provider.cluster.x-k8s.io/compressed=true \ + -o yaml \ + >out/operator-configmaps.yaml + +cat >out/operator-provider-patches.yaml <<'EOF' +apiVersion: operator.cluster.x-k8s.io/v1alpha2 +kind: CoreProvider +metadata: + name: cluster-api + namespace: mgt-system +spec: + version: v1.10.7 + fetchConfig: + selector: + matchLabels: + provider-components: core-custom + deployment: + containers: + - name: manager + imageUrl: ghcr.io/syself/cluster-api-prod/cluster-api-controller:v1.10.7-syself.6 +EOF From df7a08c92290567543102ad2484ab4ebef22623c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=BCttler?= Date: Thu, 26 Feb 2026 12:32:34 +0100 Subject: [PATCH 21/35] run create-capi-op-yaml.sh directly in release.sh --- hack/create-capi-op-yaml.sh | 31 +++++++++++++++++++------------ hack/release.sh | 3 +++ 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/hack/create-capi-op-yaml.sh b/hack/create-capi-op-yaml.sh index fc58f46dd129..00000894ce73 100755 --- a/hack/create-capi-op-yaml.sh +++ b/hack/create-capi-op-yaml.sh @@ -3,21 +3,25 @@ trap 'echo -e "\n🤷 🚨 šŸ”„ Warning: A command has failed. Exiting the script. Line was ($0:$LINENO): $(sed -n "${LINENO}p" "$0" 2>/dev/null || true) šŸ”„ 🚨 🤷 "; exit 3' ERR set -Eeuo pipefail +if [[ -z ${RELEASE_TAG:-} ]]; then + echo "RELEASE_TAG is missing" + exit 1 +fi tmpdir="$(mktemp -d)" gzip -c out/core-components.yaml >"${tmpdir}/core-components.yaml.gz" kubectl create configmap capi-core-custom-v1.10.7 -n mgt-system \ - --from-file=metadata=out/metadata.yaml \ - --from-file=components="${tmpdir}/core-components.yaml.gz" \ - --dry-run=client -o yaml | - kubectl label --local -f - \ - provider-components=core-custom \ - provider.cluster.x-k8s.io/version=v1.10.7 \ - -o yaml | - kubectl annotate --local -f - \ - provider.cluster.x-k8s.io/compressed=true \ - -o yaml \ - >out/operator-configmaps.yaml + --from-file=metadata=out/metadata.yaml \ + --from-file=components="${tmpdir}/core-components.yaml.gz" \ + --dry-run=client -o yaml | + kubectl label --local -f - \ + provider-components=core-custom \ + provider.cluster.x-k8s.io/version=v1.10.7 \ + -o yaml | + kubectl annotate --local -f - \ + provider.cluster.x-k8s.io/compressed=true \ + -o yaml \ + >out/operator-configmaps.yaml cat >out/operator-provider-patches.yaml <<'EOF' apiVersion: operator.cluster.x-k8s.io/v1alpha2 @@ -34,5 +38,8 @@ spec: deployment: containers: - name: manager - imageUrl: ghcr.io/syself/cluster-api-prod/cluster-api-controller:v1.10.7-syself.6 + imageUrl: ghcr.io/syself/cluster-api-prod/cluster-api-controller:$RELEASE_TAG EOF + +echo "Created: out/operator-configmaps.yaml and out/operator-provider-patches.yaml" +echo "You can apply that to a mgt-cluster running capi-op (like capi-patch-mgt-cluster2)" diff --git a/hack/release.sh b/hack/release.sh index bfc45edc649d..df1f5930527b 100755 --- a/hack/release.sh +++ b/hack/release.sh @@ -37,3 +37,6 @@ export REGISTRY=$PROD_REGISTRY make release make docker-build-all make docker-push-all + +./hack/create-capi-op-yaml.sh + From 2ef631ce4d3819490b37b7fbbaaebb042f3b248c Mon Sep 17 00:00:00 2001 From: Dhairya Arora Date: Thu, 26 Feb 2026 16:54:37 +0530 Subject: [PATCH 22/35] make class default to "" in clusterclass.spec.controlplaneclasses In order to fix the error on applying the updated CRDs ``` failed to patch provider object: CustomResourceDefinition.apiextensions.k8s.io \"clusterclasses.cluster.x-k8s.io\" is invalid: spec.versions[1].schema.openAPIV3Schema.properties[spec].properties[controlPlaneClasses].items.properties[class].default: Required value: this property is in x-kubernetes-list-map-keys, so it must have a default or be a required property ``` Signed-off-by: Dhairya Arora --- api/v1beta1/clusterclass_types.go | 3 +-- api/v1beta1/zz_generated.openapi.go | 3 ++- config/crd/bases/cluster.x-k8s.io_clusterclasses.yaml | 7 +++---- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/api/v1beta1/clusterclass_types.go b/api/v1beta1/clusterclass_types.go index 897d48166554..69aa872aa03f 100644 --- a/api/v1beta1/clusterclass_types.go +++ b/api/v1beta1/clusterclass_types.go @@ -164,8 +164,7 @@ type ControlPlaneClass struct { // When used in ControlPlaneTopologyClass.Classes, this name MUST be unique // within the list and can be referenced from the Cluster topology. // +optional - // +kubebuilder:validation:MinLength=1 - // +kubebuilder:validation:MaxLength=256 + // +default="" Class string `json:"class,omitempty"` // LocalObjectTemplate contains the reference to the control plane provider. diff --git a/api/v1beta1/zz_generated.openapi.go b/api/v1beta1/zz_generated.openapi.go index 11f46abe5e70..4e896cbb9d51 100644 --- a/api/v1beta1/zz_generated.openapi.go +++ b/api/v1beta1/zz_generated.openapi.go @@ -1283,6 +1283,7 @@ func schema_sigsk8sio_cluster_api_api_v1beta1_ControlPlaneClass(ref common.Refer "class": { SchemaProps: spec.SchemaProps{ Description: "class denotes a type of control-plane node present in the cluster. When used in ControlPlaneTopologyClass.Classes, this name MUST be unique within the list and can be referenced from the Cluster topology.", + Default: "", Type: []string{"string"}, Format: "", }, @@ -4603,7 +4604,7 @@ func schema_sigsk8sio_cluster_api_api_v1beta1_PatchSelectorMatch(ref common.Refe }, "controlPlaneClass": { SchemaProps: spec.SchemaProps{ - Description: "controlPlaneClass selects templates referenced in specific ControlPlaneClasses in .spec.controlPlane.classes.", + Description: "controlPlaneClass selects templates referenced in specific ControlPlaneClasses in .spec.controlPlane.classes. syself new field.", Ref: ref("sigs.k8s.io/cluster-api/api/v1beta1.PatchSelectorMatchControlPlaneClass"), }, }, diff --git a/config/crd/bases/cluster.x-k8s.io_clusterclasses.yaml b/config/crd/bases/cluster.x-k8s.io_clusterclasses.yaml index 8b869b64e6e3..534f16af7255 100644 --- a/config/crd/bases/cluster.x-k8s.io_clusterclasses.yaml +++ b/config/crd/bases/cluster.x-k8s.io_clusterclasses.yaml @@ -491,12 +491,11 @@ spec: for provisioning the Control Plane for the Cluster. properties: class: + default: "" description: |- class denotes a type of control-plane node present in the cluster. When used in ControlPlaneTopologyClass.Classes, this name MUST be unique within the list and can be referenced from the Cluster topology. - maxLength: 256 - minLength: 1 type: string machineHealthCheck: description: |- @@ -851,12 +850,11 @@ spec: plane. properties: class: + default: "" description: |- class denotes a type of control-plane node present in the cluster. When used in ControlPlaneTopologyClass.Classes, this name MUST be unique within the list and can be referenced from the Cluster topology. - maxLength: 256 - minLength: 1 type: string machineHealthCheck: description: |- @@ -1387,6 +1385,7 @@ spec: description: |- controlPlaneClass selects templates referenced in specific ControlPlaneClasses in .spec.controlPlane.classes. + syself new field. properties: names: description: names selects templates by class From 32c00a3ed29e59e82c56869a1ff9249e5f5cd73a Mon Sep 17 00:00:00 2001 From: Dhairya Arora Date: Thu, 26 Feb 2026 17:50:44 +0530 Subject: [PATCH 23/35] build and push only core images --- Makefile | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 0adfa0028072..148783f2fa1c 100644 --- a/Makefile +++ b/Makefile @@ -822,10 +822,11 @@ docker-build-%: # Choice of images to build/push ALL_DOCKER_BUILD ?= core kubeadm-bootstrap kubeadm-control-plane docker-infrastructure test-extension clusterctl +SYSELF_RELEVANT_DOCKER_BUILD ?= core .PHONY: docker-build docker-build: docker-pull-prerequisites ## Run docker-build-* targets for all the images - $(MAKE) ARCH=$(ARCH) $(addprefix docker-build-,$(ALL_DOCKER_BUILD)) + $(MAKE) ARCH=$(ARCH) $(addprefix docker-build-,$(SYSELF_RELEVANT_DOCKER_BUILD)) ALL_DOCKER_BUILD_E2E = core kubeadm-bootstrap kubeadm-control-plane docker-infrastructure test-extension @@ -1197,13 +1198,13 @@ docker-image-verify: ## Verifies all built images to contain the correct binary .PHONY: docker-push-all docker-push-all: $(addprefix docker-push-,$(ALL_ARCH)) ## Push the docker images to be included in the release for all architectures + related multiarch manifests - $(MAKE) ALL_ARCH="$(ALL_ARCH)" $(addprefix docker-push-manifest-,$(ALL_DOCKER_BUILD)) + $(MAKE) ALL_ARCH="$(ALL_ARCH)" $(addprefix docker-push-manifest-,$(SYSELF_RELEVANT_DOCKER_BUILD)) docker-push-%: $(MAKE) ARCH=$* docker-push .PHONY: docker-push -docker-push: $(addprefix docker-push-,$(ALL_DOCKER_BUILD)) ## Push the docker images to be included in the release +docker-push: $(addprefix docker-push-,$(SYSELF_RELEVANT_DOCKER_BUILD)) ## Push the docker images to be included in the release .PHONY: docker-push-core docker-push-core: ## Push the core docker image From 6932aa495feba2bdb3b65b7712c14da7d262b8c5 Mon Sep 17 00:00:00 2001 From: Dhairya Arora Date: Thu, 26 Feb 2026 18:31:20 +0530 Subject: [PATCH 24/35] fixup! build and push only core images --- hack/release.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/hack/release.sh b/hack/release.sh index df1f5930527b..80790aedcbfe 100755 --- a/hack/release.sh +++ b/hack/release.sh @@ -33,10 +33,12 @@ fi export PROD_REGISTRY=ghcr.io/syself/cluster-api-prod export STAGING_REGISTRY=ghcr.io/syself/cluster-api-staging export REGISTRY=$PROD_REGISTRY +export TAG=$RELEASE_TAG + +echo $RELEASE_TAG make release make docker-build-all make docker-push-all ./hack/create-capi-op-yaml.sh - From d942b0cc1c2d297acd566c662a2f90b77450f4bf Mon Sep 17 00:00:00 2001 From: Dhairya Arora Date: Thu, 26 Feb 2026 18:48:19 +0530 Subject: [PATCH 25/35] enable cluster topology feature gate in capi-operator --- hack/create-capi-op-yaml.sh | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/hack/create-capi-op-yaml.sh b/hack/create-capi-op-yaml.sh index 00000894ce73..60b175c2db29 100755 --- a/hack/create-capi-op-yaml.sh +++ b/hack/create-capi-op-yaml.sh @@ -35,6 +35,10 @@ spec: selector: matchLabels: provider-components: core-custom + manager: + featureGates: + ClusterTopology: true + RuntimeSDK: true deployment: containers: - name: manager From 5a3233075624f551991fcc0fc0dedcc2b9257002 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=BCttler?= Date: Fri, 27 Feb 2026 11:52:43 +0100 Subject: [PATCH 26/35] fix var expansion in script. --- hack/create-capi-op-yaml.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hack/create-capi-op-yaml.sh b/hack/create-capi-op-yaml.sh index 60b175c2db29..0e7ac0617719 100755 --- a/hack/create-capi-op-yaml.sh +++ b/hack/create-capi-op-yaml.sh @@ -23,7 +23,7 @@ kubectl create configmap capi-core-custom-v1.10.7 -n mgt-system \ -o yaml \ >out/operator-configmaps.yaml -cat >out/operator-provider-patches.yaml <<'EOF' +cat >out/operator-provider-patches.yaml < Date: Fri, 27 Feb 2026 16:34:48 +0530 Subject: [PATCH 27/35] add our local release/development flow to docs --- README.md | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 2f87ecc9b480..e7a604b99355 100644 --- a/README.md +++ b/README.md @@ -62,4 +62,32 @@ Participation in the Kubernetes community is governed by the [Kubernetes Code of [Good first issue]: https://github.com/kubernetes-sigs/cluster-api/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22 [Help wanted]: https://github.com/kubernetes-sigs/cluster-api/issues?utf8=%E2%9C%93&q=is%3Aopen+is%3Aissue+label%3A%22help+wanted%22+ - +# Release/Development (Syself Fork) + +1. Create the git tag for the release. + ```shell + git tag v1.10.7-syself.8 + ``` + +2. Export the release tag. + ```shell + export RELEASE_TAG=v1.10.7-syself.8 + ``` + +3. Run the release script. + ```shell + ./hack/release.sh + ``` + This will create the manifests in the `out/` directory. And push the CAPI controller-image to http://ghcr.io/syself/cluster-api-prod/cluster-api-controller-amd64:v1.10.7-syself.8 + +4. You can apply the generated `out/operator-configmaps.yaml` and `out/operator-provider-patches.yaml` to the management cluster. + ```shell + k apply -f ../cluster-api/out/operator-configmaps.yaml + k apply -f ../cluster-api/out/operator-provider-patches.yaml + ``` + +5. Ensure that the configured `--source` flag in CSO and CSPH deployments is set as `oci`. If its not set as `oci`, edit the same in `addonprovider` for `cso` and `csph`. + ```shell + k edit addonproviders.operator.cluster.x-k8s.io -n mgt-system cso + k edit addonproviders.operator.cluster.x-k8s.io -n mgt-system csph + ``` From c11e76523039024e8a51d578d68b6a768788a5cf Mon Sep 17 00:00:00 2001 From: Dhairya Arora Date: Fri, 27 Feb 2026 16:41:52 +0530 Subject: [PATCH 28/35] fixup! add our local release/development flow to docs --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index e7a604b99355..2783f3cbb677 100644 --- a/README.md +++ b/README.md @@ -91,3 +91,8 @@ Participation in the Kubernetes community is governed by the [Kubernetes Code of k edit addonproviders.operator.cluster.x-k8s.io -n mgt-system cso k edit addonproviders.operator.cluster.x-k8s.io -n mgt-system csph ``` + +6. Ensure that the secret `cluster-stack` containing Github and OCI credentials exist and is same as that of testing-cluster. Otherwise modify it and copy the values from the secret present in testing cluster. + ```shell + kubectl-modify-secret -n mgt-system cluster-stack + ``` From 3a9b32a912349bdb2355af6437b5276eae87656f Mon Sep 17 00:00:00 2001 From: Dhairya Arora Date: Fri, 27 Feb 2026 20:19:35 +0530 Subject: [PATCH 29/35] allow machine infrastructured to be changed for control-plane Signed-off-by: Dhairya Arora --- .../controllers/topology/cluster/reconcile_state.go | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/internal/controllers/topology/cluster/reconcile_state.go b/internal/controllers/topology/cluster/reconcile_state.go index b7626623e8d9..d98c3a7e19ad 100644 --- a/internal/controllers/topology/cluster/reconcile_state.go +++ b/internal/controllers/topology/cluster/reconcile_state.go @@ -311,13 +311,21 @@ func (r *Reconciler) reconcileControlPlane(ctx context.Context, s *scope.Scope) return false, errors.Wrapf(err, "failed to reconcile %s %s", s.Desired.ControlPlane.InfrastructureMachineTemplate.GetKind(), klog.KObj(s.Desired.ControlPlane.InfrastructureMachineTemplate)) } - // Create or update the MachineInfrastructureTemplate of the control plane. + // syself change. + // Determine the compatibility checker for the control plane infrastructure machine template. + // If the control plane class has changed, allow kind changes since the infrastructure type. + cpCompatibilityChecker := check.ObjectsAreCompatible + if s.Current.Cluster.Spec.Topology != nil && + s.Current.Cluster.Spec.Topology.ControlPlane.Class != s.Desired.Cluster.Spec.Topology.ControlPlane.Class { + cpCompatibilityChecker = check.ObjectsAreInTheSameNamespace + } + createdInfrastructureTemplate, err := r.reconcileReferencedTemplate(ctx, reconcileReferencedTemplateInput{ cluster: s.Current.Cluster, ref: cpInfraRef, current: s.Current.ControlPlane.InfrastructureMachineTemplate, desired: s.Desired.ControlPlane.InfrastructureMachineTemplate, - compatibilityChecker: check.ObjectsAreCompatible, + compatibilityChecker: cpCompatibilityChecker, templateNamePrefix: topologynames.ControlPlaneInfrastructureMachineTemplateNamePrefix(s.Current.Cluster.Name), }) if err != nil { From 95a06b49fa4f1f6a2bfca15a661ed758aed361e8 Mon Sep 17 00:00:00 2001 From: Dhairya Arora Date: Fri, 27 Feb 2026 20:44:03 +0530 Subject: [PATCH 30/35] fixup! allow machine infrastructured to be changed for control-plane --- internal/controllers/topology/cluster/reconcile_state.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/internal/controllers/topology/cluster/reconcile_state.go b/internal/controllers/topology/cluster/reconcile_state.go index d98c3a7e19ad..74d6dadfde9b 100644 --- a/internal/controllers/topology/cluster/reconcile_state.go +++ b/internal/controllers/topology/cluster/reconcile_state.go @@ -1185,10 +1185,12 @@ func (r *Reconciler) reconcileReferencedObject(ctx context.Context, in reconcile return true, nil } + // syself change + // Commenting out the strictly compatible check to allow changes in controlplane InfrastructureMachine template. // Check if the current and desired referenced object are compatible. - if allErrs := check.ObjectsAreStrictlyCompatible(in.current, in.desired); len(allErrs) > 0 { - return false, allErrs.ToAggregate() - } + // if allErrs := check.ObjectsAreStrictlyCompatible(in.current, in.desired); len(allErrs) > 0 { + // return false, allErrs.ToAggregate() + // } log = log.WithValues(in.current.GetKind(), klog.KObj(in.current)) ctx = ctrl.LoggerInto(ctx, log) From faf11ddf2cd4739d57017c74ce302cf765115521 Mon Sep 17 00:00:00 2001 From: Dhairya Arora Date: Fri, 27 Feb 2026 21:53:31 +0530 Subject: [PATCH 31/35] fixup! fixup! allow machine infrastructured to be changed for control-plane --- internal/controllers/topology/cluster/reconcile_state.go | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/internal/controllers/topology/cluster/reconcile_state.go b/internal/controllers/topology/cluster/reconcile_state.go index 74d6dadfde9b..4b93824e5426 100644 --- a/internal/controllers/topology/cluster/reconcile_state.go +++ b/internal/controllers/topology/cluster/reconcile_state.go @@ -312,20 +312,13 @@ func (r *Reconciler) reconcileControlPlane(ctx context.Context, s *scope.Scope) } // syself change. - // Determine the compatibility checker for the control plane infrastructure machine template. // If the control plane class has changed, allow kind changes since the infrastructure type. - cpCompatibilityChecker := check.ObjectsAreCompatible - if s.Current.Cluster.Spec.Topology != nil && - s.Current.Cluster.Spec.Topology.ControlPlane.Class != s.Desired.Cluster.Spec.Topology.ControlPlane.Class { - cpCompatibilityChecker = check.ObjectsAreInTheSameNamespace - } - createdInfrastructureTemplate, err := r.reconcileReferencedTemplate(ctx, reconcileReferencedTemplateInput{ cluster: s.Current.Cluster, ref: cpInfraRef, current: s.Current.ControlPlane.InfrastructureMachineTemplate, desired: s.Desired.ControlPlane.InfrastructureMachineTemplate, - compatibilityChecker: cpCompatibilityChecker, + compatibilityChecker: check.ObjectsAreInTheSameNamespace, templateNamePrefix: topologynames.ControlPlaneInfrastructureMachineTemplateNamePrefix(s.Current.Cluster.Name), }) if err != nil { From 975113e18e7eefa151bb23978e6cf8112952aca9 Mon Sep 17 00:00:00 2001 From: Dhairya Arora Date: Fri, 27 Feb 2026 22:09:30 +0530 Subject: [PATCH 32/35] fixup! fixup! fixup! allow machine infrastructured to be changed for control-plane --- .../topology/cluster/reconcile_state.go | 28 ++++++++++++++++--- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/internal/controllers/topology/cluster/reconcile_state.go b/internal/controllers/topology/cluster/reconcile_state.go index 4b93824e5426..92f0a599d5c0 100644 --- a/internal/controllers/topology/cluster/reconcile_state.go +++ b/internal/controllers/topology/cluster/reconcile_state.go @@ -311,14 +311,34 @@ func (r *Reconciler) reconcileControlPlane(ctx context.Context, s *scope.Scope) return false, errors.Wrapf(err, "failed to reconcile %s %s", s.Desired.ControlPlane.InfrastructureMachineTemplate.GetKind(), klog.KObj(s.Desired.ControlPlane.InfrastructureMachineTemplate)) } - // syself change. - // If the control plane class has changed, allow kind changes since the infrastructure type. + // syself change: determine if control plane class has changed. + cpClassChanged := false + if s.Current.Cluster.Spec.Topology != nil && s.Desired.Cluster.Spec.Topology != nil { + cpClassChanged = s.Current.Cluster.Spec.Topology.ControlPlane.Class != s.Desired.Cluster.Spec.Topology.ControlPlane.Class + } + + // syself change: if the control plane class has changed, the infrastructure + // machine template kind is different. We cant patch across kinds, so treat + // this as a new creation by passing nil as current. + currentCPInfraMachineTemplate := s.Current.ControlPlane.InfrastructureMachineTemplate + cpCompatibilityChecker := check.ObjectsAreCompatible + if cpClassChanged { + log.Info( + "Control plane class changed, forcing infrastructure template rotation", + "currentKind", s.Current.ControlPlane.InfrastructureMachineTemplate.GetKind(), + "desiredKind", s.Desired.ControlPlane.InfrastructureMachineTemplate.GetKind(), + ) + + currentCPInfraMachineTemplate = nil + cpCompatibilityChecker = check.ObjectsAreInTheSameNamespace + } + createdInfrastructureTemplate, err := r.reconcileReferencedTemplate(ctx, reconcileReferencedTemplateInput{ cluster: s.Current.Cluster, ref: cpInfraRef, - current: s.Current.ControlPlane.InfrastructureMachineTemplate, + current: currentCPInfraMachineTemplate, desired: s.Desired.ControlPlane.InfrastructureMachineTemplate, - compatibilityChecker: check.ObjectsAreInTheSameNamespace, + compatibilityChecker: cpCompatibilityChecker, templateNamePrefix: topologynames.ControlPlaneInfrastructureMachineTemplateNamePrefix(s.Current.Cluster.Name), }) if err != nil { From 97941e6962bb8a133e0a0459dd1448f606c2d16e Mon Sep 17 00:00:00 2001 From: Dhairya Arora Date: Fri, 27 Feb 2026 22:16:35 +0530 Subject: [PATCH 33/35] fixup! fixup! fixup! fixup! allow machine infrastructured to be changed for control-plane --- internal/controllers/topology/cluster/reconcile_state.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/internal/controllers/topology/cluster/reconcile_state.go b/internal/controllers/topology/cluster/reconcile_state.go index 92f0a599d5c0..d71a27f49024 100644 --- a/internal/controllers/topology/cluster/reconcile_state.go +++ b/internal/controllers/topology/cluster/reconcile_state.go @@ -321,7 +321,7 @@ func (r *Reconciler) reconcileControlPlane(ctx context.Context, s *scope.Scope) // machine template kind is different. We cant patch across kinds, so treat // this as a new creation by passing nil as current. currentCPInfraMachineTemplate := s.Current.ControlPlane.InfrastructureMachineTemplate - cpCompatibilityChecker := check.ObjectsAreCompatible + if cpClassChanged { log.Info( "Control plane class changed, forcing infrastructure template rotation", @@ -330,7 +330,6 @@ func (r *Reconciler) reconcileControlPlane(ctx context.Context, s *scope.Scope) ) currentCPInfraMachineTemplate = nil - cpCompatibilityChecker = check.ObjectsAreInTheSameNamespace } createdInfrastructureTemplate, err := r.reconcileReferencedTemplate(ctx, reconcileReferencedTemplateInput{ @@ -338,7 +337,7 @@ func (r *Reconciler) reconcileControlPlane(ctx context.Context, s *scope.Scope) ref: cpInfraRef, current: currentCPInfraMachineTemplate, desired: s.Desired.ControlPlane.InfrastructureMachineTemplate, - compatibilityChecker: cpCompatibilityChecker, + compatibilityChecker: check.ObjectsAreInTheSameNamespace, templateNamePrefix: topologynames.ControlPlaneInfrastructureMachineTemplateNamePrefix(s.Current.Cluster.Name), }) if err != nil { From d40d2ee60ef2bc02800c624b55098324a38e61de Mon Sep 17 00:00:00 2001 From: Dhairya Arora Date: Fri, 27 Feb 2026 22:27:48 +0530 Subject: [PATCH 34/35] fixup! fixup! fixup! fixup! fixup! allow machine infrastructured to be changed for control-plane --- .../topology/cluster/reconcile_state.go | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/internal/controllers/topology/cluster/reconcile_state.go b/internal/controllers/topology/cluster/reconcile_state.go index d71a27f49024..7ed4b0c1be9d 100644 --- a/internal/controllers/topology/cluster/reconcile_state.go +++ b/internal/controllers/topology/cluster/reconcile_state.go @@ -312,32 +312,31 @@ func (r *Reconciler) reconcileControlPlane(ctx context.Context, s *scope.Scope) } // syself change: determine if control plane class has changed. - cpClassChanged := false - if s.Current.Cluster.Spec.Topology != nil && s.Desired.Cluster.Spec.Topology != nil { - cpClassChanged = s.Current.Cluster.Spec.Topology.ControlPlane.Class != s.Desired.Cluster.Spec.Topology.ControlPlane.Class - } - - // syself change: if the control plane class has changed, the infrastructure - // machine template kind is different. We cant patch across kinds, so treat - // this as a new creation by passing nil as current. currentCPInfraMachineTemplate := s.Current.ControlPlane.InfrastructureMachineTemplate - - if cpClassChanged { + cpInfraKindChanged := false + if s.Current.ControlPlane.InfrastructureMachineTemplate != nil && + s.Desired.ControlPlane.InfrastructureMachineTemplate != nil && + s.Current.ControlPlane.InfrastructureMachineTemplate.GetKind() != s.Desired.ControlPlane.InfrastructureMachineTemplate.GetKind() { + cpInfraKindChanged = true log.Info( - "Control plane class changed, forcing infrastructure template rotation", + "Control plane infrastructure kind changed", "currentKind", s.Current.ControlPlane.InfrastructureMachineTemplate.GetKind(), "desiredKind", s.Desired.ControlPlane.InfrastructureMachineTemplate.GetKind(), ) - currentCPInfraMachineTemplate = nil } + compatibilityChecker := check.ObjectsAreCompatible + if cpInfraKindChanged { + compatibilityChecker = check.ObjectsAreInTheSameNamespace + } + createdInfrastructureTemplate, err := r.reconcileReferencedTemplate(ctx, reconcileReferencedTemplateInput{ cluster: s.Current.Cluster, ref: cpInfraRef, current: currentCPInfraMachineTemplate, desired: s.Desired.ControlPlane.InfrastructureMachineTemplate, - compatibilityChecker: check.ObjectsAreInTheSameNamespace, + compatibilityChecker: compatibilityChecker, templateNamePrefix: topologynames.ControlPlaneInfrastructureMachineTemplateNamePrefix(s.Current.Cluster.Name), }) if err != nil { From 1c8b9c30f65d5750a6a89009a442418db828503c Mon Sep 17 00:00:00 2001 From: Dhairya Arora Date: Sat, 28 Feb 2026 12:33:13 +0530 Subject: [PATCH 35/35] fixup! fixup! fixup! fixup! fixup! fixup! allow machine infrastructured to be changed for control-plane --- internal/controllers/topology/cluster/reconcile_state.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/internal/controllers/topology/cluster/reconcile_state.go b/internal/controllers/topology/cluster/reconcile_state.go index 7ed4b0c1be9d..b9c1aebf0f98 100644 --- a/internal/controllers/topology/cluster/reconcile_state.go +++ b/internal/controllers/topology/cluster/reconcile_state.go @@ -323,6 +323,10 @@ func (r *Reconciler) reconcileControlPlane(ctx context.Context, s *scope.Scope) "currentKind", s.Current.ControlPlane.InfrastructureMachineTemplate.GetKind(), "desiredKind", s.Desired.ControlPlane.InfrastructureMachineTemplate.GetKind(), ) + + // Setting currentCPInfraMachineTemplate as nil so that method reconcileReferencedTemplate do not + // try to patch the existing template. Otherwise patching will fail as we cannot patch the `Kind` + // of an object. currentCPInfraMachineTemplate = nil }