/** * Copyright (c) HashiCorp, Inc. * SPDX-License-Identifier: MPL-2.0 */ import { inject as service } from '@ember/service'; import { computed } from '@ember/object'; import { equal, none } from '@ember/object/computed'; import Model from '@ember-data/model'; import { attr, belongsTo, hasMany } from '@ember-data/model'; import { fragment, fragmentArray } from 'ember-data-model-fragments/attributes'; import isEqual from 'lodash.isequal'; import intersection from 'lodash.intersection'; import shortUUIDProperty from '../utils/properties/short-uuid'; import classic from 'ember-classic-decorator'; const STATUS_ORDER = { pending: 1, running: 2, complete: 3, unknown: 4, failed: 5, lost: 6, }; @classic export default class Allocation extends Model { @service token; @service store; @shortUUIDProperty('id') shortId; @belongsTo('job') job; @belongsTo('node') node; @attr('string') namespace; @attr('string') nodeID; @attr('string') name; @attr('string') taskGroupName; @fragment('resources') resources; @fragment('resources') allocatedResources; @attr('number') jobVersion; @attr('number') modifyIndex; @attr('date') modifyTime; @attr('number') createIndex; @attr('date') createTime; @attr('string') clientStatus; @attr('string') desiredStatus; @attr() desiredTransition; @attr() deploymentStatus; get isCanary() { return this.deploymentStatus?.Canary; } // deploymentStatus.Healthy can be true, false, or null. Null implies pending get isHealthy() { return this.deploymentStatus?.Healthy; } get isUnhealthy() { return this.deploymentStatus?.Healthy === false; } get willNotRestart() { return this.clientStatus === 'failed' || this.clientStatus === 'lost'; } get willNotReschedule() { return ( this.willNotRestart && !this.get('nextAllocation.content') && !this.get('followUpEvaluation.content') ); } get hasBeenRescheduled() { return this.get('followUpEvaluation.content'); } get hasBeenRestarted() { return this.states .map((s) => s.events.content) .flat() .find((e) => e.type === 'Restarting'); } @attr healthChecks; async getServiceHealth() { const data = await this.store.adapterFor('allocation').check(this); // Compare Results if (!isEqual(this.healthChecks, data)) { this.set('healthChecks', data); } } @computed('') get plainJobId() { return JSON.parse(this.belongsTo('job').id())[0]; } @computed('clientStatus') get statusIndex() { return STATUS_ORDER[this.clientStatus] || 100; } @equal('clientStatus', 'running') isRunning; @attr('boolean') isMigrating; @computed('clientStatus') get isScheduled() { return ['pending', 'running'].includes(this.clientStatus); } // An allocation model created from any allocation list response will be lacking // many properties (some of which can always be null). This is an indicator that // the allocation needs to be reloaded to get the complete allocation state. @none('allocationTaskGroup') isPartial; // When allocations are server-side rescheduled, a paper trail // is left linking all reschedule attempts. @belongsTo('allocation', { inverse: 'nextAllocation' }) previousAllocation; @belongsTo('allocation', { inverse: 'previousAllocation' }) nextAllocation; @hasMany('allocation', { inverse: 'preemptedByAllocation' }) preemptedAllocations; @belongsTo('allocation', { inverse: 'preemptedAllocations' }) preemptedByAllocation; @attr('boolean') wasPreempted; @belongsTo('evaluation') followUpEvaluation; @computed('clientStatus') get statusClass() { const classMap = { pending: 'is-pending', running: 'is-primary', complete: 'is-complete', failed: 'is-error', lost: 'is-light', unknown: 'is-unknown', }; return classMap[this.clientStatus] || 'is-dark'; } @computed('jobVersion', 'job.version') get isOld() { return this.jobVersion !== this.get('job.version'); } @computed('isOld', 'jobTaskGroup', 'allocationTaskGroup') get taskGroup() { if (!this.isOld) return this.jobTaskGroup; return this.allocationTaskGroup; } @computed('taskGroupName', 'job.taskGroups.[]') get jobTaskGroup() { const taskGroups = this.get('job.taskGroups'); return taskGroups && taskGroups.findBy('name', this.taskGroupName); } @fragment('task-group', { defaultValue: null }) allocationTaskGroup; @computed('taskGroup.drivers.[]', 'node.unhealthyDriverNames.[]') get unhealthyDrivers() { const taskGroupUnhealthyDrivers = this.get('taskGroup.drivers'); const nodeUnhealthyDrivers = this.get('node.unhealthyDriverNames'); if (taskGroupUnhealthyDrivers && nodeUnhealthyDrivers) { return intersection(taskGroupUnhealthyDrivers, nodeUnhealthyDrivers); } return []; } // When per_alloc is set to true on a volume, the volumes are duplicated between active allocations. // We differentiate them with a [#] suffix, inferred from a volume's allocation's name property. @computed('name') get volumeExtension() { return this.name.substring(this.name.lastIndexOf('[')); } @fragmentArray('task-state') states; @fragmentArray('reschedule-event') rescheduleEvents; @computed('rescheduleEvents.length', 'nextAllocation') get hasRescheduleEvents() { return this.get('rescheduleEvents.length') > 0 || this.nextAllocation; } @computed( 'clientStatus', 'followUpEvaluation.content', 'nextAllocation.content' ) get hasStoppedRescheduling() { return ( !this.get('nextAllocation.content') && !this.get('followUpEvaluation.content') && this.clientStatus === 'failed' ); } stop() { return this.store.adapterFor('allocation').stop(this); } restart(taskName) { return this.store.adapterFor('allocation').restart(this, taskName); } restartAll() { return this.store.adapterFor('allocation').restartAll(this); } ls(path) { return this.store.adapterFor('allocation').ls(this, path); } stat(path) { return this.store.adapterFor('allocation').stat(this, path); } }