diff --git a/ui/app/components/lifecycle-chart-row.js b/ui/app/components/lifecycle-chart-row.js index 44985d747..5a2422cb9 100644 --- a/ui/app/components/lifecycle-chart-row.js +++ b/ui/app/components/lifecycle-chart-row.js @@ -23,4 +23,17 @@ export default class LifecycleChartRow extends Component { return undefined; } + + @computed('task.lifecycleName') + get lifecycleLabel() { + const name = this.task.lifecycleName; + + if (name.includes('sidecar')) { + return 'sidecar'; + } else if (name.includes('ephemeral')) { + return name.substr(0, name.indexOf('-')); + } else { + return name; + } + } } diff --git a/ui/app/components/lifecycle-chart.js b/ui/app/components/lifecycle-chart.js index d2e1735ea..a58e6b839 100644 --- a/ui/app/components/lifecycle-chart.js +++ b/ui/app/components/lifecycle-chart.js @@ -14,8 +14,11 @@ export default class LifecycleChart extends Component { get lifecyclePhases() { const tasksOrStates = this.taskStates || this.tasks; const lifecycles = { - prestarts: [], - sidecars: [], + 'prestart-ephemerals': [], + 'prestart-sidecars': [], + 'poststart-ephemerals': [], + 'poststart-sidecars': [], + poststops: [], mains: [], }; @@ -25,18 +28,29 @@ export default class LifecycleChart extends Component { }); const phases = []; + const stateActiveIterator = state => state.state === 'running'; - if (lifecycles.prestarts.length || lifecycles.sidecars.length) { + if (lifecycles.mains.length < tasksOrStates.length) { phases.push({ name: 'Prestart', - isActive: lifecycles.prestarts.some(state => state.state === 'running'), + isActive: lifecycles['prestart-ephemerals'].some(stateActiveIterator), }); - } - if (lifecycles.sidecars.length || lifecycles.mains.length) { phases.push({ name: 'Main', - isActive: lifecycles.mains.some(state => state.state === 'running'), + isActive: + lifecycles.mains.some(stateActiveIterator) || + lifecycles['poststart-ephemerals'].some(stateActiveIterator), + }); + + // Poststart is rendered as a subphase of main and therefore has no independent active state + phases.push({ + name: 'Poststart', + }); + + phases.push({ + name: 'Poststop', + isActive: lifecycles.poststops.some(stateActiveIterator), }); } @@ -55,12 +69,14 @@ export default class LifecycleChart extends Component { } const lifecycleNameSortPrefix = { - prestart: 0, - sidecar: 1, + 'prestart-ephemeral': 0, + 'prestart-sidecar': 1, main: 2, + 'poststart-sidecar': 3, + 'poststart-ephemeral': 4, + poststop: 5, }; function getTaskSortPrefix(task) { - // Prestarts first, then sidecars, then mains return `${lifecycleNameSortPrefix[task.lifecycleName]}-${task.name}`; } diff --git a/ui/app/controllers/allocations/allocation/task/index.js b/ui/app/controllers/allocations/allocation/task/index.js index 6af893dff..57e661d1f 100644 --- a/ui/app/controllers/allocations/allocation/task/index.js +++ b/ui/app/controllers/allocations/allocation/task/index.js @@ -1,22 +1,10 @@ import Controller from '@ember/controller'; -import { computed } from '@ember/object'; import { computed as overridable } from 'ember-overridable-computed'; import { task } from 'ember-concurrency'; import classic from 'ember-classic-decorator'; @classic export default class IndexController extends Controller { - @computed('model.task.taskGroup.tasks.@each.name') - get otherTaskStates() { - const taskName = this.model.task.name; - return this.model.allocation.states.rejectBy('name', taskName); - } - - @computed('otherTaskStates.@each.lifecycle') - get prestartTaskStates() { - return this.otherTaskStates.filterBy('task.lifecycle'); - } - @overridable(() => { // { title, description } return null; diff --git a/ui/app/models/task.js b/ui/app/models/task.js index 7c2b95a6f..41722c217 100644 --- a/ui/app/models/task.js +++ b/ui/app/models/task.js @@ -14,8 +14,18 @@ export default class Task extends Fragment { @computed('lifecycle', 'lifecycle.sidecar') get lifecycleName() { - if (this.lifecycle && this.lifecycle.sidecar) return 'sidecar'; - if (this.lifecycle && this.lifecycle.hook === 'prestart') return 'prestart'; + if (this.lifecycle) { + const { hook, sidecar } = this.lifecycle; + + if (hook === 'prestart') { + return sidecar ? 'prestart-sidecar' : 'prestart-ephemeral'; + } else if (hook === 'poststart') { + return sidecar ? 'poststart-sidecar' : 'poststart-ephemeral'; + } else if (hook === 'poststop') { + return 'poststop'; + } + } + return 'main'; } diff --git a/ui/app/styles/components/lifecycle-chart.scss b/ui/app/styles/components/lifecycle-chart.scss index 3c009058a..aec38bd91 100644 --- a/ui/app/styles/components/lifecycle-chart.scss +++ b/ui/app/styles/components/lifecycle-chart.scss @@ -11,7 +11,6 @@ .divider { position: absolute; - left: 25%; height: 100%; stroke: $ui-gray-200; @@ -19,6 +18,14 @@ stroke-dasharray: 1, 7; stroke-dashoffset: 1; stroke-linecap: square; + + &.prestart { + left: 25%; + } + + &.poststop { + left: 75%; + } } } @@ -52,6 +59,16 @@ &.main { left: 25%; + right: 25%; + } + + &.poststart { + left: 35%; + right: 25%; + } + + &.poststop { + left: 75%; right: 0; } } @@ -110,12 +127,34 @@ &.main { margin-left: 25%; + margin-right: 25%; } - &.prestart { + &.prestart-ephemeral { margin-right: 75%; } + &.prestart-sidecar { + margin-right: 25%; + } + + &.poststart-ephemeral, + &.poststart-sidecar { + margin-left: 35%; + } + + &.poststart-sidecar { + margin-right: 25%; + } + + &.poststart-ephemeral { + margin-right: 35%; + } + + &.poststop { + margin-left: 75%; + } + &:last-child .task { margin-bottom: 0.9em; } diff --git a/ui/app/templates/allocations/allocation/task/index.hbs b/ui/app/templates/allocations/allocation/task/index.hbs index 4b36e9147..72dbd3edf 100644 --- a/ui/app/templates/allocations/allocation/task/index.hbs +++ b/ui/app/templates/allocations/allocation/task/index.hbs @@ -93,38 +93,6 @@ - {{#if (and (not this.model.task.lifecycle) this.prestartTaskStates)}} -
-
- Prestart Tasks -
-
- - - - Task - State - Lifecycle - - - - - {{#if (and row.model.isRunning (eq row.model.task.lifecycleName "prestart"))}} - - {{x-icon "warning" class="is-warning"}} - - {{/if}} - - {{row.model.task.name}} - {{row.model.state}} - {{row.model.task.lifecycleName}} - - - -
-
- {{/if}} - {{#if this.model.task.volumeMounts.length}}
diff --git a/ui/app/templates/components/lifecycle-chart-row.hbs b/ui/app/templates/components/lifecycle-chart-row.hbs index 9236d24a1..bdd9fb239 100644 --- a/ui/app/templates/components/lifecycle-chart-row.hbs +++ b/ui/app/templates/components/lifecycle-chart-row.hbs @@ -11,6 +11,6 @@ {{this.task.name}} {{/if}}
-
{{capitalize this.task.lifecycleName}} Task
+
{{capitalize this.lifecycleLabel}} Task
diff --git a/ui/app/templates/components/lifecycle-chart.hbs b/ui/app/templates/components/lifecycle-chart.hbs index 1cbb232a0..5cf17a9b1 100644 --- a/ui/app/templates/components/lifecycle-chart.hbs +++ b/ui/app/templates/components/lifecycle-chart.hbs @@ -7,11 +7,14 @@
{{#each this.lifecyclePhases as |phase|}} -
+
{{phase.name}}
{{/each}} - + + + +
diff --git a/ui/mirage/factories/task.js b/ui/mirage/factories/task.js index 03ac9270b..6f898aaae 100644 --- a/ui/mirage/factories/task.js +++ b/ui/mirage/factories/task.js @@ -19,14 +19,20 @@ export default Factory.extend({ Resources: generateResources, Lifecycle: i => { - const cycle = i % 3; + const cycle = i % 6; if (cycle === 0) { return null; } else if (cycle === 1) { return { Hook: 'prestart', Sidecar: false }; - } else { + } else if (cycle === 2) { return { Hook: 'prestart', Sidecar: true }; + } else if (cycle === 3) { + return { Hook: 'poststart', Sidecar: false }; + } else if (cycle === 4) { + return { Hook: 'poststart', Sidecar: true }; + } else if (cycle === 5) { + return { Hook: 'poststop' }; } }, }); diff --git a/ui/tests/acceptance/allocation-detail-test.js b/ui/tests/acceptance/allocation-detail-test.js index 3223868e5..67943f525 100644 --- a/ui/tests/acceptance/allocation-detail-test.js +++ b/ui/tests/acceptance/allocation-detail-test.js @@ -82,7 +82,7 @@ module('Acceptance | allocation detail', function(hooks) { test('/allocation/:id should present task lifecycles', async function(assert) { const job = server.create('job', { groupsCount: 1, - groupTaskCount: 3, + groupTaskCount: 6, withGroupServices: true, createAllocations: false, }); @@ -92,96 +92,23 @@ module('Acceptance | allocation detail', function(hooks) { jobId: job.id, }); - const taskStatePhases = server.db.taskStates.where({ allocationId: allocation.id }).reduce( - (phases, state) => { - const lifecycle = server.db.tasks.findBy({ name: state.name }).Lifecycle; - - if (lifecycle) { - if (lifecycle.Sidecar) { - phases.sidecars.push(state); - state.lifecycleString = 'Sidecar'; - } else { - phases.prestarts.push(state); - state.lifecycleString = 'Prestart'; - } - } else { - phases.mains.push(state); - state.lifecycleString = 'Main'; - } - - return phases; - }, - { - prestarts: [], - sidecars: [], - mains: [], - } - ); - - taskStatePhases.prestarts = taskStatePhases.prestarts.sortBy('name'); - taskStatePhases.sidecars = taskStatePhases.sidecars.sortBy('name'); - taskStatePhases.mains = taskStatePhases.mains.sortBy('name'); - - const sortedServerStates = taskStatePhases.prestarts.concat( - taskStatePhases.sidecars, - taskStatePhases.mains - ); - await Allocation.visit({ id: allocation.id }); assert.ok(Allocation.lifecycleChart.isPresent); assert.equal(Allocation.lifecycleChart.title, 'Task Lifecycle Status'); - assert.equal(Allocation.lifecycleChart.phases.length, 2); - assert.equal(Allocation.lifecycleChart.tasks.length, sortedServerStates.length); - - const stateActiveIterator = state => state.state === 'running'; - const anyPrestartsActive = taskStatePhases.prestarts.some(stateActiveIterator); - - if (anyPrestartsActive) { - assert.ok(Allocation.lifecycleChart.phases[0].isActive); - } else { - assert.notOk(Allocation.lifecycleChart.phases[0].isActive); - } - - const anyMainsActive = taskStatePhases.mains.some(stateActiveIterator); - - if (anyMainsActive) { - assert.ok(Allocation.lifecycleChart.phases[1].isActive); - } else { - assert.notOk(Allocation.lifecycleChart.phases[1].isActive); - } - - Allocation.lifecycleChart.tasks.forEach((Task, index) => { - const serverState = sortedServerStates[index]; - - assert.equal(Task.name, serverState.name); - - if (serverState.lifecycleString === 'Sidecar') { - assert.ok(Task.isSidecar); - } else if (serverState.lifecycleString === 'Prestart') { - assert.ok(Task.isPrestart); - } else { - assert.ok(Task.isMain); - } - - assert.equal(Task.lifecycle, `${serverState.lifecycleString} Task`); - - if (serverState.state === 'running') { - assert.ok(Task.isActive); - } else { - assert.notOk(Task.isActive); - } - - // Task state factory uses invalid dates for tasks that aren’t finished - if (isNaN(serverState.finishedAt)) { - assert.notOk(Task.isFinished); - } else { - assert.ok(Task.isFinished); - } - }); + assert.equal(Allocation.lifecycleChart.phases.length, 4); + assert.equal(Allocation.lifecycleChart.tasks.length, 6); await Allocation.lifecycleChart.tasks[0].visit(); - assert.equal(currentURL(), `/allocations/${allocation.id}/${sortedServerStates[0].name}`); + + const prestartEphemeralTask = server.db.taskStates + .where({ allocationId: allocation.id }) + .find(taskState => { + const task = server.db.tasks.findBy({ name: taskState.name }); + return task.Lifecycle && task.Lifecycle.Hook === 'prestart' && !task.Lifecycle.Sidecar; + }); + + assert.equal(currentURL(), `/allocations/${allocation.id}/${prestartEphemeralTask.name}`); }); test('/allocation/:id should list all tasks for the allocation', async function(assert) { diff --git a/ui/tests/acceptance/task-detail-test.js b/ui/tests/acceptance/task-detail-test.js index 85d49c5fa..32b9c11cd 100644 --- a/ui/tests/acceptance/task-detail-test.js +++ b/ui/tests/acceptance/task-detail-test.js @@ -101,86 +101,6 @@ module('Acceptance | task detail', function(hooks) { assert.equal(Task.resourceCharts.objectAt(1).name, 'Memory', 'Second chart is Memory'); }); - test('/allocation/:id/:task_name lists related prestart tasks for a main task when they exist', async function(assert) { - const job = server.create('job', { - groupsCount: 2, - groupTaskCount: 3, - createAllocations: false, - status: 'running', - }); - - job.taskGroups.models.forEach(taskGroup => { - server.create('allocation', { - jobId: job.id, - taskGroup: taskGroup.name, - forceRunningClientStatus: true, - }); - }); - - const taskGroup = job.taskGroups.models[0]; - const [mainTask, sidecarTask, prestartTask] = taskGroup.tasks.models; - - mainTask.attrs.Lifecycle = null; - mainTask.save(); - - sidecarTask.attrs.Lifecycle = { Sidecar: true, Hook: 'prestart' }; - sidecarTask.save(); - - prestartTask.attrs.Lifecycle = { Sidecar: false, Hook: 'prestart' }; - prestartTask.save(); - - taskGroup.save(); - - const noPrestartTasksTaskGroup = job.taskGroups.models[1]; - noPrestartTasksTaskGroup.tasks.models.forEach(task => { - task.attrs.Lifecycle = null; - task.save(); - }); - - const mainTaskState = server.schema.taskStates.findBy({ name: mainTask.name }); - const sidecarTaskState = server.schema.taskStates.findBy({ name: sidecarTask.name }); - const prestartTaskState = server.schema.taskStates.findBy({ name: prestartTask.name }); - - prestartTaskState.attrs.state = 'running'; - prestartTaskState.attrs.finishedAt = null; - prestartTaskState.save(); - - await Task.visit({ id: mainTaskState.allocationId, name: mainTask.name }); - - assert.ok(Task.hasPrestartTasks); - assert.equal(Task.prestartTasks.length, 2); - - Task.prestartTasks[0].as(SidecarTask => { - assert.equal(SidecarTask.name, sidecarTask.name); - assert.equal(SidecarTask.state, sidecarTaskState.state); - assert.equal(SidecarTask.lifecycle, 'sidecar'); - assert.notOk(SidecarTask.isBlocking); - }); - - Task.prestartTasks[1].as(PrestartTask => { - assert.equal(PrestartTask.name, prestartTask.name); - assert.equal(PrestartTask.state, prestartTaskState.state); - assert.equal(PrestartTask.lifecycle, 'prestart'); - assert.ok(PrestartTask.isBlocking); - }); - - await Task.visit({ id: sidecarTaskState.allocationId, name: sidecarTask.name }); - - assert.notOk(Task.hasPrestartTasks); - - const noPrestartTasksTask = noPrestartTasksTaskGroup.tasks.models[0]; - const noPrestartTasksTaskState = server.db.taskStates.findBy({ - name: noPrestartTasksTask.name, - }); - - await Task.visit({ - id: noPrestartTasksTaskState.allocationId, - name: noPrestartTasksTaskState.name, - }); - - assert.notOk(Task.hasPrestartTasks); - }); - test('the events table lists all recent events', async function(assert) { const events = server.db.taskEvents.where({ taskStateId: task.id }); diff --git a/ui/tests/integration/components/lifecycle-chart-test.js b/ui/tests/integration/components/lifecycle-chart-test.js index 3c47d0b96..864f3fcfb 100644 --- a/ui/tests/integration/components/lifecycle-chart-test.js +++ b/ui/tests/integration/components/lifecycle-chart-test.js @@ -2,6 +2,7 @@ import { module, test } from 'qunit'; import { setupRenderingTest } from 'ember-qunit'; import { render, settled } from '@ember/test-helpers'; import hbs from 'htmlbars-inline-precompile'; +import { set } from '@ember/object'; import { componentA11yAudit } from 'nomad-ui/tests/helpers/a11y-audit'; import { create } from 'ember-cli-page-object'; import LifecycleChart from 'nomad-ui/tests/pages/components/lifecycle-chart'; @@ -11,19 +12,31 @@ const Chart = create(LifecycleChart); const tasks = [ { lifecycleName: 'main', - name: 'main two', + name: 'main two: 3', }, { lifecycleName: 'main', - name: 'main one', + name: 'main one: 2', }, { - lifecycleName: 'prestart', - name: 'prestart', + lifecycleName: 'prestart-ephemeral', + name: 'prestart ephemeral: 0', }, { - lifecycleName: 'sidecar', - name: 'sidecar', + lifecycleName: 'prestart-sidecar', + name: 'prestart sidecar: 1', + }, + { + lifecycleName: 'poststart-ephemeral', + name: 'poststart ephemeral: 5', + }, + { + lifecycleName: 'poststart-sidecar', + name: 'poststart sidecar: 4', + }, + { + lifecycleName: 'poststop', + name: 'poststop: 6', }, ]; @@ -38,20 +51,36 @@ module('Integration | Component | lifecycle-chart', function(hooks) { assert.equal(Chart.phases[0].name, 'Prestart'); assert.equal(Chart.phases[1].name, 'Main'); + assert.equal(Chart.phases[2].name, 'Poststart'); + assert.equal(Chart.phases[3].name, 'Poststop'); Chart.phases.forEach(phase => assert.notOk(phase.isActive)); - assert.deepEqual(Chart.tasks.mapBy('name'), ['prestart', 'sidecar', 'main one', 'main two']); + assert.deepEqual(Chart.tasks.mapBy('name'), [ + 'prestart ephemeral: 0', + 'prestart sidecar: 1', + 'main one: 2', + 'main two: 3', + 'poststart sidecar: 4', + 'poststart ephemeral: 5', + 'poststop: 6', + ]); assert.deepEqual(Chart.tasks.mapBy('lifecycle'), [ 'Prestart Task', 'Sidecar Task', 'Main Task', 'Main Task', + 'Sidecar Task', + 'Poststart Task', + 'Poststop Task', ]); - assert.ok(Chart.tasks[0].isPrestart); - assert.ok(Chart.tasks[1].isSidecar); + assert.ok(Chart.tasks[0].isPrestartEphemeral); + assert.ok(Chart.tasks[1].isPrestartSidecar); assert.ok(Chart.tasks[2].isMain); + assert.ok(Chart.tasks[4].isPoststartSidecar); + assert.ok(Chart.tasks[5].isPoststartEphemeral); + assert.ok(Chart.tasks[6].isPoststop); Chart.tasks.forEach(task => { assert.notOk(task.isActive); @@ -72,6 +101,13 @@ module('Integration | Component | lifecycle-chart', function(hooks) { assert.notOk(Chart.isPresent); }); + test('it renders all phases when there are any non-main tasks', async function(assert) { + this.set('tasks', [tasks[0], tasks[6]]); + + await render(hbs``); + assert.ok(Chart.phases.length, 4); + }); + test('it reflects phase and task states when states are passed in', async function(assert) { this.set( 'taskStates', @@ -90,16 +126,64 @@ module('Integration | Component | lifecycle-chart', function(hooks) { assert.notOk(task.isFinished); }); - this.set('taskStates.firstObject.state', 'running'); + // Change poststart-ephemeral to be running + this.set('taskStates.4.state', 'running'); await settled(); - assert.ok(Chart.phases[1].isActive); - assert.ok(Chart.tasks[3].isActive); await componentA11yAudit(this.element, assert); - this.set('taskStates.firstObject.finishedAt', new Date()); + assert.ok(Chart.tasks[5].isActive); + + assert.ok(Chart.phases[1].isActive); + assert.notOk( + Chart.phases[2].isActive, + 'the poststart phase is nested within main and should never have the active class' + ); + + this.set('taskStates.4.finishedAt', new Date()); await settled(); - assert.ok(Chart.tasks[3].isFinished); + assert.ok(Chart.tasks[5].isFinished); + }); + + [ + { + testName: 'expected active phases', + runningTaskNames: ['prestart ephemeral', 'main one', 'poststop'], + activePhaseNames: ['Prestart', 'Main', 'Poststop'], + }, + { + testName: 'sidecar task states don’t affect phase active states', + runningTaskNames: ['prestart sidecar', 'poststart sidecar'], + activePhaseNames: [], + }, + { + testName: 'poststart ephemeral task states affect main phase active state', + runningTaskNames: ['poststart ephemeral'], + activePhaseNames: ['Main'], + }, + ].forEach(async ({ testName, runningTaskNames, activePhaseNames }) => { + test(testName, async function(assert) { + this.set('taskStates', tasks.map(task => ({ task }))); + + await render(hbs``); + + runningTaskNames.forEach(taskName => { + const taskState = this.get('taskStates').find(taskState => + taskState.task.name.includes(taskName) + ); + set(taskState, 'state', 'running'); + }); + + await settled(); + + Chart.phases.forEach(Phase => { + if (activePhaseNames.includes(Phase.name)) { + assert.ok(Phase.isActive, `expected ${Phase.name} not to be active`); + } else { + assert.notOk(Phase.isActive, `expected ${Phase.name} phase not to be active`); + } + }); + }); }); }); diff --git a/ui/tests/pages/components/lifecycle-chart.js b/ui/tests/pages/components/lifecycle-chart.js index 1733849b7..47f1493e4 100644 --- a/ui/tests/pages/components/lifecycle-chart.js +++ b/ui/tests/pages/components/lifecycle-chart.js @@ -19,8 +19,11 @@ export default { isFinished: hasClass('is-finished'), isMain: hasClass('main'), - isPrestart: hasClass('prestart'), - isSidecar: hasClass('sidecar'), + isPrestartEphemeral: hasClass('prestart-ephemeral'), + isPrestartSidecar: hasClass('prestart-sidecar'), + isPoststartEphemeral: hasClass('poststart-ephemeral'), + isPoststartSidecar: hasClass('poststart-sidecar'), + isPoststop: hasClass('poststop'), visit: clickable('a'), }),