sigs.k8s.io/cluster-api@v1.7.1/internal/webhooks/cluster_test.go (about)

     1  /*
     2  Copyright 2021 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package webhooks
    18  
    19  import (
    20  	"context"
    21  	"testing"
    22  
    23  	"github.com/blang/semver/v4"
    24  	. "github.com/onsi/gomega"
    25  	"github.com/pkg/errors"
    26  	corev1 "k8s.io/api/core/v1"
    27  	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
    28  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    29  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    30  	"k8s.io/apimachinery/pkg/labels"
    31  	"k8s.io/apimachinery/pkg/types"
    32  	utilfeature "k8s.io/component-base/featuregate/testing"
    33  	"k8s.io/utils/ptr"
    34  	"sigs.k8s.io/controller-runtime/pkg/client"
    35  	"sigs.k8s.io/controller-runtime/pkg/client/fake"
    36  	"sigs.k8s.io/controller-runtime/pkg/client/interceptor"
    37  
    38  	clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
    39  	expv1 "sigs.k8s.io/cluster-api/exp/api/v1beta1"
    40  	"sigs.k8s.io/cluster-api/feature"
    41  	"sigs.k8s.io/cluster-api/internal/test/builder"
    42  	"sigs.k8s.io/cluster-api/internal/webhooks/util"
    43  	"sigs.k8s.io/cluster-api/util/conditions"
    44  )
    45  
    46  func TestClusterDefaultNamespaces(t *testing.T) {
    47  	g := NewWithT(t)
    48  
    49  	c := &clusterv1.Cluster{
    50  		ObjectMeta: metav1.ObjectMeta{
    51  			Namespace: "fooboo",
    52  		},
    53  		Spec: clusterv1.ClusterSpec{
    54  			InfrastructureRef: &corev1.ObjectReference{},
    55  			ControlPlaneRef:   &corev1.ObjectReference{},
    56  		},
    57  	}
    58  	webhook := &Cluster{}
    59  	t.Run("for Cluster", util.CustomDefaultValidateTest(ctx, c, webhook))
    60  
    61  	g.Expect(webhook.Default(ctx, c)).To(Succeed())
    62  
    63  	g.Expect(c.Spec.InfrastructureRef.Namespace).To(Equal(c.Namespace))
    64  	g.Expect(c.Spec.ControlPlaneRef.Namespace).To(Equal(c.Namespace))
    65  }
    66  
    67  // TestClusterDefaultAndValidateVariables cases where cluster.spec.topology.class is altered.
    68  func TestClusterDefaultAndValidateVariables(t *testing.T) {
    69  	defer utilfeature.SetFeatureGateDuringTest(t, feature.Gates, feature.ClusterTopology, true)()
    70  
    71  	tests := []struct {
    72  		name         string
    73  		clusterClass *clusterv1.ClusterClass
    74  		topology     *clusterv1.Topology
    75  		expect       *clusterv1.Topology
    76  		wantErr      bool
    77  	}{
    78  		{
    79  			name: "default a single variable to its correct values",
    80  			clusterClass: builder.ClusterClass(metav1.NamespaceDefault, "class1").
    81  				WithStatusVariables(clusterv1.ClusterClassStatusVariable{
    82  					Name: "location",
    83  					Definitions: []clusterv1.ClusterClassStatusVariableDefinition{
    84  						{
    85  							Required: true,
    86  							From:     clusterv1.VariableDefinitionFromInline,
    87  							Schema: clusterv1.VariableSchema{
    88  								OpenAPIV3Schema: clusterv1.JSONSchemaProps{
    89  									Type:    "string",
    90  									Default: &apiextensionsv1.JSON{Raw: []byte(`"us-east"`)},
    91  								},
    92  							},
    93  						},
    94  					},
    95  				},
    96  				).
    97  				Build(),
    98  			topology: &clusterv1.Topology{},
    99  			expect: &clusterv1.Topology{
   100  				Variables: []clusterv1.ClusterVariable{
   101  					{
   102  						Name: "location",
   103  						Value: apiextensionsv1.JSON{
   104  							Raw: []byte(`"us-east"`),
   105  						},
   106  					},
   107  				},
   108  			},
   109  		},
   110  		{
   111  			name: "don't change a variable if it is already set",
   112  			clusterClass: builder.ClusterClass(metav1.NamespaceDefault, "class1").
   113  				WithStatusVariables(clusterv1.ClusterClassStatusVariable{
   114  					Name: "location",
   115  					Definitions: []clusterv1.ClusterClassStatusVariableDefinition{
   116  						{
   117  							Required: true,
   118  							From:     clusterv1.VariableDefinitionFromInline,
   119  							Schema: clusterv1.VariableSchema{
   120  								OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   121  									Type:    "string",
   122  									Default: &apiextensionsv1.JSON{Raw: []byte(`"us-east"`)},
   123  								},
   124  							},
   125  						},
   126  					},
   127  				},
   128  				).
   129  				Build(),
   130  			topology: &clusterv1.Topology{
   131  				Variables: []clusterv1.ClusterVariable{
   132  					{
   133  						Name: "location",
   134  						Value: apiextensionsv1.JSON{
   135  							Raw: []byte(`"A different location"`),
   136  						},
   137  					},
   138  				},
   139  			},
   140  			expect: &clusterv1.Topology{
   141  				Variables: []clusterv1.ClusterVariable{
   142  					{
   143  						Name: "location",
   144  						Value: apiextensionsv1.JSON{
   145  							Raw: []byte(`"A different location"`),
   146  						},
   147  					},
   148  				},
   149  			},
   150  		},
   151  		{
   152  			name: "default many variables to their correct values",
   153  			clusterClass: builder.ClusterClass(metav1.NamespaceDefault, "class1").
   154  				WithStatusVariables([]clusterv1.ClusterClassStatusVariable{
   155  					{
   156  						Name: "location",
   157  						Definitions: []clusterv1.ClusterClassStatusVariableDefinition{
   158  							{
   159  								Required: true,
   160  								From:     clusterv1.VariableDefinitionFromInline,
   161  								Schema: clusterv1.VariableSchema{
   162  									OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   163  										Type:    "string",
   164  										Default: &apiextensionsv1.JSON{Raw: []byte(`"us-east"`)},
   165  									},
   166  								},
   167  							},
   168  						},
   169  					},
   170  					{
   171  						Name: "count",
   172  						Definitions: []clusterv1.ClusterClassStatusVariableDefinition{
   173  							{
   174  								Required: true,
   175  								From:     clusterv1.VariableDefinitionFromInline,
   176  								Schema: clusterv1.VariableSchema{
   177  									OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   178  										Type:    "number",
   179  										Default: &apiextensionsv1.JSON{Raw: []byte(`0.1`)},
   180  									},
   181  								},
   182  							},
   183  						},
   184  					},
   185  				}...).
   186  				Build(),
   187  			topology: &clusterv1.Topology{},
   188  			expect: &clusterv1.Topology{
   189  				Variables: []clusterv1.ClusterVariable{
   190  					{
   191  						Name: "location",
   192  						Value: apiextensionsv1.JSON{
   193  							Raw: []byte(`"us-east"`),
   194  						},
   195  					},
   196  					{
   197  						Name: "count",
   198  						Value: apiextensionsv1.JSON{
   199  							Raw: []byte(`0.1`),
   200  						},
   201  					},
   202  				},
   203  			},
   204  		},
   205  		{
   206  			name: "don't add new variable overrides",
   207  			clusterClass: builder.ClusterClass(metav1.NamespaceDefault, "class1").
   208  				WithWorkerMachineDeploymentClasses(
   209  					*builder.MachineDeploymentClass("default-worker").Build(),
   210  				).
   211  				WithWorkerMachinePoolClasses(
   212  					*builder.MachinePoolClass("default-worker").Build(),
   213  				).
   214  				WithStatusVariables(clusterv1.ClusterClassStatusVariable{
   215  					Name: "location",
   216  					Definitions: []clusterv1.ClusterClassStatusVariableDefinition{
   217  						{
   218  							Required: true,
   219  							From:     clusterv1.VariableDefinitionFromInline,
   220  							Schema: clusterv1.VariableSchema{
   221  								OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   222  									Type:    "string",
   223  									Default: &apiextensionsv1.JSON{Raw: []byte(`"us-east"`)},
   224  								},
   225  							},
   226  						},
   227  					},
   228  				}).
   229  				Build(),
   230  			topology: &clusterv1.Topology{
   231  				Workers: &clusterv1.WorkersTopology{
   232  					MachineDeployments: []clusterv1.MachineDeploymentTopology{
   233  						{
   234  							Class: "default-worker",
   235  							Name:  "md-1",
   236  						},
   237  					},
   238  					MachinePools: []clusterv1.MachinePoolTopology{
   239  						{
   240  							Class: "default-worker",
   241  							Name:  "mp-1",
   242  						},
   243  					},
   244  				},
   245  			},
   246  			expect: &clusterv1.Topology{
   247  				Workers: &clusterv1.WorkersTopology{
   248  					MachineDeployments: []clusterv1.MachineDeploymentTopology{
   249  						{
   250  							Class: "default-worker",
   251  							Name:  "md-1",
   252  							// "location" has not been added to .variables.overrides.
   253  						},
   254  					},
   255  					MachinePools: []clusterv1.MachinePoolTopology{
   256  						{
   257  							Class: "default-worker",
   258  							Name:  "mp-1",
   259  							// "location" has not been added to .variables.overrides.
   260  						},
   261  					},
   262  				},
   263  				Variables: []clusterv1.ClusterVariable{
   264  					{
   265  						Name: "location",
   266  						Value: apiextensionsv1.JSON{
   267  							Raw: []byte(`"us-east"`),
   268  						},
   269  					},
   270  				},
   271  			},
   272  		},
   273  		{
   274  			name: "default nested fields of variable overrides",
   275  			clusterClass: builder.ClusterClass(metav1.NamespaceDefault, "class1").
   276  				WithWorkerMachineDeploymentClasses(
   277  					*builder.MachineDeploymentClass("default-worker").Build(),
   278  				).
   279  				WithWorkerMachinePoolClasses(
   280  					*builder.MachinePoolClass("default-worker").Build(),
   281  				).
   282  				WithStatusVariables(clusterv1.ClusterClassStatusVariable{
   283  					Name: "httpProxy",
   284  					Definitions: []clusterv1.ClusterClassStatusVariableDefinition{
   285  						{
   286  							Required: true,
   287  							From:     clusterv1.VariableDefinitionFromInline,
   288  							Schema: clusterv1.VariableSchema{
   289  								OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   290  									Type: "object",
   291  									Properties: map[string]clusterv1.JSONSchemaProps{
   292  										"enabled": {
   293  											Type: "boolean",
   294  										},
   295  										"url": {
   296  											Type:    "string",
   297  											Default: &apiextensionsv1.JSON{Raw: []byte(`"http://localhost:3128"`)},
   298  										},
   299  									},
   300  								},
   301  							},
   302  						},
   303  					},
   304  				}).
   305  				Build(),
   306  			topology: &clusterv1.Topology{
   307  				Workers: &clusterv1.WorkersTopology{
   308  					MachineDeployments: []clusterv1.MachineDeploymentTopology{
   309  						{
   310  							Class: "default-worker",
   311  							Name:  "md-1",
   312  							Variables: &clusterv1.MachineDeploymentVariables{
   313  								Overrides: []clusterv1.ClusterVariable{
   314  									{
   315  										Name:  "httpProxy",
   316  										Value: apiextensionsv1.JSON{Raw: []byte(`{"enabled":true}`)},
   317  									},
   318  								},
   319  							},
   320  						},
   321  					},
   322  					MachinePools: []clusterv1.MachinePoolTopology{
   323  						{
   324  							Class: "default-worker",
   325  							Name:  "md-1",
   326  							Variables: &clusterv1.MachinePoolVariables{
   327  								Overrides: []clusterv1.ClusterVariable{
   328  									{
   329  										Name:  "httpProxy",
   330  										Value: apiextensionsv1.JSON{Raw: []byte(`{"enabled":true}`)},
   331  									},
   332  								},
   333  							},
   334  						},
   335  					},
   336  				},
   337  				Variables: []clusterv1.ClusterVariable{
   338  					{
   339  						Name:  "httpProxy",
   340  						Value: apiextensionsv1.JSON{Raw: []byte(`{"enabled":true}`)},
   341  					},
   342  				},
   343  			},
   344  			expect: &clusterv1.Topology{
   345  				Workers: &clusterv1.WorkersTopology{
   346  					MachineDeployments: []clusterv1.MachineDeploymentTopology{
   347  						{
   348  							Class: "default-worker",
   349  							Name:  "md-1",
   350  							Variables: &clusterv1.MachineDeploymentVariables{
   351  								Overrides: []clusterv1.ClusterVariable{
   352  									{
   353  										Name: "httpProxy",
   354  										// url has been added by defaulting.
   355  										Value: apiextensionsv1.JSON{Raw: []byte(`{"enabled":true,"url":"http://localhost:3128"}`)},
   356  									},
   357  								},
   358  							},
   359  						},
   360  					},
   361  					MachinePools: []clusterv1.MachinePoolTopology{
   362  						{
   363  							Class: "default-worker",
   364  							Name:  "md-1",
   365  							Variables: &clusterv1.MachinePoolVariables{
   366  								Overrides: []clusterv1.ClusterVariable{
   367  									{
   368  										Name: "httpProxy",
   369  										// url has been added by defaulting.
   370  										Value: apiextensionsv1.JSON{Raw: []byte(`{"enabled":true,"url":"http://localhost:3128"}`)},
   371  									},
   372  								},
   373  							},
   374  						},
   375  					},
   376  				},
   377  				Variables: []clusterv1.ClusterVariable{
   378  					{
   379  						Name: "httpProxy",
   380  						Value: apiextensionsv1.JSON{
   381  							// url has been added by defaulting.
   382  							Raw: []byte(`{"enabled":true,"url":"http://localhost:3128"}`),
   383  						},
   384  					},
   385  				},
   386  			},
   387  		},
   388  		{
   389  			name: "Use one value for multiple definitions when variables don't conflict",
   390  			clusterClass: builder.ClusterClass(metav1.NamespaceDefault, "class1").
   391  				WithStatusVariables(clusterv1.ClusterClassStatusVariable{
   392  					Name: "location",
   393  					Definitions: []clusterv1.ClusterClassStatusVariableDefinition{
   394  						{
   395  							Required: true,
   396  							From:     clusterv1.VariableDefinitionFromInline,
   397  							Schema: clusterv1.VariableSchema{
   398  								OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   399  									Type:    "string",
   400  									Default: &apiextensionsv1.JSON{Raw: []byte(`"us-east"`)},
   401  								},
   402  							},
   403  						},
   404  						{
   405  							Required: true,
   406  							From:     "somepatch",
   407  							Schema: clusterv1.VariableSchema{
   408  								OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   409  									Type:    "string",
   410  									Default: &apiextensionsv1.JSON{Raw: []byte(`"us-east"`)},
   411  								},
   412  							},
   413  						},
   414  						{
   415  							Required: true,
   416  							From:     "anotherpatch",
   417  							Schema: clusterv1.VariableSchema{
   418  								OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   419  									Type:    "string",
   420  									Default: &apiextensionsv1.JSON{Raw: []byte(`"us-east"`)},
   421  								},
   422  							},
   423  						},
   424  					},
   425  				},
   426  				).
   427  				Build(),
   428  			topology: &clusterv1.Topology{},
   429  			expect: &clusterv1.Topology{
   430  				Variables: []clusterv1.ClusterVariable{
   431  					{
   432  						Name: "location",
   433  						Value: apiextensionsv1.JSON{
   434  							Raw: []byte(`"us-east"`),
   435  						},
   436  					},
   437  				},
   438  			},
   439  		},
   440  		{
   441  			name: "Add defaults for each definitionFrom if variable is defined for some definitionFrom",
   442  			clusterClass: builder.ClusterClass(metav1.NamespaceDefault, "class1").
   443  				WithStatusVariables(clusterv1.ClusterClassStatusVariable{
   444  					Name: "location",
   445  					Definitions: []clusterv1.ClusterClassStatusVariableDefinition{
   446  						{
   447  							Required: true,
   448  							From:     clusterv1.VariableDefinitionFromInline,
   449  							Schema: clusterv1.VariableSchema{
   450  								OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   451  									Type:    "string",
   452  									Default: &apiextensionsv1.JSON{Raw: []byte(`"us-east"`)},
   453  								},
   454  							},
   455  						},
   456  						{
   457  							Required: true,
   458  							From:     "somepatch",
   459  							Schema: clusterv1.VariableSchema{
   460  								OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   461  									Type:    "string",
   462  									Default: &apiextensionsv1.JSON{Raw: []byte(`"us-east"`)},
   463  								},
   464  							},
   465  						},
   466  						{
   467  							Required: true,
   468  							From:     "anotherpatch",
   469  							Schema: clusterv1.VariableSchema{
   470  								OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   471  									Type:    "string",
   472  									Default: &apiextensionsv1.JSON{Raw: []byte(`"us-east"`)},
   473  								},
   474  							},
   475  						},
   476  					},
   477  				},
   478  				).
   479  				Build(),
   480  			topology: &clusterv1.Topology{
   481  				Variables: []clusterv1.ClusterVariable{
   482  					{
   483  						Name: "location",
   484  						Value: apiextensionsv1.JSON{
   485  							Raw: []byte(`"us-west"`),
   486  						},
   487  						DefinitionFrom: "somepatch",
   488  					},
   489  				},
   490  			},
   491  			expect: &clusterv1.Topology{
   492  				Variables: []clusterv1.ClusterVariable{
   493  					{
   494  						Name: "location",
   495  						Value: apiextensionsv1.JSON{
   496  							Raw: []byte(`"us-west"`),
   497  						},
   498  						DefinitionFrom: "somepatch",
   499  					},
   500  					{
   501  						Name: "location",
   502  						Value: apiextensionsv1.JSON{
   503  							Raw: []byte(`"us-east"`),
   504  						},
   505  						DefinitionFrom: clusterv1.VariableDefinitionFromInline,
   506  					},
   507  					{
   508  						Name: "location",
   509  						Value: apiextensionsv1.JSON{
   510  							Raw: []byte(`"us-east"`),
   511  						},
   512  						DefinitionFrom: "anotherpatch",
   513  					},
   514  				},
   515  			},
   516  		},
   517  		{
   518  			name: "set definitionFrom on defaults when variables conflict",
   519  			clusterClass: builder.ClusterClass(metav1.NamespaceDefault, "class1").
   520  				WithStatusVariables(clusterv1.ClusterClassStatusVariable{
   521  					Name:                "location",
   522  					DefinitionsConflict: true,
   523  					Definitions: []clusterv1.ClusterClassStatusVariableDefinition{
   524  						{
   525  							Required: true,
   526  							From:     clusterv1.VariableDefinitionFromInline,
   527  							Schema: clusterv1.VariableSchema{
   528  								OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   529  									Type:    "string",
   530  									Default: &apiextensionsv1.JSON{Raw: []byte(`"first-region"`)},
   531  								},
   532  							},
   533  						},
   534  						{
   535  							Required: true,
   536  							From:     "somepatch",
   537  							Schema: clusterv1.VariableSchema{
   538  								OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   539  									Type:    "string",
   540  									Default: &apiextensionsv1.JSON{Raw: []byte(`"another-region"`)},
   541  								},
   542  							},
   543  						},
   544  						{
   545  							Required: true,
   546  							From:     "anotherpatch",
   547  							Schema: clusterv1.VariableSchema{
   548  								OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   549  									Type:    "string",
   550  									Default: &apiextensionsv1.JSON{Raw: []byte(`"us-east"`)},
   551  								},
   552  							},
   553  						},
   554  					},
   555  				},
   556  				).
   557  				Build(),
   558  			topology: &clusterv1.Topology{},
   559  			expect: &clusterv1.Topology{
   560  				Variables: []clusterv1.ClusterVariable{
   561  					{
   562  						Name: "location",
   563  						Value: apiextensionsv1.JSON{
   564  							Raw: []byte(`"first-region"`),
   565  						},
   566  						DefinitionFrom: clusterv1.VariableDefinitionFromInline,
   567  					},
   568  					{
   569  						Name: "location",
   570  						Value: apiextensionsv1.JSON{
   571  							Raw: []byte(`"another-region"`),
   572  						},
   573  						DefinitionFrom: "somepatch",
   574  					},
   575  					{
   576  						Name: "location",
   577  						Value: apiextensionsv1.JSON{
   578  							Raw: []byte(`"us-east"`),
   579  						},
   580  						DefinitionFrom: "anotherpatch",
   581  					},
   582  				},
   583  			},
   584  		},
   585  		// Testing validation of variables.
   586  		{
   587  			name: "should fail when required variable is missing top-level",
   588  			clusterClass: builder.ClusterClass(metav1.NamespaceDefault, "class1").
   589  				WithStatusVariables(clusterv1.ClusterClassStatusVariable{
   590  					Name: "cpu",
   591  					Definitions: []clusterv1.ClusterClassStatusVariableDefinition{
   592  						{
   593  							Required: true,
   594  							From:     clusterv1.VariableDefinitionFromInline,
   595  							Schema: clusterv1.VariableSchema{
   596  								OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   597  									Type: "integer",
   598  								},
   599  							},
   600  						},
   601  					},
   602  				}).Build(),
   603  			topology: builder.ClusterTopology().Build(),
   604  			expect:   builder.ClusterTopology().Build(),
   605  			wantErr:  true,
   606  		},
   607  		{
   608  			name: "should fail when top-level variable is invalid",
   609  			clusterClass: builder.ClusterClass(metav1.NamespaceDefault, "class1").
   610  				WithStatusVariables(clusterv1.ClusterClassStatusVariable{
   611  					Name: "cpu",
   612  					Definitions: []clusterv1.ClusterClassStatusVariableDefinition{
   613  						{
   614  							Required: true,
   615  							From:     clusterv1.VariableDefinitionFromInline,
   616  							Schema: clusterv1.VariableSchema{
   617  								OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   618  									Type: "integer",
   619  								},
   620  							},
   621  						},
   622  					}},
   623  				).Build(),
   624  			topology: builder.ClusterTopology().
   625  				WithVariables(clusterv1.ClusterVariable{
   626  					Name:  "cpu",
   627  					Value: apiextensionsv1.JSON{Raw: []byte(`"text"`)},
   628  				}).
   629  				Build(),
   630  			expect:  builder.ClusterTopology().Build(),
   631  			wantErr: true,
   632  		},
   633  		{
   634  			name: "should fail when variable override is invalid",
   635  			clusterClass: builder.ClusterClass(metav1.NamespaceDefault, "class1").
   636  				WithStatusVariables(clusterv1.ClusterClassStatusVariable{
   637  					Name: "cpu",
   638  					Definitions: []clusterv1.ClusterClassStatusVariableDefinition{
   639  						{
   640  							Required: true,
   641  							From:     clusterv1.VariableDefinitionFromInline,
   642  							Schema: clusterv1.VariableSchema{
   643  								OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   644  									Type: "integer",
   645  								},
   646  							},
   647  						},
   648  					}}).Build(),
   649  			topology: builder.ClusterTopology().
   650  				WithVariables(clusterv1.ClusterVariable{
   651  					Name:  "cpu",
   652  					Value: apiextensionsv1.JSON{Raw: []byte(`2`)},
   653  				}).
   654  				WithMachineDeployment(builder.MachineDeploymentTopology("workers1").
   655  					WithClass("aa").
   656  					WithVariables(clusterv1.ClusterVariable{
   657  						Name:  "cpu",
   658  						Value: apiextensionsv1.JSON{Raw: []byte(`"text"`)},
   659  					}).
   660  					Build()).
   661  				WithMachinePool(builder.MachinePoolTopology("workers1").
   662  					WithClass("aa").
   663  					WithVariables(clusterv1.ClusterVariable{
   664  						Name:  "cpu",
   665  						Value: apiextensionsv1.JSON{Raw: []byte(`"text"`)},
   666  					}).
   667  					Build()).
   668  				Build(),
   669  			expect:  builder.ClusterTopology().Build(),
   670  			wantErr: true,
   671  		},
   672  		{
   673  			name: "should pass when required variable exists top-level",
   674  			clusterClass: builder.ClusterClass(metav1.NamespaceDefault, "class1").
   675  				WithStatusVariables(clusterv1.ClusterClassStatusVariable{
   676  					Name: "cpu",
   677  					Definitions: []clusterv1.ClusterClassStatusVariableDefinition{
   678  						{
   679  							Required: true,
   680  							From:     clusterv1.VariableDefinitionFromInline,
   681  							Schema: clusterv1.VariableSchema{
   682  								OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   683  									Type: "integer",
   684  								},
   685  							},
   686  						},
   687  					}}).Build(),
   688  			topology: builder.ClusterTopology().
   689  				WithClass("foo").
   690  				WithVersion("v1.19.1").
   691  				WithVariables(clusterv1.ClusterVariable{
   692  					Name:  "cpu",
   693  					Value: apiextensionsv1.JSON{Raw: []byte(`2`)},
   694  				}).
   695  				// Variable is not required in MachineDeployment or MachinePool topologies.
   696  				Build(),
   697  			expect: builder.ClusterTopology().
   698  				WithClass("foo").
   699  				WithVersion("v1.19.1").
   700  				WithVariables(clusterv1.ClusterVariable{
   701  					Name:  "cpu",
   702  					Value: apiextensionsv1.JSON{Raw: []byte(`2`)},
   703  				}).
   704  				// Variable is not required in MachineDeployment or MachinePool topologies.
   705  				Build(),
   706  		},
   707  		{
   708  			name: "should pass when top-level variable and override are valid",
   709  			clusterClass: builder.ClusterClass(metav1.NamespaceDefault, "class1").
   710  				WithWorkerMachineDeploymentClasses(*builder.MachineDeploymentClass("md1").Build()).
   711  				WithWorkerMachinePoolClasses(*builder.MachinePoolClass("mp1").Build()).
   712  				WithStatusVariables(clusterv1.ClusterClassStatusVariable{
   713  					Name: "cpu",
   714  					Definitions: []clusterv1.ClusterClassStatusVariableDefinition{
   715  						{
   716  							Required: true,
   717  							From:     clusterv1.VariableDefinitionFromInline,
   718  							Schema: clusterv1.VariableSchema{
   719  								OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   720  									Type: "integer",
   721  								},
   722  							},
   723  						},
   724  					}}).Build(),
   725  			topology: builder.ClusterTopology().
   726  				WithClass("foo").
   727  				WithVersion("v1.19.1").
   728  				WithVariables(clusterv1.ClusterVariable{
   729  					Name:  "cpu",
   730  					Value: apiextensionsv1.JSON{Raw: []byte(`2`)},
   731  				}).
   732  				WithMachineDeployment(builder.MachineDeploymentTopology("workers1").
   733  					WithClass("md1").
   734  					WithVariables(clusterv1.ClusterVariable{
   735  						Name:  "cpu",
   736  						Value: apiextensionsv1.JSON{Raw: []byte(`2`)},
   737  					}).
   738  					Build()).
   739  				WithMachinePool(builder.MachinePoolTopology("workers1").
   740  					WithClass("mp1").
   741  					WithVariables(clusterv1.ClusterVariable{
   742  						Name:  "cpu",
   743  						Value: apiextensionsv1.JSON{Raw: []byte(`2`)},
   744  					}).
   745  					Build()).
   746  				Build(),
   747  			expect: builder.ClusterTopology().
   748  				WithClass("foo").
   749  				WithVersion("v1.19.1").
   750  				WithVariables(clusterv1.ClusterVariable{
   751  					Name:  "cpu",
   752  					Value: apiextensionsv1.JSON{Raw: []byte(`2`)},
   753  				}).
   754  				WithMachineDeployment(builder.MachineDeploymentTopology("workers1").
   755  					WithClass("md1").
   756  					WithVariables(clusterv1.ClusterVariable{
   757  						Name:  "cpu",
   758  						Value: apiextensionsv1.JSON{Raw: []byte(`2`)},
   759  					}).
   760  					Build()).
   761  				WithMachinePool(builder.MachinePoolTopology("workers1").
   762  					WithClass("mp1").
   763  					WithVariables(clusterv1.ClusterVariable{
   764  						Name:  "cpu",
   765  						Value: apiextensionsv1.JSON{Raw: []byte(`2`)},
   766  					}).
   767  					Build()).
   768  				Build(),
   769  		},
   770  		{
   771  			name: "should pass even when variable override is missing the corresponding top-level variable",
   772  			clusterClass: builder.ClusterClass(metav1.NamespaceDefault, "class1").
   773  				WithWorkerMachineDeploymentClasses(*builder.MachineDeploymentClass("md1").Build()).
   774  				WithWorkerMachinePoolClasses(*builder.MachinePoolClass("mp1").Build()).
   775  				WithStatusVariables(clusterv1.ClusterClassStatusVariable{
   776  					Name: "cpu",
   777  					Definitions: []clusterv1.ClusterClassStatusVariableDefinition{
   778  						{
   779  							Required: false,
   780  							From:     clusterv1.VariableDefinitionFromInline,
   781  							Schema: clusterv1.VariableSchema{
   782  								OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   783  									Type: "integer",
   784  								},
   785  							},
   786  						},
   787  					}}).Build(),
   788  			topology: builder.ClusterTopology().
   789  				WithClass("foo").
   790  				WithVersion("v1.19.1").
   791  				WithMachineDeployment(builder.MachineDeploymentTopology("workers1").
   792  					WithClass("md1").
   793  					WithVariables(clusterv1.ClusterVariable{
   794  						Name:  "cpu",
   795  						Value: apiextensionsv1.JSON{Raw: []byte(`2`)},
   796  					}).
   797  					Build()).
   798  				WithMachinePool(builder.MachinePoolTopology("workers1").
   799  					WithClass("mp1").
   800  					WithVariables(clusterv1.ClusterVariable{
   801  						Name:  "cpu",
   802  						Value: apiextensionsv1.JSON{Raw: []byte(`2`)},
   803  					}).
   804  					Build()).
   805  				Build(),
   806  			expect: builder.ClusterTopology().
   807  				WithClass("foo").
   808  				WithVersion("v1.19.1").
   809  				WithVariables([]clusterv1.ClusterVariable{}...).
   810  				WithMachineDeployment(builder.MachineDeploymentTopology("workers1").
   811  					WithClass("md1").
   812  					WithVariables(clusterv1.ClusterVariable{
   813  						Name:  "cpu",
   814  						Value: apiextensionsv1.JSON{Raw: []byte(`2`)},
   815  					}).
   816  					Build()).
   817  				WithMachinePool(builder.MachinePoolTopology("workers1").
   818  					WithClass("mp1").
   819  					WithVariables(clusterv1.ClusterVariable{
   820  						Name:  "cpu",
   821  						Value: apiextensionsv1.JSON{Raw: []byte(`2`)},
   822  					}).
   823  					Build()).
   824  				Build(),
   825  		},
   826  	}
   827  	for _, tt := range tests {
   828  		t.Run(tt.name, func(t *testing.T) {
   829  			// Setting Class and Version here to avoid obfuscating the test cases above.
   830  			tt.topology.Class = "class1"
   831  			tt.topology.Version = "v1.22.2"
   832  			tt.expect.Class = "class1"
   833  			tt.expect.Version = "v1.22.2"
   834  
   835  			cluster := builder.Cluster(metav1.NamespaceDefault, "cluster1").
   836  				WithTopology(tt.topology).
   837  				Build()
   838  
   839  			// Mark this condition to true so the webhook sees the ClusterClass as up to date.
   840  			conditions.MarkTrue(tt.clusterClass, clusterv1.ClusterClassVariablesReconciledCondition)
   841  			fakeClient := fake.NewClientBuilder().
   842  				WithObjects(tt.clusterClass).
   843  				WithScheme(fakeScheme).
   844  				Build()
   845  			// Create the webhook and add the fakeClient as its client. This is required because the test uses a Managed Topology.
   846  			webhook := &Cluster{Client: fakeClient}
   847  
   848  			// Test defaulting.
   849  			t.Run("default", func(t *testing.T) {
   850  				g := NewWithT(t)
   851  				if tt.wantErr {
   852  					g.Expect(webhook.Default(ctx, cluster)).To(Not(Succeed()))
   853  					return
   854  				}
   855  				g.Expect(webhook.Default(ctx, cluster)).To(Succeed())
   856  				g.Expect(cluster.Spec.Topology).To(BeEquivalentTo(tt.expect))
   857  			})
   858  
   859  			// Test if defaulting works in combination with validation.
   860  			// Note this test is not run for the case where the webhook should fail.
   861  			if tt.wantErr {
   862  				t.Skip("skipping test for combination of defaulting and validation (not supported by the test)")
   863  			}
   864  			util.CustomDefaultValidateTest(ctx, cluster, webhook)(t)
   865  		})
   866  	}
   867  }
   868  
   869  func TestClusterDefaultTopologyVersion(t *testing.T) {
   870  	// NOTE: ClusterTopology feature flag is disabled by default, thus preventing to set Cluster.Topologies.
   871  	// Enabling the feature flag temporarily for this test.
   872  	defer utilfeature.SetFeatureGateDuringTest(t, feature.Gates, feature.ClusterTopology, true)()
   873  
   874  	g := NewWithT(t)
   875  
   876  	c := builder.Cluster("fooboo", "cluster1").
   877  		WithTopology(builder.ClusterTopology().
   878  			WithClass("foo").
   879  			WithVersion("1.19.1").
   880  			Build()).
   881  		Build()
   882  
   883  	clusterClass := builder.ClusterClass("fooboo", "foo").Build()
   884  	conditions.MarkTrue(clusterClass, clusterv1.ClusterClassVariablesReconciledCondition)
   885  	// Sets up the fakeClient for the test case. This is required because the test uses a Managed Topology.
   886  	fakeClient := fake.NewClientBuilder().
   887  		WithObjects(clusterClass).
   888  		WithScheme(fakeScheme).
   889  		Build()
   890  
   891  	// Create the webhook and add the fakeClient as its client.
   892  	webhook := &Cluster{Client: fakeClient}
   893  	t.Run("for Cluster", util.CustomDefaultValidateTest(ctx, c, webhook))
   894  
   895  	g.Expect(webhook.Default(ctx, c)).To(Succeed())
   896  
   897  	g.Expect(c.Spec.Topology.Version).To(HavePrefix("v"))
   898  }
   899  
   900  func TestClusterValidation(t *testing.T) {
   901  	// NOTE: ClusterTopology feature flag is disabled by default, thus preventing to set Cluster.Topologies.
   902  
   903  	var (
   904  		tests = []struct {
   905  			name      string
   906  			in        *clusterv1.Cluster
   907  			old       *clusterv1.Cluster
   908  			expectErr bool
   909  		}{
   910  			{
   911  				name:      "should return error when cluster namespace and infrastructure ref namespace mismatch",
   912  				expectErr: true,
   913  				in: builder.Cluster("fooNamespace", "cluster1").
   914  					WithInfrastructureCluster(
   915  						builder.InfrastructureClusterTemplate("barNamespace", "infra1").Build()).
   916  					WithControlPlane(
   917  						builder.ControlPlane("fooNamespace", "cp1").Build()).
   918  					Build(),
   919  			},
   920  			{
   921  				name:      "should return error when cluster namespace and controlPlane ref namespace mismatch",
   922  				expectErr: true,
   923  				in: builder.Cluster("fooNamespace", "cluster1").
   924  					WithInfrastructureCluster(
   925  						builder.InfrastructureClusterTemplate("fooNamespace", "infra1").Build()).
   926  					WithControlPlane(
   927  						builder.ControlPlane("barNamespace", "cp1").Build()).
   928  					Build(),
   929  			},
   930  			{
   931  				name:      "should succeed when namespaces match",
   932  				expectErr: false,
   933  				in: builder.Cluster("fooNamespace", "cluster1").
   934  					WithInfrastructureCluster(
   935  						builder.InfrastructureClusterTemplate("fooNamespace", "infra1").Build()).
   936  					WithControlPlane(
   937  						builder.ControlPlane("fooNamespace", "cp1").Build()).
   938  					Build(),
   939  			},
   940  			{
   941  				name:      "fails if topology is set but feature flag is disabled",
   942  				expectErr: true,
   943  				in: builder.Cluster("fooNamespace", "cluster1").
   944  					WithInfrastructureCluster(
   945  						builder.InfrastructureClusterTemplate("fooNamespace", "infra1").Build()).
   946  					WithControlPlane(
   947  						builder.ControlPlane("fooNamespace", "cp1").Build()).
   948  					WithTopology(&clusterv1.Topology{}).
   949  					Build(),
   950  			},
   951  			{
   952  				name:      "pass with undefined CIDR ranges",
   953  				expectErr: false,
   954  				in: builder.Cluster("fooNamespace", "cluster1").
   955  					WithClusterNetwork(&clusterv1.ClusterNetwork{
   956  						Services: &clusterv1.NetworkRanges{
   957  							CIDRBlocks: []string{}},
   958  						Pods: &clusterv1.NetworkRanges{
   959  							CIDRBlocks: []string{}},
   960  					}).
   961  					Build(),
   962  			},
   963  			{
   964  				name:      "pass with nil CIDR ranges",
   965  				expectErr: false,
   966  				in: builder.Cluster("fooNamespace", "cluster1").
   967  					WithClusterNetwork(&clusterv1.ClusterNetwork{
   968  						Services: &clusterv1.NetworkRanges{
   969  							CIDRBlocks: nil},
   970  						Pods: &clusterv1.NetworkRanges{
   971  							CIDRBlocks: nil},
   972  					}).
   973  					Build(),
   974  			},
   975  			{
   976  				name:      "pass with valid IPv4 CIDR ranges",
   977  				expectErr: false,
   978  				in: builder.Cluster("fooNamespace", "cluster1").
   979  					WithClusterNetwork(&clusterv1.ClusterNetwork{
   980  						Services: &clusterv1.NetworkRanges{
   981  							CIDRBlocks: []string{"10.10.10.10/24"}},
   982  						Pods: &clusterv1.NetworkRanges{
   983  							CIDRBlocks: []string{"10.10.10.10/24"}},
   984  					}).
   985  					Build(),
   986  			},
   987  			{
   988  				name:      "pass with valid IPv6 CIDR ranges",
   989  				expectErr: false,
   990  				in: builder.Cluster("fooNamespace", "cluster1").
   991  					WithClusterNetwork(&clusterv1.ClusterNetwork{
   992  						Services: &clusterv1.NetworkRanges{
   993  							CIDRBlocks: []string{"2004::1234:abcd:ffff:c0a8:101/64"}},
   994  						Pods: &clusterv1.NetworkRanges{
   995  							CIDRBlocks: []string{"2004::1234:abcd:ffff:c0a8:101/64"}},
   996  					}).
   997  					Build(),
   998  			},
   999  			{
  1000  				name:      "pass with valid dualstack CIDR ranges",
  1001  				expectErr: false,
  1002  				in: builder.Cluster("fooNamespace", "cluster1").
  1003  					WithClusterNetwork(&clusterv1.ClusterNetwork{
  1004  						Services: &clusterv1.NetworkRanges{
  1005  							CIDRBlocks: []string{"2004::1234:abcd:ffff:c0a8:101/64", "10.10.10.10/24"}},
  1006  						Pods: &clusterv1.NetworkRanges{
  1007  							CIDRBlocks: []string{"2004::1234:abcd:ffff:c0a8:101/64", "10.10.10.10/24"}},
  1008  					}).
  1009  					Build(),
  1010  			},
  1011  			{
  1012  				name:      "pass if multiple CIDR ranges of IPv4 are passed",
  1013  				expectErr: false,
  1014  				in: builder.Cluster("fooNamespace", "cluster1").
  1015  					WithClusterNetwork(&clusterv1.ClusterNetwork{
  1016  						Services: &clusterv1.NetworkRanges{
  1017  							CIDRBlocks: []string{"10.10.10.10/24", "11.11.11.11/24"}},
  1018  					}).
  1019  					Build(),
  1020  			},
  1021  			{
  1022  				name:      "pass if multiple CIDR ranges of IPv6 are passed",
  1023  				expectErr: false,
  1024  				in: builder.Cluster("fooNamespace", "cluster1").
  1025  					WithClusterNetwork(&clusterv1.ClusterNetwork{
  1026  						Services: &clusterv1.NetworkRanges{
  1027  							CIDRBlocks: []string{"2002::1234:abcd:ffff:c0a8:101/64", "2004::1234:abcd:ffff:c0a8:101/64"}},
  1028  					}).
  1029  					Build(),
  1030  			},
  1031  			{
  1032  				name:      "pass if too many cidr ranges are specified in the clusterNetwork pods field",
  1033  				expectErr: false,
  1034  				in: builder.Cluster("fooNamespace", "cluster1").
  1035  					WithClusterNetwork(&clusterv1.ClusterNetwork{
  1036  						Pods: &clusterv1.NetworkRanges{
  1037  							CIDRBlocks: []string{"10.10.10.10/24", "11.11.11.11/24", "12.12.12.12/24"}}}).
  1038  					Build(),
  1039  			},
  1040  			{
  1041  				name:      "fails if service cidr ranges are not valid",
  1042  				expectErr: true,
  1043  				in: builder.Cluster("fooNamespace", "cluster1").
  1044  					WithClusterNetwork(&clusterv1.ClusterNetwork{
  1045  						Services: &clusterv1.NetworkRanges{
  1046  							// Invalid ranges: missing network suffix
  1047  							CIDRBlocks: []string{"10.10.10.10", "11.11.11.11"}}}).
  1048  					Build(),
  1049  			},
  1050  			{
  1051  				name:      "fails if pod cidr ranges are not valid",
  1052  				expectErr: true,
  1053  				in: builder.Cluster("fooNamespace", "cluster1").
  1054  					WithClusterNetwork(&clusterv1.ClusterNetwork{
  1055  						Pods: &clusterv1.NetworkRanges{
  1056  							// Invalid ranges: missing network suffix
  1057  							CIDRBlocks: []string{"10.10.10.10", "11.11.11.11"}}}).
  1058  					Build(),
  1059  			},
  1060  			{
  1061  				name:      "pass with name of under 63 characters",
  1062  				expectErr: false,
  1063  				in:        builder.Cluster("fooNamespace", "short-name").Build(),
  1064  			},
  1065  			{
  1066  				name:      "pass with _, -, . characters in name",
  1067  				in:        builder.Cluster("fooNamespace", "thisNameContains.A_Non-Alphanumeric").Build(),
  1068  				expectErr: false,
  1069  			},
  1070  			{
  1071  				name:      "fails if cluster name is longer than 63 characters",
  1072  				in:        builder.Cluster("fooNamespace", "thisNameIsReallyMuchLongerThanTheMaximumLengthOfSixtyThreeCharacters").Build(),
  1073  				expectErr: true,
  1074  			},
  1075  			{
  1076  				name:      "error when name starts with NonAlphanumeric character",
  1077  				in:        builder.Cluster("fooNamespace", "-thisNameStartsWithANonAlphanumeric").Build(),
  1078  				expectErr: true,
  1079  			},
  1080  			{
  1081  				name:      "error when name ends with NonAlphanumeric character",
  1082  				in:        builder.Cluster("fooNamespace", "thisNameEndsWithANonAlphanumeric.").Build(),
  1083  				expectErr: true,
  1084  			},
  1085  			{
  1086  				name:      "error when name contains invalid NonAlphanumeric character",
  1087  				in:        builder.Cluster("fooNamespace", "thisNameContainsInvalid!@NonAlphanumerics").Build(),
  1088  				expectErr: true,
  1089  			},
  1090  		}
  1091  	)
  1092  	for _, tt := range tests {
  1093  		t.Run(tt.name, func(t *testing.T) {
  1094  			g := NewWithT(t)
  1095  
  1096  			// Create the webhook.
  1097  			webhook := &Cluster{}
  1098  
  1099  			warnings, err := webhook.validate(ctx, tt.old, tt.in)
  1100  			g.Expect(warnings).To(BeEmpty())
  1101  			if tt.expectErr {
  1102  				g.Expect(err).To(HaveOccurred())
  1103  				return
  1104  			}
  1105  			g.Expect(err).ToNot(HaveOccurred())
  1106  		})
  1107  	}
  1108  }
  1109  
  1110  func TestClusterTopologyValidation(t *testing.T) {
  1111  	// NOTE: ClusterTopology feature flag is disabled by default, thus preventing to set Cluster.Topologies.
  1112  	// Enabling the feature flag temporarily for this test.
  1113  	defer utilfeature.SetFeatureGateDuringTest(t, feature.Gates, feature.ClusterTopology, true)()
  1114  
  1115  	tests := []struct {
  1116  		name                        string
  1117  		clusterClassStatusVariables []clusterv1.ClusterClassStatusVariable
  1118  		in                          *clusterv1.Cluster
  1119  		old                         *clusterv1.Cluster
  1120  		additionalObjects           []client.Object
  1121  		expectErr                   bool
  1122  		expectWarning               bool
  1123  	}{
  1124  		{
  1125  			name:      "should return error when topology does not have class",
  1126  			expectErr: true,
  1127  			in: builder.Cluster("fooboo", "cluster1").
  1128  				WithTopology(&clusterv1.Topology{}).
  1129  				Build(),
  1130  		},
  1131  		{
  1132  			name:      "should return error when topology does not have valid version",
  1133  			expectErr: true,
  1134  			in: builder.Cluster("fooboo", "cluster1").
  1135  				WithTopology(builder.ClusterTopology().
  1136  					WithClass("foo").
  1137  					WithVersion("invalid").Build()).
  1138  				Build(),
  1139  		},
  1140  		{
  1141  			name:      "should return error when downgrading topology version - major",
  1142  			expectErr: true,
  1143  			old: builder.Cluster("fooboo", "cluster1").
  1144  				WithTopology(builder.ClusterTopology().
  1145  					WithClass("foo").
  1146  					WithVersion("v2.2.3").
  1147  					Build()).
  1148  				Build(),
  1149  			in: builder.Cluster("fooboo", "cluster1").
  1150  				WithTopology(builder.ClusterTopology().
  1151  					WithClass("foo").
  1152  					WithVersion("v1.2.3").
  1153  					Build()).
  1154  				Build(),
  1155  		},
  1156  		{
  1157  			name:      "should return error when downgrading topology version - minor",
  1158  			expectErr: true,
  1159  			old: builder.Cluster("fooboo", "cluster1").
  1160  				WithTopology(builder.ClusterTopology().
  1161  					WithClass("foo").
  1162  					WithVersion("v1.2.3").
  1163  					Build()).
  1164  				Build(),
  1165  			in: builder.Cluster("fooboo", "cluster1").
  1166  				WithTopology(builder.ClusterTopology().
  1167  					WithClass("foo").
  1168  					WithVersion("v1.1.3").
  1169  					Build()).
  1170  				Build(),
  1171  		},
  1172  		{
  1173  			name:      "should return error when downgrading topology version - patch",
  1174  			expectErr: true,
  1175  			old: builder.Cluster("fooboo", "cluster1").
  1176  				WithTopology(builder.ClusterTopology().
  1177  					WithClass("foo").
  1178  					WithVersion("v1.2.3").
  1179  					Build()).
  1180  				Build(),
  1181  			in: builder.Cluster("fooboo", "cluster1").
  1182  				WithTopology(builder.ClusterTopology().
  1183  					WithClass("foo").
  1184  					WithVersion("v1.2.2").
  1185  					Build()).
  1186  				Build(),
  1187  		},
  1188  		{
  1189  			name:      "should return error when downgrading topology version - pre-release",
  1190  			expectErr: true,
  1191  			old: builder.Cluster("fooboo", "cluster1").
  1192  				WithTopology(builder.ClusterTopology().
  1193  					WithClass("foo").
  1194  					WithVersion("v1.2.3-xyz.2").
  1195  					Build()).
  1196  				Build(),
  1197  			in: builder.Cluster("fooboo", "cluster1").
  1198  				WithTopology(builder.ClusterTopology().
  1199  					WithClass("foo").
  1200  					WithVersion("v1.2.3-xyz.1").
  1201  					Build()).
  1202  				Build(),
  1203  		},
  1204  		{
  1205  			name:      "should return error when downgrading topology version - build tag",
  1206  			expectErr: true,
  1207  			old: builder.Cluster("fooboo", "cluster1").
  1208  				WithTopology(builder.ClusterTopology().
  1209  					WithClass("foo").
  1210  					WithVersion("v1.2.3+xyz.2").
  1211  					Build()).
  1212  				Build(),
  1213  			in: builder.Cluster("fooboo", "cluster1").
  1214  				WithTopology(builder.ClusterTopology().
  1215  					WithClass("foo").
  1216  					WithVersion("v1.2.3+xyz.1").
  1217  					Build()).
  1218  				Build(),
  1219  		},
  1220  		{
  1221  			name:      "should return error when upgrading +2 minor version",
  1222  			expectErr: true,
  1223  			old: builder.Cluster("fooboo", "cluster1").
  1224  				WithTopology(builder.ClusterTopology().
  1225  					WithClass("foo").
  1226  					WithVersion("v1.2.3").
  1227  					Build()).
  1228  				Build(),
  1229  			in: builder.Cluster("fooboo", "cluster1").
  1230  				WithTopology(builder.ClusterTopology().
  1231  					WithClass("foo").
  1232  					WithVersion("v1.4.0").
  1233  					Build()).
  1234  				Build(),
  1235  		},
  1236  		{
  1237  			name:      "should return error when duplicated MachineDeployments names exists in a Topology",
  1238  			expectErr: true,
  1239  			in: builder.Cluster("fooboo", "cluster1").
  1240  				WithTopology(builder.ClusterTopology().
  1241  					WithClass("foo").
  1242  					WithVersion("v1.19.1").
  1243  					WithMachineDeployment(
  1244  						builder.MachineDeploymentTopology("workers1").
  1245  							WithClass("aa").
  1246  							Build()).
  1247  					WithMachineDeployment(
  1248  						builder.MachineDeploymentTopology("workers1").
  1249  							WithClass("bb").
  1250  							Build()).
  1251  					Build()).
  1252  				Build(),
  1253  		},
  1254  		{
  1255  			name:      "should return error when duplicated MachinePools names exists in a Topology",
  1256  			expectErr: true,
  1257  			in: builder.Cluster("fooboo", "cluster1").
  1258  				WithTopology(builder.ClusterTopology().
  1259  					WithClass("foo").
  1260  					WithVersion("v1.19.1").
  1261  					WithMachinePool(
  1262  						builder.MachinePoolTopology("workers1").
  1263  							WithClass("aa").
  1264  							Build()).
  1265  					WithMachinePool(
  1266  						builder.MachinePoolTopology("workers1").
  1267  							WithClass("bb").
  1268  							Build()).
  1269  					Build()).
  1270  				Build(),
  1271  		},
  1272  		{
  1273  			name:      "should pass when MachineDeployments names in a Topology are unique",
  1274  			expectErr: false,
  1275  			in: builder.Cluster("fooboo", "cluster1").
  1276  				WithTopology(builder.ClusterTopology().
  1277  					WithClass("foo").
  1278  					WithVersion("v1.19.1").
  1279  					WithMachineDeployment(
  1280  						builder.MachineDeploymentTopology("workers1").
  1281  							WithClass("aa").
  1282  							Build()).
  1283  					WithMachineDeployment(
  1284  						builder.MachineDeploymentTopology("workers2").
  1285  							WithClass("bb").
  1286  							Build()).
  1287  					Build()).
  1288  				Build(),
  1289  		},
  1290  		{
  1291  			name:      "should pass when MachinePools names in a Topology are unique",
  1292  			expectErr: false,
  1293  			in: builder.Cluster("fooboo", "cluster1").
  1294  				WithTopology(builder.ClusterTopology().
  1295  					WithClass("foo").
  1296  					WithVersion("v1.19.1").
  1297  					WithMachinePool(
  1298  						builder.MachinePoolTopology("workers1").
  1299  							WithClass("aa").
  1300  							Build()).
  1301  					WithMachinePool(
  1302  						builder.MachinePoolTopology("workers2").
  1303  							WithClass("bb").
  1304  							Build()).
  1305  					Build()).
  1306  				Build(),
  1307  		},
  1308  		{
  1309  			name:      "should update",
  1310  			expectErr: false,
  1311  			old: builder.Cluster("fooboo", "cluster1").
  1312  				WithTopology(builder.ClusterTopology().
  1313  					WithClass("foo").
  1314  					WithVersion("v1.19.1").
  1315  					WithMachineDeployment(
  1316  						builder.MachineDeploymentTopology("workers1").
  1317  							WithClass("aa").
  1318  							Build()).
  1319  					WithMachineDeployment(
  1320  						builder.MachineDeploymentTopology("workers2").
  1321  							WithClass("bb").
  1322  							Build()).
  1323  					WithMachinePool(
  1324  						builder.MachinePoolTopology("workers1").
  1325  							WithClass("aa").
  1326  							Build()).
  1327  					WithMachinePool(
  1328  						builder.MachinePoolTopology("workers2").
  1329  							WithClass("bb").
  1330  							Build()).
  1331  					Build()).
  1332  				Build(),
  1333  			in: builder.Cluster("fooboo", "cluster1").
  1334  				WithTopology(builder.ClusterTopology().
  1335  					WithClass("foo").
  1336  					WithVersion("v1.19.2").
  1337  					WithMachineDeployment(
  1338  						builder.MachineDeploymentTopology("workers1").
  1339  							WithClass("aa").
  1340  							Build()).
  1341  					WithMachineDeployment(
  1342  						builder.MachineDeploymentTopology("workers2").
  1343  							WithClass("bb").
  1344  							Build()).
  1345  					WithMachinePool(
  1346  						builder.MachinePoolTopology("workers1").
  1347  							WithClass("aa").
  1348  							Build()).
  1349  					WithMachinePool(
  1350  						builder.MachinePoolTopology("workers2").
  1351  							WithClass("bb").
  1352  							Build()).
  1353  					Build()).
  1354  				Build(),
  1355  		},
  1356  		{
  1357  			name:      "should return error when upgrade concurrency annotation value is < 1",
  1358  			expectErr: true,
  1359  			in: builder.Cluster("fooboo", "cluster1").
  1360  				WithAnnotations(map[string]string{
  1361  					clusterv1.ClusterTopologyUpgradeConcurrencyAnnotation: "-1",
  1362  				}).
  1363  				WithTopology(builder.ClusterTopology().
  1364  					WithClass("foo").
  1365  					WithVersion("v1.19.2").
  1366  					Build()).
  1367  				Build(),
  1368  		},
  1369  		{
  1370  			name:      "should return error when upgrade concurrency annotation value is not numeric",
  1371  			expectErr: true,
  1372  			in: builder.Cluster("fooboo", "cluster1").
  1373  				WithAnnotations(map[string]string{
  1374  					clusterv1.ClusterTopologyUpgradeConcurrencyAnnotation: "abc",
  1375  				}).
  1376  				WithTopology(builder.ClusterTopology().
  1377  					WithClass("foo").
  1378  					WithVersion("v1.19.2").
  1379  					Build()).
  1380  				Build(),
  1381  		},
  1382  		{
  1383  			name:      "should pass upgrade concurrency annotation value is >= 1",
  1384  			expectErr: false,
  1385  			in: builder.Cluster("fooboo", "cluster1").
  1386  				WithAnnotations(map[string]string{
  1387  					clusterv1.ClusterTopologyUpgradeConcurrencyAnnotation: "2",
  1388  				}).
  1389  				WithTopology(builder.ClusterTopology().
  1390  					WithClass("foo").
  1391  					WithVersion("v1.19.2").
  1392  					Build()).
  1393  				Build(),
  1394  		},
  1395  		{
  1396  			name:      "should update if cluster is fully upgraded and up to date",
  1397  			expectErr: false,
  1398  			old: builder.Cluster("fooboo", "cluster1").
  1399  				WithControlPlane(builder.ControlPlane("fooboo", "cluster1-cp").Build()).
  1400  				WithTopology(builder.ClusterTopology().
  1401  					WithClass("foo").
  1402  					WithVersion("v1.19.1").
  1403  					WithMachineDeployment(
  1404  						builder.MachineDeploymentTopology("workers1").
  1405  							WithClass("aa").
  1406  							Build()).
  1407  					WithMachinePool(
  1408  						builder.MachinePoolTopology("pool1").
  1409  							WithClass("aa").
  1410  							Build()).
  1411  					Build()).
  1412  				Build(),
  1413  			in: builder.Cluster("fooboo", "cluster1").
  1414  				WithControlPlane(builder.ControlPlane("fooboo", "cluster1-cp").Build()).
  1415  				WithTopology(builder.ClusterTopology().
  1416  					WithClass("foo").
  1417  					WithVersion("v1.20.2").
  1418  					Build()).
  1419  				Build(),
  1420  			additionalObjects: []client.Object{
  1421  				builder.ControlPlane("fooboo", "cluster1-cp").WithVersion("v1.19.1").
  1422  					WithStatusFields(map[string]interface{}{"status.version": "v1.19.1"}).
  1423  					Build(),
  1424  				builder.MachineDeployment("fooboo", "cluster1-workers1").WithLabels(map[string]string{
  1425  					clusterv1.ClusterNameLabel:                          "cluster1",
  1426  					clusterv1.ClusterTopologyOwnedLabel:                 "",
  1427  					clusterv1.ClusterTopologyMachineDeploymentNameLabel: "workers1",
  1428  				}).WithVersion("v1.19.1").Build(),
  1429  				builder.MachinePool("fooboo", "cluster1-pool1").WithLabels(map[string]string{
  1430  					clusterv1.ClusterNameLabel:                    "cluster1",
  1431  					clusterv1.ClusterTopologyOwnedLabel:           "",
  1432  					clusterv1.ClusterTopologyMachinePoolNameLabel: "pool1",
  1433  				}).WithVersion("v1.19.1").Build(),
  1434  			},
  1435  		},
  1436  		{
  1437  			name:          "should skip validation if cluster kcp is not yet provisioned but annotation is set",
  1438  			expectErr:     false,
  1439  			expectWarning: true,
  1440  			old: builder.Cluster("fooboo", "cluster1").
  1441  				WithControlPlane(builder.ControlPlane("fooboo", "cluster1-cp").Build()).
  1442  				WithTopology(builder.ClusterTopology().
  1443  					WithClass("foo").
  1444  					WithVersion("v1.19.1").
  1445  					Build()).
  1446  				Build(),
  1447  			in: builder.Cluster("fooboo", "cluster1").
  1448  				WithControlPlane(builder.ControlPlane("fooboo", "cluster1-cp").Build()).
  1449  				WithAnnotations(map[string]string{clusterv1.ClusterTopologyUnsafeUpdateVersionAnnotation: "true"}).
  1450  				WithTopology(builder.ClusterTopology().
  1451  					WithClass("foo").
  1452  					WithVersion("v1.20.2").
  1453  					Build()).
  1454  				Build(),
  1455  			additionalObjects: []client.Object{
  1456  				builder.ControlPlane("fooboo", "cluster1-cp").WithVersion("v1.18.1").Build(),
  1457  			},
  1458  		},
  1459  		{
  1460  			name:      "should block update if cluster kcp is not yet provisioned",
  1461  			expectErr: true,
  1462  			old: builder.Cluster("fooboo", "cluster1").
  1463  				WithControlPlane(builder.ControlPlane("fooboo", "cluster1-cp").Build()).
  1464  				WithTopology(builder.ClusterTopology().
  1465  					WithClass("foo").
  1466  					WithVersion("v1.19.1").
  1467  					Build()).
  1468  				Build(),
  1469  			in: builder.Cluster("fooboo", "cluster1").
  1470  				WithControlPlane(builder.ControlPlane("fooboo", "cluster1-cp").Build()).
  1471  				WithTopology(builder.ClusterTopology().
  1472  					WithClass("foo").
  1473  					WithVersion("v1.20.2").
  1474  					Build()).
  1475  				Build(),
  1476  			additionalObjects: []client.Object{
  1477  				builder.ControlPlane("fooboo", "cluster1-cp").WithVersion("v1.18.1").Build(),
  1478  			},
  1479  		},
  1480  		{
  1481  			name:      "should block update if md is not yet upgraded",
  1482  			expectErr: true,
  1483  			old: builder.Cluster("fooboo", "cluster1").
  1484  				WithControlPlane(builder.ControlPlane("fooboo", "cluster1-cp").Build()).
  1485  				WithTopology(builder.ClusterTopology().
  1486  					WithClass("foo").
  1487  					WithVersion("v1.19.1").
  1488  					WithMachineDeployment(
  1489  						builder.MachineDeploymentTopology("workers1").
  1490  							WithClass("aa").
  1491  							Build()).
  1492  					Build()).
  1493  				Build(),
  1494  			in: builder.Cluster("fooboo", "cluster1").
  1495  				WithControlPlane(builder.ControlPlane("fooboo", "cluster1-cp").Build()).
  1496  				WithTopology(builder.ClusterTopology().
  1497  					WithClass("foo").
  1498  					WithVersion("v1.20.2").
  1499  					Build()).
  1500  				Build(),
  1501  			additionalObjects: []client.Object{
  1502  				builder.ControlPlane("fooboo", "cluster1-cp").WithVersion("v1.19.1").
  1503  					WithStatusFields(map[string]interface{}{"status.version": "v1.19.1"}).
  1504  					Build(),
  1505  				builder.MachineDeployment("fooboo", "cluster1-workers1").WithLabels(map[string]string{
  1506  					clusterv1.ClusterNameLabel:                          "cluster1",
  1507  					clusterv1.ClusterTopologyOwnedLabel:                 "",
  1508  					clusterv1.ClusterTopologyMachineDeploymentNameLabel: "workers1",
  1509  				}).WithVersion("v1.18.1").Build(),
  1510  			},
  1511  		},
  1512  		{
  1513  			name:      "should block update if mp is not yet upgraded",
  1514  			expectErr: true,
  1515  			old: builder.Cluster("fooboo", "cluster1").
  1516  				WithControlPlane(builder.ControlPlane("fooboo", "cluster1-cp").Build()).
  1517  				WithTopology(builder.ClusterTopology().
  1518  					WithClass("foo").
  1519  					WithVersion("v1.19.1").
  1520  					WithMachinePool(
  1521  						builder.MachinePoolTopology("pool1").
  1522  							WithClass("aa").
  1523  							Build()).
  1524  					Build()).
  1525  				Build(),
  1526  			in: builder.Cluster("fooboo", "cluster1").
  1527  				WithControlPlane(builder.ControlPlane("fooboo", "cluster1-cp").Build()).
  1528  				WithTopology(builder.ClusterTopology().
  1529  					WithClass("foo").
  1530  					WithVersion("v1.20.2").
  1531  					Build()).
  1532  				Build(),
  1533  			additionalObjects: []client.Object{
  1534  				builder.ControlPlane("fooboo", "cluster1-cp").WithVersion("v1.19.1").
  1535  					WithStatusFields(map[string]interface{}{"status.version": "v1.19.1"}).
  1536  					Build(),
  1537  				builder.MachinePool("fooboo", "cluster1-pool1").WithLabels(map[string]string{
  1538  					clusterv1.ClusterNameLabel:                    "cluster1",
  1539  					clusterv1.ClusterTopologyOwnedLabel:           "",
  1540  					clusterv1.ClusterTopologyMachinePoolNameLabel: "pool1",
  1541  				}).WithVersion("v1.18.1").Build(),
  1542  			},
  1543  		},
  1544  	}
  1545  	for _, tt := range tests {
  1546  		t.Run(tt.name, func(t *testing.T) {
  1547  			g := NewWithT(t)
  1548  			class := builder.ClusterClass("fooboo", "foo").
  1549  				WithWorkerMachineDeploymentClasses(
  1550  					*builder.MachineDeploymentClass("bb").Build(),
  1551  					*builder.MachineDeploymentClass("aa").Build(),
  1552  				).
  1553  				WithWorkerMachinePoolClasses(
  1554  					*builder.MachinePoolClass("bb").Build(),
  1555  					*builder.MachinePoolClass("aa").Build(),
  1556  				).
  1557  				WithStatusVariables(tt.clusterClassStatusVariables...).
  1558  				Build()
  1559  
  1560  			// Mark this condition to true so the webhook sees the ClusterClass as up to date.
  1561  			conditions.MarkTrue(class, clusterv1.ClusterClassVariablesReconciledCondition)
  1562  			// Sets up the fakeClient for the test case.
  1563  			fakeClient := fake.NewClientBuilder().
  1564  				WithObjects(class).
  1565  				WithObjects(tt.additionalObjects...).
  1566  				WithScheme(fakeScheme).
  1567  				Build()
  1568  
  1569  			// Use an empty fakeClusterCacheTracker here because the real cases are tested in Test_validateTopologyMachinePoolVersions.
  1570  			fakeClusterCacheTrackerReader := &fakeClusterCacheTracker{client: fake.NewFakeClient()}
  1571  
  1572  			// Create the webhook and add the fakeClient as its client. This is required because the test uses a Managed Topology.
  1573  			webhook := &Cluster{Client: fakeClient, Tracker: fakeClusterCacheTrackerReader}
  1574  
  1575  			warnings, err := webhook.validate(ctx, tt.old, tt.in)
  1576  			if tt.expectErr {
  1577  				g.Expect(err).To(HaveOccurred())
  1578  			} else {
  1579  				g.Expect(err).ToNot(HaveOccurred())
  1580  			}
  1581  			if tt.expectWarning {
  1582  				g.Expect(warnings).ToNot(BeEmpty())
  1583  			} else {
  1584  				g.Expect(warnings).To(BeEmpty())
  1585  			}
  1586  		})
  1587  	}
  1588  }
  1589  
  1590  // TestClusterTopologyValidationWithClient tests the additional cases introduced in new validation in the webhook package.
  1591  func TestClusterTopologyValidationWithClient(t *testing.T) {
  1592  	defer utilfeature.SetFeatureGateDuringTest(t, feature.Gates, feature.ClusterTopology, true)()
  1593  	g := NewWithT(t)
  1594  
  1595  	tests := []struct {
  1596  		name            string
  1597  		cluster         *clusterv1.Cluster
  1598  		class           *clusterv1.ClusterClass
  1599  		classReconciled bool
  1600  		objects         []client.Object
  1601  		wantErr         bool
  1602  		wantWarnings    bool
  1603  	}{
  1604  		{
  1605  			name: "Accept a cluster with an existing ClusterClass named in cluster.spec.topology.class",
  1606  			cluster: builder.Cluster(metav1.NamespaceDefault, "cluster1").
  1607  				WithTopology(
  1608  					builder.ClusterTopology().
  1609  						WithClass("clusterclass").
  1610  						WithVersion("v1.22.2").
  1611  						WithControlPlaneReplicas(3).
  1612  						Build()).
  1613  				Build(),
  1614  			class: builder.ClusterClass(metav1.NamespaceDefault, "clusterclass").
  1615  				Build(),
  1616  			classReconciled: true,
  1617  			wantErr:         false,
  1618  		},
  1619  		{
  1620  			name: "Warning for a cluster with non-existent ClusterClass referenced cluster.spec.topology.class",
  1621  			cluster: builder.Cluster(metav1.NamespaceDefault, "cluster1").
  1622  				WithTopology(
  1623  					builder.ClusterTopology().
  1624  						WithClass("wrongName").
  1625  						WithVersion("v1.22.2").
  1626  						WithControlPlaneReplicas(3).
  1627  						Build()).
  1628  				Build(),
  1629  			class: builder.ClusterClass(metav1.NamespaceDefault, "clusterclass").
  1630  				Build(),
  1631  			// There should be a warning for a ClusterClass which can not be found.
  1632  			wantWarnings: true,
  1633  			wantErr:      false,
  1634  		},
  1635  		{
  1636  			name: "Warning for a cluster with an unreconciled ClusterClass named in cluster.spec.topology.class",
  1637  			cluster: builder.Cluster(metav1.NamespaceDefault, "cluster1").
  1638  				WithTopology(
  1639  					builder.ClusterTopology().
  1640  						WithClass("clusterclass").
  1641  						WithVersion("v1.22.2").
  1642  						WithControlPlaneReplicas(3).
  1643  						Build()).
  1644  				Build(),
  1645  			class: builder.ClusterClass(metav1.NamespaceDefault, "clusterclass").
  1646  				Build(),
  1647  			classReconciled: false,
  1648  			// There should be a warning for a ClusterClass which is not yet reconciled.
  1649  			wantWarnings: true,
  1650  			wantErr:      false,
  1651  		},
  1652  		{
  1653  			name: "Reject a cluster that has MHC enabled for control plane but is missing MHC definition in cluster topology and clusterclass",
  1654  			cluster: builder.Cluster(metav1.NamespaceDefault, "cluster1").
  1655  				WithTopology(
  1656  					builder.ClusterTopology().
  1657  						WithClass("clusterclass").
  1658  						WithVersion("v1.22.2").
  1659  						WithControlPlaneReplicas(3).
  1660  						WithControlPlaneMachineHealthCheck(&clusterv1.MachineHealthCheckTopology{
  1661  							Enable: ptr.To(true),
  1662  						}).
  1663  						Build()).
  1664  				Build(),
  1665  			class: builder.ClusterClass(metav1.NamespaceDefault, "clusterclass").
  1666  				Build(),
  1667  			classReconciled: true,
  1668  			wantErr:         true,
  1669  		},
  1670  		{
  1671  			name: "Reject a cluster that MHC override defined for control plane but is missing unhealthy conditions",
  1672  			cluster: builder.Cluster(metav1.NamespaceDefault, "cluster1").
  1673  				WithTopology(
  1674  					builder.ClusterTopology().
  1675  						WithClass("clusterclass").
  1676  						WithVersion("v1.22.2").
  1677  						WithControlPlaneReplicas(3).
  1678  						WithControlPlaneMachineHealthCheck(&clusterv1.MachineHealthCheckTopology{
  1679  							MachineHealthCheckClass: clusterv1.MachineHealthCheckClass{
  1680  								UnhealthyConditions: []clusterv1.UnhealthyCondition{},
  1681  							},
  1682  						}).
  1683  						Build()).
  1684  				Build(),
  1685  			class: builder.ClusterClass(metav1.NamespaceDefault, "clusterclass").
  1686  				Build(),
  1687  			classReconciled: true,
  1688  			wantErr:         true,
  1689  		},
  1690  		{
  1691  			name: "Reject a cluster that MHC override defined for control plane but is set when control plane is missing machineInfrastructure",
  1692  			cluster: builder.Cluster(metav1.NamespaceDefault, "cluster1").
  1693  				WithTopology(
  1694  					builder.ClusterTopology().
  1695  						WithClass("clusterclass").
  1696  						WithVersion("v1.22.2").
  1697  						WithControlPlaneReplicas(3).
  1698  						WithControlPlaneMachineHealthCheck(&clusterv1.MachineHealthCheckTopology{
  1699  							MachineHealthCheckClass: clusterv1.MachineHealthCheckClass{
  1700  								UnhealthyConditions: []clusterv1.UnhealthyCondition{
  1701  									{
  1702  										Type:   corev1.NodeReady,
  1703  										Status: corev1.ConditionFalse,
  1704  									},
  1705  								},
  1706  							},
  1707  						}).
  1708  						Build()).
  1709  				Build(),
  1710  			class: builder.ClusterClass(metav1.NamespaceDefault, "clusterclass").
  1711  				Build(),
  1712  			classReconciled: true,
  1713  			wantErr:         true,
  1714  		},
  1715  		{
  1716  			name: "Accept a cluster that has MHC enabled for control plane with control plane MHC defined in ClusterClass",
  1717  			cluster: builder.Cluster(metav1.NamespaceDefault, "cluster1").
  1718  				WithTopology(
  1719  					builder.ClusterTopology().
  1720  						WithClass("clusterclass").
  1721  						WithVersion("v1.22.2").
  1722  						WithControlPlaneReplicas(3).
  1723  						WithControlPlaneMachineHealthCheck(&clusterv1.MachineHealthCheckTopology{
  1724  							Enable: ptr.To(true),
  1725  						}).
  1726  						Build()).
  1727  				Build(),
  1728  			class: builder.ClusterClass(metav1.NamespaceDefault, "clusterclass").
  1729  				WithControlPlaneMachineHealthCheck(&clusterv1.MachineHealthCheckClass{}).
  1730  				Build(),
  1731  			classReconciled: true,
  1732  			wantErr:         false,
  1733  		},
  1734  		{
  1735  			name: "Accept a cluster that has MHC enabled for control plane with control plane MHC defined in cluster topology",
  1736  			cluster: builder.Cluster(metav1.NamespaceDefault, "cluster1").
  1737  				WithTopology(
  1738  					builder.ClusterTopology().
  1739  						WithClass("clusterclass").
  1740  						WithVersion("v1.22.2").
  1741  						WithControlPlaneReplicas(3).
  1742  						WithControlPlaneMachineHealthCheck(&clusterv1.MachineHealthCheckTopology{
  1743  							Enable: ptr.To(true),
  1744  							MachineHealthCheckClass: clusterv1.MachineHealthCheckClass{
  1745  								UnhealthyConditions: []clusterv1.UnhealthyCondition{
  1746  									{
  1747  										Type:   corev1.NodeReady,
  1748  										Status: corev1.ConditionFalse,
  1749  									},
  1750  								},
  1751  							},
  1752  						}).
  1753  						Build()).
  1754  				Build(),
  1755  			class: builder.ClusterClass(metav1.NamespaceDefault, "clusterclass").
  1756  				WithControlPlaneInfrastructureMachineTemplate(&unstructured.Unstructured{}).
  1757  				Build(),
  1758  			classReconciled: true,
  1759  			wantErr:         false,
  1760  		},
  1761  		{
  1762  			name: "Reject a cluster that has MHC enabled for machine deployment but is missing MHC definition in cluster topology and ClusterClass",
  1763  			cluster: builder.Cluster(metav1.NamespaceDefault, "cluster1").
  1764  				WithTopology(
  1765  					builder.ClusterTopology().
  1766  						WithClass("clusterclass").
  1767  						WithVersion("v1.22.2").
  1768  						WithControlPlaneReplicas(3).
  1769  						WithMachineDeployment(
  1770  							builder.MachineDeploymentTopology("md1").
  1771  								WithClass("worker-class").
  1772  								WithMachineHealthCheck(&clusterv1.MachineHealthCheckTopology{
  1773  									Enable: ptr.To(true),
  1774  								}).
  1775  								Build(),
  1776  						).
  1777  						Build()).
  1778  				Build(),
  1779  			class: builder.ClusterClass(metav1.NamespaceDefault, "clusterclass").
  1780  				WithWorkerMachineDeploymentClasses(
  1781  					*builder.MachineDeploymentClass("worker-class").Build(),
  1782  				).
  1783  				Build(),
  1784  			classReconciled: true,
  1785  			wantErr:         true,
  1786  		},
  1787  		{
  1788  			name: "Reject a cluster that has MHC override defined for machine deployment but is missing unhealthy conditions",
  1789  			cluster: builder.Cluster(metav1.NamespaceDefault, "cluster1").
  1790  				WithTopology(
  1791  					builder.ClusterTopology().
  1792  						WithClass("clusterclass").
  1793  						WithVersion("v1.22.2").
  1794  						WithControlPlaneReplicas(3).
  1795  						WithMachineDeployment(
  1796  							builder.MachineDeploymentTopology("md1").
  1797  								WithClass("worker-class").
  1798  								WithMachineHealthCheck(&clusterv1.MachineHealthCheckTopology{
  1799  									MachineHealthCheckClass: clusterv1.MachineHealthCheckClass{
  1800  										UnhealthyConditions: []clusterv1.UnhealthyCondition{},
  1801  									},
  1802  								}).
  1803  								Build(),
  1804  						).
  1805  						Build()).
  1806  				Build(),
  1807  			class: builder.ClusterClass(metav1.NamespaceDefault, "clusterclass").
  1808  				WithWorkerMachineDeploymentClasses(
  1809  					*builder.MachineDeploymentClass("worker-class").Build(),
  1810  				).
  1811  				Build(),
  1812  			classReconciled: true,
  1813  			wantErr:         true,
  1814  		},
  1815  		{
  1816  			name: "Accept a cluster that has MHC enabled for machine deployment with machine deployment MHC defined in ClusterClass",
  1817  			cluster: builder.Cluster(metav1.NamespaceDefault, "cluster1").
  1818  				WithTopology(
  1819  					builder.ClusterTopology().
  1820  						WithClass("clusterclass").
  1821  						WithVersion("v1.22.2").
  1822  						WithControlPlaneReplicas(3).
  1823  						WithMachineDeployment(
  1824  							builder.MachineDeploymentTopology("md1").
  1825  								WithClass("worker-class").
  1826  								WithMachineHealthCheck(&clusterv1.MachineHealthCheckTopology{
  1827  									Enable: ptr.To(true),
  1828  								}).
  1829  								Build(),
  1830  						).
  1831  						Build()).
  1832  				Build(),
  1833  			class: builder.ClusterClass(metav1.NamespaceDefault, "clusterclass").
  1834  				WithWorkerMachineDeploymentClasses(
  1835  					*builder.MachineDeploymentClass("worker-class").
  1836  						WithMachineHealthCheckClass(&clusterv1.MachineHealthCheckClass{}).
  1837  						Build(),
  1838  				).
  1839  				Build(),
  1840  			classReconciled: true,
  1841  			wantErr:         false,
  1842  		},
  1843  		{
  1844  			name: "Accept a cluster that has MHC enabled for machine deployment with machine deployment MHC defined in cluster topology",
  1845  			cluster: builder.Cluster(metav1.NamespaceDefault, "cluster1").
  1846  				WithTopology(
  1847  					builder.ClusterTopology().
  1848  						WithClass("clusterclass").
  1849  						WithVersion("v1.22.2").
  1850  						WithControlPlaneReplicas(3).
  1851  						WithMachineDeployment(
  1852  							builder.MachineDeploymentTopology("md1").
  1853  								WithClass("worker-class").
  1854  								WithMachineHealthCheck(&clusterv1.MachineHealthCheckTopology{
  1855  									Enable: ptr.To(true),
  1856  									MachineHealthCheckClass: clusterv1.MachineHealthCheckClass{
  1857  										UnhealthyConditions: []clusterv1.UnhealthyCondition{
  1858  											{
  1859  												Type:   corev1.NodeReady,
  1860  												Status: corev1.ConditionFalse,
  1861  											},
  1862  										},
  1863  									},
  1864  								}).
  1865  								Build(),
  1866  						).
  1867  						Build()).
  1868  				Build(),
  1869  			class: builder.ClusterClass(metav1.NamespaceDefault, "clusterclass").
  1870  				WithWorkerMachineDeploymentClasses(
  1871  					*builder.MachineDeploymentClass("worker-class").Build(),
  1872  				).
  1873  				Build(),
  1874  			classReconciled: true,
  1875  			wantErr:         false,
  1876  		},
  1877  	}
  1878  	for _, tt := range tests {
  1879  		t.Run(tt.name, func(*testing.T) {
  1880  			// Mark this condition to true so the webhook sees the ClusterClass as up to date.
  1881  			if tt.classReconciled {
  1882  				conditions.MarkTrue(tt.class, clusterv1.ClusterClassVariablesReconciledCondition)
  1883  			}
  1884  			// Sets up the fakeClient for the test case.
  1885  			fakeClient := fake.NewClientBuilder().
  1886  				WithObjects(tt.class).
  1887  				WithScheme(fakeScheme).
  1888  				Build()
  1889  
  1890  			// Create the webhook and add the fakeClient as its client. This is required because the test uses a Managed Topology.
  1891  			c := &Cluster{Client: fakeClient}
  1892  
  1893  			// Checks the return error.
  1894  			warnings, err := c.ValidateCreate(ctx, tt.cluster)
  1895  			if tt.wantErr {
  1896  				g.Expect(err).To(HaveOccurred())
  1897  			} else {
  1898  				g.Expect(err).ToNot(HaveOccurred())
  1899  			}
  1900  			if tt.wantWarnings {
  1901  				g.Expect(warnings).ToNot(BeEmpty())
  1902  			} else {
  1903  				g.Expect(warnings).To(BeEmpty())
  1904  			}
  1905  		})
  1906  	}
  1907  }
  1908  
  1909  // TestClusterTopologyValidationForTopologyClassChange cases where cluster.spec.topology.class is altered.
  1910  func TestClusterTopologyValidationForTopologyClassChange(t *testing.T) {
  1911  	defer utilfeature.SetFeatureGateDuringTest(t, feature.Gates, feature.ClusterTopology, true)()
  1912  	g := NewWithT(t)
  1913  
  1914  	cluster := builder.Cluster(metav1.NamespaceDefault, "cluster1").
  1915  		WithTopology(
  1916  			builder.ClusterTopology().
  1917  				WithClass("class1").
  1918  				WithVersion("v1.22.2").
  1919  				WithControlPlaneReplicas(3).
  1920  				Build()).
  1921  		Build()
  1922  
  1923  	ref := &corev1.ObjectReference{
  1924  		APIVersion: "group.test.io/foo",
  1925  		Kind:       "barTemplate",
  1926  		Name:       "baz",
  1927  		Namespace:  "default",
  1928  	}
  1929  	compatibleNameChangeRef := &corev1.ObjectReference{
  1930  		APIVersion: "group.test.io/foo",
  1931  		Kind:       "barTemplate",
  1932  		Name:       "differentbaz",
  1933  		Namespace:  "default",
  1934  	}
  1935  	compatibleAPIVersionChangeRef := &corev1.ObjectReference{
  1936  		APIVersion: "group.test.io/foo2",
  1937  		Kind:       "barTemplate",
  1938  		Name:       "differentbaz",
  1939  		Namespace:  "default",
  1940  	}
  1941  	incompatibleKindRef := &corev1.ObjectReference{
  1942  		APIVersion: "group.test.io/foo",
  1943  		Kind:       "another-barTemplate",
  1944  		Name:       "another-baz",
  1945  		Namespace:  "default",
  1946  	}
  1947  	incompatibleAPIGroupRef := &corev1.ObjectReference{
  1948  		APIVersion: "group.nottest.io/foo",
  1949  		Kind:       "barTemplate",
  1950  		Name:       "another-baz",
  1951  		Namespace:  "default",
  1952  	}
  1953  
  1954  	tests := []struct {
  1955  		name        string
  1956  		cluster     *clusterv1.Cluster
  1957  		firstClass  *clusterv1.ClusterClass
  1958  		secondClass *clusterv1.ClusterClass
  1959  		wantErr     bool
  1960  	}{
  1961  		// InfrastructureCluster changes.
  1962  		{
  1963  			name: "Accept cluster.topology.class change with a compatible infrastructureCluster Kind ref change",
  1964  			firstClass: builder.ClusterClass(metav1.NamespaceDefault, "class1").
  1965  				WithInfrastructureClusterTemplate(refToUnstructured(ref)).
  1966  				WithControlPlaneTemplate(refToUnstructured(ref)).
  1967  				WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)).
  1968  				Build(),
  1969  			secondClass: builder.ClusterClass(metav1.NamespaceDefault, "class2").
  1970  				WithInfrastructureClusterTemplate(refToUnstructured(compatibleNameChangeRef)).
  1971  				WithControlPlaneTemplate(refToUnstructured(ref)).
  1972  				WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)).
  1973  				Build(),
  1974  			wantErr: false,
  1975  		},
  1976  		{
  1977  			name: "Accept cluster.topology.class change with a compatible infrastructureCluster APIVersion ref change",
  1978  			firstClass: builder.ClusterClass(metav1.NamespaceDefault, "class1").
  1979  				WithInfrastructureClusterTemplate(refToUnstructured(ref)).
  1980  				WithControlPlaneTemplate(refToUnstructured(ref)).
  1981  				WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)).
  1982  				Build(),
  1983  			secondClass: builder.ClusterClass(metav1.NamespaceDefault, "class2").
  1984  				WithInfrastructureClusterTemplate(refToUnstructured(compatibleAPIVersionChangeRef)).
  1985  				WithControlPlaneTemplate(refToUnstructured(ref)).
  1986  				WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)).
  1987  				Build(),
  1988  			wantErr: false,
  1989  		},
  1990  
  1991  		{
  1992  			name: "Reject cluster.topology.class change with an incompatible infrastructureCluster Kind ref change",
  1993  			firstClass: builder.ClusterClass(metav1.NamespaceDefault, "class1").
  1994  				WithInfrastructureClusterTemplate(refToUnstructured(ref)).
  1995  				WithControlPlaneTemplate(refToUnstructured(ref)).
  1996  				WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)).
  1997  				Build(),
  1998  			secondClass: builder.ClusterClass(metav1.NamespaceDefault, "class2").
  1999  				WithInfrastructureClusterTemplate(refToUnstructured(incompatibleKindRef)).
  2000  				WithControlPlaneTemplate(refToUnstructured(ref)).
  2001  				WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)).
  2002  				Build(),
  2003  			wantErr: true,
  2004  		},
  2005  		{
  2006  			name: "Reject cluster.topology.class change with an incompatible infrastructureCluster APIGroup ref change",
  2007  			firstClass: builder.ClusterClass(metav1.NamespaceDefault, "class1").
  2008  				WithInfrastructureClusterTemplate(refToUnstructured(ref)).
  2009  				WithControlPlaneTemplate(refToUnstructured(ref)).
  2010  				WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)).
  2011  				Build(),
  2012  			secondClass: builder.ClusterClass(metav1.NamespaceDefault, "class2").
  2013  				WithInfrastructureClusterTemplate(refToUnstructured(incompatibleAPIGroupRef)).
  2014  				WithControlPlaneTemplate(refToUnstructured(ref)).
  2015  				WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)).
  2016  				Build(),
  2017  			wantErr: true,
  2018  		},
  2019  
  2020  		// ControlPlane changes.
  2021  		{
  2022  			name: "Accept cluster.topology.class change with a compatible controlPlaneTemplate ref change",
  2023  			firstClass: builder.ClusterClass(metav1.NamespaceDefault, "class1").
  2024  				WithInfrastructureClusterTemplate(refToUnstructured(ref)).
  2025  				WithControlPlaneTemplate(refToUnstructured(ref)).
  2026  				WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)).
  2027  				Build(),
  2028  			secondClass: builder.ClusterClass(metav1.NamespaceDefault, "class2").
  2029  				WithInfrastructureClusterTemplate(refToUnstructured(ref)).
  2030  				WithControlPlaneTemplate(refToUnstructured(compatibleNameChangeRef)).
  2031  				WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)).
  2032  				Build(),
  2033  			wantErr: false,
  2034  		},
  2035  		{
  2036  			name: "Accept cluster.topology.class change with a compatible controlPlaneTemplate ref change",
  2037  			firstClass: builder.ClusterClass(metav1.NamespaceDefault, "class1").
  2038  				WithInfrastructureClusterTemplate(refToUnstructured(ref)).
  2039  				WithControlPlaneTemplate(refToUnstructured(ref)).
  2040  				WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)).
  2041  				Build(),
  2042  			secondClass: builder.ClusterClass(metav1.NamespaceDefault, "class2").
  2043  				WithInfrastructureClusterTemplate(refToUnstructured(ref)).
  2044  				WithControlPlaneTemplate(refToUnstructured(compatibleAPIVersionChangeRef)).
  2045  				WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)).
  2046  				Build(),
  2047  			wantErr: false,
  2048  		},
  2049  
  2050  		{
  2051  			name: "Reject cluster.topology.class change with an incompatible controlPlane Kind ref change",
  2052  			firstClass: builder.ClusterClass(metav1.NamespaceDefault, "class1").
  2053  				WithInfrastructureClusterTemplate(refToUnstructured(ref)).
  2054  				WithControlPlaneTemplate(refToUnstructured(ref)).
  2055  				WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)).
  2056  				Build(),
  2057  			secondClass: builder.ClusterClass(metav1.NamespaceDefault, "class2").
  2058  				WithInfrastructureClusterTemplate(refToUnstructured(incompatibleKindRef)).
  2059  				WithControlPlaneTemplate(refToUnstructured(ref)).
  2060  				WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)).
  2061  				Build(),
  2062  			wantErr: true,
  2063  		},
  2064  		{
  2065  			name: "Reject cluster.topology.class change with an incompatible controlPlane APIVersion ref change",
  2066  			firstClass: builder.ClusterClass(metav1.NamespaceDefault, "class1").
  2067  				WithInfrastructureClusterTemplate(refToUnstructured(ref)).
  2068  				WithControlPlaneTemplate(refToUnstructured(ref)).
  2069  				WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)).
  2070  				Build(),
  2071  			secondClass: builder.ClusterClass(metav1.NamespaceDefault, "class2").
  2072  				WithInfrastructureClusterTemplate(refToUnstructured(ref)).
  2073  				WithControlPlaneTemplate(refToUnstructured(incompatibleAPIGroupRef)).
  2074  				WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(compatibleNameChangeRef)).
  2075  				Build(),
  2076  			wantErr: true,
  2077  		},
  2078  		{
  2079  			name: "Accept cluster.topology.class change with a compatible controlPlane.MachineInfrastructure ref change",
  2080  			firstClass: builder.ClusterClass(metav1.NamespaceDefault, "class1").
  2081  				WithInfrastructureClusterTemplate(refToUnstructured(ref)).
  2082  				WithControlPlaneTemplate(refToUnstructured(ref)).
  2083  				WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)).
  2084  				Build(),
  2085  			secondClass: builder.ClusterClass(metav1.NamespaceDefault, "class2").
  2086  				WithInfrastructureClusterTemplate(refToUnstructured(ref)).
  2087  				WithControlPlaneTemplate(refToUnstructured(ref)).
  2088  				WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(compatibleNameChangeRef)).
  2089  				Build(),
  2090  			wantErr: false,
  2091  		},
  2092  		{
  2093  			name: "Accept cluster.topology.class change with a compatible controlPlane.MachineInfrastructure ref change",
  2094  			firstClass: builder.ClusterClass(metav1.NamespaceDefault, "class1").
  2095  				WithInfrastructureClusterTemplate(refToUnstructured(ref)).
  2096  				WithControlPlaneTemplate(refToUnstructured(ref)).
  2097  				WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)).
  2098  				Build(),
  2099  			secondClass: builder.ClusterClass(metav1.NamespaceDefault, "class2").
  2100  				WithInfrastructureClusterTemplate(refToUnstructured(ref)).
  2101  				WithControlPlaneTemplate(refToUnstructured(ref)).
  2102  				WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(compatibleAPIVersionChangeRef)).
  2103  				Build(),
  2104  			wantErr: false,
  2105  		},
  2106  		{
  2107  			name: "Reject cluster.topology.class change with an incompatible controlPlane.MachineInfrastructure Kind ref change",
  2108  			firstClass: builder.ClusterClass(metav1.NamespaceDefault, "class1").
  2109  				WithInfrastructureClusterTemplate(refToUnstructured(ref)).
  2110  				WithControlPlaneTemplate(refToUnstructured(ref)).
  2111  				WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)).
  2112  				Build(),
  2113  			secondClass: builder.ClusterClass(metav1.NamespaceDefault, "class2").
  2114  				WithInfrastructureClusterTemplate(refToUnstructured(ref)).
  2115  				WithControlPlaneTemplate(refToUnstructured(ref)).
  2116  				WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(incompatibleKindRef)).
  2117  				Build(),
  2118  			wantErr: true,
  2119  		},
  2120  		{
  2121  			name: "Reject cluster.topology.class change with an incompatible controlPlane.MachineInfrastructure APIVersion ref change",
  2122  			firstClass: builder.ClusterClass(metav1.NamespaceDefault, "class1").
  2123  				WithInfrastructureClusterTemplate(refToUnstructured(ref)).
  2124  				WithControlPlaneTemplate(refToUnstructured(ref)).
  2125  				WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)).
  2126  				Build(),
  2127  			secondClass: builder.ClusterClass(metav1.NamespaceDefault, "class2").
  2128  				WithInfrastructureClusterTemplate(refToUnstructured(ref)).
  2129  				WithControlPlaneTemplate(refToUnstructured(ref)).
  2130  				WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(incompatibleAPIGroupRef)).
  2131  				Build(),
  2132  			wantErr: true,
  2133  		},
  2134  
  2135  		// MachineDeploymentClass & MachinePoolClass changes
  2136  		{
  2137  			name: "Accept cluster.topology.class change with a compatible MachineDeploymentClass and MachinePoolClass InfrastructureTemplate",
  2138  			firstClass: builder.ClusterClass(metav1.NamespaceDefault, "class1").
  2139  				WithInfrastructureClusterTemplate(refToUnstructured(ref)).
  2140  				WithControlPlaneTemplate(refToUnstructured(ref)).
  2141  				WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)).
  2142  				WithWorkerMachineDeploymentClasses(
  2143  					*builder.MachineDeploymentClass("aa").
  2144  						WithInfrastructureTemplate(refToUnstructured(ref)).
  2145  						WithBootstrapTemplate(refToUnstructured(ref)).
  2146  						Build(),
  2147  				).
  2148  				WithWorkerMachinePoolClasses(
  2149  					*builder.MachinePoolClass("aa").
  2150  						WithInfrastructureTemplate(refToUnstructured(ref)).
  2151  						WithBootstrapTemplate(refToUnstructured(ref)).
  2152  						Build(),
  2153  				).
  2154  				Build(),
  2155  			secondClass: builder.ClusterClass(metav1.NamespaceDefault, "class2").
  2156  				WithInfrastructureClusterTemplate(refToUnstructured(ref)).
  2157  				WithControlPlaneTemplate(refToUnstructured(ref)).
  2158  				WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)).
  2159  				WithWorkerMachineDeploymentClasses(
  2160  					*builder.MachineDeploymentClass("aa").
  2161  						WithInfrastructureTemplate(refToUnstructured(compatibleNameChangeRef)).
  2162  						WithBootstrapTemplate(refToUnstructured(ref)).
  2163  						Build(),
  2164  				).
  2165  				WithWorkerMachinePoolClasses(
  2166  					*builder.MachinePoolClass("aa").
  2167  						WithInfrastructureTemplate(refToUnstructured(compatibleNameChangeRef)).
  2168  						WithBootstrapTemplate(refToUnstructured(ref)).
  2169  						Build(),
  2170  				).
  2171  				Build(),
  2172  			wantErr: false,
  2173  		},
  2174  		{
  2175  			name: "Accept cluster.topology.class change with an incompatible MachineDeploymentClass and MachinePoolClass BootstrapTemplate",
  2176  			firstClass: builder.ClusterClass(metav1.NamespaceDefault, "class1").
  2177  				WithInfrastructureClusterTemplate(refToUnstructured(ref)).
  2178  				WithControlPlaneTemplate(refToUnstructured(ref)).
  2179  				WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)).
  2180  				WithWorkerMachineDeploymentClasses(
  2181  					*builder.MachineDeploymentClass("aa").
  2182  						WithInfrastructureTemplate(refToUnstructured(ref)).
  2183  						WithBootstrapTemplate(refToUnstructured(ref)).
  2184  						Build(),
  2185  				).
  2186  				WithWorkerMachinePoolClasses(
  2187  					*builder.MachinePoolClass("aa").
  2188  						WithInfrastructureTemplate(refToUnstructured(ref)).
  2189  						WithBootstrapTemplate(refToUnstructured(ref)).
  2190  						Build(),
  2191  				).
  2192  				Build(),
  2193  			secondClass: builder.ClusterClass(metav1.NamespaceDefault, "class2").
  2194  				WithInfrastructureClusterTemplate(refToUnstructured(ref)).
  2195  				WithControlPlaneTemplate(refToUnstructured(ref)).
  2196  				WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)).
  2197  				WithWorkerMachineDeploymentClasses(
  2198  					*builder.MachineDeploymentClass("aa").
  2199  						WithInfrastructureTemplate(refToUnstructured(compatibleNameChangeRef)).
  2200  						WithBootstrapTemplate(refToUnstructured(incompatibleKindRef)).
  2201  						Build(),
  2202  				).
  2203  				WithWorkerMachinePoolClasses(
  2204  					*builder.MachinePoolClass("aa").
  2205  						WithInfrastructureTemplate(refToUnstructured(compatibleNameChangeRef)).
  2206  						WithBootstrapTemplate(refToUnstructured(incompatibleKindRef)).
  2207  						Build(),
  2208  				).
  2209  				Build(),
  2210  			wantErr: false,
  2211  		},
  2212  		{
  2213  			name: "Accept cluster.topology.class change with a deleted MachineDeploymentClass and MachinePoolClass",
  2214  			firstClass: builder.ClusterClass(metav1.NamespaceDefault, "class1").
  2215  				WithInfrastructureClusterTemplate(refToUnstructured(ref)).
  2216  				WithControlPlaneTemplate(refToUnstructured(ref)).
  2217  				WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)).
  2218  				WithWorkerMachineDeploymentClasses(
  2219  					*builder.MachineDeploymentClass("aa").
  2220  						WithInfrastructureTemplate(refToUnstructured(ref)).
  2221  						WithBootstrapTemplate(refToUnstructured(ref)).
  2222  						Build(),
  2223  					*builder.MachineDeploymentClass("bb").
  2224  						WithInfrastructureTemplate(refToUnstructured(ref)).
  2225  						WithBootstrapTemplate(refToUnstructured(ref)).
  2226  						Build(),
  2227  				).
  2228  				WithWorkerMachinePoolClasses(
  2229  					*builder.MachinePoolClass("aa").
  2230  						WithInfrastructureTemplate(refToUnstructured(ref)).
  2231  						WithBootstrapTemplate(refToUnstructured(ref)).
  2232  						Build(),
  2233  					*builder.MachinePoolClass("bb").
  2234  						WithInfrastructureTemplate(refToUnstructured(ref)).
  2235  						WithBootstrapTemplate(refToUnstructured(ref)).
  2236  						Build(),
  2237  				).
  2238  				Build(),
  2239  			secondClass: builder.ClusterClass(metav1.NamespaceDefault, "class2").
  2240  				WithInfrastructureClusterTemplate(refToUnstructured(ref)).
  2241  				WithControlPlaneTemplate(refToUnstructured(ref)).
  2242  				WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)).
  2243  				WithWorkerMachineDeploymentClasses(
  2244  					*builder.MachineDeploymentClass("aa").
  2245  						WithInfrastructureTemplate(refToUnstructured(ref)).
  2246  						WithBootstrapTemplate(refToUnstructured(ref)).
  2247  						Build(),
  2248  				).
  2249  				WithWorkerMachinePoolClasses(
  2250  					*builder.MachinePoolClass("aa").
  2251  						WithInfrastructureTemplate(refToUnstructured(ref)).
  2252  						WithBootstrapTemplate(refToUnstructured(ref)).
  2253  						Build(),
  2254  				).
  2255  				Build(),
  2256  			wantErr: false,
  2257  		},
  2258  		{
  2259  			name: "Accept cluster.topology.class change with an added MachineDeploymentClass and MachinePoolClass",
  2260  			firstClass: builder.ClusterClass(metav1.NamespaceDefault, "class1").
  2261  				WithInfrastructureClusterTemplate(refToUnstructured(ref)).
  2262  				WithControlPlaneTemplate(refToUnstructured(ref)).
  2263  				WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)).
  2264  				WithWorkerMachineDeploymentClasses(
  2265  					*builder.MachineDeploymentClass("aa").
  2266  						WithInfrastructureTemplate(refToUnstructured(ref)).
  2267  						WithBootstrapTemplate(refToUnstructured(ref)).
  2268  						Build(),
  2269  				).
  2270  				WithWorkerMachinePoolClasses(
  2271  					*builder.MachinePoolClass("aa").
  2272  						WithInfrastructureTemplate(refToUnstructured(ref)).
  2273  						WithBootstrapTemplate(refToUnstructured(ref)).
  2274  						Build(),
  2275  				).
  2276  				Build(),
  2277  			secondClass: builder.ClusterClass(metav1.NamespaceDefault, "class2").
  2278  				WithInfrastructureClusterTemplate(refToUnstructured(ref)).
  2279  				WithControlPlaneTemplate(refToUnstructured(ref)).
  2280  				WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)).
  2281  				WithWorkerMachineDeploymentClasses(
  2282  					*builder.MachineDeploymentClass("aa").
  2283  						WithInfrastructureTemplate(refToUnstructured(ref)).
  2284  						WithBootstrapTemplate(refToUnstructured(ref)).
  2285  						Build(),
  2286  					*builder.MachineDeploymentClass("bb").
  2287  						WithInfrastructureTemplate(refToUnstructured(ref)).
  2288  						WithBootstrapTemplate(refToUnstructured(ref)).
  2289  						Build(),
  2290  				).
  2291  				WithWorkerMachinePoolClasses(
  2292  					*builder.MachinePoolClass("aa").
  2293  						WithInfrastructureTemplate(refToUnstructured(ref)).
  2294  						WithBootstrapTemplate(refToUnstructured(ref)).
  2295  						Build(),
  2296  					*builder.MachinePoolClass("bb").
  2297  						WithInfrastructureTemplate(refToUnstructured(ref)).
  2298  						WithBootstrapTemplate(refToUnstructured(ref)).
  2299  						Build(),
  2300  				).
  2301  				Build(),
  2302  			wantErr: false,
  2303  		},
  2304  		{
  2305  			name: "Reject cluster.topology.class change with an incompatible Kind change to MachineDeploymentClass InfrastructureTemplate",
  2306  			firstClass: builder.ClusterClass(metav1.NamespaceDefault, "class1").
  2307  				WithInfrastructureClusterTemplate(refToUnstructured(ref)).
  2308  				WithControlPlaneTemplate(refToUnstructured(ref)).
  2309  				WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)).
  2310  				WithWorkerMachineDeploymentClasses(
  2311  					*builder.MachineDeploymentClass("aa").
  2312  						WithInfrastructureTemplate(refToUnstructured(ref)).
  2313  						WithBootstrapTemplate(refToUnstructured(ref)).
  2314  						Build(),
  2315  				).
  2316  				Build(),
  2317  			secondClass: builder.ClusterClass(metav1.NamespaceDefault, "class2").
  2318  				WithInfrastructureClusterTemplate(refToUnstructured(ref)).
  2319  				WithControlPlaneTemplate(refToUnstructured(ref)).
  2320  				WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)).
  2321  				WithWorkerMachineDeploymentClasses(
  2322  					*builder.MachineDeploymentClass("aa").
  2323  						WithInfrastructureTemplate(refToUnstructured(incompatibleKindRef)).
  2324  						WithBootstrapTemplate(refToUnstructured(ref)).
  2325  						Build(),
  2326  				).
  2327  				Build(),
  2328  			wantErr: true,
  2329  		},
  2330  		{
  2331  			name: "Reject cluster.topology.class change with an incompatible APIGroup change to MachineDeploymentClass InfrastructureTemplate",
  2332  			firstClass: builder.ClusterClass(metav1.NamespaceDefault, "class1").
  2333  				WithInfrastructureClusterTemplate(refToUnstructured(ref)).
  2334  				WithControlPlaneTemplate(refToUnstructured(ref)).
  2335  				WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)).
  2336  				WithWorkerMachineDeploymentClasses(
  2337  					*builder.MachineDeploymentClass("aa").
  2338  						WithInfrastructureTemplate(refToUnstructured(ref)).
  2339  						WithBootstrapTemplate(refToUnstructured(ref)).
  2340  						Build(),
  2341  				).
  2342  				Build(),
  2343  			secondClass: builder.ClusterClass(metav1.NamespaceDefault, "class2").
  2344  				WithInfrastructureClusterTemplate(refToUnstructured(ref)).
  2345  				WithControlPlaneTemplate(refToUnstructured(ref)).
  2346  				WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)).
  2347  				WithWorkerMachineDeploymentClasses(
  2348  					*builder.MachineDeploymentClass("aa").
  2349  						WithInfrastructureTemplate(refToUnstructured(incompatibleAPIGroupRef)).
  2350  						WithBootstrapTemplate(refToUnstructured(ref)).
  2351  						Build(),
  2352  				).
  2353  				Build(),
  2354  			wantErr: true,
  2355  		},
  2356  
  2357  		// MachinePoolClass reject changes
  2358  		{
  2359  			name: "Reject cluster.topology.class change with an incompatible Kind change to MachinePoolClass InfrastructureTemplate",
  2360  			firstClass: builder.ClusterClass(metav1.NamespaceDefault, "class1").
  2361  				WithInfrastructureClusterTemplate(refToUnstructured(ref)).
  2362  				WithControlPlaneTemplate(refToUnstructured(ref)).
  2363  				WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)).
  2364  				WithWorkerMachinePoolClasses(
  2365  					*builder.MachinePoolClass("aa").
  2366  						WithInfrastructureTemplate(refToUnstructured(ref)).
  2367  						WithBootstrapTemplate(refToUnstructured(ref)).
  2368  						Build(),
  2369  				).
  2370  				Build(),
  2371  			secondClass: builder.ClusterClass(metav1.NamespaceDefault, "class2").
  2372  				WithInfrastructureClusterTemplate(refToUnstructured(ref)).
  2373  				WithControlPlaneTemplate(refToUnstructured(ref)).
  2374  				WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)).
  2375  				WithWorkerMachinePoolClasses(
  2376  					*builder.MachinePoolClass("aa").
  2377  						WithInfrastructureTemplate(refToUnstructured(incompatibleKindRef)).
  2378  						WithBootstrapTemplate(refToUnstructured(ref)).
  2379  						Build(),
  2380  				).
  2381  				Build(),
  2382  			wantErr: true,
  2383  		},
  2384  		{
  2385  			name: "Reject cluster.topology.class change with an incompatible APIGroup change to MachinePoolClass InfrastructureTemplate",
  2386  			firstClass: builder.ClusterClass(metav1.NamespaceDefault, "class1").
  2387  				WithInfrastructureClusterTemplate(refToUnstructured(ref)).
  2388  				WithControlPlaneTemplate(refToUnstructured(ref)).
  2389  				WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)).
  2390  				WithWorkerMachinePoolClasses(
  2391  					*builder.MachinePoolClass("aa").
  2392  						WithInfrastructureTemplate(refToUnstructured(ref)).
  2393  						WithBootstrapTemplate(refToUnstructured(ref)).
  2394  						Build(),
  2395  				).
  2396  				Build(),
  2397  			secondClass: builder.ClusterClass(metav1.NamespaceDefault, "class2").
  2398  				WithInfrastructureClusterTemplate(refToUnstructured(ref)).
  2399  				WithControlPlaneTemplate(refToUnstructured(ref)).
  2400  				WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)).
  2401  				WithWorkerMachinePoolClasses(
  2402  					*builder.MachinePoolClass("aa").
  2403  						WithInfrastructureTemplate(refToUnstructured(incompatibleAPIGroupRef)).
  2404  						WithBootstrapTemplate(refToUnstructured(ref)).
  2405  						Build(),
  2406  				).
  2407  				Build(),
  2408  			wantErr: true,
  2409  		},
  2410  	}
  2411  	for _, tt := range tests {
  2412  		t.Run(tt.name, func(*testing.T) {
  2413  			// Mark this condition to true so the webhook sees the ClusterClass as up to date.
  2414  			conditions.MarkTrue(tt.firstClass, clusterv1.ClusterClassVariablesReconciledCondition)
  2415  			conditions.MarkTrue(tt.secondClass, clusterv1.ClusterClassVariablesReconciledCondition)
  2416  
  2417  			// Sets up the fakeClient for the test case.
  2418  			fakeClient := fake.NewClientBuilder().
  2419  				WithObjects(tt.firstClass, tt.secondClass).
  2420  				WithScheme(fakeScheme).
  2421  				Build()
  2422  
  2423  			// Create the webhook and add the fakeClient as its client. This is required because the test uses a Managed Topology.
  2424  			c := &Cluster{Client: fakeClient}
  2425  
  2426  			// Create and updated cluster which uses the name of the second class from the test definition in its '.spec.topology.'
  2427  			secondCluster := cluster.DeepCopy()
  2428  			secondCluster.Spec.Topology.Class = tt.secondClass.Name
  2429  
  2430  			// Checks the return error.
  2431  			warnings, err := c.ValidateUpdate(ctx, cluster, secondCluster)
  2432  			if tt.wantErr {
  2433  				g.Expect(err).To(HaveOccurred())
  2434  			} else {
  2435  				g.Expect(err).ToNot(HaveOccurred())
  2436  			}
  2437  			g.Expect(warnings).To(BeEmpty())
  2438  		})
  2439  	}
  2440  }
  2441  
  2442  // TestMovingBetweenManagedAndUnmanaged cluster tests cases where a clusterClass is added or removed during a cluster update.
  2443  func TestMovingBetweenManagedAndUnmanaged(t *testing.T) {
  2444  	defer utilfeature.SetFeatureGateDuringTest(t, feature.Gates, feature.ClusterTopology, true)()
  2445  	ref := &corev1.ObjectReference{
  2446  		APIVersion: "group.test.io/foo",
  2447  		Kind:       "barTemplate",
  2448  		Name:       "baz",
  2449  		Namespace:  "default",
  2450  	}
  2451  
  2452  	g := NewWithT(t)
  2453  
  2454  	tests := []struct {
  2455  		name            string
  2456  		cluster         *clusterv1.Cluster
  2457  		clusterClass    *clusterv1.ClusterClass
  2458  		updatedTopology *clusterv1.Topology
  2459  		wantErr         bool
  2460  	}{
  2461  		{
  2462  			name: "Reject cluster moving from Unmanaged to Managed i.e. adding the spec.topology.class field on update",
  2463  			cluster: builder.Cluster(metav1.NamespaceDefault, "cluster1").
  2464  				Build(),
  2465  			clusterClass: builder.ClusterClass(metav1.NamespaceDefault, "class1").
  2466  				WithInfrastructureClusterTemplate(refToUnstructured(ref)).
  2467  				WithControlPlaneTemplate(refToUnstructured(ref)).
  2468  				WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)).
  2469  				Build(),
  2470  			updatedTopology: builder.ClusterTopology().
  2471  				WithClass("class1").
  2472  				WithVersion("v1.22.2").
  2473  				WithControlPlaneReplicas(3).
  2474  				Build(),
  2475  			wantErr: true,
  2476  		},
  2477  		{
  2478  			name: "Allow cluster moving from Unmanaged to Managed i.e. adding the spec.topology.class field on update " +
  2479  				"if and only if ClusterTopologyUnsafeUpdateClassNameAnnotation is set",
  2480  			cluster: builder.Cluster(metav1.NamespaceDefault, "cluster1").
  2481  				WithAnnotations(map[string]string{clusterv1.ClusterTopologyUnsafeUpdateClassNameAnnotation: ""}).
  2482  				Build(),
  2483  			clusterClass: builder.ClusterClass(metav1.NamespaceDefault, "class1").
  2484  				WithInfrastructureClusterTemplate(refToUnstructured(ref)).
  2485  				WithControlPlaneTemplate(refToUnstructured(ref)).
  2486  				WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)).
  2487  				Build(),
  2488  			updatedTopology: builder.ClusterTopology().
  2489  				WithClass("class1").
  2490  				WithVersion("v1.22.2").
  2491  				WithControlPlaneReplicas(3).
  2492  				Build(),
  2493  			wantErr: false,
  2494  		},
  2495  		{
  2496  			name: "Reject cluster moving from Managed to Unmanaged i.e. removing the spec.topology.class field on update",
  2497  			cluster: builder.Cluster(metav1.NamespaceDefault, "cluster1").
  2498  				WithTopology(builder.ClusterTopology().
  2499  					WithClass("class1").
  2500  					WithVersion("v1.22.2").
  2501  					WithControlPlaneReplicas(3).
  2502  					Build()).
  2503  				Build(),
  2504  			clusterClass: builder.ClusterClass(metav1.NamespaceDefault, "class1").
  2505  				WithInfrastructureClusterTemplate(refToUnstructured(ref)).
  2506  				WithControlPlaneTemplate(refToUnstructured(ref)).
  2507  				WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)).
  2508  				Build(),
  2509  			updatedTopology: nil,
  2510  			wantErr:         true,
  2511  		},
  2512  		{
  2513  			name: "Reject cluster update if ClusterClass does not exist",
  2514  			cluster: builder.Cluster(metav1.NamespaceDefault, "cluster1").
  2515  				WithTopology(builder.ClusterTopology().
  2516  					WithClass("class1").
  2517  					WithVersion("v1.22.2").
  2518  					WithControlPlaneReplicas(3).
  2519  					Build()).
  2520  				Build(),
  2521  			clusterClass:
  2522  			// ClusterClass name is different to that in the Cluster `.spec.topology.class`
  2523  			builder.ClusterClass(metav1.NamespaceDefault, "completely-different-class").
  2524  				WithInfrastructureClusterTemplate(refToUnstructured(ref)).
  2525  				WithControlPlaneTemplate(refToUnstructured(ref)).
  2526  				WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)).
  2527  				Build(),
  2528  			updatedTopology: builder.ClusterTopology().
  2529  				WithClass("class1").
  2530  				WithVersion("v1.22.2").
  2531  				WithControlPlaneReplicas(3).
  2532  				Build(),
  2533  			wantErr: true,
  2534  		},
  2535  	}
  2536  	for _, tt := range tests {
  2537  		t.Run(tt.name, func(*testing.T) {
  2538  			// Mark this condition to true so the webhook sees the ClusterClass as up to date.
  2539  			conditions.MarkTrue(tt.clusterClass, clusterv1.ClusterClassVariablesReconciledCondition)
  2540  			// Sets up the fakeClient for the test case.
  2541  			fakeClient := fake.NewClientBuilder().
  2542  				WithObjects(tt.clusterClass, tt.cluster).
  2543  				WithScheme(fakeScheme).
  2544  				Build()
  2545  
  2546  			// Create the webhook and add the fakeClient as its client. This is required because the test uses a Managed Topology.
  2547  			c := &Cluster{Client: fakeClient}
  2548  
  2549  			// Create and updated cluster which uses the name of the second class from the test definition in its '.spec.topology.'
  2550  			updatedCluster := tt.cluster.DeepCopy()
  2551  			updatedCluster.Spec.Topology = tt.updatedTopology
  2552  
  2553  			// Checks the return error.
  2554  			warnings, err := c.ValidateUpdate(ctx, tt.cluster, updatedCluster)
  2555  			if tt.wantErr {
  2556  				g.Expect(err).To(HaveOccurred())
  2557  			} else {
  2558  				g.Expect(err).NotTo(HaveOccurred())
  2559  				// Errors may be duplicated as warnings. There should be no warnings in this case if there are no errors.
  2560  				g.Expect(warnings).To(BeEmpty())
  2561  			}
  2562  		})
  2563  	}
  2564  }
  2565  
  2566  // TestClusterClassPollingErrors tests when a Cluster can be reconciled given different reconcile states of the ClusterClass.
  2567  func TestClusterClassPollingErrors(t *testing.T) {
  2568  	defer utilfeature.SetFeatureGateDuringTest(t, feature.Gates, feature.ClusterTopology, true)()
  2569  	g := NewWithT(t)
  2570  	ref := &corev1.ObjectReference{
  2571  		APIVersion: "group.test.io/foo",
  2572  		Kind:       "barTemplate",
  2573  		Name:       "baz",
  2574  		Namespace:  "default",
  2575  	}
  2576  
  2577  	topology := builder.ClusterTopology().WithClass("class1").WithVersion("v1.24.3").Build()
  2578  	secondTopology := builder.ClusterTopology().WithClass("class2").WithVersion("v1.24.3").Build()
  2579  	notFoundTopology := builder.ClusterTopology().WithClass("doesnotexist").WithVersion("v1.24.3").Build()
  2580  
  2581  	baseClusterClass := builder.ClusterClass(metav1.NamespaceDefault, "class1").
  2582  		WithInfrastructureClusterTemplate(refToUnstructured(ref)).
  2583  		WithControlPlaneTemplate(refToUnstructured(ref)).
  2584  		WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref))
  2585  
  2586  	// ccFullyReconciled is a ClusterClass with a matching generation and observed generation, and VariablesReconciled=True.
  2587  	ccFullyReconciled := baseClusterClass.DeepCopy().Build()
  2588  	ccFullyReconciled.Generation = 1
  2589  	ccFullyReconciled.Status.ObservedGeneration = 1
  2590  	conditions.MarkTrue(ccFullyReconciled, clusterv1.ClusterClassVariablesReconciledCondition)
  2591  
  2592  	// secondFullyReconciled is a second ClusterClass with a matching generation and observed generation, and VariablesReconciled=True.
  2593  	secondFullyReconciled := ccFullyReconciled.DeepCopy()
  2594  	secondFullyReconciled.SetName("class2")
  2595  
  2596  	// ccGenerationMismatch is a ClusterClass with a mismatched generation and observed generation, but VariablesReconciledCondition=True.
  2597  	ccGenerationMismatch := baseClusterClass.DeepCopy().Build()
  2598  	ccGenerationMismatch.Generation = 999
  2599  	ccGenerationMismatch.Status.ObservedGeneration = 1
  2600  	conditions.MarkTrue(ccGenerationMismatch, clusterv1.ClusterClassVariablesReconciledCondition)
  2601  
  2602  	// ccVariablesReconciledFalse with VariablesReconciled=False.
  2603  	ccVariablesReconciledFalse := baseClusterClass.DeepCopy().Build()
  2604  	conditions.MarkFalse(ccGenerationMismatch, clusterv1.ClusterClassVariablesReconciledCondition, "", clusterv1.ConditionSeverityError, "")
  2605  
  2606  	tests := []struct {
  2607  		name           string
  2608  		cluster        *clusterv1.Cluster
  2609  		oldCluster     *clusterv1.Cluster
  2610  		clusterClasses []*clusterv1.ClusterClass
  2611  		injectedErr    interceptor.Funcs
  2612  		wantErr        bool
  2613  		wantWarnings   bool
  2614  	}{
  2615  		{
  2616  			name:           "Pass on create if ClusterClass is fully reconciled",
  2617  			cluster:        builder.Cluster(metav1.NamespaceDefault, "cluster1").WithTopology(topology).Build(),
  2618  			clusterClasses: []*clusterv1.ClusterClass{ccFullyReconciled},
  2619  			wantErr:        false,
  2620  		},
  2621  		{
  2622  			name:           "Pass on create if ClusterClass generation does not match observedGeneration",
  2623  			cluster:        builder.Cluster(metav1.NamespaceDefault, "cluster1").WithTopology(topology).Build(),
  2624  			clusterClasses: []*clusterv1.ClusterClass{ccGenerationMismatch},
  2625  			wantErr:        false,
  2626  			wantWarnings:   true,
  2627  		},
  2628  		{
  2629  			name:           "Pass on create if ClusterClass generation matches observedGeneration but VariablesReconciled=False",
  2630  			cluster:        builder.Cluster(metav1.NamespaceDefault, "cluster1").WithTopology(topology).Build(),
  2631  			clusterClasses: []*clusterv1.ClusterClass{ccVariablesReconciledFalse},
  2632  			wantErr:        false,
  2633  			wantWarnings:   true,
  2634  		},
  2635  		{
  2636  			name:           "Pass on create if ClusterClass is not found",
  2637  			cluster:        builder.Cluster(metav1.NamespaceDefault, "cluster1").WithTopology(notFoundTopology).Build(),
  2638  			clusterClasses: []*clusterv1.ClusterClass{ccFullyReconciled},
  2639  			wantErr:        false,
  2640  			wantWarnings:   true,
  2641  		},
  2642  		{
  2643  			name:           "Pass on update if oldCluster ClusterClass is fully reconciled",
  2644  			cluster:        builder.Cluster(metav1.NamespaceDefault, "cluster1").WithTopology(secondTopology).Build(),
  2645  			oldCluster:     builder.Cluster(metav1.NamespaceDefault, "cluster1").WithTopology(topology).Build(),
  2646  			clusterClasses: []*clusterv1.ClusterClass{ccFullyReconciled, secondFullyReconciled},
  2647  			wantErr:        false,
  2648  		},
  2649  		{
  2650  			name:           "Fail on update if oldCluster ClusterClass generation does not match observedGeneration",
  2651  			cluster:        builder.Cluster(metav1.NamespaceDefault, "cluster1").WithTopology(secondTopology).Build(),
  2652  			oldCluster:     builder.Cluster(metav1.NamespaceDefault, "cluster1").WithTopology(topology).Build(),
  2653  			clusterClasses: []*clusterv1.ClusterClass{ccGenerationMismatch, secondFullyReconciled},
  2654  			wantErr:        true,
  2655  		},
  2656  		{
  2657  			name:           "Fail on update if old Cluster ClusterClass is not found",
  2658  			cluster:        builder.Cluster(metav1.NamespaceDefault, "cluster1").WithTopology(topology).Build(),
  2659  			oldCluster:     builder.Cluster(metav1.NamespaceDefault, "cluster1").WithTopology(notFoundTopology).Build(),
  2660  			clusterClasses: []*clusterv1.ClusterClass{ccFullyReconciled},
  2661  			wantErr:        true,
  2662  		},
  2663  		{
  2664  			name:           "Fail on update if new Cluster ClusterClass is not found",
  2665  			cluster:        builder.Cluster(metav1.NamespaceDefault, "cluster1").WithTopology(notFoundTopology).Build(),
  2666  			oldCluster:     builder.Cluster(metav1.NamespaceDefault, "cluster1").WithTopology(topology).Build(),
  2667  			clusterClasses: []*clusterv1.ClusterClass{ccFullyReconciled},
  2668  			wantErr:        true,
  2669  			wantWarnings:   true,
  2670  		},
  2671  		{
  2672  			name:           "Fail on update if new ClusterClass returns connection error",
  2673  			cluster:        builder.Cluster(metav1.NamespaceDefault, "cluster1").WithTopology(secondTopology).Build(),
  2674  			oldCluster:     builder.Cluster(metav1.NamespaceDefault, "cluster1").WithTopology(topology).Build(),
  2675  			clusterClasses: []*clusterv1.ClusterClass{ccFullyReconciled, secondFullyReconciled},
  2676  			injectedErr: interceptor.Funcs{
  2677  				Get: func(ctx context.Context, client client.WithWatch, key client.ObjectKey, obj client.Object, _ ...client.GetOption) error {
  2678  					// Throw an error if the second ClusterClass `class2` used as the new ClusterClass is being retrieved.
  2679  					if key.Name == secondTopology.Class {
  2680  						return errors.New("connection error")
  2681  					}
  2682  					return client.Get(ctx, key, obj)
  2683  				},
  2684  			},
  2685  			wantErr:      true,
  2686  			wantWarnings: false,
  2687  		},
  2688  		{
  2689  			name:           "Fail on update if old ClusterClass returns connection error",
  2690  			cluster:        builder.Cluster(metav1.NamespaceDefault, "cluster1").WithTopology(secondTopology).Build(),
  2691  			oldCluster:     builder.Cluster(metav1.NamespaceDefault, "cluster1").WithTopology(topology).Build(),
  2692  			clusterClasses: []*clusterv1.ClusterClass{ccFullyReconciled, secondFullyReconciled},
  2693  			injectedErr: interceptor.Funcs{
  2694  				Get: func(ctx context.Context, client client.WithWatch, key client.ObjectKey, obj client.Object, _ ...client.GetOption) error {
  2695  					// Throw an error if the ClusterClass `class1` used as the old ClusterClass is being retrieved.
  2696  					if key.Name == topology.Class {
  2697  						return errors.New("connection error")
  2698  					}
  2699  					return client.Get(ctx, key, obj)
  2700  				},
  2701  			},
  2702  			wantErr:      true,
  2703  			wantWarnings: false,
  2704  		},
  2705  	}
  2706  
  2707  	for _, tt := range tests {
  2708  		t.Run(tt.name, func(*testing.T) {
  2709  			// Sets up a reconcile with a fakeClient for the test case.
  2710  			objs := []client.Object{}
  2711  			for _, cc := range tt.clusterClasses {
  2712  				objs = append(objs, cc)
  2713  			}
  2714  			c := &Cluster{Client: fake.NewClientBuilder().
  2715  				WithInterceptorFuncs(tt.injectedErr).
  2716  				WithScheme(fakeScheme).
  2717  				WithObjects(objs...).
  2718  				Build(),
  2719  			}
  2720  
  2721  			// Checks the return error.
  2722  			warnings, err := c.validate(ctx, tt.oldCluster, tt.cluster)
  2723  			if tt.wantErr {
  2724  				g.Expect(err).To(HaveOccurred())
  2725  			} else {
  2726  				g.Expect(err).ToNot(HaveOccurred())
  2727  			}
  2728  			if tt.wantWarnings {
  2729  				g.Expect(warnings).NotTo(BeEmpty())
  2730  			} else {
  2731  				g.Expect(warnings).To(BeEmpty())
  2732  			}
  2733  		})
  2734  	}
  2735  }
  2736  
  2737  func Test_validateTopologyControlPlaneVersion(t *testing.T) {
  2738  	tests := []struct {
  2739  		name              string
  2740  		expectErr         bool
  2741  		old               *clusterv1.Cluster
  2742  		additionalObjects []client.Object
  2743  	}{
  2744  		{
  2745  			name:      "should update if kcp is fully upgraded and up to date",
  2746  			expectErr: false,
  2747  			old: builder.Cluster("fooboo", "cluster1").
  2748  				WithControlPlane(builder.ControlPlane("fooboo", "cluster1-cp").Build()).
  2749  				WithTopology(builder.ClusterTopology().
  2750  					WithClass("foo").
  2751  					WithVersion("v1.19.1").
  2752  					Build()).
  2753  				Build(),
  2754  			additionalObjects: []client.Object{
  2755  				builder.ControlPlane("fooboo", "cluster1-cp").WithVersion("v1.19.1").
  2756  					WithStatusFields(map[string]interface{}{"status.version": "v1.19.1"}).
  2757  					Build(),
  2758  			},
  2759  		},
  2760  		{
  2761  			name:      "should block update if kcp is provisioning",
  2762  			expectErr: true,
  2763  			old: builder.Cluster("fooboo", "cluster1").
  2764  				WithControlPlane(builder.ControlPlane("fooboo", "cluster1-cp").Build()).
  2765  				WithTopology(builder.ClusterTopology().
  2766  					WithClass("foo").
  2767  					WithVersion("v1.19.1").
  2768  					Build()).
  2769  				Build(),
  2770  			additionalObjects: []client.Object{
  2771  				builder.ControlPlane("fooboo", "cluster1-cp").WithVersion("v1.19.1").
  2772  					Build(),
  2773  			},
  2774  		},
  2775  		{
  2776  			name:      "should block update if kcp is upgrading",
  2777  			expectErr: true,
  2778  			old: builder.Cluster("fooboo", "cluster1").
  2779  				WithControlPlane(builder.ControlPlane("fooboo", "cluster1-cp").Build()).
  2780  				WithTopology(builder.ClusterTopology().
  2781  					WithClass("foo").
  2782  					WithVersion("v1.19.1").
  2783  					Build()).
  2784  				Build(),
  2785  			additionalObjects: []client.Object{
  2786  				builder.ControlPlane("fooboo", "cluster1-cp").WithVersion("v1.18.1").
  2787  					WithStatusFields(map[string]interface{}{"status.version": "v1.17.1"}).
  2788  					Build(),
  2789  			},
  2790  		},
  2791  		{
  2792  			name:      "should block update if kcp is not yet upgraded",
  2793  			expectErr: true,
  2794  			old: builder.Cluster("fooboo", "cluster1").
  2795  				WithControlPlane(builder.ControlPlane("fooboo", "cluster1-cp").Build()).
  2796  				WithTopology(builder.ClusterTopology().
  2797  					WithClass("foo").
  2798  					WithVersion("v1.19.1").
  2799  					Build()).
  2800  				Build(),
  2801  			additionalObjects: []client.Object{
  2802  				builder.ControlPlane("fooboo", "cluster1-cp").WithVersion("v1.18.1").
  2803  					WithStatusFields(map[string]interface{}{"status.version": "v1.18.1"}).
  2804  					Build(),
  2805  			},
  2806  		},
  2807  	}
  2808  	for _, tt := range tests {
  2809  		t.Run(tt.name, func(t *testing.T) {
  2810  			g := NewWithT(t)
  2811  
  2812  			fakeClient := fake.NewClientBuilder().
  2813  				WithObjects(tt.additionalObjects...).
  2814  				WithScheme(fakeScheme).
  2815  				Build()
  2816  
  2817  			oldVersion, err := semver.ParseTolerant(tt.old.Spec.Topology.Version)
  2818  			g.Expect(err).ToNot(HaveOccurred())
  2819  
  2820  			err = validateTopologyControlPlaneVersion(ctx, fakeClient, tt.old, oldVersion)
  2821  			if tt.expectErr {
  2822  				g.Expect(err).To(HaveOccurred())
  2823  				return
  2824  			}
  2825  			g.Expect(err).ToNot(HaveOccurred())
  2826  		})
  2827  	}
  2828  }
  2829  
  2830  func Test_validateTopologyMachineDeploymentVersions(t *testing.T) {
  2831  	tests := []struct {
  2832  		name              string
  2833  		expectErr         bool
  2834  		old               *clusterv1.Cluster
  2835  		additionalObjects []client.Object
  2836  	}{
  2837  		{
  2838  			name:      "should update if no machine deployment is exists",
  2839  			expectErr: false,
  2840  			old: builder.Cluster("fooboo", "cluster1").
  2841  				WithTopology(builder.ClusterTopology().
  2842  					WithClass("foo").
  2843  					WithVersion("v1.19.1").
  2844  					Build()).
  2845  				Build(),
  2846  			additionalObjects: []client.Object{},
  2847  		},
  2848  		{
  2849  			name:      "should update if machine deployments are fully upgraded and up to date",
  2850  			expectErr: false,
  2851  			old: builder.Cluster("fooboo", "cluster1").
  2852  				WithTopology(builder.ClusterTopology().
  2853  					WithClass("foo").
  2854  					WithVersion("v1.19.1").
  2855  					WithMachineDeployment(
  2856  						builder.MachineDeploymentTopology("workers1").
  2857  							WithClass("aa").
  2858  							Build()).
  2859  					Build()).
  2860  				Build(),
  2861  			additionalObjects: []client.Object{
  2862  				builder.MachineDeployment("fooboo", "cluster1-workers1").WithLabels(map[string]string{
  2863  					clusterv1.ClusterNameLabel:                          "cluster1",
  2864  					clusterv1.ClusterTopologyOwnedLabel:                 "",
  2865  					clusterv1.ClusterTopologyMachineDeploymentNameLabel: "workers1",
  2866  				}).WithVersion("v1.19.1").Build(),
  2867  			},
  2868  		},
  2869  		{
  2870  			name:      "should block update if machine deployment is not yet upgraded",
  2871  			expectErr: true,
  2872  			old: builder.Cluster("fooboo", "cluster1").
  2873  				WithTopology(builder.ClusterTopology().
  2874  					WithClass("foo").
  2875  					WithVersion("v1.19.1").
  2876  					WithMachineDeployment(
  2877  						builder.MachineDeploymentTopology("workers1").
  2878  							WithClass("aa").
  2879  							Build()).
  2880  					Build()).
  2881  				Build(),
  2882  			additionalObjects: []client.Object{
  2883  				builder.MachineDeployment("fooboo", "cluster1-workers1").WithLabels(map[string]string{
  2884  					clusterv1.ClusterNameLabel:                          "cluster1",
  2885  					clusterv1.ClusterTopologyOwnedLabel:                 "",
  2886  					clusterv1.ClusterTopologyMachineDeploymentNameLabel: "workers1",
  2887  				}).WithVersion("v1.18.1").Build(),
  2888  			},
  2889  		},
  2890  		{
  2891  			name:      "should block update if machine deployment is upgrading",
  2892  			expectErr: true,
  2893  			old: builder.Cluster("fooboo", "cluster1").
  2894  				WithTopology(builder.ClusterTopology().
  2895  					WithClass("foo").
  2896  					WithVersion("v1.19.1").
  2897  					WithMachineDeployment(
  2898  						builder.MachineDeploymentTopology("workers1").
  2899  							WithClass("aa").
  2900  							Build()).
  2901  					Build()).
  2902  				Build(),
  2903  			additionalObjects: []client.Object{
  2904  				builder.MachineDeployment("fooboo", "cluster1-workers1").WithLabels(map[string]string{
  2905  					clusterv1.ClusterNameLabel:                          "cluster1",
  2906  					clusterv1.ClusterTopologyOwnedLabel:                 "",
  2907  					clusterv1.ClusterTopologyMachineDeploymentNameLabel: "workers1",
  2908  				}).WithVersion("v1.19.1").WithSelector(*metav1.SetAsLabelSelector(labels.Set{clusterv1.ClusterTopologyMachineDeploymentNameLabel: "workers1"})).Build(),
  2909  				builder.Machine("fooboo", "cluster1-workers1-1").WithLabels(map[string]string{
  2910  					clusterv1.ClusterNameLabel: "cluster1",
  2911  					// clusterv1.ClusterTopologyOwnedLabel:                 "",
  2912  					clusterv1.ClusterTopologyMachineDeploymentNameLabel: "workers1",
  2913  				}).WithVersion("v1.18.1").Build(),
  2914  			},
  2915  		},
  2916  	}
  2917  	for _, tt := range tests {
  2918  		t.Run(tt.name, func(t *testing.T) {
  2919  			g := NewWithT(t)
  2920  
  2921  			fakeClient := fake.NewClientBuilder().
  2922  				WithObjects(tt.additionalObjects...).
  2923  				WithScheme(fakeScheme).
  2924  				Build()
  2925  
  2926  			oldVersion, err := semver.ParseTolerant(tt.old.Spec.Topology.Version)
  2927  			g.Expect(err).ToNot(HaveOccurred())
  2928  
  2929  			err = validateTopologyMachineDeploymentVersions(ctx, fakeClient, tt.old, oldVersion)
  2930  			if tt.expectErr {
  2931  				g.Expect(err).To(HaveOccurred())
  2932  				return
  2933  			}
  2934  			g.Expect(err).ToNot(HaveOccurred())
  2935  		})
  2936  	}
  2937  }
  2938  
  2939  func Test_validateTopologyMachinePoolVersions(t *testing.T) {
  2940  	tests := []struct {
  2941  		name              string
  2942  		expectErr         bool
  2943  		old               *clusterv1.Cluster
  2944  		additionalObjects []client.Object
  2945  		workloadObjects   []client.Object
  2946  	}{
  2947  		{
  2948  			name:      "should update if no machine pool is exists",
  2949  			expectErr: false,
  2950  			old: builder.Cluster("fooboo", "cluster1").
  2951  				WithTopology(builder.ClusterTopology().
  2952  					WithClass("foo").
  2953  					WithVersion("v1.19.1").
  2954  					Build()).
  2955  				Build(),
  2956  			additionalObjects: []client.Object{},
  2957  			workloadObjects:   []client.Object{},
  2958  		},
  2959  		{
  2960  			name:      "should update if machine pools are fully upgraded and up to date",
  2961  			expectErr: false,
  2962  			old: builder.Cluster("fooboo", "cluster1").
  2963  				WithTopology(builder.ClusterTopology().
  2964  					WithClass("foo").
  2965  					WithVersion("v1.19.1").
  2966  					WithMachinePool(
  2967  						builder.MachinePoolTopology("pool1").
  2968  							WithClass("aa").
  2969  							Build()).
  2970  					Build()).
  2971  				Build(),
  2972  			additionalObjects: []client.Object{
  2973  				builder.MachinePool("fooboo", "cluster1-pool1").WithLabels(map[string]string{
  2974  					clusterv1.ClusterNameLabel:                    "cluster1",
  2975  					clusterv1.ClusterTopologyOwnedLabel:           "",
  2976  					clusterv1.ClusterTopologyMachinePoolNameLabel: "pool1",
  2977  				}).WithVersion("v1.19.1").Build(),
  2978  			},
  2979  			workloadObjects: []client.Object{},
  2980  		},
  2981  		{
  2982  			name:      "should block update if machine pool is not yet upgraded",
  2983  			expectErr: true,
  2984  			old: builder.Cluster("fooboo", "cluster1").
  2985  				WithTopology(builder.ClusterTopology().
  2986  					WithClass("foo").
  2987  					WithVersion("v1.19.1").
  2988  					WithMachinePool(
  2989  						builder.MachinePoolTopology("pool1").
  2990  							WithClass("aa").
  2991  							Build()).
  2992  					Build()).
  2993  				Build(),
  2994  			additionalObjects: []client.Object{
  2995  				builder.MachinePool("fooboo", "cluster1-pool1").WithLabels(map[string]string{
  2996  					clusterv1.ClusterNameLabel:                    "cluster1",
  2997  					clusterv1.ClusterTopologyOwnedLabel:           "",
  2998  					clusterv1.ClusterTopologyMachinePoolNameLabel: "pool1",
  2999  				}).WithVersion("v1.18.1").Build(),
  3000  			},
  3001  			workloadObjects: []client.Object{},
  3002  		},
  3003  		{
  3004  			name:      "should block update machine pool is upgrading",
  3005  			expectErr: true,
  3006  			old: builder.Cluster("fooboo", "cluster1").
  3007  				WithTopology(builder.ClusterTopology().
  3008  					WithClass("foo").
  3009  					WithVersion("v1.19.1").
  3010  					WithMachinePool(
  3011  						builder.MachinePoolTopology("pool1").
  3012  							WithClass("aa").
  3013  							Build()).
  3014  					Build()).
  3015  				Build(),
  3016  			additionalObjects: []client.Object{
  3017  				builder.MachinePool("fooboo", "cluster1-pool1").WithLabels(map[string]string{
  3018  					clusterv1.ClusterNameLabel:                    "cluster1",
  3019  					clusterv1.ClusterTopologyOwnedLabel:           "",
  3020  					clusterv1.ClusterTopologyMachinePoolNameLabel: "pool1",
  3021  				}).WithVersion("v1.19.1").WithStatus(expv1.MachinePoolStatus{NodeRefs: []corev1.ObjectReference{{Name: "mp-node-1"}}}).Build(),
  3022  			},
  3023  			workloadObjects: []client.Object{
  3024  				&corev1.Node{
  3025  					ObjectMeta: metav1.ObjectMeta{Name: "mp-node-1"},
  3026  					Status:     corev1.NodeStatus{NodeInfo: corev1.NodeSystemInfo{KubeletVersion: "v1.18.1"}},
  3027  				},
  3028  			},
  3029  		},
  3030  		{
  3031  			name:      "should block update if it cannot get the node of a machine pool",
  3032  			expectErr: true,
  3033  			old: builder.Cluster("fooboo", "cluster1").
  3034  				WithTopology(builder.ClusterTopology().
  3035  					WithClass("foo").
  3036  					WithVersion("v1.19.1").
  3037  					WithMachinePool(
  3038  						builder.MachinePoolTopology("pool1").
  3039  							WithClass("aa").
  3040  							Build()).
  3041  					Build()).
  3042  				Build(),
  3043  			additionalObjects: []client.Object{
  3044  				builder.MachinePool("fooboo", "cluster1-pool1").WithLabels(map[string]string{
  3045  					clusterv1.ClusterNameLabel:                    "cluster1",
  3046  					clusterv1.ClusterTopologyOwnedLabel:           "",
  3047  					clusterv1.ClusterTopologyMachinePoolNameLabel: "pool1",
  3048  				}).WithVersion("v1.19.1").WithStatus(expv1.MachinePoolStatus{NodeRefs: []corev1.ObjectReference{{Name: "mp-node-1"}}}).Build(),
  3049  			},
  3050  			workloadObjects: []client.Object{},
  3051  		},
  3052  	}
  3053  	for _, tt := range tests {
  3054  		t.Run(tt.name, func(t *testing.T) {
  3055  			g := NewWithT(t)
  3056  
  3057  			fakeClient := fake.NewClientBuilder().
  3058  				WithObjects(tt.additionalObjects...).
  3059  				WithScheme(fakeScheme).
  3060  				Build()
  3061  
  3062  			oldVersion, err := semver.ParseTolerant(tt.old.Spec.Topology.Version)
  3063  			g.Expect(err).ToNot(HaveOccurred())
  3064  
  3065  			fakeClusterCacheTracker := &fakeClusterCacheTracker{
  3066  				client: fake.NewClientBuilder().
  3067  					WithObjects(tt.workloadObjects...).
  3068  					Build(),
  3069  			}
  3070  
  3071  			err = validateTopologyMachinePoolVersions(ctx, fakeClient, fakeClusterCacheTracker, tt.old, oldVersion)
  3072  			if tt.expectErr {
  3073  				g.Expect(err).To(HaveOccurred())
  3074  				return
  3075  			}
  3076  			g.Expect(err).ToNot(HaveOccurred())
  3077  		})
  3078  	}
  3079  }
  3080  
  3081  func refToUnstructured(ref *corev1.ObjectReference) *unstructured.Unstructured {
  3082  	gvk := ref.GetObjectKind().GroupVersionKind()
  3083  	output := &unstructured.Unstructured{}
  3084  	output.SetKind(gvk.Kind)
  3085  	output.SetAPIVersion(gvk.GroupVersion().String())
  3086  	output.SetName(ref.Name)
  3087  	output.SetNamespace(ref.Namespace)
  3088  	return output
  3089  }
  3090  
  3091  type fakeClusterCacheTracker struct {
  3092  	client client.Reader
  3093  }
  3094  
  3095  func (f *fakeClusterCacheTracker) GetReader(_ context.Context, _ types.NamespacedName) (client.Reader, error) {
  3096  	return f.client, nil
  3097  }