From 33fecc360b39a50ffc7a6e2f4641d59ed1416cc7 Mon Sep 17 00:00:00 2001 From: Ian Howell Date: Thu, 23 May 2019 14:34:28 -0500 Subject: [PATCH] Overhaul for plugin system --- cmd/plugins.go | 19 --------- cmd/root.go | 38 +----------------- cmd/workflow/workflow.go | 13 ++++-- cmd/workflow/workflow_init.go | 17 +++----- cmd/workflow/workflow_list.go | 15 +++---- go.sum | 1 + internal/test/utilities.go | 41 +------------------ main.go | 27 ++++++++++++- pkg/environment/settings.go | 49 +++++++---------------- pkg/workflow/environment/settings.go | 60 ++++++++++++++++++++++++++++ 10 files changed, 127 insertions(+), 153 deletions(-) delete mode 100644 cmd/plugins.go create mode 100644 pkg/workflow/environment/settings.go diff --git a/cmd/plugins.go b/cmd/plugins.go deleted file mode 100644 index 708b71559..000000000 --- a/cmd/plugins.go +++ /dev/null @@ -1,19 +0,0 @@ -package cmd - -import ( - "io" - - "github.com/spf13/cobra" - - "github.com/ian-howell/airshipctl/cmd/workflow" - - // "github.com/ian-howell/exampleplugin" - - "github.com/ian-howell/airshipctl/pkg/environment" -) - -// pluginCommands are the functions that create the entrypoint command for a plugin -var pluginCommands = []func(io.Writer, *environment.AirshipCTLSettings, []string) *cobra.Command{ - // exampleplugin.NewExampleCommand, // This is an example and shouldn't be enabled in production builds - workflow.NewWorkflowCommand, -} diff --git a/cmd/root.go b/cmd/root.go index a83551f3f..6ad53f3d9 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -1,55 +1,19 @@ package cmd import ( - "fmt" "io" - "os" "github.com/spf13/cobra" - - "github.com/ian-howell/airshipctl/pkg/environment" - "github.com/ian-howell/airshipctl/pkg/log" ) // NewRootCmd creates the root `airshipctl` command. All other commands are // subcommands branching from this one -func NewRootCmd(out io.Writer, settings *environment.AirshipCTLSettings, args []string) (*cobra.Command, error) { +func NewRootCmd(out io.Writer) (*cobra.Command, error) { rootCmd := &cobra.Command{ Use: "airshipctl", Short: "airshipctl is a unified entrypoint to various airship components", } rootCmd.SetOutput(out) - - settings.InitFlags(rootCmd) - rootCmd.AddCommand(NewVersionCommand(out)) - - loadPluginCommands(rootCmd, out, settings, args) - - rootCmd.PersistentFlags().Parse(args) - - settings.InitDefaults() - log.Init(settings, out) - return rootCmd, nil } - -// Execute runs the base airshipctl command -func Execute(out io.Writer) { - rootCmd, err := NewRootCmd(out, &environment.AirshipCTLSettings{}, os.Args[1:]) - if err != nil { - fmt.Fprintln(out, err) - os.Exit(1) - } - if err := rootCmd.Execute(); err != nil { - fmt.Fprintln(out, err) - os.Exit(1) - } -} - -// loadPluginCommands loads all of the plugins as subcommands to cmd -func loadPluginCommands(cmd *cobra.Command, out io.Writer, settings *environment.AirshipCTLSettings, args []string) { - for _, subcmd := range pluginCommands { - cmd.AddCommand(subcmd(out, settings, args)) - } -} diff --git a/cmd/workflow/workflow.go b/cmd/workflow/workflow.go index c2a297a53..3c6d1e5f2 100644 --- a/cmd/workflow/workflow.go +++ b/cmd/workflow/workflow.go @@ -6,18 +6,25 @@ import ( "github.com/spf13/cobra" "github.com/ian-howell/airshipctl/pkg/environment" + wfenv "github.com/ian-howell/airshipctl/pkg/workflow/environment" ) +// PluginSettingsID is used as a key in the root settings map of plugin settings +const PluginSettingsID = "argo" + // NewWorkflowCommand creates a new command for working with argo workflows -func NewWorkflowCommand(out io.Writer, settings *environment.AirshipCTLSettings, args []string) *cobra.Command { +func NewWorkflowCommand(out io.Writer, rootSettings *environment.AirshipCTLSettings) *cobra.Command { workflowRootCmd := &cobra.Command{ Use: "workflow", Short: "Access to argo workflows", Aliases: []string{"workflows", "wf"}, } - workflowRootCmd.AddCommand(NewWorkflowInitCommand(out, settings, args)) - workflowRootCmd.AddCommand(NewWorkflowListCommand(out, settings, args)) + wfSettings := &wfenv.Settings{} + wfSettings.InitFlags(workflowRootCmd) + workflowRootCmd.AddCommand(NewWorkflowInitCommand(out, wfSettings)) + workflowRootCmd.AddCommand(NewWorkflowListCommand(out, rootSettings)) + rootSettings.Register(PluginSettingsID, wfSettings) return workflowRootCmd } diff --git a/cmd/workflow/workflow_init.go b/cmd/workflow/workflow_init.go index f5e4c9c50..b1daf409a 100644 --- a/cmd/workflow/workflow_init.go +++ b/cmd/workflow/workflow_init.go @@ -13,9 +13,8 @@ import ( "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" - "k8s.io/client-go/rest" - "github.com/ian-howell/airshipctl/pkg/environment" + "github.com/ian-howell/airshipctl/pkg/workflow/environment" ) const ( @@ -28,16 +27,16 @@ var ( type workflowInitCmd struct { out io.Writer - config *rest.Config kubeclient kubernetes.Interface + crdclient apixv1beta1client.ApiextensionsV1beta1Interface } // NewWorkflowInitCommand is a command for bootstrapping a kubernetes cluster with the necessary components for Argo workflows -func NewWorkflowInitCommand(out io.Writer, settings *environment.AirshipCTLSettings, args []string) *cobra.Command { +func NewWorkflowInitCommand(out io.Writer, settings *environment.Settings) *cobra.Command { workflowInit := &workflowInitCmd{ out: out, - config: settings.KubeConfig, kubeclient: settings.KubeClient, + crdclient: settings.CRDClient, } workflowInitCommand := &cobra.Command{ Use: "init [flags]", @@ -160,11 +159,7 @@ func (wfInit *workflowInitCmd) createCustomObjects(manifestPath string) { } func (wfInit *workflowInitCmd) registerDefaultWorkflow() error { - apixClient, err := apixv1beta1client.NewForConfig(wfInit.config) - if err != nil { - return fmt.Errorf("Could not create CRD client: %s", err.Error()) - } - + apixClient := wfInit.crdclient crds := apixClient.CustomResourceDefinitions() workflowCRD := &apixv1beta1.CustomResourceDefinition{ ObjectMeta: metav1.ObjectMeta{ @@ -185,7 +180,7 @@ func (wfInit *workflowInitCmd) registerDefaultWorkflow() error { Scope: apixv1beta1.NamespaceScoped, }, } - _, err = crds.Create(workflowCRD) + _, err := crds.Create(workflowCRD) return err } diff --git a/cmd/workflow/workflow_list.go b/cmd/workflow/workflow_list.go index 97eaf4719..ae0f0a1a2 100644 --- a/cmd/workflow/workflow_list.go +++ b/cmd/workflow/workflow_list.go @@ -5,26 +5,27 @@ import ( "io" "text/tabwriter" - "github.com/argoproj/argo/pkg/client/clientset/versioned/typed/workflow/v1alpha1" "github.com/spf13/cobra" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "github.com/ian-howell/airshipctl/pkg/environment" + wfenv "github.com/ian-howell/airshipctl/pkg/workflow/environment" ) // NewWorkflowListCommand is a command for listing argo workflows -func NewWorkflowListCommand(out io.Writer, settings *environment.AirshipCTLSettings, args []string) *cobra.Command { +func NewWorkflowListCommand(out io.Writer, rootSettings *environment.AirshipCTLSettings) *cobra.Command { workflowListCmd := &cobra.Command{ Use: "list", Short: "list workflows", Aliases: []string{"ls"}, Run: func(cmd *cobra.Command, args []string) { - clientSet, err := v1alpha1.NewForConfig(settings.KubeConfig) - if err != nil { - panic(err.Error()) + wfSettings, ok := rootSettings.PluginSettings[PluginSettingsID].(*wfenv.Settings) + if !ok { + fmt.Fprintf(out, "settings for %s were not registered\n", PluginSettingsID) + return } - - wflist, err := clientSet.Workflows(settings.Namespace).List(v1.ListOptions{}) + clientSet := wfSettings.ArgoClient + wflist, err := clientSet.Workflows(wfSettings.Namespace).List(v1.ListOptions{}) if err != nil { panic(err.Error()) } diff --git a/go.sum b/go.sum index 4b1db7586..8f37a877e 100644 --- a/go.sum +++ b/go.sum @@ -70,6 +70,7 @@ github.com/go-openapi/validate v0.18.0/go.mod h1:Uh4HdOzKt19xGIGm1qHf/ofbX1YQ4Y+ github.com/gogo/protobuf v0.0.0-20171007142547-342cbe0a0415/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.1 h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903 h1:LbsanbbD6LieFkXbj9YNNBupiGHJgFeLpO0j0Fza1h8= github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= diff --git a/internal/test/utilities.go b/internal/test/utilities.go index fd1899ff3..43a5feb1f 100644 --- a/internal/test/utilities.go +++ b/internal/test/utilities.go @@ -6,12 +6,7 @@ import ( "io/ioutil" "os" "path/filepath" - "strings" "testing" - - "github.com/ian-howell/airshipctl/cmd" - "github.com/ian-howell/airshipctl/pkg/environment" - "k8s.io/client-go/kubernetes/fake" ) // UpdateGolden writes out the golden files with the latest values, rather than failing the test. @@ -26,41 +21,7 @@ const ( // CmdTest is a command to be run on the command line as a test type CmdTest struct { Name string - Command string -} - -// RunCmdTests checks all of the tests actual output against their expected outputs -func RunCmdTests(t *testing.T, tests []CmdTest) { - t.Helper() - for _, test := range tests { - cmdOutput := executeCmd(t, test.Command) - if *shouldUpdateGolden { - updateGolden(t, test, cmdOutput) - } else { - assertEqualGolden(t, test, cmdOutput) - } - } -} - -func executeCmd(t *testing.T, command string) []byte { - var actual bytes.Buffer - settings := &environment.AirshipCTLSettings{ - KubeClient: fake.NewSimpleClientset(), - } - // TODO(howell): switch to shellwords (or similar) - args := strings.Fields(command) - rootCmd, err := cmd.NewRootCmd(&actual, settings, args) - if err != nil { - t.Fatalf(err.Error()) - } - - rootCmd.SetArgs(args) - - if err := rootCmd.Execute(); err != nil { - t.Fatalf(err.Error()) - } - - return actual.Bytes() + CmdLine string } func updateGolden(t *testing.T, test CmdTest, actual []byte) { diff --git a/main.go b/main.go index 16b8d455e..8a9ba755a 100644 --- a/main.go +++ b/main.go @@ -1,11 +1,36 @@ package main import ( + "fmt" "os" "github.com/ian-howell/airshipctl/cmd" + "github.com/ian-howell/airshipctl/cmd/workflow" + + "github.com/ian-howell/airshipctl/pkg/environment" + "github.com/ian-howell/airshipctl/pkg/log" ) func main() { - cmd.Execute(os.Stdout) + rootCmd, err := cmd.NewRootCmd(os.Stdout) + if err != nil { + fmt.Fprintln(os.Stdout, err) + os.Exit(1) + } + + settings := &environment.AirshipCTLSettings{} + settings.InitFlags(rootCmd) + + rootCmd.AddCommand(workflow.NewWorkflowCommand(os.Stdout, settings)) + + rootCmd.PersistentFlags().Parse(os.Args[1:]) + + settings.Init() + + log.Init(settings, os.Stdout) + + if err := rootCmd.Execute(); err != nil { + fmt.Fprintln(os.Stdout, err) + os.Exit(1) + } } diff --git a/pkg/environment/settings.go b/pkg/environment/settings.go index 370560982..fe9c13b98 100644 --- a/pkg/environment/settings.go +++ b/pkg/environment/settings.go @@ -2,57 +2,36 @@ package environment import ( "github.com/spf13/cobra" - "k8s.io/client-go/kubernetes" - restclient "k8s.io/client-go/rest" - "k8s.io/client-go/tools/clientcmd" ) +type PluginSettings interface { + Init() error +} + // AirshipCTLSettings is a container for all of the settings needed by airshipctl type AirshipCTLSettings struct { - // KubeConfigFilePath is the path to the kubernetes configuration file. - // This flag is only needed when airshipctl is being used - // out-of-cluster - KubeConfigFilePath string - - // KubeConfig contains the configuration details needed for interacting - // with the cluster - KubeConfig *restclient.Config - - // KubeClient contains a kubernetes clientset - KubeClient kubernetes.Interface - - // Namespace is the kubernetes namespace to be used during the context of this action - Namespace string - // Debug is used for verbose output Debug bool + + PluginSettings map[string]PluginSettings } // InitFlags adds the default settings flags to cmd func (a *AirshipCTLSettings) InitFlags(cmd *cobra.Command) { flags := cmd.PersistentFlags() flags.BoolVar(&a.Debug, "debug", false, "enable verbose output") - flags.StringVar(&a.KubeConfigFilePath, "kubeconfig", "", "path to kubeconfig") - flags.StringVar(&a.Namespace, "namespace", "default", "kubernetes namespace to use for the context of this command") } -// InitDefaults assigns default values for any value that has not been previously set -func (a *AirshipCTLSettings) InitDefaults() error { - if a.KubeConfigFilePath == "" { - a.KubeConfigFilePath = clientcmd.RecommendedHomeFile +func (a *AirshipCTLSettings) Register(pluginName string, pluginSettings PluginSettings) { + if a.PluginSettings == nil { + a.PluginSettings = make(map[string]PluginSettings, 0) } + a.PluginSettings[pluginName] = pluginSettings +} - var err error - if a.KubeConfig == nil { - a.KubeConfig, err = clientcmd.BuildConfigFromFlags("", a.KubeConfigFilePath) - if err != nil { - return err - } - } - - if a.KubeClient == nil { - a.KubeClient, err = kubernetes.NewForConfig(a.KubeConfig) - if err != nil { +func (a *AirshipCTLSettings) Init() error { + for _, pluginSettings := range a.PluginSettings { + if err := pluginSettings.Init(); err != nil { return err } } diff --git a/pkg/workflow/environment/settings.go b/pkg/workflow/environment/settings.go new file mode 100644 index 000000000..e17de68a0 --- /dev/null +++ b/pkg/workflow/environment/settings.go @@ -0,0 +1,60 @@ +package environment + +import ( + "github.com/spf13/cobra" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/tools/clientcmd" + argo "github.com/argoproj/argo/pkg/client/clientset/versioned/typed/workflow/v1alpha1" + + apixv1beta1 "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1beta1" +) + +// Settings is a container for all of the settings needed by workflows +type Settings struct { + // Namespace is the kubernetes namespace to be used during the context of this action + Namespace string + + // KubeConfigFilePath is the path to the kubernetes configuration file. + // This flag is only needed when airshipctl is being used + // out-of-cluster + KubeConfigFilePath string + + // KubeClient is an instrument for interacting with a kubernetes cluster + KubeClient kubernetes.Interface + + // ArgoClient is an instrument for interacting with Argo workflows + ArgoClient argo.ArgoprojV1alpha1Interface + + CRDClient apixv1beta1.ApiextensionsV1beta1Interface +} + +// InitFlags adds the default settings flags to cmd +func (s *Settings) InitFlags(cmd *cobra.Command) { + flags := cmd.PersistentFlags() + flags.StringVar(&s.KubeConfigFilePath, "kubeconfig", "", "path to kubeconfig") + flags.StringVar(&s.Namespace, "namespace", "default", "kubernetes namespace to use for the context of this command") +} + +// Init assigns default values +func (s *Settings) Init() error { + if s.KubeConfigFilePath == "" { + s.KubeConfigFilePath = clientcmd.RecommendedHomeFile + } + + var err error + kubeConfig, err := clientcmd.BuildConfigFromFlags("", s.KubeConfigFilePath) + if err != nil { + return err + } + + s.KubeClient, err = kubernetes.NewForConfig(kubeConfig) + if err != nil { + return err + } + + s.ArgoClient, err = argo.NewForConfig(kubeConfig) + if err != nil { + return err + } + return nil +}