/* Copyright 2015 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 controller import ( "context" "encoding/json" "fmt" "math" "math/rand" "net/http/httptest" "sort" "sync" "testing" "time" apps "k8s.io/api/apps/v1" v1 "k8s.io/api/core/v1" apiequality "k8s.io/apimachinery/pkg/api/equality" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/util/uuid" utilfeature "k8s.io/apiserver/pkg/util/feature" clientset "k8s.io/client-go/kubernetes" "k8s.io/client-go/kubernetes/fake" clientscheme "k8s.io/client-go/kubernetes/scheme" restclient "k8s.io/client-go/rest" "k8s.io/client-go/tools/cache" "k8s.io/client-go/tools/record" utiltesting "k8s.io/client-go/util/testing" featuregatetesting "k8s.io/component-base/featuregate/testing" "k8s.io/kubernetes/pkg/apis/core" _ "k8s.io/kubernetes/pkg/apis/core/install" "k8s.io/kubernetes/pkg/controller/testutil" "k8s.io/kubernetes/pkg/features" "k8s.io/kubernetes/pkg/securitycontext" "k8s.io/kubernetes/test/utils/ktesting" testingclock "k8s.io/utils/clock/testing" "k8s.io/utils/pointer" "github.com/google/go-cmp/cmp" "github.com/stretchr/testify/assert" ) // NewFakeControllerExpectationsLookup creates a fake store for PodExpectations. func NewFakeControllerExpectationsLookup(ttl time.Duration) (*ControllerExpectations, *testingclock.FakeClock) { fakeTime := time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC) fakeClock := testingclock.NewFakeClock(fakeTime) ttlPolicy := &cache.TTLPolicy{TTL: ttl, Clock: fakeClock} ttlStore := cache.NewFakeExpirationStore( ExpKeyFunc, nil, ttlPolicy, fakeClock) return &ControllerExpectations{ttlStore}, fakeClock } func newReplicationController(replicas int) *v1.ReplicationController { rc := &v1.ReplicationController{ TypeMeta: metav1.TypeMeta{APIVersion: "v1"}, ObjectMeta: metav1.ObjectMeta{ UID: uuid.NewUUID(), Name: "foobar", Namespace: metav1.NamespaceDefault, ResourceVersion: "18", }, Spec: v1.ReplicationControllerSpec{ Replicas: pointer.Int32(int32(replicas)), Selector: map[string]string{"foo": "bar"}, Template: &v1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ Labels: map[string]string{ "name": "foo", "type": "production", }, }, Spec: v1.PodSpec{ Containers: []v1.Container{ { Image: "foo/bar", TerminationMessagePath: v1.TerminationMessagePathDefault, ImagePullPolicy: v1.PullIfNotPresent, SecurityContext: securitycontext.ValidSecurityContextWithContainerDefaults(), }, }, RestartPolicy: v1.RestartPolicyAlways, DNSPolicy: v1.DNSDefault, NodeSelector: map[string]string{ "baz": "blah", }, }, }, }, } return rc } // create count pods with the given phase for the given rc (same selectors and namespace), and add them to the store. func newPodList(store cache.Store, count int, status v1.PodPhase, rc *v1.ReplicationController) *v1.PodList { pods := []v1.Pod{} for i := 0; i < count; i++ { newPod := v1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: fmt.Sprintf("pod%d", i), Labels: rc.Spec.Selector, Namespace: rc.Namespace, }, Status: v1.PodStatus{Phase: status}, } if store != nil { store.Add(&newPod) } pods = append(pods, newPod) } return &v1.PodList{ Items: pods, } } func newReplicaSet(name string, replicas int, rsUuid types.UID) *apps.ReplicaSet { return &apps.ReplicaSet{ TypeMeta: metav1.TypeMeta{APIVersion: "v1"}, ObjectMeta: metav1.ObjectMeta{ UID: rsUuid, Name: name, Namespace: metav1.NamespaceDefault, ResourceVersion: "18", }, Spec: apps.ReplicaSetSpec{ Replicas: pointer.Int32(int32(replicas)), Selector: &metav1.LabelSelector{MatchLabels: map[string]string{"foo": "bar"}}, Template: v1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ Labels: map[string]string{ "name": "foo", "type": "production", }, }, Spec: v1.PodSpec{ Containers: []v1.Container{ { Image: "foo/bar", TerminationMessagePath: v1.TerminationMessagePathDefault, ImagePullPolicy: v1.PullIfNotPresent, SecurityContext: securitycontext.ValidSecurityContextWithContainerDefaults(), }, }, RestartPolicy: v1.RestartPolicyAlways, DNSPolicy: v1.DNSDefault, NodeSelector: map[string]string{ "baz": "blah", }, }, }, }, } } func TestControllerExpectations(t *testing.T) { logger, _ := ktesting.NewTestContext(t) ttl := 30 * time.Second e, fakeClock := NewFakeControllerExpectationsLookup(ttl) // In practice we can't really have add and delete expectations since we only either create or // delete replicas in one rc pass, and the rc goes to sleep soon after until the expectations are // either fulfilled or timeout. adds, dels := 10, 30 rc := newReplicationController(1) // RC fires off adds and deletes at apiserver, then sets expectations rcKey, err := KeyFunc(rc) assert.NoError(t, err, "Couldn't get key for object %#v: %v", rc, err) e.SetExpectations(logger, rcKey, adds, dels) var wg sync.WaitGroup for i := 0; i < adds+1; i++ { wg.Add(1) go func() { // In prod this can happen either because of a failed create by the rc // or after having observed a create via informer e.CreationObserved(logger, rcKey) wg.Done() }() } wg.Wait() // There are still delete expectations assert.False(t, e.SatisfiedExpectations(logger, rcKey), "Rc will sync before expectations are met") for i := 0; i < dels+1; i++ { wg.Add(1) go func() { e.DeletionObserved(logger, rcKey) wg.Done() }() } wg.Wait() tests := []struct { name string expectationsToSet []int expireExpectations bool wantPodExpectations []int64 wantExpectationsSatisfied bool }{ { name: "Expectations have been surpassed", expireExpectations: false, wantPodExpectations: []int64{int64(-1), int64(-1)}, wantExpectationsSatisfied: true, }, { name: "Old expectations are cleared because of ttl", expectationsToSet: []int{1, 2}, expireExpectations: true, wantPodExpectations: []int64{int64(1), int64(2)}, wantExpectationsSatisfied: false, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { if len(test.expectationsToSet) > 0 { e.SetExpectations(logger, rcKey, test.expectationsToSet[0], test.expectationsToSet[1]) } podExp, exists, err := e.GetExpectations(rcKey) assert.NoError(t, err, "Could not get expectations for rc, exists %v and err %v", exists, err) assert.True(t, exists, "Could not get expectations for rc, exists %v and err %v", exists, err) add, del := podExp.GetExpectations() assert.Equal(t, test.wantPodExpectations[0], add, "Unexpected pod expectations %#v", podExp) assert.Equal(t, test.wantPodExpectations[1], del, "Unexpected pod expectations %#v", podExp) assert.Equal(t, test.wantExpectationsSatisfied, e.SatisfiedExpectations(logger, rcKey), "Expectations are met but the rc will not sync") if test.expireExpectations { fakeClock.Step(ttl + 1) assert.True(t, e.SatisfiedExpectations(logger, rcKey), "Expectations should have expired but didn't") } }) } } func TestUIDExpectations(t *testing.T) { logger, _ := ktesting.NewTestContext(t) uidExp := NewUIDTrackingControllerExpectations(NewControllerExpectations()) type test struct { name string numReplicas int } shuffleTests := func(tests []test) { for i := range tests { j := rand.Intn(i + 1) tests[i], tests[j] = tests[j], tests[i] } } getRcDataFrom := func(test test) (string, []string) { rc := newReplicationController(test.numReplicas) rcName := fmt.Sprintf("rc-%v", test.numReplicas) rc.Name = rcName rc.Spec.Selector[rcName] = rcName podList := newPodList(nil, 5, v1.PodRunning, rc) rcKey, err := KeyFunc(rc) if err != nil { t.Fatalf("Couldn't get key for object %#v: %v", rc, err) } rcPodNames := []string{} for i := range podList.Items { p := &podList.Items[i] p.Name = fmt.Sprintf("%v-%v", p.Name, rc.Name) rcPodNames = append(rcPodNames, PodKey(p)) } uidExp.ExpectDeletions(logger, rcKey, rcPodNames) return rcKey, rcPodNames } tests := []test{ {name: "Replication controller with 2 replicas", numReplicas: 2}, {name: "Replication controller with 1 replica", numReplicas: 1}, {name: "Replication controller with no replicas", numReplicas: 0}, {name: "Replication controller with 5 replicas", numReplicas: 5}, } shuffleTests(tests) for _, test := range tests { t.Run(test.name, func(t *testing.T) { rcKey, rcPodNames := getRcDataFrom(test) assert.False(t, uidExp.SatisfiedExpectations(logger, rcKey), "Controller %v satisfied expectations before deletion", rcKey) for _, p := range rcPodNames { uidExp.DeletionObserved(logger, rcKey, p) } assert.True(t, uidExp.SatisfiedExpectations(logger, rcKey), "Controller %v didn't satisfy expectations after deletion", rcKey) uidExp.DeleteExpectations(logger, rcKey) assert.Nil(t, uidExp.GetUIDs(rcKey), "Failed to delete uid expectations for %v", rcKey) }) } } func TestCreatePodsWithGenerateName(t *testing.T) { ns := metav1.NamespaceDefault generateName := "hello-" controllerSpec := newReplicationController(1) controllerRef := metav1.NewControllerRef(controllerSpec, v1.SchemeGroupVersion.WithKind("ReplicationController")) type test struct { name string podCreationFunc func(podControl RealPodControl) error wantPod *v1.Pod } var tests = []test{ { name: "Create pod", podCreationFunc: func(podControl RealPodControl) error { return podControl.CreatePods(context.TODO(), ns, controllerSpec.Spec.Template, controllerSpec, controllerRef) }, wantPod: &v1.Pod{ ObjectMeta: metav1.ObjectMeta{ Labels: controllerSpec.Spec.Template.Labels, GenerateName: fmt.Sprintf("%s-", controllerSpec.Name), }, Spec: controllerSpec.Spec.Template.Spec, }, }, { name: "Create pod with generate name", podCreationFunc: func(podControl RealPodControl) error { // Make sure createReplica sends a POST to the apiserver with a pod from the controllers pod template return podControl.CreatePodsWithGenerateName(context.TODO(), ns, controllerSpec.Spec.Template, controllerSpec, controllerRef, generateName) }, wantPod: &v1.Pod{ ObjectMeta: metav1.ObjectMeta{ Labels: controllerSpec.Spec.Template.Labels, GenerateName: generateName, OwnerReferences: []metav1.OwnerReference{*controllerRef}, }, Spec: controllerSpec.Spec.Template.Spec, }, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { body := runtime.EncodeOrDie(clientscheme.Codecs.LegacyCodec(v1.SchemeGroupVersion), &v1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "empty_pod"}}) fakeHandler := utiltesting.FakeHandler{ StatusCode: 200, ResponseBody: string(body), } testServer := httptest.NewServer(&fakeHandler) defer testServer.Close() clientset := clientset.NewForConfigOrDie(&restclient.Config{Host: testServer.URL, ContentConfig: restclient.ContentConfig{GroupVersion: &schema.GroupVersion{Group: "", Version: "v1"}}}) podControl := RealPodControl{ KubeClient: clientset, Recorder: &record.FakeRecorder{}, } err := test.podCreationFunc(podControl) assert.NoError(t, err, "unexpected error: %v", err) fakeHandler.ValidateRequest(t, "/api/v1/namespaces/default/pods", "POST", nil) var actualPod = &v1.Pod{} err = json.Unmarshal([]byte(fakeHandler.RequestBody), actualPod) assert.NoError(t, err, "unexpected error: %v", err) assert.True(t, apiequality.Semantic.DeepDerivative(test.wantPod, actualPod), "Body: %s", fakeHandler.RequestBody) }) } } func TestDeletePodsAllowsMissing(t *testing.T) { fakeClient := fake.NewSimpleClientset() podControl := RealPodControl{ KubeClient: fakeClient, Recorder: &record.FakeRecorder{}, } controllerSpec := newReplicationController(1) err := podControl.DeletePod(context.TODO(), "namespace-name", "podName", controllerSpec) assert.True(t, apierrors.IsNotFound(err)) } func TestCountTerminatingPods(t *testing.T) { now := metav1.Now() // This rc is not needed by the test, only the newPodList to give the pods labels/a namespace. rc := newReplicationController(0) podList := newPodList(nil, 7, v1.PodRunning, rc) podList.Items[0].Status.Phase = v1.PodSucceeded podList.Items[1].Status.Phase = v1.PodFailed podList.Items[2].Status.Phase = v1.PodPending podList.Items[2].SetDeletionTimestamp(&now) podList.Items[3].Status.Phase = v1.PodRunning podList.Items[3].SetDeletionTimestamp(&now) var podPointers []*v1.Pod for i := range podList.Items { podPointers = append(podPointers, &podList.Items[i]) } terminatingPods := CountTerminatingPods(podPointers) assert.Equal(t, terminatingPods, int32(2)) terminatingList := FilterTerminatingPods(podPointers) assert.Equal(t, len(terminatingList), int(2)) } func TestActivePodFiltering(t *testing.T) { logger, _ := ktesting.NewTestContext(t) type podData struct { podName string podPhase v1.PodPhase } type test struct { name string pods []podData wantPodNames []string } tests := []test{ { name: "Filters active pods", pods: []podData{ {podName: "pod-1", podPhase: v1.PodSucceeded}, {podName: "pod-2", podPhase: v1.PodFailed}, {podName: "pod-3"}, {podName: "pod-4"}, {podName: "pod-5"}, }, wantPodNames: []string{"pod-3", "pod-4", "pod-5"}, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { // This rc is not needed by the test, only the newPodList to give the pods labels/a namespace. rc := newReplicationController(0) podList := newPodList(nil, 5, v1.PodRunning, rc) for idx, testPod := range test.pods { podList.Items[idx].Name = testPod.podName podList.Items[idx].Status.Phase = testPod.podPhase } var podPointers []*v1.Pod for i := range podList.Items { podPointers = append(podPointers, &podList.Items[i]) } got := FilterActivePods(logger, podPointers) gotNames := sets.NewString() for _, pod := range got { gotNames.Insert(pod.Name) } if diff := cmp.Diff(test.wantPodNames, gotNames.List()); diff != "" { t.Errorf("Active pod names (-want,+got):\n%s", diff) } }) } } func TestSortingActivePods(t *testing.T) { now := metav1.Now() then := metav1.Time{Time: now.AddDate(0, -1, 0)} tests := []struct { name string pods []v1.Pod wantOrder []string }{ { name: "Sorts by active pod", pods: []v1.Pod{ { ObjectMeta: metav1.ObjectMeta{Name: "unscheduled"}, Spec: v1.PodSpec{NodeName: ""}, Status: v1.PodStatus{Phase: v1.PodPending}, }, { ObjectMeta: metav1.ObjectMeta{Name: "scheduledButPending"}, Spec: v1.PodSpec{NodeName: "bar"}, Status: v1.PodStatus{Phase: v1.PodPending}, }, { ObjectMeta: metav1.ObjectMeta{Name: "unknownPhase"}, Spec: v1.PodSpec{NodeName: "foo"}, Status: v1.PodStatus{Phase: v1.PodUnknown}, }, { ObjectMeta: metav1.ObjectMeta{Name: "runningButNotReady"}, Spec: v1.PodSpec{NodeName: "foo"}, Status: v1.PodStatus{Phase: v1.PodRunning}, }, { ObjectMeta: metav1.ObjectMeta{Name: "runningNoLastTransitionTime"}, Spec: v1.PodSpec{NodeName: "foo"}, Status: v1.PodStatus{ Phase: v1.PodRunning, Conditions: []v1.PodCondition{{Type: v1.PodReady, Status: v1.ConditionTrue}}, ContainerStatuses: []v1.ContainerStatus{{RestartCount: 3}, {RestartCount: 0}}, }, }, { ObjectMeta: metav1.ObjectMeta{Name: "runningWithLastTransitionTime"}, Spec: v1.PodSpec{NodeName: "foo"}, Status: v1.PodStatus{ Phase: v1.PodRunning, Conditions: []v1.PodCondition{{Type: v1.PodReady, Status: v1.ConditionTrue, LastTransitionTime: now}}, ContainerStatuses: []v1.ContainerStatus{{RestartCount: 3}, {RestartCount: 0}}, }, }, { ObjectMeta: metav1.ObjectMeta{Name: "runningLongerTime"}, Spec: v1.PodSpec{NodeName: "foo"}, Status: v1.PodStatus{ Phase: v1.PodRunning, Conditions: []v1.PodCondition{{Type: v1.PodReady, Status: v1.ConditionTrue, LastTransitionTime: then}}, ContainerStatuses: []v1.ContainerStatus{{RestartCount: 3}, {RestartCount: 0}}, }, }, { ObjectMeta: metav1.ObjectMeta{Name: "lowerContainerRestartCount", CreationTimestamp: now}, Spec: v1.PodSpec{NodeName: "foo"}, Status: v1.PodStatus{ Phase: v1.PodRunning, Conditions: []v1.PodCondition{{Type: v1.PodReady, Status: v1.ConditionTrue, LastTransitionTime: then}}, ContainerStatuses: []v1.ContainerStatus{{RestartCount: 2}, {RestartCount: 1}}, }, }, { ObjectMeta: metav1.ObjectMeta{Name: "oldest", CreationTimestamp: then}, Spec: v1.PodSpec{NodeName: "foo"}, Status: v1.PodStatus{ Phase: v1.PodRunning, Conditions: []v1.PodCondition{{Type: v1.PodReady, Status: v1.ConditionTrue, LastTransitionTime: then}}, ContainerStatuses: []v1.ContainerStatus{{RestartCount: 2}, {RestartCount: 1}}, }, }, }, wantOrder: []string{ "unscheduled", "scheduledButPending", "unknownPhase", "runningButNotReady", "runningNoLastTransitionTime", "runningWithLastTransitionTime", "runningLongerTime", "lowerContainerRestartCount", "oldest", }, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { numPods := len(test.pods) for i := 0; i < 20; i++ { idx := rand.Perm(numPods) randomizedPods := make([]*v1.Pod, numPods) for j := 0; j < numPods; j++ { randomizedPods[j] = &test.pods[idx[j]] } sort.Sort(ActivePods(randomizedPods)) gotOrder := make([]string, len(randomizedPods)) for i := range randomizedPods { gotOrder[i] = randomizedPods[i].Name } if diff := cmp.Diff(test.wantOrder, gotOrder); diff != "" { t.Errorf("Sorted active pod names (-want,+got):\n%s", diff) } } }) } } func TestSortingActivePodsWithRanks(t *testing.T) { now := metav1.Now() then1Month := metav1.Time{Time: now.AddDate(0, -1, 0)} then2Hours := metav1.Time{Time: now.Add(-2 * time.Hour)} then5Hours := metav1.Time{Time: now.Add(-5 * time.Hour)} then8Hours := metav1.Time{Time: now.Add(-8 * time.Hour)} zeroTime := metav1.Time{} pod := func(podName, nodeName string, phase v1.PodPhase, ready bool, restarts int32, readySince metav1.Time, created metav1.Time, annotations map[string]string) *v1.Pod { var conditions []v1.PodCondition var containerStatuses []v1.ContainerStatus if ready { conditions = []v1.PodCondition{{Type: v1.PodReady, Status: v1.ConditionTrue, LastTransitionTime: readySince}} containerStatuses = []v1.ContainerStatus{{RestartCount: restarts}} } return &v1.Pod{ ObjectMeta: metav1.ObjectMeta{ CreationTimestamp: created, Name: podName, Annotations: annotations, }, Spec: v1.PodSpec{NodeName: nodeName}, Status: v1.PodStatus{ Conditions: conditions, ContainerStatuses: containerStatuses, Phase: phase, }, } } var ( unscheduledPod = pod("unscheduled", "", v1.PodPending, false, 0, zeroTime, zeroTime, nil) scheduledPendingPod = pod("pending", "node", v1.PodPending, false, 0, zeroTime, zeroTime, nil) unknownPhasePod = pod("unknown-phase", "node", v1.PodUnknown, false, 0, zeroTime, zeroTime, nil) runningNotReadyPod = pod("not-ready", "node", v1.PodRunning, false, 0, zeroTime, zeroTime, nil) runningReadyNoLastTransitionTimePod = pod("ready-no-last-transition-time", "node", v1.PodRunning, true, 0, zeroTime, zeroTime, nil) runningReadyNow = pod("ready-now", "node", v1.PodRunning, true, 0, now, now, nil) runningReadyThen = pod("ready-then", "node", v1.PodRunning, true, 0, then1Month, then1Month, nil) runningReadyNowHighRestarts = pod("ready-high-restarts", "node", v1.PodRunning, true, 9001, now, now, nil) runningReadyNowCreatedThen = pod("ready-now-created-then", "node", v1.PodRunning, true, 0, now, then1Month, nil) lowPodDeletionCost = pod("low-deletion-cost", "node", v1.PodRunning, true, 0, now, then1Month, map[string]string{core.PodDeletionCost: "10"}) highPodDeletionCost = pod("high-deletion-cost", "node", v1.PodRunning, true, 0, now, then1Month, map[string]string{core.PodDeletionCost: "100"}) unscheduled5Hours = pod("unscheduled-5-hours", "", v1.PodPending, false, 0, then5Hours, then5Hours, nil) unscheduled8Hours = pod("unscheduled-10-hours", "", v1.PodPending, false, 0, then8Hours, then8Hours, nil) ready2Hours = pod("ready-2-hours", "", v1.PodRunning, true, 0, then2Hours, then1Month, nil) ready5Hours = pod("ready-5-hours", "", v1.PodRunning, true, 0, then5Hours, then1Month, nil) ready10Hours = pod("ready-10-hours", "", v1.PodRunning, true, 0, then8Hours, then1Month, nil) ) equalityTests := []struct { p1 *v1.Pod p2 *v1.Pod disableLogarithmicScaleDown bool }{ {p1: unscheduledPod}, {p1: scheduledPendingPod}, {p1: unknownPhasePod}, {p1: runningNotReadyPod}, {p1: runningReadyNowCreatedThen}, {p1: runningReadyNow}, {p1: runningReadyThen}, {p1: runningReadyNowHighRestarts}, {p1: runningReadyNowCreatedThen}, {p1: unscheduled5Hours, p2: unscheduled8Hours}, {p1: ready5Hours, p2: ready10Hours}, } for i, test := range equalityTests { t.Run(fmt.Sprintf("Equality tests %d", i), func(t *testing.T) { defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.LogarithmicScaleDown, !test.disableLogarithmicScaleDown)() if test.p2 == nil { test.p2 = test.p1 } podsWithRanks := ActivePodsWithRanks{ Pods: []*v1.Pod{test.p1, test.p2}, Rank: []int{1, 1}, Now: now, } if podsWithRanks.Less(0, 1) || podsWithRanks.Less(1, 0) { t.Errorf("expected pod %q to be equivalent to %q", test.p1.Name, test.p2.Name) } }) } type podWithRank struct { pod *v1.Pod rank int } inequalityTests := []struct { lesser, greater podWithRank disablePodDeletioncost bool disableLogarithmicScaleDown bool }{ {lesser: podWithRank{unscheduledPod, 1}, greater: podWithRank{scheduledPendingPod, 2}}, {lesser: podWithRank{unscheduledPod, 2}, greater: podWithRank{scheduledPendingPod, 1}}, {lesser: podWithRank{scheduledPendingPod, 1}, greater: podWithRank{unknownPhasePod, 2}}, {lesser: podWithRank{unknownPhasePod, 1}, greater: podWithRank{runningNotReadyPod, 2}}, {lesser: podWithRank{runningNotReadyPod, 1}, greater: podWithRank{runningReadyNoLastTransitionTimePod, 1}}, {lesser: podWithRank{runningReadyNoLastTransitionTimePod, 1}, greater: podWithRank{runningReadyNow, 1}}, {lesser: podWithRank{runningReadyNow, 2}, greater: podWithRank{runningReadyNoLastTransitionTimePod, 1}}, {lesser: podWithRank{runningReadyNow, 1}, greater: podWithRank{runningReadyThen, 1}}, {lesser: podWithRank{runningReadyNow, 2}, greater: podWithRank{runningReadyThen, 1}}, {lesser: podWithRank{runningReadyNowHighRestarts, 1}, greater: podWithRank{runningReadyNow, 1}}, {lesser: podWithRank{runningReadyNow, 2}, greater: podWithRank{runningReadyNowHighRestarts, 1}}, {lesser: podWithRank{runningReadyNow, 1}, greater: podWithRank{runningReadyNowCreatedThen, 1}}, {lesser: podWithRank{runningReadyNowCreatedThen, 2}, greater: podWithRank{runningReadyNow, 1}}, {lesser: podWithRank{lowPodDeletionCost, 2}, greater: podWithRank{highPodDeletionCost, 1}}, {lesser: podWithRank{highPodDeletionCost, 2}, greater: podWithRank{lowPodDeletionCost, 1}, disablePodDeletioncost: true}, {lesser: podWithRank{ready2Hours, 1}, greater: podWithRank{ready5Hours, 1}}, } for i, test := range inequalityTests { t.Run(fmt.Sprintf("Inequality tests %d", i), func(t *testing.T) { defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.PodDeletionCost, !test.disablePodDeletioncost)() defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.LogarithmicScaleDown, !test.disableLogarithmicScaleDown)() podsWithRanks := ActivePodsWithRanks{ Pods: []*v1.Pod{test.lesser.pod, test.greater.pod}, Rank: []int{test.lesser.rank, test.greater.rank}, Now: now, } if !podsWithRanks.Less(0, 1) { t.Errorf("expected pod %q with rank %v to be less than %q with rank %v", podsWithRanks.Pods[0].Name, podsWithRanks.Rank[0], podsWithRanks.Pods[1].Name, podsWithRanks.Rank[1]) } if podsWithRanks.Less(1, 0) { t.Errorf("expected pod %q with rank %v not to be less than %v with rank %v", podsWithRanks.Pods[1].Name, podsWithRanks.Rank[1], podsWithRanks.Pods[0].Name, podsWithRanks.Rank[0]) } }) } } func TestActiveReplicaSetsFiltering(t *testing.T) { rsUuid := uuid.NewUUID() tests := []struct { name string replicaSets []*apps.ReplicaSet wantReplicaSets []*apps.ReplicaSet }{ { name: "Filters active replica sets", replicaSets: []*apps.ReplicaSet{ newReplicaSet("zero", 0, rsUuid), nil, newReplicaSet("foo", 1, rsUuid), newReplicaSet("bar", 2, rsUuid), }, wantReplicaSets: []*apps.ReplicaSet{ newReplicaSet("foo", 1, rsUuid), newReplicaSet("bar", 2, rsUuid), }, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { gotReplicaSets := FilterActiveReplicaSets(test.replicaSets) if diff := cmp.Diff(test.wantReplicaSets, gotReplicaSets); diff != "" { t.Errorf("Active replica set names (-want,+got):\n%s", diff) } }) } } func TestComputeHash(t *testing.T) { collisionCount := int32(1) otherCollisionCount := int32(2) maxCollisionCount := int32(math.MaxInt32) tests := []struct { name string template *v1.PodTemplateSpec collisionCount *int32 otherCollisionCount *int32 }{ { name: "simple", template: &v1.PodTemplateSpec{}, collisionCount: &collisionCount, otherCollisionCount: &otherCollisionCount, }, { name: "using math.MaxInt64", template: &v1.PodTemplateSpec{}, collisionCount: nil, otherCollisionCount: &maxCollisionCount, }, } for _, test := range tests { hash := ComputeHash(test.template, test.collisionCount) otherHash := ComputeHash(test.template, test.otherCollisionCount) assert.NotEqual(t, hash, otherHash, "expected different hashes but got the same: %d", hash) } } func TestRemoveTaintOffNode(t *testing.T) { tests := []struct { name string nodeHandler *testutil.FakeNodeHandler nodeName string taintsToRemove []*v1.Taint expectedTaints []v1.Taint requestCount int }{ { name: "remove one taint from node", nodeHandler: &testutil.FakeNodeHandler{ Existing: []*v1.Node{ { ObjectMeta: metav1.ObjectMeta{ Name: "node1", }, Spec: v1.NodeSpec{ Taints: []v1.Taint{ {Key: "key1", Value: "value1", Effect: "NoSchedule"}, {Key: "key2", Value: "value2", Effect: "NoExecute"}, }, }, }, }, Clientset: fake.NewSimpleClientset(&v1.PodList{Items: []v1.Pod{*testutil.NewPod("pod0", "node0")}}), }, nodeName: "node1", taintsToRemove: []*v1.Taint{ {Key: "key2", Value: "value2", Effect: "NoExecute"}, }, expectedTaints: []v1.Taint{ {Key: "key1", Value: "value1", Effect: "NoSchedule"}, }, requestCount: 4, }, { name: "remove multiple taints from node", nodeHandler: &testutil.FakeNodeHandler{ Existing: []*v1.Node{ { ObjectMeta: metav1.ObjectMeta{ Name: "node1", }, Spec: v1.NodeSpec{ Taints: []v1.Taint{ {Key: "key1", Value: "value1", Effect: "NoSchedule"}, {Key: "key2", Value: "value2", Effect: "NoExecute"}, {Key: "key3", Value: "value3", Effect: "NoSchedule"}, {Key: "key4", Value: "value4", Effect: "NoExecute"}, }, }, }, }, Clientset: fake.NewSimpleClientset(&v1.PodList{Items: []v1.Pod{*testutil.NewPod("pod0", "node0")}}), }, nodeName: "node1", taintsToRemove: []*v1.Taint{ {Key: "key2", Value: "value2", Effect: "NoExecute"}, {Key: "key3", Value: "value3", Effect: "NoSchedule"}, }, expectedTaints: []v1.Taint{ {Key: "key1", Value: "value1", Effect: "NoSchedule"}, {Key: "key4", Value: "value4", Effect: "NoExecute"}, }, requestCount: 4, }, { name: "remove no-exist taints from node", nodeHandler: &testutil.FakeNodeHandler{ Existing: []*v1.Node{ { ObjectMeta: metav1.ObjectMeta{ Name: "node1", }, Spec: v1.NodeSpec{ Taints: []v1.Taint{ {Key: "key1", Value: "value1", Effect: "NoSchedule"}, {Key: "key2", Value: "value2", Effect: "NoExecute"}, }, }, }, }, Clientset: fake.NewSimpleClientset(&v1.PodList{Items: []v1.Pod{*testutil.NewPod("pod0", "node0")}}), }, nodeName: "node1", taintsToRemove: []*v1.Taint{ {Key: "key3", Value: "value3", Effect: "NoSchedule"}, }, expectedTaints: []v1.Taint{ {Key: "key1", Value: "value1", Effect: "NoSchedule"}, {Key: "key2", Value: "value2", Effect: "NoExecute"}, }, requestCount: 2, }, { name: "remove taint from node without taints", nodeHandler: &testutil.FakeNodeHandler{ Existing: []*v1.Node{ { ObjectMeta: metav1.ObjectMeta{ Name: "node1", }, }, }, Clientset: fake.NewSimpleClientset(&v1.PodList{Items: []v1.Pod{*testutil.NewPod("pod0", "node0")}}), }, nodeName: "node1", taintsToRemove: []*v1.Taint{ {Key: "key3", Value: "value3", Effect: "NoSchedule"}, }, expectedTaints: nil, requestCount: 2, }, { name: "remove empty taint list from node without taints", nodeHandler: &testutil.FakeNodeHandler{ Existing: []*v1.Node{ { ObjectMeta: metav1.ObjectMeta{ Name: "node1", }, }, }, Clientset: fake.NewSimpleClientset(&v1.PodList{Items: []v1.Pod{*testutil.NewPod("pod0", "node0")}}), }, nodeName: "node1", taintsToRemove: []*v1.Taint{}, expectedTaints: nil, requestCount: 2, }, { name: "remove empty taint list from node", nodeHandler: &testutil.FakeNodeHandler{ Existing: []*v1.Node{ { ObjectMeta: metav1.ObjectMeta{ Name: "node1", }, Spec: v1.NodeSpec{ Taints: []v1.Taint{ {Key: "key1", Value: "value1", Effect: "NoSchedule"}, {Key: "key2", Value: "value2", Effect: "NoExecute"}, }, }, }, }, Clientset: fake.NewSimpleClientset(&v1.PodList{Items: []v1.Pod{*testutil.NewPod("pod0", "node0")}}), }, nodeName: "node1", taintsToRemove: []*v1.Taint{}, expectedTaints: []v1.Taint{ {Key: "key1", Value: "value1", Effect: "NoSchedule"}, {Key: "key2", Value: "value2", Effect: "NoExecute"}, }, requestCount: 2, }, } for _, test := range tests { node, _ := test.nodeHandler.Get(context.TODO(), test.nodeName, metav1.GetOptions{}) err := RemoveTaintOffNode(context.TODO(), test.nodeHandler, test.nodeName, node, test.taintsToRemove...) assert.NoError(t, err, "%s: RemoveTaintOffNode() error = %v", test.name, err) node, _ = test.nodeHandler.Get(context.TODO(), test.nodeName, metav1.GetOptions{}) assert.EqualValues(t, test.expectedTaints, node.Spec.Taints, "%s: failed to remove taint off node: expected %+v, got %+v", test.name, test.expectedTaints, node.Spec.Taints) assert.Equal(t, test.requestCount, test.nodeHandler.RequestCount, "%s: unexpected request count: expected %+v, got %+v", test.name, test.requestCount, test.nodeHandler.RequestCount) } } func TestAddOrUpdateTaintOnNode(t *testing.T) { tests := []struct { name string nodeHandler *testutil.FakeNodeHandler nodeName string taintsToAdd []*v1.Taint expectedTaints []v1.Taint requestCount int expectedErr error }{ { name: "add one taint on node", nodeHandler: &testutil.FakeNodeHandler{ Existing: []*v1.Node{ { ObjectMeta: metav1.ObjectMeta{ Name: "node1", }, Spec: v1.NodeSpec{ Taints: []v1.Taint{ {Key: "key1", Value: "value1", Effect: "NoSchedule"}, }, }, }, }, Clientset: fake.NewSimpleClientset(&v1.PodList{Items: []v1.Pod{*testutil.NewPod("pod0", "node0")}}), }, nodeName: "node1", taintsToAdd: []*v1.Taint{ {Key: "key2", Value: "value2", Effect: "NoExecute"}, }, expectedTaints: []v1.Taint{ {Key: "key1", Value: "value1", Effect: "NoSchedule"}, {Key: "key2", Value: "value2", Effect: "NoExecute"}, }, requestCount: 3, }, { name: "add multiple taints to node", nodeHandler: &testutil.FakeNodeHandler{ Existing: []*v1.Node{ { ObjectMeta: metav1.ObjectMeta{ Name: "node1", }, Spec: v1.NodeSpec{ Taints: []v1.Taint{ {Key: "key1", Value: "value1", Effect: "NoSchedule"}, {Key: "key2", Value: "value2", Effect: "NoExecute"}, }, }, }, }, Clientset: fake.NewSimpleClientset(&v1.PodList{Items: []v1.Pod{*testutil.NewPod("pod0", "node0")}}), }, nodeName: "node1", taintsToAdd: []*v1.Taint{ {Key: "key3", Value: "value3", Effect: "NoSchedule"}, {Key: "key4", Value: "value4", Effect: "NoExecute"}, }, expectedTaints: []v1.Taint{ {Key: "key1", Value: "value1", Effect: "NoSchedule"}, {Key: "key2", Value: "value2", Effect: "NoExecute"}, {Key: "key3", Value: "value3", Effect: "NoSchedule"}, {Key: "key4", Value: "value4", Effect: "NoExecute"}, }, requestCount: 3, }, { name: "add exist taints to node", nodeHandler: &testutil.FakeNodeHandler{ Existing: []*v1.Node{ { ObjectMeta: metav1.ObjectMeta{ Name: "node1", }, Spec: v1.NodeSpec{ Taints: []v1.Taint{ {Key: "key1", Value: "value1", Effect: "NoSchedule"}, {Key: "key2", Value: "value2", Effect: "NoExecute"}, }, }, }, }, Clientset: fake.NewSimpleClientset(&v1.PodList{Items: []v1.Pod{*testutil.NewPod("pod0", "node0")}}), }, nodeName: "node1", taintsToAdd: []*v1.Taint{ {Key: "key2", Value: "value2", Effect: "NoExecute"}, }, expectedTaints: []v1.Taint{ {Key: "key1", Value: "value1", Effect: "NoSchedule"}, {Key: "key2", Value: "value2", Effect: "NoExecute"}, }, requestCount: 2, }, { name: "add taint to node without taints", nodeHandler: &testutil.FakeNodeHandler{ Existing: []*v1.Node{ { ObjectMeta: metav1.ObjectMeta{ Name: "node1", }, }, }, Clientset: fake.NewSimpleClientset(&v1.PodList{Items: []v1.Pod{*testutil.NewPod("pod0", "node0")}}), }, nodeName: "node1", taintsToAdd: []*v1.Taint{ {Key: "key3", Value: "value3", Effect: "NoSchedule"}, }, expectedTaints: []v1.Taint{ {Key: "key3", Value: "value3", Effect: "NoSchedule"}, }, requestCount: 3, }, { name: "add empty taint list to node without taints", nodeHandler: &testutil.FakeNodeHandler{ Existing: []*v1.Node{ { ObjectMeta: metav1.ObjectMeta{ Name: "node1", }, }, }, Clientset: fake.NewSimpleClientset(&v1.PodList{Items: []v1.Pod{*testutil.NewPod("pod0", "node0")}}), }, nodeName: "node1", taintsToAdd: []*v1.Taint{}, expectedTaints: nil, requestCount: 1, }, { name: "add empty taint list to node", nodeHandler: &testutil.FakeNodeHandler{ Existing: []*v1.Node{ { ObjectMeta: metav1.ObjectMeta{ Name: "node1", }, Spec: v1.NodeSpec{ Taints: []v1.Taint{ {Key: "key1", Value: "value1", Effect: "NoSchedule"}, {Key: "key2", Value: "value2", Effect: "NoExecute"}, }, }, }, }, Clientset: fake.NewSimpleClientset(&v1.PodList{Items: []v1.Pod{*testutil.NewPod("pod0", "node0")}}), }, nodeName: "node1", taintsToAdd: []*v1.Taint{}, expectedTaints: []v1.Taint{ {Key: "key1", Value: "value1", Effect: "NoSchedule"}, {Key: "key2", Value: "value2", Effect: "NoExecute"}, }, requestCount: 1, }, { name: "add taint to changed node", nodeHandler: &testutil.FakeNodeHandler{ Existing: []*v1.Node{ { ObjectMeta: metav1.ObjectMeta{ Name: "node1", ResourceVersion: "1", }, Spec: v1.NodeSpec{ Taints: []v1.Taint{ {Key: "key1", Value: "value1", Effect: "NoSchedule"}, }, }, }, }, Clientset: fake.NewSimpleClientset(&v1.PodList{Items: []v1.Pod{*testutil.NewPod("pod0", "node0")}}), AsyncCalls: []func(*testutil.FakeNodeHandler){func(m *testutil.FakeNodeHandler) { if len(m.UpdatedNodes) == 0 { m.UpdatedNodes = append(m.UpdatedNodes, &v1.Node{ ObjectMeta: metav1.ObjectMeta{ Name: "node1", ResourceVersion: "2", }, Spec: v1.NodeSpec{ Taints: []v1.Taint{}, }}) } }}, }, nodeName: "node1", taintsToAdd: []*v1.Taint{{Key: "key2", Value: "value2", Effect: "NoExecute"}}, expectedTaints: []v1.Taint{ {Key: "key2", Value: "value2", Effect: "NoExecute"}, }, requestCount: 5, }, { name: "add taint to non-exist node", nodeHandler: &testutil.FakeNodeHandler{ Existing: []*v1.Node{ { ObjectMeta: metav1.ObjectMeta{ Name: "node1", ResourceVersion: "1", }, Spec: v1.NodeSpec{ Taints: []v1.Taint{ {Key: "key1", Value: "value1", Effect: "NoSchedule"}, }, }, }, }, Clientset: fake.NewSimpleClientset(&v1.PodList{Items: []v1.Pod{*testutil.NewPod("pod0", "node0")}}), }, nodeName: "node2", taintsToAdd: []*v1.Taint{{Key: "key2", Value: "value2", Effect: "NoExecute"}}, expectedErr: apierrors.NewNotFound(schema.GroupResource{Resource: "nodes"}, "node2"), }, } for _, test := range tests { err := AddOrUpdateTaintOnNode(context.TODO(), test.nodeHandler, test.nodeName, test.taintsToAdd...) if test.expectedErr != nil { assert.Equal(t, test.expectedErr, err, "AddOrUpdateTaintOnNode get unexpected error") continue } assert.NoError(t, err, "%s: AddOrUpdateTaintOnNode() error = %v", test.name, err) node, _ := test.nodeHandler.Get(context.TODO(), test.nodeName, metav1.GetOptions{}) assert.EqualValues(t, test.expectedTaints, node.Spec.Taints, "%s: failed to add taint to node: expected %+v, got %+v", test.name, test.expectedTaints, node.Spec.Taints) assert.Equal(t, test.requestCount, test.nodeHandler.RequestCount, "%s: unexpected request count: expected %+v, got %+v", test.name, test.requestCount, test.nodeHandler.RequestCount) } }