HCP Link Status Parsing and Modal Update (#17279)

* updates hcp link status parsing for new format and updates to modal view

* fixes missing wormhole in tests

* fixes transit backend tests

* reverts adding wormhole to LinkStatus for testing and instead adds it to impacted tests
This commit is contained in:
Jordan Reimer 2022-09-23 08:18:20 -06:00 committed by GitHub
parent 0c76168d3d
commit efe5193a59
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 125 additions and 143 deletions

View File

@ -14,7 +14,6 @@ import { inject as service } from '@ember/service';
*/
export default class LinkStatus extends Component {
@service store;
@service version;
get state() {
@ -28,47 +27,14 @@ export default class LinkStatus extends Component {
get timestamp() {
try {
return this.state !== 'connected' ? this.args.status.split('since')[1].split('m=')[0].trim() : null;
return this.state !== 'connected' ? this.args.status.split('since')[1].split(';')[0].trim() : null;
} catch {
return null;
}
}
get message() {
if (this.args.status) {
const error = this.args.status.split('error:')[1] || '';
const timestamp = this.timestamp ? ` [${this.timestamp}]` : '';
const sinceTimestamp = timestamp ? ` since${timestamp}` : '';
if (this.state === 'disconnected') {
// if generally disconnected hide the banner
return !error || error.includes('UNKNOWN')
? null
: `Vault has been disconnected from HCP${sinceTimestamp}. Error: ${error}`;
} else if (this.state === 'connecting') {
if (error.includes('connection refused')) {
return `Vault has been trying to connect to HCP${sinceTimestamp}, but HCP is not reachable. Vault will try again soon.`;
} else if (error.includes('principal does not have permission to register as provider')) {
return `Vault tried connecting to HCP, but the Resource ID is invalid. Check your resource ID.${timestamp}`;
} else if (error.includes('cannot fetch token: 401 Unauthorized')) {
return `Vault tried connecting to HCP, but the authorization information is wrong. Update it and try again.${timestamp}`;
} else {
// catch all for any unknown errors or missing error
const errorMessage = error ? ` Error: ${error}` : '';
return `Vault has been trying to connect to HCP${sinceTimestamp}. Vault will try again soon.${errorMessage}`;
}
}
}
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 !== 'connected' && !this.message) {
return false;
}
return true;
get error() {
const status = this.args.status;
return status && status !== 'connected' ? status.split('error:')[1] : null;
}
}

View File

@ -280,5 +280,4 @@ a.button.disabled {
font-size: inherit;
font-weight: inherit;
cursor: pointer;
color: $link;
}

View File

@ -1,4 +1,4 @@
{{#if this.showStatus}}
{{#if (and this.state this.version.isEnterprise)}}
<div class="navbar-status {{if (eq this.state 'connected') 'connected' 'warning'}}">
<Icon @name="info" />
<p data-test-link-status>
@ -8,8 +8,55 @@
HCP.
</a>
{{else}}
{{this.message}}
There was an error connecting to HCP. Click
<button type="button" class="text-button is-underline" {{on "click" (fn (mut this.showModal))}}>
here
</button>
for more information.
{{/if}}
</p>
</div>
{{/if}}
{{/if}}
<Modal
@title="HCP Link error"
@onClose={{fn (mut this.showModal) false}}
@isActive={{this.showModal}}
@type="info"
@showCloseButton={{true}}
>
<section class="modal-card-body">
<div>
<p class="has-text-weight-bold">Timestamp</p>
<p data-test-link-status-timestamp>
{{or this.timestamp "Not available"}}
</p>
</div>
<div class="has-top-bottom-margin">
<p class="has-text-weight-bold">Error</p>
{{#if this.error}}
<code class="tag has-text-danger" data-test-link-status-error>
{{this.error}}
</code>
{{else}}
<p data-test-link-status-error>
Not available
</p>
{{/if}}
</div>
<div>
<p class="has-text-weight-bold">Additional information</p>
<p>Check the logs for more information</p>
</div>
</section>
<footer class="modal-card-foot modal-card-foot-outlined">
<button
type="button"
class="button is-primary"
{{on "click" (fn (mut this.showModal) false)}}
data-test-link-status-close
>
Close
</button>
</footer>
</Modal>

View File

@ -39,7 +39,7 @@
You can use Alt+Tab (Option+Tab on MacOS) in the code editor to skip to the next field. See
<button
type="button"
class="text-button"
class="text-button has-text-info"
{{on "click" (fn (mut this.showTemplateModal))}}
data-test-oidc-scope-example
>

View File

@ -1,14 +1,9 @@
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',
'disconnected since 2022-09-21T11:25:02.196835-07:00; error: unable to establish a connection with HCP',
'connecting since 2022-09-21T11:25:02.196835-07:00; error: unable to establish a connection with HCP',
'connecting since 2022-09-21T11:25:02.196835-07:00; error: principal does not have the permission to register as a provider',
'connecting since 2022-09-21T11:25:02.196835-07:00; error: could not obtain a token with the supplied credentials',
];
let index = null;

View File

@ -258,7 +258,7 @@ const testConvergentEncryption = async function (assert, keyName) {
}
// store ciphertext for decryption step
const copiedCiphertext = find('[data-test-encrypted-value="ciphertext"]').innerText;
await click('[data-test-modal-background]');
await click('.modal.is-active [data-test-modal-background]');
assert.dom('.modal.is-active').doesNotExist(`${name}: Modal closes after background clicked`);
await click('[data-test-transit-action-link="decrypt"]');
@ -275,7 +275,7 @@ const testConvergentEncryption = async function (assert, keyName) {
testCase.assertAfterDecrypt(keyName);
}
await click('[data-test-modal-background]');
await click('.modal.is-active [data-test-modal-background]');
assert.dom('.modal.is-active').doesNotExist(`${name}: Modal closes after background clicked`);
}

View File

@ -1,12 +1,10 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render } from '@ember/test-helpers';
import { click, render } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
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) {
setupRenderingTest(hooks);
setupMirage(hooks);
@ -17,111 +15,79 @@ module('Integration | Component | link-status', function (hooks) {
this.statuses = statuses;
});
test('it does not render banner or error when status is not present', async function (assert) {
await render(hbs`<LinkStatus @status={{undefined}} />`);
test('it does not render banner when status is not present', async function (assert) {
await render(hbs`
<div id="modal-wormhole"></div>
<LinkStatus @status={{undefined}} />
`);
assert.dom('.navbar-status').doesNotExist('Banner is hidden for disconnected state');
assert.dom('.navbar-status').doesNotExist('Banner is hidden for missing status message');
});
test('it does not render banner if not enterprise version', async function (assert) {
test('it does not render banner in oss version', async function (assert) {
this.owner.lookup('service:version').set('isEnterprise', false);
await render(hbs`<LinkStatus @status={{get this.statuses 0}} />`);
await render(hbs`
<div id="modal-wormhole"></div>
<LinkStatus @status={{get this.statuses 0}} />
`);
assert.dom('.navbar-status').doesNotExist('Banner is hidden for disconnected state');
assert.dom('.navbar-status').doesNotExist('Banner is hidden in oss');
});
test('it renders connected status', async function (assert) {
await render(hbs`<LinkStatus @status={{get this.statuses 0}} />`);
await render(hbs`
<div id="modal-wormhole"></div>
<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 banner class renders for connected state');
assert
.dom('[data-test-link-status]')
.hasText('This self-managed Vault is linked to HCP.', 'Copy renders for connected state');
.hasText('This self-managed Vault is linked to HCP.', 'Banner copy renders for connected state');
assert
.dom('[data-test-link-status] a')
.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}} />`);
test('it should render error states', async function (assert) {
// disconnected error
await render(hbs`
<div id="modal-wormhole"></div>
<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('.navbar-status').hasClass('warning', 'Correct banner class renders for error state');
assert
.dom('[data-test-link-status]')
.hasText(
`Vault has been disconnected from HCP since ${timestamp}. Error: some other error other than unknown`,
'Copy renders for disconnected error state'
'There was an error connecting to HCP. Click here for more information.',
'Banner copy renders for error state'
);
});
test('it should render for connection refused error state', async function (assert) {
await render(hbs`<LinkStatus @status={{get this.statuses 3}} />`);
await click('[data-test-link-status] button');
assert
.dom('.navbar-status')
.hasClass('warning', 'Correct class renders for connection refused error state');
.dom('[data-test-link-status-timestamp]')
.hasText('2022-09-21T11:25:02.196835-07:00', 'Timestamp renders');
assert
.dom('[data-test-link-status]')
.hasText(
`Vault has been trying to connect to HCP since ${timestamp}, but HCP is not reachable. Vault will try again soon.`,
'Copy renders for connection refused error state'
);
});
.dom('[data-test-link-status-error]')
.hasText('unable to establish a connection with HCP', 'Error renders');
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');
// connecting error
await render(hbs`
<div id="modal-wormhole"></div>
<LinkStatus @status={{get this.statuses 3}} />
`);
assert
.dom('[data-test-link-status]')
.hasText(
`Vault tried connecting to HCP, but the Resource ID is invalid. Check your resource ID. ${timestamp}`,
'Copy renders for resource id error state'
);
});
.dom('[data-test-link-status-error]')
.hasText('principal does not have the permission to register as a provider', 'Error renders');
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 HCP, 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 HCP since ${timestamp}. Vault will try again soon. Error: connection error we are unaware of`,
'Copy renders for unknown error state'
);
});
// connecting state should always be returned with timestamp and error
// this case came up in manual testing and should be fixed on the backend but additional checks were added just in case
test('it renders generic message for connecting state with no timestamp or error', async function (assert) {
await render(hbs`<LinkStatus @status="connecting" />`);
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 HCP. Vault will try again soon.`,
'Copy renders for unknown error state'
);
// this shouldn't happen but placeholders should render if disconnected/connecting status is returned without timestamp and/or error
await render(hbs`
<div id="modal-wormhole"></div>
<LinkStatus @status="connecting" />
`);
assert.dom('[data-test-link-status-timestamp]').hasText('Not available', 'Timestamp placeholder renders');
assert.dom('[data-test-link-status-error]').hasText('Not available', 'Error placeholder renders');
});
});

View File

@ -12,6 +12,7 @@ module('Integration | Component | nav header', function (hooks) {
test('it renders', async function (assert) {
await render(hbs`
<div id="modal-wormhole"></div>
{{#nav-header as |h|}}
{{#h.home}}
Home!

View File

@ -28,15 +28,19 @@ module('Integration | Component | replication-header', function (hooks) {
});
test('it renders', async function (assert) {
await render(hbs`<ReplicationHeader @data={{data}} @isSecondary={{isSecondary}} @title={{title}}/>`);
await render(hbs`
<div id="modal-wormhole"></div>
<ReplicationHeader @data={{data}} @isSecondary={{isSecondary}} @title={{title}}/>
`);
assert.dom('[data-test-replication-header]').exists();
});
test('it renders with mode and secondaryId when set', async function (assert) {
await render(
hbs`<ReplicationHeader @data={{data}} @isSecondary={{isSecondary}} @title={{title}} @secondaryId={{secondaryId}}/>`
);
await render(hbs`
<div id="modal-wormhole"></div>
<ReplicationHeader @data={{data}} @isSecondary={{isSecondary}} @title={{title}} @secondaryId={{secondaryId}}/>
`);
assert.dom('[data-test-secondaryId]').includesText(SECONDARY_ID, `shows the correct secondaryId value`);
assert.dom('[data-test-mode]').includesText('secondary', `shows the correct mode value`);
@ -48,16 +52,20 @@ module('Integration | Component | replication-header', function (hooks) {
this.set('data', notEnabled);
this.set('secondaryId', noId);
await render(
hbs`<ReplicationHeader @data={{data}} @isSecondary={{isSecondary}} @title={{title}} @secondaryId={{secondaryId}}/>`
);
await render(hbs`
<div id="modal-wormhole"></div>
<ReplicationHeader @data={{data}} @isSecondary={{isSecondary}} @title={{title}} @secondaryId={{secondaryId}}/>
`);
assert.dom('[data-test-secondaryId]').doesNotExist();
assert.dom('[data-test-mode]').doesNotExist();
});
test('it does not show tabs when showTabs is not set', async function (assert) {
await render(hbs`<ReplicationHeader @data={{data}} @isSecondary={{isSecondary}} @title={{title}}/>`);
await render(hbs`
<div id="modal-wormhole"></div>
<ReplicationHeader @data={{data}} @isSecondary={{isSecondary}} @title={{title}}/>
`);
assert.dom('[data-test-tabs]').doesNotExist();
});