Skip to content

Commit 3e31856

Browse files
authored
Merge pull request #12816 from Karthik-K-N/chained-upgrades
✨ Additional validation in Cluster/ClusterClass webhook for chained upgrades
2 parents dd34fd2 + 099c49d commit 3e31856

File tree

3 files changed

+158
-2
lines changed

3 files changed

+158
-2
lines changed

internal/webhooks/cluster_test.go

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2703,7 +2703,6 @@ func TestClusterTopologyValidationForTopologyClassChange(t *testing.T) {
27032703
Build(),
27042704
wantErr: false,
27052705
},
2706-
27072706
{
27082707
name: "Reject cluster.topology.class change with an incompatible infrastructureCluster Kind ref change",
27092708
firstClass: builder.ClusterClass(metav1.NamespaceDefault, "class1").
@@ -2762,7 +2761,6 @@ func TestClusterTopologyValidationForTopologyClassChange(t *testing.T) {
27622761
Build(),
27632762
wantErr: false,
27642763
},
2765-
27662764
{
27672765
name: "Reject cluster.topology.class change with an incompatible controlPlane Kind ref change",
27682766
firstClass: builder.ClusterClass(metav1.NamespaceDefault, "class1").
@@ -3123,6 +3121,38 @@ func TestClusterTopologyValidationForTopologyClassChange(t *testing.T) {
31233121
Build(),
31243122
wantErr: true,
31253123
},
3124+
3125+
// Kubernetes Version changes.
3126+
{
3127+
name: "Accept cluster.topology.class change with a compatible Kubernetes Version",
3128+
firstClass: builder.ClusterClass(metav1.NamespaceDefault, "class1").
3129+
WithInfrastructureClusterTemplate(refToUnstructured(ref)).
3130+
WithControlPlaneTemplate(refToUnstructured(ref)).
3131+
WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)).
3132+
Build(),
3133+
secondClass: builder.ClusterClass(metav1.NamespaceDefault, "class2").
3134+
WithInfrastructureClusterTemplate(refToUnstructured(compatibleNameChangeRef)).
3135+
WithControlPlaneTemplate(refToUnstructured(ref)).
3136+
WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)).
3137+
WithVersions("v1.22.2", "v1.23.2").
3138+
Build(),
3139+
wantErr: false,
3140+
},
3141+
{
3142+
name: "Reject cluster.topology.class change with an incompatible Kubernetes Version",
3143+
firstClass: builder.ClusterClass(metav1.NamespaceDefault, "class1").
3144+
WithInfrastructureClusterTemplate(refToUnstructured(ref)).
3145+
WithControlPlaneTemplate(refToUnstructured(ref)).
3146+
WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)).
3147+
Build(),
3148+
secondClass: builder.ClusterClass(metav1.NamespaceDefault, "class2").
3149+
WithInfrastructureClusterTemplate(refToUnstructured(compatibleNameChangeRef)).
3150+
WithControlPlaneTemplate(refToUnstructured(ref)).
3151+
WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)).
3152+
WithVersions("v1.33.0", "v1.34.0").
3153+
Build(),
3154+
wantErr: true,
3155+
},
31263156
}
31273157
for _, tt := range tests {
31283158
t.Run(tt.name, func(*testing.T) {

internal/webhooks/clusterclass.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,10 @@ func (webhook *ClusterClass) validate(ctx context.Context, oldClusterClass, newC
163163
return apierrors.NewInvalid(clusterv1.GroupVersion.WithKind("ClusterClass").GroupKind(), newClusterClass.Name, allErrs)
164164
}
165165

166+
// Ensure New ClusterClass contains Kubernetes versions of all the Clusters of ClusterClass.
167+
allErrs = append(allErrs,
168+
webhook.validateKubernetesVersionsOfClusters(clusters, oldClusterClass, newClusterClass)...)
169+
166170
// Ensure no MachineDeploymentClass currently in use has been removed from the ClusterClass.
167171
allErrs = append(allErrs,
168172
webhook.validateRemovedMachineDeploymentClassesAreNotUsed(clusters, oldClusterClass, newClusterClass)...)
@@ -246,6 +250,32 @@ func validateUpdatesToMachineHealthCheckClasses(clusters []clusterv1.Cluster, ol
246250
return allErrs
247251
}
248252

253+
func (webhook *ClusterClass) validateKubernetesVersionsOfClusters(clusters []clusterv1.Cluster, _, newClusterClass *clusterv1.ClusterClass) field.ErrorList {
254+
var allErrs field.ErrorList
255+
256+
// If there is no KubernetesVersions is set in the ClusterClass return early.
257+
if len(newClusterClass.Spec.KubernetesVersions) == 0 {
258+
return allErrs
259+
}
260+
261+
kubernetesVersions := sets.Set[string]{}
262+
for _, v := range newClusterClass.Spec.KubernetesVersions {
263+
kubernetesVersions.Insert(v)
264+
}
265+
266+
// Error if any Cluster's Kubernetes version is not set in the ClusterClass.
267+
for _, c := range clusters {
268+
if !kubernetesVersions.Has(c.Spec.Topology.Version) {
269+
allErrs = append(allErrs, field.Forbidden(field.NewPath("spec", "kubernetesVersions"),
270+
fmt.Sprintf("Kubernetes Version %s is used by Cluster %q but not set in ClusterClass",
271+
c.Spec.Topology.Version, c.Name),
272+
))
273+
}
274+
}
275+
276+
return allErrs
277+
}
278+
249279
func (webhook *ClusterClass) validateRemovedMachineDeploymentClassesAreNotUsed(clusters []clusterv1.Cluster, oldClusterClass, newClusterClass *clusterv1.ClusterClass) field.ErrorList {
250280
var allErrs field.ErrorList
251281

internal/webhooks/clusterclass_test.go

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2489,6 +2489,102 @@ func TestClusterClassValidationWithClusterAwareChecks(t *testing.T) {
24892489
Build(),
24902490
expectErr: false,
24912491
},
2492+
{
2493+
name: "pass if ClusterClass does not contain any kubernetes version",
2494+
clusters: []client.Object{
2495+
builder.Cluster(metav1.NamespaceDefault, "cluster1").
2496+
WithLabels(map[string]string{clusterv1.ClusterTopologyOwnedLabel: ""}).
2497+
WithTopology(
2498+
builder.ClusterTopology().
2499+
WithClass("class1").
2500+
WithVersion("v1.33.0").
2501+
Build()).
2502+
Build(),
2503+
},
2504+
oldClusterClass: builder.ClusterClass(metav1.NamespaceDefault, "class1").
2505+
WithInfrastructureClusterTemplate(
2506+
builder.InfrastructureClusterTemplate(metav1.NamespaceDefault, "inf").Build()).
2507+
WithControlPlaneTemplate(
2508+
builder.ControlPlaneTemplate(metav1.NamespaceDefault, "cp1").Build()).
2509+
Build(),
2510+
newClusterClass: builder.ClusterClass(metav1.NamespaceDefault, "class1").
2511+
WithInfrastructureClusterTemplate(
2512+
builder.InfrastructureClusterTemplate(metav1.NamespaceDefault, "inf").Build()).
2513+
WithControlPlaneTemplate(
2514+
builder.ControlPlaneTemplate(metav1.NamespaceDefault, "cp1").Build()).
2515+
Build(),
2516+
expectErr: false,
2517+
},
2518+
{
2519+
name: "pass if ClusterClass contains cluster's kubernetes version",
2520+
clusters: []client.Object{
2521+
builder.Cluster(metav1.NamespaceDefault, "cluster1").
2522+
WithLabels(map[string]string{clusterv1.ClusterTopologyOwnedLabel: ""}).
2523+
WithTopology(
2524+
builder.ClusterTopology().
2525+
WithClass("class1").
2526+
WithVersion("v1.33.0").
2527+
Build()).
2528+
Build(),
2529+
builder.Cluster(metav1.NamespaceDefault, "cluster2").
2530+
WithLabels(map[string]string{clusterv1.ClusterTopologyOwnedLabel: ""}).
2531+
WithTopology(
2532+
builder.ClusterTopology().
2533+
WithClass("class1").
2534+
WithVersion("v1.34.0").
2535+
Build()).
2536+
Build(),
2537+
},
2538+
oldClusterClass: builder.ClusterClass(metav1.NamespaceDefault, "class1").
2539+
WithInfrastructureClusterTemplate(
2540+
builder.InfrastructureClusterTemplate(metav1.NamespaceDefault, "inf").Build()).
2541+
WithControlPlaneTemplate(
2542+
builder.ControlPlaneTemplate(metav1.NamespaceDefault, "cp1").Build()).
2543+
Build(),
2544+
newClusterClass: builder.ClusterClass(metav1.NamespaceDefault, "class1").
2545+
WithInfrastructureClusterTemplate(
2546+
builder.InfrastructureClusterTemplate(metav1.NamespaceDefault, "inf").Build()).
2547+
WithControlPlaneTemplate(
2548+
builder.ControlPlaneTemplate(metav1.NamespaceDefault, "cp1").Build()).
2549+
WithVersions("v1.32.0", "v1.33.0", "v1.34.0").
2550+
Build(),
2551+
expectErr: false,
2552+
},
2553+
{
2554+
name: "fail if ClusterClass does not contains all of cluster's kubernetes version",
2555+
clusters: []client.Object{
2556+
builder.Cluster(metav1.NamespaceDefault, "cluster1").
2557+
WithLabels(map[string]string{clusterv1.ClusterTopologyOwnedLabel: ""}).
2558+
WithTopology(
2559+
builder.ClusterTopology().
2560+
WithClass("class1").
2561+
WithVersion("v1.33.0").
2562+
Build()).
2563+
Build(),
2564+
builder.Cluster(metav1.NamespaceDefault, "cluster2").
2565+
WithLabels(map[string]string{clusterv1.ClusterTopologyOwnedLabel: ""}).
2566+
WithTopology(
2567+
builder.ClusterTopology().
2568+
WithClass("class1").
2569+
WithVersion("v1.34.0").
2570+
Build()).
2571+
Build(),
2572+
},
2573+
oldClusterClass: builder.ClusterClass(metav1.NamespaceDefault, "class1").
2574+
WithInfrastructureClusterTemplate(
2575+
builder.InfrastructureClusterTemplate(metav1.NamespaceDefault, "inf").Build()).
2576+
WithControlPlaneTemplate(
2577+
builder.ControlPlaneTemplate(metav1.NamespaceDefault, "cp1").Build()).
2578+
Build(),
2579+
newClusterClass: builder.ClusterClass(metav1.NamespaceDefault, "class1").
2580+
WithInfrastructureClusterTemplate(
2581+
builder.InfrastructureClusterTemplate(metav1.NamespaceDefault, "inf").Build()).
2582+
WithControlPlaneTemplate(
2583+
builder.ControlPlaneTemplate(metav1.NamespaceDefault, "cp1").Build()).
2584+
WithVersions("v1.32.0", "v1.33.0").
2585+
Build(),
2586+
expectErr: true,
2587+
},
24922588
}
24932589

24942590
for _, tt := range tests {

0 commit comments

Comments
 (0)