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