mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-09-21 01:50:55 +00:00
Adding EndpointSlice controller
This commit is contained in:
@@ -1,25 +1,20 @@
|
||||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
load(
|
||||
"@io_bazel_rules_go//go:def.bzl",
|
||||
"go_library",
|
||||
"go_test",
|
||||
)
|
||||
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
|
||||
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = [
|
||||
"doc.go",
|
||||
"endpoints_controller.go",
|
||||
"trigger_time_tracker.go",
|
||||
],
|
||||
importpath = "k8s.io/kubernetes/pkg/controller/endpoint",
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
"//pkg/api/v1/endpoints:go_default_library",
|
||||
"//pkg/api/v1/pod:go_default_library",
|
||||
"//pkg/apis/core:go_default_library",
|
||||
"//pkg/apis/core/v1/helper:go_default_library",
|
||||
"//pkg/controller:go_default_library",
|
||||
"//pkg/controller/util/endpoint:go_default_library",
|
||||
"//pkg/features:go_default_library",
|
||||
"//pkg/util/metrics:go_default_library",
|
||||
"//staging/src/k8s.io/api/core/v1:go_default_library",
|
||||
@@ -47,23 +42,20 @@ go_library(
|
||||
|
||||
go_test(
|
||||
name = "go_default_test",
|
||||
srcs = [
|
||||
"endpoints_controller_test.go",
|
||||
"trigger_time_tracker_test.go",
|
||||
],
|
||||
srcs = ["endpoints_controller_test.go"],
|
||||
embed = [":go_default_library"],
|
||||
deps = [
|
||||
"//pkg/api/testapi:go_default_library",
|
||||
"//pkg/api/v1/endpoints:go_default_library",
|
||||
"//pkg/apis/core:go_default_library",
|
||||
"//pkg/controller:go_default_library",
|
||||
"//pkg/controller/util/endpoint:go_default_library",
|
||||
"//pkg/features:go_default_library",
|
||||
"//staging/src/k8s.io/api/core/v1: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/intstr:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/util/sets:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/util/wait:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/util/feature:go_default_library",
|
||||
"//staging/src/k8s.io/client-go/informers:go_default_library",
|
||||
@@ -89,4 +81,5 @@ filegroup(
|
||||
"//pkg/controller/endpoint/config:all-srcs",
|
||||
],
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
|
@@ -45,6 +45,7 @@ import (
|
||||
api "k8s.io/kubernetes/pkg/apis/core"
|
||||
helper "k8s.io/kubernetes/pkg/apis/core/v1/helper"
|
||||
"k8s.io/kubernetes/pkg/controller"
|
||||
endpointutil "k8s.io/kubernetes/pkg/controller/util/endpoint"
|
||||
"k8s.io/kubernetes/pkg/util/metrics"
|
||||
|
||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||
@@ -113,7 +114,7 @@ func NewEndpointController(podInformer coreinformers.PodInformer, serviceInforme
|
||||
e.endpointsLister = endpointsInformer.Lister()
|
||||
e.endpointsSynced = endpointsInformer.Informer().HasSynced
|
||||
|
||||
e.triggerTimeTracker = NewTriggerTimeTracker()
|
||||
e.triggerTimeTracker = endpointutil.NewTriggerTimeTracker()
|
||||
e.eventBroadcaster = broadcaster
|
||||
e.eventRecorder = recorder
|
||||
|
||||
@@ -161,7 +162,7 @@ type EndpointController struct {
|
||||
|
||||
// triggerTimeTracker is an util used to compute and export the EndpointsLastChangeTriggerTime
|
||||
// annotation.
|
||||
triggerTimeTracker *TriggerTimeTracker
|
||||
triggerTimeTracker *endpointutil.TriggerTimeTracker
|
||||
|
||||
endpointUpdatesBatchPeriod time.Duration
|
||||
}
|
||||
@@ -267,124 +268,33 @@ func podToEndpointAddress(pod *v1.Pod) *v1.EndpointAddress {
|
||||
}}
|
||||
}
|
||||
|
||||
func podChanged(oldPod, newPod *v1.Pod) bool {
|
||||
// If the pod's deletion timestamp is set, remove endpoint from ready address.
|
||||
if newPod.DeletionTimestamp != oldPod.DeletionTimestamp {
|
||||
return true
|
||||
}
|
||||
// If the pod's readiness has changed, the associated endpoint address
|
||||
// will move from the unready endpoints set to the ready endpoints.
|
||||
// So for the purposes of an endpoint, a readiness change on a pod
|
||||
// means we have a changed pod.
|
||||
if podutil.IsPodReady(oldPod) != podutil.IsPodReady(newPod) {
|
||||
return true
|
||||
}
|
||||
// Convert the pod to an EndpointAddress, clear inert fields,
|
||||
// and see if they are the same. Even in a dual stack (multi pod IP) a pod
|
||||
// will never change just one of its IPs, it will always change all. the below
|
||||
// comparison to check if a pod has changed will still work
|
||||
newEndpointAddress := podToEndpointAddress(newPod)
|
||||
oldEndpointAddress := podToEndpointAddress(oldPod)
|
||||
// Ignore the ResourceVersion because it changes
|
||||
// with every pod update. This allows the comparison to
|
||||
// show equality if all other relevant fields match.
|
||||
newEndpointAddress.TargetRef.ResourceVersion = ""
|
||||
oldEndpointAddress.TargetRef.ResourceVersion = ""
|
||||
if reflect.DeepEqual(newEndpointAddress, oldEndpointAddress) {
|
||||
// The pod has not changed in any way that impacts the endpoints
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
func endpointChanged(pod1, pod2 *v1.Pod) bool {
|
||||
endpointAddress1 := podToEndpointAddress(pod1)
|
||||
endpointAddress2 := podToEndpointAddress(pod2)
|
||||
|
||||
func determineNeededServiceUpdates(oldServices, services sets.String, podChanged bool) sets.String {
|
||||
if podChanged {
|
||||
// if the labels and pod changed, all services need to be updated
|
||||
services = services.Union(oldServices)
|
||||
} else {
|
||||
// if only the labels changed, services not common to
|
||||
// both the new and old service set (i.e the disjunctive union)
|
||||
// need to be updated
|
||||
services = services.Difference(oldServices).Union(oldServices.Difference(services))
|
||||
}
|
||||
return services
|
||||
endpointAddress1.TargetRef.ResourceVersion = ""
|
||||
endpointAddress2.TargetRef.ResourceVersion = ""
|
||||
|
||||
return !reflect.DeepEqual(endpointAddress1, endpointAddress2)
|
||||
}
|
||||
|
||||
// When a pod is updated, figure out what services it used to be a member of
|
||||
// and what services it will be a member of, and enqueue the union of these.
|
||||
// old and cur must be *v1.Pod types.
|
||||
func (e *EndpointController) updatePod(old, cur interface{}) {
|
||||
newPod := cur.(*v1.Pod)
|
||||
oldPod := old.(*v1.Pod)
|
||||
if newPod.ResourceVersion == oldPod.ResourceVersion {
|
||||
// Periodic resync will send update events for all known pods.
|
||||
// Two different versions of the same pod will always have different RVs.
|
||||
return
|
||||
}
|
||||
|
||||
podChangedFlag := podChanged(oldPod, newPod)
|
||||
|
||||
// Check if the pod labels have changed, indicating a possible
|
||||
// change in the service membership
|
||||
labelsChanged := false
|
||||
if !reflect.DeepEqual(newPod.Labels, oldPod.Labels) ||
|
||||
!hostNameAndDomainAreEqual(newPod, oldPod) {
|
||||
labelsChanged = true
|
||||
}
|
||||
|
||||
// If both the pod and labels are unchanged, no update is needed
|
||||
if !podChangedFlag && !labelsChanged {
|
||||
return
|
||||
}
|
||||
|
||||
services, err := e.getPodServiceMemberships(newPod)
|
||||
if err != nil {
|
||||
utilruntime.HandleError(fmt.Errorf("Unable to get pod %v/%v's service memberships: %v", newPod.Namespace, newPod.Name, err))
|
||||
return
|
||||
}
|
||||
|
||||
if labelsChanged {
|
||||
oldServices, err := e.getPodServiceMemberships(oldPod)
|
||||
if err != nil {
|
||||
utilruntime.HandleError(fmt.Errorf("Unable to get pod %v/%v's service memberships: %v", oldPod.Namespace, oldPod.Name, err))
|
||||
return
|
||||
}
|
||||
services = determineNeededServiceUpdates(oldServices, services, podChangedFlag)
|
||||
}
|
||||
|
||||
services := endpointutil.GetServicesToUpdateOnPodChange(e.serviceLister, old, cur, endpointChanged)
|
||||
for key := range services {
|
||||
e.queue.AddAfter(key, e.endpointUpdatesBatchPeriod)
|
||||
}
|
||||
}
|
||||
|
||||
func hostNameAndDomainAreEqual(pod1, pod2 *v1.Pod) bool {
|
||||
return pod1.Spec.Hostname == pod2.Spec.Hostname &&
|
||||
pod1.Spec.Subdomain == pod2.Spec.Subdomain
|
||||
}
|
||||
|
||||
// When a pod is deleted, enqueue the services the pod used to be a member of.
|
||||
// obj could be an *v1.Pod, or a DeletionFinalStateUnknown marker item.
|
||||
func (e *EndpointController) deletePod(obj interface{}) {
|
||||
if _, ok := obj.(*v1.Pod); ok {
|
||||
// Enqueue all the services that the pod used to be a member
|
||||
// of. This happens to be exactly the same thing we do when a
|
||||
// pod is added.
|
||||
e.addPod(obj)
|
||||
return
|
||||
pod := endpointutil.GetPodFromDeleteAction(obj)
|
||||
if pod != nil {
|
||||
e.addPod(pod)
|
||||
}
|
||||
// If we reached here it means the pod 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
|
||||
}
|
||||
pod, ok := tombstone.Obj.(*v1.Pod)
|
||||
if !ok {
|
||||
utilruntime.HandleError(fmt.Errorf("Tombstone contained object that is not a Pod: %#v", obj))
|
||||
return
|
||||
}
|
||||
klog.V(4).Infof("Enqueuing services of deleted pod %s/%s having final state unrecorded", pod.Namespace, pod.Name)
|
||||
e.addPod(pod)
|
||||
}
|
||||
|
||||
// obj could be an *v1.Service, or a DeletionFinalStateUnknown marker item.
|
||||
@@ -462,7 +372,7 @@ func (e *EndpointController) syncService(key string) error {
|
||||
if err != nil && !errors.IsNotFound(err) {
|
||||
return err
|
||||
}
|
||||
e.triggerTimeTracker.DeleteEndpoints(namespace, name)
|
||||
e.triggerTimeTracker.DeleteService(namespace, name)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -491,11 +401,11 @@ func (e *EndpointController) syncService(key string) error {
|
||||
}
|
||||
}
|
||||
|
||||
// We call ComputeEndpointsLastChangeTriggerTime here to make sure that the state of the trigger
|
||||
// time tracker gets updated even if the sync turns out to be no-op and we don't update the
|
||||
// endpoints object.
|
||||
// We call ComputeEndpointLastChangeTriggerTime here to make sure that the
|
||||
// state of the trigger time tracker gets updated even if the sync turns out
|
||||
// to be no-op and we don't update the endpoints object.
|
||||
endpointsLastChangeTriggerTime := e.triggerTimeTracker.
|
||||
ComputeEndpointsLastChangeTriggerTime(namespace, name, service, pods)
|
||||
ComputeEndpointLastChangeTriggerTime(namespace, service, pods)
|
||||
|
||||
subsets := []v1.EndpointSubset{}
|
||||
var totalReadyEps int
|
||||
|
@@ -29,7 +29,6 @@ import (
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/util/intstr"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||
"k8s.io/client-go/informers"
|
||||
@@ -42,6 +41,7 @@ import (
|
||||
endptspkg "k8s.io/kubernetes/pkg/api/v1/endpoints"
|
||||
api "k8s.io/kubernetes/pkg/apis/core"
|
||||
"k8s.io/kubernetes/pkg/controller"
|
||||
endpointutil "k8s.io/kubernetes/pkg/controller/util/endpoint"
|
||||
"k8s.io/kubernetes/pkg/features"
|
||||
)
|
||||
|
||||
@@ -1272,24 +1272,24 @@ func TestPodChanged(t *testing.T) {
|
||||
oldPod := pods[0].(*v1.Pod)
|
||||
newPod := oldPod.DeepCopy()
|
||||
|
||||
if podChanged(oldPod, newPod) {
|
||||
if podChangedHelper(oldPod, newPod, endpointChanged) {
|
||||
t.Errorf("Expected pod to be unchanged for copied pod")
|
||||
}
|
||||
|
||||
newPod.Spec.NodeName = "changed"
|
||||
if !podChanged(oldPod, newPod) {
|
||||
if !podChangedHelper(oldPod, newPod, endpointChanged) {
|
||||
t.Errorf("Expected pod to be changed for pod with NodeName changed")
|
||||
}
|
||||
newPod.Spec.NodeName = oldPod.Spec.NodeName
|
||||
|
||||
newPod.ObjectMeta.ResourceVersion = "changed"
|
||||
if podChanged(oldPod, newPod) {
|
||||
if podChangedHelper(oldPod, newPod, endpointChanged) {
|
||||
t.Errorf("Expected pod to be unchanged for pod with only ResourceVersion changed")
|
||||
}
|
||||
newPod.ObjectMeta.ResourceVersion = oldPod.ObjectMeta.ResourceVersion
|
||||
|
||||
newPod.Status.PodIP = "1.2.3.1"
|
||||
if !podChanged(oldPod, newPod) {
|
||||
if !podChangedHelper(oldPod, newPod, endpointChanged) {
|
||||
t.Errorf("Expected pod to be changed with pod IP address change")
|
||||
}
|
||||
newPod.Status.PodIP = oldPod.Status.PodIP
|
||||
@@ -1306,7 +1306,7 @@ func TestPodChanged(t *testing.T) {
|
||||
IP: "2000::1",
|
||||
},
|
||||
}
|
||||
if !podChanged(oldPod, newPod) {
|
||||
if !podChangedHelper(oldPod, newPod, endpointChanged) {
|
||||
t.Errorf("Expected pod to be changed with adding secondary IP")
|
||||
}
|
||||
// reset
|
||||
@@ -1369,90 +1369,26 @@ func TestPodChanged(t *testing.T) {
|
||||
/* end dual stack testing */
|
||||
|
||||
newPod.ObjectMeta.Name = "wrong-name"
|
||||
if !podChanged(oldPod, newPod) {
|
||||
if !podChangedHelper(oldPod, newPod, endpointChanged) {
|
||||
t.Errorf("Expected pod to be changed with pod name change")
|
||||
}
|
||||
newPod.ObjectMeta.Name = oldPod.ObjectMeta.Name
|
||||
|
||||
saveConditions := oldPod.Status.Conditions
|
||||
oldPod.Status.Conditions = nil
|
||||
if !podChanged(oldPod, newPod) {
|
||||
if !podChangedHelper(oldPod, newPod, endpointChanged) {
|
||||
t.Errorf("Expected pod to be changed with pod readiness change")
|
||||
}
|
||||
oldPod.Status.Conditions = saveConditions
|
||||
|
||||
now := metav1.NewTime(time.Now().UTC())
|
||||
newPod.ObjectMeta.DeletionTimestamp = &now
|
||||
if !podChanged(oldPod, newPod) {
|
||||
if !podChangedHelper(oldPod, newPod, endpointChanged) {
|
||||
t.Errorf("Expected pod to be changed with DeletionTimestamp change")
|
||||
}
|
||||
newPod.ObjectMeta.DeletionTimestamp = oldPod.ObjectMeta.DeletionTimestamp.DeepCopy()
|
||||
}
|
||||
|
||||
func TestDetermineNeededServiceUpdates(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
a sets.String
|
||||
b sets.String
|
||||
union sets.String
|
||||
xor sets.String
|
||||
}{
|
||||
{
|
||||
name: "no services changed",
|
||||
a: sets.NewString("a", "b", "c"),
|
||||
b: sets.NewString("a", "b", "c"),
|
||||
xor: sets.NewString(),
|
||||
union: sets.NewString("a", "b", "c"),
|
||||
},
|
||||
{
|
||||
name: "all old services removed, new services added",
|
||||
a: sets.NewString("a", "b", "c"),
|
||||
b: sets.NewString("d", "e", "f"),
|
||||
xor: sets.NewString("a", "b", "c", "d", "e", "f"),
|
||||
union: sets.NewString("a", "b", "c", "d", "e", "f"),
|
||||
},
|
||||
{
|
||||
name: "all old services removed, no new services added",
|
||||
a: sets.NewString("a", "b", "c"),
|
||||
b: sets.NewString(),
|
||||
xor: sets.NewString("a", "b", "c"),
|
||||
union: sets.NewString("a", "b", "c"),
|
||||
},
|
||||
{
|
||||
name: "no old services, but new services added",
|
||||
a: sets.NewString(),
|
||||
b: sets.NewString("a", "b", "c"),
|
||||
xor: sets.NewString("a", "b", "c"),
|
||||
union: sets.NewString("a", "b", "c"),
|
||||
},
|
||||
{
|
||||
name: "one service removed, one service added, two unchanged",
|
||||
a: sets.NewString("a", "b", "c"),
|
||||
b: sets.NewString("b", "c", "d"),
|
||||
xor: sets.NewString("a", "d"),
|
||||
union: sets.NewString("a", "b", "c", "d"),
|
||||
},
|
||||
{
|
||||
name: "no services",
|
||||
a: sets.NewString(),
|
||||
b: sets.NewString(),
|
||||
xor: sets.NewString(),
|
||||
union: sets.NewString(),
|
||||
},
|
||||
}
|
||||
for _, testCase := range testCases {
|
||||
retval := determineNeededServiceUpdates(testCase.a, testCase.b, false)
|
||||
if !retval.Equal(testCase.xor) {
|
||||
t.Errorf("%s (with podChanged=false): expected: %v got: %v", testCase.name, testCase.xor.List(), retval.List())
|
||||
}
|
||||
|
||||
retval = determineNeededServiceUpdates(testCase.a, testCase.b, true)
|
||||
if !retval.Equal(testCase.union) {
|
||||
t.Errorf("%s (with podChanged=true): expected: %v got: %v", testCase.name, testCase.union.List(), retval.List())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLastTriggerChangeTimeAnnotation(t *testing.T) {
|
||||
ns := "other"
|
||||
testServer, endpointsHandler := makeTestServer(t, ns)
|
||||
@@ -1999,3 +1935,8 @@ func TestSyncEndpointsServiceNotFound(t *testing.T) {
|
||||
endpointsHandler.ValidateRequestCount(t, 1)
|
||||
endpointsHandler.ValidateRequest(t, testapi.Default.ResourcePath("endpoints", ns, "foo"), "DELETE", nil)
|
||||
}
|
||||
|
||||
func podChangedHelper(oldPod, newPod *v1.Pod, endpointChanged endpointutil.EndpointsMatch) bool {
|
||||
podChanged, _ := endpointutil.PodChanged(oldPod, newPod, endpointChanged)
|
||||
return podChanged
|
||||
}
|
||||
|
@@ -1,163 +0,0 @@
|
||||
/*
|
||||
Copyright 2019 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 endpoint
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"k8s.io/api/core/v1"
|
||||
podutil "k8s.io/kubernetes/pkg/api/v1/pod"
|
||||
)
|
||||
|
||||
// TriggerTimeTracker is a util used to compute the EndpointsLastChangeTriggerTime annotation which
|
||||
// is exported in the endpoints controller's sync function.
|
||||
// See the documentation of the EndpointsLastChangeTriggerTime annotation for more details.
|
||||
//
|
||||
// Please note that this util may compute a wrong EndpointsLastChangeTriggerTime if a same object
|
||||
// changes multiple times between two consecutive syncs. We're aware of this limitation but we
|
||||
// decided to accept it, as fixing it would require a major rewrite of the endpoints controller and
|
||||
// Informer framework. Such situations, i.e. frequent updates of the same object in a single sync
|
||||
// period, should be relatively rare and therefore this util should provide a good approximation of
|
||||
// the EndpointsLastChangeTriggerTime.
|
||||
// TODO(mm4tt): Implement a more robust mechanism that is not subject to the above limitations.
|
||||
type TriggerTimeTracker struct {
|
||||
// endpointsStates is a map, indexed by Endpoints object key, storing the last known Endpoints
|
||||
// object state observed during the most recent call of the ComputeEndpointsLastChangeTriggerTime
|
||||
// function.
|
||||
endpointsStates map[endpointsKey]endpointsState
|
||||
|
||||
// mutex guarding the endpointsStates map.
|
||||
mutex sync.Mutex
|
||||
}
|
||||
|
||||
// NewTriggerTimeTracker creates a new instance of the TriggerTimeTracker.
|
||||
func NewTriggerTimeTracker() *TriggerTimeTracker {
|
||||
return &TriggerTimeTracker{
|
||||
endpointsStates: make(map[endpointsKey]endpointsState),
|
||||
}
|
||||
}
|
||||
|
||||
// endpointsKey is a key uniquely identifying an Endpoints object.
|
||||
type endpointsKey struct {
|
||||
// namespace, name composing a namespaced name - an unique identifier of every Endpoints object.
|
||||
namespace, name string
|
||||
}
|
||||
|
||||
// endpointsState represents a state of an Endpoints object that is known to this util.
|
||||
type endpointsState struct {
|
||||
// lastServiceTriggerTime is a service trigger time observed most recently.
|
||||
lastServiceTriggerTime time.Time
|
||||
// lastPodTriggerTimes is a map (Pod name -> time) storing the pod trigger times that were
|
||||
// observed during the most recent call of the ComputeEndpointsLastChangeTriggerTime function.
|
||||
lastPodTriggerTimes map[string]time.Time
|
||||
}
|
||||
|
||||
// ComputeEndpointsLastChangeTriggerTime updates the state of the Endpoints object being synced
|
||||
// and returns the time that should be exported as the EndpointsLastChangeTriggerTime annotation.
|
||||
//
|
||||
// If the method returns a 'zero' time the EndpointsLastChangeTriggerTime annotation shouldn't be
|
||||
// exported.
|
||||
//
|
||||
// Please note that this function may compute a wrong EndpointsLastChangeTriggerTime value if the
|
||||
// same object (pod/service) changes multiple times between two consecutive syncs.
|
||||
//
|
||||
// Important: This method is go-routing safe but only when called for different keys. The method
|
||||
// shouldn't be called concurrently for the same key! This contract is fulfilled in the current
|
||||
// implementation of the endpoints controller.
|
||||
func (t *TriggerTimeTracker) ComputeEndpointsLastChangeTriggerTime(
|
||||
namespace, name string, service *v1.Service, pods []*v1.Pod) time.Time {
|
||||
|
||||
key := endpointsKey{namespace: namespace, name: name}
|
||||
// As there won't be any concurrent calls for the same key, we need to guard access only to the
|
||||
// endpointsStates map.
|
||||
t.mutex.Lock()
|
||||
state, wasKnown := t.endpointsStates[key]
|
||||
t.mutex.Unlock()
|
||||
|
||||
// Update the state before returning.
|
||||
defer func() {
|
||||
t.mutex.Lock()
|
||||
t.endpointsStates[key] = state
|
||||
t.mutex.Unlock()
|
||||
}()
|
||||
|
||||
// minChangedTriggerTime is the min trigger time of all trigger times that have changed since the
|
||||
// last sync.
|
||||
var minChangedTriggerTime time.Time
|
||||
// TODO(mm4tt): If memory allocation / GC performance impact of recreating map in every call
|
||||
// turns out to be too expensive, we should consider rewriting this to reuse the existing map.
|
||||
podTriggerTimes := make(map[string]time.Time)
|
||||
for _, pod := range pods {
|
||||
if podTriggerTime := getPodTriggerTime(pod); !podTriggerTime.IsZero() {
|
||||
podTriggerTimes[pod.Name] = podTriggerTime
|
||||
if podTriggerTime.After(state.lastPodTriggerTimes[pod.Name]) {
|
||||
// Pod trigger time has changed since the last sync, update minChangedTriggerTime.
|
||||
minChangedTriggerTime = min(minChangedTriggerTime, podTriggerTime)
|
||||
}
|
||||
}
|
||||
}
|
||||
serviceTriggerTime := getServiceTriggerTime(service)
|
||||
if serviceTriggerTime.After(state.lastServiceTriggerTime) {
|
||||
// Service trigger time has changed since the last sync, update minChangedTriggerTime.
|
||||
minChangedTriggerTime = min(minChangedTriggerTime, serviceTriggerTime)
|
||||
}
|
||||
|
||||
state.lastPodTriggerTimes = podTriggerTimes
|
||||
state.lastServiceTriggerTime = serviceTriggerTime
|
||||
|
||||
if !wasKnown {
|
||||
// New Endpoints object / new Service, use Service creationTimestamp.
|
||||
return service.CreationTimestamp.Time
|
||||
} else {
|
||||
// Regular update of the Endpoints object, return min of changed trigger times.
|
||||
return minChangedTriggerTime
|
||||
}
|
||||
}
|
||||
|
||||
// DeleteEndpoints deletes endpoints state stored in this util.
|
||||
func (t *TriggerTimeTracker) DeleteEndpoints(namespace, name string) {
|
||||
key := endpointsKey{namespace: namespace, name: name}
|
||||
t.mutex.Lock()
|
||||
defer t.mutex.Unlock()
|
||||
delete(t.endpointsStates, key)
|
||||
}
|
||||
|
||||
// getPodTriggerTime returns the time of the pod change (trigger) that resulted or will result in
|
||||
// the endpoints object change.
|
||||
func getPodTriggerTime(pod *v1.Pod) (triggerTime time.Time) {
|
||||
if readyCondition := podutil.GetPodReadyCondition(pod.Status); readyCondition != nil {
|
||||
triggerTime = readyCondition.LastTransitionTime.Time
|
||||
}
|
||||
// TODO(#81360): Implement missing cases: deletionTime set, pod label change
|
||||
return triggerTime
|
||||
}
|
||||
|
||||
// getServiceTriggerTime returns the time of the service change (trigger) that resulted or will
|
||||
// result in the endpoints object change.
|
||||
func getServiceTriggerTime(service *v1.Service) (triggerTime time.Time) {
|
||||
// TODO(mm4tt): Ideally we should look at service.LastUpdateTime, but such thing doesn't exist.
|
||||
return service.CreationTimestamp.Time
|
||||
}
|
||||
|
||||
// min returns minimum of the currentMin and newValue or newValue if the currentMin is not set.
|
||||
func min(currentMin, newValue time.Time) time.Time {
|
||||
if currentMin.IsZero() || newValue.Before(currentMin) {
|
||||
return newValue
|
||||
}
|
||||
return currentMin
|
||||
}
|
@@ -1,204 +0,0 @@
|
||||
/*
|
||||
Copyright 2019 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 endpoint
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/util/intstr"
|
||||
)
|
||||
|
||||
var (
|
||||
t0 = time.Date(2019, 01, 01, 0, 0, 0, 0, time.UTC)
|
||||
t1 = t0.Add(time.Second)
|
||||
t2 = t1.Add(time.Second)
|
||||
t3 = t2.Add(time.Second)
|
||||
t4 = t3.Add(time.Second)
|
||||
t5 = t4.Add(time.Second)
|
||||
|
||||
ns = "ns1"
|
||||
name = "my-service"
|
||||
)
|
||||
|
||||
func TestNewService_NoPods(t *testing.T) {
|
||||
tester := newTester(t)
|
||||
|
||||
service := createService(ns, name, t2)
|
||||
tester.whenComputeEndpointsLastChangeTriggerTime(ns, name, service).expect(t2)
|
||||
}
|
||||
|
||||
func TestNewService_ExistingPods(t *testing.T) {
|
||||
tester := newTester(t)
|
||||
|
||||
service := createService(ns, name, t3)
|
||||
pod1 := createPod(ns, "pod1", t0)
|
||||
pod2 := createPod(ns, "pod2", t1)
|
||||
pod3 := createPod(ns, "pod3", t5)
|
||||
tester.whenComputeEndpointsLastChangeTriggerTime(ns, name, service, pod1, pod2, pod3).
|
||||
// Pods were created before service, but trigger time is the time when service was created.
|
||||
expect(t3)
|
||||
}
|
||||
|
||||
func TestPodsAdded(t *testing.T) {
|
||||
tester := newTester(t)
|
||||
|
||||
service := createService(ns, name, t0)
|
||||
tester.whenComputeEndpointsLastChangeTriggerTime(ns, name, service).expect(t0)
|
||||
|
||||
pod1 := createPod(ns, "pod1", t2)
|
||||
pod2 := createPod(ns, "pod2", t1)
|
||||
tester.whenComputeEndpointsLastChangeTriggerTime(ns, name, service, pod1, pod2).expect(t1)
|
||||
}
|
||||
|
||||
func TestPodsUpdated(t *testing.T) {
|
||||
tester := newTester(t)
|
||||
|
||||
service := createService(ns, name, t0)
|
||||
pod1 := createPod(ns, "pod1", t1)
|
||||
pod2 := createPod(ns, "pod2", t2)
|
||||
pod3 := createPod(ns, "pod3", t3)
|
||||
tester.whenComputeEndpointsLastChangeTriggerTime(ns, name, service, pod1, pod2, pod3).expect(t0)
|
||||
|
||||
pod1 = createPod(ns, "pod1", t5)
|
||||
pod2 = createPod(ns, "pod2", t4)
|
||||
// pod3 doesn't change.
|
||||
tester.whenComputeEndpointsLastChangeTriggerTime(ns, name, service, pod1, pod2, pod3).expect(t4)
|
||||
}
|
||||
|
||||
func TestPodsUpdated_NoOp(t *testing.T) {
|
||||
tester := newTester(t)
|
||||
|
||||
service := createService(ns, name, t0)
|
||||
pod1 := createPod(ns, "pod1", t1)
|
||||
pod2 := createPod(ns, "pod2", t2)
|
||||
pod3 := createPod(ns, "pod3", t3)
|
||||
tester.whenComputeEndpointsLastChangeTriggerTime(ns, name, service, pod1, pod2, pod3).expect(t0)
|
||||
|
||||
// Nothing has changed.
|
||||
tester.whenComputeEndpointsLastChangeTriggerTime(ns, name, service, pod1, pod2, pod3).expectNil()
|
||||
}
|
||||
|
||||
func TestPodDeletedThenAdded(t *testing.T) {
|
||||
tester := newTester(t)
|
||||
|
||||
service := createService(ns, name, t0)
|
||||
pod1 := createPod(ns, "pod1", t1)
|
||||
pod2 := createPod(ns, "pod2", t2)
|
||||
tester.whenComputeEndpointsLastChangeTriggerTime(ns, name, service, pod1, pod2).expect(t0)
|
||||
|
||||
tester.whenComputeEndpointsLastChangeTriggerTime(ns, name, service, pod1).expectNil()
|
||||
|
||||
pod2 = createPod(ns, "pod2", t4)
|
||||
tester.whenComputeEndpointsLastChangeTriggerTime(ns, name, service, pod1, pod2).expect(t4)
|
||||
}
|
||||
|
||||
func TestServiceDeletedThenAdded(t *testing.T) {
|
||||
tester := newTester(t)
|
||||
|
||||
service := createService(ns, name, t0)
|
||||
pod1 := createPod(ns, "pod1", t1)
|
||||
pod2 := createPod(ns, "pod2", t2)
|
||||
tester.whenComputeEndpointsLastChangeTriggerTime(ns, name, service, pod1, pod2).expect(t0)
|
||||
|
||||
tester.DeleteEndpoints(ns, name)
|
||||
|
||||
service = createService(ns, name, t3)
|
||||
tester.whenComputeEndpointsLastChangeTriggerTime(ns, name, service, pod1, pod2).expect(t3)
|
||||
}
|
||||
|
||||
func TestServiceUpdated_NoPodChange(t *testing.T) {
|
||||
tester := newTester(t)
|
||||
|
||||
service := createService(ns, name, t0)
|
||||
pod1 := createPod(ns, "pod1", t1)
|
||||
pod2 := createPod(ns, "pod2", t2)
|
||||
tester.whenComputeEndpointsLastChangeTriggerTime(ns, name, service, pod1, pod2).expect(t0)
|
||||
|
||||
// service's ports have changed.
|
||||
service.Spec = v1.ServiceSpec{
|
||||
Selector: map[string]string{},
|
||||
Ports: []v1.ServicePort{{Port: 80, TargetPort: intstr.FromInt(8080), Protocol: "TCP"}},
|
||||
}
|
||||
|
||||
// Currently we're not able to calculate trigger time for service updates, hence the returned
|
||||
// value is a nil time.
|
||||
tester.whenComputeEndpointsLastChangeTriggerTime(ns, name, service, pod1, pod2).expectNil()
|
||||
}
|
||||
|
||||
// ------- Test Utils -------
|
||||
|
||||
type tester struct {
|
||||
*TriggerTimeTracker
|
||||
t *testing.T
|
||||
}
|
||||
|
||||
func newTester(t *testing.T) *tester {
|
||||
return &tester{NewTriggerTimeTracker(), t}
|
||||
}
|
||||
|
||||
func (t *tester) whenComputeEndpointsLastChangeTriggerTime(
|
||||
namespace, name string, service *v1.Service, pods ...*v1.Pod) subject {
|
||||
return subject{t.ComputeEndpointsLastChangeTriggerTime(namespace, name, service, pods), t.t}
|
||||
}
|
||||
|
||||
type subject struct {
|
||||
got time.Time
|
||||
t *testing.T
|
||||
}
|
||||
|
||||
func (s subject) expect(expected time.Time) {
|
||||
s.doExpect(expected)
|
||||
}
|
||||
|
||||
func (s subject) expectNil() {
|
||||
s.doExpect(time.Time{})
|
||||
}
|
||||
|
||||
func (s subject) doExpect(expected time.Time) {
|
||||
if s.got != expected {
|
||||
_, fn, line, _ := runtime.Caller(2)
|
||||
s.t.Errorf("Wrong trigger time in %s:%d expected %s, got %s", fn, line, expected, s.got)
|
||||
}
|
||||
}
|
||||
|
||||
func createPod(namespace, name string, readyTime time.Time) *v1.Pod {
|
||||
return &v1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{Namespace: namespace, Name: name},
|
||||
Status: v1.PodStatus{Conditions: []v1.PodCondition{
|
||||
{
|
||||
Type: v1.PodReady,
|
||||
Status: v1.ConditionTrue,
|
||||
LastTransitionTime: metav1.NewTime(readyTime),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func createService(namespace, name string, creationTime time.Time) *v1.Service {
|
||||
return &v1.Service{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: namespace,
|
||||
Name: name,
|
||||
CreationTimestamp: metav1.NewTime(creationTime),
|
||||
},
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user