github.com/hernad/nomad@v1.6.112/ui/tests/integration/components/job-status-panel-test.js (about)

     1  /**
     2   * Copyright (c) HashiCorp, Inc.
     3   * SPDX-License-Identifier: MPL-2.0
     4   */
     5  
     6  import { module, test } from 'qunit';
     7  import { setupRenderingTest } from 'ember-qunit';
     8  import { find, render, settled } from '@ember/test-helpers';
     9  import hbs from 'htmlbars-inline-precompile';
    10  import { startMirage } from 'nomad-ui/initializers/ember-cli-mirage';
    11  import { initialize as fragmentSerializerInitializer } from 'nomad-ui/initializers/fragment-serializer';
    12  import { componentA11yAudit } from 'nomad-ui/tests/helpers/a11y-audit';
    13  import percySnapshot from '@percy/ember';
    14  
    15  module(
    16    'Integration | Component | job status panel | active deployment',
    17    function (hooks) {
    18      setupRenderingTest(hooks);
    19  
    20      hooks.beforeEach(function () {
    21        fragmentSerializerInitializer(this.owner);
    22        window.localStorage.clear();
    23        this.store = this.owner.lookup('service:store');
    24        this.server = startMirage();
    25        this.server.create('node-pool');
    26        this.server.create('namespace');
    27      });
    28  
    29      hooks.afterEach(function () {
    30        this.server.shutdown();
    31        window.localStorage.clear();
    32      });
    33  
    34      test('there is no latest deployment section when the job has no deployments', async function (assert) {
    35        this.server.create('job', {
    36          type: 'service',
    37          noDeployments: true,
    38          createAllocations: false,
    39        });
    40  
    41        await this.store.findAll('job');
    42  
    43        this.set('job', this.store.peekAll('job').get('firstObject'));
    44        await render(hbs`
    45        <JobStatus::Panel @job={{this.job}} />)
    46      `);
    47  
    48        assert.notOk(find('.active-deployment'), 'No active deployment');
    49      });
    50  
    51      test('the latest deployment section shows up for the currently running deployment: Ungrouped Allocations (small cluster)', async function (assert) {
    52        assert.expect(24);
    53  
    54        this.server.create('node');
    55  
    56        const NUMBER_OF_GROUPS = 2;
    57        const ALLOCS_PER_GROUP = 10;
    58        const allocStatusDistribution = {
    59          running: 0.5,
    60          failed: 0.2,
    61          unknown: 0.1,
    62          lost: 0,
    63          complete: 0.1,
    64          pending: 0.1,
    65        };
    66  
    67        const job = await this.server.create('job', {
    68          type: 'service',
    69          createAllocations: true,
    70          noDeployments: true, // manually created below
    71          activeDeployment: true,
    72          groupTaskCount: ALLOCS_PER_GROUP,
    73          shallow: true,
    74          resourceSpec: Array(NUMBER_OF_GROUPS).fill(['M: 257, C: 500']), // length of this array determines number of groups
    75          allocStatusDistribution,
    76        });
    77  
    78        const jobRecord = await this.store.find(
    79          'job',
    80          JSON.stringify([job.id, 'default'])
    81        );
    82        await this.server.create('deployment', false, 'active', {
    83          jobId: job.id,
    84          groupDesiredTotal: ALLOCS_PER_GROUP,
    85          versionNumber: 1,
    86          status: 'failed',
    87        });
    88  
    89        const OLD_ALLOCATIONS_TO_SHOW = 25;
    90        const OLD_ALLOCATIONS_TO_COMPLETE = 5;
    91  
    92        this.server.createList('allocation', OLD_ALLOCATIONS_TO_SHOW, {
    93          jobId: job.id,
    94          jobVersion: 0,
    95          clientStatus: 'running',
    96        });
    97  
    98        this.set('job', jobRecord);
    99        await this.get('job.allocations');
   100  
   101        await render(hbs`
   102          <JobStatus::Panel @job={{this.job}} />
   103        `);
   104  
   105        // Initially no active deployment
   106        assert.notOk(
   107          find('.active-deployment'),
   108          'Does not show an active deployment when latest is failed'
   109        );
   110  
   111        const deployment = await this.get('job.latestDeployment');
   112  
   113        await this.set('job.latestDeployment.status', 'running');
   114  
   115        assert.ok(
   116          find('.active-deployment'),
   117          'Shows an active deployment if latest status is Running'
   118        );
   119  
   120        // Half the shown allocations are running, 1 is pending, 1 is failed; none are canaries or healthy.
   121        // The rest (lost, unknown, etc.) all show up as "Unplaced"
   122        assert
   123          .dom('.new-allocations .allocation-status-row .represented-allocation')
   124          .exists(
   125            { count: NUMBER_OF_GROUPS * ALLOCS_PER_GROUP },
   126            'All allocations are shown (ungrouped)'
   127          );
   128        assert
   129          .dom(
   130            '.new-allocations .allocation-status-row .represented-allocation.running'
   131          )
   132          .exists(
   133            {
   134              count:
   135                NUMBER_OF_GROUPS *
   136                ALLOCS_PER_GROUP *
   137                allocStatusDistribution.running,
   138            },
   139            'Correct number of running allocations are shown'
   140          );
   141        assert
   142          .dom(
   143            '.new-allocations .allocation-status-row .represented-allocation.running.canary'
   144          )
   145          .exists({ count: 0 }, 'No running canaries shown by default');
   146        assert
   147          .dom(
   148            '.new-allocations .allocation-status-row .represented-allocation.running.healthy'
   149          )
   150          .exists({ count: 0 }, 'No running healthy shown by default');
   151        assert
   152          .dom(
   153            '.new-allocations .allocation-status-row .represented-allocation.failed'
   154          )
   155          .exists(
   156            {
   157              count:
   158                NUMBER_OF_GROUPS *
   159                ALLOCS_PER_GROUP *
   160                allocStatusDistribution.failed,
   161            },
   162            'Correct number of failed allocations are shown'
   163          );
   164        assert
   165          .dom(
   166            '.new-allocations .allocation-status-row .represented-allocation.failed.canary'
   167          )
   168          .exists({ count: 0 }, 'No failed canaries shown by default');
   169        assert
   170          .dom(
   171            '.new-allocations .allocation-status-row .represented-allocation.pending'
   172          )
   173          .exists(
   174            {
   175              count:
   176                NUMBER_OF_GROUPS *
   177                ALLOCS_PER_GROUP *
   178                allocStatusDistribution.pending,
   179            },
   180            'Correct number of pending allocations are shown'
   181          );
   182        assert
   183          .dom(
   184            '.new-allocations .allocation-status-row .represented-allocation.pending.canary'
   185          )
   186          .exists({ count: 0 }, 'No pending canaries shown by default');
   187        assert
   188          .dom(
   189            '.new-allocations .allocation-status-row .represented-allocation.unplaced'
   190          )
   191          .exists(
   192            {
   193              count:
   194                NUMBER_OF_GROUPS *
   195                ALLOCS_PER_GROUP *
   196                (allocStatusDistribution.lost +
   197                  allocStatusDistribution.unknown +
   198                  allocStatusDistribution.complete),
   199            },
   200            'Correct number of unplaced allocations are shown'
   201          );
   202  
   203        assert.equal(
   204          find('[data-test-new-allocation-tally] > span').textContent.trim(),
   205          `New allocations: ${
   206            this.job.allocations.filter(
   207              (a) =>
   208                a.clientStatus === 'running' &&
   209                a.deploymentStatus?.Healthy === true
   210            ).length
   211          }/${deployment.get('desiredTotal')} running and healthy`,
   212          'Summary text shows accurate numbers when 0 are running/healthy'
   213        );
   214  
   215        let NUMBER_OF_RUNNING_CANARIES = 2;
   216        let NUMBER_OF_RUNNING_HEALTHY = 5;
   217        let NUMBER_OF_FAILED_CANARIES = 1;
   218        let NUMBER_OF_PENDING_CANARIES = 1;
   219  
   220        // Set some allocs to canary, and to healthy
   221        this.get('job.allocations')
   222          .filter((a) => a.clientStatus === 'running')
   223          .slice(0, NUMBER_OF_RUNNING_CANARIES)
   224          .forEach((alloc) =>
   225            alloc.set('deploymentStatus', {
   226              Canary: true,
   227              Healthy: alloc.deploymentStatus?.Healthy,
   228            })
   229          );
   230        this.get('job.allocations')
   231          .filter((a) => a.clientStatus === 'running')
   232          .slice(0, NUMBER_OF_RUNNING_HEALTHY)
   233          .forEach((alloc) =>
   234            alloc.set('deploymentStatus', {
   235              Canary: alloc.deploymentStatus?.Canary,
   236              Healthy: true,
   237            })
   238          );
   239        this.get('job.allocations')
   240          .filter((a) => a.clientStatus === 'failed')
   241          .slice(0, NUMBER_OF_FAILED_CANARIES)
   242          .forEach((alloc) =>
   243            alloc.set('deploymentStatus', {
   244              Canary: true,
   245              Healthy: alloc.deploymentStatus?.Healthy,
   246            })
   247          );
   248        this.get('job.allocations')
   249          .filter((a) => a.clientStatus === 'pending')
   250          .slice(0, NUMBER_OF_PENDING_CANARIES)
   251          .forEach((alloc) =>
   252            alloc.set('deploymentStatus', {
   253              Canary: true,
   254              Healthy: alloc.deploymentStatus?.Healthy,
   255            })
   256          );
   257  
   258        await render(hbs`
   259          <JobStatus::Panel @job={{this.job}} />
   260        `);
   261        assert
   262          .dom(
   263            '.new-allocations .allocation-status-row .represented-allocation.running.canary'
   264          )
   265          .exists(
   266            { count: NUMBER_OF_RUNNING_CANARIES },
   267            'Running Canaries shown when deployment info dictates'
   268          );
   269        assert
   270          .dom(
   271            '.new-allocations .allocation-status-row .represented-allocation.running.healthy'
   272          )
   273          .exists(
   274            { count: NUMBER_OF_RUNNING_HEALTHY },
   275            'Running Healthy allocs shown when deployment info dictates'
   276          );
   277        assert
   278          .dom(
   279            '.new-allocations .allocation-status-row .represented-allocation.failed.canary'
   280          )
   281          .exists(
   282            { count: NUMBER_OF_FAILED_CANARIES },
   283            'Failed Canaries shown when deployment info dictates'
   284          );
   285        assert
   286          .dom(
   287            '.new-allocations .allocation-status-row .represented-allocation.pending.canary'
   288          )
   289          .exists(
   290            { count: NUMBER_OF_PENDING_CANARIES },
   291            'Pending Canaries shown when deployment info dictates'
   292          );
   293  
   294        assert.equal(
   295          find('[data-test-new-allocation-tally] > span').textContent.trim(),
   296          `New allocations: ${
   297            this.job.allocations.filter(
   298              (a) =>
   299                a.clientStatus === 'running' &&
   300                a.deploymentStatus?.Healthy === true
   301            ).length
   302          }/${deployment.get('desiredTotal')} running and healthy`,
   303          'Summary text shows accurate numbers when some are running/healthy'
   304        );
   305  
   306        assert.equal(
   307          find('[data-test-old-allocation-tally] > span').textContent.trim(),
   308          `Previous allocations: ${
   309            this.job.allocations.filter(
   310              (a) =>
   311                (a.clientStatus === 'running' || a.clientStatus === 'complete') &&
   312                a.jobVersion !== deployment.versionNumber
   313            ).length
   314          } running`,
   315          'Old Alloc Summary text shows accurate numbers'
   316        );
   317  
   318        assert.equal(
   319          find('[data-test-previous-allocations-legend]')
   320            .textContent.trim()
   321            .replace(/\s\s+/g, ' '),
   322          '25 Running 0 Complete'
   323        );
   324  
   325        await percySnapshot(
   326          "Job Status Panel: 'New' and 'Previous' allocations, initial deploying state"
   327        );
   328  
   329        // Try setting a few of the old allocs to complete and make sure number ticks down
   330        await Promise.all(
   331          this.get('job.allocations')
   332            .filter(
   333              (a) =>
   334                a.clientStatus === 'running' &&
   335                a.jobVersion !== deployment.versionNumber
   336            )
   337            .slice(0, OLD_ALLOCATIONS_TO_COMPLETE)
   338            .map(async (a) => await a.set('clientStatus', 'complete'))
   339        );
   340  
   341        assert
   342          .dom(
   343            '.previous-allocations .allocation-status-row .represented-allocation'
   344          )
   345          .exists(
   346            { count: OLD_ALLOCATIONS_TO_SHOW },
   347            'All old allocations are shown'
   348          );
   349        assert
   350          .dom(
   351            '.previous-allocations .allocation-status-row .represented-allocation.complete'
   352          )
   353          .exists(
   354            { count: OLD_ALLOCATIONS_TO_COMPLETE },
   355            'Correct number of old allocations are in completed state'
   356          );
   357  
   358        assert.equal(
   359          find('[data-test-old-allocation-tally] > span').textContent.trim(),
   360          `Previous allocations: ${
   361            this.job.allocations.filter(
   362              (a) =>
   363                (a.clientStatus === 'running' || a.clientStatus === 'complete') &&
   364                a.jobVersion !== deployment.versionNumber
   365            ).length - OLD_ALLOCATIONS_TO_COMPLETE
   366          } running`,
   367          'Old Alloc Summary text shows accurate numbers after some are marked complete'
   368        );
   369  
   370        assert.equal(
   371          find('[data-test-previous-allocations-legend]')
   372            .textContent.trim()
   373            .replace(/\s\s+/g, ' '),
   374          '20 Running 5 Complete'
   375        );
   376  
   377        await percySnapshot(
   378          "Job Status Panel: 'New' and 'Previous' allocations, some old marked complete"
   379        );
   380  
   381        await componentA11yAudit(
   382          this.element,
   383          assert,
   384          'scrollable-region-focusable'
   385        ); //keyframe animation fades from opacity 0
   386      });
   387  
   388      test('non-running allocations are grouped regardless of health', async function (assert) {
   389        this.server.create('node');
   390  
   391        const NUMBER_OF_GROUPS = 1;
   392        const ALLOCS_PER_GROUP = 100;
   393        const allocStatusDistribution = {
   394          running: 0.9,
   395          failed: 0.1,
   396          unknown: 0,
   397          lost: 0,
   398          complete: 0,
   399          pending: 0,
   400        };
   401  
   402        const job = await this.server.create('job', {
   403          type: 'service',
   404          createAllocations: true,
   405          noDeployments: true, // manually created below
   406          activeDeployment: true,
   407          groupTaskCount: ALLOCS_PER_GROUP,
   408          shallow: true,
   409          resourceSpec: Array(NUMBER_OF_GROUPS).fill(['M: 257, C: 500']), // length of this array determines number of groups
   410          allocStatusDistribution,
   411        });
   412  
   413        const jobRecord = await this.store.find(
   414          'job',
   415          JSON.stringify([job.id, 'default'])
   416        );
   417        await this.server.create('deployment', false, 'active', {
   418          jobId: job.id,
   419          groupDesiredTotal: ALLOCS_PER_GROUP,
   420          versionNumber: 1,
   421          status: 'failed',
   422        });
   423  
   424        let activelyDeployingJobAllocs = server.schema.allocations
   425          .all()
   426          .filter((a) => a.jobId === job.id);
   427  
   428        activelyDeployingJobAllocs.models
   429          .filter((a) => a.clientStatus === 'failed')
   430          .slice(0, 10)
   431          .forEach((a) =>
   432            a.update({ deploymentStatus: { Healthy: true, Canary: false } })
   433          );
   434  
   435        this.set('job', jobRecord);
   436  
   437        await this.get('job.latestDeployment');
   438        await this.set('job.latestDeployment.status', 'running');
   439  
   440        await this.get('job.allocations');
   441  
   442        await render(hbs`
   443          <JobStatus::Panel @job={{this.job}} />
   444        `);
   445  
   446        assert
   447          .dom('.allocation-status-block .represented-allocation.failed')
   448          .exists({ count: 1 }, 'Failed block exists only once');
   449        assert
   450          .dom('.allocation-status-block .represented-allocation.failed')
   451          .hasClass('rest', 'Failed block is a summary block');
   452  
   453        await Promise.all(
   454          this.get('job.allocations')
   455            .filterBy('clientStatus', 'failed')
   456            .slice(0, 3)
   457            .map(async (a) => {
   458              await a.set('deploymentStatus', { Healthy: false, Canary: true });
   459            })
   460        );
   461  
   462        assert
   463          .dom('.represented-allocation.failed.rest')
   464          .exists(
   465            { count: 2 },
   466            'Now that some are canaries, they still make up two blocks'
   467          );
   468      });
   469  
   470      test('During a deployment with canaries, canary alerts are handled', async function (assert) {
   471        this.server.create('node');
   472  
   473        const NUMBER_OF_GROUPS = 1;
   474        const ALLOCS_PER_GROUP = 10;
   475        const allocStatusDistribution = {
   476          running: 0.9,
   477          failed: 0.1,
   478          unknown: 0,
   479          lost: 0,
   480          complete: 0,
   481          pending: 0,
   482        };
   483  
   484        const job = await this.server.create('job', {
   485          type: 'service',
   486          createAllocations: true,
   487          noDeployments: true, // manually created below
   488          activeDeployment: true,
   489          groupTaskCount: ALLOCS_PER_GROUP,
   490          shallow: true,
   491          resourceSpec: Array(NUMBER_OF_GROUPS).fill(['M: 257, C: 500']), // length of this array determines number of groups
   492          allocStatusDistribution,
   493        });
   494  
   495        const jobRecord = await this.store.find(
   496          'job',
   497          JSON.stringify([job.id, 'default'])
   498        );
   499        const deployment = await this.server.create(
   500          'deployment',
   501          false,
   502          'active',
   503          {
   504            jobId: job.id,
   505            groupDesiredTotal: ALLOCS_PER_GROUP,
   506            versionNumber: 1,
   507            status: 'failed',
   508            // requiresPromotion: false,
   509          }
   510        );
   511  
   512        // requiresPromotion goes to false
   513        deployment.deploymentTaskGroupSummaries.models.forEach((d) => {
   514          d.update({
   515            desiredCanaries: 0,
   516            requiresPromotion: false,
   517            promoted: false,
   518          });
   519        });
   520  
   521        // All allocations set to Healthy and non-canary
   522        let activelyDeployingJobAllocs = server.schema.allocations
   523          .all()
   524          .filter((a) => a.jobId === job.id);
   525  
   526        activelyDeployingJobAllocs.models.forEach((a) => {
   527          a.update({ deploymentStatus: { Healthy: true, Canary: false } });
   528        });
   529  
   530        this.set('job', jobRecord);
   531  
   532        await this.get('job.latestDeployment');
   533        await this.set('job.latestDeployment.status', 'running');
   534  
   535        await this.get('job.allocations');
   536  
   537        await render(hbs`
   538          <JobStatus::Panel @job={{this.job}} />
   539        `);
   540  
   541        assert
   542          .dom(find('.legend-item .represented-allocation.running').parentElement)
   543          .hasText('9 Running');
   544        assert
   545          .dom(find('.legend-item .represented-allocation.healthy').parentElement)
   546          .hasText('9 Healthy');
   547  
   548        assert
   549          .dom('.canary-promotion-alert')
   550          .doesNotExist('No canary promotion alert when no canaries');
   551  
   552        // Set 3 allocations to health-pending canaries
   553        await Promise.all(
   554          this.get('job.allocations')
   555            .filterBy('clientStatus', 'running')
   556            .slice(0, 3)
   557            .map(async (a) => {
   558              await a.set('deploymentStatus', { Healthy: null, Canary: true });
   559            })
   560        );
   561  
   562        // Set the deployment's requiresPromotion to true
   563        await Promise.all(
   564          this.get('job.latestDeployment.taskGroupSummaries').map(async (a) => {
   565            await a.set('desiredCanaries', 3);
   566            await a.set('requiresPromotion', true);
   567          })
   568        );
   569  
   570        await settled();
   571  
   572        assert
   573          .dom('.canary-promotion-alert')
   574          .exists('Canary promotion alert when canaries are present');
   575  
   576        assert
   577          .dom('.canary-promotion-alert')
   578          .containsText('Checking Canary health');
   579  
   580        // Fail the health check on 1 canary
   581        await Promise.all(
   582          this.get('job.allocations')
   583            .filterBy('clientStatus', 'running')
   584            .slice(0, 1)
   585            .map(async (a) => {
   586              await a.set('deploymentStatus', { Healthy: false, Canary: true });
   587            })
   588        );
   589  
   590        assert
   591          .dom('.canary-promotion-alert')
   592          .containsText('Some Canaries have failed');
   593  
   594        // That 1 passes its health checks, but two peers remain pending
   595        await Promise.all(
   596          this.get('job.allocations')
   597            .filterBy('clientStatus', 'running')
   598            .slice(0, 1)
   599            .map(async (a) => {
   600              await a.set('deploymentStatus', { Healthy: true, Canary: true });
   601            })
   602        );
   603        await settled();
   604        assert
   605          .dom('.canary-promotion-alert')
   606          .containsText('Checking Canary health');
   607  
   608        // Fail one of the running canaries, but dont specifically touch its deploymentStatus.health
   609        await Promise.all(
   610          this.get('job.allocations')
   611            .filterBy('clientStatus', 'running')
   612            .slice(0, 1)
   613            .map(async (a) => {
   614              await a.set('clientStatus', 'failed');
   615            })
   616        );
   617  
   618        assert
   619          .dom('.canary-promotion-alert')
   620          .containsText('Some Canaries have failed');
   621  
   622        // Canaries all running and healthy
   623        await Promise.all(
   624          this.get('job.allocations')
   625            .slice(0, 3)
   626            .map(async (a) => {
   627              await a.setProperties({
   628                deploymentStatus: { Healthy: true, Canary: true },
   629                clientStatus: 'running',
   630              });
   631            })
   632        );
   633  
   634        await settled();
   635  
   636        assert
   637          .dom('.canary-promotion-alert')
   638          .containsText('Deployment requires promotion');
   639      });
   640  
   641      test('when there is no running deployment, the latest deployment section shows up for the last deployment', async function (assert) {
   642        this.server.create('job', {
   643          type: 'service',
   644          createAllocations: false,
   645          noActiveDeployment: true,
   646        });
   647  
   648        await this.store.findAll('job');
   649  
   650        this.set('job', this.store.peekAll('job').get('firstObject'));
   651        await render(hbs`
   652        <JobStatus::Panel @job={{this.job}} />
   653      `);
   654  
   655        assert.notOk(find('.active-deployment'), 'No active deployment');
   656        assert.ok(
   657          find('.running-allocs-title'),
   658          'Steady-state mode shown instead'
   659        );
   660      });
   661    }
   662  );