ui: Expose checks (#6575)

Adds visibility for `Expose.Checks` config setting for proxies.

1. Adds an 'Exposed Path' tab to the proxy detail page to show the user information on exposed paths.
2. If the users has exposed their healthchecks we also add this information to the Service detail page for this proxy (only for http2 and gRPC checks)
This commit is contained in:
John Cowen 2019-11-25 18:45:10 +00:00 committed by John Cowen
parent 5a2fc5bffb
commit e6ecb47765
24 changed files with 373 additions and 97 deletions

View File

@ -42,7 +42,14 @@
}
/* TODO: Try and use the same vertical positioning all tooltips */
/* this is only for pseudo tooltips be want to avoid */
/* specifiying pseudo in this file */
/* specifying pseudo in this file */
%tooltip::after {
bottom: calc(100% - 7px);
}
%tooltip-bottom::before {
bottom: auto;
top: calc(100% + 7px);
}
%tooltip-bottom::after {
bottom: -12px;
}

View File

@ -3,12 +3,21 @@
color: $white;
background-color: $gray-500;
}
/* borders here are used to draw a triangle in CSS */
/* the are not actual borders */
%tooltip-tail {
background-color: transparent !important;
border-left: 9px solid transparent;
border-right: 9px solid transparent;
border-top: 18px solid $gray-500;
}
%tooltip-bottom::after {
border-top: 0;
border-bottom: 18px solid $gray-500;
}
%tooltip-bubble {
font-weight: normal;
border-radius: $decor-radius-200;

View File

@ -4,14 +4,17 @@
main {
@extend %app-view;
}
%app-view > div > header {
@extend %app-view-header;
}
%app-view > div > div {
@extend %app-content;
@extend %app-view-content;
}
%app-view header form {
@extend %filter-bar;
}
@media #{$--lt-spacious-page-header} {
%app-view header .actions {
%app-view-header .actions {
margin-top: 9px;
}
}
@ -30,8 +33,8 @@ main {
%app-view h1 em {
color: $gray-600;
}
%app-view header .actions a,
%app-view header .actions button {
%app-view-header .actions a,
%app-view-header .actions button {
@extend %button-compact;
}
%app-content div > dl {
@ -39,10 +42,10 @@ main {
}
[role='tabpanel'] > p:only-child,
.template-error > div,
%app-content > p:only-child,
%app-view-content > p:only-child,
%app-view > div.disabled > div,
%app-view.empty > div {
@extend %app-content-empty;
@extend %app-view-content-empty;
}
[role='tabpanel'] > *:first-child {
margin-top: 1.25em;

View File

@ -1,67 +1,67 @@
/* layout */
%app-view header > div:last-of-type > div:first-child {
%app-view-header > div:last-of-type > div:first-child {
flex-grow: 1;
}
%app-view {
position: relative;
}
%app-view header .actions {
%app-view-header .actions {
float: right;
display: flex;
align-items: flex-start;
margin-top: 9px;
}
%app-view header dl {
%app-view-header dl {
float: left;
margin-top: 25px;
margin-right: 50px;
margin-bottom: 20px;
}
%app-view header dt {
%app-view-header dt {
font-weight: bold;
}
/* units */
%app-view {
margin-top: 50px;
}
%app-view header + div > *:first-child {
%app-view-header + div > *:first-child {
margin-top: 1.8em;
}
%app-view h2 {
padding-bottom: 0.2em;
margin-bottom: 0.2em;
}
%app-view header .actions > *:not(:last-child) {
%app-view-header .actions > *:not(:last-child) {
margin-right: 12px;
}
// content
%app-content div > dl > dt {
%app-view-content div > dl > dt {
position: absolute;
}
%app-content div > dl {
%app-view-content div > dl {
position: relative;
}
%app-content-empty {
%app-view-content-empty {
margin-top: 0;
padding: 50px;
text-align: center;
}
%app-content form:not(:last-child) {
%app-view-content form:not(:last-child) {
margin-bottom: 2.2em;
}
%app-content div > dl > dt {
%app-view-content div > dl > dt {
width: 140px;
}
%app-content div > dl > dd {
%app-view-content div > dl > dd {
padding-left: 140px;
}
%app-content div > dl > * {
%app-view-content div > dl > * {
min-height: 1em;
margin-bottom: 0.4em;
}
// TODO: Think about an %app-form or similar
%app-content fieldset:not(.freetext-filter) {
%app-view-content fieldset:not(.freetext-filter) {
padding-bottom: 0.3em;
margin-bottom: 2em;
}

View File

@ -2,3 +2,10 @@
.healthcheck-output {
@extend %healthcheck-output;
}
%healthcheck-output em::before {
width: 250px;
/* TODO: All tooltips previously used */
/* nowrap, they shouldn't */
white-space: normal !important;
}
/**/

View File

@ -12,45 +12,54 @@
%healthcheck-output > div {
flex: 1 1 auto;
}
%healthcheck-output header,
%healthcheck-output dl:last-of-type {
width: 100%;
}
%healthcheck-output header {
margin-bottom: 0.9em;
}
%healthcheck-output dl:not(:last-of-type) {
float: left;
width: 25%;
margin-right: 2%;
%healthcheck-output > div {
// 100% minus the width of the icon space (26)
width: calc(100% - 26px);
display: flex;
flex-wrap: wrap;
justify-content: space-between;
}
%healthcheck-output dl:nth-of-type(3) {
width: 46%;
margin-right: 0;
}
%healthcheck-output dl:not(:last-of-type) dd {
word-break: break-all;
}
%healthcheck-output dt {
margin-bottom: 0;
}
%healthcheck-output dl:last-of-type {
clear: both;
margin-bottom: 0;
}
%healthcheck-output dl:last-of-type dt {
margin-bottom: 0.3em;
}
%healthcheck-output dl:last-of-type dd {
position: relative;
%healthcheck-output dl {
min-width: 110px;
}
%healthcheck-output dl > * {
float: none;
display: block;
width: auto;
position: static;
padding-left: 0;
}
%healthcheck-output dt {
margin-bottom: 0;
}
%healthcheck-output dd {
position: relative;
}
%healthcheck-output dl:nth-last-of-type(2) {
width: 50%;
}
%healthcheck-output dl:last-of-type {
margin-bottom: 0;
}
%healthcheck-output dl:last-of-type dt {
margin-bottom: 0.3em;
}
%healthcheck-output pre {
padding: 12px;
padding-right: 40px;
white-space: pre-wrap;
position: relative;
}
%healthcheck-output pre code {
word-wrap: break-word;
}
%healthcheck-output .with-feedback {
position: absolute;
@ -67,9 +76,10 @@
%healthcheck-output::before {
margin-right: 8px;
}
%healthcheck-output dl:nth-last-of-type(2) {
width: 100%;
}
%healthcheck-output dl:not(:last-of-type) {
float: none;
width: auto;
margin-right: 0;
}
}

View File

@ -14,6 +14,14 @@
margin-right: 8px;
}
}
%healthcheck-output dd em {
@extend %pill;
/*TODO: Should this be merged into %pill? */
cursor: default;
font-style: normal;
margin-top: -2px;
margin-left: 0.5em;
}
%healthcheck-output.passing::before {
@extend %with-check-circle-fill-color-icon;
}

View File

@ -45,6 +45,7 @@ th span em {
font-style: normal;
white-space: normal !important;
}
/**/
/* ideally these would be in route css files, but left here as they */
/* accomplish the same thing (hide non-essential columns for tables) */

View File

@ -95,6 +95,9 @@ html.template-token.template-list main table tr th {
html.template-node.template-show main table.sessions tr {
@extend %node-sessions-row;
}
html.template-instance.template-show main table.exposedpaths tr {
@extend %instance-paths-row;
}
// this will get auto calculated later in tabular-collection.js
// keeping this here for reference
// %services-row > * {
@ -110,6 +113,22 @@ html.template-node.template-show main table.sessions tr {
%instances-row > * {
width: calc(100% / 5);
}
// instance-paths are for exposed paths
// we make the columns that need as much space as possible
// as wide as possible so 50% each minus enough room
// for the 3 port columns - we probably need a max of 55px
// for each port column so 55 * 3 = 165
// so column 1 and 5 are 50% - 165 each
// the 3 remaining columns split the 165 thats left between them
%instance-paths-row > *:nth-child(1),
%instance-paths-row > *:nth-child(5) {
width: calc(50% - 165px) !important;
}
%instance-paths-row > *:nth-child(2),
%instance-paths-row > *:nth-child(3),
%instance-paths-row > *:nth-child(4) {
width: 110px !important;
}
%tokens-row > *:first-child,
%tokens-minimal-row > *:not(last-child),
%tokens-row > *:nth-child(2),

View File

@ -1,5 +1,9 @@
/* TODO: need to standardize on the selectors used here */
/* I would guess at the time of writing this we shojuld prefer */
/* classes */
html.template-instance.template-show #addresses table tr,
html.template-instance.template-show #upstreams table tr,
html.template-instance.template-show #meta-data table tr {
html.template-instance.template-show #meta-data table tr,
html.template-instance.template-show table.exposedpaths tr {
cursor: default;
}

View File

@ -1,10 +1,10 @@
<ul data-test-healthchecks>
{{#each (sort-by (action 'sortChecksByImportance') items) as |item| }}
{{! TODO: this component and its child should be moved to a single component }}
{{#healthcheck-output
data-test-node-healthcheck=item.Name
class=item.Status
tagName='li'
output=item.Output
}}
{{#block-slot 'header'}}
<h3>{{item.Name}}</h3>
@ -18,10 +18,42 @@
<dt>CheckID</dt>
<dd>{{or item.CheckID '-'}}</dd>
</dl>
<dl>
<dt>Type</dt>
<dd>
{{item.Type}}
{{#if (and exposed (contains item.Type (array 'http' 'grpc')))}}
<em data-test-exposed="true" data-tooltip="Expose.checks is set to true, so all registered HTTP and gRPC check paths are exposed through Envoy for the Consul agent.">Exposed</em>
{{/if}}
</dd>
</dl>
<dl>
<dt>Notes</dt>
<dd>{{or item.Notes '-'}}</dd>
</dl>
<dl>
{{#if (not-eq item.Type 'ttl')}}
<dt>Output</dt>
<dd>
<pre><code>{{item.Output}}</code></pre>
{{#feedback-dialog type='inline'}}
{{#block-slot 'action' as |success error|}}
{{copy-button success=(action success) error=(action error) clipboardText=item.Output title='copy output to clipboard'}}
{{/block-slot}}
{{#block-slot 'success' as |transition|}}
<p class={{transition}}>
Copied output!
</p>
{{/block-slot}}
{{#block-slot 'error' as |transition|}}
<p class={{transition}}>
Sorry, something went wrong!
</p>
{{/block-slot}}
{{/feedback-dialog}}
</dd>
{{/if}}
</dl>
{{/block-slot}}
{{/healthcheck-output}}
{{/each}}

View File

@ -1,28 +1,8 @@
{{! TODO: this component and its parent should be moved to a single component }}
{{yield}}
<div>
<header>
{{#yield-slot 'header'}}{{yield}}{{/yield-slot}}
</header>
{{#yield-slot 'content'}}{{yield}}{{/yield-slot}}
<dl>
<dt>Output</dt>
<dd>
<pre><code>{{output}}</code></pre>
{{#feedback-dialog type='inline'}}
{{#block-slot 'action' as |success error|}}
{{copy-button success=(action success) error=(action error) clipboardText=output title='copy output to clipboard'}}
{{/block-slot}}
{{#block-slot 'success' as |transition|}}
<p class={{transition}}>
Copied output!
</p>
{{/block-slot}}
{{#block-slot 'error' as |transition|}}
<p class={{transition}}>
Sorry, something went wrong!
</p>
{{/block-slot}}
{{/feedback-dialog}}
</dd>
</dl>
</div>

View File

@ -0,0 +1,33 @@
<p>
You can expose individual HTTP paths like /metrics through Envoy for external services like Prometheus.
</p>
{{#tabular-collection
data-test-exposedpaths
class="exposedpaths"
items=item.Proxy.Expose.Paths as |path index|
}}
{{#block-slot 'header'}}
<th>Path</th>
<th>Protocol</th>
<th>Listener port</th>
<th>Local path port</th>
<th>Combined address<span><em role="tooltip">Service address, listener port, and path all combined into one URL.</em></span></th>
{{/block-slot}}
{{#block-slot 'row'}}
<td>
<span>{{path.Path}}</span>
</td>
<td>
{{path.Protocol}}
</td>
<td>
{{path.ListenerPort}}
</td>
<td>
{{path.LocalPathPort}}
</td>
<td>
<span data-test-combined-address>{{item.Address}}:{{path.ListenerPort}}{{path.Path}}</span>
</td>
{{/block-slot}}
{{/tabular-collection}}

View File

@ -1,5 +1,5 @@
{{#if (gt item.ServiceChecks.length 0) }}
{{healthcheck-list items=item.ServiceChecks}}
{{healthcheck-list items=item.ServiceChecks exposed=proxy.ServiceProxy.Expose.Checks}}
{{else}}
<p>
This instance has no service health checks.

View File

@ -11,7 +11,7 @@
{{/block-slot}}
{{#block-slot 'row'}}
<td>
<a data-test-destination-name>{{item.DestinationName}}</a>
<span data-test-destination-name>{{item.DestinationName}}</span>
</td>
<td data-test-destination-datacenter>
{{item.Datacenter}}

View File

@ -72,6 +72,10 @@
(eq item.Kind 'connect-proxy')
'Upstreams' ''
)
(if
(and (eq item.Kind 'connect-proxy') (gt item.Proxy.Expose.Paths.length 0))
'Exposed Paths' ''
)
(if
(eq item.Kind 'mesh-gateway')
'Addresses' ''
@ -91,6 +95,10 @@
(eq item.Kind 'connect-proxy')
(hash id=(slugify 'Upstreams') partial='dc/services/upstreams') ''
)
(if
(and (eq item.Kind 'connect-proxy') (gt item.Proxy.Expose.Paths.length 0))
(hash id=(slugify 'Exposed Paths') partial='dc/services/exposedpaths') ''
)
(if
(eq item.Kind 'mesh-gateway')
(hash id=(slugify 'Addresses') partial='dc/services/addresses') ''

View File

@ -48,7 +48,7 @@
"@babel/plugin-proposal-object-rest-spread": "^7.5.5",
"@ember/jquery": "^0.6.0",
"@ember/optional-features": "^0.7.0",
"@hashicorp/consul-api-double": "^2.0.1",
"@hashicorp/consul-api-double": "^2.6.2",
"@hashicorp/ember-cli-api-double": "^2.0.0",
"base64-js": "^1.3.0",
"broccoli-asset-rev": "^3.0.0",

View File

@ -3,6 +3,18 @@ Feature: components / copy-button
Background:
Given 1 datacenter model with the value "dc-1"
Scenario: Clicking the copy button
Given 1 node model from yaml
---
ID: node-0
Checks:
- Name: gprc-check
Node: node-0
CheckID: grpc-check
Status: passing
Type: grpc
Output: The output
Notes: The notes
---
When I visit the node page for yaml
---
dc: dc-1

View File

@ -1,6 +1,6 @@
@setupApplicationTest
Feature: dc / services / instances / proxy: Show Proxy Service Instance
Scenario: A Proxy Service instance
Scenario: A Proxy Service instance with no exposed checks
Given 1 datacenter model with the value "dc1"
And 1 instance model from yaml
---
@ -10,6 +10,9 @@ Feature: dc / services / instances / proxy: Show Proxy Service Instance
ID: service-0-proxy-with-id
Proxy:
DestinationServiceName: service-0
Expose:
Checks: false
Paths: []
Upstreams:
- DestinationType: service
DestinationName: service-1
@ -32,6 +35,9 @@ Feature: dc / services / instances / proxy: Show Proxy Service Instance
And I see serviceChecksIsSelected on the tabs
When I click serviceChecks on the tabs
And I don't see exposed on the serviceChecks
When I click upstreams on the tabs
And I see upstreamsIsSelected on the tabs
And I see 2 of the upstreams object
@ -45,5 +51,83 @@ Feature: dc / services / instances / proxy: Show Proxy Service Instance
- service
- prepared_query
---
And I don't see exposedPaths on the tabs
Scenario: A Proxy Service instance with no automatically exposed checks but with paths
Given 1 datacenter model with the value "dc1"
And 1 instance model from yaml
---
- Service:
Kind: connect-proxy
Name: service-0-proxy
ID: service-0-proxy-with-id
Address: 10.0.0.1
Proxy:
DestinationServiceName: service-0
Expose:
Paths:
- Path: /grpc-metrics
Protocol: grpc
LocalPathPort: 8081
ListenerPort: 8080
- Path: /http-metrics
Protocol: http
LocalPathPort: 8082
ListenerPort: 8083
---
When I visit the instance page for yaml
---
dc: dc1
service: service-0-proxy
node: node-0
id: service-0-proxy-with-id
---
Then the url should be /dc1/services/service-0-proxy/node-0/service-0-proxy-with-id
And I see serviceChecksIsSelected on the tabs
When I click serviceChecks on the tabs
And I don't see exposed on the serviceChecks
When I click exposedPaths on the tabs
And I see exposedPaths on the tabs
And I see 2 of the exposedPaths object
And I see combinedAddress on the exposedPaths like yaml
---
- 10.0.0.1:8080/grpc-metrics
- 10.0.0.1:8083/http-metrics
---
Scenario: A Proxy Service instance with only automatically exposed checks but no paths
Given 1 datacenter model with the value "dc1"
And 1 instance model from yaml
---
- Service:
Kind: connect-proxy
Name: service-0-proxy
ID: service-0-proxy-with-id
Address: 10.0.0.1
Proxy:
DestinationServiceName: service-0
Expose:
Checks: true
Paths: []
Checks:
- Name: http-check
Type: http
---
When I visit the instance page for yaml
---
dc: dc1
service: service-0-proxy
node: node-0
id: service-0-proxy-with-id
---
Then the url should be /dc1/services/service-0-proxy/node-0/service-0-proxy-with-id
And I see serviceChecksIsSelected on the tabs
And I don't see exposedPaths on the tabs
When I click serviceChecks on the tabs
And I don't see exposed on the serviceChecks
When I click nodeChecks on the tabs
And I don't see exposed on the nodeChecks

View File

@ -29,6 +29,7 @@ Feature: dc / services / instances / show: Show Service Instance
Output: Output of check
Status: warning
- Name: Service check
Type: http
ServiceID: service-0
Output: Output of check
Status: critical
@ -45,13 +46,13 @@ Feature: dc / services / instances / show: Show Service Instance
Output: Output of check
Status: critical
---
And 1 proxy model from yaml
Scenario: A Service instance has no Proxy
Given 1 proxy model from yaml
---
- ServiceProxy:
DestinationServiceName: service-1
DestinationServiceID: ~
---
Scenario: A Service instance has no Proxy
When I visit the instance page for yaml
---
dc: dc1
@ -99,6 +100,63 @@ Feature: dc / services / instances / show: Show Service Instance
Then the url should be /dc1/services/service-0/node-0/service-0-with-id
And an external edit results in 0 instance models
And pause until I see the text "deregistered" in "[data-notification]"
Scenario: A Service instance with a Proxy with only automatically exposed checks but no paths
Given 1 proxy model from yaml
---
- ServiceProxy:
DestinationServiceName: service-0
DestinationServiceID: ~
Expose:
Checks: true
Paths: []
---
When I visit the instance page for yaml
---
dc: dc1
service: service-0
node: another-node
id: service-0-with-id
---
Then the url should be /dc1/services/service-0/another-node/service-0-with-id
And I see serviceChecksIsSelected on the tabs
And I don't see exposedPaths on the tabs
When I click serviceChecks on the tabs
And I see exposed on the serviceChecks
When I click nodeChecks on the tabs
And I don't see exposed on the nodeChecks
Scenario: A Service Instance with a Proxy with no automatically exposed checks
Given 1 proxy model from yaml
---
- ServiceProxy:
DestinationServiceName: service-0
DestinationServiceID: ~
Expose:
Checks: false
Paths: []
---
When I visit the instance page for yaml
---
dc: dc1
service: service-0
node: another-node
id: service-0-with-id
---
Then the url should be /dc1/services/service-0/another-node/service-0-with-id
And I see serviceChecksIsSelected on the tabs
And I don't see exposedPaths on the tabs
When I click serviceChecks on the tabs
And I don't see exposed on the serviceChecks
When I click nodeChecks on the tabs
And I don't see exposed on the nodeChecks
@ignore
Scenario: A Service Instance's proxy blocking query is closed when the instance is deregistered
Then ok

View File

@ -12,23 +12,12 @@ module('Integration | Component | healthcheck output', function(hooks) {
await render(hbs`{{healthcheck-output}}`);
assert.notEqual(
find('*')
.textContent.trim()
.indexOf('Output'),
-1
);
assert.equal(find('*').textContent.trim(), '');
// Template block usage:
await render(hbs`
{{#healthcheck-output}}{{/healthcheck-output}}
`);
assert.notEqual(
find('*')
.textContent.trim()
.indexOf('Output'),
-1
);
assert.equal(find('*').textContent.trim(), '');
});
});

View File

@ -73,7 +73,7 @@ test('findBySlug returns the correct data for item endpoint', function(assert) {
const nodes = payload.Nodes;
const service = payload.Nodes[0];
service.Nodes = nodes;
service.Tags = payload.Nodes[0].Service.Tags;
service.Tags = [...new Set(payload.Nodes[0].Service.Tags)];
service.meta = {
cursor: undefined,
};

View File

@ -7,17 +7,25 @@ export default function(visitable, attribute, collection, text, radiogroup) {
'node-checks',
'addresses',
'upstreams',
'exposed-paths',
'tags',
'meta-data',
]),
serviceChecks: collection('#service-checks [data-test-healthchecks] li', {}),
nodeChecks: collection('#node-checks [data-test-healthchecks] li', {}),
serviceChecks: collection('#service-checks [data-test-healthchecks] li', {
exposed: attribute('data-test-exposed', '[data-test-exposed]'),
}),
nodeChecks: collection('#node-checks [data-test-healthchecks] li', {
exposed: attribute('data-test-exposed', '[data-test-exposed]'),
}),
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]'),
}),
exposedPaths: collection('#exposed-paths [data-test-tabular-row]', {
combinedAddress: text('[data-test-combined-address]'),
}),
addresses: collection('#addresses [data-test-tabular-row]', {
address: text('[data-test-address]'),
}),

View File

@ -991,10 +991,10 @@
faker "^4.1.0"
js-yaml "^3.13.1"
"@hashicorp/consul-api-double@^2.0.1":
version "2.5.0"
resolved "https://registry.yarnpkg.com/@hashicorp/consul-api-double/-/consul-api-double-2.5.0.tgz#d9540a38ee652d55ed90956850c9e5cbcde89454"
integrity sha512-RZcVIPQ4M4TZzFe2mWm7M5w28yOIpVgiYZI5ax+JG0Yr5TVbhJPMxhdb1es73cILuqIi9Fr+73OJ5IAospgPBw==
"@hashicorp/consul-api-double@^2.6.2":
version "2.8.0"
resolved "https://registry.yarnpkg.com/@hashicorp/consul-api-double/-/consul-api-double-2.8.0.tgz#d4bd0999f2ce8419112934bb66af9c4c009c503e"
integrity sha512-Q25JJA8KDMnSwKLvCySokumaEIccq/uI0sRpHDuLjBzOA5gZUnBqrDlQUYlTPVYr98DEeiHCJiAacAnOpRpf7Q==
"@hashicorp/ember-cli-api-double@^2.0.0":
version "2.0.0"
@ -2173,6 +2173,10 @@ babel-plugin-transform-es2015-sticky-regex@^6.22.0:
version "6.24.1"
resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-sticky-regex/-/babel-plugin-transform-es2015-sticky-regex-6.24.1.tgz#00c1cdb1aca71112cdf0cf6126c2ed6b457ccdbc"
integrity sha1-AMHNsaynERLN8M9hJsLta0V8zbw=
dependencies:
babel-helper-regex "^6.24.1"
babel-runtime "^6.22.0"
babel-types "^6.24.1"
babel-plugin-transform-es2015-template-literals@^6.22.0:
version "6.22.0"