From 122a074fe6d7bda6807a3999c1ee52f76509253f Mon Sep 17 00:00:00 2001 From: temp Date: Fri, 14 Jun 2024 23:33:20 -0400 Subject: [PATCH 1/2] no-op commit due to failed cherry-picking From 9488d91a2909d89804f2699d3e8319aeec60c5c0 Mon Sep 17 00:00:00 2001 From: Phil Renaud Date: Fri, 14 Jun 2024 23:33:00 -0400 Subject: [PATCH 2/2] [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 --- .changelog/23328.txt | 3 + nomad/job_endpoint_statuses.go | 2 + nomad/structs/job.go | 2 + ui/app/components/job-status/panel/steady.js | 11 +- ui/app/models/job.js | 12 +- ui/app/serializers/job.js | 6 + ui/mirage/config.js | 2 +- ui/mirage/factories/job.js | 14 ++ ui/tests/acceptance/jobs-list-test.js | 132 +++++++++++++++++++ 9 files changed, 180 insertions(+), 4 deletions(-) create mode 100644 .changelog/23328.txt diff --git a/.changelog/23328.txt b/.changelog/23328.txt new file mode 100644 index 00000000000..3e0f8928010 --- /dev/null +++ b/.changelog/23328.txt @@ -0,0 +1,3 @@ +```release-note:improvement +ui: adds a Stopped label for jobs that a user has manually stopped +``` diff --git a/nomad/job_endpoint_statuses.go b/nomad/job_endpoint_statuses.go index 32b051271a3..7b54c2b831f 100644 --- a/nomad/job_endpoint_statuses.go +++ b/nomad/job_endpoint_statuses.go @@ -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 diff --git a/nomad/structs/job.go b/nomad/structs/job.go index 981992fbea0..93ca19a9f75 100644 --- a/nomad/structs/job.go +++ b/nomad/structs/job.go @@ -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. diff --git a/ui/app/components/job-status/panel/steady.js b/ui/app/components/job-status/panel/steady.js index ac03654b7dd..22111e454ba 100644 --- a/ui/app/components/job-status/panel/steady.js +++ b/ui/app/components/job-status/panel/steady.js @@ -201,8 +201,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 - */ /** @@ -213,6 +213,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; diff --git a/ui/app/models/job.js b/ui/app/models/job.js index 629ab638f6c..94ff651d286 100644 --- a/ui/app/models/job.js +++ b/ui/app/models/job.js @@ -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 diff --git a/ui/app/serializers/job.js b/ui/app/serializers/job.js index a7876c435bb..940e558b82b 100644 --- a/ui/app/serializers/job.js +++ b/ui/app/serializers/job.js @@ -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); } diff --git a/ui/mirage/config.js b/ui/mirage/config.js index 3c79d9fcc40..4c6e67b6af2 100644 --- a/ui/mirage/config.js +++ b/ui/mirage/config.js @@ -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 diff --git a/ui/mirage/factories/job.js b/ui/mirage/factories/job.js index ebe84f36323..89bc6903de4 100644 --- a/ui/mirage/factories/job.js +++ b/ui/mirage/factories/job.js @@ -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, diff --git a/ui/tests/acceptance/jobs-list-test.js b/ui/tests/acceptance/jobs-list-test.js index 3f978cae98d..e27f24f7f65 100644 --- a/ui/tests/acceptance/jobs-list-test.js +++ b/ui/tests/acceptance/jobs-list-test.js @@ -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',