Skip to content

Commit a53c8eb

Browse files
Merge pull request #9733 from hashicorp/b-ui/topo-viz-old-agent
UI: Guard against nodes running an old version of the Nomad agent
2 parents 08af8eb + 37e1551 commit a53c8eb

File tree

6 files changed

+91
-3
lines changed

6 files changed

+91
-3
lines changed

ui/app/components/topo-viz.js

+18-1
Original file line numberDiff line numberDiff line change
@@ -83,10 +83,18 @@ export default class TopoViz extends Component {
8383
const nodes = this.args.nodes;
8484
const allocations = this.args.allocations;
8585

86+
// Nodes may not have a resources property due to having an old Nomad agent version.
87+
const badNodes = [];
88+
8689
// Wrap nodes in a topo viz specific data structure and build an index to speed up allocation assignment
8790
const nodeContainers = [];
8891
const nodeIndex = {};
8992
nodes.forEach(node => {
93+
if (!node.resources) {
94+
badNodes.push(node);
95+
return;
96+
}
97+
9098
const container = this.dataForNode(node);
9199
nodeContainers.push(container);
92100
nodeIndex[node.id] = container;
@@ -99,7 +107,7 @@ export default class TopoViz extends Component {
99107
const nodeId = allocation.belongsTo('node').id();
100108
const nodeContainer = nodeIndex[nodeId];
101109

102-
// Ignore orphaned allocations
110+
// Ignore orphaned allocations and allocations on nodes with an old Nomad agent version.
103111
if (!nodeContainer) return;
104112

105113
const allocationContainer = this.dataForAllocation(allocation, nodeContainer);
@@ -131,6 +139,15 @@ export default class TopoViz extends Component {
131139
.domain(extent(nodeContainers.mapBy('memory'))),
132140
};
133141
this.topology = topology;
142+
143+
if (badNodes.length && this.args.onDataError) {
144+
this.args.onDataError([
145+
{
146+
type: 'filtered-nodes',
147+
context: badNodes,
148+
},
149+
]);
150+
}
134151
}
135152

136153
@action

ui/app/controllers/topology.js

+11
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import Controller from '@ember/controller';
22
import { computed, action } from '@ember/object';
33
import { alias } from '@ember/object/computed';
44
import { inject as service } from '@ember/service';
5+
import { tracked } from '@glimmer/tracking';
56
import classic from 'ember-classic-decorator';
67
import { reduceToLargestUnit } from 'nomad-ui/helpers/format-bytes';
78

@@ -13,6 +14,8 @@ export default class TopologyControllers extends Controller {
1314

1415
@alias('userSettings.showTopoVizPollingNotice') showPollingNotice;
1516

17+
@tracked filteredNodes = null;
18+
1619
@computed('[email protected]')
1720
get datacenters() {
1821
return Array.from(new Set(this.model.nodes.mapBy('datacenter'))).compact();
@@ -117,4 +120,12 @@ export default class TopologyControllers extends Controller {
117120
setNode(node) {
118121
this.set('activeNode', node);
119122
}
123+
124+
@action
125+
handleTopoVizDataError(errors) {
126+
const filteredNodesError = errors.findBy('type', 'filtered-nodes');
127+
if (filteredNodesError) {
128+
this.filteredNodes = filteredNodesError.context;
129+
}
130+
}
120131
}

ui/app/templates/topology.hbs

+15-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,19 @@
44
{{#if this.isForbidden}}
55
<ForbiddenMessage />
66
{{else}}
7+
{{#if this.filteredNodes}}
8+
<div class="notification is-warning">
9+
<div data-test-filtered-nodes-warning class="columns">
10+
<div class="column">
11+
<h3 data-test-title class="title is-4">Some Clients Were Filtered</h3>
12+
<p data-test-message>{{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 &lt;0.9.0.</p>
13+
</div>
14+
<div class="column is-centered is-minimum">
15+
<button data-test-dismiss class="button is-warning" onclick={{action (mut this.filteredNodes) null}} type="button">Okay</button>
16+
</div>
17+
</div>
18+
</div>
19+
{{/if}}
720
<div class="columns">
821
<div class="column is-narrow is-400">
922
{{#if this.showPollingNotice}}
@@ -217,7 +230,8 @@
217230
@nodes={{this.model.nodes}}
218231
@allocations={{this.model.allocations}}
219232
@onAllocationSelect={{action this.setAllocation}}
220-
@onNodeSelect={{action this.setNode}} />
233+
@onNodeSelect={{action this.setNode}}
234+
@onDataError={{action this.handleTopoVizDataError}}/>
221235
</div>
222236
</div>
223237
{{/if}}

ui/tests/acceptance/topology-test.js

+12
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ module('Acceptance | topology', function(hooks) {
2929

3030
await Topology.visit();
3131
assert.equal(Topology.infoPanelTitle, 'Cluster Details');
32+
assert.notOk(Topology.filteredNodesWarning.isPresent);
3233
});
3334

3435
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) {
7475
await Topology.viz.datacenters[0].nodes[0].selectNode();
7576
assert.equal(Topology.infoPanelTitle, 'Client Details');
7677
});
78+
79+
test('when one or more nodes lack the NodeResources property, a warning message is shown', async function(assert) {
80+
server.createList('node', 3);
81+
server.createList('allocation', 5);
82+
83+
server.schema.nodes.all().models[0].update({ nodeResources: null });
84+
85+
await Topology.visit();
86+
assert.ok(Topology.filteredNodesWarning.isPresent);
87+
assert.ok(Topology.filteredNodesWarning.message.startsWith('1'));
88+
});
7789
});

ui/tests/integration/components/topo-viz-test.js

+33-1
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,8 @@ module('Integration | Component | TopoViz', function(hooks) {
3939
@nodes={{this.nodes}}
4040
@allocations={{this.allocations}}
4141
@onAllocationSelect={{this.onAllocationSelect}}
42-
@onNodeSelect={{this.onNodeSelect}} />
42+
@onNodeSelect={{this.onNodeSelect}}
43+
@onDataError={{this.onDataError}} />
4344
`;
4445

4546
test('presents as a FlexMasonry of datacenters', async function(assert) {
@@ -167,4 +168,35 @@ module('Integration | Component | TopoViz', function(hooks) {
167168
await TopoViz.datacenters[0].nodes[0].memoryRects[0].select();
168169
assert.equal(TopoViz.allocationAssociations.length, 0);
169170
});
171+
172+
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) {
173+
const badNode = node('dc1', 'node0', 1000, 500);
174+
delete badNode.resources;
175+
176+
this.setProperties({
177+
nodes: [badNode, node('dc1', 'node1', 1000, 500)],
178+
allocations: [
179+
alloc('node0', 'job1', 'group', 100, 100),
180+
alloc('node0', 'job1', 'group', 100, 100),
181+
alloc('node1', 'job1', 'group', 100, 100),
182+
alloc('node1', 'job1', 'group', 100, 100),
183+
alloc('node0', 'job1', 'groupTwo', 100, 100),
184+
],
185+
onNodeSelect: sinon.spy(),
186+
onAllocationSelect: sinon.spy(),
187+
onDataError: sinon.spy(),
188+
});
189+
190+
await this.render(commonTemplate);
191+
192+
assert.ok(this.onDataError.calledOnce);
193+
assert.deepEqual(this.onDataError.getCall(0).args[0], [
194+
{
195+
type: 'filtered-nodes',
196+
context: [this.nodes[0]],
197+
},
198+
]);
199+
200+
assert.equal(TopoViz.datacenters[0].nodes.length, 1);
201+
});
170202
});

ui/tests/pages/topology.js

+2
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import { create, text, visitable } from 'ember-cli-page-object';
22

33
import TopoViz from 'nomad-ui/tests/pages/components/topo-viz';
4+
import notification from 'nomad-ui/tests/pages/components/notification';
45

56
export default create({
67
visit: visitable('/topology'),
78

89
infoPanelTitle: text('[data-test-info-panel-title]'),
10+
filteredNodesWarning: notification('[data-test-filtered-nodes-warning]'),
911

1012
viz: TopoViz('[data-test-topo-viz]'),
1113
});

0 commit comments

Comments
 (0)