github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/cmd/juju/cloud/addcredential.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  	"fmt"
     8  	"io"
     9  	"io/ioutil"
    10  	"strings"
    11  
    12  	"github.com/juju/cmd"
    13  	"github.com/juju/errors"
    14  	"github.com/juju/gnuflag"
    15  
    16  	jujucloud "github.com/juju/juju/cloud"
    17  	jujucmd "github.com/juju/juju/cmd"
    18  	"github.com/juju/juju/cmd/juju/common"
    19  	"github.com/juju/juju/cmd/juju/interact"
    20  	"github.com/juju/juju/environs"
    21  	"github.com/juju/juju/jujuclient"
    22  )
    23  
    24  var usageAddCredentialSummary = `
    25  Adds or replaces credentials for a cloud, stored locally on this client.`[1:]
    26  
    27  var usageAddCredentialDetails = `
    28  The user is prompted to add credentials interactively if a YAML-formatted
    29  credentials file is not specified. Here is a sample credentials file:
    30  
    31  credentials:
    32    aws:
    33      <credential name>:
    34        auth-type: access-key
    35        access-key: <key>
    36        secret-key: <key>
    37    azure:
    38      <credential name>:
    39        auth-type: service-principal-secret
    40        application-id: <uuid1>
    41        application-password: <password>
    42        subscription-id: <uuid2>
    43    lxd:
    44      <credential name>:
    45        auth-type: interactive
    46        trust-password: <password>
    47  
    48  A "credential name" is arbitrary and is used solely to represent a set of
    49  credentials, of which there may be multiple per cloud.
    50  The ` + "`--replace`" + ` option is required if credential information for the named
    51  cloud already exists locally. All such information will be overwritten.
    52  This command does not set default regions nor default credentials. Note
    53  that if only one credential name exists, it will become the effective
    54  default credential.
    55  For credentials which are already in use by tools other than Juju, ` + "`juju \nautoload-credentials`" + ` may be used.
    56  When Juju needs credentials for a cloud, i) if there are multiple
    57  available; ii) there's no set default; iii) and one is not specified ('--
    58  credential'), an error will be emitted.
    59  
    60  Examples:
    61      juju add-credential google
    62      juju add-credential aws -f ~/credentials.yaml
    63  
    64  See also: 
    65      credentials
    66      remove-credential
    67      set-default-credential
    68      autoload-credentials`
    69  
    70  type addCredentialCommand struct {
    71  	cmd.CommandBase
    72  	store           jujuclient.CredentialStore
    73  	cloudByNameFunc func(string) (*jujucloud.Cloud, error)
    74  
    75  	// Replace, if true, existing credential information is overwritten.
    76  	Replace bool
    77  
    78  	// CloudName is the name of the cloud for which we add credentials.
    79  	CloudName string
    80  
    81  	// CredentialsFile is the name of the credentials YAML file.
    82  	CredentialsFile string
    83  
    84  	cloud *jujucloud.Cloud
    85  }
    86  
    87  // NewAddCredentialCommand returns a command to add credential information.
    88  func NewAddCredentialCommand() cmd.Command {
    89  	return &addCredentialCommand{
    90  		store:           jujuclient.NewFileCredentialStore(),
    91  		cloudByNameFunc: jujucloud.CloudByName,
    92  	}
    93  }
    94  
    95  func (c *addCredentialCommand) Info() *cmd.Info {
    96  	return jujucmd.Info(&cmd.Info{
    97  		Name:    "add-credential",
    98  		Args:    "<cloud name>",
    99  		Purpose: usageAddCredentialSummary,
   100  		Doc:     usageAddCredentialDetails,
   101  	})
   102  }
   103  
   104  func (c *addCredentialCommand) SetFlags(f *gnuflag.FlagSet) {
   105  	c.CommandBase.SetFlags(f)
   106  	f.BoolVar(&c.Replace, "replace", false, "Overwrite existing credential information")
   107  	f.StringVar(&c.CredentialsFile, "f", "", "The YAML file containing credentials to add")
   108  }
   109  
   110  func (c *addCredentialCommand) Init(args []string) (err error) {
   111  	if len(args) < 1 {
   112  		return errors.New("Usage: juju add-credential <cloud-name> [-f <credentials.yaml>]")
   113  	}
   114  	c.CloudName = args[0]
   115  	return cmd.CheckEmpty(args[1:])
   116  }
   117  
   118  func (c *addCredentialCommand) Run(ctxt *cmd.Context) error {
   119  	// Check that the supplied cloud is valid.
   120  	var err error
   121  	if c.cloud, err = common.CloudOrProvider(c.CloudName, c.cloudByNameFunc); err != nil {
   122  		if !errors.IsNotFound(err) {
   123  			return err
   124  		}
   125  	}
   126  
   127  	credentialsProvider, err := environs.Provider(c.cloud.Type)
   128  	if err != nil {
   129  		return errors.Annotate(err, "getting provider for cloud")
   130  	}
   131  
   132  	if len(c.cloud.AuthTypes) == 0 {
   133  		return errors.Errorf("cloud %q does not require credentials", c.CloudName)
   134  	}
   135  
   136  	schemas := credentialsProvider.CredentialSchemas()
   137  	if c.CredentialsFile == "" {
   138  		return c.interactiveAddCredential(ctxt, schemas)
   139  	}
   140  
   141  	data, err := ioutil.ReadFile(c.CredentialsFile)
   142  	if err != nil {
   143  		return errors.Annotate(err, "reading credentials file")
   144  	}
   145  
   146  	specifiedCredentials, err := jujucloud.ParseCredentials(data)
   147  	if err != nil {
   148  		return errors.Annotate(err, "parsing credentials file")
   149  	}
   150  	credentials, ok := specifiedCredentials[c.CloudName]
   151  	if !ok {
   152  		return errors.Errorf("no credentials for cloud %s exist in file %s", c.CloudName, c.CredentialsFile)
   153  	}
   154  	existingCredentials, err := c.existingCredentialsForCloud()
   155  	if err != nil {
   156  		return errors.Trace(err)
   157  	}
   158  	// If there are *any* credentials already for the cloud, we'll ask for the --replace flag.
   159  	if !c.Replace && len(existingCredentials.AuthCredentials) > 0 && len(credentials.AuthCredentials) > 0 {
   160  		return errors.Errorf("local credentials for cloud %q already exist; use --replace to overwrite / merge", c.CloudName)
   161  	}
   162  
   163  	// We could get a duplicate "interactive" entry for the validAuthType() call,
   164  	// however it doesn't matter for the validation, so just add it.
   165  	authTypeNames := c.cloud.AuthTypes
   166  	if _, ok := schemas[jujucloud.InteractiveAuthType]; ok {
   167  		authTypeNames = append(authTypeNames, jujucloud.InteractiveAuthType)
   168  	}
   169  
   170  	validAuthType := func(authType jujucloud.AuthType) bool {
   171  		for _, authT := range authTypeNames {
   172  			if authT == authType {
   173  				return true
   174  			}
   175  		}
   176  		return false
   177  	}
   178  
   179  	var names []string
   180  	for name, cred := range credentials.AuthCredentials {
   181  		if !validAuthType(cred.AuthType()) {
   182  			return errors.Errorf("credential %q contains invalid auth type %q, valid auth types for cloud %q are %v", name, cred.AuthType(), c.CloudName, c.cloud.AuthTypes)
   183  		}
   184  
   185  		provider, err := environs.Provider(c.cloud.Type)
   186  		if err != nil {
   187  			return errors.Trace(err)
   188  		}
   189  
   190  		// When in non-interactive mode we still sometimes want to finalize a
   191  		// cloud, so that we can either validate the credentials work before a
   192  		// bootstrap happens or improve security models, where by we remove any
   193  		// shared/secret passwords (lxd remote security).
   194  		// This is optional and is backwards compatible with other providers.
   195  		if shouldFinalizeCredential(provider, cred) {
   196  			newCredential, err := c.finalizeProvider(ctxt, cred.AuthType(), cred.Attributes())
   197  			if err != nil {
   198  				return errors.Trace(err)
   199  			}
   200  			cred = *newCredential
   201  		}
   202  		existingCredentials.AuthCredentials[name] = cred
   203  		names = append(names, name)
   204  	}
   205  	err = c.store.UpdateCredential(c.CloudName, *existingCredentials)
   206  	if err != nil {
   207  		return err
   208  	}
   209  	verb := "added"
   210  	if c.Replace {
   211  		verb = "updated"
   212  	}
   213  	fmt.Fprintf(ctxt.Stdout, "Credentials %q %v for cloud %q.\n", strings.Join(names, ", "), verb, c.CloudName)
   214  	return nil
   215  }
   216  
   217  func (c *addCredentialCommand) existingCredentialsForCloud() (*jujucloud.CloudCredential, error) {
   218  	existingCredentials, err := c.store.CredentialForCloud(c.CloudName)
   219  	if err != nil && !errors.IsNotFound(err) {
   220  		return nil, errors.Annotate(err, "reading existing credentials for cloud")
   221  	}
   222  	if errors.IsNotFound(err) {
   223  		existingCredentials = &jujucloud.CloudCredential{
   224  			AuthCredentials: make(map[string]jujucloud.Credential),
   225  		}
   226  	}
   227  	return existingCredentials, nil
   228  }
   229  
   230  func (c *addCredentialCommand) interactiveAddCredential(ctxt *cmd.Context, schemas map[jujucloud.AuthType]jujucloud.CredentialSchema) error {
   231  	errout := interact.NewErrWriter(ctxt.Stdout)
   232  	pollster := interact.New(ctxt.Stdin, ctxt.Stdout, errout)
   233  
   234  	var err error
   235  	credentialName, err := pollster.Enter("credential name")
   236  	if err != nil {
   237  		return errors.Trace(err)
   238  	}
   239  
   240  	// Prompt to overwrite if needed.
   241  	existingCredentials, err := c.existingCredentialsForCloud()
   242  	if err != nil {
   243  		return errors.Trace(err)
   244  	}
   245  	verb := "added"
   246  	if _, ok := existingCredentials.AuthCredentials[credentialName]; ok {
   247  		fmt.Fprint(ctxt.Stdout, fmt.Sprintf("A credential %q already exists locally on this client.\n", credentialName))
   248  		overwrite, err := pollster.YN("Replace local credential", false)
   249  		if err != nil {
   250  			return errors.Trace(err)
   251  		}
   252  		if !overwrite {
   253  			return nil
   254  		}
   255  		verb = "updated"
   256  	}
   257  	authTypeNames := c.cloud.AuthTypes
   258  	// Check the credential schema for "interactive", add to list of
   259  	// possible authTypes for add-credential
   260  	if _, ok := schemas[jujucloud.InteractiveAuthType]; ok {
   261  		foundIt := false
   262  		for _, name := range authTypeNames {
   263  			if name == jujucloud.InteractiveAuthType {
   264  				foundIt = true
   265  			}
   266  		}
   267  		if !foundIt {
   268  			authTypeNames = append(authTypeNames, jujucloud.InteractiveAuthType)
   269  		}
   270  	}
   271  	authType, err := c.promptAuthType(pollster, authTypeNames, ctxt.Stdout)
   272  	if err != nil {
   273  		return errors.Trace(err)
   274  	}
   275  	schema, ok := schemas[authType]
   276  	if !ok {
   277  		return errors.NotSupportedf("auth type %q for cloud %q", authType, c.CloudName)
   278  	}
   279  
   280  	attrs, err := c.promptCredentialAttributes(pollster, authType, schema)
   281  	if err != nil {
   282  		return errors.Trace(err)
   283  	}
   284  
   285  	newCredential, err := c.finalizeProvider(ctxt, authType, attrs)
   286  	if err != nil {
   287  		return errors.Trace(err)
   288  	}
   289  
   290  	existingCredentials.AuthCredentials[credentialName] = *newCredential
   291  	err = c.store.UpdateCredential(c.CloudName, *existingCredentials)
   292  	if err != nil {
   293  		return errors.Trace(err)
   294  	}
   295  	fmt.Fprintf(ctxt.Stdout, "Credential %q %v locally for cloud %q.\n\n", credentialName, verb, c.CloudName)
   296  	return nil
   297  }
   298  
   299  func (c *addCredentialCommand) finalizeProvider(ctxt *cmd.Context, authType jujucloud.AuthType, attrs map[string]string) (*jujucloud.Credential, error) {
   300  	cloudEndpoint := c.cloud.Endpoint
   301  	cloudStorageEndpoint := c.cloud.StorageEndpoint
   302  	cloudIdentityEndpoint := c.cloud.IdentityEndpoint
   303  	if len(c.cloud.Regions) > 0 {
   304  		// NOTE(axw) we use the first region in the cloud,
   305  		// because this is all we need for Azure right now.
   306  		// Each region has the same endpoints, so it does
   307  		// not matter which one we use. If we expand
   308  		// credential generation to other providers, and
   309  		// they do have region-specific endpoints, then we
   310  		// should prompt the user for the region to use.
   311  		// That would be better left to the provider, though.
   312  		region := c.cloud.Regions[0]
   313  		cloudEndpoint = region.Endpoint
   314  		cloudStorageEndpoint = region.StorageEndpoint
   315  		cloudIdentityEndpoint = region.IdentityEndpoint
   316  	}
   317  
   318  	credentialsProvider, err := environs.Provider(c.cloud.Type)
   319  	if err != nil {
   320  		return nil, errors.Trace(err)
   321  	}
   322  	newCredential, err := credentialsProvider.FinalizeCredential(
   323  		ctxt, environs.FinalizeCredentialParams{
   324  			Credential:            jujucloud.NewCredential(authType, attrs),
   325  			CloudEndpoint:         cloudEndpoint,
   326  			CloudStorageEndpoint:  cloudStorageEndpoint,
   327  			CloudIdentityEndpoint: cloudIdentityEndpoint,
   328  		},
   329  	)
   330  	return newCredential, errors.Annotate(err, "finalizing credential")
   331  }
   332  
   333  func (c *addCredentialCommand) promptAuthType(p *interact.Pollster, authTypes []jujucloud.AuthType, out io.Writer) (jujucloud.AuthType, error) {
   334  	if len(authTypes) == 1 {
   335  		fmt.Fprintf(out, "Using auth-type %q.\n\n", authTypes[0])
   336  		return authTypes[0], nil
   337  	}
   338  	choices := make([]string, len(authTypes))
   339  	for i, a := range authTypes {
   340  		choices[i] = string(a)
   341  	}
   342  	// If "interactive" is a valid credential type, choose by default
   343  	// o.w. take the top of the slice
   344  	def := string(jujucloud.InteractiveAuthType)
   345  	if !strings.Contains(strings.Join(choices, " "), def) {
   346  		def = choices[0]
   347  	}
   348  	authType, err := p.Select(interact.List{
   349  		Singular: "auth type",
   350  		Plural:   "auth types",
   351  		Options:  choices,
   352  		Default:  def,
   353  	})
   354  	if err != nil {
   355  		return "", errors.Trace(err)
   356  	}
   357  	return jujucloud.AuthType(authType), nil
   358  }
   359  
   360  func (c *addCredentialCommand) promptCredentialAttributes(p *interact.Pollster, authType jujucloud.AuthType, schema jujucloud.CredentialSchema) (attributes map[string]string, err error) {
   361  	// Interactive add does not support adding multi-line values, which
   362  	// is what we typically get when the attribute can come from a file.
   363  	// For now we'll skip, and just get the user to enter the file path.
   364  	// TODO(wallyworld) - add support for multi-line entry
   365  
   366  	attrs := make(map[string]string)
   367  	for _, attr := range schema {
   368  		currentAttr := attr
   369  		value := ""
   370  		var err error
   371  
   372  		if currentAttr.FileAttr == "" {
   373  			value, err = c.promptFieldValue(p, currentAttr)
   374  			if err != nil {
   375  				return nil, err
   376  			}
   377  		} else {
   378  			currentAttr.Name = currentAttr.FileAttr
   379  			currentAttr.Hidden = false
   380  			currentAttr.FilePath = true
   381  			value, err = c.promptFieldValue(p, currentAttr)
   382  			if err != nil {
   383  				return nil, err
   384  			}
   385  		}
   386  		if value != "" {
   387  			attrs[currentAttr.Name] = value
   388  		}
   389  	}
   390  	return attrs, nil
   391  }
   392  
   393  func (c *addCredentialCommand) promptFieldValue(p *interact.Pollster, attr jujucloud.NamedCredentialAttr) (string, error) {
   394  	name := attr.Name
   395  
   396  	if len(attr.Options) > 0 {
   397  		options := make([]string, len(attr.Options))
   398  		for i, opt := range attr.Options {
   399  			options[i] = fmt.Sprintf("%v", opt)
   400  		}
   401  		return p.Select(interact.List{
   402  			Singular: name,
   403  			Plural:   name,
   404  			Options:  options,
   405  			Default:  options[0],
   406  		})
   407  	}
   408  
   409  	// We assume that Hidden, ExpandFilePath and FilePath are mutually
   410  	// exclusive here.
   411  	switch {
   412  	case attr.Hidden:
   413  		return p.EnterPassword(name)
   414  	case attr.ExpandFilePath:
   415  		return enterFile(name, attr.Description, p, true, attr.Optional)
   416  	case attr.FilePath:
   417  		return enterFile(name, attr.Description, p, false, attr.Optional)
   418  	case attr.Optional:
   419  		return p.EnterOptional(name)
   420  	default:
   421  		return p.Enter(name)
   422  	}
   423  }
   424  
   425  func enterFile(name, descr string, p *interact.Pollster, expanded, optional bool) (string, error) {
   426  	inputSuffix := ""
   427  	if optional {
   428  		inputSuffix += " (optional)"
   429  	}
   430  	input, err := p.EnterVerify(fmt.Sprintf("%s%s", descr, inputSuffix), func(s string) (ok bool, msg string, err error) {
   431  		if optional && s == "" {
   432  			return true, "", nil
   433  		}
   434  		_, err = jujucloud.ValidateFileAttrValue(s)
   435  		if err != nil {
   436  			return false, err.Error(), nil
   437  		}
   438  
   439  		return true, "", nil
   440  	})
   441  	if err != nil {
   442  		return "", errors.Trace(err)
   443  	}
   444  
   445  	// If it's optional and the input is empty, then return back out.
   446  	if optional && input == "" {
   447  		return "", nil
   448  	}
   449  
   450  	// We have to run this twice, since it has glommed together
   451  	// validation and normalization, and Pollster doesn't deal with the
   452  	// verification function modifying the value.
   453  	abs, err := jujucloud.ValidateFileAttrValue(input)
   454  	if err != nil {
   455  		return "", errors.Trace(err)
   456  	}
   457  
   458  	// If we don't need to expand the file path, exit out early.
   459  	if !expanded {
   460  		return abs, err
   461  	}
   462  
   463  	// Expand the file path to consume the contents
   464  	contents, err := ioutil.ReadFile(abs)
   465  	return string(contents), errors.Trace(err)
   466  }
   467  
   468  func shouldFinalizeCredential(provider environs.EnvironProvider, cred jujucloud.Credential) bool {
   469  	if finalizer, ok := provider.(environs.RequestFinalizeCredential); ok {
   470  		return finalizer.ShouldFinalizeCredential(cred)
   471  	}
   472  	return false
   473  }