sigs.k8s.io/cluster-api@v1.7.1/internal/topology/variables/clusterclass_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  	"context"
    21  	"testing"
    22  
    23  	. "github.com/onsi/gomega"
    24  	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
    25  	"k8s.io/apimachinery/pkg/util/validation/field"
    26  	"k8s.io/utils/ptr"
    27  
    28  	clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
    29  )
    30  
    31  func Test_ValidateClusterClassVariables(t *testing.T) {
    32  	tests := []struct {
    33  		name                  string
    34  		clusterClassVariables []clusterv1.ClusterClassVariable
    35  		wantErr               bool
    36  	}{
    37  		{
    38  			name: "Error if multiple variables share a name",
    39  			clusterClassVariables: []clusterv1.ClusterClassVariable{
    40  				{
    41  					Name: "cpu",
    42  					Schema: clusterv1.VariableSchema{
    43  						OpenAPIV3Schema: clusterv1.JSONSchemaProps{
    44  							Type:    "integer",
    45  							Minimum: ptr.To[int64](1),
    46  						},
    47  					},
    48  				},
    49  				{
    50  					Name: "cpu",
    51  					Schema: clusterv1.VariableSchema{
    52  						OpenAPIV3Schema: clusterv1.JSONSchemaProps{
    53  							Type:    "integer",
    54  							Minimum: ptr.To[int64](1),
    55  						},
    56  					},
    57  				},
    58  			},
    59  			wantErr: true,
    60  		},
    61  		{
    62  			name: "Pass multiple variable validation",
    63  			clusterClassVariables: []clusterv1.ClusterClassVariable{
    64  				{
    65  					Name: "cpu",
    66  					Schema: clusterv1.VariableSchema{
    67  						OpenAPIV3Schema: clusterv1.JSONSchemaProps{
    68  							Type:    "integer",
    69  							Minimum: ptr.To[int64](1),
    70  						},
    71  					},
    72  				},
    73  				{
    74  					Name: "validNumber",
    75  					Schema: clusterv1.VariableSchema{
    76  						OpenAPIV3Schema: clusterv1.JSONSchemaProps{
    77  							Type:    "number",
    78  							Maximum: ptr.To[int64](1),
    79  						},
    80  					},
    81  				},
    82  
    83  				{
    84  					Name: "validVariable",
    85  					Schema: clusterv1.VariableSchema{
    86  						OpenAPIV3Schema: clusterv1.JSONSchemaProps{
    87  							Type:      "string",
    88  							MinLength: ptr.To[int64](1),
    89  						},
    90  					},
    91  				},
    92  				{
    93  					Name: "location",
    94  					Schema: clusterv1.VariableSchema{
    95  						OpenAPIV3Schema: clusterv1.JSONSchemaProps{
    96  							Type:      "string",
    97  							MinLength: ptr.To[int64](1),
    98  						},
    99  					},
   100  				},
   101  			},
   102  		},
   103  	}
   104  
   105  	for _, tt := range tests {
   106  		t.Run(tt.name, func(t *testing.T) {
   107  			g := NewWithT(t)
   108  
   109  			errList := ValidateClusterClassVariables(context.TODO(),
   110  				tt.clusterClassVariables,
   111  				field.NewPath("spec", "variables"))
   112  
   113  			if tt.wantErr {
   114  				g.Expect(errList).NotTo(BeEmpty())
   115  				return
   116  			}
   117  			g.Expect(errList).To(BeEmpty())
   118  		})
   119  	}
   120  }
   121  
   122  func Test_ValidateClusterClassVariable(t *testing.T) {
   123  	tests := []struct {
   124  		name                 string
   125  		clusterClassVariable *clusterv1.ClusterClassVariable
   126  		wantErr              bool
   127  	}{
   128  		{
   129  			name: "Valid integer schema",
   130  			clusterClassVariable: &clusterv1.ClusterClassVariable{
   131  				Name: "cpu",
   132  				Schema: clusterv1.VariableSchema{
   133  					OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   134  						Type:    "integer",
   135  						Minimum: ptr.To[int64](1),
   136  					},
   137  				},
   138  			},
   139  		},
   140  		{
   141  			name: "Valid string schema",
   142  			clusterClassVariable: &clusterv1.ClusterClassVariable{
   143  				Name: "location",
   144  				Schema: clusterv1.VariableSchema{
   145  					OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   146  						Type:      "string",
   147  						MinLength: ptr.To[int64](1),
   148  					},
   149  				},
   150  			},
   151  		},
   152  		{
   153  			name: "Valid variable name",
   154  			clusterClassVariable: &clusterv1.ClusterClassVariable{
   155  				Name: "validVariable",
   156  				Schema: clusterv1.VariableSchema{
   157  					OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   158  						Type:      "string",
   159  						MinLength: ptr.To[int64](1),
   160  					},
   161  				},
   162  			},
   163  		},
   164  		{
   165  			name: "fail on variable name is builtin",
   166  			clusterClassVariable: &clusterv1.ClusterClassVariable{
   167  				Name: "builtin",
   168  				Schema: clusterv1.VariableSchema{
   169  					OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   170  						Type:      "string",
   171  						MinLength: ptr.To[int64](1),
   172  					},
   173  				},
   174  			},
   175  			wantErr: true,
   176  		},
   177  		{
   178  			name: "fail on empty variable name",
   179  			clusterClassVariable: &clusterv1.ClusterClassVariable{
   180  				Name: "",
   181  				Schema: clusterv1.VariableSchema{
   182  					OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   183  						Type:      "string",
   184  						MinLength: ptr.To[int64](1),
   185  					},
   186  				},
   187  			},
   188  			wantErr: true,
   189  		},
   190  		{
   191  			name: "fail on variable name containing dot (.)",
   192  			clusterClassVariable: &clusterv1.ClusterClassVariable{
   193  				Name: "path.tovariable",
   194  				Schema: clusterv1.VariableSchema{
   195  					OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   196  						Type:      "string",
   197  						MinLength: ptr.To[int64](1),
   198  					},
   199  				},
   200  			},
   201  			wantErr: true,
   202  		},
   203  		{
   204  			name: "Valid variable metadata",
   205  			clusterClassVariable: &clusterv1.ClusterClassVariable{
   206  				Name: "validVariable",
   207  				Metadata: clusterv1.ClusterClassVariableMetadata{
   208  					Labels: map[string]string{
   209  						"label-key": "label-value",
   210  					},
   211  					Annotations: map[string]string{
   212  						"annotation-key": "annotation-value",
   213  					},
   214  				},
   215  				Schema: clusterv1.VariableSchema{
   216  					OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   217  						Type:      "string",
   218  						MinLength: ptr.To[int64](1),
   219  					},
   220  				},
   221  			},
   222  		},
   223  		{
   224  			name: "fail on invalid variable label: key does not start with alphanumeric character",
   225  			clusterClassVariable: &clusterv1.ClusterClassVariable{
   226  				Name: "path.tovariable",
   227  				Metadata: clusterv1.ClusterClassVariableMetadata{
   228  					Labels: map[string]string{
   229  						".label-key": "label-value",
   230  					},
   231  				},
   232  				Schema: clusterv1.VariableSchema{
   233  					OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   234  						Type:      "string",
   235  						MinLength: ptr.To[int64](1),
   236  					},
   237  				},
   238  			},
   239  			wantErr: true,
   240  		},
   241  		{
   242  			name: "fail on invalid variable annotation: key does not start with alphanumeric character",
   243  			clusterClassVariable: &clusterv1.ClusterClassVariable{
   244  				Name: "path.tovariable",
   245  				Metadata: clusterv1.ClusterClassVariableMetadata{
   246  					Annotations: map[string]string{
   247  						".annotation-key": "annotation-value",
   248  					},
   249  				},
   250  				Schema: clusterv1.VariableSchema{
   251  					OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   252  						Type:      "string",
   253  						MinLength: ptr.To[int64](1),
   254  					},
   255  				},
   256  			},
   257  			wantErr: true,
   258  		},
   259  		{
   260  			name: "Valid default value regular string",
   261  			clusterClassVariable: &clusterv1.ClusterClassVariable{
   262  				Name:     "var",
   263  				Required: true,
   264  				Schema: clusterv1.VariableSchema{
   265  					OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   266  						Type: "string",
   267  						Default: &apiextensionsv1.JSON{
   268  							Raw: []byte(`"defaultValue"`),
   269  						},
   270  					},
   271  				},
   272  			},
   273  		},
   274  		{
   275  			name: "fail on default value with invalid JSON",
   276  			clusterClassVariable: &clusterv1.ClusterClassVariable{
   277  				Name:     "var",
   278  				Required: true,
   279  				Schema: clusterv1.VariableSchema{
   280  					OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   281  						Type: "string",
   282  						Default: &apiextensionsv1.JSON{
   283  							Raw: []byte(`"defaultValue": "value"`), // invalid JSON
   284  						},
   285  					},
   286  				},
   287  			},
   288  			wantErr: true,
   289  		},
   290  		{
   291  			name: "Valid example value regular string",
   292  			clusterClassVariable: &clusterv1.ClusterClassVariable{
   293  				Name:     "var",
   294  				Required: true,
   295  				Schema: clusterv1.VariableSchema{
   296  					OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   297  						Type: "string",
   298  						Example: &apiextensionsv1.JSON{
   299  							Raw: []byte(`"exampleValue"`),
   300  						},
   301  					},
   302  				},
   303  			},
   304  		},
   305  		{
   306  			name: "fail on example value with invalid JSON",
   307  			clusterClassVariable: &clusterv1.ClusterClassVariable{
   308  				Name:     "var",
   309  				Required: true,
   310  				Schema: clusterv1.VariableSchema{
   311  					OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   312  						Type: "string",
   313  						Example: &apiextensionsv1.JSON{
   314  							Raw: []byte(`"exampleValue": "value"`), // invalid JSON
   315  						},
   316  					},
   317  				},
   318  			},
   319  			wantErr: true,
   320  		},
   321  		{
   322  			name: "Valid enum values",
   323  			clusterClassVariable: &clusterv1.ClusterClassVariable{
   324  				Name:     "var",
   325  				Required: true,
   326  				Schema: clusterv1.VariableSchema{
   327  					OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   328  						Type: "string",
   329  						Enum: []apiextensionsv1.JSON{
   330  							{Raw: []byte(`"enumValue1"`)},
   331  							{Raw: []byte(`"enumValue2"`)},
   332  						},
   333  					},
   334  				},
   335  			},
   336  		},
   337  		{
   338  			name: "fail on enum value with invalid JSON",
   339  			clusterClassVariable: &clusterv1.ClusterClassVariable{
   340  				Name:     "var",
   341  				Required: true,
   342  				Schema: clusterv1.VariableSchema{
   343  					OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   344  						Type: "string",
   345  						Enum: []apiextensionsv1.JSON{
   346  							{Raw: []byte(`"defaultValue": "value"`)}, // invalid JSON
   347  						},
   348  					},
   349  				},
   350  			},
   351  			wantErr: true,
   352  		},
   353  		{
   354  			name: "fail on variable type is null",
   355  			clusterClassVariable: &clusterv1.ClusterClassVariable{
   356  				Name: "var",
   357  				Schema: clusterv1.VariableSchema{
   358  					OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   359  						Type: "null",
   360  					},
   361  				},
   362  			},
   363  			wantErr: true,
   364  		},
   365  		{
   366  			name: "fail on variable type is not valid",
   367  			clusterClassVariable: &clusterv1.ClusterClassVariable{
   368  				Name: "var",
   369  				Schema: clusterv1.VariableSchema{
   370  					OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   371  						Type: "invalidVariableType",
   372  					},
   373  				},
   374  			},
   375  			wantErr: true,
   376  		},
   377  		{
   378  			name: "fail on variable type length zero",
   379  			clusterClassVariable: &clusterv1.ClusterClassVariable{
   380  				Name: "var",
   381  				Schema: clusterv1.VariableSchema{
   382  					OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   383  						Type: "",
   384  					},
   385  				},
   386  			},
   387  			wantErr: true,
   388  		},
   389  		{
   390  			name: "Valid object schema",
   391  			clusterClassVariable: &clusterv1.ClusterClassVariable{
   392  				Name:     "httpProxy",
   393  				Required: true,
   394  				Schema: clusterv1.VariableSchema{
   395  					OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   396  						Type: "object",
   397  						Properties: map[string]clusterv1.JSONSchemaProps{
   398  							"enabled": {
   399  								Type:    "boolean",
   400  								Default: &apiextensionsv1.JSON{Raw: []byte(`false`)},
   401  							},
   402  							"url": {
   403  								Type: "string",
   404  							},
   405  							"noProxy": {
   406  								Type: "string",
   407  							},
   408  						},
   409  					},
   410  				},
   411  			},
   412  		},
   413  		{
   414  			name: "fail on invalid object schema",
   415  			clusterClassVariable: &clusterv1.ClusterClassVariable{
   416  				Name:     "httpProxy",
   417  				Required: true,
   418  				Schema: clusterv1.VariableSchema{
   419  					OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   420  						Type: "object",
   421  						Properties: map[string]clusterv1.JSONSchemaProps{
   422  							"enabled": {
   423  								Type:    "boolean",
   424  								Default: &apiextensionsv1.JSON{Raw: []byte(`false`)},
   425  							},
   426  							"url": {
   427  								Type: "string",
   428  							},
   429  							"noProxy": {
   430  								Type: "invalidType", // invalid type.
   431  							},
   432  						},
   433  					},
   434  				},
   435  			},
   436  			wantErr: true,
   437  		},
   438  		{
   439  			name: "Valid map schema",
   440  			clusterClassVariable: &clusterv1.ClusterClassVariable{
   441  				Name:     "httpProxy",
   442  				Required: true,
   443  				Schema: clusterv1.VariableSchema{
   444  					OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   445  						Type: "object",
   446  						AdditionalProperties: &clusterv1.JSONSchemaProps{
   447  							Type: "object",
   448  							Properties: map[string]clusterv1.JSONSchemaProps{
   449  								"enabled": {
   450  									Type:    "boolean",
   451  									Default: &apiextensionsv1.JSON{Raw: []byte(`false`)},
   452  								},
   453  								"url": {
   454  									Type: "string",
   455  								},
   456  								"noProxy": {
   457  									Type: "string",
   458  								},
   459  							},
   460  						},
   461  					},
   462  				},
   463  			},
   464  		},
   465  		{
   466  			name: "fail on invalid map schema",
   467  			clusterClassVariable: &clusterv1.ClusterClassVariable{
   468  				Name:     "httpProxy",
   469  				Required: true,
   470  				Schema: clusterv1.VariableSchema{
   471  					OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   472  						Type: "object",
   473  						AdditionalProperties: &clusterv1.JSONSchemaProps{
   474  							Type: "object",
   475  							Properties: map[string]clusterv1.JSONSchemaProps{
   476  								"enabled": {
   477  									Type:    "boolean",
   478  									Default: &apiextensionsv1.JSON{Raw: []byte(`false`)},
   479  								},
   480  								"url": {
   481  									Type: "string",
   482  								},
   483  								"noProxy": {
   484  									Type: "invalidType", // invalid type.
   485  								},
   486  							},
   487  						},
   488  					},
   489  				},
   490  			},
   491  			wantErr: true,
   492  		},
   493  		{
   494  			name: "fail on object (properties) and map (additionalProperties) both set",
   495  			clusterClassVariable: &clusterv1.ClusterClassVariable{
   496  				Name:     "httpProxy",
   497  				Required: true,
   498  				Schema: clusterv1.VariableSchema{
   499  					OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   500  						Type: "object",
   501  						AdditionalProperties: &clusterv1.JSONSchemaProps{
   502  							Type: "object",
   503  							Properties: map[string]clusterv1.JSONSchemaProps{
   504  								"enabled": {
   505  									Type:    "boolean",
   506  									Default: &apiextensionsv1.JSON{Raw: []byte(`false`)},
   507  								},
   508  							},
   509  						},
   510  						Properties: map[string]clusterv1.JSONSchemaProps{
   511  							"enabled": {
   512  								Type:    "boolean",
   513  								Default: &apiextensionsv1.JSON{Raw: []byte(`false`)},
   514  							},
   515  						},
   516  					},
   517  				},
   518  			},
   519  			wantErr: true,
   520  		},
   521  		{
   522  			name: "Valid array schema",
   523  			clusterClassVariable: &clusterv1.ClusterClassVariable{
   524  				Name:     "arrayVariable",
   525  				Required: true,
   526  				Schema: clusterv1.VariableSchema{
   527  					OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   528  						Type: "array",
   529  						Items: &clusterv1.JSONSchemaProps{
   530  							Type:    "boolean",
   531  							Default: &apiextensionsv1.JSON{Raw: []byte(`false`)},
   532  						},
   533  					},
   534  				},
   535  			},
   536  		},
   537  		{
   538  			name: "fail on invalid array schema",
   539  			clusterClassVariable: &clusterv1.ClusterClassVariable{
   540  				Name: "arrayVariable",
   541  				Schema: clusterv1.VariableSchema{
   542  					OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   543  						Type: "array",
   544  						Items: &clusterv1.JSONSchemaProps{
   545  							Type:    "string",
   546  							Default: &apiextensionsv1.JSON{Raw: []byte(`invalidString`)}, // missing quotes.
   547  						},
   548  					},
   549  				},
   550  			},
   551  			wantErr: true,
   552  		},
   553  		{
   554  			name: "pass on variable with required set true with a default defined",
   555  			clusterClassVariable: &clusterv1.ClusterClassVariable{
   556  				Name:     "var",
   557  				Required: true,
   558  				Schema: clusterv1.VariableSchema{
   559  					OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   560  						Type:    "string",
   561  						Default: &apiextensionsv1.JSON{Raw: []byte(`"defaultValue"`)},
   562  					},
   563  				},
   564  			},
   565  		},
   566  		{
   567  			name: "pass on variable with a default that is valid by the given schema",
   568  			clusterClassVariable: &clusterv1.ClusterClassVariable{
   569  				Name: "var",
   570  				Schema: clusterv1.VariableSchema{
   571  					OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   572  						Type:      "string",
   573  						MaxLength: ptr.To[int64](6),
   574  						Default:   &apiextensionsv1.JSON{Raw: []byte(`"short"`)},
   575  					},
   576  				},
   577  			},
   578  		},
   579  		{
   580  			name: "fail on variable with a default that is invalid by the given schema",
   581  			clusterClassVariable: &clusterv1.ClusterClassVariable{
   582  				Name: "var",
   583  				Schema: clusterv1.VariableSchema{
   584  					OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   585  						Type:      "string",
   586  						MaxLength: ptr.To[int64](6),
   587  						Default:   &apiextensionsv1.JSON{Raw: []byte(`"veryLongValueIsInvalid"`)},
   588  					},
   589  				},
   590  			},
   591  			wantErr: true,
   592  		},
   593  		{
   594  			name: "pass if variable is an object with default value valid by the given schema",
   595  			clusterClassVariable: &clusterv1.ClusterClassVariable{
   596  				Name: "var",
   597  				Schema: clusterv1.VariableSchema{
   598  					OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   599  						Type: "object",
   600  						Properties: map[string]clusterv1.JSONSchemaProps{
   601  							"spec": {
   602  								Type: "object",
   603  								Properties: map[string]clusterv1.JSONSchemaProps{
   604  									"replicas": {
   605  										Type:    "integer",
   606  										Default: &apiextensionsv1.JSON{Raw: []byte(`100`)},
   607  										Minimum: ptr.To[int64](1),
   608  									},
   609  								},
   610  							},
   611  						},
   612  					},
   613  				},
   614  			},
   615  		},
   616  		{
   617  			name: "fail if variable is an object with default value invalidated by the given schema",
   618  			clusterClassVariable: &clusterv1.ClusterClassVariable{
   619  				Name: "var",
   620  				Schema: clusterv1.VariableSchema{
   621  					OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   622  						Type: "object",
   623  						Properties: map[string]clusterv1.JSONSchemaProps{
   624  							"spec": {
   625  								Type: "object",
   626  								Properties: map[string]clusterv1.JSONSchemaProps{
   627  									"replicas": {
   628  										Type:    "integer",
   629  										Default: &apiextensionsv1.JSON{Raw: []byte(`-100`)},
   630  										Minimum: ptr.To[int64](1),
   631  									},
   632  								},
   633  							},
   634  						},
   635  					},
   636  				},
   637  			},
   638  			wantErr: true,
   639  		},
   640  		{
   641  			name: "fail if variable is an object with a top level default value invalidated by the given schema",
   642  			clusterClassVariable: &clusterv1.ClusterClassVariable{
   643  				Name: "var",
   644  				Schema: clusterv1.VariableSchema{
   645  					OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   646  						Type: "object",
   647  						Default: &apiextensionsv1.JSON{
   648  							Raw: []byte(`{"spec":{"replicas": -100}}`),
   649  						},
   650  						Properties: map[string]clusterv1.JSONSchemaProps{
   651  							"spec": {
   652  								Type: "object",
   653  								Properties: map[string]clusterv1.JSONSchemaProps{
   654  									"replicas": {
   655  										Type:    "integer",
   656  										Minimum: ptr.To[int64](1),
   657  									},
   658  								},
   659  							},
   660  						},
   661  					},
   662  				},
   663  			},
   664  			wantErr: true,
   665  		},
   666  		{
   667  			name: "pass if variable is an object with a top level default value valid by the given schema",
   668  			clusterClassVariable: &clusterv1.ClusterClassVariable{
   669  				Name: "var",
   670  				Schema: clusterv1.VariableSchema{
   671  					OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   672  						Type: "object",
   673  						Default: &apiextensionsv1.JSON{
   674  							Raw: []byte(`{"spec":{"replicas": 100}}`),
   675  						},
   676  						Properties: map[string]clusterv1.JSONSchemaProps{
   677  							"spec": {
   678  								Type: "object",
   679  								Properties: map[string]clusterv1.JSONSchemaProps{
   680  									"replicas": {
   681  										Type:    "integer",
   682  										Minimum: ptr.To[int64](1),
   683  									},
   684  								},
   685  							},
   686  						},
   687  					},
   688  				},
   689  			},
   690  		},
   691  		{
   692  			name: "fail if field is required below properties and sets a default that misses the field",
   693  			clusterClassVariable: &clusterv1.ClusterClassVariable{
   694  				Name: "var",
   695  				Schema: clusterv1.VariableSchema{
   696  					OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   697  						Type: "object",
   698  						Properties: map[string]clusterv1.JSONSchemaProps{
   699  							"spec": {
   700  								Type:     "object",
   701  								Required: []string{"replicas"},
   702  								Default: &apiextensionsv1.JSON{
   703  									// replicas missing results in failure
   704  									Raw: []byte(`{"value": 100}`),
   705  								},
   706  								Properties: map[string]clusterv1.JSONSchemaProps{
   707  									"replicas": {
   708  										Type:    "integer",
   709  										Default: &apiextensionsv1.JSON{Raw: []byte(`100`)},
   710  										Minimum: ptr.To[int64](1),
   711  									},
   712  									"value": {
   713  										Type:    "integer",
   714  										Default: &apiextensionsv1.JSON{Raw: []byte(`100`)},
   715  										Minimum: ptr.To[int64](1),
   716  									},
   717  								},
   718  							},
   719  						},
   720  					},
   721  				},
   722  			},
   723  			wantErr: true,
   724  		},
   725  		{
   726  			name: "pass if field is required below properties and sets a default",
   727  			clusterClassVariable: &clusterv1.ClusterClassVariable{
   728  				Name: "var",
   729  				Schema: clusterv1.VariableSchema{
   730  					OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   731  						Type: "object",
   732  						Properties: map[string]clusterv1.JSONSchemaProps{
   733  							"spec": {
   734  								Type:     "object",
   735  								Required: []string{"replicas"},
   736  								Default: &apiextensionsv1.JSON{
   737  									// replicas is set here so the `required` property is met.
   738  									Raw: []byte(`{"replicas": 100}`),
   739  								},
   740  								Properties: map[string]clusterv1.JSONSchemaProps{
   741  									"replicas": {
   742  										Type:    "integer",
   743  										Default: &apiextensionsv1.JSON{Raw: []byte(`100`)},
   744  										Minimum: ptr.To[int64](1),
   745  									},
   746  								},
   747  							},
   748  						},
   749  					},
   750  				},
   751  			},
   752  		},
   753  
   754  		{
   755  			name: "pass on variable with an example that is valid by the given schema",
   756  			clusterClassVariable: &clusterv1.ClusterClassVariable{
   757  				Name: "var",
   758  				Schema: clusterv1.VariableSchema{
   759  					OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   760  						Type:      "string",
   761  						MaxLength: ptr.To[int64](6),
   762  						Example:   &apiextensionsv1.JSON{Raw: []byte(`"short"`)},
   763  					},
   764  				},
   765  			},
   766  		},
   767  		{
   768  			name: "fail on variable with an example that is invalid by the given schema",
   769  			clusterClassVariable: &clusterv1.ClusterClassVariable{
   770  				Name: "var",
   771  				Schema: clusterv1.VariableSchema{
   772  					OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   773  						Type:      "string",
   774  						MaxLength: ptr.To[int64](6),
   775  						Example:   &apiextensionsv1.JSON{Raw: []byte(`"veryLongValueIsInvalid"`)},
   776  					},
   777  				},
   778  			},
   779  			wantErr: true,
   780  		},
   781  		{
   782  			name: "pass if variable is an object with an example valid by the given schema",
   783  			clusterClassVariable: &clusterv1.ClusterClassVariable{
   784  				Name: "var",
   785  				Schema: clusterv1.VariableSchema{
   786  					OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   787  						Type: "object",
   788  						Properties: map[string]clusterv1.JSONSchemaProps{
   789  							"spec": {
   790  								Type: "object",
   791  								Properties: map[string]clusterv1.JSONSchemaProps{
   792  									"replicas": {
   793  										Type:    "integer",
   794  										Minimum: ptr.To[int64](0),
   795  										Example: &apiextensionsv1.JSON{Raw: []byte(`100`)},
   796  									},
   797  								},
   798  							},
   799  						},
   800  					},
   801  				},
   802  			},
   803  		},
   804  		{
   805  			name: "fail if variable is an object with an example invalidated by the given schema",
   806  			clusterClassVariable: &clusterv1.ClusterClassVariable{
   807  				Name: "var",
   808  				Schema: clusterv1.VariableSchema{
   809  					OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   810  						Type: "object",
   811  						Properties: map[string]clusterv1.JSONSchemaProps{
   812  							"spec": {
   813  								Type: "object",
   814  								Properties: map[string]clusterv1.JSONSchemaProps{
   815  									"replicas": {
   816  										Type:    "integer",
   817  										Minimum: ptr.To[int64](0),
   818  										Example: &apiextensionsv1.JSON{Raw: []byte(`-100`)},
   819  									},
   820  								},
   821  							},
   822  						},
   823  					},
   824  				},
   825  			},
   826  			wantErr: true,
   827  		},
   828  		{
   829  			name: "fail if variable is an object with a top level example value invalidated by the given schema",
   830  			clusterClassVariable: &clusterv1.ClusterClassVariable{
   831  				Name: "var",
   832  				Schema: clusterv1.VariableSchema{
   833  					OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   834  						Type: "object",
   835  						Example: &apiextensionsv1.JSON{
   836  							Raw: []byte(`{"spec":{"replicas": -100}}`),
   837  						},
   838  						Properties: map[string]clusterv1.JSONSchemaProps{
   839  							"spec": {
   840  								Type: "object",
   841  								Properties: map[string]clusterv1.JSONSchemaProps{
   842  									"replicas": {
   843  										Type:    "integer",
   844  										Minimum: ptr.To[int64](1),
   845  									},
   846  								},
   847  							},
   848  						},
   849  					},
   850  				},
   851  			},
   852  			wantErr: true,
   853  		},
   854  		{
   855  			name: "pass if variable is an object with a top level default value valid by the given schema",
   856  			clusterClassVariable: &clusterv1.ClusterClassVariable{
   857  				Name: "var",
   858  				Schema: clusterv1.VariableSchema{
   859  					OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   860  						Type: "object",
   861  						Example: &apiextensionsv1.JSON{
   862  							Raw: []byte(`{"spec":{"replicas": 100}}`),
   863  						},
   864  						Properties: map[string]clusterv1.JSONSchemaProps{
   865  							"spec": {
   866  								Type: "object",
   867  								Properties: map[string]clusterv1.JSONSchemaProps{
   868  									"replicas": {
   869  										Type:    "integer",
   870  										Minimum: ptr.To[int64](1),
   871  									},
   872  								},
   873  							},
   874  						},
   875  					},
   876  				},
   877  			},
   878  		},
   879  		{
   880  			name: "pass on variable with an enum with all variables valid by the given schema",
   881  			clusterClassVariable: &clusterv1.ClusterClassVariable{
   882  				Name: "var",
   883  				Schema: clusterv1.VariableSchema{
   884  					OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   885  						Type:      "string",
   886  						MaxLength: ptr.To[int64](6),
   887  						Enum: []apiextensionsv1.JSON{
   888  							{Raw: []byte(`"short1"`)},
   889  							{Raw: []byte(`"short2"`)},
   890  						},
   891  					},
   892  				},
   893  			},
   894  		},
   895  		{
   896  			name: "fail on variable with an enum with a value that is invalid by the given schema",
   897  			clusterClassVariable: &clusterv1.ClusterClassVariable{
   898  				Name: "var",
   899  				Schema: clusterv1.VariableSchema{
   900  					OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   901  						Type:      "string",
   902  						MaxLength: ptr.To[int64](6),
   903  						Enum: []apiextensionsv1.JSON{
   904  							{Raw: []byte(`"veryLongValueIsInvalid"`)},
   905  							{Raw: []byte(`"short"`)},
   906  						},
   907  					},
   908  				},
   909  			},
   910  			wantErr: true,
   911  		},
   912  		{
   913  			name: "pass if variable is an object with an enum value that is valid by the given schema",
   914  			clusterClassVariable: &clusterv1.ClusterClassVariable{
   915  				Name: "var",
   916  				Schema: clusterv1.VariableSchema{
   917  					OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   918  						Type: "object",
   919  						Properties: map[string]clusterv1.JSONSchemaProps{
   920  							"spec": {
   921  								Type: "object",
   922  								Properties: map[string]clusterv1.JSONSchemaProps{
   923  									"replicas": {
   924  										Type:    "integer",
   925  										Minimum: ptr.To[int64](0),
   926  										Enum: []apiextensionsv1.JSON{
   927  											{Raw: []byte(`100`)},
   928  											{Raw: []byte(`5`)},
   929  										},
   930  									},
   931  								},
   932  							},
   933  						},
   934  					},
   935  				},
   936  			},
   937  		},
   938  		{
   939  			name: "fail if variable is an object with an enum value invalidated by the given schema",
   940  			clusterClassVariable: &clusterv1.ClusterClassVariable{
   941  				Name: "var",
   942  				Schema: clusterv1.VariableSchema{
   943  					OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   944  						Type: "object",
   945  						Properties: map[string]clusterv1.JSONSchemaProps{
   946  							"spec": {
   947  								Type: "object",
   948  								Properties: map[string]clusterv1.JSONSchemaProps{
   949  									"replicas": {
   950  										Type:    "integer",
   951  										Minimum: ptr.To[int64](0),
   952  										Enum: []apiextensionsv1.JSON{
   953  											{Raw: []byte(`100`)},
   954  											{Raw: []byte(`-100`)},
   955  										},
   956  									},
   957  								},
   958  							},
   959  						},
   960  					},
   961  				},
   962  			},
   963  			wantErr: true,
   964  		},
   965  		{
   966  			name: "fail if variable is an object with a top level enum value invalidated by the given schema",
   967  			clusterClassVariable: &clusterv1.ClusterClassVariable{
   968  				Name: "var",
   969  				Schema: clusterv1.VariableSchema{
   970  					OpenAPIV3Schema: clusterv1.JSONSchemaProps{
   971  						Type: "object",
   972  						Enum: []apiextensionsv1.JSON{
   973  							{
   974  								Raw: []byte(`{"spec":{"replicas": 100}}`),
   975  							},
   976  							{
   977  								Raw: []byte(`{"spec":{"replicas": -100}}`),
   978  							},
   979  						},
   980  						Properties: map[string]clusterv1.JSONSchemaProps{
   981  							"spec": {
   982  								Type: "object",
   983  								Properties: map[string]clusterv1.JSONSchemaProps{
   984  									"replicas": {
   985  										Type:    "integer",
   986  										Minimum: ptr.To[int64](1),
   987  									},
   988  								},
   989  							},
   990  						},
   991  					},
   992  				},
   993  			},
   994  			wantErr: true,
   995  		},
   996  		{
   997  			name: "pass if variable is an object with a top level enum value that is valid by the given schema",
   998  			clusterClassVariable: &clusterv1.ClusterClassVariable{
   999  				Name: "var",
  1000  				Schema: clusterv1.VariableSchema{
  1001  					OpenAPIV3Schema: clusterv1.JSONSchemaProps{
  1002  						Type: "object",
  1003  						Enum: []apiextensionsv1.JSON{
  1004  							{
  1005  								Raw: []byte(`{"spec":{"replicas": 100}}`),
  1006  							},
  1007  							{
  1008  								Raw: []byte(`{"spec":{"replicas": 200}}`),
  1009  							},
  1010  						},
  1011  						Properties: map[string]clusterv1.JSONSchemaProps{
  1012  							"spec": {
  1013  								Type: "object",
  1014  								Properties: map[string]clusterv1.JSONSchemaProps{
  1015  									"replicas": {
  1016  										Type:    "integer",
  1017  										Minimum: ptr.To[int64](1),
  1018  									},
  1019  								},
  1020  							},
  1021  						},
  1022  					},
  1023  				},
  1024  			},
  1025  		},
  1026  	}
  1027  	for _, tt := range tests {
  1028  		t.Run(tt.name, func(t *testing.T) {
  1029  			g := NewWithT(t)
  1030  
  1031  			errList := validateClusterClassVariable(context.TODO(),
  1032  				tt.clusterClassVariable,
  1033  				field.NewPath("spec", "variables").Index(0))
  1034  
  1035  			if tt.wantErr {
  1036  				g.Expect(errList).NotTo(BeEmpty())
  1037  				return
  1038  			}
  1039  			g.Expect(errList).To(BeEmpty())
  1040  		})
  1041  	}
  1042  }