From 3cad93d395282c33de7a3dcae1e062ceafc0dda2 Mon Sep 17 00:00:00 2001 From: Christian Schlotter Date: Tue, 23 Dec 2025 08:52:05 +0100 Subject: [PATCH 1/8] topology: implement taint fields and conversion --- api/core/v1beta1/conversion.go | 10 +++++ api/core/v1beta2/cluster_types.go | 51 ++++++++++++++++++++++++++ api/core/v1beta2/clusterclass_types.go | 51 ++++++++++++++++++++++++++ 3 files changed, 112 insertions(+) diff --git a/api/core/v1beta1/conversion.go b/api/core/v1beta1/conversion.go index 0cbf6fb8930c..195097055441 100644 --- a/api/core/v1beta1/conversion.go +++ b/api/core/v1beta1/conversion.go @@ -74,8 +74,13 @@ func (src *Cluster) ConvertTo(dstRaw conversion.Hub) error { } dst.Spec.Topology.ControlPlane.HealthCheck.Checks.UnhealthyMachineConditions = restored.Spec.Topology.ControlPlane.HealthCheck.Checks.UnhealthyMachineConditions + dst.Spec.Topology.ControlPlane.Taints = restored.Spec.Topology.ControlPlane.Taints for i, md := range restored.Spec.Topology.Workers.MachineDeployments { dst.Spec.Topology.Workers.MachineDeployments[i].HealthCheck.Checks.UnhealthyMachineConditions = md.HealthCheck.Checks.UnhealthyMachineConditions + dst.Spec.Topology.Workers.MachineDeployments[i].Taints = md.Taints + } + for i, mp := range restored.Spec.Topology.Workers.MachinePools { + dst.Spec.Topology.Workers.MachinePools[i].Taints = mp.Taints } // Recover intent for bool values converted to *bool. @@ -151,8 +156,13 @@ func (src *ClusterClass) ConvertTo(dstRaw conversion.Hub) error { } dst.Spec.ControlPlane.HealthCheck.Checks.UnhealthyMachineConditions = restored.Spec.ControlPlane.HealthCheck.Checks.UnhealthyMachineConditions + dst.Spec.ControlPlane.Taints = restored.Spec.ControlPlane.Taints for i, md := range restored.Spec.Workers.MachineDeployments { dst.Spec.Workers.MachineDeployments[i].HealthCheck.Checks.UnhealthyMachineConditions = md.HealthCheck.Checks.UnhealthyMachineConditions + dst.Spec.Workers.MachineDeployments[i].Taints = md.Taints + } + for i, mp := range restored.Spec.Workers.MachinePools { + dst.Spec.Workers.MachinePools[i].Taints = mp.Taints } // Recover intent for bool values converted to *bool. diff --git a/api/core/v1beta2/cluster_types.go b/api/core/v1beta2/cluster_types.go index 4c17b933282a..7fa3da2c64df 100644 --- a/api/core/v1beta2/cluster_types.go +++ b/api/core/v1beta2/cluster_types.go @@ -641,6 +641,23 @@ type ControlPlaneTopology struct { // +optional Deletion ControlPlaneTopologyMachineDeletionSpec `json:"deletion,omitempty,omitzero"` + // taints are the node taints that Cluster API will manage. + // This list is not necessarily complete: other Kubernetes components may add or remove other taints from nodes, + // e.g. the node controller might add the node.kubernetes.io/not-ready taint. + // Only those taints defined in this list will be added or removed by core Cluster API controllers. + // + // There can be at most 64 taints. + // A pod would have to tolerate all existing taints to run on the corresponding node. + // + // NOTE: This list is implemented as a "map" type, meaning that individual elements can be managed by different owners. + // +optional + // +listType=map + // +listMapKey=key + // +listMapKey=effect + // +kubebuilder:validation:MinItems=1 + // +kubebuilder:validation:MaxItems=64 + Taints []MachineTaint `json:"taints,omitempty"` + // 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 @@ -890,6 +907,23 @@ type MachineDeploymentTopology struct { // +optional Deletion MachineDeploymentTopologyMachineDeletionSpec `json:"deletion,omitempty,omitzero"` + // taints are the node taints that Cluster API will manage. + // This list is not necessarily complete: other Kubernetes components may add or remove other taints from nodes, + // e.g. the node controller might add the node.kubernetes.io/not-ready taint. + // Only those taints defined in this list will be added or removed by core Cluster API controllers. + // + // There can be at most 64 taints. + // A pod would have to tolerate all existing taints to run on the corresponding node. + // + // NOTE: This list is implemented as a "map" type, meaning that individual elements can be managed by different owners. + // +optional + // +listType=map + // +listMapKey=key + // +listMapKey=effect + // +kubebuilder:validation:MinItems=1 + // +kubebuilder:validation:MaxItems=64 + Taints []MachineTaint `json:"taints,omitempty"` + // minReadySeconds is the minimum number of seconds for which a newly created machine should // be ready. // Defaults to 0 (machine will be considered available as soon as it @@ -1202,6 +1236,23 @@ type MachinePoolTopology struct { // +optional Deletion MachinePoolTopologyMachineDeletionSpec `json:"deletion,omitempty,omitzero"` + // taints are the node taints that Cluster API will manage. + // This list is not necessarily complete: other Kubernetes components may add or remove other taints from nodes, + // e.g. the node controller might add the node.kubernetes.io/not-ready taint. + // Only those taints defined in this list will be added or removed by core Cluster API controllers. + // + // There can be at most 64 taints. + // A pod would have to tolerate all existing taints to run on the corresponding node. + // + // NOTE: This list is implemented as a "map" type, meaning that individual elements can be managed by different owners. + // +optional + // +listType=map + // +listMapKey=key + // +listMapKey=effect + // +kubebuilder:validation:MinItems=1 + // +kubebuilder:validation:MaxItems=64 + Taints []MachineTaint `json:"taints,omitempty"` + // minReadySeconds is the minimum number of seconds for which a newly created machine pool should // be ready. // Defaults to 0 (machine will be considered available as soon as it diff --git a/api/core/v1beta2/clusterclass_types.go b/api/core/v1beta2/clusterclass_types.go index 12e8cc19c5d4..17e09e48a0cb 100644 --- a/api/core/v1beta2/clusterclass_types.go +++ b/api/core/v1beta2/clusterclass_types.go @@ -203,6 +203,23 @@ type ControlPlaneClass struct { // +optional Deletion ControlPlaneClassMachineDeletionSpec `json:"deletion,omitempty,omitzero"` + // taints are the node taints that Cluster API will manage. + // This list is not necessarily complete: other Kubernetes components may add or remove other taints from nodes, + // e.g. the node controller might add the node.kubernetes.io/not-ready taint. + // Only those taints defined in this list will be added or removed by core Cluster API controllers. + // + // There can be at most 64 taints. + // A pod would have to tolerate all existing taints to run on the corresponding node. + // + // NOTE: This list is implemented as a "map" type, meaning that individual elements can be managed by different owners. + // +optional + // +listType=map + // +listMapKey=key + // +listMapKey=effect + // +kubebuilder:validation:MinItems=1 + // +kubebuilder:validation:MaxItems=64 + Taints []MachineTaint `json:"taints,omitempty"` + // 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 @@ -462,6 +479,23 @@ type MachineDeploymentClass struct { // +optional Deletion MachineDeploymentClassMachineDeletionSpec `json:"deletion,omitempty,omitzero"` + // taints are the node taints that Cluster API will manage. + // This list is not necessarily complete: other Kubernetes components may add or remove other taints from nodes, + // e.g. the node controller might add the node.kubernetes.io/not-ready taint. + // Only those taints defined in this list will be added or removed by core Cluster API controllers. + // + // There can be at most 64 taints. + // A pod would have to tolerate all existing taints to run on the corresponding node. + // + // NOTE: This list is implemented as a "map" type, meaning that individual elements can be managed by different owners. + // +optional + // +listType=map + // +listMapKey=key + // +listMapKey=effect + // +kubebuilder:validation:MinItems=1 + // +kubebuilder:validation:MaxItems=64 + Taints []MachineTaint `json:"taints,omitempty"` + // minReadySeconds is the minimum number of seconds for which a newly created machine should // be ready. // Defaults to 0 (machine will be considered available as soon as it @@ -775,6 +809,23 @@ type MachinePoolClass struct { // +optional Deletion MachinePoolClassMachineDeletionSpec `json:"deletion,omitempty,omitzero"` + // taints are the node taints that Cluster API will manage. + // This list is not necessarily complete: other Kubernetes components may add or remove other taints from nodes, + // e.g. the node controller might add the node.kubernetes.io/not-ready taint. + // Only those taints defined in this list will be added or removed by core Cluster API controllers. + // + // There can be at most 64 taints. + // A pod would have to tolerate all existing taints to run on the corresponding node. + // + // NOTE: This list is implemented as a "map" type, meaning that individual elements can be managed by different owners. + // +optional + // +listType=map + // +listMapKey=key + // +listMapKey=effect + // +kubebuilder:validation:MinItems=1 + // +kubebuilder:validation:MaxItems=64 + Taints []MachineTaint `json:"taints,omitempty"` + // minReadySeconds is the minimum number of seconds for which a newly created machine pool should // be ready. // Defaults to 0 (machine will be considered available as soon as it From c331a2068946689ee3c3892af52ad38651d8a707 Mon Sep 17 00:00:00 2001 From: Christian Schlotter Date: Tue, 23 Dec 2025 08:52:44 +0100 Subject: [PATCH 2/8] make generate --- api/core/v1beta1/zz_generated.conversion.go | 6 + api/core/v1beta2/zz_generated.deepcopy.go | 30 +++ api/core/v1beta2/zz_generated.openapi.go | 150 +++++++++++- .../cluster.x-k8s.io_clusterclasses.yaml | 214 +++++++++++++++++ .../crd/bases/cluster.x-k8s.io_clusters.yaml | 217 ++++++++++++++++++ 5 files changed, 611 insertions(+), 6 deletions(-) diff --git a/api/core/v1beta1/zz_generated.conversion.go b/api/core/v1beta1/zz_generated.conversion.go index 56dd2d9a585c..2b1f8a88b6c3 100644 --- a/api/core/v1beta1/zz_generated.conversion.go +++ b/api/core/v1beta1/zz_generated.conversion.go @@ -1623,6 +1623,7 @@ func autoConvert_v1beta2_ControlPlaneClass_To_v1beta1_ControlPlaneClass(in *v1be // WARNING: in.HealthCheck requires manual conversion: does not exist in peer-type // WARNING: in.Naming requires manual conversion: does not exist in peer-type // WARNING: in.Deletion requires manual conversion: does not exist in peer-type + // WARNING: in.Taints requires manual conversion: does not exist in peer-type out.ReadinessGates = *(*[]MachineReadinessGate)(unsafe.Pointer(&in.ReadinessGates)) return nil } @@ -1648,6 +1649,7 @@ func autoConvert_v1beta2_ControlPlaneTopology_To_v1beta1_ControlPlaneTopology(in out.Replicas = (*int32)(unsafe.Pointer(in.Replicas)) // WARNING: in.HealthCheck requires manual conversion: does not exist in peer-type // WARNING: in.Deletion requires manual conversion: does not exist in peer-type + // WARNING: in.Taints requires manual conversion: does not exist in peer-type out.ReadinessGates = *(*[]MachineReadinessGate)(unsafe.Pointer(&in.ReadinessGates)) // WARNING: in.Variables requires manual conversion: inconvertible types (sigs.k8s.io/cluster-api/api/core/v1beta2.ControlPlaneVariables vs *sigs.k8s.io/cluster-api/api/core/v1beta1.ControlPlaneVariables) return nil @@ -2131,6 +2133,7 @@ func autoConvert_v1beta2_MachineDeploymentClass_To_v1beta1_MachineDeploymentClas } // WARNING: in.Naming requires manual conversion: does not exist in peer-type // WARNING: in.Deletion requires manual conversion: does not exist in peer-type + // WARNING: in.Taints requires manual conversion: does not exist in peer-type out.MinReadySeconds = (*int32)(unsafe.Pointer(in.MinReadySeconds)) out.ReadinessGates = *(*[]MachineReadinessGate)(unsafe.Pointer(&in.ReadinessGates)) // WARNING: in.Rollout requires manual conversion: does not exist in peer-type @@ -2307,6 +2310,7 @@ func autoConvert_v1beta2_MachineDeploymentTopology_To_v1beta1_MachineDeploymentT out.Replicas = (*int32)(unsafe.Pointer(in.Replicas)) // WARNING: in.HealthCheck requires manual conversion: does not exist in peer-type // WARNING: in.Deletion requires manual conversion: does not exist in peer-type + // WARNING: in.Taints requires manual conversion: does not exist in peer-type out.MinReadySeconds = (*int32)(unsafe.Pointer(in.MinReadySeconds)) out.ReadinessGates = *(*[]MachineReadinessGate)(unsafe.Pointer(&in.ReadinessGates)) // WARNING: in.Rollout requires manual conversion: does not exist in peer-type @@ -2737,6 +2741,7 @@ func autoConvert_v1beta2_MachinePoolClass_To_v1beta1_MachinePoolClass(in *v1beta out.FailureDomains = *(*[]string)(unsafe.Pointer(&in.FailureDomains)) // WARNING: in.Naming requires manual conversion: does not exist in peer-type // WARNING: in.Deletion requires manual conversion: does not exist in peer-type + // WARNING: in.Taints requires manual conversion: does not exist in peer-type out.MinReadySeconds = (*int32)(unsafe.Pointer(in.MinReadySeconds)) return nil } @@ -2898,6 +2903,7 @@ func autoConvert_v1beta2_MachinePoolTopology_To_v1beta1_MachinePoolTopology(in * out.Name = in.Name out.FailureDomains = *(*[]string)(unsafe.Pointer(&in.FailureDomains)) // WARNING: in.Deletion requires manual conversion: does not exist in peer-type + // WARNING: in.Taints requires manual conversion: does not exist in peer-type out.MinReadySeconds = (*int32)(unsafe.Pointer(in.MinReadySeconds)) out.Replicas = (*int32)(unsafe.Pointer(in.Replicas)) // WARNING: in.Variables requires manual conversion: inconvertible types (sigs.k8s.io/cluster-api/api/core/v1beta2.MachinePoolVariables vs *sigs.k8s.io/cluster-api/api/core/v1beta1.MachinePoolVariables) diff --git a/api/core/v1beta2/zz_generated.deepcopy.go b/api/core/v1beta2/zz_generated.deepcopy.go index 5adb8a56c456..a461d97f1965 100644 --- a/api/core/v1beta2/zz_generated.deepcopy.go +++ b/api/core/v1beta2/zz_generated.deepcopy.go @@ -791,6 +791,11 @@ func (in *ControlPlaneClass) DeepCopyInto(out *ControlPlaneClass) { in.HealthCheck.DeepCopyInto(&out.HealthCheck) out.Naming = in.Naming in.Deletion.DeepCopyInto(&out.Deletion) + if in.Taints != nil { + in, out := &in.Taints, &out.Taints + *out = make([]MachineTaint, len(*in)) + copy(*out, *in) + } if in.ReadinessGates != nil { in, out := &in.ReadinessGates, &out.ReadinessGates *out = make([]MachineReadinessGate, len(*in)) @@ -968,6 +973,11 @@ func (in *ControlPlaneTopology) DeepCopyInto(out *ControlPlaneTopology) { } in.HealthCheck.DeepCopyInto(&out.HealthCheck) in.Deletion.DeepCopyInto(&out.Deletion) + if in.Taints != nil { + in, out := &in.Taints, &out.Taints + *out = make([]MachineTaint, len(*in)) + copy(*out, *in) + } if in.ReadinessGates != nil { in, out := &in.ReadinessGates, &out.ReadinessGates *out = make([]MachineReadinessGate, len(*in)) @@ -1547,6 +1557,11 @@ func (in *MachineDeploymentClass) DeepCopyInto(out *MachineDeploymentClass) { in.HealthCheck.DeepCopyInto(&out.HealthCheck) out.Naming = in.Naming in.Deletion.DeepCopyInto(&out.Deletion) + if in.Taints != nil { + in, out := &in.Taints, &out.Taints + *out = make([]MachineTaint, len(*in)) + copy(*out, *in) + } if in.MinReadySeconds != nil { in, out := &in.MinReadySeconds, &out.MinReadySeconds *out = new(int32) @@ -2031,6 +2046,11 @@ func (in *MachineDeploymentTopology) DeepCopyInto(out *MachineDeploymentTopology } in.HealthCheck.DeepCopyInto(&out.HealthCheck) in.Deletion.DeepCopyInto(&out.Deletion) + if in.Taints != nil { + in, out := &in.Taints, &out.Taints + *out = make([]MachineTaint, len(*in)) + copy(*out, *in) + } if in.MinReadySeconds != nil { in, out := &in.MinReadySeconds, &out.MinReadySeconds *out = new(int32) @@ -2841,6 +2861,11 @@ func (in *MachinePoolClass) DeepCopyInto(out *MachinePoolClass) { } out.Naming = in.Naming in.Deletion.DeepCopyInto(&out.Deletion) + if in.Taints != nil { + in, out := &in.Taints, &out.Taints + *out = make([]MachineTaint, len(*in)) + copy(*out, *in) + } if in.MinReadySeconds != nil { in, out := &in.MinReadySeconds, &out.MinReadySeconds *out = new(int32) @@ -3106,6 +3131,11 @@ func (in *MachinePoolTopology) DeepCopyInto(out *MachinePoolTopology) { copy(*out, *in) } in.Deletion.DeepCopyInto(&out.Deletion) + if in.Taints != nil { + in, out := &in.Taints, &out.Taints + *out = make([]MachineTaint, len(*in)) + copy(*out, *in) + } if in.MinReadySeconds != nil { in, out := &in.MinReadySeconds, &out.MinReadySeconds *out = new(int32) diff --git a/api/core/v1beta2/zz_generated.openapi.go b/api/core/v1beta2/zz_generated.openapi.go index ecbbcd8de077..764438addd5f 100644 --- a/api/core/v1beta2/zz_generated.openapi.go +++ b/api/core/v1beta2/zz_generated.openapi.go @@ -1596,6 +1596,29 @@ func schema_cluster_api_api_core_v1beta2_ControlPlaneClass(ref common.ReferenceC Ref: ref("sigs.k8s.io/cluster-api/api/core/v1beta2.ControlPlaneClassMachineDeletionSpec"), }, }, + "taints": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-list-map-keys": []interface{}{ + "key", + "effect", + }, + "x-kubernetes-list-type": "map", + }, + }, + SchemaProps: spec.SchemaProps{ + Description: "taints are the node taints that Cluster API will manage. This list is not necessarily complete: other Kubernetes components may add or remove other taints from nodes, e.g. the node controller might add the node.kubernetes.io/not-ready taint. Only those taints defined in this list will be added or removed by core Cluster API controllers.\n\nThere can be at most 64 taints. A pod would have to tolerate all existing taints to run on the corresponding node.\n\nNOTE: This list is implemented as a \"map\" type, meaning that individual elements can be managed by different owners.", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("sigs.k8s.io/cluster-api/api/core/v1beta2.MachineTaint"), + }, + }, + }, + }, + }, "readinessGates": { VendorExtensible: spec.VendorExtensible{ Extensions: spec.Extensions{ @@ -1623,7 +1646,7 @@ func schema_cluster_api_api_core_v1beta2_ControlPlaneClass(ref common.ReferenceC }, }, Dependencies: []string{ - "sigs.k8s.io/cluster-api/api/core/v1beta2.ClusterClassTemplateReference", "sigs.k8s.io/cluster-api/api/core/v1beta2.ControlPlaneClassHealthCheck", "sigs.k8s.io/cluster-api/api/core/v1beta2.ControlPlaneClassMachineDeletionSpec", "sigs.k8s.io/cluster-api/api/core/v1beta2.ControlPlaneClassMachineInfrastructureTemplate", "sigs.k8s.io/cluster-api/api/core/v1beta2.ControlPlaneClassNamingSpec", "sigs.k8s.io/cluster-api/api/core/v1beta2.MachineReadinessGate", "sigs.k8s.io/cluster-api/api/core/v1beta2.ObjectMeta"}, + "sigs.k8s.io/cluster-api/api/core/v1beta2.ClusterClassTemplateReference", "sigs.k8s.io/cluster-api/api/core/v1beta2.ControlPlaneClassHealthCheck", "sigs.k8s.io/cluster-api/api/core/v1beta2.ControlPlaneClassMachineDeletionSpec", "sigs.k8s.io/cluster-api/api/core/v1beta2.ControlPlaneClassMachineInfrastructureTemplate", "sigs.k8s.io/cluster-api/api/core/v1beta2.ControlPlaneClassNamingSpec", "sigs.k8s.io/cluster-api/api/core/v1beta2.MachineReadinessGate", "sigs.k8s.io/cluster-api/api/core/v1beta2.MachineTaint", "sigs.k8s.io/cluster-api/api/core/v1beta2.ObjectMeta"}, } } @@ -1885,6 +1908,29 @@ func schema_cluster_api_api_core_v1beta2_ControlPlaneTopology(ref common.Referen Ref: ref("sigs.k8s.io/cluster-api/api/core/v1beta2.ControlPlaneTopologyMachineDeletionSpec"), }, }, + "taints": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-list-map-keys": []interface{}{ + "key", + "effect", + }, + "x-kubernetes-list-type": "map", + }, + }, + SchemaProps: spec.SchemaProps{ + Description: "taints are the node taints that Cluster API will manage. This list is not necessarily complete: other Kubernetes components may add or remove other taints from nodes, e.g. the node controller might add the node.kubernetes.io/not-ready taint. Only those taints defined in this list will be added or removed by core Cluster API controllers.\n\nThere can be at most 64 taints. A pod would have to tolerate all existing taints to run on the corresponding node.\n\nNOTE: This list is implemented as a \"map\" type, meaning that individual elements can be managed by different owners.", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("sigs.k8s.io/cluster-api/api/core/v1beta2.MachineTaint"), + }, + }, + }, + }, + }, "readinessGates": { VendorExtensible: spec.VendorExtensible{ Extensions: spec.Extensions{ @@ -1918,7 +1964,7 @@ func schema_cluster_api_api_core_v1beta2_ControlPlaneTopology(ref common.Referen }, }, Dependencies: []string{ - "sigs.k8s.io/cluster-api/api/core/v1beta2.ControlPlaneTopologyHealthCheck", "sigs.k8s.io/cluster-api/api/core/v1beta2.ControlPlaneTopologyMachineDeletionSpec", "sigs.k8s.io/cluster-api/api/core/v1beta2.ControlPlaneVariables", "sigs.k8s.io/cluster-api/api/core/v1beta2.MachineReadinessGate", "sigs.k8s.io/cluster-api/api/core/v1beta2.ObjectMeta"}, + "sigs.k8s.io/cluster-api/api/core/v1beta2.ControlPlaneTopologyHealthCheck", "sigs.k8s.io/cluster-api/api/core/v1beta2.ControlPlaneTopologyMachineDeletionSpec", "sigs.k8s.io/cluster-api/api/core/v1beta2.ControlPlaneVariables", "sigs.k8s.io/cluster-api/api/core/v1beta2.MachineReadinessGate", "sigs.k8s.io/cluster-api/api/core/v1beta2.MachineTaint", "sigs.k8s.io/cluster-api/api/core/v1beta2.ObjectMeta"}, } } @@ -2901,6 +2947,29 @@ func schema_cluster_api_api_core_v1beta2_MachineDeploymentClass(ref common.Refer Ref: ref("sigs.k8s.io/cluster-api/api/core/v1beta2.MachineDeploymentClassMachineDeletionSpec"), }, }, + "taints": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-list-map-keys": []interface{}{ + "key", + "effect", + }, + "x-kubernetes-list-type": "map", + }, + }, + SchemaProps: spec.SchemaProps{ + Description: "taints are the node taints that Cluster API will manage. This list is not necessarily complete: other Kubernetes components may add or remove other taints from nodes, e.g. the node controller might add the node.kubernetes.io/not-ready taint. Only those taints defined in this list will be added or removed by core Cluster API controllers.\n\nThere can be at most 64 taints. A pod would have to tolerate all existing taints to run on the corresponding node.\n\nNOTE: This list is implemented as a \"map\" type, meaning that individual elements can be managed by different owners.", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("sigs.k8s.io/cluster-api/api/core/v1beta2.MachineTaint"), + }, + }, + }, + }, + }, "minReadySeconds": { SchemaProps: spec.SchemaProps{ Description: "minReadySeconds is the minimum number of seconds for which a newly created machine should be ready. Defaults to 0 (machine will be considered available as soon as it is ready) NOTE: This value can be overridden while defining a Cluster.Topology using this MachineDeploymentClass.", @@ -2942,7 +3011,7 @@ func schema_cluster_api_api_core_v1beta2_MachineDeploymentClass(ref common.Refer }, }, Dependencies: []string{ - "sigs.k8s.io/cluster-api/api/core/v1beta2.MachineDeploymentClassBootstrapTemplate", "sigs.k8s.io/cluster-api/api/core/v1beta2.MachineDeploymentClassHealthCheck", "sigs.k8s.io/cluster-api/api/core/v1beta2.MachineDeploymentClassInfrastructureTemplate", "sigs.k8s.io/cluster-api/api/core/v1beta2.MachineDeploymentClassMachineDeletionSpec", "sigs.k8s.io/cluster-api/api/core/v1beta2.MachineDeploymentClassNamingSpec", "sigs.k8s.io/cluster-api/api/core/v1beta2.MachineDeploymentClassRolloutSpec", "sigs.k8s.io/cluster-api/api/core/v1beta2.MachineReadinessGate", "sigs.k8s.io/cluster-api/api/core/v1beta2.ObjectMeta"}, + "sigs.k8s.io/cluster-api/api/core/v1beta2.MachineDeploymentClassBootstrapTemplate", "sigs.k8s.io/cluster-api/api/core/v1beta2.MachineDeploymentClassHealthCheck", "sigs.k8s.io/cluster-api/api/core/v1beta2.MachineDeploymentClassInfrastructureTemplate", "sigs.k8s.io/cluster-api/api/core/v1beta2.MachineDeploymentClassMachineDeletionSpec", "sigs.k8s.io/cluster-api/api/core/v1beta2.MachineDeploymentClassNamingSpec", "sigs.k8s.io/cluster-api/api/core/v1beta2.MachineDeploymentClassRolloutSpec", "sigs.k8s.io/cluster-api/api/core/v1beta2.MachineReadinessGate", "sigs.k8s.io/cluster-api/api/core/v1beta2.MachineTaint", "sigs.k8s.io/cluster-api/api/core/v1beta2.ObjectMeta"}, } } @@ -3709,6 +3778,29 @@ func schema_cluster_api_api_core_v1beta2_MachineDeploymentTopology(ref common.Re Ref: ref("sigs.k8s.io/cluster-api/api/core/v1beta2.MachineDeploymentTopologyMachineDeletionSpec"), }, }, + "taints": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-list-map-keys": []interface{}{ + "key", + "effect", + }, + "x-kubernetes-list-type": "map", + }, + }, + SchemaProps: spec.SchemaProps{ + Description: "taints are the node taints that Cluster API will manage. This list is not necessarily complete: other Kubernetes components may add or remove other taints from nodes, e.g. the node controller might add the node.kubernetes.io/not-ready taint. Only those taints defined in this list will be added or removed by core Cluster API controllers.\n\nThere can be at most 64 taints. A pod would have to tolerate all existing taints to run on the corresponding node.\n\nNOTE: This list is implemented as a \"map\" type, meaning that individual elements can be managed by different owners.", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("sigs.k8s.io/cluster-api/api/core/v1beta2.MachineTaint"), + }, + }, + }, + }, + }, "minReadySeconds": { SchemaProps: spec.SchemaProps{ Description: "minReadySeconds is the minimum number of seconds for which a newly created machine should be ready. Defaults to 0 (machine will be considered available as soon as it is ready)", @@ -3757,7 +3849,7 @@ func schema_cluster_api_api_core_v1beta2_MachineDeploymentTopology(ref common.Re }, }, Dependencies: []string{ - "sigs.k8s.io/cluster-api/api/core/v1beta2.MachineDeploymentTopologyHealthCheck", "sigs.k8s.io/cluster-api/api/core/v1beta2.MachineDeploymentTopologyMachineDeletionSpec", "sigs.k8s.io/cluster-api/api/core/v1beta2.MachineDeploymentTopologyRolloutSpec", "sigs.k8s.io/cluster-api/api/core/v1beta2.MachineDeploymentVariables", "sigs.k8s.io/cluster-api/api/core/v1beta2.MachineReadinessGate", "sigs.k8s.io/cluster-api/api/core/v1beta2.ObjectMeta"}, + "sigs.k8s.io/cluster-api/api/core/v1beta2.MachineDeploymentTopologyHealthCheck", "sigs.k8s.io/cluster-api/api/core/v1beta2.MachineDeploymentTopologyMachineDeletionSpec", "sigs.k8s.io/cluster-api/api/core/v1beta2.MachineDeploymentTopologyRolloutSpec", "sigs.k8s.io/cluster-api/api/core/v1beta2.MachineDeploymentVariables", "sigs.k8s.io/cluster-api/api/core/v1beta2.MachineReadinessGate", "sigs.k8s.io/cluster-api/api/core/v1beta2.MachineTaint", "sigs.k8s.io/cluster-api/api/core/v1beta2.ObjectMeta"}, } } @@ -5075,6 +5167,29 @@ func schema_cluster_api_api_core_v1beta2_MachinePoolClass(ref common.ReferenceCa Ref: ref("sigs.k8s.io/cluster-api/api/core/v1beta2.MachinePoolClassMachineDeletionSpec"), }, }, + "taints": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-list-map-keys": []interface{}{ + "key", + "effect", + }, + "x-kubernetes-list-type": "map", + }, + }, + SchemaProps: spec.SchemaProps{ + Description: "taints are the node taints that Cluster API will manage. This list is not necessarily complete: other Kubernetes components may add or remove other taints from nodes, e.g. the node controller might add the node.kubernetes.io/not-ready taint. Only those taints defined in this list will be added or removed by core Cluster API controllers.\n\nThere can be at most 64 taints. A pod would have to tolerate all existing taints to run on the corresponding node.\n\nNOTE: This list is implemented as a \"map\" type, meaning that individual elements can be managed by different owners.", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("sigs.k8s.io/cluster-api/api/core/v1beta2.MachineTaint"), + }, + }, + }, + }, + }, "minReadySeconds": { SchemaProps: spec.SchemaProps{ Description: "minReadySeconds is the minimum number of seconds for which a newly created machine pool should be ready. Defaults to 0 (machine will be considered available as soon as it is ready) NOTE: This value can be overridden while defining a Cluster.Topology using this MachinePoolClass.", @@ -5087,7 +5202,7 @@ func schema_cluster_api_api_core_v1beta2_MachinePoolClass(ref common.ReferenceCa }, }, Dependencies: []string{ - "sigs.k8s.io/cluster-api/api/core/v1beta2.MachinePoolClassBootstrapTemplate", "sigs.k8s.io/cluster-api/api/core/v1beta2.MachinePoolClassInfrastructureTemplate", "sigs.k8s.io/cluster-api/api/core/v1beta2.MachinePoolClassMachineDeletionSpec", "sigs.k8s.io/cluster-api/api/core/v1beta2.MachinePoolClassNamingSpec", "sigs.k8s.io/cluster-api/api/core/v1beta2.ObjectMeta"}, + "sigs.k8s.io/cluster-api/api/core/v1beta2.MachinePoolClassBootstrapTemplate", "sigs.k8s.io/cluster-api/api/core/v1beta2.MachinePoolClassInfrastructureTemplate", "sigs.k8s.io/cluster-api/api/core/v1beta2.MachinePoolClassMachineDeletionSpec", "sigs.k8s.io/cluster-api/api/core/v1beta2.MachinePoolClassNamingSpec", "sigs.k8s.io/cluster-api/api/core/v1beta2.MachineTaint", "sigs.k8s.io/cluster-api/api/core/v1beta2.ObjectMeta"}, } } @@ -5533,6 +5648,29 @@ func schema_cluster_api_api_core_v1beta2_MachinePoolTopology(ref common.Referenc Ref: ref("sigs.k8s.io/cluster-api/api/core/v1beta2.MachinePoolTopologyMachineDeletionSpec"), }, }, + "taints": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-list-map-keys": []interface{}{ + "key", + "effect", + }, + "x-kubernetes-list-type": "map", + }, + }, + SchemaProps: spec.SchemaProps{ + Description: "taints are the node taints that Cluster API will manage. This list is not necessarily complete: other Kubernetes components may add or remove other taints from nodes, e.g. the node controller might add the node.kubernetes.io/not-ready taint. Only those taints defined in this list will be added or removed by core Cluster API controllers.\n\nThere can be at most 64 taints. A pod would have to tolerate all existing taints to run on the corresponding node.\n\nNOTE: This list is implemented as a \"map\" type, meaning that individual elements can be managed by different owners.", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("sigs.k8s.io/cluster-api/api/core/v1beta2.MachineTaint"), + }, + }, + }, + }, + }, "minReadySeconds": { SchemaProps: spec.SchemaProps{ Description: "minReadySeconds is the minimum number of seconds for which a newly created machine pool should be ready. Defaults to 0 (machine will be considered available as soon as it is ready)", @@ -5559,7 +5697,7 @@ func schema_cluster_api_api_core_v1beta2_MachinePoolTopology(ref common.Referenc }, }, Dependencies: []string{ - "sigs.k8s.io/cluster-api/api/core/v1beta2.MachinePoolTopologyMachineDeletionSpec", "sigs.k8s.io/cluster-api/api/core/v1beta2.MachinePoolVariables", "sigs.k8s.io/cluster-api/api/core/v1beta2.ObjectMeta"}, + "sigs.k8s.io/cluster-api/api/core/v1beta2.MachinePoolTopologyMachineDeletionSpec", "sigs.k8s.io/cluster-api/api/core/v1beta2.MachinePoolVariables", "sigs.k8s.io/cluster-api/api/core/v1beta2.MachineTaint", "sigs.k8s.io/cluster-api/api/core/v1beta2.ObjectMeta"}, } } diff --git a/config/crd/bases/cluster.x-k8s.io_clusterclasses.yaml b/config/crd/bases/cluster.x-k8s.io_clusterclasses.yaml index d71055b3c339..4d57069b606a 100644 --- a/config/crd/bases/cluster.x-k8s.io_clusterclasses.yaml +++ b/config/crd/bases/cluster.x-k8s.io_clusterclasses.yaml @@ -2875,6 +2875,76 @@ spec: x-kubernetes-list-map-keys: - conditionType x-kubernetes-list-type: map + taints: + description: |- + taints are the node taints that Cluster API will manage. + This list is not necessarily complete: other Kubernetes components may add or remove other taints from nodes, + e.g. the node controller might add the node.kubernetes.io/not-ready taint. + Only those taints defined in this list will be added or removed by core Cluster API controllers. + + There can be at most 64 taints. + A pod would have to tolerate all existing taints to run on the corresponding node. + + NOTE: This list is implemented as a "map" type, meaning that individual elements can be managed by different owners. + items: + description: MachineTaint defines a taint equivalent to corev1.Taint, + but additionally having a propagation field. + properties: + effect: + description: effect is the effect for the taint. Valid values + are NoSchedule, PreferNoSchedule and NoExecute. + enum: + - NoSchedule + - PreferNoSchedule + - NoExecute + type: string + key: + description: |- + key is the taint key to be applied to a node. + Must be a valid qualified name of maximum size 63 characters + with an optional subdomain prefix of maximum size 253 characters, + separated by a `/`. + maxLength: 317 + 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 + x-kubernetes-validations: + - message: key must be a valid qualified name of max size + 63 characters with an optional subdomain prefix of max + size 253 characters + rule: 'self.contains(''/'') ? ( self.split(''/'') [0].size() + <= 253 && self.split(''/'') [1].size() <= 63 && self.split(''/'').size() + == 2 ) : self.size() <= 63' + propagation: + description: |- + propagation defines how this taint should be propagated to nodes. + Valid values are 'Always' and 'OnInitialization'. + Always: The taint will be continuously reconciled. If it is not set for a node, it will be added during reconciliation. + OnInitialization: The taint will be added during node initialization. If it gets removed from the node later on it will not get added again. + enum: + - Always + - OnInitialization + type: string + value: + description: |- + value is the taint value corresponding to the taint key. + It must be a valid label value of maximum size 63 characters. + maxLength: 63 + minLength: 1 + pattern: ^(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])?$ + type: string + required: + - effect + - key + - propagation + type: object + maxItems: 64 + minItems: 1 + type: array + x-kubernetes-list-map-keys: + - key + - effect + x-kubernetes-list-type: map templateRef: description: templateRef contains the reference to a provider-specific control plane template. @@ -4188,6 +4258,78 @@ spec: - type type: object type: object + taints: + description: |- + taints are the node taints that Cluster API will manage. + This list is not necessarily complete: other Kubernetes components may add or remove other taints from nodes, + e.g. the node controller might add the node.kubernetes.io/not-ready taint. + Only those taints defined in this list will be added or removed by core Cluster API controllers. + + There can be at most 64 taints. + A pod would have to tolerate all existing taints to run on the corresponding node. + + NOTE: This list is implemented as a "map" type, meaning that individual elements can be managed by different owners. + items: + description: MachineTaint defines a taint equivalent to + corev1.Taint, but additionally having a propagation + field. + properties: + effect: + description: effect is the effect for the taint. Valid + values are NoSchedule, PreferNoSchedule and NoExecute. + enum: + - NoSchedule + - PreferNoSchedule + - NoExecute + type: string + key: + description: |- + key is the taint key to be applied to a node. + Must be a valid qualified name of maximum size 63 characters + with an optional subdomain prefix of maximum size 253 characters, + separated by a `/`. + maxLength: 317 + 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 + x-kubernetes-validations: + - message: key must be a valid qualified name of max + size 63 characters with an optional subdomain + prefix of max size 253 characters + rule: 'self.contains(''/'') ? ( self.split(''/'') + [0].size() <= 253 && self.split(''/'') [1].size() + <= 63 && self.split(''/'').size() == 2 ) : self.size() + <= 63' + propagation: + description: |- + propagation defines how this taint should be propagated to nodes. + Valid values are 'Always' and 'OnInitialization'. + Always: The taint will be continuously reconciled. If it is not set for a node, it will be added during reconciliation. + OnInitialization: The taint will be added during node initialization. If it gets removed from the node later on it will not get added again. + enum: + - Always + - OnInitialization + type: string + value: + description: |- + value is the taint value corresponding to the taint key. + It must be a valid label value of maximum size 63 characters. + maxLength: 63 + minLength: 1 + pattern: ^(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])?$ + type: string + required: + - effect + - key + - propagation + type: object + maxItems: 64 + minItems: 1 + type: array + x-kubernetes-list-map-keys: + - key + - effect + x-kubernetes-list-type: map required: - bootstrap - class @@ -4396,6 +4538,78 @@ spec: minLength: 1 type: string type: object + taints: + description: |- + taints are the node taints that Cluster API will manage. + This list is not necessarily complete: other Kubernetes components may add or remove other taints from nodes, + e.g. the node controller might add the node.kubernetes.io/not-ready taint. + Only those taints defined in this list will be added or removed by core Cluster API controllers. + + There can be at most 64 taints. + A pod would have to tolerate all existing taints to run on the corresponding node. + + NOTE: This list is implemented as a "map" type, meaning that individual elements can be managed by different owners. + items: + description: MachineTaint defines a taint equivalent to + corev1.Taint, but additionally having a propagation + field. + properties: + effect: + description: effect is the effect for the taint. Valid + values are NoSchedule, PreferNoSchedule and NoExecute. + enum: + - NoSchedule + - PreferNoSchedule + - NoExecute + type: string + key: + description: |- + key is the taint key to be applied to a node. + Must be a valid qualified name of maximum size 63 characters + with an optional subdomain prefix of maximum size 253 characters, + separated by a `/`. + maxLength: 317 + 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 + x-kubernetes-validations: + - message: key must be a valid qualified name of max + size 63 characters with an optional subdomain + prefix of max size 253 characters + rule: 'self.contains(''/'') ? ( self.split(''/'') + [0].size() <= 253 && self.split(''/'') [1].size() + <= 63 && self.split(''/'').size() == 2 ) : self.size() + <= 63' + propagation: + description: |- + propagation defines how this taint should be propagated to nodes. + Valid values are 'Always' and 'OnInitialization'. + Always: The taint will be continuously reconciled. If it is not set for a node, it will be added during reconciliation. + OnInitialization: The taint will be added during node initialization. If it gets removed from the node later on it will not get added again. + enum: + - Always + - OnInitialization + type: string + value: + description: |- + value is the taint value corresponding to the taint key. + It must be a valid label value of maximum size 63 characters. + maxLength: 63 + minLength: 1 + pattern: ^(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])?$ + type: string + required: + - effect + - key + - propagation + type: object + maxItems: 64 + minItems: 1 + type: array + x-kubernetes-list-map-keys: + - key + - effect + x-kubernetes-list-type: map required: - bootstrap - class diff --git a/config/crd/bases/cluster.x-k8s.io_clusters.yaml b/config/crd/bases/cluster.x-k8s.io_clusters.yaml index 1fb337e3be3d..754c22ab1f52 100644 --- a/config/crd/bases/cluster.x-k8s.io_clusters.yaml +++ b/config/crd/bases/cluster.x-k8s.io_clusters.yaml @@ -2096,6 +2096,77 @@ spec: When specified against a control plane provider that lacks support for this field, this value will be ignored. format: int32 type: integer + taints: + description: |- + taints are the node taints that Cluster API will manage. + This list is not necessarily complete: other Kubernetes components may add or remove other taints from nodes, + e.g. the node controller might add the node.kubernetes.io/not-ready taint. + Only those taints defined in this list will be added or removed by core Cluster API controllers. + + There can be at most 64 taints. + A pod would have to tolerate all existing taints to run on the corresponding node. + + NOTE: This list is implemented as a "map" type, meaning that individual elements can be managed by different owners. + items: + description: MachineTaint defines a taint equivalent to + corev1.Taint, but additionally having a propagation field. + properties: + effect: + description: effect is the effect for the taint. Valid + values are NoSchedule, PreferNoSchedule and NoExecute. + enum: + - NoSchedule + - PreferNoSchedule + - NoExecute + type: string + key: + description: |- + key is the taint key to be applied to a node. + Must be a valid qualified name of maximum size 63 characters + with an optional subdomain prefix of maximum size 253 characters, + separated by a `/`. + maxLength: 317 + 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 + x-kubernetes-validations: + - message: key must be a valid qualified name of max + size 63 characters with an optional subdomain prefix + of max size 253 characters + rule: 'self.contains(''/'') ? ( self.split(''/'') + [0].size() <= 253 && self.split(''/'') [1].size() + <= 63 && self.split(''/'').size() == 2 ) : self.size() + <= 63' + propagation: + description: |- + propagation defines how this taint should be propagated to nodes. + Valid values are 'Always' and 'OnInitialization'. + Always: The taint will be continuously reconciled. If it is not set for a node, it will be added during reconciliation. + OnInitialization: The taint will be added during node initialization. If it gets removed from the node later on it will not get added again. + enum: + - Always + - OnInitialization + type: string + value: + description: |- + value is the taint value corresponding to the taint key. + It must be a valid label value of maximum size 63 characters. + maxLength: 63 + minLength: 1 + pattern: ^(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])?$ + type: string + required: + - effect + - key + - propagation + type: object + maxItems: 64 + minItems: 1 + type: array + x-kubernetes-list-map-keys: + - key + - effect + x-kubernetes-list-type: map variables: description: variables can be used to customize the ControlPlane through patches. @@ -2647,6 +2718,79 @@ spec: - type type: object type: object + taints: + description: |- + taints are the node taints that Cluster API will manage. + This list is not necessarily complete: other Kubernetes components may add or remove other taints from nodes, + e.g. the node controller might add the node.kubernetes.io/not-ready taint. + Only those taints defined in this list will be added or removed by core Cluster API controllers. + + There can be at most 64 taints. + A pod would have to tolerate all existing taints to run on the corresponding node. + + NOTE: This list is implemented as a "map" type, meaning that individual elements can be managed by different owners. + items: + description: MachineTaint defines a taint equivalent + to corev1.Taint, but additionally having a propagation + field. + properties: + effect: + description: effect is the effect for the taint. + Valid values are NoSchedule, PreferNoSchedule + and NoExecute. + enum: + - NoSchedule + - PreferNoSchedule + - NoExecute + type: string + key: + description: |- + key is the taint key to be applied to a node. + Must be a valid qualified name of maximum size 63 characters + with an optional subdomain prefix of maximum size 253 characters, + separated by a `/`. + maxLength: 317 + 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 + x-kubernetes-validations: + - message: key must be a valid qualified name + of max size 63 characters with an optional + subdomain prefix of max size 253 characters + rule: 'self.contains(''/'') ? ( self.split(''/'') + [0].size() <= 253 && self.split(''/'') [1].size() + <= 63 && self.split(''/'').size() == 2 ) : + self.size() <= 63' + propagation: + description: |- + propagation defines how this taint should be propagated to nodes. + Valid values are 'Always' and 'OnInitialization'. + Always: The taint will be continuously reconciled. If it is not set for a node, it will be added during reconciliation. + OnInitialization: The taint will be added during node initialization. If it gets removed from the node later on it will not get added again. + enum: + - Always + - OnInitialization + type: string + value: + description: |- + value is the taint value corresponding to the taint key. + It must be a valid label value of maximum size 63 characters. + maxLength: 63 + minLength: 1 + pattern: ^(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])?$ + type: string + required: + - effect + - key + - propagation + type: object + maxItems: 64 + minItems: 1 + type: array + x-kubernetes-list-map-keys: + - key + - effect + x-kubernetes-list-type: map variables: description: variables can be used to customize the MachineDeployment through patches. @@ -2805,6 +2949,79 @@ spec: of this value. format: int32 type: integer + taints: + description: |- + taints are the node taints that Cluster API will manage. + This list is not necessarily complete: other Kubernetes components may add or remove other taints from nodes, + e.g. the node controller might add the node.kubernetes.io/not-ready taint. + Only those taints defined in this list will be added or removed by core Cluster API controllers. + + There can be at most 64 taints. + A pod would have to tolerate all existing taints to run on the corresponding node. + + NOTE: This list is implemented as a "map" type, meaning that individual elements can be managed by different owners. + items: + description: MachineTaint defines a taint equivalent + to corev1.Taint, but additionally having a propagation + field. + properties: + effect: + description: effect is the effect for the taint. + Valid values are NoSchedule, PreferNoSchedule + and NoExecute. + enum: + - NoSchedule + - PreferNoSchedule + - NoExecute + type: string + key: + description: |- + key is the taint key to be applied to a node. + Must be a valid qualified name of maximum size 63 characters + with an optional subdomain prefix of maximum size 253 characters, + separated by a `/`. + maxLength: 317 + 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 + x-kubernetes-validations: + - message: key must be a valid qualified name + of max size 63 characters with an optional + subdomain prefix of max size 253 characters + rule: 'self.contains(''/'') ? ( self.split(''/'') + [0].size() <= 253 && self.split(''/'') [1].size() + <= 63 && self.split(''/'').size() == 2 ) : + self.size() <= 63' + propagation: + description: |- + propagation defines how this taint should be propagated to nodes. + Valid values are 'Always' and 'OnInitialization'. + Always: The taint will be continuously reconciled. If it is not set for a node, it will be added during reconciliation. + OnInitialization: The taint will be added during node initialization. If it gets removed from the node later on it will not get added again. + enum: + - Always + - OnInitialization + type: string + value: + description: |- + value is the taint value corresponding to the taint key. + It must be a valid label value of maximum size 63 characters. + maxLength: 63 + minLength: 1 + pattern: ^(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])?$ + type: string + required: + - effect + - key + - propagation + type: object + maxItems: 64 + minItems: 1 + type: array + x-kubernetes-list-map-keys: + - key + - effect + x-kubernetes-list-type: map variables: description: variables can be used to customize the MachinePool through patches. From 47874b478f29fae7365cfc7d674c81636aa38e2c Mon Sep 17 00:00:00 2001 From: Christian Schlotter Date: Tue, 23 Dec 2025 12:53:40 +0100 Subject: [PATCH 3/8] taints: change validation to not use cel because rule would exceed budget factor for cluster and clusterclass --- api/core/v1beta2/common_types.go | 3 ++- .../cluster.x-k8s.io_clusterclasses.yaml | 23 ------------------ .../crd/bases/cluster.x-k8s.io_clusters.yaml | 24 ------------------- .../cluster.x-k8s.io_machinedeployments.yaml | 8 ------- .../bases/cluster.x-k8s.io_machinepools.yaml | 8 ------- .../crd/bases/cluster.x-k8s.io_machines.yaml | 7 ------ .../bases/cluster.x-k8s.io_machinesets.yaml | 8 ------- ...cluster.x-k8s.io_kubeadmcontrolplanes.yaml | 8 ------- ...x-k8s.io_kubeadmcontrolplanetemplates.yaml | 8 ------- internal/util/taints/validation.go | 9 +++++++ 10 files changed, 11 insertions(+), 95 deletions(-) diff --git a/api/core/v1beta2/common_types.go b/api/core/v1beta2/common_types.go index 78b064afb12c..9e2af760a524 100644 --- a/api/core/v1beta2/common_types.go +++ b/api/core/v1beta2/common_types.go @@ -411,6 +411,8 @@ func (r *ContractVersionedObjectReference) GroupKind() schema.GroupKind { // MachineTaint defines a taint equivalent to corev1.Taint, but additionally having a propagation field. type MachineTaint struct { + // Note: we do not use CEL for validating the key as qualified name, because it would be too expensive for Cluster and ClusterClass objects. + // key is the taint key to be applied to a node. // Must be a valid qualified name of maximum size 63 characters // with an optional subdomain prefix of maximum size 253 characters, @@ -419,7 +421,6 @@ type MachineTaint struct { // +kubebuilder:validation:MinLength=1 // +kubebuilder:validation:MaxLength=317 // +kubebuilder:validation: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]$ - // +kubebuilder:validation:XValidation:rule="self.contains('/') ? ( self.split('/') [0].size() <= 253 && self.split('/') [1].size() <= 63 && self.split('/').size() == 2 ) : self.size() <= 63",message="key must be a valid qualified name of max size 63 characters with an optional subdomain prefix of max size 253 characters" Key string `json:"key,omitempty"` // value is the taint value corresponding to the taint key. diff --git a/config/crd/bases/cluster.x-k8s.io_clusterclasses.yaml b/config/crd/bases/cluster.x-k8s.io_clusterclasses.yaml index 4d57069b606a..e42c61e33e3e 100644 --- a/config/crd/bases/cluster.x-k8s.io_clusterclasses.yaml +++ b/config/crd/bases/cluster.x-k8s.io_clusterclasses.yaml @@ -2908,13 +2908,6 @@ spec: 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 - x-kubernetes-validations: - - message: key must be a valid qualified name of max size - 63 characters with an optional subdomain prefix of max - size 253 characters - rule: 'self.contains(''/'') ? ( self.split(''/'') [0].size() - <= 253 && self.split(''/'') [1].size() <= 63 && self.split(''/'').size() - == 2 ) : self.size() <= 63' propagation: description: |- propagation defines how this taint should be propagated to nodes. @@ -4292,14 +4285,6 @@ spec: 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 - x-kubernetes-validations: - - message: key must be a valid qualified name of max - size 63 characters with an optional subdomain - prefix of max size 253 characters - rule: 'self.contains(''/'') ? ( self.split(''/'') - [0].size() <= 253 && self.split(''/'') [1].size() - <= 63 && self.split(''/'').size() == 2 ) : self.size() - <= 63' propagation: description: |- propagation defines how this taint should be propagated to nodes. @@ -4572,14 +4557,6 @@ spec: 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 - x-kubernetes-validations: - - message: key must be a valid qualified name of max - size 63 characters with an optional subdomain - prefix of max size 253 characters - rule: 'self.contains(''/'') ? ( self.split(''/'') - [0].size() <= 253 && self.split(''/'') [1].size() - <= 63 && self.split(''/'').size() == 2 ) : self.size() - <= 63' propagation: description: |- propagation defines how this taint should be propagated to nodes. diff --git a/config/crd/bases/cluster.x-k8s.io_clusters.yaml b/config/crd/bases/cluster.x-k8s.io_clusters.yaml index 754c22ab1f52..fccc616e0e59 100644 --- a/config/crd/bases/cluster.x-k8s.io_clusters.yaml +++ b/config/crd/bases/cluster.x-k8s.io_clusters.yaml @@ -2129,14 +2129,6 @@ spec: 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 - x-kubernetes-validations: - - message: key must be a valid qualified name of max - size 63 characters with an optional subdomain prefix - of max size 253 characters - rule: 'self.contains(''/'') ? ( self.split(''/'') - [0].size() <= 253 && self.split(''/'') [1].size() - <= 63 && self.split(''/'').size() == 2 ) : self.size() - <= 63' propagation: description: |- propagation defines how this taint should be propagated to nodes. @@ -2753,14 +2745,6 @@ spec: 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 - x-kubernetes-validations: - - message: key must be a valid qualified name - of max size 63 characters with an optional - subdomain prefix of max size 253 characters - rule: 'self.contains(''/'') ? ( self.split(''/'') - [0].size() <= 253 && self.split(''/'') [1].size() - <= 63 && self.split(''/'').size() == 2 ) : - self.size() <= 63' propagation: description: |- propagation defines how this taint should be propagated to nodes. @@ -2984,14 +2968,6 @@ spec: 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 - x-kubernetes-validations: - - message: key must be a valid qualified name - of max size 63 characters with an optional - subdomain prefix of max size 253 characters - rule: 'self.contains(''/'') ? ( self.split(''/'') - [0].size() <= 253 && self.split(''/'') [1].size() - <= 63 && self.split(''/'').size() == 2 ) : - self.size() <= 63' propagation: description: |- propagation defines how this taint should be propagated to nodes. diff --git a/config/crd/bases/cluster.x-k8s.io_machinedeployments.yaml b/config/crd/bases/cluster.x-k8s.io_machinedeployments.yaml index b40849d87050..2d43c323f321 100644 --- a/config/crd/bases/cluster.x-k8s.io_machinedeployments.yaml +++ b/config/crd/bases/cluster.x-k8s.io_machinedeployments.yaml @@ -1336,14 +1336,6 @@ spec: 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 - x-kubernetes-validations: - - message: key must be a valid qualified name of max - size 63 characters with an optional subdomain prefix - of max size 253 characters - rule: 'self.contains(''/'') ? ( self.split(''/'') - [0].size() <= 253 && self.split(''/'') [1].size() - <= 63 && self.split(''/'').size() == 2 ) : self.size() - <= 63' propagation: description: |- propagation defines how this taint should be propagated to nodes. diff --git a/config/crd/bases/cluster.x-k8s.io_machinepools.yaml b/config/crd/bases/cluster.x-k8s.io_machinepools.yaml index bdbe6cf4c884..6f1740195f49 100644 --- a/config/crd/bases/cluster.x-k8s.io_machinepools.yaml +++ b/config/crd/bases/cluster.x-k8s.io_machinepools.yaml @@ -994,14 +994,6 @@ spec: 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 - x-kubernetes-validations: - - message: key must be a valid qualified name of max - size 63 characters with an optional subdomain prefix - of max size 253 characters - rule: 'self.contains(''/'') ? ( self.split(''/'') - [0].size() <= 253 && self.split(''/'') [1].size() - <= 63 && self.split(''/'').size() == 2 ) : self.size() - <= 63' propagation: description: |- propagation defines how this taint should be propagated to nodes. diff --git a/config/crd/bases/cluster.x-k8s.io_machines.yaml b/config/crd/bases/cluster.x-k8s.io_machines.yaml index 5e0ea3fd07b1..2efd496a529d 100644 --- a/config/crd/bases/cluster.x-k8s.io_machines.yaml +++ b/config/crd/bases/cluster.x-k8s.io_machines.yaml @@ -968,13 +968,6 @@ spec: 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 - x-kubernetes-validations: - - message: key must be a valid qualified name of max size 63 - characters with an optional subdomain prefix of max size - 253 characters - rule: 'self.contains(''/'') ? ( self.split(''/'') [0].size() - <= 253 && self.split(''/'') [1].size() <= 63 && self.split(''/'').size() - == 2 ) : self.size() <= 63' propagation: description: |- propagation defines how this taint should be propagated to nodes. diff --git a/config/crd/bases/cluster.x-k8s.io_machinesets.yaml b/config/crd/bases/cluster.x-k8s.io_machinesets.yaml index a35333eb48ff..e28b4d329d02 100644 --- a/config/crd/bases/cluster.x-k8s.io_machinesets.yaml +++ b/config/crd/bases/cluster.x-k8s.io_machinesets.yaml @@ -1118,14 +1118,6 @@ spec: 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 - x-kubernetes-validations: - - message: key must be a valid qualified name of max - size 63 characters with an optional subdomain prefix - of max size 253 characters - rule: 'self.contains(''/'') ? ( self.split(''/'') - [0].size() <= 253 && self.split(''/'') [1].size() - <= 63 && self.split(''/'').size() == 2 ) : self.size() - <= 63' propagation: description: |- propagation defines how this taint should be propagated to nodes. diff --git a/controlplane/kubeadm/config/crd/bases/controlplane.cluster.x-k8s.io_kubeadmcontrolplanes.yaml b/controlplane/kubeadm/config/crd/bases/controlplane.cluster.x-k8s.io_kubeadmcontrolplanes.yaml index 8e8f351a34fa..b97e5fd94145 100644 --- a/controlplane/kubeadm/config/crd/bases/controlplane.cluster.x-k8s.io_kubeadmcontrolplanes.yaml +++ b/controlplane/kubeadm/config/crd/bases/controlplane.cluster.x-k8s.io_kubeadmcontrolplanes.yaml @@ -5301,14 +5301,6 @@ spec: 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 - x-kubernetes-validations: - - message: key must be a valid qualified name of max - size 63 characters with an optional subdomain prefix - of max size 253 characters - rule: 'self.contains(''/'') ? ( self.split(''/'') - [0].size() <= 253 && self.split(''/'') [1].size() - <= 63 && self.split(''/'').size() == 2 ) : self.size() - <= 63' propagation: description: |- propagation defines how this taint should be propagated to nodes. diff --git a/controlplane/kubeadm/config/crd/bases/controlplane.cluster.x-k8s.io_kubeadmcontrolplanetemplates.yaml b/controlplane/kubeadm/config/crd/bases/controlplane.cluster.x-k8s.io_kubeadmcontrolplanetemplates.yaml index a639d5dedf60..1be0566951ab 100644 --- a/controlplane/kubeadm/config/crd/bases/controlplane.cluster.x-k8s.io_kubeadmcontrolplanetemplates.yaml +++ b/controlplane/kubeadm/config/crd/bases/controlplane.cluster.x-k8s.io_kubeadmcontrolplanetemplates.yaml @@ -4952,14 +4952,6 @@ spec: 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 - x-kubernetes-validations: - - message: key must be a valid qualified name - of max size 63 characters with an optional - subdomain prefix of max size 253 characters - rule: 'self.contains(''/'') ? ( self.split(''/'') - [0].size() <= 253 && self.split(''/'') [1].size() - <= 63 && self.split(''/'').size() == 2 ) - : self.size() <= 63' propagation: description: |- propagation defines how this taint should be propagated to nodes. diff --git a/internal/util/taints/validation.go b/internal/util/taints/validation.go index e82a2732526f..63a19595be1e 100644 --- a/internal/util/taints/validation.go +++ b/internal/util/taints/validation.go @@ -19,6 +19,7 @@ package taints import ( "strings" + "k8s.io/apimachinery/pkg/util/validation" "k8s.io/apimachinery/pkg/util/validation/field" clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta2" @@ -56,6 +57,14 @@ func ValidateMachineTaints(taints []clusterv1.MachineTaint, taintsPath *field.Pa case taint.Key == "node-role.kubernetes.io/master": allErrs = append(allErrs, field.Invalid(idxPath.Child("key"), taint.Key, "taint is deprecated since 1.24 and should not be used anymore")) } + + if validation.IsQualifiedName(taint.Key) != nil { + allErrs = append(allErrs, field.Invalid( + idxPath.Child("key"), + taint.Key, + "key must be a valid qualified name of max size 63 characters with an optional subdomain prefix of max size 253 characters", + )) + } } return allErrs From b8f99162ea330c72a30215f06bde4f2ae338348a Mon Sep 17 00:00:00 2001 From: Christian Schlotter Date: Tue, 23 Dec 2025 13:13:01 +0100 Subject: [PATCH 4/8] webhooks: validate taints for cluster and clusterclass --- internal/webhooks/cluster.go | 21 +++++++++++++++++++++ internal/webhooks/clusterclass.go | 22 ++++++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/internal/webhooks/cluster.go b/internal/webhooks/cluster.go index 1e9ecd185eb1..a822ff0e809e 100644 --- a/internal/webhooks/cluster.go +++ b/internal/webhooks/cluster.go @@ -44,6 +44,7 @@ import ( "sigs.k8s.io/cluster-api/internal/hooks" "sigs.k8s.io/cluster-api/internal/topology/check" "sigs.k8s.io/cluster-api/internal/topology/variables" + "sigs.k8s.io/cluster-api/internal/util/taints" "sigs.k8s.io/cluster-api/util/conditions" "sigs.k8s.io/cluster-api/util/version" ) @@ -276,6 +277,8 @@ func (webhook *Cluster) validateTopology(ctx context.Context, oldCluster, newClu allErrs = append(allErrs, validateTopologyRollout(newCluster.Spec.Topology, fldPath)...) + allErrs = append(allErrs, validateTopologyTaints(newCluster.Spec.Topology, fldPath)...) + // upgrade concurrency should be a numeric value. if concurrency, ok := newCluster.Annotations[clusterv1.ClusterTopologyUpgradeConcurrencyAnnotation]; ok { concurrencyAnnotationField := field.NewPath("metadata", "annotations", clusterv1.ClusterTopologyUpgradeConcurrencyAnnotation) @@ -639,6 +642,24 @@ func validateTopologyRollout(topology clusterv1.Topology, fldPath *field.Path) f return allErrs } +func validateTopologyTaints(topology clusterv1.Topology, fldPath *field.Path) field.ErrorList { + var allErrs field.ErrorList + + allErrs = append(allErrs, taints.ValidateMachineTaints(topology.ControlPlane.Taints, fldPath.Child("controlPlane", "taints"))...) + + for _, md := range topology.Workers.MachineDeployments { + fldPath := fldPath.Child("workers", "machineDeployments").Key(md.Name).Child("taints") + allErrs = append(allErrs, taints.ValidateMachineTaints(md.Taints, fldPath)...) + } + + for _, mp := range topology.Workers.MachinePools { + fldPath := fldPath.Child("workers", "machinePools").Key(mp.Name).Child("taints") + allErrs = append(allErrs, taints.ValidateMachineTaints(mp.Taints, fldPath)...) + } + + return allErrs +} + func validateMachineHealthChecks(cluster *clusterv1.Cluster, clusterClass *clusterv1.ClusterClass) field.ErrorList { var allErrs field.ErrorList diff --git a/internal/webhooks/clusterclass.go b/internal/webhooks/clusterclass.go index 00e16201aa5f..197085ccafd8 100644 --- a/internal/webhooks/clusterclass.go +++ b/internal/webhooks/clusterclass.go @@ -38,6 +38,7 @@ import ( "sigs.k8s.io/cluster-api/internal/topology/check" topologynames "sigs.k8s.io/cluster-api/internal/topology/names" "sigs.k8s.io/cluster-api/internal/topology/variables" + "sigs.k8s.io/cluster-api/internal/util/taints" clog "sigs.k8s.io/cluster-api/util/log" "sigs.k8s.io/cluster-api/util/version" ) @@ -112,6 +113,9 @@ func (webhook *ClusterClass) validate(ctx context.Context, oldClusterClass, newC // Ensure NamingStrategies are valid. allErrs = append(allErrs, validateNamingStrategies(newClusterClass)...) + // Ensure Taints are valid. + allErrs = append(allErrs, validateTaints(newClusterClass)...) + // Validate variables. var oldClusterClassVariables []clusterv1.ClusterClassVariable if oldClusterClass != nil { @@ -507,6 +511,24 @@ func validateClusterClassMetadata(clusterClass *clusterv1.ClusterClass) field.Er return allErrs } +func validateTaints(clusterClass *clusterv1.ClusterClass) field.ErrorList { + var allErrs field.ErrorList + + allErrs = append(allErrs, taints.ValidateMachineTaints(clusterClass.Spec.ControlPlane.Taints, field.NewPath("spec", "controlPlane", "taints"))...) + + for _, md := range clusterClass.Spec.Workers.MachineDeployments { + fldPath := field.NewPath("spec", "workers", "machineDeployments").Key(md.Class).Child("taints") + allErrs = append(allErrs, taints.ValidateMachineTaints(md.Taints, fldPath)...) + } + + for _, mp := range clusterClass.Spec.Workers.MachinePools { + fldPath := field.NewPath("spec", "workers", "machinePools").Key(mp.Class).Child("taints") + allErrs = append(allErrs, taints.ValidateMachineTaints(mp.Taints, fldPath)...) + } + + return allErrs +} + // validateAutoscalerAnnotationsForClusterClass iterates over a list of Clusters that use a ClusterClass and returns // errors if the ClusterClass contains autoscaler annotations while a Cluster has worker replicas. func validateAutoscalerAnnotationsForClusterClass(clusters []clusterv1.Cluster, newClusterClass *clusterv1.ClusterClass) field.ErrorList { From d88e698544988b7560217c6b0793a470cb303ee4 Mon Sep 17 00:00:00 2001 From: Christian Schlotter Date: Tue, 23 Dec 2025 12:55:21 +0100 Subject: [PATCH 5/8] topology: propagate taints --- exp/topology/desiredstate/desired_state.go | 25 +++++++++++++++++++ .../topology/cluster/patches/engine.go | 1 + 2 files changed, 26 insertions(+) diff --git a/exp/topology/desiredstate/desired_state.go b/exp/topology/desiredstate/desired_state.go index 0879bbb10a57..947f090ade66 100644 --- a/exp/topology/desiredstate/desired_state.go +++ b/exp/topology/desiredstate/desired_state.go @@ -447,6 +447,19 @@ func (g *generator) computeControlPlane(ctx context.Context, s *scope.Scope, inf } } + // If it is required to manage the taints for the control plane, set the corresponding field. + // NOTE: If taints value from both Cluster and ClusterClass is nil, it is assumed that the control plane controller + // does not implement support for this field and the ControlPlane object is generated without taints. + if s.Blueprint.Topology.ControlPlane.Taints != nil { + if err := contract.ControlPlane().MachineTemplate().Taints().Set(controlPlane, s.Blueprint.Topology.ControlPlane.Taints); err != nil { + return nil, errors.Wrapf(err, "failed to set %s in the ControlPlane object", contract.ControlPlane().MachineTemplate().Taints().Path()) + } + } else if s.Blueprint.ClusterClass.Spec.ControlPlane.Taints != nil { + if err := contract.ControlPlane().MachineTemplate().Taints().Set(controlPlane, s.Blueprint.ClusterClass.Spec.ControlPlane.Taints); err != nil { + return nil, errors.Wrapf(err, "failed to set %s in the ControlPlane object", contract.ControlPlane().MachineTemplate().Taints().Path()) + } + } + // If it is required to manage the NodeDrainTimeoutSeconds for the control plane, set the corresponding field. nodeDrainTimeout := s.Blueprint.ClusterClass.Spec.ControlPlane.Deletion.NodeDrainTimeoutSeconds if s.Blueprint.Topology.ControlPlane.Deletion.NodeDrainTimeoutSeconds != nil { @@ -940,6 +953,11 @@ func (g *generator) computeMachineDeployment(ctx context.Context, s *scope.Scope readinessGates = machineDeploymentTopology.ReadinessGates } + taints := machineDeploymentClass.Taints + if machineDeploymentTopology.Taints != nil { + taints = machineDeploymentTopology.Taints + } + // Compute the MachineDeployment object. desiredBootstrapTemplateRef := contract.ObjToContractVersionedObjectReference(desiredMachineDeployment.BootstrapTemplate) desiredInfraMachineTemplateRef := contract.ObjToContractVersionedObjectReference(desiredMachineDeployment.InfrastructureMachineTemplate) @@ -985,6 +1003,7 @@ func (g *generator) computeMachineDeployment(ctx context.Context, s *scope.Scope NodeDeletionTimeoutSeconds: nodeDeletionTimeout, }, ReadinessGates: readinessGates, + Taints: taints, MinReadySeconds: minReadySeconds, }, }, @@ -1295,6 +1314,11 @@ func (g *generator) computeMachinePool(ctx context.Context, s *scope.Scope, mach nodeDeletionTimeout = machinePoolTopology.Deletion.NodeDeletionTimeoutSeconds } + taints := machinePoolClass.Taints + if machinePoolTopology.Taints != nil { + taints = machinePoolTopology.Taints + } + // Compute the MachinePool object. desiredBootstrapConfigRef := contract.ObjToContractVersionedObjectReference(desiredMachinePool.BootstrapObject) desiredInfraMachinePoolRef := contract.ObjToContractVersionedObjectReference(desiredMachinePool.InfrastructureMachinePoolObject) @@ -1333,6 +1357,7 @@ func (g *generator) computeMachinePool(ctx context.Context, s *scope.Scope, mach NodeDeletionTimeoutSeconds: nodeDeletionTimeout, }, MinReadySeconds: minReadySeconds, + Taints: taints, }, }, }, diff --git a/internal/controllers/topology/cluster/patches/engine.go b/internal/controllers/topology/cluster/patches/engine.go index 428102b02b80..aec6cdfebe3c 100644 --- a/internal/controllers/topology/cluster/patches/engine.go +++ b/internal/controllers/topology/cluster/patches/engine.go @@ -556,6 +556,7 @@ func updateDesiredState(ctx context.Context, req *runtimehooksv1.GeneratePatches // requiring a client here to retrieve the contract version of the ControlPlane object. contract.ControlPlane().MachineTemplate().ReadinessGates("v1beta1").Path(), contract.ControlPlane().MachineTemplate().ReadinessGates("v1beta2").Path(), + contract.ControlPlane().MachineTemplate().Taints().Path(), contract.ControlPlane().MachineTemplate().InfrastructureV1Beta1Ref().Path(), contract.ControlPlane().MachineTemplate().InfrastructureRef().Path(), contract.ControlPlane().MachineTemplate().NodeDrainTimeout().Path(), From 7668c1b26b006750778f732e43d779330ce1d4d8 Mon Sep 17 00:00:00 2001 From: Christian Schlotter Date: Tue, 23 Dec 2025 14:54:06 +0100 Subject: [PATCH 6/8] unit test coverage --- .../desiredstate/desired_state_test.go | 112 +++++++++++++++++- util/test/builder/builders.go | 18 +++ 2 files changed, 129 insertions(+), 1 deletion(-) diff --git a/exp/topology/desiredstate/desired_state_test.go b/exp/topology/desiredstate/desired_state_test.go index d46ef40f320e..f65fcea6e59c 100644 --- a/exp/topology/desiredstate/desired_state_test.go +++ b/exp/topology/desiredstate/desired_state_test.go @@ -380,9 +380,13 @@ func TestComputeControlPlane(t *testing.T) { clusterClassReadinessGates := []clusterv1.MachineReadinessGate{ {ConditionType: "foo"}, } + clusterClassTaints := []clusterv1.MachineTaint{ + {Key: "foo", Effect: corev1.TaintEffectPreferNoSchedule, Propagation: clusterv1.MachineTaintPropagationAlways}, + } clusterClass := builder.ClusterClass(metav1.NamespaceDefault, "class1"). WithControlPlaneMetadata(labels, annotations). WithControlPlaneReadinessGates(clusterClassReadinessGates). + WithControlPlaneTaints(clusterClassTaints). WithControlPlaneTemplate(controlPlaneTemplate). WithControlPlaneNodeDrainTimeout(&clusterClassDuration). WithControlPlaneNodeVolumeDetachTimeout(&clusterClassDuration). @@ -400,6 +404,10 @@ func TestComputeControlPlane(t *testing.T) { {ConditionType: "foo"}, {ConditionType: "bar"}, } + taints := []clusterv1.MachineTaint{ + {Key: "foo", Effect: corev1.TaintEffectPreferNoSchedule, Propagation: clusterv1.MachineTaintPropagationAlways}, + {Key: "bar", Effect: corev1.TaintEffectPreferNoSchedule, Propagation: clusterv1.MachineTaintPropagationAlways}, + } cluster := &clusterv1.Cluster{ ObjectMeta: metav1.ObjectMeta{ Name: "cluster1", @@ -414,6 +422,7 @@ func TestComputeControlPlane(t *testing.T) { Annotations: map[string]string{"a2": ""}, }, ReadinessGates: readinessGates, + Taints: taints, Replicas: &replicas, Deletion: clusterv1.ControlPlaneTopologyMachineDeletionSpec{ NodeDrainTimeoutSeconds: &nodeDrainTimeout, @@ -434,6 +443,15 @@ func TestComputeControlPlane(t *testing.T) { var expectedReadinessGates []interface{} g.Expect(json.Unmarshal(jsonValue, &expectedReadinessGates)).ToNot(HaveOccurred()) + jsonValue, err = json.Marshal(&clusterClassTaints) + g.Expect(err).ToNot(HaveOccurred()) + var expectedClusterClassTaints []interface{} + g.Expect(json.Unmarshal(jsonValue, &expectedClusterClassTaints)).ToNot(HaveOccurred()) + jsonValue, err = json.Marshal(&taints) + g.Expect(err).ToNot(HaveOccurred()) + var expectedTaints []interface{} + g.Expect(json.Unmarshal(jsonValue, &expectedTaints)).ToNot(HaveOccurred()) + scheme := runtime.NewScheme() _ = clusterv1.AddToScheme(scheme) _ = apiextensionsv1.AddToScheme(scheme) @@ -522,6 +540,7 @@ func TestComputeControlPlane(t *testing.T) { assertNestedField(g, obj, version, contract.ControlPlane().Version().Path()...) assertNestedField(g, obj, int64(replicas), contract.ControlPlane().Replicas().Path()...) assertNestedField(g, obj, expectedReadinessGates, contract.ControlPlane().MachineTemplate().ReadinessGates("v1beta2").Path()...) + assertNestedField(g, obj, expectedTaints, contract.ControlPlane().MachineTemplate().Taints().Path()...) assertNestedField(g, obj, int64(topologyDuration), contract.ControlPlane().MachineTemplate().NodeDrainTimeoutSeconds().Path()...) assertNestedField(g, obj, int64(topologyDuration), contract.ControlPlane().MachineTemplate().NodeVolumeDetachTimeoutSeconds().Path()...) assertNestedField(g, obj, int64(topologyDuration), contract.ControlPlane().MachineTemplate().NodeDeletionTimeoutSeconds().Path()...) @@ -616,6 +635,7 @@ func TestComputeControlPlane(t *testing.T) { // checking only values from CC defaults assertNestedField(g, obj, expectedClusterClassReadinessGates, contract.ControlPlane().MachineTemplate().ReadinessGates("v1beta2").Path()...) + assertNestedField(g, obj, expectedClusterClassTaints, contract.ControlPlane().MachineTemplate().Taints().Path()...) assertNestedField(g, obj, int64(clusterClassDuration), contract.ControlPlane().MachineTemplate().NodeDrainTimeoutSeconds().Path()...) assertNestedField(g, obj, int64(clusterClassDuration), contract.ControlPlane().MachineTemplate().NodeVolumeDetachTimeoutSeconds().Path()...) assertNestedField(g, obj, int64(clusterClassDuration), contract.ControlPlane().MachineTemplate().NodeDeletionTimeoutSeconds().Path()...) @@ -685,6 +705,33 @@ func TestComputeControlPlane(t *testing.T) { assertNestedFieldUnset(g, obj, contract.ControlPlane().MachineTemplate().ReadinessGates("v1beta1").Path()...) assertNestedFieldUnset(g, obj, contract.ControlPlane().MachineTemplate().ReadinessGates("v1beta2").Path()...) }) + t.Run("Skips setting taints if not set in Cluster and ClusterClass", func(t *testing.T) { + g := NewWithT(t) + + clusterClassWithoutTaints := clusterClass.DeepCopy() + clusterClassWithoutTaints.Spec.ControlPlane.Taints = nil + + clusterWithoutTaints := cluster.DeepCopy() + clusterWithoutTaints.Spec.Topology.ControlPlane.Taints = nil + + blueprint := &scope.ClusterBlueprint{ + Topology: clusterWithoutTaints.Spec.Topology, + ClusterClass: clusterClassWithoutTaints, + ControlPlane: &scope.ControlPlaneBlueprint{ + Template: controlPlaneTemplate, + }, + } + + // aggregating current cluster objects into ClusterState (simulating getCurrentState) + scope := scope.New(clusterWithoutTaints) + scope.Blueprint = blueprint + + obj, err := (&generator{Client: clientWithV1Beta2ContractCRD}).computeControlPlane(ctx, scope, nil) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(obj).ToNot(BeNil()) + + assertNestedFieldUnset(g, obj, contract.ControlPlane().MachineTemplate().Taints().Path()...) + }) t.Run("Generates the ControlPlane from the template and adds the infrastructure machine template if required (v1beta1 contract)", func(t *testing.T) { g := NewWithT(t) @@ -1799,6 +1846,9 @@ func TestComputeMachineDeployment(t *testing.T) { clusterClassReadinessGates := []clusterv1.MachineReadinessGate{ {ConditionType: "foo"}, } + clusterClassTaints := []clusterv1.MachineTaint{ + {Key: "clusterClassTaintFoo", Effect: corev1.TaintEffectPreferNoSchedule, Propagation: clusterv1.MachineTaintPropagationAlways}, + } md1 := builder.MachineDeploymentClass("linux-worker"). WithLabels(labels). WithAnnotations(annotations). @@ -1812,6 +1862,7 @@ func TestComputeMachineDeployment(t *testing.T) { }, }). WithReadinessGates(clusterClassReadinessGates). + WithTaints(clusterClassTaints). WithFailureDomain(clusterClassFailureDomain). WithNodeDrainTimeout(&clusterClassDuration). WithNodeVolumeDetachTimeout(&clusterClassDuration). @@ -1881,6 +1932,10 @@ func TestComputeMachineDeployment(t *testing.T) { {ConditionType: "foo"}, {ConditionType: "bar"}, } + taints := []clusterv1.MachineTaint{ + {Key: "clusterTaintFoo", Effect: corev1.TaintEffectPreferNoSchedule, Propagation: clusterv1.MachineTaintPropagationAlways}, + {Key: "clusterTaintBar", Effect: corev1.TaintEffectPreferNoSchedule, Propagation: clusterv1.MachineTaintPropagationAlways}, + } mdTopology := clusterv1.MachineDeploymentTopology{ Metadata: clusterv1.ObjectMeta{ Labels: map[string]string{ @@ -1911,6 +1966,7 @@ func TestComputeMachineDeployment(t *testing.T) { Rollout: clusterv1.MachineDeploymentTopologyRolloutSpec{ Strategy: topologyStrategy, }, + Taints: taints, } t.Run("Generates the machine deployment and the referenced templates", func(t *testing.T) { @@ -1948,6 +2004,7 @@ func TestComputeMachineDeployment(t *testing.T) { g.Expect(*actualMd.Spec.Template.Spec.Deletion.NodeVolumeDetachTimeoutSeconds).To(Equal(topologyDuration)) g.Expect(*actualMd.Spec.Template.Spec.Deletion.NodeDeletionTimeoutSeconds).To(Equal(topologyDuration)) g.Expect(actualMd.Spec.Template.Spec.ReadinessGates).To(Equal(readinessGates)) + g.Expect(actualMd.Spec.Template.Spec.Taints).To(Equal(taints)) g.Expect(actualMd.Spec.ClusterName).To(Equal("cluster1")) g.Expect(actualMd.Name).To(ContainSubstring("cluster1")) g.Expect(actualMd.Name).To(ContainSubstring("big-pool-of-machines")) @@ -1989,7 +2046,7 @@ func TestComputeMachineDeployment(t *testing.T) { Class: "linux-worker", Name: "big-pool-of-machines", Replicas: &replicas, - // missing ReadinessGates, FailureDomain, NodeDrainTimeoutSeconds, NodeVolumeDetachTimeoutSeconds, NodeDeletionTimeoutSeconds, MinReadySeconds, Strategy, deletion.Order, remediation + // missing ReadinessGates, FailureDomain, NodeDrainTimeoutSeconds, NodeVolumeDetachTimeoutSeconds, NodeDeletionTimeoutSeconds, MinReadySeconds, Strategy, deletion.Order, remediation, taints } e := generator{} @@ -2003,6 +2060,7 @@ func TestComputeMachineDeployment(t *testing.T) { g.Expect(actualMd.Spec.Template.Spec.MinReadySeconds).To(HaveValue(Equal(clusterClassMinReadySeconds))) g.Expect(actualMd.Spec.Template.Spec.FailureDomain).To(Equal(clusterClassFailureDomain)) g.Expect(actualMd.Spec.Template.Spec.ReadinessGates).To(Equal(clusterClassReadinessGates)) + g.Expect(actualMd.Spec.Template.Spec.Taints).To(Equal(clusterClassTaints)) g.Expect(actualMd.Spec.Remediation.MaxInFlight).To(Equal(clusterClassHealthCheck.Remediation.MaxInFlight)) g.Expect(actualMd.Spec.Deletion.Order).To(Equal(clusterClassDeletionOrder)) g.Expect(*actualMd.Spec.Template.Spec.Deletion.NodeDrainTimeoutSeconds).To(Equal(clusterClassDuration)) @@ -2061,6 +2119,57 @@ func TestComputeMachineDeployment(t *testing.T) { g.Expect(actualMd.Spec.Template.Spec.ReadinessGates).To(BeNil()) }) + t.Run("Skips setting taints if not set in Cluster and ClusterClass", func(t *testing.T) { + g := NewWithT(t) + + clusterClassWithoutTaints := fakeClass.DeepCopy() + clusterClassWithoutTaints.Spec.Workers.MachineDeployments[0].Taints = nil + + blueprint := &scope.ClusterBlueprint{ + Topology: cluster.Spec.Topology, + ClusterClass: clusterClassWithoutTaints, + MachineDeployments: map[string]*scope.MachineDeploymentBlueprint{ + "linux-worker": { + Metadata: clusterv1.ObjectMeta{ + Labels: labels, + Annotations: annotations, + }, + BootstrapTemplate: workerBootstrapTemplate, + InfrastructureMachineTemplate: workerInfrastructureMachineTemplate, + HealthCheck: clusterv1.MachineDeploymentClassHealthCheck{ + Checks: clusterv1.MachineDeploymentClassHealthCheckChecks{ + UnhealthyNodeConditions: unhealthyNodeConditions, + UnhealthyMachineConditions: unhealthyMachineConditions, + NodeStartupTimeoutSeconds: ptr.To(int32(1)), + }, + }, + }, + }, + } + + scope := scope.New(cluster) + scope.Blueprint = blueprint + + mdTopology := clusterv1.MachineDeploymentTopology{ + Metadata: clusterv1.ObjectMeta{ + Labels: map[string]string{"foo": "baz"}, + }, + Class: "linux-worker", + Name: "big-pool-of-machines", + Replicas: &replicas, + // missing Taints + } + + e := generator{} + + actual, err := e.computeMachineDeployment(ctx, scope, mdTopology) + g.Expect(err).ToNot(HaveOccurred()) + + // checking only values from CC defaults + actualMd := actual.Object + g.Expect(actualMd.Spec.Template.Spec.Taints).To(BeNil()) + }) + t.Run("If there is already a machine deployment, it preserves the object name and the reference names", func(t *testing.T) { g := NewWithT(t) s := scope.New(cluster) @@ -3557,6 +3666,7 @@ func assertTemplateToTemplate(g *WithT, in assertTemplateInput) { } func assertNestedField(g *WithT, obj *unstructured.Unstructured, value interface{}, fields ...string) { + g.THelper() v, ok, err := unstructured.NestedFieldCopy(obj.UnstructuredContent(), fields...) g.Expect(err).ToNot(HaveOccurred()) diff --git a/util/test/builder/builders.go b/util/test/builder/builders.go index 5187827d3383..ab73c20f15c3 100644 --- a/util/test/builder/builders.go +++ b/util/test/builder/builders.go @@ -346,6 +346,7 @@ type ClusterClassBuilder struct { infrastructureClusterTemplate *unstructured.Unstructured controlPlaneMetadata *clusterv1.ObjectMeta controlPlaneReadinessGates []clusterv1.MachineReadinessGate + controlPlaneTaints []clusterv1.MachineTaint controlPlaneTemplate *unstructured.Unstructured controlPlaneInfrastructureMachineTemplate *unstructured.Unstructured controlPlaneMHC clusterv1.ControlPlaneClassHealthCheck @@ -404,6 +405,12 @@ func (c *ClusterClassBuilder) WithControlPlaneReadinessGates(readinessGates []cl return c } +// WithControlPlaneTaints adds the given taints for use with the ControlPlane to the ClusterClassBuilder. +func (c *ClusterClassBuilder) WithControlPlaneTaints(taints []clusterv1.MachineTaint) *ClusterClassBuilder { + c.controlPlaneTaints = taints + return c +} + // WithControlPlaneInfrastructureMachineTemplate adds the ControlPlane's InfrastructureMachineTemplate to the ClusterClassBuilder. func (c *ClusterClassBuilder) WithControlPlaneInfrastructureMachineTemplate(t *unstructured.Unstructured) *ClusterClassBuilder { c.controlPlaneInfrastructureMachineTemplate = t @@ -524,6 +531,9 @@ func (c *ClusterClassBuilder) Build() *clusterv1.ClusterClass { if c.controlPlaneReadinessGates != nil { obj.Spec.ControlPlane.ReadinessGates = c.controlPlaneReadinessGates } + if c.controlPlaneTaints != nil { + obj.Spec.ControlPlane.Taints = c.controlPlaneTaints + } if c.controlPlaneTemplate != nil { obj.Spec.ControlPlane.TemplateRef = objToClusterClassTemplateRef(c.controlPlaneTemplate) } @@ -572,6 +582,7 @@ type MachineDeploymentClassBuilder struct { strategy clusterv1.MachineDeploymentClassRolloutStrategy deletionOrder clusterv1.MachineSetDeletionOrder naming *clusterv1.MachineDeploymentClassNamingSpec + taints []clusterv1.MachineTaint } // MachineDeploymentClass returns a MachineDeploymentClassBuilder with the given name and namespace. @@ -665,6 +676,12 @@ func (m *MachineDeploymentClassBuilder) WithNaming(n *clusterv1.MachineDeploymen return m } +// WithTaints sets the Taints for the MachineDeploymentClassBuilder. +func (m *MachineDeploymentClassBuilder) WithTaints(taints []clusterv1.MachineTaint) *MachineDeploymentClassBuilder { + m.taints = taints + return m +} + // Build creates a full MachineDeploymentClass object with the variables passed to the MachineDeploymentClassBuilder. func (m *MachineDeploymentClassBuilder) Build() *clusterv1.MachineDeploymentClass { obj := &clusterv1.MachineDeploymentClass{ @@ -704,6 +721,7 @@ func (m *MachineDeploymentClassBuilder) Build() *clusterv1.MachineDeploymentClas if m.naming != nil { obj.Naming = *m.naming } + obj.Taints = m.taints return obj } From 8c5c025095801f9d61d36716942686669bc4cb51 Mon Sep 17 00:00:00 2001 From: Christian Schlotter Date: Tue, 23 Dec 2025 15:46:20 +0100 Subject: [PATCH 7/8] e2e: topology e2e test coverage --- Makefile | 1 + test/e2e/clusterclass_rollout.go | 187 ++++++++++++++++++ test/e2e/config/docker.yaml | 1 + .../controlplane-taints.yaml | 17 ++ .../kustomization.yaml | 10 + .../machinedeployment-taints.yaml | 11 ++ 6 files changed, 227 insertions(+) create mode 100644 test/e2e/data/infrastructure-docker/main/cluster-template-topology-taints/controlplane-taints.yaml create mode 100644 test/e2e/data/infrastructure-docker/main/cluster-template-topology-taints/kustomization.yaml create mode 100644 test/e2e/data/infrastructure-docker/main/cluster-template-topology-taints/machinedeployment-taints.yaml diff --git a/Makefile b/Makefile index 8c3c5a673932..7a9075662434 100644 --- a/Makefile +++ b/Makefile @@ -591,6 +591,7 @@ generate-e2e-templates-main: $(KUSTOMIZE) $(KUSTOMIZE) build $(DOCKER_TEMPLATES)/main/cluster-template-topology-kcp-only --load-restrictor LoadRestrictionsNone > $(DOCKER_TEMPLATES)/main/cluster-template-topology-kcp-only.yaml $(KUSTOMIZE) build $(DOCKER_TEMPLATES)/main/cluster-template-topology-autoscaler --load-restrictor LoadRestrictionsNone > $(DOCKER_TEMPLATES)/main/cluster-template-topology-autoscaler.yaml $(KUSTOMIZE) build $(DOCKER_TEMPLATES)/main/cluster-template-topology --load-restrictor LoadRestrictionsNone > $(DOCKER_TEMPLATES)/main/cluster-template-topology.yaml + $(KUSTOMIZE) build $(DOCKER_TEMPLATES)/main/cluster-template-topology-taints --load-restrictor LoadRestrictionsNone > $(DOCKER_TEMPLATES)/main/cluster-template-topology-taints.yaml $(KUSTOMIZE) build $(DOCKER_TEMPLATES)/main/cluster-template-ignition --load-restrictor LoadRestrictionsNone > $(DOCKER_TEMPLATES)/main/cluster-template-ignition.yaml $(KUSTOMIZE) build $(DOCKER_TEMPLATES)/main/clusterclass-quick-start-kcp-only --load-restrictor LoadRestrictionsNone > $(DOCKER_TEMPLATES)/main/clusterclass-quick-start-kcp-only.yaml diff --git a/test/e2e/clusterclass_rollout.go b/test/e2e/clusterclass_rollout.go index 252fb03d3c78..5bca9795079a 100644 --- a/test/e2e/clusterclass_rollout.go +++ b/test/e2e/clusterclass_rollout.go @@ -226,6 +226,193 @@ func ClusterClassRolloutSpec(ctx context.Context, inputGetter func() ClusterClas }, WaitForMachinePools: input.E2EConfig.GetIntervals(specName, "wait-machine-pool-nodes"), }) + + additionalControlPlaneNodeTaint := corev1.Taint{ + Key: "node-role.kubernetes.io/control-plane", + Value: "", + Effect: "NoSchedule", + } + + preExistingAlwaysTaint := clusterv1.MachineTaint{ + Key: "pre-existing-always-taint", + Value: "always-value", + Effect: corev1.TaintEffectPreferNoSchedule, + Propagation: clusterv1.MachineTaintPropagationAlways, + } + + preExistingOnInitializationTaint := clusterv1.MachineTaint{ + Key: "pre-existing-on-initialization-taint", + Value: "on-initialization-value", + Effect: corev1.TaintEffectPreferNoSchedule, + Propagation: clusterv1.MachineTaintPropagationOnInitialization, + } + addingAlwaysTaint := clusterv1.MachineTaint{ + Key: "added-always-taint", + Value: "added-always-value", + Effect: corev1.TaintEffectPreferNoSchedule, + Propagation: clusterv1.MachineTaintPropagationAlways, + } + + addingOnInitializationTaint := clusterv1.MachineTaint{ + Key: "added-on-initialization-taint", + Value: "added-on-initialization-value", + Effect: corev1.TaintEffectPreferNoSchedule, + Propagation: clusterv1.MachineTaintPropagationOnInitialization, + } + + wantMachineTaints := []clusterv1.MachineTaint{ + preExistingAlwaysTaint, + preExistingOnInitializationTaint, + } + wantNodeTaints := toCoreV1Taints( + preExistingAlwaysTaint, + preExistingOnInitializationTaint, + ) + + wlClient := input.BootstrapClusterProxy.GetWorkloadCluster(ctx, clusterResources.Cluster.Namespace, clusterResources.Cluster.Name).GetClient() + + Byf("Verify ControlPlane Machines and Nodes have the correct taints") + verifyControlPlaneMachineAndNodeTaints(ctx, verifyControlPlaneMachineAndNodeTaintsInput{ + BootstrapClusterClient: input.BootstrapClusterProxy.GetClient(), + WorkloadClusterClient: wlClient, + ClusterName: clusterResources.Cluster.Name, + Namespace: clusterResources.Cluster.Namespace, + ControlPlaneReplicas: clusterResources.ControlPlane.Spec.Replicas, + MachineTaints: wantMachineTaints, + NodeTaints: append(wantNodeTaints, additionalControlPlaneNodeTaint), + }) + + Byf("Verify MachineDeployment Machines and Nodes have the correct taints") + verifyMachineDeploymentMachineAndNodeTaints(ctx, verifyMachineDeploymentMachineAndNodeTaintsInput{ + BootstrapClusterClient: input.BootstrapClusterProxy.GetClient(), + WorkloadClusterClient: wlClient, + ClusterName: clusterResources.Cluster.Name, + MachineDeployments: clusterResources.MachineDeployments, + MachineTaints: wantMachineTaints, + NodeTaints: wantNodeTaints, + }) + + Byf("Verify in-place propagation by adding new taints to the ControlPlane and MachineDeployment") + wantMachineTaints = []clusterv1.MachineTaint{ + preExistingAlwaysTaint, + preExistingOnInitializationTaint, + addingAlwaysTaint, + addingOnInitializationTaint, + } + wantNodeTaints = toCoreV1Taints( + preExistingAlwaysTaint, + preExistingOnInitializationTaint, + addingAlwaysTaint, + ) + + patchHelper, err := patch.NewHelper(clusterResources.Cluster, input.BootstrapClusterProxy.GetClient()) + Expect(err).ToNot(HaveOccurred()) + clusterResources.Cluster.Spec.Topology.ControlPlane.Taints = wantMachineTaints + for i, md := range clusterResources.Cluster.Spec.Topology.Workers.MachineDeployments { + md.Taints = wantMachineTaints + clusterResources.Cluster.Spec.Topology.Workers.MachineDeployments[i] = md + } + Expect(patchHelper.Patch(ctx, clusterResources.Cluster)).To(Succeed()) + + verifyControlPlaneMachineAndNodeTaints(ctx, verifyControlPlaneMachineAndNodeTaintsInput{ + BootstrapClusterClient: input.BootstrapClusterProxy.GetClient(), + WorkloadClusterClient: wlClient, + ClusterName: clusterResources.Cluster.Name, + Namespace: clusterResources.Cluster.Namespace, + ControlPlaneReplicas: clusterResources.ControlPlane.Spec.Replicas, + MachineTaints: wantMachineTaints, + NodeTaints: append(wantNodeTaints, additionalControlPlaneNodeTaint), + }) + + verifyMachineDeploymentMachineAndNodeTaints(ctx, verifyMachineDeploymentMachineAndNodeTaintsInput{ + BootstrapClusterClient: input.BootstrapClusterProxy.GetClient(), + WorkloadClusterClient: wlClient, + ClusterName: clusterResources.Cluster.Name, + MachineDeployments: clusterResources.MachineDeployments, + MachineTaints: wantMachineTaints, + NodeTaints: wantNodeTaints, + }) + + Byf("Verify in-place propagation when removing preExisting Always and OnInitialization taint from the nodes") + nodes := corev1.NodeList{} + Expect(wlClient.List(ctx, &nodes)).To(Succeed()) + // Remove the initial taints from the nodes. + for _, node := range nodes.Items { + patchHelper, err := patch.NewHelper(&node, wlClient) + Expect(err).ToNot(HaveOccurred()) + newTaints := []corev1.Taint{} + for _, taint := range node.Spec.Taints { + if taint.Key == preExistingAlwaysTaint.Key { + continue + } + if taint.Key == preExistingOnInitializationTaint.Key { + continue + } + newTaints = append(newTaints, taint) + } + node.Spec.Taints = newTaints + Expect(patchHelper.Patch(ctx, &node)).To(Succeed()) + } + + wantNodeTaints = toCoreV1Taints( + preExistingAlwaysTaint, + addingAlwaysTaint, + ) + + verifyControlPlaneMachineAndNodeTaints(ctx, verifyControlPlaneMachineAndNodeTaintsInput{ + BootstrapClusterClient: input.BootstrapClusterProxy.GetClient(), + WorkloadClusterClient: wlClient, + ClusterName: clusterResources.Cluster.Name, + Namespace: clusterResources.Cluster.Namespace, + ControlPlaneReplicas: clusterResources.ControlPlane.Spec.Replicas, + MachineTaints: wantMachineTaints, + NodeTaints: append(wantNodeTaints, additionalControlPlaneNodeTaint), + }) + + verifyMachineDeploymentMachineAndNodeTaints(ctx, verifyMachineDeploymentMachineAndNodeTaintsInput{ + BootstrapClusterClient: input.BootstrapClusterProxy.GetClient(), + WorkloadClusterClient: wlClient, + ClusterName: clusterResources.Cluster.Name, + MachineDeployments: clusterResources.MachineDeployments, + MachineTaints: wantMachineTaints, + NodeTaints: wantNodeTaints, + }) + + Byf("Verify in-place propagation by removing taints from ControlPlane and the MachineDeployment") + wantMachineTaints = []clusterv1.MachineTaint{ + preExistingOnInitializationTaint, + addingOnInitializationTaint, + } + wantNodeTaints = toCoreV1Taints() + + patchHelper, err = patch.NewHelper(clusterResources.Cluster, input.BootstrapClusterProxy.GetClient()) + Expect(err).ToNot(HaveOccurred()) + clusterResources.Cluster.Spec.Topology.ControlPlane.Taints = wantMachineTaints + for i, md := range clusterResources.Cluster.Spec.Topology.Workers.MachineDeployments { + md.Taints = wantMachineTaints + clusterResources.Cluster.Spec.Topology.Workers.MachineDeployments[i] = md + } + Expect(patchHelper.Patch(ctx, clusterResources.Cluster)).To(Succeed()) + + verifyControlPlaneMachineAndNodeTaints(ctx, verifyControlPlaneMachineAndNodeTaintsInput{ + BootstrapClusterClient: input.BootstrapClusterProxy.GetClient(), + WorkloadClusterClient: wlClient, + ClusterName: clusterResources.Cluster.Name, + Namespace: clusterResources.Cluster.Namespace, + ControlPlaneReplicas: clusterResources.ControlPlane.Spec.Replicas, + MachineTaints: wantMachineTaints, + NodeTaints: append(wantNodeTaints, additionalControlPlaneNodeTaint), + }) + + verifyMachineDeploymentMachineAndNodeTaints(ctx, verifyMachineDeploymentMachineAndNodeTaintsInput{ + BootstrapClusterClient: input.BootstrapClusterProxy.GetClient(), + WorkloadClusterClient: wlClient, + ClusterName: clusterResources.Cluster.Name, + MachineDeployments: clusterResources.MachineDeployments, + MachineTaints: wantMachineTaints, + NodeTaints: wantNodeTaints, + }) + By("Verifying there are no unexpected rollouts through in-place rollout") Consistently(func(g Gomega) { machinesAfterUpgrade := getMachinesByCluster(ctx, input.BootstrapClusterProxy.GetClient(), clusterResources.Cluster) diff --git a/test/e2e/config/docker.yaml b/test/e2e/config/docker.yaml index f2f710ca6924..73b68be5c188 100644 --- a/test/e2e/config/docker.yaml +++ b/test/e2e/config/docker.yaml @@ -197,6 +197,7 @@ providers: - sourcePath: "../data/infrastructure-docker/main/cluster-template-topology-kcp-only.yaml" - sourcePath: "../data/infrastructure-docker/main/cluster-template-topology-autoscaler.yaml" - sourcePath: "../data/infrastructure-docker/main/cluster-template-topology.yaml" + - sourcePath: "../data/infrastructure-docker/main/cluster-template-topology-taints.yaml" - sourcePath: "../data/infrastructure-docker/main/cluster-template-ignition.yaml" - sourcePath: "../data/infrastructure-docker/main/cluster-template-in-memory.yaml" - sourcePath: "../data/infrastructure-docker/main/clusterclass-quick-start.yaml" diff --git a/test/e2e/data/infrastructure-docker/main/cluster-template-topology-taints/controlplane-taints.yaml b/test/e2e/data/infrastructure-docker/main/cluster-template-topology-taints/controlplane-taints.yaml new file mode 100644 index 000000000000..62cf4fe38ac2 --- /dev/null +++ b/test/e2e/data/infrastructure-docker/main/cluster-template-topology-taints/controlplane-taints.yaml @@ -0,0 +1,17 @@ +apiVersion: cluster.x-k8s.io/v1beta2 +kind: Cluster +metadata: + name: '${CLUSTER_NAME}' + namespace: default +spec: + topology: + controlPlane: + taints: + - effect: PreferNoSchedule + key: pre-existing-on-initialization-taint + propagation: OnInitialization + value: on-initialization-value + - effect: PreferNoSchedule + key: pre-existing-always-taint + propagation: Always + value: always-value diff --git a/test/e2e/data/infrastructure-docker/main/cluster-template-topology-taints/kustomization.yaml b/test/e2e/data/infrastructure-docker/main/cluster-template-topology-taints/kustomization.yaml new file mode 100644 index 000000000000..100c4fc9e6f8 --- /dev/null +++ b/test/e2e/data/infrastructure-docker/main/cluster-template-topology-taints/kustomization.yaml @@ -0,0 +1,10 @@ +resources: + - ../cluster-template-topology + +patches: +- path: controlplane-taints.yaml +- path: machinedeployment-taints.yaml + target: + group: cluster.x-k8s.io + version: v1beta2 + kind: Cluster \ No newline at end of file diff --git a/test/e2e/data/infrastructure-docker/main/cluster-template-topology-taints/machinedeployment-taints.yaml b/test/e2e/data/infrastructure-docker/main/cluster-template-topology-taints/machinedeployment-taints.yaml new file mode 100644 index 000000000000..e7e4e879f9eb --- /dev/null +++ b/test/e2e/data/infrastructure-docker/main/cluster-template-topology-taints/machinedeployment-taints.yaml @@ -0,0 +1,11 @@ +- op: add + path: /spec/topology/workers/machineDeployments/0/taints + value: + - effect: PreferNoSchedule + key: pre-existing-on-initialization-taint + propagation: OnInitialization + value: on-initialization-value + - effect: PreferNoSchedule + key: pre-existing-always-taint + propagation: Always + value: always-value From 3cb3c6b7145f03920bc9fdaacda4402f6558b5f9 Mon Sep 17 00:00:00 2001 From: Christian Schlotter Date: Fri, 13 Feb 2026 07:39:16 +0100 Subject: [PATCH 8/8] review fixes --- internal/util/taints/validation.go | 6 +++--- util/test/builder/zz_generated.deepcopy.go | 10 ++++++++++ 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/internal/util/taints/validation.go b/internal/util/taints/validation.go index 63a19595be1e..727818f4b318 100644 --- a/internal/util/taints/validation.go +++ b/internal/util/taints/validation.go @@ -19,7 +19,7 @@ package taints import ( "strings" - "k8s.io/apimachinery/pkg/util/validation" + "k8s.io/apimachinery/pkg/api/validate/content" "k8s.io/apimachinery/pkg/util/validation/field" clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta2" @@ -58,11 +58,11 @@ func ValidateMachineTaints(taints []clusterv1.MachineTaint, taintsPath *field.Pa allErrs = append(allErrs, field.Invalid(idxPath.Child("key"), taint.Key, "taint is deprecated since 1.24 and should not be used anymore")) } - if validation.IsQualifiedName(taint.Key) != nil { + for _, msg := range content.IsLabelKey(taint.Key) { allErrs = append(allErrs, field.Invalid( idxPath.Child("key"), taint.Key, - "key must be a valid qualified name of max size 63 characters with an optional subdomain prefix of max size 253 characters", + msg, )) } } diff --git a/util/test/builder/zz_generated.deepcopy.go b/util/test/builder/zz_generated.deepcopy.go index b7c3d32289e8..821fa49ba68c 100644 --- a/util/test/builder/zz_generated.deepcopy.go +++ b/util/test/builder/zz_generated.deepcopy.go @@ -133,6 +133,11 @@ func (in *ClusterClassBuilder) DeepCopyInto(out *ClusterClassBuilder) { *out = make([]v1beta2.MachineReadinessGate, len(*in)) copy(*out, *in) } + if in.controlPlaneTaints != nil { + in, out := &in.controlPlaneTaints, &out.controlPlaneTaints + *out = make([]v1beta2.MachineTaint, len(*in)) + copy(*out, *in) + } if in.controlPlaneTemplate != nil { in, out := &in.controlPlaneTemplate, &out.controlPlaneTemplate *out = (*in).DeepCopy() @@ -583,6 +588,11 @@ func (in *MachineDeploymentClassBuilder) DeepCopyInto(out *MachineDeploymentClas *out = new(v1beta2.MachineDeploymentClassNamingSpec) **out = **in } + if in.taints != nil { + in, out := &in.taints, &out.taints + *out = make([]v1beta2.MachineTaint, len(*in)) + copy(*out, *in) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MachineDeploymentClassBuilder.