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