From d6d639a9ea64743e280244b7b6f9d4dffacc713c Mon Sep 17 00:00:00 2001 From: benjdlambert Date: Wed, 13 Aug 2025 07:48:16 +0200 Subject: [PATCH 1/8] chore: more of a refactor Signed-off-by: benjdlambert Signed-off-by: benjdlambert --- .../src/auth/DispatchStrategy.ts | 1 + .../src/auth/buildDefaultAuthStrategyMap.ts | 46 +++ .../cluster-locator/CatalogClusterLocator.ts | 27 +- .../src/cluster-locator/index.ts | 6 +- plugins/kubernetes-backend/src/plugin.ts | 252 ++++++++++-- .../src/routes/resourcesRoutes.ts | 14 +- .../buildDefaultServiceLocator.ts | 48 +++ .../src/service/KubernetesBuilder.ts | 387 ++---------------- .../kubernetes-backend/src/service/router.ts | 22 +- 9 files changed, 380 insertions(+), 423 deletions(-) create mode 100644 plugins/kubernetes-backend/src/auth/buildDefaultAuthStrategyMap.ts create mode 100644 plugins/kubernetes-backend/src/service-locator/buildDefaultServiceLocator.ts diff --git a/plugins/kubernetes-backend/src/auth/DispatchStrategy.ts b/plugins/kubernetes-backend/src/auth/DispatchStrategy.ts index 14a3009a65..5e1f8dd207 100644 --- a/plugins/kubernetes-backend/src/auth/DispatchStrategy.ts +++ b/plugins/kubernetes-backend/src/auth/DispatchStrategy.ts @@ -51,6 +51,7 @@ export class DispatchStrategy implements AuthenticationStrategy { ): Promise { const authProvider = clusterDetails.authMetadata[ANNOTATION_KUBERNETES_AUTH_PROVIDER]; + if (this.strategyMap[authProvider]) { return this.strategyMap[authProvider].getCredential(clusterDetails, auth); } diff --git a/plugins/kubernetes-backend/src/auth/buildDefaultAuthStrategyMap.ts b/plugins/kubernetes-backend/src/auth/buildDefaultAuthStrategyMap.ts new file mode 100644 index 0000000000..918cb759c3 --- /dev/null +++ b/plugins/kubernetes-backend/src/auth/buildDefaultAuthStrategyMap.ts @@ -0,0 +1,46 @@ +/* + * 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 { + LoggerService, + RootConfigService, +} from '@backstage/backend-plugin-api'; +import { AuthenticationStrategy } from '@backstage/plugin-kubernetes-node'; +import { AksStrategy } from './AksStrategy'; +import { AwsIamStrategy } from './AwsIamStrategy'; +import { AzureIdentityStrategy } from './AzureIdentityStrategy'; +import { GoogleStrategy } from './GoogleStrategy'; +import { GoogleServiceAccountStrategy } from './GoogleServiceAccountStrategy'; +import { AnonymousStrategy } from './AnonymousStrategy'; +import { OidcStrategy } from './OidcStrategy'; +import { ServiceAccountStrategy } from './ServiceAccountStrategy'; + +export const buildDefaultAuthStrategyMap = ({ + logger, + config, +}: { + logger: LoggerService; + config: RootConfigService; +}): Map => + new Map([ + ['aks', new AksStrategy()], + ['aws', new AwsIamStrategy({ config })], + ['azure', new AzureIdentityStrategy(logger)], + ['google', new GoogleStrategy()], + ['googleServiceAccount', new GoogleServiceAccountStrategy()], + ['localKubectlProxy', new AnonymousStrategy()], + ['oidc', new OidcStrategy()], + ['serviceAccount', new ServiceAccountStrategy()], + ]); diff --git a/plugins/kubernetes-backend/src/cluster-locator/CatalogClusterLocator.ts b/plugins/kubernetes-backend/src/cluster-locator/CatalogClusterLocator.ts index 569b55950a..6d34234c5e 100644 --- a/plugins/kubernetes-backend/src/cluster-locator/CatalogClusterLocator.ts +++ b/plugins/kubernetes-backend/src/cluster-locator/CatalogClusterLocator.ts @@ -22,7 +22,7 @@ import { ClusterDetails, KubernetesClustersSupplier, } from '@backstage/plugin-kubernetes-node'; -import { CATALOG_FILTER_EXISTS, CatalogApi } from '@backstage/catalog-client'; +import { CATALOG_FILTER_EXISTS } from '@backstage/catalog-client'; import { ANNOTATION_KUBERNETES_API_SERVER, ANNOTATION_KUBERNETES_API_SERVER_CA, @@ -34,22 +34,23 @@ import { ANNOTATION_KUBERNETES_DASHBOARD_PARAMETERS, } from '@backstage/plugin-kubernetes-common'; import { JsonObject } from '@backstage/types'; +import { CatalogService } from '@backstage/plugin-catalog-node'; function isObject(obj: unknown): obj is JsonObject { return typeof obj === 'object' && obj !== null && !Array.isArray(obj); } export class CatalogClusterLocator implements KubernetesClustersSupplier { - private catalogClient: CatalogApi; + private catalogService: CatalogService; private auth: AuthService; - constructor(catalogClient: CatalogApi, auth: AuthService) { - this.catalogClient = catalogClient; + constructor(catalogService: CatalogService, auth: AuthService) { + this.catalogService = catalogService; this.auth = auth; } static fromConfig( - catalogApi: CatalogApi, + catalogApi: CatalogService, auth: AuthService, ): CatalogClusterLocator { return new CatalogClusterLocator(catalogApi, auth); @@ -70,20 +71,14 @@ export class CatalogClusterLocator implements KubernetesClustersSupplier { [authProviderKey]: CATALOG_FILTER_EXISTS, }; - const clusters = await this.catalogClient.getEntities( + const clusters = await this.catalogService.getEntities( { filter: [filter], }, - options?.credentials - ? { - token: ( - await this.auth.getPluginRequestToken({ - onBehalfOf: options.credentials, - targetPluginId: 'catalog', - }) - ).token, - } - : undefined, + { + credentials: + options?.credentials ?? (await this.auth.getNoneCredentials()), + }, ); return clusters.items.map(entity => { const annotations = entity.metadata.annotations!; diff --git a/plugins/kubernetes-backend/src/cluster-locator/index.ts b/plugins/kubernetes-backend/src/cluster-locator/index.ts index c545823c1a..68e3e81209 100644 --- a/plugins/kubernetes-backend/src/cluster-locator/index.ts +++ b/plugins/kubernetes-backend/src/cluster-locator/index.ts @@ -14,7 +14,6 @@ * limitations under the License. */ -import { CatalogApi } from '@backstage/catalog-client'; import { Config } from '@backstage/config'; import { Duration } from 'luxon'; import { ConfigClusterLocator } from './ConfigClusterLocator'; @@ -31,6 +30,7 @@ import { ClusterDetails, KubernetesClustersSupplier, } from '@backstage/plugin-kubernetes-node'; +import { CatalogService } from '@backstage/plugin-catalog-node'; class CombinedClustersSupplier implements KubernetesClustersSupplier { constructor( @@ -72,7 +72,7 @@ class CombinedClustersSupplier implements KubernetesClustersSupplier { export const getCombinedClusterSupplier = ( rootConfig: Config, - catalogClient: CatalogApi, + catalogService: CatalogService, authStrategy: AuthenticationStrategy, logger: LoggerService, refreshInterval: Duration | undefined = undefined, @@ -84,7 +84,7 @@ export const getCombinedClusterSupplier = ( const type = clusterLocatorMethod.getString('type'); switch (type) { case 'catalog': - return CatalogClusterLocator.fromConfig(catalogClient, auth); + return CatalogClusterLocator.fromConfig(catalogService, auth); case 'localKubectlProxy': return new LocalKubectlProxyClusterLocator(); case 'config': diff --git a/plugins/kubernetes-backend/src/plugin.ts b/plugins/kubernetes-backend/src/plugin.ts index 8dfe53364b..f787f52f4a 100644 --- a/plugins/kubernetes-backend/src/plugin.ts +++ b/plugins/kubernetes-backend/src/plugin.ts @@ -18,10 +18,11 @@ import { coreServices, createBackendPlugin, } from '@backstage/backend-plugin-api'; -import { catalogServiceRef } from '@backstage/plugin-catalog-node/alpha'; +import { catalogServiceRef } from '@backstage/plugin-catalog-node'; import { type AuthenticationStrategy, + CustomResource, kubernetesAuthStrategyExtensionPoint, type KubernetesAuthStrategyExtensionPoint, type KubernetesClustersSupplier, @@ -33,14 +34,43 @@ import { type KubernetesObjectsProvider, kubernetesObjectsProviderExtensionPoint, type KubernetesObjectsProviderExtensionPoint, + KubernetesObjectTypes, type KubernetesServiceLocator, kubernetesServiceLocatorExtensionPoint, type KubernetesServiceLocatorExtensionPoint, + 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'; class ObjectsProvider implements KubernetesObjectsProviderExtensionPoint { - private objectsProvider: KubernetesObjectsProvider | undefined; + private objectsProvider: + | (({ + getDefault, + clusterSupplier, + serviceLocator, + customResources, + objectTypesToFetch, + authStrategy, + }: { + getDefault: () => KubernetesObjectsProvider; + clusterSupplier: KubernetesClustersSupplier; + serviceLocator: KubernetesServiceLocator; + customResources: CustomResource[]; + objectTypesToFetch?: ObjectToFetch[]; + authStrategy: AuthenticationStrategy; + }) => KubernetesObjectsProvider) + | undefined; getObjectsProvider() { return this.objectsProvider; @@ -52,12 +82,22 @@ class ObjectsProvider implements KubernetesObjectsProviderExtensionPoint { 'Multiple Kubernetes objects provider is not supported at this time', ); } - this.objectsProvider = provider; + if (typeof provider !== 'function') { + this.objectsProvider = () => provider; + } else { + this.objectsProvider = provider; + } } } class ClusterSuplier implements KubernetesClusterSupplierExtensionPoint { - private clusterSupplier: KubernetesClustersSupplier | undefined; + private clusterSupplier: + | (({ + getDefault, + }: { + getDefault: () => KubernetesClustersSupplier; + }) => KubernetesClustersSupplier) + | undefined; getClusterSupplier() { return this.clusterSupplier; @@ -69,29 +109,59 @@ class ClusterSuplier implements KubernetesClusterSupplierExtensionPoint { 'Multiple Kubernetes Cluster Suppliers is not supported at this time', ); } - this.clusterSupplier = clusterSupplier; + if (typeof clusterSupplier !== 'function') { + this.clusterSupplier = () => clusterSupplier; + } else { + this.clusterSupplier = clusterSupplier; + } } } class Fetcher implements KubernetesFetcherExtensionPoint { - private fetcher: KubernetesFetcher | undefined; + private fetcher: + | (({ + getDefault, + }: { + getDefault: () => KubernetesFetcher; + }) => KubernetesFetcher) + | undefined; getFetcher() { return this.fetcher; } - addFetcher(fetcher: KubernetesFetcher) { + addFetcher( + fetcher: + | KubernetesFetcher + | (({ + getDefault, + }: { + getDefault: () => KubernetesFetcher; + }) => KubernetesFetcher), + ) { if (this.fetcher) { throw new Error( 'Multiple Kubernetes Fetchers is not supported at this time', ); } - this.fetcher = fetcher; + if (typeof fetcher !== 'function') { + this.fetcher = () => fetcher; + } else { + this.fetcher = fetcher; + } } } class ServiceLocator implements KubernetesServiceLocatorExtensionPoint { - private serviceLocator: KubernetesServiceLocator | undefined; + private serviceLocator: + | (({ + getDefault, + clusterSupplier, + }: { + getDefault: () => KubernetesServiceLocator; + clusterSupplier: KubernetesClustersSupplier; + }) => KubernetesServiceLocator) + | undefined; getServiceLocator() { return this.serviceLocator; @@ -103,28 +173,20 @@ class ServiceLocator implements KubernetesServiceLocatorExtensionPoint { 'Multiple Kubernetes Service Locators is not supported at this time', ); } - this.serviceLocator = serviceLocator; + + if (typeof serviceLocator !== 'function') { + this.serviceLocator = () => serviceLocator; + } else { + this.serviceLocator = serviceLocator; + } } } class AuthStrategy implements KubernetesAuthStrategyExtensionPoint { - private authStrategies: Array<{ - key: string; - strategy: AuthenticationStrategy; - }>; + private authStrategies: Map; constructor() { - this.authStrategies = new Array<{ - key: string; - strategy: AuthenticationStrategy; - }>(); - } - - static addAuthStrategiesFromArray( - authStrategies: Array<{ key: string; strategy: AuthenticationStrategy }>, - builder: KubernetesBuilder, - ) { - authStrategies.forEach(st => builder.addAuthStrategy(st.key, st.strategy)); + this.authStrategies = new Map(); } getAuthenticationStrategies() { @@ -132,7 +194,10 @@ class AuthStrategy implements KubernetesAuthStrategyExtensionPoint { } addAuthStrategy(key: string, authStrategy: AuthenticationStrategy) { - this.authStrategies.push({ key, strategy: authStrategy }); + if (key.includes('-')) { + throw new Error('Strategy name can not include dashes'); + } + this.authStrategies.set(key, authStrategy); } } @@ -176,7 +241,7 @@ export const kubernetesPlugin = createBackendPlugin({ logger: coreServices.logger, config: coreServices.rootConfig, discovery: coreServices.discovery, - catalogApi: catalogServiceRef, + catalog: catalogServiceRef, permissions: coreServices.permissions, auth: coreServices.auth, httpAuth: coreServices.httpAuth, @@ -186,31 +251,142 @@ export const kubernetesPlugin = createBackendPlugin({ logger, config, discovery, - catalogApi, + catalog, permissions, auth, httpAuth, }) { if (config.has('kubernetes')) { // TODO: expose all of the customization & extension points of the builder here + const defaultFetcherFactory = () => + 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 defaultClusterSupplierFactory = () => + getCombinedClusterSupplier( + config, + catalog, + new DispatchStrategy({ + authStrategyMap: Object.fromEntries(authStrategyMap.entries()), + }), + logger, + refreshInterval, + auth, + ); + + const clusterSupplier = + extPointClusterSuplier.getClusterSupplier()?.({ + getDefault: defaultClusterSupplierFactory, + }) ?? defaultClusterSupplierFactory(); + + const defaultServiceLocatorFactory = () => + buildDefaultServiceLocator({ + config, + clusterSupplier, + }); + + const serviceLocator = + extPointServiceLocator.getServiceLocator()?.({ + getDefault: defaultServiceLocatorFactory, + clusterSupplier: 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 = () => + new KubernetesFanOutHandler({ + logger, + config, + fetcher, + serviceLocator, + customResources, + objectTypesToFetch, + authStrategy: new DispatchStrategy({ + authStrategyMap: Object.fromEntries(authStrategyMap.entries()), + }), + }); + + const objectsProvider = + extPointObjectsProvider.getObjectsProvider()?.({ + clusterSupplier, + getDefault: defaultObjectsProviderFactory, + serviceLocator, + customResources, + objectTypesToFetch, + authStrategy: new DispatchStrategy({ + authStrategyMap: Object.fromEntries(authStrategyMap.entries()), + }), + }) ?? defaultObjectsProviderFactory(); + const builder: KubernetesBuilder = KubernetesBuilder.createBuilder({ logger, config, - catalogApi, + catalog, permissions, discovery, auth, httpAuth, - }) - .setObjectsProvider(extPointObjectsProvider.getObjectsProvider()) - .setClusterSupplier(extPointClusterSuplier.getClusterSupplier()) - .setFetcher(extPointFetcher.getFetcher()) - .setServiceLocator(extPointServiceLocator.getServiceLocator()); + authStrategyMap: Object.fromEntries(authStrategyMap.entries()), + fetcher, + clusterSupplier, + serviceLocator, + objectsProvider, + }); - AuthStrategy.addAuthStrategiesFromArray( - extPointAuthStrategy.getAuthenticationStrategies(), - builder, - ); const { router } = await builder.build(); http.use(router); } else { diff --git a/plugins/kubernetes-backend/src/routes/resourcesRoutes.ts b/plugins/kubernetes-backend/src/routes/resourcesRoutes.ts index d500d3cb8b..33b905d473 100644 --- a/plugins/kubernetes-backend/src/routes/resourcesRoutes.ts +++ b/plugins/kubernetes-backend/src/routes/resourcesRoutes.ts @@ -18,20 +18,19 @@ import { parseEntityRef, stringifyEntityRef, } from '@backstage/catalog-model'; -import { CatalogApi } from '@backstage/catalog-client'; import { InputError } from '@backstage/errors'; import express, { Request } from 'express'; import { KubernetesObjectsProvider } from '@backstage/plugin-kubernetes-node'; -import { AuthService, HttpAuthService } from '@backstage/backend-plugin-api'; +import { HttpAuthService } from '@backstage/backend-plugin-api'; import { PermissionEvaluator } from '@backstage/plugin-permission-common'; import { requirePermission } from '../auth/requirePermission'; import { kubernetesResourcesReadPermission } from '@backstage/plugin-kubernetes-common'; +import { CatalogService } from '@backstage/plugin-catalog-node'; export const addResourceRoutesToRouter = ( router: express.Router, - catalogApi: CatalogApi, + catalog: CatalogService, objectsProvider: KubernetesObjectsProvider, - auth: AuthService, httpAuth: HttpAuthService, permissionApi: PermissionEvaluator, ) => { @@ -50,12 +49,9 @@ export const addResourceRoutesToRouter = ( throw new InputError(`Invalid entity ref, ${error}`); } - const { token } = await auth.getPluginRequestToken({ - onBehalfOf: await httpAuth.credentials(req), - targetPluginId: 'catalog', + const entity = await catalog.getEntityByRef(entityRef, { + credentials: await httpAuth.credentials(req), }); - - const entity = await catalogApi.getEntityByRef(entityRef, { token }); if (!entity) { throw new InputError( `Entity ref missing, ${stringifyEntityRef(entityRef)}`, diff --git a/plugins/kubernetes-backend/src/service-locator/buildDefaultServiceLocator.ts b/plugins/kubernetes-backend/src/service-locator/buildDefaultServiceLocator.ts new file mode 100644 index 0000000000..59d67eb877 --- /dev/null +++ b/plugins/kubernetes-backend/src/service-locator/buildDefaultServiceLocator.ts @@ -0,0 +1,48 @@ +/* + * 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 { RootConfigService } from '@backstage/backend-plugin-api'; +import { ServiceLocatorMethod } from '../types'; +import { KubernetesClustersSupplier } from '@backstage/plugin-kubernetes-node'; +import { MultiTenantServiceLocator } from './MultiTenantServiceLocator'; +import { SingleTenantServiceLocator } from './SingleTenantServiceLocator'; +import { CatalogRelationServiceLocator } from './CatalogRelationServiceLocator'; + +export const buildDefaultServiceLocator = ({ + config, + clusterSupplier, +}: { + config: RootConfigService; + clusterSupplier: KubernetesClustersSupplier; +}) => { + const method = config.getString( + 'kubernetes.serviceLocatorMethod.type', + ) as ServiceLocatorMethod; + + switch (method) { + case 'multiTenant': + return new MultiTenantServiceLocator(clusterSupplier); + case 'singleTenant': + return new SingleTenantServiceLocator(clusterSupplier); + case 'catalogRelation': + return new CatalogRelationServiceLocator(clusterSupplier); + case 'http': + throw new Error('not implemented'); + default: + throw new Error( + `Unsupported kubernetes.serviceLocatorMethod "${method}"`, + ); + } +}; diff --git a/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts b/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts index 2cf0bcf04c..93400d2f73 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts @@ -13,7 +13,6 @@ * 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 { ANNOTATION_KUBERNETES_AUTH_PROVIDER, @@ -26,20 +25,8 @@ import { PermissionEvaluator } from '@backstage/plugin-permission-common'; import { createPermissionIntegrationRouter } from '@backstage/plugin-permission-node'; import express from 'express'; import Router from 'express-promise-router'; -import { Duration } from 'luxon'; -import { - AksStrategy, - AnonymousStrategy, - AwsIamStrategy, - AzureIdentityStrategy, - DispatchStrategy, - GoogleServiceAccountStrategy, - GoogleStrategy, - OidcStrategy, - ServiceAccountStrategy, -} from '../auth'; -import { getCombinedClusterSupplier } from '../cluster-locator'; +import { DispatchStrategy } from '../auth'; import { AuthService, @@ -51,72 +38,37 @@ import { import { AuthenticationStrategy, AuthMetadata, - CustomResource, KubernetesClustersSupplier, KubernetesFetcher, KubernetesObjectsProvider, - KubernetesObjectTypes, KubernetesServiceLocator, } from '@backstage/plugin-kubernetes-node'; import { addResourceRoutesToRouter } from '../routes/resourcesRoutes'; -import { CatalogRelationServiceLocator } from '../service-locator/CatalogRelationServiceLocator'; -import { MultiTenantServiceLocator } from '../service-locator/MultiTenantServiceLocator'; -import { SingleTenantServiceLocator } from '../service-locator/SingleTenantServiceLocator'; -import { - KubernetesObjectsProviderOptions, - ObjectsByEntityRequest, - ServiceLocatorMethod, -} from '../types/types'; -import { - ALL_OBJECTS, - DEFAULT_OBJECTS, - KubernetesFanOutHandler, -} from './KubernetesFanOutHandler'; -import { KubernetesClientBasedFetcher } from './KubernetesFetcher'; +import { ObjectsByEntityRequest } from '../types/types'; import { KubernetesProxy } from './KubernetesProxy'; import { requirePermission } from '../auth/requirePermission'; +import { CatalogService } from '@backstage/plugin-catalog-node'; -/** - * @deprecated Please migrate to the new backend system as this will be removed in the future. - * @public - */ export interface KubernetesEnvironment { logger: LoggerService; config: Config; - catalogApi: CatalogApi; + catalog: CatalogService; discovery: DiscoveryService; permissions: PermissionEvaluator; auth: AuthService; httpAuth: HttpAuthService; + authStrategyMap: { [key: string]: AuthenticationStrategy }; + fetcher: KubernetesFetcher; + clusterSupplier: KubernetesClustersSupplier; + serviceLocator: KubernetesServiceLocator; + objectsProvider: KubernetesObjectsProvider; } -/** - * The return type of the `KubernetesBuilder.build` method - * @deprecated Please migrate to the new backend system as this will be removed in the future. - * @public - */ export type KubernetesBuilderReturn = Promise<{ router: express.Router; - clusterSupplier: KubernetesClustersSupplier; - customResources: CustomResource[]; - fetcher: KubernetesFetcher; - proxy: KubernetesProxy; - objectsProvider: KubernetesObjectsProvider; - serviceLocator: KubernetesServiceLocator; - authStrategyMap: { [key: string]: AuthenticationStrategy }; }>; export class KubernetesBuilder { - private clusterSupplier?: KubernetesClustersSupplier; - private defaultClusterRefreshInterval: Duration = Duration.fromObject({ - minutes: 60, - }); - private objectsProvider?: KubernetesObjectsProvider; - private fetcher?: KubernetesFetcher; - private serviceLocator?: KubernetesServiceLocator; - private proxy?: KubernetesProxy; - private authStrategyMap?: { [key: string]: AuthenticationStrategy }; - static createBuilder(env: KubernetesEnvironment) { return new KubernetesBuilder(env); } @@ -124,9 +76,17 @@ export class KubernetesBuilder { constructor(protected readonly env: KubernetesEnvironment) {} public async build(): KubernetesBuilderReturn { - const logger = this.env.logger; - const config = this.env.config; - const permissions = this.env.permissions; + const { + logger, + config, + permissions, + authStrategyMap, + clusterSupplier, + objectsProvider, + catalog, + discovery, + httpAuth, + } = this.env; logger.info('Initializing Kubernetes backend'); @@ -142,236 +102,56 @@ export class KubernetesBuilder { } as unknown as KubernetesBuilderReturn; } - const customResources = this.buildCustomResources(); - - const fetcher = this.getFetcher(); - - const clusterSupplier = this.getClusterSupplier(); - - const authStrategyMap = this.getAuthStrategyMap(); - - const proxy = this.getProxy( + const proxy = this.buildProxy( logger, clusterSupplier, - this.env.discovery, - this.env.httpAuth, + discovery, + httpAuth, + authStrategyMap, ); - const serviceLocator = this.getServiceLocator(); - - const objectsProvider = this.getObjectsProvider({ - logger, - fetcher, - config, - serviceLocator, - customResources, - objectTypesToFetch: this.getObjectTypesToFetch(), - }); - const router = this.buildRouter( objectsProvider, clusterSupplier, - this.env.catalogApi, + catalog, proxy, permissions, - this.env.auth, - this.env.httpAuth, + httpAuth, + authStrategyMap, ); return { - clusterSupplier, - customResources, - fetcher, - proxy, - objectsProvider, router, - serviceLocator, - authStrategyMap, }; } - public setClusterSupplier(clusterSupplier?: KubernetesClustersSupplier) { - this.clusterSupplier = clusterSupplier; - return this; - } - - public setDefaultClusterRefreshInterval(refreshInterval: Duration) { - this.defaultClusterRefreshInterval = refreshInterval; - return this; - } - - public setObjectsProvider(objectsProvider?: KubernetesObjectsProvider) { - this.objectsProvider = objectsProvider; - return this; - } - - public setFetcher(fetcher?: KubernetesFetcher) { - this.fetcher = fetcher; - return this; - } - - public setServiceLocator(serviceLocator?: KubernetesServiceLocator) { - this.serviceLocator = serviceLocator; - return this; - } - - public setProxy(proxy?: KubernetesProxy) { - this.proxy = proxy; - return this; - } - - public setAuthStrategyMap(authStrategyMap: { - [key: string]: AuthenticationStrategy; - }) { - this.authStrategyMap = authStrategyMap; - } - - public addAuthStrategy(key: string, strategy: AuthenticationStrategy) { - if (key.includes('-')) { - throw new Error('Strategy name can not include dashes'); - } - this.getAuthStrategyMap()[key] = strategy; - return this; - } - - protected buildCustomResources() { - const customResources: CustomResource[] = ( - this.env.config.getOptionalConfigArray('kubernetes.customResources') ?? [] - ).map( - c => - ({ - group: c.getString('group'), - apiVersion: c.getString('apiVersion'), - plural: c.getString('plural'), - objectType: 'customresources', - } as CustomResource), - ); - - this.env.logger.info( - `action=LoadingCustomResources numOfCustomResources=${customResources.length}`, - ); - return customResources; - } - - protected buildClusterSupplier( - refreshInterval: Duration, - ): KubernetesClustersSupplier { - const config = this.env.config; - this.clusterSupplier = getCombinedClusterSupplier( - config, - this.env.catalogApi, - new DispatchStrategy({ authStrategyMap: this.getAuthStrategyMap() }), - this.env.logger, - refreshInterval, - this.env.auth, - ); - - return this.clusterSupplier; - } - - protected buildObjectsProvider( - options: KubernetesObjectsProviderOptions, - ): KubernetesObjectsProvider { - const authStrategyMap = this.getAuthStrategyMap(); - this.objectsProvider = new KubernetesFanOutHandler({ - ...options, - authStrategy: new DispatchStrategy({ - authStrategyMap, - }), - }); - - return this.objectsProvider; - } - - protected buildFetcher(): KubernetesFetcher { - this.fetcher = new KubernetesClientBasedFetcher({ - logger: this.env.logger, - }); - - return this.fetcher; - } - - protected buildServiceLocator( - method: ServiceLocatorMethod, - clusterSupplier: KubernetesClustersSupplier, - ): KubernetesServiceLocator { - switch (method) { - case 'multiTenant': - this.serviceLocator = - this.buildMultiTenantServiceLocator(clusterSupplier); - break; - case 'singleTenant': - this.serviceLocator = - this.buildSingleTenantServiceLocator(clusterSupplier); - break; - case 'catalogRelation': - this.serviceLocator = - this.buildCatalogRelationServiceLocator(clusterSupplier); - break; - case 'http': - this.serviceLocator = this.buildHttpServiceLocator(clusterSupplier); - break; - default: - throw new Error( - `Unsupported kubernetes.serviceLocatorMethod "${method}"`, - ); - } - - return this.serviceLocator; - } - - protected buildMultiTenantServiceLocator( - clusterSupplier: KubernetesClustersSupplier, - ): KubernetesServiceLocator { - return new MultiTenantServiceLocator(clusterSupplier); - } - - protected buildSingleTenantServiceLocator( - clusterSupplier: KubernetesClustersSupplier, - ): KubernetesServiceLocator { - return new SingleTenantServiceLocator(clusterSupplier); - } - - protected buildCatalogRelationServiceLocator( - clusterSupplier: KubernetesClustersSupplier, - ): KubernetesServiceLocator { - return new CatalogRelationServiceLocator(clusterSupplier); - } - - protected buildHttpServiceLocator( - _clusterSupplier: KubernetesClustersSupplier, - ): KubernetesServiceLocator { - throw new Error('not implemented'); - } - - protected buildProxy( + private buildProxy( logger: LoggerService, clusterSupplier: KubernetesClustersSupplier, discovery: DiscoveryService, httpAuth: HttpAuthService, + authStrategyMap: { [key: string]: AuthenticationStrategy }, ): KubernetesProxy { - const authStrategyMap = this.getAuthStrategyMap(); const authStrategy = new DispatchStrategy({ authStrategyMap, }); - this.proxy = new KubernetesProxy({ + return new KubernetesProxy({ logger, clusterSupplier, authStrategy, discovery, httpAuth, }); - return this.proxy; } - protected buildRouter( + private buildRouter( objectsProvider: KubernetesObjectsProvider, clusterSupplier: KubernetesClustersSupplier, - catalogApi: CatalogApi, + catalog: CatalogService, proxy: KubernetesProxy, permissionApi: PermissionEvaluator, - authService: AuthService, httpAuth: HttpAuthService, + authStrategyMap: { [key: string]: AuthenticationStrategy }, ): express.Router { const logger = this.env.logger; const router = Router(); @@ -382,6 +162,7 @@ export class KubernetesBuilder { permissions: kubernetesPermissions, }), ); + // @deprecated router.post('/services/:serviceId', async (req, res) => { await requirePermission( @@ -426,7 +207,7 @@ export class KubernetesBuilder { cd.authMetadata[ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER]; const authProvider = cd.authMetadata[ANNOTATION_KUBERNETES_AUTH_PROVIDER]; - const strategy = this.getAuthStrategyMap()[authProvider]; + const strategy = authStrategyMap[authProvider]; let auth: AuthMetadata = {}; if (strategy) { auth = strategy.presentAuthMetadata(cd.authMetadata); @@ -446,9 +227,8 @@ export class KubernetesBuilder { addResourceRoutesToRouter( router, - catalogApi, + catalog, objectsProvider, - authService, httpAuth, permissionApi, ); @@ -456,23 +236,7 @@ export class KubernetesBuilder { return router; } - protected buildAuthStrategyMap() { - this.authStrategyMap = { - aks: new AksStrategy(), - aws: new AwsIamStrategy({ config: this.env.config }), - azure: new AzureIdentityStrategy(this.env.logger), - google: new GoogleStrategy(), - googleServiceAccount: new GoogleServiceAccountStrategy({ - config: this.env.config, - }), - localKubectlProxy: new AnonymousStrategy(), - oidc: new OidcStrategy(), - serviceAccount: new ServiceAccountStrategy(), - }; - return this.authStrategyMap; - } - - protected async fetchClusterDetails( + private async fetchClusterDetails( clusterSupplier: KubernetesClustersSupplier, options: { credentials: BackstageCredentials }, ) { @@ -484,81 +248,4 @@ export class KubernetesBuilder { return clusterDetails; } - - protected getServiceLocatorMethod() { - return this.env.config.getString( - 'kubernetes.serviceLocatorMethod.type', - ) 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', - ) as KubernetesObjectTypes[]; - - const apiVersionOverrides = this.env.config.getOptionalConfig( - 'kubernetes.apiVersionOverrides', - ); - - let objectTypesToFetch; - - if (objectTypesToFetchStrings) { - objectTypesToFetch = ALL_OBJECTS.filter(obj => - objectTypesToFetchStrings.includes(obj.objectType), - ); - } - - if (apiVersionOverrides) { - objectTypesToFetch = objectTypesToFetch ?? DEFAULT_OBJECTS; - - for (const obj of objectTypesToFetch) { - if (apiVersionOverrides.has(obj.objectType)) { - obj.apiVersion = apiVersionOverrides.getString(obj.objectType); - } - } - } - - return objectTypesToFetch; - } - - protected getProxy( - logger: LoggerService, - clusterSupplier: KubernetesClustersSupplier, - discovery: DiscoveryService, - httpAuth: HttpAuthService, - ) { - return ( - this.proxy ?? - this.buildProxy(logger, clusterSupplier, discovery, httpAuth) - ); - } - - protected getAuthStrategyMap() { - return this.authStrategyMap ?? this.buildAuthStrategyMap(); - } } diff --git a/plugins/kubernetes-backend/src/service/router.ts b/plugins/kubernetes-backend/src/service/router.ts index 66fc131262..8a095d00f0 100644 --- a/plugins/kubernetes-backend/src/service/router.ts +++ b/plugins/kubernetes-backend/src/service/router.ts @@ -15,10 +15,15 @@ */ import { Logger } from 'winston'; -import { KubernetesClustersSupplier } from '@backstage/plugin-kubernetes-node'; +import { + AuthenticationStrategy, + KubernetesClustersSupplier, + KubernetesFetcher, + KubernetesObjectsProvider, + KubernetesServiceLocator, +} from '@backstage/plugin-kubernetes-node'; import express from 'express'; import { KubernetesBuilder } from './KubernetesBuilder'; -import { CatalogApi } from '@backstage/catalog-client'; import { PermissionEvaluator } from '@backstage/plugin-permission-common'; import { DiscoveryService, @@ -26,23 +31,26 @@ import { RootConfigService, AuthService, } from '@backstage/backend-plugin-api'; +import { CatalogService } from '@backstage/plugin-catalog-node'; interface RouterOptions { logger: Logger; config: RootConfigService; - catalogApi: CatalogApi; - clusterSupplier?: KubernetesClustersSupplier; + 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) - .setClusterSupplier(options.clusterSupplier) - .build(); + const { router } = await KubernetesBuilder.createBuilder(options).build(); return router; } From 3b8a186bf8052c4ebfb08dd98552fd831e75c4f4 Mon Sep 17 00:00:00 2001 From: benjdlambert Date: Wed, 13 Aug 2025 07:48:40 +0200 Subject: [PATCH 2/8] chore: updating extension type Signed-off-by: benjdlambert --- plugins/kubernetes-node/src/extensions.ts | 54 +++++++++++++++++++++-- 1 file changed, 50 insertions(+), 4 deletions(-) diff --git a/plugins/kubernetes-node/src/extensions.ts b/plugins/kubernetes-node/src/extensions.ts index d898244161..86f5d0970b 100644 --- a/plugins/kubernetes-node/src/extensions.ts +++ b/plugins/kubernetes-node/src/extensions.ts @@ -16,6 +16,8 @@ import { createExtensionPoint } from '@backstage/backend-plugin-api'; import { AuthenticationStrategy, + CustomResource, + ObjectToFetch, KubernetesClustersSupplier, KubernetesFetcher, KubernetesObjectsProvider, @@ -28,7 +30,25 @@ import { * @public */ export interface KubernetesObjectsProviderExtensionPoint { - addObjectsProvider(provider: KubernetesObjectsProvider): void; + addObjectsProvider( + provider: + | KubernetesObjectsProvider + | (({ + getDefault, + clusterSupplier, + serviceLocator, + customResources, + objectTypesToFetch, + authStrategy, + }: { + getDefault: () => KubernetesObjectsProvider; + clusterSupplier: KubernetesClustersSupplier; + serviceLocator: KubernetesServiceLocator; + customResources: CustomResource[]; + objectTypesToFetch?: ObjectToFetch[]; + authStrategy: AuthenticationStrategy; + }) => KubernetesObjectsProvider), + ): void; } /** @@ -47,7 +67,15 @@ export const kubernetesObjectsProviderExtensionPoint = * @public */ export interface KubernetesClusterSupplierExtensionPoint { - addClusterSupplier(clusterSupplier: KubernetesClustersSupplier): void; + addClusterSupplier( + clusterSupplier: + | KubernetesClustersSupplier + | (({ + getDefault, + }: { + getDefault: () => KubernetesClustersSupplier; + }) => KubernetesClustersSupplier), + ): void; } /** @@ -85,7 +113,15 @@ export const kubernetesAuthStrategyExtensionPoint = * @public */ export interface KubernetesFetcherExtensionPoint { - addFetcher(fetcher: KubernetesFetcher): void; + addFetcher( + fetcher: + | KubernetesFetcher + | (({ + getDefault, + }: { + getDefault: () => KubernetesFetcher; + }) => KubernetesFetcher), + ): void; } /** @@ -104,7 +140,17 @@ export const kubernetesFetcherExtensionPoint = * @public */ export interface KubernetesServiceLocatorExtensionPoint { - addServiceLocator(serviceLocator: KubernetesServiceLocator): void; + addServiceLocator( + serviceLocator: + | KubernetesServiceLocator + | (({ + getDefault, + clusterSupplier, + }: { + getDefault: () => KubernetesServiceLocator; + clusterSupplier: KubernetesClustersSupplier; + }) => KubernetesServiceLocator), + ): void; } /** From 3bdb44a3a571c0bae1ba9e2c24ebc4dfbced731c Mon Sep 17 00:00:00 2001 From: benjdlambert Date: Wed, 13 Aug 2025 07:50:22 +0200 Subject: [PATCH 3/8] chore: fixing api reports Signed-off-by: benjdlambert Signed-off-by: benjdlambert --- plugins/kubernetes-node/report.api.md | 54 +++++++++++++++++++++++++-- 1 file changed, 50 insertions(+), 4 deletions(-) diff --git a/plugins/kubernetes-node/report.api.md b/plugins/kubernetes-node/report.api.md index 5758db9ea1..74b462dea4 100644 --- a/plugins/kubernetes-node/report.api.md +++ b/plugins/kubernetes-node/report.api.md @@ -5,6 +5,7 @@ ```ts import { AuthenticationStrategy as AuthenticationStrategy_2 } from '@backstage/plugin-kubernetes-node'; 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 { ExtensionPoint } from '@backstage/backend-plugin-api'; @@ -18,6 +19,7 @@ import { KubernetesRequestAuth } from '@backstage/plugin-kubernetes-common'; import { KubernetesServiceLocator as KubernetesServiceLocator_2 } from '@backstage/plugin-kubernetes-node'; import { LoggerService } from '@backstage/backend-plugin-api'; import { ObjectsByEntityResponse } from '@backstage/plugin-kubernetes-common'; +import { ObjectToFetch as ObjectToFetch_2 } from '@backstage/plugin-kubernetes-node'; // @public (undocumented) export interface AuthenticationStrategy { @@ -95,7 +97,15 @@ export interface KubernetesClustersSupplier { // @public export interface KubernetesClusterSupplierExtensionPoint { // (undocumented) - addClusterSupplier(clusterSupplier: KubernetesClustersSupplier_2): void; + addClusterSupplier( + clusterSupplier: + | KubernetesClustersSupplier_2 + | (({ + getDefault, + }: { + getDefault: () => KubernetesClustersSupplier_2; + }) => KubernetesClustersSupplier_2), + ): void; } // @public @@ -134,7 +144,15 @@ export interface KubernetesFetcher { // @public export interface KubernetesFetcherExtensionPoint { // (undocumented) - addFetcher(fetcher: KubernetesFetcher_2): void; + addFetcher( + fetcher: + | KubernetesFetcher_2 + | (({ + getDefault, + }: { + getDefault: () => KubernetesFetcher_2; + }) => KubernetesFetcher_2), + ): void; } // @public @@ -169,7 +187,25 @@ export interface KubernetesObjectsProvider { // @public export interface KubernetesObjectsProviderExtensionPoint { // (undocumented) - addObjectsProvider(provider: KubernetesObjectsProvider_2): void; + addObjectsProvider( + provider: + | KubernetesObjectsProvider_2 + | (({ + getDefault, + clusterSupplier, + serviceLocator, + customResources, + objectTypesToFetch, + authStrategy, + }: { + getDefault: () => KubernetesObjectsProvider_2; + clusterSupplier: KubernetesClustersSupplier_2; + serviceLocator: KubernetesServiceLocator_2; + customResources: CustomResource_2[]; + objectTypesToFetch?: ObjectToFetch_2[]; + authStrategy: AuthenticationStrategy_2; + }) => KubernetesObjectsProvider_2), + ): void; } // @public @@ -207,7 +243,17 @@ export interface KubernetesServiceLocator { // @public export interface KubernetesServiceLocatorExtensionPoint { // (undocumented) - addServiceLocator(serviceLocator: KubernetesServiceLocator_2): void; + addServiceLocator( + serviceLocator: + | KubernetesServiceLocator_2 + | (({ + getDefault, + clusterSupplier, + }: { + getDefault: () => KubernetesServiceLocator_2; + clusterSupplier: KubernetesClustersSupplier_2; + }) => KubernetesServiceLocator_2), + ): void; } // @public From 79e342ec3689f517074d49614489f4d39afd1bbc Mon Sep 17 00:00:00 2001 From: benjdlambert Date: Mon, 18 Aug 2025 11:01:50 +0200 Subject: [PATCH 4/8] chore: added changeset Signed-off-by: benjdlambert --- .changeset/small-lies-open.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/small-lies-open.md diff --git a/.changeset/small-lies-open.md b/.changeset/small-lies-open.md new file mode 100644 index 0000000000..579623f651 --- /dev/null +++ b/.changeset/small-lies-open.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-kubernetes-backend': patch +'@backstage/plugin-kubernetes-node': patch +--- + +Added support for providing a factory to the extension points From 908420bf3be34242353ad479a2d0f072eef3eac6 Mon Sep 17 00:00:00 2001 From: benjdlambert Date: Mon, 18 Aug 2025 15:30:53 +0200 Subject: [PATCH 5/8] chore: fix up code review comments Signed-off-by: benjdlambert --- .../src/auth/buildDefaultAuthStrategyMap.ts | 2 +- plugins/kubernetes-backend/src/plugin.ts | 110 +++++++----------- plugins/kubernetes-node/report.api.md | 68 +++++------ plugins/kubernetes-node/src/extensions.ts | 86 +++++++------- 4 files changed, 116 insertions(+), 150 deletions(-) diff --git a/plugins/kubernetes-backend/src/auth/buildDefaultAuthStrategyMap.ts b/plugins/kubernetes-backend/src/auth/buildDefaultAuthStrategyMap.ts index 918cb759c3..4bd5cf97f3 100644 --- a/plugins/kubernetes-backend/src/auth/buildDefaultAuthStrategyMap.ts +++ b/plugins/kubernetes-backend/src/auth/buildDefaultAuthStrategyMap.ts @@ -39,7 +39,7 @@ export const buildDefaultAuthStrategyMap = ({ ['aws', new AwsIamStrategy({ config })], ['azure', new AzureIdentityStrategy(logger)], ['google', new GoogleStrategy()], - ['googleServiceAccount', new GoogleServiceAccountStrategy()], + ['googleServiceAccount', new GoogleServiceAccountStrategy({ config })], ['localKubectlProxy', new AnonymousStrategy()], ['oidc', new OidcStrategy()], ['serviceAccount', new ServiceAccountStrategy()], diff --git a/plugins/kubernetes-backend/src/plugin.ts b/plugins/kubernetes-backend/src/plugin.ts index f787f52f4a..7b2d157911 100644 --- a/plugins/kubernetes-backend/src/plugin.ts +++ b/plugins/kubernetes-backend/src/plugin.ts @@ -28,16 +28,20 @@ import { type KubernetesClustersSupplier, kubernetesClusterSupplierExtensionPoint, type KubernetesClusterSupplierExtensionPoint, + KubernetesClusterSupplierFactory, type KubernetesFetcher, kubernetesFetcherExtensionPoint, type KubernetesFetcherExtensionPoint, + KubernetesFetcherFactory, type KubernetesObjectsProvider, kubernetesObjectsProviderExtensionPoint, type KubernetesObjectsProviderExtensionPoint, + KubernetesObjectsProviderFactory, KubernetesObjectTypes, type KubernetesServiceLocator, kubernetesServiceLocatorExtensionPoint, type KubernetesServiceLocatorExtensionPoint, + KubernetesServiceLocatorFactory, ObjectToFetch, } from '@backstage/plugin-kubernetes-node'; import { KubernetesBuilder } from './service/KubernetesBuilder'; @@ -54,36 +58,22 @@ import { } from './service/KubernetesFanOutHandler'; class ObjectsProvider implements KubernetesObjectsProviderExtensionPoint { - private objectsProvider: - | (({ - getDefault, - clusterSupplier, - serviceLocator, - customResources, - objectTypesToFetch, - authStrategy, - }: { - getDefault: () => KubernetesObjectsProvider; - clusterSupplier: KubernetesClustersSupplier; - serviceLocator: KubernetesServiceLocator; - customResources: CustomResource[]; - objectTypesToFetch?: ObjectToFetch[]; - authStrategy: AuthenticationStrategy; - }) => KubernetesObjectsProvider) - | undefined; + private objectsProvider: KubernetesObjectsProviderFactory | undefined; getObjectsProvider() { return this.objectsProvider; } - addObjectsProvider(provider: KubernetesObjectsProvider) { + addObjectsProvider( + provider: KubernetesObjectsProvider | KubernetesObjectsProviderFactory, + ) { if (this.objectsProvider) { throw new Error( 'Multiple Kubernetes objects provider is not supported at this time', ); } if (typeof provider !== 'function') { - this.objectsProvider = () => provider; + this.objectsProvider = async () => provider; } else { this.objectsProvider = provider; } @@ -91,26 +81,24 @@ class ObjectsProvider implements KubernetesObjectsProviderExtensionPoint { } class ClusterSuplier implements KubernetesClusterSupplierExtensionPoint { - private clusterSupplier: - | (({ - getDefault, - }: { - getDefault: () => KubernetesClustersSupplier; - }) => KubernetesClustersSupplier) - | undefined; + private clusterSupplier: KubernetesClusterSupplierFactory | undefined; getClusterSupplier() { return this.clusterSupplier; } - addClusterSupplier(clusterSupplier: KubernetesClustersSupplier) { + addClusterSupplier( + clusterSupplier: + | KubernetesClustersSupplier + | KubernetesClusterSupplierFactory, + ) { if (this.clusterSupplier) { throw new Error( 'Multiple Kubernetes Cluster Suppliers is not supported at this time', ); } if (typeof clusterSupplier !== 'function') { - this.clusterSupplier = () => clusterSupplier; + this.clusterSupplier = async () => clusterSupplier; } else { this.clusterSupplier = clusterSupplier; } @@ -118,34 +106,20 @@ class ClusterSuplier implements KubernetesClusterSupplierExtensionPoint { } class Fetcher implements KubernetesFetcherExtensionPoint { - private fetcher: - | (({ - getDefault, - }: { - getDefault: () => KubernetesFetcher; - }) => KubernetesFetcher) - | undefined; + private fetcher: KubernetesFetcherFactory | undefined; getFetcher() { return this.fetcher; } - addFetcher( - fetcher: - | KubernetesFetcher - | (({ - getDefault, - }: { - getDefault: () => KubernetesFetcher; - }) => KubernetesFetcher), - ) { + addFetcher(fetcher: KubernetesFetcher | KubernetesFetcherFactory) { if (this.fetcher) { throw new Error( 'Multiple Kubernetes Fetchers is not supported at this time', ); } if (typeof fetcher !== 'function') { - this.fetcher = () => fetcher; + this.fetcher = async () => fetcher; } else { this.fetcher = fetcher; } @@ -153,21 +127,15 @@ class Fetcher implements KubernetesFetcherExtensionPoint { } class ServiceLocator implements KubernetesServiceLocatorExtensionPoint { - private serviceLocator: - | (({ - getDefault, - clusterSupplier, - }: { - getDefault: () => KubernetesServiceLocator; - clusterSupplier: KubernetesClustersSupplier; - }) => KubernetesServiceLocator) - | undefined; + private serviceLocator: KubernetesServiceLocatorFactory | undefined; getServiceLocator() { return this.serviceLocator; } - addServiceLocator(serviceLocator: KubernetesServiceLocator) { + addServiceLocator( + serviceLocator: KubernetesServiceLocator | KubernetesServiceLocatorFactory, + ) { if (this.serviceLocator) { throw new Error( 'Multiple Kubernetes Service Locators is not supported at this time', @@ -175,7 +143,7 @@ class ServiceLocator implements KubernetesServiceLocatorExtensionPoint { } if (typeof serviceLocator !== 'function') { - this.serviceLocator = () => serviceLocator; + this.serviceLocator = async () => serviceLocator; } else { this.serviceLocator = serviceLocator; } @@ -256,9 +224,9 @@ export const kubernetesPlugin = createBackendPlugin({ auth, httpAuth, }) { + // TODO: this could do with a cleanup and push some of this initalization somewhere else if (config.has('kubernetes')) { - // TODO: expose all of the customization & extension points of the builder here - const defaultFetcherFactory = () => + const defaultFetcherFactory = async () => new KubernetesClientBasedFetcher({ logger, }); @@ -280,7 +248,7 @@ export const kubernetesPlugin = createBackendPlugin({ minutes: 60, }); - const defaultClusterSupplierFactory = () => + const defaultClusterSupplierFactory = async () => getCombinedClusterSupplier( config, catalog, @@ -297,16 +265,16 @@ export const kubernetesPlugin = createBackendPlugin({ getDefault: defaultClusterSupplierFactory, }) ?? defaultClusterSupplierFactory(); - const defaultServiceLocatorFactory = () => + const defaultServiceLocatorFactory = async () => buildDefaultServiceLocator({ config, - clusterSupplier, + clusterSupplier: await clusterSupplier, }); const serviceLocator = extPointServiceLocator.getServiceLocator()?.({ getDefault: defaultServiceLocatorFactory, - clusterSupplier: clusterSupplier, + clusterSupplier: await clusterSupplier, }) ?? defaultServiceLocatorFactory(); const objectTypesToFetchStrings = config.getOptionalStringArray( @@ -347,12 +315,12 @@ export const kubernetesPlugin = createBackendPlugin({ } as CustomResource), ); - const defaultObjectsProviderFactory = () => + const defaultObjectsProviderFactory = async () => new KubernetesFanOutHandler({ logger, config, - fetcher, - serviceLocator, + fetcher: await fetcher, + serviceLocator: await serviceLocator, customResources, objectTypesToFetch, authStrategy: new DispatchStrategy({ @@ -362,9 +330,9 @@ export const kubernetesPlugin = createBackendPlugin({ const objectsProvider = extPointObjectsProvider.getObjectsProvider()?.({ - clusterSupplier, + clusterSupplier: await clusterSupplier, getDefault: defaultObjectsProviderFactory, - serviceLocator, + serviceLocator: await serviceLocator, customResources, objectTypesToFetch, authStrategy: new DispatchStrategy({ @@ -381,10 +349,10 @@ export const kubernetesPlugin = createBackendPlugin({ auth, httpAuth, authStrategyMap: Object.fromEntries(authStrategyMap.entries()), - fetcher, - clusterSupplier, - serviceLocator, - objectsProvider, + fetcher: await fetcher, + clusterSupplier: await clusterSupplier, + serviceLocator: await serviceLocator, + objectsProvider: await objectsProvider, }); const { router } = await builder.build(); diff --git a/plugins/kubernetes-node/report.api.md b/plugins/kubernetes-node/report.api.md index 74b462dea4..2e96057be8 100644 --- a/plugins/kubernetes-node/report.api.md +++ b/plugins/kubernetes-node/report.api.md @@ -100,17 +100,18 @@ export interface KubernetesClusterSupplierExtensionPoint { addClusterSupplier( clusterSupplier: | KubernetesClustersSupplier_2 - | (({ - getDefault, - }: { - getDefault: () => KubernetesClustersSupplier_2; - }) => KubernetesClustersSupplier_2), + | KubernetesClusterSupplierFactory, ): void; } // @public export const kubernetesClusterSupplierExtensionPoint: ExtensionPoint; +// @public +export type KubernetesClusterSupplierFactory = (opts: { + getDefault: () => Promise; +}) => Promise; + // @public export type KubernetesCredential = | { @@ -144,20 +145,17 @@ export interface KubernetesFetcher { // @public export interface KubernetesFetcherExtensionPoint { // (undocumented) - addFetcher( - fetcher: - | KubernetesFetcher_2 - | (({ - getDefault, - }: { - getDefault: () => KubernetesFetcher_2; - }) => KubernetesFetcher_2), - ): void; + addFetcher(fetcher: KubernetesFetcher_2 | KubernetesFetcherFactory): void; } // @public export const kubernetesFetcherExtensionPoint: ExtensionPoint; +// @public +export type KubernetesFetcherFactory = (opts: { + getDefault: () => Promise; +}) => Promise; + // @public (undocumented) export interface KubernetesObjectsByEntity { // (undocumented) @@ -188,29 +186,23 @@ export interface KubernetesObjectsProvider { export interface KubernetesObjectsProviderExtensionPoint { // (undocumented) addObjectsProvider( - provider: - | KubernetesObjectsProvider_2 - | (({ - getDefault, - clusterSupplier, - serviceLocator, - customResources, - objectTypesToFetch, - authStrategy, - }: { - getDefault: () => KubernetesObjectsProvider_2; - clusterSupplier: KubernetesClustersSupplier_2; - serviceLocator: KubernetesServiceLocator_2; - customResources: CustomResource_2[]; - objectTypesToFetch?: ObjectToFetch_2[]; - authStrategy: AuthenticationStrategy_2; - }) => KubernetesObjectsProvider_2), + provider: KubernetesObjectsProvider_2 | KubernetesObjectsProviderFactory, ): void; } // @public export const kubernetesObjectsProviderExtensionPoint: ExtensionPoint; +// @public +export type KubernetesObjectsProviderFactory = (opts: { + getDefault: () => Promise; + clusterSupplier: KubernetesClustersSupplier_2; + serviceLocator: KubernetesServiceLocator_2; + customResources: CustomResource_2[]; + objectTypesToFetch?: ObjectToFetch_2[]; + authStrategy: AuthenticationStrategy_2; +}) => Promise; + // @public (undocumented) export type KubernetesObjectTypes = | 'pods' @@ -246,19 +238,19 @@ export interface KubernetesServiceLocatorExtensionPoint { addServiceLocator( serviceLocator: | KubernetesServiceLocator_2 - | (({ - getDefault, - clusterSupplier, - }: { - getDefault: () => KubernetesServiceLocator_2; - clusterSupplier: KubernetesClustersSupplier_2; - }) => KubernetesServiceLocator_2), + | KubernetesServiceLocatorFactory, ): void; } // @public export const kubernetesServiceLocatorExtensionPoint: ExtensionPoint; +// @public +export type KubernetesServiceLocatorFactory = (opts: { + getDefault: () => Promise; + clusterSupplier: KubernetesClustersSupplier_2; +}) => Promise; + // @public (undocumented) export interface ObjectFetchParams { // (undocumented) diff --git a/plugins/kubernetes-node/src/extensions.ts b/plugins/kubernetes-node/src/extensions.ts index 86f5d0970b..6f112ece23 100644 --- a/plugins/kubernetes-node/src/extensions.ts +++ b/plugins/kubernetes-node/src/extensions.ts @@ -24,6 +24,20 @@ import { KubernetesServiceLocator, } from '@backstage/plugin-kubernetes-node'; +/** + * A factory function for creating a KubernetesObjectsProvider. + * + * @public + */ +export type KubernetesObjectsProviderFactory = (opts: { + getDefault: () => Promise; + clusterSupplier: KubernetesClustersSupplier; + serviceLocator: KubernetesServiceLocator; + customResources: CustomResource[]; + objectTypesToFetch?: ObjectToFetch[]; + authStrategy: AuthenticationStrategy; +}) => Promise; + /** * The interface for {@link kubernetesObjectsProviderExtensionPoint}. * @@ -31,23 +45,7 @@ import { */ export interface KubernetesObjectsProviderExtensionPoint { addObjectsProvider( - provider: - | KubernetesObjectsProvider - | (({ - getDefault, - clusterSupplier, - serviceLocator, - customResources, - objectTypesToFetch, - authStrategy, - }: { - getDefault: () => KubernetesObjectsProvider; - clusterSupplier: KubernetesClustersSupplier; - serviceLocator: KubernetesServiceLocator; - customResources: CustomResource[]; - objectTypesToFetch?: ObjectToFetch[]; - authStrategy: AuthenticationStrategy; - }) => KubernetesObjectsProvider), + provider: KubernetesObjectsProvider | KubernetesObjectsProviderFactory, ): void; } @@ -61,6 +59,15 @@ export const kubernetesObjectsProviderExtensionPoint = id: 'kubernetes.objects-provider', }); +/** + * A factory function for creating a KubernetesClustersSupplier. + * + * @public + */ +export type KubernetesClusterSupplierFactory = (opts: { + getDefault: () => Promise; +}) => Promise; + /** * The interface for {@link kubernetesClusterSupplierExtensionPoint}. * @@ -70,11 +77,7 @@ export interface KubernetesClusterSupplierExtensionPoint { addClusterSupplier( clusterSupplier: | KubernetesClustersSupplier - | (({ - getDefault, - }: { - getDefault: () => KubernetesClustersSupplier; - }) => KubernetesClustersSupplier), + | KubernetesClusterSupplierFactory, ): void; } @@ -107,21 +110,22 @@ export const kubernetesAuthStrategyExtensionPoint = id: 'kubernetes.auth-strategy', }); +/** + * A factory function for creating a KubernetesFetcher. + * + * @public + */ +export type KubernetesFetcherFactory = (opts: { + getDefault: () => Promise; +}) => Promise; + /** * The interface for {@link kubernetesFetcherExtensionPoint}. * * @public */ export interface KubernetesFetcherExtensionPoint { - addFetcher( - fetcher: - | KubernetesFetcher - | (({ - getDefault, - }: { - getDefault: () => KubernetesFetcher; - }) => KubernetesFetcher), - ): void; + addFetcher(fetcher: KubernetesFetcher | KubernetesFetcherFactory): void; } /** @@ -134,6 +138,16 @@ export const kubernetesFetcherExtensionPoint = id: 'kubernetes.fetcher', }); +/** + * A factory function for creating a KubernetesServiceLocator. + * + * @public + */ +export type KubernetesServiceLocatorFactory = (opts: { + getDefault: () => Promise; + clusterSupplier: KubernetesClustersSupplier; +}) => Promise; + /** * The interface for {@link kubernetesServiceLocatorExtensionPoint}. * @@ -141,15 +155,7 @@ export const kubernetesFetcherExtensionPoint = */ export interface KubernetesServiceLocatorExtensionPoint { addServiceLocator( - serviceLocator: - | KubernetesServiceLocator - | (({ - getDefault, - clusterSupplier, - }: { - getDefault: () => KubernetesServiceLocator; - clusterSupplier: KubernetesClustersSupplier; - }) => KubernetesServiceLocator), + serviceLocator: KubernetesServiceLocator | KubernetesServiceLocatorFactory, ): void; } From 6ab6e5c2bfde12425eab33c6251b27a63d76cac8 Mon Sep 17 00:00:00 2001 From: benjdlambert Date: Tue, 19 Aug 2025 09:27:15 +0200 Subject: [PATCH 6/8] 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; -} From 85e6fa20a782d17194875c36a81e5043a56b8099 Mon Sep 17 00:00:00 2001 From: benjdlambert Date: Tue, 19 Aug 2025 09:40:21 +0200 Subject: [PATCH 7/8] chore: update docs and fix naming Signed-off-by: benjdlambert --- docs/features/kubernetes/installation.md | 20 ++++++++++++++++--- plugins/kubernetes-backend/src/plugin.ts | 2 +- ...nitializer.ts => KubernetesInitializer.ts} | 0 3 files changed, 18 insertions(+), 4 deletions(-) rename plugins/kubernetes-backend/src/service/{KubernetnesInitializer.ts => KubernetesInitializer.ts} (100%) diff --git a/docs/features/kubernetes/installation.md b/docs/features/kubernetes/installation.md index a99bd0b429..6649ff78c1 100644 --- a/docs/features/kubernetes/installation.md +++ b/docs/features/kubernetes/installation.md @@ -87,6 +87,7 @@ import { ClusterDetails, KubernetesClustersSupplier, kubernetesClusterSupplierExtensionPoint, + kubernetesServiceLocatorExtensionPoint, } from '@backstage/plugin-kubernetes-node'; export class CustomClustersSupplier implements KubernetesClustersSupplier { @@ -119,12 +120,25 @@ export const kubernetesModuleCustomClusterDiscovery = createBackendModule({ register(env) { env.registerInit({ deps: { - kubernetes: kubernetesClusterSupplierExtensionPoint, + clusterSupplier: kubernetesClusterSupplierExtensionPoint, + serviceLocator: kubernetesServiceLocatorExtensionPoint, }, - async init({ kubernetes }) { - kubernetes.addClusterSupplier( + async init({ clusterSupplier }) { + clusterSupplier.addClusterSupplier( CustomClustersSupplier.create(Duration.fromObject({ minutes: 60 })), ); + + // there's also the ability to get access to some of the default implementations of the extension points where + // neccessary: + serviceLocator.addServiceLocator( + async ({ getDefault, clusterSupplier }) => { + // get access to the default service locator: + const defaultImplementation = await getDefault(); + + // build your own with the clusterSupplier dependency: + return MyNewServiceLocator({ clusterSupplier }); + }, + ); }, }); }, diff --git a/plugins/kubernetes-backend/src/plugin.ts b/plugins/kubernetes-backend/src/plugin.ts index a7bd323068..b4283d4914 100644 --- a/plugins/kubernetes-backend/src/plugin.ts +++ b/plugins/kubernetes-backend/src/plugin.ts @@ -42,7 +42,7 @@ import { KubernetesServiceLocatorFactory, } from '@backstage/plugin-kubernetes-node'; import { KubernetesRouter } from './service/KubernetesRouter'; -import { KubernetesInitializer } from './service/KubernetnesInitializer'; +import { KubernetesInitializer } from './service/KubernetesInitializer'; class ObjectsProvider implements KubernetesObjectsProviderExtensionPoint { private objectsProvider: KubernetesObjectsProviderFactory | undefined; diff --git a/plugins/kubernetes-backend/src/service/KubernetnesInitializer.ts b/plugins/kubernetes-backend/src/service/KubernetesInitializer.ts similarity index 100% rename from plugins/kubernetes-backend/src/service/KubernetnesInitializer.ts rename to plugins/kubernetes-backend/src/service/KubernetesInitializer.ts From 0ad6b068bf11b934c74f6629cdca3fbeb2a51ed7 Mon Sep 17 00:00:00 2001 From: benjdlambert Date: Tue, 19 Aug 2025 09:42:11 +0200 Subject: [PATCH 8/8] chore: whoops last bit of docxs Signed-off-by: benjdlambert --- docs/features/kubernetes/installation.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/features/kubernetes/installation.md b/docs/features/kubernetes/installation.md index 6649ff78c1..fa37253cd6 100644 --- a/docs/features/kubernetes/installation.md +++ b/docs/features/kubernetes/installation.md @@ -123,7 +123,8 @@ export const kubernetesModuleCustomClusterDiscovery = createBackendModule({ clusterSupplier: kubernetesClusterSupplierExtensionPoint, serviceLocator: kubernetesServiceLocatorExtensionPoint, }, - async init({ clusterSupplier }) { + async init({ clusterSupplier, serviceLocator }) { + // simple replace of the internal dependency clusterSupplier.addClusterSupplier( CustomClustersSupplier.create(Duration.fromObject({ minutes: 60 })), ); @@ -136,7 +137,7 @@ export const kubernetesModuleCustomClusterDiscovery = createBackendModule({ const defaultImplementation = await getDefault(); // build your own with the clusterSupplier dependency: - return MyNewServiceLocator({ clusterSupplier }); + return new MyNewServiceLocator({ clusterSupplier }); }, ); },