github.com/iqoqo/nomad@v0.11.3-0.20200911112621-d7021c74d101/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.toArray().some(evaluation => evaluation.get('isBlocked')); 148 }), 149 150 hasPlacementFailures: and('latestFailureEvaluation', 'hasBlockedEvaluation'), 151 152 latestEvaluation: computed('evaluations.@each.modifyIndex', 'evaluations.isPending', function() { 153 const evaluations = this.evaluations; 154 if (!evaluations || evaluations.get('isPending')) { 155 return null; 156 } 157 return evaluations.sortBy('modifyIndex').get('lastObject'); 158 }), 159 160 latestFailureEvaluation: computed( 161 'evaluations.@each.modifyIndex', 162 'evaluations.isPending', 163 function() { 164 const evaluations = this.evaluations; 165 if (!evaluations || evaluations.get('isPending')) { 166 return null; 167 } 168 169 const failureEvaluations = evaluations.filterBy('hasPlacementFailures'); 170 if (failureEvaluations) { 171 return failureEvaluations.sortBy('modifyIndex').get('lastObject'); 172 } 173 } 174 ), 175 176 supportsDeployments: equal('type', 'service'), 177 178 latestDeployment: belongsTo('deployment', { inverse: 'jobForLatest' }), 179 180 runningDeployment: computed('latestDeployment', 'latestDeployment.isRunning', function() { 181 const latest = this.latestDeployment; 182 if (latest.get('isRunning')) return latest; 183 }), 184 185 fetchRawDefinition() { 186 return this.store.adapterFor('job').fetchRawDefinition(this); 187 }, 188 189 forcePeriodic() { 190 return this.store.adapterFor('job').forcePeriodic(this); 191 }, 192 193 stop() { 194 return this.store.adapterFor('job').stop(this); 195 }, 196 197 plan() { 198 assert('A job must be parsed before planned', this._newDefinitionJSON); 199 return this.store.adapterFor('job').plan(this); 200 }, 201 202 run() { 203 assert('A job must be parsed before ran', this._newDefinitionJSON); 204 return this.store.adapterFor('job').run(this); 205 }, 206 207 update() { 208 assert('A job must be parsed before updated', this._newDefinitionJSON); 209 return this.store.adapterFor('job').update(this); 210 }, 211 212 parse() { 213 const definition = this._newDefinition; 214 let promise; 215 216 try { 217 // If the definition is already JSON then it doesn't need to be parsed. 218 const json = JSON.parse(definition); 219 this.set('_newDefinitionJSON', json); 220 221 // You can't set the ID of a record that already exists 222 if (this.isNew) { 223 this.setIdByPayload(json); 224 } 225 226 promise = RSVP.resolve(definition); 227 } catch (err) { 228 // If the definition is invalid JSON, assume it is HCL. If it is invalid 229 // in anyway, the parse endpoint will throw an error. 230 promise = this.store 231 .adapterFor('job') 232 .parse(this._newDefinition) 233 .then(response => { 234 this.set('_newDefinitionJSON', response); 235 this.setIdByPayload(response); 236 }); 237 } 238 239 return promise; 240 }, 241 242 setIdByPayload(payload) { 243 const namespace = payload.Namespace || 'default'; 244 const id = payload.Name; 245 246 this.set('plainId', id); 247 this.set('_idBeforeSaving', JSON.stringify([id, namespace])); 248 249 const namespaceRecord = this.store.peekRecord('namespace', namespace); 250 if (namespaceRecord) { 251 this.set('namespace', namespaceRecord); 252 } 253 }, 254 255 resetId() { 256 this.set('id', JSON.stringify([this.plainId, this.get('namespace.name') || 'default'])); 257 }, 258 259 statusClass: computed('status', function() { 260 const classMap = { 261 pending: 'is-pending', 262 running: 'is-primary', 263 dead: 'is-light', 264 }; 265 266 return classMap[this.status] || 'is-dark'; 267 }), 268 269 payload: attr('string'), 270 decodedPayload: computed('payload', function() { 271 // Lazily decode the base64 encoded payload 272 return window.atob(this.payload || ''); 273 }), 274 275 // An arbitrary HCL or JSON string that is used by the serializer to plan 276 // and run this job. Used for both new job models and saved job models. 277 _newDefinition: attr('string'), 278 279 // The new definition may be HCL, in which case the API will need to parse the 280 // spec first. In order to preserve both the original HCL and the parsed response 281 // that will be submitted to the create job endpoint, another prop is necessary. 282 _newDefinitionJSON: attr('string'), 283 });