Skip to content

Commit 90a63fa

Browse files
committed
Add a new update-credential command
1 parent 1fd4c66 commit 90a63fa

7 files changed

+360
-0
lines changed

cmd/juju/cloud/export_test.go

+11
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@
44
package cloud
55

66
import (
7+
"github.com/juju/cmd"
8+
79
jujucloud "github.com/juju/juju/cloud"
10+
"github.com/juju/juju/cmd/modelcmd"
811
sstesting "github.com/juju/juju/environs/simplestreams/testing"
912
"github.com/juju/juju/jujuclient"
1013
)
@@ -70,3 +73,11 @@ func NewSetDefaultRegionCommandForTest(testStore jujuclient.CredentialStore) *se
7073
store: testStore,
7174
}
7275
}
76+
77+
func NewUpdateCredentialCommandForTest(testStore jujuclient.ClientStore, api credentialAPI) cmd.Command {
78+
c := &updateCredentialCommand{
79+
api: api,
80+
}
81+
c.SetClientStore(testStore)
82+
return modelcmd.WrapController(c)
83+
}

cmd/juju/cloud/updatecredential.go

+134
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
// Copyright 2016 Canonical Ltd.
2+
// Licensed under the AGPLv3, see LICENCE file for details.
3+
4+
package cloud
5+
6+
import (
7+
"github.com/juju/cmd"
8+
"github.com/juju/errors"
9+
"github.com/juju/gnuflag"
10+
"gopkg.in/juju/names.v2"
11+
12+
apicloud "github.com/juju/juju/api/cloud"
13+
jujucloud "github.com/juju/juju/cloud"
14+
"github.com/juju/juju/cmd/juju/common"
15+
"github.com/juju/juju/cmd/modelcmd"
16+
)
17+
18+
var usageUpdateCredentialSummary = `
19+
Updates a credential for a cloud.`[1:]
20+
21+
var usageUpdateCredentialDetails = `
22+
Updates a named credential for a cloud.
23+
24+
Examples:
25+
juju update-credential aws mysecrets
26+
27+
See also:
28+
add-credential
29+
credentials`[1:]
30+
31+
type updateCredentialCommand struct {
32+
modelcmd.ControllerCommandBase
33+
34+
api credentialAPI
35+
36+
cloud string
37+
credential string
38+
}
39+
40+
// NewUpdateCredentialCommand returns a command to update credential details.
41+
func NewUpdateCredentialCommand() cmd.Command {
42+
return modelcmd.WrapController(&updateCredentialCommand{})
43+
}
44+
45+
// Init implements Command.Init.
46+
func (c *updateCredentialCommand) Init(args []string) error {
47+
if len(args) < 2 {
48+
return errors.New("Usage: juju update-credential <cloud-name> <credential-name>")
49+
}
50+
c.cloud = args[0]
51+
c.credential = args[1]
52+
return cmd.CheckEmpty(args[2:])
53+
}
54+
55+
// Info implements Command.Info
56+
func (c *updateCredentialCommand) Info() *cmd.Info {
57+
return &cmd.Info{
58+
Name: "update-credential",
59+
Args: "<cloud-name> <credential-name>",
60+
Purpose: usageUpdateCredentialSummary,
61+
Doc: usageUpdateCredentialDetails,
62+
}
63+
}
64+
65+
// SetFlags implements Command.SetFlags.
66+
func (c *updateCredentialCommand) SetFlags(f *gnuflag.FlagSet) {
67+
c.ControllerCommandBase.SetFlags(f)
68+
f.StringVar(&c.credential, "credential", "", "Name of credential to update")
69+
f.StringVar(&c.cloud, "cloud", "", "Cloud for which to update the credential")
70+
}
71+
72+
type credentialAPI interface {
73+
UpdateCredential(tag names.CloudCredentialTag, credential jujucloud.Credential) error
74+
Clouds() (map[names.CloudTag]jujucloud.Cloud, error)
75+
Close() error
76+
}
77+
78+
func (c *updateCredentialCommand) getAPI() (credentialAPI, error) {
79+
if c.api != nil {
80+
return c.api, nil
81+
}
82+
api, err := c.NewAPIRoot()
83+
if err != nil {
84+
return nil, errors.Annotate(err, "opening API connection")
85+
}
86+
return apicloud.NewClient(api), nil
87+
}
88+
89+
// Run implements Command.Run
90+
func (c *updateCredentialCommand) Run(ctx *cmd.Context) error {
91+
cred, err := c.ClientStore().CredentialForCloud(c.cloud)
92+
if errors.IsNotFound(err) {
93+
ctx.Infof("No credentials exist for cloud %q", c.cloud)
94+
return nil
95+
} else if err != nil {
96+
return err
97+
}
98+
credToUpdate, ok := cred.AuthCredentials[c.credential]
99+
if !ok {
100+
ctx.Infof("No credential called %q exists for cloud %q", c.credential, c.cloud)
101+
return nil
102+
}
103+
104+
accountDetails, err := c.ClientStore().AccountDetails(c.ControllerName())
105+
if err != nil {
106+
return errors.Trace(err)
107+
}
108+
credentialTag, err := common.ResolveCloudCredentialTag(
109+
names.NewUserTag(accountDetails.User), names.NewCloudTag(c.cloud), c.credential,
110+
)
111+
112+
client, err := c.getAPI()
113+
if err != nil {
114+
return err
115+
}
116+
defer client.Close()
117+
118+
if err := client.UpdateCredential(credentialTag, credToUpdate); err != nil {
119+
return c.processError(client, err)
120+
}
121+
ctx.Infof("Updated credential %q for user %q on cloud %q.", c.credential, accountDetails.User, c.cloud)
122+
return nil
123+
}
124+
125+
func (c *updateCredentialCommand) processError(client credentialAPI, initialErr error) error {
126+
cloudDetails, err := client.Clouds()
127+
if err != nil {
128+
return initialErr
129+
}
130+
if _, ok := cloudDetails[names.NewCloudTag(c.cloud)]; !ok {
131+
return errors.Errorf("cannot update credential %q for cloud %q because controller does not run on the specified cloud", c.credential, c.cloud)
132+
}
133+
return initialErr
134+
}
+155
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
// Copyright 2016 Canonical Ltd.
2+
// Licensed under the AGPLv3, see LICENCE file for details.
3+
4+
package cloud_test
5+
6+
import (
7+
"fmt"
8+
"strings"
9+
10+
jc "github.com/juju/testing/checkers"
11+
gc "gopkg.in/check.v1"
12+
"gopkg.in/juju/names.v2"
13+
14+
jujucloud "github.com/juju/juju/cloud"
15+
"github.com/juju/juju/cmd/juju/cloud"
16+
"github.com/juju/juju/jujuclient"
17+
"github.com/juju/juju/jujuclient/jujuclienttesting"
18+
"github.com/juju/juju/testing"
19+
)
20+
21+
type updateCredentialSuite struct {
22+
testing.BaseSuite
23+
}
24+
25+
var _ = gc.Suite(&updateCredentialSuite{})
26+
27+
func (s *updateCredentialSuite) TestBadArgs(c *gc.C) {
28+
cmd := cloud.NewUpdateCredentialCommand()
29+
_, err := testing.RunCommand(c, cmd)
30+
c.Assert(err, gc.ErrorMatches, "Usage: juju update-credential <cloud-name> <credential-name>")
31+
_, err = testing.RunCommand(c, cmd, "cloud", "credential", "extra")
32+
c.Assert(err, gc.ErrorMatches, `unrecognized args: \["extra"\]`)
33+
}
34+
35+
func (s *updateCredentialSuite) TestMissingCredential(c *gc.C) {
36+
store := &jujuclienttesting.MemStore{
37+
Controllers: map[string]jujuclient.ControllerDetails{
38+
"controller": {},
39+
},
40+
CurrentControllerName: "controller",
41+
Credentials: map[string]jujucloud.CloudCredential{
42+
"aws": {
43+
AuthCredentials: map[string]jujucloud.Credential{
44+
"my-credential": jujucloud.NewCredential(jujucloud.AccessKeyAuthType, nil),
45+
},
46+
},
47+
},
48+
}
49+
cmd := cloud.NewUpdateCredentialCommandForTest(store, nil)
50+
ctx, err := testing.RunCommand(c, cmd, "aws", "foo")
51+
c.Assert(err, jc.ErrorIsNil)
52+
output := testing.Stderr(ctx)
53+
output = strings.Replace(output, "\n", "", -1)
54+
c.Assert(output, gc.Equals, `No credential called "foo" exists for cloud "aws"`)
55+
}
56+
57+
func (s *updateCredentialSuite) TestBadCloudName(c *gc.C) {
58+
store := &jujuclienttesting.MemStore{
59+
Controllers: map[string]jujuclient.ControllerDetails{
60+
"controller": {},
61+
},
62+
CurrentControllerName: "controller",
63+
}
64+
cmd := cloud.NewUpdateCredentialCommandForTest(store, nil)
65+
ctx, err := testing.RunCommand(c, cmd, "somecloud", "foo")
66+
c.Assert(err, jc.ErrorIsNil)
67+
output := testing.Stderr(ctx)
68+
output = strings.Replace(output, "\n", "", -1)
69+
c.Assert(output, gc.Equals, `No credentials exist for cloud "somecloud"`)
70+
}
71+
72+
func (s *updateCredentialSuite) TestUpdate(c *gc.C) {
73+
store := &jujuclienttesting.MemStore{
74+
Controllers: map[string]jujuclient.ControllerDetails{
75+
"controller": {},
76+
},
77+
CurrentControllerName: "controller",
78+
Accounts: map[string]jujuclient.AccountDetails{
79+
"controller": {
80+
User: "admin@local",
81+
},
82+
},
83+
Credentials: map[string]jujucloud.CloudCredential{
84+
"aws": {
85+
AuthCredentials: map[string]jujucloud.Credential{
86+
"my-credential": jujucloud.NewCredential(jujucloud.AccessKeyAuthType, nil),
87+
"another-credential": jujucloud.NewCredential(jujucloud.UserPassAuthType, nil),
88+
},
89+
},
90+
},
91+
}
92+
fake := &fakeUpdateCredentialAPI{
93+
clouds: map[names.CloudTag]jujucloud.Cloud{
94+
names.NewCloudTag("aws"): {},
95+
},
96+
}
97+
cmd := cloud.NewUpdateCredentialCommandForTest(store, fake)
98+
ctx, err := testing.RunCommand(c, cmd, "aws", "my-credential")
99+
c.Assert(err, jc.ErrorIsNil)
100+
output := testing.Stderr(ctx)
101+
output = strings.Replace(output, "\n", "", -1)
102+
c.Assert(output, gc.Equals, `Updated credential "my-credential" for user "admin@local" on cloud "aws".`)
103+
c.Assert(fake.creds, jc.DeepEquals, map[names.CloudCredentialTag]jujucloud.Credential{
104+
names.NewCloudCredentialTag("aws/admin@local/my-credential"): jujucloud.NewCredential(jujucloud.AccessKeyAuthType, nil),
105+
})
106+
}
107+
108+
func (s *updateCredentialSuite) TestInvalidCloud(c *gc.C) {
109+
store := &jujuclienttesting.MemStore{
110+
Controllers: map[string]jujuclient.ControllerDetails{
111+
"controller": {},
112+
},
113+
CurrentControllerName: "controller",
114+
Accounts: map[string]jujuclient.AccountDetails{
115+
"controller": {
116+
User: "admin@local",
117+
},
118+
},
119+
Credentials: map[string]jujucloud.CloudCredential{
120+
"aws": {
121+
AuthCredentials: map[string]jujucloud.Credential{
122+
"my-credential": jujucloud.NewCredential(jujucloud.AccessKeyAuthType, nil),
123+
},
124+
},
125+
},
126+
}
127+
fake := &fakeUpdateCredentialAPI{}
128+
cmd := cloud.NewUpdateCredentialCommandForTest(store, fake)
129+
_, err := testing.RunCommand(c, cmd, "aws", "my-credential")
130+
c.Assert(err, gc.ErrorMatches, `cannot update credential "my-credential" for cloud "aws" because controller does not run on the specified cloud`)
131+
}
132+
133+
type fakeUpdateCredentialAPI struct {
134+
creds map[names.CloudCredentialTag]jujucloud.Credential
135+
clouds map[names.CloudTag]jujucloud.Cloud
136+
}
137+
138+
func (f *fakeUpdateCredentialAPI) UpdateCredential(tag names.CloudCredentialTag, credential jujucloud.Credential) error {
139+
if _, ok := f.clouds[tag.Cloud()]; !ok {
140+
return fmt.Errorf("error")
141+
}
142+
if f.creds == nil {
143+
f.creds = make(map[names.CloudCredentialTag]jujucloud.Credential)
144+
}
145+
f.creds[tag] = credential
146+
return nil
147+
}
148+
149+
func (f *fakeUpdateCredentialAPI) Clouds() (map[names.CloudTag]jujucloud.Cloud, error) {
150+
return f.clouds, nil
151+
}
152+
153+
func (*fakeUpdateCredentialAPI) Close() error {
154+
return nil
155+
}

