Make cloud authentication more intuitive (#738)
The prior behavior with cloud authentication was a bit confusing when authenticating against anything but https://pulumi.com/. This change fixes a few aspects of this: * Improve error messages to differentiate between "authentication failed" and "you haven't logged into the target cloud URL." * Default to the cloud you're currently authenticated with, rather than unconditionally selecting https://pulumi.com/. This ensures $ pulumi login -c https://api.moolumi.io $ pulumi stack ls works, versus what was currently required $ pulumi login -c https://api.moolumi.io $ pulumi stack ls -c https://api.moolumi.io with confusing error messages if you forgot the second -c. * To do this, our default cloud logic changes to 1) Prefer the explicit -c if supplied; 2) Otherwise, pick the "currently authenticated" cloud; this is the last cloud to have been targeted with pulumi login, or otherwise the single cloud in the list if there is only one; 3) https://pulumi.com/ otherwise.
This commit is contained in:
parent
bd2baa5091
commit
6dc16a5548
6 changed files with 67 additions and 22 deletions
|
@ -22,7 +22,7 @@ func newLogoutCmd() *cobra.Command {
|
||||||
Run: cmdutil.RunFunc(func(cmd *cobra.Command, args []string) error {
|
Run: cmdutil.RunFunc(func(cmd *cobra.Command, args []string) error {
|
||||||
// If --all is passed, log out of all clouds.
|
// If --all is passed, log out of all clouds.
|
||||||
if all {
|
if all {
|
||||||
bes, err := cloud.CurrentBackends(cmdutil.Diag())
|
bes, _, err := cloud.CurrentBackends(cmdutil.Diag())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "could not read list of current clouds")
|
return errors.Wrap(err, "could not read list of current clouds")
|
||||||
}
|
}
|
||||||
|
|
|
@ -50,13 +50,17 @@ func NewPulumiCmd() *cobra.Command {
|
||||||
cmd.SetHelpFunc(func(cmd *cobra.Command, args []string) {
|
cmd.SetHelpFunc(func(cmd *cobra.Command, args []string) {
|
||||||
defaultHelp(cmd, args)
|
defaultHelp(cmd, args)
|
||||||
|
|
||||||
loggedInto, logErr := cloud.CurrentBackends(cmdutil.Diag())
|
loggedInto, current, logErr := cloud.CurrentBackends(cmdutil.Diag())
|
||||||
contract.IgnoreError(logErr) // we want to make progress anyway.
|
contract.IgnoreError(logErr) // we want to make progress anyway.
|
||||||
if len(loggedInto) > 0 {
|
if len(loggedInto) > 0 {
|
||||||
fmt.Printf("\n")
|
fmt.Printf("\n")
|
||||||
fmt.Printf("Currently logged into the Pulumi Cloud ☁️\n")
|
fmt.Printf("Currently logged into the Pulumi Cloud ☁️\n")
|
||||||
for _, be := range loggedInto {
|
for _, be := range loggedInto {
|
||||||
fmt.Printf(" %s\n", be.Name())
|
var marker string
|
||||||
|
if be.Name() == current {
|
||||||
|
marker = "*"
|
||||||
|
}
|
||||||
|
fmt.Printf(" %s%s\n", be.Name(), marker)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -29,7 +29,7 @@ func allBackends() ([]backend.Backend, bool) {
|
||||||
// in addition to all of those cloud backends we are currently logged into.
|
// in addition to all of those cloud backends we are currently logged into.
|
||||||
d := cmdutil.Diag()
|
d := cmdutil.Diag()
|
||||||
backends := []backend.Backend{local.New(d)}
|
backends := []backend.Backend{local.New(d)}
|
||||||
cloudBackends, err := cloud.CurrentBackends(d)
|
cloudBackends, _, err := cloud.CurrentBackends(d)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Print the error, but keep going so that the local operations still occur.
|
// Print the error, but keep going so that the local operations still occur.
|
||||||
_, fmterr := fmt.Fprintf(os.Stderr, "error: could not obtain current cloud backends: %v", err)
|
_, fmterr := fmt.Fprintf(os.Stderr, "error: could not obtain current cloud backends: %v", err)
|
||||||
|
|
|
@ -30,18 +30,38 @@ const (
|
||||||
)
|
)
|
||||||
|
|
||||||
// DefaultURL returns the default cloud URL. This may be overridden using the PULUMI_API environment
|
// DefaultURL returns the default cloud URL. This may be overridden using the PULUMI_API environment
|
||||||
// variable. If no override is found, the default is the pulumi.com cloud.
|
// variable. If no override is found, and we are authenticated with only one cloud, choose that. Otherwise,
|
||||||
|
// we will default to the https://api.pulumi.com/ endpoint.
|
||||||
func DefaultURL() string {
|
func DefaultURL() string {
|
||||||
cloudURL := os.Getenv(defaultURLEnvVar)
|
return ValueOrDefaultURL("")
|
||||||
return ValueOrDefaultURL(cloudURL)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ValueOrDefaultURL returns the value if specified, or the default cloud URL otherwise.
|
// ValueOrDefaultURL returns the value if specified, or the default cloud URL otherwise.
|
||||||
func ValueOrDefaultURL(cloudURL string) string {
|
func ValueOrDefaultURL(cloudURL string) string {
|
||||||
if cloudURL == "" {
|
// If we have a cloud URL, just return it.
|
||||||
cloudURL = defaultURL
|
if cloudURL != "" {
|
||||||
}
|
|
||||||
return cloudURL
|
return cloudURL
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, respect the PULUMI_API override.
|
||||||
|
if cloudURL := os.Getenv(defaultURLEnvVar); cloudURL != "" {
|
||||||
|
return cloudURL
|
||||||
|
}
|
||||||
|
|
||||||
|
// If that didn't work, see if we're authenticated with any clouds.
|
||||||
|
urls, current, err := CurrentBackendURLs()
|
||||||
|
if err == nil {
|
||||||
|
if current != "" {
|
||||||
|
// If there's a current cloud selected, return that.
|
||||||
|
return current
|
||||||
|
} else if len(urls) == 1 {
|
||||||
|
// Else, if we're authenticated with a single cloud, use that.
|
||||||
|
return urls[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If none of those led to a cloud URL, simply return the default.
|
||||||
|
return defaultURL
|
||||||
}
|
}
|
||||||
|
|
||||||
// cloudProjectIdentifier is the set of data needed to identify a Pulumi Cloud project. This the
|
// cloudProjectIdentifier is the set of data needed to identify a Pulumi Cloud project. This the
|
||||||
|
@ -93,6 +113,8 @@ func pulumiRESTCall(cloudAPI, method, path string, reqObj interface{}, respObj i
|
||||||
token, err := workspace.GetAccessToken(cloudAPI)
|
token, err := workspace.GetAccessToken(cloudAPI)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("getting stored credentials: %v", err)
|
return fmt.Errorf("getting stored credentials: %v", err)
|
||||||
|
} else if token == "" {
|
||||||
|
return fmt.Errorf("not yet authenticated with %s; please 'pulumi login' first", cloudAPI)
|
||||||
}
|
}
|
||||||
return pulumiRESTCallWithAccessToken(cloudAPI, method, path, reqObj, respObj, token)
|
return pulumiRESTCallWithAccessToken(cloudAPI, method, path, reqObj, respObj, token)
|
||||||
}
|
}
|
||||||
|
|
|
@ -484,7 +484,7 @@ func Login(cloudURL string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save them.
|
// Save them.
|
||||||
return workspace.StoreAccessToken(cloudURL, accessToken)
|
return workspace.StoreAccessToken(cloudURL, accessToken, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
// isValidAccessToken tries to use the provided Pulumi access token and returns if it is accepted
|
// isValidAccessToken tries to use the provided Pulumi access token and returns if it is accepted
|
||||||
|
@ -507,25 +507,37 @@ func Logout(cloudURL string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// CurrentBackends returns a list of the cloud backends the user is currently logged into.
|
// CurrentBackends returns a list of the cloud backends the user is currently logged into.
|
||||||
func CurrentBackends(d diag.Sink) ([]Backend, error) {
|
func CurrentBackends(d diag.Sink) ([]Backend, string, error) {
|
||||||
creds, err := workspace.GetStoredCredentials()
|
urls, current, err := CurrentBackendURLs()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
var backends []Backend
|
var backends []Backend
|
||||||
if creds.AccessTokens != nil {
|
for _, url := range urls {
|
||||||
// Sort the URLs so that we return them in a deterministic order.
|
backends = append(backends, New(d, url))
|
||||||
|
}
|
||||||
|
return backends, current, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CurrentBackendURLs returns a list of the cloud backend URLS the user is currently logged into.
|
||||||
|
func CurrentBackendURLs() ([]string, string, error) {
|
||||||
|
creds, err := workspace.GetStoredCredentials()
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
var current string
|
||||||
var cloudURLs []string
|
var cloudURLs []string
|
||||||
|
if creds.AccessTokens != nil {
|
||||||
|
current = creds.Current
|
||||||
|
|
||||||
|
// Sort the URLs so that we return them in a deterministic order.
|
||||||
for url := range creds.AccessTokens {
|
for url := range creds.AccessTokens {
|
||||||
cloudURLs = append(cloudURLs, url)
|
cloudURLs = append(cloudURLs, url)
|
||||||
}
|
}
|
||||||
sort.Strings(cloudURLs)
|
sort.Strings(cloudURLs)
|
||||||
|
|
||||||
for _, url := range cloudURLs {
|
|
||||||
backends = append(backends, New(d, url))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return backends, nil
|
return cloudURLs, current, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,11 +39,14 @@ func DeleteAccessToken(key string) error {
|
||||||
if creds.AccessTokens != nil {
|
if creds.AccessTokens != nil {
|
||||||
delete(creds.AccessTokens, key)
|
delete(creds.AccessTokens, key)
|
||||||
}
|
}
|
||||||
|
if creds.Current == key {
|
||||||
|
creds.Current = ""
|
||||||
|
}
|
||||||
return StoreCredentials(creds)
|
return StoreCredentials(creds)
|
||||||
}
|
}
|
||||||
|
|
||||||
// StoreAccessToken saves the given access token underneath the given key.
|
// StoreAccessToken saves the given access token underneath the given key.
|
||||||
func StoreAccessToken(key string, token string) error {
|
func StoreAccessToken(key string, token string, current bool) error {
|
||||||
creds, err := GetStoredCredentials()
|
creds, err := GetStoredCredentials()
|
||||||
if err != nil && !os.IsNotExist(err) {
|
if err != nil && !os.IsNotExist(err) {
|
||||||
return err
|
return err
|
||||||
|
@ -52,13 +55,17 @@ func StoreAccessToken(key string, token string) error {
|
||||||
creds.AccessTokens = make(map[string]string)
|
creds.AccessTokens = make(map[string]string)
|
||||||
}
|
}
|
||||||
creds.AccessTokens[key] = token
|
creds.AccessTokens[key] = token
|
||||||
|
if current {
|
||||||
|
creds.Current = key
|
||||||
|
}
|
||||||
return StoreCredentials(creds)
|
return StoreCredentials(creds)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Credentials hold the information necessary for authenticating Pulumi Cloud API requests. It contains
|
// Credentials hold the information necessary for authenticating Pulumi Cloud API requests. It contains
|
||||||
// a map from the cloud API URL to the associated access token.
|
// a map from the cloud API URL to the associated access token.
|
||||||
type Credentials struct {
|
type Credentials struct {
|
||||||
AccessTokens map[string]string `json:"accessTokens"`
|
Current string `json:"current,omitempty"` // the currently selected key.
|
||||||
|
AccessTokens map[string]string `json:"accessTokens,omitempty"` // a map of arbitrary key strings to tokens.
|
||||||
}
|
}
|
||||||
|
|
||||||
// getCredsFilePath returns the path to the Pulumi credentials file on disk, regardless of
|
// getCredsFilePath returns the path to the Pulumi credentials file on disk, regardless of
|
||||||
|
|
Loading…
Reference in a new issue