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