Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

kubectl binary plugins #37499

Merged
merged 2 commits into from
Apr 28, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's worth mentioning from where these plugins are loaded, I think.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah it started like that but I didn't like the results, too clumsy for a command help. Ahead we'll probably want something better than relying on just the help as the default command run, then we can think of something better. And we will have proper docs which we can link to.

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