diff --git a/ui/app/components/topo-viz.js b/ui/app/components/topo-viz.js index 18a9c3735..d1254a0dc 100644 --- a/ui/app/components/topo-viz.js +++ b/ui/app/components/topo-viz.js @@ -83,10 +83,18 @@ export default class TopoViz extends Component { const nodes = this.args.nodes; const allocations = this.args.allocations; + // Nodes may not have a resources property due to having an old Nomad agent version. + const badNodes = []; + // Wrap nodes in a topo viz specific data structure and build an index to speed up allocation assignment const nodeContainers = []; const nodeIndex = {}; nodes.forEach(node => { + if (!node.resources) { + badNodes.push(node); + return; + } + const container = this.dataForNode(node); nodeContainers.push(container); nodeIndex[node.id] = container; @@ -99,7 +107,7 @@ export default class TopoViz extends Component { const nodeId = allocation.belongsTo('node').id(); const nodeContainer = nodeIndex[nodeId]; - // Ignore orphaned allocations + // Ignore orphaned allocations and allocations on nodes with an old Nomad agent version. if (!nodeContainer) return; const allocationContainer = this.dataForAllocation(allocation, nodeContainer); @@ -131,6 +139,15 @@ export default class TopoViz extends Component { .domain(extent(nodeContainers.mapBy('memory'))), }; this.topology = topology; + + if (badNodes.length && this.args.onDataError) { + this.args.onDataError([ + { + type: 'filtered-nodes', + context: badNodes, + }, + ]); + } } @action diff --git a/ui/app/controllers/topology.js b/ui/app/controllers/topology.js index 9b4d98fdf..9bb0bec43 100644 --- a/ui/app/controllers/topology.js +++ b/ui/app/controllers/topology.js @@ -2,6 +2,7 @@ import Controller from '@ember/controller'; import { computed, action } from '@ember/object'; import { alias } from '@ember/object/computed'; import { inject as service } from '@ember/service'; +import { tracked } from '@glimmer/tracking'; import classic from 'ember-classic-decorator'; import { reduceToLargestUnit } from 'nomad-ui/helpers/format-bytes'; @@ -13,6 +14,8 @@ export default class TopologyControllers extends Controller { @alias('userSettings.showTopoVizPollingNotice') showPollingNotice; + @tracked filteredNodes = null; + @computed('model.nodes.@each.datacenter') get datacenters() { return Array.from(new Set(this.model.nodes.mapBy('datacenter'))).compact(); @@ -117,4 +120,12 @@ export default class TopologyControllers extends Controller { setNode(node) { this.set('activeNode', node); } + + @action + handleTopoVizDataError(errors) { + const filteredNodesError = errors.findBy('type', 'filtered-nodes'); + if (filteredNodesError) { + this.filteredNodes = filteredNodesError.context; + } + } } diff --git a/ui/app/templates/topology.hbs b/ui/app/templates/topology.hbs index e88731d8d..c41b1de9c 100644 --- a/ui/app/templates/topology.hbs +++ b/ui/app/templates/topology.hbs @@ -4,6 +4,19 @@ {{#if this.isForbidden}} {{else}} + {{#if this.filteredNodes}} +
+
+
+

Some Clients Were Filtered

+

{{this.filteredNodes.length}} {{if (eq this.filteredNodes.length 1) "client was" "clients were"}} filtered from the topology visualization. This is most likely due to the {{pluralize "client" this.filteredNodes.length}} running a version of Nomad <0.9.0.

+
+
+ +
+
+
+ {{/if}}
{{#if this.showPollingNotice}} @@ -217,7 +230,8 @@ @nodes={{this.model.nodes}} @allocations={{this.model.allocations}} @onAllocationSelect={{action this.setAllocation}} - @onNodeSelect={{action this.setNode}} /> + @onNodeSelect={{action this.setNode}} + @onDataError={{action this.handleTopoVizDataError}}/>
{{/if}} diff --git a/ui/tests/acceptance/topology-test.js b/ui/tests/acceptance/topology-test.js index e641f1dbf..70e510fb8 100644 --- a/ui/tests/acceptance/topology-test.js +++ b/ui/tests/acceptance/topology-test.js @@ -29,6 +29,7 @@ module('Acceptance | topology', function(hooks) { await Topology.visit(); assert.equal(Topology.infoPanelTitle, 'Cluster Details'); + assert.notOk(Topology.filteredNodesWarning.isPresent); }); test('all allocations for all namespaces and all clients are queried on load', async function(assert) { @@ -74,4 +75,15 @@ module('Acceptance | topology', function(hooks) { await Topology.viz.datacenters[0].nodes[0].selectNode(); assert.equal(Topology.infoPanelTitle, 'Client Details'); }); + + 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')); + }); }); diff --git a/ui/tests/integration/components/topo-viz-test.js b/ui/tests/integration/components/topo-viz-test.js index b60d72cda..76869b98e 100644 --- a/ui/tests/integration/components/topo-viz-test.js +++ b/ui/tests/integration/components/topo-viz-test.js @@ -39,7 +39,8 @@ module('Integration | Component | TopoViz', function(hooks) { @nodes={{this.nodes}} @allocations={{this.allocations}} @onAllocationSelect={{this.onAllocationSelect}} - @onNodeSelect={{this.onNodeSelect}} /> + @onNodeSelect={{this.onNodeSelect}} + @onDataError={{this.onDataError}} /> `; test('presents as a FlexMasonry of datacenters', async function(assert) { @@ -167,4 +168,35 @@ module('Integration | Component | TopoViz', function(hooks) { await TopoViz.datacenters[0].nodes[0].memoryRects[0].select(); assert.equal(TopoViz.allocationAssociations.length, 0); }); + + test('when one or more nodes are missing the resources property, those nodes are filtered out of the topology view and onDataError is called', async function(assert) { + const badNode = node('dc1', 'node0', 1000, 500); + delete badNode.resources; + + this.setProperties({ + nodes: [badNode, node('dc1', 'node1', 1000, 500)], + allocations: [ + alloc('node0', 'job1', 'group', 100, 100), + alloc('node0', 'job1', 'group', 100, 100), + alloc('node1', 'job1', 'group', 100, 100), + alloc('node1', 'job1', 'group', 100, 100), + alloc('node0', 'job1', 'groupTwo', 100, 100), + ], + onNodeSelect: sinon.spy(), + onAllocationSelect: sinon.spy(), + onDataError: sinon.spy(), + }); + + await this.render(commonTemplate); + + assert.ok(this.onDataError.calledOnce); + assert.deepEqual(this.onDataError.getCall(0).args[0], [ + { + type: 'filtered-nodes', + context: [this.nodes[0]], + }, + ]); + + assert.equal(TopoViz.datacenters[0].nodes.length, 1); + }); }); diff --git a/ui/tests/pages/topology.js b/ui/tests/pages/topology.js index b9f2c63a6..63e38de22 100644 --- a/ui/tests/pages/topology.js +++ b/ui/tests/pages/topology.js @@ -1,11 +1,13 @@ import { create, text, visitable } from 'ember-cli-page-object'; import TopoViz from 'nomad-ui/tests/pages/components/topo-viz'; +import notification from 'nomad-ui/tests/pages/components/notification'; export default create({ visit: visitable('/topology'), infoPanelTitle: text('[data-test-info-panel-title]'), + filteredNodesWarning: notification('[data-test-filtered-nodes-warning]'), viz: TopoViz('[data-test-topo-viz]'), });