github.com/hernad/nomad@v1.6.112/ui/app/components/job-status/panel/deploying.js (about)

     1  /**
     2   * Copyright (c) HashiCorp, Inc.
     3   * SPDX-License-Identifier: MPL-2.0
     4   */
     5  
     6  // @ts-check
     7  import Component from '@glimmer/component';
     8  import { task } from 'ember-concurrency';
     9  import { tracked } from '@glimmer/tracking';
    10  import { alias } from '@ember/object/computed';
    11  import messageFromAdapterError from 'nomad-ui/utils/message-from-adapter-error';
    12  import { jobAllocStatuses } from '../../../utils/allocation-client-statuses';
    13  
    14  export default class JobStatusPanelDeployingComponent extends Component {
    15    @alias('args.job') job;
    16    @alias('args.handleError') handleError = () => {};
    17  
    18    get allocTypes() {
    19      return jobAllocStatuses[this.args.job.type].map((type) => {
    20        return {
    21          label: type,
    22        };
    23      });
    24    }
    25  
    26    @tracked oldVersionAllocBlockIDs = [];
    27  
    28    // Called via did-insert; sets a static array of "outgoing"
    29    // allocations we can track throughout a deployment
    30    establishOldAllocBlockIDs() {
    31      this.oldVersionAllocBlockIDs = this.job.allocations.filter(
    32        (a) => a.clientStatus === 'running' && a.isOld
    33      );
    34    }
    35  
    36    /**
    37     * Promotion of a deployment will error if the canary allocations are not of status "Healthy";
    38     * this function will check for that and disable the promote button if necessary.
    39     * @returns {boolean}
    40     */
    41    get canariesHealthy() {
    42      const relevantAllocs = this.job.allocations.filter(
    43        (a) => !a.isOld && a.isCanary && !a.hasBeenRescheduled
    44      );
    45      return relevantAllocs.every(
    46        (a) => a.clientStatus === 'running' && a.isHealthy
    47      );
    48    }
    49  
    50    get someCanariesHaveFailed() {
    51      const relevantAllocs = this.job.allocations.filter(
    52        (a) => !a.isOld && a.isCanary && !a.hasBeenRescheduled
    53      );
    54      return relevantAllocs.some(
    55        (a) =>
    56          a.clientStatus === 'failed' ||
    57          a.clientStatus === 'lost' ||
    58          a.isUnhealthy
    59      );
    60    }
    61  
    62    @task(function* () {
    63      try {
    64        yield this.job.latestDeployment.content.promote();
    65      } catch (err) {
    66        this.handleError({
    67          title: 'Could Not Promote Deployment',
    68          description: messageFromAdapterError(err, 'promote deployments'),
    69        });
    70      }
    71    })
    72    promote;
    73  
    74    @task(function* () {
    75      try {
    76        yield this.job.latestDeployment.content.fail();
    77      } catch (err) {
    78        this.handleError({
    79          title: 'Could Not Fail Deployment',
    80          description: messageFromAdapterError(err, 'fail deployments'),
    81        });
    82      }
    83    })
    84    fail;
    85  
    86    @alias('job.latestDeployment') deployment;
    87    @alias('totalAllocs') desiredTotal;
    88  
    89    get oldVersionAllocBlocks() {
    90      return this.job.allocations
    91        .filter((allocation) => this.oldVersionAllocBlockIDs.includes(allocation))
    92        .reduce((alloGroups, currentAlloc) => {
    93          const status = currentAlloc.clientStatus;
    94  
    95          if (!alloGroups[status]) {
    96            alloGroups[status] = {
    97              healthy: { nonCanary: [] },
    98              unhealthy: { nonCanary: [] },
    99              health_unknown: { nonCanary: [] },
   100            };
   101          }
   102          alloGroups[status].healthy.nonCanary.push(currentAlloc);
   103  
   104          return alloGroups;
   105        }, {});
   106    }
   107  
   108    get newVersionAllocBlocks() {
   109      let availableSlotsToFill = this.desiredTotal;
   110      let allocationsOfDeploymentVersion = this.job.allocations.filter(
   111        (a) => !a.isOld
   112      );
   113  
   114      let allocationCategories = this.allocTypes.reduce((categories, type) => {
   115        categories[type.label] = {
   116          healthy: { canary: [], nonCanary: [] },
   117          unhealthy: { canary: [], nonCanary: [] },
   118          health_unknown: { canary: [], nonCanary: [] },
   119        };
   120        return categories;
   121      }, {});
   122  
   123      for (let alloc of allocationsOfDeploymentVersion) {
   124        if (availableSlotsToFill <= 0) {
   125          break;
   126        }
   127        let status = alloc.clientStatus;
   128        let canary = alloc.isCanary ? 'canary' : 'nonCanary';
   129  
   130        // Health status only matters in the context of a "running" allocation.
   131        // However, healthy/unhealthy is never purged when an allocation moves to a different clientStatus
   132        // Thus, we should only show something as "healthy" in the event that it is running.
   133        // Otherwise, we'd have arbitrary groupings based on previous health status.
   134        let health =
   135          status === 'running'
   136            ? alloc.isHealthy
   137              ? 'healthy'
   138              : alloc.isUnhealthy
   139              ? 'unhealthy'
   140              : 'health_unknown'
   141            : 'health_unknown';
   142  
   143        if (allocationCategories[status]) {
   144          // If status is failed or lost, we only want to show it IF it's used up its restarts/rescheds.
   145          // Otherwise, we'd be showing an alloc that had been replaced.
   146          if (alloc.willNotRestart) {
   147            if (!alloc.willNotReschedule) {
   148              // Dont count it
   149              continue;
   150            }
   151          }
   152          allocationCategories[status][health][canary].push(alloc);
   153          availableSlotsToFill--;
   154        }
   155      }
   156  
   157      // Fill unplaced slots if availableSlotsToFill > 0
   158      if (availableSlotsToFill > 0) {
   159        allocationCategories['unplaced'] = {
   160          healthy: { canary: [], nonCanary: [] },
   161          unhealthy: { canary: [], nonCanary: [] },
   162          health_unknown: { canary: [], nonCanary: [] },
   163        };
   164        allocationCategories['unplaced']['healthy']['nonCanary'] = Array(
   165          availableSlotsToFill
   166        )
   167          .fill()
   168          .map(() => {
   169            return { clientStatus: 'unplaced' };
   170          });
   171      }
   172  
   173      return allocationCategories;
   174    }
   175  
   176    get newRunningHealthyAllocBlocks() {
   177      return [
   178        ...this.newVersionAllocBlocks['running']['healthy']['canary'],
   179        ...this.newVersionAllocBlocks['running']['healthy']['nonCanary'],
   180      ];
   181    }
   182  
   183    get newRunningUnhealthyAllocBlocks() {
   184      return [
   185        ...this.newVersionAllocBlocks['running']['unhealthy']['canary'],
   186        ...this.newVersionAllocBlocks['running']['unhealthy']['nonCanary'],
   187      ];
   188    }
   189  
   190    get newRunningHealthUnknownAllocBlocks() {
   191      return [
   192        ...this.newVersionAllocBlocks['running']['health_unknown']['canary'],
   193        ...this.newVersionAllocBlocks['running']['health_unknown']['nonCanary'],
   194      ];
   195    }
   196  
   197    get rescheduledAllocs() {
   198      return this.job.allocations.filter((a) => !a.isOld && a.hasBeenRescheduled);
   199    }
   200  
   201    get restartedAllocs() {
   202      return this.job.allocations.filter((a) => !a.isOld && a.hasBeenRestarted);
   203    }
   204  
   205    // #region legend
   206    get newAllocsByStatus() {
   207      return Object.entries(this.newVersionAllocBlocks).reduce(
   208        (counts, [status, healthStatusObj]) => {
   209          counts[status] = Object.values(healthStatusObj)
   210            .flatMap((canaryStatusObj) => Object.values(canaryStatusObj))
   211            .flatMap((canaryStatusArray) => canaryStatusArray).length;
   212          return counts;
   213        },
   214        {}
   215      );
   216    }
   217  
   218    get newAllocsByCanary() {
   219      return Object.values(this.newVersionAllocBlocks)
   220        .flatMap((healthStatusObj) => Object.values(healthStatusObj))
   221        .flatMap((canaryStatusObj) => Object.entries(canaryStatusObj))
   222        .reduce((counts, [canaryStatus, items]) => {
   223          counts[canaryStatus] = (counts[canaryStatus] || 0) + items.length;
   224          return counts;
   225        }, {});
   226    }
   227  
   228    get newAllocsByHealth() {
   229      return {
   230        healthy: this.newRunningHealthyAllocBlocks.length,
   231        unhealthy: this.newRunningUnhealthyAllocBlocks.length,
   232        health_unknown: this.newRunningHealthUnknownAllocBlocks.length,
   233      };
   234    }
   235    // #endregion legend
   236  
   237    get oldRunningHealthyAllocBlocks() {
   238      return this.oldVersionAllocBlocks.running?.healthy?.nonCanary || [];
   239    }
   240    get oldCompleteHealthyAllocBlocks() {
   241      return this.oldVersionAllocBlocks.complete?.healthy?.nonCanary || [];
   242    }
   243  
   244    // TODO: eventually we will want this from a new property on a job.
   245    // TODO: consolidate w/ the one in steady.js
   246    get totalAllocs() {
   247      // v----- Experimental method: Count all allocs. Good for testing but not a realistic representation of "Desired"
   248      // return this.allocTypes.reduce((sum, type) => sum + this.args.job[type.property], 0);
   249  
   250      // v----- Realistic method: Tally a job's task groups' "count" property
   251      return this.args.job.taskGroups.reduce((sum, tg) => sum + tg.count, 0);
   252    }
   253  
   254    get deploymentIsAutoPromoted() {
   255      return this.job.latestDeployment?.get('isAutoPromoted');
   256    }
   257  
   258    get oldVersions() {
   259      const oldVersions = Object.values(this.oldRunningHealthyAllocBlocks)
   260        .map((a) => (!isNaN(a?.jobVersion) ? a.jobVersion : 'unknown')) // "starting" allocs, GC'd allocs, etc. do not have a jobVersion
   261        .sort((a, b) => a - b)
   262        .reduce((result, item) => {
   263          const existingVersion = result.find((v) => v.version === item);
   264          if (existingVersion) {
   265            existingVersion.allocations.push(item);
   266          } else {
   267            result.push({ version: item, allocations: [item] });
   268          }
   269          return result;
   270        }, []);
   271  
   272      return oldVersions;
   273    }
   274  
   275    get newVersions() {
   276      // Note: it's probably safe to assume all new allocs have the latest job version, but
   277      // let's map just in case there's ever a situation with multiple job versions going out
   278      // in a deployment for some reason
   279      const newVersions = Object.values(this.newVersionAllocBlocks)
   280        .flatMap((allocType) => Object.values(allocType))
   281        .flatMap((allocHealth) => Object.values(allocHealth))
   282        .flatMap((allocCanary) => Object.values(allocCanary))
   283        .filter((a) => a.jobVersion && a.jobVersion !== 'unknown')
   284        .map((a) => a.jobVersion)
   285        .sort((a, b) => a - b)
   286        .reduce((result, item) => {
   287          const existingVersion = result.find((v) => v.version === item);
   288          if (existingVersion) {
   289            existingVersion.allocations.push(item);
   290          } else {
   291            result.push({ version: item, allocations: [item] });
   292          }
   293          return result;
   294        }, []);
   295      return newVersions;
   296    }
   297  
   298    get versions() {
   299      return [...this.oldVersions, ...this.newVersions];
   300    }
   301  }