606 lines
18 KiB
JavaScript
606 lines
18 KiB
JavaScript
/**
|
||
* Copyright (c) HashiCorp, Inc.
|
||
* SPDX-License-Identifier: MPL-2.0
|
||
*/
|
||
|
||
/* eslint-disable qunit/require-expect */
|
||
import { module, skip, test } from 'qunit';
|
||
import { currentURL, settled } from '@ember/test-helpers';
|
||
import { setupApplicationTest } from 'ember-qunit';
|
||
import { setupMirage } from 'ember-cli-mirage/test-support';
|
||
import a11yAudit from 'nomad-ui/tests/helpers/a11y-audit';
|
||
import Service from '@ember/service';
|
||
import Exec from 'nomad-ui/tests/pages/exec';
|
||
import KEYS from 'nomad-ui/utils/keys';
|
||
import percySnapshot from '@percy/ember';
|
||
import faker from 'nomad-ui/mirage/faker';
|
||
|
||
module('Acceptance | exec', function (hooks) {
|
||
setupApplicationTest(hooks);
|
||
setupMirage(hooks);
|
||
|
||
hooks.beforeEach(async function () {
|
||
window.localStorage.clear();
|
||
window.sessionStorage.clear();
|
||
|
||
faker.seed(1);
|
||
|
||
server.create('agent');
|
||
server.create('node');
|
||
|
||
this.job = server.create('job', {
|
||
groupsCount: 2,
|
||
groupTaskCount: 5,
|
||
createAllocations: false,
|
||
status: 'running',
|
||
});
|
||
|
||
this.job.taskGroups.models.forEach((taskGroup) => {
|
||
const alloc = server.create('allocation', {
|
||
jobId: this.job.id,
|
||
taskGroup: taskGroup.name,
|
||
forceRunningClientStatus: true,
|
||
});
|
||
server.db.taskStates.update(
|
||
{ allocationId: alloc.id },
|
||
{ state: 'running' }
|
||
);
|
||
});
|
||
});
|
||
|
||
test('it passes an accessibility audit', async function (assert) {
|
||
await Exec.visitJob({ job: this.job.id });
|
||
await a11yAudit(assert);
|
||
});
|
||
|
||
test('/exec/:job should show the region, namespace, and job name', async function (assert) {
|
||
server.create('namespace');
|
||
let namespace = server.create('namespace');
|
||
|
||
server.create('region', { id: 'global' });
|
||
server.create('region', { id: 'region-2' });
|
||
|
||
this.job = server.create('job', {
|
||
createAllocations: false,
|
||
namespaceId: namespace.id,
|
||
status: 'running',
|
||
});
|
||
|
||
await Exec.visitJob({
|
||
job: this.job.id,
|
||
namespace: namespace.id,
|
||
region: 'region-2',
|
||
});
|
||
|
||
assert.equal(document.title, 'Exec - region-2 - Mirage - Nomad');
|
||
|
||
assert.equal(Exec.header.region.text, this.job.region);
|
||
assert.equal(Exec.header.namespace.text, this.job.namespace);
|
||
assert.equal(Exec.header.job, this.job.name);
|
||
|
||
assert.notOk(Exec.jobDead.isPresent);
|
||
});
|
||
|
||
test('/exec/:job should not show region and namespace when there are none', async function (assert) {
|
||
await Exec.visitJob({ job: this.job.id });
|
||
|
||
assert.ok(Exec.header.region.isHidden);
|
||
assert.ok(Exec.header.namespace.isHidden);
|
||
});
|
||
|
||
test('/exec/:job should show the task groups collapsed by default and allow the tasks to be shown', async function (assert) {
|
||
const firstTaskGroup = this.job.taskGroups.models.sortBy('name')[0];
|
||
await Exec.visitJob({ job: this.job.id });
|
||
|
||
assert.equal(Exec.taskGroups.length, this.job.taskGroups.length);
|
||
|
||
assert.equal(Exec.taskGroups[0].name, firstTaskGroup.name);
|
||
assert.equal(Exec.taskGroups[0].tasks.length, 0);
|
||
assert.ok(Exec.taskGroups[0].chevron.isRight);
|
||
assert.notOk(Exec.taskGroups[0].isLoading);
|
||
|
||
await Exec.taskGroups[0].click();
|
||
assert.equal(Exec.taskGroups[0].tasks.length, firstTaskGroup.tasks.length);
|
||
assert.notOk(Exec.taskGroups[0].tasks[0].isActive);
|
||
assert.ok(Exec.taskGroups[0].chevron.isDown);
|
||
|
||
await percySnapshot(assert);
|
||
|
||
await Exec.taskGroups[0].click();
|
||
assert.equal(Exec.taskGroups[0].tasks.length, 0);
|
||
});
|
||
|
||
test('/exec/:job should require selecting a task', async function (assert) {
|
||
await Exec.visitJob({ job: this.job.id });
|
||
|
||
assert.equal(
|
||
window.execTerminal.buffer.active.getLine(0).translateToString().trim(),
|
||
'Select a task to start your session.'
|
||
);
|
||
});
|
||
|
||
test('a task group with a pending allocation shows a loading spinner', async function (assert) {
|
||
let taskGroup = this.job.taskGroups.models.sortBy('name')[0];
|
||
this.server.db.allocations.update(
|
||
{ taskGroup: taskGroup.name },
|
||
{ clientStatus: 'pending' }
|
||
);
|
||
|
||
await Exec.visitJob({ job: this.job.id });
|
||
assert.ok(Exec.taskGroups[0].isLoading);
|
||
});
|
||
|
||
test('a task group with no running task states or pending allocations should not be shown', async function (assert) {
|
||
let taskGroup = this.job.taskGroups.models.sortBy('name')[0];
|
||
this.server.db.allocations.update(
|
||
{ taskGroup: taskGroup.name },
|
||
{ clientStatus: 'failed' }
|
||
);
|
||
|
||
await Exec.visitJob({ job: this.job.id });
|
||
assert.notEqual(Exec.taskGroups[0].name, taskGroup.name);
|
||
});
|
||
|
||
test('an inactive task should not be shown', async function (assert) {
|
||
let notRunningTaskGroup = this.job.taskGroups.models.sortBy('name')[0];
|
||
this.server.db.allocations.update(
|
||
{ taskGroup: notRunningTaskGroup.name },
|
||
{ clientStatus: 'failed' }
|
||
);
|
||
|
||
let runningTaskGroup = this.job.taskGroups.models.sortBy('name')[1];
|
||
runningTaskGroup.tasks.models.forEach((task, index) => {
|
||
let state = 'running';
|
||
if (index > 0) {
|
||
state = 'dead';
|
||
}
|
||
this.server.db.taskStates.update({ name: task.name }, { state });
|
||
});
|
||
|
||
await Exec.visitJob({ job: this.job.id });
|
||
await Exec.taskGroups[0].click();
|
||
|
||
assert.equal(Exec.taskGroups[0].tasks.length, 1);
|
||
});
|
||
|
||
test('a task that becomes active should appear', async function (assert) {
|
||
let notRunningTaskGroup = this.job.taskGroups.models.sortBy('name')[0];
|
||
this.server.db.allocations.update(
|
||
{ taskGroup: notRunningTaskGroup.name },
|
||
{ clientStatus: 'failed' }
|
||
);
|
||
|
||
let runningTaskGroup = this.job.taskGroups.models.sortBy('name')[1];
|
||
let changingTaskStateName;
|
||
runningTaskGroup.tasks.models.sortBy('name').forEach((task, index) => {
|
||
let state = 'running';
|
||
if (index > 0) {
|
||
state = 'dead';
|
||
}
|
||
this.server.db.taskStates.update({ name: task.name }, { state });
|
||
|
||
if (index === 1) {
|
||
changingTaskStateName = task.name;
|
||
}
|
||
});
|
||
|
||
await Exec.visitJob({ job: this.job.id });
|
||
await Exec.taskGroups[0].click();
|
||
|
||
assert.equal(Exec.taskGroups[0].tasks.length, 1);
|
||
|
||
// Approximate new task arrival via polling by changing a finished task state to be not finished
|
||
this.owner
|
||
.lookup('service:store')
|
||
.peekAll('allocation')
|
||
.forEach((allocation) => {
|
||
const changingTaskState = allocation.states.findBy(
|
||
'name',
|
||
changingTaskStateName
|
||
);
|
||
|
||
if (changingTaskState) {
|
||
changingTaskState.set('state', 'running');
|
||
}
|
||
});
|
||
|
||
await settled();
|
||
|
||
assert.equal(Exec.taskGroups[0].tasks.length, 2);
|
||
assert.equal(Exec.taskGroups[0].tasks[1].name, changingTaskStateName);
|
||
});
|
||
|
||
test('a dead job has an inert window', async function (assert) {
|
||
this.job.status = 'dead';
|
||
this.job.save();
|
||
|
||
let taskGroup = this.job.taskGroups.models.sortBy('name')[0];
|
||
let task = taskGroup.tasks.models.sortBy('name')[0];
|
||
|
||
this.server.db.taskStates.update({ finishedAt: new Date() });
|
||
|
||
await Exec.visitTask({
|
||
job: this.job.id,
|
||
task_group: taskGroup.name,
|
||
task_name: task.name,
|
||
});
|
||
|
||
assert.ok(Exec.jobDead.isPresent);
|
||
assert.equal(
|
||
Exec.jobDead.message,
|
||
`Job ${this.job.name} is dead and cannot host an exec session.`
|
||
);
|
||
});
|
||
|
||
test('when a job dies the exec window becomes inert', async function (assert) {
|
||
await Exec.visitJob({ job: this.job.id });
|
||
|
||
// Approximate live-polling job death
|
||
this.owner
|
||
.lookup('service:store')
|
||
.peekAll('job')
|
||
.forEach((job) => job.set('status', 'dead'));
|
||
|
||
await settled();
|
||
|
||
assert.ok(Exec.jobDead.isPresent);
|
||
});
|
||
|
||
test('visiting a path with a task group should open the group by default', async function (assert) {
|
||
let taskGroup = this.job.taskGroups.models.sortBy('name')[0];
|
||
await Exec.visitTaskGroup({ job: this.job.id, task_group: taskGroup.name });
|
||
|
||
assert.equal(Exec.taskGroups[0].tasks.length, taskGroup.tasks.length);
|
||
assert.ok(Exec.taskGroups[0].chevron.isDown);
|
||
|
||
let task = taskGroup.tasks.models.sortBy('name')[0];
|
||
await Exec.visitTask({
|
||
job: this.job.id,
|
||
task_group: taskGroup.name,
|
||
task_name: task.name,
|
||
});
|
||
|
||
assert.equal(Exec.taskGroups[0].tasks.length, taskGroup.tasks.length);
|
||
assert.ok(Exec.taskGroups[0].chevron.isDown);
|
||
});
|
||
|
||
test('navigating to a task adds its name to the route, chooses an allocation, and assigns a default command', async function (assert) {
|
||
await Exec.visitJob({ job: this.job.id });
|
||
await Exec.taskGroups[0].click();
|
||
await Exec.taskGroups[0].tasks[0].click();
|
||
|
||
let taskGroup = this.job.taskGroups.models.sortBy('name')[0];
|
||
let task = taskGroup.tasks.models.sortBy('name')[0];
|
||
|
||
let taskStates = this.server.db.taskStates.where({
|
||
name: task.name,
|
||
});
|
||
let allocationId = taskStates.find((ts) => ts.allocationId).allocationId;
|
||
|
||
await settled();
|
||
|
||
assert.equal(
|
||
currentURL(),
|
||
`/exec/${this.job.id}/${taskGroup.name}/${task.name}`
|
||
);
|
||
assert.ok(Exec.taskGroups[0].tasks[0].isActive);
|
||
|
||
assert.equal(
|
||
window.execTerminal.buffer.active.getLine(2).translateToString().trim(),
|
||
'Multiple instances of this task are running. The allocation below was selected by random draw.'
|
||
);
|
||
|
||
assert.equal(
|
||
window.execTerminal.buffer.active.getLine(4).translateToString().trim(),
|
||
'Customize your command, then hit ‘return’ to run.'
|
||
);
|
||
|
||
assert.equal(
|
||
window.execTerminal.buffer.active.getLine(6).translateToString().trim(),
|
||
`$ nomad alloc exec -i -t -task ${task.name} ${
|
||
allocationId.split('-')[0]
|
||
} /bin/bash`
|
||
);
|
||
|
||
const terminalTextRendered = assert.async();
|
||
setTimeout(async () => {
|
||
await percySnapshot(assert);
|
||
terminalTextRendered();
|
||
}, 1000);
|
||
});
|
||
|
||
test('an allocation can be specified', async function (assert) {
|
||
let taskGroup = this.job.taskGroups.models.sortBy('name')[0];
|
||
let task = taskGroup.tasks.models.sortBy('name')[0];
|
||
let allocations = this.server.db.allocations.where({
|
||
jobId: this.job.id,
|
||
taskGroup: taskGroup.name,
|
||
});
|
||
let allocation = allocations[allocations.length - 1];
|
||
|
||
this.server.db.taskStates.update(
|
||
{ name: task.name },
|
||
{ name: 'spaced name!' }
|
||
);
|
||
|
||
task.name = 'spaced name!';
|
||
task.save();
|
||
|
||
await Exec.visitTask({
|
||
job: this.job.id,
|
||
task_group: taskGroup.name,
|
||
task_name: task.name,
|
||
allocation: allocation.id.split('-')[0],
|
||
});
|
||
|
||
await settled();
|
||
|
||
assert.equal(
|
||
window.execTerminal.buffer.active.getLine(4).translateToString().trim(),
|
||
`$ nomad alloc exec -i -t -task spaced\\ name\\! ${
|
||
allocation.id.split('-')[0]
|
||
} /bin/bash`
|
||
);
|
||
});
|
||
|
||
test('running the command opens the socket for reading/writing and detects it closing', async function (assert) {
|
||
let mockSocket = new MockSocket();
|
||
let mockSockets = Service.extend({
|
||
getTaskStateSocket(taskState, command) {
|
||
assert.equal(taskState.name, task.name);
|
||
assert.equal(taskState.allocation.id, allocation.id);
|
||
|
||
assert.equal(command, '/bin/bash');
|
||
|
||
assert.step('Socket built');
|
||
|
||
return mockSocket;
|
||
},
|
||
});
|
||
|
||
this.owner.register('service:sockets', mockSockets);
|
||
|
||
let taskGroup = this.job.taskGroups.models.sortBy('name')[0];
|
||
let task = taskGroup.tasks.models.sortBy('name')[0];
|
||
let allocations = this.server.db.allocations.where({
|
||
jobId: this.job.id,
|
||
taskGroup: taskGroup.name,
|
||
});
|
||
let allocation = allocations[allocations.length - 1];
|
||
|
||
await Exec.visitTask({
|
||
job: this.job.id,
|
||
task_group: taskGroup.name,
|
||
task_name: task.name,
|
||
allocation: allocation.id.split('-')[0],
|
||
});
|
||
|
||
await settled();
|
||
|
||
await Exec.terminal.pressEnter();
|
||
await settled();
|
||
mockSocket.onopen();
|
||
|
||
assert.verifySteps(['Socket built']);
|
||
|
||
mockSocket.onmessage({
|
||
data: '{"stdout":{"data":"c2gtMy4yIPCfpbMk"}}',
|
||
});
|
||
|
||
await settled();
|
||
|
||
assert.equal(
|
||
window.execTerminal.buffer.active.getLine(5).translateToString().trim(),
|
||
'sh-3.2 🥳$'
|
||
);
|
||
|
||
await Exec.terminal.pressEnter();
|
||
await settled();
|
||
|
||
assert.deepEqual(mockSocket.sent, [
|
||
'{"version":1,"auth_token":""}',
|
||
`{"tty_size":{"width":${window.execTerminal.cols},"height":${window.execTerminal.rows}}}`,
|
||
'{"stdin":{"data":"DQ=="}}',
|
||
]);
|
||
|
||
await mockSocket.onclose();
|
||
await settled();
|
||
|
||
assert.equal(
|
||
window.execTerminal.buffer.active.getLine(6).translateToString().trim(),
|
||
'The connection has closed.'
|
||
);
|
||
});
|
||
|
||
test('the opening message includes the token if it exists', async function (assert) {
|
||
const { secretId } = server.create('token');
|
||
window.localStorage.nomadTokenSecret = secretId;
|
||
|
||
let mockSocket = new MockSocket();
|
||
let mockSockets = Service.extend({
|
||
getTaskStateSocket() {
|
||
return mockSocket;
|
||
},
|
||
});
|
||
|
||
this.owner.register('service:sockets', mockSockets);
|
||
|
||
let taskGroup = this.job.taskGroups.models[0];
|
||
let task = taskGroup.tasks.models[0];
|
||
let allocations = this.server.db.allocations.where({
|
||
jobId: this.job.id,
|
||
taskGroup: taskGroup.name,
|
||
});
|
||
let allocation = allocations[allocations.length - 1];
|
||
|
||
await Exec.visitTask({
|
||
job: this.job.id,
|
||
task_group: taskGroup.name,
|
||
task_name: task.name,
|
||
allocation: allocation.id.split('-')[0],
|
||
});
|
||
|
||
await Exec.terminal.pressEnter();
|
||
await settled();
|
||
mockSocket.onopen();
|
||
|
||
await Exec.terminal.pressEnter();
|
||
await settled();
|
||
|
||
assert.equal(
|
||
mockSocket.sent[0],
|
||
`{"version":1,"auth_token":"${secretId}"}`
|
||
);
|
||
});
|
||
|
||
test('only one socket is opened after switching between tasks', async function (assert) {
|
||
let mockSockets = Service.extend({
|
||
getTaskStateSocket() {
|
||
assert.step('Socket built');
|
||
return new MockSocket();
|
||
},
|
||
});
|
||
|
||
this.owner.register('service:sockets', mockSockets);
|
||
|
||
await Exec.visitJob({
|
||
job: this.job.id,
|
||
});
|
||
|
||
await settled();
|
||
|
||
await Exec.taskGroups[0].click();
|
||
await Exec.taskGroups[0].tasks[0].click();
|
||
|
||
await Exec.taskGroups[1].click();
|
||
await Exec.taskGroups[1].tasks[0].click();
|
||
|
||
await Exec.terminal.pressEnter();
|
||
|
||
assert.verifySteps(['Socket built']);
|
||
});
|
||
|
||
test('the command can be customised', async function (assert) {
|
||
let mockSockets = Service.extend({
|
||
getTaskStateSocket(taskState, command) {
|
||
assert.equal(command, '/sh');
|
||
window.localStorage.getItem('nomadExecCommand', JSON.stringify('/sh'));
|
||
|
||
assert.step('Socket built');
|
||
|
||
return new MockSocket();
|
||
},
|
||
});
|
||
|
||
this.owner.register('service:sockets', mockSockets);
|
||
|
||
await Exec.visitJob({ job: this.job.id });
|
||
await Exec.taskGroups[0].click();
|
||
await Exec.taskGroups[0].tasks[0].click();
|
||
|
||
let taskGroup = this.job.taskGroups.models.sortBy('name')[0];
|
||
let task = taskGroup.tasks.models.sortBy('name')[0];
|
||
let allocation = this.server.db.allocations.findBy({
|
||
jobId: this.job.id,
|
||
taskGroup: taskGroup.name,
|
||
});
|
||
|
||
await settled();
|
||
|
||
// Delete /bash
|
||
await window.execTerminal.simulateCommandDataEvent(KEYS.DELETE);
|
||
await window.execTerminal.simulateCommandDataEvent(KEYS.DELETE);
|
||
await window.execTerminal.simulateCommandDataEvent(KEYS.DELETE);
|
||
await window.execTerminal.simulateCommandDataEvent(KEYS.DELETE);
|
||
await window.execTerminal.simulateCommandDataEvent(KEYS.DELETE);
|
||
|
||
// Delete /bin and try to go beyond
|
||
await window.execTerminal.simulateCommandDataEvent(KEYS.DELETE);
|
||
await window.execTerminal.simulateCommandDataEvent(KEYS.DELETE);
|
||
await window.execTerminal.simulateCommandDataEvent(KEYS.DELETE);
|
||
await window.execTerminal.simulateCommandDataEvent(KEYS.DELETE);
|
||
await window.execTerminal.simulateCommandDataEvent(KEYS.DELETE);
|
||
await window.execTerminal.simulateCommandDataEvent(KEYS.DELETE);
|
||
await window.execTerminal.simulateCommandDataEvent(KEYS.DELETE);
|
||
|
||
await settled();
|
||
|
||
assert.equal(
|
||
window.execTerminal.buffer.active.getLine(6).translateToString().trim(),
|
||
`$ nomad alloc exec -i -t -task ${task.name} ${
|
||
allocation.id.split('-')[0]
|
||
}`
|
||
);
|
||
|
||
await window.execTerminal.simulateCommandDataEvent('/sh');
|
||
|
||
await Exec.terminal.pressEnter();
|
||
await settled();
|
||
|
||
assert.verifySteps(['Socket built']);
|
||
});
|
||
|
||
test('a persisted customised command is recalled', async function (assert) {
|
||
window.localStorage.setItem('nomadExecCommand', JSON.stringify('/bin/sh'));
|
||
|
||
let taskGroup = this.job.taskGroups.models[0];
|
||
let task = taskGroup.tasks.models[0];
|
||
let allocations = this.server.db.allocations.where({
|
||
jobId: this.job.id,
|
||
taskGroup: taskGroup.name,
|
||
});
|
||
let allocation = allocations[allocations.length - 1];
|
||
|
||
await Exec.visitTask({
|
||
job: this.job.id,
|
||
task_group: taskGroup.name,
|
||
task_name: task.name,
|
||
allocation: allocation.id.split('-')[0],
|
||
});
|
||
|
||
await settled();
|
||
|
||
assert.equal(
|
||
window.execTerminal.buffer.active.getLine(4).translateToString().trim(),
|
||
`$ nomad alloc exec -i -t -task ${task.name} ${
|
||
allocation.id.split('-')[0]
|
||
} /bin/sh`
|
||
);
|
||
});
|
||
|
||
skip('when a task state finishes submitting a command displays an error', async function (assert) {
|
||
let taskGroup = this.job.taskGroups.models.sortBy('name')[0];
|
||
let task = taskGroup.tasks.models.sortBy('name')[0];
|
||
|
||
await Exec.visitTask({
|
||
job: this.job.id,
|
||
task_group: taskGroup.name,
|
||
task_name: task.name,
|
||
});
|
||
|
||
// Approximate allocation failure via polling
|
||
this.owner
|
||
.lookup('service:store')
|
||
.peekAll('allocation')
|
||
.forEach((allocation) => allocation.set('clientStatus', 'failed'));
|
||
|
||
await Exec.terminal.pressEnter();
|
||
await settled();
|
||
|
||
assert.equal(
|
||
window.execTerminal.buffer.active.getLine(7).translateToString().trim(),
|
||
`Failed to open a socket because task ${task.name} is not active.`
|
||
);
|
||
});
|
||
});
|
||
|
||
class MockSocket {
|
||
constructor() {
|
||
this.sent = [];
|
||
}
|
||
|
||
send(message) {
|
||
this.sent.push(message);
|
||
}
|
||
}
|