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  }