From b10dce49c3cb782404e09f50547120a736c03969 Mon Sep 17 00:00:00 2001 From: Patrick Ohly Date: Thu, 26 Jan 2023 20:37:00 +0100 Subject: [PATCH] dynamic resource allocation: avoid apiserver complaint about list content This fixes the following warning (error?) in the apiserver: E0126 18:10:38.665239 16370 fieldmanager.go:210] "[SHOULD NOT HAPPEN] failed to update managedFields" err="failed to convert new object (test/claim-84; resource.k8s.io/v1alpha1, Kind=ResourceClaim) to smd typed: .status.reservedFor: element 0: associative list without keys has an element that's a map type" VersionKind="/, Kind=" namespace="test" name="claim-84" The root cause is the same as in e50e8a0c919c0e02dc9a0ffaebb685d5348027b4: nothing in Kubernetes outright complains about a list of items where the item type is comparable in Go, but not a simple type. This nonetheless isn't supposed to be done in the API and can causes problems elsewhere. For the ReservedFor field, everything seems to work okay except for the warning. However, it's better to follow conventions and use a map. This is possible in this case because UID is guaranteed to be a unique key. Validation is now stricter than before, which is a good thing: previously, two entries with the same UID were allowed as long as some other field was different, which wasn't a situation that should have been allowed. --- api/openapi-spec/swagger.json | 5 +++- ...is__resource.k8s.io__v1alpha1_openapi.json | 5 +++- pkg/apis/resource/validation/validation.go | 26 +++++++++++++++++-- .../validation_resourceclaim_test.go | 17 +++++------- pkg/generated/openapi/zz_generated.openapi.go | 5 +++- .../api/resource/v1alpha1/generated.proto | 3 ++- .../src/k8s.io/api/resource/v1alpha1/types.go | 3 ++- .../applyconfigurations/internal/internal.go | 2 ++ 8 files changed, 49 insertions(+), 17 deletions(-) diff --git a/api/openapi-spec/swagger.json b/api/openapi-spec/swagger.json index d4f4883e6a492..5d5054fc76326 100644 --- a/api/openapi-spec/swagger.json +++ b/api/openapi-spec/swagger.json @@ -13136,7 +13136,10 @@ "$ref": "#/definitions/io.k8s.api.resource.v1alpha1.ResourceClaimConsumerReference" }, "type": "array", - "x-kubernetes-list-type": "set" + "x-kubernetes-list-map-keys": [ + "uid" + ], + "x-kubernetes-list-type": "map" } }, "type": "object" diff --git a/api/openapi-spec/v3/apis__resource.k8s.io__v1alpha1_openapi.json b/api/openapi-spec/v3/apis__resource.k8s.io__v1alpha1_openapi.json index a615604dcca0e..6ea46ef9ad335 100644 --- a/api/openapi-spec/v3/apis__resource.k8s.io__v1alpha1_openapi.json +++ b/api/openapi-spec/v3/apis__resource.k8s.io__v1alpha1_openapi.json @@ -466,7 +466,10 @@ "default": {} }, "type": "array", - "x-kubernetes-list-type": "set" + "x-kubernetes-list-map-keys": [ + "uid" + ], + "x-kubernetes-list-type": "map" } }, "type": "object" diff --git a/pkg/apis/resource/validation/validation.go b/pkg/apis/resource/validation/validation.go index fcbf8c55c18ee..c82093d76bce9 100644 --- a/pkg/apis/resource/validation/validation.go +++ b/pkg/apis/resource/validation/validation.go @@ -18,6 +18,7 @@ package validation import ( apimachineryvalidation "k8s.io/apimachinery/pkg/api/validation" + "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/util/validation/field" corevalidation "k8s.io/kubernetes/pkg/apis/core/validation" @@ -131,8 +132,7 @@ func ValidateClaimStatusUpdate(resourceClaim, oldClaim *resource.ResourceClaim) } allErrs = append(allErrs, validateAllocationResult(resourceClaim.Status.Allocation, fldPath.Child("allocation"))...) - allErrs = append(allErrs, validateSliceIsASet(resourceClaim.Status.ReservedFor, resource.ResourceClaimReservedForMaxSize, - validateResourceClaimUserReference, fldPath.Child("reservedFor"))...) + allErrs = append(allErrs, validateResourceClaimConsumers(resourceClaim.Status.ReservedFor, resource.ResourceClaimReservedForMaxSize, fldPath.Child("reservedFor"))...) // Now check for invariants that must be valid for a ResourceClaim. if len(resourceClaim.Status.ReservedFor) > 0 { @@ -231,6 +231,28 @@ func validateSliceIsASet[T comparable](slice []T, maxSize int, validateItem func return allErrs } +// validateResourceClaimConsumers ensures that the slice contains no duplicate UIDs and does not exceed a certain maximum size. +func validateResourceClaimConsumers(consumers []resource.ResourceClaimConsumerReference, maxSize int, fldPath *field.Path) field.ErrorList { + var allErrs field.ErrorList + allUIDs := sets.New[types.UID]() + for i, consumer := range consumers { + idxPath := fldPath.Index(i) + if allUIDs.Has(consumer.UID) { + allErrs = append(allErrs, field.Duplicate(idxPath.Child("uid"), consumer.UID)) + } else { + allErrs = append(allErrs, validateResourceClaimUserReference(consumer, idxPath)...) + allUIDs.Insert(consumer.UID) + } + } + if len(consumers) > maxSize { + // Dumping the entire field into the error message is likely to be too long, + // in particular when it is already beyond the maximum size. Instead this + // just shows the number of entries. + allErrs = append(allErrs, field.TooLongMaxLength(fldPath, len(consumers), maxSize)) + } + return allErrs +} + // ValidatePodScheduling validates a PodScheduling. func ValidatePodScheduling(resourceClaim *resource.PodScheduling) field.ErrorList { allErrs := corevalidation.ValidateObjectMeta(&resourceClaim.ObjectMeta, true, corevalidation.ValidatePodName, field.NewPath("metadata")) diff --git a/pkg/apis/resource/validation/validation_resourceclaim_test.go b/pkg/apis/resource/validation/validation_resourceclaim_test.go index 4b3066fe3ca4c..48b817a9f6603 100644 --- a/pkg/apis/resource/validation/validation_resourceclaim_test.go +++ b/pkg/apis/resource/validation/validation_resourceclaim_test.go @@ -24,6 +24,7 @@ import ( "github.com/stretchr/testify/assert" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/validation/field" "k8s.io/kubernetes/pkg/apis/core" "k8s.io/kubernetes/pkg/apis/resource" @@ -395,7 +396,7 @@ func TestValidateClaimStatusUpdate(t *testing.T) { resource.ResourceClaimConsumerReference{ Resource: "pods", Name: fmt.Sprintf("foo-%d", i), - UID: "1", + UID: types.UID(fmt.Sprintf("%d", i)), }) } return claim @@ -410,7 +411,7 @@ func TestValidateClaimStatusUpdate(t *testing.T) { resource.ResourceClaimConsumerReference{ Resource: "pods", Name: fmt.Sprintf("foo-%d", i), - UID: "1", + UID: types.UID(fmt.Sprintf("%d", i)), }) } return claim @@ -425,19 +426,15 @@ func TestValidateClaimStatusUpdate(t *testing.T) { resource.ResourceClaimConsumerReference{ Resource: "pods", Name: fmt.Sprintf("foo-%d", i), - UID: "1", + UID: types.UID(fmt.Sprintf("%d", i)), }) } return claim }, }, "invalid-reserved-for-duplicate": { - wantFailures: field.ErrorList{field.Duplicate(field.NewPath("status", "reservedFor").Index(1), resource.ResourceClaimConsumerReference{ - Resource: "pods", - Name: "foo", - UID: "1", - })}, - oldClaim: validAllocatedClaim, + wantFailures: field.ErrorList{field.Duplicate(field.NewPath("status", "reservedFor").Index(1).Child("uid"), types.UID("1"))}, + oldClaim: validAllocatedClaim, update: func(claim *resource.ResourceClaim) *resource.ResourceClaim { for i := 0; i < 2; i++ { claim.Status.ReservedFor = append(claim.Status.ReservedFor, @@ -463,7 +460,7 @@ func TestValidateClaimStatusUpdate(t *testing.T) { resource.ResourceClaimConsumerReference{ Resource: "pods", Name: fmt.Sprintf("foo-%d", i), - UID: "1", + UID: types.UID(fmt.Sprintf("%d", i)), }) } return claim diff --git a/pkg/generated/openapi/zz_generated.openapi.go b/pkg/generated/openapi/zz_generated.openapi.go index 9a00db9a2cd8e..7205a3189ffaf 100644 --- a/pkg/generated/openapi/zz_generated.openapi.go +++ b/pkg/generated/openapi/zz_generated.openapi.go @@ -41924,7 +41924,10 @@ func schema_k8sio_api_resource_v1alpha1_ResourceClaimStatus(ref common.Reference "reservedFor": { VendorExtensible: spec.VendorExtensible{ Extensions: spec.Extensions{ - "x-kubernetes-list-type": "set", + "x-kubernetes-list-map-keys": []interface{}{ + "uid", + }, + "x-kubernetes-list-type": "map", }, }, SchemaProps: spec.SchemaProps{ diff --git a/staging/src/k8s.io/api/resource/v1alpha1/generated.proto b/staging/src/k8s.io/api/resource/v1alpha1/generated.proto index 5fc35e405ce78..2e814d155b3de 100644 --- a/staging/src/k8s.io/api/resource/v1alpha1/generated.proto +++ b/staging/src/k8s.io/api/resource/v1alpha1/generated.proto @@ -248,7 +248,8 @@ message ResourceClaimStatus { // There can be at most 32 such reservations. This may get increased in // the future, but not reduced. // - // +listType=set + // +listType=map + // +listMapKey=uid // +optional repeated ResourceClaimConsumerReference reservedFor = 3; diff --git a/staging/src/k8s.io/api/resource/v1alpha1/types.go b/staging/src/k8s.io/api/resource/v1alpha1/types.go index 9d7d4a191af86..af570384039af 100644 --- a/staging/src/k8s.io/api/resource/v1alpha1/types.go +++ b/staging/src/k8s.io/api/resource/v1alpha1/types.go @@ -112,7 +112,8 @@ type ResourceClaimStatus struct { // There can be at most 32 such reservations. This may get increased in // the future, but not reduced. // - // +listType=set + // +listType=map + // +listMapKey=uid // +optional ReservedFor []ResourceClaimConsumerReference `json:"reservedFor,omitempty" protobuf:"bytes,3,opt,name=reservedFor"` diff --git a/staging/src/k8s.io/client-go/applyconfigurations/internal/internal.go b/staging/src/k8s.io/client-go/applyconfigurations/internal/internal.go index 4f3636b27da51..94dd2160d5751 100644 --- a/staging/src/k8s.io/client-go/applyconfigurations/internal/internal.go +++ b/staging/src/k8s.io/client-go/applyconfigurations/internal/internal.go @@ -11661,6 +11661,8 @@ var schemaYAML = typed.YAMLObject(`types: elementType: namedType: io.k8s.api.resource.v1alpha1.ResourceClaimConsumerReference elementRelationship: associative + keys: + - uid - name: io.k8s.api.resource.v1alpha1.ResourceClaimTemplate map: fields: