From 697113f5f07124641df79bb6e6212b09ba29af58 Mon Sep 17 00:00:00 2001 From: Joe Betz Date: Tue, 16 Sep 2025 17:57:59 -0400 Subject: [PATCH 1/7] Add utility function to errors to allow format composition --- .../apimachinery/pkg/util/validation/field/errors.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/staging/src/k8s.io/apimachinery/pkg/util/validation/field/errors.go b/staging/src/k8s.io/apimachinery/pkg/util/validation/field/errors.go index f2a983aebf6..950d8386823 100644 --- a/staging/src/k8s.io/apimachinery/pkg/util/validation/field/errors.go +++ b/staging/src/k8s.io/apimachinery/pkg/util/validation/field/errors.go @@ -341,6 +341,14 @@ func (list ErrorList) MarkCoveredByDeclarative() ErrorList { return list } +// PrefixDetail adds a prefix to the Detail for all errors in the list and returns the updated list. +func (list ErrorList) PrefixDetail(prefix string) ErrorList { + for _, err := range list { + err.Detail = prefix + err.Detail + } + return list +} + // ToAggregate converts the ErrorList into an errors.Aggregate. func (list ErrorList) ToAggregate() utilerrors.Aggregate { if len(list) == 0 { From 83cf63581898d5958b12d13bb44854bb214b3fd9 Mon Sep 17 00:00:00 2001 From: Joe Betz Date: Tue, 16 Sep 2025 17:57:59 -0400 Subject: [PATCH 2/7] Add k8s-long-name-segments format # Conflicts: # staging/src/k8s.io/apimachinery/pkg/api/validate/strfmt_test.go # staging/src/k8s.io/code-generator/cmd/validation-gen/validators/format.go --- .../apimachinery/pkg/api/validate/strfmt.go | 25 +++++ .../pkg/api/validate/strfmt_test.go | 103 ++++++++++++++++++ .../cmd/validation-gen/validators/format.go | 6 + 3 files changed, 134 insertions(+) diff --git a/staging/src/k8s.io/apimachinery/pkg/api/validate/strfmt.go b/staging/src/k8s.io/apimachinery/pkg/api/validate/strfmt.go index 6ca7247de00..504afecc033 100644 --- a/staging/src/k8s.io/apimachinery/pkg/api/validate/strfmt.go +++ b/staging/src/k8s.io/apimachinery/pkg/api/validate/strfmt.go @@ -18,6 +18,8 @@ package validate import ( "context" + "fmt" + "strings" "k8s.io/apimachinery/pkg/api/operation" "k8s.io/apimachinery/pkg/api/validate/content" @@ -155,3 +157,26 @@ func UUID[T ~string](_ context.Context, op operation.Operation, fldPath *field.P } return nil } + +// ResourcePoolName verifies that the specified value is one or more valid "long name" +// parts separated by a '/' and no longer than 253 characters. +func ResourcePoolName[T ~string](ctx context.Context, op operation.Operation, fldPath *field.Path, value, _ *T) field.ErrorList { + if value == nil { + return nil + } + val := (string)(*value) + var allErrs field.ErrorList + if len(val) > 253 { + allErrs = append(allErrs, field.TooLong(fldPath, val, 253)) + } + parts := strings.Split(val, "/") + for i, part := range parts { + if len(part) == 0 { + allErrs = append(allErrs, field.Invalid(fldPath, val, fmt.Sprintf("segment %d: must not be empty", i))) + continue + } + // Note that we are overwriting the origin from the underlying LongName validation. + allErrs = append(allErrs, LongName(ctx, op, fldPath, &part, nil).PrefixDetail(fmt.Sprintf("segment %d: ", i))...) + } + return allErrs.WithOrigin("format=k8s-resource-pool-name") +} diff --git a/staging/src/k8s.io/apimachinery/pkg/api/validate/strfmt_test.go b/staging/src/k8s.io/apimachinery/pkg/api/validate/strfmt_test.go index 6fe7b70e326..fd89bf08b5e 100644 --- a/staging/src/k8s.io/apimachinery/pkg/api/validate/strfmt_test.go +++ b/staging/src/k8s.io/apimachinery/pkg/api/validate/strfmt_test.go @@ -469,3 +469,106 @@ func TestLongNameCaseless(t *testing.T) { }) } } + +func TestResourcePoolName(t *testing.T) { + ctx := context.Background() + fldPath := field.NewPath("test") + + testCases := []struct { + name string + input string + wantErrs field.ErrorList + }{{ + name: "valid: single segment", + input: "a.valid.long-name", + }, { + name: "valid: two segments", + input: "a.valid.long-name/another.one", + }, { + name: "valid: multiple segments", + input: "a/b/c.d.e", + }, { + name: "valid: segments with numbers", + input: "1.2.3/4.5.6", + }, { + name: "invalid: empty string", + input: "", + wantErrs: field.ErrorList{ + field.Invalid(fldPath, "", "segment 0: must not be empty").WithOrigin("format=k8s-resource-pool-name"), + }, + }, { + name: "invalid: leading slash", + input: "/a.b.c", + wantErrs: field.ErrorList{ + field.Invalid(fldPath, nil, "").WithOrigin("format=k8s-resource-pool-name"), + }, + }, { + name: "invalid: trailing slash", + input: "a.b.c/", + wantErrs: field.ErrorList{ + field.Invalid(fldPath, nil, "").WithOrigin("format=k8s-resource-pool-name"), + }, + }, { + name: "invalid: double slash", + input: "a.b.c//d.e.f", + wantErrs: field.ErrorList{ + field.Invalid(fldPath, nil, "").WithOrigin("format=k8s-resource-pool-name"), + }, + }, { + name: "invalid: one segment has uppercase", + input: "a.valid.name/Not.Valid", + wantErrs: field.ErrorList{ + field.Invalid(fldPath, nil, "segment 1: a lowercase RFC 1123").WithOrigin("format=k8s-resource-pool-name"), + }, + }, { + name: "invalid: one segment starts with dash", + input: "a.valid.name/-not-valid", + wantErrs: field.ErrorList{ + field.Invalid(fldPath, nil, "segment 1: a lowercase RFC 1123").WithOrigin("format=k8s-resource-pool-name"), + }, + }, { + name: "invalid: one segment has special characters", + input: "a.valid.name/not_valid", + wantErrs: field.ErrorList{ + field.Invalid(fldPath, nil, "segment 1: a lowercase RFC 1123").WithOrigin("format=k8s-resource-pool-name"), + }, + }, { + name: "invalid: too long", + input: "a.valid.name/" + strings.Repeat("b", 253), + wantErrs: field.ErrorList{ + field.TooLong(fldPath, nil, 253).WithOrigin("format=k8s-resource-pool-name"), + }, + }, { + name: "invalid: segment too long", + input: strings.Repeat("b", 254), + wantErrs: field.ErrorList{ + field.TooLong(fldPath, nil, 253).WithOrigin("format=k8s-resource-pool-name"), + field.Invalid(fldPath, nil, "segment 0: must be no more than 253 bytes").WithOrigin("format=k8s-resource-pool-name"), + }, + }, { + name: "invalid: multiple invalid segments", + input: "Not/Valid/Either", + wantErrs: field.ErrorList{ + field.Invalid(fldPath, nil, "segment 0: a lowercase RFC 1123").WithOrigin("format=k8s-resource-pool-name"), + field.Invalid(fldPath, nil, "segment 1: a lowercase RFC 1123").WithOrigin("format=k8s-resource-pool-name"), + field.Invalid(fldPath, nil, "segment 2: a lowercase RFC 1123").WithOrigin("format=k8s-resource-pool-name"), + }, + }, { + name: "invalid: just a slash", + input: "/", + wantErrs: field.ErrorList{ + field.Invalid(fldPath, nil, "segment 0: must not be empty").WithOrigin("format=k8s-resource-pool-name"), + field.Invalid(fldPath, nil, "segment 1: must not be empty").WithOrigin("format=k8s-resource-pool-name"), + }, + }} + + exactMatcher := field.ErrorMatcher{}.ByType().ByField().ByOrigin().ByDetailSubstring() + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + value := &tc.input + gotErrs := ResourcePoolName(ctx, operation.Operation{}, fldPath, value, nil) + exactMatcher.Test(t, tc.wantErrs, gotErrs) + }) + } +} diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/format.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/format.go index 4ceaf5e04a5..c73f9670a86 100644 --- a/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/format.go +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/format.go @@ -54,6 +54,7 @@ var ( labelValueValidator = types.Name{Package: libValidationPkg, Name: "LabelValue"} longNameCaselessValidator = types.Name{Package: libValidationPkg, Name: "LongNameCaseless"} longNameValidator = types.Name{Package: libValidationPkg, Name: "LongName"} + resourcePoolNameValidator = types.Name{Package: libValidationPkg, Name: "ResourcePoolName"} shortNameValidator = types.Name{Package: libValidationPkg, Name: "ShortName"} uuidValidator = types.Name{Package: libValidationPkg, Name: "UUID"} ) @@ -90,6 +91,8 @@ func getFormatValidationFunction(format string) (FunctionGen, error) { return Function(formatTagName, DefaultFlags, labelValueValidator), nil case "k8s-long-name": return Function(formatTagName, DefaultFlags, longNameValidator), nil + case "k8s-resource-pool-name": + return Function(formatTagName, DefaultFlags, resourcePoolNameValidator), nil case "k8s-long-name-caseless": return Function(formatTagName, DefaultFlags, longNameCaselessValidator), nil case "k8s-short-name": @@ -119,6 +122,9 @@ func (ftv formatTagValidator) Docs() TagDoc { }, { Description: "k8s-long-name", Docs: "This field holds a Kubernetes \"long name\", aka a \"DNS subdomain\" value.", + }, { + Description: "k8s-resource-pool-name", + Docs: "This field holds value with one or more Kubernetes \"long name\" parts separated by `/` and no longer than 253 characters.", }, { Description: "k8s-long-name-caseless", Docs: "Deprecated: This field holds a case-insensitive Kubernetes \"long name\", aka a \"DNS subdomain\" value.", From e8f243dac2a1d1d1755f6912360d3db50e6b4a2b Mon Sep 17 00:00:00 2001 From: Joe Betz Date: Tue, 16 Sep 2025 17:57:59 -0400 Subject: [PATCH 3/7] Add output tests --- .../tags/format/k8s-resource-pool-name/doc.go | 41 +++++++ .../format/k8s-resource-pool-name/doc_test.go | 80 ++++++++++++++ .../zz_generated.validations.go | 101 ++++++++++++++++++ 3 files changed, 222 insertions(+) create mode 100644 staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/format/k8s-resource-pool-name/doc.go create mode 100644 staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/format/k8s-resource-pool-name/doc_test.go create mode 100644 staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/format/k8s-resource-pool-name/zz_generated.validations.go diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/format/k8s-resource-pool-name/doc.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/format/k8s-resource-pool-name/doc.go new file mode 100644 index 00000000000..5da57980a4b --- /dev/null +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/format/k8s-resource-pool-name/doc.go @@ -0,0 +1,41 @@ +/* +Copyright 2024 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// +k8s:validation-gen=TypeMeta +// +k8s:validation-gen-scheme-registry=k8s.io/code-generator/cmd/validation-gen/testscheme.Scheme + +// This is a test package. +package format + +import "k8s.io/code-generator/cmd/validation-gen/testscheme" + +var localSchemeBuilder = testscheme.New() + +type Struct struct { + TypeMeta int + + // +k8s:format=k8s-resource-pool-name + ResourcePoolNameField string `json:"resourcePoolNameField"` + + // +k8s:format=k8s-resource-pool-name + ResourcePoolNamePtrField *string `json:"resourcePoolNamePtrField"` + + // Note: no validation here + ResourcePoolNameTypedefField ResourcePoolNameStringType `json:"resourcePoolNameTypedefField"` +} + +// +k8s:format=k8s-resource-pool-name +type ResourcePoolNameStringType string diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/format/k8s-resource-pool-name/doc_test.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/format/k8s-resource-pool-name/doc_test.go new file mode 100644 index 00000000000..eeab15e5306 --- /dev/null +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/format/k8s-resource-pool-name/doc_test.go @@ -0,0 +1,80 @@ +/* +Copyright 2024 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// +output_tests +package format + +import ( + "testing" + + "k8s.io/apimachinery/pkg/util/validation/field" + "k8s.io/utils/ptr" +) + +func Test(t *testing.T) { + st := localSchemeBuilder.Test(t) + + st.Value(&Struct{ + ResourcePoolNameField: "foo.bar", + ResourcePoolNamePtrField: ptr.To("foo.bar"), + ResourcePoolNameTypedefField: "foo.bar", + }).ExpectValid() + + st.Value(&Struct{ + ResourcePoolNameField: "1.2.3.4", + ResourcePoolNamePtrField: ptr.To("1.2.3.4"), + ResourcePoolNameTypedefField: "1.2.3.4", + }).ExpectValid() + + invalidStruct := &Struct{ + ResourcePoolNameField: "", + ResourcePoolNamePtrField: ptr.To(""), + ResourcePoolNameTypedefField: "", + } + st.Value(invalidStruct).ExpectMatches(field.ErrorMatcher{}.ByType().ByField().ByOrigin(), field.ErrorList{ + field.Invalid(field.NewPath("resourcePoolNameField"), nil, "").WithOrigin("format=k8s-resource-pool-name"), + field.Invalid(field.NewPath("resourcePoolNamePtrField"), nil, "").WithOrigin("format=k8s-resource-pool-name"), + field.Invalid(field.NewPath("resourcePoolNameTypedefField"), nil, "").WithOrigin("format=k8s-resource-pool-name"), + }) + // Test validation ratcheting + st.Value(invalidStruct).OldValue(invalidStruct).ExpectValid() + + invalidStruct = &Struct{ + ResourcePoolNameField: "Not a ResourcePoolName", + ResourcePoolNamePtrField: ptr.To("Not a ResourcePoolName"), + ResourcePoolNameTypedefField: "Not a ResourcePoolName", + } + st.Value(invalidStruct).ExpectMatches(field.ErrorMatcher{}.ByType().ByField().ByOrigin(), field.ErrorList{ + field.Invalid(field.NewPath("resourcePoolNameField"), nil, "").WithOrigin("format=k8s-resource-pool-name"), + field.Invalid(field.NewPath("resourcePoolNamePtrField"), nil, "").WithOrigin("format=k8s-resource-pool-name"), + field.Invalid(field.NewPath("resourcePoolNameTypedefField"), nil, "").WithOrigin("format=k8s-resource-pool-name"), + }) + // Test validation ratcheting + st.Value(invalidStruct).OldValue(invalidStruct).ExpectValid() + + invalidStruct = &Struct{ + ResourcePoolNameField: "a..b", + ResourcePoolNamePtrField: ptr.To("a..b"), + ResourcePoolNameTypedefField: "a..b", + } + st.Value(invalidStruct).ExpectMatches(field.ErrorMatcher{}.ByType().ByField().ByOrigin(), field.ErrorList{ + field.Invalid(field.NewPath("resourcePoolNameField"), nil, "").WithOrigin("format=k8s-resource-pool-name"), + field.Invalid(field.NewPath("resourcePoolNamePtrField"), nil, "").WithOrigin("format=k8s-resource-pool-name"), + field.Invalid(field.NewPath("resourcePoolNameTypedefField"), nil, "").WithOrigin("format=k8s-resource-pool-name"), + }) + // Test validation ratcheting + st.Value(invalidStruct).OldValue(invalidStruct).ExpectValid() +} diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/format/k8s-resource-pool-name/zz_generated.validations.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/format/k8s-resource-pool-name/zz_generated.validations.go new file mode 100644 index 00000000000..3c4bc6fd620 --- /dev/null +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/format/k8s-resource-pool-name/zz_generated.validations.go @@ -0,0 +1,101 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by validation-gen. DO NOT EDIT. + +package format + +import ( + context "context" + fmt "fmt" + + operation "k8s.io/apimachinery/pkg/api/operation" + safe "k8s.io/apimachinery/pkg/api/safe" + validate "k8s.io/apimachinery/pkg/api/validate" + field "k8s.io/apimachinery/pkg/util/validation/field" + testscheme "k8s.io/code-generator/cmd/validation-gen/testscheme" +) + +func init() { localSchemeBuilder.Register(RegisterValidations) } + +// RegisterValidations adds validation functions to the given scheme. +// Public to allow building arbitrary schemes. +func RegisterValidations(scheme *testscheme.Scheme) error { + // type Struct + scheme.AddValidationFunc((*Struct)(nil), func(ctx context.Context, op operation.Operation, obj, oldObj interface{}) field.ErrorList { + switch op.Request.SubresourcePath() { + case "/": + return Validate_Struct(ctx, op, nil /* fldPath */, obj.(*Struct), safe.Cast[*Struct](oldObj)) + } + return field.ErrorList{field.InternalError(nil, fmt.Errorf("no validation found for %T, subresource: %v", obj, op.Request.SubresourcePath()))} + }) + return nil +} + +// Validate_ResourcePoolNameStringType validates an instance of ResourcePoolNameStringType according +// to declarative validation rules in the API schema. +func Validate_ResourcePoolNameStringType(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *ResourcePoolNameStringType) (errs field.ErrorList) { + errs = append(errs, validate.ResourcePoolName(ctx, op, fldPath, obj, oldObj)...) + + return errs +} + +// Validate_Struct validates an instance of Struct according +// to declarative validation rules in the API schema. +func Validate_Struct(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *Struct) (errs field.ErrorList) { + // field Struct.TypeMeta has no validation + + // field Struct.ResourcePoolNameField + errs = append(errs, + func(fldPath *field.Path, obj, oldObj *string) (errs field.ErrorList) { + // don't revalidate unchanged data + if op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) { + return nil + } + // call field-attached validations + errs = append(errs, validate.ResourcePoolName(ctx, op, fldPath, obj, oldObj)...) + return + }(fldPath.Child("resourcePoolNameField"), &obj.ResourcePoolNameField, safe.Field(oldObj, func(oldObj *Struct) *string { return &oldObj.ResourcePoolNameField }))...) + + // field Struct.ResourcePoolNamePtrField + errs = append(errs, + func(fldPath *field.Path, obj, oldObj *string) (errs field.ErrorList) { + // don't revalidate unchanged data + if op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) { + return nil + } + // call field-attached validations + errs = append(errs, validate.ResourcePoolName(ctx, op, fldPath, obj, oldObj)...) + return + }(fldPath.Child("resourcePoolNamePtrField"), obj.ResourcePoolNamePtrField, safe.Field(oldObj, func(oldObj *Struct) *string { return oldObj.ResourcePoolNamePtrField }))...) + + // field Struct.ResourcePoolNameTypedefField + errs = append(errs, + func(fldPath *field.Path, obj, oldObj *ResourcePoolNameStringType) (errs field.ErrorList) { + // don't revalidate unchanged data + if op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) { + return nil + } + // call the type's validation function + errs = append(errs, Validate_ResourcePoolNameStringType(ctx, op, fldPath, obj, oldObj)...) + return + }(fldPath.Child("resourcePoolNameTypedefField"), &obj.ResourcePoolNameTypedefField, safe.Field(oldObj, func(oldObj *Struct) *ResourcePoolNameStringType { return &oldObj.ResourcePoolNameTypedefField }))...) + + return errs +} From 8606fa03dc7dbd8237c464387c793c9e738bdc2c Mon Sep 17 00:00:00 2001 From: Joe Betz Date: Tue, 16 Sep 2025 17:58:00 -0400 Subject: [PATCH 4/7] Add declarative validation of ResourceClaim status pool field --- .../resource/resourceclaim/strategy.go | 24 ++++++++++++++++--- staging/src/k8s.io/api/resource/v1/types.go | 6 +++++ .../src/k8s.io/api/resource/v1beta1/types.go | 6 +++++ .../src/k8s.io/api/resource/v1beta2/types.go | 6 +++++ 4 files changed, 39 insertions(+), 3 deletions(-) diff --git a/pkg/registry/resource/resourceclaim/strategy.go b/pkg/registry/resource/resourceclaim/strategy.go index 32aa15fb0a4..94887cd4dd4 100644 --- a/pkg/registry/resource/resourceclaim/strategy.go +++ b/pkg/registry/resource/resourceclaim/strategy.go @@ -20,6 +20,8 @@ import ( "context" "errors" + "sigs.k8s.io/structured-merge-diff/v6/fieldpath" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/labels" @@ -40,7 +42,6 @@ import ( "k8s.io/kubernetes/pkg/features" resourceutils "k8s.io/kubernetes/pkg/registry/resource" "k8s.io/utils/ptr" - "sigs.k8s.io/structured-merge-diff/v6/fieldpath" ) // resourceclaimStrategy implements behavior for ResourceClaim objects @@ -207,8 +208,25 @@ func (r *resourceclaimStatusStrategy) ValidateUpdate(ctx context.Context, obj, o if oldClaim.Status.Allocation != nil { oldAllocationResult = oldClaim.Status.Allocation.Devices.Results } - allErrs := resourceutils.AuthorizedForAdminStatus(ctx, newAllocationResult, oldAllocationResult, newClaim.Namespace, r.nsClient) - return append(allErrs, validation.ValidateResourceClaimStatusUpdate(newClaim, oldClaim)...) + errs := resourceutils.AuthorizedForAdminStatus(ctx, newAllocationResult, oldAllocationResult, newClaim.Namespace, r.nsClient) + errs = append(errs, validation.ValidateResourceClaimStatusUpdate(newClaim, oldClaim)...) + + // If DeclarativeValidation feature gate is enabled, also run declarative validation + if utilfeature.DefaultFeatureGate.Enabled(features.DeclarativeValidation) { + // Determine if takeover is enabled + takeover := utilfeature.DefaultFeatureGate.Enabled(features.DeclarativeValidationTakeover) + + // Run declarative update validation with panic recovery + declarativeErrs := rest.ValidateUpdateDeclaratively(ctx, legacyscheme.Scheme, newClaim, oldClaim, rest.WithTakeover(takeover)) + // Compare imperative and declarative errors and emit metric if there's a mismatch + rest.CompareDeclarativeErrorsAndEmitMismatches(ctx, errs, declarativeErrs, takeover, "dc_status_update") + + // Only apply declarative errors if takeover is enabled + if takeover { + errs = append(errs.RemoveCoveredByDeclarative(), declarativeErrs...) + } + } + return errs } // WarningsOnUpdate returns warnings for the given update. diff --git a/staging/src/k8s.io/api/resource/v1/types.go b/staging/src/k8s.io/api/resource/v1/types.go index f29504444ff..feb633d0f98 100644 --- a/staging/src/k8s.io/api/resource/v1/types.go +++ b/staging/src/k8s.io/api/resource/v1/types.go @@ -678,6 +678,7 @@ type ResourceSliceList struct { // +genclient // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // +k8s:prerelease-lifecycle-gen:introduced=1.34 +// +k8s:supportsSubresource=/status // ResourceClaim describes a request for access to resources in the cluster, // for use by workloads. For example, if a workload needs an accelerator device @@ -1346,6 +1347,7 @@ type ResourceClaimStatus struct { // Allocation is set once the claim has been allocated successfully. // // +optional + // +k8s:optional Allocation *AllocationResult `json:"allocation,omitempty" protobuf:"bytes,1,opt,name=allocation"` // ReservedFor indicates which entities are currently allowed to use @@ -1369,6 +1371,7 @@ type ResourceClaimStatus struct { // the future, but not reduced. // // +optional + // +k8s:optional // +listType=map // +listMapKey=uid // +patchStrategy=merge @@ -1385,6 +1388,7 @@ type ResourceClaimStatus struct { // information. Entries are owned by their respective drivers. // // +optional + // +k8s:optional // +listType=map // +listMapKey=driver // +listMapKey=device @@ -1502,6 +1506,8 @@ type DeviceRequestAllocationResult struct { // DNS sub-domains separated by slashes. // // +required + // +k8s:required + // +k8s:format=k8s-resource-pool-name Pool string `json:"pool" protobuf:"bytes,3,name=pool"` // Device references one device instance via its name in the driver's diff --git a/staging/src/k8s.io/api/resource/v1beta1/types.go b/staging/src/k8s.io/api/resource/v1beta1/types.go index 27967f38ec1..b879721b87f 100644 --- a/staging/src/k8s.io/api/resource/v1beta1/types.go +++ b/staging/src/k8s.io/api/resource/v1beta1/types.go @@ -682,6 +682,7 @@ type ResourceSliceList struct { // +genclient // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // +k8s:prerelease-lifecycle-gen:introduced=1.32 +// +k8s:supportsSubresource=/status // ResourceClaim describes a request for access to resources in the cluster, // for use by workloads. For example, if a workload needs an accelerator device @@ -1354,6 +1355,7 @@ type ResourceClaimStatus struct { // Allocation is set once the claim has been allocated successfully. // // +optional + // +k8s:optional Allocation *AllocationResult `json:"allocation,omitempty" protobuf:"bytes,1,opt,name=allocation"` // ReservedFor indicates which entities are currently allowed to use @@ -1377,6 +1379,7 @@ type ResourceClaimStatus struct { // the future, but not reduced. // // +optional + // +k8s:optional // +listType=map // +listMapKey=uid // +patchStrategy=merge @@ -1393,6 +1396,7 @@ type ResourceClaimStatus struct { // information. Entries are owned by their respective drivers. // // +optional + // +k8s:optional // +listType=map // +listMapKey=driver // +listMapKey=device @@ -1510,6 +1514,8 @@ type DeviceRequestAllocationResult struct { // DNS sub-domains separated by slashes. // // +required + // +k8s:required + // +k8s:format=k8s-resource-pool-name Pool string `json:"pool" protobuf:"bytes,3,name=pool"` // Device references one device instance via its name in the driver's diff --git a/staging/src/k8s.io/api/resource/v1beta2/types.go b/staging/src/k8s.io/api/resource/v1beta2/types.go index 9fa98abdf2d..2e5c4505e3f 100644 --- a/staging/src/k8s.io/api/resource/v1beta2/types.go +++ b/staging/src/k8s.io/api/resource/v1beta2/types.go @@ -678,6 +678,7 @@ type ResourceSliceList struct { // +genclient // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // +k8s:prerelease-lifecycle-gen:introduced=1.33 +// +k8s:supportsSubresource=/status // ResourceClaim describes a request for access to resources in the cluster, // for use by workloads. For example, if a workload needs an accelerator device @@ -1346,6 +1347,7 @@ type ResourceClaimStatus struct { // Allocation is set once the claim has been allocated successfully. // // +optional + // +k8s:optional Allocation *AllocationResult `json:"allocation,omitempty" protobuf:"bytes,1,opt,name=allocation"` // ReservedFor indicates which entities are currently allowed to use @@ -1369,6 +1371,7 @@ type ResourceClaimStatus struct { // the future, but not reduced. // // +optional + // +k8s:optional // +listType=map // +listMapKey=uid // +patchStrategy=merge @@ -1385,6 +1388,7 @@ type ResourceClaimStatus struct { // information. Entries are owned by their respective drivers. // // +optional + // +k8s:optional // +listType=map // +listMapKey=driver // +listMapKey=device @@ -1502,6 +1506,8 @@ type DeviceRequestAllocationResult struct { // DNS sub-domains separated by slashes. // // +required + // +k8s:required + // +k8s:format=k8s-resource-pool-name Pool string `json:"pool" protobuf:"bytes,3,name=pool"` // Device references one device instance via its name in the driver's From 7019a088c3932f958c4c1edb259c2ed603f06168 Mon Sep 17 00:00:00 2001 From: Joe Betz Date: Tue, 16 Sep 2025 17:57:59 -0400 Subject: [PATCH 5/7] Add declarative validation tests for ResourceClaim status --- pkg/apis/resource/validation/validation.go | 7 +- .../declarative_validation_test.go | 125 +++++++++++++++++- 2 files changed, 127 insertions(+), 5 deletions(-) diff --git a/pkg/apis/resource/validation/validation.go b/pkg/apis/resource/validation/validation.go index 4af80ffd7de..f336910497b 100644 --- a/pkg/apis/resource/validation/validation.go +++ b/pkg/apis/resource/validation/validation.go @@ -26,6 +26,7 @@ import ( "strings" "github.com/google/uuid" + corev1 "k8s.io/api/core/v1" apiequality "k8s.io/apimachinery/pkg/api/equality" apiresource "k8s.io/apimachinery/pkg/api/resource" @@ -63,14 +64,14 @@ var ( func validatePoolName(name string, fldPath *field.Path) field.ErrorList { var allErrs field.ErrorList if name == "" { - allErrs = append(allErrs, field.Required(fldPath, "")) + allErrs = append(allErrs, field.Required(fldPath, "").MarkCoveredByDeclarative()) } else { if len(name) > resource.PoolNameMaxLength { - allErrs = append(allErrs, field.TooLong(fldPath, "" /*unused*/, resource.PoolNameMaxLength)) + allErrs = append(allErrs, field.TooLong(fldPath, "" /*unused*/, resource.PoolNameMaxLength).WithOrigin("format=k8s-resource-pool-name").MarkCoveredByDeclarative()) } parts := strings.Split(name, "/") for _, part := range parts { - allErrs = append(allErrs, corevalidation.ValidateDNS1123Subdomain(part, fldPath)...) + allErrs = append(allErrs, corevalidation.ValidateDNS1123Subdomain(part, fldPath).WithOrigin("format=k8s-resource-pool-name").MarkCoveredByDeclarative()...) } } return allErrs diff --git a/pkg/registry/resource/resourceclaim/declarative_validation_test.go b/pkg/registry/resource/resourceclaim/declarative_validation_test.go index baabe9e9f53..8080d2db2f2 100644 --- a/pkg/registry/resource/resourceclaim/declarative_validation_test.go +++ b/pkg/registry/resource/resourceclaim/declarative_validation_test.go @@ -17,6 +17,7 @@ limitations under the License. package resourceclaim import ( + "strings" "testing" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -100,11 +101,87 @@ func testDeclarativeValidateUpdate(t *testing.T, apiVersion string) { } } +func TestValidateStatusUpdateForDeclarative(t *testing.T) { + fakeClient := fake.NewClientset() + mockNSClient := fakeClient.CoreV1().Namespaces() + Strategy := NewStrategy(mockNSClient) + strategy := NewStatusStrategy(Strategy) + + ctx := genericapirequest.WithRequestInfo(genericapirequest.NewDefaultContext(), &genericapirequest.RequestInfo{ + APIGroup: "resource.k8s.io", + APIVersion: "v1", + Subresource: "status", + }) + poolPath := field.NewPath("status", "allocation", "devices", "results").Index(0).Child("pool") + testCases := map[string]struct { + old resource.ResourceClaim + update resource.ResourceClaim + expectedErrs field.ErrorList + }{ + "valid pool name": { + old: mkValidResourceClaim(), + update: tweakStatusDeviceRequestAllocationResultPool(mkResourceClaimWithStatus(), "dra.example.com/pool-a"), + }, + "valid pool name, max length": { + old: mkValidResourceClaim(), + update: tweakStatusDeviceRequestAllocationResultPool(mkResourceClaimWithStatus(), strings.Repeat("a", 63)+"."+strings.Repeat("b", 63)+"."+strings.Repeat("c", 63)+"."+strings.Repeat("d", 55)), + }, + "invalid pool name, required": { + old: mkValidResourceClaim(), + update: tweakStatusDeviceRequestAllocationResultPool(mkResourceClaimWithStatus(), ""), + expectedErrs: field.ErrorList{ + field.Required(poolPath, ""), + }, + }, + "invalid pool name, too long": { + old: mkValidResourceClaim(), + update: tweakStatusDeviceRequestAllocationResultPool(mkResourceClaimWithStatus(), strings.Repeat("a", 253)+"/"+strings.Repeat("a", 253)), + expectedErrs: field.ErrorList{ + field.TooLong(poolPath, "", 253).WithOrigin("format=k8s-resource-pool-name"), + }, + }, + "invalid pool name, format": { + old: mkValidResourceClaim(), + update: tweakStatusDeviceRequestAllocationResultPool(mkResourceClaimWithStatus(), "a/Not_Valid"), + expectedErrs: field.ErrorList{ + field.Invalid(poolPath, "Not_Valid", "").WithOrigin("format=k8s-resource-pool-name"), + }, + }, + "invalid pool name, leading slash": { + old: mkValidResourceClaim(), + update: tweakStatusDeviceRequestAllocationResultPool(mkResourceClaimWithStatus(), "/a"), + expectedErrs: field.ErrorList{ + field.Invalid(poolPath, "", "").WithOrigin("format=k8s-resource-pool-name"), + }, + }, + "invalid pool name, trailing slash": { + old: mkValidResourceClaim(), + update: tweakStatusDeviceRequestAllocationResultPool(mkResourceClaimWithStatus(), "a/"), + expectedErrs: field.ErrorList{ + field.Invalid(poolPath, "", "").WithOrigin("format=k8s-resource-pool-name"), + }, + }, + "invalid pool name, double slash": { + old: mkValidResourceClaim(), + update: tweakStatusDeviceRequestAllocationResultPool(mkResourceClaimWithStatus(), "a//b"), + expectedErrs: field.ErrorList{ + field.Invalid(poolPath, "", "").WithOrigin("format=k8s-resource-pool-name"), + }, + }, + } + for k, tc := range testCases { + t.Run(k, func(t *testing.T) { + apitesting.VerifyUpdateValidationEquivalence(t, ctx, &tc.update, &tc.old, strategy.ValidateUpdate, tc.expectedErrs) + }) + } +} + func mkValidResourceClaim() resource.ResourceClaim { return resource.ResourceClaim{ ObjectMeta: v1.ObjectMeta{ - Name: "valid-claim", - Namespace: "default", + Name: "valid-claim", + Namespace: "default", + ResourceVersion: "0", }, Spec: resource.ResourceClaimSpec{ Devices: resource.DeviceClaim{ @@ -121,3 +198,47 @@ func mkValidResourceClaim() resource.ResourceClaim { }, } } + +func mkResourceClaimWithStatus() resource.ResourceClaim { + return resource.ResourceClaim{ + ObjectMeta: v1.ObjectMeta{ + Name: "valid-claim", + Namespace: "default", + ResourceVersion: "1", + }, + Spec: resource.ResourceClaimSpec{ + Devices: resource.DeviceClaim{ + Requests: []resource.DeviceRequest{ + { + Name: "req-0", + Exactly: &resource.ExactDeviceRequest{ + DeviceClassName: "class", + AllocationMode: resource.DeviceAllocationModeAll, + }, + }, + }, + }, + }, + Status: resource.ResourceClaimStatus{ + Allocation: &resource.AllocationResult{ + Devices: resource.DeviceAllocationResult{ + Results: []resource.DeviceRequestAllocationResult{ + { + Request: "req-0", + Driver: "dra.example.com", + Pool: "pool-0", + Device: "device-0", + }, + }, + }, + }, + }, + } +} + +func tweakStatusDeviceRequestAllocationResultPool(obj resource.ResourceClaim, pool string) resource.ResourceClaim { + for i := range obj.Status.Allocation.Devices.Results { + obj.Status.Allocation.Devices.Results[i].Pool = pool + } + return obj +} From dbe4143de68af976394cf91d0d4474821871a7d2 Mon Sep 17 00:00:00 2001 From: Joe Betz Date: Tue, 16 Sep 2025 17:58:00 -0400 Subject: [PATCH 6/7] generate --- .../resource/v1/zz_generated.validations.go | 193 +++++++++++++++++ .../v1beta1/zz_generated.validations.go | 201 ++++++++++++++++++ .../v1beta2/zz_generated.validations.go | 201 ++++++++++++++++++ .../k8s.io/api/resource/v1/generated.proto | 5 + .../api/resource/v1beta1/generated.proto | 5 + .../api/resource/v1beta2/generated.proto | 5 + 6 files changed, 610 insertions(+) diff --git a/pkg/apis/resource/v1/zz_generated.validations.go b/pkg/apis/resource/v1/zz_generated.validations.go index 73e28035dba..4c9a9c96ac2 100644 --- a/pkg/apis/resource/v1/zz_generated.validations.go +++ b/pkg/apis/resource/v1/zz_generated.validations.go @@ -22,7 +22,16 @@ limitations under the License. package v1 import ( + context "context" + fmt "fmt" + + resourcev1 "k8s.io/api/resource/v1" + equality "k8s.io/apimachinery/pkg/api/equality" + operation "k8s.io/apimachinery/pkg/api/operation" + safe "k8s.io/apimachinery/pkg/api/safe" + validate "k8s.io/apimachinery/pkg/api/validate" runtime "k8s.io/apimachinery/pkg/runtime" + field "k8s.io/apimachinery/pkg/util/validation/field" ) func init() { localSchemeBuilder.Register(RegisterValidations) } @@ -30,5 +39,189 @@ func init() { localSchemeBuilder.Register(RegisterValidations) } // RegisterValidations adds validation functions to the given scheme. // Public to allow building arbitrary schemes. func RegisterValidations(scheme *runtime.Scheme) error { + // type ResourceClaim + scheme.AddValidationFunc((*resourcev1.ResourceClaim)(nil), func(ctx context.Context, op operation.Operation, obj, oldObj interface{}) field.ErrorList { + switch op.Request.SubresourcePath() { + case "/", "/status": + return Validate_ResourceClaim(ctx, op, nil /* fldPath */, obj.(*resourcev1.ResourceClaim), safe.Cast[*resourcev1.ResourceClaim](oldObj)) + } + return field.ErrorList{field.InternalError(nil, fmt.Errorf("no validation found for %T, subresource: %v", obj, op.Request.SubresourcePath()))} + }) + // type ResourceClaimList + scheme.AddValidationFunc((*resourcev1.ResourceClaimList)(nil), func(ctx context.Context, op operation.Operation, obj, oldObj interface{}) field.ErrorList { + switch op.Request.SubresourcePath() { + case "/": + return Validate_ResourceClaimList(ctx, op, nil /* fldPath */, obj.(*resourcev1.ResourceClaimList), safe.Cast[*resourcev1.ResourceClaimList](oldObj)) + } + return field.ErrorList{field.InternalError(nil, fmt.Errorf("no validation found for %T, subresource: %v", obj, op.Request.SubresourcePath()))} + }) return nil } + +// Validate_AllocationResult validates an instance of AllocationResult according +// to declarative validation rules in the API schema. +func Validate_AllocationResult(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *resourcev1.AllocationResult) (errs field.ErrorList) { + // field resourcev1.AllocationResult.Devices + errs = append(errs, + func(fldPath *field.Path, obj, oldObj *resourcev1.DeviceAllocationResult) (errs field.ErrorList) { + // don't revalidate unchanged data + if op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) { + return nil + } + // call the type's validation function + errs = append(errs, Validate_DeviceAllocationResult(ctx, op, fldPath, obj, oldObj)...) + return + }(fldPath.Child("devices"), &obj.Devices, safe.Field(oldObj, func(oldObj *resourcev1.AllocationResult) *resourcev1.DeviceAllocationResult { return &oldObj.Devices }))...) + + // field resourcev1.AllocationResult.NodeSelector has no validation + // field resourcev1.AllocationResult.AllocationTimestamp has no validation + return errs +} + +// Validate_DeviceAllocationResult validates an instance of DeviceAllocationResult according +// to declarative validation rules in the API schema. +func Validate_DeviceAllocationResult(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *resourcev1.DeviceAllocationResult) (errs field.ErrorList) { + // field resourcev1.DeviceAllocationResult.Results + errs = append(errs, + func(fldPath *field.Path, obj, oldObj []resourcev1.DeviceRequestAllocationResult) (errs field.ErrorList) { + // don't revalidate unchanged data + if op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) { + return nil + } + // iterate the list and call the type's validation function + errs = append(errs, validate.EachSliceVal(ctx, op, fldPath, obj, oldObj, nil, nil, Validate_DeviceRequestAllocationResult)...) + return + }(fldPath.Child("results"), obj.Results, safe.Field(oldObj, func(oldObj *resourcev1.DeviceAllocationResult) []resourcev1.DeviceRequestAllocationResult { + return oldObj.Results + }))...) + + // field resourcev1.DeviceAllocationResult.Config has no validation + return errs +} + +// Validate_DeviceRequestAllocationResult validates an instance of DeviceRequestAllocationResult according +// to declarative validation rules in the API schema. +func Validate_DeviceRequestAllocationResult(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *resourcev1.DeviceRequestAllocationResult) (errs field.ErrorList) { + // field resourcev1.DeviceRequestAllocationResult.Request has no validation + // field resourcev1.DeviceRequestAllocationResult.Driver has no validation + + // field resourcev1.DeviceRequestAllocationResult.Pool + errs = append(errs, + func(fldPath *field.Path, obj, oldObj *string) (errs field.ErrorList) { + // don't revalidate unchanged data + if op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) { + return nil + } + // call field-attached validations + if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + return // do not proceed + } + errs = append(errs, validate.ResourcePoolName(ctx, op, fldPath, obj, oldObj)...) + return + }(fldPath.Child("pool"), &obj.Pool, safe.Field(oldObj, func(oldObj *resourcev1.DeviceRequestAllocationResult) *string { return &oldObj.Pool }))...) + + // field resourcev1.DeviceRequestAllocationResult.Device has no validation + // field resourcev1.DeviceRequestAllocationResult.AdminAccess has no validation + // field resourcev1.DeviceRequestAllocationResult.Tolerations has no validation + // field resourcev1.DeviceRequestAllocationResult.BindingConditions has no validation + // field resourcev1.DeviceRequestAllocationResult.BindingFailureConditions has no validation + // field resourcev1.DeviceRequestAllocationResult.ShareID has no validation + // field resourcev1.DeviceRequestAllocationResult.ConsumedCapacity has no validation + return errs +} + +// Validate_ResourceClaim validates an instance of ResourceClaim according +// to declarative validation rules in the API schema. +func Validate_ResourceClaim(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *resourcev1.ResourceClaim) (errs field.ErrorList) { + // field resourcev1.ResourceClaim.TypeMeta has no validation + // field resourcev1.ResourceClaim.ObjectMeta has no validation + // field resourcev1.ResourceClaim.Spec has no validation + + // field resourcev1.ResourceClaim.Status + errs = append(errs, + func(fldPath *field.Path, obj, oldObj *resourcev1.ResourceClaimStatus) (errs field.ErrorList) { + // don't revalidate unchanged data + if op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) { + return nil + } + // call the type's validation function + errs = append(errs, Validate_ResourceClaimStatus(ctx, op, fldPath, obj, oldObj)...) + return + }(fldPath.Child("status"), &obj.Status, safe.Field(oldObj, func(oldObj *resourcev1.ResourceClaim) *resourcev1.ResourceClaimStatus { return &oldObj.Status }))...) + + return errs +} + +// Validate_ResourceClaimList validates an instance of ResourceClaimList according +// to declarative validation rules in the API schema. +func Validate_ResourceClaimList(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *resourcev1.ResourceClaimList) (errs field.ErrorList) { + // field resourcev1.ResourceClaimList.TypeMeta has no validation + // field resourcev1.ResourceClaimList.ListMeta has no validation + + // field resourcev1.ResourceClaimList.Items + errs = append(errs, + func(fldPath *field.Path, obj, oldObj []resourcev1.ResourceClaim) (errs field.ErrorList) { + // don't revalidate unchanged data + if op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) { + return nil + } + // iterate the list and call the type's validation function + errs = append(errs, validate.EachSliceVal(ctx, op, fldPath, obj, oldObj, nil, nil, Validate_ResourceClaim)...) + return + }(fldPath.Child("items"), obj.Items, safe.Field(oldObj, func(oldObj *resourcev1.ResourceClaimList) []resourcev1.ResourceClaim { return oldObj.Items }))...) + + return errs +} + +// Validate_ResourceClaimStatus validates an instance of ResourceClaimStatus according +// to declarative validation rules in the API schema. +func Validate_ResourceClaimStatus(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *resourcev1.ResourceClaimStatus) (errs field.ErrorList) { + // field resourcev1.ResourceClaimStatus.Allocation + errs = append(errs, + func(fldPath *field.Path, obj, oldObj *resourcev1.AllocationResult) (errs field.ErrorList) { + // don't revalidate unchanged data + if op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) { + return nil + } + // call field-attached validations + if e := validate.OptionalPointer(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + return // do not proceed + } + // call the type's validation function + errs = append(errs, Validate_AllocationResult(ctx, op, fldPath, obj, oldObj)...) + return + }(fldPath.Child("allocation"), obj.Allocation, safe.Field(oldObj, func(oldObj *resourcev1.ResourceClaimStatus) *resourcev1.AllocationResult { return oldObj.Allocation }))...) + + // field resourcev1.ResourceClaimStatus.ReservedFor + errs = append(errs, + func(fldPath *field.Path, obj, oldObj []resourcev1.ResourceClaimConsumerReference) (errs field.ErrorList) { + // don't revalidate unchanged data + if op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) { + return nil + } + // call field-attached validations + if e := validate.OptionalSlice(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + return // do not proceed + } + return + }(fldPath.Child("reservedFor"), obj.ReservedFor, safe.Field(oldObj, func(oldObj *resourcev1.ResourceClaimStatus) []resourcev1.ResourceClaimConsumerReference { + return oldObj.ReservedFor + }))...) + + // field resourcev1.ResourceClaimStatus.Devices + errs = append(errs, + func(fldPath *field.Path, obj, oldObj []resourcev1.AllocatedDeviceStatus) (errs field.ErrorList) { + // don't revalidate unchanged data + if op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) { + return nil + } + // call field-attached validations + if e := validate.OptionalSlice(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + return // do not proceed + } + return + }(fldPath.Child("devices"), obj.Devices, safe.Field(oldObj, func(oldObj *resourcev1.ResourceClaimStatus) []resourcev1.AllocatedDeviceStatus { return oldObj.Devices }))...) + + return errs +} diff --git a/pkg/apis/resource/v1beta1/zz_generated.validations.go b/pkg/apis/resource/v1beta1/zz_generated.validations.go index 0b9e874b635..368b5bf7fa1 100644 --- a/pkg/apis/resource/v1beta1/zz_generated.validations.go +++ b/pkg/apis/resource/v1beta1/zz_generated.validations.go @@ -22,7 +22,16 @@ limitations under the License. package v1beta1 import ( + context "context" + fmt "fmt" + + resourcev1beta1 "k8s.io/api/resource/v1beta1" + equality "k8s.io/apimachinery/pkg/api/equality" + operation "k8s.io/apimachinery/pkg/api/operation" + safe "k8s.io/apimachinery/pkg/api/safe" + validate "k8s.io/apimachinery/pkg/api/validate" runtime "k8s.io/apimachinery/pkg/runtime" + field "k8s.io/apimachinery/pkg/util/validation/field" ) func init() { localSchemeBuilder.Register(RegisterValidations) } @@ -30,5 +39,197 @@ func init() { localSchemeBuilder.Register(RegisterValidations) } // RegisterValidations adds validation functions to the given scheme. // Public to allow building arbitrary schemes. func RegisterValidations(scheme *runtime.Scheme) error { + // type ResourceClaim + scheme.AddValidationFunc((*resourcev1beta1.ResourceClaim)(nil), func(ctx context.Context, op operation.Operation, obj, oldObj interface{}) field.ErrorList { + switch op.Request.SubresourcePath() { + case "/", "/status": + return Validate_ResourceClaim(ctx, op, nil /* fldPath */, obj.(*resourcev1beta1.ResourceClaim), safe.Cast[*resourcev1beta1.ResourceClaim](oldObj)) + } + return field.ErrorList{field.InternalError(nil, fmt.Errorf("no validation found for %T, subresource: %v", obj, op.Request.SubresourcePath()))} + }) + // type ResourceClaimList + scheme.AddValidationFunc((*resourcev1beta1.ResourceClaimList)(nil), func(ctx context.Context, op operation.Operation, obj, oldObj interface{}) field.ErrorList { + switch op.Request.SubresourcePath() { + case "/": + return Validate_ResourceClaimList(ctx, op, nil /* fldPath */, obj.(*resourcev1beta1.ResourceClaimList), safe.Cast[*resourcev1beta1.ResourceClaimList](oldObj)) + } + return field.ErrorList{field.InternalError(nil, fmt.Errorf("no validation found for %T, subresource: %v", obj, op.Request.SubresourcePath()))} + }) return nil } + +// Validate_AllocationResult validates an instance of AllocationResult according +// to declarative validation rules in the API schema. +func Validate_AllocationResult(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *resourcev1beta1.AllocationResult) (errs field.ErrorList) { + // field resourcev1beta1.AllocationResult.Devices + errs = append(errs, + func(fldPath *field.Path, obj, oldObj *resourcev1beta1.DeviceAllocationResult) (errs field.ErrorList) { + // don't revalidate unchanged data + if op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) { + return nil + } + // call the type's validation function + errs = append(errs, Validate_DeviceAllocationResult(ctx, op, fldPath, obj, oldObj)...) + return + }(fldPath.Child("devices"), &obj.Devices, safe.Field(oldObj, func(oldObj *resourcev1beta1.AllocationResult) *resourcev1beta1.DeviceAllocationResult { + return &oldObj.Devices + }))...) + + // field resourcev1beta1.AllocationResult.NodeSelector has no validation + // field resourcev1beta1.AllocationResult.AllocationTimestamp has no validation + return errs +} + +// Validate_DeviceAllocationResult validates an instance of DeviceAllocationResult according +// to declarative validation rules in the API schema. +func Validate_DeviceAllocationResult(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *resourcev1beta1.DeviceAllocationResult) (errs field.ErrorList) { + // field resourcev1beta1.DeviceAllocationResult.Results + errs = append(errs, + func(fldPath *field.Path, obj, oldObj []resourcev1beta1.DeviceRequestAllocationResult) (errs field.ErrorList) { + // don't revalidate unchanged data + if op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) { + return nil + } + // iterate the list and call the type's validation function + errs = append(errs, validate.EachSliceVal(ctx, op, fldPath, obj, oldObj, nil, nil, Validate_DeviceRequestAllocationResult)...) + return + }(fldPath.Child("results"), obj.Results, safe.Field(oldObj, func(oldObj *resourcev1beta1.DeviceAllocationResult) []resourcev1beta1.DeviceRequestAllocationResult { + return oldObj.Results + }))...) + + // field resourcev1beta1.DeviceAllocationResult.Config has no validation + return errs +} + +// Validate_DeviceRequestAllocationResult validates an instance of DeviceRequestAllocationResult according +// to declarative validation rules in the API schema. +func Validate_DeviceRequestAllocationResult(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *resourcev1beta1.DeviceRequestAllocationResult) (errs field.ErrorList) { + // field resourcev1beta1.DeviceRequestAllocationResult.Request has no validation + // field resourcev1beta1.DeviceRequestAllocationResult.Driver has no validation + + // field resourcev1beta1.DeviceRequestAllocationResult.Pool + errs = append(errs, + func(fldPath *field.Path, obj, oldObj *string) (errs field.ErrorList) { + // don't revalidate unchanged data + if op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) { + return nil + } + // call field-attached validations + if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + return // do not proceed + } + errs = append(errs, validate.ResourcePoolName(ctx, op, fldPath, obj, oldObj)...) + return + }(fldPath.Child("pool"), &obj.Pool, safe.Field(oldObj, func(oldObj *resourcev1beta1.DeviceRequestAllocationResult) *string { return &oldObj.Pool }))...) + + // field resourcev1beta1.DeviceRequestAllocationResult.Device has no validation + // field resourcev1beta1.DeviceRequestAllocationResult.AdminAccess has no validation + // field resourcev1beta1.DeviceRequestAllocationResult.Tolerations has no validation + // field resourcev1beta1.DeviceRequestAllocationResult.BindingConditions has no validation + // field resourcev1beta1.DeviceRequestAllocationResult.BindingFailureConditions has no validation + // field resourcev1beta1.DeviceRequestAllocationResult.ShareID has no validation + // field resourcev1beta1.DeviceRequestAllocationResult.ConsumedCapacity has no validation + return errs +} + +// Validate_ResourceClaim validates an instance of ResourceClaim according +// to declarative validation rules in the API schema. +func Validate_ResourceClaim(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *resourcev1beta1.ResourceClaim) (errs field.ErrorList) { + // field resourcev1beta1.ResourceClaim.TypeMeta has no validation + // field resourcev1beta1.ResourceClaim.ObjectMeta has no validation + // field resourcev1beta1.ResourceClaim.Spec has no validation + + // field resourcev1beta1.ResourceClaim.Status + errs = append(errs, + func(fldPath *field.Path, obj, oldObj *resourcev1beta1.ResourceClaimStatus) (errs field.ErrorList) { + // don't revalidate unchanged data + if op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) { + return nil + } + // call the type's validation function + errs = append(errs, Validate_ResourceClaimStatus(ctx, op, fldPath, obj, oldObj)...) + return + }(fldPath.Child("status"), &obj.Status, safe.Field(oldObj, func(oldObj *resourcev1beta1.ResourceClaim) *resourcev1beta1.ResourceClaimStatus { + return &oldObj.Status + }))...) + + return errs +} + +// Validate_ResourceClaimList validates an instance of ResourceClaimList according +// to declarative validation rules in the API schema. +func Validate_ResourceClaimList(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *resourcev1beta1.ResourceClaimList) (errs field.ErrorList) { + // field resourcev1beta1.ResourceClaimList.TypeMeta has no validation + // field resourcev1beta1.ResourceClaimList.ListMeta has no validation + + // field resourcev1beta1.ResourceClaimList.Items + errs = append(errs, + func(fldPath *field.Path, obj, oldObj []resourcev1beta1.ResourceClaim) (errs field.ErrorList) { + // don't revalidate unchanged data + if op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) { + return nil + } + // iterate the list and call the type's validation function + errs = append(errs, validate.EachSliceVal(ctx, op, fldPath, obj, oldObj, nil, nil, Validate_ResourceClaim)...) + return + }(fldPath.Child("items"), obj.Items, safe.Field(oldObj, func(oldObj *resourcev1beta1.ResourceClaimList) []resourcev1beta1.ResourceClaim { return oldObj.Items }))...) + + return errs +} + +// Validate_ResourceClaimStatus validates an instance of ResourceClaimStatus according +// to declarative validation rules in the API schema. +func Validate_ResourceClaimStatus(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *resourcev1beta1.ResourceClaimStatus) (errs field.ErrorList) { + // field resourcev1beta1.ResourceClaimStatus.Allocation + errs = append(errs, + func(fldPath *field.Path, obj, oldObj *resourcev1beta1.AllocationResult) (errs field.ErrorList) { + // don't revalidate unchanged data + if op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) { + return nil + } + // call field-attached validations + if e := validate.OptionalPointer(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + return // do not proceed + } + // call the type's validation function + errs = append(errs, Validate_AllocationResult(ctx, op, fldPath, obj, oldObj)...) + return + }(fldPath.Child("allocation"), obj.Allocation, safe.Field(oldObj, func(oldObj *resourcev1beta1.ResourceClaimStatus) *resourcev1beta1.AllocationResult { + return oldObj.Allocation + }))...) + + // field resourcev1beta1.ResourceClaimStatus.ReservedFor + errs = append(errs, + func(fldPath *field.Path, obj, oldObj []resourcev1beta1.ResourceClaimConsumerReference) (errs field.ErrorList) { + // don't revalidate unchanged data + if op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) { + return nil + } + // call field-attached validations + if e := validate.OptionalSlice(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + return // do not proceed + } + return + }(fldPath.Child("reservedFor"), obj.ReservedFor, safe.Field(oldObj, func(oldObj *resourcev1beta1.ResourceClaimStatus) []resourcev1beta1.ResourceClaimConsumerReference { + return oldObj.ReservedFor + }))...) + + // field resourcev1beta1.ResourceClaimStatus.Devices + errs = append(errs, + func(fldPath *field.Path, obj, oldObj []resourcev1beta1.AllocatedDeviceStatus) (errs field.ErrorList) { + // don't revalidate unchanged data + if op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) { + return nil + } + // call field-attached validations + if e := validate.OptionalSlice(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + return // do not proceed + } + return + }(fldPath.Child("devices"), obj.Devices, safe.Field(oldObj, func(oldObj *resourcev1beta1.ResourceClaimStatus) []resourcev1beta1.AllocatedDeviceStatus { + return oldObj.Devices + }))...) + + return errs +} diff --git a/pkg/apis/resource/v1beta2/zz_generated.validations.go b/pkg/apis/resource/v1beta2/zz_generated.validations.go index 2b709ed6643..2f15701ae1c 100644 --- a/pkg/apis/resource/v1beta2/zz_generated.validations.go +++ b/pkg/apis/resource/v1beta2/zz_generated.validations.go @@ -22,7 +22,16 @@ limitations under the License. package v1beta2 import ( + context "context" + fmt "fmt" + + resourcev1beta2 "k8s.io/api/resource/v1beta2" + equality "k8s.io/apimachinery/pkg/api/equality" + operation "k8s.io/apimachinery/pkg/api/operation" + safe "k8s.io/apimachinery/pkg/api/safe" + validate "k8s.io/apimachinery/pkg/api/validate" runtime "k8s.io/apimachinery/pkg/runtime" + field "k8s.io/apimachinery/pkg/util/validation/field" ) func init() { localSchemeBuilder.Register(RegisterValidations) } @@ -30,5 +39,197 @@ func init() { localSchemeBuilder.Register(RegisterValidations) } // RegisterValidations adds validation functions to the given scheme. // Public to allow building arbitrary schemes. func RegisterValidations(scheme *runtime.Scheme) error { + // type ResourceClaim + scheme.AddValidationFunc((*resourcev1beta2.ResourceClaim)(nil), func(ctx context.Context, op operation.Operation, obj, oldObj interface{}) field.ErrorList { + switch op.Request.SubresourcePath() { + case "/", "/status": + return Validate_ResourceClaim(ctx, op, nil /* fldPath */, obj.(*resourcev1beta2.ResourceClaim), safe.Cast[*resourcev1beta2.ResourceClaim](oldObj)) + } + return field.ErrorList{field.InternalError(nil, fmt.Errorf("no validation found for %T, subresource: %v", obj, op.Request.SubresourcePath()))} + }) + // type ResourceClaimList + scheme.AddValidationFunc((*resourcev1beta2.ResourceClaimList)(nil), func(ctx context.Context, op operation.Operation, obj, oldObj interface{}) field.ErrorList { + switch op.Request.SubresourcePath() { + case "/": + return Validate_ResourceClaimList(ctx, op, nil /* fldPath */, obj.(*resourcev1beta2.ResourceClaimList), safe.Cast[*resourcev1beta2.ResourceClaimList](oldObj)) + } + return field.ErrorList{field.InternalError(nil, fmt.Errorf("no validation found for %T, subresource: %v", obj, op.Request.SubresourcePath()))} + }) return nil } + +// Validate_AllocationResult validates an instance of AllocationResult according +// to declarative validation rules in the API schema. +func Validate_AllocationResult(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *resourcev1beta2.AllocationResult) (errs field.ErrorList) { + // field resourcev1beta2.AllocationResult.Devices + errs = append(errs, + func(fldPath *field.Path, obj, oldObj *resourcev1beta2.DeviceAllocationResult) (errs field.ErrorList) { + // don't revalidate unchanged data + if op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) { + return nil + } + // call the type's validation function + errs = append(errs, Validate_DeviceAllocationResult(ctx, op, fldPath, obj, oldObj)...) + return + }(fldPath.Child("devices"), &obj.Devices, safe.Field(oldObj, func(oldObj *resourcev1beta2.AllocationResult) *resourcev1beta2.DeviceAllocationResult { + return &oldObj.Devices + }))...) + + // field resourcev1beta2.AllocationResult.NodeSelector has no validation + // field resourcev1beta2.AllocationResult.AllocationTimestamp has no validation + return errs +} + +// Validate_DeviceAllocationResult validates an instance of DeviceAllocationResult according +// to declarative validation rules in the API schema. +func Validate_DeviceAllocationResult(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *resourcev1beta2.DeviceAllocationResult) (errs field.ErrorList) { + // field resourcev1beta2.DeviceAllocationResult.Results + errs = append(errs, + func(fldPath *field.Path, obj, oldObj []resourcev1beta2.DeviceRequestAllocationResult) (errs field.ErrorList) { + // don't revalidate unchanged data + if op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) { + return nil + } + // iterate the list and call the type's validation function + errs = append(errs, validate.EachSliceVal(ctx, op, fldPath, obj, oldObj, nil, nil, Validate_DeviceRequestAllocationResult)...) + return + }(fldPath.Child("results"), obj.Results, safe.Field(oldObj, func(oldObj *resourcev1beta2.DeviceAllocationResult) []resourcev1beta2.DeviceRequestAllocationResult { + return oldObj.Results + }))...) + + // field resourcev1beta2.DeviceAllocationResult.Config has no validation + return errs +} + +// Validate_DeviceRequestAllocationResult validates an instance of DeviceRequestAllocationResult according +// to declarative validation rules in the API schema. +func Validate_DeviceRequestAllocationResult(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *resourcev1beta2.DeviceRequestAllocationResult) (errs field.ErrorList) { + // field resourcev1beta2.DeviceRequestAllocationResult.Request has no validation + // field resourcev1beta2.DeviceRequestAllocationResult.Driver has no validation + + // field resourcev1beta2.DeviceRequestAllocationResult.Pool + errs = append(errs, + func(fldPath *field.Path, obj, oldObj *string) (errs field.ErrorList) { + // don't revalidate unchanged data + if op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) { + return nil + } + // call field-attached validations + if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + return // do not proceed + } + errs = append(errs, validate.ResourcePoolName(ctx, op, fldPath, obj, oldObj)...) + return + }(fldPath.Child("pool"), &obj.Pool, safe.Field(oldObj, func(oldObj *resourcev1beta2.DeviceRequestAllocationResult) *string { return &oldObj.Pool }))...) + + // field resourcev1beta2.DeviceRequestAllocationResult.Device has no validation + // field resourcev1beta2.DeviceRequestAllocationResult.AdminAccess has no validation + // field resourcev1beta2.DeviceRequestAllocationResult.Tolerations has no validation + // field resourcev1beta2.DeviceRequestAllocationResult.BindingConditions has no validation + // field resourcev1beta2.DeviceRequestAllocationResult.BindingFailureConditions has no validation + // field resourcev1beta2.DeviceRequestAllocationResult.ShareID has no validation + // field resourcev1beta2.DeviceRequestAllocationResult.ConsumedCapacity has no validation + return errs +} + +// Validate_ResourceClaim validates an instance of ResourceClaim according +// to declarative validation rules in the API schema. +func Validate_ResourceClaim(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *resourcev1beta2.ResourceClaim) (errs field.ErrorList) { + // field resourcev1beta2.ResourceClaim.TypeMeta has no validation + // field resourcev1beta2.ResourceClaim.ObjectMeta has no validation + // field resourcev1beta2.ResourceClaim.Spec has no validation + + // field resourcev1beta2.ResourceClaim.Status + errs = append(errs, + func(fldPath *field.Path, obj, oldObj *resourcev1beta2.ResourceClaimStatus) (errs field.ErrorList) { + // don't revalidate unchanged data + if op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) { + return nil + } + // call the type's validation function + errs = append(errs, Validate_ResourceClaimStatus(ctx, op, fldPath, obj, oldObj)...) + return + }(fldPath.Child("status"), &obj.Status, safe.Field(oldObj, func(oldObj *resourcev1beta2.ResourceClaim) *resourcev1beta2.ResourceClaimStatus { + return &oldObj.Status + }))...) + + return errs +} + +// Validate_ResourceClaimList validates an instance of ResourceClaimList according +// to declarative validation rules in the API schema. +func Validate_ResourceClaimList(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *resourcev1beta2.ResourceClaimList) (errs field.ErrorList) { + // field resourcev1beta2.ResourceClaimList.TypeMeta has no validation + // field resourcev1beta2.ResourceClaimList.ListMeta has no validation + + // field resourcev1beta2.ResourceClaimList.Items + errs = append(errs, + func(fldPath *field.Path, obj, oldObj []resourcev1beta2.ResourceClaim) (errs field.ErrorList) { + // don't revalidate unchanged data + if op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) { + return nil + } + // iterate the list and call the type's validation function + errs = append(errs, validate.EachSliceVal(ctx, op, fldPath, obj, oldObj, nil, nil, Validate_ResourceClaim)...) + return + }(fldPath.Child("items"), obj.Items, safe.Field(oldObj, func(oldObj *resourcev1beta2.ResourceClaimList) []resourcev1beta2.ResourceClaim { return oldObj.Items }))...) + + return errs +} + +// Validate_ResourceClaimStatus validates an instance of ResourceClaimStatus according +// to declarative validation rules in the API schema. +func Validate_ResourceClaimStatus(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *resourcev1beta2.ResourceClaimStatus) (errs field.ErrorList) { + // field resourcev1beta2.ResourceClaimStatus.Allocation + errs = append(errs, + func(fldPath *field.Path, obj, oldObj *resourcev1beta2.AllocationResult) (errs field.ErrorList) { + // don't revalidate unchanged data + if op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) { + return nil + } + // call field-attached validations + if e := validate.OptionalPointer(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + return // do not proceed + } + // call the type's validation function + errs = append(errs, Validate_AllocationResult(ctx, op, fldPath, obj, oldObj)...) + return + }(fldPath.Child("allocation"), obj.Allocation, safe.Field(oldObj, func(oldObj *resourcev1beta2.ResourceClaimStatus) *resourcev1beta2.AllocationResult { + return oldObj.Allocation + }))...) + + // field resourcev1beta2.ResourceClaimStatus.ReservedFor + errs = append(errs, + func(fldPath *field.Path, obj, oldObj []resourcev1beta2.ResourceClaimConsumerReference) (errs field.ErrorList) { + // don't revalidate unchanged data + if op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) { + return nil + } + // call field-attached validations + if e := validate.OptionalSlice(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + return // do not proceed + } + return + }(fldPath.Child("reservedFor"), obj.ReservedFor, safe.Field(oldObj, func(oldObj *resourcev1beta2.ResourceClaimStatus) []resourcev1beta2.ResourceClaimConsumerReference { + return oldObj.ReservedFor + }))...) + + // field resourcev1beta2.ResourceClaimStatus.Devices + errs = append(errs, + func(fldPath *field.Path, obj, oldObj []resourcev1beta2.AllocatedDeviceStatus) (errs field.ErrorList) { + // don't revalidate unchanged data + if op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) { + return nil + } + // call field-attached validations + if e := validate.OptionalSlice(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + return // do not proceed + } + return + }(fldPath.Child("devices"), obj.Devices, safe.Field(oldObj, func(oldObj *resourcev1beta2.ResourceClaimStatus) []resourcev1beta2.AllocatedDeviceStatus { + return oldObj.Devices + }))...) + + return errs +} diff --git a/staging/src/k8s.io/api/resource/v1/generated.proto b/staging/src/k8s.io/api/resource/v1/generated.proto index 816a430c26b..e88673ac8d8 100644 --- a/staging/src/k8s.io/api/resource/v1/generated.proto +++ b/staging/src/k8s.io/api/resource/v1/generated.proto @@ -826,6 +826,8 @@ message DeviceRequestAllocationResult { // DNS sub-domains separated by slashes. // // +required + // +k8s:required + // +k8s:format=k8s-resource-pool-name optional string pool = 3; // Device references one device instance via its name in the driver's @@ -1336,6 +1338,7 @@ message ResourceClaimStatus { // Allocation is set once the claim has been allocated successfully. // // +optional + // +k8s:optional optional AllocationResult allocation = 1; // ReservedFor indicates which entities are currently allowed to use @@ -1359,6 +1362,7 @@ message ResourceClaimStatus { // the future, but not reduced. // // +optional + // +k8s:optional // +listType=map // +listMapKey=uid // +patchStrategy=merge @@ -1370,6 +1374,7 @@ message ResourceClaimStatus { // information. Entries are owned by their respective drivers. // // +optional + // +k8s:optional // +listType=map // +listMapKey=driver // +listMapKey=device diff --git a/staging/src/k8s.io/api/resource/v1beta1/generated.proto b/staging/src/k8s.io/api/resource/v1beta1/generated.proto index 6ce65b4d812..999e0b784c5 100644 --- a/staging/src/k8s.io/api/resource/v1beta1/generated.proto +++ b/staging/src/k8s.io/api/resource/v1beta1/generated.proto @@ -947,6 +947,8 @@ message DeviceRequestAllocationResult { // DNS sub-domains separated by slashes. // // +required + // +k8s:required + // +k8s:format=k8s-resource-pool-name optional string pool = 3; // Device references one device instance via its name in the driver's @@ -1350,6 +1352,7 @@ message ResourceClaimStatus { // Allocation is set once the claim has been allocated successfully. // // +optional + // +k8s:optional optional AllocationResult allocation = 1; // ReservedFor indicates which entities are currently allowed to use @@ -1373,6 +1376,7 @@ message ResourceClaimStatus { // the future, but not reduced. // // +optional + // +k8s:optional // +listType=map // +listMapKey=uid // +patchStrategy=merge @@ -1384,6 +1388,7 @@ message ResourceClaimStatus { // information. Entries are owned by their respective drivers. // // +optional + // +k8s:optional // +listType=map // +listMapKey=driver // +listMapKey=device diff --git a/staging/src/k8s.io/api/resource/v1beta2/generated.proto b/staging/src/k8s.io/api/resource/v1beta2/generated.proto index 213a5615a7b..10ffeca2eb1 100644 --- a/staging/src/k8s.io/api/resource/v1beta2/generated.proto +++ b/staging/src/k8s.io/api/resource/v1beta2/generated.proto @@ -826,6 +826,8 @@ message DeviceRequestAllocationResult { // DNS sub-domains separated by slashes. // // +required + // +k8s:required + // +k8s:format=k8s-resource-pool-name optional string pool = 3; // Device references one device instance via its name in the driver's @@ -1336,6 +1338,7 @@ message ResourceClaimStatus { // Allocation is set once the claim has been allocated successfully. // // +optional + // +k8s:optional optional AllocationResult allocation = 1; // ReservedFor indicates which entities are currently allowed to use @@ -1359,6 +1362,7 @@ message ResourceClaimStatus { // the future, but not reduced. // // +optional + // +k8s:optional // +listType=map // +listMapKey=uid // +patchStrategy=merge @@ -1370,6 +1374,7 @@ message ResourceClaimStatus { // information. Entries are owned by their respective drivers. // // +optional + // +k8s:optional // +listType=map // +listMapKey=driver // +listMapKey=device From 7efc77f493a37e640c52a619880cd517c199f676 Mon Sep 17 00:00:00 2001 From: Joe Betz Date: Wed, 17 Sep 2025 20:20:21 -0400 Subject: [PATCH 7/7] Apply feedback --- pkg/apis/resource/validation/validation.go | 8 ++++---- pkg/registry/resource/resourceclaim/strategy.go | 3 ++- .../cmd/validation-gen/validators/format.go | 10 +++++----- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/pkg/apis/resource/validation/validation.go b/pkg/apis/resource/validation/validation.go index f336910497b..c6ceebe0ba7 100644 --- a/pkg/apis/resource/validation/validation.go +++ b/pkg/apis/resource/validation/validation.go @@ -64,14 +64,14 @@ var ( func validatePoolName(name string, fldPath *field.Path) field.ErrorList { var allErrs field.ErrorList if name == "" { - allErrs = append(allErrs, field.Required(fldPath, "").MarkCoveredByDeclarative()) + allErrs = append(allErrs, field.Required(fldPath, "")) } else { if len(name) > resource.PoolNameMaxLength { - allErrs = append(allErrs, field.TooLong(fldPath, "" /*unused*/, resource.PoolNameMaxLength).WithOrigin("format=k8s-resource-pool-name").MarkCoveredByDeclarative()) + allErrs = append(allErrs, field.TooLong(fldPath, "" /*unused*/, resource.PoolNameMaxLength).WithOrigin("format=k8s-resource-pool-name")) } parts := strings.Split(name, "/") for _, part := range parts { - allErrs = append(allErrs, corevalidation.ValidateDNS1123Subdomain(part, fldPath).WithOrigin("format=k8s-resource-pool-name").MarkCoveredByDeclarative()...) + allErrs = append(allErrs, corevalidation.ValidateDNS1123Subdomain(part, fldPath).WithOrigin("format=k8s-resource-pool-name")...) } } return allErrs @@ -1208,7 +1208,7 @@ func truncateIfTooLong(str string, maxLen int) string { func validateDeviceStatus(device resource.AllocatedDeviceStatus, fldPath *field.Path, allocatedDevices sets.Set[structured.SharedDeviceID]) field.ErrorList { var allErrs field.ErrorList allErrs = append(allErrs, validateDriverName(device.Driver, fldPath.Child("driver"))...) - allErrs = append(allErrs, validatePoolName(device.Pool, fldPath.Child("pool"))...) + allErrs = append(allErrs, validatePoolName(device.Pool, fldPath.Child("pool")).MarkCoveredByDeclarative()...) allErrs = append(allErrs, validateDeviceName(device.Device, fldPath.Child("device"))...) if device.ShareID != nil { allErrs = append(allErrs, validateUID(*device.ShareID, fldPath.Child("shareID"))...) diff --git a/pkg/registry/resource/resourceclaim/strategy.go b/pkg/registry/resource/resourceclaim/strategy.go index 94887cd4dd4..58bee514cf7 100644 --- a/pkg/registry/resource/resourceclaim/strategy.go +++ b/pkg/registry/resource/resourceclaim/strategy.go @@ -219,7 +219,8 @@ func (r *resourceclaimStatusStrategy) ValidateUpdate(ctx context.Context, obj, o // Run declarative update validation with panic recovery declarativeErrs := rest.ValidateUpdateDeclaratively(ctx, legacyscheme.Scheme, newClaim, oldClaim, rest.WithTakeover(takeover)) // Compare imperative and declarative errors and emit metric if there's a mismatch - rest.CompareDeclarativeErrorsAndEmitMismatches(ctx, errs, declarativeErrs, takeover, "dc_status_update") + const validationIdentifier = "resourceclaim_status_update" + rest.CompareDeclarativeErrorsAndEmitMismatches(ctx, errs, declarativeErrs, takeover, validationIdentifier) // Only apply declarative errors if takeover is enabled if takeover { diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/format.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/format.go index c73f9670a86..2f7cc15988f 100644 --- a/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/format.go +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/format.go @@ -91,10 +91,10 @@ func getFormatValidationFunction(format string) (FunctionGen, error) { return Function(formatTagName, DefaultFlags, labelValueValidator), nil case "k8s-long-name": return Function(formatTagName, DefaultFlags, longNameValidator), nil - case "k8s-resource-pool-name": - return Function(formatTagName, DefaultFlags, resourcePoolNameValidator), nil case "k8s-long-name-caseless": return Function(formatTagName, DefaultFlags, longNameCaselessValidator), nil + case "k8s-resource-pool-name": + return Function(formatTagName, DefaultFlags, resourcePoolNameValidator), nil case "k8s-short-name": return Function(formatTagName, DefaultFlags, shortNameValidator), nil case "k8s-uuid": @@ -122,12 +122,12 @@ func (ftv formatTagValidator) Docs() TagDoc { }, { Description: "k8s-long-name", Docs: "This field holds a Kubernetes \"long name\", aka a \"DNS subdomain\" value.", - }, { - Description: "k8s-resource-pool-name", - Docs: "This field holds value with one or more Kubernetes \"long name\" parts separated by `/` and no longer than 253 characters.", }, { Description: "k8s-long-name-caseless", Docs: "Deprecated: This field holds a case-insensitive Kubernetes \"long name\", aka a \"DNS subdomain\" value.", + }, { + Description: "k8s-resource-pool-name", + Docs: "This field holds value with one or more Kubernetes \"long name\" parts separated by `/` and no longer than 253 characters.", }, { Description: "k8s-short-name", Docs: "This field holds a Kubernetes \"short name\", aka a \"DNS label\" value.",