UI: Add forking based on service instance id existence (#5392)

* ui: Add forking based on service instance id existence

Proxies come in 2 flavours, 'normal' and sidecar. We know when a proxy
is a sidecar proxy based on whether a DestinationServiceID is set.

LocalServiceAddress and LocalServicePort are only relevant for sidecar
proxies.

This adds template logic to show different text depending on this
information.

Additionally adds test around connect proxies (#5418)

1. Adds page object for the instance detail page
2. Adds further scenario steps used in the tests
3. Adds acceptance testing around the instance detail page. Services
with proxies and the sidecar proxies and proxies themselves
4. Adds datacenter column for upstreams
5. Fixes bug routing bug for decision as to whether to request proxy
information or not
This commit is contained in:
John Cowen 2019-03-07 11:51:39 +01:00 committed by John Cowen
parent 6186e0c8d4
commit 75d8abd562
27 changed files with 356 additions and 28 deletions

View File

@ -8,5 +8,5 @@ export default Model.extend({
[SLUG_KEY]: attr('string'), [SLUG_KEY]: attr('string'),
ServiceName: attr('string'), ServiceName: attr('string'),
ServiceID: attr('string'), ServiceID: attr('string'),
ServiceProxyDestination: attr('string'), ServiceProxy: attr(),
}); });

View File

@ -15,9 +15,9 @@ export default Route.extend({
}).then(function(model) { }).then(function(model) {
return hash({ return hash({
proxy: proxy:
get(service, 'Kind') !== 'connect-proxy' get(model.item, 'Kind') === 'connect-proxy'
? proxyRepo.findInstanceBySlug(params.id, params.name, dc) ? null
: null, : proxyRepo.findInstanceBySlug(params.id, params.name, dc),
...model, ...model,
}); });
}); });

View File

@ -22,7 +22,11 @@ export default RepositoryService.extend({
findInstanceBySlug: function(id, slug, dc, configuration) { findInstanceBySlug: function(id, slug, dc, configuration) {
return this.findAllBySlug(slug, dc, configuration).then(function(items) { return this.findAllBySlug(slug, dc, configuration).then(function(items) {
if (get(items, 'length') > 0) { if (get(items, 'length') > 0) {
const instance = items.findBy('ServiceProxyDestination', id); let instance = items.findBy('ServiceProxy.DestinationServiceID', id);
if (instance) {
return instance;
}
instance = items.findBy('ServiceProxy.DestinationServiceName', slug);
if (instance) { if (instance) {
return instance; return instance;
} }

View File

@ -35,7 +35,15 @@ export default RepositoryService.extend({
}); });
return service; return service;
} }
// TODO: probably need to throw a 404 here? // TODO: Add an store.error("404", "message") or similar
const e = new Error();
e.errors = [
{
status: '404',
title: 'Unable to find instance',
},
];
throw e;
}); });
}, },
}); });

View File

@ -181,5 +181,5 @@ html.template-node.template-show main table.sessions tr {
width: calc(100% / 5); width: calc(100% / 5);
} }
%upstreams-row > * { %upstreams-row > * {
width: calc(100% / 3); width: calc(100% / 4);
} }

View File

