diff --git a/.changeset/rich-garlics-play.md b/.changeset/rich-garlics-play.md new file mode 100644 index 0000000000..5589a70e35 --- /dev/null +++ b/.changeset/rich-garlics-play.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-kubernetes-backend': patch +'@backstage/plugin-kubernetes-common': patch +--- + +Added Kubernetes proxy API route to backend Kubernetes plugin, allowing Backstage plugin developers to read/write new information from Kubernetes (if proper credentials are provided). diff --git a/plugins/kubernetes-backend/api-report.md b/plugins/kubernetes-backend/api-report.md index d51ec4c211..b88b3f2a02 100644 --- a/plugins/kubernetes-backend/api-report.md +++ b/plugins/kubernetes-backend/api-report.md @@ -22,6 +22,7 @@ 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 type { RequestHandler } from 'express'; import { TokenCredential } from '@azure/identity'; // @alpha (undocumented) @@ -142,6 +143,9 @@ export class GoogleServiceAccountAuthTranslator ): Promise; } +// @alpha +export const HEADER_KUBERNETES_CLUSTER: string; + // @alpha (undocumented) export interface KubernetesAuthTranslator { // (undocumented) @@ -188,10 +192,16 @@ export class KubernetesBuilder { options: KubernetesObjectsProviderOptions, ): KubernetesObjectsProvider; // (undocumented) + protected buildProxy( + logger: Logger, + clusterSupplier: KubernetesClustersSupplier, + ): KubernetesProxy; + // (undocumented) protected buildRouter( objectsProvider: KubernetesObjectsProvider, clusterSupplier: KubernetesClustersSupplier, catalogApi: CatalogApi, + proxy: KubernetesProxy, ): express.Router; // (undocumented) protected buildServiceLocator( @@ -207,8 +217,23 @@ export class KubernetesBuilder { clusterSupplier: KubernetesClustersSupplier, ): Promise; // (undocumented) + protected getClusterSupplier(): KubernetesClustersSupplier; + // (undocumented) + protected getFetcher(): KubernetesFetcher; + // (undocumented) + protected getObjectsProvider( + options: KubernetesObjectsProviderOptions, + ): KubernetesObjectsProvider; + // (undocumented) protected getObjectTypesToFetch(): ObjectToFetch[] | undefined; // (undocumented) + protected getProxy( + logger: Logger, + clusterSupplier: KubernetesClustersSupplier, + ): KubernetesProxy; + // (undocumented) + protected getServiceLocator(): KubernetesServiceLocator; + // (undocumented) protected getServiceLocatorMethod(): ServiceLocatorMethod; // (undocumented) setClusterSupplier(clusterSupplier?: KubernetesClustersSupplier): this; @@ -219,6 +244,8 @@ export class KubernetesBuilder { // (undocumented) setObjectsProvider(objectsProvider?: KubernetesObjectsProvider): this; // (undocumented) + setProxy(proxy?: KubernetesProxy): this; + // (undocumented) setServiceLocator(serviceLocator?: KubernetesServiceLocator): this; } @@ -228,6 +255,7 @@ export type KubernetesBuilderReturn = Promise<{ clusterSupplier: KubernetesClustersSupplier; customResources: CustomResource[]; fetcher: KubernetesFetcher; + proxy: KubernetesProxy; objectsProvider: KubernetesObjectsProvider; serviceLocator: KubernetesServiceLocator; }>; @@ -322,6 +350,13 @@ export type KubernetesObjectTypes = | 'statefulsets' | 'daemonsets'; +// @alpha +export class KubernetesProxy { + constructor(logger: Logger, clusterSupplier: KubernetesClustersSupplier); + // (undocumented) + createRequestHandler(): RequestHandler; +} + // @alpha export interface KubernetesServiceLocator { // (undocumented) diff --git a/plugins/kubernetes-backend/package.json b/plugins/kubernetes-backend/package.json index acf28eeb36..c0822d366a 100644 --- a/plugins/kubernetes-backend/package.json +++ b/plugins/kubernetes-backend/package.json @@ -36,6 +36,7 @@ "dependencies": { "@azure/identity": "^2.0.4", "@backstage/backend-common": "workspace:^", + "@backstage/backend-test-utils": "workspace:^", "@backstage/catalog-client": "workspace:^", "@backstage/catalog-model": "workspace:^", "@backstage/config": "workspace:^", @@ -43,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", @@ -54,9 +56,11 @@ "express-promise-router": "^4.1.0", "fs-extra": "10.1.0", "helmet": "^6.0.0", + "http-proxy-middleware": "^2.0.6", "lodash": "^4.17.21", "luxon": "^3.0.0", "morgan": "^1.10.0", + "node-fetch": "^2.6.7", "stream-buffers": "^3.0.2", "winston": "^3.2.1", "yn": "^4.0.0" @@ -64,7 +68,9 @@ "devDependencies": { "@backstage/cli": "workspace:^", "@types/aws4": "^1.5.1", + "@types/http-proxy-middleware": "^0.19.3", "aws-sdk-mock": "^5.2.1", + "msw": "^0.49.0", "supertest": "^6.1.3" }, "files": [ diff --git a/plugins/kubernetes-backend/src/index.ts b/plugins/kubernetes-backend/src/index.ts index e2eee3f24e..67aefbd974 100644 --- a/plugins/kubernetes-backend/src/index.ts +++ b/plugins/kubernetes-backend/src/index.ts @@ -20,19 +20,6 @@ * @packageDocumentation */ -export * from './kubernetes-auth-translator/AwsIamKubernetesAuthTranslator'; -export * from './kubernetes-auth-translator/AzureIdentityKubernetesAuthTranslator'; -export * from './kubernetes-auth-translator/GoogleKubernetesAuthTranslator'; -export * from './kubernetes-auth-translator/GoogleServiceAccountAuthProvider'; -export * from './kubernetes-auth-translator/KubernetesAuthTranslatorGenerator'; -export * from './kubernetes-auth-translator/NoopKubernetesAuthTranslator'; -export * from './kubernetes-auth-translator/OidcKubernetesAuthTranslator'; -export * from './kubernetes-auth-translator/types'; - -export * from './service/router'; -export * from './service/KubernetesBuilder'; -export * from './service/KubernetesClientProvider'; - -export * from './types/types'; - -export { DEFAULT_OBJECTS } from './service/KubernetesFanOutHandler'; +export * from './kubernetes-auth-translator'; +export * from './service'; +export * from './types'; diff --git a/plugins/kubernetes-backend/src/kubernetes-auth-translator/index.ts b/plugins/kubernetes-backend/src/kubernetes-auth-translator/index.ts new file mode 100644 index 0000000000..19f6dd80eb --- /dev/null +++ b/plugins/kubernetes-backend/src/kubernetes-auth-translator/index.ts @@ -0,0 +1,24 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './AwsIamKubernetesAuthTranslator'; +export * from './AzureIdentityKubernetesAuthTranslator'; +export * from './GoogleKubernetesAuthTranslator'; +export * from './GoogleServiceAccountAuthProvider'; +export * from './KubernetesAuthTranslatorGenerator'; +export * from './NoopKubernetesAuthTranslator'; +export * from './OidcKubernetesAuthTranslator'; +export * from './types'; diff --git a/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts b/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts index 304885f3e2..c8ff98f592 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts @@ -13,23 +13,26 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { CatalogApi } from '@backstage/catalog-client'; import { Config } from '@backstage/config'; import express from 'express'; import Router from 'express-promise-router'; -import { Logger } from 'winston'; import { Duration } from 'luxon'; +import { Logger } from 'winston'; + import { getCombinedClusterSupplier } from '../cluster-locator'; +import { addResourceRoutesToRouter } from '../routes/resourcesRoutes'; import { MultiTenantServiceLocator } from '../service-locator/MultiTenantServiceLocator'; import { - KubernetesObjectTypes, - ServiceLocatorMethod, CustomResource, - KubernetesObjectsProvider, - ObjectsByEntityRequest, KubernetesClustersSupplier, KubernetesFetcher, - KubernetesServiceLocator, + KubernetesObjectsProvider, KubernetesObjectsProviderOptions, + KubernetesObjectTypes, + KubernetesServiceLocator, + ObjectsByEntityRequest, + ServiceLocatorMethod, } from '../types/types'; import { KubernetesClientProvider } from './KubernetesClientProvider'; import { @@ -37,8 +40,7 @@ import { KubernetesFanOutHandler, } from './KubernetesFanOutHandler'; import { KubernetesClientBasedFetcher } from './KubernetesFetcher'; -import { addResourceRoutesToRouter } from '../routes/resourcesRoutes'; -import { CatalogApi } from '@backstage/catalog-client'; +import { KubernetesProxy } from './KubernetesProxy'; /** * @@ -60,6 +62,7 @@ export type KubernetesBuilderReturn = Promise<{ clusterSupplier: KubernetesClustersSupplier; customResources: CustomResource[]; fetcher: KubernetesFetcher; + proxy: KubernetesProxy; objectsProvider: KubernetesObjectsProvider; serviceLocator: KubernetesServiceLocator; }>; @@ -76,6 +79,7 @@ export class KubernetesBuilder { private objectsProvider?: KubernetesObjectsProvider; private fetcher?: KubernetesFetcher; private serviceLocator?: KubernetesServiceLocator; + private proxy?: KubernetesProxy; static createBuilder(env: KubernetesEnvironment) { return new KubernetesBuilder(env); @@ -102,36 +106,34 @@ export class KubernetesBuilder { } const customResources = this.buildCustomResources(); - const fetcher = this.fetcher ?? this.buildFetcher(); + const fetcher = this.getFetcher(); - const clusterSupplier = - this.clusterSupplier ?? - this.buildClusterSupplier(this.defaultClusterRefreshInterval); + const clusterSupplier = this.getClusterSupplier(); - const serviceLocator = - this.serviceLocator ?? - this.buildServiceLocator(this.getServiceLocatorMethod(), clusterSupplier); + const proxy = this.getProxy(logger, clusterSupplier); - const objectsProvider = - this.objectsProvider ?? - this.buildObjectsProvider({ - logger, - fetcher, - serviceLocator, - customResources, - objectTypesToFetch: this.getObjectTypesToFetch(), - }); + const serviceLocator = this.getServiceLocator(); + + const objectsProvider = this.getObjectsProvider({ + logger, + fetcher, + serviceLocator, + customResources, + objectTypesToFetch: this.getObjectTypesToFetch(), + }); const router = this.buildRouter( objectsProvider, clusterSupplier, this.env.catalogApi, + proxy, ); return { clusterSupplier, customResources, fetcher, + proxy, objectsProvider, router, serviceLocator, @@ -163,6 +165,11 @@ export class KubernetesBuilder { return this; } + public setProxy(proxy?: KubernetesProxy) { + this.proxy = proxy; + return this; + } + protected buildCustomResources() { const customResources: CustomResource[] = ( this.env.config.getOptionalConfigArray('kubernetes.customResources') ?? [] @@ -186,24 +193,29 @@ export class KubernetesBuilder { refreshInterval: Duration, ): KubernetesClustersSupplier { const config = this.env.config; - return getCombinedClusterSupplier( + this.clusterSupplier = getCombinedClusterSupplier( config, this.env.catalogApi, refreshInterval, ); + + return this.clusterSupplier; } protected buildObjectsProvider( options: KubernetesObjectsProviderOptions, ): KubernetesObjectsProvider { - return new KubernetesFanOutHandler(options); + this.objectsProvider = new KubernetesFanOutHandler(options); + return this.objectsProvider; } protected buildFetcher(): KubernetesFetcher { - return new KubernetesClientBasedFetcher({ + this.fetcher = new KubernetesClientBasedFetcher({ kubernetesClientProvider: new KubernetesClientProvider(), logger: this.env.logger, }); + + return this.fetcher; } protected buildServiceLocator( @@ -212,14 +224,19 @@ export class KubernetesBuilder { ): KubernetesServiceLocator { switch (method) { case 'multiTenant': - return this.buildMultiTenantServiceLocator(clusterSupplier); + this.serviceLocator = + this.buildMultiTenantServiceLocator(clusterSupplier); + break; case 'http': - return this.buildHttpServiceLocator(clusterSupplier); + this.serviceLocator = this.buildHttpServiceLocator(clusterSupplier); + break; default: throw new Error( `Unsupported kubernetes.clusterLocatorMethod "${method}"`, ); } + + return this.serviceLocator; } protected buildMultiTenantServiceLocator( @@ -234,10 +251,19 @@ export class KubernetesBuilder { throw new Error('not implemented'); } + protected buildProxy( + logger: Logger, + clusterSupplier: KubernetesClustersSupplier, + ): KubernetesProxy { + this.proxy = new KubernetesProxy(logger, clusterSupplier); + return this.proxy; + } + protected buildRouter( objectsProvider: KubernetesObjectsProvider, clusterSupplier: KubernetesClustersSupplier, catalogApi: CatalogApi, + proxy: KubernetesProxy, ): express.Router { const logger = this.env.logger; const router = Router(); @@ -273,6 +299,8 @@ export class KubernetesBuilder { }); }); + router.use('/proxy', proxy.createRequestHandler()); + addResourceRoutesToRouter(router, catalogApi, objectsProvider); return router; @@ -296,6 +324,31 @@ export class KubernetesBuilder { ) as ServiceLocatorMethod; } + protected getFetcher(): KubernetesFetcher { + return this.fetcher ?? this.buildFetcher(); + } + + protected getClusterSupplier() { + return ( + this.clusterSupplier ?? + this.buildClusterSupplier(this.defaultClusterRefreshInterval) + ); + } + + protected getServiceLocator(): KubernetesServiceLocator { + return ( + this.serviceLocator ?? + this.buildServiceLocator( + this.getServiceLocatorMethod(), + this.getClusterSupplier(), + ) + ); + } + + protected getObjectsProvider(options: KubernetesObjectsProviderOptions) { + return this.objectsProvider ?? this.buildObjectsProvider(options); + } + protected getObjectTypesToFetch() { const objectTypesToFetchStrings = this.env.config.getOptionalStringArray( 'kubernetes.objectTypes', @@ -325,4 +378,11 @@ export class KubernetesBuilder { return objectTypesToFetch; } + + protected getProxy( + logger: Logger, + clusterSupplier: KubernetesClustersSupplier, + ) { + return this.proxy ?? this.buildProxy(logger, clusterSupplier); + } } diff --git a/plugins/kubernetes-backend/src/service/KubernetesProxy.test.ts b/plugins/kubernetes-backend/src/service/KubernetesProxy.test.ts new file mode 100644 index 0000000000..4254bb33da --- /dev/null +++ b/plugins/kubernetes-backend/src/service/KubernetesProxy.test.ts @@ -0,0 +1,119 @@ +/* + * 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 'buffer'; +import { getVoidLogger } from '@backstage/backend-common'; +import { NotFoundError } from '@backstage/errors'; +import { getMockReq, getMockRes } from '@jest-mock/express'; +import type { Request } from 'express'; +import express from 'express'; +import request from 'supertest'; +import { rest } from 'msw'; +import { setupServer } from 'msw/node'; +import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; +import { ClusterDetails, KubernetesClustersSupplier } from '../types/types'; +import { + APPLICATION_JSON, + HEADER_KUBERNETES_CLUSTER, + KubernetesProxy, +} from './KubernetesProxy'; + +describe('KubernetesProxy', () => { + let proxy: KubernetesProxy; + const worker = setupServer(); + + setupRequestMockHandlers(worker); + + const buildMockRequest = (clusterName: any, path: string): Request => { + const req = getMockReq({ + params: { + path, + }, + header: jest.fn((key: string) => { + switch (key) { + case 'Content-Type': { + return APPLICATION_JSON; + } + case HEADER_KUBERNETES_CLUSTER: { + return clusterName; + } + default: { + return ''; + } + } + }), + }); + + return req; + }; + + const clusterSupplier: jest.Mocked = { + getClusters: jest.fn(), + }; + + beforeEach(() => { + jest.resetAllMocks(); + proxy = new KubernetesProxy(getVoidLogger(), clusterSupplier); + }); + + it('should return a ERROR_NOT_FOUND if no clusters are found', async () => { + clusterSupplier.getClusters.mockResolvedValue([]); + + const req = buildMockRequest('test', 'api'); + const { res, next } = getMockRes(); + + await expect(proxy.createRequestHandler()(req, res, next)).rejects.toThrow( + NotFoundError, + ); + }); + + it('should pass the exact response from Kubernetes', async () => { + const apiResponse = { + kind: 'APIVersions', + versions: ['v1'], + serverAddressByClientCIDRs: [ + { + clientCIDR: '0.0.0.0/0', + serverAddress: '192.168.0.1:3333', + }, + ], + }; + + clusterSupplier.getClusters.mockResolvedValue([ + { + name: 'cluster1', + url: 'https://localhost:9999', + serviceAccountToken: '', + authProvider: 'serviceAccount', + }, + ] as ClusterDetails[]); + const app = express().use('/mountpath', proxy.createRequestHandler()); + const requestPromise = request(app) + .get('/mountpath/api') + .set(HEADER_KUBERNETES_CLUSTER, 'cluster1'); + worker.use( + rest.get('https://localhost:9999/api', (_, res, ctx) => + res(ctx.status(299), ctx.json(apiResponse)), + ), + rest.all(requestPromise.url, (req, _res, _ctx) => req.passthrough()), + ); + + const response = await requestPromise; + + expect(response.status).toEqual(299); + expect(response.body).toStrictEqual(apiResponse); + }); +}); diff --git a/plugins/kubernetes-backend/src/service/KubernetesProxy.ts b/plugins/kubernetes-backend/src/service/KubernetesProxy.ts new file mode 100644 index 0000000000..9d88d2ab84 --- /dev/null +++ b/plugins/kubernetes-backend/src/service/KubernetesProxy.ts @@ -0,0 +1,130 @@ +/* + * 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 { + ErrorResponseBody, + ForwardedError, + InputError, + NotFoundError, + serializeError, +} from '@backstage/errors'; +import { bufferFromFileOrString } from '@kubernetes/client-node'; +import type { Request, RequestHandler } from 'express'; +import { createProxyMiddleware } from 'http-proxy-middleware'; +import { Logger } from 'winston'; +import { ClusterDetails, KubernetesClustersSupplier } from '../types/types'; + +export const APPLICATION_JSON: string = 'application/json'; + +/** + * The header that is used to specify the cluster name. + * + * @alpha + */ +export const HEADER_KUBERNETES_CLUSTER: string = 'X-Kubernetes-Cluster'; + +/** + * A proxy that routes requests to the Kubernetes API. + * + * @alpha + */ +export class KubernetesProxy { + private readonly middlewareForClusterName = new Map(); + + constructor( + private readonly logger: Logger, + private readonly clusterSupplier: KubernetesClustersSupplier, + ) {} + + public createRequestHandler(): RequestHandler { + return async (req, res, next) => { + const middleware = await this.getMiddleware(req); + middleware(req, res, next); + }; + } + + // We create one middleware per remote cluster and hold on to them, because + // the secure property isn't possible to decide on a per-request basis with a + // single middleware instance - and we don't expect it to change over time. + private async getMiddleware(originalReq: Request): Promise { + const originalCluster = await this.getClusterForRequest(originalReq); + let middleware = this.middlewareForClusterName.get(originalCluster.name); + if (!middleware) { + // Probably too risky without permissions protecting this endpoint + // if (cluster.serviceAccountToken) { + // options.headers = { + // Authorization: `Bearer ${cluster.serviceAccountToken}`, + // }; + // } + + const logger = this.logger.child({ cluster: originalCluster.name }); + middleware = createProxyMiddleware({ + logProvider: () => logger, + secure: !originalCluster.skipTLSVerify, + router: async req => { + // Re-evaluate the cluster on each request, in case it has changed + const cluster = await this.getClusterForRequest(req); + const url = new URL(cluster.url); + return { + protocol: url.protocol, + host: url.hostname, + port: url.port, + ca: bufferFromFileOrString('', cluster.caData)?.toString(), + }; + }, + pathRewrite: { [`^${originalReq.baseUrl}`]: '' }, + onError: (error, req, res) => { + const wrappedError = new ForwardedError( + `Cluster '${originalCluster.name}' request error`, + error, + ); + + logger.error(wrappedError); + + const body: ErrorResponseBody = { + error: serializeError(wrappedError, { + includeStack: process.env.NODE_ENV === 'development', + }), + request: { method: req.method, url: req.originalUrl }, + response: { statusCode: 500 }, + }; + + res.status(500).json(body); + }, + }); + + this.middlewareForClusterName.set(originalCluster.name, middleware); + } + + return middleware; + } + + private async getClusterForRequest(req: Request): Promise { + const clusterName = req.header(HEADER_KUBERNETES_CLUSTER); + if (!clusterName) { + throw new InputError(`Missing '${HEADER_KUBERNETES_CLUSTER}' header.`); + } + + const cluster = await this.clusterSupplier + .getClusters() + .then(clusters => clusters.find(c => c.name === clusterName)); + if (!cluster) { + throw new NotFoundError(`Cluster '${clusterName}' not found`); + } + + return cluster; + } +} diff --git a/plugins/kubernetes-backend/src/service/index.ts b/plugins/kubernetes-backend/src/service/index.ts new file mode 100644 index 0000000000..70ff530bee --- /dev/null +++ b/plugins/kubernetes-backend/src/service/index.ts @@ -0,0 +1,21 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './KubernetesBuilder'; +export * from './KubernetesClientProvider'; +export { DEFAULT_OBJECTS } from './KubernetesFanOutHandler'; +export { HEADER_KUBERNETES_CLUSTER, KubernetesProxy } from './KubernetesProxy'; +export * from './router'; diff --git a/plugins/kubernetes-backend/src/types/index.ts b/plugins/kubernetes-backend/src/types/index.ts new file mode 100644 index 0000000000..db229eae34 --- /dev/null +++ b/plugins/kubernetes-backend/src/types/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './types'; diff --git a/yarn.lock b/yarn.lock index 4134fc3035..8e941eb7cb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5964,6 +5964,7 @@ __metadata: dependencies: "@azure/identity": ^2.0.4 "@backstage/backend-common": "workspace:^" + "@backstage/backend-test-utils": "workspace:^" "@backstage/catalog-client": "workspace:^" "@backstage/catalog-model": "workspace:^" "@backstage/cli": "workspace:^" @@ -5972,9 +5973,11 @@ __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 + "@types/http-proxy-middleware": ^0.19.3 "@types/luxon": ^3.0.0 aws-sdk: ^2.840.0 aws-sdk-mock: ^5.2.1 @@ -5985,9 +5988,12 @@ __metadata: express-promise-router: ^4.1.0 fs-extra: 10.1.0 helmet: ^6.0.0 + http-proxy-middleware: ^2.0.6 lodash: ^4.17.21 luxon: ^3.0.0 morgan: ^1.10.0 + msw: ^0.49.0 + node-fetch: ^2.6.7 stream-buffers: ^3.0.2 supertest: ^6.1.3 winston: ^3.2.1 @@ -9265,6 +9271,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" @@ -23359,7 +23374,7 @@ __metadata: languageName: node linkType: hard -"http-proxy-middleware@npm:^2.0.0, http-proxy-middleware@npm:^2.0.3": +"http-proxy-middleware@npm:^2.0.0, http-proxy-middleware@npm:^2.0.3, http-proxy-middleware@npm:^2.0.6": version: 2.0.6 resolution: "http-proxy-middleware@npm:2.0.6" dependencies: