github.com/hernad/nomad@v1.6.112/ui/app/models/allocation.js (about) 1 /** 2 * Copyright (c) HashiCorp, Inc. 3 * SPDX-License-Identifier: MPL-2.0 4 */ 5 6 import { inject as service } from '@ember/service'; 7 import { computed } from '@ember/object'; 8 import { equal, none } from '@ember/object/computed'; 9 import Model from '@ember-data/model'; 10 import { attr, belongsTo, hasMany } from '@ember-data/model'; 11 import { fragment, fragmentArray } from 'ember-data-model-fragments/attributes'; 12 import isEqual from 'lodash.isequal'; 13 import intersection from 'lodash.intersection'; 14 import shortUUIDProperty from '../utils/properties/short-uuid'; 15 import classic from 'ember-classic-decorator'; 16 17 const STATUS_ORDER = { 18 pending: 1, 19 running: 2, 20 complete: 3, 21 unknown: 4, 22 failed: 5, 23 lost: 6, 24 }; 25 26 @classic 27 export default class Allocation extends Model { 28 @service token; 29 @service store; 30 31 @shortUUIDProperty('id') shortId; 32 @belongsTo('job') job; 33 @belongsTo('node') node; 34 @attr('string') namespace; 35 @attr('string') nodeID; 36 @attr('string') name; 37 @attr('string') taskGroupName; 38 @fragment('resources') resources; 39 @fragment('resources') allocatedResources; 40 @attr('number') jobVersion; 41 42 @attr('number') modifyIndex; 43 @attr('date') modifyTime; 44 45 @attr('number') createIndex; 46 @attr('date') createTime; 47 48 @attr('string') clientStatus; 49 @attr('string') desiredStatus; 50 @attr() desiredTransition; 51 @attr() deploymentStatus; 52 53 get isCanary() { 54 return this.deploymentStatus?.Canary; 55 } 56 57 // deploymentStatus.Healthy can be true, false, or null. Null implies pending 58 get isHealthy() { 59 return this.deploymentStatus?.Healthy; 60 } 61 62 get isUnhealthy() { 63 return this.deploymentStatus?.Healthy === false; 64 } 65 66 get willNotRestart() { 67 return this.clientStatus === 'failed' || this.clientStatus === 'lost'; 68 } 69 70 get willNotReschedule() { 71 return ( 72 this.willNotRestart && 73 !this.get('nextAllocation.content') && 74 !this.get('followUpEvaluation.content') 75 ); 76 } 77 78 get hasBeenRescheduled() { 79 return this.get('followUpEvaluation.content'); 80 } 81 82 get hasBeenRestarted() { 83 return this.states 84 .map((s) => s.events.content) 85 .flat() 86 .find((e) => e.type === 'Restarting'); 87 } 88 89 @attr healthChecks; 90 91 async getServiceHealth() { 92 const data = await this.store.adapterFor('allocation').check(this); 93 94 // Compare Results 95 if (!isEqual(this.healthChecks, data)) { 96 this.set('healthChecks', data); 97 } 98 } 99 100 @computed('') 101 get plainJobId() { 102 return JSON.parse(this.belongsTo('job').id())[0]; 103 } 104 105 @computed('clientStatus') 106 get statusIndex() { 107 return STATUS_ORDER[this.clientStatus] || 100; 108 } 109 110 @equal('clientStatus', 'running') isRunning; 111 @attr('boolean') isMigrating; 112 113 @computed('clientStatus') 114 get isScheduled() { 115 return ['pending', 'running'].includes(this.clientStatus); 116 } 117 118 // An allocation model created from any allocation list response will be lacking 119 // many properties (some of which can always be null). This is an indicator that 120 // the allocation needs to be reloaded to get the complete allocation state. 121 @none('allocationTaskGroup') isPartial; 122 123 // When allocations are server-side rescheduled, a paper trail 124 // is left linking all reschedule attempts. 125 @belongsTo('allocation', { inverse: 'nextAllocation' }) previousAllocation; 126 @belongsTo('allocation', { inverse: 'previousAllocation' }) nextAllocation; 127 128 @hasMany('allocation', { inverse: 'preemptedByAllocation' }) 129 preemptedAllocations; 130 @belongsTo('allocation', { inverse: 'preemptedAllocations' }) 131 preemptedByAllocation; 132 @attr('boolean') wasPreempted; 133 134 @belongsTo('evaluation') followUpEvaluation; 135 136 @computed('clientStatus') 137 get statusClass() { 138 const classMap = { 139 pending: 'is-pending', 140 running: 'is-primary', 141 complete: 'is-complete', 142 failed: 'is-error', 143 lost: 'is-light', 144 unknown: 'is-unknown', 145 }; 146 147 return classMap[this.clientStatus] || 'is-dark'; 148 } 149 150 @computed('jobVersion', 'job.version') 151 get isOld() { 152 return this.jobVersion !== this.get('job.version'); 153 } 154 155 @computed('isOld', 'jobTaskGroup', 'allocationTaskGroup') 156 get taskGroup() { 157 if (!this.isOld) return this.jobTaskGroup; 158 return this.allocationTaskGroup; 159 } 160 161 @computed('taskGroupName', 'job.taskGroups.[]') 162 get jobTaskGroup() { 163 const taskGroups = this.get('job.taskGroups'); 164 return taskGroups && taskGroups.findBy('name', this.taskGroupName); 165 } 166 167 @fragment('task-group', { defaultValue: null }) allocationTaskGroup; 168 169 @computed('taskGroup.drivers.[]', 'node.unhealthyDriverNames.[]') 170 get unhealthyDrivers() { 171 const taskGroupUnhealthyDrivers = this.get('taskGroup.drivers'); 172 const nodeUnhealthyDrivers = this.get('node.unhealthyDriverNames'); 173 174 if (taskGroupUnhealthyDrivers && nodeUnhealthyDrivers) { 175 return intersection(taskGroupUnhealthyDrivers, nodeUnhealthyDrivers); 176 } 177 178 return []; 179 } 180 181 // When per_alloc is set to true on a volume, the volumes are duplicated between active allocations. 182 // We differentiate them with a [#] suffix, inferred from a volume's allocation's name property. 183 @computed('name') 184 get volumeExtension() { 185 return this.name.substring(this.name.lastIndexOf('[')); 186 } 187 188 @fragmentArray('task-state') states; 189 @fragmentArray('reschedule-event') rescheduleEvents; 190 191 @computed('rescheduleEvents.length', 'nextAllocation') 192 get hasRescheduleEvents() { 193 return this.get('rescheduleEvents.length') > 0 || this.nextAllocation; 194 } 195 196 @computed( 197 'clientStatus', 198 'followUpEvaluation.content', 199 'nextAllocation.content' 200 ) 201 get hasStoppedRescheduling() { 202 return ( 203 !this.get('nextAllocation.content') && 204 !this.get('followUpEvaluation.content') && 205 this.clientStatus === 'failed' 206 ); 207 } 208 209 stop() { 210 return this.store.adapterFor('allocation').stop(this); 211 } 212 213 restart(taskName) { 214 return this.store.adapterFor('allocation').restart(this, taskName); 215 } 216 217 restartAll() { 218 return this.store.adapterFor('allocation').restartAll(this); 219 } 220 221 ls(path) { 222 return this.store.adapterFor('allocation').ls(this, path); 223 } 224 225 stat(path) { 226 return this.store.adapterFor('allocation').stat(this, path); 227 } 228 }