Skip to content

Commit

Permalink
Merge pull request #37499 from fabianofranz/kubectl_plugins
Browse files Browse the repository at this point in the history
Automatic merge from submit-queue

kubectl binary plugins

**What this PR does / why we need it**:

Introduces the ability to extend `kubectl` by adding third-party plugins that will be exposed through `kubectl`.

Plugins are executable commands written in any language. To be included as a plugin, a binary or script file has to

1. be located under one of the supported plugin path locations:
1.1 `~/.kubectl/plugins` dir
1.2. one or more directory set in the `KUBECTL_PLUGINS_PATH` env var
1.3. the `kubectl/plugins` dir under one or more directory set in the `XDG_DATA_DIRS` env var, which defaults to `/usr/local/share:/usr/share`
2. in any of the plugin path above, have a subfolder with the plugin file(s)
3. in the subfolder, contain at least a `plugin.yaml` file that describes the plugin

Example:

```
$ cat ~/.kube/plugins/myplugin/plugin.yaml
name: "myplugin"
shortDesc: "My plugin's short description"
command: "echo Hello plugins!"

$ kubectl myplugin
Hello plugins!
```

~~In case the plugin declares `tunnel: true`, the plugin engine will pass the `KUBECTL_PLUGIN_API_HOST` env var when calling the plugin binary. Plugins can then access the Kube REST API in "http://$KUBECTL_PLUGIN_API_HOST/api" using the same context currently in use by `kubectl`.~~

Test plugins are provided in `pkg/kubectl/plugins/examples`. Just copy (or symlink) the files to `~/.kube/plugins` to test.

**Which issue this PR fixes**:

Related to the discussions in the proposal document: #30086 and kubernetes/community#122.

**Release note**:
```release-note
Introduces the ability to extend kubectl by adding third-party plugins. Developer preview, please refer to the documentation for instructions about how to use it.
```
  • Loading branch information
Kubernetes Submit Queue authored Apr 28, 2017
2 parents 9fbefe3 + 2158473 commit d4ece0a
Show file tree
Hide file tree
Showing 33 changed files with 1,146 additions and 2 deletions.
3 changes: 3 additions & 0 deletions docs/.generated_docs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,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.1
docs/man/man1/kubectl-port-forward.1
docs/man/man1/kubectl-proxy.1
docs/man/man1/kubectl-replace.1
Expand Down Expand Up @@ -162,6 +163,7 @@ docs/user-guide/kubectl/kubectl_label.md
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_port-forward.md
docs/user-guide/kubectl/kubectl_proxy.md
docs/user-guide/kubectl/kubectl_replace.md
Expand Down Expand Up @@ -211,6 +213,7 @@ docs/yaml/kubectl/kubectl_label.yaml
docs/yaml/kubectl/kubectl_logs.yaml
docs/yaml/kubectl/kubectl_options.yaml
docs/yaml/kubectl/kubectl_patch.yaml
docs/yaml/kubectl/kubectl_plugin.yaml
docs/yaml/kubectl/kubectl_port-forward.yaml
docs/yaml/kubectl/kubectl_proxy.yaml
docs/yaml/kubectl/kubectl_replace.yaml
Expand Down
3 changes: 3 additions & 0 deletions docs/man/man1/kubectl-plugin.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.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.
3 changes: 3 additions & 0 deletions docs/yaml/kubectl/kubectl_plugin.yaml
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.
50 changes: 50 additions & 0 deletions hack/make-rules/test-cmd-util.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3679,5 +3679,55 @@ __EOF__
kube::test::get_object_assert csr "{{range.items}}{{$id_field}}{{end}}" ''
fi

###########
# Plugins #
###########
kube::log::status "Testing kubectl plugins"

# top-level plugin command
output_message=$(KUBECTL_PLUGINS_PATH=test/fixtures/pkg/kubectl/plugins kubectl -h 2>&1)
kube::test::if_has_string "${output_message}" 'plugin\s\+Runs a command-line plugin'

# no plugins
output_message=$(! kubectl plugin 2>&1)
kube::test::if_has_string "${output_message}" 'no plugins installed'

