github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/api/watcher/watcher_test.go (about) 1 // Copyright 2012, 2013 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package watcher_test 5 6 import ( 7 "context" 8 "time" 9 10 "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" 11 "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/checkers" 12 "github.com/juju/charm/v12" 13 "github.com/juju/names/v5" 14 jc "github.com/juju/testing/checkers" 15 "github.com/juju/utils/v3" 16 "github.com/juju/worker/v3" 17 "github.com/juju/worker/v3/workertest" 18 gc "gopkg.in/check.v1" 19 "gopkg.in/macaroon.v2" 20 21 "github.com/juju/juju/api" 22 "github.com/juju/juju/api/agent/migrationminion" 23 "github.com/juju/juju/api/agent/secretsmanager" 24 "github.com/juju/juju/api/controller/crossmodelrelations" 25 "github.com/juju/juju/api/watcher" 26 "github.com/juju/juju/core/crossmodel" 27 "github.com/juju/juju/core/life" 28 "github.com/juju/juju/core/migration" 29 "github.com/juju/juju/core/permission" 30 "github.com/juju/juju/core/secrets" 31 "github.com/juju/juju/core/status" 32 corewatcher "github.com/juju/juju/core/watcher" 33 "github.com/juju/juju/core/watcher/watchertest" 34 "github.com/juju/juju/juju/testing" 35 "github.com/juju/juju/rpc/params" 36 "github.com/juju/juju/state" 37 coretesting "github.com/juju/juju/testing" 38 "github.com/juju/juju/testing/factory" 39 ) 40 41 type watcherSuite struct { 42 testing.JujuConnSuite 43 44 stateAPI api.Connection 45 46 // These are raw State objects. Use them for setup and assertions, but 47 // should never be touched by the API calls themselves 48 rawMachine *state.Machine 49 } 50 51 var _ = gc.Suite(&watcherSuite{}) 52 53 func (s *watcherSuite) SetUpTest(c *gc.C) { 54 s.JujuConnSuite.SetUpTest(c) 55 s.stateAPI, s.rawMachine = s.OpenAPIAsNewMachine(c, state.JobManageModel, state.JobHostUnits) 56 s.WaitForModelWatchersIdle(c, s.Model.UUID()) 57 } 58 59 func (s *watcherSuite) TestWatchInitialEventConsumed(c *gc.C) { 60 // Machiner.Watch should send the initial event as part of the Watch 61 // call (for NotifyWatchers there is no state to be transmitted). So a 62 // call to Next() should not have anything to return. 63 var results params.NotifyWatchResults 64 args := params.Entities{Entities: []params.Entity{{Tag: s.rawMachine.Tag().String()}}} 65 err := s.stateAPI.APICall("Machiner", s.stateAPI.BestFacadeVersion("Machiner"), "", "Watch", args, &results) 66 c.Assert(err, jc.ErrorIsNil) 67 c.Assert(results.Results, gc.HasLen, 1) 68 result := results.Results[0] 69 c.Assert(result.Error, gc.IsNil) 70 71 // We expect the Call() to "Next" to block, so run it in a goroutine. 72 done := make(chan error) 73 go func() { 74 ignored := struct{}{} 75 done <- s.stateAPI.APICall("NotifyWatcher", s.stateAPI.BestFacadeVersion("NotifyWatcher"), result.NotifyWatcherId, "Next", nil, &ignored) 76 }() 77 78 select { 79 case err := <-done: 80 c.Errorf("Call(Next) did not block immediately after Watch(): err %v", err) 81 case <-time.After(coretesting.ShortWait): 82 } 83 } 84 85 func (s *watcherSuite) TestWatchMachine(c *gc.C) { 86 var results params.NotifyWatchResults 87 args := params.Entities{Entities: []params.Entity{{Tag: s.rawMachine.Tag().String()}}} 88 err := s.stateAPI.APICall("Machiner", s.stateAPI.BestFacadeVersion("Machiner"), "", "Watch", args, &results) 89 c.Assert(err, jc.ErrorIsNil) 90 c.Assert(results.Results, gc.HasLen, 1) 91 result := results.Results[0] 92 c.Assert(result.Error, gc.IsNil) 93 94 w := watcher.NewNotifyWatcher(s.stateAPI, result) 95 wc := watchertest.NewNotifyWatcherC(c, w) 96 defer wc.AssertStops() 97 wc.AssertOneChange() 98 } 99 100 func (s *watcherSuite) TestNotifyWatcherStopsWithPendingSend(c *gc.C) { 101 var results params.NotifyWatchResults 102 args := params.Entities{Entities: []params.Entity{{Tag: s.rawMachine.Tag().String()}}} 103 err := s.stateAPI.APICall("Machiner", s.stateAPI.BestFacadeVersion("Machiner"), "", "Watch", args, &results) 104 c.Assert(err, jc.ErrorIsNil) 105 c.Assert(results.Results, gc.HasLen, 1) 106 result := results.Results[0] 107 c.Assert(result.Error, gc.IsNil) 108 109 // params.NotifyWatcher conforms to the watcher.NotifyWatcher interface 110 w := watcher.NewNotifyWatcher(s.stateAPI, result) 111 wc := watchertest.NewNotifyWatcherC(c, w) 112 wc.AssertStops() 113 } 114 115 func (s *watcherSuite) TestWatchUnitsKeepsEvents(c *gc.C) { 116 // Create two applications, relate them, and add one unit to each - a 117 // principal and a subordinate. 118 mysql := s.AddTestingApplication(c, "mysql", s.AddTestingCharm(c, "mysql")) 119 s.AddTestingApplication(c, "logging", s.AddTestingCharm(c, "logging")) 120 eps, err := s.State.InferEndpoints("mysql", "logging") 121 c.Assert(err, jc.ErrorIsNil) 122 rel, err := s.State.AddRelation(eps...) 123 c.Assert(err, jc.ErrorIsNil) 124 principal, err := mysql.AddUnit(state.AddUnitParams{}) 125 c.Assert(err, jc.ErrorIsNil) 126 err = principal.AssignToMachine(s.rawMachine) 127 c.Assert(err, jc.ErrorIsNil) 128 relUnit, err := rel.Unit(principal) 129 c.Assert(err, jc.ErrorIsNil) 130 err = relUnit.EnterScope(nil) 131 c.Assert(err, jc.ErrorIsNil) 132 subordinate, err := s.State.Unit("logging/0") 133 c.Assert(err, jc.ErrorIsNil) 134 135 // Call the Deployer facade's WatchUnits for machine-0. 136 s.WaitForModelWatchersIdle(c, s.Model.UUID()) 137 var results params.StringsWatchResults 138 args := params.Entities{Entities: []params.Entity{{Tag: s.rawMachine.Tag().String()}}} 139 err = s.stateAPI.APICall("Deployer", s.stateAPI.BestFacadeVersion("Deployer"), "", "WatchUnits", args, &results) 140 c.Assert(err, jc.ErrorIsNil) 141 c.Assert(results.Results, gc.HasLen, 1) 142 result := results.Results[0] 143 c.Assert(result.Error, gc.IsNil) 144 145 // Start a StringsWatcher and check the initial event. 146 w := watcher.NewStringsWatcher(s.stateAPI, result) 147 wc := watchertest.NewStringsWatcherC(c, w) 148 defer wc.AssertStops() 149 150 wc.AssertChange("mysql/0", "logging/0") 151 wc.AssertNoChange() 152 153 // Now, without reading any changes advance the lifecycle of both 154 // units. 155 err = subordinate.EnsureDead() 156 c.Assert(err, jc.ErrorIsNil) 157 err = subordinate.Remove() 158 c.Assert(err, jc.ErrorIsNil) 159 err = principal.EnsureDead() 160 c.Assert(err, jc.ErrorIsNil) 161 162 // Expect both changes are passed back. 163 wc.AssertChange("mysql/0", "logging/0") 164 wc.AssertNoChange() 165 } 166 167 func (s *watcherSuite) TestStringsWatcherStopsWithPendingSend(c *gc.C) { 168 // Call the Deployer facade's WatchUnits for machine-0. 169 var results params.StringsWatchResults 170 args := params.Entities{Entities: []params.Entity{{Tag: s.rawMachine.Tag().String()}}} 171 err := s.stateAPI.APICall("Deployer", s.stateAPI.BestFacadeVersion("Deployer"), "", "WatchUnits", args, &results) 172 c.Assert(err, jc.ErrorIsNil) 173 c.Assert(results.Results, gc.HasLen, 1) 174 result := results.Results[0] 175 c.Assert(result.Error, gc.IsNil) 176 177 // Start a StringsWatcher and check the initial event. 178 w := watcher.NewStringsWatcher(s.stateAPI, result) 179 wc := watchertest.NewStringsWatcherC(c, w) 180 defer wc.AssertStops() 181 182 // Create an application, deploy a unit of it on the machine. 183 mysql := s.AddTestingApplication(c, "mysql", s.AddTestingCharm(c, "mysql")) 184 principal, err := mysql.AddUnit(state.AddUnitParams{}) 185 c.Assert(err, jc.ErrorIsNil) 186 err = principal.AssignToMachine(s.rawMachine) 187 c.Assert(err, jc.ErrorIsNil) 188 } 189 190 // TODO(fwereade): 2015-11-18 lp:1517391 191 func (s *watcherSuite) TestWatchMachineStorage(c *gc.C) { 192 s.Factory.MakeMachine(c, &factory.MachineParams{ 193 Volumes: []state.HostVolumeParams{{ 194 Volume: state.VolumeParams{ 195 Pool: "modelscoped", 196 Size: 1024, 197 }, 198 }}, 199 }) 200 201 var results params.MachineStorageIdsWatchResults 202 args := params.Entities{Entities: []params.Entity{{ 203 Tag: s.Model.ModelTag().String(), 204 }}} 205 err := s.stateAPI.APICall( 206 "StorageProvisioner", 207 s.stateAPI.BestFacadeVersion("StorageProvisioner"), 208 "", "WatchVolumeAttachments", args, &results) 209 c.Assert(err, jc.ErrorIsNil) 210 c.Assert(results.Results, gc.HasLen, 1) 211 result := results.Results[0] 212 c.Assert(result.Error, gc.IsNil) 213 214 w := watcher.NewVolumeAttachmentsWatcher(s.stateAPI, result) 215 defer func() { 216 217 // Check we can stop the watcher... 218 w.Kill() 219 wait := make(chan error) 220 go func() { 221 wait <- w.Wait() 222 }() 223 select { 224 case err := <-wait: 225 c.Assert(err, jc.ErrorIsNil) 226 case <-time.After(coretesting.LongWait): 227 c.Fatalf("watcher never stopped") 228 } 229 230 // ...and that its channel hasn't been closed. 231 select { 232 case change, ok := <-w.Changes(): 233 c.Fatalf("watcher sent unexpected change: (%#v, %v)", change, ok) 234 default: 235 } 236 237 }() 238 239 // Check initial event; 240 select { 241 case changes, ok := <-w.Changes(): 242 c.Assert(ok, jc.IsTrue) 243 c.Assert(changes, jc.SameContents, []corewatcher.MachineStorageId{{ 244 MachineTag: "machine-1", 245 AttachmentTag: "volume-0", 246 }}) 247 case <-time.After(coretesting.LongWait): 248 c.Fatalf("timed out waiting for change") 249 } 250 251 // check no subsequent event. 252 select { 253 case <-w.Changes(): 254 c.Fatalf("received unexpected change") 255 case <-time.After(coretesting.ShortWait): 256 } 257 } 258 259 func (s *watcherSuite) assertSetupRelationStatusWatch( 260 c *gc.C, rel *state.Relation, 261 ) (func(life life.Value, suspended bool, reason string), func()) { 262 // Export the relation so it can be found with a token. 263 re := s.State.RemoteEntities() 264 token, err := re.ExportLocalEntity(rel.Tag()) 265 c.Assert(err, jc.ErrorIsNil) 266 267 // Create the offer connection details. 268 s.Factory.MakeUser(c, &factory.UserParams{Name: "fred"}) 269 offers := state.NewApplicationOffers(s.State) 270 offer, err := offers.AddOffer(crossmodel.AddApplicationOfferArgs{ 271 OfferName: "hosted-mysql", 272 ApplicationName: "mysql", 273 Owner: "admin", 274 }) 275 c.Assert(err, jc.ErrorIsNil) 276 _, err = s.State.AddOfferConnection(state.AddOfferConnectionParams{ 277 OfferUUID: offer.OfferUUID, 278 Username: "fred", 279 RelationKey: rel.String(), 280 RelationId: rel.Id(), 281 SourceModelUUID: s.State.ModelUUID(), 282 }) 283 c.Assert(err, jc.ErrorIsNil) 284 285 // Add the consume permission for the offer so the macaroon 286 // discharge can occur. 287 err = s.State.CreateOfferAccess( 288 names.NewApplicationOfferTag(offer.OfferUUID), 289 names.NewUserTag("fred"), permission.ConsumeAccess) 290 c.Assert(err, jc.ErrorIsNil) 291 292 // Create a macaroon for authorisation. 293 store, err := s.State.NewBakeryStorage() 294 c.Assert(err, jc.ErrorIsNil) 295 b := bakery.New(bakery.BakeryParams{ 296 RootKeyStore: store, 297 }) 298 c.Assert(err, jc.ErrorIsNil) 299 mac, err := b.Oven.NewMacaroon( 300 context.Background(), 301 bakery.LatestVersion, 302 []checkers.Caveat{ 303 checkers.DeclaredCaveat("source-model-uuid", s.State.ModelUUID()), 304 checkers.DeclaredCaveat("relation-key", rel.String()), 305 checkers.DeclaredCaveat("username", "fred"), 306 }, bakery.NoOp) 307 c.Assert(err, jc.ErrorIsNil) 308 309 // Start watching for a relation change. 310 client := crossmodelrelations.NewClient(s.stateAPI) 311 w, err := client.WatchRelationSuspendedStatus(params.RemoteEntityArg{ 312 Token: token, 313 Macaroons: macaroon.Slice{mac.M()}, 314 }) 315 c.Assert(err, jc.ErrorIsNil) 316 stop := func() { 317 workertest.CleanKill(c, w) 318 } 319 modelUUID := s.BackingState.ModelUUID() 320 assertNoChange := func() { 321 s.WaitForModelWatchersIdle(c, modelUUID) 322 select { 323 case _, ok := <-w.Changes(): 324 c.Fatalf("watcher sent unexpected change: (_, %v)", ok) 325 case <-time.After(coretesting.ShortWait): 326 } 327 } 328 329 assertChange := func(life life.Value, suspended bool, reason string) { 330 s.WaitForModelWatchersIdle(c, modelUUID) 331 select { 332 case changes, ok := <-w.Changes(): 333 c.Check(ok, jc.IsTrue) 334 c.Check(changes, gc.HasLen, 1) 335 c.Check(changes[0].Life, gc.Equals, life) 336 c.Check(changes[0].Suspended, gc.Equals, suspended) 337 c.Check(changes[0].SuspendedReason, gc.Equals, reason) 338 case <-time.After(coretesting.LongWait): 339 c.Fatalf("watcher didn't emit an event") 340 } 341 assertNoChange() 342 } 343 344 // Initial event. 345 assertChange(life.Alive, false, "") 346 return assertChange, stop 347 } 348 349 func (s *watcherSuite) TestRelationStatusWatcher(c *gc.C) { 350 // Create a pair of services and a relation between them. 351 mysql := s.AddTestingApplication(c, "mysql", s.AddTestingCharm(c, "mysql")) 352 s.AddTestingApplication(c, "wordpress", s.AddTestingCharm(c, "wordpress")) 353 eps, err := s.State.InferEndpoints("wordpress", "mysql") 354 c.Assert(err, jc.ErrorIsNil) 355 rel, err := s.State.AddRelation(eps...) 356 c.Assert(err, jc.ErrorIsNil) 357 358 u, err := mysql.AddUnit(state.AddUnitParams{}) 359 c.Assert(err, jc.ErrorIsNil) 360 m := s.Factory.MakeMachine(c, &factory.MachineParams{}) 361 err = u.AssignToMachine(m) 362 c.Assert(err, jc.ErrorIsNil) 363 relUnit, err := rel.Unit(u) 364 c.Assert(err, jc.ErrorIsNil) 365 err = relUnit.EnterScope(nil) 366 c.Assert(err, jc.ErrorIsNil) 367 368 // Ensure that all the creation events have flowed through the system. 369 s.WaitForModelWatchersIdle(c, s.Model.UUID()) 370 371 assertChange, stop := s.assertSetupRelationStatusWatch(c, rel) 372 defer stop() 373 374 err = rel.SetSuspended(true, "another reason") 375 c.Assert(err, jc.ErrorIsNil) 376 assertChange(life.Alive, true, "another reason") 377 378 err = rel.SetSuspended(false, "") 379 c.Assert(err, jc.ErrorIsNil) 380 assertChange(life.Alive, false, "") 381 382 err = rel.Refresh() 383 c.Assert(err, jc.ErrorIsNil) 384 err = rel.Destroy() 385 c.Assert(err, jc.ErrorIsNil) 386 assertChange(life.Dying, false, "") 387 } 388 389 func (s *watcherSuite) TestRelationStatusWatcherDeadRelation(c *gc.C) { 390 // Create a pair of services and a relation between them. 391 s.AddTestingApplication(c, "mysql", s.AddTestingCharm(c, "mysql")) 392 s.AddTestingApplication(c, "wordpress", s.AddTestingCharm(c, "wordpress")) 393 eps, err := s.State.InferEndpoints("wordpress", "mysql") 394 c.Assert(err, jc.ErrorIsNil) 395 rel, err := s.State.AddRelation(eps...) 396 c.Assert(err, jc.ErrorIsNil) 397 398 // Ensure that all the creation events have flowed through the system. 399 s.WaitForModelWatchersIdle(c, s.Model.UUID()) 400 401 assertChange, stop := s.assertSetupRelationStatusWatch(c, rel) 402 defer stop() 403 404 err = rel.Destroy() 405 c.Assert(err, jc.ErrorIsNil) 406 assertChange(life.Dead, false, "") 407 } 408 409 func (s *watcherSuite) setupOfferStatusWatch( 410 c *gc.C, 411 ) (func(status status.Status, message string), func(), func()) { 412 // Create the offer connection details. 413 s.Factory.MakeUser(c, &factory.UserParams{Name: "fred"}) 414 offers := state.NewApplicationOffers(s.State) 415 offer, err := offers.AddOffer(crossmodel.AddApplicationOfferArgs{ 416 OfferName: "hosted-mysql", 417 ApplicationName: "mysql", 418 Owner: "admin", 419 }) 420 c.Assert(err, jc.ErrorIsNil) 421 422 // Add the consume permission for the offer so the macaroon 423 // discharge can occur. 424 err = s.State.CreateOfferAccess( 425 names.NewApplicationOfferTag(offer.OfferUUID), 426 names.NewUserTag("fred"), permission.ConsumeAccess) 427 c.Assert(err, jc.ErrorIsNil) 428 429 // Create a macaroon for authorisation. 430 store, err := s.State.NewBakeryStorage() 431 c.Assert(err, jc.ErrorIsNil) 432 b := bakery.New(bakery.BakeryParams{ 433 RootKeyStore: store, 434 }) 435 c.Assert(err, jc.ErrorIsNil) 436 mac, err := b.Oven.NewMacaroon( 437 context.Background(), 438 bakery.LatestVersion, 439 []checkers.Caveat{ 440 checkers.DeclaredCaveat("source-model-uuid", s.State.ModelUUID()), 441 checkers.DeclaredCaveat("offer-uuid", offer.OfferUUID), 442 checkers.DeclaredCaveat("username", "fred"), 443 }, bakery.NoOp) 444 c.Assert(err, jc.ErrorIsNil) 445 446 s.WaitForModelWatchersIdle(c, s.Model.UUID()) 447 // Start watching for a relation change. 448 client := crossmodelrelations.NewClient(s.stateAPI) 449 w, err := client.WatchOfferStatus(params.OfferArg{ 450 OfferUUID: offer.OfferUUID, 451 Macaroons: macaroon.Slice{mac.M()}, 452 }) 453 c.Assert(err, jc.ErrorIsNil) 454 stop := func() { 455 workertest.CleanKill(c, w) 456 } 457 458 assertNoChange := func() { 459 select { 460 case _, ok := <-w.Changes(): 461 c.Fatalf("watcher sent unexpected change: (_, %v)", ok) 462 case <-time.After(coretesting.ShortWait): 463 } 464 } 465 466 assertChange := func(status status.Status, message string) { 467 select { 468 case changes, ok := <-w.Changes(): 469 c.Check(ok, jc.IsTrue) 470 if status == "" { 471 c.Assert(changes, gc.HasLen, 0) 472 break 473 } 474 c.Assert(changes, gc.HasLen, 1) 475 c.Check(changes[0].Name, gc.Equals, "hosted-mysql") 476 c.Check(changes[0].Status.Status, gc.Equals, status) 477 c.Check(changes[0].Status.Message, gc.Equals, message) 478 case <-time.After(coretesting.LongWait): 479 c.Fatalf("watcher didn't emit an event") 480 } 481 } 482 483 // Initial event. 484 assertChange(status.Unknown, "") 485 return assertChange, assertNoChange, stop 486 } 487 488 func (s *watcherSuite) TestOfferStatusWatcher(c *gc.C) { 489 // Create a pair of services and a relation between them. 490 mysql := s.AddTestingApplication(c, "mysql", s.AddTestingCharm(c, "mysql")) 491 492 assertChange, assertNoChange, stop := s.setupOfferStatusWatch(c) 493 defer stop() 494 495 err := mysql.SetStatus(status.StatusInfo{Status: status.Unknown, Message: "another message"}) 496 c.Assert(err, jc.ErrorIsNil) 497 498 assertChange(status.Unknown, "another message") 499 500 // Removing offer and application both trigger events. 501 offers := state.NewApplicationOffers(s.State) 502 err = offers.Remove("hosted-mysql", false) 503 c.Assert(err, jc.ErrorIsNil) 504 assertChange("terminated", "offer has been removed") 505 err = mysql.Destroy() 506 c.Assert(err, jc.ErrorIsNil) 507 assertChange("terminated", "offer has been removed") 508 assertNoChange() 509 } 510 511 func ptr[T any](v T) *T { 512 return &v 513 } 514 515 func (s *watcherSuite) setupSecretRotationWatcher( 516 c *gc.C, 517 ) (*secrets.URI, func(corewatcher.SecretTriggerChange), func(), func()) { 518 app := s.Factory.MakeApplication(c, &factory.ApplicationParams{Name: "mysql"}) 519 unit, password := s.Factory.MakeUnitReturningPassword(c, &factory.UnitParams{ 520 Application: app, 521 }) 522 store := state.NewSecrets(s.State) 523 uri := secrets.NewURI() 524 nexRotateTime := time.Now().Add(time.Hour) 525 _, err := store.CreateSecret(uri, state.CreateSecretParams{ 526 Owner: unit.Tag(), 527 UpdateSecretParams: state.UpdateSecretParams{ 528 LeaderToken: &fakeToken{}, 529 RotatePolicy: ptr(secrets.RotateDaily), 530 NextRotateTime: ptr(nexRotateTime), 531 Data: map[string]string{"foo": "bar"}, 532 }, 533 }) 534 c.Assert(err, jc.ErrorIsNil) 535 536 s.WaitForModelWatchersIdle(c, s.Model.UUID()) 537 538 apiInfo := s.APIInfo(c) 539 apiInfo.Tag = unit.Tag() 540 apiInfo.Password = password 541 apiInfo.ModelTag = s.Model.ModelTag() 542 543 apiConn, err := api.Open(apiInfo, api.DialOpts{}) 544 c.Assert(err, jc.ErrorIsNil) 545 546 client := secretsmanager.NewClient(apiConn) 547 w, err := client.WatchSecretsRotationChanges(unit.Tag()) 548 if !c.Check(err, jc.ErrorIsNil) { 549 _ = apiConn.Close() 550 c.FailNow() 551 } 552 stop := func() { 553 workertest.CleanKill(c, w) 554 _ = apiConn.Close() 555 } 556 557 assertNoChange := func() { 558 select { 559 case _, ok := <-w.Changes(): 560 c.Fatalf("watcher sent unexpected change: (_, %v)", ok) 561 case <-time.After(coretesting.ShortWait): 562 } 563 } 564 565 assertChange := func(change corewatcher.SecretTriggerChange) { 566 select { 567 case changes, ok := <-w.Changes(): 568 c.Check(ok, jc.IsTrue) 569 c.Assert(changes, gc.HasLen, 1) 570 c.Assert(changes[0], jc.DeepEquals, change) 571 case <-time.After(coretesting.LongWait): 572 c.Fatalf("watcher didn't emit an event") 573 } 574 } 575 576 // Initial event. 577 assertChange(corewatcher.SecretTriggerChange{ 578 URI: uri, 579 NextTriggerTime: nexRotateTime.Round(time.Second).UTC(), 580 }) 581 return uri, assertChange, assertNoChange, stop 582 } 583 584 type fakeToken struct{} 585 586 func (t *fakeToken) Check() error { 587 return nil 588 } 589 590 func (s *watcherSuite) TestSecretsRotationWatcher(c *gc.C) { 591 uri, assertChange, assertNoChange, stop := s.setupSecretRotationWatcher(c) 592 defer stop() 593 594 store := state.NewSecrets(s.State) 595 596 nexRotateTime := time.Now().Add(24 * time.Hour).Round(time.Second) 597 _, err := store.UpdateSecret(uri, state.UpdateSecretParams{ 598 LeaderToken: &fakeToken{}, 599 NextRotateTime: ptr(nexRotateTime), 600 }) 601 c.Assert(err, jc.ErrorIsNil) 602 603 assertChange(corewatcher.SecretTriggerChange{ 604 URI: uri, 605 NextTriggerTime: nexRotateTime, 606 }) 607 assertNoChange() 608 609 _, err = store.UpdateSecret(uri, state.UpdateSecretParams{ 610 LeaderToken: &fakeToken{}, 611 RotatePolicy: ptr(secrets.RotateNever), 612 }) 613 c.Assert(err, jc.ErrorIsNil) 614 615 assertChange(corewatcher.SecretTriggerChange{ 616 URI: uri, 617 NextTriggerTime: time.Time{}, 618 }) 619 assertNoChange() 620 } 621 622 func (s *watcherSuite) setupSecretExpiryWatcher( 623 c *gc.C, 624 ) (*secrets.URI, func(corewatcher.SecretTriggerChange), func(), func()) { 625 app := s.Factory.MakeApplication(c, &factory.ApplicationParams{Name: "mysql"}) 626 unit, password := s.Factory.MakeUnitReturningPassword(c, &factory.UnitParams{ 627 Application: app, 628 }) 629 store := state.NewSecrets(s.State) 630 uri := secrets.NewURI() 631 nexRotateTime := time.Now().Add(time.Hour) 632 _, err := store.CreateSecret(uri, state.CreateSecretParams{ 633 Owner: unit.Tag(), 634 UpdateSecretParams: state.UpdateSecretParams{ 635 LeaderToken: &fakeToken{}, 636 RotatePolicy: ptr(secrets.RotateDaily), 637 NextRotateTime: ptr(nexRotateTime), 638 Data: map[string]string{"foo": "bar"}, 639 }, 640 }) 641 c.Assert(err, jc.ErrorIsNil) 642 643 s.WaitForModelWatchersIdle(c, s.Model.UUID()) 644 645 apiInfo := s.APIInfo(c) 646 apiInfo.Tag = unit.Tag() 647 apiInfo.Password = password 648 apiInfo.ModelTag = s.Model.ModelTag() 649 650 apiConn, err := api.Open(apiInfo, api.DialOpts{}) 651 c.Assert(err, jc.ErrorIsNil) 652 653 client := secretsmanager.NewClient(apiConn) 654 w, err := client.WatchSecretsRotationChanges(unit.Tag()) 655 if !c.Check(err, jc.ErrorIsNil) { 656 _ = apiConn.Close() 657 c.FailNow() 658 } 659 stop := func() { 660 workertest.CleanKill(c, w) 661 _ = apiConn.Close() 662 } 663 664 assertNoChange := func() { 665 select { 666 case _, ok := <-w.Changes(): 667 c.Fatalf("watcher sent unexpected change: (_, %v)", ok) 668 case <-time.After(coretesting.ShortWait): 669 } 670 } 671 672 assertChange := func(change corewatcher.SecretTriggerChange) { 673 select { 674 case changes, ok := <-w.Changes(): 675 c.Check(ok, jc.IsTrue) 676 c.Assert(changes, gc.HasLen, 1) 677 c.Assert(changes[0], jc.DeepEquals, change) 678 case <-time.After(coretesting.LongWait): 679 c.Fatalf("watcher didn't emit an event") 680 } 681 } 682 683 // Initial event. 684 assertChange(corewatcher.SecretTriggerChange{ 685 URI: uri, 686 NextTriggerTime: nexRotateTime.Round(time.Second).UTC(), 687 }) 688 return uri, assertChange, assertNoChange, stop 689 } 690 691 func (s *watcherSuite) TestSecretsExpiryWatcher(c *gc.C) { 692 uri, assertChange, assertNoChange, stop := s.setupSecretExpiryWatcher(c) 693 defer stop() 694 695 store := state.NewSecrets(s.State) 696 697 nexRotateTime := time.Now().Add(24 * time.Hour).Round(time.Second) 698 _, err := store.UpdateSecret(uri, state.UpdateSecretParams{ 699 LeaderToken: &fakeToken{}, 700 NextRotateTime: ptr(nexRotateTime), 701 }) 702 c.Assert(err, jc.ErrorIsNil) 703 704 assertChange(corewatcher.SecretTriggerChange{ 705 URI: uri, 706 NextTriggerTime: nexRotateTime, 707 }) 708 assertNoChange() 709 710 _, err = store.UpdateSecret(uri, state.UpdateSecretParams{ 711 LeaderToken: &fakeToken{}, 712 RotatePolicy: ptr(secrets.RotateNever), 713 }) 714 c.Assert(err, jc.ErrorIsNil) 715 716 assertChange(corewatcher.SecretTriggerChange{ 717 URI: uri, 718 NextTriggerTime: time.Time{}, 719 }) 720 assertNoChange() 721 } 722 723 func (s *watcherSuite) setupSecretsRevisionWatcher( 724 c *gc.C, 725 ) (*secrets.URI, func(uri *secrets.URI, rev int), func(), func()) { 726 // Set up the offer. 727 app := s.AddTestingApplication(c, "mysql", s.AddTestingCharm(c, "mysql")) 728 unit, password := s.Factory.MakeUnitReturningPassword(c, &factory.UnitParams{ 729 Application: app, 730 }) 731 s.Factory.MakeUser(c, &factory.UserParams{Name: "fred"}) 732 offers := state.NewApplicationOffers(s.State) 733 offer, err := offers.AddOffer(crossmodel.AddApplicationOfferArgs{ 734 OfferName: "hosted-mysql", 735 ApplicationName: "mysql", 736 Owner: "admin", 737 }) 738 c.Assert(err, jc.ErrorIsNil) 739 740 // Add the consume permission for the offer so the macaroon 741 // discharge can occur. 742 err = s.State.CreateOfferAccess( 743 names.NewApplicationOfferTag(offer.OfferUUID), 744 names.NewUserTag("fred"), permission.ConsumeAccess) 745 c.Assert(err, jc.ErrorIsNil) 746 747 // Create a macaroon for authorisation. 748 bStore, err := s.State.NewBakeryStorage() 749 c.Assert(err, jc.ErrorIsNil) 750 b := bakery.New(bakery.BakeryParams{ 751 RootKeyStore: bStore, 752 }) 753 c.Assert(err, jc.ErrorIsNil) 754 mac, err := b.Oven.NewMacaroon( 755 context.Background(), 756 bakery.LatestVersion, 757 []checkers.Caveat{ 758 checkers.DeclaredCaveat("source-model-uuid", s.State.ModelUUID()), 759 checkers.DeclaredCaveat("offer-uuid", offer.OfferUUID), 760 checkers.DeclaredCaveat("username", "fred"), 761 }, bakery.Op{ 762 Entity: "offer-uuid", 763 Action: "consume", 764 }) 765 c.Assert(err, jc.ErrorIsNil) 766 767 remoteApp, err := s.State.AddRemoteApplication(state.AddRemoteApplicationParams{ 768 Name: "foo", OfferUUID: offer.OfferUUID, URL: "me/model.foo", SourceModel: s.Model.ModelTag()}) 769 c.Assert(err, jc.ErrorIsNil) 770 remoteRel, err := s.State.AddRelation( 771 state.Endpoint{"mysql", charm.Relation{Name: "server", Interface: "mysql", Role: charm.RoleProvider, Scope: charm.ScopeGlobal}}, 772 state.Endpoint{"foo", charm.Relation{Name: "db", Interface: "mysql", Role: charm.RoleRequirer, Scope: charm.ScopeGlobal}}, 773 ) 774 c.Assert(err, jc.ErrorIsNil) 775 776 _, err = s.State.AddOfferConnection(state.AddOfferConnectionParams{ 777 SourceModelUUID: utils.MustNewUUID().String(), 778 OfferUUID: offer.OfferUUID, 779 Username: "fred", 780 RelationId: remoteRel.Id(), 781 RelationKey: remoteRel.Tag().Id(), 782 }) 783 c.Assert(err, jc.ErrorIsNil) 784 785 // Export the remote entities so they can be found with a token. 786 re := s.State.RemoteEntities() 787 appToken, err := re.ExportLocalEntity(remoteApp.Tag()) 788 c.Assert(err, jc.ErrorIsNil) 789 relToken, err := re.ExportLocalEntity(remoteRel.Tag()) 790 c.Assert(err, jc.ErrorIsNil) 791 792 // Create a secret to watch. 793 store := state.NewSecrets(s.State) 794 uri := secrets.NewURI() 795 _, err = store.CreateSecret(uri, state.CreateSecretParams{ 796 Owner: unit.Tag(), 797 UpdateSecretParams: state.UpdateSecretParams{ 798 LeaderToken: &fakeToken{}, 799 RotatePolicy: ptr(secrets.RotateDaily), 800 Data: map[string]string{"foo": "bar"}, 801 }, 802 }) 803 c.Assert(err, jc.ErrorIsNil) 804 805 s.WaitForModelWatchersIdle(c, s.Model.UUID()) 806 807 apiInfo := s.APIInfo(c) 808 apiInfo.Tag = unit.Tag() 809 apiInfo.Password = password 810 apiInfo.ModelTag = s.Model.ModelTag() 811 812 apiConn, err := api.Open(apiInfo, api.DialOpts{}) 813 c.Assert(err, jc.ErrorIsNil) 814 815 client := crossmodelrelations.NewClient(apiConn) 816 w, err := client.WatchConsumedSecretsChanges(appToken, relToken, mac.M()) 817 if !c.Check(err, jc.ErrorIsNil) { 818 _ = apiConn.Close() 819 c.FailNow() 820 } 821 stop := func() { 822 workertest.CleanKill(c, w) 823 _ = apiConn.Close() 824 } 825 826 assertNoChange := func() { 827 select { 828 case _, ok := <-w.Changes(): 829 c.Fatalf("watcher sent unexpected change: (_, %v)", ok) 830 case <-time.After(coretesting.ShortWait): 831 } 832 } 833 834 assertChange := func(uri *secrets.URI, rev int) { 835 select { 836 case changes, ok := <-w.Changes(): 837 c.Check(ok, jc.IsTrue) 838 if uri == nil { 839 c.Assert(changes, gc.HasLen, 0) 840 break 841 } 842 c.Assert(changes, gc.HasLen, 1) 843 c.Assert(changes[0].URI.String(), gc.Equals, uri.String()) 844 c.Assert(changes[0].Revision, gc.Equals, rev) 845 case <-time.After(coretesting.LongWait): 846 c.Fatalf("watcher didn't emit an event") 847 } 848 } 849 return uri, assertChange, assertNoChange, stop 850 } 851 852 func (s *watcherSuite) TestCrossModelSecretsRevisionWatcher(c *gc.C) { 853 uri, assertChange, assertNoChange, stop := s.setupSecretsRevisionWatcher(c) 854 defer stop() 855 856 store := state.NewSecrets(s.State) 857 858 // Initial event - no changes since we're at rev 1 still. 859 assertChange(nil, 0) 860 861 err := s.State.SaveSecretRemoteConsumer(uri, names.NewUnitTag("foo/0"), &secrets.SecretConsumerMetadata{ 862 CurrentRevision: 1, 863 LatestRevision: 1, 864 }) 865 c.Assert(err, jc.ErrorIsNil) 866 assertNoChange() 867 868 _, err = store.UpdateSecret(uri, state.UpdateSecretParams{ 869 LeaderToken: &fakeToken{}, 870 Data: secrets.SecretData{"foo": "bar2"}, 871 }) 872 c.Assert(err, jc.ErrorIsNil) 873 874 assertChange(uri, 2) 875 assertNoChange() 876 } 877 878 type migrationSuite struct { 879 testing.JujuConnSuite 880 } 881 882 var _ = gc.Suite(&migrationSuite{}) 883 884 func (s *migrationSuite) TestMigrationStatusWatcher(c *gc.C) { 885 const nonce = "noncey" 886 887 // Create a model to migrate. 888 hostedState := s.Factory.MakeModel(c, &factory.ModelParams{}) 889 defer hostedState.Close() 890 hostedFactory := factory.NewFactory(hostedState, s.StatePool) 891 892 // Create a machine in the hosted model to connect as. 893 m, password := hostedFactory.MakeMachineReturningPassword(c, &factory.MachineParams{ 894 Nonce: nonce, 895 }) 896 897 // Connect as the machine to watch for migration status. 898 apiInfo := s.APIInfo(c) 899 apiInfo.Tag = m.Tag() 900 apiInfo.Password = password 901 902 hostedModel, err := hostedState.Model() 903 c.Assert(err, jc.ErrorIsNil) 904 905 apiInfo.ModelTag = hostedModel.ModelTag() 906 apiInfo.Nonce = nonce 907 908 apiConn, err := api.Open(apiInfo, api.DialOpts{}) 909 c.Assert(err, jc.ErrorIsNil) 910 defer apiConn.Close() 911 912 // Start watching for a migration. 913 client := migrationminion.NewClient(apiConn) 914 w, err := client.Watch() 915 c.Assert(err, jc.ErrorIsNil) 916 defer func() { 917 c.Assert(worker.Stop(w), jc.ErrorIsNil) 918 }() 919 920 assertNoChange := func() { 921 select { 922 case _, ok := <-w.Changes(): 923 c.Fatalf("watcher sent unexpected change: (_, %v)", ok) 924 case <-time.After(coretesting.ShortWait): 925 } 926 } 927 928 assertChange := func(id string, phase migration.Phase) { 929 select { 930 case status, ok := <-w.Changes(): 931 c.Assert(ok, jc.IsTrue) 932 c.Check(status.MigrationId, gc.Equals, id) 933 c.Check(status.Phase, gc.Equals, phase) 934 case <-time.After(coretesting.LongWait): 935 c.Fatalf("watcher didn't emit an event") 936 } 937 assertNoChange() 938 } 939 940 // Initial event with no migration in progress. 941 assertChange("", migration.NONE) 942 943 // Now create a migration, should trigger watcher. 944 spec := state.MigrationSpec{ 945 InitiatedBy: names.NewUserTag("someone"), 946 TargetInfo: migration.TargetInfo{ 947 ControllerTag: names.NewControllerTag(utils.MustNewUUID().String()), 948 Addrs: []string{"1.2.3.4:5"}, 949 CACert: "cert", 950 AuthTag: names.NewUserTag("dog"), 951 Password: "sekret", 952 }, 953 } 954 mig, err := hostedState.CreateMigration(spec) 955 c.Assert(err, jc.ErrorIsNil) 956 assertChange(mig.Id(), migration.QUIESCE) 957 958 // Now abort the migration, this should be reported too. 959 c.Assert(mig.SetPhase(migration.ABORT), jc.ErrorIsNil) 960 assertChange(mig.Id(), migration.ABORT) 961 c.Assert(mig.SetPhase(migration.ABORTDONE), jc.ErrorIsNil) 962 assertChange(mig.Id(), migration.ABORTDONE) 963 964 // Start a new migration, this should also trigger. 965 mig2, err := hostedState.CreateMigration(spec) 966 c.Assert(err, jc.ErrorIsNil) 967 assertChange(mig2.Id(), migration.QUIESCE) 968 }