...

Source file src/k8s.io/kubectl/pkg/cmd/taint/taint_test.go

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

     1  /*
     2  Copyright 2014 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 taint
    18  
    19  import (
    20  	"io"
    21  	"net/http"
    22  	"reflect"
    23  	"strings"
    24  	"testing"
    25  	"time"
    26  
    27  	corev1 "k8s.io/api/core/v1"
    28  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    29  	"k8s.io/apimachinery/pkg/runtime"
    30  	"k8s.io/apimachinery/pkg/util/strategicpatch"
    31  	"k8s.io/cli-runtime/pkg/genericiooptions"
    32  	"k8s.io/client-go/rest/fake"
    33  	cmdtesting "k8s.io/kubectl/pkg/cmd/testing"
    34  	cmdutil "k8s.io/kubectl/pkg/cmd/util"
    35  	"k8s.io/kubectl/pkg/scheme"
    36  )
    37  
    38  func generateNodeAndTaintedNode(oldTaints []corev1.Taint, newTaints []corev1.Taint) (*corev1.Node, *corev1.Node) {
    39  	var taintedNode *corev1.Node
    40  
    41  	// Create a node.
    42  	node := &corev1.Node{
    43  		ObjectMeta: metav1.ObjectMeta{
    44  			Name:              "node-name",
    45  			CreationTimestamp: metav1.Time{Time: time.Now()},
    46  		},
    47  		Spec: corev1.NodeSpec{
    48  			Taints: oldTaints,
    49  		},
    50  		Status: corev1.NodeStatus{},
    51  	}
    52  
    53  	// A copy of the same node, but tainted.
    54  	taintedNode = node.DeepCopy()
    55  	taintedNode.Spec.Taints = newTaints
    56  
    57  	return node, taintedNode
    58  }
    59  
    60  func equalTaints(taintsA, taintsB []corev1.Taint) bool {
    61  	if len(taintsA) != len(taintsB) {
    62  		return false
    63  	}
    64  
    65  	for _, taintA := range taintsA {
    66  		found := false
    67  		for _, taintB := range taintsB {
    68  			if reflect.DeepEqual(taintA, taintB) {
    69  				found = true
    70  				break
    71  			}
    72  		}
    73  		if !found {
    74  			return false
    75  		}
    76  	}
    77  	return true
    78  }
    79  
    80  func TestTaint(t *testing.T) {
    81  	tests := []struct {
    82  		description string
    83  		oldTaints   []corev1.Taint
    84  		newTaints   []corev1.Taint
    85  		args        []string
    86  		expectFatal bool
    87  		expectTaint bool
    88  	}{
    89  		// success cases
    90  		{
    91  			description: "taints a node with effect NoSchedule",
    92  			newTaints: []corev1.Taint{{
    93  				Key:    "foo",
    94  				Value:  "bar",
    95  				Effect: "NoSchedule",
    96  			}},
    97  			args:        []string{"node", "node-name", "foo=bar:NoSchedule"},
    98  			expectFatal: false,
    99  			expectTaint: true,
   100  		},
   101  		{
   102  			description: "taints a node with effect PreferNoSchedule",
   103  			newTaints: []corev1.Taint{{
   104  				Key:    "foo",
   105  				Value:  "bar",
   106  				Effect: "PreferNoSchedule",
   107  			}},
   108  			args:        []string{"node", "node-name", "foo=bar:PreferNoSchedule"},
   109  			expectFatal: false,
   110  			expectTaint: true,
   111  		},
   112  		{
   113  			description: "update an existing taint on the node, change the value from bar to barz",
   114  			oldTaints: []corev1.Taint{{
   115  				Key:    "foo",
   116  				Value:  "bar",
   117  				Effect: "NoSchedule",
   118  			}},
   119  			newTaints: []corev1.Taint{{
   120  				Key:    "foo",
   121  				Value:  "barz",
   122  				Effect: "NoSchedule",
   123  			}},
   124  			args:        []string{"node", "node-name", "foo=barz:NoSchedule", "--overwrite"},
   125  			expectFatal: false,
   126  			expectTaint: true,
   127  		},
   128  		{
   129  			description: "taints a node with two taints",
   130  			newTaints: []corev1.Taint{{
   131  				Key:    "dedicated",
   132  				Value:  "namespaceA",
   133  				Effect: "NoSchedule",
   134  			}, {
   135  				Key:    "foo",
   136  				Value:  "bar",
   137  				Effect: "PreferNoSchedule",
   138  			}},
   139  			args:        []string{"node", "node-name", "dedicated=namespaceA:NoSchedule", "foo=bar:PreferNoSchedule"},
   140  			expectFatal: false,
   141  			expectTaint: true,
   142  		},
   143  		{
   144  			description: "node has two taints with the same key but different effect, remove one of them by indicating exact key and effect",
   145  			oldTaints: []corev1.Taint{{
   146  				Key:    "dedicated",
   147  				Value:  "namespaceA",
   148  				Effect: "NoSchedule",
   149  			}, {
   150  				Key:    "dedicated",
   151  				Value:  "namespaceA",
   152  				Effect: "PreferNoSchedule",
   153  			}},
   154  			newTaints: []corev1.Taint{{
   155  				Key:    "dedicated",
   156  				Value:  "namespaceA",
   157  				Effect: "PreferNoSchedule",
   158  			}},
   159  			args:        []string{"node", "node-name", "dedicated:NoSchedule-"},
   160  			expectFatal: false,
   161  			expectTaint: true,
   162  		},
   163  		{
   164  			description: "node has two taints with the same key but different effect, remove all of them with wildcard",
   165  			oldTaints: []corev1.Taint{{
   166  				Key:    "dedicated",
   167  				Value:  "namespaceA",
   168  				Effect: "NoSchedule",
   169  			}, {
   170  				Key:    "dedicated",
   171  				Value:  "namespaceA",
   172  				Effect: "PreferNoSchedule",
   173  			}},
   174  			newTaints:   []corev1.Taint{},
   175  			args:        []string{"node", "node-name", "dedicated-"},
   176  			expectFatal: false,
   177  			expectTaint: true,
   178  		},
   179  		{
   180  			description: "node has two taints, update one of them and remove the other",
   181  			oldTaints: []corev1.Taint{{
   182  				Key:    "dedicated",
   183  				Value:  "namespaceA",
   184  				Effect: "NoSchedule",
   185  			}, {
   186  				Key:    "foo",
   187  				Value:  "bar",
   188  				Effect: "PreferNoSchedule",
   189  			}},
   190  			newTaints: []corev1.Taint{{
   191  				Key:    "foo",
   192  				Value:  "barz",
   193  				Effect: "PreferNoSchedule",
   194  			}},
   195  			args:        []string{"node", "node-name", "dedicated:NoSchedule-", "foo=barz:PreferNoSchedule", "--overwrite"},
   196  			expectFatal: false,
   197  			expectTaint: true,
   198  		},
   199  
   200  		// error cases
   201  		{
   202  			description: "invalid taint key",
   203  			args:        []string{"node", "node-name", "nospecialchars^@=banana:NoSchedule"},
   204  			expectFatal: true,
   205  			expectTaint: false,
   206  		},
   207  		{
   208  			description: "invalid taint effect",
   209  			args:        []string{"node", "node-name", "foo=bar:NoExcute"},
   210  			expectFatal: true,
   211  			expectTaint: false,
   212  		},
   213  		{
   214  			description: "duplicated taints with the same key and effect should be rejected",
   215  			args:        []string{"node", "node-name", "foo=bar:NoExcute", "foo=barz:NoExcute"},
   216  			expectFatal: true,
   217  			expectTaint: false,
   218  		},
   219  		{
   220  			description: "add and remove taint with same key and effect should be rejected",
   221  			args:        []string{"node", "node-name", "foo=:NoExcute", "foo=:NoExcute-"},
   222  			expectFatal: true,
   223  			expectTaint: false,
   224  		},
   225  		{
   226  			description: "can't update existing taint on the node, since 'overwrite' flag is not set",
   227  			oldTaints: []corev1.Taint{{
   228  				Key:    "foo",
   229  				Value:  "bar",
   230  				Effect: "NoSchedule",
   231  			}},
   232  			newTaints: []corev1.Taint{{
   233  				Key:    "foo",
   234  				Value:  "bar",
   235  				Effect: "NoSchedule",
   236  			}},
   237  			args:        []string{"node", "node-name", "foo=bar:NoSchedule"},
   238  			expectFatal: true,
   239  			expectTaint: false,
   240  		},
   241  	}
   242  
   243  	for _, test := range tests {
   244  		t.Run(test.description, func(t *testing.T) {
   245  			oldNode, expectNewNode := generateNodeAndTaintedNode(test.oldTaints, test.newTaints)
   246  			newNode := &corev1.Node{}
   247  			tainted := false
   248  			tf := cmdtesting.NewTestFactory()
   249  			defer tf.Cleanup()
   250  
   251  			codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...)
   252  			ns := scheme.Codecs.WithoutConversion()
   253  
   254  			tf.Client = &fake.RESTClient{
   255  				NegotiatedSerializer: ns,
   256  				GroupVersion:         corev1.SchemeGroupVersion,
   257  				Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
   258  					m := &MyReq{req}
   259  					switch {
   260  					case m.isFor("GET", "/nodes"):
   261  						return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, oldNode)}, nil
   262  					case m.isFor("GET", "/nodes/node-name"):
   263  						return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, oldNode)}, nil
   264  					case m.isFor("PATCH", "/nodes/node-name"):
   265  						tainted = true
   266  						data, err := io.ReadAll(req.Body)
   267  						if err != nil {
   268  							t.Fatalf("%s: unexpected error: %v", test.description, err)
   269  						}
   270  						defer req.Body.Close()
   271  
   272  						// apply the patch
   273  						oldJSON, err := runtime.Encode(codec, oldNode)
   274  						if err != nil {
   275  							t.Fatalf("%s: unexpected error: %v", test.description, err)
   276  						}
   277  						appliedPatch, err := strategicpatch.StrategicMergePatch(oldJSON, data, &corev1.Node{})
   278  						if err != nil {
   279  							t.Fatalf("%s: unexpected error: %v", test.description, err)
   280  						}
   281  
   282  						// decode the patch
   283  						if err := runtime.DecodeInto(codec, appliedPatch, newNode); err != nil {
   284  							t.Fatalf("%s: unexpected error: %v", test.description, err)
   285  						}
   286  						if !equalTaints(expectNewNode.Spec.Taints, newNode.Spec.Taints) {
   287  							t.Fatalf("%s: expected:\n%v\nsaw:\n%v\n", test.description, expectNewNode.Spec.Taints, newNode.Spec.Taints)
   288  						}
   289  						return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, newNode)}, nil
   290  					case m.isFor("PUT", "/nodes/node-name"):
   291  						tainted = true
   292  						data, err := io.ReadAll(req.Body)
   293  						if err != nil {
   294  							t.Fatalf("%s: unexpected error: %v", test.description, err)
   295  						}
   296  						defer req.Body.Close()
   297  						if err := runtime.DecodeInto(codec, data, newNode); err != nil {
   298  							t.Fatalf("%s: unexpected error: %v", test.description, err)
   299  						}
   300  						if !equalTaints(expectNewNode.Spec.Taints, newNode.Spec.Taints) {
   301  							t.Fatalf("%s: expected:\n%v\nsaw:\n%v\n", test.description, expectNewNode.Spec.Taints, newNode.Spec.Taints)
   302  						}
   303  						return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, newNode)}, nil
   304  					default:
   305  						t.Fatalf("%s: unexpected request: %v %#v\n%#v", test.description, req.Method, req.URL, req)
   306  						return nil, nil
   307  					}
   308  				}),
   309  			}
   310  			tf.ClientConfigVal = cmdtesting.DefaultClientConfig()
   311  
   312  			cmd := NewCmdTaint(tf, genericiooptions.NewTestIOStreamsDiscard())
   313  
   314  			sawFatal := false
   315  			func() {
   316  				defer func() {
   317  					// Recover from the panic below.
   318  					if r := recover(); r != nil {
   319  						t.Logf("Recovered: %v", r)
   320  					}
   321  
   322  					// Restore cmdutil behavior
   323  					cmdutil.DefaultBehaviorOnFatal()
   324  				}()
   325  				cmdutil.BehaviorOnFatal(func(e string, code int) { sawFatal = true; panic(e) })
   326  				cmd.SetArgs(test.args)
   327  				cmd.Execute()
   328  			}()
   329  
   330  			if test.expectFatal {
   331  				if !sawFatal {
   332  					t.Fatalf("%s: unexpected non-error", test.description)
   333  				}
   334  			}
   335  
   336  			if test.expectTaint {
   337  				if !tainted {
   338  					t.Fatalf("%s: node not tainted", test.description)
   339  				}
   340  			}
   341  			if !test.expectTaint {
   342  				if tainted {
   343  					t.Fatalf("%s: unexpected taint", test.description)
   344  				}
   345  			}
   346  		})
   347  	}
   348  }
   349  
   350  func TestValidateFlags(t *testing.T) {
   351  	tests := []struct {
   352  		taintOpts   TaintOptions
   353  		description string
   354  		expectFatal bool
   355  	}{
   356  
   357  		{
   358  			taintOpts:   TaintOptions{selector: "myLabel=X", all: false},
   359  			description: "With Selector and without All flag",
   360  			expectFatal: false,
   361  		},
   362  		{
   363  			taintOpts:   TaintOptions{selector: "", all: true},
   364  			description: "Without selector and All flag",
   365  			expectFatal: false,
   366  		},
   367  		{
   368  			taintOpts:   TaintOptions{selector: "myLabel=X", all: true},
   369  			description: "With Selector and with All flag",
   370  			expectFatal: true,
   371  		},
   372  		{
   373  			taintOpts:   TaintOptions{selector: "", all: false, resources: []string{"node"}},
   374  			description: "Without Selector and All flags and if node name is not provided",
   375  			expectFatal: true,
   376  		},
   377  		{
   378  			taintOpts:   TaintOptions{selector: "", all: false, resources: []string{"node", "node-name"}},
   379  			description: "Without Selector and ALL flags and if node name is provided",
   380  			expectFatal: false,
   381  		},
   382  	}
   383  	for _, test := range tests {
   384  		sawFatal := false
   385  		err := test.taintOpts.validateFlags()
   386  		if err != nil {
   387  			sawFatal = true
   388  		}
   389  		if test.expectFatal {
   390  			if !sawFatal {
   391  				t.Fatalf("%s expected not to fail", test.description)
   392  			}
   393  		}
   394  	}
   395  }
   396  
   397  type MyReq struct {
   398  	Request *http.Request
   399  }
   400  
   401  func (m *MyReq) isFor(method string, path string) bool {
   402  	req := m.Request
   403  
   404  	return method == req.Method && (req.URL.Path == path ||
   405  		req.URL.Path == strings.Join([]string{"/api/v1", path}, "") ||
   406  		req.URL.Path == strings.Join([]string{"/apis/extensions/v1beta1", path}, "") ||
   407  		req.URL.Path == strings.Join([]string{"/apis/batch/v1", path}, ""))
   408  }
   409  

View as plain text