From aa6835a6bc78811efaa4c35c1058eacf578ac11f 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/access_test.go | 10 +- api/applicationoffers/client.go | 80 ++- api/applicationoffers/client_test.go | 330 +++++++++++- 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 - apiserver/applicationoffers/access_test.go | 149 +++--- .../applicationoffers/applicationoffers.go | 222 ++++++-- .../applicationoffers_test.go | 493 +++++++++++++++++- .../base.go} | 145 +++--- apiserver/applicationoffers/base_test.go | 11 +- apiserver/applicationoffers/mock_test.go | 28 +- .../state.go | 2 +- apiserver/params/crossmodel.go | 3 +- 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 | 22 +- cmd/juju/crossmodel/list.go | 30 +- cmd/juju/crossmodel/list_test.go | 6 + cmd/juju/crossmodel/offer.go | 104 ++-- cmd/juju/crossmodel/offer_test.go | 18 +- cmd/juju/crossmodel/package_test.go | 5 +- cmd/juju/crossmodel/remoteendpoints.go | 6 +- cmd/juju/model/grantrevoke.go | 58 +-- cmd/juju/model/grantrevoke_test.go | 4 +- 34 files changed, 1445 insertions(+), 1579 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} (68%) 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/access_test.go b/api/applicationoffers/access_test.go index 8c48e943a17..81135102372 100644 --- a/api/applicationoffers/access_test.go +++ b/api/applicationoffers/access_test.go @@ -23,7 +23,7 @@ type accessFunc func(string, string, ...string) error var _ = gc.Suite(&accessSuite{}) const ( - someOffer = "hosted-mysql" + someOffer = "user/prod.hosted-mysql" ) func accessCall(client *applicationoffers.Client, action params.OfferAction, user, access string, offerURLs ...string) error { @@ -72,7 +72,7 @@ func (s *accessSuite) readOnlyUser(c *gc.C, action params.OfferAction) { c.Assert(req.Changes, gc.HasLen, 1) c.Assert(string(req.Changes[0].Action), gc.Equals, string(action)) c.Assert(string(req.Changes[0].Access), gc.Equals, string(params.OfferReadAccess)) - c.Assert(req.Changes[0].OfferTag, gc.Equals, names.NewApplicationOfferTag(someOffer).String()) + c.Assert(req.Changes[0].OfferURL, gc.Equals, someOffer) resp := assertResponse(c, result) *resp = params.ErrorResults{Results: []params.ErrorResult{{Error: nil}}} @@ -101,7 +101,7 @@ func (s *accessSuite) adminUser(c *gc.C, action params.OfferAction) { c.Assert(req.Changes, gc.HasLen, 1) c.Assert(string(req.Changes[0].Action), gc.Equals, string(action)) c.Assert(string(req.Changes[0].Access), gc.Equals, string(params.OfferConsumeAccess)) - c.Assert(req.Changes[0].OfferTag, gc.Equals, names.NewApplicationOfferTag(someOffer).String()) + c.Assert(req.Changes[0].OfferURL, gc.Equals, someOffer) resp := assertResponse(c, result) *resp = params.ErrorResults{Results: []params.ErrorResult{{Error: nil}}} @@ -131,7 +131,7 @@ func (s *accessSuite) threeOffers(c *gc.C, action params.OfferAction) { for i := range req.Changes { c.Assert(string(req.Changes[i].Action), gc.Equals, string(action)) c.Assert(string(req.Changes[i].Access), gc.Equals, string(params.OfferReadAccess)) - c.Assert(req.Changes[i].OfferTag, gc.Equals, names.NewApplicationOfferTag(someOffer).String()) + c.Assert(req.Changes[i].OfferURL, gc.Equals, someOffer) } resp := assertResponse(c, result) @@ -161,7 +161,7 @@ func (s *accessSuite) errorResult(c *gc.C, action params.OfferAction) { c.Assert(req.Changes, gc.HasLen, 1) c.Assert(string(req.Changes[0].Action), gc.Equals, string(action)) c.Assert(req.Changes[0].UserTag, gc.Equals, names.NewUserTag("aaa").String()) - c.Assert(req.Changes[0].OfferTag, gc.Equals, names.NewApplicationOfferTag(someOffer).String()) + c.Assert(req.Changes[0].OfferURL, gc.Equals, someOffer) resp := assertResponse(c, result) err := ¶ms.Error{Message: "unfortunate mishap"} diff --git a/api/applicationoffers/client.go b/api/applicationoffers/client.go index c9a1a6cc3c5..b13c03122c1 100644 --- a/api/applicationoffers/client.go +++ b/api/applicationoffers/client.go @@ -30,7 +30,7 @@ func NewClient(st base.APICallCloser) *Client { } // Offer prepares application's endpoints for consumption. -func (c *Client) Offer(application string, endpoints []string, offerName string, desc string) ([]params.ErrorResult, error) { +func (c *Client) Offer(modelUUID, application string, endpoints []string, offerName string, desc string) ([]params.ErrorResult, error) { // TODO(wallyworld) - support endpoint aliases ep := make(map[string]string) for _, name := range endpoints { @@ -38,6 +38,7 @@ func (c *Client) Offer(application string, endpoints []string, offerName string, } offers := []params.AddApplicationOffer{ { + ModelTag: names.NewModelTag(modelUUID).String(), ApplicationName: application, ApplicationDescription: desc, Endpoints: ep, @@ -58,6 +59,7 @@ func (c *Client) ListOffers(filters ...crossmodel.ApplicationOfferFilter) ([]cro for _, f := range filters { // TODO(wallyworld) - include allowed users filterTerm := params.OfferFilter{ + ModelName: f.ModelName, OfferName: f.OfferName, ApplicationName: f.ApplicationName, } @@ -104,16 +106,16 @@ func convertListResultsToModel(items []params.ApplicationOfferDetails) []crossmo } // GrantOffer grants a user access to the specified offers. -func (c *Client) GrantOffer(user, access string, offers ...string) error { - return c.modifyOfferUser(params.GrantOfferAccess, user, access, offers) +func (c *Client) GrantOffer(user, access string, offerURLs ...string) error { + return c.modifyOfferUser(params.GrantOfferAccess, user, access, offerURLs) } // RevokeOffer revokes a user's access to the specified offers. -func (c *Client) RevokeOffer(user, access string, offers ...string) error { - return c.modifyOfferUser(params.RevokeOfferAccess, user, access, offers) +func (c *Client) RevokeOffer(user, access string, offerURLs ...string) error { + return c.modifyOfferUser(params.RevokeOfferAccess, user, access, offerURLs) } -func (c *Client) modifyOfferUser(action params.OfferAction, user, access string, offers []string) error { +func (c *Client) modifyOfferUser(action params.OfferAction, user, access string, offerURLs []string) error { var args params.ModifyOfferAccessRequest if !names.IsValidUser(user) { @@ -125,12 +127,12 @@ func (c *Client) modifyOfferUser(action params.OfferAction, user, access string, if err := permission.ValidateOfferAccess(offerAccess); err != nil { return errors.Trace(err) } - for _, offer := range offers { + for _, offerURL := range offerURLs { args.Changes = append(args.Changes, params.ModifyOfferAccess{ UserTag: userTag.String(), Action: action, Access: params.OfferAccessPermission(offerAccess), - OfferTag: names.NewApplicationOfferTag(offer).String(), + OfferURL: offerURL, }) } @@ -145,9 +147,69 @@ func (c *Client) modifyOfferUser(action params.OfferAction, user, access string, for i, r := range result.Results { if r.Error != nil && r.Error.Code == params.CodeAlreadyExists { - logger.Warningf("offer %q is already shared with %q", offers[i], userTag.Id()) + logger.Warningf("offer %q is already shared with %q", offerURLs[i], userTag.Id()) result.Results[i].Error = nil } } 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 a32c8e4a50e..f7a5eba7da8 100644 --- a/api/applicationoffers/client_test.go +++ b/api/applicationoffers/client_test.go @@ -48,10 +48,11 @@ func (s *crossmodelMockSuite) TestOffer(c *gc.C) { c.Assert(args.Offers, gc.HasLen, 1) offer := args.Offers[0] - c.Assert(offer.ApplicationName, gc.DeepEquals, application) + c.Assert(offer.ModelTag, gc.Equals, "model-uuid") + c.Assert(offer.ApplicationName, gc.Equals, application) c.Assert(offer.Endpoints, jc.DeepEquals, map[string]string{endPointA: endPointA, endPointB: endPointB}) c.Assert(offer.OfferName, gc.Equals, offer.OfferName) - c.Assert(offer.ApplicationDescription, gc.DeepEquals, desc) + c.Assert(offer.ApplicationDescription, gc.Equals, desc) if results, ok := result.(*params.ErrorResults); ok { all := make([]params.ErrorResult, len(args.Offers)) @@ -65,7 +66,7 @@ func (s *crossmodelMockSuite) TestOffer(c *gc.C) { }) client := applicationoffers.NewClient(apiCaller) - results, err := client.Offer(application, []string{endPointA, endPointB}, offer, desc) + results, err := client.Offer("uuid", application, []string{endPointA, endPointB}, offer, desc) c.Assert(err, jc.ErrorIsNil) c.Assert(results, gc.HasLen, 2) c.Assert(results, jc.DeepEquals, @@ -90,7 +91,7 @@ func (s *crossmodelMockSuite) TestOfferFacadeCallError(c *gc.C) { return errors.New(msg) }) client := applicationoffers.NewClient(apiCaller) - results, err := client.Offer("", nil, "", "") + results, err := client.Offer("", "", nil, "", "") c.Assert(errors.Cause(err), gc.ErrorMatches, msg) c.Assert(results, gc.IsNil) } @@ -209,3 +210,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 e8910a33b24..0f76aa31334 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 aed16b188a7..00000000000 --- 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 a0861936bf6..00000000000 --- 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 e06b21d06c2..00000000000 --- 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 047903338b9..e99d862ffe3 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/access_test.go b/apiserver/applicationoffers/access_test.go index e103c0c3f23..ba1083c34de 100644 --- a/apiserver/applicationoffers/access_test.go +++ b/apiserver/applicationoffers/access_test.go @@ -8,6 +8,7 @@ import ( "github.com/juju/errors" jc "github.com/juju/testing/checkers" + "github.com/juju/utils/set" gc "gopkg.in/check.v1" "gopkg.in/juju/names.v2" @@ -43,14 +44,14 @@ func (s *offerAccessSuite) modifyAccess( c *gc.C, user names.UserTag, action params.OfferAction, access params.OfferAccessPermission, - offerTag names.ApplicationOfferTag, + offerURL string, ) error { args := params.ModifyOfferAccessRequest{ Changes: []params.ModifyOfferAccess{{ UserTag: user.String(), Action: action, Access: access, - OfferTag: offerTag.String(), + OfferURL: offerURL, }}} result, err := s.api.ModifyOfferAccess(args) @@ -60,99 +61,118 @@ func (s *offerAccessSuite) modifyAccess( return result.OneError() } -func (s *offerAccessSuite) grant(c *gc.C, user names.UserTag, access params.OfferAccessPermission, offer string) error { - return s.modifyAccess(c, user, params.GrantOfferAccess, access, names.NewApplicationOfferTag(offer)) +func (s *offerAccessSuite) grant(c *gc.C, user names.UserTag, access params.OfferAccessPermission, offerURL string) error { + return s.modifyAccess(c, user, params.GrantOfferAccess, access, offerURL) } -func (s *offerAccessSuite) revoke(c *gc.C, user names.UserTag, access params.OfferAccessPermission, offer string) error { - return s.modifyAccess(c, user, params.RevokeOfferAccess, access, names.NewApplicationOfferTag(offer)) +func (s *offerAccessSuite) revoke(c *gc.C, user names.UserTag, access params.OfferAccessPermission, offerURL string) error { + return s.modifyAccess(c, user, params.RevokeOfferAccess, access, offerURL) } -func (s *offerAccessSuite) TestGrantMissingUserFails(c *gc.C) { - s.mockState.applicationOffers["someoffer"] = jujucrossmodel.ApplicationOffer{} +func (s *offerAccessSuite) setupOffer(modelUUID, modelName, owner, offerName string) { + s.mockState.allmodels = []applicationoffers.Model{ + &mockModel{uuid: modelUUID, name: modelName, owner: owner}} + st := &mockState{ + modelUUID: modelUUID, + applicationOffers: make(map[string]jujucrossmodel.ApplicationOffer), + users: set.NewStrings(), + accessPerms: make(map[offerAccess]permission.Access), + } + s.mockStatePool.st[modelUUID] = st + st.applicationOffers[offerName] = jujucrossmodel.ApplicationOffer{} +} +func (s *offerAccessSuite) TestGrantMissingUserFails(c *gc.C) { + s.setupOffer("model-uuid", "test", "admin", "someoffer") user := names.NewUserTag("foobar") - err := s.grant(c, user, params.OfferReadAccess, "someoffer") + err := s.grant(c, user, params.OfferReadAccess, "test.someoffer") expectedErr := `could not grant offer access: user "foobar" not found` c.Assert(err, gc.ErrorMatches, expectedErr) } func (s *offerAccessSuite) TestGrantMissingOfferFails(c *gc.C) { + s.setupOffer("model-uuid", "test", "admin", "differentoffer") user := names.NewUserTag("foobar") - err := s.grant(c, user, params.OfferReadAccess, "someoffer") + err := s.grant(c, user, params.OfferReadAccess, "test.someoffer") expectedErr := `.*application offer "someoffer" not found` c.Assert(err, gc.ErrorMatches, expectedErr) } func (s *offerAccessSuite) TestRevokeAdminLeavesReadAccess(c *gc.C) { - s.mockState.applicationOffers["someoffer"] = jujucrossmodel.ApplicationOffer{} - s.mockState.users.Add("foobar") + s.setupOffer("model-uuid", "test", "admin", "someoffer") + st := s.mockStatePool.st["model-uuid"] + st.(*mockState).users.Add("foobar") user := names.NewUserTag("foobar") offer := names.NewApplicationOfferTag("someoffer") - err := s.mockState.CreateOfferAccess(offer, user, permission.ConsumeAccess) + err := st.CreateOfferAccess(offer, user, permission.ConsumeAccess) c.Assert(err, jc.ErrorIsNil) - err = s.revoke(c, user, params.OfferConsumeAccess, "someoffer") + err = s.revoke(c, user, params.OfferConsumeAccess, "test.someoffer") c.Assert(err, jc.ErrorIsNil) - access, err := s.mockState.GetOfferAccess(offer, user) + access, err := st.GetOfferAccess(offer, user) c.Assert(err, jc.ErrorIsNil) c.Assert(access, gc.Equals, permission.ReadAccess) } func (s *offerAccessSuite) TestRevokeReadRemovesPermission(c *gc.C) { - s.mockState.applicationOffers["someoffer"] = jujucrossmodel.ApplicationOffer{} - s.mockState.users.Add("foobar") + s.setupOffer("model-uuid", "test", "admin", "someoffer") + st := s.mockStatePool.st["model-uuid"] + st.(*mockState).users.Add("foobar") user := names.NewUserTag("foobar") offer := names.NewApplicationOfferTag("someoffer") - s.mockState.UpdateOfferAccess(offer, user, permission.ConsumeAccess) + err := st.CreateOfferAccess(offer, user, permission.ConsumeAccess) + c.Assert(err, jc.ErrorIsNil) - err := s.revoke(c, user, params.OfferReadAccess, "someoffer") + err = s.revoke(c, user, params.OfferReadAccess, "test.someoffer") c.Assert(err, gc.IsNil) - _, err = s.mockState.GetOfferAccess(offer, user) + _, err = st.GetOfferAccess(offer, user) c.Assert(errors.IsNotFound(err), jc.IsTrue) } func (s *offerAccessSuite) TestRevokeMissingUser(c *gc.C) { - s.mockState.applicationOffers["someoffer"] = jujucrossmodel.ApplicationOffer{} + s.setupOffer("model-uuid", "test", "admin", "someoffer") + st := s.mockStatePool.st["model-uuid"] user := names.NewUserTag("bob") - err := s.revoke(c, user, params.OfferReadAccess, "someoffer") + err := s.revoke(c, user, params.OfferReadAccess, "test.someoffer") c.Assert(err, gc.ErrorMatches, `could not revoke offer access: offer user "bob" does not exist`) offer := names.NewApplicationOfferTag("someoffer") - _, err = s.mockState.GetOfferAccess(offer, user) + _, err = st.GetOfferAccess(offer, user) c.Assert(errors.IsNotFound(err), jc.IsTrue) } func (s *offerAccessSuite) TestGrantOnlyGreaterAccess(c *gc.C) { - s.mockState.applicationOffers["someoffer"] = jujucrossmodel.ApplicationOffer{} - s.mockState.users.Add("foobar") + s.setupOffer("model-uuid", "test", "admin", "someoffer") + st := s.mockStatePool.st["model-uuid"] + st.(*mockState).users.Add("foobar") user := names.NewUserTag("foobar") - err := s.grant(c, user, params.OfferReadAccess, "someoffer") + err := s.grant(c, user, params.OfferReadAccess, "test.someoffer") c.Assert(err, jc.ErrorIsNil) - err = s.grant(c, user, params.OfferReadAccess, "someoffer") + err = s.grant(c, user, params.OfferReadAccess, "test.someoffer") c.Assert(err, gc.ErrorMatches, `user already has "read" access or greater`) } func (s *offerAccessSuite) assertGrantOfferAddUser(c *gc.C, user names.UserTag) { - s.mockState.applicationOffers["someoffer"] = jujucrossmodel.ApplicationOffer{} - s.mockState.users.Add("other") - s.mockState.users.Add(user.Name()) + s.setupOffer("model-uuid", "test", "superuser-bob", "someoffer") + st := s.mockStatePool.st["model-uuid"] + st.(*mockState).users.Add("other") + st.(*mockState).users.Add(user.Name()) apiUser := names.NewUserTag("superuser-bob") s.authorizer.Tag = apiUser - err := s.grant(c, user, params.OfferReadAccess, "someoffer") + err := s.grant(c, user, params.OfferReadAccess, "superuser-bob/test.someoffer") c.Assert(err, jc.ErrorIsNil) offer := names.NewApplicationOfferTag("someoffer") - access, err := s.api.Backend.GetOfferAccess(offer, user) + access, err := st.GetOfferAccess(offer, user) c.Assert(err, jc.ErrorIsNil) c.Assert(access, gc.Equals, permission.ReadAccess) } @@ -166,65 +186,73 @@ func (s *offerAccessSuite) TestGrantOfferAddRemoteUser(c *gc.C) { } func (s *offerAccessSuite) TestGrantOfferSuperUser(c *gc.C) { - s.mockState.applicationOffers["someoffer"] = jujucrossmodel.ApplicationOffer{} - s.mockState.users.Add("other") + s.setupOffer("model-uuid", "test", "superuser-bob", "someoffer") + st := s.mockStatePool.st["model-uuid"] + st.(*mockState).users.Add("other") user := names.NewUserTag("superuser-bob") s.authorizer.Tag = user other := names.NewUserTag("other") - err := s.grant(c, other, params.OfferReadAccess, "someoffer") + err := s.grant(c, other, params.OfferReadAccess, "superuser-bob/test.someoffer") c.Assert(err, jc.ErrorIsNil) offer := names.NewApplicationOfferTag("someoffer") - access, err := s.mockState.GetOfferAccess(offer, other) + access, err := st.GetOfferAccess(offer, other) c.Assert(err, jc.ErrorIsNil) c.Assert(access, gc.Equals, permission.ReadAccess) } func (s *offerAccessSuite) TestGrantIncreaseAccess(c *gc.C) { - s.mockState.applicationOffers["someoffer"] = jujucrossmodel.ApplicationOffer{} - s.mockState.users.Add("other") + s.setupOffer("model-uuid", "test", "other", "someoffer") + st := s.mockStatePool.st["model-uuid"] + st.(*mockState).users.Add("other") user := names.NewUserTag("other") s.authorizer.Tag = user s.authorizer.AdminTag = user offer := names.NewApplicationOfferTag("someoffer") - s.mockState.UpdateOfferAccess(offer, user, permission.ReadAccess) + err := st.CreateOfferAccess(offer, user, permission.ReadAccess) + c.Assert(err, jc.ErrorIsNil) - err := s.grant(c, user, params.OfferConsumeAccess, offer.Name) + err = s.grant(c, user, params.OfferConsumeAccess, "other/test.someoffer") c.Assert(err, jc.ErrorIsNil) - access, err := s.mockState.GetOfferAccess(offer, user) + access, err := st.GetOfferAccess(offer, user) c.Assert(err, jc.ErrorIsNil) c.Assert(access, gc.Equals, permission.ConsumeAccess) } func (s *offerAccessSuite) TestGrantToOfferNoAccess(c *gc.C) { - s.mockState.applicationOffers["someoffer"] = jujucrossmodel.ApplicationOffer{} - s.mockState.users.Add("other") + s.setupOffer("model-uuid", "test", "bob@remote", "someoffer") + st := s.mockStatePool.st["model-uuid"] + st.(*mockState).users.Add("other") + st.(*mockState).users.Add("bob") user := names.NewUserTag("bob@remote") s.authorizer.Tag = user other := names.NewUserTag("other@remote") - err := s.grant(c, other, params.OfferReadAccess, "someoffer") + err := s.grant(c, other, params.OfferReadAccess, "bob@remote/test.someoffer") c.Assert(err, gc.ErrorMatches, "permission denied") } func (s *offerAccessSuite) assertGrantToOffer(c *gc.C, userAccess permission.Access) { - s.mockState.applicationOffers["someoffer"] = jujucrossmodel.ApplicationOffer{} - s.mockState.users.Add("other") + s.setupOffer("model-uuid", "test", "bob@remote", "someoffer") + st := s.mockStatePool.st["model-uuid"] + st.(*mockState).users.Add("other") + st.(*mockState).users.Add("bob") user := names.NewUserTag("bob@remote") s.authorizer.Tag = user offer := names.NewApplicationOfferTag("someoffer") - s.mockState.UpdateOfferAccess(offer, user, userAccess) + err := st.CreateOfferAccess(offer, user, userAccess) + c.Assert(err, jc.ErrorIsNil) other := names.NewUserTag("other@remote") - err := s.grant(c, other, params.OfferReadAccess, "someoffer") + err = s.grant(c, other, params.OfferReadAccess, "bob@remote/test.someoffer") c.Assert(err, gc.ErrorMatches, "permission denied") } @@ -237,25 +265,29 @@ func (s *offerAccessSuite) TestGrantToOfferConsumeAccess(c *gc.C) { } func (s *offerAccessSuite) TestGrantToOfferAdminAccess(c *gc.C) { - s.mockState.applicationOffers["someoffer"] = jujucrossmodel.ApplicationOffer{} - s.mockState.users.Add("other") + s.setupOffer("model-uuid", "test", "foobar", "someoffer") + st := s.mockStatePool.st["model-uuid"] + st.(*mockState).users.Add("other") + st.(*mockState).users.Add("foobar") user := names.NewUserTag("foobar") s.authorizer.Tag = user s.authorizer.AdminTag = user offer := names.NewApplicationOfferTag("someoffer") - s.mockState.UpdateOfferAccess(offer, user, permission.AdminAccess) + err := st.CreateOfferAccess(offer, user, permission.AdminAccess) + c.Assert(err, jc.ErrorIsNil) other := names.NewUserTag("other") - err := s.grant(c, other, params.OfferReadAccess, "someoffer") + err = s.grant(c, other, params.OfferReadAccess, "foobar/test.someoffer") c.Assert(err, jc.ErrorIsNil) - access, err := s.mockState.GetOfferAccess(offer, other) + access, err := st.GetOfferAccess(offer, other) c.Assert(err, jc.ErrorIsNil) c.Assert(access, gc.Equals, permission.ReadAccess) } func (s *offerAccessSuite) TestGrantOfferInvalidUserTag(c *gc.C) { + s.setupOffer("model-uuid", "test", "admin", "someoffer") for _, testParam := range []struct { tag string validTag bool @@ -313,7 +345,7 @@ func (s *offerAccessSuite) TestGrantOfferInvalidUserTag(c *gc.C) { UserTag: testParam.tag, Action: params.GrantOfferAccess, Access: params.OfferReadAccess, - OfferTag: names.NewApplicationOfferTag("someoffer").String(), + OfferURL: "test.someoffer", }}} result, err := s.api.ModifyOfferAccess(args) @@ -323,7 +355,9 @@ func (s *offerAccessSuite) TestGrantOfferInvalidUserTag(c *gc.C) { } func (s *offerAccessSuite) TestModifyOfferAccessEmptyArgs(c *gc.C) { - args := params.ModifyOfferAccessRequest{Changes: []params.ModifyOfferAccess{{}}} + s.setupOffer("model-uuid", "test", "admin", "someoffer") + args := params.ModifyOfferAccessRequest{ + Changes: []params.ModifyOfferAccess{{OfferURL: "test.someoffer"}}} result, err := s.api.ModifyOfferAccess(args) c.Assert(err, jc.ErrorIsNil) @@ -332,16 +366,15 @@ func (s *offerAccessSuite) TestModifyOfferAccessEmptyArgs(c *gc.C) { } func (s *offerAccessSuite) TestModifyOfferAccessInvalidAction(c *gc.C) { - s.mockState.applicationOffers["someoffer"] = jujucrossmodel.ApplicationOffer{} + s.setupOffer("model-uuid", "test", "admin", "someoffer") - offer := names.NewApplicationOfferTag("someoffer") var dance params.OfferAction = "dance" args := params.ModifyOfferAccessRequest{ Changes: []params.ModifyOfferAccess{{ UserTag: "user-user", Action: dance, Access: params.OfferReadAccess, - OfferTag: offer.String(), + OfferURL: "test.someoffer", }}} result, err := s.api.ModifyOfferAccess(args) diff --git a/apiserver/applicationoffers/applicationoffers.go b/apiserver/applicationoffers/applicationoffers.go index cd01c7ad97b..9a4c31af506 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,10 +33,10 @@ func createOffersAPI( } api := &OffersAPI{ - BaseAPI: crossmodelcommon.BaseAPI{ + BaseAPI: BaseAPI{ Authorizer: authorizer, GetApplicationOffers: getApplicationOffers, - Backend: backend, + ControllerModel: backend, StatePool: statePool, }} return api, nil @@ -46,31 +45,44 @@ 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 { - return params.ErrorResults{}, common.ServerError(err) - } - result := make([]params.ErrorResult, len(all.Offers)) for i, one := range all.Offers { - applicationOfferParams, err := api.makeAddOfferArgsFromParams(one) + modelTag, err := names.ParseModelTag(one.ModelTag) + if err != nil { + result[i].Error = common.ServerError(err) + continue + } + backend, releaser, err := api.StatePool.Get(modelTag.Id()) + if err != nil { + result[i].Error = common.ServerError(err) + continue + } + defer releaser() + + if err := api.checkAdmin(backend); err != nil { + result[i].Error = common.ServerError(err) + continue + } + + applicationOfferParams, err := api.makeAddOfferArgsFromParams(backend, one) if err != nil { result[i].Error = common.ServerError(err) continue } - _, err = api.GetApplicationOffers(api.Backend).AddOffer(applicationOfferParams) + _, err = api.GetApplicationOffers(backend).AddOffer(applicationOfferParams) result[i].Error = common.ServerError(err) } return params.ErrorResults{Results: result}, nil } -func (api *OffersAPI) makeAddOfferArgsFromParams(addOfferParams params.AddApplicationOffer) (jujucrossmodel.AddApplicationOfferArgs, error) { +func (api *OffersAPI) makeAddOfferArgsFromParams(backend Backend, addOfferParams params.AddApplicationOffer) (jujucrossmodel.AddApplicationOfferArgs, error) { result := jujucrossmodel.AddApplicationOfferArgs{ OfferName: addOfferParams.OfferName, ApplicationName: addOfferParams.ApplicationName, @@ -82,7 +94,7 @@ func (api *OffersAPI) makeAddOfferArgsFromParams(addOfferParams params.AddApplic if result.OfferName == "" { result.OfferName = result.ApplicationName } - application, err := api.Backend.Application(addOfferParams.ApplicationName) + application, err := backend.Application(addOfferParams.ApplicationName) if err != nil { return result, errors.Annotatef(err, "getting offered application %v", addOfferParams.ApplicationName) } @@ -102,7 +114,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 } @@ -119,38 +131,66 @@ func (api *OffersAPI) ModifyOfferAccess(args params.ModifyOfferAccessRequest) (r return result, nil } - canModifyController, err := api.Authorizer.HasPermission(permission.SuperuserAccess, api.Backend.ControllerTag()) + isControllerAdmin, err := api.Authorizer.HasPermission(permission.SuperuserAccess, api.ControllerModel.ControllerTag()) if err != nil { return result, errors.Trace(err) } - canModifyModel, err := api.Authorizer.HasPermission(permission.AdminAccess, api.Backend.ModelTag()) + + offerURLs := make([]string, len(args.Changes)) + for i, arg := range args.Changes { + offerURLs[i] = arg.OfferURL + } + models, err := api.getModelsFromOffers(offerURLs) if err != nil { return result, errors.Trace(err) } - isAdmin := canModifyController || canModifyModel for i, arg := range args.Changes { - result.Results[i].Error = common.ServerError(api.modifyOneOfferAccess(isAdmin, arg)) + if models[i].err != nil { + result.Results[i].Error = common.ServerError(models[i].err) + continue + } + err = api.modifyOneOfferAccess(models[i].model.UUID(), isControllerAdmin, arg) + result.Results[i].Error = common.ServerError(err) } return result, nil } -func (api *OffersAPI) modifyOneOfferAccess(isAdmin bool, arg params.ModifyOfferAccess) error { +func (api *OffersAPI) modifyOneOfferAccess(modelUUID string, isControllerAdmin bool, arg params.ModifyOfferAccess) error { + backend, releaser, err := api.StatePool.Get(modelUUID) + if err != nil { + return errors.Trace(err) + } + defer releaser() + offerAccess := permission.Access(arg.Access) if err := permission.ValidateOfferAccess(offerAccess); err != nil { return errors.Annotate(err, "could not modify offer access") } - offerTag, err := names.ParseApplicationOfferTag(arg.OfferTag) - if err != nil { - return errors.Annotate(err, "could not modify offer access") - } - canModifyOffer, err := api.Authorizer.HasPermission(permission.AdminAccess, offerTag) + url, err := jujucrossmodel.ParseApplicationURL(arg.OfferURL) if err != nil { return errors.Trace(err) } - canModify := isAdmin || canModifyOffer - if !canModify { + offerTag := names.NewApplicationOfferTag(url.ApplicationName) + + canModifyOffer := isControllerAdmin + if !canModifyOffer { + if canModifyOffer, err = api.Authorizer.HasPermission(permission.AdminAccess, backend.ModelTag()); err != nil { + return errors.Trace(err) + } + } + + if !canModifyOffer { + apiUser := api.Authorizer.GetAuthTag().(names.UserTag) + access, err := backend.GetOfferAccess(offerTag, apiUser) + if err != nil && !errors.IsNotFound(err) { + return errors.Trace(err) + } else if err == nil { + canModifyOffer = access == permission.AdminAccess + } + } + if !canModifyOffer { return common.ErrPerm } @@ -158,35 +198,36 @@ func (api *OffersAPI) modifyOneOfferAccess(isAdmin bool, arg params.ModifyOfferA if err != nil { return errors.Annotate(err, "could not modify offer access") } - return api.changeOfferAccess(offerTag, targetUserTag, arg.Action, offerAccess) + return api.changeOfferAccess(backend, offerTag, targetUserTag, arg.Action, offerAccess) } // changeOfferAccess performs the requested access grant or revoke action for the // specified user on the specified application offer. func (api *OffersAPI) changeOfferAccess( + backend Backend, offerTag names.ApplicationOfferTag, targetUserTag names.UserTag, action params.OfferAction, access permission.Access, ) error { - _, err := api.Backend.ApplicationOffer(offerTag.Name) + _, err := backend.ApplicationOffer(offerTag.Name) if err != nil { return errors.Trace(err) } switch action { case params.GrantOfferAccess: - return api.grantOfferAccess(offerTag, targetUserTag, access) + return api.grantOfferAccess(backend, offerTag, targetUserTag, access) case params.RevokeOfferAccess: - return api.revokeOfferAccess(offerTag, targetUserTag, access) + return api.revokeOfferAccess(backend, offerTag, targetUserTag, access) default: return errors.Errorf("unknown action %q", action) } } -func (api *OffersAPI) grantOfferAccess(offerTag names.ApplicationOfferTag, targetUserTag names.UserTag, access permission.Access) error { - err := api.Backend.CreateOfferAccess(offerTag, targetUserTag, access) +func (api *OffersAPI) grantOfferAccess(backend Backend, offerTag names.ApplicationOfferTag, targetUserTag names.UserTag, access permission.Access) error { + err := backend.CreateOfferAccess(offerTag, targetUserTag, access) if errors.IsAlreadyExists(err) { - offerAccess, err := api.Backend.GetOfferAccess(offerTag, targetUserTag) + offerAccess, err := backend.GetOfferAccess(offerTag, targetUserTag) if errors.IsNotFound(err) { // Conflicts with prior check, must be inconsistent state. err = txn.ErrExcessiveContention @@ -199,7 +240,7 @@ func (api *OffersAPI) grantOfferAccess(offerTag names.ApplicationOfferTag, targe if offerAccess.EqualOrGreaterOfferAccessThan(access) { return errors.Errorf("user already has %q access or greater", access) } - if err = api.Backend.UpdateOfferAccess(offerTag, targetUserTag, access); err != nil { + if err = backend.UpdateOfferAccess(offerTag, targetUserTag, access); err != nil { return errors.Annotate(err, "could not set offer access for user") } return nil @@ -207,22 +248,121 @@ func (api *OffersAPI) grantOfferAccess(offerTag names.ApplicationOfferTag, targe return errors.Annotate(err, "could not grant offer access") } -func (api *OffersAPI) revokeOfferAccess(offerTag names.ApplicationOfferTag, targetUserTag names.UserTag, access permission.Access) error { +func (api *OffersAPI) revokeOfferAccess(backend Backend, offerTag names.ApplicationOfferTag, targetUserTag names.UserTag, access permission.Access) error { switch access { case permission.ReadAccess: // Revoking read access removes all access. - err := api.Backend.RemoveOfferAccess(offerTag, targetUserTag) + err := backend.RemoveOfferAccess(offerTag, targetUserTag) return errors.Annotate(err, "could not revoke offer access") case permission.ConsumeAccess: // Revoking consume access sets read-only. - err := api.Backend.UpdateOfferAccess(offerTag, targetUserTag, permission.ReadAccess) + err := backend.UpdateOfferAccess(offerTag, targetUserTag, permission.ReadAccess) return errors.Annotate(err, "could not set offer access to read-only") case permission.AdminAccess: // Revoking admin access sets read-consume. - err := api.Backend.UpdateOfferAccess(offerTag, targetUserTag, permission.ConsumeAccess) + err := backend.UpdateOfferAccess(offerTag, targetUserTag, permission.ConsumeAccess) return errors.Annotate(err, "could not set offer access to read-consume") default: 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.ControllerModel.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 e93960bf1ae..48ff08a6621 100644 --- a/apiserver/applicationoffers/applicationoffers_test.go +++ b/apiserver/applicationoffers/applicationoffers_test.go @@ -8,15 +8,17 @@ 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" + "github.com/juju/juju/testing" ) type applicationOffersSuite struct { @@ -44,6 +46,7 @@ func (s *applicationOffersSuite) assertOffer(c *gc.C, expectedErr error) { applicationName := "test" s.addApplication(c, applicationName) one := params.AddApplicationOffer{ + ModelTag: testing.ModelTag.String(), OfferName: "offer-test", ApplicationName: applicationName, Endpoints: map[string]string{"db": "db"}, @@ -58,17 +61,17 @@ 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}, } errs, err := s.api.Offer(all) + c.Assert(err, jc.ErrorIsNil) + c.Assert(errs.Results, gc.HasLen, len(all.Offers)) if expectedErr != nil { - c.Assert(errors.Cause(err), gc.ErrorMatches, expectedErr.Error()) + c.Assert(errs.Results[0].Error, gc.ErrorMatches, expectedErr.Error()) return } - c.Assert(err, jc.ErrorIsNil) - c.Assert(errs.Results, gc.HasLen, len(all.Offers)) c.Assert(errs.Results[0].Error, gc.IsNil) s.applicationOffers.CheckCallNames(c, addOffersBackendCall) } @@ -79,6 +82,7 @@ func (s *applicationOffersSuite) TestOffer(c *gc.C) { } func (s *applicationOffersSuite) TestOfferPermission(c *gc.C) { + s.authorizer.Tag = names.NewUserTag("mary") s.assertOffer(c, common.ErrPerm) } @@ -88,21 +92,25 @@ func (s *applicationOffersSuite) TestOfferSomeFail(c *gc.C) { s.addApplication(c, "two") s.addApplication(c, "paramsfail") one := params.AddApplicationOffer{ + ModelTag: testing.ModelTag.String(), OfferName: "offer-one", ApplicationName: "one", Endpoints: map[string]string{"db": "db"}, } bad := params.AddApplicationOffer{ + ModelTag: testing.ModelTag.String(), OfferName: "offer-bad", ApplicationName: "notthere", Endpoints: map[string]string{"db": "db"}, } bad2 := params.AddApplicationOffer{ + ModelTag: testing.ModelTag.String(), OfferName: "offer-bad", ApplicationName: "paramsfail", Endpoints: map[string]string{"db": "db"}, } two := params.AddApplicationOffer{ + ModelTag: testing.ModelTag.String(), OfferName: "offer-two", ApplicationName: "two", Endpoints: map[string]string{"db": "db"}, @@ -115,7 +123,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}, @@ -136,6 +144,7 @@ func (s *applicationOffersSuite) TestOfferError(c *gc.C) { applicationName := "test" s.addApplication(c, applicationName) one := params.AddApplicationOffer{ + ModelTag: testing.ModelTag.String(), OfferName: "offer-test", ApplicationName: applicationName, Endpoints: map[string]string{"db": "db"}, @@ -148,7 +157,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}, } @@ -164,6 +173,8 @@ func (s *applicationOffersSuite) assertList(c *gc.C, expectedErr error) { filter := params.OfferFilters{ Filters: []params.OfferFilter{ { + OwnerName: "fred", + ModelName: "prod", OfferName: "hosted-db2", ApplicationName: "test", }, @@ -179,7 +190,7 @@ func (s *applicationOffersSuite) assertList(c *gc.C, expectedErr error) { []params.ApplicationOfferDetails{ { ApplicationOffer: params.ApplicationOffer{ - SourceModelTag: "model-uuid", + SourceModelTag: testing.ModelTag.String(), ApplicationDescription: "description", OfferName: "hosted-db2", OfferURL: "fred/prod.hosted-db2", @@ -204,10 +215,13 @@ func (s *applicationOffersSuite) TestListPermission(c *gc.C) { } func (s *applicationOffersSuite) TestListError(c *gc.C) { + s.setupOffers(c, "test") s.authorizer.Tag = names.NewUserTag("admin") filter := params.OfferFilters{ Filters: []params.OfferFilter{ { + OwnerName: "fred", + ModelName: "prod", OfferName: "hosted-db2", ApplicationName: "test", }, @@ -223,3 +237,466 @@ func (s *applicationOffersSuite) TestListError(c *gc.C) { c.Assert(err, gc.ErrorMatches, fmt.Sprintf(".*%v.*", msg)) s.applicationOffers.CheckCallNames(c, listOffersBackendCall) } + +func (s *applicationOffersSuite) TestListFilterRequiresModel(c *gc.C) { + s.setupOffers(c, "test") + filter := params.OfferFilters{ + Filters: []params.OfferFilter{ + { + OfferName: "hosted-db2", + ApplicationName: "test", + }, + }, + } + _, err := s.api.ListApplicationOffers(filter) + c.Assert(err, gc.ErrorMatches, "application offer filter must specify a model name") +} + +func (s *applicationOffersSuite) TestListRequiresFilter(c *gc.C) { + s.setupOffers(c, "test") + _, err := s.api.ListApplicationOffers(params.OfferFilters{}) + c.Assert(err, gc.ErrorMatches, "at least one offer filter is required") +} + +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: testing.ModelTag.Id(), 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: testing.ModelTag.String(), + 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: testing.ModelTag.String(), + 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: testing.ModelTag.Id(), 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: testing.ModelTag.Id(), 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: testing.ModelTag.Id(), 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: testing.ModelTag.Id(), 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: testing.ModelTag.String(), + 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: testing.ModelTag.String(), + 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: testing.ModelTag.String(), + 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) TestFindFiltersRequireModel(c *gc.C) { + s.setupOffers(c, "") + filter := params.OfferFilters{ + Filters: []params.OfferFilter{ + { + OfferName: "hosted-db2", + ApplicationName: "test", + }, { + OfferName: "hosted-mysql", + ApplicationName: "test", + }, + }, + } + _, err := s.api.FindApplicationOffers(filter) + c.Assert(err, gc.ErrorMatches, "application offer filter must specify a model name") +} + +func (s *applicationOffersSuite) TestFindRequiresFilter(c *gc.C) { + s.setupOffers(c, "") + _, err := s.api.FindApplicationOffers(params.OfferFilters{}) + c.Assert(err, gc.ErrorMatches, "at least one offer filter is required") +} + +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: testing.ModelTag.Id(), 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: testing.ModelTag.String(), + 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: testing.ModelTag.Id(), 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 68% rename from apiserver/common/crossmodelcommon/crossmodel.go rename to apiserver/applicationoffers/base.go index 6a2631dd5d7..08a81d5d49f 100644 --- a/apiserver/common/crossmodelcommon/crossmodel.go +++ b/apiserver/applicationoffers/base.go @@ -1,9 +1,10 @@ // Copyright 2015 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. -package crossmodelcommon +package applicationoffers import ( + "fmt" "sort" "github.com/juju/errors" @@ -16,16 +17,16 @@ import ( "github.com/juju/juju/permission" ) -// BaseAPI provides common facade functionality for cross model relations related facades. +// BaseAPI provides various boilerplate methods used by the facade business logic. type BaseAPI struct { Authorizer facade.Authorizer GetApplicationOffers func(interface{}) jujucrossmodel.ApplicationOffers - Backend Backend + ControllerModel Backend 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 +37,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,19 +55,19 @@ 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() } var model Model - models, err := api.Backend.AllModels() + models, err := api.ControllerModel.AllModels() if err != nil { return nil, false, err } for _, m := range models { - if m.Name() == modelName && m.Owner().Name() == ownerName { + if m.Name() == modelName && m.Owner().Id() == ownerName { model = m break } @@ -74,25 +75,23 @@ 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, ) ([]params.ApplicationOfferDetails, error) { - backend := api.Backend - if modelUUID != api.Backend.ModelUUID() { - st, releaser, err := api.StatePool.Get(modelUUID) - if err != nil { - return nil, errors.Trace(err) - } - backend = st - defer releaser() + // Get the relevant backend for the specified model. + backend, releaser, err := api.StatePool.Get(modelUUID) + if err != nil { + return nil, errors.Trace(err) } + defer releaser() + // 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) } @@ -162,6 +161,45 @@ func makeOfferParamsFromOffer(offer jujucrossmodel.ApplicationOffer, modelUUID s return result } +type offerModel struct { + model Model + err error +} + +// getModelsFromOffers returns a slice of models corresponding to the +// specified offer URLs. Each result item has either a model or an error. +func (api *BaseAPI) getModelsFromOffers(offerURLs []string) ([]offerModel, error) { + // Cache the models found so far so we don't look them up more than once. + modelsCache := make(map[string]Model) + oneModel := func(offerURL string) (Model, error) { + url, err := jujucrossmodel.ParseApplicationURL(offerURL) + if err != nil { + return nil, errors.Trace(err) + } + modelPath := fmt.Sprintf("%s/%s", url.User, url.ModelName) + if model, ok := modelsCache[modelPath]; ok { + return model, nil + } + + model, ok, err := api.modelForName(url.ModelName, url.User) + if err != nil { + return nil, errors.Trace(err) + } + if !ok { + return nil, errors.NotFoundf("model %q", modelPath) + } + return model, nil + } + + result := make([]offerModel, len(offerURLs)) + for i, offerURL := range offerURLs { + var om offerModel + om.model, om.err = oneModel(offerURL) + result[i] = om + } + return result, nil +} + // getModelFilters splits the specified filters per model and returns // the model and filter details for each. func (api *BaseAPI) getModelFilters(filters params.OfferFilters) ( @@ -176,34 +214,27 @@ func (api *BaseAPI) getModelFilters(filters params.OfferFilters) ( // for that model. modelUUIDs := make(map[string]string) for _, f := range filters.Filters { - // Default model is the current model. - modelUUID := api.Backend.ModelUUID() - model, ok := models[modelUUID] - if !ok { + if f.ModelName == "" { + return nil, nil, errors.New("application offer filter must specify a model name") + } + var ( + modelUUID string + ok bool + ) + if modelUUID, ok = modelUUIDs[f.ModelName]; !ok { var err error - model, err = api.Backend.Model() + model, ok, err := api.modelForName(f.ModelName, f.OwnerName) if err != nil { return nil, nil, errors.Trace(err) } - models[modelUUID] = model - } - // If the filter contains a model name, look up the details. - if f.ModelName != "" { - if modelUUID, ok = modelUUIDs[f.ModelName]; !ok { - var err error - model, ok, err := api.ModelForName(f.ModelName, f.OwnerName) - if err != nil { - return nil, nil, errors.Trace(err) - } - if !ok { - err := errors.NotFoundf("model %q", f.ModelName) - return nil, nil, errors.Trace(err) - } - // Record the UUID and model for next time. - modelUUID = model.UUID() - modelUUIDs[f.ModelName] = modelUUID - models[modelUUID] = model + if !ok { + err := errors.NotFoundf("model %q", f.ModelName) + return nil, nil, errors.Trace(err) } + // Record the UUID and model for next time. + modelUUID = model.UUID() + modelUUIDs[f.ModelName] = modelUUID + models[modelUUID] = model } // Record the filter and model details against the model UUID. @@ -214,27 +245,25 @@ 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) { + + // If there are no filters specified, that's an error since the + // caller is expected to specify at the least one or more models + // to avoid an unbounded query across all models. + if len(filters.Filters) == 0 { + return nil, common.ServerError(errors.New("at least one offer filter is required")) + } + // Gather all the filter details for doing a query for each model. models, filtersPerModel, err := api.getModelFilters(filters) if err != nil { return nil, common.ServerError(errors.Trace(err)) } - if len(filtersPerModel) == 0 { - thisModelUUID := api.Backend.ModelUUID() - filtersPerModel[thisModelUUID] = []jujucrossmodel.ApplicationOfferFilter{} - model, err := api.Backend.Model() - if err != nil { - return nil, common.ServerError(errors.Trace(err)) - } - models[thisModelUUID] = model - } - // Ensure the result is deterministic. var allUUIDs []string for modelUUID := range filtersPerModel { @@ -246,7 +275,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 64929f60e64..787f6779181 100644 --- a/apiserver/applicationoffers/base_test.go +++ b/apiserver/applicationoffers/base_test.go @@ -10,11 +10,12 @@ 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" + coretesting "github.com/juju/juju/testing" ) const ( @@ -39,12 +40,12 @@ func (s *baseSuite) SetUpTest(c *gc.C) { } s.mockState = &mockState{ - modelUUID: "uuid", + modelUUID: coretesting.ModelTag.Id(), users: set.NewStrings(), 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,9 +77,9 @@ 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"} + s.mockState.model = &mockModel{uuid: coretesting.ModelTag.Id(), name: "prod", owner: "fred"} s.mockState.connStatus = &mockConnectionStatus{count: 5} } diff --git a/apiserver/applicationoffers/mock_test.go b/apiserver/applicationoffers/mock_test.go index a51449b8c41..79ea7bb5f25 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 5d46fd7c051..293acc0b6b0 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/params/crossmodel.go b/apiserver/params/crossmodel.go index fe8f63f5828..b60063c23a5 100644 --- a/apiserver/params/crossmodel.go +++ b/apiserver/params/crossmodel.go @@ -69,6 +69,7 @@ type AddApplicationOffers struct { // AddApplicationOffer values are used to create an application offer. type AddApplicationOffer struct { + ModelTag string `json:"model-tag"` OfferName string `json:"offer-name"` ApplicationName string `json:"application-name"` ApplicationDescription string `json:"application-description"` @@ -376,7 +377,7 @@ type ModifyOfferAccess struct { UserTag string `json:"user-tag"` Action OfferAction `json:"action"` Access OfferAccessPermission `json:"access"` - OfferTag string `json:"offer-tag"` + OfferURL string `json:"offer-url"` } // OfferAction is an action that can be performed on an offer. diff --git a/apiserver/remoteendpoints/base_test.go b/apiserver/remoteendpoints/base_test.go deleted file mode 100644 index 499c0ff2565..00000000000 --- 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 b3bc6e8cfff..00000000000 --- 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 6a009df8826..00000000000 --- 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 e06b21d06c2..00000000000 --- 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 c190ef63556..00000000000 --- 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 24d0d6b5774..00000000000 --- 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 f75160e3e24..dce16a2e86d 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 36a31526bbe..fc39907012d 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 a7105a20d12..1836155f411 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 233ee560f5f..0b51151b292 100644 --- a/cmd/juju/crossmodel/export_test.go +++ b/cmd/juju/crossmodel/export_test.go @@ -18,12 +18,22 @@ var ( BreakOneWord = breakOneWord ) -func NewOfferCommandForTest(store jujuclient.ClientStore, api OfferAPI) cmd.Command { - aCmd := &offerCommand{newAPIFunc: func() (OfferAPI, error) { - return api, nil - }} +func noOpRefresh(jujuclient.ClientStore, string) error { + return nil +} + +func NewOfferCommandForTest( + store jujuclient.ClientStore, + api OfferAPI, +) cmd.Command { + aCmd := &offerCommand{ + newAPIFunc: func() (OfferAPI, error) { + return api, nil + }, + refreshModels: noOpRefresh, + } aCmd.SetClientStore(store) - return modelcmd.Wrap(aCmd) + return modelcmd.WrapController(aCmd) } func NewShowEndpointsCommandForTest(store jujuclient.ClientStore, api ShowAPI) cmd.Command { @@ -39,7 +49,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 ac4e93993cf..628ba202dac 100644 --- a/cmd/juju/crossmodel/list.go +++ b/cmd/juju/crossmodel/list.go @@ -13,6 +13,7 @@ import ( "github.com/juju/juju/cmd/modelcmd" "github.com/juju/juju/core/crossmodel" + "github.com/juju/juju/jujuclient" ) const listCommandDoc = ` @@ -63,7 +64,7 @@ func NewListEndpointsCommand() cmd.Command { listCmd.newAPIFunc = func() (ListAPI, error) { return listCmd.NewApplicationOffersAPI() } - return modelcmd.Wrap(listCmd) + return modelcmd.WrapController(listCmd) } // Init implements Command.Init. @@ -103,6 +104,29 @@ func (c *listCommand) Run(ctx *cmd.Context) (err error) { defer api.Close() // TODO (anastasiamac 2015-11-17) add input filters + // For now, just filter on the current model. + controllerName, err := c.ControllerName() + if err != nil { + return errors.Trace(err) + } + modelName, err := c.ClientStore().CurrentModel(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") + } + } + + unqualifiedModelName, ownerTag, err := jujuclient.SplitModelName(modelName) + if err != nil { + return errors.Trace(err) + } + c.filters = []crossmodel.ApplicationOfferFilter{{ + OwnerName: ownerTag.Name(), + ModelName: unqualifiedModelName, + }} + offeredApplications, err := api.ListOffers(c.filters...) if err != nil { return err @@ -124,10 +148,6 @@ func (c *listCommand) Run(ctx *cmd.Context) (err error) { } // For now, all offers come from the one controller. - controllerName, err := c.ControllerName() - if err != nil { - return errors.Trace(err) - } data, err := formatApplicationOfferDetails(controllerName, valid) if err != nil { return errors.Annotate(err, "failed to format found applications") diff --git a/cmd/juju/crossmodel/list_test.go b/cmd/juju/crossmodel/list_test.go index f6937dd3954..88e45f83339 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 9ce6a50cedf..03f7fe91ae7 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,12 +43,15 @@ func NewOfferCommand() cmd.Command { offerCmd.newAPIFunc = func() (OfferAPI, error) { return offerCmd.NewApplicationOffersAPI() } - return modelcmd.Wrap(offerCmd) + offerCmd.refreshModels = offerCmd.ControllerCommandBase.RefreshModels + return modelcmd.WrapController(offerCmd) } type offerCommand struct { ApplicationOffersCommandBase - newAPIFunc func() (OfferAPI, error) + newAPIFunc func() (OfferAPI, error) + refreshModels func(jujuclient.ClientStore, string) error + endpointsSpec string // Application stores application name to be offered. Application string @@ -55,8 +59,11 @@ type offerCommand struct { // Endpoints stores a list of endpoints that are being offered. Endpoints []string - // OfferName stores the name of the offer + // OfferName stores the name of the offer. OfferName string + + // QualifiedModelName stores the name of the model hosting the offer. + QualifiedModelName string } // Info implements Command.Info. @@ -64,7 +71,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, } } @@ -74,9 +81,7 @@ func (c *offerCommand) Init(args []string) error { if len(args) < 1 { return errors.New("an offer must at least specify application endpoint") } - if err := c.parseEndpoints(args[0]); err != nil { - return err - } + c.endpointsSpec = args[0] argCount := 1 if len(args) > 1 { argCount = 2 @@ -95,41 +100,58 @@ func (c *offerCommand) SetFlags(f *gnuflag.FlagSet) { // Run implements Command.Run. func (c *offerCommand) Run(ctx *cmd.Context) error { + controllerName, err := c.ControllerName() + if err != nil { + return errors.Trace(err) + } + if err := c.parseEndpoints(controllerName, c.endpointsSpec); err != nil { + return err + } + + if c.QualifiedModelName == "" { + c.QualifiedModelName, err = c.ClientStore().CurrentModel(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") + } + } + } + api, err := c.newAPIFunc() if err != nil { return errors.Trace(err) } defer api.Close() + store := c.ClientStore() + modelDetails, err := store.ModelByName(controllerName, c.QualifiedModelName) + if errors.IsNotFound(err) { + if err := c.refreshModels(store, controllerName); err != nil { + return errors.Annotate(err, "refreshing models cache") + } + // Now try again. + modelDetails, err = store.ModelByName(controllerName, c.QualifiedModelName) + } + if err != nil { + return errors.Annotate(err, "getting model details") + } + // TODO (anastasiamac 2015-11-16) Add a sensible way for user to specify long-ish (at times) description when offering - results, err := api.Offer(c.Application, c.Endpoints, c.OfferName, "") + results, err := api.Offer(modelDetails.ModelUUID, c.Application, c.Endpoints, c.OfferName, "") if err != nil { - return errors.Trace(err) + return err } if err := (params.ErrorResults{results}).Combine(); err != nil { - return errors.Trace(err) + return err } - modelName, err := c.ModelName() + + unqualifiedModelName, ownerTag, err := jujuclient.SplitModelName(c.QualifiedModelName) if err != nil { return errors.Trace(err) } - var unqualifiedModelName, owner string - if jujuclient.IsQualifiedModelName(modelName) { - var ownerTag names.UserTag - unqualifiedModelName, ownerTag, err = jujuclient.SplitModelName(modelName) - if err != nil { - return errors.Trace(err) - } - owner = ownerTag.Name() - } else { - unqualifiedModelName = modelName - account, err := c.CurrentAccountDetails() - if err != nil { - return errors.Trace(err) - } - owner = account.User - } - url := jujucrossmodel.MakeURL(owner, unqualifiedModelName, c.OfferName, "") + url := jujucrossmodel.MakeURL(ownerTag.Name(), unqualifiedModelName, c.OfferName, "") ep := strings.Join(c.Endpoints, ", ") ctx.Infof("Application %q endpoints [%s] available at %q", c.Application, ep, url) return nil @@ -138,20 +160,42 @@ func (c *offerCommand) Run(ctx *cmd.Context) error { // OfferAPI defines the API methods that the offer command uses. type OfferAPI interface { Close() error - Offer(application string, endpoints []string, offerName string, desc string) ([]params.ErrorResult, error) + Offer(modelUUID, application string, endpoints []string, offerName string, desc string) ([]params.ErrorResult, error) } // applicationParse is used to split an application string // into model, application and endpoint names. var applicationParse = regexp.MustCompile("/?((?P[^\\.]*)\\.)?(?P[^:]*)(:(?P.*))?") -func (c *offerCommand) parseEndpoints(arg string) error { +func (c *offerCommand) parseEndpoints(controllerName, arg string) error { + modelNameArg := 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 ":[,...]" `) } + var ( + modelName string + err error + ) + if modelNameArg != "" && !jujuclient.IsQualifiedModelName(modelNameArg) { + modelName = modelNameArg + account, err := c.ClientStore().AccountDetails(controllerName) + if err != nil { + return errors.Trace(err) + } + c.QualifiedModelName = jujuclient.JoinOwnerModelName(names.NewUserTag(account.User), modelName) + } else if modelNameArg != "" { + c.QualifiedModelName = modelNameArg + modelName, _, err = jujuclient.SplitModelName(modelNameArg) + if err != nil { + return errors.Trace(err) + } + } + if modelName != "" && !names.IsValidModelName(modelName) { + return errors.NotValidf(`model name %q`, 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 b78cdb7228e..148b76bcfdd 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.*`) @@ -78,6 +89,7 @@ func (s *offerSuite) TestOfferDataErred(c *gc.C) { func (s *offerSuite) TestOfferValid(c *gc.C) { s.args = []string{"tst:db"} s.assertOfferOutput(c, "test", "tst", "tst", []string{"db"}) + c.Assert(s.mockAPI.modelUUID, gc.Equals, "fred-uuid") } func (s *offerSuite) TestOfferWithAlias(c *gc.C) { @@ -86,7 +98,7 @@ func (s *offerSuite) TestOfferWithAlias(c *gc.C) { } func (s *offerSuite) TestOfferExplicitModel(c *gc.C) { - s.args = []string{"prod.tst:db"} + s.args = []string{"bob/prod.tst:db"} s.assertOfferOutput(c, "prod", "tst", "tst", []string{"db"}) } @@ -103,6 +115,7 @@ func (s *offerSuite) assertOfferOutput(c *gc.C, expectedModel, expectedOffer, ex type mockOfferAPI struct { errCall, errData bool + modelUUID string offers map[string][]string applications map[string]string descs map[string]string @@ -120,7 +133,7 @@ func (s *mockOfferAPI) Close() error { return nil } -func (s *mockOfferAPI) Offer(application string, endpoints []string, offerName, desc string) ([]params.ErrorResult, error) { +func (s *mockOfferAPI) Offer(modelUUID, application string, endpoints []string, offerName, desc string) ([]params.ErrorResult, error) { if s.errCall { return nil, errors.New("aborted") } @@ -129,6 +142,7 @@ func (s *mockOfferAPI) Offer(application string, endpoints []string, offerName, result[0].Error = common.ServerError(errors.New("failed")) return result, nil } + s.modelUUID = modelUUID if offerName == "" { offerName = application } diff --git a/cmd/juju/crossmodel/package_test.go b/cmd/juju/crossmodel/package_test.go index edfb76a5dbc..d97ac1ca01f 100644 --- a/cmd/juju/crossmodel/package_test.go +++ b/cmd/juju/crossmodel/package_test.go @@ -32,8 +32,9 @@ func (s *BaseCrossModelSuite) SetUpTest(c *gc.C) { s.store.Models[controllerName] = &jujuclient.ControllerModels{ CurrentModel: "fred/test", Models: map[string]jujuclient.ModelDetails{ - "bob/test": {"test-uuid"}, - "bob/prod": {"prod-uuid"}, + "bob/test": {"test-uuid"}, + "bob/prod": {"prod-uuid"}, + "fred/test": {"fred-uuid"}, }, } s.store.Accounts[controllerName] = jujuclient.AccountDetails{ diff --git a/cmd/juju/crossmodel/remoteendpoints.go b/cmd/juju/crossmodel/remoteendpoints.go index 489196b8649..944b4657a64 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. diff --git a/cmd/juju/model/grantrevoke.go b/cmd/juju/model/grantrevoke.go index dce4ecb22e9..55cf0c047da 100644 --- a/cmd/juju/model/grantrevoke.go +++ b/cmd/juju/model/grantrevoke.go @@ -237,11 +237,11 @@ func (c *grantCommand) getControllerAPI() (GrantControllerAPI, error) { return c.NewControllerAPIClient() } -func (c *grantCommand) getOfferAPI(modelName string) (GrantOfferAPI, error) { +func (c *grantCommand) getOfferAPI() (GrantOfferAPI, error) { if c.offersApi != nil { return c.offersApi, nil } - root, err := c.NewModelAPIRoot(modelName) + root, err := c.NewAPIRoot() if err != nil { return nil, errors.Trace(err) } @@ -263,7 +263,7 @@ type GrantControllerAPI interface { // GrantOfferAPI defines the API functions used by the grant command. type GrantOfferAPI interface { Close() error - GrantOffer(user, access string, offers ...string) error + GrantOffer(user, access string, offerURLs ...string) error } // Run implements cmd.Command. @@ -305,21 +305,18 @@ func (c *grantCommand) runForModel() error { } func (c *grantCommand) runForOffers() error { - // For each model, process the grants. - offersForModel := offersForModel(c.OfferURLs) - for model, urls := range offersForModel { - client, err := c.getOfferAPI(model) - if err != nil { - return err - } - defer client.Close() + client, err := c.getOfferAPI() + if err != nil { + return err + } + defer client.Close() - err = client.GrantOffer(c.User, c.Access, urls...) - if err != nil { - return block.ProcessBlockedError(err, block.BlockChange) - } + urls := make([]string, len(c.OfferURLs)) + for i, url := range c.OfferURLs { + urls[i] = url.String() } - return nil + err = client.GrantOffer(c.User, c.Access, urls...) + return block.ProcessBlockedError(err, block.BlockChange) } // NewRevokeCommand returns a new revoke command. @@ -366,11 +363,11 @@ func (c *revokeCommand) getControllerAPI() (RevokeControllerAPI, error) { return c.NewControllerAPIClient() } -func (c *revokeCommand) getOfferAPI(modelName string) (RevokeOfferAPI, error) { +func (c *revokeCommand) getOfferAPI() (RevokeOfferAPI, error) { if c.offersApi != nil { return c.offersApi, nil } - root, err := c.NewModelAPIRoot(modelName) + root, err := c.NewAPIRoot() if err != nil { return nil, errors.Trace(err) } @@ -392,7 +389,7 @@ type RevokeControllerAPI interface { // RevokeOfferAPI defines the API functions used by the revoke command. type RevokeOfferAPI interface { Close() error - RevokeOffer(user, access string, offers ...string) error + RevokeOffer(user, access string, offerURLs ...string) error } // Run implements cmd.Command. @@ -470,19 +467,16 @@ func offersForModel(offerURLs []*crossmodel.ApplicationURL) map[string][]string } func (c *revokeCommand) runForOffers() error { - // For each model, process the grant. - offersForModel := offersForModel(c.OfferURLs) - for model, urls := range offersForModel { - client, err := c.getOfferAPI(model) - if err != nil { - return err - } - defer client.Close() + client, err := c.getOfferAPI() + if err != nil { + return err + } + defer client.Close() - err = client.RevokeOffer(c.User, c.Access, urls...) - if err != nil { - return block.ProcessBlockedError(err, block.BlockChange) - } + urls := make([]string, len(c.OfferURLs)) + for i, url := range c.OfferURLs { + urls[i] = url.String() } - return nil + err = client.RevokeOffer(c.User, c.Access, urls...) + return block.ProcessBlockedError(err, block.BlockChange) } diff --git a/cmd/juju/model/grantrevoke_test.go b/cmd/juju/model/grantrevoke_test.go index 0c9e661edf4..e26f0029b3f 100644 --- a/cmd/juju/model/grantrevoke_test.go +++ b/cmd/juju/model/grantrevoke_test.go @@ -84,7 +84,7 @@ func (s *grantRevokeSuite) TestPassesOfferValues(c *gc.C) { _, err := s.run(c, "sam", "read", offers[0], offers[1], offers[2]) c.Assert(err, jc.ErrorIsNil) c.Assert(s.fakeOffersAPI.user, jc.DeepEquals, "sam") - c.Assert(s.fakeOffersAPI.offerURLs, jc.SameContents, []string{"hosted-mysql", "mysql", "hosted-db2"}) + c.Assert(s.fakeOffersAPI.offerURLs, jc.SameContents, []string{"bob/foo.hosted-mysql", "bob/bar.mysql", "bob/baz.hosted-db2"}) c.Assert(s.fakeOffersAPI.access, gc.Equals, "read") } @@ -93,7 +93,7 @@ func (s *grantRevokeSuite) TestPassesOfferWithDefaultModelUser(c *gc.C) { _, err := s.run(c, "sam", "read", offer) c.Assert(err, jc.ErrorIsNil) c.Assert(s.fakeOffersAPI.user, jc.DeepEquals, "sam") - c.Assert(s.fakeOffersAPI.offerURLs, jc.SameContents, []string{"hosted-mysql"}) + c.Assert(s.fakeOffersAPI.offerURLs, jc.SameContents, []string{"bob/foo.hosted-mysql"}) c.Assert(s.fakeOffersAPI.access, gc.Equals, "read") }