From 4860f2ce21f187b197fd07fa12abb6834f35bc58 Mon Sep 17 00:00:00 2001 From: Simon Behar Date: Fri, 7 Jun 2019 11:41:47 -0700 Subject: [PATCH] Added logout ability (`argocd logout`) (#1582) --- cmd/argocd/commands/context.go | 63 +++++++++++++++++++++++++++-- cmd/argocd/commands/context_test.go | 60 +++++++++++++++++++++++++++ cmd/argocd/commands/logout.go | 50 +++++++++++++++++++++++ cmd/argocd/commands/logout_test.go | 39 ++++++++++++++++++ cmd/argocd/commands/root.go | 1 + cmd/argocd/commands/testdata/config | 18 +++++++++ util/localconfig/localconfig.go | 59 ++++++++++++++++++++++++++- 7 files changed, 285 insertions(+), 5 deletions(-) create mode 100644 cmd/argocd/commands/context_test.go create mode 100644 cmd/argocd/commands/logout.go create mode 100644 cmd/argocd/commands/logout_test.go create mode 100644 cmd/argocd/commands/testdata/config diff --git a/cmd/argocd/commands/context.go b/cmd/argocd/commands/context.go index c2b34066b28be..7951db6b481b6 100644 --- a/cmd/argocd/commands/context.go +++ b/cmd/argocd/commands/context.go @@ -8,6 +8,8 @@ import ( "strings" "text/tabwriter" + "github.com/spf13/pflag" + log "github.com/sirupsen/logrus" "github.com/spf13/cobra" @@ -18,16 +20,36 @@ import ( // NewContextCommand returns a new instance of an `argocd ctx` command func NewContextCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command { + var delete bool var command = &cobra.Command{ Use: "context", Aliases: []string{"ctx"}, Short: "Switch between contexts", Run: func(c *cobra.Command, args []string) { + + localCfg, err := localconfig.ReadLocalConfig(clientOpts.ConfigPath) + errors.CheckError(err) + + deletePresentContext := false + c.Flags().Visit(func(f *pflag.Flag) { + if f.Name == "delete" { + deletePresentContext = true + } + }) + if len(args) == 0 { - printArgoCDContexts(clientOpts.ConfigPath) - return + if deletePresentContext { + err := deleteContext(localCfg.CurrentContext, clientOpts.ConfigPath) + errors.CheckError(err) + return + } else { + printArgoCDContexts(clientOpts.ConfigPath) + return + } } + ctxName := args[0] + argoCDDir, err := localconfig.DefaultConfigDir() errors.CheckError(err) prevCtxFile := path.Join(argoCDDir, ".prev-ctx") @@ -37,8 +59,6 @@ func NewContextCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command { errors.CheckError(err) ctxName = string(prevCtxBytes) } - localCfg, err := localconfig.ReadLocalConfig(clientOpts.ConfigPath) - errors.CheckError(err) if localCfg.CurrentContext == ctxName { fmt.Printf("Already at context '%s'\n", localCfg.CurrentContext) return @@ -48,6 +68,7 @@ func NewContextCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command { } prevCtx := localCfg.CurrentContext localCfg.CurrentContext = ctxName + err = localconfig.WriteLocalConfig(*localCfg, clientOpts.ConfigPath) errors.CheckError(err) err = ioutil.WriteFile(prevCtxFile, []byte(prevCtx), 0644) @@ -55,9 +76,43 @@ func NewContextCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command { fmt.Printf("Switched to context '%s'\n", localCfg.CurrentContext) }, } + command.Flags().BoolVar(&delete, "delete", false, "Delete the context instead of switching to it") return command } +func deleteContext(context, configPath string) error { + + localCfg, err := localconfig.ReadLocalConfig(configPath) + errors.CheckError(err) + if localCfg == nil { + return fmt.Errorf("Nothing to logout from") + } + + serverName, ok := localCfg.RemoveContext(context) + if !ok { + return fmt.Errorf("Context %s does not exist", context) + } + _ = localCfg.RemoveUser(context) + _ = localCfg.RemoveServer(serverName) + + if localCfg.IsEmpty() { + err = localconfig.DeleteLocalConfig(configPath) + errors.CheckError(err) + } else { + if localCfg.CurrentContext == context { + localCfg.CurrentContext = localCfg.Contexts[0].Name + } + err = localconfig.ValidateLocalConfig(*localCfg) + if err != nil { + return fmt.Errorf("Error in logging out") + } + err = localconfig.WriteLocalConfig(*localCfg, configPath) + errors.CheckError(err) + } + fmt.Printf("Context '%s' deleted\n", context) + return nil +} + func printArgoCDContexts(configPath string) { localCfg, err := localconfig.ReadLocalConfig(configPath) errors.CheckError(err) diff --git a/cmd/argocd/commands/context_test.go b/cmd/argocd/commands/context_test.go new file mode 100644 index 0000000000000..1ab5cc279f146 --- /dev/null +++ b/cmd/argocd/commands/context_test.go @@ -0,0 +1,60 @@ +package commands + +import ( + "io/ioutil" + "os" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/argoproj/argo-cd/util/localconfig" +) + +const testConfig = `contexts: +- name: argocd.example.com:443 + server: argocd.example.com:443 + user: argocd.example.com:443 +- name: localhost:8080 + server: localhost:8080 + user: localhost:8080 +current-context: localhost:8080 +servers: +- server: argocd.example.com:443 +- plain-text: true + server: localhost:8080 +users: +- auth-token: vErrYS3c3tReFRe$hToken + name: argocd.example.com:443 + refresh-token: vErrYS3c3tReFRe$hToken +- auth-token: vErrYS3c3tReFRe$hToken + name: localhost:8080` + +const testConfigFilePath = "./testdata/config" + +func TestContextDelete(t *testing.T) { + + // Write the test config file + err := ioutil.WriteFile(testConfigFilePath, []byte(testConfig), os.ModePerm) + assert.NoError(t, err) + + localConfig, err := localconfig.ReadLocalConfig(testConfigFilePath) + assert.NoError(t, err) + assert.Equal(t, localConfig.CurrentContext, "localhost:8080") + assert.Contains(t, localConfig.Contexts, localconfig.ContextRef{Name: "localhost:8080", Server: "localhost:8080", User: "localhost:8080"}) + + err = deleteContext("localhost:8080", testConfigFilePath) + assert.NoError(t, err) + + localConfig, err = localconfig.ReadLocalConfig(testConfigFilePath) + assert.NoError(t, err) + assert.Equal(t, localConfig.CurrentContext, "argocd.example.com:443") + assert.NotContains(t, localConfig.Contexts, localconfig.ContextRef{Name: "localhost:8080", Server: "localhost:8080", User: "localhost:8080"}) + assert.NotContains(t, localConfig.Servers, localconfig.Server{PlainText: true, Server: "localhost:8080"}) + assert.NotContains(t, localConfig.Users, localconfig.User{AuthToken: "vErrYS3c3tReFRe$hToken", Name: "localhost:8080"}) + assert.Contains(t, localConfig.Contexts, localconfig.ContextRef{Name: "argocd.example.com:443", Server: "argocd.example.com:443", User: "argocd.example.com:443"}) + + // Write the file again so that no conflicts are made in git + err = ioutil.WriteFile(testConfigFilePath, []byte(testConfig), os.ModePerm) + assert.NoError(t, err) + +} diff --git a/cmd/argocd/commands/logout.go b/cmd/argocd/commands/logout.go new file mode 100644 index 0000000000000..68cef2d0a863e --- /dev/null +++ b/cmd/argocd/commands/logout.go @@ -0,0 +1,50 @@ +package commands + +import ( + "fmt" + "os" + + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + + "github.com/argoproj/argo-cd/errors" + argocdclient "github.com/argoproj/argo-cd/pkg/apiclient" + "github.com/argoproj/argo-cd/util/localconfig" +) + +// NewLogoutCommand returns a new instance of `argocd logout` command +func NewLogoutCommand(globalClientOpts *argocdclient.ClientOptions) *cobra.Command { + var command = &cobra.Command{ + Use: "logout CONTEXT", + Short: "Log out from Argo CD", + Long: "Log out from Argo CD", + Run: func(c *cobra.Command, args []string) { + if len(args) == 0 { + c.HelpFunc()(c, args) + os.Exit(1) + } + context := args[0] + + localCfg, err := localconfig.ReadLocalConfig(globalClientOpts.ConfigPath) + errors.CheckError(err) + if localCfg == nil { + log.Fatalf("Nothing to logout from") + } + + ok := localCfg.RemoveToken(context) + if !ok { + log.Fatalf("Context %s does not exist", context) + } + + err = localconfig.ValidateLocalConfig(*localCfg) + if err != nil { + log.Fatalf("Error in logging out: %s", err) + } + err = localconfig.WriteLocalConfig(*localCfg, globalClientOpts.ConfigPath) + errors.CheckError(err) + + fmt.Printf("Logged out from '%s'\n", context) + }, + } + return command +} diff --git a/cmd/argocd/commands/logout_test.go b/cmd/argocd/commands/logout_test.go new file mode 100644 index 0000000000000..ea5e0a5a96de0 --- /dev/null +++ b/cmd/argocd/commands/logout_test.go @@ -0,0 +1,39 @@ +package commands + +import ( + "io/ioutil" + "os" + "testing" + + "github.com/argoproj/argo-cd/pkg/apiclient" + + "github.com/stretchr/testify/assert" + + "github.com/argoproj/argo-cd/util/localconfig" +) + +func TestLogout(t *testing.T) { + + // Write the test config file + err := ioutil.WriteFile(testConfigFilePath, []byte(testConfig), os.ModePerm) + assert.NoError(t, err) + + localConfig, err := localconfig.ReadLocalConfig(testConfigFilePath) + assert.NoError(t, err) + assert.Equal(t, localConfig.CurrentContext, "localhost:8080") + assert.Contains(t, localConfig.Contexts, localconfig.ContextRef{Name: "localhost:8080", Server: "localhost:8080", User: "localhost:8080"}) + + command := NewLogoutCommand(&apiclient.ClientOptions{ConfigPath: testConfigFilePath}) + command.Run(nil, []string{"localhost:8080"}) + + localConfig, err = localconfig.ReadLocalConfig(testConfigFilePath) + assert.NoError(t, err) + assert.Equal(t, localConfig.CurrentContext, "localhost:8080") + assert.NotContains(t, localConfig.Users, localconfig.User{AuthToken: "vErrYS3c3tReFRe$hToken", Name: "localhost:8080"}) + assert.Contains(t, localConfig.Contexts, localconfig.ContextRef{Name: "argocd.example.com:443", Server: "argocd.example.com:443", User: "argocd.example.com:443"}) + + // Write the file again so that no conflicts are made in git + err = ioutil.WriteFile(testConfigFilePath, []byte(testConfig), os.ModePerm) + assert.NoError(t, err) + +} diff --git a/cmd/argocd/commands/root.go b/cmd/argocd/commands/root.go index 0872abfc8f7d8..7e7c358016535 100644 --- a/cmd/argocd/commands/root.go +++ b/cmd/argocd/commands/root.go @@ -45,6 +45,7 @@ func NewCommand() *cobra.Command { command.AddCommand(NewContextCommand(&clientOpts)) command.AddCommand(NewProjectCommand(&clientOpts)) command.AddCommand(NewAccountCommand(&clientOpts)) + command.AddCommand(NewLogoutCommand(&clientOpts)) defaultLocalConfigPath, err := localconfig.DefaultLocalConfigPath() errors.CheckError(err) diff --git a/cmd/argocd/commands/testdata/config b/cmd/argocd/commands/testdata/config new file mode 100644 index 0000000000000..de6418f49e2d0 --- /dev/null +++ b/cmd/argocd/commands/testdata/config @@ -0,0 +1,18 @@ +contexts: +- name: argocd.example.com:443 + server: argocd.example.com:443 + user: argocd.example.com:443 +- name: localhost:8080 + server: localhost:8080 + user: localhost:8080 +current-context: localhost:8080 +servers: +- server: argocd.example.com:443 +- plain-text: true + server: localhost:8080 +users: +- auth-token: vErrYS3c3tReFRe$hToken + name: argocd.example.com:443 + refresh-token: vErrYS3c3tReFRe$hToken +- auth-token: vErrYS3c3tReFRe$hToken + name: localhost:8080 \ No newline at end of file diff --git a/util/localconfig/localconfig.go b/util/localconfig/localconfig.go index d82a9ffe856b8..8d947d3c00f37 100644 --- a/util/localconfig/localconfig.go +++ b/util/localconfig/localconfig.go @@ -6,7 +6,7 @@ import ( "os/user" "path" - jwt "github.com/dgrijalva/jwt-go" + "github.com/dgrijalva/jwt-go" configUtil "github.com/argoproj/argo-cd/util/config" ) @@ -102,6 +102,14 @@ func WriteLocalConfig(config LocalConfig, configPath string) error { return configUtil.MarshalLocalYAMLFile(configPath, config) } +func DeleteLocalConfig(configPath string) error { + _, err := os.Stat(configPath) + if os.IsNotExist(err) { + return err + } + return os.Remove(configPath) +} + // ResolveContext resolves the specified context. If unspecified, resolves the current context func (l *LocalConfig) ResolveContext(name string) (*Context, error) { if name == "" { @@ -146,6 +154,17 @@ func (l *LocalConfig) UpsertServer(server Server) { l.Servers = append(l.Servers, server) } +// Returns true if server was removed successfully +func (l *LocalConfig) RemoveServer(serverName string) bool { + for i, s := range l.Servers { + if s.Server == serverName { + l.Servers = append(l.Servers[:i], l.Servers[i+1:]...) + return true + } + } + return false +} + func (l *LocalConfig) GetUser(name string) (*User, error) { for _, u := range l.Users { if u.Name == name { @@ -165,6 +184,29 @@ func (l *LocalConfig) UpsertUser(user User) { l.Users = append(l.Users, user) } +// Returns true if user was removed successfully +func (l *LocalConfig) RemoveUser(serverName string) bool { + for i, u := range l.Users { + if u.Name == serverName { + l.Users = append(l.Users[:i], l.Users[i+1:]...) + return true + } + } + return false +} + +// Returns true if user was removed successfully +func (l *LocalConfig) RemoveToken(serverName string) bool { + for i, u := range l.Users { + if u.Name == serverName { + l.Users[i].RefreshToken = "" + l.Users[i].AuthToken = "" + return true + } + } + return false +} + func (l *LocalConfig) UpsertContext(context ContextRef) { for i, c := range l.Contexts { if c.Name == context.Name { @@ -175,6 +217,21 @@ func (l *LocalConfig) UpsertContext(context ContextRef) { l.Contexts = append(l.Contexts, context) } +// Returns true if context was removed successfully +func (l *LocalConfig) RemoveContext(serverName string) (string, bool) { + for i, c := range l.Contexts { + if c.Name == serverName { + l.Contexts = append(l.Contexts[:i], l.Contexts[i+1:]...) + return c.Server, true + } + } + return "", false +} + +func (l *LocalConfig) IsEmpty() bool { + return len(l.Servers) == 0 +} + // DefaultConfigDir returns the local configuration path for settings such as cached authentication tokens. func DefaultConfigDir() (string, error) { usr, err := user.Current()