ui: Discovery-Chain: Cope with some edge case configs (#7174)

* ui: Discovery-Chain: Cope with redirects that have failovers

We found a few stranger configurations for discovery-chain, one of which
was redirects that can then failover.

We altered the parsing here to include 2 passes, one to organize the
nodes into resolvers and children/subsets based on the nodes themselves, which
includes adding the failovers to resolvers and subsets.

We then do a second pass which can more reliably figure out whether a
target is a redirect or a failover (target failovers don't have a
corresponding node), this then adds the redirect children to the already
exising resolver (from the first pass) and then checks if the redirect
also has failovers and adds those if so.

* ui: Check to see if we have a user configured default route or not

...if we don't add one so the visualization looks complete
This commit is contained in:
John Cowen 2020-01-30 16:09:05 +00:00 committed by GitHub
parent c5f184f61d
commit 85ea64211f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 218 additions and 44 deletions

View File

@ -56,7 +56,33 @@ export default Component.extend({
return getSplitters(get(this, 'chain.Nodes'));
}),
routes: computed('chain.Nodes', function() {
return getRoutes(get(this, 'chain.Nodes'), this.dom.guid);
const routes = getRoutes(get(this, 'chain.Nodes'), this.dom.guid);
// if we have no routes with a PathPrefix of '/' or one with no definition at all
// then add our own 'default catch all'
if (
!routes.find(item => get(item, 'Definition.Match.HTTP.PathPrefix') === '/') &&
!routes.find(item => typeof item.Definition === 'undefined')
) {
let nextNode = `resolver:${this.chain.ServiceName}.${this.chain.Namespace}.${this.chain.Datacenter}`;
const splitterID = `splitter:${this.chain.ServiceName}`;
if (typeof this.chain.Nodes[splitterID] !== 'undefined') {
nextNode = splitterID;
}
routes.push({
Default: true,
ID: `route:${this.chain.ServiceName}`,
Name: this.chain.ServiceName,
Definition: {
Match: {
HTTP: {
PathPrefix: '/',
},
},
},
NextNode: nextNode,
});
}
return routes;
}),
resolvers: computed('chain.{Nodes,Targets}', function() {
return getResolvers(

View File

@ -23,7 +23,29 @@
{{#each item.Children as |child|}}
<li onclick={{onclick}} id={{concat 'resolver:' child.ID}}>
<a name="">
{{#if child.Failover}}
{{#if child.Redirect}}
<dl class="redirect">
<dt data-tooltip="Redirect">Redirect</dt>
<dd>
{{child.Name}}
</dd>
</dl>
{{#if child.Failover}}
<dl class="failover">
<dt data-tooltip={{concat child.Failover.Type ' failover'}}>{{concat child.Failover.Type ' failover'}}</dt>
<dd>
<ol>
{{#each child.Failover.Targets as |target|}}
<li>
<span>{{target}}</span>
</li>
{{/each}}
</ol>
</dd>
</dl>
{{/if}}
{{else if child.Failover}}
{{child.Name}}
<dl class="failover">
<dt data-tooltip={{concat child.Failover.Type ' failover'}}>{{concat child.Failover.Type ' failover'}}</dt>
<dd>
@ -36,13 +58,6 @@
</ol>
</dd>
</dl>
{{else if child.Redirect}}
<dl class="redirect">
<dt data-tooltip="Redirect">Redirect</dt>
<dd>
{{child.Name}}
</dd>
</dl>
{{else}}
{{child.Name}}
{{/if}}

View File

@ -53,45 +53,74 @@ export const getRoutes = function(nodes, uid) {
};
export const getResolvers = function(dc, nspace = 'default', targets = {}, nodes = {}) {
const resolvers = {};
Object.values(targets).forEach(target => {
const node = nodes[`resolver:${target.ID}`];
const resolver = findResolver(resolvers, target.Service, nspace, dc);
// We use this to figure out whether this target is a redirect target
const alternate = getAlternateServices([target.ID], `service.${nspace}.${dc}`);
let failovers;
// Figure out the failover type
if (typeof node.Resolver.Failover !== 'undefined') {
failovers = getAlternateServices(node.Resolver.Failover.Targets, target.ID);
}
switch (true) {
// This target is a redirect
case alternate.Type !== 'Service':
resolver.Children.push({
Redirect: true,
ID: target.ID,
Name: target[alternate.Type],
});
break;
// This target is a Subset
case typeof target.ServiceSubset !== 'undefined':
resolver.Children.push({
// make all our resolver nodes
Object.values(nodes)
.filter(item => item.Type === 'resolver')
.forEach(function(item) {
const parts = item.Name.split('.');
let subset;
// this will leave behind the service.name.nspace.dc even if the service name contains a dot
if (parts.length > 3) {
subset = parts.shift();
}
parts.reverse();
// slice off from dc.nspace onwards leaving the potentially dot containing service name
// const nodeDc =
parts.shift();
// const nodeNspace =
parts.shift();
// if it does contain a dot put it back to the correct order
parts.reverse();
const service = parts.join('.');
const resolver = findResolver(resolvers, service, nspace, dc);
let failovers;
if (typeof item.Resolver.Failover !== 'undefined') {
// figure out what type of failover this is
failovers = getAlternateServices(item.Resolver.Failover.Targets, item.Name);
}
if (subset) {
const child = {
Subset: true,
ID: target.ID,
Name: target.ServiceSubset,
Filter: target.Subset.Filter,
...(typeof failovers !== 'undefined'
? {
Failover: failovers,
}
: {}),
});
break;
// This target is just normal service that might have failovers
default:
ID: item.Name,
Name: subset,
};
if (typeof failovers !== 'undefined') {
child.Failover = failovers;
}
resolver.Children.push(child);
} else {
if (typeof failovers !== 'undefined') {
resolver.Failover = failovers;
}
}
});
Object.values(targets).forEach(target => {
// Failovers don't have a specific node
if (typeof nodes[`resolver:${target.ID}`] !== 'undefined') {
// We use this to figure out whether this target is a redirect target
const alternate = getAlternateServices([target.ID], `service.${nspace}.${dc}`);
// as Failovers don't make it here, we know anything that has alternateServices
// must be a redirect
if (alternate.Type !== 'Service') {
// find the already created resolver
const resolver = findResolver(resolvers, target.Service, nspace, dc);
// and add the redirect as a child, redirects are always children
const child = {
Redirect: true,
ID: target.ID,
Name: target[alternate.Type],
};
// redirects can then also have failovers
// so it this one does, figure out what type they are and add them
// to the redirect
if (typeof nodes[`resolver:${target.ID}`].Resolver.Failover !== 'undefined') {
child.Failover = getAlternateServices(
nodes[`resolver:${target.ID}`].Resolver.Failover.Targets,
target.ID
);
}
resolver.Children.push(child);
}
}
});
return Object.values(resolvers);

View File

@ -92,4 +92,108 @@ module('Unit | Utility | components/discovery-chain/get-resolvers', function() {
})
);
});
test('it finds subsets with failovers correctly', function(assert) {
return Promise.resolve({
Chain: {
ServiceName: 'service-name',
Namespace: 'default',
Datacenter: 'dc-1',
Protocol: 'http',
StartNode: '',
Nodes: {
'resolver:v2.dc-failover.default.dc-1': {
Type: 'resolver',
Name: 'v2.dc-failover.default.dc-1',
Resolver: {
Target: 'v2.dc-failover.defauilt.dc-1',
Failover: {
Targets: ['v2.dc-failover.default.dc-5', 'v2.dc-failover.default.dc-6'],
},
},
},
},
Targets: {
'v2.dc-failover.default.dc-1': {
ID: 'v2.dc-failover.default.dc-1',
Service: 'dc-failover',
Namespace: 'default',
Datacenter: 'dc-1',
Subset: {
Filter: '',
},
},
'v2.dc-failover.default.dc-6': {
ID: 'v2.dc-failover.default.dc-6',
Service: 'dc-failover',
Namespace: 'default',
Datacenter: 'dc-6',
Subset: {
Filter: '',
},
},
},
},
}).then(function({ Chain }) {
const actual = getResolvers(dc, nspace, Chain.Targets, Chain.Nodes);
const expected = {
ID: 'dc-failover.default.dc-1',
Name: 'dc-failover',
Children: [
{
Subset: true,
ID: 'v2.dc-failover.default.dc-1',
Name: 'v2',
Failover: {
Type: 'Datacenter',
Targets: ['dc-5', 'dc-6'],
},
},
],
};
assert.deepEqual(actual[0], expected);
});
});
test('it finds services with failovers correctly', function(assert) {
return Promise.resolve({
Chain: {
ServiceName: 'service-name',
Namespace: 'default',
Datacenter: 'dc-1',
Protocol: 'http',
StartNode: '',
Nodes: {
'resolver:dc-failover.default.dc-1': {
Type: 'resolver',
Name: 'dc-failover.default.dc-1',
Resolver: {
Target: 'dc-failover.defauilt.dc-1',
Failover: {
Targets: ['dc-failover.default.dc-5', 'dc-failover.default.dc-6'],
},
},
},
},
Targets: {
'dc-failover.default.dc-1': {
ID: 'dc-failover.default.dc-1',
Service: 'dc-failover',
Namespace: 'default',
Datacenter: 'dc-1',
},
},
},
}).then(function({ Chain }) {
const actual = getResolvers(dc, nspace, Chain.Targets, Chain.Nodes);
const expected = {
ID: 'dc-failover.default.dc-1',
Name: 'dc-failover',
Children: [],
Failover: {
Type: 'Datacenter',
Targets: ['dc-5', 'dc-6'],
},
};
assert.deepEqual(actual[0], expected);
});
});
});