diff --git a/cmd/main.go b/cmd/main.go index c0287f3..ed1cf54 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -1,166 +1,15 @@ package main import ( - "context" - "errors" "fmt" - "io" "os" - "github.com/leonsteinhaeuser/openshift-gitops-cli/internal/menu" - "github.com/leonsteinhaeuser/openshift-gitops-cli/internal/project" - "github.com/leonsteinhaeuser/openshift-gitops-cli/internal/template" + "github.com/leonsteinhaeuser/openshift-gitops-cli/internal/cmd" ) -var ( - projectConfig = &project.ProjectConfig{} -) - -const ( - PROJECTFILENAME = "PROJECT.yaml" -) - -// check for project file and load it -func init() { - _, err := os.Stat(PROJECTFILENAME) - if err != nil && !errors.Is(err, os.ErrNotExist) { - fmt.Println("An error occurred while checking for the PROJECT.yaml file", err) - os.Exit(1) - return - } - if errors.Is(err, os.ErrNotExist) { - f, err := os.Create(PROJECTFILENAME) - if err != nil { - fmt.Println(err) - os.Exit(1) - return - } - defer f.Close() - _, err = f.WriteString("basePath: overlays/\ntemplateBasePath: templates/\n") - if err != nil { - fmt.Println(err) - os.Exit(1) - return - } - } - - pc, err := project.ParseConfig(PROJECTFILENAME) - if err != nil { - fmt.Println("An error occurred while parsing the PROJECT.yaml file", err) - os.Exit(1) - return - } - projectConfig = pc -} - func main() { - eventsPipeline := make(chan menu.Event, 100) - ctx, cf := context.WithCancel(context.Background()) - defer cf() - - go func(ctx context.Context) { - // handle config file updates - for { - select { - case <-ctx.Done(): - close(eventsPipeline) - return - case event := <-eventsPipeline: - // we only need to update the config file if the action is a post action - // because we need to update the config only, if the action was successful - if event.Runtime == menu.EventRuntimePost { - // update config file - err := project.UpdateOrCreateConfig(PROJECTFILENAME, projectConfig) - if err != nil { - fmt.Println("An error occurred while updating the project config", err) - return - } - } - - if event.Origin == menu.EventOriginAddon { - if event.Runtime == menu.EventRuntimePre { - addonPath := projectConfig.Addons[event.Environment].Path - _, err := template.LoadManifest(projectConfig.Addons[event.Environment].Path) - if err != nil { - fmt.Printf("An error occurred while loading the addon [%s] manifest file: %s, %v\n", event.Environment, addonPath, err) - os.Exit(1) - return - } - } - continue - } - - if event.Environment != "" && event.Stage == "" && event.Cluster == "" { - env := projectConfig.GetEnvironment(event.Environment) - err := executeHook(os.Stdout, os.Stderr, event.Type, event.Runtime, env.Actions) - if err != nil { - fmt.Println(err) - return - } - } - - if event.Environment != "" && event.Stage != "" && event.Cluster == "" { - stage := projectConfig.GetStage(event.Environment, event.Stage) - err := executeHook(os.Stdout, os.Stderr, event.Type, event.Runtime, stage.Actions) - if err != nil { - fmt.Println(err) - return - } - } - - if event.Environment != "" && event.Stage != "" && event.Cluster != "" { - cluster := projectConfig.GetCluster(event.Environment, event.Stage, event.Cluster) - if event.Type == menu.EventTypeCreate || event.Type == menu.EventTypeUpdate { - err := cluster.Render(projectConfig, event.Environment, event.Stage) - if err != nil { - fmt.Printf("An error occurred while rendering the cluster [%s] configuration: %v", event.Cluster, err) - return - } - } - } - } - } - }(ctx) - - err := menu.RootMenu(projectConfig, eventsPipeline) - if err != nil { + if err := cmd.RootCmd.Execute(); err != nil { fmt.Println(err) os.Exit(1) } } - -func executeHook(stdout, errout io.Writer, t menu.EventType, r menu.EventRuntime, actions project.Actions) error { - switch t { - case menu.EventTypeCreate: - if r == menu.EventRuntimePre { - err := actions.ExecutePreCreateHooks(stdout, errout) - if err != nil { - return err - } - } - if r == menu.EventRuntimePost { - err := actions.ExecutePostCreateHooks(stdout, errout) - if err != nil { - return err - } - } - return nil - case menu.EventTypeUpdate: - if r == menu.EventRuntimePre { - err := actions.ExecutePreUpdateHooks(stdout, errout) - if err != nil { - return err - } - } - if r == menu.EventRuntimePost { - err := actions.ExecutePostUpdateHooks(stdout, errout) - if err != nil { - return err - } - } - case menu.EventTypeDelete: - default: - return fmt.Errorf("unknown event type: %v", t) - } - return nil -} diff --git a/go.mod b/go.mod index 1795930..339d479 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/Masterminds/sprig/v3 v3.3.0 github.com/google/go-cmp v0.7.0 github.com/manifoldco/promptui v0.9.0 + github.com/spf13/cobra v1.9.1 sigs.k8s.io/yaml v1.4.0 ) @@ -16,10 +17,12 @@ require ( github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect github.com/google/uuid v1.6.0 // indirect github.com/huandu/xstrings v1.5.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/shopspring/decimal v1.4.0 // indirect github.com/spf13/cast v1.7.0 // indirect + github.com/spf13/pflag v1.0.6 // indirect golang.org/x/crypto v0.31.0 // indirect golang.org/x/sys v0.28.0 // indirect ) diff --git a/go.sum b/go.sum index 2dace1f..8302adf 100644 --- a/go.sum +++ b/go.sum @@ -12,6 +12,7 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5O github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= @@ -23,6 +24,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -37,10 +40,15 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= +github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= +github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= +github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= diff --git a/internal/cmd/root.go b/internal/cmd/root.go new file mode 100644 index 0000000..f08b3a2 --- /dev/null +++ b/internal/cmd/root.go @@ -0,0 +1,202 @@ +package cmd + +import ( + "context" + "fmt" + "io" + "os" + + "github.com/leonsteinhaeuser/openshift-gitops-cli/internal/menu" + "github.com/leonsteinhaeuser/openshift-gitops-cli/internal/project" + "github.com/leonsteinhaeuser/openshift-gitops-cli/internal/template" + "github.com/spf13/cobra" +) + +const ( + flagKeyEnvironment = "environment" + flagKeyStage = "stage" + flagKeyCluster = "cluster" + flagKeyAddon = "addon" + + PROJECTFILENAME = "PROJECT.yaml" +) + +var ( + RootCmd = &cobra.Command{ + Use: "root", + RunE: func(cmd *cobra.Command, args []string) error { + return rootCmd() + }, + } + + projectConfig *project.ProjectConfig +) + +func init() { + // load project configuration + pc, err := project.ParseConfig(PROJECTFILENAME) + if err != nil { + fmt.Println("An error occurred while parsing the PROJECT.yaml file", err) + os.Exit(1) + return + } + projectConfig = pc + + // initialize the run command + run := &cobra.Command{ + Use: "run", + Short: "Render the cluster configuration", + RunE: func(cmd *cobra.Command, args []string) error { + return runCmd(cmd) + }, + } + run.PersistentFlags().StringP(flagKeyEnvironment, "e", "", "environment the check should be performed for") + run.PersistentFlags().StringP(flagKeyStage, "s", "", "stage the check should be performed for") + run.PersistentFlags().StringP(flagKeyCluster, "c", "", "cluster the check should be performed for") + RootCmd.AddCommand(run) +} + +func rootCmd() error { + eventsPipeline := make(chan menu.Event, 100) + ctx, cf := context.WithCancel(context.Background()) + defer cf() + + go func(ctx context.Context) { + // handle config file updates + for { + select { + case <-ctx.Done(): + close(eventsPipeline) + return + case event := <-eventsPipeline: + // we only need to update the config file if the action is a post action + // because we need to update the config only, if the action was successful + if event.Runtime == menu.EventRuntimePost { + // update config file + err := project.UpdateOrCreateConfig(PROJECTFILENAME, projectConfig) + if err != nil { + fmt.Println("An error occurred while updating the project config", err) + return + } + } + + if event.Origin == menu.EventOriginAddon { + if event.Runtime == menu.EventRuntimePre { + addonPath := projectConfig.Addons[event.Environment].Path + _, err := template.LoadManifest(projectConfig.Addons[event.Environment].Path) + if err != nil { + fmt.Printf("An error occurred while loading the addon [%s] manifest file: %s, %v\n", event.Environment, addonPath, err) + os.Exit(1) + return + } + } + continue + } + + if event.Environment != "" && event.Stage == "" && event.Cluster == "" { + env := projectConfig.GetEnvironment(event.Environment) + err := executeHook(os.Stdout, os.Stderr, event.Type, event.Runtime, env.Actions) + if err != nil { + fmt.Println(err) + return + } + } + + if event.Environment != "" && event.Stage != "" && event.Cluster == "" { + stage := projectConfig.GetStage(event.Environment, event.Stage) + err := executeHook(os.Stdout, os.Stderr, event.Type, event.Runtime, stage.Actions) + if err != nil { + fmt.Println(err) + return + } + } + + if event.Environment != "" && event.Stage != "" && event.Cluster != "" { + cluster := projectConfig.GetCluster(event.Environment, event.Stage, event.Cluster) + if event.Type == menu.EventTypeCreate || event.Type == menu.EventTypeUpdate { + err := cluster.Render(projectConfig, event.Environment, event.Stage) + if err != nil { + fmt.Printf("An error occurred while rendering the cluster [%s] configuration: %v", event.Cluster, err) + return + } + } + } + } + } + }(ctx) + + err := menu.RootMenu(projectConfig, eventsPipeline) + if err != nil { + return err + } + return nil +} + +func executeHook(stdout, errout io.Writer, t menu.EventType, r menu.EventRuntime, actions project.Actions) error { + switch t { + case menu.EventTypeCreate: + if r == menu.EventRuntimePre { + err := actions.ExecutePreCreateHooks(stdout, errout) + if err != nil { + return err + } + } + if r == menu.EventRuntimePost { + err := actions.ExecutePostCreateHooks(stdout, errout) + if err != nil { + return err + } + } + return nil + case menu.EventTypeUpdate: + if r == menu.EventRuntimePre { + err := actions.ExecutePreUpdateHooks(stdout, errout) + if err != nil { + return err + } + } + if r == menu.EventRuntimePost { + err := actions.ExecutePostUpdateHooks(stdout, errout) + if err != nil { + return err + } + } + case menu.EventTypeDelete: + default: + return fmt.Errorf("unknown event type: %v", t) + } + return nil +} + +// runCmd expects the environment, stage and cluster flags to be set and renders the cluster with all properties and addons to disk +func runCmd(cmd *cobra.Command) error { + env, err := cmd.Flags().GetString(flagKeyEnvironment) + if err != nil { + return fmt.Errorf("flag environment not found %w", err) + } + stg, err := cmd.Flags().GetString(flagKeyStage) + if err != nil { + return fmt.Errorf("flag stage not found: %w", err) + } + cluster, err := cmd.Flags().GetString(flagKeyCluster) + if err != nil { + return fmt.Errorf("flag cluster not found %w", err) + } + + switch { + case env == "": + return fmt.Errorf("environment is required") + case stg == "": + return fmt.Errorf("stage is required") + case cluster == "": + return fmt.Errorf("cluster is required") + } + + clusterCfg := projectConfig.GetCluster(env, stg, cluster) + + err = clusterCfg.Render(projectConfig, env, stg) + if err != nil { + return err + } + return nil +} diff --git a/internal/project/config.go b/internal/project/config.go index e384bbb..f37f331 100644 --- a/internal/project/config.go +++ b/internal/project/config.go @@ -1,6 +1,7 @@ package project import ( + "errors" "fmt" "os" @@ -9,7 +10,24 @@ import ( ) // ParseConfig reads a yaml file from the given path and unmarshals it into a ProjectConfig struct +// If the config file does not exist, it will be created with default values func ParseConfig(path string) (*ProjectConfig, error) { + _, err := os.Stat(path) + if err != nil && !errors.Is(err, os.ErrNotExist) { + return nil, fmt.Errorf("failed to check for the PROJECT.yaml file: %w", err) + } + if errors.Is(err, os.ErrNotExist) { + f, err := os.Create(path) + if err != nil { + return nil, err + } + defer f.Close() + _, err = f.WriteString("basePath: overlays/\ntemplateBasePath: templates/\n") + if err != nil { + return nil, err + } + } + bts, err := os.ReadFile(path) if err != nil { return nil, err diff --git a/internal/project/type.go b/internal/project/type.go index c643016..13557cc 100644 --- a/internal/project/type.go +++ b/internal/project/type.go @@ -72,5 +72,9 @@ func (p *ProjectConfig) GetStage(env, stage string) *Stage { } func (p *ProjectConfig) GetCluster(env, stage, cluster string) *Cluster { - return p.GetStage(env, stage).GetCluster(cluster) + cls := p.GetStage(env, stage).GetCluster(cluster) + if cls != nil && cls.Name == "" { + cls.Name = cluster + } + return cls } diff --git a/internal/project/type_test.go b/internal/project/type_test.go index 7816b1d..d0e01bc 100644 --- a/internal/project/type_test.go +++ b/internal/project/type_test.go @@ -108,7 +108,7 @@ func TestProjectConfig_GetCluster(t *testing.T) { want *Cluster }{ { - name: "should return true if the cluster exists", + name: "should return cluster if the cluster exists", fields: fields{ Environments: map[string]*Environment{ "env1": { @@ -140,7 +140,7 @@ func TestProjectConfig_GetCluster(t *testing.T) { }, }, { - name: "should return false if the cluster does not exist", + name: "should not return cluster if the cluster does not exist", fields: fields{ Environments: map[string]*Environment{ "env1": { @@ -161,6 +161,30 @@ func TestProjectConfig_GetCluster(t *testing.T) { }, want: nil, }, + { + name: "name not defined in name key", + fields: fields{ + Environments: map[string]*Environment{ + "env1": { + Stages: map[string]*Stage{ + "stage1": { + Clusters: map[string]*Cluster{ + "cluster1": {}, + }, + }, + }, + }, + }, + }, + args: args{ + env: "env1", + stage: "stage1", + cluster: "cluster1", + }, + want: &Cluster{ + Name: "cluster1", + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) {