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