From 897adb7b48cb79d5c48dcbd98f05c0499d0303c4 Mon Sep 17 00:00:00 2001 From: darshansreenivas Date: Mon, 24 Nov 2025 11:30:28 -0800 Subject: [PATCH] DRA: adding tag to node module move DRA to a update method fix boilerplate code apply node normalize code at fuzz logic adding normalization fix fix nit --- pkg/api/testing/validation_test.go | 12 +- pkg/apis/node/v1/doc.go | 3 + pkg/apis/node/v1/zz_generated.validations.go | 111 +++++++++++++++ pkg/apis/node/v1alpha1/doc.go | 3 + .../node/v1alpha1/zz_generated.validations.go | 129 +++++++++++++++++ pkg/apis/node/v1beta1/doc.go | 3 + .../node/v1beta1/zz_generated.validations.go | 111 +++++++++++++++ pkg/apis/node/validation/validation.go | 14 +- .../declarative_validation_test.go | 130 ++++++++++++++++++ pkg/registry/node/runtimeclass/strategy.go | 9 +- .../src/k8s.io/api/node/v1/generated.proto | 2 + staging/src/k8s.io/api/node/v1/types.go | 2 + .../k8s.io/api/node/v1alpha1/generated.proto | 2 + staging/src/k8s.io/api/node/v1alpha1/types.go | 2 + .../k8s.io/api/node/v1beta1/generated.proto | 2 + staging/src/k8s.io/api/node/v1beta1/types.go | 2 + 16 files changed, 532 insertions(+), 5 deletions(-) create mode 100644 pkg/apis/node/v1/zz_generated.validations.go create mode 100644 pkg/apis/node/v1alpha1/zz_generated.validations.go create mode 100644 pkg/apis/node/v1beta1/zz_generated.validations.go create mode 100644 pkg/registry/node/runtimeclass/declarative_validation_test.go diff --git a/pkg/api/testing/validation_test.go b/pkg/api/testing/validation_test.go index 41ec0785591..de1579b41de 100644 --- a/pkg/api/testing/validation_test.go +++ b/pkg/api/testing/validation_test.go @@ -23,7 +23,9 @@ import ( "k8s.io/apimachinery/pkg/api/apitesting/fuzzer" "k8s.io/apimachinery/pkg/api/apitesting/roundtrip" "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/util/validation/field" "k8s.io/kubernetes/pkg/api/legacyscheme" + nodevalidation "k8s.io/kubernetes/pkg/apis/node/validation" resourcevalidation "k8s.io/kubernetes/pkg/apis/resource/validation" ) @@ -42,6 +44,9 @@ func TestVersionedValidationByFuzzing(t *testing.T) { {Group: "storage.k8s.io", Version: "v1"}, {Group: "storage.k8s.io", Version: "v1beta1"}, {Group: "storage.k8s.io", Version: "v1alpha1"}, + {Group: "node.k8s.io", Version: "v1beta1"}, + {Group: "node.k8s.io", Version: "v1"}, + {Group: "node.k8s.io", Version: "v1alpha1"}, } fuzzIters := *roundtrip.FuzzIters / 10 // TODO: Find a better way to manage test running time @@ -59,7 +64,12 @@ func TestVersionedValidationByFuzzing(t *testing.T) { f.Fill(obj) var opts []ValidationTestConfig - opts = append(opts, WithNormalizationRules(resourcevalidation.ResourceNormalizationRules...)) + // TODO(API group level configuration): Consider configuring normalization rules at the + // API group level to avoid potential collisions when multiple rule sets are combined. + // This would allow each API group to register its own normalization rules independently. + allRules := append([]field.NormalizationRule{}, resourcevalidation.ResourceNormalizationRules...) + allRules = append(allRules, nodevalidation.NodeNormalizationRules...) + opts = append(opts, WithNormalizationRules(allRules...)) VerifyVersionedValidationEquivalence(t, obj, nil, opts...) diff --git a/pkg/apis/node/v1/doc.go b/pkg/apis/node/v1/doc.go index 6ce4ae903b1..c0829b62289 100644 --- a/pkg/apis/node/v1/doc.go +++ b/pkg/apis/node/v1/doc.go @@ -19,4 +19,7 @@ limitations under the License. // +groupName=node.k8s.io +// +k8s:validation-gen=TypeMeta +// +k8s:validation-gen-input=k8s.io/api/node/v1 + package v1 diff --git a/pkg/apis/node/v1/zz_generated.validations.go b/pkg/apis/node/v1/zz_generated.validations.go new file mode 100644 index 00000000000..dba4eab5c07 --- /dev/null +++ b/pkg/apis/node/v1/zz_generated.validations.go @@ -0,0 +1,111 @@ +//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 ( + context "context" + fmt "fmt" + + nodev1 "k8s.io/api/node/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) } + +// RegisterValidations adds validation functions to the given scheme. +// Public to allow building arbitrary schemes. +func RegisterValidations(scheme *runtime.Scheme) error { + // type RuntimeClass + scheme.AddValidationFunc((*nodev1.RuntimeClass)(nil), func(ctx context.Context, op operation.Operation, obj, oldObj interface{}) field.ErrorList { + switch op.Request.SubresourcePath() { + case "/": + return Validate_RuntimeClass(ctx, op, nil /* fldPath */, obj.(*nodev1.RuntimeClass), safe.Cast[*nodev1.RuntimeClass](oldObj)) + } + return field.ErrorList{field.InternalError(nil, fmt.Errorf("no validation found for %T, subresource: %v", obj, op.Request.SubresourcePath()))} + }) + // type RuntimeClassList + scheme.AddValidationFunc((*nodev1.RuntimeClassList)(nil), func(ctx context.Context, op operation.Operation, obj, oldObj interface{}) field.ErrorList { + switch op.Request.SubresourcePath() { + case "/": + return Validate_RuntimeClassList(ctx, op, nil /* fldPath */, obj.(*nodev1.RuntimeClassList), safe.Cast[*nodev1.RuntimeClassList](oldObj)) + } + return field.ErrorList{field.InternalError(nil, fmt.Errorf("no validation found for %T, subresource: %v", obj, op.Request.SubresourcePath()))} + }) + return nil +} + +// Validate_RuntimeClass validates an instance of RuntimeClass according +// to declarative validation rules in the API schema. +func Validate_RuntimeClass(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *nodev1.RuntimeClass) (errs field.ErrorList) { + // field nodev1.RuntimeClass.TypeMeta has no validation + // field nodev1.RuntimeClass.ObjectMeta has no validation + + // field nodev1.RuntimeClass.Handler + errs = append(errs, + func(fldPath *field.Path, obj, oldObj *string, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) { + return nil + } + // call field-attached validations + earlyReturn := false + if e := validate.Immutable(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + errs = append(errs, validate.ShortName(ctx, op, fldPath, obj, oldObj)...) + return + }(fldPath.Child("handler"), &obj.Handler, safe.Field(oldObj, func(oldObj *nodev1.RuntimeClass) *string { return &oldObj.Handler }), oldObj != nil)...) + + // field nodev1.RuntimeClass.Overhead has no validation + // field nodev1.RuntimeClass.Scheduling has no validation + return errs +} + +// Validate_RuntimeClassList validates an instance of RuntimeClassList according +// to declarative validation rules in the API schema. +func Validate_RuntimeClassList(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *nodev1.RuntimeClassList) (errs field.ErrorList) { + // field nodev1.RuntimeClassList.TypeMeta has no validation + // field nodev1.RuntimeClassList.ListMeta has no validation + + // field nodev1.RuntimeClassList.Items + errs = append(errs, + func(fldPath *field.Path, obj, oldObj []nodev1.RuntimeClass, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && 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_RuntimeClass)...) + return + }(fldPath.Child("items"), obj.Items, safe.Field(oldObj, func(oldObj *nodev1.RuntimeClassList) []nodev1.RuntimeClass { return oldObj.Items }), oldObj != nil)...) + + return errs +} diff --git a/pkg/apis/node/v1alpha1/doc.go b/pkg/apis/node/v1alpha1/doc.go index 5bc302941a2..d29e06fff2e 100644 --- a/pkg/apis/node/v1alpha1/doc.go +++ b/pkg/apis/node/v1alpha1/doc.go @@ -19,4 +19,7 @@ limitations under the License. // +groupName=node.k8s.io +// +k8s:validation-gen=TypeMeta +// +k8s:validation-gen-input=k8s.io/api/node/v1alpha1 + package v1alpha1 diff --git a/pkg/apis/node/v1alpha1/zz_generated.validations.go b/pkg/apis/node/v1alpha1/zz_generated.validations.go new file mode 100644 index 00000000000..e7cd3b07184 --- /dev/null +++ b/pkg/apis/node/v1alpha1/zz_generated.validations.go @@ -0,0 +1,129 @@ +//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 v1alpha1 + +import ( + context "context" + fmt "fmt" + + nodev1alpha1 "k8s.io/api/node/v1alpha1" + 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) } + +// RegisterValidations adds validation functions to the given scheme. +// Public to allow building arbitrary schemes. +func RegisterValidations(scheme *runtime.Scheme) error { + // type RuntimeClass + scheme.AddValidationFunc((*nodev1alpha1.RuntimeClass)(nil), func(ctx context.Context, op operation.Operation, obj, oldObj interface{}) field.ErrorList { + switch op.Request.SubresourcePath() { + case "/": + return Validate_RuntimeClass(ctx, op, nil /* fldPath */, obj.(*nodev1alpha1.RuntimeClass), safe.Cast[*nodev1alpha1.RuntimeClass](oldObj)) + } + return field.ErrorList{field.InternalError(nil, fmt.Errorf("no validation found for %T, subresource: %v", obj, op.Request.SubresourcePath()))} + }) + // type RuntimeClassList + scheme.AddValidationFunc((*nodev1alpha1.RuntimeClassList)(nil), func(ctx context.Context, op operation.Operation, obj, oldObj interface{}) field.ErrorList { + switch op.Request.SubresourcePath() { + case "/": + return Validate_RuntimeClassList(ctx, op, nil /* fldPath */, obj.(*nodev1alpha1.RuntimeClassList), safe.Cast[*nodev1alpha1.RuntimeClassList](oldObj)) + } + return field.ErrorList{field.InternalError(nil, fmt.Errorf("no validation found for %T, subresource: %v", obj, op.Request.SubresourcePath()))} + }) + return nil +} + +// Validate_RuntimeClass validates an instance of RuntimeClass according +// to declarative validation rules in the API schema. +func Validate_RuntimeClass(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *nodev1alpha1.RuntimeClass) (errs field.ErrorList) { + // field nodev1alpha1.RuntimeClass.TypeMeta has no validation + // field nodev1alpha1.RuntimeClass.ObjectMeta has no validation + + // field nodev1alpha1.RuntimeClass.Spec + errs = append(errs, + func(fldPath *field.Path, obj, oldObj *nodev1alpha1.RuntimeClassSpec, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) { + return nil + } + // call the type's validation function + errs = append(errs, Validate_RuntimeClassSpec(ctx, op, fldPath, obj, oldObj)...) + return + }(fldPath.Child("spec"), &obj.Spec, safe.Field(oldObj, func(oldObj *nodev1alpha1.RuntimeClass) *nodev1alpha1.RuntimeClassSpec { return &oldObj.Spec }), oldObj != nil)...) + + return errs +} + +// Validate_RuntimeClassList validates an instance of RuntimeClassList according +// to declarative validation rules in the API schema. +func Validate_RuntimeClassList(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *nodev1alpha1.RuntimeClassList) (errs field.ErrorList) { + // field nodev1alpha1.RuntimeClassList.TypeMeta has no validation + // field nodev1alpha1.RuntimeClassList.ListMeta has no validation + + // field nodev1alpha1.RuntimeClassList.Items + errs = append(errs, + func(fldPath *field.Path, obj, oldObj []nodev1alpha1.RuntimeClass, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && 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_RuntimeClass)...) + return + }(fldPath.Child("items"), obj.Items, safe.Field(oldObj, func(oldObj *nodev1alpha1.RuntimeClassList) []nodev1alpha1.RuntimeClass { return oldObj.Items }), oldObj != nil)...) + + return errs +} + +// Validate_RuntimeClassSpec validates an instance of RuntimeClassSpec according +// to declarative validation rules in the API schema. +func Validate_RuntimeClassSpec(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *nodev1alpha1.RuntimeClassSpec) (errs field.ErrorList) { + // field nodev1alpha1.RuntimeClassSpec.RuntimeHandler + errs = append(errs, + func(fldPath *field.Path, obj, oldObj *string, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) { + return nil + } + // call field-attached validations + earlyReturn := false + if e := validate.Immutable(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + errs = append(errs, validate.ShortName(ctx, op, fldPath, obj, oldObj)...) + return + }(fldPath.Child("runtimeHandler"), &obj.RuntimeHandler, safe.Field(oldObj, func(oldObj *nodev1alpha1.RuntimeClassSpec) *string { return &oldObj.RuntimeHandler }), oldObj != nil)...) + + // field nodev1alpha1.RuntimeClassSpec.Overhead has no validation + // field nodev1alpha1.RuntimeClassSpec.Scheduling has no validation + return errs +} diff --git a/pkg/apis/node/v1beta1/doc.go b/pkg/apis/node/v1beta1/doc.go index 7f6a5923acf..7b5f4fc14c9 100644 --- a/pkg/apis/node/v1beta1/doc.go +++ b/pkg/apis/node/v1beta1/doc.go @@ -19,4 +19,7 @@ limitations under the License. // +groupName=node.k8s.io +// +k8s:validation-gen=TypeMeta +// +k8s:validation-gen-input=k8s.io/api/node/v1beta1 + package v1beta1 diff --git a/pkg/apis/node/v1beta1/zz_generated.validations.go b/pkg/apis/node/v1beta1/zz_generated.validations.go new file mode 100644 index 00000000000..2d0129ad982 --- /dev/null +++ b/pkg/apis/node/v1beta1/zz_generated.validations.go @@ -0,0 +1,111 @@ +//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 ( + context "context" + fmt "fmt" + + nodev1beta1 "k8s.io/api/node/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) } + +// RegisterValidations adds validation functions to the given scheme. +// Public to allow building arbitrary schemes. +func RegisterValidations(scheme *runtime.Scheme) error { + // type RuntimeClass + scheme.AddValidationFunc((*nodev1beta1.RuntimeClass)(nil), func(ctx context.Context, op operation.Operation, obj, oldObj interface{}) field.ErrorList { + switch op.Request.SubresourcePath() { + case "/": + return Validate_RuntimeClass(ctx, op, nil /* fldPath */, obj.(*nodev1beta1.RuntimeClass), safe.Cast[*nodev1beta1.RuntimeClass](oldObj)) + } + return field.ErrorList{field.InternalError(nil, fmt.Errorf("no validation found for %T, subresource: %v", obj, op.Request.SubresourcePath()))} + }) + // type RuntimeClassList + scheme.AddValidationFunc((*nodev1beta1.RuntimeClassList)(nil), func(ctx context.Context, op operation.Operation, obj, oldObj interface{}) field.ErrorList { + switch op.Request.SubresourcePath() { + case "/": + return Validate_RuntimeClassList(ctx, op, nil /* fldPath */, obj.(*nodev1beta1.RuntimeClassList), safe.Cast[*nodev1beta1.RuntimeClassList](oldObj)) + } + return field.ErrorList{field.InternalError(nil, fmt.Errorf("no validation found for %T, subresource: %v", obj, op.Request.SubresourcePath()))} + }) + return nil +} + +// Validate_RuntimeClass validates an instance of RuntimeClass according +// to declarative validation rules in the API schema. +func Validate_RuntimeClass(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *nodev1beta1.RuntimeClass) (errs field.ErrorList) { + // field nodev1beta1.RuntimeClass.TypeMeta has no validation + // field nodev1beta1.RuntimeClass.ObjectMeta has no validation + + // field nodev1beta1.RuntimeClass.Handler + errs = append(errs, + func(fldPath *field.Path, obj, oldObj *string, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) { + return nil + } + // call field-attached validations + earlyReturn := false + if e := validate.Immutable(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + errs = append(errs, validate.ShortName(ctx, op, fldPath, obj, oldObj)...) + return + }(fldPath.Child("handler"), &obj.Handler, safe.Field(oldObj, func(oldObj *nodev1beta1.RuntimeClass) *string { return &oldObj.Handler }), oldObj != nil)...) + + // field nodev1beta1.RuntimeClass.Overhead has no validation + // field nodev1beta1.RuntimeClass.Scheduling has no validation + return errs +} + +// Validate_RuntimeClassList validates an instance of RuntimeClassList according +// to declarative validation rules in the API schema. +func Validate_RuntimeClassList(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *nodev1beta1.RuntimeClassList) (errs field.ErrorList) { + // field nodev1beta1.RuntimeClassList.TypeMeta has no validation + // field nodev1beta1.RuntimeClassList.ListMeta has no validation + + // field nodev1beta1.RuntimeClassList.Items + errs = append(errs, + func(fldPath *field.Path, obj, oldObj []nodev1beta1.RuntimeClass, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && 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_RuntimeClass)...) + return + }(fldPath.Child("items"), obj.Items, safe.Field(oldObj, func(oldObj *nodev1beta1.RuntimeClassList) []nodev1beta1.RuntimeClass { return oldObj.Items }), oldObj != nil)...) + + return errs +} diff --git a/pkg/apis/node/validation/validation.go b/pkg/apis/node/validation/validation.go index 58b80b55f1c..b4a54697d3a 100644 --- a/pkg/apis/node/validation/validation.go +++ b/pkg/apis/node/validation/validation.go @@ -23,14 +23,24 @@ import ( "k8s.io/kubernetes/pkg/apis/core" corevalidation "k8s.io/kubernetes/pkg/apis/core/validation" "k8s.io/kubernetes/pkg/apis/node" + "regexp" ) +// Define normalization rules to handle field name differences across API versions +// v1alpha1 uses "spec.runtimeHandler" while v1/v1beta1 use "handler" +var NodeNormalizationRules = []field.NormalizationRule{ + { + Regexp: regexp.MustCompile(`^spec\.runtimeHandler(.*)$`), + Replacement: "handler$1", + }, +} + // ValidateRuntimeClass validates the RuntimeClass func ValidateRuntimeClass(rc *node.RuntimeClass) field.ErrorList { allErrs := apivalidation.ValidateObjectMeta(&rc.ObjectMeta, false, apivalidation.NameIsDNSSubdomain, field.NewPath("metadata")) for _, msg := range apivalidation.NameIsDNSLabel(rc.Handler, false) { - allErrs = append(allErrs, field.Invalid(field.NewPath("handler"), rc.Handler, msg)) + allErrs = append(allErrs, field.Invalid(field.NewPath("handler"), rc.Handler, msg)).MarkCoveredByDeclarative().WithOrigin("format=k8s-short-name") } if rc.Overhead != nil { @@ -47,7 +57,7 @@ func ValidateRuntimeClass(rc *node.RuntimeClass) field.ErrorList { func ValidateRuntimeClassUpdate(new, old *node.RuntimeClass) field.ErrorList { allErrs := apivalidation.ValidateObjectMetaUpdate(&new.ObjectMeta, &old.ObjectMeta, field.NewPath("metadata")) - allErrs = append(allErrs, apivalidation.ValidateImmutableField(new.Handler, old.Handler, field.NewPath("handler"))...) + allErrs = append(allErrs, apivalidation.ValidateImmutableField(new.Handler, old.Handler, field.NewPath("handler"))...).MarkCoveredByDeclarative().WithOrigin("immutable") return allErrs } diff --git a/pkg/registry/node/runtimeclass/declarative_validation_test.go b/pkg/registry/node/runtimeclass/declarative_validation_test.go new file mode 100644 index 00000000000..95bc0ca6e3b --- /dev/null +++ b/pkg/registry/node/runtimeclass/declarative_validation_test.go @@ -0,0 +1,130 @@ +/* +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 runtimeclass + +import ( + "testing" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/validation/field" + genericapirequest "k8s.io/apiserver/pkg/endpoints/request" + apitesting "k8s.io/kubernetes/pkg/api/testing" + node "k8s.io/kubernetes/pkg/apis/node" + "k8s.io/kubernetes/pkg/apis/node/validation" +) + +var apiVersions = []string{"v1", "v1beta1", "v1alpha1"} + +func mkRuntimeClassHandlerOnly(tweaks ...func(*node.RuntimeClass)) node.RuntimeClass { + rc := node.RuntimeClass{ + ObjectMeta: metav1.ObjectMeta{ + Name: "myrc", + }, + Handler: "runc", + } + for _, f := range tweaks { + f(&rc) + } + return rc +} + +func TestRuntimeClass_DeclarativeValidate_Create(t *testing.T) { + for _, apiVersion := range apiVersions { + t.Run(apiVersion, func(t *testing.T) { + ctx := genericapirequest.WithRequestInfo( + genericapirequest.NewDefaultContext(), + &genericapirequest.RequestInfo{ + APIGroup: "node.k8s.io", + APIVersion: apiVersion, + }, + ) + + tests := map[string]struct { + obj node.RuntimeClass + expectedErrs field.ErrorList + }{ + "valid handler": { + obj: mkRuntimeClassHandlerOnly(func(rc *node.RuntimeClass) { + rc.Handler = "test" + }), + expectedErrs: field.ErrorList{}, + }, + "invalid handler dns label": { + obj: mkRuntimeClassHandlerOnly(func(rc *node.RuntimeClass) { + rc.Handler = "" + }), + expectedErrs: field.ErrorList{ + field.Invalid(field.NewPath("handler"), + "", "").WithOrigin("format=k8s-short-name"), + }, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + apitesting.VerifyValidationEquivalence(t, ctx, &tc.obj, Strategy.Validate, tc.expectedErrs, + apitesting.WithNormalizationRules(validation.NodeNormalizationRules...)) + }) + } + }) + } +} + +func TestRuntimeClass_DeclarativeValidate_Update(t *testing.T) { + for _, apiVersion := range apiVersions { + t.Run(apiVersion, func(t *testing.T) { + ctx := genericapirequest.WithRequestInfo( + genericapirequest.NewDefaultContext(), + &genericapirequest.RequestInfo{ + APIGroup: "node.k8s.io", + APIVersion: apiVersion, + }, + ) + + tests := map[string]struct { + oldObj, newObj node.RuntimeClass + expectedErrs field.ErrorList + }{ + "no-op update": { + oldObj: mkRuntimeClassHandlerOnly(), + newObj: mkRuntimeClassHandlerOnly(), + expectedErrs: field.ErrorList{}, + }, + "handler changed (immutable)": { + oldObj: mkRuntimeClassHandlerOnly(func(rc *node.RuntimeClass) { + rc.Handler = "runc" + }), + newObj: mkRuntimeClassHandlerOnly(func(rc *node.RuntimeClass) { + rc.Handler = "asadsasdas" + }), + expectedErrs: field.ErrorList{ + field.Invalid(field.NewPath("handler"), "", + "").WithOrigin("immutable"), + }, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + tc.oldObj.ObjectMeta.ResourceVersion = "1" + tc.newObj.ObjectMeta.ResourceVersion = "1" + apitesting.VerifyUpdateValidationEquivalence(t, ctx, &tc.newObj, &tc.oldObj, Strategy.ValidateUpdate, tc.expectedErrs, apitesting.WithNormalizationRules(validation.NodeNormalizationRules...)) + }) + } + }) + } +} diff --git a/pkg/registry/node/runtimeclass/strategy.go b/pkg/registry/node/runtimeclass/strategy.go index 6acc70d9b30..bbfece5cdd0 100644 --- a/pkg/registry/node/runtimeclass/strategy.go +++ b/pkg/registry/node/runtimeclass/strategy.go @@ -18,6 +18,7 @@ package runtimeclass import ( "context" + "k8s.io/apimachinery/pkg/api/operation" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/validation/field" @@ -70,7 +71,9 @@ func (strategy) PrepareForUpdate(ctx context.Context, obj, old runtime.Object) { // Validate validates a new RuntimeClass. Validation must check for a correct signature. func (strategy) Validate(ctx context.Context, obj runtime.Object) field.ErrorList { runtimeClass := obj.(*node.RuntimeClass) - return validation.ValidateRuntimeClass(runtimeClass) + allErrs := validation.ValidateRuntimeClass(runtimeClass) + return rest.ValidateDeclarativelyWithMigrationChecks(ctx, legacyscheme.Scheme, runtimeClass, nil, allErrs, operation.Create, rest.WithNormalizationRules(validation.NodeNormalizationRules)) + } // WarningsOnCreate returns warnings for the creation of the given object. @@ -87,7 +90,9 @@ func (strategy) Canonicalize(obj runtime.Object) { func (strategy) ValidateUpdate(ctx context.Context, obj, old runtime.Object) field.ErrorList { newObj := obj.(*node.RuntimeClass) errorList := validation.ValidateRuntimeClass(newObj) - return append(errorList, validation.ValidateRuntimeClassUpdate(newObj, old.(*node.RuntimeClass))...) + errorList = append(errorList, validation.ValidateRuntimeClassUpdate(newObj, old.(*node.RuntimeClass))...) + return rest.ValidateDeclarativelyWithMigrationChecks(ctx, legacyscheme.Scheme, newObj, old, errorList, operation.Update, rest.WithNormalizationRules(validation.NodeNormalizationRules)) + } // WarningsOnUpdate returns warnings for the given update. diff --git a/staging/src/k8s.io/api/node/v1/generated.proto b/staging/src/k8s.io/api/node/v1/generated.proto index e6b8852ec10..64110843a50 100644 --- a/staging/src/k8s.io/api/node/v1/generated.proto +++ b/staging/src/k8s.io/api/node/v1/generated.proto @@ -59,6 +59,8 @@ message RuntimeClass { // in a pod. // The Handler must be lowercase, conform to the DNS Label (RFC 1123) requirements, // and is immutable. + // +k8s:format="k8s-short-name" + // +k8s:immutable optional string handler = 2; // overhead represents the resource overhead associated with running a pod for a diff --git a/staging/src/k8s.io/api/node/v1/types.go b/staging/src/k8s.io/api/node/v1/types.go index 169862ea940..c5209cf594d 100644 --- a/staging/src/k8s.io/api/node/v1/types.go +++ b/staging/src/k8s.io/api/node/v1/types.go @@ -50,6 +50,8 @@ type RuntimeClass struct { // in a pod. // The Handler must be lowercase, conform to the DNS Label (RFC 1123) requirements, // and is immutable. + // +k8s:format="k8s-short-name" + // +k8s:immutable Handler string `json:"handler" protobuf:"bytes,2,opt,name=handler"` // overhead represents the resource overhead associated with running a pod for a diff --git a/staging/src/k8s.io/api/node/v1alpha1/generated.proto b/staging/src/k8s.io/api/node/v1alpha1/generated.proto index bc68718d90c..93decdd4228 100644 --- a/staging/src/k8s.io/api/node/v1alpha1/generated.proto +++ b/staging/src/k8s.io/api/node/v1alpha1/generated.proto @@ -80,6 +80,8 @@ message RuntimeClassSpec { // in a pod. // The runtimeHandler must be lowercase, conform to the DNS Label (RFC 1123) // requirements, and is immutable. + // +k8s:format="k8s-short-name" + // +k8s:immutable optional string runtimeHandler = 1; // overhead represents the resource overhead associated with running a pod for a diff --git a/staging/src/k8s.io/api/node/v1alpha1/types.go b/staging/src/k8s.io/api/node/v1alpha1/types.go index bf9e284bf73..aa107e1bb3a 100644 --- a/staging/src/k8s.io/api/node/v1alpha1/types.go +++ b/staging/src/k8s.io/api/node/v1alpha1/types.go @@ -59,6 +59,8 @@ type RuntimeClassSpec struct { // in a pod. // The runtimeHandler must be lowercase, conform to the DNS Label (RFC 1123) // requirements, and is immutable. + // +k8s:format="k8s-short-name" + // +k8s:immutable RuntimeHandler string `json:"runtimeHandler" protobuf:"bytes,1,opt,name=runtimeHandler"` // overhead represents the resource overhead associated with running a pod for a diff --git a/staging/src/k8s.io/api/node/v1beta1/generated.proto b/staging/src/k8s.io/api/node/v1beta1/generated.proto index 497027e033a..d9156cd052c 100644 --- a/staging/src/k8s.io/api/node/v1beta1/generated.proto +++ b/staging/src/k8s.io/api/node/v1beta1/generated.proto @@ -59,6 +59,8 @@ message RuntimeClass { // in a pod. // The handler must be lowercase, conform to the DNS Label (RFC 1123) requirements, // and is immutable. + // +k8s:format="k8s-short-name" + // +k8s:immutable optional string handler = 2; // overhead represents the resource overhead associated with running a pod for a diff --git a/staging/src/k8s.io/api/node/v1beta1/types.go b/staging/src/k8s.io/api/node/v1beta1/types.go index 74ecca26ad4..20aefdf3ab0 100644 --- a/staging/src/k8s.io/api/node/v1beta1/types.go +++ b/staging/src/k8s.io/api/node/v1beta1/types.go @@ -51,6 +51,8 @@ type RuntimeClass struct { // in a pod. // The handler must be lowercase, conform to the DNS Label (RFC 1123) requirements, // and is immutable. + // +k8s:format="k8s-short-name" + // +k8s:immutable Handler string `json:"handler" protobuf:"bytes,2,opt,name=handler"` // overhead represents the resource overhead associated with running a pod for a