Skip to content

Commit

Permalink
Merge pull request #4027 from wallyworld/find-endpoints-command
Browse files Browse the repository at this point in the history
Add find-endpoints command

Two major changes:

1. juju find-endpoints which allows the user to see what services have been offered
Includes:
- CLI command
- api and apiserver components
- feature tests

TODO - we still don't filter on endpoint name or interface

2. fix issues with juju show-endpoint command
- description column too narrow
- words in description were omitted

(Review request: http://reviews.vapour.ws/r/3446/)
  • Loading branch information
jujubot committed Jan 4, 2016
2 parents 24449cd + a848514 commit fbafea7
Show file tree
Hide file tree
Showing 23 changed files with 953 additions and 32 deletions.
58 changes: 58 additions & 0 deletions api/crossmodel/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package crossmodel

import (
"github.com/juju/errors"
"github.com/juju/names"
"gopkg.in/juju/charm.v6-unstable"

"github.com/juju/juju/api/base"
Expand Down Expand Up @@ -63,6 +64,63 @@ func (c *Client) ServiceOffer(url string) (params.ServiceOffer, error) {
return theOne.Result, nil
}

// FindServiceOffers returns all service offers matching the supplied filter.
func (c *Client) FindServiceOffers(filters ...crossmodel.ServiceOfferFilter) ([]params.ServiceOffer, error) {
// We need at least one filter. The default filter will list all local services.
if len(filters) == 0 {
return nil, errors.New("at least one filter must be specified")
}
var paramsFilter params.OfferFilterParams
for _, f := range filters {
urlParts, err := crossmodel.ParseServiceURLParts(f.ServiceURL)
if err != nil {
return nil, err
}
if urlParts.Directory == "" {
return nil, errors.Errorf("service offer filter needs a directory: %#v", f)
}
// TODO(wallyworld) - include allowed users
filterTerm := params.OfferFilter{
ServiceURL: f.ServiceURL,
ServiceName: f.ServiceName,
ServiceDescription: f.ServiceDescription,
SourceLabel: f.SourceLabel,
}
if f.SourceEnvUUID != "" {
filterTerm.SourceEnvUUIDTag = names.NewEnvironTag(f.SourceEnvUUID).String()
}
filterTerm.Endpoints = make([]params.EndpointFilterAttributes, len(f.Endpoints))
for i, ep := range f.Endpoints {
filterTerm.Endpoints[i].Name = ep.Name
filterTerm.Endpoints[i].Interface = ep.Interface
filterTerm.Endpoints[i].Role = ep.Role
}
paramsFilter.Filters = append(paramsFilter.Filters, params.OfferFilters{
Directory: urlParts.Directory,
Filters: []params.OfferFilter{filterTerm},
})
}

out := params.FindServiceOffersResults{}
err := c.facade.FacadeCall("FindServiceOffers", paramsFilter, &out)
if err != nil {
return nil, errors.Trace(err)
}

result := out.Results
// Since only one filters set was sent, expecting only one back
if len(result) != 1 {
return nil, errors.Errorf("expected to find one result but found %d", len(result))

}

theOne := result[0]
if theOne.Error != nil {
return nil, errors.Trace(theOne.Error)
}
return theOne.Offers, nil
}

// ListOffers gets all remote services that have been offered from this Juju model.
// Each returned service satisfies at least one of the the specified filters.
func (c *Client) ListOffers(filters ...crossmodel.OfferedServiceFilter) ([]crossmodel.OfferedServiceDetailsResult, error) {
Expand Down
166 changes: 166 additions & 0 deletions api/crossmodel/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -395,3 +395,169 @@ func (s *crossmodelMockSuite) TestListFacadeCallError(c *gc.C) {
c.Assert(errors.Cause(err), gc.ErrorMatches, msg)
c.Assert(results, gc.IsNil)
}

func (s *crossmodelMockSuite) TestFind(c *gc.C) {
directoryName := "local"
charmName := "db2"
serviceName := fmt.Sprintf("hosted-%s", charmName)
url := fmt.Sprintf("%s:/u/fred/%s", directoryName, serviceName)
endpoints := []params.RemoteEndpoint{{Name: "endPointA"}}
relations := []charm.Relation{{Name: "endPointA", Interface: "http"}}

filter := model.ServiceOfferFilter{
ServiceOffer: model.ServiceOffer{
ServiceURL: fmt.Sprintf("%s:/u/fred/%s", directoryName, serviceName),
ServiceName: fmt.Sprintf("hosted-%s", charmName),
Endpoints: relations,
},
}

called := false
apiCaller := basetesting.APICallerFunc(
func(objType string,
version int,
id, request string,
a, result interface{},
) error {
c.Check(objType, gc.Equals, "CrossModelRelations")
c.Check(id, gc.Equals, "")
c.Check(request, gc.Equals, "FindServiceOffers")

called = true
args, ok := a.(params.OfferFilterParams)
c.Assert(ok, jc.IsTrue)
c.Assert(args.Filters, gc.HasLen, 1)
c.Assert(args.Filters[0].Directory, gc.Equals, "local")
c.Assert(args.Filters[0].Filters, jc.DeepEquals, []params.OfferFilter{{
ServiceURL: filter.ServiceURL,
ServiceName: filter.ServiceName,
Endpoints: []params.EndpointFilterAttributes{{
Name: "endPointA",
Interface: "http",
}},
}})

if results, ok := result.(*params.FindServiceOffersResults); ok {
offer := params.ServiceOffer{
ServiceURL: url,
ServiceName: serviceName,
Endpoints: endpoints,
}
results.Results = []params.ServiceOfferResults{{
Offers: []params.ServiceOffer{offer},
}}
}

return nil
})

client := crossmodel.NewClient(apiCaller)
results, err := client.FindServiceOffers(filter)
c.Assert(err, jc.ErrorIsNil)
c.Assert(called, jc.IsTrue)
c.Assert(results, jc.DeepEquals, []params.ServiceOffer{{
ServiceName: serviceName,
ServiceURL: url,
Endpoints: endpoints,
}})
}

func (s *crossmodelMockSuite) TestFindNoFilter(c *gc.C) {
apiCaller := basetesting.APICallerFunc(
func(objType string,
version int,
id, request string,
a, result interface{},
) error {
c.Fail()
return nil
})

client := crossmodel.NewClient(apiCaller)
_, err := client.FindServiceOffers()
c.Assert(err, gc.ErrorMatches, "at least one filter must be specified")
}

func (s *crossmodelMockSuite) TestFindMultipleResults(c *gc.C) {
called := false
apiCaller := basetesting.APICallerFunc(
func(objType string,
version int,
id, request string,
a, result interface{},
) error {
c.Check(objType, gc.Equals, "CrossModelRelations")
c.Check(id, gc.Equals, "")
c.Check(request, gc.Equals, "FindServiceOffers")

called = true
if results, ok := result.(*params.FindServiceOffersResults); ok {
results.Results = []params.ServiceOfferResults{{}, {}}
}

return nil
})

client := crossmodel.NewClient(apiCaller)
filter := model.ServiceOfferFilter{
ServiceOffer: model.ServiceOffer{ServiceURL: "local:"},
}
_, err := client.FindServiceOffers(filter)
c.Assert(errors.Cause(err), gc.ErrorMatches, ".*expected to find one result but found 2.*")
c.Assert(called, jc.IsTrue)
}

func (s *crossmodelMockSuite) TestFindError(c *gc.C) {
msg := "find failure"
called := false
apiCaller := basetesting.APICallerFunc(
func(objType string,
version int,
id, request string,
a, result interface{},
) error {
c.Check(objType, gc.Equals, "CrossModelRelations")
c.Check(id, gc.Equals, "")
c.Check(request, gc.Equals, "FindServiceOffers")

called = true
if results, ok := result.(*params.FindServiceOffersResults); ok {
results.Results = []params.ServiceOfferResults{{
Error: common.ServerError(errors.New(msg)),
}}
}

return nil
})

client := crossmodel.NewClient(apiCaller)
filter := model.ServiceOfferFilter{
ServiceOffer: model.ServiceOffer{ServiceURL: "local:"},
}
_, err := client.FindServiceOffers(filter)
c.Assert(errors.Cause(err), gc.ErrorMatches, msg)
c.Assert(called, jc.IsTrue)
}

func (s *crossmodelMockSuite) TestFindFacadeCallError(c *gc.C) {
msg := "facade failure"
apiCaller := basetesting.APICallerFunc(
func(objType string,
version int,
id, request string,
a, result interface{},
) error {
c.Check(objType, gc.Equals, "CrossModelRelations")
c.Check(id, gc.Equals, "")
c.Check(request, gc.Equals, "FindServiceOffers")

return errors.New(msg)
})
client := crossmodel.NewClient(apiCaller)
filter := model.ServiceOfferFilter{
ServiceOffer: model.ServiceOffer{ServiceURL: "local:"},
}
results, err := client.FindServiceOffers(filter)
c.Assert(errors.Cause(err), gc.ErrorMatches, msg)
c.Assert(results, gc.IsNil)
}
1 change: 1 addition & 0 deletions api/crossmodel/servicedirectory.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ func (s *serviceOffersAPI) ListOffers(directory string, filters ...crossmodel.Se
eps := make([]params.EndpointFilterAttributes, len(filter.Endpoints))
for j, ep := range filter.Endpoints {
eps[j] = params.EndpointFilterAttributes{
Name: ep.Name,
Interface: ep.Interface,
Role: ep.Role,
}
Expand Down
19 changes: 19 additions & 0 deletions apiserver/crossmodel/crossmodel.go
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,25 @@ func (api *API) ServiceOffers(filter params.ServiceURLs) (params.ServiceOffersRe
return params.ServiceOffersResults{results}, nil
}

// FindServiceOffers gets details about remote services that match given filter.
func (api *API) FindServiceOffers(filters params.OfferFilterParams) (params.FindServiceOffersResults, error) {
var result params.FindServiceOffersResults
result.Results = make([]params.ServiceOfferResults, len(filters.Filters))

for i, filter := range filters.Filters {
offers, err := api.backend.ListDirectoryOffers(filter)
if err == nil && offers.Error != nil {
err = offers.Error
}
if err != nil {
result.Results[i].Error = common.ServerError(err)
continue
}
result.Results[i] = offers
}
return result, nil
}

// ListOffers gets all remote services that have been offered from this Juju model.
// Each returned service satisfies at least one of the the specified filters.
func (api *API) ListOffers(args params.OfferedServiceFilters) (params.ListOffersResults, error) {
Expand Down
72 changes: 72 additions & 0 deletions apiserver/crossmodel/crossmodel_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -285,3 +285,75 @@ func (s *crossmodelSuite) TestListError(c *gc.C) {
s.serviceBackend.CheckCallNames(c, listOfferedServicesBackendCall)
c.Assert(found.Results[0].Error, gc.ErrorMatches, fmt.Sprintf("%v", msg))
}

func (s *crossmodelSuite) TestFind(c *gc.C) {
serviceName := "test"
url := "local:/u/fred/hosted-db2"

filter := params.OfferFilterParams{
Filters: []params.OfferFilters{
{
Directory: "local",
Filters: []params.OfferFilter{
{
ServiceURL: "local:/u/fred/hosted-db2",
ServiceName: "test",
},
},
},
},
}
anOffer := params.ServiceOffer{
ServiceName: serviceName,
ServiceDescription: "description",
ServiceURL: url,
SourceEnvironTag: "environment-",
SourceLabel: "label",
Endpoints: []params.RemoteEndpoint{{Name: "db"}},
}

s.serviceBackend.listDirectoryOffers = func(filter params.OfferFilters) (params.ServiceOfferResults, error) {
c.Assert(filter, jc.DeepEquals, params.OfferFilters{
Directory: "local",
Filters: []params.OfferFilter{
{
ServiceURL: "local:/u/fred/hosted-db2",
ServiceName: "test",
},
},
})
return params.ServiceOfferResults{Offers: []params.ServiceOffer{anOffer}}, nil
}

found, err := s.api.FindServiceOffers(filter)
c.Assert(err, jc.ErrorIsNil)
c.Assert(found, gc.DeepEquals,
params.FindServiceOffersResults{
Results: []params.ServiceOfferResults{
{
Offers: []params.ServiceOffer{
{
ServiceName: serviceName,
ServiceDescription: "description",
ServiceURL: url,
SourceEnvironTag: "environment-",
SourceLabel: "label",
Endpoints: []params.RemoteEndpoint{{Name: "db"}}}},
}}})
s.serviceBackend.CheckCallNames(c, listDirectoryOffersBackendCall)
}

func (s *crossmodelSuite) TestFindError(c *gc.C) {
filter := params.OfferFilterParams{Filters: []params.OfferFilters{{}}}
msg := "fail"

s.serviceBackend.listDirectoryOffers = func(filter params.OfferFilters) (params.ServiceOfferResults, error) {
return params.ServiceOfferResults{}, errors.New(msg)
}

found, err := s.api.FindServiceOffers(filter)
c.Assert(err, jc.ErrorIsNil)
c.Assert(found.Results, gc.HasLen, 1)
c.Assert(found.Results[0].Error, gc.ErrorMatches, fmt.Sprintf(".*%v.*", msg))
s.serviceBackend.CheckCallNames(c, listDirectoryOffersBackendCall)
}
1 change: 1 addition & 0 deletions apiserver/crossmodel/servicedirectory.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ func makeOfferFilterFromParams(filters []params.OfferFilter) ([]crossmodel.Servi
}
offerFilters[i].SourceEnvUUID = envTag.Id()
}
// TODO(wallyworld) - add support for Endpoint filter attribute
}
return offerFilters, nil
}
Expand Down
13 changes: 13 additions & 0 deletions apiserver/params/crossmodel.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,18 @@ import (
"gopkg.in/juju/charm.v6-unstable"
)

// OfferFilterParams contains filters used to query service offers
// from one or more directories.
type OfferFilterParams struct {
Filters []OfferFilters `json:"filters"`
}

// EndpointFilterAttributes is used to filter offers matching the
// specified endpoint criteria.
type EndpointFilterAttributes struct {
Role charm.RelationRole `json:"role"`
Interface string `json:"interface"`
Name string `json:"name"`
}

// OfferFilters is used to query offers in a service directory.
Expand Down Expand Up @@ -95,6 +102,12 @@ type ServiceOffersParams struct {
Offers []ServiceOfferParams `json:"offers"`
}

// FindServiceOffersResults is a result of finding remote service offers.
type FindServiceOffersResults struct {
// Results contains service offers matching each filter.
Results []ServiceOfferResults `json:"results"`
}

// ServiceOfferResult is a result of listing a remote service offer.
type ServiceOfferResult struct {
// Result contains service offer information.
Expand Down
1 change: 1 addition & 0 deletions cmd/juju/commands/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ func registerCommands(r commandRegistry, ctx *cmd.Context) {
r.Register(crossmodel.NewOfferCommand())
r.Register(crossmodel.NewShowOfferedEndpointCommand())
r.Register(crossmodel.NewListEndpointsCommand())
r.Register(crossmodel.NewFindEndpointsCommand())

// Destruction commands.
r.Register(newRemoveRelationCommand())
Expand Down

0 comments on commit fbafea7

Please sign in to comment.