diff --git a/.changeset/full-needles-drive.md b/.changeset/full-needles-drive.md new file mode 100644 index 0000000000..d0d7a59551 --- /dev/null +++ b/.changeset/full-needles-drive.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-kubernetes-backend': minor +'@backstage/plugin-kubernetes-node': minor +--- + +Add possibility to extends Kubernetes REST API. Add fetcher to parameters for custom objects provider diff --git a/plugins/kubernetes-backend/src/plugin.ts b/plugins/kubernetes-backend/src/plugin.ts index b4283d4914..5a2a22f2b5 100644 --- a/plugins/kubernetes-backend/src/plugin.ts +++ b/plugins/kubernetes-backend/src/plugin.ts @@ -36,6 +36,9 @@ import { kubernetesObjectsProviderExtensionPoint, type KubernetesObjectsProviderExtensionPoint, KubernetesObjectsProviderFactory, + KubernetesRouterExtensionPoint, + kubernetesRouterExtensionPoint, + KubernetesRouterFactory, type KubernetesServiceLocator, kubernetesServiceLocatorExtensionPoint, type KubernetesServiceLocatorExtensionPoint, @@ -157,6 +160,24 @@ class AuthStrategy implements KubernetesAuthStrategyExtensionPoint { } } +class CustomRouter implements KubernetesRouterExtensionPoint { + private router: KubernetesRouterFactory | undefined; + + getRouter() { + return this.router; + } + + addRouter(router: KubernetesRouterFactory) { + if (this.router) { + throw new Error( + 'Multiple Kubernetes routers is not supported at this time', + ); + } + + this.router = router; + } +} + /** * This is the backend plugin that provides the Kubernetes integration. * @public @@ -169,6 +190,7 @@ export const kubernetesPlugin = createBackendPlugin({ const extPointAuthStrategy = new AuthStrategy(); const extPointFetcher = new Fetcher(); const extPointServiceLocator = new ServiceLocator(); + const extPointRouter = new CustomRouter(); env.registerExtensionPoint( kubernetesObjectsProviderExtensionPoint, @@ -190,6 +212,7 @@ export const kubernetesPlugin = createBackendPlugin({ kubernetesServiceLocatorExtensionPoint, extPointServiceLocator, ); + env.registerExtensionPoint(kubernetesRouterExtensionPoint, extPointRouter); env.registerInit({ deps: { @@ -247,6 +270,7 @@ export const kubernetesPlugin = createBackendPlugin({ clusterSupplier, serviceLocator, objectsProvider, + customRouter: extPointRouter.getRouter(), }); http.use(await router.getRouter()); diff --git a/plugins/kubernetes-backend/src/service/KubernetesInitializer.ts b/plugins/kubernetes-backend/src/service/KubernetesInitializer.ts index 46ce5fa888..a12f291ba2 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesInitializer.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesInitializer.ts @@ -209,6 +209,7 @@ export class KubernetesInitializer { customResources, objectTypesToFetch, }), + fetcher, clusterSupplier, serviceLocator, customResources, diff --git a/plugins/kubernetes-backend/src/service/KubernetesRouter.test.ts b/plugins/kubernetes-backend/src/service/KubernetesRouter.test.ts index 4157cf9444..eb643704c0 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesRouter.test.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesRouter.test.ts @@ -28,6 +28,7 @@ import { KubernetesFetcher, KubernetesServiceLocator, KubernetesCredential, + kubernetesRouterExtensionPoint, } from '@backstage/plugin-kubernetes-node'; import { HEADER_KUBERNETES_CLUSTER, @@ -809,4 +810,39 @@ metadata: 'Unsupported kubernetes.serviceLocatorMethod "unsupported"', ); }); + + it('custom router', async () => { + const { server } = await startTestBackend({ + features: [ + minimalValidConfigService, + import('@backstage/plugin-kubernetes-backend'), + createBackendModule({ + pluginId: 'kubernetes', + moduleId: 'testRouter', + register(env) { + env.registerInit({ + deps: { extension: kubernetesRouterExtensionPoint }, + async init({ extension }) { + extension.addRouter(({ getDefault }) => { + const router = getDefault(); + + router.get('/test', (_req, res) => { + res.json({ status: 'ok' }); + }); + + return router; + }); + }, + }); + }, + }), + ], + }); + app = server; + + const response = await request(app).get('/api/kubernetes/test'); + + expect(response.body).toEqual({ status: 'ok' }); + expect(response.status).toEqual(200); + }); }); diff --git a/plugins/kubernetes-backend/src/service/KubernetesRouter.ts b/plugins/kubernetes-backend/src/service/KubernetesRouter.ts index 589e75a172..48842f562c 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesRouter.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesRouter.ts @@ -41,6 +41,7 @@ import { KubernetesClustersSupplier, KubernetesFetcher, KubernetesObjectsProvider, + KubernetesRouterFactory, KubernetesServiceLocator, } from '@backstage/plugin-kubernetes-node'; import { addResourceRoutesToRouter } from '../routes/resourcesRoutes'; @@ -62,6 +63,7 @@ export interface KubernetesEnvironment { clusterSupplier: KubernetesClustersSupplier; serviceLocator: KubernetesServiceLocator; objectsProvider: KubernetesObjectsProvider; + customRouter?: KubernetesRouterFactory; } export class KubernetesRouter { @@ -86,6 +88,7 @@ export class KubernetesRouter { catalog, discovery, httpAuth, + customRouter, } = this.env; logger.info('Initializing Kubernetes backend'); @@ -108,14 +111,31 @@ export class KubernetesRouter { authStrategyMap, ); - return this.buildRouter( - objectsProvider, - clusterSupplier, - catalog, - proxy, - permissions, - httpAuth, - authStrategyMap, + return ( + customRouter?.({ + getDefault: () => + this.buildDefaultRouter( + objectsProvider, + clusterSupplier, + catalog, + proxy, + permissions, + httpAuth, + authStrategyMap, + ), + objectsProvider, + clusterSupplier, + authStrategyMap, + }) ?? + this.buildDefaultRouter( + objectsProvider, + clusterSupplier, + catalog, + proxy, + permissions, + httpAuth, + authStrategyMap, + ) ); } @@ -138,7 +158,7 @@ export class KubernetesRouter { }); } - private buildRouter( + private buildDefaultRouter( objectsProvider: KubernetesObjectsProvider, clusterSupplier: KubernetesClustersSupplier, catalog: CatalogService, diff --git a/plugins/kubernetes-node/package.json b/plugins/kubernetes-node/package.json index 0ae0bedd85..9796e05212 100644 --- a/plugins/kubernetes-node/package.json +++ b/plugins/kubernetes-node/package.json @@ -43,6 +43,7 @@ "@backstage/plugin-kubernetes-common": "workspace:^", "@backstage/types": "workspace:^", "@kubernetes/client-node": "1.4.0", + "@types/express": "^4.17.6", "node-fetch": "^2.7.0", "winston": "^3.2.1" }, diff --git a/plugins/kubernetes-node/report.api.md b/plugins/kubernetes-node/report.api.md index 2e96057be8..c23a0b5bb8 100644 --- a/plugins/kubernetes-node/report.api.md +++ b/plugins/kubernetes-node/report.api.md @@ -8,6 +8,7 @@ import { BackstageCredentials } from '@backstage/backend-plugin-api'; import { CustomResource as CustomResource_2 } from '@backstage/plugin-kubernetes-node'; import { CustomResourceMatcher } from '@backstage/plugin-kubernetes-common'; import { Entity } from '@backstage/catalog-model'; +import type express from 'express'; import { ExtensionPoint } from '@backstage/backend-plugin-api'; import { FetchResponse } from '@backstage/plugin-kubernetes-common'; import { JsonObject } from '@backstage/types'; @@ -198,6 +199,7 @@ export type KubernetesObjectsProviderFactory = (opts: { getDefault: () => Promise; clusterSupplier: KubernetesClustersSupplier_2; serviceLocator: KubernetesServiceLocator_2; + fetcher: KubernetesFetcher_2; customResources: CustomResource_2[]; objectTypesToFetch?: ObjectToFetch_2[]; authStrategy: AuthenticationStrategy_2; @@ -221,6 +223,25 @@ export type KubernetesObjectTypes = | 'daemonsets' | 'secrets'; +// @public +export interface KubernetesRouterExtensionPoint { + // (undocumented) + addRouter(router: KubernetesRouterFactory): void; +} + +// @public +export const kubernetesRouterExtensionPoint: ExtensionPoint; + +// @public +export type KubernetesRouterFactory = (opts: { + getDefault: () => express.Router; + objectsProvider: KubernetesObjectsProvider_2; + clusterSupplier: KubernetesClustersSupplier_2; + authStrategyMap: { + [key: string]: AuthenticationStrategy_2; + }; +}) => express.Router; + // @public export interface KubernetesServiceLocator { // (undocumented) diff --git a/plugins/kubernetes-node/src/extensions.ts b/plugins/kubernetes-node/src/extensions.ts index 6f112ece23..50b529d05b 100644 --- a/plugins/kubernetes-node/src/extensions.ts +++ b/plugins/kubernetes-node/src/extensions.ts @@ -23,6 +23,7 @@ import { KubernetesObjectsProvider, KubernetesServiceLocator, } from '@backstage/plugin-kubernetes-node'; +import type express from 'express'; /** * A factory function for creating a KubernetesObjectsProvider. @@ -33,6 +34,7 @@ export type KubernetesObjectsProviderFactory = (opts: { getDefault: () => Promise; clusterSupplier: KubernetesClustersSupplier; serviceLocator: KubernetesServiceLocator; + fetcher: KubernetesFetcher; customResources: CustomResource[]; objectTypesToFetch?: ObjectToFetch[]; authStrategy: AuthenticationStrategy; @@ -168,3 +170,34 @@ export const kubernetesServiceLocatorExtensionPoint = createExtensionPoint({ id: 'kubernetes.service-locator', }); + +/** + * A factory function for creating a kubernetes router. + * + * @public + */ +export type KubernetesRouterFactory = (opts: { + getDefault: () => express.Router; + objectsProvider: KubernetesObjectsProvider; + clusterSupplier: KubernetesClustersSupplier; + authStrategyMap: { [key: string]: AuthenticationStrategy }; +}) => express.Router; + +/** + * The interface for {@link kubernetesRouterExtensionPoint}. + * + * @public + */ +export interface KubernetesRouterExtensionPoint { + addRouter(router: KubernetesRouterFactory): void; +} + +/** + * An extension point the exposes the ability to configure a kubernetes service locator. + * + * @public + */ +export const kubernetesRouterExtensionPoint = + createExtensionPoint({ + id: 'kubernetes.router', + }); diff --git a/yarn.lock b/yarn.lock index 7fd7f16edb..d847deab41 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5894,6 +5894,7 @@ __metadata: "@backstage/plugin-kubernetes-common": "workspace:^" "@backstage/types": "workspace:^" "@kubernetes/client-node": "npm:1.4.0" + "@types/express": "npm:^4.17.6" msw: "npm:^1.3.1" node-fetch: "npm:^2.7.0" supertest: "npm:^7.0.0"