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 diff --git a/docs/features/kubernetes/installation.md b/docs/features/kubernetes/installation.md index a99bd0b429..fa37253cd6 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,26 @@ export const kubernetesModuleCustomClusterDiscovery = createBackendModule({ register(env) { env.registerInit({ deps: { - kubernetes: kubernetesClusterSupplierExtensionPoint, + clusterSupplier: kubernetesClusterSupplierExtensionPoint, + serviceLocator: kubernetesServiceLocatorExtensionPoint, }, - async init({ kubernetes }) { - kubernetes.addClusterSupplier( + async init({ clusterSupplier, serviceLocator }) { + // simple replace of the internal dependency + 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 new MyNewServiceLocator({ clusterSupplier }); + }, + ); }, }); }, 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..4bd5cf97f3 --- /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({ config })], + ['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..b4283d4914 100644 --- a/plugins/kubernetes-backend/src/plugin.ts +++ b/plugins/kubernetes-backend/src/plugin.ts @@ -18,7 +18,7 @@ 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, @@ -27,112 +27,133 @@ import { type KubernetesClustersSupplier, kubernetesClusterSupplierExtensionPoint, type KubernetesClusterSupplierExtensionPoint, + KubernetesClusterSupplierFactory, type KubernetesFetcher, kubernetesFetcherExtensionPoint, type KubernetesFetcherExtensionPoint, + KubernetesFetcherFactory, type KubernetesObjectsProvider, kubernetesObjectsProviderExtensionPoint, type KubernetesObjectsProviderExtensionPoint, + KubernetesObjectsProviderFactory, type KubernetesServiceLocator, kubernetesServiceLocatorExtensionPoint, type KubernetesServiceLocatorExtensionPoint, + KubernetesServiceLocatorFactory, } from '@backstage/plugin-kubernetes-node'; -import { KubernetesBuilder } from './service/KubernetesBuilder'; +import { KubernetesRouter } from './service/KubernetesRouter'; +import { KubernetesInitializer } from './service/KubernetesInitializer'; class ObjectsProvider implements KubernetesObjectsProviderExtensionPoint { - private objectsProvider: 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', ); } - this.objectsProvider = provider; + if (typeof provider !== 'function') { + this.objectsProvider = async () => provider; + } else { + this.objectsProvider = provider; + } } } class ClusterSuplier implements KubernetesClusterSupplierExtensionPoint { - private clusterSupplier: 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', ); } - this.clusterSupplier = clusterSupplier; + if (typeof clusterSupplier !== 'function') { + this.clusterSupplier = async () => clusterSupplier; + } else { + this.clusterSupplier = clusterSupplier; + } } } class Fetcher implements KubernetesFetcherExtensionPoint { - private fetcher: KubernetesFetcher | undefined; + private fetcher: KubernetesFetcherFactory | undefined; getFetcher() { return this.fetcher; } - addFetcher(fetcher: KubernetesFetcher) { + addFetcher(fetcher: KubernetesFetcher | KubernetesFetcherFactory) { if (this.fetcher) { throw new Error( 'Multiple Kubernetes Fetchers is not supported at this time', ); } - this.fetcher = fetcher; + if (typeof fetcher !== 'function') { + this.fetcher = async () => fetcher; + } else { + this.fetcher = fetcher; + } } } class ServiceLocator implements KubernetesServiceLocatorExtensionPoint { - private serviceLocator: 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', ); } - this.serviceLocator = serviceLocator; + + if (typeof serviceLocator !== 'function') { + this.serviceLocator = async () => serviceLocator; + } else { + this.serviceLocator = serviceLocator; + } } } class AuthStrategy implements KubernetesAuthStrategyExtensionPoint { - private authStrategies: Array<{ - key: string; - strategy: AuthenticationStrategy; - }>; - - 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)); - } + private authStrategies: Map | undefined; getAuthenticationStrategies() { return this.authStrategies; } addAuthStrategy(key: string, authStrategy: AuthenticationStrategy) { - this.authStrategies.push({ key, strategy: authStrategy }); + if (!this.authStrategies) { + this.authStrategies = new Map(); + } + + if (key.includes('-')) { + throw new Error('Strategy name can not include dashes'); + } + + this.authStrategies.set(key, authStrategy); } } @@ -176,7 +197,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,33 +207,49 @@ export const kubernetesPlugin = createBackendPlugin({ logger, config, discovery, - catalogApi, + catalog, permissions, 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 builder: KubernetesBuilder = KubernetesBuilder.createBuilder({ + const initializer = KubernetesInitializer.create({ logger, config, - catalogApi, + catalog, + auth, + fetcher: extPointFetcher.getFetcher(), + clusterSupplier: extPointClusterSuplier.getClusterSupplier(), + serviceLocator: extPointServiceLocator.getServiceLocator(), + objectsProvider: extPointObjectsProvider.getObjectsProvider(), + authStrategyMap: extPointAuthStrategy.getAuthenticationStrategies(), + }); + + const { + fetcher, + authStrategyMap, + clusterSupplier, + serviceLocator, + objectsProvider, + } = await initializer.init(); + + const router = KubernetesRouter.create({ + logger, + config, + 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); + 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/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 deleted file mode 100644 index 2cf0bcf04c..0000000000 --- a/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts +++ /dev/null @@ -1,564 +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 { CatalogApi } from '@backstage/catalog-client'; -import { Config } from '@backstage/config'; -import { - ANNOTATION_KUBERNETES_AUTH_PROVIDER, - ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER, - kubernetesClustersReadPermission, - kubernetesPermissions, - kubernetesResourcesReadPermission, -} from '@backstage/plugin-kubernetes-common'; -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 { - AuthService, - BackstageCredentials, - DiscoveryService, - HttpAuthService, - LoggerService, -} from '@backstage/backend-plugin-api'; -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 { KubernetesProxy } from './KubernetesProxy'; -import { requirePermission } from '../auth/requirePermission'; - -/** - * @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; - discovery: DiscoveryService; - permissions: PermissionEvaluator; - auth: AuthService; - httpAuth: HttpAuthService; -} - -/** - * 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); - } - - constructor(protected readonly env: KubernetesEnvironment) {} - - public async build(): KubernetesBuilderReturn { - const logger = this.env.logger; - const config = this.env.config; - const permissions = this.env.permissions; - - logger.info('Initializing Kubernetes backend'); - - if (!config.has('kubernetes')) { - if (process.env.NODE_ENV !== 'development') { - throw new Error('Kubernetes configuration is missing'); - } - logger.warn( - 'Failed to initialize kubernetes backend: kubernetes config is missing', - ); - return { - router: Router(), - } as unknown as KubernetesBuilderReturn; - } - - const customResources = this.buildCustomResources(); - - const fetcher = this.getFetcher(); - - const clusterSupplier = this.getClusterSupplier(); - - const authStrategyMap = this.getAuthStrategyMap(); - - const proxy = this.getProxy( - logger, - clusterSupplier, - this.env.discovery, - this.env.httpAuth, - ); - - 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, - proxy, - permissions, - this.env.auth, - this.env.httpAuth, - ); - - 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( - logger: LoggerService, - clusterSupplier: KubernetesClustersSupplier, - discovery: DiscoveryService, - httpAuth: HttpAuthService, - ): KubernetesProxy { - const authStrategyMap = this.getAuthStrategyMap(); - const authStrategy = new DispatchStrategy({ - authStrategyMap, - }); - this.proxy = new KubernetesProxy({ - logger, - clusterSupplier, - authStrategy, - discovery, - httpAuth, - }); - return this.proxy; - } - - protected buildRouter( - objectsProvider: KubernetesObjectsProvider, - clusterSupplier: KubernetesClustersSupplier, - catalogApi: CatalogApi, - proxy: KubernetesProxy, - permissionApi: PermissionEvaluator, - authService: AuthService, - httpAuth: HttpAuthService, - ): express.Router { - const logger = this.env.logger; - const router = Router(); - router.use('/proxy', proxy.createRequestHandler({ permissionApi })); - router.use(express.json()); - router.use( - createPermissionIntegrationRouter({ - permissions: kubernetesPermissions, - }), - ); - // @deprecated - router.post('/services/:serviceId', async (req, res) => { - await requirePermission( - permissionApi, - kubernetesResourcesReadPermission, - httpAuth, - req, - ); - const serviceId = req.params.serviceId; - const requestBody: ObjectsByEntityRequest = req.body; - try { - const response = await objectsProvider.getKubernetesObjectsByEntity( - { - entity: requestBody.entity, - auth: requestBody.auth || {}, - }, - { credentials: await httpAuth.credentials(req) }, - ); - res.json(response); - } catch (e) { - logger.error( - `action=retrieveObjectsByServiceId service=${serviceId}, error=${e}`, - ); - res.status(500).json({ error: e.message }); - } - }); - - router.get('/clusters', async (req, res) => { - await requirePermission( - permissionApi, - kubernetesClustersReadPermission, - httpAuth, - req, - ); - const credentials = await httpAuth.credentials(req); - const clusterDetails = await this.fetchClusterDetails(clusterSupplier, { - credentials, - }); - res.json({ - items: clusterDetails.map(cd => { - const oidcTokenProvider = - cd.authMetadata[ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER]; - const authProvider = - cd.authMetadata[ANNOTATION_KUBERNETES_AUTH_PROVIDER]; - const strategy = this.getAuthStrategyMap()[authProvider]; - let auth: AuthMetadata = {}; - if (strategy) { - auth = strategy.presentAuthMetadata(cd.authMetadata); - } - - return { - name: cd.name, - title: cd.title, - dashboardUrl: cd.dashboardUrl, - authProvider, - ...(oidcTokenProvider && { oidcTokenProvider }), - ...(auth && Object.keys(auth).length !== 0 && { auth }), - }; - }), - }); - }); - - addResourceRoutesToRouter( - router, - catalogApi, - objectsProvider, - authService, - httpAuth, - permissionApi, - ); - - 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( - clusterSupplier: KubernetesClustersSupplier, - options: { credentials: BackstageCredentials }, - ) { - const clusterDetails = await clusterSupplier.getClusters(options); - - this.env.logger.debug( - `action=loadClusterDetails numOfClustersLoaded=${clusterDetails.length}`, - ); - - 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/KubernetesInitializer.ts b/plugins/kubernetes-backend/src/service/KubernetesInitializer.ts new file mode 100644 index 0000000000..8e85bb152c --- /dev/null +++ b/plugins/kubernetes-backend/src/service/KubernetesInitializer.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/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/KubernetesRouter.ts b/plugins/kubernetes-backend/src/service/KubernetesRouter.ts new file mode 100644 index 0000000000..5a5518670e --- /dev/null +++ b/plugins/kubernetes-backend/src/service/KubernetesRouter.ts @@ -0,0 +1,241 @@ +/* + * 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 { Config } from '@backstage/config'; +import { + ANNOTATION_KUBERNETES_AUTH_PROVIDER, + ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER, + kubernetesClustersReadPermission, + kubernetesPermissions, + kubernetesResourcesReadPermission, +} from '@backstage/plugin-kubernetes-common'; +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 { DispatchStrategy } from '../auth'; + +import { + AuthService, + BackstageCredentials, + DiscoveryService, + HttpAuthService, + LoggerService, +} from '@backstage/backend-plugin-api'; +import { + AuthenticationStrategy, + AuthMetadata, + KubernetesClustersSupplier, + KubernetesFetcher, + KubernetesObjectsProvider, + KubernetesServiceLocator, +} from '@backstage/plugin-kubernetes-node'; +import { addResourceRoutesToRouter } from '../routes/resourcesRoutes'; +import { ObjectsByEntityRequest } from '../types/types'; +import { KubernetesProxy } from './KubernetesProxy'; +import { requirePermission } from '../auth/requirePermission'; +import { CatalogService } from '@backstage/plugin-catalog-node'; + +export interface KubernetesEnvironment { + logger: LoggerService; + config: Config; + catalog: CatalogService; + discovery: DiscoveryService; + permissions: PermissionEvaluator; + auth: AuthService; + httpAuth: HttpAuthService; + authStrategyMap: { [key: string]: AuthenticationStrategy }; + fetcher: KubernetesFetcher; + clusterSupplier: KubernetesClustersSupplier; + serviceLocator: KubernetesServiceLocator; + objectsProvider: KubernetesObjectsProvider; +} + +export class KubernetesRouter { + static create(env: KubernetesEnvironment) { + return new KubernetesRouter(env); + } + + constructor(protected readonly env: KubernetesEnvironment) {} + + public async getRouter() { + const { + logger, + config, + permissions, + authStrategyMap, + clusterSupplier, + objectsProvider, + catalog, + discovery, + httpAuth, + } = this.env; + + logger.info('Initializing Kubernetes backend'); + + if (!config.has('kubernetes')) { + if (process.env.NODE_ENV !== 'development') { + throw new Error('Kubernetes configuration is missing'); + } + logger.warn( + 'Failed to initialize kubernetes backend: kubernetes config is missing', + ); + return Router(); + } + + const proxy = this.buildProxy( + logger, + clusterSupplier, + discovery, + httpAuth, + authStrategyMap, + ); + + return this.buildRouter( + objectsProvider, + clusterSupplier, + catalog, + proxy, + permissions, + httpAuth, + authStrategyMap, + ); + } + + private buildProxy( + logger: LoggerService, + clusterSupplier: KubernetesClustersSupplier, + discovery: DiscoveryService, + httpAuth: HttpAuthService, + authStrategyMap: { [key: string]: AuthenticationStrategy }, + ): KubernetesProxy { + const authStrategy = new DispatchStrategy({ + authStrategyMap, + }); + return new KubernetesProxy({ + logger, + clusterSupplier, + authStrategy, + discovery, + httpAuth, + }); + } + + private buildRouter( + objectsProvider: KubernetesObjectsProvider, + clusterSupplier: KubernetesClustersSupplier, + catalog: CatalogService, + proxy: KubernetesProxy, + permissionApi: PermissionEvaluator, + httpAuth: HttpAuthService, + authStrategyMap: { [key: string]: AuthenticationStrategy }, + ): express.Router { + const logger = this.env.logger; + const router = Router(); + router.use('/proxy', proxy.createRequestHandler({ permissionApi })); + router.use(express.json()); + router.use( + createPermissionIntegrationRouter({ + permissions: kubernetesPermissions, + }), + ); + + // @deprecated + router.post('/services/:serviceId', async (req, res) => { + await requirePermission( + permissionApi, + kubernetesResourcesReadPermission, + httpAuth, + req, + ); + const serviceId = req.params.serviceId; + const requestBody: ObjectsByEntityRequest = req.body; + try { + const response = await objectsProvider.getKubernetesObjectsByEntity( + { + entity: requestBody.entity, + auth: requestBody.auth || {}, + }, + { credentials: await httpAuth.credentials(req) }, + ); + res.json(response); + } catch (e) { + logger.error( + `action=retrieveObjectsByServiceId service=${serviceId}, error=${e}`, + ); + res.status(500).json({ error: e.message }); + } + }); + + router.get('/clusters', async (req, res) => { + await requirePermission( + permissionApi, + kubernetesClustersReadPermission, + httpAuth, + req, + ); + const credentials = await httpAuth.credentials(req); + const clusterDetails = await this.fetchClusterDetails(clusterSupplier, { + credentials, + }); + res.json({ + items: clusterDetails.map(cd => { + const oidcTokenProvider = + cd.authMetadata[ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER]; + const authProvider = + cd.authMetadata[ANNOTATION_KUBERNETES_AUTH_PROVIDER]; + const strategy = authStrategyMap[authProvider]; + let auth: AuthMetadata = {}; + if (strategy) { + auth = strategy.presentAuthMetadata(cd.authMetadata); + } + + return { + name: cd.name, + title: cd.title, + dashboardUrl: cd.dashboardUrl, + authProvider, + ...(oidcTokenProvider && { oidcTokenProvider }), + ...(auth && Object.keys(auth).length !== 0 && { auth }), + }; + }), + }); + }); + + addResourceRoutesToRouter( + router, + catalog, + objectsProvider, + httpAuth, + permissionApi, + ); + + return router; + } + + private async fetchClusterDetails( + clusterSupplier: KubernetesClustersSupplier, + options: { credentials: BackstageCredentials }, + ) { + const clusterDetails = await clusterSupplier.getClusters(options); + + this.env.logger.debug( + `action=loadClusterDetails numOfClustersLoaded=${clusterDetails.length}`, + ); + + return clusterDetails; + } +} diff --git a/plugins/kubernetes-backend/src/service/router.ts b/plugins/kubernetes-backend/src/service/router.ts deleted file mode 100644 index 66fc131262..0000000000 --- a/plugins/kubernetes-backend/src/service/router.ts +++ /dev/null @@ -1,48 +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 { KubernetesClustersSupplier } 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, - HttpAuthService, - RootConfigService, - AuthService, -} from '@backstage/backend-plugin-api'; - -interface RouterOptions { - logger: Logger; - config: RootConfigService; - catalogApi: CatalogApi; - clusterSupplier?: KubernetesClustersSupplier; - discovery: DiscoveryService; - permissions: PermissionEvaluator; - auth: AuthService; - httpAuth: HttpAuthService; -} - -export async function createRouter( - options: RouterOptions, -): Promise { - const { router } = await KubernetesBuilder.createBuilder(options) - .setClusterSupplier(options.clusterSupplier) - .build(); - return router; -} diff --git a/plugins/kubernetes-node/report.api.md b/plugins/kubernetes-node/report.api.md index 5758db9ea1..2e96057be8 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,12 +97,21 @@ export interface KubernetesClustersSupplier { // @public export interface KubernetesClusterSupplierExtensionPoint { // (undocumented) - addClusterSupplier(clusterSupplier: KubernetesClustersSupplier_2): void; + addClusterSupplier( + clusterSupplier: + | KubernetesClustersSupplier_2 + | KubernetesClusterSupplierFactory, + ): void; } // @public export const kubernetesClusterSupplierExtensionPoint: ExtensionPoint; +// @public +export type KubernetesClusterSupplierFactory = (opts: { + getDefault: () => Promise; +}) => Promise; + // @public export type KubernetesCredential = | { @@ -134,12 +145,17 @@ export interface KubernetesFetcher { // @public export interface KubernetesFetcherExtensionPoint { // (undocumented) - addFetcher(fetcher: 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) @@ -169,12 +185,24 @@ export interface KubernetesObjectsProvider { // @public export interface KubernetesObjectsProviderExtensionPoint { // (undocumented) - addObjectsProvider(provider: KubernetesObjectsProvider_2): void; + addObjectsProvider( + 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' @@ -207,12 +235,22 @@ export interface KubernetesServiceLocator { // @public export interface KubernetesServiceLocatorExtensionPoint { // (undocumented) - addServiceLocator(serviceLocator: KubernetesServiceLocator_2): void; + addServiceLocator( + serviceLocator: + | 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 d898244161..6f112ece23 100644 --- a/plugins/kubernetes-node/src/extensions.ts +++ b/plugins/kubernetes-node/src/extensions.ts @@ -16,19 +16,37 @@ import { createExtensionPoint } from '@backstage/backend-plugin-api'; import { AuthenticationStrategy, + CustomResource, + ObjectToFetch, KubernetesClustersSupplier, KubernetesFetcher, KubernetesObjectsProvider, 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}. * * @public */ export interface KubernetesObjectsProviderExtensionPoint { - addObjectsProvider(provider: KubernetesObjectsProvider): void; + addObjectsProvider( + provider: KubernetesObjectsProvider | KubernetesObjectsProviderFactory, + ): void; } /** @@ -41,13 +59,26 @@ 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}. * * @public */ export interface KubernetesClusterSupplierExtensionPoint { - addClusterSupplier(clusterSupplier: KubernetesClustersSupplier): void; + addClusterSupplier( + clusterSupplier: + | KubernetesClustersSupplier + | KubernetesClusterSupplierFactory, + ): void; } /** @@ -79,13 +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): void; + addFetcher(fetcher: KubernetesFetcher | KubernetesFetcherFactory): void; } /** @@ -98,13 +138,25 @@ 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}. * * @public */ export interface KubernetesServiceLocatorExtensionPoint { - addServiceLocator(serviceLocator: KubernetesServiceLocator): void; + addServiceLocator( + serviceLocator: KubernetesServiceLocator | KubernetesServiceLocatorFactory, + ): void; } /**