mirror of
https://github.com/k3s-io/kubernetes.git
synced 2026-01-04 23:17:50 +00:00
Basic support for kubectl plugins
This commit is contained in:
48
pkg/kubectl/plugins/BUILD
Normal file
48
pkg/kubectl/plugins/BUILD
Normal file
@@ -0,0 +1,48 @@
|
||||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
licenses(["notice"])
|
||||
|
||||
load(
|
||||
"@io_bazel_rules_go//go:def.bzl",
|
||||
"go_library",
|
||||
"go_test",
|
||||
)
|
||||
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = [
|
||||
"loader.go",
|
||||
"plugins.go",
|
||||
"runner.go",
|
||||
],
|
||||
tags = ["automanaged"],
|
||||
deps = [
|
||||
"//vendor/github.com/ghodss/yaml:go_default_library",
|
||||
"//vendor/github.com/golang/glog:go_default_library",
|
||||
"//vendor/k8s.io/client-go/tools/clientcmd: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 = [
|
||||
"loader_test.go",
|
||||
"plugins_test.go",
|
||||
"runner_test.go",
|
||||
],
|
||||
library = ":go_default_library",
|
||||
tags = ["automanaged"],
|
||||
)
|
||||
69
pkg/kubectl/plugins/examples/aging/aging.rb
Executable file
69
pkg/kubectl/plugins/examples/aging/aging.rb
Executable file
@@ -0,0 +1,69 @@
|
||||
#!/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
|
||||
|
||||
pods_json = `kubectl 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
|
||||
|
||||
8
pkg/kubectl/plugins/examples/aging/plugin.yaml
Normal file
8
pkg/kubectl/plugins/examples/aging/plugin.yaml
Normal file
@@ -0,0 +1,8 @@
|
||||
name: "aging"
|
||||
shortDesc: "Aging shows pods by age"
|
||||
longDesc: >
|
||||
Aging shows pods from the current namespace by age.
|
||||
Once we have plugin support for global flags through
|
||||
env vars (planned for V1) we'll be able to switch
|
||||
between namespaces using the --namespace flag.
|
||||
command: ./aging.rb
|
||||
3
pkg/kubectl/plugins/examples/hello/plugin.yaml
Normal file
3
pkg/kubectl/plugins/examples/hello/plugin.yaml
Normal file
@@ -0,0 +1,3 @@
|
||||
name: "hello"
|
||||
shortDesc: "I say hello!"
|
||||
command: "echo Hello plugins!"
|
||||
192
pkg/kubectl/plugins/loader.go
Normal file
192
pkg/kubectl/plugins/loader.go
Normal file
@@ -0,0 +1,192 @@
|
||||
/*
|
||||
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"
|
||||
)
|
||||
|
||||
const PluginDescriptorFilename = "plugin.yaml"
|
||||
|
||||
// PluginLoader is capable of loading a list of plugin descriptions.
|
||||
type PluginLoader interface {
|
||||
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 fileInfo.IsDir() || fileInfo.Name() != PluginDescriptorFilename || walkErr != nil {
|
||||
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
|
||||
}
|
||||
|
||||
plugin.Dir = filepath.Dir(path)
|
||||
plugin.DescriptorName = fileInfo.Name()
|
||||
|
||||
glog.V(6).Infof("Plugin loaded: %s", plugin.Name)
|
||||
list = append(list, plugin)
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return list, err
|
||||
}
|
||||
|
||||
// UserDirPluginLoader is 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 is 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
|
||||
}
|
||||
|
||||
// PluginsEnvVarPluginLoader is a PluginLoader that loads plugins from one or more
|
||||
// directories specified by the KUBECTL_PLUGINS_PATH env var.
|
||||
func PluginsEnvVarPluginLoader() PluginLoader {
|
||||
return PathFromEnvVarPluginLoader("KUBECTL_PLUGINS_PATH")
|
||||
}
|
||||
|
||||
// XDGDataPluginLoader is 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 XDGDataPluginLoader() PluginLoader {
|
||||
envVarName := "XDG_DATA_DIRS"
|
||||
if len(os.Getenv(envVarName)) > 0 {
|
||||
return PathFromEnvVarPluginLoader(envVarName, "kubectl", "plugins")
|
||||
}
|
||||
return TolerantMultiPluginLoader{
|
||||
&DirectoryPluginLoader{
|
||||
Directory: "/usr/local/share",
|
||||
},
|
||||
&DirectoryPluginLoader{
|
||||
Directory: "/usr/share",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
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
|
||||
|
||||
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 loads nothing.
|
||||
type DummyPluginLoader struct{}
|
||||
|
||||
func (l *DummyPluginLoader) Load() (Plugins, error) {
|
||||
return Plugins{}, nil
|
||||
}
|
||||
197
pkg/kubectl/plugins/loader_test.go
Normal file
197
pkg/kubectl/plugins/loader_test.go
Normal file
@@ -0,0 +1,197 @@
|
||||
/*
|
||||
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)
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 TestPluginsEnvVarPluginLoader(t *testing.T) {
|
||||
tmp, err := setupValidPlugins(1)
|
||||
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 := PluginsEnvVarPluginLoader()
|
||||
|
||||
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 setupValidPlugins(count int) (string, error) {
|
||||
tmp, err := ioutil.TempDir("", "")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("unexpected ioutil.TempDir error: %v", err)
|
||||
}
|
||||
|
||||
for i := 1; i <= count; i++ {
|
||||
name := fmt.Sprintf("plugin%d", i)
|
||||
descriptor := fmt.Sprintf(`
|
||||
name: %[1]s
|
||||
shortDesc: The %[1]s test plugin
|
||||
command: echo %[1]s`, 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
|
||||
}
|
||||
58
pkg/kubectl/plugins/plugins.go
Normal file
58
pkg/kubectl/plugins/plugins.go
Normal file
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
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"
|
||||
|
||||
// Plugin is the representation of a CLI extension (plugin).
|
||||
type Plugin struct {
|
||||
Description
|
||||
Source
|
||||
Context RunningContext `json:"-"`
|
||||
}
|
||||
|
||||
// PluginDescription 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"`
|
||||
}
|
||||
|
||||
// PluginSource holds the location of a given plugin in the filesystem.
|
||||
type Source struct {
|
||||
Dir string `json:"-"`
|
||||
DescriptorName string `json:"-"`
|
||||
}
|
||||
|
||||
var IncompleteError = fmt.Errorf("incomplete plugin descriptor: name, shortDesc and command fields are required")
|
||||
|
||||
func (p Plugin) Validate() error {
|
||||
if len(p.Name) == 0 || len(p.ShortDesc) == 0 || len(p.Command) == 0 {
|
||||
return IncompleteError
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p Plugin) IsValid() bool {
|
||||
return p.Validate() == nil
|
||||
}
|
||||
|
||||
// Plugins is a list of plugins.
|
||||
type Plugins []*Plugin
|
||||
70
pkg/kubectl/plugins/plugins_test.go
Normal file
70
pkg/kubectl/plugins/plugins_test.go
Normal file
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
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 (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPlugin(t *testing.T) {
|
||||
tests := []struct {
|
||||
plugin Plugin
|
||||
expectedErr string
|
||||
expectedValid bool
|
||||
}{
|
||||
{
|
||||
plugin: Plugin{
|
||||
Description: Description{
|
||||
Name: "test",
|
||||
ShortDesc: "The test",
|
||||
Command: "echo 1",
|
||||
},
|
||||
},
|
||||
expectedValid: true,
|
||||
},
|
||||
{
|
||||
plugin: Plugin{
|
||||
Description: Description{
|
||||
Name: "test",
|
||||
ShortDesc: "The test",
|
||||
},
|
||||
},
|
||||
expectedErr: "incomplete",
|
||||
},
|
||||
{
|
||||
plugin: Plugin{},
|
||||
expectedErr: "incomplete",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
if is := test.plugin.IsValid(); test.expectedValid != is {
|
||||
t.Errorf("%s: expected valid=%v, got %v", test.plugin.Name, test.expectedValid, is)
|
||||
}
|
||||
err := test.plugin.Validate()
|
||||
if len(test.expectedErr) > 0 {
|
||||
if err == nil {
|
||||
t.Errorf("%s: expected error, got none", test.plugin.Name)
|
||||
} else if !strings.Contains(err.Error(), test.expectedErr) {
|
||||
t.Errorf("%s: expected error containing %q, got %v", test.plugin.Name, test.expectedErr, err)
|
||||
}
|
||||
} else if err != nil {
|
||||
t.Errorf("%s: expected no error, got %v", test.plugin.Name, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
69
pkg/kubectl/plugins/runner.go
Normal file
69
pkg/kubectl/plugins/runner.go
Normal file
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
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 (
|
||||
"io"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/golang/glog"
|
||||
)
|
||||
|
||||
// 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 {
|
||||
In io.Reader
|
||||
Out io.Writer
|
||||
ErrOut io.Writer
|
||||
Args []string
|
||||
Env []string
|
||||
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(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
|
||||
|
||||
cmd.Env = ctx.Env
|
||||
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()
|
||||
}
|
||||
71
pkg/kubectl/plugins/runner_test.go
Normal file
71
pkg/kubectl/plugins/runner_test.go
Normal file
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
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 (
|
||||
"bytes"
|
||||
"testing"
|
||||
)
|
||||
|
||||
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",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
outBuf := bytes.NewBuffer([]byte{})
|
||||
|
||||
plugin := &Plugin{
|
||||
Description: Description{
|
||||
Name: test.name,
|
||||
ShortDesc: "Test Runner Plugin",
|
||||
Command: test.command,
|
||||
},
|
||||
}
|
||||
|
||||
ctx := RunningContext{
|
||||
Out: outBuf,
|
||||
WorkingDir: ".",
|
||||
}
|
||||
|
||||
runner := &ExecPluginRunner{}
|
||||
err := runner.Run(plugin, ctx)
|
||||
|
||||
if outBuf.String() != test.expectedMsg {
|
||||
t.Errorf("%s: unexpected output: %q", test.name, outBuf.String())
|
||||
}
|
||||
|
||||
if err != nil && err.Error() != test.expectedErr {
|
||||
t.Errorf("%s: unexpected err output: %v", test.name, err)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user