(wip) refactor to more generic azure plugin

Signed-off-by: Wesley <wpattison08@gmail.com>
This commit is contained in:
Wesley
2022-09-17 11:14:36 +02:00
parent 027e824d28
commit 7ebd37c991
45 changed files with 3830 additions and 5971 deletions
+3 -3
View File
@@ -1,7 +1,7 @@
---
'@backstage/plugin-azure-functions': minor
'@backstage/plugin-azure-functions-backend': minor
'@backstage/plugin-azure-functions-common': minor
'@backstage/plugin-azure': minor
'@backstage/plugin-azure-backend': minor
'@backstage/plugin-azure-common': minor
---
Azure Functions Plugin support based on annotation supplied by the entity.
@@ -1,9 +0,0 @@
---
title: Azure Functions
author: FRISS
authorUrl: https://friss.com
category: Infrastructure
description: View Azure Functions for your components in Backstage.
documentation: https://github.com/backstage/backstage/tree/master/plugins/azure-functions
iconUrl: img/azurefunctions-icon.svg
npmPackageName: '@backstage/plugin-azure-functions'
+9
View File
@@ -0,0 +1,9 @@
---
title: Azure
author: FRISS
authorUrl: https://friss.com
category: Infrastructure
description: View related Azure information for your components in Backstage.
documentation: https://github.com/backstage/backstage/tree/master/plugins/azure
iconUrl: img/azure-icon.svg
npmPackageName: '@backstage/plugin-azure'
+23
View File
@@ -0,0 +1,23 @@
<svg aria-hidden="true" data-slug-id="azure" viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="a" x1="-960.606" y1="283.397" x2="-1032.511" y2="70.972" gradientTransform="matrix(1 0 0 -1 1075 318)" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#114a8b"/>
<stop offset="1" stop-color="#0669bc"/>
</linearGradient>
<linearGradient id="b" x1="-938.144" y1="184.402" x2="-954.778" y2="178.778" gradientTransform="matrix(1 0 0 -1 1075 318)" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-opacity=".3"/>
<stop offset=".071" stop-opacity=".2"/>
<stop offset=".321" stop-opacity=".1"/>
<stop offset=".623" stop-opacity=".05"/>
<stop offset="1" stop-opacity="0"/>
</linearGradient>
<linearGradient id="c" x1="-947.292" y1="289.594" x2="-868.363" y2="79.308" gradientTransform="matrix(1 0 0 -1 1075 318)" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#3ccbf4"/>
<stop offset="1" stop-color="#2892df"/>
</linearGradient>
</defs>
<path d="M89.158 18.266h69.238L86.523 231.224a11.041 11.041 0 01-10.461 7.51H22.179a11.023 11.023 0 01-10.445-14.548l66.963-198.41a11.04 11.04 0 0110.461-7.51z" fill="url(#a)"/>
<path d="M189.77 161.104H79.976a5.083 5.083 0 00-3.468 8.8l70.552 65.847a11.091 11.091 0 007.567 2.983h62.167z" fill="#0078d4"/>
<path d="M89.158 18.266a10.95 10.95 0 00-10.483 7.654L11.817 224.006a11.01 11.01 0 0010.393 14.728h55.274a11.814 11.814 0 009.069-7.714l13.33-39.29 47.625 44.418a11.267 11.267 0 007.089 2.586h61.937l-27.166-77.63-79.19.018 48.47-142.856z" fill="url(#b)"/>
<path d="M177.592 25.764a11.023 11.023 0 00-10.444-7.498H89.984a11.024 11.024 0 0110.445 7.498l66.967 198.421a11.024 11.024 0 01-10.445 14.549h77.164a11.024 11.024 0 0010.444-14.549z" fill="url(#c)"/>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

