improve Vault plugin error messaging

This seeks to address issue #16176 by improving the error messaging
surfaced to the UI with the Vault plugin encounters non-successful HTTP
responses from the Vault API.

Signed-off-by: Mike Ball <mikedball@gmail.com>
This commit is contained in:
Mike Ball
2023-02-04 08:05:02 -05:00
parent 0627378cc0
commit 489320214f
4 changed files with 64 additions and 40 deletions
+20 -2
View File
@@ -50,8 +50,10 @@ describe('api', () => {
const { path } = req.params;
if (path === 'test/success') {
return res(ctx.json(mockSecretsResult));
} else if (path === 'test/error') {
} else if (path === 'test/empty') {
return res(ctx.json({ items: [] }));
} else if (path === 'test/not-found') {
return res(ctx.status(404));
}
return res(ctx.status(400));
}),
@@ -72,7 +74,7 @@ describe('api', () => {
it('should return empty secret list', async () => {
setupHandlers();
const api = new VaultClient({ discoveryApi });
expect(await api.listSecrets('test/error')).toEqual([]);
expect(await api.listSecrets('test/empty')).toEqual([]);
});
it('should return all the secrets if no path defined', async () => {
@@ -80,4 +82,20 @@ describe('api', () => {
const api = new VaultClient({ discoveryApi });
expect(await api.listSecrets('')).toEqual(mockSecretsResult.items);
});
it('should throw an error if the Vault API responds with an HTTP 404', async () => {
setupHandlers();
const api = new VaultClient({ discoveryApi });
await expect(api.listSecrets('test/not-found')).rejects.toThrow(
"No secrets found in path 'v1/secrets/test%2Fnot-found'",
);
});
it('should throw an error if the Vault API responds with a non-successful HTTP status code', async () => {
setupHandlers();
const api = new VaultClient({ discoveryApi });
await expect(api.listSecrets('test/error')).rejects.toThrow(
'Request failed with 400 Error',
);
});
});
+2 -4
View File
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { DiscoveryApi, createApiRef } from '@backstage/core-plugin-api';
import { NotFoundError } from '@backstage/errors';
import { NotFoundError, ResponseError } from '@backstage/errors';
/**
* @public
@@ -75,9 +75,7 @@ export class VaultClient implements VaultApi {
} else if (response.status === 404) {
throw new NotFoundError(`No secrets found in path '${path}'`);
}
throw new Error(
`Unexpected error while fetching secrets from path '${path}'`,
);
throw await ResponseError.fromResponse(response);
}
async listSecrets(secretPath: string): Promise<VaultSecret[]> {
@@ -33,40 +33,28 @@ describe('EntityVaultTable', () => {
let apis: TestApiRegistry;
const mockBaseUrl = 'https://api-vault.com/api/vault';
const discoveryApi = UrlPatternDiscovery.compile(mockBaseUrl);
const entityOk: ComponentEntity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
metadata: {
name: 'test',
description: 'This is the description',
annotations: {
'vault.io/secrets-path': 'test/success',
const entity = (secretPath: string) => {
return {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
metadata: {
name: 'test',
description: 'This is the description',
annotations: {
'vault.io/secrets-path': secretPath,
},
},
},
spec: {
lifecycle: 'production',
owner: 'owner',
type: 'service',
},
spec: {
lifecycle: 'production',
owner: 'owner',
type: 'service',
},
} as ComponentEntity;
};
const entityNotOk: ComponentEntity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
metadata: {
name: 'test',
description: 'This is the description',
annotations: {
'vault.io/secrets-path': 'test/error',
},
},
spec: {
lifecycle: 'production',
owner: 'owner',
type: 'service',
},
};
const entityOk = entity('test/success');
const entityEmpty = entity('test/empty');
const entityNotOk = entity('test/error');
const mockSecretsResult: { items: VaultSecret[] } = {
items: [
@@ -91,7 +79,7 @@ describe('EntityVaultTable', () => {
const { path } = req.params;
if (path === 'test/success') {
return res(ctx.json(mockSecretsResult));
} else if (path === 'test/error') {
} else if (path === 'test/empty') {
return res(ctx.json([]));
}
return res(ctx.status(400));
@@ -122,10 +110,25 @@ describe('EntityVaultTable', () => {
setupHandlers();
const rendered = await renderInTestApp(
<ApiProvider apis={apis}>
<EntityVaultTable entity={entityNotOk} />
<EntityVaultTable entity={entityEmpty} />
</ApiProvider>,
);
expect(rendered.getByText(/No secrets found/)).toBeInTheDocument();
});
it('should surface an appropriate error when the Vault API responds unsuccessfully', async () => {
setupHandlers();
const rendered = await renderInTestApp(
<ApiProvider apis={apis}>
<EntityVaultTable entity={entityNotOk} />
</ApiProvider>,
);
expect(
rendered.getByText(
/Unexpected error while fetching secrets from path \'test\/error\'\: Request failed with 400 Error/,
),
).toBeInTheDocument();
});
});
@@ -81,7 +81,12 @@ export const EntityVaultTable = ({ entity }: { entity: Entity }) => {
});
if (error) {
return <Alert severity="error">{error.message}</Alert>;
return (
<Alert severity="error">
Unexpected error while fetching secrets from path '{secretPath}':{' '}
{error.message}
</Alert>
);
}
return (