Skip to content

Commit

Permalink
add updated plugin mechanism
Browse files Browse the repository at this point in the history
  • Loading branch information
juanvallejo committed Aug 15, 2018
1 parent d745508 commit 4bdc636
Show file tree
Hide file tree
Showing 37 changed files with 403 additions and 1,782 deletions.
2 changes: 2 additions & 0 deletions docs/.generated_docs
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,7 @@ docs/man/man1/kubectl-label.1
docs/man/man1/kubectl-logs.1
docs/man/man1/kubectl-options.1
docs/man/man1/kubectl-patch.1
docs/man/man1/kubectl-plugin-list.1
docs/man/man1/kubectl-plugin.1
docs/man/man1/kubectl-port-forward.1
docs/man/man1/kubectl-proxy.1
Expand Down Expand Up @@ -346,6 +347,7 @@ docs/user-guide/kubectl/kubectl_logs.md
docs/user-guide/kubectl/kubectl_options.md
docs/user-guide/kubectl/kubectl_patch.md
docs/user-guide/kubectl/kubectl_plugin.md
docs/user-guide/kubectl/kubectl_plugin_list.md
docs/user-guide/kubectl/kubectl_port-forward.md
docs/user-guide/kubectl/kubectl_proxy.md
docs/user-guide/kubectl/kubectl_replace.md
Expand Down
3 changes: 3 additions & 0 deletions docs/man/man1/kubectl-plugin-list.1
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
This file is autogenerated, but we've stopped checking such files into the
repository to reduce the need for rebases. Please run hack/generate-docs.sh to
populate this file.
3 changes: 3 additions & 0 deletions docs/user-guide/kubectl/kubectl_plugin_list.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
This file is autogenerated, but we've stopped checking such files into the
repository to reduce the need for rebases. Please run hack/generate-docs.sh to
populate this file.
1 change: 0 additions & 1 deletion pkg/kubectl/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,6 @@ filegroup(
"//pkg/kubectl/explain:all-srcs",
"//pkg/kubectl/genericclioptions:all-srcs",
"//pkg/kubectl/metricsutil:all-srcs",
"//pkg/kubectl/plugins:all-srcs",
"//pkg/kubectl/polymorphichelpers:all-srcs",
"//pkg/kubectl/proxy:all-srcs",
"//pkg/kubectl/scheme:all-srcs",
Expand Down
3 changes: 0 additions & 3 deletions pkg/kubectl/cmd/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,6 @@ go_library(
"//pkg/kubectl/genericclioptions/printers:go_default_library",
"//pkg/kubectl/genericclioptions/resource:go_default_library",
"//pkg/kubectl/metricsutil:go_default_library",
"//pkg/kubectl/plugins:go_default_library",
"//pkg/kubectl/polymorphichelpers:go_default_library",
"//pkg/kubectl/proxy:go_default_library",
"//pkg/kubectl/scheme:go_default_library",
Expand Down Expand Up @@ -171,7 +170,6 @@ go_test(
"label_test.go",
"logs_test.go",
"patch_test.go",
"plugin_test.go",
"portforward_test.go",
"replace_test.go",
"rollingupdate_test.go",
Expand Down Expand Up @@ -202,7 +200,6 @@ go_test(
"//pkg/kubectl/genericclioptions:go_default_library",
"//pkg/kubectl/genericclioptions/printers:go_default_library",
"//pkg/kubectl/genericclioptions/resource:go_default_library",
"//pkg/kubectl/plugins:go_default_library",
"//pkg/kubectl/polymorphichelpers:go_default_library",
"//pkg/kubectl/scheme:go_default_library",
"//pkg/kubectl/util/i18n:go_default_library",
Expand Down
102 changes: 100 additions & 2 deletions pkg/kubectl/cmd/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ import (
"fmt"
"io"
"os"
"os/exec"
"runtime"
"strings"
"syscall"

"github.com/spf13/cobra"

"k8s.io/apimachinery/pkg/api/meta"
utilflag "k8s.io/apiserver/pkg/util/flag"
Expand All @@ -36,7 +42,6 @@ import (
"k8s.io/kubernetes/pkg/kubectl/cmd/wait"
"k8s.io/kubernetes/pkg/kubectl/util/i18n"

"github.com/spf13/cobra"
"k8s.io/kubernetes/pkg/kubectl/genericclioptions"
)

Expand Down Expand Up @@ -257,7 +262,100 @@ var (
)

func NewDefaultKubectlCommand() *cobra.Command {
return NewKubectlCommand(os.Stdin, os.Stdout, os.Stderr)
return NewDefaultKubectlCommandWithArgs(&defaultPluginHandler{}, os.Args, os.Stdin, os.Stdout, os.Stderr)
}

func NewDefaultKubectlCommandWithArgs(pluginHandler PluginHandler, args []string, in io.Reader, out, errout io.Writer) *cobra.Command {
cmd := NewKubectlCommand(in, out, errout)

if pluginHandler == nil {
return cmd
}

if len(args) > 1 {
cmdPathPieces := args[1:]

// only look for suitable extension executables if
// the specified command does not already exist
if _, _, err := cmd.Find(cmdPathPieces); err != nil {
if err := handleEndpointExtensions(pluginHandler, cmdPathPieces); err != nil {
fmt.Fprintf(errout, "%v\n", err)
os.Exit(1)
}
}
}

return cmd
}

// PluginHandler is capable of parsing command line arguments
// and performing executable filename lookups to search
// for valid plugin files, and execute found plugins.
type PluginHandler interface {
// Lookup receives a potential filename and returns
// a full or relative path to an executable, if one
// exists at the given filename, or an error.
Lookup(filename string) (string, error)
// Execute receives an executable's filepath, a slice
// of arguments, and a slice of environment variables
// to relay to the executable.
Execute(executablePath string, cmdArgs, environment []string) error
}

type defaultPluginHandler struct{}

// Lookup implements PluginHandler
func (h *defaultPluginHandler) Lookup(filename string) (string, error) {
// if on Windows, append the "exe" extension
// to the filename that we are looking up.
if runtime.GOOS == "windows" {
filename = filename + ".exe"
}

return exec.LookPath(filename)
}

// Execute implements PluginHandler
func (h *defaultPluginHandler) Execute(executablePath string, cmdArgs, environment []string) error {
return syscall.Exec(executablePath, cmdArgs, environment)
}

func handleEndpointExtensions(pluginHandler PluginHandler, cmdArgs []string) error {
remainingArgs := []string{} // all "non-flag" arguments

for idx := range cmdArgs {
if strings.HasPrefix(cmdArgs[idx], "-") {
break
}
remainingArgs = append(remainingArgs, strings.Replace(cmdArgs[idx], "-", "_", -1))
}

foundBinaryPath := ""

// attempt to find binary, starting at longest possible name with given cmdArgs
for len(remainingArgs) > 0 {
path, err := pluginHandler.Lookup(fmt.Sprintf("kubectl-%s", strings.Join(remainingArgs, "-")))
if err != nil || len(path) == 0 {
remainingArgs = remainingArgs[:len(remainingArgs)-1]
continue
}

foundBinaryPath = path
break
}

if len(foundBinaryPath) == 0 {
return nil
}

// invoke cmd binary relaying the current environment and args given
// remainingArgs will always have at least one element.
// execve will make remainingArgs[0] the "binary name".
if err := pluginHandler.Execute(foundBinaryPath, append([]string{foundBinaryPath}, cmdArgs[len(remainingArgs):]...), os.Environ()); err != nil {
return err
}

return nil
}

// NewKubectlCommand creates the `kubectl` command and its nested children.
Expand Down
105 changes: 105 additions & 0 deletions pkg/kubectl/cmd/cmd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package cmd
import (
"bytes"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
Expand All @@ -37,6 +38,7 @@ import (
"k8s.io/kubernetes/pkg/api/testapi"
apitesting "k8s.io/kubernetes/pkg/api/testing"
cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
"k8s.io/kubernetes/pkg/kubectl/genericclioptions"
"k8s.io/kubernetes/pkg/kubectl/scheme"
)

Expand Down Expand Up @@ -213,3 +215,106 @@ func Test_deprecatedAlias(t *testing.T) {
t.Errorf("original function doesn't appear to have been called by alias")
}
}

func TestKubectlCommandHandlesPlugins(t *testing.T) {
tests := []struct {
name string
args []string
expectPlugin string
expectPluginArgs []string
expectError string
}{
{
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: "testdata/plugin/kubectl-foo",
expectPluginArgs: []string{"foo", "--bar"},
},
{
name: "test that a plugin does not execute over an existing command by the same name",
args: []string{"kubectl", "version"},
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
pluginsHandler := &testPluginHandler{
pluginsDirectory: "testdata/plugin",
}
_, in, out, errOut := genericclioptions.NewTestIOStreams()

cmdutil.BehaviorOnFatal(func(str string, code int) {
errOut.Write([]byte(str))
})

root := NewDefaultKubectlCommandWithArgs(pluginsHandler, test.args, in, out, errOut)
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: expedcted %q, got %q", test.expectPlugin, pluginsHandler.executedPlugin)
}

if len(pluginsHandler.withArgs) != len(test.expectPluginArgs) {
t.Fatalf("unexpected plugin execution args: expedcted %q, got %q", test.expectPluginArgs, pluginsHandler.withArgs)
}
})
}
}

type testPluginHandler struct {
pluginsDirectory string

// execution results
executedPlugin string
withArgs []string
withEnv []string

err error
}

func (h *testPluginHandler) Lookup(filename string) (string, error) {
dir, err := os.Stat(h.pluginsDirectory)
if err != nil {
h.err = err
return "", err
}

if !dir.IsDir() {
h.err = fmt.Errorf("expected %q to be a directory", h.pluginsDirectory)
return "", h.err
}

plugins, err := ioutil.ReadDir(h.pluginsDirectory)
if err != nil {
h.err = err
return "", err
}

for _, p := range plugins {
if p.Name() == filename {
return fmt.Sprintf("%s/%s", h.pluginsDirectory, p.Name()), nil
}
}

h.err = fmt.Errorf("unable to find a plugin executable %q", filename)
return "", h.err
}

func (h *testPluginHandler) Execute(executablePath string, cmdArgs, env []string) error {
h.executedPlugin = executablePath
h.withArgs = cmdArgs
h.withEnv = env
return nil
}

0 comments on commit 4bdc636

Please sign in to comment.