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