Skip to content
Snippets Groups Projects
Commit 524fa4bc authored by bryanl's avatar bryanl
Browse files

add undocumented plugin support to ks


Signed-off-by: default avatarbryanl <bryanliles@gmail.com>
parent 218df6ac
No related branches found
No related tags found
No related merge requests found
......@@ -115,6 +115,3 @@
[[constraint]]
name = "k8s.io/client-go"
version = "5.0.0"
[[constraint]]
branch = "master"
name = "github.com/shibukawa/configdir"
......@@ -30,6 +30,7 @@ import (
"github.com/ksonnet/ksonnet/component"
"github.com/ksonnet/ksonnet/metadata"
"github.com/ksonnet/ksonnet/plugin"
str "github.com/ksonnet/ksonnet/strings"
"github.com/ksonnet/ksonnet/template"
"github.com/pkg/errors"
......@@ -109,6 +110,57 @@ application configuration to remote clusters.
return nil
},
Args: func(cmd *cobra.Command, args []string) error {
if len(args) == 0 {
return cobra.NoArgs(cmd, args)
}
pluginName := args[0]
_, err := plugin.Find(appFs, pluginName)
if err != nil {
return cobra.NoArgs(cmd, args)
}
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) == 0 {
return cmd.Help()
}
pluginName, args := args[0], args[1:]
p, err := plugin.Find(appFs, pluginName)
if err != nil {
return err
}
return runPlugin(p, args)
},
}
func runPlugin(p plugin.Plugin, args []string) error {
env := []string{
fmt.Sprintf("KS_PLUGIN_DIR=%s", p.RootDir),
fmt.Sprintf("KS_PLUGIN_NAME=%s", p.Config.Name),
}
root, err := appRoot()
if err != nil {
return err
}
appConfig := filepath.Join(root, "app.yaml")
exists, err := afero.Exists(appFs, appConfig)
if err != nil {
return err
}
if exists {
env = append(env, fmt.Sprintf("KS_APP_DIR=%s", root))
// TODO: make kube context or something similar available to the plugin
}
cmd := p.BuildRunCmd(env, args)
return cmd.Run()
}
func logLevel(verbosity int) log.Level {
......@@ -429,3 +481,7 @@ func importEnv(manager metadata.Manager, env string) (string, error) {
return fmt.Sprintf(`%s=%s`, metadata.EnvExtCodeKey, string(marshalled)), nil
}
func appRoot() (string, error) {
return os.Getwd()
}
package plugin
import (
"os"
"os/exec"
"path/filepath"
"strings"
"github.com/pkg/errors"
"github.com/spf13/afero"
yaml "gopkg.in/yaml.v2"
)
// Config is configuration for a Plugin.
type Config struct {
// Name is the name of the plugin.
Name string `yaml:"name,omitempty"`
// Version is the version of the plugin.
Version string `yaml:"version,omitempty"`
// Description is the plugin description.
Description string `yaml:"description,omitempty"`
// IgnoreFlags is set if this plugin will ignore flags.
IgnoreFlags bool `yaml:"ignore_flags,omitempty"`
// Command is the command that needs to be called to invoke the plugin.
Command string `yaml:"command,omitempty"`
}
func readConfig(fs afero.Fs, path string) (Config, error) {
b, err := afero.ReadFile(fs, path)
if err != nil {
return Config{}, err
}
var config Config
if err := yaml.Unmarshal(b, &config); err != nil {
return Config{}, err
}
return config, nil
}
// Plugin is a ksonnet plugin.
type Plugin struct {
// RootDir is the root directory for the plugin.
RootDir string
// Config is configuration for the plugin.
Config Config
}
// BuildRunCmd builds a command that runs the plugin.
func (p *Plugin) BuildRunCmd(env, args []string) *exec.Cmd {
bin := strings.Replace(p.Config.Command, "$KS_PLUGIN_DIR", p.RootDir, 1)
cmd := exec.Command(bin, args...)
cmd.Env = env
cmd.Stderr = os.Stderr
cmd.Stdout = os.Stdout
return cmd
}
// importPlugin creates a new Plugin given a path.
func importPlugin(fs afero.Fs, path string) (Plugin, error) {
configPath := filepath.Join(path, "plugin.yaml")
exists, err := afero.Exists(fs, configPath)
if err != nil {
return Plugin{}, err
}
if !exists {
return Plugin{}, errors.Errorf("plugin in %q doesn't not a configuration", path)
}
config, err := readConfig(fs, configPath)
if err != nil {
return Plugin{}, errors.Wrapf(err, "read plugin configuration %q", configPath)
}
plugin := Plugin{
RootDir: path,
Config: config,
}
return plugin, nil
}
// Find finds a plugin by name.
func Find(fs afero.Fs, name string) (Plugin, error) {
plugins, err := List(fs)
if err != nil {
return Plugin{}, err
}
for _, plugin := range plugins {
if plugin.Config.Name == name {
return plugin, nil
}
}
return Plugin{}, errors.Errorf("%s is not a known plugin", name)
}
// List plugins
func List(fs afero.Fs) ([]Plugin, error) {
rootPath, err := pluginDir()
if err != nil {
return []Plugin{}, err
}
exist, err := afero.Exists(fs, rootPath)
if err != nil {
return nil, err
}
if !exist {
return []Plugin{}, nil
}
fis, err := afero.ReadDir(fs, rootPath)
if err != nil {
return nil, err
}
plugins := make([]Plugin, 0)
for _, fi := range fis {
if fi.IsDir() {
path := filepath.Join(rootPath, fi.Name())
plugin, err := importPlugin(fs, path)
if err != nil {
return nil, err
}
plugins = append(plugins, plugin)
}
}
return plugins, nil
}
// TODO: make this work with windows
func pluginDir() (string, error) {
homeDir := os.Getenv("HOME")
if homeDir == "" {
return "", errors.New("could not find home directory")
}
return filepath.Join(homeDir, ".config", "ksonnet", "plugins"), nil
}
package plugin
import (
"archive/tar"
"io"
"os"
"path/filepath"
"testing"
"github.com/spf13/afero"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestList(t *testing.T) {
withPluginEnv(t, func(fs afero.Fs) {
plugins, err := List(fs)
require.NoError(t, err)
require.Len(t, plugins, 1)
plugin := plugins[0]
assert.Equal(t, "/home/app/.config/ksonnet/plugins/hello", plugin.RootDir)
assert.Equal(t, "hello", plugin.Config.Name)
assert.Equal(t, "0.1.0", plugin.Config.Version)
assert.Equal(t, "Hello from a ksonnet plugin", plugin.Config.Description)
assert.Equal(t, "$KS_PLUGIN_DIR/hello.sh", plugin.Config.Command)
})
}
func TestFind(t *testing.T) {
cases := []struct {
name string
isErr bool
}{
{
name: "hello",
},
{
name: "missing",
isErr: true,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
withPluginEnv(t, func(fs afero.Fs) {
_, err := Find(fs, tc.name)
if tc.isErr {
require.Error(t, err)
} else {
require.NoError(t, err)
}
})
})
}
}
func TestBuildRunCmd(t *testing.T) {
plugin := Plugin{
RootDir: "/tmp",
Config: Config{Command: "$KS_PLUGIN_DIR/runner"},
}
env := []string{"KS_1=1"}
args := []string{"--arg1", "--foo=2", "single"}
cmd := plugin.BuildRunCmd(env, args)
assert.Equal(t, os.Stderr, cmd.Stderr)
assert.Equal(t, os.Stdout, cmd.Stdout)
assert.Equal(t, env, cmd.Env)
assert.Equal(t, append([]string{"/tmp/runner"}, args...), cmd.Args, "args")
}
type pluginFn func(afero.Fs)
func withPluginEnv(t *testing.T, fn pluginFn) {
ogHome := os.Getenv("HOME")
os.Setenv("HOME", "/home/app")
defer os.Setenv("HOME", ogHome)
fs := afero.NewMemMapFs()
fs.MkdirAll("/home/app/.config/ksonnet/plugins", 0755)
r, err := os.Open("testdata/hello.tar")
require.NoError(t, err)
err = untar(fs, "/home/app/.config/ksonnet/plugins", r)
require.NoError(t, err)
fn(fs)
}
// nolint: gocyclo
func untar(fs afero.Fs, dst string, r io.Reader) error {
tr := tar.NewReader(r)
for {
header, err := tr.Next()
switch {
case err == io.EOF:
return nil
case err != nil:
return err
case header == nil:
continue
}
target := filepath.Join(dst, header.Name)
switch header.Typeflag {
case tar.TypeDir:
if _, err := fs.Stat(target); err != nil {
if err := fs.MkdirAll(target, 0755); err != nil {
return err
}
}
case tar.TypeReg:
f, err := fs.OpenFile(target, os.O_CREATE|os.O_RDWR, os.FileMode(header.Mode))
if err != nil {
return err
}
defer f.Close()
if _, err := io.Copy(f, tr); err != nil {
return err
}
}
}
}
File added
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment