Skip to content

Commit 4435ead

Browse files
committed
Add PreferSameTrafficDistribution feature gate and associated API.
1 parent 6ca82f9 commit 4435ead

File tree

14 files changed

+422
-75
lines changed

14 files changed

+422
-75
lines changed

pkg/apis/core/types.go

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4562,12 +4562,27 @@ const (
45624562

45634563
// These are valid values for the TrafficDistribution field of a Service.
45644564
const (
4565-
// Indicates a preference for routing traffic to endpoints that are in the
4566-
// same zone as the client. Setting this value gives implementations
4567-
// permission to make different tradeoffs, e.g. optimizing for proximity
4568-
// rather than equal distribution of load. Users should not set this value
4569-
// if such tradeoffs are not acceptable.
4565+
// Indicates a preference for routing traffic to endpoints that are in the same
4566+
// zone as the client. Users should not set this value unless they have ensured
4567+
// that clients and endpoints are distributed in such a way that the "same zone"
4568+
// preference will not result in endpoints getting overloaded.
45704569
ServiceTrafficDistributionPreferClose = "PreferClose"
4570+
4571+
// Indicates a preference for routing traffic to endpoints that are in the same
4572+
// zone as the client. Users should not set this value unless they have ensured
4573+
// that clients and endpoints are distributed in such a way that the "same zone"
4574+
// preference will not result in endpoints getting overloaded.
4575+
// This is an alias for "PreferClose", but it is an Alpha feature and is only
4576+
// recognized if the PreferSameTrafficDistribution feature gate is enabled.
4577+
ServiceTrafficDistributionPreferSameZone = "PreferSameZone"
4578+
4579+
// Indicates a preference for routing traffic to endpoints that are on the same
4580+
// node as the client. Users should not set this value unless they have ensured
4581+
// that clients and endpoints are distributed in such a way that the "same node"
4582+
// preference will not result in endpoints getting overloaded.
4583+
// This is an Alpha feature and is only recognized if the
4584+
// PreferSameTrafficDistribution feature gate is enabled.
4585+
ServiceTrafficDistributionPreferSameNode = "PreferSameNode"
45714586
)
45724587

45734588
// These are the valid conditions of a service.

pkg/apis/core/validation/validation.go

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525
"path/filepath"
2626
"reflect"
2727
"regexp"
28+
"slices"
2829
"strings"
2930
"sync"
3031
"unicode"
@@ -6194,8 +6195,21 @@ func validateServiceTrafficDistribution(service *core.Service) field.ErrorList {
61946195
return allErrs
61956196
}
61966197

6197-
if *service.Spec.TrafficDistribution != v1.ServiceTrafficDistributionPreferClose {
6198-
allErrs = append(allErrs, field.NotSupported(field.NewPath("spec").Child("trafficDistribution"), *service.Spec.TrafficDistribution, []string{v1.ServiceTrafficDistributionPreferClose}))
6198+
var supportedTrafficDistribution []string
6199+
if !utilfeature.DefaultFeatureGate.Enabled(features.PreferSameTrafficDistribution) {
6200+
supportedTrafficDistribution = []string{
6201+
v1.ServiceTrafficDistributionPreferClose,
6202+
}
6203+
} else {
6204+
supportedTrafficDistribution = []string{
6205+
v1.ServiceTrafficDistributionPreferClose,
6206+
v1.ServiceTrafficDistributionPreferSameZone,
6207+
v1.ServiceTrafficDistributionPreferSameNode,
6208+
}
6209+
}
6210+
6211+
if !slices.Contains(supportedTrafficDistribution, *service.Spec.TrafficDistribution) {
6212+
allErrs = append(allErrs, field.NotSupported(field.NewPath("spec").Child("trafficDistribution"), *service.Spec.TrafficDistribution, supportedTrafficDistribution))
61996213
}
62006214

62016215
return allErrs

pkg/apis/core/validation/validation_test.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16413,12 +16413,38 @@ func TestValidateServiceCreate(t *testing.T) {
1641316413
s.Spec.TrafficDistribution = ptr.To("PreferClose")
1641416414
},
1641516415
numErrs: 0,
16416+
}, {
16417+
name: "valid: trafficDistribution field set to PreferSameZone with feature gate",
16418+
tweakSvc: func(s *core.Service) {
16419+
s.Spec.TrafficDistribution = ptr.To("PreferSameZone")
16420+
},
16421+
featureGates: []featuregate.Feature{features.PreferSameTrafficDistribution},
16422+
numErrs: 0,
16423+
}, {
16424+
name: "valid: trafficDistribution field set to PreferSameNode with feature gate",
16425+
tweakSvc: func(s *core.Service) {
16426+
s.Spec.TrafficDistribution = ptr.To("PreferSameNode")
16427+
},
16428+
featureGates: []featuregate.Feature{features.PreferSameTrafficDistribution},
16429+
numErrs: 0,
1641616430
}, {
1641716431
name: "invalid: trafficDistribution field set to Random",
1641816432
tweakSvc: func(s *core.Service) {
1641916433
s.Spec.TrafficDistribution = ptr.To("Random")
1642016434
},
1642116435
numErrs: 1,
16436+
}, {
16437+
name: "invalid: trafficDistribution field set to PreferSameZone without feature gate",
16438+
tweakSvc: func(s *core.Service) {
16439+
s.Spec.TrafficDistribution = ptr.To("PreferSameZone")
16440+
},
16441+
numErrs: 1,
16442+
}, {
16443+
name: "invalid: trafficDistribution field set to PreferSameNode without feature gate",
16444+
tweakSvc: func(s *core.Service) {
16445+
s.Spec.TrafficDistribution = ptr.To("PreferSameNode")
16446+
},
16447+
numErrs: 1,
1642216448
},
1642316449
}
1642416450