+1 -1
View File
@@ -18,8 +18,8 @@
"@backstage/plugin-airbrake": "^0.3.9-next.3",
"@backstage/plugin-apache-airflow": "^0.2.2-next.3",
"@backstage/plugin-api-docs": "^0.8.9-next.3",
"@backstage/plugin-azure": "^0.0.0",
"@backstage/plugin-azure-devops": "^0.2.0-next.3",
"@backstage/plugin-azure-functions": "^0.0.0",
"@backstage/plugin-badges": "^0.2.33-next.3",
"@backstage/plugin-catalog-common": "^1.0.6-next.0",
"@backstage/plugin-catalog-graph": "^0.2.21-next.2",
+1 -1
View File
@@ -35,8 +35,8 @@
"@backstage/plugin-app-backend": "^0.3.36-next.3",
"@backstage/plugin-auth-backend": "^0.16.0-next.3",
"@backstage/plugin-auth-node": "^0.2.5-next.3",
"@backstage/plugin-azure-backend": "^0.0.0",
"@backstage/plugin-azure-devops-backend": "^0.3.15-next.2",
"@backstage/plugin-azure-functions-backend": "^0.0.0",
"@backstage/plugin-badges-backend": "^0.1.30-next.1",
"@backstage/plugin-catalog-backend": "^1.4.0-next.3",
"@backstage/plugin-code-coverage-backend": "^0.2.2-next.2",
@@ -1,4 +1,4 @@
# Azure Functions Backend
# Azure Backend
Simple plugin that proxies requests to the Azure Portal API through Azure SDK JavaScript libraries.
@@ -6,21 +6,20 @@ _Inspired by [roadie.io AWS Lamda plugin](https://roadie.io/backstage/plugins/aw
## Setup
The following sections will help you get the Azure Functions Backend plugin setup and running.
The following sections will help you get the Azure Backend plugin setup and running.
### Configuration
The Azure Functions plugin requires the following YAML to be added to your app-config.yaml:
The Azure plugin requires the following YAML to be added to your app-config.yaml:
```yaml
azureFunctions:
azure:
domain:
tenantId:
clientId:
clientSecret:
allowedSubscriptions:
- id:
name:
```
Configuration Details:
@@ -28,26 +27,26 @@ Configuration Details:
- `domain` can be found by visiting the [Directories + Subscriptions settings page](https://portal.azure.com/#settings/directory). Alternatively you can inspect the [Azure home](https://portal.azure.com/#home) URL - `https://portal.azure.com/#@<Your_Domain>/`.
- `tenantId` can be found by visiting [Azure Directory Overview page](https://portal.azure.com/#blade/Microsoft_AAD_IAM/ActiveDirectoryMenuBlade).
- (Optional) `clientId` and `clientSecret` can be the same values you used for [Azure DevOps Backend](https://github.com/backstage/backstage/tree/master/plugins/azure-devops-backend) or [Azure Integration](https://backstage.io/docs/integrations/azure/org#app-registration) as long as this App Registration has permissions to read your function apps.
- `allowedSubscriptions` is an array of `id` and `name` that will be used to iterate over and look for the specified functions' app. `id` and `name` can be found the [Subscriptions page](https://portal.azure.com/#view/Microsoft_Azure_Billing/SubscriptionsBlade).
- (Optional) `allowedSubscriptions` is an array of `id` that will be used to iterate over and look for the specified functions' app. `id` can be found the [Subscriptions page](https://portal.azure.com/#view/Microsoft_Azure_Billing/SubscriptionsBlade).
### Integrating
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:
1. First we need to add the `@backstage/plugin-azure-backend` package to your backend:
```sh
# From the Backstage root directory
yarn add --cwd packages/backend @backstage/plugin-azure-functions-backend
yarn add --cwd packages/backend @backstage/plugin-azure-backend
```
2. Then we will create a new file named `packages/backend/src/plugins/azure-functions.ts`, and add the following to it:
2. Then we will create a new file named `packages/backend/src/plugins/azure.ts`, and add the following to it:
```ts
import {
createRouter,
AzureWebManagementApi,
} from '@backstage/plugin-azure-functions-backend';
AzureSitesApi,
} from '@backstage/plugin-azure-backend';
import { Router } from 'express';
import { PluginEnvironment } from '../types';
@@ -56,7 +55,7 @@ Here's how to get the backend plugin up and running:
): Promise<Router> {
return await createRouter({
logger: env.logger,
azureWebManagementApi: AzureWebManagementApi.fromConfig(env.config),
azureSitesApi: AzureSitesApi.fromConfig(env.config),
});
}
```
@@ -64,23 +63,23 @@ Here's how to get the backend plugin up and running:
3. Next we wire this into the overall backend router, edit `packages/backend/src/index.ts`:
```ts
import azureFunctions from './plugins/azure-functions';
import azure from './plugins/azure';
// Removed for clairty...
async function main() {
// ...
// Add this line under the other lines that follow the useHotMemoize pattern
const azureFunctionsEnv = useHotMemoize(module, () =>
createEnv('azureFunctions'),
const azureEnv = useHotMemoize(module, () =>
createEnv('azure'),
);
// ...
// Insert this line under the other lines that add their routers to apiRouter in the same way
apiRouter.use('/azure-functions', await azureFunctions(azureFunctionsEnv));
apiRouter.use('/azure', await azure(azureEnv));
}
```
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/health` in a browser, it should return `{"status":"ok"}`.
@@ -1,5 +1,5 @@
{
"name": "@backstage/plugin-azure-functions-backend",
"name": "@backstage/plugin-azure-backend",
"version": "0.0.0",
"main": "src/index.ts",
"types": "src/index.ts",
@@ -17,11 +17,10 @@
"repository": {
"type": "git",
"url": "https://github.com/backstage/backstage",
"directory": "plugins/azure-functions-backend"
"directory": "plugins/azure-backend"
},
"keywords": [
"backstage",
"functions",
"azure"
],
"scripts": {
@@ -36,9 +35,9 @@
"dependencies": {
"@azure/arm-resourcegraph": "^4.2.1",
"@azure/identity": "^2.1.0",
"@backstage/backend-common": "^0.15.1-next.2",
"@backstage/backend-common": "^0.15.1-next.3",
"@backstage/config": "^1.0.2-next.0",
"@backstage/plugin-azure-functions-common": "^0.0.0",
"@backstage/plugin-azure-common": "^0.0.0",
"@types/express": "*",
"express": "^4.17.1",
"express-promise-router": "^4.1.0",
@@ -15,23 +15,20 @@
*/
export interface Config {
azureFunctions: {
azure: {
/** @visibility backend */
domain: string;
/** @visibility backend */
tenantId: string;
/** @visibility backend */
subscriptions?: [
{
id: string;
},
];
/** @visibility backend */
clientId?: string;
/** @visibility secret */
clientSecret?: string;
/** @visibility backend */
domain: string;
/** @visibility backend */
allowedSubscriptions: [
{
/** @visibility backend */
name: string;
/** @visibility backend */
id: string;
},
];
};
}
@@ -20,46 +20,16 @@ import {
ClientSecretCredential,
} from '@azure/identity';
import { ResourceGraphClient } from '@azure/arm-resourcegraph';
import {
AzureFunctionsAllowedSubscriptionsConfig,
FunctionsListResponse,
} from '@backstage/plugin-azure-functions-common';
import { Site, SiteList } from '@backstage/plugin-azure-common';
import { AzureConfig } from '../config';
/** @public */
export class AzureFunctionsConfig {
constructor(
public readonly domain: string,
public readonly allowedSubscriptions: AzureFunctionsAllowedSubscriptionsConfig[],
public readonly tenantId: string,
public readonly clientId?: string,
public readonly clientSecret?: string,
) {}
static fromConfig(config: Config): AzureFunctionsConfig {
const azfConfig = config.getConfig('azureFunctions');
return new AzureFunctionsConfig(
azfConfig.getString('domain'),
azfConfig
.getConfigArray('allowedSubscriptions')
.map<AzureFunctionsAllowedSubscriptionsConfig>(as => ({
id: as.getString('id'),
name: as.getString('name'),
})),
azfConfig.getString('tenantId'),
azfConfig.getOptionalString('clientId'),
azfConfig.getOptionalString('clientSecret'),
);
}
}
/** @public */
export class AzureWebManagementApi {
export class AzureSitesApi {
private readonly baseHref = (domain: string) =>
`https://portal.azure.com/#@${domain}/resource`;
private readonly resourceGraphClient: ResourceGraphClient;
private readonly client: ResourceGraphClient;
constructor(private readonly config: AzureFunctionsConfig) {
constructor(private readonly config: AzureConfig) {
const creds =
config.clientId && config.clientSecret
? new ClientSecretCredential(
@@ -69,23 +39,19 @@ export class AzureWebManagementApi {
)
: new DefaultAzureCredential({ tenantId: config.tenantId });
this.resourceGraphClient = new ResourceGraphClient(creds);
this.client = new ResourceGraphClient(creds);
}
static fromConfig(config: Config): AzureWebManagementApi {
return new AzureWebManagementApi(AzureFunctionsConfig.fromConfig(config));
static fromConfig(config: Config): AzureSitesApi {
return new AzureSitesApi(AzureConfig.fromConfig(config));
}
async list({
functionName,
}: {
functionName: string;
}): Promise<FunctionsListResponse> {
const items = [];
async list({ name }: { name: string }): Promise<SiteList> {
const items: Site[] = [];
try {
const result = await this.resourceGraphClient.resources({
query: `resources | where type == 'microsoft.web/sites' | where name contains '${functionName}'`,
subscriptions: this.config.allowedSubscriptions.map(s => s.id),
const result = await this.client.resources({
query: `resources | where type == 'microsoft.web/sites' | where name contains '${name}'`,
subscriptions: this.config.subscriptions,
});
for (const v of result.data) {
items.push({
@@ -93,12 +59,15 @@ export class AzureWebManagementApi {
logstreamHref: `${this.baseHref(
this.config.domain,
)}${v.id!}/logStream`,
functionName: v.name!,
name: v.name!,
kind: v.kind!,
resourceGroup: v.resourceGroup!,
location: v.location!,
lastModifiedDate: v.properties.lastModifiedTimeUtc!,
usageState: v.properties.usageState!,
state: v.properties.state!,
containerSize: v.properties.containerSize!,
tags: v.properties.tags!,
});
}
} catch (ex) {
@@ -14,5 +14,4 @@
* limitations under the License.
*/
export * from './AzureFunctionsApi';
export * from './AzureFunctionsBackendClient';
export { AzureSitesApi } from './AzureSitesApi';
+42
View File
@@ -0,0 +1,42 @@
/*
* Copyright 2022 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 { Config } from '@backstage/config';
/** @public */
export class AzureConfig {
constructor(
public readonly domain: string,
public readonly tenantId: string,
public readonly subscriptions?: string[],
public readonly clientId?: string,
public readonly clientSecret?: string,
) {}
static fromConfig(config: Config): AzureConfig {
const azConfig = config.getConfig('azure');
return new AzureConfig(
azConfig.getString('domain'),
azConfig.getString('tenantId'),
azConfig
.getOptionalConfigArray('subscriptions')
?.map<string>(as => as.getString('id')),
azConfig.getOptionalString('clientId'),
azConfig.getOptionalString('clientSecret'),
);
}
}
@@ -19,19 +19,19 @@ import express from 'express';
import Router from 'express-promise-router';
import { Logger } from 'winston';
import { AzureWebManagementApi } from '../api';
import { AzureSitesApi } from '../api';
/** @public */
export interface RouterOptions {
logger: Logger;
azureWebManagementApi: AzureWebManagementApi;
azureSitesApi: AzureSitesApi;
}
/** @public */
export async function createRouter(
options: RouterOptions,
): Promise<express.Router> {
const { logger, azureWebManagementApi } = options;
const { logger, azureSitesApi } = options;
const router = Router();
router.use(express.json());
@@ -40,11 +40,11 @@ export async function createRouter(
logger.info('PONG!');
response.send({ status: 'ok' });
});
router.get('/list/:functionName', async (request, response) => {
const { functionName } = request.params;
router.get('/list/:name', async (request, response) => {
const { name } = request.params;
response.json(
await azureWebManagementApi.list({
functionName: functionName,
await azureSitesApi.list({
name: name,
}),
);
});
@@ -20,7 +20,7 @@ import {
} from '@backstage/backend-common';
import { Server } from 'http';
import { Logger } from 'winston';
import { AzureWebManagementApi } from '../api';
import { AzureSitesApi } from '../api';
import { createRouter } from './router';
export interface ServerOptions {
@@ -32,17 +32,17 @@ export interface ServerOptions {
export async function startStandaloneServer(
options: ServerOptions,
): Promise<Server> {
const logger = options.logger.child({ service: 'azure-functions-backend' });
const logger = options.logger.child({ service: 'azure-backend' });
const config = await loadBackendConfig({ logger, argv: process.argv });
logger.debug('Starting application server...');
const router = await createRouter({
logger,
azureWebManagementApi: AzureWebManagementApi.fromConfig(config),
azureSitesApi: AzureSitesApi.fromConfig(config),
});
let service = createServiceBuilder(module)
.setPort(options.port)
.addRouter('/azure-functions-backend', router);
.addRouter('/azure-backend', router);
if (options.enableCors) {
service = service.enableCors({ origin: 'http://localhost:3000' });
}
@@ -1,31 +1,26 @@
## API Report File for "@backstage/plugin-azure-functions-common"
## API Report File for "@backstage/plugin-azure-common"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
// @public (undocumented)
export interface AzureFunctionsAllowedSubscriptionsConfig {
// (undocumented)
id: string;
// (undocumented)
name: string;
}
// @public (undocumented)
export type FunctionsData = {
export type Site = {
href: string;
logstreamHref: string;
functionName: string;
name: string;
kind: string;
location: string;
resourceGroup: string;
state: string;
usageState: string;
containerSize: number;
lastModifiedDate: Date;
lastModifiedDate: string;
tags: {};
};
// @public (undocumented)
export type FunctionsListResponse = {
items: FunctionsData[];
export type SiteList = {
items: Site[];
};
// (No @packageDocumentation comment for this package)
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-azure-functions-common",
"description": "Common functionalities for the azure-functions plugin",
"name": "@backstage/plugin-azure-common",
"description": "Common functionalities for the azure plugin",
"version": "0.0.0",
"main": "src/index.ts",
"types": "src/index.ts",
@@ -18,7 +18,7 @@
"repository": {
"type": "git",
"url": "https://github.com/backstage/backstage",
"directory": "plugins/azure-functions-common"
"directory": "plugins/azure-common"
},
"keywords": [
"backstage"
@@ -15,24 +15,21 @@
*/
/** @public */
export interface AzureFunctionsAllowedSubscriptionsConfig {
name: string;
id: string;
}
/** @public */
export type FunctionsData = {
export type Site = {
href: string;
logstreamHref: string;
functionName: string;
name: string;
kind: string;
location: string;
resourceGroup: string;
state: string;
usageState: string;
containerSize: number;
lastModifiedDate: Date;
lastModifiedDate: string;
tags: {};
};
/** @public */
export type FunctionsListResponse = {
items: FunctionsData[];
export type SiteList = {
items: Site[];
};
@@ -1,60 +0,0 @@
## API Report File for "@backstage/plugin-azure-functions-backend"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { AzureFunctionsAllowedSubscriptionsConfig } from '@backstage/plugin-azure-functions-common';
import { Config } from '@backstage/config';
import express from 'express';
import { FunctionsListResponse } from '@backstage/plugin-azure-functions-common';
import { Logger } from 'winston';
// @public (undocumented)
export class AzureFunctionsConfig {
constructor(
domain: string,
allowedSubscriptions: AzureFunctionsAllowedSubscriptionsConfig[],
tenantId: string,
clientId?: string | undefined,
clientSecret?: string | undefined,
);
// (undocumented)
readonly allowedSubscriptions: AzureFunctionsAllowedSubscriptionsConfig[];
// (undocumented)
readonly clientId?: string | undefined;
// (undocumented)
readonly clientSecret?: string | undefined;
// (undocumented)
readonly domain: string;
// (undocumented)
static fromConfig(config: Config): AzureFunctionsConfig;
// (undocumented)
readonly tenantId: string;
}
// @public (undocumented)
export class AzureWebManagementApi {
constructor(config: AzureFunctionsConfig);
// (undocumented)
static fromConfig(config: Config): AzureWebManagementApi;
// (undocumented)
list({
functionName,
}: {
functionName: string;
}): Promise<FunctionsListResponse>;
}
// @public (undocumented)
export function createRouter(options: RouterOptions): Promise<express.Router>;
// @public (undocumented)
export interface RouterOptions {
// (undocumented)
azureWebManagementApi: AzureWebManagementApi;
// (undocumented)
logger: Logger;
}
// (No @packageDocumentation comment for this package)
```
-64
View File
@@ -1,64 +0,0 @@
## API Report File for "@backstage/plugin-azure-functions"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
/// <reference types="react" />
import { ApiRef } from '@backstage/core-plugin-api';
import { BackstagePlugin } from '@backstage/core-plugin-api';
import { DiscoveryApi } from '@backstage/core-plugin-api';
import { Entity } from '@backstage/catalog-model';
import { FunctionsListResponse } from '@backstage/plugin-azure-functions-common';
import { IdentityApi } from '@backstage/core-plugin-api';
import { RouteRef } from '@backstage/core-plugin-api';
// @public (undocumented)
export type AzureFunctionsApi = {
list: ({
functionName,
}: {
functionName: string;
}) => Promise<FunctionsListResponse>;
};
// @public (undocumented)
export const azureFunctionsApiRef: ApiRef<AzureFunctionsApi>;
// @public (undocumented)
export class AzureFunctionsBackendClient implements AzureFunctionsApi {
constructor(options: {
discoveryApi: DiscoveryApi;
identityApi: IdentityApi;
});
// (undocumented)
list({
functionName,
}: {
functionName: string;
}): Promise<FunctionsListResponse>;
}
// @public (undocumented)
export const AzureFunctionsOverviewWidget: () => JSX.Element;
// @public (undocumented)
export const azureFunctionsPlugin: BackstagePlugin<
{
entityContent: RouteRef<undefined>;
},
{},
{}
>;
// @public (undocumented)
export const EntityAzureFunctionsOverviewCard: () => JSX.Element;
// @public (undocumented)
export const entityContentRouteRef: RouteRef<undefined>;
// @public (undocumented)
export const isAzureFunctionsAvailable: (entity: Entity) => string | undefined;
// (No @packageDocumentation comment for this package)
```
@@ -1,12 +1,12 @@
# Azure Functions Plugin
# Azure Plugin
![preview of Azure Functions table](docs/functions-table.png)
![preview of Azure table](docs/functions-table.png)
_Inspired by [roadie.io AWS Lamda plugin](https://roadie.io/backstage/plugins/aws-lambda/)_
## Features
- Azure Functions overview table
- Azure overview table
## Plugin Setup
@@ -14,17 +14,17 @@ The following sections will help you get the Azure Functions plugin setup and ru
### Azure Functions Backend
You need to set up the Azure Functions backend plugin before you move forward with any of these steps if you haven't already.
You need to set up the Azure backend plugin before you move forward with any of these steps if you haven't already.
### Entity Annotation
To be able to use the Azure Functions plugin you need to add the following annotation to any entities you want to use it with:
To be able to use the Azure plugin you need to add the following annotation to any entities you want to use it with:
```yaml
portal.azure.com/functions-name: <function-name>
portal.azure.com/resource-name: <resource-name>
```
`<function-name>` can be an exact or partial name for the functions' app.
`<resource-name>` can be an exact or partial name for the site.
Example of Partial Matching:
@@ -39,7 +39,7 @@ func-testapp-us
The annotation you will use to have the three functions' app appear in the overview table would look like this:
```yaml
portal.azure.com/functions-name: func-testapp
portal.azure.com/resource-name: func-testapp
```
### Install the component
@@ -47,22 +47,22 @@ portal.azure.com/functions-name: func-testapp
1. Install the plugin in the `packages/app` directory
```sh
yarn add @backstage/plugin-azure-functions
yarn add @backstage/plugin-azure
```
2. Add widget component to your Backstage instance:
```ts
// In packages/app/src/components/catalog/EntityPage.tsx
import { EntityAzureFunctionsOverviewCard, isAzureFunctionsAvailable } from '@backstage/plugin-azure-functions';
import { EntityAzureSitesOverviewWidget, isAzureResourceNameAvailable } from '@backstage/plugin-azure';
...
const serviceEntityPage = (
<EntityLayout>
//...
<EntityLayout.Route if={e => Boolean(isAzureFunctionsAvailable(e))} path="/azure-functions" title="Azure Functions">
<EntityAzureFunctionsOverviewCard />
<EntityLayout.Route if={e => Boolean(isAzureResourceNameAvailable(e))} path="/azure" title="Azure">
<EntityAzureSitesOverviewWidget />
</EntityLayout.Route>
//...
</EntityLayout>
@@ -15,6 +15,6 @@
*/
import { createDevApp } from '@backstage/dev-utils';
import { azureFunctionsPlugin } from '../src/plugin';
import { azureSitesPlugin } from '../src/plugin';
createDevApp().registerPlugin(azureFunctionsPlugin).render();
createDevApp().registerPlugin(azureSitesPlugin).render();

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 57 KiB

@@ -1,5 +1,5 @@
{
"name": "@backstage/plugin-azure-functions",
"name": "@backstage/plugin-azure",
"version": "0.0.0",
"main": "src/index.ts",
"types": "src/index.ts",
@@ -17,11 +17,10 @@
"repository": {
"type": "git",
"url": "https://github.com/backstage/backstage",
"directory": "plugins/azure-functions"
"directory": "plugins/azure"
},
"keywords": [
"backstage",
"functions",
"azure"
],
"scripts": {
@@ -37,7 +36,7 @@
"@backstage/catalog-model": "^1.1.1-next.0",
"@backstage/core-components": "^0.11.1-next.2",
"@backstage/core-plugin-api": "^1.0.6-next.2",
"@backstage/plugin-azure-functions-common": "^0.0.0",
"@backstage/plugin-azure-common": "^0.0.0",
"@backstage/plugin-catalog-react": "^1.1.4-next.1",
"@backstage/theme": "^0.2.16",
"@material-ui/core": "^4.9.13",
@@ -15,18 +15,14 @@
*/
import { createApiRef } from '@backstage/core-plugin-api';
import { FunctionsListResponse } from '@backstage/plugin-azure-functions-common';
import { SiteList } from '@backstage/plugin-azure-common';
/** @public */
export const azureFunctionsApiRef = createApiRef<AzureFunctionsApi>({
id: 'plugin.azurefunctions.service',
export const azureSiteApiRef = createApiRef<AzureSitesApi>({
id: 'plugin.azure.service',
});
/** @public */
export type AzureFunctionsApi = {
list: ({
functionName,
}: {
functionName: string;
}) => Promise<FunctionsListResponse>;
export type AzureSitesApi = {
list: ({ name }: { name: string }) => Promise<SiteList>;
};
@@ -14,12 +14,12 @@
* limitations under the License.
*/
import { AzureFunctionsApi } from './AzureFunctionsApi';
import { FunctionsListResponse } from '@backstage/plugin-azure-functions-common';
import { AzureSitesApi } from './AzureSitesApi';
import { SiteList } from '@backstage/plugin-azure-common';
import { DiscoveryApi, IdentityApi } from '@backstage/core-plugin-api';
/** @public */
export class AzureFunctionsBackendClient implements AzureFunctionsApi {
export class AzureSitesApiBackendClient implements AzureSitesApi {
private readonly identityApi: IdentityApi;
private readonly discoveryApi: DiscoveryApi;
constructor(options: {
@@ -30,15 +30,9 @@ export class AzureFunctionsBackendClient implements AzureFunctionsApi {
this.identityApi = options.identityApi;
}
async list({
functionName,
}: {
functionName: string;
}): Promise<FunctionsListResponse> {
async list({ name }: { name: string }): Promise<SiteList> {
try {
const url = `${await this.discoveryApi.getBaseUrl(
'azure-functions',
)}/list/${functionName}`;
const url = `${await this.discoveryApi.getBaseUrl('azure')}/list/${name}`;
const { token: accessToken } = await this.identityApi.getCredentials();
const response = await fetch(url, {
method: 'GET',
@@ -14,7 +14,5 @@
* limitations under the License.
*/
export {
AzureWebManagementApi,
AzureFunctionsConfig,
} from './AzureWebManagementApi';
export * from './AzureSitesApi';
export * from './AzureSitesApiBackendClient';
@@ -16,9 +16,9 @@
import React from 'react';
import { Entity } from '@backstage/catalog-model';
import { useFunctions } from '../../hooks/useFunctions';
import { useSites } from '../../hooks/useSites';
import {
AZURE_FUNCTIONS_ANNOTATION,
AZURE_RESOURCE_NAME_ANNOTATION,
useServiceEntityAnnotations,
} from '../../hooks/useServiceEntityAnnotations';
import {
@@ -27,48 +27,50 @@ import {
ResponseErrorPanel,
} from '@backstage/core-components';
import { useEntity } from '@backstage/plugin-catalog-react';
import { OverviewTable } from '../OverviewTableComponent/OverviewTable';
import { AzureSitesOverviewTable } from '../AzureSitesOverviewTableComponent/AzureSitesOverviewTable';
/** @public */
export const isAzureFunctionsAvailable = (entity: Entity) =>
entity?.metadata.annotations?.[AZURE_FUNCTIONS_ANNOTATION];
export const isAzureResourceNameAvailable = (entity: Entity) =>
entity?.metadata.annotations?.[AZURE_RESOURCE_NAME_ANNOTATION];
const AzureFunctionsOverview = ({ entity }: { entity: Entity }) => {
const { functionsName } = useServiceEntityAnnotations(entity);
const AzureSitesOverview = ({ entity }: { entity: Entity }) => {
const { resourceName } = useServiceEntityAnnotations(entity);
const [functionsData] = useFunctions({
functionsName,
const [sites] = useSites({
name: resourceName,
});
if (functionsData.error) {
if (sites.error) {
return (
<div>
<ResponseErrorPanel error={functionsData.error} />
<ResponseErrorPanel error={sites.error} />
</div>
);
}
return (
<OverviewTable
data={functionsData.data?.items ?? []}
loading={functionsData.loading}
<AzureSitesOverviewTable
data={sites.data?.items ?? []}
loading={sites.loading}
/>
);
};
/** @public */
export const AzureFunctionsOverviewWidget = () => {
export const AzureSitesOverviewWidget = () => {
const { entity } = useEntity();
if (!isAzureFunctionsAvailable(entity)) {
if (!isAzureResourceNameAvailable(entity)) {
return (
<MissingAnnotationEmptyState annotation={AZURE_FUNCTIONS_ANNOTATION} />
<MissingAnnotationEmptyState
annotation={AZURE_RESOURCE_NAME_ANNOTATION}
/>
);
}
return (
<ErrorBoundary>
<AzureFunctionsOverview entity={entity} />
<AzureSitesOverview entity={entity} />
</ErrorBoundary>
);
};
@@ -29,9 +29,9 @@ import {
} from '@backstage/test-utils';
import { setupServer } from 'msw/node';
import { DateTime } from 'luxon';
import { functionResponseMock } from '../../mocks/mocks';
import { azureFunctionsApiRef } from '../..';
import { OverviewTable } from './OverviewTable';
import { siteMock } from '../../mocks/mocks';
import { azureSiteApiRef } from '../..';
import { AzureSitesOverviewTable } from './AzureSitesOverviewTable';
const errorApiMock = { post: jest.fn(), error$: jest.fn() };
const identityApiMock = (getCredentials: any) => ({
@@ -40,7 +40,7 @@ const identityApiMock = (getCredentials: any) => ({
getBackstageIdentity: jest.fn(),
getCredentials,
});
const azureFunctionsApiMock = {};
const azureSitesApiMock = {};
const config = {
getString: (_: string) => 'https://test-url',
@@ -49,18 +49,18 @@ const config = {
const apis: [AnyApiRef, Partial<unknown>][] = [
[errorApiRef, errorApiMock],
[configApiRef, config],
[azureFunctionsApiRef, azureFunctionsApiMock],
[azureSiteApiRef, azureSitesApiMock],
[identityApiRef, identityApiMock],
];
describe('AzureFunctionsOverviewWidget', () => {
describe('AzureSitesOverviewWidget', () => {
const worker = setupServer();
setupRequestMockHandlers(worker);
beforeEach(() => {
worker.use(
rest.get('/list/func-mock', (_, res, ctx) => {
res(ctx.json(functionResponseMock));
res(ctx.json(siteMock));
}),
);
});
@@ -68,21 +68,18 @@ describe('AzureFunctionsOverviewWidget', () => {
it('should display an overview table with the data from the requests', async () => {
const rendered = render(
<TestApiProvider apis={apis}>
<OverviewTable data={[functionResponseMock]} loading={false} />
<AzureSitesOverviewTable data={[siteMock]} loading={false} />
</TestApiProvider>,
);
expect(
await rendered.findByText(functionResponseMock.functionName),
).toBeInTheDocument();
expect(
await rendered.findByText(functionResponseMock.state),
).toBeInTheDocument();
expect(await rendered.findByText(siteMock.name)).toBeInTheDocument();
expect(await rendered.findByText(siteMock.kind)).toBeInTheDocument();
expect(await rendered.findByText(siteMock.state)).toBeInTheDocument();
expect(
await rendered.findByText(
DateTime.fromISO(
functionResponseMock.lastModifiedDate.toString(),
).toLocaleString(DateTime.DATETIME_MED),
DateTime.fromISO(siteMock.lastModifiedDate).toLocaleString(
DateTime.DATETIME_MED,
),
),
).toBeInTheDocument();
});
@@ -16,7 +16,7 @@
import React from 'react';
import { Box, Card, Link, LinearProgress } from '@material-ui/core';
import { FunctionsData } from '@backstage/plugin-azure-functions-common';
import { Site } from '@backstage/plugin-azure-common';
import { Table, TableColumn } from '@backstage/core-components';
import FlashOnIcon from '@material-ui/icons/FlashOn';
import { DateTime } from 'luxon';
@@ -47,42 +47,46 @@ const State = ({ value }: { value: States }) => {
);
};
type FunctionTableProps = {
data: FunctionsData[];
type TableProps = {
data: Site[];
loading: boolean;
};
const DEFAULT_COLUMNS: TableColumn<FunctionsData>[] = [
const DEFAULT_COLUMNS: TableColumn<Site>[] = [
{
title: 'name',
highlight: true,
render: (func: FunctionsData) => {
render: (func: Site) => {
return (
<Link href={func.href} target="_blank">
{func.functionName}
{func.name}
</Link>
);
},
},
{
title: 'kind',
render: (func: Site) => func.kind ?? 'unknown',
},
{
title: 'location',
render: (func: FunctionsData) => func.location ?? 'unknown',
render: (func: Site) => func.location ?? 'unknown',
},
{
title: 'status',
render: (func: FunctionsData) => <State value={func.state as States} />,
render: (func: Site) => <State value={func.state as States} />,
},
{
title: 'last modified',
render: (func: FunctionsData) =>
DateTime.fromISO(func.lastModifiedDate.toString()).toLocaleString(
render: (func: Site) =>
DateTime.fromISO(func.lastModifiedDate).toLocaleString(
DateTime.DATETIME_MED,
),
},
{
title: 'logs',
align: 'right',
render: (func: FunctionsData) => {
render: (func: Site) => {
return (
<Link href={func.logstreamHref} target="_blank">
View Logs
@@ -93,8 +97,8 @@ const DEFAULT_COLUMNS: TableColumn<FunctionsData>[] = [
];
/** @public */
export const OverviewTable = ({ data, loading }: FunctionTableProps) => {
const columns: TableColumn<FunctionsData>[] = [...DEFAULT_COLUMNS];
export const AzureSitesOverviewTable = ({ data, loading }: TableProps) => {
const columns: TableColumn<Site>[] = [...DEFAULT_COLUMNS];
const tableStyle = {
minWidth: '0',
width: '100%',
@@ -107,7 +111,7 @@ export const OverviewTable = ({ data, loading }: FunctionTableProps) => {
<Box display="flex" alignItems="center">
<FlashOnIcon style={{ fontSize: 30 }} />
<Box mr={1} />
Azure Functions
Azure Sites
</Box>
}
options={{ paging: true, search: false, pageSize: 10 }}
@@ -16,12 +16,12 @@
import { Entity } from '@backstage/catalog-model';
export const AZURE_FUNCTIONS_ANNOTATION = 'portal.azure.com/functions-name';
export const AZURE_RESOURCE_NAME_ANNOTATION = 'portal.azure.com/resource-name';
export const useServiceEntityAnnotations = (entity: Entity) => {
const functionsName =
entity?.metadata.annotations?.[AZURE_FUNCTIONS_ANNOTATION] ?? '';
const resourceName =
entity?.metadata.annotations?.[AZURE_RESOURCE_NAME_ANNOTATION] ?? '';
return {
functionsName,
resourceName,
};
};
@@ -16,35 +16,35 @@
import useAsyncRetry from 'react-use/lib/useAsyncRetry';
import { useApi, errorApiRef } from '@backstage/core-plugin-api';
import { FunctionsListResponse } from '@backstage/plugin-azure-functions-common';
import { azureFunctionsApiRef } from '../api';
import { SiteList } from '@backstage/plugin-azure-common';
import { azureSiteApiRef } from '../api';
import { useCallback } from 'react';
export function useFunctions({ functionsName }: { functionsName: string }) {
const azureFunctionsApi = useApi(azureFunctionsApiRef);
export function useSites({ name }: { name: string }) {
const azureApi = useApi(azureSiteApiRef);
const errorApi = useApi(errorApiRef);
const list = useCallback(
async () => {
return await azureFunctionsApi.list({
functionName: functionsName,
return await azureApi.list({
name: name,
});
},
[functionsName], // eslint-disable-line react-hooks/exhaustive-deps
[name], // eslint-disable-line react-hooks/exhaustive-deps
);
const {
loading,
value: data,
error,
retry,
} = useAsyncRetry<FunctionsListResponse | null>(async () => {
} = useAsyncRetry<SiteList | null>(async () => {
try {
const azureFunction = await list();
return azureFunction;
const sites = await list();
return sites;
} catch (e) {
if (e instanceof Error) {
if (e?.message === 'MissingAzureBackendException') {
errorApi.post(new Error('Please add azure-functions-backend plugin'));
errorApi.post(new Error('Please add azure-backend plugin'));
return null;
}
errorApi.post(e);
@@ -16,4 +16,4 @@
export * from './plugin';
export * from './api';
export * from './components/AzureFunctionsOverviewComponent/AzureFunctionsOverview';
export * from './components/AzureSitesOverviewComponent/AzureSitesOverview';
@@ -14,15 +14,15 @@
* limitations under the License.
*/
import { FunctionsData } from '@backstage/plugin-azure-functions-common';
import { Site } from '@backstage/plugin-azure-common';
export const entityMock = {
metadata: {
namespace: 'default',
annotations: {
'portal.azure.com/functions-name': 'func-mock',
'portal.azure.com/resource-name': 'func-mock',
},
name: 'sample-azure-function-service',
name: 'sample-azure-service',
description: 'A service for testing Backstage functionality.',
uid: 'c009b513-d053-4b3f-9429-8433a145e943',
},
@@ -36,14 +36,19 @@ 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',
export const siteMock: Site = {
name: 'func-mock',
kind: 'functionapp',
resourceGroup: 'mock-resourcegroup',
tags: {
isMock: true,
},
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'),
lastModifiedDate: '2022-09-02T11:09:58.9033333',
containerSize: 100,
};
@@ -14,10 +14,10 @@
* limitations under the License.
*/
import { azureFunctionsPlugin } from './plugin';
import { azureSitesPlugin } from './plugin';
describe('azure-functions', () => {
describe('azure', () => {
it('should export plugin', () => {
expect(azureFunctionsPlugin).toBeDefined();
expect(azureSitesPlugin).toBeDefined();
});
});
@@ -22,25 +22,25 @@ import {
discoveryApiRef,
identityApiRef,
} from '@backstage/core-plugin-api';
import { azureFunctionsApiRef, AzureFunctionsBackendClient } from './api';
import { azureSiteApiRef, AzureSitesApiBackendClient } from './api';
/** @public */
export const entityContentRouteRef = createRouteRef({
id: 'Azure Functions Entity Content',
id: 'Azure Entity Content',
});
/** @public */
export const azureFunctionsPlugin = createPlugin({
id: 'azure-functions',
export const azureSitesPlugin = createPlugin({
id: 'azure',
apis: [
createApiFactory({
api: azureFunctionsApiRef,
api: azureSiteApiRef,
deps: {
discoveryApi: discoveryApiRef,
identityApi: identityApiRef,
},
factory: ({ discoveryApi, identityApi }) =>
new AzureFunctionsBackendClient({ discoveryApi, identityApi }),
new AzureSitesApiBackendClient({ discoveryApi, identityApi }),
}),
],
routes: {
@@ -49,14 +49,14 @@ export const azureFunctionsPlugin = createPlugin({
});
/** @public */
export const EntityAzureFunctionsOverviewCard = azureFunctionsPlugin.provide(
export const EntityAzureSitesOverviewWidget = azureSitesPlugin.provide(
createComponentExtension({
name: 'EntityAzureFunctionsOverviewCard',
name: 'EntityAzureSitesOverviewWidget',
component: {
lazy: () =>
import(
'./components/AzureFunctionsOverviewComponent/AzureFunctionsOverview'
).then(m => m.AzureFunctionsOverviewWidget),
'./components/AzureSitesOverviewComponent/AzureSitesOverview'
).then(m => m.AzureSitesOverviewWidget),
},
}),
);
@@ -17,5 +17,5 @@
import { createRouteRef } from '@backstage/core-plugin-api';
export const rootRouteRef = createRouteRef({
id: 'azure-functions',
id: 'azure',
});
+3551 -5583
View File
File diff suppressed because it is too large Load Diff