Mark Endpoint.Addresses required, add test cases

This commit is contained in:
chris britton
2025-12-22 10:07:55 -04:00
committed by CLBRITTON2
parent 494e27a059
commit 5e4d5fe599
8 changed files with 346 additions and 1 deletions

View File

@@ -0,0 +1,106 @@
//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"
discoveryv1 "k8s.io/api/discovery/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 EndpointSlice
scheme.AddValidationFunc((*discoveryv1.EndpointSlice)(nil), func(ctx context.Context, op operation.Operation, obj, oldObj interface{}) field.ErrorList {
switch op.Request.SubresourcePath() {
case "/":
return Validate_EndpointSlice(ctx, op, nil /* fldPath */, obj.(*discoveryv1.EndpointSlice), safe.Cast[*discoveryv1.EndpointSlice](oldObj))
}
return field.ErrorList{field.InternalError(nil, fmt.Errorf("no validation found for %T, subresource: %v", obj, op.Request.SubresourcePath()))}
})
return nil
}
// Validate_Endpoint validates an instance of Endpoint according
// to declarative validation rules in the API schema.
func Validate_Endpoint(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *discoveryv1.Endpoint) (errs field.ErrorList) {
// field discoveryv1.Endpoint.Addresses
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 && equality.Semantic.DeepEqual(obj, oldObj) {
return nil
}
// call field-attached validations
earlyReturn := false
if e := validate.RequiredSlice(ctx, op, fldPath, obj, oldObj); len(e) != 0 {
errs = append(errs, e...)
earlyReturn = true
}
if earlyReturn {
return // do not proceed
}
return
}(fldPath.Child("addresses"), obj.Addresses, safe.Field(oldObj, func(oldObj *discoveryv1.Endpoint) []string { return oldObj.Addresses }), oldObj != nil)...)
// field discoveryv1.Endpoint.Conditions has no validation
// field discoveryv1.Endpoint.Hostname has no validation
// field discoveryv1.Endpoint.TargetRef has no validation
// field discoveryv1.Endpoint.DeprecatedTopology has no validation
// field discoveryv1.Endpoint.NodeName has no validation
// field discoveryv1.Endpoint.Zone has no validation
// field discoveryv1.Endpoint.Hints has no validation
return errs
}
// Validate_EndpointSlice validates an instance of EndpointSlice according
// to declarative validation rules in the API schema.
func Validate_EndpointSlice(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *discoveryv1.EndpointSlice) (errs field.ErrorList) {
// field discoveryv1.EndpointSlice.TypeMeta has no validation
// field discoveryv1.EndpointSlice.ObjectMeta has no validation
// field discoveryv1.EndpointSlice.AddressType has no validation
// field discoveryv1.EndpointSlice.Endpoints
errs = append(errs,
func(fldPath *field.Path, obj, oldObj []discoveryv1.Endpoint, 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_Endpoint)...)
return
}(fldPath.Child("endpoints"), obj.Endpoints, safe.Field(oldObj, func(oldObj *discoveryv1.EndpointSlice) []discoveryv1.Endpoint { return oldObj.Endpoints }), oldObj != nil)...)
// field discoveryv1.EndpointSlice.Ports has no validation
return errs
}

View File

