kubectl/drain: add option skip-wait-for-delete-timeout
Currently, some circumstances may cause waitForDelete to never succeed after the pod has been marked for deletion. In particular, Nodes that are unresponsive and have pods with local-storage will not be able to successfully drain. We should allow drain to ignore pods that have a DeletionTimestamp older than a user-provided age. This will allow controllers utilizing kubectl/drain to optionally account for a pod that cannot be removed due to a misbehaving node. Kubernetes-commit: da53044abdf8c8a9771a5c3dfd861f0c4ec78c40
This commit is contained in:
parent
b909fcb4a0
commit
ebda9f6262
|
@ -193,6 +193,7 @@ func NewCmdDrain(f cmdutil.Factory, ioStreams genericclioptions.IOStreams) *cobr
|
|||
cmd.Flags().StringVarP(&o.drainer.Selector, "selector", "l", o.drainer.Selector, "Selector (label query) to filter on")
|
||||
cmd.Flags().StringVarP(&o.drainer.PodSelector, "pod-selector", "", o.drainer.PodSelector, "Label selector to filter pods on the node")
|
||||
cmd.Flags().BoolVar(&o.drainer.DisableEviction, "disable-eviction", o.drainer.DisableEviction, "Force drain to use delete, even if eviction is supported. This will bypass checking PodDisruptionBudgets, use with caution.")
|
||||
cmd.Flags().IntVar(&o.drainer.SkipWaitForDeleteTimeoutSeconds, "skip-wait-for-delete-timeout", o.drainer.SkipWaitForDeleteTimeoutSeconds, "If pod DeletionTimestamp older than N seconds, skip waiting for the pod. Seconds must be greater than 0 to skip.")
|
||||
|
||||
cmdutil.AddDryRunFlag(cmd)
|
||||
return cmd
|
||||
|
|
|
@ -39,6 +39,7 @@ const (
|
|||
EvictionKind = "Eviction"
|
||||
// EvictionSubresource represents the kind of evictions object as pod's subresource
|
||||
EvictionSubresource = "pods/eviction"
|
||||
podSkipMsgTemplate = "pod %q has DeletionTimestamp older than %v seconds, skipping\n"
|
||||
)
|
||||
|
||||
// Helper contains the parameters to control the behaviour of drainer
|
||||
|
@ -56,6 +57,12 @@ type Helper struct {
|
|||
// DisableEviction forces drain to use delete rather than evict
|
||||
DisableEviction bool
|
||||
|
||||
// SkipWaitForDeleteTimeoutSeconds ignores pods that have a
|
||||
// DeletionTimeStamp > N seconds. It's up to the user to decide when this
|
||||
// option is appropriate; examples include the Node is unready and the pods
|
||||
// won't drain otherwise
|
||||
SkipWaitForDeleteTimeoutSeconds int
|
||||
|
||||
Out io.Writer
|
||||
ErrOut io.Writer
|
||||
|
||||
|
@ -66,6 +73,19 @@ type Helper struct {
|
|||
OnPodDeletedOrEvicted func(pod *corev1.Pod, usingEviction bool)
|
||||
}
|
||||
|
||||
type waitForDeleteParams struct {
|
||||
ctx context.Context
|
||||
pods []corev1.Pod
|
||||
interval time.Duration
|
||||
timeout time.Duration
|
||||
usingEviction bool
|
||||
getPodFn func(string, string) (*corev1.Pod, error)
|
||||
onDoneFn func(pod *corev1.Pod, usingEviction bool)
|
||||
globalTimeout time.Duration
|
||||
skipWaitForDeleteTimeoutSeconds int
|
||||
out io.Writer
|
||||
}
|
||||
|
||||
// CheckEvictionSupport uses Discovery API to find out if the server support
|
||||
// eviction subresource If support, it will return its groupVersion; Otherwise,
|
||||
// it will return an empty string
|
||||
|
@ -238,7 +258,19 @@ func (d *Helper) evictPods(pods []corev1.Pod, policyGroupVersion string, getPodF
|
|||
return
|
||||
}
|
||||
}
|
||||
_, err := waitForDelete(ctx, []corev1.Pod{pod}, 1*time.Second, time.Duration(math.MaxInt64), true, getPodFn, d.OnPodDeletedOrEvicted, globalTimeout)
|
||||
params := waitForDeleteParams{
|
||||
ctx: ctx,
|
||||
pods: []corev1.Pod{pod},
|
||||
interval: 1 * time.Second,
|
||||
timeout: time.Duration(math.MaxInt64),
|
||||
usingEviction: true,
|
||||
getPodFn: getPodFn,
|
||||
onDoneFn: d.OnPodDeletedOrEvicted,
|
||||
globalTimeout: globalTimeout,
|
||||
skipWaitForDeleteTimeoutSeconds: d.SkipWaitForDeleteTimeoutSeconds,
|
||||
out: d.Out,
|
||||
}
|
||||
_, err := waitForDelete(params)
|
||||
if err == nil {
|
||||
returnCh <- nil
|
||||
} else {
|
||||
|
@ -280,31 +312,48 @@ func (d *Helper) deletePods(pods []corev1.Pod, getPodFn func(namespace, name str
|
|||
}
|
||||
}
|
||||
ctx := d.getContext()
|
||||
_, err := waitForDelete(ctx, pods, 1*time.Second, globalTimeout, false, getPodFn, d.OnPodDeletedOrEvicted, globalTimeout)
|
||||
params := waitForDeleteParams{
|
||||
ctx: ctx,
|
||||
pods: pods,
|
||||
interval: 1 * time.Second,
|
||||
timeout: globalTimeout,
|
||||
usingEviction: false,
|
||||
getPodFn: getPodFn,
|
||||
onDoneFn: d.OnPodDeletedOrEvicted,
|
||||
globalTimeout: globalTimeout,
|
||||
skipWaitForDeleteTimeoutSeconds: d.SkipWaitForDeleteTimeoutSeconds,
|
||||
out: d.Out,
|
||||
}
|
||||
_, err := waitForDelete(params)
|
||||
return err
|
||||
}
|
||||
|
||||
func waitForDelete(ctx context.Context, pods []corev1.Pod, interval, timeout time.Duration, usingEviction bool, getPodFn func(string, string) (*corev1.Pod, error), onDoneFn func(pod *corev1.Pod, usingEviction bool), globalTimeout time.Duration) ([]corev1.Pod, error) {
|
||||
err := wait.PollImmediate(interval, timeout, func() (bool, error) {
|
||||
func waitForDelete(params waitForDeleteParams) ([]corev1.Pod, error) {
|
||||
pods := params.pods
|
||||
err := wait.PollImmediate(params.interval, params.timeout, func() (bool, error) {
|
||||
pendingPods := []corev1.Pod{}
|
||||
for i, pod := range pods {
|
||||
p, err := getPodFn(pod.Namespace, pod.Name)
|
||||
p, err := params.getPodFn(pod.Namespace, pod.Name)
|
||||
if apierrors.IsNotFound(err) || (p != nil && p.ObjectMeta.UID != pod.ObjectMeta.UID) {
|
||||
if onDoneFn != nil {
|
||||
onDoneFn(&pod, usingEviction)
|
||||
if params.onDoneFn != nil {
|
||||
params.onDoneFn(&pod, params.usingEviction)
|
||||
}
|
||||
continue
|
||||
} else if err != nil {
|
||||
return false, err
|
||||
} else {
|
||||
if shouldSkipPod(*p, params.skipWaitForDeleteTimeoutSeconds) {
|
||||
fmt.Fprintf(params.out, podSkipMsgTemplate, pod.Name, params.skipWaitForDeleteTimeoutSeconds)
|
||||
continue
|
||||
}
|
||||
pendingPods = append(pendingPods, pods[i])
|
||||
}
|
||||
}
|
||||
pods = pendingPods
|
||||
if len(pendingPods) > 0 {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return false, fmt.Errorf("global timeout reached: %v", globalTimeout)
|
||||
case <-params.ctx.Done():
|
||||
return false, fmt.Errorf("global timeout reached: %v", params.globalTimeout)
|
||||
default:
|
||||
return false, nil
|
||||
}
|
||||
|
|
|
@ -108,6 +108,23 @@ func TestDeletePods(t *testing.T) {
|
|||
return nil, 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 nil, fmt.Errorf("%q: not found", name)
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Client error could be passed out",
|
||||
interval: 200 * time.Millisecond,
|
||||
|
@ -124,13 +141,29 @@ func TestDeletePods(t *testing.T) {
|
|||
for _, test := range tests {
|
||||
t.Run(test.description, func(t *testing.T) {
|
||||
_, pods := createPods(false)
|
||||
ctx := context.Background()
|
||||
var ctx context.Context
|
||||
var cancel context.CancelFunc
|
||||
ctx = context.Background()
|
||||
if test.ctxTimeoutEarly {
|
||||
ctx, _ = context.WithTimeout(ctx, 100*time.Millisecond)
|
||||
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(ctx, pods, test.interval, test.timeout, false, test.getPodFn, nil, time.Duration(math.MaxInt64))
|
||||
pendingPods, err := waitForDelete(params)
|
||||
elapsed := time.Since(start)
|
||||
|
||||
if test.expectError {
|
||||
if err == nil {
|
||||
t.Fatalf("%s: unexpected non-error", test.description)
|
||||
|
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
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 drain
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
func TestSkipDeletedFilter(t *testing.T) {
|
||||
tCases := []struct {
|
||||
timeStampAgeSeconds int
|
||||
skipWaitForDeleteTimeoutSeconds int
|
||||
expectedDelete bool
|
||||
}{
|
||||
{
|
||||
timeStampAgeSeconds: 0,
|
||||
skipWaitForDeleteTimeoutSeconds: 20,
|
||||
expectedDelete: true,
|
||||
},
|
||||
{
|
||||
timeStampAgeSeconds: 1,
|
||||
skipWaitForDeleteTimeoutSeconds: 20,
|
||||
expectedDelete: true,
|
||||
},
|
||||
{
|
||||
timeStampAgeSeconds: 100,
|
||||
skipWaitForDeleteTimeoutSeconds: 20,
|
||||
expectedDelete: false,
|
||||
},
|
||||
}
|
||||
for i, tc := range tCases {
|
||||
h := &Helper{
|
||||
SkipWaitForDeleteTimeoutSeconds: tc.skipWaitForDeleteTimeoutSeconds,
|
||||
}
|
||||
pod := corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "pod",
|
||||
Namespace: "default",
|
||||
},
|
||||
}
|
||||
|
||||
if tc.timeStampAgeSeconds > 0 {
|
||||
dTime := &metav1.Time{Time: time.Now().Add(time.Duration(tc.timeStampAgeSeconds) * time.Second * -1)}
|
||||
pod.ObjectMeta.SetDeletionTimestamp(dTime)
|
||||
}
|
||||
|
||||
podDeleteStatus := h.skipDeletedFilter(pod)
|
||||
if podDeleteStatus.delete != tc.expectedDelete {
|
||||
t.Errorf("test %v: unexpected podDeleteStatus.delete; actual %v; expected %v", i, podDeleteStatus.delete, tc.expectedDelete)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -19,6 +19,7 @@ package drain
|
|||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
|
@ -133,8 +134,11 @@ func makePodDeleteStatusWithError(message string) podDeleteStatus {
|
|||
}
|
||||
}
|
||||
|
||||
// The filters are applied in a specific order, only the last filter's
|
||||
// message will be retained if there are any warnings.
|
||||
func (d *Helper) makeFilters() []podFilter {
|
||||
return []podFilter{
|
||||
d.skipDeletedFilter,
|
||||
d.daemonSetFilter,
|
||||
d.mirrorPodFilter,
|
||||
d.localStorageFilter,
|
||||
|
@ -203,6 +207,9 @@ func (d *Helper) localStorageFilter(pod corev1.Pod) podDeleteStatus {
|
|||
return makePodDeleteStatusWithError(localStorageFatal)
|
||||
}
|
||||
|
||||
// TODO: this warning gets dropped by subsequent filters;
|
||||
// consider accounting for multiple warning conditions or at least
|
||||
// preserving the last warning message.
|
||||
return makePodDeleteStatusWithWarning(true, localStorageWarning)
|
||||
}
|
||||
|
||||
|
@ -221,3 +228,16 @@ func (d *Helper) unreplicatedFilter(pod corev1.Pod) podDeleteStatus {
|
|||
}
|
||||
return makePodDeleteStatusWithError(unmanagedFatal)
|
||||
}
|
||||
|
||||
func shouldSkipPod(pod corev1.Pod, skipDeletedTimeoutSeconds int) bool {
|
||||
return skipDeletedTimeoutSeconds > 0 &&
|
||||
!pod.ObjectMeta.DeletionTimestamp.IsZero() &&
|
||||
int(time.Now().Sub(pod.ObjectMeta.GetDeletionTimestamp().Time).Seconds()) > skipDeletedTimeoutSeconds
|
||||
}
|
||||
|
||||
func (d *Helper) skipDeletedFilter(pod corev1.Pod) podDeleteStatus {
|
||||
if shouldSkipPod(pod, d.SkipWaitForDeleteTimeoutSeconds) {
|
||||
return makePodDeleteStatusSkip()
|
||||
}
|
||||
return makePodDeleteStatusOkay()
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue