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.