From 7b53f18e44657e6fd58efb0023bd3b8bb884c46b Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 19 Jan 2023 13:57:03 +0100 Subject: [PATCH 01/14] chore: started to write some more documentation about the built in services Signed-off-by: blam --- docs/backend-system/core-services/01-index.md | 163 +++++++++++++++++- .../httpRouter/httpRouterFactory.ts | 2 +- 2 files changed, 158 insertions(+), 7 deletions(-) diff --git a/docs/backend-system/core-services/01-index.md b/docs/backend-system/core-services/01-index.md index cb1ec157ae..8b314e9b6b 100644 --- a/docs/backend-system/core-services/01-index.md +++ b/docs/backend-system/core-services/01-index.md @@ -18,6 +18,8 @@ import { coreServices } from '@backstage/backend-plugin-api'; One of the most common services is the HTTP router service which is used to expose HTTP endpoints for other plugins to consume. +### Using the service + The following example shows how to register a HTTP router for the `example` plugin. This single route will be available at the `/api/example/hello` path. @@ -46,9 +48,31 @@ createBackendPlugin({ }); ``` -## Logging and Configuration Service +### Configuration of the service -It is common for plugins to need access to configuration values and log messages. +There's additional configuration that you can optionally pass to setup the `httpRouter` core service. + +- `getPath` - Can be used to generate a path for each plugin. Currently defaults to `/api/${pluginId}` + +You can configure these additional options by adding an override for the core service when calling `createBackend` like follows: + +```ts +import { httpRouterFactory } from '@backstage/backend-app-api`; + +const backend = createBackend({ + services: [ + httpRouterFactory({ getPath: (pluginId: string) => `/plugins/${pluginId}` }), + ], +}); +``` + +## Config + +You will probably want to be able to reference config that is deployed alongside your plugin that can be referenced in `app-config.yaml`. + +### Using the service + +The following example shows how you can use the default config service to be able to get a config value, and then log it to the console. ```ts import { @@ -65,10 +89,137 @@ createBackendPlugin({ log: coreServices.logger, config: coreServices.config, }, - async init({ config, log }) { - log.warn('Brace yourself for more log output'); - const url = config.getString('backend.baseUrl'); - log.info(`Backend URL is running on ${url}`); + async init({ log, config }) { + log.warn( + `The backend is running at ${config.getString('backend.baseUrl')}`, + ); + }, + }); + }, +}); +``` + +### Configuration of the service + +There's additional configuration that you can optionally pass to setup the `config` core service. + +- `argv` - Override the arguments that are passed to the config loader, instead of using `process.argv` +- `remote` - Configure the `remote` config loading + +You can configure these additional options by adding an override for the core service when calling `createBackend` like follows: + +```ts +import { configFactory } from '@backstage/backend-app-api`; + +const backend = createBackend({ + services: [ + configFactory({ + argv: ['--config', '/backstage/app-config.development.yaml', '--config', '/backstage/app-config.yaml'], + remote: { reloadIntervalSeconds: 60 } + }), + ], +}); +``` + +## Logging + +It is common for your plugins to be able to use the logger. This logger is bound to your plugin, so that you will get nice messages with the plugin ID referenced in the log lines. + +### Using the service + +The following example shows how to get config in your `example` backend plugin and create a `warn` that will be printed nicely to the console. + +```ts +import { + coreServices, + createBackendPlugin, +} from '@backstage/backend-plugin-api'; +import { Router } from 'express'; + +createBackendPlugin({ + id: 'example', + register(env) { + env.registerInit({ + deps: { + log: coreServices.logger, + }, + async init({ log }) { + log.warn('Heres a nice log line thats a warning!'); + }, + }); + }, +}); +``` + +## Cache + +There's a core service provided with the backend system that can be used to interact with a cache in your plugins. This cache is bound to your plugin too, so that you will only set and get values in your plugins namespace. + +### Using the service + +The following example shows how to get a cache client in your `example` backend plugin and `set` and `get` values from the cache. + +```ts +import { + coreServices, + createBackendPlugin, +} from '@backstage/backend-plugin-api'; +import { Router } from 'express'; + +createBackendPlugin({ + id: 'example', + register(env) { + env.registerInit({ + deps: { + cache: coreServices.cache, + }, + async init({ cache }) { + const { key, value } = { key: 'test:key', value: 'bob' }; + await cache.set(key, value, { ttl: 1000 }); + + // .. some other stuff. + + await cache.get(key); // 'bob' + }, + }); + }, +}); +``` + +## Database + +Interacting with databases inside your plugin is something that is quite common, and we provide a `PluginDatabaseManager` part of the core services that you can get `knex` client hooked up to your database which is configured in `app-config.yaml`. + +If there's no config provided in `backend.database` then you will automatically get a simple in memory `sqlite3` client for your plugin. + +These `PluginDatabaseManager`s are scoped per plugin too, so that table names do not conflict across plugins either. + +### Using the service + +The following example shows how to get a `PluginDatabaseManager` in your `example` backend plugin and get a `client` for interacting with the database and running some migrations from a `migrationsDir` for your plugin. + +```ts +import { + coreServices, + createBackendPlugin, +} from '@backstage/backend-plugin-api'; +import { Router } from 'express'; + +createBackendPlugin({ + id: 'example', + register(env) { + env.registerInit({ + deps: { + database: coreServices.database, + }, + async init({ database }) { + const client = database.getClient(); + + if (!database.migrations?.skip) { + await client.migrate.latest({ + directory: migrationsDir, + }); + } }, }); }, diff --git a/packages/backend-app-api/src/services/implementations/httpRouter/httpRouterFactory.ts b/packages/backend-app-api/src/services/implementations/httpRouter/httpRouterFactory.ts index 1d52af3b76..218910345d 100644 --- a/packages/backend-app-api/src/services/implementations/httpRouter/httpRouterFactory.ts +++ b/packages/backend-app-api/src/services/implementations/httpRouter/httpRouterFactory.ts @@ -27,7 +27,7 @@ export interface HttpRouterFactoryOptions { /** * A callback used to generate the path for each plugin, defaults to `/api/{pluginId}`. */ - getPath(pluginId: string): string; + getPath?(pluginId: string): string; } /** @public */ From 2154b539473beb440f906c4ca98f6fd15b52dba7 Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 19 Jan 2023 14:57:08 +0100 Subject: [PATCH 02/14] chore: added some more thigns Signed-off-by: blam --- docs/backend-system/core-services/01-index.md | 148 ++++++++++++++++++ 1 file changed, 148 insertions(+) diff --git a/docs/backend-system/core-services/01-index.md b/docs/backend-system/core-services/01-index.md index 8b314e9b6b..2a9e01244d 100644 --- a/docs/backend-system/core-services/01-index.md +++ b/docs/backend-system/core-services/01-index.md @@ -225,3 +225,151 @@ createBackendPlugin({ }, }); ``` + +## Discovery + +When building plugins, you might find that you will need to lookup where in fact another plugins `baseUrl`. This could be for example, a `http` route or some `ws` protocol URL. For this we have the `discovery` service that you can query both the internal and external `baseUrl`s given a plugin ID. + +### Using the service + +The following example shows how to get the `DiscoveryService` in your `example` backend plugin and making a request to both the internal and external `baseUrl`s for the `derp` plugin. + +```ts +import { + coreServices, + createBackendPlugin, +} from '@backstage/backend-plugin-api'; +import { fetch } from 'node-fetch'; +import { Router } from 'express'; + +createBackendPlugin({ + id: 'example', + register(env) { + env.registerInit({ + deps: { + discovery: coreServices.discovery, + }, + async init({ discovery }) { + const urls = await Promise.all[ + discovery.getBaseUrl('derp'), + discovery.getExternalBaseUrl('derp'), + ]; + + await Promise.all( + urls.map( + (url) => fetch(url).then((r) => r.json()), + ), + ); + }, + }); + }, +}); +``` + +## Identity + +When working with backend plugins, you might find that you will need to interact with the `auth-backend` plugin to both authenticate backstage tokens, and get things like the `entityRef` of the authenticated user, and anything that they might claim to own through `ownershipEntityRefs`. + +### Using the service + +The following example shows how to get the `IdentityService` in your `example` backend plugin and retrieve the users `entityRef` and ownership claims for the incoming `http` request. + +```ts +import { + coreServices, + createBackendPlugin, +} from '@backstage/backend-plugin-api'; +import { Router } from 'express'; + +createBackendPlugin({ + id: 'example', + register(env) { + env.registerInit({ + deps: { + identity: coreServices.identity, + http: coreServices.httpRouter, + }, + async init({ http, identity }) { + const router = Router(); + router.get('/test-me', (request, response) => { + // use the identityService pull out the header from the request and get the user + const { identity: userEntityRef, ownershipEntityRefs } = + await identity.getIdentity({ + request, + }); + + // sent the decoded and validated things back to the user + response.json({ + userEntityRef, + ownershipEntityRefs, + }); + }); + + http.use(router); + }, + }); + }, +}); +``` + +### Configuration of the service + +There's additional configuration that you can optionally pass to setup the `identity` core service. + +- `issuer` - Set an optional issuer for validation of the `jwt` +- `algorithms` - `jws` `alg` header for validation of the `jwt`, defaults to `ES256`. More info on supported algorithms under [jose](https://github.com/panva/jose) + +You can configure these additional options by adding an override for the core service when calling `createBackend` like follows: + +```ts +import { identityFactory } from '@backstage/backend-app-api`; + +const backend = createBackend({ + services: [ + identityFactory({ + issuer: 'backstage', + algorithms: ['ES256', 'RS256'] + }), + ], +}); +``` + +## Lifecycle + +When writing plugins, it's often that you will have long running things that you might want to ensure clean shutdowns of when the plugins are torn down, or when the backend is quit (think local development). You shouldn't have to worry too much about providing shutdowns for any of the core services that you use, and should really only need to take care of anything that you create that you should stop when your plugin stops. + +### Using the service + +The following example shows how to get the `LifecycleService` in your `example` backend plugin to clean a long running interval on teardown. + +```ts +import { + coreServices, + createBackendPlugin, +} from '@backstage/backend-plugin-api'; +import { Router } from 'express'; + +createBackendPlugin({ + id: 'example', + register(env) { + env.registerInit({ + deps: { + lifecycle: coreServices.lifecycle, + logger: coreServices.logger, + }, + async init({ lifecycle, logger }) { + // setup by creating an interval that does something that we want to stop after the plugin is stopped. + const interval = setInterval(async () => { + await fetch('http://google.com/keepalive').then(r => r.json()); + // do some other stuff. + }); + + lifecycle.addShutdownHook({ + fn: () => clearInterval(interval), + logger, + }); + }, + }); + }, +}); +``` From cc1b2ac08899b5383ba35eb44990ef76a52408d7 Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 19 Jan 2023 14:58:14 +0100 Subject: [PATCH 03/14] chore: fixing api-reports Signed-off-by: blam --- packages/backend-app-api/api-report.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend-app-api/api-report.md b/packages/backend-app-api/api-report.md index dcdc105b17..7ad4b590ab 100644 --- a/packages/backend-app-api/api-report.md +++ b/packages/backend-app-api/api-report.md @@ -125,7 +125,7 @@ export const httpRouterFactory: ( // @public (undocumented) export interface HttpRouterFactoryOptions { - getPath(pluginId: string): string; + getPath?(pluginId: string): string; } // @public From ed8b5967d7488fc5ff623071d3a79e340b570ee5 Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 19 Jan 2023 14:58:52 +0100 Subject: [PATCH 04/14] chore: added changeset Signed-off-by: blam --- .changeset/swift-fishes-smash.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/swift-fishes-smash.md diff --git a/.changeset/swift-fishes-smash.md b/.changeset/swift-fishes-smash.md new file mode 100644 index 0000000000..a4d32c59ea --- /dev/null +++ b/.changeset/swift-fishes-smash.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-app-api': patch +--- + +`getPath` should be optional as we provide a default value for it From 9cd16d0b36edbf5eca7ca59dc17c1296a58fd829 Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 19 Jan 2023 16:51:43 +0100 Subject: [PATCH 05/14] chore: done a first pass for now Signed-off-by: blam --- docs/backend-system/core-services/01-index.md | 100 ++++++++++++++++-- 1 file changed, 90 insertions(+), 10 deletions(-) diff --git a/docs/backend-system/core-services/01-index.md b/docs/backend-system/core-services/01-index.md index 2a9e01244d..909b1aaee7 100644 --- a/docs/backend-system/core-services/01-index.md +++ b/docs/backend-system/core-services/01-index.md @@ -79,7 +79,6 @@ import { coreServices, createBackendPlugin, } from '@backstage/backend-plugin-api'; -import { Router } from 'express'; createBackendPlugin({ id: 'example', @@ -134,7 +133,6 @@ import { coreServices, createBackendPlugin, } from '@backstage/backend-plugin-api'; -import { Router } from 'express'; createBackendPlugin({ id: 'example', @@ -164,7 +162,6 @@ import { coreServices, createBackendPlugin, } from '@backstage/backend-plugin-api'; -import { Router } from 'express'; createBackendPlugin({ id: 'example', @@ -203,7 +200,6 @@ import { coreServices, createBackendPlugin, } from '@backstage/backend-plugin-api'; -import { Router } from 'express'; createBackendPlugin({ id: 'example', @@ -240,7 +236,6 @@ import { createBackendPlugin, } from '@backstage/backend-plugin-api'; import { fetch } from 'node-fetch'; -import { Router } from 'express'; createBackendPlugin({ id: 'example', @@ -293,10 +288,11 @@ createBackendPlugin({ const router = Router(); router.get('/test-me', (request, response) => { // use the identityService pull out the header from the request and get the user - const { identity: userEntityRef, ownershipEntityRefs } = - await identity.getIdentity({ - request, - }); + const { + identity: { userEntityRef, ownershipEntityRefs }, + } = await identity.getIdentity({ + request, + }); // sent the decoded and validated things back to the user response.json({ @@ -347,7 +343,6 @@ import { coreServices, createBackendPlugin, } from '@backstage/backend-plugin-api'; -import { Router } from 'express'; createBackendPlugin({ id: 'example', @@ -373,3 +368,88 @@ createBackendPlugin({ }, }); ``` + +## Permissions + +Sometimes you want to include permissions and making sure that a user that is authorized to do some actions in your plugin. We've provide a core service out of the box for you to interact with the permissions framework. You can find out more about the permissions framework in [the documentation](https://backstage.io/docs/permissions/overview) + +### Using the service + +The following example shows how to get the `PermissionsSerice` in your `example` backend to check to see if the user has the correct permissions for `myCustomPermission`. + +```ts +import { + coreServices, + createBackendPlugin, +} from '@backstage/backend-plugin-api'; +import { Router } from 'express'; + +createBackendPlugin({ + id: 'example', + register(env) { + env.registerInit({ + deps: { + permissions: coreServices.permissions, + http: coreServices.httpRouter, + }, + async init({ permissions, http }) { + const router = Router(); + router.get('/test-me', (request, response) => { + // use the identityService pull out the header from the request and get the token + const { token } = await identity.getIdentity({ + request, + }); + + // ask the permissions framework what the decision is for the permission + const permissionResponse = await permissions.authorize( + [ + { + permission: myCustomPermission, + }, + ], + { token }, + ); + }); + + http.use(router); + }, + }); + }, +}); +``` + +## Scheduler + +When writing plugins, it's often that you want to have things running on a schedule, or something similar to cron jobs that are distributed through instances that your backend plugin might be running on. We supply a `TaskScheduler` that is scoped per plugin so that you can create these tasks and orchestrate the running of them. + +### Using the service + +The following example shows how to get the `SchedulerService` in your `example` backend to schedule a scheduled task that runs once across your instances at a given interval. + +```ts +import { + coreServices, + createBackendPlugin, +} from '@backstage/backend-plugin-api'; +import { fetch } from 'node-fetch'; + +createBackendPlugin({ + id: 'example', + register(env) { + env.registerInit({ + deps: { + scheduler: coreServices.scheduler, + }, + async init({ scheduler }) { + await scheduler.scheduleTask({ + frequency: Duration.fromObject({ minutes: 10 }), + id: 'ping-google', + fn: async () => { + await fetch('http://google.com/ping'); + }, + }); + }, + }); + }, +}); +``` From dae0550b77320a4a41a4c84b2d0b22a554946dc4 Mon Sep 17 00:00:00 2001 From: Ben Lambert Date: Fri, 20 Jan 2023 13:20:44 +0100 Subject: [PATCH 06/14] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fredrik Adelöw Signed-off-by: Ben Lambert --- docs/backend-system/core-services/01-index.md | 67 ++++++++++--------- 1 file changed, 34 insertions(+), 33 deletions(-) diff --git a/docs/backend-system/core-services/01-index.md b/docs/backend-system/core-services/01-index.md index 909b1aaee7..6cf74aeeea 100644 --- a/docs/backend-system/core-services/01-index.md +++ b/docs/backend-system/core-services/01-index.md @@ -48,7 +48,7 @@ createBackendPlugin({ }); ``` -### Configuration of the service +### Configuring the service There's additional configuration that you can optionally pass to setup the `httpRouter` core service. @@ -57,7 +57,7 @@ There's additional configuration that you can optionally pass to setup the `http You can configure these additional options by adding an override for the core service when calling `createBackend` like follows: ```ts -import { httpRouterFactory } from '@backstage/backend-app-api`; +import { httpRouterFactory } from '@backstage/backend-app-api'; const backend = createBackend({ services: [ @@ -68,7 +68,7 @@ const backend = createBackend({ ## Config -You will probably want to be able to reference config that is deployed alongside your plugin that can be referenced in `app-config.yaml`. +This service allows you to read configuration values out of your `app-config` YAML files. ### Using the service @@ -103,12 +103,12 @@ createBackendPlugin({ There's additional configuration that you can optionally pass to setup the `config` core service. - `argv` - Override the arguments that are passed to the config loader, instead of using `process.argv` -- `remote` - Configure the `remote` config loading +- `remote` - Configure remote configuration loading You can configure these additional options by adding an override for the core service when calling `createBackend` like follows: ```ts -import { configFactory } from '@backstage/backend-app-api`; +import { configFactory } from '@backstage/backend-app-api'; const backend = createBackend({ services: [ @@ -122,11 +122,11 @@ const backend = createBackend({ ## Logging -It is common for your plugins to be able to use the logger. This logger is bound to your plugin, so that you will get nice messages with the plugin ID referenced in the log lines. +This service allows plugins to output logging information. There are actually two logger services: a root logger, and a plugin logger which is bound to individual plugins, so that you will get nice messages with the plugin ID referenced in the log lines. ### Using the service -The following example shows how to get config in your `example` backend plugin and create a `warn` that will be printed nicely to the console. +The following example shows how to get the logger in your `example` backend plugin and create a warning message that will be printed nicely to the console. ```ts import { @@ -142,7 +142,7 @@ createBackendPlugin({ log: coreServices.logger, }, async init({ log }) { - log.warn('Heres a nice log line thats a warning!'); + log.warn("Here's a nice log line that's a warning!"); }, }); }, @@ -151,11 +151,11 @@ createBackendPlugin({ ## Cache -There's a core service provided with the backend system that can be used to interact with a cache in your plugins. This cache is bound to your plugin too, so that you will only set and get values in your plugins namespace. +This service lets your plugin interact with a cache. It is bound to your plugin too, so that you will only set and get values in your plugin's private namespace. ### Using the service -The following example shows how to get a cache client in your `example` backend plugin and `set` and `get` values from the cache. +The following example shows how to get a cache client in your `example` backend plugin and setting and getting values from the cache. ```ts import { @@ -185,15 +185,15 @@ createBackendPlugin({ ## Database -Interacting with databases inside your plugin is something that is quite common, and we provide a `PluginDatabaseManager` part of the core services that you can get `knex` client hooked up to your database which is configured in `app-config.yaml`. +This service lets your plugins get a `knex` client hooked up to a database which is configured in your `app-config` YAML files, for your persistence needs. -If there's no config provided in `backend.database` then you will automatically get a simple in memory `sqlite3` client for your plugin. +If there's no config provided in `backend.database` then you will automatically get a simple in-memory SQLite 3 database for your plugin whose contents will be lost when the service restarts. -These `PluginDatabaseManager`s are scoped per plugin too, so that table names do not conflict across plugins either. +This service is scoped per plugin too, so that table names do not conflict across plugins. ### Using the service -The following example shows how to get a `PluginDatabaseManager` in your `example` backend plugin and get a `client` for interacting with the database and running some migrations from a `migrationsDir` for your plugin. +The following example shows how to get access to the database service in your `example` backend plugin and getting a client for interacting with the database. It also runs some migrations from a certain directory for your plugin. ```ts import { @@ -209,7 +209,7 @@ createBackendPlugin({ database: coreServices.database, }, async init({ database }) { - const client = database.getClient(); + const client = await database.getClient(); if (!database.migrations?.skip) { await client.migrate.latest({ @@ -224,11 +224,11 @@ createBackendPlugin({ ## Discovery -When building plugins, you might find that you will need to lookup where in fact another plugins `baseUrl`. This could be for example, a `http` route or some `ws` protocol URL. For this we have the `discovery` service that you can query both the internal and external `baseUrl`s given a plugin ID. +When building plugins, you might find that you will need to look up another plugin's base URL to be able to communicate with it. This could be for example an HTTP route or some `ws` protocol URL. For this we have a discovery service which can provide both internal and external base URLs for a given a plugin ID. ### Using the service -The following example shows how to get the `DiscoveryService` in your `example` backend plugin and making a request to both the internal and external `baseUrl`s for the `derp` plugin. +The following example shows how to get the discovery service in your `example` backend plugin and making a request to both the internal and external base URLs for the `derp` plugin. ```ts import { @@ -263,11 +263,11 @@ createBackendPlugin({ ## Identity -When working with backend plugins, you might find that you will need to interact with the `auth-backend` plugin to both authenticate backstage tokens, and get things like the `entityRef` of the authenticated user, and anything that they might claim to own through `ownershipEntityRefs`. +When working with backend plugins, you might find that you will need to interact with the `auth-backend` plugin to both authenticate backstage tokens, and to deconstruct them to get the user's entity ref and/or ownership claims out of them. ### Using the service -The following example shows how to get the `IdentityService` in your `example` backend plugin and retrieve the users `entityRef` and ownership claims for the incoming `http` request. +The following example shows how to get the identity service in your `example` backend plugin and retrieve the user's entity ref and ownership claims for the incoming request. ```ts import { @@ -287,14 +287,14 @@ createBackendPlugin({ async init({ http, identity }) { const router = Router(); router.get('/test-me', (request, response) => { - // use the identityService pull out the header from the request and get the user + // use the identity service to pull out the header from the request and get the user const { identity: { userEntityRef, ownershipEntityRefs }, } = await identity.getIdentity({ request, }); - // sent the decoded and validated things back to the user + // send the decoded and validated things back to the user response.json({ userEntityRef, ownershipEntityRefs, @@ -312,13 +312,13 @@ createBackendPlugin({ There's additional configuration that you can optionally pass to setup the `identity` core service. -- `issuer` - Set an optional issuer for validation of the `jwt` -- `algorithms` - `jws` `alg` header for validation of the `jwt`, defaults to `ES256`. More info on supported algorithms under [jose](https://github.com/panva/jose) +- `issuer` - Set an optional issuer for validation of the JWT token +- `algorithms` - `alg` header for validation of the JWT token, defaults to `ES256`. More info on supported algorithms can be found in the [`jose` library documentation](https://github.com/panva/jose) You can configure these additional options by adding an override for the core service when calling `createBackend` like follows: ```ts -import { identityFactory } from '@backstage/backend-app-api`; +import { identityFactory } from '@backstage/backend-app-api'; const backend = createBackend({ services: [ @@ -332,11 +332,11 @@ const backend = createBackend({ ## Lifecycle -When writing plugins, it's often that you will have long running things that you might want to ensure clean shutdowns of when the plugins are torn down, or when the backend is quit (think local development). You shouldn't have to worry too much about providing shutdowns for any of the core services that you use, and should really only need to take care of anything that you create that you should stop when your plugin stops. +This service allows your plugins to register hooks for cleaning up resources as the service is shutting down (e.g. when a pod is being torn down, or when pressing `Ctrl+C` during local development). Other core services also leverage this same mechanism internally to stop themselves cleanly. ### Using the service -The following example shows how to get the `LifecycleService` in your `example` backend plugin to clean a long running interval on teardown. +The following example shows how to get the lifecycle service in your `example` backend plugin to clean up a long running interval when the service is shutting down. ```ts import { @@ -353,7 +353,7 @@ createBackendPlugin({ logger: coreServices.logger, }, async init({ lifecycle, logger }) { - // setup by creating an interval that does something that we want to stop after the plugin is stopped. + // some example work that we want to stop when shutting down const interval = setInterval(async () => { await fetch('http://google.com/keepalive').then(r => r.json()); // do some other stuff. @@ -371,11 +371,11 @@ createBackendPlugin({ ## Permissions -Sometimes you want to include permissions and making sure that a user that is authorized to do some actions in your plugin. We've provide a core service out of the box for you to interact with the permissions framework. You can find out more about the permissions framework in [the documentation](https://backstage.io/docs/permissions/overview) +This service allows your plugins to ask [the permissions framework](https://backstage.io/docs/permissions/overview) for authorization of user actions. ### Using the service -The following example shows how to get the `PermissionsSerice` in your `example` backend to check to see if the user has the correct permissions for `myCustomPermission`. +The following example shows how to get the permissions service in your `example` backend to check to see if the user is allowed to perform a certain action with a custom permission rule. ```ts import { @@ -395,7 +395,7 @@ createBackendPlugin({ async init({ permissions, http }) { const router = Router(); router.get('/test-me', (request, response) => { - // use the identityService pull out the header from the request and get the token + // use the identity service to pull out the token from request headers const { token } = await identity.getIdentity({ request, }); @@ -420,11 +420,11 @@ createBackendPlugin({ ## Scheduler -When writing plugins, it's often that you want to have things running on a schedule, or something similar to cron jobs that are distributed through instances that your backend plugin might be running on. We supply a `TaskScheduler` that is scoped per plugin so that you can create these tasks and orchestrate the running of them. +When writing plugins, you sometimes want to have things running on a schedule, or something similar to cron jobs that are distributed through instances that your backend plugin is running on. We supply a task scheduler for this purpose that is scoped per plugin so that you can create these tasks and orchestrate their execution. ### Using the service -The following example shows how to get the `SchedulerService` in your `example` backend to schedule a scheduled task that runs once across your instances at a given interval. +The following example shows how to get the scheduler service in your `example` backend to issue a scheduled task that runs across your instances at a given interval. ```ts import { @@ -442,7 +442,8 @@ createBackendPlugin({ }, async init({ scheduler }) { await scheduler.scheduleTask({ - frequency: Duration.fromObject({ minutes: 10 }), + frequency: { minutes: 10 }, + timeout: { seconds: 30 }, id: 'ping-google', fn: async () => { await fetch('http://google.com/ping'); From 4fbd05dd83c1595a345ef2ced18348417da07269 Mon Sep 17 00:00:00 2001 From: blam Date: Fri, 20 Jan 2023 13:27:22 +0100 Subject: [PATCH 07/14] chore: fixing and pretty: Signed-off-by: blam Signed-off-by: blam --- docs/backend-system/core-services/01-index.md | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/docs/backend-system/core-services/01-index.md b/docs/backend-system/core-services/01-index.md index 6cf74aeeea..8477137d11 100644 --- a/docs/backend-system/core-services/01-index.md +++ b/docs/backend-system/core-services/01-index.md @@ -61,7 +61,9 @@ import { httpRouterFactory } from '@backstage/backend-app-api'; const backend = createBackend({ services: [ - httpRouterFactory({ getPath: (pluginId: string) => `/plugins/${pluginId}` }), + httpRouterFactory({ + getPath: (pluginId: string) => `/plugins/${pluginId}`, + }), ], }); ``` @@ -89,9 +91,8 @@ createBackendPlugin({ config: coreServices.config, }, async init({ log, config }) { - log.warn( - `The backend is running at ${config.getString('backend.baseUrl')}`, - ); + const baseUrl = config.getString('backend.baseUrl'); + log.warn(`The backend is running at ${baseUrl}`); }, }); }, @@ -113,8 +114,13 @@ import { configFactory } from '@backstage/backend-app-api'; const backend = createBackend({ services: [ configFactory({ - argv: ['--config', '/backstage/app-config.development.yaml', '--config', '/backstage/app-config.yaml'], - remote: { reloadIntervalSeconds: 60 } + argv: [ + '--config', + '/backstage/app-config.development.yaml', + '--config', + '/backstage/app-config.yaml', + ], + remote: { reloadIntervalSeconds: 60 }, }), ], }); @@ -324,7 +330,7 @@ const backend = createBackend({ services: [ identityFactory({ issuer: 'backstage', - algorithms: ['ES256', 'RS256'] + algorithms: ['ES256', 'RS256'], }), ], }); @@ -357,7 +363,7 @@ createBackendPlugin({ const interval = setInterval(async () => { await fetch('http://google.com/keepalive').then(r => r.json()); // do some other stuff. - }); + }, 1000); lifecycle.addShutdownHook({ fn: () => clearInterval(interval), From 4db2af02c63a686c188583bd88c2a40217c8af44 Mon Sep 17 00:00:00 2001 From: blam Date: Fri, 20 Jan 2023 13:28:02 +0100 Subject: [PATCH 08/14] chore: rewrd Signed-off-by: blam Signed-off-by: blam --- .changeset/swift-fishes-smash.md | 2 +- docs/backend-system/core-services/01-index.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.changeset/swift-fishes-smash.md b/.changeset/swift-fishes-smash.md index a4d32c59ea..cf3f00129c 100644 --- a/.changeset/swift-fishes-smash.md +++ b/.changeset/swift-fishes-smash.md @@ -2,4 +2,4 @@ '@backstage/backend-app-api': patch --- -`getPath` should be optional as we provide a default value for it +`HttpRouterFactoryOptions.getPath` is now optional as a default value is always provided in the factory. diff --git a/docs/backend-system/core-services/01-index.md b/docs/backend-system/core-services/01-index.md index 8477137d11..1e37b08c63 100644 --- a/docs/backend-system/core-services/01-index.md +++ b/docs/backend-system/core-services/01-index.md @@ -99,7 +99,7 @@ createBackendPlugin({ }); ``` -### Configuration of the service +### Configuring the service There's additional configuration that you can optionally pass to setup the `config` core service. @@ -314,7 +314,7 @@ createBackendPlugin({ }); ``` -### Configuration of the service +### Configuring the service There's additional configuration that you can optionally pass to setup the `identity` core service. From 25a15af2d643780e3fb4887408968bc6db598437 Mon Sep 17 00:00:00 2001 From: blam Date: Fri, 20 Jan 2023 13:31:36 +0100 Subject: [PATCH 09/14] chore: some more small additions Signed-off-by: blam --- docs/backend-system/core-services/01-index.md | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/docs/backend-system/core-services/01-index.md b/docs/backend-system/core-services/01-index.md index 1e37b08c63..f10c4c1e02 100644 --- a/docs/backend-system/core-services/01-index.md +++ b/docs/backend-system/core-services/01-index.md @@ -206,6 +206,7 @@ import { coreServices, createBackendPlugin, } from '@backstage/backend-plugin-api'; +import { resolvePackagePath } from '@backstage/backend-common'; createBackendPlugin({ id: 'example', @@ -216,7 +217,10 @@ createBackendPlugin({ }, async init({ database }) { const client = await database.getClient(); - + const migrationsDir = resolvePackagePath( + '@internal/my-plugin', + 'migrations', + ); if (!database.migrations?.skip) { await client.migrate.latest({ directory: migrationsDir, @@ -251,16 +255,8 @@ createBackendPlugin({ discovery: coreServices.discovery, }, async init({ discovery }) { - const urls = await Promise.all[ - discovery.getBaseUrl('derp'), - discovery.getExternalBaseUrl('derp'), - ]; - - await Promise.all( - urls.map( - (url) => fetch(url).then((r) => r.json()), - ), - ); + const url = await discoverty.getBaseUrl('derp'); // can also use discovery.getBaseUrl to retrieve external URL + const response = await fetch(`${url}/hello`); }, }); }, From bb8fbac4b882e0312c3676709d253e8b7c4cbc33 Mon Sep 17 00:00:00 2001 From: blam Date: Fri, 20 Jan 2023 14:33:07 +0100 Subject: [PATCH 10/14] chore: added some docs for the rootHttpRouter Signed-off-by: blam --- docs/backend-system/core-services/01-index.md | 113 ++++++++++++++++++ 1 file changed, 113 insertions(+) diff --git a/docs/backend-system/core-services/01-index.md b/docs/backend-system/core-services/01-index.md index f10c4c1e02..9f01337eae 100644 --- a/docs/backend-system/core-services/01-index.md +++ b/docs/backend-system/core-services/01-index.md @@ -456,3 +456,116 @@ createBackendPlugin({ }, }); ``` + +## URL Readers + +Plugins will require communication with certain integrations that users have configured. Popular integrations are things like Version Control Systems (VSC), such as GitHub, BitBucket GitLab etc. These integrations are configured in the `integrations` section of the `app-config.yaml` file. + +These URL readers are basically wrappers with authentication for files and folders that could be stored in these VCS repositories. + +### Using the service + +The following example shows how to get the URL Reader service in your `example` backend plugin to read a file and a directory from a GitHub repository. + +```ts +import { + coreServices, + createBackendPlugin, +} from '@backstage/backend-plugin-api'; +import os from 'os'; + +createBackendPlugin({ + id: 'example', + register(env) { + env.registerInit({ + deps: { + urlReader: coreServices.urlReader, + }, + async init({ urlReader }) { + const reader = await urlReader + .read('https://github.com/backstage/backstage/blob/master/README.md') + .then(r => r.buffer()); + + const tmpDir = os.tmpdir(); + const directory = await urlReader + .readTree( + 'https://github.com/backstage/backstage/tree/master/packages/backend', + ) + .then(tree => tree.dir({ targetDir: tmpDir })); + }, + }); + }, +}); +``` + +## Root HTTP Router + +The root HTTP router is a service that allows you to register routes on the root of the backend service. This is useful for things like health checks, or other routes that you want to expose on the root of the backend service. It is used as the base router that backs the `httpRouter` service. Most likely you won't need to use this service directly, but rather use the `httpRouter` service. + +### Using the service + +The following example shows how to get the root HTTP router service in your `example` backend plugin to register a health check route. + +```ts +import { + coreServices, + createBackendPlugin, +} from '@backstage/backend-plugin-api'; +import { Router } from 'express'; + +createBackendPlugin({ + id: 'example', + register(env) { + env.registerInit({ + deps: { + rootRouter: coreServices.rootRouter, + }, + async init({ rootRouter }) { + const router = Router(); + router.get('/health', (request, response) => { + response.send('OK'); + }); + + rootRouter.use(router); + }, + }); + }, +}); +``` + +### Configuring the service + +There's additional options that you can pass to configure the root HTTP Router serivce. These options are passed when you call `createBackend`. + +- `indexPath` - optional path to forward all unmatched requests to. Defaults to `/api/app` which is the `app-backend` plugin responsible for serving the frontend application through the backend. + +- `configure` - this is an optional function that you can use to configure the `express` instance. This is useful if you want to add your own middleware to the root router, such as logging, or other things that you want to do before the request is handled by the backend. It's also useful to override the order in which middleware is applied. + +You can configure the root HTTP Router service by passing the options to the `createBackend` function. + +```ts +import { rootHttpRouterFactory } from '@backstage/backend-app-api'; + +const backend = createBackend({ + services: [ + rootHttpRouterFactory({ + 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()); + + // you can add you your own middleware in here + app.use(custom.logging()); + + // 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()); + }, + }), + ], +}); +``` From 947bbb0dca86ff125743e57bcbf5120ec055002e Mon Sep 17 00:00:00 2001 From: blam Date: Fri, 20 Jan 2023 14:36:14 +0100 Subject: [PATCH 11/14] chore more docs Signed-off-by: blam --- docs/backend-system/core-services/01-index.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/backend-system/core-services/01-index.md b/docs/backend-system/core-services/01-index.md index 9f01337eae..a68e492112 100644 --- a/docs/backend-system/core-services/01-index.md +++ b/docs/backend-system/core-services/01-index.md @@ -518,15 +518,15 @@ createBackendPlugin({ register(env) { env.registerInit({ deps: { - rootRouter: coreServices.rootRouter, + rootHttpRouter: coreServices.rootHttpRouter, }, - async init({ rootRouter }) { + async init({ rootHttpRouter }) { const router = Router(); router.get('/health', (request, response) => { response.send('OK'); }); - rootRouter.use(router); + rootHttpRouter.use(router); }, }); }, From 6a89a4e934a456532eeb51edce56174bb8c75407 Mon Sep 17 00:00:00 2001 From: blam Date: Fri, 20 Jan 2023 14:51:21 +0100 Subject: [PATCH 12/14] chore: last of the root deps Signed-off-by: blam --- docs/backend-system/core-services/01-index.md | 246 +++++++++++++----- 1 file changed, 174 insertions(+), 72 deletions(-) diff --git a/docs/backend-system/core-services/01-index.md b/docs/backend-system/core-services/01-index.md index a68e492112..d1e89ba69f 100644 --- a/docs/backend-system/core-services/01-index.md +++ b/docs/backend-system/core-services/01-index.md @@ -68,6 +68,78 @@ const backend = createBackend({ }); ``` +## Root HTTP Router + +The root HTTP router is a service that allows you to register routes on the root of the backend service. This is useful for things like health checks, or other routes that you want to expose on the root of the backend service. It is used as the base router that backs the `httpRouter` service. Most likely you won't need to use this service directly, but rather use the `httpRouter` service. + +### Using the service + +The following example shows how to get the root HTTP router service in your `example` backend plugin to register a health check route. + +```ts +import { + coreServices, + createBackendPlugin, +} from '@backstage/backend-plugin-api'; +import { Router } from 'express'; + +createBackendPlugin({ + id: 'example', + register(env) { + env.registerInit({ + deps: { + rootHttpRouter: coreServices.rootHttpRouter, + }, + async init({ rootHttpRouter }) { + const router = Router(); + router.get('/health', (request, response) => { + response.send('OK'); + }); + + rootHttpRouter.use(router); + }, + }); + }, +}); +``` + +### Configuring the service + +There's additional options that you can pass to configure the root HTTP Router serivce. These options are passed when you call `createBackend`. + +- `indexPath` - optional path to forward all unmatched requests to. Defaults to `/api/app` which is the `app-backend` plugin responsible for serving the frontend application through the backend. + +- `configure` - this is an optional function that you can use to configure the `express` instance. This is useful if you want to add your own middleware to the root router, such as logging, or other things that you want to do before the request is handled by the backend. It's also useful to override the order in which middleware is applied. + +You can configure the root HTTP Router service by passing the options to the `createBackend` function. + +```ts +import { rootHttpRouterFactory } from '@backstage/backend-app-api'; + +const backend = createBackend({ + services: [ + rootHttpRouterFactory({ + 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()); + + // you can add you your own middleware in here + app.use(custom.logging()); + + // 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 This service allows you to read configuration values out of your `app-config` YAML files. @@ -155,6 +227,49 @@ createBackendPlugin({ }); ``` +### Root Logger + +The root logger is the logger that is used by other root services. It's where the implemenation lies for creating child loggers around the backstage ecosystem including child loggers for plugins with the correct metadata and annotations. + +If you want to override the implementation for logging across all of the backend, this is the service that you should override. + +### Configuring the service + +The following example is how you can override the root logger service to add additional metadata to all log lines. + +```ts +import { coreServices } from '@backstage/backend-plugin-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()], + }); + + return logger; + }, + }), + ], +}); +``` + ## Cache This service lets your plugin interact with a cache. It is bound to your plugin too, so that you will only set and get values in your plugin's private namespace. @@ -371,6 +486,65 @@ createBackendPlugin({ }); ``` +## Root Lifecycle + +This service is the same as the lifecycle service, but should only be used by the root services. This is also where the implementation for the actual lifecycle hooks torn, so if you want to override the implementation of how the lifecycle hooks are executed, you should override this service. + +### Configure the service + +The following example shows how to override the default implementation of the lifecycle service with something that listens on different process events to the original. + +```ts +class MyCustomLifecycleService implements RootLifecycleService { + constructor(private readonly logger: LoggerService) { + ['SIGKILL', 'SIGTERM'].map(signal => + process.on(signal, () => this.shutdown()), + ); + } + + #isCalled = false; + #shutdownTasks: Array = []; + + addShutdownHook(options: LifecycleServiceShutdownHook): void { + this.#shutdownTasks.push(options); + } + + async shutdown(): Promise { + if (this.#isCalled) { + return; + } + this.#isCalled = true; + + this.logger.info(`Running ${this.#shutdownTasks.length} shutdown tasks...`); + await Promise.all( + this.#shutdownTasks.map(async hook => { + const { logger = this.logger } = hook; + try { + await hook.fn(); + logger.info(`Shutdown hook succeeded`); + } catch (error) { + logger.error(`Shutdown hook failed, ${error}`); + } + }), + ); + } +} + +const backend = createBackend({ + services: [ + createServiceFactory({ + service: coreServices.rootLifecycle, + deps: { + logger: coreServices.rootLogger, + }, + async factory({ logger }) { + return new MyCustomLifecycleService(logger); + }, + }), + ], +}); +``` + ## Permissions This service allows your plugins to ask [the permissions framework](https://backstage.io/docs/permissions/overview) for authorization of user actions. @@ -497,75 +671,3 @@ createBackendPlugin({ }, }); ``` - -## Root HTTP Router - -The root HTTP router is a service that allows you to register routes on the root of the backend service. This is useful for things like health checks, or other routes that you want to expose on the root of the backend service. It is used as the base router that backs the `httpRouter` service. Most likely you won't need to use this service directly, but rather use the `httpRouter` service. - -### Using the service - -The following example shows how to get the root HTTP router service in your `example` backend plugin to register a health check route. - -```ts -import { - coreServices, - createBackendPlugin, -} from '@backstage/backend-plugin-api'; -import { Router } from 'express'; - -createBackendPlugin({ - id: 'example', - register(env) { - env.registerInit({ - deps: { - rootHttpRouter: coreServices.rootHttpRouter, - }, - async init({ rootHttpRouter }) { - const router = Router(); - router.get('/health', (request, response) => { - response.send('OK'); - }); - - rootHttpRouter.use(router); - }, - }); - }, -}); -``` - -### Configuring the service - -There's additional options that you can pass to configure the root HTTP Router serivce. These options are passed when you call `createBackend`. - -- `indexPath` - optional path to forward all unmatched requests to. Defaults to `/api/app` which is the `app-backend` plugin responsible for serving the frontend application through the backend. - -- `configure` - this is an optional function that you can use to configure the `express` instance. This is useful if you want to add your own middleware to the root router, such as logging, or other things that you want to do before the request is handled by the backend. It's also useful to override the order in which middleware is applied. - -You can configure the root HTTP Router service by passing the options to the `createBackend` function. - -```ts -import { rootHttpRouterFactory } from '@backstage/backend-app-api'; - -const backend = createBackend({ - services: [ - rootHttpRouterFactory({ - 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()); - - // you can add you your own middleware in here - app.use(custom.logging()); - - // 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()); - }, - }), - ], -}); -``` From f8bf7fa2df34251983871153d3b880d12d7922ee Mon Sep 17 00:00:00 2001 From: blam Date: Fri, 20 Jan 2023 15:20:06 +0100 Subject: [PATCH 13/14] chore: make the lord and almighty saviour vale ahppy. Signed-off-by: blam --- docs/backend-system/core-services/01-index.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/backend-system/core-services/01-index.md b/docs/backend-system/core-services/01-index.md index d1e89ba69f..fbf228a937 100644 --- a/docs/backend-system/core-services/01-index.md +++ b/docs/backend-system/core-services/01-index.md @@ -105,7 +105,7 @@ createBackendPlugin({ ### Configuring the service -There's additional options that you can pass to configure the root HTTP Router serivce. These options are passed when you call `createBackend`. +There's additional options that you can pass to configure the root HTTP Router service. These options are passed when you call `createBackend`. - `indexPath` - optional path to forward all unmatched requests to. Defaults to `/api/app` which is the `app-backend` plugin responsible for serving the frontend application through the backend. @@ -229,7 +229,7 @@ createBackendPlugin({ ### Root Logger -The root logger is the logger that is used by other root services. It's where the implemenation lies for creating child loggers around the backstage ecosystem including child loggers for plugins with the correct metadata and annotations. +The root logger is the logger that is used by other root services. It's where the implementation lies for creating child loggers around the backstage ecosystem including child loggers for plugins with the correct metadata and annotations. If you want to override the implementation for logging across all of the backend, this is the service that you should override. From 536693b2383d1cee002eb47fea1f711891299e80 Mon Sep 17 00:00:00 2001 From: blam Date: Fri, 20 Jan 2023 16:51:30 +0100 Subject: [PATCH 14/14] chore: code review comments Signed-off-by: blam --- docs/backend-system/core-services/01-index.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/backend-system/core-services/01-index.md b/docs/backend-system/core-services/01-index.md index fbf228a937..4ef9d75bde 100644 --- a/docs/backend-system/core-services/01-index.md +++ b/docs/backend-system/core-services/01-index.md @@ -239,6 +239,7 @@ The following example is how you can override the root logger service to add add ```ts import { coreServices } from '@backstage/backend-plugin-api'; +import { WinstonLogger } from '@backstage/backend-app-api'; const backend = createBackend({ services: [ @@ -488,7 +489,7 @@ createBackendPlugin({ ## Root Lifecycle -This service is the same as the lifecycle service, but should only be used by the root services. This is also where the implementation for the actual lifecycle hooks torn, so if you want to override the implementation of how the lifecycle hooks are executed, you should override this service. +This service is the same as the lifecycle service, but should only be used by the root services. This is also where the implementation for the actual lifecycle hooks are collected and executed, so if you want to override the implementation of how those are processed, you should override this service. ### Configure the service @@ -656,7 +657,7 @@ createBackendPlugin({ urlReader: coreServices.urlReader, }, async init({ urlReader }) { - const reader = await urlReader + const buffer = await urlReader .read('https://github.com/backstage/backstage/blob/master/README.md') .then(r => r.buffer());