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 }