diff --git a/.changeset/tasty-colts-hug.md b/.changeset/tasty-colts-hug.md new file mode 100644 index 0000000000..e56d48a441 --- /dev/null +++ b/.changeset/tasty-colts-hug.md @@ -0,0 +1,7 @@ +--- +'@backstage/plugin-azure-sites': minor +'@backstage/plugin-azure-sites-backend': minor +'@backstage/plugin-azure-sites-common': minor +--- + +Azure Sites (Apps & Functions) support for a given entity. View the current status of the site, quickly jump to site's Overview page, or Log Stream page. diff --git a/microsite/data/plugins/azure-sites.yaml b/microsite/data/plugins/azure-sites.yaml new file mode 100644 index 0000000000..3ea9d0439b --- /dev/null +++ b/microsite/data/plugins/azure-sites.yaml @@ -0,0 +1,8 @@ +--- +title: Azure Sites +author: FRISS +authorUrl: https://friss.com +category: Infrastructure +description: Azure Sites (Apps & Functions) support for a given entity. View the current status of the site, quickly jump to site's Overview page, or Log Stream page. +documentation: https://github.com/backstage/backstage/tree/master/plugins/azure-sites +npmPackageName: '@backstage/plugin-azure-sites' diff --git a/packages/app/package.json b/packages/app/package.json index 626848a60e..441bdd1138 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -19,6 +19,7 @@ "@backstage/plugin-apache-airflow": "workspace:^", "@backstage/plugin-api-docs": "workspace:^", "@backstage/plugin-azure-devops": "workspace:^", + "@backstage/plugin-azure-sites": "workspace:^", "@backstage/plugin-badges": "workspace:^", "@backstage/plugin-catalog-common": "workspace:^", "@backstage/plugin-catalog-graph": "workspace:^", diff --git a/packages/backend/package.json b/packages/backend/package.json index 5cd4c1318e..4e296616fc 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -36,6 +36,7 @@ "@backstage/plugin-auth-backend": "workspace:^", "@backstage/plugin-auth-node": "workspace:^", "@backstage/plugin-azure-devops-backend": "workspace:^", + "@backstage/plugin-azure-sites-backend": "workspace:^", "@backstage/plugin-badges-backend": "workspace:^", "@backstage/plugin-catalog-backend": "workspace:^", "@backstage/plugin-code-coverage-backend": "workspace:^", diff --git a/plugins/azure-sites-backend/.eslintrc.js b/plugins/azure-sites-backend/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/azure-sites-backend/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/azure-sites-backend/README.md b/plugins/azure-sites-backend/README.md new file mode 100644 index 0000000000..5f19267499 --- /dev/null +++ b/plugins/azure-sites-backend/README.md @@ -0,0 +1,85 @@ +# Azure Sites Backend + +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/)_ + +## Setup + +The following sections will help you get the Azure Sites Backend plugin setup and running. + +### Configuration + +The Azure plugin requires the following YAML to be added to your app-config.yaml: + +```yaml +azureSites: + domain: + tenantId: + clientId: + clientSecret: + allowedSubscriptions: + - id: +``` + +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/#@/`. +- `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. +- (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-sites-backend` package to your backend: + + ```sh + # From the Backstage root directory + yarn add --cwd packages/backend @backstage/plugin-azure-sites-backend + ``` + +2. Then we will create a new file named `packages/backend/src/plugins/azure.ts`, and add the following to it: + + ```ts + import { + createRouter, + AzureSitesApi, + } from '@backstage/plugin-azure-sites-backend'; + import { Router } from 'express'; + import { PluginEnvironment } from '../types'; + + export default async function createPlugin( + env: PluginEnvironment, + ): Promise { + return await createRouter({ + logger: env.logger, + azureSitesApi: AzureSitesApi.fromConfig(env.config), + }); + } + ``` + +3. Next we wire this into the overall backend router, edit `packages/backend/src/index.ts`: + + ```ts + import azure from './plugins/azure'; + + // Removed for clairty... + + async function main() { + // ... + // Add this line under the other lines that follow the useHotMemoize pattern + const azureSitesEnv = useHotMemoize(module, () => + createEnv('azure-sites'), + ); + + // ... + // Insert this line under the other lines that add their routers to apiRouter in the same way + apiRouter.use('/azure-sites', await azure(azureSitesEnv)); + } + ``` + +4. Now run `yarn start-backend` from the repo root. + +5. Finally, open `http://localhost:7007/api/azure/health` in a browser, it should return `{"status":"ok"}`. diff --git a/plugins/azure-sites-backend/api-report.md b/plugins/azure-sites-backend/api-report.md new file mode 100644 index 0000000000..751ff3f033 --- /dev/null +++ b/plugins/azure-sites-backend/api-report.md @@ -0,0 +1,61 @@ +## API Report File for "@backstage/plugin-azure-sites-backend" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { AzureSiteListRequest } from '@backstage/plugin-azure-sites-common'; +import { AzureSiteListResponse } from '@backstage/plugin-azure-sites-common'; +import { AzureSiteStartStopRequest } from '@backstage/plugin-azure-sites-common'; +import { Config } from '@backstage/config'; +import express from 'express'; +import { Logger } from 'winston'; + +// @public (undocumented) +export class AzureSitesApi { + constructor(config: AzureSitesConfig); + // (undocumented) + static fromConfig(config: Config): AzureSitesApi; + // (undocumented) + list(request: AzureSiteListRequest): Promise; + // (undocumented) + start(request: AzureSiteStartStopRequest): Promise; + // (undocumented) + stop(request: AzureSiteStartStopRequest): Promise; +} + +// @public (undocumented) +export class AzureSitesConfig { + constructor( + domain: string, + tenantId: string, + subscriptions?: string[] | undefined, + clientId?: string | undefined, + clientSecret?: string | undefined, + ); + // (undocumented) + readonly clientId?: string | undefined; + // (undocumented) + readonly clientSecret?: string | undefined; + // (undocumented) + readonly domain: string; + // (undocumented) + static fromConfig(config: Config): AzureSitesConfig; + // (undocumented) + readonly subscriptions?: string[] | undefined; + // (undocumented) + readonly tenantId: string; +} + +// @public (undocumented) +export function createRouter(options: RouterOptions): Promise; + +// @public (undocumented) +export interface RouterOptions { + // (undocumented) + azureSitesApi: AzureSitesApi; + // (undocumented) + logger: Logger; +} + +// (No @packageDocumentation comment for this package) +``` diff --git a/plugins/azure-sites-backend/package.json b/plugins/azure-sites-backend/package.json new file mode 100644 index 0000000000..6930f36587 --- /dev/null +++ b/plugins/azure-sites-backend/package.json @@ -0,0 +1,56 @@ +{ + "name": "@backstage/plugin-azure-sites-backend", + "version": "0.0.0", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "publishConfig": { + "access": "public", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" + }, + "backstage": { + "role": "backend-plugin" + }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/azure-sites-backend" + }, + "keywords": [ + "backstage", + "azure" + ], + "scripts": { + "start": "backstage-cli package start", + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "clean": "backstage-cli package clean", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack" + }, + "dependencies": { + "@azure/arm-appservice": "^13.0.1", + "@azure/arm-resourcegraph": "^4.2.1", + "@azure/identity": "^2.1.0", + "@backstage/backend-common": "workspace:^", + "@backstage/config": "workspace:^", + "@backstage/plugin-azure-sites-common": "workspace:^", + "@types/express": "^4.17.6", + "express": "^4.17.1", + "express-promise-router": "^4.1.0", + "node-fetch": "^2.6.7", + "winston": "^3.2.1", + "yn": "^4.0.0" + }, + "devDependencies": { + "@backstage/cli": "workspace:^", + "@types/supertest": "^2.0.8", + "msw": "^0.47.0" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/azure-sites-backend/schema.d.ts b/plugins/azure-sites-backend/schema.d.ts new file mode 100644 index 0000000000..19a12d11a1 --- /dev/null +++ b/plugins/azure-sites-backend/schema.d.ts @@ -0,0 +1,34 @@ +/* + * 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. + */ + +export interface Config { + azureSites: { + /** @visibility backend */ + domain: string; + /** @visibility backend */ + tenantId: string; + /** @visibility backend */ + subscriptions?: [ + { + id: string; + }, + ]; + /** @visibility backend */ + clientId?: string; + /** @visibility secret */ + clientSecret?: string; + }; +} diff --git a/plugins/azure-sites-backend/src/api/AzureSitesApi.ts b/plugins/azure-sites-backend/src/api/AzureSitesApi.ts new file mode 100644 index 0000000000..73e92ea6a0 --- /dev/null +++ b/plugins/azure-sites-backend/src/api/AzureSitesApi.ts @@ -0,0 +1,98 @@ +/* + * 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'; +import { + DefaultAzureCredential, + ClientSecretCredential, +} from '@azure/identity'; +import { ResourceGraphClient } from '@azure/arm-resourcegraph'; +import { WebSiteManagementClient } from '@azure/arm-appservice'; +import { + AzureSite, + AzureSiteListRequest, + AzureSiteListResponse, + AzureSiteStartStopRequest, +} from '@backstage/plugin-azure-sites-common'; +import { AzureSitesConfig } from '../config'; + +/** @public */ +export class AzureSitesApi { + private readonly baseHref = (domain: string) => + `https://portal.azure.com/#@${domain}/resource`; + private readonly client: ResourceGraphClient; + + constructor(private readonly config: AzureSitesConfig) { + const creds = this.getCredentials(config); + + this.client = new ResourceGraphClient(creds); + } + + private getCredentials(config: AzureSitesConfig) { + return config.clientId && config.clientSecret + ? new ClientSecretCredential( + config.tenantId, + config.clientId, + config.clientSecret, + ) + : new DefaultAzureCredential({ tenantId: config.tenantId }); + } + + static fromConfig(config: Config): AzureSitesApi { + return new AzureSitesApi(AzureSitesConfig.fromConfig(config)); + } + + async start(request: AzureSiteStartStopRequest): Promise { + const client = new WebSiteManagementClient( + this.getCredentials(this.config), + request.subscription, + ); + await client.webApps.start(request.resourceGroup, request.name); + } + + async stop(request: AzureSiteStartStopRequest): Promise { + const client = new WebSiteManagementClient( + this.getCredentials(this.config), + request.subscription, + ); + await client.webApps.stop(request.resourceGroup, request.name); + } + + async list(request: AzureSiteListRequest): Promise { + const items: AzureSite[] = []; + const result = await this.client.resources({ + query: `resources | where type == 'microsoft.web/sites' | where name contains '${request.name}'`, + subscriptions: this.config.subscriptions, + }); + for (const v of result.data) { + items.push({ + href: `${this.baseHref(this.config.domain)}${v.id!}`, + logstreamHref: `${this.baseHref(this.config.domain)}${v.id!}/logStream`, + name: v.name!, + kind: v.kind!, + resourceGroup: v.resourceGroup!, + subscription: v.id!.match(/\w{8}\-\w{4}\-\w{4}\-\w{4}\-\w{12}/)[0], + location: v.location!, + lastModifiedDate: v.properties.lastModifiedTimeUtc!, + usageState: v.properties.usageState!, + state: v.properties.state!, + containerSize: v.properties.containerSize!, + tags: v.properties.tags!, + }); + } + return { items: items }; + } +} diff --git a/plugins/azure-sites-backend/src/api/index.ts b/plugins/azure-sites-backend/src/api/index.ts new file mode 100644 index 0000000000..187dc1314b --- /dev/null +++ b/plugins/azure-sites-backend/src/api/index.ts @@ -0,0 +1,17 @@ +/* + * 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. + */ + +export { AzureSitesApi } from './AzureSitesApi'; diff --git a/plugins/azure-sites-backend/src/config.ts b/plugins/azure-sites-backend/src/config.ts new file mode 100644 index 0000000000..3475175cb5 --- /dev/null +++ b/plugins/azure-sites-backend/src/config.ts @@ -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 AzureSitesConfig { + 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): AzureSitesConfig { + const azConfig = config.getConfig('azureSites'); + + return new AzureSitesConfig( + azConfig.getString('domain'), + azConfig.getString('tenantId'), + azConfig + .getOptionalConfigArray('subscriptions') + ?.map(as => as.getString('id')), + azConfig.getOptionalString('clientId'), + azConfig.getOptionalString('clientSecret'), + ); + } +} diff --git a/plugins/azure-sites-backend/src/index.ts b/plugins/azure-sites-backend/src/index.ts new file mode 100644 index 0000000000..97f94e4470 --- /dev/null +++ b/plugins/azure-sites-backend/src/index.ts @@ -0,0 +1,19 @@ +/* + * 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. + */ + +export * from './service/router'; +export * from './api'; +export * from './config'; diff --git a/plugins/azure-sites-backend/src/run.ts b/plugins/azure-sites-backend/src/run.ts new file mode 100644 index 0000000000..d945aa13f0 --- /dev/null +++ b/plugins/azure-sites-backend/src/run.ts @@ -0,0 +1,33 @@ +/* + * 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 { getRootLogger } from '@backstage/backend-common'; +import yn from 'yn'; +import { startStandaloneServer } from './service/standaloneServer'; + +const port = process.env.PLUGIN_PORT ? Number(process.env.PLUGIN_PORT) : 7007; +const enableCors = yn(process.env.PLUGIN_CORS, { default: false }); +const logger = getRootLogger(); + +startStandaloneServer({ port, enableCors, logger }).catch(err => { + logger.error(err); + process.exit(1); +}); + +process.on('SIGINT', () => { + logger.info('CTRL+C pressed; exiting.'); + process.exit(0); +}); diff --git a/plugins/azure-sites-backend/src/service/router.ts b/plugins/azure-sites-backend/src/service/router.ts new file mode 100644 index 0000000000..bea9a15396 --- /dev/null +++ b/plugins/azure-sites-backend/src/service/router.ts @@ -0,0 +1,81 @@ +/* + * 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 { errorHandler } from '@backstage/backend-common'; +import express from 'express'; +import Router from 'express-promise-router'; +import { Logger } from 'winston'; + +import { AzureSitesApi } from '../api'; + +/** @public */ +export interface RouterOptions { + logger: Logger; + azureSitesApi: AzureSitesApi; +} + +/** @public */ +export async function createRouter( + options: RouterOptions, +): Promise { + const { logger, azureSitesApi } = options; + + const router = Router(); + router.use(express.json()); + + router.get('/health', (_, response) => { + logger.info('PONG!'); + response.send({ status: 'ok' }); + }); + router.get('/list/:name', async (request, response) => { + const { name } = request.params; + response.json( + await azureSitesApi.list({ + name: name, + }), + ); + }); + router.post( + '/:subscription/:resourceGroup/:name/start', + async (request, response) => { + const { subscription, resourceGroup, name } = request.params; + console.log('starting...'); + response.json( + await azureSitesApi.start({ + subscription, + resourceGroup, + name, + }), + ); + }, + ); + router.post( + '/:subscription/:resourceGroup/:name/stop', + async (request, response) => { + const { subscription, resourceGroup, name } = request.params; + console.log('stopping...'); + response.json( + await azureSitesApi.stop({ + subscription, + resourceGroup, + name, + }), + ); + }, + ); + router.use(errorHandler()); + return router; +} diff --git a/plugins/azure-sites-backend/src/service/standaloneServer.ts b/plugins/azure-sites-backend/src/service/standaloneServer.ts new file mode 100644 index 0000000000..1998bee023 --- /dev/null +++ b/plugins/azure-sites-backend/src/service/standaloneServer.ts @@ -0,0 +1,56 @@ +/* + * 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 { + createServiceBuilder, + loadBackendConfig, +} from '@backstage/backend-common'; +import { Server } from 'http'; +import { Logger } from 'winston'; +import { AzureSitesApi } from '../api'; +import { createRouter } from './router'; + +export interface ServerOptions { + port: number; + enableCors: boolean; + logger: Logger; +} + +export async function startStandaloneServer( + options: ServerOptions, +): Promise { + 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, + azureSitesApi: AzureSitesApi.fromConfig(config), + }); + + let service = createServiceBuilder(module) + .setPort(options.port) + .addRouter('/azure-backend', router); + if (options.enableCors) { + service = service.enableCors({ origin: 'http://localhost:3000' }); + } + + return await service.start().catch(err => { + logger.error(err); + process.exit(1); + }); +} + +module.hot?.accept(); diff --git a/plugins/azure-sites-backend/src/setupTests.ts b/plugins/azure-sites-backend/src/setupTests.ts new file mode 100644 index 0000000000..813cdeaae3 --- /dev/null +++ b/plugins/azure-sites-backend/src/setupTests.ts @@ -0,0 +1,17 @@ +/* + * 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. + */ + +export {}; diff --git a/plugins/azure-sites-common/.eslintrc.js b/plugins/azure-sites-common/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/azure-sites-common/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/azure-sites-common/api-report.md b/plugins/azure-sites-common/api-report.md new file mode 100644 index 0000000000..64b4275085 --- /dev/null +++ b/plugins/azure-sites-common/api-report.md @@ -0,0 +1,40 @@ +## API Report File for "@backstage/plugin-azure-sites-common" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +// @public (undocumented) +export type AzureSite = { + href: string; + logstreamHref: string; + name: string; + kind: string; + location: string; + resourceGroup: string; + subscription: string; + state: string; + usageState: string; + containerSize: number; + lastModifiedDate: string; + tags: {}; +}; + +// @public (undocumented) +export type AzureSiteListRequest = { + name: string; +}; + +// @public (undocumented) +export type AzureSiteListResponse = { + items: AzureSite[]; +}; + +// @public (undocumented) +export type AzureSiteStartStopRequest = { + subscription: string; + resourceGroup: string; + name: string; +}; + +// (No @packageDocumentation comment for this package) +``` diff --git a/plugins/azure-sites-common/package.json b/plugins/azure-sites-common/package.json new file mode 100644 index 0000000000..eb0093bf65 --- /dev/null +++ b/plugins/azure-sites-common/package.json @@ -0,0 +1,40 @@ +{ + "name": "@backstage/plugin-azure-sites-common", + "description": "Common functionalities for the azure plugin", + "version": "0.0.0", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "publishConfig": { + "access": "public", + "main": "dist/index.cjs.js", + "module": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, + "backstage": { + "role": "common-library" + }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/azure-sites-common" + }, + "keywords": [ + "backstage" + ], + "scripts": { + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "clean": "backstage-cli package clean", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack" + }, + "devDependencies": { + "@backstage/cli": "workspace:^" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/azure-sites-common/src/index.ts b/plugins/azure-sites-common/src/index.ts new file mode 100644 index 0000000000..db229eae34 --- /dev/null +++ b/plugins/azure-sites-common/src/index.ts @@ -0,0 +1,17 @@ +/* + * 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. + */ + +export * from './types'; diff --git a/plugins/azure-sites-common/src/types.ts b/plugins/azure-sites-common/src/types.ts new file mode 100644 index 0000000000..7c8be7ce24 --- /dev/null +++ b/plugins/azure-sites-common/src/types.ts @@ -0,0 +1,48 @@ +/* + * 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. + */ + +/** @public */ +export type AzureSite = { + href: string; + logstreamHref: string; + name: string; + kind: string; + location: string; + resourceGroup: string; + subscription: string; + state: string; + usageState: string; + containerSize: number; + lastModifiedDate: string; + tags: {}; +}; + +/** @public */ +export type AzureSiteListRequest = { + name: string; +}; + +/** @public */ +export type AzureSiteListResponse = { + items: AzureSite[]; +}; + +/** @public */ +export type AzureSiteStartStopRequest = { + subscription: string; + resourceGroup: string; + name: string; +}; diff --git a/plugins/azure-sites/.eslintrc.js b/plugins/azure-sites/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/azure-sites/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/azure-sites/README.md b/plugins/azure-sites/README.md new file mode 100644 index 0000000000..f240dab140 --- /dev/null +++ b/plugins/azure-sites/README.md @@ -0,0 +1,70 @@ +# Azure Sites Plugin + +![preview of Azure table](docs/functions-table.png) + +_Inspired by [roadie.io AWS Lamda plugin](https://roadie.io/backstage/plugins/aws-lambda/)_ + +## Features + +- Azure overview table + +## Plugin Setup + +The following sections will help you get the Azure plugin setup and running + +### Azure Sites Backend + +You need to set up the Azure Sites 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 Sites plugin you need to add the following annotation to any entities you want to use it with: + +```yaml +azure.com/microsoft-web-sites: +``` + +`` supports case-insensitive exact / partial value. + +Example of Partial Matching: + +Let's say you have a number of functions apps, spread out over different regions (and possibly different subscriptions), and they follow a naming convention: + +``` +func-testapp-eu +func-testapp-ca +func-testapp-us +``` + +The annotation you will use to have the three functions' app appear in the overview table would look like this: + +```yaml +azure.com/microsoft-web-sites: func-testapp +``` + +### Install the component + +1. Install the plugin in the `packages/app` directory + +```sh +yarn add @backstage/plugin-azure-sites +``` + +2. Add widget component to your Backstage instance: + +```ts +// In packages/app/src/components/catalog/EntityPage.tsx +import { EntityAzureSitesOverviewWidget, isAzureWebSiteNameAvailable } from '@backstage/plugin-azure-sites'; + +... + +const serviceEntityPage = ( + + //... + Boolean(isAzureWebSiteNameAvailable(e))} path="/azure" title="Azure"> + + + //... + +); +``` diff --git a/plugins/azure-sites/api-report.md b/plugins/azure-sites/api-report.md new file mode 100644 index 0000000000..38ca6a8fe2 --- /dev/null +++ b/plugins/azure-sites/api-report.md @@ -0,0 +1,63 @@ +## API Report File for "@backstage/plugin-azure-sites" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +/// + +import { ApiRef } from '@backstage/core-plugin-api'; +import { AzureSiteListRequest } from '@backstage/plugin-azure-sites-common'; +import { AzureSiteListResponse } from '@backstage/plugin-azure-sites-common'; +import { AzureSiteStartStopRequest } from '@backstage/plugin-azure-sites-common'; +import { BackstagePlugin } from '@backstage/core-plugin-api'; +import { DiscoveryApi } from '@backstage/core-plugin-api'; +import { Entity } from '@backstage/catalog-model'; +import { IdentityApi } from '@backstage/core-plugin-api'; +import { RouteRef } from '@backstage/core-plugin-api'; + +// @public (undocumented) +export const azureSiteApiRef: ApiRef; + +// @public (undocumented) +export type AzureSitesApi = { + list: (request: AzureSiteListRequest) => Promise; + start: (request: AzureSiteStartStopRequest) => Promise; + stop: (request: AzureSiteStartStopRequest) => Promise; +}; + +// @public (undocumented) +export class AzureSitesApiBackendClient implements AzureSitesApi { + constructor(options: { + discoveryApi: DiscoveryApi; + identityApi: IdentityApi; + }); + // (undocumented) + list(request: AzureSiteListRequest): Promise; + // (undocumented) + start(request: AzureSiteStartStopRequest): Promise; + // (undocumented) + stop(request: AzureSiteStartStopRequest): Promise; +} + +// @public (undocumented) +export const AzureSitesOverviewWidget: () => JSX.Element; + +// @public (undocumented) +export const azureSitesPlugin: BackstagePlugin< + { + entityContent: RouteRef; + }, + {}, + {} +>; + +// @public (undocumented) +export const EntityAzureSitesOverviewWidget: () => JSX.Element; + +// @public (undocumented) +export const isAzureWebSiteNameAvailable: ( + entity: Entity, +) => string | undefined; + +// (No @packageDocumentation comment for this package) +``` diff --git a/plugins/azure-sites/dev/index.tsx b/plugins/azure-sites/dev/index.tsx new file mode 100644 index 0000000000..02f8ed99cd --- /dev/null +++ b/plugins/azure-sites/dev/index.tsx @@ -0,0 +1,20 @@ +/* + * 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 { createDevApp } from '@backstage/dev-utils'; +import { azureSitesPlugin } from '../src/plugin'; + +createDevApp().registerPlugin(azureSitesPlugin).render(); diff --git a/plugins/azure-sites/docs/functions-table.png b/plugins/azure-sites/docs/functions-table.png new file mode 100644 index 0000000000..d8603e8fa7 Binary files /dev/null and b/plugins/azure-sites/docs/functions-table.png differ diff --git a/plugins/azure-sites/package.json b/plugins/azure-sites/package.json new file mode 100644 index 0000000000..2ffe0225e6 --- /dev/null +++ b/plugins/azure-sites/package.json @@ -0,0 +1,65 @@ +{ + "name": "@backstage/plugin-azure-sites", + "version": "0.0.0", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "publishConfig": { + "access": "public", + "main": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, + "backstage": { + "role": "frontend-plugin" + }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/azure-sites" + }, + "keywords": [ + "backstage", + "azure" + ], + "scripts": { + "start": "backstage-cli package start", + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "clean": "backstage-cli package clean", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack" + }, + "dependencies": { + "@backstage/catalog-model": "workspace:^", + "@backstage/core-components": "workspace:^", + "@backstage/core-plugin-api": "workspace:^", + "@backstage/plugin-azure-sites-common": "workspace:^", + "@backstage/plugin-catalog-react": "workspace:^", + "@backstage/theme": "workspace:^", + "@material-ui/core": "^4.12.2", + "@material-ui/icons": "^4.9.1", + "@material-ui/lab": "4.0.0-alpha.57", + "luxon": "^3.0.0", + "react-use": "^17.2.4" + }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0" + }, + "devDependencies": { + "@backstage/cli": "workspace:^", + "@backstage/core-app-api": "workspace:^", + "@backstage/dev-utils": "workspace:^", + "@backstage/test-utils": "workspace:^", + "@testing-library/jest-dom": "^5.10.1", + "@testing-library/react": "^12.1.3", + "@testing-library/user-event": "^14.0.0", + "@types/node": "^16.11.26", + "cross-fetch": "^3.1.5", + "msw": "^0.47.0" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/azure-sites/src/api/AzureSitesApi.ts b/plugins/azure-sites/src/api/AzureSitesApi.ts new file mode 100644 index 0000000000..87a3b46a8d --- /dev/null +++ b/plugins/azure-sites/src/api/AzureSitesApi.ts @@ -0,0 +1,34 @@ +/* + * 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 { createApiRef } from '@backstage/core-plugin-api'; +import { + AzureSiteListRequest, + AzureSiteListResponse, + AzureSiteStartStopRequest, +} from '@backstage/plugin-azure-sites-common'; + +/** @public */ +export const azureSiteApiRef = createApiRef({ + id: 'plugin.azure-sites.service', +}); + +/** @public */ +export type AzureSitesApi = { + list: (request: AzureSiteListRequest) => Promise; + start: (request: AzureSiteStartStopRequest) => Promise; + stop: (request: AzureSiteStartStopRequest) => Promise; +}; diff --git a/plugins/azure-sites/src/api/AzureSitesApiBackendClient.ts b/plugins/azure-sites/src/api/AzureSitesApiBackendClient.ts new file mode 100644 index 0000000000..fc622e786b --- /dev/null +++ b/plugins/azure-sites/src/api/AzureSitesApiBackendClient.ts @@ -0,0 +1,78 @@ +/* + * 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 { AzureSitesApi } from './AzureSitesApi'; +import { + AzureSiteListRequest, + AzureSiteListResponse, + AzureSiteStartStopRequest, +} from '@backstage/plugin-azure-sites-common'; +import { DiscoveryApi, IdentityApi } from '@backstage/core-plugin-api'; + +/** @public */ +export class AzureSitesApiBackendClient implements AzureSitesApi { + private readonly identityApi: IdentityApi; + private readonly discoveryApi: DiscoveryApi; + constructor(options: { + discoveryApi: DiscoveryApi; + identityApi: IdentityApi; + }) { + this.discoveryApi = options.discoveryApi; + this.identityApi = options.identityApi; + } + + async stop(request: AzureSiteStartStopRequest): Promise { + const url = `${await this.discoveryApi.getBaseUrl('azure-functions')}/${ + request.subscription + }/${request.resourceGroup}/${request.name}/stop`; + const { token: accessToken } = await this.identityApi.getCredentials(); + await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + ...(accessToken && { Authorization: `Bearer ${accessToken}` }), + }, + }); + } + async start(request: AzureSiteStartStopRequest): Promise { + const url = `${await this.discoveryApi.getBaseUrl('azure-functions')}/${ + request.subscription + }/${request.resourceGroup}/${request.name}/start`; + const { token: accessToken } = await this.identityApi.getCredentials(); + await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + ...(accessToken && { Authorization: `Bearer ${accessToken}` }), + }, + }); + } + + async list(request: AzureSiteListRequest): Promise { + const url = `${await this.discoveryApi.getBaseUrl('azure-sites')}/list/${ + request.name + }`; + const { token: accessToken } = await this.identityApi.getCredentials(); + const response = await fetch(url, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + ...(accessToken && { Authorization: `Bearer ${accessToken}` }), + }, + }); + return await response.json(); + } +} diff --git a/plugins/azure-sites/src/api/index.ts b/plugins/azure-sites/src/api/index.ts new file mode 100644 index 0000000000..2b6cc64221 --- /dev/null +++ b/plugins/azure-sites/src/api/index.ts @@ -0,0 +1,18 @@ +/* + * 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. + */ + +export * from './AzureSitesApi'; +export * from './AzureSitesApiBackendClient'; diff --git a/plugins/azure-sites/src/components/AzureSitesOverviewComponent/AzureSitesOverview.tsx b/plugins/azure-sites/src/components/AzureSitesOverviewComponent/AzureSitesOverview.tsx new file mode 100644 index 0000000000..755a42f5da --- /dev/null +++ b/plugins/azure-sites/src/components/AzureSitesOverviewComponent/AzureSitesOverview.tsx @@ -0,0 +1,76 @@ +/* + * 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 React from 'react'; +import { Entity } from '@backstage/catalog-model'; +import { useSites } from '../../hooks/useSites'; +import { + AZURE_WEB_SITE_NAME_ANNOTATION, + useServiceEntityAnnotations, +} from '../../hooks/useServiceEntityAnnotations'; +import { + ErrorBoundary, + MissingAnnotationEmptyState, + ResponseErrorPanel, +} from '@backstage/core-components'; +import { useEntity } from '@backstage/plugin-catalog-react'; +import { AzureSitesOverviewTable } from '../AzureSitesOverviewTableComponent/AzureSitesOverviewTable'; + +/** @public */ +export const isAzureWebSiteNameAvailable = (entity: Entity) => + entity?.metadata.annotations?.[AZURE_WEB_SITE_NAME_ANNOTATION]; + +const AzureSitesOverview = ({ entity }: { entity: Entity }) => { + const { webSiteName } = useServiceEntityAnnotations(entity); + + const [sites] = useSites({ + name: webSiteName, + }); + + if (sites.error) { + return ( +
+ +
+ ); + } + + return ( + + ); +}; + +/** @public */ +export const AzureSitesOverviewWidget = () => { + const { entity } = useEntity(); + + if (!isAzureWebSiteNameAvailable(entity)) { + return ( + + ); + } + + return ( + + + + ); +}; diff --git a/plugins/azure-sites/src/components/AzureSitesOverviewTableComponent/AzureSitesOverviewTable.test.tsx b/plugins/azure-sites/src/components/AzureSitesOverviewTableComponent/AzureSitesOverviewTable.test.tsx new file mode 100644 index 0000000000..23476b5c3e --- /dev/null +++ b/plugins/azure-sites/src/components/AzureSitesOverviewTableComponent/AzureSitesOverviewTable.test.tsx @@ -0,0 +1,86 @@ +/* + * 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 React from 'react'; +import { render } from '@testing-library/react'; +import { + AnyApiRef, + configApiRef, + errorApiRef, + identityApiRef, +} from '@backstage/core-plugin-api'; +import { rest } from 'msw'; +import { + setupRequestMockHandlers, + TestApiProvider, +} from '@backstage/test-utils'; +import { setupServer } from 'msw/node'; +import { DateTime } from 'luxon'; +import { siteMock } from '../../mocks/mocks'; +import { azureSiteApiRef } from '../..'; +import { AzureSitesOverviewTable } from './AzureSitesOverviewTable'; + +const errorApiMock = { post: jest.fn(), error$: jest.fn() }; +const identityApiMock = (getCredentials: any) => ({ + signOut: jest.fn(), + getProfileInfo: jest.fn(), + getBackstageIdentity: jest.fn(), + getCredentials, +}); +const azureSitesApiMock = {}; + +const config = { + getString: (_: string) => 'https://test-url', +}; + +const apis: [AnyApiRef, Partial][] = [ + [errorApiRef, errorApiMock], + [configApiRef, config], + [azureSiteApiRef, azureSitesApiMock], + [identityApiRef, identityApiMock], +]; + +describe('AzureSitesOverviewWidget', () => { + const worker = setupServer(); + setupRequestMockHandlers(worker); + + beforeEach(() => { + worker.use( + rest.get('/list/func-mock', (_, res, ctx) => { + res(ctx.json(siteMock)); + }), + ); + }); + + it('should display an overview table with the data from the requests', async () => { + const rendered = render( + + + , + ); + + expect(await rendered.findByText(siteMock.name)).toBeInTheDocument(); + expect(await rendered.findByText(siteMock.location)).toBeInTheDocument(); + expect(await rendered.findByText(siteMock.state)).toBeInTheDocument(); + expect( + await rendered.findByText( + DateTime.fromISO(siteMock.lastModifiedDate).toLocaleString( + DateTime.DATETIME_MED, + ), + ), + ).toBeInTheDocument(); + }); +}); diff --git a/plugins/azure-sites/src/components/AzureSitesOverviewTableComponent/AzureSitesOverviewTable.tsx b/plugins/azure-sites/src/components/AzureSitesOverviewTableComponent/AzureSitesOverviewTable.tsx new file mode 100644 index 0000000000..3f9895a226 --- /dev/null +++ b/plugins/azure-sites/src/components/AzureSitesOverviewTableComponent/AzureSitesOverviewTable.tsx @@ -0,0 +1,271 @@ +/* + * 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 React, { Dispatch, useEffect, useState } from 'react'; +import { + Box, + Card, + IconButton, + LinearProgress, + Link, + Menu, + MenuItem, + Snackbar, + Tooltip, +} from '@material-ui/core'; +import { default as MuiAlert } from '@material-ui/lab/Alert'; +import { AzureSite } from '@backstage/plugin-azure-sites-common'; +import { Table, TableColumn } from '@backstage/core-components'; +import FlashOnIcon from '@material-ui/icons/FlashOn'; +import PublicIcon from '@material-ui/icons/Public'; +import MoreVertIcon from '@material-ui/icons/MoreVert'; +import StartIcon from '@material-ui/icons/PlayArrow'; +import StopIcon from '@material-ui/icons/Stop'; +import OpenInNewIcon from '@material-ui/icons/OpenInNew'; +import { DateTime } from 'luxon'; +import { useApi } from '@backstage/core-plugin-api'; +import { azureSiteApiRef } from '../../api'; + +type States = 'Waiting' | 'Running' | 'Paused' | 'Failed' | 'Stopped'; +type Kinds = 'app' | 'functionapp'; + +const State = ({ value }: { value: States }) => { + const colorMap = { + Waiting: '#dcbc21', + Running: 'green', + Paused: 'black', + Failed: 'red', + Stopped: 'black', + }; + return ( + + + {value} + + ); +}; + +const Kind = ({ value }: { value: Kinds }) => { + const iconMap = { + app: , + functionapp: , + }; + return ( + + {iconMap[value]} + + ); +}; + +type TableProps = { + data: AzureSite[]; + loading: boolean; +}; + +const ActionButtons = ({ + value, + onMenuItemClick, +}: { + value: AzureSite; + onMenuItemClick: Dispatch>; +}) => { + const azureApi = useApi(azureSiteApiRef); + + const [anchorEl, setAnchorEl] = useState(null); + const open = Boolean(anchorEl); + + const handleOpen = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget); + }; + const handleClose = () => { + setAnchorEl(null); + }; + const start = () => { + azureApi.start({ + name: value.name, + resourceGroup: value.resourceGroup, + subscription: value.subscription, + }); + onMenuItemClick('Starting, this may take some time...'); + handleClose(); + }; + const stop = () => { + azureApi.stop({ + name: value.name, + resourceGroup: value.resourceGroup, + subscription: value.subscription, + }); + onMenuItemClick('Stopping, this may take some time...'); + handleClose(); + }; + + return ( +
+ + + + + {value.state !== 'Running' && ( + + +  Start + + )} + {value.state !== 'Stopped' && ( + + +  Stop + + )} + + +  Log Stream + + +
+ ); +}; + +/** @public */ +export const AzureSitesOverviewTable = ({ data, loading }: TableProps) => { + const [snackbarMessage, setSnackbarMessage] = useState(null); + const [isSnackbarOpen, setSnackbarOpen] = useState(false); + + const onSnackbarClose = () => { + setSnackbarMessage(null); + }; + + useEffect(() => { + setSnackbarOpen(!!snackbarMessage); + }, [snackbarMessage]); + + const columns: TableColumn[] = [ + { + width: '25px', + field: 'kind', + render: (func: AzureSite) => , + }, + { + title: 'name', + field: 'name', + highlight: true, + render: (func: AzureSite) => { + return ( + + {func.name} + + ); + }, + }, + { + title: 'location', + field: 'location', + render: (func: AzureSite) => func.location ?? 'unknown', + }, + { + title: 'status', + field: 'status', + render: (func: AzureSite) => , + }, + { + title: 'last modified', + field: 'lastModifiedDate', + render: (func: AzureSite) => + DateTime.fromISO(func.lastModifiedDate).toLocaleString( + DateTime.DATETIME_MED, + ), + }, + { + title: 'actions', + align: 'right', + sorting: false, + field: 'actions', + render: (func: AzureSite) => ( + + ), + }, + ]; + + const tableStyle = { + minWidth: '0', + width: '100%', + }; + + return ( + + + + + Azure Sites + + } + options={{ paging: true, search: false, pageSize: 10 }} + data={data} + emptyContent={} + isLoading={loading} + columns={columns} + /> + + + {snackbarMessage} + + + + ); +}; diff --git a/plugins/azure-sites/src/hooks/useServiceEntityAnnotations.ts b/plugins/azure-sites/src/hooks/useServiceEntityAnnotations.ts new file mode 100644 index 0000000000..08070dc8e4 --- /dev/null +++ b/plugins/azure-sites/src/hooks/useServiceEntityAnnotations.ts @@ -0,0 +1,27 @@ +/* + * 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 { Entity } from '@backstage/catalog-model'; + +export const AZURE_WEB_SITE_NAME_ANNOTATION = 'azure.com/microsoft-web-sites'; +export const useServiceEntityAnnotations = (entity: Entity) => { + const webSiteName = + entity?.metadata.annotations?.[AZURE_WEB_SITE_NAME_ANNOTATION] ?? ''; + + return { + webSiteName, + }; +}; diff --git a/plugins/azure-sites/src/hooks/useSites.ts b/plugins/azure-sites/src/hooks/useSites.ts new file mode 100644 index 0000000000..fb3215c008 --- /dev/null +++ b/plugins/azure-sites/src/hooks/useSites.ts @@ -0,0 +1,72 @@ +/* + * 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 useInterval from 'react-use/lib/useInterval'; +import useAsyncRetry from 'react-use/lib/useAsyncRetry'; +import { useApi, errorApiRef } from '@backstage/core-plugin-api'; +import { AzureSiteListResponse } from '@backstage/plugin-azure-sites-common'; +import { azureSiteApiRef } from '../api'; +import { useCallback } from 'react'; + +const POLLING_INTERVAL = 15000; + +export function useSites({ name }: { name: string }) { + const azureApi = useApi(azureSiteApiRef); + const errorApi = useApi(errorApiRef); + + const list = useCallback( + async () => { + return await azureApi.list({ + name: name, + }); + }, + [name], // eslint-disable-line react-hooks/exhaustive-deps + ); + const { + loading, + value: data, + error, + retry, + } = useAsyncRetry(async () => { + try { + const sites = await list(); + return sites; + } catch (e) { + if (e instanceof Error) { + if (e?.message === 'AbortError') { + errorApi.post( + new Error( + 'Timeout reaching backend plugin, please add azure-backend plugin', + ), + ); + } + errorApi.post(e); + } + return null; + } + }, []); + + useInterval(() => retry(), POLLING_INTERVAL); + + return [ + { + loading, + data, + error, + retry, + }, + ] as const; +} diff --git a/plugins/azure-sites/src/index.ts b/plugins/azure-sites/src/index.ts new file mode 100644 index 0000000000..83029c6af5 --- /dev/null +++ b/plugins/azure-sites/src/index.ts @@ -0,0 +1,19 @@ +/* + * 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. + */ + +export * from './plugin'; +export * from './api'; +export * from './components/AzureSitesOverviewComponent/AzureSitesOverview'; diff --git a/plugins/azure-sites/src/mocks/mocks.ts b/plugins/azure-sites/src/mocks/mocks.ts new file mode 100644 index 0000000000..2bc3b8b9fe --- /dev/null +++ b/plugins/azure-sites/src/mocks/mocks.ts @@ -0,0 +1,55 @@ +/* + * 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 { AzureSite } from '@backstage/plugin-azure-sites-common'; + +export const entityMock = { + metadata: { + namespace: 'default', + annotations: { + 'azure.com/microsoft-web-sites': 'func-mock', + }, + name: 'sample-azure-service', + description: 'A service for testing Backstage functionality.', + uid: 'c009b513-d053-4b3f-9429-8433a145e943', + }, + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + spec: { + type: 'service', + owner: 'dev_group@example.com', + lifecycle: 'experimental', + }, +}; + +// https://management.azure.com/subscriptions/{{subscriptionId}}/resourceGroups/{{resourceGroup}}/providers/Microsoft.Web/sites/{{functionsName}}?api-version=2022-03-01 +export const siteMock: AzureSite = { + name: 'func-mock', + kind: 'functionapp', + resourceGroup: 'mock-resourcegroup', + subscription: '00000000-0000-0000-0000-000000000000', + 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: '2022-09-02T11:09:58.9033333', + containerSize: 100, +}; diff --git a/plugins/azure-sites/src/plugin.test.ts b/plugins/azure-sites/src/plugin.test.ts new file mode 100644 index 0000000000..fba8e36e6f --- /dev/null +++ b/plugins/azure-sites/src/plugin.test.ts @@ -0,0 +1,23 @@ +/* + * 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 { azureSitesPlugin } from './plugin'; + +describe('azure', () => { + it('should export plugin', () => { + expect(azureSitesPlugin).toBeDefined(); + }); +}); diff --git a/plugins/azure-sites/src/plugin.ts b/plugins/azure-sites/src/plugin.ts new file mode 100644 index 0000000000..f54f121ea1 --- /dev/null +++ b/plugins/azure-sites/src/plugin.ts @@ -0,0 +1,61 @@ +/* + * 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 { + createApiFactory, + createComponentExtension, + createPlugin, + createRouteRef, + discoveryApiRef, + identityApiRef, +} from '@backstage/core-plugin-api'; +import { azureSiteApiRef, AzureSitesApiBackendClient } from './api'; + +const entityContentRouteRef = createRouteRef({ + id: 'Azure Sites Entity Content', +}); + +/** @public */ +export const azureSitesPlugin = createPlugin({ + id: 'azureSites', + apis: [ + createApiFactory({ + api: azureSiteApiRef, + deps: { + discoveryApi: discoveryApiRef, + identityApi: identityApiRef, + }, + factory: ({ discoveryApi, identityApi }) => + new AzureSitesApiBackendClient({ discoveryApi, identityApi }), + }), + ], + routes: { + entityContent: entityContentRouteRef, + }, +}); + +/** @public */ +export const EntityAzureSitesOverviewWidget = azureSitesPlugin.provide( + createComponentExtension({ + name: 'EntityAzureSitesOverviewWidget', + component: { + lazy: () => + import( + './components/AzureSitesOverviewComponent/AzureSitesOverview' + ).then(m => m.AzureSitesOverviewWidget), + }, + }), +); diff --git a/plugins/azure-sites/src/routes.ts b/plugins/azure-sites/src/routes.ts new file mode 100644 index 0000000000..1b10ad8dde --- /dev/null +++ b/plugins/azure-sites/src/routes.ts @@ -0,0 +1,21 @@ +/* + * 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 { createRouteRef } from '@backstage/core-plugin-api'; + +export const rootRouteRef = createRouteRef({ + id: 'azure-sites', +}); diff --git a/plugins/azure-sites/src/setupTests.ts b/plugins/azure-sites/src/setupTests.ts new file mode 100644 index 0000000000..2b5b615b19 --- /dev/null +++ b/plugins/azure-sites/src/setupTests.ts @@ -0,0 +1,18 @@ +/* + * 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 '@testing-library/jest-dom'; +import 'cross-fetch/polyfill'; diff --git a/yarn.lock b/yarn.lock index 092659fe12..6d803469fb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -339,6 +339,33 @@ __metadata: languageName: node linkType: hard +"@azure/arm-appservice@npm:^13.0.1": + version: 13.0.2 + resolution: "@azure/arm-appservice@npm:13.0.2" + dependencies: + "@azure/abort-controller": ^1.0.0 + "@azure/core-auth": ^1.3.0 + "@azure/core-client": ^1.5.0 + "@azure/core-lro": ^2.2.0 + "@azure/core-paging": ^1.2.0 + "@azure/core-rest-pipeline": ^1.8.0 + tslib: ^2.2.0 + checksum: 6d604c492a70a6cdd4d3ad3f634a74228dc081ed4f49128873e76bfa5e65ec8cce18b7b19319fd2b1b4edbd390dbd66c9ac48646f0ed80cae09cd2f1d05fb279 + languageName: node + linkType: hard + +"@azure/arm-resourcegraph@npm:^4.2.1": + version: 4.2.1 + resolution: "@azure/arm-resourcegraph@npm:4.2.1" + dependencies: + "@azure/core-auth": ^1.1.4 + "@azure/ms-rest-azure-js": ^2.1.0 + "@azure/ms-rest-js": ^2.2.0 + tslib: ^1.10.0 + checksum: a5afea9faef84ed4576fda430301039524aefe6976a54204fe4453e672aa3d13edc7ef5c996ad6293e9afa3a0833549174aaac18ce236c4ec1242698e2c22bd2 + languageName: node + linkType: hard + "@azure/core-asynciterator-polyfill@npm:^1.0.0": version: 1.0.0 resolution: "@azure/core-asynciterator-polyfill@npm:1.0.0" @@ -346,6 +373,16 @@ __metadata: languageName: node linkType: hard +"@azure/core-auth@npm:^1.1.4, @azure/core-auth@npm:^1.4.0": + version: 1.4.0 + resolution: "@azure/core-auth@npm:1.4.0" + dependencies: + "@azure/abort-controller": ^1.0.0 + tslib: ^2.2.0 + checksum: 1c76c296fe911ad39fc780b033c25a92c41c5a15f011b816d42c13584f627a4dd153dfb4334900ec93eb5b006e14bdda37e8412a7697c5a9636a0abaccffad39 + languageName: node + linkType: hard + "@azure/core-auth@npm:^1.3.0": version: 1.3.2 resolution: "@azure/core-auth@npm:1.3.2" @@ -371,6 +408,21 @@ __metadata: languageName: node linkType: hard +"@azure/core-client@npm:^1.5.0": + version: 1.6.1 + resolution: "@azure/core-client@npm:1.6.1" + dependencies: + "@azure/abort-controller": ^1.0.0 + "@azure/core-auth": ^1.4.0 + "@azure/core-rest-pipeline": ^1.9.1 + "@azure/core-tracing": ^1.0.0 + "@azure/core-util": ^1.0.0 + "@azure/logger": ^1.0.0 + tslib: ^2.2.0 + checksum: 400890a9b5f0c8801ff92005c3cba7bb5124634e321735406731bef33688a79f23d28b49fccdfab90814dade41a13f3d3cb99f80151a2bff17628416e811afb6 + languageName: node + linkType: hard + "@azure/core-http@npm:^2.0.0": version: 2.2.4 resolution: "@azure/core-http@npm:2.2.4" @@ -415,6 +467,15 @@ __metadata: languageName: node linkType: hard +"@azure/core-paging@npm:^1.2.0": + version: 1.3.0 + resolution: "@azure/core-paging@npm:1.3.0" + dependencies: + tslib: ^2.2.0 + checksum: 3fbf3d6474e2346f7c3ea8344a41bf41e053789efbbd29df78365e9c9ca66e143da3e5407d8c9dd5ddc82a65680185cd6f0ec851c48635776d18a6a605a98e78 + languageName: node + linkType: hard + "@azure/core-rest-pipeline@npm:^1.1.0, @azure/core-rest-pipeline@npm:^1.5.0": version: 1.5.0 resolution: "@azure/core-rest-pipeline@npm:1.5.0" @@ -432,6 +493,24 @@ __metadata: languageName: node linkType: hard +"@azure/core-rest-pipeline@npm:^1.8.0, @azure/core-rest-pipeline@npm:^1.9.1": + version: 1.9.2 + resolution: "@azure/core-rest-pipeline@npm:1.9.2" + dependencies: + "@azure/abort-controller": ^1.0.0 + "@azure/core-auth": ^1.4.0 + "@azure/core-tracing": ^1.0.1 + "@azure/core-util": ^1.0.0 + "@azure/logger": ^1.0.0 + form-data: ^4.0.0 + http-proxy-agent: ^5.0.0 + https-proxy-agent: ^5.0.0 + tslib: ^2.2.0 + uuid: ^8.3.0 + checksum: cdfdaf8bdb0778f40ecfeb8b5da0b4f8643d641b75bb5869485afd38f7c3127ab088ac2fe2980df03d28fb6715928e04560343ee6416246aebe75443c4dd2906 + languageName: node + linkType: hard + "@azure/core-tracing@npm:1.0.0-preview.13": version: 1.0.0-preview.13 resolution: "@azure/core-tracing@npm:1.0.0-preview.13" @@ -442,7 +521,7 @@ __metadata: languageName: node linkType: hard -"@azure/core-tracing@npm:^1.0.0": +"@azure/core-tracing@npm:^1.0.0, @azure/core-tracing@npm:^1.0.1": version: 1.0.1 resolution: "@azure/core-tracing@npm:1.0.1" dependencies: @@ -493,6 +572,34 @@ __metadata: languageName: node linkType: hard +"@azure/ms-rest-azure-js@npm:^2.1.0": + version: 2.1.0 + resolution: "@azure/ms-rest-azure-js@npm:2.1.0" + dependencies: + "@azure/core-auth": ^1.1.4 + "@azure/ms-rest-js": ^2.2.0 + tslib: ^1.10.0 + checksum: 0e6d6700f4ee0ad14383642b02fe86b33f4034aaff38499374ed5be59154e869ee231790d20b7557622ebcdfa9eed45dc2559391a79dee51c849a8ced0c86fed + languageName: node + linkType: hard + +"@azure/ms-rest-js@npm:^2.2.0": + version: 2.6.2 + resolution: "@azure/ms-rest-js@npm:2.6.2" + dependencies: + "@azure/core-auth": ^1.1.4 + abort-controller: ^3.0.0 + form-data: ^2.5.0 + node-fetch: ^2.6.7 + tough-cookie: ^3.0.1 + tslib: ^1.10.0 + tunnel: 0.0.6 + uuid: ^8.3.2 + xml2js: ^0.4.19 + checksum: de650dfc3928a2036923fb3bf50e0324f9f36f5d320ca6d128005b1345ad80b39ab3410073ac35fb137dec4c7d799c602b1cafdf9573b3432b6b8eee8a1e39f8 + languageName: node + linkType: hard + "@azure/msal-browser@npm:^2.26.0": version: 2.27.0 resolution: "@azure/msal-browser@npm:2.27.0" @@ -4138,6 +4245,66 @@ __metadata: languageName: unknown linkType: soft +"@backstage/plugin-azure-sites-backend@workspace:^, @backstage/plugin-azure-sites-backend@workspace:plugins/azure-sites-backend": + version: 0.0.0-use.local + resolution: "@backstage/plugin-azure-sites-backend@workspace:plugins/azure-sites-backend" + dependencies: + "@azure/arm-appservice": ^13.0.1 + "@azure/arm-resourcegraph": ^4.2.1 + "@azure/identity": ^2.1.0 + "@backstage/backend-common": "workspace:^" + "@backstage/cli": "workspace:^" + "@backstage/config": "workspace:^" + "@backstage/plugin-azure-sites-common": "workspace:^" + "@types/express": ^4.17.6 + "@types/supertest": ^2.0.8 + express: ^4.17.1 + express-promise-router: ^4.1.0 + msw: ^0.47.0 + node-fetch: ^2.6.7 + winston: ^3.2.1 + yn: ^4.0.0 + languageName: unknown + linkType: soft + +"@backstage/plugin-azure-sites-common@workspace:^, @backstage/plugin-azure-sites-common@workspace:plugins/azure-sites-common": + version: 0.0.0-use.local + resolution: "@backstage/plugin-azure-sites-common@workspace:plugins/azure-sites-common" + dependencies: + "@backstage/cli": "workspace:^" + languageName: unknown + linkType: soft + +"@backstage/plugin-azure-sites@workspace:^, @backstage/plugin-azure-sites@workspace:plugins/azure-sites": + version: 0.0.0-use.local + resolution: "@backstage/plugin-azure-sites@workspace:plugins/azure-sites" + dependencies: + "@backstage/catalog-model": "workspace:^" + "@backstage/cli": "workspace:^" + "@backstage/core-app-api": "workspace:^" + "@backstage/core-components": "workspace:^" + "@backstage/core-plugin-api": "workspace:^" + "@backstage/dev-utils": "workspace:^" + "@backstage/plugin-azure-sites-common": "workspace:^" + "@backstage/plugin-catalog-react": "workspace:^" + "@backstage/test-utils": "workspace:^" + "@backstage/theme": "workspace:^" + "@material-ui/core": ^4.12.2 + "@material-ui/icons": ^4.9.1 + "@material-ui/lab": 4.0.0-alpha.57 + "@testing-library/jest-dom": ^5.10.1 + "@testing-library/react": ^12.1.3 + "@testing-library/user-event": ^14.0.0 + "@types/node": ^16.11.26 + cross-fetch: ^3.1.5 + luxon: ^3.0.0 + msw: ^0.47.0 + react-use: ^17.2.4 + peerDependencies: + react: ^16.13.1 || ^17.0.0 + languageName: unknown + linkType: soft + "@backstage/plugin-badges-backend@workspace:^, @backstage/plugin-badges-backend@workspace:plugins/badges-backend": version: 0.0.0-use.local resolution: "@backstage/plugin-badges-backend@workspace:plugins/badges-backend" @@ -21868,6 +22035,7 @@ __metadata: "@backstage/plugin-apache-airflow": "workspace:^" "@backstage/plugin-api-docs": "workspace:^" "@backstage/plugin-azure-devops": "workspace:^" + "@backstage/plugin-azure-sites": "workspace:^" "@backstage/plugin-badges": "workspace:^" "@backstage/plugin-catalog-common": "workspace:^" "@backstage/plugin-catalog-graph": "workspace:^" @@ -21973,6 +22141,7 @@ __metadata: "@backstage/plugin-auth-backend": "workspace:^" "@backstage/plugin-auth-node": "workspace:^" "@backstage/plugin-azure-devops-backend": "workspace:^" + "@backstage/plugin-azure-sites-backend": "workspace:^" "@backstage/plugin-badges-backend": "workspace:^" "@backstage/plugin-catalog-backend": "workspace:^" "@backstage/plugin-code-coverage-backend": "workspace:^" @@ -25064,6 +25233,13 @@ __metadata: languageName: node linkType: hard +"ip-regex@npm:^2.1.0": + version: 2.1.0 + resolution: "ip-regex@npm:2.1.0" + checksum: 331d95052aa53ce245745ea0fc3a6a1e2e3c8d6da65fa8ea52bf73768c1b22a9ac50629d1d2b08c04e7b3ac4c21b536693c149ce2c2615ee4796030e5b3e3cba + languageName: node + linkType: hard + "ip@npm:^1.1.5": version: 1.1.5 resolution: "ip@npm:1.1.5" @@ -37528,6 +37704,17 @@ __metadata: languageName: node linkType: hard +"tough-cookie@npm:^3.0.1": + version: 3.0.1 + resolution: "tough-cookie@npm:3.0.1" + dependencies: + ip-regex: ^2.1.0 + psl: ^1.1.28 + punycode: ^2.1.1 + checksum: 796f6239bce5674a1267b19f41972a2602a2a23715817237b5922b0dc2343512512eea7d41d29210a4ec545f8ef32173bbbf01277dd8ec3ae3841b19cbe69f67 + languageName: node + linkType: hard + "tough-cookie@npm:^4.0.0": version: 4.0.0 resolution: "tough-cookie@npm:4.0.0" @@ -37729,7 +37916,7 @@ __metadata: languageName: node linkType: hard -"tslib@npm:^1.8.1, tslib@npm:^1.9.0, tslib@npm:^1.9.3": +"tslib@npm:^1.10.0, tslib@npm:^1.8.1, tslib@npm:^1.9.0, tslib@npm:^1.9.3": version: 1.14.1 resolution: "tslib@npm:1.14.1" checksum: dbe628ef87f66691d5d2959b3e41b9ca0045c3ee3c7c7b906cc1e328b39f199bb1ad9e671c39025bd56122ac57dfbf7385a94843b1cc07c60a4db74795829acd