Skip to content

Commit

Permalink
Merge pull request #10604 from anastasiamac/askortell-addcredential
Browse files Browse the repository at this point in the history
#10604

## Description of change

In previous iteration, 'add-credential' command has been upgraded to operate on both local client and on controller. This PR ensures that a user is prompted to confirm to upload a credential to the current controller if one is detected.

Drive-by:
(*) File can be specified with an -f or --file option for both add-cloud and add-credential commands.

## QA steps

(*) Add credential interactively (local since no current nor specified controller)
```
$ juju add-credential aws
Enter credential name: sample

Regions
 us-east-1
 us-east-2
 us-west-1
 us-west-2
 ca-central-1
 eu-west-1
 eu-west-2
 eu-west-3
 eu-central-1
 ap-south-1
 ap-southeast-1
 ap-southeast-2
 ap-northeast-1
 ap-northeast-2
 sa-east-1

Select region [any region, credential is not region specific]: 

Using auth-type "access-key".

Enter access-key: 7efgugfygr

Enter secret-key: 

Credential "sample" added locally for cloud "aws".
```
(*) Add credential with file (local since no current nor specified controller)
```
$ juju add-credential aws -f ~/creds.yaml
WARNING credential "sample" for cloud "aws" already exists locally, use 'juju update-credential aws sample' to update it
No local credentials for cloud "aws" changed.

$ juju add-credential aws -f ~/creds.yaml 
Credentials "example" added locally for cloud "aws". 
```
(*) Add credential to a specified controller with file
```
$ juju add-credential aws -f ~/creds.yaml -c mycontroller 
Using remote cloud "aws" from the controller to verify credentials. 
Credential "special" added locally for cloud "aws". 
 
Controller credential "special" for user "admin" for cloud "aws" on controller "mycontroller" added. 
For more information, see ‘juju show-credential aws special’. 
```
(*) Add credential to a current controller with file (prompt)
```
$ juju add-credential aws -f ~/creds.yaml 
Do you want to add a credential to current controller "mycontroller"? (Y/n): 

Using remote cloud "aws" from the controller to verify credentials.
Credential "news" added locally for cloud "aws".

Controller credential "news" for user "admin" for cloud "aws" on controller "mycontroller" added.
For more information, see ‘juju show-credential aws news’.
```
(*) Add credential to a current controller with file (skip prompt)
```
$ juju add-credential aws -f ~/creds.yaml --no-prompt
Using remote cloud "aws" from the controller to verify credentials.
Credential "again" added locally for cloud "aws".

Controller credential "again" for user "admin" for cloud "aws" on controller "mycontroller" added.
For more information, see ‘juju show-credential aws again’.
```
(*) Add credential (remote since credential exists locally)
```
$ juju add-credential aws -f ~/creds.yaml -c mycontroller
Using remote cloud "aws" from the controller to verify credentials.
WARNING credential "trial" for cloud "aws" already exists locally, use 'juju update-credential aws trial' to update it
No local credentials for cloud "aws" changed.
Controller credential "trial" for user "admin" for cloud "aws" on controller "mycontroller" added.
For more information, see ‘juju show-credential aws trial’.
```
(*) Add credential to a specified controller interactively
```
$ juju add-credential aws -c mycontroller
Using remote cloud "aws" from the controller to verify credentials.
Enter credential name: okay

Regions
 ap-northeast-1
 ap-northeast-2
 ap-south-1
 ap-southeast-1
 ap-southeast-2
 ca-central-1
 eu-central-1
 eu-west-1
 eu-west-2
 eu-west-3
 sa-east-1
 us-east-1
 us-east-2
 us-west-1
 us-west-2

Select region [any region, credential is not region specific]: 

Using auth-type "access-key".

Enter access-key: dscsc

Enter secret-key: 

Credential "okay" added locally for cloud "aws".

Controller credential "okay" for user "admin" for cloud "aws" on controller "mycontroller" added.
For more information, see ‘juju show-credential aws okay’.
```
(*) Add credential to a current controller interactively (prompt)
```
$ juju add-credential aws
Do you want to add a credential to current controller "mycontroller"? (Y/n): 

Using remote cloud "aws" from the controller to verify credentials.
Enter credential name: fuunny

Regions
 ap-northeast-1
 ap-northeast-2
 ap-south-1
 ap-southeast-1
 ap-southeast-2
 ca-central-1
 eu-central-1
 eu-west-1
 eu-west-2
 eu-west-3
 sa-east-1
 us-east-1
 us-east-2
 us-west-1
 us-west-2

Select region [any region, credential is not region specific]: 

Using auth-type "access-key".

Enter access-key: asdsds

Enter secret-key: 

Credential "fuunny" added locally for cloud "aws".

Controller credential "fuunny" for user "admin" for cloud "aws" on controller "mycontroller" added.
For more information, see ‘juju show-credential aws fuunny’.
```
(*) Add credential to a current controller interactively (skip prompt)
```
$ juju add-credential aws --no-prompt
Using remote cloud "aws" from the controller to verify credentials.
Enter credential name: buteme 

Regions
 ap-northeast-1
 ap-northeast-2
 ap-south-1
 ap-southeast-1
 ap-southeast-2
 ca-central-1
 eu-central-1
 eu-west-1
 eu-west-2
 eu-west-3
 sa-east-1
 us-east-1
 us-east-2
 us-west-1
 us-west-2

Select region [any region, credential is not region specific]: 

Using auth-type "access-key".

Enter access-key: dfcdv

Enter secret-key: 

Credential "buteme" added locally for cloud "aws".

Controller credential "buteme" for user "admin" for cloud "aws" on controller "mycontroller" added.
For more information, see ‘juju show-credential aws buteme’.
```
(*) Add credential exists both locally and remotely
```
$ juju add-credential aws -f ~/creds.yaml -c mycontroller
Using remote cloud "aws" from the controller to verify credentials.
WARNING credential "amtest" for cloud "aws" already exists locally, use 'juju update-credential aws amtest' to update it
No local credentials for cloud "aws" changed.
No remote credentials for cloud "aws" added.
```
  • Loading branch information
