Fix crash in kubectl drain

When there's a pod that can't be evicted/deleted, and apiserver
connection breaks at a wrong moment, kubectl can panic due
to nil pointer dereference. This PR also improves drain tests
to avoid confusing panics with fatal errors.
This commit is contained in:
Ivan Shvedunov 2019-10-11 22:12:26 +03:00
parent 77b86c4adf
commit 37226f8e83
2 changed files with 58 additions and 35 deletions

View File

@ -330,13 +330,17 @@ func (o *DrainCmdOptions) deleteOrEvictPodsSimple(nodeInfo *resource.Info) error
if err := o.drainer.DeleteOrEvictPods(list.Pods()); err != nil { if err := o.drainer.DeleteOrEvictPods(list.Pods()); err != nil {
pendingList, newErrs := o.drainer.GetPodsForDeletion(nodeInfo.Name) pendingList, newErrs := o.drainer.GetPodsForDeletion(nodeInfo.Name)
if pendingList != nil {
fmt.Fprintf(o.ErrOut, "There are pending pods in node %q when an error occurred: %v\n", nodeInfo.Name, err) pods := pendingList.Pods()
for _, pendingPod := range pendingList.Pods() { if len(pods) != 0 {
fmt.Fprintf(o.ErrOut, "%s/%s\n", "pod", pendingPod.Name) fmt.Fprintf(o.ErrOut, "There are pending pods in node %q when an error occurred: %v\n", nodeInfo.Name, err)
for _, pendingPod := range pods {
fmt.Fprintf(o.ErrOut, "%s/%s\n", "pod", pendingPod.Name)
}
}
} }
if newErrs != nil { if newErrs != nil {
fmt.Fprintf(o.ErrOut, "following errors also occurred:\n%s", utilerrors.NewAggregate(newErrs)) fmt.Fprintf(o.ErrOut, "Following errors occurred while getting the list of pods to delete:\n%s", utilerrors.NewAggregate(newErrs))
} }
return err return err
} }

View File

