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 }