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 }