diff --git a/api/openapi-spec/swagger.json b/api/openapi-spec/swagger.json index c9720c06cde..f660e5ba7d0 100644 --- a/api/openapi-spec/swagger.json +++ b/api/openapi-spec/swagger.json @@ -11018,6 +11018,10 @@ "description": "deprecatedTopology contains topology information part of the v1beta1 API. This field is deprecated, and will be removed when the v1beta1 API is removed (no sooner than kubernetes v1.24). While this field can hold values, it is not writable through the v1 API, and any attempts to write to it will be silently ignored. Topology information can be found in the zone and nodeName fields instead.", "type": "object" }, + "hints": { + "$ref": "#/definitions/io.k8s.api.discovery.v1.EndpointHints", + "description": "hints contains information associated with how an endpoint should be consumed." + }, "hostname": { "description": "hostname of this endpoint. This field may be used by consumers of endpoints to distinguish endpoints from each other (e.g. in DNS names). Multiple endpoints which use the same hostname should be considered fungible (e.g. multiple A values in DNS). Must be lowercase and pass DNS Label (RFC 1123) validation.", "type": "string" @@ -11058,6 +11062,20 @@ }, "type": "object" }, + "io.k8s.api.discovery.v1.EndpointHints": { + "description": "EndpointHints provides hints describing how an endpoint should be consumed.", + "properties": { + "forZones": { + "description": "forZones indicates the zone(s) this endpoint should be consumed by to enable topology aware routing.", + "items": { + "$ref": "#/definitions/io.k8s.api.discovery.v1.ForZone" + }, + "type": "array", + "x-kubernetes-list-type": "atomic" + } + }, + "type": "object" + }, "io.k8s.api.discovery.v1.EndpointPort": { "description": "EndpointPort represents a Port used by an EndpointSlice", "properties": { @@ -11165,6 +11183,19 @@ } ] }, + "io.k8s.api.discovery.v1.ForZone": { + "description": "ForZone provides information about which zones should consume this endpoint.", + "properties": { + "name": { + "description": "name represents the name of the zone.", + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + }, "io.k8s.api.discovery.v1beta1.Endpoint": { "description": "Endpoint represents a single logical \"backend\" implementing a service.", "properties": { @@ -11180,6 +11211,10 @@ "$ref": "#/definitions/io.k8s.api.discovery.v1beta1.EndpointConditions", "description": "conditions contains information about the current status of the endpoint." }, + "hints": { + "$ref": "#/definitions/io.k8s.api.discovery.v1beta1.EndpointHints", + "description": "hints contains information associated with how an endpoint should be consumed." + }, "hostname": { "description": "hostname of this endpoint. This field may be used by consumers of endpoints to distinguish endpoints from each other (e.g. in DNS names). Multiple endpoints which use the same hostname should be considered fungible (e.g. multiple A values in DNS). Must be lowercase and pass DNS Label (RFC 1123) validation.", "type": "string" @@ -11223,6 +11258,20 @@ }, "type": "object" }, + "io.k8s.api.discovery.v1beta1.EndpointHints": { + "description": "EndpointHints provides hints describing how an endpoint should be consumed.", + "properties": { + "forZones": { + "description": "forZones indicates the zone(s) this endpoint should be consumed by to enable topology aware routing. May contain a maximum of 8 entries.", + "items": { + "$ref": "#/definitions/io.k8s.api.discovery.v1beta1.ForZone" + }, + "type": "array", + "x-kubernetes-list-type": "atomic" + } + }, + "type": "object" + }, "io.k8s.api.discovery.v1beta1.EndpointPort": { "description": "EndpointPort represents a Port used by an EndpointSlice", "properties": { @@ -11330,6 +11379,19 @@ } ] }, + "io.k8s.api.discovery.v1beta1.ForZone": { + "description": "ForZone provides information about which zones should consume this endpoint.", + "properties": { + "name": { + "description": "name represents the name of the zone.", + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + }, "io.k8s.api.events.v1.Event": { "description": "Event is a report of an event somewhere in the cluster. It generally denotes some state change in the system. Events have a limited retention time and triggers and messages may evolve with time. Event consumers should not rely on the timing of an event with a given Reason reflecting a consistent underlying trigger, or the continued existence of events with that Reason. Events should be treated as informative, best-effort, supplemental data.", "properties": { diff --git a/cmd/kube-proxy/app/server.go b/cmd/kube-proxy/app/server.go index 2d5ca952c31..f7b57b2582b 100644 --- a/cmd/kube-proxy/app/server.go +++ b/cmd/kube-proxy/app/server.go @@ -750,7 +750,7 @@ func (s *ProxyServer) Run() error { // functions must configure their shared informer event handlers first. informerFactory.Start(wait.NeverStop) - if utilfeature.DefaultFeatureGate.Enabled(features.ServiceTopology) { + if utilfeature.DefaultFeatureGate.Enabled(features.ServiceTopology) || utilfeature.DefaultFeatureGate.Enabled(features.TopologyAwareHints) { // Make an informer that selects for our nodename. currentNodeInformerFactory := informers.NewSharedInformerFactoryWithOptions(s.Client, s.ConfigSyncPeriod, informers.WithTweakListOptions(func(options *metav1.ListOptions) { diff --git a/pkg/apis/core/annotation_key_constants.go b/pkg/apis/core/annotation_key_constants.go index f7194fb2168..3bb1d1bdd05 100644 --- a/pkg/apis/core/annotation_key_constants.go +++ b/pkg/apis/core/annotation_key_constants.go @@ -116,4 +116,9 @@ const ( // // This annotation is alpha-level and is only honored when PodDeletionCost feature is enabled. PodDeletionCost = "controller.kubernetes.io/pod-deletion-cost" + + // AnnotationTopologyAwareHints can be used to enable or disable Topology + // Aware Hints for a Service. This may be set to "auto" or "disabled". Any + // other value is treated as "disabled". + AnnotationTopologyAwareHints = "service.kubernetes.io/topology-aware-hints" ) diff --git a/pkg/apis/discovery/types.go b/pkg/apis/discovery/types.go index 39eeeb26bc0..bb93ee47a1a 100644 --- a/pkg/apis/discovery/types.go +++ b/pkg/apis/discovery/types.go @@ -100,6 +100,11 @@ type Endpoint struct { // zone is the name of the Zone this endpoint exists in. // +optional Zone *string + // hints contains information associated with how an endpoint should be + // consumed. + // +featureGate=TopologyAwareHints + // +optional + Hints *EndpointHints } // EndpointConditions represents the current condition of an endpoint. @@ -127,6 +132,19 @@ type EndpointConditions struct { Terminating *bool } +// EndpointHints provides hints describing how an endpoint should be consumed. +type EndpointHints struct { + // forZones indicates the zone(s) this endpoint should be consumed by to + // enable topology aware routing. May contain a maximum of 8 entries. + ForZones []ForZone +} + +// ForZone provides information about which zones should consume this endpoint. +type ForZone struct { + // name represents the name of the zone. + Name string +} + // EndpointPort represents a Port used by an EndpointSlice. type EndpointPort struct { // The name of this port. All ports in an EndpointSlice must have a unique diff --git a/pkg/apis/discovery/v1/zz_generated.conversion.go b/pkg/apis/discovery/v1/zz_generated.conversion.go index cd5c66abade..5894e48ff54 100644 --- a/pkg/apis/discovery/v1/zz_generated.conversion.go +++ b/pkg/apis/discovery/v1/zz_generated.conversion.go @@ -58,6 +58,16 @@ func RegisterConversions(s *runtime.Scheme) error { }); err != nil { return err } + if err := s.AddGeneratedConversionFunc((*v1.EndpointHints)(nil), (*discovery.EndpointHints)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1_EndpointHints_To_discovery_EndpointHints(a.(*v1.EndpointHints), b.(*discovery.EndpointHints), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*discovery.EndpointHints)(nil), (*v1.EndpointHints)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_discovery_EndpointHints_To_v1_EndpointHints(a.(*discovery.EndpointHints), b.(*v1.EndpointHints), scope) + }); err != nil { + return err + } if err := s.AddGeneratedConversionFunc((*v1.EndpointPort)(nil), (*discovery.EndpointPort)(nil), func(a, b interface{}, scope conversion.Scope) error { return Convert_v1_EndpointPort_To_discovery_EndpointPort(a.(*v1.EndpointPort), b.(*discovery.EndpointPort), scope) }); err != nil { @@ -88,6 +98,16 @@ func RegisterConversions(s *runtime.Scheme) error { }); err != nil { return err } + if err := s.AddGeneratedConversionFunc((*v1.ForZone)(nil), (*discovery.ForZone)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1_ForZone_To_discovery_ForZone(a.(*v1.ForZone), b.(*discovery.ForZone), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*discovery.ForZone)(nil), (*v1.ForZone)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_discovery_ForZone_To_v1_ForZone(a.(*discovery.ForZone), b.(*v1.ForZone), scope) + }); err != nil { + return err + } return nil } @@ -101,6 +121,7 @@ func autoConvert_v1_Endpoint_To_discovery_Endpoint(in *v1.Endpoint, out *discove out.DeprecatedTopology = *(*map[string]string)(unsafe.Pointer(&in.DeprecatedTopology)) out.NodeName = (*string)(unsafe.Pointer(in.NodeName)) out.Zone = (*string)(unsafe.Pointer(in.Zone)) + out.Hints = (*discovery.EndpointHints)(unsafe.Pointer(in.Hints)) return nil } @@ -119,6 +140,7 @@ func autoConvert_discovery_Endpoint_To_v1_Endpoint(in *discovery.Endpoint, out * out.DeprecatedTopology = *(*map[string]string)(unsafe.Pointer(&in.DeprecatedTopology)) out.NodeName = (*string)(unsafe.Pointer(in.NodeName)) out.Zone = (*string)(unsafe.Pointer(in.Zone)) + out.Hints = (*v1.EndpointHints)(unsafe.Pointer(in.Hints)) return nil } @@ -151,6 +173,26 @@ func Convert_discovery_EndpointConditions_To_v1_EndpointConditions(in *discovery return autoConvert_discovery_EndpointConditions_To_v1_EndpointConditions(in, out, s) } +func autoConvert_v1_EndpointHints_To_discovery_EndpointHints(in *v1.EndpointHints, out *discovery.EndpointHints, s conversion.Scope) error { + out.ForZones = *(*[]discovery.ForZone)(unsafe.Pointer(&in.ForZones)) + return nil +} + +// Convert_v1_EndpointHints_To_discovery_EndpointHints is an autogenerated conversion function. +func Convert_v1_EndpointHints_To_discovery_EndpointHints(in *v1.EndpointHints, out *discovery.EndpointHints, s conversion.Scope) error { + return autoConvert_v1_EndpointHints_To_discovery_EndpointHints(in, out, s) +} + +func autoConvert_discovery_EndpointHints_To_v1_EndpointHints(in *discovery.EndpointHints, out *v1.EndpointHints, s conversion.Scope) error { + out.ForZones = *(*[]v1.ForZone)(unsafe.Pointer(&in.ForZones)) + return nil +} + +// Convert_discovery_EndpointHints_To_v1_EndpointHints is an autogenerated conversion function. +func Convert_discovery_EndpointHints_To_v1_EndpointHints(in *discovery.EndpointHints, out *v1.EndpointHints, s conversion.Scope) error { + return autoConvert_discovery_EndpointHints_To_v1_EndpointHints(in, out, s) +} + func autoConvert_v1_EndpointPort_To_discovery_EndpointPort(in *v1.EndpointPort, out *discovery.EndpointPort, s conversion.Scope) error { out.Name = (*string)(unsafe.Pointer(in.Name)) out.Protocol = (*core.Protocol)(unsafe.Pointer(in.Protocol)) @@ -224,3 +266,23 @@ func autoConvert_discovery_EndpointSliceList_To_v1_EndpointSliceList(in *discove func Convert_discovery_EndpointSliceList_To_v1_EndpointSliceList(in *discovery.EndpointSliceList, out *v1.EndpointSliceList, s conversion.Scope) error { return autoConvert_discovery_EndpointSliceList_To_v1_EndpointSliceList(in, out, s) } + +func autoConvert_v1_ForZone_To_discovery_ForZone(in *v1.ForZone, out *discovery.ForZone, s conversion.Scope) error { + out.Name = in.Name + return nil +} + +// Convert_v1_ForZone_To_discovery_ForZone is an autogenerated conversion function. +func Convert_v1_ForZone_To_discovery_ForZone(in *v1.ForZone, out *discovery.ForZone, s conversion.Scope) error { + return autoConvert_v1_ForZone_To_discovery_ForZone(in, out, s) +} + +func autoConvert_discovery_ForZone_To_v1_ForZone(in *discovery.ForZone, out *v1.ForZone, s conversion.Scope) error { + out.Name = in.Name + return nil +} + +// Convert_discovery_ForZone_To_v1_ForZone is an autogenerated conversion function. +func Convert_discovery_ForZone_To_v1_ForZone(in *discovery.ForZone, out *v1.ForZone, s conversion.Scope) error { + return autoConvert_discovery_ForZone_To_v1_ForZone(in, out, s) +} diff --git a/pkg/apis/discovery/v1beta1/zz_generated.conversion.go b/pkg/apis/discovery/v1beta1/zz_generated.conversion.go index b78c50d94b6..e13697a9ed5 100644 --- a/pkg/apis/discovery/v1beta1/zz_generated.conversion.go +++ b/pkg/apis/discovery/v1beta1/zz_generated.conversion.go @@ -48,6 +48,16 @@ func RegisterConversions(s *runtime.Scheme) error { }); err != nil { return err } + if err := s.AddGeneratedConversionFunc((*v1beta1.EndpointHints)(nil), (*discovery.EndpointHints)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1beta1_EndpointHints_To_discovery_EndpointHints(a.(*v1beta1.EndpointHints), b.(*discovery.EndpointHints), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*discovery.EndpointHints)(nil), (*v1beta1.EndpointHints)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_discovery_EndpointHints_To_v1beta1_EndpointHints(a.(*discovery.EndpointHints), b.(*v1beta1.EndpointHints), scope) + }); err != nil { + return err + } if err := s.AddGeneratedConversionFunc((*v1beta1.EndpointPort)(nil), (*discovery.EndpointPort)(nil), func(a, b interface{}, scope conversion.Scope) error { return Convert_v1beta1_EndpointPort_To_discovery_EndpointPort(a.(*v1beta1.EndpointPort), b.(*discovery.EndpointPort), scope) }); err != nil { @@ -78,6 +88,16 @@ func RegisterConversions(s *runtime.Scheme) error { }); err != nil { return err } + if err := s.AddGeneratedConversionFunc((*v1beta1.ForZone)(nil), (*discovery.ForZone)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1beta1_ForZone_To_discovery_ForZone(a.(*v1beta1.ForZone), b.(*discovery.ForZone), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*discovery.ForZone)(nil), (*v1beta1.ForZone)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_discovery_ForZone_To_v1beta1_ForZone(a.(*discovery.ForZone), b.(*v1beta1.ForZone), scope) + }); err != nil { + return err + } if err := s.AddConversionFunc((*discovery.Endpoint)(nil), (*v1beta1.Endpoint)(nil), func(a, b interface{}, scope conversion.Scope) error { return Convert_discovery_Endpoint_To_v1beta1_Endpoint(a.(*discovery.Endpoint), b.(*v1beta1.Endpoint), scope) }); err != nil { @@ -100,6 +120,7 @@ func autoConvert_v1beta1_Endpoint_To_discovery_Endpoint(in *v1beta1.Endpoint, ou out.TargetRef = (*core.ObjectReference)(unsafe.Pointer(in.TargetRef)) // WARNING: in.Topology requires manual conversion: does not exist in peer-type out.NodeName = (*string)(unsafe.Pointer(in.NodeName)) + out.Hints = (*discovery.EndpointHints)(unsafe.Pointer(in.Hints)) return nil } @@ -113,6 +134,7 @@ func autoConvert_discovery_Endpoint_To_v1beta1_Endpoint(in *discovery.Endpoint, // WARNING: in.DeprecatedTopology requires manual conversion: does not exist in peer-type out.NodeName = (*string)(unsafe.Pointer(in.NodeName)) // WARNING: in.Zone requires manual conversion: does not exist in peer-type + out.Hints = (*v1beta1.EndpointHints)(unsafe.Pointer(in.Hints)) return nil } @@ -140,6 +162,26 @@ func Convert_discovery_EndpointConditions_To_v1beta1_EndpointConditions(in *disc return autoConvert_discovery_EndpointConditions_To_v1beta1_EndpointConditions(in, out, s) } +func autoConvert_v1beta1_EndpointHints_To_discovery_EndpointHints(in *v1beta1.EndpointHints, out *discovery.EndpointHints, s conversion.Scope) error { + out.ForZones = *(*[]discovery.ForZone)(unsafe.Pointer(&in.ForZones)) + return nil +} + +// Convert_v1beta1_EndpointHints_To_discovery_EndpointHints is an autogenerated conversion function. +func Convert_v1beta1_EndpointHints_To_discovery_EndpointHints(in *v1beta1.EndpointHints, out *discovery.EndpointHints, s conversion.Scope) error { + return autoConvert_v1beta1_EndpointHints_To_discovery_EndpointHints(in, out, s) +} + +func autoConvert_discovery_EndpointHints_To_v1beta1_EndpointHints(in *discovery.EndpointHints, out *v1beta1.EndpointHints, s conversion.Scope) error { + out.ForZones = *(*[]v1beta1.ForZone)(unsafe.Pointer(&in.ForZones)) + return nil +} + +// Convert_discovery_EndpointHints_To_v1beta1_EndpointHints is an autogenerated conversion function. +func Convert_discovery_EndpointHints_To_v1beta1_EndpointHints(in *discovery.EndpointHints, out *v1beta1.EndpointHints, s conversion.Scope) error { + return autoConvert_discovery_EndpointHints_To_v1beta1_EndpointHints(in, out, s) +} + func autoConvert_v1beta1_EndpointPort_To_discovery_EndpointPort(in *v1beta1.EndpointPort, out *discovery.EndpointPort, s conversion.Scope) error { out.Name = (*string)(unsafe.Pointer(in.Name)) out.Protocol = (*core.Protocol)(unsafe.Pointer(in.Protocol)) @@ -253,3 +295,23 @@ func autoConvert_discovery_EndpointSliceList_To_v1beta1_EndpointSliceList(in *di func Convert_discovery_EndpointSliceList_To_v1beta1_EndpointSliceList(in *discovery.EndpointSliceList, out *v1beta1.EndpointSliceList, s conversion.Scope) error { return autoConvert_discovery_EndpointSliceList_To_v1beta1_EndpointSliceList(in, out, s) } + +func autoConvert_v1beta1_ForZone_To_discovery_ForZone(in *v1beta1.ForZone, out *discovery.ForZone, s conversion.Scope) error { + out.Name = in.Name + return nil +} + +// Convert_v1beta1_ForZone_To_discovery_ForZone is an autogenerated conversion function. +func Convert_v1beta1_ForZone_To_discovery_ForZone(in *v1beta1.ForZone, out *discovery.ForZone, s conversion.Scope) error { + return autoConvert_v1beta1_ForZone_To_discovery_ForZone(in, out, s) +} + +func autoConvert_discovery_ForZone_To_v1beta1_ForZone(in *discovery.ForZone, out *v1beta1.ForZone, s conversion.Scope) error { + out.Name = in.Name + return nil +} + +// Convert_discovery_ForZone_To_v1beta1_ForZone is an autogenerated conversion function. +func Convert_discovery_ForZone_To_v1beta1_ForZone(in *discovery.ForZone, out *v1beta1.ForZone, s conversion.Scope) error { + return autoConvert_discovery_ForZone_To_v1beta1_ForZone(in, out, s) +} diff --git a/pkg/apis/discovery/validation/validation.go b/pkg/apis/discovery/validation/validation.go index 19b774a2c66..c0a13ee48ea 100644 --- a/pkg/apis/discovery/validation/validation.go +++ b/pkg/apis/discovery/validation/validation.go @@ -45,6 +45,7 @@ var ( maxAddresses = 100 maxPorts = 20000 maxEndpoints = 1000 + maxZoneHints = 8 ) // ValidateEndpointSliceName can be used to check whether the given endpoint @@ -125,6 +126,10 @@ func validateEndpoints(endpoints []discovery.Endpoint, addrType discovery.Addres if endpoint.Hostname != nil { allErrs = append(allErrs, apivalidation.ValidateDNS1123Label(*endpoint.Hostname, idxPath.Child("hostname"))...) } + + if endpoint.Hints != nil { + allErrs = append(allErrs, validateHints(endpoint.Hints, idxPath.Child("hints"))...) + } } return allErrs @@ -179,3 +184,29 @@ func validateAddressType(addressType discovery.AddressType) field.ErrorList { return allErrs } + +func validateHints(endpointHints *discovery.EndpointHints, fldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + + fzPath := fldPath.Child("forZones") + if len(endpointHints.ForZones) > maxZoneHints { + allErrs = append(allErrs, field.TooMany(fzPath, len(endpointHints.ForZones), maxZoneHints)) + return allErrs + } + + zoneNames := sets.String{} + for i, forZone := range endpointHints.ForZones { + zonePath := fzPath.Index(i).Child("name") + if zoneNames.Has(forZone.Name) { + allErrs = append(allErrs, field.Duplicate(zonePath, forZone.Name)) + } else { + zoneNames.Insert(forZone.Name) + } + + for _, msg := range validation.IsValidLabelValue(forZone.Name) { + allErrs = append(allErrs, field.Invalid(zonePath, forZone.Name, msg)) + } + } + + return allErrs +} diff --git a/pkg/apis/discovery/validation/validation_test.go b/pkg/apis/discovery/validation/validation_test.go index 1343d02467b..ea62422efa0 100644 --- a/pkg/apis/discovery/validation/validation_test.go +++ b/pkg/apis/discovery/validation/validation_test.go @@ -203,6 +203,23 @@ func TestValidateEndpointSlice(t *testing.T) { }}, }, }, + "valid-hints": { + expectedErrors: 0, + endpointSlice: &discovery.EndpointSlice{ + ObjectMeta: standardMeta, + AddressType: discovery.AddressTypeIPv4, + Ports: []discovery.EndpointPort{{ + Name: utilpointer.StringPtr("http"), + Protocol: protocolPtr(api.ProtocolTCP), + }}, + Endpoints: []discovery.Endpoint{{ + Addresses: generateIPAddresses(1), + Hints: &discovery.EndpointHints{ + ForZones: []discovery.ForZone{{Name: "zone-a"}}, + }, + }}, + }, + }, // expected failures "duplicate-port-name": { @@ -451,6 +468,71 @@ func TestValidateEndpointSlice(t *testing.T) { }}, }, }, + "invalid-hints": { + expectedErrors: 1, + endpointSlice: &discovery.EndpointSlice{ + ObjectMeta: standardMeta, + AddressType: discovery.AddressTypeIPv4, + Ports: []discovery.EndpointPort{{ + Name: utilpointer.StringPtr("http"), + Protocol: protocolPtr(api.ProtocolTCP), + }}, + Endpoints: []discovery.Endpoint{{ + Addresses: generateIPAddresses(1), + Hints: &discovery.EndpointHints{ + ForZones: []discovery.ForZone{{Name: "inv@lid"}}, + }, + }}, + }, + }, + "overlapping-hints": { + expectedErrors: 1, + endpointSlice: &discovery.EndpointSlice{ + ObjectMeta: standardMeta, + AddressType: discovery.AddressTypeIPv4, + Ports: []discovery.EndpointPort{{ + Name: utilpointer.StringPtr("http"), + Protocol: protocolPtr(api.ProtocolTCP), + }}, + Endpoints: []discovery.Endpoint{{ + Addresses: generateIPAddresses(1), + Hints: &discovery.EndpointHints{ + ForZones: []discovery.ForZone{ + {Name: "zone-a"}, + {Name: "zone-b"}, + {Name: "zone-a"}, + }, + }, + }}, + }, + }, + "too-many-hints": { + expectedErrors: 1, + endpointSlice: &discovery.EndpointSlice{ + ObjectMeta: standardMeta, + AddressType: discovery.AddressTypeIPv4, + Ports: []discovery.EndpointPort{{ + Name: utilpointer.StringPtr("http"), + Protocol: protocolPtr(api.ProtocolTCP), + }}, + Endpoints: []discovery.Endpoint{{ + Addresses: generateIPAddresses(1), + Hints: &discovery.EndpointHints{ + ForZones: []discovery.ForZone{ + {Name: "zone-a"}, + {Name: "zone-b"}, + {Name: "zone-c"}, + {Name: "zone-d"}, + {Name: "zone-e"}, + {Name: "zone-f"}, + {Name: "zone-g"}, + {Name: "zone-h"}, + {Name: "zone-i"}, + }, + }, + }}, + }, + }, "empty-everything": { expectedErrors: 3, endpointSlice: &discovery.EndpointSlice{}, diff --git a/pkg/apis/discovery/zz_generated.deepcopy.go b/pkg/apis/discovery/zz_generated.deepcopy.go index 4514b56fcf2..52bfe6957cc 100644 --- a/pkg/apis/discovery/zz_generated.deepcopy.go +++ b/pkg/apis/discovery/zz_generated.deepcopy.go @@ -61,6 +61,11 @@ func (in *Endpoint) DeepCopyInto(out *Endpoint) { *out = new(string) **out = **in } + if in.Hints != nil { + in, out := &in.Hints, &out.Hints + *out = new(EndpointHints) + (*in).DeepCopyInto(*out) + } return } @@ -105,6 +110,27 @@ func (in *EndpointConditions) DeepCopy() *EndpointConditions { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *EndpointHints) DeepCopyInto(out *EndpointHints) { + *out = *in + if in.ForZones != nil { + in, out := &in.ForZones, &out.ForZones + *out = make([]ForZone, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EndpointHints. +func (in *EndpointHints) DeepCopy() *EndpointHints { + if in == nil { + return nil + } + out := new(EndpointHints) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *EndpointPort) DeepCopyInto(out *EndpointPort) { *out = *in @@ -213,3 +239,19 @@ func (in *EndpointSliceList) DeepCopyObject() runtime.Object { } return nil } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ForZone) DeepCopyInto(out *ForZone) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ForZone. +func (in *ForZone) DeepCopy() *ForZone { + if in == nil { + return nil + } + out := new(ForZone) + in.DeepCopyInto(out) + return out +} diff --git a/pkg/controller/endpointslice/endpointslice_controller.go b/pkg/controller/endpointslice/endpointslice_controller.go index b5009fa0429..b570455a7f4 100644 --- a/pkg/controller/endpointslice/endpointslice_controller.go +++ b/pkg/controller/endpointslice/endpointslice_controller.go @@ -28,6 +28,7 @@ import ( "k8s.io/apimachinery/pkg/labels" utilruntime "k8s.io/apimachinery/pkg/util/runtime" "k8s.io/apimachinery/pkg/util/wait" + utilfeature "k8s.io/apiserver/pkg/util/feature" coreinformers "k8s.io/client-go/informers/core/v1" discoveryinformers "k8s.io/client-go/informers/discovery/v1" clientset "k8s.io/client-go/kubernetes" @@ -42,7 +43,9 @@ import ( "k8s.io/klog/v2" "k8s.io/kubernetes/pkg/controller" endpointslicemetrics "k8s.io/kubernetes/pkg/controller/endpointslice/metrics" + "k8s.io/kubernetes/pkg/controller/endpointslice/topologycache" endpointutil "k8s.io/kubernetes/pkg/controller/util/endpoint" + "k8s.io/kubernetes/pkg/features" ) const ( @@ -142,13 +145,6 @@ func NewController(podInformer coreinformers.PodInformer, c.maxEndpointsPerSlice = maxEndpointsPerSlice - c.reconciler = &reconciler{ - client: c.client, - nodeLister: c.nodeLister, - maxEndpointsPerSlice: c.maxEndpointsPerSlice, - endpointSliceTracker: c.endpointSliceTracker, - metricsCache: endpointslicemetrics.NewCache(maxEndpointsPerSlice), - } c.triggerTimeTracker = endpointutil.NewTriggerTimeTracker() c.eventBroadcaster = broadcaster @@ -157,6 +153,25 @@ func NewController(podInformer coreinformers.PodInformer, c.endpointUpdatesBatchPeriod = endpointUpdatesBatchPeriod c.serviceSelectorCache = endpointutil.NewServiceSelectorCache() + if utilfeature.DefaultFeatureGate.Enabled(features.TopologyAwareHints) { + nodeInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ + AddFunc: c.addNode, + UpdateFunc: c.updateNode, + DeleteFunc: c.deleteNode, + }) + + c.topologyCache = topologycache.NewTopologyCache() + } + + c.reconciler = &reconciler{ + client: c.client, + nodeLister: c.nodeLister, + maxEndpointsPerSlice: c.maxEndpointsPerSlice, + endpointSliceTracker: c.endpointSliceTracker, + metricsCache: endpointslicemetrics.NewCache(maxEndpointsPerSlice), + topologyCache: c.topologyCache, + } + return c } @@ -227,6 +242,10 @@ type Controller struct { // serviceSelectorCache is a cache of service selectors to avoid high CPU consumption caused by frequent calls // to AsSelectorPreValidated (see #73527) serviceSelectorCache *endpointutil.ServiceSelectorCache + + // topologyCache tracks the distribution of Nodes and endpoints across zones + // to enable TopologyAwareHints. + topologyCache *topologycache.TopologyCache } // Run will not return until stopCh is closed. @@ -275,6 +294,8 @@ func (c *Controller) processNextWorkItem() bool { } func (c *Controller) handleErr(err error, key interface{}) { + trackSync(err) + if err == nil { c.queue.Forget(key) return @@ -490,3 +511,50 @@ func (c *Controller) deletePod(obj interface{}) { c.addPod(pod) } } + +func (c *Controller) addNode(obj interface{}) { + c.checkNodeTopologyDistribution() +} + +func (c *Controller) updateNode(old, cur interface{}) { + oldNode := old.(*v1.Node) + curNode := cur.(*v1.Node) + + if topologycache.NodeReady(oldNode.Status) != topologycache.NodeReady(curNode.Status) { + c.checkNodeTopologyDistribution() + } +} + +func (c *Controller) deleteNode(obj interface{}) { + c.checkNodeTopologyDistribution() +} + +// checkNodeTopologyDistribution updates Nodes in the topology cache and then +// queues any Services that are past the threshold. +func (c *Controller) checkNodeTopologyDistribution() { + if c.topologyCache == nil { + return + } + nodes, err := c.nodeLister.List(labels.Everything()) + if err != nil { + klog.Errorf("Error listing Nodes: %v", err) + } + c.topologyCache.SetNodes(nodes) + serviceKeys := c.topologyCache.GetOverloadedServices() + for _, serviceKey := range serviceKeys { + c.queue.Add(serviceKey) + } +} + +// trackSync increments the EndpointSliceSyncs metric with the result of a sync. +func trackSync(err error) { + metricLabel := "success" + if err != nil { + if isStaleInformerCacheErr(err) { + metricLabel = "stale" + } else { + metricLabel = "error" + } + } + endpointslicemetrics.EndpointSliceSyncs.WithLabelValues(metricLabel).Inc() +} diff --git a/pkg/controller/endpointslice/endpointslice_controller_test.go b/pkg/controller/endpointslice/endpointslice_controller_test.go index a2b09e40f5d..a8fda60bba3 100644 --- a/pkg/controller/endpointslice/endpointslice_controller_test.go +++ b/pkg/controller/endpointslice/endpointslice_controller_test.go @@ -27,6 +27,7 @@ import ( "github.com/stretchr/testify/assert" v1 "k8s.io/api/core/v1" discovery "k8s.io/api/discovery/v1" + "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" @@ -40,6 +41,7 @@ import ( "k8s.io/client-go/tools/cache" featuregatetesting "k8s.io/component-base/featuregate/testing" "k8s.io/kubernetes/pkg/controller" + "k8s.io/kubernetes/pkg/controller/endpointslice/topologycache" endpointutil "k8s.io/kubernetes/pkg/controller/util/endpoint" "k8s.io/kubernetes/pkg/features" utilpointer "k8s.io/utils/pointer" @@ -1501,6 +1503,183 @@ func TestSyncServiceStaleInformer(t *testing.T) { } } +func Test_checkNodeTopologyDistribution(t *testing.T) { + zoneA := "zone-a" + zoneB := "zone-b" + zoneC := "zone-c" + + readyTrue := true + readyFalse := false + + cpu100 := resource.MustParse("100m") + cpu1000 := resource.MustParse("1000m") + cpu2000 := resource.MustParse("2000m") + + type nodeInfo struct { + zoneLabel *string + ready *bool + cpu *resource.Quantity + } + + testCases := []struct { + name string + nodes []nodeInfo + topologyCacheEnabled bool + endpointZoneInfo map[string]topologycache.EndpointZoneInfo + expectedQueueLen int + }{{ + name: "empty", + nodes: []nodeInfo{}, + topologyCacheEnabled: false, + endpointZoneInfo: map[string]topologycache.EndpointZoneInfo{}, + expectedQueueLen: 0, + }, { + name: "lopsided, queue required", + nodes: []nodeInfo{ + {zoneLabel: &zoneA, ready: &readyTrue, cpu: &cpu100}, + {zoneLabel: &zoneB, ready: &readyTrue, cpu: &cpu1000}, + {zoneLabel: &zoneC, ready: &readyTrue, cpu: &cpu2000}, + }, + topologyCacheEnabled: true, + endpointZoneInfo: map[string]topologycache.EndpointZoneInfo{ + "ns/svc1": {zoneA: 1, zoneB: 2, zoneC: 3}, + }, + expectedQueueLen: 1, + }, { + name: "lopsided but 1 unready, queue required because unready node means 0 CPU in one zone", + nodes: []nodeInfo{ + {zoneLabel: &zoneA, ready: &readyFalse, cpu: &cpu100}, + {zoneLabel: &zoneB, ready: &readyTrue, cpu: &cpu1000}, + {zoneLabel: &zoneC, ready: &readyTrue, cpu: &cpu2000}, + }, + topologyCacheEnabled: true, + endpointZoneInfo: map[string]topologycache.EndpointZoneInfo{ + "ns/svc1": {zoneA: 1, zoneB: 2, zoneC: 3}, + }, + expectedQueueLen: 1, + }, { + name: "even zones, uneven endpoint distribution but within threshold, no sync required", + nodes: []nodeInfo{ + {zoneLabel: &zoneB, ready: &readyTrue, cpu: &cpu2000}, + {zoneLabel: &zoneB, ready: &readyTrue, cpu: &cpu2000}, + {zoneLabel: &zoneC, ready: &readyTrue, cpu: &cpu2000}, + {zoneLabel: &zoneC, ready: &readyTrue, cpu: &cpu2000}, + }, + topologyCacheEnabled: true, + endpointZoneInfo: map[string]topologycache.EndpointZoneInfo{ + "ns/svc1": {zoneB: 5, zoneC: 4}, + }, + expectedQueueLen: 0, + }, { + name: "even zones but node missing zone, sync required", + nodes: []nodeInfo{ + {zoneLabel: &zoneB, ready: &readyTrue, cpu: &cpu2000}, + {ready: &readyTrue, cpu: &cpu2000}, + {zoneLabel: &zoneC, ready: &readyTrue, cpu: &cpu2000}, + {zoneLabel: &zoneC, ready: &readyTrue, cpu: &cpu2000}, + }, + topologyCacheEnabled: true, + endpointZoneInfo: map[string]topologycache.EndpointZoneInfo{ + "ns/svc1": {zoneB: 5, zoneC: 4}, + }, + expectedQueueLen: 1, + }, { + name: "even zones but node missing cpu, sync required", + nodes: []nodeInfo{ + {zoneLabel: &zoneB, ready: &readyTrue, cpu: &cpu2000}, + {zoneLabel: &zoneB, ready: &readyTrue}, + {zoneLabel: &zoneC, ready: &readyTrue, cpu: &cpu2000}, + {zoneLabel: &zoneC, ready: &readyTrue, cpu: &cpu2000}, + }, + topologyCacheEnabled: true, + endpointZoneInfo: map[string]topologycache.EndpointZoneInfo{ + "ns/svc1": {zoneB: 5, zoneC: 4}, + }, + expectedQueueLen: 1, + }, { + name: "even zones, uneven endpoint distribution beyond threshold, no sync required", + nodes: []nodeInfo{ + {zoneLabel: &zoneB, ready: &readyTrue, cpu: &cpu2000}, + {zoneLabel: &zoneB, ready: &readyTrue, cpu: &cpu2000}, + {zoneLabel: &zoneC, ready: &readyTrue, cpu: &cpu2000}, + {zoneLabel: &zoneC, ready: &readyTrue, cpu: &cpu2000}, + }, + topologyCacheEnabled: true, + endpointZoneInfo: map[string]topologycache.EndpointZoneInfo{ + "ns/svc1": {zoneB: 6, zoneC: 4}, + }, + expectedQueueLen: 1, + }, { + name: "3 uneven zones, matching endpoint distribution, no sync required", + nodes: []nodeInfo{ + {zoneLabel: &zoneA, ready: &readyTrue, cpu: &cpu2000}, + {zoneLabel: &zoneB, ready: &readyTrue, cpu: &cpu1000}, + {zoneLabel: &zoneC, ready: &readyTrue, cpu: &cpu100}, + }, + topologyCacheEnabled: true, + endpointZoneInfo: map[string]topologycache.EndpointZoneInfo{ + "ns/svc1": {zoneA: 20, zoneB: 10, zoneC: 1}, + }, + expectedQueueLen: 0, + }, { + name: "3 uneven zones, endpoint distribution within threshold but below 1, sync required", + nodes: []nodeInfo{ + {zoneLabel: &zoneA, ready: &readyTrue, cpu: &cpu2000}, + {zoneLabel: &zoneB, ready: &readyTrue, cpu: &cpu1000}, + {zoneLabel: &zoneC, ready: &readyTrue, cpu: &cpu100}, + }, + topologyCacheEnabled: true, + endpointZoneInfo: map[string]topologycache.EndpointZoneInfo{ + "ns/svc1": {zoneA: 20, zoneB: 10, zoneC: 0}, + }, + expectedQueueLen: 1, + }} + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + _, esController := newController([]string{}, time.Duration(0)) + + for i, nodeInfo := range tc.nodes { + node := &v1.Node{ + ObjectMeta: metav1.ObjectMeta{Name: fmt.Sprintf("node-%d", i)}, + Status: v1.NodeStatus{}, + } + if nodeInfo.zoneLabel != nil { + node.Labels = map[string]string{v1.LabelTopologyZone: *nodeInfo.zoneLabel} + } + if nodeInfo.ready != nil { + status := v1.ConditionFalse + if *nodeInfo.ready { + status = v1.ConditionTrue + } + node.Status.Conditions = []v1.NodeCondition{{ + Type: v1.NodeReady, + Status: status, + }} + } + if nodeInfo.cpu != nil { + node.Status.Allocatable = v1.ResourceList{ + v1.ResourceCPU: *nodeInfo.cpu, + } + } + esController.nodeStore.Add(node) + if tc.topologyCacheEnabled { + esController.topologyCache = topologycache.NewTopologyCache() + for serviceKey, endpointZoneInfo := range tc.endpointZoneInfo { + esController.topologyCache.SetHints(serviceKey, discovery.AddressTypeIPv4, endpointZoneInfo) + } + } + } + + esController.checkNodeTopologyDistribution() + + if esController.queue.Len() != tc.expectedQueueLen { + t.Errorf("Expected %d services to be queued, got %d", tc.expectedQueueLen, esController.queue.Len()) + } + }) + } +} + // Test helpers func addPods(t *testing.T, esController *endpointSliceController, namespace string, podsCount int) { t.Helper() diff --git a/pkg/controller/endpointslice/metrics/metrics.go b/pkg/controller/endpointslice/metrics/metrics.go index f00b5ceabe2..90d0fbb8f4f 100644 --- a/pkg/controller/endpointslice/metrics/metrics.go +++ b/pkg/controller/endpointslice/metrics/metrics.go @@ -93,6 +93,29 @@ var ( }, []string{"operation"}, ) + + // EndpointSlicesChangedPerSync observes the number of EndpointSlices + // changed per sync. + EndpointSlicesChangedPerSync = metrics.NewHistogramVec( + &metrics.HistogramOpts{ + Subsystem: EndpointSliceSubsystem, + Name: "endpointslices_changed_per_sync", + Help: "Number of EndpointSlices changed on each Service sync", + }, + []string{"topology"}, // either "auto" or "disabled" + ) + + // EndpointSliceSyncs tracks the number of sync operations the controller + // runs along with their result. + EndpointSliceSyncs = metrics.NewCounterVec( + &metrics.CounterOpts{ + Subsystem: EndpointSliceSubsystem, + Name: "syncs", + Help: "Number of EndpointSlice syncs", + StabilityLevel: metrics.ALPHA, + }, + []string{"result"}, // either "success", "stale", or "error" + ) ) var registerMetrics sync.Once @@ -106,5 +129,7 @@ func RegisterMetrics() { legacyregistry.MustRegister(NumEndpointSlices) legacyregistry.MustRegister(DesiredEndpointSlices) legacyregistry.MustRegister(EndpointSliceChanges) + legacyregistry.MustRegister(EndpointSlicesChangedPerSync) + legacyregistry.MustRegister(EndpointSliceSyncs) }) } diff --git a/pkg/controller/endpointslice/reconciler.go b/pkg/controller/endpointslice/reconciler.go index 88fcac6b091..2767af5497a 100644 --- a/pkg/controller/endpointslice/reconciler.go +++ b/pkg/controller/endpointslice/reconciler.go @@ -32,7 +32,9 @@ import ( utilfeature "k8s.io/apiserver/pkg/util/feature" clientset "k8s.io/client-go/kubernetes" corelisters "k8s.io/client-go/listers/core/v1" + "k8s.io/klog/v2" "k8s.io/kubernetes/pkg/controller/endpointslice/metrics" + "k8s.io/kubernetes/pkg/controller/endpointslice/topologycache" endpointutil "k8s.io/kubernetes/pkg/controller/util/endpoint" "k8s.io/kubernetes/pkg/features" ) @@ -45,6 +47,9 @@ type reconciler struct { maxEndpointsPerSlice int32 endpointSliceTracker *endpointSliceTracker metricsCache *metrics.Cache + // topologyCache tracks the distribution of Nodes and endpoints across zones + // to enable TopologyAwareHints. + topologyCache *topologycache.TopologyCache } // endpointMeta includes the attributes we group slices on, this type helps with @@ -73,6 +78,15 @@ func (r *reconciler) reconcile(service *corev1.Service, pods []*corev1.Pod, exis for _, existingSlice := range existingSlices { // service no longer supports that address type, add it to deleted slices if _, ok := serviceSupportedAddressesTypes[existingSlice.AddressType]; !ok { + if r.topologyCache != nil { + svcKey, err := serviceControllerKey(existingSlice) + if err != nil { + klog.Warningf("Couldn't get key to remove EndpointSlice from topology cache %+v: %v", existingSlice, err) + } else { + r.topologyCache.RemoveHints(svcKey, existingSlice.AddressType) + } + } + slicesToDelete = append(slicesToDelete, existingSlice) continue } @@ -222,6 +236,25 @@ func (r *reconciler) reconcileByAddressType(service *corev1.Service, pods []*cor serviceNN := types.NamespacedName{Name: service.Name, Namespace: service.Namespace} r.metricsCache.UpdateServicePortCache(serviceNN, spMetrics) + // Topology hints are assigned per address type. This means it is + // theoretically possible for endpoints of one address type to be assigned + // hints while another endpoints of another address type are not. + si := &topologycache.SliceInfo{ + ServiceKey: fmt.Sprintf("%s/%s", service.Namespace, service.Name), + ToCreate: slicesToCreate, + ToUpdate: slicesToUpdate, + Unchanged: unchangedSlices(existingSlices, slicesToUpdate, slicesToDelete), + } + + if r.topologyCache != nil && hintsEnabled(service.Annotations) { + slicesToCreate, slicesToUpdate = r.topologyCache.AddHints(si) + } else { + if r.topologyCache != nil { + r.topologyCache.RemoveHints(si.ServiceKey, addressType) + } + slicesToCreate, slicesToUpdate = topologycache.RemoveHintsFromSlices(si) + } + return r.finalize(service, slicesToCreate, slicesToUpdate, slicesToDelete, triggerTime) } @@ -297,6 +330,14 @@ func (r *reconciler) finalize( metrics.EndpointSliceChanges.WithLabelValues("delete").Inc() } + topologyLabel := "disabled" + if r.topologyCache != nil && hintsEnabled(service.Annotations) { + topologyLabel = "auto" + } + + numSlicesChanged := len(slicesToCreate) + len(slicesToUpdate) + len(slicesToDelete) + metrics.EndpointSlicesChangedPerSync.WithLabelValues(topologyLabel).Observe(float64(numSlicesChanged)) + return nil } diff --git a/pkg/controller/endpointslice/reconciler_test.go b/pkg/controller/endpointslice/reconciler_test.go index 6a185e29ac5..b20aa2cfb0a 100644 --- a/pkg/controller/endpointslice/reconciler_test.go +++ b/pkg/controller/endpointslice/reconciler_test.go @@ -29,6 +29,7 @@ import ( corev1 "k8s.io/api/core/v1" discovery "k8s.io/api/discovery/v1" apiequality "k8s.io/apimachinery/pkg/api/equality" + "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/util/intstr" @@ -41,6 +42,7 @@ import ( "k8s.io/component-base/metrics/testutil" "k8s.io/kubernetes/pkg/controller" "k8s.io/kubernetes/pkg/controller/endpointslice/metrics" + "k8s.io/kubernetes/pkg/controller/endpointslice/topologycache" "k8s.io/kubernetes/pkg/features" utilpointer "k8s.io/utils/pointer" ) @@ -66,7 +68,7 @@ func TestReconcileEmpty(t *testing.T) { assert.EqualValues(t, []discovery.EndpointPort{}, slices[0].Ports) assert.EqualValues(t, []discovery.Endpoint{}, slices[0].Endpoints) expectTrackedGeneration(t, r.endpointSliceTracker, &slices[0], 1) - expectMetrics(t, expectedMetrics{desiredSlices: 1, actualSlices: 1, desiredEndpoints: 0, addedPerSync: 0, removedPerSync: 0, numCreated: 1, numUpdated: 0, numDeleted: 0}) + expectMetrics(t, expectedMetrics{desiredSlices: 1, actualSlices: 1, desiredEndpoints: 0, addedPerSync: 0, removedPerSync: 0, numCreated: 1, numUpdated: 0, numDeleted: 0, slicesChangedPerSync: 1}) } // Given a single pod matching a service selector and no existing endpoint slices, @@ -436,16 +438,22 @@ func TestReconcile1Pod(t *testing.T) { expectTrackedGeneration(t, r.endpointSliceTracker, &slice, 1) + expectSlicesChangedPerSync := 1 + if testCase.service.Spec.IPFamilies != nil && len(testCase.service.Spec.IPFamilies) > 0 { + expectSlicesChangedPerSync = len(testCase.service.Spec.IPFamilies) + } expectMetrics(t, expectedMetrics{ - desiredSlices: 1, - actualSlices: 1, - desiredEndpoints: 1, - addedPerSync: len(testCase.expectedEndpointPerSlice), - removedPerSync: 0, - numCreated: len(testCase.expectedEndpointPerSlice), - numUpdated: 0, - numDeleted: 0}) + desiredSlices: 1, + actualSlices: 1, + desiredEndpoints: 1, + addedPerSync: len(testCase.expectedEndpointPerSlice), + removedPerSync: 0, + numCreated: len(testCase.expectedEndpointPerSlice), + numUpdated: 0, + numDeleted: 0, + slicesChangedPerSync: expectSlicesChangedPerSync, + }) } }) } @@ -478,7 +486,7 @@ func TestReconcile1EndpointSlice(t *testing.T) { assert.EqualValues(t, []discovery.EndpointPort{}, slices[0].Ports) assert.EqualValues(t, []discovery.Endpoint{}, slices[0].Endpoints) expectTrackedGeneration(t, r.endpointSliceTracker, &slices[0], 1) - expectMetrics(t, expectedMetrics{desiredSlices: 1, actualSlices: 1, desiredEndpoints: 0, addedPerSync: 0, removedPerSync: 0, numCreated: 0, numUpdated: 1, numDeleted: 0}) + expectMetrics(t, expectedMetrics{desiredSlices: 1, actualSlices: 1, desiredEndpoints: 0, addedPerSync: 0, removedPerSync: 0, numCreated: 0, numUpdated: 1, numDeleted: 0, slicesChangedPerSync: 1}) } // when a Service has PublishNotReadyAddresses set to true, corresponding @@ -539,7 +547,7 @@ func TestReconcileManyPods(t *testing.T) { // Two endpoint slices should be completely full, the remainder should be in another one expectUnorderedSlicesWithLengths(t, fetchEndpointSlices(t, client, namespace), []int{100, 100, 50}) - expectMetrics(t, expectedMetrics{desiredSlices: 3, actualSlices: 3, desiredEndpoints: 250, addedPerSync: 250, removedPerSync: 0, numCreated: 3, numUpdated: 0, numDeleted: 0}) + expectMetrics(t, expectedMetrics{desiredSlices: 3, actualSlices: 3, desiredEndpoints: 250, addedPerSync: 250, removedPerSync: 0, numCreated: 3, numUpdated: 0, numDeleted: 0, slicesChangedPerSync: 3}) } // now with preexisting slices, we have 250 pods matching a service @@ -590,7 +598,7 @@ func TestReconcileEndpointSlicesSomePreexisting(t *testing.T) { // 1 new slice (0->100) + 1 updated slice (62->89) expectUnorderedSlicesWithLengths(t, fetchEndpointSlices(t, client, namespace), []int{89, 61, 100}) - expectMetrics(t, expectedMetrics{desiredSlices: 3, actualSlices: 3, desiredEndpoints: 250, addedPerSync: 127, removedPerSync: 0, numCreated: 1, numUpdated: 1, numDeleted: 0}) + expectMetrics(t, expectedMetrics{desiredSlices: 3, actualSlices: 3, desiredEndpoints: 250, addedPerSync: 127, removedPerSync: 0, numCreated: 1, numUpdated: 1, numDeleted: 0, slicesChangedPerSync: 2}) // ensure cache mutation has not occurred cmc.Check(t) @@ -645,7 +653,7 @@ func TestReconcileEndpointSlicesSomePreexistingWorseAllocation(t *testing.T) { // 2 new slices (100, 52) in addition to existing slices (74, 74) expectUnorderedSlicesWithLengths(t, fetchEndpointSlices(t, client, namespace), []int{74, 74, 100, 52}) - expectMetrics(t, expectedMetrics{desiredSlices: 3, actualSlices: 4, desiredEndpoints: 300, addedPerSync: 152, removedPerSync: 0, numCreated: 2, numUpdated: 0, numDeleted: 0}) + expectMetrics(t, expectedMetrics{desiredSlices: 3, actualSlices: 4, desiredEndpoints: 300, addedPerSync: 152, removedPerSync: 0, numCreated: 2, numUpdated: 0, numDeleted: 0, slicesChangedPerSync: 2}) // ensure cache mutation has not occurred cmc.Check(t) @@ -804,7 +812,7 @@ func TestReconcileEndpointSlicesRecycling(t *testing.T) { // thanks to recycling, we get a free repack of endpoints, resulting in 3 full slices instead of 10 mostly empty slices expectUnorderedSlicesWithLengths(t, fetchEndpointSlices(t, client, namespace), []int{100, 100, 100}) - expectMetrics(t, expectedMetrics{desiredSlices: 3, actualSlices: 3, desiredEndpoints: 300, addedPerSync: 300, removedPerSync: 0, numCreated: 0, numUpdated: 3, numDeleted: 7}) + expectMetrics(t, expectedMetrics{desiredSlices: 3, actualSlices: 3, desiredEndpoints: 300, addedPerSync: 300, removedPerSync: 0, numCreated: 0, numUpdated: 3, numDeleted: 7, slicesChangedPerSync: 10}) // ensure cache mutation has not occurred cmc.Check(t) @@ -861,7 +869,7 @@ func TestReconcileEndpointSlicesUpdatePacking(t *testing.T) { // ensure that both endpoint slices have been updated expectActions(t, client.Actions(), 2, "update", "endpointslices") - expectMetrics(t, expectedMetrics{desiredSlices: 2, actualSlices: 2, desiredEndpoints: 115, addedPerSync: 15, removedPerSync: 0, numCreated: 0, numUpdated: 2, numDeleted: 0}) + expectMetrics(t, expectedMetrics{desiredSlices: 2, actualSlices: 2, desiredEndpoints: 115, addedPerSync: 15, removedPerSync: 0, numCreated: 0, numUpdated: 2, numDeleted: 0, slicesChangedPerSync: 2}) // additional pods should get added to fuller slice expectUnorderedSlicesWithLengths(t, fetchEndpointSlices(t, client, namespace), []int{95, 20}) @@ -1036,7 +1044,7 @@ func TestReconcileEndpointSlicesNamedPorts(t *testing.T) { // reconcile should create 5 endpoint slices assert.Equal(t, 5, len(client.Actions()), "Expected 5 client actions as part of reconcile") expectActions(t, client.Actions(), 5, "create", "endpointslices") - expectMetrics(t, expectedMetrics{desiredSlices: 5, actualSlices: 5, desiredEndpoints: 300, addedPerSync: 300, removedPerSync: 0, numCreated: 5, numUpdated: 0, numDeleted: 0}) + expectMetrics(t, expectedMetrics{desiredSlices: 5, actualSlices: 5, desiredEndpoints: 300, addedPerSync: 300, removedPerSync: 0, numCreated: 5, numUpdated: 0, numDeleted: 0, slicesChangedPerSync: 5}) fetchedSlices := fetchEndpointSlices(t, client, namespace) @@ -1082,23 +1090,23 @@ func TestReconcileMaxEndpointsPerSlice(t *testing.T) { { maxEndpointsPerSlice: int32(50), expectedSliceLengths: []int{50, 50, 50, 50, 50}, - expectedMetricValues: expectedMetrics{desiredSlices: 5, actualSlices: 5, desiredEndpoints: 250, addedPerSync: 250, numCreated: 5}, + expectedMetricValues: expectedMetrics{desiredSlices: 5, actualSlices: 5, desiredEndpoints: 250, addedPerSync: 250, numCreated: 5, slicesChangedPerSync: 5}, }, { maxEndpointsPerSlice: int32(80), expectedSliceLengths: []int{80, 80, 80, 10}, - expectedMetricValues: expectedMetrics{desiredSlices: 4, actualSlices: 4, desiredEndpoints: 250, addedPerSync: 250, numCreated: 4}, + expectedMetricValues: expectedMetrics{desiredSlices: 4, actualSlices: 4, desiredEndpoints: 250, addedPerSync: 250, numCreated: 4, slicesChangedPerSync: 4}, }, { maxEndpointsPerSlice: int32(150), expectedSliceLengths: []int{150, 100}, - expectedMetricValues: expectedMetrics{desiredSlices: 2, actualSlices: 2, desiredEndpoints: 250, addedPerSync: 250, numCreated: 2}, + expectedMetricValues: expectedMetrics{desiredSlices: 2, actualSlices: 2, desiredEndpoints: 250, addedPerSync: 250, numCreated: 2, slicesChangedPerSync: 2}, }, { maxEndpointsPerSlice: int32(250), expectedSliceLengths: []int{250}, - expectedMetricValues: expectedMetrics{desiredSlices: 1, actualSlices: 1, desiredEndpoints: 250, addedPerSync: 250, numCreated: 1}, + expectedMetricValues: expectedMetrics{desiredSlices: 1, actualSlices: 1, desiredEndpoints: 250, addedPerSync: 250, numCreated: 1, slicesChangedPerSync: 1}, }, { maxEndpointsPerSlice: int32(500), expectedSliceLengths: []int{250}, - expectedMetricValues: expectedMetrics{desiredSlices: 1, actualSlices: 1, desiredEndpoints: 250, addedPerSync: 250, numCreated: 1}, + expectedMetricValues: expectedMetrics{desiredSlices: 1, actualSlices: 1, desiredEndpoints: 250, addedPerSync: 250, numCreated: 1, slicesChangedPerSync: 1}, }, } @@ -1133,11 +1141,11 @@ func TestReconcileEndpointSlicesMetrics(t *testing.T) { assert.Equal(t, 1, len(actions), "Expected 1 additional client actions as part of reconcile") assert.True(t, actions[0].Matches("create", "endpointslices"), "First action should be create endpoint slice") - expectMetrics(t, expectedMetrics{desiredSlices: 1, actualSlices: 1, desiredEndpoints: 20, addedPerSync: 20, removedPerSync: 0, numCreated: 1, numUpdated: 0, numDeleted: 0}) + expectMetrics(t, expectedMetrics{desiredSlices: 1, actualSlices: 1, desiredEndpoints: 20, addedPerSync: 20, removedPerSync: 0, numCreated: 1, numUpdated: 0, numDeleted: 0, slicesChangedPerSync: 1}) fetchedSlices := fetchEndpointSlices(t, client, namespace) reconcileHelper(t, r, &svc, pods[0:10], []*discovery.EndpointSlice{&fetchedSlices[0]}, time.Now()) - expectMetrics(t, expectedMetrics{desiredSlices: 1, actualSlices: 1, desiredEndpoints: 10, addedPerSync: 20, removedPerSync: 10, numCreated: 1, numUpdated: 1, numDeleted: 0}) + expectMetrics(t, expectedMetrics{desiredSlices: 1, actualSlices: 1, desiredEndpoints: 10, addedPerSync: 20, removedPerSync: 10, numCreated: 1, numUpdated: 1, numDeleted: 0, slicesChangedPerSync: 2}) } // When a Service has a non-nil deletionTimestamp we want to avoid creating any @@ -1310,6 +1318,271 @@ func TestReconcilerFinalizeSvcDeletionTimestamp(t *testing.T) { } } +func TestReconcileTopology(t *testing.T) { + ns := "testing" + svc, endpointMeta := newServiceAndEndpointMeta("foo", ns) + + // 3 zones, 10 nodes and pods per zone + zones := []string{"zone-a", "zone-b", "zone-c"} + + pods := []*corev1.Pod{} + nodes := []*corev1.Node{} + nodesByName := map[string]*corev1.Node{} + for i, zone := range zones { + for j := 0; j < 10; j++ { + node := &corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("node-%s-%d", zone, j), + Labels: map[string]string{ + corev1.LabelTopologyZone: zone, + }, + }, + Status: corev1.NodeStatus{ + Conditions: []corev1.NodeCondition{{ + Type: corev1.NodeReady, + Status: corev1.ConditionTrue, + }}, + Allocatable: corev1.ResourceList{"cpu": resource.MustParse("100m")}, + }, + } + nodesByName[node.Name] = node + nodes = append(nodes, node) + + pod := newPod(i*100+j, ns, true, 1, false) + pod.Spec.NodeName = node.Name + pods = append(pods, pod) + } + } + + slicesByName := map[string]*discovery.EndpointSlice{} + slicePods := map[string][]*corev1.Pod{ + "zone-a-b": {pods[7], pods[8], pods[16], pods[17], pods[18]}, + "zone-a-c": {pods[5], pods[6], pods[25], pods[26]}, + "zone-c": {pods[27], pods[28], pods[29]}, + } + + gvk := schema.GroupVersionKind{Version: "v1", Kind: "Service"} + ownerRef := metav1.NewControllerRef(&svc, gvk) + + for name, pods := range slicePods { + endpoints := []discovery.Endpoint{} + for _, pod := range pods { + endpoints = append(endpoints, podToEndpoint(pod, nodesByName[pod.Spec.NodeName], &svc, endpointMeta.AddressType)) + } + + slicesByName[name] = &discovery.EndpointSlice{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + OwnerReferences: []metav1.OwnerReference{*ownerRef}, + Labels: map[string]string{ + discovery.LabelManagedBy: controllerName, + discovery.LabelServiceName: svc.Name, + }, + }, + AddressType: endpointMeta.AddressType, + Ports: endpointMeta.Ports, + Endpoints: endpoints, + } + } + + testCases := []struct { + name string + topologyCacheEnabled bool + hintsAnnotation string + existingSlices []*discovery.EndpointSlice + pods []*corev1.Pod + nodes []*corev1.Node + expectedHints map[string]int + expectedCrossZoneHints int + expectedMetrics expectedMetrics + }{{ + name: "no change, topologyCache disabled, annotation == auto", + topologyCacheEnabled: false, + hintsAnnotation: "auto", + existingSlices: []*discovery.EndpointSlice{slicesByName["zone-c"]}, + pods: slicePods["zone-c"], + nodes: nodes, + expectedHints: nil, + expectedCrossZoneHints: 0, + expectedMetrics: expectedMetrics{ + desiredSlices: 1, + actualSlices: 1, + desiredEndpoints: 3, + addedPerSync: 0, + removedPerSync: 0, + numCreated: 0, + numUpdated: 0, + numDeleted: 0, + slicesChangedPerSync: 0, + }, + }, { + name: "enabling topologyCache, hintsAnnotation == auto", + topologyCacheEnabled: true, + hintsAnnotation: "auto", + existingSlices: []*discovery.EndpointSlice{slicesByName["zone-c"]}, + pods: slicePods["zone-c"], + nodes: nodes, + expectedHints: map[string]int{ + "zone-a": 1, + "zone-b": 1, + "zone-c": 1, + }, + expectedCrossZoneHints: 2, + expectedMetrics: expectedMetrics{ + desiredSlices: 1, + actualSlices: 1, + desiredEndpoints: 3, + addedPerSync: 0, + removedPerSync: 0, + numCreated: 0, + numUpdated: 1, + numDeleted: 0, + slicesChangedPerSyncTopology: 1, + }, + }, { + name: "topology enabled, hintsAnnotation==auto, ratio beyond threshold", + topologyCacheEnabled: true, + hintsAnnotation: "auto", + existingSlices: []*discovery.EndpointSlice{slicesByName["zone-a-c"]}, + pods: slicePods["zone-a-c"], + nodes: nodes, + expectedHints: nil, + expectedCrossZoneHints: 0, + expectedMetrics: expectedMetrics{ + desiredSlices: 1, + actualSlices: 1, + desiredEndpoints: 4, + addedPerSync: 0, + removedPerSync: 0, + numCreated: 0, + numUpdated: 0, + numDeleted: 0, + slicesChangedPerSyncTopology: 0, + }, + }, { + name: "topology enabled, hintsAnnotation==auto, more slices and endpoints", + topologyCacheEnabled: true, + hintsAnnotation: "auto", + existingSlices: []*discovery.EndpointSlice{slicesByName["zone-a-c"], slicesByName["zone-a-b"]}, + pods: append(slicePods["zone-a-c"], slicePods["zone-a-b"]...), + nodes: nodes, + expectedHints: map[string]int{ + "zone-a": 3, + "zone-b": 3, + "zone-c": 3, + }, + expectedCrossZoneHints: 1, + expectedMetrics: expectedMetrics{ + desiredSlices: 1, + actualSlices: 1, + desiredEndpoints: 9, + addedPerSync: 0, + removedPerSync: 0, + numCreated: 0, + // TODO(robscott): Since we're potentially changing more slices when + // adding topology hints we could use it as a free repacking + // opportunity. That would make this value 1. + numUpdated: 2, + numDeleted: 0, + slicesChangedPerSyncTopology: 2, + }, + }, { + name: "topology enabled, hintsAnnotation==disabled, more slices and endpoints", + topologyCacheEnabled: true, + hintsAnnotation: "disabled", + existingSlices: []*discovery.EndpointSlice{slicesByName["zone-a-c"], slicesByName["zone-a-b"]}, + pods: append(slicePods["zone-a-c"], slicePods["zone-a-b"]...), + nodes: nodes, + expectedHints: nil, + expectedCrossZoneHints: 0, + expectedMetrics: expectedMetrics{ + desiredSlices: 1, + actualSlices: 1, + desiredEndpoints: 9, + addedPerSync: 0, + removedPerSync: 0, + numCreated: 0, + numUpdated: 0, + numDeleted: 0, + slicesChangedPerSync: 0, + }, + }} + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + client := newClientset() + cmc := newCacheMutationCheck(tc.existingSlices) + createEndpointSlices(t, client, ns, tc.existingSlices) + + setupMetrics() + r := newReconciler(client, tc.nodes, defaultMaxEndpointsPerSlice) + if tc.topologyCacheEnabled { + r.topologyCache = topologycache.NewTopologyCache() + r.topologyCache.SetNodes(tc.nodes) + } + + service := svc.DeepCopy() + service.Annotations = map[string]string{ + corev1.AnnotationTopologyAwareHints: tc.hintsAnnotation, + } + r.reconcile(service, tc.pods, tc.existingSlices, time.Now()) + + cmc.Check(t) + expectMetrics(t, tc.expectedMetrics) + fetchedSlices := fetchEndpointSlices(t, client, ns) + + if tc.expectedHints == nil { + for _, slice := range fetchedSlices { + for _, endpoint := range slice.Endpoints { + if endpoint.Hints != nil && len(endpoint.Hints.ForZones) > 0 { + t.Fatalf("Expected endpoint not to have zone hints: %+v", endpoint) + } + } + } + return + } + + actualCrossZoneHints := 0 + actualHints := map[string]int{} + + for _, slice := range fetchedSlices { + for _, endpoint := range slice.Endpoints { + if endpoint.Hints == nil || len(endpoint.Hints.ForZones) == 0 { + t.Fatalf("Expected endpoint to have zone hints: %+v", endpoint) + } + if len(endpoint.Hints.ForZones) > 1 { + t.Fatalf("Expected endpoint to only have 1 zone hint, got %d", len(endpoint.Hints.ForZones)) + } + + if endpoint.Zone == nil || *endpoint.Zone == "" { + t.Fatalf("Expected endpoint to have zone: %+v", endpoint) + } + zoneHint := endpoint.Hints.ForZones[0].Name + if *endpoint.Zone != zoneHint { + actualCrossZoneHints++ + } + actualHints[zoneHint]++ + } + } + + if len(actualHints) != len(tc.expectedHints) { + t.Errorf("Expected hints for %d zones, got %d", len(tc.expectedHints), len(actualHints)) + } + + for zone, expectedNum := range tc.expectedHints { + actualNum, _ := actualHints[zone] + if actualNum != expectedNum { + t.Errorf("Expected %d hints for %s zone, got %d", expectedNum, zone, actualNum) + } + } + + if actualCrossZoneHints != tc.expectedCrossZoneHints { + t.Errorf("Expected %d cross zone hints, got %d", tc.expectedCrossZoneHints, actualCrossZoneHints) + } + }) + } +} + // Test Helpers func newReconciler(client *fake.Clientset, nodes []*corev1.Node, maxEndpointsPerSlice int32) *reconciler { @@ -1446,14 +1719,18 @@ func reconcileHelper(t *testing.T, r *reconciler, service *corev1.Service, pods // Metrics helpers type expectedMetrics struct { - desiredSlices int - actualSlices int - desiredEndpoints int - addedPerSync int - removedPerSync int - numCreated int - numUpdated int - numDeleted int + desiredSlices int + actualSlices int + desiredEndpoints int + addedPerSync int + removedPerSync int + numCreated int + numUpdated int + numDeleted int + slicesChangedPerSync int + slicesChangedPerSyncTopology int + syncSuccesses int + syncErrors int } func expectMetrics(t *testing.T, em expectedMetrics) { @@ -1506,6 +1783,30 @@ func expectMetrics(t *testing.T, em expectedMetrics) { if actualDeleted != float64(em.numDeleted) { t.Errorf("Expected endpointSliceChangesDeleted to be %d, got %v", em.numDeleted, actualDeleted) } + + actualSlicesChangedPerSync, err := testutil.GetHistogramMetricValue(metrics.EndpointSlicesChangedPerSync.WithLabelValues("disabled")) + handleErr(t, err, "slicesChangedPerSync") + if actualSlicesChangedPerSync != float64(em.slicesChangedPerSync) { + t.Errorf("Expected slicesChangedPerSync to be %d, got %v", em.slicesChangedPerSync, actualSlicesChangedPerSync) + } + + actualSlicesChangedPerSyncTopology, err := testutil.GetHistogramMetricValue(metrics.EndpointSlicesChangedPerSync.WithLabelValues("auto")) + handleErr(t, err, "slicesChangedPerSyncTopology") + if actualSlicesChangedPerSyncTopology != float64(em.slicesChangedPerSyncTopology) { + t.Errorf("Expected slicesChangedPerSyncTopology to be %d, got %v", em.slicesChangedPerSyncTopology, actualSlicesChangedPerSyncTopology) + } + + actualSyncSuccesses, err := testutil.GetCounterMetricValue(metrics.EndpointSliceSyncs.WithLabelValues("success")) + handleErr(t, err, "syncSuccesses") + if actualSyncSuccesses != float64(em.syncSuccesses) { + t.Errorf("Expected endpointSliceSyncSuccesses to be %d, got %v", em.syncSuccesses, actualSyncSuccesses) + } + + actualSyncErrors, err := testutil.GetCounterMetricValue(metrics.EndpointSliceSyncs.WithLabelValues("error")) + handleErr(t, err, "syncErrors") + if actualSyncErrors != float64(em.syncErrors) { + t.Errorf("Expected endpointSliceSyncErrors to be %d, got %v", em.syncErrors, actualSyncErrors) + } } func handleErr(t *testing.T, err error, metricName string) { @@ -1524,4 +1825,8 @@ func setupMetrics() { metrics.EndpointSliceChanges.Delete(map[string]string{"operation": "create"}) metrics.EndpointSliceChanges.Delete(map[string]string{"operation": "update"}) metrics.EndpointSliceChanges.Delete(map[string]string{"operation": "delete"}) + metrics.EndpointSlicesChangedPerSync.Delete(map[string]string{"topology": "disabled"}) + metrics.EndpointSlicesChangedPerSync.Delete(map[string]string{"topology": "auto"}) + metrics.EndpointSliceSyncs.Delete(map[string]string{"result": "success"}) + metrics.EndpointSliceSyncs.Delete(map[string]string{"result": "error"}) } diff --git a/pkg/controller/endpointslice/topologycache/sliceinfo.go b/pkg/controller/endpointslice/topologycache/sliceinfo.go new file mode 100644 index 00000000000..f525a1cdee7 --- /dev/null +++ b/pkg/controller/endpointslice/topologycache/sliceinfo.go @@ -0,0 +1,76 @@ +/* +Copyright 2021 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 topologycache + +import ( + discovery "k8s.io/api/discovery/v1" +) + +// SliceInfo stores information about EndpointSlices for the reconciliation +// process. +type SliceInfo struct { + ServiceKey string + AddressType discovery.AddressType + ToCreate []*discovery.EndpointSlice + ToUpdate []*discovery.EndpointSlice + Unchanged []*discovery.EndpointSlice +} + +func (si *SliceInfo) getTotalEndpoints() int { + totalEndpoints := 0 + for _, slice := range si.ToCreate { + totalEndpoints += len(slice.Endpoints) + } + for _, slice := range si.ToUpdate { + totalEndpoints += len(slice.Endpoints) + } + for _, slice := range si.Unchanged { + totalEndpoints += len(slice.Endpoints) + } + return totalEndpoints +} + +// getAllocatedHintsByZone sums up the allocated hints we currently have in +// unchanged slices and marks slices for update as necessary. A slice needs to +// be updated if any of the following are true: +// - It has an endpoint without zone hints +// - It has an endpoint hint for a zone that no longer needs any +// - It has endpoint hints that would make the minimum allocations necessary +// impossible with changes to slices that are already being updated or +// created. +func (si *SliceInfo) getAllocatedHintsByZone(allocations map[string]Allocation) EndpointZoneInfo { + allocatedHintsByZone := EndpointZoneInfo{} + + // Using filtering in place to remove any endpoints that are no longer + // unchanged (https://github.com/golang/go/wiki/SliceTricks#filter-in-place) + j := 0 + for _, slice := range si.Unchanged { + hintsByZone := getHintsByZone(slice, allocatedHintsByZone, allocations) + if hintsByZone == nil { + si.ToUpdate = append(si.ToUpdate, slice.DeepCopy()) + } else { + si.Unchanged[j] = slice + j++ + for zone, numHints := range hintsByZone { + allocatedHintsByZone[zone] += numHints + } + } + } + + si.Unchanged = si.Unchanged[:j] + return allocatedHintsByZone +} diff --git a/pkg/controller/endpointslice/topologycache/sliceinfo_test.go b/pkg/controller/endpointslice/topologycache/sliceinfo_test.go new file mode 100644 index 00000000000..36627f637eb --- /dev/null +++ b/pkg/controller/endpointslice/topologycache/sliceinfo_test.go @@ -0,0 +1,79 @@ +/* +Copyright 2021 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 topologycache + +import ( + "fmt" + "testing" + + discovery "k8s.io/api/discovery/v1" +) + +func TestGetTotalEndpoints(t *testing.T) { + testCases := []struct { + name string + si *SliceInfo + expectedTotal int + }{{ + name: "empty", + si: &SliceInfo{}, + expectedTotal: 0, + }, { + name: "empty slice", + si: &SliceInfo{ + ToCreate: []*discovery.EndpointSlice{sliceWithNEndpoints(0)}, + }, + expectedTotal: 0, + }, { + name: "multiple slices", + si: &SliceInfo{ + ToCreate: []*discovery.EndpointSlice{sliceWithNEndpoints(15), sliceWithNEndpoints(8)}, + }, + expectedTotal: 23, + }, { + name: "slices for all", + si: &SliceInfo{ + ToCreate: []*discovery.EndpointSlice{sliceWithNEndpoints(15), sliceWithNEndpoints(8)}, + ToUpdate: []*discovery.EndpointSlice{sliceWithNEndpoints(2)}, + Unchanged: []*discovery.EndpointSlice{sliceWithNEndpoints(100), sliceWithNEndpoints(90)}, + }, + expectedTotal: 215, + }} + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + actualTotal := tc.si.getTotalEndpoints() + if actualTotal != tc.expectedTotal { + t.Errorf("Expected %d, got %d", tc.expectedTotal, actualTotal) + } + }) + } +} + +// helpers + +func sliceWithNEndpoints(n int) *discovery.EndpointSlice { + endpoints := []discovery.Endpoint{} + + for i := 0; i < n; i++ { + endpoints = append(endpoints, discovery.Endpoint{Addresses: []string{fmt.Sprintf("10.1.2.%d", i)}}) + } + + return &discovery.EndpointSlice{ + Endpoints: endpoints, + } +} diff --git a/pkg/controller/endpointslice/topologycache/topologycache.go b/pkg/controller/endpointslice/topologycache/topologycache.go new file mode 100644 index 00000000000..7ea9afb2e84 --- /dev/null +++ b/pkg/controller/endpointslice/topologycache/topologycache.go @@ -0,0 +1,252 @@ +/* +Copyright 2021 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 topologycache + +import ( + "math" + "sync" + + "k8s.io/api/core/v1" + discovery "k8s.io/api/discovery/v1" + "k8s.io/apimachinery/pkg/api/resource" + "k8s.io/klog/v2" +) + +const ( + // OverloadThreshold represents the maximum overload any individual endpoint + // should be exposed to. + OverloadThreshold float64 = 0.2 +) + +// TopologyCache tracks the distribution of Nodes and endpoints across zones. +type TopologyCache struct { + lock sync.Mutex + sufficientNodeInfo bool + cpuByZone map[string]*resource.Quantity + cpuRatiosByZone map[string]float64 + endpointsByService map[string]map[discovery.AddressType]EndpointZoneInfo +} + +// EndpointZoneInfo tracks the distribution of endpoints across zones for a +// Service. +type EndpointZoneInfo map[string]int + +// Allocation describes the number of endpoints that should be allocated for a +// zone. +type Allocation struct { + Minimum int + Maximum int + Desired float64 +} + +// NewTopologyCache initializes a new TopologyCache. +func NewTopologyCache() *TopologyCache { + return &TopologyCache{ + cpuByZone: map[string]*resource.Quantity{}, + cpuRatiosByZone: map[string]float64{}, + endpointsByService: map[string]map[discovery.AddressType]EndpointZoneInfo{}, + } +} + +// GetOverloadedServices returns a list of Service keys that refer to Services +// that have crossed the overload threshold for any zone. +func (t *TopologyCache) GetOverloadedServices() []string { + t.lock.Lock() + defer t.lock.Unlock() + + svcKeys := []string{} + for svcKey, eziByAddrType := range t.endpointsByService { + for _, ezi := range eziByAddrType { + if serviceOverloaded(ezi, t.cpuRatiosByZone) { + svcKeys = append(svcKeys, svcKey) + break + } + } + } + + return svcKeys +} + +// AddHints adds or updates topology hints on EndpointSlices and returns updated +// lists of EndpointSlices to create and update. +func (t *TopologyCache) AddHints(si *SliceInfo) ([]*discovery.EndpointSlice, []*discovery.EndpointSlice) { + totalEndpoints := si.getTotalEndpoints() + allocations := t.getAllocations(totalEndpoints) + + if allocations == nil { + klog.V(2).Infof("Insufficient endpoints, removing hints from %s Service", si.ServiceKey) + t.RemoveHints(si.ServiceKey, si.AddressType) + return RemoveHintsFromSlices(si) + } + + allocatedHintsByZone := si.getAllocatedHintsByZone(allocations) + + allocatableSlices := si.ToCreate + for _, slice := range si.ToUpdate { + allocatableSlices = append(allocatableSlices, slice) + } + + // step 1: assign same-zone hints for all endpoints as a starting point. + for _, slice := range allocatableSlices { + for i, endpoint := range slice.Endpoints { + if endpoint.Zone == nil || *endpoint.Zone == "" { + klog.Warningf("Endpoint found without zone specified, removing hints from %s Service", si.ServiceKey) + t.RemoveHints(si.ServiceKey, si.AddressType) + return RemoveHintsFromSlices(si) + } + + allocatedHintsByZone[*endpoint.Zone]++ + slice.Endpoints[i].Hints = &discovery.EndpointHints{ForZones: []discovery.ForZone{{Name: *endpoint.Zone}}} + } + } + + // step 2. Identify which zones need to donate slices and which need more. + givingZones, receivingZones := getGivingAndReceivingZones(allocations, allocatedHintsByZone) + + // step 3. Redistribute endpoints based on data from step 2. + redistributions := redistributeHints(allocatableSlices, givingZones, receivingZones) + + for zone, diff := range redistributions { + allocatedHintsByZone[zone] += diff + } + + t.SetHints(si.ServiceKey, si.AddressType, allocatedHintsByZone) + return si.ToCreate, si.ToUpdate +} + +// SetHints sets topology hints for the provided serviceKey and addrType in this +// cache. +func (t *TopologyCache) SetHints(serviceKey string, addrType discovery.AddressType, allocatedHintsByZone EndpointZoneInfo) { + if len(allocatedHintsByZone) == 0 { + t.RemoveHints(serviceKey, addrType) + return + } + + t.lock.Lock() + defer t.lock.Unlock() + + _, ok := t.endpointsByService[serviceKey] + if !ok { + t.endpointsByService[serviceKey] = map[discovery.AddressType]EndpointZoneInfo{} + } + t.endpointsByService[serviceKey][addrType] = allocatedHintsByZone +} + +// RemoveHints removes topology hints for the provided serviceKey and addrType +// from this cache. +func (t *TopologyCache) RemoveHints(serviceKey string, addrType discovery.AddressType) { + t.lock.Lock() + defer t.lock.Unlock() + + _, ok := t.endpointsByService[serviceKey] + if ok { + delete(t.endpointsByService[serviceKey], addrType) + } + if len(t.endpointsByService[serviceKey]) == 0 { + delete(t.endpointsByService, serviceKey) + } +} + +// SetNodes updates the Node distribution for the TopologyCache. +func (t *TopologyCache) SetNodes(nodes []*v1.Node) { + cpuByZone := map[string]*resource.Quantity{} + sufficientNodeInfo := true + + totalCPU := resource.Quantity{} + + for _, node := range nodes { + if !NodeReady(node.Status) { + continue + } + nodeCPU := node.Status.Allocatable.Cpu() + zone, ok := node.Labels[v1.LabelTopologyZone] + + // TODO(robscott): Figure out if there's an acceptable proportion of + // nodes with inadequate information. The current logic means that as + // soon as we find any node without a zone or allocatable CPU specified, + // we bail out entirely. Bailing out at this level will make our cluster + // wide ratios nil, which would result in slices for all Services having + // their hints removed. + if !ok || zone == "" || nodeCPU.IsZero() { + cpuByZone = map[string]*resource.Quantity{} + sufficientNodeInfo = false + break + } + + totalCPU.Add(*nodeCPU) + if _, ok = cpuByZone[zone]; !ok { + cpuByZone[zone] = nodeCPU + } else { + cpuByZone[zone].Add(*nodeCPU) + } + } + + t.lock.Lock() + defer t.lock.Unlock() + + if totalCPU.IsZero() || !sufficientNodeInfo || len(cpuByZone) < 2 { + t.sufficientNodeInfo = false + t.cpuByZone = nil + t.cpuRatiosByZone = nil + + } else { + t.sufficientNodeInfo = sufficientNodeInfo + t.cpuByZone = cpuByZone + + t.cpuRatiosByZone = map[string]float64{} + for zone, cpu := range cpuByZone { + t.cpuRatiosByZone[zone] = float64(cpu.MilliValue()) / float64(totalCPU.MilliValue()) + } + } +} + +// getAllocations returns a set of minimum and maximum allocations per zone. If +// it is not possible to provide allocations that are below the overload +// threshold, a nil value will be returned. +func (t *TopologyCache) getAllocations(numEndpoints int) map[string]Allocation { + if t.cpuRatiosByZone == nil || len(t.cpuRatiosByZone) < 2 || len(t.cpuRatiosByZone) > numEndpoints { + return nil + } + + t.lock.Lock() + defer t.lock.Unlock() + + remainingMinEndpoints := numEndpoints + minTotal := 0 + allocations := map[string]Allocation{} + + for zone, ratio := range t.cpuRatiosByZone { + desired := ratio * float64(numEndpoints) + minimum := int(math.Ceil(desired * (1 / (1 + OverloadThreshold)))) + allocations[zone] = Allocation{ + Minimum: minimum, + Desired: math.Max(desired, float64(minimum)), + } + minTotal += minimum + remainingMinEndpoints -= minimum + if remainingMinEndpoints < 0 { + return nil + } + } + + for zone, allocation := range allocations { + allocation.Maximum = allocation.Minimum + numEndpoints - minTotal + allocations[zone] = allocation + } + + return allocations +} diff --git a/pkg/controller/endpointslice/topologycache/topologycache_test.go b/pkg/controller/endpointslice/topologycache/topologycache_test.go new file mode 100644 index 00000000000..6f8bb369a9f --- /dev/null +++ b/pkg/controller/endpointslice/topologycache/topologycache_test.go @@ -0,0 +1,486 @@ +/* +Copyright 2021 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 topologycache + +import ( + "reflect" + "testing" + + "k8s.io/api/core/v1" + discovery "k8s.io/api/discovery/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + utilpointer "k8s.io/utils/pointer" +) + +func TestAddHints(t *testing.T) { + testCases := []struct { + name string + cpuRatiosByZone map[string]float64 + sliceInfo *SliceInfo + expectedEndpointsByAddrType map[discovery.AddressType]EndpointZoneInfo + expectedSlicesToCreate []*discovery.EndpointSlice + expectedSlicesToUpdate []*discovery.EndpointSlice + }{{ + name: "empty", + cpuRatiosByZone: nil, + sliceInfo: &SliceInfo{ + ServiceKey: "ns/svc", + AddressType: discovery.AddressTypeIPv4, + }, + expectedEndpointsByAddrType: nil, + expectedSlicesToCreate: []*discovery.EndpointSlice{}, + expectedSlicesToUpdate: []*discovery.EndpointSlice{}, + }, { + name: "slice to create, no zone ratios", + cpuRatiosByZone: nil, + sliceInfo: &SliceInfo{ + ServiceKey: "ns/svc", + AddressType: discovery.AddressTypeIPv4, + ToCreate: []*discovery.EndpointSlice{{ + Endpoints: []discovery.Endpoint{{ + Addresses: []string{"10.1.2.3"}, + Zone: utilpointer.StringPtr("zone-a"), + }}, + }}, + }, + expectedEndpointsByAddrType: nil, + expectedSlicesToCreate: []*discovery.EndpointSlice{{ + Endpoints: []discovery.Endpoint{{ + Addresses: []string{"10.1.2.3"}, + Zone: utilpointer.StringPtr("zone-a"), + }}, + }}, + expectedSlicesToUpdate: []*discovery.EndpointSlice{}, + }, { + name: "slice to create with 2 endpoints, zone ratios require 3", + cpuRatiosByZone: map[string]float64{ + "zone-a": 0.3, + "zone-b": 0.4, + "zone-c": 0.3, + }, + sliceInfo: &SliceInfo{ + ServiceKey: "ns/svc", + AddressType: discovery.AddressTypeIPv4, + ToCreate: []*discovery.EndpointSlice{{ + Endpoints: []discovery.Endpoint{{ + Addresses: []string{"10.1.2.3"}, + Zone: utilpointer.StringPtr("zone-a"), + }, { + Addresses: []string{"10.1.2.4"}, + Zone: utilpointer.StringPtr("zone-b"), + }}, + }}, + }, + expectedEndpointsByAddrType: nil, + expectedSlicesToCreate: []*discovery.EndpointSlice{{ + Endpoints: []discovery.Endpoint{{ + Addresses: []string{"10.1.2.3"}, + Zone: utilpointer.StringPtr("zone-a"), + }, { + Addresses: []string{"10.1.2.4"}, + Zone: utilpointer.StringPtr("zone-b"), + }}, + }}, + expectedSlicesToUpdate: []*discovery.EndpointSlice{}, + }, { + name: "slice to create with 2 endpoints, zone ratios only require 2", + cpuRatiosByZone: map[string]float64{ + "zone-a": 0.45, + "zone-b": 0.55, + }, + sliceInfo: &SliceInfo{ + ServiceKey: "ns/svc", + AddressType: discovery.AddressTypeIPv4, + ToCreate: []*discovery.EndpointSlice{{ + Endpoints: []discovery.Endpoint{{ + Addresses: []string{"10.1.2.3"}, + Zone: utilpointer.StringPtr("zone-a"), + }, { + Addresses: []string{"10.1.2.4"}, + Zone: utilpointer.StringPtr("zone-b"), + }}, + }}, + }, + expectedEndpointsByAddrType: map[discovery.AddressType]EndpointZoneInfo{ + discovery.AddressTypeIPv4: { + "zone-a": 1, + "zone-b": 1, + }, + }, + expectedSlicesToCreate: []*discovery.EndpointSlice{{ + Endpoints: []discovery.Endpoint{{ + Addresses: []string{"10.1.2.3"}, + Zone: utilpointer.StringPtr("zone-a"), + Hints: &discovery.EndpointHints{ForZones: []discovery.ForZone{{Name: "zone-a"}}}, + }, { + Addresses: []string{"10.1.2.4"}, + Zone: utilpointer.StringPtr("zone-b"), + Hints: &discovery.EndpointHints{ForZones: []discovery.ForZone{{Name: "zone-b"}}}, + }}, + }}, + expectedSlicesToUpdate: []*discovery.EndpointSlice{}, + }, { + name: "slices to create and update within 3 zone threshold", + cpuRatiosByZone: map[string]float64{ + "zone-a": 0.35, + "zone-b": 0.35, + "zone-c": 0.30, + }, + sliceInfo: &SliceInfo{ + ServiceKey: "ns/svc", + AddressType: discovery.AddressTypeIPv4, + ToCreate: []*discovery.EndpointSlice{{ + Endpoints: []discovery.Endpoint{{ + Addresses: []string{"10.1.2.3"}, + Zone: utilpointer.StringPtr("zone-a"), + }, { + Addresses: []string{"10.1.2.4"}, + Zone: utilpointer.StringPtr("zone-b"), + }}, + }, { + Endpoints: []discovery.Endpoint{{ + Addresses: []string{"10.1.3.3"}, + Zone: utilpointer.StringPtr("zone-c"), + }, { + Addresses: []string{"10.1.3.4"}, + Zone: utilpointer.StringPtr("zone-c"), + }, { + Addresses: []string{"10.1.3.4"}, + Zone: utilpointer.StringPtr("zone-a"), + }}, + }}, + ToUpdate: []*discovery.EndpointSlice{{ + Endpoints: []discovery.Endpoint{{ + Addresses: []string{"10.2.2.3"}, + Zone: utilpointer.StringPtr("zone-a"), + }, { + Addresses: []string{"10.2.2.4"}, + Zone: utilpointer.StringPtr("zone-a"), + }}, + }, { + Endpoints: []discovery.Endpoint{{ + Addresses: []string{"10.2.3.3"}, + Zone: utilpointer.StringPtr("zone-b"), + }, { + Addresses: []string{"10.2.3.4"}, + Zone: utilpointer.StringPtr("zone-c"), + }, { + Addresses: []string{"10.2.3.4"}, + Zone: utilpointer.StringPtr("zone-a"), + }}, + }}, + }, + expectedEndpointsByAddrType: map[discovery.AddressType]EndpointZoneInfo{ + discovery.AddressTypeIPv4: { + "zone-a": 4, + "zone-b": 3, + "zone-c": 3, + }, + }, + expectedSlicesToCreate: []*discovery.EndpointSlice{{ + Endpoints: []discovery.Endpoint{{ + Addresses: []string{"10.1.2.3"}, + Zone: utilpointer.StringPtr("zone-a"), + Hints: &discovery.EndpointHints{ForZones: []discovery.ForZone{{Name: "zone-b"}}}, + }, { + Addresses: []string{"10.1.2.4"}, + Zone: utilpointer.StringPtr("zone-b"), + Hints: &discovery.EndpointHints{ForZones: []discovery.ForZone{{Name: "zone-b"}}}, + }}, + }, { + Endpoints: []discovery.Endpoint{{ + Addresses: []string{"10.1.3.3"}, + Zone: utilpointer.StringPtr("zone-c"), + Hints: &discovery.EndpointHints{ForZones: []discovery.ForZone{{Name: "zone-c"}}}, + }, { + Addresses: []string{"10.1.3.4"}, + Zone: utilpointer.StringPtr("zone-c"), + Hints: &discovery.EndpointHints{ForZones: []discovery.ForZone{{Name: "zone-c"}}}, + }, { + Addresses: []string{"10.1.3.4"}, + Zone: utilpointer.StringPtr("zone-a"), + Hints: &discovery.EndpointHints{ForZones: []discovery.ForZone{{Name: "zone-a"}}}, + }}, + }}, + expectedSlicesToUpdate: []*discovery.EndpointSlice{{ + Endpoints: []discovery.Endpoint{{ + Addresses: []string{"10.2.2.3"}, + Zone: utilpointer.StringPtr("zone-a"), + Hints: &discovery.EndpointHints{ForZones: []discovery.ForZone{{Name: "zone-a"}}}, + }, { + Addresses: []string{"10.2.2.4"}, + Zone: utilpointer.StringPtr("zone-a"), + Hints: &discovery.EndpointHints{ForZones: []discovery.ForZone{{Name: "zone-a"}}}, + }}, + }, { + Endpoints: []discovery.Endpoint{{ + Addresses: []string{"10.2.3.3"}, + Zone: utilpointer.StringPtr("zone-b"), + Hints: &discovery.EndpointHints{ForZones: []discovery.ForZone{{Name: "zone-b"}}}, + }, { + Addresses: []string{"10.2.3.4"}, + Zone: utilpointer.StringPtr("zone-c"), + Hints: &discovery.EndpointHints{ForZones: []discovery.ForZone{{Name: "zone-c"}}}, + }, { + Addresses: []string{"10.2.3.4"}, + Zone: utilpointer.StringPtr("zone-a"), + Hints: &discovery.EndpointHints{ForZones: []discovery.ForZone{{Name: "zone-a"}}}, + }}, + }}, + }} + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + cache := NewTopologyCache() + cache.cpuRatiosByZone = tc.cpuRatiosByZone + + slicesToCreate, slicesToUpdate := cache.AddHints(tc.sliceInfo) + + expectEquivalentSlices(t, slicesToCreate, tc.expectedSlicesToCreate) + expectEquivalentSlices(t, slicesToUpdate, tc.expectedSlicesToUpdate) + + endpointsByAddrType, ok := cache.endpointsByService[tc.sliceInfo.ServiceKey] + if tc.expectedEndpointsByAddrType == nil { + if ok { + t.Errorf("Expected no endpoints for Service %s, got %+v", tc.sliceInfo.ServiceKey, endpointsByAddrType) + } + } else { + if len(tc.expectedEndpointsByAddrType) != len(endpointsByAddrType) { + t.Fatalf("Expected endpoints for %d address types, got %d", len(tc.expectedEndpointsByAddrType), len(endpointsByAddrType)) + } + for addrType, expectedEndpointZoneInfo := range tc.expectedEndpointsByAddrType { + endpointZoneInfo, ok := endpointsByAddrType[addrType] + if !ok { + t.Fatalf("Expected endpoints for %s address type, got none", addrType) + } + + if len(expectedEndpointZoneInfo) != len(endpointZoneInfo) { + t.Fatalf("Expected endpoints for %d zones, got %d", len(expectedEndpointZoneInfo), len(endpointZoneInfo)) + } + + for zone, expectedNum := range expectedEndpointZoneInfo { + num, ok := endpointZoneInfo[zone] + if !ok { + t.Fatalf("Expected endpoints for %s zone, got none", zone) + } + if num != expectedNum { + t.Errorf("Expected %d endpoints for %s zone, got %d", expectedNum, zone, num) + } + } + } + } + }) + } +} + +func TestSetNodes(t *testing.T) { + type nodeInfo struct { + zone string + cpu resource.Quantity + ready v1.ConditionStatus + } + + testCases := []struct { + name string + nodes []nodeInfo + expectSufficientNodeInfo bool + expectedCPUByZone map[string]*resource.Quantity + expectedRatios map[string]float64 + }{{ + name: "empty", + nodes: []nodeInfo{}, + expectSufficientNodeInfo: false, + expectedCPUByZone: nil, + expectedRatios: nil, + }, { + name: "single node", + nodes: []nodeInfo{ + {zone: "zone-a", cpu: resource.MustParse("1000m"), ready: v1.ConditionTrue}, + }, + expectSufficientNodeInfo: false, + expectedCPUByZone: nil, + expectedRatios: nil, + }, { + name: "single zone", + nodes: []nodeInfo{ + {zone: "zone-a", cpu: resource.MustParse("1000m"), ready: v1.ConditionTrue}, + {zone: "zone-a", cpu: resource.MustParse("1000m"), ready: v1.ConditionTrue}, + }, + expectSufficientNodeInfo: false, + expectedCPUByZone: nil, + expectedRatios: nil, + }, { + name: "2 zones", + nodes: []nodeInfo{ + {zone: "zone-a", cpu: resource.MustParse("1000m"), ready: v1.ConditionTrue}, + {zone: "zone-b", cpu: resource.MustParse("1000m"), ready: v1.ConditionTrue}, + }, + expectSufficientNodeInfo: true, + expectedCPUByZone: map[string]*resource.Quantity{ + "zone-a": resource.NewQuantity(1, resource.BinarySI), + "zone-b": resource.NewQuantity(1, resource.BinarySI), + }, + expectedRatios: map[string]float64{ + "zone-a": 0.5, + "zone-b": 0.5, + }, + }, { + name: "2 zones, unready node in 1, ready node in 1", + nodes: []nodeInfo{ + {zone: "zone-a", cpu: resource.MustParse("1000m"), ready: v1.ConditionFalse}, + {zone: "zone-b", cpu: resource.MustParse("1000m"), ready: v1.ConditionTrue}, + }, + expectSufficientNodeInfo: false, + expectedCPUByZone: nil, + expectedRatios: nil, + }, { + name: "2 zones, unready node in 1, ready node in 2", + nodes: []nodeInfo{ + {zone: "zone-a", cpu: resource.MustParse("1000m"), ready: v1.ConditionTrue}, + {zone: "zone-b", cpu: resource.MustParse("1000m"), ready: v1.ConditionTrue}, + {zone: "zone-b", cpu: resource.MustParse("1000m"), ready: v1.ConditionFalse}, + }, + expectSufficientNodeInfo: true, + expectedCPUByZone: map[string]*resource.Quantity{ + "zone-a": resource.NewQuantity(1, resource.BinarySI), + "zone-b": resource.NewQuantity(1, resource.BinarySI), + }, + expectedRatios: map[string]float64{ + "zone-a": 0.5, + "zone-b": 0.5, + }, + }, { + name: "3 zones, 4 nodes in 1, 2 nodes in 1, 1 node in 1", + nodes: []nodeInfo{ + {zone: "zone-a", cpu: resource.MustParse("1000m"), ready: v1.ConditionTrue}, + {zone: "zone-a", cpu: resource.MustParse("1000m"), ready: v1.ConditionTrue}, + {zone: "zone-a", cpu: resource.MustParse("1000m"), ready: v1.ConditionTrue}, + {zone: "zone-a", cpu: resource.MustParse("2000m"), ready: v1.ConditionTrue}, + {zone: "zone-b", cpu: resource.MustParse("3000m"), ready: v1.ConditionTrue}, + {zone: "zone-b", cpu: resource.MustParse("1500m"), ready: v1.ConditionTrue}, + {zone: "zone-c", cpu: resource.MustParse("500m"), ready: v1.ConditionTrue}, + }, + expectSufficientNodeInfo: true, + expectedCPUByZone: map[string]*resource.Quantity{ + "zone-a": resource.NewMilliQuantity(5000, resource.BinarySI), + "zone-b": resource.NewMilliQuantity(4500, resource.BinarySI), + "zone-c": resource.NewMilliQuantity(500, resource.BinarySI), + }, + expectedRatios: map[string]float64{ + "zone-a": 0.5, + "zone-b": 0.45, + "zone-c": 0.05, + }, + }} + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + cache := NewTopologyCache() + nodes := make([]*v1.Node, 0, len(tc.nodes)) + for _, node := range tc.nodes { + labels := map[string]string{} + if node.zone != "" { + labels[v1.LabelTopologyZone] = node.zone + } + conditions := []v1.NodeCondition{{ + Type: v1.NodeReady, + Status: node.ready, + }} + allocatable := v1.ResourceList{ + v1.ResourceCPU: node.cpu, + } + nodes = append(nodes, &v1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Labels: labels, + }, + Status: v1.NodeStatus{ + Allocatable: allocatable, + Conditions: conditions, + }, + }) + } + + cache.SetNodes(nodes) + + if cache.sufficientNodeInfo != tc.expectSufficientNodeInfo { + t.Errorf("Expected sufficientNodeInfo to be %t, got %t", tc.expectSufficientNodeInfo, cache.sufficientNodeInfo) + } + + if cache.cpuRatiosByZone == nil || tc.expectedRatios == nil { + if (cache.cpuRatiosByZone == nil) != (tc.expectedRatios == nil) { + t.Errorf("Expected %+v, got %+v", tc.expectedRatios, cache.cpuRatiosByZone) + } + } else { + if len(cache.cpuRatiosByZone) != len(tc.expectedRatios) { + t.Errorf("Expected ratios with %d zones, got %d", len(tc.expectedRatios), len(cache.cpuRatiosByZone)) + } + for zone, expectedRatio := range tc.expectedRatios { + actualRatio, ok := cache.cpuRatiosByZone[zone] + if !ok { + t.Errorf("Expected ratio for %s zone, got none", zone) + } else if actualRatio != expectedRatio { + t.Errorf("Expected ratio to be %f, got %f", expectedRatio, actualRatio) + } + } + } + + if cache.cpuByZone == nil || tc.expectedCPUByZone == nil { + if (cache.cpuByZone == nil) != (tc.expectedCPUByZone == nil) { + t.Errorf("Expected %+v, got %+v", tc.expectedCPUByZone, cache.cpuByZone) + } + } else { + if len(cache.cpuByZone) != len(tc.expectedCPUByZone) { + t.Errorf("Expected CPU with %d zones, got %d", len(tc.expectedCPUByZone), len(cache.cpuByZone)) + } + for zone, expectedCPU := range tc.expectedCPUByZone { + actualCPU, ok := cache.cpuByZone[zone] + if !ok { + t.Errorf("Expected CPU for %s zone, got none", zone) + } else if !actualCPU.Equal(*expectedCPU) { + t.Errorf("Expected CPU to be %d, got %d", expectedCPU.MilliValue(), actualCPU.MilliValue()) + } + } + } + }) + } +} + +// Test Helpers + +func expectEquivalentSlices(t *testing.T, actualSlices, expectedSlices []*discovery.EndpointSlice) { + t.Helper() + + if len(actualSlices) != len(expectedSlices) { + t.Fatalf("Expected %d slices, got %d", len(expectedSlices), len(actualSlices)) + } + + for i, expectedSlice := range expectedSlices { + actualSlice := actualSlices[i] + + if len(expectedSlice.Endpoints) != len(actualSlice.Endpoints) { + t.Errorf("Expected %d endpoints, got %d", len(expectedSlice.Endpoints), len(actualSlice.Endpoints)) + continue + } + for j, expectedEndpoint := range expectedSlice.Endpoints { + actualEndpoint := actualSlice.Endpoints[j] + if !reflect.DeepEqual(actualEndpoint, expectedEndpoint) { + t.Errorf("Endpoints didn't match\nExpected: %+v\nGot: %+v", expectedEndpoint, actualEndpoint) + } + } + } +} diff --git a/pkg/controller/endpointslice/topologycache/utils.go b/pkg/controller/endpointslice/topologycache/utils.go new file mode 100644 index 00000000000..d5a6f0b9bc8 --- /dev/null +++ b/pkg/controller/endpointslice/topologycache/utils.go @@ -0,0 +1,246 @@ +/* +Copyright 2021 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 topologycache + +import ( + "math" + + "k8s.io/api/core/v1" + discovery "k8s.io/api/discovery/v1" + "k8s.io/klog/v2" +) + +// RemoveHintsFromSlices removes topology hints on EndpointSlices and returns +// updated lists of EndpointSlices to create and update. +func RemoveHintsFromSlices(si *SliceInfo) ([]*discovery.EndpointSlice, []*discovery.EndpointSlice) { + // Remove hints on all EndpointSlices we were already going to change. + slices := append(si.ToCreate, si.ToUpdate...) + for _, slice := range slices { + for i := range slice.Endpoints { + slice.Endpoints[i].Hints = nil + } + } + + // Remove hints on all unchanged EndpointSlices and mark them for update + // if any already had hints. We use j to track the number/index of slices + // that are still unchanged. + j := 0 + for _, slice := range si.Unchanged { + changed := false + for i, endpoint := range slice.Endpoints { + if endpoint.Hints != nil { + // Unchanged slices are still direct copies from informer cache. + // Need to deep copy before we make any modifications to avoid + // accidentally changing informer cache. + slice = slice.DeepCopy() + slice.Endpoints[i].Hints = nil + changed = true + } + } + if changed { + si.ToUpdate = append(si.ToUpdate, slice) + } else { + si.Unchanged[j] = slice + j++ + } + } + + // truncate si.Unchanged so it only includes slices that are still + // unchanged. + si.Unchanged = si.Unchanged[:j] + + return si.ToCreate, si.ToUpdate +} + +// redistributeHints redistributes hints based in the provided EndpointSlices. +// It allocates endpoints from the provided givingZones to the provided +// receivingZones. This returns a map that represents the changes in allocated +// endpoints by zone. +func redistributeHints(slices []*discovery.EndpointSlice, givingZones, receivingZones map[string]int) map[string]int { + redistributions := map[string]int{} + + for _, slice := range slices { + for i, endpoint := range slice.Endpoints { + if len(givingZones) == 0 || len(receivingZones) == 0 { + return redistributions + } + if endpoint.Zone == nil || *endpoint.Zone == "" { + // This should always be caught earlier in AddHints() + klog.Warningf("Endpoint found without zone specified") + continue + } + + givingZone := *endpoint.Zone + numToGive, ok := givingZones[givingZone] + if ok && numToGive > 0 { + for receivingZone, numToReceive := range receivingZones { + if numToReceive > 0 { + slice.Endpoints[i].Hints = &discovery.EndpointHints{ForZones: []discovery.ForZone{{Name: receivingZone}}} + if numToGive == 1 { + delete(givingZones, givingZone) + } else { + givingZones[givingZone]-- + } + if numToReceive == 1 { + delete(receivingZones, receivingZone) + } else { + receivingZones[receivingZone]-- + } + + redistributions[receivingZone]++ + redistributions[givingZone]-- + + break + } + } + } + } + } + return redistributions +} + +// getGivingAndReceivingZones returns the number of endpoints each zone should +// give to other zones along with the number of endpoints each zone should +// receive from other zones. This is calculated with the provided allocations +// (desired state) and allocatedHintsByZone (current state). +func getGivingAndReceivingZones(allocations map[string]Allocation, allocatedHintsByZone map[string]int) (map[string]int, map[string]int) { + // 1. Determine the precise number of additional endpoints each zone has + // (giving) or needs (receiving). + givingZonesDesired := map[string]float64{} + receivingZonesDesired := map[string]float64{} + + for zone, allocation := range allocations { + allocatedHints, _ := allocatedHintsByZone[zone] + target := allocation.Desired + if float64(allocatedHints) > target { + givingZonesDesired[zone] = float64(allocatedHints) - target + } else if float64(allocatedHints) < target { + receivingZonesDesired[zone] = target - float64(allocatedHints) + } + } + + // 2. Convert the precise numbers needed into ints representing real + // endpoints given from one zone to another. + givingZones := map[string]int{} + receivingZones := map[string]int{} + + for { + givingZone, numToGive := getMost(givingZonesDesired) + receivingZone, numToReceive := getMost(receivingZonesDesired) + + // return early if any of the following are true: + // - giving OR receiving zone are unspecified + // - giving AND receiving zones have less than 1 endpoint left to give or receive + // - giving OR receiving zones have less than 0.5 endpoints left to give or receive + if givingZone == "" || receivingZone == "" || (numToGive < 1.0 && numToReceive < 1.0) || numToGive < 0.5 || numToReceive < 0.5 { + break + } + + givingZones[givingZone]++ + givingZonesDesired[givingZone]-- + receivingZones[receivingZone]++ + receivingZonesDesired[receivingZone]-- + } + + return givingZones, receivingZones +} + +// getMost accepts a map[string]float64 and returns the string and float64 that +// represent the greatest value in this provided map. This function is not very +// efficient but it is expected that len() will rarely be greater than 2. +func getMost(zones map[string]float64) (string, float64) { + zone := "" + num := 0.0 + for z, n := range zones { + if n > num { + zone = z + num = n + } + } + + return zone, num +} + +// getHintsByZone returns the number of hints allocated to each zone by the +// provided EndpointSlice. This function returns nil to indicate that the +// current allocations are invalid and that the EndpointSlice needs to be +// updated. This could be caused by: +// - A hint for a zone that no longer requires any allocations. +// - An endpoint with no hints. +// - Hints that would make minimum allocations impossible. +func getHintsByZone(slice *discovery.EndpointSlice, allocatedHintsByZone EndpointZoneInfo, allocations map[string]Allocation) map[string]int { + hintsByZone := map[string]int{} + for _, endpoint := range slice.Endpoints { + if endpoint.Hints == nil || len(endpoint.Hints.ForZones) == 0 { + return nil + } + zone := endpoint.Hints.ForZones[0].Name + if _, ok := allocations[zone]; ok { + return nil + } + } + + for zone, numHints := range hintsByZone { + alreadyAllocated, _ := allocatedHintsByZone[zone] + allocation, ok := allocations[zone] + if !ok || (numHints+alreadyAllocated) > allocation.Maximum { + return nil + } + } + + return hintsByZone +} + +// serviceOverloaded returns true if the Service has an insufficient amount of +// endpoints for any zone. +func serviceOverloaded(ezi EndpointZoneInfo, zoneRatios map[string]float64) bool { + if len(ezi) == 0 { + return false + } + if len(zoneRatios) == 0 { + return true + } + + totalEndpoints := 0.0 + for _, numEndpoints := range ezi { + totalEndpoints += float64(numEndpoints) + } + + for zone, ratio := range zoneRatios { + svcEndpoints, ok := ezi[zone] + if !ok { + return true + } + minEndpoints := math.Ceil(totalEndpoints * ratio * (1 / (1 + OverloadThreshold))) + if svcEndpoints < int(minEndpoints) { + return true + } + } + + return false +} + +// NodeReady returns true if the Node has a status condition of type "NodeReady" +// with a status of "True". +func NodeReady(nodeStatus v1.NodeStatus) bool { + for _, cond := range nodeStatus.Conditions { + if cond.Type == v1.NodeReady { + return cond.Status == v1.ConditionTrue + } + } + return false +} diff --git a/pkg/controller/endpointslice/topologycache/utils_test.go b/pkg/controller/endpointslice/topologycache/utils_test.go new file mode 100644 index 00000000000..1ecb911ba8d --- /dev/null +++ b/pkg/controller/endpointslice/topologycache/utils_test.go @@ -0,0 +1,195 @@ +/* +Copyright 2021 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 topologycache + +import ( + "reflect" + "testing" + + discovery "k8s.io/api/discovery/v1" + utilpointer "k8s.io/utils/pointer" +) + +func Test_redistributeHints(t *testing.T) { + testCases := []struct { + name string + slices []*discovery.EndpointSlice + givingZones map[string]int + receivingZones map[string]int + expectedRedistributions map[string]int + }{{ + name: "empty", + slices: []*discovery.EndpointSlice{}, + givingZones: map[string]int{}, + receivingZones: map[string]int{}, + expectedRedistributions: map[string]int{}, + }, { + name: "single endpoint", + slices: []*discovery.EndpointSlice{{ + Endpoints: []discovery.Endpoint{{ + Zone: utilpointer.StringPtr("zone-a"), + Hints: &discovery.EndpointHints{ForZones: []discovery.ForZone{{Name: "zone-a"}}}, + }}, + }}, + givingZones: map[string]int{"zone-a": 1}, + receivingZones: map[string]int{"zone-b": 1}, + expectedRedistributions: map[string]int{"zone-a": -1, "zone-b": 1}, + }, { + name: "endpoints from 1 zone redistributed to 2 other zones", + slices: []*discovery.EndpointSlice{{ + Endpoints: []discovery.Endpoint{{ + Zone: utilpointer.StringPtr("zone-a"), + Hints: &discovery.EndpointHints{ForZones: []discovery.ForZone{{Name: "zone-a"}}}, + }, { + Zone: utilpointer.StringPtr("zone-a"), + Hints: &discovery.EndpointHints{ForZones: []discovery.ForZone{{Name: "zone-a"}}}, + }, { + Zone: utilpointer.StringPtr("zone-a"), + Hints: &discovery.EndpointHints{ForZones: []discovery.ForZone{{Name: "zone-a"}}}, + }}, + }}, + givingZones: map[string]int{"zone-a": 2}, + receivingZones: map[string]int{"zone-b": 1, "zone-c": 1}, + expectedRedistributions: map[string]int{"zone-a": -2, "zone-b": 1, "zone-c": 1}, + }} + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + actualRedistributions := redistributeHints(tc.slices, tc.givingZones, tc.receivingZones) + + if len(actualRedistributions) != len(tc.expectedRedistributions) { + t.Fatalf("Expected redistributions for %d zones, got %d (%+v)", len(tc.expectedRedistributions), len(actualRedistributions), actualRedistributions) + } + + for zone, expectedNum := range tc.expectedRedistributions { + actualNum, _ := actualRedistributions[zone] + if actualNum != expectedNum { + t.Errorf("Expected redistribution of %d for zone %s, got %d", expectedNum, zone, actualNum) + } + } + }) + } +} + +func Test_getGivingAndReceivingZones(t *testing.T) { + testCases := []struct { + name string + allocations map[string]Allocation + allocatedHintsByZone map[string]int + expectedGivingZones map[string]int + expectedReceivingZones map[string]int + }{{ + name: "empty", + allocations: map[string]Allocation{}, + allocatedHintsByZone: map[string]int{}, + expectedGivingZones: map[string]int{}, + expectedReceivingZones: map[string]int{}, + }, { + name: "simple allocation with no need for rebalancing", + allocations: map[string]Allocation{ + "zone-a": {Desired: 1.2}, + "zone-b": {Desired: 1.1}, + "zone-c": {Desired: 1.0}, + }, + allocatedHintsByZone: map[string]int{"zone-a": 1, "zone-b": 1, "zone-c": 1}, + expectedGivingZones: map[string]int{}, + expectedReceivingZones: map[string]int{}, + }, { + name: "preference for same zone even when giving an extra endpoint would result in slightly better distribution", + allocations: map[string]Allocation{ + "zone-a": {Desired: 5.1}, + "zone-b": {Desired: 5.1}, + "zone-c": {Desired: 5.8}, + }, + allocatedHintsByZone: map[string]int{"zone-a": 16}, + expectedGivingZones: map[string]int{"zone-a": 10}, + expectedReceivingZones: map[string]int{"zone-b": 5, "zone-c": 5}, + }, { + name: "when 2 zones need < 1 endpoint, give to zone that needs endpoint most", + allocations: map[string]Allocation{ + "zone-a": {Desired: 5.0}, + "zone-b": {Desired: 5.6}, + "zone-c": {Desired: 5.4}, + }, + allocatedHintsByZone: map[string]int{"zone-a": 16}, + expectedGivingZones: map[string]int{"zone-a": 11}, + expectedReceivingZones: map[string]int{"zone-b": 6, "zone-c": 5}, + }, { + name: "when 2 zones have extra endpoints, give from zone with most extra", + allocations: map[string]Allocation{ + "zone-a": {Desired: 5.0}, + "zone-b": {Desired: 5.6}, + "zone-c": {Desired: 5.4}, + }, + allocatedHintsByZone: map[string]int{"zone-b": 8, "zone-c": 8}, + expectedGivingZones: map[string]int{"zone-b": 2, "zone-c": 3}, + expectedReceivingZones: map[string]int{"zone-a": 5}, + }, { + name: "ensure function can handle unexpected data (more allocated than allocations)", + allocations: map[string]Allocation{ + "zone-a": {Desired: 5.0}, + "zone-b": {Desired: 5.0}, + "zone-c": {Desired: 5.0}, + }, + allocatedHintsByZone: map[string]int{"zone-a": 6, "zone-b": 6, "zone-c": 6}, + expectedGivingZones: map[string]int{}, + expectedReceivingZones: map[string]int{}, + }, { + name: "ensure function can handle unexpected data (negative allocations)", + allocations: map[string]Allocation{ + "zone-a": {Desired: -5.0}, + "zone-b": {Desired: -5.0}, + "zone-c": {Desired: -5.0}, + }, + allocatedHintsByZone: map[string]int{"zone-a": 6, "zone-b": 6, "zone-c": 6}, + expectedGivingZones: map[string]int{}, + expectedReceivingZones: map[string]int{}, + }, { + name: "ensure function can handle unexpected data (negative allocated)", + allocations: map[string]Allocation{ + "zone-a": {Desired: 5.0}, + "zone-b": {Desired: 5.0}, + "zone-c": {Desired: 5.0}, + }, + allocatedHintsByZone: map[string]int{"zone-a": -4, "zone-b": -3, "zone-c": -2}, + expectedGivingZones: map[string]int{}, + expectedReceivingZones: map[string]int{}, + }, { + name: "ensure function can handle unexpected data (negative for 1 zone)", + allocations: map[string]Allocation{ + "zone-a": {Desired: 5.0}, + "zone-b": {Desired: 5.0}, + "zone-c": {Desired: 5.0}, + }, + allocatedHintsByZone: map[string]int{"zone-a": -40, "zone-b": 20, "zone-c": 20}, + expectedGivingZones: map[string]int{"zone-b": 15, "zone-c": 15}, + expectedReceivingZones: map[string]int{"zone-a": 30}, + }} + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + actualGivingZones, actualReceivingZones := getGivingAndReceivingZones(tc.allocations, tc.allocatedHintsByZone) + + if !reflect.DeepEqual(actualGivingZones, tc.expectedGivingZones) { + t.Errorf("Expected %+v giving zones, got %+v", tc.expectedGivingZones, actualGivingZones) + } + if !reflect.DeepEqual(actualReceivingZones, tc.expectedReceivingZones) { + t.Errorf("Expected %+v receiving zones, got %+v", tc.expectedReceivingZones, actualReceivingZones) + } + }) + } +} diff --git a/pkg/controller/endpointslice/utils.go b/pkg/controller/endpointslice/utils.go index e98aba699f0..2a698405e64 100644 --- a/pkg/controller/endpointslice/utils.go +++ b/pkg/controller/endpointslice/utils.go @@ -27,6 +27,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" utilruntime "k8s.io/apimachinery/pkg/util/runtime" + "k8s.io/apimachinery/pkg/util/sets" utilfeature "k8s.io/apiserver/pkg/util/feature" "k8s.io/client-go/tools/cache" "k8s.io/klog/v2" @@ -365,3 +366,31 @@ func getAddressTypesForService(service *corev1.Service) map[discovery.AddressTyp klog.V(2).Infof("couldn't find ipfamilies for headless service: %v/%v likely because controller manager is likely connected to an old apiserver that does not support ip families yet. The service endpoint slice will use dual stack families until api-server default it correctly", service.Namespace, service.Name) return serviceSupportedAddresses } + +func unchangedSlices(existingSlices, slicesToUpdate, slicesToDelete []*discovery.EndpointSlice) []*discovery.EndpointSlice { + changedSliceNames := sets.String{} + for _, slice := range slicesToUpdate { + changedSliceNames.Insert(slice.Name) + } + for _, slice := range slicesToDelete { + changedSliceNames.Insert(slice.Name) + } + unchangedSlices := []*discovery.EndpointSlice{} + for _, slice := range existingSlices { + if !changedSliceNames.Has(slice.Name) { + unchangedSlices = append(unchangedSlices, slice) + } + } + + return unchangedSlices +} + +// hintsEnabled returns true if the provided annotations include a +// corev1.AnnotationTopologyAwareHints key with a value set to "auto". +func hintsEnabled(annotations map[string]string) bool { + val, ok := annotations[corev1.AnnotationTopologyAwareHints] + if !ok { + return false + } + return val == "auto" +} diff --git a/pkg/features/kube_features.go b/pkg/features/kube_features.go index 5b2ce6fa433..a576812e8d4 100644 --- a/pkg/features/kube_features.go +++ b/pkg/features/kube_features.go @@ -688,6 +688,12 @@ const ( // Enables controlling pod ranking on replicaset scale-down. PodDeletionCost featuregate.Feature = "PodDeletionCost" + // owner: @robscott + // alpha: v1.21 + // + // Enables topology aware hints for EndpointSlices + TopologyAwareHints featuregate.Feature = "TopologyAwareHints" + // owner: @ahg-g // alpha: v1.21 // @@ -832,6 +838,7 @@ var defaultKubernetesFeatureGates = map[featuregate.Feature]featuregate.FeatureS PreferNominatedNode: {Default: false, PreRelease: featuregate.Alpha}, RunAsGroup: {Default: true, PreRelease: featuregate.GA, LockToDefault: true}, // remove in 1.22 PodDeletionCost: {Default: false, PreRelease: featuregate.Alpha}, + TopologyAwareHints: {Default: false, PreRelease: featuregate.Alpha}, PodAffinityNamespaceSelector: {Default: false, PreRelease: featuregate.Alpha}, ServiceLoadBalancerClass: {Default: false, PreRelease: featuregate.Alpha}, LogarithmicScaleDown: {Default: false, PreRelease: featuregate.Alpha}, diff --git a/pkg/proxy/endpoints.go b/pkg/proxy/endpoints.go index 0ef4a73e1b5..8224f30954d 100644 --- a/pkg/proxy/endpoints.go +++ b/pkg/proxy/endpoints.go @@ -50,6 +50,9 @@ type BaseEndpointInfo struct { IsLocal bool Topology map[string]string + // ZoneHints represent the zone hints for the endpoint. This is based on + // endpoint.hints.forZones[*].name in the EndpointSlice API. + ZoneHints sets.String // Ready indicates whether this endpoint is ready and NOT terminating. // For pods, this is true if a pod has a ready status and a nil deletion timestamp. // This is only set when watching EndpointSlices. If using Endpoints, this is always @@ -102,6 +105,11 @@ func (info *BaseEndpointInfo) GetTopology() map[string]string { return info.Topology } +// GetZoneHints returns the zone hint for the endpoint. +func (info *BaseEndpointInfo) GetZoneHints() sets.String { + return info.ZoneHints +} + // IP returns just the IP part of the endpoint, it's a part of proxy.Endpoint interface. func (info *BaseEndpointInfo) IP() string { return utilproxy.IPPart(info.Endpoint) @@ -118,7 +126,7 @@ func (info *BaseEndpointInfo) Equal(other Endpoint) bool { } func newBaseEndpointInfo(IP string, port int, isLocal bool, topology map[string]string, - ready, serving, terminating bool) *BaseEndpointInfo { + ready, serving, terminating bool, zoneHints sets.String) *BaseEndpointInfo { return &BaseEndpointInfo{ Endpoint: net.JoinHostPort(IP, strconv.Itoa(port)), IsLocal: isLocal, @@ -126,6 +134,7 @@ func newBaseEndpointInfo(IP string, port int, isLocal bool, topology map[string] Ready: ready, Serving: serving, Terminating: terminating, + ZoneHints: zoneHints, } } @@ -427,8 +436,10 @@ func (ect *EndpointChangeTracker) endpointsToEndpointsMap(endpoints *v1.Endpoint isServing := true isTerminating := false isLocal := addr.NodeName != nil && *addr.NodeName == ect.hostname + // Only supported with EndpointSlice API + zoneHints := sets.String{} - baseEndpointInfo := newBaseEndpointInfo(addr.IP, int(port.Port), isLocal, nil, isReady, isServing, isTerminating) + baseEndpointInfo := newBaseEndpointInfo(addr.IP, int(port.Port), isLocal, nil, isReady, isServing, isTerminating, zoneHints) if ect.makeEndpointInfo != nil { endpointsMap[svcPortName] = append(endpointsMap[svcPortName], ect.makeEndpointInfo(baseEndpointInfo)) } else { diff --git a/pkg/proxy/endpoints_test.go b/pkg/proxy/endpoints_test.go index 469521e8514..2f75a0d6d87 100644 --- a/pkg/proxy/endpoints_test.go +++ b/pkg/proxy/endpoints_test.go @@ -194,7 +194,7 @@ func TestEndpointsToEndpointsMap(t *testing.T) { }), expected: map[ServicePortName][]*BaseEndpointInfo{ makeServicePortName("ns1", "ep1", "", v1.ProtocolTCP): { - {Endpoint: "1.1.1.1:11", IsLocal: false, Ready: true, Serving: true, Terminating: false}, + {Endpoint: "1.1.1.1:11", IsLocal: false, Ready: true, Serving: true, Terminating: false, ZoneHints: sets.String{}}, }, }, }, @@ -218,7 +218,7 @@ func TestEndpointsToEndpointsMap(t *testing.T) { }), expected: map[ServicePortName][]*BaseEndpointInfo{ makeServicePortName("ns1", "ep1", "port", v1.ProtocolTCP): { - {Endpoint: "1.1.1.1:11", IsLocal: false, Ready: true, Serving: true, Terminating: false}, + {Endpoint: "1.1.1.1:11", IsLocal: false, Ready: true, Serving: true, Terminating: false, ZoneHints: sets.String{}}, }, }, }, @@ -241,7 +241,7 @@ func TestEndpointsToEndpointsMap(t *testing.T) { }), expected: map[ServicePortName][]*BaseEndpointInfo{ makeServicePortName("ns1", "ep1", "", v1.ProtocolTCP): { - {Endpoint: "1.1.1.1:11", IsLocal: false, Ready: true, Serving: true, Terminating: false}, + {Endpoint: "1.1.1.1:11", IsLocal: false, Ready: true, Serving: true, Terminating: false, ZoneHints: sets.String{}}, }, }, }, @@ -278,12 +278,12 @@ func TestEndpointsToEndpointsMap(t *testing.T) { }), expected: map[ServicePortName][]*BaseEndpointInfo{ makeServicePortName("ns1", "ep1", "p1", v1.ProtocolTCP): { - {Endpoint: "1.1.1.1:11", IsLocal: false, Ready: true, Serving: true, Terminating: false}, - {Endpoint: "2.2.2.2:11", IsLocal: false, Ready: true, Serving: true, Terminating: false}, + {Endpoint: "1.1.1.1:11", IsLocal: false, Ready: true, Serving: true, Terminating: false, ZoneHints: sets.String{}}, + {Endpoint: "2.2.2.2:11", IsLocal: false, Ready: true, Serving: true, Terminating: false, ZoneHints: sets.String{}}, }, makeServicePortName("ns1", "ep1", "p2", v1.ProtocolTCP): { - {Endpoint: "1.1.1.1:22", IsLocal: false, Ready: true, Serving: true, Terminating: false}, - {Endpoint: "2.2.2.2:22", IsLocal: false, Ready: true, Serving: true, Terminating: false}, + {Endpoint: "1.1.1.1:22", IsLocal: false, Ready: true, Serving: true, Terminating: false, ZoneHints: sets.String{}}, + {Endpoint: "2.2.2.2:22", IsLocal: false, Ready: true, Serving: true, Terminating: false, ZoneHints: sets.String{}}, }, }, }, @@ -307,7 +307,7 @@ func TestEndpointsToEndpointsMap(t *testing.T) { }), expected: map[ServicePortName][]*BaseEndpointInfo{ makeServicePortName("ns1", "ep1", "p1", v1.ProtocolTCP): { - {Endpoint: "1.1.1.1:11", IsLocal: false, Ready: true, Serving: true, Terminating: false}, + {Endpoint: "1.1.1.1:11", IsLocal: false, Ready: true, Serving: true, Terminating: false, ZoneHints: sets.String{}}, }, }, }, @@ -331,7 +331,7 @@ func TestEndpointsToEndpointsMap(t *testing.T) { }), expected: map[ServicePortName][]*BaseEndpointInfo{ makeServicePortName("ns1", "ep1", "p2", v1.ProtocolTCP): { - {Endpoint: "1.1.1.1:11", IsLocal: false, Ready: true, Serving: true, Terminating: false}, + {Endpoint: "1.1.1.1:11", IsLocal: false, Ready: true, Serving: true, Terminating: false, ZoneHints: sets.String{}}, }, }, }, @@ -355,7 +355,7 @@ func TestEndpointsToEndpointsMap(t *testing.T) { }), expected: map[ServicePortName][]*BaseEndpointInfo{ makeServicePortName("ns1", "ep1", "p1", v1.ProtocolTCP): { - {Endpoint: "1.1.1.1:22", IsLocal: false, Ready: true, Serving: true, Terminating: false}, + {Endpoint: "1.1.1.1:22", IsLocal: false, Ready: true, Serving: true, Terminating: false, ZoneHints: sets.String{}}, }, }, }, @@ -385,10 +385,10 @@ func TestEndpointsToEndpointsMap(t *testing.T) { }), expected: map[ServicePortName][]*BaseEndpointInfo{ makeServicePortName("ns1", "ep1", "p1", v1.ProtocolTCP): { - {Endpoint: "1.1.1.1:11", IsLocal: false, Ready: true, Serving: true, Terminating: false}, + {Endpoint: "1.1.1.1:11", IsLocal: false, Ready: true, Serving: true, Terminating: false, ZoneHints: sets.String{}}, }, makeServicePortName("ns1", "ep1", "p2", v1.ProtocolTCP): { - {Endpoint: "1.1.1.1:22", IsLocal: false, Ready: true, Serving: true, Terminating: false}, + {Endpoint: "1.1.1.1:22", IsLocal: false, Ready: true, Serving: true, Terminating: false, ZoneHints: sets.String{}}, }, }, }, @@ -418,10 +418,10 @@ func TestEndpointsToEndpointsMap(t *testing.T) { }), expected: map[ServicePortName][]*BaseEndpointInfo{ makeServicePortName("ns1", "ep1", "p1", v1.ProtocolTCP): { - {Endpoint: "[2001:db8:85a3:0:0:8a2e:370:7334]:11", IsLocal: false, Ready: true, Serving: true, Terminating: false}, + {Endpoint: "[2001:db8:85a3:0:0:8a2e:370:7334]:11", IsLocal: false, Ready: true, Serving: true, Terminating: false, ZoneHints: sets.String{}}, }, makeServicePortName("ns1", "ep1", "p2", v1.ProtocolTCP): { - {Endpoint: "[2001:db8:85a3:0:0:8a2e:370:7334]:22", IsLocal: false, Ready: true, Serving: true, Terminating: false}, + {Endpoint: "[2001:db8:85a3:0:0:8a2e:370:7334]:22", IsLocal: false, Ready: true, Serving: true, Terminating: false, ZoneHints: sets.String{}}, }, }, }, diff --git a/pkg/proxy/endpointslicecache.go b/pkg/proxy/endpointslicecache.go index 572b68b7ba1..ec3bb8b3292 100644 --- a/pkg/proxy/endpointslicecache.go +++ b/pkg/proxy/endpointslicecache.go @@ -26,8 +26,11 @@ import ( v1 "k8s.io/api/core/v1" discovery "k8s.io/api/discovery/v1beta1" "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/sets" + utilfeature "k8s.io/apiserver/pkg/util/feature" "k8s.io/client-go/tools/record" "k8s.io/klog/v2" + "k8s.io/kubernetes/pkg/features" utilproxy "k8s.io/kubernetes/pkg/proxy/util" utilnet "k8s.io/utils/net" ) @@ -78,6 +81,7 @@ type endpointInfo struct { Addresses []string NodeName *string Topology map[string]string + ZoneHints sets.String Ready bool Serving bool @@ -136,6 +140,15 @@ func newEndpointSliceInfo(endpointSlice *discovery.EndpointSlice, remove bool) * epInfo.NodeName = endpoint.NodeName + if utilfeature.DefaultFeatureGate.Enabled(features.TopologyAwareHints) { + if endpoint.Hints != nil && len(endpoint.Hints.ForZones) > 0 { + epInfo.ZoneHints = sets.String{} + for _, zone := range endpoint.Hints.ForZones { + epInfo.ZoneHints.Insert(zone.Name) + } + } + } + esInfo.Endpoints = append(esInfo.Endpoints, epInfo) } @@ -275,7 +288,7 @@ func (cache *EndpointSliceCache) addEndpointsByIP(serviceNN types.NamespacedName } endpointInfo := newBaseEndpointInfo(endpoint.Addresses[0], portNum, isLocal, endpoint.Topology, - endpoint.Ready, endpoint.Serving, endpoint.Terminating) + endpoint.Ready, endpoint.Serving, endpoint.Terminating, endpoint.ZoneHints) // This logic ensures we're deduping potential overlapping endpoints // isLocal should not vary between matching IPs, but if it does, we diff --git a/pkg/proxy/iptables/proxier.go b/pkg/proxy/iptables/proxier.go index 292e05aa852..1d6b5551036 100644 --- a/pkg/proxy/iptables/proxier.go +++ b/pkg/proxy/iptables/proxier.go @@ -1020,22 +1020,12 @@ func (proxier *Proxier) syncProxyRules() { allEndpoints := proxier.endpointsMap[svcName] - // Service Topology will not be enabled in the following cases: - // 1. externalTrafficPolicy=Local (mutually exclusive with service topology). - // 2. ServiceTopology is not enabled. - // 3. EndpointSlice is not enabled (service topology depends on endpoint slice - // to get topology information). - if !svcInfo.NodeLocalExternal() && utilfeature.DefaultFeatureGate.Enabled(features.ServiceTopology) && utilfeature.DefaultFeatureGate.Enabled(features.EndpointSliceProxying) { - allEndpoints = proxy.FilterTopologyEndpoint(proxier.nodeLabels, svcInfo.TopologyKeys(), allEndpoints) - } + // Filtering for topology aware endpoints. This function will only + // filter endpoints if appropriate feature gates are enabled and the + // Service does not have conflicting configuration such as + // externalTrafficPolicy=Local. + allEndpoints = proxy.FilterEndpoints(allEndpoints, svcInfo, proxier.nodeLabels) - // Service InternalTrafficPolicy is only enabled when all of the - // following are true: - // 1. InternalTrafficPolicy is Local - // 2. ServiceInternalTrafficPolicy feature gate is on - if utilfeature.DefaultFeatureGate.Enabled(features.ServiceInternalTrafficPolicy) && svcInfo.NodeLocalInternal() { - allEndpoints = proxy.FilterLocalEndpoint(svcInfo.InternalTrafficPolicy(), allEndpoints) - } readyEndpoints := make([]proxy.Endpoint, 0, len(allEndpoints)) for _, endpoint := range allEndpoints { if !endpoint.IsReady() { diff --git a/pkg/proxy/ipvs/proxier.go b/pkg/proxy/ipvs/proxier.go index 2656b52caa2..4961381c8ae 100644 --- a/pkg/proxy/ipvs/proxier.go +++ b/pkg/proxy/ipvs/proxier.go @@ -2057,21 +2057,15 @@ func (proxier *Proxier) syncEndpoint(svcPortName proxy.ServicePortName, onlyNode endpoints := proxier.endpointsMap[svcPortName] - // Service Topology will not be enabled in the following cases: - // 1. externalTrafficPolicy=Local (mutually exclusive with service topology). - // 2. ServiceTopology is not enabled. - // 3. EndpointSlice is not enabled (service topology depends on endpoint slice - // to get topology information). - if !onlyNodeLocalEndpoints && utilfeature.DefaultFeatureGate.Enabled(features.ServiceTopology) && utilfeature.DefaultFeatureGate.Enabled(features.EndpointSliceProxying) { - endpoints = proxy.FilterTopologyEndpoint(proxier.nodeLabels, proxier.serviceMap[svcPortName].TopologyKeys(), endpoints) - } - - // Service InternalTrafficPolicy is only enabled when all of the - // following are true: - // 1. InternalTrafficPolicy is PreferLocal or Local - // 2. ServiceInternalTrafficPolicy feature gate is on - if utilfeature.DefaultFeatureGate.Enabled(features.ServiceInternalTrafficPolicy) && onlyNodeLocalEndpointsForInternal { - endpoints = proxy.FilterLocalEndpoint(proxier.serviceMap[svcPortName].InternalTrafficPolicy(), endpoints) + // Filtering for topology aware endpoints. This function will only + // filter endpoints if appropriate feature gates are enabled and the + // Service does not have conflicting configuration such as + // externalTrafficPolicy=Local. + svcInfo, ok := proxier.serviceMap[svcPortName] + if !ok { + klog.Warningf("Unable to filter endpoints due to missing Service info for %s", svcPortName) + } else { + endpoints = proxy.FilterEndpoints(endpoints, svcInfo, proxier.nodeLabels) } for _, epInfo := range endpoints { diff --git a/pkg/proxy/ipvs/proxier_test.go b/pkg/proxy/ipvs/proxier_test.go index a60cee03c2e..ac20a461b9d 100644 --- a/pkg/proxy/ipvs/proxier_test.go +++ b/pkg/proxy/ipvs/proxier_test.go @@ -2952,12 +2952,12 @@ func Test_updateEndpointsMap(t *testing.T) { }, oldEndpoints: map[proxy.ServicePortName][]*proxy.BaseEndpointInfo{ makeServicePortName("ns1", "ep1", "", v1.ProtocolUDP): { - {Endpoint: "1.1.1.1:11", IsLocal: false, Ready: true, Serving: true, Terminating: false}, + {Endpoint: "1.1.1.1:11", IsLocal: false, Ready: true, Serving: true, Terminating: false, ZoneHints: sets.String{}}, }, }, expectedResult: map[proxy.ServicePortName][]*proxy.BaseEndpointInfo{ makeServicePortName("ns1", "ep1", "", v1.ProtocolUDP): { - {Endpoint: "1.1.1.1:11", IsLocal: false, Ready: true, Serving: true, Terminating: false}, + {Endpoint: "1.1.1.1:11", IsLocal: false, Ready: true, Serving: true, Terminating: false, ZoneHints: sets.String{}}, }, }, expectedStaleEndpoints: []proxy.ServiceEndpoint{}, @@ -2973,12 +2973,12 @@ func Test_updateEndpointsMap(t *testing.T) { }, oldEndpoints: map[proxy.ServicePortName][]*proxy.BaseEndpointInfo{ makeServicePortName("ns1", "ep1", "p11", v1.ProtocolUDP): { - {Endpoint: "1.1.1.1:11", IsLocal: true, Ready: true, Serving: true, Terminating: false}, + {Endpoint: "1.1.1.1:11", IsLocal: true, Ready: true, Serving: true, Terminating: false, ZoneHints: sets.String{}}, }, }, expectedResult: map[proxy.ServicePortName][]*proxy.BaseEndpointInfo{ makeServicePortName("ns1", "ep1", "p11", v1.ProtocolUDP): { - {Endpoint: "1.1.1.1:11", IsLocal: true, Ready: true, Serving: true, Terminating: false}, + {Endpoint: "1.1.1.1:11", IsLocal: true, Ready: true, Serving: true, Terminating: false, ZoneHints: sets.String{}}, }, }, expectedStaleEndpoints: []proxy.ServiceEndpoint{}, @@ -2996,18 +2996,18 @@ func Test_updateEndpointsMap(t *testing.T) { }, oldEndpoints: map[proxy.ServicePortName][]*proxy.BaseEndpointInfo{ makeServicePortName("ns1", "ep1", "p11", v1.ProtocolUDP): { - {Endpoint: "1.1.1.1:11", IsLocal: false, Ready: true, Serving: true, Terminating: false}, + {Endpoint: "1.1.1.1:11", IsLocal: false, Ready: true, Serving: true, Terminating: false, ZoneHints: sets.String{}}, }, makeServicePortName("ns1", "ep1", "p12", v1.ProtocolUDP): { - {Endpoint: "1.1.1.2:12", IsLocal: false, Ready: true, Serving: true, Terminating: false}, + {Endpoint: "1.1.1.2:12", IsLocal: false, Ready: true, Serving: true, Terminating: false, ZoneHints: sets.String{}}, }, }, expectedResult: map[proxy.ServicePortName][]*proxy.BaseEndpointInfo{ makeServicePortName("ns1", "ep1", "p11", v1.ProtocolUDP): { - {Endpoint: "1.1.1.1:11", IsLocal: false, Ready: true, Serving: true, Terminating: false}, + {Endpoint: "1.1.1.1:11", IsLocal: false, Ready: true, Serving: true, Terminating: false, ZoneHints: sets.String{}}, }, makeServicePortName("ns1", "ep1", "p12", v1.ProtocolUDP): { - {Endpoint: "1.1.1.2:12", IsLocal: false, Ready: true, Serving: true, Terminating: false}, + {Endpoint: "1.1.1.2:12", IsLocal: false, Ready: true, Serving: true, Terminating: false, ZoneHints: sets.String{}}, }, }, expectedStaleEndpoints: []proxy.ServiceEndpoint{}, @@ -3023,24 +3023,24 @@ func Test_updateEndpointsMap(t *testing.T) { }, oldEndpoints: map[proxy.ServicePortName][]*proxy.BaseEndpointInfo{ makeServicePortName("ns1", "ep1", "p11", v1.ProtocolUDP): { - {Endpoint: "1.1.1.1:11", IsLocal: true, Ready: true, Serving: true, Terminating: false}, + {Endpoint: "1.1.1.1:11", IsLocal: true, Ready: true, Serving: true, Terminating: false, ZoneHints: sets.String{}}, }, makeServicePortName("ns1", "ep1", "p12", v1.ProtocolUDP): { - {Endpoint: "1.1.1.1:12", IsLocal: true, Ready: true, Serving: true, Terminating: false}, + {Endpoint: "1.1.1.1:12", IsLocal: true, Ready: true, Serving: true, Terminating: false, ZoneHints: sets.String{}}, }, makeServicePortName("ns1", "ep1", "p13", v1.ProtocolUDP): { - {Endpoint: "1.1.1.3:13", IsLocal: false, Ready: true, Serving: true, Terminating: false}, + {Endpoint: "1.1.1.3:13", IsLocal: false, Ready: true, Serving: true, Terminating: false, ZoneHints: sets.String{}}, }, }, expectedResult: map[proxy.ServicePortName][]*proxy.BaseEndpointInfo{ makeServicePortName("ns1", "ep1", "p11", v1.ProtocolUDP): { - {Endpoint: "1.1.1.1:11", IsLocal: true, Ready: true, Serving: true, Terminating: false}, + {Endpoint: "1.1.1.1:11", IsLocal: true, Ready: true, Serving: true, Terminating: false, ZoneHints: sets.String{}}, }, makeServicePortName("ns1", "ep1", "p12", v1.ProtocolUDP): { - {Endpoint: "1.1.1.1:12", IsLocal: true, Ready: true, Serving: true, Terminating: false}, + {Endpoint: "1.1.1.1:12", IsLocal: true, Ready: true, Serving: true, Terminating: false, ZoneHints: sets.String{}}, }, makeServicePortName("ns1", "ep1", "p13", v1.ProtocolUDP): { - {Endpoint: "1.1.1.3:13", IsLocal: false, Ready: true, Serving: true, Terminating: false}, + {Endpoint: "1.1.1.3:13", IsLocal: false, Ready: true, Serving: true, Terminating: false, ZoneHints: sets.String{}}, }, }, expectedStaleEndpoints: []proxy.ServiceEndpoint{}, @@ -3060,54 +3060,54 @@ func Test_updateEndpointsMap(t *testing.T) { }, oldEndpoints: map[proxy.ServicePortName][]*proxy.BaseEndpointInfo{ makeServicePortName("ns1", "ep1", "p11", v1.ProtocolUDP): { - {Endpoint: "1.1.1.1:11", IsLocal: false, Ready: true, Serving: true, Terminating: false}, - {Endpoint: "1.1.1.2:11", IsLocal: true, Ready: true, Serving: true, Terminating: false}, + {Endpoint: "1.1.1.1:11", IsLocal: false, Ready: true, Serving: true, Terminating: false, ZoneHints: sets.String{}}, + {Endpoint: "1.1.1.2:11", IsLocal: true, Ready: true, Serving: true, Terminating: false, ZoneHints: sets.String{}}, }, makeServicePortName("ns1", "ep1", "p12", v1.ProtocolUDP): { - {Endpoint: "1.1.1.1:12", IsLocal: false, Ready: true, Serving: true, Terminating: false}, - {Endpoint: "1.1.1.2:12", IsLocal: true, Ready: true, Serving: true, Terminating: false}, + {Endpoint: "1.1.1.1:12", IsLocal: false, Ready: true, Serving: true, Terminating: false, ZoneHints: sets.String{}}, + {Endpoint: "1.1.1.2:12", IsLocal: true, Ready: true, Serving: true, Terminating: false, ZoneHints: sets.String{}}, }, makeServicePortName("ns1", "ep1", "p13", v1.ProtocolUDP): { - {Endpoint: "1.1.1.3:13", IsLocal: false, Ready: true, Serving: true, Terminating: false}, - {Endpoint: "1.1.1.4:13", IsLocal: true, Ready: true, Serving: true, Terminating: false}, + {Endpoint: "1.1.1.3:13", IsLocal: false, Ready: true, Serving: true, Terminating: false, ZoneHints: sets.String{}}, + {Endpoint: "1.1.1.4:13", IsLocal: true, Ready: true, Serving: true, Terminating: false, ZoneHints: sets.String{}}, }, makeServicePortName("ns1", "ep1", "p14", v1.ProtocolUDP): { - {Endpoint: "1.1.1.3:14", IsLocal: false, Ready: true, Serving: true, Terminating: false}, - {Endpoint: "1.1.1.4:14", IsLocal: true, Ready: true, Serving: true, Terminating: false}, + {Endpoint: "1.1.1.3:14", IsLocal: false, Ready: true, Serving: true, Terminating: false, ZoneHints: sets.String{}}, + {Endpoint: "1.1.1.4:14", IsLocal: true, Ready: true, Serving: true, Terminating: false, ZoneHints: sets.String{}}, }, makeServicePortName("ns2", "ep2", "p21", v1.ProtocolUDP): { - {Endpoint: "2.2.2.1:21", IsLocal: false, Ready: true, Serving: true, Terminating: false}, - {Endpoint: "2.2.2.2:21", IsLocal: true, Ready: true, Serving: true, Terminating: false}, + {Endpoint: "2.2.2.1:21", IsLocal: false, Ready: true, Serving: true, Terminating: false, ZoneHints: sets.String{}}, + {Endpoint: "2.2.2.2:21", IsLocal: true, Ready: true, Serving: true, Terminating: false, ZoneHints: sets.String{}}, }, makeServicePortName("ns2", "ep2", "p22", v1.ProtocolUDP): { - {Endpoint: "2.2.2.1:22", IsLocal: false, Ready: true, Serving: true, Terminating: false}, - {Endpoint: "2.2.2.2:22", IsLocal: true, Ready: true, Serving: true, Terminating: false}, + {Endpoint: "2.2.2.1:22", IsLocal: false, Ready: true, Serving: true, Terminating: false, ZoneHints: sets.String{}}, + {Endpoint: "2.2.2.2:22", IsLocal: true, Ready: true, Serving: true, Terminating: false, ZoneHints: sets.String{}}, }, }, expectedResult: map[proxy.ServicePortName][]*proxy.BaseEndpointInfo{ makeServicePortName("ns1", "ep1", "p11", v1.ProtocolUDP): { - {Endpoint: "1.1.1.1:11", IsLocal: false, Ready: true, Serving: true, Terminating: false}, - {Endpoint: "1.1.1.2:11", IsLocal: true, Ready: true, Serving: true, Terminating: false}, + {Endpoint: "1.1.1.1:11", IsLocal: false, Ready: true, Serving: true, Terminating: false, ZoneHints: sets.String{}}, + {Endpoint: "1.1.1.2:11", IsLocal: true, Ready: true, Serving: true, Terminating: false, ZoneHints: sets.String{}}, }, makeServicePortName("ns1", "ep1", "p12", v1.ProtocolUDP): { - {Endpoint: "1.1.1.1:12", IsLocal: false, Ready: true, Serving: true, Terminating: false}, - {Endpoint: "1.1.1.2:12", IsLocal: true, Ready: true, Serving: true, Terminating: false}, + {Endpoint: "1.1.1.1:12", IsLocal: false, Ready: true, Serving: true, Terminating: false, ZoneHints: sets.String{}}, + {Endpoint: "1.1.1.2:12", IsLocal: true, Ready: true, Serving: true, Terminating: false, ZoneHints: sets.String{}}, }, makeServicePortName("ns1", "ep1", "p13", v1.ProtocolUDP): { - {Endpoint: "1.1.1.3:13", IsLocal: false, Ready: true, Serving: true, Terminating: false}, - {Endpoint: "1.1.1.4:13", IsLocal: true, Ready: true, Serving: true, Terminating: false}, + {Endpoint: "1.1.1.3:13", IsLocal: false, Ready: true, Serving: true, Terminating: false, ZoneHints: sets.String{}}, + {Endpoint: "1.1.1.4:13", IsLocal: true, Ready: true, Serving: true, Terminating: false, ZoneHints: sets.String{}}, }, makeServicePortName("ns1", "ep1", "p14", v1.ProtocolUDP): { - {Endpoint: "1.1.1.3:14", IsLocal: false, Ready: true, Serving: true, Terminating: false}, - {Endpoint: "1.1.1.4:14", IsLocal: true, Ready: true, Serving: true, Terminating: false}, + {Endpoint: "1.1.1.3:14", IsLocal: false, Ready: true, Serving: true, Terminating: false, ZoneHints: sets.String{}}, + {Endpoint: "1.1.1.4:14", IsLocal: true, Ready: true, Serving: true, Terminating: false, ZoneHints: sets.String{}}, }, makeServicePortName("ns2", "ep2", "p21", v1.ProtocolUDP): { - {Endpoint: "2.2.2.1:21", IsLocal: false, Ready: true, Serving: true, Terminating: false}, - {Endpoint: "2.2.2.2:21", IsLocal: true, Ready: true, Serving: true, Terminating: false}, + {Endpoint: "2.2.2.1:21", IsLocal: false, Ready: true, Serving: true, Terminating: false, ZoneHints: sets.String{}}, + {Endpoint: "2.2.2.2:21", IsLocal: true, Ready: true, Serving: true, Terminating: false, ZoneHints: sets.String{}}, }, makeServicePortName("ns2", "ep2", "p22", v1.ProtocolUDP): { - {Endpoint: "2.2.2.1:22", IsLocal: false, Ready: true, Serving: true, Terminating: false}, - {Endpoint: "2.2.2.2:22", IsLocal: true, Ready: true, Serving: true, Terminating: false}, + {Endpoint: "2.2.2.1:22", IsLocal: false, Ready: true, Serving: true, Terminating: false, ZoneHints: sets.String{}}, + {Endpoint: "2.2.2.2:22", IsLocal: true, Ready: true, Serving: true, Terminating: false, ZoneHints: sets.String{}}, }, }, expectedStaleEndpoints: []proxy.ServiceEndpoint{}, @@ -3127,7 +3127,7 @@ func Test_updateEndpointsMap(t *testing.T) { oldEndpoints: map[proxy.ServicePortName][]*proxy.BaseEndpointInfo{}, expectedResult: map[proxy.ServicePortName][]*proxy.BaseEndpointInfo{ makeServicePortName("ns1", "ep1", "", v1.ProtocolUDP): { - {Endpoint: "1.1.1.1:11", IsLocal: true, Ready: true, Serving: true, Terminating: false}, + {Endpoint: "1.1.1.1:11", IsLocal: true, Ready: true, Serving: true, Terminating: false, ZoneHints: sets.String{}}, }, }, expectedStaleEndpoints: []proxy.ServiceEndpoint{}, @@ -3147,7 +3147,7 @@ func Test_updateEndpointsMap(t *testing.T) { }, oldEndpoints: map[proxy.ServicePortName][]*proxy.BaseEndpointInfo{ makeServicePortName("ns1", "ep1", "", v1.ProtocolUDP): { - {Endpoint: "1.1.1.1:11", IsLocal: true, Ready: true, Serving: true, Terminating: false}, + {Endpoint: "1.1.1.1:11", IsLocal: true, Ready: true, Serving: true, Terminating: false, ZoneHints: sets.String{}}, }, }, expectedResult: map[proxy.ServicePortName][]*proxy.BaseEndpointInfo{}, @@ -3167,17 +3167,17 @@ func Test_updateEndpointsMap(t *testing.T) { }, oldEndpoints: map[proxy.ServicePortName][]*proxy.BaseEndpointInfo{ makeServicePortName("ns1", "ep1", "p11", v1.ProtocolUDP): { - {Endpoint: "1.1.1.1:11", IsLocal: false, Ready: true, Serving: true, Terminating: false}, + {Endpoint: "1.1.1.1:11", IsLocal: false, Ready: true, Serving: true, Terminating: false, ZoneHints: sets.String{}}, }, }, expectedResult: map[proxy.ServicePortName][]*proxy.BaseEndpointInfo{ makeServicePortName("ns1", "ep1", "p11", v1.ProtocolUDP): { - {Endpoint: "1.1.1.1:11", IsLocal: false, Ready: true, Serving: true, Terminating: false}, - {Endpoint: "1.1.1.2:11", IsLocal: true, Ready: true, Serving: true, Terminating: false}, + {Endpoint: "1.1.1.1:11", IsLocal: false, Ready: true, Serving: true, Terminating: false, ZoneHints: sets.String{}}, + {Endpoint: "1.1.1.2:11", IsLocal: true, Ready: true, Serving: true, Terminating: false, ZoneHints: sets.String{}}, }, makeServicePortName("ns1", "ep1", "p12", v1.ProtocolUDP): { - {Endpoint: "1.1.1.1:12", IsLocal: false, Ready: true, Serving: true, Terminating: false}, - {Endpoint: "1.1.1.2:12", IsLocal: true, Ready: true, Serving: true, Terminating: false}, + {Endpoint: "1.1.1.1:12", IsLocal: false, Ready: true, Serving: true, Terminating: false, ZoneHints: sets.String{}}, + {Endpoint: "1.1.1.2:12", IsLocal: true, Ready: true, Serving: true, Terminating: false, ZoneHints: sets.String{}}, }, }, expectedStaleEndpoints: []proxy.ServiceEndpoint{}, @@ -3197,17 +3197,17 @@ func Test_updateEndpointsMap(t *testing.T) { }, oldEndpoints: map[proxy.ServicePortName][]*proxy.BaseEndpointInfo{ makeServicePortName("ns1", "ep1", "p11", v1.ProtocolUDP): { - {Endpoint: "1.1.1.1:11", IsLocal: false, Ready: true, Serving: true, Terminating: false}, - {Endpoint: "1.1.1.2:11", IsLocal: true, Ready: true, Serving: true, Terminating: false}, + {Endpoint: "1.1.1.1:11", IsLocal: false, Ready: true, Serving: true, Terminating: false, ZoneHints: sets.String{}}, + {Endpoint: "1.1.1.2:11", IsLocal: true, Ready: true, Serving: true, Terminating: false, ZoneHints: sets.String{}}, }, makeServicePortName("ns1", "ep1", "p12", v1.ProtocolUDP): { - {Endpoint: "1.1.1.1:12", IsLocal: false, Ready: true, Serving: true, Terminating: false}, - {Endpoint: "1.1.1.2:12", IsLocal: true, Ready: true, Serving: true, Terminating: false}, + {Endpoint: "1.1.1.1:12", IsLocal: false, Ready: true, Serving: true, Terminating: false, ZoneHints: sets.String{}}, + {Endpoint: "1.1.1.2:12", IsLocal: true, Ready: true, Serving: true, Terminating: false, ZoneHints: sets.String{}}, }, }, expectedResult: map[proxy.ServicePortName][]*proxy.BaseEndpointInfo{ makeServicePortName("ns1", "ep1", "p11", v1.ProtocolUDP): { - {Endpoint: "1.1.1.1:11", IsLocal: false, Ready: true, Serving: true, Terminating: false}, + {Endpoint: "1.1.1.1:11", IsLocal: false, Ready: true, Serving: true, Terminating: false, ZoneHints: sets.String{}}, }, }, expectedStaleEndpoints: []proxy.ServiceEndpoint{{ @@ -3232,15 +3232,15 @@ func Test_updateEndpointsMap(t *testing.T) { }, oldEndpoints: map[proxy.ServicePortName][]*proxy.BaseEndpointInfo{ makeServicePortName("ns1", "ep1", "p11", v1.ProtocolUDP): { - {Endpoint: "1.1.1.1:11", IsLocal: false, Ready: true, Serving: true, Terminating: false}, + {Endpoint: "1.1.1.1:11", IsLocal: false, Ready: true, Serving: true, Terminating: false, ZoneHints: sets.String{}}, }, }, expectedResult: map[proxy.ServicePortName][]*proxy.BaseEndpointInfo{ makeServicePortName("ns1", "ep1", "p11", v1.ProtocolUDP): { - {Endpoint: "1.1.1.1:11", IsLocal: false, Ready: true, Serving: true, Terminating: false}, + {Endpoint: "1.1.1.1:11", IsLocal: false, Ready: true, Serving: true, Terminating: false, ZoneHints: sets.String{}}, }, makeServicePortName("ns1", "ep1", "p12", v1.ProtocolUDP): { - {Endpoint: "1.1.1.2:12", IsLocal: true, Ready: true, Serving: true, Terminating: false}, + {Endpoint: "1.1.1.2:12", IsLocal: true, Ready: true, Serving: true, Terminating: false, ZoneHints: sets.String{}}, }, }, expectedStaleEndpoints: []proxy.ServiceEndpoint{}, @@ -3260,15 +3260,15 @@ func Test_updateEndpointsMap(t *testing.T) { }, oldEndpoints: map[proxy.ServicePortName][]*proxy.BaseEndpointInfo{ makeServicePortName("ns1", "ep1", "p11", v1.ProtocolUDP): { - {Endpoint: "1.1.1.1:11", IsLocal: false, Ready: true, Serving: true, Terminating: false}, + {Endpoint: "1.1.1.1:11", IsLocal: false, Ready: true, Serving: true, Terminating: false, ZoneHints: sets.String{}}, }, makeServicePortName("ns1", "ep1", "p12", v1.ProtocolUDP): { - {Endpoint: "1.1.1.2:12", IsLocal: false, Ready: true, Serving: true, Terminating: false}, + {Endpoint: "1.1.1.2:12", IsLocal: false, Ready: true, Serving: true, Terminating: false, ZoneHints: sets.String{}}, }, }, expectedResult: map[proxy.ServicePortName][]*proxy.BaseEndpointInfo{ makeServicePortName("ns1", "ep1", "p11", v1.ProtocolUDP): { - {Endpoint: "1.1.1.1:11", IsLocal: false, Ready: true, Serving: true, Terminating: false}, + {Endpoint: "1.1.1.1:11", IsLocal: false, Ready: true, Serving: true, Terminating: false, ZoneHints: sets.String{}}, }, }, expectedStaleEndpoints: []proxy.ServiceEndpoint{{ @@ -3287,12 +3287,12 @@ func Test_updateEndpointsMap(t *testing.T) { }, oldEndpoints: map[proxy.ServicePortName][]*proxy.BaseEndpointInfo{ makeServicePortName("ns1", "ep1", "p11", v1.ProtocolUDP): { - {Endpoint: "1.1.1.1:11", IsLocal: false, Ready: true, Serving: true, Terminating: false}, + {Endpoint: "1.1.1.1:11", IsLocal: false, Ready: true, Serving: true, Terminating: false, ZoneHints: sets.String{}}, }, }, expectedResult: map[proxy.ServicePortName][]*proxy.BaseEndpointInfo{ makeServicePortName("ns1", "ep1", "p11-2", v1.ProtocolUDP): { - {Endpoint: "1.1.1.1:11", IsLocal: false, Ready: true, Serving: true, Terminating: false}, + {Endpoint: "1.1.1.1:11", IsLocal: false, Ready: true, Serving: true, Terminating: false, ZoneHints: sets.String{}}, }, }, expectedStaleEndpoints: []proxy.ServiceEndpoint{{ @@ -3313,12 +3313,12 @@ func Test_updateEndpointsMap(t *testing.T) { }, oldEndpoints: map[proxy.ServicePortName][]*proxy.BaseEndpointInfo{ makeServicePortName("ns1", "ep1", "p11", v1.ProtocolUDP): { - {Endpoint: "1.1.1.1:11", IsLocal: false, Ready: true, Serving: true, Terminating: false}, + {Endpoint: "1.1.1.1:11", IsLocal: false, Ready: true, Serving: true, Terminating: false, ZoneHints: sets.String{}}, }, }, expectedResult: map[proxy.ServicePortName][]*proxy.BaseEndpointInfo{ makeServicePortName("ns1", "ep1", "p11", v1.ProtocolUDP): { - {Endpoint: "1.1.1.1:22", IsLocal: false, Ready: true, Serving: true, Terminating: false}, + {Endpoint: "1.1.1.1:22", IsLocal: false, Ready: true, Serving: true, Terminating: false, ZoneHints: sets.String{}}, }, }, expectedStaleEndpoints: []proxy.ServiceEndpoint{{ @@ -3343,39 +3343,39 @@ func Test_updateEndpointsMap(t *testing.T) { }, oldEndpoints: map[proxy.ServicePortName][]*proxy.BaseEndpointInfo{ makeServicePortName("ns1", "ep1", "p11", v1.ProtocolUDP): { - {Endpoint: "1.1.1.1:11", IsLocal: false, Ready: true, Serving: true, Terminating: false}, + {Endpoint: "1.1.1.1:11", IsLocal: false, Ready: true, Serving: true, Terminating: false, ZoneHints: sets.String{}}, }, makeServicePortName("ns2", "ep2", "p22", v1.ProtocolUDP): { - {Endpoint: "2.2.2.2:22", IsLocal: true, Ready: true, Serving: true, Terminating: false}, - {Endpoint: "2.2.2.22:22", IsLocal: true, Ready: true, Serving: true, Terminating: false}, + {Endpoint: "2.2.2.2:22", IsLocal: true, Ready: true, Serving: true, Terminating: false, ZoneHints: sets.String{}}, + {Endpoint: "2.2.2.22:22", IsLocal: true, Ready: true, Serving: true, Terminating: false, ZoneHints: sets.String{}}, }, makeServicePortName("ns2", "ep2", "p23", v1.ProtocolUDP): { - {Endpoint: "2.2.2.3:23", IsLocal: true, Ready: true, Serving: true, Terminating: false}, + {Endpoint: "2.2.2.3:23", IsLocal: true, Ready: true, Serving: true, Terminating: false, ZoneHints: sets.String{}}, }, makeServicePortName("ns4", "ep4", "p44", v1.ProtocolUDP): { - {Endpoint: "4.4.4.4:44", IsLocal: true, Ready: true, Serving: true, Terminating: false}, - {Endpoint: "4.4.4.5:44", IsLocal: true, Ready: true, Serving: true, Terminating: false}, + {Endpoint: "4.4.4.4:44", IsLocal: true, Ready: true, Serving: true, Terminating: false, ZoneHints: sets.String{}}, + {Endpoint: "4.4.4.5:44", IsLocal: true, Ready: true, Serving: true, Terminating: false, ZoneHints: sets.String{}}, }, makeServicePortName("ns4", "ep4", "p45", v1.ProtocolUDP): { - {Endpoint: "4.4.4.6:45", IsLocal: true, Ready: true, Serving: true, Terminating: false}, + {Endpoint: "4.4.4.6:45", IsLocal: true, Ready: true, Serving: true, Terminating: false, ZoneHints: sets.String{}}, }, }, expectedResult: map[proxy.ServicePortName][]*proxy.BaseEndpointInfo{ makeServicePortName("ns1", "ep1", "p11", v1.ProtocolUDP): { - {Endpoint: "1.1.1.1:11", IsLocal: false, Ready: true, Serving: true, Terminating: false}, - {Endpoint: "1.1.1.11:11", IsLocal: false, Ready: true, Serving: true, Terminating: false}, + {Endpoint: "1.1.1.1:11", IsLocal: false, Ready: true, Serving: true, Terminating: false, ZoneHints: sets.String{}}, + {Endpoint: "1.1.1.11:11", IsLocal: false, Ready: true, Serving: true, Terminating: false, ZoneHints: sets.String{}}, }, makeServicePortName("ns1", "ep1", "p12", v1.ProtocolUDP): { - {Endpoint: "1.1.1.2:12", IsLocal: false, Ready: true, Serving: true, Terminating: false}, + {Endpoint: "1.1.1.2:12", IsLocal: false, Ready: true, Serving: true, Terminating: false, ZoneHints: sets.String{}}, }, makeServicePortName("ns1", "ep1", "p122", v1.ProtocolUDP): { - {Endpoint: "1.1.1.2:122", IsLocal: false, Ready: true, Serving: true, Terminating: false}, + {Endpoint: "1.1.1.2:122", IsLocal: false, Ready: true, Serving: true, Terminating: false, ZoneHints: sets.String{}}, }, makeServicePortName("ns3", "ep3", "p33", v1.ProtocolUDP): { - {Endpoint: "3.3.3.3:33", IsLocal: false, Ready: true, Serving: true, Terminating: false}, + {Endpoint: "3.3.3.3:33", IsLocal: false, Ready: true, Serving: true, Terminating: false, ZoneHints: sets.String{}}, }, makeServicePortName("ns4", "ep4", "p44", v1.ProtocolUDP): { - {Endpoint: "4.4.4.4:44", IsLocal: true, Ready: true, Serving: true, Terminating: false}, + {Endpoint: "4.4.4.4:44", IsLocal: true, Ready: true, Serving: true, Terminating: false, ZoneHints: sets.String{}}, }, }, expectedStaleEndpoints: []proxy.ServiceEndpoint{{ @@ -3413,7 +3413,7 @@ func Test_updateEndpointsMap(t *testing.T) { oldEndpoints: map[proxy.ServicePortName][]*proxy.BaseEndpointInfo{}, expectedResult: map[proxy.ServicePortName][]*proxy.BaseEndpointInfo{ makeServicePortName("ns1", "ep1", "", v1.ProtocolUDP): { - {Endpoint: "1.1.1.1:11", IsLocal: false, Ready: true, Serving: true, Terminating: false}, + {Endpoint: "1.1.1.1:11", IsLocal: false, Ready: true, Serving: true, Terminating: false, ZoneHints: sets.String{}}, }, }, expectedStaleEndpoints: []proxy.ServiceEndpoint{}, diff --git a/pkg/proxy/service.go b/pkg/proxy/service.go index a78930944a1..d56e0aed81a 100644 --- a/pkg/proxy/service.go +++ b/pkg/proxy/service.go @@ -55,6 +55,7 @@ type BaseServiceInfo struct { nodeLocalInternal bool internalTrafficPolicy *v1.ServiceInternalTrafficPolicyType topologyKeys []string + hintsAnnotation string } var _ ServicePort = &BaseServiceInfo{} @@ -138,6 +139,11 @@ func (info *BaseServiceInfo) TopologyKeys() []string { return info.topologyKeys } +// HintsAnnotation is part of ServicePort interface. +func (info *BaseServiceInfo) HintsAnnotation() string { + return info.hintsAnnotation +} + func (sct *ServiceChangeTracker) newBaseServiceInfo(port *v1.ServicePort, service *v1.Service) *BaseServiceInfo { nodeLocalExternal := false if apiservice.RequestsOnlyLocalTraffic(service) { @@ -165,6 +171,7 @@ func (sct *ServiceChangeTracker) newBaseServiceInfo(port *v1.ServicePort, servic nodeLocalInternal: nodeLocalInternal, internalTrafficPolicy: service.Spec.InternalTrafficPolicy, topologyKeys: service.Spec.TopologyKeys, + hintsAnnotation: service.Annotations[v1.AnnotationTopologyAwareHints], } loadBalancerSourceRanges := make([]string, len(service.Spec.LoadBalancerSourceRanges)) diff --git a/pkg/proxy/topology.go b/pkg/proxy/topology.go index b272f365257..03657185822 100644 --- a/pkg/proxy/topology.go +++ b/pkg/proxy/topology.go @@ -19,11 +19,80 @@ package proxy import ( v1 "k8s.io/api/core/v1" utilfeature "k8s.io/apiserver/pkg/util/feature" + "k8s.io/klog/v2" "k8s.io/kubernetes/pkg/features" ) -// FilterTopologyEndpoint returns the appropriate endpoints based on the cluster -// topology. +// FilterEndpoints filters endpoints based on Service configuration, node +// labels, and enabled feature gates. This is primarily used to enable topology +// aware routing. +func FilterEndpoints(endpoints []Endpoint, svcInfo ServicePort, nodeLabels map[string]string) []Endpoint { + if svcInfo.NodeLocalExternal() || !utilfeature.DefaultFeatureGate.Enabled(features.EndpointSliceProxying) { + return endpoints + } + + if utilfeature.DefaultFeatureGate.Enabled(features.ServiceTopology) { + return deprecatedTopologyFilter(nodeLabels, svcInfo.TopologyKeys(), endpoints) + } + + if utilfeature.DefaultFeatureGate.Enabled(features.ServiceInternalTrafficPolicy) && svcInfo.NodeLocalInternal() { + return filterEndpointsInternalTrafficPolicy(svcInfo.InternalTrafficPolicy(), endpoints) + } + + if utilfeature.DefaultFeatureGate.Enabled(features.TopologyAwareHints) { + return filterEndpointsWithHints(endpoints, svcInfo.HintsAnnotation(), nodeLabels) + } + + return endpoints +} + +// filterEndpointsWithHints provides filtering based on the hints included in +// EndpointSlices. If any of the following are true, the full list of endpoints +// will be returned without any filtering: +// * The AnnotationTopologyAwareHints annotation is not set to "auto" for this +// Service. +// * No zone is specified in node labels. +// * No endpoints for this Service have a hint pointing to the zone this +// instance of kube-proxy is running in. +// * One or more endpoints for this Service do not have hints specified. +func filterEndpointsWithHints(endpoints []Endpoint, hintsAnnotation string, nodeLabels map[string]string) []Endpoint { + if hintsAnnotation != "auto" { + if hintsAnnotation != "" && hintsAnnotation != "disabled" { + klog.Warningf("Skipping topology aware endpoint filtering since Service has unexpected value for %s annotation: %s", v1.AnnotationTopologyAwareHints, hintsAnnotation) + } + return endpoints + } + + zone, ok := nodeLabels[v1.LabelTopologyZone] + if !ok || zone == "" { + klog.Warningf("Skipping topology aware endpoint filtering since node is missing %s label", v1.LabelTopologyZone) + return endpoints + } + + filteredEndpoints := []Endpoint{} + + for _, endpoint := range endpoints { + if endpoint.GetZoneHints().Len() == 0 { + klog.Warningf("Skipping topology aware endpoint filtering since one or more endpoints is missing a zone hint") + return endpoints + } + if endpoint.GetZoneHints().Has(zone) { + filteredEndpoints = append(filteredEndpoints, endpoint) + } + } + + if len(filteredEndpoints) > 0 { + klog.Warningf("Skipping topology aware endpoint filtering since no hints were provided for zone %s", zone) + return filteredEndpoints + } + + return endpoints +} + +// deprecatedTopologyFilter returns the appropriate endpoints based on the +// cluster topology. This will be removed in an upcoming release along with the +// ServiceTopology feature gate. +// // This uses the current node's labels, which contain topology information, and // the required topologyKeys to find appropriate endpoints. If both the endpoint's // topology and the current node have matching values for topologyKeys[0], the @@ -40,7 +109,7 @@ import ( // // If topologyKeys is not specified or empty, no topology constraints will be // applied and this will return all endpoints. -func FilterTopologyEndpoint(nodeLabels map[string]string, topologyKeys []string, endpoints []Endpoint) []Endpoint { +func deprecatedTopologyFilter(nodeLabels map[string]string, topologyKeys []string, endpoints []Endpoint) []Endpoint { // Do not filter endpoints if service has no topology keys. if len(topologyKeys) == 0 { return endpoints @@ -81,13 +150,13 @@ func FilterTopologyEndpoint(nodeLabels map[string]string, topologyKeys []string, return filteredEndpoints } -// FilterLocalEndpoint returns the node local endpoints based on configured -// InternalTrafficPolicy. +// filterEndpointsInternalTrafficPolicy returns the node local endpoints based +// on configured InternalTrafficPolicy. // // If ServiceInternalTrafficPolicy feature gate is off, returns the original -// endpoints slice. +// EndpointSlice. // Otherwise, if InternalTrafficPolicy is Local, only return the node local endpoints. -func FilterLocalEndpoint(internalTrafficPolicy *v1.ServiceInternalTrafficPolicyType, endpoints []Endpoint) []Endpoint { +func filterEndpointsInternalTrafficPolicy(internalTrafficPolicy *v1.ServiceInternalTrafficPolicyType, endpoints []Endpoint) []Endpoint { if !utilfeature.DefaultFeatureGate.Enabled(features.ServiceInternalTrafficPolicy) { return endpoints } diff --git a/pkg/proxy/topology_test.go b/pkg/proxy/topology_test.go index 9f501152f3a..24bcad9887e 100644 --- a/pkg/proxy/topology_test.go +++ b/pkg/proxy/topology_test.go @@ -22,12 +22,328 @@ import ( v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/sets" utilfeature "k8s.io/apiserver/pkg/util/feature" featuregatetesting "k8s.io/component-base/featuregate/testing" "k8s.io/kubernetes/pkg/features" ) -func TestFilterTopologyEndpoint(t *testing.T) { +func TestFilterEndpoints(t *testing.T) { + type endpoint struct { + ip string + zoneHints sets.String + } + testCases := []struct { + name string + epsProxyingEnabled bool + serviceTopologyEnabled bool + hintsEnabled bool + nodeLabels map[string]string + serviceInfo ServicePort + endpoints []endpoint + expectedEndpoints []endpoint + }{{ + name: "hints + eps proxying enabled, hints annotation == auto", + hintsEnabled: true, + epsProxyingEnabled: true, + nodeLabels: map[string]string{v1.LabelTopologyZone: "zone-a"}, + serviceInfo: &BaseServiceInfo{nodeLocalExternal: false, hintsAnnotation: "auto"}, + endpoints: []endpoint{ + {ip: "10.1.2.3", zoneHints: sets.NewString("zone-a")}, + {ip: "10.1.2.4", zoneHints: sets.NewString("zone-b")}, + {ip: "10.1.2.5", zoneHints: sets.NewString("zone-c")}, + {ip: "10.1.2.6", zoneHints: sets.NewString("zone-a")}, + }, + expectedEndpoints: []endpoint{ + {ip: "10.1.2.3", zoneHints: sets.NewString("zone-a")}, + {ip: "10.1.2.6", zoneHints: sets.NewString("zone-a")}, + }, + }, { + name: "hints + eps proxying enabled, hints annotation == disabled, hints ignored", + hintsEnabled: true, + epsProxyingEnabled: true, + nodeLabels: map[string]string{v1.LabelTopologyZone: "zone-a"}, + serviceInfo: &BaseServiceInfo{nodeLocalExternal: false, hintsAnnotation: "disabled"}, + endpoints: []endpoint{ + {ip: "10.1.2.3", zoneHints: sets.NewString("zone-a")}, + {ip: "10.1.2.4", zoneHints: sets.NewString("zone-b")}, + {ip: "10.1.2.5", zoneHints: sets.NewString("zone-c")}, + {ip: "10.1.2.6", zoneHints: sets.NewString("zone-a")}, + }, + expectedEndpoints: []endpoint{ + {ip: "10.1.2.3", zoneHints: sets.NewString("zone-a")}, + {ip: "10.1.2.4", zoneHints: sets.NewString("zone-b")}, + {ip: "10.1.2.5", zoneHints: sets.NewString("zone-c")}, + {ip: "10.1.2.6", zoneHints: sets.NewString("zone-a")}, + }, + }, { + name: "hints + eps proxying enabled, hints annotation == Auto (wrong capitalization), hints ignored", + hintsEnabled: true, + epsProxyingEnabled: true, + nodeLabels: map[string]string{v1.LabelTopologyZone: "zone-a"}, + serviceInfo: &BaseServiceInfo{nodeLocalExternal: false, hintsAnnotation: "Auto"}, + endpoints: []endpoint{ + {ip: "10.1.2.3", zoneHints: sets.NewString("zone-a")}, + {ip: "10.1.2.4", zoneHints: sets.NewString("zone-b")}, + {ip: "10.1.2.5", zoneHints: sets.NewString("zone-c")}, + {ip: "10.1.2.6", zoneHints: sets.NewString("zone-a")}, + }, + expectedEndpoints: []endpoint{ + {ip: "10.1.2.3", zoneHints: sets.NewString("zone-a")}, + {ip: "10.1.2.4", zoneHints: sets.NewString("zone-b")}, + {ip: "10.1.2.5", zoneHints: sets.NewString("zone-c")}, + {ip: "10.1.2.6", zoneHints: sets.NewString("zone-a")}, + }, + }, { + name: "hints + eps proxying enabled, hints annotation empty, hints ignored", + hintsEnabled: true, + epsProxyingEnabled: true, + nodeLabels: map[string]string{v1.LabelTopologyZone: "zone-a"}, + serviceInfo: &BaseServiceInfo{nodeLocalExternal: false}, + endpoints: []endpoint{ + {ip: "10.1.2.3", zoneHints: sets.NewString("zone-a")}, + {ip: "10.1.2.4", zoneHints: sets.NewString("zone-b")}, + {ip: "10.1.2.5", zoneHints: sets.NewString("zone-c")}, + {ip: "10.1.2.6", zoneHints: sets.NewString("zone-a")}, + }, + expectedEndpoints: []endpoint{ + {ip: "10.1.2.3", zoneHints: sets.NewString("zone-a")}, + {ip: "10.1.2.4", zoneHints: sets.NewString("zone-b")}, + {ip: "10.1.2.5", zoneHints: sets.NewString("zone-c")}, + {ip: "10.1.2.6", zoneHints: sets.NewString("zone-a")}, + }, + }, { + name: "hints enabled, eps proxying not, hints are ignored", + hintsEnabled: true, + epsProxyingEnabled: false, + nodeLabels: map[string]string{v1.LabelTopologyZone: "zone-a"}, + serviceInfo: &BaseServiceInfo{nodeLocalExternal: false}, + endpoints: []endpoint{ + {ip: "10.1.2.3", zoneHints: sets.NewString("zone-a")}, + {ip: "10.1.2.4", zoneHints: sets.NewString("zone-b")}, + {ip: "10.1.2.5", zoneHints: sets.NewString("zone-c")}, + {ip: "10.1.2.6", zoneHints: sets.NewString("zone-a")}, + }, + expectedEndpoints: []endpoint{ + {ip: "10.1.2.3", zoneHints: sets.NewString("zone-a")}, + {ip: "10.1.2.4", zoneHints: sets.NewString("zone-b")}, + {ip: "10.1.2.5", zoneHints: sets.NewString("zone-c")}, + {ip: "10.1.2.6", zoneHints: sets.NewString("zone-a")}, + }, + }, { + name: "node local endpoints, hints are ignored", + hintsEnabled: true, + epsProxyingEnabled: true, + nodeLabels: map[string]string{v1.LabelTopologyZone: "zone-a"}, + serviceInfo: &BaseServiceInfo{nodeLocalExternal: true}, + endpoints: []endpoint{ + {ip: "10.1.2.3", zoneHints: sets.NewString("zone-a")}, + {ip: "10.1.2.4", zoneHints: sets.NewString("zone-b")}, + {ip: "10.1.2.5", zoneHints: sets.NewString("zone-c")}, + {ip: "10.1.2.6", zoneHints: sets.NewString("zone-a")}, + }, + expectedEndpoints: []endpoint{ + {ip: "10.1.2.3", zoneHints: sets.NewString("zone-a")}, + {ip: "10.1.2.4", zoneHints: sets.NewString("zone-b")}, + {ip: "10.1.2.5", zoneHints: sets.NewString("zone-c")}, + {ip: "10.1.2.6", zoneHints: sets.NewString("zone-a")}, + }, + }, { + name: "all gates enabled, serviceTopology gate takes precedence and hints are ignored", + hintsEnabled: true, + epsProxyingEnabled: true, + serviceTopologyEnabled: true, + nodeLabels: map[string]string{v1.LabelTopologyZone: "zone-a"}, + serviceInfo: &BaseServiceInfo{nodeLocalExternal: true}, + endpoints: []endpoint{ + {ip: "10.1.2.3", zoneHints: sets.NewString("zone-a")}, + {ip: "10.1.2.4", zoneHints: sets.NewString("zone-b")}, + {ip: "10.1.2.5", zoneHints: sets.NewString("zone-c")}, + {ip: "10.1.2.6", zoneHints: sets.NewString("zone-a")}, + }, + expectedEndpoints: []endpoint{ + {ip: "10.1.2.3", zoneHints: sets.NewString("zone-a")}, + {ip: "10.1.2.4", zoneHints: sets.NewString("zone-b")}, + {ip: "10.1.2.5", zoneHints: sets.NewString("zone-c")}, + {ip: "10.1.2.6", zoneHints: sets.NewString("zone-a")}, + }, + }} + + endpointsToStringArray := func(endpoints []Endpoint) []string { + result := make([]string, 0, len(endpoints)) + for _, ep := range endpoints { + result = append(result, ep.String()) + } + return result + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.EndpointSliceProxying, tc.epsProxyingEnabled)() + defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.ServiceTopology, tc.serviceTopologyEnabled)() + defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.TopologyAwareHints, tc.hintsEnabled)() + + endpoints := []Endpoint{} + for _, ep := range tc.endpoints { + endpoints = append(endpoints, &BaseEndpointInfo{Endpoint: ep.ip, ZoneHints: ep.zoneHints}) + } + + expectedEndpoints := []Endpoint{} + for _, ep := range tc.expectedEndpoints { + expectedEndpoints = append(expectedEndpoints, &BaseEndpointInfo{Endpoint: ep.ip, ZoneHints: ep.zoneHints}) + } + + filteredEndpoints := FilterEndpoints(endpoints, tc.serviceInfo, tc.nodeLabels) + if len(filteredEndpoints) != len(expectedEndpoints) { + t.Errorf("expected %d filtered endpoints, got %d", len(expectedEndpoints), len(filteredEndpoints)) + } + if !reflect.DeepEqual(filteredEndpoints, expectedEndpoints) { + t.Errorf("expected %v, got %v", endpointsToStringArray(expectedEndpoints), endpointsToStringArray(filteredEndpoints)) + } + }) + } +} + +func Test_filterEndpointsWithHints(t *testing.T) { + type endpoint struct { + ip string + zoneHints sets.String + } + testCases := []struct { + name string + nodeLabels map[string]string + hintsAnnotation string + endpoints []endpoint + expectedEndpoints []endpoint + }{{ + name: "empty node labels", + nodeLabels: map[string]string{}, + hintsAnnotation: "auto", + endpoints: []endpoint{{ip: "10.1.2.3", zoneHints: sets.NewString("zone-a")}}, + expectedEndpoints: []endpoint{{ip: "10.1.2.3", zoneHints: sets.NewString("zone-a")}}, + }, { + name: "empty zone label", + nodeLabels: map[string]string{v1.LabelTopologyZone: ""}, + hintsAnnotation: "auto", + endpoints: []endpoint{{ip: "10.1.2.3", zoneHints: sets.NewString("zone-a")}}, + expectedEndpoints: []endpoint{{ip: "10.1.2.3", zoneHints: sets.NewString("zone-a")}}, + }, { + name: "node in different zone, no endpoint filtering", + nodeLabels: map[string]string{v1.LabelTopologyZone: "zone-b"}, + hintsAnnotation: "auto", + endpoints: []endpoint{{ip: "10.1.2.3", zoneHints: sets.NewString("zone-a")}}, + expectedEndpoints: []endpoint{{ip: "10.1.2.3", zoneHints: sets.NewString("zone-a")}}, + }, { + name: "normal endpoint filtering", + nodeLabels: map[string]string{v1.LabelTopologyZone: "zone-a"}, + hintsAnnotation: "auto", + endpoints: []endpoint{ + {ip: "10.1.2.3", zoneHints: sets.NewString("zone-a")}, + {ip: "10.1.2.4", zoneHints: sets.NewString("zone-b")}, + {ip: "10.1.2.5", zoneHints: sets.NewString("zone-c")}, + {ip: "10.1.2.6", zoneHints: sets.NewString("zone-a")}, + }, + expectedEndpoints: []endpoint{ + {ip: "10.1.2.3", zoneHints: sets.NewString("zone-a")}, + {ip: "10.1.2.6", zoneHints: sets.NewString("zone-a")}, + }, + }, { + name: "hintsAnnotation empty, no filtering applied", + nodeLabels: map[string]string{v1.LabelTopologyZone: "zone-a"}, + hintsAnnotation: "", + endpoints: []endpoint{ + {ip: "10.1.2.3", zoneHints: sets.NewString("zone-a")}, + {ip: "10.1.2.4", zoneHints: sets.NewString("zone-b")}, + {ip: "10.1.2.5", zoneHints: sets.NewString("zone-c")}, + {ip: "10.1.2.6", zoneHints: sets.NewString("zone-a")}, + }, + expectedEndpoints: []endpoint{ + {ip: "10.1.2.3", zoneHints: sets.NewString("zone-a")}, + {ip: "10.1.2.4", zoneHints: sets.NewString("zone-b")}, + {ip: "10.1.2.5", zoneHints: sets.NewString("zone-c")}, + {ip: "10.1.2.6", zoneHints: sets.NewString("zone-a")}, + }, + }, { + name: "hintsAnnotation disabled, no filtering applied", + nodeLabels: map[string]string{v1.LabelTopologyZone: "zone-a"}, + hintsAnnotation: "disabled", + endpoints: []endpoint{ + {ip: "10.1.2.3", zoneHints: sets.NewString("zone-a")}, + {ip: "10.1.2.4", zoneHints: sets.NewString("zone-b")}, + {ip: "10.1.2.5", zoneHints: sets.NewString("zone-c")}, + {ip: "10.1.2.6", zoneHints: sets.NewString("zone-a")}, + }, + expectedEndpoints: []endpoint{ + {ip: "10.1.2.3", zoneHints: sets.NewString("zone-a")}, + {ip: "10.1.2.4", zoneHints: sets.NewString("zone-b")}, + {ip: "10.1.2.5", zoneHints: sets.NewString("zone-c")}, + {ip: "10.1.2.6", zoneHints: sets.NewString("zone-a")}, + }, + }, { + name: "missing hints, no filtering applied", + nodeLabels: map[string]string{v1.LabelTopologyZone: "zone-a"}, + hintsAnnotation: "auto", + endpoints: []endpoint{ + {ip: "10.1.2.3", zoneHints: sets.NewString("zone-a")}, + {ip: "10.1.2.4", zoneHints: sets.NewString("zone-b")}, + {ip: "10.1.2.5"}, + {ip: "10.1.2.6", zoneHints: sets.NewString("zone-a")}, + }, + expectedEndpoints: []endpoint{ + {ip: "10.1.2.3", zoneHints: sets.NewString("zone-a")}, + {ip: "10.1.2.4", zoneHints: sets.NewString("zone-b")}, + {ip: "10.1.2.5"}, + {ip: "10.1.2.6", zoneHints: sets.NewString("zone-a")}, + }, + }, { + name: "multiple hints per endpoint, filtering includes any endpoint with zone included", + nodeLabels: map[string]string{v1.LabelTopologyZone: "zone-c"}, + hintsAnnotation: "auto", + endpoints: []endpoint{ + {ip: "10.1.2.3", zoneHints: sets.NewString("zone-a", "zone-b", "zone-c")}, + {ip: "10.1.2.4", zoneHints: sets.NewString("zone-b", "zone-c")}, + {ip: "10.1.2.5", zoneHints: sets.NewString("zone-b", "zone-d")}, + {ip: "10.1.2.6", zoneHints: sets.NewString("zone-c")}, + }, + expectedEndpoints: []endpoint{ + {ip: "10.1.2.3", zoneHints: sets.NewString("zone-a", "zone-b", "zone-c")}, + {ip: "10.1.2.4", zoneHints: sets.NewString("zone-b", "zone-c")}, + {ip: "10.1.2.6", zoneHints: sets.NewString("zone-c")}, + }, + }} + + endpointsToStringArray := func(endpoints []Endpoint) []string { + result := make([]string, 0, len(endpoints)) + for _, ep := range endpoints { + result = append(result, ep.String()) + } + return result + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + endpoints := []Endpoint{} + for _, ep := range tc.endpoints { + endpoints = append(endpoints, &BaseEndpointInfo{Endpoint: ep.ip, ZoneHints: ep.zoneHints}) + } + + expectedEndpoints := []Endpoint{} + for _, ep := range tc.expectedEndpoints { + expectedEndpoints = append(expectedEndpoints, &BaseEndpointInfo{Endpoint: ep.ip, ZoneHints: ep.zoneHints}) + } + + filteredEndpoints := filterEndpointsWithHints(endpoints, tc.hintsAnnotation, tc.nodeLabels) + if len(filteredEndpoints) != len(expectedEndpoints) { + t.Errorf("expected %d filtered endpoints, got %d", len(expectedEndpoints), len(filteredEndpoints)) + } + if !reflect.DeepEqual(filteredEndpoints, expectedEndpoints) { + t.Errorf("expected %v, got %v", endpointsToStringArray(expectedEndpoints), endpointsToStringArray(filteredEndpoints)) + } + }) + } +} + +func Test_deprecatedTopologyFilter(t *testing.T) { type endpoint struct { Endpoint string NodeName types.NodeName @@ -470,7 +786,7 @@ func TestFilterTopologyEndpoint(t *testing.T) { } currentNodeLabels := tc.nodeLabels[tc.currentNodeName] filteredEndpoint := []endpoint{} - for _, ep := range FilterTopologyEndpoint(currentNodeLabels, tc.topologyKeys, endpoints) { + for _, ep := range deprecatedTopologyFilter(currentNodeLabels, tc.topologyKeys, endpoints) { filteredEndpoint = append(filteredEndpoint, m[ep]) } if !reflect.DeepEqual(filteredEndpoint, tc.expected) { @@ -480,7 +796,7 @@ func TestFilterTopologyEndpoint(t *testing.T) { } } -func TestFilterLocalEndpoint(t *testing.T) { +func Test_filterEndpointsInternalTrafficPolicy(t *testing.T) { cluster := v1.ServiceInternalTrafficPolicyCluster local := v1.ServiceInternalTrafficPolicyLocal @@ -566,7 +882,7 @@ func TestFilterLocalEndpoint(t *testing.T) { for _, tc := range testCases { defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.ServiceInternalTrafficPolicy, tc.featureGateOn)() t.Run(tc.name, func(t *testing.T) { - filteredEndpoint := FilterLocalEndpoint(tc.internalTrafficPolicy, tc.endpoints) + filteredEndpoint := filterEndpointsInternalTrafficPolicy(tc.internalTrafficPolicy, tc.endpoints) if !reflect.DeepEqual(filteredEndpoint, tc.expected) { t.Errorf("expected %v, got %v", tc.expected, filteredEndpoint) } diff --git a/pkg/proxy/types.go b/pkg/proxy/types.go index 9085bf0c743..a33d6ba145e 100644 --- a/pkg/proxy/types.go +++ b/pkg/proxy/types.go @@ -22,6 +22,7 @@ import ( v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/sets" "k8s.io/kubernetes/pkg/proxy/config" ) @@ -91,6 +92,8 @@ type ServicePort interface { InternalTrafficPolicy() *v1.ServiceInternalTrafficPolicyType // TopologyKeys returns service TopologyKeys as a string array. TopologyKeys() []string + // HintsAnnotation returns the value of the v1.AnnotationTopologyAwareHints annotation. + HintsAnnotation() string } // Endpoint in an interface which abstracts information about an endpoint. @@ -117,6 +120,9 @@ type Endpoint interface { IsTerminating() bool // GetTopology returns the topology information of the endpoint. GetTopology() map[string]string + // GetZoneHint returns the zone hint for the endpoint. This is based on + // endpoint.hints.forZones[0].name in the EndpointSlice API. + GetZoneHints() sets.String // IP returns IP part of the endpoint. IP() string // Port returns the Port part of the endpoint. diff --git a/pkg/proxy/winkernel/proxier.go b/pkg/proxy/winkernel/proxier.go index fa585a62403..6ff9444d979 100644 --- a/pkg/proxy/winkernel/proxier.go +++ b/pkg/proxy/winkernel/proxier.go @@ -37,6 +37,7 @@ import ( v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/intstr" + "k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/util/wait" utilfeature "k8s.io/apiserver/pkg/util/feature" "k8s.io/client-go/tools/record" @@ -201,6 +202,11 @@ func (info *endpointsInfo) GetTopology() map[string]string { return nil } +// GetZoneHint returns the zone hint for the endpoint. +func (info *endpointsInfo) GetZoneHints() sets.String { + return sets.String{} +} + // IP returns just the IP part of the endpoint, it's a part of proxy.Endpoint interface. func (info *endpointsInfo) IP() string { return info.ip diff --git a/pkg/registry/discovery/endpointslice/strategy.go b/pkg/registry/discovery/endpointslice/strategy.go index 9b2e8902bd2..c5e20b64bc2 100644 --- a/pkg/registry/discovery/endpointslice/strategy.go +++ b/pkg/registry/discovery/endpointslice/strategy.go @@ -111,13 +111,17 @@ func (endpointSliceStrategy) AllowUnconditionalUpdate() bool { // dropDisabledConditionsOnCreate will drop any fields that are disabled. func dropDisabledFieldsOnCreate(endpointSlice *discovery.EndpointSlice) { dropTerminating := !utilfeature.DefaultFeatureGate.Enabled(features.EndpointSliceTerminatingCondition) + dropHints := !utilfeature.DefaultFeatureGate.Enabled(features.TopologyAwareHints) - if dropTerminating { + if dropHints || dropTerminating { for i := range endpointSlice.Endpoints { if dropTerminating { endpointSlice.Endpoints[i].Conditions.Serving = nil endpointSlice.Endpoints[i].Conditions.Terminating = nil } + if dropHints { + endpointSlice.Endpoints[i].Hints = nil + } } } } @@ -135,12 +139,25 @@ func dropDisabledFieldsOnUpdate(oldEPS, newEPS *discovery.EndpointSlice) { } } - if dropTerminating { + dropHints := !utilfeature.DefaultFeatureGate.Enabled(features.TopologyAwareHints) + if dropHints { + for _, ep := range oldEPS.Endpoints { + if ep.Hints != nil { + dropHints = false + break + } + } + } + + if dropHints || dropTerminating { for i := range newEPS.Endpoints { if dropTerminating { newEPS.Endpoints[i].Conditions.Serving = nil newEPS.Endpoints[i].Conditions.Terminating = nil } + if dropHints { + newEPS.Endpoints[i].Hints = nil + } } } } diff --git a/pkg/registry/discovery/endpointslice/strategy_test.go b/pkg/registry/discovery/endpointslice/strategy_test.go index 3b89b843dd9..6d80cea7e72 100644 --- a/pkg/registry/discovery/endpointslice/strategy_test.go +++ b/pkg/registry/discovery/endpointslice/strategy_test.go @@ -35,6 +35,7 @@ func Test_dropDisabledFieldsOnCreate(t *testing.T) { testcases := []struct { name string terminatingGateEnabled bool + hintsGateEnabled bool eps *discovery.EndpointSlice expectedEPS *discovery.EndpointSlice }{ @@ -162,6 +163,7 @@ func Test_dropDisabledFieldsOnCreate(t *testing.T) { for _, testcase := range testcases { t.Run(testcase.name, func(t *testing.T) { defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.EndpointSliceTerminatingCondition, testcase.terminatingGateEnabled)() + defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.TopologyAwareHints, testcase.hintsGateEnabled)() dropDisabledFieldsOnCreate(testcase.eps) if !apiequality.Semantic.DeepEqual(testcase.eps, testcase.expectedEPS) { @@ -177,6 +179,7 @@ func Test_dropDisabledFieldsOnUpdate(t *testing.T) { testcases := []struct { name string terminatingGateEnabled bool + hintsGateEnabled bool oldEPS *discovery.EndpointSlice newEPS *discovery.EndpointSlice expectedEPS *discovery.EndpointSlice @@ -524,11 +527,138 @@ func Test_dropDisabledFieldsOnUpdate(t *testing.T) { }, }, }, + { + name: "hints gate enabled, set on new EPS", + hintsGateEnabled: true, + oldEPS: &discovery.EndpointSlice{ + Endpoints: []discovery.Endpoint{ + { + Hints: nil, + }, + { + Hints: nil, + }, + }, + }, + newEPS: &discovery.EndpointSlice{ + Endpoints: []discovery.Endpoint{ + { + Hints: &discovery.EndpointHints{ + ForZones: []discovery.ForZone{{Name: "zone-a"}}, + }, + }, + { + Hints: &discovery.EndpointHints{ + ForZones: []discovery.ForZone{{Name: "zone-b"}}, + }, + }, + }, + }, + expectedEPS: &discovery.EndpointSlice{ + Endpoints: []discovery.Endpoint{ + { + Hints: &discovery.EndpointHints{ + ForZones: []discovery.ForZone{{Name: "zone-a"}}, + }, + }, + { + Hints: &discovery.EndpointHints{ + ForZones: []discovery.ForZone{{Name: "zone-b"}}, + }, + }, + }, + }, + }, + { + name: "hints gate disabled, set on new EPS", + hintsGateEnabled: false, + oldEPS: &discovery.EndpointSlice{ + Endpoints: []discovery.Endpoint{ + { + Hints: nil, + }, + { + Hints: nil, + }, + }, + }, + newEPS: &discovery.EndpointSlice{ + Endpoints: []discovery.Endpoint{ + { + Hints: &discovery.EndpointHints{ + ForZones: []discovery.ForZone{{Name: "zone-a"}}, + }, + }, + { + Hints: &discovery.EndpointHints{ + ForZones: []discovery.ForZone{{Name: "zone-b"}}, + }, + }, + }, + }, + expectedEPS: &discovery.EndpointSlice{ + Endpoints: []discovery.Endpoint{ + { + Hints: nil, + }, + { + Hints: nil, + }, + }, + }, + }, + { + name: "hints gate disabled, set on new and old EPS", + hintsGateEnabled: false, + oldEPS: &discovery.EndpointSlice{ + Endpoints: []discovery.Endpoint{ + { + Hints: &discovery.EndpointHints{ + ForZones: []discovery.ForZone{{Name: "zone-a-old"}}, + }, + }, + { + Hints: &discovery.EndpointHints{ + ForZones: []discovery.ForZone{{Name: "zone-b-old"}}, + }, + }, + }, + }, + newEPS: &discovery.EndpointSlice{ + Endpoints: []discovery.Endpoint{ + { + Hints: &discovery.EndpointHints{ + ForZones: []discovery.ForZone{{Name: "zone-a"}}, + }, + }, + { + Hints: &discovery.EndpointHints{ + ForZones: []discovery.ForZone{{Name: "zone-b"}}, + }, + }, + }, + }, + expectedEPS: &discovery.EndpointSlice{ + Endpoints: []discovery.Endpoint{ + { + Hints: &discovery.EndpointHints{ + ForZones: []discovery.ForZone{{Name: "zone-a"}}, + }, + }, + { + Hints: &discovery.EndpointHints{ + ForZones: []discovery.ForZone{{Name: "zone-b"}}, + }, + }, + }, + }, + }, } for _, testcase := range testcases { t.Run(testcase.name, func(t *testing.T) { defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.EndpointSliceTerminatingCondition, testcase.terminatingGateEnabled)() + defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.TopologyAwareHints, testcase.hintsGateEnabled)() dropDisabledFieldsOnUpdate(testcase.oldEPS, testcase.newEPS) if !apiequality.Semantic.DeepEqual(testcase.newEPS, testcase.expectedEPS) { diff --git a/staging/src/k8s.io/api/core/v1/annotation_key_constants.go b/staging/src/k8s.io/api/core/v1/annotation_key_constants.go index b3902b6d16f..2b486415a60 100644 --- a/staging/src/k8s.io/api/core/v1/annotation_key_constants.go +++ b/staging/src/k8s.io/api/core/v1/annotation_key_constants.go @@ -138,4 +138,9 @@ const ( // // This annotation is alpha-level and is only honored when PodDeletionCost feature is enabled. PodDeletionCost = "controller.kubernetes.io/pod-deletion-cost" + + // AnnotationTopologyAwareHints can be used to enable or disable Topology + // Aware Hints for a Service. This may be set to "auto" or "disabled". Any + // other value is treated as "disabled". + AnnotationTopologyAwareHints = "service.kubernetes.io/topology-aware-hints" ) diff --git a/staging/src/k8s.io/api/discovery/v1/generated.pb.go b/staging/src/k8s.io/api/discovery/v1/generated.pb.go index 34d0b385b7d..38bdb02a52b 100644 --- a/staging/src/k8s.io/api/discovery/v1/generated.pb.go +++ b/staging/src/k8s.io/api/discovery/v1/generated.pb.go @@ -102,10 +102,38 @@ func (m *EndpointConditions) XXX_DiscardUnknown() { var xxx_messageInfo_EndpointConditions proto.InternalMessageInfo +func (m *EndpointHints) Reset() { *m = EndpointHints{} } +func (*EndpointHints) ProtoMessage() {} +func (*EndpointHints) Descriptor() ([]byte, []int) { + return fileDescriptor_3a5d310fb1396ddf, []int{2} +} +func (m *EndpointHints) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *EndpointHints) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil +} +func (m *EndpointHints) XXX_Merge(src proto.Message) { + xxx_messageInfo_EndpointHints.Merge(m, src) +} +func (m *EndpointHints) XXX_Size() int { + return m.Size() +} +func (m *EndpointHints) XXX_DiscardUnknown() { + xxx_messageInfo_EndpointHints.DiscardUnknown(m) +} + +var xxx_messageInfo_EndpointHints proto.InternalMessageInfo + func (m *EndpointPort) Reset() { *m = EndpointPort{} } func (*EndpointPort) ProtoMessage() {} func (*EndpointPort) Descriptor() ([]byte, []int) { - return fileDescriptor_3a5d310fb1396ddf, []int{2} + return fileDescriptor_3a5d310fb1396ddf, []int{3} } func (m *EndpointPort) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -133,7 +161,7 @@ var xxx_messageInfo_EndpointPort proto.InternalMessageInfo func (m *EndpointSlice) Reset() { *m = EndpointSlice{} } func (*EndpointSlice) ProtoMessage() {} func (*EndpointSlice) Descriptor() ([]byte, []int) { - return fileDescriptor_3a5d310fb1396ddf, []int{3} + return fileDescriptor_3a5d310fb1396ddf, []int{4} } func (m *EndpointSlice) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -161,7 +189,7 @@ var xxx_messageInfo_EndpointSlice proto.InternalMessageInfo func (m *EndpointSliceList) Reset() { *m = EndpointSliceList{} } func (*EndpointSliceList) ProtoMessage() {} func (*EndpointSliceList) Descriptor() ([]byte, []int) { - return fileDescriptor_3a5d310fb1396ddf, []int{4} + return fileDescriptor_3a5d310fb1396ddf, []int{5} } func (m *EndpointSliceList) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -186,13 +214,43 @@ func (m *EndpointSliceList) XXX_DiscardUnknown() { var xxx_messageInfo_EndpointSliceList proto.InternalMessageInfo +func (m *ForZone) Reset() { *m = ForZone{} } +func (*ForZone) ProtoMessage() {} +func (*ForZone) Descriptor() ([]byte, []int) { + return fileDescriptor_3a5d310fb1396ddf, []int{6} +} +func (m *ForZone) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *ForZone) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil +} +func (m *ForZone) XXX_Merge(src proto.Message) { + xxx_messageInfo_ForZone.Merge(m, src) +} +func (m *ForZone) XXX_Size() int { + return m.Size() +} +func (m *ForZone) XXX_DiscardUnknown() { + xxx_messageInfo_ForZone.DiscardUnknown(m) +} + +var xxx_messageInfo_ForZone proto.InternalMessageInfo + func init() { proto.RegisterType((*Endpoint)(nil), "k8s.io.api.discovery.v1.Endpoint") proto.RegisterMapType((map[string]string)(nil), "k8s.io.api.discovery.v1.Endpoint.DeprecatedTopologyEntry") proto.RegisterType((*EndpointConditions)(nil), "k8s.io.api.discovery.v1.EndpointConditions") + proto.RegisterType((*EndpointHints)(nil), "k8s.io.api.discovery.v1.EndpointHints") proto.RegisterType((*EndpointPort)(nil), "k8s.io.api.discovery.v1.EndpointPort") proto.RegisterType((*EndpointSlice)(nil), "k8s.io.api.discovery.v1.EndpointSlice") proto.RegisterType((*EndpointSliceList)(nil), "k8s.io.api.discovery.v1.EndpointSliceList") + proto.RegisterType((*ForZone)(nil), "k8s.io.api.discovery.v1.ForZone") } func init() { @@ -200,59 +258,63 @@ func init() { } var fileDescriptor_3a5d310fb1396ddf = []byte{ - // 823 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x8c, 0x54, 0xcf, 0x6f, 0xe3, 0x44, - 0x14, 0x8e, 0x9b, 0x86, 0xda, 0x93, 0xad, 0xd8, 0x1d, 0x21, 0x6d, 0x14, 0x90, 0x5d, 0x82, 0x16, - 0x45, 0xaa, 0xb0, 0x69, 0x85, 0xd0, 0xc2, 0x89, 0x9a, 0xad, 0xf8, 0xbd, 0x54, 0xb3, 0x3d, 0xad, - 0x90, 0x60, 0x6a, 0xbf, 0x75, 0x4d, 0xe2, 0x19, 0x6b, 0x66, 0x12, 0x29, 0x9c, 0xb8, 0x70, 0x86, - 0xff, 0x83, 0xff, 0x81, 0x23, 0xea, 0x71, 0x6f, 0xec, 0xc9, 0xa2, 0xe6, 0xbf, 0xd8, 0x13, 0x9a, - 0xb1, 0x1d, 0x7b, 0x49, 0xab, 0x70, 0xf3, 0x7c, 0xef, 0x7d, 0xdf, 0x7b, 0xef, 0x9b, 0x79, 0x46, - 0x9f, 0xcc, 0x1e, 0x4a, 0x3f, 0xe5, 0xc1, 0x6c, 0x71, 0x01, 0x82, 0x81, 0x02, 0x19, 0x2c, 0x81, - 0xc5, 0x5c, 0x04, 0x75, 0x80, 0xe6, 0x69, 0x10, 0xa7, 0x32, 0xe2, 0x4b, 0x10, 0xab, 0x60, 0x79, - 0x14, 0x24, 0xc0, 0x40, 0x50, 0x05, 0xb1, 0x9f, 0x0b, 0xae, 0x38, 0xbe, 0x5f, 0x25, 0xfa, 0x34, - 0x4f, 0xfd, 0x75, 0xa2, 0xbf, 0x3c, 0x1a, 0xbf, 0x97, 0xa4, 0xea, 0x72, 0x71, 0xe1, 0x47, 0x3c, - 0x0b, 0x12, 0x9e, 0xf0, 0xc0, 0xe4, 0x5f, 0x2c, 0x9e, 0x99, 0x93, 0x39, 0x98, 0xaf, 0x4a, 0x67, - 0x3c, 0xe9, 0x14, 0x8c, 0xb8, 0x80, 0x1b, 0x6a, 0x8d, 0x3f, 0x68, 0x73, 0x32, 0x1a, 0x5d, 0xa6, - 0x4c, 0xf7, 0x94, 0xcf, 0x12, 0x0d, 0xc8, 0x20, 0x03, 0x45, 0x6f, 0x62, 0x05, 0xb7, 0xb1, 0xc4, - 0x82, 0xa9, 0x34, 0x83, 0x0d, 0xc2, 0x87, 0xdb, 0x08, 0x32, 0xba, 0x84, 0x8c, 0xfe, 0x97, 0x37, - 0xf9, 0x7d, 0x17, 0xd9, 0xa7, 0x2c, 0xce, 0x79, 0xca, 0x14, 0x3e, 0x44, 0x0e, 0x8d, 0x63, 0x01, - 0x52, 0x82, 0x1c, 0x59, 0x07, 0xfd, 0xa9, 0x13, 0xee, 0x97, 0x85, 0xe7, 0x9c, 0x34, 0x20, 0x69, - 0xe3, 0xf8, 0x7b, 0x84, 0x22, 0xce, 0xe2, 0x54, 0xa5, 0x9c, 0xc9, 0xd1, 0xce, 0x81, 0x35, 0x1d, - 0x1e, 0x1f, 0xfa, 0xb7, 0x38, 0xeb, 0x37, 0x35, 0x3e, 0x5d, 0x53, 0x42, 0x7c, 0x55, 0x78, 0xbd, - 0xb2, 0xf0, 0x50, 0x8b, 0x91, 0x8e, 0x24, 0x9e, 0x22, 0xfb, 0x92, 0x4b, 0xc5, 0x68, 0x06, 0xa3, - 0xfe, 0x81, 0x35, 0x75, 0xc2, 0x3b, 0x65, 0xe1, 0xd9, 0x9f, 0xd7, 0x18, 0x59, 0x47, 0xf1, 0x19, - 0x72, 0x14, 0x15, 0x09, 0x28, 0x02, 0xcf, 0x46, 0xbb, 0xa6, 0x93, 0x77, 0xba, 0x9d, 0xe8, 0xbb, - 0xd1, 0x4d, 0x7c, 0x7b, 0xf1, 0x23, 0x44, 0x3a, 0x09, 0x04, 0xb0, 0x08, 0xaa, 0xe1, 0xce, 0x1b, - 0x26, 0x69, 0x45, 0xf0, 0x2f, 0x16, 0xc2, 0x31, 0xe4, 0x02, 0x22, 0xed, 0xd5, 0x39, 0xcf, 0xf9, - 0x9c, 0x27, 0xab, 0xd1, 0xe0, 0xa0, 0x3f, 0x1d, 0x1e, 0x7f, 0xb4, 0x75, 0x4a, 0xff, 0xd1, 0x06, - 0xf7, 0x94, 0x29, 0xb1, 0x0a, 0xc7, 0xf5, 0xcc, 0x78, 0x33, 0x81, 0xdc, 0x50, 0x50, 0x7b, 0xc0, - 0x78, 0x0c, 0x8f, 0xb5, 0x07, 0xaf, 0xb5, 0x1e, 0x3c, 0xae, 0x31, 0xb2, 0x8e, 0xe2, 0xb7, 0xd0, - 0xee, 0x4f, 0x9c, 0xc1, 0x68, 0xcf, 0x64, 0xd9, 0x65, 0xe1, 0xed, 0x3e, 0xe5, 0x0c, 0x88, 0x41, - 0xc7, 0xa7, 0xe8, 0xfe, 0x2d, 0x2d, 0xe1, 0xbb, 0xa8, 0x3f, 0x83, 0xd5, 0xc8, 0xd2, 0x3c, 0xa2, - 0x3f, 0xf1, 0x1b, 0x68, 0xb0, 0xa4, 0xf3, 0x05, 0x98, 0x4b, 0x75, 0x48, 0x75, 0xf8, 0x78, 0xe7, - 0xa1, 0x35, 0xf9, 0xd5, 0x42, 0x78, 0xf3, 0x26, 0xb1, 0x87, 0x06, 0x02, 0x68, 0x5c, 0x89, 0xd8, - 0xa1, 0x53, 0x16, 0xde, 0x80, 0x68, 0x80, 0x54, 0x38, 0x7e, 0x80, 0xf6, 0x24, 0x88, 0x65, 0xca, - 0x12, 0xa3, 0x69, 0x87, 0xc3, 0xb2, 0xf0, 0xf6, 0x9e, 0x54, 0x10, 0x69, 0x62, 0xf8, 0x08, 0x0d, - 0x15, 0x88, 0x2c, 0x65, 0x54, 0xe9, 0xd4, 0xbe, 0x49, 0x7d, 0xbd, 0x2c, 0xbc, 0xe1, 0x79, 0x0b, - 0x93, 0x6e, 0xce, 0xe4, 0x4f, 0x0b, 0xdd, 0x69, 0x3a, 0x3a, 0xe3, 0x42, 0x69, 0x1f, 0xcc, 0x8b, - 0xb1, 0x5a, 0x1f, 0x8c, 0x53, 0x06, 0xc5, 0x9f, 0x21, 0xdb, 0xbc, 0xfb, 0x88, 0xcf, 0xab, 0xe9, - 0xc2, 0x43, 0xed, 0xe7, 0x59, 0x8d, 0xbd, 0x2c, 0xbc, 0x37, 0x37, 0x77, 0xda, 0x6f, 0xc2, 0x64, - 0x4d, 0xd6, 0x65, 0x72, 0x2e, 0x94, 0xe9, 0x71, 0x50, 0x95, 0xd1, 0xe5, 0x89, 0x41, 0xf5, 0x20, - 0x34, 0xcf, 0x1b, 0x9a, 0x79, 0x92, 0x4e, 0x35, 0xc8, 0x49, 0x0b, 0x93, 0x6e, 0xce, 0xe4, 0xaf, - 0x1d, 0xb4, 0xdf, 0x0c, 0xf2, 0x64, 0x9e, 0x46, 0x80, 0x7f, 0x40, 0xb6, 0xfe, 0x3d, 0xc4, 0x54, - 0x51, 0x33, 0xcd, 0xf0, 0xf8, 0xfd, 0xce, 0xc3, 0x5b, 0x6f, 0xb9, 0x9f, 0xcf, 0x12, 0x0d, 0x48, - 0x5f, 0x67, 0xb7, 0xcf, 0xfc, 0x1b, 0x50, 0xb4, 0xdd, 0xb1, 0x16, 0x23, 0x6b, 0x55, 0xfc, 0x08, - 0x0d, 0xeb, 0x7d, 0x3e, 0x5f, 0xe5, 0x50, 0xb7, 0x39, 0xa9, 0x29, 0xc3, 0x93, 0x36, 0xf4, 0xf2, - 0xd5, 0x23, 0xe9, 0xd2, 0x30, 0x41, 0x0e, 0xd4, 0x8d, 0xeb, 0xff, 0x80, 0xde, 0x90, 0xb7, 0xb7, - 0x6e, 0x48, 0x78, 0xaf, 0x2e, 0xe3, 0x34, 0x88, 0x24, 0xad, 0x0c, 0xfe, 0x12, 0x0d, 0xb4, 0x91, - 0x72, 0xd4, 0x37, 0x7a, 0x0f, 0xb6, 0xea, 0x69, 0xf3, 0xc3, 0xfd, 0x5a, 0x73, 0xa0, 0x4f, 0x92, - 0x54, 0x12, 0x93, 0x3f, 0x2c, 0x74, 0xef, 0x15, 0x67, 0xbf, 0x4e, 0xa5, 0xc2, 0xdf, 0x6d, 0xb8, - 0xeb, 0xff, 0x3f, 0x77, 0x35, 0xdb, 0x78, 0x7b, 0xb7, 0xae, 0x66, 0x37, 0x48, 0xc7, 0xd9, 0xaf, - 0xd0, 0x20, 0x55, 0x90, 0x35, 0x7e, 0xbc, 0xbb, 0xb5, 0x7f, 0xd3, 0x58, 0x3b, 0xc0, 0x17, 0x9a, - 0x4c, 0x2a, 0x8d, 0x70, 0x7a, 0x75, 0xed, 0xf6, 0x9e, 0x5f, 0xbb, 0xbd, 0x17, 0xd7, 0x6e, 0xef, - 0xe7, 0xd2, 0xb5, 0xae, 0x4a, 0xd7, 0x7a, 0x5e, 0xba, 0xd6, 0x8b, 0xd2, 0xb5, 0xfe, 0x2e, 0x5d, - 0xeb, 0xb7, 0x7f, 0xdc, 0xde, 0xd3, 0x9d, 0xe5, 0xd1, 0xbf, 0x01, 0x00, 0x00, 0xff, 0xff, 0x34, - 0x9c, 0x0c, 0xa4, 0x1b, 0x07, 0x00, 0x00, + // 889 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x8c, 0x54, 0x4d, 0x6f, 0xe3, 0x44, + 0x18, 0x8e, 0x9b, 0x86, 0xda, 0x93, 0x56, 0xec, 0x8e, 0x90, 0x36, 0x0a, 0x28, 0x0e, 0x46, 0x8b, + 0x22, 0x55, 0xd8, 0xb4, 0x42, 0x68, 0xe1, 0x44, 0xcd, 0x96, 0x5d, 0xbe, 0x4a, 0x35, 0xdb, 0xd3, + 0x0a, 0x69, 0x71, 0xed, 0xb7, 0x8e, 0x49, 0x33, 0x63, 0xcd, 0x4c, 0x22, 0x85, 0x13, 0x17, 0xce, + 0xf0, 0x8b, 0x38, 0xa2, 0x1e, 0xf7, 0xc6, 0x9e, 0x2c, 0x6a, 0xfe, 0x02, 0xa7, 0x3d, 0xa1, 0x19, + 0x7f, 0x96, 0xb4, 0x0a, 0xb7, 0x99, 0x67, 0x9e, 0xe7, 0xfd, 0x78, 0x66, 0xe6, 0x45, 0x9f, 0xcd, + 0x1e, 0x09, 0x37, 0x61, 0xde, 0x6c, 0x71, 0x0e, 0x9c, 0x82, 0x04, 0xe1, 0x2d, 0x81, 0x46, 0x8c, + 0x7b, 0xe5, 0x41, 0x90, 0x26, 0x5e, 0x94, 0x88, 0x90, 0x2d, 0x81, 0xaf, 0xbc, 0xe5, 0x81, 0x17, + 0x03, 0x05, 0x1e, 0x48, 0x88, 0xdc, 0x94, 0x33, 0xc9, 0xf0, 0x83, 0x82, 0xe8, 0x06, 0x69, 0xe2, + 0xd6, 0x44, 0x77, 0x79, 0x30, 0xfc, 0x20, 0x4e, 0xe4, 0x74, 0x71, 0xee, 0x86, 0x6c, 0xee, 0xc5, + 0x2c, 0x66, 0x9e, 0xe6, 0x9f, 0x2f, 0x2e, 0xf4, 0x4e, 0x6f, 0xf4, 0xaa, 0x88, 0x33, 0x74, 0x5a, + 0x09, 0x43, 0xc6, 0xe1, 0x96, 0x5c, 0xc3, 0x8f, 0x1a, 0xce, 0x3c, 0x08, 0xa7, 0x09, 0x55, 0x35, + 0xa5, 0xb3, 0x58, 0x01, 0xc2, 0x9b, 0x83, 0x0c, 0x6e, 0x53, 0x79, 0x77, 0xa9, 0xf8, 0x82, 0xca, + 0x64, 0x0e, 0x6b, 0x82, 0x8f, 0x37, 0x09, 0x44, 0x38, 0x85, 0x79, 0xf0, 0x5f, 0x9d, 0xf3, 0xcf, + 0x36, 0x32, 0x8f, 0x69, 0x94, 0xb2, 0x84, 0x4a, 0xbc, 0x8f, 0xac, 0x20, 0x8a, 0x38, 0x08, 0x01, + 0x62, 0x60, 0x8c, 0xbb, 0x13, 0xcb, 0xdf, 0xcb, 0x33, 0xdb, 0x3a, 0xaa, 0x40, 0xd2, 0x9c, 0xe3, + 0x17, 0x08, 0x85, 0x8c, 0x46, 0x89, 0x4c, 0x18, 0x15, 0x83, 0xad, 0xb1, 0x31, 0xe9, 0x1f, 0xee, + 0xbb, 0x77, 0x38, 0xeb, 0x56, 0x39, 0x3e, 0xaf, 0x25, 0x3e, 0xbe, 0xca, 0xec, 0x4e, 0x9e, 0xd9, + 0xa8, 0xc1, 0x48, 0x2b, 0x24, 0x9e, 0x20, 0x73, 0xca, 0x84, 0xa4, 0xc1, 0x1c, 0x06, 0xdd, 0xb1, + 0x31, 0xb1, 0xfc, 0xdd, 0x3c, 0xb3, 0xcd, 0xa7, 0x25, 0x46, 0xea, 0x53, 0x7c, 0x8a, 0x2c, 0x19, + 0xf0, 0x18, 0x24, 0x81, 0x8b, 0xc1, 0xb6, 0xae, 0xe4, 0xbd, 0x76, 0x25, 0xea, 0x6e, 0x54, 0x11, + 0xdf, 0x9d, 0xff, 0x08, 0xa1, 0x22, 0x01, 0x07, 0x1a, 0x42, 0xd1, 0xdc, 0x59, 0xa5, 0x24, 0x4d, + 0x10, 0xfc, 0x8b, 0x81, 0x70, 0x04, 0x29, 0x87, 0x50, 0x79, 0x75, 0xc6, 0x52, 0x76, 0xc9, 0xe2, + 0xd5, 0xa0, 0x37, 0xee, 0x4e, 0xfa, 0x87, 0x9f, 0x6c, 0xec, 0xd2, 0x7d, 0xbc, 0xa6, 0x3d, 0xa6, + 0x92, 0xaf, 0xfc, 0x61, 0xd9, 0x33, 0x5e, 0x27, 0x90, 0x5b, 0x12, 0x2a, 0x0f, 0x28, 0x8b, 0xe0, + 0x44, 0x79, 0xf0, 0x46, 0xe3, 0xc1, 0x49, 0x89, 0x91, 0xfa, 0x14, 0xbf, 0x83, 0xb6, 0x7f, 0x62, + 0x14, 0x06, 0x3b, 0x9a, 0x65, 0xe6, 0x99, 0xbd, 0xfd, 0x9c, 0x51, 0x20, 0x1a, 0xc5, 0x4f, 0x50, + 0x6f, 0x9a, 0x50, 0x29, 0x06, 0xa6, 0x76, 0xe7, 0xfd, 0x8d, 0x1d, 0x3c, 0x55, 0x6c, 0xdf, 0xca, + 0x33, 0xbb, 0xa7, 0x97, 0xa4, 0xd0, 0x0f, 0x8f, 0xd1, 0x83, 0x3b, 0x7a, 0xc3, 0xf7, 0x50, 0x77, + 0x06, 0xab, 0x81, 0xa1, 0x0a, 0x20, 0x6a, 0x89, 0xdf, 0x42, 0xbd, 0x65, 0x70, 0xb9, 0x00, 0xfd, + 0x3a, 0x2c, 0x52, 0x6c, 0x3e, 0xdd, 0x7a, 0x64, 0x38, 0xbf, 0x1a, 0x08, 0xaf, 0x3f, 0x09, 0x6c, + 0xa3, 0x1e, 0x87, 0x20, 0x2a, 0x82, 0x98, 0x45, 0x7a, 0xa2, 0x00, 0x52, 0xe0, 0xf8, 0x21, 0xda, + 0x11, 0xc0, 0x97, 0x09, 0x8d, 0x75, 0x4c, 0xd3, 0xef, 0xe7, 0x99, 0xbd, 0xf3, 0xac, 0x80, 0x48, + 0x75, 0x86, 0x0f, 0x50, 0x5f, 0x02, 0x9f, 0x27, 0x34, 0x90, 0x8a, 0xda, 0xd5, 0xd4, 0x37, 0xf3, + 0xcc, 0xee, 0x9f, 0x35, 0x30, 0x69, 0x73, 0x9c, 0x17, 0x68, 0xef, 0x46, 0xef, 0xf8, 0x04, 0x99, + 0x17, 0x8c, 0x2b, 0x0f, 0x8b, 0xbf, 0xd0, 0x3f, 0x1c, 0xdf, 0xe9, 0xda, 0x17, 0x05, 0xd1, 0xbf, + 0x57, 0x5e, 0xaf, 0x59, 0x02, 0x82, 0xd4, 0x31, 0x9c, 0x3f, 0x0c, 0xb4, 0x5b, 0x65, 0x38, 0x65, + 0x5c, 0xaa, 0x1b, 0xd3, 0x6f, 0xdb, 0x68, 0x6e, 0x4c, 0xdf, 0xa9, 0x46, 0xf1, 0x13, 0x64, 0xea, + 0x1f, 0x1a, 0xb2, 0xcb, 0xc2, 0x3e, 0x7f, 0x5f, 0x05, 0x3e, 0x2d, 0xb1, 0xd7, 0x99, 0xfd, 0xf6, + 0xfa, 0xf4, 0x71, 0xab, 0x63, 0x52, 0x8b, 0x55, 0x9a, 0x94, 0x71, 0xa9, 0x4d, 0xe8, 0x15, 0x69, + 0x54, 0x7a, 0xa2, 0x51, 0xe5, 0x54, 0x90, 0xa6, 0x95, 0x4c, 0x7f, 0x1e, 0xab, 0x70, 0xea, 0xa8, + 0x81, 0x49, 0x9b, 0xe3, 0xfc, 0xb9, 0xd5, 0x58, 0xf5, 0xec, 0x32, 0x09, 0x01, 0xff, 0x80, 0x4c, + 0x35, 0xc8, 0xa2, 0x40, 0x06, 0xba, 0x9b, 0xfe, 0xe1, 0x87, 0x2d, 0xab, 0xea, 0x79, 0xe4, 0xa6, + 0xb3, 0x58, 0x01, 0xc2, 0x55, 0xec, 0xe6, 0x43, 0x7e, 0x0b, 0x32, 0x68, 0xa6, 0x41, 0x83, 0x91, + 0x3a, 0x2a, 0x7e, 0x8c, 0xfa, 0xe5, 0xe4, 0x39, 0x5b, 0xa5, 0x50, 0x96, 0xe9, 0x94, 0x92, 0xfe, + 0x51, 0x73, 0xf4, 0xfa, 0xe6, 0x96, 0xb4, 0x65, 0x98, 0x20, 0x0b, 0xca, 0xc2, 0xd5, 0xc4, 0x52, + 0x77, 0xfa, 0xee, 0xc6, 0x9f, 0xe0, 0xdf, 0x2f, 0xd3, 0x58, 0x15, 0x22, 0x48, 0x13, 0x06, 0x7f, + 0x85, 0x7a, 0xca, 0x48, 0x31, 0xe8, 0xea, 0x78, 0x0f, 0x37, 0xc6, 0x53, 0xe6, 0xfb, 0x7b, 0x65, + 0xcc, 0x9e, 0xda, 0x09, 0x52, 0x84, 0x70, 0x7e, 0x37, 0xd0, 0xfd, 0x1b, 0xce, 0x7e, 0x93, 0x08, + 0x89, 0xbf, 0x5f, 0x73, 0xd7, 0xfd, 0x7f, 0xee, 0x2a, 0xb5, 0xf6, 0xb6, 0x7e, 0x96, 0x15, 0xd2, + 0x72, 0xf6, 0x6b, 0xd4, 0x4b, 0x24, 0xcc, 0x2b, 0x3f, 0x36, 0x4f, 0x06, 0x5d, 0x58, 0xd3, 0xc0, + 0x97, 0x4a, 0x4c, 0x8a, 0x18, 0xce, 0x3e, 0xda, 0x29, 0x5f, 0x3e, 0x1e, 0xdf, 0x78, 0xdd, 0xbb, + 0x25, 0xbd, 0xf5, 0xc2, 0xfd, 0xc9, 0xd5, 0xf5, 0xa8, 0xf3, 0xf2, 0x7a, 0xd4, 0x79, 0x75, 0x3d, + 0xea, 0xfc, 0x9c, 0x8f, 0x8c, 0xab, 0x7c, 0x64, 0xbc, 0xcc, 0x47, 0xc6, 0xab, 0x7c, 0x64, 0xfc, + 0x95, 0x8f, 0x8c, 0xdf, 0xfe, 0x1e, 0x75, 0x9e, 0x6f, 0x2d, 0x0f, 0xfe, 0x0d, 0x00, 0x00, 0xff, + 0xff, 0x66, 0x0f, 0x26, 0x7b, 0xf2, 0x07, 0x00, 0x00, } func (m *Endpoint) Marshal() (dAtA []byte, err error) { @@ -275,6 +337,18 @@ func (m *Endpoint) MarshalToSizedBuffer(dAtA []byte) (int, error) { _ = i var l int _ = l + if m.Hints != nil { + { + size, err := m.Hints.MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintGenerated(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x42 + } if m.Zone != nil { i -= len(*m.Zone) copy(dAtA[i:], *m.Zone) @@ -407,6 +481,43 @@ func (m *EndpointConditions) MarshalToSizedBuffer(dAtA []byte) (int, error) { return len(dAtA) - i, nil } +func (m *EndpointHints) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *EndpointHints) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *EndpointHints) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if len(m.ForZones) > 0 { + for iNdEx := len(m.ForZones) - 1; iNdEx >= 0; iNdEx-- { + { + size, err := m.ForZones[iNdEx].MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintGenerated(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0xa + } + } + return len(dAtA) - i, nil +} + func (m *EndpointPort) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) @@ -569,6 +680,34 @@ func (m *EndpointSliceList) MarshalToSizedBuffer(dAtA []byte) (int, error) { return len(dAtA) - i, nil } +func (m *ForZone) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *ForZone) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *ForZone) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + i -= len(m.Name) + copy(dAtA[i:], m.Name) + i = encodeVarintGenerated(dAtA, i, uint64(len(m.Name))) + i-- + dAtA[i] = 0xa + return len(dAtA) - i, nil +} + func encodeVarintGenerated(dAtA []byte, offset int, v uint64) int { offset -= sovGenerated(v) base := offset @@ -618,6 +757,10 @@ func (m *Endpoint) Size() (n int) { l = len(*m.Zone) n += 1 + l + sovGenerated(uint64(l)) } + if m.Hints != nil { + l = m.Hints.Size() + n += 1 + l + sovGenerated(uint64(l)) + } return n } @@ -639,6 +782,21 @@ func (m *EndpointConditions) Size() (n int) { return n } +func (m *EndpointHints) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if len(m.ForZones) > 0 { + for _, e := range m.ForZones { + l = e.Size() + n += 1 + l + sovGenerated(uint64(l)) + } + } + return n +} + func (m *EndpointPort) Size() (n int) { if m == nil { return 0 @@ -705,6 +863,17 @@ func (m *EndpointSliceList) Size() (n int) { return n } +func (m *ForZone) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + l = len(m.Name) + n += 1 + l + sovGenerated(uint64(l)) + return n +} + func sovGenerated(x uint64) (n int) { return (math_bits.Len64(x|1) + 6) / 7 } @@ -733,6 +902,7 @@ func (this *Endpoint) String() string { `DeprecatedTopology:` + mapStringForDeprecatedTopology + `,`, `NodeName:` + valueToStringGenerated(this.NodeName) + `,`, `Zone:` + valueToStringGenerated(this.Zone) + `,`, + `Hints:` + strings.Replace(this.Hints.String(), "EndpointHints", "EndpointHints", 1) + `,`, `}`, }, "") return s @@ -749,6 +919,21 @@ func (this *EndpointConditions) String() string { }, "") return s } +func (this *EndpointHints) String() string { + if this == nil { + return "nil" + } + repeatedStringForForZones := "[]ForZone{" + for _, f := range this.ForZones { + repeatedStringForForZones += strings.Replace(strings.Replace(f.String(), "ForZone", "ForZone", 1), `&`, ``, 1) + "," + } + repeatedStringForForZones += "}" + s := strings.Join([]string{`&EndpointHints{`, + `ForZones:` + repeatedStringForForZones + `,`, + `}`, + }, "") + return s +} func (this *EndpointPort) String() string { if this == nil { return "nil" @@ -801,6 +986,16 @@ func (this *EndpointSliceList) String() string { }, "") return s } +func (this *ForZone) String() string { + if this == nil { + return "nil" + } + s := strings.Join([]string{`&ForZone{`, + `Name:` + fmt.Sprintf("%v", this.Name) + `,`, + `}`, + }, "") + return s +} func valueToStringGenerated(v interface{}) string { rv := reflect.ValueOf(v) if rv.IsNil() { @@ -1165,6 +1360,42 @@ func (m *Endpoint) Unmarshal(dAtA []byte) error { s := string(dAtA[iNdEx:postIndex]) m.Zone = &s iNdEx = postIndex + case 8: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Hints", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenerated + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthGenerated + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthGenerated + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if m.Hints == nil { + m.Hints = &EndpointHints{} + } + if err := m.Hints.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipGenerated(dAtA[iNdEx:]) @@ -1299,6 +1530,90 @@ func (m *EndpointConditions) Unmarshal(dAtA []byte) error { } return nil } +func (m *EndpointHints) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenerated + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: EndpointHints: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: EndpointHints: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field ForZones", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenerated + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthGenerated + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthGenerated + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.ForZones = append(m.ForZones, ForZone{}) + if err := m.ForZones[len(m.ForZones)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipGenerated(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthGenerated + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} func (m *EndpointPort) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 @@ -1768,6 +2083,88 @@ func (m *EndpointSliceList) Unmarshal(dAtA []byte) error { } return nil } +func (m *ForZone) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenerated + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: ForZone: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: ForZone: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Name", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenerated + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthGenerated + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthGenerated + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Name = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipGenerated(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthGenerated + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} func skipGenerated(dAtA []byte) (n int, err error) { l := len(dAtA) iNdEx := 0 diff --git a/staging/src/k8s.io/api/discovery/v1/generated.proto b/staging/src/k8s.io/api/discovery/v1/generated.proto index 689f2a8127f..5844965d095 100644 --- a/staging/src/k8s.io/api/discovery/v1/generated.proto +++ b/staging/src/k8s.io/api/discovery/v1/generated.proto @@ -73,6 +73,11 @@ message Endpoint { // zone is the name of the Zone this endpoint exists in. // +optional optional string zone = 7; + + // hints contains information associated with how an endpoint should be + // consumed. + // +optional + optional EndpointHints hints = 8; } // EndpointConditions represents the current condition of an endpoint. @@ -101,6 +106,14 @@ message EndpointConditions { optional bool terminating = 3; } +// EndpointHints provides hints describing how an endpoint should be consumed. +message EndpointHints { + // forZones indicates the zone(s) this endpoint should be consumed by to + // enable topology aware routing. + // +listType=atomic + repeated ForZone forZones = 1; +} + // EndpointPort represents a Port used by an EndpointSlice message EndpointPort { // The name of this port. All ports in an EndpointSlice must have a unique @@ -175,3 +188,9 @@ message EndpointSliceList { repeated EndpointSlice items = 2; } +// ForZone provides information about which zones should consume this endpoint. +message ForZone { + // name represents the name of the zone. + optional string name = 1; +} + diff --git a/staging/src/k8s.io/api/discovery/v1/types.go b/staging/src/k8s.io/api/discovery/v1/types.go index a8b145f3f5f..fa990efdb73 100644 --- a/staging/src/k8s.io/api/discovery/v1/types.go +++ b/staging/src/k8s.io/api/discovery/v1/types.go @@ -106,6 +106,10 @@ type Endpoint struct { // zone is the name of the Zone this endpoint exists in. // +optional Zone *string `json:"zone,omitempty" protobuf:"bytes,7,opt,name=zone"` + // hints contains information associated with how an endpoint should be + // consumed. + // +optional + Hints *EndpointHints `json:"hints,omitempty" protobuf:"bytes,8,opt,name=hints"` } // EndpointConditions represents the current condition of an endpoint. @@ -134,6 +138,20 @@ type EndpointConditions struct { Terminating *bool `json:"terminating,omitempty" protobuf:"bytes,3,name=terminating"` } +// EndpointHints provides hints describing how an endpoint should be consumed. +type EndpointHints struct { + // forZones indicates the zone(s) this endpoint should be consumed by to + // enable topology aware routing. + // +listType=atomic + ForZones []ForZone `json:"forZones,omitempty" protobuf:"bytes,1,name=forZones"` +} + +// ForZone provides information about which zones should consume this endpoint. +type ForZone struct { + // name represents the name of the zone. + Name string `json:"name" protobuf:"bytes,1,name=name"` +} + // EndpointPort represents a Port used by an EndpointSlice type EndpointPort struct { // The name of this port. All ports in an EndpointSlice must have a unique diff --git a/staging/src/k8s.io/api/discovery/v1/types_swagger_doc_generated.go b/staging/src/k8s.io/api/discovery/v1/types_swagger_doc_generated.go index cc51e2f3ee1..b424a1cf046 100644 --- a/staging/src/k8s.io/api/discovery/v1/types_swagger_doc_generated.go +++ b/staging/src/k8s.io/api/discovery/v1/types_swagger_doc_generated.go @@ -36,6 +36,7 @@ var map_Endpoint = map[string]string{ "deprecatedTopology": "deprecatedTopology contains topology information part of the v1beta1 API. This field is deprecated, and will be removed when the v1beta1 API is removed (no sooner than kubernetes v1.24). While this field can hold values, it is not writable through the v1 API, and any attempts to write to it will be silently ignored. Topology information can be found in the zone and nodeName fields instead.", "nodeName": "nodeName represents the name of the Node hosting this endpoint. This can be used to determine endpoints local to a Node. This field can be enabled with the EndpointSliceNodeName feature gate.", "zone": "zone is the name of the Zone this endpoint exists in.", + "hints": "hints contains information associated with how an endpoint should be consumed.", } func (Endpoint) SwaggerDoc() map[string]string { @@ -53,6 +54,15 @@ func (EndpointConditions) SwaggerDoc() map[string]string { return map_EndpointConditions } +var map_EndpointHints = map[string]string{ + "": "EndpointHints provides hints describing how an endpoint should be consumed.", + "forZones": "forZones indicates the zone(s) this endpoint should be consumed by to enable topology aware routing.", +} + +func (EndpointHints) SwaggerDoc() map[string]string { + return map_EndpointHints +} + var map_EndpointPort = map[string]string{ "": "EndpointPort represents a Port used by an EndpointSlice", "name": "The name of this port. All ports in an EndpointSlice must have a unique name. If the EndpointSlice is dervied from a Kubernetes service, this corresponds to the Service.ports[].name. Name must either be an empty string or pass DNS_LABEL validation: * must be no more than 63 characters long. * must consist of lower case alphanumeric characters or '-'. * must start and end with an alphanumeric character. Default is empty string.", @@ -87,4 +97,13 @@ func (EndpointSliceList) SwaggerDoc() map[string]string { return map_EndpointSliceList } +var map_ForZone = map[string]string{ + "": "ForZone provides information about which zones should consume this endpoint.", + "name": "name represents the name of the zone.", +} + +func (ForZone) SwaggerDoc() map[string]string { + return map_ForZone +} + // AUTO-GENERATED FUNCTIONS END HERE diff --git a/staging/src/k8s.io/api/discovery/v1/zz_generated.deepcopy.go b/staging/src/k8s.io/api/discovery/v1/zz_generated.deepcopy.go index a052e903999..31a912386f1 100644 --- a/staging/src/k8s.io/api/discovery/v1/zz_generated.deepcopy.go +++ b/staging/src/k8s.io/api/discovery/v1/zz_generated.deepcopy.go @@ -61,6 +61,11 @@ func (in *Endpoint) DeepCopyInto(out *Endpoint) { *out = new(string) **out = **in } + if in.Hints != nil { + in, out := &in.Hints, &out.Hints + *out = new(EndpointHints) + (*in).DeepCopyInto(*out) + } return } @@ -105,6 +110,27 @@ func (in *EndpointConditions) DeepCopy() *EndpointConditions { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *EndpointHints) DeepCopyInto(out *EndpointHints) { + *out = *in + if in.ForZones != nil { + in, out := &in.ForZones, &out.ForZones + *out = make([]ForZone, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EndpointHints. +func (in *EndpointHints) DeepCopy() *EndpointHints { + if in == nil { + return nil + } + out := new(EndpointHints) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *EndpointPort) DeepCopyInto(out *EndpointPort) { *out = *in @@ -213,3 +239,19 @@ func (in *EndpointSliceList) DeepCopyObject() runtime.Object { } return nil } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ForZone) DeepCopyInto(out *ForZone) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ForZone. +func (in *ForZone) DeepCopy() *ForZone { + if in == nil { + return nil + } + out := new(ForZone) + in.DeepCopyInto(out) + return out +} diff --git a/staging/src/k8s.io/api/discovery/v1beta1/generated.pb.go b/staging/src/k8s.io/api/discovery/v1beta1/generated.pb.go index 1fdb8ddb77d..e024cc0a16d 100644 --- a/staging/src/k8s.io/api/discovery/v1beta1/generated.pb.go +++ b/staging/src/k8s.io/api/discovery/v1beta1/generated.pb.go @@ -102,10 +102,38 @@ func (m *EndpointConditions) XXX_DiscardUnknown() { var xxx_messageInfo_EndpointConditions proto.InternalMessageInfo +func (m *EndpointHints) Reset() { *m = EndpointHints{} } +func (*EndpointHints) ProtoMessage() {} +func (*EndpointHints) Descriptor() ([]byte, []int) { + return fileDescriptor_ece80bbc872d519b, []int{2} +} +func (m *EndpointHints) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *EndpointHints) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil +} +func (m *EndpointHints) XXX_Merge(src proto.Message) { + xxx_messageInfo_EndpointHints.Merge(m, src) +} +func (m *EndpointHints) XXX_Size() int { + return m.Size() +} +func (m *EndpointHints) XXX_DiscardUnknown() { + xxx_messageInfo_EndpointHints.DiscardUnknown(m) +} + +var xxx_messageInfo_EndpointHints proto.InternalMessageInfo + func (m *EndpointPort) Reset() { *m = EndpointPort{} } func (*EndpointPort) ProtoMessage() {} func (*EndpointPort) Descriptor() ([]byte, []int) { - return fileDescriptor_ece80bbc872d519b, []int{2} + return fileDescriptor_ece80bbc872d519b, []int{3} } func (m *EndpointPort) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -133,7 +161,7 @@ var xxx_messageInfo_EndpointPort proto.InternalMessageInfo func (m *EndpointSlice) Reset() { *m = EndpointSlice{} } func (*EndpointSlice) ProtoMessage() {} func (*EndpointSlice) Descriptor() ([]byte, []int) { - return fileDescriptor_ece80bbc872d519b, []int{3} + return fileDescriptor_ece80bbc872d519b, []int{4} } func (m *EndpointSlice) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -161,7 +189,7 @@ var xxx_messageInfo_EndpointSlice proto.InternalMessageInfo func (m *EndpointSliceList) Reset() { *m = EndpointSliceList{} } func (*EndpointSliceList) ProtoMessage() {} func (*EndpointSliceList) Descriptor() ([]byte, []int) { - return fileDescriptor_ece80bbc872d519b, []int{4} + return fileDescriptor_ece80bbc872d519b, []int{5} } func (m *EndpointSliceList) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -186,13 +214,43 @@ func (m *EndpointSliceList) XXX_DiscardUnknown() { var xxx_messageInfo_EndpointSliceList proto.InternalMessageInfo +func (m *ForZone) Reset() { *m = ForZone{} } +func (*ForZone) ProtoMessage() {} +func (*ForZone) Descriptor() ([]byte, []int) { + return fileDescriptor_ece80bbc872d519b, []int{6} +} +func (m *ForZone) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *ForZone) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil +} +func (m *ForZone) XXX_Merge(src proto.Message) { + xxx_messageInfo_ForZone.Merge(m, src) +} +func (m *ForZone) XXX_Size() int { + return m.Size() +} +func (m *ForZone) XXX_DiscardUnknown() { + xxx_messageInfo_ForZone.DiscardUnknown(m) +} + +var xxx_messageInfo_ForZone proto.InternalMessageInfo + func init() { proto.RegisterType((*Endpoint)(nil), "k8s.io.api.discovery.v1beta1.Endpoint") proto.RegisterMapType((map[string]string)(nil), "k8s.io.api.discovery.v1beta1.Endpoint.TopologyEntry") proto.RegisterType((*EndpointConditions)(nil), "k8s.io.api.discovery.v1beta1.EndpointConditions") + proto.RegisterType((*EndpointHints)(nil), "k8s.io.api.discovery.v1beta1.EndpointHints") proto.RegisterType((*EndpointPort)(nil), "k8s.io.api.discovery.v1beta1.EndpointPort") proto.RegisterType((*EndpointSlice)(nil), "k8s.io.api.discovery.v1beta1.EndpointSlice") proto.RegisterType((*EndpointSliceList)(nil), "k8s.io.api.discovery.v1beta1.EndpointSliceList") + proto.RegisterType((*ForZone)(nil), "k8s.io.api.discovery.v1beta1.ForZone") } func init() { @@ -200,57 +258,62 @@ func init() { } var fileDescriptor_ece80bbc872d519b = []byte{ - // 798 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x8c, 0x53, 0x4d, 0x8f, 0xe3, 0x44, - 0x10, 0x8d, 0x27, 0x63, 0xc6, 0xee, 0xec, 0x88, 0xdd, 0x16, 0x87, 0x68, 0x58, 0xd9, 0xa3, 0x20, - 0x50, 0xc4, 0x68, 0x6d, 0x66, 0xb5, 0x42, 0x2b, 0x38, 0x8d, 0x61, 0x04, 0x48, 0xb0, 0x1b, 0xf5, - 0x46, 0x42, 0x42, 0x1c, 0xe8, 0xd8, 0xb5, 0x8e, 0x49, 0xec, 0xb6, 0xba, 0x3b, 0x91, 0x72, 0xe3, - 0x1f, 0xc0, 0x7f, 0x42, 0x42, 0x73, 0xdc, 0xe3, 0x9e, 0x2c, 0x62, 0xf8, 0x15, 0x7b, 0x42, 0xdd, - 0xfe, 0x4a, 0x08, 0x1f, 0xb9, 0x75, 0xbf, 0xaa, 0xf7, 0xaa, 0x5e, 0x75, 0x17, 0xba, 0x5d, 0x3c, - 0x15, 0x5e, 0xc2, 0xfc, 0xc5, 0x6a, 0x06, 0x3c, 0x03, 0x09, 0xc2, 0x5f, 0x43, 0x16, 0x31, 0xee, - 0xd7, 0x01, 0x9a, 0x27, 0x7e, 0x94, 0x88, 0x90, 0xad, 0x81, 0x6f, 0xfc, 0xf5, 0xf5, 0x0c, 0x24, - 0xbd, 0xf6, 0x63, 0xc8, 0x80, 0x53, 0x09, 0x91, 0x97, 0x73, 0x26, 0x19, 0x7e, 0x58, 0x65, 0x7b, - 0x34, 0x4f, 0xbc, 0x36, 0xdb, 0xab, 0xb3, 0x2f, 0x1e, 0xc5, 0x89, 0x9c, 0xaf, 0x66, 0x5e, 0xc8, - 0x52, 0x3f, 0x66, 0x31, 0xf3, 0x35, 0x69, 0xb6, 0x7a, 0xa9, 0x6f, 0xfa, 0xa2, 0x4f, 0x95, 0xd8, - 0xc5, 0x68, 0xa7, 0x74, 0xc8, 0x38, 0xf8, 0xeb, 0x83, 0x82, 0x17, 0x4f, 0xba, 0x9c, 0x94, 0x86, - 0xf3, 0x24, 0x53, 0xdd, 0xe5, 0x8b, 0x58, 0x01, 0xc2, 0x4f, 0x41, 0xd2, 0x7f, 0x62, 0xf9, 0xff, - 0xc6, 0xe2, 0xab, 0x4c, 0x26, 0x29, 0x1c, 0x10, 0x3e, 0xfe, 0x3f, 0x82, 0x08, 0xe7, 0x90, 0xd2, - 0xbf, 0xf3, 0x46, 0x7f, 0xf6, 0x91, 0x75, 0x9b, 0x45, 0x39, 0x4b, 0x32, 0x89, 0xaf, 0x90, 0x4d, - 0xa3, 0x88, 0x83, 0x10, 0x20, 0x86, 0xc6, 0x65, 0x7f, 0x6c, 0x07, 0xe7, 0x65, 0xe1, 0xda, 0x37, - 0x0d, 0x48, 0xba, 0x38, 0x8e, 0x10, 0x0a, 0x59, 0x16, 0x25, 0x32, 0x61, 0x99, 0x18, 0x9e, 0x5c, - 0x1a, 0xe3, 0xc1, 0xe3, 0x8f, 0xbc, 0xff, 0x1a, 0xaf, 0xd7, 0x14, 0xfa, 0xac, 0xe5, 0x05, 0xf8, - 0xae, 0x70, 0x7b, 0x65, 0xe1, 0xa2, 0x0e, 0x23, 0x3b, 0xba, 0x78, 0x8c, 0xac, 0x39, 0x13, 0x32, - 0xa3, 0x29, 0x0c, 0xfb, 0x97, 0xc6, 0xd8, 0x0e, 0xee, 0x95, 0x85, 0x6b, 0x7d, 0x59, 0x63, 0xa4, - 0x8d, 0xe2, 0x09, 0xb2, 0x25, 0xe5, 0x31, 0x48, 0x02, 0x2f, 0x87, 0xa7, 0xba, 0x9d, 0xf7, 0x76, - 0xdb, 0x51, 0x0f, 0xe4, 0xad, 0xaf, 0xbd, 0xe7, 0xb3, 0x1f, 0x21, 0x54, 0x49, 0xc0, 0x21, 0x0b, - 0xa1, 0x72, 0x38, 0x6d, 0x98, 0xa4, 0x13, 0xc1, 0x33, 0x64, 0x49, 0x96, 0xb3, 0x25, 0x8b, 0x37, - 0x43, 0xf3, 0xb2, 0x3f, 0x1e, 0x3c, 0x7e, 0x72, 0x9c, 0x3f, 0x6f, 0x5a, 0xd3, 0x6e, 0x33, 0xc9, - 0x37, 0xc1, 0xfd, 0xda, 0xa3, 0xd5, 0xc0, 0xa4, 0xd5, 0x55, 0xfe, 0x32, 0x16, 0xc1, 0x33, 0xe5, - 0xef, 0xad, 0xce, 0xdf, 0xb3, 0x1a, 0x23, 0x6d, 0xf4, 0xe2, 0x53, 0x74, 0xbe, 0x27, 0x8b, 0xef, - 0xa3, 0xfe, 0x02, 0x36, 0x43, 0x43, 0xb1, 0x88, 0x3a, 0xe2, 0x77, 0x90, 0xb9, 0xa6, 0xcb, 0x15, - 0xe8, 0xd7, 0xb0, 0x49, 0x75, 0xf9, 0xe4, 0xe4, 0xa9, 0x31, 0xfa, 0xd9, 0x40, 0xf8, 0x70, 0xfa, - 0xd8, 0x45, 0x26, 0x07, 0x1a, 0x55, 0x22, 0x56, 0x60, 0x97, 0x85, 0x6b, 0x12, 0x05, 0x90, 0x0a, - 0xc7, 0xef, 0xa3, 0x33, 0x01, 0x7c, 0x9d, 0x64, 0xb1, 0xd6, 0xb4, 0x82, 0x41, 0x59, 0xb8, 0x67, - 0x2f, 0x2a, 0x88, 0x34, 0x31, 0x7c, 0x8d, 0x06, 0x12, 0x78, 0x9a, 0x64, 0x54, 0xaa, 0xd4, 0xbe, - 0x4e, 0x7d, 0xbb, 0x2c, 0xdc, 0xc1, 0xb4, 0x83, 0xc9, 0x6e, 0xce, 0xe8, 0x37, 0x03, 0xdd, 0x6b, - 0x3a, 0x9a, 0x30, 0x2e, 0xf1, 0x43, 0x74, 0xaa, 0x5f, 0x59, 0xfb, 0x09, 0xac, 0xb2, 0x70, 0x4f, - 0xf5, 0x04, 0x34, 0x8a, 0xbf, 0x40, 0x96, 0xfe, 0xb0, 0x21, 0x5b, 0x56, 0xee, 0x82, 0x2b, 0x35, - 0xa7, 0x49, 0x8d, 0xbd, 0x29, 0xdc, 0x77, 0x0f, 0x97, 0xd1, 0x6b, 0xc2, 0xa4, 0x25, 0xab, 0x32, - 0x39, 0xe3, 0x52, 0xf7, 0x68, 0x56, 0x65, 0x54, 0x79, 0xa2, 0x51, 0x65, 0x84, 0xe6, 0x79, 0x43, - 0xd3, 0xdf, 0xc8, 0xae, 0x8c, 0xdc, 0x74, 0x30, 0xd9, 0xcd, 0x19, 0x6d, 0x4f, 0xd0, 0x79, 0x63, - 0xe4, 0xc5, 0x32, 0x09, 0x01, 0xff, 0x80, 0x2c, 0xb5, 0xd7, 0x11, 0x95, 0x54, 0xbb, 0xd9, 0xdf, - 0x8b, 0x76, 0x3d, 0xbd, 0x7c, 0x11, 0x2b, 0x40, 0x78, 0x2a, 0xbb, 0xfb, 0x9a, 0xdf, 0x80, 0xa4, - 0xdd, 0x5e, 0x74, 0x18, 0x69, 0x55, 0xf1, 0xe7, 0x68, 0x50, 0x2f, 0xe2, 0x74, 0x93, 0x43, 0xdd, - 0xe6, 0xa8, 0xa6, 0x0c, 0x6e, 0xba, 0xd0, 0x9b, 0xfd, 0x2b, 0xd9, 0xa5, 0xe1, 0x6f, 0x91, 0x0d, - 0x75, 0xe3, 0x6a, 0x81, 0xd5, 0x07, 0xff, 0xe0, 0xb8, 0x0f, 0x1e, 0x3c, 0xa8, 0x6b, 0xd9, 0x0d, - 0x22, 0x48, 0xa7, 0x85, 0x9f, 0x23, 0x53, 0x4d, 0x53, 0x0c, 0xfb, 0x5a, 0xf4, 0xc3, 0xe3, 0x44, - 0xd5, 0x33, 0x04, 0xe7, 0xb5, 0xb0, 0xa9, 0x6e, 0x82, 0x54, 0x3a, 0xa3, 0x5f, 0x0d, 0xf4, 0x60, - 0x6f, 0xc6, 0x5f, 0x27, 0x42, 0xe2, 0xef, 0x0f, 0xe6, 0xec, 0x1d, 0x37, 0x67, 0xc5, 0xd6, 0x53, - 0x6e, 0x37, 0xb3, 0x41, 0x76, 0x66, 0x3c, 0x41, 0x66, 0x22, 0x21, 0x6d, 0x26, 0x73, 0x75, 0x9c, - 0x09, 0xdd, 0x5d, 0xe7, 0xe2, 0x2b, 0xa5, 0x40, 0x2a, 0xa1, 0xe0, 0xd1, 0xdd, 0xd6, 0xe9, 0xbd, - 0xda, 0x3a, 0xbd, 0xd7, 0x5b, 0xa7, 0xf7, 0x53, 0xe9, 0x18, 0x77, 0xa5, 0x63, 0xbc, 0x2a, 0x1d, - 0xe3, 0x75, 0xe9, 0x18, 0xbf, 0x97, 0x8e, 0xf1, 0xcb, 0x1f, 0x4e, 0xef, 0xbb, 0xb3, 0x5a, 0xf2, - 0xaf, 0x00, 0x00, 0x00, 0xff, 0xff, 0xa6, 0x35, 0xe6, 0xf5, 0xf2, 0x06, 0x00, 0x00, + // 870 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x8c, 0x54, 0x41, 0x8f, 0xe3, 0x34, + 0x14, 0x6e, 0xa6, 0x53, 0x9a, 0xb8, 0x33, 0x62, 0xd7, 0xe2, 0x50, 0x0d, 0xab, 0xa4, 0x0a, 0x5a, + 0x54, 0x31, 0xda, 0x84, 0x19, 0xad, 0xd0, 0x0a, 0x4e, 0x13, 0x18, 0x58, 0xa4, 0x65, 0x77, 0xe4, + 0x19, 0x09, 0x69, 0xc5, 0x01, 0x37, 0xf1, 0xa4, 0xa1, 0x53, 0x3b, 0xb2, 0xdd, 0x4a, 0xbd, 0xf1, + 0x0f, 0xe0, 0xb7, 0xf0, 0x17, 0x90, 0xd0, 0x1c, 0xf7, 0xb8, 0xa7, 0x88, 0x09, 0xff, 0x62, 0x4f, + 0xc8, 0x8e, 0x93, 0xb4, 0x14, 0x86, 0xde, 0xec, 0xcf, 0xef, 0xfb, 0xde, 0x7b, 0xdf, 0xb3, 0x0d, + 0xce, 0x67, 0xcf, 0x44, 0x90, 0xb1, 0x70, 0xb6, 0x98, 0x10, 0x4e, 0x89, 0x24, 0x22, 0x5c, 0x12, + 0x9a, 0x30, 0x1e, 0x9a, 0x03, 0x9c, 0x67, 0x61, 0x92, 0x89, 0x98, 0x2d, 0x09, 0x5f, 0x85, 0xcb, + 0x93, 0x09, 0x91, 0xf8, 0x24, 0x4c, 0x09, 0x25, 0x1c, 0x4b, 0x92, 0x04, 0x39, 0x67, 0x92, 0xc1, + 0x47, 0x55, 0x74, 0x80, 0xf3, 0x2c, 0x68, 0xa2, 0x03, 0x13, 0x7d, 0xf4, 0x24, 0xcd, 0xe4, 0x74, + 0x31, 0x09, 0x62, 0x36, 0x0f, 0x53, 0x96, 0xb2, 0x50, 0x93, 0x26, 0x8b, 0x6b, 0xbd, 0xd3, 0x1b, + 0xbd, 0xaa, 0xc4, 0x8e, 0xfc, 0xb5, 0xd4, 0x31, 0xe3, 0x24, 0x5c, 0x6e, 0x25, 0x3c, 0x7a, 0xda, + 0xc6, 0xcc, 0x71, 0x3c, 0xcd, 0xa8, 0xaa, 0x2e, 0x9f, 0xa5, 0x0a, 0x10, 0xe1, 0x9c, 0x48, 0xfc, + 0x6f, 0xac, 0xf0, 0xbf, 0x58, 0x7c, 0x41, 0x65, 0x36, 0x27, 0x5b, 0x84, 0xcf, 0xfe, 0x8f, 0x20, + 0xe2, 0x29, 0x99, 0xe3, 0x7f, 0xf2, 0xfc, 0xdf, 0xf6, 0x81, 0x7d, 0x4e, 0x93, 0x9c, 0x65, 0x54, + 0xc2, 0x63, 0xe0, 0xe0, 0x24, 0xe1, 0x44, 0x08, 0x22, 0x86, 0xd6, 0xa8, 0x3b, 0x76, 0xa2, 0xc3, + 0xb2, 0xf0, 0x9c, 0xb3, 0x1a, 0x44, 0xed, 0x39, 0x4c, 0x00, 0x88, 0x19, 0x4d, 0x32, 0x99, 0x31, + 0x2a, 0x86, 0x7b, 0x23, 0x6b, 0x3c, 0x38, 0xfd, 0x34, 0xb8, 0xcf, 0xde, 0xa0, 0x4e, 0xf4, 0x65, + 0xc3, 0x8b, 0xe0, 0x6d, 0xe1, 0x75, 0xca, 0xc2, 0x03, 0x2d, 0x86, 0xd6, 0x74, 0xe1, 0x18, 0xd8, + 0x53, 0x26, 0x24, 0xc5, 0x73, 0x32, 0xec, 0x8e, 0xac, 0xb1, 0x13, 0x1d, 0x94, 0x85, 0x67, 0x3f, + 0x37, 0x18, 0x6a, 0x4e, 0xe1, 0x05, 0x70, 0x24, 0xe6, 0x29, 0x91, 0x88, 0x5c, 0x0f, 0xf7, 0x75, + 0x39, 0x1f, 0xad, 0x97, 0xa3, 0x06, 0x14, 0x2c, 0x4f, 0x82, 0x57, 0x93, 0x9f, 0x48, 0xac, 0x82, + 0x08, 0x27, 0x34, 0x26, 0x55, 0x87, 0x57, 0x35, 0x13, 0xb5, 0x22, 0x70, 0x02, 0x6c, 0xc9, 0x72, + 0x76, 0xc3, 0xd2, 0xd5, 0xb0, 0x37, 0xea, 0x8e, 0x07, 0xa7, 0x4f, 0x77, 0xeb, 0x2f, 0xb8, 0x32, + 0xb4, 0x73, 0x2a, 0xf9, 0x2a, 0x7a, 0x60, 0x7a, 0xb4, 0x6b, 0x18, 0x35, 0xba, 0xaa, 0x3f, 0xca, + 0x12, 0xf2, 0x52, 0xf5, 0xf7, 0x5e, 0xdb, 0xdf, 0x4b, 0x83, 0xa1, 0xe6, 0x14, 0xbe, 0x00, 0xbd, + 0x69, 0x46, 0xa5, 0x18, 0xf6, 0x75, 0x6f, 0xc7, 0xbb, 0x95, 0xf2, 0x5c, 0x51, 0x22, 0xa7, 0x2c, + 0xbc, 0x9e, 0x5e, 0xa2, 0x4a, 0xe4, 0xe8, 0x0b, 0x70, 0xb8, 0x51, 0x24, 0x7c, 0x00, 0xba, 0x33, + 0xb2, 0x1a, 0x5a, 0xaa, 0x06, 0xa4, 0x96, 0xf0, 0x03, 0xd0, 0x5b, 0xe2, 0x9b, 0x05, 0xd1, 0xb3, + 0x75, 0x50, 0xb5, 0xf9, 0x7c, 0xef, 0x99, 0xe5, 0xff, 0x62, 0x01, 0xb8, 0x3d, 0x4b, 0xe8, 0x81, + 0x1e, 0x27, 0x38, 0xa9, 0x44, 0xec, 0x2a, 0x29, 0x52, 0x00, 0xaa, 0x70, 0xf8, 0x18, 0xf4, 0x05, + 0xe1, 0xcb, 0x8c, 0xa6, 0x5a, 0xd3, 0x8e, 0x06, 0x65, 0xe1, 0xf5, 0x2f, 0x2b, 0x08, 0xd5, 0x67, + 0xf0, 0x04, 0x0c, 0x24, 0xe1, 0xf3, 0x8c, 0x62, 0xa9, 0x42, 0xbb, 0x3a, 0xf4, 0xfd, 0xb2, 0xf0, + 0x06, 0x57, 0x2d, 0x8c, 0xd6, 0x63, 0xfc, 0x04, 0x1c, 0x6e, 0x74, 0x0c, 0x2f, 0x81, 0x7d, 0xcd, + 0xf8, 0x6b, 0x46, 0xcd, 0x4d, 0x1e, 0x9c, 0x3e, 0xbe, 0xdf, 0xb0, 0xaf, 0xab, 0xe8, 0x76, 0x58, + 0x06, 0x10, 0xa8, 0x11, 0xf2, 0xff, 0xb0, 0xc0, 0x41, 0x9d, 0xe6, 0x82, 0x71, 0x09, 0x1f, 0x81, + 0x7d, 0x7d, 0x33, 0xb5, 0x6b, 0x91, 0x5d, 0x16, 0xde, 0xbe, 0x9e, 0x9a, 0x46, 0xe1, 0x37, 0xc0, + 0xd6, 0x8f, 0x2c, 0x66, 0x37, 0x95, 0x87, 0xd1, 0xb1, 0x12, 0xbe, 0x30, 0xd8, 0xbb, 0xc2, 0xfb, + 0x70, 0xfb, 0x03, 0x09, 0xea, 0x63, 0xd4, 0x90, 0x55, 0x9a, 0x9c, 0x71, 0xa9, 0x9d, 0xe8, 0x55, + 0x69, 0x54, 0x7a, 0xa4, 0x51, 0x65, 0x17, 0xce, 0xf3, 0x9a, 0xa6, 0xaf, 0xbe, 0x53, 0xd9, 0x75, + 0xd6, 0xc2, 0x68, 0x3d, 0xc6, 0xbf, 0xdb, 0x6b, 0xfd, 0xba, 0xbc, 0xc9, 0x62, 0x02, 0x7f, 0x04, + 0xb6, 0xfa, 0x8b, 0x12, 0x2c, 0xb1, 0xee, 0x66, 0xf3, 0x2d, 0x37, 0x5f, 0x4a, 0x90, 0xcf, 0x52, + 0x05, 0x88, 0x40, 0x45, 0xb7, 0xcf, 0xe9, 0x3b, 0x22, 0x71, 0xfb, 0x96, 0x5b, 0x0c, 0x35, 0xaa, + 0xf0, 0x2b, 0x30, 0x30, 0x9f, 0xc7, 0xd5, 0x2a, 0x27, 0xa6, 0x4c, 0xdf, 0x50, 0x06, 0x67, 0xed, + 0xd1, 0xbb, 0xcd, 0x2d, 0x5a, 0xa7, 0xc1, 0xef, 0x81, 0x43, 0x4c, 0xe1, 0xea, 0xd3, 0x51, 0x83, + 0xfd, 0x78, 0xb7, 0x97, 0x10, 0x3d, 0x34, 0xb9, 0x9c, 0x1a, 0x11, 0xa8, 0xd5, 0x82, 0xaf, 0x40, + 0x4f, 0xb9, 0x29, 0x86, 0x5d, 0x2d, 0xfa, 0xc9, 0x6e, 0xa2, 0x6a, 0x0c, 0xd1, 0xa1, 0x11, 0xee, + 0xa9, 0x9d, 0x40, 0x95, 0x8e, 0xff, 0xbb, 0x05, 0x1e, 0x6e, 0x78, 0xfc, 0x22, 0x13, 0x12, 0xfe, + 0xb0, 0xe5, 0x73, 0xb0, 0x9b, 0xcf, 0x8a, 0xad, 0x5d, 0x6e, 0x2e, 0x68, 0x8d, 0xac, 0x79, 0x7c, + 0x01, 0x7a, 0x99, 0x24, 0xf3, 0xda, 0x99, 0x1d, 0xff, 0x08, 0x5d, 0x5d, 0xdb, 0xc5, 0xb7, 0x4a, + 0x01, 0x55, 0x42, 0xfe, 0x31, 0xe8, 0x9b, 0x87, 0x00, 0x47, 0x1b, 0x97, 0xfd, 0xc0, 0x84, 0xaf, + 0x5d, 0xf8, 0xe8, 0xc9, 0xed, 0x9d, 0xdb, 0x79, 0x73, 0xe7, 0x76, 0xde, 0xde, 0xb9, 0x9d, 0x9f, + 0x4b, 0xd7, 0xba, 0x2d, 0x5d, 0xeb, 0x4d, 0xe9, 0x5a, 0x6f, 0x4b, 0xd7, 0xfa, 0xb3, 0x74, 0xad, + 0x5f, 0xff, 0x72, 0x3b, 0xaf, 0xfb, 0x26, 0xff, 0xdf, 0x01, 0x00, 0x00, 0xff, 0xff, 0xf7, 0x0d, + 0x6f, 0x98, 0xd3, 0x07, 0x00, 0x00, } func (m *Endpoint) Marshal() (dAtA []byte, err error) { @@ -273,6 +336,18 @@ func (m *Endpoint) MarshalToSizedBuffer(dAtA []byte) (int, error) { _ = i var l int _ = l + if m.Hints != nil { + { + size, err := m.Hints.MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintGenerated(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x3a + } if m.NodeName != nil { i -= len(*m.NodeName) copy(dAtA[i:], *m.NodeName) @@ -398,6 +473,43 @@ func (m *EndpointConditions) MarshalToSizedBuffer(dAtA []byte) (int, error) { return len(dAtA) - i, nil } +func (m *EndpointHints) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *EndpointHints) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *EndpointHints) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if len(m.ForZones) > 0 { + for iNdEx := len(m.ForZones) - 1; iNdEx >= 0; iNdEx-- { + { + size, err := m.ForZones[iNdEx].MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintGenerated(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0xa + } + } + return len(dAtA) - i, nil +} + func (m *EndpointPort) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) @@ -560,6 +672,34 @@ func (m *EndpointSliceList) MarshalToSizedBuffer(dAtA []byte) (int, error) { return len(dAtA) - i, nil } +func (m *ForZone) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *ForZone) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *ForZone) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + i -= len(m.Name) + copy(dAtA[i:], m.Name) + i = encodeVarintGenerated(dAtA, i, uint64(len(m.Name))) + i-- + dAtA[i] = 0xa + return len(dAtA) - i, nil +} + func encodeVarintGenerated(dAtA []byte, offset int, v uint64) int { offset -= sovGenerated(v) base := offset @@ -605,6 +745,10 @@ func (m *Endpoint) Size() (n int) { l = len(*m.NodeName) n += 1 + l + sovGenerated(uint64(l)) } + if m.Hints != nil { + l = m.Hints.Size() + n += 1 + l + sovGenerated(uint64(l)) + } return n } @@ -626,6 +770,21 @@ func (m *EndpointConditions) Size() (n int) { return n } +func (m *EndpointHints) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if len(m.ForZones) > 0 { + for _, e := range m.ForZones { + l = e.Size() + n += 1 + l + sovGenerated(uint64(l)) + } + } + return n +} + func (m *EndpointPort) Size() (n int) { if m == nil { return 0 @@ -692,6 +851,17 @@ func (m *EndpointSliceList) Size() (n int) { return n } +func (m *ForZone) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + l = len(m.Name) + n += 1 + l + sovGenerated(uint64(l)) + return n +} + func sovGenerated(x uint64) (n int) { return (math_bits.Len64(x|1) + 6) / 7 } @@ -719,6 +889,7 @@ func (this *Endpoint) String() string { `TargetRef:` + strings.Replace(fmt.Sprintf("%v", this.TargetRef), "ObjectReference", "v1.ObjectReference", 1) + `,`, `Topology:` + mapStringForTopology + `,`, `NodeName:` + valueToStringGenerated(this.NodeName) + `,`, + `Hints:` + strings.Replace(this.Hints.String(), "EndpointHints", "EndpointHints", 1) + `,`, `}`, }, "") return s @@ -735,6 +906,21 @@ func (this *EndpointConditions) String() string { }, "") return s } +func (this *EndpointHints) String() string { + if this == nil { + return "nil" + } + repeatedStringForForZones := "[]ForZone{" + for _, f := range this.ForZones { + repeatedStringForForZones += strings.Replace(strings.Replace(f.String(), "ForZone", "ForZone", 1), `&`, ``, 1) + "," + } + repeatedStringForForZones += "}" + s := strings.Join([]string{`&EndpointHints{`, + `ForZones:` + repeatedStringForForZones + `,`, + `}`, + }, "") + return s +} func (this *EndpointPort) String() string { if this == nil { return "nil" @@ -787,6 +973,16 @@ func (this *EndpointSliceList) String() string { }, "") return s } +func (this *ForZone) String() string { + if this == nil { + return "nil" + } + s := strings.Join([]string{`&ForZone{`, + `Name:` + fmt.Sprintf("%v", this.Name) + `,`, + `}`, + }, "") + return s +} func valueToStringGenerated(v interface{}) string { rv := reflect.ValueOf(v) if rv.IsNil() { @@ -1118,6 +1314,42 @@ func (m *Endpoint) Unmarshal(dAtA []byte) error { s := string(dAtA[iNdEx:postIndex]) m.NodeName = &s iNdEx = postIndex + case 7: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Hints", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenerated + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthGenerated + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthGenerated + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if m.Hints == nil { + m.Hints = &EndpointHints{} + } + if err := m.Hints.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipGenerated(dAtA[iNdEx:]) @@ -1252,6 +1484,90 @@ func (m *EndpointConditions) Unmarshal(dAtA []byte) error { } return nil } +func (m *EndpointHints) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenerated + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: EndpointHints: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: EndpointHints: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field ForZones", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenerated + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthGenerated + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthGenerated + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.ForZones = append(m.ForZones, ForZone{}) + if err := m.ForZones[len(m.ForZones)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipGenerated(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthGenerated + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} func (m *EndpointPort) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 @@ -1721,6 +2037,88 @@ func (m *EndpointSliceList) Unmarshal(dAtA []byte) error { } return nil } +func (m *ForZone) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenerated + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: ForZone: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: ForZone: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Name", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenerated + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthGenerated + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthGenerated + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Name = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipGenerated(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthGenerated + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} func skipGenerated(dAtA []byte) (n int, err error) { l := len(dAtA) iNdEx := 0 diff --git a/staging/src/k8s.io/api/discovery/v1beta1/generated.proto b/staging/src/k8s.io/api/discovery/v1beta1/generated.proto index e5d21caadf2..6925f7ce3b0 100644 --- a/staging/src/k8s.io/api/discovery/v1beta1/generated.proto +++ b/staging/src/k8s.io/api/discovery/v1beta1/generated.proto @@ -76,6 +76,12 @@ message Endpoint { // with the EndpointSliceNodeName feature gate. // +optional optional string nodeName = 6; + + // hints contains information associated with how an endpoint should be + // consumed. + // +featureGate=TopologyAwareHints + // +optional + optional EndpointHints hints = 7; } // EndpointConditions represents the current condition of an endpoint. @@ -104,6 +110,14 @@ message EndpointConditions { optional bool terminating = 3; } +// EndpointHints provides hints describing how an endpoint should be consumed. +message EndpointHints { + // forZones indicates the zone(s) this endpoint should be consumed by to + // enable topology aware routing. May contain a maximum of 8 entries. + // +listType=atomic + repeated ForZone forZones = 1; +} + // EndpointPort represents a Port used by an EndpointSlice message EndpointPort { // The name of this port. All ports in an EndpointSlice must have a unique @@ -178,3 +192,9 @@ message EndpointSliceList { repeated EndpointSlice items = 2; } +// ForZone provides information about which zones should consume this endpoint. +message ForZone { + // name represents the name of the zone. + optional string name = 1; +} + diff --git a/staging/src/k8s.io/api/discovery/v1beta1/types.go b/staging/src/k8s.io/api/discovery/v1beta1/types.go index e14088e8b2c..35b0be29a5b 100644 --- a/staging/src/k8s.io/api/discovery/v1beta1/types.go +++ b/staging/src/k8s.io/api/discovery/v1beta1/types.go @@ -110,6 +110,11 @@ type Endpoint struct { // with the EndpointSliceNodeName feature gate. // +optional NodeName *string `json:"nodeName,omitempty" protobuf:"bytes,6,opt,name=nodeName"` + // hints contains information associated with how an endpoint should be + // consumed. + // +featureGate=TopologyAwareHints + // +optional + Hints *EndpointHints `json:"hints,omitempty" protobuf:"bytes,7,opt,name=hints"` } // EndpointConditions represents the current condition of an endpoint. @@ -138,6 +143,20 @@ type EndpointConditions struct { Terminating *bool `json:"terminating,omitempty" protobuf:"bytes,3,name=terminating"` } +// EndpointHints provides hints describing how an endpoint should be consumed. +type EndpointHints struct { + // forZones indicates the zone(s) this endpoint should be consumed by to + // enable topology aware routing. May contain a maximum of 8 entries. + // +listType=atomic + ForZones []ForZone `json:"forZones,omitempty" protobuf:"bytes,1,name=forZones"` +} + +// ForZone provides information about which zones should consume this endpoint. +type ForZone struct { + // name represents the name of the zone. + Name string `json:"name" protobuf:"bytes,1,name=name"` +} + // EndpointPort represents a Port used by an EndpointSlice type EndpointPort struct { // The name of this port. All ports in an EndpointSlice must have a unique diff --git a/staging/src/k8s.io/api/discovery/v1beta1/types_swagger_doc_generated.go b/staging/src/k8s.io/api/discovery/v1beta1/types_swagger_doc_generated.go index d48b93d8b50..b4c221999ad 100644 --- a/staging/src/k8s.io/api/discovery/v1beta1/types_swagger_doc_generated.go +++ b/staging/src/k8s.io/api/discovery/v1beta1/types_swagger_doc_generated.go @@ -35,6 +35,7 @@ var map_Endpoint = map[string]string{ "targetRef": "targetRef is a reference to a Kubernetes object that represents this endpoint.", "topology": "topology contains arbitrary topology information associated with the endpoint. These key/value pairs must conform with the label format. https://kubernetes.io/docs/concepts/overview/working-with-objects/labels Topology may include a maximum of 16 key/value pairs. This includes, but is not limited to the following well known keys: * kubernetes.io/hostname: the value indicates the hostname of the node\n where the endpoint is located. This should match the corresponding\n node label.\n* topology.kubernetes.io/zone: the value indicates the zone where the\n endpoint is located. This should match the corresponding node label.\n* topology.kubernetes.io/region: the value indicates the region where the\n endpoint is located. This should match the corresponding node label.\nThis field is deprecated and will be removed in future api versions.", "nodeName": "nodeName represents the name of the Node hosting this endpoint. This can be used to determine endpoints local to a Node. This field can be enabled with the EndpointSliceNodeName feature gate.", + "hints": "hints contains information associated with how an endpoint should be consumed.", } func (Endpoint) SwaggerDoc() map[string]string { @@ -52,6 +53,15 @@ func (EndpointConditions) SwaggerDoc() map[string]string { return map_EndpointConditions } +var map_EndpointHints = map[string]string{ + "": "EndpointHints provides hints describing how an endpoint should be consumed.", + "forZones": "forZones indicates the zone(s) this endpoint should be consumed by to enable topology aware routing. May contain a maximum of 8 entries.", +} + +func (EndpointHints) SwaggerDoc() map[string]string { + return map_EndpointHints +} + var map_EndpointPort = map[string]string{ "": "EndpointPort represents a Port used by an EndpointSlice", "name": "The name of this port. All ports in an EndpointSlice must have a unique name. If the EndpointSlice is dervied from a Kubernetes service, this corresponds to the Service.ports[].name. Name must either be an empty string or pass DNS_LABEL validation: * must be no more than 63 characters long. * must consist of lower case alphanumeric characters or '-'. * must start and end with an alphanumeric character. Default is empty string.", @@ -86,4 +96,13 @@ func (EndpointSliceList) SwaggerDoc() map[string]string { return map_EndpointSliceList } +var map_ForZone = map[string]string{ + "": "ForZone provides information about which zones should consume this endpoint.", + "name": "name represents the name of the zone.", +} + +func (ForZone) SwaggerDoc() map[string]string { + return map_ForZone +} + // AUTO-GENERATED FUNCTIONS END HERE diff --git a/staging/src/k8s.io/api/discovery/v1beta1/zz_generated.deepcopy.go b/staging/src/k8s.io/api/discovery/v1beta1/zz_generated.deepcopy.go index 7076553d291..f13536b4bc6 100644 --- a/staging/src/k8s.io/api/discovery/v1beta1/zz_generated.deepcopy.go +++ b/staging/src/k8s.io/api/discovery/v1beta1/zz_generated.deepcopy.go @@ -56,6 +56,11 @@ func (in *Endpoint) DeepCopyInto(out *Endpoint) { *out = new(string) **out = **in } + if in.Hints != nil { + in, out := &in.Hints, &out.Hints + *out = new(EndpointHints) + (*in).DeepCopyInto(*out) + } return } @@ -100,6 +105,27 @@ func (in *EndpointConditions) DeepCopy() *EndpointConditions { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *EndpointHints) DeepCopyInto(out *EndpointHints) { + *out = *in + if in.ForZones != nil { + in, out := &in.ForZones, &out.ForZones + *out = make([]ForZone, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EndpointHints. +func (in *EndpointHints) DeepCopy() *EndpointHints { + if in == nil { + return nil + } + out := new(EndpointHints) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *EndpointPort) DeepCopyInto(out *EndpointPort) { *out = *in @@ -208,3 +234,19 @@ func (in *EndpointSliceList) DeepCopyObject() runtime.Object { } return nil } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ForZone) DeepCopyInto(out *ForZone) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ForZone. +func (in *ForZone) DeepCopy() *ForZone { + if in == nil { + return nil + } + out := new(ForZone) + in.DeepCopyInto(out) + return out +} diff --git a/staging/src/k8s.io/api/testdata/HEAD/discovery.k8s.io.v1.EndpointSlice.json b/staging/src/k8s.io/api/testdata/HEAD/discovery.k8s.io.v1.EndpointSlice.json index fa17b9f3c54..186facfa284 100644 --- a/staging/src/k8s.io/api/testdata/HEAD/discovery.k8s.io.v1.EndpointSlice.json +++ b/staging/src/k8s.io/api/testdata/HEAD/discovery.k8s.io.v1.EndpointSlice.json @@ -65,15 +65,22 @@ "27": "28" }, "nodeName": "29", - "zone": "30" + "zone": "30", + "hints": { + "forZones": [ + { + "name": "31" + } + ] + } } ], "ports": [ { - "name": "31", - "protocol": "脽ěĂ凗蓏Ŋ蛊ĉy緅縕", + "name": "32", + "protocol": "ěĂ凗蓏Ŋ蛊ĉy緅縕", "port": -591435092, - "appProtocol": "32" + "appProtocol": "33" } ] } \ No newline at end of file diff --git a/staging/src/k8s.io/api/testdata/HEAD/discovery.k8s.io.v1.EndpointSlice.pb b/staging/src/k8s.io/api/testdata/HEAD/discovery.k8s.io.v1.EndpointSlice.pb index 9bf7f091e21..312a5a75cd9 100644 Binary files a/staging/src/k8s.io/api/testdata/HEAD/discovery.k8s.io.v1.EndpointSlice.pb and b/staging/src/k8s.io/api/testdata/HEAD/discovery.k8s.io.v1.EndpointSlice.pb differ diff --git a/staging/src/k8s.io/api/testdata/HEAD/discovery.k8s.io.v1.EndpointSlice.yaml b/staging/src/k8s.io/api/testdata/HEAD/discovery.k8s.io.v1.EndpointSlice.yaml index d84ec7aea72..9605ccbcfeb 100644 --- a/staging/src/k8s.io/api/testdata/HEAD/discovery.k8s.io.v1.EndpointSlice.yaml +++ b/staging/src/k8s.io/api/testdata/HEAD/discovery.k8s.io.v1.EndpointSlice.yaml @@ -9,6 +9,9 @@ endpoints: terminating: false deprecatedTopology: "27": "28" + hints: + forZones: + - name: "31" hostname: "20" nodeName: "29" targetRef: @@ -51,7 +54,7 @@ metadata: selfLink: "5" uid: "7" ports: -- appProtocol: "32" - name: "31" +- appProtocol: "33" + name: "32" port: -591435092 - protocol: 脽ěĂ凗蓏Ŋ蛊ĉy緅縕 + protocol: ěĂ凗蓏Ŋ蛊ĉy緅縕 diff --git a/staging/src/k8s.io/api/testdata/HEAD/discovery.k8s.io.v1beta1.EndpointSlice.json b/staging/src/k8s.io/api/testdata/HEAD/discovery.k8s.io.v1beta1.EndpointSlice.json index 595b4f92841..e8166ae43b8 100644 --- a/staging/src/k8s.io/api/testdata/HEAD/discovery.k8s.io.v1beta1.EndpointSlice.json +++ b/staging/src/k8s.io/api/testdata/HEAD/discovery.k8s.io.v1beta1.EndpointSlice.json @@ -64,15 +64,22 @@ "topology": { "27": "28" }, - "nodeName": "29" + "nodeName": "29", + "hints": { + "forZones": [ + { + "name": "30" + } + ] + } } ], "ports": [ { - "name": "30", - "protocol": "脽ěĂ凗蓏Ŋ蛊ĉy緅縕", + "name": "31", + "protocol": "ěĂ凗蓏Ŋ蛊ĉy緅縕", "port": -591435092, - "appProtocol": "31" + "appProtocol": "32" } ] } \ No newline at end of file diff --git a/staging/src/k8s.io/api/testdata/HEAD/discovery.k8s.io.v1beta1.EndpointSlice.pb b/staging/src/k8s.io/api/testdata/HEAD/discovery.k8s.io.v1beta1.EndpointSlice.pb index 45ce2a2efa1..72a9c5936e3 100644 Binary files a/staging/src/k8s.io/api/testdata/HEAD/discovery.k8s.io.v1beta1.EndpointSlice.pb and b/staging/src/k8s.io/api/testdata/HEAD/discovery.k8s.io.v1beta1.EndpointSlice.pb differ diff --git a/staging/src/k8s.io/api/testdata/HEAD/discovery.k8s.io.v1beta1.EndpointSlice.yaml b/staging/src/k8s.io/api/testdata/HEAD/discovery.k8s.io.v1beta1.EndpointSlice.yaml index 85e0ad7def4..28ed5d85e1e 100644 --- a/staging/src/k8s.io/api/testdata/HEAD/discovery.k8s.io.v1beta1.EndpointSlice.yaml +++ b/staging/src/k8s.io/api/testdata/HEAD/discovery.k8s.io.v1beta1.EndpointSlice.yaml @@ -7,6 +7,9 @@ endpoints: ready: false serving: false terminating: false + hints: + forZones: + - name: "30" hostname: "20" nodeName: "29" targetRef: @@ -50,7 +53,7 @@ metadata: selfLink: "5" uid: "7" ports: -- appProtocol: "31" - name: "30" +- appProtocol: "32" + name: "31" port: -591435092 - protocol: 脽ěĂ凗蓏Ŋ蛊ĉy緅縕 + protocol: ěĂ凗蓏Ŋ蛊ĉy緅縕 diff --git a/staging/src/k8s.io/client-go/applyconfigurations/discovery/v1/endpoint.go b/staging/src/k8s.io/client-go/applyconfigurations/discovery/v1/endpoint.go index 9930326687a..d8c2359a3b7 100644 --- a/staging/src/k8s.io/client-go/applyconfigurations/discovery/v1/endpoint.go +++ b/staging/src/k8s.io/client-go/applyconfigurations/discovery/v1/endpoint.go @@ -32,6 +32,7 @@ type EndpointApplyConfiguration struct { DeprecatedTopology map[string]string `json:"deprecatedTopology,omitempty"` NodeName *string `json:"nodeName,omitempty"` Zone *string `json:"zone,omitempty"` + Hints *EndpointHintsApplyConfiguration `json:"hints,omitempty"` } // EndpointApplyConfiguration constructs an declarative configuration of the Endpoint type for use with @@ -103,3 +104,11 @@ func (b *EndpointApplyConfiguration) WithZone(value string) *EndpointApplyConfig b.Zone = &value return b } + +// WithHints sets the Hints field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Hints field is set to the value of the last call. +func (b *EndpointApplyConfiguration) WithHints(value *EndpointHintsApplyConfiguration) *EndpointApplyConfiguration { + b.Hints = value + return b +} diff --git a/staging/src/k8s.io/client-go/applyconfigurations/discovery/v1/endpointhints.go b/staging/src/k8s.io/client-go/applyconfigurations/discovery/v1/endpointhints.go new file mode 100644 index 00000000000..6eb9f21a513 --- /dev/null +++ b/staging/src/k8s.io/client-go/applyconfigurations/discovery/v1/endpointhints.go @@ -0,0 +1,44 @@ +/* +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 applyconfiguration-gen. DO NOT EDIT. + +package v1 + +// EndpointHintsApplyConfiguration represents an declarative configuration of the EndpointHints type for use +// with apply. +type EndpointHintsApplyConfiguration struct { + ForZones []ForZoneApplyConfiguration `json:"forZones,omitempty"` +} + +// EndpointHintsApplyConfiguration constructs an declarative configuration of the EndpointHints type for use with +// apply. +func EndpointHints() *EndpointHintsApplyConfiguration { + return &EndpointHintsApplyConfiguration{} +} + +// WithForZones adds the given value to the ForZones field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the ForZones field. +func (b *EndpointHintsApplyConfiguration) WithForZones(values ...*ForZoneApplyConfiguration) *EndpointHintsApplyConfiguration { + for i := range values { + if values[i] == nil { + panic("nil value passed to WithForZones") + } + b.ForZones = append(b.ForZones, *values[i]) + } + return b +} diff --git a/staging/src/k8s.io/client-go/applyconfigurations/discovery/v1/forzone.go b/staging/src/k8s.io/client-go/applyconfigurations/discovery/v1/forzone.go new file mode 100644 index 00000000000..192a5ad2e8c --- /dev/null +++ b/staging/src/k8s.io/client-go/applyconfigurations/discovery/v1/forzone.go @@ -0,0 +1,39 @@ +/* +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 applyconfiguration-gen. DO NOT EDIT. + +package v1 + +// ForZoneApplyConfiguration represents an declarative configuration of the ForZone type for use +// with apply. +type ForZoneApplyConfiguration struct { + Name *string `json:"name,omitempty"` +} + +// ForZoneApplyConfiguration constructs an declarative configuration of the ForZone type for use with +// apply. +func ForZone() *ForZoneApplyConfiguration { + return &ForZoneApplyConfiguration{} +} + +// WithName sets the Name field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Name field is set to the value of the last call. +func (b *ForZoneApplyConfiguration) WithName(value string) *ForZoneApplyConfiguration { + b.Name = &value + return b +} diff --git a/staging/src/k8s.io/client-go/applyconfigurations/discovery/v1beta1/endpoint.go b/staging/src/k8s.io/client-go/applyconfigurations/discovery/v1beta1/endpoint.go index f3dfd2ab8b2..724c2d007c0 100644 --- a/staging/src/k8s.io/client-go/applyconfigurations/discovery/v1beta1/endpoint.go +++ b/staging/src/k8s.io/client-go/applyconfigurations/discovery/v1beta1/endpoint.go @@ -31,6 +31,7 @@ type EndpointApplyConfiguration struct { TargetRef *v1.ObjectReferenceApplyConfiguration `json:"targetRef,omitempty"` Topology map[string]string `json:"topology,omitempty"` NodeName *string `json:"nodeName,omitempty"` + Hints *EndpointHintsApplyConfiguration `json:"hints,omitempty"` } // EndpointApplyConfiguration constructs an declarative configuration of the Endpoint type for use with @@ -94,3 +95,11 @@ func (b *EndpointApplyConfiguration) WithNodeName(value string) *EndpointApplyCo b.NodeName = &value return b } + +// WithHints sets the Hints field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Hints field is set to the value of the last call. +func (b *EndpointApplyConfiguration) WithHints(value *EndpointHintsApplyConfiguration) *EndpointApplyConfiguration { + b.Hints = value + return b +} diff --git a/staging/src/k8s.io/client-go/applyconfigurations/discovery/v1beta1/endpointhints.go b/staging/src/k8s.io/client-go/applyconfigurations/discovery/v1beta1/endpointhints.go new file mode 100644 index 00000000000..41d80206b3b --- /dev/null +++ b/staging/src/k8s.io/client-go/applyconfigurations/discovery/v1beta1/endpointhints.go @@ -0,0 +1,44 @@ +/* +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 applyconfiguration-gen. DO NOT EDIT. + +package v1beta1 + +// EndpointHintsApplyConfiguration represents an declarative configuration of the EndpointHints type for use +// with apply. +type EndpointHintsApplyConfiguration struct { + ForZones []ForZoneApplyConfiguration `json:"forZones,omitempty"` +} + +// EndpointHintsApplyConfiguration constructs an declarative configuration of the EndpointHints type for use with +// apply. +func EndpointHints() *EndpointHintsApplyConfiguration { + return &EndpointHintsApplyConfiguration{} +} + +// WithForZones adds the given value to the ForZones field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the ForZones field. +func (b *EndpointHintsApplyConfiguration) WithForZones(values ...*ForZoneApplyConfiguration) *EndpointHintsApplyConfiguration { + for i := range values { + if values[i] == nil { + panic("nil value passed to WithForZones") + } + b.ForZones = append(b.ForZones, *values[i]) + } + return b +} diff --git a/staging/src/k8s.io/client-go/applyconfigurations/discovery/v1beta1/forzone.go b/staging/src/k8s.io/client-go/applyconfigurations/discovery/v1beta1/forzone.go new file mode 100644 index 00000000000..4d1455ed384 --- /dev/null +++ b/staging/src/k8s.io/client-go/applyconfigurations/discovery/v1beta1/forzone.go @@ -0,0 +1,39 @@ +/* +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 applyconfiguration-gen. DO NOT EDIT. + +package v1beta1 + +// ForZoneApplyConfiguration represents an declarative configuration of the ForZone type for use +// with apply. +type ForZoneApplyConfiguration struct { + Name *string `json:"name,omitempty"` +} + +// ForZoneApplyConfiguration constructs an declarative configuration of the ForZone type for use with +// apply. +func ForZone() *ForZoneApplyConfiguration { + return &ForZoneApplyConfiguration{} +} + +// WithName sets the Name field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Name field is set to the value of the last call. +func (b *ForZoneApplyConfiguration) WithName(value string) *ForZoneApplyConfiguration { + b.Name = &value + return b +} diff --git a/staging/src/k8s.io/client-go/applyconfigurations/utils.go b/staging/src/k8s.io/client-go/applyconfigurations/utils.go index de94fe27806..1aef170ee5f 100644 --- a/staging/src/k8s.io/client-go/applyconfigurations/utils.go +++ b/staging/src/k8s.io/client-go/applyconfigurations/utils.go @@ -777,20 +777,28 @@ func ForKind(kind schema.GroupVersionKind) interface{} { return &applyconfigurationsdiscoveryv1.EndpointApplyConfiguration{} case discoveryv1.SchemeGroupVersion.WithKind("EndpointConditions"): return &applyconfigurationsdiscoveryv1.EndpointConditionsApplyConfiguration{} + case discoveryv1.SchemeGroupVersion.WithKind("EndpointHints"): + return &applyconfigurationsdiscoveryv1.EndpointHintsApplyConfiguration{} case discoveryv1.SchemeGroupVersion.WithKind("EndpointPort"): return &applyconfigurationsdiscoveryv1.EndpointPortApplyConfiguration{} case discoveryv1.SchemeGroupVersion.WithKind("EndpointSlice"): return &applyconfigurationsdiscoveryv1.EndpointSliceApplyConfiguration{} + case discoveryv1.SchemeGroupVersion.WithKind("ForZone"): + return &applyconfigurationsdiscoveryv1.ForZoneApplyConfiguration{} // Group=discovery.k8s.io, Version=v1beta1 case discoveryv1beta1.SchemeGroupVersion.WithKind("Endpoint"): return &applyconfigurationsdiscoveryv1beta1.EndpointApplyConfiguration{} case discoveryv1beta1.SchemeGroupVersion.WithKind("EndpointConditions"): return &applyconfigurationsdiscoveryv1beta1.EndpointConditionsApplyConfiguration{} + case discoveryv1beta1.SchemeGroupVersion.WithKind("EndpointHints"): + return &applyconfigurationsdiscoveryv1beta1.EndpointHintsApplyConfiguration{} case discoveryv1beta1.SchemeGroupVersion.WithKind("EndpointPort"): return &applyconfigurationsdiscoveryv1beta1.EndpointPortApplyConfiguration{} case discoveryv1beta1.SchemeGroupVersion.WithKind("EndpointSlice"): return &applyconfigurationsdiscoveryv1beta1.EndpointSliceApplyConfiguration{} + case discoveryv1beta1.SchemeGroupVersion.WithKind("ForZone"): + return &applyconfigurationsdiscoveryv1beta1.ForZoneApplyConfiguration{} // Group=events.k8s.io, Version=v1 case eventsv1.SchemeGroupVersion.WithKind("Event"):