mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-09-20 17:38:50 +00:00
Adding EndpointSlice controller
This commit is contained in:
49
pkg/controller/util/endpoint/BUILD
Normal file
49
pkg/controller/util/endpoint/BUILD
Normal 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"],
|
||||
)
|
174
pkg/controller/util/endpoint/controller_utils.go
Normal file
174
pkg/controller/util/endpoint/controller_utils.go
Normal 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
|
||||
}
|
226
pkg/controller/util/endpoint/controller_utils_test.go
Normal file
226
pkg/controller/util/endpoint/controller_utils_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
161
pkg/controller/util/endpoint/trigger_time_tracker.go
Normal file
161
pkg/controller/util/endpoint/trigger_time_tracker.go
Normal 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
|
||||
}
|
204
pkg/controller/util/endpoint/trigger_time_tracker_test.go
Normal file
204
pkg/controller/util/endpoint/trigger_time_tracker_test.go
Normal 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),
|
||||
},
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user