First implementation of vault-plugin

Signed-off-by: ivgo <ivgo@spreadgroup.com>
This commit is contained in:
ivgo
2022-05-09 16:15:22 +02:00
parent 63f01a3554
commit 09f1256a85
33 changed files with 3658 additions and 2706 deletions
+73
View File
@@ -0,0 +1,73 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { DiscoveryApi, createApiRef } from '@backstage/core-plugin-api';
export const vaultApiRef = createApiRef<VaultApi>({
id: 'plugin.vault.service',
});
export type VaultSecretList = {
data: {
keys: string[];
};
};
export type Secret = {
name: string;
showUrl: string;
editUrl: string;
};
export interface VaultApi {
listSecrets(secretPath: string): Promise<Secret[]>;
}
export class VaultClient implements VaultApi {
private readonly discoveryApi: DiscoveryApi;
constructor({ discoveryApi }: { discoveryApi: DiscoveryApi }) {
this.discoveryApi = discoveryApi;
}
private async callApi<T>(
path: string,
query: { [key in string]: any },
): Promise<T | undefined> {
const apiUrl = `${await this.discoveryApi.getBaseUrl('vault')}`;
const response = await fetch(
`${apiUrl}/${path}?${new URLSearchParams(query).toString()}`,
{
headers: {
Accept: 'application/json',
},
},
);
if (response.status === 200) {
return (await response.json()) as T;
}
return undefined;
}
async listSecrets(secretPath: string): Promise<Secret[]> {
const result = await this.callApi<Secret[]>('v1/secrets', {
path: secretPath,
});
if (!result) {
return [];
}
return result;
}
}
@@ -0,0 +1,75 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { useEntity } from '@backstage/plugin-catalog-react';
import { makeStyles, Typography } from '@material-ui/core';
import { isVaultAvailable } from '../../conditions';
import { CodeSnippet, InfoCard, Button } from '@backstage/core-components';
import { BackstageTheme } from '@backstage/theme';
import { VAULT_SECRET_PATH_ANNOTATION } from '../../constants';
import { EntityVaultTable } from '../EntityVaultTable';
const COMPONENT_YAML = `metadata:
name: example
annotations:
${VAULT_SECRET_PATH_ANNOTATION}: value`;
const useStyles = makeStyles<BackstageTheme>(
theme => ({
code: {
borderRadius: 6,
margin: `${theme.spacing(2)}px 0px`,
background: theme.palette.type === 'dark' ? '#444' : '#fff',
},
}),
{ name: 'BackstageMissingVaultAnnotation' },
);
export const EntityVaultCard = () => {
const { entity } = useEntity();
const classes = useStyles();
if (isVaultAvailable(entity)) {
return <EntityVaultTable entity={entity} />;
}
return (
<InfoCard title="Vault">
<>
<Typography variant="body1">
Add the annotation to your component YAML as shown in the highlighted
example below:
</Typography>
<div className={classes.code}>
<CodeSnippet
text={COMPONENT_YAML}
language="yaml"
showLineNumbers
highlightedNumbers={[3, 4]}
customStyle={{ background: 'inherit', fontSize: '115%' }}
/>
</div>
<Button
color="primary"
variant="contained"
style={{ textDecoration: 'none' }}
to="https://backstage.io/docs/features/software-catalog/well-known-annotations"
>
Read more
</Button>
</>
</InfoCard>
);
};
@@ -0,0 +1,16 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { EntityVaultCard } from './EntityVaultCard';
@@ -0,0 +1,98 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { Entity } from '@backstage/catalog-model';
import { 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';
import Visibility from '@material-ui/icons/Visibility';
import Alert from '@material-ui/lab/Alert';
import useAsync from 'react-use/lib/useAsync';
import { Secret, vaultApiRef } from '../../api';
import { VAULT_SECRET_PATH_ANNOTATION } from '../../constants';
export const vaultSecretPath = (entity: Entity) => {
const secretPath =
entity.metadata.annotations?.[VAULT_SECRET_PATH_ANNOTATION] ?? '';
return { secretPath };
};
export const EntityVaultTable = ({ entity }: { entity: Entity }) => {
const vaultApi = useApi(vaultApiRef);
const { secretPath } = vaultSecretPath(entity);
const { value, loading, error } = useAsync(async (): Promise<Secret[]> => {
return vaultApi.listSecrets(secretPath);
}, []);
const columns: TableColumn[] = [
{ title: 'Secret', field: 'secret', highlight: true },
{ title: 'View URL', field: 'view', width: '10%' },
{ title: 'Edit URL', field: 'edit', width: '10%' },
];
const data = (value || []).map(secret => {
return {
secret: secret.name,
view: (
<a
aria-label="View"
title={`View ${secret.name}`}
href={secret.showUrl}
>
<Visibility style={{ fontSize: 16 }} />
</a>
),
edit: (
<a
aria-label="Edit"
title={`Edit ${secret.name}`}
href={secret.editUrl}
>
<Edit style={{ fontSize: 16 }} />
</a>
),
};
});
if (error) {
return <Alert severity="error">{error.message}</Alert>;
}
return (
<Table
title="Vault"
subtitle={`Secrets for ${entity.metadata.name} in ${secretPath}`}
columns={columns}
data={data}
isLoading={loading}
options={{
padding: 'dense',
pageSize: 10,
emptyRowsWhenPaging: false,
search: false,
}}
emptyContent={
<Box style={{ textAlign: 'center', padding: '15px' }}>
<Typography variant="body1">
No secrets found for {entity.metadata.name} in {secretPath}
</Typography>
</Box>
}
/>
);
};
@@ -0,0 +1,16 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { EntityVaultTable } from './EntityVaultTable';
+23
View File
@@ -0,0 +1,23 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Entity } from '@backstage/catalog-model';
import { VAULT_SECRET_PATH_ANNOTATION } from './constants';
export function isVaultAvailable(entity: Entity): boolean {
return Boolean(
entity.metadata.annotations?.hasOwnProperty(VAULT_SECRET_PATH_ANNOTATION),
);
}
+16
View File
@@ -0,0 +1,16 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export const VAULT_SECRET_PATH_ANNOTATION = 'vault.io/secrets-path';
+16
View File
@@ -0,0 +1,16 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { vaultPlugin, EntityVaultCard } from './plugin';
+22
View File
@@ -0,0 +1,22 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { vaultPlugin } from './plugin';
describe('vault', () => {
it('should export plugin', () => {
expect(vaultPlugin).toBeDefined();
});
});
+48
View File
@@ -0,0 +1,48 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
createApiFactory,
createComponentExtension,
createPlugin,
DiscoveryApi,
discoveryApiRef,
} from '@backstage/core-plugin-api';
import { VaultClient, vaultApiRef } from './api';
export const vaultPlugin = createPlugin({
id: 'vault',
apis: [
createApiFactory({
api: vaultApiRef,
deps: { discoveryApi: discoveryApiRef },
factory: ({ discoveryApi }: { discoveryApi: DiscoveryApi }) =>
new VaultClient({
discoveryApi,
}),
}),
],
});
export const EntityVaultCard = vaultPlugin.provide(
createComponentExtension({
name: 'EntityVaultCard',
component: {
lazy: () =>
import('./components/EntityVaultCard').then(m => m.EntityVaultCard),
},
}),
);
+20
View File
@@ -0,0 +1,20 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { createRouteRef } from '@backstage/core-plugin-api';
export const rootRouteRef = createRouteRef({
id: 'vault',
});
+17
View File
@@ -0,0 +1,17 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import '@testing-library/jest-dom';
import 'cross-fetch/polyfill';