github.com/axw/juju@v0.0.0-20161005053422-4bd6544d08d4/cmd/juju/cloud/detectcredentials.go (about)

     1  // Copyright 2016 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package cloud
     5  
     6  import (
     7  	"bufio"
     8  	"fmt"
     9  	"io"
    10  	"sort"
    11  	"strconv"
    12  	"strings"
    13  
    14  	"github.com/juju/cmd"
    15  	"github.com/juju/errors"
    16  	"github.com/juju/utils/set"
    17  
    18  	jujucloud "github.com/juju/juju/cloud"
    19  	"github.com/juju/juju/cmd/juju/common"
    20  	"github.com/juju/juju/environs"
    21  	"github.com/juju/juju/jujuclient"
    22  )
    23  
    24  type detectCredentialsCommand struct {
    25  	cmd.CommandBase
    26  	out cmd.Output
    27  
    28  	store jujuclient.CredentialStore
    29  
    30  	// registeredProvidersFunc is set by tests to return all registered environ providers
    31  	registeredProvidersFunc func() []string
    32  
    33  	// allCloudsFunc is set by tests to return all public and personal clouds
    34  	allCloudsFunc func() (map[string]jujucloud.Cloud, error)
    35  
    36  	// cloudByNameFunc is set by tests to return a named cloud.
    37  	cloudByNameFunc func(string) (*jujucloud.Cloud, error)
    38  }
    39  
    40  const detectCredentialsSummary = `Attempts to automatically add or replace credentials for a cloud.`
    41  
    42  var detectCredentialsDoc = `
    43  Well known locations for specific clouds are searched and any found
    44  information is presented interactively to the user.
    45  An alternative to this command is ` + "`juju add-credential`" + `.
    46  Below are the cloud types for which credentials may be autoloaded,
    47  including the locations searched.
    48  
    49  EC2
    50    Credentials and regions:
    51      1. On Linux, $HOME/.aws/credentials and $HOME/.aws/config
    52      2. Environment variables AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY
    53  
    54  GCE
    55    Credentials:
    56      1. A JSON file whose path is specified by the
    57         GOOGLE_APPLICATION_CREDENTIALS environment variable
    58      2. On Linux, $HOME/.config/gcloud/application_default_credentials.json
    59         Default region is specified by the CLOUDSDK_COMPUTE_REGION environment
    60         variable.
    61      3. On Windows, %APPDATA%\gcloud\application_default_credentials.json
    62  
    63  OpenStack
    64    Credentials:
    65      1. On Linux, $HOME/.novarc
    66      2. Environment variables OS_USERNAME, OS_PASSWORD, OS_TENANT_NAME,
    67         OS_DOMAIN_NAME
    68  
    69  Example:
    70      juju autoload-credentials
    71     
    72  See also:
    73      list-credentials
    74      remove-credential
    75      set-default-credential
    76      add-credential
    77  `[1:]
    78  
    79  // NewDetectCredentialsCommand returns a command to add credential information to credentials.yaml.
    80  func NewDetectCredentialsCommand() cmd.Command {
    81  	c := &detectCredentialsCommand{
    82  		store: jujuclient.NewFileCredentialStore(),
    83  		registeredProvidersFunc: environs.RegisteredProviders,
    84  		cloudByNameFunc:         jujucloud.CloudByName,
    85  	}
    86  	c.allCloudsFunc = func() (map[string]jujucloud.Cloud, error) {
    87  		return c.allClouds()
    88  	}
    89  	return c
    90  }
    91  
    92  func (c *detectCredentialsCommand) Info() *cmd.Info {
    93  	return &cmd.Info{
    94  		Name:    "autoload-credentials",
    95  		Purpose: detectCredentialsSummary,
    96  		Doc:     detectCredentialsDoc,
    97  	}
    98  }
    99  
   100  type discoveredCredential struct {
   101  	defaultCloudName string
   102  	cloudType        string
   103  	region           string
   104  	credentialName   string
   105  	credential       jujucloud.Credential
   106  	isNew            bool
   107  }
   108  
   109  func (c *detectCredentialsCommand) allClouds() (map[string]jujucloud.Cloud, error) {
   110  	clouds, _, err := jujucloud.PublicCloudMetadata(jujucloud.JujuPublicCloudsPath())
   111  	if err != nil {
   112  		return nil, err
   113  	}
   114  	personalClouds, err := jujucloud.PersonalCloudMetadata()
   115  	if err != nil {
   116  		return nil, err
   117  	}
   118  	for k, v := range personalClouds {
   119  		clouds[k] = v
   120  	}
   121  	return clouds, nil
   122  }
   123  
   124  func (c *detectCredentialsCommand) Run(ctxt *cmd.Context) error {
   125  	fmt.Fprintln(ctxt.Stderr, "\nLooking for cloud and credential information locally...")
   126  
   127  	clouds, err := c.allCloudsFunc()
   128  	if err != nil {
   129  		return errors.Trace(err)
   130  	}
   131  
   132  	// Let's ensure a consistent order.
   133  	var sortedCloudNames []string
   134  	for cloudName := range clouds {
   135  		sortedCloudNames = append(sortedCloudNames, cloudName)
   136  	}
   137  	sort.Strings(sortedCloudNames)
   138  
   139  	// The default cloud name for each provider type is the
   140  	// first cloud in the sorted list.
   141  	defaultCloudNames := make(map[string]string)
   142  	for _, cloudName := range sortedCloudNames {
   143  		cloud := clouds[cloudName]
   144  		if _, ok := defaultCloudNames[cloud.Type]; ok {
   145  			continue
   146  		}
   147  		defaultCloudNames[cloud.Type] = cloudName
   148  	}
   149  
   150  	providerNames := c.registeredProvidersFunc()
   151  	sort.Strings(providerNames)
   152  
   153  	var discovered []discoveredCredential
   154  	discoveredLabels := set.NewStrings()
   155  	for _, providerName := range providerNames {
   156  		provider, err := environs.Provider(providerName)
   157  		if err != nil {
   158  			// Should never happen but it will on go 1.2
   159  			// because lxd provider is not built.
   160  			logger.Errorf("provider %q not available on this platform", providerName)
   161  			continue
   162  		}
   163  		if detectCredentials, ok := provider.(environs.ProviderCredentials); ok {
   164  			detected, err := detectCredentials.DetectCredentials()
   165  			if err != nil && !errors.IsNotFound(err) {
   166  				logger.Errorf("could not detect credentials for provider %q: %v", providerName, err)
   167  				continue
   168  			}
   169  			if errors.IsNotFound(err) || len(detected.AuthCredentials) == 0 {
   170  				continue
   171  			}
   172  
   173  			// For each credential, construct meta info for which cloud it may pertain to etc.
   174  			for credName, newCred := range detected.AuthCredentials {
   175  				if credName == "" {
   176  					logger.Debugf("ignoring unnamed credential for provider %s", providerName)
   177  					continue
   178  				}
   179  				// Ignore empty credentials.
   180  				if newCred.AuthType() == jujucloud.EmptyAuthType {
   181  					continue
   182  				}
   183  				// Check that another provider hasn't loaded the same credential.
   184  				if discoveredLabels.Contains(newCred.Label) {
   185  					continue
   186  				}
   187  				discoveredLabels.Add(newCred.Label)
   188  
   189  				credInfo := discoveredCredential{
   190  					cloudType:      providerName,
   191  					credentialName: credName,
   192  					credential:     newCred,
   193  				}
   194  
   195  				// Fill in the default cloud and other meta information.
   196  				defaultCloud, existingDefaultRegion, isNew, err := c.guessCloudInfo(sortedCloudNames, clouds, providerName, credName)
   197  				if err != nil {
   198  					return errors.Trace(err)
   199  				}
   200  				if defaultCloud == "" {
   201  					defaultCloud = defaultCloudNames[providerName]
   202  				}
   203  				credInfo.defaultCloudName = defaultCloud
   204  				if isNew {
   205  					credInfo.defaultCloudName = defaultCloudNames[providerName]
   206  				}
   207  				if (isNew || existingDefaultRegion == "") && detected.DefaultRegion != "" {
   208  					credInfo.region = detected.DefaultRegion
   209  				}
   210  				credInfo.isNew = isNew
   211  				discovered = append(discovered, credInfo)
   212  			}
   213  		}
   214  	}
   215  	if len(discovered) == 0 {
   216  		fmt.Fprintln(ctxt.Stderr, "No cloud credentials found.")
   217  		return nil
   218  	}
   219  	return c.interactiveCredentialsUpdate(ctxt, discovered)
   220  }
   221  
   222  // guessCloudInfo looks at all the compatible clouds for the provider name and
   223  // looks to see whether the credential name exists already.
   224  // The first match allows the default cloud and region to be set. The default
   225  // cloud is used when prompting to save a credential. The sorted cloud names
   226  // ensures that "aws" is preferred over "aws-china".
   227  func (c *detectCredentialsCommand) guessCloudInfo(
   228  	sortedCloudNames []string,
   229  	clouds map[string]jujucloud.Cloud,
   230  	providerName, credName string,
   231  ) (defaultCloud, defaultRegion string, isNew bool, _ error) {
   232  	isNew = true
   233  	for _, cloudName := range sortedCloudNames {
   234  		cloud := clouds[cloudName]
   235  		if cloud.Type != providerName {
   236  			continue
   237  		}
   238  		credentials, err := c.store.CredentialForCloud(cloudName)
   239  		if err != nil && !errors.IsNotFound(err) {
   240  			return "", "", false, errors.Trace(err)
   241  		}
   242  		if err != nil {
   243  			// None found.
   244  			continue
   245  		}
   246  		existingCredNames := set.NewStrings()
   247  		for name := range credentials.AuthCredentials {
   248  			existingCredNames.Add(name)
   249  		}
   250  		isNew = !existingCredNames.Contains(credName)
   251  		if defaultRegion == "" && credentials.DefaultRegion != "" {
   252  			defaultRegion = credentials.DefaultRegion
   253  		}
   254  		if defaultCloud == "" {
   255  			defaultCloud = cloudName
   256  		}
   257  	}
   258  	return defaultCloud, defaultRegion, isNew, nil
   259  }
   260  
   261  // interactiveCredentialsUpdate prints a list of the discovered credentials
   262  // and prompts the user to update their local credentials.
   263  func (c *detectCredentialsCommand) interactiveCredentialsUpdate(ctxt *cmd.Context, discovered []discoveredCredential) error {
   264  	for {
   265  		// Prompt for a credential to save.
   266  		c.printCredentialOptions(ctxt, discovered)
   267  		var input string
   268  		for {
   269  			var err error
   270  			input, err = c.promptCredentialNumber(ctxt.Stderr, ctxt.Stdin)
   271  			if err != nil {
   272  				return errors.Trace(err)
   273  			}
   274  			if strings.ToLower(input) == "q" {
   275  				return nil
   276  			}
   277  			if input != "" {
   278  				break
   279  			}
   280  		}
   281  
   282  		// Check the entered number.
   283  		num, err := strconv.Atoi(input)
   284  		if err != nil || num < 1 || num > len(discovered) {
   285  			fmt.Fprintf(ctxt.Stderr, "Invalid choice, enter a number between 1 and %v\n", len(discovered))
   286  			continue
   287  		}
   288  		cred := discovered[num-1]
   289  		// Prompt for the cloud for which to save the credential.
   290  		cloudName, err := c.promptCloudName(ctxt.Stderr, ctxt.Stdin, cred.defaultCloudName, cred.cloudType)
   291  		if err != nil {
   292  			fmt.Fprintln(ctxt.Stderr, err.Error())
   293  			continue
   294  		}
   295  		if cloudName == "" {
   296  			fmt.Fprintln(ctxt.Stderr, "No cloud name entered.")
   297  			continue
   298  		}
   299  
   300  		// Reading existing info so we can apply updated values.
   301  		existing, err := c.store.CredentialForCloud(cloudName)
   302  		if err != nil && !errors.IsNotFound(err) {
   303  			fmt.Fprintf(ctxt.Stderr, "error reading credential file: %v\n", err)
   304  			continue
   305  		}
   306  		if errors.IsNotFound(err) {
   307  			existing = &jujucloud.CloudCredential{
   308  				AuthCredentials: make(map[string]jujucloud.Credential),
   309  			}
   310  		}
   311  		if cred.region != "" {
   312  			existing.DefaultRegion = cred.region
   313  		}
   314  		existing.AuthCredentials[cred.credentialName] = cred.credential
   315  		if err := c.store.UpdateCredential(cloudName, *existing); err != nil {
   316  			fmt.Fprintf(ctxt.Stderr, "error saving credential: %v\n", err)
   317  		} else {
   318  			// Update so we display correctly next time list is printed.
   319  			cred.isNew = false
   320  			discovered[num-1] = cred
   321  			fmt.Fprintf(ctxt.Stderr, "Saved %s to cloud %s\n", cred.credential.Label, cloudName)
   322  		}
   323  	}
   324  }
   325  
   326  func (c *detectCredentialsCommand) printCredentialOptions(ctxt *cmd.Context, discovered []discoveredCredential) {
   327  	fmt.Fprintln(ctxt.Stderr)
   328  	for i, cred := range discovered {
   329  		suffixText := " (existing, will overwrite)"
   330  		if cred.isNew {
   331  			suffixText = " (new)"
   332  		}
   333  		fmt.Fprintf(ctxt.Stderr, "%d. %s%s\n", i+1, cred.credential.Label, suffixText)
   334  	}
   335  }
   336  
   337  func (c *detectCredentialsCommand) promptCredentialNumber(out io.Writer, in io.Reader) (string, error) {
   338  	fmt.Fprint(out, "Select a credential to save by number, or type Q to quit: ")
   339  	defer out.Write([]byte{'\n'})
   340  	input, err := readLine(in)
   341  	if err != nil {
   342  		return "", errors.Trace(err)
   343  	}
   344  	return strings.TrimSpace(input), nil
   345  }
   346  
   347  func (c *detectCredentialsCommand) promptCloudName(out io.Writer, in io.Reader, defaultCloudName, cloudType string) (string, error) {
   348  	text := fmt.Sprintf(`Select the cloud it belongs to, or type Q to quit [%s]: `, defaultCloudName)
   349  	fmt.Fprint(out, text)
   350  	defer out.Write([]byte{'\n'})
   351  	input, err := readLine(in)
   352  	if err != nil {
   353  		return "", errors.Trace(err)
   354  	}
   355  	cloudName := strings.TrimSpace(input)
   356  	if strings.ToLower(cloudName) == "q" {
   357  		return "", nil
   358  	}
   359  	if cloudName == "" {
   360  		return defaultCloudName, nil
   361  	}
   362  	cloud, err := common.CloudOrProvider(cloudName, c.cloudByNameFunc)
   363  	if err != nil {
   364  		return "", err
   365  	}
   366  	if cloud.Type != cloudType {
   367  		return "", errors.Errorf("chosen credentials not compatible with a %s cloud", cloud.Type)
   368  	}
   369  	return cloudName, nil
   370  }
   371  
   372  func readLine(stdin io.Reader) (string, error) {
   373  	// Read one byte at a time to avoid reading beyond the delimiter.
   374  	line, err := bufio.NewReader(byteAtATimeReader{stdin}).ReadString('\n')
   375  	if err != nil {
   376  		return "", errors.Trace(err)
   377  	}
   378  	return line[:len(line)-1], nil
   379  }
   380  
   381  type byteAtATimeReader struct {
   382  	io.Reader
   383  }
   384  
   385  func (r byteAtATimeReader) Read(out []byte) (int, error) {
   386  	return r.Reader.Read(out[:1])
   387  }