From a8274dcc0270ba605ee84b267c91920392058747 Mon Sep 17 00:00:00 2001 From: Gabriel Adrian Samfira Date: Fri, 17 Jun 2022 10:58:35 +0000 Subject: [PATCH] Add profile management in the CLI --- cmd/garm-cli/cmd/init.go | 4 +- cmd/garm-cli/cmd/login.go | 108 -------------- cmd/garm-cli/cmd/profile.go | 267 ++++++++++++++++++++++++++++++++++ cmd/garm-cli/cmd/root.go | 2 +- cmd/garm-cli/config/config.go | 58 ++++++++ 5 files changed, 328 insertions(+), 111 deletions(-) delete mode 100644 cmd/garm-cli/cmd/login.go create mode 100644 cmd/garm-cli/cmd/profile.go diff --git a/cmd/garm-cli/cmd/init.go b/cmd/garm-cli/cmd/init.go index dc0057c6..b0b5959e 100644 --- a/cmd/garm-cli/cmd/init.go +++ b/cmd/garm-cli/cmd/init.go @@ -37,11 +37,11 @@ var initCmd = &cobra.Command{ A newly installed runner manager needs to be initialized to become functional. This command sets the administrative user and password, generates a controller UUID which is used internally to identify runners -created by the manager and enables the service. +created by the manager and adds the profile to the local client config. Example usage: -garm-cli login --name=dev --url=https://runner.example.com --username=admin --password=superSecretPassword +garm-cli init --name=dev --url=https://runner.example.com --username=admin --password=superSecretPassword `, RunE: func(cmd *cobra.Command, args []string) error { if cfg != nil { diff --git a/cmd/garm-cli/cmd/login.go b/cmd/garm-cli/cmd/login.go deleted file mode 100644 index 738e2edd..00000000 --- a/cmd/garm-cli/cmd/login.go +++ /dev/null @@ -1,108 +0,0 @@ -// Copyright 2022 Cloudbase Solutions SRL -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may -// not use this file except in compliance with the License. You may obtain -// a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -// License for the specific language governing permissions and limitations -// under the License. - -package cmd - -import ( - "fmt" - "garm/cmd/garm-cli/common" - "garm/cmd/garm-cli/config" - "garm/params" - "strings" - - "github.com/spf13/cobra" -) - -var ( - loginProfileName string - loginURL string - loginPassword string - loginUserName string - loginFullName string - loginEmail string -) - -// loginCmd represents the login command -var loginCmd = &cobra.Command{ - Use: "login", - SilenceUsage: true, - Short: "Log into a manager", - Long: `Performs login for this machine on a remote garm: - -garm-cli login --name=dev --url=https://runner.example.com`, - RunE: func(cmd *cobra.Command, args []string) error { - if cfg != nil { - if cfg.HasManager(loginProfileName) { - return fmt.Errorf("a manager with name %s already exists in your local config", loginProfileName) - } - } - - if err := promptUnsetLoginVariables(); err != nil { - return err - } - - url := strings.TrimSuffix(loginURL, "/") - loginParams := params.PasswordLoginParams{ - Username: loginUserName, - Password: loginPassword, - } - - resp, err := cli.Login(url, loginParams) - if err != nil { - return err - } - - cfg.Managers = append(cfg.Managers, config.Manager{ - Name: loginProfileName, - BaseURL: url, - Token: resp, - }) - cfg.ActiveManager = loginProfileName - - if err := cfg.SaveConfig(); err != nil { - return err - } - return nil - }, -} - -func promptUnsetLoginVariables() error { - var err error - if loginUserName == "" { - loginUserName, err = common.PromptString("Username") - if err != nil { - return err - } - } - - if loginPassword == "" { - loginPassword, err = common.PromptPassword("Password") - if err != nil { - return err - } - } - return nil -} - -func init() { - rootCmd.AddCommand(loginCmd) - - loginCmd.Flags().StringVarP(&loginProfileName, "name", "n", "", "A name for this runner manager") - loginCmd.Flags().StringVarP(&loginURL, "url", "a", "", "The base URL for the runner manager API") - loginCmd.Flags().StringVarP(&loginUserName, "username", "u", "", "Username to log in as") - loginCmd.Flags().StringVarP(&loginPassword, "password", "p", "", "The user passowrd") - - loginCmd.MarkFlagRequired("name") - loginCmd.MarkFlagRequired("url") -} diff --git a/cmd/garm-cli/cmd/profile.go b/cmd/garm-cli/cmd/profile.go new file mode 100644 index 00000000..ca9b6689 --- /dev/null +++ b/cmd/garm-cli/cmd/profile.go @@ -0,0 +1,267 @@ +// Copyright 2022 Cloudbase Solutions SRL +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +package cmd + +import ( + "fmt" + "garm/cmd/garm-cli/common" + "garm/cmd/garm-cli/config" + "garm/params" + "strings" + + "github.com/jedib0t/go-pretty/v6/table" + "github.com/spf13/cobra" +) + +var ( + loginProfileName string + loginURL string + loginPassword string + loginUserName string + loginFullName string + loginEmail string +) + +// runnerCmd represents the runner command +var profileCmd = &cobra.Command{ + Use: "profile", + SilenceUsage: false, + Short: "Add, delete or update profiles", + Long: `Creates, deletes or updates bearer tokens for profiles.`, + Run: nil, +} + +var profileListCmd = &cobra.Command{ + Use: "list", + Aliases: []string{"ls"}, + Short: "List profiles", + Long: `List profiles. + +This command will list all currently defined profiles in the local configuration +file of the garm client. +`, + SilenceUsage: true, + RunE: func(cmd *cobra.Command, args []string) error { + if needsInit { + return needsInitError + } + + if cfg == nil { + return nil + } + + formatProfiles(cfg.Managers) + + return nil + }, +} + +var profileDeleteCmd = &cobra.Command{ + Use: "delete", + Aliases: []string{"remove", "rm", "del"}, + Short: "Delete profile", + Long: `Delete a profile from the local CLI configuration.`, + SilenceUsage: true, + RunE: func(cmd *cobra.Command, args []string) error { + if needsInit { + return needsInitError + } + + if len(args) == 0 { + return fmt.Errorf("requires a profile name") + } + + if err := cfg.DeleteProfile(args[0]); err != nil { + return err + } + + if err := cfg.SaveConfig(); err != nil { + return err + } + return nil + }, +} + +var poolSwitchCmd = &cobra.Command{ + Use: "switch", + Short: "Switch to a different profile", + Long: `Switch the CLI to a different profile.`, + SilenceUsage: true, + RunE: func(cmd *cobra.Command, args []string) error { + if needsInit { + return needsInitError + } + + if len(args) == 0 { + return fmt.Errorf("requires a profile name") + } + + if cfg != nil { + if !cfg.HasManager(args[0]) { + return fmt.Errorf("a profile with name %s does not exist", args[0]) + } + } + + cfg.ActiveManager = args[0] + + if err := cfg.SaveConfig(); err != nil { + return fmt.Errorf("error saving config: %s", err) + } + + return nil + }, +} + +var profileAddCmd = &cobra.Command{ + Use: "add", + Aliases: []string{"create"}, + Short: "Add profile", + Long: `Create a profile for a new garm installation.`, + SilenceUsage: true, + RunE: func(cmd *cobra.Command, args []string) error { + if cfg != nil { + if cfg.HasManager(loginProfileName) { + return fmt.Errorf("a manager with name %s already exists in your local config", loginProfileName) + } + } + + if err := promptUnsetLoginVariables(); err != nil { + return err + } + + url := strings.TrimSuffix(loginURL, "/") + loginParams := params.PasswordLoginParams{ + Username: loginUserName, + Password: loginPassword, + } + + resp, err := cli.Login(url, loginParams) + if err != nil { + return err + } + + cfg.Managers = append(cfg.Managers, config.Manager{ + Name: loginProfileName, + BaseURL: url, + Token: resp, + }) + cfg.ActiveManager = loginProfileName + + if err := cfg.SaveConfig(); err != nil { + return err + } + return nil + }, +} + +var profileLoginCmd = &cobra.Command{ + Use: "login", + Short: "Refresh bearer token for profile", + Long: `Logs into an existing garm installation. + +This command will refresh the bearer token associated with an already defined garm +installation, by performing a login. + `, + SilenceUsage: true, + RunE: func(cmd *cobra.Command, args []string) error { + if needsInit { + return needsInitError + } + + if cfg == nil { + // We should probably error out here + return nil + } + + if err := promptUnsetLoginVariables(); err != nil { + return err + } + + loginParams := params.PasswordLoginParams{ + Username: loginUserName, + Password: loginPassword, + } + + resp, err := cli.Login(mgr.BaseURL, loginParams) + if err != nil { + return err + } + if err := cfg.SetManagerToken(mgr.Name, resp); err != nil { + return fmt.Errorf("error saving new token: %s", err) + } + + if err := cfg.SaveConfig(); err != nil { + return fmt.Errorf("error saving config: %s", err) + } + + return nil + }, +} + +func init() { + profileLoginCmd.Flags().StringVarP(&loginUserName, "username", "u", "", "Username to log in as") + profileLoginCmd.Flags().StringVarP(&loginPassword, "password", "p", "", "The user passowrd") + + profileAddCmd.Flags().StringVarP(&loginProfileName, "name", "n", "", "A name for this runner manager") + profileAddCmd.Flags().StringVarP(&loginURL, "url", "a", "", "The base URL for the runner manager API") + profileAddCmd.Flags().StringVarP(&loginUserName, "username", "u", "", "Username to log in as") + profileAddCmd.Flags().StringVarP(&loginPassword, "password", "p", "", "The user passowrd") + profileAddCmd.MarkFlagRequired("name") + profileAddCmd.MarkFlagRequired("url") + + profileCmd.AddCommand( + profileListCmd, + profileLoginCmd, + poolSwitchCmd, + profileDeleteCmd, + profileAddCmd, + ) + + rootCmd.AddCommand(profileCmd) +} + +func formatProfiles(profiles []config.Manager) { + t := table.NewWriter() + header := table.Row{"Name", "Base URL"} + t.AppendHeader(header) + + for _, profile := range profiles { + name := profile.Name + if profile.Name == mgr.Name { + name = fmt.Sprintf("%s (current)", name) + } + t.AppendRow(table.Row{name, profile.BaseURL}) + t.AppendSeparator() + } + fmt.Println(t.Render()) +} + +func promptUnsetLoginVariables() error { + var err error + if loginUserName == "" { + loginUserName, err = common.PromptString("Username") + if err != nil { + return err + } + } + + if loginPassword == "" { + loginPassword, err = common.PromptPassword("Password") + if err != nil { + return err + } + } + return nil +} diff --git a/cmd/garm-cli/cmd/root.go b/cmd/garm-cli/cmd/root.go index 79df3236..7fb4f818 100644 --- a/cmd/garm-cli/cmd/root.go +++ b/cmd/garm-cli/cmd/root.go @@ -30,7 +30,7 @@ var ( active string needsInit bool debug bool - needsInitError = fmt.Errorf("Please log into a garm first") + needsInitError = fmt.Errorf("Please log into a garm installation first") ) // rootCmd represents the base command when called without any subcommands diff --git a/cmd/garm-cli/config/config.go b/cmd/garm-cli/config/config.go index 6192ac89..15b48d4d 100644 --- a/cmd/garm-cli/config/config.go +++ b/cmd/garm-cli/config/config.go @@ -15,8 +15,10 @@ package config import ( + "fmt" "os" "path/filepath" + "sync" "github.com/BurntSushi/toml" "github.com/pkg/errors" @@ -66,11 +68,14 @@ func LoadConfig() (*Config, error) { } type Config struct { + mux sync.Mutex Managers []Manager `toml:"manager"` ActiveManager string `toml:"active_manager"` } func (c *Config) HasManager(mgr string) bool { + c.mux.Lock() + defer c.mux.Unlock() if mgr == "" { return false } @@ -82,7 +87,58 @@ func (c *Config) HasManager(mgr string) bool { return false } +func (c *Config) SetManagerToken(name, token string) error { + c.mux.Lock() + defer c.mux.Unlock() + found := false + newManagerList := []Manager{} + for _, mgr := range c.Managers { + newMgr := Manager{ + Name: mgr.Name, + BaseURL: mgr.BaseURL, + Token: mgr.Token, + } + if mgr.Name == name { + found = true + newMgr.Token = token + } + newManagerList = append(newManagerList, newMgr) + } + if !found { + return fmt.Errorf("profile %s not found", name) + } + c.Managers = newManagerList + return nil +} + +func (c *Config) DeleteProfile(name string) error { + c.mux.Lock() + defer c.mux.Unlock() + newManagers := []Manager{} + for _, val := range c.Managers { + if val.Name == name { + continue + } + newManagers = append(newManagers, Manager{ + Name: val.Name, + BaseURL: val.BaseURL, + Token: val.Token, + }) + } + c.Managers = newManagers + if c.ActiveManager == name { + if len(c.Managers) > 0 { + c.ActiveManager = c.Managers[0].Name + } else { + c.ActiveManager = "" + } + } + return nil +} + func (c *Config) GetActiveConfig() (Manager, error) { + c.mux.Lock() + defer c.mux.Unlock() if c.ActiveManager == "" { return Manager{}, runnerErrors.ErrNotFound } @@ -96,6 +152,8 @@ func (c *Config) GetActiveConfig() (Manager, error) { } func (c *Config) SaveConfig() error { + c.mux.Lock() + defer c.mux.Unlock() cfgFile, err := getConfigFilePath() if err != nil { if !errors.Is(err, os.ErrNotExist) {