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