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 }