Add richer login command output for TFC/TFE

This adds a changed login experience for Terraform Cloud and Terraform
Enterprise hosts, which has always run on its own custom protocol.
update-tfe-login-flow
Chris Arcand 5 years ago
parent c6278bbe37
commit b2c4298bb7

@ -4,8 +4,10 @@ import (
"context"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"log"
"math/rand"
"net"
@ -111,29 +113,15 @@ func (c *LoginCommand) Run(args []string) int {
}
clientConfig, err := host.ServiceOAuthClient("login.v1")
var terraformCloudServiceURL *url.URL
switch err.(type) {
case nil:
// Great! No problem, then.
case *disco.ErrServiceNotProvided:
// This is also fine! We'll try the manual token creation process.
case *disco.ErrVersionNotSupported:
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Warning,
"Host does not support Terraform login",
fmt.Sprintf("The given hostname %q allows creating Terraform authorization tokens, but requires a newer version of Terraform CLI to do so.", dispHostname),
))
default:
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Warning,
"Host does not support Terraform login",
fmt.Sprintf("The given hostname %q cannot support \"terraform login\": %s.", dispHostname, err),
))
}
// If login service is unavailable, check for a TFE v2 API as fallback
var service *url.URL
if clientConfig == nil {
service, err = host.ServiceURL("tfe.v2")
// Ok! Are we speaking to Terraform Cloud/Enterprise? If so we'll try Terraform Cloud/Enterprise's
// manual token creation process.
terraformCloudServiceURL, err = host.ServiceURL("tfe.v2")
switch err.(type) {
case nil:
// Success!
@ -156,6 +144,18 @@ func (c *LoginCommand) Run(args []string) int {
fmt.Sprintf("The given hostname %q cannot support \"terraform login\": %s.", dispHostname, err),
))
}
case *disco.ErrVersionNotSupported:
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Warning,
"Host does not support Terraform login",
fmt.Sprintf("The given hostname %q allows creating Terraform authorization tokens, but requires a newer version of Terraform CLI to do so.", dispHostname),
))
default:
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Warning,
"Host does not support Terraform login",
fmt.Sprintf("The given hostname %q cannot support \"terraform login\": %s.", dispHostname, err),
))
}
if credsCtx.Location == cliconfig.CredentialsInOtherFile {
@ -174,7 +174,7 @@ func (c *LoginCommand) Run(args []string) int {
var token svcauth.HostCredentialsToken
var tokenDiags tfdiags.Diagnostics
// Prefer Terraform login if available
// Fetch the token
if clientConfig != nil {
var oauthToken *oauth2.Token
@ -195,8 +195,8 @@ func (c *LoginCommand) Run(args []string) int {
if oauthToken != nil {
token = svcauth.HostCredentialsToken(oauthToken.AccessToken)
}
} else if service != nil {
token, tokenDiags = c.interactiveGetTokenByUI(hostname, credsCtx, service)
} else if terraformCloudServiceURL != nil {
token, tokenDiags = c.interactiveGetTokenByUI(hostname, credsCtx, terraformCloudServiceURL)
}
diags = diags.Append(tokenDiags)
@ -220,17 +220,68 @@ func (c *LoginCommand) Run(args []string) int {
}
c.Ui.Output("\n---------------------------------------------------------------------------------\n")
c.Ui.Output(
fmt.Sprintf(
c.Colorize().Color(strings.TrimSpace(`
if terraformCloudServiceURL != nil {
if hostname == "app.terraform.io" { // Terraform Cloud
var motd struct {
Message string `json:"msg"`
Errors []interface{} `json:"errors"`
}
req, _ := http.NewRequest("GET", fmt.Sprintf("https://%s/api/terraform/motd?source=terraform-login", hostname), nil)
if err != nil {
diags = diags.Append(err)
c.showDiagnostics(diags)
return 1
}
req.Header.Set("Authorization", "Bearer "+token.Token())
resp, err := httpclient.New().Do(req)
if err == nil {
body, _ := ioutil.ReadAll(resp.Body)
defer resp.Body.Close()
json.Unmarshal(body, &motd)
}
// Use the message payload, else a default message if the platform-provided
// message is unavailable for any reason - be it the request failing or any
// sort of platform error returned.
if motd.Errors == nil && motd.Message != "" {
c.Ui.Output(
c.Colorize().Color(motd.Message),
)
} else {
c.Ui.Output(
fmt.Sprintf(
c.Colorize().Color(strings.TrimSpace(`
[green][bold]Success![reset] [bold]Logged in to Terraform Cloud[reset]
`)),
) + "\n",
)
}
} else { // Terraform Enterprise
c.Ui.Output(
fmt.Sprintf(
c.Colorize().Color(strings.TrimSpace(`
[green][bold]Success![reset] [bold]Logged in to Terraform Enterprise (%s)[reset]
`)),
dispHostname,
) + "\n",
)
}
} else {
c.Ui.Output(
fmt.Sprintf(
c.Colorize().Color(strings.TrimSpace(`
[green][bold]Success![reset] [bold]Terraform has obtained and saved an API token.[reset]
The new API token will be used for any future Terraform command that must make
authenticated requests to %s.
`)),
dispHostname,
) + "\n",
)
dispHostname,
) + "\n",
)
}
return 0
}

@ -87,8 +87,7 @@ func TestLogin(t *testing.T) {
},
})
svcs.ForceHostServices(svchost.Hostname("tfe.acme.com"), map[string]interface{}{
// This represents a Terraform Enterprise instance which does not
// yet support the login API, but does support the TFE tokens API.
// This represents a Terraform Enterprise instance which supports the TFE tokens API.
"tfe.v2": ts.URL + "/api/v2",
"tfe.v2.1": ts.URL + "/api/v2",
"tfe.v2.2": ts.URL + "/api/v2",
@ -196,7 +195,7 @@ func TestLogin(t *testing.T) {
}
}))
t.Run("TFE host without login support", loginTestCase(func(t *testing.T, c *LoginCommand, ui *cli.MockUi) {
t.Run("TFE host", loginTestCase(func(t *testing.T, c *LoginCommand, ui *cli.MockUi) {
// Enter "yes" at the consent prompt, then paste a token with some
// accidental whitespace.
defer testInputMap(t, map[string]string{
@ -218,7 +217,7 @@ func TestLogin(t *testing.T) {
}
}))
t.Run("TFE host without login support, incorrectly pasted token", loginTestCase(func(t *testing.T, c *LoginCommand, ui *cli.MockUi) {
t.Run("TFE host, incorrectly pasted token", loginTestCase(func(t *testing.T, c *LoginCommand, ui *cli.MockUi) {
// Enter "yes" at the consent prompt, then paste an invalid token.
defer testInputMap(t, map[string]string{
"approve": "yes",

@ -250,8 +250,11 @@ func (m *Meta) StateOutPath() string {
// Colorize returns the colorization structure for a command.
func (m *Meta) Colorize() *colorstring.Colorize {
colors := colorstring.DefaultColors
colors["purple"] = "38;5;57"
return &colorstring.Colorize{
Colors: colorstring.DefaultColors,
Colors: colors,
Disable: !m.color,
Reset: true,
}

Loading…
Cancel
Save