refactor(backend-dynamic-feature-service): single line activation.

- DynamicPlugins service is restored, since it is required for plugins to depend on it in order to get the details of loaded dynamic plugins
- An all-in-one feature loader is provided that allows  1-liner installation of both the dynamic features and  additional services or plugins required to have the dynamic plugins work correctly with dynamic plugins config schemas.

Signed-off-by: David Festal <dfestal@redhat.com>
This commit is contained in:
David Festal
2024-09-25 15:40:10 +02:00
parent e6c05502d5
commit d18d4942f9
12 changed files with 263 additions and 138 deletions
+8
View File
@@ -0,0 +1,8 @@
---
'@backstage/backend-dynamic-feature-service': patch
---
Enhance and simplify the activation of the dynamic plugins feature:
- The dynamic plugins service (which implements the `DynamicPluginsProvider`) is restored, since it is required for plugins to depend on it in order to get the details of loaded dynamic plugins (possibly with loading errors to be surfaced in some UI).
- A new all-in-one feature loader (`dynamicPluginsFeatureLoader`) is provided that allows a 1-liner activation of both the dynamic features and additional services or plugins required to have the dynamic plugins work correctly with dynamic plugins config schemas. Previous service factories or feature loaders are deprecated.
@@ -15,8 +15,7 @@ In the `backend` application, it can be enabled by adding the `backend-dynamic-f
```ts
const backend = createBackend();
+
+ backend.add(dynamicPluginsFeatureDiscoveryServiceFactory) // overridden version of the FeatureDiscoveryService which provides features loaded by dynamic plugins
+ backend.add(dynamicPluginsServiceFactory)
+ backend.add(dynamicPluginsFeatureLoader) which provides features loaded by dynamic plugins
+
```
@@ -0,0 +1,111 @@
/*
* Copyright 2024 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 {
coreServices,
createBackendFeatureLoader,
} from '@backstage/backend-plugin-api';
import {
DynamicPluginsSchemasOptions,
dynamicPluginsFrontendSchemas,
dynamicPluginsRootLoggerServiceFactory,
dynamicPluginsSchemasServiceFactory,
} from '../schemas';
import {
DynamicPluginsFactoryOptions,
dynamicPluginsFeatureDiscoveryLoader,
dynamicPluginsServiceFactory,
} from '../manager';
import { DynamicPluginsRootLoggerFactoryOptions } from '../schemas';
import { configKey } from '../scanner/plugin-scanner';
/**
* @public
*/
export type DynamicPluginsFeatureLoaderOptions = DynamicPluginsFactoryOptions &
DynamicPluginsSchemasOptions &
DynamicPluginsRootLoggerFactoryOptions;
const dynamicPluginsFeatureLoaderWithOptions = (
options?: DynamicPluginsFeatureLoaderOptions,
) =>
createBackendFeatureLoader({
deps: {
config: coreServices.rootConfig,
},
*loader({ config }) {
const dynamicPluginsEnabled = config.has(configKey);
yield* [
dynamicPluginsSchemasServiceFactory(options),
dynamicPluginsServiceFactory(options),
];
if (dynamicPluginsEnabled) {
yield* [
dynamicPluginsRootLoggerServiceFactory(options),
dynamicPluginsFrontendSchemas,
dynamicPluginsFeatureDiscoveryLoader,
];
}
},
});
/**
* A backend feature loader that fully enable backend dynamic plugins.
* More precisely it:
* - adds the dynamic plugins root service (typically depended upon by plugins),
* - adds additional required features to allow supporting dynamic plugins config schemas
* in the frontend application and the backend root logger,
* - uses the dynamic plugins service to discover and expose dynamic plugins as features.
*
* @public
*
* @example
* Using the `dynamicPluginsFeatureLoader` loader in a backend instance:
* ```ts
* //...
* import { createBackend } from '@backstage/backend-defaults';
* import { dynamicPluginsFeatureLoader } from '@backstage/backend-dynamic-feature-service';
*
* const backend = createBackend();
* backend.add(dynamicPluginsFeatureLoader);
* //...
* backend.start();
* ```
*
* @example
* Passing options to the `dynamicPluginsFeatureLoader` loader in a backend instance:
* ```ts
* //...
* import { createBackend } from '@backstage/backend-defaults';
* import { dynamicPluginsFeatureLoader } from '@backstage/backend-dynamic-feature-service';
* import { myCustomModuleLoader } from './myCustomModuleLoader';
* import { myCustomSchemaLocator } from './myCustomSchemaLocator';
*
* const backend = createBackend();
* backend.add(dynamicPluginsFeatureLoader({
* moduleLoader: myCustomModuleLoader,
* schemaLocator: myCustomSchemaLocator,
*
* }));
* //...
* backend.start();
* ```
*/
export const dynamicPluginsFeatureLoader = Object.assign(
dynamicPluginsFeatureLoaderWithOptions,
dynamicPluginsFeatureLoaderWithOptions(),
);
@@ -0,0 +1,17 @@
/*
* Copyright 2023 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.
*/
export * from './features';
@@ -18,3 +18,4 @@ export * from './loader';
export * from './scanner';
export * from './manager';
export * from './schemas';
export * from './features';
@@ -250,7 +250,6 @@ export class DynamicPluginManager implements DynamicPluginProvider {
/**
* @public
* @deprecated The `featureDiscoveryService` is deprecated in favor of using {@link dynamicPluginsFeatureDiscoveryLoader} instead.
*/
export const dynamicPluginsServiceRef = createServiceRef<DynamicPluginProvider>(
{
@@ -268,7 +267,7 @@ export interface DynamicPluginsFactoryOptions {
/**
* @public
* @deprecated Use {@link dynamicPluginsFeatureDiscoveryLoader} instead.
* @deprecated Use {@link dynamicPluginsFeatureLoader} instead, which gathers all services and features required for dynamic plugins.
*/
export const dynamicPluginsServiceFactoryWithOptions = (
options?: DynamicPluginsFactoryOptions,
@@ -291,10 +290,12 @@ export const dynamicPluginsServiceFactoryWithOptions = (
/**
* @public
* @deprecated Use {@link dynamicPluginsFeatureDiscoveryLoader} instead.
* @deprecated Use {@link dynamicPluginsFeatureLoader} instead, which gathers all services and features required for dynamic plugins.
*/
export const dynamicPluginsServiceFactory =
dynamicPluginsServiceFactoryWithOptions();
export const dynamicPluginsServiceFactory = Object.assign(
dynamicPluginsServiceFactoryWithOptions,
dynamicPluginsServiceFactoryWithOptions(),
);
class DynamicPluginsEnabledFeatureDiscoveryService
implements FeatureDiscoveryService
@@ -331,7 +332,7 @@ class DynamicPluginsEnabledFeatureDiscoveryService
/**
* @public
* @deprecated The `featureDiscoveryService` is deprecated in favor of using {@link dynamicPluginsFeatureDiscoveryLoader} instead.
* @deprecated Use {@link dynamicPluginsFeatureLoader} instead, which gathers all services and features required for dynamic plugins.
*/
export const dynamicPluginsFeatureDiscoveryServiceFactory =
createServiceFactory({
@@ -345,65 +346,22 @@ export const dynamicPluginsFeatureDiscoveryServiceFactory =
},
});
const dynamicPluginsFeatureDiscoveryLoaderWithOptions = (
options?: DynamicPluginsFactoryOptions,
) =>
createBackendFeatureLoader({
deps: {
config: coreServices.rootConfig,
logger: coreServices.rootLogger,
},
async loader({ config, logger }) {
const manager = await DynamicPluginManager.create({
config,
logger,
preferAlpha: true,
moduleLoader: options?.moduleLoader?.(logger),
});
const service = new DynamicPluginsEnabledFeatureDiscoveryService(manager);
const { features } = await service.getBackendFeatures();
return features;
},
});
/**
* A backend feature loader that uses the dynamic plugins system to discover features.
*
* @public
*
* @example
* Using the `dynamicPluginsFeatureDiscoveryLoader` loader in a backend instance:
* ```ts
* //...
* import { createBackend } from '@backstage/backend-defaults';
* import { dynamicPluginsFeatureDiscoveryLoader } from '@backstage/backend-dynamic-feature-service';
*
* const backend = createBackend();
* backend.add(dynamicPluginsFeatureDiscoveryLoader);
* //...
* backend.start();
* ```
*
* @example
* Passing options to the `dynamicPluginsFeatureDiscoveryLoader` loader in a backend instance:
* ```ts
* //...
* import { createBackend } from '@backstage/backend-defaults';
* import { dynamicPluginsFeatureDiscoveryLoader } from '@backstage/backend-dynamic-feature-service';
* import { myCustomModuleLoader } from './myCustomModuleLoader';
*
* const backend = createBackend();
* backend.add(dynamicPluginsFeatureDiscoveryLoader({
* moduleLoader: myCustomModuleLoader
* }));
* //...
* backend.start();
* ```
* @deprecated Use {@link dynamicPluginsFeatureLoader} instead, which gathers all services and features required for dynamic plugins.
*/
export const dynamicPluginsFeatureDiscoveryLoader = Object.assign(
dynamicPluginsFeatureDiscoveryLoaderWithOptions,
dynamicPluginsFeatureDiscoveryLoaderWithOptions(),
);
export const dynamicPluginsFeatureDiscoveryLoader = createBackendFeatureLoader({
deps: {
dynamicPlugins: dynamicPluginsServiceRef,
},
async loader({ dynamicPlugins }) {
const service = new DynamicPluginsEnabledFeatureDiscoveryService(
dynamicPlugins,
);
const { features } = await service.getBackendFeatures();
return features;
},
});
function isBackendFeature(value: unknown): value is BackendFeature {
return (
@@ -35,6 +35,8 @@ export interface ScanRootResponse {
packages: ScannedPluginPackage[];
}
export const configKey = 'dynamicPlugins';
export class PluginScanner {
private _rootDirectory?: string;
private configUnsubscribe?: () => void;
@@ -68,7 +70,7 @@ export class PluginScanner {
}
private applyConfig(): void | never {
const dynamicPlugins = this.config.getOptional('dynamicPlugins');
const dynamicPlugins = this.config.getOptional(configKey);
if (!dynamicPlugins) {
this.logger.info("'dynamicPlugins' config entry not found.");
this._rootDirectory = undefined;
@@ -25,7 +25,10 @@ import {
loadCompiledConfigSchema,
} from '@backstage/plugin-app-node';
/** @public */
/**
* @public
* @deprecated Use {@link dynamicPluginsFeatureLoader} instead, which gathers all services and features required for dynamic plugins.
*/
export const dynamicPluginsFrontendSchemas = createBackendModule({
pluginId: 'app',
moduleId: 'core.dynamicplugins.frontendSchemas',
@@ -16,10 +16,12 @@
export {
dynamicPluginsSchemasServiceFactory,
dynamicPluginsSchemasServiceFactoryWithOptions,
type DynamicPluginsSchemasService,
type DynamicPluginsSchemasOptions,
} from './schemas';
export { dynamicPluginsFrontendSchemas } from './appBackendModule';
export { dynamicPluginsRootLoggerServiceFactory } from './rootLoggerServiceFactory';
export { dynamicPluginsFrontendSchemas } from './frontend';
export {
dynamicPluginsRootLoggerServiceFactory,
type DynamicPluginsRootLoggerFactoryOptions,
} from './rootLogger';
@@ -0,0 +1,86 @@
/*
* Copyright 2024 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 {
createServiceFactory,
coreServices,
} from '@backstage/backend-plugin-api';
import {
WinstonLogger,
WinstonLoggerOptions,
} from '@backstage/backend-defaults/rootLogger';
import { createConfigSecretEnumerator } from '@backstage/backend-defaults/rootConfig';
import { transports, format } from 'winston';
import { loadConfigSchema } from '@backstage/config-loader';
import { getPackages } from '@manypkg/get-packages';
import { dynamicPluginsSchemasServiceRef } from './schemas';
/**
* @public
*/
export type DynamicPluginsRootLoggerFactoryOptions = Omit<
WinstonLoggerOptions,
'meta'
>;
const dynamicPluginsRootLoggerServiceFactoryWithOptions = (
options?: DynamicPluginsRootLoggerFactoryOptions,
) =>
createServiceFactory({
service: coreServices.rootLogger,
deps: {
config: coreServices.rootConfig,
schemas: dynamicPluginsSchemasServiceRef,
},
async factory({ config, schemas }) {
const logger = WinstonLogger.create({
level: process.env.LOG_LEVEL || 'info',
format:
process.env.NODE_ENV === 'production'
? format.json()
: WinstonLogger.colorFormat(),
transports: [new transports.Console()],
...options,
meta: {
service: 'backstage',
},
});
const configSchema = await loadConfigSchema({
dependencies: (
await getPackages(process.cwd())
).packages.map(p => p.packageJson.name),
});
const secretEnumerator = await createConfigSecretEnumerator({
logger,
schema: (await schemas.addDynamicPluginsSchemas(configSchema)).schema,
});
logger.addRedactions(secretEnumerator(config));
config.subscribe?.(() => logger.addRedactions(secretEnumerator(config)));
return logger;
},
});
/**
* @public
* @deprecated Use {@link dynamicPluginsFeatureLoader} instead, which gathers all services and features required for dynamic plugins.
*/
export const dynamicPluginsRootLoggerServiceFactory = Object.assign(
dynamicPluginsRootLoggerServiceFactoryWithOptions,
dynamicPluginsRootLoggerServiceFactoryWithOptions(),
);
@@ -1,63 +0,0 @@
/*
* Copyright 2024 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 {
createServiceFactory,
coreServices,
} from '@backstage/backend-plugin-api';
import { WinstonLogger } from '@backstage/backend-defaults/rootLogger';
import { transports, format } from 'winston';
import { createConfigSecretEnumerator } from '@backstage/backend-common';
import { loadConfigSchema } from '@backstage/config-loader';
import { getPackages } from '@manypkg/get-packages';
import { dynamicPluginsSchemasServiceRef } from './schemas';
/** @public */
export const dynamicPluginsRootLoggerServiceFactory = createServiceFactory({
service: coreServices.rootLogger,
deps: {
config: coreServices.rootConfig,
schemas: dynamicPluginsSchemasServiceRef,
},
async factory({ config, schemas }) {
const logger = WinstonLogger.create({
meta: {
service: 'backstage',
},
level: process.env.LOG_LEVEL || 'info',
format:
process.env.NODE_ENV === 'production'
? format.json()
: WinstonLogger.colorFormat(),
transports: [new transports.Console()],
});
const configSchema = await loadConfigSchema({
dependencies: (
await getPackages(process.cwd())
).packages.map(p => p.packageJson.name),
});
const secretEnumerator = await createConfigSecretEnumerator({
logger,
schema: (await schemas.addDynamicPluginsSchemas(configSchema)).schema,
});
logger.addRedactions(secretEnumerator(config));
config.subscribe?.(() => logger.addRedactions(secretEnumerator(config)));
return logger;
},
});
@@ -30,6 +30,7 @@ import { LoggerService } from '@backstage/backend-plugin-api';
import { JsonObject } from '@backstage/types';
import { PluginScanner } from '../scanner/plugin-scanner';
import { ConfigSchema, loadConfigSchema } from '@backstage/config-loader';
import { dynamicPluginsFeatureLoader } from '../features';
/**
*
@@ -68,10 +69,7 @@ export interface DynamicPluginsSchemasOptions {
schemaLocator?: (pluginPackage: ScannedPluginPackage) => string;
}
/**
* @public
*/
export const dynamicPluginsSchemasServiceFactoryWithOptions = (
const dynamicPluginsSchemasServiceFactoryWithOptions = (
options?: DynamicPluginsSchemasOptions,
) =>
createServiceFactory({
@@ -143,9 +141,12 @@ export const dynamicPluginsSchemasServiceFactoryWithOptions = (
/**
* @public
* @deprecated Use {@link dynamicPluginsFeatureLoader} instead, which gathers all services and features required for dynamic plugins.
*/
export const dynamicPluginsSchemasServiceFactory =
dynamicPluginsSchemasServiceFactoryWithOptions();
export const dynamicPluginsSchemasServiceFactory = Object.assign(
dynamicPluginsSchemasServiceFactoryWithOptions,
dynamicPluginsSchemasServiceFactoryWithOptions(),
);
/** @internal */
async function gatherDynamicPluginsSchemas(