@ -17,6 +17,7 @@ limitations under the License.
package drain package drain
import ( import (
"errors"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"net/url" "net/url"
@ -214,11 +215,12 @@ func TestCordon(t *testing.T) {
ioStreams, _, _, _ := genericclioptions.NewTestIOStreams() ioStreams, _, _, _ := genericclioptions.NewTestIOStreams()
cmd := test.cmd(tf, ioStreams) cmd := test.cmd(tf, ioStreams)
var recovered interface{}
sawFatal := false sawFatal := false
func() { func() {
defer func() { defer func() {
// Recover from the panic below. // Recover from the panic below.
_ = recover() recovered = recover()
// Restore cmdutil behavior // Restore cmdutil behavior
cmdutil.DefaultBehaviorOnFatal() cmdutil.DefaultBehaviorOnFatal()
}() }()
@ -230,19 +232,19 @@ func TestCordon(t *testing.T) {
cmd.Execute() cmd.Execute()
}() }()
if test.expectFatal { switch {
case recovered != nil && !sawFatal:
t.Fatalf("got panic: %v", recovered)
case test.expectFatal:
if !sawFatal { if !sawFatal {
t.Fatalf("%s: unexpected non-error", test.description) t.Fatalf("%s: unexpected non-error", test.description)
} }
if updated { if updated {
t.Fatalf("%s: unexpected update", test.description) t.Fatalf("%s: unexpected update", test.description)
} }
} case !test.expectFatal && sawFatal:
if !test.expectFatal && sawFatal {
t.Fatalf("%s: unexpected error", test.description) t.Fatalf("%s: unexpected error", test.description)
} case !reflect.DeepEqual(test.expected.Spec, test.node.Spec) && !updated:
if !reflect.DeepEqual(test.expected.Spec, test.node.Spec) && !updated {
t.Fatalf("%s: node never updated", test.description) t.Fatalf("%s: node never updated", test.description)
} }
}) })
@ -534,16 +536,17 @@ func TestDrain(t *testing.T) {
} }
tests := []struct { tests := []struct {
description string description string
node *corev1.Node node *corev1.Node
expected *corev1.Node expected *corev1.Node
pods []corev1.Pod pods []corev1.Pod
rcs []corev1.ReplicationController rcs []corev1.ReplicationController
replicaSets []appsv1.ReplicaSet replicaSets []appsv1.ReplicaSet
args []string args []string
expectWarning string failUponEvictionOrDeletion bool
expectFatal bool expectWarning string
expectDelete bool expectFatal bool
expectDelete bool
}{ }{
{ {
description: "RC-managed pod", description: "RC-managed pod",
@ -705,6 +708,17 @@ func TestDrain(t *testing.T) {
expectFatal: false, expectFatal: false,
expectDelete: false, expectDelete: false,
}, },
{
description: "fail to list pods",
node: node,
expected: cordonedNode,
pods: []corev1.Pod{rsPod},
replicaSets: []appsv1.ReplicaSet{rs},
args: []string{"node"},
expectFatal: true,
expectDelete: true,
failUponEvictionOrDeletion: true,
},
} }
testEviction := false testEviction := false
@ -777,6 +791,9 @@ func TestDrain(t *testing.T) {
case m.isFor("GET", "/namespaces/default/pods/bar"): case m.isFor("GET", "/namespaces/default/pods/bar"):
return &http.Response{StatusCode: 404, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &corev1.Pod{})}, nil return &http.Response{StatusCode: 404, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &corev1.Pod{})}, nil
case m.isFor("GET", "/pods"): case m.isFor("GET", "/pods"):
if test.failUponEvictionOrDeletion && atomic.LoadInt32(&evictions) > 0 || atomic.LoadInt32(&deletions) > 0 {
return nil, errors.New("request failed")
}
values, err := url.ParseQuery(req.URL.RawQuery) values, err := url.ParseQuery(req.URL.RawQuery)
if err != nil { if err != nil {
t.Fatalf("%s: unexpected error: %v", test.description, err) t.Fatalf("%s: unexpected error: %v", test.description, err)
@ -812,10 +829,15 @@ func TestDrain(t *testing.T) {
return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, newNode)}, nil return &http.Response{StatusCode: 200, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, newNode)}, nil
case m.isFor("DELETE", "/namespaces/default/pods/bar"): case m.isFor("DELETE", "/namespaces/default/pods/bar"):
atomic.AddInt32(&deletions, 1) atomic.AddInt32(&deletions, 1)
if test.failUponEvictionOrDeletion {
return nil, errors.New("request failed")
}
return &http.Response{StatusCode: 204, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &test.pods[0])}, nil return &http.Response{StatusCode: 204, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &test.pods[0])}, nil
case m.isFor("POST", "/namespaces/default/pods/bar/eviction"): case m.isFor("POST", "/namespaces/default/pods/bar/eviction"):
atomic.AddInt32(&evictions, 1) atomic.AddInt32(&evictions, 1)
if test.failUponEvictionOrDeletion {
return nil, errors.New("request failed")
}
return &http.Response{StatusCode: 201, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &policyv1beta1.Eviction{})}, nil return &http.Response{StatusCode: 201, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &policyv1beta1.Eviction{})}, nil
default: default:
t.Fatalf("%s: unexpected request: %v %#v\n%#v", test.description, req.Method, req.URL, req) t.Fatalf("%s: unexpected request: %v %#v\n%#v", test.description, req.Method, req.URL, req)
@ -828,12 +850,13 @@ func TestDrain(t *testing.T) {
ioStreams, _, _, errBuf := genericclioptions.NewTestIOStreams() ioStreams, _, _, errBuf := genericclioptions.NewTestIOStreams()
cmd := NewCmdDrain(tf, ioStreams) cmd := NewCmdDrain(tf, ioStreams)
var recovered interface{}
sawFatal := false sawFatal := false
fatalMsg := "" fatalMsg := ""
func() { func() {
defer func() { defer func() {
// Recover from the panic below. // Recover from the panic below.
_ = recover() recovered = recover()
// Restore cmdutil behavior // Restore cmdutil behavior
cmdutil.DefaultBehaviorOnFatal() cmdutil.DefaultBehaviorOnFatal()
}() }()
@ -841,17 +864,13 @@ func TestDrain(t *testing.T) {
cmd.SetArgs(test.args) cmd.SetArgs(test.args)
cmd.Execute() cmd.Execute()
}() }()
if test.expectFatal { switch {
if !sawFatal { case recovered != nil && !sawFatal:
//t.Logf("outBuf = %s", outBuf.String()) t.Fatalf("got panic: %v", recovered)
//t.Logf("errBuf = %s", errBuf.String()) case test.expectFatal && !sawFatal:
t.Fatalf("%s: unexpected non-error when using %s", test.description, currMethod) t.Fatalf("%s: unexpected non-error when using %s", test.description, currMethod)
} case !test.expectFatal && sawFatal:
} else { t.Fatalf("%s: unexpected error when using %s: %s", test.description, currMethod, fatalMsg)
if sawFatal {
t.Fatalf("%s: unexpected error when using %s: %s", test.description, currMethod, fatalMsg)
}
} }
deleted := deletions > 0 deleted := deletions > 0