github.com/hernad/nomad@v1.6.112/ui/tests/integration/components/job-status-panel-test.js (about) 1 /** 2 * Copyright (c) HashiCorp, Inc. 3 * SPDX-License-Identifier: MPL-2.0 4 */ 5 6 import { module, test } from 'qunit'; 7 import { setupRenderingTest } from 'ember-qunit'; 8 import { find, render, settled } from '@ember/test-helpers'; 9 import hbs from 'htmlbars-inline-precompile'; 10 import { startMirage } from 'nomad-ui/initializers/ember-cli-mirage'; 11 import { initialize as fragmentSerializerInitializer } from 'nomad-ui/initializers/fragment-serializer'; 12 import { componentA11yAudit } from 'nomad-ui/tests/helpers/a11y-audit'; 13 import percySnapshot from '@percy/ember'; 14 15 module( 16 'Integration | Component | job status panel | active deployment', 17 function (hooks) { 18 setupRenderingTest(hooks); 19 20 hooks.beforeEach(function () { 21 fragmentSerializerInitializer(this.owner); 22 window.localStorage.clear(); 23 this.store = this.owner.lookup('service:store'); 24 this.server = startMirage(); 25 this.server.create('node-pool'); 26 this.server.create('namespace'); 27 }); 28 29 hooks.afterEach(function () { 30 this.server.shutdown(); 31 window.localStorage.clear(); 32 }); 33 34 test('there is no latest deployment section when the job has no deployments', async function (assert) { 35 this.server.create('job', { 36 type: 'service', 37 noDeployments: true, 38 createAllocations: false, 39 }); 40 41 await this.store.findAll('job'); 42 43 this.set('job', this.store.peekAll('job').get('firstObject')); 44 await render(hbs` 45 <JobStatus::Panel @job={{this.job}} />) 46 `); 47 48 assert.notOk(find('.active-deployment'), 'No active deployment'); 49 }); 50 51 test('the latest deployment section shows up for the currently running deployment: Ungrouped Allocations (small cluster)', async function (assert) { 52 assert.expect(24); 53 54 this.server.create('node'); 55 56 const NUMBER_OF_GROUPS = 2; 57 const ALLOCS_PER_GROUP = 10; 58 const allocStatusDistribution = { 59 running: 0.5, 60 failed: 0.2, 61 unknown: 0.1, 62 lost: 0, 63 complete: 0.1, 64 pending: 0.1, 65 }; 66 67 const job = await this.server.create('job', { 68 type: 'service', 69 createAllocations: true, 70 noDeployments: true, // manually created below 71 activeDeployment: true, 72 groupTaskCount: ALLOCS_PER_GROUP, 73 shallow: true, 74 resourceSpec: Array(NUMBER_OF_GROUPS).fill(['M: 257, C: 500']), // length of this array determines number of groups 75 allocStatusDistribution, 76 }); 77 78 const jobRecord = await this.store.find( 79 'job', 80 JSON.stringify([job.id, 'default']) 81 ); 82 await this.server.create('deployment', false, 'active', { 83 jobId: job.id, 84 groupDesiredTotal: ALLOCS_PER_GROUP, 85 versionNumber: 1, 86 status: 'failed', 87 }); 88 89 const OLD_ALLOCATIONS_TO_SHOW = 25; 90 const OLD_ALLOCATIONS_TO_COMPLETE = 5; 91 92 this.server.createList('allocation', OLD_ALLOCATIONS_TO_SHOW, { 93 jobId: job.id, 94 jobVersion: 0, 95 clientStatus: 'running', 96 }); 97 98 this.set('job', jobRecord); 99 await this.get('job.allocations'); 100 101 await render(hbs` 102 <JobStatus::Panel @job={{this.job}} /> 103 `); 104 105 // Initially no active deployment 106 assert.notOk( 107 find('.active-deployment'), 108 'Does not show an active deployment when latest is failed' 109 ); 110 111 const deployment = await this.get('job.latestDeployment'); 112 113 await this.set('job.latestDeployment.status', 'running'); 114 115 assert.ok( 116 find('.active-deployment'), 117 'Shows an active deployment if latest status is Running' 118 ); 119 120 // Half the shown allocations are running, 1 is pending, 1 is failed; none are canaries or healthy. 121 // The rest (lost, unknown, etc.) all show up as "Unplaced" 122 assert 123 .dom('.new-allocations .allocation-status-row .represented-allocation') 124 .exists( 125 { count: NUMBER_OF_GROUPS * ALLOCS_PER_GROUP }, 126 'All allocations are shown (ungrouped)' 127 ); 128 assert 129 .dom( 130 '.new-allocations .allocation-status-row .represented-allocation.running' 131 ) 132 .exists( 133 { 134 count: 135 NUMBER_OF_GROUPS * 136 ALLOCS_PER_GROUP * 137 allocStatusDistribution.running, 138 }, 139 'Correct number of running allocations are shown' 140 ); 141 assert 142 .dom( 143 '.new-allocations .allocation-status-row .represented-allocation.running.canary' 144 ) 145 .exists({ count: 0 }, 'No running canaries shown by default'); 146 assert 147 .dom( 148 '.new-allocations .allocation-status-row .represented-allocation.running.healthy' 149 ) 150 .exists({ count: 0 }, 'No running healthy shown by default'); 151 assert 152 .dom( 153 '.new-allocations .allocation-status-row .represented-allocation.failed' 154 ) 155 .exists( 156 { 157 count: 158 NUMBER_OF_GROUPS * 159 ALLOCS_PER_GROUP * 160 allocStatusDistribution.failed, 161 }, 162 'Correct number of failed allocations are shown' 163 ); 164 assert 165 .dom( 166 '.new-allocations .allocation-status-row .represented-allocation.failed.canary' 167 ) 168 .exists({ count: 0 }, 'No failed canaries shown by default'); 169 assert 170 .dom( 171 '.new-allocations .allocation-status-row .represented-allocation.pending' 172 ) 173 .exists( 174 { 175 count: 176 NUMBER_OF_GROUPS * 177 ALLOCS_PER_GROUP * 178 allocStatusDistribution.pending, 179 }, 180 'Correct number of pending allocations are shown' 181 ); 182 assert 183 .dom( 184 '.new-allocations .allocation-status-row .represented-allocation.pending.canary' 185 ) 186 .exists({ count: 0 }, 'No pending canaries shown by default'); 187 assert 188 .dom( 189 '.new-allocations .allocation-status-row .represented-allocation.unplaced' 190 ) 191 .exists( 192 { 193 count: 194 NUMBER_OF_GROUPS * 195 ALLOCS_PER_GROUP * 196 (allocStatusDistribution.lost + 197 allocStatusDistribution.unknown + 198 allocStatusDistribution.complete), 199 }, 200 'Correct number of unplaced allocations are shown' 201 ); 202 203 assert.equal( 204 find('[data-test-new-allocation-tally] > span').textContent.trim(), 205 `New allocations: ${ 206 this.job.allocations.filter( 207 (a) => 208 a.clientStatus === 'running' && 209 a.deploymentStatus?.Healthy === true 210 ).length 211 }/${deployment.get('desiredTotal')} running and healthy`, 212 'Summary text shows accurate numbers when 0 are running/healthy' 213 ); 214 215 let NUMBER_OF_RUNNING_CANARIES = 2; 216 let NUMBER_OF_RUNNING_HEALTHY = 5; 217 let NUMBER_OF_FAILED_CANARIES = 1; 218 let NUMBER_OF_PENDING_CANARIES = 1; 219 220 // Set some allocs to canary, and to healthy 221 this.get('job.allocations') 222 .filter((a) => a.clientStatus === 'running') 223 .slice(0, NUMBER_OF_RUNNING_CANARIES) 224 .forEach((alloc) => 225 alloc.set('deploymentStatus', { 226 Canary: true, 227 Healthy: alloc.deploymentStatus?.Healthy, 228 }) 229 ); 230 this.get('job.allocations') 231 .filter((a) => a.clientStatus === 'running') 232 .slice(0, NUMBER_OF_RUNNING_HEALTHY) 233 .forEach((alloc) => 234 alloc.set('deploymentStatus', { 235 Canary: alloc.deploymentStatus?.Canary, 236 Healthy: true, 237 }) 238 ); 239 this.get('job.allocations') 240 .filter((a) => a.clientStatus === 'failed') 241 .slice(0, NUMBER_OF_FAILED_CANARIES) 242 .forEach((alloc) => 243 alloc.set('deploymentStatus', { 244 Canary: true, 245 Healthy: alloc.deploymentStatus?.Healthy, 246 }) 247 ); 248 this.get('job.allocations') 249 .filter((a) => a.clientStatus === 'pending') 250 .slice(0, NUMBER_OF_PENDING_CANARIES) 251 .forEach((alloc) => 252 alloc.set('deploymentStatus', { 253 Canary: true, 254 Healthy: alloc.deploymentStatus?.Healthy, 255 }) 256 ); 257 258 await render(hbs` 259 <JobStatus::Panel @job={{this.job}} /> 260 `); 261 assert 262 .dom( 263 '.new-allocations .allocation-status-row .represented-allocation.running.canary' 264 ) 265 .exists( 266 { count: NUMBER_OF_RUNNING_CANARIES }, 267 'Running Canaries shown when deployment info dictates' 268 ); 269 assert 270 .dom( 271 '.new-allocations .allocation-status-row .represented-allocation.running.healthy' 272 ) 273 .exists( 274 { count: NUMBER_OF_RUNNING_HEALTHY }, 275 'Running Healthy allocs shown when deployment info dictates' 276 ); 277 assert 278 .dom( 279 '.new-allocations .allocation-status-row .represented-allocation.failed.canary' 280 ) 281 .exists( 282 { count: NUMBER_OF_FAILED_CANARIES }, 283 'Failed Canaries shown when deployment info dictates' 284 ); 285 assert 286 .dom( 287 '.new-allocations .allocation-status-row .represented-allocation.pending.canary' 288 ) 289 .exists( 290 { count: NUMBER_OF_PENDING_CANARIES }, 291 'Pending Canaries shown when deployment info dictates' 292 ); 293 294 assert.equal( 295 find('[data-test-new-allocation-tally] > span').textContent.trim(), 296 `New allocations: ${ 297 this.job.allocations.filter( 298 (a) => 299 a.clientStatus === 'running' && 300 a.deploymentStatus?.Healthy === true 301 ).length 302 }/${deployment.get('desiredTotal')} running and healthy`, 303 'Summary text shows accurate numbers when some are running/healthy' 304 ); 305 306 assert.equal( 307 find('[data-test-old-allocation-tally] > span').textContent.trim(), 308 `Previous allocations: ${ 309 this.job.allocations.filter( 310 (a) => 311 (a.clientStatus === 'running' || a.clientStatus === 'complete') && 312 a.jobVersion !== deployment.versionNumber 313 ).length 314 } running`, 315 'Old Alloc Summary text shows accurate numbers' 316 ); 317 318 assert.equal( 319 find('[data-test-previous-allocations-legend]') 320 .textContent.trim() 321 .replace(/\s\s+/g, ' '), 322 '25 Running 0 Complete' 323 ); 324 325 await percySnapshot( 326 "Job Status Panel: 'New' and 'Previous' allocations, initial deploying state" 327 ); 328 329 // Try setting a few of the old allocs to complete and make sure number ticks down 330 await Promise.all( 331 this.get('job.allocations') 332 .filter( 333 (a) => 334 a.clientStatus === 'running' && 335 a.jobVersion !== deployment.versionNumber 336 ) 337 .slice(0, OLD_ALLOCATIONS_TO_COMPLETE) 338 .map(async (a) => await a.set('clientStatus', 'complete')) 339 ); 340 341 assert 342 .dom( 343 '.previous-allocations .allocation-status-row .represented-allocation' 344 ) 345 .exists( 346 { count: OLD_ALLOCATIONS_TO_SHOW }, 347 'All old allocations are shown' 348 ); 349 assert 350 .dom( 351 '.previous-allocations .allocation-status-row .represented-allocation.complete' 352 ) 353 .exists( 354 { count: OLD_ALLOCATIONS_TO_COMPLETE }, 355 'Correct number of old allocations are in completed state' 356 ); 357 358 assert.equal( 359 find('[data-test-old-allocation-tally] > span').textContent.trim(), 360 `Previous allocations: ${ 361 this.job.allocations.filter( 362 (a) => 363 (a.clientStatus === 'running' || a.clientStatus === 'complete') && 364 a.jobVersion !== deployment.versionNumber 365 ).length - OLD_ALLOCATIONS_TO_COMPLETE 366 } running`, 367 'Old Alloc Summary text shows accurate numbers after some are marked complete' 368 ); 369 370 assert.equal( 371 find('[data-test-previous-allocations-legend]') 372 .textContent.trim() 373 .replace(/\s\s+/g, ' '), 374 '20 Running 5 Complete' 375 ); 376 377 await percySnapshot( 378 "Job Status Panel: 'New' and 'Previous' allocations, some old marked complete" 379 ); 380 381 await componentA11yAudit( 382 this.element, 383 assert, 384 'scrollable-region-focusable' 385 ); //keyframe animation fades from opacity 0 386 }); 387 388 test('non-running allocations are grouped regardless of health', async function (assert) { 389 this.server.create('node'); 390 391 const NUMBER_OF_GROUPS = 1; 392 const ALLOCS_PER_GROUP = 100; 393 const allocStatusDistribution = { 394 running: 0.9, 395 failed: 0.1, 396 unknown: 0, 397 lost: 0, 398 complete: 0, 399 pending: 0, 400 }; 401 402 const job = await this.server.create('job', { 403 type: 'service', 404 createAllocations: true, 405 noDeployments: true, // manually created below 406 activeDeployment: true, 407 groupTaskCount: ALLOCS_PER_GROUP, 408 shallow: true, 409 resourceSpec: Array(NUMBER_OF_GROUPS).fill(['M: 257, C: 500']), // length of this array determines number of groups 410 allocStatusDistribution, 411 }); 412 413 const jobRecord = await this.store.find( 414 'job', 415 JSON.stringify([job.id, 'default']) 416 ); 417 await this.server.create('deployment', false, 'active', { 418 jobId: job.id, 419 groupDesiredTotal: ALLOCS_PER_GROUP, 420 versionNumber: 1, 421 status: 'failed', 422 }); 423 424 let activelyDeployingJobAllocs = server.schema.allocations 425 .all() 426 .filter((a) => a.jobId === job.id); 427 428 activelyDeployingJobAllocs.models 429 .filter((a) => a.clientStatus === 'failed') 430 .slice(0, 10) 431 .forEach((a) => 432 a.update({ deploymentStatus: { Healthy: true, Canary: false } }) 433 ); 434 435 this.set('job', jobRecord); 436 437 await this.get('job.latestDeployment'); 438 await this.set('job.latestDeployment.status', 'running'); 439 440 await this.get('job.allocations'); 441 442 await render(hbs` 443 <JobStatus::Panel @job={{this.job}} /> 444 `); 445 446 assert 447 .dom('.allocation-status-block .represented-allocation.failed') 448 .exists({ count: 1 }, 'Failed block exists only once'); 449 assert 450 .dom('.allocation-status-block .represented-allocation.failed') 451 .hasClass('rest', 'Failed block is a summary block'); 452 453 await Promise.all( 454 this.get('job.allocations') 455 .filterBy('clientStatus', 'failed') 456 .slice(0, 3) 457 .map(async (a) => { 458 await a.set('deploymentStatus', { Healthy: false, Canary: true }); 459 }) 460 ); 461 462 assert 463 .dom('.represented-allocation.failed.rest') 464 .exists( 465 { count: 2 }, 466 'Now that some are canaries, they still make up two blocks' 467 ); 468 }); 469 470 test('During a deployment with canaries, canary alerts are handled', async function (assert) { 471 this.server.create('node'); 472 473 const NUMBER_OF_GROUPS = 1; 474 const ALLOCS_PER_GROUP = 10; 475 const allocStatusDistribution = { 476 running: 0.9, 477 failed: 0.1, 478 unknown: 0, 479 lost: 0, 480 complete: 0, 481 pending: 0, 482 }; 483 484 const job = await this.server.create('job', { 485 type: 'service', 486 createAllocations: true, 487 noDeployments: true, // manually created below 488 activeDeployment: true, 489 groupTaskCount: ALLOCS_PER_GROUP, 490 shallow: true, 491 resourceSpec: Array(NUMBER_OF_GROUPS).fill(['M: 257, C: 500']), // length of this array determines number of groups 492 allocStatusDistribution, 493 }); 494 495 const jobRecord = await this.store.find( 496 'job', 497 JSON.stringify([job.id, 'default']) 498 ); 499 const deployment = await this.server.create( 500 'deployment', 501 false, 502 'active', 503 { 504 jobId: job.id, 505 groupDesiredTotal: ALLOCS_PER_GROUP, 506 versionNumber: 1, 507 status: 'failed', 508 // requiresPromotion: false, 509 } 510 ); 511 512 // requiresPromotion goes to false 513 deployment.deploymentTaskGroupSummaries.models.forEach((d) => { 514 d.update({ 515 desiredCanaries: 0, 516 requiresPromotion: false, 517 promoted: false, 518 }); 519 }); 520 521 // All allocations set to Healthy and non-canary 522 let activelyDeployingJobAllocs = server.schema.allocations 523 .all() 524 .filter((a) => a.jobId === job.id); 525 526 activelyDeployingJobAllocs.models.forEach((a) => { 527 a.update({ deploymentStatus: { Healthy: true, Canary: false } }); 528 }); 529 530 this.set('job', jobRecord); 531 532 await this.get('job.latestDeployment'); 533 await this.set('job.latestDeployment.status', 'running'); 534 535 await this.get('job.allocations'); 536 537 await render(hbs` 538 <JobStatus::Panel @job={{this.job}} /> 539 `); 540 541 assert 542 .dom(find('.legend-item .represented-allocation.running').parentElement) 543 .hasText('9 Running'); 544 assert 545 .dom(find('.legend-item .represented-allocation.healthy').parentElement) 546 .hasText('9 Healthy'); 547 548 assert 549 .dom('.canary-promotion-alert') 550 .doesNotExist('No canary promotion alert when no canaries'); 551 552 // Set 3 allocations to health-pending canaries 553 await Promise.all( 554 this.get('job.allocations') 555 .filterBy('clientStatus', 'running') 556 .slice(0, 3) 557 .map(async (a) => { 558 await a.set('deploymentStatus', { Healthy: null, Canary: true }); 559 }) 560 ); 561 562 // Set the deployment's requiresPromotion to true 563 await Promise.all( 564 this.get('job.latestDeployment.taskGroupSummaries').map(async (a) => { 565 await a.set('desiredCanaries', 3); 566 await a.set('requiresPromotion', true); 567 }) 568 ); 569 570 await settled(); 571 572 assert 573 .dom('.canary-promotion-alert') 574 .exists('Canary promotion alert when canaries are present'); 575 576 assert 577 .dom('.canary-promotion-alert') 578 .containsText('Checking Canary health'); 579 580 // Fail the health check on 1 canary 581 await Promise.all( 582 this.get('job.allocations') 583 .filterBy('clientStatus', 'running') 584 .slice(0, 1) 585 .map(async (a) => { 586 await a.set('deploymentStatus', { Healthy: false, Canary: true }); 587 }) 588 ); 589 590 assert 591 .dom('.canary-promotion-alert') 592 .containsText('Some Canaries have failed'); 593 594 // That 1 passes its health checks, but two peers remain pending 595 await Promise.all( 596 this.get('job.allocations') 597 .filterBy('clientStatus', 'running') 598 .slice(0, 1) 599 .map(async (a) => { 600 await a.set('deploymentStatus', { Healthy: true, Canary: true }); 601 }) 602 ); 603 await settled(); 604 assert 605 .dom('.canary-promotion-alert') 606 .containsText('Checking Canary health'); 607 608 // Fail one of the running canaries, but dont specifically touch its deploymentStatus.health 609 await Promise.all( 610 this.get('job.allocations') 611 .filterBy('clientStatus', 'running') 612 .slice(0, 1) 613 .map(async (a) => { 614 await a.set('clientStatus', 'failed'); 615 }) 616 ); 617 618 assert 619 .dom('.canary-promotion-alert') 620 .containsText('Some Canaries have failed'); 621 622 // Canaries all running and healthy 623 await Promise.all( 624 this.get('job.allocations') 625 .slice(0, 3) 626 .map(async (a) => { 627 await a.setProperties({ 628 deploymentStatus: { Healthy: true, Canary: true }, 629 clientStatus: 'running', 630 }); 631 }) 632 ); 633 634 await settled(); 635 636 assert 637 .dom('.canary-promotion-alert') 638 .containsText('Deployment requires promotion'); 639 }); 640 641 test('when there is no running deployment, the latest deployment section shows up for the last deployment', async function (assert) { 642 this.server.create('job', { 643 type: 'service', 644 createAllocations: false, 645 noActiveDeployment: true, 646 }); 647 648 await this.store.findAll('job'); 649 650 this.set('job', this.store.peekAll('job').get('firstObject')); 651 await render(hbs` 652 <JobStatus::Panel @job={{this.job}} /> 653 `); 654 655 assert.notOk(find('.active-deployment'), 'No active deployment'); 656 assert.ok( 657 find('.running-allocs-title'), 658 'Steady-state mode shown instead' 659 ); 660 }); 661 } 662 );