Merge pull request #19902 from backstage/rugvip/backend-docs
docs: update backend-system docs to match implementation
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
<svg host="65bd71144e" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="642px" height="327px" viewBox="-0.5 -0.5 642 327" content="<mxfile scale="1.5" border="20"><diagram id="gKBvn7eBFHudv9VMIRD-" name="Page-1">3VnbTuMwEP2aPFLl1tsjlMLuw0pI3dXCo0ncxMK1K8dtU75+7dhJnBhKFMJlGySUGV/nnDNjE5xgsclvGdimv2gMseO7ce4E147vz1xX/JaOo3JMZ9qRMBQrl1c7VugZamfZbYdimDU6ckoxR9umM6KEwIg3fIAxemh2W1PcXHULEmg5VhHAtvcvinmqvEHo1v4fECVpuXIZ3iOInhJGd0Qv5/jBunhU8waUU+n+WQpiejBcwdIJFoxSrt42+QJiiWyJmhp380prtW0GCe8ywFcD9gDvdORXIgJI4kzvjx9LSIqooBznOsHVIUUcrrYgkq0HoQHhS/kGC8sTr2uE8YJiyoqxwXIif4Q/44w+QaNlUjxyBCXc8KtH+O2IdJB7yDjMDZeO8BbSDeTsKLro1otAo63FeFGif6ipDUvFpgatJd1Aqymppq4hFS8a1ZcRDiyEV5DtUQTPCWGvhfDUBnj2QfjOLXzvGN2L6mHBC2OR4toklMAmnjBH/N54f5A0jMbaus41K4VxLA0SX8piI8xHTKOn3ykiyn2DcDUxKetbWNADGDfsJueSIt04P8VMRncs0mGFujwClkDda6xcMuCT7DGIAUf7Zs17iQw99I4isZG6C12vM7Fmm61qhU4EenYN+pP1Zc8dTccmgW/QJzZ5bxoG6dKshxXWh9EuEw2Jw+cSo0TMdL1BcYyLCClDz0IToMr6YQQSdBSITm935Lr+TI35FpoJ7aTHuwSRc6qp7VOrqrGfUFTHFr7LnEOSIUqEu6D0nKBuH1+fCXV54zWwXgDZo1/9+99Or8knnF6dqbDvau85ifweJ5H3xSfRQBR2PV8Gp3BiMSj+NN3hs7ps+/Ovq1bVbmuAfxIhqf4Fa+TO50aeeCM37Hhpq7PjwWz79pc2Gc4dZEiAD5merFOi+XaihUMnWudbW90nb8qrVGVbbioSPejEPF57Irc1kYrfmqjHNfKFzx891XwWolQBtLLRP52NA2p58n20HLQuhH217L+REwNK2b679PwOckZSNi9g3kkdxyBLi5W9dwh48FvPOwTcKqKBO5rOjWc87ifo9rzDKVqY9Wdv1b3+z0Kw/Ac=</diagram></mxfile>" style="background-color: rgb(255, 255, 255);">
|
||||
<svg host="65bd71144e" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="642px" height="327px" viewBox="-0.5 -0.5 642 327" content="<mxfile scale="1.5" border="20"><diagram id="gKBvn7eBFHudv9VMIRD-" name="Page-1">3Vldb9owFP01PBblCyiPHaXdHiZVYtPaRzcxiVVjI8dA6K+fHTuJEzOahZQiUqmKj79y7zn3+iYM/Nkqe2RgnfykEcQDz4mygX8/8Lxg6on/EtgrYORNFBAzFCnIrYAFeocadDS6QRFMawM5pZijdR0MKSEw5DUMMEZ39WFLiuu7rkEMLWARAmyjf1DEE4X6gVPh3yGKk2LnW93xCsK3mNEN0dsNPH+ZX6p7BYql9Pg0ARHdGZA/H/gzRilXd6tsBrH0bOE1Ne/hH73lYzNIeJsJmqYtwBtt+TdhASRRqp+P7wuX5FZBOc8Z+N92CeJwsQah7N0JDQgs4SssWq64XSKMZxRTls/152P5J/CUM/oGjZ5xfskZlHADV5fAbYu0kVvIOMwMSFv4COkKcrYXQ3Tvja+9rcV4U3h/V1EbOBpLDFoLuoFWU1wuXblU3GivHvawb3l4AdkWhfCaPOw2PDyxHXz7Sf6dWv59YnQrsoflXhiJENdNQgms+xNmiD8b9y+ShuFIt+4zzUre2BcNEt3JZCOar5iGb78SRBT8gHC5MCnyW5DTAxg32nXOJUW6c3qMmZRuWKjNCnR6BCyGetRIQdLgo+wxiAFH23rOO0SGnvpEkXiQaghdLlOxZ5OtcodWBLp2DvqddmXPGU5GJoEf0Cce8tlsGKTLZjUtb30a7TLQkDh87jCKxUr3KxRFOLeQMvQuNAHKqO9HIH5LgejwdoaO492qORehmcAOeryJEbmmnNo8tcoce4akOrL8O884JCmiRMA5pdfk6ubxdU5XFxVvbweYY+a/Kh1+zQlWpli3nmInH+XYVpltfIajrzWPdqF3yjHmdTjGGj4++zHW5XA6QGHbw6l3CscWg+K9doOvqlL3pl+X6sqnrRz8gwhJyUEdw8SZTo04cYdO0LLiq6Ljxey7+IpPmvMEGRLOh+x/cqVnB1rQd6C1LvmqMVldXoUqm3JTluhJR9Zxmws5jYWU/dZCHWrQA99OOqr5KkSpDGhEo3c8GnvU8vhytOw3qsmuWvY+iIkepWzXLh1r0CuSslmAuUd1HIE0yXd2TxBw71XPCQJuJFFf1OtT4xqNugm6ue5nKtr+QjA7oc6ovVJ5l/lK5bV7pWoIrPM7Vn/KLNThNNQxkZ+dqsvtJjr19cpapt0mnQUpmtWPOGp49TuZP/8L</diagram></mxfile>" style="background-color: rgb(255, 255, 255);">
|
||||
<defs/>
|
||||
<g>
|
||||
<rect x="19.5" y="19.5" width="600" height="60" fill="#e6e6e6" stroke="#666666" stroke-width="1.5" pointer-events="all"/>
|
||||
@@ -105,21 +105,21 @@
|
||||
</text>
|
||||
</switch>
|
||||
</g>
|
||||
<path d="M 499.5 274.5 L 388.68 274.5" fill="none" stroke="rgb(0, 0, 0)" stroke-width="1.5" stroke-miterlimit="10" pointer-events="stroke"/>
|
||||
<path d="M 381.18 274.5 L 388.68 272 L 388.68 277 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-width="1.5" stroke-miterlimit="10" pointer-events="all"/>
|
||||
<path d="M 499.5 289.5 L 388.68 289.5" fill="none" stroke="rgb(0, 0, 0)" stroke-width="1.5" stroke-miterlimit="10" pointer-events="stroke"/>
|
||||
<path d="M 381.18 289.5 L 388.68 287 L 388.68 292 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-width="1.5" stroke-miterlimit="10" pointer-events="all"/>
|
||||
<g transform="translate(-0.5 -0.5)scale(1.5)">
|
||||
<switch>
|
||||
<foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;">
|
||||
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 183px; margin-left: 293px;">
|
||||
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 193px; margin-left: 293px;">
|
||||
<div data-drawio-colors="color: rgb(0, 0, 0); background-color: rgb(255, 255, 255); " style="box-sizing: border-box; font-size: 0px; text-align: center;">
|
||||
<div style="display: inline-block; font-size: 9px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; background-color: rgb(255, 255, 255); white-space: nowrap;">
|
||||
Call
|
||||
Provide
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</foreignObject>
|
||||
<text x="293" y="186" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="9px" text-anchor="middle">
|
||||
Call
|
||||
<text x="293" y="196" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="9px" text-anchor="middle">
|
||||
Provide
|
||||
</text>
|
||||
</switch>
|
||||
</g>
|
||||
@@ -171,7 +171,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</foreignObject>
|
||||
<text x="53" y="112" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="9px" text-anchor="middle">
|
||||
<text x="53" y="111" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="9px" text-anchor="middle">
|
||||
Install
|
||||
</text>
|
||||
</switch>
|
||||
@@ -181,7 +181,7 @@
|
||||
<g transform="translate(-0.5 -0.5)scale(1.5)">
|
||||
<switch>
|
||||
<foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;">
|
||||
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 108px; margin-left: 373px;">
|
||||
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 109px; margin-left: 373px;">
|
||||
<div data-drawio-colors="color: rgb(0, 0, 0); background-color: rgb(255, 255, 255); " style="box-sizing: border-box; font-size: 0px; text-align: center;">
|
||||
<div style="display: inline-block; font-size: 9px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; background-color: rgb(255, 255, 255); white-space: nowrap;">
|
||||
Install
|
||||
@@ -212,6 +212,24 @@
|
||||
</text>
|
||||
</switch>
|
||||
</g>
|
||||
<path d="M 499.5 259.5 L 388.68 259.5" fill="none" stroke="rgb(0, 0, 0)" stroke-width="1.5" stroke-miterlimit="10" pointer-events="stroke"/>
|
||||
<path d="M 381.18 259.5 L 388.68 257 L 388.68 262 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-width="1.5" stroke-miterlimit="10" pointer-events="all"/>
|
||||
<g transform="translate(-0.5 -0.5)scale(1.5)">
|
||||
<switch>
|
||||
<foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;">
|
||||
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 173px; margin-left: 293px;">
|
||||
<div data-drawio-colors="color: rgb(0, 0, 0); background-color: rgb(255, 255, 255); " style="box-sizing: border-box; font-size: 0px; text-align: center;">
|
||||
<div style="display: inline-block; font-size: 9px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; background-color: rgb(255, 255, 255); white-space: nowrap;">
|
||||
Call
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</foreignObject>
|
||||
<text x="293" y="176" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="9px" text-anchor="middle">
|
||||
Call
|
||||
</text>
|
||||
</switch>
|
||||
</g>
|
||||
</g>
|
||||
<switch>
|
||||
<g requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"/>
|
||||
|
||||
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 21 KiB |
@@ -43,15 +43,15 @@ Services are also a customization point for individual backend installations. Yo
|
||||
|
||||
Many plugins have ways in which you can extend them, for example entity providers for the Catalog, or custom actions for the Scaffolder. These extension patterns are now encoded into Extension Points.
|
||||
|
||||
Extension Points look a little bit like services, since you depended on them just like you would a service. A key difference is that extension points are registered and provided by plugins themselves, based on what customizations each individual plugin wants to expose.
|
||||
Extension Points look a little bit like services, since you depended on them just like you would a service. A key difference is that extension points are registered and provided by plugins or modules themselves, based on what customizations each of them want to expose.
|
||||
|
||||
Extension Points are also exported separately from the plugin instance itself, and a single plugin can also expose multiple different extension points at once. This makes it easier to evolve and deprecate individual Extension Points over time, rather than dealing with a single large API surface.
|
||||
Extension Points are exported separately from the plugin or module instance itself, and it is possible to expose multiple different extension points at once. This makes it easier to evolve and deprecate individual Extension Points over time, rather than dealing with a single large API surface.
|
||||
|
||||
### Modules
|
||||
|
||||
Modules use the plugin Extension Points to add new features for plugins. They might for example add an individual Catalog Entity Provider, or one or more Scaffolder Actions. Modules are basically plugins for plugins.
|
||||
Modules use Extension Points to add new features to other plugins or modules. They might for example add an individual Catalog Entity Provider, or one or more Scaffolder Actions.
|
||||
|
||||
Each module may only extend a single plugin, and the module must be deployed together with that plugin in the same backend instance. Modules may however only communicate with their plugin through its registered extension points.
|
||||
Each module may only use Extension Points that belong to a single plugin, and the module must be deployed together with that plugin in the same backend instance. Modules may only communicate with their plugin or other modules through the registered extension points.
|
||||
|
||||
Just like plugins, modules also have access to services and can depend on their own service implementations. They will however share services with the plugin that they extend - there are no module-specific service implementations.
|
||||
|
||||
|
||||
@@ -17,24 +17,27 @@ Below is a simple example of a backend that installs only the catalog plugin and
|
||||
|
||||
```ts
|
||||
import { createBackend } from '@backstage/backend-defaults';
|
||||
import { catalogPlugin } from '@backstage/plugin-catalog-backend';
|
||||
import scaffolderPlugin from '@backstage/plugin-scaffolder-backend';
|
||||
|
||||
// Create your backend instance
|
||||
const backend = createBackend();
|
||||
|
||||
// Install all desired features
|
||||
backend.add(catalogPlugin());
|
||||
// Install desired features
|
||||
backend.add(import('@backstage/plugin-catalog-backend'));
|
||||
|
||||
// Features can also be installed using an explicit reference
|
||||
backend.add(scaffolderPlugin());
|
||||
|
||||
// Start up the backend
|
||||
await backend.start();
|
||||
backend.start();
|
||||
```
|
||||
|
||||
`createBackend` is responsible for creating your backend instance, and wiring up all the services that you have provided. It deals with creating default implementations of all the [core services](../core-services/01-index.md) that are used by the plugins, and also provides a way to override the default implementations with your own. You can read more about creating services and overriding them in the [building backends docs](../building-backends/01-index.md).
|
||||
We call `createBackend` to create a new backend instance, which is responsible for wiring together all of the features that we provide to the app. It also provides default implementations of all [core services](../core-services/01-index.md) for use in plugins. No real work is done at the point of creating the backend though, it's all deferred to the `backend.start()` call.
|
||||
|
||||
The backend instance has the ability to add features to the backend which are done using the `.add` method. Features are either plugins or modules, and you can read more about them in the [building plugins and modules docs](../building-plugins-and-modules/01-index.md). By default, a backend instance has no default features, and the services are responsible for wiring everything together.
|
||||
To add any feature to a backend instance you use the `.add(...)` method. Features are either plugins, modules, or service factories. You can read more about building plugins and modules in the [building plugins and modules docs](../building-plugins-and-modules/01-index.md), as well as how to install services factories in the [building backends docs](../building-backends/01-index.md).
|
||||
|
||||
At a high level, when you call `createBackend`, it will create a new backend instance, which has a registry of all the services that are currently registered, and by adding features to the backend instance and calling the `.start()` method it will ensure that all the dependencies are wired up correctly and the `registerInit` methods are called in the correct order.
|
||||
Once you have added all desired features we call the `.start()` method. This causes the backend to start up and initialize all features. When starting up the backend will validate all features to make sure that there are no conflicts. For example making sure that there are no circular dependencies.
|
||||
|
||||
Underneath the hood, `createBackend` calls `createSpecializedBackend` from `@backstage/backend-app-api` which is responsible for actually creating the backend instance, but with no services or no features. You can think of `createBackend` more of a 'batteries included' approach, and `createSpecializedBackend` a little more low level.
|
||||
Underneath the hood, `createBackend` calls `createSpecializedBackend` from `@backstage/backend-app-api` which is responsible for actually creating the backend instance, without any services or features. You can think of `createBackend` more of a 'batteries included' approach, while `createSpecializedBackend` is more low level.
|
||||
|
||||
As mentioned previously there's also the ability to create multiple of these backends in your project so that you can split apart your backend and deploy different backends that can scale independently of each other. For instance you might choose to deploy a backend with only the catalog plugin enabled, and one with just the scaffolder plugin enabled.
|
||||
|
||||
@@ -88,42 +88,6 @@ export const fooServiceFactory = createServiceFactory({
|
||||
|
||||
Note that circular dependencies among service factories are not allowed. This is verified at runtime, and your backend instance will refuse to start up if it detects any conflicts. Likewise, the backend will also fail to start up if a service factory depends on a service that is not provided by any registered service factory.
|
||||
|
||||
## Service Factory Options
|
||||
|
||||
To install a service factory in a backend instance, we pass it in through the `services` option to `createBackend`:
|
||||
|
||||
```ts
|
||||
const backend = createBackend({
|
||||
services: [fooServiceFactory()],
|
||||
});
|
||||
```
|
||||
|
||||
Note that we call `fooServiceFactory` to create the service factory instance. This is because `createServiceFactory` always returns a factory function that creates the actual service factory. This is done to always allow for options to be added to the service factory in the future, without breaking existing code. To add options to your service factory, you wrap the object passed to `createServiceFactory` in a callback that accepts the desired options. For example:
|
||||
|
||||
```ts
|
||||
export interface FooFactoryOptions {
|
||||
mode: 'eager' | 'lazy';
|
||||
}
|
||||
|
||||
export const fooServiceFactory = createServiceFactory(
|
||||
(options?: FooFactoryOptions) => ({
|
||||
service: fooServiceRef,
|
||||
deps: { bar: barServiceRef },
|
||||
factory({ bar }) {
|
||||
return new DefaultFooService(bar, options?.mode);
|
||||
},
|
||||
}),
|
||||
);
|
||||
```
|
||||
|
||||
This lets us use the options to customize the factory implementation in any way we want. From the outside the service factory looks just like before, except that we're now also able to pass options when installing the factory:
|
||||
|
||||
```ts
|
||||
const backend = createBackend({
|
||||
services: [fooServiceFactory({ mode: 'eager' })],
|
||||
});
|
||||
```
|
||||
|
||||
## Core Services
|
||||
|
||||
The backend system provides a number of core service definitions that both help implement the main functionality of the backend, but also provide a set of utilities for common concerns, such as logging, database access, job scheduling, and so on. These core services will always be present in a backend instance created with `createBackend`, and they can all be overridden with custom implementations if needed.
|
||||
@@ -227,3 +191,40 @@ Note that we don't use the `fooServiceRef` when creating our service factory, bu
|
||||
If a service defines a default factory, that factory will be used if there is no explicit factory registered in the backend for that service. This allows users of your service to directly import and use a service, without worrying about whether it is installed or not. It is recommended to always define a default factory for any service that you are exporting for use in other plugins or modules.
|
||||
|
||||
When defining a default factory for a service, it is possible for it to end up with duplicate implementations at runtime. This applies both to any shared root context in your factory, as well as plugin specific instances of your service. This is because package dependency version ranges may not line up perfectly, causing duplicate installations of the same package. This can happen both for two different plugins using the same service, but also across a plugin and its modules. If your service would break in this scenario, you should not define a default factory for it, but instead require that users of your service explicitly install a factory in their backend instance.
|
||||
|
||||
## Service Factory Options
|
||||
|
||||
> NOTE: This pattern is discouraged, only use it when necessary. If possible you should prefer to make services configurable via static configuration instead.
|
||||
|
||||
When declaring a service factory it's possible to include an options callback. This allows you to customize the factory through code when installing it in the backend. For example, this is how you install an explicit factory instance in the backend without any options:
|
||||
|
||||
```ts
|
||||
const backend = createBackend();
|
||||
|
||||
backend.add(fooServiceFactory());
|
||||
```
|
||||
|
||||
Note that we call `fooServiceFactory` to create the service factory instance. This is because `createServiceFactory` always returns a factory function that creates the actual service factory. To add options to your service factory, you wrap the object passed to `createServiceFactory` in a callback that accepts the desired options. Note that the options must always be optional. For example:
|
||||
|
||||
```ts
|
||||
export interface FooFactoryOptions {
|
||||
transform: (foo: string) => string;
|
||||
}
|
||||
|
||||
export const fooServiceFactory = createServiceFactory(
|
||||
(options?: FooFactoryOptions) => ({
|
||||
service: fooServiceRef,
|
||||
factory() {
|
||||
return new DefaultFooService(options?.transform);
|
||||
},
|
||||
}),
|
||||
);
|
||||
```
|
||||
|
||||
This lets us use the options to customize the factory implementation in any way we want. From the outside the service factory looks just like before, except that we're now also able to pass options when installing the factory:
|
||||
|
||||
```ts
|
||||
const backend = createBackend();
|
||||
|
||||
backend.add(fooServiceFactory({ transform: foo => foo.toUpperCase() }));
|
||||
```
|
||||
|
||||
@@ -41,44 +41,32 @@ The `env` object passed to the `register` callback contains different methods th
|
||||
The `createBackendPlugin` return value is exported as `examplePlugin`, which is a factory function used to create the actual plugin instance. For example, to install the plugin in your backend instance, you would do the following:
|
||||
|
||||
```ts
|
||||
import { examplePlugin } from 'backstage-plugin-example-backend';
|
||||
|
||||
backend.add(examplePlugin());
|
||||
```
|
||||
|
||||
The reason for why our plugin instance has been wrapped up in a factory function is so that you can always chose to add options to your plugin in the future, without breaking existing usage. To add options you wrap the object passed to `createBackendPlugin` in a callback that accepts the desired options. For example:
|
||||
By convention every plugin package should export its plugin instance as the default export from the package:
|
||||
|
||||
```ts
|
||||
export interface ExamplePluginOptions {
|
||||
skipHello: boolean;
|
||||
}
|
||||
|
||||
export const examplePlugin = createBackendPlugin(
|
||||
(options?: ExamplePluginOptions) => ({
|
||||
pluginId: 'example',
|
||||
register(env) {
|
||||
env.registerInit({
|
||||
deps: {
|
||||
logger: coreServices.logger,
|
||||
},
|
||||
async init({ logger }) {
|
||||
if (!options?.skipHello) {
|
||||
logger.info('Hello from example plugin');
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
}),
|
||||
);
|
||||
// plugins/example-backend/src/index.ts
|
||||
export { examplePlugin as default } from './plugin.ts';
|
||||
```
|
||||
|
||||
Now your plugin accepts an optional options object, which can be used to configure each plugin instance. To supply options to the plugin, you pass them to the plugin factory method:
|
||||
This allows you to install the plugin in your backend instance by just referencing the package:
|
||||
|
||||
```ts
|
||||
backend.add(examplePlugin({ skipHello: true }));
|
||||
backend.add(import('backstage-plugin-example-backend'));
|
||||
```
|
||||
|
||||
It is also possible to make the options required, simply remove the `?` from the parameter declaration. This will be reflected in the returned factory function, which will now require the options parameter.
|
||||
To make your plugins customizable you should generally prefer to use [static configuration](../../conf/defining.md). By convention plugins should place their configuration under a top-level configuration key that matches the plugin ID. For example, our example plugin might be configured as follows:
|
||||
|
||||
Options are a simple way to allow for more lightweight customization of a plugin, but they do not allow for more complex extensions that require access to services. For that, you need to create and register extension points for your plugin, which are covered in the [next section](./05-extension-points.md).
|
||||
```yaml
|
||||
example:
|
||||
message: Welcome to the example plugin
|
||||
```
|
||||
|
||||
For situations where static configuration is too limiting, you can instead register extension points for your plugin. Extension Points are covered in the [next section](./05-extension-points.md).
|
||||
|
||||
## Rules of Plugins
|
||||
|
||||
|
||||
@@ -8,11 +8,11 @@ description: Extension points of backend plugins
|
||||
|
||||
> **DISCLAIMER: The new backend system is in alpha, and still under active development. While we have reviewed the interfaces carefully, they may still be iterated on before the stable release.**
|
||||
|
||||
While plugins are able to accept options for lightweight forms of customization and extension, you quickly hit a limit where you need something more powerful to allow users to extend your plugin. For this purpose, the backend system provides a mechanism for plugins to provide extension points, which can be used to expose deeper customizations for your plugin. Extension points are used by modules, which are installed in the backend adjacent to plugins. Modules are covered more in-depth in the [next section](./06-modules.md).
|
||||
While plugins are able to use static configuration for lightweight forms of customization, you can quickly hit a limit where you need something more powerful to allow users to extend your plugin. For this purpose, the backend system provides a mechanism for plugins to provide extension points, which can be used to expose deeper customizations for your plugin. Extension points are used by modules, which are installed in the backend adjacent to plugins. Modules are covered more in-depth in the [next section](./06-modules.md).
|
||||
|
||||
Extension points are quite similar to services, in that they both encapsulate an interface in a reference object. The key difference is that extension points are registered and provided by plugins themselves, and do not have any factory associated with them. Extension points for a given plugin are also only accessible to modules that extend that same plugin.
|
||||
|
||||
Extension points should always be exported from a plugin node library package, for example `@backstage/plugin-catalog-node`. This is to allow for modules to avoid a direct dependency on the plugin, and make it easier to evolve extension points over time. You can export as many different extension points as you want, just be mindful of the complexity of the API surface. It is however often better to export multiple extension points with few methods, rather than few extension points with many methods, as that tends to be easier to maintain.
|
||||
Plugin extension points should always be exported from a plugin node library package, for example `@backstage/plugin-catalog-node`. This is to allow for modules to avoid a direct dependency on the plugin, and make it easier to evolve extension points over time. You can export as many different extension points as you want, just be mindful of the complexity of the API surface. It is however often better to export multiple extension points with few methods, rather than few extension points with many methods, as that tends to be easier to maintain.
|
||||
|
||||
## Defining an Extension Point
|
||||
|
||||
@@ -36,28 +36,29 @@ export const scaffolderActionsExtensionPoint =
|
||||
For modules to be able to use your extension point, an implementation of it must be registered by the plugin. This is done using the `registerExtensionPoint` method in the `register` callback of the plugin definition.
|
||||
|
||||
```ts
|
||||
class ActionsExtension implements ScaffolderActionsExtensionPoint {
|
||||
addActions(...actions: TemplateAction<any>[]): void { ... }
|
||||
|
||||
getRegisteredActions() { ... }
|
||||
}
|
||||
|
||||
export const scaffolderPlugin = createBackendPlugin(
|
||||
{
|
||||
pluginId: 'scaffolder',
|
||||
register(env) {
|
||||
const actionsExtensions = new ActionsExtension();
|
||||
const actions = new Map<string, TemplateAction<any>>();
|
||||
|
||||
env.registerExtensionPoint(
|
||||
scaffolderActionsExtensionPoint,
|
||||
actionsExtensions,
|
||||
{
|
||||
addAction(action) {
|
||||
if (actions.has(action.id)) {
|
||||
throw new Error(`Scaffolder actions with ID '${action.id}' has already been installed`);
|
||||
}
|
||||
actions.set(action.id, action);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
env.registerInit({
|
||||
deps: { ... },
|
||||
async init({ ... }) {
|
||||
const actions = actionsExtension.getRegisteredActions();
|
||||
|
||||
// Use the registered actions when setting up the scaffolder ...
|
||||
const installedActions = Array.from(actions.values());
|
||||
},
|
||||
});
|
||||
},
|
||||
@@ -65,9 +66,11 @@ export const scaffolderPlugin = createBackendPlugin(
|
||||
);
|
||||
```
|
||||
|
||||
There are a couple of things to note here. The first is that our `ActionsExtension` class both implements the `ScaffolderActionsExtensionPoint` interface, but also has additional public methods. These methods won't be available to the modules that use this extension, but we can use them here in the implementation of our plugin. Note also that the `ActionsExtension` class is _not_ exported to the outside, since it's only for the internal use of this plugin during its setup phase.
|
||||
Note that we create a closure that adds to a shared `actions` structure when `addAction` is called by users of your extension point. It is safe for us to then access our `actions` in the `init` method of our plugin, since all modules that extend our plugin will be completely initialized before our plugin gets initialized. That means that at the point where our `init` method is called, all actions have been added and can be accessed.
|
||||
|
||||
The second is that we create our `ActionsExtension` instance within the `register` method, and then access it directly in our `init` method. This is both safe to do and an intended convenience. All modules that extend our plugin will be completely initialized before our plugin gets initialized, which means that at the point where our `init` method is called, all actions have been added and can be accessed.
|
||||
## Module Extension Points
|
||||
|
||||
Just like plugins, modules can also provide their own extension points. The API for registering and using extension points is the same as for plugins. However, modules should typically only use extension points to allow for complex internal customizations by users of the plugin module. It is therefore preferred to export the extension point directly from the module package, rather than creating a separate node library for that purpose.
|
||||
|
||||
## Extension Point Design
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ description: Modules for backend plugins
|
||||
|
||||
> **DISCLAIMER: The new backend system is in alpha, and still under active development. While we have reviewed the interfaces carefully, they may still be iterated on before the stable release.**
|
||||
|
||||
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.
|
||||
Backend modules are used to extend [plugins](./04-plugins.md) or sometimes other modules 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.
|
||||
|
||||
Both modules and plugins register an `init` method that is called during startup. In order to ensure that modules have registered all their extensions before the plugin starts up, all modules for each plugin are completely initialized before the plugin itself is initialized. In practice this means that all promises returned by each `init` method of the modules need to resolve before the plugin `init` method is called. This also means that it is not possible to further interact with the extension points once the `init` method has resolved.
|
||||
|
||||
@@ -19,6 +19,7 @@ A module depends on the extension points exported by the target plugin's library
|
||||
The following is an example on how to create a module that adds a new processor using the `catalogProcessingExtensionPoint`:
|
||||
|
||||
```ts
|
||||
// plugins/catalog-backend-module-example-processor/src/module.ts
|
||||
import { createBackendModule } from '@backstage/backend-plugin-api';
|
||||
import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node';
|
||||
import { MyCustomProcessor } from './MyCustomProcessor';
|
||||
@@ -42,4 +43,19 @@ export const catalogModuleExampleCustomProcessor = createBackendModule({
|
||||
|
||||
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.
|
||||
Just like plugins there is a convention that every module package should export its module instance as the default export from the package:
|
||||
|
||||
```ts
|
||||
// plugins/catalog-backend-module-example-processor/src/index.ts
|
||||
export { catalogModuleExampleCustomProcessor as default } from './module.ts';
|
||||
```
|
||||
|
||||
This allows you to install the module in your backend instance by just referencing the package:
|
||||
|
||||
```ts
|
||||
backend.add(
|
||||
import('backstage-plugin-catalog-backend-module-example-processor'),
|
||||
);
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
@@ -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 services that we want to be part of that backend. Plugins are standalone features, modules augment existing plugins or modules, 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.
|
||||
|
||||
@@ -36,8 +36,10 @@ describe('myPlugin', () => {
|
||||
const fakeConfig = { myPlugin: { value: 7 } };
|
||||
|
||||
const { server } = await startTestBackend({
|
||||
features: [myPlugin()],
|
||||
services: [mockServices.rootConfig.factory({ data: fakeConfig })],
|
||||
features: [
|
||||
myPlugin(),
|
||||
mockServices.rootConfig.factory({ data: fakeConfig }),
|
||||
],
|
||||
});
|
||||
|
||||
const response = await request(server).get('/api/example/get-value');
|
||||
@@ -150,8 +152,10 @@ your test database.
|
||||
```ts
|
||||
const { knex, subject } = await createSubject(databaseId);
|
||||
const { server } = await startTestBackend({
|
||||
features: [myPlugin()],
|
||||
services: [[coreServices.database, { getClient: async () => knex }]],
|
||||
features: [
|
||||
myPlugin(),
|
||||
mockServices.database.mock({ getClient: async () => knex }),
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
@@ -166,3 +170,37 @@ it'll take into account when present.
|
||||
- `BACKSTAGE_TEST_DATABASE_POSTGRES13_CONNECTION_STRING`
|
||||
- `BACKSTAGE_TEST_DATABASE_POSTGRES9_CONNECTION_STRING`
|
||||
- `BACKSTAGE_TEST_DATABASE_MYSQL8_CONNECTION_STRING`
|
||||
|
||||
## Testing Service Factories
|
||||
|
||||
To facilitate testing of service factories, the `@backstage/backend-test-utils`
|
||||
package provides a `ServiceFactoryTester` helper that lets you instantiate services
|
||||
in a controlled context.
|
||||
|
||||
The following example shows how to test a service factory where we also provide
|
||||
a mocked implementation of the `rootConfig` service.
|
||||
|
||||
```ts
|
||||
import {
|
||||
mockServices,
|
||||
ServiceFactoryTester,
|
||||
} from '@backstage/backend-test-utils';
|
||||
import { myServiceFactory } from './myServiceFactory.ts';
|
||||
|
||||
describe('myServiceFactory', () => {
|
||||
it('should provide value', async () => {
|
||||
const fakeConfig = { myConfiguredValue: 7 };
|
||||
|
||||
const tester = ServiceFactoryTester.from(myServiceFactory, {
|
||||
dependencies: [mockServices.rootConfig.factory({ data: fakeConfig })],
|
||||
});
|
||||
|
||||
const myService = await tester.get('test-plugin');
|
||||
|
||||
expect(myService.getValue()).toBe(7);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
The service factory tester also provides mocked implementations of the majority
|
||||
of all core services by default.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -61,13 +61,13 @@ You can configure these additional options by adding an override for the core se
|
||||
```ts
|
||||
import { httpRouterServiceFactory } from '@backstage/backend-app-api';
|
||||
|
||||
const backend = createBackend({
|
||||
services: [
|
||||
httpRouterServiceFactory({
|
||||
getPath: (pluginId: string) => `/plugins/${pluginId}`,
|
||||
}),
|
||||
],
|
||||
});
|
||||
const backend = createBackend();
|
||||
|
||||
backend.add(
|
||||
httpRouterServiceFactory({
|
||||
getPath: (pluginId: string) => `/plugins/${pluginId}`,
|
||||
}),
|
||||
);
|
||||
```
|
||||
|
||||
## Root HTTP Router
|
||||
@@ -118,31 +118,31 @@ You can configure the root HTTP Router service by passing the options to the `cr
|
||||
```ts
|
||||
import { rootHttpRouterServiceFactory } from '@backstage/backend-app-api';
|
||||
|
||||
const backend = createBackend({
|
||||
services: [
|
||||
rootHttpRouterServiceFactory({
|
||||
configure: ({ app, middleware, routes, config, logger, lifecycle }) => {
|
||||
// the built in middleware is provided through an option in the configure function
|
||||
app.use(middleware.helmet());
|
||||
app.use(middleware.cors());
|
||||
app.use(middleware.compression());
|
||||
const backend = createBackend();
|
||||
|
||||
// you can add you your own middleware in here
|
||||
app.use(custom.logging());
|
||||
backend.add(
|
||||
rootHttpRouterServiceFactory({
|
||||
configure: ({ app, middleware, routes, config, logger, lifecycle }) => {
|
||||
// the built in middleware is provided through an option in the configure function
|
||||
app.use(middleware.helmet());
|
||||
app.use(middleware.cors());
|
||||
app.use(middleware.compression());
|
||||
|
||||
// here the routes that are registered by other plugins
|
||||
app.use(routes);
|
||||
// you can add you your own middleware in here
|
||||
app.use(custom.logging());
|
||||
|
||||
// some other middleware that comes after the other routes
|
||||
app.use(middleware.notFound());
|
||||
app.use(middleware.error());
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
// here the routes that are registered by other plugins
|
||||
app.use(routes);
|
||||
|
||||
// some other middleware that comes after the other routes
|
||||
app.use(middleware.notFound());
|
||||
app.use(middleware.error());
|
||||
},
|
||||
}),
|
||||
);
|
||||
```
|
||||
|
||||
## 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');
|
||||
@@ -185,19 +185,19 @@ You can configure these additional options by adding an override for the core se
|
||||
```ts
|
||||
import { rootConfigServiceFactory } from '@backstage/backend-app-api';
|
||||
|
||||
const backend = createBackend({
|
||||
services: [
|
||||
rootConfigServiceFactory({
|
||||
argv: [
|
||||
'--config',
|
||||
'/backstage/app-config.development.yaml',
|
||||
'--config',
|
||||
'/backstage/app-config.yaml',
|
||||
],
|
||||
remote: { reloadIntervalSeconds: 60 },
|
||||
}),
|
||||
],
|
||||
});
|
||||
const backend = createBackend();
|
||||
|
||||
backend.add(
|
||||
rootConfigServiceFactory({
|
||||
argv: [
|
||||
'--config',
|
||||
'/backstage/app-config.development.yaml',
|
||||
'--config',
|
||||
'/backstage/app-config.yaml',
|
||||
],
|
||||
remote: { reloadIntervalSeconds: 60 },
|
||||
}),
|
||||
);
|
||||
```
|
||||
|
||||
## Logging
|
||||
@@ -243,34 +243,34 @@ The following example is how you can override the root logger service to add add
|
||||
import { coreServices } from '@backstage/backend-plugin-api';
|
||||
import { WinstonLogger } from '@backstage/backend-app-api';
|
||||
|
||||
const backend = createBackend({
|
||||
services: [
|
||||
createServiceFactory({
|
||||
service: coreServices.rootLogger,
|
||||
deps: {
|
||||
config: coreServices.config,
|
||||
},
|
||||
async factory({ config }) {
|
||||
const logger = WinstonLogger.create({
|
||||
meta: {
|
||||
service: 'backstage',
|
||||
// here's some additional information that is not part of the
|
||||
// original implementation
|
||||
podName: 'myk8spod',
|
||||
},
|
||||
level: process.env.LOG_LEVEL || 'info',
|
||||
format:
|
||||
process.env.NODE_ENV === 'production'
|
||||
? format.json()
|
||||
: WinstonLogger.colorFormat(),
|
||||
transports: [new transports.Console()],
|
||||
});
|
||||
const backend = createBackend();
|
||||
|
||||
return logger;
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
backend.add(
|
||||
createServiceFactory({
|
||||
service: coreServices.rootLogger,
|
||||
deps: {
|
||||
config: coreServices.rootConfig,
|
||||
},
|
||||
async factory({ config }) {
|
||||
const logger = WinstonLogger.create({
|
||||
meta: {
|
||||
service: 'backstage',
|
||||
// here's some additional information that is not part of the
|
||||
// original implementation
|
||||
podName: 'myk8spod',
|
||||
},
|
||||
level: process.env.LOG_LEVEL || 'info',
|
||||
format:
|
||||
process.env.NODE_ENV === 'production'
|
||||
? format.json()
|
||||
: WinstonLogger.colorFormat(),
|
||||
transports: [new transports.Console()],
|
||||
});
|
||||
|
||||
return logger;
|
||||
},
|
||||
}),
|
||||
);
|
||||
```
|
||||
|
||||
## Cache
|
||||
@@ -440,14 +440,14 @@ You can configure these additional options by adding an override for the core se
|
||||
```ts
|
||||
import { identityServiceFactory } from '@backstage/backend-app-api';
|
||||
|
||||
const backend = createBackend({
|
||||
services: [
|
||||
identityServiceFactory({
|
||||
issuer: 'backstage',
|
||||
algorithms: ['ES256', 'RS256'],
|
||||
}),
|
||||
],
|
||||
});
|
||||
const backend = createBackend();
|
||||
|
||||
backend.add(
|
||||
identityServiceFactory({
|
||||
issuer: 'backstage',
|
||||
algorithms: ['ES256', 'RS256'],
|
||||
}),
|
||||
);
|
||||
```
|
||||
|
||||
## Lifecycle
|
||||
@@ -532,19 +532,19 @@ class MyCustomLifecycleService implements RootLifecycleService {
|
||||
}
|
||||
}
|
||||
|
||||
const backend = createBackend({
|
||||
services: [
|
||||
createServiceFactory({
|
||||
service: coreServices.rootLifecycle,
|
||||
deps: {
|
||||
logger: coreServices.rootLogger,
|
||||
},
|
||||
async factory({ logger }) {
|
||||
return new MyCustomLifecycleService(logger);
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
const backend = createBackend();
|
||||
|
||||
backend.add(
|
||||
createServiceFactory({
|
||||
service: coreServices.rootLifecycle,
|
||||
deps: {
|
||||
logger: coreServices.rootLogger,
|
||||
},
|
||||
async factory({ logger }) {
|
||||
return new MyCustomLifecycleService(logger);
|
||||
},
|
||||
}),
|
||||
);
|
||||
```
|
||||
|
||||
## Permissions
|
||||
|
||||
@@ -266,12 +266,10 @@ class GoogleCloudLogger implements LoggerService {
|
||||
}
|
||||
|
||||
// packages/backend/src/index.ts
|
||||
const backend = createBackend({
|
||||
services: [
|
||||
// supplies additional or replacement services to the backend
|
||||
GoogleCloudLogger.factory(),
|
||||
],
|
||||
});
|
||||
const backend = createBackend();
|
||||
|
||||
// supplies additional or replacement services to the backend
|
||||
backend.add(GoogleCloudLogger.factory());
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
Reference in New Issue
Block a user