Merge branch 'master' into vault-display-full-secret-name

This commit is contained in:
df11
2022-06-23 15:53:47 +02:00
committed by GitHub
503 changed files with 8187 additions and 4268 deletions
+10
View File
@@ -1,5 +1,15 @@
# @backstage/plugin-vault
## 0.1.1-next.0
### Patch Changes
- 5ebf2c7023: Export missing parameters and added them to the api-report. Also adapted the API to the expected response from the backend
- Updated dependencies
- @backstage/catalog-model@1.1.0-next.0
- @backstage/core-components@0.9.6-next.0
- @backstage/plugin-catalog-react@1.1.2-next.0
## 0.1.0
### Minor Changes
+23
View File
@@ -5,13 +5,36 @@
```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;
// @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)
```
+9 -8
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-vault",
"description": "A Backstage plugin that integrates towards Vault",
"version": "0.1.0",
"version": "0.1.1-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -34,10 +34,11 @@
"postpack": "backstage-cli package postpack"
},
"dependencies": {
"@backstage/catalog-model": "^1.0.3",
"@backstage/core-components": "^0.9.5",
"@backstage/catalog-model": "^1.1.0-next.0",
"@backstage/core-components": "^0.9.6-next.0",
"@backstage/core-plugin-api": "^1.0.3",
"@backstage/plugin-catalog-react": "^1.1.1",
"@backstage/errors": "^1.0.0",
"@backstage/plugin-catalog-react": "^1.1.2-next.0",
"@backstage/theme": "^0.2.15",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
@@ -48,10 +49,10 @@
"react": "^16.13.1 || ^17.0.0"
},
"devDependencies": {
"@backstage/cli": "^0.17.2",
"@backstage/core-app-api": "^1.0.3",
"@backstage/dev-utils": "^1.0.3",
"@backstage/test-utils": "^1.1.1",
"@backstage/cli": "^0.17.3-next.0",
"@backstage/core-app-api": "^1.0.4-next.0",
"@backstage/dev-utils": "^1.0.4-next.0",
"@backstage/test-utils": "^1.1.2-next.0",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^12.1.3",
"@testing-library/user-event": "^14.0.0",
+25 -21
View File
@@ -27,20 +27,22 @@ describe('api', () => {
const mockBaseUrl = 'https://api-vault.com/api/vault';
const discoveryApi = UrlPatternDiscovery.compile(mockBaseUrl);
const mockSecretsResult: VaultSecret[] = [
{
name: 'secret::one',
path: 'test/success',
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',
path: 'test/success',
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',
path: 'test/success',
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',
path: 'test/success',
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(
@@ -49,31 +51,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
View File
@@ -14,11 +14,19 @@
* 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;
path: string;
@@ -26,10 +34,22 @@ export type VaultSecret = {
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;
@@ -40,7 +60,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()}`,
@@ -50,23 +70,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,20 +68,22 @@ describe('EntityVaultTable', () => {
},
};
const mockSecretsResult: VaultSecret[] = [
{
name: 'secret::one',
path: 'test/success',
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',
path: 'test/success',
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',
path: 'test/success',
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',
path: 'test/success',
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.path.replace(secretPath + "/", "")}/${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>
),
};
});
+6
View File
@@ -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),
+4
View File
@@ -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';
+4
View File
@@ -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';