github.com/ferranbt/nomad@v0.9.3-0.20190607002617-85c449b7667c/ui/tests/acceptance/client-detail-test.js (about)

     1  import { currentURL } from '@ember/test-helpers';
     2  import { assign } from '@ember/polyfills';
     3  import { module, test } from 'qunit';
     4  import { setupApplicationTest } from 'ember-qunit';
     5  import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
     6  import { formatBytes } from 'nomad-ui/helpers/format-bytes';
     7  import formatDuration from 'nomad-ui/utils/format-duration';
     8  import moment from 'moment';
     9  import ClientDetail from 'nomad-ui/tests/pages/clients/detail';
    10  import Clients from 'nomad-ui/tests/pages/clients/list';
    11  import Jobs from 'nomad-ui/tests/pages/jobs/list';
    12  
    13  let node;
    14  
    15  const wasPreemptedFilter = allocation => !!allocation.preemptedByAllocation;
    16  
    17  module('Acceptance | client detail', function(hooks) {
    18    setupApplicationTest(hooks);
    19    setupMirage(hooks);
    20  
    21    hooks.beforeEach(function() {
    22      server.create('node', 'forceIPv4', { schedulingEligibility: 'eligible' });
    23      node = server.db.nodes[0];
    24  
    25      // Related models
    26      server.create('agent');
    27      server.create('job', { createAllocations: false });
    28      server.createList('allocation', 3);
    29      server.create('allocation', 'preempted');
    30  
    31      // Force all allocations into the running state so now allocation rows are missing
    32      // CPU/Mem runtime metrics
    33      server.schema.allocations.all().models.forEach(allocation => {
    34        allocation.update({ clientStatus: 'running' });
    35      });
    36    });
    37  
    38    test('/clients/:id should have a breadcrumb trail linking back to clients', async function(assert) {
    39      await ClientDetail.visit({ id: node.id });
    40  
    41      assert.equal(
    42        ClientDetail.breadcrumbFor('clients.index').text,
    43        'Clients',
    44        'First breadcrumb says clients'
    45      );
    46      assert.equal(
    47        ClientDetail.breadcrumbFor('clients.client').text,
    48        node.id.split('-')[0],
    49        'Second breadcrumb says the node short id'
    50      );
    51      await ClientDetail.breadcrumbFor('clients.index').visit();
    52      assert.equal(currentURL(), '/clients', 'First breadcrumb links back to clients');
    53    });
    54  
    55    test('/clients/:id should list immediate details for the node in the title', async function(assert) {
    56      await ClientDetail.visit({ id: node.id });
    57  
    58      assert.ok(ClientDetail.title.includes(node.name), 'Title includes name');
    59      assert.ok(ClientDetail.title.includes(node.id), 'Title includes id');
    60      assert.equal(
    61        ClientDetail.statusLight.objectAt(0).id,
    62        node.status,
    63        'Title includes status light'
    64      );
    65    });
    66  
    67    test('/clients/:id should list additional detail for the node below the title', async function(assert) {
    68      await ClientDetail.visit({ id: node.id });
    69  
    70      assert.ok(
    71        ClientDetail.statusDefinition.includes(node.status),
    72        'Status is in additional details'
    73      );
    74      assert.ok(
    75        ClientDetail.statusDecorationClass.includes(`node-${node.status}`),
    76        'Status is decorated with a status class'
    77      );
    78      assert.ok(
    79        ClientDetail.addressDefinition.includes(node.httpAddr),
    80        'Address is in additional details'
    81      );
    82      assert.ok(
    83        ClientDetail.drainingDefinition.includes(node.drain + ''),
    84        'Drain status is in additional details'
    85      );
    86      assert.ok(
    87        ClientDetail.eligibilityDefinition.includes(node.schedulingEligibility),
    88        'Scheduling eligibility is in additional details'
    89      );
    90      assert.ok(
    91        ClientDetail.datacenterDefinition.includes(node.datacenter),
    92        'Datacenter is in additional details'
    93      );
    94    });
    95  
    96    test('/clients/:id should include resource utilization graphs', async function(assert) {
    97      await ClientDetail.visit({ id: node.id });
    98  
    99      assert.equal(ClientDetail.resourceCharts.length, 2, 'Two resource utilization graphs');
   100      assert.equal(ClientDetail.resourceCharts.objectAt(0).name, 'CPU', 'First chart is CPU');
   101      assert.equal(ClientDetail.resourceCharts.objectAt(1).name, 'Memory', 'Second chart is Memory');
   102    });
   103  
   104    test('/clients/:id should list all allocations on the node', async function(assert) {
   105      const allocationsCount = server.db.allocations.where({ nodeId: node.id }).length;
   106  
   107      await ClientDetail.visit({ id: node.id });
   108  
   109      assert.equal(
   110        ClientDetail.allocations.length,
   111        allocationsCount,
   112        `Allocations table lists all ${allocationsCount} associated allocations`
   113      );
   114    });
   115  
   116    test('each allocation should have high-level details for the allocation', async function(assert) {
   117      const allocation = server.db.allocations
   118        .where({ nodeId: node.id })
   119        .sortBy('modifyIndex')
   120        .reverse()[0];
   121  
   122      const allocStats = server.db.clientAllocationStats.find(allocation.id);
   123      const taskGroup = server.db.taskGroups.findBy({
   124        name: allocation.taskGroup,
   125        jobId: allocation.jobId,
   126      });
   127  
   128      const tasks = taskGroup.taskIds.map(id => server.db.tasks.find(id));
   129      const cpuUsed = tasks.reduce((sum, task) => sum + task.Resources.CPU, 0);
   130      const memoryUsed = tasks.reduce((sum, task) => sum + task.Resources.MemoryMB, 0);
   131  
   132      await ClientDetail.visit({ id: node.id });
   133  
   134      const allocationRow = ClientDetail.allocations.objectAt(0);
   135  
   136      assert.equal(allocationRow.shortId, allocation.id.split('-')[0], 'Allocation short ID');
   137      assert.equal(
   138        allocationRow.createTime,
   139        moment(allocation.createTime / 1000000).format('MMM DD HH:mm:ss ZZ'),
   140        'Allocation create time'
   141      );
   142      assert.equal(
   143        allocationRow.modifyTime,
   144        moment(allocation.modifyTime / 1000000).fromNow(),
   145        'Allocation modify time'
   146      );
   147      assert.equal(allocationRow.status, allocation.clientStatus, 'Client status');
   148      assert.equal(allocationRow.job, server.db.jobs.find(allocation.jobId).name, 'Job name');
   149      assert.ok(allocationRow.taskGroup, 'Task group name');
   150      assert.ok(allocationRow.jobVersion, 'Job Version');
   151      assert.equal(
   152        allocationRow.cpu,
   153        Math.floor(allocStats.resourceUsage.CpuStats.TotalTicks) / cpuUsed,
   154        'CPU %'
   155      );
   156      assert.equal(
   157        allocationRow.cpuTooltip,
   158        `${Math.floor(allocStats.resourceUsage.CpuStats.TotalTicks)} / ${cpuUsed} MHz`,
   159        'Detailed CPU information is in a tooltip'
   160      );
   161      assert.equal(
   162        allocationRow.mem,
   163        allocStats.resourceUsage.MemoryStats.RSS / 1024 / 1024 / memoryUsed,
   164        'Memory used'
   165      );
   166      assert.equal(
   167        allocationRow.memTooltip,
   168        `${formatBytes([allocStats.resourceUsage.MemoryStats.RSS])} / ${memoryUsed} MiB`,
   169        'Detailed memory information is in a tooltip'
   170      );
   171    });
   172  
   173    test('each allocation should show job information even if the job is incomplete and already in the store', async function(assert) {
   174      // First, visit clients to load the allocations for each visible node.
   175      // Don't load the job belongsTo of the allocation! Leave it unfulfilled.
   176  
   177      await Clients.visit();
   178  
   179      // Then, visit jobs to load all jobs, which should implicitly fulfill
   180      // the job belongsTo of each allocation pointed at each job.
   181  
   182      await Jobs.visit();
   183  
   184      // Finally, visit a node to assert that the job name and task group name are
   185      // present. This will require reloading the job, since task groups aren't a
   186      // part of the jobs list response.
   187  
   188      await ClientDetail.visit({ id: node.id });
   189  
   190      const allocationRow = ClientDetail.allocations.objectAt(0);
   191      const allocation = server.db.allocations
   192        .where({ nodeId: node.id })
   193        .sortBy('modifyIndex')
   194        .reverse()[0];
   195  
   196      assert.equal(allocationRow.job, server.db.jobs.find(allocation.jobId).name, 'Job name');
   197      assert.ok(allocationRow.taskGroup.includes(allocation.taskGroup), 'Task group name');
   198    });
   199  
   200    test('each allocation should link to the allocation detail page', async function(assert) {
   201      const allocation = server.db.allocations
   202        .where({ nodeId: node.id })
   203        .sortBy('modifyIndex')
   204        .reverse()[0];
   205  
   206      await ClientDetail.visit({ id: node.id });
   207      await ClientDetail.allocations.objectAt(0).visit();
   208  
   209      assert.equal(
   210        currentURL(),
   211        `/allocations/${allocation.id}`,
   212        'Allocation rows link to allocation detail pages'
   213      );
   214    });
   215  
   216    test('each allocation should link to the job the allocation belongs to', async function(assert) {
   217      await ClientDetail.visit({ id: node.id });
   218  
   219      const allocation = server.db.allocations.where({ nodeId: node.id })[0];
   220      const job = server.db.jobs.find(allocation.jobId);
   221  
   222      await ClientDetail.allocations.objectAt(0).visitJob();
   223  
   224      assert.equal(
   225        currentURL(),
   226        `/jobs/${job.id}`,
   227        'Allocation rows link to the job detail page for the allocation'
   228      );
   229    });
   230  
   231    test('the allocation section should show the count of preempted allocations on the client', async function(assert) {
   232      const allocations = server.db.allocations.where({ nodeId: node.id });
   233  
   234      await ClientDetail.visit({ id: node.id });
   235  
   236      assert.equal(
   237        ClientDetail.allocationFilter.allCount,
   238        allocations.length,
   239        'All filter/badge shows all allocations count'
   240      );
   241      assert.ok(
   242        ClientDetail.allocationFilter.preemptionsCount.startsWith(
   243          allocations.filter(wasPreemptedFilter).length
   244        ),
   245        'Preemptions filter/badge shows preempted allocations count'
   246      );
   247    });
   248  
   249    test('clicking the preemption badge filters the allocations table and sets a query param', async function(assert) {
   250      const allocations = server.db.allocations.where({ nodeId: node.id });
   251  
   252      await ClientDetail.visit({ id: node.id });
   253      await ClientDetail.allocationFilter.preemptions();
   254  
   255      assert.equal(
   256        ClientDetail.allocations.length,
   257        allocations.filter(wasPreemptedFilter).length,
   258        'Only preempted allocations are shown'
   259      );
   260      assert.equal(
   261        currentURL(),
   262        `/clients/${node.id}?preemptions=true`,
   263        'Filter is persisted in the URL'
   264      );
   265    });
   266  
   267    test('clicking the total allocations badge resets the filter and removes the query param', async function(assert) {
   268      const allocations = server.db.allocations.where({ nodeId: node.id });
   269  
   270      await ClientDetail.visit({ id: node.id });
   271      await ClientDetail.allocationFilter.preemptions();
   272      await ClientDetail.allocationFilter.all();
   273  
   274      assert.equal(ClientDetail.allocations.length, allocations.length, 'All allocations are shown');
   275      assert.equal(currentURL(), `/clients/${node.id}`, 'Filter is persisted in the URL');
   276    });
   277  
   278    test('navigating directly to the client detail page with the preemption query param set will filter the allocations table', async function(assert) {
   279      const allocations = server.db.allocations.where({ nodeId: node.id });
   280  
   281      await ClientDetail.visit({ id: node.id, preemptions: true });
   282  
   283      assert.equal(
   284        ClientDetail.allocations.length,
   285        allocations.filter(wasPreemptedFilter).length,
   286        'Only preempted allocations are shown'
   287      );
   288    });
   289  
   290    test('/clients/:id should list all attributes for the node', async function(assert) {
   291      await ClientDetail.visit({ id: node.id });
   292  
   293      assert.ok(ClientDetail.attributesTable, 'Attributes table is on the page');
   294    });
   295  
   296    test('/clients/:id lists all meta attributes', async function(assert) {
   297      node = server.create('node', 'forceIPv4', 'withMeta');
   298  
   299      await ClientDetail.visit({ id: node.id });
   300  
   301      assert.ok(ClientDetail.metaTable, 'Meta attributes table is on the page');
   302      assert.notOk(ClientDetail.emptyMetaMessage, 'Meta attributes is not empty');
   303  
   304      const firstMetaKey = Object.keys(node.meta)[0];
   305      const firstMetaAttribute = ClientDetail.metaAttributes.objectAt(0);
   306      assert.equal(
   307        firstMetaAttribute.key,
   308        firstMetaKey,
   309        'Meta attributes for the node are bound to the attributes table'
   310      );
   311      assert.equal(
   312        firstMetaAttribute.value,
   313        node.meta[firstMetaKey],
   314        'Meta attributes for the node are bound to the attributes table'
   315      );
   316    });
   317  
   318    test('/clients/:id shows an empty message when there is no meta data', async function(assert) {
   319      await ClientDetail.visit({ id: node.id });
   320  
   321      assert.notOk(ClientDetail.metaTable, 'Meta attributes table is not on the page');
   322      assert.ok(ClientDetail.emptyMetaMessage, 'Meta attributes is empty');
   323    });
   324  
   325    test('when the node is not found, an error message is shown, but the URL persists', async function(assert) {
   326      await ClientDetail.visit({ id: 'not-a-real-node' });
   327  
   328      assert.equal(
   329        server.pretender.handledRequests.findBy('status', 404).url,
   330        '/v1/node/not-a-real-node',
   331        'A request to the nonexistent node is made'
   332      );
   333      assert.equal(currentURL(), '/clients/not-a-real-node', 'The URL persists');
   334      assert.ok(ClientDetail.error.isShown, 'Error message is shown');
   335      assert.equal(ClientDetail.error.title, 'Not Found', 'Error message is for 404');
   336    });
   337  
   338    test('/clients/:id shows the recent events list', async function(assert) {
   339      await ClientDetail.visit({ id: node.id });
   340  
   341      assert.ok(ClientDetail.hasEvents, 'Client events section exists');
   342    });
   343  
   344    test('each node event shows basic node event information', async function(assert) {
   345      const event = server.db.nodeEvents
   346        .where({ nodeId: node.id })
   347        .sortBy('time')
   348        .reverse()[0];
   349  
   350      await ClientDetail.visit({ id: node.id });
   351  
   352      const eventRow = ClientDetail.events.objectAt(0);
   353      assert.equal(
   354        eventRow.time,
   355        moment(event.time).format("MMM DD, 'YY HH:mm:ss ZZ"),
   356        'Event timestamp'
   357      );
   358      assert.equal(eventRow.subsystem, event.subsystem, 'Event subsystem');
   359      assert.equal(eventRow.message, event.message, 'Event message');
   360    });
   361  
   362    test('/clients/:id shows the driver status of every driver for the node', async function(assert) {
   363      // Set the drivers up so health and detection is well tested
   364      const nodeDrivers = node.drivers;
   365      const undetectedDriver = 'raw_exec';
   366  
   367      Object.values(nodeDrivers).forEach(driver => {
   368        driver.Detected = true;
   369      });
   370  
   371      nodeDrivers[undetectedDriver].Detected = false;
   372      node.drivers = nodeDrivers;
   373  
   374      const drivers = Object.keys(node.drivers)
   375        .map(driverName => assign({ Name: driverName }, node.drivers[driverName]))
   376        .sortBy('Name');
   377  
   378      assert.ok(drivers.length > 0, 'Node has drivers');
   379  
   380      await ClientDetail.visit({ id: node.id });
   381  
   382      drivers.forEach((driver, index) => {
   383        const driverHead = ClientDetail.driverHeads.objectAt(index);
   384  
   385        assert.equal(driverHead.name, driver.Name, `${driver.Name}: Name is correct`);
   386        assert.equal(
   387          driverHead.detected,
   388          driver.Detected ? 'Yes' : 'No',
   389          `${driver.Name}: Detection is correct`
   390        );
   391        assert.equal(
   392          driverHead.lastUpdated,
   393          moment(driver.UpdateTime).fromNow(),
   394          `${driver.Name}: Last updated shows time since now`
   395        );
   396  
   397        if (driver.Name === undetectedDriver) {
   398          assert.notOk(
   399            driverHead.healthIsShown,
   400            `${driver.Name}: No health for the undetected driver`
   401          );
   402        } else {
   403          assert.equal(
   404            driverHead.health,
   405            driver.Healthy ? 'Healthy' : 'Unhealthy',
   406            `${driver.Name}: Health is correct`
   407          );
   408          assert.ok(
   409            driverHead.healthClass.includes(driver.Healthy ? 'running' : 'failed'),
   410            `${driver.Name}: Swatch with correct class is shown`
   411          );
   412        }
   413      });
   414    });
   415  
   416    test('each driver can be opened to see a message and attributes', async function(assert) {
   417      // Only detected drivers can be expanded
   418      const nodeDrivers = node.drivers;
   419      Object.values(nodeDrivers).forEach(driver => {
   420        driver.Detected = true;
   421      });
   422      node.drivers = nodeDrivers;
   423  
   424      const driver = Object.keys(node.drivers)
   425        .map(driverName => assign({ Name: driverName }, node.drivers[driverName]))
   426        .sortBy('Name')[0];
   427  
   428      await ClientDetail.visit({ id: node.id });
   429      const driverHead = ClientDetail.driverHeads.objectAt(0);
   430      const driverBody = ClientDetail.driverBodies.objectAt(0);
   431  
   432      assert.notOk(driverBody.descriptionIsShown, 'Driver health description is not shown');
   433      assert.notOk(driverBody.attributesAreShown, 'Driver attributes section is not shown');
   434  
   435      await driverHead.toggle();
   436      assert.equal(
   437        driverBody.description,
   438        driver.HealthDescription,
   439        'Driver health description is now shown'
   440      );
   441      assert.ok(driverBody.attributesAreShown, 'Driver attributes section is now shown');
   442    });
   443  
   444    test('the status light indicates when the node is ineligible for scheduling', async function(assert) {
   445      node = server.create('node', {
   446        schedulingEligibility: 'ineligible',
   447      });
   448  
   449      await ClientDetail.visit({ id: node.id });
   450  
   451      assert.equal(
   452        ClientDetail.statusLight.objectAt(0).id,
   453        'ineligible',
   454        'Title status light is in the ineligible state'
   455      );
   456    });
   457  
   458    test('when the node has a drain strategy with a positive deadline, the drain stategy section prints the duration', async function(assert) {
   459      const deadline = 5400000000000; // 1.5 hours in nanoseconds
   460      const forceDeadline = moment().add(1, 'd');
   461  
   462      node = server.create('node', {
   463        drain: true,
   464        schedulingEligibility: 'ineligible',
   465        drainStrategy: {
   466          Deadline: deadline,
   467          ForceDeadline: forceDeadline.toISOString(),
   468          IgnoreSystemJobs: false,
   469        },
   470      });
   471  
   472      await ClientDetail.visit({ id: node.id });
   473  
   474      assert.ok(
   475        ClientDetail.drain.deadline.includes(formatDuration(deadline)),
   476        'Deadline is shown in a human formatted way'
   477      );
   478  
   479      assert.ok(
   480        ClientDetail.drain.forcedDeadline.includes(forceDeadline.format("MMM DD, 'YY HH:mm:ss ZZ")),
   481        'Force deadline is shown as an absolute date'
   482      );
   483  
   484      assert.ok(
   485        ClientDetail.drain.forcedDeadline.includes(forceDeadline.fromNow()),
   486        'Force deadline is shown as a relative date'
   487      );
   488  
   489      assert.ok(
   490        ClientDetail.drain.ignoreSystemJobs.endsWith('No'),
   491        'Ignore System Jobs state is shown'
   492      );
   493    });
   494  
   495    test('when the node has a drain stategy with no deadline, the drain stategy section mentions that and omits the force deadline', async function(assert) {
   496      const deadline = 0;
   497  
   498      node = server.create('node', {
   499        drain: true,
   500        schedulingEligibility: 'ineligible',
   501        drainStrategy: {
   502          Deadline: deadline,
   503          ForceDeadline: '0001-01-01T00:00:00Z', // null as a date
   504          IgnoreSystemJobs: true,
   505        },
   506      });
   507  
   508      await ClientDetail.visit({ id: node.id });
   509  
   510      assert.ok(
   511        ClientDetail.drain.deadline.includes('No deadline'),
   512        'The value for Deadline is "no deadline"'
   513      );
   514  
   515      assert.notOk(
   516        ClientDetail.drain.hasForcedDeadline,
   517        'Forced deadline is not shown since there is no forced deadline'
   518      );
   519  
   520      assert.ok(
   521        ClientDetail.drain.ignoreSystemJobs.endsWith('Yes'),
   522        'Ignore System Jobs state is shown'
   523      );
   524    });
   525  
   526    test('when the node has a drain stategy with a negative deadline, the drain strategy section shows the force badge', async function(assert) {
   527      const deadline = -1;
   528  
   529      node = server.create('node', {
   530        drain: true,
   531        schedulingEligibility: 'ineligible',
   532        drainStrategy: {
   533          Deadline: deadline,
   534          ForceDeadline: '0001-01-01T00:00:00Z', // null as a date
   535          IgnoreSystemJobs: false,
   536        },
   537      });
   538  
   539      await ClientDetail.visit({ id: node.id });
   540  
   541      assert.equal(ClientDetail.drain.badgeLabel, 'Forced Drain', 'Forced Drain badge is described');
   542      assert.ok(ClientDetail.drain.badgeIsDangerous, 'Forced Drain is shown in a red badge');
   543  
   544      assert.notOk(
   545        ClientDetail.drain.hasForcedDeadline,
   546        'Forced deadline is not shown since there is no forced deadline'
   547      );
   548  
   549      assert.ok(
   550        ClientDetail.drain.ignoreSystemJobs.endsWith('No'),
   551        'Ignore System Jobs state is shown'
   552      );
   553    });
   554  });
   555  
   556  module('Acceptance | client detail (multi-namespace)', function(hooks) {
   557    setupApplicationTest(hooks);
   558    setupMirage(hooks);
   559  
   560    hooks.beforeEach(function() {
   561      server.create('node', 'forceIPv4', { schedulingEligibility: 'eligible' });
   562      node = server.db.nodes[0];
   563  
   564      // Related models
   565      server.create('namespace');
   566      server.create('namespace', { id: 'other-namespace' });
   567  
   568      server.create('agent');
   569  
   570      // Make a job for each namespace, but have both scheduled on the same node
   571      server.create('job', { id: 'job-1', namespaceId: 'default', createAllocations: false });
   572      server.createList('allocation', 3, { nodeId: node.id, clientStatus: 'running' });
   573  
   574      server.create('job', { id: 'job-2', namespaceId: 'other-namespace', createAllocations: false });
   575      server.createList('allocation', 3, {
   576        nodeId: node.id,
   577        jobId: 'job-2',
   578        clientStatus: 'running',
   579      });
   580    });
   581  
   582    test('when the node has allocations on different namespaces, the associated jobs are fetched correctly', async function(assert) {
   583      window.localStorage.nomadActiveNamespace = 'other-namespace';
   584  
   585      await ClientDetail.visit({ id: node.id });
   586  
   587      assert.equal(
   588        ClientDetail.allocations.length,
   589        server.db.allocations.length,
   590        'All allocations are scheduled on this node'
   591      );
   592      assert.ok(
   593        server.pretender.handledRequests.findBy('url', '/v1/job/job-1'),
   594        'Job One fetched correctly'
   595      );
   596      assert.ok(
   597        server.pretender.handledRequests.findBy('url', '/v1/job/job-2?namespace=other-namespace'),
   598        'Job Two fetched correctly'
   599      );
   600    });
   601  });