From b1306ae9f07df17087b210d6ab600b7ea7cbf434 Mon Sep 17 00:00:00 2001 From: Ted Teng Date: Fri, 12 May 2023 09:08:45 +0800 Subject: [PATCH] plugin feature --- README.md | 26 ++++++++++ internal/gen/markdown.go | 7 +++ pkg/cmd/cmd.go | 10 ++++ pkg/plugins/plugins.go | 94 +++++++++++++++++++++++++++++++++++++ pkg/plugins/plugins_test.go | 38 +++++++++++++++ 5 files changed, 175 insertions(+) create mode 100644 pkg/plugins/plugins.go create mode 100644 pkg/plugins/plugins_test.go diff --git a/README.md b/README.md index efef5c63..1265bd31 100644 --- a/README.md +++ b/README.md @@ -185,3 +185,29 @@ Establish an SSH connection to a Shoot cluster's node. ```bash gardenctl ssh my-node ``` +### Plugins +To use [plugins](https://pkg.go.dev/plugin) , you have to make a `plugins` directory under `.garden`, and place the plugin files(with a .so extension) within that directory. + +example plugin, +``` +package main + +import ( + "fmt" + "github.com/spf13/cobra" +) + +var NewCmd = &cobra.Command{ //must be `NewCmd` name + Use: "example", + Short: "A brief description of your command ", + Run: func(cmd *cobra.Command, args []string) { + fmt.Println("Test called") + }, +} + +``` + +build plugin `.so` file +``` +go build -buildmode=plugin +``` \ No newline at end of file diff --git a/internal/gen/markdown.go b/internal/gen/markdown.go index 5efee742..adac6df4 100644 --- a/internal/gen/markdown.go +++ b/internal/gen/markdown.go @@ -29,6 +29,13 @@ func main() { outDir = "./docs/help" } + for _, c := range gardenctl.Commands() { + _, ok := c.Annotations["plugins"] + if ok { + gardenctl.RemoveCommand(c) + } + } + err := doc.GenMarkdownTree(gardenctl, outDir) if err != nil { log.Fatal(err) diff --git a/pkg/cmd/cmd.go b/pkg/cmd/cmd.go index e9be39d3..ba9561cf 100644 --- a/pkg/cmd/cmd.go +++ b/pkg/cmd/cmd.go @@ -29,6 +29,7 @@ import ( cmdsshpatch "github.com/gardener/gardenctl-v2/pkg/cmd/sshpatch" cmdtarget "github.com/gardener/gardenctl-v2/pkg/cmd/target" cmdversion "github.com/gardener/gardenctl-v2/pkg/cmd/version" + "github.com/gardener/gardenctl-v2/pkg/plugins" ) const ( @@ -124,6 +125,15 @@ Find more information at: https://github.com/gardener/gardenctl-v2/blob/master/R cmd.AddCommand(cmdrc.NewCmdRC(f, ioStreams)) cmd.AddCommand(kubeconfig.NewCmdKubeconfig(f, ioStreams)) + for _, c := range plugins.Load() { + _, ok := c.Annotations["plugins"] + if !ok { + c.Annotations = map[string]string{"plugins": "yes"} + } + + cmd.AddCommand(c) + } + return cmd } diff --git a/pkg/plugins/plugins.go b/pkg/plugins/plugins.go new file mode 100644 index 00000000..ef0b5cca --- /dev/null +++ b/pkg/plugins/plugins.go @@ -0,0 +1,94 @@ +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and Gardener contributors + +SPDX-License-Identifier: Apache-2.0 +*/ + +package plugins + +import ( + "fmt" + "os" + "path" + "path/filepath" + "plugin" + + "github.com/mitchellh/go-homedir" + "github.com/spf13/cobra" +) + +const ( + gardenHomeFolder = ".garden" + pluginsFolder = "plugins" +) + +func LoadPlugins(pluginPath string) ([]*cobra.Command, error) { + plugins := []string{} + + var b plugin.Symbol + + var cmds []*cobra.Command + + files, err := os.ReadDir(pluginPath) + if err != nil { + return nil, err + } + + for _, file := range files { + filePath := filepath.Join(pluginPath, file.Name()) + ext := path.Ext(filePath) + + if ext == ".so" { + plugins = append(plugins, filePath) + } + } + + if len(plugins) == 0 { + return nil, nil + } + + for _, item := range plugins { + p, err := plugin.Open(item) + if err != nil { + return nil, fmt.Errorf("plugin open error or invalid file %w", err) + } + + b, err = p.Lookup("NewCmd") + if err != nil { + return nil, fmt.Errorf("plugin cmd name not found %w", err) + } + + _, ok := b.(**cobra.Command) + if !ok { + return nil, fmt.Errorf("type assertion error") + } + + cmds = append(cmds, *b.(**cobra.Command)) + } + + return cmds, nil +} + +func Load() []*cobra.Command { + var cmds []*cobra.Command + + home, err := homedir.Dir() + if err != nil { + fmt.Fprintln(os.Stderr, "Plugins Error:", err) + } + + pluginsPath := filepath.Join(home, gardenHomeFolder, pluginsFolder) + + if _, err := os.Stat(pluginsPath); err != nil { + if os.IsNotExist(err) { + return nil + } + } else { + cmds, err = LoadPlugins(pluginsPath) + if err != nil { + fmt.Fprintln(os.Stderr, "Plugins Error:", err) + } + } + + return cmds +} diff --git a/pkg/plugins/plugins_test.go b/pkg/plugins/plugins_test.go new file mode 100644 index 00000000..7003dbd3 --- /dev/null +++ b/pkg/plugins/plugins_test.go @@ -0,0 +1,38 @@ +/* +SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and Gardener contributors + +SPDX-License-Identifier: Apache-2.0 +*/ + +package plugins_test + +import ( + "os" + "path/filepath" + "testing" + + "github.com/gardener/gardenctl-v2/pkg/plugins" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestPlugins(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Plugins Package Test Suite") +} + +var _ = Describe("plugins", func() { + It("should return an error when plugins loading fake example.so", func() { + pluginsPath, err := os.MkdirTemp("", "plugins") + Expect(err).NotTo(HaveOccurred()) + + filename := filepath.Join(pluginsPath, "example.so") + _, err = os.Create(filename) + Expect(err).NotTo(HaveOccurred()) + + _, err = plugins.LoadPlugins(pluginsPath) + Expect(err).To(MatchError(MatchRegexp("^plugin open error or invalid file "))) + Expect(os.RemoveAll(pluginsPath)).To(Succeed()) + }) +})