add declarative validation tests for ipaddress

This commit is contained in:
Abhishek Srivastava
2026-01-13 22:34:23 -08:00
parent 864c54f658
commit ed083ab6b7
10 changed files with 265 additions and 35 deletions

View File

@@ -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
}
}

View File

@@ -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

View File

@@ -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
}

View File

@@ -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
}
}

View File

@@ -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.

View File

@@ -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")
}

View File

@@ -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;
}

View File

@@ -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"`
}

View File

@@ -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;
}

View File

@@ -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"`
}