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