[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:
Phil Renaud
2024-06-14 23:33:00 -04:00
committed by GitHub
parent d9a10a6298
commit 8e589a9319
9 changed files with 180 additions and 4 deletions

3
.changelog/23328.txt Normal file
View File

@@ -0,0 +1,3 @@
```release-note:improvement
ui: adds a Stopped label for jobs that a user has manually stopped
```

View File

@@ -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

View File

@@ -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.

View File

@@ -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;

View File

@@ -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

View File

@@ -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);
}

View File

@@ -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

View File

@@ -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,

View File

@@ -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',