From 92571a40b1d7453e46e0e883afa97506cfa2d1db Mon Sep 17 00:00:00 2001 From: Ian Booth Date: Thu, 27 Apr 2017 15:01:32 +1000 Subject: [PATCH] Combine applicationoffers and remoteendpoints facades onto a single controller facade --- api/applicationoffers/client.go | 60 +++ api/applicationoffers/client_test.go | 321 +++++++++++++ api/facadeversions.go | 1 - api/remoteendpoints/client.go | 84 ---- api/remoteendpoints/client_test.go | 347 -------------- api/remoteendpoints/package_test.go | 14 - apiserver/allfacades.go | 2 - .../applicationoffers/applicationoffers.go | 116 ++++- .../applicationoffers_test.go | 429 ++++++++++++++++- .../base.go} | 28 +- apiserver/applicationoffers/base_test.go | 6 +- apiserver/applicationoffers/mock_test.go | 28 +- .../state.go | 2 +- apiserver/remoteendpoints/base_test.go | 71 --- apiserver/remoteendpoints/export_test.go | 8 - apiserver/remoteendpoints/mock_test.go | 158 ------- apiserver/remoteendpoints/package_test.go | 14 - apiserver/remoteendpoints/remoteendpoints.go | 147 ------ .../remoteendpoints/remoteendpoints_test.go | 446 ------------------ apiserver/restrict_controller.go | 2 +- apiserver/restrict_controller_test.go | 2 +- cmd/juju/crossmodel/applicationoffers.go | 2 +- cmd/juju/crossmodel/export_test.go | 4 +- cmd/juju/crossmodel/list.go | 15 +- cmd/juju/crossmodel/list_test.go | 6 + cmd/juju/crossmodel/offer.go | 28 +- cmd/juju/crossmodel/offer_test.go | 11 + cmd/juju/crossmodel/remoteendpoints.go | 6 +- 28 files changed, 1009 insertions(+), 1349 deletions(-) delete mode 100644 api/remoteendpoints/client.go delete mode 100644 api/remoteendpoints/client_test.go delete mode 100644 api/remoteendpoints/package_test.go rename apiserver/{common/crossmodelcommon/crossmodel.go => applicationoffers/base.go} (90%) rename apiserver/{common/crossmodelcommon => applicationoffers}/state.go (99%) delete mode 100644 apiserver/remoteendpoints/base_test.go delete mode 100644 apiserver/remoteendpoints/export_test.go delete mode 100644 apiserver/remoteendpoints/mock_test.go delete mode 100644 apiserver/remoteendpoints/package_test.go delete mode 100644 apiserver/remoteendpoints/remoteendpoints.go delete mode 100644 apiserver/remoteendpoints/remoteendpoints_test.go diff --git a/api/applicationoffers/client.go b/api/applicationoffers/client.go index c9a1a6cc3c51..7c3bc1880b35 100644 --- a/api/applicationoffers/client.go +++ b/api/applicationoffers/client.go @@ -151,3 +151,63 @@ func (c *Client) modifyOfferUser(action params.OfferAction, user, access string, } return result.Combine() } + +// ApplicationOffer returns offered remote application details for a given URL. +func (c *Client) ApplicationOffer(urlStr string) (params.ApplicationOffer, error) { + + url, err := crossmodel.ParseApplicationURL(urlStr) + if err != nil { + return params.ApplicationOffer{}, errors.Trace(err) + } + if url.Source != "" { + return params.ApplicationOffer{}, errors.NotSupportedf("query for non-local application offers") + } + + found := params.ApplicationOffersResults{} + + err = c.facade.FacadeCall("ApplicationOffers", params.ApplicationURLs{[]string{urlStr}}, &found) + if err != nil { + return params.ApplicationOffer{}, errors.Trace(err) + } + + result := found.Results + if len(result) != 1 { + return params.ApplicationOffer{}, errors.Errorf("expected to find one result for url %q but found %d", url, len(result)) + } + + theOne := result[0] + if theOne.Error != nil { + return params.ApplicationOffer{}, errors.Trace(theOne.Error) + } + return theOne.Result, nil +} + +// FindApplicationOffers returns all application offers matching the supplied filter. +func (c *Client) FindApplicationOffers(filters ...crossmodel.ApplicationOfferFilter) ([]params.ApplicationOffer, error) { + // We need at least one filter. The default filter will list all local applications. + if len(filters) == 0 { + return nil, errors.New("at least one filter must be specified") + } + var paramsFilter params.OfferFilters + for _, f := range filters { + filterTerm := params.OfferFilter{ + OfferName: f.OfferName, + ModelName: f.ModelName, + OwnerName: f.OwnerName, + } + 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, filterTerm) + } + + out := params.FindApplicationOffersResults{} + err := c.facade.FacadeCall("FindApplicationOffers", paramsFilter, &out) + if err != nil { + return nil, errors.Trace(err) + } + return out.Results, nil +} diff --git a/api/applicationoffers/client_test.go b/api/applicationoffers/client_test.go index a32c8e4a50e8..3d48bc2e4139 100644 --- a/api/applicationoffers/client_test.go +++ b/api/applicationoffers/client_test.go @@ -209,3 +209,324 @@ func (s *crossmodelMockSuite) TestListFacadeCallError(c *gc.C) { c.Assert(errors.Cause(err), gc.ErrorMatches, msg) c.Assert(results, gc.IsNil) } + +func (s *crossmodelMockSuite) TestShow(c *gc.C) { + url := "fred/model.db2" + + desc := "IBM DB2 Express Server Edition is an entry level database system" + endpoints := []params.RemoteEndpoint{ + {Name: "db2", Interface: "db2", Role: charm.RoleProvider}, + {Name: "log", Interface: "http", Role: charm.RoleRequirer}, + } + offerName := "hosted-db2" + access := "consume" + + called := false + + apiCaller := basetesting.APICallerFunc( + func(objType string, + version int, + id, request string, + a, result interface{}, + ) error { + called = true + + c.Check(objType, gc.Equals, "applicationoffers") + c.Check(id, gc.Equals, "") + c.Check(request, gc.Equals, "ApplicationOffers") + + args, ok := a.(params.ApplicationURLs) + c.Assert(ok, jc.IsTrue) + c.Assert(args.ApplicationURLs, gc.DeepEquals, []string{url}) + + if points, ok := result.(*params.ApplicationOffersResults); ok { + points.Results = []params.ApplicationOfferResult{ + {Result: params.ApplicationOffer{ + ApplicationDescription: desc, + Endpoints: endpoints, + OfferURL: url, + OfferName: offerName, + Access: access, + }}, + } + } + return nil + }) + client := applicationoffers.NewClient(apiCaller) + found, err := client.ApplicationOffer(url) + c.Assert(err, jc.ErrorIsNil) + + c.Assert(called, jc.IsTrue) + c.Assert(found, gc.DeepEquals, params.ApplicationOffer{ + ApplicationDescription: desc, + Endpoints: endpoints, + OfferURL: url, + OfferName: offerName, + Access: access}) +} + +func (s *crossmodelMockSuite) TestShowURLError(c *gc.C) { + url := "fred/model.db2" + msg := "facade failure" + + called := false + + apiCaller := basetesting.APICallerFunc( + func(objType string, + version int, + id, request string, + a, result interface{}, + ) error { + called = true + + c.Check(objType, gc.Equals, "applicationoffers") + c.Check(id, gc.Equals, "") + c.Check(request, gc.Equals, "ApplicationOffers") + + args, ok := a.(params.ApplicationURLs) + c.Assert(ok, jc.IsTrue) + c.Assert(args.ApplicationURLs, gc.DeepEquals, []string{url}) + + if points, ok := result.(*params.ApplicationOffersResults); ok { + points.Results = []params.ApplicationOfferResult{ + {Error: common.ServerError(errors.New(msg))}} + } + return nil + }) + client := applicationoffers.NewClient(apiCaller) + found, err := client.ApplicationOffer(url) + + c.Assert(errors.Cause(err), gc.ErrorMatches, msg) + c.Assert(found, gc.DeepEquals, params.ApplicationOffer{}) + c.Assert(called, jc.IsTrue) +} + +func (s *crossmodelMockSuite) TestShowMultiple(c *gc.C) { + url := "fred/model.db2" + + desc := "IBM DB2 Express Server Edition is an entry level database system" + endpoints := []params.RemoteEndpoint{ + {Name: "db2", Interface: "db2", Role: charm.RoleProvider}, + {Name: "log", Interface: "http", Role: charm.RoleRequirer}, + } + offerName := "hosted-db2" + access := "consume" + + called := false + + apiCaller := basetesting.APICallerFunc( + func(objType string, + version int, + id, request string, + a, result interface{}, + ) error { + called = true + + c.Check(objType, gc.Equals, "applicationoffers") + c.Check(id, gc.Equals, "") + c.Check(request, gc.Equals, "ApplicationOffers") + + args, ok := a.(params.ApplicationURLs) + c.Assert(ok, jc.IsTrue) + c.Assert(args.ApplicationURLs, gc.DeepEquals, []string{url}) + + if points, ok := result.(*params.ApplicationOffersResults); ok { + points.Results = []params.ApplicationOfferResult{ + {Result: params.ApplicationOffer{ + ApplicationDescription: desc, + Endpoints: endpoints, + OfferURL: url, + OfferName: offerName, + Access: access, + }}, + {Result: params.ApplicationOffer{ + ApplicationDescription: desc, + Endpoints: endpoints, + OfferURL: url, + OfferName: offerName, + Access: access, + }}} + } + return nil + }) + client := applicationoffers.NewClient(apiCaller) + found, err := client.ApplicationOffer(url) + c.Assert(err, gc.ErrorMatches, fmt.Sprintf(`expected to find one result for url %q but found 2`, url)) + c.Assert(found, gc.DeepEquals, params.ApplicationOffer{}) + + c.Assert(called, jc.IsTrue) +} + +func (s *crossmodelMockSuite) TestShowNonLocal(c *gc.C) { + url := "jaas:fred/model.db2" + msg := "query for non-local application offers not supported" + + called := false + + apiCaller := basetesting.APICallerFunc( + func(objType string, + version int, + id, request string, + a, result interface{}, + ) error { + called = true + return nil + }) + client := applicationoffers.NewClient(apiCaller) + found, err := client.ApplicationOffer(url) + + c.Assert(errors.Cause(err), gc.ErrorMatches, msg) + c.Assert(found, gc.DeepEquals, params.ApplicationOffer{}) + c.Assert(called, jc.IsFalse) +} + +func (s *crossmodelMockSuite) TestShowFacadeCallError(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, "applicationoffers") + c.Check(id, gc.Equals, "") + c.Check(request, gc.Equals, "ApplicationOffers") + + return errors.New(msg) + }) + client := applicationoffers.NewClient(apiCaller) + found, err := client.ApplicationOffer("fred/model.db2") + c.Assert(errors.Cause(err), gc.ErrorMatches, msg) + c.Assert(found, gc.DeepEquals, params.ApplicationOffer{}) +} + +func (s *crossmodelMockSuite) TestFind(c *gc.C) { + offerName := "hosted-db2" + ownerName := "owner" + modelName := "model" + access := "consume" + url := fmt.Sprintf("fred/model.%s", offerName) + endpoints := []params.RemoteEndpoint{{Name: "endPointA"}} + relations := []jujucrossmodel.EndpointFilterTerm{{Name: "endPointA", Interface: "http"}} + + filter := jujucrossmodel.ApplicationOfferFilter{ + OwnerName: ownerName, + ModelName: modelName, + OfferName: offerName, + Endpoints: relations, + } + + called := false + apiCaller := basetesting.APICallerFunc( + func(objType string, + version int, + id, request string, + a, result interface{}, + ) error { + c.Check(objType, gc.Equals, "applicationoffers") + c.Check(id, gc.Equals, "") + c.Check(request, gc.Equals, "FindApplicationOffers") + + called = true + args, ok := a.(params.OfferFilters) + c.Assert(ok, jc.IsTrue) + c.Assert(args.Filters, gc.HasLen, 1) + c.Assert(args.Filters[0], jc.DeepEquals, params.OfferFilter{ + OwnerName: filter.OwnerName, + ModelName: filter.ModelName, + OfferName: filter.OfferName, + ApplicationName: filter.ApplicationName, + Endpoints: []params.EndpointFilterAttributes{{ + Name: "endPointA", + Interface: "http", + }}, + }) + + if results, ok := result.(*params.FindApplicationOffersResults); ok { + offer := params.ApplicationOffer{ + OfferURL: url, + OfferName: offerName, + Endpoints: endpoints, + Access: access, + } + results.Results = []params.ApplicationOffer{offer} + } + return nil + }) + + client := applicationoffers.NewClient(apiCaller) + results, err := client.FindApplicationOffers(filter) + c.Assert(err, jc.ErrorIsNil) + c.Assert(called, jc.IsTrue) + c.Assert(results, jc.DeepEquals, []params.ApplicationOffer{{ + OfferURL: url, + OfferName: offerName, + Endpoints: endpoints, + Access: access, + }}) +} + +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 := applicationoffers.NewClient(apiCaller) + _, err := client.FindApplicationOffers() + c.Assert(err, gc.ErrorMatches, "at least one filter must be specified") +} + +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, "applicationoffers") + c.Check(id, gc.Equals, "") + c.Check(request, gc.Equals, "FindApplicationOffers") + + called = true + return errors.New(msg) + }) + + client := applicationoffers.NewClient(apiCaller) + filter := jujucrossmodel.ApplicationOfferFilter{ + OfferName: "foo", + } + _, err := client.FindApplicationOffers(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, "applicationoffers") + c.Check(id, gc.Equals, "") + c.Check(request, gc.Equals, "FindApplicationOffers") + + return errors.New(msg) + }) + client := applicationoffers.NewClient(apiCaller) + filter := jujucrossmodel.ApplicationOfferFilter{ + OfferName: "foo", + } + results, err := client.FindApplicationOffers(filter) + c.Assert(errors.Cause(err), gc.ErrorMatches, msg) + c.Assert(results, gc.IsNil) +} diff --git a/api/facadeversions.go b/api/facadeversions.go index e8910a33b24e..0f76aa313342 100644 --- a/api/facadeversions.go +++ b/api/facadeversions.go @@ -69,7 +69,6 @@ var facadeVersions = map[string]int{ "ProxyUpdater": 1, "Reboot": 2, "RelationUnitsWatcher": 1, - "RemoteEndpoints": 1, "RemoteFirewaller": 1, "RemoteRelations": 1, "Resources": 1, diff --git a/api/remoteendpoints/client.go b/api/remoteendpoints/client.go deleted file mode 100644 index aed16b188a70..000000000000 --- a/api/remoteendpoints/client.go +++ /dev/null @@ -1,84 +0,0 @@ -// Copyright 2015 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package remoteendpoints - -import ( - "github.com/juju/errors" - - "github.com/juju/juju/api/base" - "github.com/juju/juju/apiserver/params" - "github.com/juju/juju/core/crossmodel" -) - -// Client allows access to the cross model management API end points. -type Client struct { - base.ClientFacade - facade base.FacadeCaller -} - -// NewClient creates a new client for accessing the cross model relations API. -func NewClient(st base.APICallCloser) *Client { - frontend, backend := base.NewClientFacade(st, "RemoteEndpoints") - return &Client{ClientFacade: frontend, facade: backend} -} - -// ApplicationOffer returns offered remote application details for a given URL. -func (c *Client) ApplicationOffer(urlStr string) (params.ApplicationOffer, error) { - - url, err := crossmodel.ParseApplicationURL(urlStr) - if err != nil { - return params.ApplicationOffer{}, errors.Trace(err) - } - if url.Source != "" { - return params.ApplicationOffer{}, errors.NotSupportedf("query for non-local application offers") - } - - found := params.ApplicationOffersResults{} - - err = c.facade.FacadeCall("ApplicationOffers", params.ApplicationURLs{[]string{urlStr}}, &found) - if err != nil { - return params.ApplicationOffer{}, errors.Trace(err) - } - - result := found.Results - if len(result) != 1 { - return params.ApplicationOffer{}, errors.Errorf("expected to find one result for url %q but found %d", url, len(result)) - } - - theOne := result[0] - if theOne.Error != nil { - return params.ApplicationOffer{}, errors.Trace(theOne.Error) - } - return theOne.Result, nil -} - -// FindApplicationOffers returns all application offers matching the supplied filter. -func (c *Client) FindApplicationOffers(filters ...crossmodel.ApplicationOfferFilter) ([]params.ApplicationOffer, error) { - // We need at least one filter. The default filter will list all local applications. - if len(filters) == 0 { - return nil, errors.New("at least one filter must be specified") - } - var paramsFilter params.OfferFilters - for _, f := range filters { - filterTerm := params.OfferFilter{ - OfferName: f.OfferName, - ModelName: f.ModelName, - OwnerName: f.OwnerName, - } - 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, filterTerm) - } - - out := params.FindApplicationOffersResults{} - err := c.facade.FacadeCall("FindApplicationOffers", paramsFilter, &out) - if err != nil { - return nil, errors.Trace(err) - } - return out.Results, nil -} diff --git a/api/remoteendpoints/client_test.go b/api/remoteendpoints/client_test.go deleted file mode 100644 index a0861936bf6f..000000000000 --- a/api/remoteendpoints/client_test.go +++ /dev/null @@ -1,347 +0,0 @@ -// Copyright 2015 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package remoteendpoints_test - -import ( - "fmt" - - "github.com/juju/errors" - jc "github.com/juju/testing/checkers" - gc "gopkg.in/check.v1" - "gopkg.in/juju/charm.v6-unstable" - - basetesting "github.com/juju/juju/api/base/testing" - "github.com/juju/juju/api/remoteendpoints" - "github.com/juju/juju/apiserver/common" - "github.com/juju/juju/apiserver/params" - jujucrossmodel "github.com/juju/juju/core/crossmodel" - "github.com/juju/juju/testing" -) - -type crossmodelMockSuite struct { - testing.BaseSuite -} - -var _ = gc.Suite(&crossmodelMockSuite{}) - -func (s *crossmodelMockSuite) TestShow(c *gc.C) { - url := "fred/model.db2" - - desc := "IBM DB2 Express Server Edition is an entry level database system" - endpoints := []params.RemoteEndpoint{ - {Name: "db2", Interface: "db2", Role: charm.RoleProvider}, - {Name: "log", Interface: "http", Role: charm.RoleRequirer}, - } - offerName := "hosted-db2" - access := "consume" - - called := false - - apiCaller := basetesting.APICallerFunc( - func(objType string, - version int, - id, request string, - a, result interface{}, - ) error { - called = true - - c.Check(objType, gc.Equals, "RemoteEndpoints") - c.Check(id, gc.Equals, "") - c.Check(request, gc.Equals, "ApplicationOffers") - - args, ok := a.(params.ApplicationURLs) - c.Assert(ok, jc.IsTrue) - c.Assert(args.ApplicationURLs, gc.DeepEquals, []string{url}) - - if points, ok := result.(*params.ApplicationOffersResults); ok { - points.Results = []params.ApplicationOfferResult{ - {Result: params.ApplicationOffer{ - ApplicationDescription: desc, - Endpoints: endpoints, - OfferURL: url, - OfferName: offerName, - Access: access, - }}, - } - } - return nil - }) - client := remoteendpoints.NewClient(apiCaller) - found, err := client.ApplicationOffer(url) - c.Assert(err, jc.ErrorIsNil) - - c.Assert(called, jc.IsTrue) - c.Assert(found, gc.DeepEquals, params.ApplicationOffer{ - ApplicationDescription: desc, - Endpoints: endpoints, - OfferURL: url, - OfferName: offerName, - Access: access}) -} - -func (s *crossmodelMockSuite) TestShowURLError(c *gc.C) { - url := "fred/model.db2" - msg := "facade failure" - - called := false - - apiCaller := basetesting.APICallerFunc( - func(objType string, - version int, - id, request string, - a, result interface{}, - ) error { - called = true - - c.Check(objType, gc.Equals, "RemoteEndpoints") - c.Check(id, gc.Equals, "") - c.Check(request, gc.Equals, "ApplicationOffers") - - args, ok := a.(params.ApplicationURLs) - c.Assert(ok, jc.IsTrue) - c.Assert(args.ApplicationURLs, gc.DeepEquals, []string{url}) - - if points, ok := result.(*params.ApplicationOffersResults); ok { - points.Results = []params.ApplicationOfferResult{ - {Error: common.ServerError(errors.New(msg))}} - } - return nil - }) - client := remoteendpoints.NewClient(apiCaller) - found, err := client.ApplicationOffer(url) - - c.Assert(errors.Cause(err), gc.ErrorMatches, msg) - c.Assert(found, gc.DeepEquals, params.ApplicationOffer{}) - c.Assert(called, jc.IsTrue) -} - -func (s *crossmodelMockSuite) TestShowMultiple(c *gc.C) { - url := "fred/model.db2" - - desc := "IBM DB2 Express Server Edition is an entry level database system" - endpoints := []params.RemoteEndpoint{ - {Name: "db2", Interface: "db2", Role: charm.RoleProvider}, - {Name: "log", Interface: "http", Role: charm.RoleRequirer}, - } - offerName := "hosted-db2" - access := "consume" - - called := false - - apiCaller := basetesting.APICallerFunc( - func(objType string, - version int, - id, request string, - a, result interface{}, - ) error { - called = true - - c.Check(objType, gc.Equals, "RemoteEndpoints") - c.Check(id, gc.Equals, "") - c.Check(request, gc.Equals, "ApplicationOffers") - - args, ok := a.(params.ApplicationURLs) - c.Assert(ok, jc.IsTrue) - c.Assert(args.ApplicationURLs, gc.DeepEquals, []string{url}) - - if points, ok := result.(*params.ApplicationOffersResults); ok { - points.Results = []params.ApplicationOfferResult{ - {Result: params.ApplicationOffer{ - ApplicationDescription: desc, - Endpoints: endpoints, - OfferURL: url, - OfferName: offerName, - Access: access, - }}, - {Result: params.ApplicationOffer{ - ApplicationDescription: desc, - Endpoints: endpoints, - OfferURL: url, - OfferName: offerName, - Access: access, - }}} - } - return nil - }) - client := remoteendpoints.NewClient(apiCaller) - found, err := client.ApplicationOffer(url) - c.Assert(err, gc.ErrorMatches, fmt.Sprintf(`expected to find one result for url %q but found 2`, url)) - c.Assert(found, gc.DeepEquals, params.ApplicationOffer{}) - - c.Assert(called, jc.IsTrue) -} - -func (s *crossmodelMockSuite) TestShowNonLocal(c *gc.C) { - url := "jaas:fred/model.db2" - msg := "query for non-local application offers not supported" - - called := false - - apiCaller := basetesting.APICallerFunc( - func(objType string, - version int, - id, request string, - a, result interface{}, - ) error { - called = true - return nil - }) - client := remoteendpoints.NewClient(apiCaller) - found, err := client.ApplicationOffer(url) - - c.Assert(errors.Cause(err), gc.ErrorMatches, msg) - c.Assert(found, gc.DeepEquals, params.ApplicationOffer{}) - c.Assert(called, jc.IsFalse) -} - -func (s *crossmodelMockSuite) TestShowFacadeCallError(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, "RemoteEndpoints") - c.Check(id, gc.Equals, "") - c.Check(request, gc.Equals, "ApplicationOffers") - - return errors.New(msg) - }) - client := remoteendpoints.NewClient(apiCaller) - found, err := client.ApplicationOffer("fred/model.db2") - c.Assert(errors.Cause(err), gc.ErrorMatches, msg) - c.Assert(found, gc.DeepEquals, params.ApplicationOffer{}) -} - -func (s *crossmodelMockSuite) TestFind(c *gc.C) { - offerName := "hosted-db2" - ownerName := "owner" - modelName := "model" - access := "consume" - url := fmt.Sprintf("fred/model.%s", offerName) - endpoints := []params.RemoteEndpoint{{Name: "endPointA"}} - relations := []jujucrossmodel.EndpointFilterTerm{{Name: "endPointA", Interface: "http"}} - - filter := jujucrossmodel.ApplicationOfferFilter{ - OwnerName: ownerName, - ModelName: modelName, - OfferName: offerName, - Endpoints: relations, - } - - called := false - apiCaller := basetesting.APICallerFunc( - func(objType string, - version int, - id, request string, - a, result interface{}, - ) error { - c.Check(objType, gc.Equals, "RemoteEndpoints") - c.Check(id, gc.Equals, "") - c.Check(request, gc.Equals, "FindApplicationOffers") - - called = true - args, ok := a.(params.OfferFilters) - c.Assert(ok, jc.IsTrue) - c.Assert(args.Filters, gc.HasLen, 1) - c.Assert(args.Filters[0], jc.DeepEquals, params.OfferFilter{ - OwnerName: filter.OwnerName, - ModelName: filter.ModelName, - OfferName: filter.OfferName, - ApplicationName: filter.ApplicationName, - Endpoints: []params.EndpointFilterAttributes{{ - Name: "endPointA", - Interface: "http", - }}, - }) - - if results, ok := result.(*params.FindApplicationOffersResults); ok { - offer := params.ApplicationOffer{ - OfferURL: url, - OfferName: offerName, - Endpoints: endpoints, - Access: access, - } - results.Results = []params.ApplicationOffer{offer} - } - return nil - }) - - client := remoteendpoints.NewClient(apiCaller) - results, err := client.FindApplicationOffers(filter) - c.Assert(err, jc.ErrorIsNil) - c.Assert(called, jc.IsTrue) - c.Assert(results, jc.DeepEquals, []params.ApplicationOffer{{ - OfferURL: url, - OfferName: offerName, - Endpoints: endpoints, - Access: access, - }}) -} - -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 := remoteendpoints.NewClient(apiCaller) - _, err := client.FindApplicationOffers() - c.Assert(err, gc.ErrorMatches, "at least one filter must be specified") -} - -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, "RemoteEndpoints") - c.Check(id, gc.Equals, "") - c.Check(request, gc.Equals, "FindApplicationOffers") - - called = true - return errors.New(msg) - }) - - client := remoteendpoints.NewClient(apiCaller) - filter := jujucrossmodel.ApplicationOfferFilter{ - OfferName: "foo", - } - _, err := client.FindApplicationOffers(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, "RemoteEndpoints") - c.Check(id, gc.Equals, "") - c.Check(request, gc.Equals, "FindApplicationOffers") - - return errors.New(msg) - }) - client := remoteendpoints.NewClient(apiCaller) - filter := jujucrossmodel.ApplicationOfferFilter{ - OfferName: "foo", - } - results, err := client.FindApplicationOffers(filter) - c.Assert(errors.Cause(err), gc.ErrorMatches, msg) - c.Assert(results, gc.IsNil) -} diff --git a/api/remoteendpoints/package_test.go b/api/remoteendpoints/package_test.go deleted file mode 100644 index e06b21d06c2e..000000000000 --- a/api/remoteendpoints/package_test.go +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright 2015 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package remoteendpoints_test - -import ( - "testing" - - gc "gopkg.in/check.v1" -) - -func TestAll(t *testing.T) { - gc.TestingT(t) -} diff --git a/apiserver/allfacades.go b/apiserver/allfacades.go index 047903338b94..e99d862ffe3c 100644 --- a/apiserver/allfacades.go +++ b/apiserver/allfacades.go @@ -62,7 +62,6 @@ import ( "github.com/juju/juju/apiserver/provisioner" "github.com/juju/juju/apiserver/proxyupdater" "github.com/juju/juju/apiserver/reboot" - "github.com/juju/juju/apiserver/remoteendpoints" "github.com/juju/juju/apiserver/remotefirewaller" "github.com/juju/juju/apiserver/remoterelations" "github.com/juju/juju/apiserver/resources" @@ -211,7 +210,6 @@ func AllFacades() *facade.Registry { if featureflag.Enabled(feature.CrossModelRelations) { reg("ApplicationOffers", 1, applicationoffers.NewOffersAPI) - reg("RemoteEndpoints", 1, remoteendpoints.NewEndpointsAPI) reg("RemoteFirewaller", 1, remotefirewaller.NewStateRemoteFirewallerAPI) reg("RemoteRelations", 1, remoterelations.NewStateRemoteRelationsAPI) } diff --git a/apiserver/applicationoffers/applicationoffers.go b/apiserver/applicationoffers/applicationoffers.go index cd01c7ad97be..4c142b197cc6 100644 --- a/apiserver/applicationoffers/applicationoffers.go +++ b/apiserver/applicationoffers/applicationoffers.go @@ -9,7 +9,6 @@ import ( "gopkg.in/juju/names.v2" "github.com/juju/juju/apiserver/common" - "github.com/juju/juju/apiserver/common/crossmodelcommon" "github.com/juju/juju/apiserver/facade" "github.com/juju/juju/apiserver/params" jujucrossmodel "github.com/juju/juju/core/crossmodel" @@ -19,14 +18,14 @@ import ( // OffersAPI implements the cross model interface and is the concrete // implementation of the api end point. type OffersAPI struct { - crossmodelcommon.BaseAPI + BaseAPI } // createAPI returns a new application offers OffersAPI facade. func createOffersAPI( getApplicationOffers func(interface{}) jujucrossmodel.ApplicationOffers, - backend crossmodelcommon.Backend, - statePool crossmodelcommon.StatePool, + backend Backend, + statePool StatePool, authorizer facade.Authorizer, ) (*OffersAPI, error) { if !authorizer.AuthClient() { @@ -34,7 +33,7 @@ func createOffersAPI( } api := &OffersAPI{ - BaseAPI: crossmodelcommon.BaseAPI{ + BaseAPI: BaseAPI{ Authorizer: authorizer, GetApplicationOffers: getApplicationOffers, Backend: backend, @@ -46,13 +45,13 @@ func createOffersAPI( // NewOffersAPI returns a new application offers OffersAPI facade. func NewOffersAPI(ctx facade.Context) (*OffersAPI, error) { return createOffersAPI( - crossmodelcommon.GetApplicationOffers, crossmodelcommon.GetStateAccess(ctx.State()), - crossmodelcommon.GetStatePool(ctx.StatePool()), ctx.Auth()) + GetApplicationOffers, GetStateAccess(ctx.State()), + GetStatePool(ctx.StatePool()), ctx.Auth()) } // Offer makes application endpoints available for consumption at a specified URL. func (api *OffersAPI) Offer(all params.AddApplicationOffers) (params.ErrorResults, error) { - if err := api.CheckAdmin(api.Backend); err != nil { + if err := api.checkAdmin(api.Backend); err != nil { return params.ErrorResults{}, common.ServerError(err) } @@ -102,7 +101,7 @@ func (api *OffersAPI) makeAddOfferArgsFromParams(addOfferParams params.AddApplic // The results contain details about the deployed applications such as connection count. func (api *OffersAPI) ListApplicationOffers(filters params.OfferFilters) (params.ListApplicationOffersResults, error) { var result params.ListApplicationOffersResults - offers, err := api.GetApplicationOffersDetails(filters, true) + offers, err := api.getApplicationOffersDetails(filters, true) if err != nil { return result, err } @@ -226,3 +225,102 @@ func (api *OffersAPI) revokeOfferAccess(offerTag names.ApplicationOfferTag, targ return errors.Errorf("don't know how to revoke %q access", access) } } + +// ApplicationOffers gets details about remote applications that match given URLs. +func (api *OffersAPI) ApplicationOffers(urls params.ApplicationURLs) (params.ApplicationOffersResults, error) { + var results params.ApplicationOffersResults + results.Results = make([]params.ApplicationOfferResult, len(urls.ApplicationURLs)) + + for i, urlStr := range urls.ApplicationURLs { + offer, err := api.offerForURL(urlStr) + if err != nil { + results.Results[i].Error = common.ServerError(err) + continue + } + results.Results[i].Result = offer.ApplicationOffer + } + return results, nil +} + +// offerForURL finds the single offer for a specified (possibly relative) URL, +// returning the offer and full URL. +func (api *OffersAPI) offerForURL(urlStr string) (params.ApplicationOfferDetails, error) { + fail := func(err error) (params.ApplicationOfferDetails, error) { + return params.ApplicationOfferDetails{}, errors.Trace(err) + } + + url, err := jujucrossmodel.ParseApplicationURL(urlStr) + if err != nil { + return fail(errors.Trace(err)) + } + if url.Source != "" { + err = errors.NotSupportedf("query for non-local application offers") + return fail(errors.Trace(err)) + } + + model, ok, err := api.modelForName(url.ModelName, url.User) + if err != nil { + return fail(errors.Trace(err)) + } + if !ok { + err = errors.NotFoundf("model %q", url.ModelName) + return fail(err) + } + filter := jujucrossmodel.ApplicationOfferFilter{ + OfferName: url.ApplicationName, + } + offers, err := api.applicationOffersFromModel(model.UUID(), false, filter) + if err != nil { + return fail(errors.Trace(err)) + } + if len(offers) == 0 { + err := errors.NotFoundf("application offer %q", url.ApplicationName) + return fail(err) + } + if len(offers) > 1 { + err := errors.Errorf("too many application offers for %q", url.ApplicationName) + return fail(err) + } + fullURL := jujucrossmodel.MakeURL(model.Owner().Name(), model.Name(), url.ApplicationName, "") + offer := offers[0] + offer.OfferURL = fullURL + return offer, nil +} + +// FindApplicationOffers gets details about remote applications that match given filter. +func (api *OffersAPI) FindApplicationOffers(filters params.OfferFilters) (params.FindApplicationOffersResults, error) { + var result params.FindApplicationOffersResults + var filtersToUse params.OfferFilters + + // If there is only one filter term, and no model is specified, add in + // any models the user can see and query across those. + // If there's more than one filter term, each must specify a model. + if len(filters.Filters) == 1 && filters.Filters[0].ModelName == "" { + allModels, err := api.Backend.AllModels() + if err != nil { + return result, errors.Trace(err) + } + for _, m := range allModels { + modelFilter := filters.Filters[0] + modelFilter.ModelName = m.Name() + modelFilter.OwnerName = m.Owner().Name() + filtersToUse.Filters = append(filtersToUse.Filters, modelFilter) + } + } else { + filtersToUse = filters + } + for _, f := range filtersToUse.Filters { + if f.ModelName == "" { + return result, errors.New("application offer filter must specify a model name") + } + } + + offers, err := api.getApplicationOffersDetails(filtersToUse, false) + if err != nil { + return result, errors.Trace(err) + } + for _, offer := range offers { + result.Results = append(result.Results, offer.ApplicationOffer) + } + return result, nil +} diff --git a/apiserver/applicationoffers/applicationoffers_test.go b/apiserver/applicationoffers/applicationoffers_test.go index e93960bf1aec..64d58db3f551 100644 --- a/apiserver/applicationoffers/applicationoffers_test.go +++ b/apiserver/applicationoffers/applicationoffers_test.go @@ -8,15 +8,16 @@ import ( "github.com/juju/errors" jc "github.com/juju/testing/checkers" + "github.com/juju/utils/set" gc "gopkg.in/check.v1" "gopkg.in/juju/charm.v6-unstable" "gopkg.in/juju/names.v2" "github.com/juju/juju/apiserver/applicationoffers" "github.com/juju/juju/apiserver/common" - "github.com/juju/juju/apiserver/common/crossmodelcommon" "github.com/juju/juju/apiserver/params" jujucrossmodel "github.com/juju/juju/core/crossmodel" + "github.com/juju/juju/permission" ) type applicationOffersSuite struct { @@ -58,7 +59,7 @@ func (s *applicationOffersSuite) assertOffer(c *gc.C, expectedErr error) { return &jujucrossmodel.ApplicationOffer{}, nil } charm := &mockCharm{meta: &charm.Meta{Description: "A pretty popular blog engine"}} - s.mockState.applications = map[string]crossmodelcommon.Application{ + s.mockState.applications = map[string]applicationoffers.Application{ applicationName: &mockApplication{charm: charm}, } @@ -115,7 +116,7 @@ func (s *applicationOffersSuite) TestOfferSomeFail(c *gc.C) { return &jujucrossmodel.ApplicationOffer{}, nil } charm := &mockCharm{meta: &charm.Meta{Description: "A pretty popular blog engine"}} - s.mockState.applications = map[string]crossmodelcommon.Application{ + s.mockState.applications = map[string]applicationoffers.Application{ "one": &mockApplication{charm: charm}, "two": &mockApplication{charm: charm}, "paramsfail": &mockApplication{charm: charm}, @@ -148,7 +149,7 @@ func (s *applicationOffersSuite) TestOfferError(c *gc.C) { return nil, errors.New(msg) } charm := &mockCharm{meta: &charm.Meta{Description: "A pretty popular blog engine"}} - s.mockState.applications = map[string]crossmodelcommon.Application{ + s.mockState.applications = map[string]applicationoffers.Application{ applicationName: &mockApplication{charm: charm}, } @@ -223,3 +224,423 @@ func (s *applicationOffersSuite) TestListError(c *gc.C) { c.Assert(err, gc.ErrorMatches, fmt.Sprintf(".*%v.*", msg)) s.applicationOffers.CheckCallNames(c, listOffersBackendCall) } + +func (s *applicationOffersSuite) assertShow(c *gc.C, expected []params.ApplicationOfferResult) { + applicationName := "test" + filter := params.ApplicationURLs{[]string{"fred/prod.hosted-db2"}} + anOffer := jujucrossmodel.ApplicationOffer{ + ApplicationName: applicationName, + ApplicationDescription: "description", + OfferName: "hosted-db2", + Endpoints: map[string]charm.Relation{"db": {Name: "db"}}, + } + + s.applicationOffers.listOffers = func(filters ...jujucrossmodel.ApplicationOfferFilter) ([]jujucrossmodel.ApplicationOffer, error) { + return []jujucrossmodel.ApplicationOffer{anOffer}, nil + } + ch := &mockCharm{meta: &charm.Meta{Description: "A pretty popular blog engine"}} + s.mockState.applications = map[string]applicationoffers.Application{ + applicationName: &mockApplication{charm: ch, curl: charm.MustParseURL("db2-2")}, + } + s.mockState.model = &mockModel{uuid: "uuid", name: "prod", owner: "fred"} + s.mockState.connStatus = &mockConnectionStatus{count: 5} + + found, err := s.api.ApplicationOffers(filter) + c.Assert(err, jc.ErrorIsNil) + c.Assert(found.Results, jc.DeepEquals, expected) + s.applicationOffers.CheckCallNames(c, listOffersBackendCall) +} + +func (s *applicationOffersSuite) TestShow(c *gc.C) { + expected := []params.ApplicationOfferResult{{ + Result: params.ApplicationOffer{ + SourceModelTag: "model-uuid", + ApplicationDescription: "description", + OfferURL: "fred/prod.hosted-db2", + OfferName: "hosted-db2", + Endpoints: []params.RemoteEndpoint{{Name: "db"}}, + Access: "admin"}, + }} + s.authorizer.Tag = names.NewUserTag("admin") + s.assertShow(c, expected) +} + +func (s *applicationOffersSuite) TestShowNoPermission(c *gc.C) { + s.authorizer.Tag = names.NewUserTag("someone") + expected := []params.ApplicationOfferResult{{ + Error: common.ServerError(errors.NotFoundf("application offer %q", "hosted-db2")), + }} + s.assertShow(c, expected) +} + +func (s *applicationOffersSuite) TestShowPermission(c *gc.C) { + user := names.NewUserTag("someone") + s.authorizer.Tag = user + expected := []params.ApplicationOfferResult{{ + Result: params.ApplicationOffer{ + SourceModelTag: "model-uuid", + ApplicationDescription: "description", + OfferURL: "fred/prod.hosted-db2", + OfferName: "hosted-db2", + Endpoints: []params.RemoteEndpoint{{Name: "db"}}, + Access: "read"}, + }} + s.mockState.users.Add(user.Name()) + s.mockState.CreateOfferAccess(names.NewApplicationOfferTag("hosted-db2"), user, permission.ReadAccess) + s.assertShow(c, expected) +} + +func (s *applicationOffersSuite) TestShowError(c *gc.C) { + url := "fred/prod.hosted-db2" + filter := params.ApplicationURLs{[]string{url}} + msg := "fail" + + s.applicationOffers.listOffers = func(filters ...jujucrossmodel.ApplicationOfferFilter) ([]jujucrossmodel.ApplicationOffer, error) { + return nil, errors.New(msg) + } + s.mockState.model = &mockModel{uuid: "uuid", name: "prod", owner: "fred"} + + result, err := s.api.ApplicationOffers(filter) + c.Assert(err, jc.ErrorIsNil) + c.Assert(result.Results, gc.HasLen, 1) + c.Assert(result.Results[0].Error, gc.ErrorMatches, fmt.Sprintf(".*%v.*", msg)) + s.applicationOffers.CheckCallNames(c, listOffersBackendCall) +} + +func (s *applicationOffersSuite) TestShowNotFound(c *gc.C) { + urls := []string{"fred/prod.hosted-db2"} + filter := params.ApplicationURLs{urls} + + s.applicationOffers.listOffers = func(filters ...jujucrossmodel.ApplicationOfferFilter) ([]jujucrossmodel.ApplicationOffer, error) { + return nil, nil + } + s.mockState.model = &mockModel{uuid: "uuid", name: "prod", owner: "fred"} + + found, err := s.api.ApplicationOffers(filter) + c.Assert(err, jc.ErrorIsNil) + c.Assert(found.Results, gc.HasLen, 1) + c.Assert(found.Results[0].Error.Error(), gc.Matches, `application offer "hosted-db2" not found`) + s.applicationOffers.CheckCallNames(c, listOffersBackendCall) +} + +func (s *applicationOffersSuite) TestShowErrorMsgMultipleURLs(c *gc.C) { + urls := []string{"fred/prod.hosted-mysql", "fred/test.hosted-db2"} + filter := params.ApplicationURLs{urls} + + s.applicationOffers.listOffers = func(filters ...jujucrossmodel.ApplicationOfferFilter) ([]jujucrossmodel.ApplicationOffer, error) { + return nil, nil + } + s.mockState.model = &mockModel{uuid: "uuid", name: "prod", owner: "fred"} + s.mockState.allmodels = []applicationoffers.Model{ + s.mockState.model, + &mockModel{uuid: "uuid2", name: "test", owner: "fred"}, + } + anotherState := &mockState{modelUUID: "uuid2"} + s.mockStatePool.st["uuid2"] = anotherState + + found, err := s.api.ApplicationOffers(filter) + c.Assert(err, jc.ErrorIsNil) + c.Assert(found.Results, gc.HasLen, 2) + c.Assert(found.Results[0].Error.Error(), gc.Matches, `application offer "hosted-mysql" not found`) + c.Assert(found.Results[1].Error.Error(), gc.Matches, `application offer "hosted-db2" not found`) + s.applicationOffers.CheckCallNames(c, listOffersBackendCall, listOffersBackendCall) +} + +func (s *applicationOffersSuite) TestShowFoundMultiple(c *gc.C) { + name := "test" + url := "fred/prod.hosted-" + name + anOffer := jujucrossmodel.ApplicationOffer{ + ApplicationName: name, + ApplicationDescription: "description", + OfferName: "hosted-" + name, + Endpoints: map[string]charm.Relation{"db": {Name: "db"}}, + } + + name2 := "testagain" + url2 := "mary/test.hosted-" + name2 + anOffer2 := jujucrossmodel.ApplicationOffer{ + ApplicationName: name2, + ApplicationDescription: "description2", + OfferName: "hosted-" + name2, + Endpoints: map[string]charm.Relation{"db2": {Name: "db2"}}, + } + + filter := params.ApplicationURLs{[]string{url, url2}} + + s.applicationOffers.listOffers = func(filters ...jujucrossmodel.ApplicationOfferFilter) ([]jujucrossmodel.ApplicationOffer, error) { + c.Assert(filters, gc.HasLen, 1) + if filters[0].OfferName == "hosted-test" { + return []jujucrossmodel.ApplicationOffer{anOffer}, nil + } + return []jujucrossmodel.ApplicationOffer{anOffer2}, nil + } + ch := &mockCharm{meta: &charm.Meta{Description: "A pretty popular blog engine"}} + s.mockState.applications = map[string]applicationoffers.Application{ + "test": &mockApplication{charm: ch, curl: charm.MustParseURL("db2-2")}, + } + s.mockState.model = &mockModel{uuid: "uuid", name: "prod", owner: "fred"} + s.mockState.allmodels = []applicationoffers.Model{ + s.mockState.model, + &mockModel{uuid: "uuid2", name: "test", owner: "mary"}, + } + s.mockState.connStatus = &mockConnectionStatus{count: 5} + + user := names.NewUserTag("someone") + s.authorizer.Tag = user + s.mockState.users.Add(user.Name()) + s.mockState.CreateOfferAccess(names.NewApplicationOfferTag("hosted-test"), user, permission.ReadAccess) + + anotherState := &mockState{ + modelUUID: "uuid2", + users: set.NewStrings(), + accessPerms: make(map[offerAccess]permission.Access), + } + anotherState.applications = map[string]applicationoffers.Application{ + "testagain": &mockApplication{charm: ch, curl: charm.MustParseURL("mysql-2")}, + } + anotherState.connStatus = &mockConnectionStatus{count: 5} + anotherState.users.Add(user.Name()) + anotherState.CreateOfferAccess(names.NewApplicationOfferTag("hosted-testagain"), user, permission.ConsumeAccess) + s.mockStatePool.st["uuid2"] = anotherState + + found, err := s.api.ApplicationOffers(filter) + c.Assert(err, jc.ErrorIsNil) + var results []params.ApplicationOffer + for _, r := range found.Results { + c.Assert(r.Error, gc.IsNil) + results = append(results, r.Result) + } + c.Assert(results, jc.DeepEquals, []params.ApplicationOffer{ + { + SourceModelTag: "model-uuid", + ApplicationDescription: "description", + OfferName: "hosted-" + name, + OfferURL: url, + Access: "read", + Endpoints: []params.RemoteEndpoint{{Name: "db"}}}, + { + SourceModelTag: "model-uuid2", + ApplicationDescription: "description2", + OfferName: "hosted-" + name2, + OfferURL: url2, + Access: "consume", + Endpoints: []params.RemoteEndpoint{{Name: "db2"}}}, + }) + s.applicationOffers.CheckCallNames(c, listOffersBackendCall, listOffersBackendCall) +} + +func (s *applicationOffersSuite) assertFind(c *gc.C, expected []params.ApplicationOffer) { + filter := params.OfferFilters{ + Filters: []params.OfferFilter{ + { + OfferName: "hosted-db2", + }, + }, + } + found, err := s.api.FindApplicationOffers(filter) + c.Assert(err, jc.ErrorIsNil) + c.Assert(found, jc.DeepEquals, params.FindApplicationOffersResults{ + Results: expected, + }) + s.applicationOffers.CheckCallNames(c, listOffersBackendCall) +} + +func (s *applicationOffersSuite) TestFind(c *gc.C) { + s.setupOffers(c, "") + s.authorizer.Tag = names.NewUserTag("admin") + expected := []params.ApplicationOffer{ + { + SourceModelTag: "model-uuid", + ApplicationDescription: "description", + OfferName: "hosted-db2", + OfferURL: "fred/prod.hosted-db2", + Endpoints: []params.RemoteEndpoint{{Name: "db"}}, + Access: "admin"}} + s.assertFind(c, expected) +} + +func (s *applicationOffersSuite) TestFindNoPermission(c *gc.C) { + s.setupOffers(c, "") + s.authorizer.Tag = names.NewUserTag("someone") + s.assertFind(c, []params.ApplicationOffer{}) +} + +func (s *applicationOffersSuite) TestFindPermission(c *gc.C) { + s.setupOffers(c, "") + user := names.NewUserTag("someone") + s.authorizer.Tag = user + expected := []params.ApplicationOffer{ + { + SourceModelTag: "model-uuid", + ApplicationDescription: "description", + OfferName: "hosted-db2", + OfferURL: "fred/prod.hosted-db2", + Endpoints: []params.RemoteEndpoint{{Name: "db"}}, + Access: "read"}} + s.mockState.users.Add(user.Name()) + s.mockState.CreateOfferAccess(names.NewApplicationOfferTag("hosted-db2"), user, permission.ReadAccess) + s.assertFind(c, expected) +} + +func (s *applicationOffersSuite) TestFindMulti(c *gc.C) { + db2Offer := jujucrossmodel.ApplicationOffer{ + OfferName: "hosted-db2", + ApplicationName: "db2", + ApplicationDescription: "db2 description", + Endpoints: map[string]charm.Relation{"db": {Name: "db2"}}, + } + mysqlOffer := jujucrossmodel.ApplicationOffer{ + OfferName: "hosted-mysql", + ApplicationName: "mysql", + ApplicationDescription: "mysql description", + Endpoints: map[string]charm.Relation{"db": {Name: "mysql"}}, + } + postgresqlOffer := jujucrossmodel.ApplicationOffer{ + OfferName: "hosted-postgresql", + ApplicationName: "postgresql", + ApplicationDescription: "postgresql description", + Endpoints: map[string]charm.Relation{"db": {Name: "postgresql"}}, + } + + s.applicationOffers.listOffers = func(filters ...jujucrossmodel.ApplicationOfferFilter) ([]jujucrossmodel.ApplicationOffer, error) { + var result []jujucrossmodel.ApplicationOffer + for _, f := range filters { + switch f.OfferName { + case "hosted-db2": + result = append(result, db2Offer) + case "hosted-mysql": + result = append(result, mysqlOffer) + case "hosted-postgresql": + result = append(result, postgresqlOffer) + } + } + return result, nil + } + ch := &mockCharm{meta: &charm.Meta{Description: "A pretty popular blog engine"}} + s.mockState.applications = map[string]applicationoffers.Application{ + "db2": &mockApplication{charm: ch, curl: charm.MustParseURL("db2-2")}, + } + s.mockState.model = &mockModel{uuid: "uuid", name: "prod", owner: "fred"} + s.mockState.connStatus = &mockConnectionStatus{count: 5} + + user := names.NewUserTag("someone") + s.authorizer.Tag = user + s.mockState.users.Add(user.Name()) + s.mockState.CreateOfferAccess(names.NewApplicationOfferTag("hosted-db2"), user, permission.ConsumeAccess) + + anotherState := &mockState{ + modelUUID: "uuid2", + users: set.NewStrings(), + accessPerms: make(map[offerAccess]permission.Access), + } + s.mockStatePool.st["uuid2"] = anotherState + anotherState.applications = map[string]applicationoffers.Application{ + "mysql": &mockApplication{charm: ch, curl: charm.MustParseURL("mysql-2")}, + "postgresql": &mockApplication{charm: ch, curl: charm.MustParseURL("postgresql-2")}, + } + anotherState.model = &mockModel{uuid: "uuid2", name: "another", owner: "mary"} + anotherState.connStatus = &mockConnectionStatus{count: 15} + anotherState.users.Add(user.Name()) + anotherState.CreateOfferAccess(names.NewApplicationOfferTag("hosted-mysql"), user, permission.ReadAccess) + anotherState.CreateOfferAccess(names.NewApplicationOfferTag("hosted-postgresql"), user, permission.AdminAccess) + + s.mockState.allmodels = []applicationoffers.Model{ + s.mockState.model, + anotherState.model, + } + + filter := params.OfferFilters{ + Filters: []params.OfferFilter{ + { + OfferName: "hosted-db2", + OwnerName: "fred", + ModelName: "prod", + }, + { + OfferName: "hosted-mysql", + OwnerName: "mary", + ModelName: "another", + }, + { + OfferName: "hosted-postgresql", + OwnerName: "mary", + ModelName: "another", + }, + }, + } + found, err := s.api.FindApplicationOffers(filter) + c.Assert(err, jc.ErrorIsNil) + c.Assert(found, jc.DeepEquals, params.FindApplicationOffersResults{ + []params.ApplicationOffer{ + { + SourceModelTag: "model-uuid", + ApplicationDescription: "db2 description", + OfferName: "hosted-db2", + OfferURL: "fred/prod.hosted-db2", + Access: "consume", + Endpoints: []params.RemoteEndpoint{{Name: "db"}}, + }, + { + SourceModelTag: "model-uuid2", + ApplicationDescription: "mysql description", + OfferName: "hosted-mysql", + OfferURL: "mary/another.hosted-mysql", + Access: "read", + Endpoints: []params.RemoteEndpoint{{Name: "db"}}, + }, + { + SourceModelTag: "model-uuid2", + ApplicationDescription: "postgresql description", + OfferName: "hosted-postgresql", + OfferURL: "mary/another.hosted-postgresql", + Access: "admin", + Endpoints: []params.RemoteEndpoint{{Name: "db"}}, + }, + }, + }) + s.applicationOffers.CheckCallNames(c, listOffersBackendCall, listOffersBackendCall) +} + +func (s *applicationOffersSuite) TestFindError(c *gc.C) { + filter := params.OfferFilters{ + Filters: []params.OfferFilter{ + { + OfferName: "hosted-db2", + ApplicationName: "test", + }, + }, + } + msg := "fail" + + s.applicationOffers.listOffers = func(filters ...jujucrossmodel.ApplicationOfferFilter) ([]jujucrossmodel.ApplicationOffer, error) { + return nil, errors.New(msg) + } + s.mockState.model = &mockModel{uuid: "uuid", name: "prod", owner: "fred"} + + _, err := s.api.FindApplicationOffers(filter) + c.Assert(err, gc.ErrorMatches, fmt.Sprintf(".*%v.*", msg)) + s.applicationOffers.CheckCallNames(c, listOffersBackendCall) +} + +func (s *applicationOffersSuite) TestFindMissingModelInMultipleFilters(c *gc.C) { + filter := params.OfferFilters{ + Filters: []params.OfferFilter{ + { + OfferName: "hosted-db2", + ApplicationName: "test", + }, + { + OfferName: "hosted-mysql", + ApplicationName: "test", + }, + }, + } + + s.applicationOffers.listOffers = func(filters ...jujucrossmodel.ApplicationOfferFilter) ([]jujucrossmodel.ApplicationOffer, error) { + panic("should not be called") + } + + _, err := s.api.FindApplicationOffers(filter) + c.Assert(err, gc.ErrorMatches, "application offer filter must specify a model name") + s.applicationOffers.CheckCallNames(c) +} diff --git a/apiserver/common/crossmodelcommon/crossmodel.go b/apiserver/applicationoffers/base.go similarity index 90% rename from apiserver/common/crossmodelcommon/crossmodel.go rename to apiserver/applicationoffers/base.go index 6a2631dd5d7b..9f5fd365746e 100644 --- a/apiserver/common/crossmodelcommon/crossmodel.go +++ b/apiserver/applicationoffers/base.go @@ -1,7 +1,7 @@ // Copyright 2015 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. -package crossmodelcommon +package applicationoffers import ( "sort" @@ -24,8 +24,8 @@ type BaseAPI struct { StatePool StatePool } -// CheckPermission ensures that the logged in user holds the given permission on an entity. -func (api *BaseAPI) CheckPermission(tag names.Tag, perm permission.Access) error { +// checkPermission ensures that the logged in user holds the given permission on an entity. +func (api *BaseAPI) checkPermission(tag names.Tag, perm permission.Access) error { allowed, err := api.Authorizer.HasPermission(perm, tag) if err != nil { return errors.Trace(err) @@ -36,8 +36,8 @@ func (api *BaseAPI) CheckPermission(tag names.Tag, perm permission.Access) error return nil } -// CheckAdmin ensures that the logged in user is a model or controller admin. -func (api *BaseAPI) CheckAdmin(backend Backend) error { +// checkAdmin ensures that the logged in user is a model or controller admin. +func (api *BaseAPI) checkAdmin(backend Backend) error { allowed, err := api.Authorizer.HasPermission(permission.AdminAccess, backend.ModelTag()) if err != nil { return errors.Trace(err) @@ -54,8 +54,8 @@ func (api *BaseAPI) CheckAdmin(backend Backend) error { return nil } -// ModelForName looks up the model details for the named model. -func (api *BaseAPI) ModelForName(modelName, ownerName string) (Model, bool, error) { +// modelForName looks up the model details for the named model. +func (api *BaseAPI) modelForName(modelName, ownerName string) (Model, bool, error) { user := api.Authorizer.GetAuthTag().(names.UserTag) if ownerName == "" { ownerName = user.Name() @@ -74,8 +74,8 @@ func (api *BaseAPI) ModelForName(modelName, ownerName string) (Model, bool, erro return model, model != nil, nil } -// ApplicationOffersFromModel gets details about remote applications that match given filters. -func (api *BaseAPI) ApplicationOffersFromModel( +// applicationOffersFromModel gets details about remote applications that match given filters. +func (api *BaseAPI) applicationOffersFromModel( modelUUID string, requireAdmin bool, filters ...jujucrossmodel.ApplicationOfferFilter, @@ -92,7 +92,7 @@ func (api *BaseAPI) ApplicationOffersFromModel( // If requireAdmin is true, the user must be a controller superuser // or model admin to proceed. isAdmin := false - err := api.CheckAdmin(backend) + err := api.checkAdmin(backend) if err != nil && err != common.ErrPerm { return nil, errors.Trace(err) } @@ -191,7 +191,7 @@ func (api *BaseAPI) getModelFilters(filters params.OfferFilters) ( if f.ModelName != "" { if modelUUID, ok = modelUUIDs[f.ModelName]; !ok { var err error - model, ok, err := api.ModelForName(f.ModelName, f.OwnerName) + model, ok, err := api.modelForName(f.ModelName, f.OwnerName) if err != nil { return nil, nil, errors.Trace(err) } @@ -214,8 +214,8 @@ func (api *BaseAPI) getModelFilters(filters params.OfferFilters) ( return models, filtersPerModel, nil } -// GetApplicationOffersDetails gets details about remote applications that match given filter. -func (api *BaseAPI) GetApplicationOffersDetails( +// getApplicationOffersDetails gets details about remote applications that match given filter. +func (api *BaseAPI) getApplicationOffersDetails( filters params.OfferFilters, requireAdmin bool, ) ([]params.ApplicationOfferDetails, error) { @@ -246,7 +246,7 @@ func (api *BaseAPI) GetApplicationOffersDetails( var result []params.ApplicationOfferDetails for _, modelUUID := range allUUIDs { filters := filtersPerModel[modelUUID] - offers, err := api.ApplicationOffersFromModel(modelUUID, requireAdmin, filters...) + offers, err := api.applicationOffersFromModel(modelUUID, requireAdmin, filters...) if err != nil { return nil, common.ServerError(errors.Trace(err)) } diff --git a/apiserver/applicationoffers/base_test.go b/apiserver/applicationoffers/base_test.go index 64929f60e64e..e8f3f8dbba1f 100644 --- a/apiserver/applicationoffers/base_test.go +++ b/apiserver/applicationoffers/base_test.go @@ -10,8 +10,8 @@ import ( "gopkg.in/juju/charm.v6-unstable" "gopkg.in/juju/names.v2" + "github.com/juju/juju/apiserver/applicationoffers" "github.com/juju/juju/apiserver/common" - "github.com/juju/juju/apiserver/common/crossmodelcommon" "github.com/juju/juju/apiserver/testing" jujucrossmodel "github.com/juju/juju/core/crossmodel" "github.com/juju/juju/permission" @@ -44,7 +44,7 @@ func (s *baseSuite) SetUpTest(c *gc.C) { applicationOffers: make(map[string]jujucrossmodel.ApplicationOffer), accessPerms: make(map[offerAccess]permission.Access), } - s.mockStatePool = &mockStatePool{map[string]crossmodelcommon.Backend{s.mockState.modelUUID: s.mockState}} + s.mockStatePool = &mockStatePool{map[string]applicationoffers.Backend{s.mockState.modelUUID: s.mockState}} } func (s *baseSuite) addApplication(c *gc.C, name string) jujucrossmodel.ApplicationOffer { @@ -76,7 +76,7 @@ func (s *baseSuite) setupOffers(c *gc.C, filterAppName string) { return []jujucrossmodel.ApplicationOffer{anOffer}, nil } ch := &mockCharm{meta: &charm.Meta{Description: "A pretty popular blog engine"}} - s.mockState.applications = map[string]crossmodelcommon.Application{ + s.mockState.applications = map[string]applicationoffers.Application{ "test": &mockApplication{charm: ch, curl: charm.MustParseURL("db2-2")}, } s.mockState.model = &mockModel{uuid: "uuid", name: "prod", owner: "fred"} diff --git a/apiserver/applicationoffers/mock_test.go b/apiserver/applicationoffers/mock_test.go index a51449b8c415..79ea7bb5f252 100644 --- a/apiserver/applicationoffers/mock_test.go +++ b/apiserver/applicationoffers/mock_test.go @@ -12,7 +12,7 @@ import ( "gopkg.in/juju/charm.v6-unstable" "gopkg.in/juju/names.v2" - "github.com/juju/juju/apiserver/common/crossmodelcommon" + "github.com/juju/juju/apiserver/applicationoffers" jujucrossmodel "github.com/juju/juju/core/crossmodel" "github.com/juju/juju/permission" "github.com/juju/juju/testing" @@ -95,7 +95,7 @@ func (m *mockApplication) Name() string { return m.name } -func (m *mockApplication) Charm() (crossmodelcommon.Charm, bool, error) { +func (m *mockApplication) Charm() (applicationoffers.Charm, bool, error) { return m.charm, true, nil } @@ -118,11 +118,12 @@ type offerAccess struct { type mockState struct { modelUUID string - model crossmodelcommon.Model + model applicationoffers.Model + allmodels []applicationoffers.Model users set.Strings - applications map[string]crossmodelcommon.Application + applications map[string]applicationoffers.Application applicationOffers map[string]jujucrossmodel.ApplicationOffer - connStatus crossmodelcommon.RemoteConnectionStatus + connStatus applicationoffers.RemoteConnectionStatus accessPerms map[offerAccess]permission.Access } @@ -130,7 +131,7 @@ func (m *mockState) ControllerTag() names.ControllerTag { return testing.ControllerTag } -func (m *mockState) Application(name string) (crossmodelcommon.Application, error) { +func (m *mockState) Application(name string) (applicationoffers.Application, error) { app, ok := m.applications[name] if !ok { return nil, errors.NotFoundf("application %q", name) @@ -146,7 +147,7 @@ func (m *mockState) ApplicationOffer(name string) (*jujucrossmodel.ApplicationOf return &offer, nil } -func (m *mockState) Model() (crossmodelcommon.Model, error) { +func (m *mockState) Model() (applicationoffers.Model, error) { return m.model, nil } @@ -158,11 +159,14 @@ func (m *mockState) ModelTag() names.ModelTag { return names.NewModelTag(m.modelUUID) } -func (m *mockState) AllModels() ([]crossmodelcommon.Model, error) { - return []crossmodelcommon.Model{m.model}, nil +func (m *mockState) AllModels() ([]applicationoffers.Model, error) { + if len(m.allmodels) > 0 { + return m.allmodels, nil + } + return []applicationoffers.Model{m.model}, nil } -func (m *mockState) RemoteConnectionStatus(offerName string) (crossmodelcommon.RemoteConnectionStatus, error) { +func (m *mockState) RemoteConnectionStatus(offerName string) (applicationoffers.RemoteConnectionStatus, error) { return m.connStatus, nil } @@ -205,10 +209,10 @@ func (m *mockState) RemoveOfferAccess(offer names.ApplicationOfferTag, user name } type mockStatePool struct { - st map[string]crossmodelcommon.Backend + st map[string]applicationoffers.Backend } -func (st *mockStatePool) Get(modelUUID string) (crossmodelcommon.Backend, func(), error) { +func (st *mockStatePool) Get(modelUUID string) (applicationoffers.Backend, func(), error) { backend, ok := st.st[modelUUID] if !ok { return nil, nil, errors.NotFoundf("model for uuid %s", modelUUID) diff --git a/apiserver/common/crossmodelcommon/state.go b/apiserver/applicationoffers/state.go similarity index 99% rename from apiserver/common/crossmodelcommon/state.go rename to apiserver/applicationoffers/state.go index 5d46fd7c051a..293acc0b6b03 100644 --- a/apiserver/common/crossmodelcommon/state.go +++ b/apiserver/applicationoffers/state.go @@ -1,7 +1,7 @@ // Copyright 2015 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. -package crossmodelcommon +package applicationoffers import ( "github.com/juju/errors" diff --git a/apiserver/remoteendpoints/base_test.go b/apiserver/remoteendpoints/base_test.go deleted file mode 100644 index 499c0ff25653..000000000000 --- a/apiserver/remoteendpoints/base_test.go +++ /dev/null @@ -1,71 +0,0 @@ -// Copyright 2015 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package remoteendpoints_test - -import ( - jc "github.com/juju/testing/checkers" - gc "gopkg.in/check.v1" - "gopkg.in/juju/charm.v6-unstable" - "gopkg.in/juju/names.v2" - - "github.com/juju/juju/apiserver/common" - "github.com/juju/juju/apiserver/common/crossmodelcommon" - "github.com/juju/juju/apiserver/testing" - jujucrossmodel "github.com/juju/juju/core/crossmodel" - "github.com/juju/juju/permission" -) - -const ( - listOffersBackendCall = "listOffersCall" -) - -type baseSuite struct { - resources *common.Resources - authorizer *testing.FakeAuthorizer - - mockState *mockState - mockStatePool *mockStatePool - applicationOffers *mockApplicationOffers -} - -func (s *baseSuite) SetUpTest(c *gc.C) { - s.resources = common.NewResources() - s.authorizer = &testing.FakeAuthorizer{ - Tag: names.NewUserTag("read"), - AdminTag: names.NewUserTag("admin"), - } - - s.mockState = &mockState{ - modelUUID: "uuid", - offerAccess: make(map[names.ApplicationOfferTag]permission.Access), - } - s.mockStatePool = &mockStatePool{map[string]crossmodelcommon.Backend{s.mockState.modelUUID: s.mockState}} -} - -func (s *baseSuite) setupOffers(c *gc.C, filterAppName string) { - applicationName := "test" - offerName := "hosted-db2" - - anOffer := jujucrossmodel.ApplicationOffer{ - OfferName: offerName, - ApplicationName: applicationName, - ApplicationDescription: "description", - Endpoints: map[string]charm.Relation{"db": {Name: "db2"}}, - } - - s.applicationOffers.listOffers = func(filters ...jujucrossmodel.ApplicationOfferFilter) ([]jujucrossmodel.ApplicationOffer, error) { - c.Assert(filters, gc.HasLen, 1) - c.Assert(filters[0], jc.DeepEquals, jujucrossmodel.ApplicationOfferFilter{ - OfferName: offerName, - ApplicationName: filterAppName, - }) - return []jujucrossmodel.ApplicationOffer{anOffer}, nil - } - ch := &mockCharm{meta: &charm.Meta{Description: "A pretty popular blog engine"}} - s.mockState.applications = map[string]crossmodelcommon.Application{ - "test": &mockApplication{charm: ch, curl: charm.MustParseURL("db2-2")}, - } - s.mockState.model = &mockModel{uuid: "uuid", name: "prod", owner: "fred"} - s.mockState.connStatus = &mockConnectionStatus{count: 5} -} diff --git a/apiserver/remoteendpoints/export_test.go b/apiserver/remoteendpoints/export_test.go deleted file mode 100644 index b3bc6e8cfff1..000000000000 --- a/apiserver/remoteendpoints/export_test.go +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright 2015 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package remoteendpoints - -var ( - CreateEndpointsAPI = createEndpointsAPI -) diff --git a/apiserver/remoteendpoints/mock_test.go b/apiserver/remoteendpoints/mock_test.go deleted file mode 100644 index 6a009df8826d..000000000000 --- a/apiserver/remoteendpoints/mock_test.go +++ /dev/null @@ -1,158 +0,0 @@ -// Copyright 2015 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package remoteendpoints_test - -import ( - "github.com/juju/errors" - jtesting "github.com/juju/testing" - "gopkg.in/juju/charm.v6-unstable" - "gopkg.in/juju/names.v2" - - "github.com/juju/juju/apiserver/common/crossmodelcommon" - jujucrossmodel "github.com/juju/juju/core/crossmodel" - "github.com/juju/juju/permission" - "github.com/juju/juju/testing" -) - -const ( - listOffersCall = "listOffersCall" -) - -type mockApplicationOffers struct { - jtesting.Stub - jujucrossmodel.ApplicationOffers - - addOffer func(offer jujucrossmodel.AddApplicationOfferArgs) (*jujucrossmodel.ApplicationOffer, error) - listOffers func(filters ...jujucrossmodel.ApplicationOfferFilter) ([]jujucrossmodel.ApplicationOffer, error) -} - -func (m *mockApplicationOffers) ListOffers(filters ...jujucrossmodel.ApplicationOfferFilter) ([]jujucrossmodel.ApplicationOffer, error) { - m.AddCall(listOffersCall) - return m.listOffers(filters...) -} - -type mockModel struct { - uuid string - name string - owner string -} - -func (m *mockModel) UUID() string { - return m.uuid -} - -func (m *mockModel) Name() string { - return m.name -} - -func (m *mockModel) Owner() names.UserTag { - return names.NewUserTag(m.owner) -} - -type mockUserModel struct { - model crossmodelcommon.Model -} - -func (m *mockUserModel) Model() crossmodelcommon.Model { - return m.model -} - -type mockCharm struct { - meta *charm.Meta -} - -func (m *mockCharm) Meta() *charm.Meta { - return m.meta -} - -type mockApplication struct { - name string - charm *mockCharm - curl *charm.URL -} - -func (m *mockApplication) Name() string { - return m.name -} - -func (m *mockApplication) Charm() (crossmodelcommon.Charm, bool, error) { - return m.charm, true, nil -} - -func (m *mockApplication) CharmURL() (curl *charm.URL, force bool) { - return m.curl, true -} - -type mockConnectionStatus struct { - count int -} - -func (m *mockConnectionStatus) ConnectionCount() int { - return m.count -} - -type mockState struct { - crossmodelcommon.Backend - modelUUID string - model crossmodelcommon.Model - allmodels []crossmodelcommon.Model - applications map[string]crossmodelcommon.Application - connStatus crossmodelcommon.RemoteConnectionStatus - offerAccess map[names.ApplicationOfferTag]permission.Access -} - -func (m *mockState) Application(name string) (crossmodelcommon.Application, error) { - app, ok := m.applications[name] - if !ok { - return nil, errors.NotFoundf("application %q", name) - } - return app, nil -} - -func (m *mockState) Model() (crossmodelcommon.Model, error) { - return m.model, nil -} - -func (m *mockState) ModelUUID() string { - return m.modelUUID -} - -func (m *mockState) ModelTag() names.ModelTag { - return names.NewModelTag(m.modelUUID) -} - -func (m *mockState) ControllerTag() names.ControllerTag { - return testing.ControllerTag -} - -func (m *mockState) AllModels() ([]crossmodelcommon.Model, error) { - if len(m.allmodels) > 0 { - return m.allmodels, nil - } - return []crossmodelcommon.Model{m.model}, nil -} - -func (m *mockState) GetOfferAccess(offer names.ApplicationOfferTag, user names.UserTag) (permission.Access, error) { - access, ok := m.offerAccess[offer] - if !ok { - return permission.NoAccess, errors.NotFoundf("access for %q", offer) - } - return access, nil -} - -func (m *mockState) RemoteConnectionStatus(offerName string) (crossmodelcommon.RemoteConnectionStatus, error) { - return m.connStatus, nil -} - -type mockStatePool struct { - st map[string]crossmodelcommon.Backend -} - -func (st *mockStatePool) Get(modelUUID string) (crossmodelcommon.Backend, func(), error) { - backend, ok := st.st[modelUUID] - if !ok { - return nil, nil, errors.NotFoundf("model for uuid %s", modelUUID) - } - return backend, func() {}, nil -} diff --git a/apiserver/remoteendpoints/package_test.go b/apiserver/remoteendpoints/package_test.go deleted file mode 100644 index e06b21d06c2e..000000000000 --- a/apiserver/remoteendpoints/package_test.go +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright 2015 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package remoteendpoints_test - -import ( - "testing" - - gc "gopkg.in/check.v1" -) - -func TestAll(t *testing.T) { - gc.TestingT(t) -} diff --git a/apiserver/remoteendpoints/remoteendpoints.go b/apiserver/remoteendpoints/remoteendpoints.go deleted file mode 100644 index c190ef635569..000000000000 --- a/apiserver/remoteendpoints/remoteendpoints.go +++ /dev/null @@ -1,147 +0,0 @@ -// Copyright 2015 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package remoteendpoints - -import ( - "github.com/juju/errors" - - "github.com/juju/juju/apiserver/common" - "github.com/juju/juju/apiserver/common/crossmodelcommon" - "github.com/juju/juju/apiserver/facade" - "github.com/juju/juju/apiserver/params" - jujucrossmodel "github.com/juju/juju/core/crossmodel" -) - -// EndpointsAPI implements the cross model interface and is the concrete -// implementation of the api end point. -type EndpointsAPI struct { - crossmodelcommon.BaseAPI -} - -// createEndpointsAPI returns a new EndpointsAPI facade. -func createEndpointsAPI( - getApplicationOffers func(interface{}) jujucrossmodel.ApplicationOffers, - backend crossmodelcommon.Backend, - statePool crossmodelcommon.StatePool, - authorizer facade.Authorizer, -) (*EndpointsAPI, error) { - if !authorizer.AuthClient() { - return nil, common.ErrPerm - } - - api := &EndpointsAPI{ - BaseAPI: crossmodelcommon.BaseAPI{ - Authorizer: authorizer, - GetApplicationOffers: getApplicationOffers, - Backend: backend, - StatePool: statePool, - }} - return api, nil -} - -// NewEndpointsAPI returns a new EndpointsAPI facade. -func NewEndpointsAPI(ctx facade.Context) (*EndpointsAPI, error) { - return createEndpointsAPI( - crossmodelcommon.GetApplicationOffers, crossmodelcommon.GetStateAccess(ctx.State()), - crossmodelcommon.GetStatePool(ctx.StatePool()), ctx.Auth()) -} - -// ApplicationOffers gets details about remote applications that match given URLs. -func (api *EndpointsAPI) ApplicationOffers(urls params.ApplicationURLs) (params.ApplicationOffersResults, error) { - var results params.ApplicationOffersResults - results.Results = make([]params.ApplicationOfferResult, len(urls.ApplicationURLs)) - - for i, urlStr := range urls.ApplicationURLs { - offer, err := api.offerForURL(urlStr) - if err != nil { - results.Results[i].Error = common.ServerError(err) - continue - } - results.Results[i].Result = offer.ApplicationOffer - } - return results, nil -} - -// offerForURL finds the single offer for a specified (possibly relative) URL, -// returning the offer and full URL. -func (api *EndpointsAPI) offerForURL(urlStr string) (params.ApplicationOfferDetails, error) { - fail := func(err error) (params.ApplicationOfferDetails, error) { - return params.ApplicationOfferDetails{}, errors.Trace(err) - } - - url, err := jujucrossmodel.ParseApplicationURL(urlStr) - if err != nil { - return fail(errors.Trace(err)) - } - if url.Source != "" { - err = errors.NotSupportedf("query for non-local application offers") - return fail(errors.Trace(err)) - } - - model, ok, err := api.ModelForName(url.ModelName, url.User) - if err != nil { - return fail(errors.Trace(err)) - } - if !ok { - err = errors.NotFoundf("model %q", url.ModelName) - return fail(err) - } - filter := jujucrossmodel.ApplicationOfferFilter{ - OfferName: url.ApplicationName, - } - offers, err := api.ApplicationOffersFromModel(model.UUID(), false, filter) - if err != nil { - return fail(errors.Trace(err)) - } - if len(offers) == 0 { - err := errors.NotFoundf("application offer %q", url.ApplicationName) - return fail(err) - } - if len(offers) > 1 { - err := errors.Errorf("too many application offers for %q", url.ApplicationName) - return fail(err) - } - fullURL := jujucrossmodel.MakeURL(model.Owner().Name(), model.Name(), url.ApplicationName, "") - offer := offers[0] - offer.OfferURL = fullURL - return offer, nil -} - -// FindApplicationOffers gets details about remote applications that match given filter. -func (api *EndpointsAPI) FindApplicationOffers(filters params.OfferFilters) (params.FindApplicationOffersResults, error) { - var result params.FindApplicationOffersResults - var filtersToUse params.OfferFilters - - // If there is only one filter term, and no model is specified, add in - // any models the user can see and query across those. - // If there's more than one filter term, each must specify a model. - if len(filters.Filters) == 1 && filters.Filters[0].ModelName == "" { - allModels, err := api.Backend.AllModels() - if err != nil { - return result, errors.Trace(err) - } - for _, m := range allModels { - modelFilter := filters.Filters[0] - modelFilter.ModelName = m.Name() - modelFilter.OwnerName = m.Owner().Name() - filtersToUse.Filters = append(filtersToUse.Filters, modelFilter) - } - } else { - filtersToUse = filters - } - for _, f := range filtersToUse.Filters { - if f.ModelName == "" { - return result, errors.New("application offer filter must specify a model name") - } - } - - offers, err := api.GetApplicationOffersDetails(filtersToUse, false) - if err != nil { - return result, errors.Trace(err) - } - for _, offer := range offers { - result.Results = append(result.Results, offer.ApplicationOffer) - } - return result, nil -} diff --git a/apiserver/remoteendpoints/remoteendpoints_test.go b/apiserver/remoteendpoints/remoteendpoints_test.go deleted file mode 100644 index 24d0d6b57740..000000000000 --- a/apiserver/remoteendpoints/remoteendpoints_test.go +++ /dev/null @@ -1,446 +0,0 @@ -// Copyright 2015 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package remoteendpoints_test - -import ( - "fmt" - - "github.com/juju/errors" - jc "github.com/juju/testing/checkers" - gc "gopkg.in/check.v1" - "gopkg.in/juju/charm.v6-unstable" - "gopkg.in/juju/names.v2" - - "github.com/juju/juju/apiserver/common" - "github.com/juju/juju/apiserver/common/crossmodelcommon" - "github.com/juju/juju/apiserver/params" - "github.com/juju/juju/apiserver/remoteendpoints" - jujucrossmodel "github.com/juju/juju/core/crossmodel" - "github.com/juju/juju/permission" -) - -type remoteEndpointsSuite struct { - baseSuite - api *remoteendpoints.EndpointsAPI -} - -var _ = gc.Suite(&remoteEndpointsSuite{}) - -func (s *remoteEndpointsSuite) SetUpTest(c *gc.C) { - s.baseSuite.SetUpTest(c) - s.applicationOffers = &mockApplicationOffers{} - getApplicationOffers := func(interface{}) jujucrossmodel.ApplicationOffers { - return s.applicationOffers - } - - var err error - s.api, err = remoteendpoints.CreateEndpointsAPI( - getApplicationOffers, s.mockState, s.mockStatePool, s.authorizer, - ) - c.Assert(err, jc.ErrorIsNil) -} - -func (s *remoteEndpointsSuite) assertShow(c *gc.C, expected []params.ApplicationOfferResult) { - applicationName := "test" - filter := params.ApplicationURLs{[]string{"fred/prod.hosted-db2"}} - anOffer := jujucrossmodel.ApplicationOffer{ - ApplicationName: applicationName, - ApplicationDescription: "description", - OfferName: "hosted-db2", - Endpoints: map[string]charm.Relation{"db": {Name: "db"}}, - } - - s.applicationOffers.listOffers = func(filters ...jujucrossmodel.ApplicationOfferFilter) ([]jujucrossmodel.ApplicationOffer, error) { - return []jujucrossmodel.ApplicationOffer{anOffer}, nil - } - ch := &mockCharm{meta: &charm.Meta{Description: "A pretty popular blog engine"}} - s.mockState.applications = map[string]crossmodelcommon.Application{ - applicationName: &mockApplication{charm: ch, curl: charm.MustParseURL("db2-2")}, - } - s.mockState.model = &mockModel{uuid: "uuid", name: "prod", owner: "fred"} - s.mockState.connStatus = &mockConnectionStatus{count: 5} - - found, err := s.api.ApplicationOffers(filter) - c.Assert(err, jc.ErrorIsNil) - c.Assert(found.Results, jc.DeepEquals, expected) - s.applicationOffers.CheckCallNames(c, listOffersBackendCall) -} - -func (s *remoteEndpointsSuite) TestShow(c *gc.C) { - expected := []params.ApplicationOfferResult{{ - Result: params.ApplicationOffer{ - SourceModelTag: "model-uuid", - ApplicationDescription: "description", - OfferURL: "fred/prod.hosted-db2", - OfferName: "hosted-db2", - Endpoints: []params.RemoteEndpoint{{Name: "db"}}, - Access: "admin"}, - }} - s.authorizer.Tag = names.NewUserTag("admin") - s.assertShow(c, expected) -} - -func (s *remoteEndpointsSuite) TestShowNoPermission(c *gc.C) { - s.authorizer.Tag = names.NewUserTag("someone") - expected := []params.ApplicationOfferResult{{ - Error: common.ServerError(errors.NotFoundf("application offer %q", "hosted-db2")), - }} - s.assertShow(c, expected) -} - -func (s *remoteEndpointsSuite) TestShowPermission(c *gc.C) { - s.authorizer.Tag = names.NewUserTag("someone") - expected := []params.ApplicationOfferResult{{ - Result: params.ApplicationOffer{ - SourceModelTag: "model-uuid", - ApplicationDescription: "description", - OfferURL: "fred/prod.hosted-db2", - OfferName: "hosted-db2", - Endpoints: []params.RemoteEndpoint{{Name: "db"}}, - Access: "read"}, - }} - s.mockState.offerAccess[names.NewApplicationOfferTag("hosted-db2")] = permission.ReadAccess - s.assertShow(c, expected) -} - -func (s *remoteEndpointsSuite) TestShowError(c *gc.C) { - url := "fred/prod.hosted-db2" - filter := params.ApplicationURLs{[]string{url}} - msg := "fail" - - s.applicationOffers.listOffers = func(filters ...jujucrossmodel.ApplicationOfferFilter) ([]jujucrossmodel.ApplicationOffer, error) { - return nil, errors.New(msg) - } - s.mockState.model = &mockModel{uuid: "uuid", name: "prod", owner: "fred"} - - result, err := s.api.ApplicationOffers(filter) - c.Assert(err, jc.ErrorIsNil) - c.Assert(result.Results, gc.HasLen, 1) - c.Assert(result.Results[0].Error, gc.ErrorMatches, fmt.Sprintf(".*%v.*", msg)) - s.applicationOffers.CheckCallNames(c, listOffersBackendCall) -} - -func (s *remoteEndpointsSuite) TestShowNotFound(c *gc.C) { - urls := []string{"fred/prod.hosted-db2"} - filter := params.ApplicationURLs{urls} - - s.applicationOffers.listOffers = func(filters ...jujucrossmodel.ApplicationOfferFilter) ([]jujucrossmodel.ApplicationOffer, error) { - return nil, nil - } - s.mockState.model = &mockModel{uuid: "uuid", name: "prod", owner: "fred"} - - found, err := s.api.ApplicationOffers(filter) - c.Assert(err, jc.ErrorIsNil) - c.Assert(found.Results, gc.HasLen, 1) - c.Assert(found.Results[0].Error.Error(), gc.Matches, `application offer "hosted-db2" not found`) - s.applicationOffers.CheckCallNames(c, listOffersBackendCall) -} - -func (s *remoteEndpointsSuite) TestShowErrorMsgMultipleURLs(c *gc.C) { - urls := []string{"fred/prod.hosted-mysql", "fred/test.hosted-db2"} - filter := params.ApplicationURLs{urls} - - s.applicationOffers.listOffers = func(filters ...jujucrossmodel.ApplicationOfferFilter) ([]jujucrossmodel.ApplicationOffer, error) { - return nil, nil - } - s.mockState.model = &mockModel{uuid: "uuid", name: "prod", owner: "fred"} - s.mockState.allmodels = []crossmodelcommon.Model{ - s.mockState.model, - &mockModel{uuid: "uuid2", name: "test", owner: "fred"}, - } - anotherState := &mockState{modelUUID: "uuid2"} - s.mockStatePool.st["uuid2"] = anotherState - - found, err := s.api.ApplicationOffers(filter) - c.Assert(err, jc.ErrorIsNil) - c.Assert(found.Results, gc.HasLen, 2) - c.Assert(found.Results[0].Error.Error(), gc.Matches, `application offer "hosted-mysql" not found`) - c.Assert(found.Results[1].Error.Error(), gc.Matches, `application offer "hosted-db2" not found`) - s.applicationOffers.CheckCallNames(c, listOffersBackendCall, listOffersBackendCall) -} - -func (s *remoteEndpointsSuite) TestShowFoundMultiple(c *gc.C) { - name := "test" - url := "fred/prod.hosted-" + name - anOffer := jujucrossmodel.ApplicationOffer{ - ApplicationName: name, - ApplicationDescription: "description", - OfferName: "hosted-" + name, - Endpoints: map[string]charm.Relation{"db": {Name: "db"}}, - } - - name2 := "testagain" - url2 := "mary/test.hosted-" + name2 - anOffer2 := jujucrossmodel.ApplicationOffer{ - ApplicationName: name2, - ApplicationDescription: "description2", - OfferName: "hosted-" + name2, - Endpoints: map[string]charm.Relation{"db2": {Name: "db2"}}, - } - - filter := params.ApplicationURLs{[]string{url, url2}} - - s.applicationOffers.listOffers = func(filters ...jujucrossmodel.ApplicationOfferFilter) ([]jujucrossmodel.ApplicationOffer, error) { - c.Assert(filters, gc.HasLen, 1) - if filters[0].OfferName == "hosted-test" { - return []jujucrossmodel.ApplicationOffer{anOffer}, nil - } - return []jujucrossmodel.ApplicationOffer{anOffer2}, nil - } - ch := &mockCharm{meta: &charm.Meta{Description: "A pretty popular blog engine"}} - s.mockState.applications = map[string]crossmodelcommon.Application{ - "test": &mockApplication{charm: ch, curl: charm.MustParseURL("db2-2")}, - } - s.mockState.model = &mockModel{uuid: "uuid", name: "prod", owner: "fred"} - s.mockState.allmodels = []crossmodelcommon.Model{ - s.mockState.model, - &mockModel{uuid: "uuid2", name: "test", owner: "mary"}, - } - s.mockState.connStatus = &mockConnectionStatus{count: 5} - s.mockState.offerAccess[names.NewApplicationOfferTag("hosted-test")] = permission.ReadAccess - - anotherState := &mockState{ - modelUUID: "uuid2", - offerAccess: make(map[names.ApplicationOfferTag]permission.Access), - } - anotherState.applications = map[string]crossmodelcommon.Application{ - "testagain": &mockApplication{charm: ch, curl: charm.MustParseURL("mysql-2")}, - } - anotherState.connStatus = &mockConnectionStatus{count: 5} - anotherState.offerAccess[names.NewApplicationOfferTag("hosted-testagain")] = permission.ConsumeAccess - s.mockStatePool.st["uuid2"] = anotherState - - found, err := s.api.ApplicationOffers(filter) - c.Assert(err, jc.ErrorIsNil) - var results []params.ApplicationOffer - for _, r := range found.Results { - c.Assert(r.Error, gc.IsNil) - results = append(results, r.Result) - } - c.Assert(results, jc.DeepEquals, []params.ApplicationOffer{ - { - SourceModelTag: "model-uuid", - ApplicationDescription: "description", - OfferName: "hosted-" + name, - OfferURL: url, - Access: "read", - Endpoints: []params.RemoteEndpoint{{Name: "db"}}}, - { - SourceModelTag: "model-uuid2", - ApplicationDescription: "description2", - OfferName: "hosted-" + name2, - OfferURL: url2, - Access: "consume", - Endpoints: []params.RemoteEndpoint{{Name: "db2"}}}, - }) - s.applicationOffers.CheckCallNames(c, listOffersBackendCall, listOffersBackendCall) -} - -func (s *remoteEndpointsSuite) assertFind(c *gc.C, expected []params.ApplicationOffer) { - filter := params.OfferFilters{ - Filters: []params.OfferFilter{ - { - OfferName: "hosted-db2", - }, - }, - } - found, err := s.api.FindApplicationOffers(filter) - c.Assert(err, jc.ErrorIsNil) - c.Assert(found, jc.DeepEquals, params.FindApplicationOffersResults{ - Results: expected, - }) - s.applicationOffers.CheckCallNames(c, listOffersBackendCall) -} - -func (s *remoteEndpointsSuite) TestFind(c *gc.C) { - s.setupOffers(c, "") - s.authorizer.Tag = names.NewUserTag("admin") - expected := []params.ApplicationOffer{ - { - SourceModelTag: "model-uuid", - ApplicationDescription: "description", - OfferName: "hosted-db2", - OfferURL: "fred/prod.hosted-db2", - Endpoints: []params.RemoteEndpoint{{Name: "db"}}, - Access: "admin"}} - s.assertFind(c, expected) -} - -func (s *remoteEndpointsSuite) TestFindNoPermission(c *gc.C) { - s.setupOffers(c, "") - s.authorizer.Tag = names.NewUserTag("someone") - s.assertFind(c, []params.ApplicationOffer{}) -} - -func (s *remoteEndpointsSuite) TestFindPermission(c *gc.C) { - s.setupOffers(c, "") - s.authorizer.Tag = names.NewUserTag("someone") - expected := []params.ApplicationOffer{ - { - SourceModelTag: "model-uuid", - ApplicationDescription: "description", - OfferName: "hosted-db2", - OfferURL: "fred/prod.hosted-db2", - Endpoints: []params.RemoteEndpoint{{Name: "db"}}, - Access: "read"}} - s.mockState.offerAccess[names.NewApplicationOfferTag("hosted-db2")] = permission.ReadAccess - s.assertFind(c, expected) -} - -func (s *remoteEndpointsSuite) TestFindMulti(c *gc.C) { - db2Offer := jujucrossmodel.ApplicationOffer{ - OfferName: "hosted-db2", - ApplicationName: "db2", - ApplicationDescription: "db2 description", - Endpoints: map[string]charm.Relation{"db": {Name: "db2"}}, - } - mysqlOffer := jujucrossmodel.ApplicationOffer{ - OfferName: "hosted-mysql", - ApplicationName: "mysql", - ApplicationDescription: "mysql description", - Endpoints: map[string]charm.Relation{"db": {Name: "mysql"}}, - } - postgresqlOffer := jujucrossmodel.ApplicationOffer{ - OfferName: "hosted-postgresql", - ApplicationName: "postgresql", - ApplicationDescription: "postgresql description", - Endpoints: map[string]charm.Relation{"db": {Name: "postgresql"}}, - } - - s.applicationOffers.listOffers = func(filters ...jujucrossmodel.ApplicationOfferFilter) ([]jujucrossmodel.ApplicationOffer, error) { - var result []jujucrossmodel.ApplicationOffer - for _, f := range filters { - switch f.OfferName { - case "hosted-db2": - result = append(result, db2Offer) - case "hosted-mysql": - result = append(result, mysqlOffer) - case "hosted-postgresql": - result = append(result, postgresqlOffer) - } - } - return result, nil - } - ch := &mockCharm{meta: &charm.Meta{Description: "A pretty popular blog engine"}} - s.mockState.applications = map[string]crossmodelcommon.Application{ - "db2": &mockApplication{charm: ch, curl: charm.MustParseURL("db2-2")}, - } - s.mockState.model = &mockModel{uuid: "uuid", name: "prod", owner: "fred"} - s.mockState.connStatus = &mockConnectionStatus{count: 5} - s.mockState.offerAccess[names.NewApplicationOfferTag("hosted-db2")] = permission.ConsumeAccess - - anotherState := &mockState{ - modelUUID: "uuid2", - offerAccess: make(map[names.ApplicationOfferTag]permission.Access), - } - s.mockStatePool.st["uuid2"] = anotherState - anotherState.applications = map[string]crossmodelcommon.Application{ - "mysql": &mockApplication{charm: ch, curl: charm.MustParseURL("mysql-2")}, - "postgresql": &mockApplication{charm: ch, curl: charm.MustParseURL("postgresql-2")}, - } - anotherState.model = &mockModel{uuid: "uuid2", name: "another", owner: "mary"} - anotherState.connStatus = &mockConnectionStatus{count: 15} - anotherState.offerAccess[names.NewApplicationOfferTag("hosted-mysql")] = permission.ReadAccess - anotherState.offerAccess[names.NewApplicationOfferTag("hosted-postgresql")] = permission.AdminAccess - - s.mockState.allmodels = []crossmodelcommon.Model{ - s.mockState.model, - anotherState.model, - } - - filter := params.OfferFilters{ - Filters: []params.OfferFilter{ - { - OfferName: "hosted-db2", - OwnerName: "fred", - ModelName: "prod", - }, - { - OfferName: "hosted-mysql", - OwnerName: "mary", - ModelName: "another", - }, - { - OfferName: "hosted-postgresql", - OwnerName: "mary", - ModelName: "another", - }, - }, - } - found, err := s.api.FindApplicationOffers(filter) - c.Assert(err, jc.ErrorIsNil) - c.Assert(found, jc.DeepEquals, params.FindApplicationOffersResults{ - []params.ApplicationOffer{ - { - SourceModelTag: "model-uuid", - ApplicationDescription: "db2 description", - OfferName: "hosted-db2", - OfferURL: "fred/prod.hosted-db2", - Access: "consume", - Endpoints: []params.RemoteEndpoint{{Name: "db"}}, - }, - { - SourceModelTag: "model-uuid2", - ApplicationDescription: "mysql description", - OfferName: "hosted-mysql", - OfferURL: "mary/another.hosted-mysql", - Access: "read", - Endpoints: []params.RemoteEndpoint{{Name: "db"}}, - }, - { - SourceModelTag: "model-uuid2", - ApplicationDescription: "postgresql description", - OfferName: "hosted-postgresql", - OfferURL: "mary/another.hosted-postgresql", - Access: "admin", - Endpoints: []params.RemoteEndpoint{{Name: "db"}}, - }, - }, - }) - s.applicationOffers.CheckCallNames(c, listOffersBackendCall, listOffersBackendCall) -} - -func (s *remoteEndpointsSuite) TestFindError(c *gc.C) { - filter := params.OfferFilters{ - Filters: []params.OfferFilter{ - { - OfferName: "hosted-db2", - ApplicationName: "test", - }, - }, - } - msg := "fail" - - s.applicationOffers.listOffers = func(filters ...jujucrossmodel.ApplicationOfferFilter) ([]jujucrossmodel.ApplicationOffer, error) { - return nil, errors.New(msg) - } - s.mockState.model = &mockModel{uuid: "uuid", name: "prod", owner: "fred"} - - _, err := s.api.FindApplicationOffers(filter) - c.Assert(err, gc.ErrorMatches, fmt.Sprintf(".*%v.*", msg)) - s.applicationOffers.CheckCallNames(c, listOffersBackendCall) -} - -func (s *remoteEndpointsSuite) TestFindMissingModelInMultipleFilters(c *gc.C) { - filter := params.OfferFilters{ - Filters: []params.OfferFilter{ - { - OfferName: "hosted-db2", - ApplicationName: "test", - }, - { - OfferName: "hosted-mysql", - ApplicationName: "test", - }, - }, - } - - s.applicationOffers.listOffers = func(filters ...jujucrossmodel.ApplicationOfferFilter) ([]jujucrossmodel.ApplicationOffer, error) { - panic("should not be called") - } - - _, err := s.api.FindApplicationOffers(filter) - c.Assert(err, gc.ErrorMatches, "application offer filter must specify a model name") - s.applicationOffers.CheckCallNames(c) -} diff --git a/apiserver/restrict_controller.go b/apiserver/restrict_controller.go index f75160e3e242..dce16a2e86da 100644 --- a/apiserver/restrict_controller.go +++ b/apiserver/restrict_controller.go @@ -15,11 +15,11 @@ import ( // independently of individual models. var controllerFacadeNames = set.NewStrings( "AllModelWatcher", + "ApplicationOffers", "Cloud", "Controller", "MigrationTarget", "ModelManager", - "RemoteEndpoints", "UserManager", ) diff --git a/apiserver/restrict_controller_test.go b/apiserver/restrict_controller_test.go index 36a31526bbe6..fc39907012d7 100644 --- a/apiserver/restrict_controller_test.go +++ b/apiserver/restrict_controller_test.go @@ -35,7 +35,7 @@ func (s *restrictControllerSuite) TestAllowed(c *gc.C) { s.assertMethod(c, "Pinger", 1, "Ping") s.assertMethod(c, "Bundle", 1, "GetChanges") s.assertMethod(c, "HighAvailability", 2, "EnableHA") - s.assertMethod(c, "RemoteEndpoints", 1, "ApplicationOffers") + s.assertMethod(c, "ApplicationOffers", 1, "ApplicationOffers") } func (s *restrictControllerSuite) TestNotAllowed(c *gc.C) { diff --git a/cmd/juju/crossmodel/applicationoffers.go b/cmd/juju/crossmodel/applicationoffers.go index a7105a20d128..1836155f411e 100644 --- a/cmd/juju/crossmodel/applicationoffers.go +++ b/cmd/juju/crossmodel/applicationoffers.go @@ -10,7 +10,7 @@ import ( // ApplicationOffersCommandBase is a base for various cross model commands. type ApplicationOffersCommandBase struct { - modelcmd.ModelCommandBase + modelcmd.ControllerCommandBase } // NewApplicationOffersAPI returns an application offers api for the root api endpoint diff --git a/cmd/juju/crossmodel/export_test.go b/cmd/juju/crossmodel/export_test.go index 233ee560f5f1..52a859fd8963 100644 --- a/cmd/juju/crossmodel/export_test.go +++ b/cmd/juju/crossmodel/export_test.go @@ -23,7 +23,7 @@ func NewOfferCommandForTest(store jujuclient.ClientStore, api OfferAPI) cmd.Comm return api, nil }} aCmd.SetClientStore(store) - return modelcmd.Wrap(aCmd) + return modelcmd.WrapController(aCmd) } func NewShowEndpointsCommandForTest(store jujuclient.ClientStore, api ShowAPI) cmd.Command { @@ -39,7 +39,7 @@ func NewListEndpointsCommandForTest(store jujuclient.ClientStore, api ListAPI) c return api, nil }} aCmd.SetClientStore(store) - return modelcmd.Wrap(aCmd) + return modelcmd.WrapController(aCmd) } func NewFindEndpointsCommandForTest(store jujuclient.ClientStore, api FindAPI) cmd.Command { diff --git a/cmd/juju/crossmodel/list.go b/cmd/juju/crossmodel/list.go index 6a7a3ae7643d..d48bd692b018 100644 --- a/cmd/juju/crossmodel/list.go +++ b/cmd/juju/crossmodel/list.go @@ -63,12 +63,25 @@ func NewListEndpointsCommand() cmd.Command { listCmd.newAPIFunc = func() (ListAPI, error) { return listCmd.NewApplicationOffersAPI() } - return modelcmd.Wrap(listCmd) + return modelcmd.WrapController(listCmd) } // Init implements Command.Init. func (c *listCommand) Init(args []string) (err error) { // TODO (anastasiamac 2015-11-17) need to get filters from user input + // For now, just filter on the current model. + modelName, err := c.ClientStore().CurrentModel(c.ControllerName()) + if err != nil { + if errors.IsNotFound(err) { + return errors.New("no current model, use juju switch to select a model on which to operate") + } else { + return errors.Annotate(err, "cannot load current model") + } + } + c.filters = []crossmodel.ApplicationOfferFilter{{ + ModelName: modelName, + }} + return cmd.CheckEmpty(args) } diff --git a/cmd/juju/crossmodel/list_test.go b/cmd/juju/crossmodel/list_test.go index f6937dd39544..88e45f833393 100644 --- a/cmd/juju/crossmodel/list_test.go +++ b/cmd/juju/crossmodel/list_test.go @@ -48,6 +48,12 @@ func (s *ListSuite) SetUpTest(c *gc.C) { } } +func (s *ListSuite) TestListNoCurrentModel(c *gc.C) { + s.store.Models["test-master"].CurrentModel = "" + _, err := s.runList(c, nil) + c.Assert(err, gc.ErrorMatches, `no current model, use juju switch to select a model on which to operate`) +} + func (s *ListSuite) TestListError(c *gc.C) { msg := "fail api" diff --git a/cmd/juju/crossmodel/offer.go b/cmd/juju/crossmodel/offer.go index 8f55c670fe09..3657b1d3810d 100644 --- a/cmd/juju/crossmodel/offer.go +++ b/cmd/juju/crossmodel/offer.go @@ -27,6 +27,7 @@ an offer name is explicitly specified. Examples: $ juju offer mysql:db +$ juju offer mymodel.mysql:db $ juju offer db2:db hosted-db2 $ juju offer db2:db,log hosted-db2 @@ -42,7 +43,7 @@ func NewOfferCommand() cmd.Command { offerCmd.newAPIFunc = func() (OfferAPI, error) { return offerCmd.NewApplicationOffersAPI() } - return modelcmd.Wrap(offerCmd) + return modelcmd.WrapController(offerCmd) } type offerCommand struct { @@ -57,6 +58,9 @@ type offerCommand struct { // OfferName stores the name of the offer OfferName string + + // ModelName stores the name of the offer + ModelName string } // Info implements Command.Info. @@ -64,7 +68,7 @@ func (c *offerCommand) Info() *cmd.Info { return &cmd.Info{ Name: "offer", Purpose: "Offer application endpoints for use in other models", - Args: ":[,...] [offer-name]", + Args: "[model-name.]:[,...] [offer-name]", Doc: offerCommandDoc, } } @@ -109,16 +113,26 @@ func (c *offerCommand) Run(ctx *cmd.Context) error { if err := (params.ErrorResults{results}).Combine(); err != nil { return err } + if c.ModelName == "" { + c.ModelName, err = c.ClientStore().CurrentModel(c.ControllerName()) + if err != nil { + if errors.IsNotFound(err) { + return errors.New("no current model, use juju switch to select a model on which to operate") + } else { + return errors.Annotate(err, "cannot load current model") + } + } + } var unqualifiedModelName, owner string - if jujuclient.IsQualifiedModelName(c.ModelName()) { + if jujuclient.IsQualifiedModelName(c.ModelName) { var ownerTag names.UserTag - unqualifiedModelName, ownerTag, err = jujuclient.SplitModelName(c.ModelName()) + unqualifiedModelName, ownerTag, err = jujuclient.SplitModelName(c.ModelName) if err != nil { return err } owner = ownerTag.Name() } else { - unqualifiedModelName = c.ModelName() + unqualifiedModelName = c.ModelName account, err := c.ClientStore().AccountDetails(c.ControllerName()) if err != nil { return err @@ -142,12 +156,16 @@ type OfferAPI interface { var applicationParse = regexp.MustCompile("/?((?P[^\\.]*)\\.)?(?P[^:]*)(:(?P.*))?") func (c *offerCommand) parseEndpoints(arg string) error { + c.ModelName = applicationParse.ReplaceAllString(arg, "$model") c.Application = applicationParse.ReplaceAllString(arg, "$appname") endpoints := applicationParse.ReplaceAllString(arg, "$endpoints") if !strings.Contains(arg, ":") { return errors.New(`endpoints must conform to format ":[,...]" `) } + if c.ModelName != "" && !names.IsValidModelName(c.ModelName) { + return errors.NotValidf(`model name %q`, c.ModelName) + } if !names.IsValidApplication(c.Application) { return errors.NotValidf(`application name %q`, c.Application) } diff --git a/cmd/juju/crossmodel/offer_test.go b/cmd/juju/crossmodel/offer_test.go index b78cdb7228eb..2340323bf620 100644 --- a/cmd/juju/crossmodel/offer_test.go +++ b/cmd/juju/crossmodel/offer_test.go @@ -44,6 +44,17 @@ func (s *offerSuite) TestOfferInvalidApplication(c *gc.C) { s.assertOfferErrorOutput(c, `.*application name "123" not valid.*`) } +func (s *offerSuite) TestOfferInvalidModel(c *gc.C) { + s.args = []string{"$model.123:db"} + s.assertOfferErrorOutput(c, `.*model name "\$model" not valid.*`) +} + +func (s *offerSuite) TestOfferNoCurrentModel(c *gc.C) { + s.store.Models["test-master"].CurrentModel = "" + s.args = []string{"app:db"} + s.assertOfferErrorOutput(c, `no current model, use juju switch to select a model on which to operate`) +} + func (s *offerSuite) TestOfferInvalidEndpoints(c *gc.C) { s.args = []string{"tst/123"} s.assertOfferErrorOutput(c, `.*endpoints must conform to format.*`) diff --git a/cmd/juju/crossmodel/remoteendpoints.go b/cmd/juju/crossmodel/remoteendpoints.go index 489196b86494..944b4657a64b 100644 --- a/cmd/juju/crossmodel/remoteendpoints.go +++ b/cmd/juju/crossmodel/remoteendpoints.go @@ -4,7 +4,7 @@ package crossmodel import ( - "github.com/juju/juju/api/remoteendpoints" + "github.com/juju/juju/api/applicationoffers" "github.com/juju/juju/apiserver/params" "github.com/juju/juju/cmd/modelcmd" ) @@ -16,12 +16,12 @@ type RemoteEndpointsCommandBase struct { // NewRemoteEndpointsAPI returns a remote endpoints api for the root api endpoint // that the command returns. -func (c *RemoteEndpointsCommandBase) NewRemoteEndpointsAPI() (*remoteendpoints.Client, error) { +func (c *RemoteEndpointsCommandBase) NewRemoteEndpointsAPI() (*applicationoffers.Client, error) { root, err := c.NewAPIRoot() if err != nil { return nil, err } - return remoteendpoints.NewClient(root), nil + return applicationoffers.NewClient(root), nil } // RemoteEndpoint defines the serialization behaviour of remote endpoints.