Overhaul for plugin system

This commit is contained in:
Ian Howell 2019-05-23 14:34:28 -05:00
parent 49ddd449a7
commit 33fecc360b
10 changed files with 127 additions and 153 deletions

View File

@ -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,
}

View File

@ -1,55 +1,19 @@
package cmd package cmd
import ( import (
"fmt"
"io" "io"
"os"
"github.com/spf13/cobra" "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 // NewRootCmd creates the root `airshipctl` command. All other commands are
// subcommands branching from this one // 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{ rootCmd := &cobra.Command{
Use: "airshipctl", Use: "airshipctl",
Short: "airshipctl is a unified entrypoint to various airship components", Short: "airshipctl is a unified entrypoint to various airship components",
} }
rootCmd.SetOutput(out) rootCmd.SetOutput(out)
settings.InitFlags(rootCmd)
rootCmd.AddCommand(NewVersionCommand(out)) rootCmd.AddCommand(NewVersionCommand(out))
loadPluginCommands(rootCmd, out, settings, args)
rootCmd.PersistentFlags().Parse(args)
settings.InitDefaults()
log.Init(settings, out)
return rootCmd, nil 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))
}
}

View File

@ -6,18 +6,25 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/ian-howell/airshipctl/pkg/environment" "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 // 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{ workflowRootCmd := &cobra.Command{
Use: "workflow", Use: "workflow",
Short: "Access to argo workflows", Short: "Access to argo workflows",
Aliases: []string{"workflows", "wf"}, Aliases: []string{"workflows", "wf"},
} }
workflowRootCmd.AddCommand(NewWorkflowInitCommand(out, settings, args)) wfSettings := &wfenv.Settings{}
workflowRootCmd.AddCommand(NewWorkflowListCommand(out, settings, args)) wfSettings.InitFlags(workflowRootCmd)
workflowRootCmd.AddCommand(NewWorkflowInitCommand(out, wfSettings))
workflowRootCmd.AddCommand(NewWorkflowListCommand(out, rootSettings))
rootSettings.Register(PluginSettingsID, wfSettings)
return workflowRootCmd return workflowRootCmd
} }

View File

@ -13,9 +13,8 @@ import (
"k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes" "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 ( const (
@ -28,16 +27,16 @@ var (
type workflowInitCmd struct { type workflowInitCmd struct {
out io.Writer out io.Writer
config *rest.Config
kubeclient kubernetes.Interface kubeclient kubernetes.Interface
crdclient apixv1beta1client.ApiextensionsV1beta1Interface
} }
// NewWorkflowInitCommand is a command for bootstrapping a kubernetes cluster with the necessary components for Argo workflows // 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{ workflowInit := &workflowInitCmd{
out: out, out: out,
config: settings.KubeConfig,
kubeclient: settings.KubeClient, kubeclient: settings.KubeClient,
crdclient: settings.CRDClient,
} }
workflowInitCommand := &cobra.Command{ workflowInitCommand := &cobra.Command{
Use: "init [flags]", Use: "init [flags]",
@ -160,11 +159,7 @@ func (wfInit *workflowInitCmd) createCustomObjects(manifestPath string) {
} }
func (wfInit *workflowInitCmd) registerDefaultWorkflow() error { func (wfInit *workflowInitCmd) registerDefaultWorkflow() error {
apixClient, err := apixv1beta1client.NewForConfig(wfInit.config) apixClient := wfInit.crdclient
if err != nil {
return fmt.Errorf("Could not create CRD client: %s", err.Error())
}
crds := apixClient.CustomResourceDefinitions() crds := apixClient.CustomResourceDefinitions()
workflowCRD := &apixv1beta1.CustomResourceDefinition{ workflowCRD := &apixv1beta1.CustomResourceDefinition{
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
@ -185,7 +180,7 @@ func (wfInit *workflowInitCmd) registerDefaultWorkflow() error {
Scope: apixv1beta1.NamespaceScoped, Scope: apixv1beta1.NamespaceScoped,
}, },
} }
_, err = crds.Create(workflowCRD) _, err := crds.Create(workflowCRD)
return err return err
} }

