github.com/hernad/nomad@v1.6.112/ui/app/components/job-status/panel/deploying.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 { task } from 'ember-concurrency'; 9 import { tracked } from '@glimmer/tracking'; 10 import { alias } from '@ember/object/computed'; 11 import messageFromAdapterError from 'nomad-ui/utils/message-from-adapter-error'; 12 import { jobAllocStatuses } from '../../../utils/allocation-client-statuses'; 13 14 export default class JobStatusPanelDeployingComponent extends Component { 15 @alias('args.job') job; 16 @alias('args.handleError') handleError = () => {}; 17 18 get allocTypes() { 19 return jobAllocStatuses[this.args.job.type].map((type) => { 20 return { 21 label: type, 22 }; 23 }); 24 } 25 26 @tracked oldVersionAllocBlockIDs = []; 27 28 // Called via did-insert; sets a static array of "outgoing" 29 // allocations we can track throughout a deployment 30 establishOldAllocBlockIDs() { 31 this.oldVersionAllocBlockIDs = this.job.allocations.filter( 32 (a) => a.clientStatus === 'running' && a.isOld 33 ); 34 } 35 36 /** 37 * Promotion of a deployment will error if the canary allocations are not of status "Healthy"; 38 * this function will check for that and disable the promote button if necessary. 39 * @returns {boolean} 40 */ 41 get canariesHealthy() { 42 const relevantAllocs = this.job.allocations.filter( 43 (a) => !a.isOld && a.isCanary && !a.hasBeenRescheduled 44 ); 45 return relevantAllocs.every( 46 (a) => a.clientStatus === 'running' && a.isHealthy 47 ); 48 } 49 50 get someCanariesHaveFailed() { 51 const relevantAllocs = this.job.allocations.filter( 52 (a) => !a.isOld && a.isCanary && !a.hasBeenRescheduled 53 ); 54 return relevantAllocs.some( 55 (a) => 56 a.clientStatus === 'failed' || 57 a.clientStatus === 'lost' || 58 a.isUnhealthy 59 ); 60 } 61 62 @task(function* () { 63 try { 64 yield this.job.latestDeployment.content.promote(); 65 } catch (err) { 66 this.handleError({ 67 title: 'Could Not Promote Deployment', 68 description: messageFromAdapterError(err, 'promote deployments'), 69 }); 70 } 71 }) 72 promote; 73 74 @task(function* () { 75 try { 76 yield this.job.latestDeployment.content.fail(); 77 } catch (err) { 78 this.handleError({ 79 title: 'Could Not Fail Deployment', 80 description: messageFromAdapterError(err, 'fail deployments'), 81 }); 82 } 83 }) 84 fail; 85 86 @alias('job.latestDeployment') deployment; 87 @alias('totalAllocs') desiredTotal; 88 89 get oldVersionAllocBlocks() { 90 return this.job.allocations 91 .filter((allocation) => this.oldVersionAllocBlockIDs.includes(allocation)) 92 .reduce((alloGroups, currentAlloc) => { 93 const status = currentAlloc.clientStatus; 94 95 if (!alloGroups[status]) { 96 alloGroups[status] = { 97 healthy: { nonCanary: [] }, 98 unhealthy: { nonCanary: [] }, 99 health_unknown: { nonCanary: [] }, 100 }; 101 } 102 alloGroups[status].healthy.nonCanary.push(currentAlloc); 103 104 return alloGroups; 105 }, {}); 106 } 107 108 get newVersionAllocBlocks() { 109 let availableSlotsToFill = this.desiredTotal; 110 let allocationsOfDeploymentVersion = this.job.allocations.filter( 111 (a) => !a.isOld 112 ); 113 114 let allocationCategories = this.allocTypes.reduce((categories, type) => { 115 categories[type.label] = { 116 healthy: { canary: [], nonCanary: [] }, 117 unhealthy: { canary: [], nonCanary: [] }, 118 health_unknown: { canary: [], nonCanary: [] }, 119 }; 120 return categories; 121 }, {}); 122 123 for (let alloc of allocationsOfDeploymentVersion) { 124 if (availableSlotsToFill <= 0) { 125 break; 126 } 127 let status = alloc.clientStatus; 128 let canary = alloc.isCanary ? 'canary' : 'nonCanary'; 129 130 // Health status only matters in the context of a "running" allocation. 131 // However, healthy/unhealthy is never purged when an allocation moves to a different clientStatus 132 // Thus, we should only show something as "healthy" in the event that it is running. 133 // Otherwise, we'd have arbitrary groupings based on previous health status. 134 let health = 135 status === 'running' 136 ? alloc.isHealthy 137 ? 'healthy' 138 : alloc.isUnhealthy 139 ? 'unhealthy' 140 : 'health_unknown' 141 : 'health_unknown'; 142 143 if (allocationCategories[status]) { 144 // If status is failed or lost, we only want to show it IF it's used up its restarts/rescheds. 145 // Otherwise, we'd be showing an alloc that had been replaced. 146 if (alloc.willNotRestart) { 147 if (!alloc.willNotReschedule) { 148 // Dont count it 149 continue; 150 } 151 } 152 allocationCategories[status][health][canary].push(alloc); 153 availableSlotsToFill--; 154 } 155 } 156 157 // Fill unplaced slots if availableSlotsToFill > 0 158 if (availableSlotsToFill > 0) { 159 allocationCategories['unplaced'] = { 160 healthy: { canary: [], nonCanary: [] }, 161 unhealthy: { canary: [], nonCanary: [] }, 162 health_unknown: { canary: [], nonCanary: [] }, 163 }; 164 allocationCategories['unplaced']['healthy']['nonCanary'] = Array( 165 availableSlotsToFill 166 ) 167 .fill() 168 .map(() => { 169 return { clientStatus: 'unplaced' }; 170 }); 171 } 172 173 return allocationCategories; 174 } 175 176 get newRunningHealthyAllocBlocks() { 177 return [ 178 ...this.newVersionAllocBlocks['running']['healthy']['canary'], 179 ...this.newVersionAllocBlocks['running']['healthy']['nonCanary'], 180 ]; 181 } 182 183 get newRunningUnhealthyAllocBlocks() { 184 return [ 185 ...this.newVersionAllocBlocks['running']['unhealthy']['canary'], 186 ...this.newVersionAllocBlocks['running']['unhealthy']['nonCanary'], 187 ]; 188 } 189 190 get newRunningHealthUnknownAllocBlocks() { 191 return [ 192 ...this.newVersionAllocBlocks['running']['health_unknown']['canary'], 193 ...this.newVersionAllocBlocks['running']['health_unknown']['nonCanary'], 194 ]; 195 } 196 197 get rescheduledAllocs() { 198 return this.job.allocations.filter((a) => !a.isOld && a.hasBeenRescheduled); 199 } 200 201 get restartedAllocs() { 202 return this.job.allocations.filter((a) => !a.isOld && a.hasBeenRestarted); 203 } 204 205 // #region legend 206 get newAllocsByStatus() { 207 return Object.entries(this.newVersionAllocBlocks).reduce( 208 (counts, [status, healthStatusObj]) => { 209 counts[status] = Object.values(healthStatusObj) 210 .flatMap((canaryStatusObj) => Object.values(canaryStatusObj)) 211 .flatMap((canaryStatusArray) => canaryStatusArray).length; 212 return counts; 213 }, 214 {} 215 ); 216 } 217 218 get newAllocsByCanary() { 219 return Object.values(this.newVersionAllocBlocks) 220 .flatMap((healthStatusObj) => Object.values(healthStatusObj)) 221 .flatMap((canaryStatusObj) => Object.entries(canaryStatusObj)) 222 .reduce((counts, [canaryStatus, items]) => { 223 counts[canaryStatus] = (counts[canaryStatus] || 0) + items.length; 224 return counts; 225 }, {}); 226 } 227 228 get newAllocsByHealth() { 229 return { 230 healthy: this.newRunningHealthyAllocBlocks.length, 231 unhealthy: this.newRunningUnhealthyAllocBlocks.length, 232 health_unknown: this.newRunningHealthUnknownAllocBlocks.length, 233 }; 234 } 235 // #endregion legend 236 237 get oldRunningHealthyAllocBlocks() { 238 return this.oldVersionAllocBlocks.running?.healthy?.nonCanary || []; 239 } 240 get oldCompleteHealthyAllocBlocks() { 241 return this.oldVersionAllocBlocks.complete?.healthy?.nonCanary || []; 242 } 243 244 // TODO: eventually we will want this from a new property on a job. 245 // TODO: consolidate w/ the one in steady.js 246 get totalAllocs() { 247 // v----- Experimental method: Count all allocs. Good for testing but not a realistic representation of "Desired" 248 // return this.allocTypes.reduce((sum, type) => sum + this.args.job[type.property], 0); 249 250 // v----- Realistic method: Tally a job's task groups' "count" property 251 return this.args.job.taskGroups.reduce((sum, tg) => sum + tg.count, 0); 252 } 253 254 get deploymentIsAutoPromoted() { 255 return this.job.latestDeployment?.get('isAutoPromoted'); 256 } 257 258 get oldVersions() { 259 const oldVersions = Object.values(this.oldRunningHealthyAllocBlocks) 260 .map((a) => (!isNaN(a?.jobVersion) ? a.jobVersion : 'unknown')) // "starting" allocs, GC'd allocs, etc. do not have a jobVersion 261 .sort((a, b) => a - b) 262 .reduce((result, item) => { 263 const existingVersion = result.find((v) => v.version === item); 264 if (existingVersion) { 265 existingVersion.allocations.push(item); 266 } else { 267 result.push({ version: item, allocations: [item] }); 268 } 269 return result; 270 }, []); 271 272 return oldVersions; 273 } 274 275 get newVersions() { 276 // Note: it's probably safe to assume all new allocs have the latest job version, but 277 // let's map just in case there's ever a situation with multiple job versions going out 278 // in a deployment for some reason 279 const newVersions = Object.values(this.newVersionAllocBlocks) 280 .flatMap((allocType) => Object.values(allocType)) 281 .flatMap((allocHealth) => Object.values(allocHealth)) 282 .flatMap((allocCanary) => Object.values(allocCanary)) 283 .filter((a) => a.jobVersion && a.jobVersion !== 'unknown') 284 .map((a) => a.jobVersion) 285 .sort((a, b) => a - b) 286 .reduce((result, item) => { 287 const existingVersion = result.find((v) => v.version === item); 288 if (existingVersion) { 289 existingVersion.allocations.push(item); 290 } else { 291 result.push({ version: item, allocations: [item] }); 292 } 293 return result; 294 }, []); 295 return newVersions; 296 } 297 298 get versions() { 299 return [...this.oldVersions, ...this.newVersions]; 300 } 301 }