k8s.io/kubernetes@v1.29.3/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 "invalid-allocation-resourceHandles": { 384 wantFailures: field.ErrorList{field.TooLongMaxLength(field.NewPath("status", "allocation", "resourceHandles"), resource.AllocationResultResourceHandlesMaxSize+1, resource.AllocationResultResourceHandlesMaxSize)}, 385 oldClaim: validClaim, 386 update: func(claim *resource.ResourceClaim) *resource.ResourceClaim { 387 claim.Status.DriverName = "valid" 388 claim.Status.Allocation = &resource.AllocationResult{ 389 ResourceHandles: func() []resource.ResourceHandle { 390 var handles []resource.ResourceHandle 391 for i := 0; i < resource.AllocationResultResourceHandlesMaxSize+1; i++ { 392 handles = append(handles, resource.ResourceHandle{DriverName: "valid"}) 393 } 394 return handles 395 }(), 396 } 397 return claim 398 }, 399 }, 400 "invalid-allocation-resource-handle-drivername": { 401 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])?)*')")}, 402 oldClaim: validClaim, 403 update: func(claim *resource.ResourceClaim) *resource.ResourceClaim { 404 claim.Status.DriverName = "valid" 405 claim.Status.Allocation = &resource.AllocationResult{ 406 ResourceHandles: []resource.ResourceHandle{ 407 { 408 DriverName: invalidName, 409 }, 410 }, 411 } 412 return claim 413 }, 414 }, 415 "invalid-allocation-resource-handle-data": { 416 wantFailures: field.ErrorList{field.TooLongMaxLength(field.NewPath("status", "allocation", "resourceHandles[0]", "data"), resource.ResourceHandleDataMaxSize+1, resource.ResourceHandleDataMaxSize)}, 417 oldClaim: validClaim, 418 update: func(claim *resource.ResourceClaim) *resource.ResourceClaim { 419 claim.Status.DriverName = "valid" 420 claim.Status.Allocation = &resource.AllocationResult{ 421 ResourceHandles: []resource.ResourceHandle{ 422 { 423 DriverName: "valid", 424 Data: strings.Repeat(" ", resource.ResourceHandleDataMaxSize+1), 425 }, 426 }, 427 } 428 return claim 429 }, 430 }, 431 "invalid-node-selector": { 432 wantFailures: field.ErrorList{field.Required(field.NewPath("status", "allocation", "availableOnNodes", "nodeSelectorTerms"), "must have at least one node selector term")}, 433 oldClaim: validClaim, 434 update: func(claim *resource.ResourceClaim) *resource.ResourceClaim { 435 claim.Status.DriverName = "valid" 436 claim.Status.Allocation = &resource.AllocationResult{ 437 AvailableOnNodes: &core.NodeSelector{ 438 // Must not be empty. 439 }, 440 } 441 return claim 442 }, 443 }, 444 "add-reservation": { 445 oldClaim: validAllocatedClaim, 446 update: func(claim *resource.ResourceClaim) *resource.ResourceClaim { 447 for i := 0; i < resource.ResourceClaimReservedForMaxSize; i++ { 448 claim.Status.ReservedFor = append(claim.Status.ReservedFor, 449 resource.ResourceClaimConsumerReference{ 450 Resource: "pods", 451 Name: fmt.Sprintf("foo-%d", i), 452 UID: types.UID(fmt.Sprintf("%d", i)), 453 }) 454 } 455 return claim 456 }, 457 }, 458 "add-reservation-and-allocation": { 459 oldClaim: validClaim, 460 update: func(claim *resource.ResourceClaim) *resource.ResourceClaim { 461 claim.Status = *validAllocatedClaim.Status.DeepCopy() 462 for i := 0; i < resource.ResourceClaimReservedForMaxSize; i++ { 463 claim.Status.ReservedFor = append(claim.Status.ReservedFor, 464 resource.ResourceClaimConsumerReference{ 465 Resource: "pods", 466 Name: fmt.Sprintf("foo-%d", i), 467 UID: types.UID(fmt.Sprintf("%d", i)), 468 }) 469 } 470 return claim 471 }, 472 }, 473 "invalid-reserved-for-too-large": { 474 wantFailures: field.ErrorList{field.TooLongMaxLength(field.NewPath("status", "reservedFor"), resource.ResourceClaimReservedForMaxSize+1, resource.ResourceClaimReservedForMaxSize)}, 475 oldClaim: validAllocatedClaim, 476 update: func(claim *resource.ResourceClaim) *resource.ResourceClaim { 477 for i := 0; i < resource.ResourceClaimReservedForMaxSize+1; i++ { 478 claim.Status.ReservedFor = append(claim.Status.ReservedFor, 479 resource.ResourceClaimConsumerReference{ 480 Resource: "pods", 481 Name: fmt.Sprintf("foo-%d", i), 482 UID: types.UID(fmt.Sprintf("%d", i)), 483 }) 484 } 485 return claim 486 }, 487 }, 488 "invalid-reserved-for-duplicate": { 489 wantFailures: field.ErrorList{field.Duplicate(field.NewPath("status", "reservedFor").Index(1).Child("uid"), types.UID("1"))}, 490 oldClaim: validAllocatedClaim, 491 update: func(claim *resource.ResourceClaim) *resource.ResourceClaim { 492 for i := 0; i < 2; i++ { 493 claim.Status.ReservedFor = append(claim.Status.ReservedFor, 494 resource.ResourceClaimConsumerReference{ 495 Resource: "pods", 496 Name: "foo", 497 UID: "1", 498 }) 499 } 500 return claim 501 }, 502 }, 503 "invalid-reserved-for-not-shared": { 504 wantFailures: field.ErrorList{field.Forbidden(field.NewPath("status", "reservedFor"), "may not be reserved more than once")}, 505 oldClaim: func() *resource.ResourceClaim { 506 claim := validAllocatedClaim.DeepCopy() 507 claim.Status.Allocation.Shareable = false 508 return claim 509 }(), 510 update: func(claim *resource.ResourceClaim) *resource.ResourceClaim { 511 for i := 0; i < 2; i++ { 512 claim.Status.ReservedFor = append(claim.Status.ReservedFor, 513 resource.ResourceClaimConsumerReference{ 514 Resource: "pods", 515 Name: fmt.Sprintf("foo-%d", i), 516 UID: types.UID(fmt.Sprintf("%d", i)), 517 }) 518 } 519 return claim 520 }, 521 }, 522 "invalid-reserved-for-no-allocation": { 523 wantFailures: field.ErrorList{field.Forbidden(field.NewPath("status", "reservedFor"), "may not be specified when `allocated` is not set")}, 524 oldClaim: validClaim, 525 update: func(claim *resource.ResourceClaim) *resource.ResourceClaim { 526 claim.Status.DriverName = "valid" 527 claim.Status.ReservedFor = []resource.ResourceClaimConsumerReference{ 528 { 529 Resource: "pods", 530 Name: "foo", 531 UID: "1", 532 }, 533 } 534 return claim 535 }, 536 }, 537 "invalid-reserved-for-no-resource": { 538 wantFailures: field.ErrorList{field.Required(field.NewPath("status", "reservedFor").Index(0).Child("resource"), "")}, 539 oldClaim: validAllocatedClaim, 540 update: func(claim *resource.ResourceClaim) *resource.ResourceClaim { 541 claim.Status.ReservedFor = []resource.ResourceClaimConsumerReference{ 542 { 543 Name: "foo", 544 UID: "1", 545 }, 546 } 547 return claim 548 }, 549 }, 550 "invalid-reserved-for-no-name": { 551 wantFailures: field.ErrorList{field.Required(field.NewPath("status", "reservedFor").Index(0).Child("name"), "")}, 552 oldClaim: validAllocatedClaim, 553 update: func(claim *resource.ResourceClaim) *resource.ResourceClaim { 554 claim.Status.ReservedFor = []resource.ResourceClaimConsumerReference{ 555 { 556 Resource: "pods", 557 UID: "1", 558 }, 559 } 560 return claim 561 }, 562 }, 563 "invalid-reserved-for-no-uid": { 564 wantFailures: field.ErrorList{field.Required(field.NewPath("status", "reservedFor").Index(0).Child("uid"), "")}, 565 oldClaim: validAllocatedClaim, 566 update: func(claim *resource.ResourceClaim) *resource.ResourceClaim { 567 claim.Status.ReservedFor = []resource.ResourceClaimConsumerReference{ 568 { 569 Resource: "pods", 570 Name: "foo", 571 }, 572 } 573 return claim 574 }, 575 }, 576 "invalid-reserved-deleted": { 577 wantFailures: field.ErrorList{field.Forbidden(field.NewPath("status", "reservedFor"), "new entries may not be added while `deallocationRequested` or `deletionTimestamp` are set")}, 578 oldClaim: func() *resource.ResourceClaim { 579 claim := validAllocatedClaim.DeepCopy() 580 var deletionTimestamp metav1.Time 581 claim.DeletionTimestamp = &deletionTimestamp 582 return claim 583 }(), 584 update: func(claim *resource.ResourceClaim) *resource.ResourceClaim { 585 claim.Status.ReservedFor = []resource.ResourceClaimConsumerReference{ 586 { 587 Resource: "pods", 588 Name: "foo", 589 UID: "1", 590 }, 591 } 592 return claim 593 }, 594 }, 595 "invalid-reserved-deallocation-requested": { 596 wantFailures: field.ErrorList{field.Forbidden(field.NewPath("status", "reservedFor"), "new entries may not be added while `deallocationRequested` or `deletionTimestamp` are set")}, 597 oldClaim: func() *resource.ResourceClaim { 598 claim := validAllocatedClaim.DeepCopy() 599 claim.Status.DeallocationRequested = true 600 return claim 601 }(), 602 update: func(claim *resource.ResourceClaim) *resource.ResourceClaim { 603 claim.Status.ReservedFor = []resource.ResourceClaimConsumerReference{ 604 { 605 Resource: "pods", 606 Name: "foo", 607 UID: "1", 608 }, 609 } 610 return claim 611 }, 612 }, 613 "add-deallocation-requested": { 614 oldClaim: validAllocatedClaim, 615 update: func(claim *resource.ResourceClaim) *resource.ResourceClaim { 616 claim.Status.DeallocationRequested = true 617 return claim 618 }, 619 }, 620 "remove-allocation": { 621 oldClaim: func() *resource.ResourceClaim { 622 claim := validAllocatedClaim.DeepCopy() 623 claim.Status.DeallocationRequested = true 624 return claim 625 }(), 626 update: func(claim *resource.ResourceClaim) *resource.ResourceClaim { 627 claim.Status.DeallocationRequested = false 628 claim.Status.Allocation = nil 629 return claim 630 }, 631 }, 632 "invalid-deallocation-requested-removal": { 633 wantFailures: field.ErrorList{field.Forbidden(field.NewPath("status", "deallocationRequested"), "may not be cleared when `allocation` is set")}, 634 oldClaim: func() *resource.ResourceClaim { 635 claim := validAllocatedClaim.DeepCopy() 636 claim.Status.DeallocationRequested = true 637 return claim 638 }(), 639 update: func(claim *resource.ResourceClaim) *resource.ResourceClaim { 640 claim.Status.DeallocationRequested = false 641 return claim 642 }, 643 }, 644 "invalid-allocation-modification": { 645 wantFailures: field.ErrorList{field.Invalid(field.NewPath("status.allocation"), func() *resource.AllocationResult { 646 claim := validAllocatedClaim.DeepCopy() 647 claim.Status.Allocation.ResourceHandles = []resource.ResourceHandle{ 648 { 649 DriverName: "valid", 650 Data: strings.Repeat(" ", resource.ResourceHandleDataMaxSize/2), 651 }, 652 } 653 return claim.Status.Allocation 654 }(), "field is immutable")}, 655 oldClaim: func() *resource.ResourceClaim { 656 claim := validAllocatedClaim.DeepCopy() 657 claim.Status.DeallocationRequested = false 658 return claim 659 }(), 660 update: func(claim *resource.ResourceClaim) *resource.ResourceClaim { 661 claim.Status.Allocation.ResourceHandles = []resource.ResourceHandle{ 662 { 663 DriverName: "valid", 664 Data: strings.Repeat(" ", resource.ResourceHandleDataMaxSize/2), 665 }, 666 } 667 return claim 668 }, 669 }, 670 "invalid-deallocation-requested-in-use": { 671 wantFailures: field.ErrorList{field.Forbidden(field.NewPath("status", "deallocationRequested"), "deallocation cannot be requested while `reservedFor` is set")}, 672 oldClaim: func() *resource.ResourceClaim { 673 claim := validAllocatedClaim.DeepCopy() 674 claim.Status.ReservedFor = []resource.ResourceClaimConsumerReference{ 675 { 676 Resource: "pods", 677 Name: "foo", 678 UID: "1", 679 }, 680 } 681 return claim 682 }(), 683 update: func(claim *resource.ResourceClaim) *resource.ResourceClaim { 684 claim.Status.DeallocationRequested = true 685 return claim 686 }, 687 }, 688 "invalid-deallocation-not-allocated": { 689 wantFailures: field.ErrorList{field.Forbidden(field.NewPath("status"), "`allocation` must be set when `deallocationRequested` is set")}, 690 oldClaim: validClaim, 691 update: func(claim *resource.ResourceClaim) *resource.ResourceClaim { 692 claim.Status.DeallocationRequested = true 693 return claim 694 }, 695 }, 696 "invalid-allocation-removal-not-reset": { 697 wantFailures: field.ErrorList{field.Forbidden(field.NewPath("status"), "`allocation` must be set when `deallocationRequested` is set")}, 698 oldClaim: func() *resource.ResourceClaim { 699 claim := validAllocatedClaim.DeepCopy() 700 claim.Status.DeallocationRequested = true 701 return claim 702 }(), 703 update: func(claim *resource.ResourceClaim) *resource.ResourceClaim { 704 claim.Status.Allocation = nil 705 return claim 706 }, 707 }, 708 } 709 710 for name, scenario := range scenarios { 711 t.Run(name, func(t *testing.T) { 712 scenario.oldClaim.ResourceVersion = "1" 713 errs := ValidateClaimStatusUpdate(scenario.update(scenario.oldClaim.DeepCopy()), scenario.oldClaim) 714 assert.Equal(t, scenario.wantFailures, errs) 715 }) 716 } 717 }