HCP Link Status Updates (#17213)

* updates hcp link status message parsing and adds handling for connection errors

* adds handling for missing status to link-status component
This commit is contained in:
Jordan Reimer
2022-09-19 14:37:40 -06:00
committed by GitHub
parent ade0b417a4
commit a2d818bf0a
4 changed files with 150 additions and 31 deletions

View File

@@ -7,23 +7,66 @@ import { inject as service } from '@ember/service';
* *
* @example * @example
* ```js * ```js
* <LinkStatus @status={{this.currentCluser.cluster.hcpLinkStatus}} /> * <LinkStatus @status={{this.currentCluster.cluster.hcpLinkStatus}} />
* ``` * ```
* *
* @param {string} status - cluster.hcpLinkStatus value from currentCluster service * @param {string} status - cluster.hcpLinkStatus value from currentCluster service -- returned from seal-status endpoint
*/ */
export default class LinkStatus extends Component { export default class LinkStatus extends Component {
@service store; @service store;
@service version; @service version;
get showBanner() { get state() {
// enterprise only feature at this time but will expand to OSS in future release if (!this.args.status) return null;
// there are plans to handle connection failure states -- only alert if connected until further states are returned // connected state is returned with no further information
return this.version.isEnterprise && this.args.status === 'connected'; if (this.args.status === 'connected') return this.args.status;
// disconnected and connecting states are returned with a timestamp and error
// state is always the first word of the string
return this.args.status.split(' ', 1).toString();
} }
get bannerClass() { get timestamp() {
return this.args.status === 'connected' ? 'connected' : 'warning'; try {
return this.state !== 'connected' ? this.args.status.split('since')[1].split('m=')[0].trim() : null;
} catch {
return null;
}
}
get message() {
if (this.args.status) {
const error = this.args.status.split('error:')[1];
const time = `[${this.timestamp}]`;
if (this.state === 'disconnected') {
// if generally disconnected hide the banner
return !error || error.includes('UNKNOWN')
? null
: `Vault has been disconnected from the Hashicorp Cloud Platform since ${time}. Error: ${error}`;
} else if (this.state === 'connecting') {
if (error.includes('connection refused')) {
return `Vault has been trying to connect to the Hashicorp Cloud Platform since ${time}, but the Scada provider is down. Vault will try again soon.`;
} else if (error.includes('principal does not have permission to register as provider')) {
return `Vault tried connecting to the Hashicorp Cloud Platform, but the Resource ID is invalid. Check your resource ID. ${time}`;
} else if (error.includes('cannot fetch token: 401 Unauthorized')) {
return `Vault tried connecting to the Hashicorp Cloud Platform, but the authorization information is wrong. Update it and try again. ${time}`;
} else {
// catch all for any unknown errors
return `Vault has been trying to connect to the Hashicorp Cloud Platform since ${time}. Vault will try again soon. Error: ${error}`;
}
}
}
return null;
}
get showStatus() {
// enterprise only feature at this time but will expand to OSS in future release
if (!this.version.isEnterprise || !this.args.status) {
return false;
}
if (this.state === 'disconnected' && !this.message) {
return false;
}
return true;
} }
} }

View File

@@ -1,21 +1,14 @@
{{#if this.showBanner}} {{#if this.showStatus}}
<div class="navbar-status {{this.bannerClass}}"> <div class="navbar-status {{if (eq this.state 'connected') 'connected' 'warning'}}">
<Icon @name="info" /> <Icon @name="info" />
<p data-test-link-status> <p data-test-link-status>
{{#if (eq @status "connected")}} {{#if (eq this.state "connected")}}
This self-managed Vault is linked to the This self-managed Vault is linked to the
<a href="https://portal.cloud.hashicorp.com/sign-in" target="_blank" rel="noopener noreferrer"> <a href="https://portal.cloud.hashicorp.com/sign-in" target="_blank" rel="noopener noreferrer">
HashiCorp Cloud Platform. HashiCorp Cloud Platform.
</a> </a>
{{else if (eq @status "401")}} {{else}}
{{! roughing in 401 and 500 connection statuses -- update strings once they are exposed by the API }} {{this.message}}
Vault cant connect to HashiCorp Cloud Portal. Check your config file to ensure that credentials are correct.
{{else if (eq @status "500")}}
Vaults connection to HashiCorp Cloud Portal is down. Check
<a href="https://status.hashicorp.com" target="_blank" rel="noopener noreferrer">
the status page
</a>
for details.
{{/if}} {{/if}}
</p> </p>
</div> </div>

View File

@@ -1,3 +1,17 @@
export const statuses = [
'connected',
'disconnected since 2022-09-13 14:45:40.666697 -0700 PDT m=+21.065498483; error: UNKNOWN',
'disconnected since 2022-09-13 14:45:40.666697 -0700 PDT m=+21.065498483; error: some other error other than unknown',
'connecting since 2022-09-13 14:45:40.666697 -0700 PDT m=+21.065498483; error: dial tcp [::1]:28083: connect: connection refused',
'connecting since 2022-09-13 14:45:40.666697 -0700 PDT m=+21.065498483; error: principal does not have permission to register as provider: rpc error: code = PermissionDenied desc =',
'connecting since 2022-09-13 14:45:40.666697 -0700 PDT m=+21.065498483; error: failed to get access token: oauth2: cannot fetch token: 401 Unauthorized. Response: {"error":"access_denied","error_description":"Unauthorized"}',
'connecting since 2022-09-13 14:45:40.666697 -0700 PDT m=+21.065498483; error: connection error we are unaware of',
// the following were identified as dev only errors -- leaving in case they need to be handled
// 'connecting since 2022-09-13 14:45:40.666697 -0700 PDT m=+21.065498483; error: failed to get access token: Post "https://aauth.idp.hcp.dev/oauth2/token": x509: “*.hcp.dev” certificate name does not match input',
// 'connecting since 2022-09-13 14:45:40.666697 -0700 PDT m=+21.065498483; error: UNKNOWN',
];
let index = null;
export default function (server) { export default function (server) {
const handleResponse = (req, props) => { const handleResponse = (req, props) => {
const xhr = req.passthrough(); const xhr = req.passthrough();
@@ -16,10 +30,13 @@ export default function (server) {
}; };
server.get('sys/seal-status', (schema, req) => { server.get('sys/seal-status', (schema, req) => {
// randomly return one of the various states to test polling // return next status from statuses array
// 401 and 500 are stubs -- update with actual API values once determined if (index === null || index === statuses.length - 1) {
const hcp_link_status = ['connected', 'disconnected', '401', '500'][Math.floor(Math.random() * 2)]; index = 0;
return handleResponse(req, { hcp_link_status }); } else {
index++;
}
return handleResponse(req, { hcp_link_status: statuses[index] });
}); });
// enterprise only feature initially // enterprise only feature initially
server.get('sys/health', (schema, req) => handleResponse(req, { version: '1.12.0-dev1+ent' })); server.get('sys/health', (schema, req) => handleResponse(req, { version: '1.12.0-dev1+ent' }));

View File

@@ -3,6 +3,9 @@ import { setupRenderingTest } from 'ember-qunit';
import { render } from '@ember/test-helpers'; import { render } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars'; import { hbs } from 'ember-cli-htmlbars';
import { setupMirage } from 'ember-cli-mirage/test-support'; import { setupMirage } from 'ember-cli-mirage/test-support';
import { statuses } from '../../../mirage/handlers/hcp-link';
const timestamp = '[2022-09-13 14:45:40.666697 -0700 PDT]';
module('Integration | Component | link-status', function (hooks) { module('Integration | Component | link-status', function (hooks) {
setupRenderingTest(hooks); setupRenderingTest(hooks);
@@ -11,16 +14,11 @@ module('Integration | Component | link-status', function (hooks) {
// this can be removed once feature is released for OSS // this can be removed once feature is released for OSS
hooks.beforeEach(function () { hooks.beforeEach(function () {
this.owner.lookup('service:version').set('isEnterprise', true); this.owner.lookup('service:version').set('isEnterprise', true);
}); this.statuses = statuses;
test('it does not render disconnected status', async function (assert) {
await render(hbs`<LinkStatus @status="disconnected" />`);
assert.dom('.navbar-status').doesNotExist('Banner is hidden for disconnected state');
}); });
test('it renders connected status', async function (assert) { test('it renders connected status', async function (assert) {
await render(hbs`<LinkStatus @status="connected" />`); await render(hbs`<LinkStatus @status={{get this.statuses 0}} />`);
assert.dom('.navbar-status').hasClass('connected', 'Correct class renders for connected state'); assert.dom('.navbar-status').hasClass('connected', 'Correct class renders for connected state');
assert assert
@@ -33,4 +31,72 @@ module('Integration | Component | link-status', function (hooks) {
.dom('[data-test-link-status] a') .dom('[data-test-link-status] a')
.hasAttribute('href', 'https://portal.cloud.hashicorp.com/sign-in', 'HCP sign in link renders'); .hasAttribute('href', 'https://portal.cloud.hashicorp.com/sign-in', 'HCP sign in link renders');
}); });
test('it does not render banner for disconnected state with unknown error', async function (assert) {
await render(hbs`<LinkStatus @status={{get this.statuses 1}} />`);
assert.dom('.navbar-status').doesNotExist('Banner is hidden for disconnected state');
});
test('it should render for disconnected error state', async function (assert) {
await render(hbs`<LinkStatus @status={{get this.statuses 2}} />`);
assert.dom('.navbar-status').hasClass('warning', 'Correct class renders for disconnected error state');
assert
.dom('[data-test-link-status]')
.hasText(
`Vault has been disconnected from the Hashicorp Cloud Platform since ${timestamp}. Error: some other error other than unknown`,
'Copy renders for disconnected error state'
);
});
test('it should render for connection refused error state', async function (assert) {
await render(hbs`<LinkStatus @status={{get this.statuses 3}} />`);
assert
.dom('.navbar-status')
.hasClass('warning', 'Correct class renders for connection refused error state');
assert
.dom('[data-test-link-status]')
.hasText(
`Vault has been trying to connect to the Hashicorp Cloud Platform since ${timestamp}, but the Scada provider is down. Vault will try again soon.`,
'Copy renders for connection refused error state'
);
});
test('it should render for resource id error state', async function (assert) {
await render(hbs`<LinkStatus @status={{get this.statuses 4}} />`);
assert.dom('.navbar-status').hasClass('warning', 'Correct class renders for resource id error state');
assert
.dom('[data-test-link-status]')
.hasText(
`Vault tried connecting to the Hashicorp Cloud Platform, but the Resource ID is invalid. Check your resource ID. ${timestamp}`,
'Copy renders for resource id error state'
);
});
test('it should render for unauthorized error state', async function (assert) {
await render(hbs`<LinkStatus @status={{get this.statuses 5}} />`);
assert.dom('.navbar-status').hasClass('warning', 'Correct class renders for unauthorized error state');
assert
.dom('[data-test-link-status]')
.hasText(
`Vault tried connecting to the Hashicorp Cloud Platform, but the authorization information is wrong. Update it and try again. ${timestamp}`,
'Copy renders for unauthorized error state'
);
});
test('it should render generic message for unknown error state', async function (assert) {
await render(hbs`<LinkStatus @status={{get this.statuses 6}} />`);
assert.dom('.navbar-status').hasClass('warning', 'Correct class renders for unknown error state');
assert
.dom('[data-test-link-status]')
.hasText(
`Vault has been trying to connect to the Hashicorp Cloud Platform since ${timestamp}. Vault will try again soon. Error: connection error we are unaware of`,
'Copy renders for unknown error state'
);
});
}); });