github.com/makyo/juju@v0.0.0-20160425123129-2608902037e9/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  	"golang.org/x/crypto/ssh/terminal"
    16  	"launchpad.net/gnuflag"
    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      list-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  	f.BoolVar(&c.Replace, "replace", false, "Overwrite existing credential information")
   103  	f.StringVar(&c.CredentialsFile, "f", "", "The YAML file containing credentials to add")
   104  }
   105  
   106  func (c *addCredentialCommand) Init(args []string) (err error) {
   107  	if len(args) < 1 {
   108  		return errors.New("Usage: juju add-credential <cloud-name> [-f <credentials.yaml>]")
   109  	}
   110  	c.CloudName = args[0]
   111  	return cmd.CheckEmpty(args[1:])
   112  }
   113  
   114  func (c *addCredentialCommand) Run(ctxt *cmd.Context) error {
   115  	// Check that the supplied cloud is valid.
   116  	var err error
   117  	if c.cloud, err = common.CloudOrProvider(c.CloudName, c.cloudByNameFunc); err != nil {
   118  		if !errors.IsNotFound(err) {
   119  			return err
   120  		}
   121  	}
   122  	if len(c.cloud.AuthTypes) == 0 {
   123  		return errors.Errorf("cloud %q does not require credentials", c.CloudName)
   124  	}
   125  
   126  	if c.CredentialsFile == "" {
   127  		credentialsProvider, err := environs.Provider(c.cloud.Type)
   128  		if err != nil {
   129  			return errors.Annotate(err, "getting provider for cloud")
   130  		}
   131  		return c.interactiveAddCredential(ctxt, credentialsProvider.CredentialSchemas())
   132  	}
   133  	data, err := ioutil.ReadFile(c.CredentialsFile)
   134  	if err != nil {
   135  		return errors.Annotate(err, "reading credentials file")
   136  	}
   137  
   138  	specifiedCredentials, err := jujucloud.ParseCredentials(data)
   139  	if err != nil {
   140  		return errors.Annotate(err, "parsing credentials file")
   141  	}
   142  	credentials, ok := specifiedCredentials[c.CloudName]
   143  	if !ok {
   144  		return errors.Errorf("no credentials for cloud %s exist in file %s", c.CloudName, c.CredentialsFile)
   145  	}
   146  	existingCredentials, err := c.existingCredentialsForCloud()
   147  	if err != nil {
   148  		return errors.Trace(err)
   149  	}
   150  	// If there are *any* credentials already for the cloud, we'll ask for the --replace flag.
   151  	if !c.Replace && len(existingCredentials.AuthCredentials) > 0 && len(credentials.AuthCredentials) > 0 {
   152  		return errors.Errorf("credentials for cloud %s already exist; use --replace to overwrite / merge", c.CloudName)
   153  	}
   154  	for name, cred := range credentials.AuthCredentials {
   155  		existingCredentials.AuthCredentials[name] = cred
   156  	}
   157  	err = c.store.UpdateCredential(c.CloudName, *existingCredentials)
   158  	if err != nil {
   159  		return err
   160  	}
   161  	fmt.Fprintf(ctxt.Stdout, "credentials updated for cloud %s\n", c.CloudName)
   162  	return nil
   163  }
   164  
   165  func (c *addCredentialCommand) existingCredentialsForCloud() (*jujucloud.CloudCredential, error) {
   166  	existingCredentials, err := c.store.CredentialForCloud(c.CloudName)
   167  	if err != nil && !errors.IsNotFound(err) {
   168  		return nil, errors.Annotate(err, "reading existing credentials for cloud")
   169  	}
   170  	if errors.IsNotFound(err) {
   171  		existingCredentials = &jujucloud.CloudCredential{
   172  			AuthCredentials: make(map[string]jujucloud.Credential),
   173  		}
   174  	}
   175  	return existingCredentials, nil
   176  }
   177  
   178  func (c *addCredentialCommand) interactiveAddCredential(ctxt *cmd.Context, schemas map[jujucloud.AuthType]jujucloud.CredentialSchema) error {
   179  	var err error
   180  	credentialName, err := c.promptCredentialName(ctxt.Stderr, ctxt.Stdin)
   181  	if err != nil {
   182  		return errors.Trace(err)
   183  	}
   184  	if credentialName == "" {
   185  		fmt.Fprintln(ctxt.Stderr, "credentials entry aborted")
   186  		return nil
   187  	}
   188  
   189  	// Prompt to overwrite if needed.
   190  	existingCredentials, err := c.existingCredentialsForCloud()
   191  	if err != nil {
   192  		return errors.Trace(err)
   193  	}
   194  	if _, ok := existingCredentials.AuthCredentials[credentialName]; ok {
   195  		overwrite, err := c.promptReplace(ctxt.Stderr, ctxt.Stdin)
   196  		if err != nil {
   197  			return errors.Trace(err)
   198  		}
   199  		if !overwrite {
   200  			return nil
   201  		}
   202  	}
   203  
   204  	authType, err := c.promptAuthType(ctxt.Stderr, ctxt.Stdin, c.cloud.AuthTypes)
   205  	if err != nil {
   206  		return errors.Trace(err)
   207  	}
   208  	schema, ok := schemas[authType]
   209  	if !ok {
   210  		return errors.NotSupportedf("auth type %q for cloud %q", authType, c.CloudName)
   211  	}
   212  
   213  	attrs, err := c.promptCredentialAttributes(ctxt, ctxt.Stderr, ctxt.Stdin, authType, schema)
   214  	if err != nil {
   215  		return errors.Trace(err)
   216  	}
   217  	newCredential := jujucloud.NewCredential(authType, attrs)
   218  	existingCredentials.AuthCredentials[credentialName] = newCredential
   219  	err = c.store.UpdateCredential(c.CloudName, *existingCredentials)
   220  	if err != nil {
   221  		return errors.Trace(err)
   222  	}
   223  	fmt.Fprintf(ctxt.Stdout, "credentials added for cloud %s\n\n", c.CloudName)
   224  	return nil
   225  }
   226  
   227  func (c *addCredentialCommand) promptCredentialName(out io.Writer, in io.Reader) (string, error) {
   228  	fmt.Fprint(out, "  credential name: ")
   229  	input, err := readLine(in)
   230  	if err != nil {
   231  		return "", errors.Trace(err)
   232  	}
   233  	return strings.TrimSpace(input), nil
   234  }
   235  
   236  func (c *addCredentialCommand) promptReplace(out io.Writer, in io.Reader) (bool, error) {
   237  	fmt.Fprint(out, "  replace existing credential? [y/N]: ")
   238  	input, err := readLine(in)
   239  	if err != nil {
   240  		return false, errors.Trace(err)
   241  	}
   242  	return strings.ToLower(strings.TrimSpace(input)) == "y", nil
   243  }
   244  
   245  func (c *addCredentialCommand) promptAuthType(out io.Writer, in io.Reader, authTypes []jujucloud.AuthType) (jujucloud.AuthType, error) {
   246  	if len(authTypes) == 1 {
   247  		fmt.Fprintf(out, "  auth-type: %v\n", authTypes[0])
   248  		return authTypes[0], nil
   249  	}
   250  	authType := ""
   251  	choices := make([]string, len(authTypes))
   252  	for i, a := range authTypes {
   253  		choices[i] = string(a)
   254  		if i == 0 {
   255  			choices[i] += "*"
   256  		}
   257  	}
   258  	for {
   259  		fmt.Fprintf(out, "  select auth-type [%v]: ", strings.Join(choices, ", "))
   260  		input, err := readLine(in)
   261  		if err != nil {
   262  			return "", errors.Trace(err)
   263  		}
   264  		authType = strings.ToLower(strings.TrimSpace(input))
   265  		if authType == "" {
   266  			authType = string(authTypes[0])
   267  		}
   268  		isValid := false
   269  		for _, a := range authTypes {
   270  			if string(a) == authType {
   271  				isValid = true
   272  				break
   273  			}
   274  		}
   275  		if isValid {
   276  			break
   277  		}
   278  		fmt.Fprintf(out, "  ...invalid auth type %q\n", authType)
   279  	}
   280  	return jujucloud.AuthType(authType), nil
   281  }
   282  
   283  func (c *addCredentialCommand) promptCredentialAttributes(
   284  	ctxt *cmd.Context, out io.Writer, in io.Reader, authType jujucloud.AuthType, schema jujucloud.CredentialSchema,
   285  ) (map[string]string, error) {
   286  
   287  	attrs := make(map[string]string)
   288  	for _, attr := range schema {
   289  		currentAttr := attr
   290  		value := ""
   291  		for {
   292  			var err error
   293  			// Interactive add does not support adding multi-line values, which
   294  			// is what we typically get when the attribute can come from a file.
   295  			// For now we'll skip, and just get the user to enter the file path.
   296  			// TODO(wallyworld) - add support for multi-line entry
   297  			if currentAttr.FileAttr == "" {
   298  				value, err = c.promptFieldValue(out, in, currentAttr)
   299  				if err != nil {
   300  					return nil, err
   301  				}
   302  			}
   303  			// Validate the entered value matches any options.
   304  			// If the user just hits Enter, the first option is used.
   305  			if len(currentAttr.Options) > 0 {
   306  				isValid := false
   307  				for _, choice := range currentAttr.Options {
   308  					if choice == value || value == "" {
   309  						isValid = true
   310  						break
   311  					}
   312  				}
   313  				if !isValid {
   314  					fmt.Fprintf(out, "  ...invalid value %q\n", value)
   315  					continue
   316  				}
   317  				if value == "" && !currentAttr.Optional {
   318  					value = fmt.Sprintf("%v", currentAttr.Options[0])
   319  				}
   320  			}
   321  
   322  			// If the entered value is empty and the attribute can come
   323  			// from a file, prompt for that.
   324  			if value == "" && currentAttr.FileAttr != "" {
   325  				fileAttr := currentAttr
   326  				fileAttr.Name = currentAttr.FileAttr
   327  				fileAttr.Hidden = false
   328  				fileAttr.FilePath = true
   329  				currentAttr = fileAttr
   330  				value, err = c.promptFieldValue(out, in, currentAttr)
   331  				if err != nil {
   332  					return nil, err
   333  				}
   334  			}
   335  
   336  			// Validate any file attribute is a valid file.
   337  			if value != "" && currentAttr.FilePath {
   338  				value, err = jujucloud.ValidateFileAttrValue(value)
   339  				if err != nil {
   340  					fmt.Fprintf(out, "  ...%s\n", err.Error())
   341  					continue
   342  				}
   343  			}
   344  
   345  			// Stay in the loop if we need a mandatory value.
   346  			if value != "" || currentAttr.Optional {
   347  				break
   348  			}
   349  		}
   350  		if value != "" {
   351  			attrs[currentAttr.Name] = value
   352  		}
   353  	}
   354  	return attrs, nil
   355  }
   356  
   357  func (c *addCredentialCommand) promptFieldValue(
   358  	out io.Writer, in io.Reader, attr jujucloud.NamedCredentialAttr,
   359  ) (string, error) {
   360  
   361  	name := attr.Name
   362  	// Formulate the prompt for the list of valid options.
   363  	optionsPrompt := ""
   364  	if len(attr.Options) > 0 {
   365  		options := make([]string, len(attr.Options))
   366  		for i, opt := range attr.Options {
   367  			options[i] = fmt.Sprintf("%v", opt)
   368  			if i == 0 {
   369  				options[i] += "*"
   370  			}
   371  		}
   372  		optionsPrompt = fmt.Sprintf(" [%v]", strings.Join(options, ","))
   373  	}
   374  
   375  	// Prompt for and accept input for field value.
   376  	fmt.Fprintf(out, "  %s%s: ", name, optionsPrompt)
   377  	var input string
   378  	var err error
   379  	if attr.Hidden {
   380  		input, err = c.readHiddenField(in)
   381  		fmt.Fprintln(out)
   382  	} else {
   383  		input, err = readLine(in)
   384  	}
   385  	if err != nil {
   386  		return "", errors.Trace(err)
   387  	}
   388  	value := strings.TrimSpace(input)
   389  	return value, nil
   390  }
   391  
   392  func (c *addCredentialCommand) readHiddenField(in io.Reader) (string, error) {
   393  	if f, ok := in.(*os.File); ok && terminal.IsTerminal(int(f.Fd())) {
   394  		value, err := terminal.ReadPassword(int(f.Fd()))
   395  		if err != nil {
   396  			return "", errors.Trace(err)
   397  		}
   398  		return string(value), nil
   399  	}
   400  	return readLine(in)
   401  }