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 }