UI: add exec handling for dead jobs/task states (#7637)

This closes #7456. It hides the terminal when the job is dead and
displays an error when trying to open an exec session for a task
that isn’t running. There’s a skipped test for the latter behaviour
that I’ll have to come back for.
This commit is contained in:
Buck Doyle 2020-04-06 14:08:22 -05:00 committed by GitHub
parent fc7de8b153
commit f10906e006
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 144 additions and 51 deletions

View file

@ -87,45 +87,51 @@ export default Controller.extend({
taskGroupName,
});
this.terminal.write(ANSI_UI_GRAY_400);
this.terminal.writeln('');
if (!allocationShortId) {
this.terminal.writeln(
'Multiple instances of this task are running. The allocation below was selected by random draw.'
);
if (this.taskState) {
this.terminal.write(ANSI_UI_GRAY_400);
this.terminal.writeln('');
if (!allocationShortId) {
this.terminal.writeln(
'Multiple instances of this task are running. The allocation below was selected by random draw.'
);
this.terminal.writeln('');
}
this.terminal.writeln('Customize your command, then hit return to run.');
this.terminal.writeln('');
this.terminal.write(
`$ nomad alloc exec -i -t -task ${escapeTaskName(taskName)} ${
this.taskState.allocation.shortId
} `
);
this.terminal.write(ANSI_WHITE);
this.terminal.write(this.command);
if (this.commandEditorAdapter) {
this.commandEditorAdapter.destroy();
}
this.commandEditorAdapter = new ExecCommandEditorXtermAdapter(
this.terminal,
this.openAndConnectSocket.bind(this),
this.command
);
}
this.terminal.writeln('Customize your command, then hit return to run.');
this.terminal.writeln('');
this.terminal.write(
`$ nomad alloc exec -i -t -task ${escapeTaskName(taskName)} ${
this.taskState.allocation.shortId
} `
);
this.terminal.write(ANSI_WHITE);
this.terminal.write(this.command);
if (this.commandEditorAdapter) {
this.commandEditorAdapter.destroy();
}
this.commandEditorAdapter = new ExecCommandEditorXtermAdapter(
this.terminal,
this.openAndConnectSocket.bind(this),
this.command
);
},
},
openAndConnectSocket(command) {
this.set('socketOpen', true);
this.set('command', command);
this.socket = this.sockets.getTaskStateSocket(this.taskState, command);
if (this.taskState) {
this.set('socketOpen', true);
this.set('command', command);
this.socket = this.sockets.getTaskStateSocket(this.taskState, command);
new ExecSocketXtermAdapter(this.terminal, this.socket, this.token.secret);
new ExecSocketXtermAdapter(this.terminal, this.socket, this.token.secret);
} else {
this.terminal.writeln(`Failed to open a socket because task ${this.taskName} is not active.`);
}
},
});

View file

@ -12,6 +12,7 @@
padding: 16px;
height: 100%;
position: relative;
color: white;
.terminal {
height: 100%;

View file

@ -29,20 +29,30 @@
</div>
</nav>
<div class="tree-and-terminal">
<div class="task-group-tree">
<h4 class="title is-6">Tasks</h4>
<ul>
{{#each sortedTaskGroups as |taskGroup|}}
<li data-test-task-group>
{{exec/task-group-parent
taskGroup=taskGroup
openInNewWindow=socketOpen
activeTaskName=taskName
activeTaskGroupName=taskGroupName}}
</li>
{{/each}}
</ul>
{{#if (eq model.status "dead")}}
<div class="tree-and-terminal" data-test-exec-job-dead>
<div class="task-group-tree">
</div>
<div class="terminal-container" data-test-exec-job-dead-message>
Job <code>{{model.name}}</code> is dead and cannot host an exec session.
</div>
</div>
{{exec-terminal terminal=terminal}}
</div>
{{else}}
<div class="tree-and-terminal">
<div class="task-group-tree">
<h4 class="title is-6">Tasks</h4>
<ul>
{{#each sortedTaskGroups as |taskGroup|}}
<li data-test-task-group>
{{exec/task-group-parent
taskGroup=taskGroup
openInNewWindow=socketOpen
activeTaskName=taskName
activeTaskGroupName=taskGroupName}}
</li>
{{/each}}
</ul>
</div>
{{exec-terminal terminal=terminal}}
</div>
{{/if}}

View file

@ -1,4 +1,4 @@
import { module, test } from 'qunit';
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';
@ -21,6 +21,7 @@ module('Acceptance | exec', function(hooks) {
groupsCount: 2,
groupTaskCount: 5,
createAllocations: false,
status: 'running',
});
this.job.task_group_ids.forEach(taskGroupId => {
@ -39,7 +40,11 @@ module('Acceptance | exec', function(hooks) {
server.create('region', { id: 'global' });
server.create('region', { id: 'region-2' });
this.job = server.create('job', { createAllocations: false, namespaceId: namespace.id });
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' });
@ -48,6 +53,8 @@ module('Acceptance | exec', function(hooks) {
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) {
@ -167,6 +174,42 @@ module('Acceptance | exec', function(hooks) {
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.task_groups.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.task_groups.models.sortBy('name')[0];
await Exec.visitTaskGroup({ job: this.job.id, task_group: taskGroup.name });
@ -485,6 +528,34 @@ module('Acceptance | exec', function(hooks) {
`$ 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.task_groups.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
.getLine(7)
.translateToString()
.trim(),
`Failed to open a socket because task ${task.name} is not active.`
);
});
});
class MockSocket {

View file

@ -42,4 +42,9 @@ export default create({
scope: '.xterm-helper-textarea',
pressEnter: triggerable('keydown', '', { eventProperties: { keyCode: 13 } }),
},
jobDead: {
scope: '[data-test-exec-job-dead]',
message: text('[data-test-exec-job-dead-message]'),
},
});