move over most trivial services to backend-defaults

Signed-off-by: Fredrik Adelöw <freben@gmail.com>
This commit is contained in:
Fredrik Adelöw
2024-04-24 15:15:10 +02:00
parent ddfd6608c5
commit b0210ec5b6
48 changed files with 1412 additions and 52 deletions
+5 -1
View File
@@ -2,4 +2,8 @@
'@backstage/backend-defaults': patch
---
Added the `schedulerServiceFactory` and its implementation, migrated over from `@backstage/backend-app-api`
Added core service factories and implementations from
`@backstage/backend-app-api`. They are now available as subpath exports, e.g.
`@backstage/backend-defaults/scheduler` is where the service factory and default
implementation of `coreServices.scheduler` now lives. They have been marked as
deprecated in their old locations.
+4 -1
View File
@@ -2,4 +2,7 @@
'@backstage/backend-app-api': patch
---
Deprecated `schedulerServiceFactory`, which should now instead be imported from `@backstage/backend-defaults/scheduler` instead
Deprecated core service factories and implementations and moved them over to
subpath exports on `@backstage/backend-defaults` instead. E.g.
`@backstage/backend-defaults/scheduler` is where the service factory and default
implementation of `coreServices.scheduler` now lives.
+13 -13
View File
@@ -65,7 +65,7 @@ export interface Backend {
stop(): Promise<void>;
}
// @public (undocumented)
// @public @deprecated (undocumented)
export const cacheServiceFactory: () => ServiceFactory<CacheClient, 'plugin'>;
// @public (undocumented)
@@ -100,7 +100,7 @@ export interface CreateSpecializedBackendOptions {
defaultServiceFactories: ServiceFactoryOrFunction[];
}
// @public (undocumented)
// @public @deprecated (undocumented)
export const databaseServiceFactory: () => ServiceFactory<
PluginDatabaseManager,
'plugin'
@@ -121,7 +121,7 @@ export interface DefaultRootHttpRouterOptions {
indexPath?: string | false;
}
// @public (undocumented)
// @public @deprecated (undocumented)
export const discoveryServiceFactory: () => ServiceFactory<
DiscoveryService,
'plugin'
@@ -137,7 +137,7 @@ export interface ExtendedHttpServer extends http.Server {
stop(): Promise<void>;
}
// @public
// @public @deprecated
export class HostDiscovery implements DiscoveryService {
static fromConfig(
config: Config,
@@ -190,13 +190,13 @@ export type HttpServerOptions = {
};
};
// @public
// @public @deprecated
export type IdentityFactoryOptions = {
issuer?: string;
algorithms?: string[];
};
// @public (undocumented)
// @public @deprecated (undocumented)
export const identityServiceFactory: (
options?: IdentityFactoryOptions | undefined,
) => ServiceFactory<IdentityService, 'plugin'>;
@@ -208,7 +208,7 @@ export interface LifecycleMiddlewareOptions {
startupRequestPauseTimeout?: HumanDuration;
}
// @public
// @public @deprecated
export const lifecycleServiceFactory: () => ServiceFactory<
LifecycleService,
'plugin'
@@ -255,7 +255,7 @@ export interface MiddlewareFactoryOptions {
logger: LoggerService;
}
// @public (undocumented)
// @public @deprecated (undocumented)
export const permissionsServiceFactory: () => ServiceFactory<
PermissionsService,
'plugin'
@@ -270,7 +270,7 @@ export function readHelmetOptions(config?: Config): HelmetOptions;
// @public
export function readHttpServerOptions(config?: Config): HttpServerOptions;
// @public (undocumented)
// @public @deprecated (undocumented)
export interface RootConfigFactoryOptions {
argv?: string[];
remote?: Pick<RemoteConfigSourceOptions, 'reloadInterval'>;
@@ -278,7 +278,7 @@ export interface RootConfigFactoryOptions {
watch?: boolean;
}
// @public (undocumented)
// @public @deprecated (undocumented)
export const rootConfigServiceFactory: (
options?: RootConfigFactoryOptions | undefined,
) => ServiceFactory<RootConfigService, 'root'>;
@@ -314,7 +314,7 @@ export const rootHttpRouterServiceFactory: (
options?: RootHttpRouterFactoryOptions | undefined,
) => ServiceFactory<RootHttpRouterService, 'root'>;
// @public
// @public @deprecated
export const rootLifecycleServiceFactory: () => ServiceFactory<
RootLifecycleService,
'root'
@@ -332,13 +332,13 @@ export const schedulerServiceFactory: () => ServiceFactory<
'plugin'
>;
// @public (undocumented)
// @public @deprecated (undocumented)
export const tokenManagerServiceFactory: () => ServiceFactory<
TokenManagerService,
'plugin'
>;
// @public (undocumented)
// @public @deprecated (undocumented)
export const urlReaderServiceFactory: () => ServiceFactory<UrlReader, 'plugin'>;
// @public (undocumented)
@@ -20,7 +20,10 @@ import {
createServiceFactory,
} from '@backstage/backend-plugin-api';
/** @public */
/**
* @public
* @deprecated Please import from `@backstage/backend-defaults/cache` instead.
*/
export const cacheServiceFactory = createServiceFactory({
service: coreServices.cache,
deps: {
@@ -23,7 +23,10 @@ import {
RemoteConfigSourceOptions,
} from '@backstage/config-loader';
/** @public */
/**
* @public
* @deprecated Please import from `@backstage/backend-defaults/rootConfig` instead.
*/
export interface RootConfigFactoryOptions {
/**
* Process arguments to use instead of the default `process.argv()`.
@@ -37,7 +40,10 @@ export interface RootConfigFactoryOptions {
watch?: boolean;
}
/** @public */
/**
* @public
* @deprecated Please import from `@backstage/backend-defaults/rootConfig` instead.
*/
export const rootConfigServiceFactory = createServiceFactory(
(options?: RootConfigFactoryOptions) => ({
service: coreServices.rootConfig,
@@ -21,7 +21,10 @@ import {
} from '@backstage/backend-plugin-api';
import { ConfigReader } from '@backstage/config';
/** @public */
/**
* @public
* @deprecated Please import from `@backstage/backend-defaults/database` instead.
*/
export const databaseServiceFactory = createServiceFactory({
service: coreServices.database,
deps: {
@@ -29,6 +29,7 @@ type Target = string | { internal: string; external: string };
* resolved to the same host, so there won't be any balancing of internal traffic.
*
* @public
* @deprecated Please import from `@backstage/backend-defaults/discovery` instead.
*/
export class HostDiscovery implements DiscoveryService {
/**
@@ -20,7 +20,10 @@ import {
} from '@backstage/backend-plugin-api';
import { HostDiscovery } from './HostDiscovery';
/** @public */
/**
* @public
* @deprecated Please import from `@backstage/backend-defaults/discovery` instead.
*/
export const discoveryServiceFactory = createServiceFactory({
service: coreServices.discovery,
deps: {
@@ -24,16 +24,22 @@ import { DefaultIdentityClient } from '@backstage/plugin-auth-node';
* An identity client options object which allows extra configurations
*
* @public
* @deprecated Please migrate to the new `coreServices.auth`, `coreServices.httpAuth`, and `coreServices.userInfo` services as needed instead
*/
export type IdentityFactoryOptions = {
issuer?: string;
/** JWS "alg" (Algorithm) Header Parameter values. Defaults to an array containing just ES256.
* More info on supported algorithms: https://github.com/panva/jose */
/**
* JWS "alg" (Algorithm) Header Parameter values. Defaults to an array containing just ES256.
* More info on supported algorithms: https://github.com/panva/jose
*/
algorithms?: string[];
};
/** @public */
/**
* @public
* @deprecated Please migrate to the new `coreServices.auth`, `coreServices.httpAuth`, and `coreServices.userInfo` services as needed instead
*/
export const identityServiceFactory = createServiceFactory(
(options?: IdentityFactoryOptions) => ({
service: coreServices.identity,
@@ -26,7 +26,10 @@ import {
createServiceFactory,
} from '@backstage/backend-plugin-api';
/** @internal */
/**
* @internal
* @deprecated
*/
export class BackendPluginLifecycleImpl implements LifecycleService {
constructor(
private readonly logger: LoggerService,
@@ -85,7 +88,9 @@ export class BackendPluginLifecycleImpl implements LifecycleService {
/**
* Allows plugins to register shutdown hooks that are run when the process is about to exit.
*
* @public
* @deprecated Please import from `@backstage/backend-defaults/lifecycle` instead.
*/
export const lifecycleServiceFactory = createServiceFactory({
service: coreServices.lifecycle,
@@ -20,7 +20,10 @@ import {
} from '@backstage/backend-plugin-api';
import { ServerPermissionClient } from '@backstage/plugin-permission-node';
/** @public */
/**
* @public
* @deprecated Please import from `@backstage/backend-defaults/permissions` instead.
*/
export const permissionsServiceFactory = createServiceFactory({
service: coreServices.permissions,
deps: {
@@ -25,7 +25,10 @@ import {
LoggerService,
} from '@backstage/backend-plugin-api';
/** @internal */
/**
* @internal
* @deprecated
*/
export class BackendLifecycleImpl implements RootLifecycleService {
constructor(private readonly logger: LoggerService) {}
@@ -108,6 +111,7 @@ export class BackendLifecycleImpl implements RootLifecycleService {
* Allows plugins to register shutdown hooks that are run when the process is about to exit.
*
* @public
* @deprecated Please import from `@backstage/backend-defaults/rootLifecycle` instead.
*/
export const rootLifecycleServiceFactory = createServiceFactory({
service: coreServices.rootLifecycle,
@@ -20,7 +20,10 @@ import {
} from '@backstage/backend-plugin-api';
import { ServerTokenManager } from '@backstage/backend-common';
/** @public */
/**
* @public
* @deprecated Please migrate to the new `coreServices.auth`, `coreServices.httpAuth`, and `coreServices.userInfo` services as needed instead
*/
export const tokenManagerServiceFactory = createServiceFactory({
service: coreServices.tokenManager,
deps: {
@@ -20,7 +20,10 @@ import {
createServiceFactory,
} from '@backstage/backend-plugin-api';
/** @public */
/**
* @public
* @deprecated Please import from `@backstage/backend-defaults/urlReader` instead.
*/
export const urlReaderServiceFactory = createServiceFactory({
service: coreServices.urlReader,
deps: {
@@ -20,9 +20,9 @@ import {
coreServices,
ServiceRef,
ServiceFactory,
LifecycleService,
RootLifecycleService,
} from '@backstage/backend-plugin-api';
import { BackendLifecycleImpl } from '../services/implementations/rootLifecycle/rootLifecycleServiceFactory';
import { BackendPluginLifecycleImpl } from '../services/implementations/lifecycle/lifecycleServiceFactory';
import { ServiceOrExtensionPoint } from './types';
// Direct internal import to avoid duplication
// eslint-disable-next-line @backstage/no-forbidden-package-imports
@@ -345,27 +345,42 @@ export class BackendInitializer {
}
// Bit of a hacky way to grab the lifecycle services, potentially find a nicer way to do this
async #getRootLifecycleImpl(): Promise<BackendLifecycleImpl> {
async #getRootLifecycleImpl(): Promise<
RootLifecycleService & {
startup(): Promise<void>;
shutdown(): Promise<void>;
}
> {
const lifecycleService = await this.#serviceRegistry.get(
coreServices.rootLifecycle,
'root',
);
if (lifecycleService instanceof BackendLifecycleImpl) {
return lifecycleService;
const service = lifecycleService as any;
if (
service &&
typeof service.startup === 'function' &&
typeof service.shutdown === 'function'
) {
return service;
}
throw new Error('Unexpected root lifecycle service implementation');
}
async #getPluginLifecycleImpl(
pluginId: string,
): Promise<BackendPluginLifecycleImpl> {
): Promise<LifecycleService & { startup(): Promise<void> }> {
const lifecycleService = await this.#serviceRegistry.get(
coreServices.lifecycle,
pluginId,
);
if (lifecycleService instanceof BackendPluginLifecycleImpl) {
return lifecycleService;
const service = lifecycleService as any;
if (service && typeof service.startup === 'function') {
return service;
}
throw new Error('Unexpected plugin lifecycle service implementation');
}
}
@@ -0,0 +1,13 @@
## API Report File for "@backstage/backend-defaults"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { CacheClient } from '@backstage/backend-common';
import { ServiceFactory } from '@backstage/backend-plugin-api';
// @public (undocumented)
export const cacheServiceFactory: () => ServiceFactory<CacheClient, 'plugin'>;
// (No @packageDocumentation comment for this package)
```
@@ -0,0 +1,16 @@
## API Report File for "@backstage/backend-defaults"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { PluginDatabaseManager } from '@backstage/backend-common';
import { ServiceFactory } from '@backstage/backend-plugin-api';
// @public (undocumented)
export const databaseServiceFactory: () => ServiceFactory<
PluginDatabaseManager,
'plugin'
>;
// (No @packageDocumentation comment for this package)
```
@@ -0,0 +1,31 @@
## API Report File for "@backstage/backend-defaults"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { Config } from '@backstage/config';
import { DiscoveryService } from '@backstage/backend-plugin-api';
import { ServiceFactory } from '@backstage/backend-plugin-api';
// @public (undocumented)
export const discoveryServiceFactory: () => ServiceFactory<
DiscoveryService,
'plugin'
>;
// @public
export class HostDiscovery implements DiscoveryService {
static fromConfig(
config: Config,
options?: {
basePath?: string;
},
): HostDiscovery;
// (undocumented)
getBaseUrl(pluginId: string): Promise<string>;
// (undocumented)
getExternalBaseUrl(pluginId: string): Promise<string>;
}
// (No @packageDocumentation comment for this package)
```
@@ -0,0 +1,16 @@
## API Report File for "@backstage/backend-defaults"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { LifecycleService } from '@backstage/backend-plugin-api';
import { ServiceFactory } from '@backstage/backend-plugin-api';
// @public
export const lifecycleServiceFactory: () => ServiceFactory<
LifecycleService,
'plugin'
>;
// (No @packageDocumentation comment for this package)
```
@@ -0,0 +1,16 @@
## API Report File for "@backstage/backend-defaults"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { PermissionsService } from '@backstage/backend-plugin-api';
import { ServiceFactory } from '@backstage/backend-plugin-api';
// @public (undocumented)
export const permissionsServiceFactory: () => ServiceFactory<
PermissionsService,
'plugin'
>;
// (No @packageDocumentation comment for this package)
```
@@ -0,0 +1,24 @@
## API Report File for "@backstage/backend-defaults"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { RemoteConfigSourceOptions } from '@backstage/config-loader';
import { RootConfigService } from '@backstage/backend-plugin-api';
import { ServiceFactory } from '@backstage/backend-plugin-api';
// @public (undocumented)
export interface RootConfigFactoryOptions {
argv?: string[];
remote?: Pick<RemoteConfigSourceOptions, 'reloadInterval'>;
// (undocumented)
watch?: boolean;
}
// @public (undocumented)
export const rootConfigServiceFactory: (
options?: RootConfigFactoryOptions | undefined,
) => ServiceFactory<RootConfigService, 'root'>;
// (No @packageDocumentation comment for this package)
```
@@ -0,0 +1,16 @@
## API Report File for "@backstage/backend-defaults"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { RootLifecycleService } from '@backstage/backend-plugin-api';
import { ServiceFactory } from '@backstage/backend-plugin-api';
// @public
export const rootLifecycleServiceFactory: () => ServiceFactory<
RootLifecycleService,
'root'
>;
// (No @packageDocumentation comment for this package)
```
@@ -0,0 +1,13 @@
## API Report File for "@backstage/backend-defaults"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { ServiceFactory } from '@backstage/backend-plugin-api';
import { UrlReader } from '@backstage/backend-common';
// @public (undocumented)
export const urlReaderServiceFactory: () => ServiceFactory<UrlReader, 'plugin'>;
// (No @packageDocumentation comment for this package)
```
+39
View File
@@ -0,0 +1,39 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export interface Config {
/**
* Options used by the default discovery service.
*/
discovery?: {
/**
* A list of target baseUrls and the associated plugins.
*/
endpoints: Array<{
/**
* The target base URL to use for the plugin.
*
* Can be either a string or an object with internal and external keys.
* Targets with `{{pluginId}}` or `{{ pluginId }} in the URL will be replaced with the plugin ID.
*/
target: string | { internal: string; external: string };
/**
* Array of plugins which use the target base URL.
*/
plugins: string[];
}>;
};
}
+38 -1
View File
@@ -20,22 +20,55 @@
"license": "Apache-2.0",
"exports": {
".": "./src/index.ts",
"./cache": "./src/entrypoints/cache/index.ts",
"./database": "./src/entrypoints/database/index.ts",
"./discovery": "./src/entrypoints/discovery/index.ts",
"./lifecycle": "./src/entrypoints/lifecycle/index.ts",
"./permissions": "./src/entrypoints/permissions/index.ts",
"./rootConfig": "./src/entrypoints/rootConfig/index.ts",
"./rootLifecycle": "./src/entrypoints/rootLifecycle/index.ts",
"./scheduler": "./src/entrypoints/scheduler/index.ts",
"./urlReader": "./src/entrypoints/urlReader/index.ts",
"./package.json": "./package.json"
},
"main": "src/index.ts",
"types": "src/index.ts",
"typesVersions": {
"*": {
"cache": [
"src/entrypoints/cache/index.ts"
],
"database": [
"src/entrypoints/database/index.ts"
],
"discovery": [
"src/entrypoints/discovery/index.ts"
],
"lifecycle": [
"src/entrypoints/lifecycle/index.ts"
],
"permissions": [
"src/entrypoints/permissions/index.ts"
],
"rootConfig": [
"src/entrypoints/rootConfig/index.ts"
],
"rootLifecycle": [
"src/entrypoints/rootLifecycle/index.ts"
],
"scheduler": [
"src/entrypoints/scheduler/index.ts"
],
"urlReader": [
"src/entrypoints/urlReader/index.ts"
],
"package.json": [
"package.json"
]
}
},
"files": [
"config.d.ts",
"dist",
"migrations"
],
@@ -52,8 +85,11 @@
"@backstage/backend-app-api": "workspace:^",
"@backstage/backend-common": "workspace:^",
"@backstage/backend-plugin-api": "workspace:^",
"@backstage/config": "workspace:^",
"@backstage/config-loader": "workspace:^",
"@backstage/errors": "workspace:^",
"@backstage/plugin-events-node": "workspace:^",
"@backstage/plugin-permission-node": "workspace:^",
"@backstage/types": "workspace:^",
"@opentelemetry/api": "^1.3.0",
"cron": "^3.0.0",
@@ -68,5 +104,6 @@
"@backstage/backend-test-utils": "workspace:^",
"@backstage/cli": "workspace:^",
"wait-for-expect": "^3.0.2"
}
},
"configSchema": "config.d.ts"
}
+13 -13
View File
@@ -16,27 +16,27 @@
import {
Backend,
cacheServiceFactory,
rootConfigServiceFactory,
authServiceFactory,
createSpecializedBackend,
databaseServiceFactory,
discoveryServiceFactory,
httpAuthServiceFactory,
httpRouterServiceFactory,
rootHttpRouterServiceFactory,
lifecycleServiceFactory,
rootLifecycleServiceFactory,
identityServiceFactory,
loggerServiceFactory,
permissionsServiceFactory,
rootHttpRouterServiceFactory,
rootLoggerServiceFactory,
tokenManagerServiceFactory,
urlReaderServiceFactory,
identityServiceFactory,
authServiceFactory,
httpAuthServiceFactory,
userInfoServiceFactory,
} from '@backstage/backend-app-api';
import { eventsServiceFactory } from '@backstage/plugin-events-node';
import { cacheServiceFactory } from '@backstage/backend-defaults/cache';
import { databaseServiceFactory } from '@backstage/backend-defaults/database';
import { discoveryServiceFactory } from '@backstage/backend-defaults/discovery';
import { lifecycleServiceFactory } from '@backstage/backend-defaults/lifecycle';
import { permissionsServiceFactory } from '@backstage/backend-defaults/permissions';
import { rootConfigServiceFactory } from '@backstage/backend-defaults/rootConfig';
import { rootLifecycleServiceFactory } from '@backstage/backend-defaults/rootLifecycle';
import { schedulerServiceFactory } from '@backstage/backend-defaults/scheduler';
import { urlReaderServiceFactory } from '@backstage/backend-defaults/urlReader';
import { eventsServiceFactory } from '@backstage/plugin-events-node';
export const defaultServiceFactories = [
authServiceFactory(),
@@ -0,0 +1,38 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { CacheManager } from '@backstage/backend-common';
import {
coreServices,
createServiceFactory,
} from '@backstage/backend-plugin-api';
/**
* @public
*/
export const cacheServiceFactory = createServiceFactory({
service: coreServices.cache,
deps: {
config: coreServices.rootConfig,
plugin: coreServices.pluginMetadata,
},
async createRootContext({ config }) {
return CacheManager.fromConfig(config);
},
async factory({ plugin }, manager) {
return manager.forPlugin(plugin.getId()).getClient();
},
});
@@ -0,0 +1,17 @@
/*
* Copyright 2023 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { cacheServiceFactory } from './cacheServiceFactory';
@@ -0,0 +1,51 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { DatabaseManager } from '@backstage/backend-common';
import {
coreServices,
createServiceFactory,
} from '@backstage/backend-plugin-api';
import { ConfigReader } from '@backstage/config';
/**
* @public
*/
export const databaseServiceFactory = createServiceFactory({
service: coreServices.database,
deps: {
config: coreServices.rootConfig,
lifecycle: coreServices.lifecycle,
pluginMetadata: coreServices.pluginMetadata,
},
async createRootContext({ config }) {
return config.getOptional('backend.database')
? DatabaseManager.fromConfig(config)
: DatabaseManager.fromConfig(
new ConfigReader({
backend: {
database: { client: 'better-sqlite3', connection: ':memory:' },
},
}),
);
},
async factory({ pluginMetadata, lifecycle }, databaseManager) {
return databaseManager.forPlugin(pluginMetadata.getId(), {
pluginMetadata,
lifecycle,
});
},
});
@@ -0,0 +1,17 @@
/*
* Copyright 2023 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { databaseServiceFactory } from './databaseServiceFactory';
@@ -0,0 +1,257 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { ConfigReader } from '@backstage/config';
import { HostDiscovery } from './HostDiscovery';
describe('HostDiscovery', () => {
it('is created from config', async () => {
const discovery = HostDiscovery.fromConfig(
new ConfigReader({
backend: {
baseUrl: 'http://localhost:40',
listen: { port: 80, host: 'localhost' },
},
}),
);
await expect(discovery.getBaseUrl('catalog')).resolves.toBe(
'http://localhost:80/api/catalog',
);
await expect(discovery.getExternalBaseUrl('catalog')).resolves.toBe(
'http://localhost:40/api/catalog',
);
});
it('strips trailing slashes in config', async () => {
const discovery = HostDiscovery.fromConfig(
new ConfigReader({
backend: {
baseUrl: 'http://localhost:40//',
listen: { port: 80, host: 'localhost' },
},
}),
);
await expect(discovery.getBaseUrl('catalog')).resolves.toBe(
'http://localhost:80/api/catalog',
);
await expect(discovery.getExternalBaseUrl('catalog')).resolves.toBe(
'http://localhost:40/api/catalog',
);
});
it('can configure the base path', async () => {
const discovery = HostDiscovery.fromConfig(
new ConfigReader({
backend: {
baseUrl: 'http://localhost:40',
listen: { port: 80, host: 'localhost' },
},
}),
{ basePath: '/service' },
);
await expect(discovery.getBaseUrl('catalog')).resolves.toBe(
'http://localhost:80/service/catalog',
);
await expect(discovery.getExternalBaseUrl('catalog')).resolves.toBe(
'http://localhost:40/service/catalog',
);
});
it.each([
[{ listen: ':80' }, 'http://localhost:80'],
[{ listen: ':40', https: true }, 'https://localhost:40'],
[{ listen: '127.0.0.1:80' }, 'http://127.0.0.1:80'],
[{ listen: '127.0.0.1:80', https: true }, 'https://127.0.0.1:80'],
[{ listen: '0.0.0.0:40' }, 'http://127.0.0.1:40'],
[{ listen: { port: 80 } }, 'http://localhost:80'],
[{ listen: { port: 8000 } }, 'http://localhost:8000'],
[{ listen: { port: 80, host: '0.0.0.0' } }, 'http://127.0.0.1:80'],
[{ listen: { port: 80, host: '::' } }, 'http://localhost:80'],
[{ listen: { port: 80, host: '::1' } }, 'http://[::1]:80'],
[{ listen: { port: 90, host: '::2' }, https: true }, 'https://[::2]:90'],
])('resolves internal baseUrl for %j as %s', async (config, expected) => {
const discovery = HostDiscovery.fromConfig(
new ConfigReader({
backend: {
baseUrl: 'http://localhost:40',
...config,
},
}),
);
await expect(discovery.getBaseUrl('catalog')).resolves.toBe(
`${expected}/api/catalog`,
);
});
it('uses plugin specific targets from config if provided', async () => {
const discovery = HostDiscovery.fromConfig(
new ConfigReader({
backend: {
baseUrl: 'http://localhost:40',
listen: { port: 80, host: 'localhost' },
},
discovery: {
endpoints: [
{
target: {
internal: 'http://catalog-backend-internal:8080/api/catalog',
external: 'http://catalog-backend-external:8080/api/catalog',
},
plugins: ['catalog'],
},
],
},
}),
);
await expect(discovery.getBaseUrl('catalog')).resolves.toBe(
'http://catalog-backend-internal:8080/api/catalog',
);
await expect(discovery.getExternalBaseUrl('catalog')).resolves.toBe(
'http://catalog-backend-external:8080/api/catalog',
);
});
it('uses a single target for internal and external for a plugin', async () => {
const discovery = HostDiscovery.fromConfig(
new ConfigReader({
backend: {
baseUrl: 'http://localhost:40',
listen: { port: 80, host: 'localhost' },
},
discovery: {
endpoints: [
{
target: 'http://catalog-backend:8080/api/catalog',
plugins: ['catalog'],
},
],
},
}),
);
await expect(discovery.getBaseUrl('catalog')).resolves.toBe(
'http://catalog-backend:8080/api/catalog',
);
await expect(discovery.getExternalBaseUrl('catalog')).resolves.toBe(
'http://catalog-backend:8080/api/catalog',
);
});
it('defaults to the backend baseUrl when there is not an endpoint for a plugin', async () => {
const discovery = HostDiscovery.fromConfig(
new ConfigReader({
backend: {
baseUrl: 'http://localhost:40',
listen: { port: 80, host: 'localhost' },
},
discovery: {
endpoints: [
{
target: 'http://catalog-backend:8080/api/catalog',
plugins: ['catalog'],
},
],
},
}),
);
await expect(discovery.getBaseUrl('scaffolder')).resolves.toBe(
'http://localhost:80/api/scaffolder',
);
await expect(discovery.getExternalBaseUrl('scaffolder')).resolves.toBe(
'http://localhost:40/api/scaffolder',
);
});
it('replaces {{pluginId}} or {{ pluginId }} in the target', async () => {
const discovery = HostDiscovery.fromConfig(
new ConfigReader({
backend: {
baseUrl: 'http://localhost:40',
listen: { port: 80, host: 'localhost' },
},
discovery: {
endpoints: [
{
target: 'http://common-backend:8080/api/{{pluginId}}',
plugins: ['catalog', 'docs'],
},
{
target: {
internal: 'http://scaffolder-internal:8080/api/{{ pluginId }}',
external: 'http://scaffolder-external:8080/api/{{ pluginId }}',
},
plugins: ['scaffolder'],
},
],
},
}),
);
await expect(discovery.getBaseUrl('catalog')).resolves.toBe(
'http://common-backend:8080/api/catalog',
);
await expect(discovery.getExternalBaseUrl('catalog')).resolves.toBe(
'http://common-backend:8080/api/catalog',
);
await expect(discovery.getBaseUrl('docs')).resolves.toBe(
'http://common-backend:8080/api/docs',
);
await expect(discovery.getExternalBaseUrl('docs')).resolves.toBe(
'http://common-backend:8080/api/docs',
);
await expect(discovery.getBaseUrl('scaffolder')).resolves.toBe(
'http://scaffolder-internal:8080/api/scaffolder',
);
await expect(discovery.getExternalBaseUrl('scaffolder')).resolves.toBe(
'http://scaffolder-external:8080/api/scaffolder',
);
});
it('encodes the pluginId', async () => {
const discovery = HostDiscovery.fromConfig(
new ConfigReader({
backend: {
baseUrl: 'http://localhost:40',
listen: { port: 80, host: 'localhost' },
},
discovery: {
endpoints: [
{
target: 'http://common-backend:8080/api/{{pluginId}}',
plugins: ['plugin/beta'],
},
],
},
}),
);
await expect(discovery.getBaseUrl('plugin/beta')).resolves.toBe(
'http://common-backend:8080/api/plugin%2Fbeta',
);
await expect(discovery.getBaseUrl('plugin/alpha')).resolves.toBe(
'http://localhost:80/api/plugin%2Falpha',
);
await expect(discovery.getExternalBaseUrl('plugin/alpha')).resolves.toBe(
'http://localhost:40/api/plugin%2Falpha',
);
});
});
@@ -0,0 +1,132 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Config } from '@backstage/config';
import { readHttpServerOptions } from '@backstage/backend-app-api';
import { DiscoveryService } from '@backstage/backend-plugin-api';
type Target = string | { internal: string; external: string };
/**
* HostDiscovery is a basic PluginEndpointDiscovery implementation
* that can handle plugins that are hosted in a single or multiple deployments.
*
* The deployment may be scaled horizontally, as long as the external URL
* is the same for all instances. However, internal URLs will always be
* resolved to the same host, so there won't be any balancing of internal traffic.
*
* @public
*/
export class HostDiscovery implements DiscoveryService {
/**
* Creates a new HostDiscovery discovery instance by reading
* from the `backend` config section, specifically the `.baseUrl` for
* discovering the external URL, and the `.listen` and `.https` config
* for the internal one.
*
* Can be overridden in config by providing a target and corresponding plugins in `discovery.endpoints`.
* eg.
* ```yaml
* discovery:
* endpoints:
* - target: https://internal.example.com/internal-catalog
* plugins: [catalog]
* - target: https://internal.example.com/secure/api/{{pluginId}}
* plugins: [auth, permission]
* - target:
* internal: https://internal.example.com/search
* external: https://example.com/search
* plugins: [search]
* ```
*
* The basePath defaults to `/api`, meaning the default full internal
* path for the `catalog` plugin will be `http://localhost:7007/api/catalog`.
*/
static fromConfig(config: Config, options?: { basePath?: string }) {
const basePath = options?.basePath ?? '/api';
const externalBaseUrl = config
.getString('backend.baseUrl')
.replace(/\/+$/, '');
const {
listen: { host: listenHost = '::', port: listenPort },
} = readHttpServerOptions(config.getConfig('backend'));
const protocol = config.has('backend.https') ? 'https' : 'http';
// Translate bind-all to localhost, and support IPv6
let host = listenHost;
if (host === '::' || host === '') {
// We use localhost instead of ::1, since IPv6-compatible systems should default
// to using IPv6 when they see localhost, but if the system doesn't support IPv6
// things will still work.
host = 'localhost';
} else if (host === '0.0.0.0') {
host = '127.0.0.1';
}
if (host.includes(':')) {
host = `[${host}]`;
}
const internalBaseUrl = `${protocol}://${host}:${listenPort}`;
return new HostDiscovery(
internalBaseUrl + basePath,
externalBaseUrl + basePath,
config.getOptionalConfig('discovery'),
);
}
private constructor(
private readonly internalBaseUrl: string,
private readonly externalBaseUrl: string,
private readonly discoveryConfig: Config | undefined,
) {}
private getTargetFromConfig(pluginId: string, type: 'internal' | 'external') {
const endpoints = this.discoveryConfig?.getOptionalConfigArray('endpoints');
const target = endpoints
?.find(endpoint => endpoint.getStringArray('plugins').includes(pluginId))
?.get<Target>('target');
if (!target) {
const baseUrl =
type === 'external' ? this.externalBaseUrl : this.internalBaseUrl;
return `${baseUrl}/${encodeURIComponent(pluginId)}`;
}
if (typeof target === 'string') {
return target.replace(
/\{\{\s*pluginId\s*\}\}/g,
encodeURIComponent(pluginId),
);
}
return target[type].replace(
/\{\{\s*pluginId\s*\}\}/g,
encodeURIComponent(pluginId),
);
}
async getBaseUrl(pluginId: string): Promise<string> {
return this.getTargetFromConfig(pluginId, 'internal');
}
async getExternalBaseUrl(pluginId: string): Promise<string> {
return this.getTargetFromConfig(pluginId, 'external');
}
}
@@ -0,0 +1,32 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
coreServices,
createServiceFactory,
} from '@backstage/backend-plugin-api';
import { HostDiscovery } from './HostDiscovery';
/** @public */
export const discoveryServiceFactory = createServiceFactory({
service: coreServices.discovery,
deps: {
config: coreServices.rootConfig,
},
async factory({ config }) {
return HostDiscovery.fromConfig(config);
},
});
@@ -0,0 +1,18 @@
/*
* Copyright 2023 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { discoveryServiceFactory } from './discoveryServiceFactory';
export { HostDiscovery } from './HostDiscovery';
@@ -0,0 +1,17 @@
/*
* Copyright 2023 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { lifecycleServiceFactory } from './lifecycleServiceFactory';
@@ -0,0 +1,106 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
LifecycleService,
LifecycleServiceShutdownHook,
LifecycleServiceShutdownOptions,
LifecycleServiceStartupHook,
LifecycleServiceStartupOptions,
LoggerService,
PluginMetadataService,
RootLifecycleService,
coreServices,
createServiceFactory,
} from '@backstage/backend-plugin-api';
/** @internal */
export class BackendPluginLifecycleImpl implements LifecycleService {
constructor(
private readonly logger: LoggerService,
private readonly rootLifecycle: RootLifecycleService,
private readonly pluginMetadata: PluginMetadataService,
) {}
#hasStarted = false;
#startupTasks: Array<{
hook: LifecycleServiceStartupHook;
options?: LifecycleServiceStartupOptions;
}> = [];
addStartupHook(
hook: LifecycleServiceStartupHook,
options?: LifecycleServiceStartupOptions,
): void {
if (this.#hasStarted) {
throw new Error('Attempted to add startup hook after startup');
}
this.#startupTasks.push({ hook, options });
}
async startup(): Promise<void> {
if (this.#hasStarted) {
return;
}
this.#hasStarted = true;
this.logger.debug(
`Running ${this.#startupTasks.length} plugin startup tasks...`,
);
await Promise.all(
this.#startupTasks.map(async ({ hook, options }) => {
const logger = options?.logger ?? this.logger;
try {
await hook();
logger.debug(`Plugin startup hook succeeded`);
} catch (error) {
logger.error(`Plugin startup hook failed, ${error}`);
}
}),
);
}
addShutdownHook(
hook: LifecycleServiceShutdownHook,
options?: LifecycleServiceShutdownOptions,
): void {
const plugin = this.pluginMetadata.getId();
this.rootLifecycle.addShutdownHook(hook, {
logger: options?.logger?.child({ plugin }) ?? this.logger,
});
}
}
/**
* Allows plugins to register shutdown hooks that are run when the process is about to exit.
*
* @public
*/
export const lifecycleServiceFactory = createServiceFactory({
service: coreServices.lifecycle,
deps: {
logger: coreServices.logger,
rootLifecycle: coreServices.rootLifecycle,
pluginMetadata: coreServices.pluginMetadata,
},
async factory({ rootLifecycle, logger, pluginMetadata }) {
return new BackendPluginLifecycleImpl(
logger,
rootLifecycle,
pluginMetadata,
);
},
});
@@ -0,0 +1,17 @@
/*
* Copyright 2023 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { permissionsServiceFactory } from './permissionsServiceFactory';
@@ -0,0 +1,41 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
coreServices,
createServiceFactory,
} from '@backstage/backend-plugin-api';
import { ServerPermissionClient } from '@backstage/plugin-permission-node';
/**
* @public
*/
export const permissionsServiceFactory = createServiceFactory({
service: coreServices.permissions,
deps: {
auth: coreServices.auth,
config: coreServices.rootConfig,
discovery: coreServices.discovery,
tokenManager: coreServices.tokenManager,
},
async factory({ auth, config, discovery, tokenManager }) {
return ServerPermissionClient.fromConfig(config, {
auth,
discovery,
tokenManager,
});
},
});
@@ -0,0 +1,18 @@
/*
* Copyright 2023 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { rootConfigServiceFactory } from './rootConfigServiceFactory';
export type { RootConfigFactoryOptions } from './rootConfigServiceFactory';
@@ -0,0 +1,59 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
coreServices,
createServiceFactory,
} from '@backstage/backend-plugin-api';
import {
ConfigSources,
RemoteConfigSourceOptions,
} from '@backstage/config-loader';
/**
* @public
*/
export interface RootConfigFactoryOptions {
/**
* Process arguments to use instead of the default `process.argv()`.
*/
argv?: string[];
/**
* Enables and sets options for remote configuration loading.
*/
remote?: Pick<RemoteConfigSourceOptions, 'reloadInterval'>;
watch?: boolean;
}
/**
* @public
*/
export const rootConfigServiceFactory = createServiceFactory(
(options?: RootConfigFactoryOptions) => ({
service: coreServices.rootConfig,
deps: {},
async factory() {
const source = ConfigSources.default({
argv: options?.argv,
remote: options?.remote,
watch: options?.watch,
});
console.log(`Loading config from ${source}`);
return await ConfigSources.toConfig(source);
},
}),
);
@@ -0,0 +1,17 @@
/*
* Copyright 2023 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { rootLifecycleServiceFactory } from './rootLifecycleServiceFactory';
@@ -0,0 +1,60 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { getVoidLogger } from '@backstage/backend-common';
import { BackendLifecycleImpl } from './rootLifecycleServiceFactory';
describe('lifecycleService', () => {
it('should execute registered shutdown hook', async () => {
const service = new BackendLifecycleImpl(getVoidLogger());
const hook = jest.fn();
service.addShutdownHook(() => hook());
// should not execute the hook more than once.
await service.shutdown();
await service.shutdown();
await service.shutdown();
expect(hook).toHaveBeenCalledTimes(1);
});
it('should not throw errors', async () => {
const service = new BackendLifecycleImpl(getVoidLogger());
service.addShutdownHook(() => {
throw new Error('oh no');
});
await expect(service.shutdown()).resolves.toBeUndefined();
});
it('should not throw async errors', async () => {
const service = new BackendLifecycleImpl(getVoidLogger());
service.addShutdownHook(async () => {
throw new Error('oh no');
});
await expect(service.shutdown()).resolves.toBeUndefined();
});
it('should reject hooks after trigger', async () => {
const service = new BackendLifecycleImpl(getVoidLogger());
await service.startup();
expect(() => {
service.addStartupHook(() => {});
}).toThrow('Attempted to add startup hook after startup');
await service.shutdown();
expect(() => {
service.addShutdownHook(() => {});
}).toThrow('Attempted to add shutdown hook after shutdown');
});
});
@@ -0,0 +1,120 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
createServiceFactory,
coreServices,
LifecycleServiceStartupHook,
LifecycleServiceStartupOptions,
LifecycleServiceShutdownHook,
LifecycleServiceShutdownOptions,
RootLifecycleService,
LoggerService,
} from '@backstage/backend-plugin-api';
/** @internal */
export class BackendLifecycleImpl implements RootLifecycleService {
constructor(private readonly logger: LoggerService) {}
#hasStarted = false;
#startupTasks: Array<{
hook: LifecycleServiceStartupHook;
options?: LifecycleServiceStartupOptions;
}> = [];
addStartupHook(
hook: LifecycleServiceStartupHook,
options?: LifecycleServiceStartupOptions,
): void {
if (this.#hasStarted) {
throw new Error('Attempted to add startup hook after startup');
}
this.#startupTasks.push({ hook, options });
}
async startup(): Promise<void> {
if (this.#hasStarted) {
return;
}
this.#hasStarted = true;
this.logger.debug(`Running ${this.#startupTasks.length} startup tasks...`);
await Promise.all(
this.#startupTasks.map(async ({ hook, options }) => {
const logger = options?.logger ?? this.logger;
try {
await hook();
logger.debug(`Startup hook succeeded`);
} catch (error) {
logger.error(`Startup hook failed, ${error}`);
}
}),
);
}
#hasShutdown = false;
#shutdownTasks: Array<{
hook: LifecycleServiceShutdownHook;
options?: LifecycleServiceShutdownOptions;
}> = [];
addShutdownHook(
hook: LifecycleServiceShutdownHook,
options?: LifecycleServiceShutdownOptions,
): void {
if (this.#hasShutdown) {
throw new Error('Attempted to add shutdown hook after shutdown');
}
this.#shutdownTasks.push({ hook, options });
}
async shutdown(): Promise<void> {
if (this.#hasShutdown) {
return;
}
this.#hasShutdown = true;
this.logger.debug(
`Running ${this.#shutdownTasks.length} shutdown tasks...`,
);
await Promise.all(
this.#shutdownTasks.map(async ({ hook, options }) => {
const logger = options?.logger ?? this.logger;
try {
await hook();
logger.debug(`Shutdown hook succeeded`);
} catch (error) {
logger.error(`Shutdown hook failed, ${error}`);
}
}),
);
}
}
/**
* Allows plugins to register shutdown hooks that are run when the process is about to exit.
*
* @public
*/
export const rootLifecycleServiceFactory = createServiceFactory({
service: coreServices.rootLifecycle,
deps: {
logger: coreServices.rootLogger,
},
async factory({ logger }) {
return new BackendLifecycleImpl(logger);
},
});
@@ -0,0 +1,17 @@
/*
* Copyright 2023 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { urlReaderServiceFactory } from './urlReaderServiceFactory';
@@ -0,0 +1,36 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { UrlReaders } from '@backstage/backend-common';
import {
coreServices,
createServiceFactory,
} from '@backstage/backend-plugin-api';
/** @public */
export const urlReaderServiceFactory = createServiceFactory({
service: coreServices.urlReader,
deps: {
config: coreServices.rootConfig,
logger: coreServices.logger,
},
async factory({ config, logger }) {
return UrlReaders.default({
config,
logger,
});
},
});
+4 -2
View File
@@ -204,9 +204,11 @@ export namespace coreServices {
const rootLifecycle: ServiceRef<RootLifecycleService, 'root'>;
const rootLogger: ServiceRef<RootLoggerService, 'root'>;
const scheduler: ServiceRef<SchedulerService, 'plugin'>;
const tokenManager: ServiceRef<TokenManagerService, 'plugin'>;
const // @deprecated
tokenManager: ServiceRef<TokenManagerService, 'plugin'>;
const urlReader: ServiceRef<UrlReaderService, 'plugin'>;
const identity: ServiceRef<IdentityService, 'plugin'>;
const // @deprecated
identity: ServiceRef<IdentityService, 'plugin'>;
}
// @public
@@ -172,6 +172,7 @@ export namespace coreServices {
* The service reference for the plugin scoped {@link TokenManagerService}.
*
* @public
* @deprecated Please migrate to the new `coreServices.auth`, `coreServices.httpAuth`, and `coreServices.userInfo` services as needed instead
*/
export const tokenManager = createServiceRef<
import('./TokenManagerService').TokenManagerService
@@ -190,6 +191,7 @@ export namespace coreServices {
* The service reference for the plugin scoped {@link IdentityService}.
*
* @public
* @deprecated Please migrate to the new `coreServices.auth`, `coreServices.httpAuth`, and `coreServices.userInfo` services as needed instead
*/
export const identity = createServiceRef<
import('./IdentityService').IdentityService
+3
View File
@@ -3433,8 +3433,11 @@ __metadata:
"@backstage/backend-plugin-api": "workspace:^"
"@backstage/backend-test-utils": "workspace:^"
"@backstage/cli": "workspace:^"
"@backstage/config": "workspace:^"
"@backstage/config-loader": "workspace:^"
"@backstage/errors": "workspace:^"
"@backstage/plugin-events-node": "workspace:^"
"@backstage/plugin-permission-node": "workspace:^"
"@backstage/types": "workspace:^"
"@opentelemetry/api": ^1.3.0
cron: ^3.0.0