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 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..00458b52b3d --- /dev/null +++ b/pkg/apis/discovery/v1/zz_generated.validations.go @@ -0,0 +1,145 @@ +//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" + sets "k8s.io/apimachinery/pkg/util/sets" + 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 +} + +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) { + // 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.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 + } + 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 + 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.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 + } + 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)...) + + // 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/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..2bf2d20f6fe --- /dev/null +++ b/pkg/apis/discovery/v1beta1/zz_generated.validations.go @@ -0,0 +1,144 @@ +//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" + sets "k8s.io/apimachinery/pkg/util/sets" + 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 +} + +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) { + // 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.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 + } + 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 + 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.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 + } + 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)...) + + // 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..10bdc69a1d4 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 } @@ -104,11 +104,10 @@ 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)) + 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. @@ -208,9 +207,9 @@ 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))) + 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 new file mode 100644 index 00000000000..9f17498b3c3 --- /dev/null +++ b/pkg/registry/discovery/endpointslice/declarative_validation_test.go @@ -0,0 +1,191 @@ +/* +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 + +import ( + "fmt" + "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(), + }, + "valid at limit endpoint addresses": { + input: mkValidEndpointSlice(tweakAddresses(100)), + }, + "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"), ""), + }, + }, + "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"), + }, + }, + "invalid missing addressType": { + input: mkValidEndpointSlice(tweakAddressType("")), + expectedErrs: field.ErrorList{ + 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) { + 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(), + updateObj: mkValidEndpointSlice(), + }, + "valid update at limit endpoint addresses": { + oldObj: mkValidEndpointSlice(), + updateObj: mkValidEndpointSlice(tweakAddresses(100)), + }, + "invalid update missing addresses": { + oldObj: mkValidEndpointSlice(), + updateObj: mkValidEndpointSlice(func(obj *discovery.EndpointSlice) { + 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"), + }, + }, + "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) { + ctx := genericapirequest.WithRequestInfo(genericapirequest.NewDefaultContext(), &genericapirequest.RequestInfo{ + APIPrefix: "apis", + APIGroup: "discovery.k8s.io", + APIVersion: apiVersion, + Resource: "endpointslices", + Name: "valid-endpointslice", + IsResourceRequest: true, + Verb: "update", + }) + tc.oldObj.ResourceVersion = "1" + tc.updateObj.ResourceVersion = "2" + 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 +} + +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 + } +} + +func tweakAddressType(addrType discovery.AddressType) func(*discovery.EndpointSlice) { + return func(obj *discovery.EndpointSlice) { + obj.AddressType = addrType + } +} 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. diff --git a/staging/src/k8s.io/api/discovery/v1/generated.proto b/staging/src/k8s.io/api/discovery/v1/generated.proto index 684b6358e33..d8bbf2d4ab7 100644 --- a/staging/src/k8s.io/api/discovery/v1/generated.proto +++ b/staging/src/k8s.io/api/discovery/v1/generated.proto @@ -38,6 +38,9 @@ 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 + // +k8s:maxItems=100 repeated string addresses = 1; // conditions contains information about the current status of the endpoint. @@ -181,6 +184,9 @@ 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 + // +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 7b49933c05d..b38104553cd 100644 --- a/staging/src/k8s.io/api/discovery/v1/types.go +++ b/staging/src/k8s.io/api/discovery/v1/types.go @@ -48,6 +48,9 @@ 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 + // +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 @@ -69,6 +72,7 @@ type EndpointSlice struct { // AddressType represents the type of address referred to by an endpoint. // +enum +// +k8s:enum type AddressType string const ( @@ -91,6 +95,9 @@ 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 + // +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 7b9d983b831..d540d2ea06f 100644 --- a/staging/src/k8s.io/api/discovery/v1beta1/generated.proto +++ b/staging/src/k8s.io/api/discovery/v1beta1/generated.proto @@ -38,6 +38,9 @@ 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 + // +k8s:maxItems=100 repeated string addresses = 1; // conditions contains information about the current status of the endpoint. @@ -167,6 +170,9 @@ message EndpointSlice { // * IPv4: Represents an IPv4 Address. // * IPv6: Represents an IPv6 Address. // * 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 11ec1b3968a..9d882019336 100644 --- a/staging/src/k8s.io/api/discovery/v1beta1/types.go +++ b/staging/src/k8s.io/api/discovery/v1beta1/types.go @@ -45,6 +45,9 @@ type EndpointSlice struct { // * IPv4: Represents an IPv4 Address. // * IPv6: Represents an IPv6 Address. // * 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 @@ -63,6 +66,8 @@ type EndpointSlice struct { } // AddressType represents the type of address referred to by an endpoint. +// +enum +// +k8s:enum type AddressType string const ( @@ -85,6 +90,9 @@ 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 + // +k8s:maxItems=100 Addresses []string `json:"addresses" protobuf:"bytes,1,rep,name=addresses"` // conditions contains information about the current status of the endpoint.