@@ -0,0 +1,105 @@
//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"
discoveryv1beta1 "k8s.io/api/discovery/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 EndpointSlice
scheme.AddValidationFunc((*discoveryv1beta1.EndpointSlice)(nil), func(ctx context.Context, op operation.Operation, obj, oldObj interface{}) field.ErrorList {
switch op.Request.SubresourcePath() {
case "/":
return Validate_EndpointSlice(ctx, op, nil /* fldPath */, obj.(*discoveryv1beta1.EndpointSlice), safe.Cast[*discoveryv1beta1.EndpointSlice](oldObj))
}
return field.ErrorList{field.InternalError(nil, fmt.Errorf("no validation found for %T, subresource: %v", obj, op.Request.SubresourcePath()))}
})
return nil
}
// Validate_Endpoint validates an instance of Endpoint according
// to declarative validation rules in the API schema.
func Validate_Endpoint(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *discoveryv1beta1.Endpoint) (errs field.ErrorList) {
// field discoveryv1beta1.Endpoint.Addresses
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 && equality.Semantic.DeepEqual(obj, oldObj) {
return nil
}
// call field-attached validations
earlyReturn := false
if e := validate.RequiredSlice(ctx, op, fldPath, obj, oldObj); len(e) != 0 {
errs = append(errs, e...)
earlyReturn = true
}
if earlyReturn {
return // do not proceed
}
return
}(fldPath.Child("addresses"), obj.Addresses, safe.Field(oldObj, func(oldObj *discoveryv1beta1.Endpoint) []string { return oldObj.Addresses }), oldObj != nil)...)
// field discoveryv1beta1.Endpoint.Conditions has no validation
// field discoveryv1beta1.Endpoint.Hostname has no validation
// field discoveryv1beta1.Endpoint.TargetRef has no validation
// field discoveryv1beta1.Endpoint.Topology has no validation
// field discoveryv1beta1.Endpoint.NodeName has no validation
// field discoveryv1beta1.Endpoint.Hints has no validation
return errs
}
// Validate_EndpointSlice validates an instance of EndpointSlice according
// to declarative validation rules in the API schema.
func Validate_EndpointSlice(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *discoveryv1beta1.EndpointSlice) (errs field.ErrorList) {
// field discoveryv1beta1.EndpointSlice.TypeMeta has no validation
// field discoveryv1beta1.EndpointSlice.ObjectMeta has no validation
// field discoveryv1beta1.EndpointSlice.AddressType has no validation
// field discoveryv1beta1.EndpointSlice.Endpoints
errs = append(errs,
func(fldPath *field.Path, obj, oldObj []discoveryv1beta1.Endpoint, 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_Endpoint)...)
return
}(fldPath.Child("endpoints"), obj.Endpoints, safe.Field(oldObj, func(oldObj *discoveryv1beta1.EndpointSlice) []discoveryv1beta1.Endpoint { return oldObj.Endpoints }), oldObj != nil)...)
// field discoveryv1beta1.EndpointSlice.Ports has no validation
return errs
}

View File

