From 5153d4ca27b15c1bdf20e06114e6494dd08d4991 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Tue, 24 Jan 2023 14:51:28 +0100 Subject: [PATCH 1/3] Building Backend Plugins and Modules MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .../building-plugins-and-modules/01-index.md | 233 +++++++++++++++++- .../02-testing.md | 6 +- 2 files changed, 233 insertions(+), 6 deletions(-) diff --git a/docs/backend-system/building-plugins-and-modules/01-index.md b/docs/backend-system/building-plugins-and-modules/01-index.md index fff82286ad..2e43dd4473 100644 --- a/docs/backend-system/building-plugins-and-modules/01-index.md +++ b/docs/backend-system/building-plugins-and-modules/01-index.md @@ -9,4 +9,235 @@ description: Building backend plugins and modules using the new backend system > NOTE: If you have an existing backend and/or backend plugins that are not yet > using the new backend system, see [migrating](./08-migrating.md). -# Overview +## Overview + +Backend [plugins](../architecture/04-plugins.md) and +[modules](../architecture/06-modules.md), sometimes collectively referred to as +backend _features_, are the building blocks that adopters add to their +[backends](../architecture/02-backends.md). + +## Plugins + +A basic backend plugin might look as follows: + +```ts +import { + createBackendPlugin, + coreServices, +} from '@backstage/backend-plugin-api'; +import { createExampleRouter } from './router'; + +export const examplePlugin = createBackendPlugin({ + id: 'example', + register(env) { + env.registerInit({ + deps: { + // Declare dependencies to services that you want to consume + logger: coreServices.logger, + httpRouter: coreServices.httpRouter, + }, + async init({ + // Requested service instances get injected as per above + logger, + httpRouter, + }) { + // Perform your initialization and access the services as needed + const router = createExampleRouter(logger); + logger.info('Hello from example plugin'); + httpRouter.use(example.router()); + }, + }); + }, +}); +``` + +When you depend on `plugin` scoped services, you'll receive an instance of them +that's specific to your plugin. In the example above, the logger might tag +messages with your plugin ID, and the HTTP router might prefix API routes with +your plugin ID, depending on the implementation used. + +See [the article on naming patterns](../architecture/07-naming-patterns.md) for +details on how to best choose names/IDs for plugins and related backend system +items. + +## Modules + +Backend modules are used to extend [plugins](./04-plugins.md) with additional +features or change existing behavior. They must always be installed in the same +backend instance as the plugin that they extend, and may only extend a single +plugin. Modules interact with their target plugin using the [extension +points](./05-extension-points.md) registered by the plugin, while also being +able to depend on the [services](./03-services.md) of that plugin. That last +point is worth reiterating: injected `plugin` scoped services will be the exact +same ones as the target plugin will receive later, i.e. they will be scoped +using the target `pluginId` of the module. + +A module depends on the extension points exported by the target plugin's library +package, for example `@backstage/plugin-catalog-node`, and does not directly +declare a dependency on the plugin package itself. This is to avoid a direct +dependency and potentially cause duplicate installations of the plugin package, +while duplicate installations of library packages should always be supported. + +The following is an example of how to create a module that adds a new processor +using the `catalogProcessingExtensionPoint`: + +```ts +import { createBackendModule } from '@backstage/backend-plugin-api'; +import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node'; +import { MyCustomProcessor } from './MyCustomProcessor'; + +export const catalogModuleExampleCustomProcessor = createBackendModule({ + moduleId: 'exampleCustomProcessor', + pluginId: 'catalog', + register(env) { + env.registerInit({ + deps: { + catalog: catalogProcessingExtensionPoint, + logger: coreServices.logger, + }, + async init({ catalog }) { + catalog.addProcessor(new MyCustomProcessor(logger)); + }, + }); + }, +}); +``` + +See [the article on naming patterns](../architecture/07-naming-patterns.md) for +details on how to best choose names/IDs for modules and related backend system +items. + +Notice that we're placing the extension point we want to interact with in the +`deps` option, while also depending on the logger service at the same time. When +initializing modules we can depend on both extension points and services +interchangeably. You can also depend on multiple extension points at once, in +case the implementation of the module requires it. + +It is typically best to keep modules slim and to each only add a single new +feature. It is often the case that it is better to create two separate modules +rather than one that provides both features. The one limitation here is that +modules can not interact with each other and need to be self contained. + +### HTTP Handlers + +Since modules have access to the same services as the plugin they extend, they +are also able to register their own HTTP handlers. For more information about +the HTTP service, see [core services](../core-services/01-index.md). When +registering HTTP handlers, it is important to try to avoid any future conflict +with the plugin itself, or other modules. A recommended naming pattern is to +register the handlers under the `/modules/` path, where `` +is the kebab-case ID of the module, for example +`/modules/example-custom-processor/v1/validators`. In a standard backend setup +the full path would then be +`/api/catalog/modules/example-custom-processor/v1/validators`. + +### Database Access + +The same applies for modules that perform their own migrations and interact with +the database. They will run on the same logical database instance as the target +plugin, so care must be taken to choose table names that do not risk colliding +with those of the plugin. A recommended naming pattern is `__`, for example the `@backstage/backend-tasks` package creates tables named +`backstage_backend_tasks__
`. If you use the default Knex migration +facilities, you will also want to make sure that it uses a similarly prefixed +lock file for its internal bookkeeping needs, so it does not collide with the +main lock files used by the plugin itself. You can do this as follows: + +```ts +await knex.migrate.latest({ + directory: migrationsDir, + tableName: 'backstage_backend_tasks__knex_migrations', +}); +``` + +## Customization + +There are several ways of configuring and customizing plugins and modules. + +### Extension Points + +Whenever you want to allow modules to configure your plugin dynamically, for +example in the way that the catalog backend lets catalog modules inject +additional entity providers, you can use the extension points mechanism. This is +described in detail with code examples in [the extension points architecture +article](../architecture/05-extension-points.md). + +### Configuration + +Your plugin or module can leverage the app configuration to configure its own +internal behavior. You do this by adding a dependency on `coreServices.config` +and reading from that. This pattern is a good fit especially for customization +that needs to be different across environments. + +```ts +import { coreServices } from '@backstage/backend-plugin-api'; + +export const examplePlugin = createBackendPlugin({ + id: 'example', + register(env) { + env.registerInit({ + deps: { config: coreServices.config }, + async init({ config }) { + // Here you can read from the current config as you see fit, e.g.: + const value = config.getOptionalString('example.value'); + }, + }); + }, +}); +``` + +Before adding custom configuration options, make sure to read [the configuration +docs](../../conf/index.md), in particular the section on [defining configuration +for your own plugins](../../conf/defining.md) which explains how to establish a +configuration schema for your specific plugin. + +### Options + +You'll have noted that the return values from `createBackendPlugin` and +`createBackendModule` are actually factory functions. These can be made to +accept options that shall be passed in at initialization time. + +This pattern can be a good fit for fairly simple, static configuration values. + +```ts +export interface ExampleOptions { + silent?: boolean; +} + +export const examplePlugin = createBackendPlugin( + (options?: ExampleOptions) => ({ + id: 'example', + register(env) { + env.registerInit({ + deps: { + // Omitted dependencies but they remain the same as above + }, + async init( + { + /* ... */ + }, + ) { + // Here you can access the given options and act accordingly, e.g.: + if (!options?.silent) { + // ... + } + }, + }); + }, + }), +); +``` + +The return type from `createBackendPlugin` and `createBackendModule` will mimic +this, resulting in a factory function that accepts an optional options object. +You can also make it required to pass in options, by removing the optionality +(the question mark on the options) above. + +```ts +backend.add(examplePlugin({ silent: true })); +``` + +Use this pattern sparingly. There is a big convenience benefit in allowing +people to easily install backend plugins without having to always pass in a +large number of options, and these options cannot easily be made dynamic based +on the environment etc. diff --git a/docs/backend-system/building-plugins-and-modules/02-testing.md b/docs/backend-system/building-plugins-and-modules/02-testing.md index 3b19a7cfc8..20bef6fec4 100644 --- a/docs/backend-system/building-plugins-and-modules/02-testing.md +++ b/docs/backend-system/building-plugins-and-modules/02-testing.md @@ -107,11 +107,7 @@ The base setup for such a test could look as follows: ```ts // MyDatabaseClass.test.ts import { TestDatabaseId, TestDatabases } from '@backstage/backend-test-utils'; -import { - MyDatabaseClass, - applyDatabaseMigrations, - type FooTableRow, -} from './MyDatabaseClass'; +import { MyDatabaseClass, type FooTableRow } from './MyDatabaseClass'; describe('MyDatabaseClass', () => { // Change this to the set of constants that you actually actively intend to From dc467f5ba4d96491ca17f7fd0293794f0563c703 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Tue, 24 Jan 2023 15:10:14 +0100 Subject: [PATCH 2/3] correct links MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .../building-plugins-and-modules/01-index.md | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/docs/backend-system/building-plugins-and-modules/01-index.md b/docs/backend-system/building-plugins-and-modules/01-index.md index 2e43dd4473..b8eb1ed2a2 100644 --- a/docs/backend-system/building-plugins-and-modules/01-index.md +++ b/docs/backend-system/building-plugins-and-modules/01-index.md @@ -62,13 +62,14 @@ items. ## Modules -Backend modules are used to extend [plugins](./04-plugins.md) with additional -features or change existing behavior. They must always be installed in the same -backend instance as the plugin that they extend, and may only extend a single -plugin. Modules interact with their target plugin using the [extension +Backend modules are used to extend [plugins](../architecture/04-plugins.md) with +additional features or change existing behavior. They must always be installed +in the same backend instance as the plugin that they extend, and may only extend +a single plugin. Modules interact with their target plugin using the [extension points](./05-extension-points.md) registered by the plugin, while also being -able to depend on the [services](./03-services.md) of that plugin. That last -point is worth reiterating: injected `plugin` scoped services will be the exact +able to depend on the [services](../architecture/03-services.md) of that plugin. +That last point is worth reiterating: injected `plugin` scoped services will be +the exact same ones as the target plugin will receive later, i.e. they will be scoped using the target `pluginId` of the module. From 08ef7995c44ba1dec08f4aafaa8c4ab6d4b77349 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Tue, 24 Jan 2023 15:51:53 +0100 Subject: [PATCH 3/3] review comments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .../building-plugins-and-modules/01-index.md | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/docs/backend-system/building-plugins-and-modules/01-index.md b/docs/backend-system/building-plugins-and-modules/01-index.md index b8eb1ed2a2..05908e59cb 100644 --- a/docs/backend-system/building-plugins-and-modules/01-index.md +++ b/docs/backend-system/building-plugins-and-modules/01-index.md @@ -42,9 +42,9 @@ export const examplePlugin = createBackendPlugin({ httpRouter, }) { // Perform your initialization and access the services as needed - const router = createExampleRouter(logger); + const example = createExampleRouter(logger); logger.info('Hello from example plugin'); - httpRouter.use(example.router()); + httpRouter.use(example); }, }); }, @@ -137,12 +137,13 @@ the full path would then be The same applies for modules that perform their own migrations and interact with the database. They will run on the same logical database instance as the target plugin, so care must be taken to choose table names that do not risk colliding -with those of the plugin. A recommended naming pattern is `__
`, for example the `@backstage/backend-tasks` package creates tables named -`backstage_backend_tasks__
`. If you use the default Knex migration -facilities, you will also want to make sure that it uses a similarly prefixed -lock file for its internal bookkeeping needs, so it does not collide with the -main lock files used by the plugin itself. You can do this as follows: +with those of the plugin. A recommended naming pattern is `__
`, for example the `@backstage/backend-tasks` package creates +tables named `backstage_backend_tasks__
`. If you use the default [`Knex` +migration facilities](https://knexjs.org/guide/migrations.html), you will also +want to make sure that it uses similarly prefixed migration state tables for its +internal bookkeeping needs, so they do not collide with the main ones used by +the plugin itself. You can do this as follows: ```ts await knex.migrate.latest({