diff --git a/internal/webhooks/cluster_test.go b/internal/webhooks/cluster_test.go index ec114b445d01..3f81f5cb994e 100644 --- a/internal/webhooks/cluster_test.go +++ b/internal/webhooks/cluster_test.go @@ -2703,7 +2703,6 @@ func TestClusterTopologyValidationForTopologyClassChange(t *testing.T) { Build(), wantErr: false, }, - { name: "Reject cluster.topology.class change with an incompatible infrastructureCluster Kind ref change", firstClass: builder.ClusterClass(metav1.NamespaceDefault, "class1"). @@ -2762,7 +2761,6 @@ func TestClusterTopologyValidationForTopologyClassChange(t *testing.T) { Build(), wantErr: false, }, - { name: "Reject cluster.topology.class change with an incompatible controlPlane Kind ref change", firstClass: builder.ClusterClass(metav1.NamespaceDefault, "class1"). @@ -3123,6 +3121,38 @@ func TestClusterTopologyValidationForTopologyClassChange(t *testing.T) { Build(), wantErr: true, }, + + // Kubernetes Version changes. + { + name: "Accept cluster.topology.class change with a compatible Kubernetes Version", + firstClass: builder.ClusterClass(metav1.NamespaceDefault, "class1"). + WithInfrastructureClusterTemplate(refToUnstructured(ref)). + WithControlPlaneTemplate(refToUnstructured(ref)). + WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)). + Build(), + secondClass: builder.ClusterClass(metav1.NamespaceDefault, "class2"). + WithInfrastructureClusterTemplate(refToUnstructured(compatibleNameChangeRef)). + WithControlPlaneTemplate(refToUnstructured(ref)). + WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)). + WithVersions("v1.22.2", "v1.23.2"). + Build(), + wantErr: false, + }, + { + name: "Reject cluster.topology.class change with an incompatible Kubernetes Version", + firstClass: builder.ClusterClass(metav1.NamespaceDefault, "class1"). + WithInfrastructureClusterTemplate(refToUnstructured(ref)). + WithControlPlaneTemplate(refToUnstructured(ref)). + WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)). + Build(), + secondClass: builder.ClusterClass(metav1.NamespaceDefault, "class2"). + WithInfrastructureClusterTemplate(refToUnstructured(compatibleNameChangeRef)). + WithControlPlaneTemplate(refToUnstructured(ref)). + WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)). + WithVersions("v1.33.0", "v1.34.0"). + Build(), + wantErr: true, + }, } for _, tt := range tests { t.Run(tt.name, func(*testing.T) { diff --git a/internal/webhooks/clusterclass.go b/internal/webhooks/clusterclass.go index f984f369b5a2..8bba1ae80188 100644 --- a/internal/webhooks/clusterclass.go +++ b/internal/webhooks/clusterclass.go @@ -163,6 +163,10 @@ func (webhook *ClusterClass) validate(ctx context.Context, oldClusterClass, newC return apierrors.NewInvalid(clusterv1.GroupVersion.WithKind("ClusterClass").GroupKind(), newClusterClass.Name, allErrs) } + // Ensure New ClusterClass contains Kubernetes versions of all the Clusters of ClusterClass. + allErrs = append(allErrs, + webhook.validateKubernetesVersionsOfClusters(clusters, oldClusterClass, newClusterClass)...) + // Ensure no MachineDeploymentClass currently in use has been removed from the ClusterClass. allErrs = append(allErrs, webhook.validateRemovedMachineDeploymentClassesAreNotUsed(clusters, oldClusterClass, newClusterClass)...) @@ -246,6 +250,32 @@ func validateUpdatesToMachineHealthCheckClasses(clusters []clusterv1.Cluster, ol return allErrs } +func (webhook *ClusterClass) validateKubernetesVersionsOfClusters(clusters []clusterv1.Cluster, _, newClusterClass *clusterv1.ClusterClass) field.ErrorList { + var allErrs field.ErrorList + + // If there is no KubernetesVersions is set in the ClusterClass return early. + if len(newClusterClass.Spec.KubernetesVersions) == 0 { + return allErrs + } + + kubernetesVersions := sets.Set[string]{} + for _, v := range newClusterClass.Spec.KubernetesVersions { + kubernetesVersions.Insert(v) + } + + // Error if any Cluster's Kubernetes version is not set in the ClusterClass. + for _, c := range clusters { + if !kubernetesVersions.Has(c.Spec.Topology.Version) { + allErrs = append(allErrs, field.Forbidden(field.NewPath("spec", "kubernetesVersions"), + fmt.Sprintf("Kubernetes Version %s is used by Cluster %q but not set in ClusterClass", + c.Spec.Topology.Version, c.Name), + )) + } + } + + return allErrs +} + func (webhook *ClusterClass) validateRemovedMachineDeploymentClassesAreNotUsed(clusters []clusterv1.Cluster, oldClusterClass, newClusterClass *clusterv1.ClusterClass) field.ErrorList { var allErrs field.ErrorList diff --git a/internal/webhooks/clusterclass_test.go b/internal/webhooks/clusterclass_test.go index 600fa24c304a..71009d1d8509 100644 --- a/internal/webhooks/clusterclass_test.go +++ b/internal/webhooks/clusterclass_test.go @@ -2489,6 +2489,102 @@ func TestClusterClassValidationWithClusterAwareChecks(t *testing.T) { Build(), expectErr: false, }, + { + name: "pass if ClusterClass does not contain any kubernetes version", + clusters: []client.Object{ + builder.Cluster(metav1.NamespaceDefault, "cluster1"). + WithLabels(map[string]string{clusterv1.ClusterTopologyOwnedLabel: ""}). + WithTopology( + builder.ClusterTopology(). + WithClass("class1"). + WithVersion("v1.33.0"). + Build()). + Build(), + }, + oldClusterClass: builder.ClusterClass(metav1.NamespaceDefault, "class1"). + WithInfrastructureClusterTemplate( + builder.InfrastructureClusterTemplate(metav1.NamespaceDefault, "inf").Build()). + WithControlPlaneTemplate( + builder.ControlPlaneTemplate(metav1.NamespaceDefault, "cp1").Build()). + Build(), + newClusterClass: builder.ClusterClass(metav1.NamespaceDefault, "class1"). + WithInfrastructureClusterTemplate( + builder.InfrastructureClusterTemplate(metav1.NamespaceDefault, "inf").Build()). + WithControlPlaneTemplate( + builder.ControlPlaneTemplate(metav1.NamespaceDefault, "cp1").Build()). + Build(), + expectErr: false, + }, + { + name: "pass if ClusterClass contains cluster's kubernetes version", + clusters: []client.Object{ + builder.Cluster(metav1.NamespaceDefault, "cluster1"). + WithLabels(map[string]string{clusterv1.ClusterTopologyOwnedLabel: ""}). + WithTopology( + builder.ClusterTopology(). + WithClass("class1"). + WithVersion("v1.33.0"). + Build()). + Build(), + builder.Cluster(metav1.NamespaceDefault, "cluster2"). + WithLabels(map[string]string{clusterv1.ClusterTopologyOwnedLabel: ""}). + WithTopology( + builder.ClusterTopology(). + WithClass("class1"). + WithVersion("v1.34.0"). + Build()). + Build(), + }, + oldClusterClass: builder.ClusterClass(metav1.NamespaceDefault, "class1"). + WithInfrastructureClusterTemplate( + builder.InfrastructureClusterTemplate(metav1.NamespaceDefault, "inf").Build()). + WithControlPlaneTemplate( + builder.ControlPlaneTemplate(metav1.NamespaceDefault, "cp1").Build()). + Build(), + newClusterClass: builder.ClusterClass(metav1.NamespaceDefault, "class1"). + WithInfrastructureClusterTemplate( + builder.InfrastructureClusterTemplate(metav1.NamespaceDefault, "inf").Build()). + WithControlPlaneTemplate( + builder.ControlPlaneTemplate(metav1.NamespaceDefault, "cp1").Build()). + WithVersions("v1.32.0", "v1.33.0", "v1.34.0"). + Build(), + expectErr: false, + }, + { + name: "fail if ClusterClass does not contains all of cluster's kubernetes version", + clusters: []client.Object{ + builder.Cluster(metav1.NamespaceDefault, "cluster1"). + WithLabels(map[string]string{clusterv1.ClusterTopologyOwnedLabel: ""}). + WithTopology( + builder.ClusterTopology(). + WithClass("class1"). + WithVersion("v1.33.0"). + Build()). + Build(), + builder.Cluster(metav1.NamespaceDefault, "cluster2"). + WithLabels(map[string]string{clusterv1.ClusterTopologyOwnedLabel: ""}). + WithTopology( + builder.ClusterTopology(). + WithClass("class1"). + WithVersion("v1.34.0"). + Build()). + Build(), + }, + oldClusterClass: builder.ClusterClass(metav1.NamespaceDefault, "class1"). + WithInfrastructureClusterTemplate( + builder.InfrastructureClusterTemplate(metav1.NamespaceDefault, "inf").Build()). + WithControlPlaneTemplate( + builder.ControlPlaneTemplate(metav1.NamespaceDefault, "cp1").Build()). + Build(), + newClusterClass: builder.ClusterClass(metav1.NamespaceDefault, "class1"). + WithInfrastructureClusterTemplate( + builder.InfrastructureClusterTemplate(metav1.NamespaceDefault, "inf").Build()). + WithControlPlaneTemplate( + builder.ControlPlaneTemplate(metav1.NamespaceDefault, "cp1").Build()). + WithVersions("v1.32.0", "v1.33.0"). + Build(), + expectErr: true, + }, } for _, tt := range tests {