github.com/Ilhicas/nomad@v1.0.4-0.20210304152020-e86851182bc3/ui/app/models/job.js (about)

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