Skip to content

Commit

Permalink
Cloud credential validator, api for agent worker.
Browse files Browse the repository at this point in the history
  • Loading branch information
anastasiamac committed Apr 9, 2018
1 parent 6d41053 commit 6e563cd
Show file tree
Hide file tree
Showing 15 changed files with 861 additions and 2 deletions.
4 changes: 4 additions & 0 deletions api/agent/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,10 @@ func (st *State) IsMaster() (bool, error) {

// WatchCredential returns a watcher which reports when the specified
// credential has changed.
// TODO (anastasiamac 2018-04-06) I think that this is wrong and it does not
// look to be used and should be removed:
// * why are we ignoring the tag argument?;
// * what is actually being called since there is no equivalent apiserver.*.WatchCredential?...
func (c *State) WatchCredential(tag names.CloudCredentialTag) (watcher.NotifyWatcher, error) {
var result params.NotifyWatchResult
err := c.facade.FacadeCall("WatchCredential", nil, &result)
Expand Down
10 changes: 10 additions & 0 deletions api/base/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,3 +142,13 @@ type SLASummary struct {
Level string
Owner string
}

// StoredCredential contains information about the cloud credential stored on the controller
// and used by models.
type StoredCredential struct {
// CloudCredential is a cloud credential that identifies cloud credential on the controller.
CloudCredential string

// Valid is a flag that indicates whether the credential is valid.
Valid bool
}
106 changes: 106 additions & 0 deletions api/credentialvalidator/credentialvalidator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// Copyright 2018 Canonical Ltd.
// Licensed under the AGPLv3, see LICENCE file for details.

package credentialvalidator

import (
"github.com/juju/errors"
"github.com/juju/loggo"
"gopkg.in/juju/names.v2"

"github.com/juju/juju/api/base"
apiwatcher "github.com/juju/juju/api/watcher"
"github.com/juju/juju/apiserver/params"
"github.com/juju/juju/watcher"
)

var logger = loggo.GetLogger("juju.api.credentialvalidator")

// Facade provides methods that the Juju client command uses to interact
// with the Juju backend.
type Facade struct {
facade base.FacadeCaller
}

// NewFacade creates a new `Facade` based on an existing authenticated API
// connection.
func NewFacade(caller base.APICaller) *Facade {
return &Facade{base.NewFacadeCaller(caller, "CredentialValidator")}
}

// ModelCredential get cloud credential that a given model uses, including
// useful data such as "is this credential valid"...
// Some clouds do not require a credential and support the "empty" authentication
// type. Models on these clouds will have no credentials set, and thus, will return
// a false as 2nd argument.
func (c *Facade) ModelCredential(modelUUID string) (base.StoredCredential, bool, error) {
// Construct model tag from model uuid.
in := params.Entities{
Entities: []params.Entity{{Tag: names.NewModelTag(modelUUID).String()}},
}

// Call apiserver to get credential for this model.
out := params.ModelCredentialResults{}
emptyResult := base.StoredCredential{}
if err := c.facade.FacadeCall("ModelCredentials", in, &out); err != nil {
return emptyResult, false, errors.Trace(err)
}

// There should be just 1.
if count := len(out.Results); count != 1 {
return emptyResult, false, errors.Errorf("expected 1 model credential for model %q, got %d", modelUUID, count)
}
if err := out.Results[0].Error; err != nil {
return emptyResult, false, errors.Trace(err)
}

result := out.Results[0].Result

modelTag, err := names.ParseModelTag(result.Model)
if err != nil {
return emptyResult, false, errors.Trace(err)
}
if modelTag.Id() != modelUUID {
return emptyResult, false, errors.Errorf("unexpected credential for model %q, expected credential for model %q", modelTag.Id(), modelUUID)
}

if !result.Exists {
return emptyResult, false, nil
}

credentialTag, err := names.ParseCloudCredentialTag(result.CloudCredential)
if err != nil {
return emptyResult, false, errors.Trace(err)
}
return base.StoredCredential{
CloudCredential: credentialTag.Id(),
Valid: result.Valid,
}, true, nil
}

// WatchCredential provides notify watcher that is responsive to changes
// to a given cloud credential.
func (c *Facade) WatchCredential(credentialID string) (watcher.NotifyWatcher, error) {
// Construct credential tag from given id.
in := params.Entities{
Entities: []params.Entity{{Tag: names.NewCloudCredentialTag(credentialID).String()}},
}

// Call apiserver to get the watcher for this credential.
var results params.NotifyWatchResults
err := c.facade.FacadeCall("WatchCredential", in, &results)
if err != nil {
return nil, errors.Trace(err)
}

// There should be just 1.
if count := len(results.Results); count != 1 {
return nil, errors.Errorf("expected 1 watcher for credential %q, got %d", credentialID, count)
}

if err := results.Results[0].Error; err != nil {
return nil, errors.Trace(err)
}
w := apiwatcher.NewNotifyWatcher(c.facade.RawAPICaller(), results.Results[0])
return w, nil
}
243 changes: 243 additions & 0 deletions api/credentialvalidator/credentialvalidator_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
// Copyright 2018 Canonical Ltd.
// Licensed under the AGPLv3, see LICENCE file for details.

package credentialvalidator_test

import (
"fmt"

"github.com/juju/errors"
jc "github.com/juju/testing/checkers"
gc "gopkg.in/check.v1"
names "gopkg.in/juju/names.v2"

"github.com/juju/juju/api/base"
"github.com/juju/juju/api/base/testing"
"github.com/juju/juju/api/credentialvalidator"
"github.com/juju/juju/apiserver/params"
coretesting "github.com/juju/juju/testing"
)

var _ = gc.Suite(&CredentialValidatorSuite{})

type CredentialValidatorSuite struct {
coretesting.BaseSuite
}

func (s *CredentialValidatorSuite) TestModelCredential(c *gc.C) {
apiCaller := testing.APICallerFunc(func(objType string, version int, id, request string, arg, result interface{}) error {
c.Check(objType, gc.Equals, "CredentialValidator")
c.Check(request, gc.Equals, "ModelCredentials")
c.Check(arg, jc.DeepEquals, params.Entities{
Entities: []params.Entity{{Tag: modelTag.String()}},
})
c.Assert(result, gc.FitsTypeOf, &params.ModelCredentialResults{})
*(result.(*params.ModelCredentialResults)) = params.ModelCredentialResults{
Results: []params.ModelCredentialResult{
{Result: &params.ModelCredential{
Model: modelTag.String(),
CloudCredential: credentialTag.String(),
Exists: true,
Valid: true,
}},
},
}
return nil
})

client := credentialvalidator.NewFacade(apiCaller)
found, exists, err := client.ModelCredential(modelUUID)
c.Assert(err, jc.ErrorIsNil)
c.Assert(exists, jc.IsTrue)
c.Assert(found, gc.Equals, base.StoredCredential{CloudCredential: "cloud/user/credential", Valid: true})
}

func (s *CredentialValidatorSuite) TestModelCredentialIsNotNeeded(c *gc.C) {
apiCaller := testing.APICallerFunc(func(objType string, version int, id, request string, arg, result interface{}) error {
*(result.(*params.ModelCredentialResults)) = params.ModelCredentialResults{
Results: []params.ModelCredentialResult{
{Result: &params.ModelCredential{
Model: modelTag.String(),
Exists: false,
}},
},
}
return nil
})

client := credentialvalidator.NewFacade(apiCaller)
_, exists, err := client.ModelCredential(modelUUID)
c.Assert(err, jc.ErrorIsNil)
c.Assert(exists, jc.IsFalse)
}

func (s *CredentialValidatorSuite) TestModelCredentialManyResults(c *gc.C) {
apiCaller := testing.APICallerFunc(func(objType string, version int, id, request string, arg, result interface{}) error {
*(result.(*params.ModelCredentialResults)) = params.ModelCredentialResults{
Results: []params.ModelCredentialResult{
{Result: &params.ModelCredential{}},
{Result: &params.ModelCredential{}},
},
}
return nil
})

client := credentialvalidator.NewFacade(apiCaller)
_, exists, err := client.ModelCredential(modelUUID)
c.Assert(err, gc.ErrorMatches, fmt.Sprintf(`expected 1 model credential for model %q, got 2`, modelUUID))
c.Assert(exists, jc.IsFalse)
}

func (s *CredentialValidatorSuite) TestModelCredentialForWrongModel(c *gc.C) {
diffUUID := "d5757ef7-c86a-4835-84bc-7174af535e25"

apiCaller := testing.APICallerFunc(func(objType string, version int, id, request string, arg, result interface{}) error {
*(result.(*params.ModelCredentialResults)) = params.ModelCredentialResults{
Results: []params.ModelCredentialResult{
{Result: &params.ModelCredential{
// different model UUID than the one supplied to the call
Model: names.NewModelTag(diffUUID).String(),
}},
},
}
return nil
})

client := credentialvalidator.NewFacade(apiCaller)
_, exists, err := client.ModelCredential(modelUUID)
c.Assert(err, gc.ErrorMatches, fmt.Sprintf(`unexpected credential for model %q, expected credential for model %q`, diffUUID, modelUUID))
c.Assert(exists, jc.IsFalse)
}

func (s *CredentialValidatorSuite) TestModelCredentialInvalidModelTag(c *gc.C) {
apiCaller := testing.APICallerFunc(func(objType string, version int, id, request string, arg, result interface{}) error {
*(result.(*params.ModelCredentialResults)) = params.ModelCredentialResults{
Results: []params.ModelCredentialResult{
{Result: &params.ModelCredential{
Model: "some-invalid-string-for-uuid",
}},
},
}
return nil
})

client := credentialvalidator.NewFacade(apiCaller)
_, exists, err := client.ModelCredential(modelUUID)
c.Assert(err, gc.ErrorMatches, `"some-invalid-string-for-uuid" is not a valid tag`)
c.Assert(exists, jc.IsFalse)
}

func (s *CredentialValidatorSuite) TestModelCredentialInvalidCredentialTag(c *gc.C) {
apiCaller := testing.APICallerFunc(func(objType string, version int, id, request string, arg, result interface{}) error {
*(result.(*params.ModelCredentialResults)) = params.ModelCredentialResults{
Results: []params.ModelCredentialResult{
{Result: &params.ModelCredential{
Model: modelTag.String(),
Exists: true,
CloudCredential: "some-invalid-cloud-credential-tag-as-string",
}},
},
}
return nil
})

client := credentialvalidator.NewFacade(apiCaller)
_, exists, err := client.ModelCredential(modelUUID)
c.Assert(err, gc.ErrorMatches, `"some-invalid-cloud-credential-tag-as-string" is not a valid tag`)
c.Assert(exists, jc.IsFalse)
}

func (s *CredentialValidatorSuite) TestModelCredentialError(c *gc.C) {
apiCaller := testing.APICallerFunc(func(objType string, version int, id, request string, arg, result interface{}) error {
*(result.(*params.ModelCredentialResults)) = params.ModelCredentialResults{
Results: []params.ModelCredentialResult{
{Error: &params.Error{Message: "foo"}}},
}
return nil
})

client := credentialvalidator.NewFacade(apiCaller)
_, exists, err := client.ModelCredential(modelUUID)
c.Assert(err, gc.ErrorMatches, "foo")
c.Assert(exists, jc.IsFalse)
}

func (s *CredentialValidatorSuite) TestModelCredentialCallError(c *gc.C) {
apiCaller := testing.APICallerFunc(func(objType string, version int, id, request string, arg, result interface{}) error {
return errors.New("foo")
})

client := credentialvalidator.NewFacade(apiCaller)
_, _, err := client.ModelCredential(modelUUID)
c.Assert(err, gc.ErrorMatches, "foo")
}

func (s *CredentialValidatorSuite) TestWatchCredential(c *gc.C) {
apiCaller := testing.APICallerFunc(func(objType string, version int, id, request string, arg, result interface{}) error {
c.Check(objType, gc.Equals, "CredentialValidator")
c.Check(request, gc.Equals, "WatchCredential")
c.Check(arg, jc.DeepEquals, params.Entities{
Entities: []params.Entity{{Tag: credentialTag.String()}},
})
c.Assert(result, gc.FitsTypeOf, &params.NotifyWatchResults{})
*(result.(*params.NotifyWatchResults)) = params.NotifyWatchResults{
Results: []params.NotifyWatchResult{
{NotifyWatcherId: "notify-watcher-id"},
},
}
return nil
})

client := credentialvalidator.NewFacade(apiCaller)
found, err := client.WatchCredential(credentialID)
c.Assert(err, jc.ErrorIsNil)
c.Assert(found, gc.NotNil)
}

func (s *CredentialValidatorSuite) TestWatchCredentialTooMany(c *gc.C) {
apiCaller := testing.APICallerFunc(func(objType string, version int, id, request string, arg, result interface{}) error {
*(result.(*params.NotifyWatchResults)) = params.NotifyWatchResults{
Results: []params.NotifyWatchResult{
{},
{},
},
}
return nil
})

client := credentialvalidator.NewFacade(apiCaller)
_, err := client.WatchCredential(credentialID)
c.Assert(err, gc.ErrorMatches, fmt.Sprintf("expected 1 watcher for credential %q, got 2", credentialID))
}

func (s *CredentialValidatorSuite) TestWatchCredentialError(c *gc.C) {
apiCaller := testing.APICallerFunc(func(objType string, version int, id, request string, arg, result interface{}) error {
*(result.(*params.NotifyWatchResults)) = params.NotifyWatchResults{
Results: []params.NotifyWatchResult{
{Error: &params.Error{Message: "foo"}}},
}
return nil
})

client := credentialvalidator.NewFacade(apiCaller)
_, err := client.WatchCredential(credentialID)
c.Assert(err, gc.ErrorMatches, "foo")
}

func (s *CredentialValidatorSuite) TestWatchCredentialCallError(c *gc.C) {
apiCaller := testing.APICallerFunc(func(objType string, version int, id, request string, arg, result interface{}) error {
return errors.New("foo")
})

client := credentialvalidator.NewFacade(apiCaller)
_, err := client.WatchCredential(credentialID)
c.Assert(err, gc.ErrorMatches, "foo")
}

var (
modelUUID = "e5757df7-c86a-4835-84bc-7174af535d25"
modelTag = names.NewModelTag(modelUUID)

credentialID = "cloud/user/credential"
credentialTag = names.NewCloudCredentialTag(credentialID)
)
14 changes: 14 additions & 0 deletions api/credentialvalidator/package_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Copyright 2018 Canonical Ltd.
// Licensed under the AGPLv3, see LICENCE file for details.

package credentialvalidator_test

import (
"testing"

gc "gopkg.in/check.v1"
)

func TestAll(t *testing.T) {
gc.TestingT(t)
}
1 change: 1 addition & 0 deletions api/facadeversions.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ var facadeVersions = map[string]int{
"Client": 1,
"Cloud": 2,
"Controller": 5,
"CredentialValidator": 1,
"CrossController": 1,
"CrossModelRelations": 1,
"Deployer": 1,
Expand Down
Loading

0 comments on commit 6e563cd

Please sign in to comment.