Skip to content

Commit

Permalink
[ui] Add "stopped" as a valid status on jobs index/job detail (#23328)
Browse files Browse the repository at this point in the history
* Stopped status passed through to the statuses endpoint and observed on job model and steady-state panel

* Status passed to statuses endpoint and test for FE model statuses
  • Loading branch information
philrenaud authored Jun 15, 2024
1 parent d9a10a6 commit 8e589a9
Show file tree
Hide file tree
Showing 9 changed files with 180 additions and 4 deletions.
3 changes: 3 additions & 0 deletions .changelog/23328.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:improvement
ui: adds a Stopped label for jobs that a user has manually stopped
```
2 changes: 2 additions & 0 deletions nomad/job_endpoint_statuses.go
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,8 @@ func jobStatusesJobFromJob(ws memdb.WatchSet, store *state.StateStore, job *stru
GroupCountSum: 0,
ChildStatuses: nil,
LatestDeployment: nil,
Stop: job.Stop,
Status: job.Status,
}

// the GroupCountSum will map to how many allocations we expect to run
Expand Down
2 changes: 2 additions & 0 deletions nomad/structs/job.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,8 @@ type JobStatusesJob struct {
// ParentID is set on child (batch) jobs, specifying the parent job ID
ParentID string
LatestDeployment *JobStatusesLatestDeployment
Stop bool // has the job been manually stopped?
Status string
}

// JobStatusesAlloc contains a subset of Allocation info.
Expand Down
11 changes: 9 additions & 2 deletions ui/app/components/job-status/panel/steady.js
Original file line number Diff line number Diff line change
Expand Up @@ -205,8 +205,8 @@ export default class JobStatusPanelSteadyComponent extends Component {

/**
* @typedef {Object} CurrentStatus
* @property {"Healthy"|"Failed"|"Degraded"|"Recovering"|"Complete"|"Running"} label - The current status of the job
* @property {"highlight"|"success"|"warning"|"critical"} state -
* @property {"Healthy"|"Failed"|"Degraded"|"Recovering"|"Complete"|"Running"|"Stopped"} label - The current status of the job
* @property {"highlight"|"success"|"warning"|"critical"|"neutral"} state -
*/

/**
Expand All @@ -217,6 +217,13 @@ export default class JobStatusPanelSteadyComponent extends Component {
// If all allocs are running, the job is Healthy
const totalAllocs = this.totalAllocs;

if (this.job.status === 'dead' && this.job.stopped) {
return {
label: 'Stopped',
state: 'neutral',
};
}

if (this.job.type === 'batch' || this.job.type === 'sysbatch') {
// If all the allocs are complete, the job is Complete
const completeAllocs = this.allocBlocks.complete?.healthy?.nonCanary;
Expand Down
12 changes: 11 additions & 1 deletion ui/app/models/job.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export default class Job extends Model {
@attr('number') modifyIndex;
@attr('date') submitTime;
@attr('string') nodePool; // Jobs are related to Node Pools either directly or via its Namespace, but no relationship.
@attr('boolean') stopped;
@attr() ui;

@attr('number') groupCountSum;
Expand Down Expand Up @@ -89,7 +90,7 @@ export default class Job extends Model {

/**
* @typedef {Object} CurrentStatus
* @property {"Healthy"|"Failed"|"Deploying"|"Degraded"|"Recovering"|"Complete"|"Running"|"Removed"} label - The current status of the job
* @property {"Healthy"|"Failed"|"Deploying"|"Degraded"|"Recovering"|"Complete"|"Running"|"Removed"|"Stopped"} label - The current status of the job
* @property {"highlight"|"success"|"warning"|"critical"|"neutral"} state -
*/

Expand Down Expand Up @@ -224,6 +225,7 @@ export default class Job extends Model {
* - Degraded: A deployment is not taking place, and some allocations are failed, lost, or unplaced
* - Failed: All allocations are failed, lost, or unplaced
* - Removed: The job appeared in our initial query, but has since been garbage collected
* - Stopped: The job has been manually stopped (and not purged or yet garbage collected) by a user
* @returns {CurrentStatus}
*/
/**
Expand All @@ -238,6 +240,14 @@ export default class Job extends Model {
return { label: 'Deploying', state: 'highlight' };
}

// if manually stopped by a user:
if (this.status === 'dead' && this.stopped) {
return {
label: 'Stopped',
state: 'neutral',
};
}

// If the job was requested initially, but a subsequent request for it was
// not found, we can remove links to it but maintain its presence in the list
// until the user specifies they want a refresh
Expand Down
6 changes: 6 additions & 0 deletions ui/app/serializers/job.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,12 @@ export default class JobSerializer extends ApplicationSerializer {
});
}

// job.stop is reserved as a method (points to adapter method) so we rename it here
if (hash.Stop) {
hash.Stopped = hash.Stop;
delete hash.Stop;
}

return super.normalize(typeHash, hash);
}

Expand Down
2 changes: 1 addition & 1 deletion ui/mirage/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,7 @@ export default function () {
});
job.ChildStatuses = children ? children.mapBy('Status') : null;
job.Datacenters = j.Datacenters;
job.DeploymentID = j.DeploymentID;
job.LatestDeployment = j.LatestDeployment;
job.GroupCountSum = j.TaskGroups.mapBy('Count').reduce(
(a, b) => a + b,
0
Expand Down
14 changes: 14 additions & 0 deletions ui/mirage/factories/job.js
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,8 @@ export default Factory.extend({
// When true, the job will simulate a "scheduled" block's paused state
withPausedTasks: false,

latestDeployment: null,

afterCreate(job, server) {
Ember.assert(
'[Mirage] No node pools! make sure node pools are created before jobs',
Expand Down Expand Up @@ -319,6 +321,18 @@ export default Factory.extend({
});
}

if (job.activeDeployment) {
job.latestDeployment = {
IsActive: true,
Status: 'running',
StatusDescription: 'Deployment is running',
RequiresPromotion: false,
AllAutoPromote: true,
JobVersion: 1,
ID: faker.random.uuid(),
};
}

if (!job.shallow) {
const knownEvaluationProperties = {
jobId: job.id,
Expand Down
132 changes: 132 additions & 0 deletions ui/tests/acceptance/jobs-list-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -554,6 +554,138 @@ module('Acceptance | jobs list', function (hooks) {
localStorage.removeItem('nomadPageSize');
});

test('aggregateAllocStatus reflects job status correctly', async function (assert) {
const defaultJobParams = {
createAllocations: true,
shallow: true,
resourceSpec: Array(1).fill('M: 257, C: 500'),
groupAllocCount: 10,
noActiveDeployment: true,
noFailedPlacements: true,
status: 'running',
type: 'service',
};

server.create('job', {
...defaultJobParams,
id: 'healthy-job',
allocStatusDistribution: {
running: 1,
},
});

server.create('job', {
...defaultJobParams,
id: 'degraded-job',
allocStatusDistribution: {
running: 0.9,
failed: 0.1,
},
});

server.create('job', {
...defaultJobParams,
id: 'recovering-job',
allocStatusDistribution: {
running: 0.9,
pending: 0.1,
},
});

server.create('job', {
...defaultJobParams,
id: 'completed-job',
allocStatusDistribution: {
complete: 1,
},
type: 'batch',
});

server.create('job', {
...defaultJobParams,
id: 'running-job',
allocStatusDistribution: {
running: 1,
},
type: 'batch',
});

server.create('job', {
...defaultJobParams,
id: 'failed-job',
allocStatusDistribution: {
failed: 1,
},
});

server.create('job', {
...defaultJobParams,
id: 'failed-garbage-collected-job',
type: 'service',
allocStatusDistribution: {
unknown: 1,
},
status: 'running',
});

server.create('job', {
...defaultJobParams,
id: 'stopped-job',
type: 'service',
allocStatusDistribution: {
unknown: 1,
},
status: 'dead',
stopped: true,
});

server.create('job', {
...defaultJobParams,
id: 'deploying-job',
allocStatusDistribution: {
running: 0.5,
pending: 0.5,
},
noActiveDeployment: false,
activeDeployment: true,
});

await JobsList.visit();

assert
.dom('[data-test-job-row="healthy-job"] [data-test-job-status]')
.hasText('Healthy', 'Healthy job is healthy');
// and all the rest
assert
.dom('[data-test-job-row="degraded-job"] [data-test-job-status]')
.hasText('Degraded', 'Degraded job is degraded');
assert
.dom('[data-test-job-row="recovering-job"] [data-test-job-status]')
.hasText('Recovering', 'Recovering job is recovering');
assert
.dom('[data-test-job-row="completed-job"] [data-test-job-status]')
.hasText('Complete', 'Completed job is completed');
assert
.dom('[data-test-job-row="running-job"] [data-test-job-status]')
.hasText('Running', 'Running job is running');
assert
.dom('[data-test-job-row="failed-job"] [data-test-job-status]')
.hasText('Failed', 'Failed job is failed');
assert
.dom(
'[data-test-job-row="failed-garbage-collected-job"] [data-test-job-status]'
)
.hasText('Failed', 'Failed garbage collected job is failed');
assert
.dom('[data-test-job-row="stopped-job"] [data-test-job-status]')
.hasText('Stopped', 'Stopped job is stopped');
assert
.dom('[data-test-job-row="deploying-job"] [data-test-job-status]')
.hasText('Deploying', 'Deploying job is deploying');

await percySnapshot(assert);
});

test('Jobs with schedule blocks indicate when a task is paused', async function (assert) {
server.create('job', {
name: 'regular-job-1',
Expand Down

0 comments on commit 8e589a9

Please sign in to comment.