feat: last cleanup and fix tests

Signed-off-by: benjdlambert <ben@blam.sh>
This commit is contained in:
benjdlambert
2025-08-19 09:27:15 +02:00
parent 908420bf3b
commit 6ab6e5c2bf
5 changed files with 279 additions and 210 deletions
+31 -138
View File
@@ -22,7 +22,6 @@ import { catalogServiceRef } from '@backstage/plugin-catalog-node';
import {
type AuthenticationStrategy,
CustomResource,
kubernetesAuthStrategyExtensionPoint,
type KubernetesAuthStrategyExtensionPoint,
type KubernetesClustersSupplier,
@@ -37,25 +36,13 @@ import {
kubernetesObjectsProviderExtensionPoint,
type KubernetesObjectsProviderExtensionPoint,
KubernetesObjectsProviderFactory,
KubernetesObjectTypes,
type KubernetesServiceLocator,
kubernetesServiceLocatorExtensionPoint,
type KubernetesServiceLocatorExtensionPoint,
KubernetesServiceLocatorFactory,
ObjectToFetch,
} from '@backstage/plugin-kubernetes-node';
import { KubernetesBuilder } from './service/KubernetesBuilder';
import { KubernetesClientBasedFetcher } from './service/KubernetesFetcher';
import { DispatchStrategy } from './auth/DispatchStrategy';
import { getCombinedClusterSupplier } from './cluster-locator';
import { buildDefaultAuthStrategyMap } from './auth/buildDefaultAuthStrategyMap';
import { buildDefaultServiceLocator } from './service-locator/buildDefaultServiceLocator';
import { Duration } from 'luxon';
import {
ALL_OBJECTS,
DEFAULT_OBJECTS,
KubernetesFanOutHandler,
} from './service/KubernetesFanOutHandler';
import { KubernetesRouter } from './service/KubernetesRouter';
import { KubernetesInitializer } from './service/KubernetnesInitializer';
class ObjectsProvider implements KubernetesObjectsProviderExtensionPoint {
private objectsProvider: KubernetesObjectsProviderFactory | undefined;
@@ -151,20 +138,21 @@ class ServiceLocator implements KubernetesServiceLocatorExtensionPoint {
}
class AuthStrategy implements KubernetesAuthStrategyExtensionPoint {
private authStrategies: Map<string, AuthenticationStrategy>;
constructor() {
this.authStrategies = new Map<string, AuthenticationStrategy>();
}
private authStrategies: Map<string, AuthenticationStrategy> | undefined;
getAuthenticationStrategies() {
return this.authStrategies;
}
addAuthStrategy(key: string, authStrategy: AuthenticationStrategy) {
if (!this.authStrategies) {
this.authStrategies = new Map<string, AuthenticationStrategy>();
}
if (key.includes('-')) {
throw new Error('Strategy name can not include dashes');
}
this.authStrategies.set(key, authStrategy);
}
}
@@ -226,121 +214,27 @@ export const kubernetesPlugin = createBackendPlugin({
}) {
// TODO: this could do with a cleanup and push some of this initalization somewhere else
if (config.has('kubernetes')) {
const defaultFetcherFactory = async () =>
new KubernetesClientBasedFetcher({
logger,
});
const fetcher =
extPointFetcher.getFetcher()?.({
getDefault: defaultFetcherFactory,
}) ?? defaultFetcherFactory();
const returnedAuthStrategyMap =
extPointAuthStrategy.getAuthenticationStrategies();
const authStrategyMap =
returnedAuthStrategyMap.size > 0
? returnedAuthStrategyMap
: buildDefaultAuthStrategyMap({ logger, config });
const refreshInterval = Duration.fromObject({
minutes: 60,
const initializer = KubernetesInitializer.create({
logger,
config,
catalog,
auth,
fetcher: extPointFetcher.getFetcher(),
clusterSupplier: extPointClusterSuplier.getClusterSupplier(),
serviceLocator: extPointServiceLocator.getServiceLocator(),
objectsProvider: extPointObjectsProvider.getObjectsProvider(),
authStrategyMap: extPointAuthStrategy.getAuthenticationStrategies(),
});
const defaultClusterSupplierFactory = async () =>
getCombinedClusterSupplier(
config,
catalog,
new DispatchStrategy({
authStrategyMap: Object.fromEntries(authStrategyMap.entries()),
}),
logger,
refreshInterval,
auth,
);
const {
fetcher,
authStrategyMap,
clusterSupplier,
serviceLocator,
objectsProvider,
} = await initializer.init();
const clusterSupplier =
extPointClusterSuplier.getClusterSupplier()?.({
getDefault: defaultClusterSupplierFactory,
}) ?? defaultClusterSupplierFactory();
const defaultServiceLocatorFactory = async () =>
buildDefaultServiceLocator({
config,
clusterSupplier: await clusterSupplier,
});
const serviceLocator =
extPointServiceLocator.getServiceLocator()?.({
getDefault: defaultServiceLocatorFactory,
clusterSupplier: await clusterSupplier,
}) ?? defaultServiceLocatorFactory();
const objectTypesToFetchStrings = config.getOptionalStringArray(
'kubernetes.objectTypes',
) as KubernetesObjectTypes[];
const apiVersionOverrides = config.getOptionalConfig(
'kubernetes.apiVersionOverrides',
);
let objectTypesToFetch: ObjectToFetch[] | undefined = undefined;
if (objectTypesToFetchStrings) {
objectTypesToFetch = ALL_OBJECTS.filter(obj =>
objectTypesToFetchStrings.includes(obj.objectType),
);
}
if (apiVersionOverrides) {
objectTypesToFetch ??= DEFAULT_OBJECTS;
for (const obj of objectTypesToFetch) {
if (apiVersionOverrides.has(obj.objectType)) {
obj.apiVersion = apiVersionOverrides.getString(obj.objectType);
}
}
}
const customResources: CustomResource[] = (
config.getOptionalConfigArray('kubernetes.customResources') ?? []
).map(
c =>
({
group: c.getString('group'),
apiVersion: c.getString('apiVersion'),
plural: c.getString('plural'),
objectType: 'customresources',
} as CustomResource),
);
const defaultObjectsProviderFactory = async () =>
new KubernetesFanOutHandler({
logger,
config,
fetcher: await fetcher,
serviceLocator: await serviceLocator,
customResources,
objectTypesToFetch,
authStrategy: new DispatchStrategy({
authStrategyMap: Object.fromEntries(authStrategyMap.entries()),
}),
});
const objectsProvider =
extPointObjectsProvider.getObjectsProvider()?.({
clusterSupplier: await clusterSupplier,
getDefault: defaultObjectsProviderFactory,
serviceLocator: await serviceLocator,
customResources,
objectTypesToFetch,
authStrategy: new DispatchStrategy({
authStrategyMap: Object.fromEntries(authStrategyMap.entries()),
}),
}) ?? defaultObjectsProviderFactory();
const builder: KubernetesBuilder = KubernetesBuilder.createBuilder({
const router = KubernetesRouter.create({
logger,
config,
catalog,
@@ -349,14 +243,13 @@ export const kubernetesPlugin = createBackendPlugin({
auth,
httpAuth,
authStrategyMap: Object.fromEntries(authStrategyMap.entries()),
fetcher: await fetcher,
clusterSupplier: await clusterSupplier,
serviceLocator: await serviceLocator,
objectsProvider: await objectsProvider,
fetcher,
clusterSupplier,
serviceLocator,
objectsProvider,
});
const { router } = await builder.build();
http.use(router);
http.use(await router.getRouter());
} else {
logger.warn(
'Failed to initialize kubernetes backend: valid kubernetes config is missing',
@@ -64,18 +64,14 @@ export interface KubernetesEnvironment {
objectsProvider: KubernetesObjectsProvider;
}
export type KubernetesBuilderReturn = Promise<{
router: express.Router;
}>;
export class KubernetesBuilder {
static createBuilder(env: KubernetesEnvironment) {
return new KubernetesBuilder(env);
export class KubernetesRouter {
static create(env: KubernetesEnvironment) {
return new KubernetesRouter(env);
}
constructor(protected readonly env: KubernetesEnvironment) {}
public async build(): KubernetesBuilderReturn {
public async getRouter() {
const {
logger,
config,
@@ -97,9 +93,7 @@ export class KubernetesBuilder {
logger.warn(
'Failed to initialize kubernetes backend: kubernetes config is missing',
);
return {
router: Router(),
} as unknown as KubernetesBuilderReturn;
return Router();
}
const proxy = this.buildProxy(
@@ -110,7 +104,7 @@ export class KubernetesBuilder {
authStrategyMap,
);
const router = this.buildRouter(
return this.buildRouter(
objectsProvider,
clusterSupplier,
catalog,
@@ -119,10 +113,6 @@ export class KubernetesBuilder {
httpAuth,
authStrategyMap,
);
return {
router,
};
}
private buildProxy(
@@ -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<string, AuthenticationStrategy>;
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<string, AuthenticationStrategy>;
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<string, AuthenticationStrategy>;
}) {
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<string, AuthenticationStrategy>;
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,
};
}
}
@@ -1,56 +0,0 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Logger } from 'winston';
import {
AuthenticationStrategy,
KubernetesClustersSupplier,
KubernetesFetcher,
KubernetesObjectsProvider,
KubernetesServiceLocator,
} from '@backstage/plugin-kubernetes-node';
import express from 'express';
import { KubernetesBuilder } from './KubernetesBuilder';
import { PermissionEvaluator } from '@backstage/plugin-permission-common';
import {
DiscoveryService,
HttpAuthService,
RootConfigService,
AuthService,
} from '@backstage/backend-plugin-api';
import { CatalogService } from '@backstage/plugin-catalog-node';
interface RouterOptions {
logger: Logger;
config: RootConfigService;
catalog: CatalogService;
clusterSupplier: KubernetesClustersSupplier;
discovery: DiscoveryService;
permissions: PermissionEvaluator;
auth: AuthService;
httpAuth: HttpAuthService;
authStrategyMap: { [key: string]: AuthenticationStrategy };
fetcher: KubernetesFetcher;
serviceLocator: KubernetesServiceLocator;
objectsProvider: KubernetesObjectsProvider;
}
export async function createRouter(
options: RouterOptions,
): Promise<express.Router> {
const { router } = await KubernetesBuilder.createBuilder(options).build();
return router;
}