chore: more of a refactor

Signed-off-by: benjdlambert <ben@blam.sh>

Signed-off-by: benjdlambert <ben@blam.sh>
This commit is contained in:
benjdlambert
2025-08-13 07:48:16 +02:00
parent 643db70f09
commit d6d639a9ea
9 changed files with 380 additions and 423 deletions
@@ -51,6 +51,7 @@ export class DispatchStrategy implements AuthenticationStrategy {
): Promise<KubernetesCredential> {
const authProvider =
clusterDetails.authMetadata[ANNOTATION_KUBERNETES_AUTH_PROVIDER];
if (this.strategyMap[authProvider]) {
return this.strategyMap[authProvider].getCredential(clusterDetails, auth);
}
@@ -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<string, AuthenticationStrategy> =>
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()],
]);
@@ -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!;
@@ -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':
+214 -38
View File
@@ -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<string, 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));
this.authStrategies = new Map<string, AuthenticationStrategy>();
}
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 {
@@ -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)}`,
@@ -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}"`,
);
}
};
@@ -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();
}
}
@@ -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<express.Router> {
const { router } = await KubernetesBuilder.createBuilder(options)
.setClusterSupplier(options.clusterSupplier)
.build();
const { router } = await KubernetesBuilder.createBuilder(options).build();
return router;
}