feat(cli): make url optional for login command (#10925) (#12466)

Allow `coder login` to log into existing deployment if available.

Update help and error messages to indicate that `coder login` is
available as a command.

Fixes #10925
Fixes #9551
This commit is contained in:
elasticspoon 2024-03-11 10:14:19 -04:00 committed by GitHub
parent bed61f7d2a
commit 773862a9f5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 75 additions and 7 deletions

View File

@ -136,16 +136,28 @@ func (r *RootCmd) login() *clibase.Cmd {
useTokenForSession bool useTokenForSession bool
) )
cmd := &clibase.Cmd{ cmd := &clibase.Cmd{
Use: "login <url>", Use: "login [<url>]",
Short: "Authenticate with Coder deployment", Short: "Authenticate with Coder deployment",
Middleware: clibase.RequireRangeArgs(0, 1), Middleware: clibase.RequireRangeArgs(0, 1),
Handler: func(inv *clibase.Invocation) error { Handler: func(inv *clibase.Invocation) error {
ctx := inv.Context() ctx := inv.Context()
rawURL := "" rawURL := ""
var urlSource string
if len(inv.Args) == 0 { if len(inv.Args) == 0 {
rawURL = r.clientURL.String() rawURL = r.clientURL.String()
urlSource = "flag"
if rawURL != "" && rawURL == inv.Environ.Get(envURL) {
urlSource = "environment"
}
} else { } else {
rawURL = inv.Args[0] rawURL = inv.Args[0]
urlSource = "argument"
}
if url, err := r.createConfig().URL().Read(); rawURL == "" && err == nil {
urlSource = "config"
rawURL = url
} }
if rawURL == "" { if rawURL == "" {
@ -187,6 +199,9 @@ func (r *RootCmd) login() *clibase.Cmd {
if err != nil { if err != nil {
return xerrors.Errorf("Failed to check server %q for first user, is the URL correct and is coder accessible from your browser? Error - has initial user: %w", serverURL.String(), err) return xerrors.Errorf("Failed to check server %q for first user, is the URL correct and is coder accessible from your browser? Error - has initial user: %w", serverURL.String(), err)
} }
_, _ = fmt.Fprintf(inv.Stdout, "Attempting to authenticate with %s URL: '%s'\n", urlSource, serverURL)
if !hasFirstUser { if !hasFirstUser {
_, _ = fmt.Fprintf(inv.Stdout, Caret+"Your Coder deployment hasn't been set up!\n") _, _ = fmt.Fprintf(inv.Stdout, Caret+"Your Coder deployment hasn't been set up!\n")

View File

@ -116,6 +116,7 @@ func TestLogin(t *testing.T) {
clitest.Start(t, inv) clitest.Start(t, inv)
pty.ExpectMatch(fmt.Sprintf("Attempting to authenticate with flag URL: '%s'", client.URL.String()))
matches := []string{ matches := []string{
"first user?", "yes", "first user?", "yes",
"username", "testuser", "username", "testuser",
@ -205,6 +206,7 @@ func TestLogin(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
}() }()
pty.ExpectMatch(fmt.Sprintf("Attempting to authenticate with argument URL: '%s'", client.URL.String()))
pty.ExpectMatch("Paste your token here:") pty.ExpectMatch("Paste your token here:")
pty.WriteLine(client.SessionToken()) pty.WriteLine(client.SessionToken())
if runtime.GOOS != "windows" { if runtime.GOOS != "windows" {
@ -215,6 +217,52 @@ func TestLogin(t *testing.T) {
<-doneChan <-doneChan
}) })
t.Run("ExistingUserURLSavedInConfig", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
url := client.URL.String()
coderdtest.CreateFirstUser(t, client)
inv, root := clitest.New(t, "login", "--no-open")
clitest.SetupConfig(t, client, root)
doneChan := make(chan struct{})
pty := ptytest.New(t).Attach(inv)
go func() {
defer close(doneChan)
err := inv.Run()
assert.NoError(t, err)
}()
pty.ExpectMatch(fmt.Sprintf("Attempting to authenticate with config URL: '%s'", url))
pty.ExpectMatch("Paste your token here:")
pty.WriteLine(client.SessionToken())
<-doneChan
})
t.Run("ExistingUserURLSavedInEnv", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
url := client.URL.String()
coderdtest.CreateFirstUser(t, client)
inv, _ := clitest.New(t, "login", "--no-open")
inv.Environ.Set("CODER_URL", url)
doneChan := make(chan struct{})
pty := ptytest.New(t).Attach(inv)
go func() {
defer close(doneChan)
err := inv.Run()
assert.NoError(t, err)
}()
pty.ExpectMatch(fmt.Sprintf("Attempting to authenticate with environment URL: '%s'", url))
pty.ExpectMatch("Paste your token here:")
pty.WriteLine(client.SessionToken())
<-doneChan
})
t.Run("ExistingUserInvalidTokenTTY", func(t *testing.T) { t.Run("ExistingUserInvalidTokenTTY", func(t *testing.T) {
t.Parallel() t.Parallel()
client := coderdtest.New(t, nil) client := coderdtest.New(t, nil)

View File

@ -119,7 +119,7 @@ func TestLogout(t *testing.T) {
go func() { go func() {
defer close(logoutChan) defer close(logoutChan)
err = logout.Run() err = logout.Run()
assert.ErrorContains(t, err, "You are not logged in. Try logging in using 'coder login <url>'.") assert.ErrorContains(t, err, "You are not logged in. Try logging in using 'coder login'.")
}() }()
<-logoutChan <-logoutChan

View File

@ -65,7 +65,9 @@ const (
varVerbose = "verbose" varVerbose = "verbose"
varOrganizationSelect = "organization" varOrganizationSelect = "organization"
varDisableDirect = "disable-direct-connections" varDisableDirect = "disable-direct-connections"
notLoggedInMessage = "You are not logged in. Try logging in using 'coder login <url>'."
notLoggedInMessage = "You are not logged in. Try logging in using 'coder login <url>'."
notLoggedInURLSavedMessage = "You are not logged in. Try logging in using 'coder login'."
envNoVersionCheck = "CODER_NO_VERSION_WARNING" envNoVersionCheck = "CODER_NO_VERSION_WARNING"
envNoFeatureWarning = "CODER_NO_FEATURE_WARNING" envNoFeatureWarning = "CODER_NO_FEATURE_WARNING"
@ -77,7 +79,10 @@ const (
envURL = "CODER_URL" envURL = "CODER_URL"
) )
var errUnauthenticated = xerrors.New(notLoggedInMessage) var (
errUnauthenticated = xerrors.New(notLoggedInMessage)
errUnauthenticatedURLSaved = xerrors.New(notLoggedInURLSavedMessage)
)
func (r *RootCmd) Core() []*clibase.Cmd { func (r *RootCmd) Core() []*clibase.Cmd {
// Please re-sort this list alphabetically if you change it! // Please re-sort this list alphabetically if you change it!
@ -574,7 +579,7 @@ func (r *RootCmd) initClientInternal(client *codersdk.Client, allowTokenMissing
// If the configuration files are absent, the user is logged out // If the configuration files are absent, the user is logged out
if os.IsNotExist(err) { if os.IsNotExist(err) {
if !allowTokenMissing { if !allowTokenMissing {
return errUnauthenticated return errUnauthenticatedURLSaved
} }
} else if err != nil { } else if err != nil {
return err return err

View File

@ -1,7 +1,7 @@
coder v0.0.0-devel coder v0.0.0-devel
USAGE: USAGE:
coder login [flags] <url> coder login [flags] [<url>]
Authenticate with Coder deployment Authenticate with Coder deployment

2
docs/cli/login.md generated
View File

@ -7,7 +7,7 @@ Authenticate with Coder deployment
## Usage ## Usage
```console ```console
coder login [flags] <url> coder login [flags] [<url>]
``` ```
## Options ## Options