cmd/juju/commands/main.go

+1
Original file line numberDiff line numberDiff line change
@@ -393,6 +393,7 @@ func registerCommands(r commandRegistry, ctx *cmd.Context) {
393393
r.Register(cloud.NewSetDefaultCredentialCommand())
394394
r.Register(cloud.NewAddCredentialCommand())
395395
r.Register(cloud.NewRemoveCredentialCommand())
396+
r.Register(cloud.NewUpdateCredentialCommand())
396397

397398
// Juju GUI commands.
398399
r.Register(gui.NewGUICommand())

cmd/juju/commands/main_test.go

+1
Original file line numberDiff line numberDiff line change
@@ -516,6 +516,7 @@ var commandNames = []string{
516516
"upload-backup",
517517
"unregister",
518518
"update-clouds",
519+
"update-credential",
519520
"upgrade-charm",
520521
"upgrade-gui",
521522
"upgrade-juju",
+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
// Copyright 2016 Canonical Ltd.
2+
// Licensed under the AGPLv3, see LICENCE file for details.
3+
4+
package featuretests
5+
6+
import (
7+
"github.com/juju/cmd"
8+
"github.com/juju/loggo"
9+
jc "github.com/juju/testing/checkers"
10+
gc "gopkg.in/check.v1"
11+
"gopkg.in/juju/names.v2"
12+
13+
apicloud "github.com/juju/juju/api/cloud"
14+
"github.com/juju/juju/apiserver/params"
15+
"github.com/juju/juju/cloud"
16+
"github.com/juju/juju/cmd/juju/commands"
17+
jujutesting "github.com/juju/juju/juju/testing"
18+
"github.com/juju/juju/jujuclient"
19+
"github.com/juju/juju/testing"
20+
)
21+
22+
type cmdCredentialSuite struct {
23+
jujutesting.JujuConnSuite
24+
}
25+
26+
func (s *cmdCredentialSuite) run(c *gc.C, args ...string) *cmd.Context {
27+
context := testing.Context(c)
28+
command := commands.NewJujuCommand(context)
29+
c.Assert(testing.InitCommand(command, args), jc.ErrorIsNil)
30+
c.Assert(command.Run(context), jc.ErrorIsNil)
31+
loggo.RemoveWriter("warning")
32+
return context
33+
}
34+
35+
func (s *cmdCredentialSuite) TestUpdateCredentialCommand(c *gc.C) {
36+
store := jujuclient.NewFileClientStore()
37+
store.UpdateCredential("dummy", cloud.CloudCredential{
38+
AuthCredentials: map[string]cloud.Credential{
39+
"mine": cloud.NewCredential(cloud.UserPassAuthType, map[string]string{"username": "fred", "password": "secret"}),
40+
},
41+
})
42+
s.run(c, "update-credential", "dummy", "mine")
43+
44+
client := apicloud.NewClient(s.OpenControllerAPI(c))
45+
defer client.Close()
46+
47+
tag := names.NewCloudCredentialTag("dummy/admin@local/mine")
48+
result, err := client.Credentials(tag)
49+
c.Assert(err, jc.ErrorIsNil)
50+
c.Assert(result, jc.DeepEquals, []params.CloudCredentialResult{
51+
{Result: &params.CloudCredential{
52+
AuthType: "userpass",
53+
Attributes: map[string]string{"username": "fred"},
54+
Redacted: []string{"password"},
55+
}},
56+
})
57+
}

featuretests/package_test.go

+1
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ func init() {
3232
gc.Suite(&BakeryStorageSuite{})
3333
gc.Suite(&blockSuite{})
3434
gc.Suite(&cmdControllerSuite{})
35+
gc.Suite(&cmdCredentialSuite{})
3536
gc.Suite(&cmdJujuSuite{})
3637
gc.Suite(&cmdLoginSuite{})
3738
gc.Suite(&cmdModelSuite{})

0 commit comments

Comments
 (0)