From 494e27a059a3639113f52c45f7940c42c83bbd43 Mon Sep 17 00:00:00 2001 From: chris britton Date: Fri, 19 Dec 2025 15:20:49 -0400 Subject: [PATCH 1/7] Wire discovery group for declarative validation --- pkg/apis/discovery/v1/doc.go | 2 ++ .../discovery/v1/zz_generated.validations.go | 0 pkg/apis/discovery/v1beta1/doc.go | 2 ++ .../v1beta1/zz_generated.validations.go | 0 .../declarative_validation_test.go | 17 +++++++++++++++++ .../discovery/endpointslice/strategy.go | 9 ++++++--- 6 files changed, 27 insertions(+), 3 deletions(-) create mode 100644 pkg/apis/discovery/v1/zz_generated.validations.go create mode 100644 pkg/apis/discovery/v1beta1/zz_generated.validations.go create mode 100644 pkg/registry/discovery/endpointslice/declarative_validation_test.go diff --git a/pkg/apis/discovery/v1/doc.go b/pkg/apis/discovery/v1/doc.go index 83d3f4ab281..45029f3368c 100644 --- a/pkg/apis/discovery/v1/doc.go +++ b/pkg/apis/discovery/v1/doc.go @@ -18,5 +18,7 @@ limitations under the License. // +k8s:conversion-gen-external-types=k8s.io/api/discovery/v1 // +k8s:defaulter-gen=TypeMeta // +k8s:defaulter-gen-input=k8s.io/api/discovery/v1 +// +k8s:validation-gen=TypeMeta +// +k8s:validation-gen-input=k8s.io/api/discovery/v1 package v1 diff --git a/pkg/apis/discovery/v1/zz_generated.validations.go b/pkg/apis/discovery/v1/zz_generated.validations.go new file mode 100644 index 00000000000..e69de29bb2d diff --git a/pkg/apis/discovery/v1beta1/doc.go b/pkg/apis/discovery/v1beta1/doc.go index fbd710a604b..01ae0dfb3fc 100644 --- a/pkg/apis/discovery/v1beta1/doc.go +++ b/pkg/apis/discovery/v1beta1/doc.go @@ -18,5 +18,7 @@ limitations under the License. // +k8s:conversion-gen-external-types=k8s.io/api/discovery/v1beta1 // +k8s:defaulter-gen=TypeMeta // +k8s:defaulter-gen-input=k8s.io/api/discovery/v1beta1 +// +k8s:validation-gen=TypeMeta +// +k8s:validation-gen-input=k8s.io/api/discovery/v1beta1 package v1beta1 diff --git a/pkg/apis/discovery/v1beta1/zz_generated.validations.go b/pkg/apis/discovery/v1beta1/zz_generated.validations.go new file mode 100644 index 00000000000..e69de29bb2d diff --git a/pkg/registry/discovery/endpointslice/declarative_validation_test.go b/pkg/registry/discovery/endpointslice/declarative_validation_test.go new file mode 100644 index 00000000000..070abc5204e --- /dev/null +++ b/pkg/registry/discovery/endpointslice/declarative_validation_test.go @@ -0,0 +1,17 @@ +/* +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 endpointslice diff --git a/pkg/registry/discovery/endpointslice/strategy.go b/pkg/registry/discovery/endpointslice/strategy.go index 51d8f3c5ea9..0be895645e6 100644 --- a/pkg/registry/discovery/endpointslice/strategy.go +++ b/pkg/registry/discovery/endpointslice/strategy.go @@ -24,6 +24,7 @@ import ( discoveryv1 "k8s.io/api/discovery/v1" discoveryv1beta1 "k8s.io/api/discovery/v1beta1" apiequality "k8s.io/apimachinery/pkg/api/equality" + "k8s.io/apimachinery/pkg/api/operation" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" @@ -31,6 +32,7 @@ import ( utilvalidation "k8s.io/apimachinery/pkg/util/validation" "k8s.io/apimachinery/pkg/util/validation/field" genericapirequest "k8s.io/apiserver/pkg/endpoints/request" + "k8s.io/apiserver/pkg/registry/rest" "k8s.io/apiserver/pkg/storage/names" utilfeature "k8s.io/apiserver/pkg/util/feature" "k8s.io/kubernetes/pkg/api/legacyscheme" @@ -91,8 +93,8 @@ func (endpointSliceStrategy) PrepareForUpdate(ctx context.Context, obj, old runt // Validate validates a new EndpointSlice. func (endpointSliceStrategy) Validate(ctx context.Context, obj runtime.Object) field.ErrorList { endpointSlice := obj.(*discovery.EndpointSlice) - err := validation.ValidateEndpointSliceCreate(endpointSlice) - return err + allErrs := validation.ValidateEndpointSliceCreate(endpointSlice) + return rest.ValidateDeclarativelyWithMigrationChecks(ctx, legacyscheme.Scheme, endpointSlice, nil, allErrs, operation.Create) } // WarningsOnCreate returns warnings for the creation of the given object. @@ -120,7 +122,8 @@ func (endpointSliceStrategy) AllowCreateOnUpdate() bool { func (endpointSliceStrategy) ValidateUpdate(ctx context.Context, new, old runtime.Object) field.ErrorList { newEPS := new.(*discovery.EndpointSlice) oldEPS := old.(*discovery.EndpointSlice) - return validation.ValidateEndpointSliceUpdate(newEPS, oldEPS) + allErrs := validation.ValidateEndpointSliceUpdate(newEPS, oldEPS) + return rest.ValidateDeclarativelyWithMigrationChecks(ctx, legacyscheme.Scheme, newEPS, oldEPS, allErrs, operation.Update) } // WarningsOnUpdate returns warnings for the given update. From 5e4d5fe5991b043e080cdcd49c08408337139160 Mon Sep 17 00:00:00 2001 From: chris britton Date: Mon, 22 Dec 2025 10:07:55 -0400 Subject: [PATCH 2/7] Mark Endpoint.Addresses required, add test cases --- .../discovery/v1/zz_generated.validations.go | 106 +++++++++++++++ .../v1beta1/zz_generated.validations.go | 105 +++++++++++++++ pkg/apis/discovery/validation/validation.go | 2 +- .../declarative_validation_test.go | 126 ++++++++++++++++++ .../k8s.io/api/discovery/v1/generated.proto | 2 + staging/src/k8s.io/api/discovery/v1/types.go | 2 + .../api/discovery/v1beta1/generated.proto | 2 + .../src/k8s.io/api/discovery/v1beta1/types.go | 2 + 8 files changed, 346 insertions(+), 1 deletion(-) diff --git a/pkg/apis/discovery/v1/zz_generated.validations.go b/pkg/apis/discovery/v1/zz_generated.validations.go index e69de29bb2d..2f0021ab7f1 100644 --- a/pkg/apis/discovery/v1/zz_generated.validations.go +++ b/pkg/apis/discovery/v1/zz_generated.validations.go @@ -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 +} diff --git a/pkg/apis/discovery/v1beta1/zz_generated.validations.go b/pkg/apis/discovery/v1beta1/zz_generated.validations.go index e69de29bb2d..19c0e9a3b22 100644 --- a/pkg/apis/discovery/v1beta1/zz_generated.validations.go +++ b/pkg/apis/discovery/v1beta1/zz_generated.validations.go @@ -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 +} diff --git a/pkg/apis/discovery/validation/validation.go b/pkg/apis/discovery/validation/validation.go index 98e9f9f03b1..14bed617490 100644 --- a/pkg/apis/discovery/validation/validation.go +++ b/pkg/apis/discovery/validation/validation.go @@ -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)) } diff --git a/pkg/registry/discovery/endpointslice/declarative_validation_test.go b/pkg/registry/discovery/endpointslice/declarative_validation_test.go index 070abc5204e..181a4ae681c 100644 --- a/pkg/registry/discovery/endpointslice/declarative_validation_test.go +++ b/pkg/registry/discovery/endpointslice/declarative_validation_test.go @@ -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 +} diff --git a/staging/src/k8s.io/api/discovery/v1/generated.proto b/staging/src/k8s.io/api/discovery/v1/generated.proto index 684b6358e33..8bb809bb7c6 100644 --- a/staging/src/k8s.io/api/discovery/v1/generated.proto +++ b/staging/src/k8s.io/api/discovery/v1/generated.proto @@ -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. diff --git a/staging/src/k8s.io/api/discovery/v1/types.go b/staging/src/k8s.io/api/discovery/v1/types.go index 7b49933c05d..4f7c68dc0aa 100644 --- a/staging/src/k8s.io/api/discovery/v1/types.go +++ b/staging/src/k8s.io/api/discovery/v1/types.go @@ -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. diff --git a/staging/src/k8s.io/api/discovery/v1beta1/generated.proto b/staging/src/k8s.io/api/discovery/v1beta1/generated.proto index 7b9d983b831..e0ceb9eb9ad 100644 --- a/staging/src/k8s.io/api/discovery/v1beta1/generated.proto +++ b/staging/src/k8s.io/api/discovery/v1beta1/generated.proto @@ -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. diff --git a/staging/src/k8s.io/api/discovery/v1beta1/types.go b/staging/src/k8s.io/api/discovery/v1beta1/types.go index 11ec1b3968a..3cc11dca458 100644 --- a/staging/src/k8s.io/api/discovery/v1beta1/types.go +++ b/staging/src/k8s.io/api/discovery/v1beta1/types.go @@ -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. From 2c3516edb0bae3cb90c0aa1f3d1b81010168b0d5 Mon Sep 17 00:00:00 2001 From: chris britton Date: Mon, 22 Dec 2025 10:59:57 -0400 Subject: [PATCH 3/7] Add maxItems tag to Endpoint.Addresses, update tests --- .../discovery/v1/zz_generated.validations.go | 4 ++ .../v1beta1/zz_generated.validations.go | 4 ++ pkg/apis/discovery/validation/validation.go | 3 +- .../declarative_validation_test.go | 49 ++++++++++++++----- .../k8s.io/api/discovery/v1/generated.proto | 1 + staging/src/k8s.io/api/discovery/v1/types.go | 1 + .../api/discovery/v1beta1/generated.proto | 1 + .../src/k8s.io/api/discovery/v1beta1/types.go | 1 + 8 files changed, 49 insertions(+), 15 deletions(-) diff --git a/pkg/apis/discovery/v1/zz_generated.validations.go b/pkg/apis/discovery/v1/zz_generated.validations.go index 2f0021ab7f1..6a7d813549c 100644 --- a/pkg/apis/discovery/v1/zz_generated.validations.go +++ b/pkg/apis/discovery/v1/zz_generated.validations.go @@ -62,6 +62,10 @@ func Validate_Endpoint(ctx context.Context, op operation.Operation, fldPath *fie } // call field-attached validations earlyReturn := false + if e := validate.MaxItems(ctx, op, fldPath, obj, oldObj, 100); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } if e := validate.RequiredSlice(ctx, op, fldPath, obj, oldObj); len(e) != 0 { errs = append(errs, e...) earlyReturn = true diff --git a/pkg/apis/discovery/v1beta1/zz_generated.validations.go b/pkg/apis/discovery/v1beta1/zz_generated.validations.go index 19c0e9a3b22..ef44355086f 100644 --- a/pkg/apis/discovery/v1beta1/zz_generated.validations.go +++ b/pkg/apis/discovery/v1beta1/zz_generated.validations.go @@ -62,6 +62,10 @@ func Validate_Endpoint(ctx context.Context, op operation.Operation, fldPath *fie } // call field-attached validations earlyReturn := false + if e := validate.MaxItems(ctx, op, fldPath, obj, oldObj, 100); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } if e := validate.RequiredSlice(ctx, op, fldPath, obj, oldObj); len(e) != 0 { errs = append(errs, e...) earlyReturn = true diff --git a/pkg/apis/discovery/validation/validation.go b/pkg/apis/discovery/validation/validation.go index 14bed617490..9c9e441a583 100644 --- a/pkg/apis/discovery/validation/validation.go +++ b/pkg/apis/discovery/validation/validation.go @@ -106,9 +106,8 @@ func validateEndpoints(endpoints []discovery.Endpoint, addrType discovery.Addres if len(endpoint.Addresses) == 0 { 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)) + allErrs = append(allErrs, field.TooMany(addressPath, len(endpoint.Addresses), maxAddresses).WithOrigin("maxItems").MarkCoveredByDeclarative()) } - for i, address := range endpoint.Addresses { // This validates known address types, unknown types fall through // and do not get validated. diff --git a/pkg/registry/discovery/endpointslice/declarative_validation_test.go b/pkg/registry/discovery/endpointslice/declarative_validation_test.go index 181a4ae681c..cf4bb110d4e 100644 --- a/pkg/registry/discovery/endpointslice/declarative_validation_test.go +++ b/pkg/registry/discovery/endpointslice/declarative_validation_test.go @@ -17,6 +17,7 @@ limitations under the License. package endpointslice import ( + "fmt" "testing" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -52,6 +53,9 @@ func testDeclarativeValidate(t *testing.T, apiVersion string) { "valid": { input: mkValidEndpointSlice(), }, + "valid at limit endpoint addresses": { + input: mkValidEndpointSlice(tweakAddresses(100)), + }, "invalid missing endpoint addresses": { input: mkValidEndpointSlice(func(obj *discovery.EndpointSlice) { obj.Endpoints[0].Addresses = nil @@ -60,8 +64,13 @@ func testDeclarativeValidate(t *testing.T, apiVersion string) { field.Required(field.NewPath("endpoints").Index(0).Child("addresses"), ""), }, }, + "invalid too many endpoint addresses": { + input: mkValidEndpointSlice(tweakAddresses(101)), + expectedErrs: field.ErrorList{ + field.TooMany(field.NewPath("endpoints").Index(0).Child("addresses"), 101, 100).WithOrigin("maxItems"), + }, + }, } - for name, tc := range testCases { t.Run(name, func(t *testing.T) { apitesting.VerifyValidationEquivalence(t, ctx, &tc.input, Strategy.Validate, tc.expectedErrs) @@ -84,27 +93,30 @@ func testDeclarativeValidateUpdate(t *testing.T, apiVersion string) { expectedErrs field.ErrorList }{ "valid update": { - oldObj: mkValidEndpointSlice(func(obj *discovery.EndpointSlice) { - obj.ResourceVersion = "1" - }), - updateObj: mkValidEndpointSlice(func(obj *discovery.EndpointSlice) { - obj.ResourceVersion = "1" - }), + oldObj: mkValidEndpointSlice(), + updateObj: mkValidEndpointSlice(), + }, + "valid update at limit endpoint addresses": { + oldObj: mkValidEndpointSlice(), + updateObj: mkValidEndpointSlice(tweakAddresses(100)), }, "invalid update missing addresses": { - oldObj: mkValidEndpointSlice(func(obj *discovery.EndpointSlice) { - obj.ResourceVersion = "1" - }), + oldObj: mkValidEndpointSlice(), 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"), ""), }, }, + "invalid update too many addresses": { + oldObj: mkValidEndpointSlice(), + updateObj: mkValidEndpointSlice(tweakAddresses(101)), + expectedErrs: field.ErrorList{ + field.TooMany(field.NewPath("endpoints").Index(0).Child("addresses"), 101, 100).WithOrigin("maxItems"), + }, + }, } - for name, tc := range testCases { t.Run(name, func(t *testing.T) { ctx := genericapirequest.WithRequestInfo(genericapirequest.NewDefaultContext(), &genericapirequest.RequestInfo{ @@ -116,7 +128,8 @@ func testDeclarativeValidateUpdate(t *testing.T, apiVersion string) { IsResourceRequest: true, Verb: "update", }) - + tc.oldObj.ResourceVersion = "1" + tc.updateObj.ResourceVersion = "2" apitesting.VerifyUpdateValidationEquivalence(t, ctx, &tc.updateObj, &tc.oldObj, Strategy.ValidateUpdate, tc.expectedErrs) }) } @@ -141,3 +154,13 @@ func mkValidEndpointSlice(tweaks ...func(obj *discovery.EndpointSlice)) discover } return obj } + +func tweakAddresses(count int) func(*discovery.EndpointSlice) { + return func(obj *discovery.EndpointSlice) { + addrs := make([]string, count) + for i := range addrs { + addrs[i] = fmt.Sprintf("10.0.0.%d", i%255) + } + obj.Endpoints[0].Addresses = addrs + } +} diff --git a/staging/src/k8s.io/api/discovery/v1/generated.proto b/staging/src/k8s.io/api/discovery/v1/generated.proto index 8bb809bb7c6..7c987d8162f 100644 --- a/staging/src/k8s.io/api/discovery/v1/generated.proto +++ b/staging/src/k8s.io/api/discovery/v1/generated.proto @@ -40,6 +40,7 @@ message Endpoint { // +listType=set // +required // +k8s:required + // +k8s:maxItems=100 repeated string addresses = 1; // conditions contains information about the current status of the endpoint. diff --git a/staging/src/k8s.io/api/discovery/v1/types.go b/staging/src/k8s.io/api/discovery/v1/types.go index 4f7c68dc0aa..20b4c43f044 100644 --- a/staging/src/k8s.io/api/discovery/v1/types.go +++ b/staging/src/k8s.io/api/discovery/v1/types.go @@ -93,6 +93,7 @@ type Endpoint struct { // +listType=set // +required // +k8s:required + // +k8s:maxItems=100 Addresses []string `json:"addresses" protobuf:"bytes,1,rep,name=addresses"` // conditions contains information about the current status of the endpoint. diff --git a/staging/src/k8s.io/api/discovery/v1beta1/generated.proto b/staging/src/k8s.io/api/discovery/v1beta1/generated.proto index e0ceb9eb9ad..1de1d5e3c2a 100644 --- a/staging/src/k8s.io/api/discovery/v1beta1/generated.proto +++ b/staging/src/k8s.io/api/discovery/v1beta1/generated.proto @@ -40,6 +40,7 @@ message Endpoint { // +listType=set // +required // +k8s:required + // +k8s:maxItems=100 repeated string addresses = 1; // conditions contains information about the current status of the endpoint. diff --git a/staging/src/k8s.io/api/discovery/v1beta1/types.go b/staging/src/k8s.io/api/discovery/v1beta1/types.go index 3cc11dca458..0f3b403d5ae 100644 --- a/staging/src/k8s.io/api/discovery/v1beta1/types.go +++ b/staging/src/k8s.io/api/discovery/v1beta1/types.go @@ -87,6 +87,7 @@ type Endpoint struct { // +listType=set // +required // +k8s:required + // +k8s:maxItems=100 Addresses []string `json:"addresses" protobuf:"bytes,1,rep,name=addresses"` // conditions contains information about the current status of the endpoint. From a6618afc29fddd70e30d11620c03d14de6b2a668 Mon Sep 17 00:00:00 2001 From: CLBRITTON2 Date: Fri, 9 Jan 2026 08:10:47 -0500 Subject: [PATCH 4/7] Wire discovery v1/v1beta1 for fuzz testing --- pkg/api/testing/validation_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/api/testing/validation_test.go b/pkg/api/testing/validation_test.go index 15bcae6231b..14ae0032f6f 100644 --- a/pkg/api/testing/validation_test.go +++ b/pkg/api/testing/validation_test.go @@ -53,6 +53,8 @@ func TestVersionedValidationByFuzzing(t *testing.T) { {Group: "autoscaling", Version: "v1beta1"}, {Group: "autoscaling", Version: "v1beta2"}, {Group: "autoscaling", Version: "v2"}, + {Group: "discovery.k8s.io", Version: "v1"}, + {Group: "discovery.k8s.io", Version: "v1beta1"}, } fuzzIters := *roundtrip.FuzzIters / 10 // TODO: Find a better way to manage test running time From 1ee5d304a9c53407e7a2fdb109cc3a712dc72038 Mon Sep 17 00:00:00 2001 From: CLBRITTON2 Date: Fri, 9 Jan 2026 08:48:53 -0500 Subject: [PATCH 5/7] Add declarative validation tests for EndpointSlice addressType required --- .../discovery/v1/zz_generated.validations.go | 20 ++++++++++++++++++- .../v1beta1/zz_generated.validations.go | 20 ++++++++++++++++++- pkg/apis/discovery/validation/validation.go | 2 +- .../declarative_validation_test.go | 12 +++++++++++ .../k8s.io/api/discovery/v1/generated.proto | 2 ++ staging/src/k8s.io/api/discovery/v1/types.go | 2 ++ .../api/discovery/v1beta1/generated.proto | 2 ++ .../src/k8s.io/api/discovery/v1beta1/types.go | 2 ++ 8 files changed, 59 insertions(+), 3 deletions(-) diff --git a/pkg/apis/discovery/v1/zz_generated.validations.go b/pkg/apis/discovery/v1/zz_generated.validations.go index 6a7d813549c..2766f592446 100644 --- a/pkg/apis/discovery/v1/zz_generated.validations.go +++ b/pkg/apis/discovery/v1/zz_generated.validations.go @@ -91,7 +91,25 @@ func Validate_Endpoint(ctx context.Context, op operation.Operation, fldPath *fie 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.AddressType + errs = append(errs, + func(fldPath *field.Path, obj, oldObj *discoveryv1.AddressType, 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("addressType"), &obj.AddressType, safe.Field(oldObj, func(oldObj *discoveryv1.EndpointSlice) *discoveryv1.AddressType { return &oldObj.AddressType }), oldObj != nil)...) // field discoveryv1.EndpointSlice.Endpoints errs = append(errs, diff --git a/pkg/apis/discovery/v1beta1/zz_generated.validations.go b/pkg/apis/discovery/v1beta1/zz_generated.validations.go index ef44355086f..ba337446aa7 100644 --- a/pkg/apis/discovery/v1beta1/zz_generated.validations.go +++ b/pkg/apis/discovery/v1beta1/zz_generated.validations.go @@ -90,7 +90,25 @@ func Validate_Endpoint(ctx context.Context, op operation.Operation, fldPath *fie 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.AddressType + errs = append(errs, + func(fldPath *field.Path, obj, oldObj *discoveryv1beta1.AddressType, 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("addressType"), &obj.AddressType, safe.Field(oldObj, func(oldObj *discoveryv1beta1.EndpointSlice) *discoveryv1beta1.AddressType { return &oldObj.AddressType }), oldObj != nil)...) // field discoveryv1beta1.EndpointSlice.Endpoints errs = append(errs, diff --git a/pkg/apis/discovery/validation/validation.go b/pkg/apis/discovery/validation/validation.go index 9c9e441a583..be93d297206 100644 --- a/pkg/apis/discovery/validation/validation.go +++ b/pkg/apis/discovery/validation/validation.go @@ -207,7 +207,7 @@ func validateAddressType(addressType discovery.AddressType) field.ErrorList { allErrs := field.ErrorList{} if addressType == "" { - allErrs = append(allErrs, field.Required(field.NewPath("addressType"), "")) + allErrs = append(allErrs, field.Required(field.NewPath("addressType"), "").MarkCoveredByDeclarative()) } else if !supportedAddressTypes.Has(addressType) { allErrs = append(allErrs, field.NotSupported(field.NewPath("addressType"), addressType, sets.List(supportedAddressTypes))) } diff --git a/pkg/registry/discovery/endpointslice/declarative_validation_test.go b/pkg/registry/discovery/endpointslice/declarative_validation_test.go index cf4bb110d4e..9c78e9caa06 100644 --- a/pkg/registry/discovery/endpointslice/declarative_validation_test.go +++ b/pkg/registry/discovery/endpointslice/declarative_validation_test.go @@ -70,6 +70,12 @@ func testDeclarativeValidate(t *testing.T, apiVersion string) { field.TooMany(field.NewPath("endpoints").Index(0).Child("addresses"), 101, 100).WithOrigin("maxItems"), }, }, + "invalid missing addressType": { + input: mkValidEndpointSlice(tweakAddressType("")), + expectedErrs: field.ErrorList{ + field.Required(field.NewPath("addressType"), ""), + }, + }, } for name, tc := range testCases { t.Run(name, func(t *testing.T) { @@ -164,3 +170,9 @@ func tweakAddresses(count int) func(*discovery.EndpointSlice) { obj.Endpoints[0].Addresses = addrs } } + +func tweakAddressType(addrType discovery.AddressType) func(*discovery.EndpointSlice) { + return func(obj *discovery.EndpointSlice) { + obj.AddressType = addrType + } +} diff --git a/staging/src/k8s.io/api/discovery/v1/generated.proto b/staging/src/k8s.io/api/discovery/v1/generated.proto index 7c987d8162f..146865d8fc0 100644 --- a/staging/src/k8s.io/api/discovery/v1/generated.proto +++ b/staging/src/k8s.io/api/discovery/v1/generated.proto @@ -184,6 +184,8 @@ message EndpointSlice { // The EndpointSlice controller only generates, and kube-proxy only processes, // slices of addressType "IPv4" and "IPv6". No semantics are defined for // the "FQDN" type. + // +required + // +k8s:required optional string addressType = 4; // endpoints is a list of unique endpoints in this slice. Each slice may diff --git a/staging/src/k8s.io/api/discovery/v1/types.go b/staging/src/k8s.io/api/discovery/v1/types.go index 20b4c43f044..a682a4596df 100644 --- a/staging/src/k8s.io/api/discovery/v1/types.go +++ b/staging/src/k8s.io/api/discovery/v1/types.go @@ -48,6 +48,8 @@ type EndpointSlice struct { // The EndpointSlice controller only generates, and kube-proxy only processes, // slices of addressType "IPv4" and "IPv6". No semantics are defined for // the "FQDN" type. + // +required + // +k8s:required AddressType AddressType `json:"addressType" protobuf:"bytes,4,rep,name=addressType"` // endpoints is a list of unique endpoints in this slice. Each slice may diff --git a/staging/src/k8s.io/api/discovery/v1beta1/generated.proto b/staging/src/k8s.io/api/discovery/v1beta1/generated.proto index 1de1d5e3c2a..2be880149e1 100644 --- a/staging/src/k8s.io/api/discovery/v1beta1/generated.proto +++ b/staging/src/k8s.io/api/discovery/v1beta1/generated.proto @@ -170,6 +170,8 @@ message EndpointSlice { // * IPv4: Represents an IPv4 Address. // * IPv6: Represents an IPv6 Address. // * FQDN: Represents a Fully Qualified Domain Name. + // +required + // +k8s:required optional string addressType = 4; // endpoints is a list of unique endpoints in this slice. Each slice may diff --git a/staging/src/k8s.io/api/discovery/v1beta1/types.go b/staging/src/k8s.io/api/discovery/v1beta1/types.go index 0f3b403d5ae..fbc65ab5fdf 100644 --- a/staging/src/k8s.io/api/discovery/v1beta1/types.go +++ b/staging/src/k8s.io/api/discovery/v1beta1/types.go @@ -45,6 +45,8 @@ type EndpointSlice struct { // * IPv4: Represents an IPv4 Address. // * IPv6: Represents an IPv6 Address. // * FQDN: Represents a Fully Qualified Domain Name. + // +required + // +k8s:required AddressType AddressType `json:"addressType" protobuf:"bytes,4,rep,name=addressType"` // endpoints is a list of unique endpoints in this slice. Each slice may From 5ee4b49ebd79b4fdef802c7e92af5b5cab6cdb9b Mon Sep 17 00:00:00 2001 From: CLBRITTON2 Date: Fri, 9 Jan 2026 08:36:47 -0500 Subject: [PATCH 6/7] Add declarative validation tests for EndpointSlice addressType immutability --- pkg/apis/discovery/v1/zz_generated.validations.go | 4 ++++ pkg/apis/discovery/v1beta1/zz_generated.validations.go | 4 ++++ pkg/apis/discovery/validation/validation.go | 2 +- .../discovery/endpointslice/declarative_validation_test.go | 7 +++++++ staging/src/k8s.io/api/discovery/v1/generated.proto | 1 + staging/src/k8s.io/api/discovery/v1/types.go | 1 + staging/src/k8s.io/api/discovery/v1beta1/generated.proto | 1 + staging/src/k8s.io/api/discovery/v1beta1/types.go | 1 + 8 files changed, 20 insertions(+), 1 deletion(-) diff --git a/pkg/apis/discovery/v1/zz_generated.validations.go b/pkg/apis/discovery/v1/zz_generated.validations.go index 2766f592446..e5fed4b47f8 100644 --- a/pkg/apis/discovery/v1/zz_generated.validations.go +++ b/pkg/apis/discovery/v1/zz_generated.validations.go @@ -101,6 +101,10 @@ func Validate_EndpointSlice(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.RequiredValue(ctx, op, fldPath, obj, oldObj); len(e) != 0 { errs = append(errs, e...) earlyReturn = true diff --git a/pkg/apis/discovery/v1beta1/zz_generated.validations.go b/pkg/apis/discovery/v1beta1/zz_generated.validations.go index ba337446aa7..c41ef7de08b 100644 --- a/pkg/apis/discovery/v1beta1/zz_generated.validations.go +++ b/pkg/apis/discovery/v1beta1/zz_generated.validations.go @@ -100,6 +100,10 @@ func Validate_EndpointSlice(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.RequiredValue(ctx, op, fldPath, obj, oldObj); len(e) != 0 { errs = append(errs, e...) earlyReturn = true diff --git a/pkg/apis/discovery/validation/validation.go b/pkg/apis/discovery/validation/validation.go index be93d297206..89a6b584dfd 100644 --- a/pkg/apis/discovery/validation/validation.go +++ b/pkg/apis/discovery/validation/validation.go @@ -86,7 +86,7 @@ func ValidateEndpointSliceCreate(endpointSlice *discovery.EndpointSlice) field.E // ValidateEndpointSliceUpdate validates an EndpointSlice when it is updated. func ValidateEndpointSliceUpdate(newEndpointSlice, oldEndpointSlice *discovery.EndpointSlice) field.ErrorList { allErrs := ValidateEndpointSlice(newEndpointSlice, oldEndpointSlice) - allErrs = append(allErrs, apivalidation.ValidateImmutableField(newEndpointSlice.AddressType, oldEndpointSlice.AddressType, field.NewPath("addressType"))...) + allErrs = append(allErrs, apivalidation.ValidateImmutableField(newEndpointSlice.AddressType, oldEndpointSlice.AddressType, field.NewPath("addressType")).WithOrigin("immutable").MarkCoveredByDeclarative()...) return allErrs } diff --git a/pkg/registry/discovery/endpointslice/declarative_validation_test.go b/pkg/registry/discovery/endpointslice/declarative_validation_test.go index 9c78e9caa06..496a4b042ae 100644 --- a/pkg/registry/discovery/endpointslice/declarative_validation_test.go +++ b/pkg/registry/discovery/endpointslice/declarative_validation_test.go @@ -122,6 +122,13 @@ func testDeclarativeValidateUpdate(t *testing.T, apiVersion string) { field.TooMany(field.NewPath("endpoints").Index(0).Child("addresses"), 101, 100).WithOrigin("maxItems"), }, }, + "invalid update addressType immutable": { + oldObj: mkValidEndpointSlice(), + updateObj: mkValidEndpointSlice(tweakAddressType(discovery.AddressTypeIPv6)), + expectedErrs: field.ErrorList{ + field.Invalid(field.NewPath("addressType"), discovery.AddressTypeIPv6, "field is immutable").WithOrigin("immutable"), + }, + }, } for name, tc := range testCases { t.Run(name, func(t *testing.T) { diff --git a/staging/src/k8s.io/api/discovery/v1/generated.proto b/staging/src/k8s.io/api/discovery/v1/generated.proto index 146865d8fc0..d8bbf2d4ab7 100644 --- a/staging/src/k8s.io/api/discovery/v1/generated.proto +++ b/staging/src/k8s.io/api/discovery/v1/generated.proto @@ -186,6 +186,7 @@ message EndpointSlice { // the "FQDN" type. // +required // +k8s:required + // +k8s:immutable optional string addressType = 4; // endpoints is a list of unique endpoints in this slice. Each slice may diff --git a/staging/src/k8s.io/api/discovery/v1/types.go b/staging/src/k8s.io/api/discovery/v1/types.go index a682a4596df..de83652ac0c 100644 --- a/staging/src/k8s.io/api/discovery/v1/types.go +++ b/staging/src/k8s.io/api/discovery/v1/types.go @@ -50,6 +50,7 @@ type EndpointSlice struct { // the "FQDN" type. // +required // +k8s:required + // +k8s:immutable AddressType AddressType `json:"addressType" protobuf:"bytes,4,rep,name=addressType"` // endpoints is a list of unique endpoints in this slice. Each slice may diff --git a/staging/src/k8s.io/api/discovery/v1beta1/generated.proto b/staging/src/k8s.io/api/discovery/v1beta1/generated.proto index 2be880149e1..d540d2ea06f 100644 --- a/staging/src/k8s.io/api/discovery/v1beta1/generated.proto +++ b/staging/src/k8s.io/api/discovery/v1beta1/generated.proto @@ -172,6 +172,7 @@ message EndpointSlice { // * FQDN: Represents a Fully Qualified Domain Name. // +required // +k8s:required + // +k8s:immutable optional string addressType = 4; // endpoints is a list of unique endpoints in this slice. Each slice may diff --git a/staging/src/k8s.io/api/discovery/v1beta1/types.go b/staging/src/k8s.io/api/discovery/v1beta1/types.go index fbc65ab5fdf..a04d98d01c1 100644 --- a/staging/src/k8s.io/api/discovery/v1beta1/types.go +++ b/staging/src/k8s.io/api/discovery/v1beta1/types.go @@ -47,6 +47,7 @@ type EndpointSlice struct { // * FQDN: Represents a Fully Qualified Domain Name. // +required // +k8s:required + // +k8s:immutable AddressType AddressType `json:"addressType" protobuf:"bytes,4,rep,name=addressType"` // endpoints is a list of unique endpoints in this slice. Each slice may From 6d69ee09379ef380f2eed61b6f1c0dcfe3776132 Mon Sep 17 00:00:00 2001 From: CLBRITTON2 Date: Fri, 9 Jan 2026 08:53:41 -0500 Subject: [PATCH 7/7] Add declarative validation tests for EndpointSlice addressType supported values --- pkg/apis/discovery/v1/zz_generated.validations.go | 13 +++++++++++++ .../discovery/v1beta1/zz_generated.validations.go | 13 +++++++++++++ pkg/apis/discovery/validation/validation.go | 2 +- pkg/generated/openapi/zz_generated.openapi.go | 3 ++- .../endpointslice/declarative_validation_test.go | 6 ++++++ staging/src/k8s.io/api/discovery/v1/types.go | 1 + staging/src/k8s.io/api/discovery/v1beta1/types.go | 2 ++ 7 files changed, 38 insertions(+), 2 deletions(-) diff --git a/pkg/apis/discovery/v1/zz_generated.validations.go b/pkg/apis/discovery/v1/zz_generated.validations.go index e5fed4b47f8..00458b52b3d 100644 --- a/pkg/apis/discovery/v1/zz_generated.validations.go +++ b/pkg/apis/discovery/v1/zz_generated.validations.go @@ -31,6 +31,7 @@ import ( safe "k8s.io/apimachinery/pkg/api/safe" validate "k8s.io/apimachinery/pkg/api/validate" runtime "k8s.io/apimachinery/pkg/runtime" + sets "k8s.io/apimachinery/pkg/util/sets" field "k8s.io/apimachinery/pkg/util/validation/field" ) @@ -50,6 +51,16 @@ func RegisterValidations(scheme *runtime.Scheme) error { return nil } +var symbolsForAddressType = sets.New(discoveryv1.AddressTypeFQDN, discoveryv1.AddressTypeIPv4, discoveryv1.AddressTypeIPv6) + +// Validate_AddressType validates an instance of AddressType according +// to declarative validation rules in the API schema. +func Validate_AddressType(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *discoveryv1.AddressType) (errs field.ErrorList) { + errs = append(errs, validate.Enum(ctx, op, fldPath, obj, oldObj, symbolsForAddressType, nil)...) + + return errs +} + // 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) { @@ -112,6 +123,8 @@ func Validate_EndpointSlice(ctx context.Context, op operation.Operation, fldPath if earlyReturn { return // do not proceed } + // call the type's validation function + errs = append(errs, Validate_AddressType(ctx, op, fldPath, obj, oldObj)...) return }(fldPath.Child("addressType"), &obj.AddressType, safe.Field(oldObj, func(oldObj *discoveryv1.EndpointSlice) *discoveryv1.AddressType { return &oldObj.AddressType }), oldObj != nil)...) diff --git a/pkg/apis/discovery/v1beta1/zz_generated.validations.go b/pkg/apis/discovery/v1beta1/zz_generated.validations.go index c41ef7de08b..2bf2d20f6fe 100644 --- a/pkg/apis/discovery/v1beta1/zz_generated.validations.go +++ b/pkg/apis/discovery/v1beta1/zz_generated.validations.go @@ -31,6 +31,7 @@ import ( safe "k8s.io/apimachinery/pkg/api/safe" validate "k8s.io/apimachinery/pkg/api/validate" runtime "k8s.io/apimachinery/pkg/runtime" + sets "k8s.io/apimachinery/pkg/util/sets" field "k8s.io/apimachinery/pkg/util/validation/field" ) @@ -50,6 +51,16 @@ func RegisterValidations(scheme *runtime.Scheme) error { return nil } +var symbolsForAddressType = sets.New(discoveryv1beta1.AddressTypeFQDN, discoveryv1beta1.AddressTypeIPv4, discoveryv1beta1.AddressTypeIPv6) + +// Validate_AddressType validates an instance of AddressType according +// to declarative validation rules in the API schema. +func Validate_AddressType(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *discoveryv1beta1.AddressType) (errs field.ErrorList) { + errs = append(errs, validate.Enum(ctx, op, fldPath, obj, oldObj, symbolsForAddressType, nil)...) + + return errs +} + // 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) { @@ -111,6 +122,8 @@ func Validate_EndpointSlice(ctx context.Context, op operation.Operation, fldPath if earlyReturn { return // do not proceed } + // call the type's validation function + errs = append(errs, Validate_AddressType(ctx, op, fldPath, obj, oldObj)...) return }(fldPath.Child("addressType"), &obj.AddressType, safe.Field(oldObj, func(oldObj *discoveryv1beta1.EndpointSlice) *discoveryv1beta1.AddressType { return &oldObj.AddressType }), oldObj != nil)...) diff --git a/pkg/apis/discovery/validation/validation.go b/pkg/apis/discovery/validation/validation.go index 89a6b584dfd..10bdc69a1d4 100644 --- a/pkg/apis/discovery/validation/validation.go +++ b/pkg/apis/discovery/validation/validation.go @@ -209,7 +209,7 @@ func validateAddressType(addressType discovery.AddressType) field.ErrorList { if addressType == "" { allErrs = append(allErrs, field.Required(field.NewPath("addressType"), "").MarkCoveredByDeclarative()) } else if !supportedAddressTypes.Has(addressType) { - allErrs = append(allErrs, field.NotSupported(field.NewPath("addressType"), addressType, sets.List(supportedAddressTypes))) + allErrs = append(allErrs, field.NotSupported(field.NewPath("addressType"), addressType, sets.List(supportedAddressTypes)).MarkCoveredByDeclarative()) } return allErrs diff --git a/pkg/generated/openapi/zz_generated.openapi.go b/pkg/generated/openapi/zz_generated.openapi.go index abfc6af67c8..68b1c0dfd2f 100644 --- a/pkg/generated/openapi/zz_generated.openapi.go +++ b/pkg/generated/openapi/zz_generated.openapi.go @@ -33603,10 +33603,11 @@ func schema_k8sio_api_discovery_v1beta1_EndpointSlice(ref common.ReferenceCallba }, "addressType": { SchemaProps: spec.SchemaProps{ - Description: "addressType specifies the type of address carried by this EndpointSlice. All addresses in this slice must be the same type. This field is immutable after creation. The following address types are currently supported: * IPv4: Represents an IPv4 Address. * IPv6: Represents an IPv6 Address. * FQDN: Represents a Fully Qualified Domain Name.", + Description: "addressType specifies the type of address carried by this EndpointSlice. All addresses in this slice must be the same type. This field is immutable after creation. The following address types are currently supported: * IPv4: Represents an IPv4 Address. * IPv6: Represents an IPv6 Address. * FQDN: Represents a Fully Qualified Domain Name.\n\nPossible enum values:\n - `\"FQDN\"` represents a FQDN.\n - `\"IPv4\"` represents an IPv4 Address.\n - `\"IPv6\"` represents an IPv6 Address.", Default: "", Type: []string{"string"}, Format: "", + Enum: []interface{}{"FQDN", "IPv4", "IPv6"}, }, }, "endpoints": { diff --git a/pkg/registry/discovery/endpointslice/declarative_validation_test.go b/pkg/registry/discovery/endpointslice/declarative_validation_test.go index 496a4b042ae..9f17498b3c3 100644 --- a/pkg/registry/discovery/endpointslice/declarative_validation_test.go +++ b/pkg/registry/discovery/endpointslice/declarative_validation_test.go @@ -76,6 +76,12 @@ func testDeclarativeValidate(t *testing.T, apiVersion string) { field.Required(field.NewPath("addressType"), ""), }, }, + "invalid addressType not supported": { + input: mkValidEndpointSlice(tweakAddressType("invalid")), + expectedErrs: field.ErrorList{ + field.NotSupported(field.NewPath("addressType"), discovery.AddressType("invalid"), []string{string(discovery.AddressTypeIPv4), string(discovery.AddressTypeIPv6), string(discovery.AddressTypeFQDN)}), + }, + }, } for name, tc := range testCases { t.Run(name, func(t *testing.T) { diff --git a/staging/src/k8s.io/api/discovery/v1/types.go b/staging/src/k8s.io/api/discovery/v1/types.go index de83652ac0c..b38104553cd 100644 --- a/staging/src/k8s.io/api/discovery/v1/types.go +++ b/staging/src/k8s.io/api/discovery/v1/types.go @@ -72,6 +72,7 @@ type EndpointSlice struct { // AddressType represents the type of address referred to by an endpoint. // +enum +// +k8s:enum type AddressType string const ( diff --git a/staging/src/k8s.io/api/discovery/v1beta1/types.go b/staging/src/k8s.io/api/discovery/v1beta1/types.go index a04d98d01c1..9d882019336 100644 --- a/staging/src/k8s.io/api/discovery/v1beta1/types.go +++ b/staging/src/k8s.io/api/discovery/v1beta1/types.go @@ -66,6 +66,8 @@ type EndpointSlice struct { } // AddressType represents the type of address referred to by an endpoint. +// +enum +// +k8s:enum type AddressType string const (