chore: more of a refactor
Signed-off-by: benjdlambert <ben@blam.sh> Signed-off-by: benjdlambert <ben@blam.sh>
This commit is contained in:
@@ -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':
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user