diff --git a/plugins/kubernetes-backend/api-report.md b/plugins/kubernetes-backend/api-report.md index f5b8ff85b7..827ec5e74c 100644 --- a/plugins/kubernetes-backend/api-report.md +++ b/plugins/kubernetes-backend/api-report.md @@ -22,10 +22,12 @@ import { Logger } from 'winston'; import { Metrics } from '@kubernetes/client-node'; import type { ObjectsByEntityResponse } from '@backstage/plugin-kubernetes-common'; import { PluginEndpointDiscovery } from '@backstage/backend-common'; -import { PodStatus } from '@kubernetes/client-node/dist/top'; -import type { Request as Request_2 } from 'express'; +import type { RequestHandler } from 'express'; import { TokenCredential } from '@azure/identity'; +// @alpha (undocumented) +export const APPLICATION_JSON: string; + // @alpha (undocumented) export interface AWSClusterDetails extends ClusterDetails { // (undocumented) @@ -144,6 +146,9 @@ export class GoogleServiceAccountAuthTranslator ): Promise; } +// @alpha (undocumented) +export const HEADER_KUBERNETES_CLUSTER: string; + // @alpha (undocumented) export interface KubernetesAuthTranslator { // (undocumented) @@ -227,11 +232,6 @@ export class KubernetesBuilder { // (undocumented) protected getServiceLocatorMethod(): ServiceLocatorMethod; // (undocumented) - protected makeProxyRequest( - req: express.Request, - res: express.Response, - ): Promise; - // (undocumented) setClusterSupplier(clusterSupplier?: KubernetesClustersSupplier): this; // (undocumented) setDefaultClusterRefreshInterval(refreshInterval: Duration): this; @@ -251,6 +251,7 @@ export type KubernetesBuilderReturn = Promise<{ clusterSupplier: KubernetesClustersSupplier; customResources: CustomResource[]; fetcher: KubernetesFetcher; + proxy: KubernetesProxy; objectsProvider: KubernetesObjectsProvider; serviceLocator: KubernetesServiceLocator; }>; @@ -349,22 +350,14 @@ export type KubernetesObjectTypes = export class KubernetesProxy { constructor(logger: Logger); // (undocumented) - handleProxyRequest( - req: Request_2, - clusterSupplier: KubernetesClustersSupplier, - ): Promise; + get clustersSupplier(): KubernetesClustersSupplier; + set clustersSupplier(clustersSupplier: KubernetesClustersSupplier); // (undocumented) protected readonly logger: Logger; -} - -// @alpha (undocumented) -export interface KubernetesProxyResponse { // (undocumented) - cluster?: string; + static readonly PROXY_PATH: string; // (undocumented) - code: number; - // (undocumented) - data: any; + proxyRequestHandler: RequestHandler; } // @alpha diff --git a/plugins/kubernetes-backend/package.json b/plugins/kubernetes-backend/package.json index d1352a21dc..d5ab468790 100644 --- a/plugins/kubernetes-backend/package.json +++ b/plugins/kubernetes-backend/package.json @@ -44,6 +44,7 @@ "@backstage/plugin-auth-node": "workspace:^", "@backstage/plugin-kubernetes-common": "workspace:^", "@google-cloud/container": "^4.0.0", + "@jest-mock/express": "^2.0.1", "@kubernetes/client-node": "0.17.0", "@types/express": "^4.17.6", "@types/luxon": "^3.0.0", @@ -58,7 +59,7 @@ "lodash": "^4.17.21", "luxon": "^3.0.0", "morgan": "^1.10.0", - "node-fetch": "^2.6.0", + "node-fetch": "^2.6.7", "stream-buffers": "^3.0.2", "winston": "^3.2.1", "yn": "^4.0.0" diff --git a/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts b/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts index ff79bd83de..3ac27decc8 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts @@ -40,7 +40,7 @@ import { KubernetesFanOutHandler, } from './KubernetesFanOutHandler'; import { KubernetesClientBasedFetcher } from './KubernetesFetcher'; -import { KubernetesProxy, KubernetesProxyResponse } from './KubernetesProxy'; +import { KubernetesProxy } from './KubernetesProxy'; /** * @@ -62,6 +62,7 @@ export type KubernetesBuilderReturn = Promise<{ clusterSupplier: KubernetesClustersSupplier; customResources: CustomResource[]; fetcher: KubernetesFetcher; + proxy: KubernetesProxy; objectsProvider: KubernetesObjectsProvider; serviceLocator: KubernetesServiceLocator; }>; @@ -107,8 +108,12 @@ export class KubernetesBuilder { const fetcher = this.getFetcher(); + const proxy = this.getProxy(); + const clusterSupplier = this.getClusterSupplier(); + proxy.clustersSupplier = clusterSupplier; + const serviceLocator = this.getServiceLocator(); const objectsProvider = this.getObjectsProvider({ @@ -129,6 +134,7 @@ export class KubernetesBuilder { clusterSupplier, customResources, fetcher, + proxy, objectsProvider, router, serviceLocator, @@ -292,12 +298,12 @@ export class KubernetesBuilder { }); }); - if (typeof proxy?.handleProxyRequest === 'function') { - router.get('/proxy/:encodedQuery', this.makeProxyRequest.bind(this)); - router.post('/proxy/:encodedQuery', this.makeProxyRequest.bind(this)); - router.put('/proxy/:encodedQuery', this.makeProxyRequest.bind(this)); - router.patch('/proxy/:encodedQuery', this.makeProxyRequest.bind(this)); - router.delete('/proxy/:encodedQuery', this.makeProxyRequest.bind(this)); + if (typeof proxy?.proxyRequestHandler === 'function') { + router.get(KubernetesProxy.PROXY_PATH, proxy.proxyRequestHandler); + router.post(KubernetesProxy.PROXY_PATH, proxy.proxyRequestHandler); + router.put(KubernetesProxy.PROXY_PATH, proxy.proxyRequestHandler); + router.patch(KubernetesProxy.PROXY_PATH, proxy.proxyRequestHandler); + router.delete(KubernetesProxy.PROXY_PATH, proxy.proxyRequestHandler); } addResourceRoutesToRouter(router, catalogApi, objectsProvider); @@ -381,24 +387,4 @@ export class KubernetesBuilder { protected getProxy() { return this.proxy ?? this.buildProxy(); } - - protected async makeProxyRequest( - req: express.Request, - res: express.Response, - ) { - const supplier = this.getClustersSupplier(); - const proxy = this.getProxy(); - - const proxyResponse: KubernetesProxyResponse = - await proxy.handleProxyRequest(req, supplier); - - res.status(proxyResponse.code).json(proxyResponse.data); - } - - private getClustersSupplier(): KubernetesClustersSupplier { - return ( - this.clusterSupplier ?? - this.buildClusterSupplier(this.defaultClusterRefreshInterval) - ); - } } diff --git a/plugins/kubernetes-backend/src/service/KubernetesProxy.test.ts b/plugins/kubernetes-backend/src/service/KubernetesProxy.test.ts index b9bb1d425f..adf9357bb3 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesProxy.test.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesProxy.test.ts @@ -22,50 +22,38 @@ import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { ClusterDetails, KubernetesClustersSupplier } from '../types/types'; -import { KubernetesProxy } from './KubernetesProxy'; +import { + APPLICATION_JSON, + HEADER_KUBERNETES_CLUSTER, + KubernetesProxy, +} from './KubernetesProxy'; import { NotFoundError } from '@backstage/errors'; +import { getMockReq, getMockRes } from '@jest-mock/express'; describe('KubernetesProxy', () => { let proxy: KubernetesProxy; const worker = setupServer(); setupRequestMockHandlers(worker); - const buildEncodedRequest = ( - clustersHeader: any, - query: string, - body?: unknown, - ): Request => { - const encodedQuery = encodeURIComponent(query); - const encodedClusters = Buffer.from( - JSON.stringify(clustersHeader), - ).toString('base64'); - - const req = { + const buildMockRequest = (clusterName: any, path: string): Request => { + const req = getMockReq({ params: { - encodedQuery, + path, }, - header: (key: string) => { - let value: string = ''; + header: jest.fn((key: string) => { switch (key) { case 'Content-Type': { - value = 'application/json'; - break; + return APPLICATION_JSON; } - case 'X-Kubernetes-Clusters': { - value = encodedClusters; - break; + case HEADER_KUBERNETES_CLUSTER: { + return clusterName; } default: { - break; + return ''; } } - return value; - }, - } as unknown as Request; - - if (body) { - req.body = body; - } + }), + }); return req; }; @@ -83,15 +71,17 @@ describe('KubernetesProxy', () => { }); it('should return a ERROR_NOT_FOUND if no clusters are found', async () => { - const clustersSupplier = buildClustersSupplierWithClusters([]); - const req = buildEncodedRequest({}, 'api'); + proxy.clustersSupplier = buildClustersSupplierWithClusters([]); - await expect( - proxy.handleProxyRequest(req, clustersSupplier), - ).rejects.toThrow(NotFoundError); + const req = buildMockRequest('test', 'api'); + const { res, next } = getMockRes(); + + await expect(proxy.proxyRequestHandler(req, res, next)).rejects.toThrow( + NotFoundError, + ); }); - it('should match the response code of the Kubernetes response (single cluster)', async () => { + it('should match the response code of the Kubernetes response', async () => { const clusters: ClusterDetails[] = [ { name: 'cluster1', @@ -102,8 +92,10 @@ describe('KubernetesProxy', () => { }, ]; - const clustersSupplier = buildClustersSupplierWithClusters(clusters); - const req = buildEncodedRequest({ cluster1: 'token' }, 'api'); + proxy.clustersSupplier = buildClustersSupplierWithClusters(clusters); + + const req = buildMockRequest('cluster1', 'api'); + const { res: response, next } = getMockRes(); const apiResponse = { kind: 'APIVersions', @@ -117,76 +109,18 @@ describe('KubernetesProxy', () => { }; worker.use( - rest.get(`${clusters[0].url}/${req.params.encodedQuery}`, (_, res, ctx) => + rest.get(`${clusters[0].url}/${req.params.path}`, (_, res, ctx) => res(ctx.status(299), ctx.body(JSON.stringify(apiResponse))), ), ); - const result = await proxy.handleProxyRequest(req, clustersSupplier); + await proxy.proxyRequestHandler(req, response, next); - expect(result.code).toEqual(299); + expect(response.status).toHaveBeenCalledWith(299); + expect(response.json).toHaveBeenCalledWith(apiResponse); }); - it('should match the response code of the best Kubernetes response (multi cluster)', async () => { - const clusters: ClusterDetails[] = [ - { - name: 'cluster1', - url: 'http://localhost:9998', - serviceAccountToken: 'token', - authProvider: 'serviceAccount', - skipTLSVerify: true, - }, - { - name: 'cluster2', - url: 'http://localhost:9999', - serviceAccountToken: 'token', - authProvider: 'serviceAccount', - skipTLSVerify: true, - }, - ]; - - const clustersSupplier = buildClustersSupplierWithClusters(clusters); - const req = buildEncodedRequest( - { cluster1: 'token', cluster2: 'token' }, - 'api', - ); - - const apiResponse1 = { - kind: 'APIVersions', - versions: ['v1'], - serverAddressByClientCIDRs: [ - { - clientCIDR: '0.0.0.0/0', - serverAddress: '192.168.0.1:3333', - }, - ], - }; - - const apiResponse2 = { - kind: 'Status', - apiVersion: 'v1', - metadata: {}, - status: 'Failure', - message: 'Unauthorized', - reason: 'Unauthorized', - code: 401, - }; - - worker.use( - rest.get(`${clusters[0].url}/${req.params.encodedQuery}`, (_, res, ctx) => - res(ctx.status(200), ctx.body(JSON.stringify(apiResponse1))), - ), - rest.get(`${clusters[1].url}/${req.params.encodedQuery}`, (_, res, ctx) => - res(ctx.status(401), ctx.body(JSON.stringify(apiResponse2))), - ), - ); - - const result = await proxy.handleProxyRequest(req, clustersSupplier); - - expect(result.code).toEqual(200); - }); - - it('should pass the exact response data from Kubernetes (single cluster)', async () => { + it('should pass the exact response data from Kubernetes', async () => { const clusters: ClusterDetails[] = [ { name: 'cluster1', @@ -197,8 +131,10 @@ describe('KubernetesProxy', () => { }, ]; - const clustersSupplier = buildClustersSupplierWithClusters(clusters); - const req = buildEncodedRequest({ cluster1: 'token' }, 'api'); + proxy.clustersSupplier = buildClustersSupplierWithClusters(clusters); + + const req = buildMockRequest('cluster1', 'api'); + const { res: response, next } = getMockRes(); const apiResponse = { kind: 'APIVersions', @@ -212,109 +148,14 @@ describe('KubernetesProxy', () => { }; worker.use( - rest.get(`${clusters[0].url}/${req.params.encodedQuery}`, (_, res, ctx) => + rest.get(`${clusters[0].url}/${req.params.path}`, (_, res, ctx) => res(ctx.status(200), ctx.body(JSON.stringify(apiResponse))), ), ); - const result = await proxy.handleProxyRequest(req, clustersSupplier); + await proxy.proxyRequestHandler(req, response, next); - const resultString = JSON.stringify(result.data); - const expectedString = JSON.stringify({ - cluster1: { - kind: 'APIVersions', - versions: ['v1'], - serverAddressByClientCIDRs: [ - { - clientCIDR: '0.0.0.0/0', - serverAddress: '192.168.0.1:3333', - }, - ], - }, - }); - - expect(resultString).toEqual(expectedString); - }); - - it('should pass the exact response data from Kubernetes (multi cluster)', async () => { - const clusters: ClusterDetails[] = [ - { - name: 'cluster1', - url: 'http://localhost:9998', - serviceAccountToken: 'token', - authProvider: 'serviceAccount', - skipTLSVerify: true, - }, - { - name: 'cluster2', - url: 'http://localhost:9999', - serviceAccountToken: 'token', - authProvider: 'serviceAccount', - skipTLSVerify: true, - }, - ]; - - const clustersSupplier = buildClustersSupplierWithClusters(clusters); - const req = buildEncodedRequest( - { cluster1: 'token', cluster2: 'token' }, - 'api', - ); - - const apiResponse1 = { - kind: 'APIVersions', - versions: ['v1'], - serverAddressByClientCIDRs: [ - { - clientCIDR: '0.0.0.0/0', - serverAddress: '192.168.0.1:3333', - }, - ], - }; - - const apiResponse2 = { - kind: 'Status', - apiVersion: 'v1', - metadata: {}, - status: 'Failure', - message: 'Unauthorized', - reason: 'Unauthorized', - code: 401, - }; - - worker.use( - rest.get(`${clusters[0].url}/${req.params.encodedQuery}`, (_, res, ctx) => - res(ctx.status(200), ctx.body(JSON.stringify(apiResponse1))), - ), - rest.get(`${clusters[1].url}/${req.params.encodedQuery}`, (_, res, ctx) => - res(ctx.status(401), ctx.body(JSON.stringify(apiResponse2))), - ), - ); - - const result = await proxy.handleProxyRequest(req, clustersSupplier); - - const resultString = JSON.stringify(result.data); - const expectedString = JSON.stringify({ - cluster1: { - kind: 'APIVersions', - versions: ['v1'], - serverAddressByClientCIDRs: [ - { - clientCIDR: '0.0.0.0/0', - serverAddress: '192.168.0.1:3333', - }, - ], - }, - cluster2: { - kind: 'Status', - apiVersion: 'v1', - metadata: {}, - status: 'Failure', - message: 'Unauthorized', - reason: 'Unauthorized', - code: 401, - }, - }); - - expect(resultString).toEqual(expectedString); + expect(response.status).toHaveBeenCalledWith(200); + expect(response.json).toHaveBeenCalledWith(apiResponse); }); }); diff --git a/plugins/kubernetes-backend/src/service/KubernetesProxy.ts b/plugins/kubernetes-backend/src/service/KubernetesProxy.ts index dea4786820..d1a34cb2cf 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesProxy.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesProxy.ts @@ -19,151 +19,105 @@ import { ForwardedError, InputError, NotFoundError, - stringifyError, } from '@backstage/errors'; import { bufferFromFileOrString, KubeConfig } from '@kubernetes/client-node'; import * as https from 'https'; -import fetch, { RequestInit } from 'node-fetch'; +import fetch, { RequestInit, Response } from 'node-fetch'; import { Logger } from 'winston'; import { ClusterDetails, KubernetesClustersSupplier } from '../types/types'; -import type { Request } from 'express'; +import type { Request as ExpressRequest, RequestHandler } from 'express'; + +/** + * + * @alpha + */ +export const APPLICATION_JSON: string = 'application/json'; + +/** + * + * @alpha + */ +export const HEADER_KUBERNETES_CLUSTER: string = 'X-Kubernetes-Cluster'; const HEADER_CONTENT_TYPE: string = 'Content-Type'; -const APPLICATION_JSON: string = 'application/json'; - -const HEADER_KUBERNETES_CLUSTERS: string = 'X-Kubernetes-Clusters'; - -const ERROR_INTERNAL_SERVER: number = 500; const CLUSTER_USER_NAME: string = 'backstage'; -/** - * - * @alpha - */ -export interface KubernetesProxyResponse { - code: number; - data: any; - cluster?: string; -} - -/** - * - * @alpha - */ -interface KubernetesProxyClusters { - [key: string]: string; -} - /** * * @alpha */ export class KubernetesProxy { + private _clustersSupplier?: KubernetesClustersSupplier; + + static readonly PROXY_PATH: string = '/proxy/:path(*)'; + constructor(protected readonly logger: Logger) {} - public async handleProxyRequest( - req: Request, - clusterSupplier: KubernetesClustersSupplier, - ): Promise { - const requestedClusters = this.getKubernetesRequestedClusters(req); + public proxyRequestHandler: RequestHandler = async (req, res) => { + const requestedCluster = this.getKubernetesRequestedCluster(req); - if (Object.keys(requestedClusters).length < 1) { - this.logger.error(`No clusters found`); - throw new NotFoundError('No clusters found!'); + const clusterDetails = await this.getClusterDetails(requestedCluster); + + const response = await this.makeRequestToCluster(clusterDetails, req); + + const contentType = req.header(HEADER_CONTENT_TYPE) || APPLICATION_JSON; + + const data = contentType.includes(APPLICATION_JSON) + ? await response.json() + : await response.text(); + + res.status(response.status).json(data); + }; + + public get clustersSupplier(): KubernetesClustersSupplier { + if (this._clustersSupplier ? false : this._clustersSupplier ?? true) { + throw new ConflictError("Missing Proxy's Clusters Supplier"); } - const clusterDetails = await this.getClusterDetails( - clusterSupplier, - requestedClusters, - ); - - if (clusterDetails.length < 1) { - this.logger.error(`No clusters found`); - throw new NotFoundError('No clusters found!'); - } - - const responses = await Promise.all( - clusterDetails.map(async clusterDetail => { - const response = await this.makeRequestToCluster(clusterDetail, req); - return response; - }), - ); - - const data: { [key: string]: any } = {}; - const codes: number[] = []; - - responses.forEach(kpr => { - if (kpr.cluster) { - data[kpr.cluster] = kpr.data; - codes.push(kpr.code); - } - }); - - const code = this.getBestResponseCode(codes); - - const res: KubernetesProxyResponse = { - code, - data, - }; - - return res; + return this._clustersSupplier as KubernetesClustersSupplier; } - private getKubernetesRequestedClusters( - req: Request, - ): KubernetesProxyClusters { - const encodedClusters: string = - req.header(HEADER_KUBERNETES_CLUSTERS) ?? ''; + public set clustersSupplier(clustersSupplier) { + this._clustersSupplier = clustersSupplier; + } - if (!encodedClusters) { - return {}; + private getKubernetesRequestedCluster(req: ExpressRequest): string { + const requestedClusterName: string = + req.header(HEADER_KUBERNETES_CLUSTER) ?? ''; + + if (!requestedClusterName) { + this.logger.error(`Malformed ${HEADER_KUBERNETES_CLUSTER} header.`); + throw new InputError(`Malformed ${HEADER_KUBERNETES_CLUSTER} header.`); } - try { - const decodedClusters = Buffer.from(encodedClusters, 'base64').toString(); - const clusters: KubernetesProxyClusters = JSON.parse(decodedClusters); - return clusters; - } catch (e: any) { - this.logger.debug( - `error with encoded cluster header: ${stringifyError(e)}`, - ); - } - return {}; + return requestedClusterName; } private async getClusterDetails( - clusterSupplier: KubernetesClustersSupplier, - requestedClusters: KubernetesProxyClusters, - ): Promise { - const clusters = await clusterSupplier.getClusters(); + requestedCluster: string, + ): Promise { + const clusters = await this.clustersSupplier.getClusters(); - const clusterNames = Object.keys(requestedClusters); + const clusterDetail = clusters.find(cluster => + requestedCluster.includes(cluster.name), + ); - const clusterDetails = clusters.filter(c => clusterNames.includes(c.name)); + if (clusterDetail ? false : clusterDetail ?? true) { + this.logger.error( + `Cluster ${requestedCluster} details not found in config`, + ); - const clusterDetailsAuth = clusterDetails.map(c => { - const cAuth: ClusterDetails = Object.assign(c, { - serviceAccountToken: requestedClusters[c.name], - }); - return cAuth; - }); + throw new NotFoundError("Cluster's detail not found"); + } - return clusterDetailsAuth; + return clusterDetail as ClusterDetails; } private getClusterURI(details: ClusterDetails): string { - const client = this.getKubeConfig(details); - return client.getCurrentCluster()?.server || ''; - } - - private async makeRequestToCluster( - details: ClusterDetails, - req: Request, - ): Promise { - const serverURI = this.getClusterURI(details); + const serverURI = this.getKubeConfig(details)?.getCurrentCluster()?.server; if (!serverURI) { this.logger.error(`Cluster ${details.name} details IP error`); @@ -171,27 +125,26 @@ export class KubernetesProxy { throw new ConflictError('Cluster detail error'); } - const query = decodeURIComponent(req.params.encodedQuery) || ''; - const uri = `${serverURI}/${query}`; + return serverURI; + } - const contentType = req.header(HEADER_CONTENT_TYPE) || APPLICATION_JSON; + private async makeRequestToCluster( + details: ClusterDetails, + req: ExpressRequest, + ): Promise { + const serverURI = this.getClusterURI(details); - return await this.sendClusterRequest( - details, - uri, - req.method, - contentType, - req.body, - ); + const path = decodeURIComponent(req.params.path) || ''; + const uri = `${serverURI}/${path}`; + + return await this.sendClusterRequest(details, uri, req); } private async sendClusterRequest( details: ClusterDetails, uri: string, - method: string, - contentType: string, - body?: any, - ): Promise { + req: ExpressRequest, + ): Promise { const bearerToken = details.serviceAccountToken; if (!bearerToken) { @@ -200,12 +153,11 @@ export class KubernetesProxy { throw new AuthenticationError('Invalid service account token'); } + const { method, headers, body } = req; + const reqData: RequestInit = { method, - headers: { - 'Content-Type': contentType, - Authorization: `Bearer ${bearerToken}`, - }, + headers: headers as { [key: string]: string }, }; if (!details.skipTLSVerify) { @@ -214,6 +166,7 @@ export class KubernetesProxy { reqData.agent = new https.Agent({ ca }); } else { this.logger.error('could not find CA certificate!'); + throw new InputError( 'Invalid CA certificate configured within Backstage', ); @@ -225,23 +178,7 @@ export class KubernetesProxy { } try { - const res = await fetch(uri, reqData); - - let data: string | any; - - if (contentType.includes(APPLICATION_JSON)) { - data = await res.json(); - } else { - data = await res.text(); - } - - const proxyResponse: KubernetesProxyResponse = { - code: res.status, - data, - cluster: details.name, - }; - - return proxyResponse; + return fetch(uri, reqData); } catch (e: any) { throw new ForwardedError(`Cluster ${details.name} request error`, e); } @@ -281,9 +218,4 @@ export class KubernetesProxy { return kubeConfig; } - - private getBestResponseCode(codes: number[]): number { - const sorted = codes.sort(); - return sorted[0] ?? ERROR_INTERNAL_SERVER; - } } diff --git a/yarn.lock b/yarn.lock index ae10e493f5..653eeec7ce 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5973,6 +5973,7 @@ __metadata: "@backstage/plugin-auth-node": "workspace:^" "@backstage/plugin-kubernetes-common": "workspace:^" "@google-cloud/container": ^4.0.0 + "@jest-mock/express": ^2.0.1 "@kubernetes/client-node": 0.17.0 "@types/aws4": ^1.5.1 "@types/express": ^4.17.6 @@ -5990,7 +5991,7 @@ __metadata: luxon: ^3.0.0 morgan: ^1.10.0 msw: ^0.48.0 - node-fetch: ^2.6.0 + node-fetch: ^2.6.7 stream-buffers: ^3.0.2 supertest: ^6.1.3 winston: ^3.2.1 @@ -9250,6 +9251,15 @@ __metadata: languageName: node linkType: hard +"@jest-mock/express@npm:^2.0.1": + version: 2.0.1 + resolution: "@jest-mock/express@npm:2.0.1" + dependencies: + "@types/express": ^4.17.13 + checksum: 999ea0a953b3e911d0b8ecc4cb3b78ac252b3354832db9659be6754094410b10508f1688f3e623ab13fe40c28d89fd5a81699e6acd148dd8128abec4e81f3f14 + languageName: node + linkType: hard + "@jest/console@npm:^29.0.3": version: 29.0.3 resolution: "@jest/console@npm:29.0.3"