Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
K
k3s
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Registry
Registry
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Jacklull
k3s
Commits
4bdc6363
Unverified
Commit
4bdc6363
authored
Aug 01, 2018
by
juanvallejo
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
add updated plugin mechanism
parent
d745508c
Hide whitespace changes
Inline
Side-by-side
Showing
37 changed files
with
403 additions
and
1782 deletions
+403
-1782
.generated_docs
docs/.generated_docs
+2
-0
kubectl-plugin-list.1
docs/man/man1/kubectl-plugin-list.1
+3
-0
kubectl_plugin_list.md
docs/user-guide/kubectl/kubectl_plugin_list.md
+3
-0
BUILD
pkg/kubectl/BUILD
+0
-1
BUILD
pkg/kubectl/cmd/BUILD
+0
-3
cmd.go
pkg/kubectl/cmd/cmd.go
+100
-2
cmd_test.go
pkg/kubectl/cmd/cmd_test.go
+105
-0
plugin.go
pkg/kubectl/cmd/plugin.go
+150
-108
plugin_test.go
pkg/kubectl/cmd/plugin_test.go
+0
-115
run_test.go
pkg/kubectl/cmd/run_test.go
+4
-0
kubectl-foo
pkg/kubectl/cmd/testdata/plugin/kubectl-foo
+3
-0
kubectl-version
pkg/kubectl/cmd/testdata/plugin/kubectl-version
+4
-0
BUILD
pkg/kubectl/plugins/BUILD
+0
-53
env.go
pkg/kubectl/plugins/env.go
+0
-161
env_test.go
pkg/kubectl/plugins/env_test.go
+0
-186
aging.rb
pkg/kubectl/plugins/examples/aging/aging.rb
+0
-70
plugin.yaml
pkg/kubectl/plugins/examples/aging/plugin.yaml
+0
-5
plugin.yaml
pkg/kubectl/plugins/examples/hello/plugin.yaml
+0
-3
loader.go
pkg/kubectl/plugins/loader.go
+0
-204
loader_test.go
pkg/kubectl/plugins/loader_test.go
+0
-262
plugins.go
pkg/kubectl/plugins/plugins.go
+0
-123
plugins_test.go
pkg/kubectl/plugins/plugins_test.go
+0
-181
runner.go
pkg/kubectl/plugins/runner.go
+0
-72
runner_test.go
pkg/kubectl/plugins/runner_test.go
+0
-83
plugins.sh
test/cmd/plugins.sh
+19
-70
plugin.yaml
test/fixtures/pkg/kubectl/plugins/echo/plugin.yaml
+0
-4
env.sh
test/fixtures/pkg/kubectl/plugins/env/env.sh
+0
-17
plugin.yaml
test/fixtures/pkg/kubectl/plugins/env/plugin.yaml
+0
-12
plugin.yaml
test/fixtures/pkg/kubectl/plugins/error/plugin.yaml
+0
-3
kubectl-foo
test/fixtures/pkg/kubectl/plugins/foo/kubectl-foo
+3
-0
plugin.yaml
test/fixtures/pkg/kubectl/plugins/get/plugin.yaml
+0
-3
plugin.yaml
test/fixtures/pkg/kubectl/plugins/incomplete/plugin.yaml
+0
-2
kubectl-foo
test/fixtures/pkg/kubectl/plugins/kubectl-foo
+3
-0
plugin.yaml
test/fixtures/pkg/kubectl/plugins/tree/plugin.yaml
+0
-13
kubectl-version
test/fixtures/pkg/kubectl/plugins/version/kubectl-version
+4
-0
hello.sh
test/fixtures/pkg/kubectl/plugins2/hello/hello.sh
+0
-19
plugin.yaml
test/fixtures/pkg/kubectl/plugins2/hello/plugin.yaml
+0
-7
No files found.
docs/.generated_docs
View file @
4bdc6363
...
...
@@ -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
...
...
@@ -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
...
...
docs/man/man1/kubectl-plugin-list.1
0 → 100644
View file @
4bdc6363
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.
docs/user-guide/kubectl/kubectl_plugin_list.md
0 → 100644
View file @
4bdc6363
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.
pkg/kubectl/BUILD
View file @
4bdc6363
...
...
@@ -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",
...
...
pkg/kubectl/cmd/BUILD
View file @
4bdc6363
...
...
@@ -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",
...
...
@@ -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",
...
...
@@ -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",
...
...
pkg/kubectl/cmd/cmd.go
View file @
4bdc6363
...
...
@@ -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"
...
...
@@ -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"
)
...
...
@@ -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.
...
...
pkg/kubectl/cmd/cmd_test.go
View file @
4bdc6363
...
...
@@ -19,6 +19,7 @@ package cmd
import
(
"bytes"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
...
...
@@ -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"
)
...
...
@@ -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
}
pkg/kubectl/cmd/plugin.go
View file @
4bdc6363
...
...
@@ -18,170 +18,212 @@ package cmd
import
(
"fmt"
"io/ioutil"
"os"
"os/exec"
"syscall"
"path/filepath"
"runtime"
"strings"
"github.com/golang/glog"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"k8s.io/kubernetes/pkg/kubectl/cmd/templates"
cmdutil
"k8s.io/kubernetes/pkg/kubectl/cmd/util"
"k8s.io/kubernetes/pkg/kubectl/genericclioptions"
"k8s.io/kubernetes/pkg/kubectl/plugins"
"k8s.io/kubernetes/pkg/kubectl/util/i18n"
)
var
(
plugin_long
=
templates
.
LongDesc
(
`
Runs a command-line plugin.
Provides utilities for interacting with plugins.
Plugins provide extended functionality that is not part of the major command-line distribution.
Please refer to the documentation and examples for more information about how write your own plugins.`
)
plugin_list_long
=
templates
.
LongDesc
(
`
List all available plugin files on a user's PATH.
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.`
)
Available plugin files are those that are:
- executable
- anywhere on the user's PATH
- begin with "kubectl-"
`
)
)
// NewCmdPlugin creates the command that is the top-level for plugin commands.
func
NewCmdPlugin
(
f
cmdutil
.
Factory
,
streams
genericclioptions
.
IOStreams
)
*
cobra
.
Command
{
// Loads plugins and create commands for each plugin identified
loadedPlugins
,
loadErr
:=
pluginLoader
()
.
Load
()
if
loadErr
!=
nil
{
glog
.
V
(
1
)
.
Infof
(
"Unable to load plugins: %v"
,
loadErr
)
}
cmd
:=
&
cobra
.
Command
{
Use
:
"plugin
NAME
"
,
Use
:
"plugin
[flags]
"
,
DisableFlagsInUseLine
:
true
,
Short
:
i18n
.
T
(
"
Runs a command-line plugin
"
),
Short
:
i18n
.
T
(
"
Provides utilities for interacting with plugins.
"
),
Long
:
plugin_long
,
Run
:
func
(
cmd
*
cobra
.
Command
,
args
[]
string
)
{
if
len
(
loadedPlugins
)
==
0
{
cmdutil
.
CheckErr
(
fmt
.
Errorf
(
"no plugins installed."
))
}
cmdutil
.
DefaultSubCommandRun
(
streams
.
ErrOut
)(
cmd
,
args
)
},
}
if
len
(
loadedPlugins
)
>
0
{
pluginRunner
:=
pluginRunner
()
for
_
,
p
:=
range
loadedPlugins
{
cmd
.
AddCommand
(
NewCmdForPlugin
(
f
,
p
,
pluginRunner
,
streams
))
}
}
cmd
.
AddCommand
(
NewCmdPluginList
(
f
,
streams
))
return
cmd
}
// NewCmdForPlugin creates a command capable of running the provided plugin.
func
NewCmdForPlugin
(
f
cmdutil
.
Factory
,
plugin
*
plugins
.
Plugin
,
runner
plugins
.
PluginRunner
,
streams
genericclioptions
.
IOStreams
)
*
cobra
.
Command
{
if
!
plugin
.
IsValid
()
{
return
nil
type
PluginListOptions
struct
{
Verifier
PathVerifier
NameOnly
bool
genericclioptions
.
IOStreams
}
// NewCmdPluginList provides a way to list all plugin executables visible to kubectl
func
NewCmdPluginList
(
f
cmdutil
.
Factory
,
streams
genericclioptions
.
IOStreams
)
*
cobra
.
Command
{
o
:=
&
PluginListOptions
{
IOStreams
:
streams
,
}
cmd
:=
&
cobra
.
Command
{
Use
:
plugin
.
Name
,
Short
:
plugin
.
ShortDesc
,
Long
:
templates
.
LongDesc
(
plugin
.
LongDesc
),
Example
:
templates
.
Examples
(
plugin
.
Example
),
Use
:
"list"
,
Short
:
"list all visible plugin executables on a user's PATH"
,
Long
:
plugin_list_long
,
Run
:
func
(
cmd
*
cobra
.
Command
,
args
[]
string
)
{
if
len
(
plugin
.
Command
)
==
0
{
cmdutil
.
DefaultSubCommandRun
(
streams
.
ErrOut
)(
cmd
,
args
)
return
cmdutil
.
CheckErr
(
o
.
Complete
(
cmd
))
cmdutil
.
CheckErr
(
o
.
Run
())
},
}
cmd
.
Flags
()
.
BoolVar
(
&
o
.
NameOnly
,
"name-only"
,
o
.
NameOnly
,
"If true, display only the binary name of each plugin, rather than its full path"
)
return
cmd
}
func
(
o
*
PluginListOptions
)
Complete
(
cmd
*
cobra
.
Command
)
error
{
o
.
Verifier
=
&
CommandOverrideVerifier
{
root
:
cmd
.
Root
(),
seenPlugins
:
make
(
map
[
string
]
string
,
0
),
}
return
nil
}
func
(
o
*
PluginListOptions
)
Run
()
error
{
path
:=
"PATH"
if
runtime
.
GOOS
==
"windows"
{
path
=
"path"
}
pluginsFound
:=
false
isFirstFile
:=
true
pluginWarnings
:=
0
for
_
,
dir
:=
range
filepath
.
SplitList
(
os
.
Getenv
(
path
))
{
files
,
err
:=
ioutil
.
ReadDir
(
dir
)
if
err
!=
nil
{
continue
}
for
_
,
f
:=
range
files
{
if
f
.
IsDir
()
{
continue
}
if
!
strings
.
HasPrefix
(
f
.
Name
(),
"kubectl-"
)
{
continue
}
envProvider
:=
&
plugins
.
MultiEnvProvider
{
&
plugins
.
PluginCallerEnvProvider
{},
&
plugins
.
OSEnvProvider
{},
&
plugins
.
PluginDescriptorEnvProvider
{
Plugin
:
plugin
,
},
&
flagsPluginEnvProvider
{
cmd
:
cmd
,
},
&
factoryAttrsPluginEnvProvider
{
factory
:
f
,
},
if
isFirstFile
{
fmt
.
Fprintf
(
o
.
ErrOut
,
"The following kubectl-compatible plugins are available:
\n\n
"
)
pluginsFound
=
true
isFirstFile
=
false
}
runningContext
:=
plugins
.
RunningContext
{
IOStreams
:
streams
,
Args
:
args
,
EnvProvider
:
envProvider
,
WorkingDir
:
plugin
.
Dir
,
pluginPath
:=
f
.
Name
()
if
!
o
.
NameOnly
{
pluginPath
=
filepath
.
Join
(
dir
,
pluginPath
)
}
if
err
:=
runner
.
Run
(
plugin
,
runningContext
);
err
!=
nil
{
if
exiterr
,
ok
:=
err
.
(
*
exec
.
ExitError
);
ok
{
// check for (and exit with) the correct exit code
// from a failed plugin command execution
if
status
,
ok
:=
exiterr
.
Sys
()
.
(
syscall
.
WaitStatus
);
ok
{
fmt
.
Fprintf
(
streams
.
ErrOut
,
"error: %v
\n
"
,
err
)
os
.
Exit
(
status
.
ExitStatus
())
}
fmt
.
Fprintf
(
o
.
Out
,
"%s
\n
"
,
pluginPath
)
if
errs
:=
o
.
Verifier
.
Verify
(
filepath
.
Join
(
dir
,
f
.
Name
()));
len
(
errs
)
!=
0
{
for
_
,
err
:=
range
errs
{
fmt
.
Fprintf
(
o
.
ErrOut
,
" - %s
\n
"
,
err
)
pluginWarnings
++
}
cmdutil
.
CheckErr
(
err
)
}
}
,
}
}
for
_
,
flag
:=
range
plugin
.
Flags
{
cmd
.
Flags
()
.
StringP
(
flag
.
Name
,
flag
.
Shorthand
,
flag
.
DefValue
,
flag
.
Desc
)
if
!
pluginsFound
{
return
fmt
.
Errorf
(
"error: unable to find any kubectl plugins in your PATH"
)
}
for
_
,
childPlugin
:=
range
plugin
.
Tree
{
cmd
.
AddCommand
(
NewCmdForPlugin
(
f
,
childPlugin
,
runner
,
streams
))
if
pluginWarnings
>
0
{
fmt
.
Fprintln
(
o
.
ErrOut
)
if
pluginWarnings
==
1
{
return
fmt
.
Errorf
(
"one plugin warning was found"
)
}
return
fmt
.
Errorf
(
"%v plugin warnings were found"
,
pluginWarnings
)
}
return
cmd
return
nil
}
type
flagsPluginEnvProvider
struct
{
cmd
*
cobra
.
Command
// pathVerifier receives a path and determines if it is valid or not
type
PathVerifier
interface
{
// Verify determines if a given path is valid
Verify
(
path
string
)
[]
error
}
func
(
p
*
flagsPluginEnvProvider
)
Env
()
(
plugins
.
EnvList
,
error
)
{
globalPrefix
:=
"KUBECTL_PLUGINS_GLOBAL_FLAG_"
env
:=
plugins
.
EnvList
{}
p
.
cmd
.
InheritedFlags
()
.
VisitAll
(
func
(
flag
*
pflag
.
Flag
)
{
env
=
append
(
env
,
plugins
.
FlagToEnv
(
flag
,
globalPrefix
))
})
localPrefix
:=
"KUBECTL_PLUGINS_LOCAL_FLAG_"
p
.
cmd
.
LocalFlags
()
.
VisitAll
(
func
(
flag
*
pflag
.
Flag
)
{
env
=
append
(
env
,
plugins
.
FlagToEnv
(
flag
,
localPrefix
))
})
return
env
,
nil
type
CommandOverrideVerifier
struct
{
root
*
cobra
.
Command
seenPlugins
map
[
string
]
string
}
type
factoryAttrsPluginEnvProvider
struct
{
factory
cmdutil
.
Factory
// Verify implements PathVerifier and determines if a given path
// is valid depending on whether or not it overwrites an existing
// kubectl command path, or a previously seen plugin.
func
(
v
*
CommandOverrideVerifier
)
Verify
(
path
string
)
[]
error
{
if
v
.
root
==
nil
{
return
[]
error
{
fmt
.
Errorf
(
"unable to verify path with nil root"
)}
}
// extract the plugin binary name
segs
:=
strings
.
Split
(
path
,
"/"
)
binName
:=
segs
[
len
(
segs
)
-
1
]
cmdPath
:=
strings
.
Split
(
binName
,
"-"
)
if
len
(
cmdPath
)
>
1
{
// the first argument is always "kubectl" for a plugin binary
cmdPath
=
cmdPath
[
1
:
]
}
errors
:=
[]
error
{}
if
isExec
,
err
:=
isExecutable
(
path
);
err
==
nil
&&
!
isExec
{
errors
=
append
(
errors
,
fmt
.
Errorf
(
"warning: %s identified as a kubectl plugin, but it is not executable"
,
path
))
}
else
if
err
!=
nil
{
errors
=
append
(
errors
,
fmt
.
Errorf
(
"error: unable to identify %s as an executable file: %v"
,
path
,
err
))
}
if
existingPath
,
ok
:=
v
.
seenPlugins
[
binName
];
ok
{
errors
=
append
(
errors
,
fmt
.
Errorf
(
"warning: %s is overshadowed by a similarly named plugin: %s"
,
path
,
existingPath
))
}
else
{
v
.
seenPlugins
[
binName
]
=
path
}
if
cmd
,
_
,
err
:=
v
.
root
.
Find
(
cmdPath
);
err
==
nil
{
errors
=
append
(
errors
,
fmt
.
Errorf
(
"warning: %s overwrites existing command: %q"
,
binName
,
cmd
.
CommandPath
()))
}
return
errors
}
func
(
p
*
factoryAttrsPluginEnvProvider
)
Env
()
(
plugins
.
EnvList
,
error
)
{
cmdNamespace
,
_
,
err
:=
p
.
factory
.
ToRawKubeConfigLoader
()
.
Namespace
(
)
func
isExecutable
(
fullPath
string
)
(
bool
,
error
)
{
info
,
err
:=
os
.
Stat
(
fullPath
)
if
err
!=
nil
{
return
plugins
.
EnvList
{}
,
err
return
false
,
err
}
return
plugins
.
EnvList
{
plugins
.
Env
{
N
:
"KUBECTL_PLUGINS_CURRENT_NAMESPACE"
,
V
:
cmdNamespace
},
},
nil
}
// pluginLoader loads plugins from a path set by the KUBECTL_PLUGINS_PATH env var.
// If this env var is not set, it defaults to
// "~/.kube/plugins", plus
// "./kubectl/plugins" directory under the "data dir" directory specified by the XDG
// system directory structure spec for the given platform.
func
pluginLoader
()
plugins
.
PluginLoader
{
if
len
(
os
.
Getenv
(
"KUBECTL_PLUGINS_PATH"
))
>
0
{
return
plugins
.
KubectlPluginsPathPluginLoader
()
if
runtime
.
GOOS
==
"windows"
{
if
strings
.
HasSuffix
(
info
.
Name
(),
".exe"
)
{
return
true
,
nil
}
return
false
,
nil
}
return
plugins
.
TolerantMultiPluginLoader
{
plugins
.
XDGDataDirsPluginLoader
(),
plugins
.
UserDirPluginLoader
(),
if
m
:=
info
.
Mode
();
!
m
.
IsDir
()
&&
m
&
0111
!=
0
{
return
true
,
nil
}
}
func
pluginRunner
()
plugins
.
PluginRunner
{
return
&
plugins
.
ExecPluginRunner
{}
return
false
,
nil
}
pkg/kubectl/cmd/plugin_test.go
deleted
100644 → 0
View file @
d745508c
/*
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"
"testing"
cmdtesting
"k8s.io/kubernetes/pkg/kubectl/cmd/testing"
cmdutil
"k8s.io/kubernetes/pkg/kubectl/cmd/util"
"k8s.io/kubernetes/pkg/kubectl/genericclioptions"
"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
{
t
.
Run
(
test
.
name
,
func
(
t
*
testing
.
T
)
{
streams
,
_
,
outBuf
,
errBuf
:=
genericclioptions
.
NewTestIOStreams
()
cmdutil
.
BehaviorOnFatal
(
func
(
str
string
,
code
int
)
{
errBuf
.
Write
([]
byte
(
str
))
})
runner
:=
&
mockPluginRunner
{
success
:
test
.
expectedSuccess
,
}
f
:=
cmdtesting
.
NewTestFactory
()
defer
f
.
Cleanup
()
cmd
:=
NewCmdForPlugin
(
f
,
test
.
plugin
,
runner
,
streams
)
if
cmd
==
nil
{
if
!
test
.
expectedNilCmd
{
t
.
Fatalf
(
"%s: command was unexpectedly not registered"
,
test
.
name
)
}
return
}
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
())
}
})
}
}
pkg/kubectl/cmd/run_test.go
View file @
4bdc6363
...
...
@@ -510,6 +510,10 @@ func TestRunValidations(t *testing.T) {
tf
.
ClientConfigVal
=
defaultClientConfig
()
streams
,
_
,
_
,
bufErr
:=
genericclioptions
.
NewTestIOStreams
()
cmdutil
.
BehaviorOnFatal
(
func
(
str
string
,
code
int
)
{
bufErr
.
Write
([]
byte
(
str
))
})
cmd
:=
NewCmdRun
(
tf
,
streams
)
for
flagName
,
flagValue
:=
range
test
.
flags
{
cmd
.
Flags
()
.
Set
(
flagName
,
flagValue
)
...
...
pkg/kubectl/cmd/testdata/plugin/kubectl-foo
0 → 100755
View file @
4bdc6363
#!/bin/bash
echo
"I am plugin foo"
pkg/kubectl/cmd/testdata/plugin/kubectl-version
0 → 100755
View file @
4bdc6363
#!/bin/bash
# This plugin is a no-op and is used to test plugins
# that overshadow existing kubectl commands
pkg/kubectl/plugins/BUILD
deleted
100644 → 0
View file @
d745508c
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
"go_test",
)
go_library(
name = "go_default_library",
srcs = [
"env.go",
"loader.go",
"plugins.go",
"runner.go",
],
importpath = "k8s.io/kubernetes/pkg/kubectl/plugins",
deps = [
"//pkg/kubectl/genericclioptions:go_default_library",
"//staging/src/k8s.io/client-go/tools/clientcmd:go_default_library",
"//vendor/github.com/ghodss/yaml:go_default_library",
"//vendor/github.com/golang/glog:go_default_library",
"//vendor/github.com/spf13/pflag:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
)
go_test(
name = "go_default_test",
srcs = [
"env_test.go",
"loader_test.go",
"plugins_test.go",
"runner_test.go",
],
embed = [":go_default_library"],
deps = [
"//pkg/kubectl/genericclioptions:go_default_library",
"//vendor/github.com/spf13/pflag:go_default_library",
],
)
pkg/kubectl/plugins/env.go
deleted
100644 → 0
View file @
d745508c
/*
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
plugins
import
(
"fmt"
"os"
"strings"
"github.com/spf13/pflag"
)
// Env represents an environment variable with its name and value.
type
Env
struct
{
N
string
V
string
}
// String returns "name=value" string.
func
(
e
Env
)
String
()
string
{
return
fmt
.
Sprintf
(
"%s=%s"
,
e
.
N
,
e
.
V
)
}
// EnvList is a list of Env.
type
EnvList
[]
Env
// Slice returns a slice of "name=value" strings.
func
(
e
EnvList
)
Slice
()
[]
string
{
envs
:=
[]
string
{}
for
_
,
env
:=
range
e
{
envs
=
append
(
envs
,
env
.
String
())
}
return
envs
}
// Merge converts "name=value" strings into Env values and merges them into e.
func
(
e
EnvList
)
Merge
(
s
...
string
)
EnvList
{
newList
:=
e
newList
=
append
(
newList
,
fromSlice
(
s
)
...
)
return
newList
}
// EnvProvider provides the environment in which the plugin will run.
type
EnvProvider
interface
{
// Env returns the env list.
Env
()
(
EnvList
,
error
)
}
// MultiEnvProvider satisfies the EnvProvider interface for multiple env providers.
type
MultiEnvProvider
[]
EnvProvider
// Env returns the combined env list of multiple env providers, returns on first error.
func
(
p
MultiEnvProvider
)
Env
()
(
EnvList
,
error
)
{
env
:=
EnvList
{}
for
_
,
provider
:=
range
p
{
pEnv
,
err
:=
provider
.
Env
()
if
err
!=
nil
{
return
EnvList
{},
err
}
env
=
append
(
env
,
pEnv
...
)
}
return
env
,
nil
}
// PluginCallerEnvProvider satisfies the EnvProvider interface.
type
PluginCallerEnvProvider
struct
{}
// Env returns env with the path to the caller binary (usually full path to 'kubectl').
func
(
p
*
PluginCallerEnvProvider
)
Env
()
(
EnvList
,
error
)
{
caller
,
err
:=
os
.
Executable
()
if
err
!=
nil
{
return
EnvList
{},
err
}
return
EnvList
{
{
"KUBECTL_PLUGINS_CALLER"
,
caller
},
},
nil
}
// PluginDescriptorEnvProvider satisfies the EnvProvider interface.
type
PluginDescriptorEnvProvider
struct
{
Plugin
*
Plugin
}
// Env returns env with information about the running plugin.
func
(
p
*
PluginDescriptorEnvProvider
)
Env
()
(
EnvList
,
error
)
{
if
p
.
Plugin
==
nil
{
return
[]
Env
{},
fmt
.
Errorf
(
"plugin not present to extract env"
)
}
prefix
:=
"KUBECTL_PLUGINS_DESCRIPTOR_"
env
:=
EnvList
{
{
prefix
+
"NAME"
,
p
.
Plugin
.
Name
},
{
prefix
+
"SHORT_DESC"
,
p
.
Plugin
.
ShortDesc
},
{
prefix
+
"LONG_DESC"
,
p
.
Plugin
.
LongDesc
},
{
prefix
+
"EXAMPLE"
,
p
.
Plugin
.
Example
},
{
prefix
+
"COMMAND"
,
p
.
Plugin
.
Command
},
}
return
env
,
nil
}
// OSEnvProvider satisfies the EnvProvider interface.
type
OSEnvProvider
struct
{}
// Env returns the current environment from the operating system.
func
(
p
*
OSEnvProvider
)
Env
()
(
EnvList
,
error
)
{
return
fromSlice
(
os
.
Environ
()),
nil
}
// EmptyEnvProvider satisfies the EnvProvider interface.
type
EmptyEnvProvider
struct
{}
// Env returns an empty environment.
func
(
p
*
EmptyEnvProvider
)
Env
()
(
EnvList
,
error
)
{
return
EnvList
{},
nil
}
// FlagToEnvName converts a flag string into a UNIX like environment variable name.
// e.g --some-flag => "PREFIX_SOME_FLAG"
func
FlagToEnvName
(
flagName
,
prefix
string
)
string
{
envName
:=
strings
.
TrimPrefix
(
flagName
,
"--"
)
envName
=
strings
.
ToUpper
(
envName
)
envName
=
strings
.
Replace
(
envName
,
"-"
,
"_"
,
-
1
)
envName
=
prefix
+
envName
return
envName
}
// FlagToEnv converts a flag and its value into an Env.
// e.g --some-flag some-value => Env{N: "PREFIX_SOME_FLAG", V="SOME_VALUE"}
func
FlagToEnv
(
flag
*
pflag
.
Flag
,
prefix
string
)
Env
{
envName
:=
FlagToEnvName
(
flag
.
Name
,
prefix
)
return
Env
{
envName
,
flag
.
Value
.
String
()}
}
func
fromSlice
(
envs
[]
string
)
EnvList
{
list
:=
EnvList
{}
for
_
,
env
:=
range
envs
{
list
=
append
(
list
,
parseEnv
(
env
))
}
return
list
}
func
parseEnv
(
env
string
)
Env
{
if
!
strings
.
Contains
(
env
,
"="
)
{
env
=
env
+
"="
}
parsed
:=
strings
.
SplitN
(
env
,
"="
,
2
)
return
Env
{
parsed
[
0
],
parsed
[
1
]}
}
pkg/kubectl/plugins/env_test.go
deleted
100644 → 0
View file @
d745508c
/*
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
plugins
import
(
"reflect"
"testing"
"github.com/spf13/pflag"
)
func
TestEnv
(
t
*
testing
.
T
)
{
tests
:=
[]
struct
{
name
string
env
Env
expected
string
}{
{
name
:
"test1"
,
env
:
Env
{
"FOO"
,
"BAR"
},
expected
:
"FOO=BAR"
,
},
{
name
:
"test2"
,
env
:
Env
{
"FOO"
,
"BAR="
},
expected
:
"FOO=BAR="
,
},
{
name
:
"test3"
,
env
:
Env
{
"FOO"
,
""
},
expected
:
"FOO="
,
},
}
for
_
,
tt
:=
range
tests
{
t
.
Run
(
tt
.
name
,
func
(
t
*
testing
.
T
)
{
if
s
:=
tt
.
env
.
String
();
s
!=
tt
.
expected
{
t
.
Errorf
(
"%v: expected string %q, got %q"
,
tt
.
env
,
tt
.
expected
,
s
)
}
})
}
}
func
TestEnvListToSlice
(
t
*
testing
.
T
)
{
tests
:=
[]
struct
{
name
string
env
EnvList
expected
[]
string
}{
{
name
:
"test1"
,
env
:
EnvList
{
{
"FOO"
,
"BAR"
},
{
"ZEE"
,
"YO"
},
{
"ONE"
,
"1"
},
{
"EQUALS"
,
"=="
},
{
"EMPTY"
,
""
},
},
expected
:
[]
string
{
"FOO=BAR"
,
"ZEE=YO"
,
"ONE=1"
,
"EQUALS==="
,
"EMPTY="
},
},
}
for
_
,
tt
:=
range
tests
{
t
.
Run
(
tt
.
name
,
func
(
t
*
testing
.
T
)
{
if
s
:=
tt
.
env
.
Slice
();
!
reflect
.
DeepEqual
(
tt
.
expected
,
s
)
{
t
.
Errorf
(
"%v: expected %v, got %v"
,
tt
.
env
,
tt
.
expected
,
s
)
}
})
}
}
func
TestAddToEnvList
(
t
*
testing
.
T
)
{
tests
:=
[]
struct
{
name
string
add
[]
string
expected
EnvList
}{
{
name
:
"test1"
,
add
:
[]
string
{
"FOO=BAR"
,
"EMPTY="
,
"EQUALS==="
,
"JUSTNAME"
},
expected
:
EnvList
{
{
"FOO"
,
"BAR"
},
{
"EMPTY"
,
""
},
{
"EQUALS"
,
"=="
},
{
"JUSTNAME"
,
""
},
},
},
}
for
_
,
tt
:=
range
tests
{
t
.
Run
(
tt
.
name
,
func
(
t
*
testing
.
T
)
{
env
:=
EnvList
{}
.
Merge
(
tt
.
add
...
)
if
!
reflect
.
DeepEqual
(
tt
.
expected
,
env
)
{
t
.
Errorf
(
"%v: expected %v, got %v"
,
tt
.
add
,
tt
.
expected
,
env
)
}
})
}
}
func
TestFlagToEnv
(
t
*
testing
.
T
)
{
flags
:=
pflag
.
NewFlagSet
(
""
,
pflag
.
ContinueOnError
)
flags
.
String
(
"test"
,
"ok"
,
""
)
flags
.
String
(
"kube-master"
,
"http://something"
,
""
)
flags
.
String
(
"from-file"
,
"default"
,
""
)
flags
.
Parse
([]
string
{
"--from-file=nondefault"
})
tests
:=
[]
struct
{
name
string
flag
*
pflag
.
Flag
prefix
string
expected
Env
}{
{
name
:
"test1"
,
flag
:
flags
.
Lookup
(
"test"
),
expected
:
Env
{
"TEST"
,
"ok"
},
},
{
name
:
"test2"
,
flag
:
flags
.
Lookup
(
"kube-master"
),
expected
:
Env
{
"KUBE_MASTER"
,
"http://something"
},
},
{
name
:
"test3"
,
prefix
:
"KUBECTL_"
,
flag
:
flags
.
Lookup
(
"from-file"
),
expected
:
Env
{
"KUBECTL_FROM_FILE"
,
"nondefault"
},
},
}
for
_
,
tt
:=
range
tests
{
t
.
Run
(
tt
.
name
,
func
(
t
*
testing
.
T
)
{
if
env
:=
FlagToEnv
(
tt
.
flag
,
tt
.
prefix
);
!
reflect
.
DeepEqual
(
tt
.
expected
,
env
)
{
t
.
Errorf
(
"%v: expected %v, got %v"
,
tt
.
flag
.
Name
,
tt
.
expected
,
env
)
}
})
}
}
func
TestPluginDescriptorEnvProvider
(
t
*
testing
.
T
)
{
tests
:=
[]
struct
{
name
string
plugin
*
Plugin
expected
EnvList
}{
{
name
:
"test1"
,
plugin
:
&
Plugin
{
Description
:
Description
{
Name
:
"test"
,
ShortDesc
:
"Short Description"
,
Command
:
"foo --bar"
,
},
},
expected
:
EnvList
{
{
"KUBECTL_PLUGINS_DESCRIPTOR_NAME"
,
"test"
},
{
"KUBECTL_PLUGINS_DESCRIPTOR_SHORT_DESC"
,
"Short Description"
},
{
"KUBECTL_PLUGINS_DESCRIPTOR_LONG_DESC"
,
""
},
{
"KUBECTL_PLUGINS_DESCRIPTOR_EXAMPLE"
,
""
},
{
"KUBECTL_PLUGINS_DESCRIPTOR_COMMAND"
,
"foo --bar"
},
},
},
}
for
_
,
tt
:=
range
tests
{
t
.
Run
(
tt
.
name
,
func
(
t
*
testing
.
T
)
{
provider
:=
&
PluginDescriptorEnvProvider
{
Plugin
:
tt
.
plugin
,
}
env
,
_
:=
provider
.
Env
()
if
!
reflect
.
DeepEqual
(
tt
.
expected
,
env
)
{
t
.
Errorf
(
"%v: expected %v, got %v"
,
tt
.
plugin
.
Name
,
tt
.
expected
,
env
)
}
})
}
}
pkg/kubectl/plugins/examples/aging/aging.rb
deleted
100755 → 0
View file @
d745508c
#!/usr/bin/env ruby
require
'json'
require
'date'
class
Numeric
def
duration
secs
=
self
.
to_int
mins
=
secs
/
60
hours
=
mins
/
60
days
=
hours
/
24
if
days
>
0
"
#{
days
}
days and
#{
hours
%
24
}
hours"
elsif
hours
>
0
"
#{
hours
}
hours and
#{
mins
%
60
}
minutes"
elsif
mins
>
0
"
#{
mins
}
minutes and
#{
secs
%
60
}
seconds"
elsif
secs
>=
0
"
#{
secs
}
seconds"
end
end
end
namespace
=
ENV
[
'KUBECTL_PLUGINS_CURRENT_NAMESPACE'
]
||
'default'
pods_json
=
`kubectl --namespace
#{
namespace
}
get pods -o json`
pods_parsed
=
JSON
.
parse
(
pods_json
)
puts
"The Magnificent Aging Plugin."
data
=
Hash
.
new
max_name_length
=
0
max_age
=
0
min_age
=
0
pods_parsed
[
'items'
].
each
{
|
pod
|
name
=
pod
[
'metadata'
][
'name'
]
creation
=
pod
[
'metadata'
][
'creationTimestamp'
]
age
=
Time
.
now
-
DateTime
.
parse
(
creation
).
to_time
data
[
name
]
=
age
if
name
.
length
>
max_name_length
max_name_length
=
name
.
length
end
if
age
>
max_age
max_age
=
age
end
if
age
<
min_age
min_age
=
age
end
}
data
=
data
.
sort_by
{
|
name
,
age
|
age
}
if
data
.
length
>
0
puts
""
data
.
each
{
|
name
,
age
|
output
=
""
output
+=
name
.
rjust
(
max_name_length
,
' '
)
+
": "
bar_size
=
(
age
*
80
/
max_age
).
ceil
bar_size
.
times
{
output
+=
"▒"
}
output
+=
" "
+
age
.
duration
puts
output
puts
""
}
else
puts
"No pods"
end
pkg/kubectl/plugins/examples/aging/plugin.yaml
deleted
100644 → 0
View file @
d745508c
name
:
"
aging"
shortDesc
:
"
Aging
shows
pods
by
age"
longDesc
:
>
Aging shows pods from the current namespace by age.
command
:
./aging.rb
pkg/kubectl/plugins/examples/hello/plugin.yaml
deleted
100644 → 0
View file @
d745508c
name
:
"
hello"
shortDesc
:
"
I
say
hello!"
command
:
"
echo
Hello
plugins!"
pkg/kubectl/plugins/loader.go
deleted
100644 → 0
View file @
d745508c
/*
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
plugins
import
(
"fmt"
"io/ioutil"
"os"
"path/filepath"
"github.com/ghodss/yaml"
"github.com/golang/glog"
"k8s.io/client-go/tools/clientcmd"
)
// PluginDescriptorFilename is the default file name for plugin descriptions.
const
PluginDescriptorFilename
=
"plugin.yaml"
// PluginLoader is capable of loading a list of plugin descriptions.
type
PluginLoader
interface
{
// Load loads the plugin descriptions.
Load
()
(
Plugins
,
error
)
}
// DirectoryPluginLoader is a PluginLoader that loads plugin descriptions
// from a given directory in the filesystem. Plugins are located in subdirs
// under the loader "root", where each subdir must contain, at least, a plugin
// descriptor file called "plugin.yaml" that translates into a PluginDescription.
type
DirectoryPluginLoader
struct
{
Directory
string
}
// Load reads the directory the loader holds and loads plugin descriptions.
func
(
l
*
DirectoryPluginLoader
)
Load
()
(
Plugins
,
error
)
{
if
len
(
l
.
Directory
)
==
0
{
return
nil
,
fmt
.
Errorf
(
"directory not specified"
)
}
list
:=
Plugins
{}
stat
,
err
:=
os
.
Stat
(
l
.
Directory
)
if
err
!=
nil
{
return
nil
,
err
}
if
!
stat
.
IsDir
()
{
return
nil
,
fmt
.
Errorf
(
"not a directory: %s"
,
l
.
Directory
)
}
base
,
err
:=
filepath
.
Abs
(
l
.
Directory
)
if
err
!=
nil
{
return
nil
,
err
}
// read the base directory tree searching for plugin descriptors
// fails silently (descriptors unable to be read or unmarshalled are logged but skipped)
err
=
filepath
.
Walk
(
base
,
func
(
path
string
,
fileInfo
os
.
FileInfo
,
walkErr
error
)
error
{
if
walkErr
!=
nil
||
fileInfo
.
IsDir
()
||
fileInfo
.
Name
()
!=
PluginDescriptorFilename
{
return
nil
}
file
,
err
:=
ioutil
.
ReadFile
(
path
)
if
err
!=
nil
{
glog
.
V
(
1
)
.
Infof
(
"Unable to read plugin descriptor %s: %v"
,
path
,
err
)
return
nil
}
plugin
:=
&
Plugin
{}
if
err
:=
yaml
.
Unmarshal
(
file
,
plugin
);
err
!=
nil
{
glog
.
V
(
1
)
.
Infof
(
"Unable to unmarshal plugin descriptor %s: %v"
,
path
,
err
)
return
nil
}
if
err
:=
plugin
.
Validate
();
err
!=
nil
{
glog
.
V
(
1
)
.
Infof
(
"%v"
,
err
)
return
nil
}
var
setSource
func
(
path
string
,
fileInfo
os
.
FileInfo
,
p
*
Plugin
)
setSource
=
func
(
path
string
,
fileInfo
os
.
FileInfo
,
p
*
Plugin
)
{
p
.
Dir
=
filepath
.
Dir
(
path
)
p
.
DescriptorName
=
fileInfo
.
Name
()
for
_
,
child
:=
range
p
.
Tree
{
setSource
(
path
,
fileInfo
,
child
)
}
}
setSource
(
path
,
fileInfo
,
plugin
)
glog
.
V
(
6
)
.
Infof
(
"Plugin loaded: %s"
,
plugin
.
Name
)
list
=
append
(
list
,
plugin
)
return
nil
})
return
list
,
err
}
// UserDirPluginLoader returns a PluginLoader that loads plugins from the
// "plugins" directory under the user's kubeconfig dir (usually "~/.kube/plugins/").
func
UserDirPluginLoader
()
PluginLoader
{
dir
:=
filepath
.
Join
(
clientcmd
.
RecommendedConfigDir
,
"plugins"
)
return
&
DirectoryPluginLoader
{
Directory
:
dir
,
}
}
// PathFromEnvVarPluginLoader returns a PluginLoader that loads plugins from one or more
// directories specified by the provided env var name. In case the env var is not
// set, the PluginLoader just loads nothing. A list of subdirectories can be provided,
// which will be appended to each path specified by the env var.
func
PathFromEnvVarPluginLoader
(
envVarName
string
,
subdirs
...
string
)
PluginLoader
{
env
:=
os
.
Getenv
(
envVarName
)
if
len
(
env
)
==
0
{
return
&
DummyPluginLoader
{}
}
loader
:=
MultiPluginLoader
{}
for
_
,
path
:=
range
filepath
.
SplitList
(
env
)
{
dir
:=
append
([]
string
{
path
},
subdirs
...
)
loader
=
append
(
loader
,
&
DirectoryPluginLoader
{
Directory
:
filepath
.
Join
(
dir
...
),
})
}
return
loader
}
// KubectlPluginsPathPluginLoader returns a PluginLoader that loads plugins from one or more
// directories specified by the KUBECTL_PLUGINS_PATH env var.
func
KubectlPluginsPathPluginLoader
()
PluginLoader
{
return
PathFromEnvVarPluginLoader
(
"KUBECTL_PLUGINS_PATH"
)
}
// XDGDataDirsPluginLoader returns a PluginLoader that loads plugins from one or more
// directories specified by the XDG system directory structure spec in the
// XDG_DATA_DIRS env var, plus the "kubectl/plugins/" suffix. According to the
// spec, if XDG_DATA_DIRS is not set it defaults to "/usr/local/share:/usr/share".
func
XDGDataDirsPluginLoader
()
PluginLoader
{
envVarName
:=
"XDG_DATA_DIRS"
if
len
(
os
.
Getenv
(
envVarName
))
>
0
{
return
PathFromEnvVarPluginLoader
(
envVarName
,
"kubectl"
,
"plugins"
)
}
return
TolerantMultiPluginLoader
{
&
DirectoryPluginLoader
{
Directory
:
"/usr/local/share/kubectl/plugins"
,
},
&
DirectoryPluginLoader
{
Directory
:
"/usr/share/kubectl/plugins"
,
},
}
}
// MultiPluginLoader is a PluginLoader that can encapsulate multiple plugin loaders,
// a successful loading means every encapsulated loader was able to load without errors.
type
MultiPluginLoader
[]
PluginLoader
// Load calls Load() for each of the encapsulated Loaders.
func
(
l
MultiPluginLoader
)
Load
()
(
Plugins
,
error
)
{
plugins
:=
Plugins
{}
for
_
,
loader
:=
range
l
{
loaded
,
err
:=
loader
.
Load
()
if
err
!=
nil
{
return
nil
,
err
}
plugins
=
append
(
plugins
,
loaded
...
)
}
return
plugins
,
nil
}
// TolerantMultiPluginLoader is a PluginLoader than encapsulates multiple plugins loaders,
// but is tolerant to errors while loading from them.
type
TolerantMultiPluginLoader
[]
PluginLoader
// Load calls Load() for each of the encapsulated Loaders.
func
(
l
TolerantMultiPluginLoader
)
Load
()
(
Plugins
,
error
)
{
plugins
:=
Plugins
{}
for
_
,
loader
:=
range
l
{
loaded
,
_
:=
loader
.
Load
()
if
loaded
!=
nil
{
plugins
=
append
(
plugins
,
loaded
...
)
}
}
return
plugins
,
nil
}
// DummyPluginLoader is a noop PluginLoader.
type
DummyPluginLoader
struct
{}
// Load loads nothing.
func
(
l
*
DummyPluginLoader
)
Load
()
(
Plugins
,
error
)
{
return
Plugins
{},
nil
}
pkg/kubectl/plugins/loader_test.go
deleted
100644 → 0
View file @
d745508c
/*
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
plugins
import
(
"fmt"
"io/ioutil"
"os"
"path/filepath"
"regexp"
"strings"
"testing"
)
func
TestSuccessfulDirectoryPluginLoader
(
t
*
testing
.
T
)
{
tmp
,
err
:=
setupValidPlugins
(
3
,
0
)
if
err
!=
nil
{
t
.
Fatalf
(
"unexpected error: %v"
,
err
)
}
defer
os
.
RemoveAll
(
tmp
)
loader
:=
&
DirectoryPluginLoader
{
Directory
:
tmp
,
}
plugins
,
err
:=
loader
.
Load
()
if
err
!=
nil
{
t
.
Errorf
(
"Unexpected error loading plugins: %v"
,
err
)
}
if
count
:=
len
(
plugins
);
count
!=
3
{
t
.
Errorf
(
"Unexpected number of loaded plugins, wanted 3, got %d"
,
count
)
}
for
_
,
plugin
:=
range
plugins
{
if
m
,
_
:=
regexp
.
MatchString
(
"^plugin[123]$"
,
plugin
.
Name
);
!
m
{
t
.
Errorf
(
"Unexpected plugin name %s"
,
plugin
.
Name
)
}
if
m
,
_
:=
regexp
.
MatchString
(
"^The plugin[123] test plugin$"
,
plugin
.
ShortDesc
);
!
m
{
t
.
Errorf
(
"Unexpected plugin short desc %s"
,
plugin
.
ShortDesc
)
}
if
m
,
_
:=
regexp
.
MatchString
(
"^echo plugin[123]$"
,
plugin
.
Command
);
!
m
{
t
.
Errorf
(
"Unexpected plugin command %s"
,
plugin
.
Command
)
}
if
count
:=
len
(
plugin
.
Tree
);
count
!=
0
{
t
.
Errorf
(
"Unexpected number of loaded child plugins, wanted 0, got %d"
,
count
)
}
}
}
func
TestEmptyDirectoryPluginLoader
(
t
*
testing
.
T
)
{
loader
:=
&
DirectoryPluginLoader
{}
_
,
err
:=
loader
.
Load
()
if
err
==
nil
{
t
.
Errorf
(
"Expected error, got none"
)
}
if
m
,
_
:=
regexp
.
MatchString
(
"^directory not specified$"
,
err
.
Error
());
!
m
{
t
.
Errorf
(
"Unexpected error %v"
,
err
)
}
}
func
TestNotDirectoryPluginLoader
(
t
*
testing
.
T
)
{
tmp
,
err
:=
ioutil
.
TempDir
(
""
,
""
)
if
err
!=
nil
{
t
.
Fatalf
(
"unexpected ioutil.TempDir error: %v"
,
err
)
}
defer
os
.
RemoveAll
(
tmp
)
file
:=
filepath
.
Join
(
tmp
,
"test.tmp"
)
if
err
:=
ioutil
.
WriteFile
(
file
,
[]
byte
(
"test"
),
644
);
err
!=
nil
{
t
.
Fatalf
(
"unexpected ioutil.WriteFile error: %v"
,
err
)
}
loader
:=
&
DirectoryPluginLoader
{
Directory
:
file
,
}
_
,
err
=
loader
.
Load
()
if
err
==
nil
{
t
.
Errorf
(
"Expected error, got none"
)
}
if
!
strings
.
Contains
(
err
.
Error
(),
"not a directory"
)
{
t
.
Errorf
(
"Unexpected error %v"
,
err
)
}
}
func
TestUnexistentDirectoryPluginLoader
(
t
*
testing
.
T
)
{
loader
:=
&
DirectoryPluginLoader
{
Directory
:
"/hopefully-does-not-exist"
,
}
_
,
err
:=
loader
.
Load
()
if
err
==
nil
{
t
.
Errorf
(
"Expected error, got none"
)
}
if
!
strings
.
Contains
(
err
.
Error
(),
"no such file or directory"
)
{
t
.
Errorf
(
"Unexpected error %v"
,
err
)
}
}
func
TestKubectlPluginsPathPluginLoader
(
t
*
testing
.
T
)
{
tmp
,
err
:=
setupValidPlugins
(
1
,
0
)
if
err
!=
nil
{
t
.
Fatalf
(
"unexpected error: %v"
,
err
)
}
defer
os
.
RemoveAll
(
tmp
)
env
:=
"KUBECTL_PLUGINS_PATH"
os
.
Setenv
(
env
,
tmp
)
defer
os
.
Unsetenv
(
env
)
loader
:=
KubectlPluginsPathPluginLoader
()
plugins
,
err
:=
loader
.
Load
()
if
err
!=
nil
{
t
.
Errorf
(
"Unexpected error loading plugins: %v"
,
err
)
}
if
count
:=
len
(
plugins
);
count
!=
1
{
t
.
Errorf
(
"Unexpected number of loaded plugins, wanted 1, got %d"
,
count
)
}
plugin
:=
plugins
[
0
]
if
"plugin1"
!=
plugin
.
Name
{
t
.
Errorf
(
"Unexpected plugin name %s"
,
plugin
.
Name
)
}
if
"The plugin1 test plugin"
!=
plugin
.
ShortDesc
{
t
.
Errorf
(
"Unexpected plugin short desc %s"
,
plugin
.
ShortDesc
)
}
if
"echo plugin1"
!=
plugin
.
Command
{
t
.
Errorf
(
"Unexpected plugin command %s"
,
plugin
.
Command
)
}
}
func
TestIncompletePluginDescriptor
(
t
*
testing
.
T
)
{
tmp
,
err
:=
ioutil
.
TempDir
(
""
,
""
)
if
err
!=
nil
{
t
.
Fatalf
(
"unexpected ioutil.TempDir error: %v"
,
err
)
}
descriptor
:=
`
name: incomplete
shortDesc: The incomplete test plugin`
if
err
:=
os
.
Mkdir
(
filepath
.
Join
(
tmp
,
"incomplete"
),
0755
);
err
!=
nil
{
t
.
Fatalf
(
"unexpected os.Mkdir error: %v"
,
err
)
}
if
err
:=
ioutil
.
WriteFile
(
filepath
.
Join
(
tmp
,
"incomplete"
,
"plugin.yaml"
),
[]
byte
(
descriptor
),
0644
);
err
!=
nil
{
t
.
Fatalf
(
"unexpected ioutil.WriteFile error: %v"
,
err
)
}
defer
os
.
RemoveAll
(
tmp
)
loader
:=
&
DirectoryPluginLoader
{
Directory
:
tmp
,
}
plugins
,
err
:=
loader
.
Load
()
if
err
!=
nil
{
t
.
Errorf
(
"unexpected error: %v"
,
err
)
}
if
count
:=
len
(
plugins
);
count
!=
0
{
t
.
Errorf
(
"Unexpected number of loaded plugins, wanted 0, got %d"
,
count
)
}
}
func
TestDirectoryTreePluginLoader
(
t
*
testing
.
T
)
{
tmp
,
err
:=
setupValidPlugins
(
1
,
2
)
if
err
!=
nil
{
t
.
Fatalf
(
"unexpected error: %v"
,
err
)
}
defer
os
.
RemoveAll
(
tmp
)
loader
:=
&
DirectoryPluginLoader
{
Directory
:
tmp
,
}
plugins
,
err
:=
loader
.
Load
()
if
err
!=
nil
{
t
.
Errorf
(
"Unexpected error loading plugins: %v"
,
err
)
}
if
count
:=
len
(
plugins
);
count
!=
1
{
t
.
Errorf
(
"Unexpected number of loaded plugins, wanted 1, got %d"
,
count
)
}
for
_
,
plugin
:=
range
plugins
{
if
m
,
_
:=
regexp
.
MatchString
(
"^plugin1$"
,
plugin
.
Name
);
!
m
{
t
.
Errorf
(
"Unexpected plugin name %s"
,
plugin
.
Name
)
}
if
m
,
_
:=
regexp
.
MatchString
(
"^The plugin1 test plugin$"
,
plugin
.
ShortDesc
);
!
m
{
t
.
Errorf
(
"Unexpected plugin short desc %s"
,
plugin
.
ShortDesc
)
}
if
m
,
_
:=
regexp
.
MatchString
(
"^echo plugin1$"
,
plugin
.
Command
);
!
m
{
t
.
Errorf
(
"Unexpected plugin command %s"
,
plugin
.
Command
)
}
if
count
:=
len
(
plugin
.
Tree
);
count
!=
2
{
t
.
Errorf
(
"Unexpected number of loaded child plugins, wanted 2, got %d"
,
count
)
}
for
_
,
child
:=
range
plugin
.
Tree
{
if
m
,
_
:=
regexp
.
MatchString
(
"^child[12]$"
,
child
.
Name
);
!
m
{
t
.
Errorf
(
"Unexpected plugin child name %s"
,
child
.
Name
)
}
if
m
,
_
:=
regexp
.
MatchString
(
"^The child[12] test plugin child of plugin1 of House Targaryen$"
,
child
.
ShortDesc
);
!
m
{
t
.
Errorf
(
"Unexpected plugin child short desc %s"
,
child
.
ShortDesc
)
}
if
m
,
_
:=
regexp
.
MatchString
(
"^echo child[12]$"
,
child
.
Command
);
!
m
{
t
.
Errorf
(
"Unexpected plugin child command %s"
,
child
.
Command
)
}
}
}
}
func
setupValidPlugins
(
nPlugins
,
nChildren
int
)
(
string
,
error
)
{
tmp
,
err
:=
ioutil
.
TempDir
(
""
,
""
)
if
err
!=
nil
{
return
""
,
fmt
.
Errorf
(
"unexpected ioutil.TempDir error: %v"
,
err
)
}
for
i
:=
1
;
i
<=
nPlugins
;
i
++
{
name
:=
fmt
.
Sprintf
(
"plugin%d"
,
i
)
descriptor
:=
fmt
.
Sprintf
(
`
name: %[1]s
shortDesc: The %[1]s test plugin
command: echo %[1]s
flags:
- name: %[1]s-flag
desc: A flag for %[1]s`
,
name
)
if
nChildren
>
0
{
descriptor
+=
`
tree:`
}
for
j
:=
1
;
j
<=
nChildren
;
j
++
{
child
:=
fmt
.
Sprintf
(
"child%d"
,
i
)
descriptor
+=
fmt
.
Sprintf
(
`
- name: %[1]s
shortDesc: The %[1]s test plugin child of %[2]s of House Targaryen
command: echo %[1]s`
,
child
,
name
)
}
if
err
:=
os
.
Mkdir
(
filepath
.
Join
(
tmp
,
name
),
0755
);
err
!=
nil
{
return
""
,
fmt
.
Errorf
(
"unexpected os.Mkdir error: %v"
,
err
)
}
if
err
:=
ioutil
.
WriteFile
(
filepath
.
Join
(
tmp
,
name
,
"plugin.yaml"
),
[]
byte
(
descriptor
),
0644
);
err
!=
nil
{
return
""
,
fmt
.
Errorf
(
"unexpected ioutil.WriteFile error: %v"
,
err
)
}
}
return
tmp
,
nil
}
pkg/kubectl/plugins/plugins.go
deleted
100644 → 0
View file @
d745508c
/*
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
plugins
import
(
"fmt"
"strings"
"unicode"
)
var
(
// ErrIncompletePlugin indicates plugin is incomplete.
ErrIncompletePlugin
=
fmt
.
Errorf
(
"incomplete plugin descriptor: name, shortDesc and command fields are required"
)
// ErrInvalidPluginName indicates plugin name is invalid.
ErrInvalidPluginName
=
fmt
.
Errorf
(
"plugin name can't contain spaces"
)
// ErrIncompleteFlag indicates flag is incomplete.
ErrIncompleteFlag
=
fmt
.
Errorf
(
"incomplete flag descriptor: name and desc fields are required"
)
// ErrInvalidFlagName indicates flag name is invalid.
ErrInvalidFlagName
=
fmt
.
Errorf
(
"flag name can't contain spaces"
)
// ErrInvalidFlagShorthand indicates flag shorthand is invalid.
ErrInvalidFlagShorthand
=
fmt
.
Errorf
(
"flag shorthand must be only one letter"
)
)
// Plugin is the representation of a CLI extension (plugin).
type
Plugin
struct
{
Description
Source
Context
RunningContext
`json:"-"`
}
// Description holds everything needed to register a
// plugin as a command. Usually comes from a descriptor file.
type
Description
struct
{
Name
string
`json:"name"`
ShortDesc
string
`json:"shortDesc"`
LongDesc
string
`json:"longDesc,omitempty"`
Example
string
`json:"example,omitempty"`
Command
string
`json:"command"`
Flags
[]
Flag
`json:"flags,omitempty"`
Tree
Plugins
`json:"tree,omitempty"`
}
// Source holds the location of a given plugin in the filesystem.
type
Source
struct
{
Dir
string
`json:"-"`
DescriptorName
string
`json:"-"`
}
// Validate validates plugin data.
func
(
p
Plugin
)
Validate
()
error
{
if
len
(
p
.
Name
)
==
0
||
len
(
p
.
ShortDesc
)
==
0
||
(
len
(
p
.
Command
)
==
0
&&
len
(
p
.
Tree
)
==
0
)
{
return
ErrIncompletePlugin
}
if
strings
.
Contains
(
p
.
Name
,
" "
)
{
return
ErrInvalidPluginName
}
for
_
,
flag
:=
range
p
.
Flags
{
if
err
:=
flag
.
Validate
();
err
!=
nil
{
return
err
}
}
for
_
,
child
:=
range
p
.
Tree
{
if
err
:=
child
.
Validate
();
err
!=
nil
{
return
err
}
}
return
nil
}
// IsValid returns true if plugin data is valid.
func
(
p
Plugin
)
IsValid
()
bool
{
return
p
.
Validate
()
==
nil
}
// Plugins is a list of plugins.
type
Plugins
[]
*
Plugin
// Flag describes a single flag supported by a given plugin.
type
Flag
struct
{
Name
string
`json:"name"`
Shorthand
string
`json:"shorthand,omitempty"`
Desc
string
`json:"desc"`
DefValue
string
`json:"defValue,omitempty"`
}
// Validate validates flag data.
func
(
f
Flag
)
Validate
()
error
{
if
len
(
f
.
Name
)
==
0
||
len
(
f
.
Desc
)
==
0
{
return
ErrIncompleteFlag
}
if
strings
.
Contains
(
f
.
Name
,
" "
)
{
return
ErrInvalidFlagName
}
return
f
.
ValidateShorthand
()
}
// ValidateShorthand validates flag shorthand data.
func
(
f
Flag
)
ValidateShorthand
()
error
{
length
:=
len
(
f
.
Shorthand
)
if
length
==
0
||
(
length
==
1
&&
unicode
.
IsLetter
(
rune
(
f
.
Shorthand
[
0
])))
{
return
nil
}
return
ErrInvalidFlagShorthand
}
// Shorthanded returns true if flag shorthand data is valid.
func
(
f
Flag
)
Shorthanded
()
bool
{
return
f
.
ValidateShorthand
()
==
nil
}
pkg/kubectl/plugins/plugins_test.go
deleted
100644 → 0
View file @
d745508c
/*
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
plugins
import
"testing"
func
TestPlugin
(
t
*
testing
.
T
)
{
tests
:=
[]
struct
{
name
string
plugin
*
Plugin
expectedErr
error
}{
{
name
:
"test1"
,
plugin
:
&
Plugin
{
Description
:
Description
{
Name
:
"test"
,
ShortDesc
:
"The test"
,
Command
:
"echo 1"
,
},
},
},
{
name
:
"test2"
,
plugin
:
&
Plugin
{
Description
:
Description
{
Name
:
"test"
,
ShortDesc
:
"The test"
,
},
},
expectedErr
:
ErrIncompletePlugin
,
},
{
name
:
"test3"
,
plugin
:
&
Plugin
{},
expectedErr
:
ErrIncompletePlugin
,
},
{
name
:
"test4"
,
plugin
:
&
Plugin
{
Description
:
Description
{
Name
:
"test spaces"
,
ShortDesc
:
"The test"
,
Command
:
"echo 1"
,
},
},
expectedErr
:
ErrInvalidPluginName
,
},
{
name
:
"test5"
,
plugin
:
&
Plugin
{
Description
:
Description
{
Name
:
"test"
,
ShortDesc
:
"The test"
,
Command
:
"echo 1"
,
Flags
:
[]
Flag
{
{
Name
:
"aflag"
,
},
},
},
},
expectedErr
:
ErrIncompleteFlag
,
},
{
name
:
"test6"
,
plugin
:
&
Plugin
{
Description
:
Description
{
Name
:
"test"
,
ShortDesc
:
"The test"
,
Command
:
"echo 1"
,
Flags
:
[]
Flag
{
{
Name
:
"a flag"
,
Desc
:
"Invalid flag"
,
},
},
},
},
expectedErr
:
ErrInvalidFlagName
,
},
{
name
:
"test7"
,
plugin
:
&
Plugin
{
Description
:
Description
{
Name
:
"test"
,
ShortDesc
:
"The test"
,
Command
:
"echo 1"
,
Flags
:
[]
Flag
{
{
Name
:
"aflag"
,
Desc
:
"Invalid shorthand"
,
Shorthand
:
"aa"
,
},
},
},
},
expectedErr
:
ErrInvalidFlagShorthand
,
},
{
name
:
"test8"
,
plugin
:
&
Plugin
{
Description
:
Description
{
Name
:
"test"
,
ShortDesc
:
"The test"
,
Command
:
"echo 1"
,
Flags
:
[]
Flag
{
{
Name
:
"aflag"
,
Desc
:
"Invalid shorthand"
,
Shorthand
:
"2"
,
},
},
},
},
expectedErr
:
ErrInvalidFlagShorthand
,
},
{
name
:
"test9"
,
plugin
:
&
Plugin
{
Description
:
Description
{
Name
:
"test"
,
ShortDesc
:
"The test"
,
Command
:
"echo 1"
,
Flags
:
[]
Flag
{
{
Name
:
"aflag"
,
Desc
:
"A flag"
,
Shorthand
:
"a"
,
},
},
Tree
:
Plugins
{
&
Plugin
{
Description
:
Description
{
Name
:
"child"
,
ShortDesc
:
"The child"
,
LongDesc
:
"The child long desc"
,
Example
:
"You can use it like this but you're not supposed to"
,
Command
:
"echo 1"
,
Flags
:
[]
Flag
{
{
Name
:
"childflag"
,
Desc
:
"A child flag"
,
},
{
Name
:
"childshorthand"
,
Desc
:
"A child shorthand flag"
,
Shorthand
:
"s"
,
},
},
},
},
},
},
},
},
}
for
_
,
tt
:=
range
tests
{
t
.
Run
(
tt
.
name
,
func
(
t
*
testing
.
T
)
{
err
:=
tt
.
plugin
.
Validate
()
if
err
!=
tt
.
expectedErr
{
t
.
Errorf
(
"%s: expected error %v, got %v"
,
tt
.
plugin
.
Name
,
tt
.
expectedErr
,
err
)
}
})
}
}
pkg/kubectl/plugins/runner.go
deleted
100644 → 0
View file @
d745508c
/*
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
plugins
import
(
"os"
"os/exec"
"strings"
"github.com/golang/glog"
"k8s.io/kubernetes/pkg/kubectl/genericclioptions"
)
// PluginRunner is capable of running a plugin in a given running context.
type
PluginRunner
interface
{
Run
(
plugin
*
Plugin
,
ctx
RunningContext
)
error
}
// RunningContext holds the context in which a given plugin is running - the
// in, out, and err streams, arguments and environment passed to it, and the
// working directory.
type
RunningContext
struct
{
genericclioptions
.
IOStreams
Args
[]
string
EnvProvider
EnvProvider
WorkingDir
string
}
// ExecPluginRunner is a PluginRunner that uses Go's os/exec to run plugins.
type
ExecPluginRunner
struct
{}
// Run takes a given plugin and runs it in a given context using os/exec, returning
// any error found while running.
func
(
r
*
ExecPluginRunner
)
Run
(
plugin
*
Plugin
,
ctx
RunningContext
)
error
{
command
:=
strings
.
Split
(
os
.
ExpandEnv
(
plugin
.
Command
),
" "
)
base
:=
command
[
0
]
args
:=
[]
string
{}
if
len
(
command
)
>
1
{
args
=
command
[
1
:
]
}
args
=
append
(
args
,
ctx
.
Args
...
)
cmd
:=
exec
.
Command
(
base
,
args
...
)
cmd
.
Stdin
=
ctx
.
In
cmd
.
Stdout
=
ctx
.
Out
cmd
.
Stderr
=
ctx
.
ErrOut
env
,
err
:=
ctx
.
EnvProvider
.
Env
()
if
err
!=
nil
{
return
err
}
cmd
.
Env
=
env
.
Slice
()
cmd
.
Dir
=
ctx
.
WorkingDir
glog
.
V
(
9
)
.
Infof
(
"Running plugin %q as base command %q with args %v"
,
plugin
.
Name
,
base
,
args
)
return
cmd
.
Run
()
}
pkg/kubectl/plugins/runner_test.go
deleted
100644 → 0
View file @
d745508c
/*
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
plugins
import
(
"os"
"testing"
"k8s.io/kubernetes/pkg/kubectl/genericclioptions"
)
func
TestExecRunner
(
t
*
testing
.
T
)
{
tests
:=
[]
struct
{
name
string
command
string
expectedMsg
string
expectedErr
string
}{
{
name
:
"success"
,
command
:
"echo test ok"
,
expectedMsg
:
"test ok
\n
"
,
},
{
name
:
"invalid"
,
command
:
"false"
,
expectedErr
:
"exit status 1"
,
},
{
name
:
"env"
,
command
:
"echo $KUBECTL_PLUGINS_TEST"
,
expectedMsg
:
"ok
\n
"
,
},
}
os
.
Setenv
(
"KUBECTL_PLUGINS_TEST"
,
"ok"
)
defer
os
.
Unsetenv
(
"KUBECTL_PLUGINS_TEST"
)
for
_
,
tt
:=
range
tests
{
t
.
Run
(
tt
.
name
,
func
(
t
*
testing
.
T
)
{
streams
,
_
,
outBuf
,
_
:=
genericclioptions
.
NewTestIOStreams
()
plugin
:=
&
Plugin
{
Description
:
Description
{
Name
:
tt
.
name
,
ShortDesc
:
"Test Runner Plugin"
,
Command
:
tt
.
command
,
},
}
ctx
:=
RunningContext
{
IOStreams
:
streams
,
WorkingDir
:
"."
,
EnvProvider
:
&
EmptyEnvProvider
{},
}
runner
:=
&
ExecPluginRunner
{}
err
:=
runner
.
Run
(
plugin
,
ctx
)
if
outBuf
.
String
()
!=
tt
.
expectedMsg
{
t
.
Errorf
(
"%s: unexpected output: %q"
,
tt
.
name
,
outBuf
.
String
())
}
if
err
!=
nil
&&
err
.
Error
()
!=
tt
.
expectedErr
{
t
.
Errorf
(
"%s: unexpected err output: %v"
,
tt
.
name
,
err
)
}
})
}
}
test/cmd/plugins.sh
View file @
4bdc6363
...
...
@@ -24,81 +24,30 @@ run_plugins_tests() {
kube::log::status
"Testing kubectl plugins"
# t
op-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
'
# t
est plugins that overwrite existing kubectl commands
output_message
=
$(
!
PATH
=
${
PATH
}
:
"test/fixtures/pkg/kubectl/plugins/version"
kubectl plugin list
2>&1
)
kube::test::if_has_string
"
${
output_message
}
"
'
kubectl-version overwrites existing command: "kubectl version"
'
# 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'
# test plugins that overwrite similarly-named plugins
output_message
=
$(
!
PATH
=
${
PATH
}
:
"test/fixtures/pkg/kubectl/plugins:test/fixtures/pkg/kubectl/plugins/foo"
kubectl plugin list 2>&1
)
kube::test::if_has_string
"
${
output_message
}
"
'test/fixtures/pkg/kubectl/plugins/foo/kubectl-foo is overshadowed by a similarly named 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'
# test plugins with no warnings
output_message
=
$(
PATH
=
${
PATH
}
:
"test/fixtures/pkg/kubectl/plugins"
kubectl plugin list 2>&1
)
kube::test::if_has_string
"
${
output_message
}
"
'plugins are available'
# 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'
# no plugins
output_message
=
$(
!
PATH
=
${
PATH
}
:
"test/fixtures/pkg/kubectl/plugins/empty"
kubectl plugin list 2>&1
)
kube::test::if_has_string
"
${
output_message
}
"
'unable to find any kubectl plugins in your PATH'
# plugin tree
output_message
=
$(
!
KUBECTL_PLUGINS_PATH
=
test
/fixtures/pkg/kubectl/plugins kubectl plugin tree 2>&1
)
kube::test::if_has_string
"
${
output_message
}
"
'Plugin with a tree of commands'
kube::test::if_has_string
"
${
output_message
}
"
'child1\s\+The first child of a tree'
kube::test::if_has_string
"
${
output_message
}
"
'child2\s\+The second child of a tree'
kube::test::if_has_string
"
${
output_message
}
"
'child3\s\+The third child of a tree'
output_message
=
$(
KUBECTL_PLUGINS_PATH
=
test
/fixtures/pkg/kubectl/plugins kubectl plugin tree child1
--help
2>&1
)
kube::test::if_has_string
"
${
output_message
}
"
'The first child of a tree'
kube::test::if_has_not_string
"
${
output_message
}
"
'The second child'
kube::test::if_has_not_string
"
${
output_message
}
"
'child2'
output_message
=
$(
KUBECTL_PLUGINS_PATH
=
test
/fixtures/pkg/kubectl/plugins kubectl plugin tree child1 2>&1
)
kube::test::if_has_string
"
${
output_message
}
"
'child one'
kube::test::if_has_not_string
"
${
output_message
}
"
'child1'
kube::test::if_has_not_string
"
${
output_message
}
"
'The first child'
# attempt to run a plugin in the user's PATH
output_message
=
$(
PATH
=
${
PATH
}
:
"test/fixtures/pkg/kubectl/plugins"
kubectl foo
)
kube::test::if_has_string
"
${
output_message
}
"
'plugin foo'
# plugin env
output_message
=
$(
KUBECTL_PLUGINS_PATH
=
test
/fixtures/pkg/kubectl/plugins kubectl plugin
env
-h
2>&1
)
kube::test::if_has_string
"
${
output_message
}
"
"This is a flag 1"
kube::test::if_has_string
"
${
output_message
}
"
"This is a flag 2"
kube::test::if_has_string
"
${
output_message
}
"
"This is a flag 3"
output_message
=
$(
KUBECTL_PLUGINS_PATH
=
test
/fixtures/pkg/kubectl/plugins kubectl plugin
env
--test1
=
value1
-t
value2 2>&1
)
kube::test::if_has_string
"
${
output_message
}
"
'KUBECTL_PLUGINS_CURRENT_NAMESPACE'
kube::test::if_has_string
"
${
output_message
}
"
'KUBECTL_PLUGINS_CALLER'
kube::test::if_has_string
"
${
output_message
}
"
'KUBECTL_PLUGINS_DESCRIPTOR_COMMAND=./env.sh'
kube::test::if_has_string
"
${
output_message
}
"
'KUBECTL_PLUGINS_DESCRIPTOR_SHORT_DESC=The plugin envs plugin'
kube::test::if_has_string
"
${
output_message
}
"
'KUBECTL_PLUGINS_GLOBAL_FLAG_KUBECONFIG'
kube::test::if_has_string
"
${
output_message
}
"
'KUBECTL_PLUGINS_GLOBAL_FLAG_REQUEST_TIMEOUT=0'
kube::test::if_has_string
"
${
output_message
}
"
'KUBECTL_PLUGINS_LOCAL_FLAG_TEST1=value1'
kube::test::if_has_string
"
${
output_message
}
"
'KUBECTL_PLUGINS_LOCAL_FLAG_TEST2=value2'
kube::test::if_has_string
"
${
output_message
}
"
'KUBECTL_PLUGINS_LOCAL_FLAG_TEST3=default'
# ensure that a kubectl command supersedes a plugin that overshadows it
output_message
=
$(
PATH
=
${
PATH
}
:
"test/fixtures/pkg/kubectl/plugins/version"
kubectl version
)
kube::test::if_has_string
"
${
output_message
}
"
'Client Version'
kube::test::if_has_not_string
"
${
output_message
}
"
'overshadows an existing plugin'
set
+o nounset
set
+o errexit
...
...
test/fixtures/pkg/kubectl/plugins/echo/plugin.yaml
deleted
100644 → 0
View file @
d745508c
name
:
"
echo"
shortDesc
:
"
Echoes
for
test-cmd"
longDesc
:
"
Long
description
for
the
test-cmd
echo
plugin"
command
:
"
echo
This
plugin
works!"
test/fixtures/pkg/kubectl/plugins/env/env.sh
deleted
100755 → 0
View file @
d745508c
#!/usr/bin/env bash
# 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.
env
|
grep
'KUBECTL_PLUGINS'
|
sort
test/fixtures/pkg/kubectl/plugins/env/plugin.yaml
deleted
100644 → 0
View file @
d745508c
name
:
env
shortDesc
:
"
The
plugin
envs
plugin"
command
:
"
./env.sh"
flags
:
-
name
:
"
test1"
desc
:
"
This
is
a
flag
1"
-
name
:
"
test2"
desc
:
"
This
is
a
flag
2"
shorthand
:
"
t"
-
name
:
"
test3"
desc
:
"
This
is
a
flag
3"
defValue
:
"
default"
test/fixtures/pkg/kubectl/plugins/error/plugin.yaml
deleted
100644 → 0
View file @
d745508c
name
:
"
error"
shortDesc
:
"
The
tremendous
plugin
that
always
fails!"
command
:
"
false"
test/fixtures/pkg/kubectl/plugins/foo/kubectl-foo
0 → 100755
View file @
4bdc6363
#!/bin/bash
echo
"I am plugin foo"
test/fixtures/pkg/kubectl/plugins/get/plugin.yaml
deleted
100644 → 0
View file @
d745508c
name
:
"
get"
shortDesc
:
"
The
wonderful
new
plugin-based
get!"
command
:
"
echo
new-get"
test/fixtures/pkg/kubectl/plugins/incomplete/plugin.yaml
deleted
100644 → 0
View file @
d745508c
name
:
"
incomplete"
shortDesc
:
"
Incomplete
plugin"
test/fixtures/pkg/kubectl/plugins/kubectl-foo
0 → 100755
View file @
4bdc6363
#!/bin/bash
echo
"I am plugin foo"
test/fixtures/pkg/kubectl/plugins/tree/plugin.yaml
deleted
100644 → 0
View file @
d745508c
name
:
"
tree"
shortDesc
:
"
Plugin
with
a
tree
of
commands"
tree
:
-
name
:
"
child1"
shortDesc
:
"
The
first
child
of
a
tree"
command
:
echo child one
-
name
:
"
child2"
shortDesc
:
"
The
second
child
of
a
tree"
command
:
echo child two
-
name
:
"
child3"
shortDesc
:
"
The
third
child
of
a
tree"
command
:
echo child three
test/fixtures/pkg/kubectl/plugins/version/kubectl-version
0 → 100755
View file @
4bdc6363
#!/bin/bash
# This plugin is a no-op and is used to test
# a plugin that overshadows an existing plugin
test/fixtures/pkg/kubectl/plugins2/hello/hello.sh
deleted
100755 → 0
View file @
d745508c
#!/usr/bin/env bash
# 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.
echo
"#######"
echo
"#hello#"
echo
"#######"
test/fixtures/pkg/kubectl/plugins2/hello/plugin.yaml
deleted
100644 → 0
View file @
d745508c
name
:
hello
shortDesc
:
"
The
hello
plugin"
longDesc
:
>
The hello plugin is a new
plugin used by test-cmd
to test multiple plugin locations.
command
:
./hello.sh
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment