From 1c24a05ce16db3f68ccfa707f49c8ff664afa728 Mon Sep 17 00:00:00 2001 From: Darshan Murthy <128639591+darshansreenivas@users.noreply.github.com> Date: Thu, 8 Jan 2026 13:41:43 -0800 Subject: [PATCH] wire network group for declarative validation and +k8s:required to IngressClassParametersReference.Name and IngressClassParametersReference.Kind (#134909) --- pkg/api/testing/validation_test.go | 11 +- pkg/apis/networking/v1/doc.go | 2 + .../networking/v1/zz_generated.validations.go | 150 ++++++++++++++ pkg/apis/networking/v1beta1/doc.go | 2 + .../v1beta1/zz_generated.validations.go | 150 ++++++++++++++ pkg/apis/networking/validation/validation.go | 26 ++- .../declarative_validation_test.go | 191 ++++++++++++++++++ .../networking/ingressclass/strategy.go | 8 +- .../k8s.io/api/networking/v1/generated.proto | 5 + staging/src/k8s.io/api/networking/v1/types.go | 5 + .../api/networking/v1beta1/generated.proto | 5 + .../k8s.io/api/networking/v1beta1/types.go | 5 + 12 files changed, 547 insertions(+), 13 deletions(-) create mode 100644 pkg/apis/networking/v1/zz_generated.validations.go create mode 100644 pkg/apis/networking/v1beta1/zz_generated.validations.go create mode 100644 pkg/registry/networking/ingressclass/declarative_validation_test.go diff --git a/pkg/api/testing/validation_test.go b/pkg/api/testing/validation_test.go index 89154a58e5a..b0d16077b9b 100644 --- a/pkg/api/testing/validation_test.go +++ b/pkg/api/testing/validation_test.go @@ -38,15 +38,17 @@ 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: "v1"}, {Group: "resource.k8s.io", Version: "v1beta1"}, {Group: "resource.k8s.io", Version: "v1beta2"}, - {Group: "resource.k8s.io", Version: "v1"}, {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: "storage.k8s.io", Version: "v1beta1"}, {Group: "node.k8s.io", Version: "v1"}, {Group: "node.k8s.io", Version: "v1alpha1"}, + {Group: "node.k8s.io", Version: "v1beta1"}, + {Group: "network.k8s.io", Version: "v1"}, + {Group: "network.k8s.io", Version: "v1beta1"}, {Group: "autoscaling", Version: "v1"}, {Group: "autoscaling", Version: "v1beta1"}, {Group: "autoscaling", Version: "v1beta2"}, @@ -74,9 +76,6 @@ func TestVersionedValidationByFuzzing(t *testing.T) { allRules := append([]field.NormalizationRule{}, resourcevalidation.ResourceNormalizationRules...) allRules = append(allRules, nodevalidation.NodeNormalizationRules...) opts = append(opts, WithNormalizationRules(allRules...)) - if gv.Group == "autoscaling" { - opts = append(opts, WithIgnoreObjectConversionErrors()) - } VerifyVersionedValidationEquivalence(t, obj, nil, opts...) diff --git a/pkg/apis/networking/v1/doc.go b/pkg/apis/networking/v1/doc.go index 36d390fe889..b64058b4a5f 100644 --- a/pkg/apis/networking/v1/doc.go +++ b/pkg/apis/networking/v1/doc.go @@ -20,5 +20,7 @@ limitations under the License. // +k8s:defaulter-gen=TypeMeta // +k8s:defaulter-gen-input=k8s.io/api/networking/v1 // +groupName=networking.k8s.io +// +k8s:validation-gen=TypeMeta +// +k8s:validation-gen-input=k8s.io/api/networking/v1 package v1 diff --git a/pkg/apis/networking/v1/zz_generated.validations.go b/pkg/apis/networking/v1/zz_generated.validations.go new file mode 100644 index 00000000000..b2186f291e3 --- /dev/null +++ b/pkg/apis/networking/v1/zz_generated.validations.go @@ -0,0 +1,150 @@ +//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" + + networkingv1 "k8s.io/api/networking/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 IngressClass + scheme.AddValidationFunc((*networkingv1.IngressClass)(nil), func(ctx context.Context, op operation.Operation, obj, oldObj interface{}) field.ErrorList { + switch op.Request.SubresourcePath() { + case "/": + return Validate_IngressClass(ctx, op, nil /* fldPath */, obj.(*networkingv1.IngressClass), safe.Cast[*networkingv1.IngressClass](oldObj)) + } + return field.ErrorList{field.InternalError(nil, fmt.Errorf("no validation found for %T, subresource: %v", obj, op.Request.SubresourcePath()))} + }) + return nil +} + +// Validate_IngressClass validates an instance of IngressClass according +// to declarative validation rules in the API schema. +func Validate_IngressClass(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *networkingv1.IngressClass) (errs field.ErrorList) { + // field networkingv1.IngressClass.TypeMeta has no validation + // field networkingv1.IngressClass.ObjectMeta has no validation + + // field networkingv1.IngressClass.Spec + errs = append(errs, + func(fldPath *field.Path, obj, oldObj *networkingv1.IngressClassSpec, 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_IngressClassSpec(ctx, op, fldPath, obj, oldObj)...) + return + }(fldPath.Child("spec"), &obj.Spec, safe.Field(oldObj, func(oldObj *networkingv1.IngressClass) *networkingv1.IngressClassSpec { return &oldObj.Spec }), oldObj != nil)...) + + return errs +} + +// Validate_IngressClassParametersReference validates an instance of IngressClassParametersReference according +// to declarative validation rules in the API schema. +func Validate_IngressClassParametersReference(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *networkingv1.IngressClassParametersReference) (errs field.ErrorList) { + // field networkingv1.IngressClassParametersReference.APIGroup has no validation + + // field networkingv1.IngressClassParametersReference.Kind + 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.RequiredValue(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + return + }(fldPath.Child("kind"), &obj.Kind, safe.Field(oldObj, func(oldObj *networkingv1.IngressClassParametersReference) *string { return &oldObj.Kind }), oldObj != nil)...) + + // field networkingv1.IngressClassParametersReference.Name + 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.RequiredValue(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + return + }(fldPath.Child("name"), &obj.Name, safe.Field(oldObj, func(oldObj *networkingv1.IngressClassParametersReference) *string { return &oldObj.Name }), oldObj != nil)...) + + // field networkingv1.IngressClassParametersReference.Scope has no validation + // field networkingv1.IngressClassParametersReference.Namespace has no validation + return errs +} + +// Validate_IngressClassSpec validates an instance of IngressClassSpec according +// to declarative validation rules in the API schema. +func Validate_IngressClassSpec(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *networkingv1.IngressClassSpec) (errs field.ErrorList) { + // field networkingv1.IngressClassSpec.Controller has no validation + + // field networkingv1.IngressClassSpec.Parameters + errs = append(errs, + func(fldPath *field.Path, obj, oldObj *networkingv1.IngressClassParametersReference, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) { + return nil + } + // call field-attached validations + earlyReturn := false + if e := validate.OptionalPointer(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + // call the type's validation function + errs = append(errs, Validate_IngressClassParametersReference(ctx, op, fldPath, obj, oldObj)...) + return + }(fldPath.Child("parameters"), obj.Parameters, safe.Field(oldObj, func(oldObj *networkingv1.IngressClassSpec) *networkingv1.IngressClassParametersReference { + return oldObj.Parameters + }), oldObj != nil)...) + + return errs +} diff --git a/pkg/apis/networking/v1beta1/doc.go b/pkg/apis/networking/v1beta1/doc.go index 914234d39e2..0cc2573f562 100644 --- a/pkg/apis/networking/v1beta1/doc.go +++ b/pkg/apis/networking/v1beta1/doc.go @@ -20,5 +20,7 @@ limitations under the License. // +k8s:defaulter-gen=TypeMeta // +k8s:defaulter-gen-input=k8s.io/api/networking/v1beta1 // +groupName=networking.k8s.io +// +k8s:validation-gen=TypeMeta +// +k8s:validation-gen-input=k8s.io/api/networking/v1beta1 package v1beta1 diff --git a/pkg/apis/networking/v1beta1/zz_generated.validations.go b/pkg/apis/networking/v1beta1/zz_generated.validations.go new file mode 100644 index 00000000000..20b603b94e2 --- /dev/null +++ b/pkg/apis/networking/v1beta1/zz_generated.validations.go @@ -0,0 +1,150 @@ +//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" + + networkingv1beta1 "k8s.io/api/networking/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 IngressClass + scheme.AddValidationFunc((*networkingv1beta1.IngressClass)(nil), func(ctx context.Context, op operation.Operation, obj, oldObj interface{}) field.ErrorList { + switch op.Request.SubresourcePath() { + case "/": + return Validate_IngressClass(ctx, op, nil /* fldPath */, obj.(*networkingv1beta1.IngressClass), safe.Cast[*networkingv1beta1.IngressClass](oldObj)) + } + return field.ErrorList{field.InternalError(nil, fmt.Errorf("no validation found for %T, subresource: %v", obj, op.Request.SubresourcePath()))} + }) + return nil +} + +// Validate_IngressClass validates an instance of IngressClass according +// to declarative validation rules in the API schema. +func Validate_IngressClass(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *networkingv1beta1.IngressClass) (errs field.ErrorList) { + // field networkingv1beta1.IngressClass.TypeMeta has no validation + // field networkingv1beta1.IngressClass.ObjectMeta has no validation + + // field networkingv1beta1.IngressClass.Spec + errs = append(errs, + func(fldPath *field.Path, obj, oldObj *networkingv1beta1.IngressClassSpec, 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_IngressClassSpec(ctx, op, fldPath, obj, oldObj)...) + return + }(fldPath.Child("spec"), &obj.Spec, safe.Field(oldObj, func(oldObj *networkingv1beta1.IngressClass) *networkingv1beta1.IngressClassSpec { return &oldObj.Spec }), oldObj != nil)...) + + return errs +} + +// Validate_IngressClassParametersReference validates an instance of IngressClassParametersReference according +// to declarative validation rules in the API schema. +func Validate_IngressClassParametersReference(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *networkingv1beta1.IngressClassParametersReference) (errs field.ErrorList) { + // field networkingv1beta1.IngressClassParametersReference.APIGroup has no validation + + // field networkingv1beta1.IngressClassParametersReference.Kind + 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.RequiredValue(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + return + }(fldPath.Child("kind"), &obj.Kind, safe.Field(oldObj, func(oldObj *networkingv1beta1.IngressClassParametersReference) *string { return &oldObj.Kind }), oldObj != nil)...) + + // field networkingv1beta1.IngressClassParametersReference.Name + 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.RequiredValue(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + return + }(fldPath.Child("name"), &obj.Name, safe.Field(oldObj, func(oldObj *networkingv1beta1.IngressClassParametersReference) *string { return &oldObj.Name }), oldObj != nil)...) + + // field networkingv1beta1.IngressClassParametersReference.Scope has no validation + // field networkingv1beta1.IngressClassParametersReference.Namespace has no validation + return errs +} + +// Validate_IngressClassSpec validates an instance of IngressClassSpec according +// to declarative validation rules in the API schema. +func Validate_IngressClassSpec(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *networkingv1beta1.IngressClassSpec) (errs field.ErrorList) { + // field networkingv1beta1.IngressClassSpec.Controller has no validation + + // field networkingv1beta1.IngressClassSpec.Parameters + errs = append(errs, + func(fldPath *field.Path, obj, oldObj *networkingv1beta1.IngressClassParametersReference, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) { + return nil + } + // call field-attached validations + earlyReturn := false + if e := validate.OptionalPointer(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + // call the type's validation function + errs = append(errs, Validate_IngressClassParametersReference(ctx, op, fldPath, obj, oldObj)...) + return + }(fldPath.Child("parameters"), obj.Parameters, safe.Field(oldObj, func(oldObj *networkingv1beta1.IngressClassSpec) *networkingv1beta1.IngressClassParametersReference { + return oldObj.Parameters + }), oldObj != nil)...) + + return errs +} diff --git a/pkg/apis/networking/validation/validation.go b/pkg/apis/networking/validation/validation.go index 31e5b29a2f5..682e9ad8c85 100644 --- a/pkg/apis/networking/validation/validation.go +++ b/pkg/apis/networking/validation/validation.go @@ -632,11 +632,27 @@ func validateIngressClassParametersReference(params *networking.IngressClassPara return allErrs } - allErrs = append(allErrs, validateIngressTypedLocalObjectReference(&api.TypedLocalObjectReference{ - APIGroup: params.APIGroup, - Kind: params.Kind, - Name: params.Name, - }, fldPath)...) + if params.APIGroup != nil { + for _, msg := range validation.IsDNS1123Subdomain(*params.APIGroup) { + allErrs = append(allErrs, field.Invalid(fldPath.Child("apiGroup"), *params.APIGroup, msg)) + } + } + + if params.Kind == "" { + allErrs = append(allErrs, field.Required(fldPath.Child("kind"), "")).MarkCoveredByDeclarative() + } else { + for _, msg := range pathvalidation.IsValidPathSegmentName(params.Kind) { + allErrs = append(allErrs, field.Invalid(fldPath.Child("kind"), params.Kind, msg)) + } + } + + if params.Name == "" { + allErrs = append(allErrs, field.Required(fldPath.Child("name"), "")).MarkCoveredByDeclarative() + } else { + for _, msg := range pathvalidation.IsValidPathSegmentName(params.Name) { + allErrs = append(allErrs, field.Invalid(fldPath.Child("name"), params.Name, msg)) + } + } if params.Scope == nil { allErrs = append(allErrs, field.Required(fldPath.Child("scope"), "")) diff --git a/pkg/registry/networking/ingressclass/declarative_validation_test.go b/pkg/registry/networking/ingressclass/declarative_validation_test.go new file mode 100644 index 00000000000..fcb94a93233 --- /dev/null +++ b/pkg/registry/networking/ingressclass/declarative_validation_test.go @@ -0,0 +1,191 @@ +/* +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 ingressclass + +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" + networking "k8s.io/kubernetes/pkg/apis/networking" +) + +var apiVersions = []string{"v1", "v1beta1"} + +func TestDeclarativeValidateParameter(t *testing.T) { + for _, apiVersion := range apiVersions { + t.Run(apiVersion, func(t *testing.T) { + ctx := genericapirequest.WithRequestInfo( + genericapirequest.NewDefaultContext(), + &genericapirequest.RequestInfo{ + APIGroup: "networking.k8s.io", + APIVersion: apiVersion, + Resource: "ingressclasses", + IsResourceRequest: true, + Verb: "create", + }, + ) + + testCases := map[string]struct { + input networking.IngressClass + expectedErrs field.ErrorList + }{ + "valid": { + input: mkValidIngressClass(), + }, + "nil parameters": { + input: mkValidIngressClass(func(obj *networking.IngressClass) { + obj.Spec.Parameters = nil + }), + }, + "missing parameter name": { + input: mkValidIngressClass(func(obj *networking.IngressClass) { + obj.Spec.Parameters.Name = "" + }), + expectedErrs: field.ErrorList{ + field.Required(field.NewPath("spec", "parameters", "name"), "")}, + }, + "missing parameter kind": { + input: mkValidIngressClass(func(obj *networking.IngressClass) { + obj.Spec.Parameters.Kind = "" + }), + expectedErrs: field.ErrorList{ + field.Required(field.NewPath("spec", "parameters", "kind"), ""), + }, + }, + } + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + apitesting.VerifyValidationEquivalence( + t, + ctx, + &tc.input, + Strategy.Validate, + tc.expectedErrs, + ) + }) + } + }) + } +} + +func TestDeclarativeValidateUpdateParameters(t *testing.T) { + for _, apiVersion := range apiVersions { + t.Run(apiVersion, func(t *testing.T) { + testCases := map[string]struct { + oldObj networking.IngressClass + updateObj networking.IngressClass + expectedErrs field.ErrorList + }{ + "valid update": { + oldObj: mkValidIngressClass(func(obj *networking.IngressClass) { + obj.ResourceVersion = "1" + }), + updateObj: mkValidIngressClass(func(obj *networking.IngressClass) { + obj.ResourceVersion = "1" + }), + }, + "nil parameters update": { + oldObj: mkValidIngressClass(func(obj *networking.IngressClass) { + obj.ResourceVersion = "1" + }), + updateObj: mkValidIngressClass(func(obj *networking.IngressClass) { + obj.ResourceVersion = "1" + obj.Spec.Parameters = nil + }), + }, + "update fails when parameters name is cleared": { + oldObj: mkValidIngressClass(func(obj *networking.IngressClass) { + obj.ResourceVersion = "1" + }), + updateObj: mkValidIngressClass(func(obj *networking.IngressClass) { + obj.ResourceVersion = "1" + obj.Spec.Parameters.Name = "" + }), + expectedErrs: field.ErrorList{ + field.Required(field.NewPath("spec", "parameters", "name"), ""), + }, + }, + "update fails when parameters kind is cleared": { + oldObj: mkValidIngressClass(func(obj *networking.IngressClass) { + obj.ResourceVersion = "1" + }), + updateObj: mkValidIngressClass(func(obj *networking.IngressClass) { + obj.ResourceVersion = "1" + obj.Spec.Parameters.Kind = "" + }), + expectedErrs: field.ErrorList{ + field.Required(field.NewPath("spec", "parameters", "kind"), ""), + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + ctx := genericapirequest.WithRequestInfo( + genericapirequest.NewDefaultContext(), + &genericapirequest.RequestInfo{ + APIPrefix: "apis", + APIGroup: "networking.k8s.io", + APIVersion: apiVersion, + Resource: "ingressclasses", + Name: "valid-ingress-class", + IsResourceRequest: true, + Verb: "update", + }, + ) + apitesting.VerifyUpdateValidationEquivalence( + t, + ctx, + &tc.updateObj, + &tc.oldObj, + Strategy.ValidateUpdate, + tc.expectedErrs, + ) + }) + } + }) + } +} + +func mkValidIngressClass(tweaks ...func(obj *networking.IngressClass)) networking.IngressClass { + apiGroup := "example.com" + scope := networking.IngressClassParametersReferenceScopeCluster + + obj := networking.IngressClass{ + ObjectMeta: metav1.ObjectMeta{ + Name: "valid-ingress-class", + }, + Spec: networking.IngressClassSpec{ + Controller: "example.com/ingress-controller", + Parameters: &networking.IngressClassParametersReference{ + APIGroup: &apiGroup, + Kind: "IngressParameters", + Name: "valid-params", + Scope: &scope, + Namespace: nil, + }, + }, + } + + for _, tweak := range tweaks { + tweak(&obj) + } + return obj +} diff --git a/pkg/registry/networking/ingressclass/strategy.go b/pkg/registry/networking/ingressclass/strategy.go index 4e1b22688ca..15ca00da8e0 100644 --- a/pkg/registry/networking/ingressclass/strategy.go +++ b/pkg/registry/networking/ingressclass/strategy.go @@ -18,6 +18,8 @@ package ingressclass import ( "context" + "k8s.io/apimachinery/pkg/api/operation" + "k8s.io/apiserver/pkg/registry/rest" apiequality "k8s.io/apimachinery/pkg/api/equality" "k8s.io/apimachinery/pkg/runtime" @@ -67,7 +69,8 @@ func (ingressClassStrategy) PrepareForUpdate(ctx context.Context, obj, old runti // Validate validates a new IngressClass. func (ingressClassStrategy) Validate(ctx context.Context, obj runtime.Object) field.ErrorList { ingressClass := obj.(*networking.IngressClass) - return validation.ValidateIngressClass(ingressClass) + allErrs := validation.ValidateIngressClass(ingressClass) + return rest.ValidateDeclarativelyWithMigrationChecks(ctx, legacyscheme.Scheme, ingressClass, nil, allErrs, operation.Create) } // WarningsOnCreate returns warnings for the creation of the given object. @@ -90,7 +93,8 @@ func (ingressClassStrategy) ValidateUpdate(ctx context.Context, obj, old runtime newIngressClass := obj.(*networking.IngressClass) oldIngressClass := old.(*networking.IngressClass) - return validation.ValidateIngressClassUpdate(newIngressClass, oldIngressClass) + allErrs := validation.ValidateIngressClassUpdate(newIngressClass, oldIngressClass) + return rest.ValidateDeclarativelyWithMigrationChecks(ctx, legacyscheme.Scheme, newIngressClass, oldIngressClass, allErrs, operation.Update) } // WarningsOnUpdate returns warnings for the given update. diff --git a/staging/src/k8s.io/api/networking/v1/generated.proto b/staging/src/k8s.io/api/networking/v1/generated.proto index 16a2792aa64..2231eda22ad 100644 --- a/staging/src/k8s.io/api/networking/v1/generated.proto +++ b/staging/src/k8s.io/api/networking/v1/generated.proto @@ -199,9 +199,13 @@ message IngressClassParametersReference { optional string aPIGroup = 1; // kind is the type of resource being referenced. + // +required + // +k8s:required optional string kind = 2; // name is the name of resource being referenced. + // +required + // +k8s:required optional string name = 3; // scope represents if this refers to a cluster or namespace scoped resource. @@ -230,6 +234,7 @@ message IngressClassSpec { // configuration for the controller. This is optional if the controller does // not require extra parameters. // +optional + // +k8s:optional optional IngressClassParametersReference parameters = 2; } diff --git a/staging/src/k8s.io/api/networking/v1/types.go b/staging/src/k8s.io/api/networking/v1/types.go index 7d9a4fc94cb..0188ee0fd5b 100644 --- a/staging/src/k8s.io/api/networking/v1/types.go +++ b/staging/src/k8s.io/api/networking/v1/types.go @@ -583,6 +583,7 @@ type IngressClassSpec struct { // configuration for the controller. This is optional if the controller does // not require extra parameters. // +optional + // +k8s:optional Parameters *IngressClassParametersReference `json:"parameters,omitempty" protobuf:"bytes,2,opt,name=parameters"` } @@ -605,9 +606,13 @@ type IngressClassParametersReference struct { APIGroup *string `json:"apiGroup,omitempty" protobuf:"bytes,1,opt,name=aPIGroup"` // kind is the type of resource being referenced. + // +required + // +k8s:required Kind string `json:"kind" protobuf:"bytes,2,opt,name=kind"` // name is the name of resource being referenced. + // +required + // +k8s:required Name string `json:"name" protobuf:"bytes,3,opt,name=name"` // scope represents if this refers to a cluster or namespace scoped resource. diff --git a/staging/src/k8s.io/api/networking/v1beta1/generated.proto b/staging/src/k8s.io/api/networking/v1beta1/generated.proto index 3368dcaec3f..7cef8b90cb7 100644 --- a/staging/src/k8s.io/api/networking/v1beta1/generated.proto +++ b/staging/src/k8s.io/api/networking/v1beta1/generated.proto @@ -186,9 +186,13 @@ message IngressClassParametersReference { optional string aPIGroup = 1; // kind is the type of resource being referenced. + // +required + // +k8s:required optional string kind = 2; // name is the name of resource being referenced. + // +required + // +k8s:required optional string name = 3; // scope represents if this refers to a cluster or namespace scoped resource. @@ -216,6 +220,7 @@ message IngressClassSpec { // configuration for the controller. This is optional if the controller does // not require extra parameters. // +optional + // +k8s:optional optional IngressClassParametersReference parameters = 2; } diff --git a/staging/src/k8s.io/api/networking/v1beta1/types.go b/staging/src/k8s.io/api/networking/v1beta1/types.go index cd7126a5a8e..a49c678802c 100644 --- a/staging/src/k8s.io/api/networking/v1beta1/types.go +++ b/staging/src/k8s.io/api/networking/v1beta1/types.go @@ -368,6 +368,7 @@ type IngressClassSpec struct { // configuration for the controller. This is optional if the controller does // not require extra parameters. // +optional + // +k8s:optional Parameters *IngressClassParametersReference `json:"parameters,omitempty" protobuf:"bytes,2,opt,name=parameters"` } @@ -390,9 +391,13 @@ type IngressClassParametersReference struct { APIGroup *string `json:"apiGroup,omitempty" protobuf:"bytes,1,opt,name=aPIGroup"` // kind is the type of resource being referenced. + // +required + // +k8s:required Kind string `json:"kind" protobuf:"bytes,2,opt,name=kind"` // name is the name of resource being referenced. + // +required + // +k8s:required Name string `json:"name" protobuf:"bytes,3,opt,name=name"` // scope represents if this refers to a cluster or namespace scoped resource.