k8s.io/kubernetes@v1.29.3/pkg/apis/resource/validation/validation_resourceclass_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/core"
    27  	"k8s.io/kubernetes/pkg/apis/resource"
    28  	"k8s.io/utils/pointer"
    29  )
    30  
    31  func testClass(name, driverName string) *resource.ResourceClass {
    32  	return &resource.ResourceClass{
    33  		ObjectMeta: metav1.ObjectMeta{
    34  			Name: name,
    35  		},
    36  		DriverName: driverName,
    37  	}
    38  }
    39  
    40  func TestValidateClass(t *testing.T) {
    41  	goodName := "foo"
    42  	now := metav1.Now()
    43  	goodParameters := resource.ResourceClassParametersReference{
    44  		Name:      "valid",
    45  		Namespace: "valid",
    46  		Kind:      "foo",
    47  	}
    48  	badName := "!@#$%^"
    49  	badValue := "spaces not allowed"
    50  
    51  	scenarios := map[string]struct {
    52  		class        *resource.ResourceClass
    53  		wantFailures field.ErrorList
    54  	}{
    55  		"good-class": {
    56  			class: testClass(goodName, goodName),
    57  		},
    58  		"good-long-driver-name": {
    59  			class: testClass(goodName, "acme.example.com"),
    60  		},
    61  		"missing-name": {
    62  			wantFailures: field.ErrorList{field.Required(field.NewPath("metadata", "name"), "name or generateName is required")},
    63  			class:        testClass("", goodName),
    64  		},
    65  		"bad-name": {
    66  			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])?)*')")},
    67  			class:        testClass(badName, goodName),
    68  		},
    69  		"generate-name": {
    70  			class: func() *resource.ResourceClass {
    71  				class := testClass(goodName, goodName)
    72  				class.GenerateName = "pvc-"
    73  				return class
    74  			}(),
    75  		},
    76  		"uid": {
    77  			class: func() *resource.ResourceClass {
    78  				class := testClass(goodName, goodName)
    79  				class.UID = "ac051fac-2ead-46d9-b8b4-4e0fbeb7455d"
    80  				return class
    81  			}(),
    82  		},
    83  		"resource-version": {
    84  			class: func() *resource.ResourceClass {
    85  				class := testClass(goodName, goodName)
    86  				class.ResourceVersion = "1"
    87  				return class
    88  			}(),
    89  		},
    90  		"generation": {
    91  			class: func() *resource.ResourceClass {
    92  				class := testClass(goodName, goodName)
    93  				class.Generation = 100
    94  				return class
    95  			}(),
    96  		},
    97  		"creation-timestamp": {
    98  			class: func() *resource.ResourceClass {
    99  				class := testClass(goodName, goodName)
   100  				class.CreationTimestamp = now
   101  				return class
   102  			}(),
   103  		},
   104  		"deletion-grace-period-seconds": {
   105  			class: func() *resource.ResourceClass {
   106  				class := testClass(goodName, goodName)
   107  				class.DeletionGracePeriodSeconds = pointer.Int64(10)
   108  				return class
   109  			}(),
   110  		},
   111  		"owner-references": {
   112  			class: func() *resource.ResourceClass {
   113  				class := testClass(goodName, goodName)
   114  				class.OwnerReferences = []metav1.OwnerReference{
   115  					{
   116  						APIVersion: "v1",
   117  						Kind:       "pod",
   118  						Name:       "foo",
   119  						UID:        "ac051fac-2ead-46d9-b8b4-4e0fbeb7455d",
   120  					},
   121  				}
   122  				return class
   123  			}(),
   124  		},
   125  		"finalizers": {
   126  			class: func() *resource.ResourceClass {
   127  				class := testClass(goodName, goodName)
   128  				class.Finalizers = []string{
   129  					"example.com/foo",
   130  				}
   131  				return class
   132  			}(),
   133  		},
   134  		"managed-fields": {
   135  			class: func() *resource.ResourceClass {
   136  				class := testClass(goodName, goodName)
   137  				class.ManagedFields = []metav1.ManagedFieldsEntry{
   138  					{
   139  						FieldsType: "FieldsV1",
   140  						Operation:  "Apply",
   141  						APIVersion: "apps/v1",
   142  						Manager:    "foo",
   143  					},
   144  				}
   145  				return class
   146  			}(),
   147  		},
   148  		"good-labels": {
   149  			class: func() *resource.ResourceClass {
   150  				class := testClass(goodName, goodName)
   151  				class.Labels = map[string]string{
   152  					"apps.kubernetes.io/name": "test",
   153  				}
   154  				return class
   155  			}(),
   156  		},
   157  		"bad-labels": {
   158  			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])?')")},
   159  			class: func() *resource.ResourceClass {
   160  				class := testClass(goodName, goodName)
   161  				class.Labels = map[string]string{
   162  					"hello-world": badValue,
   163  				}
   164  				return class
   165  			}(),
   166  		},
   167  		"good-annotations": {
   168  			class: func() *resource.ResourceClass {
   169  				class := testClass(goodName, goodName)
   170  				class.Annotations = map[string]string{
   171  					"foo": "bar",
   172  				}
   173  				return class
   174  			}(),
   175  		},
   176  		"bad-annotations": {
   177  			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]')")},
   178  			class: func() *resource.ResourceClass {
   179  				class := testClass(goodName, goodName)
   180  				class.Annotations = map[string]string{
   181  					badName: "hello world",
   182  				}
   183  				return class
   184  			}(),
   185  		},
   186  		"missing-driver-name": {
   187  			wantFailures: field.ErrorList{field.Required(field.NewPath("driverName"), ""),
   188  				field.Invalid(field.NewPath("driverName"), "", "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])?)*')"),
   189  			},
   190  			class: testClass(goodName, ""),
   191  		},
   192  		"invalid-driver-name": {
   193  			wantFailures: field.ErrorList{field.Invalid(field.NewPath("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])?)*')")},
   194  			class:        testClass(goodName, badName),
   195  		},
   196  		"invalid-qualified-driver-name": {
   197  			wantFailures: field.ErrorList{field.Invalid(field.NewPath("driverName"), goodName+"/path", "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])?)*')")},
   198  			class:        testClass(goodName, goodName+"/path"),
   199  		},
   200  		"good-parameters": {
   201  			class: func() *resource.ResourceClass {
   202  				class := testClass(goodName, goodName)
   203  				class.ParametersRef = goodParameters.DeepCopy()
   204  				return class
   205  			}(),
   206  		},
   207  		"missing-parameters-name": {
   208  			wantFailures: field.ErrorList{field.Required(field.NewPath("parametersRef", "name"), "")},
   209  			class: func() *resource.ResourceClass {
   210  				class := testClass(goodName, goodName)
   211  				class.ParametersRef = goodParameters.DeepCopy()
   212  				class.ParametersRef.Name = ""
   213  				return class
   214  			}(),
   215  		},
   216  		"bad-parameters-namespace": {
   217  			wantFailures: field.ErrorList{field.Invalid(field.NewPath("parametersRef", "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])?')")},
   218  			class: func() *resource.ResourceClass {
   219  				class := testClass(goodName, goodName)
   220  				class.ParametersRef = goodParameters.DeepCopy()
   221  				class.ParametersRef.Namespace = badName
   222  				return class
   223  			}(),
   224  		},
   225  		"missing-parameters-kind": {
   226  			wantFailures: field.ErrorList{field.Required(field.NewPath("parametersRef", "kind"), "")},
   227  			class: func() *resource.ResourceClass {
   228  				class := testClass(goodName, goodName)
   229  				class.ParametersRef = goodParameters.DeepCopy()
   230  				class.ParametersRef.Kind = ""
   231  				return class
   232  			}(),
   233  		},
   234  		"invalid-node-selector": {
   235  			wantFailures: field.ErrorList{field.Required(field.NewPath("suitableNodes", "nodeSelectorTerms"), "must have at least one node selector term")},
   236  			class: func() *resource.ResourceClass {
   237  				class := testClass(goodName, goodName)
   238  				class.SuitableNodes = &core.NodeSelector{
   239  					// Must not be empty.
   240  				}
   241  				return class
   242  			}(),
   243  		},
   244  	}
   245  
   246  	for name, scenario := range scenarios {
   247  		t.Run(name, func(t *testing.T) {
   248  			errs := ValidateClass(scenario.class)
   249  			assert.Equal(t, scenario.wantFailures, errs)
   250  		})
   251  	}
   252  }
   253  
   254  func TestValidateClassUpdate(t *testing.T) {
   255  	validClass := testClass("foo", "valid")
   256  
   257  	scenarios := map[string]struct {
   258  		oldClass     *resource.ResourceClass
   259  		update       func(class *resource.ResourceClass) *resource.ResourceClass
   260  		wantFailures field.ErrorList
   261  	}{
   262  		"valid-no-op-update": {
   263  			oldClass: validClass,
   264  			update:   func(class *resource.ResourceClass) *resource.ResourceClass { return class },
   265  		},
   266  		"update-driver": {
   267  			oldClass: validClass,
   268  			update: func(class *resource.ResourceClass) *resource.ResourceClass {
   269  				class.DriverName += "2"
   270  				return class
   271  			},
   272  		},
   273  	}
   274  
   275  	for name, scenario := range scenarios {
   276  		t.Run(name, func(t *testing.T) {
   277  			scenario.oldClass.ResourceVersion = "1"
   278  			errs := ValidateClassUpdate(scenario.update(scenario.oldClass.DeepCopy()), scenario.oldClass)
   279  			assert.Equal(t, scenario.wantFailures, errs)
   280  		})
   281  	}
   282  }