Adding EndpointSliceMirroring controller
This will mirror custom Endpoints to EndpointSlices to ensure that applications will not need to maintain both separately.
This commit is contained in:
parent
91bc902e20
commit
8691466059
97
pkg/controller/endpointslicemirroring/BUILD
Normal file
97
pkg/controller/endpointslicemirroring/BUILD
Normal file
@ -0,0 +1,97 @@
|
||||
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
|
||||
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = [
|
||||
"endpointset.go",
|
||||
"endpointslice_tracker.go",
|
||||
"endpointslicemirroring_controller.go",
|
||||
"events.go",
|
||||
"reconciler.go",
|
||||
"reconciler_helpers.go",
|
||||
"utils.go",
|
||||
],
|
||||
importpath = "k8s.io/kubernetes/pkg/controller/endpointslicemirroring",
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
"//pkg/apis/discovery/validation:go_default_library",
|
||||
"//pkg/controller:go_default_library",
|
||||
"//pkg/controller/endpointslicemirroring/metrics:go_default_library",
|
||||
"//pkg/controller/util/endpoint:go_default_library",
|
||||
"//staging/src/k8s.io/api/core/v1:go_default_library",
|
||||
"//staging/src/k8s.io/api/discovery/v1beta1:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/api/equality:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/api/errors:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/labels:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/types:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/util/errors:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/util/runtime:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/util/wait:go_default_library",
|
||||
"//staging/src/k8s.io/client-go/informers/core/v1:go_default_library",
|
||||
"//staging/src/k8s.io/client-go/informers/discovery/v1beta1:go_default_library",
|
||||
"//staging/src/k8s.io/client-go/kubernetes:go_default_library",
|
||||
"//staging/src/k8s.io/client-go/kubernetes/scheme:go_default_library",
|
||||
"//staging/src/k8s.io/client-go/kubernetes/typed/core/v1:go_default_library",
|
||||
"//staging/src/k8s.io/client-go/listers/core/v1:go_default_library",
|
||||
"//staging/src/k8s.io/client-go/listers/discovery/v1beta1:go_default_library",
|
||||
"//staging/src/k8s.io/client-go/tools/cache:go_default_library",
|
||||
"//staging/src/k8s.io/client-go/tools/leaderelection/resourcelock:go_default_library",
|
||||
"//staging/src/k8s.io/client-go/tools/record:go_default_library",
|
||||
"//staging/src/k8s.io/client-go/util/workqueue:go_default_library",
|
||||
"//staging/src/k8s.io/component-base/metrics/prometheus/ratelimiter:go_default_library",
|
||||
"//vendor/golang.org/x/time/rate:go_default_library",
|
||||
"//vendor/k8s.io/klog/v2:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
go_test(
|
||||
name = "go_default_test",
|
||||
srcs = [
|
||||
"endpointslice_tracker_test.go",
|
||||
"endpointslicemirroring_controller_test.go",
|
||||
"reconciler_helpers_test.go",
|
||||
"reconciler_test.go",
|
||||
"utils_test.go",
|
||||
],
|
||||
embed = [":go_default_library"],
|
||||
deps = [
|
||||
"//pkg/controller:go_default_library",
|
||||
"//pkg/controller/endpointslicemirroring/metrics:go_default_library",
|
||||
"//staging/src/k8s.io/api/core/v1:go_default_library",
|
||||
"//staging/src/k8s.io/api/discovery/v1beta1:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/util/rand:go_default_library",
|
||||
"//staging/src/k8s.io/client-go/informers:go_default_library",
|
||||
"//staging/src/k8s.io/client-go/kubernetes/fake:go_default_library",
|
||||
"//staging/src/k8s.io/client-go/kubernetes/scheme:go_default_library",
|
||||
"//staging/src/k8s.io/client-go/testing:go_default_library",
|
||||
"//staging/src/k8s.io/client-go/tools/cache:go_default_library",
|
||||
"//staging/src/k8s.io/client-go/tools/leaderelection/resourcelock:go_default_library",
|
||||
"//staging/src/k8s.io/client-go/tools/record:go_default_library",
|
||||
"//staging/src/k8s.io/component-base/metrics/testutil:go_default_library",
|
||||
"//vendor/github.com/stretchr/testify/assert:go_default_library",
|
||||
"//vendor/k8s.io/utils/pointer:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "package-srcs",
|
||||
srcs = glob(["**"]),
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:private"],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "all-srcs",
|
||||
srcs = [
|
||||
":package-srcs",
|
||||
"//pkg/controller/endpointslicemirroring/config:all-srcs",
|
||||
"//pkg/controller/endpointslicemirroring/metrics:all-srcs",
|
||||
],
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
12
pkg/controller/endpointslicemirroring/OWNERS
Normal file
12
pkg/controller/endpointslicemirroring/OWNERS
Normal file
@ -0,0 +1,12 @@
|
||||
# See the OWNERS docs at https://go.k8s.io/owners
|
||||
|
||||
approvers:
|
||||
- robscott
|
||||
- freehan
|
||||
- sig-network-approvers
|
||||
reviewers:
|
||||
- robscott
|
||||
- freehan
|
||||
- sig-network-reviewers
|
||||
labels:
|
||||
- sig/network
|
30
pkg/controller/endpointslicemirroring/config/BUILD
Normal file
30
pkg/controller/endpointslicemirroring/config/BUILD
Normal file
@ -0,0 +1,30 @@
|
||||
load("@io_bazel_rules_go//go:def.bzl", "go_library")
|
||||
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = [
|
||||
"doc.go",
|
||||
"types.go",
|
||||
"zz_generated.deepcopy.go",
|
||||
],
|
||||
importpath = "k8s.io/kubernetes/pkg/controller/endpointslicemirroring/config",
|
||||
visibility = ["//visibility:public"],
|
||||
deps = ["//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library"],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "package-srcs",
|
||||
srcs = glob(["**"]),
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:private"],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "all-srcs",
|
||||
srcs = [
|
||||
":package-srcs",
|
||||
"//pkg/controller/endpointslicemirroring/config/v1alpha1:all-srcs",
|
||||
],
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
19
pkg/controller/endpointslicemirroring/config/doc.go
Normal file
19
pkg/controller/endpointslicemirroring/config/doc.go
Normal file
@ -0,0 +1,19 @@
|
||||
/*
|
||||
Copyright 2020 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.
|
||||
*/
|
||||
|
||||
// +k8s:deepcopy-gen=package
|
||||
|
||||
package config // import "k8s.io/kubernetes/pkg/controller/endpointslicemirroring/config"
|
42
pkg/controller/endpointslicemirroring/config/types.go
Normal file
42
pkg/controller/endpointslicemirroring/config/types.go
Normal file
@ -0,0 +1,42 @@
|
||||
/*
|
||||
Copyright 2020 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 config
|
||||
|
||||
import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
// EndpointSliceMirroringControllerConfiguration contains elements describing
|
||||
// EndpointSliceMirroringController.
|
||||
type EndpointSliceMirroringControllerConfiguration struct {
|
||||
// mirroringConcurrentServiceEndpointSyncs is the number of service endpoint
|
||||
// syncing operations that will be done concurrently. Larger number = faster
|
||||
// endpoint slice updating, but more CPU (and network) load.
|
||||
MirroringConcurrentServiceEndpointSyncs int32
|
||||
|
||||
// mirroringMaxEndpointsPerSubset is the maximum number of endpoints that
|
||||
// will be mirrored to an EndpointSlice for an EndpointSubset.
|
||||
MirroringMaxEndpointsPerSubset int32
|
||||
|
||||
// mirroringEndpointUpdatesBatchPeriod can be used to batch EndpointSlice
|
||||
// updates. All updates triggered by EndpointSlice changes will be delayed
|
||||
// by up to 'mirroringEndpointUpdatesBatchPeriod'. If other addresses in the
|
||||
// same Endpoints resource change in that period, they will be batched to a
|
||||
// single EndpointSlice update. Default 0 value means that each Endpoints
|
||||
// update triggers an EndpointSlice update.
|
||||
MirroringEndpointUpdatesBatchPeriod metav1.Duration
|
||||
}
|
36
pkg/controller/endpointslicemirroring/config/v1alpha1/BUILD
Normal file
36
pkg/controller/endpointslicemirroring/config/v1alpha1/BUILD
Normal file
@ -0,0 +1,36 @@
|
||||
load("@io_bazel_rules_go//go:def.bzl", "go_library")
|
||||
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = [
|
||||
"conversion.go",
|
||||
"defaults.go",
|
||||
"doc.go",
|
||||
"register.go",
|
||||
"zz_generated.conversion.go",
|
||||
"zz_generated.deepcopy.go",
|
||||
],
|
||||
importpath = "k8s.io/kubernetes/pkg/controller/endpointslicemirroring/config/v1alpha1",
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
"//pkg/controller/endpointslicemirroring/config:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/conversion:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library",
|
||||
"//staging/src/k8s.io/kube-controller-manager/config/v1alpha1:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "package-srcs",
|
||||
srcs = glob(["**"]),
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:private"],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "all-srcs",
|
||||
srcs = [":package-srcs"],
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
@ -0,0 +1,40 @@
|
||||
/*
|
||||
Copyright 2020 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 v1alpha1
|
||||
|
||||
import (
|
||||
"k8s.io/apimachinery/pkg/conversion"
|
||||
"k8s.io/kube-controller-manager/config/v1alpha1"
|
||||
endpointslicemirroringconfig "k8s.io/kubernetes/pkg/controller/endpointslicemirroring/config"
|
||||
)
|
||||
|
||||
// Important! The public back-and-forth conversion functions for the types in
|
||||
// this package with EndpointSliceMirroringControllerConfiguratio types need to
|
||||
// be manually exposed like this in order for other packages that reference this
|
||||
// package to be able to call these conversion functions in an autogenerated
|
||||
// manner. TODO: Fix the bug in conversion-gen so it automatically discovers
|
||||
// these Convert_* functions in autogenerated code as well.
|
||||
|
||||
// Convert_v1alpha1_EndpointSliceMirroringControllerConfiguration_To_config_EndpointSliceMirroringControllerConfiguration is an autogenerated conversion function.
|
||||
func Convert_v1alpha1_EndpointSliceMirroringControllerConfiguration_To_config_EndpointSliceMirroringControllerConfiguration(in *v1alpha1.EndpointSliceMirroringControllerConfiguration, out *endpointslicemirroringconfig.EndpointSliceMirroringControllerConfiguration, s conversion.Scope) error {
|
||||
return autoConvert_v1alpha1_EndpointSliceMirroringControllerConfiguration_To_config_EndpointSliceMirroringControllerConfiguration(in, out, s)
|
||||
}
|
||||
|
||||
// Convert_config_EndpointSliceMirroringControllerConfiguration_To_v1alpha1_EndpointSliceMirroringControllerConfiguration is an autogenerated conversion function.
|
||||
func Convert_config_EndpointSliceMirroringControllerConfiguration_To_v1alpha1_EndpointSliceMirroringControllerConfiguration(in *endpointslicemirroringconfig.EndpointSliceMirroringControllerConfiguration, out *v1alpha1.EndpointSliceMirroringControllerConfiguration, s conversion.Scope) error {
|
||||
return autoConvert_config_EndpointSliceMirroringControllerConfiguration_To_v1alpha1_EndpointSliceMirroringControllerConfiguration(in, out, s)
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
/*
|
||||
Copyright 2020 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 v1alpha1
|
||||
|
||||
import (
|
||||
kubectrlmgrconfigv1alpha1 "k8s.io/kube-controller-manager/config/v1alpha1"
|
||||
)
|
||||
|
||||
// RecommendedDefaultEndpointSliceMirroringControllerConfiguration defaults a
|
||||
// pointer to a EndpointSliceMirroringControllerConfiguration struct. This will
|
||||
// set the recommended default values, but they may be subject to change between
|
||||
// API versions. This function is intentionally not registered in the scheme as
|
||||
// a "normal" `SetDefaults_Foo` function to allow consumers of this type to set
|
||||
// whatever defaults for their embedded configs. Forcing consumers to use these
|
||||
// defaults would be problematic as defaulting in the scheme is done as part of
|
||||
// the conversion, and there would be no easy way to opt-out. Instead, if you
|
||||
// want to use this defaulting method run it in your wrapper struct of this type
|
||||
// in its `SetDefaults_` method.
|
||||
func RecommendedDefaultEndpointSliceMirroringControllerConfiguration(obj *kubectrlmgrconfigv1alpha1.EndpointSliceMirroringControllerConfiguration) {
|
||||
if obj.MirroringConcurrentServiceEndpointSyncs == 0 {
|
||||
obj.MirroringConcurrentServiceEndpointSyncs = 5
|
||||
}
|
||||
|
||||
if obj.MirroringMaxEndpointsPerSubset == 0 {
|
||||
obj.MirroringMaxEndpointsPerSubset = 1000
|
||||
}
|
||||
}
|
21
pkg/controller/endpointslicemirroring/config/v1alpha1/doc.go
Normal file
21
pkg/controller/endpointslicemirroring/config/v1alpha1/doc.go
Normal file
@ -0,0 +1,21 @@
|
||||
/*
|
||||
Copyright 2020 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.
|
||||
*/
|
||||
|
||||
// +k8s:deepcopy-gen=package
|
||||
// +k8s:conversion-gen=k8s.io/kubernetes/pkg/controller/endpointslicemirroring/config
|
||||
// +k8s:conversion-gen-external-types=k8s.io/kube-controller-manager/config/v1alpha1
|
||||
|
||||
package v1alpha1 // import "k8s.io/kubernetes/pkg/controller/endpointslicemirroring/config/v1alpha1"
|
@ -0,0 +1,34 @@
|
||||
/*
|
||||
Copyright 2020 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 v1alpha1
|
||||
|
||||
import (
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
var (
|
||||
// SchemeBuilder is the scheme builder with scheme init functions to run for
|
||||
// this API package
|
||||
SchemeBuilder runtime.SchemeBuilder
|
||||
// localSchemeBuilder extends the SchemeBuilder instance with the external
|
||||
// types. In this package, defaulting and conversion init funcs are
|
||||
// registered as well.
|
||||
localSchemeBuilder = &SchemeBuilder
|
||||
// AddToScheme is a global function that registers this API group & version
|
||||
// to a scheme
|
||||
AddToScheme = localSchemeBuilder.AddToScheme
|
||||
)
|
95
pkg/controller/endpointslicemirroring/config/v1alpha1/zz_generated.conversion.go
generated
Normal file
95
pkg/controller/endpointslicemirroring/config/v1alpha1/zz_generated.conversion.go
generated
Normal file
@ -0,0 +1,95 @@
|
||||
// +build !ignore_autogenerated
|
||||
|
||||
/*
|
||||
Copyright The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// Code generated by conversion-gen. DO NOT EDIT.
|
||||
|
||||
package v1alpha1
|
||||
|
||||
import (
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
conversion "k8s.io/apimachinery/pkg/conversion"
|
||||
runtime "k8s.io/apimachinery/pkg/runtime"
|
||||
v1alpha1 "k8s.io/kube-controller-manager/config/v1alpha1"
|
||||
config "k8s.io/kubernetes/pkg/controller/endpointslicemirroring/config"
|
||||
)
|
||||
|
||||
func init() {
|
||||
localSchemeBuilder.Register(RegisterConversions)
|
||||
}
|
||||
|
||||
// RegisterConversions adds conversion functions to the given scheme.
|
||||
// Public to allow building arbitrary schemes.
|
||||
func RegisterConversions(s *runtime.Scheme) error {
|
||||
if err := s.AddGeneratedConversionFunc((*v1alpha1.GroupResource)(nil), (*v1.GroupResource)(nil), func(a, b interface{}, scope conversion.Scope) error {
|
||||
return Convert_v1alpha1_GroupResource_To_v1_GroupResource(a.(*v1alpha1.GroupResource), b.(*v1.GroupResource), scope)
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.AddGeneratedConversionFunc((*v1.GroupResource)(nil), (*v1alpha1.GroupResource)(nil), func(a, b interface{}, scope conversion.Scope) error {
|
||||
return Convert_v1_GroupResource_To_v1alpha1_GroupResource(a.(*v1.GroupResource), b.(*v1alpha1.GroupResource), scope)
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.AddConversionFunc((*config.EndpointSliceMirroringControllerConfiguration)(nil), (*v1alpha1.EndpointSliceMirroringControllerConfiguration)(nil), func(a, b interface{}, scope conversion.Scope) error {
|
||||
return Convert_config_EndpointSliceMirroringControllerConfiguration_To_v1alpha1_EndpointSliceMirroringControllerConfiguration(a.(*config.EndpointSliceMirroringControllerConfiguration), b.(*v1alpha1.EndpointSliceMirroringControllerConfiguration), scope)
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.AddConversionFunc((*v1alpha1.EndpointSliceMirroringControllerConfiguration)(nil), (*config.EndpointSliceMirroringControllerConfiguration)(nil), func(a, b interface{}, scope conversion.Scope) error {
|
||||
return Convert_v1alpha1_EndpointSliceMirroringControllerConfiguration_To_config_EndpointSliceMirroringControllerConfiguration(a.(*v1alpha1.EndpointSliceMirroringControllerConfiguration), b.(*config.EndpointSliceMirroringControllerConfiguration), scope)
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func autoConvert_v1alpha1_EndpointSliceMirroringControllerConfiguration_To_config_EndpointSliceMirroringControllerConfiguration(in *v1alpha1.EndpointSliceMirroringControllerConfiguration, out *config.EndpointSliceMirroringControllerConfiguration, s conversion.Scope) error {
|
||||
out.MirroringConcurrentServiceEndpointSyncs = in.MirroringConcurrentServiceEndpointSyncs
|
||||
out.MirroringMaxEndpointsPerSubset = in.MirroringMaxEndpointsPerSubset
|
||||
out.MirroringEndpointUpdatesBatchPeriod = in.MirroringEndpointUpdatesBatchPeriod
|
||||
return nil
|
||||
}
|
||||
|
||||
func autoConvert_config_EndpointSliceMirroringControllerConfiguration_To_v1alpha1_EndpointSliceMirroringControllerConfiguration(in *config.EndpointSliceMirroringControllerConfiguration, out *v1alpha1.EndpointSliceMirroringControllerConfiguration, s conversion.Scope) error {
|
||||
out.MirroringConcurrentServiceEndpointSyncs = in.MirroringConcurrentServiceEndpointSyncs
|
||||
out.MirroringMaxEndpointsPerSubset = in.MirroringMaxEndpointsPerSubset
|
||||
out.MirroringEndpointUpdatesBatchPeriod = in.MirroringEndpointUpdatesBatchPeriod
|
||||
return nil
|
||||
}
|
||||
|
||||
func autoConvert_v1alpha1_GroupResource_To_v1_GroupResource(in *v1alpha1.GroupResource, out *v1.GroupResource, s conversion.Scope) error {
|
||||
out.Group = in.Group
|
||||
out.Resource = in.Resource
|
||||
return nil
|
||||
}
|
||||
|
||||
// Convert_v1alpha1_GroupResource_To_v1_GroupResource is an autogenerated conversion function.
|
||||
func Convert_v1alpha1_GroupResource_To_v1_GroupResource(in *v1alpha1.GroupResource, out *v1.GroupResource, s conversion.Scope) error {
|
||||
return autoConvert_v1alpha1_GroupResource_To_v1_GroupResource(in, out, s)
|
||||
}
|
||||
|
||||
func autoConvert_v1_GroupResource_To_v1alpha1_GroupResource(in *v1.GroupResource, out *v1alpha1.GroupResource, s conversion.Scope) error {
|
||||
out.Group = in.Group
|
||||
out.Resource = in.Resource
|
||||
return nil
|
||||
}
|
||||
|
||||
// Convert_v1_GroupResource_To_v1alpha1_GroupResource is an autogenerated conversion function.
|
||||
func Convert_v1_GroupResource_To_v1alpha1_GroupResource(in *v1.GroupResource, out *v1alpha1.GroupResource, s conversion.Scope) error {
|
||||
return autoConvert_v1_GroupResource_To_v1alpha1_GroupResource(in, out, s)
|
||||
}
|
21
pkg/controller/endpointslicemirroring/config/v1alpha1/zz_generated.deepcopy.go
generated
Normal file
21
pkg/controller/endpointslicemirroring/config/v1alpha1/zz_generated.deepcopy.go
generated
Normal file
@ -0,0 +1,21 @@
|
||||
// +build !ignore_autogenerated
|
||||
|
||||
/*
|
||||
Copyright The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// Code generated by deepcopy-gen. DO NOT EDIT.
|
||||
|
||||
package v1alpha1
|
38
pkg/controller/endpointslicemirroring/config/zz_generated.deepcopy.go
generated
Normal file
38
pkg/controller/endpointslicemirroring/config/zz_generated.deepcopy.go
generated
Normal file
@ -0,0 +1,38 @@
|
||||
// +build !ignore_autogenerated
|
||||
|
||||
/*
|
||||
Copyright The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// Code generated by deepcopy-gen. DO NOT EDIT.
|
||||
|
||||
package config
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *EndpointSliceMirroringControllerConfiguration) DeepCopyInto(out *EndpointSliceMirroringControllerConfiguration) {
|
||||
*out = *in
|
||||
out.MirroringEndpointUpdatesBatchPeriod = in.MirroringEndpointUpdatesBatchPeriod
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EndpointSliceMirroringControllerConfiguration.
|
||||
func (in *EndpointSliceMirroringControllerConfiguration) DeepCopy() *EndpointSliceMirroringControllerConfiguration {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(EndpointSliceMirroringControllerConfiguration)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
96
pkg/controller/endpointslicemirroring/endpointset.go
Normal file
96
pkg/controller/endpointslicemirroring/endpointset.go
Normal file
@ -0,0 +1,96 @@
|
||||
/*
|
||||
Copyright 2020 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 endpointslicemirroring
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
discovery "k8s.io/api/discovery/v1beta1"
|
||||
endpointutil "k8s.io/kubernetes/pkg/controller/util/endpoint"
|
||||
)
|
||||
|
||||
// endpointHash is used to uniquely identify endpoints. Only including addresses
|
||||
// and hostnames as unique identifiers allows us to do more in place updates
|
||||
// should attributes such as topology, conditions, or targetRef change.
|
||||
type endpointHash string
|
||||
type endpointHashObj struct {
|
||||
Addresses []string
|
||||
Hostname string
|
||||
}
|
||||
|
||||
func hashEndpoint(endpoint *discovery.Endpoint) endpointHash {
|
||||
sort.Strings(endpoint.Addresses)
|
||||
hashObj := endpointHashObj{Addresses: endpoint.Addresses}
|
||||
if endpoint.Hostname != nil {
|
||||
hashObj.Hostname = *endpoint.Hostname
|
||||
}
|
||||
|
||||
return endpointHash(endpointutil.DeepHashObjectToString(hashObj))
|
||||
}
|
||||
|
||||
// endpointSet provides simple methods for comparing sets of Endpoints.
|
||||
type endpointSet map[endpointHash]*discovery.Endpoint
|
||||
|
||||
// Insert adds items to the set.
|
||||
func (s endpointSet) Insert(items ...*discovery.Endpoint) endpointSet {
|
||||
for _, item := range items {
|
||||
s[hashEndpoint(item)] = item
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// Delete removes all items from the set.
|
||||
func (s endpointSet) Delete(items ...*discovery.Endpoint) endpointSet {
|
||||
for _, item := range items {
|
||||
delete(s, hashEndpoint(item))
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// Has returns true if and only if item is contained in the set.
|
||||
func (s endpointSet) Has(item *discovery.Endpoint) bool {
|
||||
_, contained := s[hashEndpoint(item)]
|
||||
return contained
|
||||
}
|
||||
|
||||
// Returns an endpoint matching the hash if contained in the set.
|
||||
func (s endpointSet) Get(item *discovery.Endpoint) *discovery.Endpoint {
|
||||
return s[hashEndpoint(item)]
|
||||
}
|
||||
|
||||
// UnsortedList returns the slice with contents in random order.
|
||||
func (s endpointSet) UnsortedList() []*discovery.Endpoint {
|
||||
endpoints := make([]*discovery.Endpoint, 0, len(s))
|
||||
for _, endpoint := range s {
|
||||
endpoints = append(endpoints, endpoint)
|
||||
}
|
||||
return endpoints
|
||||
}
|
||||
|
||||
// Returns a single element from the set.
|
||||
func (s endpointSet) PopAny() (*discovery.Endpoint, bool) {
|
||||
for _, endpoint := range s {
|
||||
s.Delete(endpoint)
|
||||
return endpoint, true
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// Len returns the size of the set.
|
||||
func (s endpointSet) Len() int {
|
||||
return len(s)
|
||||
}
|
123
pkg/controller/endpointslicemirroring/endpointslice_tracker.go
Normal file
123
pkg/controller/endpointslicemirroring/endpointslice_tracker.go
Normal file
@ -0,0 +1,123 @@
|
||||
/*
|
||||
Copyright 2020 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 endpointslicemirroring
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
discovery "k8s.io/api/discovery/v1beta1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
)
|
||||
|
||||
// endpointSliceResourceVersions tracks expected EndpointSlice resource versions
|
||||
// by EndpointSlice name.
|
||||
type endpointSliceResourceVersions map[string]string
|
||||
|
||||
// endpointSliceTracker tracks EndpointSlices and their associated resource
|
||||
// versions to help determine if a change to an EndpointSlice has been processed
|
||||
// by the EndpointSlice controller.
|
||||
type endpointSliceTracker struct {
|
||||
// lock protects resourceVersionsByService.
|
||||
lock sync.Mutex
|
||||
// resourceVersionsByService tracks the list of EndpointSlices and
|
||||
// associated resource versions expected for a given Service.
|
||||
resourceVersionsByService map[types.NamespacedName]endpointSliceResourceVersions
|
||||
}
|
||||
|
||||
// newEndpointSliceTracker creates and initializes a new endpointSliceTracker.
|
||||
func newEndpointSliceTracker() *endpointSliceTracker {
|
||||
return &endpointSliceTracker{
|
||||
resourceVersionsByService: map[types.NamespacedName]endpointSliceResourceVersions{},
|
||||
}
|
||||
}
|
||||
|
||||
// has returns true if the endpointSliceTracker has a resource version for the
|
||||
// provided EndpointSlice.
|
||||
func (est *endpointSliceTracker) has(endpointSlice *discovery.EndpointSlice) bool {
|
||||
est.lock.Lock()
|
||||
defer est.lock.Unlock()
|
||||
|
||||
rrv := est.relatedResourceVersions(endpointSlice)
|
||||
_, ok := rrv[endpointSlice.Name]
|
||||
return ok
|
||||
}
|
||||
|
||||
// stale returns true if this endpointSliceTracker does not have a resource
|
||||
// version for the provided EndpointSlice or it does not match the resource
|
||||
// version of the provided EndpointSlice.
|
||||
func (est *endpointSliceTracker) stale(endpointSlice *discovery.EndpointSlice) bool {
|
||||
est.lock.Lock()
|
||||
defer est.lock.Unlock()
|
||||
|
||||
rrv := est.relatedResourceVersions(endpointSlice)
|
||||
return rrv[endpointSlice.Name] != endpointSlice.ResourceVersion
|
||||
}
|
||||
|
||||
// update adds or updates the resource version in this endpointSliceTracker for
|
||||
// the provided EndpointSlice.
|
||||
func (est *endpointSliceTracker) update(endpointSlice *discovery.EndpointSlice) {
|
||||
est.lock.Lock()
|
||||
defer est.lock.Unlock()
|
||||
|
||||
rrv := est.relatedResourceVersions(endpointSlice)
|
||||
rrv[endpointSlice.Name] = endpointSlice.ResourceVersion
|
||||
}
|
||||
|
||||
// delete removes the resource version in this endpointSliceTracker for the
|
||||
// provided EndpointSlice.
|
||||
func (est *endpointSliceTracker) delete(endpointSlice *discovery.EndpointSlice) {
|
||||
est.lock.Lock()
|
||||
defer est.lock.Unlock()
|
||||
|
||||
rrv := est.relatedResourceVersions(endpointSlice)
|
||||
delete(rrv, endpointSlice.Name)
|
||||
}
|
||||
|
||||
// relatedResourceVersions returns the set of resource versions tracked for the
|
||||
// Service corresponding to the provided EndpointSlice. If no resource versions
|
||||
// are currently tracked for this service, an empty set is initialized.
|
||||
func (est *endpointSliceTracker) relatedResourceVersions(endpointSlice *discovery.EndpointSlice) endpointSliceResourceVersions {
|
||||
serviceNN := getServiceNN(endpointSlice)
|
||||
vers, ok := est.resourceVersionsByService[serviceNN]
|
||||
|
||||
if !ok {
|
||||
vers = endpointSliceResourceVersions{}
|
||||
est.resourceVersionsByService[serviceNN] = vers
|
||||
}
|
||||
|
||||
return vers
|
||||
}
|
||||
|
||||
// getServiceNN returns a namespaced name for the Service corresponding to the
|
||||
// provided EndpointSlice.
|
||||
func getServiceNN(endpointSlice *discovery.EndpointSlice) types.NamespacedName {
|
||||
serviceName, _ := endpointSlice.Labels[discovery.LabelServiceName]
|
||||
return types.NamespacedName{Name: serviceName, Namespace: endpointSlice.Namespace}
|
||||
}
|
||||
|
||||
// managedByChanged returns true if one of the provided EndpointSlices is
|
||||
// managed by the EndpointSlice controller while the other is not.
|
||||
func managedByChanged(endpointSlice1, endpointSlice2 *discovery.EndpointSlice) bool {
|
||||
return managedByController(endpointSlice1) != managedByController(endpointSlice2)
|
||||
}
|
||||
|
||||
// managedByController returns true if the controller of the provided
|
||||
// EndpointSlices is the EndpointSlice controller.
|
||||
func managedByController(endpointSlice *discovery.EndpointSlice) bool {
|
||||
managedBy, _ := endpointSlice.Labels[discovery.LabelManagedBy]
|
||||
return managedBy == controllerName
|
||||
}
|
@ -0,0 +1,174 @@
|
||||
/*
|
||||
Copyright 2020 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 endpointslicemirroring
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
discovery "k8s.io/api/discovery/v1beta1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
func TestEndpointSliceTrackerUpdate(t *testing.T) {
|
||||
epSlice1 := &discovery.EndpointSlice{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "example-1",
|
||||
Namespace: "ns1",
|
||||
ResourceVersion: "rv1",
|
||||
Labels: map[string]string{discovery.LabelServiceName: "svc1"},
|
||||
},
|
||||
}
|
||||
|
||||
epSlice1DifferentNS := epSlice1.DeepCopy()
|
||||
epSlice1DifferentNS.Namespace = "ns2"
|
||||
|
||||
epSlice1DifferentService := epSlice1.DeepCopy()
|
||||
epSlice1DifferentService.Labels[discovery.LabelServiceName] = "svc2"
|
||||
|
||||
epSlice1DifferentRV := epSlice1.DeepCopy()
|
||||
epSlice1DifferentRV.ResourceVersion = "rv2"
|
||||
|
||||
testCases := map[string]struct {
|
||||
updateParam *discovery.EndpointSlice
|
||||
checksParam *discovery.EndpointSlice
|
||||
expectHas bool
|
||||
expectStale bool
|
||||
}{
|
||||
"same slice": {
|
||||
updateParam: epSlice1,
|
||||
checksParam: epSlice1,
|
||||
expectHas: true,
|
||||
expectStale: false,
|
||||
},
|
||||
"different namespace": {
|
||||
updateParam: epSlice1,
|
||||
checksParam: epSlice1DifferentNS,
|
||||
expectHas: false,
|
||||
expectStale: true,
|
||||
},
|
||||
"different service": {
|
||||
updateParam: epSlice1,
|
||||
checksParam: epSlice1DifferentService,
|
||||
expectHas: false,
|
||||
expectStale: true,
|
||||
},
|
||||
"different resource version": {
|
||||
updateParam: epSlice1,
|
||||
checksParam: epSlice1DifferentRV,
|
||||
expectHas: true,
|
||||
expectStale: true,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
esTracker := newEndpointSliceTracker()
|
||||
esTracker.update(tc.updateParam)
|
||||
if esTracker.has(tc.checksParam) != tc.expectHas {
|
||||
t.Errorf("tc.tracker.has(%+v) == %t, expected %t", tc.checksParam, esTracker.has(tc.checksParam), tc.expectHas)
|
||||
}
|
||||
if esTracker.stale(tc.checksParam) != tc.expectStale {
|
||||
t.Errorf("tc.tracker.stale(%+v) == %t, expected %t", tc.checksParam, esTracker.stale(tc.checksParam), tc.expectStale)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEndpointSliceTrackerDelete(t *testing.T) {
|
||||
epSlice1 := &discovery.EndpointSlice{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "example-1",
|
||||
Namespace: "ns1",
|
||||
ResourceVersion: "rv1",
|
||||
Labels: map[string]string{discovery.LabelServiceName: "svc1"},
|
||||
},
|
||||
}
|
||||
|
||||
epSlice1DifferentNS := epSlice1.DeepCopy()
|
||||
epSlice1DifferentNS.Namespace = "ns2"
|
||||
|
||||
epSlice1DifferentService := epSlice1.DeepCopy()
|
||||
epSlice1DifferentService.Labels[discovery.LabelServiceName] = "svc2"
|
||||
|
||||
epSlice1DifferentRV := epSlice1.DeepCopy()
|
||||
epSlice1DifferentRV.ResourceVersion = "rv2"
|
||||
|
||||
testCases := map[string]struct {
|
||||
deleteParam *discovery.EndpointSlice
|
||||
checksParam *discovery.EndpointSlice
|
||||
expectHas bool
|
||||
expectStale bool
|
||||
}{
|
||||
"same slice": {
|
||||
deleteParam: epSlice1,
|
||||
checksParam: epSlice1,
|
||||
expectHas: false,
|
||||
expectStale: true,
|
||||
},
|
||||
"different namespace": {
|
||||
deleteParam: epSlice1DifferentNS,
|
||||
checksParam: epSlice1DifferentNS,
|
||||
expectHas: false,
|
||||
expectStale: true,
|
||||
},
|
||||
"different namespace, check original ep slice": {
|
||||
deleteParam: epSlice1DifferentNS,
|
||||
checksParam: epSlice1,
|
||||
expectHas: true,
|
||||
expectStale: false,
|
||||
},
|
||||
"different service": {
|
||||
deleteParam: epSlice1DifferentService,
|
||||
checksParam: epSlice1DifferentService,
|
||||
expectHas: false,
|
||||
expectStale: true,
|
||||
},
|
||||
"different service, check original ep slice": {
|
||||
deleteParam: epSlice1DifferentService,
|
||||
checksParam: epSlice1,
|
||||
expectHas: true,
|
||||
expectStale: false,
|
||||
},
|
||||
"different resource version": {
|
||||
deleteParam: epSlice1DifferentRV,
|
||||
checksParam: epSlice1DifferentRV,
|
||||
expectHas: false,
|
||||
expectStale: true,
|
||||
},
|
||||
"different resource version, check original ep slice": {
|
||||
deleteParam: epSlice1DifferentRV,
|
||||
checksParam: epSlice1,
|
||||
expectHas: false,
|
||||
expectStale: true,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
esTracker := newEndpointSliceTracker()
|
||||
esTracker.update(epSlice1)
|
||||
|
||||
esTracker.delete(tc.deleteParam)
|
||||
if esTracker.has(tc.checksParam) != tc.expectHas {
|
||||
t.Errorf("esTracker.has(%+v) == %t, expected %t", tc.checksParam, esTracker.has(tc.checksParam), tc.expectHas)
|
||||
}
|
||||
if esTracker.stale(tc.checksParam) != tc.expectStale {
|
||||
t.Errorf("esTracker.stale(%+v) == %t, expected %t", tc.checksParam, esTracker.stale(tc.checksParam), tc.expectStale)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -0,0 +1,446 @@
|
||||
/*
|
||||
Copyright 2020 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 endpointslicemirroring
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"golang.org/x/time/rate"
|
||||
|
||||
v1 "k8s.io/api/core/v1"
|
||||
discovery "k8s.io/api/discovery/v1beta1"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
coreinformers "k8s.io/client-go/informers/core/v1"
|
||||
discoveryinformers "k8s.io/client-go/informers/discovery/v1beta1"
|
||||
clientset "k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/kubernetes/scheme"
|
||||
v1core "k8s.io/client-go/kubernetes/typed/core/v1"
|
||||
corelisters "k8s.io/client-go/listers/core/v1"
|
||||
discoverylisters "k8s.io/client-go/listers/discovery/v1beta1"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
"k8s.io/client-go/tools/record"
|
||||
"k8s.io/client-go/util/workqueue"
|
||||
"k8s.io/component-base/metrics/prometheus/ratelimiter"
|
||||
"k8s.io/klog/v2"
|
||||
"k8s.io/kubernetes/pkg/controller"
|
||||
"k8s.io/kubernetes/pkg/controller/endpointslicemirroring/metrics"
|
||||
)
|
||||
|
||||
const (
|
||||
// maxRetries is the number of times an Endpoints resource will be retried
|
||||
// before it is dropped out of the queue. Any sync error, such as a failure
|
||||
// to create or update an EndpointSlice could trigger a retry. With the
|
||||
// current rate-limiter in use (1s*2^(numRetries-1)) up to a max of 100s.
|
||||
// The following numbers represent the sequence of delays between successive
|
||||
// queuings of an Endpoints resource.
|
||||
//
|
||||
// 1s, 2s, 4s, 8s, 16s, 32s, 64s, 100s (max)
|
||||
maxRetries = 15
|
||||
|
||||
// defaultSyncBackOff is the default backoff period for syncEndpoints calls.
|
||||
defaultSyncBackOff = 1 * time.Second
|
||||
// maxSyncBackOff is the max backoff period for syncEndpoints calls.
|
||||
maxSyncBackOff = 100 * time.Second
|
||||
|
||||
// controllerName is a unique value used with LabelManagedBy to indicated
|
||||
// the component managing an EndpointSlice.
|
||||
controllerName = "endpointslicemirroring-controller.k8s.io"
|
||||
)
|
||||
|
||||
// NewController creates and initializes a new Controller
|
||||
func NewController(endpointsInformer coreinformers.EndpointsInformer,
|
||||
endpointSliceInformer discoveryinformers.EndpointSliceInformer,
|
||||
serviceInformer coreinformers.ServiceInformer,
|
||||
maxEndpointsPerSubset int32,
|
||||
client clientset.Interface,
|
||||
endpointUpdatesBatchPeriod time.Duration,
|
||||
) *Controller {
|
||||
broadcaster := record.NewBroadcaster()
|
||||
broadcaster.StartLogging(klog.Infof)
|
||||
broadcaster.StartRecordingToSink(&v1core.EventSinkImpl{Interface: client.CoreV1().Events("")})
|
||||
recorder := broadcaster.NewRecorder(scheme.Scheme, v1.EventSource{Component: "endpoint-slice-mirroring-controller"})
|
||||
|
||||
if client != nil && client.CoreV1().RESTClient().GetRateLimiter() != nil {
|
||||
ratelimiter.RegisterMetricAndTrackRateLimiterUsage("endpoint_slice_mirroring_controller", client.DiscoveryV1beta1().RESTClient().GetRateLimiter())
|
||||
}
|
||||
|
||||
metrics.RegisterMetrics()
|
||||
|
||||
c := &Controller{
|
||||
client: client,
|
||||
// This is similar to the DefaultControllerRateLimiter, just with a
|
||||
// significantly higher default backoff (1s vs 5ms). This controller
|
||||
// processes events that can require significant EndpointSlice changes.
|
||||
// A more significant rate limit back off here helps ensure that the
|
||||
// Controller does not overwhelm the API Server.
|
||||
queue: workqueue.NewNamedRateLimitingQueue(workqueue.NewMaxOfRateLimiter(
|
||||
workqueue.NewItemExponentialFailureRateLimiter(defaultSyncBackOff, maxSyncBackOff),
|
||||
// 10 qps, 100 bucket size. This is only for retry speed and its
|
||||
// only the overall factor (not per item).
|
||||
&workqueue.BucketRateLimiter{Limiter: rate.NewLimiter(rate.Limit(10), 100)},
|
||||
), "endpoint_slice_mirroring"),
|
||||
workerLoopPeriod: time.Second,
|
||||
}
|
||||
|
||||
endpointsInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
|
||||
AddFunc: c.onEndpointsAdd,
|
||||
UpdateFunc: c.onEndpointsUpdate,
|
||||
DeleteFunc: c.onEndpointsDelete,
|
||||
})
|
||||
c.endpointsLister = endpointsInformer.Lister()
|
||||
c.endpointsSynced = endpointsInformer.Informer().HasSynced
|
||||
|
||||
endpointSliceInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
|
||||
AddFunc: c.onEndpointSliceAdd,
|
||||
UpdateFunc: c.onEndpointSliceUpdate,
|
||||
DeleteFunc: c.onEndpointSliceDelete,
|
||||
})
|
||||
|
||||
c.endpointSliceLister = endpointSliceInformer.Lister()
|
||||
c.endpointSlicesSynced = endpointSliceInformer.Informer().HasSynced
|
||||
c.endpointSliceTracker = newEndpointSliceTracker()
|
||||
|
||||
c.serviceLister = serviceInformer.Lister()
|
||||
c.servicesSynced = serviceInformer.Informer().HasSynced
|
||||
|
||||
c.maxEndpointsPerSubset = maxEndpointsPerSubset
|
||||
|
||||
c.reconciler = &reconciler{
|
||||
client: c.client,
|
||||
maxEndpointsPerSubset: c.maxEndpointsPerSubset,
|
||||
endpointSliceTracker: c.endpointSliceTracker,
|
||||
metricsCache: metrics.NewCache(maxEndpointsPerSubset),
|
||||
eventRecorder: recorder,
|
||||
}
|
||||
|
||||
c.eventBroadcaster = broadcaster
|
||||
c.eventRecorder = recorder
|
||||
|
||||
c.endpointUpdatesBatchPeriod = endpointUpdatesBatchPeriod
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
// Controller manages selector-based service endpoint slices
|
||||
type Controller struct {
|
||||
client clientset.Interface
|
||||
eventBroadcaster record.EventBroadcaster
|
||||
eventRecorder record.EventRecorder
|
||||
|
||||
// endpointsLister is able to list/get endpoints and is populated by the
|
||||
// shared informer passed to NewController.
|
||||
endpointsLister corelisters.EndpointsLister
|
||||
// endpointsSynced returns true if the endpoints shared informer has been
|
||||
// synced at least once. Added as a member to the struct to allow injection
|
||||
// for testing.
|
||||
endpointsSynced cache.InformerSynced
|
||||
|
||||
// endpointSliceLister is able to list/get endpoint slices and is populated
|
||||
// by the shared informer passed to NewController
|
||||
endpointSliceLister discoverylisters.EndpointSliceLister
|
||||
// endpointSlicesSynced returns true if the endpoint slice shared informer
|
||||
// has been synced at least once. Added as a member to the struct to allow
|
||||
// injection for testing.
|
||||
endpointSlicesSynced cache.InformerSynced
|
||||
// endpointSliceTracker tracks the list of EndpointSlices and associated
|
||||
// resource versions expected for each Endpoints resource. It can help
|
||||
// determine if a cached EndpointSlice is out of date.
|
||||
endpointSliceTracker *endpointSliceTracker
|
||||
|
||||
// serviceLister is able to list/get services and is populated by the shared
|
||||
// informer passed to NewController.
|
||||
serviceLister corelisters.ServiceLister
|
||||
// servicesSynced returns true if the services shared informer has been
|
||||
// synced at least once. Added as a member to the struct to allow injection
|
||||
// for testing.
|
||||
servicesSynced cache.InformerSynced
|
||||
|
||||
// reconciler is an util used to reconcile EndpointSlice changes.
|
||||
reconciler *reconciler
|
||||
|
||||
// Endpoints that need to be updated. A channel is inappropriate here,
|
||||
// because it allows Endpoints with lots of addresses to be serviced much
|
||||
// more often than Endpoints with few addresses; it also would cause an
|
||||
// Endpoints resource that's inserted multiple times to be processed more
|
||||
// than necessary.
|
||||
queue workqueue.RateLimitingInterface
|
||||
|
||||
// maxEndpointsPerSubset references the maximum number of endpoints that
|
||||
// should be added to an EndpointSlice for an EndpointSubset.
|
||||
maxEndpointsPerSubset int32
|
||||
|
||||
// workerLoopPeriod is the time between worker runs. The workers process the
|
||||
// queue of changes to Endpoints resources.
|
||||
workerLoopPeriod time.Duration
|
||||
|
||||
// endpointUpdatesBatchPeriod is an artificial delay added to all Endpoints
|
||||
// syncs triggered by EndpointSlice changes. This can be used to reduce
|
||||
// overall number of all EndpointSlice updates.
|
||||
endpointUpdatesBatchPeriod time.Duration
|
||||
}
|
||||
|
||||
// Run will not return until stopCh is closed.
|
||||
func (c *Controller) Run(workers int, stopCh <-chan struct{}) {
|
||||
defer utilruntime.HandleCrash()
|
||||
defer c.queue.ShutDown()
|
||||
|
||||
klog.Infof("Starting EndpointSliceMirroring controller")
|
||||
defer klog.Infof("Shutting down EndpointSliceMirroring controller")
|
||||
|
||||
if !cache.WaitForNamedCacheSync("endpoint_slice_mirroring", stopCh, c.endpointsSynced, c.endpointSlicesSynced, c.servicesSynced) {
|
||||
return
|
||||
}
|
||||
|
||||
klog.V(2).Infof("Starting %d worker threads", workers)
|
||||
for i := 0; i < workers; i++ {
|
||||
go wait.Until(c.worker, c.workerLoopPeriod, stopCh)
|
||||
}
|
||||
|
||||
<-stopCh
|
||||
}
|
||||
|
||||
// worker runs a worker thread that just dequeues items, processes them, and
|
||||
// marks them done. You may run as many of these in parallel as you wish; the
|
||||
// workqueue guarantees that they will not end up processing the same service
|
||||
// at the same time
|
||||
func (c *Controller) worker() {
|
||||
for c.processNextWorkItem() {
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Controller) processNextWorkItem() bool {
|
||||
cKey, quit := c.queue.Get()
|
||||
if quit {
|
||||
return false
|
||||
}
|
||||
defer c.queue.Done(cKey)
|
||||
|
||||
err := c.syncEndpoints(cKey.(string))
|
||||
c.handleErr(err, cKey)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (c *Controller) handleErr(err error, key interface{}) {
|
||||
if err == nil {
|
||||
c.queue.Forget(key)
|
||||
return
|
||||
}
|
||||
|
||||
if c.queue.NumRequeues(key) < maxRetries {
|
||||
klog.Warningf("Error mirroring EndpointSlices for %q Endpoints, retrying. Error: %v", key, err)
|
||||
c.queue.AddRateLimited(key)
|
||||
return
|
||||
}
|
||||
|
||||
klog.Warningf("Retry budget exceeded, dropping %q Endpoints out of the queue: %v", key, err)
|
||||
c.queue.Forget(key)
|
||||
utilruntime.HandleError(err)
|
||||
}
|
||||
|
||||
func (c *Controller) syncEndpoints(key string) error {
|
||||
startTime := time.Now()
|
||||
defer func() {
|
||||
syncDuration := float64(time.Since(startTime).Milliseconds()) / 1000
|
||||
metrics.EndpointsSyncDuration.WithLabelValues().Observe(syncDuration)
|
||||
klog.V(4).Infof("Finished syncing EndpointSlices for %q Endpoints. (%v)", key, time.Since(startTime))
|
||||
}()
|
||||
|
||||
klog.V(4).Infof("syncEndpoints(%q)", key)
|
||||
|
||||
namespace, name, err := cache.SplitMetaNamespaceKey(key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
endpointSlices, err := endpointSlicesMirroredForService(c.endpointSliceLister, namespace, name)
|
||||
|
||||
if err != nil {
|
||||
ep := &v1.Endpoints{ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: namespace}}
|
||||
c.eventRecorder.Eventf(ep, FailedToListEndpointSlices,
|
||||
"Error listing EndpointSlices for Endpoints %s/%s: %v", ep.Namespace, ep.Name, err)
|
||||
return err
|
||||
}
|
||||
|
||||
endpoints, err := c.endpointsLister.Endpoints(namespace).Get(name)
|
||||
if err != nil || !c.shouldMirror(endpoints) {
|
||||
if apierrors.IsNotFound(err) || !c.shouldMirror(endpoints) {
|
||||
return c.reconciler.deleteEndpoints(namespace, name, endpointSlices)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
err = c.reconciler.reconcile(endpoints, endpointSlices)
|
||||
if err != nil {
|
||||
c.eventRecorder.Eventf(endpoints, v1.EventTypeWarning, FailedToUpdateEndpointSlices,
|
||||
"Error updating EndpointSlices for Endpoints %s/%s: %v", endpoints.Namespace, endpoints.Name, err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// queueEndpoints queues the Endpoints resource for processing.
|
||||
func (c *Controller) queueEndpoints(obj interface{}) {
|
||||
key, err := controller.KeyFunc(obj)
|
||||
if err != nil {
|
||||
utilruntime.HandleError(fmt.Errorf("Couldn't get key for object %+v (type %T): %v", obj, obj, err))
|
||||
return
|
||||
}
|
||||
|
||||
c.queue.Add(key)
|
||||
}
|
||||
|
||||
// shouldMirror returns true if an Endpoints resource should be mirrored by this
|
||||
// controller. This will be false if:
|
||||
// - the Endpoints resource has a skip-mirror label.
|
||||
// - the Endpoints resource has a leader election annotation.
|
||||
// - the corresponding Service resource does not exist.
|
||||
// - the corresponding Service resource has a non-nil selector.
|
||||
func (c *Controller) shouldMirror(endpoints *v1.Endpoints) bool {
|
||||
if endpoints == nil || skipMirror(endpoints.Labels) || hasLeaderElection(endpoints.Annotations) {
|
||||
return false
|
||||
}
|
||||
|
||||
svc, err := c.serviceLister.Services(endpoints.Namespace).Get(endpoints.Name)
|
||||
if err != nil {
|
||||
if !apierrors.IsNotFound(err) {
|
||||
klog.Errorf("Error fetching %s/%s Service: %v", endpoints.Namespace, endpoints.Name, err)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
if svc.Spec.Selector != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// onEndpointsAdd queues a sync for the relevant Endpoints resource.
|
||||
func (c *Controller) onEndpointsAdd(obj interface{}) {
|
||||
endpoints := obj.(*v1.Endpoints)
|
||||
if endpoints == nil {
|
||||
utilruntime.HandleError(fmt.Errorf("onEndpointsAdd() expected type v1.Endpoints, got %T", obj))
|
||||
return
|
||||
}
|
||||
if !c.shouldMirror(endpoints) {
|
||||
klog.V(5).Infof("Skipping mirroring for %s/%s", endpoints.Namespace, endpoints.Name)
|
||||
return
|
||||
}
|
||||
c.queueEndpoints(obj)
|
||||
}
|
||||
|
||||
// onEndpointsUpdate queues a sync for the relevant Endpoints resource.
|
||||
func (c *Controller) onEndpointsUpdate(prevObj, obj interface{}) {
|
||||
endpoints := obj.(*v1.Endpoints)
|
||||
prevEndpoints := prevObj.(*v1.Endpoints)
|
||||
if endpoints == nil || prevEndpoints == nil {
|
||||
utilruntime.HandleError(fmt.Errorf("onEndpointsUpdate() expected type v1.Endpoints, got %T, %T", prevObj, obj))
|
||||
return
|
||||
}
|
||||
if !c.shouldMirror(endpoints) && !c.shouldMirror(prevEndpoints) {
|
||||
klog.V(5).Infof("Skipping mirroring for %s/%s", endpoints.Namespace, endpoints.Name)
|
||||
return
|
||||
}
|
||||
c.queueEndpoints(obj)
|
||||
}
|
||||
|
||||
// onEndpointsDelete queues a sync for the relevant Endpoints resource.
|
||||
func (c *Controller) onEndpointsDelete(obj interface{}) {
|
||||
endpoints := getEndpointsFromDeleteAction(obj)
|
||||
if endpoints == nil {
|
||||
utilruntime.HandleError(fmt.Errorf("onEndpointsDelete() expected type v1.Endpoints, got %T", obj))
|
||||
return
|
||||
}
|
||||
if !c.shouldMirror(endpoints) {
|
||||
klog.V(5).Infof("Skipping mirroring for %s/%s", endpoints.Namespace, endpoints.Name)
|
||||
return
|
||||
}
|
||||
c.queueEndpoints(obj)
|
||||
}
|
||||
|
||||
// onEndpointSliceAdd queues a sync for the relevant Endpoints resource for a
|
||||
// sync if the EndpointSlice resource version does not match the expected
|
||||
// version in the endpointSliceTracker.
|
||||
func (c *Controller) onEndpointSliceAdd(obj interface{}) {
|
||||
endpointSlice := obj.(*discovery.EndpointSlice)
|
||||
if endpointSlice == nil {
|
||||
utilruntime.HandleError(fmt.Errorf("onEndpointSliceAdd() expected type discovery.EndpointSlice, got %T", obj))
|
||||
return
|
||||
}
|
||||
if managedByController(endpointSlice) && c.endpointSliceTracker.stale(endpointSlice) {
|
||||
c.queueEndpointsForEndpointSlice(endpointSlice)
|
||||
}
|
||||
}
|
||||
|
||||
// onEndpointSliceUpdate queues a sync for the relevant Endpoints resource for a
|
||||
// sync if the EndpointSlice resource version does not match the expected
|
||||
// version in the endpointSliceTracker or the managed-by value of the
|
||||
// EndpointSlice has changed from or to this controller.
|
||||
func (c *Controller) onEndpointSliceUpdate(prevObj, obj interface{}) {
|
||||
prevEndpointSlice := obj.(*discovery.EndpointSlice)
|
||||
endpointSlice := prevObj.(*discovery.EndpointSlice)
|
||||
if endpointSlice == nil || prevEndpointSlice == nil {
|
||||
utilruntime.HandleError(fmt.Errorf("onEndpointSliceUpdated() expected type discovery.EndpointSlice, got %T, %T", prevObj, obj))
|
||||
return
|
||||
}
|
||||
if managedByChanged(prevEndpointSlice, endpointSlice) || (managedByController(endpointSlice) && c.endpointSliceTracker.stale(endpointSlice)) {
|
||||
c.queueEndpointsForEndpointSlice(endpointSlice)
|
||||
}
|
||||
}
|
||||
|
||||
// onEndpointSliceDelete queues a sync for the relevant Endpoints resource for a
|
||||
// sync if the EndpointSlice resource version does not match the expected
|
||||
// version in the endpointSliceTracker.
|
||||
func (c *Controller) onEndpointSliceDelete(obj interface{}) {
|
||||
endpointSlice := getEndpointSliceFromDeleteAction(obj)
|
||||
if endpointSlice == nil {
|
||||
utilruntime.HandleError(fmt.Errorf("onEndpointSliceDelete() expected type discovery.EndpointSlice, got %T", obj))
|
||||
return
|
||||
}
|
||||
if managedByController(endpointSlice) && c.endpointSliceTracker.has(endpointSlice) {
|
||||
c.queueEndpointsForEndpointSlice(endpointSlice)
|
||||
}
|
||||
}
|
||||
|
||||
// queueEndpointsForEndpointSlice attempts to queue the corresponding Endpoints
|
||||
// resource for the provided EndpointSlice.
|
||||
func (c *Controller) queueEndpointsForEndpointSlice(endpointSlice *discovery.EndpointSlice) {
|
||||
key, err := endpointsControllerKey(endpointSlice)
|
||||
if err != nil {
|
||||
utilruntime.HandleError(fmt.Errorf("Couldn't get key for EndpointSlice %+v (type %T): %v", endpointSlice, endpointSlice, err))
|
||||
return
|
||||
}
|
||||
|
||||
c.queue.AddAfter(key, c.endpointUpdatesBatchPeriod)
|
||||
}
|
||||
|
||||
// endpointSlicesMirroredForService returns the EndpointSlices that have been
|
||||
// mirrored for a Service by this controller.
|
||||
func endpointSlicesMirroredForService(endpointSliceLister discoverylisters.EndpointSliceLister, namespace, name string) ([]*discovery.EndpointSlice, error) {
|
||||
esLabelSelector := labels.Set(map[string]string{
|
||||
discovery.LabelServiceName: name,
|
||||
discovery.LabelManagedBy: controllerName,
|
||||
}).AsSelectorPreValidated()
|
||||
return endpointSliceLister.EndpointSlices(namespace).List(esLabelSelector)
|
||||
}
|
@ -0,0 +1,479 @@
|
||||
/*
|
||||
Copyright 2020 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 endpointslicemirroring
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
v1 "k8s.io/api/core/v1"
|
||||
discovery "k8s.io/api/discovery/v1beta1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/informers"
|
||||
"k8s.io/client-go/kubernetes/fake"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
"k8s.io/client-go/tools/leaderelection/resourcelock"
|
||||
"k8s.io/kubernetes/pkg/controller"
|
||||
)
|
||||
|
||||
// Most of the tests related to EndpointSlice allocation can be found in reconciler_test.go
|
||||
// Tests here primarily focus on unique controller functionality before the reconciler begins
|
||||
|
||||
var alwaysReady = func() bool { return true }
|
||||
|
||||
type endpointSliceMirroringController struct {
|
||||
*Controller
|
||||
endpointsStore cache.Store
|
||||
endpointSliceStore cache.Store
|
||||
serviceStore cache.Store
|
||||
}
|
||||
|
||||
func newController(batchPeriod time.Duration) (*fake.Clientset, *endpointSliceMirroringController) {
|
||||
client := newClientset()
|
||||
informerFactory := informers.NewSharedInformerFactory(client, controller.NoResyncPeriodFunc())
|
||||
|
||||
esController := NewController(
|
||||
informerFactory.Core().V1().Endpoints(),
|
||||
informerFactory.Discovery().V1beta1().EndpointSlices(),
|
||||
informerFactory.Core().V1().Services(),
|
||||
int32(1000),
|
||||
client,
|
||||
batchPeriod)
|
||||
|
||||
esController.endpointsSynced = alwaysReady
|
||||
esController.endpointSlicesSynced = alwaysReady
|
||||
esController.servicesSynced = alwaysReady
|
||||
|
||||
return client, &endpointSliceMirroringController{
|
||||
esController,
|
||||
informerFactory.Core().V1().Endpoints().Informer().GetStore(),
|
||||
informerFactory.Discovery().V1beta1().EndpointSlices().Informer().GetStore(),
|
||||
informerFactory.Core().V1().Services().Informer().GetStore(),
|
||||
}
|
||||
}
|
||||
|
||||
func TestSyncEndpoints(t *testing.T) {
|
||||
endpointsName := "testing-sync-endpoints"
|
||||
namespace := metav1.NamespaceDefault
|
||||
|
||||
testCases := []struct {
|
||||
testName string
|
||||
endpoints *v1.Endpoints
|
||||
endpointSlices []*discovery.EndpointSlice
|
||||
expectedNumActions int
|
||||
expectedNumSlices int
|
||||
}{{
|
||||
testName: "Endpoints with no addresses",
|
||||
endpoints: &v1.Endpoints{
|
||||
Subsets: []v1.EndpointSubset{{
|
||||
Ports: []v1.EndpointPort{{Port: 80}},
|
||||
}},
|
||||
},
|
||||
endpointSlices: []*discovery.EndpointSlice{},
|
||||
expectedNumActions: 0,
|
||||
expectedNumSlices: 0,
|
||||
}, {
|
||||
testName: "Endpoints with skip label true",
|
||||
endpoints: &v1.Endpoints{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Labels: map[string]string{discovery.LabelSkipMirror: "true"},
|
||||
},
|
||||
Subsets: []v1.EndpointSubset{{
|
||||
Ports: []v1.EndpointPort{{Port: 80}},
|
||||
Addresses: []v1.EndpointAddress{{IP: "10.0.0.1"}},
|
||||
}},
|
||||
},
|
||||
endpointSlices: []*discovery.EndpointSlice{},
|
||||
expectedNumActions: 0,
|
||||
expectedNumSlices: 0,
|
||||
}, {
|
||||
testName: "Endpoints with skip label false",
|
||||
endpoints: &v1.Endpoints{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Labels: map[string]string{discovery.LabelSkipMirror: "false"},
|
||||
},
|
||||
Subsets: []v1.EndpointSubset{{
|
||||
Ports: []v1.EndpointPort{{Port: 80}},
|
||||
Addresses: []v1.EndpointAddress{{IP: "10.0.0.1"}},
|
||||
}},
|
||||
},
|
||||
endpointSlices: []*discovery.EndpointSlice{},
|
||||
expectedNumActions: 1,
|
||||
expectedNumSlices: 1,
|
||||
}, {
|
||||
testName: "Existing EndpointSlices that need to be cleaned up",
|
||||
endpoints: &v1.Endpoints{
|
||||
Subsets: []v1.EndpointSubset{{
|
||||
Ports: []v1.EndpointPort{{Port: 80}},
|
||||
}},
|
||||
},
|
||||
endpointSlices: []*discovery.EndpointSlice{{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: endpointsName + "-1",
|
||||
Labels: map[string]string{
|
||||
discovery.LabelServiceName: endpointsName,
|
||||
discovery.LabelManagedBy: controllerName,
|
||||
},
|
||||
},
|
||||
}},
|
||||
expectedNumActions: 1,
|
||||
expectedNumSlices: 0,
|
||||
}, {
|
||||
testName: "Existing EndpointSlices managed by a different controller, no addresses to sync",
|
||||
endpoints: &v1.Endpoints{
|
||||
Subsets: []v1.EndpointSubset{{
|
||||
Ports: []v1.EndpointPort{{Port: 80}},
|
||||
}},
|
||||
},
|
||||
endpointSlices: []*discovery.EndpointSlice{{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: endpointsName + "-1",
|
||||
Labels: map[string]string{
|
||||
discovery.LabelManagedBy: "something-else",
|
||||
},
|
||||
},
|
||||
}},
|
||||
expectedNumActions: 0,
|
||||
// This only queries for EndpointSlices managed by this controller.
|
||||
expectedNumSlices: 0,
|
||||
}, {
|
||||
testName: "Endpoints with 1000 addresses",
|
||||
endpoints: &v1.Endpoints{
|
||||
Subsets: []v1.EndpointSubset{{
|
||||
Ports: []v1.EndpointPort{{Port: 80}},
|
||||
Addresses: generateAddresses(1000),
|
||||
}},
|
||||
},
|
||||
endpointSlices: []*discovery.EndpointSlice{},
|
||||
expectedNumActions: 1,
|
||||
expectedNumSlices: 1,
|
||||
}, {
|
||||
testName: "Endpoints with 1001 addresses - 1 should not be mirrored",
|
||||
endpoints: &v1.Endpoints{
|
||||
Subsets: []v1.EndpointSubset{{
|
||||
Ports: []v1.EndpointPort{{Port: 80}},
|
||||
Addresses: generateAddresses(1001),
|
||||
}},
|
||||
},
|
||||
endpointSlices: []*discovery.EndpointSlice{},
|
||||
expectedNumActions: 1,
|
||||
expectedNumSlices: 1,
|
||||
}}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.testName, func(t *testing.T) {
|
||||
client, esController := newController(time.Duration(0))
|
||||
tc.endpoints.Name = endpointsName
|
||||
tc.endpoints.Namespace = namespace
|
||||
esController.endpointsStore.Add(tc.endpoints)
|
||||
esController.serviceStore.Add(&v1.Service{ObjectMeta: metav1.ObjectMeta{
|
||||
Name: endpointsName,
|
||||
Namespace: namespace,
|
||||
}})
|
||||
|
||||
for _, epSlice := range tc.endpointSlices {
|
||||
epSlice.Namespace = namespace
|
||||
esController.endpointSliceStore.Add(epSlice)
|
||||
_, err := client.DiscoveryV1beta1().EndpointSlices(namespace).Create(context.TODO(), epSlice, metav1.CreateOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error creating EndpointSlice, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
err := esController.syncEndpoints(fmt.Sprintf("%s/%s", namespace, endpointsName))
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error from syncEndpoints: %v", err)
|
||||
}
|
||||
|
||||
numInitialActions := len(tc.endpointSlices)
|
||||
numExtraActions := len(client.Actions()) - numInitialActions
|
||||
if numExtraActions != tc.expectedNumActions {
|
||||
t.Fatalf("Expected %d additional client actions, got %d: %#v", tc.expectedNumActions, numExtraActions, client.Actions()[numInitialActions:])
|
||||
}
|
||||
|
||||
endpointSlices := fetchEndpointSlices(t, client, namespace)
|
||||
expectEndpointSlices(t, tc.expectedNumSlices, int(defaultMaxEndpointsPerSubset), *tc.endpoints, endpointSlices)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestShouldMirror(t *testing.T) {
|
||||
svcWithSelector := &v1.Service{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "with-selector",
|
||||
Namespace: "example1",
|
||||
},
|
||||
Spec: v1.ServiceSpec{
|
||||
Selector: map[string]string{"with": "selector"},
|
||||
},
|
||||
}
|
||||
svcWithoutSelector := &v1.Service{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "without-selector",
|
||||
Namespace: "example1",
|
||||
},
|
||||
Spec: v1.ServiceSpec{},
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
testName string
|
||||
endpoints *v1.Endpoints
|
||||
service *v1.Service
|
||||
shouldMirror bool
|
||||
}{{
|
||||
testName: "Service without selector with matching endpoints",
|
||||
service: svcWithoutSelector,
|
||||
endpoints: &v1.Endpoints{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: svcWithoutSelector.Name,
|
||||
Namespace: svcWithoutSelector.Namespace,
|
||||
},
|
||||
},
|
||||
shouldMirror: true,
|
||||
}, {
|
||||
testName: "Service without selector, matching Endpoints with skip-mirror=true",
|
||||
service: svcWithoutSelector,
|
||||
endpoints: &v1.Endpoints{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: svcWithSelector.Name,
|
||||
Namespace: svcWithSelector.Namespace,
|
||||
Labels: map[string]string{
|
||||
discovery.LabelSkipMirror: "true",
|
||||
},
|
||||
},
|
||||
},
|
||||
shouldMirror: false,
|
||||
}, {
|
||||
testName: "Service without selector, matching Endpoints with skip-mirror=invalid",
|
||||
service: svcWithoutSelector,
|
||||
endpoints: &v1.Endpoints{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: svcWithoutSelector.Name,
|
||||
Namespace: svcWithoutSelector.Namespace,
|
||||
Labels: map[string]string{
|
||||
discovery.LabelSkipMirror: "invalid",
|
||||
},
|
||||
},
|
||||
},
|
||||
shouldMirror: true,
|
||||
}, {
|
||||
testName: "Service without selector, matching Endpoints with leader election annotation",
|
||||
service: svcWithoutSelector,
|
||||
endpoints: &v1.Endpoints{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: svcWithSelector.Name,
|
||||
Namespace: svcWithSelector.Namespace,
|
||||
Annotations: map[string]string{
|
||||
resourcelock.LeaderElectionRecordAnnotationKey: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
shouldMirror: false,
|
||||
}, {
|
||||
testName: "Service without selector, matching Endpoints without skip label in different namespace",
|
||||
service: svcWithSelector,
|
||||
endpoints: &v1.Endpoints{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: svcWithSelector.Name,
|
||||
Namespace: svcWithSelector.Namespace + "different",
|
||||
},
|
||||
},
|
||||
shouldMirror: false,
|
||||
}, {
|
||||
testName: "Service without selector or matching endpoints",
|
||||
service: svcWithoutSelector,
|
||||
endpoints: nil,
|
||||
shouldMirror: false,
|
||||
}, {
|
||||
testName: "Endpoints without matching Service",
|
||||
service: nil,
|
||||
endpoints: &v1.Endpoints{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: svcWithoutSelector.Name,
|
||||
Namespace: svcWithoutSelector.Namespace,
|
||||
},
|
||||
},
|
||||
shouldMirror: false,
|
||||
}}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.testName, func(t *testing.T) {
|
||||
_, c := newController(time.Duration(0))
|
||||
|
||||
if tc.endpoints != nil {
|
||||
err := c.endpointsStore.Add(tc.endpoints)
|
||||
if err != nil {
|
||||
t.Fatalf("Error adding Endpoints to store: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if tc.service != nil {
|
||||
err := c.serviceStore.Add(tc.service)
|
||||
if err != nil {
|
||||
t.Fatalf("Error adding Service to store: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
shouldMirror := c.shouldMirror(tc.endpoints)
|
||||
|
||||
if shouldMirror != tc.shouldMirror {
|
||||
t.Errorf("Expected %t to be returned, got %t", tc.shouldMirror, shouldMirror)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEndpointSlicesMirroredForService(t *testing.T) {
|
||||
testCases := []struct {
|
||||
testName string
|
||||
namespace string
|
||||
name string
|
||||
endpointSlice *discovery.EndpointSlice
|
||||
expectedInList bool
|
||||
}{{
|
||||
testName: "Service with matching EndpointSlice",
|
||||
namespace: "ns1",
|
||||
name: "svc1",
|
||||
endpointSlice: &discovery.EndpointSlice{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "example-1",
|
||||
Namespace: "ns1",
|
||||
Labels: map[string]string{
|
||||
discovery.LabelServiceName: "svc1",
|
||||
discovery.LabelManagedBy: controllerName,
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedInList: true,
|
||||
}, {
|
||||
testName: "Service with EndpointSlice that has different namespace",
|
||||
namespace: "ns1",
|
||||
name: "svc1",
|
||||
endpointSlice: &discovery.EndpointSlice{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "example-1",
|
||||
Namespace: "ns2",
|
||||
Labels: map[string]string{
|
||||
discovery.LabelServiceName: "svc1",
|
||||
discovery.LabelManagedBy: controllerName,
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedInList: false,
|
||||
}, {
|
||||
testName: "Service with EndpointSlice that has different service name",
|
||||
namespace: "ns1",
|
||||
name: "svc1",
|
||||
endpointSlice: &discovery.EndpointSlice{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "example-1",
|
||||
Namespace: "ns1",
|
||||
Labels: map[string]string{
|
||||
discovery.LabelServiceName: "svc2",
|
||||
discovery.LabelManagedBy: controllerName,
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedInList: false,
|
||||
}, {
|
||||
testName: "Service with EndpointSlice that has different controller name",
|
||||
namespace: "ns1",
|
||||
name: "svc1",
|
||||
endpointSlice: &discovery.EndpointSlice{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "example-1",
|
||||
Namespace: "ns1",
|
||||
Labels: map[string]string{
|
||||
discovery.LabelServiceName: "svc1",
|
||||
discovery.LabelManagedBy: controllerName + "foo",
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedInList: false,
|
||||
}, {
|
||||
testName: "Service with EndpointSlice that has missing controller name",
|
||||
namespace: "ns1",
|
||||
name: "svc1",
|
||||
endpointSlice: &discovery.EndpointSlice{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "example-1",
|
||||
Namespace: "ns1",
|
||||
Labels: map[string]string{
|
||||
discovery.LabelServiceName: "svc1",
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedInList: false,
|
||||
}, {
|
||||
testName: "Service with EndpointSlice that has missing service name",
|
||||
namespace: "ns1",
|
||||
name: "svc1",
|
||||
endpointSlice: &discovery.EndpointSlice{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "example-1",
|
||||
Namespace: "ns1",
|
||||
Labels: map[string]string{
|
||||
discovery.LabelManagedBy: controllerName,
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedInList: false,
|
||||
}}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.testName, func(t *testing.T) {
|
||||
_, c := newController(time.Duration(0))
|
||||
|
||||
err := c.endpointSliceStore.Add(tc.endpointSlice)
|
||||
if err != nil {
|
||||
t.Fatalf("Error adding EndpointSlice to store: %v", err)
|
||||
}
|
||||
|
||||
endpointSlices, err := endpointSlicesMirroredForService(c.endpointSliceLister, tc.namespace, tc.name)
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %v", err)
|
||||
}
|
||||
|
||||
if tc.expectedInList {
|
||||
if len(endpointSlices) != 1 {
|
||||
t.Fatalf("Expected 1 EndpointSlice to be in list, got %d", len(endpointSlices))
|
||||
}
|
||||
|
||||
if endpointSlices[0].Name != tc.endpointSlice.Name {
|
||||
t.Fatalf("Expected %s EndpointSlice to be in list, got %s", tc.endpointSlice.Name, endpointSlices[0].Name)
|
||||
}
|
||||
} else {
|
||||
if len(endpointSlices) != 0 {
|
||||
t.Fatalf("Expected no EndpointSlices to be in list, got %d", len(endpointSlices))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func generateAddresses(num int) []v1.EndpointAddress {
|
||||
addresses := make([]v1.EndpointAddress, num)
|
||||
for i := 0; i < num; i++ {
|
||||
part1 := i / 255
|
||||
part2 := i % 255
|
||||
ip := fmt.Sprintf("10.0.%d.%d", part1, part2)
|
||||
addresses[i] = v1.EndpointAddress{IP: ip}
|
||||
}
|
||||
return addresses
|
||||
}
|
29
pkg/controller/endpointslicemirroring/events.go
Normal file
29
pkg/controller/endpointslicemirroring/events.go
Normal file
@ -0,0 +1,29 @@
|
||||
/*
|
||||
Copyright 2020 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 endpointslicemirroring
|
||||
|
||||
const (
|
||||
// FailedToListEndpointSlices indicates the controller has failed to list
|
||||
// EndpointSlices.
|
||||
FailedToListEndpointSlices = "FailedToListEndpointSlices"
|
||||
// FailedToUpdateEndpointSlices indicates the controller has failed to
|
||||
// update EndpointSlices.
|
||||
FailedToUpdateEndpointSlices = "FailedToUpdateEndpointSlices"
|
||||
// InvalidIPAddress indicates that an IP address found in an Endpoints
|
||||
// resource is invalid.
|
||||
InvalidIPAddress = "InvalidIPAddress"
|
||||
)
|
42
pkg/controller/endpointslicemirroring/metrics/BUILD
Normal file
42
pkg/controller/endpointslicemirroring/metrics/BUILD
Normal file
@ -0,0 +1,42 @@
|
||||
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
|
||||
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = [
|
||||
"cache.go",
|
||||
"metrics.go",
|
||||
],
|
||||
importpath = "k8s.io/kubernetes/pkg/controller/endpointslicemirroring/metrics",
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
"//pkg/controller/util/endpoint:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/types:go_default_library",
|
||||
"//staging/src/k8s.io/component-base/metrics:go_default_library",
|
||||
"//staging/src/k8s.io/component-base/metrics/legacyregistry:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "package-srcs",
|
||||
srcs = glob(["**"]),
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:private"],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "all-srcs",
|
||||
srcs = [":package-srcs"],
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
|
||||
go_test(
|
||||
name = "go_default_test",
|
||||
srcs = ["cache_test.go"],
|
||||
embed = [":go_default_library"],
|
||||
deps = [
|
||||
"//pkg/controller/util/endpoint:go_default_library",
|
||||
"//staging/src/k8s.io/api/discovery/v1beta1:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/types:go_default_library",
|
||||
],
|
||||
)
|
158
pkg/controller/endpointslicemirroring/metrics/cache.go
Normal file
158
pkg/controller/endpointslicemirroring/metrics/cache.go
Normal file
@ -0,0 +1,158 @@
|
||||
/*
|
||||
Copyright 2020 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 metrics
|
||||
|
||||
import (
|
||||
"math"
|
||||
"sync"
|
||||
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
endpointutil "k8s.io/kubernetes/pkg/controller/util/endpoint"
|
||||
)
|
||||
|
||||
// NewCache returns a new Cache with the specified endpointsPerSlice.
|
||||
func NewCache(endpointsPerSlice int32) *Cache {
|
||||
return &Cache{
|
||||
maxEndpointsPerSlice: endpointsPerSlice,
|
||||
cache: map[types.NamespacedName]*EndpointPortCache{},
|
||||
}
|
||||
}
|
||||
|
||||
// Cache tracks values for total numbers of desired endpoints as well as the
|
||||
// efficiency of EndpointSlice endpoints distribution.
|
||||
type Cache struct {
|
||||
// maxEndpointsPerSlice references the maximum number of endpoints that
|
||||
// should be added to an EndpointSlice.
|
||||
maxEndpointsPerSlice int32
|
||||
|
||||
// lock protects changes to numEndpoints and cache.
|
||||
lock sync.Mutex
|
||||
// numEndpoints represents the total number of endpoints stored in
|
||||
// EndpointSlices.
|
||||
numEndpoints int
|
||||
// cache stores a EndpointPortCache grouped by NamespacedNames representing
|
||||
// Services.
|
||||
cache map[types.NamespacedName]*EndpointPortCache
|
||||
}
|
||||
|
||||
// EndpointPortCache tracks values for total numbers of desired endpoints as well
|
||||
// as the efficiency of EndpointSlice endpoints distribution for each unique
|
||||
// Service Port combination.
|
||||
type EndpointPortCache struct {
|
||||
items map[endpointutil.PortMapKey]EfficiencyInfo
|
||||
}
|
||||
|
||||
// EfficiencyInfo stores the number of Endpoints and Slices for calculating
|
||||
// total numbers of desired endpoints and the efficiency of EndpointSlice
|
||||
// endpoints distribution.
|
||||
type EfficiencyInfo struct {
|
||||
Endpoints int
|
||||
Slices int
|
||||
}
|
||||
|
||||
// NewEndpointPortCache initializes and returns a new EndpointPortCache.
|
||||
func NewEndpointPortCache() *EndpointPortCache {
|
||||
return &EndpointPortCache{
|
||||
items: map[endpointutil.PortMapKey]EfficiencyInfo{},
|
||||
}
|
||||
}
|
||||
|
||||
// Set updates the EndpointPortCache to contain the provided EfficiencyInfo
|
||||
// for the provided PortMapKey.
|
||||
func (spc *EndpointPortCache) Set(pmKey endpointutil.PortMapKey, eInfo EfficiencyInfo) {
|
||||
spc.items[pmKey] = eInfo
|
||||
}
|
||||
|
||||
// numEndpoints returns the total number of endpoints represented by a
|
||||
// EndpointPortCache.
|
||||
func (spc *EndpointPortCache) numEndpoints() int {
|
||||
num := 0
|
||||
for _, eInfo := range spc.items {
|
||||
num += eInfo.Endpoints
|
||||
}
|
||||
return num
|
||||
}
|
||||
|
||||
// UpdateEndpointPortCache updates a EndpointPortCache in the global cache for a
|
||||
// given Service and updates the corresponding metrics.
|
||||
// Parameters:
|
||||
// * endpointsNN refers to a NamespacedName representing the Endpoints resource.
|
||||
// * epCache refers to a EndpointPortCache for the specified Endpoints reosource.
|
||||
func (c *Cache) UpdateEndpointPortCache(endpointsNN types.NamespacedName, epCache *EndpointPortCache) {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
|
||||
prevNumEndpoints := 0
|
||||
if existingEPCache, ok := c.cache[endpointsNN]; ok {
|
||||
prevNumEndpoints = existingEPCache.numEndpoints()
|
||||
}
|
||||
|
||||
currNumEndpoints := epCache.numEndpoints()
|
||||
// To keep numEndpoints up to date, add the difference between the number of
|
||||
// endpoints in the provided spCache and any spCache it might be replacing.
|
||||
c.numEndpoints = c.numEndpoints + currNumEndpoints - prevNumEndpoints
|
||||
|
||||
c.cache[endpointsNN] = epCache
|
||||
c.updateMetrics()
|
||||
}
|
||||
|
||||
// DeleteEndpoints removes references to an Endpoints resource from the global
|
||||
// cache and updates the corresponding metrics.
|
||||
func (c *Cache) DeleteEndpoints(endpointsNN types.NamespacedName) {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
|
||||
if spCache, ok := c.cache[endpointsNN]; ok {
|
||||
c.numEndpoints = c.numEndpoints - spCache.numEndpoints()
|
||||
delete(c.cache, endpointsNN)
|
||||
c.updateMetrics()
|
||||
}
|
||||
}
|
||||
|
||||
// metricsUpdate stores a desired and actual number of EndpointSlices.
|
||||
type metricsUpdate struct {
|
||||
desired, actual int
|
||||
}
|
||||
|
||||
// desiredAndActualSlices returns a metricsUpdate with the desired and actual
|
||||
// number of EndpointSlices given the current values in the cache.
|
||||
// Must be called holding lock.
|
||||
func (c *Cache) desiredAndActualSlices() metricsUpdate {
|
||||
mUpdate := metricsUpdate{}
|
||||
for _, spCache := range c.cache {
|
||||
for _, eInfo := range spCache.items {
|
||||
mUpdate.actual += eInfo.Slices
|
||||
mUpdate.desired += numDesiredSlices(eInfo.Endpoints, int(c.maxEndpointsPerSlice))
|
||||
}
|
||||
}
|
||||
return mUpdate
|
||||
}
|
||||
|
||||
// updateMetrics updates metrics with the values from this Cache.
|
||||
// Must be called holding lock.
|
||||
func (c *Cache) updateMetrics() {
|
||||
mUpdate := c.desiredAndActualSlices()
|
||||
NumEndpointSlices.WithLabelValues().Set(float64(mUpdate.actual))
|
||||
DesiredEndpointSlices.WithLabelValues().Set(float64(mUpdate.desired))
|
||||
EndpointsDesired.WithLabelValues().Set(float64(c.numEndpoints))
|
||||
}
|
||||
|
||||
// numDesiredSlices calculates the number of EndpointSlices that would exist
|
||||
// with ideal endpoint distribution.
|
||||
func numDesiredSlices(numEndpoints, maxPerSlice int) int {
|
||||
return int(math.Ceil(float64(numEndpoints) / float64(maxPerSlice)))
|
||||
}
|
72
pkg/controller/endpointslicemirroring/metrics/cache_test.go
Normal file
72
pkg/controller/endpointslicemirroring/metrics/cache_test.go
Normal file
@ -0,0 +1,72 @@
|
||||
/*
|
||||
Copyright 2020 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 metrics
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
discovery "k8s.io/api/discovery/v1beta1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
endpointutil "k8s.io/kubernetes/pkg/controller/util/endpoint"
|
||||
)
|
||||
|
||||
func TestNumEndpointsAndSlices(t *testing.T) {
|
||||
c := NewCache(int32(100))
|
||||
|
||||
p80 := int32(80)
|
||||
p443 := int32(443)
|
||||
|
||||
pmKey80443 := endpointutil.NewPortMapKey([]discovery.EndpointPort{{Port: &p80}, {Port: &p443}})
|
||||
pmKey80 := endpointutil.NewPortMapKey([]discovery.EndpointPort{{Port: &p80}})
|
||||
|
||||
spCacheEfficient := NewEndpointPortCache()
|
||||
spCacheEfficient.Set(pmKey80, EfficiencyInfo{Endpoints: 45, Slices: 1})
|
||||
spCacheEfficient.Set(pmKey80443, EfficiencyInfo{Endpoints: 35, Slices: 1})
|
||||
|
||||
spCacheInefficient := NewEndpointPortCache()
|
||||
spCacheInefficient.Set(pmKey80, EfficiencyInfo{Endpoints: 12, Slices: 5})
|
||||
spCacheInefficient.Set(pmKey80443, EfficiencyInfo{Endpoints: 18, Slices: 8})
|
||||
|
||||
c.UpdateEndpointPortCache(types.NamespacedName{Namespace: "ns1", Name: "svc1"}, spCacheInefficient)
|
||||
expectNumEndpointsAndSlices(t, c, 2, 13, 30)
|
||||
|
||||
c.UpdateEndpointPortCache(types.NamespacedName{Namespace: "ns1", Name: "svc2"}, spCacheEfficient)
|
||||
expectNumEndpointsAndSlices(t, c, 4, 15, 110)
|
||||
|
||||
c.UpdateEndpointPortCache(types.NamespacedName{Namespace: "ns1", Name: "svc3"}, spCacheInefficient)
|
||||
expectNumEndpointsAndSlices(t, c, 6, 28, 140)
|
||||
|
||||
c.UpdateEndpointPortCache(types.NamespacedName{Namespace: "ns1", Name: "svc1"}, spCacheEfficient)
|
||||
expectNumEndpointsAndSlices(t, c, 6, 17, 190)
|
||||
|
||||
c.DeleteEndpoints(types.NamespacedName{Namespace: "ns1", Name: "svc3"})
|
||||
expectNumEndpointsAndSlices(t, c, 4, 4, 160)
|
||||
}
|
||||
|
||||
func expectNumEndpointsAndSlices(t *testing.T, c *Cache, desired int, actual int, numEndpoints int) {
|
||||
t.Helper()
|
||||
mUpdate := c.desiredAndActualSlices()
|
||||
if mUpdate.desired != desired {
|
||||
t.Errorf("Expected numEndpointSlices to be %d, got %d", desired, mUpdate.desired)
|
||||
}
|
||||
if mUpdate.actual != actual {
|
||||
t.Errorf("Expected desiredEndpointSlices to be %d, got %d", actual, mUpdate.actual)
|
||||
}
|
||||
if c.numEndpoints != numEndpoints {
|
||||
t.Errorf("Expected numEndpoints to be %d, got %d", numEndpoints, c.numEndpoints)
|
||||
}
|
||||
}
|
136
pkg/controller/endpointslicemirroring/metrics/metrics.go
Normal file
136
pkg/controller/endpointslicemirroring/metrics/metrics.go
Normal file
@ -0,0 +1,136 @@
|
||||
/*
|
||||
Copyright 2020 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 metrics
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"k8s.io/component-base/metrics"
|
||||
"k8s.io/component-base/metrics/legacyregistry"
|
||||
)
|
||||
|
||||
// EndpointSliceMirroringSubsystem is the name of the subsystem used for
|
||||
// EndpointSliceMirroring controller.
|
||||
const EndpointSliceMirroringSubsystem = "endpoint_slice_mirroring_controller"
|
||||
|
||||
var (
|
||||
// EndpointsAddedPerSync tracks the number of endpoints added on each
|
||||
// Endpoints sync.
|
||||
EndpointsAddedPerSync = metrics.NewHistogramVec(
|
||||
&metrics.HistogramOpts{
|
||||
Subsystem: EndpointSliceMirroringSubsystem,
|
||||
Name: "endpoints_added_per_sync",
|
||||
Help: "Number of endpoints added on each Endpoints sync",
|
||||
StabilityLevel: metrics.ALPHA,
|
||||
Buckets: metrics.ExponentialBuckets(2, 2, 15),
|
||||
},
|
||||
[]string{},
|
||||
)
|
||||
// EndpointsUpdatedPerSync tracks the number of endpoints updated on each
|
||||
// Endpoints sync.
|
||||
EndpointsUpdatedPerSync = metrics.NewHistogramVec(
|
||||
&metrics.HistogramOpts{
|
||||
Subsystem: EndpointSliceMirroringSubsystem,
|
||||
Name: "endpoints_updated_per_sync",
|
||||
Help: "Number of endpoints updated on each Endpoints sync",
|
||||
StabilityLevel: metrics.ALPHA,
|
||||
Buckets: metrics.ExponentialBuckets(2, 2, 15),
|
||||
},
|
||||
[]string{},
|
||||
)
|
||||
// EndpointsRemovedPerSync tracks the number of endpoints removed on each
|
||||
// Endpoints sync.
|
||||
EndpointsRemovedPerSync = metrics.NewHistogramVec(
|
||||
&metrics.HistogramOpts{
|
||||
Subsystem: EndpointSliceMirroringSubsystem,
|
||||
Name: "endpoints_removed_per_sync",
|
||||
Help: "Number of endpoints removed on each Endpoints sync",
|
||||
StabilityLevel: metrics.ALPHA,
|
||||
Buckets: metrics.ExponentialBuckets(2, 2, 15),
|
||||
},
|
||||
[]string{},
|
||||
)
|
||||
// EndpointsSyncDuration tracks how long syncEndpoints() takes in a number
|
||||
// of Seconds.
|
||||
EndpointsSyncDuration = metrics.NewHistogramVec(
|
||||
&metrics.HistogramOpts{
|
||||
Subsystem: EndpointSliceMirroringSubsystem,
|
||||
Name: "endpoints_sync_duration",
|
||||
Help: "Duration of syncEndpoints() in seconds",
|
||||
StabilityLevel: metrics.ALPHA,
|
||||
Buckets: metrics.ExponentialBuckets(0.001, 2, 15),
|
||||
},
|
||||
[]string{},
|
||||
)
|
||||
// EndpointsDesired tracks the total number of desired endpoints.
|
||||
EndpointsDesired = metrics.NewGaugeVec(
|
||||
&metrics.GaugeOpts{
|
||||
Subsystem: EndpointSliceMirroringSubsystem,
|
||||
Name: "endpoints_desired",
|
||||
Help: "Number of endpoints desired",
|
||||
StabilityLevel: metrics.ALPHA,
|
||||
},
|
||||
[]string{},
|
||||
)
|
||||
// NumEndpointSlices tracks the number of EndpointSlices in a cluster.
|
||||
NumEndpointSlices = metrics.NewGaugeVec(
|
||||
&metrics.GaugeOpts{
|
||||
Subsystem: EndpointSliceMirroringSubsystem,
|
||||
Name: "num_endpoint_slices",
|
||||
Help: "Number of EndpointSlices",
|
||||
StabilityLevel: metrics.ALPHA,
|
||||
},
|
||||
[]string{},
|
||||
)
|
||||
// DesiredEndpointSlices tracks the number of EndpointSlices that would
|
||||
// exist with perfect endpoint allocation.
|
||||
DesiredEndpointSlices = metrics.NewGaugeVec(
|
||||
&metrics.GaugeOpts{
|
||||
Subsystem: EndpointSliceMirroringSubsystem,
|
||||
Name: "desired_endpoint_slices",
|
||||
Help: "Number of EndpointSlices that would exist with perfect endpoint allocation",
|
||||
StabilityLevel: metrics.ALPHA,
|
||||
},
|
||||
[]string{},
|
||||
)
|
||||
// EndpointSliceChanges tracks the number of changes to Endpoint Slices.
|
||||
EndpointSliceChanges = metrics.NewCounterVec(
|
||||
&metrics.CounterOpts{
|
||||
Subsystem: EndpointSliceMirroringSubsystem,
|
||||
Name: "changes",
|
||||
Help: "Number of EndpointSlice changes",
|
||||
StabilityLevel: metrics.ALPHA,
|
||||
},
|
||||
[]string{"operation"},
|
||||
)
|
||||
)
|
||||
|
||||
var registerMetrics sync.Once
|
||||
|
||||
// RegisterMetrics registers EndpointSlice metrics.
|
||||
func RegisterMetrics() {
|
||||
registerMetrics.Do(func() {
|
||||
legacyregistry.MustRegister(EndpointsAddedPerSync)
|
||||
legacyregistry.MustRegister(EndpointsUpdatedPerSync)
|
||||
legacyregistry.MustRegister(EndpointsRemovedPerSync)
|
||||
legacyregistry.MustRegister(EndpointsSyncDuration)
|
||||
legacyregistry.MustRegister(EndpointsDesired)
|
||||
legacyregistry.MustRegister(NumEndpointSlices)
|
||||
legacyregistry.MustRegister(DesiredEndpointSlices)
|
||||
legacyregistry.MustRegister(EndpointSliceChanges)
|
||||
})
|
||||
}
|
291
pkg/controller/endpointslicemirroring/reconciler.go
Normal file
291
pkg/controller/endpointslicemirroring/reconciler.go
Normal file
@ -0,0 +1,291 @@
|
||||
/*
|
||||
Copyright 2020 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 endpointslicemirroring
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
discovery "k8s.io/api/discovery/v1beta1"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
utilerrors "k8s.io/apimachinery/pkg/util/errors"
|
||||
clientset "k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/tools/record"
|
||||
"k8s.io/klog/v2"
|
||||
"k8s.io/kubernetes/pkg/controller/endpointslicemirroring/metrics"
|
||||
endpointutil "k8s.io/kubernetes/pkg/controller/util/endpoint"
|
||||
)
|
||||
|
||||
// reconciler is responsible for transforming current EndpointSlice state into
|
||||
// desired state
|
||||
type reconciler struct {
|
||||
client clientset.Interface
|
||||
maxEndpointsPerSubset int32
|
||||
endpointSliceTracker *endpointSliceTracker
|
||||
metricsCache *metrics.Cache
|
||||
eventRecorder record.EventRecorder
|
||||
}
|
||||
|
||||
// reconcile takes an Endpoints resource and ensures that corresponding
|
||||
// EndpointSlices exist. It creates, updates, or deletes EndpointSlices to
|
||||
// ensure the desired set of addresses are represented by EndpointSlices.
|
||||
func (r *reconciler) reconcile(endpoints *corev1.Endpoints, existingSlices []*discovery.EndpointSlice) error {
|
||||
// Calculate desired state.
|
||||
d := newDesiredCalc()
|
||||
|
||||
for _, subset := range endpoints.Subsets {
|
||||
multiKey := d.initPorts(subset.Ports)
|
||||
|
||||
totalAddresses := 0
|
||||
numInvalidAddresses := 0
|
||||
|
||||
for _, address := range subset.Addresses {
|
||||
totalAddresses++
|
||||
if totalAddresses > int(r.maxEndpointsPerSubset) {
|
||||
break
|
||||
}
|
||||
if ok := d.addAddress(address, multiKey, true); !ok {
|
||||
numInvalidAddresses++
|
||||
klog.Warningf("Address in %s/%s Endpoints is not a valid IP, it will not be mirrored to an EndpointSlice: %s", endpoints.Namespace, endpoints.Name, address.IP)
|
||||
}
|
||||
}
|
||||
|
||||
for _, address := range subset.NotReadyAddresses {
|
||||
totalAddresses++
|
||||
if totalAddresses > int(r.maxEndpointsPerSubset) {
|
||||
break
|
||||
}
|
||||
if ok := d.addAddress(address, multiKey, false); !ok {
|
||||
numInvalidAddresses++
|
||||
klog.Warningf("Address in %s/%s Endpoints is not a valid IP, it will not be mirrored to an EndpointSlice: %s", endpoints.Namespace, endpoints.Name, address.IP)
|
||||
}
|
||||
}
|
||||
|
||||
if numInvalidAddresses > 0 {
|
||||
r.eventRecorder.Eventf(endpoints, corev1.EventTypeWarning, InvalidIPAddress,
|
||||
"Skipped %d invalid IP addresses when mirroring to EndpointSlices", numInvalidAddresses)
|
||||
}
|
||||
}
|
||||
|
||||
// Build data structures for existing state.
|
||||
existingSlicesByKey := endpointSlicesByKey(existingSlices)
|
||||
|
||||
// Determine changes necessary for each group of slices by port map.
|
||||
epMetrics := metrics.NewEndpointPortCache()
|
||||
totals := totalsByAction{}
|
||||
slices := slicesByAction{}
|
||||
|
||||
for portKey, desiredEndpoints := range d.endpointsByKey {
|
||||
numEndpoints := len(desiredEndpoints)
|
||||
pmSlices, pmTotals := r.reconcileByPortMapping(
|
||||
endpoints, existingSlicesByKey[portKey], desiredEndpoints, d.portsByKey[portKey], portKey.addressType())
|
||||
|
||||
slices.append(pmSlices)
|
||||
totals.add(pmTotals)
|
||||
|
||||
epMetrics.Set(endpointutil.PortMapKey(portKey), metrics.EfficiencyInfo{
|
||||
Endpoints: numEndpoints,
|
||||
Slices: len(existingSlicesByKey[portKey]) + len(pmSlices.toCreate) - len(pmSlices.toDelete),
|
||||
})
|
||||
}
|
||||
|
||||
// If there are unique sets of ports that are no longer desired, mark
|
||||
// the corresponding endpoint slices for deletion.
|
||||
for portKey, existingSlices := range existingSlicesByKey {
|
||||
if _, ok := d.endpointsByKey[portKey]; !ok {
|
||||
for _, existingSlice := range existingSlices {
|
||||
slices.toDelete = append(slices.toDelete, existingSlice)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
metrics.EndpointsAddedPerSync.WithLabelValues().Observe(float64(totals.added))
|
||||
metrics.EndpointsUpdatedPerSync.WithLabelValues().Observe(float64(totals.updated))
|
||||
metrics.EndpointsRemovedPerSync.WithLabelValues().Observe(float64(totals.removed))
|
||||
|
||||
endpointsNN := types.NamespacedName{Name: endpoints.Name, Namespace: endpoints.Namespace}
|
||||
r.metricsCache.UpdateEndpointPortCache(endpointsNN, epMetrics)
|
||||
|
||||
return r.finalize(endpoints, slices)
|
||||
}
|
||||
|
||||
// reconcileByPortMapping compares the endpoints found in existing slices with
|
||||
// the list of desired endpoints and returns lists of slices to create, update,
|
||||
// and delete.
|
||||
func (r *reconciler) reconcileByPortMapping(
|
||||
endpoints *corev1.Endpoints,
|
||||
existingSlices []*discovery.EndpointSlice,
|
||||
desiredSet endpointSet,
|
||||
endpointPorts []discovery.EndpointPort,
|
||||
addressType discovery.AddressType,
|
||||
) (slicesByAction, totalsByAction) {
|
||||
slices := slicesByAction{}
|
||||
totals := totalsByAction{}
|
||||
|
||||
// If no endpoints are desired, mark existing slices for deletion and
|
||||
// return.
|
||||
if desiredSet.Len() == 0 {
|
||||
slices.toDelete = existingSlices
|
||||
for _, epSlice := range existingSlices {
|
||||
totals.removed += len(epSlice.Endpoints)
|
||||
}
|
||||
return slices, totals
|
||||
}
|
||||
|
||||
if len(existingSlices) == 0 {
|
||||
// if no existing slices, all desired endpoints will be added.
|
||||
totals.added = desiredSet.Len()
|
||||
} else {
|
||||
// if >0 existing slices, mark all but 1 for deletion.
|
||||
slices.toDelete = existingSlices[1:]
|
||||
|
||||
// Return early if first slice matches desired endpoints.
|
||||
totals = totalChanges(existingSlices[0], desiredSet)
|
||||
if totals.added == 0 && totals.updated == 0 && totals.removed == 0 {
|
||||
return slices, totals
|
||||
}
|
||||
}
|
||||
|
||||
// generate a new slice with the desired endpoints.
|
||||
var sliceName string
|
||||
if len(existingSlices) > 0 {
|
||||
sliceName = existingSlices[0].Name
|
||||
}
|
||||
newSlice := newEndpointSlice(endpoints, endpointPorts, addressType, sliceName)
|
||||
for desiredSet.Len() > 0 && len(newSlice.Endpoints) < int(r.maxEndpointsPerSubset) {
|
||||
endpoint, _ := desiredSet.PopAny()
|
||||
newSlice.Endpoints = append(newSlice.Endpoints, *endpoint)
|
||||
}
|
||||
|
||||
if newSlice.Name != "" {
|
||||
slices.toUpdate = []*discovery.EndpointSlice{newSlice}
|
||||
} else { // Slices to be created set GenerateName instead of Name.
|
||||
slices.toCreate = []*discovery.EndpointSlice{newSlice}
|
||||
}
|
||||
|
||||
return slices, totals
|
||||
}
|
||||
|
||||
// finalize creates, updates, and deletes slices as specified
|
||||
func (r *reconciler) finalize(endpoints *corev1.Endpoints, slices slicesByAction) error {
|
||||
// If there are slices to create and delete, recycle the slices marked for
|
||||
// deletion by replacing creates with updates of slices that would otherwise
|
||||
// be deleted.
|
||||
recycleSlices(&slices)
|
||||
|
||||
var errs []error
|
||||
epsClient := r.client.DiscoveryV1beta1().EndpointSlices(endpoints.Namespace)
|
||||
|
||||
// Don't create more EndpointSlices if corresponding Endpoints resource is
|
||||
// being deleted.
|
||||
if endpoints.DeletionTimestamp == nil {
|
||||
for _, endpointSlice := range slices.toCreate {
|
||||
createdSlice, err := epsClient.Create(context.TODO(), endpointSlice, metav1.CreateOptions{})
|
||||
if err != nil {
|
||||
// If the namespace is terminating, creates will continue to fail. Simply drop the item.
|
||||
if errors.HasStatusCause(err, corev1.NamespaceTerminatingCause) {
|
||||
return nil
|
||||
}
|
||||
errs = append(errs, fmt.Errorf("Error creating EndpointSlice for Endpoints %s/%s: %v", endpoints.Namespace, endpoints.Name, err))
|
||||
} else {
|
||||
r.endpointSliceTracker.update(createdSlice)
|
||||
metrics.EndpointSliceChanges.WithLabelValues("create").Inc()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, endpointSlice := range slices.toUpdate {
|
||||
updatedSlice, err := epsClient.Update(context.TODO(), endpointSlice, metav1.UpdateOptions{})
|
||||
if err != nil {
|
||||
errs = append(errs, fmt.Errorf("Error updating %s EndpointSlice for Endpoints %s/%s: %v", endpointSlice.Name, endpoints.Namespace, endpoints.Name, err))
|
||||
} else {
|
||||
r.endpointSliceTracker.update(updatedSlice)
|
||||
metrics.EndpointSliceChanges.WithLabelValues("update").Inc()
|
||||
}
|
||||
}
|
||||
|
||||
for _, endpointSlice := range slices.toDelete {
|
||||
err := epsClient.Delete(context.TODO(), endpointSlice.Name, metav1.DeleteOptions{})
|
||||
if err != nil {
|
||||
errs = append(errs, fmt.Errorf("Error deleting %s EndpointSlice for Endpoints %s/%s: %v", endpointSlice.Name, endpoints.Namespace, endpoints.Name, err))
|
||||
} else {
|
||||
r.endpointSliceTracker.delete(endpointSlice)
|
||||
metrics.EndpointSliceChanges.WithLabelValues("delete").Inc()
|
||||
}
|
||||
}
|
||||
|
||||
return utilerrors.NewAggregate(errs)
|
||||
}
|
||||
|
||||
// deleteEndpoints deletes any associated EndpointSlices and cleans up any
|
||||
// Endpoints references from the metricsCache.
|
||||
func (r *reconciler) deleteEndpoints(namespace, name string, endpointSlices []*discovery.EndpointSlice) error {
|
||||
r.metricsCache.DeleteEndpoints(types.NamespacedName{Namespace: namespace, Name: name})
|
||||
var errs []error
|
||||
for _, endpointSlice := range endpointSlices {
|
||||
err := r.client.DiscoveryV1beta1().EndpointSlices(namespace).Delete(context.TODO(), endpointSlice.Name, metav1.DeleteOptions{})
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
if len(errs) > 0 {
|
||||
return fmt.Errorf("Error(s) deleting %d/%d EndpointSlices for %s/%s Endpoints, including: %s", len(errs), len(endpointSlices), namespace, name, errs[0])
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// endpointSlicesByKey returns a map that groups EndpointSlices by unique
|
||||
// addrTypePortMapKey values.
|
||||
func endpointSlicesByKey(existingSlices []*discovery.EndpointSlice) map[addrTypePortMapKey][]*discovery.EndpointSlice {
|
||||
slicesByKey := map[addrTypePortMapKey][]*discovery.EndpointSlice{}
|
||||
for _, existingSlice := range existingSlices {
|
||||
epKey := newAddrTypePortMapKey(existingSlice.Ports, existingSlice.AddressType)
|
||||
slicesByKey[epKey] = append(slicesByKey[epKey], existingSlice)
|
||||
}
|
||||
return slicesByKey
|
||||
}
|
||||
|
||||
// totalChanges returns the total changes that will be required for an
|
||||
// EndpointSlice to match a desired set of endpoints.
|
||||
func totalChanges(existingSlice *discovery.EndpointSlice, desiredSet endpointSet) totalsByAction {
|
||||
totals := totalsByAction{}
|
||||
existingMatches := 0
|
||||
|
||||
for _, endpoint := range existingSlice.Endpoints {
|
||||
got := desiredSet.Get(&endpoint)
|
||||
if got == nil {
|
||||
// If not desired, increment number of endpoints to be deleted.
|
||||
totals.removed++
|
||||
} else {
|
||||
existingMatches++
|
||||
|
||||
// If existing version of endpoint doesn't match desired version
|
||||
// increment number of endpoints to be updated.
|
||||
if !endpointsEqualBeyondHash(got, &endpoint) {
|
||||
totals.updated++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Any desired endpoints that have not been found in the existing slice will
|
||||
// be added.
|
||||
totals.added = desiredSet.Len() - existingMatches
|
||||
return totals
|
||||
}
|
137
pkg/controller/endpointslicemirroring/reconciler_helpers.go
Normal file
137
pkg/controller/endpointslicemirroring/reconciler_helpers.go
Normal file
@ -0,0 +1,137 @@
|
||||
/*
|
||||
Copyright 2020 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 endpointslicemirroring
|
||||
|
||||
import (
|
||||
"k8s.io/api/core/v1"
|
||||
discovery "k8s.io/api/discovery/v1beta1"
|
||||
)
|
||||
|
||||
// slicesByAction includes lists of slices to create, update, or delete.
|
||||
type slicesByAction struct {
|
||||
toCreate, toUpdate, toDelete []*discovery.EndpointSlice
|
||||
}
|
||||
|
||||
// append appends slices from another slicesByAction struct.
|
||||
func (s *slicesByAction) append(slices slicesByAction) {
|
||||
s.toCreate = append(s.toCreate, slices.toCreate...)
|
||||
s.toUpdate = append(s.toUpdate, slices.toUpdate...)
|
||||
s.toDelete = append(s.toDelete, slices.toDelete...)
|
||||
}
|
||||
|
||||
// totalsByAction includes total numbers for added and removed.
|
||||
type totalsByAction struct {
|
||||
added, updated, removed int
|
||||
}
|
||||
|
||||
// add adds totals from another totalsByAction struct.
|
||||
func (t *totalsByAction) add(totals totalsByAction) {
|
||||
t.added += totals.added
|
||||
t.updated += totals.updated
|
||||
t.removed += totals.removed
|
||||
}
|
||||
|
||||
// newDesiredCalc initializes and returns a new desiredCalc.
|
||||
func newDesiredCalc() *desiredCalc {
|
||||
return &desiredCalc{
|
||||
portsByKey: map[addrTypePortMapKey][]discovery.EndpointPort{},
|
||||
endpointsByKey: map[addrTypePortMapKey]endpointSet{},
|
||||
numDesiredEndpoints: 0,
|
||||
}
|
||||
}
|
||||
|
||||
// desiredCalc helps calculate desired endpoints and ports.
|
||||
type desiredCalc struct {
|
||||
portsByKey map[addrTypePortMapKey][]discovery.EndpointPort
|
||||
endpointsByKey map[addrTypePortMapKey]endpointSet
|
||||
numDesiredEndpoints int
|
||||
}
|
||||
|
||||
// multiAddrTypePortMapKey stores addrTypePortMapKey for different address
|
||||
// types.
|
||||
type multiAddrTypePortMapKey map[discovery.AddressType]addrTypePortMapKey
|
||||
|
||||
// initPorts initializes ports for a subset and address type and returns the
|
||||
// corresponding addrTypePortMapKey.
|
||||
func (d *desiredCalc) initPorts(subsetPorts []v1.EndpointPort) multiAddrTypePortMapKey {
|
||||
endpointPorts := epPortsToEpsPorts(subsetPorts)
|
||||
addrTypes := []discovery.AddressType{discovery.AddressTypeIPv4, discovery.AddressTypeIPv6}
|
||||
multiKey := multiAddrTypePortMapKey{}
|
||||
|
||||
for _, addrType := range addrTypes {
|
||||
multiKey[addrType] = newAddrTypePortMapKey(endpointPorts, addrType)
|
||||
if _, ok := d.endpointsByKey[multiKey[addrType]]; !ok {
|
||||
d.endpointsByKey[multiKey[addrType]] = endpointSet{}
|
||||
}
|
||||
d.portsByKey[multiKey[addrType]] = endpointPorts
|
||||
}
|
||||
|
||||
return multiKey
|
||||
}
|
||||
|
||||
// addAddress adds an EndpointAddress to the desired state if it is valid. It
|
||||
// returns false if the address was invalid.
|
||||
func (d *desiredCalc) addAddress(address v1.EndpointAddress, multiKey multiAddrTypePortMapKey, ready bool) bool {
|
||||
endpoint := addressToEndpoint(address, ready)
|
||||
addrType := getAddressType(address.IP)
|
||||
if addrType == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
d.endpointsByKey[multiKey[*addrType]].Insert(endpoint)
|
||||
d.numDesiredEndpoints++
|
||||
return true
|
||||
}
|
||||
|
||||
type slicesByAddrType map[discovery.AddressType][]*discovery.EndpointSlice
|
||||
|
||||
// recycleSlices will recycle the slices marked for deletion by replacing
|
||||
// creates with updates of slices that would otherwise be deleted.
|
||||
func recycleSlices(slices *slicesByAction) {
|
||||
toCreateByAddrType := toSlicesByAddrType(slices.toCreate)
|
||||
toDeleteByAddrType := toSlicesByAddrType(slices.toDelete)
|
||||
|
||||
for addrType, slicesToCreate := range toCreateByAddrType {
|
||||
slicesToDelete := toDeleteByAddrType[addrType]
|
||||
for i := 0; len(slicesToCreate) > i && len(slicesToDelete) > i; i++ {
|
||||
slices.toCreate = removeSlice(slices.toCreate, slicesToCreate[i])
|
||||
slices.toDelete = removeSlice(slices.toDelete, slicesToDelete[i])
|
||||
slice := slicesToCreate[i]
|
||||
slice.Name = slicesToDelete[i].Name
|
||||
slices.toUpdate = append(slices.toUpdate, slice)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// removeSlice removes an EndpointSlice from a list of EndpointSlices.
|
||||
func removeSlice(slices []*discovery.EndpointSlice, sliceToRemove *discovery.EndpointSlice) []*discovery.EndpointSlice {
|
||||
for i, slice := range slices {
|
||||
if slice.Name == sliceToRemove.Name {
|
||||
return append(slices[:i], slices[i+1:]...)
|
||||
}
|
||||
}
|
||||
return slices
|
||||
}
|
||||
|
||||
// toSliceByAddrType returns lists of EndpointSlices grouped by address.
|
||||
func toSlicesByAddrType(slices []*discovery.EndpointSlice) slicesByAddrType {
|
||||
byAddrType := slicesByAddrType{}
|
||||
for _, slice := range slices {
|
||||
byAddrType[slice.AddressType] = append(byAddrType[slice.AddressType], slice)
|
||||
}
|
||||
return byAddrType
|
||||
}
|
180
pkg/controller/endpointslicemirroring/reconciler_helpers_test.go
Normal file
180
pkg/controller/endpointslicemirroring/reconciler_helpers_test.go
Normal file
@ -0,0 +1,180 @@
|
||||
/*
|
||||
Copyright 2020 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 endpointslicemirroring
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
discovery "k8s.io/api/discovery/v1beta1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
func TestRecycleSlices(t *testing.T) {
|
||||
testCases := []struct {
|
||||
testName string
|
||||
startingSlices *slicesByAction
|
||||
expectedSlices *slicesByAction
|
||||
}{{
|
||||
testName: "Empty slices",
|
||||
startingSlices: &slicesByAction{},
|
||||
expectedSlices: &slicesByAction{},
|
||||
}, {
|
||||
testName: "1 to create and 1 to delete",
|
||||
startingSlices: &slicesByAction{
|
||||
toCreate: []*discovery.EndpointSlice{simpleEndpointSlice("foo", "10.1.2.3", discovery.AddressTypeIPv4)},
|
||||
toDelete: []*discovery.EndpointSlice{simpleEndpointSlice("bar", "10.2.3.4", discovery.AddressTypeIPv4)},
|
||||
},
|
||||
expectedSlices: &slicesByAction{
|
||||
toUpdate: []*discovery.EndpointSlice{simpleEndpointSlice("bar", "10.1.2.3", discovery.AddressTypeIPv4)},
|
||||
},
|
||||
}, {
|
||||
testName: "1 to create, update, and delete",
|
||||
startingSlices: &slicesByAction{
|
||||
toCreate: []*discovery.EndpointSlice{simpleEndpointSlice("foo", "10.1.2.3", discovery.AddressTypeIPv4)},
|
||||
toUpdate: []*discovery.EndpointSlice{simpleEndpointSlice("baz", "10.2.3.4", discovery.AddressTypeIPv4)},
|
||||
toDelete: []*discovery.EndpointSlice{simpleEndpointSlice("bar", "10.3.4.5", discovery.AddressTypeIPv4)},
|
||||
},
|
||||
expectedSlices: &slicesByAction{
|
||||
toUpdate: []*discovery.EndpointSlice{
|
||||
simpleEndpointSlice("baz", "10.2.3.4", discovery.AddressTypeIPv4),
|
||||
simpleEndpointSlice("bar", "10.1.2.3", discovery.AddressTypeIPv4),
|
||||
},
|
||||
},
|
||||
}, {
|
||||
testName: "2 to create and 1 to delete",
|
||||
startingSlices: &slicesByAction{
|
||||
toCreate: []*discovery.EndpointSlice{
|
||||
simpleEndpointSlice("foo1", "10.1.2.3", discovery.AddressTypeIPv4),
|
||||
simpleEndpointSlice("foo2", "10.3.4.5", discovery.AddressTypeIPv4),
|
||||
},
|
||||
toDelete: []*discovery.EndpointSlice{simpleEndpointSlice("bar", "10.2.3.4", discovery.AddressTypeIPv4)},
|
||||
},
|
||||
expectedSlices: &slicesByAction{
|
||||
toCreate: []*discovery.EndpointSlice{simpleEndpointSlice("foo2", "10.3.4.5", discovery.AddressTypeIPv4)},
|
||||
toUpdate: []*discovery.EndpointSlice{simpleEndpointSlice("bar", "10.1.2.3", discovery.AddressTypeIPv4)},
|
||||
},
|
||||
}, {
|
||||
testName: "1 to create and 2 to delete",
|
||||
startingSlices: &slicesByAction{
|
||||
toCreate: []*discovery.EndpointSlice{
|
||||
simpleEndpointSlice("foo1", "10.1.2.3", discovery.AddressTypeIPv4),
|
||||
},
|
||||
toDelete: []*discovery.EndpointSlice{
|
||||
simpleEndpointSlice("bar1", "10.2.3.4", discovery.AddressTypeIPv4),
|
||||
simpleEndpointSlice("bar2", "10.3.4.5", discovery.AddressTypeIPv4),
|
||||
},
|
||||
},
|
||||
expectedSlices: &slicesByAction{
|
||||
toUpdate: []*discovery.EndpointSlice{simpleEndpointSlice("bar1", "10.1.2.3", discovery.AddressTypeIPv4)},
|
||||
toDelete: []*discovery.EndpointSlice{simpleEndpointSlice("bar2", "10.3.4.5", discovery.AddressTypeIPv4)},
|
||||
},
|
||||
}, {
|
||||
testName: "1 to create and 1 to delete for each IP family",
|
||||
startingSlices: &slicesByAction{
|
||||
toCreate: []*discovery.EndpointSlice{
|
||||
simpleEndpointSlice("foo-v4", "10.1.2.3", discovery.AddressTypeIPv4),
|
||||
simpleEndpointSlice("foo-v6", "2001:db8:1111:3333:4444:5555:6666:7777", discovery.AddressTypeIPv6),
|
||||
},
|
||||
toDelete: []*discovery.EndpointSlice{
|
||||
simpleEndpointSlice("bar-v4", "10.2.2.3", discovery.AddressTypeIPv4),
|
||||
simpleEndpointSlice("bar-v6", "2001:db8:2222:3333:4444:5555:6666:7777", discovery.AddressTypeIPv6),
|
||||
},
|
||||
},
|
||||
expectedSlices: &slicesByAction{
|
||||
toUpdate: []*discovery.EndpointSlice{
|
||||
simpleEndpointSlice("bar-v4", "10.1.2.3", discovery.AddressTypeIPv4),
|
||||
simpleEndpointSlice("bar-v6", "2001:db8:1111:3333:4444:5555:6666:7777", discovery.AddressTypeIPv6),
|
||||
},
|
||||
},
|
||||
}, {
|
||||
testName: "1 to create and 1 to delete, wrong IP family",
|
||||
startingSlices: &slicesByAction{
|
||||
toCreate: []*discovery.EndpointSlice{
|
||||
simpleEndpointSlice("foo-v4", "10.1.2.3", discovery.AddressTypeIPv4),
|
||||
},
|
||||
toDelete: []*discovery.EndpointSlice{
|
||||
simpleEndpointSlice("bar-v6", "2001:db8:2222:3333:4444:5555:6666:7777", discovery.AddressTypeIPv6),
|
||||
},
|
||||
},
|
||||
expectedSlices: &slicesByAction{
|
||||
toCreate: []*discovery.EndpointSlice{
|
||||
simpleEndpointSlice("foo-v4", "10.1.2.3", discovery.AddressTypeIPv4),
|
||||
},
|
||||
toDelete: []*discovery.EndpointSlice{
|
||||
simpleEndpointSlice("bar-v6", "2001:db8:2222:3333:4444:5555:6666:7777", discovery.AddressTypeIPv6),
|
||||
},
|
||||
},
|
||||
}}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.testName, func(t *testing.T) {
|
||||
recycleSlices(tc.startingSlices)
|
||||
|
||||
expectEqualSlices(t, tc.startingSlices.toCreate, tc.expectedSlices.toCreate)
|
||||
expectEqualSlices(t, tc.startingSlices.toUpdate, tc.expectedSlices.toUpdate)
|
||||
expectEqualSlices(t, tc.startingSlices.toDelete, tc.expectedSlices.toDelete)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Test helpers
|
||||
func expectEqualSlices(t *testing.T, actual, expected []*discovery.EndpointSlice) {
|
||||
t.Helper()
|
||||
if len(actual) != len(expected) {
|
||||
t.Fatalf("Expected %d EndpointSlices, got %d: %v", len(expected), len(actual), actual)
|
||||
}
|
||||
|
||||
for i, expectedSlice := range expected {
|
||||
if expectedSlice.AddressType != actual[i].AddressType {
|
||||
t.Errorf("Expected Slice to have %s address type, got %s", expectedSlice.AddressType, actual[i].AddressType)
|
||||
}
|
||||
|
||||
if expectedSlice.Name != actual[i].Name {
|
||||
t.Errorf("Expected Slice to have %s name, got %s", expectedSlice.Name, actual[i].Name)
|
||||
}
|
||||
|
||||
if len(expectedSlice.Endpoints) != len(actual[i].Endpoints) {
|
||||
t.Fatalf("Expected Slice to have %d endpoints, got %d", len(expectedSlice.Endpoints), len(actual[i].Endpoints))
|
||||
}
|
||||
|
||||
for j, expectedEndpoint := range expectedSlice.Endpoints {
|
||||
actualEndpoint := actual[i].Endpoints[j]
|
||||
if len(expectedEndpoint.Addresses) != len(actualEndpoint.Addresses) {
|
||||
t.Fatalf("Expected Endpoint to have %d addresses, got %d", len(expectedEndpoint.Addresses), len(actualEndpoint.Addresses))
|
||||
}
|
||||
|
||||
for k, expectedAddress := range expectedEndpoint.Addresses {
|
||||
actualAddress := actualEndpoint.Addresses[k]
|
||||
if expectedAddress != actualAddress {
|
||||
t.Fatalf("Expected address to be %s, got %s", expectedAddress, actualAddress)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func simpleEndpointSlice(name, ip string, addrType discovery.AddressType) *discovery.EndpointSlice {
|
||||
return &discovery.EndpointSlice{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
},
|
||||
AddressType: addrType,
|
||||
Endpoints: []discovery.Endpoint{{
|
||||
Addresses: []string{ip},
|
||||
}},
|
||||
}
|
||||
}
|
968
pkg/controller/endpointslicemirroring/reconciler_test.go
Normal file
968
pkg/controller/endpointslicemirroring/reconciler_test.go
Normal file
@ -0,0 +1,968 @@
|
||||
/*
|
||||
Copyright 2020 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 endpointslicemirroring
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
discovery "k8s.io/api/discovery/v1beta1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/kubernetes/fake"
|
||||
"k8s.io/client-go/kubernetes/scheme"
|
||||
"k8s.io/client-go/tools/record"
|
||||
"k8s.io/component-base/metrics/testutil"
|
||||
"k8s.io/kubernetes/pkg/controller/endpointslicemirroring/metrics"
|
||||
utilpointer "k8s.io/utils/pointer"
|
||||
)
|
||||
|
||||
const defaultMaxEndpointsPerSubset = int32(1000)
|
||||
|
||||
// TestReconcile ensures that Endpoints are reconciled into corresponding
|
||||
// EndpointSlices with appropriate fields.
|
||||
func TestReconcile(t *testing.T) {
|
||||
protoTCP := corev1.ProtocolTCP
|
||||
protoUDP := corev1.ProtocolUDP
|
||||
|
||||
testCases := []struct {
|
||||
testName string
|
||||
subsets []corev1.EndpointSubset
|
||||
endpointsDeletionPending bool
|
||||
maxEndpointsPerSubset int32
|
||||
existingEndpointSlices []*discovery.EndpointSlice
|
||||
expectedNumSlices int
|
||||
expectedClientActions int
|
||||
expectedMetrics *expectedMetrics
|
||||
}{{
|
||||
testName: "Endpoints with no subsets",
|
||||
subsets: []corev1.EndpointSubset{},
|
||||
existingEndpointSlices: []*discovery.EndpointSlice{},
|
||||
expectedNumSlices: 0,
|
||||
expectedClientActions: 0,
|
||||
expectedMetrics: &expectedMetrics{},
|
||||
}, {
|
||||
testName: "Endpoints with no addresses",
|
||||
subsets: []corev1.EndpointSubset{{
|
||||
Ports: []corev1.EndpointPort{{
|
||||
Name: "http",
|
||||
Port: 80,
|
||||
Protocol: corev1.ProtocolTCP,
|
||||
}},
|
||||
}},
|
||||
existingEndpointSlices: []*discovery.EndpointSlice{},
|
||||
expectedNumSlices: 0,
|
||||
expectedClientActions: 0,
|
||||
expectedMetrics: &expectedMetrics{},
|
||||
}, {
|
||||
testName: "Endpoints with 1 subset, port, and address",
|
||||
subsets: []corev1.EndpointSubset{{
|
||||
Ports: []corev1.EndpointPort{{
|
||||
Name: "http",
|
||||
Port: 80,
|
||||
Protocol: corev1.ProtocolTCP,
|
||||
}},
|
||||
Addresses: []corev1.EndpointAddress{{
|
||||
IP: "10.0.0.1",
|
||||
Hostname: "pod-1",
|
||||
NodeName: utilpointer.StringPtr("node-1"),
|
||||
}},
|
||||
}},
|
||||
existingEndpointSlices: []*discovery.EndpointSlice{},
|
||||
expectedNumSlices: 1,
|
||||
expectedClientActions: 1,
|
||||
expectedMetrics: &expectedMetrics{desiredSlices: 1, actualSlices: 1, desiredEndpoints: 1, addedPerSync: 1, numCreated: 1},
|
||||
}, {
|
||||
testName: "Endpoints with 1 subset, port, and address, pending deletion",
|
||||
subsets: []corev1.EndpointSubset{{
|
||||
Ports: []corev1.EndpointPort{{
|
||||
Name: "http",
|
||||
Port: 80,
|
||||
Protocol: corev1.ProtocolTCP,
|
||||
}},
|
||||
Addresses: []corev1.EndpointAddress{{
|
||||
IP: "10.0.0.1",
|
||||
Hostname: "pod-1",
|
||||
NodeName: utilpointer.StringPtr("node-1"),
|
||||
}},
|
||||
}},
|
||||
endpointsDeletionPending: true,
|
||||
existingEndpointSlices: []*discovery.EndpointSlice{},
|
||||
expectedNumSlices: 0,
|
||||
expectedClientActions: 0,
|
||||
}, {
|
||||
testName: "Endpoints with 1 subset, 2 ports, and 2 addresses",
|
||||
subsets: []corev1.EndpointSubset{{
|
||||
Ports: []corev1.EndpointPort{{
|
||||
Name: "http",
|
||||
Port: 80,
|
||||
Protocol: corev1.ProtocolTCP,
|
||||
}, {
|
||||
Name: "https",
|
||||
Port: 443,
|
||||
Protocol: corev1.ProtocolUDP,
|
||||
}},
|
||||
Addresses: []corev1.EndpointAddress{{
|
||||
IP: "10.0.0.1",
|
||||
Hostname: "pod-1",
|
||||
NodeName: utilpointer.StringPtr("node-1"),
|
||||
}, {
|
||||
IP: "10.0.0.2",
|
||||
Hostname: "pod-2",
|
||||
NodeName: utilpointer.StringPtr("node-2"),
|
||||
}},
|
||||
}},
|
||||
existingEndpointSlices: []*discovery.EndpointSlice{},
|
||||
expectedNumSlices: 1,
|
||||
expectedClientActions: 1,
|
||||
expectedMetrics: &expectedMetrics{desiredSlices: 1, actualSlices: 1, desiredEndpoints: 2, addedPerSync: 2, numCreated: 1},
|
||||
}, {
|
||||
testName: "Endpoints with 2 subsets, multiple ports and addresses",
|
||||
subsets: []corev1.EndpointSubset{{
|
||||
Ports: []corev1.EndpointPort{{
|
||||
Name: "http",
|
||||
Port: 80,
|
||||
Protocol: corev1.ProtocolTCP,
|
||||
}, {
|
||||
Name: "https",
|
||||
Port: 443,
|
||||
Protocol: corev1.ProtocolUDP,
|
||||
}},
|
||||
Addresses: []corev1.EndpointAddress{{
|
||||
IP: "10.0.0.1",
|
||||
Hostname: "pod-1",
|
||||
NodeName: utilpointer.StringPtr("node-1"),
|
||||
}, {
|
||||
IP: "10.0.0.2",
|
||||
Hostname: "pod-2",
|
||||
NodeName: utilpointer.StringPtr("node-2"),
|
||||
}},
|
||||
}, {
|
||||
Ports: []corev1.EndpointPort{{
|
||||
Name: "http",
|
||||
Port: 3000,
|
||||
Protocol: corev1.ProtocolTCP,
|
||||
}, {
|
||||
Name: "https",
|
||||
Port: 3001,
|
||||
Protocol: corev1.ProtocolUDP,
|
||||
}},
|
||||
Addresses: []corev1.EndpointAddress{{
|
||||
IP: "10.0.1.1",
|
||||
Hostname: "pod-11",
|
||||
NodeName: utilpointer.StringPtr("node-1"),
|
||||
}, {
|
||||
IP: "10.0.1.2",
|
||||
Hostname: "pod-12",
|
||||
NodeName: utilpointer.StringPtr("node-2"),
|
||||
}, {
|
||||
IP: "10.0.1.3",
|
||||
Hostname: "pod-13",
|
||||
NodeName: utilpointer.StringPtr("node-3"),
|
||||
}},
|
||||
}},
|
||||
existingEndpointSlices: []*discovery.EndpointSlice{},
|
||||
expectedNumSlices: 2,
|
||||
expectedClientActions: 2,
|
||||
expectedMetrics: &expectedMetrics{desiredSlices: 2, actualSlices: 2, desiredEndpoints: 5, addedPerSync: 5, numCreated: 2},
|
||||
}, {
|
||||
testName: "Endpoints with 2 subsets, multiple ports and addresses, existing empty EndpointSlice",
|
||||
subsets: []corev1.EndpointSubset{{
|
||||
Ports: []corev1.EndpointPort{{
|
||||
Name: "http",
|
||||
Port: 80,
|
||||
Protocol: corev1.ProtocolTCP,
|
||||
}, {
|
||||
Name: "https",
|
||||
Port: 443,
|
||||
Protocol: corev1.ProtocolUDP,
|
||||
}},
|
||||
Addresses: []corev1.EndpointAddress{{
|
||||
IP: "10.0.0.1",
|
||||
Hostname: "pod-1",
|
||||
NodeName: utilpointer.StringPtr("node-1"),
|
||||
}, {
|
||||
IP: "10.0.0.2",
|
||||
Hostname: "pod-2",
|
||||
NodeName: utilpointer.StringPtr("node-2"),
|
||||
}},
|
||||
}, {
|
||||
Ports: []corev1.EndpointPort{{
|
||||
Name: "http",
|
||||
Port: 3000,
|
||||
Protocol: corev1.ProtocolTCP,
|
||||
}, {
|
||||
Name: "https",
|
||||
Port: 3001,
|
||||
Protocol: corev1.ProtocolUDP,
|
||||
}},
|
||||
Addresses: []corev1.EndpointAddress{{
|
||||
IP: "10.0.1.1",
|
||||
Hostname: "pod-11",
|
||||
NodeName: utilpointer.StringPtr("node-1"),
|
||||
}, {
|
||||
IP: "10.0.1.2",
|
||||
Hostname: "pod-12",
|
||||
NodeName: utilpointer.StringPtr("node-2"),
|
||||
}, {
|
||||
IP: "10.0.1.3",
|
||||
Hostname: "pod-13",
|
||||
NodeName: utilpointer.StringPtr("node-3"),
|
||||
}},
|
||||
}},
|
||||
existingEndpointSlices: []*discovery.EndpointSlice{{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-ep-1",
|
||||
},
|
||||
AddressType: discovery.AddressTypeIPv4,
|
||||
Ports: []discovery.EndpointPort{{
|
||||
Name: utilpointer.StringPtr("http"),
|
||||
Port: utilpointer.Int32Ptr(80),
|
||||
Protocol: &protoTCP,
|
||||
}, {
|
||||
Name: utilpointer.StringPtr("https"),
|
||||
Port: utilpointer.Int32Ptr(443),
|
||||
Protocol: &protoUDP,
|
||||
}},
|
||||
}},
|
||||
expectedNumSlices: 2,
|
||||
expectedClientActions: 2,
|
||||
expectedMetrics: &expectedMetrics{desiredSlices: 2, actualSlices: 2, desiredEndpoints: 5, addedPerSync: 5, numCreated: 1, numUpdated: 1},
|
||||
}, {
|
||||
testName: "Endpoints with 2 subsets, multiple ports and addresses, existing EndpointSlice with some addresses",
|
||||
subsets: []corev1.EndpointSubset{{
|
||||
Ports: []corev1.EndpointPort{{
|
||||
Name: "http",
|
||||
Port: 80,
|
||||
Protocol: corev1.ProtocolTCP,
|
||||
}, {
|
||||
Name: "https",
|
||||
Port: 443,
|
||||
Protocol: corev1.ProtocolUDP,
|
||||
}},
|
||||
Addresses: []corev1.EndpointAddress{{
|
||||
IP: "10.0.0.1",
|
||||
Hostname: "pod-1",
|
||||
NodeName: utilpointer.StringPtr("node-1"),
|
||||
}, {
|
||||
IP: "10.0.0.2",
|
||||
Hostname: "pod-2",
|
||||
NodeName: utilpointer.StringPtr("node-2"),
|
||||
}},
|
||||
}, {
|
||||
Ports: []corev1.EndpointPort{{
|
||||
Name: "http",
|
||||
Port: 3000,
|
||||
Protocol: corev1.ProtocolTCP,
|
||||
}, {
|
||||
Name: "https",
|
||||
Port: 3001,
|
||||
Protocol: corev1.ProtocolUDP,
|
||||
}},
|
||||
Addresses: []corev1.EndpointAddress{{
|
||||
IP: "10.0.1.1",
|
||||
Hostname: "pod-11",
|
||||
NodeName: utilpointer.StringPtr("node-1"),
|
||||
}, {
|
||||
IP: "10.0.1.2",
|
||||
Hostname: "pod-12",
|
||||
NodeName: utilpointer.StringPtr("node-2"),
|
||||
}, {
|
||||
IP: "10.0.1.3",
|
||||
Hostname: "pod-13",
|
||||
NodeName: utilpointer.StringPtr("node-3"),
|
||||
}},
|
||||
}},
|
||||
existingEndpointSlices: []*discovery.EndpointSlice{{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-ep-1",
|
||||
},
|
||||
AddressType: discovery.AddressTypeIPv4,
|
||||
Ports: []discovery.EndpointPort{{
|
||||
Name: utilpointer.StringPtr("http"),
|
||||
Port: utilpointer.Int32Ptr(80),
|
||||
Protocol: &protoTCP,
|
||||
}, {
|
||||
Name: utilpointer.StringPtr("https"),
|
||||
Port: utilpointer.Int32Ptr(443),
|
||||
Protocol: &protoUDP,
|
||||
}},
|
||||
Endpoints: []discovery.Endpoint{{
|
||||
Addresses: []string{"10.0.0.2"},
|
||||
Hostname: utilpointer.StringPtr("pod-2"),
|
||||
}, {
|
||||
Addresses: []string{"10.0.0.1", "10.0.0.3"},
|
||||
Hostname: utilpointer.StringPtr("pod-1"),
|
||||
}},
|
||||
}},
|
||||
expectedNumSlices: 2,
|
||||
expectedClientActions: 2,
|
||||
expectedMetrics: &expectedMetrics{desiredSlices: 2, actualSlices: 2, desiredEndpoints: 5, addedPerSync: 4, updatedPerSync: 1, removedPerSync: 1, numCreated: 1, numUpdated: 1},
|
||||
}, {
|
||||
testName: "Endpoints with 2 subsets, multiple ports and addresses, existing EndpointSlice identical to subset",
|
||||
subsets: []corev1.EndpointSubset{{
|
||||
Ports: []corev1.EndpointPort{{
|
||||
Name: "http",
|
||||
Port: 80,
|
||||
Protocol: corev1.ProtocolTCP,
|
||||
}, {
|
||||
Name: "https",
|
||||
Port: 443,
|
||||
Protocol: corev1.ProtocolUDP,
|
||||
}},
|
||||
Addresses: []corev1.EndpointAddress{{
|
||||
IP: "10.0.0.1",
|
||||
Hostname: "pod-1",
|
||||
NodeName: utilpointer.StringPtr("node-1"),
|
||||
}, {
|
||||
IP: "10.0.0.2",
|
||||
Hostname: "pod-2",
|
||||
NodeName: utilpointer.StringPtr("node-2"),
|
||||
}},
|
||||
}, {
|
||||
Ports: []corev1.EndpointPort{{
|
||||
Name: "http",
|
||||
Port: 3000,
|
||||
Protocol: corev1.ProtocolTCP,
|
||||
}, {
|
||||
Name: "https",
|
||||
Port: 3001,
|
||||
Protocol: corev1.ProtocolUDP,
|
||||
}},
|
||||
Addresses: []corev1.EndpointAddress{{
|
||||
IP: "10.0.1.1",
|
||||
Hostname: "pod-11",
|
||||
NodeName: utilpointer.StringPtr("node-1"),
|
||||
}, {
|
||||
IP: "10.0.1.2",
|
||||
Hostname: "pod-12",
|
||||
NodeName: utilpointer.StringPtr("node-2"),
|
||||
}, {
|
||||
IP: "10.0.1.3",
|
||||
Hostname: "pod-13",
|
||||
NodeName: utilpointer.StringPtr("node-3"),
|
||||
}},
|
||||
}},
|
||||
existingEndpointSlices: []*discovery.EndpointSlice{{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-ep-1",
|
||||
},
|
||||
AddressType: discovery.AddressTypeIPv4,
|
||||
Ports: []discovery.EndpointPort{{
|
||||
Name: utilpointer.StringPtr("http"),
|
||||
Port: utilpointer.Int32Ptr(80),
|
||||
Protocol: &protoTCP,
|
||||
}, {
|
||||
Name: utilpointer.StringPtr("https"),
|
||||
Port: utilpointer.Int32Ptr(443),
|
||||
Protocol: &protoUDP,
|
||||
}},
|
||||
Endpoints: []discovery.Endpoint{{
|
||||
Addresses: []string{"10.0.0.1"},
|
||||
Hostname: utilpointer.StringPtr("pod-1"),
|
||||
Topology: map[string]string{"kubernetes.io/hostname": "node-1"},
|
||||
Conditions: discovery.EndpointConditions{Ready: utilpointer.BoolPtr(true)},
|
||||
}, {
|
||||
Addresses: []string{"10.0.0.2"},
|
||||
Hostname: utilpointer.StringPtr("pod-2"),
|
||||
Topology: map[string]string{"kubernetes.io/hostname": "node-2"},
|
||||
Conditions: discovery.EndpointConditions{Ready: utilpointer.BoolPtr(true)},
|
||||
}},
|
||||
}},
|
||||
expectedNumSlices: 2,
|
||||
expectedClientActions: 1,
|
||||
expectedMetrics: &expectedMetrics{desiredSlices: 2, actualSlices: 2, desiredEndpoints: 5, addedPerSync: 3, numCreated: 1},
|
||||
}, {
|
||||
testName: "Endpoints with 2 subsets, multiple ports, and dual stack addresses",
|
||||
subsets: []corev1.EndpointSubset{{
|
||||
Ports: []corev1.EndpointPort{{
|
||||
Name: "http",
|
||||
Port: 80,
|
||||
Protocol: corev1.ProtocolTCP,
|
||||
}, {
|
||||
Name: "https",
|
||||
Port: 443,
|
||||
Protocol: corev1.ProtocolUDP,
|
||||
}},
|
||||
Addresses: []corev1.EndpointAddress{{
|
||||
IP: "2001:db8:2222:3333:4444:5555:6666:7777",
|
||||
Hostname: "pod-1",
|
||||
NodeName: utilpointer.StringPtr("node-1"),
|
||||
}, {
|
||||
IP: "10.0.0.2",
|
||||
Hostname: "pod-2",
|
||||
NodeName: utilpointer.StringPtr("node-2"),
|
||||
}},
|
||||
}, {
|
||||
Ports: []corev1.EndpointPort{{
|
||||
Name: "http",
|
||||
Port: 3000,
|
||||
Protocol: corev1.ProtocolTCP,
|
||||
}, {
|
||||
Name: "https",
|
||||
Port: 3001,
|
||||
Protocol: corev1.ProtocolUDP,
|
||||
}},
|
||||
Addresses: []corev1.EndpointAddress{{
|
||||
IP: "10.0.1.1",
|
||||
Hostname: "pod-11",
|
||||
NodeName: utilpointer.StringPtr("node-1"),
|
||||
}, {
|
||||
IP: "10.0.1.2",
|
||||
Hostname: "pod-12",
|
||||
NodeName: utilpointer.StringPtr("node-2"),
|
||||
}, {
|
||||
IP: "2001:db8:3333:4444:5555:6666:7777:8888",
|
||||
Hostname: "pod-13",
|
||||
NodeName: utilpointer.StringPtr("node-3"),
|
||||
}},
|
||||
}},
|
||||
existingEndpointSlices: []*discovery.EndpointSlice{},
|
||||
expectedNumSlices: 4,
|
||||
expectedClientActions: 4,
|
||||
expectedMetrics: &expectedMetrics{desiredSlices: 4, actualSlices: 4, desiredEndpoints: 5, addedPerSync: 5, numCreated: 4},
|
||||
}, {
|
||||
testName: "Endpoints with 2 subsets, multiple ports, ipv6 only addresses",
|
||||
subsets: []corev1.EndpointSubset{{
|
||||
Ports: []corev1.EndpointPort{{
|
||||
Name: "http",
|
||||
Port: 80,
|
||||
Protocol: corev1.ProtocolTCP,
|
||||
}, {
|
||||
Name: "https",
|
||||
Port: 443,
|
||||
Protocol: corev1.ProtocolUDP,
|
||||
}},
|
||||
Addresses: []corev1.EndpointAddress{{
|
||||
IP: "2001:db8:1111:3333:4444:5555:6666:7777",
|
||||
Hostname: "pod-1",
|
||||
NodeName: utilpointer.StringPtr("node-1"),
|
||||
}, {
|
||||
IP: "2001:db8:2222:3333:4444:5555:6666:7777",
|
||||
Hostname: "pod-2",
|
||||
NodeName: utilpointer.StringPtr("node-2"),
|
||||
}},
|
||||
}, {
|
||||
Ports: []corev1.EndpointPort{{
|
||||
Name: "http",
|
||||
Port: 3000,
|
||||
Protocol: corev1.ProtocolTCP,
|
||||
}, {
|
||||
Name: "https",
|
||||
Port: 3001,
|
||||
Protocol: corev1.ProtocolUDP,
|
||||
}},
|
||||
Addresses: []corev1.EndpointAddress{{
|
||||
IP: "2001:db8:3333:3333:4444:5555:6666:7777",
|
||||
Hostname: "pod-11",
|
||||
NodeName: utilpointer.StringPtr("node-1"),
|
||||
}, {
|
||||
IP: "2001:db8:4444:3333:4444:5555:6666:7777",
|
||||
Hostname: "pod-12",
|
||||
NodeName: utilpointer.StringPtr("node-2"),
|
||||
}, {
|
||||
IP: "2001:db8:5555:3333:4444:5555:6666:7777",
|
||||
Hostname: "pod-13",
|
||||
NodeName: utilpointer.StringPtr("node-3"),
|
||||
}},
|
||||
}},
|
||||
existingEndpointSlices: []*discovery.EndpointSlice{},
|
||||
expectedNumSlices: 2,
|
||||
expectedClientActions: 2,
|
||||
expectedMetrics: &expectedMetrics{desiredSlices: 2, actualSlices: 2, desiredEndpoints: 5, addedPerSync: 5, numCreated: 2},
|
||||
}, {
|
||||
testName: "Endpoints with 2 subsets, multiple ports, some invalid addresses",
|
||||
subsets: []corev1.EndpointSubset{{
|
||||
Ports: []corev1.EndpointPort{{
|
||||
Name: "http",
|
||||
Port: 80,
|
||||
Protocol: corev1.ProtocolTCP,
|
||||
}, {
|
||||
Name: "https",
|
||||
Port: 443,
|
||||
Protocol: corev1.ProtocolUDP,
|
||||
}},
|
||||
Addresses: []corev1.EndpointAddress{{
|
||||
IP: "2001:db8:1111:3333:4444:5555:6666:7777",
|
||||
Hostname: "pod-1",
|
||||
NodeName: utilpointer.StringPtr("node-1"),
|
||||
}, {
|
||||
IP: "this-is-not-an-ip",
|
||||
Hostname: "pod-2",
|
||||
NodeName: utilpointer.StringPtr("node-2"),
|
||||
}},
|
||||
}, {
|
||||
Ports: []corev1.EndpointPort{{
|
||||
Name: "http",
|
||||
Port: 3000,
|
||||
Protocol: corev1.ProtocolTCP,
|
||||
}, {
|
||||
Name: "https",
|
||||
Port: 3001,
|
||||
Protocol: corev1.ProtocolUDP,
|
||||
}},
|
||||
Addresses: []corev1.EndpointAddress{{
|
||||
IP: "this-is-also-not-an-ip",
|
||||
Hostname: "pod-11",
|
||||
NodeName: utilpointer.StringPtr("node-1"),
|
||||
}, {
|
||||
IP: "2001:db8:4444:3333:4444:5555:6666:7777",
|
||||
Hostname: "pod-12",
|
||||
NodeName: utilpointer.StringPtr("node-2"),
|
||||
}, {
|
||||
IP: "2001:db8:5555:3333:4444:5555:6666:7777",
|
||||
Hostname: "pod-13",
|
||||
NodeName: utilpointer.StringPtr("node-3"),
|
||||
}},
|
||||
}},
|
||||
existingEndpointSlices: []*discovery.EndpointSlice{},
|
||||
expectedNumSlices: 2,
|
||||
expectedClientActions: 2,
|
||||
expectedMetrics: &expectedMetrics{desiredSlices: 2, actualSlices: 2, desiredEndpoints: 3, addedPerSync: 3, numCreated: 2},
|
||||
}, {
|
||||
testName: "Endpoints with 2 subsets, multiple ports, all invalid addresses",
|
||||
subsets: []corev1.EndpointSubset{{
|
||||
Ports: []corev1.EndpointPort{{
|
||||
Name: "http",
|
||||
Port: 80,
|
||||
Protocol: corev1.ProtocolTCP,
|
||||
}, {
|
||||
Name: "https",
|
||||
Port: 443,
|
||||
Protocol: corev1.ProtocolUDP,
|
||||
}},
|
||||
Addresses: []corev1.EndpointAddress{{
|
||||
IP: "this-is-not-an-ip1",
|
||||
Hostname: "pod-1",
|
||||
NodeName: utilpointer.StringPtr("node-1"),
|
||||
}, {
|
||||
IP: "this-is-not-an-ip12",
|
||||
Hostname: "pod-2",
|
||||
NodeName: utilpointer.StringPtr("node-2"),
|
||||
}},
|
||||
}, {
|
||||
Ports: []corev1.EndpointPort{{
|
||||
Name: "http",
|
||||
Port: 3000,
|
||||
Protocol: corev1.ProtocolTCP,
|
||||
}, {
|
||||
Name: "https",
|
||||
Port: 3001,
|
||||
Protocol: corev1.ProtocolUDP,
|
||||
}},
|
||||
Addresses: []corev1.EndpointAddress{{
|
||||
IP: "this-is-not-an-ip11",
|
||||
Hostname: "pod-11",
|
||||
NodeName: utilpointer.StringPtr("node-1"),
|
||||
}, {
|
||||
IP: "this-is-not-an-ip12",
|
||||
Hostname: "pod-12",
|
||||
NodeName: utilpointer.StringPtr("node-2"),
|
||||
}, {
|
||||
IP: "this-is-not-an-ip3",
|
||||
Hostname: "pod-13",
|
||||
NodeName: utilpointer.StringPtr("node-3"),
|
||||
}},
|
||||
}},
|
||||
existingEndpointSlices: []*discovery.EndpointSlice{},
|
||||
expectedNumSlices: 0,
|
||||
expectedClientActions: 0,
|
||||
expectedMetrics: &expectedMetrics{desiredSlices: 0, actualSlices: 0, desiredEndpoints: 0, addedPerSync: 0, numCreated: 0},
|
||||
}, {
|
||||
testName: "Endpoints with 2 subsets, multiple ports and addresses, existing EndpointSlice with some addresses",
|
||||
subsets: []corev1.EndpointSubset{{
|
||||
Ports: []corev1.EndpointPort{{
|
||||
Name: "http",
|
||||
Port: 80,
|
||||
Protocol: corev1.ProtocolTCP,
|
||||
}, {
|
||||
Name: "https",
|
||||
Port: 443,
|
||||
Protocol: corev1.ProtocolUDP,
|
||||
}},
|
||||
Addresses: []corev1.EndpointAddress{{
|
||||
IP: "10.0.0.1",
|
||||
Hostname: "pod-1",
|
||||
NodeName: utilpointer.StringPtr("node-1"),
|
||||
}, {
|
||||
IP: "10.0.0.2",
|
||||
Hostname: "pod-2",
|
||||
NodeName: utilpointer.StringPtr("node-2"),
|
||||
}},
|
||||
}, {
|
||||
Ports: []corev1.EndpointPort{{
|
||||
Name: "http",
|
||||
Port: 3000,
|
||||
Protocol: corev1.ProtocolTCP,
|
||||
}, {
|
||||
Name: "https",
|
||||
Port: 3001,
|
||||
Protocol: corev1.ProtocolUDP,
|
||||
}},
|
||||
Addresses: []corev1.EndpointAddress{{
|
||||
IP: "10.0.1.1",
|
||||
Hostname: "pod-11",
|
||||
NodeName: utilpointer.StringPtr("node-1"),
|
||||
}, {
|
||||
IP: "10.0.1.2",
|
||||
Hostname: "pod-12",
|
||||
NodeName: utilpointer.StringPtr("node-2"),
|
||||
}, {
|
||||
IP: "10.0.1.3",
|
||||
Hostname: "pod-13",
|
||||
NodeName: utilpointer.StringPtr("node-3"),
|
||||
}},
|
||||
}},
|
||||
existingEndpointSlices: []*discovery.EndpointSlice{},
|
||||
expectedNumSlices: 2,
|
||||
expectedClientActions: 2,
|
||||
maxEndpointsPerSubset: 2,
|
||||
expectedMetrics: &expectedMetrics{desiredSlices: 2, actualSlices: 2, desiredEndpoints: 4, addedPerSync: 4, updatedPerSync: 0, removedPerSync: 0, numCreated: 2, numUpdated: 0},
|
||||
}}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.testName, func(t *testing.T) {
|
||||
client := newClientset()
|
||||
setupMetrics()
|
||||
namespace := "test"
|
||||
endpoints := corev1.Endpoints{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "test-ep", Namespace: namespace},
|
||||
Subsets: tc.subsets,
|
||||
}
|
||||
|
||||
if tc.endpointsDeletionPending {
|
||||
now := metav1.Now()
|
||||
endpoints.DeletionTimestamp = &now
|
||||
}
|
||||
|
||||
numInitialActions := 0
|
||||
for _, epSlice := range tc.existingEndpointSlices {
|
||||
epSlice.Labels = map[string]string{
|
||||
discovery.LabelServiceName: endpoints.Name,
|
||||
discovery.LabelManagedBy: controllerName,
|
||||
}
|
||||
_, err := client.DiscoveryV1beta1().EndpointSlices(namespace).Create(context.TODO(), epSlice, metav1.CreateOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error creating EndpointSlice, got %v", err)
|
||||
}
|
||||
numInitialActions++
|
||||
}
|
||||
|
||||
maxEndpointsPerSubset := tc.maxEndpointsPerSubset
|
||||
if maxEndpointsPerSubset == 0 {
|
||||
maxEndpointsPerSubset = defaultMaxEndpointsPerSubset
|
||||
}
|
||||
r := newReconciler(client, maxEndpointsPerSubset)
|
||||
reconcileHelper(t, r, &endpoints, tc.existingEndpointSlices)
|
||||
|
||||
numExtraActions := len(client.Actions()) - numInitialActions
|
||||
if numExtraActions != tc.expectedClientActions {
|
||||
t.Fatalf("Expected %d additional client actions, got %d: %#v", tc.expectedClientActions, numExtraActions, client.Actions()[numInitialActions:])
|
||||
}
|
||||
|
||||
if tc.expectedMetrics != nil {
|
||||
expectMetrics(t, *tc.expectedMetrics)
|
||||
}
|
||||
|
||||
endpointSlices := fetchEndpointSlices(t, client, namespace)
|
||||
expectEndpointSlices(t, tc.expectedNumSlices, int(maxEndpointsPerSubset), endpoints, endpointSlices)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Test Helpers
|
||||
|
||||
func newReconciler(client *fake.Clientset, maxEndpointsPerSubset int32) *reconciler {
|
||||
broadcaster := record.NewBroadcaster()
|
||||
recorder := broadcaster.NewRecorder(scheme.Scheme, corev1.EventSource{Component: "endpoint-slice-mirroring-controller"})
|
||||
|
||||
return &reconciler{
|
||||
client: client,
|
||||
maxEndpointsPerSubset: maxEndpointsPerSubset,
|
||||
endpointSliceTracker: newEndpointSliceTracker(),
|
||||
metricsCache: metrics.NewCache(maxEndpointsPerSubset),
|
||||
eventRecorder: recorder,
|
||||
}
|
||||
}
|
||||
|
||||
func expectEndpointSlices(t *testing.T, num, maxEndpointsPerSubset int, endpoints corev1.Endpoints, endpointSlices []discovery.EndpointSlice) {
|
||||
t.Helper()
|
||||
if len(endpointSlices) != num {
|
||||
t.Fatalf("Expected %d EndpointSlices, got %d", num, len(endpointSlices))
|
||||
}
|
||||
|
||||
if num == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
for _, epSlice := range endpointSlices {
|
||||
if !strings.HasPrefix(epSlice.Name, endpoints.Name) {
|
||||
t.Errorf("Expected EndpointSlice name to start with %s, got %s", endpoints.Name, epSlice.Name)
|
||||
}
|
||||
|
||||
serviceNameVal, ok := epSlice.Labels[discovery.LabelServiceName]
|
||||
if !ok {
|
||||
t.Errorf("Expected EndpointSlice to have %s label set", discovery.LabelServiceName)
|
||||
}
|
||||
if serviceNameVal != endpoints.Name {
|
||||
t.Errorf("Expected EndpointSlice to have %s label set to %s, got %s", discovery.LabelServiceName, endpoints.Name, serviceNameVal)
|
||||
}
|
||||
}
|
||||
|
||||
for _, epSubset := range endpoints.Subsets {
|
||||
if len(epSubset.Addresses) == 0 && len(epSubset.NotReadyAddresses) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
var matchingEndpointsV4, matchingEndpointsV6 []discovery.Endpoint
|
||||
|
||||
for _, epSlice := range endpointSlices {
|
||||
if portsMatch(epSubset.Ports, epSlice.Ports) {
|
||||
switch epSlice.AddressType {
|
||||
case discovery.AddressTypeIPv4:
|
||||
matchingEndpointsV4 = append(matchingEndpointsV4, epSlice.Endpoints...)
|
||||
case discovery.AddressTypeIPv6:
|
||||
matchingEndpointsV6 = append(matchingEndpointsV6, epSlice.Endpoints...)
|
||||
default:
|
||||
t.Fatalf("Unexpected EndpointSlice address type found: %v", epSlice.AddressType)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(matchingEndpointsV4) == 0 && len(matchingEndpointsV6) == 0 {
|
||||
t.Fatalf("No EndpointSlices match Endpoints subset: %#v", epSubset.Ports)
|
||||
}
|
||||
|
||||
expectMatchingAddresses(t, epSubset, matchingEndpointsV4, discovery.AddressTypeIPv4, maxEndpointsPerSubset)
|
||||
expectMatchingAddresses(t, epSubset, matchingEndpointsV6, discovery.AddressTypeIPv6, maxEndpointsPerSubset)
|
||||
}
|
||||
}
|
||||
|
||||
func portsMatch(epPorts []corev1.EndpointPort, epsPorts []discovery.EndpointPort) bool {
|
||||
if len(epPorts) != len(epsPorts) {
|
||||
return false
|
||||
}
|
||||
|
||||
portsToBeMatched := map[int32]corev1.EndpointPort{}
|
||||
|
||||
for _, epPort := range epPorts {
|
||||
portsToBeMatched[epPort.Port] = epPort
|
||||
}
|
||||
|
||||
for _, epsPort := range epsPorts {
|
||||
epPort, ok := portsToBeMatched[*epsPort.Port]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
delete(portsToBeMatched, *epsPort.Port)
|
||||
|
||||
if epPort.Name != *epsPort.Name {
|
||||
return false
|
||||
}
|
||||
if epPort.Port != *epsPort.Port {
|
||||
return false
|
||||
}
|
||||
if epPort.Protocol != *epsPort.Protocol {
|
||||
return false
|
||||
}
|
||||
if epPort.AppProtocol != epsPort.AppProtocol {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func expectMatchingAddresses(t *testing.T, epSubset corev1.EndpointSubset, esEndpoints []discovery.Endpoint, addrType discovery.AddressType, maxEndpointsPerSubset int) {
|
||||
t.Helper()
|
||||
type addressInfo struct {
|
||||
ready bool
|
||||
epAddress corev1.EndpointAddress
|
||||
}
|
||||
|
||||
// This approach assumes that each IP is unique within an EndpointSubset.
|
||||
expectedEndpoints := map[string]addressInfo{}
|
||||
|
||||
for _, address := range epSubset.Addresses {
|
||||
at := getAddressType(address.IP)
|
||||
if at != nil && *at == addrType && len(expectedEndpoints) < maxEndpointsPerSubset {
|
||||
expectedEndpoints[address.IP] = addressInfo{
|
||||
ready: true,
|
||||
epAddress: address,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, address := range epSubset.NotReadyAddresses {
|
||||
at := getAddressType(address.IP)
|
||||
if at != nil && *at == addrType && len(expectedEndpoints) < maxEndpointsPerSubset {
|
||||
expectedEndpoints[address.IP] = addressInfo{
|
||||
ready: false,
|
||||
epAddress: address,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(expectedEndpoints) != len(esEndpoints) {
|
||||
t.Errorf("Expected %d endpoints, got %d", len(expectedEndpoints), len(esEndpoints))
|
||||
}
|
||||
|
||||
for _, endpoint := range esEndpoints {
|
||||
if len(endpoint.Addresses) != 1 {
|
||||
t.Fatalf("Expected endpoint to have 1 address, got %d", len(endpoint.Addresses))
|
||||
}
|
||||
address := endpoint.Addresses[0]
|
||||
expectedEndpoint, ok := expectedEndpoints[address]
|
||||
|
||||
if !ok {
|
||||
t.Fatalf("EndpointSlice has endpoint with unexpected address: %s", address)
|
||||
}
|
||||
|
||||
if expectedEndpoint.ready != *endpoint.Conditions.Ready {
|
||||
t.Errorf("Expected ready to be %t, got %t", expectedEndpoint.ready, *endpoint.Conditions.Ready)
|
||||
}
|
||||
|
||||
if endpoint.Hostname == nil {
|
||||
if expectedEndpoint.epAddress.Hostname != "" {
|
||||
t.Errorf("Expected hostname to be %s, got nil", expectedEndpoint.epAddress.Hostname)
|
||||
}
|
||||
} else if expectedEndpoint.epAddress.Hostname != *endpoint.Hostname {
|
||||
t.Errorf("Expected hostname to be %s, got %s", expectedEndpoint.epAddress.Hostname, *endpoint.Hostname)
|
||||
}
|
||||
|
||||
if expectedEndpoint.epAddress.NodeName != nil {
|
||||
topologyHostname, ok := endpoint.Topology["kubernetes.io/hostname"]
|
||||
if !ok {
|
||||
t.Errorf("Expected topology[kubernetes.io/hostname] to be set")
|
||||
} else if *expectedEndpoint.epAddress.NodeName != topologyHostname {
|
||||
t.Errorf("Expected topology[kubernetes.io/hostname] to be %s, got %s", *expectedEndpoint.epAddress.NodeName, topologyHostname)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func fetchEndpointSlices(t *testing.T, client *fake.Clientset, namespace string) []discovery.EndpointSlice {
|
||||
t.Helper()
|
||||
fetchedSlices, err := client.DiscoveryV1beta1().EndpointSlices(namespace).List(context.TODO(), metav1.ListOptions{
|
||||
LabelSelector: discovery.LabelManagedBy + "=" + controllerName,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error fetching Endpoint Slices, got: %v", err)
|
||||
return []discovery.EndpointSlice{}
|
||||
}
|
||||
return fetchedSlices.Items
|
||||
}
|
||||
|
||||
func reconcileHelper(t *testing.T, r *reconciler, endpoints *corev1.Endpoints, existingSlices []*discovery.EndpointSlice) {
|
||||
t.Helper()
|
||||
err := r.reconcile(endpoints, existingSlices)
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error reconciling Endpoint Slices, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Metrics helpers
|
||||
|
||||
type expectedMetrics struct {
|
||||
desiredSlices int
|
||||
actualSlices int
|
||||
desiredEndpoints int
|
||||
addedPerSync int
|
||||
updatedPerSync int
|
||||
removedPerSync int
|
||||
numCreated int
|
||||
numUpdated int
|
||||
numDeleted int
|
||||
}
|
||||
|
||||
func expectMetrics(t *testing.T, em expectedMetrics) {
|
||||
t.Helper()
|
||||
|
||||
actualDesiredSlices, err := testutil.GetGaugeMetricValue(metrics.DesiredEndpointSlices.WithLabelValues())
|
||||
handleErr(t, err, "desiredEndpointSlices")
|
||||
if actualDesiredSlices != float64(em.desiredSlices) {
|
||||
t.Errorf("Expected desiredEndpointSlices to be %d, got %v", em.desiredSlices, actualDesiredSlices)
|
||||
}
|
||||
|
||||
actualNumSlices, err := testutil.GetGaugeMetricValue(metrics.NumEndpointSlices.WithLabelValues())
|
||||
handleErr(t, err, "numEndpointSlices")
|
||||
if actualNumSlices != float64(em.actualSlices) {
|
||||
t.Errorf("Expected numEndpointSlices to be %d, got %v", em.actualSlices, actualNumSlices)
|
||||
}
|
||||
|
||||
actualEndpointsDesired, err := testutil.GetGaugeMetricValue(metrics.EndpointsDesired.WithLabelValues())
|
||||
handleErr(t, err, "desiredEndpoints")
|
||||
if actualEndpointsDesired != float64(em.desiredEndpoints) {
|
||||
t.Errorf("Expected desiredEndpoints to be %d, got %v", em.desiredEndpoints, actualEndpointsDesired)
|
||||
}
|
||||
|
||||
actualAddedPerSync, err := testutil.GetHistogramMetricValue(metrics.EndpointsAddedPerSync.WithLabelValues())
|
||||
handleErr(t, err, "endpointsAddedPerSync")
|
||||
if actualAddedPerSync != float64(em.addedPerSync) {
|
||||
t.Errorf("Expected endpointsAddedPerSync to be %d, got %v", em.addedPerSync, actualAddedPerSync)
|
||||
}
|
||||
|
||||
actualUpdatedPerSync, err := testutil.GetHistogramMetricValue(metrics.EndpointsUpdatedPerSync.WithLabelValues())
|
||||
handleErr(t, err, "endpointsUpdatedPerSync")
|
||||
if actualUpdatedPerSync != float64(em.updatedPerSync) {
|
||||
t.Errorf("Expected endpointsUpdatedPerSync to be %d, got %v", em.updatedPerSync, actualUpdatedPerSync)
|
||||
}
|
||||
|
||||
actualRemovedPerSync, err := testutil.GetHistogramMetricValue(metrics.EndpointsRemovedPerSync.WithLabelValues())
|
||||
handleErr(t, err, "endpointsRemovedPerSync")
|
||||
if actualRemovedPerSync != float64(em.removedPerSync) {
|
||||
t.Errorf("Expected endpointsRemovedPerSync to be %d, got %v", em.removedPerSync, actualRemovedPerSync)
|
||||
}
|
||||
|
||||
actualCreated, err := testutil.GetCounterMetricValue(metrics.EndpointSliceChanges.WithLabelValues("create"))
|
||||
handleErr(t, err, "endpointSliceChangesCreated")
|
||||
if actualCreated != float64(em.numCreated) {
|
||||
t.Errorf("Expected endpointSliceChangesCreated to be %d, got %v", em.numCreated, actualCreated)
|
||||
}
|
||||
|
||||
actualUpdated, err := testutil.GetCounterMetricValue(metrics.EndpointSliceChanges.WithLabelValues("update"))
|
||||
handleErr(t, err, "endpointSliceChangesUpdated")
|
||||
if actualUpdated != float64(em.numUpdated) {
|
||||
t.Errorf("Expected endpointSliceChangesUpdated to be %d, got %v", em.numUpdated, actualUpdated)
|
||||
}
|
||||
|
||||
actualDeleted, err := testutil.GetCounterMetricValue(metrics.EndpointSliceChanges.WithLabelValues("delete"))
|
||||
handleErr(t, err, "desiredEndpointSlices")
|
||||
if actualDeleted != float64(em.numDeleted) {
|
||||
t.Errorf("Expected endpointSliceChangesDeleted to be %d, got %v", em.numDeleted, actualDeleted)
|
||||
}
|
||||
}
|
||||
|
||||
func handleErr(t *testing.T, err error, metricName string) {
|
||||
if err != nil {
|
||||
t.Errorf("Failed to get %s value, err: %v", metricName, err)
|
||||
}
|
||||
}
|
||||
|
||||
func setupMetrics() {
|
||||
metrics.RegisterMetrics()
|
||||
metrics.NumEndpointSlices.Delete(map[string]string{})
|
||||
metrics.DesiredEndpointSlices.Delete(map[string]string{})
|
||||
metrics.EndpointsDesired.Delete(map[string]string{})
|
||||
metrics.EndpointsAddedPerSync.Delete(map[string]string{})
|
||||
metrics.EndpointsUpdatedPerSync.Delete(map[string]string{})
|
||||
metrics.EndpointsRemovedPerSync.Delete(map[string]string{})
|
||||
metrics.EndpointSliceChanges.Delete(map[string]string{"operation": "create"})
|
||||
metrics.EndpointSliceChanges.Delete(map[string]string{"operation": "update"})
|
||||
metrics.EndpointSliceChanges.Delete(map[string]string{"operation": "delete"})
|
||||
}
|
250
pkg/controller/endpointslicemirroring/utils.go
Normal file
250
pkg/controller/endpointslicemirroring/utils.go
Normal file
@ -0,0 +1,250 @@
|
||||
/*
|
||||
Copyright 2020 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 endpointslicemirroring
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
discovery "k8s.io/api/discovery/v1beta1"
|
||||
apiequality "k8s.io/apimachinery/pkg/api/equality"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
"k8s.io/client-go/tools/leaderelection/resourcelock"
|
||||
"k8s.io/kubernetes/pkg/apis/discovery/validation"
|
||||
endpointutil "k8s.io/kubernetes/pkg/controller/util/endpoint"
|
||||
)
|
||||
|
||||
// addrTypePortMapKey is used to uniquely identify groups of endpoint ports and
|
||||
// address types.
|
||||
type addrTypePortMapKey string
|
||||
|
||||
// newAddrTypePortMapKey generates a PortMapKey from endpoint ports.
|
||||
func newAddrTypePortMapKey(endpointPorts []discovery.EndpointPort, addrType discovery.AddressType) addrTypePortMapKey {
|
||||
pmk := fmt.Sprintf("%s-%s", addrType, endpointutil.NewPortMapKey(endpointPorts))
|
||||
return addrTypePortMapKey(pmk)
|
||||
}
|
||||
|
||||
func (pk addrTypePortMapKey) addressType() discovery.AddressType {
|
||||
if strings.HasPrefix(string(pk), string(discovery.AddressTypeIPv6)) {
|
||||
return discovery.AddressTypeIPv6
|
||||
}
|
||||
return discovery.AddressTypeIPv4
|
||||
}
|
||||
|
||||
func getAddressType(address string) *discovery.AddressType {
|
||||
ip := net.ParseIP(address)
|
||||
if ip == nil {
|
||||
return nil
|
||||
}
|
||||
addressType := discovery.AddressTypeIPv4
|
||||
if ip.To4() == nil {
|
||||
addressType = discovery.AddressTypeIPv6
|
||||
}
|
||||
return &addressType
|
||||
}
|
||||
|
||||
// endpointsEqualBeyondHash returns true if endpoints have equal attributes
|
||||
// but excludes equality checks that would have already been covered with
|
||||
// endpoint hashing (see hashEndpoint func for more info).
|
||||
func endpointsEqualBeyondHash(ep1, ep2 *discovery.Endpoint) bool {
|
||||
if !apiequality.Semantic.DeepEqual(ep1.Topology, ep2.Topology) {
|
||||
return false
|
||||
}
|
||||
|
||||
if !boolPtrEqual(ep1.Conditions.Ready, ep2.Conditions.Ready) {
|
||||
return false
|
||||
}
|
||||
|
||||
if !objectRefPtrEqual(ep1.TargetRef, ep2.TargetRef) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// newEndpointSlice returns an EndpointSlice generated from an Endpoints
|
||||
// resource, ports, and address type.
|
||||
func newEndpointSlice(endpoints *corev1.Endpoints, ports []discovery.EndpointPort, addrType discovery.AddressType, sliceName string) *discovery.EndpointSlice {
|
||||
gvk := schema.GroupVersionKind{Version: "v1", Kind: "Endpoints"}
|
||||
ownerRef := metav1.NewControllerRef(endpoints, gvk)
|
||||
epSlice := &discovery.EndpointSlice{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Labels: map[string]string{
|
||||
discovery.LabelServiceName: endpoints.Name,
|
||||
discovery.LabelManagedBy: controllerName,
|
||||
},
|
||||
OwnerReferences: []metav1.OwnerReference{*ownerRef},
|
||||
Namespace: endpoints.Namespace,
|
||||
},
|
||||
Ports: ports,
|
||||
AddressType: addrType,
|
||||
Endpoints: []discovery.Endpoint{},
|
||||
}
|
||||
|
||||
if sliceName == "" {
|
||||
epSlice.GenerateName = getEndpointSlicePrefix(endpoints.Name)
|
||||
} else {
|
||||
epSlice.Name = sliceName
|
||||
}
|
||||
|
||||
return epSlice
|
||||
}
|
||||
|
||||
// getEndpointSlicePrefix returns a suitable prefix for an EndpointSlice name.
|
||||
func getEndpointSlicePrefix(serviceName string) string {
|
||||
// use the dash (if the name isn't too long) to make the name a bit prettier.
|
||||
prefix := fmt.Sprintf("%s-", serviceName)
|
||||
if len(validation.ValidateEndpointSliceName(prefix, true)) != 0 {
|
||||
prefix = serviceName
|
||||
}
|
||||
return prefix
|
||||
}
|
||||
|
||||
// addressToEndpoint converts an address from an Endpoints resource to an
|
||||
// EndpointSlice endpoint.
|
||||
func addressToEndpoint(address corev1.EndpointAddress, ready bool) *discovery.Endpoint {
|
||||
endpoint := &discovery.Endpoint{
|
||||
Addresses: []string{address.IP},
|
||||
Conditions: discovery.EndpointConditions{
|
||||
Ready: &ready,
|
||||
},
|
||||
TargetRef: address.TargetRef,
|
||||
}
|
||||
|
||||
if address.NodeName != nil {
|
||||
endpoint.Topology = map[string]string{
|
||||
"kubernetes.io/hostname": *address.NodeName,
|
||||
}
|
||||
}
|
||||
if address.Hostname != "" {
|
||||
endpoint.Hostname = &address.Hostname
|
||||
}
|
||||
|
||||
return endpoint
|
||||
}
|
||||
|
||||
// epPortsToEpsPorts converts ports from an Endpoints resource to ports for an
|
||||
// EndpointSlice resource.
|
||||
func epPortsToEpsPorts(epPorts []corev1.EndpointPort) []discovery.EndpointPort {
|
||||
epsPorts := []discovery.EndpointPort{}
|
||||
for _, epPort := range epPorts {
|
||||
epp := epPort.DeepCopy()
|
||||
epsPorts = append(epsPorts, discovery.EndpointPort{
|
||||
Name: &epp.Name,
|
||||
Port: &epp.Port,
|
||||
Protocol: &epp.Protocol,
|
||||
AppProtocol: epp.AppProtocol,
|
||||
})
|
||||
}
|
||||
return epsPorts
|
||||
}
|
||||
|
||||
// boolPtrEqual returns true if a set of bool pointers have equivalent values.
|
||||
func boolPtrEqual(ptr1, ptr2 *bool) bool {
|
||||
if (ptr1 == nil) != (ptr2 == nil) {
|
||||
return false
|
||||
}
|
||||
if ptr1 != nil && ptr2 != nil && *ptr1 != *ptr2 {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// objectRefPtrEqual returns true if a set of object ref pointers have
|
||||
// equivalent values.
|
||||
func objectRefPtrEqual(ref1, ref2 *corev1.ObjectReference) bool {
|
||||
if (ref1 == nil) != (ref2 == nil) {
|
||||
return false
|
||||
}
|
||||
if ref1 != nil && ref2 != nil && !apiequality.Semantic.DeepEqual(*ref1, *ref2) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// getEndpointsFromDeleteAction parses an Endpoints resource from a delete
|
||||
// action.
|
||||
func getEndpointsFromDeleteAction(obj interface{}) *corev1.Endpoints {
|
||||
if endpointSlice, ok := obj.(*corev1.Endpoints); ok {
|
||||
return endpointSlice
|
||||
}
|
||||
// If we reached here it means the EndpointSlice was deleted but its final
|
||||
// state is unrecorded.
|
||||
tombstone, ok := obj.(cache.DeletedFinalStateUnknown)
|
||||
if !ok {
|
||||
utilruntime.HandleError(fmt.Errorf("Couldn't get object from tombstone %#v", obj))
|
||||
return nil
|
||||
}
|
||||
endpoints, ok := tombstone.Obj.(*corev1.Endpoints)
|
||||
if !ok {
|
||||
utilruntime.HandleError(fmt.Errorf("Tombstone contained object that is not an Endpoints resource: %#v", obj))
|
||||
return nil
|
||||
}
|
||||
return endpoints
|
||||
}
|
||||
|
||||
// getEndpointSliceFromDeleteAction parses an EndpointSlice from a delete action.
|
||||
func getEndpointSliceFromDeleteAction(obj interface{}) *discovery.EndpointSlice {
|
||||
if endpointSlice, ok := obj.(*discovery.EndpointSlice); ok {
|
||||
return endpointSlice
|
||||
}
|
||||
// If we reached here it means the EndpointSlice was deleted but its final
|
||||
// state is unrecorded.
|
||||
tombstone, ok := obj.(cache.DeletedFinalStateUnknown)
|
||||
if !ok {
|
||||
utilruntime.HandleError(fmt.Errorf("Couldn't get object from tombstone %#v", obj))
|
||||
return nil
|
||||
}
|
||||
endpointSlice, ok := tombstone.Obj.(*discovery.EndpointSlice)
|
||||
if !ok {
|
||||
utilruntime.HandleError(fmt.Errorf("Tombstone contained object that is not an EndpointSlice resource: %#v", obj))
|
||||
return nil
|
||||
}
|
||||
return endpointSlice
|
||||
}
|
||||
|
||||
// endpointsControllerKey returns a controller key for an Endpoints resource but
|
||||
// derived from an EndpointSlice.
|
||||
func endpointsControllerKey(endpointSlice *discovery.EndpointSlice) (string, error) {
|
||||
if endpointSlice == nil {
|
||||
return "", fmt.Errorf("nil EndpointSlice passed to serviceControllerKey()")
|
||||
}
|
||||
serviceName, ok := endpointSlice.Labels[discovery.LabelServiceName]
|
||||
if !ok || serviceName == "" {
|
||||
return "", fmt.Errorf("EndpointSlice missing %s label", discovery.LabelServiceName)
|
||||
}
|
||||
return fmt.Sprintf("%s/%s", endpointSlice.Namespace, serviceName), nil
|
||||
}
|
||||
|
||||
// skipMirror return true if the the LabelSkipMirror label has been set to
|
||||
// "true".
|
||||
func skipMirror(labels map[string]string) bool {
|
||||
skipMirror, _ := labels[discovery.LabelSkipMirror]
|
||||
return skipMirror == "true"
|
||||
}
|
||||
|
||||
// hasLeaderElection returns true if the LeaderElectionRecordAnnotationKey is
|
||||
// set as an annotation.
|
||||
func hasLeaderElection(annotations map[string]string) bool {
|
||||
_, ok := annotations[resourcelock.LeaderElectionRecordAnnotationKey]
|
||||
return ok
|
||||
}
|
93
pkg/controller/endpointslicemirroring/utils_test.go
Normal file
93
pkg/controller/endpointslicemirroring/utils_test.go
Normal file
@ -0,0 +1,93 @@
|
||||
/*
|
||||
Copyright 2020 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 endpointslicemirroring
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
discovery "k8s.io/api/discovery/v1beta1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/util/rand"
|
||||
"k8s.io/client-go/kubernetes/fake"
|
||||
k8stesting "k8s.io/client-go/testing"
|
||||
)
|
||||
|
||||
func TestNewEndpointSlice(t *testing.T) {
|
||||
portName := "foo"
|
||||
protocol := v1.ProtocolTCP
|
||||
|
||||
ports := []discovery.EndpointPort{{Name: &portName, Protocol: &protocol}}
|
||||
addrType := discovery.AddressTypeIPv4
|
||||
|
||||
endpoints := v1.Endpoints{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "test"},
|
||||
Subsets: []v1.EndpointSubset{{
|
||||
Ports: []v1.EndpointPort{{Port: 80}},
|
||||
}},
|
||||
}
|
||||
|
||||
gvk := schema.GroupVersionKind{Version: "v1", Kind: "Endpoints"}
|
||||
ownerRef := metav1.NewControllerRef(&endpoints, gvk)
|
||||
|
||||
expectedSlice := discovery.EndpointSlice{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Labels: map[string]string{
|
||||
discovery.LabelServiceName: endpoints.Name,
|
||||
discovery.LabelManagedBy: controllerName,
|
||||
},
|
||||
GenerateName: fmt.Sprintf("%s-", endpoints.Name),
|
||||
OwnerReferences: []metav1.OwnerReference{*ownerRef},
|
||||
Namespace: endpoints.Namespace,
|
||||
},
|
||||
Ports: ports,
|
||||
AddressType: addrType,
|
||||
Endpoints: []discovery.Endpoint{},
|
||||
}
|
||||
generatedSlice := newEndpointSlice(&endpoints, ports, addrType, "")
|
||||
|
||||
assert.EqualValues(t, expectedSlice, *generatedSlice)
|
||||
}
|
||||
|
||||
// Test helpers
|
||||
|
||||
func newClientset() *fake.Clientset {
|
||||
client := fake.NewSimpleClientset()
|
||||
|
||||
client.PrependReactor("create", "endpointslices", k8stesting.ReactionFunc(func(action k8stesting.Action) (bool, runtime.Object, error) {
|
||||
endpointSlice := action.(k8stesting.CreateAction).GetObject().(*discovery.EndpointSlice)
|
||||
|
||||
if endpointSlice.ObjectMeta.GenerateName != "" {
|
||||
endpointSlice.ObjectMeta.Name = fmt.Sprintf("%s-%s", endpointSlice.ObjectMeta.GenerateName, rand.String(8))
|
||||
endpointSlice.ObjectMeta.GenerateName = ""
|
||||
}
|
||||
endpointSlice.ObjectMeta.ResourceVersion = "100"
|
||||
|
||||
return false, endpointSlice, nil
|
||||
}))
|
||||
client.PrependReactor("update", "endpointslices", k8stesting.ReactionFunc(func(action k8stesting.Action) (bool, runtime.Object, error) {
|
||||
endpointSlice := action.(k8stesting.CreateAction).GetObject().(*discovery.EndpointSlice)
|
||||
endpointSlice.ObjectMeta.ResourceVersion = "200"
|
||||
return false, endpointSlice, nil
|
||||
}))
|
||||
|
||||
return client
|
||||
}
|
Loading…
Reference in New Issue
Block a user