From 9ac3fb025e6b8bd3230ea9aa852c065e97c99cd0 Mon Sep 17 00:00:00 2001 From: Vincent Demeester Date: Fri, 13 Nov 2020 09:22:43 +0100 Subject: [PATCH] =?UTF-8?q?Basic=20execution=20plugin=20module=20?= =?UTF-8?q?=F0=9F=93=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This adds a basic execution plugin model, very simple to be honest. Most of the work has been done by @sthaha. In a gist this is how it works - If the specified subcommand doesn't exists, we check if a command with the subcommand prefixed by `tkn-` exists in the `$HOME/.config/tkn/pulgins` (for `foo` we look for `tkn-foo`). - The default folder (`$HOME/.config/tkn/plugins`) can be overriden with `TKN_PLUGINS_DIR`. - If the binary doesn't exists, we proceed as usual - If the binary exists, we execute it "à-la" `exec …`, aka we give full hand to the binary. ```bash $ ls ~/.config/tkn/plugins/tkn-* /home/vincent/.config/tkn/plugins/tkn-bar /home/vincent/.config/tkn/plugins/tkn-foo $ tkn foo is bar --hello args is bar --hello fooo bar $ tkn bar bar /usr/bin/env: ‘invalidcommand’: No such file or directory ``` Signed-off-by: Sunil Thaha Signed-off-by: Vincent Demeester --- cmd/tkn/main.go | 59 +++++++++ go.mod | 1 + test/e2e/plugin/plugin_test.go | 49 ++++++++ test/e2e/plugin/tkn-failure | 6 + test/e2e/plugin/tkn-success | 3 + vendor/gotest.tools/v3/env/env.go | 118 ++++++++++++++++++ .../v3/internal/cleanup/cleanup.go | 45 +++++++ vendor/gotest.tools/v3/x/subtest/context.go | 84 +++++++++++++ vendor/modules.txt | 3 + 9 files changed, 368 insertions(+) create mode 100644 test/e2e/plugin/plugin_test.go create mode 100755 test/e2e/plugin/tkn-failure create mode 100755 test/e2e/plugin/tkn-success create mode 100644 vendor/gotest.tools/v3/env/env.go create mode 100644 vendor/gotest.tools/v3/internal/cleanup/cleanup.go create mode 100644 vendor/gotest.tools/v3/x/subtest/context.go diff --git a/cmd/tkn/main.go b/cmd/tkn/main.go index 866b00e23f..dafc478d62 100644 --- a/cmd/tkn/main.go +++ b/cmd/tkn/main.go @@ -15,18 +15,77 @@ package main import ( + "fmt" "os" + "path/filepath" + "syscall" + homedir "github.com/mitchellh/go-homedir" + "github.com/pkg/errors" "github.com/tektoncd/cli/pkg/cli" "github.com/tektoncd/cli/pkg/cmd" _ "k8s.io/client-go/plugin/pkg/client/auth" ) +const ( + pluginDirEnv = "TKN_PLUGINS_DIR" + pluginDir = "~/.config/tkn/plugins" +) + func main() { tp := &cli.TektonParams{} tkn := cmd.Root(tp) + args := os.Args[1:] + cmd, _, _ := tkn.Find(args) + if cmd != nil && cmd == tkn && len(args) > 0 { + pluginCmd := "tkn-" + os.Args[1] + pluginDir, err := getPluginDir() + if err != nil { + fmt.Fprintf(os.Stderr, "Error getting plugin folder: %v", err) + } + exCmd, err := findPlugin(pluginDir, pluginCmd) + if err != nil { + // Can't find the binary in PATH, bailing to usual execution + goto CoreTkn + } + + if err := syscall.Exec(exCmd, append([]string{exCmd}, os.Args[2:]...), os.Environ()); err != nil { + fmt.Fprintf(os.Stderr, "Command finished with error: %v", err) + os.Exit(127) + } + return + } + +CoreTkn: if err := tkn.Execute(); err != nil { os.Exit(1) } } + +func getPluginDir() (string, error) { + dir := os.Getenv(pluginDirEnv) + // if TKN_PLUGINS_DIR is set, follow it + if dir != "" { + return dir, nil + } + // Respect XDG_CONFIG_HOME if set + if xdgHome := os.Getenv("XDG_CONFIG_HOME"); xdgHome != "" { + return filepath.Join(xdgHome, "tkn", "plugins"), nil + } + // Fallback to default pluginDir (~/.config/tkn/plugins) + return homedir.Expand(pluginDir) +} + +func findPlugin(dir, cmd string) (string, error) { + path := filepath.Join(dir, cmd) + _, err := os.Stat(path) + if err == nil { + // Found in dir + return path, nil + } + if !os.IsNotExist(err) { + return "", errors.Wrap(err, fmt.Sprintf("i/o error while reading %s", path)) + } + return "", err +} diff --git a/go.mod b/go.mod index 280dc97d14..3b2a8fcd71 100644 --- a/go.mod +++ b/go.mod @@ -14,6 +14,7 @@ require ( github.com/hinshun/vt10x v0.0.0-20180809195222-d55458df857c github.com/jonboulle/clockwork v0.1.1-0.20190114141812-62fb9bc030d1 github.com/ktr0731/go-fuzzyfinder v0.2.0 + github.com/mitchellh/go-homedir v1.1.0 github.com/pkg/errors v0.9.1 github.com/spf13/cobra v1.0.0 github.com/spf13/pflag v1.0.5 diff --git a/test/e2e/plugin/plugin_test.go b/test/e2e/plugin/plugin_test.go new file mode 100644 index 0000000000..dbb24e30c2 --- /dev/null +++ b/test/e2e/plugin/plugin_test.go @@ -0,0 +1,49 @@ +// +build e2e +// Copyright © 2020 The Tekton 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 e2e + +import ( + "os" + "testing" + + "github.com/tektoncd/cli/test/cli" + "gotest.tools/v3/assert" + "gotest.tools/v3/env" + "gotest.tools/v3/icmd" +) + +func TestTknPlugin(t *testing.T) { + tkn, err := cli.NewTknRunner("any-namespace") + assert.NilError(t, err) + currentpath, err := os.Getwd() + assert.NilError(t, err) + defer env.Patch(t, "TKN_PLUGINS_DIR", currentpath)() + t.Run("Success", func(t *testing.T) { + tkn.MustSucceed(t, "success") + tkn.MustSucceed(t, "success", "with", "args") + }) + t.Run("Failure", func(t *testing.T) { + tkn.Run("failure").Assert(t, icmd.Expected{ + ExitCode: 12, + }) + tkn.Run("failure", "with", "args").Assert(t, icmd.Expected{ + ExitCode: 12, + }) + tkn.Run("failure", "exit20").Assert(t, icmd.Expected{ + ExitCode: 20, + }) + }) +} diff --git a/test/e2e/plugin/tkn-failure b/test/e2e/plugin/tkn-failure new file mode 100755 index 0000000000..777ba3ac74 --- /dev/null +++ b/test/e2e/plugin/tkn-failure @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +echo "This is a failure" +if [[ "$1" == "exit20" ]]; then + exit 20 +fi +exit 12 \ No newline at end of file diff --git a/test/e2e/plugin/tkn-success b/test/e2e/plugin/tkn-success new file mode 100755 index 0000000000..ed0c8b14fe --- /dev/null +++ b/test/e2e/plugin/tkn-success @@ -0,0 +1,3 @@ +#!/usr/bin/env bash +echo "This is a success" +exit 0 \ No newline at end of file diff --git a/vendor/gotest.tools/v3/env/env.go b/vendor/gotest.tools/v3/env/env.go new file mode 100644 index 0000000000..7a71acd0d7 --- /dev/null +++ b/vendor/gotest.tools/v3/env/env.go @@ -0,0 +1,118 @@ +/*Package env provides functions to test code that read environment variables +or the current working directory. +*/ +package env // import "gotest.tools/v3/env" + +import ( + "os" + "strings" + + "gotest.tools/v3/assert" + "gotest.tools/v3/internal/cleanup" +) + +type helperT interface { + Helper() +} + +// Patch changes the value of an environment variable, and returns a +// function which will reset the the value of that variable back to the +// previous state. +// +// When used with Go 1.14+ the unpatch function will be called automatically +// when the test ends, unless the TEST_NOCLEANUP env var is set to true. +func Patch(t assert.TestingT, key, value string) func() { + if ht, ok := t.(helperT); ok { + ht.Helper() + } + oldValue, envVarExists := os.LookupEnv(key) + assert.NilError(t, os.Setenv(key, value)) + clean := func() { + if ht, ok := t.(helperT); ok { + ht.Helper() + } + if !envVarExists { + assert.NilError(t, os.Unsetenv(key)) + return + } + assert.NilError(t, os.Setenv(key, oldValue)) + } + cleanup.Cleanup(t, clean) + return clean +} + +// PatchAll sets the environment to env, and returns a function which will +// reset the environment back to the previous state. +// +// When used with Go 1.14+ the unpatch function will be called automatically +// when the test ends, unless the TEST_NOCLEANUP env var is set to true. +func PatchAll(t assert.TestingT, env map[string]string) func() { + if ht, ok := t.(helperT); ok { + ht.Helper() + } + oldEnv := os.Environ() + os.Clearenv() + + for key, value := range env { + assert.NilError(t, os.Setenv(key, value), "setenv %s=%s", key, value) + } + clean := func() { + if ht, ok := t.(helperT); ok { + ht.Helper() + } + os.Clearenv() + for key, oldVal := range ToMap(oldEnv) { + assert.NilError(t, os.Setenv(key, oldVal), "setenv %s=%s", key, oldVal) + } + } + cleanup.Cleanup(t, clean) + return clean +} + +// ToMap takes a list of strings in the format returned by os.Environ() and +// returns a mapping of keys to values. +func ToMap(env []string) map[string]string { + result := map[string]string{} + for _, raw := range env { + key, value := getParts(raw) + result[key] = value + } + return result +} + +func getParts(raw string) (string, string) { + if raw == "" { + return "", "" + } + // Environment variables on windows can begin with = + // http://blogs.msdn.com/b/oldnewthing/archive/2010/05/06/10008132.aspx + parts := strings.SplitN(raw[1:], "=", 2) + key := raw[:1] + parts[0] + if len(parts) == 1 { + return key, "" + } + return key, parts[1] +} + +// ChangeWorkingDir to the directory, and return a function which restores the +// previous working directory. +// +// When used with Go 1.14+ the previous working directory will be restored +// automatically when the test ends, unless the TEST_NOCLEANUP env var is set to +// true. +func ChangeWorkingDir(t assert.TestingT, dir string) func() { + if ht, ok := t.(helperT); ok { + ht.Helper() + } + cwd, err := os.Getwd() + assert.NilError(t, err) + assert.NilError(t, os.Chdir(dir)) + clean := func() { + if ht, ok := t.(helperT); ok { + ht.Helper() + } + assert.NilError(t, os.Chdir(cwd)) + } + cleanup.Cleanup(t, clean) + return clean +} diff --git a/vendor/gotest.tools/v3/internal/cleanup/cleanup.go b/vendor/gotest.tools/v3/internal/cleanup/cleanup.go new file mode 100644 index 0000000000..7e2922f409 --- /dev/null +++ b/vendor/gotest.tools/v3/internal/cleanup/cleanup.go @@ -0,0 +1,45 @@ +/*Package cleanup handles migration to and support for the Go 1.14+ +testing.TB.Cleanup() function. +*/ +package cleanup + +import ( + "os" + "strings" + + "gotest.tools/v3/x/subtest" +) + +type cleanupT interface { + Cleanup(f func()) +} + +type logT interface { + Log(...interface{}) +} + +type helperT interface { + Helper() +} + +var noCleanup = strings.ToLower(os.Getenv("TEST_NOCLEANUP")) == "true" + +// Cleanup registers f as a cleanup function on t if any mechanisms are available. +// +// Skips registering f if TEST_NOCLEANUP is set to true. +func Cleanup(t logT, f func()) { + if ht, ok := t.(helperT); ok { + ht.Helper() + } + if noCleanup { + t.Log("skipping cleanup because TEST_NOCLEANUP was enabled.") + return + } + if ct, ok := t.(cleanupT); ok { + ct.Cleanup(f) + return + } + if tc, ok := t.(subtest.TestContext); ok { + tc.AddCleanup(f) + } +} diff --git a/vendor/gotest.tools/v3/x/subtest/context.go b/vendor/gotest.tools/v3/x/subtest/context.go new file mode 100644 index 0000000000..5750bf409f --- /dev/null +++ b/vendor/gotest.tools/v3/x/subtest/context.go @@ -0,0 +1,84 @@ +/*Package subtest provides a TestContext to subtests which handles cleanup, and +provides a testing.TB, and context.Context. + +This package was inspired by github.com/frankban/quicktest. +*/ +package subtest // import "gotest.tools/v3/x/subtest" + +import ( + "context" + "testing" +) + +type testcase struct { + testing.TB + ctx context.Context + cleanupFuncs []cleanupFunc +} + +type cleanupFunc func() + +func (tc *testcase) Ctx() context.Context { + if tc.ctx == nil { + var cancel func() + tc.ctx, cancel = context.WithCancel(context.Background()) + tc.AddCleanup(cancel) + } + return tc.ctx +} + +// cleanup runs all cleanup functions. Functions are run in the opposite order +// in which they were added. Cleanup is called automatically before Run exits. +func (tc *testcase) cleanup() { + for _, f := range tc.cleanupFuncs { + // Defer all cleanup functions so they all run even if one calls + // t.FailNow() or panics. Deferring them also runs them in reverse order. + defer f() + } + tc.cleanupFuncs = nil +} + +func (tc *testcase) AddCleanup(f func()) { + tc.cleanupFuncs = append(tc.cleanupFuncs, f) +} + +func (tc *testcase) Parallel() { + tp, ok := tc.TB.(parallel) + if !ok { + panic("Parallel called with a testing.B") + } + tp.Parallel() +} + +type parallel interface { + Parallel() +} + +// Run a subtest. When subtest exits, every cleanup function added with +// TestContext.AddCleanup will be run. +func Run(t *testing.T, name string, subtest func(t TestContext)) bool { + return t.Run(name, func(t *testing.T) { + tc := &testcase{TB: t} + defer tc.cleanup() + subtest(tc) + }) +} + +// TestContext provides a testing.TB and a context.Context for a test case. +type TestContext interface { + testing.TB + // AddCleanup function which will be run when before Run returns. + // + // Deprecated: Go 1.14+ now includes a testing.TB.Cleanup(func()) which + // should be used instead. AddCleanup will be removed in a future release. + AddCleanup(f func()) + // Ctx returns a context for the test case. Multiple calls from the same subtest + // will return the same context. The context is cancelled when Run + // returns. + Ctx() context.Context + // Parallel calls t.Parallel on the testing.TB. Panics if testing.TB does + // not implement Parallel. + Parallel() +} + +var _ TestContext = &testcase{} diff --git a/vendor/modules.txt b/vendor/modules.txt index 2a123df3b7..8e3ca748a5 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -678,11 +678,14 @@ gotest.tools/internal/source # gotest.tools/v3 v3.0.2 gotest.tools/v3/assert gotest.tools/v3/assert/cmp +gotest.tools/v3/env gotest.tools/v3/golden gotest.tools/v3/icmd +gotest.tools/v3/internal/cleanup gotest.tools/v3/internal/difflib gotest.tools/v3/internal/format gotest.tools/v3/internal/source +gotest.tools/v3/x/subtest # k8s.io/api v0.18.9 => k8s.io/api v0.18.9 k8s.io/api/admissionregistration/v1 k8s.io/api/admissionregistration/v1beta1