github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/state/allwatcher_internal_test.go (about) 1 // Copyright 2013 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package state 5 6 import ( 7 "fmt" 8 "sort" 9 "time" 10 11 "github.com/juju/charm/v12" 12 "github.com/juju/errors" 13 "github.com/juju/loggo" 14 "github.com/juju/names/v5" 15 jc "github.com/juju/testing/checkers" 16 "github.com/juju/utils/v3" 17 "github.com/juju/version/v2" 18 gc "gopkg.in/check.v1" 19 20 "github.com/juju/juju/core/constraints" 21 "github.com/juju/juju/core/crossmodel" 22 "github.com/juju/juju/core/instance" 23 "github.com/juju/juju/core/life" 24 "github.com/juju/juju/core/model" 25 coremodel "github.com/juju/juju/core/model" 26 "github.com/juju/juju/core/multiwatcher" 27 "github.com/juju/juju/core/network" 28 corenetwork "github.com/juju/juju/core/network" 29 "github.com/juju/juju/core/permission" 30 "github.com/juju/juju/core/status" 31 "github.com/juju/juju/state/watcher" 32 "github.com/juju/juju/testing" 33 ) 34 35 const allEndpoints = "" 36 37 var ( 38 _ backingEntityDoc = (*backingMachine)(nil) 39 _ backingEntityDoc = (*backingUnit)(nil) 40 _ backingEntityDoc = (*backingApplication)(nil) 41 _ backingEntityDoc = (*backingCharm)(nil) 42 _ backingEntityDoc = (*backingRemoteApplication)(nil) 43 _ backingEntityDoc = (*backingApplicationOffer)(nil) 44 _ backingEntityDoc = (*backingRelation)(nil) 45 _ backingEntityDoc = (*backingAnnotation)(nil) 46 _ backingEntityDoc = (*backingStatus)(nil) 47 _ backingEntityDoc = (*backingConstraints)(nil) 48 _ backingEntityDoc = (*backingSettings)(nil) 49 _ backingEntityDoc = (*backingOpenedPorts)(nil) 50 _ backingEntityDoc = (*backingAction)(nil) 51 _ backingEntityDoc = (*backingBlock)(nil) 52 _ backingEntityDoc = (*backingGeneration)(nil) 53 _ backingEntityDoc = (*backingPodSpec)(nil) 54 ) 55 56 var dottedConfig = ` 57 options: 58 key.dotted: {default: My Key, description: Desc, type: string} 59 ` 60 61 type allWatcherBaseSuite struct { 62 internalStateSuite 63 currentTime time.Time 64 } 65 66 func (s *allWatcherBaseSuite) SetUpTest(c *gc.C) { 67 s.internalStateSuite.SetUpTest(c) 68 s.currentTime = s.state.clock().Now() 69 loggo.GetLogger("juju.state.allwatcher").SetLogLevel(loggo.TRACE) 70 } 71 72 // setUpScenario adds some entities to the state so that 73 // we can check that they all get pulled in by 74 // all(Model)WatcherStateBacking.GetAll. 75 func (s *allWatcherBaseSuite) setUpScenario(c *gc.C, st *State, units int) (entities entityInfoSlice) { 76 modelUUID := st.ModelUUID() 77 model, err := st.Model() 78 c.Assert(err, jc.ErrorIsNil) 79 add := func(e multiwatcher.EntityInfo) { 80 entities = append(entities, e) 81 } 82 83 modelCfg, err := model.Config() 84 c.Assert(err, jc.ErrorIsNil) 85 modelStatus, err := model.Status() 86 c.Assert(err, jc.ErrorIsNil) 87 credential, _ := model.CloudCredentialTag() 88 add(&multiwatcher.ModelInfo{ 89 ModelUUID: model.UUID(), 90 Type: coremodel.ModelType(model.Type()), 91 Name: model.Name(), 92 Life: life.Alive, 93 Owner: model.Owner().Id(), 94 ControllerUUID: model.ControllerUUID(), 95 IsController: model.IsControllerModel(), 96 Cloud: model.CloudName(), 97 CloudRegion: model.CloudRegion(), 98 CloudCredential: credential.Id(), 99 Config: modelCfg.AllAttrs(), 100 Status: multiwatcher.StatusInfo{ 101 Current: modelStatus.Status, 102 Message: modelStatus.Message, 103 Data: modelStatus.Data, 104 Since: modelStatus.Since, 105 }, 106 SLA: multiwatcher.ModelSLAInfo{ 107 Level: "unsupported", 108 }, 109 UserPermissions: map[string]permission.Access{ 110 "test-admin": permission.AdminAccess, 111 }, 112 }) 113 114 now := s.currentTime 115 m, err := st.AddMachine(UbuntuBase("12.10"), JobHostUnits) 116 c.Assert(err, jc.ErrorIsNil) 117 c.Assert(m.Tag(), gc.Equals, names.NewMachineTag("0")) 118 // Ensure there's one and only one controller. 119 controllerIds, err := st.ControllerIds() 120 c.Assert(err, jc.ErrorIsNil) 121 needController := len(controllerIds) == 0 122 if needController { 123 _, err = st.EnableHA(1, constraints.Value{}, UbuntuBase("20.04"), []string{m.Id()}) 124 c.Assert(err, jc.ErrorIsNil) 125 node, err := st.ControllerNode(m.Id()) 126 c.Assert(err, jc.ErrorIsNil) 127 err = node.SetHasVote(true) 128 c.Assert(err, jc.ErrorIsNil) 129 } 130 // TODO(dfc) instance.Id should take a TAG! 131 err = m.SetProvisioned(instance.Id("i-"+m.Tag().String()), "", "fake_nonce", nil) 132 c.Assert(err, jc.ErrorIsNil) 133 hc, err := m.HardwareCharacteristics() 134 c.Assert(err, jc.ErrorIsNil) 135 cp, err := m.CharmProfiles() 136 c.Assert(err, jc.ErrorIsNil) 137 138 // Add a space and an address on the space. 139 space, err := st.AddSpace("test-space", "provider-space", nil, true) 140 c.Assert(err, jc.ErrorIsNil) 141 providerAddr := network.NewSpaceAddress("example.com") 142 providerAddr.SpaceID = space.Id() 143 err = m.SetProviderAddresses(providerAddr) 144 c.Assert(err, jc.ErrorIsNil) 145 146 var addresses []network.ProviderAddress 147 for _, addr := range m.Addresses() { 148 addresses = append(addresses, network.ProviderAddress{ 149 MachineAddress: addr.MachineAddress, 150 SpaceName: network.SpaceName(space.Name()), 151 ProviderSpaceID: space.ProviderId(), 152 }) 153 } 154 155 jobs := []coremodel.MachineJob{JobHostUnits.ToParams()} 156 if needController { 157 jobs = append(jobs, JobManageModel.ToParams()) 158 } 159 add(&multiwatcher.MachineInfo{ 160 ModelUUID: modelUUID, 161 ID: "0", 162 InstanceID: "i-machine-0", 163 AgentStatus: multiwatcher.StatusInfo{ 164 Current: status.Pending, 165 Data: map[string]interface{}{}, 166 Since: &now, 167 }, 168 InstanceStatus: multiwatcher.StatusInfo{ 169 Current: status.Pending, 170 Data: map[string]interface{}{}, 171 Since: &now, 172 }, 173 Life: life.Alive, 174 Base: "ubuntu@12.10", 175 Jobs: jobs, 176 Addresses: addresses, 177 HardwareCharacteristics: hc, 178 CharmProfiles: cp, 179 HasVote: needController, 180 WantsVote: needController, 181 PreferredPublicAddress: providerAddr, 182 PreferredPrivateAddress: providerAddr, 183 }) 184 185 wordpress := AddTestingApplication(c, st, "wordpress", AddTestingCharm(c, st, "wordpress")) 186 err = wordpress.MergeExposeSettings(nil) 187 c.Assert(err, jc.ErrorIsNil) 188 err = wordpress.SetMinUnits(units) 189 c.Assert(err, jc.ErrorIsNil) 190 err = wordpress.SetConstraints(constraints.MustParse("mem=100M")) 191 c.Assert(err, jc.ErrorIsNil) 192 setApplicationConfigAttr(c, wordpress, "blog-title", "boring") 193 pairs := map[string]string{"x": "12", "y": "99"} 194 add(&multiwatcher.ApplicationInfo{ 195 ModelUUID: modelUUID, 196 Name: "wordpress", 197 Exposed: true, 198 CharmURL: applicationCharmURL(wordpress).String(), 199 Life: life.Alive, 200 MinUnits: units, 201 Constraints: constraints.MustParse("mem=100M"), 202 Annotations: pairs, 203 Config: charm.Settings{"blog-title": "boring"}, 204 Subordinate: false, 205 Status: multiwatcher.StatusInfo{ 206 Current: "unset", 207 Message: "", 208 Data: map[string]interface{}{}, 209 Since: &now, 210 }, 211 }) 212 err = model.SetAnnotations(wordpress, pairs) 213 c.Assert(err, jc.ErrorIsNil) 214 add(&multiwatcher.AnnotationInfo{ 215 ModelUUID: modelUUID, 216 Tag: "application-wordpress", 217 Annotations: pairs, 218 }) 219 220 add(&multiwatcher.CharmInfo{ 221 ModelUUID: modelUUID, 222 CharmURL: applicationCharmURL(wordpress).String(), 223 Life: life.Alive, 224 DefaultConfig: map[string]interface{}{"blog-title": "My Title"}, 225 }) 226 227 logging := AddTestingApplication(c, st, "logging", AddTestingCharm(c, st, "logging")) 228 add(&multiwatcher.ApplicationInfo{ 229 ModelUUID: modelUUID, 230 Name: "logging", 231 CharmURL: applicationCharmURL(logging).String(), 232 Life: life.Alive, 233 Config: charm.Settings{}, 234 Subordinate: true, 235 Status: multiwatcher.StatusInfo{ 236 Current: "unset", 237 Message: "", 238 Data: map[string]interface{}{}, 239 Since: &now, 240 }, 241 }) 242 243 add(&multiwatcher.CharmInfo{ 244 ModelUUID: modelUUID, 245 CharmURL: applicationCharmURL(logging).String(), 246 Life: life.Alive, 247 }) 248 249 eps, err := st.InferEndpoints("logging", "wordpress") 250 c.Assert(err, jc.ErrorIsNil) 251 rel, err := st.AddRelation(eps...) 252 c.Assert(err, jc.ErrorIsNil) 253 add(&multiwatcher.RelationInfo{ 254 ModelUUID: modelUUID, 255 Key: "logging:logging-directory wordpress:logging-dir", 256 ID: rel.Id(), 257 Endpoints: []multiwatcher.Endpoint{ 258 {ApplicationName: "logging", Relation: multiwatcher.CharmRelation{Name: "logging-directory", Role: "requirer", Interface: "logging", Optional: false, Limit: 0, Scope: "container"}}, 259 {ApplicationName: "wordpress", Relation: multiwatcher.CharmRelation{Name: "logging-dir", Role: "provider", Interface: "logging", Optional: false, Limit: 0, Scope: "container"}}}, 260 }) 261 262 for i := 0; i < units; i++ { 263 wu, err := wordpress.AddUnit(AddUnitParams{}) 264 c.Assert(err, jc.ErrorIsNil) 265 c.Assert(wu.Tag().String(), gc.Equals, fmt.Sprintf("unit-wordpress-%d", i)) 266 267 m, err := st.AddMachine(UbuntuBase("12.10"), JobHostUnits) 268 c.Assert(err, jc.ErrorIsNil) 269 c.Assert(m.Tag().String(), gc.Equals, fmt.Sprintf("machine-%d", i+1)) 270 271 pairs := map[string]string{"name": fmt.Sprintf("bar %d", i)} 272 add(&multiwatcher.UnitInfo{ 273 ModelUUID: modelUUID, 274 Name: fmt.Sprintf("wordpress/%d", i), 275 Application: wordpress.Name(), 276 Base: "ubuntu@12.10", 277 Life: life.Alive, 278 MachineID: m.Id(), 279 Annotations: pairs, 280 Subordinate: false, 281 WorkloadStatus: multiwatcher.StatusInfo{ 282 Current: "waiting", 283 Message: "waiting for machine", 284 Data: map[string]interface{}{}, 285 Since: &now, 286 }, 287 AgentStatus: multiwatcher.StatusInfo{ 288 Current: "allocating", 289 Message: "", 290 Data: map[string]interface{}{}, 291 Since: &now, 292 }, 293 }) 294 err = model.SetAnnotations(wu, pairs) 295 c.Assert(err, jc.ErrorIsNil) 296 add(&multiwatcher.AnnotationInfo{ 297 ModelUUID: modelUUID, 298 Tag: fmt.Sprintf("unit-wordpress-%d", i), 299 Annotations: pairs, 300 }) 301 err = m.SetProvisioned(instance.Id("i-"+m.Tag().String()), "", "fake_nonce", nil) 302 c.Assert(err, jc.ErrorIsNil) 303 sInfo := status.StatusInfo{ 304 Status: status.Error, 305 Message: m.Tag().String(), 306 Since: &now, 307 } 308 err = m.SetStatus(sInfo) 309 c.Assert(err, jc.ErrorIsNil) 310 hc, err := m.HardwareCharacteristics() 311 c.Assert(err, jc.ErrorIsNil) 312 cp, err := m.CharmProfiles() 313 c.Assert(err, jc.ErrorIsNil) 314 add(&multiwatcher.MachineInfo{ 315 ModelUUID: modelUUID, 316 ID: fmt.Sprint(i + 1), 317 InstanceID: "i-" + m.Tag().String(), 318 AgentStatus: multiwatcher.StatusInfo{ 319 Current: status.Error, 320 Message: m.Tag().String(), 321 Data: map[string]interface{}{}, 322 Since: &now, 323 }, 324 InstanceStatus: multiwatcher.StatusInfo{ 325 Current: status.Pending, 326 Data: map[string]interface{}{}, 327 Since: &now, 328 }, 329 Life: life.Alive, 330 Base: "ubuntu@12.10", 331 Jobs: []coremodel.MachineJob{JobHostUnits.ToParams()}, 332 Addresses: []network.ProviderAddress{}, 333 HardwareCharacteristics: hc, 334 CharmProfiles: cp, 335 HasVote: false, 336 WantsVote: false, 337 }) 338 err = wu.AssignToMachine(m) 339 c.Assert(err, jc.ErrorIsNil) 340 341 wru, err := rel.Unit(wu) 342 c.Assert(err, jc.ErrorIsNil) 343 344 // Create the subordinate unit as a side-effect of entering 345 // scope in the principal's relation-unit. 346 err = wru.EnterScope(nil) 347 c.Assert(err, jc.ErrorIsNil) 348 349 lu, err := st.Unit(fmt.Sprintf("logging/%d", i)) 350 c.Assert(err, jc.ErrorIsNil) 351 c.Assert(lu.IsPrincipal(), jc.IsFalse) 352 unitName := fmt.Sprintf("wordpress/%d", i) 353 add(&multiwatcher.UnitInfo{ 354 ModelUUID: modelUUID, 355 Name: fmt.Sprintf("logging/%d", i), 356 Application: "logging", 357 Base: "ubuntu@12.10", 358 Life: life.Alive, 359 MachineID: m.Id(), 360 Principal: unitName, 361 Subordinate: true, 362 WorkloadStatus: multiwatcher.StatusInfo{ 363 Current: "waiting", 364 Message: "waiting for machine", 365 Data: map[string]interface{}{}, 366 Since: &now, 367 }, 368 AgentStatus: multiwatcher.StatusInfo{ 369 Current: "allocating", 370 Message: "", 371 Data: map[string]interface{}{}, 372 Since: &now, 373 }, 374 }) 375 } 376 377 _, remoteApplicationInfo := addTestingRemoteApplication( 378 c, st, "remote-mysql1", "me/model.mysql", mysqlRelations, false, 379 ) 380 add(&remoteApplicationInfo) 381 382 mysql := AddTestingApplication(c, st, "mysql", AddTestingCharm(c, st, "mysql")) 383 curl := applicationCharmURL(mysql) 384 add(&multiwatcher.ApplicationInfo{ 385 ModelUUID: modelUUID, 386 Name: "mysql", 387 CharmURL: curl.String(), 388 Life: life.Alive, 389 Config: charm.Settings{}, 390 Constraints: constraints.MustParse("arch=amd64"), 391 Status: multiwatcher.StatusInfo{ 392 Current: "unset", 393 Message: "", 394 Data: map[string]interface{}{}, 395 Since: &now, 396 }, 397 }) 398 399 add(&multiwatcher.CharmInfo{ 400 ModelUUID: modelUUID, 401 CharmURL: applicationCharmURL(mysql).String(), 402 Life: life.Alive, 403 DefaultConfig: map[string]interface{}{"dataset-size": "80%"}, 404 }) 405 406 // Set up a remote application related to the offer. 407 // It won't be included in the backing model. 408 addTestingRemoteApplication( 409 c, st, "remote-wordpress", "", []charm.Relation{{ 410 Name: "db", 411 Role: "requirer", 412 Scope: charm.ScopeGlobal, 413 Interface: "mysql", 414 }}, true, 415 ) 416 addTestingRemoteApplication( 417 c, st, "remote-wordpress2", "", []charm.Relation{{ 418 Name: "db", 419 Role: "requirer", 420 Scope: charm.ScopeGlobal, 421 Interface: "mysql", 422 }}, true, 423 ) 424 eps, err = st.InferEndpoints("mysql", "remote-wordpress2") 425 c.Assert(err, jc.ErrorIsNil) 426 rel, err = st.AddRelation(eps...) 427 c.Assert(err, jc.ErrorIsNil) 428 add(&multiwatcher.RelationInfo{ 429 ModelUUID: modelUUID, 430 Key: rel.Tag().Id(), 431 ID: rel.Id(), 432 Endpoints: []multiwatcher.Endpoint{ 433 {ApplicationName: "mysql", Relation: multiwatcher.CharmRelation{Name: "server", Role: "provider", Interface: "mysql", Optional: false, Limit: 0, Scope: "global"}}, 434 {ApplicationName: "remote-wordpress2", Relation: multiwatcher.CharmRelation{Name: "db", Role: "requirer", Interface: "mysql", Optional: false, Limit: 0, Scope: "global"}}}, 435 }) 436 437 applicationOfferInfo, rel2 := addTestingApplicationOffer( 438 c, st, s.owner, "hosted-mysql", "mysql", curl.Name, []string{"server"}, 439 ) 440 add(&multiwatcher.RelationInfo{ 441 ModelUUID: modelUUID, 442 Key: rel2.Tag().Id(), 443 ID: rel2.Id(), 444 Endpoints: []multiwatcher.Endpoint{ 445 {ApplicationName: "mysql", Relation: multiwatcher.CharmRelation{Name: "server", Role: "provider", Interface: "mysql", Optional: false, Limit: 0, Scope: "global"}}, 446 {ApplicationName: "remote-wordpress", Relation: multiwatcher.CharmRelation{Name: "db", Role: "requirer", Interface: "mysql", Optional: false, Limit: 0, Scope: "global"}}}, 447 }) 448 add(&applicationOfferInfo) 449 450 return 451 } 452 453 var mysqlRelations = []charm.Relation{{ 454 Name: "db", 455 Role: "provider", 456 Scope: charm.ScopeGlobal, 457 Interface: "mysql", 458 }, { 459 Name: "nrpe-external-master", 460 Role: "provider", 461 Scope: charm.ScopeGlobal, 462 Interface: "nrpe-external-master", 463 }} 464 465 func addTestingRemoteApplication( 466 c *gc.C, st *State, name, url string, relations []charm.Relation, isProxy bool, 467 ) (*RemoteApplication, multiwatcher.RemoteApplicationUpdate) { 468 469 rs, err := st.AddRemoteApplication(AddRemoteApplicationParams{ 470 Name: name, 471 URL: url, 472 SourceModel: testing.ModelTag, 473 Endpoints: relations, 474 IsConsumerProxy: isProxy, 475 }) 476 c.Assert(err, jc.ErrorIsNil) 477 var appStatus multiwatcher.StatusInfo 478 if !isProxy { 479 status, err := rs.Status() 480 c.Assert(err, jc.ErrorIsNil) 481 appStatus = multiwatcher.StatusInfo{ 482 Current: status.Status, 483 Message: status.Message, 484 Data: status.Data, 485 Since: status.Since, 486 } 487 } 488 return rs, multiwatcher.RemoteApplicationUpdate{ 489 ModelUUID: st.ModelUUID(), 490 Name: name, 491 OfferURL: url, 492 Life: life.Value(rs.Life().String()), 493 Status: appStatus, 494 } 495 } 496 497 func addTestingApplicationOffer( 498 c *gc.C, st *State, owner names.UserTag, offerName, applicationName, charmName string, endpoints []string, 499 ) (multiwatcher.ApplicationOfferInfo, *Relation) { 500 501 eps := make(map[string]string) 502 for _, ep := range endpoints { 503 eps[ep] = ep 504 } 505 offers := NewApplicationOffers(st) 506 offer, err := offers.AddOffer(crossmodel.AddApplicationOfferArgs{ 507 OfferName: offerName, 508 Owner: owner.Name(), 509 ApplicationName: applicationName, 510 Endpoints: eps, 511 }) 512 c.Assert(err, jc.ErrorIsNil) 513 relEps, err := st.InferEndpoints("mysql", "remote-wordpress") 514 c.Assert(err, jc.ErrorIsNil) 515 rel, err := st.AddRelation(relEps...) 516 c.Assert(err, jc.ErrorIsNil) 517 err = rel.SetStatus(status.StatusInfo{Status: status.Joined}) 518 c.Assert(err, jc.ErrorIsNil) 519 _, err = st.AddOfferConnection(AddOfferConnectionParams{ 520 SourceModelUUID: utils.MustNewUUID().String(), 521 RelationId: rel.Id(), 522 Username: "fred", 523 OfferUUID: offer.OfferUUID, 524 RelationKey: rel.Tag().Id(), 525 }) 526 c.Assert(err, jc.ErrorIsNil) 527 return multiwatcher.ApplicationOfferInfo{ 528 ModelUUID: st.ModelUUID(), 529 OfferName: offerName, 530 OfferUUID: offer.OfferUUID, 531 ApplicationName: applicationName, 532 CharmName: charmName, 533 TotalConnectedCount: 1, 534 ActiveConnectedCount: 1, 535 }, rel 536 } 537 538 var _ = gc.Suite(&allWatcherStateSuite{}) 539 540 type allWatcherStateSuite struct { 541 allWatcherBaseSuite 542 } 543 544 func (s *allWatcherStateSuite) reset(c *gc.C) { 545 s.TearDownTest(c) 546 s.SetUpTest(c) 547 } 548 549 func (s *allWatcherStateSuite) TestGetAll(c *gc.C) { 550 expectEntities := s.setUpScenario(c, s.state, 2) 551 s.checkGetAll(c, expectEntities) 552 } 553 554 func (s *allWatcherStateSuite) TestGetAllMultiModel(c *gc.C) { 555 // Set up 2 models and ensure that GetAll returns the 556 // entities for both models with no errors. 557 expectEntities := s.setUpScenario(c, s.state, 2) 558 559 // Use more units in the second model. 560 moreEntities := s.setUpScenario(c, s.newState(c), 4) 561 562 expectEntities = append(expectEntities, moreEntities...) 563 564 s.checkGetAll(c, expectEntities) 565 } 566 567 func (s *allWatcherStateSuite) checkGetAll(c *gc.C, expectEntities entityInfoSlice) { 568 b, err := NewAllWatcherBacking(s.pool) 569 c.Assert(err, jc.ErrorIsNil) 570 all := multiwatcher.NewStore(loggo.GetLogger("test")) 571 err = b.GetAll(all) 572 c.Assert(err, jc.ErrorIsNil) 573 var gotEntities entityInfoSlice = all.All() 574 sort.Sort(gotEntities) 575 sort.Sort(expectEntities) 576 assertEntitiesEqual(c, gotEntities, expectEntities) 577 } 578 579 func applicationCharmURL(app *Application) *charm.URL { 580 urlStr, _ := app.CharmURL() 581 url := charm.MustParseURL(*urlStr) 582 return url 583 } 584 585 func setApplicationConfigAttr(c *gc.C, app *Application, attr string, val interface{}) { 586 err := app.UpdateCharmConfig(model.GenerationMaster, charm.Settings{attr: val}) 587 c.Assert(err, jc.ErrorIsNil) 588 } 589 590 func setModelConfigAttr(c *gc.C, st *State, attr string, val interface{}) { 591 m, err := st.Model() 592 c.Assert(err, jc.ErrorIsNil) 593 594 err = m.UpdateModelConfig(map[string]interface{}{attr: val}, nil) 595 c.Assert(err, jc.ErrorIsNil) 596 } 597 598 // changeTestCase encapsulates entities to add, a change, and 599 // the expected contents for a test. 600 type changeTestCase struct { 601 // about describes the test case. 602 about string 603 604 // initialContents contains the infos of the 605 // watcher before signaling the change. 606 initialContents []multiwatcher.EntityInfo 607 608 // change signals the change of the watcher. 609 change watcher.Change 610 611 // expectContents contains the expected infos of 612 // the watcher after signaling the change. 613 expectContents []multiwatcher.EntityInfo 614 } 615 616 // changeTestFunc is a function for the preparation of a test and 617 // the creation of the according case. 618 type changeTestFunc func(c *gc.C, st *State) changeTestCase 619 620 // performChangeTestCases runs a passed number of test cases for changes. 621 func (s *allWatcherStateSuite) performChangeTestCases(c *gc.C, changeTestFuncs []changeTestFunc) { 622 for i, changeTestFunc := range changeTestFuncs { 623 test := changeTestFunc(c, s.state) 624 625 c.Logf("test %d. %s", i, test.about) 626 b, err := NewAllWatcherBacking(s.pool) 627 c.Assert(err, jc.ErrorIsNil) 628 all := multiwatcher.NewStore(loggo.GetLogger("test")) 629 for _, info := range test.initialContents { 630 all.Update(info) 631 } 632 err = b.Changed(all, test.change) 633 c.Assert(err, jc.ErrorIsNil) 634 entities := all.All() 635 assertEntitiesEqual(c, entities, test.expectContents) 636 s.reset(c) 637 } 638 } 639 640 func (s *allWatcherStateSuite) TestChangePermissions(c *gc.C) { 641 testChangePermissions(c, s.performChangeTestCases) 642 } 643 644 func (s *allWatcherStateSuite) TestChangeAnnotations(c *gc.C) { 645 testChangeAnnotations(c, s.performChangeTestCases) 646 } 647 648 func (s *allWatcherStateSuite) TestChangeMachines(c *gc.C) { 649 testChangeMachines(c, s.performChangeTestCases) 650 } 651 652 func (s *allWatcherStateSuite) TestChangeRelations(c *gc.C) { 653 testChangeRelations(c, s.owner, s.performChangeTestCases) 654 } 655 656 func (s *allWatcherStateSuite) TestChangeApplications(c *gc.C) { 657 testChangeApplications(c, s.owner, s.performChangeTestCases) 658 } 659 660 func strPtr(s string) *string { 661 return &s 662 } 663 664 func (s *allWatcherStateSuite) TestChangeCAASApplications(c *gc.C) { 665 loggo.GetLogger("juju.txn").SetLogLevel(loggo.TRACE) 666 changeTestFuncs := []changeTestFunc{ 667 // Applications. 668 func(c *gc.C, st *State) changeTestCase { 669 return changeTestCase{ 670 about: "not finding a podspec for a change is fine", 671 change: watcher.Change{ 672 C: "podSpecs", 673 Id: st.docID(applicationGlobalKey("mysql")), 674 }} 675 }, 676 func(c *gc.C, st *State) changeTestCase { 677 caasSt := s.newCAASState(c) 678 m, err := caasSt.Model() 679 c.Assert(err, jc.ErrorIsNil) 680 cm, err := m.CAASModel() 681 c.Assert(err, jc.ErrorIsNil) 682 ch := AddTestingCharmForSeries(c, caasSt, "kubernetes", "mysql") 683 mysql := AddTestingApplicationForBase(c, caasSt, UbuntuBase("20.04"), "mysql", ch) 684 err = cm.SetPodSpec(nil, mysql.ApplicationTag(), strPtr("some podspec")) 685 c.Assert(err, jc.ErrorIsNil) 686 now := st.clock().Now() 687 return changeTestCase{ 688 about: "initial CAAS application has podspec", 689 change: watcher.Change{ 690 C: "applications", 691 Id: caasSt.docID("mysql"), 692 }, 693 expectContents: []multiwatcher.EntityInfo{ 694 &multiwatcher.ApplicationInfo{ 695 ModelUUID: caasSt.ModelUUID(), 696 Name: "mysql", 697 CharmURL: "local:kubernetes/kubernetes-mysql-0", 698 Life: "alive", 699 Config: map[string]interface{}{}, 700 Constraints: constraints.MustParse("arch=amd64"), 701 Status: multiwatcher.StatusInfo{ 702 Current: "unset", 703 Data: map[string]interface{}{}, 704 Since: &now, 705 }, 706 OperatorStatus: multiwatcher.StatusInfo{ 707 Current: "waiting", 708 Message: "waiting for container", 709 Data: map[string]interface{}{}, 710 Since: &now, 711 }, 712 PodSpec: &multiwatcher.PodSpec{ 713 Spec: "some podspec", 714 }, 715 }, 716 }, 717 } 718 }, 719 func(c *gc.C, st *State) changeTestCase { 720 caasSt := s.newCAASState(c) 721 m, err := caasSt.Model() 722 c.Assert(err, jc.ErrorIsNil) 723 cm, err := m.CAASModel() 724 c.Assert(err, jc.ErrorIsNil) 725 ch := AddTestingCharmForSeries(c, caasSt, "kubernetes", "mysql") 726 mysql := AddTestingApplicationForBase(c, caasSt, UbuntuBase("20.04"), "mysql", ch) 727 err = cm.SetPodSpec(nil, mysql.ApplicationTag(), strPtr("some podspec")) 728 c.Assert(err, jc.ErrorIsNil) 729 return changeTestCase{ 730 about: "application podspec is updated", 731 initialContents: []multiwatcher.EntityInfo{ 732 &multiwatcher.ApplicationInfo{ 733 ModelUUID: caasSt.ModelUUID(), 734 Name: "mysql", 735 }, 736 }, 737 change: watcher.Change{ 738 C: "podSpecs", 739 Id: caasSt.docID(applicationGlobalKey("mysql")), 740 }, 741 expectContents: []multiwatcher.EntityInfo{ 742 &multiwatcher.ApplicationInfo{ 743 ModelUUID: caasSt.ModelUUID(), 744 Name: "mysql", 745 PodSpec: &multiwatcher.PodSpec{ 746 Spec: "some podspec", 747 }, 748 }, 749 }, 750 } 751 }, 752 func(c *gc.C, st *State) changeTestCase { 753 caasSt := s.newCAASState(c) 754 ch := AddTestingCharmForSeries(c, caasSt, "kubernetes", "mysql") 755 mysql := AddTestingApplicationForBase(c, caasSt, UbuntuBase("20.04"), "mysql", ch) 756 now := st.clock().Now() 757 sInfo := status.StatusInfo{ 758 Status: status.Error, 759 Message: "failure", 760 Since: &now, 761 } 762 err := mysql.SetOperatorStatus(sInfo) 763 c.Assert(err, jc.ErrorIsNil) 764 return changeTestCase{ 765 about: "operator status update, updates application", 766 initialContents: []multiwatcher.EntityInfo{ 767 &multiwatcher.ApplicationInfo{ 768 ModelUUID: caasSt.ModelUUID(), 769 Name: "mysql", 770 }, 771 }, 772 change: watcher.Change{ 773 C: "statuses", 774 Id: caasSt.docID(applicationGlobalOperatorKey("mysql")), 775 }, 776 expectContents: []multiwatcher.EntityInfo{ 777 &multiwatcher.ApplicationInfo{ 778 ModelUUID: caasSt.ModelUUID(), 779 Name: "mysql", 780 OperatorStatus: multiwatcher.StatusInfo{ 781 Current: "error", 782 Message: "failure", 783 Data: map[string]interface{}{}, 784 Since: &now, 785 }, 786 }, 787 }, 788 } 789 }, 790 } 791 s.performChangeTestCases(c, changeTestFuncs) 792 } 793 794 func (s *allWatcherStateSuite) TestChangeCAASUnits(c *gc.C) { 795 changeTestFuncs := []changeTestFunc{ 796 func(c *gc.C, st *State) changeTestCase { 797 caasSt := s.newCAASState(c) 798 ch := AddTestingCharmForSeries(c, caasSt, "kubernetes", "mysql") 799 mysql := AddTestingApplicationForBase(c, caasSt, UbuntuBase("20.04"), "mysql", ch) 800 unit, err := mysql.AddUnit(AddUnitParams{}) 801 c.Assert(err, jc.ErrorIsNil) 802 803 updateUnits := UpdateUnitsOperation{ 804 Updates: []*UpdateUnitOperation{ 805 unit.UpdateOperation(UnitUpdateProperties{ 806 CloudContainerStatus: &status.StatusInfo{Status: status.Maintenance, Message: "setting up"}, 807 }), 808 }, 809 } 810 err = mysql.UpdateUnits(&updateUnits) 811 c.Assert(err, jc.ErrorIsNil) 812 813 now := st.clock().Now() 814 return changeTestCase{ 815 about: "initial CAAS unit has container status", 816 change: watcher.Change{ 817 C: "units", 818 Id: caasSt.docID("mysql/0"), 819 }, 820 expectContents: []multiwatcher.EntityInfo{ 821 &multiwatcher.UnitInfo{ 822 ModelUUID: caasSt.ModelUUID(), 823 Name: "mysql/0", 824 Application: "mysql", 825 Base: "ubuntu@20.04", 826 Life: "alive", 827 WorkloadStatus: multiwatcher.StatusInfo{ 828 Current: "waiting", 829 Message: "installing agent", 830 Data: map[string]interface{}{}, 831 Since: &now, 832 }, 833 AgentStatus: multiwatcher.StatusInfo{ 834 Current: "allocating", 835 Data: map[string]interface{}{}, 836 Since: &now, 837 }, 838 ContainerStatus: multiwatcher.StatusInfo{ 839 Current: "maintenance", 840 Message: "setting up", 841 Data: map[string]interface{}{}, 842 Since: &now, 843 }, 844 }, 845 }, 846 } 847 }, 848 func(c *gc.C, st *State) changeTestCase { 849 caasSt := s.newCAASState(c) 850 ch := AddTestingCharmForSeries(c, caasSt, "kubernetes", "mysql") 851 mysql := AddTestingApplicationForBase(c, caasSt, UbuntuBase("20.04"), "mysql", ch) 852 unit, err := mysql.AddUnit(AddUnitParams{}) 853 c.Assert(err, jc.ErrorIsNil) 854 855 updateUnits := UpdateUnitsOperation{ 856 Updates: []*UpdateUnitOperation{ 857 unit.UpdateOperation(UnitUpdateProperties{ 858 CloudContainerStatus: &status.StatusInfo{Status: status.Maintenance, Message: "setting up"}, 859 }), 860 }, 861 } 862 err = mysql.UpdateUnits(&updateUnits) 863 c.Assert(err, jc.ErrorIsNil) 864 865 now := st.clock().Now() 866 return changeTestCase{ 867 about: "container status updates existing unit", 868 initialContents: []multiwatcher.EntityInfo{ 869 &multiwatcher.UnitInfo{ 870 ModelUUID: caasSt.ModelUUID(), 871 Name: "mysql/0", 872 Application: "mysql", 873 Base: "ubuntu@20.04", 874 }, 875 }, 876 change: watcher.Change{ 877 C: "statuses", 878 Id: caasSt.docID(unit.globalCloudContainerKey()), 879 }, 880 expectContents: []multiwatcher.EntityInfo{ 881 &multiwatcher.UnitInfo{ 882 ModelUUID: caasSt.ModelUUID(), 883 Name: "mysql/0", 884 Application: "mysql", 885 Base: "ubuntu@20.04", 886 ContainerStatus: multiwatcher.StatusInfo{ 887 Current: "maintenance", 888 Message: "setting up", 889 Data: map[string]interface{}{}, 890 Since: &now, 891 }, 892 }, 893 }, 894 } 895 }, 896 } 897 s.performChangeTestCases(c, changeTestFuncs) 898 } 899 900 func (s *allWatcherStateSuite) TestChangeCharms(c *gc.C) { 901 testChangeCharms(c, s.owner, s.performChangeTestCases) 902 } 903 904 func (s *allWatcherStateSuite) TestChangeApplicationsConstraints(c *gc.C) { 905 testChangeApplicationsConstraints(c, s.owner, s.performChangeTestCases) 906 } 907 908 func (s *allWatcherStateSuite) TestChangeUnits(c *gc.C) { 909 testChangeUnits(c, s.owner, s.performChangeTestCases) 910 } 911 912 func (s *allWatcherStateSuite) TestChangeUnitsNonNilPorts(c *gc.C) { 913 testChangeUnitsNonNilPorts(c, s.owner, s.performChangeTestCases) 914 } 915 916 func (s *allWatcherStateSuite) TestChangeRemoteApplications(c *gc.C) { 917 testChangeRemoteApplications(c, s.performChangeTestCases) 918 } 919 920 func (s *allWatcherStateSuite) TestChangeApplicationOffers(c *gc.C) { 921 testChangeApplicationOffers(c, s.performChangeTestCases) 922 } 923 924 func (s *allWatcherStateSuite) TestChangeGenerations(c *gc.C) { 925 testChangeGenerations(c, s.performChangeTestCases) 926 } 927 928 func (s *allWatcherStateSuite) TestChangeActions(c *gc.C) { 929 changeTestFuncs := []changeTestFunc{ 930 func(c *gc.C, st *State) changeTestCase { 931 wordpress := AddTestingApplication(c, st, "wordpress", AddTestingCharm(c, st, "wordpress")) 932 u, err := wordpress.AddUnit(AddUnitParams{}) 933 c.Assert(err, jc.ErrorIsNil) 934 m, err := st.Model() 935 c.Assert(err, jc.ErrorIsNil) 936 operationID, err := m.EnqueueOperation("a test", 1) 937 c.Assert(err, jc.ErrorIsNil) 938 action, err := m.EnqueueAction(operationID, u.Tag(), "vacuumdb", map[string]interface{}{}, true, "group", nil) 939 c.Assert(err, jc.ErrorIsNil) 940 enqueued := makeActionInfo(action, st) 941 action, err = action.Begin() 942 c.Assert(err, jc.ErrorIsNil) 943 started := makeActionInfo(action, st) 944 return changeTestCase{ 945 about: "action change picks up last change", 946 initialContents: []multiwatcher.EntityInfo{&enqueued, &started}, 947 change: watcher.Change{C: actionsC, Id: st.docID(action.Id())}, 948 expectContents: []multiwatcher.EntityInfo{&started}, 949 } 950 }, 951 } 952 s.performChangeTestCases(c, changeTestFuncs) 953 } 954 955 func (s *allWatcherStateSuite) TestChangeBlocks(c *gc.C) { 956 changeTestFuncs := []changeTestFunc{ 957 func(c *gc.C, st *State) changeTestCase { 958 return changeTestCase{ 959 about: "no blocks in state, no blocks in store -> do nothing", 960 change: watcher.Change{ 961 C: blocksC, 962 Id: st.docID("1"), 963 }} 964 }, 965 func(c *gc.C, st *State) changeTestCase { 966 blockId := st.docID("0") 967 blockType := DestroyBlock.ToParams() 968 blockMsg := "woot" 969 m, err := st.Model() 970 c.Assert(err, jc.ErrorIsNil) 971 return changeTestCase{ 972 about: "no change if block is not in backing", 973 initialContents: []multiwatcher.EntityInfo{&multiwatcher.BlockInfo{ 974 ModelUUID: st.ModelUUID(), 975 ID: blockId, 976 Type: blockType, 977 Message: blockMsg, 978 Tag: m.ModelTag().String(), 979 }}, 980 change: watcher.Change{ 981 C: blocksC, 982 Id: blockId, 983 }, 984 expectContents: []multiwatcher.EntityInfo{&multiwatcher.BlockInfo{ 985 ModelUUID: st.ModelUUID(), 986 ID: blockId, 987 Type: blockType, 988 Message: blockMsg, 989 Tag: m.ModelTag().String(), 990 }}, 991 } 992 }, 993 func(c *gc.C, st *State) changeTestCase { 994 err := st.SwitchBlockOn(DestroyBlock, "multiwatcher testing") 995 c.Assert(err, jc.ErrorIsNil) 996 b, found, err := st.GetBlockForType(DestroyBlock) 997 c.Assert(err, jc.ErrorIsNil) 998 c.Assert(found, jc.IsTrue) 999 blockId := b.Id() 1000 m, err := st.Model() 1001 c.Assert(err, jc.ErrorIsNil) 1002 return changeTestCase{ 1003 about: "block is added if it's in backing but not in Store", 1004 change: watcher.Change{ 1005 C: blocksC, 1006 Id: blockId, 1007 }, 1008 expectContents: []multiwatcher.EntityInfo{ 1009 &multiwatcher.BlockInfo{ 1010 ModelUUID: st.ModelUUID(), 1011 ID: st.localID(blockId), 1012 Type: b.Type().ToParams(), 1013 Message: b.Message(), 1014 Tag: m.ModelTag().String(), 1015 }}} 1016 }, 1017 func(c *gc.C, st *State) changeTestCase { 1018 err := st.SwitchBlockOn(DestroyBlock, "multiwatcher testing") 1019 c.Assert(err, jc.ErrorIsNil) 1020 b, found, err := st.GetBlockForType(DestroyBlock) 1021 c.Assert(err, jc.ErrorIsNil) 1022 c.Assert(found, jc.IsTrue) 1023 err = st.SwitchBlockOff(DestroyBlock) 1024 c.Assert(err, jc.ErrorIsNil) 1025 1026 return changeTestCase{ 1027 about: "block is removed if it's in backing and in multiwatcher.Store", 1028 change: watcher.Change{ 1029 C: blocksC, 1030 Id: b.Id(), 1031 }, 1032 } 1033 }, 1034 } 1035 s.performChangeTestCases(c, changeTestFuncs) 1036 } 1037 1038 func (s *allWatcherStateSuite) TestClosingPorts(c *gc.C) { 1039 // Init the test model. 1040 wordpress := AddTestingApplication(c, s.state, "wordpress", AddTestingCharm(c, s.state, "wordpress")) 1041 u, err := wordpress.AddUnit(AddUnitParams{}) 1042 c.Assert(err, jc.ErrorIsNil) 1043 m, err := s.state.AddMachine(UbuntuBase("12.10"), JobHostUnits) 1044 c.Assert(err, jc.ErrorIsNil) 1045 err = u.AssignToMachine(m) 1046 c.Assert(err, jc.ErrorIsNil) 1047 publicAddress := network.NewSpaceAddress("1.2.3.4", network.WithScope(network.ScopePublic)) 1048 privateAddress := network.NewSpaceAddress("4.3.2.1", network.WithScope(network.ScopeCloudLocal)) 1049 MustOpenUnitPortRange(c, s.state, m, u.Name(), allEndpoints, corenetwork.MustParsePortRange("12345/tcp")) 1050 // Create all watcher state backing. 1051 b, err := NewAllWatcherBacking(s.pool) 1052 c.Assert(err, jc.ErrorIsNil) 1053 all := multiwatcher.NewStore(loggo.GetLogger("test")) 1054 machineInfo := &multiwatcher.MachineInfo{ 1055 ModelUUID: s.state.ModelUUID(), 1056 ID: "0", 1057 PreferredPublicAddress: publicAddress, 1058 PreferredPrivateAddress: privateAddress, 1059 } 1060 all.Update(machineInfo) 1061 // Check opened ports. 1062 err = b.Changed(all, watcher.Change{ 1063 C: "units", 1064 Id: s.state.docID("wordpress/0"), 1065 }) 1066 c.Assert(err, jc.ErrorIsNil) 1067 entities := all.All() 1068 now := s.currentTime 1069 assertEntitiesEqual(c, entities, []multiwatcher.EntityInfo{ 1070 &multiwatcher.UnitInfo{ 1071 ModelUUID: s.state.ModelUUID(), 1072 Name: "wordpress/0", 1073 Application: "wordpress", 1074 Base: "ubuntu@12.10", 1075 Life: life.Alive, 1076 MachineID: "0", 1077 PublicAddress: "1.2.3.4", 1078 PrivateAddress: "4.3.2.1", 1079 OpenPortRangesByEndpoint: corenetwork.GroupedPortRanges{ 1080 allEndpoints: {corenetwork.MustParsePortRange("12345/tcp")}, 1081 }, 1082 WorkloadStatus: multiwatcher.StatusInfo{ 1083 Current: "waiting", 1084 Message: "waiting for machine", 1085 Data: map[string]interface{}{}, 1086 Since: &now, 1087 }, 1088 AgentStatus: multiwatcher.StatusInfo{ 1089 Current: "allocating", 1090 Data: map[string]interface{}{}, 1091 Since: &now, 1092 }, 1093 }, 1094 machineInfo, 1095 }) 1096 // Close the ports. 1097 MustCloseUnitPortRange(c, s.state, m, u.Name(), allEndpoints, corenetwork.MustParsePortRange("12345/tcp")) 1098 err = b.Changed(all, watcher.Change{ 1099 C: openedPortsC, 1100 Id: s.state.docID("0"), 1101 }) 1102 c.Assert(err, jc.ErrorIsNil) 1103 entities = all.All() 1104 assertEntitiesEqual(c, entities, []multiwatcher.EntityInfo{ 1105 &multiwatcher.UnitInfo{ 1106 ModelUUID: s.state.ModelUUID(), 1107 Name: "wordpress/0", 1108 Application: "wordpress", 1109 Base: "ubuntu@12.10", 1110 MachineID: "0", 1111 Life: life.Alive, 1112 PublicAddress: "1.2.3.4", 1113 PrivateAddress: "4.3.2.1", 1114 WorkloadStatus: multiwatcher.StatusInfo{ 1115 Current: "waiting", 1116 Message: "waiting for machine", 1117 Data: map[string]interface{}{}, 1118 Since: &now, 1119 }, 1120 AgentStatus: multiwatcher.StatusInfo{ 1121 Current: "allocating", 1122 Data: map[string]interface{}{}, 1123 Since: &now, 1124 }, 1125 }, 1126 machineInfo, 1127 }) 1128 } 1129 1130 func (s *allWatcherStateSuite) TestApplicationSettings(c *gc.C) { 1131 // Init the test model. 1132 app := AddTestingApplication(c, s.state, "dummy-application", AddTestingCharm(c, s.state, "dummy")) 1133 b, err := NewAllWatcherBacking(s.pool) 1134 c.Assert(err, jc.ErrorIsNil) 1135 all := multiwatcher.NewStore(loggo.GetLogger("test")) 1136 // 1st scenario part: set settings and signal change. 1137 setApplicationConfigAttr(c, app, "username", "foo") 1138 setApplicationConfigAttr(c, app, "outlook", "foo@bar") 1139 all.Update(&multiwatcher.ApplicationInfo{ 1140 ModelUUID: s.state.ModelUUID(), 1141 Name: "dummy-application", 1142 CharmURL: "local:quantal/quantal-dummy-1", 1143 }) 1144 err = b.Changed(all, watcher.Change{ 1145 C: "settings", 1146 Id: s.state.docID("a#dummy-application#local:quantal/quantal-dummy-1"), 1147 }) 1148 c.Assert(err, jc.ErrorIsNil) 1149 entities := all.All() 1150 assertEntitiesEqual(c, entities, []multiwatcher.EntityInfo{ 1151 &multiwatcher.ApplicationInfo{ 1152 ModelUUID: s.state.ModelUUID(), 1153 Name: "dummy-application", 1154 CharmURL: "local:quantal/quantal-dummy-1", 1155 Config: charm.Settings{"outlook": "foo@bar", "username": "foo"}, 1156 }, 1157 }) 1158 // 2nd scenario part: destroy the application and signal change. 1159 err = app.Destroy() 1160 c.Assert(err, jc.ErrorIsNil) 1161 err = b.Changed(all, watcher.Change{ 1162 C: "applications", 1163 Id: s.state.docID("dummy-application"), 1164 }) 1165 c.Assert(err, jc.ErrorIsNil) 1166 entities = all.All() 1167 assertEntitiesEqual(c, entities, []multiwatcher.EntityInfo{}) 1168 } 1169 1170 var _ = gc.Suite(&allModelWatcherStateSuite{}) 1171 1172 type allModelWatcherStateSuite struct { 1173 allWatcherBaseSuite 1174 state1 *State 1175 } 1176 1177 func (s *allModelWatcherStateSuite) SetUpTest(c *gc.C) { 1178 s.allWatcherBaseSuite.SetUpTest(c) 1179 s.state1 = s.newState(c) 1180 } 1181 1182 func (s *allModelWatcherStateSuite) Reset(c *gc.C) { 1183 s.TearDownTest(c) 1184 s.SetUpTest(c) 1185 } 1186 1187 func (s *allModelWatcherStateSuite) NewAllWatcherBacking() (AllWatcherBacking, error) { 1188 return NewAllWatcherBacking(s.pool) 1189 } 1190 1191 func (s *allModelWatcherStateSuite) TestMissingModelNotError(c *gc.C) { 1192 b, err := s.NewAllWatcherBacking() 1193 c.Assert(err, jc.ErrorIsNil) 1194 all := multiwatcher.NewStore(loggo.GetLogger("test")) 1195 1196 dyingModel := "fake-uuid" 1197 st, err := s.pool.Get(dyingModel) 1198 c.Assert(err, jc.ErrorIsNil) 1199 defer st.Release() 1200 1201 removed, err := s.pool.Remove(dyingModel) 1202 c.Assert(err, jc.ErrorIsNil) 1203 c.Assert(removed, jc.IsFalse) 1204 1205 // If the state pool is in the process of removing a model, it will 1206 // return a NotFound error. 1207 err = b.Changed(all, watcher.Change{C: modelsC, Id: dyingModel}) 1208 c.Assert(err, jc.ErrorIsNil) 1209 } 1210 1211 // performChangeTestCases runs a passed number of test cases for changes. 1212 func (s *allModelWatcherStateSuite) performChangeTestCases(c *gc.C, changeTestFuncs []changeTestFunc) { 1213 for i, changeTestFunc := range changeTestFuncs { 1214 func() { // in aid of per-loop defers 1215 defer s.Reset(c) 1216 1217 test0 := changeTestFunc(c, s.state) 1218 1219 c.Logf("test %d. %s", i, test0.about) 1220 b, err := s.NewAllWatcherBacking() 1221 c.Assert(err, jc.ErrorIsNil) 1222 all := multiwatcher.NewStore(loggo.GetLogger("test")) 1223 1224 // Do updates and check for first model. 1225 for _, info := range test0.initialContents { 1226 all.Update(info) 1227 } 1228 err = b.Changed(all, test0.change) 1229 c.Assert(err, jc.ErrorIsNil) 1230 var entities entityInfoSlice = all.All() 1231 assertEntitiesEqual(c, entities, test0.expectContents) 1232 1233 // Now do the same updates for a second model. 1234 test1 := changeTestFunc(c, s.state1) 1235 for _, info := range test1.initialContents { 1236 all.Update(info) 1237 } 1238 err = b.Changed(all, test1.change) 1239 c.Assert(err, jc.ErrorIsNil) 1240 1241 entities = all.All() 1242 1243 // Expected to see entities for both models. 1244 var expectedEntities entityInfoSlice = append( 1245 test0.expectContents, 1246 test1.expectContents...) 1247 sort.Sort(entities) 1248 sort.Sort(expectedEntities) 1249 1250 assertEntitiesEqual(c, entities, expectedEntities) 1251 }() 1252 } 1253 } 1254 1255 func (s *allModelWatcherStateSuite) TestChangePermissions(c *gc.C) { 1256 testChangePermissions(c, s.performChangeTestCases) 1257 } 1258 1259 func (s *allModelWatcherStateSuite) TestChangeAnnotations(c *gc.C) { 1260 testChangeAnnotations(c, s.performChangeTestCases) 1261 } 1262 1263 func (s *allModelWatcherStateSuite) TestChangeMachines(c *gc.C) { 1264 testChangeMachines(c, s.performChangeTestCases) 1265 } 1266 1267 func (s *allModelWatcherStateSuite) TestChangeRelations(c *gc.C) { 1268 testChangeRelations(c, s.owner, s.performChangeTestCases) 1269 } 1270 1271 func (s *allModelWatcherStateSuite) TestChangeApplications(c *gc.C) { 1272 testChangeApplications(c, s.owner, s.performChangeTestCases) 1273 } 1274 1275 func (s *allModelWatcherStateSuite) TestChangeApplicationsConstraints(c *gc.C) { 1276 testChangeApplicationsConstraints(c, s.owner, s.performChangeTestCases) 1277 } 1278 1279 func (s *allModelWatcherStateSuite) TestChangeUnits(c *gc.C) { 1280 testChangeUnits(c, s.owner, s.performChangeTestCases) 1281 } 1282 1283 func (s *allModelWatcherStateSuite) TestChangeUnitsNonNilPorts(c *gc.C) { 1284 testChangeUnitsNonNilPorts(c, s.owner, s.performChangeTestCases) 1285 } 1286 1287 func (s *allModelWatcherStateSuite) TestChangeRemoteApplications(c *gc.C) { 1288 testChangeRemoteApplications(c, s.performChangeTestCases) 1289 } 1290 1291 func (s *allModelWatcherStateSuite) TestChangeModels(c *gc.C) { 1292 changeTestFuncs := []changeTestFunc{ 1293 func(c *gc.C, st *State) changeTestCase { 1294 return changeTestCase{ 1295 about: "no model in state -> do nothing", 1296 change: watcher.Change{ 1297 C: "models", 1298 Id: "non-existing-uuid", 1299 }} 1300 }, 1301 func(c *gc.C, st *State) changeTestCase { 1302 return changeTestCase{ 1303 about: "model is removed if it's not in backing", 1304 initialContents: []multiwatcher.EntityInfo{&multiwatcher.ModelInfo{ 1305 ModelUUID: "some-uuid", 1306 }}, 1307 change: watcher.Change{ 1308 C: "models", 1309 Id: "some-uuid", 1310 }} 1311 }, 1312 func(c *gc.C, st *State) changeTestCase { 1313 model, err := st.Model() 1314 c.Assert(err, jc.ErrorIsNil) 1315 err = model.SetSLA("essential", "test-sla-owner", nil) 1316 c.Assert(err, jc.ErrorIsNil) 1317 cfg, err := model.Config() 1318 c.Assert(err, jc.ErrorIsNil) 1319 status, err := model.Status() 1320 c.Assert(err, jc.ErrorIsNil) 1321 cons := constraints.MustParse("mem=4G") 1322 err = st.SetModelConstraints(cons) 1323 c.Assert(err, jc.ErrorIsNil) 1324 credential, _ := model.CloudCredentialTag() 1325 1326 return changeTestCase{ 1327 about: "model is added if it's in backing but not in Store", 1328 change: watcher.Change{ 1329 C: "models", 1330 Id: st.ModelUUID(), 1331 }, 1332 expectContents: []multiwatcher.EntityInfo{ 1333 &multiwatcher.ModelInfo{ 1334 ModelUUID: model.UUID(), 1335 Type: coremodel.ModelType(model.Type()), 1336 Name: model.Name(), 1337 Life: life.Alive, 1338 Owner: model.Owner().Id(), 1339 ControllerUUID: model.ControllerUUID(), 1340 IsController: model.IsControllerModel(), 1341 Cloud: model.CloudName(), 1342 CloudRegion: model.CloudRegion(), 1343 CloudCredential: credential.Id(), 1344 Config: cfg.AllAttrs(), 1345 Constraints: cons, 1346 Status: multiwatcher.StatusInfo{ 1347 Current: status.Status, 1348 Message: status.Message, 1349 Data: status.Data, 1350 Since: status.Since, 1351 }, 1352 SLA: multiwatcher.ModelSLAInfo{ 1353 Level: "essential", 1354 Owner: "test-sla-owner", 1355 }, 1356 UserPermissions: map[string]permission.Access{ 1357 "test-admin": permission.AdminAccess, 1358 }, 1359 }}} 1360 }, 1361 func(c *gc.C, st *State) changeTestCase { 1362 model, err := st.Model() 1363 c.Assert(err, jc.ErrorIsNil) 1364 cfg, err := model.Config() 1365 c.Assert(err, jc.ErrorIsNil) 1366 status, err := model.Status() 1367 c.Assert(err, jc.ErrorIsNil) 1368 credential, _ := model.CloudCredentialTag() 1369 return changeTestCase{ 1370 about: "model is updated if it's in backing and in Store", 1371 initialContents: []multiwatcher.EntityInfo{ 1372 &multiwatcher.ModelInfo{ 1373 ModelUUID: model.UUID(), 1374 Name: "", 1375 Life: life.Alive, 1376 Owner: model.Owner().Id(), 1377 ControllerUUID: model.ControllerUUID(), 1378 IsController: model.IsControllerModel(), 1379 Config: cfg.AllAttrs(), 1380 Status: multiwatcher.StatusInfo{ 1381 Current: status.Status, 1382 Message: status.Message, 1383 Data: status.Data, 1384 Since: status.Since, 1385 }, 1386 SLA: multiwatcher.ModelSLAInfo{ 1387 Level: "unsupported", 1388 }, 1389 UserPermissions: map[string]permission.Access{ 1390 "test-admin": permission.AdminAccess, 1391 }, 1392 }, 1393 }, 1394 change: watcher.Change{ 1395 C: "models", 1396 Id: model.UUID(), 1397 }, 1398 expectContents: []multiwatcher.EntityInfo{ 1399 &multiwatcher.ModelInfo{ 1400 ModelUUID: model.UUID(), 1401 Type: coremodel.ModelType(model.Type()), 1402 Name: model.Name(), 1403 Life: life.Alive, 1404 Owner: model.Owner().Id(), 1405 ControllerUUID: model.ControllerUUID(), 1406 IsController: model.IsControllerModel(), 1407 Cloud: model.CloudName(), 1408 CloudRegion: model.CloudRegion(), 1409 CloudCredential: credential.Id(), 1410 Config: cfg.AllAttrs(), 1411 Status: multiwatcher.StatusInfo{ 1412 Current: status.Status, 1413 Message: status.Message, 1414 Data: status.Data, 1415 Since: status.Since, 1416 }, 1417 SLA: multiwatcher.ModelSLAInfo{ 1418 Level: "unsupported", 1419 }, 1420 UserPermissions: map[string]permission.Access{ 1421 "test-admin": permission.AdminAccess, 1422 }, 1423 }}} 1424 }, 1425 func(c *gc.C, st *State) changeTestCase { 1426 app := AddTestingApplication(c, st, "wordpress", AddTestingCharm(c, st, "wordpress")) 1427 err := app.SetConstraints(constraints.MustParse("mem=4G arch=amd64")) 1428 c.Assert(err, jc.ErrorIsNil) 1429 1430 return changeTestCase{ 1431 about: "status is changed if the application exists in the store", 1432 initialContents: []multiwatcher.EntityInfo{&multiwatcher.ApplicationInfo{ 1433 ModelUUID: st.ModelUUID(), 1434 Name: "wordpress", 1435 Constraints: constraints.MustParse("mem=99M cores=2 cpu-power=4"), 1436 }}, 1437 change: watcher.Change{ 1438 C: "constraints", 1439 Id: st.docID("a#wordpress"), 1440 }, 1441 expectContents: []multiwatcher.EntityInfo{ 1442 &multiwatcher.ApplicationInfo{ 1443 ModelUUID: st.ModelUUID(), 1444 Name: "wordpress", 1445 Constraints: constraints.MustParse("mem=4G arch=amd64"), 1446 }}} 1447 }, 1448 } 1449 s.performChangeTestCases(c, changeTestFuncs) 1450 } 1451 1452 func (s *allModelWatcherStateSuite) TestChangeForDeadModel(c *gc.C) { 1453 // Ensure an entity is removed when a change is seen but 1454 // the model the entity belonged to has already died. 1455 1456 b, err := NewAllWatcherBacking(s.pool) 1457 c.Assert(err, jc.ErrorIsNil) 1458 all := multiwatcher.NewStore(loggo.GetLogger("test")) 1459 1460 // Insert a machine for an model that doesn't actually 1461 // exist (mimics model removal). 1462 all.Update(&multiwatcher.MachineInfo{ 1463 ModelUUID: "uuid", 1464 ID: "0", 1465 }) 1466 c.Assert(all.All(), gc.HasLen, 1) 1467 1468 err = b.Changed(all, watcher.Change{ 1469 C: "machines", 1470 Id: ensureModelUUID("uuid", "0"), 1471 }) 1472 c.Assert(err, jc.ErrorIsNil) 1473 1474 // Entity info should be gone now. 1475 c.Assert(all.All(), gc.HasLen, 0) 1476 } 1477 1478 func (s *allModelWatcherStateSuite) TestModelSettings(c *gc.C) { 1479 // Init the test model. 1480 b, err := s.NewAllWatcherBacking() 1481 c.Assert(err, jc.ErrorIsNil) 1482 all := multiwatcher.NewStore(loggo.GetLogger("test")) 1483 setModelConfigAttr(c, s.state, "http-proxy", "http://invalid") 1484 setModelConfigAttr(c, s.state, "foo", "bar") 1485 1486 m, err := s.state.Model() 1487 c.Assert(err, jc.ErrorIsNil) 1488 1489 cfg, err := m.ModelConfig() 1490 c.Assert(err, jc.ErrorIsNil) 1491 expectedModelSettings := cfg.AllAttrs() 1492 expectedModelSettings["http-proxy"] = "http://invalid" 1493 expectedModelSettings["foo"] = "bar" 1494 1495 all.Update(&multiwatcher.ModelInfo{ 1496 ModelUUID: s.state.ModelUUID(), 1497 Name: "dummy-model", 1498 }) 1499 err = b.Changed(all, watcher.Change{ 1500 C: "settings", 1501 Id: s.state.docID(modelGlobalKey), 1502 }) 1503 c.Assert(err, jc.ErrorIsNil) 1504 entities := all.All() 1505 assertEntitiesEqual(c, entities, []multiwatcher.EntityInfo{ 1506 &multiwatcher.ModelInfo{ 1507 ModelUUID: s.state.ModelUUID(), 1508 Name: "dummy-model", 1509 Config: expectedModelSettings, 1510 }, 1511 }) 1512 } 1513 1514 // The testChange* funcs are extracted so the test cases can be used 1515 // to test both the allWatcher and allModelWatcher. 1516 1517 func testChangePermissions(c *gc.C, runChangeTests func(*gc.C, []changeTestFunc)) { 1518 changeTestFuncs := []changeTestFunc{ 1519 func(c *gc.C, st *State) changeTestCase { 1520 model, err := st.Model() 1521 c.Assert(err, jc.ErrorIsNil) 1522 credential, _ := model.CloudCredentialTag() 1523 1524 return changeTestCase{ 1525 about: "model update keeps permissions", 1526 initialContents: []multiwatcher.EntityInfo{&multiwatcher.ModelInfo{ 1527 ModelUUID: st.ModelUUID(), 1528 // Existence doesn't care about the other values, and they are 1529 // not entirely relevant to this test. 1530 UserPermissions: map[string]permission.Access{ 1531 "bob": permission.ReadAccess, 1532 "mary": permission.AdminAccess, 1533 }, 1534 }}, 1535 change: watcher.Change{ 1536 C: "models", 1537 Id: st.ModelUUID(), 1538 }, 1539 expectContents: []multiwatcher.EntityInfo{&multiwatcher.ModelInfo{ 1540 ModelUUID: st.ModelUUID(), 1541 Type: coremodel.ModelType(model.Type()), 1542 Name: model.Name(), 1543 Life: "alive", 1544 Owner: model.Owner().Id(), 1545 ControllerUUID: testing.ControllerTag.Id(), 1546 IsController: model.IsControllerModel(), 1547 Cloud: model.CloudName(), 1548 CloudRegion: model.CloudRegion(), 1549 CloudCredential: credential.Id(), 1550 SLA: multiwatcher.ModelSLAInfo{Level: "unsupported"}, 1551 UserPermissions: map[string]permission.Access{ 1552 "bob": permission.ReadAccess, 1553 "mary": permission.AdminAccess, 1554 }, 1555 }}} 1556 }, 1557 1558 func(c *gc.C, st *State) changeTestCase { 1559 model, err := st.Model() 1560 c.Assert(err, jc.ErrorIsNil) 1561 1562 _, err = model.AddUser(UserAccessSpec{ 1563 User: names.NewUserTag("tony@external"), 1564 CreatedBy: model.Owner(), 1565 Access: permission.WriteAccess, 1566 }) 1567 c.Assert(err, jc.ErrorIsNil) 1568 1569 return changeTestCase{ 1570 about: "adding a model user updates model", 1571 initialContents: []multiwatcher.EntityInfo{&multiwatcher.ModelInfo{ 1572 ModelUUID: st.ModelUUID(), 1573 Name: model.Name(), 1574 // Existence doesn't care about the other values, and they are 1575 // not entirely relevant to this test. 1576 UserPermissions: map[string]permission.Access{ 1577 "bob": permission.ReadAccess, 1578 "mary": permission.AdminAccess, 1579 }, 1580 }}, 1581 change: watcher.Change{ 1582 C: permissionsC, 1583 Id: permissionID(modelKey(st.ModelUUID()), userGlobalKey("tony@external")), 1584 }, 1585 expectContents: []multiwatcher.EntityInfo{&multiwatcher.ModelInfo{ 1586 ModelUUID: st.ModelUUID(), 1587 Name: model.Name(), 1588 // When the permissions are updated, only the user permissions are changed. 1589 UserPermissions: map[string]permission.Access{ 1590 "bob": permission.ReadAccess, 1591 "mary": permission.AdminAccess, 1592 "tony@external": permission.WriteAccess, 1593 }, 1594 }}} 1595 }, 1596 1597 func(c *gc.C, st *State) changeTestCase { 1598 model, err := st.Model() 1599 c.Assert(err, jc.ErrorIsNil) 1600 1601 return changeTestCase{ 1602 about: "removing a permission document removes user permission", 1603 initialContents: []multiwatcher.EntityInfo{&multiwatcher.ModelInfo{ 1604 ModelUUID: st.ModelUUID(), 1605 Name: model.Name(), 1606 // Existence doesn't care about the other values, and they are 1607 // not entirely relevant to this test. 1608 UserPermissions: map[string]permission.Access{ 1609 "bob": permission.ReadAccess, 1610 "mary": permission.AdminAccess, 1611 }, 1612 }}, 1613 change: watcher.Change{ 1614 C: permissionsC, 1615 Id: permissionID(modelKey(st.ModelUUID()), userGlobalKey("bob")), 1616 Revno: -1, 1617 }, 1618 expectContents: []multiwatcher.EntityInfo{&multiwatcher.ModelInfo{ 1619 ModelUUID: st.ModelUUID(), 1620 Name: model.Name(), 1621 // When the permissions are updated, only the user permissions are changed. 1622 UserPermissions: map[string]permission.Access{ 1623 "mary": permission.AdminAccess, 1624 }, 1625 }}} 1626 }, 1627 1628 func(c *gc.C, st *State) changeTestCase { 1629 model, err := st.Model() 1630 c.Assert(err, jc.ErrorIsNil) 1631 1632 // With the allModelWatcher variant, this function is called twice 1633 // within the same test loop, so we look for bob, and if not found, 1634 // add him in. 1635 bob, err := st.User(names.NewUserTag("bob")) 1636 if errors.IsNotFound(err) { 1637 bob, err = st.AddUser("bob", "", "pwd", "admin") 1638 } 1639 c.Assert(err, jc.ErrorIsNil) 1640 1641 _, err = model.AddUser(UserAccessSpec{ 1642 User: bob.UserTag(), 1643 CreatedBy: model.Owner(), 1644 Access: permission.WriteAccess, 1645 }) 1646 c.Assert(err, jc.ErrorIsNil) 1647 1648 return changeTestCase{ 1649 about: "updating a permission document updates user permission", 1650 initialContents: []multiwatcher.EntityInfo{&multiwatcher.ModelInfo{ 1651 ModelUUID: st.ModelUUID(), 1652 Name: model.Name(), 1653 // Existence doesn't care about the other values, and they are 1654 // not entirely relevant to this test. 1655 UserPermissions: map[string]permission.Access{ 1656 "bob": permission.ReadAccess, 1657 "mary": permission.AdminAccess, 1658 }, 1659 }}, 1660 change: watcher.Change{ 1661 C: permissionsC, 1662 Id: permissionID(modelKey(st.ModelUUID()), userGlobalKey("bob")), 1663 }, 1664 expectContents: []multiwatcher.EntityInfo{&multiwatcher.ModelInfo{ 1665 ModelUUID: st.ModelUUID(), 1666 Name: model.Name(), 1667 UserPermissions: map[string]permission.Access{ 1668 // Bob's permission updated to write. 1669 "bob": permission.WriteAccess, 1670 "mary": permission.AdminAccess, 1671 }, 1672 }}} 1673 }, 1674 } 1675 runChangeTests(c, changeTestFuncs) 1676 } 1677 1678 func testChangeAnnotations(c *gc.C, runChangeTests func(*gc.C, []changeTestFunc)) { 1679 changeTestFuncs := []changeTestFunc{ 1680 func(c *gc.C, st *State) changeTestCase { 1681 return changeTestCase{ 1682 about: "no annotation in state, no annotation in store -> do nothing", 1683 change: watcher.Change{ 1684 C: "annotations", 1685 Id: st.docID("m#0"), 1686 }} 1687 }, 1688 func(c *gc.C, st *State) changeTestCase { 1689 return changeTestCase{ 1690 about: "annotation is removed if it's not in backing", 1691 initialContents: []multiwatcher.EntityInfo{&multiwatcher.AnnotationInfo{ 1692 ModelUUID: st.ModelUUID(), 1693 Tag: "machine-0", 1694 }}, 1695 change: watcher.Change{ 1696 C: "annotations", 1697 Id: st.docID("m#0"), 1698 }} 1699 }, 1700 func(c *gc.C, st *State) changeTestCase { 1701 m, err := st.AddMachine(UbuntuBase("12.10"), JobHostUnits) 1702 c.Assert(err, jc.ErrorIsNil) 1703 model, err := st.Model() 1704 c.Assert(err, jc.ErrorIsNil) 1705 err = model.SetAnnotations(m, map[string]string{"foo": "bar", "arble": "baz"}) 1706 c.Assert(err, jc.ErrorIsNil) 1707 1708 return changeTestCase{ 1709 about: "annotation is added if it's in backing but not in Store", 1710 initialContents: []multiwatcher.EntityInfo{ 1711 &multiwatcher.MachineInfo{ 1712 ModelUUID: st.ModelUUID(), 1713 ID: "0", 1714 }, 1715 }, 1716 change: watcher.Change{ 1717 C: "annotations", 1718 Id: st.docID("m#0"), 1719 }, 1720 expectContents: []multiwatcher.EntityInfo{ 1721 &multiwatcher.MachineInfo{ 1722 ModelUUID: st.ModelUUID(), 1723 ID: "0", 1724 Annotations: map[string]string{"foo": "bar", "arble": "baz"}, 1725 }, 1726 &multiwatcher.AnnotationInfo{ 1727 ModelUUID: st.ModelUUID(), 1728 Tag: "machine-0", 1729 Annotations: map[string]string{"foo": "bar", "arble": "baz"}, 1730 }}} 1731 }, 1732 func(c *gc.C, st *State) changeTestCase { 1733 m, err := st.AddMachine(UbuntuBase("12.10"), JobHostUnits) 1734 c.Assert(err, jc.ErrorIsNil) 1735 model, err := st.Model() 1736 c.Assert(err, jc.ErrorIsNil) 1737 err = model.SetAnnotations(m, map[string]string{ 1738 "arble": "khroomph", 1739 "pretty": "", 1740 "new": "attr", 1741 }) 1742 c.Assert(err, jc.ErrorIsNil) 1743 1744 return changeTestCase{ 1745 about: "annotation is updated if it's in backing and in multiwatcher.Store", 1746 initialContents: []multiwatcher.EntityInfo{ 1747 &multiwatcher.MachineInfo{ 1748 ModelUUID: st.ModelUUID(), 1749 ID: "0", 1750 Annotations: map[string]string{ 1751 "arble": "baz", 1752 "foo": "bar", 1753 "pretty": "polly", 1754 }}, 1755 &multiwatcher.AnnotationInfo{ 1756 ModelUUID: st.ModelUUID(), 1757 Tag: "machine-0", 1758 Annotations: map[string]string{ 1759 "arble": "baz", 1760 "foo": "bar", 1761 "pretty": "polly", 1762 }, 1763 }}, 1764 change: watcher.Change{ 1765 C: "annotations", 1766 Id: st.docID("m#0"), 1767 }, 1768 expectContents: []multiwatcher.EntityInfo{ 1769 &multiwatcher.MachineInfo{ 1770 ModelUUID: st.ModelUUID(), 1771 ID: "0", 1772 Annotations: map[string]string{ 1773 "arble": "khroomph", 1774 "new": "attr", 1775 }}, 1776 &multiwatcher.AnnotationInfo{ 1777 ModelUUID: st.ModelUUID(), 1778 Tag: "machine-0", 1779 Annotations: map[string]string{ 1780 "arble": "khroomph", 1781 "new": "attr", 1782 }}}} 1783 }, 1784 } 1785 runChangeTests(c, changeTestFuncs) 1786 } 1787 1788 func testChangeMachines(c *gc.C, runChangeTests func(*gc.C, []changeTestFunc)) { 1789 changeTestFuncs := []changeTestFunc{ 1790 func(c *gc.C, st *State) changeTestCase { 1791 return changeTestCase{ 1792 about: "no machine in state -> do nothing", 1793 change: watcher.Change{ 1794 C: "statuses", 1795 Id: st.docID("m#0"), 1796 }} 1797 }, 1798 func(c *gc.C, st *State) changeTestCase { 1799 return changeTestCase{ 1800 about: "no machine in state, no machine in store -> do nothing", 1801 change: watcher.Change{ 1802 C: "machines", 1803 Id: st.docID("1"), 1804 }} 1805 }, 1806 func(c *gc.C, st *State) changeTestCase { 1807 return changeTestCase{ 1808 about: "machine is removed if it's not in backing", 1809 initialContents: []multiwatcher.EntityInfo{&multiwatcher.MachineInfo{ 1810 ModelUUID: st.ModelUUID(), 1811 ID: "1", 1812 }}, 1813 change: watcher.Change{ 1814 C: "machines", 1815 Id: st.docID("1"), 1816 }} 1817 }, 1818 func(c *gc.C, st *State) changeTestCase { 1819 m, err := st.AddMachine(UbuntuBase("12.10"), JobHostUnits) 1820 c.Assert(err, jc.ErrorIsNil) 1821 now := st.clock().Now() 1822 sInfo := status.StatusInfo{ 1823 Status: status.Error, 1824 Message: "failure", 1825 Since: &now, 1826 } 1827 err = m.SetStatus(sInfo) 1828 c.Assert(err, jc.ErrorIsNil) 1829 1830 return changeTestCase{ 1831 about: "machine is added if it's in backing but not in Store", 1832 change: watcher.Change{ 1833 C: "machines", 1834 Id: st.docID("0"), 1835 }, 1836 expectContents: []multiwatcher.EntityInfo{ 1837 &multiwatcher.MachineInfo{ 1838 ModelUUID: st.ModelUUID(), 1839 ID: "0", 1840 AgentStatus: multiwatcher.StatusInfo{ 1841 Current: status.Error, 1842 Message: "failure", 1843 Data: map[string]interface{}{}, 1844 Since: &now, 1845 }, 1846 InstanceStatus: multiwatcher.StatusInfo{ 1847 Current: status.Pending, 1848 Data: map[string]interface{}{}, 1849 Since: &now, 1850 }, 1851 Life: life.Alive, 1852 Base: "ubuntu@12.10", 1853 Jobs: []coremodel.MachineJob{JobHostUnits.ToParams()}, 1854 Addresses: []network.ProviderAddress{}, 1855 HasVote: false, 1856 WantsVote: false, 1857 }}} 1858 }, 1859 func(c *gc.C, st *State) changeTestCase { 1860 m, err := st.AddMachine(UbuntuBase("22.04"), JobHostUnits) 1861 c.Assert(err, jc.ErrorIsNil) 1862 err = m.SetProvisioned("i-0", "", "bootstrap_nonce", nil) 1863 c.Assert(err, jc.ErrorIsNil) 1864 err = m.SetSupportedContainers([]instance.ContainerType{instance.LXD}) 1865 c.Assert(err, jc.ErrorIsNil) 1866 1867 err = m.SetAgentVersion(version.MustParseBinary("2.4.1-ubuntu-amd64")) 1868 c.Assert(err, jc.ErrorIsNil) 1869 now := st.clock().Now() 1870 return changeTestCase{ 1871 about: "machine is updated if it's in backing and in Store", 1872 initialContents: []multiwatcher.EntityInfo{ 1873 &multiwatcher.MachineInfo{ 1874 ModelUUID: st.ModelUUID(), 1875 ID: "0", 1876 AgentStatus: multiwatcher.StatusInfo{ 1877 Current: status.Error, 1878 Message: "another failure", 1879 Data: map[string]interface{}{}, 1880 Since: &now, 1881 }, 1882 InstanceStatus: multiwatcher.StatusInfo{ 1883 Current: status.Pending, 1884 Data: map[string]interface{}{}, 1885 Since: &now, 1886 }, 1887 }, 1888 }, 1889 change: watcher.Change{ 1890 C: "machines", 1891 Id: st.docID("0"), 1892 }, 1893 expectContents: []multiwatcher.EntityInfo{ 1894 &multiwatcher.MachineInfo{ 1895 ModelUUID: st.ModelUUID(), 1896 ID: "0", 1897 InstanceID: "i-0", 1898 AgentStatus: multiwatcher.StatusInfo{ 1899 Current: status.Error, 1900 Message: "another failure", 1901 Data: map[string]interface{}{}, 1902 Version: "2.4.1", 1903 Since: &now, 1904 }, 1905 InstanceStatus: multiwatcher.StatusInfo{ 1906 Current: status.Pending, 1907 Data: map[string]interface{}{}, 1908 Since: &now, 1909 }, 1910 Life: life.Alive, 1911 Base: "ubuntu@22.04", 1912 Jobs: []coremodel.MachineJob{JobHostUnits.ToParams()}, 1913 Addresses: []network.ProviderAddress{}, 1914 HardwareCharacteristics: &instance.HardwareCharacteristics{}, 1915 CharmProfiles: []string{}, 1916 SupportedContainers: []instance.ContainerType{instance.LXD}, 1917 SupportedContainersKnown: true, 1918 }}} 1919 }, 1920 func(c *gc.C, st *State) changeTestCase { 1921 now := st.clock().Now() 1922 return changeTestCase{ 1923 about: "no change if status is not in backing", 1924 initialContents: []multiwatcher.EntityInfo{&multiwatcher.MachineInfo{ 1925 ModelUUID: st.ModelUUID(), 1926 ID: "0", 1927 AgentStatus: multiwatcher.StatusInfo{ 1928 Current: status.Error, 1929 Message: "failure", 1930 Data: map[string]interface{}{}, 1931 Since: &now, 1932 }, 1933 }}, 1934 change: watcher.Change{ 1935 C: "statuses", 1936 Id: st.docID("m#0"), 1937 }, 1938 expectContents: []multiwatcher.EntityInfo{ 1939 &multiwatcher.MachineInfo{ 1940 ModelUUID: st.ModelUUID(), 1941 ID: "0", 1942 AgentStatus: multiwatcher.StatusInfo{ 1943 Current: status.Error, 1944 Message: "failure", 1945 Data: map[string]interface{}{}, 1946 Since: &now, 1947 }, 1948 }}} 1949 }, 1950 func(c *gc.C, st *State) changeTestCase { 1951 m, err := st.AddMachine(UbuntuBase("12.10"), JobHostUnits) 1952 c.Assert(err, jc.ErrorIsNil) 1953 now := st.clock().Now() 1954 sInfo := status.StatusInfo{ 1955 Status: status.Started, 1956 Message: "", 1957 Since: &now, 1958 } 1959 err = m.SetStatus(sInfo) 1960 c.Assert(err, jc.ErrorIsNil) 1961 1962 return changeTestCase{ 1963 about: "status is changed if the machine exists in the store", 1964 initialContents: []multiwatcher.EntityInfo{&multiwatcher.MachineInfo{ 1965 ModelUUID: st.ModelUUID(), 1966 ID: "0", 1967 AgentStatus: multiwatcher.StatusInfo{ 1968 Current: status.Error, 1969 Message: "failure", 1970 Data: map[string]interface{}{}, 1971 Since: &now, 1972 }, 1973 }}, 1974 change: watcher.Change{ 1975 C: "statuses", 1976 Id: st.docID("m#0"), 1977 }, 1978 expectContents: []multiwatcher.EntityInfo{ 1979 &multiwatcher.MachineInfo{ 1980 ModelUUID: st.ModelUUID(), 1981 ID: "0", 1982 AgentStatus: multiwatcher.StatusInfo{ 1983 Current: status.Started, 1984 Data: make(map[string]interface{}), 1985 Since: &now, 1986 }, 1987 }}} 1988 }, 1989 func(c *gc.C, st *State) changeTestCase { 1990 return changeTestCase{ 1991 about: "no change if instanceData is not in backing", 1992 initialContents: []multiwatcher.EntityInfo{&multiwatcher.MachineInfo{ 1993 ModelUUID: st.ModelUUID(), 1994 ID: "0", 1995 }}, 1996 change: watcher.Change{ 1997 C: "instanceData", 1998 Id: st.docID("0"), 1999 }, 2000 expectContents: []multiwatcher.EntityInfo{ 2001 &multiwatcher.MachineInfo{ 2002 ModelUUID: st.ModelUUID(), 2003 ID: "0", 2004 }}} 2005 }, 2006 func(c *gc.C, st *State) changeTestCase { 2007 m, err := st.AddMachine(UbuntuBase("12.10"), JobHostUnits) 2008 c.Assert(err, jc.ErrorIsNil) 2009 2010 hc := &instance.HardwareCharacteristics{} 2011 err = m.SetProvisioned(instance.Id("i-"+m.Tag().String()), "", "fake_nonce", hc) 2012 c.Assert(err, jc.ErrorIsNil) 2013 2014 profiles := []string{"default, juju-default"} 2015 err = m.SetCharmProfiles(profiles) 2016 c.Assert(err, jc.ErrorIsNil) 2017 2018 return changeTestCase{ 2019 about: "instanceData is changed (CharmProfiles) if the machine exists in the store", 2020 initialContents: []multiwatcher.EntityInfo{&multiwatcher.MachineInfo{ 2021 ModelUUID: st.ModelUUID(), 2022 ID: "0", 2023 }}, 2024 change: watcher.Change{ 2025 C: "instanceData", 2026 Id: st.docID("0"), 2027 }, 2028 expectContents: []multiwatcher.EntityInfo{ 2029 &multiwatcher.MachineInfo{ 2030 ModelUUID: st.ModelUUID(), 2031 ID: "0", 2032 CharmProfiles: profiles, 2033 HardwareCharacteristics: hc, 2034 }}} 2035 }, 2036 } 2037 runChangeTests(c, changeTestFuncs) 2038 } 2039 2040 func testChangeRelations(c *gc.C, owner names.UserTag, runChangeTests func(*gc.C, []changeTestFunc)) { 2041 changeTestFuncs := []changeTestFunc{ 2042 func(c *gc.C, st *State) changeTestCase { 2043 return changeTestCase{ 2044 about: "no relation in state, no application in store -> do nothing", 2045 change: watcher.Change{ 2046 C: "relations", 2047 Id: st.docID("logging:logging-directory wordpress:logging-dir"), 2048 }} 2049 }, 2050 func(c *gc.C, st *State) changeTestCase { 2051 return changeTestCase{ 2052 about: "relation is removed if it's not in backing", 2053 initialContents: []multiwatcher.EntityInfo{&multiwatcher.RelationInfo{ 2054 ModelUUID: st.ModelUUID(), 2055 Key: "logging:logging-directory wordpress:logging-dir", 2056 }}, 2057 change: watcher.Change{ 2058 C: "relations", 2059 Id: st.docID("logging:logging-directory wordpress:logging-dir"), 2060 }} 2061 }, 2062 func(c *gc.C, st *State) changeTestCase { 2063 AddTestingApplication(c, st, "wordpress", AddTestingCharm(c, st, "wordpress")) 2064 AddTestingApplication(c, st, "logging", AddTestingCharm(c, st, "logging")) 2065 eps, err := st.InferEndpoints("logging", "wordpress") 2066 c.Assert(err, jc.ErrorIsNil) 2067 _, err = st.AddRelation(eps...) 2068 c.Assert(err, jc.ErrorIsNil) 2069 2070 return changeTestCase{ 2071 about: "relation is added if it's in backing but not in Store", 2072 change: watcher.Change{ 2073 C: "relations", 2074 Id: st.docID("logging:logging-directory wordpress:logging-dir"), 2075 }, 2076 expectContents: []multiwatcher.EntityInfo{ 2077 &multiwatcher.RelationInfo{ 2078 ModelUUID: st.ModelUUID(), 2079 Key: "logging:logging-directory wordpress:logging-dir", 2080 Endpoints: []multiwatcher.Endpoint{ 2081 {ApplicationName: "logging", Relation: multiwatcher.CharmRelation{Name: "logging-directory", Role: "requirer", Interface: "logging", Optional: false, Limit: 0, Scope: "container"}}, 2082 {ApplicationName: "wordpress", Relation: multiwatcher.CharmRelation{Name: "logging-dir", Role: "provider", Interface: "logging", Optional: false, Limit: 0, Scope: "container"}}}, 2083 }}} 2084 }, 2085 } 2086 runChangeTests(c, changeTestFuncs) 2087 } 2088 2089 func testChangeApplications(c *gc.C, owner names.UserTag, runChangeTests func(*gc.C, []changeTestFunc)) { 2090 // TODO(wallyworld) - add test for changing application status when that is implemented 2091 changeTestFuncs := []changeTestFunc{ 2092 // Applications. 2093 func(c *gc.C, st *State) changeTestCase { 2094 return changeTestCase{ 2095 about: "no application in state, no application in store -> do nothing", 2096 change: watcher.Change{ 2097 C: "applications", 2098 Id: st.docID("wordpress"), 2099 }} 2100 }, 2101 func(c *gc.C, st *State) changeTestCase { 2102 return changeTestCase{ 2103 about: "application is removed if it's not in backing", 2104 initialContents: []multiwatcher.EntityInfo{ 2105 &multiwatcher.ApplicationInfo{ 2106 ModelUUID: st.ModelUUID(), 2107 Name: "wordpress", 2108 }, 2109 }, 2110 change: watcher.Change{ 2111 C: "applications", 2112 Id: st.docID("wordpress"), 2113 }} 2114 }, 2115 func(c *gc.C, st *State) changeTestCase { 2116 wordpress := AddTestingApplication(c, st, "wordpress", AddTestingCharm(c, st, "wordpress")) 2117 err := wordpress.MergeExposeSettings(nil) 2118 c.Assert(err, jc.ErrorIsNil) 2119 err = wordpress.SetMinUnits(42) 2120 c.Assert(err, jc.ErrorIsNil) 2121 now := st.clock().Now() 2122 return changeTestCase{ 2123 about: "application is added if it's in backing but not in Store", 2124 change: watcher.Change{ 2125 C: "applications", 2126 Id: st.docID("wordpress"), 2127 }, 2128 expectContents: []multiwatcher.EntityInfo{ 2129 &multiwatcher.ApplicationInfo{ 2130 ModelUUID: st.ModelUUID(), 2131 Name: "wordpress", 2132 Exposed: true, 2133 CharmURL: "local:quantal/quantal-wordpress-3", 2134 Life: life.Alive, 2135 MinUnits: 42, 2136 Config: charm.Settings{}, 2137 Constraints: constraints.MustParse("arch=amd64"), 2138 Status: multiwatcher.StatusInfo{ 2139 Current: "unset", 2140 Message: "", 2141 Data: map[string]interface{}{}, 2142 Since: &now, 2143 }, 2144 }}} 2145 }, 2146 func(c *gc.C, st *State) changeTestCase { 2147 app := AddTestingApplication(c, st, "wordpress", AddTestingCharm(c, st, "wordpress")) 2148 setApplicationConfigAttr(c, app, "blog-title", "boring") 2149 2150 return changeTestCase{ 2151 about: "application is updated if it's in backing and in multiwatcher.Store", 2152 initialContents: []multiwatcher.EntityInfo{&multiwatcher.ApplicationInfo{ 2153 ModelUUID: st.ModelUUID(), 2154 Name: "wordpress", 2155 Exposed: true, 2156 CharmURL: "local:quantal/quantal-wordpress-3", 2157 MinUnits: 47, 2158 Constraints: constraints.MustParse("mem=99M"), 2159 Config: charm.Settings{"blog-title": "boring"}, 2160 }}, 2161 change: watcher.Change{ 2162 C: "applications", 2163 Id: st.docID("wordpress"), 2164 }, 2165 expectContents: []multiwatcher.EntityInfo{ 2166 &multiwatcher.ApplicationInfo{ 2167 ModelUUID: st.ModelUUID(), 2168 Name: "wordpress", 2169 CharmURL: "local:quantal/quantal-wordpress-3", 2170 Life: life.Alive, 2171 Constraints: constraints.MustParse("mem=99M"), 2172 Config: charm.Settings{"blog-title": "boring"}, 2173 }}} 2174 }, 2175 func(c *gc.C, st *State) changeTestCase { 2176 app := AddTestingApplication(c, st, "wordpress", AddTestingCharm(c, st, "wordpress")) 2177 unit, err := app.AddUnit(AddUnitParams{}) 2178 c.Assert(err, jc.ErrorIsNil) 2179 err = unit.SetWorkloadVersion("42.47") 2180 c.Assert(err, jc.ErrorIsNil) 2181 return changeTestCase{ 2182 about: "workload version is updated when set on a unit", 2183 initialContents: []multiwatcher.EntityInfo{ 2184 &multiwatcher.UnitInfo{ 2185 ModelUUID: st.ModelUUID(), 2186 Name: "wordpress/0", 2187 Application: "wordpress", 2188 }, 2189 &multiwatcher.ApplicationInfo{ 2190 ModelUUID: st.ModelUUID(), 2191 Name: "wordpress", 2192 CharmURL: "local:quantal/quantal-wordpress-3", 2193 }, 2194 }, 2195 change: watcher.Change{ 2196 C: "statuses", 2197 Id: st.docID("u#" + unit.Name() + "#charm#sat#workload-version"), 2198 }, 2199 expectContents: []multiwatcher.EntityInfo{ 2200 &multiwatcher.ApplicationInfo{ 2201 ModelUUID: st.ModelUUID(), 2202 Name: "wordpress", 2203 CharmURL: "local:quantal/quantal-wordpress-3", 2204 WorkloadVersion: "42.47", 2205 }, 2206 &multiwatcher.UnitInfo{ 2207 ModelUUID: st.ModelUUID(), 2208 Name: "wordpress/0", 2209 Application: "wordpress", 2210 }, 2211 }, 2212 } 2213 }, 2214 func(c *gc.C, st *State) changeTestCase { 2215 app := AddTestingApplication(c, st, "wordpress", AddTestingCharm(c, st, "wordpress")) 2216 unit, err := app.AddUnit(AddUnitParams{}) 2217 c.Assert(err, jc.ErrorIsNil) 2218 err = unit.SetWorkloadVersion("") 2219 c.Assert(err, jc.ErrorIsNil) 2220 return changeTestCase{ 2221 about: "workload version is not updated when empty", 2222 initialContents: []multiwatcher.EntityInfo{ 2223 &multiwatcher.UnitInfo{ 2224 ModelUUID: st.ModelUUID(), 2225 Name: "wordpress/0", 2226 Application: "wordpress", 2227 }, 2228 &multiwatcher.ApplicationInfo{ 2229 ModelUUID: st.ModelUUID(), 2230 Name: "wordpress", 2231 CharmURL: "local:quantal/quantal-wordpress-3", 2232 WorkloadVersion: "ultimate", 2233 }, 2234 }, 2235 change: watcher.Change{ 2236 C: "statuses", 2237 Id: st.docID("u#" + unit.Name() + "#charm#sat#workload-version"), 2238 }, 2239 expectContents: []multiwatcher.EntityInfo{ 2240 &multiwatcher.ApplicationInfo{ 2241 ModelUUID: st.ModelUUID(), 2242 Name: "wordpress", 2243 CharmURL: "local:quantal/quantal-wordpress-3", 2244 WorkloadVersion: "ultimate", 2245 }, 2246 &multiwatcher.UnitInfo{ 2247 ModelUUID: st.ModelUUID(), 2248 Name: "wordpress/0", 2249 Application: "wordpress", 2250 }, 2251 }, 2252 } 2253 }, 2254 func(c *gc.C, st *State) changeTestCase { 2255 app := AddTestingApplication(c, st, "wordpress", AddTestingCharm(c, st, "wordpress")) 2256 unit, err := app.AddUnit(AddUnitParams{}) 2257 c.Assert(err, jc.ErrorIsNil) 2258 err = unit.SetWorkloadVersion("42.47") 2259 c.Assert(err, jc.ErrorIsNil) 2260 return changeTestCase{ 2261 about: "workload version is ignored if there is no application info", 2262 initialContents: []multiwatcher.EntityInfo{ 2263 &multiwatcher.UnitInfo{ 2264 ModelUUID: st.ModelUUID(), 2265 Name: "wordpress/0", 2266 Application: "wordpress", 2267 }, 2268 }, 2269 change: watcher.Change{ 2270 C: "statuses", 2271 Id: st.docID("u#" + unit.Name() + "#charm#sat#workload-version"), 2272 }, 2273 expectContents: []multiwatcher.EntityInfo{ 2274 &multiwatcher.UnitInfo{ 2275 ModelUUID: st.ModelUUID(), 2276 Name: "wordpress/0", 2277 Application: "wordpress", 2278 }, 2279 }, 2280 } 2281 }, 2282 func(c *gc.C, st *State) changeTestCase { 2283 app := AddTestingApplication(c, st, "wordpress", AddTestingCharm(c, st, "wordpress")) 2284 setApplicationConfigAttr(c, app, "blog-title", "boring") 2285 2286 return changeTestCase{ 2287 about: "application re-reads config when charm URL changes", 2288 initialContents: []multiwatcher.EntityInfo{&multiwatcher.ApplicationInfo{ 2289 ModelUUID: st.ModelUUID(), 2290 Name: "wordpress", 2291 // Note: CharmURL has a different revision number from 2292 // the wordpress revision in the testing repo. 2293 CharmURL: "local:quantal/quantal-wordpress-2", 2294 Config: charm.Settings{"foo": "bar"}, 2295 }}, 2296 change: watcher.Change{ 2297 C: "applications", 2298 Id: st.docID("wordpress"), 2299 }, 2300 expectContents: []multiwatcher.EntityInfo{ 2301 &multiwatcher.ApplicationInfo{ 2302 ModelUUID: st.ModelUUID(), 2303 Name: "wordpress", 2304 CharmURL: "local:quantal/quantal-wordpress-3", 2305 Life: life.Alive, 2306 Config: charm.Settings{"blog-title": "boring"}, 2307 }}} 2308 }, 2309 // Settings. 2310 func(c *gc.C, st *State) changeTestCase { 2311 return changeTestCase{ 2312 about: "no application in state -> do nothing", 2313 change: watcher.Change{ 2314 C: "settings", 2315 Id: st.docID("a#dummy-application#local:quantal/quantal-dummy-1"), 2316 }} 2317 }, 2318 func(c *gc.C, st *State) changeTestCase { 2319 return changeTestCase{ 2320 about: "no change if application is not in backing", 2321 initialContents: []multiwatcher.EntityInfo{&multiwatcher.ApplicationInfo{ 2322 ModelUUID: st.ModelUUID(), 2323 Name: "dummy-application", 2324 CharmURL: "local:quantal/quantal-dummy-1", 2325 }}, 2326 change: watcher.Change{ 2327 C: "settings", 2328 Id: st.docID("a#dummy-application#local:quantal/quantal-dummy-1"), 2329 }, 2330 expectContents: []multiwatcher.EntityInfo{&multiwatcher.ApplicationInfo{ 2331 ModelUUID: st.ModelUUID(), 2332 Name: "dummy-application", 2333 CharmURL: "local:quantal/quantal-dummy-1", 2334 }}} 2335 }, 2336 func(c *gc.C, st *State) changeTestCase { 2337 app := AddTestingApplication(c, st, "dummy-application", AddTestingCharm(c, st, "dummy")) 2338 setApplicationConfigAttr(c, app, "username", "foo") 2339 setApplicationConfigAttr(c, app, "outlook", "foo@bar") 2340 2341 return changeTestCase{ 2342 about: "application config is changed if application exists in the store with the same URL", 2343 initialContents: []multiwatcher.EntityInfo{&multiwatcher.ApplicationInfo{ 2344 ModelUUID: st.ModelUUID(), 2345 Name: "dummy-application", 2346 CharmURL: "local:quantal/quantal-dummy-1", 2347 }}, 2348 change: watcher.Change{ 2349 C: "settings", 2350 Id: st.docID("a#dummy-application#local:quantal/quantal-dummy-1"), 2351 }, 2352 expectContents: []multiwatcher.EntityInfo{ 2353 &multiwatcher.ApplicationInfo{ 2354 ModelUUID: st.ModelUUID(), 2355 Name: "dummy-application", 2356 CharmURL: "local:quantal/quantal-dummy-1", 2357 Config: charm.Settings{"username": "foo", "outlook": "foo@bar"}, 2358 }}} 2359 }, 2360 func(c *gc.C, st *State) changeTestCase { 2361 app := AddTestingApplication(c, st, "dummy-application", AddTestingCharm(c, st, "dummy")) 2362 setApplicationConfigAttr(c, app, "username", "foo") 2363 setApplicationConfigAttr(c, app, "outlook", "foo@bar") 2364 setApplicationConfigAttr(c, app, "username", nil) 2365 2366 return changeTestCase{ 2367 about: "application config is changed after removing of a setting", 2368 initialContents: []multiwatcher.EntityInfo{&multiwatcher.ApplicationInfo{ 2369 ModelUUID: st.ModelUUID(), 2370 Name: "dummy-application", 2371 CharmURL: "local:quantal/quantal-dummy-1", 2372 Config: charm.Settings{"username": "foo", "outlook": "foo@bar"}, 2373 }}, 2374 change: watcher.Change{ 2375 C: "settings", 2376 Id: st.docID("a#dummy-application#local:quantal/quantal-dummy-1"), 2377 }, 2378 expectContents: []multiwatcher.EntityInfo{ 2379 &multiwatcher.ApplicationInfo{ 2380 ModelUUID: st.ModelUUID(), 2381 Name: "dummy-application", 2382 CharmURL: "local:quantal/quantal-dummy-1", 2383 Config: charm.Settings{"outlook": "foo@bar"}, 2384 }}} 2385 }, 2386 func(c *gc.C, st *State) changeTestCase { 2387 testCharm := AddCustomCharm( 2388 c, st, "dummy", 2389 "config.yaml", dottedConfig, 2390 "quantal", 1) 2391 app := AddTestingApplication(c, st, "dummy-application", testCharm) 2392 setApplicationConfigAttr(c, app, "key.dotted", "foo") 2393 2394 return changeTestCase{ 2395 about: "application config is unescaped when reading from the backing store", 2396 initialContents: []multiwatcher.EntityInfo{&multiwatcher.ApplicationInfo{ 2397 ModelUUID: st.ModelUUID(), 2398 Name: "dummy-application", 2399 CharmURL: "local:quantal/quantal-dummy-1", 2400 Config: charm.Settings{"key.dotted": "bar"}, 2401 }}, 2402 change: watcher.Change{ 2403 C: "settings", 2404 Id: st.docID("a#dummy-application#local:quantal/quantal-dummy-1"), 2405 }, 2406 expectContents: []multiwatcher.EntityInfo{ 2407 &multiwatcher.ApplicationInfo{ 2408 ModelUUID: st.ModelUUID(), 2409 Name: "dummy-application", 2410 CharmURL: "local:quantal/quantal-dummy-1", 2411 Config: charm.Settings{"key.dotted": "foo"}, 2412 }}} 2413 }, 2414 func(c *gc.C, st *State) changeTestCase { 2415 app := AddTestingApplication(c, st, "dummy-application", AddTestingCharm(c, st, "dummy")) 2416 setApplicationConfigAttr(c, app, "username", "foo") 2417 2418 return changeTestCase{ 2419 about: "application config is unchanged if application exists in the store with a different URL", 2420 initialContents: []multiwatcher.EntityInfo{&multiwatcher.ApplicationInfo{ 2421 ModelUUID: st.ModelUUID(), 2422 Name: "dummy-application", 2423 CharmURL: "local:quantal/quantal-dummy-2", // Note different revno. 2424 Config: charm.Settings{"username": "bar"}, 2425 }}, 2426 change: watcher.Change{ 2427 C: "settings", 2428 Id: st.docID("a#dummy-application#local:quantal/quantal-dummy-1"), 2429 }, 2430 expectContents: []multiwatcher.EntityInfo{ 2431 &multiwatcher.ApplicationInfo{ 2432 ModelUUID: st.ModelUUID(), 2433 Name: "dummy-application", 2434 CharmURL: "local:quantal/quantal-dummy-2", 2435 Config: charm.Settings{"username": "bar"}, 2436 }}} 2437 }, 2438 func(c *gc.C, st *State) changeTestCase { 2439 return changeTestCase{ 2440 about: "non-application config change is ignored", 2441 change: watcher.Change{ 2442 C: "settings", 2443 Id: st.docID("m#0"), 2444 }} 2445 }, 2446 func(c *gc.C, st *State) changeTestCase { 2447 return changeTestCase{ 2448 about: "application config change with no charm url is ignored", 2449 change: watcher.Change{ 2450 C: "settings", 2451 Id: st.docID("a#foo"), 2452 }} 2453 }, 2454 } 2455 runChangeTests(c, changeTestFuncs) 2456 } 2457 2458 func testChangeCharms(c *gc.C, owner names.UserTag, runChangeTests func(*gc.C, []changeTestFunc)) { 2459 changeTestFuncs := []changeTestFunc{ 2460 func(c *gc.C, st *State) changeTestCase { 2461 return changeTestCase{ 2462 about: "no charm in state, no charm in store -> do nothing", 2463 change: watcher.Change{ 2464 C: "charms", 2465 Id: st.docID("wordpress"), 2466 }} 2467 }, 2468 func(c *gc.C, st *State) changeTestCase { 2469 return changeTestCase{ 2470 about: "charm is removed if it's not in backing", 2471 initialContents: []multiwatcher.EntityInfo{ 2472 &multiwatcher.CharmInfo{ 2473 ModelUUID: st.ModelUUID(), 2474 CharmURL: "local:quantal/quantal-wordpress-2", 2475 }, 2476 }, 2477 change: watcher.Change{ 2478 C: "charms", 2479 Id: st.docID("local:quantal/quantal-wordpress-2"), 2480 }} 2481 }, 2482 func(c *gc.C, st *State) changeTestCase { 2483 ch := AddTestingCharm(c, st, "wordpress") 2484 return changeTestCase{ 2485 about: "charm is added if it's in backing but not in Store", 2486 change: watcher.Change{ 2487 C: "charms", 2488 Id: st.docID(ch.URL()), 2489 }, 2490 expectContents: []multiwatcher.EntityInfo{ 2491 &multiwatcher.CharmInfo{ 2492 ModelUUID: st.ModelUUID(), 2493 CharmURL: ch.URL(), 2494 Life: life.Alive, 2495 DefaultConfig: map[string]interface{}{"blog-title": "My Title"}, 2496 }}} 2497 }, 2498 } 2499 runChangeTests(c, changeTestFuncs) 2500 } 2501 2502 func testChangeApplicationsConstraints(c *gc.C, owner names.UserTag, runChangeTests func(*gc.C, []changeTestFunc)) { 2503 changeTestFuncs := []changeTestFunc{ 2504 func(c *gc.C, st *State) changeTestCase { 2505 return changeTestCase{ 2506 about: "no application in state -> do nothing", 2507 change: watcher.Change{ 2508 C: "constraints", 2509 Id: st.docID("a#wordpress"), 2510 }} 2511 }, 2512 func(c *gc.C, st *State) changeTestCase { 2513 return changeTestCase{ 2514 about: "no change if application is not in backing", 2515 initialContents: []multiwatcher.EntityInfo{&multiwatcher.ApplicationInfo{ 2516 ModelUUID: st.ModelUUID(), 2517 Name: "wordpress", 2518 Constraints: constraints.MustParse("mem=99M"), 2519 }}, 2520 change: watcher.Change{ 2521 C: "constraints", 2522 Id: st.docID("a#wordpress"), 2523 }, 2524 expectContents: []multiwatcher.EntityInfo{&multiwatcher.ApplicationInfo{ 2525 ModelUUID: st.ModelUUID(), 2526 Name: "wordpress", 2527 Constraints: constraints.MustParse("mem=99M"), 2528 }}} 2529 }, 2530 func(c *gc.C, st *State) changeTestCase { 2531 app := AddTestingApplication(c, st, "wordpress", AddTestingCharm(c, st, "wordpress")) 2532 err := app.SetConstraints(constraints.MustParse("mem=4G arch=amd64")) 2533 c.Assert(err, jc.ErrorIsNil) 2534 2535 return changeTestCase{ 2536 about: "status is changed if the application exists in the store", 2537 initialContents: []multiwatcher.EntityInfo{&multiwatcher.ApplicationInfo{ 2538 ModelUUID: st.ModelUUID(), 2539 Name: "wordpress", 2540 Constraints: constraints.MustParse("mem=99M cores=2 cpu-power=4"), 2541 }}, 2542 change: watcher.Change{ 2543 C: "constraints", 2544 Id: st.docID("a#wordpress"), 2545 }, 2546 expectContents: []multiwatcher.EntityInfo{ 2547 &multiwatcher.ApplicationInfo{ 2548 ModelUUID: st.ModelUUID(), 2549 Name: "wordpress", 2550 Constraints: constraints.MustParse("mem=4G arch=amd64"), 2551 }}} 2552 }, 2553 } 2554 runChangeTests(c, changeTestFuncs) 2555 } 2556 2557 func testChangeUnits(c *gc.C, owner names.UserTag, runChangeTests func(*gc.C, []changeTestFunc)) { 2558 changeTestFuncs := []changeTestFunc{ 2559 func(c *gc.C, st *State) changeTestCase { 2560 return changeTestCase{ 2561 about: "no unit in state, no unit in store -> do nothing", 2562 change: watcher.Change{ 2563 C: "units", 2564 Id: st.docID("1"), 2565 }} 2566 }, 2567 func(c *gc.C, st *State) changeTestCase { 2568 return changeTestCase{ 2569 about: "unit is removed if it's not in backing", 2570 initialContents: []multiwatcher.EntityInfo{ 2571 &multiwatcher.UnitInfo{ 2572 ModelUUID: st.ModelUUID(), 2573 Name: "wordpress/1", 2574 Life: life.Alive, 2575 }, 2576 }, 2577 change: watcher.Change{ 2578 C: "units", 2579 Id: st.docID("wordpress/1"), 2580 }} 2581 }, 2582 func(c *gc.C, st *State) changeTestCase { 2583 wordpress := AddTestingApplication(c, st, "wordpress", AddTestingCharm(c, st, "wordpress")) 2584 u, err := wordpress.AddUnit(AddUnitParams{}) 2585 c.Assert(err, jc.ErrorIsNil) 2586 m, err := st.AddMachine(UbuntuBase("12.10"), JobHostUnits) 2587 c.Assert(err, jc.ErrorIsNil) 2588 err = u.AssignToMachine(m) 2589 c.Assert(err, jc.ErrorIsNil) 2590 MustOpenUnitPortRanges(c, u.st, m, u.Name(), allEndpoints, []corenetwork.PortRange{ 2591 corenetwork.MustParsePortRange("12345/tcp"), 2592 corenetwork.MustParsePortRange("54321/udp"), 2593 corenetwork.MustParsePortRange("5555-5558/tcp"), 2594 }) 2595 c.Assert(err, jc.ErrorIsNil) 2596 now := st.clock().Now() 2597 sInfo := status.StatusInfo{ 2598 Status: status.Error, 2599 Message: "failure", 2600 Since: &now, 2601 } 2602 err = u.SetAgentStatus(sInfo) 2603 c.Assert(err, jc.ErrorIsNil) 2604 2605 return changeTestCase{ 2606 about: "unit is added if it's in backing but not in Store", 2607 change: watcher.Change{ 2608 C: "units", 2609 Id: st.docID("wordpress/0"), 2610 }, 2611 expectContents: []multiwatcher.EntityInfo{ 2612 &multiwatcher.UnitInfo{ 2613 ModelUUID: st.ModelUUID(), 2614 Name: "wordpress/0", 2615 Application: "wordpress", 2616 Base: "ubuntu@12.10", 2617 Life: life.Alive, 2618 MachineID: "0", 2619 OpenPortRangesByEndpoint: network.GroupedPortRanges{ 2620 allEndpoints: { 2621 corenetwork.MustParsePortRange("5555-5558/tcp"), 2622 corenetwork.MustParsePortRange("12345/tcp"), 2623 corenetwork.MustParsePortRange("54321/udp"), 2624 }, 2625 }, 2626 AgentStatus: multiwatcher.StatusInfo{ 2627 Current: "idle", 2628 Message: "", 2629 Data: map[string]interface{}{}, 2630 Since: &now, 2631 }, 2632 WorkloadStatus: multiwatcher.StatusInfo{ 2633 Current: "error", 2634 Message: "failure", 2635 Data: map[string]interface{}{}, 2636 Since: &now, 2637 }, 2638 }}} 2639 }, 2640 func(c *gc.C, st *State) changeTestCase { 2641 wordpress := AddTestingApplication(c, st, "wordpress", AddTestingCharm(c, st, "wordpress")) 2642 u, err := wordpress.AddUnit(AddUnitParams{}) 2643 c.Assert(err, jc.ErrorIsNil) 2644 m, err := st.AddMachine(UbuntuBase("12.10"), JobHostUnits) 2645 c.Assert(err, jc.ErrorIsNil) 2646 err = u.AssignToMachine(m) 2647 c.Assert(err, jc.ErrorIsNil) 2648 MustOpenUnitPortRange(c, st, m, u.Name(), allEndpoints, corenetwork.MustParsePortRange("17070/udp")) 2649 err = u.SetAgentVersion(version.MustParseBinary("2.4.1-ubuntu-amd64")) 2650 c.Assert(err, jc.ErrorIsNil) 2651 now := st.clock().Now() 2652 2653 return changeTestCase{ 2654 about: "unit is updated if it's in backing and in multiwatcher.Store", 2655 initialContents: []multiwatcher.EntityInfo{&multiwatcher.UnitInfo{ 2656 ModelUUID: st.ModelUUID(), 2657 Name: "wordpress/0", 2658 AgentStatus: multiwatcher.StatusInfo{ 2659 Current: "idle", 2660 Message: "", 2661 Data: map[string]interface{}{}, 2662 Since: &now, 2663 }, 2664 WorkloadStatus: multiwatcher.StatusInfo{ 2665 Current: "error", 2666 Message: "another failure", 2667 Data: map[string]interface{}{}, 2668 Since: &now, 2669 }, 2670 OpenPortRangesByEndpoint: network.GroupedPortRanges{ 2671 allEndpoints: { 2672 corenetwork.MustParsePortRange("17070/udp"), 2673 }, 2674 }, 2675 }}, 2676 change: watcher.Change{ 2677 C: "units", 2678 Id: st.docID("wordpress/0"), 2679 }, 2680 expectContents: []multiwatcher.EntityInfo{ 2681 &multiwatcher.UnitInfo{ 2682 ModelUUID: st.ModelUUID(), 2683 Name: "wordpress/0", 2684 Application: "wordpress", 2685 Base: "ubuntu@12.10", 2686 Life: life.Alive, 2687 MachineID: "0", 2688 OpenPortRangesByEndpoint: network.GroupedPortRanges{ 2689 allEndpoints: { 2690 corenetwork.MustParsePortRange("17070/udp"), 2691 }, 2692 }, 2693 AgentStatus: multiwatcher.StatusInfo{ 2694 Current: "idle", 2695 Message: "", 2696 Data: map[string]interface{}{}, 2697 Version: "2.4.1", 2698 Since: &now, 2699 }, 2700 WorkloadStatus: multiwatcher.StatusInfo{ 2701 Current: "error", 2702 Message: "another failure", 2703 Data: map[string]interface{}{}, 2704 Since: &now, 2705 }, 2706 }}} 2707 }, 2708 func(c *gc.C, st *State) changeTestCase { 2709 wordpress := AddTestingApplication(c, st, "wordpress", AddTestingCharm(c, st, "wordpress")) 2710 u, err := wordpress.AddUnit(AddUnitParams{}) 2711 c.Assert(err, jc.ErrorIsNil) 2712 m, err := st.AddMachine(UbuntuBase("12.10"), JobHostUnits) 2713 c.Assert(err, jc.ErrorIsNil) 2714 err = u.AssignToMachine(m) 2715 c.Assert(err, jc.ErrorIsNil) 2716 MustOpenUnitPortRange(c, st, m, u.Name(), allEndpoints, corenetwork.MustParsePortRange("4242/tcp")) 2717 2718 return changeTestCase{ 2719 about: "unit info is updated if a port is opened on the machine it is placed in", 2720 initialContents: []multiwatcher.EntityInfo{ 2721 &multiwatcher.UnitInfo{ 2722 ModelUUID: st.ModelUUID(), 2723 Name: "wordpress/0", 2724 }, 2725 &multiwatcher.MachineInfo{ 2726 ModelUUID: st.ModelUUID(), 2727 ID: "0", 2728 }, 2729 }, 2730 change: watcher.Change{ 2731 C: openedPortsC, 2732 Id: st.docID("0"), 2733 }, 2734 expectContents: []multiwatcher.EntityInfo{ 2735 &multiwatcher.UnitInfo{ 2736 ModelUUID: st.ModelUUID(), 2737 Name: "wordpress/0", 2738 OpenPortRangesByEndpoint: network.GroupedPortRanges{ 2739 allEndpoints: { 2740 corenetwork.MustParsePortRange("4242/tcp"), 2741 }, 2742 }, 2743 }, 2744 &multiwatcher.MachineInfo{ 2745 ModelUUID: st.ModelUUID(), 2746 ID: "0", 2747 }, 2748 }} 2749 }, 2750 func(c *gc.C, st *State) changeTestCase { 2751 wordpress := AddTestingApplication(c, st, "wordpress", AddTestingCharm(c, st, "wordpress")) 2752 u, err := wordpress.AddUnit(AddUnitParams{}) 2753 c.Assert(err, jc.ErrorIsNil) 2754 m, err := st.AddMachine(UbuntuBase("12.10"), JobHostUnits) 2755 c.Assert(err, jc.ErrorIsNil) 2756 err = u.AssignToMachine(m) 2757 c.Assert(err, jc.ErrorIsNil) 2758 MustOpenUnitPortRange(c, st, m, u.Name(), allEndpoints, corenetwork.MustParsePortRange("21-22/tcp")) 2759 now := st.clock().Now() 2760 return changeTestCase{ 2761 about: "unit is created if a port is opened on the machine it is placed in", 2762 initialContents: []multiwatcher.EntityInfo{ 2763 &multiwatcher.MachineInfo{ 2764 ModelUUID: st.ModelUUID(), 2765 ID: "0", 2766 }, 2767 }, 2768 change: watcher.Change{ 2769 C: "units", 2770 Id: st.docID("wordpress/0"), 2771 }, 2772 expectContents: []multiwatcher.EntityInfo{ 2773 &multiwatcher.UnitInfo{ 2774 ModelUUID: st.ModelUUID(), 2775 Name: "wordpress/0", 2776 Application: "wordpress", 2777 Base: "ubuntu@12.10", 2778 Life: life.Alive, 2779 MachineID: "0", 2780 WorkloadStatus: multiwatcher.StatusInfo{ 2781 Current: "waiting", 2782 Message: "waiting for machine", 2783 Data: map[string]interface{}{}, 2784 Since: &now, 2785 }, 2786 AgentStatus: multiwatcher.StatusInfo{ 2787 Current: "allocating", 2788 Data: map[string]interface{}{}, 2789 Since: &now, 2790 }, 2791 OpenPortRangesByEndpoint: network.GroupedPortRanges{ 2792 allEndpoints: { 2793 corenetwork.MustParsePortRange("21-22/tcp"), 2794 }, 2795 }, 2796 }, 2797 &multiwatcher.MachineInfo{ 2798 ModelUUID: st.ModelUUID(), 2799 ID: "0", 2800 }, 2801 }} 2802 }, 2803 func(c *gc.C, st *State) changeTestCase { 2804 wordpress := AddTestingApplication(c, st, "wordpress", AddTestingCharm(c, st, "wordpress")) 2805 u, err := wordpress.AddUnit(AddUnitParams{}) 2806 c.Assert(err, jc.ErrorIsNil) 2807 m, err := st.AddMachine(UbuntuBase("12.10"), JobHostUnits) 2808 c.Assert(err, jc.ErrorIsNil) 2809 err = u.AssignToMachine(m) 2810 c.Assert(err, jc.ErrorIsNil) 2811 MustOpenUnitPortRange(c, st, m, u.Name(), allEndpoints, corenetwork.MustParsePortRange("12345/tcp")) 2812 publicAddress := network.NewSpaceAddress("public", corenetwork.WithScope(corenetwork.ScopePublic)) 2813 privateAddress := network.NewSpaceAddress("private", corenetwork.WithScope(corenetwork.ScopeCloudLocal)) 2814 now := st.clock().Now() 2815 sInfo := status.StatusInfo{ 2816 Status: status.Error, 2817 Message: "failure", 2818 Since: &now, 2819 } 2820 err = u.SetAgentStatus(sInfo) 2821 c.Assert(err, jc.ErrorIsNil) 2822 2823 return changeTestCase{ 2824 about: "unit addresses are read from the assigned machine for recent Juju releases", 2825 initialContents: []multiwatcher.EntityInfo{ 2826 &multiwatcher.MachineInfo{ 2827 ModelUUID: st.ModelUUID(), 2828 ID: "0", 2829 PreferredPublicAddress: publicAddress, 2830 PreferredPrivateAddress: privateAddress, 2831 }, 2832 }, 2833 change: watcher.Change{ 2834 C: "units", 2835 Id: st.docID("wordpress/0"), 2836 }, 2837 expectContents: []multiwatcher.EntityInfo{ 2838 &multiwatcher.UnitInfo{ 2839 ModelUUID: st.ModelUUID(), 2840 Name: "wordpress/0", 2841 Application: "wordpress", 2842 Base: "ubuntu@12.10", 2843 Life: life.Alive, 2844 PublicAddress: "public", 2845 PrivateAddress: "private", 2846 MachineID: "0", 2847 OpenPortRangesByEndpoint: network.GroupedPortRanges{ 2848 allEndpoints: { 2849 corenetwork.MustParsePortRange("12345/tcp"), 2850 }, 2851 }, 2852 AgentStatus: multiwatcher.StatusInfo{ 2853 Current: "idle", 2854 Message: "", 2855 Data: map[string]interface{}{}, 2856 Since: &now, 2857 }, 2858 WorkloadStatus: multiwatcher.StatusInfo{ 2859 Current: "error", 2860 Message: "failure", 2861 Data: map[string]interface{}{}, 2862 Since: &now, 2863 }, 2864 }, 2865 &multiwatcher.MachineInfo{ 2866 ModelUUID: st.ModelUUID(), 2867 ID: "0", 2868 PreferredPublicAddress: publicAddress, 2869 PreferredPrivateAddress: privateAddress, 2870 }, 2871 }, 2872 } 2873 }, 2874 func(c *gc.C, st *State) changeTestCase { 2875 return changeTestCase{ 2876 about: "no unit in state -> do nothing", 2877 change: watcher.Change{ 2878 C: "statuses", 2879 Id: st.docID("u#wordpress/0"), 2880 }} 2881 }, 2882 func(c *gc.C, st *State) changeTestCase { 2883 now := st.clock().Now() 2884 return changeTestCase{ 2885 about: "no change if status is not in backing", 2886 initialContents: []multiwatcher.EntityInfo{&multiwatcher.UnitInfo{ 2887 ModelUUID: st.ModelUUID(), 2888 Name: "wordpress/0", 2889 Application: "wordpress", 2890 AgentStatus: multiwatcher.StatusInfo{ 2891 Current: "idle", 2892 Message: "", 2893 Data: map[string]interface{}{}, 2894 Since: &now, 2895 }, 2896 WorkloadStatus: multiwatcher.StatusInfo{ 2897 Current: "error", 2898 Message: "failure", 2899 Data: map[string]interface{}{}, 2900 Since: &now, 2901 }, 2902 }}, 2903 change: watcher.Change{ 2904 C: "statuses", 2905 Id: st.docID("u#wordpress/0"), 2906 }, 2907 expectContents: []multiwatcher.EntityInfo{ 2908 &multiwatcher.UnitInfo{ 2909 ModelUUID: st.ModelUUID(), 2910 Name: "wordpress/0", 2911 Application: "wordpress", 2912 AgentStatus: multiwatcher.StatusInfo{ 2913 Current: "idle", 2914 Message: "", 2915 Data: map[string]interface{}{}, 2916 Since: &now, 2917 }, 2918 WorkloadStatus: multiwatcher.StatusInfo{ 2919 Current: "error", 2920 Message: "failure", 2921 Data: map[string]interface{}{}, 2922 Since: &now, 2923 }, 2924 }}} 2925 }, 2926 func(c *gc.C, st *State) changeTestCase { 2927 wordpress := AddTestingApplication(c, st, "wordpress", AddTestingCharm(c, st, "wordpress")) 2928 u, err := wordpress.AddUnit(AddUnitParams{}) 2929 c.Assert(err, jc.ErrorIsNil) 2930 err = u.AssignToNewMachine() 2931 c.Assert(err, jc.ErrorIsNil) 2932 now := st.clock().Now() 2933 sInfo := status.StatusInfo{ 2934 Status: status.Idle, 2935 Message: "", 2936 Since: &now, 2937 } 2938 err = u.SetAgentStatus(sInfo) 2939 c.Assert(err, jc.ErrorIsNil) 2940 2941 return changeTestCase{ 2942 about: "status is changed if the unit exists in the store", 2943 initialContents: []multiwatcher.EntityInfo{&multiwatcher.UnitInfo{ 2944 ModelUUID: st.ModelUUID(), 2945 Name: "wordpress/0", 2946 Application: "wordpress", 2947 AgentStatus: multiwatcher.StatusInfo{ 2948 Current: "idle", 2949 Message: "", 2950 Data: map[string]interface{}{}, 2951 Since: &now, 2952 }, 2953 WorkloadStatus: multiwatcher.StatusInfo{ 2954 Current: "maintenance", 2955 Message: "working", 2956 Data: map[string]interface{}{}, 2957 Since: &now, 2958 }, 2959 }}, 2960 change: watcher.Change{ 2961 C: "statuses", 2962 Id: st.docID("u#wordpress/0"), 2963 }, 2964 expectContents: []multiwatcher.EntityInfo{ 2965 &multiwatcher.UnitInfo{ 2966 ModelUUID: st.ModelUUID(), 2967 Name: "wordpress/0", 2968 Application: "wordpress", 2969 WorkloadStatus: multiwatcher.StatusInfo{ 2970 Current: "maintenance", 2971 Message: "working", 2972 Data: map[string]interface{}{}, 2973 Since: &now, 2974 }, 2975 AgentStatus: multiwatcher.StatusInfo{ 2976 Current: "idle", 2977 Message: "", 2978 Data: map[string]interface{}{}, 2979 Since: &now, 2980 }, 2981 }}} 2982 }, 2983 func(c *gc.C, st *State) changeTestCase { 2984 wordpress := AddTestingApplication(c, st, "wordpress", AddTestingCharm(c, st, "wordpress")) 2985 u, err := wordpress.AddUnit(AddUnitParams{}) 2986 c.Assert(err, jc.ErrorIsNil) 2987 now := st.clock().Now() 2988 sInfo := status.StatusInfo{ 2989 Status: status.Idle, 2990 Message: "", 2991 Since: &now, 2992 } 2993 err = u.AssignToNewMachine() 2994 c.Assert(err, jc.ErrorIsNil) 2995 err = u.SetAgentStatus(sInfo) 2996 c.Assert(err, jc.ErrorIsNil) 2997 sInfo = status.StatusInfo{ 2998 Status: status.Maintenance, 2999 Message: "doing work", 3000 Since: &now, 3001 } 3002 err = u.SetStatus(sInfo) 3003 c.Assert(err, jc.ErrorIsNil) 3004 3005 return changeTestCase{ 3006 about: "unit status is changed if the agent comes off error state", 3007 initialContents: []multiwatcher.EntityInfo{&multiwatcher.UnitInfo{ 3008 ModelUUID: st.ModelUUID(), 3009 Name: "wordpress/0", 3010 Application: "wordpress", 3011 AgentStatus: multiwatcher.StatusInfo{ 3012 Current: "idle", 3013 Message: "", 3014 Data: map[string]interface{}{}, 3015 Since: &now, 3016 }, 3017 WorkloadStatus: multiwatcher.StatusInfo{ 3018 Current: "error", 3019 Message: "failure", 3020 Data: map[string]interface{}{}, 3021 Since: &now, 3022 }, 3023 }}, 3024 change: watcher.Change{ 3025 C: "statuses", 3026 Id: st.docID("u#wordpress/0"), 3027 }, 3028 expectContents: []multiwatcher.EntityInfo{ 3029 &multiwatcher.UnitInfo{ 3030 ModelUUID: st.ModelUUID(), 3031 Name: "wordpress/0", 3032 Application: "wordpress", 3033 WorkloadStatus: multiwatcher.StatusInfo{ 3034 Current: "maintenance", 3035 Message: "doing work", 3036 Data: map[string]interface{}{}, 3037 Since: &now, 3038 }, 3039 AgentStatus: multiwatcher.StatusInfo{ 3040 Current: "idle", 3041 Message: "", 3042 Data: map[string]interface{}{}, 3043 Since: &now, 3044 }, 3045 }}} 3046 }, 3047 func(c *gc.C, st *State) changeTestCase { 3048 wordpress := AddTestingApplication(c, st, "wordpress", AddTestingCharm(c, st, "wordpress")) 3049 u, err := wordpress.AddUnit(AddUnitParams{}) 3050 c.Assert(err, jc.ErrorIsNil) 3051 now := st.clock().Now() 3052 sInfo := status.StatusInfo{ 3053 Status: status.Error, 3054 Message: "hook error", 3055 Data: map[string]interface{}{ 3056 "1st-key": "one", 3057 "2nd-key": 2, 3058 "3rd-key": true, 3059 }, 3060 Since: &now, 3061 } 3062 err = u.SetAgentStatus(sInfo) 3063 c.Assert(err, jc.ErrorIsNil) 3064 3065 return changeTestCase{ 3066 about: "agent status is changed with additional status data", 3067 initialContents: []multiwatcher.EntityInfo{&multiwatcher.UnitInfo{ 3068 ModelUUID: st.ModelUUID(), 3069 Name: "wordpress/0", 3070 Application: "wordpress", 3071 AgentStatus: multiwatcher.StatusInfo{ 3072 Current: "idle", 3073 Message: "", 3074 Data: map[string]interface{}{}, 3075 Since: &now, 3076 }, 3077 WorkloadStatus: multiwatcher.StatusInfo{ 3078 Current: "active", 3079 Since: &now, 3080 }, 3081 }}, 3082 change: watcher.Change{ 3083 C: "statuses", 3084 Id: st.docID("u#wordpress/0"), 3085 }, 3086 expectContents: []multiwatcher.EntityInfo{ 3087 &multiwatcher.UnitInfo{ 3088 ModelUUID: st.ModelUUID(), 3089 Name: "wordpress/0", 3090 Application: "wordpress", 3091 WorkloadStatus: multiwatcher.StatusInfo{ 3092 Current: "error", 3093 Message: "hook error", 3094 Data: map[string]interface{}{ 3095 "1st-key": "one", 3096 "2nd-key": 2, 3097 "3rd-key": true, 3098 }, 3099 Since: &now, 3100 }, 3101 AgentStatus: multiwatcher.StatusInfo{ 3102 Current: "idle", 3103 Message: "", 3104 Data: map[string]interface{}{}, 3105 Since: &now, 3106 }, 3107 }}} 3108 }, 3109 func(c *gc.C, st *State) changeTestCase { 3110 wordpress := AddTestingApplication(c, st, "wordpress", AddTestingCharm(c, st, "wordpress")) 3111 u, err := wordpress.AddUnit(AddUnitParams{}) 3112 c.Assert(err, jc.ErrorIsNil) 3113 now := st.clock().Now() 3114 sInfo := status.StatusInfo{ 3115 Status: status.Error, 3116 Message: "hook error", 3117 Data: map[string]interface{}{ 3118 "1st-key": "one", 3119 "2nd-key": 2, 3120 "3rd-key": true, 3121 }, 3122 Since: &now, 3123 } 3124 err = u.SetAgentStatus(sInfo) 3125 c.Assert(err, jc.ErrorIsNil) 3126 3127 return changeTestCase{ 3128 about: "workload status takes into account agent error", 3129 initialContents: []multiwatcher.EntityInfo{&multiwatcher.UnitInfo{ 3130 ModelUUID: st.ModelUUID(), 3131 Name: "wordpress/0", 3132 Application: "wordpress", 3133 WorkloadStatus: multiwatcher.StatusInfo{ 3134 Current: "error", 3135 Message: "hook error", 3136 Data: map[string]interface{}{ 3137 "1st-key": "one", 3138 "2nd-key": 2, 3139 "3rd-key": true, 3140 }, 3141 Since: &now, 3142 }, 3143 AgentStatus: multiwatcher.StatusInfo{ 3144 Current: "idle", 3145 Message: "", 3146 Data: map[string]interface{}{}, 3147 Since: &now, 3148 }, 3149 }}, 3150 change: watcher.Change{ 3151 C: "statuses", 3152 Id: st.docID("u#wordpress/0#charm"), 3153 }, 3154 expectContents: []multiwatcher.EntityInfo{ 3155 &multiwatcher.UnitInfo{ 3156 ModelUUID: st.ModelUUID(), 3157 Name: "wordpress/0", 3158 Application: "wordpress", 3159 WorkloadStatus: multiwatcher.StatusInfo{ 3160 Current: "error", 3161 Message: "hook error", 3162 Data: map[string]interface{}{ 3163 "1st-key": "one", 3164 "2nd-key": 2, 3165 "3rd-key": true, 3166 }, 3167 Since: &now, 3168 }, 3169 AgentStatus: multiwatcher.StatusInfo{ 3170 Current: "idle", 3171 Message: "", 3172 Data: map[string]interface{}{}, 3173 Since: &now, 3174 }, 3175 }}} 3176 }, 3177 } 3178 runChangeTests(c, changeTestFuncs) 3179 } 3180 3181 // initFlag helps to control the different test scenarios. 3182 type initFlag int 3183 3184 const ( 3185 assignUnit initFlag = 1 3186 openPorts initFlag = 2 3187 closePorts initFlag = 4 3188 ) 3189 3190 func testChangeUnitsNonNilPorts(c *gc.C, owner names.UserTag, runChangeTests func(*gc.C, []changeTestFunc)) { 3191 initModel := func(c *gc.C, st *State, flag initFlag) { 3192 wordpress := AddTestingApplication(c, st, "wordpress", AddTestingCharm(c, st, "wordpress")) 3193 u, err := wordpress.AddUnit(AddUnitParams{}) 3194 c.Assert(err, jc.ErrorIsNil) 3195 m, err := st.AddMachine(UbuntuBase("12.10"), JobHostUnits) 3196 c.Assert(err, jc.ErrorIsNil) 3197 if flag&assignUnit != 0 { 3198 // Assign the unit. 3199 err = u.AssignToMachine(m) 3200 c.Assert(err, jc.ErrorIsNil) 3201 } 3202 if flag&openPorts != 0 { 3203 // Add a network to the machine and open a port. 3204 publicAddress := network.NewSpaceAddress("1.2.3.4", corenetwork.WithScope(corenetwork.ScopePublic)) 3205 privateAddress := network.NewSpaceAddress("4.3.2.1", corenetwork.WithScope(corenetwork.ScopeCloudLocal)) 3206 err = m.SetProviderAddresses(publicAddress, privateAddress) 3207 c.Assert(err, jc.ErrorIsNil) 3208 3209 unitPortRanges, err := u.OpenedPortRanges() 3210 if flag&assignUnit == 0 { 3211 c.Assert(err, jc.Satisfies, errors.IsNotAssigned) 3212 } else { 3213 c.Assert(err, jc.ErrorIsNil) 3214 unitPortRanges.Open(allEndpoints, corenetwork.MustParsePortRange("12345/tcp")) 3215 c.Assert(st.ApplyOperation(unitPortRanges.Changes()), jc.ErrorIsNil) 3216 } 3217 } 3218 if flag&closePorts != 0 { 3219 // Close the port again (only if been opened before). 3220 unitPortRanges, err := u.OpenedPortRanges() 3221 c.Assert(err, jc.ErrorIsNil) 3222 unitPortRanges.Close(allEndpoints, corenetwork.MustParsePortRange("12345/tcp")) 3223 c.Assert(st.ApplyOperation(unitPortRanges.Changes()), jc.ErrorIsNil) 3224 } 3225 } 3226 changeTestFuncs := []changeTestFunc{ 3227 func(c *gc.C, st *State) changeTestCase { 3228 initModel(c, st, assignUnit) 3229 now := st.clock().Now() 3230 return changeTestCase{ 3231 about: "don't open ports on unit", 3232 change: watcher.Change{ 3233 C: "units", 3234 Id: st.docID("wordpress/0"), 3235 }, 3236 expectContents: []multiwatcher.EntityInfo{ 3237 &multiwatcher.UnitInfo{ 3238 ModelUUID: st.ModelUUID(), 3239 Name: "wordpress/0", 3240 Application: "wordpress", 3241 Base: "ubuntu@12.10", 3242 Life: life.Alive, 3243 MachineID: "0", 3244 WorkloadStatus: multiwatcher.StatusInfo{ 3245 Current: "waiting", 3246 Message: "waiting for machine", 3247 Data: map[string]interface{}{}, 3248 Since: &now, 3249 }, 3250 AgentStatus: multiwatcher.StatusInfo{ 3251 Current: "allocating", 3252 Message: "", 3253 Data: map[string]interface{}{}, 3254 Since: &now, 3255 }, 3256 }}} 3257 }, 3258 func(c *gc.C, st *State) changeTestCase { 3259 initModel(c, st, assignUnit|openPorts) 3260 now := st.clock().Now() 3261 3262 return changeTestCase{ 3263 about: "open a port on unit", 3264 change: watcher.Change{ 3265 C: "units", 3266 Id: st.docID("wordpress/0"), 3267 }, 3268 expectContents: []multiwatcher.EntityInfo{ 3269 &multiwatcher.UnitInfo{ 3270 ModelUUID: st.ModelUUID(), 3271 Name: "wordpress/0", 3272 Application: "wordpress", 3273 Base: "ubuntu@12.10", 3274 Life: life.Alive, 3275 MachineID: "0", 3276 OpenPortRangesByEndpoint: network.GroupedPortRanges{ 3277 allEndpoints: {corenetwork.MustParsePortRange("12345/tcp")}, 3278 }, 3279 WorkloadStatus: multiwatcher.StatusInfo{ 3280 Current: "waiting", 3281 Message: "waiting for machine", 3282 Data: map[string]interface{}{}, 3283 Since: &now, 3284 }, 3285 AgentStatus: multiwatcher.StatusInfo{ 3286 Current: "allocating", 3287 Message: "", 3288 Data: map[string]interface{}{}, 3289 Since: &now, 3290 }, 3291 }}} 3292 }, 3293 func(c *gc.C, st *State) changeTestCase { 3294 initModel(c, st, assignUnit|openPorts|closePorts) 3295 now := st.clock().Now() 3296 3297 return changeTestCase{ 3298 about: "open a port on unit and close it again", 3299 change: watcher.Change{ 3300 C: "units", 3301 Id: st.docID("wordpress/0"), 3302 }, 3303 expectContents: []multiwatcher.EntityInfo{ 3304 &multiwatcher.UnitInfo{ 3305 ModelUUID: st.ModelUUID(), 3306 Name: "wordpress/0", 3307 Application: "wordpress", 3308 Base: "ubuntu@12.10", 3309 Life: life.Alive, 3310 MachineID: "0", 3311 WorkloadStatus: multiwatcher.StatusInfo{ 3312 Current: "waiting", 3313 Message: "waiting for machine", 3314 Data: map[string]interface{}{}, 3315 Since: &now, 3316 }, 3317 AgentStatus: multiwatcher.StatusInfo{ 3318 Current: "allocating", 3319 Message: "", 3320 Data: map[string]interface{}{}, 3321 Since: &now, 3322 }, 3323 }}} 3324 }, 3325 func(c *gc.C, st *State) changeTestCase { 3326 initModel(c, st, openPorts) 3327 now := st.clock().Now() 3328 3329 return changeTestCase{ 3330 about: "open ports on an unassigned unit", 3331 change: watcher.Change{ 3332 C: "units", 3333 Id: st.docID("wordpress/0"), 3334 }, 3335 expectContents: []multiwatcher.EntityInfo{ 3336 &multiwatcher.UnitInfo{ 3337 ModelUUID: st.ModelUUID(), 3338 Name: "wordpress/0", 3339 Application: "wordpress", 3340 Base: "ubuntu@12.10", 3341 Life: life.Alive, 3342 WorkloadStatus: multiwatcher.StatusInfo{ 3343 Current: "waiting", 3344 Message: "waiting for machine", 3345 Data: map[string]interface{}{}, 3346 Since: &now, 3347 }, 3348 AgentStatus: multiwatcher.StatusInfo{ 3349 Current: "allocating", 3350 Message: "", 3351 Data: map[string]interface{}{}, 3352 Since: &now, 3353 }, 3354 }}} 3355 }, 3356 } 3357 runChangeTests(c, changeTestFuncs) 3358 } 3359 3360 func testChangeRemoteApplications(c *gc.C, runChangeTests func(*gc.C, []changeTestFunc)) { 3361 changeTestFuncs := []changeTestFunc{ 3362 func(c *gc.C, st *State) changeTestCase { 3363 return changeTestCase{ 3364 about: "no remote application in state, no remote application in store -> do nothing", 3365 change: watcher.Change{ 3366 C: "remoteApplications", 3367 Id: st.docID("remote-mysql2"), 3368 }} 3369 }, 3370 func(c *gc.C, st *State) changeTestCase { 3371 return changeTestCase{ 3372 about: "remote application is removed if it's not in backing", 3373 initialContents: []multiwatcher.EntityInfo{ 3374 &multiwatcher.RemoteApplicationUpdate{ 3375 ModelUUID: st.ModelUUID(), 3376 Name: "remote-mysql2", 3377 OfferURL: "me/model.mysql", 3378 }, 3379 }, 3380 change: watcher.Change{ 3381 C: "remoteApplications", 3382 Id: st.docID("remote-mysql2"), 3383 }} 3384 }, 3385 func(c *gc.C, st *State) changeTestCase { 3386 _, remoteApplicationInfo := addTestingRemoteApplication( 3387 c, st, "remote-mysql2", "me/model.mysql", mysqlRelations, false) 3388 return changeTestCase{ 3389 about: "remote application is added if it's in backing but not in Store", 3390 change: watcher.Change{ 3391 C: "remoteApplications", 3392 Id: st.docID("remote-mysql2"), 3393 }, 3394 expectContents: []multiwatcher.EntityInfo{&remoteApplicationInfo}, 3395 } 3396 }, 3397 func(c *gc.C, st *State) changeTestCase { 3398 // Currently the only change we can make to a remote 3399 // application is to destroy it. 3400 // 3401 // We must add a relation to the remote application, and 3402 // a unit to the relation, so that the relation is not 3403 // removed and thus the remote application is not removed 3404 // upon destroying. 3405 wordpress := AddTestingApplication(c, st, "wordpress", AddTestingCharm(c, st, "wordpress")) 3406 mysql, remoteApplicationInfo := addTestingRemoteApplication( 3407 c, st, "remote-mysql2", "me/model.mysql", mysqlRelations, false, 3408 ) 3409 3410 eps, err := st.InferEndpoints("wordpress", "remote-mysql2") 3411 c.Assert(err, jc.ErrorIsNil) 3412 rel, err := st.AddRelation(eps[0], eps[1]) 3413 c.Assert(err, jc.ErrorIsNil) 3414 c.Assert(wordpress.Refresh(), jc.ErrorIsNil) 3415 c.Assert(mysql.Refresh(), jc.ErrorIsNil) 3416 3417 wu, err := wordpress.AddUnit(AddUnitParams{}) 3418 c.Assert(err, jc.ErrorIsNil) 3419 wru, err := rel.Unit(wu) 3420 c.Assert(err, jc.ErrorIsNil) 3421 err = wru.EnterScope(nil) 3422 c.Assert(err, jc.ErrorIsNil) 3423 3424 status, err := mysql.Status() 3425 c.Assert(err, jc.ErrorIsNil) 3426 3427 err = mysql.Destroy() 3428 c.Assert(err, jc.ErrorIsNil) 3429 3430 now := st.clock().Now() 3431 initialRemoteApplicationInfo := remoteApplicationInfo 3432 remoteApplicationInfo.Life = "dying" 3433 remoteApplicationInfo.Status = multiwatcher.StatusInfo{ 3434 Current: status.Status, 3435 Message: status.Message, 3436 Data: status.Data, 3437 Since: &now, 3438 } 3439 return changeTestCase{ 3440 about: "remote application is updated if it's in backing and in multiwatcher.Store", 3441 initialContents: []multiwatcher.EntityInfo{&initialRemoteApplicationInfo}, 3442 change: watcher.Change{ 3443 C: "remoteApplications", 3444 Id: st.docID("remote-mysql2"), 3445 }, 3446 expectContents: []multiwatcher.EntityInfo{&remoteApplicationInfo}, 3447 } 3448 }, 3449 func(c *gc.C, st *State) changeTestCase { 3450 mysql, remoteApplicationInfo := addTestingRemoteApplication( 3451 c, st, "remote-mysql2", "me/model.mysql", mysqlRelations, false, 3452 ) 3453 now := st.clock().Now() 3454 sInfo := status.StatusInfo{ 3455 Status: status.Active, 3456 Message: "running", 3457 Data: map[string]interface{}{"foo": "bar"}, 3458 Since: &now, 3459 } 3460 err := mysql.SetStatus(sInfo) 3461 c.Assert(err, jc.ErrorIsNil) 3462 initialRemoteApplicationInfo := remoteApplicationInfo 3463 remoteApplicationInfo.Status = multiwatcher.StatusInfo{ 3464 Current: "active", 3465 Message: "running", 3466 Data: map[string]interface{}{"foo": "bar"}, 3467 Since: &now, 3468 } 3469 return changeTestCase{ 3470 about: "remote application status is updated if it's in backing and in multiwatcher.Store", 3471 initialContents: []multiwatcher.EntityInfo{&initialRemoteApplicationInfo}, 3472 change: watcher.Change{ 3473 C: "statuses", 3474 Id: st.docID(mysql.globalKey()), 3475 }, 3476 expectContents: []multiwatcher.EntityInfo{&remoteApplicationInfo}, 3477 } 3478 }, 3479 } 3480 runChangeTests(c, changeTestFuncs) 3481 } 3482 3483 func testChangeApplicationOffers(c *gc.C, runChangeTests func(*gc.C, []changeTestFunc)) { 3484 addOffer := func(c *gc.C, st *State) (multiwatcher.ApplicationOfferInfo, *User) { 3485 owner, err := st.AddUser("owner", "owner", "password", "admin") 3486 c.Assert(err, jc.ErrorIsNil) 3487 AddTestingApplication(c, st, "mysql", AddTestingCharm(c, st, "mysql")) 3488 addTestingRemoteApplication( 3489 c, st, "remote-wordpress", "", []charm.Relation{{ 3490 Name: "db", 3491 Role: "requirer", 3492 Scope: charm.ScopeGlobal, 3493 Interface: "mysql", 3494 }}, true, 3495 ) 3496 applicationOfferInfo, _ := addTestingApplicationOffer( 3497 c, st, owner.UserTag(), "hosted-mysql", "mysql", 3498 "quantal-mysql", []string{"server"}) 3499 return applicationOfferInfo, owner 3500 } 3501 3502 changeTestFuncs := []changeTestFunc{ 3503 func(c *gc.C, st *State) changeTestCase { 3504 return changeTestCase{ 3505 about: "no application offer in state, no application offer in store -> do nothing", 3506 change: watcher.Change{ 3507 C: "applicationOffers", 3508 Id: st.docID("hosted-mysql"), 3509 }} 3510 }, 3511 func(c *gc.C, st *State) changeTestCase { 3512 return changeTestCase{ 3513 about: "application offer is removed if it's not in backing", 3514 initialContents: []multiwatcher.EntityInfo{ 3515 &multiwatcher.ApplicationOfferInfo{ 3516 ModelUUID: st.ModelUUID(), 3517 OfferName: "hosted-mysql", 3518 OfferUUID: "hosted-mysql-uuid", 3519 ApplicationName: "mysql", 3520 }, 3521 }, 3522 change: watcher.Change{ 3523 C: "applicationOffers", 3524 Id: st.docID("hosted-mysql"), 3525 }} 3526 }, 3527 func(c *gc.C, st *State) changeTestCase { 3528 applicationOfferInfo, _ := addOffer(c, st) 3529 return changeTestCase{ 3530 about: "application offer is added if it's in backing but not in Store", 3531 change: watcher.Change{ 3532 C: "applicationOffers", 3533 Id: st.docID("hosted-mysql"), 3534 }, 3535 expectContents: []multiwatcher.EntityInfo{&applicationOfferInfo}, 3536 } 3537 }, 3538 func(c *gc.C, st *State) changeTestCase { 3539 applicationOfferInfo, owner := addOffer(c, st) 3540 app, err := st.Application("mysql") 3541 c.Assert(err, jc.ErrorIsNil) 3542 curl, _ := app.CharmURL() 3543 ch, err := st.Charm(*curl) 3544 c.Assert(err, jc.ErrorIsNil) 3545 AddTestingApplication(c, st, "another-mysql", ch) 3546 offers := NewApplicationOffers(st) 3547 _, err = offers.UpdateOffer(crossmodel.AddApplicationOfferArgs{ 3548 OfferName: "hosted-mysql", 3549 Owner: owner.Name(), 3550 ApplicationName: "another-mysql", 3551 Endpoints: map[string]string{"server": "server"}, 3552 }) 3553 c.Assert(err, jc.ErrorIsNil) 3554 3555 initialApplicationOfferInfo := applicationOfferInfo 3556 applicationOfferInfo.ApplicationName = "another-mysql" 3557 return changeTestCase{ 3558 about: "application offer is updated if it's in backing and not in multiwatcher.Store", 3559 initialContents: []multiwatcher.EntityInfo{&initialApplicationOfferInfo}, 3560 change: watcher.Change{ 3561 C: "applicationOffers", 3562 Id: st.docID("hosted-mysql"), 3563 }, 3564 expectContents: []multiwatcher.EntityInfo{&applicationOfferInfo}, 3565 } 3566 }, 3567 func(c *gc.C, st *State) changeTestCase { 3568 applicationOfferInfo, _ := addOffer(c, st) 3569 initialApplicationOfferInfo := applicationOfferInfo 3570 addTestingRemoteApplication( 3571 c, st, "remote-wordpress2", "", []charm.Relation{{ 3572 Name: "db", 3573 Role: "requirer", 3574 Scope: charm.ScopeGlobal, 3575 Interface: "mysql", 3576 }}, true, 3577 ) 3578 eps, err := st.InferEndpoints("mysql", "remote-wordpress2") 3579 c.Assert(err, jc.ErrorIsNil) 3580 rel, err := st.AddRelation(eps...) 3581 c.Assert(err, jc.ErrorIsNil) 3582 _, err = st.AddOfferConnection(AddOfferConnectionParams{ 3583 SourceModelUUID: utils.MustNewUUID().String(), 3584 RelationId: rel.Id(), 3585 RelationKey: rel.Tag().Id(), 3586 Username: "fred", 3587 OfferUUID: initialApplicationOfferInfo.OfferUUID, 3588 }) 3589 c.Assert(err, jc.ErrorIsNil) 3590 3591 applicationOfferInfo.TotalConnectedCount = 2 3592 return changeTestCase{ 3593 about: "application offer count is updated if it's in backing and in multiwatcher.Store", 3594 initialContents: []multiwatcher.EntityInfo{&initialApplicationOfferInfo}, 3595 change: watcher.Change{ 3596 C: "remoteApplications", 3597 Id: st.docID("remote-wordpress2"), 3598 }, 3599 expectContents: []multiwatcher.EntityInfo{&applicationOfferInfo}, 3600 } 3601 }, 3602 } 3603 runChangeTests(c, changeTestFuncs) 3604 } 3605 3606 func testChangeGenerations(c *gc.C, runChangeTests func(*gc.C, []changeTestFunc)) { 3607 changeTestFuncs := []changeTestFunc{ 3608 func(c *gc.C, st *State) changeTestCase { 3609 return changeTestCase{ 3610 about: "no change if generation absent from state and store", 3611 change: watcher.Change{ 3612 C: "generations", 3613 Id: st.docID("does-not-exist"), 3614 }} 3615 }, 3616 func(c *gc.C, st *State) changeTestCase { 3617 return changeTestCase{ 3618 about: "generation is removed if not in backing", 3619 initialContents: []multiwatcher.EntityInfo{ 3620 &multiwatcher.BranchInfo{ 3621 ModelUUID: st.ModelUUID(), 3622 ID: "to-be-removed", 3623 }, 3624 }, 3625 change: watcher.Change{ 3626 C: "generations", 3627 Id: st.docID("to-be-removed"), 3628 }, 3629 } 3630 }, 3631 func(c *gc.C, st *State) changeTestCase { 3632 c.Assert(st.AddBranch("new-branch", "some-user"), jc.ErrorIsNil) 3633 branch, err := st.Branch("new-branch") 3634 c.Assert(err, jc.ErrorIsNil) 3635 3636 return changeTestCase{ 3637 about: "generation is added if in backing but not in store", 3638 change: watcher.Change{ 3639 C: "generations", 3640 Id: st.docID(branch.doc.DocId), 3641 }, 3642 expectContents: []multiwatcher.EntityInfo{ 3643 &multiwatcher.BranchInfo{ 3644 ModelUUID: st.ModelUUID(), 3645 ID: st.localID(branch.doc.DocId), 3646 Name: "new-branch", 3647 AssignedUnits: map[string][]string{}, 3648 CreatedBy: "some-user", 3649 }}, 3650 } 3651 }, 3652 func(c *gc.C, st *State) changeTestCase { 3653 c.Assert(st.AddBranch("new-branch", "some-user"), jc.ErrorIsNil) 3654 branch, err := st.Branch("new-branch") 3655 c.Assert(err, jc.ErrorIsNil) 3656 3657 app := AddTestingApplication(c, st, "wordpress", AddTestingCharm(c, st, "wordpress")) 3658 u, err := app.AddUnit(AddUnitParams{}) 3659 c.Assert(err, jc.ErrorIsNil) 3660 3661 c.Assert(branch.AssignUnit(u.Name()), jc.ErrorIsNil) 3662 3663 return changeTestCase{ 3664 about: "generation is updated if in backing and in store", 3665 change: watcher.Change{ 3666 C: "generations", 3667 Id: st.docID(branch.doc.DocId), 3668 }, 3669 initialContents: []multiwatcher.EntityInfo{ 3670 &multiwatcher.BranchInfo{ 3671 ModelUUID: st.ModelUUID(), 3672 ID: st.localID(branch.doc.DocId), 3673 Name: "new-branch", 3674 AssignedUnits: map[string][]string{}, 3675 CreatedBy: "some-user", 3676 }}, 3677 expectContents: []multiwatcher.EntityInfo{ 3678 &multiwatcher.BranchInfo{ 3679 ModelUUID: st.ModelUUID(), 3680 ID: st.localID(branch.doc.DocId), 3681 Name: "new-branch", 3682 AssignedUnits: map[string][]string{app.Name(): {u.Name()}}, 3683 CreatedBy: "some-user", 3684 }}, 3685 } 3686 }, 3687 } 3688 runChangeTests(c, changeTestFuncs) 3689 } 3690 3691 type entityInfoSlice []multiwatcher.EntityInfo 3692 3693 func (s entityInfoSlice) Len() int { return len(s) } 3694 func (s entityInfoSlice) Swap(i, j int) { s[i], s[j] = s[j], s[i] } 3695 func (s entityInfoSlice) Less(i, j int) bool { 3696 id0, id1 := s[i].EntityID(), s[j].EntityID() 3697 if id0.Kind != id1.Kind { 3698 return id0.Kind < id1.Kind 3699 } 3700 if id0.ModelUUID != id1.ModelUUID { 3701 return id0.ModelUUID < id1.ModelUUID 3702 } 3703 return id0.ID < id1.ID 3704 } 3705 3706 func makeActionInfo(a Action, st *State) multiwatcher.ActionInfo { 3707 results, message := a.Results() 3708 return multiwatcher.ActionInfo{ 3709 ModelUUID: st.ModelUUID(), 3710 ID: a.Id(), 3711 Receiver: a.Receiver(), 3712 Name: a.Name(), 3713 Parameters: a.Parameters(), 3714 Parallel: a.Parallel(), 3715 ExecutionGroup: a.ExecutionGroup(), 3716 Status: string(a.Status()), 3717 Message: message, 3718 Results: results, 3719 Enqueued: a.Enqueued(), 3720 Started: a.Started(), 3721 Completed: a.Completed(), 3722 } 3723 } 3724 3725 func jcDeepEqualsCheck(c *gc.C, got, want interface{}) bool { 3726 ok, err := jc.DeepEqual(got, want) 3727 if ok { 3728 c.Check(err, jc.ErrorIsNil) 3729 } 3730 return ok 3731 } 3732 3733 // assertEntitiesEqual is a specialised version of the typical 3734 // jc.DeepEquals check that provides more informative output when 3735 // comparing EntityInfo slices. 3736 func assertEntitiesEqual(c *gc.C, got, want []multiwatcher.EntityInfo) { 3737 if jcDeepEqualsCheck(c, got, want) { 3738 return 3739 } 3740 if len(got) != len(want) { 3741 c.Errorf("entity length mismatch; got %d; want %d", len(got), len(want)) 3742 } else { 3743 c.Errorf("entity contents mismatch; same length %d", len(got)) 3744 } 3745 // Lets construct a decent output. 3746 var errorOutput string 3747 errorOutput = "\ngot: \n" 3748 for _, e := range got { 3749 errorOutput += fmt.Sprintf(" %T %#v\n", e, e) 3750 } 3751 errorOutput += "expected: \n" 3752 for _, e := range want { 3753 errorOutput += fmt.Sprintf(" %T %#v\n", e, e) 3754 } 3755 3756 c.Errorf(errorOutput) 3757 3758 if len(got) == len(want) { 3759 for i := 0; i < len(got); i++ { 3760 g := got[i] 3761 w := want[i] 3762 if !jcDeepEqualsCheck(c, g, w) { 3763 if ok := c.Check(g, jc.DeepEquals, w); !ok { 3764 c.Logf("first difference at position %d\n", i) 3765 } 3766 break 3767 } 3768 } 3769 } 3770 c.FailNow() 3771 }