From 1454bf98e7a0e20cbcb7a42de7db96d0b80729ce Mon Sep 17 00:00:00 2001 From: Matthew Clarke Date: Tue, 5 Jul 2022 13:13:49 -0400 Subject: [PATCH] (feat): New kubernetes backend endpoints (#12393) * feat: add new kubernetes backend endpoints Signed-off-by: Matthew Clarke * add more tests Signed-off-by: Matthew Clarke * add eslint exceptions Signed-off-by: Matthew Clarke * register new endpoints Signed-off-by: Matthew Clarke * add changeset Signed-off-by: Matthew Clarke * add breaking change ot changeset Signed-off-by: Matthew Clarke * fix tsc in test Signed-off-by: Matthew Clarke * changeset typo Signed-off-by: Matthew Clarke * api report update Signed-off-by: Matthew Clarke --- .changeset/little-geckos-end.md | 18 + packages/backend/src/plugins/kubernetes.ts | 3 + plugins/kubernetes-backend/api-report.md | 6 + plugins/kubernetes-backend/package.json | 4 +- .../src/routes/resourceRoutes.test.ts | 545 ++++++++++++++++++ .../src/routes/resourcesRoutes.ts | 94 +++ .../src/service/KubernetesBuilder.test.ts | 11 +- .../src/service/KubernetesBuilder.ts | 14 +- .../kubernetes-backend/src/service/router.ts | 2 + .../src/service/standaloneApplication.ts | 8 +- 10 files changed, 701 insertions(+), 4 deletions(-) create mode 100644 .changeset/little-geckos-end.md create mode 100644 plugins/kubernetes-backend/src/routes/resourceRoutes.test.ts create mode 100644 plugins/kubernetes-backend/src/routes/resourcesRoutes.ts diff --git a/.changeset/little-geckos-end.md b/.changeset/little-geckos-end.md new file mode 100644 index 0000000000..328095aa44 --- /dev/null +++ b/.changeset/little-geckos-end.md @@ -0,0 +1,18 @@ +--- +'@backstage/plugin-kubernetes-backend': minor +--- + +Add new endpoints to Kubernetes backend plugin + +BREAKING: Kubernetes backend plugin now depends on CatalogApi + +```typescript +// Create new CatalogClient +const catalogApi = new CatalogClient({ discoveryApi: env.discovery }); +const { router } = await KubernetesBuilder.createBuilder({ + logger: env.logger, + config: env.config, + // Inject it into createBuilder params + catalogApi, +}).build(); +``` diff --git a/packages/backend/src/plugins/kubernetes.ts b/packages/backend/src/plugins/kubernetes.ts index 8023d549ff..3bc7a5dcbe 100644 --- a/packages/backend/src/plugins/kubernetes.ts +++ b/packages/backend/src/plugins/kubernetes.ts @@ -17,13 +17,16 @@ import { KubernetesBuilder } from '@backstage/plugin-kubernetes-backend'; import { Router } from 'express'; import { PluginEnvironment } from '../types'; +import { CatalogClient } from '@backstage/catalog-client'; export default async function createPlugin( env: PluginEnvironment, ): Promise { + const catalogApi = new CatalogClient({ discoveryApi: env.discovery }); const { router } = await KubernetesBuilder.createBuilder({ logger: env.logger, config: env.config, + catalogApi, }).build(); return router; } diff --git a/plugins/kubernetes-backend/api-report.md b/plugins/kubernetes-backend/api-report.md index adc8fb503f..363af1b02f 100644 --- a/plugins/kubernetes-backend/api-report.md +++ b/plugins/kubernetes-backend/api-report.md @@ -3,6 +3,7 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +import { CatalogApi } from '@backstage/catalog-client'; import { Config } from '@backstage/config'; import { Duration } from 'luxon'; import { Entity } from '@backstage/catalog-model'; @@ -108,6 +109,7 @@ export class KubernetesBuilder { protected buildRouter( objectsProvider: KubernetesObjectsProvider, clusterSupplier: KubernetesClustersSupplier, + catalogApi: CatalogApi, ): express.Router; // (undocumented) protected buildServiceLocator( @@ -155,6 +157,8 @@ export interface KubernetesClustersSupplier { // @alpha (undocumented) export interface KubernetesEnvironment { + // (undocumented) + catalogApi: CatalogApi; // (undocumented) config: Config; // (undocumented) @@ -268,6 +272,8 @@ export interface ObjectToFetch { // @alpha (undocumented) export interface RouterOptions { + // (undocumented) + catalogApi: CatalogApi; // (undocumented) clusterSupplier?: KubernetesClustersSupplier; // (undocumented) diff --git a/plugins/kubernetes-backend/package.json b/plugins/kubernetes-backend/package.json index e7d8d2c3c0..10eb6828f5 100644 --- a/plugins/kubernetes-backend/package.json +++ b/plugins/kubernetes-backend/package.json @@ -37,10 +37,12 @@ "dependencies": { "@azure/identity": "^2.0.4", "@backstage/backend-common": "^0.14.1-next.2", + "@backstage/catalog-client": "^1.0.4-next.1", "@backstage/catalog-model": "^1.1.0-next.2", "@backstage/config": "^1.0.1", "@backstage/errors": "^1.1.0-next.0", - "@backstage/plugin-kubernetes-common": "^0.4.0-next.1", + "@backstage/plugin-auth-node": "^0.2.3-next.1", + "@backstage/plugin-kubernetes-common": "^0.4.0-next.0", "@google-cloud/container": "^4.0.0", "@kubernetes/client-node": "^0.16.0", "@types/express": "^4.17.6", diff --git a/plugins/kubernetes-backend/src/routes/resourceRoutes.test.ts b/plugins/kubernetes-backend/src/routes/resourceRoutes.test.ts new file mode 100644 index 0000000000..57e6514a96 --- /dev/null +++ b/plugins/kubernetes-backend/src/routes/resourceRoutes.test.ts @@ -0,0 +1,545 @@ +/* + * 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 request from 'supertest'; +import Router from 'express-promise-router'; +import { addResourceRoutesToRouter } from './resourcesRoutes'; +import { Entity } from '@backstage/catalog-model'; + +describe('resourcesRoutes', () => { + let app: express.Express; + + beforeAll(() => { + app = express(); + app.use(express.json()); + const router = Router(); + addResourceRoutesToRouter( + router, + { + getEntityByRef: jest.fn().mockImplementation(entityRef => { + if (entityRef.name === 'noentity') { + return Promise.resolve(undefined); + } + return Promise.resolve({ + kind: entityRef.kind, + metadata: { + name: entityRef.name, + namespace: entityRef.namespace, + }, + } as Entity); + }), + } as any, + { + getKubernetesObjectsByEntity: jest.fn().mockImplementation(args => { + if (args.entity.metadata.name === 'inject500') { + return Promise.reject(new Error('some internal error')); + } + + return Promise.resolve({ + items: [ + { + clusterOne: { + pods: [ + { + metadata: { + name: 'pod1', + }, + }, + ], + }, + }, + ], + }); + }), + getCustomResourcesByEntity: jest.fn().mockImplementation(args => { + if (args.entity.metadata.name === 'inject500') { + return Promise.reject(new Error('some internal error')); + } + + return Promise.resolve({ + items: [ + { + clusterOne: { + pods: [ + { + metadata: { + name: 'pod1', + }, + }, + ], + }, + }, + ], + }); + }), + } as any, + ); + app.use('/', router); + app.use(errorHandler()); + }); + + describe('POST /resources/workloads/query', () => { + // eslint-disable-next-line jest/expect-expect + it('200 happy path', async () => { + await request(app) + .post('/resources/workloads/query') + .send({ + entityRef: 'kind:namespacec/someComponent', + auth: { + google: 'something', + }, + }) + .set('Content-Type', 'application/json') + .set('Authorization', 'Bearer Zm9vYmFy') + .expect(200, { + items: [ + { + clusterOne: { + pods: [ + { + metadata: { + name: 'pod1', + }, + }, + ], + }, + }, + ], + }); + }); + // eslint-disable-next-line jest/expect-expect + it('400 when missing entity ref', async () => { + await request(app) + .post('/resources/workloads/query') + .send({ + auth: { + google: 'something', + }, + }) + .set('Content-Type', 'application/json') + .set('Authorization', 'Bearer Zm9vYmFy') + .expect(400, { + error: { name: 'InputError', message: 'entity is a required field' }, + request: { + method: 'POST', + url: '/resources/workloads/query', + }, + response: { statusCode: 400 }, + }); + }); + // eslint-disable-next-line jest/expect-expect + it('400 when bad entity ref', async () => { + await request(app) + .post('/resources/workloads/query') + .send({ + entityRef: 'ffff', + auth: { + google: 'something', + }, + }) + .set('Content-Type', 'application/json') + .set('Authorization', 'Bearer Zm9vYmFy') + .expect(400, { + error: { + name: 'InputError', + message: + 'Invalid entity ref, Error: Entity reference "ffff" had missing or empty kind (e.g. did not start with "component:" or similar)', + }, + request: { + method: 'POST', + url: '/resources/workloads/query', + }, + response: { statusCode: 400 }, + }); + }); + // eslint-disable-next-line jest/expect-expect + it('400 when no entity in catalog', async () => { + await request(app) + .post('/resources/workloads/query') + .send({ + entityRef: 'noentity:noentity', + auth: { + google: 'something', + }, + }) + .set('Content-Type', 'application/json') + .set('Authorization', 'Bearer Zm9vYmFy') + .expect(400, { + error: { + name: 'InputError', + message: 'Entity ref missing, noentity:default/noentity', + }, + request: { + method: 'POST', + url: '/resources/workloads/query', + }, + response: { statusCode: 400 }, + }); + }); + // eslint-disable-next-line jest/expect-expect + it('401 when no Auth header', async () => { + await request(app) + .post('/resources/workloads/query') + .send({ + entityRef: 'component:someComponent', + auth: { + google: 'something', + }, + }) + .set('Content-Type', 'application/json') + .expect(401, { + error: { name: 'AuthenticationError', message: 'No Backstage token' }, + request: { + method: 'POST', + url: '/resources/workloads/query', + }, + response: { statusCode: 401 }, + }); + }); + // eslint-disable-next-line jest/expect-expect + it('401 when invalid Auth header', async () => { + await request(app) + .post('/resources/workloads/query') + .send({ + entityRef: 'component:someComponent', + auth: { + google: 'something', + }, + }) + .set('Content-Type', 'application/json') + .set('Authorization', 'ffffff') + .expect(401, { + error: { name: 'AuthenticationError', message: 'No Backstage token' }, + request: { + method: 'POST', + url: '/resources/workloads/query', + }, + response: { statusCode: 401 }, + }); + }); + // eslint-disable-next-line jest/expect-expect + it('500 handle gracefully', async () => { + await request(app) + .post('/resources/workloads/query') + .send({ + entityRef: 'inject500:inject500/inject500', + auth: { + google: 'something', + }, + }) + .set('Content-Type', 'application/json') + .set('Authorization', 'Bearer Zm9vYmFy') + .expect(500, { + error: { + name: 'Error', + message: 'some internal error', + level: 'error', + service: 'backstage', + }, + request: { method: 'POST', url: '/resources/workloads/query' }, + response: { statusCode: 500 }, + }); + }); + }); + describe('POST /resources/custom/query', () => { + // eslint-disable-next-line jest/expect-expect + it('200 happy path', async () => { + await request(app) + .post('/resources/custom/query') + .send({ + entityRef: 'component:someComponent', + auth: { + google: 'something', + }, + customResources: [ + { + group: 'someGroup', + apiVersion: 'someApiVersion', + plural: 'somePlural', + }, + ], + }) + .set('Content-Type', 'application/json') + .set('Authorization', 'Bearer Zm9vYmFy') + .expect(200, { + items: [ + { + clusterOne: { + pods: [ + { + metadata: { + name: 'pod1', + }, + }, + ], + }, + }, + ], + }); + }); + // eslint-disable-next-line jest/expect-expect + it('400 when missing custom resources', async () => { + await request(app) + .post('/resources/custom/query') + .send({ + entityRef: 'component:someComponent', + auth: { + google: 'something', + }, + }) + .set('Content-Type', 'application/json') + .set('Authorization', 'Bearer Zm9vYmFy') + .expect(400, { + error: { + name: 'InputError', + message: 'customResources is a required field', + }, + request: { + method: 'POST', + url: '/resources/custom/query', + }, + response: { statusCode: 400 }, + }); + }); + // eslint-disable-next-line jest/expect-expect + it('400 when custom resources not array', async () => { + await request(app) + .post('/resources/custom/query') + .send({ + entityRef: 'component:someComponent', + auth: { + google: 'something', + }, + customResources: 'somestring', + }) + .set('Content-Type', 'application/json') + .set('Authorization', 'Bearer Zm9vYmFy') + .expect(400, { + error: { + name: 'InputError', + message: 'customResources must be an array', + }, + request: { + method: 'POST', + url: '/resources/custom/query', + }, + response: { statusCode: 400 }, + }); + }); + // eslint-disable-next-line jest/expect-expect + it('400 when custom resources empty', async () => { + await request(app) + .post('/resources/custom/query') + .send({ + entityRef: 'component:someComponent', + auth: { + google: 'something', + }, + customResources: [], + }) + .set('Content-Type', 'application/json') + .set('Authorization', 'Bearer Zm9vYmFy') + .expect(400, { + error: { + name: 'InputError', + message: 'at least 1 customResource is required', + }, + request: { + method: 'POST', + url: '/resources/custom/query', + }, + response: { statusCode: 400 }, + }); + }); + // eslint-disable-next-line jest/expect-expect + it('400 when missing entity ref', async () => { + await request(app) + .post('/resources/custom/query') + .send({ + auth: { + google: 'something', + }, + customResources: [ + { + group: 'someGroup', + apiVersion: 'someApiVersion', + plural: 'somePlural', + }, + ], + }) + .set('Content-Type', 'application/json') + .set('Authorization', 'Bearer Zm9vYmFy') + .expect(400, { + error: { name: 'InputError', message: 'entity is a required field' }, + request: { + method: 'POST', + url: '/resources/custom/query', + }, + response: { statusCode: 400 }, + }); + }); + // eslint-disable-next-line jest/expect-expect + it('400 when bad entity ref', async () => { + await request(app) + .post('/resources/custom/query') + .send({ + entityRef: 'ffff', + auth: { + google: 'something', + }, + customResources: [ + { + group: 'someGroup', + apiVersion: 'someApiVersion', + plural: 'somePlural', + }, + ], + }) + .set('Content-Type', 'application/json') + .set('Authorization', 'Bearer Zm9vYmFy') + .expect(400, { + error: { + name: 'InputError', + message: + 'Invalid entity ref, Error: Entity reference "ffff" had missing or empty kind (e.g. did not start with "component:" or similar)', + }, + request: { + method: 'POST', + url: '/resources/custom/query', + }, + response: { statusCode: 400 }, + }); + }); + // eslint-disable-next-line jest/expect-expect + it('400 when no entity in catalog', async () => { + await request(app) + .post('/resources/custom/query') + .send({ + entityRef: 'noentity:noentity', + auth: { + google: 'something', + }, + customResources: [ + { + group: 'someGroup', + apiVersion: 'someApiVersion', + plural: 'somePlural', + }, + ], + }) + .set('Content-Type', 'application/json') + .set('Authorization', 'Bearer Zm9vYmFy') + .expect(400, { + error: { + name: 'InputError', + message: 'Entity ref missing, noentity:default/noentity', + }, + request: { + method: 'POST', + url: '/resources/custom/query', + }, + response: { statusCode: 400 }, + }); + }); + // eslint-disable-next-line jest/expect-expect + it('401 when no Auth header', async () => { + await request(app) + .post('/resources/custom/query') + .send({ + entityRef: 'component:someComponent', + auth: { + google: 'something', + }, + customResources: [ + { + group: 'someGroup', + apiVersion: 'someApiVersion', + plural: 'somePlural', + }, + ], + }) + .set('Content-Type', 'application/json') + .expect(401, { + error: { name: 'AuthenticationError', message: 'No Backstage token' }, + request: { + method: 'POST', + url: '/resources/custom/query', + }, + response: { statusCode: 401 }, + }); + }); + // eslint-disable-next-line jest/expect-expect + it('401 when invalid Auth header', async () => { + await request(app) + .post('/resources/custom/query') + .send({ + entityRef: 'component:someComponent', + auth: { + google: 'something', + }, + customResources: [ + { + group: 'someGroup', + apiVersion: 'someApiVersion', + plural: 'somePlural', + }, + ], + }) + .set('Content-Type', 'application/json') + .set('Authorization', 'ffffff') + .expect(401, { + error: { name: 'AuthenticationError', message: 'No Backstage token' }, + request: { + method: 'POST', + url: '/resources/custom/query', + }, + response: { statusCode: 401 }, + }); + }); + // eslint-disable-next-line jest/expect-expect + it('500 handle gracefully', async () => { + await request(app) + .post('/resources/custom/query') + .send({ + entityRef: 'inject500:inject500/inject500', + auth: { + google: 'something', + }, + customResources: [ + { + group: 'someGroup', + apiVersion: 'someApiVersion', + plural: 'somePlural', + }, + ], + }) + .set('Content-Type', 'application/json') + .set('Authorization', 'Bearer Zm9vYmFy') + .expect(500, { + error: { + name: 'Error', + message: 'some internal error', + level: 'error', + service: 'backstage', + }, + request: { method: 'POST', url: '/resources/custom/query' }, + response: { statusCode: 500 }, + }); + }); + }); +}); diff --git a/plugins/kubernetes-backend/src/routes/resourcesRoutes.ts b/plugins/kubernetes-backend/src/routes/resourcesRoutes.ts new file mode 100644 index 0000000000..2b2a48f707 --- /dev/null +++ b/plugins/kubernetes-backend/src/routes/resourcesRoutes.ts @@ -0,0 +1,94 @@ +/* + * 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 { + CompoundEntityRef, + parseEntityRef, + stringifyEntityRef, +} from '@backstage/catalog-model'; +import { CatalogApi } from '@backstage/catalog-client'; +import { InputError, AuthenticationError } from '@backstage/errors'; +import express, { Request } from 'express'; +import { KubernetesObjectsProvider } from '../types/types'; +import { getBearerTokenFromAuthorizationHeader } from '@backstage/plugin-auth-node'; + +export const addResourceRoutesToRouter = ( + router: express.Router, + catalogApi: CatalogApi, + objectsProvider: KubernetesObjectsProvider, +) => { + const getEntityByReq = async (req: Request) => { + const rawEntityRef = req.body.entityRef; + if (rawEntityRef && typeof rawEntityRef !== 'string') { + throw new InputError(`entity query must be a string`); + } else if (!rawEntityRef) { + throw new InputError('entity is a required field'); + } + let entityRef: CompoundEntityRef | undefined = undefined; + + try { + entityRef = parseEntityRef(rawEntityRef); + } catch (error) { + throw new InputError(`Invalid entity ref, ${error}`); + } + + const token = getBearerTokenFromAuthorizationHeader( + req.headers.authorization, + ); + + if (!token) { + throw new AuthenticationError('No Backstage token'); + } + + const entity = await catalogApi.getEntityByRef(entityRef, { + token: token, + }); + + if (!entity) { + throw new InputError( + `Entity ref missing, ${stringifyEntityRef(entityRef)}`, + ); + } + return entity; + }; + + router.post('/resources/workloads/query', async (req, res) => { + const entity = await getEntityByReq(req); + const response = await objectsProvider.getKubernetesObjectsByEntity({ + entity, + auth: req.body.auth, + }); + res.json(response); + }); + + router.post('/resources/custom/query', async (req, res) => { + const entity = await getEntityByReq(req); + + if (!req.body.customResources) { + throw new InputError('customResources is a required field'); + } else if (!Array.isArray(req.body.customResources)) { + throw new InputError('customResources must be an array'); + } else if (req.body.customResources.length === 0) { + throw new InputError('at least 1 customResource is required'); + } + + const response = await objectsProvider.getCustomResourcesByEntity({ + entity, + customResources: req.body.customResources, + auth: req.body.auth, + }); + res.json(response); + }); +}; diff --git a/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts b/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts index ecaf9f3095..309141ec0e 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts @@ -31,11 +31,13 @@ import { import { KubernetesBuilder } from './KubernetesBuilder'; import { KubernetesFanOutHandler } from './KubernetesFanOutHandler'; import { PodStatus } from '@kubernetes/client-node'; +import { CatalogApi } from '@backstage/catalog-client'; describe('KubernetesBuilder', () => { let app: express.Express; let kubernetesFanOutHandler: jest.Mocked; let config: Config; + let catalogApi: CatalogApi; beforeAll(async () => { const logger = getVoidLogger(); @@ -69,7 +71,13 @@ describe('KubernetesBuilder', () => { getKubernetesObjectsByEntity: jest.fn(), } as any; - const { router } = await KubernetesBuilder.createBuilder({ config, logger }) + catalogApi = {} as CatalogApi; + + const { router } = await KubernetesBuilder.createBuilder({ + config, + logger, + catalogApi, + }) .setObjectsProvider(kubernetesFanOutHandler) .setClusterSupplier(clusterSupplier) .build(); @@ -240,6 +248,7 @@ describe('KubernetesBuilder', () => { const { router } = await KubernetesBuilder.createBuilder({ logger, config, + catalogApi, }) .setClusterSupplier(clusterSupplier) .setServiceLocator(serviceLocator) diff --git a/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts b/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts index 1b058752c9..59bac4f30a 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts @@ -37,6 +37,8 @@ import { KubernetesFanOutHandler, } from './KubernetesFanOutHandler'; import { KubernetesClientBasedFetcher } from './KubernetesFetcher'; +import { addResourceRoutesToRouter } from '../routes/resourcesRoutes'; +import { CatalogApi } from '@backstage/catalog-client'; /** * @@ -45,6 +47,7 @@ import { KubernetesClientBasedFetcher } from './KubernetesFetcher'; export interface KubernetesEnvironment { logger: Logger; config: Config; + catalogApi: CatalogApi; } /** @@ -119,7 +122,11 @@ export class KubernetesBuilder { objectTypesToFetch: this.getObjectTypesToFetch(), }); - const router = this.buildRouter(objectsProvider, clusterSupplier); + const router = this.buildRouter( + objectsProvider, + clusterSupplier, + this.env.catalogApi, + ); return { clusterSupplier, @@ -226,11 +233,13 @@ export class KubernetesBuilder { protected buildRouter( objectsProvider: KubernetesObjectsProvider, clusterSupplier: KubernetesClustersSupplier, + catalogApi: CatalogApi, ): express.Router { const logger = this.env.logger; const router = Router(); router.use(express.json()); + // @deprecated router.post('/services/:serviceId', async (req, res) => { const serviceId = req.params.serviceId; const requestBody: ObjectsByEntityRequest = req.body; @@ -259,6 +268,9 @@ export class KubernetesBuilder { })), }); }); + + addResourceRoutesToRouter(router, catalogApi, objectsProvider); + return router; } diff --git a/plugins/kubernetes-backend/src/service/router.ts b/plugins/kubernetes-backend/src/service/router.ts index 7bd7cca8a9..6c26175058 100644 --- a/plugins/kubernetes-backend/src/service/router.ts +++ b/plugins/kubernetes-backend/src/service/router.ts @@ -19,6 +19,7 @@ import { Logger } from 'winston'; import { KubernetesClustersSupplier } from '../types/types'; import express from 'express'; import { KubernetesBuilder } from './KubernetesBuilder'; +import { CatalogApi } from '@backstage/catalog-client'; /** * @@ -27,6 +28,7 @@ import { KubernetesBuilder } from './KubernetesBuilder'; export interface RouterOptions { logger: Logger; config: Config; + catalogApi: CatalogApi; clusterSupplier?: KubernetesClustersSupplier; } diff --git a/plugins/kubernetes-backend/src/service/standaloneApplication.ts b/plugins/kubernetes-backend/src/service/standaloneApplication.ts index 29262744e5..c12d7be2bb 100644 --- a/plugins/kubernetes-backend/src/service/standaloneApplication.ts +++ b/plugins/kubernetes-backend/src/service/standaloneApplication.ts @@ -18,6 +18,7 @@ import { errorHandler, notFoundHandler, requestLoggingHandler, + SingleHostDiscovery, } from '@backstage/backend-common'; import compression from 'compression'; import cors from 'cors'; @@ -26,6 +27,7 @@ import helmet from 'helmet'; import { Logger } from 'winston'; import { createRouter } from './router'; import { ConfigReader } from '@backstage/config'; +import { CatalogClient } from '@backstage/catalog-client'; export interface ApplicationOptions { enableCors: boolean; @@ -39,6 +41,10 @@ export async function createStandaloneApplication( const config = new ConfigReader({}); const app = express(); + const catalogApi = new CatalogClient({ + discoveryApi: SingleHostDiscovery.fromConfig(config), + }); + app.use(helmet()); if (enableCors) { app.use(cors()); @@ -46,7 +52,7 @@ export async function createStandaloneApplication( app.use(compression()); app.use(express.json()); app.use(requestLoggingHandler()); - app.use('/', await createRouter({ logger, config })); + app.use('/', await createRouter({ logger, config, catalogApi })); app.use(notFoundHandler()); app.use(errorHandler());