mirror of
				https://github.com/optim-enterprises-bv/kubernetes.git
				synced 2025-11-04 04:08:16 +00:00 
			
		
		
		
	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:
		
							
								
								
									
										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
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
		Reference in New Issue
	
	Block a user