Add profile management in the CLI

This commit is contained in:
Gabriel Adrian Samfira 2022-06-17 10:58:35 +00:00
parent c089217a53
commit a8274dcc02
5 changed files with 328 additions and 111 deletions

View file

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

View file

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

267
cmd/garm-cli/cmd/profile.go Normal file
View file

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

View file

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

View file

@ -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) {