diff --git a/pkg/api/testing/validation_test.go b/pkg/api/testing/validation_test.go index 922fb526f76..82f9fcbf106 100644 --- a/pkg/api/testing/validation_test.go +++ b/pkg/api/testing/validation_test.go @@ -35,6 +35,9 @@ func TestVersionedValidationByFuzzing(t *testing.T) { {Group: "certificates.k8s.io", Version: "v1"}, {Group: "certificates.k8s.io", Version: "v1alpha1"}, {Group: "certificates.k8s.io", Version: "v1beta1"}, + {Group: "resource.k8s.io", Version: "v1beta1"}, + {Group: "resource.k8s.io", Version: "v1beta2"}, + {Group: "resource.k8s.io", Version: "v1"}, } for _, gv := range typesWithDeclarativeValidation { diff --git a/pkg/apis/resource/v1/doc.go b/pkg/apis/resource/v1/doc.go index 9166fefdbd6..d608dbef1ef 100644 --- a/pkg/apis/resource/v1/doc.go +++ b/pkg/apis/resource/v1/doc.go @@ -18,6 +18,8 @@ limitations under the License. // +k8s:conversion-gen-external-types=k8s.io/api/resource/v1 // +k8s:defaulter-gen=TypeMeta // +k8s:defaulter-gen-input=k8s.io/api/resource/v1 +// +k8s:validation-gen=TypeMeta +// +k8s:validation-gen-input=k8s.io/api/resource/v1 // Package v1 is the v1 version of the resource API. package v1 diff --git a/pkg/apis/resource/v1/zz_generated.validations.go b/pkg/apis/resource/v1/zz_generated.validations.go new file mode 100644 index 00000000000..73e28035dba --- /dev/null +++ b/pkg/apis/resource/v1/zz_generated.validations.go @@ -0,0 +1,34 @@ +//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 v1 + +import ( + runtime "k8s.io/apimachinery/pkg/runtime" +) + +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 { + return nil +} diff --git a/pkg/apis/resource/v1beta1/doc.go b/pkg/apis/resource/v1beta1/doc.go index f014667f8da..f95eb5f3c9f 100644 --- a/pkg/apis/resource/v1beta1/doc.go +++ b/pkg/apis/resource/v1beta1/doc.go @@ -18,6 +18,8 @@ limitations under the License. // +k8s:conversion-gen-external-types=k8s.io/api/resource/v1beta1 // +k8s:defaulter-gen=TypeMeta // +k8s:defaulter-gen-input=k8s.io/api/resource/v1beta1 +// +k8s:validation-gen=TypeMeta +// +k8s:validation-gen-input=k8s.io/api/resource/v1beta1 // Package v1beta1 is the v1beta1 version of the resource API. package v1beta1 diff --git a/pkg/apis/resource/v1beta1/zz_generated.validations.go b/pkg/apis/resource/v1beta1/zz_generated.validations.go new file mode 100644 index 00000000000..0b9e874b635 --- /dev/null +++ b/pkg/apis/resource/v1beta1/zz_generated.validations.go @@ -0,0 +1,34 @@ +//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 v1beta1 + +import ( + runtime "k8s.io/apimachinery/pkg/runtime" +) + +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 { + return nil +} diff --git a/pkg/apis/resource/v1beta2/doc.go b/pkg/apis/resource/v1beta2/doc.go index beea4f266b9..ca447ac7154 100644 --- a/pkg/apis/resource/v1beta2/doc.go +++ b/pkg/apis/resource/v1beta2/doc.go @@ -18,6 +18,8 @@ limitations under the License. // +k8s:conversion-gen-external-types=k8s.io/api/resource/v1beta2 // +k8s:defaulter-gen=TypeMeta // +k8s:defaulter-gen-input=k8s.io/api/resource/v1beta2 +// +k8s:validation-gen=TypeMeta +// +k8s:validation-gen-input=k8s.io/api/resource/v1beta2 // Package v1beta2 is the v1beta2 version of the resource API. package v1beta2 diff --git a/pkg/apis/resource/v1beta2/zz_generated.validations.go b/pkg/apis/resource/v1beta2/zz_generated.validations.go new file mode 100644 index 00000000000..2b709ed6643 --- /dev/null +++ b/pkg/apis/resource/v1beta2/zz_generated.validations.go @@ -0,0 +1,34 @@ +//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 v1beta2 + +import ( + runtime "k8s.io/apimachinery/pkg/runtime" +) + +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 { + return nil +} diff --git a/pkg/registry/resource/resourceclaim/declarative_validation_test.go b/pkg/registry/resource/resourceclaim/declarative_validation_test.go new file mode 100644 index 00000000000..baabe9e9f53 --- /dev/null +++ b/pkg/registry/resource/resourceclaim/declarative_validation_test.go @@ -0,0 +1,123 @@ +/* +Copyright 2025 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. +*/ + +package resourceclaim + +import ( + "testing" + + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/validation/field" + genericapirequest "k8s.io/apiserver/pkg/endpoints/request" + "k8s.io/client-go/kubernetes/fake" + apitesting "k8s.io/kubernetes/pkg/api/testing" + "k8s.io/kubernetes/pkg/apis/resource" +) + +var apiVersions = []string{"v1beta1", "v1beta2", "v1"} // "v1alpha3" is excluded because it doesn't have ResourceClaim + +func TestDeclarativeValidate(t *testing.T) { + for _, apiVersion := range apiVersions { + t.Run(apiVersion, func(t *testing.T) { + testDeclarativeValidate(t, apiVersion) + }) + } +} + +func testDeclarativeValidate(t *testing.T, apiVersion string) { + ctx := genericapirequest.WithRequestInfo(genericapirequest.NewDefaultContext(), &genericapirequest.RequestInfo{ + APIGroup: "resource.k8s.io", + APIVersion: apiVersion, + Resource: "resourceclaims", + }) + fakeClient := fake.NewClientset() + mockNSClient := fakeClient.CoreV1().Namespaces() + Strategy := NewStrategy(mockNSClient) + testCases := map[string]struct { + input resource.ResourceClaim + expectedErrs field.ErrorList + }{ + "valid": { + input: mkValidResourceClaim(), + }, + // TODO: Add more test cases + } + for k, tc := range testCases { + t.Run(k, func(t *testing.T) { + apitesting.VerifyValidationEquivalence(t, ctx, &tc.input, Strategy.Validate, tc.expectedErrs) + }) + } +} + +func TestDeclarativeValidateUpdate(t *testing.T) { + for _, apiVersion := range apiVersions { + t.Run(apiVersion, func(t *testing.T) { + testDeclarativeValidateUpdate(t, apiVersion) + }) + } +} + +func testDeclarativeValidateUpdate(t *testing.T, apiVersion string) { + ctx := genericapirequest.WithRequestInfo(genericapirequest.NewDefaultContext(), &genericapirequest.RequestInfo{ + APIGroup: "resource.k8s.io", + APIVersion: apiVersion, + Resource: "resourceclaims", + }) + fakeClient := fake.NewClientset() + mockNSClient := fakeClient.CoreV1().Namespaces() + Strategy := NewStrategy(mockNSClient) + validClaim := mkValidResourceClaim() + testCases := map[string]struct { + update resource.ResourceClaim + old resource.ResourceClaim + expectedErrs field.ErrorList + }{ + "valid": { + update: validClaim, + old: validClaim, + }, + // TODO: Add more test cases + } + for k, tc := range testCases { + t.Run(k, func(t *testing.T) { + tc.old.ResourceVersion = "1" + tc.update.ResourceVersion = "2" + 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", + }, + Spec: resource.ResourceClaimSpec{ + Devices: resource.DeviceClaim{ + Requests: []resource.DeviceRequest{ + { + Name: "req-0", + Exactly: &resource.ExactDeviceRequest{ + DeviceClassName: "class", + AllocationMode: resource.DeviceAllocationModeAll, + }, + }, + }, + }, + }, + } +} diff --git a/pkg/registry/resource/resourceclaim/strategy.go b/pkg/registry/resource/resourceclaim/strategy.go index 5b57d4c842a..32aa15fb0a4 100644 --- a/pkg/registry/resource/resourceclaim/strategy.go +++ b/pkg/registry/resource/resourceclaim/strategy.go @@ -28,6 +28,7 @@ import ( "k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/util/validation/field" "k8s.io/apiserver/pkg/registry/generic" + "k8s.io/apiserver/pkg/registry/rest" "k8s.io/apiserver/pkg/storage" "k8s.io/apiserver/pkg/storage/names" utilfeature "k8s.io/apiserver/pkg/util/feature" @@ -96,7 +97,18 @@ func (s *resourceclaimStrategy) Validate(ctx context.Context, obj runtime.Object claim := obj.(*resource.ResourceClaim) allErrs := resourceutils.AuthorizedForAdmin(ctx, claim.Spec.Devices.Requests, claim.Namespace, s.nsClient) - return append(allErrs, validation.ValidateResourceClaim(claim)...) + allErrs = append(allErrs, validation.ValidateResourceClaim(claim)...) + + if utilfeature.DefaultFeatureGate.Enabled(features.DeclarativeValidation) { + takeover := utilfeature.DefaultFeatureGate.Enabled(features.DeclarativeValidationTakeover) + const validationIdentifier = "resourceclaim_create" + declarativeErrs := rest.ValidateDeclaratively(ctx, legacyscheme.Scheme, claim, rest.WithTakeover(takeover), rest.WithValidationIdentifier(validationIdentifier)) + rest.CompareDeclarativeErrorsAndEmitMismatches(ctx, allErrs, declarativeErrs, takeover, validationIdentifier) + if takeover { + allErrs = append(allErrs.RemoveCoveredByDeclarative(), declarativeErrs...) + } + } + return allErrs } func (*resourceclaimStrategy) WarningsOnCreate(ctx context.Context, obj runtime.Object) []string { @@ -123,7 +135,18 @@ func (s *resourceclaimStrategy) ValidateUpdate(ctx context.Context, obj, old run oldClaim := old.(*resource.ResourceClaim) // AuthorizedForAdmin isn't needed here because the spec is immutable. errorList := validation.ValidateResourceClaim(newClaim) - return append(errorList, validation.ValidateResourceClaimUpdate(newClaim, oldClaim)...) + errorList = append(errorList, validation.ValidateResourceClaimUpdate(newClaim, oldClaim)...) + + if utilfeature.DefaultFeatureGate.Enabled(features.DeclarativeValidation) { + takeover := utilfeature.DefaultFeatureGate.Enabled(features.DeclarativeValidationTakeover) + const validationIdentifier = "resourceclaim_update" + declarativeErrs := rest.ValidateUpdateDeclaratively(ctx, legacyscheme.Scheme, newClaim, oldClaim, rest.WithTakeover(takeover), rest.WithValidationIdentifier(validationIdentifier)) + rest.CompareDeclarativeErrorsAndEmitMismatches(ctx, errorList, declarativeErrs, takeover, validationIdentifier) + if takeover { + errorList = append(errorList.RemoveCoveredByDeclarative(), declarativeErrs...) + } + } + return errorList } func (*resourceclaimStrategy) WarningsOnUpdate(ctx context.Context, obj, old runtime.Object) []string {