From eef62546ce987f0bc39473fa7e62e3eb73bb4c6f Mon Sep 17 00:00:00 2001 From: Dominik Pfaffenbauer Date: Thu, 19 Jan 2023 08:59:01 +0100 Subject: [PATCH] Introduce Lighthouse Backend with Scheduling of Audits Signed-off-by: Dominik Pfaffenbauer --- .changeset/cool-spoons-bathe.md | 5 + .changeset/eight-vans-drum.md | 5 + .changeset/fluffy-books-tickle.md | 5 + .changeset/young-beans-float.md | 5 + example.yaml | 0 packages/backend/package.json | 1 + packages/backend/src/index.ts | 4 + packages/backend/src/plugins/lighthouse.ts | 29 ++ plugins/lighthouse-backend/.eslintrc.js | 1 + plugins/lighthouse-backend/CHANGELOG.md | 0 plugins/lighthouse-backend/README.md | 70 +++++ plugins/lighthouse-backend/api-report.md | 286 ++++++++++++++++++ plugins/lighthouse-backend/package.json | 58 ++++ plugins/lighthouse-backend/src/config.ts | 86 ++++++ plugins/lighthouse-backend/src/index.ts | 17 ++ .../src/service/EntitiesLoader.ts | 32 ++ .../lighthouse-backend/src/service/plugin.ts | 124 ++++++++ plugins/lighthouse-backend/src/setupTests.ts | 17 ++ plugins/lighthouse-common/.eslintrc.js | 1 + plugins/lighthouse-common/CHANGELOG.md | 0 plugins/lighthouse-common/README.md | 3 + plugins/lighthouse-common/api-report.md | 286 ++++++++++++++++++ plugins/lighthouse-common/package.json | 51 ++++ plugins/lighthouse-common/src/api.ts | 188 ++++++++++++ plugins/lighthouse-common/src/index.ts | 23 ++ plugins/lighthouse/README.md | 4 + plugins/lighthouse/package.json | 1 + plugins/lighthouse/src/api.ts | 173 +---------- .../AuditList/AuditListForEntity.test.tsx | 4 +- .../AuditList/AuditListTable.test.tsx | 4 +- .../components/AuditList/AuditListTable.tsx | 3 +- .../src/components/AuditList/index.test.tsx | 4 +- .../src/components/AuditStatusIcon/index.tsx | 2 +- .../src/components/AuditView/index.test.tsx | 7 +- .../src/components/AuditView/index.tsx | 3 +- .../Cards/LastLighthouseAuditCard.test.tsx | 2 +- .../Cards/LastLighthouseAuditCard.tsx | 6 +- .../src/components/CreateAudit/index.test.tsx | 3 +- .../src/components/CreateAudit/index.tsx | 4 +- .../src/hooks/useWebsiteForEntity.test.tsx | 3 +- plugins/lighthouse/src/plugin.ts | 3 +- plugins/lighthouse/src/utils.ts | 7 +- yarn.lock | 27 ++ 43 files changed, 1367 insertions(+), 190 deletions(-) create mode 100644 .changeset/cool-spoons-bathe.md create mode 100644 .changeset/eight-vans-drum.md create mode 100644 .changeset/fluffy-books-tickle.md create mode 100644 .changeset/young-beans-float.md create mode 100644 example.yaml create mode 100644 packages/backend/src/plugins/lighthouse.ts create mode 100644 plugins/lighthouse-backend/.eslintrc.js create mode 100644 plugins/lighthouse-backend/CHANGELOG.md create mode 100644 plugins/lighthouse-backend/README.md create mode 100644 plugins/lighthouse-backend/api-report.md create mode 100644 plugins/lighthouse-backend/package.json create mode 100644 plugins/lighthouse-backend/src/config.ts create mode 100644 plugins/lighthouse-backend/src/index.ts create mode 100644 plugins/lighthouse-backend/src/service/EntitiesLoader.ts create mode 100644 plugins/lighthouse-backend/src/service/plugin.ts create mode 100644 plugins/lighthouse-backend/src/setupTests.ts create mode 100644 plugins/lighthouse-common/.eslintrc.js create mode 100644 plugins/lighthouse-common/CHANGELOG.md create mode 100644 plugins/lighthouse-common/README.md create mode 100644 plugins/lighthouse-common/api-report.md create mode 100644 plugins/lighthouse-common/package.json create mode 100644 plugins/lighthouse-common/src/api.ts create mode 100644 plugins/lighthouse-common/src/index.ts diff --git a/.changeset/cool-spoons-bathe.md b/.changeset/cool-spoons-bathe.md new file mode 100644 index 0000000000..b6bcaf89aa --- /dev/null +++ b/.changeset/cool-spoons-bathe.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-lighthouse-backend': minor +--- + +Introduce Lighthouse Backend Plugin to run scheduled Lighthouse Audits diff --git a/.changeset/eight-vans-drum.md b/.changeset/eight-vans-drum.md new file mode 100644 index 0000000000..8e815a1701 --- /dev/null +++ b/.changeset/eight-vans-drum.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-lighthouse-common': minor +--- + +Introduce @backstage/plugin-lighthouse-common with the API implementation diff --git a/.changeset/fluffy-books-tickle.md b/.changeset/fluffy-books-tickle.md new file mode 100644 index 0000000000..c647b68f7c --- /dev/null +++ b/.changeset/fluffy-books-tickle.md @@ -0,0 +1,5 @@ +--- +'example-backend': minor +--- + +Add Lighthouse Backend to Example Backend diff --git a/.changeset/young-beans-float.md b/.changeset/young-beans-float.md new file mode 100644 index 0000000000..1f05c4cb26 --- /dev/null +++ b/.changeset/young-beans-float.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-lighthouse': minor +--- + +Require @backstage/plugin-lighthouse-common package where API implementation moved to diff --git a/example.yaml b/example.yaml new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/backend/package.json b/packages/backend/package.json index e3240f8cff..ccac950e58 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -49,6 +49,7 @@ "@backstage/plugin-jenkins-backend": "workspace:^", "@backstage/plugin-kafka-backend": "workspace:^", "@backstage/plugin-kubernetes-backend": "workspace:^", + "@backstage/plugin-lighthouse-backend": "workspace:^", "@backstage/plugin-permission-backend": "workspace:^", "@backstage/plugin-permission-common": "workspace:^", "@backstage/plugin-permission-node": "workspace:^", diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index a721c443d6..b68b162e52 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -62,6 +62,7 @@ import jenkins from './plugins/jenkins'; import permission from './plugins/permission'; import playlist from './plugins/playlist'; import adr from './plugins/adr'; +import lighthouse from './plugins/lighthouse'; import { PluginEnvironment } from './types'; import { ServerPermissionClient } from '@backstage/plugin-permission-node'; import { DefaultIdentityClient } from '@backstage/plugin-auth-node'; @@ -148,6 +149,7 @@ async function main() { const playlistEnv = useHotMemoize(module, () => createEnv('playlist')); const eventsEnv = useHotMemoize(module, () => createEnv('events')); const exploreEnv = useHotMemoize(module, () => createEnv('explore')); + const lighthouseEnv = useHotMemoize(module, () => createEnv('lighthouse')); const eventBasedEntityProviders = await catalogEventBasedProviders( catalogEnv, @@ -180,6 +182,8 @@ async function main() { apiRouter.use('/adr', await adr(adrEnv)); apiRouter.use(notFoundHandler()); + await lighthouse(lighthouseEnv); + const service = createServiceBuilder(module) .loadConfig(config) .addRouter('', await healthcheck(healthcheckEnv)) diff --git a/packages/backend/src/plugins/lighthouse.ts b/packages/backend/src/plugins/lighthouse.ts new file mode 100644 index 0000000000..5ac9310e98 --- /dev/null +++ b/packages/backend/src/plugins/lighthouse.ts @@ -0,0 +1,29 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { create } from '@backstage/plugin-lighthouse-backend'; +import { PluginEnvironment } from '../types'; +import { CatalogClient } from '@backstage/catalog-client'; + +export default async function createPlugin(env: PluginEnvironment) { + const { logger, scheduler, config } = env; + + const catalogClient = new CatalogClient({ + discoveryApi: env.discovery, + }); + + await create({ logger, scheduler, config, catalogClient }); +} diff --git a/plugins/lighthouse-backend/.eslintrc.js b/plugins/lighthouse-backend/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/lighthouse-backend/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/lighthouse-backend/CHANGELOG.md b/plugins/lighthouse-backend/CHANGELOG.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/lighthouse-backend/README.md b/plugins/lighthouse-backend/README.md new file mode 100644 index 0000000000..2db7ffa583 --- /dev/null +++ b/plugins/lighthouse-backend/README.md @@ -0,0 +1,70 @@ +# Lighthouse Backend + +Lighthouse Backend allows you to run scheduled lighthouse Tests for each Website with the annotation `lighthouse.com/website-url`. + +## Setup + +1. Install the plugin using: + +```bash +# From your Backstage root directory +yarn add --cwd packages/backend @backstage/lighthouse-backend +``` + +2. Create a `lighthouse.ts` file inside `packages/backend/src/plugins/`: + +```typescript +import { Router } from 'express'; +import { create } from '@backstage/plugin-lighthouse-backend'; +import { PluginEnvironment } from '../types'; +import { CatalogClient } from '@backstage/catalog-client'; + +export default async function createPlugin(env: PluginEnvironment) { + const { logger, scheduler, config } = env; + + const catalogClient = new CatalogClient({ + discoveryApi: env.discovery, + }); + + await create({ logger, scheduler, config, catalogClient }); +} +``` + +3. Modify your `packages/backend/src/index.ts` to include: + +```diff + ... + + import { Config } from '@backstage/config'; + import app from './plugins/app'; ++import lighthouse from './plugins/lighthouse'; + import scaffolder from './plugins/scaffolder'; + + ... + + async function main() { + + ... + + const authEnv = useHotMemoize(module, () => createEnv('auth')); ++ const lighthouseEnv = useHotMemoize(module, () => createEnv('lighthouse')); + const proxyEnv = useHotMemoize(module, () => createEnv('proxy')); + + ... + + const apiRouter = Router(); + apiRouter.use('/catalog', await catalog(catalogEnv)); + apiRouter.use('/scaffolder', await scaffolder(scaffolderEnv)); + ++ await lighthouse(lighthouseEnv) +``` + +## Configuration + +You can define how often and when the scheduler should run the audits: + +```yaml +lighthouse: + schedule: + days: 1 +``` diff --git a/plugins/lighthouse-backend/api-report.md b/plugins/lighthouse-backend/api-report.md new file mode 100644 index 0000000000..fe8aa19966 --- /dev/null +++ b/plugins/lighthouse-backend/api-report.md @@ -0,0 +1,286 @@ +## API Report File for "@backstage/plugin-kubernetes-common" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { Entity } from '@backstage/catalog-model'; +import type { JsonObject } from '@backstage/types'; +import { PodStatus } from '@kubernetes/client-node'; +import { V1ConfigMap } from '@kubernetes/client-node'; +import { V1CronJob } from '@kubernetes/client-node'; +import { V1DaemonSet } from '@kubernetes/client-node'; +import { V1Deployment } from '@kubernetes/client-node'; +import { V1HorizontalPodAutoscaler } from '@kubernetes/client-node'; +import { V1Ingress } from '@kubernetes/client-node'; +import { V1Job } from '@kubernetes/client-node'; +import { V1LimitRange } from '@kubernetes/client-node'; +import { V1Pod } from '@kubernetes/client-node'; +import { V1ReplicaSet } from '@kubernetes/client-node'; +import { V1Service } from '@kubernetes/client-node'; +import { V1StatefulSet } from '@kubernetes/client-node'; + +// @public (undocumented) +export type AuthProviderType = 'google' | 'serviceAccount' | 'aws' | 'azure'; + +// @public (undocumented) +export interface ClientContainerStatus { + // (undocumented) + container: string; + // (undocumented) + cpuUsage: ClientCurrentResourceUsage; + // (undocumented) + memoryUsage: ClientCurrentResourceUsage; +} + +// @public (undocumented) +export interface ClientCurrentResourceUsage { + // (undocumented) + currentUsage: number | string; + // (undocumented) + limitTotal: number | string; + // (undocumented) + requestTotal: number | string; +} + +// @public (undocumented) +export interface ClientPodStatus { + // (undocumented) + containers: ClientContainerStatus[]; + // (undocumented) + cpu: ClientCurrentResourceUsage; + // (undocumented) + memory: ClientCurrentResourceUsage; + // (undocumented) + pod: V1Pod; +} + +// @public (undocumented) +export interface ClusterAttributes { + dashboardApp?: string; + dashboardParameters?: JsonObject; + dashboardUrl?: string; + name: string; +} + +// @public (undocumented) +export interface ClusterObjects { + // (undocumented) + cluster: ClusterAttributes; + // (undocumented) + errors: KubernetesFetchError[]; + // (undocumented) + podMetrics: ClientPodStatus[]; + // (undocumented) + resources: FetchResponse[]; +} + +// @public (undocumented) +export interface ConfigMapFetchResponse { + // (undocumented) + resources: Array; + // (undocumented) + type: 'configmaps'; +} + +// @public (undocumented) +export interface CronJobsFetchResponse { + // (undocumented) + resources: Array; + // (undocumented) + type: 'cronjobs'; +} + +// @public (undocumented) +export interface CustomObjectsByEntityRequest { + // (undocumented) + auth: KubernetesRequestAuth; + // (undocumented) + customResources: CustomResourceMatcher[]; + // (undocumented) + entity: Entity; +} + +// @public (undocumented) +export interface CustomResourceFetchResponse { + // (undocumented) + resources: Array; + // (undocumented) + type: 'customresources'; +} + +// @public (undocumented) +export interface CustomResourceMatcher { + // (undocumented) + apiVersion: string; + // (undocumented) + group: string; + // (undocumented) + plural: string; +} + +// @public (undocumented) +export interface DaemonSetsFetchResponse { + // (undocumented) + resources: Array; + // (undocumented) + type: 'daemonsets'; +} + +// @public (undocumented) +export interface DeploymentFetchResponse { + // (undocumented) + resources: Array; + // (undocumented) + type: 'deployments'; +} + +// @public (undocumented) +export type FetchResponse = + | PodFetchResponse + | ServiceFetchResponse + | ConfigMapFetchResponse + | DeploymentFetchResponse + | LimitRangeFetchReponse + | ReplicaSetsFetchResponse + | HorizontalPodAutoscalersFetchResponse + | JobsFetchResponse + | CronJobsFetchResponse + | IngressesFetchResponse + | CustomResourceFetchResponse + | StatefulSetsFetchResponse + | DaemonSetsFetchResponse + | PodStatusFetchResponse; + +// @public (undocumented) +export interface HorizontalPodAutoscalersFetchResponse { + // (undocumented) + resources: Array; + // (undocumented) + type: 'horizontalpodautoscalers'; +} + +// @public (undocumented) +export interface IngressesFetchResponse { + // (undocumented) + resources: Array; + // (undocumented) + type: 'ingresses'; +} + +// @public (undocumented) +export interface JobsFetchResponse { + // (undocumented) + resources: Array; + // (undocumented) + type: 'jobs'; +} + +// @public (undocumented) +export type KubernetesErrorTypes = + | 'BAD_REQUEST' + | 'UNAUTHORIZED_ERROR' + | 'NOT_FOUND' + | 'SYSTEM_ERROR' + | 'UNKNOWN_ERROR'; + +// @public (undocumented) +export type KubernetesFetchError = StatusError | RawFetchError; + +// @public (undocumented) +export interface KubernetesRequestAuth { + // (undocumented) + google?: string; + // (undocumented) + oidc?: { + [key: string]: string; + }; +} + +// @public (undocumented) +export interface KubernetesRequestBody { + // (undocumented) + auth?: KubernetesRequestAuth; + // (undocumented) + entity: Entity; +} + +// @public (undocumented) +export interface LimitRangeFetchReponse { + // (undocumented) + resources: Array; + // (undocumented) + type: 'limitranges'; +} + +// @public (undocumented) +export interface ObjectsByEntityResponse { + // (undocumented) + items: ClusterObjects[]; +} + +// @public (undocumented) +export interface PodFetchResponse { + // (undocumented) + resources: Array; + // (undocumented) + type: 'pods'; +} + +// @public (undocumented) +export interface PodStatusFetchResponse { + // (undocumented) + resources: Array; + // (undocumented) + type: 'podstatus'; +} + +// @public (undocumented) +export interface RawFetchError { + // (undocumented) + errorType: 'FETCH_ERROR'; + // (undocumented) + message: string; +} + +// @public (undocumented) +export interface ReplicaSetsFetchResponse { + // (undocumented) + resources: Array; + // (undocumented) + type: 'replicasets'; +} + +// @public (undocumented) +export interface ServiceFetchResponse { + // (undocumented) + resources: Array; + // (undocumented) + type: 'services'; +} + +// @public (undocumented) +export interface StatefulSetsFetchResponse { + // (undocumented) + resources: Array; + // (undocumented) + type: 'statefulsets'; +} + +// @public (undocumented) +export interface StatusError { + // (undocumented) + errorType: KubernetesErrorTypes; + // (undocumented) + resourcePath?: string; + // (undocumented) + statusCode?: number; +} + +// @public (undocumented) +export interface WorkloadsByEntityRequest { + // (undocumented) + auth: KubernetesRequestAuth; + // (undocumented) + entity: Entity; +} +``` diff --git a/plugins/lighthouse-backend/package.json b/plugins/lighthouse-backend/package.json new file mode 100644 index 0000000000..91ef1a4765 --- /dev/null +++ b/plugins/lighthouse-backend/package.json @@ -0,0 +1,58 @@ +{ + "name": "@backstage/plugin-lighthouse-backend", + "description": "Backend functionalities for lighthouse", + "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/lighthouse-backend" + }, + "keywords": [ + "lighthouse" + ], + "files": [ + "dist" + ], + "scripts": { + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean" + }, + "bugs": { + "url": "https://github.com/backstage/backstage/issues" + }, + "dependencies": { + "@backstage/backend-common": "workspace:^", + "@backstage/backend-tasks": "workspace:^", + "@backstage/catalog-client": "workspace:^", + "@backstage/catalog-model": "workspace:^", + "@backstage/config": "workspace:^", + "@backstage/plugin-lighthouse-common": "workspace:^", + "@backstage/types": "workspace:^", + "winston": "^3.2.1" + }, + "devDependencies": { + "@backstage/cli": "workspace:^" + }, + "jest": { + "roots": [ + ".." + ] + } +} diff --git a/plugins/lighthouse-backend/src/config.ts b/plugins/lighthouse-backend/src/config.ts new file mode 100644 index 0000000000..0394e5eb4a --- /dev/null +++ b/plugins/lighthouse-backend/src/config.ts @@ -0,0 +1,86 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Config } from '@backstage/config'; +import { HumanDuration as HumanDuration } from '@backstage/types'; + +export interface LighthouseAuditScheduleConfig { + schedule: HumanDuration; + timeout: HumanDuration; + auditDetail: HumanDuration; +} + +/** @public */ +export type LighthouseAuditSchedule = { + getSchedule: () => HumanDuration; + getTimeout: () => HumanDuration; +}; + +/** @public */ +export class LighthouseAuditScheduleImpl implements LighthouseAuditSchedule { + static fromConfig(config: Config) { + const lighthouse = config.getOptionalConfig('lighthouse'); + + let schedule: HumanDuration = { days: 1 }; + let timeout: HumanDuration = {}; + + if (lighthouse) { + const scheduleConfig = lighthouse.getOptionalConfig('schedule'); + const timeoutConfig = lighthouse.getOptionalConfig('timeout'); + + if (scheduleConfig) { + schedule = { + milliseconds: scheduleConfig.getOptionalNumber('milliseconds'), + seconds: scheduleConfig.getOptionalNumber('seconds'), + minutes: scheduleConfig.getOptionalNumber('minutes'), + hours: scheduleConfig.getOptionalNumber('hours'), + days: scheduleConfig.getOptionalNumber('days'), + weeks: scheduleConfig.getOptionalNumber('weeks'), + months: scheduleConfig.getOptionalNumber('months'), + years: scheduleConfig.getOptionalNumber('years'), + }; + } + + if (timeoutConfig) { + timeout = { + milliseconds: timeoutConfig.getOptionalNumber('milliseconds'), + seconds: timeoutConfig.getOptionalNumber('seconds'), + minutes: timeoutConfig.getOptionalNumber('minutes'), + hours: timeoutConfig.getOptionalNumber('hours'), + days: timeoutConfig.getOptionalNumber('days'), + weeks: timeoutConfig.getOptionalNumber('weeks'), + months: timeoutConfig.getOptionalNumber('months'), + years: timeoutConfig.getOptionalNumber('years'), + }; + } + } + + return new LighthouseAuditScheduleImpl(schedule, timeout); + } + + constructor( + private schedule: HumanDuration, + private timeout: HumanDuration, + ) {} + + getSchedule(): HumanDuration { + return this.schedule; + } + + getTimeout(): HumanDuration { + return this.timeout; + } +} diff --git a/plugins/lighthouse-backend/src/index.ts b/plugins/lighthouse-backend/src/index.ts new file mode 100644 index 0000000000..ee878b9214 --- /dev/null +++ b/plugins/lighthouse-backend/src/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './service/plugin'; diff --git a/plugins/lighthouse-backend/src/service/EntitiesLoader.ts b/plugins/lighthouse-backend/src/service/EntitiesLoader.ts new file mode 100644 index 0000000000..fe9aab48cc --- /dev/null +++ b/plugins/lighthouse-backend/src/service/EntitiesLoader.ts @@ -0,0 +1,32 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + CatalogClient, + CATALOG_FILTER_EXISTS, +} from '@backstage/catalog-client'; + +export async function loadLighthouseEntities(catalogClient: CatalogClient) { + const filter: Record = { + kind: 'Component', + 'spec.type': 'website', + ['lighthouse.com/website-url']: CATALOG_FILTER_EXISTS, + }; + + return await catalogClient.getEntities({ + filter: [filter], + }); +} diff --git a/plugins/lighthouse-backend/src/service/plugin.ts b/plugins/lighthouse-backend/src/service/plugin.ts new file mode 100644 index 0000000000..8a89a9f18b --- /dev/null +++ b/plugins/lighthouse-backend/src/service/plugin.ts @@ -0,0 +1,124 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Logger } from 'winston'; +import { PluginTaskScheduler } from '@backstage/backend-tasks'; +import { + CATALOG_FILTER_EXISTS, + CatalogClient, +} from '@backstage/catalog-client'; +import { Config } from '@backstage/config'; +import { LighthouseRestApi } from '@backstage/plugin-lighthouse-common'; +import { stringifyEntityRef } from '@backstage/catalog-model'; +import { LighthouseAuditScheduleImpl } from '../config'; + +export interface RouterOptions { + logger: Logger; + config: Config; + scheduler?: PluginTaskScheduler; + catalogClient: CatalogClient; +} + +export async function create(options: RouterOptions) { + const { logger, scheduler, catalogClient, config } = options; + const lighthouseApi = LighthouseRestApi.fromConfig(config); + + const lighthouseAuditConfig = LighthouseAuditScheduleImpl.fromConfig(config); + const formFactorToScreenEmulationMap = { + // the default is mobile, so no need to override + mobile: undefined, + // Values from lighthouse's cli "desktop" preset + // https://github.com/GoogleChrome/lighthouse/blob/a6738e0033e7e5ca308b97c1c36f298b7d399402/lighthouse-core/config/constants.js#L71-L77 + desktop: { + mobile: false, + width: 1350, + height: 940, + deviceScaleFactor: 1, + disabled: false, + }, + }; + + logger.info( + `Running with Scheduler Config ${JSON.stringify( + lighthouseAuditConfig.getSchedule(), + )} and timeout ${JSON.stringify(lighthouseAuditConfig.getTimeout())}`, + ); + + if (scheduler) { + await scheduler.scheduleTask({ + id: 'lighthouse_audit', + frequency: lighthouseAuditConfig.getSchedule(), + timeout: lighthouseAuditConfig.getTimeout(), + initialDelay: { minutes: 15 }, + fn: async () => { + const filter: Record = { + kind: 'Component', + 'spec.type': 'website', + ['metadata.annotations.lighthouse.com/website-url']: + CATALOG_FILTER_EXISTS, + }; + + logger.info('Running Lighthouse Audit Task'); + + const websitesWithUrl = await catalogClient.getEntities({ + filter: [filter], + }); + + let index = 0; + for (const entity of websitesWithUrl.items) { + const websiteUrl = + entity.metadata.annotations?.['lighthouse.com/website-url'] ?? ''; + + if (!websiteUrl) { + continue; + } + + const controller = new AbortController(); + + await scheduler.scheduleTask({ + id: `lighthouse_audit_${stringifyEntityRef(entity)}`, + frequency: {}, + timeout: {}, + initialDelay: { minutes: index * 2 }, + signal: controller.signal, + fn: async () => { + logger.info( + `Processing Website Url ${websiteUrl} for Entity ${entity.metadata.name}`, + ); + + await lighthouseApi.triggerAudit({ + url: websiteUrl, + options: { + lighthouseConfig: { + settings: { + formFactor: 'mobile', + emulatedFormFactor: 'mobile', + screenEmulation: formFactorToScreenEmulationMap.mobile, + }, + }, + }, + }); + + logger.info('Stop Scheduled Task'); + controller.abort(); + }, + }); + index++; + } + }, + }); + } +} diff --git a/plugins/lighthouse-backend/src/setupTests.ts b/plugins/lighthouse-backend/src/setupTests.ts new file mode 100644 index 0000000000..d3232290a7 --- /dev/null +++ b/plugins/lighthouse-backend/src/setupTests.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export {}; diff --git a/plugins/lighthouse-common/.eslintrc.js b/plugins/lighthouse-common/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/lighthouse-common/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/lighthouse-common/CHANGELOG.md b/plugins/lighthouse-common/CHANGELOG.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/lighthouse-common/README.md b/plugins/lighthouse-common/README.md new file mode 100644 index 0000000000..9ab6d9a86d --- /dev/null +++ b/plugins/lighthouse-common/README.md @@ -0,0 +1,3 @@ +# @backstage/plugin-lighthouse-common + +Common types and functionalities for lighthouse, to be shared between lighthouse and lighthouse-backend. diff --git a/plugins/lighthouse-common/api-report.md b/plugins/lighthouse-common/api-report.md new file mode 100644 index 0000000000..fe8aa19966 --- /dev/null +++ b/plugins/lighthouse-common/api-report.md @@ -0,0 +1,286 @@ +## API Report File for "@backstage/plugin-kubernetes-common" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { Entity } from '@backstage/catalog-model'; +import type { JsonObject } from '@backstage/types'; +import { PodStatus } from '@kubernetes/client-node'; +import { V1ConfigMap } from '@kubernetes/client-node'; +import { V1CronJob } from '@kubernetes/client-node'; +import { V1DaemonSet } from '@kubernetes/client-node'; +import { V1Deployment } from '@kubernetes/client-node'; +import { V1HorizontalPodAutoscaler } from '@kubernetes/client-node'; +import { V1Ingress } from '@kubernetes/client-node'; +import { V1Job } from '@kubernetes/client-node'; +import { V1LimitRange } from '@kubernetes/client-node'; +import { V1Pod } from '@kubernetes/client-node'; +import { V1ReplicaSet } from '@kubernetes/client-node'; +import { V1Service } from '@kubernetes/client-node'; +import { V1StatefulSet } from '@kubernetes/client-node'; + +// @public (undocumented) +export type AuthProviderType = 'google' | 'serviceAccount' | 'aws' | 'azure'; + +// @public (undocumented) +export interface ClientContainerStatus { + // (undocumented) + container: string; + // (undocumented) + cpuUsage: ClientCurrentResourceUsage; + // (undocumented) + memoryUsage: ClientCurrentResourceUsage; +} + +// @public (undocumented) +export interface ClientCurrentResourceUsage { + // (undocumented) + currentUsage: number | string; + // (undocumented) + limitTotal: number | string; + // (undocumented) + requestTotal: number | string; +} + +// @public (undocumented) +export interface ClientPodStatus { + // (undocumented) + containers: ClientContainerStatus[]; + // (undocumented) + cpu: ClientCurrentResourceUsage; + // (undocumented) + memory: ClientCurrentResourceUsage; + // (undocumented) + pod: V1Pod; +} + +// @public (undocumented) +export interface ClusterAttributes { + dashboardApp?: string; + dashboardParameters?: JsonObject; + dashboardUrl?: string; + name: string; +} + +// @public (undocumented) +export interface ClusterObjects { + // (undocumented) + cluster: ClusterAttributes; + // (undocumented) + errors: KubernetesFetchError[]; + // (undocumented) + podMetrics: ClientPodStatus[]; + // (undocumented) + resources: FetchResponse[]; +} + +// @public (undocumented) +export interface ConfigMapFetchResponse { + // (undocumented) + resources: Array; + // (undocumented) + type: 'configmaps'; +} + +// @public (undocumented) +export interface CronJobsFetchResponse { + // (undocumented) + resources: Array; + // (undocumented) + type: 'cronjobs'; +} + +// @public (undocumented) +export interface CustomObjectsByEntityRequest { + // (undocumented) + auth: KubernetesRequestAuth; + // (undocumented) + customResources: CustomResourceMatcher[]; + // (undocumented) + entity: Entity; +} + +// @public (undocumented) +export interface CustomResourceFetchResponse { + // (undocumented) + resources: Array; + // (undocumented) + type: 'customresources'; +} + +// @public (undocumented) +export interface CustomResourceMatcher { + // (undocumented) + apiVersion: string; + // (undocumented) + group: string; + // (undocumented) + plural: string; +} + +// @public (undocumented) +export interface DaemonSetsFetchResponse { + // (undocumented) + resources: Array; + // (undocumented) + type: 'daemonsets'; +} + +// @public (undocumented) +export interface DeploymentFetchResponse { + // (undocumented) + resources: Array; + // (undocumented) + type: 'deployments'; +} + +// @public (undocumented) +export type FetchResponse = + | PodFetchResponse + | ServiceFetchResponse + | ConfigMapFetchResponse + | DeploymentFetchResponse + | LimitRangeFetchReponse + | ReplicaSetsFetchResponse + | HorizontalPodAutoscalersFetchResponse + | JobsFetchResponse + | CronJobsFetchResponse + | IngressesFetchResponse + | CustomResourceFetchResponse + | StatefulSetsFetchResponse + | DaemonSetsFetchResponse + | PodStatusFetchResponse; + +// @public (undocumented) +export interface HorizontalPodAutoscalersFetchResponse { + // (undocumented) + resources: Array; + // (undocumented) + type: 'horizontalpodautoscalers'; +} + +// @public (undocumented) +export interface IngressesFetchResponse { + // (undocumented) + resources: Array; + // (undocumented) + type: 'ingresses'; +} + +// @public (undocumented) +export interface JobsFetchResponse { + // (undocumented) + resources: Array; + // (undocumented) + type: 'jobs'; +} + +// @public (undocumented) +export type KubernetesErrorTypes = + | 'BAD_REQUEST' + | 'UNAUTHORIZED_ERROR' + | 'NOT_FOUND' + | 'SYSTEM_ERROR' + | 'UNKNOWN_ERROR'; + +// @public (undocumented) +export type KubernetesFetchError = StatusError | RawFetchError; + +// @public (undocumented) +export interface KubernetesRequestAuth { + // (undocumented) + google?: string; + // (undocumented) + oidc?: { + [key: string]: string; + }; +} + +// @public (undocumented) +export interface KubernetesRequestBody { + // (undocumented) + auth?: KubernetesRequestAuth; + // (undocumented) + entity: Entity; +} + +// @public (undocumented) +export interface LimitRangeFetchReponse { + // (undocumented) + resources: Array; + // (undocumented) + type: 'limitranges'; +} + +// @public (undocumented) +export interface ObjectsByEntityResponse { + // (undocumented) + items: ClusterObjects[]; +} + +// @public (undocumented) +export interface PodFetchResponse { + // (undocumented) + resources: Array; + // (undocumented) + type: 'pods'; +} + +// @public (undocumented) +export interface PodStatusFetchResponse { + // (undocumented) + resources: Array; + // (undocumented) + type: 'podstatus'; +} + +// @public (undocumented) +export interface RawFetchError { + // (undocumented) + errorType: 'FETCH_ERROR'; + // (undocumented) + message: string; +} + +// @public (undocumented) +export interface ReplicaSetsFetchResponse { + // (undocumented) + resources: Array; + // (undocumented) + type: 'replicasets'; +} + +// @public (undocumented) +export interface ServiceFetchResponse { + // (undocumented) + resources: Array; + // (undocumented) + type: 'services'; +} + +// @public (undocumented) +export interface StatefulSetsFetchResponse { + // (undocumented) + resources: Array; + // (undocumented) + type: 'statefulsets'; +} + +// @public (undocumented) +export interface StatusError { + // (undocumented) + errorType: KubernetesErrorTypes; + // (undocumented) + resourcePath?: string; + // (undocumented) + statusCode?: number; +} + +// @public (undocumented) +export interface WorkloadsByEntityRequest { + // (undocumented) + auth: KubernetesRequestAuth; + // (undocumented) + entity: Entity; +} +``` diff --git a/plugins/lighthouse-common/package.json b/plugins/lighthouse-common/package.json new file mode 100644 index 0000000000..f8c4cbc798 --- /dev/null +++ b/plugins/lighthouse-common/package.json @@ -0,0 +1,51 @@ +{ + "name": "@backstage/plugin-lighthouse-common", + "description": "Common functionalities for lighthouse, to be shared between lighthouse and lighthouse-backend 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/lighthouse-common" + }, + "keywords": [ + "lighthouse" + ], + "files": [ + "dist" + ], + "scripts": { + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean" + }, + "bugs": { + "url": "https://github.com/backstage/backstage/issues" + }, + "dependencies": { + "@backstage/config": "workspace:^" + }, + "devDependencies": { + "@backstage/cli": "workspace:^" + }, + "jest": { + "roots": [ + ".." + ] + } +} diff --git a/plugins/lighthouse-common/src/api.ts b/plugins/lighthouse-common/src/api.ts new file mode 100644 index 0000000000..0abd6dacdc --- /dev/null +++ b/plugins/lighthouse-common/src/api.ts @@ -0,0 +1,188 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Config } from '@backstage/config'; + +/** @public */ +export type LighthouseCategoryId = + | 'pwa' + | 'seo' + | 'performance' + | 'accessibility' + | 'best-practices'; + +/** @public */ +export interface LighthouseCategoryAbbr { + id: LighthouseCategoryId; + score: number; + title: string; +} + +/** @public */ +export interface LASListRequest { + offset?: number; + limit?: number; +} + +/** @public */ +export interface LASListResponse { + items: Item[]; + total: number; + offset: number; + limit: number; +} + +/** @public */ +export interface AuditBase { + id: string; + url: string; + timeCreated: string; +} + +/** @public */ +export interface AuditRunning extends AuditBase { + status: 'RUNNING'; +} + +/** @public */ +export interface AuditFailed extends AuditBase { + status: 'FAILED'; + timeCompleted: string; +} + +/** @public */ +export interface AuditCompleted extends AuditBase { + status: 'COMPLETED'; + timeCompleted: string; + report: Object; + categories: Record; +} + +/** @public */ +export type Audit = AuditRunning | AuditFailed | AuditCompleted; + +/** @public */ +export interface Website { + url: string; + audits: Audit[]; + lastAudit: Audit; +} + +/** @public */ +export type WebsiteListResponse = LASListResponse; + +/** @public */ +export type FormFactor = 'mobile' | 'desktop'; + +/** @public */ +export type LighthouseConfigSettings = { + // For lighthouse 7+ + formFactor: FormFactor; + screenEmulation: + | { + mobile: boolean; + width: number; + height: number; + deviceScaleFactor: number; + disabled: boolean; + } + | undefined; + // For lighthouse before 7 + emulatedFormFactor: FormFactor; +}; + +/** @public */ +export interface TriggerAuditPayload { + url: string; + options: { + lighthouseConfig: { + settings: LighthouseConfigSettings; + }; + }; +} + +/** @public */ +export class FetchError extends Error { + get name(): string { + return this.constructor.name; + } + + static async forResponse(resp: Response): Promise { + return new FetchError( + `Request failed with status code ${ + resp.status + }.\nReason: ${await resp.text()}`, + ); + } +} + +/** @public */ +export type LighthouseApi = { + url: string; + getWebsiteList: (listOptions: LASListRequest) => Promise; + getWebsiteForAuditId: (auditId: string) => Promise; + triggerAudit: (payload: TriggerAuditPayload) => Promise; + getWebsiteByUrl: (websiteUrl: string) => Promise; +}; + +/** @public */ +export class LighthouseRestApi implements LighthouseApi { + static fromConfig(config: Config) { + return new LighthouseRestApi(config.getString('lighthouse.baseUrl')); + } + + constructor(public url: string) {} + + private async fetch(input: string, init?: RequestInit): Promise { + const resp = await fetch(`${this.url}${input}`, init); + if (!resp.ok) throw await FetchError.forResponse(resp); + return await resp.json(); + } + + async getWebsiteList( + options: LASListRequest = {}, + ): Promise { + const { limit, offset } = options; + const params = new URLSearchParams(); + if (typeof limit === 'number') params.append('limit', limit.toString()); + if (typeof offset === 'number') params.append('offset', offset.toString()); + return await this.fetch( + `/v1/websites?${params.toString()}`, + ); + } + + async getWebsiteForAuditId(auditId: string): Promise { + return await this.fetch( + `/v1/audits/${encodeURIComponent(auditId)}/website`, + ); + } + + async triggerAudit(payload: TriggerAuditPayload): Promise { + return await this.fetch('/v1/audits', { + method: 'POST', + body: JSON.stringify(payload), + headers: { + 'Content-Type': 'application/json', + }, + }); + } + + async getWebsiteByUrl(websiteUrl: string): Promise { + return this.fetch( + `/v1/websites/${encodeURIComponent(websiteUrl)}`, + ); + } +} diff --git a/plugins/lighthouse-common/src/index.ts b/plugins/lighthouse-common/src/index.ts new file mode 100644 index 0000000000..41b839087f --- /dev/null +++ b/plugins/lighthouse-common/src/index.ts @@ -0,0 +1,23 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Common functionalities for Lighthouse, to be shared between the `lighthouse` and `lighthouse-backend` plugins + * + * @packageDocumentation + */ + +export * from './api'; diff --git a/plugins/lighthouse/README.md b/plugins/lighthouse/README.md index f07041d9ff..1d2385e4c4 100644 --- a/plugins/lighthouse/README.md +++ b/plugins/lighthouse/README.md @@ -105,3 +105,7 @@ const overviewContent = ( ``` + +## Schedule + +If you want to run automated scheduled runs, you can install the [@backstage/lighthouse-backend](../lighthouse-backend/README.md) plugin diff --git a/plugins/lighthouse/package.json b/plugins/lighthouse/package.json index 4e67412b17..2115e7a0d6 100644 --- a/plugins/lighthouse/package.json +++ b/plugins/lighthouse/package.json @@ -38,6 +38,7 @@ "@backstage/core-components": "workspace:^", "@backstage/core-plugin-api": "workspace:^", "@backstage/plugin-catalog-react": "workspace:^", + "@backstage/plugin-lighthouse-common": "workspace:^", "@backstage/theme": "workspace:^", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", diff --git a/plugins/lighthouse/src/api.ts b/plugins/lighthouse/src/api.ts index a7197c4b96..8912f4838b 100644 --- a/plugins/lighthouse/src/api.ts +++ b/plugins/lighthouse/src/api.ts @@ -14,181 +14,10 @@ * limitations under the License. */ -import { Config } from '@backstage/config'; import { createApiRef } from '@backstage/core-plugin-api'; - -/** @public */ -export type LighthouseCategoryId = - | 'pwa' - | 'seo' - | 'performance' - | 'accessibility' - | 'best-practices'; - -/** @public */ -export interface LighthouseCategoryAbbr { - id: LighthouseCategoryId; - score: number; - title: string; -} - -/** @public */ -export interface LASListRequest { - offset?: number; - limit?: number; -} - -/** @public */ -export interface LASListResponse { - items: Item[]; - total: number; - offset: number; - limit: number; -} - -/** @public */ -export interface AuditBase { - id: string; - url: string; - timeCreated: string; -} - -/** @public */ -export interface AuditRunning extends AuditBase { - status: 'RUNNING'; -} - -/** @public */ -export interface AuditFailed extends AuditBase { - status: 'FAILED'; - timeCompleted: string; -} - -/** @public */ -export interface AuditCompleted extends AuditBase { - status: 'COMPLETED'; - timeCompleted: string; - report: Object; - categories: Record; -} - -/** @public */ -export type Audit = AuditRunning | AuditFailed | AuditCompleted; - -/** @public */ -export interface Website { - url: string; - audits: Audit[]; - lastAudit: Audit; -} - -/** @public */ -export type WebsiteListResponse = LASListResponse; - -/** @public */ -export type FormFactor = 'mobile' | 'desktop'; - -/** @public */ -export type LighthouseConfigSettings = { - // For lighthouse 7+ - formFactor: FormFactor; - screenEmulation: - | { - mobile: boolean; - width: number; - height: number; - deviceScaleFactor: number; - disabled: boolean; - } - | undefined; - // For lighthouse before 7 - emulatedFormFactor: FormFactor; -}; - -/** @public */ -export interface TriggerAuditPayload { - url: string; - options: { - lighthouseConfig: { - settings: LighthouseConfigSettings; - }; - }; -} - -/** @public */ -export class FetchError extends Error { - get name(): string { - return this.constructor.name; - } - - static async forResponse(resp: Response): Promise { - return new FetchError( - `Request failed with status code ${ - resp.status - }.\nReason: ${await resp.text()}`, - ); - } -} - -/** @public */ -export type LighthouseApi = { - url: string; - getWebsiteList: (listOptions: LASListRequest) => Promise; - getWebsiteForAuditId: (auditId: string) => Promise; - triggerAudit: (payload: TriggerAuditPayload) => Promise; - getWebsiteByUrl: (websiteUrl: string) => Promise; -}; +import { LighthouseApi } from '@backstage/plugin-lighthouse-common'; /** @public */ export const lighthouseApiRef = createApiRef({ id: 'plugin.lighthouse.service', }); - -/** @public */ -export class LighthouseRestApi implements LighthouseApi { - static fromConfig(config: Config) { - return new LighthouseRestApi(config.getString('lighthouse.baseUrl')); - } - - constructor(public url: string) {} - - private async fetch(input: string, init?: RequestInit): Promise { - const resp = await fetch(`${this.url}${input}`, init); - if (!resp.ok) throw await FetchError.forResponse(resp); - return await resp.json(); - } - - async getWebsiteList( - options: LASListRequest = {}, - ): Promise { - const { limit, offset } = options; - const params = new URLSearchParams(); - if (typeof limit === 'number') params.append('limit', limit.toString()); - if (typeof offset === 'number') params.append('offset', offset.toString()); - return await this.fetch( - `/v1/websites?${params.toString()}`, - ); - } - - async getWebsiteForAuditId(auditId: string): Promise { - return await this.fetch( - `/v1/audits/${encodeURIComponent(auditId)}/website`, - ); - } - - async triggerAudit(payload: TriggerAuditPayload): Promise { - return await this.fetch('/v1/audits', { - method: 'POST', - body: JSON.stringify(payload), - headers: { - 'Content-Type': 'application/json', - }, - }); - } - - async getWebsiteByUrl(websiteUrl: string): Promise { - return this.fetch( - `/v1/websites/${encodeURIComponent(websiteUrl)}`, - ); - } -} diff --git a/plugins/lighthouse/src/components/AuditList/AuditListForEntity.test.tsx b/plugins/lighthouse/src/components/AuditList/AuditListForEntity.test.tsx index 59892e1475..d1ed72c3fb 100644 --- a/plugins/lighthouse/src/components/AuditList/AuditListForEntity.test.tsx +++ b/plugins/lighthouse/src/components/AuditList/AuditListForEntity.test.tsx @@ -19,10 +19,10 @@ import { EntityProvider } from '@backstage/plugin-catalog-react'; import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import React from 'react'; import { - lighthouseApiRef, LighthouseRestApi, WebsiteListResponse, -} from '../../api'; +} from '@backstage/plugin-lighthouse-common'; +import { lighthouseApiRef } from '../../api'; import { useWebsiteForEntity } from '../../hooks/useWebsiteForEntity'; import * as data from '../../__fixtures__/website-list-response.json'; import { AuditListForEntity } from './AuditListForEntity'; diff --git a/plugins/lighthouse/src/components/AuditList/AuditListTable.test.tsx b/plugins/lighthouse/src/components/AuditList/AuditListTable.test.tsx index c2ee202841..45889a2cd3 100644 --- a/plugins/lighthouse/src/components/AuditList/AuditListTable.test.tsx +++ b/plugins/lighthouse/src/components/AuditList/AuditListTable.test.tsx @@ -25,9 +25,9 @@ import AuditListTable from './AuditListTable'; import { WebsiteListResponse, - lighthouseApiRef, LighthouseRestApi, -} from '../../api'; +} from '@backstage/plugin-lighthouse-common'; +import { lighthouseApiRef } from '../../api'; import { formatTime } from '../../utils'; import { setupServer } from 'msw/node'; import * as data from '../../__fixtures__/website-list-response.json'; diff --git a/plugins/lighthouse/src/components/AuditList/AuditListTable.tsx b/plugins/lighthouse/src/components/AuditList/AuditListTable.tsx index 9f5fa94436..8f718cda64 100644 --- a/plugins/lighthouse/src/components/AuditList/AuditListTable.tsx +++ b/plugins/lighthouse/src/components/AuditList/AuditListTable.tsx @@ -15,7 +15,8 @@ */ import React, { useState, useEffect } from 'react'; import Typography from '@material-ui/core/Typography'; -import { Website, lighthouseApiRef } from '../../api'; +import { Website } from '@backstage/plugin-lighthouse-common'; +import { lighthouseApiRef } from '../../api'; import useInterval from 'react-use/lib/useInterval'; import { formatTime, diff --git a/plugins/lighthouse/src/components/AuditList/index.test.tsx b/plugins/lighthouse/src/components/AuditList/index.test.tsx index 2b422cce7a..55bde9aebd 100644 --- a/plugins/lighthouse/src/components/AuditList/index.test.tsx +++ b/plugins/lighthouse/src/components/AuditList/index.test.tsx @@ -33,10 +33,10 @@ import { rest } from 'msw'; import { setupServer } from 'msw/node'; import React from 'react'; import { - lighthouseApiRef, LighthouseRestApi, WebsiteListResponse, -} from '../../api'; +} from '@backstage/plugin-lighthouse-common'; +import { lighthouseApiRef } from '../../api'; import * as data from '../../__fixtures__/website-list-response.json'; import AuditList from './index'; import { ApiProvider } from '@backstage/core-app-api'; diff --git a/plugins/lighthouse/src/components/AuditStatusIcon/index.tsx b/plugins/lighthouse/src/components/AuditStatusIcon/index.tsx index 52ba2e00cf..b0e1c876ae 100644 --- a/plugins/lighthouse/src/components/AuditStatusIcon/index.tsx +++ b/plugins/lighthouse/src/components/AuditStatusIcon/index.tsx @@ -15,7 +15,7 @@ */ import React from 'react'; -import { Audit } from '../../api'; +import { Audit } from '@backstage/plugin-lighthouse-common'; import { StatusError, StatusOK, diff --git a/plugins/lighthouse/src/components/AuditView/index.test.tsx b/plugins/lighthouse/src/components/AuditView/index.test.tsx index d555133f3e..7e7d8e4b7d 100644 --- a/plugins/lighthouse/src/components/AuditView/index.test.tsx +++ b/plugins/lighthouse/src/components/AuditView/index.test.tsx @@ -38,7 +38,12 @@ import { render } from '@testing-library/react'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; import React from 'react'; -import { Audit, lighthouseApiRef, LighthouseRestApi, Website } from '../../api'; +import { + Audit, + LighthouseRestApi, + Website, +} from '@backstage/plugin-lighthouse-common'; +import { lighthouseApiRef } from '../../api'; import { formatTime } from '../../utils'; import * as data from '../../__fixtures__/website-response.json'; import AuditView from './index'; diff --git a/plugins/lighthouse/src/components/AuditView/index.tsx b/plugins/lighthouse/src/components/AuditView/index.tsx index ade485b09a..8d82f920cd 100644 --- a/plugins/lighthouse/src/components/AuditView/index.tsx +++ b/plugins/lighthouse/src/components/AuditView/index.tsx @@ -33,7 +33,8 @@ import { useParams, } from 'react-router-dom'; import useAsync from 'react-use/lib/useAsync'; -import { Audit, lighthouseApiRef, Website } from '../../api'; +import { Audit, Website } from '@backstage/plugin-lighthouse-common'; +import { lighthouseApiRef } from '../../api'; import { formatTime } from '../../utils'; import AuditStatusIcon from '../AuditStatusIcon'; import LighthouseSupportButton from '../SupportButton'; diff --git a/plugins/lighthouse/src/components/Cards/LastLighthouseAuditCard.test.tsx b/plugins/lighthouse/src/components/Cards/LastLighthouseAuditCard.test.tsx index c59458f3f3..09b21fd028 100644 --- a/plugins/lighthouse/src/components/Cards/LastLighthouseAuditCard.test.tsx +++ b/plugins/lighthouse/src/components/Cards/LastLighthouseAuditCard.test.tsx @@ -22,7 +22,7 @@ import { AuditCompleted, LighthouseCategoryId, WebsiteListResponse, -} from '../../api'; +} from '@backstage/plugin-lighthouse-common'; import { useWebsiteForEntity } from '../../hooks/useWebsiteForEntity'; import * as data from '../../__fixtures__/website-list-response.json'; import { LastLighthouseAuditCard } from './LastLighthouseAuditCard'; diff --git a/plugins/lighthouse/src/components/Cards/LastLighthouseAuditCard.tsx b/plugins/lighthouse/src/components/Cards/LastLighthouseAuditCard.tsx index 1562031007..af5ce94487 100644 --- a/plugins/lighthouse/src/components/Cards/LastLighthouseAuditCard.tsx +++ b/plugins/lighthouse/src/components/Cards/LastLighthouseAuditCard.tsx @@ -15,7 +15,11 @@ */ import React from 'react'; import Typography from '@material-ui/core/Typography'; -import { Audit, AuditCompleted, LighthouseCategoryId } from '../../api'; +import { + Audit, + AuditCompleted, + LighthouseCategoryId, +} from '@backstage/plugin-lighthouse-common'; import { useWebsiteForEntity } from '../../hooks/useWebsiteForEntity'; import AuditStatusIcon from '../AuditStatusIcon'; import { diff --git a/plugins/lighthouse/src/components/CreateAudit/index.test.tsx b/plugins/lighthouse/src/components/CreateAudit/index.test.tsx index 60a7bc1301..1b394a30f0 100644 --- a/plugins/lighthouse/src/components/CreateAudit/index.test.tsx +++ b/plugins/lighthouse/src/components/CreateAudit/index.test.tsx @@ -32,7 +32,8 @@ import { fireEvent, render, waitFor } from '@testing-library/react'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; import React from 'react'; -import { Audit, lighthouseApiRef, LighthouseRestApi } from '../../api'; +import { Audit, LighthouseRestApi } from '@backstage/plugin-lighthouse-common'; +import { lighthouseApiRef } from '../../api'; import * as data from '../../__fixtures__/create-audit-response.json'; import CreateAudit from './index'; diff --git a/plugins/lighthouse/src/components/CreateAudit/index.tsx b/plugins/lighthouse/src/components/CreateAudit/index.tsx index 8ed7dc8996..a76b031796 100644 --- a/plugins/lighthouse/src/components/CreateAudit/index.tsx +++ b/plugins/lighthouse/src/components/CreateAudit/index.tsx @@ -27,9 +27,9 @@ import React, { useCallback, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { FormFactor, - lighthouseApiRef, LighthouseConfigSettings, -} from '../../api'; +} from '@backstage/plugin-lighthouse-common'; +import { lighthouseApiRef } from '../../api'; import { useQuery } from '../../utils'; import LighthouseSupportButton from '../SupportButton'; diff --git a/plugins/lighthouse/src/hooks/useWebsiteForEntity.test.tsx b/plugins/lighthouse/src/hooks/useWebsiteForEntity.test.tsx index 13da0f6f4f..7ab717c57d 100644 --- a/plugins/lighthouse/src/hooks/useWebsiteForEntity.test.tsx +++ b/plugins/lighthouse/src/hooks/useWebsiteForEntity.test.tsx @@ -17,7 +17,8 @@ import { Entity } from '@backstage/catalog-model'; import { EntityProvider } from '@backstage/plugin-catalog-react'; import { renderHook } from '@testing-library/react-hooks'; import React, { PropsWithChildren } from 'react'; -import { lighthouseApiRef, WebsiteListResponse } from '../api'; +import { WebsiteListResponse } from '@backstage/plugin-lighthouse-common'; +import { lighthouseApiRef } from '../api'; import * as data from '../__fixtures__/website-list-response.json'; import { useWebsiteForEntity } from './useWebsiteForEntity'; diff --git a/plugins/lighthouse/src/plugin.ts b/plugins/lighthouse/src/plugin.ts index 24052f7771..3458ae7240 100644 --- a/plugins/lighthouse/src/plugin.ts +++ b/plugins/lighthouse/src/plugin.ts @@ -14,7 +14,8 @@ * limitations under the License. */ -import { lighthouseApiRef, LighthouseRestApi } from './api'; +import { LighthouseRestApi } from '@backstage/plugin-lighthouse-common'; +import { lighthouseApiRef } from './api'; import { createPlugin, createRouteRef, diff --git a/plugins/lighthouse/src/utils.ts b/plugins/lighthouse/src/utils.ts index b255f2f90e..918995386d 100644 --- a/plugins/lighthouse/src/utils.ts +++ b/plugins/lighthouse/src/utils.ts @@ -14,7 +14,12 @@ * limitations under the License. */ import { useLocation } from 'react-router-dom'; -import { Website, Audit, LighthouseCategoryId, AuditCompleted } from './api'; +import { + Website, + Audit, + LighthouseCategoryId, + AuditCompleted, +} from '@backstage/plugin-lighthouse-common'; export function useQuery(): URLSearchParams { return new URLSearchParams(useLocation().search); diff --git a/yarn.lock b/yarn.lock index 7cc0dbd8b5..c546289bb3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6589,6 +6589,31 @@ __metadata: languageName: unknown linkType: soft +"@backstage/plugin-lighthouse-backend@workspace:^, @backstage/plugin-lighthouse-backend@workspace:plugins/lighthouse-backend": + version: 0.0.0-use.local + resolution: "@backstage/plugin-lighthouse-backend@workspace:plugins/lighthouse-backend" + dependencies: + "@backstage/backend-common": "workspace:^" + "@backstage/backend-tasks": "workspace:^" + "@backstage/catalog-client": "workspace:^" + "@backstage/catalog-model": "workspace:^" + "@backstage/cli": "workspace:^" + "@backstage/config": "workspace:^" + "@backstage/plugin-lighthouse-common": "workspace:^" + "@backstage/types": "workspace:^" + winston: ^3.2.1 + languageName: unknown + linkType: soft + +"@backstage/plugin-lighthouse-common@workspace:^, @backstage/plugin-lighthouse-common@workspace:plugins/lighthouse-common": + version: 0.0.0-use.local + resolution: "@backstage/plugin-lighthouse-common@workspace:plugins/lighthouse-common" + dependencies: + "@backstage/cli": "workspace:^" + "@backstage/config": "workspace:^" + languageName: unknown + linkType: soft + "@backstage/plugin-lighthouse@workspace:^, @backstage/plugin-lighthouse@workspace:plugins/lighthouse": version: 0.0.0-use.local resolution: "@backstage/plugin-lighthouse@workspace:plugins/lighthouse" @@ -6601,6 +6626,7 @@ __metadata: "@backstage/core-plugin-api": "workspace:^" "@backstage/dev-utils": "workspace:^" "@backstage/plugin-catalog-react": "workspace:^" + "@backstage/plugin-lighthouse-common": "workspace:^" "@backstage/test-utils": "workspace:^" "@backstage/theme": "workspace:^" "@material-ui/core": ^4.12.2 @@ -22295,6 +22321,7 @@ __metadata: "@backstage/plugin-jenkins-backend": "workspace:^" "@backstage/plugin-kafka-backend": "workspace:^" "@backstage/plugin-kubernetes-backend": "workspace:^" + "@backstage/plugin-lighthouse-backend": "workspace:^" "@backstage/plugin-permission-backend": "workspace:^" "@backstage/plugin-permission-common": "workspace:^" "@backstage/plugin-permission-node": "workspace:^"