diff --git a/api/cloud/cloud.go b/api/cloud/cloud.go index 4ee96393a07..ade7f35f725 100644 --- a/api/cloud/cloud.go +++ b/api/cloud/cloud.go @@ -96,10 +96,20 @@ func (c *Client) UserCredentials(user names.UserTag, cloud names.CloudTag) ([]na // UpdateCloudsCredentials updates clouds credentials content on the controller. // Passed in credentials are keyed on the credential tag. -func (c *Client) UpdateCloudsCredentials(cloudCredentials map[string]jujucloud.Credential) ([]params.UpdateCredentialResult, error) { - var tagged []params.TaggedCredential +// This operation can be forced to ignore validation checks. +func (c *Client) UpdateCloudsCredentials(cloudCredentials map[string]jujucloud.Credential, force bool) ([]params.UpdateCredentialResult, error) { + return c.internalUpdateCloudsCredentials(params.UpdateCredentialArgs{Force: force}, cloudCredentials) +} + +// AddCloudsCredentials adds/uploads clouds credentials content to the controller. +// Passed in credentials are keyed on the credential tag. +func (c *Client) AddCloudsCredentials(cloudCredentials map[string]jujucloud.Credential) ([]params.UpdateCredentialResult, error) { + return c.internalUpdateCloudsCredentials(params.UpdateCredentialArgs{}, cloudCredentials) +} + +func (c *Client) internalUpdateCloudsCredentials(in params.UpdateCredentialArgs, cloudCredentials map[string]jujucloud.Credential) ([]params.UpdateCredentialResult, error) { for tag, credential := range cloudCredentials { - tagged = append(tagged, params.TaggedCredential{ + in.Credentials = append(in.Credentials, params.TaggedCredential{ Tag: tag, Credential: params.CloudCredential{ AuthType: string(credential.AuthType()), @@ -107,7 +117,6 @@ func (c *Client) UpdateCloudsCredentials(cloudCredentials map[string]jujucloud.C }, }) } - in := params.UpdateCredentialArgs{Credentials: tagged} count := len(cloudCredentials) countErr := func(got int) error { @@ -127,7 +136,7 @@ func (c *Client) UpdateCloudsCredentials(cloudCredentials map[string]jujucloud.C } converted := make([]params.UpdateCredentialResult, count) for i, one := range out.Results { - converted[i] = params.UpdateCredentialResult{CredentialTag: tagged[i].Tag, Error: one.Error} + converted[i] = params.UpdateCredentialResult{CredentialTag: in.Credentials[i].Tag, Error: one.Error} } return converted, nil } @@ -146,7 +155,7 @@ func (c *Client) UpdateCloudsCredentials(cloudCredentials map[string]jujucloud.C // stored on the controller. This call validates that the new content works // for all models that are using this credential. func (c *Client) UpdateCredentialsCheckModels(tag names.CloudCredentialTag, credential jujucloud.Credential) ([]params.UpdateCredentialModelResult, error) { - out, err := c.UpdateCloudsCredentials(map[string]jujucloud.Credential{tag.String(): credential}) + out, err := c.UpdateCloudsCredentials(map[string]jujucloud.Credential{tag.String(): credential}, false) if err != nil { return nil, errors.Trace(err) } @@ -158,7 +167,7 @@ func (c *Client) UpdateCredentialsCheckModels(tag names.CloudCredentialTag, cred } // RevokeCredential revokes/deletes a cloud credential. -func (c *Client) RevokeCredential(tag names.CloudCredentialTag) error { +func (c *Client) RevokeCredential(tag names.CloudCredentialTag, force bool) error { var results params.ErrorResults if c.facade.BestAPIVersion() < 3 { @@ -175,7 +184,7 @@ func (c *Client) RevokeCredential(tag names.CloudCredentialTag) error { args := params.RevokeCredentialArgs{ Credentials: []params.RevokeCredentialArg{ - {Tag: tag.String()}, + {Tag: tag.String(), Force: force}, }, } if err := c.facade.FacadeCall("RevokeCredentialsCheckModels", args, &results); err != nil { diff --git a/api/cloud/cloud_test.go b/api/cloud/cloud_test.go index f4c895874e1..48ed6665a27 100644 --- a/api/cloud/cloud_test.go +++ b/api/cloud/cloud_test.go @@ -455,7 +455,7 @@ func (s *cloudSuite) TestRevokeCredential(c *gc.C) { c.Assert(result, gc.FitsTypeOf, ¶ms.ErrorResults{}) c.Assert(a, jc.DeepEquals, params.RevokeCredentialArgs{ Credentials: []params.RevokeCredentialArg{ - {Tag: "cloudcred-foo_bob_bar"}, + {Tag: "cloudcred-foo_bob_bar", Force: true}, }, }) *result.(*params.ErrorResults) = params.ErrorResults{ @@ -472,7 +472,7 @@ func (s *cloudSuite) TestRevokeCredential(c *gc.C) { client := cloudapi.NewClient(apiCaller) tag := names.NewCloudCredentialTag("foo/bob/bar") - err := client.RevokeCredential(tag) + err := client.RevokeCredential(tag, true) c.Assert(err, jc.ErrorIsNil) c.Assert(s.called, jc.IsTrue) } @@ -505,7 +505,7 @@ func (s *cloudSuite) TestRevokeCredentialV2(c *gc.C) { client := cloudapi.NewClient(apiCaller) tag := names.NewCloudCredentialTag("foo/bob/bar") - err := client.RevokeCredential(tag) + err := client.RevokeCredential(tag, false) c.Assert(err, jc.ErrorIsNil) c.Assert(s.called, jc.IsTrue) } @@ -1087,6 +1087,7 @@ func (s *cloudSuite) TestUpdateCloudsCredentials(c *gc.C) { c.Check(request, gc.Equals, "UpdateCredentialsCheckModels") c.Assert(result, gc.FitsTypeOf, ¶ms.UpdateCredentialResults{}) c.Assert(a, jc.DeepEquals, params.UpdateCredentialArgs{ + Force: true, Credentials: []params.TaggedCredential{{ Tag: "cloudcred-foo_bob_bar0", Credential: params.CloudCredential{ @@ -1107,7 +1108,7 @@ func (s *cloudSuite) TestUpdateCloudsCredentials(c *gc.C) { BestVersion: 3, } client := cloudapi.NewClient(apiCaller) - result, err := client.UpdateCloudsCredentials(createCredentials(1)) + result, err := client.UpdateCloudsCredentials(createCredentials(1), true) c.Assert(err, jc.ErrorIsNil) c.Assert(result, gc.DeepEquals, []params.UpdateCredentialResult{{}}) c.Assert(s.called, jc.IsTrue) @@ -1132,7 +1133,7 @@ func (s *cloudSuite) TestUpdateCloudsCredentialsErrorV2(c *gc.C) { BestVersion: 2, } client := cloudapi.NewClient(apiCaller) - errs, err := client.UpdateCloudsCredentials(createCredentials(1)) + errs, err := client.UpdateCloudsCredentials(createCredentials(1), true) c.Assert(err, jc.ErrorIsNil) c.Assert(errs, gc.DeepEquals, []params.UpdateCredentialResult{ {CredentialTag: "cloudcred-foo_bob_bar0", Error: ¶ms.Error{Message: "validation failure", Code: ""}}, @@ -1163,7 +1164,7 @@ func (s *cloudSuite) TestUpdateCloudsCredentialsError(c *gc.C) { BestVersion: 3, } client := cloudapi.NewClient(apiCaller) - errs, err := client.UpdateCloudsCredentials(createCredentials(1)) + errs, err := client.UpdateCloudsCredentials(createCredentials(1), false) c.Assert(err, jc.ErrorIsNil) c.Assert(errs, gc.DeepEquals, []params.UpdateCredentialResult{ {CredentialTag: "cloudcred-foo_bob_bar0", Error: common.ServerError(errors.New("validation failure"))}, @@ -1187,7 +1188,7 @@ func (s *cloudSuite) TestUpdateCloudsCredentialsCallErrorV2(c *gc.C) { BestVersion: 2, } client := cloudapi.NewClient(apiCaller) - result, err := client.UpdateCloudsCredentials(createCredentials(1)) + result, err := client.UpdateCloudsCredentials(createCredentials(1), false) c.Assert(err, gc.ErrorMatches, "scary but true") c.Assert(result, gc.IsNil) c.Assert(s.called, jc.IsTrue) @@ -1209,7 +1210,7 @@ func (s *cloudSuite) TestUpdateCloudsCredentialsCallError(c *gc.C) { BestVersion: 3, } client := cloudapi.NewClient(apiCaller) - result, err := client.UpdateCloudsCredentials(createCredentials(1)) + result, err := client.UpdateCloudsCredentials(createCredentials(1), false) c.Assert(err, gc.ErrorMatches, "scary but true") c.Assert(result, gc.IsNil) c.Assert(s.called, jc.IsTrue) @@ -1237,7 +1238,7 @@ func (s *cloudSuite) TestUpdateCloudsCredentialsManyResultsV2(c *gc.C) { BestVersion: 2, } client := cloudapi.NewClient(apiCaller) - result, err := client.UpdateCloudsCredentials(createCredentials(1)) + result, err := client.UpdateCloudsCredentials(createCredentials(1), false) c.Assert(err, gc.ErrorMatches, `expected 1 result got 2 when updating credentials`) c.Assert(result, gc.IsNil) c.Assert(s.called, jc.IsTrue) @@ -1266,7 +1267,7 @@ func (s *cloudSuite) TestUpdateCloudsCredentialsManyMatchingResultsV2(c *gc.C) { } client := cloudapi.NewClient(apiCaller) in := createCredentials(2) - result, err := client.UpdateCloudsCredentials(in) + result, err := client.UpdateCloudsCredentials(in, false) c.Assert(err, jc.ErrorIsNil) c.Assert(result, gc.HasLen, len(in)) for _, one := range result { @@ -1297,7 +1298,7 @@ func (s *cloudSuite) TestUpdateCloudsCredentialsManyResults(c *gc.C) { BestVersion: 3, } client := cloudapi.NewClient(apiCaller) - result, err := client.UpdateCloudsCredentials(createCredentials(1)) + result, err := client.UpdateCloudsCredentials(createCredentials(1), false) c.Assert(err, gc.ErrorMatches, `expected 1 result got 2 when updating credentials`) c.Assert(result, gc.IsNil) c.Assert(s.called, jc.IsTrue) @@ -1325,7 +1326,7 @@ func (s *cloudSuite) TestUpdateCloudsCredentialsManyMatchingResults(c *gc.C) { } client := cloudapi.NewClient(apiCaller) count := 2 - result, err := client.UpdateCloudsCredentials(createCredentials(count)) + result, err := client.UpdateCloudsCredentials(createCredentials(count), false) c.Assert(err, jc.ErrorIsNil) c.Assert(result, gc.HasLen, count) c.Assert(s.called, jc.IsTrue) @@ -1363,7 +1364,7 @@ func (s *cloudSuite) TestUpdateCloudsCredentialsModelErrors(c *gc.C) { BestVersion: 3, } client := cloudapi.NewClient(apiCaller) - errs, err := client.UpdateCloudsCredentials(createCredentials(1)) + errs, err := client.UpdateCloudsCredentials(createCredentials(1), false) c.Assert(err, jc.ErrorIsNil) c.Assert(errs, gc.DeepEquals, []params.UpdateCredentialResult{ {CredentialTag: "cloudcred-foo_bob_bar", @@ -1380,3 +1381,42 @@ func (s *cloudSuite) TestUpdateCloudsCredentialsModelErrors(c *gc.C) { }) c.Assert(s.called, jc.IsTrue) } + +func (s *cloudSuite) TestAddCloudsCredentials(c *gc.C) { + apiCaller := basetesting.BestVersionCaller{ + APICallerFunc: basetesting.APICallerFunc( + func(objType string, + version int, + id, request string, + a, result interface{}, + ) error { + c.Check(objType, gc.Equals, "Cloud") + c.Check(id, gc.Equals, "") + c.Check(request, gc.Equals, "UpdateCredentialsCheckModels") + c.Assert(result, gc.FitsTypeOf, ¶ms.UpdateCredentialResults{}) + c.Assert(a, jc.DeepEquals, params.UpdateCredentialArgs{ + Credentials: []params.TaggedCredential{{ + Tag: "cloudcred-foo_bob_bar0", + Credential: params.CloudCredential{ + AuthType: "userpass", + Attributes: map[string]string{ + "username": "admin", + "password": "adm1n", + }, + }, + }}}) + *result.(*params.UpdateCredentialResults) = params.UpdateCredentialResults{ + Results: []params.UpdateCredentialResult{{}}, + } + s.called = true + return nil + }, + ), + BestVersion: 3, + } + client := cloudapi.NewClient(apiCaller) + result, err := client.AddCloudsCredentials(createCredentials(1)) + c.Assert(err, jc.ErrorIsNil) + c.Assert(result, gc.DeepEquals, []params.UpdateCredentialResult{{}}) + c.Assert(s.called, jc.IsTrue) +} diff --git a/apiserver/common/credentialcommon/modelcredential.go b/apiserver/common/credentialcommon/modelcredential.go index 5e07460298d..70ae5808cba 100644 --- a/apiserver/common/credentialcommon/modelcredential.go +++ b/apiserver/common/credentialcommon/modelcredential.go @@ -18,7 +18,7 @@ import ( ) // ValidateExistingModelCredential checks if the cloud credential that a given model uses is valid for it. -func ValidateExistingModelCredential(backend PersistentBackend, callCtx context.ProviderCallContext, migrating bool) (params.ErrorResults, error) { +func ValidateExistingModelCredential(backend PersistentBackend, callCtx context.ProviderCallContext, checkCloudInstances bool) (params.ErrorResults, error) { model, err := backend.Model() if err != nil { return params.ErrorResults{}, errors.Trace(err) @@ -38,11 +38,11 @@ func ValidateExistingModelCredential(backend PersistentBackend, callCtx context. return params.ErrorResults{}, errors.NotValidf("credential %q", storedCredential.Name) } credential := cloud.NewCredential(cloud.AuthType(storedCredential.AuthType), storedCredential.Attributes) - return ValidateNewModelCredential(backend, callCtx, credentialTag, &credential, migrating) + return ValidateNewModelCredential(backend, callCtx, credentialTag, &credential, checkCloudInstances) } // ValidateNewModelCredential checks if a new cloud credential could be valid for a given model. -func ValidateNewModelCredential(backend PersistentBackend, callCtx context.ProviderCallContext, credentialTag names.CloudCredentialTag, credential *cloud.Credential, migrating bool) (params.ErrorResults, error) { +func ValidateNewModelCredential(backend PersistentBackend, callCtx context.ProviderCallContext, credentialTag names.CloudCredentialTag, credential *cloud.Credential, checkCloudInstances bool) (params.ErrorResults, error) { openParams, err := buildOpenParams(backend, credentialTag, credential) if err != nil { return params.ErrorResults{}, errors.Trace(err) @@ -55,7 +55,7 @@ func ValidateNewModelCredential(backend PersistentBackend, callCtx context.Provi case state.ModelTypeCAAS: return checkCAASModelCredential(openParams) case state.ModelTypeIAAS: - return checkIAASModelCredential(openParams, backend, callCtx, migrating) + return checkIAASModelCredential(openParams, backend, callCtx, checkCloudInstances) default: return params.ErrorResults{}, errors.NotSupportedf("model type %q", model.Type()) } @@ -74,7 +74,7 @@ func checkCAASModelCredential(brokerParams environs.OpenParams) (params.ErrorRes return params.ErrorResults{}, nil } -func checkIAASModelCredential(openParams environs.OpenParams, backend PersistentBackend, callCtx context.ProviderCallContext, migrating bool) (params.ErrorResults, error) { +func checkIAASModelCredential(openParams environs.OpenParams, backend PersistentBackend, callCtx context.ProviderCallContext, checkCloudInstances bool) (params.ErrorResults, error) { env, err := newEnv(openParams) if err != nil { return params.ErrorResults{}, errors.Trace(err) @@ -83,13 +83,13 @@ func checkIAASModelCredential(openParams environs.OpenParams, backend Persistent // In the future, this check may be extended to other cloud resources, // entities and operation-level authorisations such as interfaces, // ability to CRUD storage, etc. - return checkMachineInstances(backend, env, callCtx, migrating) + return checkMachineInstances(backend, env, callCtx, checkCloudInstances) } // checkMachineInstances compares model machines from state with // the ones reported by the provider using supplied credential. // This only makes sense for non-k8s providers. -func checkMachineInstances(backend PersistentBackend, provider CloudProvider, callCtx context.ProviderCallContext, migrating bool) (params.ErrorResults, error) { +func checkMachineInstances(backend PersistentBackend, provider CloudProvider, callCtx context.ProviderCallContext, checkCloudInstances bool) (params.ErrorResults, error) { fail := func(original error) (params.ErrorResults, error) { return params.ErrorResults{}, original } @@ -149,7 +149,7 @@ func checkMachineInstances(backend PersistentBackend, provider CloudProvider, ca for _, instance := range instances { id := string(instance.Id()) instanceIds.Add(id) - if migrating { + if checkCloudInstances { if _, found := machinesByInstance[id]; !found { results = append(results, serverError(errors.Errorf("no machine with instance %q", id))) } diff --git a/cmd/juju/cloud/addcredential.go b/cmd/juju/cloud/addcredential.go index 7493b700284..07442bc3987 100644 --- a/cmd/juju/cloud/addcredential.go +++ b/cmd/juju/cloud/addcredential.go @@ -642,7 +642,7 @@ func (c *addCredentialCommand) addRemoteCredentials(ctxt *cmd.Context, all map[s return err } defer client.Close() - results, err := client.UpdateCloudsCredentials(verified) + results, err := client.AddCloudsCredentials(verified) if err != nil { logger.Errorf("%v", err) ctxt.Warningf("Could not upload credentials to controller %q", c.ControllerName) diff --git a/cmd/juju/cloud/addcredential_test.go b/cmd/juju/cloud/addcredential_test.go index 293f88fa5c8..3f26e4afe58 100644 --- a/cmd/juju/cloud/addcredential_test.go +++ b/cmd/juju/cloud/addcredential_test.go @@ -851,7 +851,7 @@ credentials: sourceFile := s.createTestCredentialFile(c, expectedContents) called := false - s.api.updateCloudsCredentials = func(cloudCredentials map[string]jujucloud.Credential) ([]params.UpdateCredentialResult, error) { + s.api.addCloudsCredentials = func(cloudCredentials map[string]jujucloud.Credential) ([]params.UpdateCredentialResult, error) { c.Assert(cloudCredentials, gc.HasLen, 1) called = true expectedTag := names.NewCloudCredentialTag(fmt.Sprintf("%v/admin@local/blah", cloudName)).String() diff --git a/cmd/juju/cloud/detectcredentials.go b/cmd/juju/cloud/detectcredentials.go index 2a04abf56b0..de0629348a4 100644 --- a/cmd/juju/cloud/detectcredentials.go +++ b/cmd/juju/cloud/detectcredentials.go @@ -491,7 +491,7 @@ func (c *detectCredentialsCommand) addRemoteCredentials(ctxt *cmd.Context, cloud if len(verified) == 0 { return erred } - result, err := client.UpdateCloudsCredentials(verified) + result, err := client.AddCloudsCredentials(verified) if err != nil { logger.Errorf("%v", err) ctxt.Warningf("Could not upload credentials to controller %q", c.ControllerName) diff --git a/cmd/juju/cloud/detectcredentials_test.go b/cmd/juju/cloud/detectcredentials_test.go index d1f41f41738..37d080caf17 100644 --- a/cmd/juju/cloud/detectcredentials_test.go +++ b/cmd/juju/cloud/detectcredentials_test.go @@ -372,7 +372,7 @@ func (s *detectCredentialsSuite) TestRemoteLoad(c *gc.C) { s.setupStore(c) cloudName := "test-cloud" called := false - s.api.updateCloudsCredentials = func(cloudCredentials map[string]jujucloud.Credential) ([]params.UpdateCredentialResult, error) { + s.api.addCloudsCredentials = func(cloudCredentials map[string]jujucloud.Credential) ([]params.UpdateCredentialResult, error) { c.Assert(cloudCredentials, gc.HasLen, 1) called = true expectedTag := names.NewCloudCredentialTag(fmt.Sprintf("%v/admin@local/blah", cloudName)).String() @@ -446,7 +446,7 @@ func (s *detectCredentialsSuite) assertAutoloadCredentials(c *gc.C, expectedStde s.setupStore(c) cloudName := "test-cloud" called := false - s.api.updateCloudsCredentials = func(cloudCredentials map[string]jujucloud.Credential) ([]params.UpdateCredentialResult, error) { + s.api.addCloudsCredentials = func(cloudCredentials map[string]jujucloud.Credential) ([]params.UpdateCredentialResult, error) { c.Assert(cloudCredentials, gc.HasLen, 1) called = true expectedTag := names.NewCloudCredentialTag(fmt.Sprintf("%v/admin@local/blah", cloudName)).String() diff --git a/cmd/juju/cloud/removecredential.go b/cmd/juju/cloud/removecredential.go index dd168ecfd1d..ebcd5629c2a 100644 --- a/cmd/juju/cloud/removecredential.go +++ b/cmd/juju/cloud/removecredential.go @@ -31,6 +31,9 @@ type removeCredentialCommand struct { remoteCloudFound bool localCloudFound bool credentialAPIFunc func() (RemoveCredentialAPI, error) + + // Force determines whether the remove will be forced on the controller side. + Force bool } // RemoveCredentialAPI defines api Cloud facade that can remove a remote credential. @@ -38,7 +41,7 @@ type RemoveCredentialAPI interface { // Clouds returns all remote clouds that the currently logged-in user can access. Clouds() (map[names.CloudTag]jujucloud.Cloud, error) // RevokeCredential removes remote credential. - RevokeCredential(tag names.CloudCredentialTag) error + RevokeCredential(tag names.CloudCredentialTag, force bool) error // BestAPIVersion returns current best api version. BestAPIVersion() int // Close closes api client. @@ -55,19 +58,31 @@ material, can be listed with `[1:] + "`juju credentials`" + `. Use --controller option to remove credentials from a controller. +When removing cloud credential from a controller, Juju performs additional +checks to ensure that there are no models using this credential. +Occasionally, these check may not be desired by the user and can be by-passed using --force. +If force remove was performed and some models were still using the credential, these models +will be left with un-reachable machines. +Consequently, it is not recommended as a default remove action. +Models with un-reachable machines are most commonly fixed by using another cloud credential, +see ' + "'juju set-credential'" + ' for more information. + + Use --client option to remove credentials from the current client. Examples: juju remove-credential rackspace credential_name juju remove-credential rackspace credential_name --client juju remove-credential rackspace credential_name -c mycontroller + juju remove-credential rackspace credential_name -c mycontroller --force See also: - credentials add-credential - update-credential + autoload-credentials + credentials default-credential - autoload-credentials` + set-credential + update-credential` // NewRemoveCredentialCommand returns a command to remove a named credential for a cloud. func NewRemoveCredentialCommand() cmd.Command { @@ -113,6 +128,7 @@ func (c *removeCredentialCommand) Init(args []string) (err error) { func (c *removeCredentialCommand) SetFlags(f *gnuflag.FlagSet) { c.OptionalControllerCommand.SetFlags(f) + f.BoolVar(&c.Force, "force", false, "Force remove controller side credential, ignore validation errors") } func (c *removeCredentialCommand) Run(ctxt *cmd.Context) error { @@ -204,7 +220,7 @@ func (c *removeCredentialCommand) removeFromController(ctxt *cmd.Context, client ctxt.Warningf("Could not remove controller credential %v for user %v on cloud %v: %v", c.credential, accountDetails.User, c.cloud, errors.NotValidf("cloud credential ID %q", id)) return cmd.ErrSilent } - if err := client.RevokeCredential(names.NewCloudCredentialTag(id)); err != nil { + if err := client.RevokeCredential(names.NewCloudCredentialTag(id), c.Force); err != nil { return errors.Annotate(err, "could not remove remote credential") } ctxt.Infof("Credential %q for cloud %q removed from the controller %q.", c.credential, c.cloud, c.ControllerName) diff --git a/cmd/juju/cloud/removecredential_test.go b/cmd/juju/cloud/removecredential_test.go index 443a2dd2682..66e04de552b 100644 --- a/cmd/juju/cloud/removecredential_test.go +++ b/cmd/juju/cloud/removecredential_test.go @@ -194,6 +194,21 @@ func (s *removeCredentialSuite) TestRemoveRemoteCredentialFail(c *gc.C) { c.Assert(err, gc.Equals, cmd.ErrSilent) c.Assert(cmdtesting.Stdout(ctx), gc.Equals, "") c.Assert(cmdtesting.Stderr(ctx), gc.Equals, "Found remote cloud \"somecloud\" from the controller.\nERROR could not remove remote credential: kaboom\n") + s.fakeClient.CheckCallNames(c, "Clouds", "RevokeCredential", "Close") + s.fakeClient.CheckCall(c, 1, "RevokeCredential", names.NewCloudCredentialTag("somecloud/admin/foo"), false) +} + +func (s *removeCredentialSuite) TestRemoveRemoteCredentialForce(c *gc.C) { + store := s.setupStore(c) + s.setupClientForRemote(c) + s.fakeClient.revokeCredentialF = func(tag names.CloudCredentialTag) error { + return nil + } + command := cloud.NewRemoveCredentialCommandForTest(store, s.cloudByNameFunc, s.clientF) + _, err := cmdtesting.RunCommand(c, command, "somecloud", "foo", "-c", "controller", "--force") + c.Assert(err, jc.ErrorIsNil) + s.fakeClient.CheckCallNames(c, "Clouds", "RevokeCredential", "Close") + s.fakeClient.CheckCall(c, 1, "RevokeCredential", names.NewCloudCredentialTag("somecloud/admin/foo"), true) } type fakeRemoveCredentialAPI struct { @@ -213,8 +228,8 @@ func (f *fakeRemoveCredentialAPI) BestAPIVersion() int { return f.v } -func (f *fakeRemoveCredentialAPI) RevokeCredential(c names.CloudCredentialTag) error { - f.AddCall("RevokeCredential", c) +func (f *fakeRemoveCredentialAPI) RevokeCredential(c names.CloudCredentialTag, force bool) error { + f.AddCall("RevokeCredential", c, force) return f.revokeCredentialF(c) } diff --git a/cmd/juju/cloud/updatecredential.go b/cmd/juju/cloud/updatecredential.go index 6110c1387c7..cd94a32c1a1 100644 --- a/cmd/juju/cloud/updatecredential.go +++ b/cmd/juju/cloud/updatecredential.go @@ -37,6 +37,15 @@ cloud-specific credential on a controller as well as the one from this client. Use --controller option to update a credential definition on a controller. +When updating cloud credential on a controller, Juju performs additional +checks to ensure that the models that use this credential can still +access cloud instances after the update. Occasionally, these checks may not be desired +by the user and can be by-passed using --force option. +Force update may leave some models with un-reachable machines. +Consequently, it is not recommended as a default update action. +Models with un-reachable machines are most commonly fixed by using another cloud credential, +see ' + "'juju set-credential'" + ' for more information. + Use --client to update a credential definition on this client. If a user will use a different client, say a different laptop, the update will not affect that client's (laptop's) copy. @@ -51,11 +60,13 @@ Examples: juju update-credential -f mine.yaml --client juju update-credential aws -f mine.yaml juju update-credential azure --region brazilsouth -f mine.yaml + juju update-credential -f mine.yaml --controller mycontroller --force See also: add-credential + credentials remove-credential - credentials`[1:] + set-credential`[1:] type updateCredentialCommand struct { modelcmd.OptionalControllerCommand @@ -70,6 +81,9 @@ type updateCredentialCommand struct { // Region is the region that credentials will be validated for before an update. Region string + + // Force determines whether the update will be forced on the controller side. + Force bool } // NewUpdateCredentialCommand returns a command to update credential details. @@ -123,11 +137,13 @@ func (c *updateCredentialCommand) SetFlags(f *gnuflag.FlagSet) { f.StringVar(&c.CredentialsFile, "f", "", "The YAML file containing credential details to update") f.StringVar(&c.CredentialsFile, "file", "", "The YAML file containing credential details to update") f.StringVar(&c.Region, "region", "", "Cloud region that credential is valid for") + f.BoolVar(&c.Force, "force", false, "Force update controller side credential, ignore validation errors") } type CredentialAPI interface { Clouds() (map[names.CloudTag]jujucloud.Cloud, error) - UpdateCloudsCredentials(cloudCredentials map[string]jujucloud.Credential) ([]params.UpdateCredentialResult, error) + AddCloudsCredentials(cloudCredentials map[string]jujucloud.Credential) ([]params.UpdateCredentialResult, error) + UpdateCloudsCredentials(cloudCredentials map[string]jujucloud.Credential, force bool) ([]params.UpdateCredentialResult, error) BestAPIVersion() int Close() error } @@ -368,7 +384,7 @@ func (c *updateCredentialCommand) updateRemoteCredentials(ctx *cmd.Context, upda if len(verified) == 0 { return erred } - results, err := client.UpdateCloudsCredentials(verified) + results, err := client.UpdateCloudsCredentials(verified, c.Force) if err != nil { logger.Errorf("%v", err) ctx.Warningf("Could not update credentials remotely, on controller %q", c.ControllerName) diff --git a/cmd/juju/cloud/updatecredential_test.go b/cmd/juju/cloud/updatecredential_test.go index 2a327920965..b09b55b7df0 100644 --- a/cmd/juju/cloud/updatecredential_test.go +++ b/cmd/juju/cloud/updatecredential_test.go @@ -336,7 +336,7 @@ func (s *updateCredentialSuite) TestUpdateRemoteCredentialWithFilePath(c *gc.C) // Double check credential from local cache does not contain contents. We expect it to be file path. c.Assert(s.store.Credentials["google"].AuthCredentials["gce"].Attributes()["file"], gc.Not(gc.Equals), string(contents)) - s.api.updateCloudsCredentials = func(cloudCredentials map[string]jujucloud.Credential) ([]params.UpdateCredentialResult, error) { + s.api.updateCloudsCredentials = func(cloudCredentials map[string]jujucloud.Credential, f bool) ([]params.UpdateCredentialResult, error) { c.Assert(cloudCredentials, gc.HasLen, 1) for k, v := range cloudCredentials { c.Assert(k, gc.DeepEquals, names.NewCloudCredentialTag("google/admin@local/gce").String()) @@ -391,7 +391,7 @@ credentials: } func (s *updateCredentialSuite) TestUpdateRemote(c *gc.C) { - s.api.updateCloudsCredentials = func(cloudCredentials map[string]jujucloud.Credential) ([]params.UpdateCredentialResult, error) { + s.api.updateCloudsCredentials = func(cloudCredentials map[string]jujucloud.Credential, f bool) ([]params.UpdateCredentialResult, error) { c.Assert(cloudCredentials, gc.HasLen, 1) expectedTag := names.NewCloudCredentialTag("aws/admin@local/my-credential").String() for k, v := range cloudCredentials { @@ -461,7 +461,7 @@ func (s *updateCredentialSuite) TestUpdateRemoteResultNotUserCloudError(c *gc.C) } func (s *updateCredentialSuite) TestUpdateRemoteResultError(c *gc.C) { - s.api.updateCloudsCredentials = func(cloudCredentials map[string]jujucloud.Credential) ([]params.UpdateCredentialResult, error) { + s.api.updateCloudsCredentials = func(cloudCredentials map[string]jujucloud.Credential, f bool) ([]params.UpdateCredentialResult, error) { return nil, errors.New("kaboom") } s.storeWithCredentials(c) @@ -471,8 +471,18 @@ func (s *updateCredentialSuite) TestUpdateRemoteResultError(c *gc.C) { c.Assert(c.GetTestLog(), jc.Contains, `Could not update credentials remotely, on controller "controller"`) } +func (s *updateCredentialSuite) TestUpdateRemoteForce(c *gc.C) { + s.api.updateCloudsCredentials = func(cloudCredentials map[string]jujucloud.Credential, f bool) ([]params.UpdateCredentialResult, error) { + c.Assert(f, jc.IsTrue) + return nil, nil + } + s.storeWithCredentials(c) + _, err := cmdtesting.RunCommand(c, s.testCommand, "aws", "my-credential", "-c", "controller", "--force") + c.Assert(err, jc.ErrorIsNil) +} + func (s *updateCredentialSuite) TestUpdateRemoteWithModels(c *gc.C) { - s.api.updateCloudsCredentials = func(cloudCredentials map[string]jujucloud.Credential) ([]params.UpdateCredentialResult, error) { + s.api.updateCloudsCredentials = func(cloudCredentials map[string]jujucloud.Credential, f bool) ([]params.UpdateCredentialResult, error) { return []params.UpdateCredentialResult{ { CredentialTag: names.NewCloudCredentialTag("aws/admin/my-credential").String(), @@ -519,7 +529,8 @@ Use ‘juju set-credential’ to change credential for these models before repea type fakeUpdateCredentialAPI struct { v int - updateCloudsCredentials func(cloudCredentials map[string]jujucloud.Credential) ([]params.UpdateCredentialResult, error) + updateCloudsCredentials func(cloudCredentials map[string]jujucloud.Credential, force bool) ([]params.UpdateCredentialResult, error) + addCloudsCredentials func(cloudCredentials map[string]jujucloud.Credential) ([]params.UpdateCredentialResult, error) clouds func() (map[names.CloudTag]jujucloud.Cloud, error) } @@ -531,8 +542,12 @@ func (f *fakeUpdateCredentialAPI) BestAPIVersion() int { return f.v } -func (f *fakeUpdateCredentialAPI) UpdateCloudsCredentials(c map[string]jujucloud.Credential) ([]params.UpdateCredentialResult, error) { - return f.updateCloudsCredentials(c) +func (f *fakeUpdateCredentialAPI) UpdateCloudsCredentials(c map[string]jujucloud.Credential, force bool) ([]params.UpdateCredentialResult, error) { + return f.updateCloudsCredentials(c, force) +} + +func (f *fakeUpdateCredentialAPI) AddCloudsCredentials(c map[string]jujucloud.Credential) ([]params.UpdateCredentialResult, error) { + return f.addCloudsCredentials(c) } func (f *fakeUpdateCredentialAPI) Clouds() (map[names.CloudTag]jujucloud.Cloud, error) {