@ -1,4 +1,4 @@
<ul data-test-node-healthchecks> <ul data-test-healthchecks>
{{#each (sort-by (action 'sortChecksByImportance') items) as |check| }} {{#each (sort-by (action 'sortChecksByImportance') items) as |check| }}
{{healthcheck-output data-test-node-healthcheck=check.Name tagName='li' name=check.Name class=check.Status status=check.Status notes=check.Notes output=check.Output}} {{healthcheck-output data-test-node-healthcheck=check.Name tagName='li' name=check.Name class=check.Status status=check.Status notes=check.Notes output=check.Output}}
{{/each}} {{/each}}

View File

@ -4,19 +4,23 @@
items=item.Proxy.Upstreams as |item index| items=item.Proxy.Upstreams as |item index|
}} }}
{{#block-slot 'header'}} {{#block-slot 'header'}}
<th>Destination Name</th> <th>Upstream</th>
<th>Destination Type</th> <th>Datacenter</th>
<th>Local Bind Port</th> <th>Type</th>
<th>Local Bind Address</th>
{{/block-slot}} {{/block-slot}}
{{#block-slot 'row'}} {{#block-slot 'row'}}
<td data-test-destination-name> <td>
<a>{{item.DestinationName}}</a> <a data-test-destination-name>{{item.DestinationName}}</a>
</td>
<td data-test-destination-datacenter>
{{item.Datacenter}}
</td> </td>
<td data-test-destination-type> <td data-test-destination-type>
{{item.DestinationType}} {{item.DestinationType}}
</td> </td>
<td data-test-local-bind-port> <td data-test-local-bind-address>
{{item.LocalBindPort}} {{item.LocalBindAddress}}:{{item.LocalBindPort}}
</td> </td>
{{/block-slot}} {{/block-slot}}
{{/tabular-collection}} {{/tabular-collection}}

View File

@ -27,19 +27,26 @@
</dl> </dl>
{{#if proxy}} {{#if proxy}}
<dl> <dl>
<dt>Sidecar Proxy</dt> <dt data-test-proxy-type="{{if proxy.ServiceProxy.DestinationServiceID "sidecar-proxy" "proxy"}}">{{if proxy.ServiceProxy.DestinationServiceID "Sidecar " ""}}Proxy</dt>
<dd><a href="{{href-to 'dc.services.instance' proxy.ServiceName proxy.ServiceID}}">{{proxy.ServiceID}}</a></dd> <dd><a href="{{href-to 'dc.services.instance' proxy.ServiceName proxy.ServiceID}}">{{proxy.ServiceID}}</a></dd>
</dl> </dl>
{{/if}} {{/if}}
{{#if (eq item.Kind 'connect-proxy')}} {{#if (eq item.Kind 'connect-proxy')}}
{{#if item.Proxy.DestinationServiceID}}
<dl> <dl>
<dt>Dest. Service Instance</dt> <dt data-test-proxy-destination="instance">Dest. Service Instance</dt>
<dd><a href="{{href-to 'dc.services.instance' item.Proxy.DestinationServiceName item.Proxy.DestinationServiceID}}">{{item.Proxy.DestinationServiceID}}</a></dd> <dd><a href="{{href-to 'dc.services.instance' item.Proxy.DestinationServiceName item.Proxy.DestinationServiceID}}">{{item.Proxy.DestinationServiceID}}</a></dd>
</dl> </dl>
<dl> <dl>
<dt>Local Service Address</dt> <dt>Local Service Address</dt>
<dd>{{item.Proxy.LocalServiceAddress}}:{{item.Proxy.LocalServicePort}}</dd> <dd>{{item.Proxy.LocalServiceAddress}}:{{item.Proxy.LocalServicePort}}</dd>
</dl> </dl>
{{else}}
<dl>
<dt data-test-proxy-destination="service">Dest. Service</dt>
<dd><a href="{{href-to 'dc.services.show' item.Proxy.DestinationServiceName}}">{{item.Proxy.DestinationServiceName}}</a></dd>
</dl>
{{/if}}
{{/if}} {{/if}}
{{/block-slot}} {{/block-slot}}
{{#block-slot 'content'}} {{#block-slot 'content'}}

View File

@ -0,0 +1,15 @@
@setupApplicationTest
Feature: dc / services / instances / error: Visit Service Instance what doesn't exist
Scenario: No instance can be found in the API response
Given 1 datacenter model with the value "dc1"
And 1 service model
When I visit the instance page for yaml
---
dc: dc1
service: service-0
id: id-that-doesnt-exist
---
Then the url should be /dc1/services/service-0/id-that-doesnt-exist
And I see the text "404 (Unable to find instance)" in "[data-test-error]"

View File

@ -0,0 +1,48 @@
@setupApplicationTest
Feature: dc / services / instances / proxy: Show Proxy Service Instance
Scenario: A Proxy Service instance
Given 1 datacenter model with the value "dc1"
And 1 service model from yaml
---
- Service:
Kind: connect-proxy
Name: service-0-proxy
ID: service-0-proxy-with-id
Proxy:
DestinationServiceName: service-0
Upstreams:
- DestinationType: service
DestinationName: service-1
LocalBindAddress: 127.0.0.1
LocalBindPort: 1111
- DestinationType: prepared_query
DestinationName: service-group
LocalBindAddress: 127.0.0.1
LocalBindPort: 1112
---
When I visit the instance page for yaml
---
dc: dc1
service: service-0-proxy
id: service-0-proxy-with-id
---
Then the url should be /dc1/services/service-0-proxy/service-0-proxy-with-id
And I see destination on the proxy like "service"
And I see serviceChecksIsSelected on the tabs
When I click upstreams on the tabs
And I see upstreamsIsSelected on the tabs
And I see 2 of the upstreams object
And I see name on the upstreams like yaml
---
- service-1
- service-group
---
And I see type on the upstreams like yaml
---
- service
- prepared_query
---

View File

@ -0,0 +1,68 @@
@setupApplicationTest
Feature: dc / services / instances / show: Show Service Instance
Scenario: A Service instance has no Proxy
Given 1 datacenter model with the value "dc1"
And 1 service model from yaml
---
- Service:
ID: service-0-with-id
Tags: ['Tag1', 'Tag2']
Meta:
external-source: nomad
Checks:
- Name: Service check
ServiceID: service-0
Output: Output of check
Status: passing
- Name: Service check
ServiceID: service-0
Output: Output of check
Status: warning
- Name: Service check
ServiceID: service-0
Output: Output of check
Status: critical
- Name: Node check
ServiceID: ""
Output: Output of check
Status: passing
- Name: Node check
ServiceID: ""
Output: Output of check
Status: warning
- Name: Node check
ServiceID: ""
Output: Output of check
Status: critical
---
And 1 proxy model from yaml
---
- ServiceProxy:
DestinationServiceName: service-1
DestinationServiceID: ~
---
When I visit the instance page for yaml
---
dc: dc1
service: service-0
id: service-0-with-id
---
Then the url should be /dc1/services/service-0/service-0-with-id
Then I don't see type on the proxy
Then I see externalSource like "nomad"
And I don't see upstreams on the tabs
And I see serviceChecksIsSelected on the tabs
And I see 3 of the serviceChecks object
When I click nodeChecks on the tabs
And I see nodeChecksIsSelected on the tabs
And I see 3 of the nodeChecks object
When I click tags on the tabs
And I see tagsIsSelected on the tabs
Then I see the text "Tag1" in "[data-test-tags] span:nth-child(1)"
Then I see the text "Tag2" in "[data-test-tags] span:nth-child(2)"

View File

@ -0,0 +1,29 @@
@setupApplicationTest
Feature: dc / services / instances / sidecar-proxy: Show Sidecar Proxy Service Instance
Scenario: A Sidecar Proxy Service instance
Given 1 datacenter model with the value "dc1"
And 1 service model from yaml
---
- Service:
Kind: connect-proxy
Name: service-0-sidecar-proxy
ID: service-0-sidecar-proxy-with-id
Proxy:
DestinationServiceName: service-0
DestinationServiceID: service-0-with-id
---
When I visit the instance page for yaml
---
dc: dc1
service: service-0-sidecar-proxy
id: service-0-sidecar-proxy-with-id
---
Then the url should be /dc1/services/service-0-sidecar-proxy/service-0-sidecar-proxy-with-id
And I see destination on the proxy like "instance"
And I see serviceChecksIsSelected on the tabs
When I click upstreams on the tabs
And I see upstreamsIsSelected on the tabs

View File

@ -0,0 +1,20 @@
@setupApplicationTest
Feature: dc / services / instances / with-proxy: Show Service Instance with a proxy
Scenario: A Service instance has a Proxy (no DestinationServiceID)
Given 1 datacenter model with the value "dc1"
And 1 proxy model from yaml
---
- ServiceProxy:
DestinationServiceID: ~
---
When I visit the instance page for yaml
---
dc: dc1
service: service-0
id: service-0-with-id
---
Then the url should be /dc1/services/service-0/service-0-with-id
And I see type on the proxy like "proxy"
And I see serviceChecksIsSelected on the tabs
And I don't see upstreams on the tabs

View File

@ -0,0 +1,22 @@
@setupApplicationTest
Feature: dc / services / instances / with-sidecar: Show Service Instance with a Sidecar Proxy
Scenario: A Service instance has a Sidecar Proxy (a DestinationServiceID)
Given 1 datacenter model with the value "dc1"
And 1 proxy model from yaml
---
- ServiceProxy:
DestinationServiceID: service-1
---
When I visit the instance page for yaml
---
dc: dc1
service: service-0
id: service-0-with-id
---
Then the url should be /dc1/services/service-0/service-0-with-id
And I see type on the proxy like "sidecar-proxy"
And I see serviceChecksIsSelected on the tabs
And I don't see upstreams on the tabs

View File

@ -0,0 +1,10 @@
import steps from '../../../steps';
// step definitions that are shared between features should be moved to the
// tests/acceptance/steps/steps.js file
export default function(assert) {
return steps(assert).then('I should find a file', function() {
assert.ok(true, this.step);
});
}

View File

@ -0,0 +1,10 @@
import steps from '../../../steps';
// step definitions that are shared between features should be moved to the
// tests/acceptance/steps/steps.js file
export default function(assert) {
return steps(assert).then('I should find a file', function() {
assert.ok(true, this.step);
});
}

View File

@ -0,0 +1,10 @@
import steps from '../../../steps';
// step definitions that are shared between features should be moved to the
// tests/acceptance/steps/steps.js file
export default function(assert) {
return steps(assert).then('I should find a file', function() {
assert.ok(true, this.step);
});
}

View File

@ -0,0 +1,10 @@
import steps from '../../../steps';
// step definitions that are shared between features should be moved to the
// tests/acceptance/steps/steps.js file
export default function(assert) {
return steps(assert).then('I should find a file', function() {
assert.ok(true, this.step);
});
}

View File

@ -0,0 +1,10 @@
import steps from '../../../steps';
// step definitions that are shared between features should be moved to the
// tests/acceptance/steps/steps.js file
export default function(assert) {
return steps(assert).then('I should find a file', function() {
assert.ok(true, this.step);
});
}

View File

@ -0,0 +1,10 @@
import steps from '../../../steps';
// step definitions that are shared between features should be moved to the
// tests/acceptance/steps/steps.js file
export default function(assert) {
return steps(assert).then('I should find a file', function() {
assert.ok(true, this.step);
});
}

View File

@ -7,6 +7,9 @@ export default function(type) {
case 'service': case 'service':
requests = ['/v1/internal/ui/services', '/v1/health/service/']; requests = ['/v1/internal/ui/services', '/v1/health/service/'];
break; break;
case 'proxy':
requests = ['/v1/catalog/connect'];
break;
case 'node': case 'node':
requests = ['/v1/internal/ui/nodes', '/v1/internal/ui/node/']; requests = ['/v1/internal/ui/nodes', '/v1/internal/ui/node/'];
break; break;

View File

@ -19,6 +19,7 @@ import dcs from 'consul-ui/tests/pages/dc';
import settings from 'consul-ui/tests/pages/settings'; import settings from 'consul-ui/tests/pages/settings';
import services from 'consul-ui/tests/pages/dc/services/index'; import services from 'consul-ui/tests/pages/dc/services/index';
import service from 'consul-ui/tests/pages/dc/services/show'; import service from 'consul-ui/tests/pages/dc/services/show';
import instance from 'consul-ui/tests/pages/dc/services/instance';
import nodes from 'consul-ui/tests/pages/dc/nodes/index'; import nodes from 'consul-ui/tests/pages/dc/nodes/index';
import node from 'consul-ui/tests/pages/dc/nodes/show'; import node from 'consul-ui/tests/pages/dc/nodes/show';
import kvs from 'consul-ui/tests/pages/dc/kv/index'; import kvs from 'consul-ui/tests/pages/dc/kv/index';
@ -41,6 +42,7 @@ export default {
dcs: create(dcs(visitable, clickable, attribute, collection)), dcs: create(dcs(visitable, clickable, attribute, collection)),
services: create(services(visitable, clickable, attribute, collection, page, catalogFilter)), services: create(services(visitable, clickable, attribute, collection, page, catalogFilter)),
service: create(service(visitable, attribute, collection, text, catalogFilter)), service: create(service(visitable, attribute, collection, text, catalogFilter)),
instance: create(instance(visitable, attribute, collection, text, radiogroup)),
nodes: create(nodes(visitable, clickable, attribute, collection, catalogFilter)), nodes: create(nodes(visitable, clickable, attribute, collection, catalogFilter)),
node: create(node(visitable, deletable, clickable, attribute, collection, radiogroup)), node: create(node(visitable, deletable, clickable, attribute, collection, radiogroup)),
kvs: create(kvs(visitable, deletable, creatable, clickable, attribute, collection)), kvs: create(kvs(visitable, deletable, creatable, clickable, attribute, collection)),

View File

@ -0,0 +1,19 @@
export default function(visitable, attribute, collection, text, radiogroup) {
return {
visit: visitable('/:dc/services/:service/:id'),
externalSource: attribute('data-test-external-source', 'h1 span'),
tabs: radiogroup('tab', ['service-checks', 'node-checks', 'upstreams', 'tags']),
serviceChecks: collection('#service-checks [data-test-healthchecks] li', {}),
nodeChecks: collection('#node-checks [data-test-healthchecks] li', {}),
upstreams: collection('#upstreams [data-test-tabular-row]', {
name: text('[data-test-destination-name]'),
datacenter: text('[data-test-destination-datacenter]'),
type: text('[data-test-destination-type]'),
address: text('[data-test-local-bind-address]'),
}),
proxy: {
type: attribute('data-test-proxy-type', '[data-test-proxy-type]'),
destination: attribute('data-test-proxy-destination', '[data-test-proxy-destination]'),
},
};
}

View File

@ -6,6 +6,12 @@ export default function(scenario, assert, find, currentURL) {
`Expected to see "${text}" in "${selector}"` `Expected to see "${text}" in "${selector}"`
); );
}) })
.then(['I see the exact text "$text" in "$selector"'], function(text, selector) {
assert.ok(
find(selector).textContent.trim() === text,
`Expected to see the exact "${text}" in "${selector}"`
);
})
// TODO: Think of better language // TODO: Think of better language
// TODO: These should be mergeable // TODO: These should be mergeable
.then(['"$selector" has the "$class" class'], function(selector, cls) { .then(['"$selector" has the "$class" class'], function(selector, cls) {

View File

@ -20,10 +20,7 @@ export default function(scenario, assert, currentPage, pluralize) {
}, 100); }, 100);
}); });
}) })
.then(['I see $num $model', 'I see $num $model model', 'I see $num $model models'], function( .then(['I see $num $model model[s]?'], function(num, model) {
num,
model
) {
const len = currentPage()[pluralize(model)].filter(function(item) { const len = currentPage()[pluralize(model)].filter(function(item) {
return item.isVisible; return item.isVisible;
}).length; }).length;

View File

@ -56,6 +56,13 @@ export default function(scenario, assert, currentPage) {
} }
assert.ok(_component[property], `Expected to see ${property} on ${component}`); assert.ok(_component[property], `Expected to see ${property} on ${component}`);
}) })
.then(['I see $num of the $component object'], function(num, component) {
assert.equal(
currentPage()[component].length,
num,
`Expected to see ${num} items in the ${component} object`
);
})
.then(["I don't see $property on the $component"], function(property, component) { .then(["I don't see $property on the $component"], function(property, component) {
// Collection // Collection
var obj; var obj;
@ -64,9 +71,9 @@ export default function(scenario, assert, currentPage) {
} else { } else {
obj = currentPage()[component]; obj = currentPage()[component];
} }
const func = obj[property].bind(obj);
assert.throws( assert.throws(
function() { function() {
const func = obj[property].bind(obj);
func(); func();
}, },
function(e) { function(e) {
@ -89,11 +96,20 @@ export default function(scenario, assert, currentPage) {
.then(['I see $property'], function(property) { .then(['I see $property'], function(property) {
assert.ok(currentPage()[property], `Expected to see ${property}`); assert.ok(currentPage()[property], `Expected to see ${property}`);
}) })
.then(['I see $property like "$value"'], function(property, value) { .then(['I see $property on the $component like "$value"'], function(
property,
component,
value
) {
const target = currentPage()[component][property];
assert.equal( assert.equal(
currentPage()[property], target,
value, value,
`Expected to see ${property}, was ${currentPage()[property]}` `Expected to see ${property} on ${component} as ${value}, was ${target}`
); );
})
.then(['I see $property like "$value"'], function(property, value) {
const target = currentPage()[property];
assert.equal(target, value, `Expected to see ${property} as ${value}, was ${target}`);
}); });
} }

View File

@ -690,9 +690,9 @@
js-yaml "^3.10.0" js-yaml "^3.10.0"
"@hashicorp/consul-api-double@^2.0.1": "@hashicorp/consul-api-double@^2.0.1":
version "2.1.0" version "2.2.0"
resolved "https://registry.yarnpkg.com/@hashicorp/consul-api-double/-/consul-api-double-2.1.0.tgz#511e6a48842ad31133e2070f3b2307568539b10e" resolved "https://registry.yarnpkg.com/@hashicorp/consul-api-double/-/consul-api-double-2.2.0.tgz#b72b086dd4c88485f83b6f2fe30c5cb45b8d0e6a"
integrity sha512-cyW7TiKQylrWzVUORT1e6m4SU8tQ1V5BYEKW2th7QwHP8OFazn/+om9hud/9X5YtjEuSPIQCmFIvhEVwZgLVpQ== integrity sha512-9U+pqdBJn/ZruUYg7J2A3k9FS44FZQsMGBfKQSxaBRU50dgkpGLtOyOOFUqgi/RCAwn4GUOzgXgHRRs5TAeStg==
"@hashicorp/ember-cli-api-double@^1.3.0": "@hashicorp/ember-cli-api-double@^1.3.0":
version "1.7.0" version "1.7.0"