diff --git a/apiserver/applicationoffers/applicationoffers_test.go b/apiserver/applicationoffers/applicationoffers_test.go index f3422fddb90..fe7c34e4804 100644 --- a/apiserver/applicationoffers/applicationoffers_test.go +++ b/apiserver/applicationoffers/applicationoffers_test.go @@ -707,6 +707,11 @@ func (s *applicationOffersSuite) TestFindMulti(c *gc.C) { ApplicationDescription: "postgresql description", Endpoints: map[string]charm.Relation{"db": {Name: "postgresql"}}, } + // Include an offer with bad data to ensure it is ignored. + offerAppNotFound := jujucrossmodel.ApplicationOffer{ + OfferName: "badoffer", + ApplicationName: "missing", + } s.applicationOffers.listOffers = func(filters ...jujucrossmodel.ApplicationOfferFilter) ([]jujucrossmodel.ApplicationOffer, error) { var result []jujucrossmodel.ApplicationOffer @@ -718,6 +723,8 @@ func (s *applicationOffersSuite) TestFindMulti(c *gc.C) { result = append(result, mysqlOffer) case "hosted-postgresql": result = append(result, postgresqlOffer) + default: + result = append(result, offerAppNotFound) } } return result, nil @@ -801,6 +808,11 @@ func (s *applicationOffersSuite) TestFindMulti(c *gc.C) { OwnerName: "mary", ModelName: "another", }, + { + OfferName: "badoffer", + OwnerName: "mary", + ModelName: "another", + }, }, } found, err := s.api.FindApplicationOffers(filter) diff --git a/apiserver/applicationoffers/base.go b/apiserver/applicationoffers/base.go index 75966a0d1f2..a183cfe1aa5 100644 --- a/apiserver/applicationoffers/base.go +++ b/apiserver/applicationoffers/base.go @@ -123,8 +123,11 @@ func (api *BaseAPI) applicationOffersFromModel( isAdmin = userAccess == permission.AdminAccess } offerParams, app, err := api.makeOfferParams(backend, &appOffer, userAccess) + // Just because we can't compose the result for one offer, log + // that and move on to the next one. if err != nil { - return nil, errors.Trace(err) + logger.Warningf("cannot get application offer: %v", err) + continue } offer := params.ApplicationOfferDetails{ ApplicationOffer: *offerParams, @@ -134,7 +137,8 @@ func (api *BaseAPI) applicationOffersFromModel( curl, _ := app.CharmURL() status, err := backend.RemoteConnectionStatus(offer.OfferName) if err != nil { - return nil, errors.Trace(err) + logger.Warningf("cannot get offer connection status: %v", err) + continue } offer.ApplicationName = app.Name() offer.CharmName = curl.Name diff --git a/cmd/juju/crossmodel/find.go b/cmd/juju/crossmodel/find.go index 99ae3fe11b0..c06dbc29837 100644 --- a/cmd/juju/crossmodel/find.go +++ b/cmd/juju/crossmodel/find.go @@ -126,7 +126,7 @@ func (c *findCommand) Run(ctx *cmd.Context) (err error) { return err } - output, err := convertFoundOffers(found...) + output, err := convertFoundOffers(c.source, found...) if err != nil { return err } @@ -187,17 +187,24 @@ type ApplicationOfferResult struct { // convertFoundOffers takes any number of api-formatted remote applications and // creates a collection of ui-formatted applications. -func convertFoundOffers(services ...params.ApplicationOffer) (map[string]ApplicationOfferResult, error) { - if len(services) == 0 { +func convertFoundOffers(store string, offers ...params.ApplicationOffer) (map[string]ApplicationOfferResult, error) { + if len(offers) == 0 { return nil, nil } - output := make(map[string]ApplicationOfferResult, len(services)) - for _, one := range services { + output := make(map[string]ApplicationOfferResult, len(offers)) + for _, one := range offers { app := ApplicationOfferResult{ Access: one.Access, Endpoints: convertRemoteEndpoints(one.Endpoints...), } - output[one.OfferURL] = app + url, err := crossmodel.ParseApplicationURL(one.OfferURL) + if err != nil { + return nil, err + } + if url.Source == "" { + url.Source = store + } + output[url.String()] = app } return output, nil } diff --git a/cmd/juju/crossmodel/find_test.go b/cmd/juju/crossmodel/find_test.go index 79d4bf5e34a..30dd37608ba 100644 --- a/cmd/juju/crossmodel/find_test.go +++ b/cmd/juju/crossmodel/find_test.go @@ -45,8 +45,8 @@ func (s *findSuite) TestFindNoArgs(c *gc.C) { c, []string{}, ` -URL Access Interfaces -fred/test.hosted-db2 consume http:db2, http:log +Store URL Access Interfaces +master fred/test.hosted-db2 consume http:db2, http:log `[1:], ) @@ -84,8 +84,8 @@ func (s *findSuite) TestSimpleFilter(c *gc.C) { c, []string{"--format", "tabular", "--url", "fred/model.hosted-db2"}, ` -URL Access Interfaces -fred/model.hosted-db2 consume http:db2, http:log +Store URL Access Interfaces +master fred/model.hosted-db2 consume http:db2, http:log `[1:], ) @@ -106,8 +106,8 @@ func (s *findSuite) TestEndpointFilter(c *gc.C) { c, []string{"--format", "tabular", "--url", "fred/model", "--endpoint", "db", "--interface", "mysql"}, ` -URL Access Interfaces -fred/model.hosted-db2 consume http:db2, http:log +Store URL Access Interfaces +master fred/model.hosted-db2 consume http:db2, http:log `[1:], ) @@ -124,7 +124,7 @@ func (s *findSuite) TestFindYaml(c *gc.C) { c, []string{"fred/model.hosted-db2", "--format", "yaml"}, ` -fred/model.hosted-db2: +master:fred/model.hosted-db2: access: consume endpoints: db2: @@ -143,8 +143,8 @@ func (s *findSuite) TestFindTabular(c *gc.C) { c, []string{"fred/model.hosted-db2", "--format", "tabular"}, ` -URL Access Interfaces -fred/model.hosted-db2 consume http:db2, http:log +Store URL Access Interfaces +master fred/model.hosted-db2 consume http:db2, http:log `[1:], ) @@ -155,10 +155,10 @@ func (s *findSuite) TestFindDifferentController(c *gc.C) { s.mockAPI.controllerName = "different" s.assertFind( c, - []string{"different:fred/model.hosted-db2", "--format", "tabular"}, + []string{"fred/model.hosted-db2", "--format", "tabular"}, ` -URL Access Interfaces -different:fred/model.hosted-db2 consume http:db2, http:log +Store URL Access Interfaces +different fred/model.hosted-db2 consume http:db2, http:log `[1:], ) @@ -202,10 +202,11 @@ func (s mockFindAPI) FindApplicationOffers(filters ...jujucrossmodel.Application if s.results != nil { return s.results, nil } - offerURL := fmt.Sprintf("fred/%s.%s", s.expectedModelName, s.offerName) - if s.controllerName != "" { - offerURL = s.controllerName + ":" + offerURL + store := s.controllerName + if store == "" { + store = "master" } + offerURL := fmt.Sprintf("%s:fred/%s.%s", store, s.expectedModelName, s.offerName) return []params.ApplicationOffer{{ OfferURL: offerURL, OfferName: s.offerName, diff --git a/cmd/juju/crossmodel/findformatter.go b/cmd/juju/crossmodel/findformatter.go index 210ccc20b8f..bc7f3dadfb3 100644 --- a/cmd/juju/crossmodel/findformatter.go +++ b/cmd/juju/crossmodel/findformatter.go @@ -12,6 +12,7 @@ import ( "github.com/juju/errors" "github.com/juju/juju/cmd/output" + "github.com/juju/juju/core/crossmodel" ) // formatFindTabular returns a tabular summary of remote applications or @@ -28,17 +29,22 @@ func formatFindTabular(writer io.Writer, value interface{}) error { func formatFoundEndpointsTabular(writer io.Writer, all map[string]ApplicationOfferResult) error { tw := output.TabWriter(writer) w := output.Wrapper{tw} - w.Println("URL", "Access", "Interfaces") + w.Println("Store", "URL", "Access", "Interfaces") - for url, one := range all { - applicationURL := url + for urlStr, one := range all { + url, err := crossmodel.ParseApplicationURL(urlStr) + if err != nil { + return err + } + store := url.Source + url.Source = "" interfaces := []string{} for name, ep := range one.Endpoints { interfaces = append(interfaces, fmt.Sprintf("%s:%s", ep.Interface, name)) } sort.Strings(interfaces) - w.Println(applicationURL, one.Access, strings.Join(interfaces, ", ")) + w.Println(store, url.String(), one.Access, strings.Join(interfaces, ", ")) } tw.Flush() diff --git a/cmd/juju/crossmodel/show.go b/cmd/juju/crossmodel/show.go index 575052675b3..f3ab075cec0 100644 --- a/cmd/juju/crossmodel/show.go +++ b/cmd/juju/crossmodel/show.go @@ -103,7 +103,7 @@ func (c *showCommand) Run(ctx *cmd.Context) (err error) { return err } - output, err := convertOffers(found) + output, err := convertOffers(controllerName, found) if err != nil { return err } @@ -131,7 +131,7 @@ type ShowOfferedApplication struct { // convertOffers takes any number of api-formatted remote applications and // creates a collection of ui-formatted offers. -func convertOffers(offers ...params.ApplicationOffer) (map[string]ShowOfferedApplication, error) { +func convertOffers(store string, offers ...params.ApplicationOffer) (map[string]ShowOfferedApplication, error) { if len(offers) == 0 { return nil, nil } @@ -144,7 +144,14 @@ func convertOffers(offers ...params.ApplicationOffer) (map[string]ShowOfferedApp if one.ApplicationDescription != "" { app.Description = one.ApplicationDescription } - output[one.OfferURL] = app + url, err := crossmodel.ParseApplicationURL(one.OfferURL) + if err != nil { + return nil, err + } + if url.Source == "" { + url.Source = store + } + output[url.String()] = app } return output, nil } diff --git a/cmd/juju/crossmodel/show_test.go b/cmd/juju/crossmodel/show_test.go index e818b597382..63311ea06ad 100644 --- a/cmd/juju/crossmodel/show_test.go +++ b/cmd/juju/crossmodel/show_test.go @@ -52,7 +52,7 @@ func (s *showSuite) TestShowYaml(c *gc.C) { c, []string{"fred/model.db2", "--format", "yaml"}, ` -fred/model.db2: +test-master:fred/model.db2: access: consume endpoints: db2: @@ -71,9 +71,9 @@ func (s *showSuite) TestShowTabular(c *gc.C) { c, []string{"fred/model.db2", "--format", "tabular"}, ` -URL Access Description Endpoint Interface Role -fred/model.db2 consume IBM DB2 Express Server Edition is an entry db2 http requirer - level database system log http provider +Store URL Access Description Endpoint Interface Role +test-master fred/model.db2 consume IBM DB2 Express Server Edition is an entry db2 http requirer + level database system log http provider `[1:], ) @@ -85,9 +85,9 @@ func (s *showSuite) TestShowDifferentController(c *gc.C) { c, []string{"different:fred/model.db2", "--format", "tabular"}, ` -URL Access Description Endpoint Interface Role -different:fred/model.db2 consume IBM DB2 Express Server Edition is an entry db2 http requirer - level database system log http provider +Store URL Access Description Endpoint Interface Role +different fred/model.db2 consume IBM DB2 Express Server Edition is an entry db2 http requirer + level database system log http provider `[1:], ) @@ -99,12 +99,12 @@ func (s *showSuite) TestShowTabularExactly180Desc(c *gc.C) { c, []string{"fred/model.db2", "--format", "tabular"}, ` -URL Access Description Endpoint Interface Role -fred/model.db2 consume IBM DB2 Express Server Edition is an entry db2 http requirer - level database systemIBM DB2 Express Server log http provider - Edition is an entry level database systemIBM - DB2 Express Server Edition is an entry level - dat +Store URL Access Description Endpoint Interface Role +test-master fred/model.db2 consume IBM DB2 Express Server Edition is an entry db2 http requirer + level database systemIBM DB2 Express Server log http provider + Edition is an entry level database systemIBM + DB2 Express Server Edition is an entry level + dat `[1:], ) @@ -116,12 +116,12 @@ func (s *showSuite) TestShowTabularMoreThan180Desc(c *gc.C) { c, []string{"fred/model.db2", "--format", "tabular"}, ` -URL Access Description Endpoint Interface Role -fred/model.db2 consume IBM DB2 Express Server Edition is an entry db2 http requirer - level database systemIBM DB2 Express Server log http provider - Edition is an entry level database systemIBM - DB2 Express Server Edition is an entry level - ... +Store URL Access Description Endpoint Interface Role +test-master fred/model.db2 consume IBM DB2 Express Server Edition is an entry db2 http requirer + level database systemIBM DB2 Express Server log http provider + Edition is an entry level database systemIBM + DB2 Express Server Edition is an entry level + ... `[1:], ) diff --git a/cmd/juju/crossmodel/showformatter.go b/cmd/juju/crossmodel/showformatter.go index d9f0434b40e..c803029db13 100644 --- a/cmd/juju/crossmodel/showformatter.go +++ b/cmd/juju/crossmodel/showformatter.go @@ -12,6 +12,7 @@ import ( "github.com/juju/errors" "github.com/juju/juju/cmd/output" + "github.com/juju/juju/core/crossmodel" ) const ( @@ -37,10 +38,16 @@ func formatOfferedEndpointsTabular(writer io.Writer, all map[string]ShowOfferedA tw := output.TabWriter(writer) w := output.Wrapper{tw} - w.Println("URL", "Access", "Description", "Endpoint", "Interface", "Role") + w.Println("Store", "URL", "Access", "Description", "Endpoint", "Interface", "Role") - for name, one := range all { - offerName := name + for urlStr, one := range all { + url, err := crossmodel.ParseApplicationURL(urlStr) + if err != nil { + return err + } + store := url.Source + url.Source = "" + offerURL := url.String() offerAccess := one.Access offerDesc := one.Description @@ -63,9 +70,10 @@ func formatOfferedEndpointsTabular(writer io.Writer, all map[string]ShowOfferedA for i := 0; i < maxIterations; i++ { descLine := descAt(descLines, i) name, endpoint := endpointAt(one.Endpoints, names, i) - w.Println(offerName, offerAccess, descLine, name, endpoint.Interface, endpoint.Role) + w.Println(store, offerURL, offerAccess, descLine, name, endpoint.Interface, endpoint.Role) // Only print once. - offerName = "" + store = "" + offerURL = "" offerAccess = "" } } diff --git a/cmd/juju/status/output_tabular.go b/cmd/juju/status/output_tabular.go index 058f089b929..90954a04bf0 100644 --- a/cmd/juju/status/output_tabular.go +++ b/cmd/juju/status/output_tabular.go @@ -125,7 +125,7 @@ func FormatTabular(writer io.Writer, forceColor bool, value interface{}) error { p(values...) if len(fs.RemoteApplications) > 0 { - outputHeaders("SAAS name", "Status", "Store", "URL") + outputHeaders("SAAS", "Status", "Store", "URL") for _, appName := range utils.SortStringsNaturally(stringKeysFromMap(fs.RemoteApplications)) { app := fs.RemoteApplications[appName] var store, urlPath string diff --git a/cmd/juju/status/status_test.go b/cmd/juju/status/status_test.go index c202bd0a22c..e9fe1ffe0c7 100644 --- a/cmd/juju/status/status_test.go +++ b/cmd/juju/status/status_test.go @@ -4037,7 +4037,7 @@ func (s *StatusSuite) testStatusWithFormatTabular(c *gc.C, useFeatureFlag bool) Model Controller Cloud/Region Version Notes SLA controller kontroll dummy/dummy-region 1.2.3 upgrade available: 1.2.4 unsupported -SAAS name Status Store URL +SAAS Status Store URL hosted-riak unknown local me/model.riak App Version Status Scale Charm Store Rev OS Notes diff --git a/featuretests/cmd_juju_crossmodel_test.go b/featuretests/cmd_juju_crossmodel_test.go index e5f4bfaa207..bdd704da8ff 100644 --- a/featuretests/cmd_juju_crossmodel_test.go +++ b/featuretests/cmd_juju_crossmodel_test.go @@ -86,7 +86,7 @@ func (s *crossmodelSuite) TestShow(c *gc.C) { "admin/controller.varnish", "--format", "yaml") c.Assert(err, jc.ErrorIsNil) c.Assert(ctx.Stdout.(*bytes.Buffer).String(), gc.Equals, ` -admin/controller.varnish: +kontroll:admin/controller.varnish: access: admin endpoints: webcache: @@ -103,7 +103,7 @@ func (s *crossmodelSuite) TestShowOtherModel(c *gc.C) { "otheruser/othermodel.hosted-mysql", "--format", "yaml") c.Assert(err, jc.ErrorIsNil) c.Assert(ctx.Stdout.(*bytes.Buffer).String(), gc.Equals, ` -otheruser/othermodel.hosted-mysql: +kontroll:otheruser/othermodel.hosted-mysql: access: admin endpoints: database: @@ -131,13 +131,13 @@ func (s *crossmodelSuite) TestFind(c *gc.C) { "admin/controller", "--format", "yaml") c.Assert(err, jc.ErrorIsNil) c.Assert(ctx.Stdout.(*bytes.Buffer).String(), gc.Equals, ` -admin/controller.riak: +kontroll:admin/controller.riak: access: admin endpoints: endpoint: interface: http role: provider -admin/controller.varnish: +kontroll:admin/controller.varnish: access: admin endpoints: webcache: @@ -153,7 +153,7 @@ func (s *crossmodelSuite) TestFindOtherModel(c *gc.C) { "otheruser/othermodel", "--format", "yaml") c.Assert(err, jc.ErrorIsNil) c.Assert(ctx.Stdout.(*bytes.Buffer).String(), gc.Equals, ` -otheruser/othermodel.hosted-mysql: +kontroll:otheruser/othermodel.hosted-mysql: access: admin endpoints: database: @@ -169,19 +169,19 @@ func (s *crossmodelSuite) TestFindAllModels(c *gc.C) { ctx, err := cmdtesting.RunCommand(c, crossmodel.NewFindEndpointsCommand(), "kontroll:", "--format", "yaml") c.Assert(err, jc.ErrorIsNil) c.Assert(ctx.Stdout.(*bytes.Buffer).String(), gc.Equals, ` -admin/controller.riak: +kontroll:admin/controller.riak: access: admin endpoints: endpoint: interface: http role: provider -admin/controller.varnish: +kontroll:admin/controller.varnish: access: admin endpoints: webcache: interface: varnish role: provider -otheruser/othermodel.hosted-mysql: +kontroll:otheruser/othermodel.hosted-mysql: access: admin endpoints: database: @@ -397,7 +397,7 @@ func (s *crossmodelSuite) TestFindOffersWithPermission(c *gc.C) { "otheruser/othermodel", "--format", "yaml") c.Assert(err, jc.ErrorIsNil) c.Assert(ctx.Stdout.(*bytes.Buffer).String(), gc.Equals, ` -otheruser/othermodel.hosted-mysql: +kontroll:otheruser/othermodel.hosted-mysql: access: read endpoints: database: