...

Source file src/k8s.io/kubectl/pkg/cmd/drain/drain_test.go

Documentation: k8s.io/kubectl/pkg/cmd/drain

     1  /*
     2  Copyright 2015 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package drain
    18  
    19  import (
    20  	"errors"
    21  	"io"
    22  	"net/http"
    23  	"net/url"
    24  	"os"
    25  	"reflect"
    26  	"strings"
    27  	"sync/atomic"
    28  	"testing"
    29  	"time"
    30  
    31  	"github.com/spf13/cobra"
    32  	appsv1 "k8s.io/api/apps/v1"
    33  	batchv1 "k8s.io/api/batch/v1"
    34  	corev1 "k8s.io/api/core/v1"
    35  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    36  	"k8s.io/apimachinery/pkg/runtime"
    37  	"k8s.io/apimachinery/pkg/runtime/schema"
    38  	"k8s.io/apimachinery/pkg/util/strategicpatch"
    39  	"k8s.io/cli-runtime/pkg/genericiooptions"
    40  	"k8s.io/client-go/rest/fake"
    41  	cmdtesting "k8s.io/kubectl/pkg/cmd/testing"
    42  	cmdutil "k8s.io/kubectl/pkg/cmd/util"
    43  	"k8s.io/kubectl/pkg/drain"
    44  	"k8s.io/kubectl/pkg/scheme"
    45  	utilpointer "k8s.io/utils/pointer"
    46  )
    47  
    48  const (
    49  	EvictionMethod = "Eviction"
    50  	DeleteMethod   = "Delete"
    51  )
    52  
    53  var node *corev1.Node
    54  var cordonedNode *corev1.Node
    55  
    56  func TestMain(m *testing.M) {
    57  	// Create a node.
    58  	node = &corev1.Node{
    59  		ObjectMeta: metav1.ObjectMeta{
    60  			Name:              "node",
    61  			CreationTimestamp: metav1.Time{Time: time.Now()},
    62  		},
    63  		Status: corev1.NodeStatus{},
    64  	}
    65  
    66  	// A copy of the same node, but cordoned.
    67  	cordonedNode = node.DeepCopy()
    68  	cordonedNode.Spec.Unschedulable = true
    69  	os.Exit(m.Run())
    70  }
    71  
    72  func TestCordon(t *testing.T) {
    73  	tests := []struct {
    74  		description string
    75  		node        *corev1.Node
    76  		expected    *corev1.Node
    77  		cmd         func(cmdutil.Factory, genericiooptions.IOStreams) *cobra.Command
    78  		arg         string
    79  		expectFatal bool
    80  	}{
    81  		{
    82  			description: "node/node syntax",
    83  			node:        cordonedNode,
    84  			expected:    node,
    85  			cmd:         NewCmdUncordon,
    86  			arg:         "node/node",
    87  			expectFatal: false,
    88  		},
    89  		{
    90  			description: "uncordon for real",
    91  			node:        cordonedNode,
    92  			expected:    node,
    93  			cmd:         NewCmdUncordon,
    94  			arg:         "node",
    95  			expectFatal: false,
    96  		},
    97  		{
    98  			description: "uncordon does nothing",
    99  			node:        node,
   100  			expected:    node,
   101  			cmd:         NewCmdUncordon,
   102  			arg:         "node",
   103  			expectFatal: false,
   104  		},
   105  		{
   106  			description: "cordon does nothing",
   107  			node:        cordonedNode,
   108  			expected:    cordonedNode,
   109  			cmd:         NewCmdCordon,
   110  			arg:         "node",
   111  			expectFatal: false,
   112  		},
   113  		{
   114  			description: "cordon for real",
   115  			node:        node,
   116  			expected:    cordonedNode,
   117  			cmd:         NewCmdCordon,
   118  			arg:         "node",
   119  			expectFatal: false,
   120  		},
   121  		{
   122  			description: "cordon missing node",
   123  			node:        node,
   124  			expected:    node,
   125  			cmd:         NewCmdCordon,
   126  			arg:         "bar",
   127  			expectFatal: true,
   128  		},
   129  		{
   130  			description: "uncordon missing node",
   131  			node:        node,
   132  			expected:    node,
   133  			cmd:         NewCmdUncordon,
   134  			arg:         "bar",
   135  			expectFatal: true,
   136  		},
   137  		{
   138  			description: "cordon for multiple nodes",
   139  			node:        node,
   140  			expected:    cordonedNode,
   141  			cmd:         NewCmdCordon,
   142  			arg:         "node node1 node2",
   143  			expectFatal: false,
   144  		},
   145  		{
   146  			description: "uncordon for multiple nodes",
   147  			node:        cordonedNode,
   148  			expected:    node,
   149  			cmd:         NewCmdUncordon,
   150  			arg:         "node node1 node2",
   151  			expectFatal: false,
   152  		},
   153  	}
   154  
   155  	for _, test := range tests {
   156  		t.Run(test.description, func(t *testing.T) {
   157  			tf := cmdtesting.NewTestFactory()
   158  			defer tf.Cleanup()
   159  
   160  			codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...)
   161  			ns := scheme.Codecs.WithoutConversion()
   162  
   163  			newNode := &corev1.Node{}
   164  			updated := false
   165  			tf.Client = &fake.RESTClient{
   166  				GroupVersion:         schema.GroupVersion{Group: "", Version: "v1"},
   167  				NegotiatedSerializer: ns,
   168  				Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
   169  					m := &MyReq{req}
   170  					switch {
   171  					case m.isFor("GET", "/nodes/node1"):
   172  						fallthrough
   173  					case m.isFor("GET", "/nodes/node2"):
   174  						fallthrough
   175  					case m.isFor("GET", "/nodes/node"):
   176  						return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, test.node)}, nil
   177  					case m.isFor("GET", "/nodes/bar"):
   178  						return &http.Response{StatusCode: http.StatusNotFound, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.StringBody("nope")}, nil
   179  					case m.isFor("PATCH", "/nodes/node1"):
   180  						fallthrough
   181  					case m.isFor("PATCH", "/nodes/node2"):
   182  						fallthrough
   183  					case m.isFor("PATCH", "/nodes/node"):
   184  						data, err := io.ReadAll(req.Body)
   185  						if err != nil {
   186  							t.Fatalf("%s: unexpected error: %v", test.description, err)
   187  						}
   188  						defer req.Body.Close()
   189  						oldJSON, err := runtime.Encode(codec, node)
   190  						if err != nil {
   191  							t.Fatalf("%s: unexpected error: %v", test.description, err)
   192  						}
   193  						appliedPatch, err := strategicpatch.StrategicMergePatch(oldJSON, data, &corev1.Node{})
   194  						if err != nil {
   195  							t.Fatalf("%s: unexpected error: %v", test.description, err)
   196  						}
   197  						if err := runtime.DecodeInto(codec, appliedPatch, newNode); err != nil {
   198  							t.Fatalf("%s: unexpected error: %v", test.description, err)
   199  						}
   200  						if !reflect.DeepEqual(test.expected.Spec, newNode.Spec) {
   201  							t.Fatalf("%s: expected:\n%v\nsaw:\n%v\n", test.description, test.expected.Spec.Unschedulable, newNode.Spec.Unschedulable)
   202  						}
   203  						updated = true
   204  						return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, newNode)}, nil
   205  					default:
   206  						t.Fatalf("%s: unexpected request: %v %#v\n%#v", test.description, req.Method, req.URL, req)
   207  						return nil, nil
   208  					}
   209  				}),
   210  			}
   211  			tf.ClientConfigVal = cmdtesting.DefaultClientConfig()
   212  
   213  			ioStreams, _, _, _ := genericiooptions.NewTestIOStreams()
   214  			cmd := test.cmd(tf, ioStreams)
   215  
   216  			var recovered interface{}
   217  			sawFatal := false
   218  			func() {
   219  				defer func() {
   220  					// Recover from the panic below.
   221  					recovered = recover()
   222  					// Restore cmdutil behavior
   223  					cmdutil.DefaultBehaviorOnFatal()
   224  				}()
   225  				cmdutil.BehaviorOnFatal(func(e string, code int) {
   226  					sawFatal = true
   227  					panic(e)
   228  				})
   229  				cmd.SetArgs(strings.Split(test.arg, " "))
   230  				cmd.Execute()
   231  			}()
   232  
   233  			switch {
   234  			case recovered != nil && !sawFatal:
   235  				t.Fatalf("got panic: %v", recovered)
   236  			case test.expectFatal:
   237  				if !sawFatal {
   238  					t.Fatalf("%s: unexpected non-error", test.description)
   239  				}
   240  				if updated {
   241  					t.Fatalf("%s: unexpected update", test.description)
   242  				}
   243  			case !test.expectFatal && sawFatal:
   244  				t.Fatalf("%s: unexpected error", test.description)
   245  			case !reflect.DeepEqual(test.expected.Spec, test.node.Spec) && !updated:
   246  				t.Fatalf("%s: node never updated", test.description)
   247  			}
   248  		})
   249  	}
   250  }
   251  
   252  func TestDrain(t *testing.T) {
   253  	labels := make(map[string]string)
   254  	labels["my_key"] = "my_value"
   255  
   256  	rc := corev1.ReplicationController{
   257  		ObjectMeta: metav1.ObjectMeta{
   258  			Name:              "rc",
   259  			Namespace:         "default",
   260  			CreationTimestamp: metav1.Time{Time: time.Now()},
   261  			Labels:            labels,
   262  		},
   263  		Spec: corev1.ReplicationControllerSpec{
   264  			Selector: labels,
   265  		},
   266  	}
   267  
   268  	rcPod := corev1.Pod{
   269  		ObjectMeta: metav1.ObjectMeta{
   270  			Name:              "bar",
   271  			Namespace:         "default",
   272  			CreationTimestamp: metav1.Time{Time: time.Now()},
   273  			Labels:            labels,
   274  			OwnerReferences: []metav1.OwnerReference{
   275  				{
   276  					APIVersion:         "v1",
   277  					Kind:               "ReplicationController",
   278  					Name:               "rc",
   279  					UID:                "123",
   280  					BlockOwnerDeletion: utilpointer.BoolPtr(true),
   281  					Controller:         utilpointer.BoolPtr(true),
   282  				},
   283  			},
   284  		},
   285  		Spec: corev1.PodSpec{
   286  			NodeName: "node",
   287  		},
   288  	}
   289  
   290  	ds := appsv1.DaemonSet{
   291  		ObjectMeta: metav1.ObjectMeta{
   292  			Name:              "ds",
   293  			Namespace:         "default",
   294  			CreationTimestamp: metav1.Time{Time: time.Now()},
   295  		},
   296  		Spec: appsv1.DaemonSetSpec{
   297  			Selector: &metav1.LabelSelector{MatchLabels: labels},
   298  		},
   299  	}
   300  
   301  	dsPod := corev1.Pod{
   302  		ObjectMeta: metav1.ObjectMeta{
   303  			Name:              "bar",
   304  			Namespace:         "default",
   305  			CreationTimestamp: metav1.Time{Time: time.Now()},
   306  			Labels:            labels,
   307  			OwnerReferences: []metav1.OwnerReference{
   308  				{
   309  					APIVersion:         "apps/v1",
   310  					Kind:               "DaemonSet",
   311  					Name:               "ds",
   312  					BlockOwnerDeletion: utilpointer.BoolPtr(true),
   313  					Controller:         utilpointer.BoolPtr(true),
   314  				},
   315  			},
   316  		},
   317  		Spec: corev1.PodSpec{
   318  			NodeName: "node",
   319  		},
   320  	}
   321  
   322  	dsTerminatedPod := corev1.Pod{
   323  		ObjectMeta: metav1.ObjectMeta{
   324  			Name:              "bar",
   325  			Namespace:         "default",
   326  			CreationTimestamp: metav1.Time{Time: time.Now()},
   327  			Labels:            labels,
   328  			OwnerReferences: []metav1.OwnerReference{
   329  				{
   330  					APIVersion:         "apps/v1",
   331  					Kind:               "DaemonSet",
   332  					Name:               "ds",
   333  					BlockOwnerDeletion: utilpointer.BoolPtr(true),
   334  					Controller:         utilpointer.BoolPtr(true),
   335  				},
   336  			},
   337  		},
   338  		Spec: corev1.PodSpec{
   339  			NodeName: "node",
   340  		},
   341  		Status: corev1.PodStatus{
   342  			Phase: corev1.PodSucceeded,
   343  		},
   344  	}
   345  
   346  	dsPodWithEmptyDir := corev1.Pod{
   347  		ObjectMeta: metav1.ObjectMeta{
   348  			Name:              "bar",
   349  			Namespace:         "default",
   350  			CreationTimestamp: metav1.Time{Time: time.Now()},
   351  			Labels:            labels,
   352  			OwnerReferences: []metav1.OwnerReference{
   353  				{
   354  					APIVersion:         "apps/v1",
   355  					Kind:               "DaemonSet",
   356  					Name:               "ds",
   357  					BlockOwnerDeletion: utilpointer.BoolPtr(true),
   358  					Controller:         utilpointer.BoolPtr(true),
   359  				},
   360  			},
   361  		},
   362  		Spec: corev1.PodSpec{
   363  			NodeName: "node",
   364  			Volumes: []corev1.Volume{
   365  				{
   366  					Name:         "scratch",
   367  					VolumeSource: corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{Medium: ""}},
   368  				},
   369  			},
   370  		},
   371  	}
   372  
   373  	orphanedDsPod := corev1.Pod{
   374  		ObjectMeta: metav1.ObjectMeta{
   375  			Name:              "bar",
   376  			Namespace:         "default",
   377  			CreationTimestamp: metav1.Time{Time: time.Now()},
   378  			Labels:            labels,
   379  		},
   380  		Spec: corev1.PodSpec{
   381  			NodeName: "node",
   382  		},
   383  	}
   384  
   385  	job := batchv1.Job{
   386  		ObjectMeta: metav1.ObjectMeta{
   387  			Name:              "job",
   388  			Namespace:         "default",
   389  			CreationTimestamp: metav1.Time{Time: time.Now()},
   390  		},
   391  		Spec: batchv1.JobSpec{
   392  			Selector: &metav1.LabelSelector{MatchLabels: labels},
   393  		},
   394  	}
   395  
   396  	jobPod := corev1.Pod{
   397  		ObjectMeta: metav1.ObjectMeta{
   398  			Name:              "bar",
   399  			Namespace:         "default",
   400  			CreationTimestamp: metav1.Time{Time: time.Now()},
   401  			Labels:            labels,
   402  			OwnerReferences: []metav1.OwnerReference{
   403  				{
   404  					APIVersion:         "v1",
   405  					Kind:               "Job",
   406  					Name:               "job",
   407  					BlockOwnerDeletion: utilpointer.BoolPtr(true),
   408  					Controller:         utilpointer.BoolPtr(true),
   409  				},
   410  			},
   411  		},
   412  		Spec: corev1.PodSpec{
   413  			NodeName: "node",
   414  			Volumes: []corev1.Volume{
   415  				{
   416  					Name:         "scratch",
   417  					VolumeSource: corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{Medium: ""}},
   418  				},
   419  			},
   420  		},
   421  	}
   422  
   423  	terminatedJobPodWithLocalStorage := corev1.Pod{
   424  		ObjectMeta: metav1.ObjectMeta{
   425  			Name:              "bar",
   426  			Namespace:         "default",
   427  			CreationTimestamp: metav1.Time{Time: time.Now()},
   428  			Labels:            labels,
   429  			OwnerReferences: []metav1.OwnerReference{
   430  				{
   431  					APIVersion:         "v1",
   432  					Kind:               "Job",
   433  					Name:               "job",
   434  					BlockOwnerDeletion: utilpointer.BoolPtr(true),
   435  					Controller:         utilpointer.BoolPtr(true),
   436  				},
   437  			},
   438  		},
   439  		Spec: corev1.PodSpec{
   440  			NodeName: "node",
   441  			Volumes: []corev1.Volume{
   442  				{
   443  					Name:         "scratch",
   444  					VolumeSource: corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{Medium: ""}},
   445  				},
   446  			},
   447  		},
   448  		Status: corev1.PodStatus{
   449  			Phase: corev1.PodSucceeded,
   450  		},
   451  	}
   452  
   453  	rs := appsv1.ReplicaSet{
   454  		ObjectMeta: metav1.ObjectMeta{
   455  			Name:              "rs",
   456  			Namespace:         "default",
   457  			CreationTimestamp: metav1.Time{Time: time.Now()},
   458  			Labels:            labels,
   459  		},
   460  		Spec: appsv1.ReplicaSetSpec{
   461  			Selector: &metav1.LabelSelector{MatchLabels: labels},
   462  		},
   463  	}
   464  
   465  	rsPod := corev1.Pod{
   466  		ObjectMeta: metav1.ObjectMeta{
   467  			Name:              "bar",
   468  			Namespace:         "default",
   469  			CreationTimestamp: metav1.Time{Time: time.Now()},
   470  			Labels:            labels,
   471  			OwnerReferences: []metav1.OwnerReference{
   472  				{
   473  					APIVersion:         "v1",
   474  					Kind:               "ReplicaSet",
   475  					Name:               "rs",
   476  					BlockOwnerDeletion: utilpointer.BoolPtr(true),
   477  					Controller:         utilpointer.BoolPtr(true),
   478  				},
   479  			},
   480  		},
   481  		Spec: corev1.PodSpec{
   482  			NodeName: "node",
   483  		},
   484  	}
   485  
   486  	nakedPod := corev1.Pod{
   487  		ObjectMeta: metav1.ObjectMeta{
   488  			Name:              "bar",
   489  			Namespace:         "default",
   490  			CreationTimestamp: metav1.Time{Time: time.Now()},
   491  			Labels:            labels,
   492  		},
   493  		Spec: corev1.PodSpec{
   494  			NodeName: "node",
   495  		},
   496  	}
   497  
   498  	emptydirPod := corev1.Pod{
   499  		ObjectMeta: metav1.ObjectMeta{
   500  			Name:              "bar",
   501  			Namespace:         "default",
   502  			CreationTimestamp: metav1.Time{Time: time.Now()},
   503  			Labels:            labels,
   504  		},
   505  		Spec: corev1.PodSpec{
   506  			NodeName: "node",
   507  			Volumes: []corev1.Volume{
   508  				{
   509  					Name:         "scratch",
   510  					VolumeSource: corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{Medium: ""}},
   511  				},
   512  			},
   513  		},
   514  	}
   515  	emptydirTerminatedPod := corev1.Pod{
   516  		ObjectMeta: metav1.ObjectMeta{
   517  			Name:              "bar",
   518  			Namespace:         "default",
   519  			CreationTimestamp: metav1.Time{Time: time.Now()},
   520  			Labels:            labels,
   521  		},
   522  		Spec: corev1.PodSpec{
   523  			NodeName: "node",
   524  			Volumes: []corev1.Volume{
   525  				{
   526  					Name:         "scratch",
   527  					VolumeSource: corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{Medium: ""}},
   528  				},
   529  			},
   530  		},
   531  		Status: corev1.PodStatus{
   532  			Phase: corev1.PodFailed,
   533  		},
   534  	}
   535  
   536  	tests := []struct {
   537  		description                string
   538  		node                       *corev1.Node
   539  		expected                   *corev1.Node
   540  		pods                       []corev1.Pod
   541  		rcs                        []corev1.ReplicationController
   542  		replicaSets                []appsv1.ReplicaSet
   543  		args                       []string
   544  		failUponEvictionOrDeletion bool
   545  		expectWarning              string
   546  		expectFatal                bool
   547  		expectDelete               bool
   548  		expectOutputToContain      string
   549  	}{
   550  		{
   551  			description:           "RC-managed pod",
   552  			node:                  node,
   553  			expected:              cordonedNode,
   554  			pods:                  []corev1.Pod{rcPod},
   555  			rcs:                   []corev1.ReplicationController{rc},
   556  			args:                  []string{"node"},
   557  			expectFatal:           false,
   558  			expectDelete:          true,
   559  			expectOutputToContain: "node/node drained",
   560  		},
   561  		{
   562  			description:  "DS-managed pod",
   563  			node:         node,
   564  			expected:     cordonedNode,
   565  			pods:         []corev1.Pod{dsPod},
   566  			rcs:          []corev1.ReplicationController{rc},
   567  			args:         []string{"node"},
   568  			expectFatal:  true,
   569  			expectDelete: false,
   570  		},
   571  		{
   572  			description:           "DS-managed terminated pod",
   573  			node:                  node,
   574  			expected:              cordonedNode,
   575  			pods:                  []corev1.Pod{dsTerminatedPod},
   576  			rcs:                   []corev1.ReplicationController{rc},
   577  			args:                  []string{"node"},
   578  			expectFatal:           false,
   579  			expectDelete:          true,
   580  			expectOutputToContain: "node/node drained",
   581  		},
   582  		{
   583  			description:  "orphaned DS-managed pod",
   584  			node:         node,
   585  			expected:     cordonedNode,
   586  			pods:         []corev1.Pod{orphanedDsPod},
   587  			rcs:          []corev1.ReplicationController{},
   588  			args:         []string{"node"},
   589  			expectFatal:  true,
   590  			expectDelete: false,
   591  		},
   592  		{
   593  			description:           "orphaned DS-managed pod with --force",
   594  			node:                  node,
   595  			expected:              cordonedNode,
   596  			pods:                  []corev1.Pod{orphanedDsPod},
   597  			rcs:                   []corev1.ReplicationController{},
   598  			args:                  []string{"node", "--force"},
   599  			expectFatal:           false,
   600  			expectDelete:          true,
   601  			expectWarning:         "Warning: deleting Pods that declare no controller: default/bar",
   602  			expectOutputToContain: "node/node drained",
   603  		},
   604  		{
   605  			description:           "DS-managed pod with --ignore-daemonsets",
   606  			node:                  node,
   607  			expected:              cordonedNode,
   608  			pods:                  []corev1.Pod{dsPod},
   609  			rcs:                   []corev1.ReplicationController{rc},
   610  			args:                  []string{"node", "--ignore-daemonsets"},
   611  			expectFatal:           false,
   612  			expectDelete:          false,
   613  			expectOutputToContain: "node/node drained",
   614  		},
   615  		{
   616  			description:           "DS-managed pod with emptyDir with --ignore-daemonsets",
   617  			node:                  node,
   618  			expected:              cordonedNode,
   619  			pods:                  []corev1.Pod{dsPodWithEmptyDir},
   620  			rcs:                   []corev1.ReplicationController{rc},
   621  			args:                  []string{"node", "--ignore-daemonsets"},
   622  			expectWarning:         "Warning: ignoring DaemonSet-managed Pods: default/bar",
   623  			expectFatal:           false,
   624  			expectDelete:          false,
   625  			expectOutputToContain: "node/node drained",
   626  		},
   627  		{
   628  			description:           "Job-managed pod with local storage",
   629  			node:                  node,
   630  			expected:              cordonedNode,
   631  			pods:                  []corev1.Pod{jobPod},
   632  			rcs:                   []corev1.ReplicationController{rc},
   633  			args:                  []string{"node", "--force", "--delete-emptydir-data=true"},
   634  			expectFatal:           false,
   635  			expectDelete:          true,
   636  			expectOutputToContain: "node/node drained",
   637  		},
   638  		{
   639  			description:           "Ensure compatibility for --delete-local-data until fully deprecated",
   640  			node:                  node,
   641  			expected:              cordonedNode,
   642  			pods:                  []corev1.Pod{jobPod},
   643  			rcs:                   []corev1.ReplicationController{rc},
   644  			args:                  []string{"node", "--force", "--delete-local-data=true"},
   645  			expectFatal:           false,
   646  			expectDelete:          true,
   647  			expectOutputToContain: "node/node drained",
   648  		},
   649  		{
   650  			description:           "Job-managed terminated pod",
   651  			node:                  node,
   652  			expected:              cordonedNode,
   653  			pods:                  []corev1.Pod{terminatedJobPodWithLocalStorage},
   654  			rcs:                   []corev1.ReplicationController{rc},
   655  			args:                  []string{"node"},
   656  			expectFatal:           false,
   657  			expectDelete:          true,
   658  			expectOutputToContain: "node/node drained",
   659  		},
   660  		{
   661  			description:           "RS-managed pod",
   662  			node:                  node,
   663  			expected:              cordonedNode,
   664  			pods:                  []corev1.Pod{rsPod},
   665  			replicaSets:           []appsv1.ReplicaSet{rs},
   666  			args:                  []string{"node"},
   667  			expectFatal:           false,
   668  			expectDelete:          true,
   669  			expectOutputToContain: "node/node drained",
   670  		},
   671  		{
   672  			description:  "naked pod",
   673  			node:         node,
   674  			expected:     cordonedNode,
   675  			pods:         []corev1.Pod{nakedPod},
   676  			rcs:          []corev1.ReplicationController{},
   677  			args:         []string{"node"},
   678  			expectFatal:  true,
   679  			expectDelete: false,
   680  		},
   681  		{
   682  			description:           "naked pod with --force",
   683  			node:                  node,
   684  			expected:              cordonedNode,
   685  			pods:                  []corev1.Pod{nakedPod},
   686  			rcs:                   []corev1.ReplicationController{},
   687  			args:                  []string{"node", "--force"},
   688  			expectFatal:           false,
   689  			expectDelete:          true,
   690  			expectOutputToContain: "node/node drained",
   691  		},
   692  		{
   693  			description:  "pod with EmptyDir",
   694  			node:         node,
   695  			expected:     cordonedNode,
   696  			pods:         []corev1.Pod{emptydirPod},
   697  			args:         []string{"node", "--force"},
   698  			expectFatal:  true,
   699  			expectDelete: false,
   700  		},
   701  		{
   702  			description:           "terminated pod with emptyDir",
   703  			node:                  node,
   704  			expected:              cordonedNode,
   705  			pods:                  []corev1.Pod{emptydirTerminatedPod},
   706  			rcs:                   []corev1.ReplicationController{rc},
   707  			args:                  []string{"node"},
   708  			expectFatal:           false,
   709  			expectDelete:          true,
   710  			expectOutputToContain: "node/node drained",
   711  		},
   712  		{
   713  			description:           "pod with EmptyDir and --delete-emptydir-data",
   714  			node:                  node,
   715  			expected:              cordonedNode,
   716  			pods:                  []corev1.Pod{emptydirPod},
   717  			args:                  []string{"node", "--force", "--delete-emptydir-data=true"},
   718  			expectFatal:           false,
   719  			expectDelete:          true,
   720  			expectOutputToContain: "node/node drained",
   721  		},
   722  		{
   723  			description:           "empty node",
   724  			node:                  node,
   725  			expected:              cordonedNode,
   726  			pods:                  []corev1.Pod{},
   727  			rcs:                   []corev1.ReplicationController{rc},
   728  			args:                  []string{"node"},
   729  			expectFatal:           false,
   730  			expectDelete:          false,
   731  			expectOutputToContain: "node/node drained",
   732  		},
   733  		{
   734  			description:                "fail to list pods",
   735  			node:                       node,
   736  			expected:                   cordonedNode,
   737  			pods:                       []corev1.Pod{rsPod},
   738  			replicaSets:                []appsv1.ReplicaSet{rs},
   739  			args:                       []string{"node"},
   740  			expectFatal:                true,
   741  			expectDelete:               true,
   742  			failUponEvictionOrDeletion: true,
   743  		},
   744  	}
   745  
   746  	testEviction := false
   747  	for i := 0; i < 2; i++ {
   748  		testEviction = !testEviction
   749  		var currMethod string
   750  		if testEviction {
   751  			currMethod = EvictionMethod
   752  		} else {
   753  			currMethod = DeleteMethod
   754  		}
   755  		for _, test := range tests {
   756  			t.Run(test.description, func(t *testing.T) {
   757  				newNode := &corev1.Node{}
   758  				var deletions, evictions int32
   759  				tf := cmdtesting.NewTestFactory()
   760  				defer tf.Cleanup()
   761  
   762  				codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...)
   763  				ns := scheme.Codecs.WithoutConversion()
   764  
   765  				tf.Client = &fake.RESTClient{
   766  					GroupVersion:         schema.GroupVersion{Group: "", Version: "v1"},
   767  					NegotiatedSerializer: ns,
   768  					Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
   769  						m := &MyReq{req}
   770  						switch {
   771  						case req.Method == "GET" && req.URL.Path == "/api":
   772  							apiVersions := metav1.APIVersions{
   773  								Versions: []string{"v1"},
   774  							}
   775  							return cmdtesting.GenResponseWithJsonEncodedBody(apiVersions)
   776  						case req.Method == "GET" && req.URL.Path == "/apis":
   777  							groupList := metav1.APIGroupList{
   778  								Groups: []metav1.APIGroup{
   779  									{
   780  										Name: "policy",
   781  										PreferredVersion: metav1.GroupVersionForDiscovery{
   782  											GroupVersion: "policy/v1",
   783  										},
   784  									},
   785  								},
   786  							}
   787  							return cmdtesting.GenResponseWithJsonEncodedBody(groupList)
   788  						case req.Method == "GET" && req.URL.Path == "/api/v1":
   789  							resourceList := metav1.APIResourceList{
   790  								GroupVersion: "v1",
   791  							}
   792  							if testEviction {
   793  								resourceList.APIResources = []metav1.APIResource{
   794  									{
   795  										Name:    drain.EvictionSubresource,
   796  										Kind:    drain.EvictionKind,
   797  										Group:   "policy",
   798  										Version: "v1",
   799  									},
   800  								}
   801  							}
   802  							return cmdtesting.GenResponseWithJsonEncodedBody(resourceList)
   803  						case m.isFor("GET", "/nodes/node"):
   804  							return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, test.node)}, nil
   805  						case m.isFor("GET", "/namespaces/default/replicationcontrollers/rc"):
   806  							return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &test.rcs[0])}, nil
   807  						case m.isFor("GET", "/namespaces/default/daemonsets/ds"):
   808  							return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &ds)}, nil
   809  						case m.isFor("GET", "/namespaces/default/daemonsets/missing-ds"):
   810  							return &http.Response{StatusCode: http.StatusNotFound, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &appsv1.DaemonSet{})}, nil
   811  						case m.isFor("GET", "/namespaces/default/jobs/job"):
   812  							return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &job)}, nil
   813  						case m.isFor("GET", "/namespaces/default/replicasets/rs"):
   814  							return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &test.replicaSets[0])}, nil
   815  						case m.isFor("GET", "/namespaces/default/pods/bar"):
   816  							return &http.Response{StatusCode: http.StatusNotFound, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &corev1.Pod{})}, nil
   817  						case m.isFor("GET", "/pods"):
   818  							if test.failUponEvictionOrDeletion && atomic.LoadInt32(&evictions) > 0 || atomic.LoadInt32(&deletions) > 0 {
   819  								return nil, errors.New("request failed")
   820  							}
   821  							values, err := url.ParseQuery(req.URL.RawQuery)
   822  							if err != nil {
   823  								t.Fatalf("%s: unexpected error: %v", test.description, err)
   824  							}
   825  							getParams := make(url.Values)
   826  							getParams["fieldSelector"] = []string{"spec.nodeName=node"}
   827  							getParams["limit"] = []string{"500"}
   828  							if !reflect.DeepEqual(getParams, values) {
   829  								t.Fatalf("%s: expected:\n%v\nsaw:\n%v\n", test.description, getParams, values)
   830  							}
   831  							return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &corev1.PodList{Items: test.pods})}, nil
   832  						case m.isFor("GET", "/replicationcontrollers"):
   833  							return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &corev1.ReplicationControllerList{Items: test.rcs})}, nil
   834  						case m.isFor("PATCH", "/nodes/node"):
   835  							data, err := io.ReadAll(req.Body)
   836  							if err != nil {
   837  								t.Fatalf("%s: unexpected error: %v", test.description, err)
   838  							}
   839  							defer req.Body.Close()
   840  							oldJSON, err := runtime.Encode(codec, node)
   841  							if err != nil {
   842  								t.Fatalf("%s: unexpected error: %v", test.description, err)
   843  							}
   844  							appliedPatch, err := strategicpatch.StrategicMergePatch(oldJSON, data, &corev1.Node{})
   845  							if err != nil {
   846  								t.Fatalf("%s: unexpected error: %v", test.description, err)
   847  							}
   848  							if err := runtime.DecodeInto(codec, appliedPatch, newNode); err != nil {
   849  								t.Fatalf("%s: unexpected error: %v", test.description, err)
   850  							}
   851  							if !reflect.DeepEqual(test.expected.Spec, newNode.Spec) {
   852  								t.Fatalf("%s: expected:\n%v\nsaw:\n%v\n", test.description, test.expected.Spec, newNode.Spec)
   853  							}
   854  							return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, newNode)}, nil
   855  						case m.isFor("DELETE", "/namespaces/default/pods/bar"):
   856  							atomic.AddInt32(&deletions, 1)
   857  							if test.failUponEvictionOrDeletion {
   858  								return nil, errors.New("request failed")
   859  							}
   860  							return &http.Response{StatusCode: http.StatusNoContent, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &test.pods[0])}, nil
   861  						case m.isFor("POST", "/namespaces/default/pods/bar/eviction"):
   862  							atomic.AddInt32(&evictions, 1)
   863  							if test.failUponEvictionOrDeletion {
   864  								return nil, errors.New("request failed")
   865  							}
   866  							return &http.Response{StatusCode: http.StatusCreated, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &metav1.Status{})}, nil
   867  						default:
   868  							t.Fatalf("%s: unexpected request: %v %#v\n%#v", test.description, req.Method, req.URL, req)
   869  							return nil, nil
   870  						}
   871  					}),
   872  				}
   873  				tf.ClientConfigVal = cmdtesting.DefaultClientConfig()
   874  
   875  				ioStreams, _, outBuf, errBuf := genericiooptions.NewTestIOStreams()
   876  				cmd := NewCmdDrain(tf, ioStreams)
   877  
   878  				var recovered interface{}
   879  				sawFatal := false
   880  				fatalMsg := ""
   881  				func() {
   882  					defer func() {
   883  						// Recover from the panic below.
   884  						recovered = recover()
   885  						// Restore cmdutil behavior
   886  						cmdutil.DefaultBehaviorOnFatal()
   887  					}()
   888  					cmdutil.BehaviorOnFatal(func(e string, code int) { sawFatal = true; fatalMsg = e; panic(e) })
   889  					cmd.SetArgs(test.args)
   890  					cmd.Execute()
   891  				}()
   892  				switch {
   893  				case recovered != nil && !sawFatal:
   894  					t.Fatalf("got panic: %v", recovered)
   895  				case test.expectFatal && !sawFatal:
   896  					t.Fatalf("%s: unexpected non-error when using %s", test.description, currMethod)
   897  				case !test.expectFatal && sawFatal:
   898  					t.Fatalf("%s: unexpected error when using %s: %s", test.description, currMethod, fatalMsg)
   899  				}
   900  
   901  				deleted := deletions > 0
   902  				evicted := evictions > 0
   903  
   904  				if test.expectDelete {
   905  					// Test Delete
   906  					if !testEviction && !deleted {
   907  						t.Fatalf("%s: pod never deleted", test.description)
   908  					}
   909  					// Test Eviction
   910  					if testEviction {
   911  						if !evicted {
   912  							t.Fatalf("%s: pod never evicted", test.description)
   913  						}
   914  						if evictions > 1 {
   915  							t.Fatalf("%s: asked to evict same pod %d too many times", test.description, evictions-1)
   916  						}
   917  					}
   918  				}
   919  				if !test.expectDelete {
   920  					if deleted {
   921  						t.Fatalf("%s: unexpected delete when using %s", test.description, currMethod)
   922  					}
   923  					if deletions > 1 {
   924  						t.Fatalf("%s: asked to deleted same pod %d too many times", test.description, deletions-1)
   925  					}
   926  				}
   927  				if deleted && evicted {
   928  					t.Fatalf("%s: same pod deleted %d times and evicted %d times", test.description, deletions, evictions)
   929  				}
   930  
   931  				if len(test.expectWarning) > 0 {
   932  					if len(errBuf.String()) == 0 {
   933  						t.Fatalf("%s: expected warning, but found no stderr output", test.description)
   934  					}
   935  
   936  					// Mac and Bazel on Linux behave differently when returning newlines
   937  					if a, e := errBuf.String(), test.expectWarning; !strings.Contains(a, e) {
   938  						t.Fatalf("%s: actual warning message did not match expected warning message.\n Expecting:\n%v\n  Got:\n%v", test.description, e, a)
   939  					}
   940  				}
   941  
   942  				if len(test.expectOutputToContain) > 0 {
   943  					out := outBuf.String()
   944  					if !strings.Contains(out, test.expectOutputToContain) {
   945  						t.Fatalf("%s: expected output to contain: %s\nGot:\n%s", test.description, test.expectOutputToContain, out)
   946  					}
   947  				}
   948  			})
   949  		}
   950  	}
   951  }
   952  
   953  type MyReq struct {
   954  	Request *http.Request
   955  }
   956  
   957  func (m *MyReq) isFor(method string, path string) bool {
   958  	req := m.Request
   959  
   960  	return method == req.Method && (req.URL.Path == path ||
   961  		req.URL.Path == strings.Join([]string{"/api/v1", path}, "") ||
   962  		req.URL.Path == strings.Join([]string{"/apis/apps/v1", path}, "") ||
   963  		req.URL.Path == strings.Join([]string{"/apis/batch/v1", path}, ""))
   964  }
   965  

View as plain text