github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/worker/uniter/relation/relations_test.go (about) 1 // Copyright 2015 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package relation_test 5 6 import ( 7 "io/ioutil" 8 "os" 9 "path/filepath" 10 "sync/atomic" 11 12 "github.com/juju/errors" 13 "github.com/juju/testing" 14 jc "github.com/juju/testing/checkers" 15 gc "gopkg.in/check.v1" 16 "gopkg.in/juju/charm.v6" 17 "gopkg.in/juju/charm.v6/hooks" 18 "gopkg.in/juju/names.v2" 19 20 apitesting "github.com/juju/juju/api/base/testing" 21 "github.com/juju/juju/api/uniter" 22 "github.com/juju/juju/apiserver/common" 23 "github.com/juju/juju/apiserver/params" 24 "github.com/juju/juju/core/leadership" 25 "github.com/juju/juju/state/multiwatcher" 26 coretesting "github.com/juju/juju/testing" 27 "github.com/juju/juju/worker/uniter/hook" 28 "github.com/juju/juju/worker/uniter/operation" 29 "github.com/juju/juju/worker/uniter/relation" 30 "github.com/juju/juju/worker/uniter/remotestate" 31 "github.com/juju/juju/worker/uniter/resolver" 32 "github.com/juju/juju/worker/uniter/runner/context" 33 ) 34 35 /* 36 TODO(wallyworld) 37 DO NOT COPY THE METHODOLOGY USED IN THESE TESTS. 38 We want to write unit tests without resorting to JujuConnSuite. 39 However, the current api/uniter code uses structs instead of 40 interfaces for its component model, and it's not possible to 41 implement a stub uniter api at the model level due to the way 42 the domain objects reference each other. 43 44 The best we can do for now is to stub out the facade caller and 45 return curated values for each API call. 46 */ 47 48 type relationsSuite struct { 49 coretesting.BaseSuite 50 51 stateDir string 52 relationsDir string 53 leadershipContextFunc relation.LeadershipContextFunc 54 } 55 56 var _ = gc.Suite(&relationsSuite{}) 57 58 type apiCall struct { 59 request string 60 args interface{} 61 result interface{} 62 err error 63 } 64 65 func uniterAPICall(request string, args, result interface{}, err error) apiCall { 66 return apiCall{ 67 request: request, 68 args: args, 69 result: result, 70 err: err, 71 } 72 } 73 74 func mockAPICaller(c *gc.C, callNumber *int32, apiCalls ...apiCall) apitesting.APICallerFunc { 75 apiCaller := apitesting.APICallerFunc(func(objType string, version int, id, request string, arg, result interface{}) error { 76 switch objType { 77 case "NotifyWatcher": 78 return nil 79 case "Uniter": 80 index := int(atomic.AddInt32(callNumber, 1)) - 1 81 c.Check(index < len(apiCalls), jc.IsTrue) 82 call := apiCalls[index] 83 c.Logf("request %d, %s", index, request) 84 c.Check(version, gc.Equals, 9) 85 c.Check(id, gc.Equals, "") 86 c.Check(request, gc.Equals, call.request) 87 c.Check(arg, jc.DeepEquals, call.args) 88 if call.err != nil { 89 return common.ServerError(call.err) 90 } 91 testing.PatchValue(result, call.result) 92 default: 93 c.Fail() 94 } 95 return nil 96 }) 97 return apiCaller 98 } 99 100 type stubLeadershipContext struct { 101 context.LeadershipContext 102 isLeader bool 103 } 104 105 func (stub *stubLeadershipContext) IsLeader() (bool, error) { 106 return stub.isLeader, nil 107 } 108 109 var minimalMetadata = ` 110 name: wordpress 111 summary: "test" 112 description: "test" 113 requires: 114 mysql: db 115 `[1:] 116 117 func (s *relationsSuite) SetUpTest(c *gc.C) { 118 s.stateDir = filepath.Join(c.MkDir(), "charm") 119 err := os.MkdirAll(s.stateDir, 0755) 120 c.Assert(err, jc.ErrorIsNil) 121 err = ioutil.WriteFile(filepath.Join(s.stateDir, "metadata.yaml"), []byte(minimalMetadata), 0755) 122 c.Assert(err, jc.ErrorIsNil) 123 s.relationsDir = filepath.Join(c.MkDir(), "relations") 124 s.leadershipContextFunc = func(accessor context.LeadershipSettingsAccessor, tracker leadership.Tracker, unitName string) context.LeadershipContext { 125 return &stubLeadershipContext{isLeader: true} 126 } 127 } 128 129 func assertNumCalls(c *gc.C, numCalls *int32, expected int32) { 130 v := atomic.LoadInt32(numCalls) 131 c.Assert(v, gc.Equals, expected) 132 } 133 134 func (s *relationsSuite) setupRelations(c *gc.C) relation.Relations { 135 unitTag := names.NewUnitTag("wordpress/0") 136 abort := make(chan struct{}) 137 138 var numCalls int32 139 unitEntity := params.Entities{Entities: []params.Entity{{Tag: "unit-wordpress-0"}}} 140 apiCaller := mockAPICaller(c, &numCalls, 141 uniterAPICall("Refresh", unitEntity, params.UnitRefreshResults{Results: []params.UnitRefreshResult{{Life: params.Alive, Resolved: params.ResolvedNone}}}, nil), 142 uniterAPICall("GetPrincipal", unitEntity, params.StringBoolResults{Results: []params.StringBoolResult{{Result: "", Ok: false}}}, nil), 143 uniterAPICall("RelationsStatus", unitEntity, params.RelationUnitStatusResults{Results: []params.RelationUnitStatusResult{{RelationResults: []params.RelationUnitStatus{}}}}, nil), 144 ) 145 st := uniter.NewState(apiCaller, unitTag) 146 r, err := relation.NewRelations( 147 relation.RelationsConfig{ 148 State: st, 149 UnitTag: unitTag, 150 CharmDir: s.stateDir, 151 RelationsDir: s.relationsDir, 152 NewLeadershipContext: s.leadershipContextFunc, 153 Abort: abort, 154 }) 155 c.Assert(err, jc.ErrorIsNil) 156 assertNumCalls(c, &numCalls, 3) 157 return r 158 } 159 160 func (s *relationsSuite) TestNewRelationsNoRelations(c *gc.C) { 161 r := s.setupRelations(c) 162 //No relations created. 163 c.Assert(r.GetInfo(), gc.HasLen, 0) 164 } 165 166 func (s *relationsSuite) assertNewRelationsWithExistingRelations(c *gc.C, isLeader bool) { 167 unitTag := names.NewUnitTag("wordpress/0") 168 abort := make(chan struct{}) 169 s.leadershipContextFunc = func(accessor context.LeadershipSettingsAccessor, tracker leadership.Tracker, unitName string) context.LeadershipContext { 170 return &stubLeadershipContext{isLeader: isLeader} 171 } 172 173 var numCalls int32 174 unitEntity := params.Entities{Entities: []params.Entity{{Tag: "unit-wordpress-0"}}} 175 relationUnits := params.RelationUnits{RelationUnits: []params.RelationUnit{ 176 {Relation: "relation-wordpress.db#mysql.db", Unit: "unit-wordpress-0"}, 177 }} 178 relationResults := params.RelationResults{ 179 Results: []params.RelationResult{ 180 { 181 Id: 1, 182 Key: "wordpress:db mysql:db", 183 Life: params.Alive, 184 Endpoint: multiwatcher.Endpoint{ 185 ApplicationName: "wordpress", 186 Relation: multiwatcher.CharmRelation{Name: "mysql", Role: string(charm.RoleProvider), Interface: "db"}, 187 }}, 188 }, 189 } 190 relationStatus := params.RelationStatusArgs{Args: []params.RelationStatusArg{{ 191 UnitTag: "unit-wordpress-0", 192 RelationId: 1, 193 Status: params.Joined, 194 }}} 195 196 apiCalls := []apiCall{ 197 uniterAPICall("Refresh", unitEntity, params.UnitRefreshResults{Results: []params.UnitRefreshResult{{Life: params.Alive, Resolved: params.ResolvedNone}}}, nil), 198 uniterAPICall("GetPrincipal", unitEntity, params.StringBoolResults{Results: []params.StringBoolResult{{Result: "", Ok: false}}}, nil), 199 uniterAPICall("RelationsStatus", unitEntity, params.RelationUnitStatusResults{Results: []params.RelationUnitStatusResult{ 200 {RelationResults: []params.RelationUnitStatus{{RelationTag: "relation-wordpress:db mysql:db", InScope: true}}}}}, nil), 201 uniterAPICall("Relation", relationUnits, relationResults, nil), 202 uniterAPICall("Relation", relationUnits, relationResults, nil), 203 uniterAPICall("Watch", unitEntity, params.NotifyWatchResults{Results: []params.NotifyWatchResult{{NotifyWatcherId: "1"}}}, nil), 204 uniterAPICall("EnterScope", relationUnits, params.ErrorResults{Results: []params.ErrorResult{{}}}, nil), 205 } 206 if isLeader { 207 apiCalls = append(apiCalls, 208 uniterAPICall("SetRelationStatus", relationStatus, noErrorResult, nil), 209 ) 210 } 211 apiCaller := mockAPICaller(c, &numCalls, apiCalls...) 212 st := uniter.NewState(apiCaller, unitTag) 213 r, err := relation.NewRelations( 214 relation.RelationsConfig{ 215 State: st, 216 UnitTag: unitTag, 217 CharmDir: s.stateDir, 218 RelationsDir: s.relationsDir, 219 NewLeadershipContext: s.leadershipContextFunc, 220 Abort: abort, 221 }) 222 c.Assert(err, jc.ErrorIsNil) 223 assertNumCalls(c, &numCalls, int32(len(apiCalls))) 224 225 info := r.GetInfo() 226 c.Assert(info, gc.HasLen, 1) 227 oneInfo := info[1] 228 c.Assert(oneInfo.RelationUnit.Relation().Tag(), gc.Equals, names.NewRelationTag("wordpress:db mysql:db")) 229 c.Assert(oneInfo.RelationUnit.Endpoint(), jc.DeepEquals, uniter.Endpoint{ 230 Relation: charm.Relation{Name: "mysql", Role: "provider", Interface: "db", Optional: false, Limit: 0, Scope: ""}, 231 }) 232 c.Assert(oneInfo.MemberNames, gc.HasLen, 0) 233 } 234 235 func (s *relationsSuite) TestNewRelationsWithExistingRelationsLeader(c *gc.C) { 236 s.assertNewRelationsWithExistingRelations(c, true) 237 } 238 239 func (s *relationsSuite) TestNewRelationsWithExistingRelationsNotLeader(c *gc.C) { 240 s.assertNewRelationsWithExistingRelations(c, false) 241 } 242 243 func (s *relationsSuite) TestNextOpNothing(c *gc.C) { 244 unitTag := names.NewUnitTag("wordpress/0") 245 abort := make(chan struct{}) 246 247 var numCalls int32 248 unitEntity := params.Entities{Entities: []params.Entity{{Tag: "unit-wordpress-0"}}} 249 apiCaller := mockAPICaller(c, &numCalls, 250 uniterAPICall("Refresh", unitEntity, params.UnitRefreshResults{Results: []params.UnitRefreshResult{{Life: params.Alive, Resolved: params.ResolvedNone}}}, nil), 251 uniterAPICall("GetPrincipal", unitEntity, params.StringBoolResults{Results: []params.StringBoolResult{{Result: "", Ok: false}}}, nil), 252 uniterAPICall("RelationsStatus", unitEntity, params.RelationUnitStatusResults{Results: []params.RelationUnitStatusResult{{RelationResults: []params.RelationUnitStatus{}}}}, nil), 253 ) 254 st := uniter.NewState(apiCaller, unitTag) 255 r, err := relation.NewRelations( 256 relation.RelationsConfig{ 257 State: st, 258 UnitTag: unitTag, 259 CharmDir: s.stateDir, 260 RelationsDir: s.relationsDir, 261 NewLeadershipContext: s.leadershipContextFunc, 262 Abort: abort, 263 }) 264 c.Assert(err, jc.ErrorIsNil) 265 assertNumCalls(c, &numCalls, 3) 266 267 localState := resolver.LocalState{ 268 State: operation.State{ 269 Kind: operation.Continue, 270 }, 271 } 272 remoteState := remotestate.Snapshot{} 273 relationsResolver := relation.NewRelationsResolver(r) 274 _, err = relationsResolver.NextOp(localState, remoteState, &mockOperations{}) 275 c.Assert(errors.Cause(err), gc.Equals, resolver.ErrNoOperation) 276 } 277 278 func relationJoinedAPICalls() []apiCall { 279 unitEntity := params.Entities{Entities: []params.Entity{{Tag: "unit-wordpress-0"}}} 280 relationResults := params.RelationResults{ 281 Results: []params.RelationResult{ 282 { 283 Id: 1, 284 Key: "wordpress:db mysql:db", 285 Life: params.Alive, 286 Endpoint: multiwatcher.Endpoint{ 287 ApplicationName: "wordpress", 288 Relation: multiwatcher.CharmRelation{Name: "mysql", Role: string(charm.RoleRequirer), Interface: "db", Scope: "global"}, 289 }}, 290 }, 291 } 292 relationUnits := params.RelationUnits{RelationUnits: []params.RelationUnit{ 293 {Relation: "relation-wordpress.db#mysql.db", Unit: "unit-wordpress-0"}, 294 }} 295 relationStatus := params.RelationStatusArgs{Args: []params.RelationStatusArg{{ 296 UnitTag: "unit-wordpress-0", 297 RelationId: 1, 298 Status: params.Joined, 299 }}} 300 apiCalls := []apiCall{ 301 uniterAPICall("Refresh", unitEntity, params.UnitRefreshResults{Results: []params.UnitRefreshResult{{Life: params.Alive, Resolved: params.ResolvedNone}}}, nil), 302 uniterAPICall("GetPrincipal", unitEntity, params.StringBoolResults{Results: []params.StringBoolResult{{Result: "", Ok: false}}}, nil), 303 uniterAPICall("RelationsStatus", unitEntity, params.RelationUnitStatusResults{Results: []params.RelationUnitStatusResult{{RelationResults: []params.RelationUnitStatus{}}}}, nil), 304 uniterAPICall("RelationById", params.RelationIds{RelationIds: []int{1}}, relationResults, nil), 305 uniterAPICall("Relation", relationUnits, relationResults, nil), 306 uniterAPICall("Relation", relationUnits, relationResults, nil), 307 uniterAPICall("Watch", unitEntity, params.NotifyWatchResults{Results: []params.NotifyWatchResult{{NotifyWatcherId: "1"}}}, nil), 308 uniterAPICall("EnterScope", relationUnits, params.ErrorResults{Results: []params.ErrorResult{{}}}, nil), 309 uniterAPICall("SetRelationStatus", relationStatus, noErrorResult, nil), 310 } 311 return apiCalls 312 } 313 314 func (s *relationsSuite) assertHookRelationJoined(c *gc.C, numCalls *int32, apiCalls ...apiCall) relation.Relations { 315 unitTag := names.NewUnitTag("wordpress/0") 316 abort := make(chan struct{}) 317 318 apiCaller := mockAPICaller(c, numCalls, apiCalls...) 319 st := uniter.NewState(apiCaller, unitTag) 320 r, err := relation.NewRelations( 321 relation.RelationsConfig{ 322 State: st, 323 UnitTag: unitTag, 324 CharmDir: s.stateDir, 325 RelationsDir: s.relationsDir, 326 NewLeadershipContext: s.leadershipContextFunc, 327 Abort: abort, 328 }) 329 c.Assert(err, jc.ErrorIsNil) 330 assertNumCalls(c, numCalls, 3) 331 332 localState := resolver.LocalState{ 333 State: operation.State{ 334 Kind: operation.Continue, 335 }, 336 } 337 remoteState := remotestate.Snapshot{ 338 Relations: map[int]remotestate.RelationSnapshot{ 339 1: { 340 Life: params.Alive, 341 Suspended: false, 342 Members: map[string]int64{ 343 "wordpress": 1, 344 }, 345 }, 346 }, 347 } 348 relationsResolver := relation.NewRelationsResolver(r) 349 op, err := relationsResolver.NextOp(localState, remoteState, &mockOperations{}) 350 c.Assert(err, jc.ErrorIsNil) 351 assertNumCalls(c, numCalls, 9) 352 c.Assert(op.String(), gc.Equals, "run hook relation-joined on unit with relation 1") 353 354 // Commit the operation so we save local state for any next operation. 355 _, err = r.PrepareHook(op.(*mockOperation).hookInfo) 356 c.Assert(err, jc.ErrorIsNil) 357 err = r.CommitHook(op.(*mockOperation).hookInfo) 358 c.Assert(err, jc.ErrorIsNil) 359 return r 360 } 361 362 func (s *relationsSuite) TestHookRelationJoined(c *gc.C) { 363 var numCalls int32 364 s.assertHookRelationJoined(c, &numCalls, relationJoinedAPICalls()...) 365 } 366 367 func (s *relationsSuite) assertHookRelationChanged( 368 c *gc.C, r relation.Relations, 369 remoteRelationSnapshot remotestate.RelationSnapshot, 370 numCalls *int32, 371 ) { 372 numCallsBefore := *numCalls 373 localState := resolver.LocalState{ 374 State: operation.State{ 375 Kind: operation.Continue, 376 }, 377 } 378 remoteState := remotestate.Snapshot{ 379 Relations: map[int]remotestate.RelationSnapshot{ 380 1: remoteRelationSnapshot, 381 }, 382 } 383 relationsResolver := relation.NewRelationsResolver(r) 384 op, err := relationsResolver.NextOp(localState, remoteState, &mockOperations{}) 385 c.Assert(err, jc.ErrorIsNil) 386 assertNumCalls(c, numCalls, numCallsBefore) 387 c.Assert(op.String(), gc.Equals, "run hook relation-changed on unit with relation 1") 388 389 // Commit the operation so we save local state for any next operation. 390 _, err = r.PrepareHook(op.(*mockOperation).hookInfo) 391 c.Assert(err, jc.ErrorIsNil) 392 err = r.CommitHook(op.(*mockOperation).hookInfo) 393 c.Assert(err, jc.ErrorIsNil) 394 } 395 396 func (s *relationsSuite) TestHookRelationChanged(c *gc.C) { 397 var numCalls int32 398 apiCalls := relationJoinedAPICalls() 399 r := s.assertHookRelationJoined(c, &numCalls, apiCalls...) 400 401 // There will be an initial relation-changed regardless of 402 // members, due to the "changed pending" local persistent 403 // state. 404 s.assertHookRelationChanged(c, r, remotestate.RelationSnapshot{ 405 Life: params.Alive, 406 Suspended: false, 407 }, &numCalls) 408 409 // wordpress starts at 1, changing to 2 should trigger a 410 // relation-changed hook. 411 s.assertHookRelationChanged(c, r, remotestate.RelationSnapshot{ 412 Life: params.Alive, 413 Suspended: false, 414 Members: map[string]int64{ 415 "wordpress": 2, 416 }, 417 }, &numCalls) 418 419 // NOTE(axw) this is a test for the temporary to fix lp:1495542. 420 // 421 // wordpress is at 2, changing to 1 should trigger a 422 // relation-changed hook. This is to cater for the scenario 423 // where the relation settings document is removed and 424 // recreated, thus resetting the txn-revno. 425 s.assertHookRelationChanged(c, r, remotestate.RelationSnapshot{ 426 Life: params.Alive, 427 Members: map[string]int64{ 428 "wordpress": 1, 429 }, 430 }, &numCalls) 431 } 432 433 func (s *relationsSuite) TestHookRelationChangedSuspended(c *gc.C) { 434 var numCalls int32 435 apiCalls := relationJoinedAPICalls() 436 r := s.assertHookRelationJoined(c, &numCalls, apiCalls...) 437 438 // There will be an initial relation-changed regardless of 439 // members, due to the "changed pending" local persistent 440 // state. 441 s.assertHookRelationChanged(c, r, remotestate.RelationSnapshot{ 442 Life: params.Alive, 443 Suspended: true, 444 }, &numCalls) 445 c.Assert(r.GetInfo()[1].RelationUnit.Relation().Suspended(), jc.IsTrue) 446 447 numCallsBefore := numCalls 448 449 localState := resolver.LocalState{ 450 State: operation.State{ 451 Kind: operation.Continue, 452 }, 453 } 454 remoteState := remotestate.Snapshot{ 455 Relations: map[int]remotestate.RelationSnapshot{ 456 1: { 457 Life: params.Alive, 458 Suspended: true, 459 }, 460 }, 461 } 462 463 relationsResolver := relation.NewRelationsResolver(r) 464 op, err := relationsResolver.NextOp(localState, remoteState, &mockOperations{}) 465 c.Assert(err, jc.ErrorIsNil) 466 assertNumCalls(c, &numCalls, numCallsBefore) 467 c.Assert(op.String(), gc.Equals, "run hook relation-departed on unit with relation 1") 468 } 469 470 func (s *relationsSuite) assertHookRelationDeparted(c *gc.C, numCalls *int32, apiCalls ...apiCall) relation.Relations { 471 r := s.assertHookRelationJoined(c, numCalls, apiCalls...) 472 s.assertHookRelationChanged(c, r, remotestate.RelationSnapshot{ 473 Life: params.Alive, 474 Suspended: false, 475 }, numCalls) 476 numCallsBefore := *numCalls 477 478 localState := resolver.LocalState{ 479 State: operation.State{ 480 Kind: operation.Continue, 481 }, 482 } 483 remoteState := remotestate.Snapshot{ 484 Relations: map[int]remotestate.RelationSnapshot{ 485 1: { 486 Life: params.Dying, 487 Members: map[string]int64{ 488 "wordpress": 1, 489 }, 490 }, 491 }, 492 } 493 relationsResolver := relation.NewRelationsResolver(r) 494 op, err := relationsResolver.NextOp(localState, remoteState, &mockOperations{}) 495 c.Assert(err, jc.ErrorIsNil) 496 assertNumCalls(c, numCalls, numCallsBefore) 497 c.Assert(op.String(), gc.Equals, "run hook relation-departed on unit with relation 1") 498 499 // Commit the operation so we save local state for any next operation. 500 _, err = r.PrepareHook(op.(*mockOperation).hookInfo) 501 c.Assert(err, jc.ErrorIsNil) 502 err = r.CommitHook(op.(*mockOperation).hookInfo) 503 c.Assert(err, jc.ErrorIsNil) 504 return r 505 } 506 507 func (s *relationsSuite) TestHookRelationDeparted(c *gc.C) { 508 var numCalls int32 509 apiCalls := relationJoinedAPICalls() 510 511 s.assertHookRelationDeparted(c, &numCalls, apiCalls...) 512 } 513 514 func (s *relationsSuite) TestHookRelationBroken(c *gc.C) { 515 var numCalls int32 516 apiCalls := relationJoinedAPICalls() 517 518 r := s.assertHookRelationDeparted(c, &numCalls, apiCalls...) 519 520 localState := resolver.LocalState{ 521 State: operation.State{ 522 Kind: operation.Continue, 523 }, 524 } 525 remoteState := remotestate.Snapshot{ 526 Relations: map[int]remotestate.RelationSnapshot{ 527 1: { 528 Life: params.Dying, 529 }, 530 }, 531 } 532 relationsResolver := relation.NewRelationsResolver(r) 533 op, err := relationsResolver.NextOp(localState, remoteState, &mockOperations{}) 534 c.Assert(err, jc.ErrorIsNil) 535 assertNumCalls(c, &numCalls, 9) 536 c.Assert(op.String(), gc.Equals, "run hook relation-broken on unit with relation 1") 537 } 538 539 func (s *relationsSuite) TestHookRelationBrokenWhenSuspended(c *gc.C) { 540 var numCalls int32 541 apiCalls := relationJoinedAPICalls() 542 543 r := s.assertHookRelationDeparted(c, &numCalls, apiCalls...) 544 545 localState := resolver.LocalState{ 546 State: operation.State{ 547 Kind: operation.Continue, 548 }, 549 } 550 remoteState := remotestate.Snapshot{ 551 Relations: map[int]remotestate.RelationSnapshot{ 552 1: { 553 Life: params.Alive, 554 Suspended: true, 555 }, 556 }, 557 } 558 relationsResolver := relation.NewRelationsResolver(r) 559 op, err := relationsResolver.NextOp(localState, remoteState, &mockOperations{}) 560 c.Assert(err, jc.ErrorIsNil) 561 assertNumCalls(c, &numCalls, 9) 562 c.Assert(op.String(), gc.Equals, "run hook relation-broken on unit with relation 1") 563 } 564 565 func (s *relationsSuite) TestHookRelationBrokenOnlyOnce(c *gc.C) { 566 var numCalls int32 567 apiCalls := relationJoinedAPICalls() 568 relationUnits := params.RelationUnits{RelationUnits: []params.RelationUnit{ 569 {Relation: "relation-wordpress.db#mysql.db", Unit: "unit-wordpress-0"}, 570 }} 571 apiCalls = append(apiCalls, 572 uniterAPICall("LeaveScope", relationUnits, params.ErrorResults{Results: []params.ErrorResult{{}}}, nil), 573 ) 574 575 r := s.assertHookRelationDeparted(c, &numCalls, apiCalls...) 576 577 localState := resolver.LocalState{ 578 State: operation.State{ 579 Kind: operation.Continue, 580 }, 581 } 582 remoteState := remotestate.Snapshot{ 583 Relations: map[int]remotestate.RelationSnapshot{ 584 1: { 585 Life: params.Alive, 586 Suspended: true, 587 }, 588 }, 589 } 590 relationsResolver := relation.NewRelationsResolver(r) 591 592 // Remove the state directory to check that the hook is not run again. 593 err := os.RemoveAll(s.relationsDir) 594 c.Assert(err, jc.ErrorIsNil) 595 _, err = relationsResolver.NextOp(localState, remoteState, &mockOperations{}) 596 c.Assert(errors.Cause(err), gc.Equals, resolver.ErrNoOperation) 597 } 598 599 func (s *relationsSuite) TestCommitHook(c *gc.C) { 600 var numCalls int32 601 apiCalls := relationJoinedAPICalls() 602 relationUnits := params.RelationUnits{RelationUnits: []params.RelationUnit{ 603 {Relation: "relation-wordpress.db#mysql.db", Unit: "unit-wordpress-0"}, 604 }} 605 apiCalls = append(apiCalls, 606 uniterAPICall("LeaveScope", relationUnits, params.ErrorResults{Results: []params.ErrorResult{{}}}, nil), 607 ) 608 stateFile := filepath.Join(s.relationsDir, "1", "wordpress") 609 c.Assert(stateFile, jc.DoesNotExist) 610 r := s.assertHookRelationJoined(c, &numCalls, apiCalls...) 611 612 data, err := ioutil.ReadFile(stateFile) 613 c.Assert(err, jc.ErrorIsNil) 614 c.Assert(string(data), gc.Equals, "change-version: 1\nchanged-pending: true\n") 615 616 err = r.CommitHook(hook.Info{ 617 Kind: hooks.RelationChanged, 618 RemoteUnit: "wordpress", 619 RelationId: 1, 620 ChangeVersion: 2, 621 }) 622 c.Assert(err, jc.ErrorIsNil) 623 data, err = ioutil.ReadFile(stateFile) 624 c.Assert(err, jc.ErrorIsNil) 625 c.Assert(string(data), gc.Equals, "change-version: 2\n") 626 627 err = r.CommitHook(hook.Info{ 628 Kind: hooks.RelationDeparted, 629 RemoteUnit: "wordpress", 630 RelationId: 1, 631 }) 632 c.Assert(err, jc.ErrorIsNil) 633 c.Assert(stateFile, jc.DoesNotExist) 634 } 635 636 func (s *relationsSuite) TestImplicitRelationNoHooks(c *gc.C) { 637 unitTag := names.NewUnitTag("wordpress/0") 638 abort := make(chan struct{}) 639 640 unitEntity := params.Entities{Entities: []params.Entity{{Tag: "unit-wordpress-0"}}} 641 relationResults := params.RelationResults{ 642 Results: []params.RelationResult{ 643 { 644 Id: 1, 645 Key: "wordpress:juju-info juju-info:juju-info", 646 Life: params.Alive, 647 Endpoint: multiwatcher.Endpoint{ 648 ApplicationName: "wordpress", 649 Relation: multiwatcher.CharmRelation{Name: "juju-info", Role: string(charm.RoleProvider), Interface: "juju-info", Scope: "global"}, 650 }}, 651 }, 652 } 653 relationUnits := params.RelationUnits{RelationUnits: []params.RelationUnit{ 654 {Relation: "relation-wordpress.juju-info#juju-info.juju-info", Unit: "unit-wordpress-0"}, 655 }} 656 relationStatus := params.RelationStatusArgs{Args: []params.RelationStatusArg{{ 657 UnitTag: "unit-wordpress-0", 658 RelationId: 1, 659 Status: params.Joined, 660 }}} 661 apiCalls := []apiCall{ 662 uniterAPICall("Refresh", unitEntity, params.UnitRefreshResults{Results: []params.UnitRefreshResult{{Life: params.Alive, Resolved: params.ResolvedNone}}}, nil), 663 uniterAPICall("GetPrincipal", unitEntity, params.StringBoolResults{Results: []params.StringBoolResult{{Result: "", Ok: false}}}, nil), 664 uniterAPICall("RelationsStatus", unitEntity, params.RelationUnitStatusResults{Results: []params.RelationUnitStatusResult{{RelationResults: []params.RelationUnitStatus{}}}}, nil), 665 uniterAPICall("RelationById", params.RelationIds{RelationIds: []int{1}}, relationResults, nil), 666 uniterAPICall("Relation", relationUnits, relationResults, nil), 667 uniterAPICall("Relation", relationUnits, relationResults, nil), 668 uniterAPICall("Watch", unitEntity, params.NotifyWatchResults{Results: []params.NotifyWatchResult{{NotifyWatcherId: "1"}}}, nil), 669 uniterAPICall("EnterScope", relationUnits, params.ErrorResults{Results: []params.ErrorResult{{}}}, nil), 670 uniterAPICall("SetRelationStatus", relationStatus, noErrorResult, nil), 671 } 672 673 var numCalls int32 674 apiCaller := mockAPICaller(c, &numCalls, apiCalls...) 675 st := uniter.NewState(apiCaller, unitTag) 676 r, err := relation.NewRelations( 677 relation.RelationsConfig{ 678 State: st, 679 UnitTag: unitTag, 680 CharmDir: s.stateDir, 681 RelationsDir: s.relationsDir, 682 NewLeadershipContext: s.leadershipContextFunc, 683 Abort: abort, 684 }) 685 c.Assert(err, jc.ErrorIsNil) 686 687 localState := resolver.LocalState{ 688 State: operation.State{ 689 Kind: operation.Continue, 690 }, 691 } 692 remoteState := remotestate.Snapshot{ 693 Relations: map[int]remotestate.RelationSnapshot{ 694 1: { 695 Life: params.Alive, 696 Members: map[string]int64{ 697 "wordpress": 1, 698 }, 699 }, 700 }, 701 } 702 relationsResolver := relation.NewRelationsResolver(r) 703 _, err = relationsResolver.NextOp(localState, remoteState, &mockOperations{}) 704 c.Assert(errors.Cause(err), gc.Equals, resolver.ErrNoOperation) 705 } 706 707 var ( 708 noErrorResult = params.ErrorResults{Results: []params.ErrorResult{{}}} 709 nrpeUnitTag = names.NewUnitTag("nrpe/0") 710 nrpeUnitEntity = params.Entities{Entities: []params.Entity{{Tag: nrpeUnitTag.String()}}} 711 ) 712 713 func subSubRelationAPICalls() []apiCall { 714 relationStatusResults := params.RelationUnitStatusResults{Results: []params.RelationUnitStatusResult{{ 715 RelationResults: []params.RelationUnitStatus{{ 716 RelationTag: "relation-wordpress:juju-info nrpe:general-info", 717 InScope: true, 718 }, { 719 RelationTag: "relation-ntp:nrpe-external-master nrpe:external-master", 720 InScope: true, 721 }, 722 }}}} 723 relationUnits1 := params.RelationUnits{RelationUnits: []params.RelationUnit{ 724 {Relation: "relation-wordpress.juju-info#nrpe.general-info", Unit: "unit-nrpe-0"}, 725 }} 726 relationResults1 := params.RelationResults{ 727 Results: []params.RelationResult{{ 728 Id: 1, 729 Key: "wordpress:juju-info nrpe:general-info", 730 Life: params.Alive, 731 OtherApplication: "wordpress", 732 Endpoint: multiwatcher.Endpoint{ 733 ApplicationName: "nrpe", 734 Relation: multiwatcher.CharmRelation{ 735 Name: "general-info", 736 Role: string(charm.RoleRequirer), 737 Interface: "juju-info", 738 Scope: "container", 739 }, 740 }, 741 }}, 742 } 743 relationUnits2 := params.RelationUnits{RelationUnits: []params.RelationUnit{ 744 {Relation: "relation-ntp.nrpe-external-master#nrpe.external-master", Unit: "unit-nrpe-0"}, 745 }} 746 relationResults2 := params.RelationResults{ 747 Results: []params.RelationResult{{ 748 Id: 2, 749 Key: "ntp:nrpe-external-master nrpe:external-master", 750 Life: params.Alive, 751 OtherApplication: "ntp", 752 Endpoint: multiwatcher.Endpoint{ 753 ApplicationName: "nrpe", 754 Relation: multiwatcher.CharmRelation{ 755 Name: "external-master", 756 Role: string(charm.RoleRequirer), 757 Interface: "nrpe-external-master", 758 Scope: "container", 759 }, 760 }, 761 }}, 762 } 763 relationStatus1 := params.RelationStatusArgs{Args: []params.RelationStatusArg{{ 764 UnitTag: "unit-nrpe-0", 765 RelationId: 1, 766 Status: params.Joined, 767 }}} 768 relationStatus2 := params.RelationStatusArgs{Args: []params.RelationStatusArg{{ 769 UnitTag: "unit-nrpe-0", 770 RelationId: 2, 771 Status: params.Joined, 772 }}} 773 774 return []apiCall{ 775 uniterAPICall("Refresh", nrpeUnitEntity, params.UnitRefreshResults{Results: []params.UnitRefreshResult{{Life: params.Alive, Resolved: params.ResolvedNone}}}, nil), 776 uniterAPICall("GetPrincipal", nrpeUnitEntity, params.StringBoolResults{Results: []params.StringBoolResult{{Result: "unit-wordpress-0", Ok: true}}}, nil), 777 uniterAPICall("RelationsStatus", nrpeUnitEntity, relationStatusResults, nil), 778 uniterAPICall("Relation", relationUnits1, relationResults1, nil), 779 uniterAPICall("Relation", relationUnits2, relationResults2, nil), 780 uniterAPICall("Relation", relationUnits1, relationResults1, nil), 781 uniterAPICall("Watch", nrpeUnitEntity, params.NotifyWatchResults{Results: []params.NotifyWatchResult{{NotifyWatcherId: "1"}}}, nil), 782 uniterAPICall("EnterScope", relationUnits1, noErrorResult, nil), 783 uniterAPICall("SetRelationStatus", relationStatus1, noErrorResult, nil), 784 uniterAPICall("Relation", relationUnits2, relationResults2, nil), 785 uniterAPICall("Watch", nrpeUnitEntity, params.NotifyWatchResults{Results: []params.NotifyWatchResult{{NotifyWatcherId: "2"}}}, nil), 786 uniterAPICall("EnterScope", relationUnits2, noErrorResult, nil), 787 uniterAPICall("SetRelationStatus", relationStatus2, noErrorResult, nil), 788 } 789 } 790 791 func (s *relationsSuite) TestSubSubPrincipalRelationDyingDestroysUnit(c *gc.C) { 792 // When two subordinate units are related on a principal unit's 793 // machine, the sub-sub relation shouldn't keep them alive if the 794 // relation to the principal dies. 795 var numCalls int32 796 apiCalls := subSubRelationAPICalls() 797 callsBeforeDestroy := int32(len(apiCalls)) 798 callsAfterDestroy := callsBeforeDestroy + 1 799 // This should only be called once the relation to the 800 // principal app is destroyed. 801 apiCalls = append(apiCalls, uniterAPICall("Destroy", nrpeUnitEntity, noErrorResult, nil)) 802 apiCaller := mockAPICaller(c, &numCalls, apiCalls...) 803 804 st := uniter.NewState(apiCaller, nrpeUnitTag) 805 r, err := relation.NewRelations( 806 relation.RelationsConfig{ 807 State: st, 808 UnitTag: nrpeUnitTag, 809 CharmDir: s.stateDir, 810 RelationsDir: s.relationsDir, 811 NewLeadershipContext: s.leadershipContextFunc, 812 Abort: make(chan struct{}), 813 }) 814 c.Assert(err, jc.ErrorIsNil) 815 assertNumCalls(c, &numCalls, callsBeforeDestroy) 816 817 // So now we have a relations object with two relations, one to 818 // wordpress and one to ntp. We want to ensure that if the 819 // relation to wordpress changes to Dying, the unit is destroyed, 820 // even if the ntp relation is still going strong. 821 localState := resolver.LocalState{ 822 State: operation.State{ 823 Kind: operation.Continue, 824 }, 825 } 826 827 remoteState := remotestate.Snapshot{ 828 Relations: map[int]remotestate.RelationSnapshot{ 829 1: { 830 Life: params.Dying, 831 Members: map[string]int64{ 832 "wordpress/0": 1, 833 }, 834 }, 835 2: { 836 Life: params.Alive, 837 Members: map[string]int64{ 838 "ntp/0": 1, 839 }, 840 }, 841 }, 842 } 843 844 rr := relation.NewRelationsResolver(r) 845 _, err = rr.NextOp(localState, remoteState, &mockOperations{}) 846 c.Assert(err, jc.ErrorIsNil) 847 848 // Check that we've made the destroy unit call. 849 assertNumCalls(c, &numCalls, callsAfterDestroy) 850 } 851 852 func (s *relationsSuite) TestSubSubOtherRelationDyingNotDestroyed(c *gc.C) { 853 var numCalls int32 854 apiCalls := subSubRelationAPICalls() 855 // Sanity check: there shouldn't be a destroy at the end. 856 c.Assert(apiCalls[len(apiCalls)-1].request, gc.Not(gc.Equals), "Destroy") 857 858 expectedCalls := int32(len(apiCalls)) 859 apiCaller := mockAPICaller(c, &numCalls, apiCalls...) 860 861 st := uniter.NewState(apiCaller, nrpeUnitTag) 862 r, err := relation.NewRelations( 863 relation.RelationsConfig{ 864 State: st, 865 UnitTag: nrpeUnitTag, 866 CharmDir: s.stateDir, 867 RelationsDir: s.relationsDir, 868 NewLeadershipContext: s.leadershipContextFunc, 869 Abort: make(chan struct{}), 870 }) 871 c.Assert(err, jc.ErrorIsNil) 872 assertNumCalls(c, &numCalls, expectedCalls) 873 874 // So now we have a relations object with two relations, one to 875 // wordpress and one to ntp. We want to ensure that if the 876 // relation to ntp changes to Dying, the unit isn't destroyed, 877 // since it's kept alive by the principal relation. 878 localState := resolver.LocalState{ 879 State: operation.State{ 880 Kind: operation.Continue, 881 }, 882 } 883 884 remoteState := remotestate.Snapshot{ 885 Relations: map[int]remotestate.RelationSnapshot{ 886 1: { 887 Life: params.Alive, 888 Members: map[string]int64{ 889 "wordpress/0": 1, 890 }, 891 }, 892 2: { 893 Life: params.Dying, 894 Members: map[string]int64{ 895 "ntp/0": 1, 896 }, 897 }, 898 }, 899 } 900 901 rr := relation.NewRelationsResolver(r) 902 _, err = rr.NextOp(localState, remoteState, &mockOperations{}) 903 c.Assert(err, jc.ErrorIsNil) 904 905 // Check that we didn't try to make a destroy call (the apiCaller 906 // should panic in that case anyway). 907 assertNumCalls(c, &numCalls, expectedCalls) 908 }