From cdf92c8a54e05df4213c5b3241ae08374b47a7bd Mon Sep 17 00:00:00 2001 From: Ryan McGuire Date: Sun, 10 Dec 2023 10:10:46 -0500 Subject: [PATCH] Bug fixes, config generator --- README.md | 10 ++++++++++ cmd/alias.go | 6 ++++++ cmd/alias_delete.go | 1 + cmd/alias_list.go | 1 + cmd/cache.go | 1 + cmd/config.go | 16 ++++++++++++++++ cmd/config_generate.go | 27 +++++++++++++++++++++++++++ cmd/project.go | 7 +++++++ cmd/root.go | 24 +++++++++++++++++------- cmd/util_constants.go | 12 +++++++++--- internal/config/config.go | 33 ++++++++++++++++++++++++--------- internal/gitlab/gitlab.go | 9 +++++---- 12 files changed, 124 insertions(+), 23 deletions(-) create mode 100644 cmd/config.go create mode 100644 cmd/config_generate.go diff --git a/README.md b/README.md index 812f31e..10cae45 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,16 @@ ## TODO - Fix NPE when cache is reset or project for whatever reason leaves an orphaned alias +- Add config setters and getters +- Add TTL check to cache load, and add -f / --force flag to re-build regardless +- For each project loaded, check if it is the same, and do nothing + - prevents clobbering cache on a partial update + - track loaded projects in []*Project to diff after load + - should prune missing after the load is complete +- Add open command + - config should exist for editor (vim, code, etc..) +- Update README for shell completion, aliases, usage +- Make a Makefile ## Purpose diff --git a/cmd/alias.go b/cmd/alias.go index fe1e028..286259e 100644 --- a/cmd/alias.go +++ b/cmd/alias.go @@ -17,3 +17,9 @@ var aliasCmd = &cobra.Command{ func init() { rootCmd.AddCommand(aliasCmd) } + +func mustHaveAliases(cmd *cobra.Command, args []string) { + if len(cache.Aliases) == 0 { + plog.Fatal("No aliases set, nothing to " + cmd.Name()) + } +} diff --git a/cmd/alias_delete.go b/cmd/alias_delete.go index 46b5e12..6998955 100644 --- a/cmd/alias_delete.go +++ b/cmd/alias_delete.go @@ -14,6 +14,7 @@ var aliasDeleteCmd = &cobra.Command{ Aliases: []string{"rm", "del", "d"}, Short: "Delete a project alias", Long: aliasDeleteCmdLong, + PreRun: mustHaveAliases, Run: runDeleteAliasCmd, } diff --git a/cmd/alias_list.go b/cmd/alias_list.go index e0f0b84..062b9b0 100644 --- a/cmd/alias_list.go +++ b/cmd/alias_list.go @@ -12,6 +12,7 @@ var aliasListCmd = &cobra.Command{ Use: "list", Aliases: []string{"dump", "show", "ls", "ll", "l"}, Short: "List Aliases", + PreRun: mustHaveAliases, Long: aliasListCmdLong, Run: runListAliasCmd, } diff --git a/cmd/cache.go b/cmd/cache.go index e4a9429..afc1afb 100644 --- a/cmd/cache.go +++ b/cmd/cache.go @@ -12,6 +12,7 @@ var cache *projects.Cache var cacheCmd = &cobra.Command{ Use: "cache", + Aliases: []string{"a", "ln"}, Short: "Manage GitLab project cache", Long: cacheCmdLong, PersistentPreRun: runCacheCmd, diff --git a/cmd/config.go b/cmd/config.go new file mode 100644 index 0000000..037c8ee --- /dev/null +++ b/cmd/config.go @@ -0,0 +1,16 @@ +package cmd + +import ( + "github.com/spf13/cobra" +) + +var configCmd = &cobra.Command{ + Use: "config", + Short: "GitLab Project Manager Configuration", + Aliases: []string{"conf"}, + Long: configCmdLong, +} + +func init() { + rootCmd.AddCommand(configCmd) +} diff --git a/cmd/config_generate.go b/cmd/config_generate.go new file mode 100644 index 0000000..73e7a58 --- /dev/null +++ b/cmd/config_generate.go @@ -0,0 +1,27 @@ +package cmd + +import ( + "fmt" + + "github.com/spf13/cobra" + "gitlab.sweetwater.com/it/devops/tools/gitlab-project-manager/internal/config" + "gopkg.in/yaml.v3" +) + +var configGenerateCmd = &cobra.Command{ + Use: "generate", + Short: "Generate a default configuration", + Aliases: []string{"gen", "new", "default"}, + Long: configGenCmdLong, + Run: runConfigGenerateCmd, +} + +func runConfigGenerateCmd(cmd *cobra.Command, args []string) { + plog.Info("Suggest running with > " + defConfigPath) + c, _ := yaml.Marshal(config.DefaultConfig) + fmt.Print(string(c)) +} + +func init() { + configCmd.AddCommand(configGenerateCmd) +} diff --git a/cmd/project.go b/cmd/project.go index 062e917..92301e4 100644 --- a/cmd/project.go +++ b/cmd/project.go @@ -46,6 +46,7 @@ func getProject(args []string) *gitlab.Project { func initProjectCmd(cmd *cobra.Command, args []string) { initProjectCache(cmd, args) + mustHaveProjects(cmd, args) } func postProjectCmd(cmd *cobra.Command, args []string) { @@ -55,3 +56,9 @@ func postProjectCmd(cmd *cobra.Command, args []string) { func init() { rootCmd.AddCommand(projectCmd) } + +func mustHaveProjects(cmd *cobra.Command, args []string) { + if len(cache.Aliases) == 0 { + plog.Fatal("No projects to " + cmd.Name() + ", try running cache load") + } +} diff --git a/cmd/root.go b/cmd/root.go index 1f21350..416433e 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -38,7 +38,7 @@ func Execute() { err := rootCmd.ExecuteContext(ctx) if err != nil { - plog.Error("Failed to execute command", plog.Args("err", err)) + pterm.Error.Printfln(pterm.LightYellow("Command failed, " + err.Error())) os.Exit(1) } } @@ -48,12 +48,12 @@ func init() { cobra.OnInitialize(initConfig) rootCmd.PersistentFlags().String("config", "", - "config file (default is $HOME/.gitlab-project-manager.yaml)") + "config file (default is "+defConfigPath+")") rootCmd.PersistentFlags().String("gitlabHost", defGitlabHost, "GitLab Hostname (e.g. gitlab.com)") rootCmd.PersistentFlags().String("gitlabToken", "", "GitLab Tokenname (e.g. gitlab.com)") - rootCmd.PersistentFlags().String("projectPath", defProjectsPath, + rootCmd.PersistentFlags().String("projectPath", "", "Sets a path for local clones of projects") rootCmd.PersistentFlags().String("logLevel", defLogLevel, "Default log level -- info, warn, error, debug") @@ -73,16 +73,14 @@ func initConfig() { cobra.CheckErr(err) // Search config in home directory with name ".gitlab-project-manager" (without extension). - viper.AddConfigPath(home) + viper.AddConfigPath(home + "/.config") viper.SetConfigType("yaml") - viper.SetConfigName(".gitlab-project-manager") + viper.SetConfigName("gitlab-project-manager") } viper.AutomaticEnv() viper.ReadInConfig() - checkConfigPerms(viper.ConfigFileUsed()) // Abort on world-readable config - // Configure pretty logger plog = pterm.DefaultLogger. WithLevel(getPtermLogLevel(viper.GetString("logLevel"))). @@ -97,6 +95,18 @@ func initConfig() { plog.Error("Failed loading config", plog.Args("err", err)) } + if len(os.Args) > 0 && strings.HasPrefix(os.Args[1], "conf") { + plog.Debug("Permitting missing config for config sub-command") + return + } else if conf.ProjectPath == "" { + plog.Fatal("Minimal configuration missing, must have projectPath", plog.Args( + "do", + "Try running `gitlab-project-manager config default > "+defConfigPath, + )) + } + + checkConfigPerms(viper.ConfigFileUsed()) // Abort on world-readable config + plog.Debug("Configuration loaded", plog.Args("conf", conf)) } diff --git a/cmd/util_constants.go b/cmd/util_constants.go index 94ab809..60db429 100644 --- a/cmd/util_constants.go +++ b/cmd/util_constants.go @@ -1,9 +1,9 @@ package cmd const ( - defGitlabHost = "gitlab.sweetwater.com" - defProjectsPath = "~/work/projects" - defLogLevel = "info" + defGitlabHost = "gitlab.sweetwater.com" + defLogLevel = "info" + defConfigPath = "~/.config/gitlab-project-manager.yaml" ) const aliasCmdLong = `Manages project aliases, with options for @@ -43,3 +43,9 @@ uses fuzzy find to locate the project` const projShowCmdLong = `Shows detail for a particular project Will always fuzzy find` + +const configCmdLong = `Commands for managing configuration, particulary +useful for seeding a new config file` + +const configGenCmdLong = `Produces yaml to stdout that can be used +to seed the configuration file` diff --git a/internal/config/config.go b/internal/config/config.go index 956dcb1..797fe17 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -13,13 +13,28 @@ type Config struct { } } -type cacheConfig struct { - Ttl time.Duration `yaml:"ttl" json:"ttl"` - File string `yaml:"file" json:"file"` - Load struct { - OwnerOnly bool `yaml:"ownerOnly" json:"ownerOnly"` - } `yaml:"load" json:"load"` - Clear struct { - ClearAliases bool `yaml:"clearAliases" json:"clearAliases"` - } `yaml:"clear" json:"clear"` +type loadConfig struct { + OwnerOnly bool `yaml:"ownerOnly" json:"ownerOnly"` +} + +type cacheConfig struct { + Ttl time.Duration `yaml:"ttl,omitempty" json:"ttl,omitempty"` + File string `yaml:"file,omitempty" json:"file,omitempty"` + Load loadConfig `yaml:"load" json:"load"` + Clear struct { + ClearAliases bool `yaml:"clearAliases,omitempty" json:"clearAliases,omitempty"` + } `yaml:"clear,omitempty" json:"clear,omitempty"` +} + +var DefaultConfig = Config{ + GitlabHost: "https://gitlab.com", + GitlabToken: "yourtokenhere", + LogLevel: "warn", + ProjectPath: "~/work/projects", + Cache: cacheConfig{ + Ttl: 168 * time.Hour, + Load: loadConfig{ + OwnerOnly: true, + }, + }, } diff --git a/internal/gitlab/gitlab.go b/internal/gitlab/gitlab.go index 84931ab..a577b28 100644 --- a/internal/gitlab/gitlab.go +++ b/internal/gitlab/gitlab.go @@ -129,16 +129,17 @@ func (c *Client) streamProjects(pi *ProgressInfo, ownerOnly bool) { for { projects, resp, err := c.ListProjects(listOpts) + if err != nil { + pi.ErrorChan <- err + break + } + // We're done when we have it all or our context is done if c.Ctx.Err() != nil || resp.NextPage == 0 { pi.DoneChan <- nil break } - if err != nil { - pi.ErrorChan <- err - } - numProjects += len(projects) pi.ProjectsChan <- projects pi.ProgressChan <- Progress{