# single plugins path
output_message=$(KUBECTL_PLUGINS_PATH=test/fixtures/pkg/kubectl/plugins kubectl plugin 2>&1)
kube::test::if_has_string "${output_message}" 'echo\s\+Echoes for test-cmd'
kube::test::if_has_string "${output_message}" 'get\s\+The wonderful new plugin-based get!'
kube::test::if_has_string "${output_message}" 'error\s\+The tremendous plugin that always fails!'
kube::test::if_has_not_string "${output_message}" 'The hello plugin'
kube::test::if_has_not_string "${output_message}" 'Incomplete plugin'
kube::test::if_has_not_string "${output_message}" 'no plugins installed'

# multiple plugins path
output_message=$(KUBECTL_PLUGINS_PATH=test/fixtures/pkg/kubectl/plugins/:test/fixtures/pkg/kubectl/plugins2/ kubectl plugin -h 2>&1)
kube::test::if_has_string "${output_message}" 'echo\s\+Echoes for test-cmd'
kube::test::if_has_string "${output_message}" 'get\s\+The wonderful new plugin-based get!'
kube::test::if_has_string "${output_message}" 'error\s\+The tremendous plugin that always fails!'
kube::test::if_has_string "${output_message}" 'hello\s\+The hello plugin'
kube::test::if_has_not_string "${output_message}" 'Incomplete plugin'

# don't override existing commands
output_message=$(KUBECTL_PLUGINS_PATH=test/fixtures/pkg/kubectl/plugins/:test/fixtures/pkg/kubectl/plugins2/ kubectl get -h 2>&1)
kube::test::if_has_string "${output_message}" 'Display one or many resources'
kube::test::if_has_not_string "$output_message{output_message}" 'The wonderful new plugin-based get'

# plugin help
output_message=$(KUBECTL_PLUGINS_PATH=test/fixtures/pkg/kubectl/plugins/:test/fixtures/pkg/kubectl/plugins2/ kubectl plugin hello -h 2>&1)
kube::test::if_has_string "${output_message}" 'The hello plugin is a new plugin used by test-cmd to test multiple plugin locations.'
kube::test::if_has_string "${output_message}" 'Usage:'

# run plugin
output_message=$(KUBECTL_PLUGINS_PATH=test/fixtures/pkg/kubectl/plugins/:test/fixtures/pkg/kubectl/plugins2/ kubectl plugin hello 2>&1)
kube::test::if_has_string "${output_message}" '#hello#'
output_message=$(KUBECTL_PLUGINS_PATH=test/fixtures/pkg/kubectl/plugins/:test/fixtures/pkg/kubectl/plugins2/ kubectl plugin echo 2>&1)
kube::test::if_has_string "${output_message}" 'This plugin works!'
output_message=$(! KUBECTL_PLUGINS_PATH=test/fixtures/pkg/kubectl/plugins/ kubectl plugin hello 2>&1)
kube::test::if_has_string "${output_message}" 'unknown command'
output_message=$(! KUBECTL_PLUGINS_PATH=test/fixtures/pkg/kubectl/plugins/ kubectl plugin error 2>&1)
kube::test::if_has_string "${output_message}" 'error: exit status 1'

