docs: update backend system building, migration, and core service docs

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2023-09-12 16:20:46 +02:00
parent 9d253ff59c
commit 75d3ad1433
5 changed files with 85 additions and 154 deletions
@@ -20,27 +20,23 @@ A minimal Backstage backend is very lightweight. It is a single package with a `
When you create a new project with `@backstage/create-app`, you'll get a backend package with a `src/index.ts` that looks something like this:
```ts
import { createBackend } from '@backstage/backend-defaults';
import { appPlugin } from '@backstage/plugin-app-backend';
import { catalogPlugin } from '@backstage/plugin-catalog-backend';
import {
scaffolderPlugin,
catalogModuleTemplateKind,
} from '@backstage/plugin-scaffolder-backend';
import { createBackend } from '@backstage/backend-defaults'; // Omitted in the examples below
const backend = createBackend();
backend.add(appPlugin());
backend.add(catalogPlugin());
backend.add(catalogModuleTemplateKind());
backend.add(scaffolderPlugin());
backend.add(import('@backstage/plugin-app-backend'));
backend.add(import('@backstage/plugin-catalog-backend'));
backend.add(import('@backstage/plugin-scaffolder-backend'));
backend.add(
import('@backstage/plugin-catalog-backend-module-scaffolder-entity-model'),
);
backend.start();
```
There will be a couple more plugins and modules in the initial setup, but the overall layout is the same.
What we're doing in this file is creating a new backend using `createBackend`, and then installing a collection of different plugins and modules that we want to be part of that backend. Plugins are standalone features, while modules augment existing plugins. Each module can only target a single plugin, and that plugin must also be present in the same backend. Finally, we start up the backend by calling the `start` method.
What we're doing in this file is creating a new backend using `createBackend`, and then installing a collection of different plugins, modules, and service that we want to be part of that backend. Plugins are standalone features, modules augment existing plugins, while services can be used to override behavior for deeper customizations. Each module can only target a single plugin, and that plugin must also be present in the same backend. Finally, we start up the backend by calling the `start` method.
## Customization
@@ -63,13 +59,13 @@ For example, let's say we want to customize the core configuration service to en
```ts
import { rootConfigServiceFactory } from '@backstage/backend-app-api';
const backend = createBackend({
services: [
rootConfigServiceFactory({
remote: { reloadIntervalSeconds: 60 },
}),
],
});
const backend = createBackend();
backend.add(
rootConfigServiceFactory({
remote: { reloadIntervalSeconds: 60 },
}),
);
```
This will make it possible to pass URLs as configuration targets, and those URLs will be polled every 60 seconds for changes.
@@ -83,22 +79,22 @@ When overriding services you are not limited to the existing implementations, yo
To override a service, you provide it in the `services` option just like above, but this time we need to use `createServiceFactory` to create our factory. For example, if you want to replace the default `LoggerService` with your own, it might look like this:
```ts
const backend = createBackend({
services: [
createServiceFactory({
service: coreServices.logger,
deps: {
rootLogger: coreServices.rootLogger,
plugin: coreServices.pluginMetadata,
config: coreServices.config,
},
factory({ rootLogger, plugin, config }) {
const labels = readCustomLogLabelsForPlugin(config, plugin); // custom logic
return rootLogger.child(labels);
},
}),
],
});
const backend = createBackend();
backend.add(
createServiceFactory({
service: coreServices.logger,
deps: {
rootLogger: coreServices.rootLogger,
plugin: coreServices.pluginMetadata,
config: coreServices.rootConfig,
},
factory({ rootLogger, plugin, config }) {
const labels = readCustomLogLabelsForPlugin(config, plugin); // custom logic
return rootLogger.child(labels);
},
}),
);
```
The `LoggerService` is responsible for creating a specialized logger instance for each plugin, while the `RootLoggerService` is the actual logging implementation. The default implementation of `LoggerService` will decorate the logger with a `plugin` label that contains the plugin ID. Here in our custom implementation we read out additional labels from the configuration and add those as well.
@@ -128,22 +124,22 @@ packages/
You can now trim down the `src/index.ts` files to only include the plugins and modules that you want to be part of that backend. For example, if you want to split out the scaffolder plugin, you might end up with something like this:
```ts
// packages/backend-a/src/index.ts, imports omitted
const backend = createBackend();
backend.add(appPlugin());
backend.add(catalogPlugin());
backend.add(catalogModuleTemplateKind());
backend.add(import('@backstage/plugin-app-backend'));
backend.add(import('@backstage/plugin-catalog-backend'));
backend.add(
import('@backstage/plugin-catalog-backend-module-scaffolder-entity-model'),
);
backend.start();
```
And `backend-b`, don't forget to clean up dependencies in `package.json` as well:
```ts
// packages/backend-b/src/index.ts, imports omitted
const backend = createBackend();
backend.add(scaffolderPlugin());
backend.add(import('@backstage/plugin-scaffolder-backend'));
backend.start();
```
@@ -145,7 +145,7 @@ import { coreServices } from '@backstage/backend-plugin-api';
const legacyPlugin = makeLegacyPlugin(
{
cache: coreServices.cache,
config: coreServices.config,
config: coreServices.rootConfig,
database: coreServices.database,
discovery: coreServices.discovery,
logger: coreServices.logger,
@@ -232,18 +232,13 @@ The app backend plugin that serves the frontend from the backend can trivially
be used in its new form.
```ts title="packages/backend/src/index.ts"
/* highlight-add-next-line */
import { appPlugin } from '@backstage/plugin-app-backend';
const backend = createBackend();
/* highlight-add-next-line */
backend.add(appPlugin({ appPackageName: 'app' }));
backend.add(import('@backstage/plugin-app-backend'));
```
This is an example of how options can be passed into some backend plugins. The
app plugin specifically needs to know the name of the package that holds the
frontend code. This is the `"name"` field in that package's `package.json`,
typically found in your `packages/app` folder. By default it's just plain "app".
If you need to override the app package name, which otherwise defaults to `"app"`,
you can do so via the `app.packageName` configuration key.
You should be able to delete the `plugins/app.ts` file at this point.
@@ -252,21 +247,18 @@ You should be able to delete the `plugins/app.ts` file at this point.
A basic installation of the catalog plugin looks as follows.
```ts title="packages/backend/src/index.ts"
/* highlight-add-start */
import { catalogPlugin } from '@backstage/plugin-catalog-backend';
import { catalogModuleTemplateKind } from '@backstage/plugin-scaffolder-backend';
/* highlight-add-end */
const backend = createBackend();
/* highlight-add-start */
backend.add(catalogPlugin());
backend.add(catalogModuleTemplateKind());
backend.add(import('@backstage/plugin-catalog-backend'));
backend.add(
import('@backstage/plugin-catalog-backend-module-scaffolder-entity-model'),
);
/* highlight-add-end */
```
Note that this also installs a module from the scaffolder, namely the one which
enables the use of the `Template` kind. In the unlikely event that you do not
use templates at all, you can remove those lines.
Note that this also installs the scaffolder module for the catalog, which
enables the use of the `Template` kind. In the event that you do not
use templates at all, you can remove that line.
If you have other customizations made to `plugins/catalog.ts`, such as adding
custom processors or entity providers, read on. Otherwise, you should be able to
@@ -305,8 +297,10 @@ const catalogModuleCustomExtensions = createBackendModule({
/* highlight-add-end */
const backend = createBackend();
backend.add(catalogPlugin());
backend.add(catalogModuleTemplateKind());
backend.add(import('@backstage/plugin-catalog-backend'));
backend.add(
import('@backstage/plugin-catalog-backend-module-scaffolder-entity-model'),
);
/* highlight-add-next-line */
backend.add(catalogModuleCustomExtensions());
```
@@ -330,12 +324,9 @@ implementations that they represent, and being exported from there.
A basic installation of the events plugin looks as follows.
```ts title="packages/backend/src/index.ts"
/* highlight-add-next-line */
import { eventsPlugin } from '@backstage/plugin-events-backend';
const backend = createBackend();
/* highlight-add-next-line */
backend.add(eventsPlugin());
backend.add(import('@backstage/plugin-events-backend'));
```
If you have other customizations made to `plugins/events.ts`, such as adding
@@ -374,7 +365,7 @@ const eventsModuleCustomExtensions = createBackendModule({
/* highlight-add-end */
const backend = createBackend();
backend.add(eventsPlugin());
backend.add(import('@backstage/plugin-events-backend'));
/* highlight-add-next-line */
backend.add(eventsModuleCustomExtensions());
```
@@ -398,12 +389,9 @@ implementations that they represent, and being exported from there.
A basic installation of the scaffolder plugin looks as follows.
```ts title="packages/backend/src/index.ts"
/* highlight-add-next-line */
import { scaffolderPlugin } from '@backstage/plugin-scaffolder-backend';
const backend = createBackend();
/* highlight-add-next-line */
backend.add(scaffolderPlugin());
backend.add(import('@backstage/plugin-scaffolder-backend'));
```
If you have other customizations made to `plugins/scaffolder.ts`, such as adding
@@ -442,7 +430,7 @@ const scaffolderModuleCustomExtensions = createBackendModule({
/* highlight-add-end */
const backend = createBackend();
backend.add(scaffolderPlugin());
backend.add(import('@backstage/plugin-scaffolder-backend'));
/* highlight-add-next-line */
backend.add(scaffolderModuleCustomExtensions());
```
@@ -27,6 +27,7 @@ To create a Backend plugin, run `yarn new`, select `backend-plugin`, and fill ou
A basic backend plugin might look as follows:
```ts
// src/plugin.ts
import {
createBackendPlugin,
coreServices,
@@ -55,6 +56,9 @@ export const examplePlugin = createBackendPlugin({
});
},
});
// src/index.ts
export { examplePlugin as default } from './plugin';
```
When you depend on `plugin` scoped services, you'll receive an instance of them
@@ -91,6 +95,7 @@ The following is an example of how to create a module that adds a new processor
using the `catalogProcessingExtensionPoint`:
```ts
// src/module.ts
import { createBackendModule } from '@backstage/backend-plugin-api';
import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node';
import { MyCustomProcessor } from './MyCustomProcessor';
@@ -110,6 +115,9 @@ export const catalogModuleExampleCustomProcessor = createBackendModule({
});
},
});
// src/index.ts
export { catalogModuleExampleCustomProcessor as default } from './module';
```
See [the article on naming patterns](../architecture/07-naming-patterns.md) for
@@ -122,10 +130,11 @@ 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.
Each module package should only contain a single module, but this module may
extend multiple extension points. A module may also use configuration to
conditionally enable or disable certain extensions. This pattern should only be
used for extensions that are related to each other, otherwise it is best to
create a separate module package with its own module.
### HTTP Handlers
@@ -187,34 +196,25 @@ export const examplesExtensionPoint =
id: 'example.examples',
});
// This is the implementation of the extension point, which is internal to your plugin.
class ExamplesExtension implements ExamplesExtensionPoint {
#examples: Example[] = [];
addExample(example: Example): void {
this.#examples.push(example);
}
// Note that this method is internal to this implementation
getRegisteredExamples() {
return this.#examples;
}
}
// The following shows how your plugin would register the extension point
// and use the features that other modules have registered.
export const examplePlugin = createBackendPlugin({
pluginId: 'example',
register(env) {
const examplesExtensions = new ExamplesExtension();
env.registerExtensionPoint(examplesExtensionPoint, examplesExtensions);
// We can share data between the extension point implementation and our init method.
const examples = new Array<Example>();
// This registers the implementation of the extension point, which is internal to your plugin.
env.registerExtensionPoint(examplesExtensionPoint, {
addExample(example) {
examples.push(example);
},
});
env.registerInit({
deps: { logger: coreServices.logger },
async init({ logger }) {
// We can access `examplesExtension` directly, giving us access to the internal interface.
const examples = examplesExtension.getRegisteredExamples();
// We can access `examples` directly
logger.info(`The following examples have been registered: ${examples}`);
},
});
@@ -224,12 +224,10 @@ export const examplePlugin = createBackendPlugin({
This is a very common type of extension point, one where modules are given the opportunity to register features to be used by the plugin. In this case modules are able to register examples that are then used by our examples plugin.
Note that the public extension point interface only needs to expose the `addExample` method, while the `getRegisteredExamples()` method is kept internal to the plugin.
### 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`
internal behavior. You do this by adding a dependency on `coreServices.rootConfig`
and reading from that. This pattern is a good fit especially for customization
that needs to be different across environments.
@@ -240,7 +238,7 @@ export const examplePlugin = createBackendPlugin({
pluginId: 'example',
register(env) {
env.registerInit({
deps: { config: coreServices.config },
deps: { config: coreServices.rootConfig },
async init({ config }) {
// Here you can read from the current config as you see fit, e.g.:
const value = config.getOptionalString('example.value');
@@ -254,54 +252,3 @@ 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) => ({
pluginId: '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.
@@ -51,7 +51,7 @@ export const kubernetesPlugin = createBackendPlugin({
env.registerInit({
deps: {
logger: coreServices.logger,
config: coreServices.config,
config: coreServices.rootConfig,
catalogApi: catalogServiceRef,
discovery: coreServices.discovery,
// The http router service is used to register the router created by the KubernetesBuilder.
@@ -142,7 +142,7 @@ const backend = createBackend({
});
```
## Config
## Root Config
This service allows you to read configuration values out of your `app-config` YAML files.
@@ -162,7 +162,7 @@ createBackendPlugin({
env.registerInit({
deps: {
log: coreServices.logger,
config: coreServices.config,
config: coreServices.rootConfig,
},
async init({ log, config }) {
const baseUrl = config.getString('backend.baseUrl');
@@ -248,7 +248,7 @@ const backend = createBackend({
createServiceFactory({
service: coreServices.rootLogger,
deps: {
config: coreServices.config,
config: coreServices.rootConfig,
},
async factory({ config }) {
const logger = WinstonLogger.create({