344 lines
11 KiB
JavaScript
344 lines
11 KiB
JavaScript
/**
|
|
* Copyright (c) HashiCorp, Inc.
|
|
* SPDX-License-Identifier: MPL-2.0
|
|
*/
|
|
|
|
/* eslint-disable qunit/require-expect */
|
|
import { get } from '@ember/object';
|
|
import { currentURL, typeIn, click } from '@ember/test-helpers';
|
|
import { module, test } from 'qunit';
|
|
import { setupApplicationTest } from 'ember-qunit';
|
|
import { setupMirage } from 'ember-cli-mirage/test-support';
|
|
import a11yAudit from 'nomad-ui/tests/helpers/a11y-audit';
|
|
import Topology from 'nomad-ui/tests/pages/topology';
|
|
import {
|
|
formatBytes,
|
|
formatScheduledBytes,
|
|
formatHertz,
|
|
formatScheduledHertz,
|
|
} from 'nomad-ui/utils/units';
|
|
import queryString from 'query-string';
|
|
import percySnapshot from '@percy/ember';
|
|
import faker from 'nomad-ui/mirage/faker';
|
|
|
|
const sumResources = (list, dimension) =>
|
|
list.reduce((agg, val) => agg + (get(val, dimension) || 0), 0);
|
|
|
|
module('Acceptance | topology', function (hooks) {
|
|
setupApplicationTest(hooks);
|
|
setupMirage(hooks);
|
|
|
|
hooks.beforeEach(function () {
|
|
server.create('job', { createAllocations: false });
|
|
});
|
|
|
|
test('it passes an accessibility audit', async function (assert) {
|
|
assert.expect(1);
|
|
|
|
server.createList('node', 3);
|
|
server.createList('allocation', 5);
|
|
|
|
await Topology.visit();
|
|
await a11yAudit(assert);
|
|
});
|
|
|
|
test('by default the info panel shows cluster aggregate stats', async function (assert) {
|
|
faker.seed(1);
|
|
server.createList('node', 3);
|
|
server.createList('allocation', 5);
|
|
|
|
await Topology.visit();
|
|
|
|
await percySnapshot(assert);
|
|
|
|
assert.equal(Topology.infoPanelTitle, 'Cluster Details');
|
|
assert.notOk(Topology.filteredNodesWarning.isPresent);
|
|
|
|
assert.equal(
|
|
Topology.clusterInfoPanel.nodeCount,
|
|
`${server.schema.nodes.all().length} Clients`
|
|
);
|
|
|
|
const allocs = server.schema.allocations.all().models;
|
|
const scheduledAllocs = allocs.filter((alloc) =>
|
|
['pending', 'running'].includes(alloc.clientStatus)
|
|
);
|
|
assert.equal(
|
|
Topology.clusterInfoPanel.allocCount,
|
|
`${scheduledAllocs.length} Allocations`
|
|
);
|
|
|
|
const nodeResources = server.schema.nodes
|
|
.all()
|
|
.models.mapBy('nodeResources');
|
|
const taskResources = scheduledAllocs
|
|
.mapBy('taskResources.models')
|
|
.flat()
|
|
.mapBy('resources');
|
|
|
|
const totalMem = sumResources(nodeResources, 'Memory.MemoryMB');
|
|
const totalCPU = sumResources(nodeResources, 'Cpu.CpuShares');
|
|
const reservedMem = sumResources(taskResources, 'Memory.MemoryMB');
|
|
const reservedCPU = sumResources(taskResources, 'Cpu.CpuShares');
|
|
|
|
assert.equal(
|
|
Topology.clusterInfoPanel.memoryProgressValue,
|
|
reservedMem / totalMem
|
|
);
|
|
assert.equal(
|
|
Topology.clusterInfoPanel.cpuProgressValue,
|
|
reservedCPU / totalCPU
|
|
);
|
|
|
|
assert.equal(
|
|
Topology.clusterInfoPanel.memoryAbsoluteValue,
|
|
`${formatBytes(reservedMem * 1024 * 1024)} / ${formatBytes(
|
|
totalMem * 1024 * 1024
|
|
)} reserved`
|
|
);
|
|
|
|
assert.equal(
|
|
Topology.clusterInfoPanel.cpuAbsoluteValue,
|
|
`${formatHertz(reservedCPU, 'MHz')} / ${formatHertz(
|
|
totalCPU,
|
|
'MHz'
|
|
)} reserved`
|
|
);
|
|
});
|
|
|
|
test('all allocations for all namespaces and all clients are queried on load', async function (assert) {
|
|
server.createList('node', 3);
|
|
server.createList('allocation', 5);
|
|
|
|
await Topology.visit();
|
|
const requests = this.server.pretender.handledRequests;
|
|
assert.ok(requests.findBy('url', '/v1/nodes?resources=true'));
|
|
|
|
const allocationsRequest = requests.find((req) =>
|
|
req.url.startsWith('/v1/allocations')
|
|
);
|
|
assert.ok(allocationsRequest);
|
|
|
|
const allocationRequestParams = queryString.parse(
|
|
allocationsRequest.url.split('?')[1]
|
|
);
|
|
assert.deepEqual(allocationRequestParams, {
|
|
namespace: '*',
|
|
task_states: 'false',
|
|
resources: 'true',
|
|
});
|
|
});
|
|
|
|
test('when an allocation is selected, the info panel shows information on the allocation', async function (assert) {
|
|
const nodes = server.createList('node', 5);
|
|
const job = server.create('job', { createAllocations: false });
|
|
const taskGroup = server.schema.find('taskGroup', job.taskGroupIds[0]).name;
|
|
const allocs = server.createList('allocation', 5, {
|
|
forceRunningClientStatus: true,
|
|
jobId: job.id,
|
|
taskGroup,
|
|
});
|
|
|
|
// Get the first alloc of the first node that has an alloc
|
|
const sortedNodes = nodes.sortBy('datacenter');
|
|
let node, alloc;
|
|
for (let n of sortedNodes) {
|
|
alloc = allocs.find((a) => a.nodeId === n.id);
|
|
if (alloc) {
|
|
node = n;
|
|
break;
|
|
}
|
|
}
|
|
|
|
const dcIndex = nodes
|
|
.mapBy('datacenter')
|
|
.uniq()
|
|
.sort()
|
|
.indexOf(node.datacenter);
|
|
const nodeIndex = nodes
|
|
.filterBy('datacenter', node.datacenter)
|
|
.indexOf(node);
|
|
|
|
const reset = async () => {
|
|
await Topology.visit();
|
|
await Topology.viz.datacenters[dcIndex].nodes[
|
|
nodeIndex
|
|
].memoryRects[0].select();
|
|
};
|
|
|
|
await reset();
|
|
assert.equal(Topology.infoPanelTitle, 'Allocation Details');
|
|
|
|
assert.equal(Topology.allocInfoPanel.id, alloc.id.split('-')[0]);
|
|
|
|
const uniqueClients = allocs.mapBy('nodeId').uniq();
|
|
assert.equal(
|
|
Topology.allocInfoPanel.siblingAllocs,
|
|
`Sibling Allocations: ${allocs.length}`
|
|
);
|
|
assert.equal(
|
|
Topology.allocInfoPanel.uniquePlacements,
|
|
`Unique Client Placements: ${uniqueClients.length}`
|
|
);
|
|
|
|
assert.equal(Topology.allocInfoPanel.job, job.name);
|
|
assert.ok(Topology.allocInfoPanel.taskGroup.endsWith(alloc.taskGroup));
|
|
assert.equal(Topology.allocInfoPanel.client, node.id.split('-')[0]);
|
|
|
|
await Topology.allocInfoPanel.visitAlloc();
|
|
assert.equal(currentURL(), `/allocations/${alloc.id}`);
|
|
|
|
await reset();
|
|
|
|
await Topology.allocInfoPanel.visitJob();
|
|
assert.equal(currentURL(), `/jobs/${job.id}@default`);
|
|
|
|
await reset();
|
|
|
|
await Topology.allocInfoPanel.visitClient();
|
|
assert.equal(currentURL(), `/clients/${node.id}`);
|
|
});
|
|
|
|
test('changing which allocation is selected changes the metric charts', async function (assert) {
|
|
server.create('node');
|
|
const job1 = server.create('job', { createAllocations: false });
|
|
const taskGroup1 = server.schema.find(
|
|
'taskGroup',
|
|
job1.taskGroupIds[0]
|
|
).name;
|
|
server.create('allocation', {
|
|
forceRunningClientStatus: true,
|
|
jobId: job1.id,
|
|
taskGroup1,
|
|
});
|
|
|
|
const job2 = server.create('job', { createAllocations: false });
|
|
const taskGroup2 = server.schema.find(
|
|
'taskGroup',
|
|
job2.taskGroupIds[0]
|
|
).name;
|
|
server.create('allocation', {
|
|
forceRunningClientStatus: true,
|
|
jobId: job2.id,
|
|
taskGroup2,
|
|
});
|
|
|
|
await Topology.visit();
|
|
await Topology.viz.datacenters[0].nodes[0].memoryRects[0].select();
|
|
const firstAllocationTaskNames =
|
|
Topology.allocInfoPanel.charts[0].areas.mapBy('taskName');
|
|
|
|
await Topology.viz.datacenters[0].nodes[0].memoryRects[1].select();
|
|
const secondAllocationTaskNames =
|
|
Topology.allocInfoPanel.charts[0].areas.mapBy('taskName');
|
|
|
|
assert.notDeepEqual(firstAllocationTaskNames, secondAllocationTaskNames);
|
|
});
|
|
|
|
test('when a node is selected, the info panel shows information on the node', async function (assert) {
|
|
// A high node count is required for node selection
|
|
const nodes = server.createList('node', 51);
|
|
const node = nodes.sortBy('datacenter')[0];
|
|
server.createList('allocation', 5, { forceRunningClientStatus: true });
|
|
|
|
const allocs = server.schema.allocations.where({ nodeId: node.id }).models;
|
|
|
|
await Topology.visit();
|
|
|
|
await Topology.viz.datacenters[0].nodes[0].selectNode();
|
|
assert.equal(Topology.infoPanelTitle, 'Client Details');
|
|
|
|
assert.equal(Topology.nodeInfoPanel.id, node.id.split('-')[0]);
|
|
assert.equal(Topology.nodeInfoPanel.name, `Name: ${node.name}`);
|
|
assert.equal(Topology.nodeInfoPanel.address, `Address: ${node.httpAddr}`);
|
|
assert.equal(Topology.nodeInfoPanel.status, `Status: ${node.status}`);
|
|
|
|
assert.equal(
|
|
Topology.nodeInfoPanel.drainingLabel,
|
|
node.drain ? 'Yes' : 'No'
|
|
);
|
|
assert.equal(
|
|
Topology.nodeInfoPanel.eligibleLabel,
|
|
node.schedulingEligibility === 'eligible' ? 'Yes' : 'No'
|
|
);
|
|
|
|
assert.equal(Topology.nodeInfoPanel.drainingIsAccented, node.drain);
|
|
assert.equal(
|
|
Topology.nodeInfoPanel.eligibleIsAccented,
|
|
node.schedulingEligibility !== 'eligible'
|
|
);
|
|
|
|
const taskResources = allocs
|
|
.mapBy('taskResources.models')
|
|
.flat()
|
|
.mapBy('resources');
|
|
const reservedMem = sumResources(taskResources, 'Memory.MemoryMB');
|
|
const reservedCPU = sumResources(taskResources, 'Cpu.CpuShares');
|
|
|
|
const totalMem = node.nodeResources.Memory.MemoryMB;
|
|
const totalCPU = node.nodeResources.Cpu.CpuShares;
|
|
|
|
assert.equal(
|
|
Topology.nodeInfoPanel.memoryProgressValue,
|
|
reservedMem / totalMem
|
|
);
|
|
assert.equal(
|
|
Topology.nodeInfoPanel.cpuProgressValue,
|
|
reservedCPU / totalCPU
|
|
);
|
|
|
|
assert.equal(
|
|
Topology.nodeInfoPanel.memoryAbsoluteValue,
|
|
`${formatScheduledBytes(
|
|
reservedMem * 1024 * 1024
|
|
)} / ${formatScheduledBytes(totalMem, 'MiB')} reserved`
|
|
);
|
|
|
|
assert.equal(
|
|
Topology.nodeInfoPanel.cpuAbsoluteValue,
|
|
`${formatScheduledHertz(reservedCPU, 'MHz')} / ${formatScheduledHertz(
|
|
totalCPU,
|
|
'MHz'
|
|
)} reserved`
|
|
);
|
|
|
|
await Topology.nodeInfoPanel.visitNode();
|
|
assert.equal(currentURL(), `/clients/${node.id}`);
|
|
});
|
|
|
|
test('when one or more nodes lack the NodeResources property, a warning message is shown', async function (assert) {
|
|
server.createList('node', 3);
|
|
server.createList('allocation', 5);
|
|
|
|
server.schema.nodes.all().models[0].update({ nodeResources: null });
|
|
|
|
await Topology.visit();
|
|
assert.ok(Topology.filteredNodesWarning.isPresent);
|
|
assert.ok(Topology.filteredNodesWarning.message.startsWith('1'));
|
|
});
|
|
|
|
test('Filtering and Querying reduces the number of nodes shown', async function (assert) {
|
|
server.createList('node', 10);
|
|
server.createList('node', 2, {
|
|
nodeClass: 'foo-bar-baz',
|
|
});
|
|
server.createList('allocation', 5);
|
|
|
|
await Topology.visit();
|
|
assert.dom('[data-test-topo-viz-node]').exists({ count: 12 });
|
|
|
|
await typeIn('input.node-search', server.schema.nodes.first().name);
|
|
assert.dom('[data-test-topo-viz-node]').exists({ count: 1 });
|
|
await typeIn('input.node-search', server.schema.nodes.first().name);
|
|
assert.dom('[data-test-topo-viz-node]').doesNotExist();
|
|
await click('[title="Clear search"]');
|
|
assert.dom('[data-test-topo-viz-node]').exists({ count: 12 });
|
|
|
|
await Topology.facets.class.toggle();
|
|
await Topology.facets.class.options
|
|
.findOneBy('label', 'foo-bar-baz')
|
|
.toggle();
|
|
assert.dom('[data-test-topo-viz-node]').exists({ count: 2 });
|
|
});
|
|
});
|