From 4a951261c254775a4a3775bc98847e6cfb244619 Mon Sep 17 00:00:00 2001 From: Kamal Nasser Date: Sun, 25 Mar 2018 21:16:45 +0300 Subject: [PATCH 1/4] Implement context switching, allowing for multiple configured API access keys --- args.go | 2 ++ commands/auth.go | 8 +++----- commands/commands_test.go | 2 +- commands/doit.go | 39 ++++++++++++++++++++++++++++++++++++++- doit.go | 9 ++++----- 5 files changed, 48 insertions(+), 12 deletions(-) diff --git a/args.go b/args.go index 121d465ea..0d8cb1980 100644 --- a/args.go +++ b/args.go @@ -16,6 +16,8 @@ package doctl const ( // ArgAccessToken is the access token to be used for the operations ArgAccessToken = "access-token" + // ArgContext is the name of the auth context to use + ArgContext = "context" // ArgActionID is an action id argument. ArgActionID = "action-id" // ArgActionAfter is an action after argument. diff --git a/commands/auth.go b/commands/auth.go index cf34da423..684781959 100644 --- a/commands/auth.go +++ b/commands/auth.go @@ -23,11 +23,9 @@ import ( "golang.org/x/crypto/ssh/terminal" "github.com/spf13/cobra" - "github.com/spf13/viper" - "github.com/digitalocean/doctl" ) -// ErrUnknownTerminal signies an unknown terminal. It is returned when doit +// ErrUnknownTerminal signifies an unknown terminal. It is returned when doit // can't ascertain the current terminal type with requesting an auth token. var ErrUnknownTerminal = errors.New("unknown terminal") @@ -73,7 +71,7 @@ func Auth() *Command { // XDG_CONFIG_HOME is not set, use $HOME/.config. On Windows use %APPDATA%/doctl/config. func RunAuthInit( retrieveUserTokenFunc func() (string, error) ) func (c *CmdConfig) error { return func(c * CmdConfig) error { - token := viper.GetString(doctl.ArgAccessToken) + token := c.getContextAccessToken() if token == "" { in, err := retrieveUserTokenFunc() @@ -86,7 +84,7 @@ func RunAuthInit( retrieveUserTokenFunc func() (string, error) ) func (c *CmdCon fmt.Fprintln(c.Out) } - viper.Set(doctl.ArgAccessToken, string(token)) + c.setContextAccessToken(string(token)) fmt.Fprintln(c.Out) fmt.Fprint(c.Out, "Validating token... ") diff --git a/commands/commands_test.go b/commands/commands_test.go index 4be0891af..f2a5d2f58 100644 --- a/commands/commands_test.go +++ b/commands/commands_test.go @@ -246,7 +246,7 @@ func NewTestConfig() *TestConfig { var _ doctl.Config = &TestConfig{} -func (c *TestConfig) GetGodoClient(trace bool) (*godo.Client, error) { +func (c *TestConfig) GetGodoClient(trace bool, accessToken string) (*godo.Client, error) { return &godo.Client{}, nil } diff --git a/commands/doit.go b/commands/doit.go index 60506bf68..45d3fa33f 100644 --- a/commands/doit.go +++ b/commands/doit.go @@ -44,6 +44,9 @@ var DoitCmd = &Command{ }, } +// Context holds the current auth context +var Context string + // ApiURL holds the API URL to use. var ApiURL string @@ -78,6 +81,7 @@ func init() { DoitCmd.PersistentFlags().StringVarP(&cfgFile, "config", "c", "", "config file (default is $HOME/.config/doctl/config.yaml)") DoitCmd.PersistentFlags().StringVarP(&Token, doctl.ArgAccessToken, "t", "", "API V2 Access Token") + DoitCmd.PersistentFlags().StringVarP(&Context, doctl.ArgContext, "", "default", "authentication context name") DoitCmd.PersistentFlags().StringVarP(&Output, "output", "o", "text", "output format [text|json]") DoitCmd.PersistentFlags().StringVarP(&ApiURL, "api-url", "u", "", "Override default API V2 endpoint") DoitCmd.PersistentFlags().BoolVarP(&Verbose, "verbose", "v", false, "verbose output") @@ -90,6 +94,8 @@ func init() { viper.BindPFlag("api-url", DoitCmd.PersistentFlags().Lookup("api-url")) viper.BindPFlag("output", DoitCmd.PersistentFlags().Lookup("output")) viper.BindEnv("enable-beta", "DIGITALOCEAN_ENABLE_BETA") + viper.BindEnv(doctl.ArgContext, "DIGITALOCEAN_CONTEXT") + viper.BindPFlag(doctl.ArgContext, DoitCmd.PersistentFlags().Lookup("context")) addCommands() } @@ -302,6 +308,8 @@ type CmdConfig struct { Args []string initServices func(*CmdConfig) error + getContextAccessToken func() string + setContextAccessToken func(string) // services Keys func() do.KeysService @@ -335,7 +343,8 @@ func NewCmdConfig(ns string, dc doctl.Config, out io.Writer, args []string, init Args: args, initServices: func(c *CmdConfig) error { - godoClient, err := c.Doit.GetGodoClient(Trace) + accessToken := c.getContextAccessToken() + godoClient, err := c.Doit.GetGodoClient(Trace, accessToken) if err != nil { return fmt.Errorf("unable to initialize DigitalOcean api client: %s", err) } @@ -362,6 +371,34 @@ func NewCmdConfig(ns string, dc doctl.Config, out io.Writer, args []string, init return nil }, + + getContextAccessToken: func() string { + context := viper.GetString("context") + token := "" + switch context { + case "default": + token = viper.GetString(doctl.ArgAccessToken) + default: + contexts := viper.GetStringMapString("auth-contexts") + + token = contexts[context] + } + + return token + }, + + setContextAccessToken: func(token string) { + context := viper.GetString("context") + switch context { + case "default": + viper.Set(doctl.ArgAccessToken, token) + default: + contexts := viper.GetStringMapString("auth-contexts") + contexts[context] = token + + viper.Set("auth-contexts", contexts) + } + }, } if initGodo { diff --git a/doit.go b/doit.go index 6aeb868f2..2241b7ee2 100644 --- a/doit.go +++ b/doit.go @@ -153,7 +153,7 @@ func (glv *GithubLatestVersioner) LatestVersion() (string, error) { // Config is an interface that represent doit's config. type Config interface { - GetGodoClient(trace bool) (*godo.Client, error) + GetGodoClient(trace bool, accessToken string) (*godo.Client, error) SSH(user, host, keyPath string, port int, opts ssh.Options) runner.Runner Set(ns, key string, val interface{}) GetString(ns, key string) (string, error) @@ -170,13 +170,12 @@ type LiveConfig struct { var _ Config = &LiveConfig{} // GetGodoClient returns a GodoClient. -func (c *LiveConfig) GetGodoClient(trace bool) (*godo.Client, error) { - token := viper.GetString(ArgAccessToken) - if token == "" { +func (c *LiveConfig) GetGodoClient(trace bool, accessToken string) (*godo.Client, error) { + if accessToken == "" { return nil, fmt.Errorf("access token is required. (hint: run 'doctl auth init')") } - tokenSource := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token}) + tokenSource := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: accessToken}) oauthClient := oauth2.NewClient(oauth2.NoContext, tokenSource) if trace { From 44a2aa1c650e2824d4cb08367ae8c7243b1f6d3a Mon Sep 17 00:00:00 2001 From: Kamal Nasser Date: Thu, 29 Mar 2018 21:16:15 +0300 Subject: [PATCH 2/4] update commands_test to work with auth contexts --- commands/commands_test.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/commands/commands_test.go b/commands/commands_test.go index f2a5d2f58..74a7382cf 100644 --- a/commands/commands_test.go +++ b/commands/commands_test.go @@ -184,6 +184,12 @@ func withTestClient(t *testing.T, tFn testFn) { // can stub this out, since the return is dictated by the mocks. initServices: func(c *CmdConfig) error { return nil }, + getContextAccessToken: func() string { + return viper.GetString(doctl.ArgAccessToken) + }, + + setContextAccessToken: func(token string) { }, + Keys: func() do.KeysService { return &tm.keys }, Sizes: func() do.SizesService { return &tm.sizes }, Regions: func() do.RegionsService { return &tm.regions }, From 6635256180e7115aaad580a8dec2efa536407eb2 Mon Sep 17 00:00:00 2001 From: Kamal Nasser Date: Sun, 8 Apr 2018 09:50:04 +0300 Subject: [PATCH 3/4] separate context config value and cli flag --- commands/auth.go | 20 +++++++++++++++++--- commands/doit.go | 18 +++++++++++++----- 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/commands/auth.go b/commands/auth.go index 684781959..9fcbbcf07 100644 --- a/commands/auth.go +++ b/commands/auth.go @@ -23,6 +23,7 @@ import ( "golang.org/x/crypto/ssh/terminal" "github.com/spf13/cobra" + "github.com/spf13/viper" ) // ErrUnknownTerminal signifies an unknown terminal. It is returned when doit @@ -63,14 +64,15 @@ func Auth() *Command { } cmdBuilderWithInit(cmd, RunAuthInit(retrieveUserTokenFromCommandLine), "init", "initialize configuration", Writer, false, docCategories("auth")) + cmdBuilderWithInit(cmd, RunAuthSwitch, "switch", "writes the auth context permanently to config", Writer, false, docCategories("auth")) return cmd } // RunAuthInit initializes the doctl config. Configuration is stored in $XDG_CONFIG_HOME/doctl. On Unix, if // XDG_CONFIG_HOME is not set, use $HOME/.config. On Windows use %APPDATA%/doctl/config. -func RunAuthInit( retrieveUserTokenFunc func() (string, error) ) func (c *CmdConfig) error { - return func(c * CmdConfig) error { +func RunAuthInit(retrieveUserTokenFunc func() (string, error)) func(c *CmdConfig) error { + return func(c *CmdConfig) error { token := c.getContextAccessToken() if token == "" { @@ -80,7 +82,7 @@ func RunAuthInit( retrieveUserTokenFunc func() (string, error) ) func (c *CmdCon } token = strings.TrimSpace(in) } else { - fmt.Fprintf(c.Out,"Using token [%v]", token) + fmt.Fprintf(c.Out, "Using token [%v]", token) fmt.Fprintln(c.Out) } @@ -106,3 +108,15 @@ func RunAuthInit( retrieveUserTokenFunc func() (string, error) ) func (c *CmdCon return writeConfig() } } + +func RunAuthSwitch(c *CmdConfig) error { + context := Context + if context == "" { + context = viper.GetString("context") + } + + viper.Set("context", context) + + fmt.Printf("Now using context [%s] by default\n", context) + return writeConfig() +} diff --git a/commands/doit.go b/commands/doit.go index 45d3fa33f..10c34ca99 100644 --- a/commands/doit.go +++ b/commands/doit.go @@ -81,12 +81,13 @@ func init() { DoitCmd.PersistentFlags().StringVarP(&cfgFile, "config", "c", "", "config file (default is $HOME/.config/doctl/config.yaml)") DoitCmd.PersistentFlags().StringVarP(&Token, doctl.ArgAccessToken, "t", "", "API V2 Access Token") - DoitCmd.PersistentFlags().StringVarP(&Context, doctl.ArgContext, "", "default", "authentication context name") DoitCmd.PersistentFlags().StringVarP(&Output, "output", "o", "text", "output format [text|json]") DoitCmd.PersistentFlags().StringVarP(&ApiURL, "api-url", "u", "", "Override default API V2 endpoint") DoitCmd.PersistentFlags().BoolVarP(&Verbose, "verbose", "v", false, "verbose output") DoitCmd.PersistentFlags().BoolVarP(&Trace, "trace", "", false, "trace api access") + DoitCmd.PersistentFlags().StringVarP(&Context, doctl.ArgContext, "", "", "authentication context name") + viper.SetEnvPrefix("DIGITALOCEAN") viper.BindEnv(doctl.ArgAccessToken, "DIGITALOCEAN_ACCESS_TOKEN") viper.BindPFlag(doctl.ArgAccessToken, DoitCmd.PersistentFlags().Lookup("access-token")) @@ -94,8 +95,6 @@ func init() { viper.BindPFlag("api-url", DoitCmd.PersistentFlags().Lookup("api-url")) viper.BindPFlag("output", DoitCmd.PersistentFlags().Lookup("output")) viper.BindEnv("enable-beta", "DIGITALOCEAN_ENABLE_BETA") - viper.BindEnv(doctl.ArgContext, "DIGITALOCEAN_CONTEXT") - viper.BindPFlag(doctl.ArgContext, DoitCmd.PersistentFlags().Lookup("context")) addCommands() } @@ -122,6 +121,7 @@ func initConfig() { } viper.SetDefault("output", "text") + viper.SetDefault("context", "default") } func findConfig() (string, error) { @@ -373,8 +373,12 @@ func NewCmdConfig(ns string, dc doctl.Config, out io.Writer, args []string, init }, getContextAccessToken: func() string { - context := viper.GetString("context") + context := Context + if context == "" { + context = viper.GetString("context") + } token := "" + switch context { case "default": token = viper.GetString(doctl.ArgAccessToken) @@ -388,7 +392,11 @@ func NewCmdConfig(ns string, dc doctl.Config, out io.Writer, args []string, init }, setContextAccessToken: func(token string) { - context := viper.GetString("context") + context := Context + if context == "" { + context = viper.GetString("context") + } + switch context { case "default": viper.Set(doctl.ArgAccessToken, token) From 95dc11a2e7cb32be9a4cc4d7e2430c8118e5ba68 Mon Sep 17 00:00:00 2001 From: Kamal Nasser Date: Sun, 8 Apr 2018 09:54:34 +0300 Subject: [PATCH 4/4] fix tests --- commands/auth_test.go | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/commands/auth_test.go b/commands/auth_test.go index efe010a2d..2f79e5033 100644 --- a/commands/auth_test.go +++ b/commands/auth_test.go @@ -18,22 +18,23 @@ import ( "io/ioutil" "testing" - "github.com/digitalocean/doctl/do" - "github.com/stretchr/testify/assert" "errors" - "github.com/spf13/viper" + "github.com/digitalocean/doctl" + "github.com/digitalocean/doctl/do" + "github.com/spf13/viper" + "github.com/stretchr/testify/assert" ) func TestAuthCommand(t *testing.T) { cmd := Auth() assert.NotNil(t, cmd) - assertCommandNames(t, cmd, "init") + assertCommandNames(t, cmd, "init", "switch") } func TestAuthInit(t *testing.T) { cfw := cfgFileWriter - viper.Set(doctl.ArgAccessToken, nil); + viper.Set(doctl.ArgAccessToken, nil) defer func() { cfgFileWriter = cfw }() @@ -54,10 +55,10 @@ func TestAuthInit(t *testing.T) { func TestAuthInitWithProvidedToken(t *testing.T) { cfw := cfgFileWriter - viper.Set(doctl.ArgAccessToken, "valid-token"); + viper.Set(doctl.ArgAccessToken, "valid-token") defer func() { cfgFileWriter = cfw - viper.Set(doctl.ArgAccessToken, nil); + viper.Set(doctl.ArgAccessToken, nil) }() retrieveUserTokenFunc := func() (string, error) {