sigs.k8s.io/cluster-api@v1.6.3/internal/webhooks/patch_validation_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  	"testing"
    21  
    22  	. "github.com/onsi/gomega"
    23  	corev1 "k8s.io/api/core/v1"
    24  	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
    25  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    26  	"k8s.io/apimachinery/pkg/util/validation/field"
    27  	utilfeature "k8s.io/component-base/featuregate/testing"
    28  	"k8s.io/utils/pointer"
    29  
    30  	clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
    31  	"sigs.k8s.io/cluster-api/feature"
    32  	"sigs.k8s.io/cluster-api/internal/test/builder"
    33  )
    34  
    35  func TestValidatePatches(t *testing.T) {
    36  	tests := []struct {
    37  		name         string
    38  		clusterClass clusterv1.ClusterClass
    39  		runtimeSDK   bool
    40  		wantErr      bool
    41  	}{
    42  		{
    43  			name: "pass multiple patches that are correctly formatted",
    44  			clusterClass: clusterv1.ClusterClass{
    45  				Spec: clusterv1.ClusterClassSpec{
    46  					ControlPlane: clusterv1.ControlPlaneClass{
    47  						LocalObjectTemplate: clusterv1.LocalObjectTemplate{
    48  							Ref: &corev1.ObjectReference{
    49  								APIVersion: "controlplane.cluster.x-k8s.io/v1beta1",
    50  								Kind:       "ControlPlaneTemplate",
    51  							},
    52  						},
    53  					},
    54  
    55  					Patches: []clusterv1.ClusterClassPatch{
    56  						{
    57  							Name: "patch1",
    58  							Definitions: []clusterv1.PatchDefinition{
    59  								{
    60  									Selector: clusterv1.PatchSelector{
    61  										APIVersion: "controlplane.cluster.x-k8s.io/v1beta1",
    62  										Kind:       "ControlPlaneTemplate",
    63  										MatchResources: clusterv1.PatchSelectorMatch{
    64  											ControlPlane: true,
    65  										},
    66  									},
    67  									JSONPatches: []clusterv1.JSONPatch{
    68  										{
    69  											Op:   "add",
    70  											Path: "/spec/template/spec/variableSetting/variableValue1",
    71  											ValueFrom: &clusterv1.JSONPatchValue{
    72  												Variable: pointer.String("variableName1"),
    73  											},
    74  										},
    75  									},
    76  								},
    77  							},
    78  						},
    79  						{
    80  							Name: "patch2",
    81  							Definitions: []clusterv1.PatchDefinition{
    82  								{
    83  									Selector: clusterv1.PatchSelector{
    84  										APIVersion: "controlplane.cluster.x-k8s.io/v1beta1",
    85  										Kind:       "ControlPlaneTemplate",
    86  										MatchResources: clusterv1.PatchSelectorMatch{
    87  											ControlPlane: true,
    88  										},
    89  									},
    90  									JSONPatches: []clusterv1.JSONPatch{
    91  										{
    92  											Op:   "add",
    93  											Path: "/spec/template/spec/variableSetting/variableValue2",
    94  											ValueFrom: &clusterv1.JSONPatchValue{
    95  												Variable: pointer.String("variableName2"),
    96  											},
    97  										},
    98  									},
    99  								},
   100  							},
   101  						},
   102  					},
   103  					Variables: []clusterv1.ClusterClassVariable{
   104  						{
   105  							Name:     "variableName1",
   106  							Required: true,
   107  							Schema: clusterv1.VariableSchema{
   108  								OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   109  									Type: "string",
   110  								},
   111  							},
   112  						},
   113  						{
   114  							Name:     "variableName2",
   115  							Required: true,
   116  							Schema: clusterv1.VariableSchema{
   117  								OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   118  									Type: "string",
   119  								},
   120  							},
   121  						},
   122  					},
   123  				},
   124  			},
   125  			wantErr: false,
   126  		},
   127  
   128  		// Patch name validation
   129  		{
   130  			name: "error if patch name is empty",
   131  			clusterClass: clusterv1.ClusterClass{
   132  				Spec: clusterv1.ClusterClassSpec{
   133  					ControlPlane: clusterv1.ControlPlaneClass{
   134  						LocalObjectTemplate: clusterv1.LocalObjectTemplate{
   135  							Ref: &corev1.ObjectReference{
   136  								APIVersion: "controlplane.cluster.x-k8s.io/v1beta1",
   137  								Kind:       "ControlPlaneTemplate",
   138  							},
   139  						},
   140  					},
   141  
   142  					Patches: []clusterv1.ClusterClassPatch{
   143  
   144  						{
   145  							Name: "",
   146  							Definitions: []clusterv1.PatchDefinition{
   147  								{
   148  									Selector: clusterv1.PatchSelector{
   149  										APIVersion: "controlplane.cluster.x-k8s.io/v1beta1",
   150  										Kind:       "ControlPlaneTemplate",
   151  										MatchResources: clusterv1.PatchSelectorMatch{
   152  											ControlPlane: true,
   153  										},
   154  									},
   155  									JSONPatches: []clusterv1.JSONPatch{
   156  										{
   157  											Op:   "add",
   158  											Path: "/spec/template/spec/kubeadmConfigSpec/clusterConfiguration/controllerManager/extraArgs/cluster-name",
   159  											ValueFrom: &clusterv1.JSONPatchValue{
   160  												Variable: pointer.String("variableName"),
   161  											},
   162  										},
   163  									},
   164  								},
   165  							},
   166  						},
   167  					},
   168  					Variables: []clusterv1.ClusterClassVariable{
   169  						{
   170  							Name:     "variableName",
   171  							Required: true,
   172  							Schema: clusterv1.VariableSchema{
   173  								OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   174  									Type: "string",
   175  								},
   176  							},
   177  						},
   178  					},
   179  				},
   180  			},
   181  			wantErr: true,
   182  		},
   183  		{
   184  			name: "error if patches name is not unique",
   185  			clusterClass: clusterv1.ClusterClass{
   186  				Spec: clusterv1.ClusterClassSpec{
   187  					ControlPlane: clusterv1.ControlPlaneClass{
   188  						LocalObjectTemplate: clusterv1.LocalObjectTemplate{
   189  							Ref: &corev1.ObjectReference{
   190  								APIVersion: "controlplane.cluster.x-k8s.io/v1beta1",
   191  								Kind:       "ControlPlaneTemplate",
   192  							},
   193  						},
   194  					},
   195  
   196  					Patches: []clusterv1.ClusterClassPatch{
   197  
   198  						{
   199  							Name: "patch1",
   200  							Definitions: []clusterv1.PatchDefinition{
   201  								{
   202  									Selector: clusterv1.PatchSelector{
   203  										APIVersion: "controlplane.cluster.x-k8s.io/v1beta1",
   204  										Kind:       "ControlPlaneTemplate",
   205  										MatchResources: clusterv1.PatchSelectorMatch{
   206  											ControlPlane: true,
   207  										},
   208  									},
   209  									JSONPatches: []clusterv1.JSONPatch{
   210  										{
   211  											Op:   "add",
   212  											Path: "/spec/template/spec/kubeadmConfigSpec/clusterConfiguration/controllerManager/extraArgs/cluster-name",
   213  											ValueFrom: &clusterv1.JSONPatchValue{
   214  												Variable: pointer.String("variableName1"),
   215  											},
   216  										},
   217  									},
   218  								},
   219  							},
   220  						},
   221  						{
   222  							Name: "patch1",
   223  							Definitions: []clusterv1.PatchDefinition{
   224  								{
   225  									Selector: clusterv1.PatchSelector{
   226  										APIVersion: "controlplane.cluster.x-k8s.io/v1beta1",
   227  										Kind:       "ControlPlaneTemplate",
   228  										MatchResources: clusterv1.PatchSelectorMatch{
   229  											ControlPlane: true,
   230  										},
   231  									},
   232  									JSONPatches: []clusterv1.JSONPatch{
   233  										{
   234  											Op:   "add",
   235  											Path: "/spec/template/spec/variableSetting/variableValue",
   236  											ValueFrom: &clusterv1.JSONPatchValue{
   237  												Variable: pointer.String("variableName2"),
   238  											},
   239  										},
   240  									},
   241  								},
   242  							},
   243  						},
   244  					},
   245  					Variables: []clusterv1.ClusterClassVariable{
   246  						{
   247  							Name:     "variableName1",
   248  							Required: true,
   249  							Schema: clusterv1.VariableSchema{
   250  								OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   251  									Type: "string",
   252  								},
   253  							},
   254  						},
   255  						{
   256  							Name:     "variableName2",
   257  							Required: true,
   258  							Schema: clusterv1.VariableSchema{
   259  								OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   260  									Type: "string",
   261  								},
   262  							},
   263  						},
   264  					},
   265  				},
   266  			},
   267  			wantErr: true,
   268  		},
   269  
   270  		// enabledIf validation
   271  		{
   272  			name: "pass if enabledIf is a valid Go template",
   273  			clusterClass: clusterv1.ClusterClass{
   274  				Spec: clusterv1.ClusterClassSpec{
   275  					ControlPlane: clusterv1.ControlPlaneClass{
   276  						LocalObjectTemplate: clusterv1.LocalObjectTemplate{
   277  							Ref: &corev1.ObjectReference{
   278  								APIVersion: "controlplane.cluster.x-k8s.io/v1beta1",
   279  								Kind:       "ControlPlaneTemplate",
   280  							},
   281  						},
   282  					},
   283  					Patches: []clusterv1.ClusterClassPatch{
   284  						{
   285  							Name:        "patch1",
   286  							EnabledIf:   pointer.String(`template {{ .variableB }}`),
   287  							Definitions: []clusterv1.PatchDefinition{},
   288  						},
   289  					},
   290  				},
   291  			},
   292  			wantErr: false,
   293  		},
   294  		{
   295  			name: "error if enabledIf is an invalid Go template",
   296  			clusterClass: clusterv1.ClusterClass{
   297  				Spec: clusterv1.ClusterClassSpec{
   298  					ControlPlane: clusterv1.ControlPlaneClass{
   299  						LocalObjectTemplate: clusterv1.LocalObjectTemplate{
   300  							Ref: &corev1.ObjectReference{
   301  								APIVersion: "controlplane.cluster.x-k8s.io/v1beta1",
   302  								Kind:       "ControlPlaneTemplate",
   303  							},
   304  						},
   305  					},
   306  					Patches: []clusterv1.ClusterClassPatch{
   307  						{
   308  							Name:      "patch1",
   309  							EnabledIf: pointer.String(`template {{{{{{{{ .variableB }}`),
   310  						},
   311  					},
   312  				},
   313  			},
   314  			wantErr: true,
   315  		},
   316  		// Patch "op" (operation) validation
   317  		{
   318  			name: "error if patch op is not \"add\" \"remove\" or \"replace\"",
   319  			clusterClass: clusterv1.ClusterClass{
   320  				Spec: clusterv1.ClusterClassSpec{
   321  					ControlPlane: clusterv1.ControlPlaneClass{
   322  						LocalObjectTemplate: clusterv1.LocalObjectTemplate{
   323  							Ref: &corev1.ObjectReference{
   324  								APIVersion: "controlplane.cluster.x-k8s.io/v1beta1",
   325  								Kind:       "ControlPlaneTemplate",
   326  							},
   327  						},
   328  					},
   329  
   330  					Patches: []clusterv1.ClusterClassPatch{
   331  
   332  						{
   333  							Name: "patch1",
   334  							Definitions: []clusterv1.PatchDefinition{
   335  								{
   336  									Selector: clusterv1.PatchSelector{
   337  										APIVersion: "controlplane.cluster.x-k8s.io/v1beta1",
   338  										Kind:       "ControlPlaneTemplate",
   339  										MatchResources: clusterv1.PatchSelectorMatch{
   340  											ControlPlane: true,
   341  										},
   342  									},
   343  									JSONPatches: []clusterv1.JSONPatch{
   344  										{
   345  											// OP is set to an unrecognized value here.
   346  											Op:   "drop",
   347  											Path: "/spec/template/spec/variableSetting/variableValue2",
   348  										},
   349  									},
   350  								},
   351  							},
   352  						},
   353  					},
   354  				},
   355  			},
   356  			wantErr: true,
   357  		},
   358  
   359  		// Patch path validation
   360  		{
   361  			name: "error if jsonPath does not begin with \"/spec/\"",
   362  			clusterClass: clusterv1.ClusterClass{
   363  				Spec: clusterv1.ClusterClassSpec{
   364  					ControlPlane: clusterv1.ControlPlaneClass{
   365  						LocalObjectTemplate: clusterv1.LocalObjectTemplate{
   366  							Ref: &corev1.ObjectReference{
   367  								APIVersion: "controlplane.cluster.x-k8s.io/v1beta1",
   368  								Kind:       "ControlPlaneTemplate",
   369  							},
   370  						},
   371  					},
   372  
   373  					Patches: []clusterv1.ClusterClassPatch{
   374  
   375  						{
   376  							Name: "patch1",
   377  							Definitions: []clusterv1.PatchDefinition{
   378  								{
   379  									Selector: clusterv1.PatchSelector{
   380  										APIVersion: "controlplane.cluster.x-k8s.io/v1beta1",
   381  										Kind:       "ControlPlaneTemplate",
   382  										MatchResources: clusterv1.PatchSelectorMatch{
   383  											ControlPlane: true,
   384  										},
   385  									},
   386  									JSONPatches: []clusterv1.JSONPatch{
   387  										{
   388  											Op: "remove",
   389  											// Path is set to status.
   390  											Path: "/status/template/spec/variableSetting/variableValue2",
   391  										},
   392  									},
   393  								},
   394  							},
   395  						},
   396  					},
   397  				},
   398  			},
   399  			wantErr: true,
   400  		},
   401  		{
   402  			name: "pass if jsonPatch path uses a valid index for add i.e. 0",
   403  			clusterClass: clusterv1.ClusterClass{
   404  				Spec: clusterv1.ClusterClassSpec{
   405  					ControlPlane: clusterv1.ControlPlaneClass{
   406  						LocalObjectTemplate: clusterv1.LocalObjectTemplate{
   407  							Ref: &corev1.ObjectReference{
   408  								APIVersion: "controlplane.cluster.x-k8s.io/v1beta1",
   409  								Kind:       "ControlPlaneTemplate",
   410  							},
   411  						},
   412  					},
   413  
   414  					Patches: []clusterv1.ClusterClassPatch{
   415  						{
   416  							Name: "patch1",
   417  							Definitions: []clusterv1.PatchDefinition{
   418  								{
   419  									Selector: clusterv1.PatchSelector{
   420  										APIVersion: "controlplane.cluster.x-k8s.io/v1beta1",
   421  										Kind:       "ControlPlaneTemplate",
   422  										MatchResources: clusterv1.PatchSelectorMatch{
   423  											ControlPlane: true,
   424  										},
   425  									},
   426  									JSONPatches: []clusterv1.JSONPatch{
   427  										{
   428  											Op:   "add",
   429  											Path: "/spec/template/0/",
   430  											ValueFrom: &clusterv1.JSONPatchValue{
   431  												Variable: pointer.String("variableName"),
   432  											},
   433  										},
   434  									},
   435  								},
   436  							},
   437  						},
   438  					},
   439  					Variables: []clusterv1.ClusterClassVariable{
   440  						{
   441  							Name:     "variableName",
   442  							Required: true,
   443  							Schema: clusterv1.VariableSchema{
   444  								OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   445  									Type: "string",
   446  								},
   447  							},
   448  						},
   449  					},
   450  				},
   451  			},
   452  		},
   453  		{
   454  			name: "error if jsonPatch path uses an invalid index for add i.e. a number greater than 0.",
   455  			clusterClass: clusterv1.ClusterClass{
   456  				Spec: clusterv1.ClusterClassSpec{
   457  					ControlPlane: clusterv1.ControlPlaneClass{
   458  						LocalObjectTemplate: clusterv1.LocalObjectTemplate{
   459  							Ref: &corev1.ObjectReference{
   460  								APIVersion: "controlplane.cluster.x-k8s.io/v1beta1",
   461  								Kind:       "ControlPlaneTemplate",
   462  							},
   463  						},
   464  					},
   465  
   466  					Patches: []clusterv1.ClusterClassPatch{
   467  
   468  						{
   469  							Name: "patch1",
   470  							Definitions: []clusterv1.PatchDefinition{
   471  								{
   472  									Selector: clusterv1.PatchSelector{
   473  										APIVersion: "controlplane.cluster.x-k8s.io/v1beta1",
   474  										Kind:       "ControlPlaneTemplate",
   475  										MatchResources: clusterv1.PatchSelectorMatch{
   476  											ControlPlane: true,
   477  										},
   478  									},
   479  									JSONPatches: []clusterv1.JSONPatch{
   480  										{
   481  											Op:   "add",
   482  											Path: "/spec/template/1/",
   483  											ValueFrom: &clusterv1.JSONPatchValue{
   484  												Variable: pointer.String("variableName"),
   485  											},
   486  										},
   487  									},
   488  								},
   489  							},
   490  						},
   491  					},
   492  					Variables: []clusterv1.ClusterClassVariable{
   493  						{
   494  							Name:     "variableName",
   495  							Required: true,
   496  							Schema: clusterv1.VariableSchema{
   497  								OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   498  									Type: "string",
   499  								},
   500  							},
   501  						},
   502  					},
   503  				},
   504  			},
   505  			wantErr: true,
   506  		},
   507  		{
   508  			name: "error if jsonPatch path uses an invalid index for add i.e. 01",
   509  			clusterClass: clusterv1.ClusterClass{
   510  				Spec: clusterv1.ClusterClassSpec{
   511  					ControlPlane: clusterv1.ControlPlaneClass{
   512  						LocalObjectTemplate: clusterv1.LocalObjectTemplate{
   513  							Ref: &corev1.ObjectReference{
   514  								APIVersion: "controlplane.cluster.x-k8s.io/v1beta1",
   515  								Kind:       "ControlPlaneTemplate",
   516  							},
   517  						},
   518  					},
   519  
   520  					Patches: []clusterv1.ClusterClassPatch{
   521  
   522  						{
   523  							Name: "patch1",
   524  							Definitions: []clusterv1.PatchDefinition{
   525  								{
   526  									Selector: clusterv1.PatchSelector{
   527  										APIVersion: "controlplane.cluster.x-k8s.io/v1beta1",
   528  										Kind:       "ControlPlaneTemplate",
   529  										MatchResources: clusterv1.PatchSelectorMatch{
   530  											ControlPlane: true,
   531  										},
   532  									},
   533  									JSONPatches: []clusterv1.JSONPatch{
   534  										{
   535  											Op:   "add",
   536  											Path: "/spec/template/01/",
   537  											ValueFrom: &clusterv1.JSONPatchValue{
   538  												Variable: pointer.String("variableName"),
   539  											},
   540  										},
   541  									},
   542  								},
   543  							},
   544  						},
   545  					},
   546  					Variables: []clusterv1.ClusterClassVariable{
   547  						{
   548  							Name:     "variableName",
   549  							Required: true,
   550  							Schema: clusterv1.VariableSchema{
   551  								OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   552  									Type: "string",
   553  								},
   554  							},
   555  						},
   556  					},
   557  				},
   558  			},
   559  			wantErr: true,
   560  		},
   561  		{
   562  			name: "error if jsonPatch path uses any index for remove i.e. 0 or -.",
   563  			clusterClass: clusterv1.ClusterClass{
   564  				Spec: clusterv1.ClusterClassSpec{
   565  					ControlPlane: clusterv1.ControlPlaneClass{
   566  						LocalObjectTemplate: clusterv1.LocalObjectTemplate{
   567  							Ref: &corev1.ObjectReference{
   568  								APIVersion: "controlplane.cluster.x-k8s.io/v1beta1",
   569  								Kind:       "ControlPlaneTemplate",
   570  							},
   571  						},
   572  					},
   573  
   574  					Patches: []clusterv1.ClusterClassPatch{
   575  
   576  						{
   577  							Name: "patch1",
   578  							Definitions: []clusterv1.PatchDefinition{
   579  								{
   580  									Selector: clusterv1.PatchSelector{
   581  										APIVersion: "controlplane.cluster.x-k8s.io/v1beta1",
   582  										Kind:       "ControlPlaneTemplate",
   583  										MatchResources: clusterv1.PatchSelectorMatch{
   584  											ControlPlane: true,
   585  										},
   586  									},
   587  									JSONPatches: []clusterv1.JSONPatch{
   588  										{
   589  											Op:   "remove",
   590  											Path: "/spec/template/0/",
   591  											ValueFrom: &clusterv1.JSONPatchValue{
   592  												Variable: pointer.String("variableName"),
   593  											},
   594  										},
   595  									},
   596  								},
   597  							},
   598  						},
   599  					},
   600  					Variables: []clusterv1.ClusterClassVariable{
   601  						{
   602  							Name:     "variableName",
   603  							Required: true,
   604  							Schema: clusterv1.VariableSchema{
   605  								OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   606  									Type: "string",
   607  								},
   608  							},
   609  						},
   610  					},
   611  				},
   612  			},
   613  			wantErr: true,
   614  		},
   615  		{
   616  			name: "error if jsonPatch path uses any index for replace i.e. 0",
   617  			clusterClass: clusterv1.ClusterClass{
   618  				Spec: clusterv1.ClusterClassSpec{
   619  					ControlPlane: clusterv1.ControlPlaneClass{
   620  						LocalObjectTemplate: clusterv1.LocalObjectTemplate{
   621  							Ref: &corev1.ObjectReference{
   622  								APIVersion: "controlplane.cluster.x-k8s.io/v1beta1",
   623  								Kind:       "ControlPlaneTemplate",
   624  							},
   625  						},
   626  					},
   627  
   628  					Patches: []clusterv1.ClusterClassPatch{
   629  
   630  						{
   631  							Name: "patch1",
   632  							Definitions: []clusterv1.PatchDefinition{
   633  								{
   634  									Selector: clusterv1.PatchSelector{
   635  										APIVersion: "controlplane.cluster.x-k8s.io/v1beta1",
   636  										Kind:       "ControlPlaneTemplate",
   637  										MatchResources: clusterv1.PatchSelectorMatch{
   638  											ControlPlane: true,
   639  										},
   640  									},
   641  									JSONPatches: []clusterv1.JSONPatch{
   642  										{
   643  											Op:   "replace",
   644  											Path: "/spec/template/0/",
   645  											ValueFrom: &clusterv1.JSONPatchValue{
   646  												Variable: pointer.String("variableName"),
   647  											},
   648  										},
   649  									},
   650  								},
   651  							},
   652  						},
   653  					},
   654  					Variables: []clusterv1.ClusterClassVariable{
   655  						{
   656  							Name:     "variableName",
   657  							Required: true,
   658  							Schema: clusterv1.VariableSchema{
   659  								OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   660  									Type: "string",
   661  								},
   662  							},
   663  						},
   664  					},
   665  				},
   666  			},
   667  			wantErr: true,
   668  		},
   669  
   670  		// Patch Value/ValueFrom validation
   671  		{
   672  			name: "error if jsonPatch has neither Value nor ValueFrom",
   673  			clusterClass: clusterv1.ClusterClass{
   674  				Spec: clusterv1.ClusterClassSpec{
   675  					ControlPlane: clusterv1.ControlPlaneClass{
   676  						LocalObjectTemplate: clusterv1.LocalObjectTemplate{
   677  							Ref: &corev1.ObjectReference{
   678  								APIVersion: "controlplane.cluster.x-k8s.io/v1beta1",
   679  								Kind:       "ControlPlaneTemplate",
   680  							},
   681  						},
   682  					},
   683  
   684  					Patches: []clusterv1.ClusterClassPatch{
   685  
   686  						{
   687  							Name: "patch1",
   688  							Definitions: []clusterv1.PatchDefinition{
   689  								{
   690  									Selector: clusterv1.PatchSelector{
   691  										APIVersion: "controlplane.cluster.x-k8s.io/v1beta1",
   692  										Kind:       "ControlPlaneTemplate",
   693  										MatchResources: clusterv1.PatchSelectorMatch{
   694  											ControlPlane: true,
   695  										},
   696  									},
   697  									JSONPatches: []clusterv1.JSONPatch{
   698  										{
   699  											Op:   "add",
   700  											Path: "/spec/template/spec/",
   701  											// Value and ValueFrom not defined.
   702  										},
   703  									},
   704  								},
   705  							},
   706  						},
   707  					},
   708  				},
   709  			},
   710  			wantErr: true,
   711  		},
   712  		{
   713  			name: "error if jsonPatch has both Value and ValueFrom",
   714  			clusterClass: clusterv1.ClusterClass{
   715  				Spec: clusterv1.ClusterClassSpec{
   716  					ControlPlane: clusterv1.ControlPlaneClass{
   717  						LocalObjectTemplate: clusterv1.LocalObjectTemplate{
   718  							Ref: &corev1.ObjectReference{
   719  								APIVersion: "controlplane.cluster.x-k8s.io/v1beta1",
   720  								Kind:       "ControlPlaneTemplate",
   721  							},
   722  						},
   723  					},
   724  					Patches: []clusterv1.ClusterClassPatch{
   725  
   726  						{
   727  							Name: "patch1",
   728  							Definitions: []clusterv1.PatchDefinition{
   729  								{
   730  									Selector: clusterv1.PatchSelector{
   731  										APIVersion: "controlplane.cluster.x-k8s.io/v1beta1",
   732  										Kind:       "ControlPlaneTemplate",
   733  										MatchResources: clusterv1.PatchSelectorMatch{
   734  											ControlPlane: true,
   735  										},
   736  									},
   737  									JSONPatches: []clusterv1.JSONPatch{
   738  										{
   739  											Op:   "add",
   740  											Path: "/spec/template/spec/",
   741  											ValueFrom: &clusterv1.JSONPatchValue{
   742  												Variable: pointer.String("variableName"),
   743  											},
   744  											Value: &apiextensionsv1.JSON{Raw: []byte("1")},
   745  										},
   746  									},
   747  								},
   748  							},
   749  						},
   750  					},
   751  					Variables: []clusterv1.ClusterClassVariable{
   752  						{
   753  							Name:     "variableName",
   754  							Required: true,
   755  							Schema: clusterv1.VariableSchema{
   756  								OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   757  									Type: "string",
   758  								},
   759  							},
   760  						},
   761  					},
   762  				},
   763  			},
   764  			wantErr: true,
   765  		},
   766  
   767  		// Patch value validation
   768  		{
   769  			name: "pass if jsonPatch value is valid json literal",
   770  			clusterClass: clusterv1.ClusterClass{
   771  				Spec: clusterv1.ClusterClassSpec{
   772  					ControlPlane: clusterv1.ControlPlaneClass{
   773  						LocalObjectTemplate: clusterv1.LocalObjectTemplate{
   774  							Ref: &corev1.ObjectReference{
   775  								APIVersion: "controlplane.cluster.x-k8s.io/v1beta1",
   776  								Kind:       "ControlPlaneTemplate",
   777  							},
   778  						},
   779  					},
   780  					Patches: []clusterv1.ClusterClassPatch{
   781  
   782  						{
   783  							Name: "patch1",
   784  							Definitions: []clusterv1.PatchDefinition{
   785  								{
   786  									Selector: clusterv1.PatchSelector{
   787  										APIVersion: "controlplane.cluster.x-k8s.io/v1beta1",
   788  										Kind:       "ControlPlaneTemplate",
   789  										MatchResources: clusterv1.PatchSelectorMatch{
   790  											ControlPlane: true,
   791  										},
   792  									},
   793  									JSONPatches: []clusterv1.JSONPatch{
   794  										{
   795  											Op:    "add",
   796  											Path:  "/spec/template/spec/",
   797  											Value: &apiextensionsv1.JSON{Raw: []byte(`"stringValue"`)},
   798  										},
   799  									},
   800  								},
   801  							},
   802  						},
   803  					},
   804  				},
   805  			},
   806  			wantErr: false,
   807  		},
   808  		{
   809  			name: "pass if jsonPatch value is valid json",
   810  			clusterClass: clusterv1.ClusterClass{
   811  				Spec: clusterv1.ClusterClassSpec{
   812  					ControlPlane: clusterv1.ControlPlaneClass{
   813  						LocalObjectTemplate: clusterv1.LocalObjectTemplate{
   814  							Ref: &corev1.ObjectReference{
   815  								APIVersion: "controlplane.cluster.x-k8s.io/v1beta1",
   816  								Kind:       "ControlPlaneTemplate",
   817  							},
   818  						},
   819  					},
   820  					Patches: []clusterv1.ClusterClassPatch{
   821  
   822  						{
   823  							Name: "patch1",
   824  							Definitions: []clusterv1.PatchDefinition{
   825  								{
   826  									Selector: clusterv1.PatchSelector{
   827  										APIVersion: "controlplane.cluster.x-k8s.io/v1beta1",
   828  										Kind:       "ControlPlaneTemplate",
   829  										MatchResources: clusterv1.PatchSelectorMatch{
   830  											ControlPlane: true,
   831  										},
   832  									},
   833  									JSONPatches: []clusterv1.JSONPatch{
   834  										{
   835  											Op:   "add",
   836  											Path: "/spec/template/spec/",
   837  											Value: &apiextensionsv1.JSON{Raw: []byte(
   838  												"{\"id\": \"file\"" +
   839  													"," +
   840  													"\"value\": \"File\"}")},
   841  										},
   842  									},
   843  								},
   844  							},
   845  						},
   846  					},
   847  				},
   848  			},
   849  			wantErr: false,
   850  		},
   851  		{
   852  			name: "pass if jsonPatch value is nil",
   853  			clusterClass: clusterv1.ClusterClass{
   854  				Spec: clusterv1.ClusterClassSpec{
   855  					ControlPlane: clusterv1.ControlPlaneClass{
   856  						LocalObjectTemplate: clusterv1.LocalObjectTemplate{
   857  							Ref: &corev1.ObjectReference{
   858  								APIVersion: "controlplane.cluster.x-k8s.io/v1beta1",
   859  								Kind:       "ControlPlaneTemplate",
   860  							},
   861  						},
   862  					},
   863  					Patches: []clusterv1.ClusterClassPatch{
   864  
   865  						{
   866  							Name: "patch1",
   867  							Definitions: []clusterv1.PatchDefinition{
   868  								{
   869  									Selector: clusterv1.PatchSelector{
   870  										APIVersion: "controlplane.cluster.x-k8s.io/v1beta1",
   871  										Kind:       "ControlPlaneTemplate",
   872  										MatchResources: clusterv1.PatchSelectorMatch{
   873  											ControlPlane: true,
   874  										},
   875  									},
   876  									JSONPatches: []clusterv1.JSONPatch{
   877  										{
   878  											Op:   "add",
   879  											Path: "/spec/template/spec/",
   880  											Value: &apiextensionsv1.JSON{
   881  												Raw: nil,
   882  											},
   883  										},
   884  									},
   885  								},
   886  							},
   887  						},
   888  					},
   889  				},
   890  			},
   891  			wantErr: false,
   892  		},
   893  		{
   894  			name: "error if jsonPatch value is invalid json",
   895  			clusterClass: clusterv1.ClusterClass{
   896  				Spec: clusterv1.ClusterClassSpec{
   897  					ControlPlane: clusterv1.ControlPlaneClass{
   898  						LocalObjectTemplate: clusterv1.LocalObjectTemplate{
   899  							Ref: &corev1.ObjectReference{
   900  								APIVersion: "controlplane.cluster.x-k8s.io/v1beta1",
   901  								Kind:       "ControlPlaneTemplate",
   902  							},
   903  						},
   904  					},
   905  
   906  					Patches: []clusterv1.ClusterClassPatch{
   907  
   908  						{
   909  							Name: "patch1",
   910  							Definitions: []clusterv1.PatchDefinition{
   911  								{
   912  									Selector: clusterv1.PatchSelector{
   913  										APIVersion: "controlplane.cluster.x-k8s.io/v1beta1",
   914  										Kind:       "ControlPlaneTemplate",
   915  										MatchResources: clusterv1.PatchSelectorMatch{
   916  											ControlPlane: true,
   917  										},
   918  									},
   919  									JSONPatches: []clusterv1.JSONPatch{
   920  										{
   921  											Op:   "add",
   922  											Path: "/spec/template/spec/",
   923  											Value: &apiextensionsv1.JSON{Raw: []byte(
   924  												"{\"id\": \"file\"" +
   925  													// missing comma here +
   926  													"\"value\": \"File\"}")},
   927  										},
   928  									},
   929  								},
   930  							},
   931  						},
   932  					},
   933  				},
   934  			},
   935  			wantErr: true,
   936  		},
   937  
   938  		// Patch valueFrom validation
   939  		{
   940  			name: "error if jsonPatch defines neither ValueFrom.Template nor ValueFrom.Variable",
   941  			clusterClass: clusterv1.ClusterClass{
   942  				Spec: clusterv1.ClusterClassSpec{
   943  					ControlPlane: clusterv1.ControlPlaneClass{
   944  						LocalObjectTemplate: clusterv1.LocalObjectTemplate{
   945  							Ref: &corev1.ObjectReference{
   946  								APIVersion: "controlplane.cluster.x-k8s.io/v1beta1",
   947  								Kind:       "ControlPlaneTemplate",
   948  							},
   949  						},
   950  					},
   951  					Patches: []clusterv1.ClusterClassPatch{
   952  						{
   953  							Name: "patch1",
   954  							Definitions: []clusterv1.PatchDefinition{
   955  								{
   956  									Selector: clusterv1.PatchSelector{
   957  										APIVersion: "controlplane.cluster.x-k8s.io/v1beta1",
   958  										Kind:       "ControlPlaneTemplate",
   959  										MatchResources: clusterv1.PatchSelectorMatch{
   960  											ControlPlane: true,
   961  										},
   962  									},
   963  									JSONPatches: []clusterv1.JSONPatch{
   964  										{
   965  											Op:        "add",
   966  											Path:      "/spec/template/spec/",
   967  											ValueFrom: &clusterv1.JSONPatchValue{},
   968  										},
   969  									},
   970  								},
   971  							},
   972  						},
   973  					},
   974  				},
   975  			},
   976  			wantErr: true,
   977  		},
   978  		{
   979  			name: "error if jsonPatch has both ValueFrom.Template and ValueFrom.Variable",
   980  			clusterClass: clusterv1.ClusterClass{
   981  				Spec: clusterv1.ClusterClassSpec{
   982  					ControlPlane: clusterv1.ControlPlaneClass{
   983  						LocalObjectTemplate: clusterv1.LocalObjectTemplate{
   984  							Ref: &corev1.ObjectReference{
   985  								APIVersion: "controlplane.cluster.x-k8s.io/v1beta1",
   986  								Kind:       "ControlPlaneTemplate",
   987  							},
   988  						},
   989  					},
   990  					Patches: []clusterv1.ClusterClassPatch{
   991  						{
   992  							Name: "patch1",
   993  							Definitions: []clusterv1.PatchDefinition{
   994  								{
   995  									Selector: clusterv1.PatchSelector{
   996  										APIVersion: "controlplane.cluster.x-k8s.io/v1beta1",
   997  										Kind:       "ControlPlaneTemplate",
   998  										MatchResources: clusterv1.PatchSelectorMatch{
   999  											ControlPlane: true,
  1000  										},
  1001  									},
  1002  									JSONPatches: []clusterv1.JSONPatch{
  1003  										{
  1004  											Op:   "add",
  1005  											Path: "/spec/template/spec/",
  1006  											ValueFrom: &clusterv1.JSONPatchValue{
  1007  												Variable: pointer.String("variableName"),
  1008  												Template: pointer.String(`template {{ .variableB }}`),
  1009  											},
  1010  										},
  1011  									},
  1012  								},
  1013  							},
  1014  						},
  1015  					},
  1016  					Variables: []clusterv1.ClusterClassVariable{
  1017  						{
  1018  							Name:     "variableName",
  1019  							Required: true,
  1020  							Schema: clusterv1.VariableSchema{
  1021  								OpenAPIV3Schema: clusterv1.JSONSchemaProps{
  1022  									Type: "string",
  1023  								},
  1024  							},
  1025  						},
  1026  					},
  1027  				},
  1028  			},
  1029  			wantErr: true,
  1030  		},
  1031  
  1032  		// Patch valueFrom.Template validation
  1033  		{
  1034  			name: "pass if jsonPatch defines a valid ValueFrom.Template",
  1035  			clusterClass: clusterv1.ClusterClass{
  1036  				Spec: clusterv1.ClusterClassSpec{
  1037  					ControlPlane: clusterv1.ControlPlaneClass{
  1038  						LocalObjectTemplate: clusterv1.LocalObjectTemplate{
  1039  							Ref: &corev1.ObjectReference{
  1040  								APIVersion: "controlplane.cluster.x-k8s.io/v1beta1",
  1041  								Kind:       "ControlPlaneTemplate",
  1042  							},
  1043  						},
  1044  					},
  1045  					Patches: []clusterv1.ClusterClassPatch{
  1046  						{
  1047  							Name: "patch1",
  1048  							Definitions: []clusterv1.PatchDefinition{
  1049  								{
  1050  									Selector: clusterv1.PatchSelector{
  1051  										APIVersion: "controlplane.cluster.x-k8s.io/v1beta1",
  1052  										Kind:       "ControlPlaneTemplate",
  1053  										MatchResources: clusterv1.PatchSelectorMatch{
  1054  											ControlPlane: true,
  1055  										},
  1056  									},
  1057  									JSONPatches: []clusterv1.JSONPatch{
  1058  										{
  1059  											Op:   "add",
  1060  											Path: "/spec/template/spec/",
  1061  											ValueFrom: &clusterv1.JSONPatchValue{
  1062  												Template: pointer.String(`template {{ .variableB }}`),
  1063  											},
  1064  										},
  1065  									},
  1066  								},
  1067  							},
  1068  						},
  1069  					},
  1070  					Variables: []clusterv1.ClusterClassVariable{
  1071  						{
  1072  							Name:     "variableName",
  1073  							Required: true,
  1074  							Schema: clusterv1.VariableSchema{
  1075  								OpenAPIV3Schema: clusterv1.JSONSchemaProps{
  1076  									Type: "string",
  1077  								},
  1078  							},
  1079  						},
  1080  					},
  1081  				},
  1082  			},
  1083  			wantErr: false,
  1084  		},
  1085  		{
  1086  			name: "error if jsonPatch defines an invalid ValueFrom.Template",
  1087  			clusterClass: clusterv1.ClusterClass{
  1088  				Spec: clusterv1.ClusterClassSpec{
  1089  					ControlPlane: clusterv1.ControlPlaneClass{
  1090  						LocalObjectTemplate: clusterv1.LocalObjectTemplate{
  1091  							Ref: &corev1.ObjectReference{
  1092  								APIVersion: "controlplane.cluster.x-k8s.io/v1beta1",
  1093  								Kind:       "ControlPlaneTemplate",
  1094  							},
  1095  						},
  1096  					},
  1097  					Patches: []clusterv1.ClusterClassPatch{
  1098  						{
  1099  							Name: "patch1",
  1100  							Definitions: []clusterv1.PatchDefinition{
  1101  								{
  1102  									Selector: clusterv1.PatchSelector{
  1103  										APIVersion: "controlplane.cluster.x-k8s.io/v1beta1",
  1104  										Kind:       "ControlPlaneTemplate",
  1105  										MatchResources: clusterv1.PatchSelectorMatch{
  1106  											ControlPlane: true,
  1107  										},
  1108  									},
  1109  									JSONPatches: []clusterv1.JSONPatch{
  1110  										{
  1111  											Op:   "add",
  1112  											Path: "/spec/template/spec/",
  1113  											ValueFrom: &clusterv1.JSONPatchValue{
  1114  												// Template is invalid - too many leading curly braces.
  1115  												Template: pointer.String(`template {{{{{{{{ .variableB }}`),
  1116  											},
  1117  										},
  1118  									},
  1119  								},
  1120  							},
  1121  						},
  1122  					},
  1123  					Variables: []clusterv1.ClusterClassVariable{
  1124  						{
  1125  							Name:     "variableName",
  1126  							Required: true,
  1127  							Schema: clusterv1.VariableSchema{
  1128  								OpenAPIV3Schema: clusterv1.JSONSchemaProps{
  1129  									Type: "string",
  1130  								},
  1131  							},
  1132  						},
  1133  					},
  1134  				},
  1135  			},
  1136  			wantErr: true,
  1137  		},
  1138  
  1139  		// Patch valueFrom.Variable validation
  1140  		{
  1141  			name: "error if jsonPatch valueFrom uses a variable which is not defined",
  1142  			clusterClass: clusterv1.ClusterClass{
  1143  				Spec: clusterv1.ClusterClassSpec{
  1144  					ControlPlane: clusterv1.ControlPlaneClass{
  1145  						LocalObjectTemplate: clusterv1.LocalObjectTemplate{
  1146  							Ref: &corev1.ObjectReference{
  1147  								APIVersion: "controlplane.cluster.x-k8s.io/v1beta1",
  1148  								Kind:       "ControlPlaneTemplate",
  1149  							},
  1150  						},
  1151  					},
  1152  					Patches: []clusterv1.ClusterClassPatch{
  1153  						{
  1154  							Name: "patch1",
  1155  							Definitions: []clusterv1.PatchDefinition{
  1156  								{
  1157  									Selector: clusterv1.PatchSelector{
  1158  										APIVersion: "controlplane.cluster.x-k8s.io/v1beta1",
  1159  										Kind:       "ControlPlaneTemplate",
  1160  										MatchResources: clusterv1.PatchSelectorMatch{
  1161  											ControlPlane: true,
  1162  										},
  1163  									},
  1164  									JSONPatches: []clusterv1.JSONPatch{
  1165  										{
  1166  											Op:   "add",
  1167  											Path: "/spec/template/spec/",
  1168  											ValueFrom: &clusterv1.JSONPatchValue{
  1169  												Variable: pointer.String("undefinedVariable"),
  1170  											},
  1171  										},
  1172  									},
  1173  								},
  1174  							},
  1175  						},
  1176  					},
  1177  					Variables: []clusterv1.ClusterClassVariable{
  1178  						{
  1179  							Name:     "variableName",
  1180  							Required: true,
  1181  							Schema: clusterv1.VariableSchema{
  1182  								OpenAPIV3Schema: clusterv1.JSONSchemaProps{
  1183  									Type: "string",
  1184  								},
  1185  							},
  1186  						},
  1187  					},
  1188  				},
  1189  			},
  1190  			wantErr: true,
  1191  		},
  1192  		{
  1193  			name: "pass if jsonPatch uses a user-defined variable which is defined",
  1194  			clusterClass: clusterv1.ClusterClass{
  1195  				Spec: clusterv1.ClusterClassSpec{
  1196  					ControlPlane: clusterv1.ControlPlaneClass{
  1197  						LocalObjectTemplate: clusterv1.LocalObjectTemplate{
  1198  							Ref: &corev1.ObjectReference{
  1199  								APIVersion: "controlplane.cluster.x-k8s.io/v1beta1",
  1200  								Kind:       "ControlPlaneTemplate",
  1201  							},
  1202  						},
  1203  					},
  1204  					Patches: []clusterv1.ClusterClassPatch{
  1205  						{
  1206  							Name: "patch1",
  1207  							Definitions: []clusterv1.PatchDefinition{
  1208  								{
  1209  									Selector: clusterv1.PatchSelector{
  1210  										APIVersion: "controlplane.cluster.x-k8s.io/v1beta1",
  1211  										Kind:       "ControlPlaneTemplate",
  1212  										MatchResources: clusterv1.PatchSelectorMatch{
  1213  											ControlPlane: true,
  1214  										},
  1215  									},
  1216  									JSONPatches: []clusterv1.JSONPatch{
  1217  										{
  1218  											Op:   "add",
  1219  											Path: "/spec/template/spec/",
  1220  											ValueFrom: &clusterv1.JSONPatchValue{
  1221  												Variable: pointer.String("variableName"),
  1222  											},
  1223  										},
  1224  									},
  1225  								},
  1226  							},
  1227  						},
  1228  					},
  1229  					Variables: []clusterv1.ClusterClassVariable{
  1230  						{
  1231  							Name:     "variableName",
  1232  							Required: true,
  1233  							Schema: clusterv1.VariableSchema{
  1234  								OpenAPIV3Schema: clusterv1.JSONSchemaProps{
  1235  									Type: "string",
  1236  								},
  1237  							},
  1238  						},
  1239  					},
  1240  				},
  1241  			},
  1242  			wantErr: false,
  1243  		},
  1244  		{
  1245  			name: "pass if jsonPatch uses a nested user-defined variable which is defined",
  1246  			clusterClass: clusterv1.ClusterClass{
  1247  				Spec: clusterv1.ClusterClassSpec{
  1248  					ControlPlane: clusterv1.ControlPlaneClass{
  1249  						LocalObjectTemplate: clusterv1.LocalObjectTemplate{
  1250  							Ref: &corev1.ObjectReference{
  1251  								APIVersion: "controlplane.cluster.x-k8s.io/v1beta1",
  1252  								Kind:       "ControlPlaneTemplate",
  1253  							},
  1254  						},
  1255  					},
  1256  					Patches: []clusterv1.ClusterClassPatch{
  1257  						{
  1258  							Name: "patch1",
  1259  							Definitions: []clusterv1.PatchDefinition{
  1260  								{
  1261  									Selector: clusterv1.PatchSelector{
  1262  										APIVersion: "controlplane.cluster.x-k8s.io/v1beta1",
  1263  										Kind:       "ControlPlaneTemplate",
  1264  										MatchResources: clusterv1.PatchSelectorMatch{
  1265  											ControlPlane: true,
  1266  										},
  1267  									},
  1268  									JSONPatches: []clusterv1.JSONPatch{
  1269  										{
  1270  											Op:   "add",
  1271  											Path: "/spec/template/spec/",
  1272  											ValueFrom: &clusterv1.JSONPatchValue{
  1273  												Variable: pointer.String("variableName.nestedField"),
  1274  											},
  1275  										},
  1276  									},
  1277  								},
  1278  							},
  1279  						},
  1280  					},
  1281  					Variables: []clusterv1.ClusterClassVariable{
  1282  						{
  1283  							Name:     "variableName",
  1284  							Required: true,
  1285  							Schema: clusterv1.VariableSchema{
  1286  								OpenAPIV3Schema: clusterv1.JSONSchemaProps{
  1287  									Type: "object",
  1288  									Properties: map[string]clusterv1.JSONSchemaProps{
  1289  										"nestedField": {
  1290  											Type: "string",
  1291  										},
  1292  									},
  1293  								},
  1294  							},
  1295  						},
  1296  					},
  1297  				},
  1298  			},
  1299  			wantErr: false,
  1300  		},
  1301  		{
  1302  			name: "error if jsonPatch uses a builtin variable which is not defined",
  1303  			clusterClass: clusterv1.ClusterClass{
  1304  				Spec: clusterv1.ClusterClassSpec{
  1305  					ControlPlane: clusterv1.ControlPlaneClass{
  1306  						LocalObjectTemplate: clusterv1.LocalObjectTemplate{
  1307  							Ref: &corev1.ObjectReference{
  1308  								APIVersion: "controlplane.cluster.x-k8s.io/v1beta1",
  1309  								Kind:       "ControlPlaneTemplate",
  1310  							},
  1311  						},
  1312  					},
  1313  					Patches: []clusterv1.ClusterClassPatch{
  1314  						{
  1315  							Name: "patch1",
  1316  							Definitions: []clusterv1.PatchDefinition{
  1317  								{
  1318  									Selector: clusterv1.PatchSelector{
  1319  										APIVersion: "controlplane.cluster.x-k8s.io/v1beta1",
  1320  										Kind:       "ControlPlaneTemplate",
  1321  										MatchResources: clusterv1.PatchSelectorMatch{
  1322  											ControlPlane: true,
  1323  										},
  1324  									},
  1325  									JSONPatches: []clusterv1.JSONPatch{
  1326  										{
  1327  											Op:   "add",
  1328  											Path: "/spec/template/spec/",
  1329  											ValueFrom: &clusterv1.JSONPatchValue{
  1330  												Variable: pointer.String("builtin.notDefined"),
  1331  											},
  1332  										},
  1333  									},
  1334  								},
  1335  							},
  1336  						},
  1337  					},
  1338  				},
  1339  			},
  1340  			wantErr: true,
  1341  		},
  1342  		{
  1343  			name: "pass if jsonPatch uses a builtin variable which is defined",
  1344  			clusterClass: clusterv1.ClusterClass{
  1345  				Spec: clusterv1.ClusterClassSpec{
  1346  					ControlPlane: clusterv1.ControlPlaneClass{
  1347  						LocalObjectTemplate: clusterv1.LocalObjectTemplate{
  1348  							Ref: &corev1.ObjectReference{
  1349  								APIVersion: "controlplane.cluster.x-k8s.io/v1beta1",
  1350  								Kind:       "ControlPlaneTemplate",
  1351  							},
  1352  						},
  1353  					},
  1354  
  1355  					Patches: []clusterv1.ClusterClassPatch{
  1356  
  1357  						{
  1358  							Name: "patch1",
  1359  							Definitions: []clusterv1.PatchDefinition{
  1360  								{
  1361  									Selector: clusterv1.PatchSelector{
  1362  										APIVersion: "controlplane.cluster.x-k8s.io/v1beta1",
  1363  										Kind:       "ControlPlaneTemplate",
  1364  										MatchResources: clusterv1.PatchSelectorMatch{
  1365  											ControlPlane: true,
  1366  										},
  1367  									},
  1368  									JSONPatches: []clusterv1.JSONPatch{
  1369  										{
  1370  											Op:   "add",
  1371  											Path: "/spec/template/spec/",
  1372  											ValueFrom: &clusterv1.JSONPatchValue{
  1373  												Variable: pointer.String("builtin.machineDeployment.version"),
  1374  											},
  1375  										},
  1376  									},
  1377  								},
  1378  							},
  1379  						},
  1380  					},
  1381  				},
  1382  			},
  1383  			wantErr: false,
  1384  		},
  1385  
  1386  		// Patch with External
  1387  		{
  1388  			name: "pass if patch defines both external.generateExtension and external.validateExtension",
  1389  			clusterClass: clusterv1.ClusterClass{
  1390  				Spec: clusterv1.ClusterClassSpec{
  1391  					ControlPlane: clusterv1.ControlPlaneClass{
  1392  						LocalObjectTemplate: clusterv1.LocalObjectTemplate{
  1393  							Ref: &corev1.ObjectReference{
  1394  								APIVersion: "controlplane.cluster.x-k8s.io/v1beta1",
  1395  								Kind:       "ControlPlaneTemplate",
  1396  							},
  1397  						},
  1398  					},
  1399  
  1400  					Patches: []clusterv1.ClusterClassPatch{
  1401  						{
  1402  							Name: "patch1",
  1403  							External: &clusterv1.ExternalPatchDefinition{
  1404  								GenerateExtension: pointer.String("generate-extension"),
  1405  								ValidateExtension: pointer.String("generate-extension"),
  1406  							},
  1407  						},
  1408  					},
  1409  				},
  1410  			},
  1411  			runtimeSDK: true,
  1412  			wantErr:    false,
  1413  		},
  1414  		{
  1415  			name: "error if patch defines both external and RuntimeSDK is not enabled",
  1416  			clusterClass: clusterv1.ClusterClass{
  1417  				Spec: clusterv1.ClusterClassSpec{
  1418  					ControlPlane: clusterv1.ControlPlaneClass{
  1419  						LocalObjectTemplate: clusterv1.LocalObjectTemplate{
  1420  							Ref: &corev1.ObjectReference{
  1421  								APIVersion: "controlplane.cluster.x-k8s.io/v1beta1",
  1422  								Kind:       "ControlPlaneTemplate",
  1423  							},
  1424  						},
  1425  					},
  1426  
  1427  					Patches: []clusterv1.ClusterClassPatch{
  1428  						{
  1429  							Name: "patch1",
  1430  							External: &clusterv1.ExternalPatchDefinition{
  1431  								GenerateExtension: pointer.String("generate-extension"),
  1432  								ValidateExtension: pointer.String("generate-extension"),
  1433  							},
  1434  						},
  1435  					},
  1436  				},
  1437  			},
  1438  			runtimeSDK: false,
  1439  			wantErr:    true,
  1440  		},
  1441  		{
  1442  			name: "error if patch defines neither external.generateExtension nor external.validateExtension",
  1443  			clusterClass: clusterv1.ClusterClass{
  1444  				Spec: clusterv1.ClusterClassSpec{
  1445  					ControlPlane: clusterv1.ControlPlaneClass{
  1446  						LocalObjectTemplate: clusterv1.LocalObjectTemplate{
  1447  							Ref: &corev1.ObjectReference{
  1448  								APIVersion: "controlplane.cluster.x-k8s.io/v1beta1",
  1449  								Kind:       "ControlPlaneTemplate",
  1450  							},
  1451  						},
  1452  					},
  1453  
  1454  					Patches: []clusterv1.ClusterClassPatch{
  1455  						{
  1456  							Name:     "patch1",
  1457  							External: &clusterv1.ExternalPatchDefinition{},
  1458  						},
  1459  					},
  1460  				},
  1461  			},
  1462  			runtimeSDK: true,
  1463  			wantErr:    true,
  1464  		},
  1465  		{
  1466  			name: "error if patch defines both external and definitions",
  1467  			clusterClass: clusterv1.ClusterClass{
  1468  				Spec: clusterv1.ClusterClassSpec{
  1469  					ControlPlane: clusterv1.ControlPlaneClass{
  1470  						LocalObjectTemplate: clusterv1.LocalObjectTemplate{
  1471  							Ref: &corev1.ObjectReference{
  1472  								APIVersion: "controlplane.cluster.x-k8s.io/v1beta1",
  1473  								Kind:       "ControlPlaneTemplate",
  1474  							},
  1475  						},
  1476  					},
  1477  
  1478  					Patches: []clusterv1.ClusterClassPatch{
  1479  						{
  1480  							Name: "patch1",
  1481  							External: &clusterv1.ExternalPatchDefinition{
  1482  								GenerateExtension: pointer.String("generate-extension"),
  1483  								ValidateExtension: pointer.String("generate-extension"),
  1484  							},
  1485  							Definitions: []clusterv1.PatchDefinition{},
  1486  						},
  1487  					},
  1488  				},
  1489  			},
  1490  			runtimeSDK: true,
  1491  			wantErr:    true,
  1492  		},
  1493  		{
  1494  			name: "error if neither external nor definitions is defined",
  1495  			clusterClass: clusterv1.ClusterClass{
  1496  				Spec: clusterv1.ClusterClassSpec{
  1497  					ControlPlane: clusterv1.ControlPlaneClass{
  1498  						LocalObjectTemplate: clusterv1.LocalObjectTemplate{
  1499  							Ref: &corev1.ObjectReference{
  1500  								APIVersion: "controlplane.cluster.x-k8s.io/v1beta1",
  1501  								Kind:       "ControlPlaneTemplate",
  1502  							},
  1503  						},
  1504  					},
  1505  
  1506  					Patches: []clusterv1.ClusterClassPatch{
  1507  						{
  1508  							Name: "patch1",
  1509  						},
  1510  					},
  1511  				},
  1512  			},
  1513  			runtimeSDK: true,
  1514  			wantErr:    true,
  1515  		},
  1516  	}
  1517  	for i := range tests {
  1518  		tt := tests[i]
  1519  		t.Run(tt.name, func(t *testing.T) {
  1520  			defer utilfeature.SetFeatureGateDuringTest(t, feature.Gates, feature.RuntimeSDK, tt.runtimeSDK)()
  1521  
  1522  			g := NewWithT(t)
  1523  
  1524  			errList := validatePatches(&tt.clusterClass)
  1525  			if tt.wantErr {
  1526  				g.Expect(errList).NotTo(BeEmpty())
  1527  				return
  1528  			}
  1529  			g.Expect(errList).To(BeEmpty())
  1530  		})
  1531  	}
  1532  }
  1533  
  1534  func Test_validateSelectors(t *testing.T) {
  1535  	tests := []struct {
  1536  		name         string
  1537  		selector     clusterv1.PatchSelector
  1538  		clusterClass *clusterv1.ClusterClass
  1539  		wantErr      bool
  1540  	}{
  1541  		{
  1542  			name: "error if selectors are all set to false or empty",
  1543  			selector: clusterv1.PatchSelector{
  1544  				APIVersion: "infrastructure.cluster.x-k8s.io/v1beta1",
  1545  				Kind:       "InfrastructureClusterTemplate",
  1546  				MatchResources: clusterv1.PatchSelectorMatch{
  1547  					ControlPlane:           false,
  1548  					InfrastructureCluster:  false,
  1549  					MachineDeploymentClass: &clusterv1.PatchSelectorMatchMachineDeploymentClass{},
  1550  				},
  1551  			},
  1552  			clusterClass: builder.ClusterClass(metav1.NamespaceDefault, "class1").
  1553  				WithControlPlaneTemplate(
  1554  					refToUnstructured(
  1555  						&corev1.ObjectReference{
  1556  							APIVersion: "infrastructure.cluster.x-k8s.io/v1beta1",
  1557  							Kind:       "InfrastructureClusterTemplate",
  1558  						}),
  1559  				).
  1560  				Build(),
  1561  			wantErr: true,
  1562  		},
  1563  		{
  1564  			name: "pass if selector targets an existing infrastructureCluster reference",
  1565  			selector: clusterv1.PatchSelector{
  1566  				APIVersion: "infrastructure.cluster.x-k8s.io/v1beta1",
  1567  				Kind:       "InfrastructureClusterTemplate",
  1568  				MatchResources: clusterv1.PatchSelectorMatch{
  1569  					InfrastructureCluster: true,
  1570  				},
  1571  			},
  1572  			clusterClass: builder.ClusterClass(metav1.NamespaceDefault, "class1").
  1573  				WithInfrastructureClusterTemplate(
  1574  					refToUnstructured(
  1575  						&corev1.ObjectReference{
  1576  							APIVersion: "infrastructure.cluster.x-k8s.io/v1beta1",
  1577  							Kind:       "InfrastructureClusterTemplate",
  1578  						}),
  1579  				).
  1580  				Build(),
  1581  		},
  1582  		{
  1583  			name: "error if selector targets a non-existing infrastructureCluster APIVersion reference",
  1584  			selector: clusterv1.PatchSelector{
  1585  				APIVersion: "infrastructure.cluster.x-k8s.io/v1beta1",
  1586  				Kind:       "InfrastructureClusterTemplate",
  1587  				MatchResources: clusterv1.PatchSelectorMatch{
  1588  					InfrastructureCluster: true,
  1589  				},
  1590  			},
  1591  			clusterClass: builder.ClusterClass(metav1.NamespaceDefault, "class1").
  1592  				WithInfrastructureClusterTemplate(
  1593  					refToUnstructured(
  1594  						&corev1.ObjectReference{
  1595  							APIVersion: "nonmatchinginfrastructure.cluster.x-k8s.io/v1beta1",
  1596  							Kind:       "InfrastructureClusterTemplate",
  1597  						}),
  1598  				).
  1599  				Build(),
  1600  			wantErr: true,
  1601  		},
  1602  		{
  1603  			name: "pass if selector targets an existing controlPlane reference",
  1604  			selector: clusterv1.PatchSelector{
  1605  				APIVersion: "controlplane.cluster.x-k8s.io/v1beta1",
  1606  				Kind:       "ControlPlaneTemplate",
  1607  				MatchResources: clusterv1.PatchSelectorMatch{
  1608  					ControlPlane: true,
  1609  				},
  1610  			},
  1611  			clusterClass: builder.ClusterClass(metav1.NamespaceDefault, "class1").
  1612  				WithControlPlaneTemplate(
  1613  					refToUnstructured(
  1614  						&corev1.ObjectReference{
  1615  							APIVersion: "controlplane.cluster.x-k8s.io/v1beta1",
  1616  							Kind:       "ControlPlaneTemplate",
  1617  						}),
  1618  				).
  1619  				Build(),
  1620  		},
  1621  		{
  1622  			name: "error if selector targets a non-existing controlPlane Kind reference",
  1623  			selector: clusterv1.PatchSelector{
  1624  				APIVersion: "controlplane.cluster.x-k8s.io/v1beta1",
  1625  				Kind:       "ControlPlaneTemplate",
  1626  				MatchResources: clusterv1.PatchSelectorMatch{
  1627  					ControlPlane: true,
  1628  				},
  1629  			},
  1630  			clusterClass: builder.ClusterClass(metav1.NamespaceDefault, "class1").
  1631  				WithControlPlaneTemplate(
  1632  					refToUnstructured(
  1633  						&corev1.ObjectReference{
  1634  							APIVersion: "controlplane.cluster.x-k8s.io/v1beta1",
  1635  							Kind:       "NonMatchingControlPlaneTemplate",
  1636  						}),
  1637  				).
  1638  				Build(),
  1639  			wantErr: true,
  1640  		},
  1641  		{
  1642  			name: "pass if selector targets an existing controlPlane machineInfrastructure reference",
  1643  			selector: clusterv1.PatchSelector{
  1644  				APIVersion: "infrastructure.cluster.x-k8s.io/v1beta1",
  1645  				Kind:       "InfrastructureMachineTemplate",
  1646  				MatchResources: clusterv1.PatchSelectorMatch{
  1647  					ControlPlane: true,
  1648  				},
  1649  			},
  1650  			clusterClass: builder.ClusterClass(metav1.NamespaceDefault, "class1").
  1651  				WithControlPlaneTemplate(
  1652  					refToUnstructured(
  1653  						&corev1.ObjectReference{
  1654  							APIVersion: "controlplane.cluster.x-k8s.io/v1beta1",
  1655  							Kind:       "NonMatchingControlPlaneTemplate",
  1656  						}),
  1657  				).
  1658  				WithControlPlaneInfrastructureMachineTemplate(
  1659  					refToUnstructured(
  1660  						&corev1.ObjectReference{
  1661  							APIVersion: "infrastructure.cluster.x-k8s.io/v1beta1",
  1662  							Kind:       "InfrastructureMachineTemplate",
  1663  						}),
  1664  				).
  1665  				Build(),
  1666  		},
  1667  		{
  1668  			name: "error if selector targets a non-existing controlPlane machineInfrastructure reference",
  1669  			selector: clusterv1.PatchSelector{
  1670  				APIVersion: "infrastructure.cluster.x-k8s.io/v1beta1",
  1671  				Kind:       "InfrastructureMachineTemplate",
  1672  				MatchResources: clusterv1.PatchSelectorMatch{
  1673  					ControlPlane: true,
  1674  				},
  1675  			},
  1676  			clusterClass: builder.ClusterClass(metav1.NamespaceDefault, "class1").
  1677  				WithControlPlaneTemplate(
  1678  					refToUnstructured(
  1679  						&corev1.ObjectReference{
  1680  							APIVersion: "controlplane.cluster.x-k8s.io/v1beta1",
  1681  							Kind:       "NonMatchingControlPlaneTemplate",
  1682  						}),
  1683  				).
  1684  				WithControlPlaneInfrastructureMachineTemplate(
  1685  					refToUnstructured(
  1686  						&corev1.ObjectReference{
  1687  							APIVersion: "infrastructure.cluster.x-k8s.io/v1beta1",
  1688  							Kind:       "NonMatchingInfrastructureMachineTemplate",
  1689  						}),
  1690  				).
  1691  				Build(),
  1692  			wantErr: true,
  1693  		},
  1694  		{
  1695  			name: "pass if selector targets an existing MachineDeploymentClass BootstrapTemplate",
  1696  			selector: clusterv1.PatchSelector{
  1697  				APIVersion: "bootstrap.cluster.x-k8s.io/v1beta1",
  1698  				Kind:       "BootstrapTemplate",
  1699  				MatchResources: clusterv1.PatchSelectorMatch{
  1700  					MachineDeploymentClass: &clusterv1.PatchSelectorMatchMachineDeploymentClass{
  1701  						Names: []string{"aa"},
  1702  					},
  1703  				},
  1704  			},
  1705  			clusterClass: builder.ClusterClass(metav1.NamespaceDefault, "class1").
  1706  				WithWorkerMachineDeploymentClasses(
  1707  					*builder.MachineDeploymentClass("aa").
  1708  						WithInfrastructureTemplate(
  1709  							refToUnstructured(&corev1.ObjectReference{
  1710  								APIVersion: "infrastructure.cluster.x-k8s.io/v1beta1",
  1711  								Kind:       "InfrastructureMachineTemplate",
  1712  							})).
  1713  						WithBootstrapTemplate(
  1714  							refToUnstructured(&corev1.ObjectReference{
  1715  								APIVersion: "bootstrap.cluster.x-k8s.io/v1beta1",
  1716  								Kind:       "BootstrapTemplate",
  1717  							})).
  1718  						Build(),
  1719  				).
  1720  				Build(),
  1721  		},
  1722  		{
  1723  			name: "pass if selector targets an existing MachineDeploymentClass InfrastructureTemplate",
  1724  			selector: clusterv1.PatchSelector{
  1725  				APIVersion: "infrastructure.cluster.x-k8s.io/v1beta1",
  1726  				Kind:       "InfrastructureMachineTemplate",
  1727  				MatchResources: clusterv1.PatchSelectorMatch{
  1728  					MachineDeploymentClass: &clusterv1.PatchSelectorMatchMachineDeploymentClass{
  1729  						Names: []string{"aa"},
  1730  					},
  1731  				},
  1732  			},
  1733  			clusterClass: builder.ClusterClass(metav1.NamespaceDefault, "class1").
  1734  				WithWorkerMachineDeploymentClasses(
  1735  					*builder.MachineDeploymentClass("aa").
  1736  						WithInfrastructureTemplate(
  1737  							refToUnstructured(&corev1.ObjectReference{
  1738  								APIVersion: "infrastructure.cluster.x-k8s.io/v1beta1",
  1739  								Kind:       "InfrastructureMachineTemplate",
  1740  							})).
  1741  						WithBootstrapTemplate(
  1742  							refToUnstructured(&corev1.ObjectReference{
  1743  								APIVersion: "bootstrap.cluster.x-k8s.io/v1beta1",
  1744  								Kind:       "BootstrapTemplate",
  1745  							})).
  1746  						Build(),
  1747  				).
  1748  				Build(),
  1749  		},
  1750  		{
  1751  			name: "error if selector targets a non-existing MachineDeploymentClass InfrastructureTemplate",
  1752  			selector: clusterv1.PatchSelector{
  1753  				APIVersion: "infrastructure.cluster.x-k8s.io/v1beta1",
  1754  				Kind:       "InfrastructureMachineTemplate",
  1755  				MatchResources: clusterv1.PatchSelectorMatch{
  1756  					MachineDeploymentClass: &clusterv1.PatchSelectorMatchMachineDeploymentClass{
  1757  						Names: []string{"bb"},
  1758  					},
  1759  				},
  1760  			},
  1761  			clusterClass: builder.ClusterClass(metav1.NamespaceDefault, "class1").
  1762  				WithWorkerMachineDeploymentClasses(
  1763  					*builder.MachineDeploymentClass("aa").
  1764  						WithInfrastructureTemplate(
  1765  							refToUnstructured(&corev1.ObjectReference{
  1766  								APIVersion: "infrastructure.cluster.x-k8s.io/v1beta1",
  1767  								Kind:       "InfrastructureMachineTemplate",
  1768  							})).
  1769  						WithBootstrapTemplate(
  1770  							refToUnstructured(&corev1.ObjectReference{
  1771  								APIVersion: "bootstrap.cluster.x-k8s.io/v1beta1",
  1772  								Kind:       "BootstrapTemplate",
  1773  							})).
  1774  						Build(),
  1775  					*builder.MachineDeploymentClass("bb").
  1776  						WithInfrastructureTemplate(
  1777  							refToUnstructured(&corev1.ObjectReference{
  1778  								APIVersion: "infrastructure.cluster.x-k8s.io/v1beta1",
  1779  								Kind:       "NonMatchingInfrastructureMachineTemplate",
  1780  							})).
  1781  						WithBootstrapTemplate(
  1782  							refToUnstructured(&corev1.ObjectReference{
  1783  								APIVersion: "bootstrap.cluster.x-k8s.io/v1beta1",
  1784  								Kind:       "BootstrapTemplate",
  1785  							})).
  1786  						Build(),
  1787  				).
  1788  				Build(),
  1789  			wantErr: true,
  1790  		},
  1791  		{
  1792  			name: "fail if selector targets ControlPlane Machine Infrastructure but does not have MatchResources.ControlPlane enabled",
  1793  			selector: clusterv1.PatchSelector{
  1794  				APIVersion: "infrastructure.cluster.x-k8s.io/v1beta1",
  1795  				Kind:       "InfrastructureMachineTemplate",
  1796  				MatchResources: clusterv1.PatchSelectorMatch{
  1797  					MachineDeploymentClass: &clusterv1.PatchSelectorMatchMachineDeploymentClass{
  1798  						Names: []string{"bb"},
  1799  					},
  1800  					ControlPlane: false,
  1801  				},
  1802  			},
  1803  			clusterClass: builder.ClusterClass(metav1.NamespaceDefault, "class1").
  1804  				WithControlPlaneInfrastructureMachineTemplate(
  1805  					refToUnstructured(
  1806  						&corev1.ObjectReference{
  1807  							APIVersion: "infrastructure.cluster.x-k8s.io/v1beta1",
  1808  							Kind:       "InfrastructureMachineTemplate",
  1809  						}),
  1810  				).
  1811  				Build(),
  1812  			wantErr: true,
  1813  		},
  1814  		{
  1815  			name: "error if selector targets an empty MachineDeploymentClass InfrastructureTemplate",
  1816  			selector: clusterv1.PatchSelector{
  1817  				APIVersion:     "infrastructure.cluster.x-k8s.io/v1beta1",
  1818  				Kind:           "InfrastructureMachineTemplate",
  1819  				MatchResources: clusterv1.PatchSelectorMatch{},
  1820  			},
  1821  			clusterClass: builder.ClusterClass(metav1.NamespaceDefault, "class1").
  1822  				WithWorkerMachineDeploymentClasses(
  1823  					*builder.MachineDeploymentClass("aa").
  1824  						WithInfrastructureTemplate(
  1825  							refToUnstructured(&corev1.ObjectReference{
  1826  								APIVersion: "infrastructure.cluster.x-k8s.io/v1beta1",
  1827  								Kind:       "InfrastructureMachineTemplate",
  1828  							})).
  1829  						WithBootstrapTemplate(
  1830  							refToUnstructured(&corev1.ObjectReference{
  1831  								APIVersion: "bootstrap.cluster.x-k8s.io/v1beta1",
  1832  								Kind:       "BootstrapTemplate",
  1833  							})).
  1834  						Build(),
  1835  					*builder.MachineDeploymentClass("bb").
  1836  						WithInfrastructureTemplate(
  1837  							refToUnstructured(&corev1.ObjectReference{
  1838  								APIVersion: "infrastructure.cluster.x-k8s.io/v1beta1",
  1839  								Kind:       "NonMatchingInfrastructureMachineTemplate",
  1840  							})).
  1841  						WithBootstrapTemplate(
  1842  							refToUnstructured(&corev1.ObjectReference{
  1843  								APIVersion: "bootstrap.cluster.x-k8s.io/v1beta1",
  1844  								Kind:       "BootstrapTemplate",
  1845  							})).
  1846  						Build(),
  1847  				).
  1848  				Build(),
  1849  			wantErr: true,
  1850  		},
  1851  		{
  1852  			name: "error if selector targets a bad pattern for matching MachineDeploymentClass InfrastructureTemplate",
  1853  			selector: clusterv1.PatchSelector{
  1854  				APIVersion: "infrastructure.cluster.x-k8s.io/v1beta1",
  1855  				Kind:       "InfrastructureMachineTemplate",
  1856  				MatchResources: clusterv1.PatchSelectorMatch{
  1857  					MachineDeploymentClass: &clusterv1.PatchSelectorMatchMachineDeploymentClass{
  1858  						Names: []string{"a*a"},
  1859  					},
  1860  				},
  1861  			},
  1862  			clusterClass: builder.ClusterClass(metav1.NamespaceDefault, "class1").
  1863  				WithWorkerMachineDeploymentClasses(
  1864  					*builder.MachineDeploymentClass("a-something-a").
  1865  						WithInfrastructureTemplate(
  1866  							refToUnstructured(&corev1.ObjectReference{
  1867  								APIVersion: "infrastructure.cluster.x-k8s.io/v1beta1",
  1868  								Kind:       "InfrastructureMachineTemplate",
  1869  							})).
  1870  						WithBootstrapTemplate(
  1871  							refToUnstructured(&corev1.ObjectReference{
  1872  								APIVersion: "bootstrap.cluster.x-k8s.io/v1beta1",
  1873  								Kind:       "BootstrapTemplate",
  1874  							})).
  1875  						Build(),
  1876  				).
  1877  				Build(),
  1878  			wantErr: true,
  1879  		},
  1880  		{
  1881  			name: "pass if selector targets an existing MachineDeploymentClass InfrastructureTemplate with prefix *",
  1882  			selector: clusterv1.PatchSelector{
  1883  				APIVersion: "infrastructure.cluster.x-k8s.io/v1beta1",
  1884  				Kind:       "InfrastructureMachineTemplate",
  1885  				MatchResources: clusterv1.PatchSelectorMatch{
  1886  					MachineDeploymentClass: &clusterv1.PatchSelectorMatchMachineDeploymentClass{
  1887  						Names: []string{"a-*"},
  1888  					},
  1889  				},
  1890  			},
  1891  			clusterClass: builder.ClusterClass(metav1.NamespaceDefault, "class1").
  1892  				WithWorkerMachineDeploymentClasses(
  1893  					*builder.MachineDeploymentClass("a-something-a").
  1894  						WithInfrastructureTemplate(
  1895  							refToUnstructured(&corev1.ObjectReference{
  1896  								APIVersion: "infrastructure.cluster.x-k8s.io/v1beta1",
  1897  								Kind:       "InfrastructureMachineTemplate",
  1898  							})).
  1899  						WithBootstrapTemplate(
  1900  							refToUnstructured(&corev1.ObjectReference{
  1901  								APIVersion: "bootstrap.cluster.x-k8s.io/v1beta1",
  1902  								Kind:       "BootstrapTemplate",
  1903  							})).
  1904  						Build(),
  1905  				).
  1906  				Build(),
  1907  		},
  1908  		{
  1909  			name: "pass if selector targets an existing MachineDeploymentClass InfrastructureTemplate with suffix *",
  1910  			selector: clusterv1.PatchSelector{
  1911  				APIVersion: "infrastructure.cluster.x-k8s.io/v1beta1",
  1912  				Kind:       "InfrastructureMachineTemplate",
  1913  				MatchResources: clusterv1.PatchSelectorMatch{
  1914  					MachineDeploymentClass: &clusterv1.PatchSelectorMatchMachineDeploymentClass{
  1915  						Names: []string{"*-a"},
  1916  					},
  1917  				},
  1918  			},
  1919  			clusterClass: builder.ClusterClass(metav1.NamespaceDefault, "class1").
  1920  				WithWorkerMachineDeploymentClasses(
  1921  					*builder.MachineDeploymentClass("a-something-a").
  1922  						WithInfrastructureTemplate(
  1923  							refToUnstructured(&corev1.ObjectReference{
  1924  								APIVersion: "infrastructure.cluster.x-k8s.io/v1beta1",
  1925  								Kind:       "InfrastructureMachineTemplate",
  1926  							})).
  1927  						WithBootstrapTemplate(
  1928  							refToUnstructured(&corev1.ObjectReference{
  1929  								APIVersion: "bootstrap.cluster.x-k8s.io/v1beta1",
  1930  								Kind:       "BootstrapTemplate",
  1931  							})).
  1932  						Build(),
  1933  				).
  1934  				Build(),
  1935  		},
  1936  		{
  1937  			name: "pass if selector targets all existing MachineDeploymentClass InfrastructureTemplate with *",
  1938  			selector: clusterv1.PatchSelector{
  1939  				APIVersion: "infrastructure.cluster.x-k8s.io/v1beta1",
  1940  				Kind:       "InfrastructureMachineTemplate",
  1941  				MatchResources: clusterv1.PatchSelectorMatch{
  1942  					MachineDeploymentClass: &clusterv1.PatchSelectorMatchMachineDeploymentClass{
  1943  						Names: []string{"*"},
  1944  					},
  1945  				},
  1946  			},
  1947  			clusterClass: builder.ClusterClass(metav1.NamespaceDefault, "class1").
  1948  				WithWorkerMachineDeploymentClasses(
  1949  					*builder.MachineDeploymentClass("a-something-a").
  1950  						WithInfrastructureTemplate(
  1951  							refToUnstructured(&corev1.ObjectReference{
  1952  								APIVersion: "infrastructure.cluster.x-k8s.io/v1beta1",
  1953  								Kind:       "InfrastructureMachineTemplate",
  1954  							})).
  1955  						WithBootstrapTemplate(
  1956  							refToUnstructured(&corev1.ObjectReference{
  1957  								APIVersion: "bootstrap.cluster.x-k8s.io/v1beta1",
  1958  								Kind:       "BootstrapTemplate",
  1959  							})).
  1960  						Build(),
  1961  				).
  1962  				Build(),
  1963  		},
  1964  		// The following tests have selectors which match multiple resources at the same time.
  1965  		{
  1966  			name: "fail if selector targets a matching infrastructureCluster reference and a not matching control plane",
  1967  			selector: clusterv1.PatchSelector{
  1968  				APIVersion: "infrastructure.cluster.x-k8s.io/v1beta1",
  1969  				Kind:       "InfrastructureClusterTemplate",
  1970  				MatchResources: clusterv1.PatchSelectorMatch{
  1971  					InfrastructureCluster: true,
  1972  					ControlPlane:          true,
  1973  				},
  1974  			},
  1975  			clusterClass: builder.ClusterClass(metav1.NamespaceDefault, "class1").
  1976  				WithInfrastructureClusterTemplate(
  1977  					refToUnstructured(
  1978  						&corev1.ObjectReference{
  1979  							APIVersion: "infrastructure.cluster.x-k8s.io/v1beta1",
  1980  							Kind:       "InfrastructureClusterTemplate",
  1981  						}),
  1982  				).
  1983  				WithControlPlaneTemplate(
  1984  					refToUnstructured(
  1985  						&corev1.ObjectReference{
  1986  							APIVersion: "controlplane.cluster.x-k8s.io/v1beta1",
  1987  							Kind:       "NonMatchingControlPlaneTemplate",
  1988  						}),
  1989  				).
  1990  				WithControlPlaneInfrastructureMachineTemplate(
  1991  					refToUnstructured(
  1992  						&corev1.ObjectReference{
  1993  							APIVersion: "infrastructure.cluster.x-k8s.io/v1beta1",
  1994  							Kind:       "NonMatchingInfrastructureMachineTemplate",
  1995  						}),
  1996  				).
  1997  				Build(),
  1998  			wantErr: true,
  1999  		},
  2000  		{
  2001  			name: "pass if selector targets BOTH an existing ControlPlane MachineInfrastructureTemplate and an existing MachineDeploymentClass InfrastructureTemplate",
  2002  			selector: clusterv1.PatchSelector{
  2003  				APIVersion: "infrastructure.cluster.x-k8s.io/v1beta1",
  2004  				Kind:       "InfrastructureMachineTemplate",
  2005  				MatchResources: clusterv1.PatchSelectorMatch{
  2006  					MachineDeploymentClass: &clusterv1.PatchSelectorMatchMachineDeploymentClass{
  2007  						Names: []string{"bb"},
  2008  					},
  2009  					ControlPlane: true,
  2010  				},
  2011  			},
  2012  			clusterClass: builder.ClusterClass(metav1.NamespaceDefault, "class1").
  2013  				WithControlPlaneInfrastructureMachineTemplate(
  2014  					refToUnstructured(
  2015  						&corev1.ObjectReference{
  2016  							APIVersion: "infrastructure.cluster.x-k8s.io/v1beta1",
  2017  							Kind:       "InfrastructureMachineTemplate",
  2018  						}),
  2019  				).
  2020  				WithWorkerMachineDeploymentClasses(
  2021  					*builder.MachineDeploymentClass("aa").
  2022  						WithInfrastructureTemplate(
  2023  							refToUnstructured(&corev1.ObjectReference{
  2024  								APIVersion: "infrastructure.cluster.x-k8s.io/v1beta1",
  2025  								Kind:       "InfrastructureMachineTemplate",
  2026  							})).
  2027  						WithBootstrapTemplate(
  2028  							refToUnstructured(&corev1.ObjectReference{
  2029  								APIVersion: "bootstrap.cluster.x-k8s.io/v1beta1",
  2030  								Kind:       "BootstrapTemplate",
  2031  							})).
  2032  						Build(),
  2033  					*builder.MachineDeploymentClass("bb").
  2034  						WithInfrastructureTemplate(
  2035  							refToUnstructured(&corev1.ObjectReference{
  2036  								APIVersion: "infrastructure.cluster.x-k8s.io/v1beta1",
  2037  								Kind:       "InfrastructureMachineTemplate",
  2038  							})).
  2039  						WithBootstrapTemplate(
  2040  							refToUnstructured(&corev1.ObjectReference{
  2041  								APIVersion: "bootstrap.cluster.x-k8s.io/v1beta1",
  2042  								Kind:       "BootstrapTemplate",
  2043  							})).
  2044  						Build(),
  2045  				).
  2046  				Build(),
  2047  			wantErr: false,
  2048  		},
  2049  		{
  2050  			name: "fail if selector targets BOTH an existing ControlPlane MachineInfrastructureTemplate and an existing MachineDeploymentClass InfrastructureTemplate but does not match all MachineDeployment classes",
  2051  			selector: clusterv1.PatchSelector{
  2052  				APIVersion: "infrastructure.cluster.x-k8s.io/v1beta1",
  2053  				Kind:       "InfrastructureMachineTemplate",
  2054  				MatchResources: clusterv1.PatchSelectorMatch{
  2055  					MachineDeploymentClass: &clusterv1.PatchSelectorMatchMachineDeploymentClass{
  2056  						Names: []string{"aa", "bb"},
  2057  					},
  2058  					ControlPlane: true,
  2059  				},
  2060  			},
  2061  			clusterClass: builder.ClusterClass(metav1.NamespaceDefault, "class1").
  2062  				WithControlPlaneTemplate(
  2063  					refToUnstructured(
  2064  						&corev1.ObjectReference{
  2065  							APIVersion: "controlplane.cluster.x-k8s.io/v1beta1",
  2066  							Kind:       "NonMatchingControlPlaneTemplate",
  2067  						}),
  2068  				).
  2069  				WithControlPlaneInfrastructureMachineTemplate(
  2070  					refToUnstructured(
  2071  						&corev1.ObjectReference{
  2072  							APIVersion: "infrastructure.cluster.x-k8s.io/v1beta1",
  2073  							Kind:       "InfrastructureMachineTemplate",
  2074  						}),
  2075  				).
  2076  				WithWorkerMachineDeploymentClasses(
  2077  					*builder.MachineDeploymentClass("aa").
  2078  						WithInfrastructureTemplate(
  2079  							refToUnstructured(&corev1.ObjectReference{
  2080  								APIVersion: "infrastructure.cluster.x-k8s.io/v1beta1",
  2081  								Kind:       "NonMatchingInfrastructureMachineTemplate",
  2082  							})).
  2083  						WithBootstrapTemplate(
  2084  							refToUnstructured(&corev1.ObjectReference{
  2085  								APIVersion: "bootstrap.cluster.x-k8s.io/v1beta1",
  2086  								Kind:       "BootstrapTemplate",
  2087  							})).
  2088  						Build(),
  2089  					*builder.MachineDeploymentClass("bb").
  2090  						WithInfrastructureTemplate(
  2091  							refToUnstructured(&corev1.ObjectReference{
  2092  								APIVersion: "infrastructure.cluster.x-k8s.io/v1beta1",
  2093  								Kind:       "InfrastructureMachineTemplate",
  2094  							})).
  2095  						WithBootstrapTemplate(
  2096  							refToUnstructured(&corev1.ObjectReference{
  2097  								APIVersion: "bootstrap.cluster.x-k8s.io/v1beta1",
  2098  								Kind:       "BootstrapTemplate",
  2099  							})).
  2100  						Build(),
  2101  				).
  2102  				Build(),
  2103  			wantErr: true,
  2104  		},
  2105  		{
  2106  			name: "fail if selector targets BOTH an existing ControlPlane MachineInfrastructureTemplate and an existing MachineDeploymentClass InfrastructureTemplate but matches only one",
  2107  			selector: clusterv1.PatchSelector{
  2108  				APIVersion: "infrastructure.cluster.x-k8s.io/v1beta1",
  2109  				Kind:       "InfrastructureMachineTemplate",
  2110  				MatchResources: clusterv1.PatchSelectorMatch{
  2111  					MachineDeploymentClass: &clusterv1.PatchSelectorMatchMachineDeploymentClass{
  2112  						Names: []string{"bb"},
  2113  					},
  2114  					ControlPlane: true,
  2115  				},
  2116  			},
  2117  			clusterClass: builder.ClusterClass(metav1.NamespaceDefault, "class1").
  2118  				WithControlPlaneTemplate(
  2119  					refToUnstructured(
  2120  						&corev1.ObjectReference{
  2121  							APIVersion: "controlplane.cluster.x-k8s.io/v1beta1",
  2122  							Kind:       "NonMatchingControlPlaneTemplate",
  2123  						}),
  2124  				).
  2125  				WithControlPlaneInfrastructureMachineTemplate(
  2126  					refToUnstructured(
  2127  						&corev1.ObjectReference{
  2128  							APIVersion: "infrastructure.cluster.x-k8s.io/v1beta1",
  2129  							Kind:       "InfrastructureMachineTemplate",
  2130  						}),
  2131  				).
  2132  				WithWorkerMachineDeploymentClasses(
  2133  					*builder.MachineDeploymentClass("bb").
  2134  						WithInfrastructureTemplate(
  2135  							refToUnstructured(&corev1.ObjectReference{
  2136  								APIVersion: "infrastructure.cluster.x-k8s.io/v1beta1",
  2137  								Kind:       "OtherInfrastructureMachineTemplate",
  2138  							})).
  2139  						WithBootstrapTemplate(
  2140  							refToUnstructured(&corev1.ObjectReference{
  2141  								APIVersion: "bootstrap.cluster.x-k8s.io/v1beta1",
  2142  								Kind:       "BootstrapTemplate",
  2143  							})).
  2144  						Build(),
  2145  				).
  2146  				Build(),
  2147  			wantErr: true,
  2148  		},
  2149  		{
  2150  			name: "fail if selector targets everything but nothing matches",
  2151  			selector: clusterv1.PatchSelector{
  2152  				APIVersion: "infrastructure.cluster.x-k8s.io/v1beta1",
  2153  				Kind:       "NotMatchingInfrastructureMachineTemplate",
  2154  				MatchResources: clusterv1.PatchSelectorMatch{
  2155  					ControlPlane:          true,
  2156  					InfrastructureCluster: true,
  2157  					MachineDeploymentClass: &clusterv1.PatchSelectorMatchMachineDeploymentClass{
  2158  						Names: []string{"bb"},
  2159  					},
  2160  				},
  2161  			},
  2162  			clusterClass: builder.ClusterClass(metav1.NamespaceDefault, "class1").
  2163  				WithInfrastructureClusterTemplate(
  2164  					refToUnstructured(
  2165  						&corev1.ObjectReference{
  2166  							APIVersion: "infrastructure.cluster.x-k8s.io/v1beta1",
  2167  							Kind:       "InfrastructureClusterTemplate",
  2168  						}),
  2169  				).
  2170  				WithControlPlaneTemplate(
  2171  					refToUnstructured(
  2172  						&corev1.ObjectReference{
  2173  							APIVersion: "controlplane.cluster.x-k8s.io/v1beta1",
  2174  							Kind:       "NonMatchingControlPlaneTemplate",
  2175  						}),
  2176  				).
  2177  				WithControlPlaneInfrastructureMachineTemplate(
  2178  					refToUnstructured(
  2179  						&corev1.ObjectReference{
  2180  							APIVersion: "infrastructure.cluster.x-k8s.io/v1beta1",
  2181  							Kind:       "InfrastructureMachineTemplate",
  2182  						}),
  2183  				).
  2184  				WithWorkerMachineDeploymentClasses(
  2185  					*builder.MachineDeploymentClass("bb").
  2186  						WithInfrastructureTemplate(
  2187  							refToUnstructured(&corev1.ObjectReference{
  2188  								APIVersion: "infrastructure.cluster.x-k8s.io/v1beta1",
  2189  								Kind:       "OtherInfrastructureMachineTemplate",
  2190  							})).
  2191  						WithBootstrapTemplate(
  2192  							refToUnstructured(&corev1.ObjectReference{
  2193  								APIVersion: "bootstrap.cluster.x-k8s.io/v1beta1",
  2194  								Kind:       "BootstrapTemplate",
  2195  							})).
  2196  						Build(),
  2197  				).
  2198  				Build(),
  2199  			wantErr: true,
  2200  		},
  2201  	}
  2202  	for _, tt := range tests {
  2203  		t.Run(tt.name, func(t *testing.T) {
  2204  			g := NewWithT(t)
  2205  
  2206  			err := validateSelectors(tt.selector, tt.clusterClass, field.NewPath(""))
  2207  
  2208  			if tt.wantErr {
  2209  				g.Expect(err.ToAggregate()).To(HaveOccurred())
  2210  				return
  2211  			}
  2212  			g.Expect(err.ToAggregate()).ToNot(HaveOccurred())
  2213  		})
  2214  	}
  2215  }
  2216  
  2217  func TestGetVariableName(t *testing.T) {
  2218  	tests := []struct {
  2219  		name         string
  2220  		variable     string
  2221  		variableName string
  2222  	}{
  2223  		{
  2224  			name:         "simple variable",
  2225  			variable:     "variableA",
  2226  			variableName: "variableA",
  2227  		},
  2228  		{
  2229  			name:         "variable object",
  2230  			variable:     "variableObject.field",
  2231  			variableName: "variableObject",
  2232  		},
  2233  		{
  2234  			name:         "variable array",
  2235  			variable:     "variableArray[0]",
  2236  			variableName: "variableArray",
  2237  		},
  2238  	}
  2239  
  2240  	for _, tt := range tests {
  2241  		t.Run(tt.name, func(t *testing.T) {
  2242  			g := NewWithT(t)
  2243  
  2244  			g.Expect(getVariableName(tt.variable)).To(Equal(tt.variableName))
  2245  		})
  2246  	}
  2247  }