github.com/axw/juju@v0.0.0-20161005053422-4bd6544d08d4/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  	"os"
    11  	"strings"
    12  
    13  	"github.com/juju/cmd"
    14  	"github.com/juju/errors"
    15  	"github.com/juju/gnuflag"
    16  	"golang.org/x/crypto/ssh/terminal"
    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  var usageAddCredentialSummary = `
    25  Adds or replaces credentials for a cloud.`[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: userpass
    40        application-id: <uuid1>
    41        application-password: <password>
    42        subscription-id: <uuid2>
    43        tenant-id: <uuid3>
    44  
    45  A "credential name" is arbitrary and is used solely to represent a set of
    46  credentials, of which there may be multiple per cloud.
    47  The ` + "`--replace`" + ` option is required if credential information for the named
    48  cloud already exists. All such information will be overwritten.
    49  This command does not set default regions nor default credentials. Note
    50  that if only one credential name exists, it will become the effective
    51  default credential.
    52  For credentials which are already in use by tools other than Juju, ` + "`juju \nautoload-credentials`" + ` may be used.
    53  When Juju needs credentials for a cloud, i) if there are multiple
    54  available; ii) there's no set default; iii) and one is not specified ('--
    55  credential'), an error will be emitted.
    56  
    57  Examples:
    58      juju add-credential google
    59      juju add-credential aws -f ~/credentials.yaml
    60  
    61  See also: 
    62      credentials
    63      remove-credential
    64      set-default-credential
    65      autoload-credentials`
    66  
    67  type addCredentialCommand struct {
    68  	cmd.CommandBase
    69  	store           jujuclient.CredentialStore
    70  	cloudByNameFunc func(string) (*jujucloud.Cloud, error)
    71  
    72  	// Replace, if true, existing credential information is overwritten.
    73  	Replace bool
    74  
    75  	// CloudName is the name of the cloud for which we add credentials.
    76  	CloudName string
    77  
    78  	// CredentialsFile is the name of the credentials YAML file.
    79  	CredentialsFile string
    80  
    81  	cloud *jujucloud.Cloud
    82  }
    83  
    84  // NewAddCredentialCommand returns a command to add credential information.
    85  func NewAddCredentialCommand() cmd.Command {
    86  	return &addCredentialCommand{
    87  		store:           jujuclient.NewFileCredentialStore(),
    88  		cloudByNameFunc: jujucloud.CloudByName,
    89  	}
    90  }
    91  
    92  func (c *addCredentialCommand) Info() *cmd.Info {
    93  	return &cmd.Info{
    94  		Name:    "add-credential",
    95  		Args:    "<cloud name>",
    96  		Purpose: usageAddCredentialSummary,
    97  		Doc:     usageAddCredentialDetails,
    98  	}
    99  }
   100  
   101  func (c *addCredentialCommand) SetFlags(f *gnuflag.FlagSet) {
   102  	c.CommandBase.SetFlags(f)
   103  	f.BoolVar(&c.Replace, "replace", false, "Overwrite existing credential information")
   104  	f.StringVar(&c.CredentialsFile, "f", "", "The YAML file containing credentials to add")
   105  }
   106  
   107  func (c *addCredentialCommand) Init(args []string) (err error) {
   108  	if len(args) < 1 {
   109  		return errors.New("Usage: juju add-credential <cloud-name> [-f <credentials.yaml>]")
   110  	}
   111  	c.CloudName = args[0]
   112  	return cmd.CheckEmpty(args[1:])
   113  }
   114  
   115  func (c *addCredentialCommand) Run(ctxt *cmd.Context) error {
   116  	// Check that the supplied cloud is valid.
   117  	var err error
   118  	if c.cloud, err = common.CloudOrProvider(c.CloudName, c.cloudByNameFunc); err != nil {
   119  		if !errors.IsNotFound(err) {
   120  			return err
   121  		}
   122  	}
   123  	if len(c.cloud.AuthTypes) == 0 {
   124  		return errors.Errorf("cloud %q does not require credentials", c.CloudName)
   125  	}
   126  
   127  	if c.CredentialsFile == "" {
   128  		credentialsProvider, err := environs.Provider(c.cloud.Type)
   129  		if err != nil {
   130  			return errors.Annotate(err, "getting provider for cloud")
   131  		}
   132  		return c.interactiveAddCredential(ctxt, credentialsProvider.CredentialSchemas())
   133  	}
   134  	data, err := ioutil.ReadFile(c.CredentialsFile)
   135  	if err != nil {
   136  		return errors.Annotate(err, "reading credentials file")
   137  	}
   138  
   139  	specifiedCredentials, err := jujucloud.ParseCredentials(data)
   140  	if err != nil {
   141  		return errors.Annotate(err, "parsing credentials file")
   142  	}
   143  	credentials, ok := specifiedCredentials[c.CloudName]
   144  	if !ok {
   145  		return errors.Errorf("no credentials for cloud %s exist in file %s", c.CloudName, c.CredentialsFile)
   146  	}
   147  	existingCredentials, err := c.existingCredentialsForCloud()
   148  	if err != nil {
   149  		return errors.Trace(err)
   150  	}
   151  	// If there are *any* credentials already for the cloud, we'll ask for the --replace flag.
   152  	if !c.Replace && len(existingCredentials.AuthCredentials) > 0 && len(credentials.AuthCredentials) > 0 {
   153  		return errors.Errorf("credentials for cloud %s already exist; use --replace to overwrite / merge", c.CloudName)
   154  	}
   155  	for name, cred := range credentials.AuthCredentials {
   156  		existingCredentials.AuthCredentials[name] = cred
   157  	}
   158  	err = c.store.UpdateCredential(c.CloudName, *existingCredentials)
   159  	if err != nil {
   160  		return err
   161  	}
   162  	fmt.Fprintf(ctxt.Stdout, "Credentials updated for cloud %q.\n", c.CloudName)
   163  	return nil
   164  }
   165  
   166  func (c *addCredentialCommand) existingCredentialsForCloud() (*jujucloud.CloudCredential, error) {
   167  	existingCredentials, err := c.store.CredentialForCloud(c.CloudName)
   168  	if err != nil && !errors.IsNotFound(err) {
   169  		return nil, errors.Annotate(err, "reading existing credentials for cloud")
   170  	}
   171  	if errors.IsNotFound(err) {
   172  		existingCredentials = &jujucloud.CloudCredential{
   173  			AuthCredentials: make(map[string]jujucloud.Credential),
   174  		}
   175  	}
   176  	return existingCredentials, nil
   177  }
   178  
   179  func (c *addCredentialCommand) interactiveAddCredential(ctxt *cmd.Context, schemas map[jujucloud.AuthType]jujucloud.CredentialSchema) error {
   180  	var err error
   181  	credentialName, err := c.promptCredentialName(ctxt.Stderr, ctxt.Stdin)
   182  	if err != nil {
   183  		return errors.Trace(err)
   184  	}
   185  	if credentialName == "" {
   186  		fmt.Fprintln(ctxt.Stderr, "Credentials entry aborted.")
   187  		return nil
   188  	}
   189  
   190  	// Prompt to overwrite if needed.
   191  	existingCredentials, err := c.existingCredentialsForCloud()
   192  	if err != nil {
   193  		return errors.Trace(err)
   194  	}
   195  	if _, ok := existingCredentials.AuthCredentials[credentialName]; ok {
   196  		overwrite, err := c.promptReplace(ctxt.Stderr, ctxt.Stdin)
   197  		if err != nil {
   198  			return errors.Trace(err)
   199  		}
   200  		if !overwrite {
   201  			return nil
   202  		}
   203  	}
   204  
   205  	authType, err := c.promptAuthType(ctxt.Stderr, ctxt.Stdin, c.cloud.AuthTypes)
   206  	if err != nil {
   207  		return errors.Trace(err)
   208  	}
   209  	schema, ok := schemas[authType]
   210  	if !ok {
   211  		return errors.NotSupportedf("auth type %q for cloud %q", authType, c.CloudName)
   212  	}
   213  
   214  	attrs, err := c.promptCredentialAttributes(ctxt, ctxt.Stderr, ctxt.Stdin, authType, schema)
   215  	if err != nil {
   216  		return errors.Trace(err)
   217  	}
   218  
   219  	cloudEndpoint := c.cloud.Endpoint
   220  	cloudIdentityEndpoint := c.cloud.IdentityEndpoint
   221  	if len(c.cloud.Regions) > 0 {
   222  		// NOTE(axw) we use the first region in the cloud,
   223  		// because this is all we need for Azure right now.
   224  		// Each region has the same endpoints, so it does
   225  		// not matter which one we use. If we expand
   226  		// credential generation to other providers, and
   227  		// they do have region-specific endpoints, then we
   228  		// should prompt the user for the region to use.
   229  		// That would be better left to the provider, though.
   230  		region := c.cloud.Regions[0]
   231  		cloudEndpoint = region.Endpoint
   232  		cloudIdentityEndpoint = region.IdentityEndpoint
   233  	}
   234  
   235  	credentialsProvider, err := environs.Provider(c.cloud.Type)
   236  	if err != nil {
   237  		return errors.Trace(err)
   238  	}
   239  	newCredential, err := credentialsProvider.FinalizeCredential(
   240  		ctxt, environs.FinalizeCredentialParams{
   241  			Credential:            jujucloud.NewCredential(authType, attrs),
   242  			CloudEndpoint:         cloudEndpoint,
   243  			CloudIdentityEndpoint: cloudIdentityEndpoint,
   244  		},
   245  	)
   246  	if err != nil {
   247  		return errors.Annotate(err, "finalizing credential")
   248  	}
   249  
   250  	existingCredentials.AuthCredentials[credentialName] = *newCredential
   251  	err = c.store.UpdateCredential(c.CloudName, *existingCredentials)
   252  	if err != nil {
   253  		return errors.Trace(err)
   254  	}
   255  	fmt.Fprintf(ctxt.Stdout, "Credentials added for cloud %s.\n\n", c.CloudName)
   256  	return nil
   257  }
   258  
   259  func (c *addCredentialCommand) promptCredentialName(out io.Writer, in io.Reader) (string, error) {
   260  	fmt.Fprint(out, "Enter credential name: ")
   261  	input, err := readLine(in)
   262  	if err != nil {
   263  		return "", errors.Trace(err)
   264  	}
   265  	return strings.TrimSpace(input), nil
   266  }
   267  
   268  func (c *addCredentialCommand) promptReplace(out io.Writer, in io.Reader) (bool, error) {
   269  	fmt.Fprint(out, `
   270  A credential with that name already exists.
   271  
   272  Replace the existing credential? (y/N): `[1:])
   273  
   274  	input, err := readLine(in)
   275  	if err != nil {
   276  		return false, errors.Trace(err)
   277  	}
   278  	return strings.ToLower(strings.TrimSpace(input)) == "y", nil
   279  }
   280  
   281  func (c *addCredentialCommand) promptAuthType(out io.Writer, in io.Reader, authTypes []jujucloud.AuthType) (jujucloud.AuthType, error) {
   282  	if len(authTypes) == 1 {
   283  		fmt.Fprintf(out, "Using auth-type %q.\n", authTypes[0])
   284  		return authTypes[0], nil
   285  	}
   286  	authType := ""
   287  	choices := make([]string, len(authTypes))
   288  	for i, a := range authTypes {
   289  		choices[i] = string(a)
   290  		if i == 0 {
   291  			choices[i] += "*"
   292  		}
   293  	}
   294  	for {
   295  		fmt.Fprintf(out, "Auth Types\n%s\n\nSelect auth-type: ",
   296  			strings.Join(choices, "\n"))
   297  		input, err := readLine(in)
   298  		if err != nil {
   299  			return "", errors.Trace(err)
   300  		}
   301  		authType = strings.ToLower(strings.TrimSpace(input))
   302  		if authType == "" {
   303  			authType = string(authTypes[0])
   304  		}
   305  		isValid := false
   306  		for _, a := range authTypes {
   307  			if string(a) == authType {
   308  				isValid = true
   309  				break
   310  			}
   311  		}
   312  		if isValid {
   313  			break
   314  		}
   315  		fmt.Fprintf(out, "Invalid auth type %q.\n", authType)
   316  	}
   317  	return jujucloud.AuthType(authType), nil
   318  }
   319  
   320  func (c *addCredentialCommand) promptCredentialAttributes(
   321  	ctxt *cmd.Context, out io.Writer, in io.Reader, authType jujucloud.AuthType, schema jujucloud.CredentialSchema,
   322  ) (map[string]string, error) {
   323  
   324  	attrs := make(map[string]string)
   325  	for _, attr := range schema {
   326  		currentAttr := attr
   327  		value := ""
   328  		for {
   329  			var err error
   330  			// Interactive add does not support adding multi-line values, which
   331  			// is what we typically get when the attribute can come from a file.
   332  			// For now we'll skip, and just get the user to enter the file path.
   333  			// TODO(wallyworld) - add support for multi-line entry
   334  			if currentAttr.FileAttr == "" {
   335  				value, err = c.promptFieldValue(out, in, currentAttr)
   336  				if err != nil {
   337  					return nil, err
   338  				}
   339  			}
   340  			// Validate the entered value matches any options.
   341  			// If the user just hits Enter, the first option is used.
   342  			if len(currentAttr.Options) > 0 {
   343  				isValid := false
   344  				for _, choice := range currentAttr.Options {
   345  					if choice == value || value == "" {
   346  						isValid = true
   347  						break
   348  					}
   349  				}
   350  				if !isValid {
   351  					fmt.Fprintf(out, "Invalid value %q.\n", value)
   352  					continue
   353  				}
   354  				if value == "" && !currentAttr.Optional {
   355  					value = fmt.Sprintf("%v", currentAttr.Options[0])
   356  				}
   357  			}
   358  
   359  			// If the entered value is empty and the attribute can come
   360  			// from a file, prompt for that.
   361  			if value == "" && currentAttr.FileAttr != "" {
   362  				fileAttr := currentAttr
   363  				fileAttr.Name = currentAttr.FileAttr
   364  				fileAttr.Hidden = false
   365  				fileAttr.FilePath = true
   366  				currentAttr = fileAttr
   367  				value, err = c.promptFieldValue(out, in, currentAttr)
   368  				if err != nil {
   369  					return nil, err
   370  				}
   371  			}
   372  
   373  			// Validate any file attribute is a valid file.
   374  			if value != "" && currentAttr.FilePath {
   375  				value, err = jujucloud.ValidateFileAttrValue(value)
   376  				if err != nil {
   377  					fmt.Fprintf(out, "Invalid file attribute %q.\n", err.Error())
   378  					continue
   379  				}
   380  			}
   381  
   382  			// Stay in the loop if we need a mandatory value.
   383  			if value != "" || currentAttr.Optional {
   384  				break
   385  			}
   386  		}
   387  		if value != "" {
   388  			attrs[currentAttr.Name] = value
   389  		}
   390  	}
   391  	return attrs, nil
   392  }
   393  
   394  func (c *addCredentialCommand) promptFieldValue(
   395  	out io.Writer, in io.Reader, attr jujucloud.NamedCredentialAttr,
   396  ) (string, error) {
   397  
   398  	name := attr.Name
   399  	// Formulate the prompt for the list of valid options.
   400  	optionsPrompt := ""
   401  	if len(attr.Options) > 0 {
   402  		options := make([]string, len(attr.Options))
   403  		for i, opt := range attr.Options {
   404  			options[i] = fmt.Sprintf("%v", opt)
   405  			if i == 0 {
   406  				options[i] += "*"
   407  			}
   408  		}
   409  		optionsPrompt = fmt.Sprintf(" [%v]", strings.Join(options, ","))
   410  	}
   411  
   412  	// Prompt for and accept input for field value.
   413  	fmt.Fprintf(out, "Enter %s%s: ", name, optionsPrompt)
   414  	var input string
   415  	var err error
   416  	if attr.Hidden {
   417  		input, err = c.readHiddenField(in)
   418  		fmt.Fprintln(out)
   419  	} else {
   420  		input, err = readLine(in)
   421  	}
   422  	if err != nil {
   423  		return "", errors.Trace(err)
   424  	}
   425  	value := strings.TrimSpace(input)
   426  	return value, nil
   427  }
   428  
   429  func (c *addCredentialCommand) readHiddenField(in io.Reader) (string, error) {
   430  	if f, ok := in.(*os.File); ok && terminal.IsTerminal(int(f.Fd())) {
   431  		value, err := terminal.ReadPassword(int(f.Fd()))
   432  		if err != nil {
   433  			return "", errors.Trace(err)
   434  		}
   435  		return string(value), nil
   436  	}
   437  	return readLine(in)
   438  }