jujubot committed Sep 6, 2019
2 parents 87dceb6 + 5a8981c commit d7b574b
Show file tree
Hide file tree
Showing 8 changed files with 157 additions and 106 deletions.
18 changes: 12 additions & 6 deletions cmd/juju/cloud/add.go
Expand Up @@ -71,10 +71,15 @@ positional argument:
When <cloud definition file> is provided with <cloud name>,
Juju stores that definition in the current controller (after
validating the contents), or the specified controller if
--controller is used. To make use of this multi-cloud feature,
the controller needs to have the "multi-cloud" feature flag turned on.
--controller is used.
If --local is used, Juju stores that definition its internal cache directly.
If a current controller is detected, Juju will prompt the user to confirm
whether this new cloud also needs to be uploaded.
Use --no-prompt option when this prompt is undesirable, but the upload to
the current controller is wanted.
Use --controller option to upload a cloud to a different controller.
Use --local option to add cloud to the current device only.
DEPRECATED If <cloud name> already exists in Juju's cache, then the `[1:] + "`--replace`" + `
option is required. Use 'update-credential' instead.
Expand Down Expand Up @@ -108,8 +113,9 @@ When a a running controller is updated, the credential for the cloud
is also uploaded. As with the cloud, the credential needs
to have been added to the local Juju cache; add-credential is used to
do that. If there's only one credential for the cloud it will be
uploaded to the controller. If the cloud has multiple local credentials
you can specify which to upload with the --credential option.
uploaded to the controller automatically by add-clloud command.
However, if the cloud has multiple local credentials you can specify
which to upload with the --credential option.
When adding clouds to a controller, some clouds are whitelisted and can be easily added:
%v
Expand Down Expand Up @@ -166,7 +172,6 @@ type AddCloudCommand struct {
cloudMetadataStore CloudMetadataStore

// These attributes are used when adding a cloud to a controller.
controllerName string
credentialName string
addCloudAPIFunc func() (AddCloudAPI, error)

Expand Down Expand Up @@ -221,6 +226,7 @@ func (c *AddCloudCommand) SetFlags(f *gnuflag.FlagSet) {
f.BoolVar(&c.Replace, "replace", false, "DEPRECATED: Overwrite any existing cloud information for <cloud name>")
f.BoolVar(&c.Force, "force", false, "Force add cloud to the controller")
f.StringVar(&c.CloudFile, "f", "", "The path to a cloud definition file")
f.StringVar(&c.CloudFile, "file", "", "The path to a cloud definition file")
f.StringVar(&c.credentialName, "credential", "", "Credential to use for new cloud")
}

Expand Down
156 changes: 85 additions & 71 deletions cmd/juju/cloud/addcredential.go
Expand Up @@ -25,7 +25,7 @@ import (
)

var usageAddCredentialSummary = `
Adds or replaces credentials for a cloud, stored locally on this client.`[1:]
Adds a credential for a cloud to a local client and uploads it to a controller.`[1:]

var usageAddCredentialDetails = `
The juju add-credential command operates in two modes.
Expand All @@ -36,16 +36,9 @@ the cloud provider.
Providing the ` + "`-f <credentials.yaml>` " + `option switches to the
non-interactive mode. <credentials.yaml> must be a path to a correctly
formatted YAML-formatted file. Details of the format are provided at
"About credentials.yaml" below.
formatted YAML-formatted file.
The ` + "`--replace`" + ` option is required if credential information
for the named cloud already exists locally. All such information will be
overwritten. This option is DEPRECATED, use 'juju update-credential' instead.
About credentials.yaml:
Here is a sample credentials.yaml showing four credentials being stored
against three clouds:
Sample yaml file shows four credentials being stored against three clouds:
credentials:
aws:
Expand All @@ -67,29 +60,27 @@ against three clouds:
auth-type: interactive
trust-password: <password>
More generally, here is a loosely defined grammar for credentials.yaml:
credentials:
<cloud-name>:
<credential-name>:
auth-type: <auth-type>
<auth-type-key>: <auth-type-value>
[<auth-type-key>: <auth-type-value>]
Every <auth-type> requires its own <auth-type-key> and <auth-type-value>
pairs.
The <credential-name> parameter of each credential is arbitrary, but must
be unique within each <cloud-name>. This allows allow each cloud to store
multiple credentials.
The format for a credential is cloud-specific. Thus, it's best to use
'add-credential' command in an interactive mode. This will result in
adding this new credential locally and / or uploading it to a controller
in a correct format for the desired cloud.
The ` + "`--replace`" + ` option is required if credential information
for the named cloud already exists locally. All such information will be
overwritten. This option is DEPRECATED, use 'juju update-credential' instead.
Examples:
juju add-credential google
juju add-credential google --local
juju add-credential google -c mycontroller
juju add-credential aws -f ~/credentials.yaml -c mycontroller
juju add-credential aws -f ~/credentials.yaml
juju add-credential aws -f ~/credentials.yaml --local
juju add-credential aws -f ~/credentials.yaml --no-prompt
Notes:
If you are setting up Juju for the first time, consider running
Expand All @@ -100,9 +91,15 @@ This command does not set default regions nor default credentials for the
cloud. The commands ` + "`juju default-region`" + ` and ` + "`juju default-credential`" + `
provide that functionality.
By default, after validating the contents, credentials are added both
to the current controller and the current client device.
Use --controller option to add credentials to a different controller.
By default, after validating the contents, Juju will add a credential locally,
to the current client device, and will upload it to a controller.
If a current controller is detected, Juju will prompt the user to confirm
whether this new credential also needs to be uploaded.
Use --no-prompt option when this prompt is undesirable, but the upload to
the current controller is wanted.
Use --controller option to upload a credential to a different controller.
Use --local option to add credentials to the current device only.
Further help:
Expand All @@ -112,6 +109,7 @@ instructions.
See also:
credentials
remove-credential
update-credential
default-credential
default-region
autoload-credentials
Expand All @@ -136,9 +134,11 @@ type addCredentialCommand struct {
Region string

// These attributes are used when adding credentials to a controller.
controllerName string
remoteCloudFound bool
credentialAPIFunc func() (CredentialAPI, error)

// existsLocally whether this credential already exists locally.
existsLocally bool
}

// NewAddCredentialCommand returns a command to add credential information.
Expand All @@ -165,8 +165,9 @@ func (c *addCredentialCommand) Info() *cmd.Info {

func (c *addCredentialCommand) SetFlags(f *gnuflag.FlagSet) {
c.OptionalControllerCommand.SetFlags(f)
f.BoolVar(&c.Replace, "replace", false, "Overwrite existing credential information")
f.BoolVar(&c.Replace, "replace", false, "DEPRECATED: Overwrite existing credential information")
f.StringVar(&c.CredentialsFile, "f", "", "The YAML file containing credentials to add")
f.StringVar(&c.CredentialsFile, "file", "", "The YAML file containing credentials to add")
f.StringVar(&c.Region, "region", "", "Cloud region that credential is valid for")
}

Expand All @@ -175,15 +176,6 @@ func (c *addCredentialCommand) Init(args []string) (err error) {
return errors.New("Usage: juju add-credential <cloud-name> [-f <credentials.yaml>]")
}
c.CloudName = args[0]
c.ControllerName, err = c.ControllerNameFromArg()
if err != nil && errors.Cause(err) != modelcmd.ErrNoControllersDefined {
return errors.Trace(err)
}
if c.ControllerName == "" {
// No controller was specified explicitly and we did not detect a current controller,
// this operation should be local only.
c.Local = true
}
return cmd.CheckEmpty(args[1:])
}

Expand All @@ -193,14 +185,24 @@ func (c *addCredentialCommand) Run(ctxt *cmd.Context) error {
// https://bugs.launchpad.net/juju/+bug/1821279
ctxt.Warningf("--replace is DEPRECATED. Use 'juju update-credential' to update credentials.")
}
// Check that the supplied cloud is valid.

var err error
if !c.Local {
if !c.Local && c.ControllerName == "" {
// The user may have specified the controller via a --controller option.
// If not, let's see if there is a current controller that can be detected.
c.ControllerName, err = c.MaybePromptCurrentController(ctxt, "add a credential to")
if err != nil {
return errors.Trace(err)
}
}

// Check that the supplied cloud is valid.
if !c.Local && c.ControllerName != "" {
if err := c.maybeRemoteCloud(ctxt); err != nil {
if !errors.IsNotFound(err) {
logger.Errorf("%v", err)
}
ctxt.Infof("Cloud %q is not remotely found on the controller, looking for a locally stored cloud.", c.CloudName)
ctxt.Infof("Cloud %q is not found on the controller, looking for a locally stored cloud.", c.CloudName)
}
}
if c.cloud == nil {
Expand Down Expand Up @@ -278,11 +280,6 @@ func (c *addCredentialCommand) Run(ctxt *cmd.Context) error {
if !names.IsValidCloudCredentialName(name) {
return errors.Errorf("%q is not a valid credential name", name)
}

if _, ok := existingCredentials.AuthCredentials[name]; ok {
ctxt.Warningf("credential %q for cloud %q already exists locally, use 'juju update-credential %v %v' to update it", name, c.CloudName, c.CloudName, name)
continue
}
if !validAuthType(cred.AuthType()) {
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)
}
Expand All @@ -300,23 +297,48 @@ func (c *addCredentialCommand) Run(ctxt *cmd.Context) error {
cred = *newCredential
}

added[name] = cred
if _, ok := existingCredentials.AuthCredentials[name]; ok {
ctxt.Warningf("credential %q for cloud %q already exists locally, use 'juju update-credential %v %v -f %v' to update this local client copy", name, c.CloudName, c.CloudName, name, c.CredentialsFile)
continue
}

existingCredentials.AuthCredentials[name] = cred
allNames = append(allNames, name)
added[name] = cred
}
return c.internalAddCredential(ctxt, "added", *existingCredentials, added, allNames)
}

func (c *addCredentialCommand) internalAddCredential(ctxt *cmd.Context, verb string, existingCredentials jujucloud.CloudCredential, added map[string]jujucloud.Credential, allNames []string) error {
var err error
// Local processing.
if len(allNames) == 0 {
fmt.Fprintf(ctxt.Stdout, "No local credentials for cloud %q changed.\n", c.CloudName)
return nil
}
err = c.Store.UpdateCredential(c.CloudName, *existingCredentials)
if err != nil {
return err
} else {
var msg string
if len(allNames) == 1 {
msg = fmt.Sprintf(" %q", allNames[0])
} else {
msg = fmt.Sprintf("s %q", strings.Join(allNames, ", "))
}
err = c.Store.UpdateCredential(c.CloudName, existingCredentials)
if err == nil {
fmt.Fprintf(ctxt.Stdout, "Credential%s %s locally for cloud %q.\n\n", msg, verb, c.CloudName)
} else {
fmt.Fprintf(ctxt.Stdout, "Credential%s not %v locally for cloud %q: %v\n\n", msg, verb, c.CloudName, err)
err = cmd.ErrSilent
}
}
fmt.Fprintf(ctxt.Stdout, "Credentials %q added locally for cloud %q.\n", strings.Join(allNames, ", "), c.CloudName)
// Remote processing.
if !c.Local {
return c.addRemoteCredentials(ctxt, added)
if c.ControllerName != "" {
return c.addRemoteCredentials(ctxt, added, err)
} else {
ctxt.Infof("There are no controllers specified - not adding a credential to any controller.")
return err
}
}
return nil
return err
}

func (c *addCredentialCommand) existingCredentialsForCloud() (*jujucloud.CloudCredential, error) {
Expand Down Expand Up @@ -392,15 +414,7 @@ func (c *addCredentialCommand) interactiveAddCredential(ctxt *cmd.Context, schem
}

existingCredentials.AuthCredentials[credentialName] = *newCredential
err = c.Store.UpdateCredential(c.CloudName, *existingCredentials)
if err != nil {
return errors.Trace(err)
}
fmt.Fprintf(ctxt.Stdout, "Credential %q %v locally for cloud %q.\n\n", credentialName, verb, c.CloudName)
if !c.Local {
return c.addRemoteCredentials(ctxt, map[string]jujucloud.Credential{credentialName: *newCredential})
}
return nil
return c.internalAddCredential(ctxt, verb, *existingCredentials, map[string]jujucloud.Credential{credentialName: *newCredential}, []string{credentialName})
}

func finalizeProvider(ctxt *cmd.Context, cloud *jujucloud.Cloud, regionName, defaultRegion string, authType jujucloud.AuthType, attrs map[string]string) (*jujucloud.Credential, error) {
Expand Down Expand Up @@ -605,24 +619,24 @@ func (c *addCredentialCommand) maybeRemoteCloud(ctxt *cmd.Context) error {
return err
}
if remoteCloud, ok := remoteUserClouds[names.NewCloudTag(c.CloudName)]; ok {
ctxt.Infof("Using remote cloud %q from the controller to verify credentials.", c.CloudName)
ctxt.Infof("Using cloud %q from the controller to verify credentials.", c.CloudName)
c.cloud = &remoteCloud
c.remoteCloudFound = true
}
return nil
}

func (c *addCredentialCommand) addRemoteCredentials(ctxt *cmd.Context, all map[string]jujucloud.Credential) error {
func (c *addCredentialCommand) addRemoteCredentials(ctxt *cmd.Context, all map[string]jujucloud.Credential, localError error) error {
if len(all) == 0 {
fmt.Fprintf(ctxt.Stdout, "No remote credentials for cloud %q added.\n", c.CloudName)
return nil
fmt.Fprintf(ctxt.Stdout, "No credentials for cloud %q uploaded to controller %q.\n", c.CloudName, c.ControllerName)
return localError
}
if !c.remoteCloudFound {
fmt.Fprintf(ctxt.Stdout, "No remote cloud %v found on the controller %v: credentials are not added remotely.\n"+
"Use 'juju clouds -c %v' to see what clouds are available remotely.\n"+
fmt.Fprintf(ctxt.Stdout, "No cloud %q found on the controller %q: credentials are not uploaded.\n"+
"Use 'juju clouds -c %v' to see what clouds are available on the controller.\n"+
"User 'juju add-cloud %v -c %v' to add your cloud to the controller.\n",
c.CloudName, c.ControllerName, c.ControllerName, c.CloudName, c.ControllerName)
return nil
return localError
}

accountDetails, err := c.Store.AccountDetails(c.ControllerName)
Expand All @@ -641,9 +655,9 @@ func (c *addCredentialCommand) addRemoteCredentials(ctxt *cmd.Context, all map[s
results, err := client.UpdateCloudsCredentials(verified)
if err != nil {
logger.Errorf("%v", err)
ctxt.Warningf("Could not add credentials remotely, on controller %q", c.ControllerName)
ctxt.Warningf("Could not upload credentials to controller %q", c.ControllerName)
}
return processUpdateCredentialResult(ctxt, accountDetails, "added", results)
return processUpdateCredentialResult(ctxt, accountDetails, "added", results, c.ControllerName, localError)
}

func enterFile(name, descr string, p *interact.Pollster, expanded, optional bool) (string, error) {
Expand Down

0 comments on commit d7b574b

Please sign in to comment.