github.com/Ilhicas/nomad@v1.0.4-0.20210304152020-e86851182bc3/ui/app/models/job.js (about) 1 import { alias, equal, or, and, mapBy } from '@ember/object/computed'; 2 import { computed } from '@ember/object'; 3 import Model from '@ember-data/model'; 4 import { attr, belongsTo, hasMany } from '@ember-data/model'; 5 import { fragmentArray } from 'ember-data-model-fragments/attributes'; 6 import RSVP from 'rsvp'; 7 import { assert } from '@ember/debug'; 8 import classic from 'ember-classic-decorator'; 9 10 const JOB_TYPES = ['service', 'batch', 'system']; 11 12 @classic 13 export default class Job extends Model { 14 @attr('string') region; 15 @attr('string') name; 16 @attr('string') plainId; 17 @attr('string') type; 18 @attr('number') priority; 19 @attr('boolean') allAtOnce; 20 21 @attr('string') status; 22 @attr('string') statusDescription; 23 @attr('number') createIndex; 24 @attr('number') modifyIndex; 25 @attr('date') submitTime; 26 27 // True when the job is the parent periodic or parameterized jobs 28 // Instances of periodic or parameterized jobs are false for both properties 29 @attr('boolean') periodic; 30 @attr('boolean') parameterized; 31 @attr('boolean') dispatched; 32 33 @attr() periodicDetails; 34 @attr() parameterizedDetails; 35 36 @computed('periodic', 'parameterized', 'dispatched') 37 get hasChildren() { 38 return this.periodic || (this.parameterized && !this.dispatched); 39 } 40 41 @belongsTo('job', { inverse: 'children' }) parent; 42 @hasMany('job', { inverse: 'parent' }) children; 43 44 // The parent job name is prepended to child launch job names 45 @computed('name', 'parent.content') 46 get trimmedName() { 47 return this.get('parent.content') ? this.name.replace(/.+?\//, '') : this.name; 48 } 49 50 // A composite of type and other job attributes to determine 51 // a better type descriptor for human interpretation rather 52 // than for scheduling. 53 @computed('type', 'periodic', 'parameterized') 54 get displayType() { 55 if (this.periodic) { 56 return 'periodic'; 57 } else if (this.parameterized) { 58 return 'parameterized'; 59 } 60 return this.type; 61 } 62 63 // A composite of type and other job attributes to determine 64 // type for templating rather than scheduling 65 @computed('type', 'periodic', 'parameterized', 'parent.{periodic,parameterized}') 66 get templateType() { 67 const type = this.type; 68 69 if (this.get('parent.periodic')) { 70 return 'periodic-child'; 71 } else if (this.get('parent.parameterized')) { 72 return 'parameterized-child'; 73 } else if (this.periodic) { 74 return 'periodic'; 75 } else if (this.parameterized) { 76 return 'parameterized'; 77 } else if (JOB_TYPES.includes(type)) { 78 // Guard against the API introducing a new type before the UI 79 // is prepared to handle it. 80 return this.type; 81 } 82 83 // A fail-safe in the event the API introduces a new type. 84 return 'service'; 85 } 86 87 @attr() datacenters; 88 @fragmentArray('task-group', { defaultValue: () => [] }) taskGroups; 89 @belongsTo('job-summary') summary; 90 91 // A job model created from the jobs list response will be lacking 92 // task groups. This is an indicator that it needs to be reloaded 93 // if task group information is important. 94 @equal('taskGroups.length', 0) isPartial; 95 96 // If a job has only been loaded through the list request, the task groups 97 // are still unknown. However, the count of task groups is available through 98 // the job-summary model which is embedded in the jobs list response. 99 @or('taskGroups.length', 'taskGroupSummaries.length') taskGroupCount; 100 101 // Alias through to the summary, as if there was no relationship 102 @alias('summary.taskGroupSummaries') taskGroupSummaries; 103 @alias('summary.queuedAllocs') queuedAllocs; 104 @alias('summary.startingAllocs') startingAllocs; 105 @alias('summary.runningAllocs') runningAllocs; 106 @alias('summary.completeAllocs') completeAllocs; 107 @alias('summary.failedAllocs') failedAllocs; 108 @alias('summary.lostAllocs') lostAllocs; 109 @alias('summary.totalAllocs') totalAllocs; 110 @alias('summary.pendingChildren') pendingChildren; 111 @alias('summary.runningChildren') runningChildren; 112 @alias('summary.deadChildren') deadChildren; 113 @alias('summary.totalChildren') totalChildren; 114 115 @attr('number') version; 116 117 @hasMany('job-versions') versions; 118 @hasMany('allocations') allocations; 119 @hasMany('deployments') deployments; 120 @hasMany('evaluations') evaluations; 121 @belongsTo('namespace') namespace; 122 @belongsTo('job-scale') scaleState; 123 124 @hasMany('recommendation-summary') recommendationSummaries; 125 126 @computed('taskGroups.@each.drivers') 127 get drivers() { 128 return this.taskGroups 129 .mapBy('drivers') 130 .reduce((all, drivers) => { 131 all.push(...drivers); 132 return all; 133 }, []) 134 .uniq(); 135 } 136 137 @mapBy('allocations', 'unhealthyDrivers') allocationsUnhealthyDrivers; 138 139 // Getting all unhealthy drivers for a job can be incredibly expensive if the job 140 // has many allocations. This can lead to making an API request for many nodes. 141 @computed('allocations', 'allocationsUnhealthyDrivers.[]') 142 get unhealthyDrivers() { 143 return this.allocations 144 .mapBy('unhealthyDrivers') 145 .reduce((all, drivers) => { 146 all.push(...drivers); 147 return all; 148 }, []) 149 .uniq(); 150 } 151 152 @computed('evaluations.@each.isBlocked') 153 get hasBlockedEvaluation() { 154 return this.evaluations.toArray().some(evaluation => evaluation.get('isBlocked')); 155 } 156 157 @and('latestFailureEvaluation', 'hasBlockedEvaluation') hasPlacementFailures; 158 159 @computed('evaluations.{@each.modifyIndex,isPending}') 160 get latestEvaluation() { 161 const evaluations = this.evaluations; 162 if (!evaluations || evaluations.get('isPending')) { 163 return null; 164 } 165 return evaluations.sortBy('modifyIndex').get('lastObject'); 166 } 167 168 @computed('evaluations.{@each.modifyIndex,isPending}') 169 get latestFailureEvaluation() { 170 const evaluations = this.evaluations; 171 if (!evaluations || evaluations.get('isPending')) { 172 return null; 173 } 174 175 const failureEvaluations = evaluations.filterBy('hasPlacementFailures'); 176 if (failureEvaluations) { 177 return failureEvaluations.sortBy('modifyIndex').get('lastObject'); 178 } 179 180 return undefined; 181 } 182 183 @equal('type', 'service') supportsDeployments; 184 185 @belongsTo('deployment', { inverse: 'jobForLatest' }) latestDeployment; 186 187 @computed('latestDeployment', 'latestDeployment.isRunning') 188 get runningDeployment() { 189 const latest = this.latestDeployment; 190 if (latest.get('isRunning')) return latest; 191 return undefined; 192 } 193 194 fetchRawDefinition() { 195 return this.store.adapterFor('job').fetchRawDefinition(this); 196 } 197 198 forcePeriodic() { 199 return this.store.adapterFor('job').forcePeriodic(this); 200 } 201 202 stop() { 203 return this.store.adapterFor('job').stop(this); 204 } 205 206 plan() { 207 assert('A job must be parsed before planned', this._newDefinitionJSON); 208 return this.store.adapterFor('job').plan(this); 209 } 210 211 run() { 212 assert('A job must be parsed before ran', this._newDefinitionJSON); 213 return this.store.adapterFor('job').run(this); 214 } 215 216 update() { 217 assert('A job must be parsed before updated', this._newDefinitionJSON); 218 return this.store.adapterFor('job').update(this); 219 } 220 221 parse() { 222 const definition = this._newDefinition; 223 let promise; 224 225 try { 226 // If the definition is already JSON then it doesn't need to be parsed. 227 const json = JSON.parse(definition); 228 this.set('_newDefinitionJSON', json); 229 230 // You can't set the ID of a record that already exists 231 if (this.isNew) { 232 this.setIdByPayload(json); 233 } 234 235 promise = RSVP.resolve(definition); 236 } catch (err) { 237 // If the definition is invalid JSON, assume it is HCL. If it is invalid 238 // in anyway, the parse endpoint will throw an error. 239 promise = this.store 240 .adapterFor('job') 241 .parse(this._newDefinition) 242 .then(response => { 243 this.set('_newDefinitionJSON', response); 244 this.setIdByPayload(response); 245 }); 246 } 247 248 return promise; 249 } 250 251 scale(group, count, message) { 252 if (message == null) message = `Manually scaled to ${count} from the Nomad UI`; 253 return this.store.adapterFor('job').scale(this, group, count, message); 254 } 255 256 setIdByPayload(payload) { 257 const namespace = payload.Namespace || 'default'; 258 const id = payload.Name; 259 260 this.set('plainId', id); 261 this.set('_idBeforeSaving', JSON.stringify([id, namespace])); 262 263 const namespaceRecord = this.store.peekRecord('namespace', namespace); 264 if (namespaceRecord) { 265 this.set('namespace', namespaceRecord); 266 } 267 } 268 269 resetId() { 270 this.set('id', JSON.stringify([this.plainId, this.get('namespace.name') || 'default'])); 271 } 272 273 @computed('status') 274 get statusClass() { 275 const classMap = { 276 pending: 'is-pending', 277 running: 'is-primary', 278 dead: 'is-light', 279 }; 280 281 return classMap[this.status] || 'is-dark'; 282 } 283 284 @attr('string') payload; 285 286 @computed('payload') 287 get decodedPayload() { 288 // Lazily decode the base64 encoded payload 289 return window.atob(this.payload || ''); 290 } 291 292 // An arbitrary HCL or JSON string that is used by the serializer to plan 293 // and run this job. Used for both new job models and saved job models. 294 @attr('string') _newDefinition; 295 296 // The new definition may be HCL, in which case the API will need to parse the 297 // spec first. In order to preserve both the original HCL and the parsed response 298 // that will be submitted to the create job endpoint, another prop is necessary. 299 @attr('string') _newDefinitionJSON; 300 }