sigs.k8s.io/cluster-api@v1.6.3/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/onsi/gomega"
    24  	"github.com/pkg/errors"
    25  	corev1 "k8s.io/api/core/v1"
    26  	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
    27  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    28  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    29  	utilfeature "k8s.io/component-base/featuregate/testing"
    30  	"k8s.io/utils/pointer"
    31  	"sigs.k8s.io/controller-runtime/pkg/client"
    32  	"sigs.k8s.io/controller-runtime/pkg/client/fake"
    33  	"sigs.k8s.io/controller-runtime/pkg/client/interceptor"
    34  
    35  	clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
    36  	"sigs.k8s.io/cluster-api/feature"
    37  	"sigs.k8s.io/cluster-api/internal/test/builder"
    38  	"sigs.k8s.io/cluster-api/internal/webhooks/util"
    39  	"sigs.k8s.io/cluster-api/util/conditions"
    40  )
    41  
    42  func TestClusterDefaultNamespaces(t *testing.T) {
    43  	g := NewWithT(t)
    44  
    45  	c := &clusterv1.Cluster{
    46  		ObjectMeta: metav1.ObjectMeta{
    47  			Namespace: "fooboo",
    48  		},
    49  		Spec: clusterv1.ClusterSpec{
    50  			InfrastructureRef: &corev1.ObjectReference{},
    51  			ControlPlaneRef:   &corev1.ObjectReference{},
    52  		},
    53  	}
    54  	webhook := &Cluster{}
    55  	t.Run("for Cluster", util.CustomDefaultValidateTest(ctx, c, webhook))
    56  
    57  	g.Expect(webhook.Default(ctx, c)).To(Succeed())
    58  
    59  	g.Expect(c.Spec.InfrastructureRef.Namespace).To(Equal(c.Namespace))
    60  	g.Expect(c.Spec.ControlPlaneRef.Namespace).To(Equal(c.Namespace))
    61  }
    62  
    63  // TestClusterDefaultAndValidateVariables cases where cluster.spec.topology.class is altered.
    64  func TestClusterDefaultAndValidateVariables(t *testing.T) {
    65  	defer utilfeature.SetFeatureGateDuringTest(t, feature.Gates, feature.ClusterTopology, true)()
    66  
    67  	tests := []struct {
    68  		name         string
    69  		clusterClass *clusterv1.ClusterClass
    70  		topology     *clusterv1.Topology
    71  		expect       *clusterv1.Topology
    72  		wantErr      bool
    73  	}{
    74  		{
    75  			name: "default a single variable to its correct values",
    76  			clusterClass: builder.ClusterClass(metav1.NamespaceDefault, "class1").
    77  				WithStatusVariables(clusterv1.ClusterClassStatusVariable{
    78  					Name: "location",
    79  					Definitions: []clusterv1.ClusterClassStatusVariableDefinition{
    80  						{
    81  							Required: true,
    82  							From:     clusterv1.VariableDefinitionFromInline,
    83  							Schema: clusterv1.VariableSchema{
    84  								OpenAPIV3Schema: clusterv1.JSONSchemaProps{
    85  									Type:    "string",
    86  									Default: &apiextensionsv1.JSON{Raw: []byte(`"us-east"`)},
    87  								},
    88  							},
    89  						},
    90  					},
    91  				},
    92  				).
    93  				Build(),
    94  			topology: &clusterv1.Topology{},
    95  			expect: &clusterv1.Topology{
    96  				Variables: []clusterv1.ClusterVariable{
    97  					{
    98  						Name: "location",
    99  						Value: apiextensionsv1.JSON{
   100  							Raw: []byte(`"us-east"`),
   101  						},
   102  					},
   103  				},
   104  			},
   105  		},
   106  		{
   107  			name: "don't change a variable if it is already set",
   108  			clusterClass: builder.ClusterClass(metav1.NamespaceDefault, "class1").
   109  				WithStatusVariables(clusterv1.ClusterClassStatusVariable{
   110  					Name: "location",
   111  					Definitions: []clusterv1.ClusterClassStatusVariableDefinition{
   112  						{
   113  							Required: true,
   114  							From:     clusterv1.VariableDefinitionFromInline,
   115  							Schema: clusterv1.VariableSchema{
   116  								OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   117  									Type:    "string",
   118  									Default: &apiextensionsv1.JSON{Raw: []byte(`"us-east"`)},
   119  								},
   120  							},
   121  						},
   122  					},
   123  				},
   124  				).
   125  				Build(),
   126  			topology: &clusterv1.Topology{
   127  				Variables: []clusterv1.ClusterVariable{
   128  					{
   129  						Name: "location",
   130  						Value: apiextensionsv1.JSON{
   131  							Raw: []byte(`"A different location"`),
   132  						},
   133  					},
   134  				},
   135  			},
   136  			expect: &clusterv1.Topology{
   137  				Variables: []clusterv1.ClusterVariable{
   138  					{
   139  						Name: "location",
   140  						Value: apiextensionsv1.JSON{
   141  							Raw: []byte(`"A different location"`),
   142  						},
   143  					},
   144  				},
   145  			},
   146  		},
   147  		{
   148  			name: "default many variables to their correct values",
   149  			clusterClass: builder.ClusterClass(metav1.NamespaceDefault, "class1").
   150  				WithStatusVariables([]clusterv1.ClusterClassStatusVariable{
   151  					{
   152  						Name: "location",
   153  						Definitions: []clusterv1.ClusterClassStatusVariableDefinition{
   154  							{
   155  								Required: true,
   156  								From:     clusterv1.VariableDefinitionFromInline,
   157  								Schema: clusterv1.VariableSchema{
   158  									OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   159  										Type:    "string",
   160  										Default: &apiextensionsv1.JSON{Raw: []byte(`"us-east"`)},
   161  									},
   162  								},
   163  							},
   164  						},
   165  					},
   166  					{
   167  						Name: "count",
   168  						Definitions: []clusterv1.ClusterClassStatusVariableDefinition{
   169  							{
   170  								Required: true,
   171  								From:     clusterv1.VariableDefinitionFromInline,
   172  								Schema: clusterv1.VariableSchema{
   173  									OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   174  										Type:    "number",
   175  										Default: &apiextensionsv1.JSON{Raw: []byte(`0.1`)},
   176  									},
   177  								},
   178  							},
   179  						},
   180  					},
   181  				}...).
   182  				Build(),
   183  			topology: &clusterv1.Topology{},
   184  			expect: &clusterv1.Topology{
   185  				Variables: []clusterv1.ClusterVariable{
   186  					{
   187  						Name: "location",
   188  						Value: apiextensionsv1.JSON{
   189  							Raw: []byte(`"us-east"`),
   190  						},
   191  					},
   192  					{
   193  						Name: "count",
   194  						Value: apiextensionsv1.JSON{
   195  							Raw: []byte(`0.1`),
   196  						},
   197  					},
   198  				},
   199  			},
   200  		},
   201  		{
   202  			name: "don't add new variable overrides",
   203  			clusterClass: builder.ClusterClass(metav1.NamespaceDefault, "class1").
   204  				WithWorkerMachineDeploymentClasses(
   205  					*builder.MachineDeploymentClass("default-worker").Build(),
   206  				).
   207  				WithStatusVariables(clusterv1.ClusterClassStatusVariable{
   208  					Name: "location",
   209  					Definitions: []clusterv1.ClusterClassStatusVariableDefinition{
   210  						{
   211  							Required: true,
   212  							From:     clusterv1.VariableDefinitionFromInline,
   213  							Schema: clusterv1.VariableSchema{
   214  								OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   215  									Type:    "string",
   216  									Default: &apiextensionsv1.JSON{Raw: []byte(`"us-east"`)},
   217  								},
   218  							},
   219  						},
   220  					},
   221  				}).
   222  				Build(),
   223  			topology: &clusterv1.Topology{
   224  				Workers: &clusterv1.WorkersTopology{
   225  					MachineDeployments: []clusterv1.MachineDeploymentTopology{
   226  						{
   227  							Class: "default-worker",
   228  							Name:  "md-1",
   229  						},
   230  					},
   231  				},
   232  			},
   233  			expect: &clusterv1.Topology{
   234  				Workers: &clusterv1.WorkersTopology{
   235  					MachineDeployments: []clusterv1.MachineDeploymentTopology{
   236  						{
   237  							Class: "default-worker",
   238  							Name:  "md-1",
   239  							// "location" has not been added to .variables.overrides.
   240  						},
   241  					},
   242  				},
   243  				Variables: []clusterv1.ClusterVariable{
   244  					{
   245  						Name: "location",
   246  						Value: apiextensionsv1.JSON{
   247  							Raw: []byte(`"us-east"`),
   248  						},
   249  					},
   250  				},
   251  			},
   252  		},
   253  		{
   254  			name: "default nested fields of variable overrides",
   255  			clusterClass: builder.ClusterClass(metav1.NamespaceDefault, "class1").
   256  				WithWorkerMachineDeploymentClasses(
   257  					*builder.MachineDeploymentClass("default-worker").Build(),
   258  				).
   259  				WithStatusVariables(clusterv1.ClusterClassStatusVariable{
   260  					Name: "httpProxy",
   261  					Definitions: []clusterv1.ClusterClassStatusVariableDefinition{
   262  						{
   263  							Required: true,
   264  							From:     clusterv1.VariableDefinitionFromInline,
   265  							Schema: clusterv1.VariableSchema{
   266  								OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   267  									Type: "object",
   268  									Properties: map[string]clusterv1.JSONSchemaProps{
   269  										"enabled": {
   270  											Type: "boolean",
   271  										},
   272  										"url": {
   273  											Type:    "string",
   274  											Default: &apiextensionsv1.JSON{Raw: []byte(`"http://localhost:3128"`)},
   275  										},
   276  									},
   277  								},
   278  							},
   279  						},
   280  					},
   281  				}).
   282  				Build(),
   283  			topology: &clusterv1.Topology{
   284  				Workers: &clusterv1.WorkersTopology{
   285  					MachineDeployments: []clusterv1.MachineDeploymentTopology{
   286  						{
   287  							Class: "default-worker",
   288  							Name:  "md-1",
   289  							Variables: &clusterv1.MachineDeploymentVariables{
   290  								Overrides: []clusterv1.ClusterVariable{
   291  									{
   292  										Name:  "httpProxy",
   293  										Value: apiextensionsv1.JSON{Raw: []byte(`{"enabled":true}`)},
   294  									},
   295  								},
   296  							},
   297  						},
   298  					},
   299  				},
   300  				Variables: []clusterv1.ClusterVariable{
   301  					{
   302  						Name:  "httpProxy",
   303  						Value: apiextensionsv1.JSON{Raw: []byte(`{"enabled":true}`)},
   304  					},
   305  				},
   306  			},
   307  			expect: &clusterv1.Topology{
   308  				Workers: &clusterv1.WorkersTopology{
   309  					MachineDeployments: []clusterv1.MachineDeploymentTopology{
   310  						{
   311  							Class: "default-worker",
   312  							Name:  "md-1",
   313  							Variables: &clusterv1.MachineDeploymentVariables{
   314  								Overrides: []clusterv1.ClusterVariable{
   315  									{
   316  										Name: "httpProxy",
   317  										// url has been added by defaulting.
   318  										Value: apiextensionsv1.JSON{Raw: []byte(`{"enabled":true,"url":"http://localhost:3128"}`)},
   319  									},
   320  								},
   321  							},
   322  						},
   323  					},
   324  				},
   325  				Variables: []clusterv1.ClusterVariable{
   326  					{
   327  						Name: "httpProxy",
   328  						Value: apiextensionsv1.JSON{
   329  							// url has been added by defaulting.
   330  							Raw: []byte(`{"enabled":true,"url":"http://localhost:3128"}`),
   331  						},
   332  					},
   333  				},
   334  			},
   335  		},
   336  		{
   337  			name: "Use one value for multiple definitions when variables don't conflict",
   338  			clusterClass: builder.ClusterClass(metav1.NamespaceDefault, "class1").
   339  				WithStatusVariables(clusterv1.ClusterClassStatusVariable{
   340  					Name: "location",
   341  					Definitions: []clusterv1.ClusterClassStatusVariableDefinition{
   342  						{
   343  							Required: true,
   344  							From:     clusterv1.VariableDefinitionFromInline,
   345  							Schema: clusterv1.VariableSchema{
   346  								OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   347  									Type:    "string",
   348  									Default: &apiextensionsv1.JSON{Raw: []byte(`"us-east"`)},
   349  								},
   350  							},
   351  						},
   352  						{
   353  							Required: true,
   354  							From:     "somepatch",
   355  							Schema: clusterv1.VariableSchema{
   356  								OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   357  									Type:    "string",
   358  									Default: &apiextensionsv1.JSON{Raw: []byte(`"us-east"`)},
   359  								},
   360  							},
   361  						},
   362  						{
   363  							Required: true,
   364  							From:     "anotherpatch",
   365  							Schema: clusterv1.VariableSchema{
   366  								OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   367  									Type:    "string",
   368  									Default: &apiextensionsv1.JSON{Raw: []byte(`"us-east"`)},
   369  								},
   370  							},
   371  						},
   372  					},
   373  				},
   374  				).
   375  				Build(),
   376  			topology: &clusterv1.Topology{},
   377  			expect: &clusterv1.Topology{
   378  				Variables: []clusterv1.ClusterVariable{
   379  					{
   380  						Name: "location",
   381  						Value: apiextensionsv1.JSON{
   382  							Raw: []byte(`"us-east"`),
   383  						},
   384  					},
   385  				},
   386  			},
   387  		},
   388  		{
   389  			name: "Add defaults for each definitionFrom if variable is defined for some definitionFrom",
   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  				Variables: []clusterv1.ClusterVariable{
   430  					{
   431  						Name: "location",
   432  						Value: apiextensionsv1.JSON{
   433  							Raw: []byte(`"us-west"`),
   434  						},
   435  						DefinitionFrom: "somepatch",
   436  					},
   437  				},
   438  			},
   439  			expect: &clusterv1.Topology{
   440  				Variables: []clusterv1.ClusterVariable{
   441  					{
   442  						Name: "location",
   443  						Value: apiextensionsv1.JSON{
   444  							Raw: []byte(`"us-west"`),
   445  						},
   446  						DefinitionFrom: "somepatch",
   447  					},
   448  					{
   449  						Name: "location",
   450  						Value: apiextensionsv1.JSON{
   451  							Raw: []byte(`"us-east"`),
   452  						},
   453  						DefinitionFrom: clusterv1.VariableDefinitionFromInline,
   454  					},
   455  					{
   456  						Name: "location",
   457  						Value: apiextensionsv1.JSON{
   458  							Raw: []byte(`"us-east"`),
   459  						},
   460  						DefinitionFrom: "anotherpatch",
   461  					},
   462  				},
   463  			},
   464  		},
   465  		{
   466  			name: "set definitionFrom on defaults when variables conflict",
   467  			clusterClass: builder.ClusterClass(metav1.NamespaceDefault, "class1").
   468  				WithStatusVariables(clusterv1.ClusterClassStatusVariable{
   469  					Name:                "location",
   470  					DefinitionsConflict: true,
   471  					Definitions: []clusterv1.ClusterClassStatusVariableDefinition{
   472  						{
   473  							Required: true,
   474  							From:     clusterv1.VariableDefinitionFromInline,
   475  							Schema: clusterv1.VariableSchema{
   476  								OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   477  									Type:    "string",
   478  									Default: &apiextensionsv1.JSON{Raw: []byte(`"first-region"`)},
   479  								},
   480  							},
   481  						},
   482  						{
   483  							Required: true,
   484  							From:     "somepatch",
   485  							Schema: clusterv1.VariableSchema{
   486  								OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   487  									Type:    "string",
   488  									Default: &apiextensionsv1.JSON{Raw: []byte(`"another-region"`)},
   489  								},
   490  							},
   491  						},
   492  						{
   493  							Required: true,
   494  							From:     "anotherpatch",
   495  							Schema: clusterv1.VariableSchema{
   496  								OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   497  									Type:    "string",
   498  									Default: &apiextensionsv1.JSON{Raw: []byte(`"us-east"`)},
   499  								},
   500  							},
   501  						},
   502  					},
   503  				},
   504  				).
   505  				Build(),
   506  			topology: &clusterv1.Topology{},
   507  			expect: &clusterv1.Topology{
   508  				Variables: []clusterv1.ClusterVariable{
   509  					{
   510  						Name: "location",
   511  						Value: apiextensionsv1.JSON{
   512  							Raw: []byte(`"first-region"`),
   513  						},
   514  						DefinitionFrom: clusterv1.VariableDefinitionFromInline,
   515  					},
   516  					{
   517  						Name: "location",
   518  						Value: apiextensionsv1.JSON{
   519  							Raw: []byte(`"another-region"`),
   520  						},
   521  						DefinitionFrom: "somepatch",
   522  					},
   523  					{
   524  						Name: "location",
   525  						Value: apiextensionsv1.JSON{
   526  							Raw: []byte(`"us-east"`),
   527  						},
   528  						DefinitionFrom: "anotherpatch",
   529  					},
   530  				},
   531  			},
   532  		},
   533  		// Testing validation of variables.
   534  		{
   535  			name: "should fail when required variable is missing top-level",
   536  			clusterClass: builder.ClusterClass(metav1.NamespaceDefault, "class1").
   537  				WithStatusVariables(clusterv1.ClusterClassStatusVariable{
   538  					Name: "cpu",
   539  					Definitions: []clusterv1.ClusterClassStatusVariableDefinition{
   540  						{
   541  							Required: true,
   542  							From:     clusterv1.VariableDefinitionFromInline,
   543  							Schema: clusterv1.VariableSchema{
   544  								OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   545  									Type: "integer",
   546  								},
   547  							},
   548  						},
   549  					},
   550  				}).Build(),
   551  			topology: builder.ClusterTopology().Build(),
   552  			expect:   builder.ClusterTopology().Build(),
   553  			wantErr:  true,
   554  		},
   555  		{
   556  			name: "should fail when top-level variable is invalid",
   557  			clusterClass: builder.ClusterClass(metav1.NamespaceDefault, "class1").
   558  				WithStatusVariables(clusterv1.ClusterClassStatusVariable{
   559  					Name: "cpu",
   560  					Definitions: []clusterv1.ClusterClassStatusVariableDefinition{
   561  						{
   562  							Required: true,
   563  							From:     clusterv1.VariableDefinitionFromInline,
   564  							Schema: clusterv1.VariableSchema{
   565  								OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   566  									Type: "integer",
   567  								},
   568  							},
   569  						},
   570  					}},
   571  				).Build(),
   572  			topology: builder.ClusterTopology().
   573  				WithVariables(clusterv1.ClusterVariable{
   574  					Name:  "cpu",
   575  					Value: apiextensionsv1.JSON{Raw: []byte(`"text"`)},
   576  				}).
   577  				Build(),
   578  			expect:  builder.ClusterTopology().Build(),
   579  			wantErr: true,
   580  		},
   581  		{
   582  			name: "should fail when variable override is invalid",
   583  			clusterClass: builder.ClusterClass(metav1.NamespaceDefault, "class1").
   584  				WithStatusVariables(clusterv1.ClusterClassStatusVariable{
   585  					Name: "cpu",
   586  					Definitions: []clusterv1.ClusterClassStatusVariableDefinition{
   587  						{
   588  							Required: true,
   589  							From:     clusterv1.VariableDefinitionFromInline,
   590  							Schema: clusterv1.VariableSchema{
   591  								OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   592  									Type: "integer",
   593  								},
   594  							},
   595  						},
   596  					}}).Build(),
   597  			topology: builder.ClusterTopology().
   598  				WithVariables(clusterv1.ClusterVariable{
   599  					Name:  "cpu",
   600  					Value: apiextensionsv1.JSON{Raw: []byte(`2`)},
   601  				}).
   602  				WithMachineDeployment(builder.MachineDeploymentTopology("workers1").
   603  					WithClass("aa").
   604  					WithVariables(clusterv1.ClusterVariable{
   605  						Name:  "cpu",
   606  						Value: apiextensionsv1.JSON{Raw: []byte(`"text"`)},
   607  					}).
   608  					Build()).
   609  				Build(),
   610  			expect:  builder.ClusterTopology().Build(),
   611  			wantErr: true,
   612  		},
   613  		{
   614  			name: "should pass when required variable exists top-level",
   615  			clusterClass: builder.ClusterClass(metav1.NamespaceDefault, "class1").
   616  				WithStatusVariables(clusterv1.ClusterClassStatusVariable{
   617  					Name: "cpu",
   618  					Definitions: []clusterv1.ClusterClassStatusVariableDefinition{
   619  						{
   620  							Required: true,
   621  							From:     clusterv1.VariableDefinitionFromInline,
   622  							Schema: clusterv1.VariableSchema{
   623  								OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   624  									Type: "integer",
   625  								},
   626  							},
   627  						},
   628  					}}).Build(),
   629  			topology: builder.ClusterTopology().
   630  				WithClass("foo").
   631  				WithVersion("v1.19.1").
   632  				WithVariables(clusterv1.ClusterVariable{
   633  					Name:  "cpu",
   634  					Value: apiextensionsv1.JSON{Raw: []byte(`2`)},
   635  				}).
   636  				// Variable is not required in MachineDeployment topologies.
   637  				Build(),
   638  			expect: builder.ClusterTopology().
   639  				WithClass("foo").
   640  				WithVersion("v1.19.1").
   641  				WithVariables(clusterv1.ClusterVariable{
   642  					Name:  "cpu",
   643  					Value: apiextensionsv1.JSON{Raw: []byte(`2`)},
   644  				}).
   645  				// Variable is not required in MachineDeployment topologies.
   646  				Build(),
   647  		},
   648  		{
   649  			name: "should pass when top-level variable and override are valid",
   650  			clusterClass: builder.ClusterClass(metav1.NamespaceDefault, "class1").
   651  				WithWorkerMachineDeploymentClasses(*builder.MachineDeploymentClass("md1").Build()).
   652  				WithStatusVariables(clusterv1.ClusterClassStatusVariable{
   653  					Name: "cpu",
   654  					Definitions: []clusterv1.ClusterClassStatusVariableDefinition{
   655  						{
   656  							Required: true,
   657  							From:     clusterv1.VariableDefinitionFromInline,
   658  							Schema: clusterv1.VariableSchema{
   659  								OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   660  									Type: "integer",
   661  								},
   662  							},
   663  						},
   664  					}}).Build(),
   665  			topology: builder.ClusterTopology().
   666  				WithClass("foo").
   667  				WithVersion("v1.19.1").
   668  				WithVariables(clusterv1.ClusterVariable{
   669  					Name:  "cpu",
   670  					Value: apiextensionsv1.JSON{Raw: []byte(`2`)},
   671  				}).
   672  				WithMachineDeployment(builder.MachineDeploymentTopology("workers1").
   673  					WithClass("md1").
   674  					WithVariables(clusterv1.ClusterVariable{
   675  						Name:  "cpu",
   676  						Value: apiextensionsv1.JSON{Raw: []byte(`2`)},
   677  					}).
   678  					Build()).
   679  				Build(),
   680  			expect: builder.ClusterTopology().
   681  				WithClass("foo").
   682  				WithVersion("v1.19.1").
   683  				WithVariables(clusterv1.ClusterVariable{
   684  					Name:  "cpu",
   685  					Value: apiextensionsv1.JSON{Raw: []byte(`2`)},
   686  				}).
   687  				WithMachineDeployment(builder.MachineDeploymentTopology("workers1").
   688  					WithClass("md1").
   689  					WithVariables(clusterv1.ClusterVariable{
   690  						Name:  "cpu",
   691  						Value: apiextensionsv1.JSON{Raw: []byte(`2`)},
   692  					}).
   693  					Build()).
   694  				Build(),
   695  		},
   696  		{
   697  			name: "should pass even when variable override is missing the corresponding top-level variable",
   698  			clusterClass: builder.ClusterClass(metav1.NamespaceDefault, "class1").
   699  				WithWorkerMachineDeploymentClasses(*builder.MachineDeploymentClass("md1").Build()).
   700  				WithStatusVariables(clusterv1.ClusterClassStatusVariable{
   701  					Name: "cpu",
   702  					Definitions: []clusterv1.ClusterClassStatusVariableDefinition{
   703  						{
   704  							Required: false,
   705  							From:     clusterv1.VariableDefinitionFromInline,
   706  							Schema: clusterv1.VariableSchema{
   707  								OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   708  									Type: "integer",
   709  								},
   710  							},
   711  						},
   712  					}}).Build(),
   713  			topology: builder.ClusterTopology().
   714  				WithClass("foo").
   715  				WithVersion("v1.19.1").
   716  				WithMachineDeployment(builder.MachineDeploymentTopology("workers1").
   717  					WithClass("md1").
   718  					WithVariables(clusterv1.ClusterVariable{
   719  						Name:  "cpu",
   720  						Value: apiextensionsv1.JSON{Raw: []byte(`2`)},
   721  					}).
   722  					Build()).
   723  				Build(),
   724  			expect: builder.ClusterTopology().
   725  				WithClass("foo").
   726  				WithVersion("v1.19.1").
   727  				WithVariables([]clusterv1.ClusterVariable{}...).
   728  				WithMachineDeployment(builder.MachineDeploymentTopology("workers1").
   729  					WithClass("md1").
   730  					WithVariables(clusterv1.ClusterVariable{
   731  						Name:  "cpu",
   732  						Value: apiextensionsv1.JSON{Raw: []byte(`2`)},
   733  					}).
   734  					Build()).
   735  				Build(),
   736  		},
   737  	}
   738  	for _, tt := range tests {
   739  		t.Run(tt.name, func(t *testing.T) {
   740  			// Setting Class and Version here to avoid obfuscating the test cases above.
   741  			tt.topology.Class = "class1"
   742  			tt.topology.Version = "v1.22.2"
   743  			tt.expect.Class = "class1"
   744  			tt.expect.Version = "v1.22.2"
   745  
   746  			cluster := builder.Cluster(metav1.NamespaceDefault, "cluster1").
   747  				WithTopology(tt.topology).
   748  				Build()
   749  
   750  			// Mark this condition to true so the webhook sees the ClusterClass as up to date.
   751  			conditions.MarkTrue(tt.clusterClass, clusterv1.ClusterClassVariablesReconciledCondition)
   752  			fakeClient := fake.NewClientBuilder().
   753  				WithObjects(tt.clusterClass).
   754  				WithScheme(fakeScheme).
   755  				Build()
   756  			// Create the webhook and add the fakeClient as its client. This is required because the test uses a Managed Topology.
   757  			webhook := &Cluster{Client: fakeClient}
   758  
   759  			// Test defaulting.
   760  			t.Run("default", func(t *testing.T) {
   761  				g := NewWithT(t)
   762  				if tt.wantErr {
   763  					g.Expect(webhook.Default(ctx, cluster)).To(Not(Succeed()))
   764  					return
   765  				}
   766  				g.Expect(webhook.Default(ctx, cluster)).To(Succeed())
   767  				g.Expect(cluster.Spec.Topology).To(BeEquivalentTo(tt.expect))
   768  			})
   769  
   770  			// Test if defaulting works in combination with validation.
   771  			// Note this test is not run for the case where the webhook should fail.
   772  			if tt.wantErr {
   773  				t.Skip("skipping test for combination of defaulting and validation (not supported by the test)")
   774  			}
   775  			util.CustomDefaultValidateTest(ctx, cluster, webhook)(t)
   776  		})
   777  	}
   778  }
   779  
   780  func TestClusterDefaultTopologyVersion(t *testing.T) {
   781  	// NOTE: ClusterTopology feature flag is disabled by default, thus preventing to set Cluster.Topologies.
   782  	// Enabling the feature flag temporarily for this test.
   783  	defer utilfeature.SetFeatureGateDuringTest(t, feature.Gates, feature.ClusterTopology, true)()
   784  
   785  	g := NewWithT(t)
   786  
   787  	c := builder.Cluster("fooboo", "cluster1").
   788  		WithTopology(builder.ClusterTopology().
   789  			WithClass("foo").
   790  			WithVersion("1.19.1").
   791  			Build()).
   792  		Build()
   793  
   794  	clusterClass := builder.ClusterClass("fooboo", "foo").Build()
   795  	conditions.MarkTrue(clusterClass, clusterv1.ClusterClassVariablesReconciledCondition)
   796  	// Sets up the fakeClient for the test case. This is required because the test uses a Managed Topology.
   797  	fakeClient := fake.NewClientBuilder().
   798  		WithObjects(clusterClass).
   799  		WithScheme(fakeScheme).
   800  		Build()
   801  
   802  	// Create the webhook and add the fakeClient as its client.
   803  	webhook := &Cluster{Client: fakeClient}
   804  	t.Run("for Cluster", util.CustomDefaultValidateTest(ctx, c, webhook))
   805  
   806  	g.Expect(webhook.Default(ctx, c)).To(Succeed())
   807  
   808  	g.Expect(c.Spec.Topology.Version).To(HavePrefix("v"))
   809  }
   810  
   811  func TestClusterValidation(t *testing.T) {
   812  	// NOTE: ClusterTopology feature flag is disabled by default, thus preventing to set Cluster.Topologies.
   813  
   814  	var (
   815  		tests = []struct {
   816  			name      string
   817  			in        *clusterv1.Cluster
   818  			old       *clusterv1.Cluster
   819  			expectErr bool
   820  		}{
   821  			{
   822  				name:      "should return error when cluster namespace and infrastructure ref namespace mismatch",
   823  				expectErr: true,
   824  				in: builder.Cluster("fooNamespace", "cluster1").
   825  					WithInfrastructureCluster(
   826  						builder.InfrastructureClusterTemplate("barNamespace", "infra1").Build()).
   827  					WithControlPlane(
   828  						builder.ControlPlane("fooNamespace", "cp1").Build()).
   829  					Build(),
   830  			},
   831  			{
   832  				name:      "should return error when cluster namespace and controlPlane ref namespace mismatch",
   833  				expectErr: true,
   834  				in: builder.Cluster("fooNamespace", "cluster1").
   835  					WithInfrastructureCluster(
   836  						builder.InfrastructureClusterTemplate("fooNamespace", "infra1").Build()).
   837  					WithControlPlane(
   838  						builder.ControlPlane("barNamespace", "cp1").Build()).
   839  					Build(),
   840  			},
   841  			{
   842  				name:      "should succeed when namespaces match",
   843  				expectErr: false,
   844  				in: builder.Cluster("fooNamespace", "cluster1").
   845  					WithInfrastructureCluster(
   846  						builder.InfrastructureClusterTemplate("fooNamespace", "infra1").Build()).
   847  					WithControlPlane(
   848  						builder.ControlPlane("fooNamespace", "cp1").Build()).
   849  					Build(),
   850  			},
   851  			{
   852  				name:      "fails if topology is set but feature flag is disabled",
   853  				expectErr: true,
   854  				in: builder.Cluster("fooNamespace", "cluster1").
   855  					WithInfrastructureCluster(
   856  						builder.InfrastructureClusterTemplate("fooNamespace", "infra1").Build()).
   857  					WithControlPlane(
   858  						builder.ControlPlane("fooNamespace", "cp1").Build()).
   859  					WithTopology(&clusterv1.Topology{}).
   860  					Build(),
   861  			},
   862  			{
   863  				name:      "pass with undefined CIDR ranges",
   864  				expectErr: false,
   865  				in: builder.Cluster("fooNamespace", "cluster1").
   866  					WithClusterNetwork(&clusterv1.ClusterNetwork{
   867  						Services: &clusterv1.NetworkRanges{
   868  							CIDRBlocks: []string{}},
   869  						Pods: &clusterv1.NetworkRanges{
   870  							CIDRBlocks: []string{}},
   871  					}).
   872  					Build(),
   873  			},
   874  			{
   875  				name:      "pass with nil CIDR ranges",
   876  				expectErr: false,
   877  				in: builder.Cluster("fooNamespace", "cluster1").
   878  					WithClusterNetwork(&clusterv1.ClusterNetwork{
   879  						Services: &clusterv1.NetworkRanges{
   880  							CIDRBlocks: nil},
   881  						Pods: &clusterv1.NetworkRanges{
   882  							CIDRBlocks: nil},
   883  					}).
   884  					Build(),
   885  			},
   886  			{
   887  				name:      "pass with valid IPv4 CIDR ranges",
   888  				expectErr: false,
   889  				in: builder.Cluster("fooNamespace", "cluster1").
   890  					WithClusterNetwork(&clusterv1.ClusterNetwork{
   891  						Services: &clusterv1.NetworkRanges{
   892  							CIDRBlocks: []string{"10.10.10.10/24"}},
   893  						Pods: &clusterv1.NetworkRanges{
   894  							CIDRBlocks: []string{"10.10.10.10/24"}},
   895  					}).
   896  					Build(),
   897  			},
   898  			{
   899  				name:      "pass with valid IPv6 CIDR ranges",
   900  				expectErr: false,
   901  				in: builder.Cluster("fooNamespace", "cluster1").
   902  					WithClusterNetwork(&clusterv1.ClusterNetwork{
   903  						Services: &clusterv1.NetworkRanges{
   904  							CIDRBlocks: []string{"2004::1234:abcd:ffff:c0a8:101/64"}},
   905  						Pods: &clusterv1.NetworkRanges{
   906  							CIDRBlocks: []string{"2004::1234:abcd:ffff:c0a8:101/64"}},
   907  					}).
   908  					Build(),
   909  			},
   910  			{
   911  				name:      "pass with valid dualstack CIDR ranges",
   912  				expectErr: false,
   913  				in: builder.Cluster("fooNamespace", "cluster1").
   914  					WithClusterNetwork(&clusterv1.ClusterNetwork{
   915  						Services: &clusterv1.NetworkRanges{
   916  							CIDRBlocks: []string{"2004::1234:abcd:ffff:c0a8:101/64", "10.10.10.10/24"}},
   917  						Pods: &clusterv1.NetworkRanges{
   918  							CIDRBlocks: []string{"2004::1234:abcd:ffff:c0a8:101/64", "10.10.10.10/24"}},
   919  					}).
   920  					Build(),
   921  			},
   922  			{
   923  				name:      "pass if multiple CIDR ranges of IPv4 are passed",
   924  				expectErr: false,
   925  				in: builder.Cluster("fooNamespace", "cluster1").
   926  					WithClusterNetwork(&clusterv1.ClusterNetwork{
   927  						Services: &clusterv1.NetworkRanges{
   928  							CIDRBlocks: []string{"10.10.10.10/24", "11.11.11.11/24"}},
   929  					}).
   930  					Build(),
   931  			},
   932  			{
   933  				name:      "pass if multiple CIDR ranges of IPv6 are passed",
   934  				expectErr: false,
   935  				in: builder.Cluster("fooNamespace", "cluster1").
   936  					WithClusterNetwork(&clusterv1.ClusterNetwork{
   937  						Services: &clusterv1.NetworkRanges{
   938  							CIDRBlocks: []string{"2002::1234:abcd:ffff:c0a8:101/64", "2004::1234:abcd:ffff:c0a8:101/64"}},
   939  					}).
   940  					Build(),
   941  			},
   942  			{
   943  				name:      "pass if too many cidr ranges are specified in the clusterNetwork pods field",
   944  				expectErr: false,
   945  				in: builder.Cluster("fooNamespace", "cluster1").
   946  					WithClusterNetwork(&clusterv1.ClusterNetwork{
   947  						Pods: &clusterv1.NetworkRanges{
   948  							CIDRBlocks: []string{"10.10.10.10/24", "11.11.11.11/24", "12.12.12.12/24"}}}).
   949  					Build(),
   950  			},
   951  			{
   952  				name:      "fails if service cidr ranges are not valid",
   953  				expectErr: true,
   954  				in: builder.Cluster("fooNamespace", "cluster1").
   955  					WithClusterNetwork(&clusterv1.ClusterNetwork{
   956  						Services: &clusterv1.NetworkRanges{
   957  							// Invalid ranges: missing network suffix
   958  							CIDRBlocks: []string{"10.10.10.10", "11.11.11.11"}}}).
   959  					Build(),
   960  			},
   961  			{
   962  				name:      "fails if pod cidr ranges are not valid",
   963  				expectErr: true,
   964  				in: builder.Cluster("fooNamespace", "cluster1").
   965  					WithClusterNetwork(&clusterv1.ClusterNetwork{
   966  						Pods: &clusterv1.NetworkRanges{
   967  							// Invalid ranges: missing network suffix
   968  							CIDRBlocks: []string{"10.10.10.10", "11.11.11.11"}}}).
   969  					Build(),
   970  			},
   971  			{
   972  				name:      "pass with name of under 63 characters",
   973  				expectErr: false,
   974  				in:        builder.Cluster("fooNamespace", "short-name").Build(),
   975  			},
   976  			{
   977  				name:      "pass with _, -, . characters in name",
   978  				in:        builder.Cluster("fooNamespace", "thisNameContains.A_Non-Alphanumeric").Build(),
   979  				expectErr: false,
   980  			},
   981  			{
   982  				name:      "fails if cluster name is longer than 63 characters",
   983  				in:        builder.Cluster("fooNamespace", "thisNameIsReallyMuchLongerThanTheMaximumLengthOfSixtyThreeCharacters").Build(),
   984  				expectErr: true,
   985  			},
   986  			{
   987  				name:      "error when name starts with NonAlphanumeric character",
   988  				in:        builder.Cluster("fooNamespace", "-thisNameStartsWithANonAlphanumeric").Build(),
   989  				expectErr: true,
   990  			},
   991  			{
   992  				name:      "error when name ends with NonAlphanumeric character",
   993  				in:        builder.Cluster("fooNamespace", "thisNameEndsWithANonAlphanumeric.").Build(),
   994  				expectErr: true,
   995  			},
   996  			{
   997  				name:      "error when name contains invalid NonAlphanumeric character",
   998  				in:        builder.Cluster("fooNamespace", "thisNameContainsInvalid!@NonAlphanumerics").Build(),
   999  				expectErr: true,
  1000  			},
  1001  		}
  1002  	)
  1003  	for _, tt := range tests {
  1004  		t.Run(tt.name, func(t *testing.T) {
  1005  			g := NewWithT(t)
  1006  
  1007  			// Create the webhook.
  1008  			webhook := &Cluster{}
  1009  
  1010  			warnings, err := webhook.validate(ctx, tt.old, tt.in)
  1011  			g.Expect(warnings).To(BeEmpty())
  1012  			if tt.expectErr {
  1013  				g.Expect(err).To(HaveOccurred())
  1014  				return
  1015  			}
  1016  			g.Expect(err).ToNot(HaveOccurred())
  1017  		})
  1018  	}
  1019  }
  1020  
  1021  func TestClusterTopologyValidation(t *testing.T) {
  1022  	// NOTE: ClusterTopology feature flag is disabled by default, thus preventing to set Cluster.Topologies.
  1023  	// Enabling the feature flag temporarily for this test.
  1024  	defer utilfeature.SetFeatureGateDuringTest(t, feature.Gates, feature.ClusterTopology, true)()
  1025  
  1026  	tests := []struct {
  1027  		name                        string
  1028  		clusterClassStatusVariables []clusterv1.ClusterClassStatusVariable
  1029  		in                          *clusterv1.Cluster
  1030  		old                         *clusterv1.Cluster
  1031  		expectErr                   bool
  1032  	}{
  1033  		{
  1034  			name:      "should return error when topology does not have class",
  1035  			expectErr: true,
  1036  			in: builder.Cluster("fooboo", "cluster1").
  1037  				WithTopology(&clusterv1.Topology{}).
  1038  				Build(),
  1039  		},
  1040  		{
  1041  			name:      "should return error when topology does not have valid version",
  1042  			expectErr: true,
  1043  			in: builder.Cluster("fooboo", "cluster1").
  1044  				WithTopology(builder.ClusterTopology().
  1045  					WithClass("foo").
  1046  					WithVersion("invalid").Build()).
  1047  				Build(),
  1048  		},
  1049  		{
  1050  			name:      "should return error when downgrading topology version - major",
  1051  			expectErr: true,
  1052  			old: builder.Cluster("fooboo", "cluster1").
  1053  				WithTopology(builder.ClusterTopology().
  1054  					WithClass("foo").
  1055  					WithVersion("v2.2.3").
  1056  					Build()).
  1057  				Build(),
  1058  			in: builder.Cluster("fooboo", "cluster1").
  1059  				WithTopology(builder.ClusterTopology().
  1060  					WithClass("foo").
  1061  					WithVersion("v1.2.3").
  1062  					Build()).
  1063  				Build(),
  1064  		},
  1065  		{
  1066  			name:      "should return error when downgrading topology version - minor",
  1067  			expectErr: true,
  1068  			old: builder.Cluster("fooboo", "cluster1").
  1069  				WithTopology(builder.ClusterTopology().
  1070  					WithClass("foo").
  1071  					WithVersion("v1.2.3").
  1072  					Build()).
  1073  				Build(),
  1074  			in: builder.Cluster("fooboo", "cluster1").
  1075  				WithTopology(builder.ClusterTopology().
  1076  					WithClass("foo").
  1077  					WithVersion("v1.1.3").
  1078  					Build()).
  1079  				Build(),
  1080  		},
  1081  		{
  1082  			name:      "should return error when downgrading topology version - patch",
  1083  			expectErr: true,
  1084  			old: builder.Cluster("fooboo", "cluster1").
  1085  				WithTopology(builder.ClusterTopology().
  1086  					WithClass("foo").
  1087  					WithVersion("v1.2.3").
  1088  					Build()).
  1089  				Build(),
  1090  			in: builder.Cluster("fooboo", "cluster1").
  1091  				WithTopology(builder.ClusterTopology().
  1092  					WithClass("foo").
  1093  					WithVersion("v1.2.2").
  1094  					Build()).
  1095  				Build(),
  1096  		},
  1097  		{
  1098  			name:      "should return error when downgrading topology version - pre-release",
  1099  			expectErr: true,
  1100  			old: builder.Cluster("fooboo", "cluster1").
  1101  				WithTopology(builder.ClusterTopology().
  1102  					WithClass("foo").
  1103  					WithVersion("v1.2.3-xyz.2").
  1104  					Build()).
  1105  				Build(),
  1106  			in: builder.Cluster("fooboo", "cluster1").
  1107  				WithTopology(builder.ClusterTopology().
  1108  					WithClass("foo").
  1109  					WithVersion("v1.2.3-xyz.1").
  1110  					Build()).
  1111  				Build(),
  1112  		},
  1113  		{
  1114  			name:      "should return error when downgrading topology version - build tag",
  1115  			expectErr: true,
  1116  			old: builder.Cluster("fooboo", "cluster1").
  1117  				WithTopology(builder.ClusterTopology().
  1118  					WithClass("foo").
  1119  					WithVersion("v1.2.3+xyz.2").
  1120  					Build()).
  1121  				Build(),
  1122  			in: builder.Cluster("fooboo", "cluster1").
  1123  				WithTopology(builder.ClusterTopology().
  1124  					WithClass("foo").
  1125  					WithVersion("v1.2.3+xyz.1").
  1126  					Build()).
  1127  				Build(),
  1128  		},
  1129  		{
  1130  			name:      "should return error when upgrading +2 minor version",
  1131  			expectErr: true,
  1132  			old: builder.Cluster("fooboo", "cluster1").
  1133  				WithTopology(builder.ClusterTopology().
  1134  					WithClass("foo").
  1135  					WithVersion("v1.2.3").
  1136  					Build()).
  1137  				Build(),
  1138  			in: builder.Cluster("fooboo", "cluster1").
  1139  				WithTopology(builder.ClusterTopology().
  1140  					WithClass("foo").
  1141  					WithVersion("v1.4.0").
  1142  					Build()).
  1143  				Build(),
  1144  		},
  1145  		{
  1146  			name:      "should return error when duplicated MachineDeployments names exists in a Topology",
  1147  			expectErr: true,
  1148  			in: builder.Cluster("fooboo", "cluster1").
  1149  				WithTopology(builder.ClusterTopology().
  1150  					WithClass("foo").
  1151  					WithVersion("v1.19.1").
  1152  					WithMachineDeployment(
  1153  						builder.MachineDeploymentTopology("workers1").
  1154  							WithClass("aa").
  1155  							Build()).
  1156  					WithMachineDeployment(
  1157  						builder.MachineDeploymentTopology("workers1").
  1158  							WithClass("bb").
  1159  							Build()).
  1160  					Build()).
  1161  				Build(),
  1162  		},
  1163  		{
  1164  			name:      "should pass when MachineDeployments names in a Topology are unique",
  1165  			expectErr: false,
  1166  			in: builder.Cluster("fooboo", "cluster1").
  1167  				WithTopology(builder.ClusterTopology().
  1168  					WithClass("foo").
  1169  					WithVersion("v1.19.1").
  1170  					WithMachineDeployment(
  1171  						builder.MachineDeploymentTopology("workers1").
  1172  							WithClass("aa").
  1173  							Build()).
  1174  					WithMachineDeployment(
  1175  						builder.MachineDeploymentTopology("workers2").
  1176  							WithClass("bb").
  1177  							Build()).
  1178  					Build()).
  1179  				Build(),
  1180  		},
  1181  		{
  1182  			name:      "should update",
  1183  			expectErr: false,
  1184  			old: builder.Cluster("fooboo", "cluster1").
  1185  				WithTopology(builder.ClusterTopology().
  1186  					WithClass("foo").
  1187  					WithVersion("v1.19.1").
  1188  					WithMachineDeployment(
  1189  						builder.MachineDeploymentTopology("workers1").
  1190  							WithClass("aa").
  1191  							Build()).
  1192  					WithMachineDeployment(
  1193  						builder.MachineDeploymentTopology("workers2").
  1194  							WithClass("bb").
  1195  							Build()).
  1196  					Build()).
  1197  				Build(),
  1198  			in: builder.Cluster("fooboo", "cluster1").
  1199  				WithTopology(builder.ClusterTopology().
  1200  					WithClass("foo").
  1201  					WithVersion("v1.19.2").
  1202  					WithMachineDeployment(
  1203  						builder.MachineDeploymentTopology("workers1").
  1204  							WithClass("aa").
  1205  							Build()).
  1206  					WithMachineDeployment(
  1207  						builder.MachineDeploymentTopology("workers2").
  1208  							WithClass("bb").
  1209  							Build()).
  1210  					Build()).
  1211  				Build(),
  1212  		},
  1213  		{
  1214  			name:      "should return error when upgrade concurrency annotation value is < 1",
  1215  			expectErr: true,
  1216  			in: builder.Cluster("fooboo", "cluster1").
  1217  				WithAnnotations(map[string]string{
  1218  					clusterv1.ClusterTopologyUpgradeConcurrencyAnnotation: "-1",
  1219  				}).
  1220  				WithTopology(builder.ClusterTopology().
  1221  					WithClass("foo").
  1222  					WithVersion("v1.19.2").
  1223  					Build()).
  1224  				Build(),
  1225  		},
  1226  		{
  1227  			name:      "should return error when upgrade concurrency annotation value is not numeric",
  1228  			expectErr: true,
  1229  			in: builder.Cluster("fooboo", "cluster1").
  1230  				WithAnnotations(map[string]string{
  1231  					clusterv1.ClusterTopologyUpgradeConcurrencyAnnotation: "abc",
  1232  				}).
  1233  				WithTopology(builder.ClusterTopology().
  1234  					WithClass("foo").
  1235  					WithVersion("v1.19.2").
  1236  					Build()).
  1237  				Build(),
  1238  		},
  1239  		{
  1240  			name:      "should pass upgrade concurrency annotation value is >= 1",
  1241  			expectErr: false,
  1242  			in: builder.Cluster("fooboo", "cluster1").
  1243  				WithAnnotations(map[string]string{
  1244  					clusterv1.ClusterTopologyUpgradeConcurrencyAnnotation: "2",
  1245  				}).
  1246  				WithTopology(builder.ClusterTopology().
  1247  					WithClass("foo").
  1248  					WithVersion("v1.19.2").
  1249  					Build()).
  1250  				Build(),
  1251  		},
  1252  	}
  1253  	for _, tt := range tests {
  1254  		t.Run(tt.name, func(t *testing.T) {
  1255  			g := NewWithT(t)
  1256  			class := builder.ClusterClass("fooboo", "foo").
  1257  				WithWorkerMachineDeploymentClasses(
  1258  					*builder.MachineDeploymentClass("bb").Build(),
  1259  					*builder.MachineDeploymentClass("aa").Build(),
  1260  				).
  1261  				WithStatusVariables(tt.clusterClassStatusVariables...).
  1262  				Build()
  1263  
  1264  			// Mark this condition to true so the webhook sees the ClusterClass as up to date.
  1265  			conditions.MarkTrue(class, clusterv1.ClusterClassVariablesReconciledCondition)
  1266  			// Sets up the fakeClient for the test case.
  1267  			fakeClient := fake.NewClientBuilder().
  1268  				WithObjects(class).
  1269  				WithScheme(fakeScheme).
  1270  				Build()
  1271  
  1272  			// Create the webhook and add the fakeClient as its client. This is required because the test uses a Managed Topology.
  1273  			webhook := &Cluster{Client: fakeClient}
  1274  
  1275  			warnings, err := webhook.validate(ctx, tt.old, tt.in)
  1276  			if tt.expectErr {
  1277  				g.Expect(err).To(HaveOccurred())
  1278  				g.Expect(warnings).To(BeEmpty())
  1279  				return
  1280  			}
  1281  			g.Expect(err).ToNot(HaveOccurred())
  1282  			g.Expect(warnings).To(BeEmpty())
  1283  		})
  1284  	}
  1285  }
  1286  
  1287  // TestClusterTopologyValidationWithClient tests the additional cases introduced in new validation in the webhook package.
  1288  func TestClusterTopologyValidationWithClient(t *testing.T) {
  1289  	defer utilfeature.SetFeatureGateDuringTest(t, feature.Gates, feature.ClusterTopology, true)()
  1290  	g := NewWithT(t)
  1291  
  1292  	tests := []struct {
  1293  		name            string
  1294  		cluster         *clusterv1.Cluster
  1295  		class           *clusterv1.ClusterClass
  1296  		classReconciled bool
  1297  		objects         []client.Object
  1298  		wantErr         bool
  1299  		wantWarnings    bool
  1300  	}{
  1301  		{
  1302  			name: "Accept a cluster with an existing ClusterClass named in cluster.spec.topology.class",
  1303  			cluster: builder.Cluster(metav1.NamespaceDefault, "cluster1").
  1304  				WithTopology(
  1305  					builder.ClusterTopology().
  1306  						WithClass("clusterclass").
  1307  						WithVersion("v1.22.2").
  1308  						WithControlPlaneReplicas(3).
  1309  						Build()).
  1310  				Build(),
  1311  			class: builder.ClusterClass(metav1.NamespaceDefault, "clusterclass").
  1312  				Build(),
  1313  			classReconciled: true,
  1314  			wantErr:         false,
  1315  		},
  1316  		{
  1317  			name: "Warning for a cluster with non-existent ClusterClass referenced cluster.spec.topology.class",
  1318  			cluster: builder.Cluster(metav1.NamespaceDefault, "cluster1").
  1319  				WithTopology(
  1320  					builder.ClusterTopology().
  1321  						WithClass("wrongName").
  1322  						WithVersion("v1.22.2").
  1323  						WithControlPlaneReplicas(3).
  1324  						Build()).
  1325  				Build(),
  1326  			class: builder.ClusterClass(metav1.NamespaceDefault, "clusterclass").
  1327  				Build(),
  1328  			// There should be a warning for a ClusterClass which can not be found.
  1329  			wantWarnings: true,
  1330  			wantErr:      false,
  1331  		},
  1332  		{
  1333  			name: "Warning for a cluster with an unreconciled ClusterClass named in cluster.spec.topology.class",
  1334  			cluster: builder.Cluster(metav1.NamespaceDefault, "cluster1").
  1335  				WithTopology(
  1336  					builder.ClusterTopology().
  1337  						WithClass("clusterclass").
  1338  						WithVersion("v1.22.2").
  1339  						WithControlPlaneReplicas(3).
  1340  						Build()).
  1341  				Build(),
  1342  			class: builder.ClusterClass(metav1.NamespaceDefault, "clusterclass").
  1343  				Build(),
  1344  			classReconciled: false,
  1345  			// There should be a warning for a ClusterClass which is not yet reconciled.
  1346  			wantWarnings: true,
  1347  			wantErr:      false,
  1348  		},
  1349  		{
  1350  			name: "Reject a cluster that has MHC enabled for control plane but is missing MHC definition in cluster topology and clusterclass",
  1351  			cluster: builder.Cluster(metav1.NamespaceDefault, "cluster1").
  1352  				WithTopology(
  1353  					builder.ClusterTopology().
  1354  						WithClass("clusterclass").
  1355  						WithVersion("v1.22.2").
  1356  						WithControlPlaneReplicas(3).
  1357  						WithControlPlaneMachineHealthCheck(&clusterv1.MachineHealthCheckTopology{
  1358  							Enable: pointer.Bool(true),
  1359  						}).
  1360  						Build()).
  1361  				Build(),
  1362  			class: builder.ClusterClass(metav1.NamespaceDefault, "clusterclass").
  1363  				Build(),
  1364  			classReconciled: true,
  1365  			wantErr:         true,
  1366  		},
  1367  		{
  1368  			name: "Reject a cluster that MHC override defined for control plane but is missing unhealthy conditions",
  1369  			cluster: builder.Cluster(metav1.NamespaceDefault, "cluster1").
  1370  				WithTopology(
  1371  					builder.ClusterTopology().
  1372  						WithClass("clusterclass").
  1373  						WithVersion("v1.22.2").
  1374  						WithControlPlaneReplicas(3).
  1375  						WithControlPlaneMachineHealthCheck(&clusterv1.MachineHealthCheckTopology{
  1376  							MachineHealthCheckClass: clusterv1.MachineHealthCheckClass{
  1377  								UnhealthyConditions: []clusterv1.UnhealthyCondition{},
  1378  							},
  1379  						}).
  1380  						Build()).
  1381  				Build(),
  1382  			class: builder.ClusterClass(metav1.NamespaceDefault, "clusterclass").
  1383  				Build(),
  1384  			classReconciled: true,
  1385  			wantErr:         true,
  1386  		},
  1387  		{
  1388  			name: "Reject a cluster that MHC override defined for control plane but is set when control plane is missing machineInfrastructure",
  1389  			cluster: builder.Cluster(metav1.NamespaceDefault, "cluster1").
  1390  				WithTopology(
  1391  					builder.ClusterTopology().
  1392  						WithClass("clusterclass").
  1393  						WithVersion("v1.22.2").
  1394  						WithControlPlaneReplicas(3).
  1395  						WithControlPlaneMachineHealthCheck(&clusterv1.MachineHealthCheckTopology{
  1396  							MachineHealthCheckClass: clusterv1.MachineHealthCheckClass{
  1397  								UnhealthyConditions: []clusterv1.UnhealthyCondition{
  1398  									{
  1399  										Type:   corev1.NodeReady,
  1400  										Status: corev1.ConditionFalse,
  1401  									},
  1402  								},
  1403  							},
  1404  						}).
  1405  						Build()).
  1406  				Build(),
  1407  			class: builder.ClusterClass(metav1.NamespaceDefault, "clusterclass").
  1408  				Build(),
  1409  			classReconciled: true,
  1410  			wantErr:         true,
  1411  		},
  1412  		{
  1413  			name: "Accept a cluster that has MHC enabled for control plane with control plane MHC defined in ClusterClass",
  1414  			cluster: builder.Cluster(metav1.NamespaceDefault, "cluster1").
  1415  				WithTopology(
  1416  					builder.ClusterTopology().
  1417  						WithClass("clusterclass").
  1418  						WithVersion("v1.22.2").
  1419  						WithControlPlaneReplicas(3).
  1420  						WithControlPlaneMachineHealthCheck(&clusterv1.MachineHealthCheckTopology{
  1421  							Enable: pointer.Bool(true),
  1422  						}).
  1423  						Build()).
  1424  				Build(),
  1425  			class: builder.ClusterClass(metav1.NamespaceDefault, "clusterclass").
  1426  				WithControlPlaneMachineHealthCheck(&clusterv1.MachineHealthCheckClass{}).
  1427  				Build(),
  1428  			classReconciled: true,
  1429  			wantErr:         false,
  1430  		},
  1431  		{
  1432  			name: "Accept a cluster that has MHC enabled for control plane with control plane MHC defined in cluster topology",
  1433  			cluster: builder.Cluster(metav1.NamespaceDefault, "cluster1").
  1434  				WithTopology(
  1435  					builder.ClusterTopology().
  1436  						WithClass("clusterclass").
  1437  						WithVersion("v1.22.2").
  1438  						WithControlPlaneReplicas(3).
  1439  						WithControlPlaneMachineHealthCheck(&clusterv1.MachineHealthCheckTopology{
  1440  							Enable: pointer.Bool(true),
  1441  							MachineHealthCheckClass: clusterv1.MachineHealthCheckClass{
  1442  								UnhealthyConditions: []clusterv1.UnhealthyCondition{
  1443  									{
  1444  										Type:   corev1.NodeReady,
  1445  										Status: corev1.ConditionFalse,
  1446  									},
  1447  								},
  1448  							},
  1449  						}).
  1450  						Build()).
  1451  				Build(),
  1452  			class: builder.ClusterClass(metav1.NamespaceDefault, "clusterclass").
  1453  				WithControlPlaneInfrastructureMachineTemplate(&unstructured.Unstructured{}).
  1454  				Build(),
  1455  			classReconciled: true,
  1456  			wantErr:         false,
  1457  		},
  1458  		{
  1459  			name: "Reject a cluster that has MHC enabled for machine deployment but is missing MHC definition in cluster topology and ClusterClass",
  1460  			cluster: builder.Cluster(metav1.NamespaceDefault, "cluster1").
  1461  				WithTopology(
  1462  					builder.ClusterTopology().
  1463  						WithClass("clusterclass").
  1464  						WithVersion("v1.22.2").
  1465  						WithControlPlaneReplicas(3).
  1466  						WithMachineDeployment(
  1467  							builder.MachineDeploymentTopology("md1").
  1468  								WithClass("worker-class").
  1469  								WithMachineHealthCheck(&clusterv1.MachineHealthCheckTopology{
  1470  									Enable: pointer.Bool(true),
  1471  								}).
  1472  								Build(),
  1473  						).
  1474  						Build()).
  1475  				Build(),
  1476  			class: builder.ClusterClass(metav1.NamespaceDefault, "clusterclass").
  1477  				WithWorkerMachineDeploymentClasses(
  1478  					*builder.MachineDeploymentClass("worker-class").Build(),
  1479  				).
  1480  				Build(),
  1481  			classReconciled: true,
  1482  			wantErr:         true,
  1483  		},
  1484  		{
  1485  			name: "Reject a cluster that has MHC override defined for machine deployment but is missing unhealthy conditions",
  1486  			cluster: builder.Cluster(metav1.NamespaceDefault, "cluster1").
  1487  				WithTopology(
  1488  					builder.ClusterTopology().
  1489  						WithClass("clusterclass").
  1490  						WithVersion("v1.22.2").
  1491  						WithControlPlaneReplicas(3).
  1492  						WithMachineDeployment(
  1493  							builder.MachineDeploymentTopology("md1").
  1494  								WithClass("worker-class").
  1495  								WithMachineHealthCheck(&clusterv1.MachineHealthCheckTopology{
  1496  									MachineHealthCheckClass: clusterv1.MachineHealthCheckClass{
  1497  										UnhealthyConditions: []clusterv1.UnhealthyCondition{},
  1498  									},
  1499  								}).
  1500  								Build(),
  1501  						).
  1502  						Build()).
  1503  				Build(),
  1504  			class: builder.ClusterClass(metav1.NamespaceDefault, "clusterclass").
  1505  				WithWorkerMachineDeploymentClasses(
  1506  					*builder.MachineDeploymentClass("worker-class").Build(),
  1507  				).
  1508  				Build(),
  1509  			classReconciled: true,
  1510  			wantErr:         true,
  1511  		},
  1512  		{
  1513  			name: "Accept a cluster that has MHC enabled for machine deployment with machine deployment MHC defined in ClusterClass",
  1514  			cluster: builder.Cluster(metav1.NamespaceDefault, "cluster1").
  1515  				WithTopology(
  1516  					builder.ClusterTopology().
  1517  						WithClass("clusterclass").
  1518  						WithVersion("v1.22.2").
  1519  						WithControlPlaneReplicas(3).
  1520  						WithMachineDeployment(
  1521  							builder.MachineDeploymentTopology("md1").
  1522  								WithClass("worker-class").
  1523  								WithMachineHealthCheck(&clusterv1.MachineHealthCheckTopology{
  1524  									Enable: pointer.Bool(true),
  1525  								}).
  1526  								Build(),
  1527  						).
  1528  						Build()).
  1529  				Build(),
  1530  			class: builder.ClusterClass(metav1.NamespaceDefault, "clusterclass").
  1531  				WithWorkerMachineDeploymentClasses(
  1532  					*builder.MachineDeploymentClass("worker-class").
  1533  						WithMachineHealthCheckClass(&clusterv1.MachineHealthCheckClass{}).
  1534  						Build(),
  1535  				).
  1536  				Build(),
  1537  			classReconciled: true,
  1538  			wantErr:         false,
  1539  		},
  1540  		{
  1541  			name: "Accept a cluster that has MHC enabled for machine deployment with machine deployment MHC defined in cluster topology",
  1542  			cluster: builder.Cluster(metav1.NamespaceDefault, "cluster1").
  1543  				WithTopology(
  1544  					builder.ClusterTopology().
  1545  						WithClass("clusterclass").
  1546  						WithVersion("v1.22.2").
  1547  						WithControlPlaneReplicas(3).
  1548  						WithMachineDeployment(
  1549  							builder.MachineDeploymentTopology("md1").
  1550  								WithClass("worker-class").
  1551  								WithMachineHealthCheck(&clusterv1.MachineHealthCheckTopology{
  1552  									Enable: pointer.Bool(true),
  1553  									MachineHealthCheckClass: clusterv1.MachineHealthCheckClass{
  1554  										UnhealthyConditions: []clusterv1.UnhealthyCondition{
  1555  											{
  1556  												Type:   corev1.NodeReady,
  1557  												Status: corev1.ConditionFalse,
  1558  											},
  1559  										},
  1560  									},
  1561  								}).
  1562  								Build(),
  1563  						).
  1564  						Build()).
  1565  				Build(),
  1566  			class: builder.ClusterClass(metav1.NamespaceDefault, "clusterclass").
  1567  				WithWorkerMachineDeploymentClasses(
  1568  					*builder.MachineDeploymentClass("worker-class").Build(),
  1569  				).
  1570  				Build(),
  1571  			classReconciled: true,
  1572  			wantErr:         false,
  1573  		},
  1574  	}
  1575  	for _, tt := range tests {
  1576  		t.Run(tt.name, func(t *testing.T) {
  1577  			// Mark this condition to true so the webhook sees the ClusterClass as up to date.
  1578  			if tt.classReconciled {
  1579  				conditions.MarkTrue(tt.class, clusterv1.ClusterClassVariablesReconciledCondition)
  1580  			}
  1581  			// Sets up the fakeClient for the test case.
  1582  			fakeClient := fake.NewClientBuilder().
  1583  				WithObjects(tt.class).
  1584  				WithScheme(fakeScheme).
  1585  				Build()
  1586  
  1587  			// Create the webhook and add the fakeClient as its client. This is required because the test uses a Managed Topology.
  1588  			c := &Cluster{Client: fakeClient}
  1589  
  1590  			// Checks the return error.
  1591  			warnings, err := c.ValidateCreate(ctx, tt.cluster)
  1592  			if tt.wantErr {
  1593  				g.Expect(err).To(HaveOccurred())
  1594  			} else {
  1595  				g.Expect(err).ToNot(HaveOccurred())
  1596  			}
  1597  			if tt.wantWarnings {
  1598  				g.Expect(warnings).ToNot(BeEmpty())
  1599  			} else {
  1600  				g.Expect(warnings).To(BeEmpty())
  1601  			}
  1602  		})
  1603  	}
  1604  }
  1605  
  1606  // TestClusterTopologyValidationForTopologyClassChange cases where cluster.spec.topology.class is altered.
  1607  func TestClusterTopologyValidationForTopologyClassChange(t *testing.T) {
  1608  	defer utilfeature.SetFeatureGateDuringTest(t, feature.Gates, feature.ClusterTopology, true)()
  1609  	g := NewWithT(t)
  1610  
  1611  	cluster := builder.Cluster(metav1.NamespaceDefault, "cluster1").
  1612  		WithTopology(
  1613  			builder.ClusterTopology().
  1614  				WithClass("class1").
  1615  				WithVersion("v1.22.2").
  1616  				WithControlPlaneReplicas(3).
  1617  				Build()).
  1618  		Build()
  1619  
  1620  	ref := &corev1.ObjectReference{
  1621  		APIVersion: "group.test.io/foo",
  1622  		Kind:       "barTemplate",
  1623  		Name:       "baz",
  1624  		Namespace:  "default",
  1625  	}
  1626  	compatibleNameChangeRef := &corev1.ObjectReference{
  1627  		APIVersion: "group.test.io/foo",
  1628  		Kind:       "barTemplate",
  1629  		Name:       "differentbaz",
  1630  		Namespace:  "default",
  1631  	}
  1632  	compatibleAPIVersionChangeRef := &corev1.ObjectReference{
  1633  		APIVersion: "group.test.io/foo2",
  1634  		Kind:       "barTemplate",
  1635  		Name:       "differentbaz",
  1636  		Namespace:  "default",
  1637  	}
  1638  	incompatibleKindRef := &corev1.ObjectReference{
  1639  		APIVersion: "group.test.io/foo",
  1640  		Kind:       "another-barTemplate",
  1641  		Name:       "another-baz",
  1642  		Namespace:  "default",
  1643  	}
  1644  	incompatibleAPIGroupRef := &corev1.ObjectReference{
  1645  		APIVersion: "group.nottest.io/foo",
  1646  		Kind:       "barTemplate",
  1647  		Name:       "another-baz",
  1648  		Namespace:  "default",
  1649  	}
  1650  
  1651  	tests := []struct {
  1652  		name        string
  1653  		cluster     *clusterv1.Cluster
  1654  		firstClass  *clusterv1.ClusterClass
  1655  		secondClass *clusterv1.ClusterClass
  1656  		wantErr     bool
  1657  	}{
  1658  		// InfrastructureCluster changes.
  1659  		{
  1660  			name: "Accept cluster.topology.class change with a compatible infrastructureCluster Kind ref change",
  1661  			firstClass: builder.ClusterClass(metav1.NamespaceDefault, "class1").
  1662  				WithInfrastructureClusterTemplate(refToUnstructured(ref)).
  1663  				WithControlPlaneTemplate(refToUnstructured(ref)).
  1664  				WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)).
  1665  				Build(),
  1666  			secondClass: builder.ClusterClass(metav1.NamespaceDefault, "class2").
  1667  				WithInfrastructureClusterTemplate(refToUnstructured(compatibleNameChangeRef)).
  1668  				WithControlPlaneTemplate(refToUnstructured(ref)).
  1669  				WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)).
  1670  				Build(),
  1671  			wantErr: false,
  1672  		},
  1673  		{
  1674  			name: "Accept cluster.topology.class change with a compatible infrastructureCluster APIVersion ref change",
  1675  			firstClass: builder.ClusterClass(metav1.NamespaceDefault, "class1").
  1676  				WithInfrastructureClusterTemplate(refToUnstructured(ref)).
  1677  				WithControlPlaneTemplate(refToUnstructured(ref)).
  1678  				WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)).
  1679  				Build(),
  1680  			secondClass: builder.ClusterClass(metav1.NamespaceDefault, "class2").
  1681  				WithInfrastructureClusterTemplate(refToUnstructured(compatibleAPIVersionChangeRef)).
  1682  				WithControlPlaneTemplate(refToUnstructured(ref)).
  1683  				WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)).
  1684  				Build(),
  1685  			wantErr: false,
  1686  		},
  1687  
  1688  		{
  1689  			name: "Reject cluster.topology.class change with an incompatible infrastructureCluster Kind ref change",
  1690  			firstClass: builder.ClusterClass(metav1.NamespaceDefault, "class1").
  1691  				WithInfrastructureClusterTemplate(refToUnstructured(ref)).
  1692  				WithControlPlaneTemplate(refToUnstructured(ref)).
  1693  				WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)).
  1694  				Build(),
  1695  			secondClass: builder.ClusterClass(metav1.NamespaceDefault, "class2").
  1696  				WithInfrastructureClusterTemplate(refToUnstructured(incompatibleKindRef)).
  1697  				WithControlPlaneTemplate(refToUnstructured(ref)).
  1698  				WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)).
  1699  				Build(),
  1700  			wantErr: true,
  1701  		},
  1702  		{
  1703  			name: "Reject cluster.topology.class change with an incompatible infrastructureCluster APIGroup ref change",
  1704  			firstClass: builder.ClusterClass(metav1.NamespaceDefault, "class1").
  1705  				WithInfrastructureClusterTemplate(refToUnstructured(ref)).
  1706  				WithControlPlaneTemplate(refToUnstructured(ref)).
  1707  				WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)).
  1708  				Build(),
  1709  			secondClass: builder.ClusterClass(metav1.NamespaceDefault, "class2").
  1710  				WithInfrastructureClusterTemplate(refToUnstructured(incompatibleAPIGroupRef)).
  1711  				WithControlPlaneTemplate(refToUnstructured(ref)).
  1712  				WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)).
  1713  				Build(),
  1714  			wantErr: true,
  1715  		},
  1716  
  1717  		// ControlPlane changes.
  1718  		{
  1719  			name: "Accept cluster.topology.class change with a compatible controlPlaneTemplate ref change",
  1720  			firstClass: builder.ClusterClass(metav1.NamespaceDefault, "class1").
  1721  				WithInfrastructureClusterTemplate(refToUnstructured(ref)).
  1722  				WithControlPlaneTemplate(refToUnstructured(ref)).
  1723  				WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)).
  1724  				Build(),
  1725  			secondClass: builder.ClusterClass(metav1.NamespaceDefault, "class2").
  1726  				WithInfrastructureClusterTemplate(refToUnstructured(ref)).
  1727  				WithControlPlaneTemplate(refToUnstructured(compatibleNameChangeRef)).
  1728  				WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)).
  1729  				Build(),
  1730  			wantErr: false,
  1731  		},
  1732  		{
  1733  			name: "Accept cluster.topology.class change with a compatible controlPlaneTemplate ref change",
  1734  			firstClass: builder.ClusterClass(metav1.NamespaceDefault, "class1").
  1735  				WithInfrastructureClusterTemplate(refToUnstructured(ref)).
  1736  				WithControlPlaneTemplate(refToUnstructured(ref)).
  1737  				WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)).
  1738  				Build(),
  1739  			secondClass: builder.ClusterClass(metav1.NamespaceDefault, "class2").
  1740  				WithInfrastructureClusterTemplate(refToUnstructured(ref)).
  1741  				WithControlPlaneTemplate(refToUnstructured(compatibleAPIVersionChangeRef)).
  1742  				WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)).
  1743  				Build(),
  1744  			wantErr: false,
  1745  		},
  1746  
  1747  		{
  1748  			name: "Reject cluster.topology.class change with an incompatible controlPlane Kind ref change",
  1749  			firstClass: builder.ClusterClass(metav1.NamespaceDefault, "class1").
  1750  				WithInfrastructureClusterTemplate(refToUnstructured(ref)).
  1751  				WithControlPlaneTemplate(refToUnstructured(ref)).
  1752  				WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)).
  1753  				Build(),
  1754  			secondClass: builder.ClusterClass(metav1.NamespaceDefault, "class2").
  1755  				WithInfrastructureClusterTemplate(refToUnstructured(incompatibleKindRef)).
  1756  				WithControlPlaneTemplate(refToUnstructured(ref)).
  1757  				WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)).
  1758  				Build(),
  1759  			wantErr: true,
  1760  		},
  1761  		{
  1762  			name: "Reject cluster.topology.class change with an incompatible controlPlane APIVersion ref change",
  1763  			firstClass: builder.ClusterClass(metav1.NamespaceDefault, "class1").
  1764  				WithInfrastructureClusterTemplate(refToUnstructured(ref)).
  1765  				WithControlPlaneTemplate(refToUnstructured(ref)).
  1766  				WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)).
  1767  				Build(),
  1768  			secondClass: builder.ClusterClass(metav1.NamespaceDefault, "class2").
  1769  				WithInfrastructureClusterTemplate(refToUnstructured(ref)).
  1770  				WithControlPlaneTemplate(refToUnstructured(incompatibleAPIGroupRef)).
  1771  				WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(compatibleNameChangeRef)).
  1772  				Build(),
  1773  			wantErr: true,
  1774  		},
  1775  		{
  1776  			name: "Accept cluster.topology.class change with a compatible controlPlane.MachineInfrastructure ref change",
  1777  			firstClass: builder.ClusterClass(metav1.NamespaceDefault, "class1").
  1778  				WithInfrastructureClusterTemplate(refToUnstructured(ref)).
  1779  				WithControlPlaneTemplate(refToUnstructured(ref)).
  1780  				WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)).
  1781  				Build(),
  1782  			secondClass: builder.ClusterClass(metav1.NamespaceDefault, "class2").
  1783  				WithInfrastructureClusterTemplate(refToUnstructured(ref)).
  1784  				WithControlPlaneTemplate(refToUnstructured(ref)).
  1785  				WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(compatibleNameChangeRef)).
  1786  				Build(),
  1787  			wantErr: false,
  1788  		},
  1789  		{
  1790  			name: "Accept cluster.topology.class change with a compatible controlPlane.MachineInfrastructure ref change",
  1791  			firstClass: builder.ClusterClass(metav1.NamespaceDefault, "class1").
  1792  				WithInfrastructureClusterTemplate(refToUnstructured(ref)).
  1793  				WithControlPlaneTemplate(refToUnstructured(ref)).
  1794  				WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)).
  1795  				Build(),
  1796  			secondClass: builder.ClusterClass(metav1.NamespaceDefault, "class2").
  1797  				WithInfrastructureClusterTemplate(refToUnstructured(ref)).
  1798  				WithControlPlaneTemplate(refToUnstructured(ref)).
  1799  				WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(compatibleAPIVersionChangeRef)).
  1800  				Build(),
  1801  			wantErr: false,
  1802  		},
  1803  		{
  1804  			name: "Reject cluster.topology.class change with an incompatible controlPlane.MachineInfrastructure Kind ref change",
  1805  			firstClass: builder.ClusterClass(metav1.NamespaceDefault, "class1").
  1806  				WithInfrastructureClusterTemplate(refToUnstructured(ref)).
  1807  				WithControlPlaneTemplate(refToUnstructured(ref)).
  1808  				WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)).
  1809  				Build(),
  1810  			secondClass: builder.ClusterClass(metav1.NamespaceDefault, "class2").
  1811  				WithInfrastructureClusterTemplate(refToUnstructured(ref)).
  1812  				WithControlPlaneTemplate(refToUnstructured(ref)).
  1813  				WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(incompatibleKindRef)).
  1814  				Build(),
  1815  			wantErr: true,
  1816  		},
  1817  		{
  1818  			name: "Reject cluster.topology.class change with an incompatible controlPlane.MachineInfrastructure APIVersion ref change",
  1819  			firstClass: builder.ClusterClass(metav1.NamespaceDefault, "class1").
  1820  				WithInfrastructureClusterTemplate(refToUnstructured(ref)).
  1821  				WithControlPlaneTemplate(refToUnstructured(ref)).
  1822  				WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)).
  1823  				Build(),
  1824  			secondClass: builder.ClusterClass(metav1.NamespaceDefault, "class2").
  1825  				WithInfrastructureClusterTemplate(refToUnstructured(ref)).
  1826  				WithControlPlaneTemplate(refToUnstructured(ref)).
  1827  				WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(incompatibleAPIGroupRef)).
  1828  				Build(),
  1829  			wantErr: true,
  1830  		},
  1831  
  1832  		// MachineDeploymentClass changes
  1833  		{
  1834  			name: "Accept cluster.topology.class change with a compatible MachineDeploymentClass InfrastructureTemplate",
  1835  			firstClass: builder.ClusterClass(metav1.NamespaceDefault, "class1").
  1836  				WithInfrastructureClusterTemplate(refToUnstructured(ref)).
  1837  				WithControlPlaneTemplate(refToUnstructured(ref)).
  1838  				WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)).
  1839  				WithWorkerMachineDeploymentClasses(
  1840  					*builder.MachineDeploymentClass("aa").
  1841  						WithInfrastructureTemplate(refToUnstructured(ref)).
  1842  						WithBootstrapTemplate(refToUnstructured(ref)).
  1843  						Build(),
  1844  				).
  1845  				Build(),
  1846  			secondClass: builder.ClusterClass(metav1.NamespaceDefault, "class2").
  1847  				WithInfrastructureClusterTemplate(refToUnstructured(ref)).
  1848  				WithControlPlaneTemplate(refToUnstructured(ref)).
  1849  				WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)).
  1850  				WithWorkerMachineDeploymentClasses(
  1851  					*builder.MachineDeploymentClass("aa").
  1852  						WithInfrastructureTemplate(refToUnstructured(compatibleNameChangeRef)).
  1853  						WithBootstrapTemplate(refToUnstructured(ref)).
  1854  						Build(),
  1855  				).
  1856  				Build(),
  1857  			wantErr: false,
  1858  		},
  1859  		{
  1860  			name: "Accept cluster.topology.class change with an incompatible MachineDeploymentClass BootstrapTemplate",
  1861  			firstClass: builder.ClusterClass(metav1.NamespaceDefault, "class1").
  1862  				WithInfrastructureClusterTemplate(refToUnstructured(ref)).
  1863  				WithControlPlaneTemplate(refToUnstructured(ref)).
  1864  				WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)).
  1865  				WithWorkerMachineDeploymentClasses(
  1866  					*builder.MachineDeploymentClass("aa").
  1867  						WithInfrastructureTemplate(refToUnstructured(ref)).
  1868  						WithBootstrapTemplate(refToUnstructured(ref)).
  1869  						Build(),
  1870  				).
  1871  				Build(),
  1872  			secondClass: builder.ClusterClass(metav1.NamespaceDefault, "class2").
  1873  				WithInfrastructureClusterTemplate(refToUnstructured(ref)).
  1874  				WithControlPlaneTemplate(refToUnstructured(ref)).
  1875  				WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)).
  1876  				WithWorkerMachineDeploymentClasses(
  1877  					*builder.MachineDeploymentClass("aa").
  1878  						WithInfrastructureTemplate(refToUnstructured(compatibleNameChangeRef)).
  1879  						WithBootstrapTemplate(refToUnstructured(incompatibleKindRef)).
  1880  						Build(),
  1881  				).
  1882  				Build(),
  1883  			wantErr: false,
  1884  		},
  1885  		{
  1886  			name: "Accept cluster.topology.class change with a deleted MachineDeploymentClass",
  1887  			firstClass: builder.ClusterClass(metav1.NamespaceDefault, "class1").
  1888  				WithInfrastructureClusterTemplate(refToUnstructured(ref)).
  1889  				WithControlPlaneTemplate(refToUnstructured(ref)).
  1890  				WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)).
  1891  				WithWorkerMachineDeploymentClasses(
  1892  					*builder.MachineDeploymentClass("aa").
  1893  						WithInfrastructureTemplate(refToUnstructured(ref)).
  1894  						WithBootstrapTemplate(refToUnstructured(ref)).
  1895  						Build(),
  1896  					*builder.MachineDeploymentClass("bb").
  1897  						WithInfrastructureTemplate(refToUnstructured(ref)).
  1898  						WithBootstrapTemplate(refToUnstructured(ref)).
  1899  						Build(),
  1900  				).
  1901  				Build(),
  1902  			secondClass: builder.ClusterClass(metav1.NamespaceDefault, "class2").
  1903  				WithInfrastructureClusterTemplate(refToUnstructured(ref)).
  1904  				WithControlPlaneTemplate(refToUnstructured(ref)).
  1905  				WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)).
  1906  				WithWorkerMachineDeploymentClasses(
  1907  					*builder.MachineDeploymentClass("aa").
  1908  						WithInfrastructureTemplate(refToUnstructured(ref)).
  1909  						WithBootstrapTemplate(refToUnstructured(ref)).
  1910  						Build(),
  1911  				).
  1912  				Build(),
  1913  			wantErr: false,
  1914  		},
  1915  		{
  1916  			name: "Accept cluster.topology.class change with an added MachineDeploymentClass",
  1917  			firstClass: builder.ClusterClass(metav1.NamespaceDefault, "class1").
  1918  				WithInfrastructureClusterTemplate(refToUnstructured(ref)).
  1919  				WithControlPlaneTemplate(refToUnstructured(ref)).
  1920  				WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)).
  1921  				WithWorkerMachineDeploymentClasses(
  1922  					*builder.MachineDeploymentClass("aa").
  1923  						WithInfrastructureTemplate(refToUnstructured(ref)).
  1924  						WithBootstrapTemplate(refToUnstructured(ref)).
  1925  						Build(),
  1926  				).
  1927  				Build(),
  1928  			secondClass: builder.ClusterClass(metav1.NamespaceDefault, "class2").
  1929  				WithInfrastructureClusterTemplate(refToUnstructured(ref)).
  1930  				WithControlPlaneTemplate(refToUnstructured(ref)).
  1931  				WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)).
  1932  				WithWorkerMachineDeploymentClasses(
  1933  					*builder.MachineDeploymentClass("aa").
  1934  						WithInfrastructureTemplate(refToUnstructured(ref)).
  1935  						WithBootstrapTemplate(refToUnstructured(ref)).
  1936  						Build(),
  1937  					*builder.MachineDeploymentClass("bb").
  1938  						WithInfrastructureTemplate(refToUnstructured(ref)).
  1939  						WithBootstrapTemplate(refToUnstructured(ref)).
  1940  						Build(),
  1941  				).
  1942  				Build(),
  1943  			wantErr: false,
  1944  		},
  1945  		{
  1946  			name: "Reject cluster.topology.class change with an incompatible Kind change to MachineDeploymentClass InfrastructureTemplate",
  1947  			firstClass: builder.ClusterClass(metav1.NamespaceDefault, "class1").
  1948  				WithInfrastructureClusterTemplate(refToUnstructured(ref)).
  1949  				WithControlPlaneTemplate(refToUnstructured(ref)).
  1950  				WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)).
  1951  				WithWorkerMachineDeploymentClasses(
  1952  					*builder.MachineDeploymentClass("aa").
  1953  						WithInfrastructureTemplate(refToUnstructured(ref)).
  1954  						WithBootstrapTemplate(refToUnstructured(ref)).
  1955  						Build(),
  1956  				).
  1957  				Build(),
  1958  			secondClass: builder.ClusterClass(metav1.NamespaceDefault, "class2").
  1959  				WithInfrastructureClusterTemplate(refToUnstructured(ref)).
  1960  				WithControlPlaneTemplate(refToUnstructured(ref)).
  1961  				WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)).
  1962  				WithWorkerMachineDeploymentClasses(
  1963  					*builder.MachineDeploymentClass("aa").
  1964  						WithInfrastructureTemplate(refToUnstructured(incompatibleKindRef)).
  1965  						WithBootstrapTemplate(refToUnstructured(ref)).
  1966  						Build(),
  1967  				).
  1968  				Build(),
  1969  			wantErr: true,
  1970  		},
  1971  		{
  1972  			name: "Reject cluster.topology.class change with an incompatible APIGroup change to MachineDeploymentClass InfrastructureTemplate",
  1973  			firstClass: builder.ClusterClass(metav1.NamespaceDefault, "class1").
  1974  				WithInfrastructureClusterTemplate(refToUnstructured(ref)).
  1975  				WithControlPlaneTemplate(refToUnstructured(ref)).
  1976  				WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)).
  1977  				WithWorkerMachineDeploymentClasses(
  1978  					*builder.MachineDeploymentClass("aa").
  1979  						WithInfrastructureTemplate(refToUnstructured(ref)).
  1980  						WithBootstrapTemplate(refToUnstructured(ref)).
  1981  						Build(),
  1982  				).
  1983  				Build(),
  1984  			secondClass: builder.ClusterClass(metav1.NamespaceDefault, "class2").
  1985  				WithInfrastructureClusterTemplate(refToUnstructured(ref)).
  1986  				WithControlPlaneTemplate(refToUnstructured(ref)).
  1987  				WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)).
  1988  				WithWorkerMachineDeploymentClasses(
  1989  					*builder.MachineDeploymentClass("aa").
  1990  						WithInfrastructureTemplate(refToUnstructured(incompatibleAPIGroupRef)).
  1991  						WithBootstrapTemplate(refToUnstructured(ref)).
  1992  						Build(),
  1993  				).
  1994  				Build(),
  1995  			wantErr: true,
  1996  		},
  1997  	}
  1998  	for _, tt := range tests {
  1999  		t.Run(tt.name, func(t *testing.T) {
  2000  			// Mark this condition to true so the webhook sees the ClusterClass as up to date.
  2001  			conditions.MarkTrue(tt.firstClass, clusterv1.ClusterClassVariablesReconciledCondition)
  2002  			conditions.MarkTrue(tt.secondClass, clusterv1.ClusterClassVariablesReconciledCondition)
  2003  
  2004  			// Sets up the fakeClient for the test case.
  2005  			fakeClient := fake.NewClientBuilder().
  2006  				WithObjects(tt.firstClass, tt.secondClass).
  2007  				WithScheme(fakeScheme).
  2008  				Build()
  2009  
  2010  			// Create the webhook and add the fakeClient as its client. This is required because the test uses a Managed Topology.
  2011  			c := &Cluster{Client: fakeClient}
  2012  
  2013  			// Create and updated cluster which uses the name of the second class from the test definition in its '.spec.topology.'
  2014  			secondCluster := cluster.DeepCopy()
  2015  			secondCluster.Spec.Topology.Class = tt.secondClass.Name
  2016  
  2017  			// Checks the return error.
  2018  			warnings, err := c.ValidateUpdate(ctx, cluster, secondCluster)
  2019  			if tt.wantErr {
  2020  				g.Expect(err).To(HaveOccurred())
  2021  			} else {
  2022  				g.Expect(err).ToNot(HaveOccurred())
  2023  			}
  2024  			g.Expect(warnings).To(BeEmpty())
  2025  		})
  2026  	}
  2027  }
  2028  
  2029  // TestMovingBetweenManagedAndUnmanaged cluster tests cases where a clusterClass is added or removed during a cluster update.
  2030  func TestMovingBetweenManagedAndUnmanaged(t *testing.T) {
  2031  	defer utilfeature.SetFeatureGateDuringTest(t, feature.Gates, feature.ClusterTopology, true)()
  2032  	ref := &corev1.ObjectReference{
  2033  		APIVersion: "group.test.io/foo",
  2034  		Kind:       "barTemplate",
  2035  		Name:       "baz",
  2036  		Namespace:  "default",
  2037  	}
  2038  
  2039  	g := NewWithT(t)
  2040  
  2041  	tests := []struct {
  2042  		name            string
  2043  		cluster         *clusterv1.Cluster
  2044  		clusterClass    *clusterv1.ClusterClass
  2045  		updatedTopology *clusterv1.Topology
  2046  		wantErr         bool
  2047  	}{
  2048  		{
  2049  			name: "Reject cluster moving from Unmanaged to Managed i.e. adding the spec.topology.class field on update",
  2050  			cluster: builder.Cluster(metav1.NamespaceDefault, "cluster1").
  2051  				Build(),
  2052  			clusterClass: builder.ClusterClass(metav1.NamespaceDefault, "class1").
  2053  				WithInfrastructureClusterTemplate(refToUnstructured(ref)).
  2054  				WithControlPlaneTemplate(refToUnstructured(ref)).
  2055  				WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)).
  2056  				Build(),
  2057  			updatedTopology: builder.ClusterTopology().
  2058  				WithClass("class1").
  2059  				WithVersion("v1.22.2").
  2060  				WithControlPlaneReplicas(3).
  2061  				Build(),
  2062  			wantErr: true,
  2063  		},
  2064  		{
  2065  			name: "Allow cluster moving from Unmanaged to Managed i.e. adding the spec.topology.class field on update " +
  2066  				"if and only if ClusterTopologyUnsafeUpdateClassNameAnnotation is set",
  2067  			cluster: builder.Cluster(metav1.NamespaceDefault, "cluster1").
  2068  				WithAnnotations(map[string]string{clusterv1.ClusterTopologyUnsafeUpdateClassNameAnnotation: ""}).
  2069  				Build(),
  2070  			clusterClass: builder.ClusterClass(metav1.NamespaceDefault, "class1").
  2071  				WithInfrastructureClusterTemplate(refToUnstructured(ref)).
  2072  				WithControlPlaneTemplate(refToUnstructured(ref)).
  2073  				WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)).
  2074  				Build(),
  2075  			updatedTopology: builder.ClusterTopology().
  2076  				WithClass("class1").
  2077  				WithVersion("v1.22.2").
  2078  				WithControlPlaneReplicas(3).
  2079  				Build(),
  2080  			wantErr: false,
  2081  		},
  2082  		{
  2083  			name: "Reject cluster moving from Managed to Unmanaged i.e. removing the spec.topology.class field on update",
  2084  			cluster: builder.Cluster(metav1.NamespaceDefault, "cluster1").
  2085  				WithTopology(builder.ClusterTopology().
  2086  					WithClass("class1").
  2087  					WithVersion("v1.22.2").
  2088  					WithControlPlaneReplicas(3).
  2089  					Build()).
  2090  				Build(),
  2091  			clusterClass: builder.ClusterClass(metav1.NamespaceDefault, "class1").
  2092  				WithInfrastructureClusterTemplate(refToUnstructured(ref)).
  2093  				WithControlPlaneTemplate(refToUnstructured(ref)).
  2094  				WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)).
  2095  				Build(),
  2096  			updatedTopology: nil,
  2097  			wantErr:         true,
  2098  		},
  2099  		{
  2100  			name: "Reject cluster update if ClusterClass does not exist",
  2101  			cluster: builder.Cluster(metav1.NamespaceDefault, "cluster1").
  2102  				WithTopology(builder.ClusterTopology().
  2103  					WithClass("class1").
  2104  					WithVersion("v1.22.2").
  2105  					WithControlPlaneReplicas(3).
  2106  					Build()).
  2107  				Build(),
  2108  			clusterClass:
  2109  			// ClusterClass name is different to that in the Cluster `.spec.topology.class`
  2110  			builder.ClusterClass(metav1.NamespaceDefault, "completely-different-class").
  2111  				WithInfrastructureClusterTemplate(refToUnstructured(ref)).
  2112  				WithControlPlaneTemplate(refToUnstructured(ref)).
  2113  				WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref)).
  2114  				Build(),
  2115  			updatedTopology: builder.ClusterTopology().
  2116  				WithClass("class1").
  2117  				WithVersion("v1.22.2").
  2118  				WithControlPlaneReplicas(3).
  2119  				Build(),
  2120  			wantErr: true,
  2121  		},
  2122  	}
  2123  	for _, tt := range tests {
  2124  		t.Run(tt.name, func(t *testing.T) {
  2125  			// Mark this condition to true so the webhook sees the ClusterClass as up to date.
  2126  			conditions.MarkTrue(tt.clusterClass, clusterv1.ClusterClassVariablesReconciledCondition)
  2127  			// Sets up the fakeClient for the test case.
  2128  			fakeClient := fake.NewClientBuilder().
  2129  				WithObjects(tt.clusterClass, tt.cluster).
  2130  				WithScheme(fakeScheme).
  2131  				Build()
  2132  
  2133  			// Create the webhook and add the fakeClient as its client. This is required because the test uses a Managed Topology.
  2134  			c := &Cluster{Client: fakeClient}
  2135  
  2136  			// Create and updated cluster which uses the name of the second class from the test definition in its '.spec.topology.'
  2137  			updatedCluster := tt.cluster.DeepCopy()
  2138  			updatedCluster.Spec.Topology = tt.updatedTopology
  2139  
  2140  			// Checks the return error.
  2141  			warnings, err := c.ValidateUpdate(ctx, tt.cluster, updatedCluster)
  2142  			if tt.wantErr {
  2143  				g.Expect(err).To(HaveOccurred())
  2144  			} else {
  2145  				g.Expect(err).NotTo(HaveOccurred())
  2146  				// Errors may be duplicated as warnings. There should be no warnings in this case if there are no errors.
  2147  				g.Expect(warnings).To(BeEmpty())
  2148  			}
  2149  		})
  2150  	}
  2151  }
  2152  
  2153  // TestClusterClassPollingErrors tests when a Cluster can be reconciled given different reconcile states of the ClusterClass.
  2154  func TestClusterClassPollingErrors(t *testing.T) {
  2155  	defer utilfeature.SetFeatureGateDuringTest(t, feature.Gates, feature.ClusterTopology, true)()
  2156  	g := NewWithT(t)
  2157  	ref := &corev1.ObjectReference{
  2158  		APIVersion: "group.test.io/foo",
  2159  		Kind:       "barTemplate",
  2160  		Name:       "baz",
  2161  		Namespace:  "default",
  2162  	}
  2163  
  2164  	topology := builder.ClusterTopology().WithClass("class1").WithVersion("v1.24.3").Build()
  2165  	secondTopology := builder.ClusterTopology().WithClass("class2").WithVersion("v1.24.3").Build()
  2166  	notFoundTopology := builder.ClusterTopology().WithClass("doesnotexist").WithVersion("v1.24.3").Build()
  2167  
  2168  	baseClusterClass := builder.ClusterClass(metav1.NamespaceDefault, "class1").
  2169  		WithInfrastructureClusterTemplate(refToUnstructured(ref)).
  2170  		WithControlPlaneTemplate(refToUnstructured(ref)).
  2171  		WithControlPlaneInfrastructureMachineTemplate(refToUnstructured(ref))
  2172  
  2173  	// ccFullyReconciled is a ClusterClass with a matching generation and observed generation, and VariablesReconciled=True.
  2174  	ccFullyReconciled := baseClusterClass.DeepCopy().Build()
  2175  	ccFullyReconciled.Generation = 1
  2176  	ccFullyReconciled.Status.ObservedGeneration = 1
  2177  	conditions.MarkTrue(ccFullyReconciled, clusterv1.ClusterClassVariablesReconciledCondition)
  2178  
  2179  	// secondFullyReconciled is a second ClusterClass with a matching generation and observed generation, and VariablesReconciled=True.
  2180  	secondFullyReconciled := ccFullyReconciled.DeepCopy()
  2181  	secondFullyReconciled.SetName("class2")
  2182  
  2183  	// ccGenerationMismatch is a ClusterClass with a mismatched generation and observed generation, but VariablesReconciledCondition=True.
  2184  	ccGenerationMismatch := baseClusterClass.DeepCopy().Build()
  2185  	ccGenerationMismatch.Generation = 999
  2186  	ccGenerationMismatch.Status.ObservedGeneration = 1
  2187  	conditions.MarkTrue(ccGenerationMismatch, clusterv1.ClusterClassVariablesReconciledCondition)
  2188  
  2189  	// ccVariablesReconciledFalse with VariablesReconciled=False.
  2190  	ccVariablesReconciledFalse := baseClusterClass.DeepCopy().Build()
  2191  	conditions.MarkFalse(ccGenerationMismatch, clusterv1.ClusterClassVariablesReconciledCondition, "", clusterv1.ConditionSeverityError, "")
  2192  
  2193  	tests := []struct {
  2194  		name           string
  2195  		cluster        *clusterv1.Cluster
  2196  		oldCluster     *clusterv1.Cluster
  2197  		clusterClasses []*clusterv1.ClusterClass
  2198  		injectedErr    interceptor.Funcs
  2199  		wantErr        bool
  2200  		wantWarnings   bool
  2201  	}{
  2202  		{
  2203  			name:           "Pass on create if ClusterClass is fully reconciled",
  2204  			cluster:        builder.Cluster(metav1.NamespaceDefault, "cluster1").WithTopology(topology).Build(),
  2205  			clusterClasses: []*clusterv1.ClusterClass{ccFullyReconciled},
  2206  			wantErr:        false,
  2207  		},
  2208  		{
  2209  			name:           "Pass on create if ClusterClass generation does not match observedGeneration",
  2210  			cluster:        builder.Cluster(metav1.NamespaceDefault, "cluster1").WithTopology(topology).Build(),
  2211  			clusterClasses: []*clusterv1.ClusterClass{ccGenerationMismatch},
  2212  			wantErr:        false,
  2213  			wantWarnings:   true,
  2214  		},
  2215  		{
  2216  			name:           "Pass on create if ClusterClass generation matches observedGeneration but VariablesReconciled=False",
  2217  			cluster:        builder.Cluster(metav1.NamespaceDefault, "cluster1").WithTopology(topology).Build(),
  2218  			clusterClasses: []*clusterv1.ClusterClass{ccVariablesReconciledFalse},
  2219  			wantErr:        false,
  2220  			wantWarnings:   true,
  2221  		},
  2222  		{
  2223  			name:           "Pass on create if ClusterClass is not found",
  2224  			cluster:        builder.Cluster(metav1.NamespaceDefault, "cluster1").WithTopology(notFoundTopology).Build(),
  2225  			clusterClasses: []*clusterv1.ClusterClass{ccFullyReconciled},
  2226  			wantErr:        false,
  2227  			wantWarnings:   true,
  2228  		},
  2229  		{
  2230  			name:           "Pass on update if oldCluster ClusterClass is fully reconciled",
  2231  			cluster:        builder.Cluster(metav1.NamespaceDefault, "cluster1").WithTopology(secondTopology).Build(),
  2232  			oldCluster:     builder.Cluster(metav1.NamespaceDefault, "cluster1").WithTopology(topology).Build(),
  2233  			clusterClasses: []*clusterv1.ClusterClass{ccFullyReconciled, secondFullyReconciled},
  2234  			wantErr:        false,
  2235  		},
  2236  		{
  2237  			name:           "Fail on update if oldCluster ClusterClass generation does not match observedGeneration",
  2238  			cluster:        builder.Cluster(metav1.NamespaceDefault, "cluster1").WithTopology(secondTopology).Build(),
  2239  			oldCluster:     builder.Cluster(metav1.NamespaceDefault, "cluster1").WithTopology(topology).Build(),
  2240  			clusterClasses: []*clusterv1.ClusterClass{ccGenerationMismatch, secondFullyReconciled},
  2241  			wantErr:        true,
  2242  		},
  2243  		{
  2244  			name:           "Fail on update if old Cluster ClusterClass is not found",
  2245  			cluster:        builder.Cluster(metav1.NamespaceDefault, "cluster1").WithTopology(topology).Build(),
  2246  			oldCluster:     builder.Cluster(metav1.NamespaceDefault, "cluster1").WithTopology(notFoundTopology).Build(),
  2247  			clusterClasses: []*clusterv1.ClusterClass{ccFullyReconciled},
  2248  			wantErr:        true,
  2249  		},
  2250  		{
  2251  			name:           "Fail on update if new Cluster ClusterClass is not found",
  2252  			cluster:        builder.Cluster(metav1.NamespaceDefault, "cluster1").WithTopology(notFoundTopology).Build(),
  2253  			oldCluster:     builder.Cluster(metav1.NamespaceDefault, "cluster1").WithTopology(topology).Build(),
  2254  			clusterClasses: []*clusterv1.ClusterClass{ccFullyReconciled},
  2255  			wantErr:        true,
  2256  			wantWarnings:   true,
  2257  		},
  2258  		{
  2259  			name:           "Fail on update if new ClusterClass returns connection error",
  2260  			cluster:        builder.Cluster(metav1.NamespaceDefault, "cluster1").WithTopology(secondTopology).Build(),
  2261  			oldCluster:     builder.Cluster(metav1.NamespaceDefault, "cluster1").WithTopology(topology).Build(),
  2262  			clusterClasses: []*clusterv1.ClusterClass{ccFullyReconciled, secondFullyReconciled},
  2263  			injectedErr: interceptor.Funcs{
  2264  				Get: func(ctx context.Context, client client.WithWatch, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error {
  2265  					// Throw an error if the second ClusterClass `class2` used as the new ClusterClass is being retrieved.
  2266  					if key.Name == secondTopology.Class {
  2267  						return errors.New("connection error")
  2268  					}
  2269  					return client.Get(ctx, key, obj)
  2270  				},
  2271  			},
  2272  			wantErr:      true,
  2273  			wantWarnings: false,
  2274  		},
  2275  		{
  2276  			name:           "Fail on update if old ClusterClass returns connection error",
  2277  			cluster:        builder.Cluster(metav1.NamespaceDefault, "cluster1").WithTopology(secondTopology).Build(),
  2278  			oldCluster:     builder.Cluster(metav1.NamespaceDefault, "cluster1").WithTopology(topology).Build(),
  2279  			clusterClasses: []*clusterv1.ClusterClass{ccFullyReconciled, secondFullyReconciled},
  2280  			injectedErr: interceptor.Funcs{
  2281  				Get: func(ctx context.Context, client client.WithWatch, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error {
  2282  					// Throw an error if the ClusterClass `class1` used as the old ClusterClass is being retrieved.
  2283  					if key.Name == topology.Class {
  2284  						return errors.New("connection error")
  2285  					}
  2286  					return client.Get(ctx, key, obj)
  2287  				},
  2288  			},
  2289  			wantErr:      true,
  2290  			wantWarnings: false,
  2291  		},
  2292  	}
  2293  
  2294  	for _, tt := range tests {
  2295  		t.Run(tt.name, func(t *testing.T) {
  2296  			// Sets up a reconcile with a fakeClient for the test case.
  2297  			objs := []client.Object{}
  2298  			for _, cc := range tt.clusterClasses {
  2299  				objs = append(objs, cc)
  2300  			}
  2301  			c := &Cluster{Client: fake.NewClientBuilder().
  2302  				WithInterceptorFuncs(tt.injectedErr).
  2303  				WithScheme(fakeScheme).
  2304  				WithObjects(objs...).
  2305  				Build(),
  2306  			}
  2307  
  2308  			// Checks the return error.
  2309  			warnings, err := c.validate(ctx, tt.oldCluster, tt.cluster)
  2310  			if tt.wantErr {
  2311  				g.Expect(err).To(HaveOccurred())
  2312  			} else {
  2313  				g.Expect(err).ToNot(HaveOccurred())
  2314  			}
  2315  			if tt.wantWarnings {
  2316  				g.Expect(warnings).NotTo(BeNil())
  2317  			} else {
  2318  				g.Expect(warnings).To(BeNil())
  2319  			}
  2320  		})
  2321  	}
  2322  }
  2323  
  2324  func refToUnstructured(ref *corev1.ObjectReference) *unstructured.Unstructured {
  2325  	gvk := ref.GetObjectKind().GroupVersionKind()
  2326  	output := &unstructured.Unstructured{}
  2327  	output.SetKind(gvk.Kind)
  2328  	output.SetAPIVersion(gvk.GroupVersion().String())
  2329  	output.SetName(ref.Name)
  2330  	output.SetNamespace(ref.Namespace)
  2331  	return output
  2332  }