From d18d4942f9159e9b57f21e1fbd007e7a416f03fd Mon Sep 17 00:00:00 2001 From: David Festal Date: Wed, 25 Sep 2024 15:40:10 +0200 Subject: [PATCH] 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 --- .changeset/fluffy-dogs-mate.md | 8 ++ .../backend-dynamic-feature-service/README.md | 3 +- .../src/features/features.ts | 111 ++++++++++++++++++ .../src/features/index.ts | 17 +++ .../src/index.ts | 1 + .../src/manager/plugin-manager.ts | 82 ++++--------- .../src/scanner/plugin-scanner.ts | 4 +- .../{appBackendModule.ts => frontend.ts} | 5 +- .../src/schemas/index.ts | 8 +- .../src/schemas/rootLogger.ts | 86 ++++++++++++++ .../src/schemas/rootLoggerServiceFactory.ts | 63 ---------- .../src/schemas/schemas.ts | 13 +- 12 files changed, 263 insertions(+), 138 deletions(-) create mode 100644 .changeset/fluffy-dogs-mate.md create mode 100644 packages/backend-dynamic-feature-service/src/features/features.ts create mode 100644 packages/backend-dynamic-feature-service/src/features/index.ts rename packages/backend-dynamic-feature-service/src/schemas/{appBackendModule.ts => frontend.ts} (92%) create mode 100644 packages/backend-dynamic-feature-service/src/schemas/rootLogger.ts delete mode 100644 packages/backend-dynamic-feature-service/src/schemas/rootLoggerServiceFactory.ts diff --git a/.changeset/fluffy-dogs-mate.md b/.changeset/fluffy-dogs-mate.md new file mode 100644 index 0000000000..cb7a78d0fb --- /dev/null +++ b/.changeset/fluffy-dogs-mate.md @@ -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. diff --git a/packages/backend-dynamic-feature-service/README.md b/packages/backend-dynamic-feature-service/README.md index 33033cf884..f4ca11bb1f 100644 --- a/packages/backend-dynamic-feature-service/README.md +++ b/packages/backend-dynamic-feature-service/README.md @@ -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 + ``` diff --git a/packages/backend-dynamic-feature-service/src/features/features.ts b/packages/backend-dynamic-feature-service/src/features/features.ts new file mode 100644 index 0000000000..4788f1da75 --- /dev/null +++ b/packages/backend-dynamic-feature-service/src/features/features.ts @@ -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(), +); diff --git a/packages/backend-dynamic-feature-service/src/features/index.ts b/packages/backend-dynamic-feature-service/src/features/index.ts new file mode 100644 index 0000000000..c877359d42 --- /dev/null +++ b/packages/backend-dynamic-feature-service/src/features/index.ts @@ -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'; diff --git a/packages/backend-dynamic-feature-service/src/index.ts b/packages/backend-dynamic-feature-service/src/index.ts index 3768146124..abdbff677b 100644 --- a/packages/backend-dynamic-feature-service/src/index.ts +++ b/packages/backend-dynamic-feature-service/src/index.ts @@ -18,3 +18,4 @@ export * from './loader'; export * from './scanner'; export * from './manager'; export * from './schemas'; +export * from './features'; diff --git a/packages/backend-dynamic-feature-service/src/manager/plugin-manager.ts b/packages/backend-dynamic-feature-service/src/manager/plugin-manager.ts index 5b88c0619b..87ff4334ed 100644 --- a/packages/backend-dynamic-feature-service/src/manager/plugin-manager.ts +++ b/packages/backend-dynamic-feature-service/src/manager/plugin-manager.ts @@ -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( { @@ -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 ( diff --git a/packages/backend-dynamic-feature-service/src/scanner/plugin-scanner.ts b/packages/backend-dynamic-feature-service/src/scanner/plugin-scanner.ts index 9229f5d78a..80ae2ed876 100644 --- a/packages/backend-dynamic-feature-service/src/scanner/plugin-scanner.ts +++ b/packages/backend-dynamic-feature-service/src/scanner/plugin-scanner.ts @@ -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; diff --git a/packages/backend-dynamic-feature-service/src/schemas/appBackendModule.ts b/packages/backend-dynamic-feature-service/src/schemas/frontend.ts similarity index 92% rename from packages/backend-dynamic-feature-service/src/schemas/appBackendModule.ts rename to packages/backend-dynamic-feature-service/src/schemas/frontend.ts index 6e54379e90..306ecc9255 100644 --- a/packages/backend-dynamic-feature-service/src/schemas/appBackendModule.ts +++ b/packages/backend-dynamic-feature-service/src/schemas/frontend.ts @@ -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', diff --git a/packages/backend-dynamic-feature-service/src/schemas/index.ts b/packages/backend-dynamic-feature-service/src/schemas/index.ts index 14c6734b6b..d67e77232b 100644 --- a/packages/backend-dynamic-feature-service/src/schemas/index.ts +++ b/packages/backend-dynamic-feature-service/src/schemas/index.ts @@ -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'; diff --git a/packages/backend-dynamic-feature-service/src/schemas/rootLogger.ts b/packages/backend-dynamic-feature-service/src/schemas/rootLogger.ts new file mode 100644 index 0000000000..ac4bf3fa5e --- /dev/null +++ b/packages/backend-dynamic-feature-service/src/schemas/rootLogger.ts @@ -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(), +); diff --git a/packages/backend-dynamic-feature-service/src/schemas/rootLoggerServiceFactory.ts b/packages/backend-dynamic-feature-service/src/schemas/rootLoggerServiceFactory.ts deleted file mode 100644 index 00d5c85e6e..0000000000 --- a/packages/backend-dynamic-feature-service/src/schemas/rootLoggerServiceFactory.ts +++ /dev/null @@ -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; - }, -}); diff --git a/packages/backend-dynamic-feature-service/src/schemas/schemas.ts b/packages/backend-dynamic-feature-service/src/schemas/schemas.ts index 1cfb42a093..52fb5eab6d 100644 --- a/packages/backend-dynamic-feature-service/src/schemas/schemas.ts +++ b/packages/backend-dynamic-feature-service/src/schemas/schemas.ts @@ -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(