pkg/apis/discovery/types.go

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -133,9 +133,16 @@ type EndpointConditions struct {
133133

134134
// EndpointHints provides hints describing how an endpoint should be consumed.
135135
type EndpointHints struct {
136-
// forZones indicates the zone(s) this endpoint should be consumed by to
137-
// enable topology aware routing. May contain a maximum of 8 entries.
136+
// forZones indicates the zone(s) this endpoint should be consumed by when
137+
// using topology aware routing. May contain a maximum of 8 entries.
138138
ForZones []ForZone
139+
140+
// forNodes indicates the node(s) this endpoint should be consumed by when
141+
// using topology aware routing.
142+
// This is an Alpha feature and is only used when the PreferSameTrafficDistribution
143+
// feature gate is enabled. May contain a maximum of 8 entries.
144+
// +featureGate=PreferSameTrafficDistribution
145+
ForNodes []ForNode
139146
}
140147

141148
// ForZone provides information about which zones should consume this endpoint.
@@ -144,6 +151,12 @@ type ForZone struct {
144151
Name string
145152
}
146153

154+
// ForNode provides information about which nodes should consume this endpoint.
155+
type ForNode struct {
156+
// name represents the name of the node.
157+
Name string
158+
}
159+
147160
// EndpointPort represents a Port used by an EndpointSlice.
148161
type EndpointPort struct {
149162
// The name of this port. All ports in an EndpointSlice must have a unique

pkg/apis/discovery/validation/validation.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ var (
4949
maxPorts = 20000
5050
maxEndpoints = 1000
5151
maxZoneHints = 8
52+
maxNodeHints = 8
5253
)
5354

5455
// ValidateEndpointSliceName can be used to check whether the given endpoint
@@ -240,5 +241,25 @@ func validateHints(endpointHints *discovery.EndpointHints, fldPath *field.Path)
240241
}
241242
}
242243

244+
fnPath := fldPath.Child("forNodes")
245+
if len(endpointHints.ForNodes) > maxNodeHints {
246+
allErrs = append(allErrs, field.TooMany(fnPath, len(endpointHints.ForNodes), maxNodeHints))
247+
return allErrs
248+
}
249+
250+
nodeNames := make([]string, 0, len(endpointHints.ForNodes))
251+
for i, forNode := range endpointHints.ForNodes {
252+
nodePath := fnPath.Index(i).Child("name")
253+
if slices.Contains(nodeNames, forNode.Name) {
254+
allErrs = append(allErrs, field.Duplicate(nodePath, forNode.Name))
255+
} else {
256+
nodeNames = append(nodeNames, forNode.Name)
257+
}
258+
259+
for _, msg := range apivalidation.ValidateNodeName(forNode.Name, false) {
260+
allErrs = append(allErrs, field.Invalid(nodePath, forNode.Name, msg))
261+
}
262+
}
263+
243264
return allErrs
244265
}

