k8s.io/kubernetes@v1.31.0-alpha.0.0.20240520171757-56147500dadc/pkg/apis/resource/validation/validation_resourceclaim_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 "fmt" 21 "strings" 22 "testing" 23 24 "github.com/stretchr/testify/assert" 25 26 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 27 "k8s.io/apimachinery/pkg/types" 28 "k8s.io/apimachinery/pkg/util/validation/field" 29 "k8s.io/kubernetes/pkg/apis/core" 30 "k8s.io/kubernetes/pkg/apis/resource" 31 "k8s.io/utils/pointer" 32 ) 33 34 func testClaim(name, namespace string, spec resource.ResourceClaimSpec) *resource.ResourceClaim { 35 return &resource.ResourceClaim{ 36 ObjectMeta: metav1.ObjectMeta{ 37 Name: name, 38 Namespace: namespace, 39 }, 40 Spec: spec, 41 } 42 } 43 44 func TestValidateClaim(t *testing.T) { 45 validMode := resource.AllocationModeImmediate 46 invalidMode := resource.AllocationMode("invalid") 47 goodName := "foo" 48 badName := "!@#$%^" 49 goodNS := "ns" 50 goodClaimSpec := resource.ResourceClaimSpec{ 51 ResourceClassName: goodName, 52 AllocationMode: validMode, 53 } 54 now := metav1.Now() 55 badValue := "spaces not allowed" 56 57 scenarios := map[string]struct { 58 claim *resource.ResourceClaim 59 wantFailures field.ErrorList 60 }{ 61 "good-claim": { 62 claim: testClaim(goodName, goodNS, goodClaimSpec), 63 }, 64 "missing-name": { 65 wantFailures: field.ErrorList{field.Required(field.NewPath("metadata", "name"), "name or generateName is required")}, 66 claim: testClaim("", goodNS, goodClaimSpec), 67 }, 68 "bad-name": { 69 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])?)*')")}, 70 claim: testClaim(badName, goodNS, goodClaimSpec), 71 }, 72 "missing-namespace": { 73 wantFailures: field.ErrorList{field.Required(field.NewPath("metadata", "namespace"), "")}, 74 claim: testClaim(goodName, "", goodClaimSpec), 75 }, 76 "generate-name": { 77 claim: func() *resource.ResourceClaim { 78 claim := testClaim(goodName, goodNS, goodClaimSpec) 79 claim.GenerateName = "pvc-" 80 return claim 81 }(), 82 }, 83 "uid": { 84 claim: func() *resource.ResourceClaim { 85 claim := testClaim(goodName, goodNS, goodClaimSpec) 86 claim.UID = "ac051fac-2ead-46d9-b8b4-4e0fbeb7455d" 87 return claim 88 }(), 89 }, 90 "resource-version": { 91 claim: func() *resource.ResourceClaim { 92 claim := testClaim(goodName, goodNS, goodClaimSpec) 93 claim.ResourceVersion = "1" 94 return claim 95 }(), 96 }, 97 "generation": { 98 claim: func() *resource.ResourceClaim { 99 claim := testClaim(goodName, goodNS, goodClaimSpec) 100 claim.Generation = 100 101 return claim 102 }(), 103 }, 104 "creation-timestamp": { 105 claim: func() *resource.ResourceClaim { 106 claim := testClaim(goodName, goodNS, goodClaimSpec) 107 claim.CreationTimestamp = now 108 return claim 109 }(), 110 }, 111 "deletion-grace-period-seconds": { 112 claim: func() *resource.ResourceClaim { 113 claim := testClaim(goodName, goodNS, goodClaimSpec) 114 claim.DeletionGracePeriodSeconds = pointer.Int64(10) 115 return claim 116 }(), 117 }, 118 "owner-references": { 119 claim: func() *resource.ResourceClaim { 120 claim := testClaim(goodName, goodNS, goodClaimSpec) 121 claim.OwnerReferences = []metav1.OwnerReference{ 122 { 123 APIVersion: "v1", 124 Kind: "pod", 125 Name: "foo", 126 UID: "ac051fac-2ead-46d9-b8b4-4e0fbeb7455d", 127 }, 128 } 129 return claim 130 }(), 131 }, 132 "finalizers": { 133 claim: func() *resource.ResourceClaim { 134 claim := testClaim(goodName, goodNS, goodClaimSpec) 135 claim.Finalizers = []string{ 136 "example.com/foo", 137 } 138 return claim 139 }(), 140 }, 141 "managed-fields": { 142 claim: func() *resource.ResourceClaim { 143 claim := testClaim(goodName, goodNS, goodClaimSpec) 144 claim.ManagedFields = []metav1.ManagedFieldsEntry{ 145 { 146 FieldsType: "FieldsV1", 147 Operation: "Apply", 148 APIVersion: "apps/v1", 149 Manager: "foo", 150 }, 151 } 152 return claim 153 }(), 154 }, 155 "good-labels": { 156 claim: func() *resource.ResourceClaim { 157 claim := testClaim(goodName, goodNS, goodClaimSpec) 158 claim.Labels = map[string]string{ 159 "apps.kubernetes.io/name": "test", 160 } 161 return claim 162 }(), 163 }, 164 "bad-labels": { 165 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])?')")}, 166 claim: func() *resource.ResourceClaim { 167 claim := testClaim(goodName, goodNS, goodClaimSpec) 168 claim.Labels = map[string]string{ 169 "hello-world": badValue, 170 } 171 return claim 172 }(), 173 }, 174 "good-annotations": { 175 claim: func() *resource.ResourceClaim { 176 claim := testClaim(goodName, goodNS, goodClaimSpec) 177 claim.Annotations = map[string]string{ 178 "foo": "bar", 179 } 180 return claim 181 }(), 182 }, 183 "bad-annotations": { 184 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]')")}, 185 claim: func() *resource.ResourceClaim { 186 claim := testClaim(goodName, goodNS, goodClaimSpec) 187 claim.Annotations = map[string]string{ 188 badName: "hello world", 189 } 190 return claim 191 }(), 192 }, 193 "bad-classname": { 194 wantFailures: field.ErrorList{field.Invalid(field.NewPath("spec", "resourceClassName"), 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])?)*')")}, 195 claim: func() *resource.ResourceClaim { 196 claim := testClaim(goodName, goodNS, goodClaimSpec) 197 claim.Spec.ResourceClassName = badName 198 return claim 199 }(), 200 }, 201 "bad-mode": { 202 wantFailures: field.ErrorList{field.NotSupported(field.NewPath("spec", "allocationMode"), invalidMode, supportedAllocationModes.List())}, 203 claim: func() *resource.ResourceClaim { 204 claim := testClaim(goodName, goodNS, goodClaimSpec) 205 claim.Spec.AllocationMode = invalidMode 206 return claim 207 }(), 208 }, 209 "good-parameters": { 210 claim: func() *resource.ResourceClaim { 211 claim := testClaim(goodName, goodNS, goodClaimSpec) 212 claim.Spec.ParametersRef = &resource.ResourceClaimParametersReference{ 213 Kind: "foo", 214 Name: "bar", 215 } 216 return claim 217 }(), 218 }, 219 "missing-parameters-kind": { 220 wantFailures: field.ErrorList{field.Required(field.NewPath("spec", "parametersRef", "kind"), "")}, 221 claim: func() *resource.ResourceClaim { 222 claim := testClaim(goodName, goodNS, goodClaimSpec) 223 claim.Spec.ParametersRef = &resource.ResourceClaimParametersReference{ 224 Name: "bar", 225 } 226 return claim 227 }(), 228 }, 229 "missing-parameters-name": { 230 wantFailures: field.ErrorList{field.Required(field.NewPath("spec", "parametersRef", "name"), "")}, 231 claim: func() *resource.ResourceClaim { 232 claim := testClaim(goodName, goodNS, goodClaimSpec) 233 claim.Spec.ParametersRef = &resource.ResourceClaimParametersReference{ 234 Kind: "foo", 235 } 236 return claim 237 }(), 238 }, 239 } 240 241 for name, scenario := range scenarios { 242 t.Run(name, func(t *testing.T) { 243 errs := ValidateClaim(scenario.claim) 244 assert.Equal(t, scenario.wantFailures, errs) 245 }) 246 } 247 } 248 249 func TestValidateClaimUpdate(t *testing.T) { 250 name := "valid" 251 parameters := &resource.ResourceClaimParametersReference{ 252 Kind: "foo", 253 Name: "bar", 254 } 255 validClaim := testClaim("foo", "ns", resource.ResourceClaimSpec{ 256 ResourceClassName: name, 257 AllocationMode: resource.AllocationModeImmediate, 258 ParametersRef: parameters, 259 }) 260 261 scenarios := map[string]struct { 262 oldClaim *resource.ResourceClaim 263 update func(claim *resource.ResourceClaim) *resource.ResourceClaim 264 wantFailures field.ErrorList 265 }{ 266 "valid-no-op-update": { 267 oldClaim: validClaim, 268 update: func(claim *resource.ResourceClaim) *resource.ResourceClaim { return claim }, 269 }, 270 "invalid-update-class": { 271 wantFailures: field.ErrorList{field.Invalid(field.NewPath("spec"), func() resource.ResourceClaimSpec { 272 spec := validClaim.Spec.DeepCopy() 273 spec.ResourceClassName += "2" 274 return *spec 275 }(), "field is immutable")}, 276 oldClaim: validClaim, 277 update: func(claim *resource.ResourceClaim) *resource.ResourceClaim { 278 claim.Spec.ResourceClassName += "2" 279 return claim 280 }, 281 }, 282 "invalid-update-remove-parameters": { 283 wantFailures: field.ErrorList{field.Invalid(field.NewPath("spec"), func() resource.ResourceClaimSpec { 284 spec := validClaim.Spec.DeepCopy() 285 spec.ParametersRef = nil 286 return *spec 287 }(), "field is immutable")}, 288 oldClaim: validClaim, 289 update: func(claim *resource.ResourceClaim) *resource.ResourceClaim { 290 claim.Spec.ParametersRef = nil 291 return claim 292 }, 293 }, 294 "invalid-update-mode": { 295 wantFailures: field.ErrorList{field.Invalid(field.NewPath("spec"), func() resource.ResourceClaimSpec { 296 spec := validClaim.Spec.DeepCopy() 297 spec.AllocationMode = resource.AllocationModeWaitForFirstConsumer 298 return *spec 299 }(), "field is immutable")}, 300 oldClaim: validClaim, 301 update: func(claim *resource.ResourceClaim) *resource.ResourceClaim { 302 claim.Spec.AllocationMode = resource.AllocationModeWaitForFirstConsumer 303 return claim 304 }, 305 }, 306 } 307 308 for name, scenario := range scenarios { 309 t.Run(name, func(t *testing.T) { 310 scenario.oldClaim.ResourceVersion = "1" 311 errs := ValidateClaimUpdate(scenario.update(scenario.oldClaim.DeepCopy()), scenario.oldClaim) 312 assert.Equal(t, scenario.wantFailures, errs) 313 }) 314 } 315 } 316 317 func TestValidateClaimStatusUpdate(t *testing.T) { 318 invalidName := "!@#$%^" 319 validClaim := testClaim("foo", "ns", resource.ResourceClaimSpec{ 320 ResourceClassName: "valid", 321 AllocationMode: resource.AllocationModeImmediate, 322 }) 323 324 validAllocatedClaim := validClaim.DeepCopy() 325 validAllocatedClaim.Status = resource.ResourceClaimStatus{ 326 DriverName: "valid", 327 Allocation: &resource.AllocationResult{ 328 ResourceHandles: func() []resource.ResourceHandle { 329 var handles []resource.ResourceHandle 330 for i := 0; i < resource.AllocationResultResourceHandlesMaxSize; i++ { 331 handle := resource.ResourceHandle{ 332 DriverName: "valid", 333 Data: strings.Repeat(" ", resource.ResourceHandleDataMaxSize), 334 } 335 handles = append(handles, handle) 336 } 337 return handles 338 }(), 339 Shareable: true, 340 }, 341 } 342 343 scenarios := map[string]struct { 344 oldClaim *resource.ResourceClaim 345 update func(claim *resource.ResourceClaim) *resource.ResourceClaim 346 wantFailures field.ErrorList 347 }{ 348 "valid-no-op-update": { 349 oldClaim: validClaim, 350 update: func(claim *resource.ResourceClaim) *resource.ResourceClaim { return claim }, 351 }, 352 "add-driver": { 353 oldClaim: validClaim, 354 update: func(claim *resource.ResourceClaim) *resource.ResourceClaim { 355 claim.Status.DriverName = "valid" 356 return claim 357 }, 358 }, 359 "invalid-add-allocation": { 360 wantFailures: field.ErrorList{field.Required(field.NewPath("status", "driverName"), "must be specified when `allocation` is set")}, 361 oldClaim: validClaim, 362 update: func(claim *resource.ResourceClaim) *resource.ResourceClaim { 363 // DriverName must also get set here! 364 claim.Status.Allocation = &resource.AllocationResult{} 365 return claim 366 }, 367 }, 368 "valid-add-allocation": { 369 oldClaim: validClaim, 370 update: func(claim *resource.ResourceClaim) *resource.ResourceClaim { 371 claim.Status.DriverName = "valid" 372 claim.Status.Allocation = &resource.AllocationResult{ 373 ResourceHandles: []resource.ResourceHandle{ 374 { 375 DriverName: "valid", 376 Data: strings.Repeat(" ", resource.ResourceHandleDataMaxSize), 377 }, 378 }, 379 } 380 return claim 381 }, 382 }, 383 "valid-add-empty-allocation-structured": { 384 oldClaim: validClaim, 385 update: func(claim *resource.ResourceClaim) *resource.ResourceClaim { 386 claim.Status.DriverName = "valid" 387 claim.Status.Allocation = &resource.AllocationResult{ 388 ResourceHandles: []resource.ResourceHandle{ 389 { 390 DriverName: "valid", 391 StructuredData: &resource.StructuredResourceHandle{}, 392 }, 393 }, 394 } 395 return claim 396 }, 397 }, 398 "valid-add-allocation-structured": { 399 oldClaim: validClaim, 400 update: func(claim *resource.ResourceClaim) *resource.ResourceClaim { 401 claim.Status.DriverName = "valid" 402 claim.Status.Allocation = &resource.AllocationResult{ 403 ResourceHandles: []resource.ResourceHandle{ 404 { 405 DriverName: "valid", 406 StructuredData: &resource.StructuredResourceHandle{ 407 NodeName: "worker", 408 }, 409 }, 410 }, 411 } 412 return claim 413 }, 414 }, 415 "invalid-add-allocation-structured": { 416 wantFailures: field.ErrorList{ 417 field.Invalid(field.NewPath("status", "allocation", "resourceHandles").Index(0).Child("structuredData", "nodeName"), "&^!", "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])?)*')"), 418 field.Required(field.NewPath("status", "allocation", "resourceHandles").Index(0).Child("structuredData", "results").Index(1), "exactly one structured model field must be set"), 419 }, 420 oldClaim: validClaim, 421 update: func(claim *resource.ResourceClaim) *resource.ResourceClaim { 422 claim.Status.DriverName = "valid" 423 claim.Status.Allocation = &resource.AllocationResult{ 424 ResourceHandles: []resource.ResourceHandle{ 425 { 426 DriverName: "valid", 427 StructuredData: &resource.StructuredResourceHandle{ 428 NodeName: "&^!", 429 Results: []resource.DriverAllocationResult{ 430 { 431 AllocationResultModel: resource.AllocationResultModel{ 432 NamedResources: &resource.NamedResourcesAllocationResult{ 433 Name: "some-resource-instance", 434 }, 435 }, 436 }, 437 { 438 AllocationResultModel: resource.AllocationResultModel{}, // invalid 439 }, 440 }, 441 }, 442 }, 443 }, 444 } 445 return claim 446 }, 447 }, 448 "invalid-duplicated-data": { 449 wantFailures: field.ErrorList{field.Invalid(field.NewPath("status", "allocation", "resourceHandles").Index(0), nil, "data and structuredData are mutually exclusive")}, 450 oldClaim: validClaim, 451 update: func(claim *resource.ResourceClaim) *resource.ResourceClaim { 452 claim.Status.DriverName = "valid" 453 claim.Status.Allocation = &resource.AllocationResult{ 454 ResourceHandles: []resource.ResourceHandle{ 455 { 456 DriverName: "valid", 457 Data: "something", 458 StructuredData: &resource.StructuredResourceHandle{ 459 NodeName: "worker", 460 }, 461 }, 462 }, 463 } 464 return claim 465 }, 466 }, 467 "invalid-allocation-resourceHandles": { 468 wantFailures: field.ErrorList{field.TooLongMaxLength(field.NewPath("status", "allocation", "resourceHandles"), resource.AllocationResultResourceHandlesMaxSize+1, resource.AllocationResultResourceHandlesMaxSize)}, 469 oldClaim: validClaim, 470 update: func(claim *resource.ResourceClaim) *resource.ResourceClaim { 471 claim.Status.DriverName = "valid" 472 claim.Status.Allocation = &resource.AllocationResult{ 473 ResourceHandles: func() []resource.ResourceHandle { 474 var handles []resource.ResourceHandle 475 for i := 0; i < resource.AllocationResultResourceHandlesMaxSize+1; i++ { 476 handles = append(handles, resource.ResourceHandle{DriverName: "valid"}) 477 } 478 return handles 479 }(), 480 } 481 return claim 482 }, 483 }, 484 "invalid-allocation-resource-handle-drivername": { 485 wantFailures: field.ErrorList{field.Invalid(field.NewPath("status", "allocation", "resourceHandles[0]", "driverName"), invalidName, "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])?)*')")}, 486 oldClaim: validClaim, 487 update: func(claim *resource.ResourceClaim) *resource.ResourceClaim { 488 claim.Status.DriverName = "valid" 489 claim.Status.Allocation = &resource.AllocationResult{ 490 ResourceHandles: []resource.ResourceHandle{ 491 { 492 DriverName: invalidName, 493 }, 494 }, 495 } 496 return claim 497 }, 498 }, 499 "invalid-allocation-resource-handle-data": { 500 wantFailures: field.ErrorList{field.TooLongMaxLength(field.NewPath("status", "allocation", "resourceHandles").Index(0).Child("data"), resource.ResourceHandleDataMaxSize+1, resource.ResourceHandleDataMaxSize)}, 501 oldClaim: validClaim, 502 update: func(claim *resource.ResourceClaim) *resource.ResourceClaim { 503 claim.Status.DriverName = "valid" 504 claim.Status.Allocation = &resource.AllocationResult{ 505 ResourceHandles: []resource.ResourceHandle{ 506 { 507 DriverName: "valid", 508 Data: strings.Repeat(" ", resource.ResourceHandleDataMaxSize+1), 509 }, 510 }, 511 } 512 return claim 513 }, 514 }, 515 "invalid-node-selector": { 516 wantFailures: field.ErrorList{field.Required(field.NewPath("status", "allocation", "availableOnNodes", "nodeSelectorTerms"), "must have at least one node selector term")}, 517 oldClaim: validClaim, 518 update: func(claim *resource.ResourceClaim) *resource.ResourceClaim { 519 claim.Status.DriverName = "valid" 520 claim.Status.Allocation = &resource.AllocationResult{ 521 AvailableOnNodes: &core.NodeSelector{ 522 // Must not be empty. 523 }, 524 } 525 return claim 526 }, 527 }, 528 "add-reservation": { 529 oldClaim: validAllocatedClaim, 530 update: func(claim *resource.ResourceClaim) *resource.ResourceClaim { 531 for i := 0; i < resource.ResourceClaimReservedForMaxSize; i++ { 532 claim.Status.ReservedFor = append(claim.Status.ReservedFor, 533 resource.ResourceClaimConsumerReference{ 534 Resource: "pods", 535 Name: fmt.Sprintf("foo-%d", i), 536 UID: types.UID(fmt.Sprintf("%d", i)), 537 }) 538 } 539 return claim 540 }, 541 }, 542 "add-reservation-and-allocation": { 543 oldClaim: validClaim, 544 update: func(claim *resource.ResourceClaim) *resource.ResourceClaim { 545 claim.Status = *validAllocatedClaim.Status.DeepCopy() 546 for i := 0; i < resource.ResourceClaimReservedForMaxSize; i++ { 547 claim.Status.ReservedFor = append(claim.Status.ReservedFor, 548 resource.ResourceClaimConsumerReference{ 549 Resource: "pods", 550 Name: fmt.Sprintf("foo-%d", i), 551 UID: types.UID(fmt.Sprintf("%d", i)), 552 }) 553 } 554 return claim 555 }, 556 }, 557 "invalid-reserved-for-too-large": { 558 wantFailures: field.ErrorList{field.TooLongMaxLength(field.NewPath("status", "reservedFor"), resource.ResourceClaimReservedForMaxSize+1, resource.ResourceClaimReservedForMaxSize)}, 559 oldClaim: validAllocatedClaim, 560 update: func(claim *resource.ResourceClaim) *resource.ResourceClaim { 561 for i := 0; i < resource.ResourceClaimReservedForMaxSize+1; i++ { 562 claim.Status.ReservedFor = append(claim.Status.ReservedFor, 563 resource.ResourceClaimConsumerReference{ 564 Resource: "pods", 565 Name: fmt.Sprintf("foo-%d", i), 566 UID: types.UID(fmt.Sprintf("%d", i)), 567 }) 568 } 569 return claim 570 }, 571 }, 572 "invalid-reserved-for-duplicate": { 573 wantFailures: field.ErrorList{field.Duplicate(field.NewPath("status", "reservedFor").Index(1).Child("uid"), types.UID("1"))}, 574 oldClaim: validAllocatedClaim, 575 update: func(claim *resource.ResourceClaim) *resource.ResourceClaim { 576 for i := 0; i < 2; i++ { 577 claim.Status.ReservedFor = append(claim.Status.ReservedFor, 578 resource.ResourceClaimConsumerReference{ 579 Resource: "pods", 580 Name: "foo", 581 UID: "1", 582 }) 583 } 584 return claim 585 }, 586 }, 587 "invalid-reserved-for-not-shared": { 588 wantFailures: field.ErrorList{field.Forbidden(field.NewPath("status", "reservedFor"), "may not be reserved more than once")}, 589 oldClaim: func() *resource.ResourceClaim { 590 claim := validAllocatedClaim.DeepCopy() 591 claim.Status.Allocation.Shareable = false 592 return claim 593 }(), 594 update: func(claim *resource.ResourceClaim) *resource.ResourceClaim { 595 for i := 0; i < 2; i++ { 596 claim.Status.ReservedFor = append(claim.Status.ReservedFor, 597 resource.ResourceClaimConsumerReference{ 598 Resource: "pods", 599 Name: fmt.Sprintf("foo-%d", i), 600 UID: types.UID(fmt.Sprintf("%d", i)), 601 }) 602 } 603 return claim 604 }, 605 }, 606 "invalid-reserved-for-no-allocation": { 607 wantFailures: field.ErrorList{field.Forbidden(field.NewPath("status", "reservedFor"), "may not be specified when `allocated` is not set")}, 608 oldClaim: validClaim, 609 update: func(claim *resource.ResourceClaim) *resource.ResourceClaim { 610 claim.Status.DriverName = "valid" 611 claim.Status.ReservedFor = []resource.ResourceClaimConsumerReference{ 612 { 613 Resource: "pods", 614 Name: "foo", 615 UID: "1", 616 }, 617 } 618 return claim 619 }, 620 }, 621 "invalid-reserved-for-no-resource": { 622 wantFailures: field.ErrorList{field.Required(field.NewPath("status", "reservedFor").Index(0).Child("resource"), "")}, 623 oldClaim: validAllocatedClaim, 624 update: func(claim *resource.ResourceClaim) *resource.ResourceClaim { 625 claim.Status.ReservedFor = []resource.ResourceClaimConsumerReference{ 626 { 627 Name: "foo", 628 UID: "1", 629 }, 630 } 631 return claim 632 }, 633 }, 634 "invalid-reserved-for-no-name": { 635 wantFailures: field.ErrorList{field.Required(field.NewPath("status", "reservedFor").Index(0).Child("name"), "")}, 636 oldClaim: validAllocatedClaim, 637 update: func(claim *resource.ResourceClaim) *resource.ResourceClaim { 638 claim.Status.ReservedFor = []resource.ResourceClaimConsumerReference{ 639 { 640 Resource: "pods", 641 UID: "1", 642 }, 643 } 644 return claim 645 }, 646 }, 647 "invalid-reserved-for-no-uid": { 648 wantFailures: field.ErrorList{field.Required(field.NewPath("status", "reservedFor").Index(0).Child("uid"), "")}, 649 oldClaim: validAllocatedClaim, 650 update: func(claim *resource.ResourceClaim) *resource.ResourceClaim { 651 claim.Status.ReservedFor = []resource.ResourceClaimConsumerReference{ 652 { 653 Resource: "pods", 654 Name: "foo", 655 }, 656 } 657 return claim 658 }, 659 }, 660 "invalid-reserved-deleted": { 661 wantFailures: field.ErrorList{field.Forbidden(field.NewPath("status", "reservedFor"), "new entries may not be added while `deallocationRequested` or `deletionTimestamp` are set")}, 662 oldClaim: func() *resource.ResourceClaim { 663 claim := validAllocatedClaim.DeepCopy() 664 var deletionTimestamp metav1.Time 665 claim.DeletionTimestamp = &deletionTimestamp 666 return claim 667 }(), 668 update: func(claim *resource.ResourceClaim) *resource.ResourceClaim { 669 claim.Status.ReservedFor = []resource.ResourceClaimConsumerReference{ 670 { 671 Resource: "pods", 672 Name: "foo", 673 UID: "1", 674 }, 675 } 676 return claim 677 }, 678 }, 679 "invalid-reserved-deallocation-requested": { 680 wantFailures: field.ErrorList{field.Forbidden(field.NewPath("status", "reservedFor"), "new entries may not be added while `deallocationRequested` or `deletionTimestamp` are set")}, 681 oldClaim: func() *resource.ResourceClaim { 682 claim := validAllocatedClaim.DeepCopy() 683 claim.Status.DeallocationRequested = true 684 return claim 685 }(), 686 update: func(claim *resource.ResourceClaim) *resource.ResourceClaim { 687 claim.Status.ReservedFor = []resource.ResourceClaimConsumerReference{ 688 { 689 Resource: "pods", 690 Name: "foo", 691 UID: "1", 692 }, 693 } 694 return claim 695 }, 696 }, 697 "add-deallocation-requested": { 698 oldClaim: validAllocatedClaim, 699 update: func(claim *resource.ResourceClaim) *resource.ResourceClaim { 700 claim.Status.DeallocationRequested = true 701 return claim 702 }, 703 }, 704 "remove-allocation": { 705 oldClaim: func() *resource.ResourceClaim { 706 claim := validAllocatedClaim.DeepCopy() 707 claim.Status.DeallocationRequested = true 708 return claim 709 }(), 710 update: func(claim *resource.ResourceClaim) *resource.ResourceClaim { 711 claim.Status.DeallocationRequested = false 712 claim.Status.Allocation = nil 713 return claim 714 }, 715 }, 716 "invalid-deallocation-requested-removal": { 717 wantFailures: field.ErrorList{field.Forbidden(field.NewPath("status", "deallocationRequested"), "may not be cleared when `allocation` is set")}, 718 oldClaim: func() *resource.ResourceClaim { 719 claim := validAllocatedClaim.DeepCopy() 720 claim.Status.DeallocationRequested = true 721 return claim 722 }(), 723 update: func(claim *resource.ResourceClaim) *resource.ResourceClaim { 724 claim.Status.DeallocationRequested = false 725 return claim 726 }, 727 }, 728 "invalid-allocation-modification": { 729 wantFailures: field.ErrorList{field.Invalid(field.NewPath("status.allocation"), func() *resource.AllocationResult { 730 claim := validAllocatedClaim.DeepCopy() 731 claim.Status.Allocation.ResourceHandles = []resource.ResourceHandle{ 732 { 733 DriverName: "valid", 734 Data: strings.Repeat(" ", resource.ResourceHandleDataMaxSize/2), 735 }, 736 } 737 return claim.Status.Allocation 738 }(), "field is immutable")}, 739 oldClaim: func() *resource.ResourceClaim { 740 claim := validAllocatedClaim.DeepCopy() 741 claim.Status.DeallocationRequested = false 742 return claim 743 }(), 744 update: func(claim *resource.ResourceClaim) *resource.ResourceClaim { 745 claim.Status.Allocation.ResourceHandles = []resource.ResourceHandle{ 746 { 747 DriverName: "valid", 748 Data: strings.Repeat(" ", resource.ResourceHandleDataMaxSize/2), 749 }, 750 } 751 return claim 752 }, 753 }, 754 "invalid-deallocation-requested-in-use": { 755 wantFailures: field.ErrorList{field.Forbidden(field.NewPath("status", "deallocationRequested"), "deallocation cannot be requested while `reservedFor` is set")}, 756 oldClaim: func() *resource.ResourceClaim { 757 claim := validAllocatedClaim.DeepCopy() 758 claim.Status.ReservedFor = []resource.ResourceClaimConsumerReference{ 759 { 760 Resource: "pods", 761 Name: "foo", 762 UID: "1", 763 }, 764 } 765 return claim 766 }(), 767 update: func(claim *resource.ResourceClaim) *resource.ResourceClaim { 768 claim.Status.DeallocationRequested = true 769 return claim 770 }, 771 }, 772 "invalid-deallocation-not-allocated": { 773 wantFailures: field.ErrorList{field.Forbidden(field.NewPath("status"), "`allocation` must be set when `deallocationRequested` is set")}, 774 oldClaim: validClaim, 775 update: func(claim *resource.ResourceClaim) *resource.ResourceClaim { 776 claim.Status.DeallocationRequested = true 777 return claim 778 }, 779 }, 780 "invalid-allocation-removal-not-reset": { 781 wantFailures: field.ErrorList{field.Forbidden(field.NewPath("status"), "`allocation` must be set when `deallocationRequested` is set")}, 782 oldClaim: func() *resource.ResourceClaim { 783 claim := validAllocatedClaim.DeepCopy() 784 claim.Status.DeallocationRequested = true 785 return claim 786 }(), 787 update: func(claim *resource.ResourceClaim) *resource.ResourceClaim { 788 claim.Status.Allocation = nil 789 return claim 790 }, 791 }, 792 } 793 794 for name, scenario := range scenarios { 795 t.Run(name, func(t *testing.T) { 796 scenario.oldClaim.ResourceVersion = "1" 797 errs := ValidateClaimStatusUpdate(scenario.update(scenario.oldClaim.DeepCopy()), scenario.oldClaim) 798 assert.Equal(t, scenario.wantFailures, errs) 799 }) 800 } 801 }