diff --git a/Makefile b/Makefile index 3525fe8e1..75c8993a5 100644 --- a/Makefile +++ b/Makefile @@ -5,9 +5,10 @@ EXECUTABLE_CLI := airshipctl SCRIPTS_DIR := scripts -PLUGIN_DIR := plugins -PLUGIN_SOURCES := $(wildcard $(PLUGIN_DIR)/*/*.go) -PLUGIN_OBJECTS := $(PLUGIN_SOURCES:%.go=%.so) +PLUGIN_DIR := _plugins +PLUGIN_BIN := $(PLUGIN_DIR)/bin +PLUGIN_INT := $(patsubst $(PLUGIN_DIR)/internal/%,$(PLUGIN_BIN)/%.so,$(wildcard $(PLUGIN_DIR)/internal/*)) +PLUGIN_EXT := $(wildcard $(PLUGIN_DIR)/external/*) # linting LINTER_CMD := "github.com/golangci/golangci-lint/cmd/golangci-lint" run @@ -56,6 +57,10 @@ clean: @rm -fr $(BINDIR) @rm -fr $(COVER_FILE) +.PHONY: plugin-clean +plugin-clean: + @rm -fr $(PLUGIN_BIN) + .PHONY: docs docs: @echo "TODO" @@ -67,7 +72,8 @@ update-golden: @go test $(PKG) $(TESTFLAGS) .PHONY: plugin -plugin: $(PLUGIN_OBJECTS) +plugin: $(PLUGIN_INT) + @for plugin in $(PLUGIN_EXT); do $(MAKE) -C $${plugin}; done -%.so: %.go - @go build -buildmode=plugin -o $@ $< +$(PLUGIN_BIN)/%.so: $(PLUGIN_DIR)/*/%/*.go + @go build -buildmode=plugin -o $@ $^ diff --git a/_plugins/external/example/example.go b/_plugins/external/example/example.go new file mode 100644 index 000000000..5e241be34 --- /dev/null +++ b/_plugins/external/example/example.go @@ -0,0 +1,22 @@ +package main + +import ( + "fmt" + "io" + + "github.com/spf13/cobra" +) + +//nolint:deadcode,unused,unparam +func NewCommand(out io.Writer, args []string) *cobra.Command { + exampleCommand := &cobra.Command{ + Use: "example", + Short: "an example command", + // Hidden is set to true because this is an example + Hidden: true, + Run: func(cmd *cobra.Command, args []string) { + fmt.Fprintln(out, "Hello world!") + }, + } + return exampleCommand +} diff --git a/_plugins/external/example/makefile b/_plugins/external/example/makefile new file mode 100644 index 000000000..5a7c3d07d --- /dev/null +++ b/_plugins/external/example/makefile @@ -0,0 +1,2 @@ +all: + @go build -buildmode=plugin -o ../../bin/example.so example.go diff --git a/plugins/internal/workflow.go b/_plugins/internal/workflow/list.go similarity index 77% rename from plugins/internal/workflow.go rename to _plugins/internal/workflow/list.go index 04a692c45..63934e15c 100644 --- a/plugins/internal/workflow.go +++ b/_plugins/internal/workflow/list.go @@ -13,23 +13,6 @@ import ( "k8s.io/client-go/tools/clientcmd" ) -//nolint:unused -var kubeConfigFilePath string - -//nolint:deadcode,unused -func NewCommand(out io.Writer, args []string) *cobra.Command { - workflowRootCmd := &cobra.Command{ - Use: "workflow", - Short: "access to workflows", - Aliases: []string{"workflows", "wf"}, - } - - workflowRootCmd.PersistentFlags().StringVar(&kubeConfigFilePath, "kubeconfig", "", "path to kubeconfig") - workflowRootCmd.AddCommand(NewWorkflowListCommand(out, args)) - - return workflowRootCmd -} - //nolint:unused func NewWorkflowListCommand(out io.Writer, args []string) *cobra.Command { diff --git a/_plugins/internal/workflow/workflow.go b/_plugins/internal/workflow/workflow.go new file mode 100644 index 000000000..d64cabc01 --- /dev/null +++ b/_plugins/internal/workflow/workflow.go @@ -0,0 +1,24 @@ +package main + +import ( + "io" + + "github.com/spf13/cobra" +) + +//nolint:unused +var kubeConfigFilePath string + +//nolint:deadcode,unused +func NewCommand(out io.Writer, args []string) *cobra.Command { + workflowRootCmd := &cobra.Command{ + Use: "workflow", + Short: "access to workflows", + Aliases: []string{"workflows", "wf"}, + } + + workflowRootCmd.PersistentFlags().StringVar(&kubeConfigFilePath, "kubeconfig", "", "path to kubeconfig") + workflowRootCmd.AddCommand(NewWorkflowListCommand(out, args)) + + return workflowRootCmd +} diff --git a/cmd/root.go b/cmd/root.go index 4f18dddc5..1a935b55d 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -5,16 +5,20 @@ import ( "fmt" "io" "os" + "path/filepath" "github.com/ian-howell/airshipctl/pkg/environment" "github.com/ian-howell/airshipctl/pkg/log" "github.com/ian-howell/airshipctl/pkg/plugin" + "github.com/ian-howell/airshipctl/pkg/util" "github.com/spf13/cobra" ) var settings environment.AirshipCTLSettings +const defaultPluginDir = "_plugins/bin" + // NewRootCmd creates the root `airshipctl` command. All other commands are // subcommands branching from this one func NewRootCmd(out io.Writer, args []string) (*cobra.Command, error) { @@ -29,13 +33,15 @@ func NewRootCmd(out io.Writer, args []string) (*cobra.Command, error) { rootCmd.AddCommand(NewVersionCommand(out)) - workflowPlugin := "plugins/internal/workflow.so" - if _, err := os.Stat(workflowPlugin); err == nil { - rootCmd.AddCommand(plugin.CreateCommandFromPlugin(workflowPlugin, out, args)) - } - - if err := rootCmd.PersistentFlags().Parse(args); err != nil { - return nil, errors.New("could not parse flags: " + err.Error()) + if _, err := os.Stat(defaultPluginDir); err == nil { + pluginFiles, err := util.ReadDir(defaultPluginDir) + if err != nil { + return nil, errors.New("could not read plugins: " + err.Error()) + } + for _, pluginFile := range pluginFiles { + pathToPlugin := filepath.Join(defaultPluginDir, pluginFile.Name()) + rootCmd.AddCommand(plugin.CreateCommandFromPlugin(pathToPlugin, out, args)) + } } log.Init(&settings, out) diff --git a/pkg/util/test1.txt b/pkg/util/test1.txt new file mode 100644 index 000000000..830f58170 --- /dev/null +++ b/pkg/util/test1.txt @@ -0,0 +1 @@ +testdata \ No newline at end of file diff --git a/pkg/util/test2.txt b/pkg/util/test2.txt new file mode 100644 index 000000000..830f58170 --- /dev/null +++ b/pkg/util/test2.txt @@ -0,0 +1 @@ +testdata \ No newline at end of file diff --git a/pkg/util/test3.txt b/pkg/util/test3.txt new file mode 100644 index 000000000..830f58170 --- /dev/null +++ b/pkg/util/test3.txt @@ -0,0 +1 @@ +testdata \ No newline at end of file diff --git a/pkg/util/util.go b/pkg/util/util.go index e6d795e97..711c56948 100644 --- a/pkg/util/util.go +++ b/pkg/util/util.go @@ -12,3 +12,13 @@ func IsReadable(path string) error { } return f.Close() } + +// ReadDir does the same thing as ioutil.ReadDir, but it doesn't sort the files. +func ReadDir(dirname string) ([]os.FileInfo, error) { + f, err := os.Open(dirname) + if err != nil { + return []os.FileInfo{}, err + } + defer f.Close() + return f.Readdir(-1) +} diff --git a/pkg/util/util_test.go b/pkg/util/util_test.go index dab0217e9..e160c91df 100644 --- a/pkg/util/util_test.go +++ b/pkg/util/util_test.go @@ -3,6 +3,7 @@ package util_test import ( "io/ioutil" "os" + "path/filepath" "testing" "github.com/ian-howell/airshipctl/pkg/util" @@ -38,3 +39,51 @@ func TestIsReadable(t *testing.T) { t.Errorf("Expected '%s' error, got '%s'", expected, err.Error()) } } + +func TestReadDir(t *testing.T) { + dir, err := ioutil.TempDir("", "airshipctl-tests") + if err != nil { + t.Fatalf("Could not create a temporary directory: %s", err.Error()) + } + defer os.RemoveAll(dir) + + testFiles := []string{ + "test1.txt", + "test2.txt", + "test3.txt", + } + + for _, testFile := range testFiles { + if err := ioutil.WriteFile(filepath.Join(dir, testFile), []byte("testdata"), 0666); err != nil { + t.Fatalf("Could not create test file '%s': %s", testFile, err.Error()) + } + } + + files, err := util.ReadDir(dir) + if err != nil { + t.Fatalf("Unexpected error while reading directory: %s", err.Error()) + } + + if len(files) != len(testFiles) { + t.Errorf("Expected %d files, got %d", len(testFiles), len(files)) + } + + for _, testFile := range testFiles { + found := false + for _, actualFile := range files { + if testFile == actualFile.Name() { + found = true + break + } + } + if !found { + t.Errorf("Could not find test file '%s'", testFile) + } + } + + os.RemoveAll(dir) + + if _, err := util.ReadDir(dir); err == nil { + t.Error("Expected an error when reading non-existant directory") + } +}