kube::test::clear_all
}
1 change: 1 addition & 0 deletions pkg/kubectl/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@ filegroup(
":package-srcs",
"//pkg/kubectl/cmd:all-srcs",
"//pkg/kubectl/metricsutil:all-srcs",
"//pkg/kubectl/plugins:all-srcs",
"//pkg/kubectl/resource:all-srcs",
"//pkg/kubectl/testing:all-srcs",
],
Expand Down
4 changes: 4 additions & 0 deletions pkg/kubectl/cmd/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ go_library(
"logs.go",
"options.go",
"patch.go",
"plugin.go",
"portforward.go",
"proxy.go",
"replace.go",
Expand Down Expand Up @@ -93,6 +94,7 @@ go_library(
"//pkg/kubectl/cmd/util:go_default_library",
"//pkg/kubectl/cmd/util/editor:go_default_library",
"//pkg/kubectl/metricsutil:go_default_library",
"//pkg/kubectl/plugins:go_default_library",
"//pkg/kubectl/resource:go_default_library",
"//pkg/kubelet/types:go_default_library",
"//pkg/printers:go_default_library",
Expand Down Expand Up @@ -173,6 +175,7 @@ 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 @@ -207,6 +210,7 @@ go_test(
"//pkg/kubectl:go_default_library",
"//pkg/kubectl/cmd/testing:go_default_library",
"//pkg/kubectl/cmd/util:go_default_library",
"//pkg/kubectl/plugins:go_default_library",
"//pkg/kubectl/resource:go_default_library",
"//pkg/printers:go_default_library",
"//pkg/printers/internalversion:go_default_library",
Expand Down
1 change: 1 addition & 0 deletions pkg/kubectl/cmd/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,7 @@ func NewKubectlCommand(f cmdutil.Factory, in io.Reader, out, err io.Writer) *cob
}

cmds.AddCommand(cmdconfig.NewCmdConfig(clientcmd.NewDefaultPathOptions(), out, err))
cmds.AddCommand(NewCmdPlugin(f, in, out, err))
cmds.AddCommand(NewCmdVersion(f, out))
cmds.AddCommand(NewCmdApiVersions(f, out))
cmds.AddCommand(NewCmdOptions())
Expand Down
96 changes: 96 additions & 0 deletions pkg/kubectl/cmd/plugin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/*
Copyright 2017 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package cmd

import (
"fmt"
"io"
"os"

"github.com/golang/glog"
"github.com/spf13/cobra"
"k8s.io/kubernetes/pkg/kubectl/cmd/templates"
cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
"k8s.io/kubernetes/pkg/kubectl/plugins"
"k8s.io/kubernetes/pkg/util/i18n"
)

var (
plugin_long = templates.LongDesc(`
Runs a command-line plugin.
Plugins are subcommands that are not part of the major command-line distribution
and can even be provided by third-parties. Please refer to the documentation and
examples for more information about how to install and write your own plugins.`)
)

// NewCmdPlugin creates the command that is the top-level for plugin commands.
func NewCmdPlugin(f cmdutil.Factory, in io.Reader, out, err io.Writer) *cobra.Command {
// Loads plugins and create commands for each plugin identified
loadedPlugins, loadErr := f.PluginLoader().Load()
if loadErr != nil {
glog.V(1).Infof("Unable to load plugins: %v", loadErr)
}

cmd := &cobra.Command{
Use: "plugin NAME",
Short: i18n.T("Runs a command-line plugin"),
Long: plugin_long,
Run: func(cmd *cobra.Command, args []string) {
if len(loadedPlugins) == 0 {
cmdutil.CheckErr(fmt.Errorf("no plugins installed."))
}
cmdutil.DefaultSubCommandRun(err)(cmd, args)
},
}

if len(loadedPlugins) > 0 {
pluginRunner := f.PluginRunner()
for _, p := range loadedPlugins {
cmd.AddCommand(NewCmdForPlugin(p, pluginRunner, in, out, err))
}
}

return cmd
}

// NewCmdForPlugin creates a command capable of running the provided plugin.
func NewCmdForPlugin(plugin *plugins.Plugin, runner plugins.PluginRunner, in io.Reader, out, errout io.Writer) *cobra.Command {
if !plugin.IsValid() {
return nil
}

return &cobra.Command{
Use: plugin.Name,
Short: plugin.ShortDesc,
Long: templates.LongDesc(plugin.LongDesc),
Example: templates.Examples(plugin.Example),
Run: func(cmd *cobra.Command, args []string) {
ctx := plugins.RunningContext{
In: in,
Out: out,
ErrOut: errout,
Args: args,
Env: os.Environ(),
WorkingDir: plugin.Dir,
}
if err := runner.Run(plugin, ctx); err != nil {
cmdutil.CheckErr(err)
}
},
}
}
111 changes: 111 additions & 0 deletions pkg/kubectl/cmd/plugin_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
/*
Copyright 2017 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package cmd

import (
"bytes"
"fmt"
"testing"

cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
"k8s.io/kubernetes/pkg/kubectl/plugins"
)

type mockPluginRunner struct {
success bool
}

func (r *mockPluginRunner) Run(p *plugins.Plugin, ctx plugins.RunningContext) error {
if !r.success {
return fmt.Errorf("oops %s", p.Name)
}
ctx.Out.Write([]byte(fmt.Sprintf("ok: %s", p.Name)))
return nil
}

func TestPluginCmd(t *testing.T) {
tests := []struct {
name string
plugin *plugins.Plugin
expectedSuccess bool
expectedNilCmd bool
}{
{
name: "success",
plugin: &plugins.Plugin{
Description: plugins.Description{
Name: "success",
ShortDesc: "The Test Plugin",
Command: "echo ok",
},
},
expectedSuccess: true,
},
{
name: "incomplete",
plugin: &plugins.Plugin{
Description: plugins.Description{
Name: "incomplete",
ShortDesc: "The Incomplete Plugin",
},
},
expectedNilCmd: true,
},
{
name: "failure",
plugin: &plugins.Plugin{
Description: plugins.Description{
Name: "failure",
ShortDesc: "The Failing Plugin",
Command: "false",
},
},
expectedSuccess: false,
},
}

for _, test := range tests {
inBuf := bytes.NewBuffer([]byte{})
outBuf := bytes.NewBuffer([]byte{})
errBuf := bytes.NewBuffer([]byte{})

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

runner := &mockPluginRunner{
success: test.expectedSuccess,
}

cmd := NewCmdForPlugin(test.plugin, runner, inBuf, outBuf, errBuf)
if cmd == nil {
if !test.expectedNilCmd {
t.Fatalf("%s: command was unexpectedly not registered", test.name)
}
continue
}
cmd.Run(cmd, []string{})

if test.expectedSuccess && outBuf.String() != fmt.Sprintf("ok: %s", test.plugin.Name) {
t.Errorf("%s: unexpected output: %q", test.name, outBuf.String())
}

if !test.expectedSuccess && errBuf.String() != fmt.Sprintf("error: oops %s", test.plugin.Name) {
t.Errorf("%s: unexpected err output: %q", test.name, errBuf.String())
}
}
}
6 changes: 6 additions & 0 deletions pkg/kubectl/cmd/templates/normalizers.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,17 @@ const Indentation = ` `

// LongDesc normalizes a command's long description to follow the conventions.
func LongDesc(s string) string {
if len(s) == 0 {
return s
}
return normalizer{s}.heredoc().markdown().trim().string
}

// Examples normalizes a command's examples to follow the conventions.
func Examples(s string) string {
if len(s) == 0 {
return s
}
return normalizer{s}.trim().indent().string
}

Expand Down
1 change: 1 addition & 0 deletions pkg/kubectl/cmd/testing/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ go_library(
"//pkg/kubectl:go_default_library",
"//pkg/kubectl/cmd/util:go_default_library",
"//pkg/kubectl/cmd/util/openapi:go_default_library",
"//pkg/kubectl/plugins:go_default_library",
"//pkg/kubectl/resource:go_default_library",
"//pkg/printers:go_default_library",
"//vendor/github.com/emicklei/go-restful/swagger:go_default_library",
Expand Down
9 changes: 9 additions & 0 deletions pkg/kubectl/cmd/testing/fake.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import (
"k8s.io/kubernetes/pkg/kubectl"
cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
"k8s.io/kubernetes/pkg/kubectl/cmd/util/openapi"
"k8s.io/kubernetes/pkg/kubectl/plugins"
"k8s.io/kubernetes/pkg/kubectl/resource"
"k8s.io/kubernetes/pkg/printers"
)
Expand Down Expand Up @@ -481,6 +482,14 @@ func (f *FakeFactory) SuggestedPodTemplateResources() []schema.GroupResource {
return []schema.GroupResource{}
}

func (f *FakeFactory) PluginLoader() plugins.PluginLoader {
return &plugins.DummyPluginLoader{}
}

func (f *FakeFactory) PluginRunner() plugins.PluginRunner {
return &plugins.ExecPluginRunner{}
}

type fakeMixedFactory struct {
cmdutil.Factory
tf *TestFactory
Expand Down
Loading

0 comments on commit d4ece0a

Please sign in to comment.