Add requested improvements to vault plugin
Signed-off-by: ivgo <ivgo@spreadgroup.com>
This commit is contained in:
@@ -5,13 +5,38 @@
|
||||
```ts
|
||||
/// <reference types="react" />
|
||||
|
||||
import { ApiRef } from '@backstage/core-plugin-api';
|
||||
import { BackstagePlugin } from '@backstage/core-plugin-api';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
|
||||
// @public
|
||||
export const EntityVaultCard: () => JSX.Element;
|
||||
|
||||
// Warning: (ae-missing-release-tag) "isVaultAvailable" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public
|
||||
export function isVaultAvailable(entity: Entity): boolean;
|
||||
|
||||
// @public (undocumented)
|
||||
export const VAULT_SECRET_PATH_ANNOTATION = 'vault.io/secrets-path';
|
||||
|
||||
// @public
|
||||
export interface VaultApi {
|
||||
listSecrets(secretPath: string): Promise<VaultSecret[]>;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export const vaultApiRef: ApiRef<VaultApi>;
|
||||
|
||||
// @public
|
||||
export const vaultPlugin: BackstagePlugin<{}, {}>;
|
||||
|
||||
// @public
|
||||
export type VaultSecret = {
|
||||
name: string;
|
||||
showUrl: string;
|
||||
editUrl: string;
|
||||
};
|
||||
|
||||
// (No @packageDocumentation comment for this package)
|
||||
```
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
"@backstage/catalog-model": "^1.0.3",
|
||||
"@backstage/core-components": "^0.9.5",
|
||||
"@backstage/core-plugin-api": "^1.0.3",
|
||||
"@backstage/errors": "^1.0.0",
|
||||
"@backstage/plugin-catalog-react": "^1.1.1",
|
||||
"@backstage/theme": "^0.2.15",
|
||||
"@material-ui/core": "^4.12.2",
|
||||
|
||||
@@ -27,18 +27,20 @@ describe('api', () => {
|
||||
const mockBaseUrl = 'https://api-vault.com/api/vault';
|
||||
const discoveryApi = UrlPatternDiscovery.compile(mockBaseUrl);
|
||||
|
||||
const mockSecretsResult: VaultSecret[] = [
|
||||
{
|
||||
name: 'secret::one',
|
||||
editUrl: `${mockBaseUrl}/ui/vault/secrets/secrets/edit/test/success/secret::one`,
|
||||
showUrl: `${mockBaseUrl}/ui/vault/secrets/secrets/show/test/success/secret::one`,
|
||||
},
|
||||
{
|
||||
name: 'secret::two',
|
||||
editUrl: `${mockBaseUrl}/ui/vault/secrets/secrets/edit/test/success/secret::two`,
|
||||
showUrl: `${mockBaseUrl}/ui/vault/secrets/secrets/show/test/success/secret::two`,
|
||||
},
|
||||
];
|
||||
const mockSecretsResult: { items: VaultSecret[] } = {
|
||||
items: [
|
||||
{
|
||||
name: 'secret::one',
|
||||
editUrl: `${mockBaseUrl}/ui/vault/secrets/secrets/edit/test/success/secret::one`,
|
||||
showUrl: `${mockBaseUrl}/ui/vault/secrets/secrets/show/test/success/secret::one`,
|
||||
},
|
||||
{
|
||||
name: 'secret::two',
|
||||
editUrl: `${mockBaseUrl}/ui/vault/secrets/secrets/edit/test/success/secret::two`,
|
||||
showUrl: `${mockBaseUrl}/ui/vault/secrets/secrets/show/test/success/secret::two`,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const setupHandlers = () => {
|
||||
server.use(
|
||||
@@ -47,31 +49,33 @@ describe('api', () => {
|
||||
if (path === 'test/success') {
|
||||
return res(ctx.json(mockSecretsResult));
|
||||
} else if (path === 'test/error') {
|
||||
return res(ctx.json([]));
|
||||
return res(ctx.json({ items: [] }));
|
||||
}
|
||||
return res(ctx.status(400));
|
||||
}),
|
||||
rest.get(`${mockBaseUrl}/v1/secrets/`, (_req, res, ctx) => {
|
||||
return res(ctx.json(mockSecretsResult));
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
it('should return secrets', async () => {
|
||||
setupHandlers();
|
||||
const api = new VaultClient({ discoveryApi });
|
||||
const secrets = await api.listSecrets('test/success');
|
||||
expect(secrets).toEqual(mockSecretsResult);
|
||||
expect(await api.listSecrets('test/success')).toEqual(
|
||||
mockSecretsResult.items,
|
||||
);
|
||||
});
|
||||
|
||||
it('should return empty secret list', async () => {
|
||||
setupHandlers();
|
||||
const api = new VaultClient({ discoveryApi });
|
||||
expect(await api.listSecrets('test/error')).toEqual([]);
|
||||
expect(await api.listSecrets('')).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return no secrets', async () => {
|
||||
it('should return all the secrets if no path defined', async () => {
|
||||
setupHandlers();
|
||||
const api = new VaultClient({ discoveryApi });
|
||||
const secrets = await api.listSecrets('');
|
||||
expect(secrets).toEqual([]);
|
||||
expect(await api.listSecrets('')).toEqual(mockSecretsResult.items);
|
||||
});
|
||||
});
|
||||
|
||||
+29
-11
@@ -14,21 +14,41 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { DiscoveryApi, createApiRef } from '@backstage/core-plugin-api';
|
||||
import { NotFoundError } from '@backstage/errors';
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export const vaultApiRef = createApiRef<VaultApi>({
|
||||
id: 'plugin.vault.service',
|
||||
});
|
||||
|
||||
/**
|
||||
* Object containing the secret name and some links.
|
||||
* @public
|
||||
*/
|
||||
export type VaultSecret = {
|
||||
name: string;
|
||||
showUrl: string;
|
||||
editUrl: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Interface for the VaultApi.
|
||||
* @public
|
||||
*/
|
||||
export interface VaultApi {
|
||||
/**
|
||||
* Returns a list of secrets used to show in a table.
|
||||
* @param secretPath - The path where the secrets are stored in Vault
|
||||
*/
|
||||
listSecrets(secretPath: string): Promise<VaultSecret[]>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Default implementation of the VaultApi.
|
||||
* @public
|
||||
*/
|
||||
export class VaultClient implements VaultApi {
|
||||
private readonly discoveryApi: DiscoveryApi;
|
||||
|
||||
@@ -39,7 +59,7 @@ export class VaultClient implements VaultApi {
|
||||
private async callApi<T>(
|
||||
path: string,
|
||||
query: { [key in string]: any },
|
||||
): Promise<T | undefined> {
|
||||
): Promise<T> {
|
||||
const apiUrl = `${await this.discoveryApi.getBaseUrl('vault')}`;
|
||||
const response = await fetch(
|
||||
`${apiUrl}/${path}?${new URLSearchParams(query).toString()}`,
|
||||
@@ -49,23 +69,21 @@ export class VaultClient implements VaultApi {
|
||||
},
|
||||
},
|
||||
);
|
||||
if (response.status === 200) {
|
||||
if (response.ok) {
|
||||
return (await response.json()) as T;
|
||||
} else if (response.status === 404) {
|
||||
throw new NotFoundError(`No secrets found in path '${path}'`);
|
||||
}
|
||||
return undefined;
|
||||
throw new Error(
|
||||
`Unexpected error while fetching secrets from path '${path}'`,
|
||||
);
|
||||
}
|
||||
|
||||
async listSecrets(secretPath: string): Promise<VaultSecret[]> {
|
||||
if (secretPath === '') {
|
||||
return [];
|
||||
}
|
||||
const result = await this.callApi<VaultSecret[]>(
|
||||
const result = await this.callApi<{ items: VaultSecret[] }>(
|
||||
`v1/secrets/${encodeURIComponent(secretPath)}`,
|
||||
{},
|
||||
);
|
||||
if (!result) {
|
||||
return [];
|
||||
}
|
||||
return result;
|
||||
return result.items;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,18 +68,20 @@ describe('EntityVaultTable', () => {
|
||||
},
|
||||
};
|
||||
|
||||
const mockSecretsResult: VaultSecret[] = [
|
||||
{
|
||||
name: 'secret::one',
|
||||
editUrl: `${mockBaseUrl}/ui/vault/secrets/secrets/edit/test/success/secret::one`,
|
||||
showUrl: `${mockBaseUrl}/ui/vault/secrets/secrets/show/test/success/secret::one`,
|
||||
},
|
||||
{
|
||||
name: 'secret::two',
|
||||
editUrl: `${mockBaseUrl}/ui/vault/secrets/secrets/edit/test/success/secret::two`,
|
||||
showUrl: `${mockBaseUrl}/ui/vault/secrets/secrets/show/test/success/secret::two`,
|
||||
},
|
||||
];
|
||||
const mockSecretsResult: { items: VaultSecret[] } = {
|
||||
items: [
|
||||
{
|
||||
name: 'secret::one',
|
||||
editUrl: `${mockBaseUrl}/ui/vault/secrets/secrets/edit/test/success/secret::one`,
|
||||
showUrl: `${mockBaseUrl}/ui/vault/secrets/secrets/show/test/success/secret::one`,
|
||||
},
|
||||
{
|
||||
name: 'secret::two',
|
||||
editUrl: `${mockBaseUrl}/ui/vault/secrets/secrets/edit/test/success/secret::two`,
|
||||
showUrl: `${mockBaseUrl}/ui/vault/secrets/secrets/show/test/success/secret::two`,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const setupHandlers = () => {
|
||||
server.use(
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
*/
|
||||
import React from 'react';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { Table, TableColumn } from '@backstage/core-components';
|
||||
import { Link, Table, TableColumn } from '@backstage/core-components';
|
||||
import { useApi } from '@backstage/core-plugin-api';
|
||||
import { Box, Typography } from '@material-ui/core';
|
||||
import Edit from '@material-ui/icons/Edit';
|
||||
@@ -27,7 +27,7 @@ import { VAULT_SECRET_PATH_ANNOTATION } from '../../constants';
|
||||
|
||||
export const vaultSecretPath = (entity: Entity) => {
|
||||
const secretPath =
|
||||
entity.metadata.annotations?.[VAULT_SECRET_PATH_ANNOTATION] ?? '';
|
||||
entity.metadata.annotations?.[VAULT_SECRET_PATH_ANNOTATION];
|
||||
|
||||
return { secretPath };
|
||||
};
|
||||
@@ -35,6 +35,11 @@ export const vaultSecretPath = (entity: Entity) => {
|
||||
export const EntityVaultTable = ({ entity }: { entity: Entity }) => {
|
||||
const vaultApi = useApi(vaultApiRef);
|
||||
const { secretPath } = vaultSecretPath(entity);
|
||||
if (!secretPath) {
|
||||
throw Error(
|
||||
`The secret path is undefined. Please, define the annotation ${VAULT_SECRET_PATH_ANNOTATION}`,
|
||||
);
|
||||
}
|
||||
const { value, loading, error } = useAsync(async (): Promise<
|
||||
VaultSecret[]
|
||||
> => {
|
||||
@@ -51,22 +56,22 @@ export const EntityVaultTable = ({ entity }: { entity: Entity }) => {
|
||||
return {
|
||||
secret: secret.name,
|
||||
view: (
|
||||
<a
|
||||
<Link
|
||||
aria-label="View"
|
||||
title={`View ${secret.name}`}
|
||||
href={secret.showUrl}
|
||||
to={secret.showUrl}
|
||||
>
|
||||
<Visibility style={{ fontSize: 16 }} />
|
||||
</a>
|
||||
</Link>
|
||||
),
|
||||
edit: (
|
||||
<a
|
||||
<Link
|
||||
aria-label="Edit"
|
||||
title={`Edit ${secret.name}`}
|
||||
href={secret.editUrl}
|
||||
to={secret.editUrl}
|
||||
>
|
||||
<Edit style={{ fontSize: 16 }} />
|
||||
</a>
|
||||
</Link>
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -16,6 +16,12 @@
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { VAULT_SECRET_PATH_ANNOTATION } from './constants';
|
||||
|
||||
/**
|
||||
* Checks if an entity contains the annotation for Vault.
|
||||
* @param entity - The entity to check if the annotation exists
|
||||
* @returns If the annotation exists or not
|
||||
* @public
|
||||
*/
|
||||
export function isVaultAvailable(entity: Entity): boolean {
|
||||
return Boolean(
|
||||
entity.metadata.annotations?.hasOwnProperty(VAULT_SECRET_PATH_ANNOTATION),
|
||||
|
||||
@@ -13,4 +13,8 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export const VAULT_SECRET_PATH_ANNOTATION = 'vault.io/secrets-path';
|
||||
|
||||
@@ -14,3 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
export { vaultPlugin, EntityVaultCard } from './plugin';
|
||||
export { isVaultAvailable } from './conditions';
|
||||
export { VAULT_SECRET_PATH_ANNOTATION } from './constants';
|
||||
export { vaultApiRef } from './api';
|
||||
export type { VaultApi, VaultSecret } from './api';
|
||||
|
||||
Reference in New Issue
Block a user