From 6ab6e5c2bfde12425eab33c6251b27a63d76cac8 Mon Sep 17 00:00:00 2001 From: benjdlambert Date: Tue, 19 Aug 2025 09:27:15 +0200 Subject: [PATCH] feat: last cleanup and fix tests Signed-off-by: benjdlambert --- plugins/kubernetes-backend/src/plugin.ts | 169 +++--------- ...ilder.test.ts => KubernetesRouter.test.ts} | 0 ...bernetesBuilder.ts => KubernetesRouter.ts} | 22 +- .../src/service/KubernetnesInitializer.ts | 242 ++++++++++++++++++ .../kubernetes-backend/src/service/router.ts | 56 ---- 5 files changed, 279 insertions(+), 210 deletions(-) rename plugins/kubernetes-backend/src/service/{KubernetesBuilder.test.ts => KubernetesRouter.test.ts} (100%) rename plugins/kubernetes-backend/src/service/{KubernetesBuilder.ts => KubernetesRouter.ts} (94%) create mode 100644 plugins/kubernetes-backend/src/service/KubernetnesInitializer.ts delete mode 100644 plugins/kubernetes-backend/src/service/router.ts diff --git a/plugins/kubernetes-backend/src/plugin.ts b/plugins/kubernetes-backend/src/plugin.ts index 7b2d157911..a7bd323068 100644 --- a/plugins/kubernetes-backend/src/plugin.ts +++ b/plugins/kubernetes-backend/src/plugin.ts @@ -22,7 +22,6 @@ import { catalogServiceRef } from '@backstage/plugin-catalog-node'; import { type AuthenticationStrategy, - CustomResource, kubernetesAuthStrategyExtensionPoint, type KubernetesAuthStrategyExtensionPoint, type KubernetesClustersSupplier, @@ -37,25 +36,13 @@ import { kubernetesObjectsProviderExtensionPoint, type KubernetesObjectsProviderExtensionPoint, KubernetesObjectsProviderFactory, - KubernetesObjectTypes, type KubernetesServiceLocator, kubernetesServiceLocatorExtensionPoint, type KubernetesServiceLocatorExtensionPoint, KubernetesServiceLocatorFactory, - ObjectToFetch, } from '@backstage/plugin-kubernetes-node'; -import { KubernetesBuilder } from './service/KubernetesBuilder'; -import { KubernetesClientBasedFetcher } from './service/KubernetesFetcher'; -import { DispatchStrategy } from './auth/DispatchStrategy'; -import { getCombinedClusterSupplier } from './cluster-locator'; -import { buildDefaultAuthStrategyMap } from './auth/buildDefaultAuthStrategyMap'; -import { buildDefaultServiceLocator } from './service-locator/buildDefaultServiceLocator'; -import { Duration } from 'luxon'; -import { - ALL_OBJECTS, - DEFAULT_OBJECTS, - KubernetesFanOutHandler, -} from './service/KubernetesFanOutHandler'; +import { KubernetesRouter } from './service/KubernetesRouter'; +import { KubernetesInitializer } from './service/KubernetnesInitializer'; class ObjectsProvider implements KubernetesObjectsProviderExtensionPoint { private objectsProvider: KubernetesObjectsProviderFactory | undefined; @@ -151,20 +138,21 @@ class ServiceLocator implements KubernetesServiceLocatorExtensionPoint { } class AuthStrategy implements KubernetesAuthStrategyExtensionPoint { - private authStrategies: Map; - - constructor() { - this.authStrategies = new Map(); - } + private authStrategies: Map | undefined; getAuthenticationStrategies() { return this.authStrategies; } addAuthStrategy(key: string, authStrategy: AuthenticationStrategy) { + if (!this.authStrategies) { + this.authStrategies = new Map(); + } + if (key.includes('-')) { throw new Error('Strategy name can not include dashes'); } + this.authStrategies.set(key, authStrategy); } } @@ -226,121 +214,27 @@ export const kubernetesPlugin = createBackendPlugin({ }) { // TODO: this could do with a cleanup and push some of this initalization somewhere else if (config.has('kubernetes')) { - const defaultFetcherFactory = async () => - new KubernetesClientBasedFetcher({ - logger, - }); - - const fetcher = - extPointFetcher.getFetcher()?.({ - getDefault: defaultFetcherFactory, - }) ?? defaultFetcherFactory(); - - const returnedAuthStrategyMap = - extPointAuthStrategy.getAuthenticationStrategies(); - - const authStrategyMap = - returnedAuthStrategyMap.size > 0 - ? returnedAuthStrategyMap - : buildDefaultAuthStrategyMap({ logger, config }); - - const refreshInterval = Duration.fromObject({ - minutes: 60, + const initializer = KubernetesInitializer.create({ + logger, + config, + catalog, + auth, + fetcher: extPointFetcher.getFetcher(), + clusterSupplier: extPointClusterSuplier.getClusterSupplier(), + serviceLocator: extPointServiceLocator.getServiceLocator(), + objectsProvider: extPointObjectsProvider.getObjectsProvider(), + authStrategyMap: extPointAuthStrategy.getAuthenticationStrategies(), }); - const defaultClusterSupplierFactory = async () => - getCombinedClusterSupplier( - config, - catalog, - new DispatchStrategy({ - authStrategyMap: Object.fromEntries(authStrategyMap.entries()), - }), - logger, - refreshInterval, - auth, - ); + const { + fetcher, + authStrategyMap, + clusterSupplier, + serviceLocator, + objectsProvider, + } = await initializer.init(); - const clusterSupplier = - extPointClusterSuplier.getClusterSupplier()?.({ - getDefault: defaultClusterSupplierFactory, - }) ?? defaultClusterSupplierFactory(); - - const defaultServiceLocatorFactory = async () => - buildDefaultServiceLocator({ - config, - clusterSupplier: await clusterSupplier, - }); - - const serviceLocator = - extPointServiceLocator.getServiceLocator()?.({ - getDefault: defaultServiceLocatorFactory, - clusterSupplier: await clusterSupplier, - }) ?? defaultServiceLocatorFactory(); - - const objectTypesToFetchStrings = config.getOptionalStringArray( - 'kubernetes.objectTypes', - ) as KubernetesObjectTypes[]; - - const apiVersionOverrides = config.getOptionalConfig( - 'kubernetes.apiVersionOverrides', - ); - - let objectTypesToFetch: ObjectToFetch[] | undefined = undefined; - - if (objectTypesToFetchStrings) { - objectTypesToFetch = ALL_OBJECTS.filter(obj => - objectTypesToFetchStrings.includes(obj.objectType), - ); - } - - if (apiVersionOverrides) { - objectTypesToFetch ??= DEFAULT_OBJECTS; - - for (const obj of objectTypesToFetch) { - if (apiVersionOverrides.has(obj.objectType)) { - obj.apiVersion = apiVersionOverrides.getString(obj.objectType); - } - } - } - - const customResources: CustomResource[] = ( - config.getOptionalConfigArray('kubernetes.customResources') ?? [] - ).map( - c => - ({ - group: c.getString('group'), - apiVersion: c.getString('apiVersion'), - plural: c.getString('plural'), - objectType: 'customresources', - } as CustomResource), - ); - - const defaultObjectsProviderFactory = async () => - new KubernetesFanOutHandler({ - logger, - config, - fetcher: await fetcher, - serviceLocator: await serviceLocator, - customResources, - objectTypesToFetch, - authStrategy: new DispatchStrategy({ - authStrategyMap: Object.fromEntries(authStrategyMap.entries()), - }), - }); - - const objectsProvider = - extPointObjectsProvider.getObjectsProvider()?.({ - clusterSupplier: await clusterSupplier, - getDefault: defaultObjectsProviderFactory, - serviceLocator: await serviceLocator, - customResources, - objectTypesToFetch, - authStrategy: new DispatchStrategy({ - authStrategyMap: Object.fromEntries(authStrategyMap.entries()), - }), - }) ?? defaultObjectsProviderFactory(); - - const builder: KubernetesBuilder = KubernetesBuilder.createBuilder({ + const router = KubernetesRouter.create({ logger, config, catalog, @@ -349,14 +243,13 @@ export const kubernetesPlugin = createBackendPlugin({ auth, httpAuth, authStrategyMap: Object.fromEntries(authStrategyMap.entries()), - fetcher: await fetcher, - clusterSupplier: await clusterSupplier, - serviceLocator: await serviceLocator, - objectsProvider: await objectsProvider, + fetcher, + clusterSupplier, + serviceLocator, + objectsProvider, }); - const { router } = await builder.build(); - http.use(router); + http.use(await router.getRouter()); } else { logger.warn( 'Failed to initialize kubernetes backend: valid kubernetes config is missing', diff --git a/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts b/plugins/kubernetes-backend/src/service/KubernetesRouter.test.ts similarity index 100% rename from plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts rename to plugins/kubernetes-backend/src/service/KubernetesRouter.test.ts diff --git a/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts b/plugins/kubernetes-backend/src/service/KubernetesRouter.ts similarity index 94% rename from plugins/kubernetes-backend/src/service/KubernetesBuilder.ts rename to plugins/kubernetes-backend/src/service/KubernetesRouter.ts index 93400d2f73..5a5518670e 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesRouter.ts @@ -64,18 +64,14 @@ export interface KubernetesEnvironment { objectsProvider: KubernetesObjectsProvider; } -export type KubernetesBuilderReturn = Promise<{ - router: express.Router; -}>; - -export class KubernetesBuilder { - static createBuilder(env: KubernetesEnvironment) { - return new KubernetesBuilder(env); +export class KubernetesRouter { + static create(env: KubernetesEnvironment) { + return new KubernetesRouter(env); } constructor(protected readonly env: KubernetesEnvironment) {} - public async build(): KubernetesBuilderReturn { + public async getRouter() { const { logger, config, @@ -97,9 +93,7 @@ export class KubernetesBuilder { logger.warn( 'Failed to initialize kubernetes backend: kubernetes config is missing', ); - return { - router: Router(), - } as unknown as KubernetesBuilderReturn; + return Router(); } const proxy = this.buildProxy( @@ -110,7 +104,7 @@ export class KubernetesBuilder { authStrategyMap, ); - const router = this.buildRouter( + return this.buildRouter( objectsProvider, clusterSupplier, catalog, @@ -119,10 +113,6 @@ export class KubernetesBuilder { httpAuth, authStrategyMap, ); - - return { - router, - }; } private buildProxy( diff --git a/plugins/kubernetes-backend/src/service/KubernetnesInitializer.ts b/plugins/kubernetes-backend/src/service/KubernetnesInitializer.ts new file mode 100644 index 0000000000..8e85bb152c --- /dev/null +++ b/plugins/kubernetes-backend/src/service/KubernetnesInitializer.ts @@ -0,0 +1,242 @@ +/* + * Copyright 2025 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 { + AuthenticationStrategy, + CustomResource, + KubernetesClustersSupplier, + KubernetesClusterSupplierFactory, + KubernetesFetcher, + KubernetesFetcherFactory, + KubernetesObjectsProviderFactory, + KubernetesObjectTypes, + KubernetesServiceLocator, + KubernetesServiceLocatorFactory, + ObjectToFetch, +} from '@backstage/plugin-kubernetes-node'; +import { KubernetesClientBasedFetcher } from './KubernetesFetcher'; +import { + AuthService, + LoggerService, + RootConfigService, +} from '@backstage/backend-plugin-api'; +import { buildDefaultAuthStrategyMap } from '../auth/buildDefaultAuthStrategyMap'; +import { Duration } from 'luxon'; +import { getCombinedClusterSupplier } from '../cluster-locator'; +import { DispatchStrategy } from '../auth/DispatchStrategy'; +import { CatalogService } from '@backstage/plugin-catalog-node'; +import { buildDefaultServiceLocator } from '../service-locator/buildDefaultServiceLocator'; +import { + ALL_OBJECTS, + DEFAULT_OBJECTS, + KubernetesFanOutHandler, +} from './KubernetesFanOutHandler'; + +export class KubernetesInitializer { + constructor( + private readonly opts: { + fetcher?: KubernetesFetcherFactory; + authStrategyMap?: Map; + clusterSupplier?: KubernetesClusterSupplierFactory; + logger: LoggerService; + config: RootConfigService; + catalog: CatalogService; + auth: AuthService; + serviceLocator?: KubernetesServiceLocatorFactory; + objectsProvider?: KubernetesObjectsProviderFactory; + }, + ) {} + + static create(opts: { + fetcher?: KubernetesFetcherFactory; + clusterSupplier?: KubernetesClusterSupplierFactory; + serviceLocator?: KubernetesServiceLocatorFactory; + objectsProvider?: KubernetesObjectsProviderFactory; + authStrategyMap?: Map; + logger: LoggerService; + config: RootConfigService; + catalog: CatalogService; + auth: AuthService; + }) { + return new KubernetesInitializer(opts); + } + + private async defaultFetcher() { + return new KubernetesClientBasedFetcher({ + logger: this.opts.logger, + }); + } + + private async defaultAuthStrategy() { + return buildDefaultAuthStrategyMap({ + logger: this.opts.logger, + config: this.opts.config, + }); + } + + private async defaultClusterSupplier(opts: { + authStrategyMap: Map; + }) { + const refreshInterval = Duration.fromObject({ + minutes: 60, + }); + + return getCombinedClusterSupplier( + this.opts.config, + this.opts.catalog, + new DispatchStrategy({ + authStrategyMap: Object.fromEntries(opts.authStrategyMap.entries()), + }), + this.opts.logger, + refreshInterval, + this.opts.auth, + ); + } + + private async defaultServiceLocator(opts: { + clusterSupplier: KubernetesClustersSupplier; + }) { + return buildDefaultServiceLocator({ + config: this.opts.config, + clusterSupplier: opts.clusterSupplier, + }); + } + + private async defaultObjectsProvider(opts: { + clusterSupplier: KubernetesClustersSupplier; + authStrategyMap: Map; + fetcher: KubernetesFetcher; + serviceLocator: KubernetesServiceLocator; + customResources: CustomResource[]; + objectTypesToFetch: ObjectToFetch[]; + }) { + return new KubernetesFanOutHandler({ + logger: this.opts.logger, + config: this.opts.config, + fetcher: opts.fetcher, + serviceLocator: opts.serviceLocator, + customResources: opts.customResources, + objectTypesToFetch: opts.objectTypesToFetch, + authStrategy: new DispatchStrategy({ + authStrategyMap: Object.fromEntries(opts.authStrategyMap.entries()), + }), + }); + } + + private async defaultObjectsProviderOptions() { + const customResources: CustomResource[] = ( + this.opts.config.getOptionalConfigArray('kubernetes.customResources') ?? + [] + ).map( + c => + ({ + group: c.getString('group'), + apiVersion: c.getString('apiVersion'), + plural: c.getString('plural'), + objectType: 'customresources', + } as CustomResource), + ); + const objectTypesToFetchStrings = this.opts.config.getOptionalStringArray( + 'kubernetes.objectTypes', + ) as KubernetesObjectTypes[]; + + const apiVersionOverrides = this.opts.config.getOptionalConfig( + 'kubernetes.apiVersionOverrides', + ); + + let objectTypesToFetch: ObjectToFetch[] | undefined = undefined; + + if (objectTypesToFetchStrings) { + objectTypesToFetch = ALL_OBJECTS.filter(obj => + objectTypesToFetchStrings.includes(obj.objectType), + ); + } + + if (apiVersionOverrides) { + objectTypesToFetch ??= DEFAULT_OBJECTS; + + for (const obj of objectTypesToFetch) { + if (apiVersionOverrides.has(obj.objectType)) { + obj.apiVersion = apiVersionOverrides.getString(obj.objectType); + } + } + } + + return { + customResources, + objectTypesToFetch: objectTypesToFetch ?? [], + }; + } + + async init() { + const fetcher = + (await this.opts.fetcher?.({ getDefault: this.defaultFetcher })) ?? + (await this.defaultFetcher()); + + const authStrategyMap = + this.opts.authStrategyMap ?? (await this.defaultAuthStrategy()); + + const clusterSupplier = + (await this.opts.clusterSupplier?.({ + getDefault: () => this.defaultClusterSupplier({ authStrategyMap }), + })) ?? (await this.defaultClusterSupplier({ authStrategyMap })); + + const serviceLocator = + (await this.opts.serviceLocator?.({ + getDefault: () => this.defaultServiceLocator({ clusterSupplier }), + clusterSupplier, + })) ?? (await this.defaultServiceLocator({ clusterSupplier })); + + const { customResources, objectTypesToFetch } = + await this.defaultObjectsProviderOptions(); + + const objectsProvider = + (await this.opts.objectsProvider?.({ + getDefault: () => + this.defaultObjectsProvider({ + clusterSupplier, + authStrategyMap, + fetcher, + serviceLocator, + customResources, + objectTypesToFetch, + }), + clusterSupplier, + serviceLocator, + customResources, + objectTypesToFetch, + authStrategy: new DispatchStrategy({ + authStrategyMap: Object.fromEntries(authStrategyMap.entries()), + }), + })) ?? + (await this.defaultObjectsProvider({ + clusterSupplier, + authStrategyMap, + fetcher, + serviceLocator, + customResources, + objectTypesToFetch, + })); + + return { + fetcher, + authStrategyMap, + clusterSupplier, + serviceLocator, + objectsProvider, + }; + } +} diff --git a/plugins/kubernetes-backend/src/service/router.ts b/plugins/kubernetes-backend/src/service/router.ts deleted file mode 100644 index 8a095d00f0..0000000000 --- a/plugins/kubernetes-backend/src/service/router.ts +++ /dev/null @@ -1,56 +0,0 @@ -/* - * 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 { - AuthenticationStrategy, - KubernetesClustersSupplier, - KubernetesFetcher, - KubernetesObjectsProvider, - KubernetesServiceLocator, -} from '@backstage/plugin-kubernetes-node'; -import express from 'express'; -import { KubernetesBuilder } from './KubernetesBuilder'; -import { PermissionEvaluator } from '@backstage/plugin-permission-common'; -import { - DiscoveryService, - HttpAuthService, - RootConfigService, - AuthService, -} from '@backstage/backend-plugin-api'; -import { CatalogService } from '@backstage/plugin-catalog-node'; - -interface RouterOptions { - logger: Logger; - config: RootConfigService; - catalog: CatalogService; - clusterSupplier: KubernetesClustersSupplier; - discovery: DiscoveryService; - permissions: PermissionEvaluator; - auth: AuthService; - httpAuth: HttpAuthService; - authStrategyMap: { [key: string]: AuthenticationStrategy }; - fetcher: KubernetesFetcher; - serviceLocator: KubernetesServiceLocator; - objectsProvider: KubernetesObjectsProvider; -} - -export async function createRouter( - options: RouterOptions, -): Promise { - const { router } = await KubernetesBuilder.createBuilder(options).build(); - return router; -}