diff --git a/ui/mirage/factories/allocation.js b/ui/mirage/factories/allocation.js
index 125ea5dd3fc..ba36082fda6 100644
--- a/ui/mirage/factories/allocation.js
+++ b/ui/mirage/factories/allocation.js
@@ -124,6 +124,20 @@ export default Factory.extend({
},
}),
+ preempted: trait({
+ afterCreate(allocation, server) {
+ const preempter = server.create('allocation', { preemptedAllocations: [allocation.id] });
+ allocation.update({ preemptedByAllocation: preempter.id });
+ },
+ }),
+
+ preempter: trait({
+ afterCreate(allocation, server) {
+ const preempted = server.create('allocation', { preemptedByAllocation: allocation.id });
+ allocation.update({ preemptedAllocations: [preempted.id] });
+ },
+ }),
+
afterCreate(allocation, server) {
Ember.assert(
'[Mirage] No jobs! make sure jobs are created before allocations',
diff --git a/ui/public/images/icons/boot.svg b/ui/public/images/icons/boot.svg
new file mode 100644
index 00000000000..116bd024593
--- /dev/null
+++ b/ui/public/images/icons/boot.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/tests/acceptance/allocation-detail-test.js b/ui/tests/acceptance/allocation-detail-test.js
index 21b613e2827..3e5ca598066 100644
--- a/ui/tests/acceptance/allocation-detail-test.js
+++ b/ui/tests/acceptance/allocation-detail-test.js
@@ -199,3 +199,106 @@ module('Acceptance | allocation detail (not running)', function(hooks) {
);
});
});
+
+module('Acceptance | allocation detail (preemptions)', function(hooks) {
+ setupApplicationTest(hooks);
+ setupMirage(hooks);
+
+ hooks.beforeEach(async function() {
+ server.create('agent');
+ node = server.create('node');
+ job = server.create('job', { createAllocations: false });
+ });
+
+ test('shows a dedicated section to the allocation that preempted this allocation', async function(assert) {
+ allocation = server.create('allocation', 'preempted');
+ const preempter = server.schema.find('allocation', allocation.preemptedByAllocation);
+ const preempterJob = server.schema.find('job', preempter.jobId);
+ const preempterClient = server.schema.find('node', preempter.nodeId);
+
+ await Allocation.visit({ id: allocation.id });
+ assert.ok(Allocation.wasPreempted, 'Preempted allocation section is shown');
+ assert.equal(Allocation.preempter.status, preempter.clientStatus, 'Preempter status matches');
+ assert.equal(Allocation.preempter.name, preempter.name, 'Preempter name matches');
+ assert.equal(
+ Allocation.preempter.priority,
+ preempterJob.priority,
+ 'Preempter priority matches'
+ );
+
+ await Allocation.preempter.visit();
+ assert.equal(
+ currentURL(),
+ `/allocations/${preempter.id}`,
+ 'Clicking the preempter id navigates to the preempter allocation detail page'
+ );
+
+ await Allocation.visit({ id: allocation.id });
+ await Allocation.preempter.visitJob();
+ assert.equal(
+ currentURL(),
+ `/jobs/${preempterJob.id}`,
+ 'Clicking the preempter job link navigates to the preempter job page'
+ );
+
+ await Allocation.visit({ id: allocation.id });
+ await Allocation.preempter.visitClient();
+ assert.equal(
+ currentURL(),
+ `/clients/${preempterClient.id}`,
+ 'Clicking the preempter client link navigates to the preempter client page'
+ );
+ });
+
+ test('shows a dedicated section to the allocations this allocation preempted', async function(assert) {
+ allocation = server.create('allocation', 'preempter');
+ await Allocation.visit({ id: allocation.id });
+ assert.ok(Allocation.preempted, 'The allocations this allocation preempted are shown');
+ });
+
+ test('each preempted allocation in the table lists basic allocation information', async function(assert) {
+ allocation = server.create('allocation', 'preempter');
+ await Allocation.visit({ id: allocation.id });
+
+ const preemption = allocation.preemptedAllocations
+ .map(id => server.schema.find('allocation', id))
+ .sortBy('modifyIndex')
+ .reverse()[0];
+ const preemptionRow = Allocation.preemptions.objectAt(0);
+
+ assert.equal(
+ Allocation.preemptions.length,
+ allocation.preemptedAllocations.length,
+ 'The preemptions table has a row for each preempted allocation'
+ );
+
+ assert.equal(preemptionRow.shortId, preemption.id.split('-')[0], 'Preemption short id');
+ assert.equal(
+ preemptionRow.createTime,
+ moment(preemption.createTime / 1000000).format('MMM DD HH:mm:ss ZZ'),
+ 'Preemption create time'
+ );
+ assert.equal(
+ preemptionRow.modifyTime,
+ moment(preemption.modifyTime / 1000000).fromNow(),
+ 'Preemption modify time'
+ );
+ assert.equal(preemptionRow.status, preemption.clientStatus, 'Client status');
+ assert.equal(preemptionRow.jobVersion, preemption.jobVersion, 'Job Version');
+ assert.equal(
+ preemptionRow.client,
+ server.db.nodes.find(preemption.nodeId).id.split('-')[0],
+ 'Node ID'
+ );
+
+ await preemptionRow.visitClient();
+ assert.equal(currentURL(), `/clients/${preemption.nodeId}`, 'Node links to node page');
+ });
+
+ test('when an allocation both preempted allocations and was preempted itself, both preemptions sections are shown', async function(assert) {
+ allocation = server.create('allocation', 'preempter', 'preempted');
+ await Allocation.visit({ id: allocation.id });
+ assert.ok(Allocation.preempted, 'The allocations this allocation preempted are shown');
+ assert.ok(Allocation.wasPreempted, 'Preempted allocation section is shown');
+ });
+});
diff --git a/ui/tests/acceptance/client-detail-test.js b/ui/tests/acceptance/client-detail-test.js
index 5750ee6e2c3..2307731ed94 100644
--- a/ui/tests/acceptance/client-detail-test.js
+++ b/ui/tests/acceptance/client-detail-test.js
@@ -12,6 +12,8 @@ import Jobs from 'nomad-ui/tests/pages/jobs/list';
let node;
+const wasPreemptedFilter = allocation => !!allocation.preemptedByAllocation;
+
module('Acceptance | client detail', function(hooks) {
setupApplicationTest(hooks);
setupMirage(hooks);
@@ -24,6 +26,7 @@ module('Acceptance | client detail', function(hooks) {
server.create('agent');
server.create('job', { createAllocations: false });
server.createList('allocation', 3, { nodeId: node.id, clientStatus: 'running' });
+ server.create('allocation', 'preempted', { nodeId: node.id, clientStatus: 'running' });
});
test('/clients/:id should have a breadcrumb trail linking back to clients', async function(assert) {
@@ -219,6 +222,65 @@ module('Acceptance | client detail', function(hooks) {
);
});
+ test('the allocation section should show the count of preempted allocations on the client', async function(assert) {
+ const allocations = server.db.allocations.where({ nodeId: node.id });
+
+ await ClientDetail.visit({ id: node.id });
+
+ assert.equal(
+ ClientDetail.allocationFilter.allCount,
+ allocations.length,
+ 'All filter/badge shows all allocations count'
+ );
+ assert.ok(
+ ClientDetail.allocationFilter.preemptionsCount.startsWith(
+ allocations.filter(wasPreemptedFilter).length
+ ),
+ 'Preemptions filter/badge shows preempted allocations count'
+ );
+ });
+
+ test('clicking the preemption badge filters the allocations table and sets a query param', async function(assert) {
+ const allocations = server.db.allocations.where({ nodeId: node.id });
+
+ await ClientDetail.visit({ id: node.id });
+ await ClientDetail.allocationFilter.preemptions();
+
+ assert.equal(
+ ClientDetail.allocations.length,
+ allocations.filter(wasPreemptedFilter).length,
+ 'Only preempted allocations are shown'
+ );
+ assert.equal(
+ currentURL(),
+ `/clients/${node.id}?preemptions=true`,
+ 'Filter is persisted in the URL'
+ );
+ });
+
+ test('clicking the total allocations badge resets the filter and removes the query param', async function(assert) {
+ const allocations = server.db.allocations.where({ nodeId: node.id });
+
+ await ClientDetail.visit({ id: node.id });
+ await ClientDetail.allocationFilter.preemptions();
+ await ClientDetail.allocationFilter.all();
+
+ assert.equal(ClientDetail.allocations.length, allocations.length, 'All allocations are shown');
+ assert.equal(currentURL(), `/clients/${node.id}`, 'Filter is persisted in the URL');
+ });
+
+ test('navigating directly to the client detail page with the preemption query param set will filter the allocations table', async function(assert) {
+ const allocations = server.db.allocations.where({ nodeId: node.id });
+
+ await ClientDetail.visit({ id: node.id, preemptions: true });
+
+ assert.equal(
+ ClientDetail.allocations.length,
+ allocations.filter(wasPreemptedFilter).length,
+ 'Only preempted allocations are shown'
+ );
+ });
+
test('/clients/:id should list all attributes for the node', async function(assert) {
await ClientDetail.visit({ id: node.id });
diff --git a/ui/tests/integration/allocation-row-test.js b/ui/tests/integration/allocation-row-test.js
index bd3c48f210f..70b816ac68b 100644
--- a/ui/tests/integration/allocation-row-test.js
+++ b/ui/tests/integration/allocation-row-test.js
@@ -123,6 +123,23 @@ module('Integration | Component | allocation row', function(hooks) {
});
});
+ test('Allocation row shows an icon indicator when it was preempted', async function(assert) {
+ const allocId = this.server.create('allocation', 'preempted').id;
+
+ const allocation = await this.store.findRecord('allocation', allocId);
+ await settled();
+
+ this.setProperties({ allocation, context: 'job' });
+ await render(hbs`
+ {{allocation-row
+ allocation=allocation
+ context=context}}
+ `);
+ await settled();
+
+ assert.ok(find('[data-test-icon="preemption"]'), 'Preempted icon is shown');
+ });
+
test('when an allocation is not running, the utilization graphs are omitted', function(assert) {
this.setProperties({
context: 'job',
diff --git a/ui/tests/pages/allocations/detail.js b/ui/tests/pages/allocations/detail.js
index 54ce5accaed..f1e5348f253 100644
--- a/ui/tests/pages/allocations/detail.js
+++ b/ui/tests/pages/allocations/detail.js
@@ -8,6 +8,8 @@ import {
visitable,
} from 'ember-cli-page-object';
+import allocations from 'nomad-ui/tests/pages/components/allocations';
+
export default create({
visit: visitable('/allocations/:id'),
@@ -51,6 +53,24 @@ export default create({
isEmpty: isPresent('[data-test-empty-tasks-list]'),
+ wasPreempted: isPresent('[data-test-was-preempted]'),
+ preempter: {
+ scope: '[data-test-was-preempted]',
+
+ status: text('[data-test-allocation-status]'),
+ name: text('[data-test-allocation-name]'),
+ priority: text('[data-test-job-priority]'),
+ reservedCPU: text('[data-test-allocation-cpu]'),
+ reservedMemory: text('[data-test-allocation-memory]'),
+
+ visit: clickable('[data-test-allocation-id]'),
+ visitJob: clickable('[data-test-job-link]'),
+ visitClient: clickable('[data-test-client-link]'),
+ },
+
+ preempted: isPresent('[data-test-preemptions]'),
+ ...allocations('[data-test-preemptions] [data-test-allocation]', 'preemptions'),
+
error: {
isShown: isPresent('[data-test-error]'),
title: text('[data-test-error-title]'),
diff --git a/ui/tests/pages/clients/detail.js b/ui/tests/pages/clients/detail.js
index c4b98d9ded3..c0db54b74b2 100644
--- a/ui/tests/pages/clients/detail.js
+++ b/ui/tests/pages/clients/detail.js
@@ -45,6 +45,13 @@ export default create({
...allocations(),
+ allocationFilter: {
+ preemptions: clickable('[data-test-filter-preemptions]'),
+ all: clickable('[data-test-filter-all]'),
+ preemptionsCount: text('[data-test-filter-preemptions]'),
+ allCount: text('[data-test-filter-all]'),
+ },
+
attributesTable: isPresent('[data-test-attributes]'),
metaTable: isPresent('[data-test-meta]'),
emptyMetaMessage: isPresent('[data-test-empty-meta-message]'),
diff --git a/ui/tests/pages/components/allocations.js b/ui/tests/pages/components/allocations.js
index 29cfb3ffca0..99307e5c782 100644
--- a/ui/tests/pages/components/allocations.js
+++ b/ui/tests/pages/components/allocations.js
@@ -1,8 +1,8 @@
import { attribute, collection, clickable, isPresent, text } from 'ember-cli-page-object';
-export default function(selector = '[data-test-allocation]') {
+export default function(selector = '[data-test-allocation]', propKey = 'allocations') {
return {
- allocations: collection(selector, {
+ [propKey]: collection(selector, {
id: attribute('data-test-allocation'),
shortId: text('[data-test-short-id]'),
createTime: text('[data-test-create-time]'),
diff --git a/ui/tests/unit/serializers/allocation-test.js b/ui/tests/unit/serializers/allocation-test.js
index db928953152..9036f136df6 100644
--- a/ui/tests/unit/serializers/allocation-test.js
+++ b/ui/tests/unit/serializers/allocation-test.js
@@ -44,6 +44,7 @@ module('Unit | Serializer | Allocation', function(hooks) {
failed: false,
},
],
+ wasPreempted: false,
},
relationships: {
followUpEvaluation: {
@@ -55,6 +56,12 @@ module('Unit | Serializer | Allocation', function(hooks) {
previousAllocation: {
data: null,
},
+ preemptedAllocations: {
+ data: [],
+ },
+ preemptedByAllocation: {
+ data: null,
+ },
job: {
data: {
id: '["test-summary","test-namespace"]',
@@ -108,6 +115,71 @@ module('Unit | Serializer | Allocation', function(hooks) {
failed: true,
},
],
+ wasPreempted: false,
+ },
+ relationships: {
+ followUpEvaluation: {
+ data: null,
+ },
+ nextAllocation: {
+ data: null,
+ },
+ previousAllocation: {
+ data: null,
+ },
+ preemptedAllocations: {
+ data: [],
+ },
+ preemptedByAllocation: {
+ data: null,
+ },
+ job: {
+ data: {
+ id: '["test-summary","test-namespace"]',
+ type: 'job',
+ },
+ },
+ },
+ },
+ },
+ },
+
+ {
+ name: 'With preemptions',
+ in: {
+ ID: 'test-allocation',
+ JobID: 'test-summary',
+ Name: 'test-summary[1]',
+ Namespace: 'test-namespace',
+ TaskGroup: 'test-group',
+ CreateTime: +sampleDate * 1000000,
+ ModifyTime: +sampleDate * 1000000,
+ TaskStates: {
+ task: {
+ State: 'running',
+ Failed: false,
+ },
+ },
+ PreemptedByAllocation: 'preempter-allocation',
+ PreemptedAllocations: ['preempted-one-allocation', 'preempted-two-allocation'],
+ },
+ out: {
+ data: {
+ id: 'test-allocation',
+ type: 'allocation',
+ attributes: {
+ taskGroupName: 'test-group',
+ name: 'test-summary[1]',
+ modifyTime: sampleDate,
+ createTime: sampleDate,
+ states: [
+ {
+ name: 'task',
+ state: 'running',
+ failed: false,
+ },
+ ],
+ wasPreempted: true,
},
relationships: {
followUpEvaluation: {
@@ -119,6 +191,18 @@ module('Unit | Serializer | Allocation', function(hooks) {
previousAllocation: {
data: null,
},
+ preemptedAllocations: {
+ data: [
+ { id: 'preempted-one-allocation', type: 'allocation' },
+ { id: 'preempted-two-allocation', type: 'allocation' },
+ ],
+ },
+ preemptedByAllocation: {
+ data: {
+ id: 'preempter-allocation',
+ type: 'allocation',
+ },
+ },
job: {
data: {
id: '["test-summary","test-namespace"]',
diff --git a/ui/tests/unit/serializers/job-plan-test.js b/ui/tests/unit/serializers/job-plan-test.js
index d56690c3ebb..994f811c24e 100644
--- a/ui/tests/unit/serializers/job-plan-test.js
+++ b/ui/tests/unit/serializers/job-plan-test.js
@@ -38,7 +38,11 @@ module('Unit | Serializer | JobPlan', function(hooks) {
},
],
},
- relationships: {},
+ relationships: {
+ preemptions: {
+ data: [],
+ },
+ },
},
},
},
@@ -78,7 +82,57 @@ module('Unit | Serializer | JobPlan', function(hooks) {
},
],
},
- relationships: {},
+ relationships: {
+ preemptions: {
+ data: [],
+ },
+ },
+ },
+ },
+ },
+
+ {
+ name: 'With preemptions',
+ in: {
+ ID: 'test-plan',
+ Diff: {
+ Arbitrary: 'Value',
+ },
+ FailedTGAllocs: {
+ task: {
+ NodesAvailable: 10,
+ },
+ },
+ Annotations: {
+ PreemptedAllocs: [
+ { ID: 'preemption-one-allocation' },
+ { ID: 'preemption-two-allocation' },
+ ],
+ },
+ },
+ out: {
+ data: {
+ id: 'test-plan',
+ type: 'job-plan',
+ attributes: {
+ diff: {
+ Arbitrary: 'Value',
+ },
+ failedTGAllocs: [
+ {
+ name: 'task',
+ nodesAvailable: 10,
+ },
+ ],
+ },
+ relationships: {
+ preemptions: {
+ data: [
+ { id: 'preemption-one-allocation', type: 'allocation' },
+ { id: 'preemption-two-allocation', type: 'allocation' },
+ ],
+ },
+ },
},
},
},