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

@@ -0,0 +1,49 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
go_library(
name = "go_default_library",
srcs = [
"controller_utils.go",
"trigger_time_tracker.go",
],
importpath = "k8s.io/kubernetes/pkg/controller/util/endpoint",
visibility = ["//visibility:public"],
deps = [
"//pkg/api/v1/pod:go_default_library",
"//pkg/controller:go_default_library",
"//staging/src/k8s.io/api/core/v1:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/util/runtime:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/util/sets:go_default_library",
"//staging/src/k8s.io/client-go/listers/core/v1:go_default_library",
"//staging/src/k8s.io/client-go/tools/cache:go_default_library",
],
)
go_test(
name = "go_default_test",
srcs = [
"controller_utils_test.go",
"trigger_time_tracker_test.go",
],
embed = [":go_default_library"],
deps = [
"//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/util/intstr:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/util/sets: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"],
)

View File

@@ -0,0 +1,174 @@
/*
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 (
"fmt"
"reflect"
v1 "k8s.io/api/core/v1"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apimachinery/pkg/util/sets"
v1listers "k8s.io/client-go/listers/core/v1"
"k8s.io/client-go/tools/cache"
podutil "k8s.io/kubernetes/pkg/api/v1/pod"
"k8s.io/kubernetes/pkg/controller"
)
// EndpointsMatch is a type of function that returns true if pod endpoints match.
type EndpointsMatch func(*v1.Pod, *v1.Pod) bool
// ShouldPodBeInEndpoints returns true if a specified pod should be in an
// endpoints object.
func ShouldPodBeInEndpoints(pod *v1.Pod) bool {
if len(pod.Status.PodIP) == 0 && len(pod.Status.PodIPs) == 0 {
return false
}
if pod.Spec.RestartPolicy == v1.RestartPolicyNever {
return pod.Status.Phase != v1.PodFailed && pod.Status.Phase != v1.PodSucceeded
}
if pod.Spec.RestartPolicy == v1.RestartPolicyOnFailure {
return pod.Status.Phase != v1.PodSucceeded
}
return true
}
// PodChanged returns two boolean values, the first returns true if the pod.
// has changed, the second value returns true if the pod labels have changed.
func PodChanged(oldPod, newPod *v1.Pod, endpointChanged EndpointsMatch) (bool, bool) {
// 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 the pod's deletion timestamp is set, remove endpoint from ready address.
if newPod.DeletionTimestamp != oldPod.DeletionTimestamp {
return true, labelsChanged
}
// 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, labelsChanged
}
// Convert the pod to an Endpoint, clear inert fields,
// and see if they are the same.
// TODO: Add a watcher for node changes separate from this
// We don't want to trigger multiple syncs at a pod level when a node changes
return endpointChanged(newPod, oldPod), labelsChanged
}
// GetPodServiceMemberships returns a set of Service keys for Services that have
// a selector matching the given pod.
func GetPodServiceMemberships(serviceLister v1listers.ServiceLister, pod *v1.Pod) (sets.String, error) {
set := sets.String{}
services, err := serviceLister.GetPodServices(pod)
if err != nil {
// don't log this error because this function makes pointless
// errors when no services match
return set, nil
}
for i := range services {
key, err := controller.KeyFunc(services[i])
if err != nil {
return nil, err
}
set.Insert(key)
}
return set, nil
}
// GetServicesToUpdateOnPodChange returns a set of Service keys for Services
// that have potentially been affected by a change to this pod.
func GetServicesToUpdateOnPodChange(serviceLister v1listers.ServiceLister, old, cur interface{}, endpointChanged EndpointsMatch) sets.String {
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 sets.String{}
}
podChanged, labelsChanged := PodChanged(oldPod, newPod, endpointChanged)
// If both the pod and labels are unchanged, no update is needed
if !podChanged && !labelsChanged {
return sets.String{}
}
services, err := GetPodServiceMemberships(serviceLister, newPod)
if err != nil {
utilruntime.HandleError(fmt.Errorf("Unable to get pod %s/%s's service memberships: %v", newPod.Namespace, newPod.Name, err))
return sets.String{}
}
if labelsChanged {
oldServices, err := GetPodServiceMemberships(serviceLister, oldPod)
if err != nil {
utilruntime.HandleError(fmt.Errorf("Unable to get pod %s/%s's service memberships: %v", newPod.Namespace, newPod.Name, err))
}
services = determineNeededServiceUpdates(oldServices, services, podChanged)
}
return services
}
// GetPodFromDeleteAction returns a pointer to a pod if one can be derived from
// obj (could be a *v1.Pod, or a DeletionFinalStateUnknown marker item).
func GetPodFromDeleteAction(obj interface{}) *v1.Pod {
if pod, ok := obj.(*v1.Pod); ok {
// Enqueue all the services that the pod used to be a member of.
// This is the same thing we do when we add a pod.
return 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 nil
}
pod, ok := tombstone.Obj.(*v1.Pod)
if !ok {
utilruntime.HandleError(fmt.Errorf("Tombstone contained object that is not a Pod: %#v", obj))
return nil
}
return pod
}
func hostNameAndDomainAreEqual(pod1, pod2 *v1.Pod) bool {
return pod1.Spec.Hostname == pod2.Spec.Hostname &&
pod1.Spec.Subdomain == pod2.Spec.Subdomain
}
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 (the disjuntive union) need to be updated
services = services.Difference(oldServices).Union(oldServices.Difference(services))
}
return services
}

View File

@@ -0,0 +1,226 @@
/*
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 (
"testing"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/util/sets"
)
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())
}
}
}
// There are 3*5 possibilities(3 types of RestartPolicy by 5 types of PodPhase).
// Not listing them all here. Just listing all of the 3 false cases and 3 of the
// 12 true cases.
func TestShouldPodBeInEndpoints(t *testing.T) {
testCases := []struct {
name string
pod *v1.Pod
expected bool
}{
// Pod should not be in endpoints:
{
name: "Failed pod with Never RestartPolicy",
pod: &v1.Pod{
Spec: v1.PodSpec{
RestartPolicy: v1.RestartPolicyNever,
},
Status: v1.PodStatus{
Phase: v1.PodFailed,
PodIP: "1.2.3.4",
},
},
expected: false,
},
{
name: "Succeeded pod with Never RestartPolicy",
pod: &v1.Pod{
Spec: v1.PodSpec{
RestartPolicy: v1.RestartPolicyNever,
},
Status: v1.PodStatus{
Phase: v1.PodSucceeded,
PodIP: "1.2.3.4",
},
},
expected: false,
},
{
name: "Succeeded pod with OnFailure RestartPolicy",
pod: &v1.Pod{
Spec: v1.PodSpec{
RestartPolicy: v1.RestartPolicyOnFailure,
},
Status: v1.PodStatus{
Phase: v1.PodSucceeded,
PodIP: "1.2.3.4",
},
},
expected: false,
},
{
name: "Empty Pod IPs, Running pod with OnFailure RestartPolicy",
pod: &v1.Pod{
Spec: v1.PodSpec{
RestartPolicy: v1.RestartPolicyNever,
},
Status: v1.PodStatus{
Phase: v1.PodRunning,
PodIP: "",
PodIPs: []v1.PodIP{},
},
},
expected: false,
},
// Pod should be in endpoints:
{
name: "Failed pod with Always RestartPolicy",
pod: &v1.Pod{
Spec: v1.PodSpec{
RestartPolicy: v1.RestartPolicyAlways,
},
Status: v1.PodStatus{
Phase: v1.PodFailed,
PodIP: "1.2.3.4",
},
},
expected: true,
},
{
name: "Pending pod with Never RestartPolicy",
pod: &v1.Pod{
Spec: v1.PodSpec{
RestartPolicy: v1.RestartPolicyNever,
},
Status: v1.PodStatus{
Phase: v1.PodPending,
PodIP: "1.2.3.4",
},
},
expected: true,
},
{
name: "Unknown pod with OnFailure RestartPolicy",
pod: &v1.Pod{
Spec: v1.PodSpec{
RestartPolicy: v1.RestartPolicyOnFailure,
},
Status: v1.PodStatus{
Phase: v1.PodUnknown,
PodIP: "1.2.3.4",
},
},
expected: true,
},
{
name: "Running pod with Never RestartPolicy",
pod: &v1.Pod{
Spec: v1.PodSpec{
RestartPolicy: v1.RestartPolicyNever,
},
Status: v1.PodStatus{
Phase: v1.PodRunning,
PodIP: "1.2.3.4",
},
},
expected: true,
},
{
name: "Multiple Pod IPs, Running pod with OnFailure RestartPolicy",
pod: &v1.Pod{
Spec: v1.PodSpec{
RestartPolicy: v1.RestartPolicyNever,
},
Status: v1.PodStatus{
Phase: v1.PodRunning,
PodIPs: []v1.PodIP{{IP: "1.2.3.4"}, {IP: "1234::5678:0000:0000:9abc:def0"}},
},
},
expected: true,
},
}
for _, test := range testCases {
result := ShouldPodBeInEndpoints(test.pod)
if result != test.expected {
t.Errorf("%s: expected : %t, got: %t", test.name, test.expected, result)
}
}
}

View File

@@ -0,0 +1,161 @@
/*
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"
v1 "k8s.io/api/core/v1"
podutil "k8s.io/kubernetes/pkg/api/v1/pod"
)
// TriggerTimeTracker is used to compute an EndpointsLastChangeTriggerTime
// annotation. See the documentation for that annotation for more details.
//
// Please note that this util may compute a wrong EndpointsLastChangeTriggerTime
// if the 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 endpoint(Slice) 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.
type TriggerTimeTracker struct {
// ServiceStates is a map, indexed by Service object key, storing the last
// known Service object state observed during the most recent call of the
// ComputeEndpointLastChangeTriggerTime function.
ServiceStates map[ServiceKey]ServiceState
// mutex guarding the serviceStates map.
mutex sync.Mutex
}
// NewTriggerTimeTracker creates a new instance of the TriggerTimeTracker.
func NewTriggerTimeTracker() *TriggerTimeTracker {
return &TriggerTimeTracker{
ServiceStates: make(map[ServiceKey]ServiceState),
}
}
// ServiceKey is a key uniquely identifying a Service.
type ServiceKey struct {
// namespace, name composing a namespaced name - an unique identifier of every Service.
Namespace, Name string
}
// ServiceState represents a state of an Service object that is known to this util.
type ServiceState 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
// ComputeEndpointLastChangeTriggerTime function.
lastPodTriggerTimes map[string]time.Time
}
// ComputeEndpointLastChangeTriggerTime updates the state of the Service/Endpoint
// 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 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 endpoint(slice)
// controller.
func (t *TriggerTimeTracker) ComputeEndpointLastChangeTriggerTime(
namespace string, service *v1.Service, pods []*v1.Pod) time.Time {
key := ServiceKey{Namespace: namespace, Name: service.Name}
// As there won't be any concurrent calls for the same key, we need to guard
// access only to the serviceStates map.
t.mutex.Lock()
state, wasKnown := t.ServiceStates[key]
t.mutex.Unlock()
// Update the state before returning.
defer func() {
t.mutex.Lock()
t.ServiceStates[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
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 Service, use Service creationTimestamp.
return service.CreationTimestamp.Time
}
// Regular update of endpoint objects, return min of changed trigger times.
return minChangedTriggerTime
}
// DeleteService deletes service state stored in this util.
func (t *TriggerTimeTracker) DeleteService(namespace, name string) {
key := ServiceKey{Namespace: namespace, Name: name}
t.mutex.Lock()
defer t.mutex.Unlock()
delete(t.ServiceStates, key)
}
// getPodTriggerTime returns the time of the pod change (trigger) that resulted
// or will result in the endpoint object change.
func getPodTriggerTime(pod *v1.Pod) (triggerTime time.Time) {
if readyCondition := podutil.GetPodReadyCondition(pod.Status); readyCondition != nil {
triggerTime = readyCondition.LastTransitionTime.Time
}
return triggerTime
}
// getServiceTriggerTime returns the time of the service change (trigger) that
// resulted or will result in the endpoint change.
func getServiceTriggerTime(service *v1.Service) (triggerTime time.Time) {
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

@@ -0,0 +1,204 @@
/*
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"
v1 "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)
ttNamespace = "ttNamespace1"
ttServiceName = "my-service"
)
func TestNewServiceNoPods(t *testing.T) {
tester := newTester(t)
service := createService(ttNamespace, ttServiceName, t2)
tester.whenComputeEndpointLastChangeTriggerTime(ttNamespace, service).expect(t2)
}
func TestNewServiceExistingPods(t *testing.T) {
tester := newTester(t)
service := createService(ttNamespace, ttServiceName, t3)
pod1 := createPod(ttNamespace, "pod1", t0)
pod2 := createPod(ttNamespace, "pod2", t1)
pod3 := createPod(ttNamespace, "pod3", t5)
tester.whenComputeEndpointLastChangeTriggerTime(ttNamespace, 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(ttNamespace, ttServiceName, t0)
tester.whenComputeEndpointLastChangeTriggerTime(ttNamespace, service).expect(t0)
pod1 := createPod(ttNamespace, "pod1", t2)
pod2 := createPod(ttNamespace, "pod2", t1)
tester.whenComputeEndpointLastChangeTriggerTime(ttNamespace, service, pod1, pod2).expect(t1)
}
func TestPodsUpdated(t *testing.T) {
tester := newTester(t)
service := createService(ttNamespace, ttServiceName, t0)
pod1 := createPod(ttNamespace, "pod1", t1)
pod2 := createPod(ttNamespace, "pod2", t2)
pod3 := createPod(ttNamespace, "pod3", t3)
tester.whenComputeEndpointLastChangeTriggerTime(ttNamespace, service, pod1, pod2, pod3).expect(t0)
pod1 = createPod(ttNamespace, "pod1", t5)
pod2 = createPod(ttNamespace, "pod2", t4)
// pod3 doesn't change.
tester.whenComputeEndpointLastChangeTriggerTime(ttNamespace, service, pod1, pod2, pod3).expect(t4)
}
func TestPodsUpdatedNoOp(t *testing.T) {
tester := newTester(t)
service := createService(ttNamespace, ttServiceName, t0)
pod1 := createPod(ttNamespace, "pod1", t1)
pod2 := createPod(ttNamespace, "pod2", t2)
pod3 := createPod(ttNamespace, "pod3", t3)
tester.whenComputeEndpointLastChangeTriggerTime(ttNamespace, service, pod1, pod2, pod3).expect(t0)
// Nothing has changed.
tester.whenComputeEndpointLastChangeTriggerTime(ttNamespace, service, pod1, pod2, pod3).expectNil()
}
func TestPodDeletedThenAdded(t *testing.T) {
tester := newTester(t)
service := createService(ttNamespace, ttServiceName, t0)
pod1 := createPod(ttNamespace, "pod1", t1)
pod2 := createPod(ttNamespace, "pod2", t2)
tester.whenComputeEndpointLastChangeTriggerTime(ttNamespace, service, pod1, pod2).expect(t0)
tester.whenComputeEndpointLastChangeTriggerTime(ttNamespace, service, pod1).expectNil()
pod2 = createPod(ttNamespace, "pod2", t4)
tester.whenComputeEndpointLastChangeTriggerTime(ttNamespace, service, pod1, pod2).expect(t4)
}
func TestServiceDeletedThenAdded(t *testing.T) {
tester := newTester(t)
service := createService(ttNamespace, ttServiceName, t0)
pod1 := createPod(ttNamespace, "pod1", t1)
pod2 := createPod(ttNamespace, "pod2", t2)
tester.whenComputeEndpointLastChangeTriggerTime(ttNamespace, service, pod1, pod2).expect(t0)
tester.DeleteService(ttNamespace, ttServiceName)
service = createService(ttNamespace, ttServiceName, t3)
tester.whenComputeEndpointLastChangeTriggerTime(ttNamespace, service, pod1, pod2).expect(t3)
}
func TestServiceUpdatedNoPodChange(t *testing.T) {
tester := newTester(t)
service := createService(ttNamespace, ttServiceName, t0)
pod1 := createPod(ttNamespace, "pod1", t1)
pod2 := createPod(ttNamespace, "pod2", t2)
tester.whenComputeEndpointLastChangeTriggerTime(ttNamespace, 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.whenComputeEndpointLastChangeTriggerTime(ttNamespace, 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) whenComputeEndpointLastChangeTriggerTime(
namespace string, service *v1.Service, pods ...*v1.Pod) subject {
return subject{t.ComputeEndpointLastChangeTriggerTime(namespace, 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, ttServiceName string, readyTime time.Time) *v1.Pod {
return &v1.Pod{
ObjectMeta: metav1.ObjectMeta{Namespace: namespace, Name: ttServiceName},
Status: v1.PodStatus{Conditions: []v1.PodCondition{
{
Type: v1.PodReady,
Status: v1.ConditionTrue,
LastTransitionTime: metav1.NewTime(readyTime),
},
},
},
}
}
func createService(namespace, ttServiceName string, creationTime time.Time) *v1.Service {
return &v1.Service{
ObjectMeta: metav1.ObjectMeta{
Namespace: namespace,
Name: ttServiceName,
CreationTimestamp: metav1.NewTime(creationTime),
},
}
}