github.com/hernad/nomad@v1.6.112/ui/app/components/job-status/panel/steady.js (about) 1 /** 2 * Copyright (c) HashiCorp, Inc. 3 * SPDX-License-Identifier: MPL-2.0 4 */ 5 6 // @ts-check 7 import Component from '@glimmer/component'; 8 import { alias } from '@ember/object/computed'; 9 import { jobAllocStatuses } from '../../../utils/allocation-client-statuses'; 10 11 export default class JobStatusPanelSteadyComponent extends Component { 12 @alias('args.job') job; 13 14 get allocTypes() { 15 return jobAllocStatuses[this.args.job.type].map((type) => { 16 return { 17 label: type, 18 }; 19 }); 20 } 21 22 /** 23 * @typedef {Object} HealthStatus 24 * @property {Array} nonCanary 25 * @property {Array} canary 26 */ 27 28 /** 29 * @typedef {Object} AllocationStatus 30 * @property {HealthStatus} healthy 31 * @property {HealthStatus} unhealthy 32 * @property {HealthStatus} health unknown 33 */ 34 35 /** 36 * @typedef {Object} AllocationBlock 37 * @property {AllocationStatus} [running] 38 * @property {AllocationStatus} [pending] 39 * @property {AllocationStatus} [failed] 40 * @property {AllocationStatus} [lost] 41 * @property {AllocationStatus} [unplaced] 42 * @property {AllocationStatus} [complete] 43 */ 44 45 /** 46 * Looks through running/pending allocations with the aim of filling up your desired number of allocations. 47 * If any desired remain, it will walk backwards through job versions and other allocation types to build 48 * a picture of the job's overall status. 49 * 50 * @returns {AllocationBlock} An object containing healthy non-canary allocations 51 * for each clientStatus. 52 */ 53 get allocBlocks() { 54 let availableSlotsToFill = this.totalAllocs; 55 56 // Initialize allocationsOfShowableType with empty arrays for each clientStatus 57 /** 58 * @type {AllocationBlock} 59 */ 60 let allocationsOfShowableType = this.allocTypes.reduce( 61 (accumulator, type) => { 62 accumulator[type.label] = { healthy: { nonCanary: [] } }; 63 return accumulator; 64 }, 65 {} 66 ); 67 68 // First accumulate the Running/Pending allocations 69 for (const alloc of this.job.allocations.filter( 70 (a) => a.clientStatus === 'running' || a.clientStatus === 'pending' 71 )) { 72 if (availableSlotsToFill === 0) { 73 break; 74 } 75 76 const status = alloc.clientStatus; 77 allocationsOfShowableType[status].healthy.nonCanary.push(alloc); 78 availableSlotsToFill--; 79 } 80 81 // Sort all allocs by jobVersion in descending order 82 const sortedAllocs = this.args.job.allocations 83 .filter( 84 (a) => a.clientStatus !== 'running' && a.clientStatus !== 'pending' 85 ) 86 .sort((a, b) => { 87 // First sort by jobVersion 88 if (a.jobVersion > b.jobVersion) return 1; 89 if (a.jobVersion < b.jobVersion) return -1; 90 91 // If jobVersion is the same, sort by status order 92 if (a.jobVersion === b.jobVersion) { 93 return ( 94 jobAllocStatuses[this.args.job.type].indexOf(b.clientStatus) - 95 jobAllocStatuses[this.args.job.type].indexOf(a.clientStatus) 96 ); 97 } else { 98 return 0; 99 } 100 }) 101 .reverse(); 102 103 // Iterate over the sorted allocs 104 for (const alloc of sortedAllocs) { 105 if (availableSlotsToFill === 0) { 106 break; 107 } 108 109 const status = alloc.clientStatus; 110 // If the alloc has another clientStatus, add it to the corresponding list 111 // as long as we haven't reached the totalAllocs limit for that clientStatus 112 if ( 113 this.allocTypes.map(({ label }) => label).includes(status) && 114 allocationsOfShowableType[status].healthy.nonCanary.length < 115 this.totalAllocs 116 ) { 117 allocationsOfShowableType[status].healthy.nonCanary.push(alloc); 118 availableSlotsToFill--; 119 } 120 } 121 122 // Handle unplaced allocs 123 if (availableSlotsToFill > 0) { 124 allocationsOfShowableType['unplaced'] = { 125 healthy: { 126 nonCanary: Array(availableSlotsToFill) 127 .fill() 128 .map(() => { 129 return { clientStatus: 'unplaced' }; 130 }), 131 }, 132 }; 133 } 134 135 return allocationsOfShowableType; 136 } 137 138 get nodes() { 139 return this.args.nodes; 140 } 141 142 get totalAllocs() { 143 if (this.args.job.type === 'service' || this.args.job.type === 'batch') { 144 return this.args.job.taskGroups.reduce((sum, tg) => sum + tg.count, 0); 145 } else if (this.atMostOneAllocPerNode) { 146 return this.args.job.allocations.uniqBy('nodeID').length; 147 } else { 148 return this.args.job.count; // TODO: this is probably not the correct totalAllocs count for any type. 149 } 150 } 151 152 get totalNonCompletedAllocs() { 153 return this.totalAllocs - this.completedAllocs.length; 154 } 155 156 get allAllocsComplete() { 157 return this.completedAllocs.length && this.totalNonCompletedAllocs === 0; 158 } 159 160 get atMostOneAllocPerNode() { 161 return this.args.job.type === 'system' || this.args.job.type === 'sysbatch'; 162 } 163 164 get versions() { 165 const versions = Object.values(this.allocBlocks) 166 .flatMap((allocType) => Object.values(allocType)) 167 .flatMap((allocHealth) => Object.values(allocHealth)) 168 .flatMap((allocCanary) => Object.values(allocCanary)) 169 .map((a) => (!isNaN(a?.jobVersion) ? a.jobVersion : 'unknown')) // "starting" allocs, GC'd allocs, etc. do not have a jobVersion 170 .sort((a, b) => a - b) 171 .reduce((result, item) => { 172 const existingVersion = result.find((v) => v.version === item); 173 if (existingVersion) { 174 existingVersion.allocations.push(item); 175 } else { 176 result.push({ version: item, allocations: [item] }); 177 } 178 return result; 179 }, []); 180 return versions; 181 } 182 183 get rescheduledAllocs() { 184 return this.job.allocations.filter((a) => !a.isOld && a.hasBeenRescheduled); 185 } 186 187 get restartedAllocs() { 188 return this.job.allocations.filter((a) => !a.isOld && a.hasBeenRestarted); 189 } 190 191 get completedAllocs() { 192 return this.job.allocations.filter( 193 (a) => !a.isOld && a.clientStatus === 'complete' 194 ); 195 } 196 197 get supportsRescheduling() { 198 return this.job.type !== 'system'; 199 } 200 201 get latestVersionAllocations() { 202 return this.job.allocations.filter((a) => !a.isOld); 203 } 204 205 /** 206 * @typedef {Object} CurrentStatus 207 * @property {"Healthy"|"Failed"|"Degraded"|"Recovering"|"Complete"|"Running"} label - The current status of the job 208 * @property {"highlight"|"success"|"warning"|"critical"} state - 209 */ 210 211 /** 212 * A general assessment for how a job is going, in a non-deployment state 213 * @returns {CurrentStatus} 214 */ 215 get currentStatus() { 216 // If all allocs are running, the job is Healthy 217 const totalAllocs = this.totalAllocs; 218 219 if (this.job.type === 'batch' || this.job.type === 'sysbatch') { 220 // If all the allocs are complete, the job is Complete 221 const completeAllocs = this.allocBlocks.complete?.healthy?.nonCanary; 222 if (completeAllocs?.length === totalAllocs) { 223 return { label: 'Complete', state: 'success' }; 224 } 225 226 // If any allocations are running the job is "Running" 227 const healthyAllocs = this.allocBlocks.running?.healthy?.nonCanary; 228 if (healthyAllocs?.length + completeAllocs?.length === totalAllocs) { 229 return { label: 'Running', state: 'success' }; 230 } 231 } 232 233 const healthyAllocs = this.allocBlocks.running?.healthy?.nonCanary; 234 if (healthyAllocs?.length === totalAllocs) { 235 return { label: 'Healthy', state: 'success' }; 236 } 237 238 // If any allocations are pending the job is "Recovering" 239 const pendingAllocs = this.allocBlocks.pending?.healthy?.nonCanary; 240 if (pendingAllocs?.length > 0) { 241 return { label: 'Recovering', state: 'highlight' }; 242 } 243 244 // If any allocations are failed, lost, or unplaced in a steady state, the job is "Degraded" 245 const failedOrLostAllocs = [ 246 ...this.allocBlocks.failed?.healthy?.nonCanary, 247 ...this.allocBlocks.lost?.healthy?.nonCanary, 248 ...this.allocBlocks.unplaced?.healthy?.nonCanary, 249 ]; 250 251 if (failedOrLostAllocs.length === totalAllocs) { 252 return { label: 'Failed', state: 'critical' }; 253 } else { 254 return { label: 'Degraded', state: 'warning' }; 255 } 256 } 257 }