View File

@ -5,26 +5,27 @@ import (
"io" "io"
"text/tabwriter" "text/tabwriter"
"github.com/argoproj/argo/pkg/client/clientset/versioned/typed/workflow/v1alpha1"
"github.com/spf13/cobra" "github.com/spf13/cobra"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1" v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/ian-howell/airshipctl/pkg/environment" "github.com/ian-howell/airshipctl/pkg/environment"
wfenv "github.com/ian-howell/airshipctl/pkg/workflow/environment"
) )
// NewWorkflowListCommand is a command for listing argo workflows // 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{ workflowListCmd := &cobra.Command{
Use: "list", Use: "list",
Short: "list workflows", Short: "list workflows",
Aliases: []string{"ls"}, Aliases: []string{"ls"},
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
clientSet, err := v1alpha1.NewForConfig(settings.KubeConfig) wfSettings, ok := rootSettings.PluginSettings[PluginSettingsID].(*wfenv.Settings)
if err != nil { if !ok {
panic(err.Error()) fmt.Fprintf(out, "settings for %s were not registered\n", PluginSettingsID)
return
} }
clientSet := wfSettings.ArgoClient
wflist, err := clientSet.Workflows(settings.Namespace).List(v1.ListOptions{}) wflist, err := clientSet.Workflows(wfSettings.Namespace).List(v1.ListOptions{})
if err != nil { if err != nil {
panic(err.Error()) panic(err.Error())
} }

1
go.sum
View File

@ -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 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 h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= 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/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 h1:LbsanbbD6LieFkXbj9YNNBupiGHJgFeLpO0j0Fza1h8=
github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=

View File

@ -6,12 +6,7 @@ import (
"io/ioutil" "io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
"strings"
"testing" "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. // 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 // CmdTest is a command to be run on the command line as a test
type CmdTest struct { type CmdTest struct {
Name string Name string
Command string CmdLine 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()
} }
func updateGolden(t *testing.T, test CmdTest, actual []byte) { func updateGolden(t *testing.T, test CmdTest, actual []byte) {

27
main.go
View File

@ -1,11 +1,36 @@
package main package main
import ( import (
"fmt"
"os" "os"
"github.com/ian-howell/airshipctl/cmd" "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() { 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)
}
} }

View File

@ -2,57 +2,36 @@ package environment
import ( import (
"github.com/spf13/cobra" "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 // AirshipCTLSettings is a container for all of the settings needed by airshipctl
type AirshipCTLSettings struct { 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 is used for verbose output
Debug bool Debug bool
PluginSettings map[string]PluginSettings
} }
// InitFlags adds the default settings flags to cmd // InitFlags adds the default settings flags to cmd
func (a *AirshipCTLSettings) InitFlags(cmd *cobra.Command) { func (a *AirshipCTLSettings) InitFlags(cmd *cobra.Command) {
flags := cmd.PersistentFlags() flags := cmd.PersistentFlags()
flags.BoolVar(&a.Debug, "debug", false, "enable verbose output") 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) Register(pluginName string, pluginSettings PluginSettings) {
func (a *AirshipCTLSettings) InitDefaults() error { if a.PluginSettings == nil {
if a.KubeConfigFilePath == "" { a.PluginSettings = make(map[string]PluginSettings, 0)
a.KubeConfigFilePath = clientcmd.RecommendedHomeFile }
a.PluginSettings[pluginName] = pluginSettings
} }
var err error func (a *AirshipCTLSettings) Init() error {
if a.KubeConfig == nil { for _, pluginSettings := range a.PluginSettings {
a.KubeConfig, err = clientcmd.BuildConfigFromFlags("", a.KubeConfigFilePath) if err := pluginSettings.Init(); err != nil {
if err != nil {
return err
}
}
if a.KubeClient == nil {
a.KubeClient, err = kubernetes.NewForConfig(a.KubeConfig)
if err != nil {
return err return err
} }
} }

View File

@ -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
}