yarn run prettier --write applied

Signed-off-by: Wesley <wpattison08@gmail.com>
This commit is contained in:
Wesley
2022-09-12 05:03:33 +02:00
parent 4a75ce761c
commit b518ac451e
21 changed files with 232 additions and 198 deletions
+38 -33
View File
@@ -2,7 +2,7 @@
Simple plugin that proxies requests to the Azure Portal API through Azure SDK JavaScript libraries.
*Inspired by [roadie.io AWS Lamda plugin](https://roadie.io/backstage/plugins/aws-lambda/)*
_Inspired by [roadie.io AWS Lamda plugin](https://roadie.io/backstage/plugins/aws-lambda/)_
## Setup
@@ -36,47 +36,52 @@ Here's how to get the backend plugin up and running:
1. First we need to add the `@backstage/plugin-azure-functions-backend` package to your backend:
```sh
# From the Backstage root directory
cd packages/backend
yarn add @backstage/plugin-azure-functions-backend
```
```sh
# From the Backstage root directory
cd packages/backend
yarn add @backstage/plugin-azure-functions-backend
```
2. Then we will create a new file named `packages/backend/src/plugins/azure-functions.ts`, and add the following to it:
```ts
import { createRouter, AzureWebManagementApi } from '@backstage/plugin-azure-functions-backend';
import { Router } from 'express';
import { PluginEnvironment } from '../types';
```ts
import {
createRouter,
AzureWebManagementApi,
} from '@backstage/plugin-azure-functions-backend';
import { Router } from 'express';
import { PluginEnvironment } from '../types';
export default async function createPlugin(
env: PluginEnvironment,
): Promise<Router> {
return await createRouter({
logger: env.logger,
azureWebManagementApi: AzureWebManagementApi.fromConfig(env.config)
});
}
```
export default async function createPlugin(
env: PluginEnvironment,
): Promise<Router> {
return await createRouter({
logger: env.logger,
azureWebManagementApi: AzureWebManagementApi.fromConfig(env.config),
});
}
```
3. Next we wire this into the overall backend router, edit `packages/backend/src/index.ts`:
```ts
import azureFunctions from './plugins/azure-functions';
```ts
import azureFunctions from './plugins/azure-functions';
// Removed for clairty...
// Removed for clairty...
async function main() {
// ...
// Add this line under the other lines that follow the useHotMemoize pattern
const azureFunctionsEnv = useHotMemoize(module, () => createEnv('azureFunctions'));
// ...
// Insert this line under the other lines that add their routers to apiRouter in the same way
apiRouter.use('/azure-functions', await azureFunctions(azureFunctionsEnv));
}
```
async function main() {
// ...
// Add this line under the other lines that follow the useHotMemoize pattern
const azureFunctionsEnv = useHotMemoize(module, () =>
createEnv('azureFunctions'),
);
// ...
// Insert this line under the other lines that add their routers to apiRouter in the same way
apiRouter.use('/azure-functions', await azureFunctions(azureFunctionsEnv));
}
```
4. Now run `yarn start-backend` from the repo root.
5. Finally, open `http://localhost:7007/api/azure-functions/health` in a browser, it should return `{"status":"ok"}`.
5. Finally, open `http://localhost:7007/api/azure-functions/health` in a browser, it should return `{"status":"ok"}`.
+19 -17
View File
@@ -15,21 +15,23 @@
*/
export interface Config {
azureFunctions: {
azureFunctions: {
/** @visibility backend */
tenantId: string;
/** @visibility backend */
clientId: string;
/** @visibility secret */
clientSecret: string;
/** @visibility backend */
domain: string;
/** @visibility backend */
allowedSubscriptions: [
{
/** @visibility backend */
name: string;
/** @visibility backend */
tenantId: string;
/** @visibility backend */
clientId: string;
/** @visibility secret */
clientSecret: string;
/** @visibility backend */
domain: string;
/** @visibility backend */
allowedSubscriptions: [{
/** @visibility backend */
name: string;
/** @visibility backend */
id: string;
}]
};
}
id: string;
},
];
};
}
@@ -17,69 +17,90 @@
import { Config } from '@backstage/config';
import { ClientSecretCredential } from '@azure/identity';
import { WebSiteManagementClient } from '@azure/arm-appservice';
import { AzureFunctionsAllowedSubscriptionsConfig, FunctionsData } from './types';
import {
AzureFunctionsAllowedSubscriptionsConfig,
FunctionsData,
} from './types';
export class AzureFunctionsConfig {
constructor(public readonly tenantId: string, public readonly clientId: string, public readonly clientSecret: string, public readonly domain: string, public readonly allowedSubscriptions: AzureFunctionsAllowedSubscriptionsConfig[]) { }
constructor(
public readonly tenantId: string,
public readonly clientId: string,
public readonly clientSecret: string,
public readonly domain: string,
public readonly allowedSubscriptions: AzureFunctionsAllowedSubscriptionsConfig[],
) {}
static fromConfig(config: Config): AzureFunctionsConfig {
const azfConfig = config.getConfig('azureFunctions');
static fromConfig(config: Config): AzureFunctionsConfig {
const azfConfig = config.getConfig('azureFunctions');
return new AzureFunctionsConfig(
azfConfig.getString('tenantId'),
azfConfig.getString('clientId'),
azfConfig.getString('clientSecret'),
azfConfig.getString('domain'),
azfConfig.getConfigArray('allowedSubscriptions').map<AzureFunctionsAllowedSubscriptionsConfig>(as => ({ id: as.getString('id'), name: as.getString('name') }))
)
}
return new AzureFunctionsConfig(
azfConfig.getString('tenantId'),
azfConfig.getString('clientId'),
azfConfig.getString('clientSecret'),
azfConfig.getString('domain'),
azfConfig
.getConfigArray('allowedSubscriptions')
.map<AzureFunctionsAllowedSubscriptionsConfig>(as => ({
id: as.getString('id'),
name: as.getString('name'),
})),
);
}
}
export class AzureWebManagementApi {
private readonly baseHref = (domain: string) => `https://portal.azure.com/#@${domain}/resource`;
private readonly clients: WebSiteManagementClient[] = [];
private readonly baseHref = (domain: string) =>
`https://portal.azure.com/#@${domain}/resource`;
private readonly clients: WebSiteManagementClient[] = [];
constructor(private readonly config: AzureFunctionsConfig) {
const creds = new ClientSecretCredential(config.tenantId, config.clientId, config.clientSecret);
for (const subscription of config.allowedSubscriptions) {
if (!this.clients.some(c => c.subscriptionId === subscription.id)) {
this.clients.push(new WebSiteManagementClient(creds, subscription.id));
}
constructor(private readonly config: AzureFunctionsConfig) {
const creds = new ClientSecretCredential(
config.tenantId,
config.clientId,
config.clientSecret,
);
for (const subscription of config.allowedSubscriptions) {
if (!this.clients.some(c => c.subscriptionId === subscription.id)) {
this.clients.push(new WebSiteManagementClient(creds, subscription.id));
}
}
}
static fromConfig(config: Config): AzureWebManagementApi {
return new AzureWebManagementApi(AzureFunctionsConfig.fromConfig(config));
}
async list({
functionName,
}: {
functionName: string;
}): Promise<FunctionsData[]> {
const results = [];
for (const client of this.clients) {
try {
for await (const webApp of client.webApps.list()) {
if (!webApp.name!.startsWith(functionName)) {
continue;
}
const v = webApp!;
results.push({
href: `${this.baseHref(this.config.domain)}${v.id!}`,
logstreamHref: `${this.baseHref(
this.config.domain,
)}${v.id!}/logStream`,
functionName: v.name!,
location: v.location!,
lastModifiedDate: v.lastModifiedTimeUtc!,
usageState: v.usageState!,
state: v.state!,
containerSize: v.containerSize!,
});
}
} catch (ex) {
console.log(ex);
}
}
static fromConfig(config: Config): AzureWebManagementApi {
return new AzureWebManagementApi(AzureFunctionsConfig.fromConfig(config));
}
async list({
functionName,
}: {
functionName: string;
}): Promise<FunctionsData[]> {
const results = [];
for (const client of this.clients) {
try {
for await (const webApp of client.webApps.list()) {
if (!webApp.name!.startsWith(functionName)) {
continue;
}
const v = webApp!;
results.push({
href: `${this.baseHref(this.config.domain)}${v.id!}`,
logstreamHref: `${this.baseHref(this.config.domain)}${v.id!}/logStream`,
functionName: v.name!,
location: v.location!,
lastModifiedDate: v.lastModifiedTimeUtc!,
usageState: v.usageState!,
state: v.state!,
containerSize: v.containerSize!
})
}
} catch (ex) {
console.log(ex);
}
}
return results;
}
}
return results;
}
}
@@ -14,5 +14,8 @@
* limitations under the License.
*/
export { AzureWebManagementApi, AzureFunctionsConfig } from './AzureWebManagementApi'
export * from './types'
export {
AzureWebManagementApi,
AzureFunctionsConfig,
} from './AzureWebManagementApi';
export * from './types';
@@ -15,17 +15,17 @@
*/
export interface AzureFunctionsAllowedSubscriptionsConfig {
name: string;
id: string;
name: string;
id: string;
}
export type FunctionsData = {
href: string;
logstreamHref: string;
functionName: string;
location: string;
state: string;
usageState: string;
containerSize: number;
lastModifiedDate: Date;
};
href: string;
logstreamHref: string;
functionName: string;
location: string;
state: string;
usageState: string;
containerSize: number;
lastModifiedDate: Date;
};
+1 -1
View File
@@ -15,4 +15,4 @@
*/
export * from './service/router';
export * from './api';
export * from './api';
@@ -39,7 +39,11 @@ export async function createRouter(
response.send({ status: 'ok' });
});
router.post('/list', async (request, response) => {
response.json(await azureWebManagementApi.list({ functionName: request.body.functionName!.toString() }))
response.json(
await azureWebManagementApi.list({
functionName: request.body.functionName!.toString(),
}),
);
});
router.use(errorHandler());
return router;
@@ -14,7 +14,10 @@
* limitations under the License.
*/
import { createServiceBuilder, loadBackendConfig } from '@backstage/backend-common';
import {
createServiceBuilder,
loadBackendConfig,
} from '@backstage/backend-common';
import { Server } from 'http';
import { Logger } from 'winston';
import { AzureWebManagementApi } from '../api';
@@ -30,11 +33,11 @@ export async function startStandaloneServer(
options: ServerOptions,
): Promise<Server> {
const logger = options.logger.child({ service: 'azure-functions-backend' });
const config = await loadBackendConfig({ logger, argv: process.argv })
const config = await loadBackendConfig({ logger, argv: process.argv });
logger.debug('Starting application server...');
const router = await createRouter({
logger,
azureWebManagementApi: AzureWebManagementApi.fromConfig(config)
azureWebManagementApi: AzureWebManagementApi.fromConfig(config),
});
let service = createServiceBuilder(module)
+2 -2
View File
@@ -2,7 +2,7 @@
![preview of Azure Functions table](docs/functions-table.png)
*Inspired by [roadie.io AWS Lamda plugin](https://roadie.io/backstage/plugins/aws-lambda/)*
_Inspired by [roadie.io AWS Lamda plugin](https://roadie.io/backstage/plugins/aws-lambda/)_
## Features
@@ -71,4 +71,4 @@ const serviceEntityPage = (
## Roadmap
- [ ] Metrics
- [ ] Metrics
+1 -3
View File
@@ -17,6 +17,4 @@
import { createDevApp } from '@backstage/dev-utils';
import { azureFunctionsPlugin } from '../src/plugin';
createDevApp()
.registerPlugin(azureFunctionsPlugin)
.render();
createDevApp().registerPlugin(azureFunctionsPlugin).render();
@@ -19,7 +19,6 @@ import { FunctionsData } from './types';
import { DiscoveryApi, IdentityApi } from '@backstage/core-plugin-api';
export class AzureFunctionsBackendClient implements AzureFunctionsApi {
private readonly identityApi: IdentityApi;
private readonly discoveryApi: DiscoveryApi;
constructor(options: {
@@ -36,7 +35,9 @@ export class AzureFunctionsBackendClient implements AzureFunctionsApi {
functionName: string;
}): Promise<FunctionsData[]> {
try {
const url = `${await this.discoveryApi.getBaseUrl('azure-functions')}/list`;
const url = `${await this.discoveryApi.getBaseUrl(
'azure-functions',
)}/list`;
const { token: idToken } = await this.identityApi.getCredentials();
const response = await fetch(url, {
method: 'POST',
@@ -44,11 +45,11 @@ export class AzureFunctionsBackendClient implements AzureFunctionsApi {
'Content-Type': 'application/json',
...(idToken && { Authorization: `Bearer ${idToken}` }),
},
body: JSON.stringify({ functionName: functionName })
body: JSON.stringify({ functionName: functionName }),
});
return await response.json();
} catch (e: any) {
throw new Error('MissingAzureBackendException')
throw new Error('MissingAzureBackendException');
}
}
}
+1 -1
View File
@@ -16,4 +16,4 @@
export * from './AzureFunctionsApi';
export * from './AzureFunctionsBackendClient';
export * from './types';
export * from './types';
+9 -9
View File
@@ -15,12 +15,12 @@
*/
export type FunctionsData = {
href: string;
logstreamHref: string;
functionName: string;
location: string;
state: string;
usageState: string;
containerSize: number;
lastModifiedDate: Date;
};
href: string;
logstreamHref: string;
functionName: string;
location: string;
state: string;
usageState: string;
containerSize: number;
lastModifiedDate: Date;
};
@@ -33,11 +33,16 @@ const AzureFunctionsOverview = ({ entity }: { entity: Entity }) => {
const { functionsName } = useServiceEntityAnnotations(entity);
const [functionsData] = useFunctions({
functionsName
functionsName,
});
return (
<><OverviewTable data={functionsData.data ?? []} loading={functionsData.loading} /></>
<>
<OverviewTable
data={functionsData.data ?? []}
loading={functionsData.loading}
/>
</>
);
};
@@ -45,12 +50,14 @@ export const AzureFunctionsOverviewWidget = () => {
const { entity } = useEntity();
if (!isAzureFunctionsAvailable(entity)) {
return (<MissingAnnotationEmptyState annotation={AZURE_FUNCTIONS_ANNOTATION} />);
return (
<MissingAnnotationEmptyState annotation={AZURE_FUNCTIONS_ANNOTATION} />
);
}
return (
<ErrorBoundary>
<AzureFunctionsOverview entity={entity} />
</ErrorBoundary>
)
);
};
@@ -28,9 +28,7 @@ import {
TestApiProvider,
} from '@backstage/test-utils';
import { setupServer } from 'msw/node';
import {
functionResponseMock,
} from '../../mocks/mocks';
import { functionResponseMock } from '../../mocks/mocks';
import { azureFunctionsApiRef } from '../..';
import { OverviewTable } from './OverviewTable';
@@ -60,19 +58,16 @@ describe('AzureFunctionsOverviewWidget', () => {
beforeEach(() => {
worker.use(
rest.post(
'/list',
(_, res, ctx) => {
res(ctx.json(functionResponseMock));
},
),
rest.post('/list', (_, res, ctx) => {
res(ctx.json(functionResponseMock));
}),
);
});
it('should display an overview table with the data from the requests', async () => {
const rendered = render(
<TestApiProvider apis={apis}>
<OverviewTable data={[functionResponseMock]} loading={false} />
<OverviewTable data={[functionResponseMock]} loading={false} />
</TestApiProvider>,
);
@@ -15,15 +15,10 @@
*/
import React from 'react';
import {
Box,
Card,
Link,
LinearProgress
} from '@material-ui/core';
import { Box, Card, Link, LinearProgress } from '@material-ui/core';
import { FunctionsData } from '../../api/types';
import { Table, TableColumn } from '@backstage/core-components';
import FlashOnIcon from '@material-ui/icons/FlashOn'
import FlashOnIcon from '@material-ui/icons/FlashOn';
type States = 'Waiting' | 'Running' | 'Paused' | 'Failed';
@@ -61,7 +56,11 @@ const DEFAULT_COLUMNS: TableColumn<FunctionsData>[] = [
title: 'name',
highlight: true,
render: (func: FunctionsData) => {
return (<Link href={func.href} target="_blank">{func.functionName}</Link>)
return (
<Link href={func.href} target="_blank">
{func.functionName}
</Link>
);
},
},
{
@@ -74,13 +73,18 @@ const DEFAULT_COLUMNS: TableColumn<FunctionsData>[] = [
},
{
title: 'last modified',
render: (func: FunctionsData) => new Date(func.lastModifiedDate).toUTCString(),
render: (func: FunctionsData) =>
new Date(func.lastModifiedDate).toUTCString(),
},
{
title: 'logs',
align: 'right',
render: (func: FunctionsData) => {
return (<Link href={func.logstreamHref} target="_blank">View Logs</Link>)
return (
<Link href={func.logstreamHref} target="_blank">
View Logs
</Link>
);
},
},
];
@@ -104,9 +108,7 @@ export const OverviewTable = ({ data, loading }: FunctionTableProps) => {
}
options={{ paging: true, search: false, pageSize: 10 }}
data={data}
emptyContent={
<LinearProgress />
}
emptyContent={<LinearProgress />}
isLoading={loading}
columns={columns}
/>
@@ -15,19 +15,12 @@
*/
import useAsyncRetry from 'react-use/lib/useAsyncRetry';
import {
useApi,
errorApiRef,
} from '@backstage/core-plugin-api';
import { useApi, errorApiRef } from '@backstage/core-plugin-api';
import { FunctionsData } from '../api/types';
import { azureFunctionsApiRef } from '../api';
import { useCallback } from 'react';
export function useFunctions({
functionsName
}: {
functionsName: string;
}) {
export function useFunctions({ functionsName }: { functionsName: string }) {
const azureFunctionsApi = useApi(azureFunctionsApiRef);
const errorApi = useApi(errorApiRef);
@@ -25,6 +25,6 @@ export const useServiceEntityAnnotations = (entity: Entity) => {
return {
projectName,
functionsName
functionsName,
};
};
+1 -1
View File
@@ -16,4 +16,4 @@
export * from './plugin';
export * from './api';
export * from './components/AzureFunctionsOverviewComponent/AzureFunctionsOverview';
export * from './components/AzureFunctionsOverviewComponent/AzureFunctionsOverview';
+11 -11
View File
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { FunctionsData } from "../api";
import { FunctionsData } from '../api';
export const entityMock = {
metadata: {
@@ -23,8 +23,7 @@ export const entityMock = {
'portal.azure.com/functions-name': 'func-mock',
},
name: 'sample-azure-function-service',
description:
'A service for testing Backstage functionality.',
description: 'A service for testing Backstage functionality.',
uid: 'c009b513-d053-4b3f-9429-8433a145e943',
},
apiVersion: 'backstage.io/v1alpha1',
@@ -38,12 +37,13 @@ export const entityMock = {
// https://management.azure.com/subscriptions/{{subscriptionId}}/resourceGroups/{{resourceGroup}}/providers/Microsoft.Web/sites/{{functionsName}}?api-version=2022-03-01
export const functionResponseMock: FunctionsData = {
functionName: "func-mock",
location: "West Europe",
state: "Running",
href: "https://mockurl.api.azurewebsites.windows.net:454/subscriptions/00000000-0000-0000-0000-000000000000/webspaces/rg_mock-WestEuropewebspace/sites/func-mock",
logstreamHref: "https://mockurl.api.azurewebsites.windows.net:454/subscriptions/00000000-0000-0000-0000-000000000000/webspaces/rg_mock-WestEuropewebspace/sites/func-mock/logStream",
usageState: "Normal",
lastModifiedDate: new Date("2022-09-02T11:09:58.9033333"),
containerSize: 100
functionName: 'func-mock',
location: 'West Europe',
state: 'Running',
href: 'https://mockurl.api.azurewebsites.windows.net:454/subscriptions/00000000-0000-0000-0000-000000000000/webspaces/rg_mock-WestEuropewebspace/sites/func-mock',
logstreamHref:
'https://mockurl.api.azurewebsites.windows.net:454/subscriptions/00000000-0000-0000-0000-000000000000/webspaces/rg_mock-WestEuropewebspace/sites/func-mock/logStream',
usageState: 'Normal',
lastModifiedDate: new Date('2022-09-02T11:09:58.9033333'),
containerSize: 100,
};
+6 -6
View File
@@ -20,7 +20,7 @@ import {
createPlugin,
createRouteRef,
discoveryApiRef,
identityApiRef
identityApiRef,
} from '@backstage/core-plugin-api';
import { azureFunctionsApiRef, AzureFunctionsBackendClient } from './api';
@@ -39,7 +39,7 @@ export const azureFunctionsPlugin = createPlugin({
},
factory: ({ discoveryApi, identityApi }) =>
new AzureFunctionsBackendClient({ discoveryApi, identityApi }),
})
}),
],
routes: {
entityContent: entityContentRouteRef,
@@ -51,9 +51,9 @@ export const EntityAzureFunctionsOverviewCard = azureFunctionsPlugin.provide(
name: 'EntityAzureFunctionsOverviewCard',
component: {
lazy: () =>
import('./components/AzureFunctionsOverviewComponent/AzureFunctionsOverview').then(
m => m.AzureFunctionsOverviewWidget,
),
import(
'./components/AzureFunctionsOverviewComponent/AzureFunctionsOverview'
).then(m => m.AzureFunctionsOverviewWidget),
},
}),
);
);