From ed083ab6b7720be33f5b74132b441580aa74c4ed Mon Sep 17 00:00:00 2001 From: Abhishek Srivastava Date: Tue, 13 Jan 2026 22:34:23 -0800 Subject: [PATCH] add declarative validation tests for ipaddress --- .../networking/v1/zz_generated.validations.go | 54 ++--- .../v1beta1/zz_generated.validations.go | 4 + pkg/apis/networking/validation/validation.go | 8 +- .../ipaddress/declarative_validation_test.go | 207 +++++++++++++++++- pkg/registry/networking/ipaddress/strategy.go | 5 +- .../networking/ipaddress/strategy_test.go | 12 +- .../k8s.io/api/networking/v1/generated.proto | 4 + staging/src/k8s.io/api/networking/v1/types.go | 1 + .../api/networking/v1beta1/generated.proto | 4 + .../k8s.io/api/networking/v1beta1/types.go | 1 + 10 files changed, 265 insertions(+), 35 deletions(-) diff --git a/pkg/apis/networking/v1/zz_generated.validations.go b/pkg/apis/networking/v1/zz_generated.validations.go index 609abaf4166..b0fd53dfc94 100644 --- a/pkg/apis/networking/v1/zz_generated.validations.go +++ b/pkg/apis/networking/v1/zz_generated.validations.go @@ -66,32 +66,6 @@ func RegisterValidations(scheme *runtime.Scheme) error { return nil } -// Validate_IPBlock validates an instance of IPBlock according -// to declarative validation rules in the API schema. -func Validate_IPBlock(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *networkingv1.IPBlock) (errs field.ErrorList) { - // field networkingv1.IPBlock.CIDR - 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("cidr"), &obj.CIDR, safe.Field(oldObj, func(oldObj *networkingv1.IPBlock) *string { return &oldObj.CIDR }), oldObj != nil)...) - - // field networkingv1.IPBlock.Except has no validation - return errs -} - // Validate_IPAddress validates an instance of IPAddress according // to declarative validation rules in the API schema. func Validate_IPAddress(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *networkingv1.IPAddress) (errs field.ErrorList) { @@ -144,6 +118,32 @@ func Validate_IPAddressSpec(ctx context.Context, op operation.Operation, fldPath return errs } +// Validate_IPBlock validates an instance of IPBlock according +// to declarative validation rules in the API schema. +func Validate_IPBlock(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *networkingv1.IPBlock) (errs field.ErrorList) { + // field networkingv1.IPBlock.CIDR + 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("cidr"), &obj.CIDR, safe.Field(oldObj, func(oldObj *networkingv1.IPBlock) *string { return &oldObj.CIDR }), oldObj != nil)...) + + // field networkingv1.IPBlock.Except has no validation + return errs +} + // 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) { @@ -418,4 +418,4 @@ func Validate_ParentReference(ctx context.Context, op operation.Operation, fldPa }(fldPath.Child("name"), &obj.Name, safe.Field(oldObj, func(oldObj *networkingv1.ParentReference) *string { return &oldObj.Name }), oldObj != nil)...) return errs -} \ No newline at end of file +} diff --git a/pkg/apis/networking/v1beta1/zz_generated.validations.go b/pkg/apis/networking/v1beta1/zz_generated.validations.go index 26e5569587a..b3fb9e0561d 100644 --- a/pkg/apis/networking/v1beta1/zz_generated.validations.go +++ b/pkg/apis/networking/v1beta1/zz_generated.validations.go @@ -91,6 +91,10 @@ func Validate_IPAddressSpec(ctx context.Context, op operation.Operation, fldPath } // 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 e := validate.RequiredPointer(ctx, op, fldPath, obj, oldObj); len(e) != 0 { errs = append(errs, e...) earlyReturn = true diff --git a/pkg/apis/networking/validation/validation.go b/pkg/apis/networking/validation/validation.go index 39349c2d190..c47bf01b6b6 100644 --- a/pkg/apis/networking/validation/validation.go +++ b/pkg/apis/networking/validation/validation.go @@ -785,7 +785,7 @@ func validateIPAddressParentReference(params *networking.ParentReference, fldPat allErrs := field.ErrorList{} if params == nil { - allErrs = append(allErrs, field.Required(fldPath.Child("parentRef"), "")) + allErrs = append(allErrs, field.Required(fldPath.Child("parentRef"), "").MarkCoveredByDeclarative()) return allErrs } @@ -799,7 +799,7 @@ func validateIPAddressParentReference(params *networking.ParentReference, fldPat // resource is required if params.Resource == "" { - allErrs = append(allErrs, field.Required(fldPath.Child("resource"), "")) + allErrs = append(allErrs, field.Required(fldPath.Child("resource"), "").MarkCoveredByDeclarative()) } else { for _, msg := range content.IsPathSegmentName(params.Resource) { allErrs = append(allErrs, field.Invalid(fldPath.Child("resource"), params.Resource, msg)) @@ -808,7 +808,7 @@ func validateIPAddressParentReference(params *networking.ParentReference, fldPat // name is required if params.Name == "" { - allErrs = append(allErrs, field.Required(fldPath.Child("name"), "")) + allErrs = append(allErrs, field.Required(fldPath.Child("name"), "").MarkCoveredByDeclarative()) } else { for _, msg := range content.IsPathSegmentName(params.Name) { allErrs = append(allErrs, field.Invalid(fldPath.Child("name"), params.Name, msg)) @@ -828,7 +828,7 @@ func validateIPAddressParentReference(params *networking.ParentReference, fldPat func ValidateIPAddressUpdate(update, old *networking.IPAddress) field.ErrorList { var allErrs field.ErrorList allErrs = append(allErrs, apivalidation.ValidateObjectMetaUpdate(&update.ObjectMeta, &old.ObjectMeta, field.NewPath("metadata"))...) - allErrs = append(allErrs, apivalidation.ValidateImmutableField(update.Spec.ParentRef, old.Spec.ParentRef, field.NewPath("spec").Child("parentRef"))...) + allErrs = append(allErrs, apivalidation.ValidateImmutableField(update.Spec.ParentRef, old.Spec.ParentRef, field.NewPath("spec").Child("parentRef")).MarkCoveredByDeclarative().WithOrigin("immutable")...) return allErrs } diff --git a/pkg/registry/networking/ipaddress/declarative_validation_test.go b/pkg/registry/networking/ipaddress/declarative_validation_test.go index 2bd1e156b0d..66ba30a2f50 100644 --- a/pkg/registry/networking/ipaddress/declarative_validation_test.go +++ b/pkg/registry/networking/ipaddress/declarative_validation_test.go @@ -1,5 +1,5 @@ /* -Copyright 2025 The Kubernetes Authors. +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. @@ -15,3 +15,208 @@ limitations under the License. */ package ipaddress + +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{"v1beta1", "v1"} + +func TestDeclarativeValidateIPAddress(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: "ipaddresses", + IsResourceRequest: true, + Verb: "create", + }, + ) + + testCases := map[string]struct { + input networking.IPAddress + expectedErrs field.ErrorList + }{ + "valid": { + input: mkValidIPAddress(), + }, + "missing parentRef": { + input: mkValidIPAddress(withNilParentRef), + expectedErrs: field.ErrorList{ + field.Required(field.NewPath("spec", "parentRef"), ""), + }, + }, + "missing parentRef resource": { + input: mkValidIPAddress(withEmptyParentRefResource), + expectedErrs: field.ErrorList{ + field.Required(field.NewPath("spec", "parentRef", "resource"), ""), + }, + }, + "missing parentRef name": { + input: mkValidIPAddress(withEmptyParentRefName), + expectedErrs: field.ErrorList{ + field.Required(field.NewPath("spec", "parentRef", "name"), ""), + }, + }, + } + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + apitesting.VerifyValidationEquivalence( + t, + ctx, + &tc.input, + Strategy.Validate, + tc.expectedErrs, + ) + }) + } + }) + } +} + +func TestDeclarativeValidateIPAddressUpdate(t *testing.T) { + for _, apiVersion := range apiVersions { + t.Run(apiVersion, func(t *testing.T) { + testCases := map[string]struct { + oldObj networking.IPAddress + updateObj networking.IPAddress + expectedErrs field.ErrorList + }{ + "valid update": { + oldObj: mkValidIPAddress(withResourceVersion("1")), + updateObj: mkValidIPAddress(withResourceVersion("1")), + }, + "immutable parentRef": { + oldObj: mkValidIPAddress(withResourceVersion("1")), + updateObj: mkValidIPAddress( + withResourceVersion("1"), + withParentRefName("new-name"), + ), + expectedErrs: field.ErrorList{ + func() *field.Error { + e := field.Invalid(field.NewPath("spec", "parentRef"), &networking.ParentReference{ + Group: "apps", + Resource: "deployments", + Name: "new-name", + }, "field is immutable") + e.Origin = "immutable" + return e + }(), + }, + }, + "set parentRef": { + oldObj: mkValidIPAddress(withResourceVersion("1"), withNilParentRef), + updateObj: mkValidIPAddress( + withResourceVersion("2"), + withParentRefName("new-name"), + ), + expectedErrs: field.ErrorList{ + func() *field.Error { + e := field.Invalid(field.NewPath("spec", "parentRef"), &networking.ParentReference{ + Group: "apps", + Resource: "deployments", + Name: "new-name", + }, "field is immutable") + e.Origin = "immutable" + return e + }(), + }, + }, + "unset parentRef": { + oldObj: mkValidIPAddress(withResourceVersion("1")), + updateObj: mkValidIPAddress( + withResourceVersion("1"), + withNilParentRef, + ), + expectedErrs: field.ErrorList{ + field.Required(field.NewPath("spec", "parentRef"), ""), + func() *field.Error { + e := field.Invalid(field.NewPath("spec", "parentRef"), nil, "field is immutable") + e.Origin = "immutable" + return e + }(), + }, + }, + } + + 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: "ipaddresses", + Name: "192.168.1.1", + IsResourceRequest: true, + Verb: "update", + }, + ) + + apitesting.VerifyUpdateValidationEquivalence( + t, + ctx, + &tc.updateObj, + &tc.oldObj, + Strategy.ValidateUpdate, + tc.expectedErrs, + ) + }) + } + }) + } +} + +func mkValidIPAddress(tweaks ...func(obj *networking.IPAddress)) networking.IPAddress { + obj := networking.IPAddress{ + ObjectMeta: metav1.ObjectMeta{ + Name: "192.168.1.1", + }, + Spec: networking.IPAddressSpec{ + ParentRef: &networking.ParentReference{ + Group: "apps", + Resource: "deployments", + Name: "my-deployment", + }, + }, + } + + for _, tweak := range tweaks { + tweak(&obj) + } + return obj +} + +func withNilParentRef(obj *networking.IPAddress) { + obj.Spec.ParentRef = nil +} + +func withEmptyParentRefResource(obj *networking.IPAddress) { + obj.Spec.ParentRef.Resource = "" +} + +func withEmptyParentRefName(obj *networking.IPAddress) { + obj.Spec.ParentRef.Name = "" +} + +func withResourceVersion(rv string) func(*networking.IPAddress) { + return func(obj *networking.IPAddress) { + obj.ResourceVersion = rv + } +} + +func withParentRefName(name string) func(*networking.IPAddress) { + return func(obj *networking.IPAddress) { + obj.Spec.ParentRef.Name = name + } +} diff --git a/pkg/registry/networking/ipaddress/strategy.go b/pkg/registry/networking/ipaddress/strategy.go index ef21e011e27..9afbf501848 100644 --- a/pkg/registry/networking/ipaddress/strategy.go +++ b/pkg/registry/networking/ipaddress/strategy.go @@ -19,6 +19,7 @@ package ipaddress import ( "context" + "k8s.io/apimachinery/pkg/api/operation" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/validation/field" "k8s.io/apiserver/pkg/registry/rest" @@ -73,7 +74,7 @@ func (ipAddressStrategy) PrepareForUpdate(ctx context.Context, obj, old runtime. func (ipAddressStrategy) Validate(ctx context.Context, obj runtime.Object) field.ErrorList { ipAddress := obj.(*networking.IPAddress) err := validation.ValidateIPAddress(ipAddress) - return err + return rest.ValidateDeclarativelyWithMigrationChecks(ctx, legacyscheme.Scheme, ipAddress, nil, err, operation.Create) } // Canonicalize normalizes the object after validation. @@ -91,7 +92,7 @@ func (ipAddressStrategy) ValidateUpdate(ctx context.Context, new, old runtime.Ob oldIPAddress := old.(*networking.IPAddress) errList := validation.ValidateIPAddress(newIPAddress) errList = append(errList, validation.ValidateIPAddressUpdate(newIPAddress, oldIPAddress)...) - return errList + return rest.ValidateDeclarativelyWithMigrationChecks(ctx, legacyscheme.Scheme, newIPAddress, oldIPAddress, errList, operation.Update) } // AllowUnconditionalUpdate is the default update policy for IPAddress objects. diff --git a/pkg/registry/networking/ipaddress/strategy_test.go b/pkg/registry/networking/ipaddress/strategy_test.go index 41de8fcecc4..86045eb1adc 100644 --- a/pkg/registry/networking/ipaddress/strategy_test.go +++ b/pkg/registry/networking/ipaddress/strategy_test.go @@ -42,7 +42,17 @@ func newIPAddress() networking.IPAddress { } func TestIPAddressStrategy(t *testing.T) { - ctx := genericapirequest.NewDefaultContext() + ctx := genericapirequest.WithRequestInfo( + genericapirequest.NewDefaultContext(), + &genericapirequest.RequestInfo{ + APIGroup: "networking.k8s.io", + APIVersion: "v1", + Resource: "ipaddresses", + Name: "192.168.1.1", + IsResourceRequest: true, + Verb: "create", + }, + ) if Strategy.NamespaceScoped() { t.Errorf("ipAddress must not be namespace scoped") } diff --git a/staging/src/k8s.io/api/networking/v1/generated.proto b/staging/src/k8s.io/api/networking/v1/generated.proto index 516037a5c7b..91b0fd9a04c 100644 --- a/staging/src/k8s.io/api/networking/v1/generated.proto +++ b/staging/src/k8s.io/api/networking/v1/generated.proto @@ -107,6 +107,8 @@ message IPAddressSpec { // ParentRef references the resource that an IPAddress is attached to. // An IPAddress must reference a parent object. // +required + // +k8s:required + // +k8s:immutable optional ParentReference parentRef = 1; } @@ -595,6 +597,7 @@ message ParentReference { // Resource is the resource of the object being referenced. // +required + // +k8s:required optional string resource = 2; // Namespace is the namespace of the object being referenced. @@ -603,6 +606,7 @@ message ParentReference { // Name is the name of the object being referenced. // +required + // +k8s:required optional string name = 4; } diff --git a/staging/src/k8s.io/api/networking/v1/types.go b/staging/src/k8s.io/api/networking/v1/types.go index 4b6c65d65f6..d6a2c9a8e57 100644 --- a/staging/src/k8s.io/api/networking/v1/types.go +++ b/staging/src/k8s.io/api/networking/v1/types.go @@ -675,6 +675,7 @@ type IPAddressSpec struct { // An IPAddress must reference a parent object. // +required // +k8s:required + // +k8s:immutable ParentRef *ParentReference `json:"parentRef,omitempty" protobuf:"bytes,1,opt,name=parentRef"` } diff --git a/staging/src/k8s.io/api/networking/v1beta1/generated.proto b/staging/src/k8s.io/api/networking/v1beta1/generated.proto index 616f6e9bd1a..1c2e537862b 100644 --- a/staging/src/k8s.io/api/networking/v1beta1/generated.proto +++ b/staging/src/k8s.io/api/networking/v1beta1/generated.proto @@ -108,6 +108,8 @@ message IPAddressSpec { // ParentRef references the resource that an IPAddress is attached to. // An IPAddress must reference a parent object. // +required + // +k8s:required + // +k8s:immutable optional ParentReference parentRef = 1; } @@ -402,6 +404,7 @@ message ParentReference { // Resource is the resource of the object being referenced. // +required + // +k8s:required optional string resource = 2; // Namespace is the namespace of the object being referenced. @@ -410,6 +413,7 @@ message ParentReference { // Name is the name of the object being referenced. // +required + // +k8s:required optional string name = 4; } diff --git a/staging/src/k8s.io/api/networking/v1beta1/types.go b/staging/src/k8s.io/api/networking/v1beta1/types.go index 1f251793cc1..488bb44b741 100644 --- a/staging/src/k8s.io/api/networking/v1beta1/types.go +++ b/staging/src/k8s.io/api/networking/v1beta1/types.go @@ -457,6 +457,7 @@ type IPAddressSpec struct { // An IPAddress must reference a parent object. // +required // +k8s:required + // +k8s:immutable ParentRef *ParentReference `json:"parentRef,omitempty" protobuf:"bytes,1,opt,name=parentRef"` }