sigs.k8s.io/cluster-api@v1.7.1/internal/topology/variables/cluster_variable_validation_test.go (about)

     1  /*
     2  Copyright 2021 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package variables
    18  
    19  import (
    20  	"testing"
    21  
    22  	. "github.com/onsi/gomega"
    23  	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
    24  	"k8s.io/apimachinery/pkg/util/validation/field"
    25  	"k8s.io/utils/ptr"
    26  
    27  	clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
    28  )
    29  
    30  func Test_ValidateClusterVariables(t *testing.T) {
    31  	tests := []struct {
    32  		name             string
    33  		definitions      []clusterv1.ClusterClassStatusVariable
    34  		values           []clusterv1.ClusterVariable
    35  		validateRequired bool
    36  		wantErr          bool
    37  	}{
    38  		{
    39  			name: "Pass for a number of valid values.",
    40  			definitions: []clusterv1.ClusterClassStatusVariable{
    41  				{
    42  					Name: "cpu",
    43  					Definitions: []clusterv1.ClusterClassStatusVariableDefinition{
    44  						{
    45  							Required: true,
    46  							From:     clusterv1.VariableDefinitionFromInline,
    47  							Schema: clusterv1.VariableSchema{
    48  								OpenAPIV3Schema: clusterv1.JSONSchemaProps{
    49  									Type:    "integer",
    50  									Minimum: ptr.To[int64](1),
    51  								},
    52  							},
    53  						},
    54  					},
    55  				},
    56  				{
    57  					Name: "zone",
    58  					Definitions: []clusterv1.ClusterClassStatusVariableDefinition{
    59  						{
    60  							Required: true,
    61  							From:     clusterv1.VariableDefinitionFromInline,
    62  							Schema: clusterv1.VariableSchema{
    63  								OpenAPIV3Schema: clusterv1.JSONSchemaProps{
    64  									Type:      "string",
    65  									MinLength: ptr.To[int64](1),
    66  								},
    67  							},
    68  						},
    69  					},
    70  				},
    71  				{
    72  					Name: "location",
    73  					Definitions: []clusterv1.ClusterClassStatusVariableDefinition{
    74  						{
    75  							Required: true,
    76  							From:     clusterv1.VariableDefinitionFromInline,
    77  							Schema: clusterv1.VariableSchema{
    78  								OpenAPIV3Schema: clusterv1.JSONSchemaProps{
    79  									Type: "string",
    80  									Enum: []apiextensionsv1.JSON{
    81  										{Raw: []byte(`"us-east-1"`)},
    82  										{Raw: []byte(`"us-east-2"`)},
    83  									},
    84  								},
    85  							},
    86  						},
    87  					},
    88  				},
    89  			},
    90  			values: []clusterv1.ClusterVariable{
    91  				{
    92  					Name: "cpu",
    93  					Value: apiextensionsv1.JSON{
    94  						Raw: []byte(`1`),
    95  					},
    96  				},
    97  				{
    98  					Name: "zone",
    99  					Value: apiextensionsv1.JSON{
   100  						Raw: []byte(`"longerThanOneCharacter"`),
   101  					},
   102  				},
   103  				{
   104  					Name: "location",
   105  					Value: apiextensionsv1.JSON{
   106  						Raw: []byte(`"us-east-1"`),
   107  					},
   108  				},
   109  			},
   110  			validateRequired: true,
   111  		},
   112  		{
   113  			name:    "Error when no value for required definition.",
   114  			wantErr: true,
   115  			definitions: []clusterv1.ClusterClassStatusVariable{
   116  				{
   117  					Name: "cpu",
   118  					Definitions: []clusterv1.ClusterClassStatusVariableDefinition{
   119  						{
   120  							Required: true,
   121  							From:     clusterv1.VariableDefinitionFromInline,
   122  							Schema: clusterv1.VariableSchema{
   123  								OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   124  									Type:    "integer",
   125  									Minimum: ptr.To[int64](1),
   126  								},
   127  							},
   128  						},
   129  					},
   130  				},
   131  				{
   132  					Name: "zone",
   133  					Definitions: []clusterv1.ClusterClassStatusVariableDefinition{
   134  						{
   135  							Required: true,
   136  							From:     clusterv1.VariableDefinitionFromInline,
   137  							Schema: clusterv1.VariableSchema{
   138  								OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   139  									Type:      "string",
   140  									MinLength: ptr.To[int64](1),
   141  								},
   142  							},
   143  						},
   144  					},
   145  				},
   146  			},
   147  			values: []clusterv1.ClusterVariable{
   148  				// cpu is missing in the values but is required in definition.
   149  				{
   150  					Name: "zone",
   151  					Value: apiextensionsv1.JSON{
   152  						Raw: []byte(`"longerThanOneCharacter"`),
   153  					},
   154  				},
   155  			},
   156  			validateRequired: true,
   157  		},
   158  		{
   159  			name: "Pass if validateRequired='false' and no value for required definition.",
   160  			definitions: []clusterv1.ClusterClassStatusVariable{
   161  				{
   162  					Name: "cpu",
   163  					Definitions: []clusterv1.ClusterClassStatusVariableDefinition{
   164  						{
   165  							Required: true,
   166  							From:     clusterv1.VariableDefinitionFromInline,
   167  							Schema: clusterv1.VariableSchema{
   168  								OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   169  									Type:    "integer",
   170  									Minimum: ptr.To[int64](1),
   171  								},
   172  							},
   173  						},
   174  					},
   175  				},
   176  				{
   177  					Name: "zone",
   178  					Definitions: []clusterv1.ClusterClassStatusVariableDefinition{
   179  						{
   180  							Required: true,
   181  							From:     clusterv1.VariableDefinitionFromInline,
   182  							Schema: clusterv1.VariableSchema{
   183  								OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   184  									Type:      "string",
   185  									MinLength: ptr.To[int64](1),
   186  								},
   187  							},
   188  						},
   189  					},
   190  				},
   191  			},
   192  
   193  			values: []clusterv1.ClusterVariable{
   194  				// cpu is missing in the values and is required in definitions,
   195  				// but required validation is disabled.
   196  				{
   197  					Name: "zone",
   198  					Value: apiextensionsv1.JSON{
   199  						Raw: []byte(`"longerThanOneCharacter"`),
   200  					},
   201  				},
   202  			},
   203  			validateRequired: false,
   204  		},
   205  		{
   206  			name:        "Error if value has no definition.",
   207  			wantErr:     true,
   208  			definitions: []clusterv1.ClusterClassStatusVariable{},
   209  			values: []clusterv1.ClusterVariable{
   210  				// location has a value but no definition.
   211  				{
   212  					Name: "location",
   213  					Value: apiextensionsv1.JSON{
   214  						Raw: []byte(`"us-east-1"`),
   215  					},
   216  				},
   217  			},
   218  			validateRequired: true,
   219  		},
   220  		// Non-conflicting definition tests.
   221  		{
   222  			name: "Pass if a value with empty definitionFrom set for a non-conflicting definition",
   223  			definitions: []clusterv1.ClusterClassStatusVariable{
   224  				{
   225  					Name: "cpu",
   226  					Definitions: []clusterv1.ClusterClassStatusVariableDefinition{
   227  						{
   228  							Schema: clusterv1.VariableSchema{
   229  								OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   230  									Type: "integer",
   231  								},
   232  							},
   233  							From:     clusterv1.VariableDefinitionFromInline,
   234  							Required: true,
   235  						},
   236  						{
   237  							Schema: clusterv1.VariableSchema{
   238  								OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   239  									Type: "integer",
   240  								},
   241  							},
   242  							From:     "somepatch",
   243  							Required: true,
   244  						},
   245  					},
   246  				},
   247  			},
   248  			values: []clusterv1.ClusterVariable{
   249  				{
   250  					Name: "cpu",
   251  					Value: apiextensionsv1.JSON{
   252  						Raw: []byte(`1`),
   253  					},
   254  				},
   255  			},
   256  			validateRequired: true,
   257  		},
   258  		{
   259  			name: "Pass when there is a separate value for each required definition with no definition conflicts.",
   260  			definitions: []clusterv1.ClusterClassStatusVariable{
   261  				{
   262  					Name: "cpu",
   263  					Definitions: []clusterv1.ClusterClassStatusVariableDefinition{
   264  						{
   265  							Schema: clusterv1.VariableSchema{
   266  								OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   267  									Type: "integer",
   268  								},
   269  							},
   270  							From:     clusterv1.VariableDefinitionFromInline,
   271  							Required: true,
   272  						},
   273  						{
   274  							Schema: clusterv1.VariableSchema{
   275  								OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   276  									Type: "integer",
   277  								},
   278  							},
   279  							From:     "somepatch",
   280  							Required: true,
   281  						},
   282  					},
   283  				},
   284  			},
   285  			values: []clusterv1.ClusterVariable{
   286  				// Each value is set individually.
   287  				{
   288  					Name: "cpu",
   289  					Value: apiextensionsv1.JSON{
   290  						Raw: []byte(`1`),
   291  					},
   292  					DefinitionFrom: "somepatch",
   293  				},
   294  				{
   295  					Name: "cpu",
   296  					Value: apiextensionsv1.JSON{
   297  						Raw: []byte(`2`),
   298  					},
   299  					DefinitionFrom: "inline",
   300  				},
   301  			},
   302  			validateRequired: true,
   303  		},
   304  		{
   305  			name:    "Fail if value DefinitionFrom field does not match any definition.",
   306  			wantErr: true,
   307  			definitions: []clusterv1.ClusterClassStatusVariable{
   308  				{
   309  					Name: "cpu",
   310  					Definitions: []clusterv1.ClusterClassStatusVariableDefinition{
   311  						{
   312  							From: clusterv1.VariableDefinitionFromInline,
   313  							Schema: clusterv1.VariableSchema{
   314  								OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   315  									Type: "string",
   316  								},
   317  							},
   318  						},
   319  					},
   320  				},
   321  			},
   322  			values: []clusterv1.ClusterVariable{
   323  				{
   324  					Name: "cpu",
   325  					Value: apiextensionsv1.JSON{
   326  						Raw: []byte(`1`),
   327  					},
   328  					// This definition does not exist.
   329  					DefinitionFrom: "non-existent-patch",
   330  				},
   331  			},
   332  			validateRequired: true,
   333  		},
   334  		{
   335  			name:    "Fail if a value is set twice with the same definitionFrom.",
   336  			wantErr: true,
   337  			definitions: []clusterv1.ClusterClassStatusVariable{
   338  				{
   339  					Name: "cpu",
   340  					Definitions: []clusterv1.ClusterClassStatusVariableDefinition{
   341  						{
   342  							Schema: clusterv1.VariableSchema{
   343  								OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   344  									Type: "integer",
   345  								},
   346  							},
   347  							From: "somepatch",
   348  						},
   349  					},
   350  				},
   351  			},
   352  			values: []clusterv1.ClusterVariable{
   353  				{
   354  					Name: "cpu",
   355  					Value: apiextensionsv1.JSON{
   356  						Raw: []byte(`1`),
   357  					},
   358  					DefinitionFrom: "somepatch",
   359  				},
   360  				{
   361  					Name: "cpu",
   362  					Value: apiextensionsv1.JSON{
   363  						Raw: []byte(`2`),
   364  					},
   365  					DefinitionFrom: "somepatch",
   366  				},
   367  			},
   368  			validateRequired: true,
   369  		},
   370  		{
   371  			name:    "Fail if a value is set with empty and non-empty definitionFrom.",
   372  			wantErr: true,
   373  			definitions: []clusterv1.ClusterClassStatusVariable{
   374  				{
   375  					Name: "cpu",
   376  					Definitions: []clusterv1.ClusterClassStatusVariableDefinition{
   377  						{
   378  							Schema: clusterv1.VariableSchema{
   379  								OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   380  									Type: "integer",
   381  								},
   382  							},
   383  							From: "somepatch",
   384  						},
   385  						{
   386  							Schema: clusterv1.VariableSchema{
   387  								OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   388  									Type: "integer",
   389  								},
   390  							},
   391  							From: clusterv1.VariableDefinitionFromInline,
   392  						},
   393  					},
   394  				},
   395  			},
   396  			values: []clusterv1.ClusterVariable{
   397  				{
   398  					Name: "cpu",
   399  					Value: apiextensionsv1.JSON{
   400  						Raw: []byte(`1`),
   401  					},
   402  					// Mix of empty and non-empty definitionFrom is not valid.
   403  					DefinitionFrom: "",
   404  				},
   405  				{
   406  					Name: "cpu",
   407  					Value: apiextensionsv1.JSON{
   408  						Raw: []byte(`2`),
   409  					},
   410  					DefinitionFrom: "somepatch",
   411  				},
   412  			},
   413  			validateRequired: true,
   414  		},
   415  		{
   416  			name:    "Fail when values invalid by their definition schema.",
   417  			wantErr: true,
   418  			definitions: []clusterv1.ClusterClassStatusVariable{
   419  				{
   420  					Name:                "cpu",
   421  					DefinitionsConflict: true,
   422  					Definitions: []clusterv1.ClusterClassStatusVariableDefinition{
   423  						{
   424  							From: clusterv1.VariableDefinitionFromInline,
   425  							Schema: clusterv1.VariableSchema{
   426  								OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   427  									Type: "string",
   428  								},
   429  							},
   430  						},
   431  						{
   432  							From: "somepatch",
   433  							Schema: clusterv1.VariableSchema{
   434  								OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   435  									Type: "integer",
   436  								},
   437  							},
   438  						},
   439  					},
   440  				},
   441  			},
   442  			values: []clusterv1.ClusterVariable{
   443  				{
   444  					Name: "cpu",
   445  					Value: apiextensionsv1.JSON{
   446  						Raw: []byte(`1`),
   447  					},
   448  					DefinitionFrom: "inline",
   449  				},
   450  				{
   451  					Name: "cpu",
   452  					Value: apiextensionsv1.JSON{
   453  						Raw: []byte(`"one"`),
   454  					},
   455  					DefinitionFrom: "somepatch",
   456  				},
   457  			},
   458  			validateRequired: true,
   459  		},
   460  		// Conflicting definition tests.
   461  		{
   462  			name: "Pass with a value provided for each conflicting definition.",
   463  			definitions: []clusterv1.ClusterClassStatusVariable{
   464  				{
   465  					Name:                "cpu",
   466  					DefinitionsConflict: true,
   467  					Definitions: []clusterv1.ClusterClassStatusVariableDefinition{
   468  						{
   469  							From: clusterv1.VariableDefinitionFromInline,
   470  							Schema: clusterv1.VariableSchema{
   471  								OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   472  									Type: "string",
   473  								},
   474  							},
   475  						},
   476  						{
   477  							From: "somepatch",
   478  							Schema: clusterv1.VariableSchema{
   479  								OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   480  									Type: "integer",
   481  								},
   482  							},
   483  						},
   484  					},
   485  				},
   486  			},
   487  			values: []clusterv1.ClusterVariable{
   488  				{
   489  					Name: "cpu",
   490  					Value: apiextensionsv1.JSON{
   491  						Raw: []byte(`"one"`),
   492  					},
   493  					DefinitionFrom: "inline",
   494  				},
   495  				{
   496  					Name: "cpu",
   497  					Value: apiextensionsv1.JSON{
   498  						Raw: []byte(`1`),
   499  					},
   500  					DefinitionFrom: "somepatch",
   501  				},
   502  			},
   503  			validateRequired: true,
   504  		},
   505  		{
   506  			name: "Pass if non-required definition value doesn't include definitionFrom for each required definition when definitions conflict.",
   507  			definitions: []clusterv1.ClusterClassStatusVariable{
   508  				{
   509  					Name:                "cpu",
   510  					DefinitionsConflict: true,
   511  					// There are conflicting definitions which means values should include a `definitionFrom` field.
   512  					Definitions: []clusterv1.ClusterClassStatusVariableDefinition{
   513  						{
   514  							Schema: clusterv1.VariableSchema{
   515  								OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   516  									Type: "integer",
   517  								},
   518  							},
   519  							From:     "somepatch",
   520  							Required: true,
   521  						},
   522  						// This variable is not required so it does not need a value.
   523  						{
   524  							Schema: clusterv1.VariableSchema{
   525  								OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   526  									Type: "integer",
   527  								},
   528  							},
   529  							From:     "anotherpatch",
   530  							Required: false,
   531  						},
   532  					},
   533  				},
   534  			},
   535  			values: []clusterv1.ClusterVariable{
   536  				{
   537  					Name: "cpu",
   538  					Value: apiextensionsv1.JSON{
   539  						Raw: []byte(`1`),
   540  					},
   541  					DefinitionFrom: "somepatch",
   542  				},
   543  			},
   544  			validateRequired: true,
   545  		},
   546  		{
   547  			name:    "Fail if value doesn't include definitionFrom when definitions conflict.",
   548  			wantErr: true,
   549  			definitions: []clusterv1.ClusterClassStatusVariable{
   550  				{
   551  					Name: "cpu",
   552  					// There are conflicting definitions which means values should include a `definitionFrom` field.
   553  					DefinitionsConflict: true,
   554  					Definitions: []clusterv1.ClusterClassStatusVariableDefinition{
   555  						{
   556  							From: clusterv1.VariableDefinitionFromInline,
   557  							Schema: clusterv1.VariableSchema{
   558  								OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   559  									Type: "string",
   560  								},
   561  							},
   562  						},
   563  						{
   564  							From: "somepatch",
   565  							Schema: clusterv1.VariableSchema{
   566  								OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   567  									Type: "integer",
   568  								},
   569  							},
   570  						},
   571  					},
   572  				},
   573  			},
   574  			values: []clusterv1.ClusterVariable{
   575  				{
   576  					Name: "cpu",
   577  					Value: apiextensionsv1.JSON{
   578  						Raw: []byte(`1`),
   579  					},
   580  					// No definitionFrom
   581  				},
   582  			},
   583  			validateRequired: true,
   584  		},
   585  		{
   586  			name:    "Fail if value doesn't include definitionFrom for each required definition when definitions conflict.",
   587  			wantErr: true,
   588  			definitions: []clusterv1.ClusterClassStatusVariable{
   589  				{
   590  					Name:                "cpu",
   591  					DefinitionsConflict: true,
   592  					// There are conflicting definitions which means values should include a `definitionFrom` field.
   593  					Definitions: []clusterv1.ClusterClassStatusVariableDefinition{
   594  						{
   595  
   596  							Schema: clusterv1.VariableSchema{
   597  								OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   598  									Type: "string",
   599  								},
   600  							},
   601  							From:     clusterv1.VariableDefinitionFromInline,
   602  							Required: true,
   603  						},
   604  						{
   605  							Schema: clusterv1.VariableSchema{
   606  								OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   607  									Type: "integer",
   608  								},
   609  							},
   610  							From:     "somepatch",
   611  							Required: true,
   612  						},
   613  					},
   614  				},
   615  			},
   616  			values: []clusterv1.ClusterVariable{
   617  				{
   618  					Name: "cpu",
   619  					Value: apiextensionsv1.JSON{
   620  						Raw: []byte(`1`),
   621  					},
   622  					DefinitionFrom: "somepatch",
   623  				},
   624  			},
   625  			validateRequired: true,
   626  		},
   627  	}
   628  	for _, tt := range tests {
   629  		t.Run(tt.name, func(t *testing.T) {
   630  			g := NewWithT(t)
   631  
   632  			errList := validateClusterVariables(tt.values, tt.definitions,
   633  				tt.validateRequired, field.NewPath("spec", "topology", "variables"))
   634  
   635  			if tt.wantErr {
   636  				g.Expect(errList).NotTo(BeEmpty())
   637  				return
   638  			}
   639  			g.Expect(errList).To(BeEmpty())
   640  		})
   641  	}
   642  }
   643  
   644  func Test_ValidateClusterVariable(t *testing.T) {
   645  	tests := []struct {
   646  		name                 string
   647  		clusterClassVariable *clusterv1.ClusterClassVariable
   648  		clusterVariable      *clusterv1.ClusterVariable
   649  		wantErr              bool
   650  	}{
   651  		{
   652  			name: "Valid integer",
   653  			clusterClassVariable: &clusterv1.ClusterClassVariable{
   654  				Name:     "cpu",
   655  				Required: true,
   656  				Schema: clusterv1.VariableSchema{
   657  					OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   658  						Type:    "integer",
   659  						Minimum: ptr.To[int64](1),
   660  					},
   661  				},
   662  			},
   663  			clusterVariable: &clusterv1.ClusterVariable{
   664  				Name: "cpu",
   665  				Value: apiextensionsv1.JSON{
   666  					Raw: []byte(`1`),
   667  				},
   668  			},
   669  		},
   670  		{
   671  			name:    "Error if integer is above Maximum",
   672  			wantErr: true,
   673  			clusterClassVariable: &clusterv1.ClusterClassVariable{
   674  				Name:     "cpu",
   675  				Required: true,
   676  				Schema: clusterv1.VariableSchema{
   677  					OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   678  						Type:    "integer",
   679  						Maximum: ptr.To[int64](10),
   680  					},
   681  				},
   682  			},
   683  			clusterVariable: &clusterv1.ClusterVariable{
   684  				Name: "cpu",
   685  				Value: apiextensionsv1.JSON{
   686  					Raw: []byte(`99`),
   687  				},
   688  			},
   689  		},
   690  		{
   691  			name:    "Error if integer is below Minimum",
   692  			wantErr: true,
   693  			clusterClassVariable: &clusterv1.ClusterClassVariable{
   694  				Name:     "cpu",
   695  				Required: true,
   696  				Schema: clusterv1.VariableSchema{
   697  					OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   698  						Type:    "integer",
   699  						Minimum: ptr.To[int64](1),
   700  					},
   701  				},
   702  			},
   703  			clusterVariable: &clusterv1.ClusterVariable{
   704  				Name: "cpu",
   705  				Value: apiextensionsv1.JSON{
   706  					Raw: []byte(`0`),
   707  				},
   708  			},
   709  		},
   710  
   711  		{
   712  			name:    "Fails, expected integer got string",
   713  			wantErr: true,
   714  			clusterClassVariable: &clusterv1.ClusterClassVariable{
   715  				Name:     "cpu",
   716  				Required: true,
   717  				Schema: clusterv1.VariableSchema{
   718  					OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   719  						Type:    "integer",
   720  						Minimum: ptr.To[int64](1),
   721  					},
   722  				},
   723  			},
   724  			clusterVariable: &clusterv1.ClusterVariable{
   725  				Name: "cpu",
   726  				Value: apiextensionsv1.JSON{
   727  					Raw: []byte(`"1"`),
   728  				},
   729  			},
   730  		},
   731  		{
   732  			name: "Valid string",
   733  			clusterClassVariable: &clusterv1.ClusterClassVariable{
   734  				Name:     "location",
   735  				Required: true,
   736  				Schema: clusterv1.VariableSchema{
   737  					OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   738  						Type:      "string",
   739  						MinLength: ptr.To[int64](1),
   740  					},
   741  				},
   742  			},
   743  			clusterVariable: &clusterv1.ClusterVariable{
   744  				Name: "location",
   745  				Value: apiextensionsv1.JSON{
   746  					Raw: []byte(`"us-east"`),
   747  				},
   748  			},
   749  		},
   750  		{
   751  			name:    "Error if string doesn't match pattern ",
   752  			wantErr: true,
   753  			clusterClassVariable: &clusterv1.ClusterClassVariable{
   754  				Name:     "location",
   755  				Required: true,
   756  				Schema: clusterv1.VariableSchema{
   757  					OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   758  						Type:    "string",
   759  						Pattern: "^[0-9]+$",
   760  					},
   761  				},
   762  			},
   763  			clusterVariable: &clusterv1.ClusterVariable{
   764  				Name: "location",
   765  				Value: apiextensionsv1.JSON{
   766  					Raw: []byte(`"000000a"`),
   767  				},
   768  			},
   769  		},
   770  		{
   771  			name:    "Error if string doesn't match format ",
   772  			wantErr: true,
   773  			clusterClassVariable: &clusterv1.ClusterClassVariable{
   774  				Name:     "location",
   775  				Required: true,
   776  				Schema: clusterv1.VariableSchema{
   777  					OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   778  						Type:   "string",
   779  						Format: "uri",
   780  					},
   781  				},
   782  			},
   783  			clusterVariable: &clusterv1.ClusterVariable{
   784  				Name: "location",
   785  				Value: apiextensionsv1.JSON{
   786  					Raw: []byte(`"not a URI"`),
   787  				},
   788  			},
   789  		},
   790  		{
   791  			name: "Valid enum string",
   792  			clusterClassVariable: &clusterv1.ClusterClassVariable{
   793  				Name:     "location",
   794  				Required: true,
   795  				Schema: clusterv1.VariableSchema{
   796  					OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   797  						Type: "string",
   798  						Enum: []apiextensionsv1.JSON{
   799  							{Raw: []byte(`"us-east-1"`)},
   800  							{Raw: []byte(`"us-east-2"`)},
   801  						},
   802  					},
   803  				},
   804  			},
   805  			clusterVariable: &clusterv1.ClusterVariable{
   806  				Name: "location",
   807  				Value: apiextensionsv1.JSON{
   808  					Raw: []byte(`"us-east-1"`),
   809  				},
   810  			},
   811  		},
   812  		{
   813  			name:    "Fails, value does not match one of the enum string values",
   814  			wantErr: true,
   815  			clusterClassVariable: &clusterv1.ClusterClassVariable{
   816  				Name:     "location",
   817  				Required: true,
   818  				Schema: clusterv1.VariableSchema{
   819  					OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   820  						Type: "string",
   821  						Enum: []apiextensionsv1.JSON{
   822  							{Raw: []byte(`"us-east-1"`)},
   823  							{Raw: []byte(`"us-east-2"`)},
   824  						},
   825  					},
   826  				},
   827  			},
   828  			clusterVariable: &clusterv1.ClusterVariable{
   829  				Name: "location",
   830  				Value: apiextensionsv1.JSON{
   831  					Raw: []byte(`"us-east-invalid"`),
   832  				},
   833  			},
   834  		},
   835  		{
   836  			name: "Valid enum integer",
   837  			clusterClassVariable: &clusterv1.ClusterClassVariable{
   838  				Name:     "location",
   839  				Required: true,
   840  				Schema: clusterv1.VariableSchema{
   841  					OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   842  						Type: "integer",
   843  						Enum: []apiextensionsv1.JSON{
   844  							{Raw: []byte(`1`)},
   845  							{Raw: []byte(`2`)},
   846  						},
   847  					},
   848  				},
   849  			},
   850  			clusterVariable: &clusterv1.ClusterVariable{
   851  				Name: "location",
   852  				Value: apiextensionsv1.JSON{
   853  					Raw: []byte(`1`),
   854  				},
   855  			},
   856  		},
   857  		{
   858  			name:    "Fails, value does not match one of the enum integer values",
   859  			wantErr: true,
   860  			clusterClassVariable: &clusterv1.ClusterClassVariable{
   861  				Name:     "location",
   862  				Required: true,
   863  				Schema: clusterv1.VariableSchema{
   864  					OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   865  						Type: "string",
   866  						Enum: []apiextensionsv1.JSON{
   867  							{Raw: []byte(`1`)},
   868  							{Raw: []byte(`2`)},
   869  						},
   870  					},
   871  				},
   872  			},
   873  			clusterVariable: &clusterv1.ClusterVariable{
   874  				Name: "location",
   875  				Value: apiextensionsv1.JSON{
   876  					Raw: []byte(`3`),
   877  				},
   878  			},
   879  		},
   880  		{
   881  			name: "Valid object",
   882  			clusterClassVariable: &clusterv1.ClusterClassVariable{
   883  				Name:     "httpProxy",
   884  				Required: true,
   885  				Schema: clusterv1.VariableSchema{
   886  					OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   887  						Type: "object",
   888  						Properties: map[string]clusterv1.JSONSchemaProps{
   889  							"enabled": {
   890  								Type: "boolean",
   891  							},
   892  						},
   893  					},
   894  				},
   895  			},
   896  			clusterVariable: &clusterv1.ClusterVariable{
   897  				Name: "httpProxy",
   898  				Value: apiextensionsv1.JSON{
   899  					Raw: []byte(`{"enabled":false}`),
   900  				},
   901  			},
   902  		},
   903  		{
   904  			name:    "Error if nested field is invalid",
   905  			wantErr: true,
   906  			clusterClassVariable: &clusterv1.ClusterClassVariable{
   907  				Name:     "httpProxy",
   908  				Required: true,
   909  				Schema: clusterv1.VariableSchema{
   910  					OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   911  						Type: "object",
   912  						Properties: map[string]clusterv1.JSONSchemaProps{
   913  							"enabled": {
   914  								Type: "boolean",
   915  							},
   916  						},
   917  					},
   918  				},
   919  			},
   920  			clusterVariable: &clusterv1.ClusterVariable{
   921  				Name: "httpProxy",
   922  				Value: apiextensionsv1.JSON{
   923  					Raw: []byte(`{"enabled":"not-a-bool"}`),
   924  				},
   925  			},
   926  		},
   927  		{
   928  			name:    "Error if object is a bool instead",
   929  			wantErr: true,
   930  			clusterClassVariable: &clusterv1.ClusterClassVariable{
   931  				Name:     "httpProxy",
   932  				Required: true,
   933  				Schema: clusterv1.VariableSchema{
   934  					OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   935  						Type: "object",
   936  						Properties: map[string]clusterv1.JSONSchemaProps{
   937  							"enabled": {
   938  								Type: "boolean",
   939  							},
   940  						},
   941  					},
   942  				},
   943  			},
   944  			clusterVariable: &clusterv1.ClusterVariable{
   945  				Name: "httpProxy",
   946  				Value: apiextensionsv1.JSON{
   947  					Raw: []byte(`"not-a-object"`),
   948  				},
   949  			},
   950  		},
   951  		{
   952  			name:    "Error if object is missing required field",
   953  			wantErr: true,
   954  			clusterClassVariable: &clusterv1.ClusterClassVariable{
   955  				Name:     "httpProxy",
   956  				Required: true,
   957  				Schema: clusterv1.VariableSchema{
   958  					OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   959  						Type: "object",
   960  						Properties: map[string]clusterv1.JSONSchemaProps{
   961  							"url": {
   962  								Type: "string",
   963  							},
   964  							"enabled": {
   965  								Type: "boolean",
   966  							},
   967  						},
   968  						Required: []string{
   969  							"url",
   970  						},
   971  					},
   972  				},
   973  			},
   974  			clusterVariable: &clusterv1.ClusterVariable{
   975  				Name: "httpProxy",
   976  				Value: apiextensionsv1.JSON{
   977  					Raw: []byte(`{"enabled":"true"}`),
   978  				},
   979  			},
   980  		},
   981  		{
   982  			name: "Valid object",
   983  			clusterClassVariable: &clusterv1.ClusterClassVariable{
   984  				Name:     "testObject",
   985  				Required: true,
   986  				Schema: clusterv1.VariableSchema{
   987  					OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   988  						Type: "object",
   989  						Properties: map[string]clusterv1.JSONSchemaProps{
   990  							"requiredProperty": {
   991  								Type: "boolean",
   992  							},
   993  							"boolProperty": {
   994  								Type: "boolean",
   995  							},
   996  							"integerProperty": {
   997  								Type:    "integer",
   998  								Minimum: ptr.To[int64](1),
   999  							},
  1000  							"enumProperty": {
  1001  								Type: "string",
  1002  								Enum: []apiextensionsv1.JSON{
  1003  									{Raw: []byte(`"enumValue1"`)},
  1004  									{Raw: []byte(`"enumValue2"`)},
  1005  								},
  1006  							},
  1007  						},
  1008  						Required: []string{"requiredProperty"},
  1009  					},
  1010  				},
  1011  			},
  1012  			clusterVariable: &clusterv1.ClusterVariable{
  1013  				Name: "testObject",
  1014  				Value: apiextensionsv1.JSON{
  1015  					Raw: []byte(`{"requiredProperty":false,"boolProperty":true,"integerProperty":1,"enumProperty":"enumValue2"}`),
  1016  				},
  1017  			},
  1018  		},
  1019  		{
  1020  			name: "Valid enum object",
  1021  			clusterClassVariable: &clusterv1.ClusterClassVariable{
  1022  				Name:     "enumObject",
  1023  				Required: true,
  1024  				Schema: clusterv1.VariableSchema{
  1025  					OpenAPIV3Schema: clusterv1.JSONSchemaProps{
  1026  						Type: "object",
  1027  						Properties: map[string]clusterv1.JSONSchemaProps{
  1028  							"location": {
  1029  								Type: "string",
  1030  							},
  1031  							"url": {
  1032  								Type: "string",
  1033  							},
  1034  						},
  1035  						Enum: []apiextensionsv1.JSON{
  1036  							{Raw: []byte(`{"location": "us-east-1","url":"us-east-1-url"}`)},
  1037  							{Raw: []byte(`{"location": "us-east-2","url":"us-east-2-url"}`)},
  1038  						},
  1039  					},
  1040  				},
  1041  			},
  1042  			clusterVariable: &clusterv1.ClusterVariable{
  1043  				Name: "enumObject",
  1044  				Value: apiextensionsv1.JSON{
  1045  					Raw: []byte(`{"location": "us-east-2","url":"us-east-2-url"}`),
  1046  				},
  1047  			},
  1048  		},
  1049  		{
  1050  			name:    "Fails, value does not match one of the enum object values",
  1051  			wantErr: true,
  1052  			clusterClassVariable: &clusterv1.ClusterClassVariable{
  1053  				Name:     "enumObject",
  1054  				Required: true,
  1055  				Schema: clusterv1.VariableSchema{
  1056  					OpenAPIV3Schema: clusterv1.JSONSchemaProps{
  1057  						Type: "object",
  1058  						Properties: map[string]clusterv1.JSONSchemaProps{
  1059  							"location": {
  1060  								Type: "string",
  1061  							},
  1062  							"url": {
  1063  								Type: "string",
  1064  							},
  1065  						},
  1066  						Enum: []apiextensionsv1.JSON{
  1067  							{Raw: []byte(`{"location": "us-east-1","url":"us-east-1-url"}`)},
  1068  							{Raw: []byte(`{"location": "us-east-2","url":"us-east-2-url"}`)},
  1069  						},
  1070  					},
  1071  				},
  1072  			},
  1073  			clusterVariable: &clusterv1.ClusterVariable{
  1074  				Name: "enumObject",
  1075  				Value: apiextensionsv1.JSON{
  1076  					Raw: []byte(`{"location": "us-east-2","url":"wrong-url"}`),
  1077  				},
  1078  			},
  1079  		},
  1080  		{
  1081  			name: "Valid map",
  1082  			clusterClassVariable: &clusterv1.ClusterClassVariable{
  1083  				Name:     "httpProxy",
  1084  				Required: true,
  1085  				Schema: clusterv1.VariableSchema{
  1086  					OpenAPIV3Schema: clusterv1.JSONSchemaProps{
  1087  						Type: "object",
  1088  						AdditionalProperties: &clusterv1.JSONSchemaProps{
  1089  							Type: "object",
  1090  							Properties: map[string]clusterv1.JSONSchemaProps{
  1091  								"enabled": {
  1092  									Type: "boolean",
  1093  								},
  1094  							},
  1095  						},
  1096  					},
  1097  				},
  1098  			},
  1099  			clusterVariable: &clusterv1.ClusterVariable{
  1100  				Name: "httpProxy",
  1101  				Value: apiextensionsv1.JSON{
  1102  					Raw: []byte(`{"proxy":{"enabled":false}}`),
  1103  				},
  1104  			},
  1105  		},
  1106  		{
  1107  			name:    "Error if map is missing a required field",
  1108  			wantErr: true,
  1109  			clusterClassVariable: &clusterv1.ClusterClassVariable{
  1110  				Name:     "httpProxy",
  1111  				Required: true,
  1112  				Schema: clusterv1.VariableSchema{
  1113  					OpenAPIV3Schema: clusterv1.JSONSchemaProps{
  1114  						Type: "object",
  1115  						AdditionalProperties: &clusterv1.JSONSchemaProps{
  1116  							Type: "object",
  1117  							Properties: map[string]clusterv1.JSONSchemaProps{
  1118  								"enabled": {
  1119  									Type: "boolean",
  1120  								},
  1121  								"url": {
  1122  									Type: "string",
  1123  								},
  1124  							},
  1125  							Required: []string{"url"},
  1126  						},
  1127  					},
  1128  				},
  1129  			},
  1130  			clusterVariable: &clusterv1.ClusterVariable{
  1131  				Name: "httpProxy",
  1132  				Value: apiextensionsv1.JSON{
  1133  					Raw: []byte(`{"proxy":{"enabled":false}}`),
  1134  				},
  1135  			},
  1136  		},
  1137  		{
  1138  			name: "Valid array",
  1139  			clusterClassVariable: &clusterv1.ClusterClassVariable{
  1140  				Name:     "testArray",
  1141  				Required: true,
  1142  				Schema: clusterv1.VariableSchema{
  1143  					OpenAPIV3Schema: clusterv1.JSONSchemaProps{
  1144  						Type: "array",
  1145  						Items: &clusterv1.JSONSchemaProps{
  1146  							Type: "string",
  1147  							Enum: []apiextensionsv1.JSON{
  1148  								{Raw: []byte(`"enumValue1"`)},
  1149  								{Raw: []byte(`"enumValue2"`)},
  1150  							},
  1151  						},
  1152  					},
  1153  				},
  1154  			},
  1155  			clusterVariable: &clusterv1.ClusterVariable{
  1156  				Name: "testArray",
  1157  				Value: apiextensionsv1.JSON{
  1158  					Raw: []byte(`["enumValue1","enumValue2"]`),
  1159  				},
  1160  			},
  1161  		},
  1162  		{
  1163  			name:    "Error if array element is invalid",
  1164  			wantErr: true,
  1165  			clusterClassVariable: &clusterv1.ClusterClassVariable{
  1166  				Name:     "testArray",
  1167  				Required: true,
  1168  				Schema: clusterv1.VariableSchema{
  1169  					OpenAPIV3Schema: clusterv1.JSONSchemaProps{
  1170  						Type: "array",
  1171  						Items: &clusterv1.JSONSchemaProps{
  1172  							Type: "string",
  1173  							Enum: []apiextensionsv1.JSON{
  1174  								{Raw: []byte(`"enumValue1"`)},
  1175  								{Raw: []byte(`"enumValue2"`)},
  1176  							},
  1177  						},
  1178  					},
  1179  				},
  1180  			},
  1181  			clusterVariable: &clusterv1.ClusterVariable{
  1182  				Name: "testArray",
  1183  				Value: apiextensionsv1.JSON{
  1184  					Raw: []byte(`["enumValue1","enumValueInvalid"]`),
  1185  				},
  1186  			},
  1187  		},
  1188  		{
  1189  			name:    "Error if array is too large",
  1190  			wantErr: true,
  1191  			clusterClassVariable: &clusterv1.ClusterClassVariable{
  1192  				Name:     "testArray",
  1193  				Required: true,
  1194  				Schema: clusterv1.VariableSchema{
  1195  					OpenAPIV3Schema: clusterv1.JSONSchemaProps{
  1196  						Type: "array",
  1197  						Items: &clusterv1.JSONSchemaProps{
  1198  							Type: "string",
  1199  						},
  1200  						MaxItems: ptr.To[int64](3),
  1201  					},
  1202  				},
  1203  			},
  1204  			clusterVariable: &clusterv1.ClusterVariable{
  1205  				Name: "testArray",
  1206  				Value: apiextensionsv1.JSON{
  1207  					Raw: []byte(`["value1","value2","value3","value4"]`),
  1208  				},
  1209  			},
  1210  		},
  1211  		{
  1212  			name:    "Error if array is too small",
  1213  			wantErr: true,
  1214  			clusterClassVariable: &clusterv1.ClusterClassVariable{
  1215  				Name:     "testArray",
  1216  				Required: true,
  1217  				Schema: clusterv1.VariableSchema{
  1218  					OpenAPIV3Schema: clusterv1.JSONSchemaProps{
  1219  						Type: "array",
  1220  						Items: &clusterv1.JSONSchemaProps{
  1221  							Type: "string",
  1222  						},
  1223  						MinItems: ptr.To[int64](3),
  1224  					},
  1225  				},
  1226  			},
  1227  			clusterVariable: &clusterv1.ClusterVariable{
  1228  				Name: "testArray",
  1229  				Value: apiextensionsv1.JSON{
  1230  					Raw: []byte(`["value1","value2"]`),
  1231  				},
  1232  			},
  1233  		},
  1234  		{
  1235  			name:    "Error if array contains duplicate values",
  1236  			wantErr: true,
  1237  			clusterClassVariable: &clusterv1.ClusterClassVariable{
  1238  				Name:     "testArray",
  1239  				Required: true,
  1240  				Schema: clusterv1.VariableSchema{
  1241  					OpenAPIV3Schema: clusterv1.JSONSchemaProps{
  1242  						Type: "array",
  1243  						Items: &clusterv1.JSONSchemaProps{
  1244  							Type: "string",
  1245  						},
  1246  						UniqueItems: true,
  1247  					},
  1248  				},
  1249  			},
  1250  			clusterVariable: &clusterv1.ClusterVariable{
  1251  				Name: "testArray",
  1252  				Value: apiextensionsv1.JSON{
  1253  					Raw: []byte(`["value1","value1"]`),
  1254  				},
  1255  			},
  1256  		},
  1257  		{
  1258  			name: "Valid array object",
  1259  			clusterClassVariable: &clusterv1.ClusterClassVariable{
  1260  				Name:     "enumArray",
  1261  				Required: true,
  1262  				Schema: clusterv1.VariableSchema{
  1263  					OpenAPIV3Schema: clusterv1.JSONSchemaProps{
  1264  						Type: "array",
  1265  						Items: &clusterv1.JSONSchemaProps{
  1266  							Type: "string",
  1267  						},
  1268  						Enum: []apiextensionsv1.JSON{
  1269  							{Raw: []byte(`["1","2","3"]`)},
  1270  							{Raw: []byte(`["4","5","6"]`)},
  1271  						},
  1272  					},
  1273  				},
  1274  			},
  1275  			clusterVariable: &clusterv1.ClusterVariable{
  1276  				Name: "enumArray",
  1277  				Value: apiextensionsv1.JSON{
  1278  					Raw: []byte(`["1","2","3"]`),
  1279  				},
  1280  			},
  1281  		},
  1282  		{
  1283  			name:    "Fails, value does not match one of the enum array values",
  1284  			wantErr: true,
  1285  			clusterClassVariable: &clusterv1.ClusterClassVariable{
  1286  				Name:     "enumArray",
  1287  				Required: true,
  1288  				Schema: clusterv1.VariableSchema{
  1289  					OpenAPIV3Schema: clusterv1.JSONSchemaProps{
  1290  						Type: "array",
  1291  						Items: &clusterv1.JSONSchemaProps{
  1292  							Type: "string",
  1293  						},
  1294  						Enum: []apiextensionsv1.JSON{
  1295  							{Raw: []byte(`["1","2","3"]`)},
  1296  							{Raw: []byte(`["4","5","6"]`)},
  1297  						},
  1298  					},
  1299  				},
  1300  			},
  1301  			clusterVariable: &clusterv1.ClusterVariable{
  1302  				Name: "enumArray",
  1303  				Value: apiextensionsv1.JSON{
  1304  					Raw: []byte(`["7","8","9"]`),
  1305  				},
  1306  			},
  1307  		},
  1308  		{
  1309  			name: "Valid object with x-kubernetes-preserve-unknown-fields",
  1310  			clusterClassVariable: &clusterv1.ClusterClassVariable{
  1311  				Name:     "testObject",
  1312  				Required: true,
  1313  				Schema: clusterv1.VariableSchema{
  1314  					OpenAPIV3Schema: clusterv1.JSONSchemaProps{
  1315  						Type: "object",
  1316  						Properties: map[string]clusterv1.JSONSchemaProps{
  1317  							"knownProperty": {
  1318  								Type: "boolean",
  1319  							},
  1320  						},
  1321  						// Preserves fields for the current object (in this case unknownProperty).
  1322  						XPreserveUnknownFields: true,
  1323  					},
  1324  				},
  1325  			},
  1326  			clusterVariable: &clusterv1.ClusterVariable{
  1327  				Name: "testObject",
  1328  				Value: apiextensionsv1.JSON{
  1329  					Raw: []byte(`{"knownProperty":false,"unknownProperty":true}`),
  1330  				},
  1331  			},
  1332  		},
  1333  		{
  1334  			name:    "Error if undefined field",
  1335  			wantErr: true,
  1336  			clusterClassVariable: &clusterv1.ClusterClassVariable{
  1337  				Name:     "testObject",
  1338  				Required: true,
  1339  				Schema: clusterv1.VariableSchema{
  1340  					OpenAPIV3Schema: clusterv1.JSONSchemaProps{
  1341  						Type: "object",
  1342  						Properties: map[string]clusterv1.JSONSchemaProps{
  1343  							"knownProperty": {
  1344  								Type: "boolean",
  1345  							},
  1346  						},
  1347  					},
  1348  				},
  1349  			},
  1350  			clusterVariable: &clusterv1.ClusterVariable{
  1351  				Name: "testObject",
  1352  				Value: apiextensionsv1.JSON{
  1353  					// unknownProperty is not defined in the schema.
  1354  					Raw: []byte(`{"knownProperty":false,"unknownProperty":true}`),
  1355  				},
  1356  			},
  1357  		},
  1358  		{
  1359  			name:    "Error if undefined field with different casing",
  1360  			wantErr: true,
  1361  			clusterClassVariable: &clusterv1.ClusterClassVariable{
  1362  				Name:     "testObject",
  1363  				Required: true,
  1364  				Schema: clusterv1.VariableSchema{
  1365  					OpenAPIV3Schema: clusterv1.JSONSchemaProps{
  1366  						Type: "object",
  1367  						Properties: map[string]clusterv1.JSONSchemaProps{
  1368  							"knownProperty": {
  1369  								Type: "boolean",
  1370  							},
  1371  						},
  1372  					},
  1373  				},
  1374  			},
  1375  			clusterVariable: &clusterv1.ClusterVariable{
  1376  				Name: "testObject",
  1377  				Value: apiextensionsv1.JSON{
  1378  					// KnownProperty is only defined with lower case in the schema.
  1379  					Raw: []byte(`{"KnownProperty":false}`),
  1380  				},
  1381  			},
  1382  		},
  1383  		{
  1384  			name: "Valid nested object with x-kubernetes-preserve-unknown-fields",
  1385  			clusterClassVariable: &clusterv1.ClusterClassVariable{
  1386  				Name:     "testObject",
  1387  				Required: true,
  1388  				Schema: clusterv1.VariableSchema{
  1389  					OpenAPIV3Schema: clusterv1.JSONSchemaProps{
  1390  						Type: "object",
  1391  						// XPreserveUnknownFields preservers recursively if the object has nested fields
  1392  						// as no nested Properties are defined.
  1393  						XPreserveUnknownFields: true,
  1394  					},
  1395  				},
  1396  			},
  1397  			clusterVariable: &clusterv1.ClusterVariable{
  1398  				Name: "testObject",
  1399  				Value: apiextensionsv1.JSON{
  1400  					Raw: []byte(`{"test": {"unknownProperty":false}}`),
  1401  				},
  1402  			},
  1403  		},
  1404  		{
  1405  			name: "Valid object with nested fields and x-kubernetes-preserve-unknown-fields",
  1406  			clusterClassVariable: &clusterv1.ClusterClassVariable{
  1407  				Name:     "testObject",
  1408  				Required: true,
  1409  				Schema: clusterv1.VariableSchema{
  1410  					OpenAPIV3Schema: clusterv1.JSONSchemaProps{
  1411  						Type: "object",
  1412  						Properties: map[string]clusterv1.JSONSchemaProps{
  1413  							"test": {
  1414  								Type: "object",
  1415  								Properties: map[string]clusterv1.JSONSchemaProps{
  1416  									"knownProperty": {
  1417  										Type: "boolean",
  1418  									},
  1419  								},
  1420  								// Preserves fields on the current level (in this case unknownProperty).
  1421  								XPreserveUnknownFields: true,
  1422  							},
  1423  						},
  1424  					},
  1425  				},
  1426  			},
  1427  			clusterVariable: &clusterv1.ClusterVariable{
  1428  				Name: "testObject",
  1429  				Value: apiextensionsv1.JSON{
  1430  					Raw: []byte(`{"test": {"knownProperty":false,"unknownProperty":true}}`),
  1431  				},
  1432  			},
  1433  		},
  1434  		{
  1435  			name:    "Error if undefined field nested",
  1436  			wantErr: true,
  1437  			clusterClassVariable: &clusterv1.ClusterClassVariable{
  1438  				Name:     "testObject",
  1439  				Required: true,
  1440  				Schema: clusterv1.VariableSchema{
  1441  					OpenAPIV3Schema: clusterv1.JSONSchemaProps{
  1442  						Type: "object",
  1443  						Properties: map[string]clusterv1.JSONSchemaProps{
  1444  							"test": {
  1445  								Type: "object",
  1446  								Properties: map[string]clusterv1.JSONSchemaProps{
  1447  									"knownProperty": {
  1448  										Type: "boolean",
  1449  									},
  1450  								},
  1451  							},
  1452  						},
  1453  					},
  1454  				},
  1455  			},
  1456  			clusterVariable: &clusterv1.ClusterVariable{
  1457  				Name: "testObject",
  1458  				Value: apiextensionsv1.JSON{
  1459  					// unknownProperty is not defined in the schema.
  1460  					Raw: []byte(`{"test": {"knownProperty":false,"unknownProperty":true}}`),
  1461  				},
  1462  			},
  1463  		},
  1464  		{
  1465  			name:    "Error if undefined field nested and x-kubernetes-preserve-unknown-fields one level above",
  1466  			wantErr: true,
  1467  			clusterClassVariable: &clusterv1.ClusterClassVariable{
  1468  				Name:     "testObject",
  1469  				Required: true,
  1470  				Schema: clusterv1.VariableSchema{
  1471  					OpenAPIV3Schema: clusterv1.JSONSchemaProps{
  1472  						Type: "object",
  1473  						Properties: map[string]clusterv1.JSONSchemaProps{
  1474  							"test": {
  1475  								Type: "object",
  1476  								Properties: map[string]clusterv1.JSONSchemaProps{
  1477  									"knownProperty": {
  1478  										Type: "boolean",
  1479  									},
  1480  								},
  1481  							},
  1482  						},
  1483  						// Preserves only on the current level as nested Properties are defined.
  1484  						XPreserveUnknownFields: true,
  1485  					},
  1486  				},
  1487  			},
  1488  			clusterVariable: &clusterv1.ClusterVariable{
  1489  				Name: "testObject",
  1490  				Value: apiextensionsv1.JSON{
  1491  					Raw: []byte(`{"test": {"knownProperty":false,"unknownProperty":true}}`),
  1492  				},
  1493  			},
  1494  		},
  1495  		{
  1496  			name: "Valid object with mid-level unknown fields",
  1497  			clusterClassVariable: &clusterv1.ClusterClassVariable{
  1498  				Name:     "testObject",
  1499  				Required: true,
  1500  				Schema: clusterv1.VariableSchema{
  1501  					OpenAPIV3Schema: clusterv1.JSONSchemaProps{
  1502  						Type: "object",
  1503  						Properties: map[string]clusterv1.JSONSchemaProps{
  1504  							"test": {
  1505  								Type: "object",
  1506  								Properties: map[string]clusterv1.JSONSchemaProps{
  1507  									"knownProperty": {
  1508  										Type: "boolean",
  1509  									},
  1510  								},
  1511  							},
  1512  						},
  1513  						// Preserves only on the current level as nested Properties are defined.
  1514  						XPreserveUnknownFields: true,
  1515  					},
  1516  				},
  1517  			},
  1518  			clusterVariable: &clusterv1.ClusterVariable{
  1519  				Name: "testObject",
  1520  				Value: apiextensionsv1.JSON{
  1521  					Raw: []byte(`{"test": {"knownProperty":false},"unknownProperty":true}`),
  1522  				},
  1523  			},
  1524  		},
  1525  	}
  1526  	for _, tt := range tests {
  1527  		t.Run(tt.name, func(t *testing.T) {
  1528  			g := NewWithT(t)
  1529  
  1530  			errList := ValidateClusterVariable(tt.clusterVariable, tt.clusterClassVariable,
  1531  				field.NewPath("spec", "topology", "variables"))
  1532  
  1533  			if tt.wantErr {
  1534  				g.Expect(errList).NotTo(BeEmpty())
  1535  				return
  1536  			}
  1537  			g.Expect(errList).To(BeEmpty())
  1538  		})
  1539  	}
  1540  }