github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/apiserver/facades/client/applicationoffers/applicationoffers_test.go (about) 1 // Copyright 2015 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package applicationoffers_test 5 6 import ( 7 "fmt" 8 "strings" 9 10 "github.com/juju/errors" 11 jc "github.com/juju/testing/checkers" 12 gc "gopkg.in/check.v1" 13 "gopkg.in/juju/charm.v6" 14 "gopkg.in/juju/names.v2" 15 "gopkg.in/macaroon-bakery.v2-unstable/bakery/checkers" 16 17 "github.com/juju/juju/apiserver/common" 18 "github.com/juju/juju/apiserver/common/crossmodel" 19 "github.com/juju/juju/apiserver/facades/client/applicationoffers" 20 "github.com/juju/juju/apiserver/params" 21 jujucrossmodel "github.com/juju/juju/core/crossmodel" 22 "github.com/juju/juju/environs" 23 "github.com/juju/juju/environs/context" 24 "github.com/juju/juju/network" 25 "github.com/juju/juju/permission" 26 "github.com/juju/juju/state" 27 "github.com/juju/juju/testing" 28 ) 29 30 type applicationOffersSuite struct { 31 baseSuite 32 api *applicationoffers.OffersAPIV2 33 } 34 35 var _ = gc.Suite(&applicationOffersSuite{}) 36 37 func (s *applicationOffersSuite) SetUpTest(c *gc.C) { 38 s.baseSuite.SetUpTest(c) 39 s.applicationOffers = &stubApplicationOffers{} 40 getApplicationOffers := func(interface{}) jujucrossmodel.ApplicationOffers { 41 return s.applicationOffers 42 } 43 44 resources := common.NewResources() 45 resources.RegisterNamed("dataDir", common.StringResource(c.MkDir())) 46 47 getEnviron := func(modelUUID string) (environs.Environ, error) { 48 return s.env, nil 49 } 50 var err error 51 s.bakery = &mockBakeryService{caveats: make(map[string][]checkers.Caveat)} 52 s.authContext, err = crossmodel.NewAuthContext(&mockCommonStatePool{s.mockStatePool}, s.bakery, s.bakery) 53 c.Assert(err, jc.ErrorIsNil) 54 apiV1, err := applicationoffers.CreateOffersAPI( 55 getApplicationOffers, getEnviron, getFakeControllerInfo, 56 s.mockState, s.mockStatePool, s.authorizer, resources, s.authContext, 57 context.NewCloudCallContext(), 58 ) 59 c.Assert(err, jc.ErrorIsNil) 60 s.api = &applicationoffers.OffersAPIV2{OffersAPI: apiV1} 61 } 62 63 func (s *applicationOffersSuite) assertOffer(c *gc.C, expectedErr error) { 64 applicationName := "test" 65 s.addApplication(c, applicationName) 66 one := params.AddApplicationOffer{ 67 ModelTag: testing.ModelTag.String(), 68 OfferName: "offer-test", 69 ApplicationName: applicationName, 70 Endpoints: map[string]string{"db": "db"}, 71 } 72 all := params.AddApplicationOffers{Offers: []params.AddApplicationOffer{one}} 73 s.applicationOffers.addOffer = func(offer jujucrossmodel.AddApplicationOfferArgs) (*jujucrossmodel.ApplicationOffer, error) { 74 c.Assert(offer.OfferName, gc.Equals, one.OfferName) 75 c.Assert(offer.ApplicationName, gc.Equals, one.ApplicationName) 76 c.Assert(offer.ApplicationDescription, gc.Equals, "A pretty popular blog engine") 77 c.Assert(offer.Owner, gc.Equals, "admin") 78 c.Assert(offer.HasRead, gc.DeepEquals, []string{"everyone@external"}) 79 return &jujucrossmodel.ApplicationOffer{}, nil 80 } 81 ch := &mockCharm{meta: &charm.Meta{Description: "A pretty popular blog engine"}} 82 s.mockState.applications = map[string]crossmodel.Application{ 83 applicationName: &mockApplication{charm: ch, bindings: map[string]string{"db": "myspace"}}, 84 } 85 s.mockState.spaces["myspace"] = &mockSpace{ 86 name: "myspace", 87 providerId: "juju-space-myspace", 88 subnets: []applicationoffers.Subnet{ 89 &mockSubnet{cidr: "4.3.2.0/24", providerId: "juju-subnet-1", zones: []string{"az1"}}, 90 }, 91 } 92 s.env.spaceInfo = &environs.ProviderSpaceInfo{ 93 SpaceInfo: network.SpaceInfo{ 94 Name: "myspace", 95 ProviderId: "juju-space-myspace", 96 Subnets: []network.SubnetInfo{{ 97 CIDR: "4.3.2.0/24", 98 ProviderId: "juju-subnet-1", 99 AvailabilityZones: []string{"az1"}, 100 }}, 101 }, 102 } 103 104 errs, err := s.api.Offer(all) 105 c.Assert(err, jc.ErrorIsNil) 106 c.Assert(errs.Results, gc.HasLen, len(all.Offers)) 107 if expectedErr != nil { 108 c.Assert(errs.Results[0].Error, gc.ErrorMatches, expectedErr.Error()) 109 return 110 } 111 c.Assert(errs.Results[0].Error, gc.IsNil) 112 s.applicationOffers.CheckCallNames(c, addOffersBackendCall) 113 } 114 115 func (s *applicationOffersSuite) TestOffer(c *gc.C) { 116 s.authorizer.Tag = names.NewUserTag("admin") 117 s.assertOffer(c, nil) 118 } 119 120 func (s *applicationOffersSuite) TestOfferPermission(c *gc.C) { 121 s.authorizer.Tag = names.NewUserTag("mary") 122 s.assertOffer(c, common.ErrPerm) 123 } 124 125 func (s *applicationOffersSuite) TestOfferSomeFail(c *gc.C) { 126 s.authorizer.Tag = names.NewUserTag("admin") 127 s.addApplication(c, "one") 128 s.addApplication(c, "two") 129 s.addApplication(c, "paramsfail") 130 one := params.AddApplicationOffer{ 131 ModelTag: testing.ModelTag.String(), 132 OfferName: "offer-one", 133 ApplicationName: "one", 134 Endpoints: map[string]string{"db": "db"}, 135 } 136 bad := params.AddApplicationOffer{ 137 ModelTag: testing.ModelTag.String(), 138 OfferName: "offer-bad", 139 ApplicationName: "notthere", 140 Endpoints: map[string]string{"db": "db"}, 141 } 142 bad2 := params.AddApplicationOffer{ 143 ModelTag: testing.ModelTag.String(), 144 OfferName: "offer-bad", 145 ApplicationName: "paramsfail", 146 Endpoints: map[string]string{"db": "db"}, 147 } 148 two := params.AddApplicationOffer{ 149 ModelTag: testing.ModelTag.String(), 150 OfferName: "offer-two", 151 ApplicationName: "two", 152 Endpoints: map[string]string{"db": "db"}, 153 } 154 all := params.AddApplicationOffers{Offers: []params.AddApplicationOffer{one, bad, bad2, two}} 155 s.applicationOffers.addOffer = func(offer jujucrossmodel.AddApplicationOfferArgs) (*jujucrossmodel.ApplicationOffer, error) { 156 if offer.ApplicationName == "paramsfail" { 157 return nil, errors.New("params fail") 158 } 159 return &jujucrossmodel.ApplicationOffer{}, nil 160 } 161 ch := &mockCharm{meta: &charm.Meta{Description: "A pretty popular blog engine"}} 162 s.mockState.applications = map[string]crossmodel.Application{ 163 "one": &mockApplication{charm: ch, bindings: map[string]string{"db": "myspace"}}, 164 "two": &mockApplication{charm: ch, bindings: map[string]string{"db": "myspace"}}, 165 "paramsfail": &mockApplication{charm: ch, bindings: map[string]string{"db": "myspace"}}, 166 } 167 168 errs, err := s.api.Offer(all) 169 c.Assert(err, jc.ErrorIsNil) 170 c.Assert(errs.Results, gc.HasLen, len(all.Offers)) 171 c.Assert(errs.Results[0].Error, gc.IsNil) 172 c.Assert(errs.Results[3].Error, gc.IsNil) 173 c.Assert(errs.Results[1].Error, gc.ErrorMatches, `getting offered application notthere: application "notthere" not found`) 174 c.Assert(errs.Results[2].Error, gc.ErrorMatches, `params fail`) 175 s.applicationOffers.CheckCallNames(c, addOffersBackendCall, addOffersBackendCall, addOffersBackendCall) 176 } 177 178 func (s *applicationOffersSuite) TestOfferError(c *gc.C) { 179 s.authorizer.Tag = names.NewUserTag("admin") 180 applicationName := "test" 181 s.addApplication(c, applicationName) 182 one := params.AddApplicationOffer{ 183 ModelTag: testing.ModelTag.String(), 184 OfferName: "offer-test", 185 ApplicationName: applicationName, 186 Endpoints: map[string]string{"db": "db"}, 187 } 188 all := params.AddApplicationOffers{Offers: []params.AddApplicationOffer{one}} 189 190 msg := "fail" 191 192 s.applicationOffers.addOffer = func(offer jujucrossmodel.AddApplicationOfferArgs) (*jujucrossmodel.ApplicationOffer, error) { 193 return nil, errors.New(msg) 194 } 195 ch := &mockCharm{meta: &charm.Meta{Description: "A pretty popular blog engine"}} 196 s.mockState.applications = map[string]crossmodel.Application{ 197 applicationName: &mockApplication{charm: ch, bindings: map[string]string{"db": "myspace"}}, 198 } 199 200 errs, err := s.api.Offer(all) 201 c.Assert(err, jc.ErrorIsNil) 202 c.Assert(errs.Results, gc.HasLen, len(all.Offers)) 203 c.Assert(errs.Results[0].Error, gc.ErrorMatches, fmt.Sprintf(".*%v.*", msg)) 204 s.applicationOffers.CheckCallNames(c, addOffersBackendCall) 205 } 206 207 func (s *applicationOffersSuite) assertList(c *gc.C, expectedErr error, expectedCIDRS []string) { 208 s.mockState.users["mary"] = &mockUser{"mary"} 209 s.mockState.CreateOfferAccess( 210 names.NewApplicationOfferTag("hosted-db2"), 211 names.NewUserTag("mary"), permission.ConsumeAccess) 212 filter := params.OfferFilters{ 213 Filters: []params.OfferFilter{ 214 { 215 OwnerName: "fred", 216 ModelName: "prod", 217 OfferName: "hosted-db2", 218 ApplicationName: "test", 219 }, 220 }, 221 } 222 found, err := s.api.ListApplicationOffers(filter) 223 if expectedErr != nil { 224 c.Assert(errors.Cause(err), gc.ErrorMatches, expectedErr.Error()) 225 return 226 } 227 c.Assert(err, jc.ErrorIsNil) 228 229 expectedOfferDetails := []params.ApplicationOfferAdminDetails{ 230 { 231 ApplicationOfferDetails: params.ApplicationOfferDetails{ 232 SourceModelTag: testing.ModelTag.String(), 233 ApplicationDescription: "description", 234 OfferName: "hosted-db2", 235 OfferUUID: "hosted-db2-uuid", 236 OfferURL: "fred/prod.hosted-db2", 237 Endpoints: []params.RemoteEndpoint{{Name: "db"}}, 238 Bindings: map[string]string{"db2": "myspace"}, 239 Spaces: []params.RemoteSpace{ 240 { 241 Name: "myspace", 242 ProviderId: "juju-space-myspace", 243 Subnets: []params.Subnet{{CIDR: "4.3.2.0/24", ProviderId: "juju-subnet-1", Zones: []string{"az1"}}}, 244 }, 245 }, 246 Users: []params.OfferUserDetails{ 247 {UserName: "admin", DisplayName: "", Access: "admin"}, 248 {UserName: "mary", DisplayName: "mary", Access: "consume"}, 249 }, 250 }, 251 ApplicationName: "test", 252 CharmURL: "cs:db2-2", 253 Connections: []params.OfferConnection{{ 254 SourceModelTag: testing.ModelTag.String(), 255 RelationId: 1, 256 Endpoint: "db", 257 Username: "fred", 258 Status: params.EntityStatus{Status: "joined"}, 259 IngressSubnets: expectedCIDRS, 260 }}, 261 }, 262 } 263 if s.mockState.model.modelType == state.ModelTypeCAAS { 264 expectedOfferDetails[0].Spaces = nil 265 expectedOfferDetails[0].Bindings = nil 266 } 267 c.Assert(found, jc.DeepEquals, params.QueryApplicationOffersResults{ 268 expectedOfferDetails, 269 }) 270 s.applicationOffers.CheckCallNames(c, listOffersBackendCall) 271 if s.mockState.model.modelType == state.ModelTypeCAAS { 272 s.env.stub.CheckNoCalls(c) 273 return 274 } 275 s.env.stub.CheckCallNames(c, "ProviderSpaceInfo") 276 s.env.stub.CheckCall(c, 0, "ProviderSpaceInfo", &network.SpaceInfo{ 277 Name: "myspace", 278 ProviderId: "juju-space-myspace", 279 Subnets: []network.SubnetInfo{{ 280 CIDR: "4.3.2.0/24", 281 ProviderId: "juju-subnet-1", 282 AvailabilityZones: []string{"az1"}, 283 }}, 284 }) 285 } 286 287 func (s *applicationOffersSuite) TestList(c *gc.C) { 288 s.authorizer.Tag = names.NewUserTag("admin") 289 s.setupOffers(c, "test", false) 290 s.assertList(c, nil, []string{"192.168.1.0/32", "10.0.0.0/8"}) 291 } 292 293 func (s *applicationOffersSuite) TestListCAAS(c *gc.C) { 294 s.authorizer.Tag = names.NewUserTag("admin") 295 s.setupOffers(c, "test", false) 296 s.mockState.model.modelType = state.ModelTypeCAAS 297 s.assertList(c, nil, []string{"192.168.1.0/32", "10.0.0.0/8"}) 298 } 299 300 func (s *applicationOffersSuite) TestListNoRelationNetworks(c *gc.C) { 301 s.authorizer.Tag = names.NewUserTag("admin") 302 s.mockState.relationNetworks = nil 303 s.setupOffers(c, "test", false) 304 s.assertList(c, nil, nil) 305 } 306 307 func (s *applicationOffersSuite) TestListPermission(c *gc.C) { 308 s.setupOffers(c, "test", false) 309 s.assertList(c, common.ErrPerm, nil) 310 } 311 312 func (s *applicationOffersSuite) TestListError(c *gc.C) { 313 s.setupOffers(c, "test", false) 314 s.authorizer.Tag = names.NewUserTag("admin") 315 filter := params.OfferFilters{ 316 Filters: []params.OfferFilter{ 317 { 318 OwnerName: "fred", 319 ModelName: "prod", 320 OfferName: "hosted-db2", 321 ApplicationName: "test", 322 }, 323 }, 324 } 325 msg := "fail" 326 327 s.applicationOffers.listOffers = func(filters ...jujucrossmodel.ApplicationOfferFilter) ([]jujucrossmodel.ApplicationOffer, error) { 328 return nil, errors.New(msg) 329 } 330 331 _, err := s.api.ListApplicationOffers(filter) 332 c.Assert(err, gc.ErrorMatches, fmt.Sprintf(".*%v.*", msg)) 333 s.applicationOffers.CheckCallNames(c, listOffersBackendCall) 334 } 335 336 func (s *applicationOffersSuite) TestListFilterRequiresModel(c *gc.C) { 337 s.setupOffers(c, "test", false) 338 filter := params.OfferFilters{ 339 Filters: []params.OfferFilter{ 340 { 341 OfferName: "hosted-db2", 342 ApplicationName: "test", 343 }, 344 }, 345 } 346 _, err := s.api.ListApplicationOffers(filter) 347 c.Assert(err, gc.ErrorMatches, "application offer filter must specify a model name") 348 } 349 350 func (s *applicationOffersSuite) TestListRequiresFilter(c *gc.C) { 351 s.setupOffers(c, "test", false) 352 _, err := s.api.ListApplicationOffers(params.OfferFilters{}) 353 c.Assert(err, gc.ErrorMatches, "at least one offer filter is required") 354 } 355 356 func (s *applicationOffersSuite) assertShow(c *gc.C, url string, expected []params.ApplicationOfferResult) { 357 s.setupOffers(c, "", false) 358 s.mockState.users["mary"] = &mockUser{"mary"} 359 s.mockState.CreateOfferAccess( 360 names.NewApplicationOfferTag("hosted-db2"), 361 names.NewUserTag("mary"), permission.ConsumeAccess) 362 filter := params.OfferURLs{[]string{url}} 363 364 found, err := s.api.ApplicationOffers(filter) 365 c.Assert(err, jc.ErrorIsNil) 366 c.Assert(found.Results, jc.DeepEquals, expected) 367 s.applicationOffers.CheckCallNames(c, listOffersBackendCall) 368 if len(expected) > 0 { 369 return 370 } 371 s.env.stub.CheckCallNames(c, "ProviderSpaceInfo") 372 s.env.stub.CheckCall(c, 0, "ProviderSpaceInfo", &network.SpaceInfo{ 373 Name: "myspace", 374 ProviderId: "juju-space-myspace", 375 Subnets: []network.SubnetInfo{{ 376 CIDR: "4.3.2.0/24", 377 ProviderId: "juju-subnet-1", 378 AvailabilityZones: []string{"az1"}, 379 }}, 380 }) 381 } 382 383 func (s *applicationOffersSuite) TestShow(c *gc.C) { 384 expected := []params.ApplicationOfferResult{{ 385 Result: ¶ms.ApplicationOfferAdminDetails{ 386 ApplicationOfferDetails: params.ApplicationOfferDetails{ 387 SourceModelTag: testing.ModelTag.String(), 388 ApplicationDescription: "description", 389 OfferURL: "fred/prod.hosted-db2", 390 OfferName: "hosted-db2", 391 OfferUUID: "hosted-db2-uuid", 392 Endpoints: []params.RemoteEndpoint{{Name: "db"}}, 393 Bindings: map[string]string{"db2": "myspace"}, 394 Spaces: []params.RemoteSpace{ 395 { 396 Name: "myspace", 397 ProviderId: "juju-space-myspace", 398 Subnets: []params.Subnet{{CIDR: "4.3.2.0/24", ProviderId: "juju-subnet-1", Zones: []string{"az1"}}}, 399 }, 400 }, 401 Users: []params.OfferUserDetails{ 402 {UserName: "fred", DisplayName: "", Access: "admin"}, 403 {UserName: "mary", DisplayName: "mary", Access: "consume"}, 404 }, 405 }, 406 ApplicationName: "test", 407 CharmURL: "cs:db2-2", 408 Connections: []params.OfferConnection{{ 409 SourceModelTag: "model-deadbeef-0bad-400d-8000-4b1d0d06f00d", 410 RelationId: 1, Username: "fred", Endpoint: "db", 411 Status: params.EntityStatus{Status: "joined"}, 412 IngressSubnets: []string{"192.168.1.0/32", "10.0.0.0/8"}, 413 }}, 414 }, 415 }} 416 s.authorizer.Tag = names.NewUserTag("admin") 417 expected[0].Result.Users[0].UserName = "admin" 418 s.assertShow(c, "fred/prod.hosted-db2", expected) 419 // Again with an unqualified model path. 420 s.authorizer.AdminTag = names.NewUserTag("fred") 421 s.authorizer.Tag = s.authorizer.AdminTag 422 expected[0].Result.Users[0].UserName = "fred" 423 s.applicationOffers.ResetCalls() 424 s.assertShow(c, "prod.hosted-db2", expected) 425 } 426 427 func (s *applicationOffersSuite) TestShowNoPermission(c *gc.C) { 428 s.mockState.users["someone"] = &mockUser{"someone"} 429 user := names.NewUserTag("someone") 430 offer := names.NewApplicationOfferTag("hosted-db2") 431 err := s.mockState.CreateOfferAccess(offer, user, permission.NoAccess) 432 c.Assert(err, jc.ErrorIsNil) 433 434 s.authorizer.Tag = user 435 expected := []params.ApplicationOfferResult{{ 436 Error: common.ServerError(errors.NotFoundf("application offer %q", "fred/prod.hosted-db2")), 437 }} 438 s.assertShow(c, "fred/prod.hosted-db2", expected) 439 } 440 441 func (s *applicationOffersSuite) TestShowPermission(c *gc.C) { 442 user := names.NewUserTag("someone") 443 s.authorizer.Tag = user 444 expected := []params.ApplicationOfferResult{{ 445 Result: ¶ms.ApplicationOfferAdminDetails{ 446 ApplicationOfferDetails: params.ApplicationOfferDetails{ 447 SourceModelTag: testing.ModelTag.String(), 448 ApplicationDescription: "description", 449 OfferURL: "fred/prod.hosted-db2", 450 OfferName: "hosted-db2", 451 OfferUUID: "hosted-db2-uuid", 452 Endpoints: []params.RemoteEndpoint{{Name: "db"}}, 453 Bindings: map[string]string{"db2": "myspace"}, 454 Spaces: []params.RemoteSpace{ 455 { 456 Name: "myspace", 457 ProviderId: "juju-space-myspace", 458 Subnets: []params.Subnet{{CIDR: "4.3.2.0/24", ProviderId: "juju-subnet-1", Zones: []string{"az1"}}}, 459 }, 460 }, 461 Users: []params.OfferUserDetails{ 462 {UserName: "someone", DisplayName: "someone", Access: "read"}, 463 }, 464 }, 465 }}} 466 s.mockState.users[user.Name()] = &mockUser{user.Name()} 467 s.mockState.CreateOfferAccess(names.NewApplicationOfferTag("hosted-db2"), user, permission.ReadAccess) 468 s.assertShow(c, "fred/prod.hosted-db2", expected) 469 } 470 471 func (s *applicationOffersSuite) TestShowError(c *gc.C) { 472 url := "fred/prod.hosted-db2" 473 filter := params.OfferURLs{[]string{url}} 474 msg := "fail" 475 476 s.applicationOffers.listOffers = func(filters ...jujucrossmodel.ApplicationOfferFilter) ([]jujucrossmodel.ApplicationOffer, error) { 477 return nil, errors.New(msg) 478 } 479 s.mockState.model = &mockModel{uuid: testing.ModelTag.Id(), name: "prod", owner: "fred", modelType: state.ModelTypeIAAS} 480 481 _, err := s.api.ApplicationOffers(filter) 482 c.Assert(err, gc.ErrorMatches, fmt.Sprintf(".*%v.*", msg)) 483 s.applicationOffers.CheckCallNames(c, listOffersBackendCall) 484 } 485 486 func (s *applicationOffersSuite) TestShowNotFound(c *gc.C) { 487 urls := []string{"fred/prod.hosted-db2"} 488 filter := params.OfferURLs{urls} 489 490 s.applicationOffers.listOffers = func(filters ...jujucrossmodel.ApplicationOfferFilter) ([]jujucrossmodel.ApplicationOffer, error) { 491 return nil, nil 492 } 493 s.mockState.model = &mockModel{uuid: testing.ModelTag.Id(), name: "prod", owner: "fred", modelType: state.ModelTypeIAAS} 494 495 found, err := s.api.ApplicationOffers(filter) 496 c.Assert(err, jc.ErrorIsNil) 497 c.Assert(found.Results, gc.HasLen, 1) 498 c.Assert(found.Results[0].Error.Error(), gc.Matches, `application offer "fred/prod.hosted-db2" not found`) 499 s.applicationOffers.CheckCallNames(c, listOffersBackendCall) 500 } 501 502 func (s *applicationOffersSuite) TestShowRejectsEndpoints(c *gc.C) { 503 urls := []string{"fred/prod.hosted-db2:db"} 504 filter := params.OfferURLs{urls} 505 s.mockState.model = &mockModel{uuid: testing.ModelTag.Id(), name: "prod", owner: "fred", modelType: state.ModelTypeIAAS} 506 507 found, err := s.api.ApplicationOffers(filter) 508 c.Assert(err, jc.ErrorIsNil) 509 c.Assert(found.Results, gc.HasLen, 1) 510 c.Assert(found.Results[0].Error.Message, gc.Equals, `remote application "fred/prod.hosted-db2:db" shouldn't include endpoint`) 511 } 512 513 func (s *applicationOffersSuite) TestShowErrorMsgMultipleURLs(c *gc.C) { 514 urls := []string{"fred/prod.hosted-mysql", "fred/test.hosted-db2"} 515 filter := params.OfferURLs{urls} 516 517 s.applicationOffers.listOffers = func(filters ...jujucrossmodel.ApplicationOfferFilter) ([]jujucrossmodel.ApplicationOffer, error) { 518 return nil, nil 519 } 520 s.mockState.model = &mockModel{uuid: testing.ModelTag.Id(), name: "prod", owner: "fred", modelType: state.ModelTypeIAAS} 521 anotherModel := &mockModel{uuid: "uuid2", name: "test", owner: "fred", modelType: state.ModelTypeIAAS} 522 s.mockStatePool.st["uuid2"] = &mockState{ 523 modelUUID: "uuid2", 524 model: anotherModel, 525 } 526 s.mockState.allmodels = []applicationoffers.Model{s.mockState.model, anotherModel} 527 528 found, err := s.api.ApplicationOffers(filter) 529 c.Assert(err, jc.ErrorIsNil) 530 c.Assert(found.Results, gc.HasLen, 2) 531 c.Assert(found.Results[0].Error.Error(), gc.Matches, `application offer "fred/prod.hosted-mysql" not found`) 532 c.Assert(found.Results[1].Error.Error(), gc.Matches, `application offer "fred/test.hosted-db2" not found`) 533 s.applicationOffers.CheckCallNames(c, listOffersBackendCall, listOffersBackendCall) 534 } 535 536 func (s *applicationOffersSuite) TestShowFoundMultiple(c *gc.C) { 537 name := "test" 538 url := "fred/prod.hosted-" + name 539 anOffer := jujucrossmodel.ApplicationOffer{ 540 ApplicationName: name, 541 ApplicationDescription: "description", 542 OfferName: "hosted-" + name, 543 OfferUUID: "hosted-" + name + "-uuid", 544 Endpoints: map[string]charm.Relation{"db": {Name: "db"}}, 545 } 546 547 name2 := "testagain" 548 url2 := "mary/test.hosted-" + name2 549 anOffer2 := jujucrossmodel.ApplicationOffer{ 550 ApplicationName: name2, 551 ApplicationDescription: "description2", 552 OfferName: "hosted-" + name2, 553 OfferUUID: "hosted-" + name2 + "-uuid", 554 Endpoints: map[string]charm.Relation{"db2": {Name: "db2"}}, 555 } 556 557 filter := params.OfferURLs{[]string{url, url2}} 558 559 s.applicationOffers.listOffers = func(filters ...jujucrossmodel.ApplicationOfferFilter) ([]jujucrossmodel.ApplicationOffer, error) { 560 c.Assert(filters, gc.HasLen, 1) 561 if filters[0].OfferName == "hosted-test" { 562 return []jujucrossmodel.ApplicationOffer{anOffer}, nil 563 } 564 return []jujucrossmodel.ApplicationOffer{anOffer2}, nil 565 } 566 ch := &mockCharm{meta: &charm.Meta{Description: "A pretty popular blog engine"}} 567 s.mockState.applications = map[string]crossmodel.Application{ 568 "test": &mockApplication{ 569 charm: ch, curl: charm.MustParseURL("db2-2"), bindings: map[string]string{"db": "myspace"}}, 570 } 571 572 model := &mockModel{uuid: testing.ModelTag.Id(), name: "prod", owner: "fred", modelType: state.ModelTypeIAAS} 573 anotherModel := &mockModel{uuid: "uuid2", name: "test", owner: "mary", modelType: state.ModelTypeIAAS} 574 575 s.mockState.model = model 576 s.mockState.allmodels = []applicationoffers.Model{model, anotherModel} 577 s.mockState.spaces["myspace"] = &mockSpace{ 578 name: "myspace", 579 providerId: "juju-space-myspace", 580 subnets: []applicationoffers.Subnet{ 581 &mockSubnet{cidr: "4.3.2.0/24", providerId: "juju-subnet-1", zones: []string{"az1"}}, 582 }, 583 } 584 s.env.spaceInfo = &environs.ProviderSpaceInfo{ 585 SpaceInfo: network.SpaceInfo{ 586 Name: "myspace", 587 ProviderId: "juju-space-myspace", 588 Subnets: []network.SubnetInfo{{ 589 CIDR: "4.3.2.0/24", 590 ProviderId: "juju-subnet-1", 591 AvailabilityZones: []string{"az1"}, 592 }}, 593 }, 594 } 595 596 user := names.NewUserTag("someone") 597 s.authorizer.Tag = user 598 s.mockState.users[user.Name()] = &mockUser{user.Name()} 599 s.mockState.CreateOfferAccess(names.NewApplicationOfferTag("hosted-test"), user, permission.ReadAccess) 600 601 anotherState := &mockState{ 602 modelUUID: "uuid2", 603 users: make(map[string]applicationoffers.User), 604 accessPerms: make(map[offerAccess]permission.Access), 605 spaces: make(map[string]applicationoffers.Space), 606 model: anotherModel, 607 } 608 anotherState.applications = map[string]crossmodel.Application{ 609 "testagain": &mockApplication{ 610 charm: ch, curl: charm.MustParseURL("mysql-2"), bindings: map[string]string{"db2": "anotherspace"}}, 611 } 612 anotherState.spaces["anotherspace"] = &mockSpace{ 613 name: "anotherspace", 614 providerId: "juju-space-myspace", 615 subnets: []applicationoffers.Subnet{ 616 &mockSubnet{cidr: "4.3.2.0/24", providerId: "juju-subnet-1", zones: []string{"az1"}}, 617 }, 618 } 619 anotherState.users[user.Name()] = &mockUser{user.Name()} 620 anotherState.CreateOfferAccess(names.NewApplicationOfferTag("hosted-testagain"), user, permission.ConsumeAccess) 621 s.mockStatePool.st["uuid2"] = anotherState 622 623 found, err := s.api.ApplicationOffers(filter) 624 c.Assert(err, jc.ErrorIsNil) 625 var results []params.ApplicationOfferAdminDetails 626 for _, r := range found.Results { 627 c.Assert(r.Error, gc.IsNil) 628 results = append(results, *r.Result) 629 } 630 c.Assert(results, jc.DeepEquals, []params.ApplicationOfferAdminDetails{ 631 { 632 ApplicationOfferDetails: params.ApplicationOfferDetails{ 633 SourceModelTag: testing.ModelTag.String(), 634 ApplicationDescription: "description", 635 OfferName: "hosted-" + name, 636 OfferUUID: "hosted-" + name + "-uuid", 637 OfferURL: url, 638 Endpoints: []params.RemoteEndpoint{{Name: "db"}}, 639 Bindings: map[string]string{"db": "myspace"}, 640 Spaces: []params.RemoteSpace{ 641 { 642 Name: "myspace", 643 ProviderId: "juju-space-myspace", 644 Subnets: []params.Subnet{{CIDR: "4.3.2.0/24", ProviderId: "juju-subnet-1", Zones: []string{"az1"}}}, 645 }, 646 }, 647 Users: []params.OfferUserDetails{ 648 {UserName: "someone", DisplayName: "someone", Access: "read"}, 649 }, 650 }, 651 }, { 652 ApplicationOfferDetails: params.ApplicationOfferDetails{ 653 SourceModelTag: "model-uuid2", 654 ApplicationDescription: "description2", 655 OfferName: "hosted-" + name2, 656 OfferUUID: "hosted-" + name2 + "-uuid", 657 OfferURL: url2, 658 Endpoints: []params.RemoteEndpoint{{Name: "db2"}}, 659 Users: []params.OfferUserDetails{ 660 {UserName: "someone", DisplayName: "someone", Access: "consume"}, 661 }}, 662 }, 663 }) 664 s.applicationOffers.CheckCallNames(c, listOffersBackendCall, listOffersBackendCall) 665 } 666 667 func (s *applicationOffersSuite) assertFind(c *gc.C, expected []params.ApplicationOfferAdminDetails) { 668 filter := params.OfferFilters{ 669 Filters: []params.OfferFilter{ 670 { 671 OfferName: "hosted-db2", 672 Endpoints: []params.EndpointFilterAttributes{{ 673 Interface: "db2", 674 }}, 675 }, 676 }, 677 } 678 found, err := s.api.FindApplicationOffers(filter) 679 c.Assert(err, jc.ErrorIsNil) 680 c.Assert(found, jc.DeepEquals, params.QueryApplicationOffersResults{ 681 Results: expected, 682 }) 683 s.applicationOffers.CheckCallNames(c, listOffersBackendCall) 684 if len(expected) == 0 { 685 return 686 } 687 s.env.stub.CheckCallNames(c, "ProviderSpaceInfo") 688 s.env.stub.CheckCall(c, 0, "ProviderSpaceInfo", &network.SpaceInfo{ 689 Name: "myspace", 690 ProviderId: "juju-space-myspace", 691 Subnets: []network.SubnetInfo{{ 692 CIDR: "4.3.2.0/24", 693 ProviderId: "juju-subnet-1", 694 AvailabilityZones: []string{"az1"}, 695 }}, 696 }) 697 } 698 699 func (s *applicationOffersSuite) TestFid(c *gc.C) { 700 s.setupOffers(c, "", true) 701 s.authorizer.Tag = names.NewUserTag("admin") 702 expected := []params.ApplicationOfferAdminDetails{ 703 { 704 ApplicationOfferDetails: params.ApplicationOfferDetails{ 705 SourceModelTag: testing.ModelTag.String(), 706 ApplicationDescription: "description", 707 OfferName: "hosted-db2", 708 OfferUUID: "hosted-db2-uuid", 709 OfferURL: "fred/prod.hosted-db2", 710 Endpoints: []params.RemoteEndpoint{{Name: "db"}}, 711 Bindings: map[string]string{"db2": "myspace"}, 712 Spaces: []params.RemoteSpace{ 713 { 714 Name: "myspace", 715 ProviderId: "juju-space-myspace", 716 Subnets: []params.Subnet{{CIDR: "4.3.2.0/24", ProviderId: "juju-subnet-1", Zones: []string{"az1"}}}, 717 }, 718 }, 719 Users: []params.OfferUserDetails{ 720 {UserName: "admin", DisplayName: "", Access: "admin"}, 721 }}, 722 ApplicationName: "test", 723 CharmURL: "cs:db2-2", 724 Connections: []params.OfferConnection{{ 725 SourceModelTag: "model-deadbeef-0bad-400d-8000-4b1d0d06f00d", 726 RelationId: 1, Username: "fred", Endpoint: "db", 727 Status: params.EntityStatus{Status: "joined"}, 728 IngressSubnets: []string{"192.168.1.0/32", "10.0.0.0/8"}, 729 }}, 730 }, 731 } 732 s.assertFind(c, expected) 733 } 734 735 func (s *applicationOffersSuite) TestFindNoPermission(c *gc.C) { 736 s.mockState.users["someone"] = &mockUser{"someone"} 737 user := names.NewUserTag("someone") 738 offer := names.NewApplicationOfferTag("hosted-db2") 739 err := s.mockState.CreateOfferAccess(offer, user, permission.NoAccess) 740 c.Assert(err, jc.ErrorIsNil) 741 742 s.setupOffers(c, "", true) 743 s.authorizer.Tag = names.NewUserTag("someone") 744 s.assertFind(c, []params.ApplicationOfferAdminDetails{}) 745 } 746 747 func (s *applicationOffersSuite) TestFindPermission(c *gc.C) { 748 s.setupOffers(c, "", true) 749 user := names.NewUserTag("someone") 750 s.authorizer.Tag = user 751 expected := []params.ApplicationOfferAdminDetails{ 752 { 753 ApplicationOfferDetails: params.ApplicationOfferDetails{ 754 SourceModelTag: testing.ModelTag.String(), 755 ApplicationDescription: "description", 756 OfferName: "hosted-db2", 757 OfferUUID: "hosted-db2-uuid", 758 OfferURL: "fred/prod.hosted-db2", 759 Endpoints: []params.RemoteEndpoint{{Name: "db"}}, 760 Bindings: map[string]string{"db2": "myspace"}, 761 Spaces: []params.RemoteSpace{ 762 { 763 Name: "myspace", 764 ProviderId: "juju-space-myspace", 765 Subnets: []params.Subnet{{CIDR: "4.3.2.0/24", ProviderId: "juju-subnet-1", Zones: []string{"az1"}}}, 766 }, 767 }, 768 Users: []params.OfferUserDetails{ 769 {UserName: "someone", DisplayName: "someone", Access: "read"}, 770 }}, 771 }, 772 } 773 s.mockState.users[user.Name()] = &mockUser{user.Name()} 774 s.mockState.CreateOfferAccess(names.NewApplicationOfferTag("hosted-db2"), user, permission.ReadAccess) 775 s.assertFind(c, expected) 776 } 777 778 func (s *applicationOffersSuite) TestFindFiltersRequireModel(c *gc.C) { 779 s.setupOffers(c, "", true) 780 filter := params.OfferFilters{ 781 Filters: []params.OfferFilter{ 782 { 783 OfferName: "hosted-db2", 784 ApplicationName: "test", 785 }, { 786 OfferName: "hosted-mysql", 787 ApplicationName: "test", 788 }, 789 }, 790 } 791 _, err := s.api.FindApplicationOffers(filter) 792 c.Assert(err, gc.ErrorMatches, "application offer filter must specify a model name") 793 } 794 795 func (s *applicationOffersSuite) TestFindRequiresFilter(c *gc.C) { 796 s.setupOffers(c, "", true) 797 _, err := s.api.FindApplicationOffers(params.OfferFilters{}) 798 c.Assert(err, gc.ErrorMatches, "at least one offer filter is required") 799 } 800 801 func (s *applicationOffersSuite) TestFindMulti(c *gc.C) { 802 db2Offer := jujucrossmodel.ApplicationOffer{ 803 OfferName: "hosted-db2", 804 OfferUUID: "hosted-db2-uuid", 805 ApplicationName: "db2", 806 ApplicationDescription: "db2 description", 807 Endpoints: map[string]charm.Relation{"db": {Name: "db2"}}, 808 } 809 mysqlOffer := jujucrossmodel.ApplicationOffer{ 810 OfferName: "hosted-mysql", 811 OfferUUID: "hosted-mysql-uuid", 812 ApplicationName: "mysql", 813 ApplicationDescription: "mysql description", 814 Endpoints: map[string]charm.Relation{"db": {Name: "mysql"}}, 815 } 816 postgresqlOffer := jujucrossmodel.ApplicationOffer{ 817 OfferName: "hosted-postgresql", 818 OfferUUID: "hosted-postgresql-uuid", 819 ApplicationName: "postgresql", 820 ApplicationDescription: "postgresql description", 821 Endpoints: map[string]charm.Relation{"db": {Name: "postgresql"}}, 822 } 823 // Include an offer with bad data to ensure it is ignored. 824 offerAppNotFound := jujucrossmodel.ApplicationOffer{ 825 OfferName: "badoffer", 826 ApplicationName: "missing", 827 } 828 829 s.applicationOffers.listOffers = func(filters ...jujucrossmodel.ApplicationOfferFilter) ([]jujucrossmodel.ApplicationOffer, error) { 830 var result []jujucrossmodel.ApplicationOffer 831 for _, f := range filters { 832 switch f.OfferName { 833 case "hosted-db2": 834 result = append(result, db2Offer) 835 case "hosted-mysql": 836 result = append(result, mysqlOffer) 837 case "hosted-postgresql": 838 result = append(result, postgresqlOffer) 839 default: 840 result = append(result, offerAppNotFound) 841 } 842 } 843 return result, nil 844 } 845 ch := &mockCharm{meta: &charm.Meta{Description: "A pretty popular blog engine"}} 846 s.mockState.applications = map[string]crossmodel.Application{ 847 "db2": &mockApplication{ 848 name: "db2", 849 charm: ch, curl: charm.MustParseURL("db2-2"), bindings: map[string]string{"db2": "myspace"}}, 850 } 851 s.mockState.model = &mockModel{uuid: testing.ModelTag.Id(), name: "prod", owner: "fred", modelType: state.ModelTypeIAAS} 852 s.mockState.spaces["myspace"] = &mockSpace{ 853 name: "myspace", 854 providerId: "juju-space-myspace", 855 subnets: []applicationoffers.Subnet{ 856 &mockSubnet{cidr: "4.3.2.0/24", providerId: "juju-subnet-1", zones: []string{"az1"}}, 857 }, 858 } 859 s.env.spaceInfo = &environs.ProviderSpaceInfo{ 860 SpaceInfo: network.SpaceInfo{ 861 Name: "myspace", 862 ProviderId: "juju-space-myspace", 863 Subnets: []network.SubnetInfo{{ 864 CIDR: "4.3.2.0/24", 865 ProviderId: "juju-subnet-1", 866 AvailabilityZones: []string{"az1"}, 867 }}, 868 }, 869 } 870 871 user := names.NewUserTag("someone") 872 s.authorizer.Tag = user 873 s.mockState.users[user.Name()] = &mockUser{user.Name()} 874 s.mockState.CreateOfferAccess(names.NewApplicationOfferTag("hosted-db2"), user, permission.ConsumeAccess) 875 876 anotherState := &mockState{ 877 modelUUID: "uuid2", 878 users: make(map[string]applicationoffers.User), 879 accessPerms: make(map[offerAccess]permission.Access), 880 spaces: make(map[string]applicationoffers.Space), 881 } 882 s.mockStatePool.st["uuid2"] = anotherState 883 anotherState.applications = map[string]crossmodel.Application{ 884 "mysql": &mockApplication{ 885 name: "mysql", 886 charm: ch, curl: charm.MustParseURL("mysql-2"), bindings: map[string]string{"mysql": "anotherspace"}}, 887 "postgresql": &mockApplication{ 888 charm: ch, curl: charm.MustParseURL("postgresql-2"), bindings: map[string]string{"postgresql": "anotherspace"}}, 889 } 890 anotherState.spaces["anotherspace"] = &mockSpace{ 891 name: "anotherspace", 892 providerId: "juju-space-anotherspace", 893 subnets: []applicationoffers.Subnet{ 894 &mockSubnet{cidr: "4.3.2.0/24", providerId: "juju-subnet-1", zones: []string{"az1"}}, 895 }, 896 } 897 anotherState.model = &mockModel{uuid: "uuid2", name: "another", owner: "mary", modelType: state.ModelTypeIAAS} 898 s.mockState.relations["hosted-mysql:server wordpress:db"] = &mockRelation{ 899 id: 1, 900 endpoint: state.Endpoint{ 901 ApplicationName: "mysql", 902 Relation: charm.Relation{ 903 Name: "server", 904 Interface: "mysql", 905 Role: "provider", 906 }, 907 }, 908 } 909 s.mockState.connections = []applicationoffers.OfferConnection{ 910 &mockOfferConnection{ 911 username: "fred", 912 modelUUID: testing.ModelTag.Id(), 913 relationKey: "hosted-db2:db wordpress:db", 914 relationId: 1, 915 }, 916 } 917 anotherState.users[user.Name()] = &mockUser{user.Name()} 918 anotherState.CreateOfferAccess(names.NewApplicationOfferTag("hosted-mysql"), user, permission.ReadAccess) 919 anotherState.CreateOfferAccess(names.NewApplicationOfferTag("hosted-postgresql"), user, permission.AdminAccess) 920 921 s.mockState.allmodels = []applicationoffers.Model{ 922 s.mockState.model, 923 anotherState.model, 924 } 925 926 filter := params.OfferFilters{ 927 Filters: []params.OfferFilter{ 928 { 929 OfferName: "hosted-db2", 930 OwnerName: "fred", 931 ModelName: "prod", 932 }, 933 { 934 OfferName: "hosted-mysql", 935 OwnerName: "mary", 936 ModelName: "another", 937 }, 938 { 939 OfferName: "hosted-postgresql", 940 OwnerName: "mary", 941 ModelName: "another", 942 }, 943 { 944 OfferName: "badoffer", 945 OwnerName: "mary", 946 ModelName: "another", 947 }, 948 }, 949 } 950 found, err := s.api.FindApplicationOffers(filter) 951 c.Assert(err, jc.ErrorIsNil) 952 c.Assert(found, jc.DeepEquals, params.QueryApplicationOffersResults{ 953 []params.ApplicationOfferAdminDetails{ 954 { 955 ApplicationOfferDetails: params.ApplicationOfferDetails{ 956 SourceModelTag: testing.ModelTag.String(), 957 ApplicationDescription: "db2 description", 958 OfferName: "hosted-db2", 959 OfferUUID: "hosted-db2-uuid", 960 OfferURL: "fred/prod.hosted-db2", 961 Endpoints: []params.RemoteEndpoint{{Name: "db"}}, 962 Bindings: map[string]string{"db2": "myspace"}, 963 Spaces: []params.RemoteSpace{ 964 { 965 Name: "myspace", 966 ProviderId: "juju-space-myspace", 967 Subnets: []params.Subnet{{CIDR: "4.3.2.0/24", ProviderId: "juju-subnet-1", Zones: []string{"az1"}}}, 968 }, 969 }, 970 Users: []params.OfferUserDetails{ 971 {UserName: "someone", DisplayName: "someone", Access: "consume"}, 972 }, 973 }, 974 }, 975 { 976 ApplicationOfferDetails: params.ApplicationOfferDetails{ 977 SourceModelTag: "model-uuid2", 978 ApplicationDescription: "mysql description", 979 OfferName: "hosted-mysql", 980 OfferUUID: "hosted-mysql-uuid", 981 OfferURL: "mary/another.hosted-mysql", 982 Endpoints: []params.RemoteEndpoint{{Name: "db"}}, 983 Users: []params.OfferUserDetails{ 984 {UserName: "someone", DisplayName: "someone", Access: "read"}, 985 }, 986 }, 987 }, 988 { 989 ApplicationOfferDetails: params.ApplicationOfferDetails{ 990 SourceModelTag: "model-uuid2", 991 ApplicationDescription: "postgresql description", 992 OfferName: "hosted-postgresql", 993 OfferUUID: "hosted-postgresql-uuid", 994 OfferURL: "mary/another.hosted-postgresql", 995 Endpoints: []params.RemoteEndpoint{{Name: "db"}}, 996 Users: []params.OfferUserDetails{ 997 {UserName: "someone", DisplayName: "someone", Access: "admin"}, 998 }, 999 }, 1000 CharmURL: "cs:postgresql-2", 1001 }, 1002 }, 1003 }) 1004 s.applicationOffers.CheckCallNames(c, listOffersBackendCall, listOffersBackendCall) 1005 } 1006 1007 func (s *applicationOffersSuite) TestFindError(c *gc.C) { 1008 filter := params.OfferFilters{ 1009 Filters: []params.OfferFilter{ 1010 { 1011 OfferName: "hosted-db2", 1012 ApplicationName: "test", 1013 }, 1014 }, 1015 } 1016 msg := "fail" 1017 1018 s.applicationOffers.listOffers = func(filters ...jujucrossmodel.ApplicationOfferFilter) ([]jujucrossmodel.ApplicationOffer, error) { 1019 return nil, errors.New(msg) 1020 } 1021 s.mockState.model = &mockModel{uuid: testing.ModelTag.Id(), name: "prod", owner: "fred", modelType: state.ModelTypeIAAS} 1022 1023 _, err := s.api.FindApplicationOffers(filter) 1024 c.Assert(err, gc.ErrorMatches, fmt.Sprintf(".*%v.*", msg)) 1025 s.applicationOffers.CheckCallNames(c, listOffersBackendCall) 1026 } 1027 1028 func (s *applicationOffersSuite) TestFindMissingModelInMultipleFilters(c *gc.C) { 1029 filter := params.OfferFilters{ 1030 Filters: []params.OfferFilter{ 1031 { 1032 OfferName: "hosted-db2", 1033 ApplicationName: "test", 1034 }, 1035 { 1036 OfferName: "hosted-mysql", 1037 ApplicationName: "test", 1038 }, 1039 }, 1040 } 1041 1042 s.applicationOffers.listOffers = func(filters ...jujucrossmodel.ApplicationOfferFilter) ([]jujucrossmodel.ApplicationOffer, error) { 1043 panic("should not be called") 1044 } 1045 1046 _, err := s.api.FindApplicationOffers(filter) 1047 c.Assert(err, gc.ErrorMatches, "application offer filter must specify a model name") 1048 s.applicationOffers.CheckCallNames(c) 1049 } 1050 1051 type consumeSuite struct { 1052 baseSuite 1053 api *applicationoffers.OffersAPIV2 1054 } 1055 1056 var _ = gc.Suite(&consumeSuite{}) 1057 1058 func (s *consumeSuite) SetUpTest(c *gc.C) { 1059 s.baseSuite.SetUpTest(c) 1060 s.bakery = &mockBakeryService{caveats: make(map[string][]checkers.Caveat)} 1061 getApplicationOffers := func(st interface{}) jujucrossmodel.ApplicationOffers { 1062 return &mockApplicationOffers{st: st.(*mockState)} 1063 } 1064 1065 resources := common.NewResources() 1066 resources.RegisterNamed("dataDir", common.StringResource(c.MkDir())) 1067 1068 getEnviron := func(modelUUID string) (environs.Environ, error) { 1069 return s.env, nil 1070 } 1071 var err error 1072 s.authContext, err = crossmodel.NewAuthContext(&mockCommonStatePool{s.mockStatePool}, s.bakery, s.bakery) 1073 c.Assert(err, jc.ErrorIsNil) 1074 apiV1, err := applicationoffers.CreateOffersAPI( 1075 getApplicationOffers, getEnviron, getFakeControllerInfo, 1076 s.mockState, s.mockStatePool, s.authorizer, resources, s.authContext, 1077 context.NewCloudCallContext(), 1078 ) 1079 c.Assert(err, jc.ErrorIsNil) 1080 s.api = &applicationoffers.OffersAPIV2{OffersAPI: apiV1} 1081 } 1082 1083 func (s *consumeSuite) TestConsumeDetailsRejectsEndpoints(c *gc.C) { 1084 results, err := s.api.GetConsumeDetails(params.OfferURLs{ 1085 OfferURLs: []string{"fred/prod.application:db"}, 1086 }) 1087 c.Assert(err, jc.ErrorIsNil) 1088 c.Assert(results.Results, gc.HasLen, 1) 1089 c.Assert(results.Results[0].Error != nil, jc.IsTrue) 1090 c.Assert(results.Results[0].Error.Message, gc.Equals, `remote application "fred/prod.application:db" shouldn't include endpoint`) 1091 } 1092 1093 func (s *consumeSuite) TestConsumeDetailsNoPermission(c *gc.C) { 1094 s.setupOffer() 1095 st := s.mockStatePool.st[testing.ModelTag.Id()] 1096 st.(*mockState).users["someone"] = &mockUser{"someone"} 1097 apiUser := names.NewUserTag("someone") 1098 offer := names.NewApplicationOfferTag("hosted-mysql") 1099 err := st.CreateOfferAccess(offer, apiUser, permission.NoAccess) 1100 c.Assert(err, jc.ErrorIsNil) 1101 1102 s.authorizer.Tag = apiUser 1103 results, err := s.api.GetConsumeDetails(params.OfferURLs{ 1104 OfferURLs: []string{"fred/prod.hosted-mysql"}, 1105 }) 1106 c.Assert(err, jc.ErrorIsNil) 1107 expected := []params.ConsumeOfferDetailsResult{{ 1108 Error: common.ServerError(errors.NotFoundf("application offer %q", "fred/prod.hosted-mysql")), 1109 }} 1110 c.Assert(results.Results, jc.DeepEquals, expected) 1111 } 1112 1113 func (s *consumeSuite) TestConsumeDetailsWithPermission(c *gc.C) { 1114 s.setupOffer() 1115 st := s.mockStatePool.st[testing.ModelTag.Id()] 1116 st.(*mockState).users["someone"] = &mockUser{"someone"} 1117 apiUser := names.NewUserTag("someone") 1118 offer := names.NewApplicationOfferTag("hosted-mysql") 1119 err := st.CreateOfferAccess(offer, apiUser, permission.ConsumeAccess) 1120 c.Assert(err, jc.ErrorIsNil) 1121 1122 s.authorizer.Tag = apiUser 1123 results, err := s.api.GetConsumeDetails(params.OfferURLs{ 1124 OfferURLs: []string{"fred/prod.hosted-mysql"}, 1125 }) 1126 c.Assert(err, jc.ErrorIsNil) 1127 c.Assert(results.Results, gc.HasLen, 1) 1128 c.Assert(results.Results[0].Error, gc.IsNil) 1129 c.Assert(results.Results[0].Offer, jc.DeepEquals, ¶ms.ApplicationOfferDetails{ 1130 SourceModelTag: "model-deadbeef-0bad-400d-8000-4b1d0d06f00d", 1131 OfferURL: "fred/prod.hosted-mysql", 1132 OfferName: "hosted-mysql", 1133 OfferUUID: "hosted-mysql-uuid", 1134 ApplicationDescription: "a database", 1135 Endpoints: []params.RemoteEndpoint{{Name: "server", Role: "provider", Interface: "mysql"}}, 1136 Bindings: map[string]string{"database": "myspace"}, 1137 Spaces: []params.RemoteSpace{ 1138 { 1139 Name: "myspace", 1140 ProviderId: "juju-space-myspace", 1141 Subnets: []params.Subnet{{CIDR: "4.3.2.0/24", ProviderId: "juju-subnet-1", Zones: []string{"az1"}}}, 1142 }, 1143 }, 1144 Users: []params.OfferUserDetails{ 1145 {UserName: "someone", DisplayName: "someone", Access: "consume"}, 1146 }, 1147 }) 1148 c.Assert(results.Results[0].ControllerInfo, jc.DeepEquals, ¶ms.ExternalControllerInfo{ 1149 ControllerTag: testing.ControllerTag.String(), 1150 Addrs: []string{"192.168.1.1:17070"}, 1151 CACert: testing.CACert, 1152 }) 1153 c.Assert(results.Results[0].Macaroon.Id(), jc.DeepEquals, []byte("id")) 1154 cav := s.bakery.caveats[string(results.Results[0].Macaroon.Id())] 1155 c.Check(cav, gc.HasLen, 4) 1156 c.Check(strings.HasPrefix(cav[0].Condition, "time-before "), jc.IsTrue) 1157 c.Check(cav[1].Condition, gc.Equals, "declared source-model-uuid deadbeef-0bad-400d-8000-4b1d0d06f00d") 1158 c.Check(cav[2].Condition, gc.Equals, "declared offer-uuid hosted-mysql-uuid") 1159 c.Check(cav[3].Condition, gc.Equals, "declared username someone") 1160 } 1161 1162 func (s *consumeSuite) TestConsumeDetailsDefaultEndpoint(c *gc.C) { 1163 s.setupOffer() 1164 1165 st := s.mockStatePool.st[testing.ModelTag.Id()].(*mockState) 1166 st.users["someone"] = &mockUser{"someone"} 1167 delete(st.applications["mysql"].(*mockApplication).bindings, "database") 1168 st.spaces[""] = &mockSpace{ 1169 name: "", 1170 } 1171 1172 apiUser := names.NewUserTag("someone") 1173 offer := names.NewApplicationOfferTag("hosted-mysql") 1174 err := st.CreateOfferAccess(offer, apiUser, permission.ConsumeAccess) 1175 c.Assert(err, jc.ErrorIsNil) 1176 1177 s.authorizer.Tag = apiUser 1178 results, err := s.api.GetConsumeDetails(params.OfferURLs{ 1179 OfferURLs: []string{"fred/prod.hosted-mysql"}, 1180 }) 1181 c.Assert(err, jc.ErrorIsNil) 1182 c.Assert(results.Results, gc.HasLen, 1) 1183 c.Assert(results.Results[0].Error, gc.IsNil) 1184 c.Assert(results.Results[0].Offer, jc.DeepEquals, ¶ms.ApplicationOfferDetails{ 1185 SourceModelTag: "model-deadbeef-0bad-400d-8000-4b1d0d06f00d", 1186 OfferURL: "fred/prod.hosted-mysql", 1187 OfferName: "hosted-mysql", 1188 OfferUUID: "hosted-mysql-uuid", 1189 ApplicationDescription: "a database", 1190 Endpoints: []params.RemoteEndpoint{{Name: "server", Role: "provider", Interface: "mysql"}}, 1191 Bindings: map[string]string{"database": ""}, 1192 Users: []params.OfferUserDetails{ 1193 {UserName: "someone", DisplayName: "someone", Access: "consume"}, 1194 }, 1195 }) 1196 } 1197 1198 func (s *consumeSuite) setupOffer() { 1199 modelUUID := testing.ModelTag.Id() 1200 offerName := "hosted-mysql" 1201 1202 model := &mockModel{uuid: modelUUID, name: "prod", owner: "fred", modelType: state.ModelTypeIAAS} 1203 s.mockState.allmodels = []applicationoffers.Model{model} 1204 st := &mockState{ 1205 modelUUID: modelUUID, 1206 model: model, 1207 applications: make(map[string]crossmodel.Application), 1208 applicationOffers: make(map[string]jujucrossmodel.ApplicationOffer), 1209 users: make(map[string]applicationoffers.User), 1210 accessPerms: make(map[offerAccess]permission.Access), 1211 spaces: make(map[string]applicationoffers.Space), 1212 relations: make(map[string]crossmodel.Relation), 1213 } 1214 s.mockStatePool.st[modelUUID] = st 1215 anOffer := jujucrossmodel.ApplicationOffer{ 1216 ApplicationName: "mysql", 1217 ApplicationDescription: "a database", 1218 OfferName: offerName, 1219 OfferUUID: offerName + "-uuid", 1220 Endpoints: map[string]charm.Relation{ 1221 "server": {Name: "database", Interface: "mysql", Role: "provider", Scope: "global"}}, 1222 } 1223 st.applicationOffers[offerName] = anOffer 1224 st.applications["mysql"] = &mockApplication{ 1225 name: "mysql", 1226 charm: &mockCharm{meta: &charm.Meta{Description: "A pretty popular database"}}, 1227 bindings: map[string]string{"database": "myspace"}, 1228 endpoints: []state.Endpoint{ 1229 {Relation: charm.Relation{Name: "juju-info", Role: "provider", Interface: "juju-info", Limit: 0, Scope: "global"}}, 1230 {Relation: charm.Relation{Name: "server", Role: "provider", Interface: "mysql", Limit: 0, Scope: "global"}}, 1231 {Relation: charm.Relation{Name: "server-admin", Role: "provider", Interface: "mysql-root", Limit: 0, Scope: "global"}}}, 1232 } 1233 st.spaces["myspace"] = &mockSpace{ 1234 name: "myspace", 1235 providerId: "juju-space-myspace", 1236 subnets: []applicationoffers.Subnet{ 1237 &mockSubnet{cidr: "4.3.2.0/24", providerId: "juju-subnet-1", zones: []string{"az1"}}, 1238 }, 1239 } 1240 s.env.spaceInfo = &environs.ProviderSpaceInfo{ 1241 SpaceInfo: network.SpaceInfo{ 1242 Name: "myspace", 1243 ProviderId: "juju-space-myspace", 1244 Subnets: []network.SubnetInfo{{ 1245 CIDR: "4.3.2.0/24", 1246 ProviderId: "juju-subnet-1", 1247 AvailabilityZones: []string{"az1"}, 1248 }}, 1249 }, 1250 } 1251 } 1252 1253 func (s *consumeSuite) TestRemoteApplicationInfo(c *gc.C) { 1254 s.setupOffer() 1255 st := s.mockStatePool.st[testing.ModelTag.Id()] 1256 st.(*mockState).users["foobar"] = &mockUser{"foobar"} 1257 1258 // Give user permission to see the offer. 1259 user := names.NewUserTag("foobar") 1260 offer := names.NewApplicationOfferTag("hosted-mysql") 1261 err := st.CreateOfferAccess(offer, user, permission.ConsumeAccess) 1262 c.Assert(err, jc.ErrorIsNil) 1263 1264 s.authorizer.Tag = user 1265 results, err := s.api.RemoteApplicationInfo(params.OfferURLs{ 1266 OfferURLs: []string{"fred/prod.hosted-mysql", "fred/prod.unknown"}, 1267 }) 1268 c.Assert(err, jc.ErrorIsNil) 1269 c.Assert(results.Results, gc.HasLen, 2) 1270 c.Assert(results.Results[0].Error, gc.IsNil) 1271 c.Assert(results.Results, jc.DeepEquals, []params.RemoteApplicationInfoResult{ 1272 {Result: ¶ms.RemoteApplicationInfo{ 1273 ModelTag: testing.ModelTag.String(), 1274 Name: "hosted-mysql", 1275 Description: "a database", 1276 OfferURL: "fred/prod.hosted-mysql", 1277 SourceModelLabel: "prod", 1278 IconURLPath: "rest/1.0/remote-application/hosted-mysql/icon", 1279 Endpoints: []params.RemoteEndpoint{ 1280 {Name: "server", Role: "provider", Interface: "mysql"}}, 1281 }}, 1282 { 1283 Error: ¶ms.Error{Message: `application offer "unknown" not found`, Code: "not found"}, 1284 }, 1285 }) 1286 s.env.stub.CheckCallNames(c, "ProviderSpaceInfo") 1287 s.env.stub.CheckCall(c, 0, "ProviderSpaceInfo", &network.SpaceInfo{ 1288 Name: "myspace", 1289 ProviderId: "juju-space-myspace", 1290 Subnets: []network.SubnetInfo{{ 1291 CIDR: "4.3.2.0/24", 1292 ProviderId: "juju-subnet-1", 1293 AvailabilityZones: []string{"az1"}, 1294 }}, 1295 }) 1296 } 1297 1298 func (s *consumeSuite) TestDestroyOffersNoForceV1(c *gc.C) { 1299 s.assertDestroyOffersNoForce(c, s.api.OffersAPI) 1300 } 1301 1302 func (s *consumeSuite) TestDestroyOffersNoForceV2(c *gc.C) { 1303 s.assertDestroyOffersNoForce(c, s.api) 1304 } 1305 1306 type destroyOffers interface { 1307 DestroyOffers(args params.DestroyApplicationOffers) (params.ErrorResults, error) 1308 } 1309 1310 func (s *consumeSuite) assertDestroyOffersNoForce(c *gc.C, api destroyOffers) { 1311 s.setupOffer() 1312 st := s.mockStatePool.st[testing.ModelTag.Id()] 1313 st.(*mockState).users["foobar"] = &mockUser{"foobar"} 1314 st.(*mockState).connections = []applicationoffers.OfferConnection{ 1315 &mockOfferConnection{ 1316 username: "fred", 1317 modelUUID: testing.ModelTag.Id(), 1318 relationKey: "hosted-db2:db wordpress:db", 1319 relationId: 1, 1320 }, 1321 } 1322 1323 s.authorizer.Tag = names.NewUserTag("admin") 1324 results, err := s.api.DestroyOffers(params.DestroyApplicationOffers{ 1325 OfferURLs: []string{ 1326 "fred/prod.hosted-mysql"}, 1327 }) 1328 c.Assert(err, jc.ErrorIsNil) 1329 c.Assert(results.Results, gc.HasLen, 1) 1330 c.Assert(results.Results, jc.DeepEquals, []params.ErrorResult{ 1331 { 1332 Error: ¶ms.Error{Message: `offer has 1 relations`}, 1333 }, 1334 }) 1335 1336 urls := []string{"fred/prod.hosted-db2"} 1337 filter := params.OfferURLs{urls} 1338 found, err := s.api.ApplicationOffers(filter) 1339 c.Assert(err, jc.ErrorIsNil) 1340 c.Assert(found.Results, gc.HasLen, 1) 1341 c.Assert(found.Results[0].Error.Error(), gc.Matches, `application offer "fred/prod.hosted-db2" not found`) 1342 } 1343 1344 func (s *consumeSuite) TestDestroyOffersForce(c *gc.C) { 1345 s.setupOffer() 1346 st := s.mockStatePool.st[testing.ModelTag.Id()] 1347 st.(*mockState).users["foobar"] = &mockUser{"foobar"} 1348 st.(*mockState).connections = []applicationoffers.OfferConnection{ 1349 &mockOfferConnection{ 1350 username: "fred", 1351 modelUUID: testing.ModelTag.Id(), 1352 relationKey: "hosted-db2:db wordpress:db", 1353 relationId: 1, 1354 }, 1355 } 1356 1357 s.authorizer.Tag = names.NewUserTag("admin") 1358 results, err := s.api.DestroyOffers(params.DestroyApplicationOffers{ 1359 Force: true, 1360 OfferURLs: []string{ 1361 "fred/prod.hosted-mysql", "fred/prod.unknown", "garbage/badmodel.someoffer", "badmodel.someoffer"}, 1362 }) 1363 c.Assert(err, jc.ErrorIsNil) 1364 c.Assert(results.Results, gc.HasLen, 4) 1365 c.Assert(results.Results[0].Error, gc.IsNil) 1366 c.Assert(results.Results, jc.DeepEquals, []params.ErrorResult{ 1367 {}, 1368 { 1369 Error: ¶ms.Error{Message: `application offer "unknown" not found`, Code: "not found"}, 1370 }, { 1371 Error: ¶ms.Error{Message: `model "garbage/badmodel" not found`, Code: "not found"}, 1372 }, { 1373 Error: ¶ms.Error{Message: `model "admin/badmodel" not found`, Code: "not found"}, 1374 }, 1375 }) 1376 1377 urls := []string{"fred/prod.hosted-db2"} 1378 filter := params.OfferURLs{urls} 1379 found, err := s.api.ApplicationOffers(filter) 1380 c.Assert(err, jc.ErrorIsNil) 1381 c.Assert(found.Results, gc.HasLen, 1) 1382 c.Assert(found.Results[0].Error.Error(), gc.Matches, `application offer "fred/prod.hosted-db2" not found`) 1383 } 1384 1385 func (s *consumeSuite) TestDestroyOffersPermission(c *gc.C) { 1386 s.setupOffer() 1387 s.authorizer.Tag = names.NewUserTag("mary") 1388 st := s.mockStatePool.st[testing.ModelTag.Id()] 1389 st.(*mockState).users["foobar"] = &mockUser{"foobar"} 1390 1391 results, err := s.api.DestroyOffers(params.DestroyApplicationOffers{ 1392 OfferURLs: []string{"fred/prod.hosted-mysql"}, 1393 }) 1394 c.Assert(err, jc.ErrorIsNil) 1395 c.Assert(results.Results, gc.HasLen, 1) 1396 c.Assert(results.Results[0].Error, gc.ErrorMatches, common.ErrPerm.Error()) 1397 }