Adding EndpointSlice controller

This commit is contained in:
Rob Scott
2019-07-30 15:42:01 -07:00
parent 550fb1bfc3
commit 75f6c24923
52 changed files with 3852 additions and 562 deletions

View File

@@ -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"],
)

View File

@@ -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

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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),
},
}
}