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; }