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,6 +87,7 @@ export default Controller.extend({
taskGroupName, taskGroupName,
}); });
if (this.taskState) {
this.terminal.write(ANSI_UI_GRAY_400); this.terminal.write(ANSI_UI_GRAY_400);
this.terminal.writeln(''); this.terminal.writeln('');
@ -118,14 +119,19 @@ export default Controller.extend({
this.openAndConnectSocket.bind(this), this.openAndConnectSocket.bind(this),
this.command this.command
); );
}
}, },
}, },
openAndConnectSocket(command) { openAndConnectSocket(command) {
if (this.taskState) {
this.set('socketOpen', true); this.set('socketOpen', true);
this.set('command', command); this.set('command', command);
this.socket = this.sockets.getTaskStateSocket(this.taskState, 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; padding: 16px;
height: 100%; height: 100%;
position: relative; position: relative;
color: white;
.terminal { .terminal {
height: 100%; height: 100%;

View file

@ -29,7 +29,16 @@
</div> </div>
</nav> </nav>
<div class="tree-and-terminal"> {{#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>
{{else}}
<div class="tree-and-terminal">
<div class="task-group-tree"> <div class="task-group-tree">
<h4 class="title is-6">Tasks</h4> <h4 class="title is-6">Tasks</h4>
<ul> <ul>
@ -45,4 +54,5 @@
</ul> </ul>
</div> </div>
{{exec-terminal terminal=terminal}} {{exec-terminal terminal=terminal}}
</div> </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 { currentURL, settled } from '@ember/test-helpers';
import { setupApplicationTest } from 'ember-qunit'; import { setupApplicationTest } from 'ember-qunit';
import { setupMirage } from 'ember-cli-mirage/test-support'; import { setupMirage } from 'ember-cli-mirage/test-support';
@ -21,6 +21,7 @@ module('Acceptance | exec', function(hooks) {
groupsCount: 2, groupsCount: 2,
groupTaskCount: 5, groupTaskCount: 5,
createAllocations: false, createAllocations: false,
status: 'running',
}); });
this.job.task_group_ids.forEach(taskGroupId => { 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: 'global' });
server.create('region', { id: 'region-2' }); 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' }); 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.region.text, this.job.region);
assert.equal(Exec.header.namespace.text, this.job.namespace); assert.equal(Exec.header.namespace.text, this.job.namespace);
assert.equal(Exec.header.job, this.job.name); 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) { 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); 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) { 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]; let taskGroup = this.job.task_groups.models.sortBy('name')[0];
await Exec.visitTaskGroup({ job: this.job.id, task_group: taskGroup.name }); 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` `$ 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 { class MockSocket {

View file

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