Merge pull request #25903 from backstage/rugvip/presets

backend-*-api: add support for feature loaders
This commit is contained in:
Patrik Oldsberg
2024-08-06 14:00:12 +02:00
committed by GitHub
32 changed files with 1207 additions and 405 deletions
+1 -1
View File
@@ -193,7 +193,7 @@ backend.add(import('@backstage/plugin-auth-backend-module-github-provider'));
backend.add(customAuth);
```
Check out [the naming patterns article](../backend-system/architecture/07-naming-patterns.md) for what rules
Check out [the naming patterns article](../backend-system/architecture/08-naming-patterns.md) for what rules
apply regarding how to form valid IDs. In this example we also put the module
declaration directly in `packages/backend/src/index.ts` but that's just for
simplicity. You can place it anywhere you like, including in other packages, and
@@ -38,7 +38,7 @@ export const fooServiceRef = createServiceRef<FooService>({
The `fooServiceRef` that we create above should be exported, and can then be used to declare a dependency on the `FooService` interface and receive an implementation of it at runtime.
When creating a service reference you need to give it an ID. This ID needs to be globally unique, and should generally be of the format `'<pluginId>.<serviceName>'`. For more naming patterns surrounding services, see the [naming patterns](./07-naming-patterns.md#services) page.
When creating a service reference you need to give it an ID. This ID needs to be globally unique, and should generally be of the format `'<pluginId>.<serviceName>'`. For more naming patterns surrounding services, see the [naming patterns](./08-naming-patterns.md#services) page.
A note on naming: the frontend and backend systems intentionally use the separate names "APIs" and "Services" for concepts that are quite similar. This is to avoid confusion between the two, both in documentation and discussion, but also in code. While the two systems are quite similar, they are not identical, and they can't be used interchangeably.
@@ -10,7 +10,7 @@ Plugins provide the actual base features of a Backstage backend. Each plugin ope
## Defining a Plugin
Plugins are created using the `createBackendPlugin` function, and should typically be exported from a plugin package. All plugins must have an ID and a `register` method, where the ID matches the plugin ID in the package name, without the `-backend` suffix. See also the [dedicated section](./07-naming-patterns.md) about proper naming patterns.
Plugins are created using the `createBackendPlugin` function, and should typically be exported from a plugin package. All plugins must have an ID and a `register` method, where the ID matches the plugin ID in the package name, without the `-backend` suffix. See also the [dedicated section](./08-naming-patterns.md) about proper naming patterns.
```ts
// plugins/example-backend/src/plugin.ts
@@ -0,0 +1,101 @@
---
id: feature-loaders
title: Backend Feature Loaders
sidebar_label: Feature Loaders
# prettier-ignore
description: Backend feature loaders
---
Backend feature loaders are used to programmatically select and install features in a Backstage backend. They can service a wide range of use cases, such as enabling or disabling features based on static configuration, dynamically load features at runtime, or conditionally load features based on the state of a system.
Feature loaders are defined using the `createBackendFeatureLoader` function, exported by `@backstage/backend-plugin-api`. It accepts a `loader` function, as well as an optional `deps` object for declaring service dependencies. Unlike plugins and modules, feature loaders are limited to only depending on root-scoped services, but that still allows access to for example the [root config](../core-services/root-config.md) and [root logger](../core-services/root-logger.md) services.
The `loader` function can be defined in many different ways, with the main requirement being that it returns a list of `BackendFeature`s in some form. A backend feature is the kind of object that you can pass to `backend.add(...)`, for example services factories, plugins, modules, or even other feature loaders. The `loader` function can be synchronous or asynchronous, and can be defined as a generator function to allow for more complex logic.
## Examples
The following are a few example of how feature loaders can be used:
### Simple list of features
A feature loader can simply return a list of features to be installed:
```ts
export default createBackendFeatureLoader({
loader() {
return [
import('@backstage/plugin-search-backend/alpha'),
import('@backstage/plugin-search-backend-module-catalog/alpha'),
import('@backstage/plugin-search-backend-module-explore/alpha'),
import('@backstage/plugin-search-backend-module-techdocs/alpha'),
];
},
});
```
It can also encapsulate a collection of custom features:
```ts
export default createBackendFeatureLoader({
// Async loader is fine too
async loader() {
return [
createBackendPlugin({
...
}),
createBackendModule({
...
}),
]
},
});
```
### Conditional loading
A feature loader can access root-scoped services, such as the config service. This allows for conditional loading of features based on configuration. It is often convenient to use a generator function for this purpose:
```ts
export default createBackendFeatureLoader({
deps: {
config: coreServices.rootConfig,
},
// The `*` in front of the function name makes it a generator function
*loader({ config }) {
// Example of a custom config flag to enable search
if (config.getOptionalString('customFeatureToggle.search')) {
yield import('@backstage/plugin-search-backend/alpha');
yield import('@backstage/plugin-search-backend-module-catalog/alpha');
yield import('@backstage/plugin-search-backend-module-explore/alpha');
yield import('@backstage/plugin-search-backend-module-techdocs/alpha');
}
},
});
```
### Dynamic logic
A feature loader can also be asynchronous, and for example fetch data from an external source to determine which features to load:
```ts
export default createBackendFeatureLoader({
// The `async *` in front of the function name makes it an async generator function.
async *loader() {
const localMetadata = await readMetadataFromDisk();
if (localMetadata.enableSearch) {
yield import('@backstage/plugin-search-backend/alpha');
yield import('@backstage/plugin-search-backend-module-catalog/alpha');
const remoteMetadata = await fetchMetadata();
if (remoteMetadata.enableExplore) {
yield import('@backstage/plugin-search-backend-module-explore/alpha');
}
if (remoteMetadata.enableTechDocs) {
yield import('@backstage/plugin-search-backend-module-techdocs/alpha');
}
}
},
});
```
@@ -68,7 +68,7 @@ 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
See [the article on naming patterns](../architecture/08-naming-patterns.md) for
details on how to best choose names/IDs for plugins and related backend system
items.
@@ -124,7 +124,7 @@ export const catalogModuleExampleCustomProcessor = createBackendModule({
export { catalogModuleExampleCustomProcessor as default } from './module';
```
See [the article on naming patterns](../architecture/07-naming-patterns.md) for
See [the article on naming patterns](../architecture/08-naming-patterns.md) for
details on how to best choose names/IDs for modules and related backend system
items.