pkg/apis/discovery/validation/validation_test.go

Lines changed: 72 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,7 @@ func TestValidateEndpointSlice(t *testing.T) {
235235
Addresses: generateIPAddresses(1),
236236
Hints: &discovery.EndpointHints{
237237
ForZones: []discovery.ForZone{{Name: "zone-a"}},
238+
ForNodes: []discovery.ForNode{{Name: "node-1"}},
238239
},
239240
}},
240241
},
@@ -518,7 +519,7 @@ func TestValidateEndpointSlice(t *testing.T) {
518519
}},
519520
},
520521
},
521-
"invalid-hints": {
522+
"invalid-zone-hint": {
522523
expectedErrors: 1,
523524
endpointSlice: &discovery.EndpointSlice{
524525
ObjectMeta: standardMeta,
@@ -535,7 +536,7 @@ func TestValidateEndpointSlice(t *testing.T) {
535536
}},
536537
},
537538
},
538-
"overlapping-hints": {
539+
"overlapping-zone-hints": {
539540
expectedErrors: 1,
540541
endpointSlice: &discovery.EndpointSlice{
541542
ObjectMeta: standardMeta,
@@ -556,7 +557,7 @@ func TestValidateEndpointSlice(t *testing.T) {
556557
}},
557558
},
558559
},
559-
"too-many-hints": {
560+
"too-many-zone-hints": {
560561
expectedErrors: 1,
561562
endpointSlice: &discovery.EndpointSlice{
562563
ObjectMeta: standardMeta,
@@ -583,6 +584,74 @@ func TestValidateEndpointSlice(t *testing.T) {
583584
}},
584585
},
585586
},
587+
"invalid-node-hints": {
588+
expectedErrors: 2,
589+
endpointSlice: &discovery.EndpointSlice{
590+
ObjectMeta: standardMeta,
591+
AddressType: discovery.AddressTypeIPv4,
592+
Ports: []discovery.EndpointPort{{
593+
Name: ptr.To("http"),
594+
Protocol: ptr.To(api.ProtocolTCP),
595+
}},
596+
Endpoints: []discovery.Endpoint{{
597+
Addresses: generateIPAddresses(1),
598+
Hints: &discovery.EndpointHints{
599+
ForNodes: []discovery.ForNode{
600+
{Name: "!@#$!@"},
601+
{Name: ""},
602+
},
603+
},
604+
}},
605+
},
606+
},
607+
"overlapping-node-hints": {
608+
expectedErrors: 1,
609+
endpointSlice: &discovery.EndpointSlice{
610+
ObjectMeta: standardMeta,
611+
AddressType: discovery.AddressTypeIPv4,
612+
Ports: []discovery.EndpointPort{{
613+
Name: ptr.To("http"),
614+
Protocol: ptr.To(api.ProtocolTCP),
615+
}},
616+
Endpoints: []discovery.Endpoint{{
617+
Addresses: generateIPAddresses(1),
618+
Hints: &discovery.EndpointHints{
619+
ForNodes: []discovery.ForNode{
620+
{Name: "node-1"},
621+
{Name: "node-2"},
622+
{Name: "node-1"},
623+
},
624+
},
625+
}},
626+
},
627+
},
628+
"too-many-node-hints": {
629+
expectedErrors: 1,
630+
endpointSlice: &discovery.EndpointSlice{
631+
ObjectMeta: standardMeta,
632+
AddressType: discovery.AddressTypeIPv4,
633+
Ports: []discovery.EndpointPort{{
634+
Name: ptr.To("http"),
635+
Protocol: ptr.To(api.ProtocolTCP),
636+
}},
637+
Endpoints: []discovery.Endpoint{{
638+
Addresses: generateIPAddresses(1),
639+
Hints: &discovery.EndpointHints{
640+
ForNodes: []discovery.ForNode{
641+
{Name: "node-1"},
642+
{Name: "node-2"},
643+
{Name: "node-3"},
644+
{Name: "node-4"},
645+
{Name: "node-5"},
646+
{Name: "node-6"},
647+
{Name: "node-7"},
648+
{Name: "node-8"},
649+
{Name: "node-9"},
650+
},
651+
},
652+
}},
653+
},
654+
},
586655
"empty-everything": {
587656
expectedErrors: 3,
588657
endpointSlice: &discovery.EndpointSlice{},

pkg/features/kube_features.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -557,6 +557,12 @@ const (
557557
// Enables PortForward to be proxied with a websocket client
558558
PortForwardWebsockets featuregate.Feature = "PortForwardWebsockets"
559559

560+
// owner: @danwinship
561+
// kep: https://kep.k8s.io/3015
562+
//
563+
// Enables PreferSameZone and PreferSameNode values for trafficDistribution
564+
PreferSameTrafficDistribution featuregate.Feature = "PreferSameTrafficDistribution"
565+
560566
// owner: @jessfraz
561567
//
562568
// Enables control over ProcMountType for containers.

pkg/features/versioned_kube_features.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -621,6 +621,10 @@ var defaultVersionedKubernetesFeatureGates = map[featuregate.Feature]featuregate
621621
{Version: version.MustParse("1.31"), Default: true, PreRelease: featuregate.Beta},
622622
},
623623

624+
PreferSameTrafficDistribution: {
625+
{Version: version.MustParse("1.33"), Default: false, PreRelease: featuregate.Alpha},
626+
},
627+
624628
ProcMountType: {
625629
{Version: version.MustParse("1.12"), Default: false, PreRelease: featuregate.Alpha},
626630
{Version: version.MustParse("1.31"), Default: false, PreRelease: featuregate.Beta},

pkg/registry/discovery/endpointslice/strategy.go

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -142,12 +142,16 @@ func (endpointSliceStrategy) AllowUnconditionalUpdate() bool {
142142
// dropDisabledConditionsOnCreate will drop any fields that are disabled.
143143
func dropDisabledFieldsOnCreate(endpointSlice *discovery.EndpointSlice) {
144144
dropHints := !utilfeature.DefaultFeatureGate.Enabled(features.TopologyAwareHints)
145+
dropNodeHints := !utilfeature.DefaultFeatureGate.Enabled(features.PreferSameTrafficDistribution)
146+
if !dropHints && !dropNodeHints {
147+
return
148+
}
145149

146-
if dropHints {
147-
for i := range endpointSlice.Endpoints {
148-
if dropHints {
149-
endpointSlice.Endpoints[i].Hints = nil
150-
}
150+
for i := range endpointSlice.Endpoints {
151+
if dropHints {
152+
endpointSlice.Endpoints[i].Hints = nil
153+
} else if endpointSlice.Endpoints[i].Hints != nil {
154+
endpointSlice.Endpoints[i].Hints.ForNodes = nil
151155
}
152156
}
153157
}
@@ -156,20 +160,27 @@ func dropDisabledFieldsOnCreate(endpointSlice *discovery.EndpointSlice) {
156160
// been set on the EndpointSlice.
157161
func dropDisabledFieldsOnUpdate(oldEPS, newEPS *discovery.EndpointSlice) {
158162
dropHints := !utilfeature.DefaultFeatureGate.Enabled(features.TopologyAwareHints)
159-
if dropHints {
163+
dropNodeHints := !utilfeature.DefaultFeatureGate.Enabled(features.PreferSameTrafficDistribution)
164+
if dropHints || dropNodeHints {
160165
for _, ep := range oldEPS.Endpoints {
161166
if ep.Hints != nil {
162167
dropHints = false
163-
break
168+
if ep.Hints.ForNodes != nil {
169+
dropNodeHints = false
170+
break
171+
}
164172
}
165173
}
166174
}
175+
if !dropHints && !dropNodeHints {
176+
return
177+
}
167178

168-
if dropHints {
169-
for i := range newEPS.Endpoints {
170-
if dropHints {
171-
newEPS.Endpoints[i].Hints = nil
172-
}
179+
for i := range newEPS.Endpoints {
180+
if dropHints {
181+
newEPS.Endpoints[i].Hints = nil
182+
} else if newEPS.Endpoints[i].Hints != nil {
183+
newEPS.Endpoints[i].Hints.ForNodes = nil
173184
}
174185
}
175186
}

0 commit comments

Comments
 (0)