k8s.io/kubernetes@v1.31.0-alpha.0.0.20240520171757-56147500dadc/pkg/apis/resource/validation/validation_resourceclassparameters_test.go (about)

     1  /*
     2  Copyright 2022 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 validation
    18  
    19  import (
    20  	"testing"
    21  
    22  	"github.com/stretchr/testify/assert"
    23  
    24  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    25  	"k8s.io/apimachinery/pkg/util/validation/field"
    26  	"k8s.io/kubernetes/pkg/apis/resource"
    27  	"k8s.io/utils/ptr"
    28  )
    29  
    30  func testResourceClassParameters(name, namespace string, filters []resource.ResourceFilter) *resource.ResourceClassParameters {
    31  	return &resource.ResourceClassParameters{
    32  		ObjectMeta: metav1.ObjectMeta{
    33  			Name:      name,
    34  			Namespace: namespace,
    35  		},
    36  		Filters: filters,
    37  	}
    38  }
    39  
    40  var goodFilters []resource.ResourceFilter
    41  
    42  func TestValidateResourceClassParameters(t *testing.T) {
    43  	goodName := "foo"
    44  	badName := "!@#$%^"
    45  	badValue := "spaces not allowed"
    46  	now := metav1.Now()
    47  
    48  	scenarios := map[string]struct {
    49  		parameters   *resource.ResourceClassParameters
    50  		wantFailures field.ErrorList
    51  	}{
    52  		"good": {
    53  			parameters: testResourceClassParameters(goodName, goodName, goodFilters),
    54  		},
    55  		"missing-name": {
    56  			wantFailures: field.ErrorList{field.Required(field.NewPath("metadata", "name"), "name or generateName is required")},
    57  			parameters:   testResourceClassParameters("", goodName, goodFilters),
    58  		},
    59  		"missing-namespace": {
    60  			wantFailures: field.ErrorList{field.Required(field.NewPath("metadata", "namespace"), "")},
    61  			parameters:   testResourceClassParameters(goodName, "", goodFilters),
    62  		},
    63  		"bad-name": {
    64  			wantFailures: field.ErrorList{field.Invalid(field.NewPath("metadata", "name"), badName, "a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')")},
    65  			parameters:   testResourceClassParameters(badName, goodName, goodFilters),
    66  		},
    67  		"bad-namespace": {
    68  			wantFailures: field.ErrorList{field.Invalid(field.NewPath("metadata", "namespace"), badName, "a lowercase RFC 1123 label must consist of lower case alphanumeric characters or '-', and must start and end with an alphanumeric character (e.g. 'my-name',  or '123-abc', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?')")},
    69  			parameters:   testResourceClassParameters(goodName, badName, goodFilters),
    70  		},
    71  		"generate-name": {
    72  			parameters: func() *resource.ResourceClassParameters {
    73  				parameters := testResourceClassParameters(goodName, goodName, goodFilters)
    74  				parameters.GenerateName = "prefix-"
    75  				return parameters
    76  			}(),
    77  		},
    78  		"uid": {
    79  			parameters: func() *resource.ResourceClassParameters {
    80  				parameters := testResourceClassParameters(goodName, goodName, goodFilters)
    81  				parameters.UID = "ac051fac-2ead-46d9-b8b4-4e0fbeb7455d"
    82  				return parameters
    83  			}(),
    84  		},
    85  		"resource-version": {
    86  			parameters: func() *resource.ResourceClassParameters {
    87  				parameters := testResourceClassParameters(goodName, goodName, goodFilters)
    88  				parameters.ResourceVersion = "1"
    89  				return parameters
    90  			}(),
    91  		},
    92  		"generation": {
    93  			parameters: func() *resource.ResourceClassParameters {
    94  				parameters := testResourceClassParameters(goodName, goodName, goodFilters)
    95  				parameters.Generation = 100
    96  				return parameters
    97  			}(),
    98  		},
    99  		"creation-timestamp": {
   100  			parameters: func() *resource.ResourceClassParameters {
   101  				parameters := testResourceClassParameters(goodName, goodName, goodFilters)
   102  				parameters.CreationTimestamp = now
   103  				return parameters
   104  			}(),
   105  		},
   106  		"deletion-grace-period-seconds": {
   107  			parameters: func() *resource.ResourceClassParameters {
   108  				parameters := testResourceClassParameters(goodName, goodName, goodFilters)
   109  				parameters.DeletionGracePeriodSeconds = ptr.To[int64](10)
   110  				return parameters
   111  			}(),
   112  		},
   113  		"owner-references": {
   114  			parameters: func() *resource.ResourceClassParameters {
   115  				parameters := testResourceClassParameters(goodName, goodName, goodFilters)
   116  				parameters.OwnerReferences = []metav1.OwnerReference{
   117  					{
   118  						APIVersion: "v1",
   119  						Kind:       "pod",
   120  						Name:       "foo",
   121  						UID:        "ac051fac-2ead-46d9-b8b4-4e0fbeb7455d",
   122  					},
   123  				}
   124  				return parameters
   125  			}(),
   126  		},
   127  		"finalizers": {
   128  			parameters: func() *resource.ResourceClassParameters {
   129  				parameters := testResourceClassParameters(goodName, goodName, goodFilters)
   130  				parameters.Finalizers = []string{
   131  					"example.com/foo",
   132  				}
   133  				return parameters
   134  			}(),
   135  		},
   136  		"managed-fields": {
   137  			parameters: func() *resource.ResourceClassParameters {
   138  				parameters := testResourceClassParameters(goodName, goodName, goodFilters)
   139  				parameters.ManagedFields = []metav1.ManagedFieldsEntry{
   140  					{
   141  						FieldsType: "FieldsV1",
   142  						Operation:  "Apply",
   143  						APIVersion: "apps/v1",
   144  						Manager:    "foo",
   145  					},
   146  				}
   147  				return parameters
   148  			}(),
   149  		},
   150  		"good-labels": {
   151  			parameters: func() *resource.ResourceClassParameters {
   152  				parameters := testResourceClassParameters(goodName, goodName, goodFilters)
   153  				parameters.Labels = map[string]string{
   154  					"apps.kubernetes.io/name": "test",
   155  				}
   156  				return parameters
   157  			}(),
   158  		},
   159  		"bad-labels": {
   160  			wantFailures: field.ErrorList{field.Invalid(field.NewPath("metadata", "labels"), badValue, "a valid label must be an empty string or consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character (e.g. 'MyValue',  or 'my_value',  or '12345', regex used for validation is '(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])?')")},
   161  			parameters: func() *resource.ResourceClassParameters {
   162  				parameters := testResourceClassParameters(goodName, goodName, goodFilters)
   163  				parameters.Labels = map[string]string{
   164  					"hello-world": badValue,
   165  				}
   166  				return parameters
   167  			}(),
   168  		},
   169  		"good-annotations": {
   170  			parameters: func() *resource.ResourceClassParameters {
   171  				parameters := testResourceClassParameters(goodName, goodName, goodFilters)
   172  				parameters.Annotations = map[string]string{
   173  					"foo": "bar",
   174  				}
   175  				return parameters
   176  			}(),
   177  		},
   178  		"bad-annotations": {
   179  			wantFailures: field.ErrorList{field.Invalid(field.NewPath("metadata", "annotations"), badName, "name part must consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character (e.g. 'MyName',  or 'my.name',  or '123-abc', regex used for validation is '([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]')")},
   180  			parameters: func() *resource.ResourceClassParameters {
   181  				parameters := testResourceClassParameters(goodName, goodName, goodFilters)
   182  				parameters.Annotations = map[string]string{
   183  					badName: "hello world",
   184  				}
   185  				return parameters
   186  			}(),
   187  		},
   188  
   189  		"empty-model": {
   190  			wantFailures: field.ErrorList{field.Required(field.NewPath("filters").Index(0), "exactly one structured model field must be set")},
   191  			parameters: func() *resource.ResourceClassParameters {
   192  				parameters := testResourceClassParameters(goodName, goodName, goodFilters)
   193  				parameters.Filters = []resource.ResourceFilter{{DriverName: goodName}}
   194  				return parameters
   195  			}(),
   196  		},
   197  
   198  		"filters-invalid-driver": {
   199  			wantFailures: field.ErrorList{field.Invalid(field.NewPath("filters").Index(1).Child("driverName"), badName, "a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')")},
   200  			parameters: func() *resource.ResourceClassParameters {
   201  				parameters := testResourceClassParameters(goodName, goodName, goodFilters)
   202  				parameters.Filters = []resource.ResourceFilter{
   203  					{
   204  						DriverName: goodName,
   205  						ResourceFilterModel: resource.ResourceFilterModel{
   206  							NamedResources: &resource.NamedResourcesFilter{Selector: "true"},
   207  						},
   208  					},
   209  					{
   210  						DriverName: badName,
   211  						ResourceFilterModel: resource.ResourceFilterModel{
   212  							NamedResources: &resource.NamedResourcesFilter{Selector: "true"},
   213  						},
   214  					},
   215  				}
   216  				return parameters
   217  			}(),
   218  		},
   219  
   220  		"filters-duplicate-driver": {
   221  			wantFailures: field.ErrorList{field.Duplicate(field.NewPath("filters").Index(1).Child("driverName"), goodName)},
   222  			parameters: func() *resource.ResourceClassParameters {
   223  				parameters := testResourceClassParameters(goodName, goodName, goodFilters)
   224  				parameters.Filters = []resource.ResourceFilter{
   225  					{
   226  						DriverName: goodName,
   227  						ResourceFilterModel: resource.ResourceFilterModel{
   228  							NamedResources: &resource.NamedResourcesFilter{Selector: "true"},
   229  						},
   230  					},
   231  					{
   232  						DriverName: goodName,
   233  						ResourceFilterModel: resource.ResourceFilterModel{
   234  							NamedResources: &resource.NamedResourcesFilter{Selector: "true"},
   235  						},
   236  					},
   237  				}
   238  				return parameters
   239  			}(),
   240  		},
   241  
   242  		"parameters-invalid-driver": {
   243  			wantFailures: field.ErrorList{field.Invalid(field.NewPath("vendorParameters").Index(1).Child("driverName"), badName, "a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')")},
   244  			parameters: func() *resource.ResourceClassParameters {
   245  				parameters := testResourceClassParameters(goodName, goodName, goodFilters)
   246  				parameters.VendorParameters = []resource.VendorParameters{
   247  					{
   248  						DriverName: goodName,
   249  					},
   250  					{
   251  						DriverName: badName,
   252  					},
   253  				}
   254  				return parameters
   255  			}(),
   256  		},
   257  
   258  		"parameters-duplicate-driver": {
   259  			wantFailures: field.ErrorList{field.Duplicate(field.NewPath("vendorParameters").Index(1).Child("driverName"), goodName)},
   260  			parameters: func() *resource.ResourceClassParameters {
   261  				parameters := testResourceClassParameters(goodName, goodName, goodFilters)
   262  				parameters.VendorParameters = []resource.VendorParameters{
   263  					{
   264  						DriverName: goodName,
   265  					},
   266  					{
   267  						DriverName: goodName,
   268  					},
   269  				}
   270  				return parameters
   271  			}(),
   272  		},
   273  	}
   274  
   275  	for name, scenario := range scenarios {
   276  		t.Run(name, func(t *testing.T) {
   277  			errs := ValidateResourceClassParameters(scenario.parameters)
   278  			assert.Equal(t, scenario.wantFailures, errs)
   279  		})
   280  	}
   281  }
   282  
   283  func TestValidateResourceClassParametersUpdate(t *testing.T) {
   284  	name := "valid"
   285  	validResourceClassParameters := testResourceClassParameters(name, name, nil)
   286  
   287  	scenarios := map[string]struct {
   288  		oldResourceClassParameters *resource.ResourceClassParameters
   289  		update                     func(class *resource.ResourceClassParameters) *resource.ResourceClassParameters
   290  		wantFailures               field.ErrorList
   291  	}{
   292  		"valid-no-op-update": {
   293  			oldResourceClassParameters: validResourceClassParameters,
   294  			update:                     func(class *resource.ResourceClassParameters) *resource.ResourceClassParameters { return class },
   295  		},
   296  		"invalid-name-update": {
   297  			oldResourceClassParameters: validResourceClassParameters,
   298  			update: func(class *resource.ResourceClassParameters) *resource.ResourceClassParameters {
   299  				class.Name += "-update"
   300  				return class
   301  			},
   302  			wantFailures: field.ErrorList{field.Invalid(field.NewPath("metadata", "name"), name+"-update", "field is immutable")},
   303  		},
   304  	}
   305  
   306  	for name, scenario := range scenarios {
   307  		t.Run(name, func(t *testing.T) {
   308  			scenario.oldResourceClassParameters.ResourceVersion = "1"
   309  			errs := ValidateResourceClassParametersUpdate(scenario.update(scenario.oldResourceClassParameters.DeepCopy()), scenario.oldResourceClassParameters)
   310  			assert.Equal(t, scenario.wantFailures, errs)
   311  		})
   312  	}
   313  }