From bcebadbbf405cb04fa81c95a3f8bf85a199dedd0 Mon Sep 17 00:00:00 2001 From: Alex Luong Date: Wed, 5 Jul 2023 10:50:23 +0700 Subject: [PATCH] feat: Finalize workspace management (#54) * chore: gofmt * chore: remove debugging log * fix: Issue where local config directory doesn't exist * feat: Validate API key before whoami and workspace commands * chore: Change "Verifying CLI key..." to "Verifying credentials..." * chore: Deprecate cli-key flag * docs: Add documentation about workspace local flag --- README.md | 15 +++++++++++ pkg/ansi/init_windows.go | 1 + pkg/cmd/listen.go | 2 +- pkg/cmd/root.go | 3 ++- pkg/cmd/whoami.go | 6 ++++- pkg/cmd/workspace_list.go | 6 +++-- pkg/cmd/workspace_use.go | 10 ++++--- pkg/config/config.go | 45 ++++++++++++++++++++------------ pkg/config/profile.go | 21 ++++++++++----- pkg/listen/listen.go | 4 +-- pkg/login/client_login.go | 4 +-- pkg/useragent/uname_unix.go | 1 + pkg/useragent/uname_unix_test.go | 1 + pkg/useragent/uname_windows.go | 1 + pkg/workspace/workspace.go | 2 +- 15 files changed, 85 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index b0ee37c..465a592 100644 --- a/README.md +++ b/README.md @@ -243,6 +243,21 @@ Using profile default Logged in as Me in workspace Yet Another One ``` +You can also pin an active workspace in the current working directory with the `--local` flag. + +```sh-session +$ hookdeck workspace use --local +Use the arrow keys to navigate: ↓ ↑ → ← +? Select Workspace: + My Workspace + Another Workspace + ▸ Yet Another One + +Selecting workspace Yet Another One +``` + +This will create a local config file in your current directory at `myproject/.hookdeck/config.toml`. Depending on your team's Hookdeck usage and workspace setup, you may or may not want to commit this configuration file to version control. + ## Developing Build from source by running: diff --git a/pkg/ansi/init_windows.go b/pkg/ansi/init_windows.go index 93d24ac..86db5d3 100644 --- a/pkg/ansi/init_windows.go +++ b/pkg/ansi/init_windows.go @@ -1,3 +1,4 @@ +//go:build windows // +build windows package ansi diff --git a/pkg/cmd/listen.go b/pkg/cmd/listen.go index 4412e69..1dd57fd 100644 --- a/pkg/cmd/listen.go +++ b/pkg/cmd/listen.go @@ -106,6 +106,6 @@ func (lc *listenCmd) runListenCmd(cmd *cobra.Command, args []string) error { } return listen.Listen(url, source_alias, connection_query, listen.Flags{ - NoWSS: lc.noWSS, + NoWSS: lc.noWSS, }, &Config) } diff --git a/pkg/cmd/root.go b/pkg/cmd/root.go index 3ba4f34..981395e 100644 --- a/pkg/cmd/root.go +++ b/pkg/cmd/root.go @@ -90,7 +90,8 @@ func init() { cobra.OnInitialize(Config.InitConfig) rootCmd.PersistentFlags().StringVarP(&Config.Profile.Name, "profile", "p", "", fmt.Sprintf("profile name (default \"%s\")", hookdeck.DefaultProfileName)) - rootCmd.PersistentFlags().StringVar(&Config.Profile.APIKey, "cli-key", "", "Your CLI key to use for the command") + rootCmd.PersistentFlags().StringVar(&Config.Profile.APIKey, "cli-key", "", "(deprecated) Your API key to use for the command") + rootCmd.PersistentFlags().StringVar(&Config.Profile.APIKey, "api-key", "", "Your API key to use for the command") rootCmd.PersistentFlags().StringVar(&Config.Color, "color", "", "turn on/off color output (on, off, auto)") rootCmd.PersistentFlags().StringVar(&Config.LocalConfigFile, "config", "", "config file (default is $HOME/.config/hookdeck/config.toml)") rootCmd.PersistentFlags().StringVar(&Config.DeviceName, "device-name", "", "device name") diff --git a/pkg/cmd/whoami.go b/pkg/cmd/whoami.go index b3b566d..c5d1703 100644 --- a/pkg/cmd/whoami.go +++ b/pkg/cmd/whoami.go @@ -29,9 +29,13 @@ func newWhoamiCmd() *whoamiCmd { } func (lc *whoamiCmd) runWhoamiCmd(cmd *cobra.Command, args []string) error { + if err := Config.Profile.ValidateAPIKey(); err != nil { + return err + } + color := ansi.Color(os.Stdout) - fmt.Printf( "Using profile %s\n", color.Bold(Config.Profile.Name)) + fmt.Printf("Using profile %s\n", color.Bold(Config.Profile.Name)) response, err := login.ValidateKey(Config.APIBaseURL, Config.Profile.APIKey, Config.Profile.TeamID) if err != nil { diff --git a/pkg/cmd/workspace_list.go b/pkg/cmd/workspace_list.go index 450a482..dc72c7d 100644 --- a/pkg/cmd/workspace_list.go +++ b/pkg/cmd/workspace_list.go @@ -28,8 +28,10 @@ func newWorkspaceListCmd() *workspaceListCmd { return lc } -func (lc *workspaceListCmd) runWorkspaceListCmd(cmd *cobra.Command, args []string) error { - // TODO: validate API key ?? +func (lc *workspaceListCmd) runWorkspaceListCmd(cmd *cobra.Command, args []string) error { + if err := Config.Profile.ValidateAPIKey(); err != nil { + return err + } workspaces, err := workspace.ListWorkspaces(&Config) if err != nil { diff --git a/pkg/cmd/workspace_use.go b/pkg/cmd/workspace_use.go index 0095514..594a476 100644 --- a/pkg/cmd/workspace_use.go +++ b/pkg/cmd/workspace_use.go @@ -27,7 +27,11 @@ func newWorkspaceUseCmd() *workspaceUseCmd { return lc } -func (lc *workspaceUseCmd) runWorkspaceUseCmd(cmd *cobra.Command, args []string) error { +func (lc *workspaceUseCmd) runWorkspaceUseCmd(cmd *cobra.Command, args []string) error { + if err := Config.Profile.ValidateAPIKey(); err != nil { + return err + } + workspaces, err := workspace.ListWorkspaces(&Config) if err != nil { return err @@ -45,8 +49,8 @@ func (lc *workspaceUseCmd) runWorkspaceUseCmd(cmd *cobra.Command, args []string) } prompt := promptui.Select{ - Label: "Select Workspace", - Items: workspaces, + Label: "Select Workspace", + Items: workspaces, Templates: templates, } diff --git a/pkg/config/config.go b/pkg/config/config.go index 2d40809..8290d4a 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -31,10 +31,10 @@ const ColorAuto = "auto" // Config handles all overall configuration for the CLI type Config struct { - Profile Profile - Color string - LogLevel string - DeviceName string + Profile Profile + Color string + LogLevel string + DeviceName string // Helpers APIBaseURL string @@ -45,9 +45,9 @@ type Config struct { // Config GlobalConfigFile string - GlobalConfig *viper.Viper + GlobalConfig *viper.Viper LocalConfigFile string - LocalConfig *viper.Viper + LocalConfig *viper.Viper } // GetConfigFolder retrieves the folder where the profiles file is stored @@ -246,23 +246,34 @@ func (c *Config) RemoveAllProfiles() error { return c.GlobalConfig.WriteConfig() } +func (c *Config) SaveLocalConfig() error { + if err := ensureDirectoy(filepath.Dir(c.LocalConfigFile)); err != nil { + return err + } + return c.LocalConfig.WriteConfig() +} + +func ensureDirectoy(path string) error { + return os.MkdirAll(path, os.ModePerm) +} + // Construct the config struct from flags > local config > global config func (c *Config) constructConfig() { - c.Color = getStringConfig([]string{c.Color , c.LocalConfig.GetString("color") , c.GlobalConfig.GetString(("color")) , "auto"}) - c.LogLevel = getStringConfig([]string{c.LogLevel , c.LocalConfig.GetString("log") , c.GlobalConfig.GetString(("log")) , "info"}) - c.APIBaseURL = getStringConfig([]string{c.APIBaseURL , c.LocalConfig.GetString("api_base") , c.GlobalConfig.GetString(("api_base")) , hookdeck.DefaultAPIBaseURL}) - c.DashboardBaseURL = getStringConfig([]string{c.DashboardBaseURL , c.LocalConfig.GetString("dashboard_base") , c.GlobalConfig.GetString(("dashboard_base")) , hookdeck.DefaultDashboardBaseURL}) - c.ConsoleBaseURL = getStringConfig([]string{c.ConsoleBaseURL , c.LocalConfig.GetString("console_base") , c.GlobalConfig.GetString(("console_base")) , hookdeck.DefaultConsoleBaseURL}) - c.WSBaseURL = getStringConfig([]string{c.WSBaseURL , c.LocalConfig.GetString("ws_base") , c.GlobalConfig.GetString(("ws_base")) , hookdeck.DefaultWebsocektURL}) - c.Profile.Name = getStringConfig([]string{c.Profile.Name , c.LocalConfig.GetString("profile") , c.GlobalConfig.GetString(("profile")) , hookdeck.DefaultProfileName}) - c.Profile.APIKey = getStringConfig([]string{c.Profile.APIKey , c.LocalConfig.GetString("api_key") , c.GlobalConfig.GetString((c.Profile.GetConfigField("api_key"))) , ""}) - c.Profile.TeamID = getStringConfig([]string{c.Profile.TeamID , c.LocalConfig.GetString("workspace_id") , c.LocalConfig.GetString("team_id") , c.GlobalConfig.GetString((c.Profile.GetConfigField("workspace_id"))) , c.GlobalConfig.GetString((c.Profile.GetConfigField("team_id"))) , ""}) - c.Profile.TeamMode = getStringConfig([]string{c.Profile.TeamMode , c.LocalConfig.GetString("workspace_mode") , c.LocalConfig.GetString("team_mode") , c.GlobalConfig.GetString((c.Profile.GetConfigField("workspace_mode"))) , c.GlobalConfig.GetString((c.Profile.GetConfigField("team_mode"))) , ""}) + c.Color = getStringConfig([]string{c.Color, c.LocalConfig.GetString("color"), c.GlobalConfig.GetString(("color")), "auto"}) + c.LogLevel = getStringConfig([]string{c.LogLevel, c.LocalConfig.GetString("log"), c.GlobalConfig.GetString(("log")), "info"}) + c.APIBaseURL = getStringConfig([]string{c.APIBaseURL, c.LocalConfig.GetString("api_base"), c.GlobalConfig.GetString(("api_base")), hookdeck.DefaultAPIBaseURL}) + c.DashboardBaseURL = getStringConfig([]string{c.DashboardBaseURL, c.LocalConfig.GetString("dashboard_base"), c.GlobalConfig.GetString(("dashboard_base")), hookdeck.DefaultDashboardBaseURL}) + c.ConsoleBaseURL = getStringConfig([]string{c.ConsoleBaseURL, c.LocalConfig.GetString("console_base"), c.GlobalConfig.GetString(("console_base")), hookdeck.DefaultConsoleBaseURL}) + c.WSBaseURL = getStringConfig([]string{c.WSBaseURL, c.LocalConfig.GetString("ws_base"), c.GlobalConfig.GetString(("ws_base")), hookdeck.DefaultWebsocektURL}) + c.Profile.Name = getStringConfig([]string{c.Profile.Name, c.LocalConfig.GetString("profile"), c.GlobalConfig.GetString(("profile")), hookdeck.DefaultProfileName}) + c.Profile.APIKey = getStringConfig([]string{c.Profile.APIKey, c.LocalConfig.GetString("api_key"), c.GlobalConfig.GetString((c.Profile.GetConfigField("api_key"))), ""}) + c.Profile.TeamID = getStringConfig([]string{c.Profile.TeamID, c.LocalConfig.GetString("workspace_id"), c.LocalConfig.GetString("team_id"), c.GlobalConfig.GetString((c.Profile.GetConfigField("workspace_id"))), c.GlobalConfig.GetString((c.Profile.GetConfigField("team_id"))), ""}) + c.Profile.TeamMode = getStringConfig([]string{c.Profile.TeamMode, c.LocalConfig.GetString("workspace_mode"), c.LocalConfig.GetString("team_mode"), c.GlobalConfig.GetString((c.Profile.GetConfigField("workspace_mode"))), c.GlobalConfig.GetString((c.Profile.GetConfigField("team_mode"))), ""}) } func getStringConfig(values []string) string { for _, str := range values { - if str != "" { + if str != "" { return str } } diff --git a/pkg/config/profile.go b/pkg/config/profile.go index de36490..f9afe91 100644 --- a/pkg/config/profile.go +++ b/pkg/config/profile.go @@ -1,9 +1,11 @@ package config +import "github.com/hookdeck/hookdeck-cli/pkg/validators" + type Profile struct { Name string // profile name - APIKey string - TeamID string + APIKey string + TeamID string TeamMode string Config *Config @@ -18,13 +20,13 @@ func (p *Profile) SaveProfile(local bool) error { // in local, we're d setting mode because it should always be inbound // as a user can't have both inbound & console teams (i think) // and we don't need to expose it to the end user - if (local) { + if local { p.Config.GlobalConfig.Set(p.GetConfigField("api_key"), p.APIKey) if err := p.Config.GlobalConfig.WriteConfig(); err != nil { return err } p.Config.LocalConfig.Set("workspace_id", p.TeamID) - return p.Config.LocalConfig.WriteConfig() + return p.Config.SaveLocalConfig() } else { p.Config.GlobalConfig.Set(p.GetConfigField("api_key"), p.APIKey) p.Config.GlobalConfig.Set(p.GetConfigField("workspace_id"), p.TeamID) @@ -37,11 +39,11 @@ func (p *Profile) RemoveProfile() error { var err error runtimeViper := p.Config.GlobalConfig - runtimeViper, err = removeKey(runtimeViper, "profile"); + runtimeViper, err = removeKey(runtimeViper, "profile") if err != nil { return err } - runtimeViper, err = removeKey(runtimeViper, p.Name); + runtimeViper, err = removeKey(runtimeViper, p.Name) if err != nil { return err } @@ -56,3 +58,10 @@ func (p *Profile) UseProfile() error { p.Config.GlobalConfig.Set("profile", p.Name) return p.Config.GlobalConfig.WriteConfig() } + +func (p *Profile) ValidateAPIKey() error { + if p.APIKey == "" { + return validators.ErrAPIKeyNotConfigured + } + return nil +} diff --git a/pkg/listen/listen.go b/pkg/listen/listen.go index 6879d7a..6c5a835 100644 --- a/pkg/listen/listen.go +++ b/pkg/listen/listen.go @@ -30,7 +30,7 @@ import ( ) type Flags struct { - NoWSS bool + NoWSS bool } // listenCmd represents the listen command @@ -94,8 +94,6 @@ func Listen(URL *url.URL, source_alias string, connection_query string, flags Fl } fmt.Println() - fmt.Println(config.WSBaseURL) - p := proxy.New(&proxy.Config{ DeviceName: config.DeviceName, Key: config.Profile.APIKey, diff --git a/pkg/login/client_login.go b/pkg/login/client_login.go index df453d3..8f7f991 100644 --- a/pkg/login/client_login.go +++ b/pkg/login/client_login.go @@ -36,7 +36,7 @@ func Login(config *config.Config, input io.Reader) error { var s *spinner.Spinner if config.Profile.APIKey != "" { - s = ansi.StartNewSpinner("Verifying CLI Key...", os.Stdout) + s = ansi.StartNewSpinner("Verifying credentials...", os.Stdout) response, err := ValidateKey(config.APIBaseURL, config.Profile.APIKey, config.Profile.TeamID) if err != nil { return err @@ -150,7 +150,7 @@ func CILogin(config *config.Config, apiKey string, name string) error { client := &hookdeck.Client{ BaseURL: parsedBaseURL, - APIKey: apiKey, + APIKey: apiKey, } deviceName := name diff --git a/pkg/useragent/uname_unix.go b/pkg/useragent/uname_unix.go index 1326692..ba56115 100644 --- a/pkg/useragent/uname_unix.go +++ b/pkg/useragent/uname_unix.go @@ -1,3 +1,4 @@ +//go:build !windows // +build !windows package useragent diff --git a/pkg/useragent/uname_unix_test.go b/pkg/useragent/uname_unix_test.go index 3952d0f..d3f94de 100644 --- a/pkg/useragent/uname_unix_test.go +++ b/pkg/useragent/uname_unix_test.go @@ -1,3 +1,4 @@ +//go:build !windows // +build !windows package useragent diff --git a/pkg/useragent/uname_windows.go b/pkg/useragent/uname_windows.go index 890415a..1d59467 100644 --- a/pkg/useragent/uname_windows.go +++ b/pkg/useragent/uname_windows.go @@ -1,3 +1,4 @@ +//go:build windows // +build windows package useragent diff --git a/pkg/workspace/workspace.go b/pkg/workspace/workspace.go index 29558da..742ec32 100644 --- a/pkg/workspace/workspace.go +++ b/pkg/workspace/workspace.go @@ -19,4 +19,4 @@ func ListWorkspaces(config *config.Config) ([]hookdeck.Workspace, error) { } return client.ListWorkspaces() -} \ No newline at end of file +}