github.com/emate/nomad@v0.8.2-wo-binpacking/nomad/deploymentwatcher/deployments_watcher_test.go (about) 1 package deploymentwatcher 2 3 import ( 4 "fmt" 5 "testing" 6 "time" 7 8 memdb "github.com/hashicorp/go-memdb" 9 "github.com/hashicorp/nomad/helper" 10 "github.com/hashicorp/nomad/helper/uuid" 11 "github.com/hashicorp/nomad/nomad/mock" 12 "github.com/hashicorp/nomad/nomad/structs" 13 "github.com/hashicorp/nomad/testutil" 14 "github.com/stretchr/testify/assert" 15 mocker "github.com/stretchr/testify/mock" 16 ) 17 18 func testDeploymentWatcher(t *testing.T, qps float64, batchDur time.Duration) (*Watcher, *mockBackend) { 19 m := newMockBackend(t) 20 w := NewDeploymentsWatcher(testLogger(), m, qps, batchDur) 21 return w, m 22 } 23 24 func defaultTestDeploymentWatcher(t *testing.T) (*Watcher, *mockBackend) { 25 return testDeploymentWatcher(t, LimitStateQueriesPerSecond, CrossDeploymentEvalBatchDuration) 26 } 27 28 // Tests that the watcher properly watches for deployments and reconciles them 29 func TestWatcher_WatchDeployments(t *testing.T) { 30 t.Parallel() 31 assert := assert.New(t) 32 w, m := defaultTestDeploymentWatcher(t) 33 34 // Create three jobs 35 j1, j2, j3 := mock.Job(), mock.Job(), mock.Job() 36 assert.Nil(m.state.UpsertJob(100, j1)) 37 assert.Nil(m.state.UpsertJob(101, j2)) 38 assert.Nil(m.state.UpsertJob(102, j3)) 39 40 // Create three deployments all running 41 d1, d2, d3 := mock.Deployment(), mock.Deployment(), mock.Deployment() 42 d1.JobID = j1.ID 43 d2.JobID = j2.ID 44 d3.JobID = j3.ID 45 46 // Upsert the first deployment 47 assert.Nil(m.state.UpsertDeployment(103, d1)) 48 49 // Next list 3 50 block1 := make(chan time.Time) 51 go func() { 52 <-block1 53 assert.Nil(m.state.UpsertDeployment(104, d2)) 54 assert.Nil(m.state.UpsertDeployment(105, d3)) 55 }() 56 57 //// Next list 3 but have one be terminal 58 block2 := make(chan time.Time) 59 d3terminal := d3.Copy() 60 d3terminal.Status = structs.DeploymentStatusFailed 61 go func() { 62 <-block2 63 assert.Nil(m.state.UpsertDeployment(106, d3terminal)) 64 }() 65 66 w.SetEnabled(true, m.state) 67 testutil.WaitForResult(func() (bool, error) { return 1 == len(w.watchers), nil }, 68 func(err error) { assert.Equal(1, len(w.watchers), "1 deployment returned") }) 69 70 close(block1) 71 testutil.WaitForResult(func() (bool, error) { return 3 == len(w.watchers), nil }, 72 func(err error) { assert.Equal(3, len(w.watchers), "3 deployment returned") }) 73 74 close(block2) 75 testutil.WaitForResult(func() (bool, error) { return 2 == len(w.watchers), nil }, 76 func(err error) { assert.Equal(3, len(w.watchers), "3 deployment returned - 1 terminal") }) 77 } 78 79 // Tests that calls against an unknown deployment fail 80 func TestWatcher_UnknownDeployment(t *testing.T) { 81 t.Parallel() 82 assert := assert.New(t) 83 w, m := defaultTestDeploymentWatcher(t) 84 w.SetEnabled(true, m.state) 85 86 // The expected error is that it should be an unknown deployment 87 dID := uuid.Generate() 88 expected := fmt.Sprintf("unknown deployment %q", dID) 89 90 // Request setting the health against an unknown deployment 91 req := &structs.DeploymentAllocHealthRequest{ 92 DeploymentID: dID, 93 HealthyAllocationIDs: []string{uuid.Generate()}, 94 } 95 var resp structs.DeploymentUpdateResponse 96 err := w.SetAllocHealth(req, &resp) 97 if assert.NotNil(err, "should have error for unknown deployment") { 98 assert.Contains(err.Error(), expected) 99 } 100 101 // Request promoting against an unknown deployment 102 req2 := &structs.DeploymentPromoteRequest{ 103 DeploymentID: dID, 104 All: true, 105 } 106 err = w.PromoteDeployment(req2, &resp) 107 if assert.NotNil(err, "should have error for unknown deployment") { 108 assert.Contains(err.Error(), expected) 109 } 110 111 // Request pausing against an unknown deployment 112 req3 := &structs.DeploymentPauseRequest{ 113 DeploymentID: dID, 114 Pause: true, 115 } 116 err = w.PauseDeployment(req3, &resp) 117 if assert.NotNil(err, "should have error for unknown deployment") { 118 assert.Contains(err.Error(), expected) 119 } 120 121 // Request failing against an unknown deployment 122 req4 := &structs.DeploymentFailRequest{ 123 DeploymentID: dID, 124 } 125 err = w.FailDeployment(req4, &resp) 126 if assert.NotNil(err, "should have error for unknown deployment") { 127 assert.Contains(err.Error(), expected) 128 } 129 } 130 131 // Test setting an unknown allocation's health 132 func TestWatcher_SetAllocHealth_Unknown(t *testing.T) { 133 t.Parallel() 134 assert := assert.New(t) 135 w, m := defaultTestDeploymentWatcher(t) 136 137 // Create a job, and a deployment 138 j := mock.Job() 139 d := mock.Deployment() 140 d.JobID = j.ID 141 assert.Nil(m.state.UpsertJob(m.nextIndex(), j), "UpsertJob") 142 assert.Nil(m.state.UpsertDeployment(m.nextIndex(), d), "UpsertDeployment") 143 144 w.SetEnabled(true, m.state) 145 testutil.WaitForResult(func() (bool, error) { return 1 == len(w.watchers), nil }, 146 func(err error) { assert.Equal(1, len(w.watchers), "Should have 1 deployment") }) 147 148 // Assert that we get a call to UpsertDeploymentAllocHealth 149 a := mock.Alloc() 150 matchConfig := &matchDeploymentAllocHealthRequestConfig{ 151 DeploymentID: d.ID, 152 Healthy: []string{a.ID}, 153 Eval: true, 154 } 155 matcher := matchDeploymentAllocHealthRequest(matchConfig) 156 m.On("UpdateDeploymentAllocHealth", mocker.MatchedBy(matcher)).Return(nil) 157 158 // Call SetAllocHealth 159 req := &structs.DeploymentAllocHealthRequest{ 160 DeploymentID: d.ID, 161 HealthyAllocationIDs: []string{a.ID}, 162 } 163 var resp structs.DeploymentUpdateResponse 164 err := w.SetAllocHealth(req, &resp) 165 if assert.NotNil(err, "Set health of unknown allocation") { 166 assert.Contains(err.Error(), "unknown") 167 } 168 assert.Equal(1, len(w.watchers), "Deployment should still be active") 169 } 170 171 // Test setting allocation health 172 func TestWatcher_SetAllocHealth_Healthy(t *testing.T) { 173 t.Parallel() 174 assert := assert.New(t) 175 w, m := defaultTestDeploymentWatcher(t) 176 177 // Create a job, alloc, and a deployment 178 j := mock.Job() 179 d := mock.Deployment() 180 d.JobID = j.ID 181 a := mock.Alloc() 182 a.DeploymentID = d.ID 183 assert.Nil(m.state.UpsertJob(m.nextIndex(), j), "UpsertJob") 184 assert.Nil(m.state.UpsertDeployment(m.nextIndex(), d), "UpsertDeployment") 185 assert.Nil(m.state.UpsertAllocs(m.nextIndex(), []*structs.Allocation{a}), "UpsertAllocs") 186 187 w.SetEnabled(true, m.state) 188 testutil.WaitForResult(func() (bool, error) { return 1 == len(w.watchers), nil }, 189 func(err error) { assert.Equal(1, len(w.watchers), "Should have 1 deployment") }) 190 191 // Assert that we get a call to UpsertDeploymentAllocHealth 192 matchConfig := &matchDeploymentAllocHealthRequestConfig{ 193 DeploymentID: d.ID, 194 Healthy: []string{a.ID}, 195 Eval: true, 196 } 197 matcher := matchDeploymentAllocHealthRequest(matchConfig) 198 m.On("UpdateDeploymentAllocHealth", mocker.MatchedBy(matcher)).Return(nil) 199 200 // Call SetAllocHealth 201 req := &structs.DeploymentAllocHealthRequest{ 202 DeploymentID: d.ID, 203 HealthyAllocationIDs: []string{a.ID}, 204 } 205 var resp structs.DeploymentUpdateResponse 206 err := w.SetAllocHealth(req, &resp) 207 assert.Nil(err, "SetAllocHealth") 208 assert.Equal(1, len(w.watchers), "Deployment should still be active") 209 m.AssertCalled(t, "UpdateDeploymentAllocHealth", mocker.MatchedBy(matcher)) 210 } 211 212 // Test setting allocation unhealthy 213 func TestWatcher_SetAllocHealth_Unhealthy(t *testing.T) { 214 t.Parallel() 215 assert := assert.New(t) 216 w, m := defaultTestDeploymentWatcher(t) 217 218 // Create a job, alloc, and a deployment 219 j := mock.Job() 220 d := mock.Deployment() 221 d.JobID = j.ID 222 a := mock.Alloc() 223 a.DeploymentID = d.ID 224 assert.Nil(m.state.UpsertJob(m.nextIndex(), j), "UpsertJob") 225 assert.Nil(m.state.UpsertDeployment(m.nextIndex(), d), "UpsertDeployment") 226 assert.Nil(m.state.UpsertAllocs(m.nextIndex(), []*structs.Allocation{a}), "UpsertAllocs") 227 228 w.SetEnabled(true, m.state) 229 testutil.WaitForResult(func() (bool, error) { return 1 == len(w.watchers), nil }, 230 func(err error) { assert.Equal(1, len(w.watchers), "Should have 1 deployment") }) 231 232 // Assert that we get a call to UpsertDeploymentAllocHealth 233 matchConfig := &matchDeploymentAllocHealthRequestConfig{ 234 DeploymentID: d.ID, 235 Unhealthy: []string{a.ID}, 236 Eval: true, 237 DeploymentUpdate: &structs.DeploymentStatusUpdate{ 238 DeploymentID: d.ID, 239 Status: structs.DeploymentStatusFailed, 240 StatusDescription: structs.DeploymentStatusDescriptionFailedAllocations, 241 }, 242 } 243 matcher := matchDeploymentAllocHealthRequest(matchConfig) 244 m.On("UpdateDeploymentAllocHealth", mocker.MatchedBy(matcher)).Return(nil) 245 246 // Call SetAllocHealth 247 req := &structs.DeploymentAllocHealthRequest{ 248 DeploymentID: d.ID, 249 UnhealthyAllocationIDs: []string{a.ID}, 250 } 251 var resp structs.DeploymentUpdateResponse 252 err := w.SetAllocHealth(req, &resp) 253 assert.Nil(err, "SetAllocHealth") 254 255 testutil.WaitForResult(func() (bool, error) { return 0 == len(w.watchers), nil }, 256 func(err error) { assert.Equal(0, len(w.watchers), "Should have no deployment") }) 257 m.AssertNumberOfCalls(t, "UpdateDeploymentAllocHealth", 1) 258 } 259 260 // Test setting allocation unhealthy and that there should be a rollback 261 func TestWatcher_SetAllocHealth_Unhealthy_Rollback(t *testing.T) { 262 t.Parallel() 263 assert := assert.New(t) 264 w, m := defaultTestDeploymentWatcher(t) 265 266 // Create a job, alloc, and a deployment 267 j := mock.Job() 268 j.TaskGroups[0].Update = structs.DefaultUpdateStrategy.Copy() 269 j.TaskGroups[0].Update.MaxParallel = 2 270 j.TaskGroups[0].Update.AutoRevert = true 271 j.Stable = true 272 d := mock.Deployment() 273 d.JobID = j.ID 274 d.TaskGroups["web"].AutoRevert = true 275 a := mock.Alloc() 276 a.DeploymentID = d.ID 277 assert.Nil(m.state.UpsertJob(m.nextIndex(), j), "UpsertJob") 278 assert.Nil(m.state.UpsertDeployment(m.nextIndex(), d), "UpsertDeployment") 279 assert.Nil(m.state.UpsertAllocs(m.nextIndex(), []*structs.Allocation{a}), "UpsertAllocs") 280 281 // Upsert the job again to get a new version 282 j2 := j.Copy() 283 j2.Stable = false 284 // Modify the job to make its specification different 285 j2.Meta["foo"] = "bar" 286 287 assert.Nil(m.state.UpsertJob(m.nextIndex(), j2), "UpsertJob2") 288 289 w.SetEnabled(true, m.state) 290 testutil.WaitForResult(func() (bool, error) { return 1 == len(w.watchers), nil }, 291 func(err error) { assert.Equal(1, len(w.watchers), "Should have 1 deployment") }) 292 293 // Assert that we get a call to UpsertDeploymentAllocHealth 294 matchConfig := &matchDeploymentAllocHealthRequestConfig{ 295 DeploymentID: d.ID, 296 Unhealthy: []string{a.ID}, 297 Eval: true, 298 DeploymentUpdate: &structs.DeploymentStatusUpdate{ 299 DeploymentID: d.ID, 300 Status: structs.DeploymentStatusFailed, 301 StatusDescription: structs.DeploymentStatusDescriptionFailedAllocations, 302 }, 303 JobVersion: helper.Uint64ToPtr(0), 304 } 305 matcher := matchDeploymentAllocHealthRequest(matchConfig) 306 m.On("UpdateDeploymentAllocHealth", mocker.MatchedBy(matcher)).Return(nil) 307 308 // Call SetAllocHealth 309 req := &structs.DeploymentAllocHealthRequest{ 310 DeploymentID: d.ID, 311 UnhealthyAllocationIDs: []string{a.ID}, 312 } 313 var resp structs.DeploymentUpdateResponse 314 err := w.SetAllocHealth(req, &resp) 315 assert.Nil(err, "SetAllocHealth") 316 317 testutil.WaitForResult(func() (bool, error) { return 0 == len(w.watchers), nil }, 318 func(err error) { assert.Equal(0, len(w.watchers), "Should have no deployment") }) 319 m.AssertNumberOfCalls(t, "UpdateDeploymentAllocHealth", 1) 320 } 321 322 // Test setting allocation unhealthy on job with identical spec and there should be no rollback 323 func TestWatcher_SetAllocHealth_Unhealthy_NoRollback(t *testing.T) { 324 t.Parallel() 325 assert := assert.New(t) 326 w, m := defaultTestDeploymentWatcher(t) 327 328 // Create a job, alloc, and a deployment 329 j := mock.Job() 330 j.TaskGroups[0].Update = structs.DefaultUpdateStrategy.Copy() 331 j.TaskGroups[0].Update.MaxParallel = 2 332 j.TaskGroups[0].Update.AutoRevert = true 333 j.Stable = true 334 d := mock.Deployment() 335 d.JobID = j.ID 336 d.TaskGroups["web"].AutoRevert = true 337 a := mock.Alloc() 338 a.DeploymentID = d.ID 339 assert.Nil(m.state.UpsertJob(m.nextIndex(), j), "UpsertJob") 340 assert.Nil(m.state.UpsertDeployment(m.nextIndex(), d), "UpsertDeployment") 341 assert.Nil(m.state.UpsertAllocs(m.nextIndex(), []*structs.Allocation{a}), "UpsertAllocs") 342 343 // Upsert the job again to get a new version 344 j2 := j.Copy() 345 j2.Stable = false 346 347 assert.Nil(m.state.UpsertJob(m.nextIndex(), j2), "UpsertJob2") 348 349 w.SetEnabled(true, m.state) 350 testutil.WaitForResult(func() (bool, error) { return 1 == len(w.watchers), nil }, 351 func(err error) { assert.Equal(1, len(w.watchers), "Should have 1 deployment") }) 352 353 // Assert that we get a call to UpsertDeploymentAllocHealth 354 matchConfig := &matchDeploymentAllocHealthRequestConfig{ 355 DeploymentID: d.ID, 356 Unhealthy: []string{a.ID}, 357 Eval: true, 358 DeploymentUpdate: &structs.DeploymentStatusUpdate{ 359 DeploymentID: d.ID, 360 Status: structs.DeploymentStatusFailed, 361 StatusDescription: structs.DeploymentStatusDescriptionFailedAllocations, 362 }, 363 JobVersion: nil, 364 } 365 matcher := matchDeploymentAllocHealthRequest(matchConfig) 366 m.On("UpdateDeploymentAllocHealth", mocker.MatchedBy(matcher)).Return(nil) 367 368 // Call SetAllocHealth 369 req := &structs.DeploymentAllocHealthRequest{ 370 DeploymentID: d.ID, 371 UnhealthyAllocationIDs: []string{a.ID}, 372 } 373 var resp structs.DeploymentUpdateResponse 374 err := w.SetAllocHealth(req, &resp) 375 assert.Nil(err, "SetAllocHealth") 376 377 testutil.WaitForResult(func() (bool, error) { return 0 == len(w.watchers), nil }, 378 func(err error) { assert.Equal(0, len(w.watchers), "Should have no deployment") }) 379 m.AssertNumberOfCalls(t, "UpdateDeploymentAllocHealth", 1) 380 } 381 382 // Test promoting a deployment 383 func TestWatcher_PromoteDeployment_HealthyCanaries(t *testing.T) { 384 t.Parallel() 385 assert := assert.New(t) 386 w, m := defaultTestDeploymentWatcher(t) 387 388 // Create a job, canary alloc, and a deployment 389 j := mock.Job() 390 j.TaskGroups[0].Update = structs.DefaultUpdateStrategy.Copy() 391 j.TaskGroups[0].Update.MaxParallel = 2 392 j.TaskGroups[0].Update.Canary = 2 393 d := mock.Deployment() 394 d.JobID = j.ID 395 a := mock.Alloc() 396 d.TaskGroups[a.TaskGroup].PlacedCanaries = []string{a.ID} 397 a.DeploymentStatus = &structs.AllocDeploymentStatus{ 398 Healthy: helper.BoolToPtr(true), 399 } 400 a.DeploymentID = d.ID 401 assert.Nil(m.state.UpsertJob(m.nextIndex(), j), "UpsertJob") 402 assert.Nil(m.state.UpsertDeployment(m.nextIndex(), d), "UpsertDeployment") 403 assert.Nil(m.state.UpsertAllocs(m.nextIndex(), []*structs.Allocation{a}), "UpsertAllocs") 404 405 w.SetEnabled(true, m.state) 406 testutil.WaitForResult(func() (bool, error) { return 1 == len(w.watchers), nil }, 407 func(err error) { assert.Equal(1, len(w.watchers), "Should have 1 deployment") }) 408 409 // Assert that we get a call to UpsertDeploymentPromotion 410 matchConfig := &matchDeploymentPromoteRequestConfig{ 411 Promotion: &structs.DeploymentPromoteRequest{ 412 DeploymentID: d.ID, 413 All: true, 414 }, 415 Eval: true, 416 } 417 matcher := matchDeploymentPromoteRequest(matchConfig) 418 m.On("UpdateDeploymentPromotion", mocker.MatchedBy(matcher)).Return(nil) 419 420 // Call PromoteDeployment 421 req := &structs.DeploymentPromoteRequest{ 422 DeploymentID: d.ID, 423 All: true, 424 } 425 var resp structs.DeploymentUpdateResponse 426 err := w.PromoteDeployment(req, &resp) 427 assert.Nil(err, "PromoteDeployment") 428 assert.Equal(1, len(w.watchers), "Deployment should still be active") 429 m.AssertCalled(t, "UpdateDeploymentPromotion", mocker.MatchedBy(matcher)) 430 } 431 432 // Test promoting a deployment with unhealthy canaries 433 func TestWatcher_PromoteDeployment_UnhealthyCanaries(t *testing.T) { 434 t.Parallel() 435 assert := assert.New(t) 436 w, m := defaultTestDeploymentWatcher(t) 437 438 // Create a job, canary alloc, and a deployment 439 j := mock.Job() 440 j.TaskGroups[0].Update = structs.DefaultUpdateStrategy.Copy() 441 j.TaskGroups[0].Update.MaxParallel = 2 442 j.TaskGroups[0].Update.Canary = 2 443 d := mock.Deployment() 444 d.JobID = j.ID 445 a := mock.Alloc() 446 d.TaskGroups[a.TaskGroup].PlacedCanaries = []string{a.ID} 447 a.DeploymentID = d.ID 448 assert.Nil(m.state.UpsertJob(m.nextIndex(), j), "UpsertJob") 449 assert.Nil(m.state.UpsertDeployment(m.nextIndex(), d), "UpsertDeployment") 450 assert.Nil(m.state.UpsertAllocs(m.nextIndex(), []*structs.Allocation{a}), "UpsertAllocs") 451 452 w.SetEnabled(true, m.state) 453 testutil.WaitForResult(func() (bool, error) { return 1 == len(w.watchers), nil }, 454 func(err error) { assert.Equal(1, len(w.watchers), "Should have 1 deployment") }) 455 456 // Assert that we get a call to UpsertDeploymentPromotion 457 matchConfig := &matchDeploymentPromoteRequestConfig{ 458 Promotion: &structs.DeploymentPromoteRequest{ 459 DeploymentID: d.ID, 460 All: true, 461 }, 462 Eval: true, 463 } 464 matcher := matchDeploymentPromoteRequest(matchConfig) 465 m.On("UpdateDeploymentPromotion", mocker.MatchedBy(matcher)).Return(nil) 466 467 // Call SetAllocHealth 468 req := &structs.DeploymentPromoteRequest{ 469 DeploymentID: d.ID, 470 All: true, 471 } 472 var resp structs.DeploymentUpdateResponse 473 err := w.PromoteDeployment(req, &resp) 474 if assert.NotNil(err, "PromoteDeployment") { 475 assert.Contains(err.Error(), "is not healthy", "Should error because canary isn't marked healthy") 476 } 477 478 assert.Equal(1, len(w.watchers), "Deployment should still be active") 479 m.AssertCalled(t, "UpdateDeploymentPromotion", mocker.MatchedBy(matcher)) 480 } 481 482 // Test pausing a deployment that is running 483 func TestWatcher_PauseDeployment_Pause_Running(t *testing.T) { 484 t.Parallel() 485 assert := assert.New(t) 486 w, m := defaultTestDeploymentWatcher(t) 487 488 // Create a job and a deployment 489 j := mock.Job() 490 d := mock.Deployment() 491 d.JobID = j.ID 492 assert.Nil(m.state.UpsertJob(m.nextIndex(), j), "UpsertJob") 493 assert.Nil(m.state.UpsertDeployment(m.nextIndex(), d), "UpsertDeployment") 494 495 w.SetEnabled(true, m.state) 496 testutil.WaitForResult(func() (bool, error) { return 1 == len(w.watchers), nil }, 497 func(err error) { assert.Equal(1, len(w.watchers), "Should have 1 deployment") }) 498 499 // Assert that we get a call to UpsertDeploymentStatusUpdate 500 matchConfig := &matchDeploymentStatusUpdateConfig{ 501 DeploymentID: d.ID, 502 Status: structs.DeploymentStatusPaused, 503 StatusDescription: structs.DeploymentStatusDescriptionPaused, 504 } 505 matcher := matchDeploymentStatusUpdateRequest(matchConfig) 506 m.On("UpdateDeploymentStatus", mocker.MatchedBy(matcher)).Return(nil) 507 508 // Call PauseDeployment 509 req := &structs.DeploymentPauseRequest{ 510 DeploymentID: d.ID, 511 Pause: true, 512 } 513 var resp structs.DeploymentUpdateResponse 514 err := w.PauseDeployment(req, &resp) 515 assert.Nil(err, "PauseDeployment") 516 517 assert.Equal(1, len(w.watchers), "Deployment should still be active") 518 m.AssertCalled(t, "UpdateDeploymentStatus", mocker.MatchedBy(matcher)) 519 } 520 521 // Test pausing a deployment that is paused 522 func TestWatcher_PauseDeployment_Pause_Paused(t *testing.T) { 523 t.Parallel() 524 assert := assert.New(t) 525 w, m := defaultTestDeploymentWatcher(t) 526 527 // Create a job and a deployment 528 j := mock.Job() 529 d := mock.Deployment() 530 d.JobID = j.ID 531 d.Status = structs.DeploymentStatusPaused 532 assert.Nil(m.state.UpsertJob(m.nextIndex(), j), "UpsertJob") 533 assert.Nil(m.state.UpsertDeployment(m.nextIndex(), d), "UpsertDeployment") 534 535 w.SetEnabled(true, m.state) 536 testutil.WaitForResult(func() (bool, error) { return 1 == len(w.watchers), nil }, 537 func(err error) { assert.Equal(1, len(w.watchers), "Should have 1 deployment") }) 538 539 // Assert that we get a call to UpsertDeploymentStatusUpdate 540 matchConfig := &matchDeploymentStatusUpdateConfig{ 541 DeploymentID: d.ID, 542 Status: structs.DeploymentStatusPaused, 543 StatusDescription: structs.DeploymentStatusDescriptionPaused, 544 } 545 matcher := matchDeploymentStatusUpdateRequest(matchConfig) 546 m.On("UpdateDeploymentStatus", mocker.MatchedBy(matcher)).Return(nil) 547 548 // Call PauseDeployment 549 req := &structs.DeploymentPauseRequest{ 550 DeploymentID: d.ID, 551 Pause: true, 552 } 553 var resp structs.DeploymentUpdateResponse 554 err := w.PauseDeployment(req, &resp) 555 assert.Nil(err, "PauseDeployment") 556 557 assert.Equal(1, len(w.watchers), "Deployment should still be active") 558 m.AssertCalled(t, "UpdateDeploymentStatus", mocker.MatchedBy(matcher)) 559 } 560 561 // Test unpausing a deployment that is paused 562 func TestWatcher_PauseDeployment_Unpause_Paused(t *testing.T) { 563 t.Parallel() 564 assert := assert.New(t) 565 w, m := defaultTestDeploymentWatcher(t) 566 567 // Create a job and a deployment 568 j := mock.Job() 569 d := mock.Deployment() 570 d.JobID = j.ID 571 d.Status = structs.DeploymentStatusPaused 572 assert.Nil(m.state.UpsertJob(m.nextIndex(), j), "UpsertJob") 573 assert.Nil(m.state.UpsertDeployment(m.nextIndex(), d), "UpsertDeployment") 574 575 w.SetEnabled(true, m.state) 576 testutil.WaitForResult(func() (bool, error) { return 1 == len(w.watchers), nil }, 577 func(err error) { assert.Equal(1, len(w.watchers), "Should have 1 deployment") }) 578 579 // Assert that we get a call to UpsertDeploymentStatusUpdate 580 matchConfig := &matchDeploymentStatusUpdateConfig{ 581 DeploymentID: d.ID, 582 Status: structs.DeploymentStatusRunning, 583 StatusDescription: structs.DeploymentStatusDescriptionRunning, 584 Eval: true, 585 } 586 matcher := matchDeploymentStatusUpdateRequest(matchConfig) 587 m.On("UpdateDeploymentStatus", mocker.MatchedBy(matcher)).Return(nil) 588 589 // Call PauseDeployment 590 req := &structs.DeploymentPauseRequest{ 591 DeploymentID: d.ID, 592 Pause: false, 593 } 594 var resp structs.DeploymentUpdateResponse 595 err := w.PauseDeployment(req, &resp) 596 assert.Nil(err, "PauseDeployment") 597 598 assert.Equal(1, len(w.watchers), "Deployment should still be active") 599 m.AssertCalled(t, "UpdateDeploymentStatus", mocker.MatchedBy(matcher)) 600 } 601 602 // Test unpausing a deployment that is running 603 func TestWatcher_PauseDeployment_Unpause_Running(t *testing.T) { 604 t.Parallel() 605 assert := assert.New(t) 606 w, m := defaultTestDeploymentWatcher(t) 607 608 // Create a job and a deployment 609 j := mock.Job() 610 d := mock.Deployment() 611 d.JobID = j.ID 612 assert.Nil(m.state.UpsertJob(m.nextIndex(), j), "UpsertJob") 613 assert.Nil(m.state.UpsertDeployment(m.nextIndex(), d), "UpsertDeployment") 614 615 w.SetEnabled(true, m.state) 616 testutil.WaitForResult(func() (bool, error) { return 1 == len(w.watchers), nil }, 617 func(err error) { assert.Equal(1, len(w.watchers), "Should have 1 deployment") }) 618 619 // Assert that we get a call to UpsertDeploymentStatusUpdate 620 matchConfig := &matchDeploymentStatusUpdateConfig{ 621 DeploymentID: d.ID, 622 Status: structs.DeploymentStatusRunning, 623 StatusDescription: structs.DeploymentStatusDescriptionRunning, 624 Eval: true, 625 } 626 matcher := matchDeploymentStatusUpdateRequest(matchConfig) 627 m.On("UpdateDeploymentStatus", mocker.MatchedBy(matcher)).Return(nil) 628 629 // Call PauseDeployment 630 req := &structs.DeploymentPauseRequest{ 631 DeploymentID: d.ID, 632 Pause: false, 633 } 634 var resp structs.DeploymentUpdateResponse 635 err := w.PauseDeployment(req, &resp) 636 assert.Nil(err, "PauseDeployment") 637 638 assert.Equal(1, len(w.watchers), "Deployment should still be active") 639 m.AssertCalled(t, "UpdateDeploymentStatus", mocker.MatchedBy(matcher)) 640 } 641 642 // Test failing a deployment that is running 643 func TestWatcher_FailDeployment_Running(t *testing.T) { 644 t.Parallel() 645 assert := assert.New(t) 646 w, m := defaultTestDeploymentWatcher(t) 647 648 // Create a job and a deployment 649 j := mock.Job() 650 d := mock.Deployment() 651 d.JobID = j.ID 652 assert.Nil(m.state.UpsertJob(m.nextIndex(), j), "UpsertJob") 653 assert.Nil(m.state.UpsertDeployment(m.nextIndex(), d), "UpsertDeployment") 654 655 w.SetEnabled(true, m.state) 656 testutil.WaitForResult(func() (bool, error) { return 1 == len(w.watchers), nil }, 657 func(err error) { assert.Equal(1, len(w.watchers), "Should have 1 deployment") }) 658 659 // Assert that we get a call to UpsertDeploymentStatusUpdate 660 matchConfig := &matchDeploymentStatusUpdateConfig{ 661 DeploymentID: d.ID, 662 Status: structs.DeploymentStatusFailed, 663 StatusDescription: structs.DeploymentStatusDescriptionFailedByUser, 664 Eval: true, 665 } 666 matcher := matchDeploymentStatusUpdateRequest(matchConfig) 667 m.On("UpdateDeploymentStatus", mocker.MatchedBy(matcher)).Return(nil) 668 669 // Call PauseDeployment 670 req := &structs.DeploymentFailRequest{ 671 DeploymentID: d.ID, 672 } 673 var resp structs.DeploymentUpdateResponse 674 err := w.FailDeployment(req, &resp) 675 assert.Nil(err, "FailDeployment") 676 677 assert.Equal(1, len(w.watchers), "Deployment should still be active") 678 m.AssertCalled(t, "UpdateDeploymentStatus", mocker.MatchedBy(matcher)) 679 } 680 681 // Tests that the watcher properly watches for allocation changes and takes the 682 // proper actions 683 func TestDeploymentWatcher_Watch(t *testing.T) { 684 t.Parallel() 685 assert := assert.New(t) 686 w, m := testDeploymentWatcher(t, 1000.0, 1*time.Millisecond) 687 688 // Create a job, alloc, and a deployment 689 j := mock.Job() 690 j.TaskGroups[0].Update = structs.DefaultUpdateStrategy.Copy() 691 j.TaskGroups[0].Update.MaxParallel = 2 692 j.TaskGroups[0].Update.AutoRevert = true 693 j.Stable = true 694 d := mock.Deployment() 695 d.JobID = j.ID 696 d.TaskGroups["web"].AutoRevert = true 697 a := mock.Alloc() 698 a.DeploymentID = d.ID 699 assert.Nil(m.state.UpsertJob(m.nextIndex(), j), "UpsertJob") 700 assert.Nil(m.state.UpsertDeployment(m.nextIndex(), d), "UpsertDeployment") 701 assert.Nil(m.state.UpsertAllocs(m.nextIndex(), []*structs.Allocation{a}), "UpsertAllocs") 702 703 // Upsert the job again to get a new version 704 j2 := j.Copy() 705 // Modify the job to make its specification different 706 j2.Meta["foo"] = "bar" 707 j2.Stable = false 708 assert.Nil(m.state.UpsertJob(m.nextIndex(), j2), "UpsertJob2") 709 710 w.SetEnabled(true, m.state) 711 testutil.WaitForResult(func() (bool, error) { return 1 == len(w.watchers), nil }, 712 func(err error) { assert.Equal(1, len(w.watchers), "Should have 1 deployment") }) 713 714 // Assert that we will get a createEvaluation call only once. This will 715 // verify that the watcher is batching allocation changes 716 m1 := matchUpsertEvals([]string{d.ID}) 717 m.On("UpsertEvals", mocker.MatchedBy(m1)).Return(nil).Once() 718 719 // Update the allocs health to healthy which should create an evaluation 720 for i := 0; i < 5; i++ { 721 req := &structs.ApplyDeploymentAllocHealthRequest{ 722 DeploymentAllocHealthRequest: structs.DeploymentAllocHealthRequest{ 723 DeploymentID: d.ID, 724 HealthyAllocationIDs: []string{a.ID}, 725 }, 726 } 727 assert.Nil(m.state.UpdateDeploymentAllocHealth(m.nextIndex(), req), "UpsertDeploymentAllocHealth") 728 } 729 730 // Wait for there to be one eval 731 testutil.WaitForResult(func() (bool, error) { 732 ws := memdb.NewWatchSet() 733 evals, err := m.state.EvalsByJob(ws, j.Namespace, j.ID) 734 if err != nil { 735 return false, err 736 } 737 738 if l := len(evals); l != 1 { 739 return false, fmt.Errorf("Got %d evals; want 1", l) 740 } 741 742 return true, nil 743 }, func(err error) { 744 t.Fatal(err) 745 }) 746 747 // Assert that we get a call to UpsertDeploymentStatusUpdate 748 c := &matchDeploymentStatusUpdateConfig{ 749 DeploymentID: d.ID, 750 Status: structs.DeploymentStatusFailed, 751 StatusDescription: structs.DeploymentStatusDescriptionRollback(structs.DeploymentStatusDescriptionFailedAllocations, 0), 752 JobVersion: helper.Uint64ToPtr(0), 753 Eval: true, 754 } 755 m2 := matchDeploymentStatusUpdateRequest(c) 756 m.On("UpdateDeploymentStatus", mocker.MatchedBy(m2)).Return(nil) 757 758 // Update the allocs health to unhealthy which should create a job rollback, 759 // status update and eval 760 req2 := &structs.ApplyDeploymentAllocHealthRequest{ 761 DeploymentAllocHealthRequest: structs.DeploymentAllocHealthRequest{ 762 DeploymentID: d.ID, 763 UnhealthyAllocationIDs: []string{a.ID}, 764 }, 765 } 766 assert.Nil(m.state.UpdateDeploymentAllocHealth(m.nextIndex(), req2), "UpsertDeploymentAllocHealth") 767 768 // Wait for there to be one eval 769 testutil.WaitForResult(func() (bool, error) { 770 ws := memdb.NewWatchSet() 771 evals, err := m.state.EvalsByJob(ws, j.Namespace, j.ID) 772 if err != nil { 773 return false, err 774 } 775 776 if l := len(evals); l != 2 { 777 return false, fmt.Errorf("Got %d evals; want 1", l) 778 } 779 780 return true, nil 781 }, func(err error) { 782 t.Fatal(err) 783 }) 784 785 m.AssertCalled(t, "UpsertEvals", mocker.MatchedBy(m1)) 786 787 // After we upsert the job version will go to 2. So use this to assert the 788 // original call happened. 789 c2 := &matchDeploymentStatusUpdateConfig{ 790 DeploymentID: d.ID, 791 Status: structs.DeploymentStatusFailed, 792 StatusDescription: structs.DeploymentStatusDescriptionRollback(structs.DeploymentStatusDescriptionFailedAllocations, 0), 793 JobVersion: helper.Uint64ToPtr(2), 794 Eval: true, 795 } 796 m3 := matchDeploymentStatusUpdateRequest(c2) 797 m.AssertCalled(t, "UpdateDeploymentStatus", mocker.MatchedBy(m3)) 798 testutil.WaitForResult(func() (bool, error) { return 0 == len(w.watchers), nil }, 799 func(err error) { assert.Equal(0, len(w.watchers), "Should have no deployment") }) 800 } 801 802 // Tests that the watcher fails rollback when the spec hasn't changed 803 func TestDeploymentWatcher_RollbackFailed(t *testing.T) { 804 t.Parallel() 805 assert := assert.New(t) 806 w, m := testDeploymentWatcher(t, 1000.0, 1*time.Millisecond) 807 808 // Create a job, alloc, and a deployment 809 j := mock.Job() 810 j.TaskGroups[0].Update = structs.DefaultUpdateStrategy.Copy() 811 j.TaskGroups[0].Update.MaxParallel = 2 812 j.TaskGroups[0].Update.AutoRevert = true 813 j.Stable = true 814 d := mock.Deployment() 815 d.JobID = j.ID 816 d.TaskGroups["web"].AutoRevert = true 817 a := mock.Alloc() 818 a.DeploymentID = d.ID 819 assert.Nil(m.state.UpsertJob(m.nextIndex(), j), "UpsertJob") 820 assert.Nil(m.state.UpsertDeployment(m.nextIndex(), d), "UpsertDeployment") 821 assert.Nil(m.state.UpsertAllocs(m.nextIndex(), []*structs.Allocation{a}), "UpsertAllocs") 822 823 // Upsert the job again to get a new version 824 j2 := j.Copy() 825 // Modify the job to make its specification different 826 j2.Stable = false 827 assert.Nil(m.state.UpsertJob(m.nextIndex(), j2), "UpsertJob2") 828 829 w.SetEnabled(true, m.state) 830 testutil.WaitForResult(func() (bool, error) { return 1 == len(w.watchers), nil }, 831 func(err error) { assert.Equal(1, len(w.watchers), "Should have 1 deployment") }) 832 833 // Assert that we will get a createEvaluation call only once. This will 834 // verify that the watcher is batching allocation changes 835 m1 := matchUpsertEvals([]string{d.ID}) 836 m.On("UpsertEvals", mocker.MatchedBy(m1)).Return(nil).Once() 837 838 // Update the allocs health to healthy which should create an evaluation 839 for i := 0; i < 5; i++ { 840 req := &structs.ApplyDeploymentAllocHealthRequest{ 841 DeploymentAllocHealthRequest: structs.DeploymentAllocHealthRequest{ 842 DeploymentID: d.ID, 843 HealthyAllocationIDs: []string{a.ID}, 844 }, 845 } 846 assert.Nil(m.state.UpdateDeploymentAllocHealth(m.nextIndex(), req), "UpsertDeploymentAllocHealth") 847 } 848 849 // Wait for there to be one eval 850 testutil.WaitForResult(func() (bool, error) { 851 ws := memdb.NewWatchSet() 852 evals, err := m.state.EvalsByJob(ws, j.Namespace, j.ID) 853 if err != nil { 854 return false, err 855 } 856 857 if l := len(evals); l != 1 { 858 return false, fmt.Errorf("Got %d evals; want 1", l) 859 } 860 861 return true, nil 862 }, func(err error) { 863 t.Fatal(err) 864 }) 865 866 // Assert that we get a call to UpsertDeploymentStatusUpdate with roll back failed as the status 867 c := &matchDeploymentStatusUpdateConfig{ 868 DeploymentID: d.ID, 869 Status: structs.DeploymentStatusFailed, 870 StatusDescription: structs.DeploymentStatusDescriptionRollbackNoop(structs.DeploymentStatusDescriptionFailedAllocations, 0), 871 JobVersion: nil, 872 Eval: true, 873 } 874 m2 := matchDeploymentStatusUpdateRequest(c) 875 m.On("UpdateDeploymentStatus", mocker.MatchedBy(m2)).Return(nil) 876 877 // Update the allocs health to unhealthy which will cause attempting a rollback, 878 // fail in that step, do status update and eval 879 req2 := &structs.ApplyDeploymentAllocHealthRequest{ 880 DeploymentAllocHealthRequest: structs.DeploymentAllocHealthRequest{ 881 DeploymentID: d.ID, 882 UnhealthyAllocationIDs: []string{a.ID}, 883 }, 884 } 885 assert.Nil(m.state.UpdateDeploymentAllocHealth(m.nextIndex(), req2), "UpsertDeploymentAllocHealth") 886 887 // Wait for there to be one eval 888 testutil.WaitForResult(func() (bool, error) { 889 ws := memdb.NewWatchSet() 890 evals, err := m.state.EvalsByJob(ws, j.Namespace, j.ID) 891 if err != nil { 892 return false, err 893 } 894 895 if l := len(evals); l != 2 { 896 return false, fmt.Errorf("Got %d evals; want 1", l) 897 } 898 899 return true, nil 900 }, func(err error) { 901 t.Fatal(err) 902 }) 903 904 m.AssertCalled(t, "UpsertEvals", mocker.MatchedBy(m1)) 905 906 // verify that the job version hasn't changed after upsert 907 m.state.JobByID(nil, structs.DefaultNamespace, j.ID) 908 assert.Equal(uint64(0), j.Version, "Expected job version 0 but got ", j.Version) 909 } 910 911 // Test evaluations are batched between watchers 912 func TestWatcher_BatchEvals(t *testing.T) { 913 t.Parallel() 914 assert := assert.New(t) 915 w, m := testDeploymentWatcher(t, 1000.0, 1*time.Second) 916 917 // Create a job, alloc, for two deployments 918 j1 := mock.Job() 919 d1 := mock.Deployment() 920 d1.JobID = j1.ID 921 a1 := mock.Alloc() 922 a1.DeploymentID = d1.ID 923 924 j2 := mock.Job() 925 d2 := mock.Deployment() 926 d2.JobID = j2.ID 927 a2 := mock.Alloc() 928 a2.DeploymentID = d2.ID 929 930 assert.Nil(m.state.UpsertJob(m.nextIndex(), j1), "UpsertJob") 931 assert.Nil(m.state.UpsertJob(m.nextIndex(), j2), "UpsertJob") 932 assert.Nil(m.state.UpsertDeployment(m.nextIndex(), d1), "UpsertDeployment") 933 assert.Nil(m.state.UpsertDeployment(m.nextIndex(), d2), "UpsertDeployment") 934 assert.Nil(m.state.UpsertAllocs(m.nextIndex(), []*structs.Allocation{a1}), "UpsertAllocs") 935 assert.Nil(m.state.UpsertAllocs(m.nextIndex(), []*structs.Allocation{a2}), "UpsertAllocs") 936 937 w.SetEnabled(true, m.state) 938 testutil.WaitForResult(func() (bool, error) { return 2 == len(w.watchers), nil }, 939 func(err error) { assert.Equal(2, len(w.watchers), "Should have 2 deployment") }) 940 941 // Assert that we will get a createEvaluation call only once and it contains 942 // both deployments. This will verify that the watcher is batching 943 // allocation changes 944 m1 := matchUpsertEvals([]string{d1.ID, d2.ID}) 945 m.On("UpsertEvals", mocker.MatchedBy(m1)).Return(nil).Once() 946 947 // Update the allocs health to healthy which should create an evaluation 948 req := &structs.ApplyDeploymentAllocHealthRequest{ 949 DeploymentAllocHealthRequest: structs.DeploymentAllocHealthRequest{ 950 DeploymentID: d1.ID, 951 HealthyAllocationIDs: []string{a1.ID}, 952 }, 953 } 954 assert.Nil(m.state.UpdateDeploymentAllocHealth(m.nextIndex(), req), "UpsertDeploymentAllocHealth") 955 956 req2 := &structs.ApplyDeploymentAllocHealthRequest{ 957 DeploymentAllocHealthRequest: structs.DeploymentAllocHealthRequest{ 958 DeploymentID: d2.ID, 959 HealthyAllocationIDs: []string{a2.ID}, 960 }, 961 } 962 assert.Nil(m.state.UpdateDeploymentAllocHealth(m.nextIndex(), req2), "UpsertDeploymentAllocHealth") 963 964 // Wait for there to be one eval for each job 965 testutil.WaitForResult(func() (bool, error) { 966 ws := memdb.NewWatchSet() 967 evals1, err := m.state.EvalsByJob(ws, j1.Namespace, j1.ID) 968 if err != nil { 969 return false, err 970 } 971 972 evals2, err := m.state.EvalsByJob(ws, j2.Namespace, j2.ID) 973 if err != nil { 974 return false, err 975 } 976 977 if l := len(evals1); l != 1 { 978 return false, fmt.Errorf("Got %d evals; want 1", l) 979 } 980 981 if l := len(evals2); l != 1 { 982 return false, fmt.Errorf("Got %d evals; want 1", l) 983 } 984 985 return true, nil 986 }, func(err error) { 987 t.Fatal(err) 988 }) 989 990 m.AssertCalled(t, "UpsertEvals", mocker.MatchedBy(m1)) 991 testutil.WaitForResult(func() (bool, error) { return 2 == len(w.watchers), nil }, 992 func(err error) { assert.Equal(2, len(w.watchers), "Should have 2 deployment") }) 993 }