@@ -104,7 +104,7 @@ func validateEndpoints(endpoints []discovery.Endpoint, addrType discovery.Addres
addressPath := idxPath.Child("addresses")
if len(endpoint.Addresses) == 0 {
allErrs = append(allErrs, field.Required(addressPath, "must contain at least 1 address"))
allErrs = append(allErrs, field.Required(addressPath, "must contain at least 1 address").MarkCoveredByDeclarative())
} else if len(endpoint.Addresses) > maxAddresses {
allErrs = append(allErrs, field.TooMany(addressPath, len(endpoint.Addresses), maxAddresses))
}

View File

@@ -15,3 +15,129 @@ limitations under the License.
*/
package endpointslice
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"
"k8s.io/kubernetes/pkg/apis/discovery"
)
var apiVersions = []string{"v1", "v1beta1"}
func TestDeclarativeValidate(t *testing.T) {
for _, apiVersion := range apiVersions {
t.Run(apiVersion, func(t *testing.T) {
testDeclarativeValidate(t, apiVersion)
})
}
}
func testDeclarativeValidate(t *testing.T, apiVersion string) {
ctx := genericapirequest.WithRequestInfo(genericapirequest.NewDefaultContext(), &genericapirequest.RequestInfo{
APIGroup: "discovery.k8s.io",
APIVersion: apiVersion,
Resource: "endpointslices",
IsResourceRequest: true,
Verb: "create",
})
testCases := map[string]struct {
input discovery.EndpointSlice
expectedErrs field.ErrorList
}{
"valid": {
input: mkValidEndpointSlice(),
},
"invalid missing endpoint addresses": {
input: mkValidEndpointSlice(func(obj *discovery.EndpointSlice) {
obj.Endpoints[0].Addresses = nil
}),
expectedErrs: field.ErrorList{
field.Required(field.NewPath("endpoints").Index(0).Child("addresses"), ""),
},
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
apitesting.VerifyValidationEquivalence(t, ctx, &tc.input, Strategy.Validate, tc.expectedErrs)
})
}
}
func TestDeclarativeValidateUpdate(t *testing.T) {
for _, apiVersion := range apiVersions {
t.Run(apiVersion, func(t *testing.T) {
testDeclarativeValidateUpdate(t, apiVersion)
})
}
}
func testDeclarativeValidateUpdate(t *testing.T, apiVersion string) {
testCases := map[string]struct {
oldObj discovery.EndpointSlice
updateObj discovery.EndpointSlice
expectedErrs field.ErrorList
}{
"valid update": {
oldObj: mkValidEndpointSlice(func(obj *discovery.EndpointSlice) {
obj.ResourceVersion = "1"
}),
updateObj: mkValidEndpointSlice(func(obj *discovery.EndpointSlice) {
obj.ResourceVersion = "1"
}),
},
"invalid update missing addresses": {
oldObj: mkValidEndpointSlice(func(obj *discovery.EndpointSlice) {
obj.ResourceVersion = "1"
}),
updateObj: mkValidEndpointSlice(func(obj *discovery.EndpointSlice) {
obj.ResourceVersion = "1"
obj.Endpoints[0].Addresses = nil
}),
expectedErrs: field.ErrorList{
field.Required(field.NewPath("endpoints").Index(0).Child("addresses"), ""),
},
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
ctx := genericapirequest.WithRequestInfo(genericapirequest.NewDefaultContext(), &genericapirequest.RequestInfo{
APIPrefix: "apis",
APIGroup: "discovery.k8s.io",
APIVersion: apiVersion,
Resource: "endpointslices",
Name: "valid-endpointslice",
IsResourceRequest: true,
Verb: "update",
})
apitesting.VerifyUpdateValidationEquivalence(t, ctx, &tc.updateObj, &tc.oldObj, Strategy.ValidateUpdate, tc.expectedErrs)
})
}
}
func mkValidEndpointSlice(tweaks ...func(obj *discovery.EndpointSlice)) discovery.EndpointSlice {
obj := discovery.EndpointSlice{
ObjectMeta: metav1.ObjectMeta{
Name: "valid-endpointslice",
Namespace: metav1.NamespaceDefault,
},
AddressType: discovery.AddressTypeIPv4,
Endpoints: []discovery.Endpoint{
{
Addresses: []string{"10.1.2.1"},
},
},
}
for _, tweak := range tweaks {
tweak(&obj)
}
return obj
}

View File

@@ -38,6 +38,8 @@ message Endpoint {
// controller will always have exactly 1 address. No semantics are defined for
// additional addresses beyond the first, and kube-proxy does not look at them.
// +listType=set
// +required
// +k8s:required
repeated string addresses = 1;
// conditions contains information about the current status of the endpoint.

View File

@@ -91,6 +91,8 @@ type Endpoint struct {
// controller will always have exactly 1 address. No semantics are defined for
// additional addresses beyond the first, and kube-proxy does not look at them.
// +listType=set
// +required
// +k8s:required
Addresses []string `json:"addresses" protobuf:"bytes,1,rep,name=addresses"`
// conditions contains information about the current status of the endpoint.

View File

@@ -38,6 +38,8 @@ message Endpoint {
// 100. These are all assumed to be fungible and clients may choose to only
// use the first element. Refer to: https://issue.k8s.io/106267
// +listType=set
// +required
// +k8s:required
repeated string addresses = 1;
// conditions contains information about the current status of the endpoint.

View File

@@ -85,6 +85,8 @@ type Endpoint struct {
// 100. These are all assumed to be fungible and clients may choose to only
// use the first element. Refer to: https://issue.k8s.io/106267
// +listType=set
// +required
// +k8s:required
Addresses []string `json:"addresses" protobuf:"bytes,1,rep,name=addresses"`
// conditions contains information about the current status of the endpoint.