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