github.com/Ilhicas/nomad@v1.0.4-0.20210304152020-e86851182bc3/ui/tests/acceptance/client-detail-test.js (about) 1 import { currentURL, waitUntil, settled } from '@ember/test-helpers'; 2 import { assign } from '@ember/polyfills'; 3 import { module, test } from 'qunit'; 4 import { setupApplicationTest } from 'ember-qunit'; 5 import { setupMirage } from 'ember-cli-mirage/test-support'; 6 import a11yAudit from 'nomad-ui/tests/helpers/a11y-audit'; 7 import { formatBytes } from 'nomad-ui/helpers/format-bytes'; 8 import moment from 'moment'; 9 import ClientDetail from 'nomad-ui/tests/pages/clients/detail'; 10 import Clients from 'nomad-ui/tests/pages/clients/list'; 11 import Jobs from 'nomad-ui/tests/pages/jobs/list'; 12 import Layout from 'nomad-ui/tests/pages/layout'; 13 14 let node; 15 let managementToken; 16 let clientToken; 17 18 const wasPreemptedFilter = allocation => !!allocation.preemptedByAllocation; 19 20 module('Acceptance | client detail', function(hooks) { 21 setupApplicationTest(hooks); 22 setupMirage(hooks); 23 24 hooks.beforeEach(function() { 25 server.create('node', 'forceIPv4', { schedulingEligibility: 'eligible' }); 26 node = server.db.nodes[0]; 27 28 managementToken = server.create('token'); 29 clientToken = server.create('token'); 30 31 window.localStorage.nomadTokenSecret = managementToken.secretId; 32 33 // Related models 34 server.create('agent'); 35 server.create('job', { createAllocations: false }); 36 server.createList('allocation', 3); 37 server.create('allocation', 'preempted'); 38 39 // Force all allocations into the running state so now allocation rows are missing 40 // CPU/Mem runtime metrics 41 server.schema.allocations.all().models.forEach(allocation => { 42 allocation.update({ clientStatus: 'running' }); 43 }); 44 }); 45 46 test('it passes an accessibility audit', async function(assert) { 47 await ClientDetail.visit({ id: node.id }); 48 await a11yAudit(assert); 49 }); 50 51 test('/clients/:id should have a breadcrumb trail linking back to clients', async function(assert) { 52 await ClientDetail.visit({ id: node.id }); 53 54 assert.equal(document.title, `Client ${node.name} - Nomad`); 55 56 assert.equal( 57 Layout.breadcrumbFor('clients.index').text, 58 'Clients', 59 'First breadcrumb says clients' 60 ); 61 assert.equal( 62 Layout.breadcrumbFor('clients.client').text, 63 node.id.split('-')[0], 64 'Second breadcrumb says the node short id' 65 ); 66 await Layout.breadcrumbFor('clients.index').visit(); 67 assert.equal(currentURL(), '/clients', 'First breadcrumb links back to clients'); 68 }); 69 70 test('/clients/:id should list immediate details for the node in the title', async function(assert) { 71 node = server.create('node', 'forceIPv4', { schedulingEligibility: 'eligible', drain: false }); 72 73 await ClientDetail.visit({ id: node.id }); 74 75 assert.ok(ClientDetail.title.includes(node.name), 'Title includes name'); 76 assert.ok(ClientDetail.clientId.includes(node.id), 'Title includes id'); 77 assert.equal( 78 ClientDetail.statusLight.objectAt(0).id, 79 node.status, 80 'Title includes status light' 81 ); 82 }); 83 84 test('/clients/:id should list additional detail for the node below the title', async function(assert) { 85 await ClientDetail.visit({ id: node.id }); 86 87 assert.ok( 88 ClientDetail.statusDefinition.includes(node.status), 89 'Status is in additional details' 90 ); 91 assert.ok( 92 ClientDetail.statusDecorationClass.includes(`node-${node.status}`), 93 'Status is decorated with a status class' 94 ); 95 assert.ok( 96 ClientDetail.addressDefinition.includes(node.httpAddr), 97 'Address is in additional details' 98 ); 99 assert.ok( 100 ClientDetail.datacenterDefinition.includes(node.datacenter), 101 'Datacenter is in additional details' 102 ); 103 }); 104 105 test('/clients/:id should include resource utilization graphs', async function(assert) { 106 await ClientDetail.visit({ id: node.id }); 107 108 assert.equal(ClientDetail.resourceCharts.length, 2, 'Two resource utilization graphs'); 109 assert.equal(ClientDetail.resourceCharts.objectAt(0).name, 'CPU', 'First chart is CPU'); 110 assert.equal(ClientDetail.resourceCharts.objectAt(1).name, 'Memory', 'Second chart is Memory'); 111 }); 112 113 test('/clients/:id should list all allocations on the node', async function(assert) { 114 const allocationsCount = server.db.allocations.where({ nodeId: node.id }).length; 115 116 await ClientDetail.visit({ id: node.id }); 117 118 assert.equal( 119 ClientDetail.allocations.length, 120 allocationsCount, 121 `Allocations table lists all ${allocationsCount} associated allocations` 122 ); 123 }); 124 125 test('each allocation should have high-level details for the allocation', async function(assert) { 126 const allocation = server.db.allocations 127 .where({ nodeId: node.id }) 128 .sortBy('modifyIndex') 129 .reverse()[0]; 130 131 const allocStats = server.db.clientAllocationStats.find(allocation.id); 132 const taskGroup = server.db.taskGroups.findBy({ 133 name: allocation.taskGroup, 134 jobId: allocation.jobId, 135 }); 136 137 const tasks = taskGroup.taskIds.map(id => server.db.tasks.find(id)); 138 const cpuUsed = tasks.reduce((sum, task) => sum + task.resources.CPU, 0); 139 const memoryUsed = tasks.reduce((sum, task) => sum + task.resources.MemoryMB, 0); 140 141 await ClientDetail.visit({ id: node.id }); 142 143 const allocationRow = ClientDetail.allocations.objectAt(0); 144 145 assert.equal(allocationRow.shortId, allocation.id.split('-')[0], 'Allocation short ID'); 146 assert.equal( 147 allocationRow.createTime, 148 moment(allocation.createTime / 1000000).format('MMM DD HH:mm:ss ZZ'), 149 'Allocation create time' 150 ); 151 assert.equal( 152 allocationRow.modifyTime, 153 moment(allocation.modifyTime / 1000000).fromNow(), 154 'Allocation modify time' 155 ); 156 assert.equal(allocationRow.status, allocation.clientStatus, 'Client status'); 157 assert.equal(allocationRow.job, server.db.jobs.find(allocation.jobId).name, 'Job name'); 158 assert.ok(allocationRow.taskGroup, 'Task group name'); 159 assert.ok(allocationRow.jobVersion, 'Job Version'); 160 assert.equal(allocationRow.volume, 'Yes', 'Volume'); 161 assert.equal( 162 allocationRow.cpu, 163 Math.floor(allocStats.resourceUsage.CpuStats.TotalTicks) / cpuUsed, 164 'CPU %' 165 ); 166 assert.equal( 167 allocationRow.cpuTooltip, 168 `${Math.floor(allocStats.resourceUsage.CpuStats.TotalTicks)} / ${cpuUsed} MHz`, 169 'Detailed CPU information is in a tooltip' 170 ); 171 assert.equal( 172 allocationRow.mem, 173 allocStats.resourceUsage.MemoryStats.RSS / 1024 / 1024 / memoryUsed, 174 'Memory used' 175 ); 176 assert.equal( 177 allocationRow.memTooltip, 178 `${formatBytes([allocStats.resourceUsage.MemoryStats.RSS])} / ${memoryUsed} MiB`, 179 'Detailed memory information is in a tooltip' 180 ); 181 }); 182 183 test('each allocation should show job information even if the job is incomplete and already in the store', async function(assert) { 184 // First, visit clients to load the allocations for each visible node. 185 // Don't load the job belongsTo of the allocation! Leave it unfulfilled. 186 187 await Clients.visit(); 188 189 // Then, visit jobs to load all jobs, which should implicitly fulfill 190 // the job belongsTo of each allocation pointed at each job. 191 192 await Jobs.visit(); 193 194 // Finally, visit a node to assert that the job name and task group name are 195 // present. This will require reloading the job, since task groups aren't a 196 // part of the jobs list response. 197 198 await ClientDetail.visit({ id: node.id }); 199 200 const allocationRow = ClientDetail.allocations.objectAt(0); 201 const allocation = server.db.allocations 202 .where({ nodeId: node.id }) 203 .sortBy('modifyIndex') 204 .reverse()[0]; 205 206 assert.equal(allocationRow.job, server.db.jobs.find(allocation.jobId).name, 'Job name'); 207 assert.ok(allocationRow.taskGroup.includes(allocation.taskGroup), 'Task group name'); 208 }); 209 210 test('each allocation should link to the allocation detail page', async function(assert) { 211 const allocation = server.db.allocations 212 .where({ nodeId: node.id }) 213 .sortBy('modifyIndex') 214 .reverse()[0]; 215 216 await ClientDetail.visit({ id: node.id }); 217 await ClientDetail.allocations.objectAt(0).visit(); 218 219 assert.equal( 220 currentURL(), 221 `/allocations/${allocation.id}`, 222 'Allocation rows link to allocation detail pages' 223 ); 224 }); 225 226 test('each allocation should link to the job the allocation belongs to', async function(assert) { 227 await ClientDetail.visit({ id: node.id }); 228 229 const allocation = server.db.allocations.where({ nodeId: node.id })[0]; 230 const job = server.db.jobs.find(allocation.jobId); 231 232 await ClientDetail.allocations.objectAt(0).visitJob(); 233 234 assert.equal( 235 currentURL(), 236 `/jobs/${job.id}`, 237 'Allocation rows link to the job detail page for the allocation' 238 ); 239 }); 240 241 test('the allocation section should show the count of preempted allocations on the client', async function(assert) { 242 const allocations = server.db.allocations.where({ nodeId: node.id }); 243 244 await ClientDetail.visit({ id: node.id }); 245 246 assert.equal( 247 ClientDetail.allocationFilter.allCount, 248 allocations.length, 249 'All filter/badge shows all allocations count' 250 ); 251 assert.ok( 252 ClientDetail.allocationFilter.preemptionsCount.startsWith( 253 allocations.filter(wasPreemptedFilter).length 254 ), 255 'Preemptions filter/badge shows preempted allocations count' 256 ); 257 }); 258 259 test('clicking the preemption badge filters the allocations table and sets a query param', async function(assert) { 260 const allocations = server.db.allocations.where({ nodeId: node.id }); 261 262 await ClientDetail.visit({ id: node.id }); 263 await ClientDetail.allocationFilter.preemptions(); 264 265 assert.equal( 266 ClientDetail.allocations.length, 267 allocations.filter(wasPreemptedFilter).length, 268 'Only preempted allocations are shown' 269 ); 270 assert.equal( 271 currentURL(), 272 `/clients/${node.id}?preemptions=true`, 273 'Filter is persisted in the URL' 274 ); 275 }); 276 277 test('clicking the total allocations badge resets the filter and removes the query param', async function(assert) { 278 const allocations = server.db.allocations.where({ nodeId: node.id }); 279 280 await ClientDetail.visit({ id: node.id }); 281 await ClientDetail.allocationFilter.preemptions(); 282 await ClientDetail.allocationFilter.all(); 283 284 assert.equal(ClientDetail.allocations.length, allocations.length, 'All allocations are shown'); 285 assert.equal(currentURL(), `/clients/${node.id}`, 'Filter is persisted in the URL'); 286 }); 287 288 test('navigating directly to the client detail page with the preemption query param set will filter the allocations table', async function(assert) { 289 const allocations = server.db.allocations.where({ nodeId: node.id }); 290 291 await ClientDetail.visit({ id: node.id, preemptions: true }); 292 293 assert.equal( 294 ClientDetail.allocations.length, 295 allocations.filter(wasPreemptedFilter).length, 296 'Only preempted allocations are shown' 297 ); 298 }); 299 300 test('/clients/:id should list all attributes for the node', async function(assert) { 301 await ClientDetail.visit({ id: node.id }); 302 303 assert.ok(ClientDetail.attributesTable, 'Attributes table is on the page'); 304 }); 305 306 test('/clients/:id lists all meta attributes', async function(assert) { 307 node = server.create('node', 'forceIPv4', 'withMeta'); 308 309 await ClientDetail.visit({ id: node.id }); 310 311 assert.ok(ClientDetail.metaTable, 'Meta attributes table is on the page'); 312 assert.notOk(ClientDetail.emptyMetaMessage, 'Meta attributes is not empty'); 313 314 const firstMetaKey = Object.keys(node.meta)[0]; 315 const firstMetaAttribute = ClientDetail.metaAttributes.objectAt(0); 316 assert.equal( 317 firstMetaAttribute.key, 318 firstMetaKey, 319 'Meta attributes for the node are bound to the attributes table' 320 ); 321 assert.equal( 322 firstMetaAttribute.value, 323 node.meta[firstMetaKey], 324 'Meta attributes for the node are bound to the attributes table' 325 ); 326 }); 327 328 test('/clients/:id shows an empty message when there is no meta data', async function(assert) { 329 await ClientDetail.visit({ id: node.id }); 330 331 assert.notOk(ClientDetail.metaTable, 'Meta attributes table is not on the page'); 332 assert.ok(ClientDetail.emptyMetaMessage, 'Meta attributes is empty'); 333 }); 334 335 test('when the node is not found, an error message is shown, but the URL persists', async function(assert) { 336 await ClientDetail.visit({ id: 'not-a-real-node' }); 337 338 assert.equal( 339 server.pretender.handledRequests 340 .filter(request => !request.url.includes('policy')) 341 .findBy('status', 404).url, 342 '/v1/node/not-a-real-node', 343 'A request to the nonexistent node is made' 344 ); 345 assert.equal(currentURL(), '/clients/not-a-real-node', 'The URL persists'); 346 assert.ok(ClientDetail.error.isShown, 'Error message is shown'); 347 assert.equal(ClientDetail.error.title, 'Not Found', 'Error message is for 404'); 348 }); 349 350 test('/clients/:id shows the recent events list', async function(assert) { 351 await ClientDetail.visit({ id: node.id }); 352 353 assert.ok(ClientDetail.hasEvents, 'Client events section exists'); 354 }); 355 356 test('each node event shows basic node event information', async function(assert) { 357 const event = server.db.nodeEvents 358 .where({ nodeId: node.id }) 359 .sortBy('time') 360 .reverse()[0]; 361 362 await ClientDetail.visit({ id: node.id }); 363 364 const eventRow = ClientDetail.events.objectAt(0); 365 assert.equal( 366 eventRow.time, 367 moment(event.time).format("MMM DD, 'YY HH:mm:ss ZZ"), 368 'Event timestamp' 369 ); 370 assert.equal(eventRow.subsystem, event.subsystem, 'Event subsystem'); 371 assert.equal(eventRow.message, event.message, 'Event message'); 372 }); 373 374 test('/clients/:id shows the driver status of every driver for the node', async function(assert) { 375 // Set the drivers up so health and detection is well tested 376 const nodeDrivers = node.drivers; 377 const undetectedDriver = 'raw_exec'; 378 379 Object.values(nodeDrivers).forEach(driver => { 380 driver.Detected = true; 381 }); 382 383 nodeDrivers[undetectedDriver].Detected = false; 384 node.drivers = nodeDrivers; 385 386 const drivers = Object.keys(node.drivers) 387 .map(driverName => assign({ Name: driverName }, node.drivers[driverName])) 388 .sortBy('Name'); 389 390 assert.ok(drivers.length > 0, 'Node has drivers'); 391 392 await ClientDetail.visit({ id: node.id }); 393 394 drivers.forEach((driver, index) => { 395 const driverHead = ClientDetail.driverHeads.objectAt(index); 396 397 assert.equal(driverHead.name, driver.Name, `${driver.Name}: Name is correct`); 398 assert.equal( 399 driverHead.detected, 400 driver.Detected ? 'Yes' : 'No', 401 `${driver.Name}: Detection is correct` 402 ); 403 assert.equal( 404 driverHead.lastUpdated, 405 moment(driver.UpdateTime).fromNow(), 406 `${driver.Name}: Last updated shows time since now` 407 ); 408 409 if (driver.Name === undetectedDriver) { 410 assert.notOk( 411 driverHead.healthIsShown, 412 `${driver.Name}: No health for the undetected driver` 413 ); 414 } else { 415 assert.equal( 416 driverHead.health, 417 driver.Healthy ? 'Healthy' : 'Unhealthy', 418 `${driver.Name}: Health is correct` 419 ); 420 assert.ok( 421 driverHead.healthClass.includes(driver.Healthy ? 'running' : 'failed'), 422 `${driver.Name}: Swatch with correct class is shown` 423 ); 424 } 425 }); 426 }); 427 428 test('each driver can be opened to see a message and attributes', async function(assert) { 429 // Only detected drivers can be expanded 430 const nodeDrivers = node.drivers; 431 Object.values(nodeDrivers).forEach(driver => { 432 driver.Detected = true; 433 }); 434 node.drivers = nodeDrivers; 435 436 const driver = Object.keys(node.drivers) 437 .map(driverName => assign({ Name: driverName }, node.drivers[driverName])) 438 .sortBy('Name')[0]; 439 440 await ClientDetail.visit({ id: node.id }); 441 const driverHead = ClientDetail.driverHeads.objectAt(0); 442 const driverBody = ClientDetail.driverBodies.objectAt(0); 443 444 assert.notOk(driverBody.descriptionIsShown, 'Driver health description is not shown'); 445 assert.notOk(driverBody.attributesAreShown, 'Driver attributes section is not shown'); 446 447 await driverHead.toggle(); 448 assert.equal( 449 driverBody.description, 450 driver.HealthDescription, 451 'Driver health description is now shown' 452 ); 453 assert.ok(driverBody.attributesAreShown, 'Driver attributes section is now shown'); 454 }); 455 456 test('the status light indicates when the node is ineligible for scheduling', async function(assert) { 457 node = server.create('node', { 458 drain: false, 459 schedulingEligibility: 'ineligible', 460 status: 'ready', 461 }); 462 463 await ClientDetail.visit({ id: node.id }); 464 465 assert.equal( 466 ClientDetail.statusLight.objectAt(0).id, 467 'ineligible', 468 'Title status light is in the ineligible state' 469 ); 470 }); 471 472 test('when the node has a drain strategy with a positive deadline, the drain stategy section prints the duration', async function(assert) { 473 const deadline = 5400000000000; // 1.5 hours in nanoseconds 474 const forceDeadline = moment().add(1, 'd'); 475 476 node = server.create('node', { 477 drain: true, 478 schedulingEligibility: 'ineligible', 479 drainStrategy: { 480 Deadline: deadline, 481 ForceDeadline: forceDeadline.toISOString(), 482 IgnoreSystemJobs: false, 483 }, 484 }); 485 486 await ClientDetail.visit({ id: node.id }); 487 488 assert.ok( 489 ClientDetail.drainDetails.deadline.includes(forceDeadline.fromNow(true)), 490 'Deadline is shown in a human formatted way' 491 ); 492 493 assert.equal( 494 ClientDetail.drainDetails.deadlineTooltip, 495 forceDeadline.format("MMM DD, 'YY HH:mm:ss ZZ"), 496 'The tooltip for deadline shows the force deadline as an absolute date' 497 ); 498 499 assert.ok( 500 ClientDetail.drainDetails.drainSystemJobsText.endsWith('Yes'), 501 'Drain System Jobs state is shown' 502 ); 503 }); 504 505 test('when the node has a drain stategy with no deadline, the drain stategy section mentions that and omits the force deadline', async function(assert) { 506 const deadline = 0; 507 508 node = server.create('node', { 509 drain: true, 510 schedulingEligibility: 'ineligible', 511 drainStrategy: { 512 Deadline: deadline, 513 ForceDeadline: '0001-01-01T00:00:00Z', // null as a date 514 IgnoreSystemJobs: true, 515 }, 516 }); 517 518 await ClientDetail.visit({ id: node.id }); 519 520 assert.notOk(ClientDetail.drainDetails.durationIsShown, 'Duration is omitted'); 521 522 assert.ok( 523 ClientDetail.drainDetails.deadline.includes('No deadline'), 524 'The value for Deadline is "no deadline"' 525 ); 526 527 assert.ok( 528 ClientDetail.drainDetails.drainSystemJobsText.endsWith('No'), 529 'Drain System Jobs state is shown' 530 ); 531 }); 532 533 test('when the node has a drain stategy with a negative deadline, the drain strategy section shows the force badge', async function(assert) { 534 const deadline = -1; 535 536 node = server.create('node', { 537 drain: true, 538 schedulingEligibility: 'ineligible', 539 drainStrategy: { 540 Deadline: deadline, 541 ForceDeadline: '0001-01-01T00:00:00Z', // null as a date 542 IgnoreSystemJobs: false, 543 }, 544 }); 545 546 await ClientDetail.visit({ id: node.id }); 547 548 assert.ok( 549 ClientDetail.drainDetails.forceDrainText.endsWith('Yes'), 550 'Forced Drain is described' 551 ); 552 553 assert.ok(ClientDetail.drainDetails.duration.includes('--'), 'Duration is shown but unset'); 554 555 assert.ok(ClientDetail.drainDetails.deadline.includes('--'), 'Deadline is shown but unset'); 556 557 assert.ok( 558 ClientDetail.drainDetails.drainSystemJobsText.endsWith('Yes'), 559 'Drain System Jobs state is shown' 560 ); 561 }); 562 563 test('toggling node eligibility disables the toggle and sends the correct POST request', async function(assert) { 564 node = server.create('node', { 565 drain: false, 566 schedulingEligibility: 'eligible', 567 }); 568 569 server.pretender.post('/v1/node/:id/eligibility', () => [200, {}, ''], true); 570 571 await ClientDetail.visit({ id: node.id }); 572 assert.ok(ClientDetail.eligibilityToggle.isActive); 573 574 ClientDetail.eligibilityToggle.toggle(); 575 await waitUntil(() => server.pretender.handledRequests.findBy('method', 'POST')); 576 577 assert.ok(ClientDetail.eligibilityToggle.isDisabled); 578 server.pretender.resolve(server.pretender.requestReferences[0].request); 579 580 await settled(); 581 582 assert.notOk(ClientDetail.eligibilityToggle.isActive); 583 assert.notOk(ClientDetail.eligibilityToggle.isDisabled); 584 585 const request = server.pretender.handledRequests.findBy('method', 'POST'); 586 assert.equal(request.url, `/v1/node/${node.id}/eligibility`); 587 assert.deepEqual(JSON.parse(request.requestBody), { 588 NodeID: node.id, 589 Eligibility: 'ineligible', 590 }); 591 592 ClientDetail.eligibilityToggle.toggle(); 593 await waitUntil(() => server.pretender.handledRequests.filterBy('method', 'POST').length === 2); 594 server.pretender.resolve(server.pretender.requestReferences[0].request); 595 596 assert.ok(ClientDetail.eligibilityToggle.isActive); 597 const request2 = server.pretender.handledRequests.filterBy('method', 'POST')[1]; 598 599 assert.equal(request2.url, `/v1/node/${node.id}/eligibility`); 600 assert.deepEqual(JSON.parse(request2.requestBody), { 601 NodeID: node.id, 602 Eligibility: 'eligible', 603 }); 604 }); 605 606 test('starting a drain sends the correct POST request', async function(assert) { 607 let request; 608 609 node = server.create('node', { 610 drain: false, 611 schedulingEligibility: 'eligible', 612 }); 613 614 await ClientDetail.visit({ id: node.id }); 615 await ClientDetail.drainPopover.toggle(); 616 await ClientDetail.drainPopover.submit(); 617 618 request = server.pretender.handledRequests.filterBy('method', 'POST').pop(); 619 620 assert.equal(request.url, `/v1/node/${node.id}/drain`); 621 assert.deepEqual( 622 JSON.parse(request.requestBody), 623 { 624 NodeID: node.id, 625 DrainSpec: { 626 Deadline: 0, 627 IgnoreSystemJobs: false, 628 }, 629 }, 630 'Drain with default settings' 631 ); 632 633 await ClientDetail.drainPopover.toggle(); 634 await ClientDetail.drainPopover.deadlineToggle.toggle(); 635 await ClientDetail.drainPopover.submit(); 636 637 request = server.pretender.handledRequests.filterBy('method', 'POST').pop(); 638 639 assert.deepEqual( 640 JSON.parse(request.requestBody), 641 { 642 NodeID: node.id, 643 DrainSpec: { 644 Deadline: 60 * 60 * 1000 * 1000000, 645 IgnoreSystemJobs: false, 646 }, 647 }, 648 'Drain with deadline toggled' 649 ); 650 651 await ClientDetail.drainPopover.toggle(); 652 await ClientDetail.drainPopover.deadlineOptions.open(); 653 await ClientDetail.drainPopover.deadlineOptions.options[1].choose(); 654 await ClientDetail.drainPopover.submit(); 655 656 request = server.pretender.handledRequests.filterBy('method', 'POST').pop(); 657 658 assert.deepEqual( 659 JSON.parse(request.requestBody), 660 { 661 NodeID: node.id, 662 DrainSpec: { 663 Deadline: 4 * 60 * 60 * 1000 * 1000000, 664 IgnoreSystemJobs: false, 665 }, 666 }, 667 'Drain with non-default preset deadline set' 668 ); 669 670 await ClientDetail.drainPopover.toggle(); 671 await ClientDetail.drainPopover.deadlineOptions.open(); 672 const optionsCount = ClientDetail.drainPopover.deadlineOptions.options.length; 673 await ClientDetail.drainPopover.deadlineOptions.options.objectAt(optionsCount - 1).choose(); 674 await ClientDetail.drainPopover.setCustomDeadline('1h40m20s'); 675 await ClientDetail.drainPopover.submit(); 676 677 request = server.pretender.handledRequests.filterBy('method', 'POST').pop(); 678 679 assert.deepEqual( 680 JSON.parse(request.requestBody), 681 { 682 NodeID: node.id, 683 DrainSpec: { 684 Deadline: ((1 * 60 + 40) * 60 + 20) * 1000 * 1000000, 685 IgnoreSystemJobs: false, 686 }, 687 }, 688 'Drain with custom deadline set' 689 ); 690 691 await ClientDetail.drainPopover.toggle(); 692 await ClientDetail.drainPopover.deadlineToggle.toggle(); 693 await ClientDetail.drainPopover.forceDrainToggle.toggle(); 694 await ClientDetail.drainPopover.submit(); 695 696 request = server.pretender.handledRequests.filterBy('method', 'POST').pop(); 697 698 assert.deepEqual( 699 JSON.parse(request.requestBody), 700 { 701 NodeID: node.id, 702 DrainSpec: { 703 Deadline: -1, 704 IgnoreSystemJobs: false, 705 }, 706 }, 707 'Drain with force set' 708 ); 709 710 await ClientDetail.drainPopover.toggle(); 711 await ClientDetail.drainPopover.systemJobsToggle.toggle(); 712 await ClientDetail.drainPopover.submit(); 713 714 request = server.pretender.handledRequests.filterBy('method', 'POST').pop(); 715 716 assert.deepEqual( 717 JSON.parse(request.requestBody), 718 { 719 NodeID: node.id, 720 DrainSpec: { 721 Deadline: -1, 722 IgnoreSystemJobs: true, 723 }, 724 }, 725 'Drain system jobs unset' 726 ); 727 }); 728 729 test('the drain popover cancel button closes the popover', async function(assert) { 730 node = server.create('node', { 731 drain: false, 732 schedulingEligibility: 'eligible', 733 }); 734 735 await ClientDetail.visit({ id: node.id }); 736 assert.notOk(ClientDetail.drainPopover.isOpen); 737 738 await ClientDetail.drainPopover.toggle(); 739 assert.ok(ClientDetail.drainPopover.isOpen); 740 741 await ClientDetail.drainPopover.cancel(); 742 assert.notOk(ClientDetail.drainPopover.isOpen); 743 assert.equal(server.pretender.handledRequests.filterBy('method', 'POST'), 0); 744 }); 745 746 test('toggling eligibility is disabled while a drain is active', async function(assert) { 747 node = server.create('node', { 748 drain: true, 749 schedulingEligibility: 'ineligible', 750 }); 751 752 await ClientDetail.visit({ id: node.id }); 753 assert.ok(ClientDetail.eligibilityToggle.isDisabled); 754 }); 755 756 test('stopping a drain sends the correct POST request', async function(assert) { 757 node = server.create('node', { 758 drain: true, 759 schedulingEligibility: 'ineligible', 760 }); 761 762 await ClientDetail.visit({ id: node.id }); 763 assert.ok(ClientDetail.stopDrainIsPresent); 764 765 await ClientDetail.stopDrain.idle(); 766 await ClientDetail.stopDrain.confirm(); 767 768 const request = server.pretender.handledRequests.findBy('method', 'POST'); 769 assert.equal(request.url, `/v1/node/${node.id}/drain`); 770 assert.deepEqual(JSON.parse(request.requestBody), { 771 NodeID: node.id, 772 DrainSpec: null, 773 }); 774 }); 775 776 test('when a drain is active, the "drain" popover is labeled as the "update" popover', async function(assert) { 777 node = server.create('node', { 778 drain: true, 779 schedulingEligibility: 'ineligible', 780 }); 781 782 await ClientDetail.visit({ id: node.id }); 783 assert.equal(ClientDetail.drainPopover.label, 'Update Drain'); 784 }); 785 786 test('forcing a drain sends the correct POST request', async function(assert) { 787 node = server.create('node', { 788 drain: true, 789 schedulingEligibility: 'ineligible', 790 drainStrategy: { 791 Deadline: 0, 792 IgnoreSystemJobs: true, 793 }, 794 }); 795 796 await ClientDetail.visit({ id: node.id }); 797 await ClientDetail.drainDetails.force.idle(); 798 await ClientDetail.drainDetails.force.confirm(); 799 800 const request = server.pretender.handledRequests.findBy('method', 'POST'); 801 assert.equal(request.url, `/v1/node/${node.id}/drain`); 802 assert.deepEqual(JSON.parse(request.requestBody), { 803 NodeID: node.id, 804 DrainSpec: { 805 Deadline: -1, 806 IgnoreSystemJobs: true, 807 }, 808 }); 809 }); 810 811 test('when stopping a drain fails, an error is shown', async function(assert) { 812 node = server.create('node', { 813 drain: true, 814 schedulingEligibility: 'ineligible', 815 }); 816 817 server.pretender.post('/v1/node/:id/drain', () => [500, {}, '']); 818 819 await ClientDetail.visit({ id: node.id }); 820 await ClientDetail.stopDrain.idle(); 821 await ClientDetail.stopDrain.confirm(); 822 823 assert.ok(ClientDetail.stopDrainError.isPresent); 824 assert.ok(ClientDetail.stopDrainError.title.includes('Stop Drain Error')); 825 826 await ClientDetail.stopDrainError.dismiss(); 827 assert.notOk(ClientDetail.stopDrainError.isPresent); 828 }); 829 830 test('when starting a drain fails, an error message is shown', async function(assert) { 831 node = server.create('node', { 832 drain: false, 833 schedulingEligibility: 'eligible', 834 }); 835 836 server.pretender.post('/v1/node/:id/drain', () => [500, {}, '']); 837 838 await ClientDetail.visit({ id: node.id }); 839 await ClientDetail.drainPopover.toggle(); 840 await ClientDetail.drainPopover.submit(); 841 842 assert.ok(ClientDetail.drainError.isPresent); 843 assert.ok(ClientDetail.drainError.title.includes('Drain Error')); 844 845 await ClientDetail.drainError.dismiss(); 846 assert.notOk(ClientDetail.drainError.isPresent); 847 }); 848 849 test('when updating a drain fails, an error message is shown', async function(assert) { 850 node = server.create('node', { 851 drain: true, 852 schedulingEligibility: 'ineligible', 853 }); 854 855 server.pretender.post('/v1/node/:id/drain', () => [500, {}, '']); 856 857 await ClientDetail.visit({ id: node.id }); 858 await ClientDetail.drainPopover.toggle(); 859 await ClientDetail.drainPopover.submit(); 860 861 assert.ok(ClientDetail.drainError.isPresent); 862 assert.ok(ClientDetail.drainError.title.includes('Drain Error')); 863 864 await ClientDetail.drainError.dismiss(); 865 assert.notOk(ClientDetail.drainError.isPresent); 866 }); 867 868 test('when toggling eligibility fails, an error message is shown', async function(assert) { 869 node = server.create('node', { 870 drain: false, 871 schedulingEligibility: 'eligible', 872 }); 873 874 server.pretender.post('/v1/node/:id/eligibility', () => [500, {}, '']); 875 876 await ClientDetail.visit({ id: node.id }); 877 await ClientDetail.eligibilityToggle.toggle(); 878 879 assert.ok(ClientDetail.eligibilityError.isPresent); 880 assert.ok(ClientDetail.eligibilityError.title.includes('Eligibility Error')); 881 882 await ClientDetail.eligibilityError.dismiss(); 883 assert.notOk(ClientDetail.eligibilityError.isPresent); 884 }); 885 886 test('when navigating away from a client that has an error message to another client, the error is not shown', async function(assert) { 887 node = server.create('node', { 888 drain: false, 889 schedulingEligibility: 'eligible', 890 }); 891 892 const node2 = server.create('node'); 893 894 server.pretender.post('/v1/node/:id/eligibility', () => [500, {}, '']); 895 896 await ClientDetail.visit({ id: node.id }); 897 await ClientDetail.eligibilityToggle.toggle(); 898 899 assert.ok(ClientDetail.eligibilityError.isPresent); 900 assert.ok(ClientDetail.eligibilityError.title.includes('Eligibility Error')); 901 902 await ClientDetail.visit({ id: node2.id }); 903 904 assert.notOk(ClientDetail.eligibilityError.isPresent); 905 }); 906 907 test('toggling eligibility and node drain are disabled when the active ACL token does not permit node write', async function(assert) { 908 window.localStorage.nomadTokenSecret = clientToken.secretId; 909 910 await ClientDetail.visit({ id: node.id }); 911 assert.ok(ClientDetail.eligibilityToggle.isDisabled); 912 assert.ok(ClientDetail.drainPopover.isDisabled); 913 }); 914 915 test('the host volumes table lists all host volumes in alphabetical order by name', async function(assert) { 916 await ClientDetail.visit({ id: node.id }); 917 918 const sortedHostVolumes = Object.keys(node.hostVolumes) 919 .map(key => node.hostVolumes[key]) 920 .sortBy('Name'); 921 922 assert.ok(ClientDetail.hasHostVolumes); 923 assert.equal(ClientDetail.hostVolumes.length, Object.keys(node.hostVolumes).length); 924 925 ClientDetail.hostVolumes.forEach((volume, index) => { 926 assert.equal(volume.name, sortedHostVolumes[index].Name); 927 }); 928 }); 929 930 test('each host volume row contains information about the host volume', async function(assert) { 931 await ClientDetail.visit({ id: node.id }); 932 933 const sortedHostVolumes = Object.keys(node.hostVolumes) 934 .map(key => node.hostVolumes[key]) 935 .sortBy('Name'); 936 937 ClientDetail.hostVolumes[0].as(volume => { 938 const volumeRow = sortedHostVolumes[0]; 939 assert.equal(volume.name, volumeRow.Name); 940 assert.equal(volume.path, volumeRow.Path); 941 assert.equal(volume.permissions, volumeRow.ReadOnly ? 'Read' : 'Read/Write'); 942 }); 943 }); 944 945 test('the host volumes table is not shown if the client has no host volumes', async function(assert) { 946 node = server.create('node', 'noHostVolumes'); 947 948 await ClientDetail.visit({ id: node.id }); 949 950 assert.notOk(ClientDetail.hasHostVolumes); 951 }); 952 }); 953 954 module('Acceptance | client detail (multi-namespace)', function(hooks) { 955 setupApplicationTest(hooks); 956 setupMirage(hooks); 957 958 hooks.beforeEach(function() { 959 server.create('node', 'forceIPv4', { schedulingEligibility: 'eligible' }); 960 node = server.db.nodes[0]; 961 962 // Related models 963 server.create('namespace'); 964 server.create('namespace', { id: 'other-namespace' }); 965 966 server.create('agent'); 967 968 // Make a job for each namespace, but have both scheduled on the same node 969 server.create('job', { id: 'job-1', namespaceId: 'default', createAllocations: false }); 970 server.createList('allocation', 3, { nodeId: node.id, clientStatus: 'running' }); 971 972 server.create('job', { id: 'job-2', namespaceId: 'other-namespace', createAllocations: false }); 973 server.createList('allocation', 3, { 974 nodeId: node.id, 975 jobId: 'job-2', 976 clientStatus: 'running', 977 }); 978 }); 979 980 test('when the node has allocations on different namespaces, the associated jobs are fetched correctly', async function(assert) { 981 window.localStorage.nomadActiveNamespace = 'other-namespace'; 982 983 await ClientDetail.visit({ id: node.id }); 984 985 assert.equal( 986 ClientDetail.allocations.length, 987 server.db.allocations.length, 988 'All allocations are scheduled on this node' 989 ); 990 assert.ok( 991 server.pretender.handledRequests.findBy('url', '/v1/job/job-1'), 992 'Job One fetched correctly' 993 ); 994 assert.ok( 995 server.pretender.handledRequests.findBy('url', '/v1/job/job-2?namespace=other-namespace'), 996 'Job Two fetched correctly' 997 ); 998 }); 999 });