diff --git a/.changeset/mean-squids-kneel.md b/.changeset/mean-squids-kneel.md new file mode 100644 index 0000000000..41a1bd4086 --- /dev/null +++ b/.changeset/mean-squids-kneel.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-vault': patch +--- + +Surface additional context and details to the Backstage UI when the Vault plugin encounters non-successful HTTP responses from the Vault API. diff --git a/plugins/vault/src/api.test.ts b/plugins/vault/src/api.test.ts index 4ce6f95c41..471cadc87e 100644 --- a/plugins/vault/src/api.test.ts +++ b/plugins/vault/src/api.test.ts @@ -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', + ); + }); }); diff --git a/plugins/vault/src/api.ts b/plugins/vault/src/api.ts index bdd881e08c..84843c9176 100644 --- a/plugins/vault/src/api.ts +++ b/plugins/vault/src/api.ts @@ -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 { diff --git a/plugins/vault/src/components/EntityVaultTable/EntityVaultTable.test.tsx b/plugins/vault/src/components/EntityVaultTable/EntityVaultTable.test.tsx index 82a90f23e1..27cc0ed91e 100644 --- a/plugins/vault/src/components/EntityVaultTable/EntityVaultTable.test.tsx +++ b/plugins/vault/src/components/EntityVaultTable/EntityVaultTable.test.tsx @@ -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( - + , ); 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( + + + , + ); + + expect( + rendered.getByText( + /Unexpected error while fetching secrets from path \'test\/error\'\: Request failed with 400 Error/, + ), + ).toBeInTheDocument(); + }); }); diff --git a/plugins/vault/src/components/EntityVaultTable/EntityVaultTable.tsx b/plugins/vault/src/components/EntityVaultTable/EntityVaultTable.tsx index efc345b07f..9191d54b0b 100644 --- a/plugins/vault/src/components/EntityVaultTable/EntityVaultTable.tsx +++ b/plugins/vault/src/components/EntityVaultTable/EntityVaultTable.tsx @@ -81,7 +81,12 @@ export const EntityVaultTable = ({ entity }: { entity: Entity }) => { }); if (error) { - return {error.message}; + return ( + + Unexpected error while fetching secrets from path '{secretPath}':{' '} + {error.message} + + ); } return (