github.com/Ilhicas/nomad@v1.0.4-0.20210304152020-e86851182bc3/ui/tests/acceptance/exec-test.js (about) 1 import { module, skip, test } from 'qunit'; 2 import { currentURL, settled } from '@ember/test-helpers'; 3 import { setupApplicationTest } from 'ember-qunit'; 4 import { setupMirage } from 'ember-cli-mirage/test-support'; 5 import a11yAudit from 'nomad-ui/tests/helpers/a11y-audit'; 6 import Service from '@ember/service'; 7 import Exec from 'nomad-ui/tests/pages/exec'; 8 import KEYS from 'nomad-ui/utils/keys'; 9 10 module('Acceptance | exec', function(hooks) { 11 setupApplicationTest(hooks); 12 setupMirage(hooks); 13 14 hooks.beforeEach(async function() { 15 window.localStorage.clear(); 16 window.sessionStorage.clear(); 17 18 server.create('agent'); 19 server.create('node'); 20 21 this.job = server.create('job', { 22 groupsCount: 2, 23 groupTaskCount: 5, 24 createAllocations: false, 25 status: 'running', 26 }); 27 28 this.job.taskGroups.models.forEach(taskGroup => { 29 server.create('allocation', { 30 jobId: this.job.id, 31 taskGroup: taskGroup.name, 32 forceRunningClientStatus: true, 33 }); 34 }); 35 }); 36 37 test('it passes an accessibility audit', async function(assert) { 38 await Exec.visitJob({ job: this.job.id }); 39 await a11yAudit(assert); 40 }); 41 42 test('/exec/:job should show the region, namespace, and job name', async function(assert) { 43 server.create('namespace'); 44 let namespace = server.create('namespace'); 45 46 server.create('region', { id: 'global' }); 47 server.create('region', { id: 'region-2' }); 48 49 this.job = server.create('job', { 50 createAllocations: false, 51 namespaceId: namespace.id, 52 status: 'running', 53 }); 54 55 await Exec.visitJob({ job: this.job.id, namespace: namespace.id, region: 'region-2' }); 56 57 assert.equal(document.title, 'Exec - region-2 - Nomad'); 58 59 assert.equal(Exec.header.region.text, this.job.region); 60 assert.equal(Exec.header.namespace.text, this.job.namespace); 61 assert.equal(Exec.header.job, this.job.name); 62 63 assert.notOk(Exec.jobDead.isPresent); 64 }); 65 66 test('/exec/:job should not show region and namespace when there are none', async function(assert) { 67 await Exec.visitJob({ job: this.job.id }); 68 69 assert.ok(Exec.header.region.isHidden); 70 assert.ok(Exec.header.namespace.isHidden); 71 }); 72 73 test('/exec/:job should show the task groups collapsed by default and allow the tasks to be shown', async function(assert) { 74 const firstTaskGroup = this.job.taskGroups.models.sortBy('name')[0]; 75 await Exec.visitJob({ job: this.job.id }); 76 77 assert.equal(Exec.taskGroups.length, this.job.taskGroups.length); 78 79 assert.equal(Exec.taskGroups[0].name, firstTaskGroup.name); 80 assert.equal(Exec.taskGroups[0].tasks.length, 0); 81 assert.ok(Exec.taskGroups[0].chevron.isRight); 82 assert.notOk(Exec.taskGroups[0].isLoading); 83 84 await Exec.taskGroups[0].click(); 85 assert.equal(Exec.taskGroups[0].tasks.length, firstTaskGroup.tasks.length); 86 assert.notOk(Exec.taskGroups[0].tasks[0].isActive); 87 assert.ok(Exec.taskGroups[0].chevron.isDown); 88 89 await Exec.taskGroups[0].click(); 90 assert.equal(Exec.taskGroups[0].tasks.length, 0); 91 }); 92 93 test('/exec/:job should require selecting a task', async function(assert) { 94 await Exec.visitJob({ job: this.job.id }); 95 96 assert.equal( 97 window.execTerminal.buffer.active 98 .getLine(0) 99 .translateToString() 100 .trim(), 101 'Select a task to start your session.' 102 ); 103 }); 104 105 test('a task group with a pending allocation shows a loading spinner', async function(assert) { 106 let taskGroup = this.job.taskGroups.models.sortBy('name')[0]; 107 this.server.db.allocations.update({ taskGroup: taskGroup.name }, { clientStatus: 'pending' }); 108 109 await Exec.visitJob({ job: this.job.id }); 110 assert.ok(Exec.taskGroups[0].isLoading); 111 }); 112 113 test('a task group with no running task states or pending allocations should not be shown', async function(assert) { 114 let taskGroup = this.job.taskGroups.models.sortBy('name')[0]; 115 this.server.db.allocations.update({ taskGroup: taskGroup.name }, { clientStatus: 'failed' }); 116 117 await Exec.visitJob({ job: this.job.id }); 118 assert.notEqual(Exec.taskGroups[0].name, taskGroup.name); 119 }); 120 121 test('an inactive task should not be shown', async function(assert) { 122 let notRunningTaskGroup = this.job.taskGroups.models.sortBy('name')[0]; 123 this.server.db.allocations.update( 124 { taskGroup: notRunningTaskGroup.name }, 125 { clientStatus: 'failed' } 126 ); 127 128 let runningTaskGroup = this.job.taskGroups.models.sortBy('name')[1]; 129 runningTaskGroup.tasks.models.forEach((task, index) => { 130 if (index > 0) { 131 this.server.db.taskStates.update({ name: task.name }, { finishedAt: new Date() }); 132 } 133 }); 134 135 await Exec.visitJob({ job: this.job.id }); 136 await Exec.taskGroups[0].click(); 137 138 assert.equal(Exec.taskGroups[0].tasks.length, 1); 139 }); 140 141 test('a task that becomes active should appear', async function(assert) { 142 let notRunningTaskGroup = this.job.taskGroups.models.sortBy('name')[0]; 143 this.server.db.allocations.update( 144 { taskGroup: notRunningTaskGroup.name }, 145 { clientStatus: 'failed' } 146 ); 147 148 let runningTaskGroup = this.job.taskGroups.models.sortBy('name')[1]; 149 let changingTaskStateName; 150 runningTaskGroup.tasks.models.sortBy('name').forEach((task, index) => { 151 if (index > 0) { 152 this.server.db.taskStates.update({ name: task.name }, { finishedAt: new Date() }); 153 } 154 155 if (index === 1) { 156 changingTaskStateName = task.name; 157 } 158 }); 159 160 await Exec.visitJob({ job: this.job.id }); 161 await Exec.taskGroups[0].click(); 162 163 assert.equal(Exec.taskGroups[0].tasks.length, 1); 164 165 // Approximate new task arrival via polling by changing a finished task state to be not finished 166 this.owner 167 .lookup('service:store') 168 .peekAll('allocation') 169 .forEach(allocation => { 170 const changingTaskState = allocation.states.findBy('name', changingTaskStateName); 171 172 if (changingTaskState) { 173 changingTaskState.set('finishedAt', undefined); 174 } 175 }); 176 177 await settled(); 178 179 assert.equal(Exec.taskGroups[0].tasks.length, 2); 180 assert.equal(Exec.taskGroups[0].tasks[1].name, changingTaskStateName); 181 }); 182 183 test('a dead job has an inert window', async function(assert) { 184 this.job.status = 'dead'; 185 this.job.save(); 186 187 let taskGroup = this.job.taskGroups.models.sortBy('name')[0]; 188 let task = taskGroup.tasks.models.sortBy('name')[0]; 189 190 this.server.db.taskStates.update({ finishedAt: new Date() }); 191 192 await Exec.visitTask({ 193 job: this.job.id, 194 task_group: taskGroup.name, 195 task_name: task.name, 196 }); 197 198 assert.ok(Exec.jobDead.isPresent); 199 assert.equal( 200 Exec.jobDead.message, 201 `Job ${this.job.name} is dead and cannot host an exec session.` 202 ); 203 }); 204 205 test('when a job dies the exec window becomes inert', async function(assert) { 206 await Exec.visitJob({ job: this.job.id }); 207 208 // Approximate live-polling job death 209 this.owner 210 .lookup('service:store') 211 .peekAll('job') 212 .forEach(job => job.set('status', 'dead')); 213 214 await settled(); 215 216 assert.ok(Exec.jobDead.isPresent); 217 }); 218 219 test('visiting a path with a task group should open the group by default', async function(assert) { 220 let taskGroup = this.job.taskGroups.models.sortBy('name')[0]; 221 await Exec.visitTaskGroup({ job: this.job.id, task_group: taskGroup.name }); 222 223 assert.equal(Exec.taskGroups[0].tasks.length, taskGroup.tasks.length); 224 assert.ok(Exec.taskGroups[0].chevron.isDown); 225 226 let task = taskGroup.tasks.models.sortBy('name')[0]; 227 await Exec.visitTask({ job: this.job.id, task_group: taskGroup.name, task_name: task.name }); 228 229 assert.equal(Exec.taskGroups[0].tasks.length, taskGroup.tasks.length); 230 assert.ok(Exec.taskGroups[0].chevron.isDown); 231 }); 232 233 test('navigating to a task adds its name to the route, chooses an allocation, and assigns a default command', async function(assert) { 234 await Exec.visitJob({ job: this.job.id }); 235 await Exec.taskGroups[0].click(); 236 await Exec.taskGroups[0].tasks[0].click(); 237 238 let taskGroup = this.job.taskGroups.models.sortBy('name')[0]; 239 let task = taskGroup.tasks.models.sortBy('name')[0]; 240 241 let taskStates = this.server.db.taskStates.where({ 242 name: task.name, 243 }); 244 let allocationId = taskStates.find(ts => ts.allocationId).allocationId; 245 246 await settled(); 247 248 assert.equal(currentURL(), `/exec/${this.job.id}/${taskGroup.name}/${task.name}`); 249 assert.ok(Exec.taskGroups[0].tasks[0].isActive); 250 251 assert.equal( 252 window.execTerminal.buffer.active 253 .getLine(2) 254 .translateToString() 255 .trim(), 256 'Multiple instances of this task are running. The allocation below was selected by random draw.' 257 ); 258 259 assert.equal( 260 window.execTerminal.buffer.active 261 .getLine(4) 262 .translateToString() 263 .trim(), 264 'Customize your command, then hit ‘return’ to run.' 265 ); 266 267 assert.equal( 268 window.execTerminal.buffer.active 269 .getLine(6) 270 .translateToString() 271 .trim(), 272 `$ nomad alloc exec -i -t -task ${task.name} ${allocationId.split('-')[0]} /bin/bash` 273 ); 274 }); 275 276 test('an allocation can be specified', async function(assert) { 277 let taskGroup = this.job.taskGroups.models.sortBy('name')[0]; 278 let task = taskGroup.tasks.models.sortBy('name')[0]; 279 let allocations = this.server.db.allocations.where({ 280 jobId: this.job.id, 281 taskGroup: taskGroup.name, 282 }); 283 let allocation = allocations[allocations.length - 1]; 284 285 this.server.db.taskStates.update({ name: task.name }, { name: 'spaced name!' }); 286 287 task.name = 'spaced name!'; 288 task.save(); 289 290 await Exec.visitTask({ 291 job: this.job.id, 292 task_group: taskGroup.name, 293 task_name: task.name, 294 allocation: allocation.id.split('-')[0], 295 }); 296 297 await settled(); 298 299 assert.equal( 300 window.execTerminal.buffer.active 301 .getLine(4) 302 .translateToString() 303 .trim(), 304 `$ nomad alloc exec -i -t -task spaced\\ name\\! ${allocation.id.split('-')[0]} /bin/bash` 305 ); 306 }); 307 308 test('running the command opens the socket for reading/writing and detects it closing', async function(assert) { 309 let mockSocket = new MockSocket(); 310 let mockSockets = Service.extend({ 311 getTaskStateSocket(taskState, command) { 312 assert.equal(taskState.name, task.name); 313 assert.equal(taskState.allocation.id, allocation.id); 314 315 assert.equal(command, '/bin/bash'); 316 317 assert.step('Socket built'); 318 319 return mockSocket; 320 }, 321 }); 322 323 this.owner.register('service:sockets', mockSockets); 324 325 let taskGroup = this.job.taskGroups.models.sortBy('name')[0]; 326 let task = taskGroup.tasks.models.sortBy('name')[0]; 327 let allocations = this.server.db.allocations.where({ 328 jobId: this.job.id, 329 taskGroup: taskGroup.name, 330 }); 331 let allocation = allocations[allocations.length - 1]; 332 333 await Exec.visitTask({ 334 job: this.job.id, 335 task_group: taskGroup.name, 336 task_name: task.name, 337 allocation: allocation.id.split('-')[0], 338 }); 339 340 await settled(); 341 342 await Exec.terminal.pressEnter(); 343 await settled(); 344 mockSocket.onopen(); 345 346 assert.verifySteps(['Socket built']); 347 348 mockSocket.onmessage({ 349 data: '{"stdout":{"data":"c2gtMy4yIPCfpbMk"}}', 350 }); 351 352 await settled(); 353 354 assert.equal( 355 window.execTerminal.buffer.active 356 .getLine(5) 357 .translateToString() 358 .trim(), 359 'sh-3.2 🥳$' 360 ); 361 362 await Exec.terminal.pressEnter(); 363 await settled(); 364 365 assert.deepEqual(mockSocket.sent, [ 366 '{"version":1,"auth_token":""}', 367 `{"tty_size":{"width":${window.execTerminal.cols},"height":${window.execTerminal.rows}}}`, 368 '{"stdin":{"data":"DQ=="}}', 369 ]); 370 371 await mockSocket.onclose(); 372 await settled(); 373 374 assert.equal( 375 window.execTerminal.buffer.active 376 .getLine(6) 377 .translateToString() 378 .trim(), 379 'The connection has closed.' 380 ); 381 }); 382 383 test('the opening message includes the token if it exists', async function(assert) { 384 const { secretId } = server.create('token'); 385 window.localStorage.nomadTokenSecret = secretId; 386 387 let mockSocket = new MockSocket(); 388 let mockSockets = Service.extend({ 389 getTaskStateSocket() { 390 return mockSocket; 391 }, 392 }); 393 394 this.owner.register('service:sockets', mockSockets); 395 396 let taskGroup = this.job.taskGroups.models[0]; 397 let task = taskGroup.tasks.models[0]; 398 let allocations = this.server.db.allocations.where({ 399 jobId: this.job.id, 400 taskGroup: taskGroup.name, 401 }); 402 let allocation = allocations[allocations.length - 1]; 403 404 await Exec.visitTask({ 405 job: this.job.id, 406 task_group: taskGroup.name, 407 task_name: task.name, 408 allocation: allocation.id.split('-')[0], 409 }); 410 411 await Exec.terminal.pressEnter(); 412 await settled(); 413 mockSocket.onopen(); 414 415 await Exec.terminal.pressEnter(); 416 await settled(); 417 418 assert.equal(mockSocket.sent[0], `{"version":1,"auth_token":"${secretId}"}`); 419 }); 420 421 test('only one socket is opened after switching between tasks', async function(assert) { 422 let mockSockets = Service.extend({ 423 getTaskStateSocket() { 424 assert.step('Socket built'); 425 return new MockSocket(); 426 }, 427 }); 428 429 this.owner.register('service:sockets', mockSockets); 430 431 await Exec.visitJob({ 432 job: this.job.id, 433 }); 434 435 await settled(); 436 437 await Exec.taskGroups[0].click(); 438 await Exec.taskGroups[0].tasks[0].click(); 439 440 await Exec.taskGroups[1].click(); 441 await Exec.taskGroups[1].tasks[0].click(); 442 443 await Exec.terminal.pressEnter(); 444 445 assert.verifySteps(['Socket built']); 446 }); 447 448 test('the command can be customised', async function(assert) { 449 let mockSockets = Service.extend({ 450 getTaskStateSocket(taskState, command) { 451 assert.equal(command, '/sh'); 452 window.localStorage.getItem('nomadExecCommand', JSON.stringify('/sh')); 453 454 assert.step('Socket built'); 455 456 return new MockSocket(); 457 }, 458 }); 459 460 this.owner.register('service:sockets', mockSockets); 461 462 await Exec.visitJob({ job: this.job.id }); 463 await Exec.taskGroups[0].click(); 464 await Exec.taskGroups[0].tasks[0].click(); 465 466 let taskGroup = this.job.taskGroups.models.sortBy('name')[0]; 467 let task = taskGroup.tasks.models.sortBy('name')[0]; 468 let allocation = this.server.db.allocations.findBy({ 469 jobId: this.job.id, 470 taskGroup: taskGroup.name, 471 }); 472 473 await settled(); 474 475 // Delete /bash 476 await window.execTerminal.simulateCommandDataEvent(KEYS.DELETE); 477 await window.execTerminal.simulateCommandDataEvent(KEYS.DELETE); 478 await window.execTerminal.simulateCommandDataEvent(KEYS.DELETE); 479 await window.execTerminal.simulateCommandDataEvent(KEYS.DELETE); 480 await window.execTerminal.simulateCommandDataEvent(KEYS.DELETE); 481 482 // Delete /bin and try to go beyond 483 await window.execTerminal.simulateCommandDataEvent(KEYS.DELETE); 484 await window.execTerminal.simulateCommandDataEvent(KEYS.DELETE); 485 await window.execTerminal.simulateCommandDataEvent(KEYS.DELETE); 486 await window.execTerminal.simulateCommandDataEvent(KEYS.DELETE); 487 await window.execTerminal.simulateCommandDataEvent(KEYS.DELETE); 488 await window.execTerminal.simulateCommandDataEvent(KEYS.DELETE); 489 await window.execTerminal.simulateCommandDataEvent(KEYS.DELETE); 490 491 await settled(); 492 493 assert.equal( 494 window.execTerminal.buffer.active 495 .getLine(6) 496 .translateToString() 497 .trim(), 498 `$ nomad alloc exec -i -t -task ${task.name} ${allocation.id.split('-')[0]}` 499 ); 500 501 await window.execTerminal.simulateCommandDataEvent('/sh'); 502 503 await Exec.terminal.pressEnter(); 504 await settled(); 505 506 assert.verifySteps(['Socket built']); 507 }); 508 509 test('a persisted customised command is recalled', async function(assert) { 510 window.localStorage.setItem('nomadExecCommand', JSON.stringify('/bin/sh')); 511 512 let taskGroup = this.job.taskGroups.models[0]; 513 let task = taskGroup.tasks.models[0]; 514 let allocations = this.server.db.allocations.where({ 515 jobId: this.job.id, 516 taskGroup: taskGroup.name, 517 }); 518 let allocation = allocations[allocations.length - 1]; 519 520 await Exec.visitTask({ 521 job: this.job.id, 522 task_group: taskGroup.name, 523 task_name: task.name, 524 allocation: allocation.id.split('-')[0], 525 }); 526 527 await settled(); 528 529 assert.equal( 530 window.execTerminal.buffer.active 531 .getLine(4) 532 .translateToString() 533 .trim(), 534 `$ nomad alloc exec -i -t -task ${task.name} ${allocation.id.split('-')[0]} /bin/sh` 535 ); 536 }); 537 538 skip('when a task state finishes submitting a command displays an error', async function(assert) { 539 let taskGroup = this.job.taskGroups.models.sortBy('name')[0]; 540 let task = taskGroup.tasks.models.sortBy('name')[0]; 541 542 await Exec.visitTask({ 543 job: this.job.id, 544 task_group: taskGroup.name, 545 task_name: task.name, 546 }); 547 548 // Approximate allocation failure via polling 549 this.owner 550 .lookup('service:store') 551 .peekAll('allocation') 552 .forEach(allocation => allocation.set('clientStatus', 'failed')); 553 554 await Exec.terminal.pressEnter(); 555 await settled(); 556 557 assert.equal( 558 window.execTerminal.buffer.active 559 .getLine(7) 560 .translateToString() 561 .trim(), 562 `Failed to open a socket because task ${task.name} is not active.` 563 ); 564 }); 565 }); 566 567 class MockSocket { 568 constructor() { 569 this.sent = []; 570 } 571 572 send(message) { 573 this.sent.push(message); 574 } 575 }