github.com/anth0d/nomad@v0.0.0-20221214183521-ae3a0a2cad06/ui/tests/acceptance/allocation-detail-test.js (about) 1 /* eslint-disable qunit/require-expect */ 2 /* Mirage fixtures are random so we can't expect a set number of assertions */ 3 import { run } from '@ember/runloop'; 4 import { currentURL, click, visit, triggerEvent } from '@ember/test-helpers'; 5 import { assign } from '@ember/polyfills'; 6 import { module, test } from 'qunit'; 7 import { setupApplicationTest } from 'ember-qunit'; 8 import { setupMirage } from 'ember-cli-mirage/test-support'; 9 import a11yAudit from 'nomad-ui/tests/helpers/a11y-audit'; 10 import Allocation from 'nomad-ui/tests/pages/allocations/detail'; 11 import moment from 'moment'; 12 import formatHost from 'nomad-ui/utils/format-host'; 13 14 let job; 15 let node; 16 let allocation; 17 18 module('Acceptance | allocation detail', function (hooks) { 19 setupApplicationTest(hooks); 20 setupMirage(hooks); 21 22 hooks.beforeEach(async function () { 23 server.create('agent'); 24 25 node = server.create('node'); 26 job = server.create('job', { 27 groupsCount: 1, 28 withGroupServices: true, 29 createAllocations: false, 30 }); 31 allocation = server.create('allocation', 'withTaskWithPorts', { 32 clientStatus: 'running', 33 }); 34 35 // Make sure the node has an unhealthy driver 36 node.update({ 37 driver: assign(node.drivers, { 38 docker: { 39 detected: true, 40 healthy: false, 41 }, 42 }), 43 }); 44 45 // Make sure a task for the allocation depends on the unhealthy driver 46 server.schema.tasks.first().update({ 47 driver: 'docker', 48 }); 49 50 await Allocation.visit({ id: allocation.id }); 51 }); 52 53 test('it passes an accessibility audit', async function (assert) { 54 await a11yAudit(assert); 55 }); 56 57 test('/allocation/:id should name the allocation and link to the corresponding job and node', async function (assert) { 58 assert.ok( 59 Allocation.title.includes(allocation.name), 60 'Allocation name is in the heading' 61 ); 62 assert.equal( 63 Allocation.details.job, 64 job.name, 65 'Job name is in the subheading' 66 ); 67 assert.equal( 68 Allocation.details.client, 69 node.id.split('-')[0], 70 'Node short id is in the subheading' 71 ); 72 assert.ok(Allocation.execButton.isPresent); 73 74 assert.equal(document.title, `Allocation ${allocation.name} - Nomad`); 75 76 await Allocation.details.visitJob(); 77 assert.equal( 78 currentURL(), 79 `/jobs/${job.id}@default`, 80 'Job link navigates to the job' 81 ); 82 83 await Allocation.visit({ id: allocation.id }); 84 85 await Allocation.details.visitClient(); 86 assert.equal( 87 currentURL(), 88 `/clients/${node.id}`, 89 'Client link navigates to the client' 90 ); 91 }); 92 93 test('/allocation/:id should include resource utilization graphs', async function (assert) { 94 assert.equal( 95 Allocation.resourceCharts.length, 96 2, 97 'Two resource utilization graphs' 98 ); 99 assert.equal( 100 Allocation.resourceCharts.objectAt(0).name, 101 'CPU', 102 'First chart is CPU' 103 ); 104 assert.equal( 105 Allocation.resourceCharts.objectAt(1).name, 106 'Memory', 107 'Second chart is Memory' 108 ); 109 }); 110 111 test('/allocation/:id should present task lifecycles', async function (assert) { 112 const job = server.create('job', { 113 groupsCount: 1, 114 groupTaskCount: 6, 115 withGroupServices: true, 116 createAllocations: false, 117 }); 118 119 const allocation = server.create('allocation', 'withTaskWithPorts', { 120 clientStatus: 'running', 121 jobId: job.id, 122 }); 123 124 await Allocation.visit({ id: allocation.id }); 125 126 assert.ok(Allocation.lifecycleChart.isPresent); 127 assert.equal(Allocation.lifecycleChart.title, 'Task Lifecycle Status'); 128 assert.equal(Allocation.lifecycleChart.phases.length, 4); 129 assert.equal(Allocation.lifecycleChart.tasks.length, 6); 130 131 await Allocation.lifecycleChart.tasks[0].visit(); 132 133 const prestartEphemeralTask = server.db.taskStates 134 .where({ allocationId: allocation.id }) 135 .sortBy('name') 136 .find((taskState) => { 137 const task = server.db.tasks.findBy({ name: taskState.name }); 138 return ( 139 task.Lifecycle && 140 task.Lifecycle.Hook === 'prestart' && 141 !task.Lifecycle.Sidecar 142 ); 143 }); 144 145 assert.equal( 146 currentURL(), 147 `/allocations/${allocation.id}/${prestartEphemeralTask.name}` 148 ); 149 }); 150 151 test('/allocation/:id should list all tasks for the allocation', async function (assert) { 152 assert.equal( 153 Allocation.tasks.length, 154 server.db.taskStates.where({ allocationId: allocation.id }).length, 155 'Table lists all tasks' 156 ); 157 assert.notOk(Allocation.isEmpty, 'Task table empty state is not shown'); 158 }); 159 160 test('each task row should list high-level information for the task', async function (assert) { 161 const job = server.create('job', { 162 groupsCount: 1, 163 groupTaskCount: 3, 164 withGroupServices: true, 165 createAllocations: false, 166 }); 167 168 const allocation = server.create('allocation', 'withTaskWithPorts', { 169 clientStatus: 'running', 170 jobId: job.id, 171 }); 172 173 const taskGroup = server.schema.taskGroups.where({ 174 jobId: allocation.jobId, 175 name: allocation.taskGroup, 176 }).models[0]; 177 178 // Set the expected task states. 179 const states = ['running', 'pending', 'dead']; 180 server.db.taskStates 181 .where({ allocationId: allocation.id }) 182 .sortBy('name') 183 .forEach((task, i) => { 184 server.db.taskStates.update(task.id, { state: states[i] }); 185 }); 186 187 await Allocation.visit({ id: allocation.id }); 188 189 Allocation.tasks.forEach((taskRow, i) => { 190 const task = server.db.taskStates 191 .where({ allocationId: allocation.id }) 192 .sortBy('name')[i]; 193 const events = server.db.taskEvents.where({ taskStateId: task.id }); 194 const event = events[events.length - 1]; 195 196 const jobTask = taskGroup.tasks.models.find((m) => m.name === task.name); 197 const volumes = jobTask.volumeMounts.map((volume) => ({ 198 name: volume.Volume, 199 source: taskGroup.volumes[volume.Volume].Source, 200 })); 201 202 assert.equal(taskRow.name, task.name, 'Name'); 203 assert.equal(taskRow.state, task.state, 'State'); 204 assert.equal(taskRow.message, event.displayMessage, 'Event Message'); 205 assert.equal( 206 taskRow.time, 207 moment(event.time / 1000000).format("MMM DD, 'YY HH:mm:ss ZZ"), 208 'Event Time' 209 ); 210 211 const expectStats = task.state === 'running'; 212 assert.equal(taskRow.hasCpuMetrics, expectStats, 'CPU metrics'); 213 assert.equal(taskRow.hasMemoryMetrics, expectStats, 'Memory metrics'); 214 215 const volumesText = taskRow.volumes; 216 volumes.forEach((volume) => { 217 assert.ok( 218 volumesText.includes(volume.name), 219 `Found label ${volume.name}` 220 ); 221 assert.ok( 222 volumesText.includes(volume.source), 223 `Found value ${volume.source}` 224 ); 225 }); 226 }); 227 }); 228 229 test('each task row should link to the task detail page', async function (assert) { 230 const task = server.db.taskStates 231 .where({ allocationId: allocation.id }) 232 .sortBy('name')[0]; 233 234 await Allocation.tasks.objectAt(0).clickLink(); 235 236 // Make sure the allocation is pending in order to ensure there are no tasks 237 assert.equal( 238 currentURL(), 239 `/allocations/${allocation.id}/${task.name}`, 240 'Task name in task row links to task detail' 241 ); 242 243 await Allocation.visit({ id: allocation.id }); 244 await Allocation.tasks.objectAt(0).clickRow(); 245 246 assert.equal( 247 currentURL(), 248 `/allocations/${allocation.id}/${task.name}`, 249 'Task row links to task detail' 250 ); 251 }); 252 253 test('tasks with an unhealthy driver have a warning icon', async function (assert) { 254 assert.ok( 255 Allocation.firstUnhealthyTask().hasUnhealthyDriver, 256 'Warning is shown' 257 ); 258 }); 259 260 test('proxy task has a proxy tag', async function (assert) { 261 // Must create a new job as existing one has loaded and it contains the tasks 262 job = server.create('job', { 263 groupsCount: 1, 264 withGroupServices: true, 265 createAllocations: false, 266 }); 267 268 allocation = server.create('allocation', 'withTaskWithPorts', { 269 clientStatus: 'running', 270 jobId: job.id, 271 }); 272 273 const taskState = allocation.taskStates.models.sortBy('name')[0]; 274 const task = server.schema.tasks.findBy({ name: taskState.name }); 275 task.update('kind', 'connect-proxy:task'); 276 task.save(); 277 278 await Allocation.visit({ id: allocation.id }); 279 280 assert.ok(Allocation.tasks[0].hasProxyTag); 281 }); 282 283 test('when there are no tasks, an empty state is shown', async function (assert) { 284 // Make sure the allocation is pending in order to ensure there are no tasks 285 allocation = server.create('allocation', 'withTaskWithPorts', { 286 clientStatus: 'pending', 287 }); 288 await Allocation.visit({ id: allocation.id }); 289 290 assert.ok(Allocation.isEmpty, 'Task table empty state is shown'); 291 }); 292 293 test('when the allocation has not been rescheduled, the reschedule events section is not rendered', async function (assert) { 294 assert.notOk( 295 Allocation.hasRescheduleEvents, 296 'Reschedule Events section exists' 297 ); 298 }); 299 300 test('ports are listed', async function (assert) { 301 const allServerPorts = allocation.taskResources.models[0].resources.Ports; 302 303 allServerPorts.sortBy('Label').forEach((serverPort, index) => { 304 const renderedPort = Allocation.ports[index]; 305 306 assert.equal(renderedPort.name, serverPort.Label); 307 assert.equal(renderedPort.to, serverPort.To); 308 assert.equal( 309 renderedPort.address, 310 formatHost(serverPort.HostIP, serverPort.Value) 311 ); 312 }); 313 }); 314 315 test('services are listed', async function (assert) { 316 const taskGroup = server.schema.taskGroups.findBy({ 317 name: allocation.taskGroup, 318 }); 319 320 assert.equal(Allocation.services.length, taskGroup.services.length); 321 322 taskGroup.services.models.sortBy('name').forEach((serverService, index) => { 323 const renderedService = Allocation.services[index]; 324 325 assert.equal(renderedService.name, serverService.name); 326 assert.equal(renderedService.port, serverService.portLabel); 327 assert.equal(renderedService.tags, (serverService.tags || []).join(' ')); 328 }); 329 }); 330 331 test('when the allocation is not found, an error message is shown, but the URL persists', async function (assert) { 332 await Allocation.visit({ id: 'not-a-real-allocation' }); 333 334 assert.equal( 335 server.pretender.handledRequests 336 .filter((request) => !request.url.includes('policy')) 337 .findBy('status', 404).url, 338 '/v1/allocation/not-a-real-allocation', 339 'A request to the nonexistent allocation is made' 340 ); 341 assert.equal( 342 currentURL(), 343 '/allocations/not-a-real-allocation', 344 'The URL persists' 345 ); 346 assert.ok(Allocation.error.isShown, 'Error message is shown'); 347 assert.equal( 348 Allocation.error.title, 349 'Not Found', 350 'Error message is for 404' 351 ); 352 }); 353 354 test('allocation can be stopped', async function (assert) { 355 await Allocation.stop.idle(); 356 await Allocation.stop.confirm(); 357 358 assert.equal( 359 server.pretender.handledRequests 360 .reject((request) => request.url.includes('fuzzy')) 361 .findBy('method', 'POST').url, 362 `/v1/allocation/${allocation.id}/stop`, 363 'Stop request is made for the allocation' 364 ); 365 }); 366 367 test('allocation can be restarted', async function (assert) { 368 await Allocation.restartAll.idle(); 369 await Allocation.restart.idle(); 370 await Allocation.restart.confirm(); 371 372 assert.equal( 373 server.pretender.handledRequests.findBy('method', 'PUT').url, 374 `/v1/client/allocation/${allocation.id}/restart`, 375 'Restart request is made for the allocation' 376 ); 377 378 await Allocation.restart.idle(); 379 await Allocation.restartAll.idle(); 380 await Allocation.restartAll.confirm(); 381 382 assert.ok( 383 server.pretender.handledRequests.filterBy( 384 'requestBody', 385 JSON.stringify({ AllTasks: true }) 386 ), 387 'Restart all tasks request is made for the allocation' 388 ); 389 }); 390 391 test('while an allocation is being restarted, the stop button is disabled', async function (assert) { 392 server.pretender.post('/v1/allocation/:id/stop', () => [204, {}, ''], true); 393 394 await Allocation.stop.idle(); 395 396 run.later(() => { 397 assert.ok(Allocation.stop.isRunning, 'Stop is loading'); 398 assert.ok(Allocation.restart.isDisabled, 'Restart is disabled'); 399 assert.ok(Allocation.restartAll.isDisabled, 'Restart All is disabled'); 400 server.pretender.resolve(server.pretender.requestReferences[0].request); 401 }, 500); 402 403 await Allocation.stop.confirm(); 404 }); 405 406 test('if stopping or restarting fails, an error message is shown', async function (assert) { 407 server.pretender.post('/v1/allocation/:id/stop', () => [403, {}, '']); 408 409 await Allocation.stop.idle(); 410 await Allocation.stop.confirm(); 411 412 assert.ok(Allocation.inlineError.isShown, 'Inline error is shown'); 413 assert.ok( 414 Allocation.inlineError.title.includes('Could Not Stop Allocation'), 415 'Title is descriptive' 416 ); 417 assert.ok( 418 /ACL token.+?allocation lifecycle/.test(Allocation.inlineError.message), 419 'Message mentions ACLs and the appropriate permission' 420 ); 421 422 await Allocation.inlineError.dismiss(); 423 424 assert.notOk( 425 Allocation.inlineError.isShown, 426 'Inline error is no longer shown' 427 ); 428 }); 429 }); 430 431 module('Acceptance | allocation detail (rescheduled)', function (hooks) { 432 setupApplicationTest(hooks); 433 setupMirage(hooks); 434 435 hooks.beforeEach(async function () { 436 server.create('agent'); 437 438 node = server.create('node'); 439 job = server.create('job', { createAllocations: false }); 440 allocation = server.create('allocation', 'rescheduled'); 441 442 await Allocation.visit({ id: allocation.id }); 443 }); 444 445 test('when the allocation has been rescheduled, the reschedule events section is rendered', async function (assert) { 446 assert.ok( 447 Allocation.hasRescheduleEvents, 448 'Reschedule Events section exists' 449 ); 450 }); 451 }); 452 453 module('Acceptance | allocation detail (not running)', function (hooks) { 454 setupApplicationTest(hooks); 455 setupMirage(hooks); 456 457 hooks.beforeEach(async function () { 458 server.create('agent'); 459 460 node = server.create('node'); 461 job = server.create('job', { createAllocations: false }); 462 allocation = server.create('allocation', { clientStatus: 'pending' }); 463 464 await Allocation.visit({ id: allocation.id }); 465 }); 466 467 test('when the allocation is not running, the utilization graphs are replaced by an empty message', async function (assert) { 468 assert.equal(Allocation.resourceCharts.length, 0, 'No resource charts'); 469 assert.equal( 470 Allocation.resourceEmptyMessage, 471 "Allocation isn't running", 472 'Empty message is appropriate' 473 ); 474 }); 475 476 test('the exec and stop/restart buttons are absent', async function (assert) { 477 assert.notOk(Allocation.execButton.isPresent); 478 assert.notOk(Allocation.stop.isPresent); 479 assert.notOk(Allocation.restart.isPresent); 480 assert.notOk(Allocation.restartAll.isPresent); 481 }); 482 }); 483 484 module('Acceptance | allocation detail (preemptions)', function (hooks) { 485 setupApplicationTest(hooks); 486 setupMirage(hooks); 487 488 hooks.beforeEach(async function () { 489 server.create('agent'); 490 node = server.create('node'); 491 job = server.create('job', { createAllocations: false }); 492 }); 493 494 test('shows a dedicated section to the allocation that preempted this allocation', async function (assert) { 495 allocation = server.create('allocation', 'preempted'); 496 const preempter = server.schema.find( 497 'allocation', 498 allocation.preemptedByAllocation 499 ); 500 const preempterJob = server.schema.find('job', preempter.jobId); 501 const preempterClient = server.schema.find('node', preempter.nodeId); 502 503 await Allocation.visit({ id: allocation.id }); 504 assert.ok(Allocation.wasPreempted, 'Preempted allocation section is shown'); 505 assert.equal( 506 Allocation.preempter.status, 507 preempter.clientStatus, 508 'Preempter status matches' 509 ); 510 assert.equal( 511 Allocation.preempter.name, 512 preempter.name, 513 'Preempter name matches' 514 ); 515 assert.equal( 516 Allocation.preempter.priority, 517 preempterJob.priority, 518 'Preempter priority matches' 519 ); 520 521 await Allocation.preempter.visit(); 522 assert.equal( 523 currentURL(), 524 `/allocations/${preempter.id}`, 525 'Clicking the preempter id navigates to the preempter allocation detail page' 526 ); 527 528 await Allocation.visit({ id: allocation.id }); 529 530 await Allocation.preempter.visitJob(); 531 assert.equal( 532 currentURL(), 533 `/jobs/${preempterJob.id}@default`, 534 'Clicking the preempter job link navigates to the preempter job page' 535 ); 536 537 await Allocation.visit({ id: allocation.id }); 538 await Allocation.preempter.visitClient(); 539 assert.equal( 540 currentURL(), 541 `/clients/${preempterClient.id}`, 542 'Clicking the preempter client link navigates to the preempter client page' 543 ); 544 }); 545 546 test('shows a dedicated section to the allocations this allocation preempted', async function (assert) { 547 allocation = server.create('allocation', 'preempter'); 548 await Allocation.visit({ id: allocation.id }); 549 assert.ok( 550 Allocation.preempted, 551 'The allocations this allocation preempted are shown' 552 ); 553 }); 554 555 test('each preempted allocation in the table lists basic allocation information', async function (assert) { 556 allocation = server.create('allocation', 'preempter'); 557 await Allocation.visit({ id: allocation.id }); 558 559 const preemption = allocation.preemptedAllocations 560 .map((id) => server.schema.find('allocation', id)) 561 .sortBy('modifyIndex') 562 .reverse()[0]; 563 const preemptionRow = Allocation.preemptions.objectAt(0); 564 565 assert.equal( 566 Allocation.preemptions.length, 567 allocation.preemptedAllocations.length, 568 'The preemptions table has a row for each preempted allocation' 569 ); 570 571 assert.equal( 572 preemptionRow.shortId, 573 preemption.id.split('-')[0], 574 'Preemption short id' 575 ); 576 assert.equal( 577 preemptionRow.createTime, 578 moment(preemption.createTime / 1000000).format('MMM DD HH:mm:ss ZZ'), 579 'Preemption create time' 580 ); 581 assert.equal( 582 preemptionRow.modifyTime, 583 moment(preemption.modifyTime / 1000000).fromNow(), 584 'Preemption modify time' 585 ); 586 assert.equal( 587 preemptionRow.status, 588 preemption.clientStatus, 589 'Client status' 590 ); 591 assert.equal( 592 preemptionRow.jobVersion, 593 preemption.jobVersion, 594 'Job Version' 595 ); 596 assert.equal( 597 preemptionRow.client, 598 server.db.nodes.find(preemption.nodeId).id.split('-')[0], 599 'Node ID' 600 ); 601 602 await preemptionRow.visitClient(); 603 assert.equal( 604 currentURL(), 605 `/clients/${preemption.nodeId}`, 606 'Node links to node page' 607 ); 608 }); 609 610 test('when an allocation both preempted allocations and was preempted itself, both preemptions sections are shown', async function (assert) { 611 allocation = server.create('allocation', 'preempter', 'preempted'); 612 await Allocation.visit({ id: allocation.id }); 613 assert.ok( 614 Allocation.preempted, 615 'The allocations this allocation preempted are shown' 616 ); 617 assert.ok(Allocation.wasPreempted, 'Preempted allocation section is shown'); 618 }); 619 }); 620 621 module('Acceptance | allocation detail (services)', function (hooks) { 622 setupApplicationTest(hooks); 623 setupMirage(hooks); 624 625 hooks.beforeEach(async function () { 626 server.create('feature', { name: 'Dynamic Application Sizing' }); 627 server.createList('agent', 3, 'withConsulLink', 'withVaultLink'); 628 server.createList('node', 5); 629 server.createList('job', 1, { createRecommendations: true }); 630 const job = server.create('job', { 631 withGroupServices: true, 632 withTaskServices: true, 633 name: 'Service-haver', 634 id: 'service-haver', 635 namespaceId: 'default', 636 }); 637 638 const currentAlloc = server.db.allocations.findBy({ jobId: job.id }); 639 const otherAlloc = server.db.allocations.reject((j) => j.jobId !== job.id); 640 641 server.db.serviceFragments.update({ 642 healthChecks: [ 643 { 644 Status: 'success', 645 Check: 'check1', 646 Timestamp: 99, 647 Alloc: currentAlloc.id, 648 }, 649 { 650 Status: 'failure', 651 Check: 'check2', 652 Output: 'One', 653 propThatDoesntMatter: 654 'this object will be ignored, since it shared a Check name with a later one.', 655 Timestamp: 98, 656 Alloc: currentAlloc.id, 657 }, 658 { 659 Status: 'success', 660 Check: 'check2', 661 Output: 'Two', 662 Timestamp: 99, 663 Alloc: currentAlloc.id, 664 }, 665 { 666 Status: 'failure', 667 Check: 'check3', 668 Output: 'Oh no!', 669 Timestamp: 99, 670 Alloc: currentAlloc.id, 671 }, 672 { 673 Status: 'success', 674 Check: 'check3', 675 Output: 'Wont be seen', 676 propThatDoesntMatter: 677 'this object will be ignored, in spite of its later timestamp, since it exists on a different alloc', 678 Timestamp: 100, 679 Alloc: otherAlloc.id, 680 }, 681 ], 682 }); 683 }); 684 685 test('Allocation has a list of services with active checks', async function (assert) { 686 await visit('jobs/service-haver@default'); 687 await click('.allocation-row'); 688 assert.dom('[data-test-service]').exists(); 689 assert.dom('.service-sidebar').exists(); 690 assert.dom('.service-sidebar').doesNotHaveClass('open'); 691 assert 692 .dom('[data-test-service-status-bar]') 693 .exists('At least one allocation has service health'); 694 await click('[data-test-service-status-bar]'); 695 assert.dom('.service-sidebar').hasClass('open'); 696 assert 697 .dom('table.health-checks tr[data-service-health="success"]') 698 .exists({ count: 2 }, 'Two successful health checks'); 699 assert 700 .dom('table.health-checks tr[data-service-health="failure"]') 701 .exists({ count: 1 }, 'One failing health check'); 702 assert 703 .dom( 704 'table.health-checks tr[data-service-health="failure"] td.service-output' 705 ) 706 .containsText('Oh no!'); 707 708 await triggerEvent('.page-layout', 'keydown', { key: 'Escape' }); 709 assert.dom('.service-sidebar').doesNotHaveClass('open'); 710 }); 711 });