Skip to content

Commit d9be37e

Browse files
authored
Merge pull request kubernetes#72046 from m1093782566/service-topology-api
Service Topology implementation
2 parents e4bde52 + 31d623b commit d9be37e

40 files changed

+2010
-889
lines changed

api/api-rules/violation_exceptions.list

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,7 @@ API rule violation: list_type_missing,k8s.io/api/core/v1,ServiceAccountList,Item
214214
API rule violation: list_type_missing,k8s.io/api/core/v1,ServiceList,Items
215215
API rule violation: list_type_missing,k8s.io/api/core/v1,ServiceSpec,ExternalIPs
216216
API rule violation: list_type_missing,k8s.io/api/core/v1,ServiceSpec,LoadBalancerSourceRanges
217+
API rule violation: list_type_missing,k8s.io/api/core/v1,ServiceSpec,TopologyKeys
217218
API rule violation: list_type_missing,k8s.io/api/core/v1,TopologySelectorLabelRequirement,Values
218219
API rule violation: list_type_missing,k8s.io/api/core/v1,TopologySelectorTerm,MatchLabelExpressions
219220
API rule violation: list_type_missing,k8s.io/api/events/v1beta1,EventList,Items

api/openapi-spec/swagger.json

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cmd/kube-proxy/app/BUILD

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ go_library(
4545
"//pkg/util/sysctl:go_default_library",
4646
"//staging/src/k8s.io/api/core/v1:go_default_library",
4747
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
48+
"//staging/src/k8s.io/apimachinery/pkg/fields:go_default_library",
4849
"//staging/src/k8s.io/apimachinery/pkg/labels:go_default_library",
4950
"//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library",
5051
"//staging/src/k8s.io/apimachinery/pkg/runtime/serializer:go_default_library",

cmd/kube-proxy/app/server.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import (
3535
gerrors "github.com/pkg/errors"
3636
v1 "k8s.io/api/core/v1"
3737
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
38+
"k8s.io/apimachinery/pkg/fields"
3839
"k8s.io/apimachinery/pkg/labels"
3940
"k8s.io/apimachinery/pkg/runtime"
4041
"k8s.io/apimachinery/pkg/runtime/serializer"
@@ -663,6 +664,7 @@ func (s *ProxyServer) Run() error {
663664
labelSelector := labels.NewSelector()
664665
labelSelector = labelSelector.Add(*noProxyName, *noHeadlessEndpoints)
665666

667+
// Make informers that filter out objects that want a non-default service proxy.
666668
informerFactory := informers.NewSharedInformerFactoryWithOptions(s.Client, s.ConfigSyncPeriod,
667669
informers.WithTweakListOptions(func(options *metav1.ListOptions) {
668670
options.LabelSelector = labelSelector.String()
@@ -690,6 +692,21 @@ func (s *ProxyServer) Run() error {
690692
// functions must configure their shared informer event handlers first.
691693
informerFactory.Start(wait.NeverStop)
692694

695+
if utilfeature.DefaultFeatureGate.Enabled(features.ServiceTopology) {
696+
// Make an informer that selects for our nodename.
697+
currentNodeInformerFactory := informers.NewSharedInformerFactoryWithOptions(s.Client, s.ConfigSyncPeriod,
698+
informers.WithTweakListOptions(func(options *metav1.ListOptions) {
699+
options.FieldSelector = fields.OneTermEqualSelector("metadata.name", s.NodeRef.Name).String()
700+
}))
701+
nodeConfig := config.NewNodeConfig(currentNodeInformerFactory.Core().V1().Nodes(), s.ConfigSyncPeriod)
702+
nodeConfig.RegisterEventHandler(s.Proxier)
703+
go nodeConfig.Run(wait.NeverStop)
704+
705+
// This has to start after the calls to NewNodeConfig because that must
706+
// configure the shared informer event handler first.
707+
currentNodeInformerFactory.Start(wait.NeverStop)
708+
}
709+
693710
// Birth Cry after the birth is successful
694711
s.birthCry()
695712

pkg/apis/core/types.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3391,6 +3391,8 @@ const (
33913391
IPv4Protocol IPFamily = "IPv4"
33923392
// IPv6Protocol indicates that this IP is IPv6 protocol
33933393
IPv6Protocol IPFamily = "IPv6"
3394+
// MaxServiceTopologyKeys is the largest number of topology keys allowed on a service
3395+
MaxServiceTopologyKeys = 16
33943396
)
33953397

33963398
// ServiceSpec describes the attributes that a user creates on a service
@@ -3503,6 +3505,21 @@ type ServiceSpec struct {
35033505
// cluster (e.g. IPv6 in IPv4 only cluster) is an error condition and will fail during clusterIP assignment.
35043506
// +optional
35053507
IPFamily *IPFamily
3508+
3509+
// topologyKeys is a preference-order list of topology keys which
3510+
// implementations of services should use to preferentially sort endpoints
3511+
// when accessing this Service, it can not be used at the same time as
3512+
// externalTrafficPolicy=Local.
3513+
// Topology keys must be valid label keys and at most 16 keys may be specified.
3514+
// Endpoints are chosen based on the first topology key with available backends.
3515+
// If this field is specified and all entries have no backends that match
3516+
// the topology of the client, the service has no backends for that client
3517+
// and connections should fail.
3518+
// The special value "*" may be used to mean "any topology". This catch-all
3519+
// value, if used, only makes sense as the last value in the list.
3520+
// If this is not specified or empty, no topology constraints will be applied.
3521+
// +optional
3522+
TopologyKeys []string
35063523
}
35073524

35083525
// ServicePort represents the port on which the service is exposed

pkg/apis/core/v1/zz_generated.conversion.go

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/apis/core/validation/validation.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4056,6 +4056,35 @@ func ValidateService(service *core.Service) field.ErrorList {
40564056
ports[key] = true
40574057
}
40584058

4059+
// Validate TopologyKeys
4060+
if len(service.Spec.TopologyKeys) > 0 {
4061+
topoPath := specPath.Child("topologyKeys")
4062+
// topologyKeys is mutually exclusive with 'externalTrafficPolicy=Local'
4063+
if service.Spec.ExternalTrafficPolicy == core.ServiceExternalTrafficPolicyTypeLocal {
4064+
allErrs = append(allErrs, field.Forbidden(topoPath, "may not be specified when `externalTrafficPolicy=Local`"))
4065+
}
4066+
if len(service.Spec.TopologyKeys) > core.MaxServiceTopologyKeys {
4067+
allErrs = append(allErrs, field.TooMany(topoPath, len(service.Spec.TopologyKeys), core.MaxServiceTopologyKeys))
4068+
}
4069+
topoKeys := sets.NewString()
4070+
for i, key := range service.Spec.TopologyKeys {
4071+
keyPath := topoPath.Index(i)
4072+
if topoKeys.Has(key) {
4073+
allErrs = append(allErrs, field.Duplicate(keyPath, key))
4074+
}
4075+
topoKeys.Insert(key)
4076+
// "Any" must be the last value specified
4077+
if key == v1.TopologyKeyAny && i != len(service.Spec.TopologyKeys)-1 {
4078+
allErrs = append(allErrs, field.Invalid(keyPath, key, `"*" must be the last value specified`))
4079+
}
4080+
if key != v1.TopologyKeyAny {
4081+
for _, msg := range validation.IsQualifiedName(key) {
4082+
allErrs = append(allErrs, field.Invalid(keyPath, service.Spec.TopologyKeys, msg))
4083+
}
4084+
}
4085+
}
4086+
}
4087+
40594088
// Validate SourceRange field and annotation
40604089
_, ok := service.Annotations[core.AnnotationLoadBalancerSourceRangesKey]
40614090
if len(service.Spec.LoadBalancerSourceRanges) > 0 || ok {
@@ -4146,6 +4175,10 @@ func validateServiceExternalTrafficFieldsValue(service *core.Service) field.Erro
41464175
allErrs = append(allErrs, field.Invalid(field.NewPath("spec").Child("externalTrafficPolicy"), service.Spec.ExternalTrafficPolicy,
41474176
fmt.Sprintf("ExternalTrafficPolicy must be empty, %v or %v", core.ServiceExternalTrafficPolicyTypeCluster, core.ServiceExternalTrafficPolicyTypeLocal)))
41484177
}
4178+
// 'externalTrafficPolicy=Local' is mutually exclusive with topologyKeys
4179+
if service.Spec.ExternalTrafficPolicy == core.ServiceExternalTrafficPolicyTypeLocal && len(service.Spec.TopologyKeys) > 0 {
4180+
allErrs = append(allErrs, field.Forbidden(field.NewPath("spec").Child("externalTrafficPolicy"), "externalTrafficPolicy must not be set to 'Local' when topologyKeys is specified"))
4181+
}
41494182
if service.Spec.HealthCheckNodePort < 0 {
41504183
allErrs = append(allErrs, field.Invalid(field.NewPath("spec").Child("healthCheckNodePort"), service.Spec.HealthCheckNodePort,
41514184
"HealthCheckNodePort must be not less than 0"))

pkg/apis/core/validation/validation_test.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package validation
1818

1919
import (
2020
"bytes"
21+
"fmt"
2122
"math"
2223
"reflect"
2324
"strings"
@@ -9389,6 +9390,7 @@ func TestValidatePodEphemeralContainersUpdate(t *testing.T) {
93899390

93909391
func TestValidateService(t *testing.T) {
93919392
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.SCTPSupport, true)()
9393+
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.ServiceTopology, true)()
93929394

93939395
testCases := []struct {
93949396
name string
@@ -10067,6 +10069,66 @@ func TestValidateService(t *testing.T) {
1006710069
},
1006810070
numErrs: 1,
1006910071
},
10072+
{
10073+
name: "valid topology keys",
10074+
tweakSvc: func(s *core.Service) {
10075+
s.Spec.TopologyKeys = []string{
10076+
"kubernetes.io/hostname",
10077+
"failure-domain.beta.kubernetes.io/zone",
10078+
"failure-domain.beta.kubernetes.io/region",
10079+
v1.TopologyKeyAny,
10080+
}
10081+
},
10082+
numErrs: 0,
10083+
},
10084+
{
10085+
name: "invalid topology key",
10086+
tweakSvc: func(s *core.Service) {
10087+
s.Spec.TopologyKeys = []string{"NoUppercaseOrSpecialCharsLike=Equals"}
10088+
},
10089+
numErrs: 1,
10090+
},
10091+
{
10092+
name: "too many topology keys",
10093+
tweakSvc: func(s *core.Service) {
10094+
for i := 0; i < core.MaxServiceTopologyKeys+1; i++ {
10095+
s.Spec.TopologyKeys = append(s.Spec.TopologyKeys, fmt.Sprintf("topologykey-%d", i))
10096+
}
10097+
},
10098+
numErrs: 1,
10099+
},
10100+
{
10101+
name: `"Any" was not the last key`,
10102+
tweakSvc: func(s *core.Service) {
10103+
s.Spec.TopologyKeys = []string{
10104+
"kubernetes.io/hostname",
10105+
v1.TopologyKeyAny,
10106+
"failure-domain.beta.kubernetes.io/zone",
10107+
}
10108+
},
10109+
numErrs: 1,
10110+
},
10111+
{
10112+
name: `duplicate topology key`,
10113+
tweakSvc: func(s *core.Service) {
10114+
s.Spec.TopologyKeys = []string{
10115+
"kubernetes.io/hostname",
10116+
"kubernetes.io/hostname",
10117+
"failure-domain.beta.kubernetes.io/zone",
10118+
}
10119+
},
10120+
numErrs: 1,
10121+
},
10122+
{
10123+
name: `use topology keys with externalTrafficPolicy=Local`,
10124+
tweakSvc: func(s *core.Service) {
10125+
s.Spec.ExternalTrafficPolicy = "Local"
10126+
s.Spec.TopologyKeys = []string{
10127+
"kubernetes.io/hostname",
10128+
}
10129+
},
10130+
numErrs: 2,
10131+
},
1007010132
}
1007110133

1007210134
for _, tc := range testCases {

pkg/apis/core/zz_generated.deepcopy.go

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/features/kube_features.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -538,6 +538,12 @@ const (
538538
//
539539
// Enable all logic related to the PodDisruptionBudget API object in policy
540540
PodDisruptionBudget featuregate.Feature = "PodDisruptionBudget"
541+
542+
// owner: @m1093782566
543+
// alpha: v1.17
544+
//
545+
// Enables topology aware service routing
546+
ServiceTopology featuregate.Feature = "ServiceTopology"
541547
)
542548

543549
func init() {
@@ -623,6 +629,7 @@ var defaultKubernetesFeatureGates = map[featuregate.Feature]featuregate.FeatureS
623629
StartupProbe: {Default: false, PreRelease: featuregate.Alpha},
624630
AllowInsecureBackendProxy: {Default: true, PreRelease: featuregate.Beta},
625631
PodDisruptionBudget: {Default: true, PreRelease: featuregate.Beta},
632+
ServiceTopology: {Default: false, PreRelease: featuregate.Alpha},
626633

627634
// inherited features from generic apiserver, relisted here to get a conflict if it is changed
628635
// unintentionally on either side:

0 commit comments

Comments
 (0)