532 lines
16 KiB
Go
532 lines
16 KiB
Go
/*
|
|
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 drain
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"math"
|
|
"os"
|
|
"reflect"
|
|
"sort"
|
|
"strconv"
|
|
"testing"
|
|
"time"
|
|
|
|
corev1 "k8s.io/api/core/v1"
|
|
policyv1 "k8s.io/api/policy/v1"
|
|
policyv1beta1 "k8s.io/api/policy/v1beta1"
|
|
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/client-go/kubernetes/fake"
|
|
ktest "k8s.io/client-go/testing"
|
|
)
|
|
|
|
func TestDeletePods(t *testing.T) {
|
|
ifHasBeenCalled := map[string]bool{}
|
|
tests := []struct {
|
|
description string
|
|
interval time.Duration
|
|
timeout time.Duration
|
|
ctxTimeoutEarly bool
|
|
expectPendingPods bool
|
|
expectError bool
|
|
expectedError *error
|
|
getPodFn func(namespace, name string) (*corev1.Pod, error)
|
|
}{
|
|
{
|
|
description: "Wait for deleting to complete",
|
|
interval: 100 * time.Millisecond,
|
|
timeout: 10 * time.Second,
|
|
expectPendingPods: false,
|
|
expectError: false,
|
|
expectedError: nil,
|
|
getPodFn: func(namespace, name string) (*corev1.Pod, error) {
|
|
oldPodMap, _ := createPods(false)
|
|
newPodMap, _ := createPods(true)
|
|
if oldPod, found := oldPodMap[name]; found {
|
|
if _, ok := ifHasBeenCalled[name]; !ok {
|
|
ifHasBeenCalled[name] = true
|
|
return &oldPod, nil
|
|
}
|
|
if oldPod.ObjectMeta.Generation < 4 {
|
|
newPod := newPodMap[name]
|
|
return &newPod, nil
|
|
}
|
|
return &corev1.Pod{}, apierrors.NewNotFound(schema.GroupResource{Resource: "pods"}, name)
|
|
}
|
|
return &corev1.Pod{}, apierrors.NewNotFound(schema.GroupResource{Resource: "pods"}, name)
|
|
},
|
|
},
|
|
{
|
|
description: "Pod found with same name but different UID",
|
|
interval: 100 * time.Millisecond,
|
|
timeout: 10 * time.Second,
|
|
expectPendingPods: false,
|
|
expectError: false,
|
|
expectedError: nil,
|
|
getPodFn: func(namespace, name string) (*corev1.Pod, error) {
|
|
// Return a pod with the same name, but different UID
|
|
return &corev1.Pod{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Namespace: namespace,
|
|
Name: name,
|
|
UID: "SOME_OTHER_UID",
|
|
},
|
|
}, nil
|
|
},
|
|
},
|
|
{
|
|
description: "Deleting could timeout",
|
|
interval: 200 * time.Millisecond,
|
|
timeout: 3 * time.Second,
|
|
expectPendingPods: true,
|
|
expectError: true,
|
|
expectedError: &context.DeadlineExceeded,
|
|
getPodFn: func(namespace, name string) (*corev1.Pod, error) {
|
|
oldPodMap, _ := createPods(false)
|
|
if oldPod, found := oldPodMap[name]; found {
|
|
return &oldPod, nil
|
|
}
|
|
return &corev1.Pod{}, fmt.Errorf("%q: not found", name)
|
|
},
|
|
},
|
|
{
|
|
description: "Context Canceled",
|
|
interval: 1000 * time.Millisecond,
|
|
timeout: 5 * time.Second,
|
|
ctxTimeoutEarly: true,
|
|
expectPendingPods: true,
|
|
expectError: true,
|
|
expectedError: &context.Canceled,
|
|
getPodFn: func(namespace, name string) (*corev1.Pod, error) {
|
|
oldPodMap, _ := createPods(false)
|
|
if oldPod, found := oldPodMap[name]; found {
|
|
return &oldPod, nil
|
|
}
|
|
return &corev1.Pod{}, fmt.Errorf("%q: not found", name)
|
|
},
|
|
},
|
|
{
|
|
description: "Skip Deleted Pod",
|
|
interval: 200 * time.Millisecond,
|
|
timeout: 3 * time.Second,
|
|
expectPendingPods: false,
|
|
expectError: false,
|
|
expectedError: nil,
|
|
getPodFn: func(namespace, name string) (*corev1.Pod, error) {
|
|
oldPodMap, _ := createPods(false)
|
|
if oldPod, found := oldPodMap[name]; found {
|
|
dTime := &metav1.Time{Time: time.Now().Add(time.Duration(100) * time.Second * -1)}
|
|
oldPod.ObjectMeta.SetDeletionTimestamp(dTime)
|
|
return &oldPod, nil
|
|
}
|
|
return &corev1.Pod{}, fmt.Errorf("%q: not found", name)
|
|
},
|
|
},
|
|
{
|
|
description: "Client error could be passed out",
|
|
interval: 200 * time.Millisecond,
|
|
timeout: 5 * time.Second,
|
|
expectPendingPods: true,
|
|
expectError: true,
|
|
expectedError: nil,
|
|
getPodFn: func(namespace, name string) (*corev1.Pod, error) {
|
|
return &corev1.Pod{}, errors.New("This is a random error for testing")
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.description, func(t *testing.T) {
|
|
_, pods := createPods(false)
|
|
var ctx context.Context
|
|
var cancel context.CancelFunc
|
|
ctx = context.Background()
|
|
if test.ctxTimeoutEarly {
|
|
ctx, cancel = context.WithTimeout(ctx, 100*time.Millisecond)
|
|
defer cancel()
|
|
}
|
|
params := waitForDeleteParams{
|
|
ctx: ctx,
|
|
pods: pods,
|
|
interval: test.interval,
|
|
timeout: test.timeout,
|
|
usingEviction: false,
|
|
getPodFn: test.getPodFn,
|
|
onDoneFn: nil,
|
|
globalTimeout: time.Duration(math.MaxInt64),
|
|
out: os.Stdout,
|
|
skipWaitForDeleteTimeoutSeconds: 10,
|
|
}
|
|
start := time.Now()
|
|
pendingPods, err := waitForDelete(params)
|
|
elapsed := time.Since(start)
|
|
|
|
if test.expectError {
|
|
if err == nil {
|
|
t.Fatalf("%s: unexpected non-error", test.description)
|
|
} else if test.expectedError != nil {
|
|
if test.ctxTimeoutEarly {
|
|
if elapsed >= test.timeout {
|
|
t.Fatalf("%s: the supplied context did not effectively cancel the waitForDelete", test.description)
|
|
}
|
|
} else if *test.expectedError != err {
|
|
t.Fatalf("%s: the error does not match expected error", test.description)
|
|
}
|
|
}
|
|
}
|
|
if !test.expectError && err != nil {
|
|
t.Fatalf("%s: unexpected error", test.description)
|
|
}
|
|
if test.expectPendingPods && len(pendingPods) == 0 {
|
|
t.Fatalf("%s: unexpected empty pods", test.description)
|
|
}
|
|
if !test.expectPendingPods && len(pendingPods) > 0 {
|
|
t.Fatalf("%s: unexpected pending pods", test.description)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func createPods(ifCreateNewPods bool) (map[string]corev1.Pod, []corev1.Pod) {
|
|
podMap := make(map[string]corev1.Pod)
|
|
podSlice := []corev1.Pod{}
|
|
for i := 0; i < 8; i++ {
|
|
var uid types.UID
|
|
if ifCreateNewPods {
|
|
uid = types.UID(strconv.Itoa(i))
|
|
} else {
|
|
uid = types.UID(strconv.Itoa(i) + strconv.Itoa(i))
|
|
}
|
|
pod := corev1.Pod{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "pod" + strconv.Itoa(i),
|
|
Namespace: "default",
|
|
UID: uid,
|
|
Generation: int64(i),
|
|
},
|
|
}
|
|
podMap[pod.Name] = pod
|
|
podSlice = append(podSlice, pod)
|
|
}
|
|
return podMap, podSlice
|
|
}
|
|
|
|
func addCoreNonEvictionSupport(t *testing.T, k *fake.Clientset) {
|
|
coreResources := &metav1.APIResourceList{
|
|
GroupVersion: "v1",
|
|
}
|
|
k.Resources = append(k.Resources, coreResources)
|
|
}
|
|
|
|
// addEvictionSupport implements simple fake eviction support on the fake.Clientset
|
|
func addEvictionSupport(t *testing.T, k *fake.Clientset, version string) {
|
|
podsEviction := metav1.APIResource{
|
|
Name: "pods/eviction",
|
|
Kind: "Eviction",
|
|
Group: "policy",
|
|
Version: version,
|
|
}
|
|
coreResources := &metav1.APIResourceList{
|
|
GroupVersion: "v1",
|
|
APIResources: []metav1.APIResource{podsEviction},
|
|
}
|
|
|
|
policyResources := &metav1.APIResourceList{
|
|
GroupVersion: "policy/v1",
|
|
}
|
|
k.Resources = append(k.Resources, coreResources, policyResources)
|
|
|
|
// Delete pods when evict is called
|
|
k.PrependReactor("create", "pods", func(action ktest.Action) (bool, runtime.Object, error) {
|
|
if action.GetSubresource() != "eviction" {
|
|
return false, nil, nil
|
|
}
|
|
|
|
namespace := ""
|
|
name := ""
|
|
switch version {
|
|
case "v1":
|
|
eviction := *action.(ktest.CreateAction).GetObject().(*policyv1.Eviction)
|
|
namespace = eviction.Namespace
|
|
name = eviction.Name
|
|
case "v1beta1":
|
|
eviction := *action.(ktest.CreateAction).GetObject().(*policyv1beta1.Eviction)
|
|
namespace = eviction.Namespace
|
|
name = eviction.Name
|
|
default:
|
|
t.Errorf("unknown version %s", version)
|
|
}
|
|
// Avoid the lock
|
|
go func() {
|
|
err := k.CoreV1().Pods(namespace).Delete(context.TODO(), name, metav1.DeleteOptions{})
|
|
if err != nil {
|
|
// Errorf because we can't call Fatalf from another goroutine
|
|
t.Errorf("failed to delete pod: %s/%s", namespace, name)
|
|
}
|
|
}()
|
|
|
|
return true, nil, nil
|
|
})
|
|
}
|
|
|
|
func TestCheckEvictionSupport(t *testing.T) {
|
|
for _, evictionVersion := range []string{"", "v1", "v1beta1"} {
|
|
t.Run(fmt.Sprintf("evictionVersion=%v", evictionVersion),
|
|
func(t *testing.T) {
|
|
k := fake.NewSimpleClientset()
|
|
if len(evictionVersion) > 0 {
|
|
addEvictionSupport(t, k, evictionVersion)
|
|
} else {
|
|
addCoreNonEvictionSupport(t, k)
|
|
}
|
|
|
|
apiGroup, err := CheckEvictionSupport(k)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
expectedAPIGroup := schema.GroupVersion{}
|
|
if len(evictionVersion) > 0 {
|
|
expectedAPIGroup = schema.GroupVersion{Group: "policy", Version: evictionVersion}
|
|
}
|
|
if apiGroup != expectedAPIGroup {
|
|
t.Fatalf("expected apigroup %q, actual=%q", expectedAPIGroup, apiGroup)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDeleteOrEvict(t *testing.T) {
|
|
tests := []struct {
|
|
description string
|
|
evictionSupported bool
|
|
disableEviction bool
|
|
}{
|
|
{
|
|
description: "eviction supported/enabled",
|
|
evictionSupported: true,
|
|
disableEviction: false,
|
|
},
|
|
{
|
|
description: "eviction unsupported/disabled",
|
|
evictionSupported: false,
|
|
disableEviction: false,
|
|
},
|
|
{
|
|
description: "eviction supported/disabled",
|
|
evictionSupported: true,
|
|
disableEviction: true,
|
|
},
|
|
{
|
|
description: "eviction unsupported/disabled",
|
|
evictionSupported: false,
|
|
disableEviction: false,
|
|
},
|
|
}
|
|
for _, tc := range tests {
|
|
t.Run(tc.description, func(t *testing.T) {
|
|
h := &Helper{
|
|
Out: os.Stdout,
|
|
GracePeriodSeconds: 10,
|
|
OnPodDeletionOrEvictionStarted: func(pod *corev1.Pod, usingEviction bool) {
|
|
if tc.evictionSupported && !tc.disableEviction {
|
|
if !usingEviction {
|
|
t.Errorf("%s: OnPodDeletionOrEvictionStarted callback failed while evicting; actual\n\t%v\nexpected\n\t%v", tc.description, usingEviction, !usingEviction)
|
|
}
|
|
} else if tc.evictionSupported && tc.disableEviction {
|
|
if usingEviction {
|
|
t.Errorf("%s: OnPodDeletionOrEvictionStarted callback failed while deleting; actual\n\t%v\nexpected\n\t%v", tc.description, !usingEviction, usingEviction)
|
|
}
|
|
}
|
|
},
|
|
OnPodDeletedOrEvicted: func(pod *corev1.Pod, usingEviction bool) {
|
|
if tc.evictionSupported && !tc.disableEviction {
|
|
if !usingEviction {
|
|
t.Errorf("%s: OnPodDeletedOrEvicted callback failed while evicting; actual\n\t%v\nexpected\n\t%v", tc.description, usingEviction, !usingEviction)
|
|
}
|
|
} else if tc.evictionSupported && tc.disableEviction {
|
|
if usingEviction {
|
|
t.Errorf("%s: OnPodDeletedOrEvicted callback failed while deleting; actual\n\t%v\nexpected\n\t%v", tc.description, !usingEviction, usingEviction)
|
|
}
|
|
}
|
|
},
|
|
OnPodDeletionOrEvictionFinished: func(pod *corev1.Pod, usingEviction bool, err error) {
|
|
if tc.evictionSupported && !tc.disableEviction {
|
|
if !usingEviction {
|
|
t.Errorf("%s: OnPodDeletionOrEvictionFinished callback failed while evicting; actual\n\t%v\nexpected\n\t%v", tc.description, usingEviction, !usingEviction)
|
|
}
|
|
} else if tc.evictionSupported && tc.disableEviction {
|
|
if usingEviction {
|
|
t.Errorf("%s: OnPodDeletionOrEvictionFinished callback failed while deleting; actual\n\t%v\nexpected\n\t%v", tc.description, !usingEviction, usingEviction)
|
|
}
|
|
}
|
|
},
|
|
}
|
|
|
|
// Create 4 pods, and try to remove the first 2
|
|
var expectedEvictions []policyv1.Eviction
|
|
var create []runtime.Object
|
|
deletePods := []corev1.Pod{}
|
|
for i := 1; i <= 4; i++ {
|
|
pod := &corev1.Pod{}
|
|
pod.Name = fmt.Sprintf("mypod-%d", i)
|
|
pod.Namespace = "default"
|
|
|
|
create = append(create, pod)
|
|
if i <= 2 {
|
|
deletePods = append(deletePods, *pod)
|
|
|
|
if tc.evictionSupported && !tc.disableEviction {
|
|
eviction := policyv1.Eviction{}
|
|
eviction.Namespace = pod.Namespace
|
|
eviction.Name = pod.Name
|
|
|
|
gracePeriodSeconds := int64(h.GracePeriodSeconds)
|
|
eviction.DeleteOptions = &metav1.DeleteOptions{
|
|
GracePeriodSeconds: &gracePeriodSeconds,
|
|
}
|
|
|
|
expectedEvictions = append(expectedEvictions, eviction)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Build the fake client
|
|
k := fake.NewSimpleClientset(create...)
|
|
if tc.evictionSupported {
|
|
addEvictionSupport(t, k, "v1")
|
|
} else {
|
|
addCoreNonEvictionSupport(t, k)
|
|
}
|
|
h.Client = k
|
|
h.DisableEviction = tc.disableEviction
|
|
// Do the eviction
|
|
if err := h.DeleteOrEvictPods(deletePods); err != nil {
|
|
t.Fatalf("error from DeleteOrEvictPods: %v", err)
|
|
}
|
|
|
|
// Test that other pods are still there
|
|
var remainingPods []string
|
|
{
|
|
podList, err := k.CoreV1().Pods("").List(context.TODO(), metav1.ListOptions{})
|
|
if err != nil {
|
|
t.Fatalf("error listing pods: %v", err)
|
|
}
|
|
|
|
for _, pod := range podList.Items {
|
|
remainingPods = append(remainingPods, pod.Namespace+"/"+pod.Name)
|
|
}
|
|
sort.Strings(remainingPods)
|
|
}
|
|
expected := []string{"default/mypod-3", "default/mypod-4"}
|
|
if !reflect.DeepEqual(remainingPods, expected) {
|
|
t.Errorf("%s: unexpected remaining pods after DeleteOrEvictPods; actual %v; expected %v", tc.description, remainingPods, expected)
|
|
}
|
|
|
|
// Test that pods were evicted as expected
|
|
var actualEvictions []policyv1.Eviction
|
|
for _, action := range k.Actions() {
|
|
if action.GetVerb() != "create" || action.GetResource().Resource != "pods" || action.GetSubresource() != "eviction" {
|
|
continue
|
|
}
|
|
eviction := *action.(ktest.CreateAction).GetObject().(*policyv1.Eviction)
|
|
actualEvictions = append(actualEvictions, eviction)
|
|
}
|
|
sort.Slice(actualEvictions, func(i, j int) bool {
|
|
return actualEvictions[i].Name < actualEvictions[j].Name
|
|
})
|
|
if !reflect.DeepEqual(actualEvictions, expectedEvictions) {
|
|
t.Errorf("%s: unexpected evictions; actual\n\t%v\nexpected\n\t%v", tc.description, actualEvictions, expectedEvictions)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func mockFilterSkip(_ corev1.Pod) PodDeleteStatus {
|
|
return MakePodDeleteStatusSkip()
|
|
}
|
|
|
|
func mockFilterOkay(_ corev1.Pod) PodDeleteStatus {
|
|
return MakePodDeleteStatusOkay()
|
|
}
|
|
|
|
func TestFilterPods(t *testing.T) {
|
|
tCases := []struct {
|
|
description string
|
|
expectedPodListLen int
|
|
additionalFilters []PodFilter
|
|
}{
|
|
{
|
|
description: "AdditionalFilter skip all",
|
|
expectedPodListLen: 0,
|
|
additionalFilters: []PodFilter{
|
|
mockFilterSkip,
|
|
mockFilterOkay,
|
|
},
|
|
},
|
|
{
|
|
description: "AdditionalFilter okay all",
|
|
expectedPodListLen: 1,
|
|
additionalFilters: []PodFilter{
|
|
mockFilterOkay,
|
|
},
|
|
},
|
|
{
|
|
description: "AdditionalFilter Skip after Okay all skip",
|
|
expectedPodListLen: 0,
|
|
additionalFilters: []PodFilter{
|
|
mockFilterOkay,
|
|
mockFilterSkip,
|
|
},
|
|
},
|
|
{
|
|
description: "No additionalFilters okay all",
|
|
expectedPodListLen: 1,
|
|
},
|
|
}
|
|
for _, tc := range tCases {
|
|
t.Run(tc.description, func(t *testing.T) {
|
|
h := &Helper{
|
|
Force: true,
|
|
AdditionalFilters: tc.additionalFilters,
|
|
}
|
|
pod := corev1.Pod{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "pod",
|
|
Namespace: "default",
|
|
},
|
|
}
|
|
podList := corev1.PodList{
|
|
Items: []corev1.Pod{
|
|
pod,
|
|
},
|
|
}
|
|
|
|
list := filterPods(&podList, h.makeFilters())
|
|
podsLen := len(list.Pods())
|
|
if podsLen != tc.expectedPodListLen {
|
|
t.Errorf("%s: unexpected evictions; actual %v; expected %v", tc.description, podsLen, tc.expectedPodListLen)
|
|
}
|
|
})
|
|
}
|
|
}
|