mirror of
https://github.com/kemko/nomad.git
synced 2026-01-01 16:05:42 +03:00
[ui] Add "stopped" as a valid status on jobs index/job detail (#23328)
* 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
This commit is contained in:
3
.changelog/23328.txt
Normal file
3
.changelog/23328.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
```release-note:improvement
|
||||
ui: adds a Stopped label for jobs that a user has manually stopped
|
||||
```
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 -
|
||||
*/
|
||||
|
||||
/**
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
@@ -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 -
|
||||
*/
|
||||
|
||||
@@ -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}
|
||||
*/
|
||||
/**
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
@@ -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,
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user