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  }