github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/state/applicationoffers_test.go (about) 1 // Copyright 2015 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package state_test 5 6 import ( 7 "github.com/juju/charm/v12" 8 "github.com/juju/errors" 9 "github.com/juju/names/v5" 10 jc "github.com/juju/testing/checkers" 11 jujutxn "github.com/juju/txn/v3" 12 gc "gopkg.in/check.v1" 13 14 "github.com/juju/juju/core/crossmodel" 15 "github.com/juju/juju/core/permission" 16 "github.com/juju/juju/core/status" 17 "github.com/juju/juju/state" 18 statetesting "github.com/juju/juju/state/testing" 19 "github.com/juju/juju/testing" 20 "github.com/juju/juju/testing/factory" 21 ) 22 23 type applicationOffersSuite struct { 24 ConnSuite 25 mysql *state.Application 26 } 27 28 var _ = gc.Suite(&applicationOffersSuite{}) 29 30 func (s *applicationOffersSuite) SetUpTest(c *gc.C) { 31 s.ConnSuite.SetUpTest(c) 32 ch := s.AddTestingCharm(c, "mysql") 33 s.mysql = s.AddTestingApplication(c, "mysql", ch) 34 } 35 36 func (s *applicationOffersSuite) createDefaultOffer(c *gc.C) crossmodel.ApplicationOffer { 37 eps := map[string]string{"db": "server", "db-admin": "server-admin"} 38 sd := state.NewApplicationOffers(s.State) 39 owner := s.Factory.MakeUser(c, nil) 40 offerArgs := crossmodel.AddApplicationOfferArgs{ 41 OfferName: "hosted-mysql", 42 ApplicationName: "mysql", 43 ApplicationDescription: "mysql is a db server", 44 Endpoints: eps, 45 Owner: owner.Name(), 46 } 47 offer, err := sd.AddOffer(offerArgs) 48 c.Assert(err, jc.ErrorIsNil) 49 return *offer 50 } 51 52 func (s *applicationOffersSuite) TestDetectingOfferConnections(c *gc.C) { 53 connected, err := state.ApplicationHasConnectedOffers(s.State, "mysql") 54 c.Assert(err, jc.ErrorIsNil) 55 c.Check(connected, jc.IsFalse) 56 57 offer := s.createDefaultOffer(c) 58 connected, err = state.ApplicationHasConnectedOffers(s.State, "mysql") 59 c.Assert(err, jc.ErrorIsNil) 60 c.Check(connected, jc.IsFalse) 61 62 s.addOfferConnection(c, offer.OfferUUID) 63 connected, err = state.ApplicationHasConnectedOffers(s.State, "mysql") 64 c.Assert(err, jc.ErrorIsNil) 65 c.Check(connected, jc.IsTrue) 66 } 67 68 func (s *applicationOffersSuite) TestEndpoints(c *gc.C) { 69 offer := s.createDefaultOffer(c) 70 _, err := state.ApplicationOfferEndpoint(offer, "foo") 71 c.Assert(err, gc.ErrorMatches, `relation "foo" on application offer "mysql" not found`) 72 73 serverEP, err := state.ApplicationOfferEndpoint(offer, "server") 74 c.Assert(err, jc.ErrorIsNil) 75 c.Assert(serverEP, gc.DeepEquals, state.Endpoint{ 76 ApplicationName: "mysql", 77 Relation: charm.Relation{ 78 Interface: "mysql", 79 Name: "server", 80 Role: charm.RoleProvider, 81 Scope: charm.ScopeGlobal, 82 }, 83 }) 84 } 85 86 func (s *applicationOffersSuite) TestRemove(c *gc.C) { 87 offer := s.createDefaultOffer(c) 88 r := s.State.RemoteEntities() 89 _, err := r.ExportLocalEntity(names.NewApplicationTag(offer.OfferName)) 90 c.Assert(err, jc.ErrorIsNil) 91 92 sd := state.NewApplicationOffers(s.State) 93 err = sd.Remove(offer.OfferName, false) 94 c.Assert(err, jc.ErrorIsNil) 95 _, err = sd.ApplicationOffer(offer.OfferName) 96 c.Assert(err, jc.Satisfies, errors.IsNotFound) 97 98 _, err = r.GetToken(names.NewApplicationTag(offer.OfferName)) 99 c.Assert(err, jc.Satisfies, errors.IsNotFound) 100 101 userPerms, err := s.State.GetOfferUsers(offer.OfferUUID) 102 c.Assert(err, jc.ErrorIsNil) 103 c.Assert(userPerms, gc.HasLen, 0) 104 } 105 106 func (s *applicationOffersSuite) TestAddApplicationOffer(c *gc.C) { 107 eps := map[string]string{"db": "server", "db-admin": "server-admin"} 108 sd := state.NewApplicationOffers(s.State) 109 owner := s.Factory.MakeUser(c, nil) 110 args := crossmodel.AddApplicationOfferArgs{ 111 OfferName: "hosted-mysql", 112 ApplicationName: "mysql", 113 ApplicationDescription: "mysql is a db server", 114 Endpoints: eps, 115 Owner: owner.Name(), 116 HasRead: []string{"everyone@external"}, 117 } 118 offer, err := sd.AddOffer(args) 119 c.Assert(err, jc.ErrorIsNil) 120 expectedOffer, err := sd.ApplicationOffer(offer.OfferName) 121 c.Assert(err, jc.ErrorIsNil) 122 c.Assert(*offer, jc.DeepEquals, *expectedOffer) 123 124 access, err := s.State.GetOfferAccess(offer.OfferUUID, owner.UserTag()) 125 c.Assert(err, jc.ErrorIsNil) 126 c.Assert(access, gc.Equals, permission.AdminAccess) 127 128 access, err = s.State.GetOfferAccess(offer.OfferUUID, names.NewUserTag("everyone@external")) 129 c.Assert(err, jc.ErrorIsNil) 130 c.Assert(access, gc.Equals, permission.ReadAccess) 131 } 132 133 func (s *applicationOffersSuite) TestAddApplicationOfferInvalidApplication(c *gc.C) { 134 sd := state.NewApplicationOffers(s.State) 135 owner := s.Factory.MakeUser(c, nil) 136 args := crossmodel.AddApplicationOfferArgs{ 137 OfferName: "hosted-mysql", 138 ApplicationName: "invalid", 139 ApplicationDescription: "mysql is a db server", 140 Endpoints: map[string]string{"db": "server", "db-admin": "server-admin"}, 141 Owner: owner.Name(), 142 HasRead: []string{"everyone@external"}, 143 } 144 _, err := sd.AddOffer(args) 145 c.Assert(err, gc.ErrorMatches, `cannot add application offer "hosted-mysql": application "invalid" not found`) 146 } 147 148 func (s *applicationOffersSuite) TestAddApplicationOfferBadEndpoints(c *gc.C) { 149 eps := map[string]string{"db": "server", "db-admin": "admin"} 150 sd := state.NewApplicationOffers(s.State) 151 owner := s.Factory.MakeUser(c, nil) 152 args := crossmodel.AddApplicationOfferArgs{ 153 OfferName: "hosted-mysql", 154 ApplicationName: "mysql", 155 ApplicationDescription: "mysql is a db server", 156 Endpoints: eps, 157 Owner: owner.Name(), 158 } 159 _, err := sd.AddOffer(args) 160 c.Assert(err, gc.ErrorMatches, `.*application "mysql" has no "admin" relation`) 161 162 // Fix the endpoints and try again. 163 // There was a bug where this failed so we test it. 164 eps = map[string]string{"db": "server", "db-admin": "server-admin"} 165 args.Endpoints = eps 166 _, err = sd.AddOffer(args) 167 c.Assert(err, jc.ErrorIsNil) 168 } 169 170 func (s *applicationOffersSuite) TestFaillAddApplicationOfferNonGlobalEndpoint(c *gc.C) { 171 s.AddTestingApplication(c, "local-wordpress", s.AddTestingCharm(c, "wordpress")) 172 // logging-dir is a container scoped relation. 173 eps := map[string]string{"logging-dir": "logging-dir"} 174 sd := state.NewApplicationOffers(s.State) 175 owner := s.Factory.MakeUser(c, nil) 176 args := crossmodel.AddApplicationOfferArgs{ 177 OfferName: "offer-name", 178 ApplicationName: "local-wordpress", 179 Endpoints: eps, 180 Owner: owner.Name(), 181 } 182 _, err := sd.AddOffer(args) 183 c.Assert(err, gc.ErrorMatches, `.*can only offer endpoints with global scope, provided scope "container".*`) 184 } 185 186 func (s *applicationOffersSuite) TestListOffersNone(c *gc.C) { 187 sd := state.NewApplicationOffers(s.State) 188 offers, err := sd.ListOffers() 189 c.Assert(err, jc.ErrorIsNil) 190 c.Assert(len(offers), gc.Equals, 0) 191 } 192 193 func (s *applicationOffersSuite) createOffer(c *gc.C, name, description string) (crossmodel.ApplicationOffer, string) { 194 eps := map[string]string{ 195 "db": "server", 196 } 197 sd := state.NewApplicationOffers(s.State) 198 owner := s.Factory.MakeUser(c, nil) 199 offerArgs := crossmodel.AddApplicationOfferArgs{ 200 OfferName: name, 201 ApplicationName: "mysql", 202 ApplicationDescription: description, 203 Endpoints: eps, 204 Owner: owner.Name(), 205 } 206 offer, err := sd.AddOffer(offerArgs) 207 c.Assert(err, jc.ErrorIsNil) 208 return *offer, owner.Name() 209 } 210 211 func (s *applicationOffersSuite) TestApplicationOffer(c *gc.C) { 212 sd := state.NewApplicationOffers(s.State) 213 expectedOffer := s.createDefaultOffer(c) 214 offer, err := sd.ApplicationOffer("hosted-mysql") 215 c.Assert(err, jc.ErrorIsNil) 216 c.Assert(*offer, jc.DeepEquals, expectedOffer) 217 } 218 219 func (s *applicationOffersSuite) TestApplicationOfferForUUID(c *gc.C) { 220 sd := state.NewApplicationOffers(s.State) 221 expectedOffer := s.createDefaultOffer(c) 222 offer, err := sd.ApplicationOfferForUUID(expectedOffer.OfferUUID) 223 c.Assert(err, jc.ErrorIsNil) 224 c.Assert(*offer, jc.DeepEquals, expectedOffer) 225 } 226 227 func (s *applicationOffersSuite) TestAllApplicationOffers(c *gc.C) { 228 eps := map[string]string{"db": "server", "db-admin": "server-admin"} 229 sd := state.NewApplicationOffers(s.State) 230 owner := s.Factory.MakeUser(c, nil) 231 anOffer := s.createDefaultOffer(c) 232 args := crossmodel.AddApplicationOfferArgs{ 233 OfferName: "another-mysql", 234 ApplicationName: "mysql", 235 ApplicationDescription: "mysql is a db server", 236 Endpoints: eps, 237 Owner: owner.Name(), 238 HasRead: []string{"everyone@external"}, 239 } 240 anotherOffer, err := sd.AddOffer(args) 241 c.Assert(err, jc.ErrorIsNil) 242 243 offers, err := sd.AllApplicationOffers() 244 c.Assert(err, jc.ErrorIsNil) 245 // Ensure ordering doesn't matter. 246 offersMap := make(map[string]*crossmodel.ApplicationOffer) 247 for _, offer := range offers { 248 offersMap[offer.OfferName] = offer 249 } 250 c.Assert(offersMap, jc.DeepEquals, map[string]*crossmodel.ApplicationOffer{ 251 anOffer.OfferName: &anOffer, 252 anotherOffer.OfferName: anotherOffer, 253 }) 254 } 255 256 func (s *applicationOffersSuite) TestListOffersAll(c *gc.C) { 257 sd := state.NewApplicationOffers(s.State) 258 offer := s.createDefaultOffer(c) 259 offers, err := sd.ListOffers() 260 c.Assert(err, jc.ErrorIsNil) 261 c.Assert(len(offers), gc.Equals, 1) 262 c.Assert(offers[0], jc.DeepEquals, offer) 263 } 264 265 func (s *applicationOffersSuite) TestListOffersOneFilter(c *gc.C) { 266 sd := state.NewApplicationOffers(s.State) 267 offer, _ := s.createOffer(c, "offer1", "description for offer1") 268 s.createOffer(c, "offer2", "description for offer2") 269 s.createOffer(c, "offer3", "description for offer3") 270 offers, err := sd.ListOffers(crossmodel.ApplicationOfferFilter{ 271 OfferName: "offer1", 272 ApplicationName: "mysql", 273 Endpoints: []crossmodel.EndpointFilterTerm{{ 274 Interface: "mysql", 275 }}, 276 }) 277 c.Assert(err, jc.ErrorIsNil) 278 c.Assert(len(offers), gc.Equals, 1) 279 c.Assert(offers[0], jc.DeepEquals, offer) 280 } 281 282 func (s *applicationOffersSuite) TestListOffersExact(c *gc.C) { 283 sd := state.NewApplicationOffers(s.State) 284 offer, _ := s.createOffer(c, "offer1", "description for offer1") 285 s.createOffer(c, "offer2", "description for offer2") 286 s.createOffer(c, "offer3", "description for offer3") 287 offers, err := sd.ListOffers(crossmodel.ApplicationOfferFilter{ 288 OfferName: "^offer1$", 289 }) 290 c.Assert(err, jc.ErrorIsNil) 291 c.Assert(len(offers), gc.Equals, 1) 292 c.Assert(offers[0], jc.DeepEquals, offer) 293 offers, err = sd.ListOffers(crossmodel.ApplicationOfferFilter{ 294 OfferName: "^offer$", 295 }) 296 c.Assert(err, jc.ErrorIsNil) 297 c.Assert(len(offers), gc.Equals, 0) 298 } 299 300 func (s *applicationOffersSuite) TestListOffersFilterExcludes(c *gc.C) { 301 sd := state.NewApplicationOffers(s.State) 302 s.createOffer(c, "offer1", "description for offer1") 303 offers, err := sd.ListOffers(crossmodel.ApplicationOfferFilter{ 304 Endpoints: []crossmodel.EndpointFilterTerm{{ 305 Interface: "db2", 306 }}, 307 }) 308 c.Assert(err, jc.ErrorIsNil) 309 c.Assert(len(offers), gc.Equals, 0) 310 } 311 312 func (s *applicationOffersSuite) TestListOffersManyFilters(c *gc.C) { 313 sd := state.NewApplicationOffers(s.State) 314 offer, _ := s.createOffer(c, "offer1", "description for offer1") 315 offer2, _ := s.createOffer(c, "offer2", "description for offer2") 316 s.createOffer(c, "offer3", "description for offer3") 317 offers, err := sd.ListOffers( 318 crossmodel.ApplicationOfferFilter{ 319 OfferName: "offer1", 320 ApplicationName: "mysql", 321 }, 322 crossmodel.ApplicationOfferFilter{ 323 OfferName: "offer2", 324 ApplicationDescription: "offer2", 325 }, 326 ) 327 c.Assert(err, jc.ErrorIsNil) 328 c.Assert(len(offers), gc.Equals, 2) 329 c.Assert(offers, jc.DeepEquals, []crossmodel.ApplicationOffer{offer, offer2}) 330 } 331 332 func (s *applicationOffersSuite) TestListOffersFilterDescriptionRegexp(c *gc.C) { 333 sd := state.NewApplicationOffers(s.State) 334 s.createOffer(c, "offer1", "description for offer1") 335 offer, _ := s.createOffer(c, "offer2", "description for offer2") 336 s.createOffer(c, "offer3", "description for offer3") 337 offers, err := sd.ListOffers(crossmodel.ApplicationOfferFilter{ 338 ApplicationDescription: "for offer2", 339 }) 340 c.Assert(err, jc.ErrorIsNil) 341 c.Assert(len(offers), gc.Equals, 1) 342 c.Assert(offers[0], jc.DeepEquals, offer) 343 } 344 345 func (s *applicationOffersSuite) TestListOffersFilterOfferNameRegexp(c *gc.C) { 346 sd := state.NewApplicationOffers(s.State) 347 offer, _ := s.createOffer(c, "hosted-offer1", "description for offer1") 348 s.createOffer(c, "offer2", "description for offer2") 349 s.createOffer(c, "offer3", "description for offer3") 350 offers, err := sd.ListOffers(crossmodel.ApplicationOfferFilter{ 351 OfferName: "offer1", 352 }) 353 c.Assert(err, jc.ErrorIsNil) 354 c.Assert(len(offers), gc.Equals, 1) 355 c.Assert(offers[0], jc.DeepEquals, offer) 356 } 357 358 func (s *applicationOffersSuite) TestListOffersAllowedConsumersOwner(c *gc.C) { 359 sd := state.NewApplicationOffers(s.State) 360 offer, owner := s.createOffer(c, "offer1", "description for offer1") 361 s.createOffer(c, "offer2", "description for offer2") 362 s.createOffer(c, "offer3", "description for offer3") 363 offers, err := sd.ListOffers(crossmodel.ApplicationOfferFilter{ 364 AllowedConsumers: []string{owner, "mary"}, 365 }) 366 c.Assert(err, jc.ErrorIsNil) 367 c.Assert(len(offers), gc.Equals, 1) 368 c.Assert(offers[0], jc.DeepEquals, offer) 369 } 370 371 func (s *applicationOffersSuite) TestListOffersAllowedConsumers(c *gc.C) { 372 sd := state.NewApplicationOffers(s.State) 373 offer, _ := s.createOffer(c, "offer1", "description for offer1") 374 offer2, _ := s.createOffer(c, "offer2", "description for offer2") 375 s.createOffer(c, "offer3", "description for offer3") 376 s.Factory.MakeUser(c, &factory.UserParams{Name: "mary"}) 377 378 mary := names.NewUserTag("mary") 379 err := s.State.CreateOfferAccess( 380 names.NewApplicationOfferTag(offer.OfferUUID), mary, permission.ConsumeAccess) 381 c.Assert(err, jc.ErrorIsNil) 382 err = s.State.CreateOfferAccess( 383 names.NewApplicationOfferTag(offer2.OfferUUID), mary, permission.ReadAccess) 384 c.Assert(err, jc.ErrorIsNil) 385 offers, err := sd.ListOffers(crossmodel.ApplicationOfferFilter{ 386 AllowedConsumers: []string{"mary"}, 387 }) 388 c.Assert(err, jc.ErrorIsNil) 389 c.Assert(len(offers), gc.Equals, 1) 390 c.Assert(offers[0], jc.DeepEquals, offer) 391 } 392 393 func (s *applicationOffersSuite) TestListOffersConnectedUsers(c *gc.C) { 394 sd := state.NewApplicationOffers(s.State) 395 offer, _ := s.createOffer(c, "offer1", "description for offer1") 396 s.createOffer(c, "offer2", "description for offer2") 397 s.createOffer(c, "offer3", "description for offer3") 398 s.Factory.MakeUser(c, &factory.UserParams{Name: "mary"}) 399 400 _, err := s.State.AddOfferConnection(state.AddOfferConnectionParams{ 401 SourceModelUUID: testing.ModelTag.Id(), 402 Username: "mary", 403 OfferUUID: offer.OfferUUID, 404 }) 405 c.Assert(err, jc.ErrorIsNil) 406 offers, err := sd.ListOffers(crossmodel.ApplicationOfferFilter{ 407 ConnectedUsers: []string{"mary"}, 408 }) 409 c.Assert(err, jc.ErrorIsNil) 410 c.Assert(len(offers), gc.Equals, 1) 411 c.Assert(offers[0], jc.DeepEquals, offer) 412 } 413 414 func (s *applicationOffersSuite) TestAddApplicationOfferDuplicate(c *gc.C) { 415 sd := state.NewApplicationOffers(s.State) 416 owner := s.Factory.MakeUser(c, nil) 417 _, err := sd.AddOffer(crossmodel.AddApplicationOfferArgs{ 418 OfferName: "hosted-mysql", 419 ApplicationName: "mysql", 420 Owner: owner.Name(), 421 }) 422 c.Assert(err, jc.ErrorIsNil) 423 _, err = sd.AddOffer(crossmodel.AddApplicationOfferArgs{ 424 OfferName: "hosted-mysql", 425 ApplicationName: "mysql", 426 Owner: owner.Name(), 427 }) 428 c.Assert(err, gc.ErrorMatches, `cannot add application offer "hosted-mysql": application offer already exists`) 429 } 430 431 func (s *applicationOffersSuite) TestAddApplicationOfferDuplicateAddedAfterInitial(c *gc.C) { 432 // Check that a record with a URL conflict cannot be added if 433 // there is no conflict initially but a record is added 434 // before the transaction is run. 435 sd := state.NewApplicationOffers(s.State) 436 owner := s.Factory.MakeUser(c, nil) 437 defer state.SetBeforeHooks(c, s.State, func() { 438 _, err := sd.AddOffer(crossmodel.AddApplicationOfferArgs{ 439 OfferName: "hosted-mysql", 440 ApplicationName: "mysql", 441 Owner: owner.Name(), 442 }) 443 c.Assert(err, jc.ErrorIsNil) 444 }).Check() 445 _, err := sd.AddOffer(crossmodel.AddApplicationOfferArgs{ 446 OfferName: "hosted-mysql", 447 ApplicationName: "mysql", 448 Owner: owner.Name(), 449 }) 450 c.Assert(err, gc.ErrorMatches, `cannot add application offer "hosted-mysql": application offer already exists`) 451 } 452 453 func (s *applicationOffersSuite) TestUpdateApplicationOffer(c *gc.C) { 454 sd := state.NewApplicationOffers(s.State) 455 owner := s.Factory.MakeUser(c, nil) 456 original, err := sd.AddOffer(crossmodel.AddApplicationOfferArgs{ 457 OfferName: "hosted-mysql", 458 ApplicationName: "mysql", 459 Owner: owner.Name(), 460 }) 461 c.Assert(err, jc.ErrorIsNil) 462 offer, err := sd.UpdateOffer(crossmodel.AddApplicationOfferArgs{ 463 OfferName: "hosted-mysql", 464 ApplicationName: "mysql", 465 ApplicationDescription: "a better database", 466 Owner: owner.Name(), 467 }) 468 c.Assert(err, jc.ErrorIsNil) 469 c.Assert(offer, jc.DeepEquals, &crossmodel.ApplicationOffer{ 470 OfferName: "hosted-mysql", 471 OfferUUID: original.OfferUUID, 472 ApplicationName: "mysql", 473 ApplicationDescription: "a better database", 474 Endpoints: map[string]charm.Relation{}, 475 }) 476 assertOffersRef(c, s.State, "mysql", 1) 477 } 478 479 func (s *applicationOffersSuite) TestUpdateApplicationOfferDifferentApp(c *gc.C) { 480 sd := state.NewApplicationOffers(s.State) 481 owner := s.Factory.MakeUser(c, nil) 482 original, err := sd.AddOffer(crossmodel.AddApplicationOfferArgs{ 483 OfferName: "hosted-mysql", 484 ApplicationName: "mysql", 485 Owner: owner.Name(), 486 }) 487 c.Assert(err, jc.ErrorIsNil) 488 s.Factory.MakeApplication(c, &factory.ApplicationParams{Name: "foo"}) 489 offer, err := sd.UpdateOffer(crossmodel.AddApplicationOfferArgs{ 490 OfferName: "hosted-mysql", 491 ApplicationName: "foo", 492 Owner: owner.Name(), 493 }) 494 c.Assert(err, jc.ErrorIsNil) 495 c.Assert(offer, jc.DeepEquals, &crossmodel.ApplicationOffer{ 496 OfferName: "hosted-mysql", 497 OfferUUID: original.OfferUUID, 498 ApplicationName: "foo", 499 Endpoints: map[string]charm.Relation{}, 500 }) 501 assertNoOffersRef(c, s.State, "mysql") 502 assertOffersRef(c, s.State, "foo", 1) 503 } 504 505 func (s *applicationOffersSuite) TestUpdateApplicationOfferNotFound(c *gc.C) { 506 sd := state.NewApplicationOffers(s.State) 507 owner := s.Factory.MakeUser(c, nil) 508 _, err := sd.UpdateOffer(crossmodel.AddApplicationOfferArgs{ 509 OfferName: "hosted-mysql", 510 ApplicationName: "mysql", 511 Owner: owner.Name(), 512 }) 513 c.Assert(err, gc.ErrorMatches, `cannot update application offer "hosted-mysql": offer "hosted-mysql" not found`) 514 } 515 516 func (s *applicationOffersSuite) TestUpdateApplicationOfferRemovedAfterInitial(c *gc.C) { 517 // Check that a record with a URL conflict cannot be added if 518 // there is no conflict initially but a record is added 519 // before the transaction is run. 520 sd := state.NewApplicationOffers(s.State) 521 owner := s.Factory.MakeUser(c, nil) 522 _, err := sd.AddOffer(crossmodel.AddApplicationOfferArgs{ 523 OfferName: "hosted-mysql", 524 ApplicationName: "mysql", 525 Owner: owner.Name(), 526 }) 527 c.Assert(err, jc.ErrorIsNil) 528 defer state.SetBeforeHooks(c, s.State, func() { 529 err := sd.Remove("hosted-mysql", false) 530 c.Assert(err, jc.ErrorIsNil) 531 }).Check() 532 _, err = sd.UpdateOffer(crossmodel.AddApplicationOfferArgs{ 533 OfferName: "hosted-mysql", 534 ApplicationName: "mysql", 535 Owner: owner.Name(), 536 }) 537 c.Assert(err, gc.ErrorMatches, `cannot update application offer "hosted-mysql": offer "hosted-mysql" not found`) 538 } 539 540 func (s *applicationOffersSuite) addOfferConnection(c *gc.C, offerUUID string) *state.RemoteApplication { 541 app, err := s.State.AddRemoteApplication(state.AddRemoteApplicationParams{ 542 Name: "wordpress", 543 SourceModel: testing.ModelTag, 544 IsConsumerProxy: true, 545 Endpoints: []charm.Relation{{ 546 Interface: "mysql", 547 Name: "server", 548 Role: charm.RoleRequirer, 549 Scope: charm.ScopeGlobal, 550 }}}) 551 c.Assert(err, jc.ErrorIsNil) 552 eps, err := s.State.InferEndpoints("wordpress", "mysql") 553 c.Assert(err, jc.ErrorIsNil) 554 rel, err := s.State.AddRelation(eps...) 555 c.Assert(err, jc.ErrorIsNil) 556 557 _, err = s.State.AddOfferConnection(state.AddOfferConnectionParams{ 558 OfferUUID: offerUUID, 559 RelationId: rel.Id(), 560 RelationKey: rel.Tag().Id(), 561 Username: "admin", 562 SourceModelUUID: testing.ModelTag.Id(), 563 }) 564 c.Assert(err, jc.ErrorIsNil) 565 566 return app 567 } 568 569 func (s *applicationOffersSuite) TestUpdateApplicationOfferRemovingEndpointsInUse(c *gc.C) { 570 owner := s.Factory.MakeUser(c, nil).Name() 571 sd := state.NewApplicationOffers(s.State) 572 offer, err := sd.AddOffer(crossmodel.AddApplicationOfferArgs{ 573 OfferName: "hosted-mysql", 574 ApplicationName: "mysql", 575 Owner: owner, 576 Endpoints: map[string]string{ 577 "server": "server", 578 "server-admin": "server-admin", 579 }, 580 }) 581 c.Assert(err, jc.ErrorIsNil) 582 583 _, err = s.State.AddOfferConnection(state.AddOfferConnectionParams{ 584 SourceModelUUID: testing.ModelTag.Id(), 585 RelationId: 1, 586 RelationKey: "remote:server mysql:server", 587 Username: "admin", 588 OfferUUID: offer.OfferUUID, 589 }) 590 c.Assert(err, jc.ErrorIsNil) 591 592 _, err = sd.UpdateOffer(crossmodel.AddApplicationOfferArgs{ 593 OfferName: "hosted-mysql", 594 ApplicationName: "mysql", 595 Owner: owner, 596 Endpoints: map[string]string{ 597 // We are attempting to remove the "server" endpoint 598 // from the offer which is currently connected to an 599 // active consumer 600 "server-admin": "server-admin", 601 }, 602 }) 603 c.Assert(err, gc.ErrorMatches, `cannot update application offer "hosted-mysql": application endpoint "server" has active consumers`) 604 } 605 606 func (s *applicationOffersSuite) TestUpdateApplicationOfferInvalidApplication(c *gc.C) { 607 owner := s.Factory.MakeUser(c, nil).Name() 608 sd := state.NewApplicationOffers(s.State) 609 610 originalOffer, err := sd.AddOffer(crossmodel.AddApplicationOfferArgs{ 611 OfferName: "myoffer", 612 Owner: owner, 613 ApplicationName: "mysql", 614 Endpoints: map[string]string{ 615 "db": "server", 616 }, 617 }) 618 c.Assert(err, jc.ErrorIsNil) 619 c.Assert(originalOffer, jc.DeepEquals, &crossmodel.ApplicationOffer{ 620 OfferName: "myoffer", 621 OfferUUID: originalOffer.OfferUUID, 622 ApplicationName: "mysql", 623 Endpoints: map[string]charm.Relation{ 624 "db": { 625 Name: "server", 626 Role: "provider", 627 Interface: "mysql", 628 Scope: "global", 629 }, 630 }, 631 }) 632 633 _, err = sd.UpdateOffer(crossmodel.AddApplicationOfferArgs{ 634 OfferName: "myoffer", 635 Owner: owner, 636 ApplicationName: "invalid", 637 Endpoints: map[string]string{ 638 "invalid-endpoint": "invalid-endpoint", 639 }, 640 }) 641 c.Assert(err, gc.ErrorMatches, `cannot update application offer "myoffer": application "invalid" not found`) 642 643 newOffer, err := sd.ApplicationOffer("myoffer") 644 c.Assert(err, jc.ErrorIsNil) 645 c.Check(newOffer, jc.DeepEquals, originalOffer) 646 } 647 648 // regression test for https://bugs.launchpad.net/juju/+bug/1954830 649 func (s *applicationOffersSuite) TestUpdateApplicationOfferInvalidEndpoint(c *gc.C) { 650 owner := s.Factory.MakeUser(c, nil).Name() 651 sd := state.NewApplicationOffers(s.State) 652 653 originalOffer, err := sd.AddOffer(crossmodel.AddApplicationOfferArgs{ 654 OfferName: "myoffer", 655 Owner: owner, 656 ApplicationName: "mysql", 657 Endpoints: map[string]string{ 658 "db": "server", 659 }, 660 }) 661 c.Assert(err, jc.ErrorIsNil) 662 c.Assert(originalOffer, jc.DeepEquals, &crossmodel.ApplicationOffer{ 663 OfferName: "myoffer", 664 OfferUUID: originalOffer.OfferUUID, 665 ApplicationName: "mysql", 666 Endpoints: map[string]charm.Relation{ 667 "db": { 668 Name: "server", 669 Role: "provider", 670 Interface: "mysql", 671 Scope: "global", 672 }, 673 }, 674 }) 675 676 _, err = sd.UpdateOffer(crossmodel.AddApplicationOfferArgs{ 677 OfferName: "myoffer", 678 Owner: owner, 679 ApplicationName: "mysql", 680 Endpoints: map[string]string{ 681 "invalid-endpoint": "invalid-endpoint", 682 }, 683 }) 684 c.Assert(err, gc.ErrorMatches, `cannot update application offer "myoffer": getting relation endpoint for relation "invalid-endpoint" and application "mysql": application "mysql" has no "invalid-endpoint" relation`) 685 686 newOffer, err := sd.ApplicationOffer("myoffer") 687 c.Assert(err, jc.ErrorIsNil) 688 c.Check(newOffer, jc.DeepEquals, originalOffer) 689 } 690 691 func (s *applicationOffersSuite) TestRemoveOffersSucceedsWithZeroConnections(c *gc.C) { 692 s.createDefaultOffer(c) 693 ao := state.NewApplicationOffers(s.State) 694 err := ao.Remove("hosted-mysql", false) 695 c.Assert(err, jc.ErrorIsNil) 696 _, err = ao.ApplicationOffer("hosted-mysql") 697 c.Assert(err, jc.Satisfies, errors.IsNotFound) 698 699 err = s.mysql.Refresh() 700 c.Assert(err, jc.ErrorIsNil) 701 assertNoOffersRef(c, s.State, "mysql") 702 } 703 704 func (s *applicationOffersSuite) TestRemoveApplicationSucceedsWithZeroConnections(c *gc.C) { 705 s.createDefaultOffer(c) 706 707 err := s.mysql.Destroy() 708 c.Assert(err, jc.ErrorIsNil) 709 err = s.mysql.Refresh() 710 c.Assert(err, jc.Satisfies, errors.IsNotFound) 711 assertNoOffersRef(c, s.State, "mysql") 712 } 713 714 func (s *applicationOffersSuite) TestRemoveApplicationSucceedsWithZeroConnectionsRace(c *gc.C) { 715 addOffer := func() { 716 s.createDefaultOffer(c) 717 } 718 defer state.SetBeforeHooks(c, s.State, addOffer).Check() 719 err := s.mysql.Destroy() 720 c.Assert(err, jc.ErrorIsNil) 721 err = s.mysql.Refresh() 722 c.Assert(err, jc.Satisfies, errors.IsNotFound) 723 assertNoOffersRef(c, s.State, "mysql") 724 } 725 726 func (s *applicationOffersSuite) TestRemoveApplicationFailsWithOfferWithConnections(c *gc.C) { 727 offer := s.createDefaultOffer(c) 728 s.addOfferConnection(c, offer.OfferUUID) 729 730 err := s.mysql.Destroy() 731 c.Assert(err, gc.ErrorMatches, `cannot destroy application "mysql": application is used by 1 consumer`) 732 err = s.mysql.Refresh() 733 c.Assert(err, jc.ErrorIsNil) 734 assertOffersRef(c, s.State, "mysql", 1) 735 } 736 737 func (s *applicationOffersSuite) TestRemoveApplicationFailsWithOfferWithConnectionsRace(c *gc.C) { 738 addConnectedOffer := func() { 739 offer := s.createDefaultOffer(c) 740 s.addOfferConnection(c, offer.OfferUUID) 741 } 742 defer state.SetBeforeHooks(c, s.State, addConnectedOffer).Check() 743 err := s.mysql.Destroy() 744 c.Assert(err, gc.ErrorMatches, `cannot destroy application "mysql": application is used by 1 consumer`) 745 err = s.mysql.Refresh() 746 c.Assert(err, jc.ErrorIsNil) 747 assertOffersRef(c, s.State, "mysql", 1) 748 } 749 750 func (s *applicationOffersSuite) TestRemoveOffersFailsWithConnections(c *gc.C) { 751 offer := s.createDefaultOffer(c) 752 s.addOfferConnection(c, offer.OfferUUID) 753 ao := state.NewApplicationOffers(s.State) 754 err := ao.Remove("hosted-mysql", false) 755 c.Assert(err, gc.ErrorMatches, `cannot delete application offer "hosted-mysql": offer has 1 relation`) 756 } 757 758 func (s *applicationOffersSuite) TestRemoveOffersFailsWithConnectionsRace(c *gc.C) { 759 offer := s.createDefaultOffer(c) 760 ao := state.NewApplicationOffers(s.State) 761 addOfferConnection := func() { 762 c.Logf("adding connection to %s", offer.OfferUUID) 763 s.addOfferConnection(c, offer.OfferUUID) 764 } 765 defer state.SetBeforeHooks(c, s.State, addOfferConnection).Check() 766 767 err := ao.Remove("hosted-mysql", false) 768 c.Assert(err, gc.ErrorMatches, `cannot delete application offer "hosted-mysql": offer has 1 relation`) 769 } 770 771 func (s *applicationOffersSuite) TestRemoveOffersSucceedsWhenLocalRelationAdded(c *gc.C) { 772 offer := s.createDefaultOffer(c) 773 s.AddTestingApplication(c, "local-wordpress", s.AddTestingCharm(c, "wordpress")) 774 _, err := s.State.Application(offer.ApplicationName) 775 c.Assert(err, jc.ErrorIsNil) 776 eps, err := s.State.InferEndpoints("local-wordpress", "mysql") 777 c.Assert(err, jc.ErrorIsNil) 778 _, err = s.State.AddRelation(eps...) 779 c.Assert(err, jc.ErrorIsNil) 780 ao := state.NewApplicationOffers(s.State) 781 782 err = ao.Remove(offer.OfferName, false) 783 c.Assert(err, jc.ErrorIsNil) 784 _, err = ao.ApplicationOffer("hosted-mysql") 785 c.Assert(err, jc.Satisfies, errors.IsNotFound) 786 } 787 788 func (s *applicationOffersSuite) assertInScope(c *gc.C, relUnit *state.RelationUnit, inScope bool) { 789 ok, err := relUnit.InScope() 790 c.Assert(err, jc.ErrorIsNil) 791 c.Assert(ok, gc.Equals, inScope) 792 } 793 794 func (s *applicationOffersSuite) TestRemoveOffersWithConnectionsForce(c *gc.C) { 795 offer := s.createDefaultOffer(c) 796 rwordpress, err := s.State.AddRemoteApplication(state.AddRemoteApplicationParams{ 797 Name: "remote-wordpress", 798 SourceModel: names.NewModelTag("source-model"), 799 IsConsumerProxy: true, 800 Endpoints: []charm.Relation{{ 801 Interface: "mysql", 802 Limit: 1, 803 Name: "db", 804 Role: charm.RoleRequirer, 805 Scope: charm.ScopeGlobal, 806 }}, 807 }) 808 c.Assert(err, jc.ErrorIsNil) 809 wordpressEP, err := rwordpress.Endpoint("db") 810 c.Assert(err, jc.ErrorIsNil) 811 812 mysql, err := s.State.Application("mysql") 813 c.Assert(err, jc.ErrorIsNil) 814 mysqlUnit, err := mysql.AddUnit(state.AddUnitParams{}) 815 c.Assert(err, jc.ErrorIsNil) 816 mysqlEP, err := mysql.Endpoint("server") 817 c.Assert(err, jc.ErrorIsNil) 818 819 rel, err := s.State.AddRelation(wordpressEP, mysqlEP) 820 c.Assert(err, jc.ErrorIsNil) 821 mysqlru, err := rel.Unit(mysqlUnit) 822 c.Assert(err, jc.ErrorIsNil) 823 err = mysqlru.EnterScope(nil) 824 c.Assert(err, jc.ErrorIsNil) 825 s.assertInScope(c, mysqlru, true) 826 827 wpru, err := rel.RemoteUnit("remote-wordpress/0") 828 c.Assert(err, jc.ErrorIsNil) 829 err = wpru.EnterScope(nil) 830 c.Assert(err, jc.ErrorIsNil) 831 s.assertInScope(c, wpru, true) 832 833 _, err = s.State.AddOfferConnection(state.AddOfferConnectionParams{ 834 OfferUUID: offer.OfferUUID, 835 RelationId: rel.Id(), 836 RelationKey: rel.Tag().Id(), 837 Username: "admin", 838 SourceModelUUID: testing.ModelTag.Id(), 839 }) 840 c.Assert(err, jc.ErrorIsNil) 841 842 ao := state.NewApplicationOffers(s.State) 843 844 err = ao.Remove("hosted-mysql", true) 845 c.Assert(err, jc.ErrorIsNil) 846 _, err = ao.ApplicationOffer("hosted-mysql") 847 c.Assert(err, jc.Satisfies, errors.IsNotFound) 848 conn, err := s.State.OfferConnections(offer.OfferUUID) 849 c.Assert(err, jc.ErrorIsNil) 850 c.Assert(conn, gc.HasLen, 1) 851 offerRel, err := s.State.Relation(conn[0].RelationId()) 852 c.Assert(err, jc.ErrorIsNil) 853 c.Assert(offerRel.Life(), gc.Equals, state.Dying) 854 s.assertInScope(c, wpru, false) 855 s.assertInScope(c, mysqlru, true) 856 err = rwordpress.Refresh() 857 c.Assert(err, jc.Satisfies, errors.IsNotFound) 858 } 859 860 func (s *applicationOffersSuite) TestRemoveOneOfferSameApplication(c *gc.C) { 861 offer, owner := s.createOffer(c, "hosted-mysql", "offer one") 862 sd := state.NewApplicationOffers(s.State) 863 offerArgs := crossmodel.AddApplicationOfferArgs{ 864 OfferName: "mysql-admin", 865 ApplicationName: "mysql", 866 ApplicationDescription: "mysql admin", 867 Endpoints: map[string]string{"db-admin": "server-admin"}, 868 Owner: owner, 869 } 870 offer2, err := sd.AddOffer(offerArgs) 871 c.Assert(err, jc.ErrorIsNil) 872 873 s.addOfferConnection(c, offer.OfferUUID) 874 ao := state.NewApplicationOffers(s.State) 875 876 err = ao.Remove(offer2.OfferName, false) 877 c.Assert(err, jc.ErrorIsNil) 878 _, err = ao.ApplicationOffer("mysql-admin") 879 c.Assert(err, jc.Satisfies, errors.IsNotFound) 880 881 // The other offer is unaffected. 882 appOffer, err := ao.ApplicationOffer("hosted-mysql") 883 c.Assert(err, jc.ErrorIsNil) 884 conn, err := s.State.OfferConnections(appOffer.OfferUUID) 885 c.Assert(err, jc.ErrorIsNil) 886 c.Assert(conn, gc.HasLen, 1) 887 } 888 889 func (s *applicationOffersSuite) TestRemovingApplicationFailsRace(c *gc.C) { 890 s.createDefaultOffer(c) 891 wp := s.AddTestingApplication(c, "local-wordpress", s.AddTestingCharm(c, "wordpress")) 892 eps, err := s.State.InferEndpoints(wp.Name(), s.mysql.Name()) 893 c.Assert(err, jc.ErrorIsNil) 894 895 addRelation := func() { 896 _, err := s.State.AddRelation(eps...) 897 c.Assert(err, jc.ErrorIsNil) 898 } 899 900 rmRelations := func() { 901 rels, err := s.State.AllRelations() 902 c.Assert(err, jc.ErrorIsNil) 903 904 for _, rel := range rels { 905 err = rel.Destroy() 906 c.Assert(err, jc.ErrorIsNil) 907 err = s.mysql.Refresh() 908 c.Assert(err, jc.ErrorIsNil) 909 } 910 } 911 912 state.SetMaxTxnAttempts(c, s.State, 3) 913 bumpTxnRevno := jujutxn.TestHook{Before: addRelation, After: rmRelations} 914 defer state.SetTestHooks(c, s.State, bumpTxnRevno, bumpTxnRevno, bumpTxnRevno).Check() 915 916 err = s.mysql.Destroy() 917 c.Assert(err, jc.Satisfies, errors.IsNotSupported) 918 c.Assert(err, gc.ErrorMatches, "cannot destroy application.*") 919 s.mysql.Refresh() 920 assertOffersRef(c, s.State, "mysql", 1) 921 } 922 923 func (s *applicationOffersSuite) TestRemoveOffersWithConnectionsRace(c *gc.C) { 924 // Create a local wordpress application to relate to the local mysql, 925 // to show that we count remote relations correctly. 926 s.AddTestingApplication(c, "local-wordpress", s.AddTestingCharm(c, "wordpress")) 927 eps, err := s.State.InferEndpoints("local-wordpress", "mysql") 928 c.Assert(err, jc.ErrorIsNil) 929 localRel, err := s.State.AddRelation(eps...) 930 c.Assert(err, jc.ErrorIsNil) 931 932 ao := state.NewApplicationOffers(s.State) 933 offer := s.createDefaultOffer(c) 934 addOfferConnection := func() { 935 // Remove the local relation and add a remote relation, 936 // so that the relation count remains stable. We should 937 // be checking the *remote* relation count. 938 c.Assert(localRel.Destroy(), jc.ErrorIsNil) 939 s.addOfferConnection(c, offer.OfferUUID) 940 } 941 defer state.SetBeforeHooks(c, s.State, addOfferConnection).Check() 942 943 err = ao.Remove(offer.OfferName, false) 944 c.Assert(err, gc.ErrorMatches, `cannot delete application offer "hosted-mysql": offer has 1 relation`) 945 } 946 947 func (s *applicationOffersSuite) TestWatchOfferStatus(c *gc.C) { 948 ao := state.NewApplicationOffers(s.State) 949 offer, err := ao.AddOffer(crossmodel.AddApplicationOfferArgs{ 950 OfferName: "hosted-mysql", 951 ApplicationName: "mysql", 952 Owner: s.Owner.Id(), 953 }) 954 c.Assert(err, jc.ErrorIsNil) 955 956 // Ensure that all the creation events have flowed through the system. 957 s.WaitForModelWatchersIdle(c, s.Model.UUID()) 958 959 w, err := s.State.WatchOfferStatus(offer.OfferUUID) 960 c.Assert(err, jc.ErrorIsNil) 961 962 defer statetesting.AssertStop(c, w) 963 wc := statetesting.NewNotifyWatcherC(c, w) 964 // Initial event. 965 wc.AssertOneChange() 966 967 app, err := s.State.Application(offer.ApplicationName) 968 c.Assert(err, jc.ErrorIsNil) 969 err = app.SetStatus(status.StatusInfo{ 970 Status: status.Waiting, 971 Message: "waiting for replication", 972 }) 973 c.Assert(err, jc.ErrorIsNil) 974 wc.AssertOneChange() 975 976 u := s.Factory.MakeUnit(c, &factory.UnitParams{ 977 Application: app, 978 }) 979 wc.AssertOneChange() 980 err = u.SetStatus(status.StatusInfo{ 981 Status: status.Blocked, 982 }) 983 c.Assert(err, jc.ErrorIsNil) 984 wc.AssertOneChange() 985 err = u.Destroy() 986 c.Assert(err, jc.ErrorIsNil) 987 wc.AssertOneChange() 988 989 err = ao.Remove(offer.OfferName, false) 990 c.Assert(err, jc.ErrorIsNil) 991 _, err = ao.ApplicationOffer("hosted-mysql") 992 c.Assert(err, jc.Satisfies, errors.IsNotFound) 993 err = app.Destroy() 994 c.Assert(err, jc.ErrorIsNil) 995 wc.AssertOneChange() 996 } 997 998 func (s *applicationOffersSuite) TestWatchOffer(c *gc.C) { 999 ao := state.NewApplicationOffers(s.State) 1000 offer, err := ao.AddOffer(crossmodel.AddApplicationOfferArgs{ 1001 OfferName: "hosted-mysql", 1002 ApplicationName: "mysql", 1003 Owner: s.Owner.Id(), 1004 }) 1005 c.Assert(err, jc.ErrorIsNil) 1006 1007 anotherOffer, err := ao.AddOffer(crossmodel.AddApplicationOfferArgs{ 1008 OfferName: "hosted-postgresql", 1009 ApplicationName: "mysql", 1010 Owner: s.Owner.Id(), 1011 }) 1012 c.Assert(err, jc.ErrorIsNil) 1013 1014 // Ensure that all the creation events have flowed through the system. 1015 s.WaitForModelWatchersIdle(c, s.Model.UUID()) 1016 1017 w := s.State.WatchOffer(offer.OfferName) 1018 1019 defer statetesting.AssertStop(c, w) 1020 wc := statetesting.NewNotifyWatcherC(c, w) 1021 // Initial event. 1022 wc.AssertOneChange() 1023 1024 err = ao.Remove(offer.OfferName, false) 1025 c.Assert(err, jc.ErrorIsNil) 1026 wc.AssertOneChange() 1027 1028 err = ao.Remove(anotherOffer.OfferName, false) 1029 c.Assert(err, jc.ErrorIsNil) 1030 wc.AssertNoChange() 1031 }