...

Source file src/k8s.io/kubectl/pkg/cmd/cmd_test.go

Documentation: k8s.io/kubectl/pkg/cmd

     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 cmd
    18  
    19  import (
    20  	"fmt"
    21  	"os"
    22  	"reflect"
    23  	"testing"
    24  
    25  	"github.com/google/go-cmp/cmp"
    26  	"github.com/google/go-cmp/cmp/cmpopts"
    27  	"github.com/spf13/cobra"
    28  
    29  	"k8s.io/cli-runtime/pkg/genericclioptions"
    30  	"k8s.io/cli-runtime/pkg/genericiooptions"
    31  	"k8s.io/kubectl/pkg/cmd/plugin"
    32  )
    33  
    34  func TestNormalizationFuncGlobalExistence(t *testing.T) {
    35  	// This test can be safely deleted when we will not support multiple flag formats
    36  	root := NewKubectlCommand(KubectlOptions{IOStreams: genericiooptions.IOStreams{In: os.Stdin, Out: os.Stdout, ErrOut: os.Stderr}})
    37  
    38  	if root.Parent() != nil {
    39  		t.Fatal("We expect the root command to be returned")
    40  	}
    41  	if root.GlobalNormalizationFunc() == nil {
    42  		t.Fatal("We expect that root command has a global normalization function")
    43  	}
    44  
    45  	if reflect.ValueOf(root.GlobalNormalizationFunc()).Pointer() != reflect.ValueOf(root.Flags().GetNormalizeFunc()).Pointer() {
    46  		t.Fatal("root command seems to have a wrong normalization function")
    47  	}
    48  
    49  	sub := root
    50  	for sub.HasSubCommands() {
    51  		sub = sub.Commands()[0]
    52  	}
    53  
    54  	// In case of failure of this test check this PR: spf13/cobra#110
    55  	if reflect.ValueOf(sub.Flags().GetNormalizeFunc()).Pointer() != reflect.ValueOf(root.Flags().GetNormalizeFunc()).Pointer() {
    56  		t.Fatal("child and root commands should have the same normalization functions")
    57  	}
    58  }
    59  
    60  func TestKubectlSubcommandShadowPlugin(t *testing.T) {
    61  	tests := []struct {
    62  		name              string
    63  		args              []string
    64  		expectPlugin      string
    65  		expectPluginArgs  []string
    66  		expectLookupError string
    67  	}{
    68  		{
    69  			name:             "test that a plugin executable is found based on command args when builtin subcommand does not exist",
    70  			args:             []string{"kubectl", "create", "foo", "--bar", "--bar2", "--namespace", "test-namespace"},
    71  			expectPlugin:     "plugin/testdata/kubectl-create-foo",
    72  			expectPluginArgs: []string{"--bar", "--bar2", "--namespace", "test-namespace"},
    73  		},
    74  		{
    75  			name:              "test that a plugin executable is not found based on command args when also builtin subcommand does not exist",
    76  			args:              []string{"kubectl", "create", "foo2", "--bar", "--bar2", "--namespace", "test-namespace"},
    77  			expectLookupError: "unable to find a plugin executable \"kubectl-create-foo2\"",
    78  		},
    79  		{
    80  			name:             "test that normal commands are able to be executed, when builtin subcommand exists",
    81  			args:             []string{"kubectl", "create", "job", "foo", "--image=busybox", "--dry-run=client", "--namespace", "test-namespace"},
    82  			expectPlugin:     "",
    83  			expectPluginArgs: []string{},
    84  		},
    85  		// rest of the tests are copied from TestKubectlCommandHandlesPlugins function,
    86  		// just to retest them also when feature is enabled.
    87  		{
    88  			name:             "test that normal commands are able to be executed, when no plugin overshadows them",
    89  			args:             []string{"kubectl", "config", "get-clusters"},
    90  			expectPlugin:     "",
    91  			expectPluginArgs: []string{},
    92  		},
    93  		{
    94  			name:             "test that a plugin executable is found based on command args",
    95  			args:             []string{"kubectl", "foo", "--bar"},
    96  			expectPlugin:     "plugin/testdata/kubectl-foo",
    97  			expectPluginArgs: []string{"--bar"},
    98  		},
    99  		{
   100  			name: "test that a plugin does not execute over an existing command by the same name",
   101  			args: []string{"kubectl", "version", "--client=true"},
   102  		},
   103  		{
   104  			name: "test that a plugin does not execute over Cobra's help command",
   105  			args: []string{"kubectl", "help"},
   106  		},
   107  		{
   108  			name: "test that a plugin does not execute over Cobra's __complete command",
   109  			args: []string{"kubectl", cobra.ShellCompRequestCmd, "de"},
   110  		},
   111  		{
   112  			name: "test that a plugin does not execute over Cobra's __completeNoDesc command",
   113  			args: []string{"kubectl", cobra.ShellCompNoDescRequestCmd, "de"},
   114  		},
   115  		{
   116  			name: "test that a flag does not break Cobra's help command",
   117  			args: []string{"kubectl", "--kubeconfig=/path/to/kubeconfig", "help"},
   118  		},
   119  		{
   120  			name: "test that a flag does not break Cobra's __complete command",
   121  			args: []string{"kubectl", "--kubeconfig=/path/to/kubeconfig", cobra.ShellCompRequestCmd},
   122  		},
   123  		{
   124  			name: "test that a flag does not break Cobra's __completeNoDesc command",
   125  			args: []string{"kubectl", "--kubeconfig=/path/to/kubeconfig", cobra.ShellCompNoDescRequestCmd},
   126  		},
   127  	}
   128  
   129  	for _, test := range tests {
   130  		t.Run(test.name, func(t *testing.T) {
   131  			pluginsHandler := &testPluginHandler{
   132  				pluginsDirectory: "plugin/testdata",
   133  				validPrefixes:    plugin.ValidPluginFilenamePrefixes,
   134  			}
   135  			ioStreams, _, _, _ := genericiooptions.NewTestIOStreams()
   136  			root := NewDefaultKubectlCommandWithArgs(KubectlOptions{PluginHandler: pluginsHandler, Arguments: test.args, IOStreams: ioStreams})
   137  			// original plugin handler (DefaultPluginHandler) is implemented by exec call so no additional actions are expected on the cobra command if we activate the plugin flow
   138  			if !pluginsHandler.lookedup && !pluginsHandler.executed {
   139  				// args must be set, otherwise Execute will use os.Args (args used for starting the test) and test.args would not be passed
   140  				// to the command which might invoke only "kubectl" without any additional args and give false positives
   141  				root.SetArgs(test.args[1:])
   142  				// Important note! Incorrect command or command failing validation might just call os.Exit(1) which would interrupt execution of the test
   143  				if err := root.Execute(); err != nil {
   144  					t.Fatalf("unexpected error: %v", err)
   145  				}
   146  			}
   147  
   148  			if (pluginsHandler.lookupErr != nil && pluginsHandler.lookupErr.Error() != test.expectLookupError) ||
   149  				(pluginsHandler.lookupErr == nil && len(test.expectLookupError) > 0) {
   150  				t.Fatalf("unexpected error: expected %q to occur, but got %q", test.expectLookupError, pluginsHandler.lookupErr)
   151  			}
   152  
   153  			if pluginsHandler.lookedup && !pluginsHandler.executed && len(test.expectLookupError) == 0 {
   154  				// we have to fail here, because we have found the plugin, but not executed the plugin, nor the command (this would normally result in an error: unknown command)
   155  				t.Fatalf("expected plugin execution, but did not occur")
   156  			}
   157  
   158  			if pluginsHandler.executedPlugin != test.expectPlugin {
   159  				t.Fatalf("unexpected plugin execution: expected %q, got %q", test.expectPlugin, pluginsHandler.executedPlugin)
   160  			}
   161  
   162  			if pluginsHandler.executed && len(test.expectPlugin) == 0 {
   163  				t.Fatalf("unexpected plugin execution: expected no plugin, got %q", pluginsHandler.executedPlugin)
   164  			}
   165  
   166  			if !cmp.Equal(pluginsHandler.withArgs, test.expectPluginArgs, cmpopts.EquateEmpty()) {
   167  				t.Fatalf("unexpected plugin execution args: expected %q, got %q", test.expectPluginArgs, pluginsHandler.withArgs)
   168  			}
   169  		})
   170  	}
   171  }
   172  
   173  func TestKubectlCommandHandlesPlugins(t *testing.T) {
   174  	tests := []struct {
   175  		name              string
   176  		args              []string
   177  		expectPlugin      string
   178  		expectPluginArgs  []string
   179  		expectLookupError string
   180  	}{
   181  		{
   182  			name:             "test that normal commands are able to be executed, when no plugin overshadows them",
   183  			args:             []string{"kubectl", "config", "get-clusters"},
   184  			expectPlugin:     "",
   185  			expectPluginArgs: []string{},
   186  		},
   187  		{
   188  			name:             "test that normal commands are able to be executed, when no plugin overshadows them and shadowing feature is not enabled",
   189  			args:             []string{"kubectl", "create", "job", "foo", "--image=busybox", "--dry-run=client"},
   190  			expectPlugin:     "",
   191  			expectPluginArgs: []string{},
   192  		},
   193  		{
   194  			name:             "test that a plugin executable is found based on command args",
   195  			args:             []string{"kubectl", "foo", "--bar"},
   196  			expectPlugin:     "plugin/testdata/kubectl-foo",
   197  			expectPluginArgs: []string{"--bar"},
   198  		},
   199  		{
   200  			name: "test that a plugin does not execute over an existing command by the same name",
   201  			args: []string{"kubectl", "version", "--client=true"},
   202  		},
   203  		// The following tests make sure that commands added by Cobra cannot be shadowed by a plugin
   204  		// See https://github.com/kubernetes/kubectl/issues/1116
   205  		{
   206  			name: "test that a plugin does not execute over Cobra's help command",
   207  			args: []string{"kubectl", "help"},
   208  		},
   209  		{
   210  			name: "test that a plugin does not execute over Cobra's __complete command",
   211  			args: []string{"kubectl", cobra.ShellCompRequestCmd, "de"},
   212  		},
   213  		{
   214  			name: "test that a plugin does not execute over Cobra's __completeNoDesc command",
   215  			args: []string{"kubectl", cobra.ShellCompNoDescRequestCmd, "de"},
   216  		},
   217  		// The following tests make sure that commands added by Cobra cannot be shadowed by a plugin
   218  		// even when a flag is specified first.  This can happen when using aliases.
   219  		// See https://github.com/kubernetes/kubectl/issues/1119
   220  		{
   221  			name: "test that a flag does not break Cobra's help command",
   222  			args: []string{"kubectl", "--kubeconfig=/path/to/kubeconfig", "help"},
   223  		},
   224  		{
   225  			name: "test that a flag does not break Cobra's __complete command",
   226  			args: []string{"kubectl", "--kubeconfig=/path/to/kubeconfig", cobra.ShellCompRequestCmd},
   227  		},
   228  		{
   229  			name: "test that a flag does not break Cobra's __completeNoDesc command",
   230  			args: []string{"kubectl", "--kubeconfig=/path/to/kubeconfig", cobra.ShellCompNoDescRequestCmd},
   231  		},
   232  		// As for the previous tests, an alias could add a flag without using the = form.
   233  		// We don't support this case as parsing the flags becomes quite complicated (flags
   234  		// that take a value, versus flags that don't)
   235  		// {
   236  		// 	name: "test that a flag with a space does not break Cobra's help command",
   237  		// 	args: []string{"kubectl", "--kubeconfig", "/path/to/kubeconfig", "help"},
   238  		// },
   239  		// {
   240  		// 	name: "test that a flag with a space does not break Cobra's __complete command",
   241  		// 	args: []string{"kubectl", "--kubeconfig", "/path/to/kubeconfig", cobra.ShellCompRequestCmd},
   242  		// },
   243  		// {
   244  		// 	name: "test that a flag with a space does not break Cobra's __completeNoDesc command",
   245  		// 	args: []string{"kubectl", "--kubeconfig", "/path/to/kubeconfig", cobra.ShellCompNoDescRequestCmd},
   246  		// },
   247  	}
   248  
   249  	for _, test := range tests {
   250  		t.Run(test.name, func(t *testing.T) {
   251  			pluginsHandler := &testPluginHandler{
   252  				pluginsDirectory: "plugin/testdata",
   253  				validPrefixes:    plugin.ValidPluginFilenamePrefixes,
   254  			}
   255  			ioStreams, _, _, _ := genericiooptions.NewTestIOStreams()
   256  			root := NewDefaultKubectlCommandWithArgs(KubectlOptions{PluginHandler: pluginsHandler, Arguments: test.args, IOStreams: ioStreams})
   257  			// original plugin handler (DefaultPluginHandler) is implemented by exec call so no additional actions are expected on the cobra command if we activate the plugin flow
   258  			if !pluginsHandler.lookedup && !pluginsHandler.executed {
   259  				// args must be set, otherwise Execute will use os.Args (args used for starting the test) and test.args would not be passed
   260  				// to the command which might invoke only "kubectl" without any additional args and give false positives
   261  				root.SetArgs(test.args[1:])
   262  				// Important note! Incorrect command or command failing validation might just call os.Exit(1) which would interrupt execution of the test
   263  				if err := root.Execute(); err != nil {
   264  					t.Fatalf("unexpected error: %v", err)
   265  				}
   266  			}
   267  
   268  			if (pluginsHandler.lookupErr != nil && pluginsHandler.lookupErr.Error() != test.expectLookupError) ||
   269  				(pluginsHandler.lookupErr == nil && len(test.expectLookupError) > 0) {
   270  				t.Fatalf("unexpected error: expected %q to occur, but got %q", test.expectLookupError, pluginsHandler.lookupErr)
   271  			}
   272  
   273  			if pluginsHandler.lookedup && !pluginsHandler.executed && len(test.expectLookupError) == 0 {
   274  				// we have to fail here, because we have found the plugin, but not executed the plugin, nor the command (this would normally result in an error: unknown command)
   275  				t.Fatalf("expected plugin execution, but did not occur")
   276  			}
   277  
   278  			if pluginsHandler.executedPlugin != test.expectPlugin {
   279  				t.Fatalf("unexpected plugin execution: expected %q, got %q", test.expectPlugin, pluginsHandler.executedPlugin)
   280  			}
   281  
   282  			if pluginsHandler.executed && len(test.expectPlugin) == 0 {
   283  				t.Fatalf("unexpected plugin execution: expected no plugin, got %q", pluginsHandler.executedPlugin)
   284  			}
   285  
   286  			if !cmp.Equal(pluginsHandler.withArgs, test.expectPluginArgs, cmpopts.EquateEmpty()) {
   287  				t.Fatalf("unexpected plugin execution args: expected %q, got %q", test.expectPluginArgs, pluginsHandler.withArgs)
   288  			}
   289  		})
   290  	}
   291  }
   292  
   293  type testPluginHandler struct {
   294  	pluginsDirectory string
   295  	validPrefixes    []string
   296  
   297  	// lookup results
   298  	lookedup  bool
   299  	lookupErr error
   300  
   301  	// execution results
   302  	executed       bool
   303  	executedPlugin string
   304  	withArgs       []string
   305  	withEnv        []string
   306  }
   307  
   308  func (h *testPluginHandler) Lookup(filename string) (string, bool) {
   309  	h.lookedup = true
   310  
   311  	dir, err := os.Stat(h.pluginsDirectory)
   312  	if err != nil {
   313  		h.lookupErr = err
   314  		return "", false
   315  	}
   316  
   317  	if !dir.IsDir() {
   318  		h.lookupErr = fmt.Errorf("expected %q to be a directory", h.pluginsDirectory)
   319  		return "", false
   320  	}
   321  
   322  	plugins, err := os.ReadDir(h.pluginsDirectory)
   323  	if err != nil {
   324  		h.lookupErr = err
   325  		return "", false
   326  	}
   327  
   328  	filenameWithSuportedPrefix := ""
   329  	for _, prefix := range h.validPrefixes {
   330  		for _, p := range plugins {
   331  			filenameWithSuportedPrefix = fmt.Sprintf("%s-%s", prefix, filename)
   332  			if p.Name() == filenameWithSuportedPrefix {
   333  				return fmt.Sprintf("%s/%s", h.pluginsDirectory, p.Name()), true
   334  			}
   335  		}
   336  	}
   337  
   338  	h.lookupErr = fmt.Errorf("unable to find a plugin executable %q", filenameWithSuportedPrefix)
   339  	return "", false
   340  }
   341  
   342  func (h *testPluginHandler) Execute(executablePath string, cmdArgs, env []string) error {
   343  	h.executed = true
   344  	h.executedPlugin = executablePath
   345  	h.withArgs = cmdArgs
   346  	h.withEnv = env
   347  	return nil
   348  }
   349  
   350  func TestKubectlCommandHeadersHooks(t *testing.T) {
   351  	tests := map[string]struct {
   352  		envVar    string
   353  		addsHooks bool
   354  	}{
   355  		"empty environment variable; hooks added": {
   356  			envVar:    "",
   357  			addsHooks: true,
   358  		},
   359  		"random env var value; hooks added": {
   360  			envVar:    "foo",
   361  			addsHooks: true,
   362  		},
   363  		"true env var value; hooks added": {
   364  			envVar:    "true",
   365  			addsHooks: true,
   366  		},
   367  		"false env var value; hooks NOT added": {
   368  			envVar:    "false",
   369  			addsHooks: false,
   370  		},
   371  		"zero env var value; hooks NOT added": {
   372  			envVar:    "0",
   373  			addsHooks: false,
   374  		},
   375  	}
   376  
   377  	for name, testCase := range tests {
   378  		t.Run(name, func(t *testing.T) {
   379  			cmds := &cobra.Command{}
   380  			kubeConfigFlags := genericclioptions.NewConfigFlags(true).WithDeprecatedPasswordFlag()
   381  			if kubeConfigFlags.WrapConfigFn != nil {
   382  				t.Fatal("expected initial nil WrapConfigFn")
   383  			}
   384  			t.Setenv(kubectlCmdHeaders, testCase.envVar)
   385  			addCmdHeaderHooks(cmds, kubeConfigFlags)
   386  			// Valdidate whether the hooks were added.
   387  			if testCase.addsHooks && kubeConfigFlags.WrapConfigFn == nil {
   388  				t.Error("after adding kubectl command header, expecting non-nil WrapConfigFn")
   389  			}
   390  			if !testCase.addsHooks && kubeConfigFlags.WrapConfigFn != nil {
   391  				t.Error("env var feature gate should have blocked setting WrapConfigFn")
   392  			}
   393  		})
   394  	}
   395  }
   396  

View as plain text