diff --git a/staging/src/k8s.io/kubectl/pkg/cmd/cmd.go b/staging/src/k8s.io/kubectl/pkg/cmd/cmd.go index 8313179b007..85b2d784251 100644 --- a/staging/src/k8s.io/kubectl/pkg/cmd/cmd.go +++ b/staging/src/k8s.io/kubectl/pkg/cmd/cmd.go @@ -83,6 +83,10 @@ import ( const kubectlCmdHeaders = "KUBECTL_COMMAND_HEADERS" +var ( + allowedCmdsSubcommandPlugin = map[string]struct{}{"create": {}} +) + type KubectlOptions struct { PluginHandler PluginHandler Arguments []string @@ -116,7 +120,7 @@ func NewDefaultKubectlCommandWithArgs(o KubectlOptions) *cobra.Command { // only look for suitable extension executables if // the specified command does not already exist - if _, _, err := cmd.Find(cmdPathPieces); err != nil { + if foundCmd, foundArgs, err := cmd.Find(cmdPathPieces); err != nil { // Also check the commands that will be added by Cobra. // These commands are only added once rootCmd.Execute() is called, so we // need to check them explicitly here. @@ -132,11 +136,39 @@ func NewDefaultKubectlCommandWithArgs(o KubectlOptions) *cobra.Command { case "help", cobra.ShellCompRequestCmd, cobra.ShellCompNoDescRequestCmd: // Don't search for a plugin default: - if err := HandlePluginCommand(o.PluginHandler, cmdPathPieces); err != nil { + if err := HandlePluginCommand(o.PluginHandler, cmdPathPieces, false); err != nil { fmt.Fprintf(o.IOStreams.ErrOut, "Error: %v\n", err) os.Exit(1) } } + } else if err == nil { + if cmdutil.CmdPluginAsSubcommand.IsEnabled() { + // Command exists(e.g. kubectl create), but it is not certain that + // subcommand also exists (e.g. kubectl create networkpolicy) + if _, ok := allowedCmdsSubcommandPlugin[foundCmd.Name()]; ok { + var subcommand string + for _, arg := range foundArgs { // first "non-flag" argument as subcommand + if !strings.HasPrefix(arg, "-") { + subcommand = arg + break + } + } + builtinSubcmdExist := false + for _, subcmd := range foundCmd.Commands() { + if subcmd.Name() == subcommand { + builtinSubcmdExist = true + break + } + } + + if !builtinSubcmdExist { + if err := HandlePluginCommand(o.PluginHandler, cmdPathPieces, true); err != nil { + fmt.Fprintf(o.IOStreams.ErrOut, "Error: %v\n", err) + os.Exit(1) + } + } + } + } } } @@ -224,7 +256,7 @@ func (h *DefaultPluginHandler) Execute(executablePath string, cmdArgs, environme // HandlePluginCommand receives a pluginHandler and command-line arguments and attempts to find // a plugin executable on the PATH that satisfies the given arguments. -func HandlePluginCommand(pluginHandler PluginHandler, cmdArgs []string) error { +func HandlePluginCommand(pluginHandler PluginHandler, cmdArgs []string, exactMatch bool) error { var remainingArgs []string // all "non-flag" arguments for _, arg := range cmdArgs { if strings.HasPrefix(arg, "-") { @@ -244,6 +276,12 @@ func HandlePluginCommand(pluginHandler PluginHandler, cmdArgs []string) error { for len(remainingArgs) > 0 { path, found := pluginHandler.Lookup(strings.Join(remainingArgs, "-")) if !found { + if exactMatch { + // if exactMatch is true, we shouldn't continue searching with shorter names. + // this is especially for not searching kubectl-create plugin + // when kubectl-create-foo plugin is not found. + break + } remainingArgs = remainingArgs[:len(remainingArgs)-1] continue } diff --git a/staging/src/k8s.io/kubectl/pkg/cmd/cmd_test.go b/staging/src/k8s.io/kubectl/pkg/cmd/cmd_test.go index a8bc0c8422f..15a7708c760 100644 --- a/staging/src/k8s.io/kubectl/pkg/cmd/cmd_test.go +++ b/staging/src/k8s.io/kubectl/pkg/cmd/cmd_test.go @@ -25,7 +25,10 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/spf13/cobra" + "k8s.io/cli-runtime/pkg/genericclioptions" + cmdtesting "k8s.io/kubectl/pkg/cmd/testing" + cmdutil "k8s.io/kubectl/pkg/cmd/util" ) func TestNormalizationFuncGlobalExistence(t *testing.T) { @@ -54,6 +57,104 @@ func TestNormalizationFuncGlobalExistence(t *testing.T) { } } +func TestKubectlSubcommandShadowPlugin(t *testing.T) { + tests := []struct { + name string + args []string + expectPlugin string + expectPluginArgs []string + expectError string + }{ + { + name: "test that a plugin executable is found based on command args when builtin subcommand does not exist", + args: []string{"kubectl", "create", "foo", "--bar", "--bar2", "--namespace", "test-namespace"}, + expectPlugin: "plugin/testdata/kubectl-create-foo", + expectPluginArgs: []string{"--bar", "--bar2", "--namespace", "test-namespace"}, + }, + { + name: "test that a plugin executable is not found based on command args when also builtin subcommand does not exist", + args: []string{"kubectl", "create", "foo2", "--bar", "--bar2", "--namespace", "test-namespace"}, + expectError: "unable to find a plugin executable \"kubectl-create-foo2\"", + }, + { + name: "test that normal commands are able to be executed, when builtin subcommand exists", + args: []string{"kubectl", "create", "role", "--bar", "--bar2", "--namespace", "test-namespace"}, + expectPlugin: "", + expectPluginArgs: []string{}, + }, + // rest of the tests are copied from TestKubectlCommandHandlesPlugins function, + // just to retest them also when feature is enabled. + { + name: "test that normal commands are able to be executed, when no plugin overshadows them", + args: []string{"kubectl", "get", "foo"}, + expectPlugin: "", + expectPluginArgs: []string{}, + }, + { + name: "test that a plugin executable is found based on command args", + args: []string{"kubectl", "foo", "--bar"}, + expectPlugin: "plugin/testdata/kubectl-foo", + expectPluginArgs: []string{"--bar"}, + }, + { + name: "test that a plugin does not execute over an existing command by the same name", + args: []string{"kubectl", "version"}, + }, + { + name: "test that a plugin does not execute over Cobra's help command", + args: []string{"kubectl", "help"}, + }, + { + name: "test that a plugin does not execute over Cobra's __complete command", + args: []string{"kubectl", cobra.ShellCompRequestCmd}, + }, + { + name: "test that a plugin does not execute over Cobra's __completeNoDesc command", + args: []string{"kubectl", cobra.ShellCompNoDescRequestCmd}, + }, + { + name: "test that a flag does not break Cobra's help command", + args: []string{"kubectl", "--kubeconfig=/path/to/kubeconfig", "help"}, + }, + { + name: "test that a flag does not break Cobra's __complete command", + args: []string{"kubectl", "--kubeconfig=/path/to/kubeconfig", cobra.ShellCompRequestCmd}, + }, + { + name: "test that a flag does not break Cobra's __completeNoDesc command", + args: []string{"kubectl", "--kubeconfig=/path/to/kubeconfig", cobra.ShellCompNoDescRequestCmd}, + }, + } + + for _, test := range tests { + cmdtesting.WithAlphaEnvs([]cmdutil.FeatureGate{cmdutil.CmdPluginAsSubcommand}, t, func(t *testing.T) { + t.Run(test.name, func(t *testing.T) { + pluginsHandler := &testPluginHandler{ + pluginsDirectory: "plugin/testdata", + } + ioStreams, _, _, _ := genericclioptions.NewTestIOStreams() + + root := NewDefaultKubectlCommandWithArgs(KubectlOptions{PluginHandler: pluginsHandler, Arguments: test.args, IOStreams: ioStreams}) + if err := root.Execute(); err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if pluginsHandler.err != nil && pluginsHandler.err.Error() != test.expectError { + t.Fatalf("unexpected error: expected %q to occur, but got %q", test.expectError, pluginsHandler.err) + } + + if pluginsHandler.executedPlugin != test.expectPlugin { + t.Fatalf("unexpected plugin execution: expected %q, got %q", test.expectPlugin, pluginsHandler.executedPlugin) + } + + if !cmp.Equal(pluginsHandler.withArgs, test.expectPluginArgs, cmpopts.EquateEmpty()) { + t.Fatalf("unexpected plugin execution args: expected %q, got %q", test.expectPluginArgs, pluginsHandler.withArgs) + } + }) + }) + } +} + func TestKubectlCommandHandlesPlugins(t *testing.T) { tests := []struct { name string @@ -68,6 +169,12 @@ func TestKubectlCommandHandlesPlugins(t *testing.T) { expectPlugin: "", expectPluginArgs: []string{}, }, + { + name: "test that normal commands are able to be executed, when no plugin overshadows them and shadowing feature is not enabled", + args: []string{"kubectl", "create", "foo"}, + expectPlugin: "", + expectPluginArgs: []string{}, + }, { name: "test that a plugin executable is found based on command args", args: []string{"kubectl", "foo", "--bar"}, diff --git a/staging/src/k8s.io/kubectl/pkg/cmd/plugin/plugin_test.go b/staging/src/k8s.io/kubectl/pkg/cmd/plugin/plugin_test.go index 817d24fdd62..f7f598eee56 100644 --- a/staging/src/k8s.io/kubectl/pkg/cmd/plugin/plugin_test.go +++ b/staging/src/k8s.io/kubectl/pkg/cmd/plugin/plugin_test.go @@ -182,6 +182,7 @@ func TestPluginPathsAreValid(t *testing.T) { func TestListPlugins(t *testing.T) { pluginPath, _ := filepath.Abs("./testdata") expectPlugins := []string{ + filepath.Join(pluginPath, "kubectl-create-foo"), filepath.Join(pluginPath, "kubectl-foo"), filepath.Join(pluginPath, "kubectl-version"), } diff --git a/staging/src/k8s.io/kubectl/pkg/cmd/plugin/testdata/kubectl-create-foo b/staging/src/k8s.io/kubectl/pkg/cmd/plugin/testdata/kubectl-create-foo new file mode 100644 index 00000000000..a9a7b5e1e19 --- /dev/null +++ b/staging/src/k8s.io/kubectl/pkg/cmd/plugin/testdata/kubectl-create-foo @@ -0,0 +1,3 @@ +#!/bin/bash + +echo "I am plugin create foo" \ No newline at end of file diff --git a/staging/src/k8s.io/kubectl/pkg/cmd/util/helpers.go b/staging/src/k8s.io/kubectl/pkg/cmd/util/helpers.go index 9df43137b38..9083be79e7d 100644 --- a/staging/src/k8s.io/kubectl/pkg/cmd/util/helpers.go +++ b/staging/src/k8s.io/kubectl/pkg/cmd/util/helpers.go @@ -425,8 +425,9 @@ func GetPodRunningTimeoutFlag(cmd *cobra.Command) (time.Duration, error) { type FeatureGate string const ( - ApplySet FeatureGate = "KUBECTL_APPLYSET" - ExplainOpenapiV3 FeatureGate = "KUBECTL_EXPLAIN_OPENAPIV3" + ApplySet FeatureGate = "KUBECTL_APPLYSET" + ExplainOpenapiV3 FeatureGate = "KUBECTL_EXPLAIN_OPENAPIV3" + CmdPluginAsSubcommand FeatureGate = "KUBECTL_ENABLE_CMD_SHADOW" ) func (f FeatureGate) IsEnabled() bool {