Merge branch 'backstage:master' into feat/add-jwks-access-type-1
This commit is contained in:
@@ -1,5 +1,32 @@
|
||||
# @backstage/backend-app-api
|
||||
|
||||
## 0.7.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 4cd5ff0: Add ability to configure the Node.js HTTP Server when configuring the root HTTP Router service
|
||||
- e8199b1: Move the JWKS registration outside of the lifecycle middleware
|
||||
- d229dc4: Move path utilities from `backend-common` to the `backend-plugin-api` package.
|
||||
- dc8c5dd: The default `TokenManager` implementation no longer requires keys to be configured in production, but it will throw an errors when generating or authenticating tokens. The default `AuthService` implementation will now also provide additional context if such an error is throw when falling back to using the `TokenManager` service to generate tokens for outgoing requests.
|
||||
- 025641b: Redact `meta` fields too with the logger
|
||||
- 09f8988: Remove explicit `alg` check for user tokens in `verifyToken`
|
||||
- 5863e02: Internal refactor to only create one external token handler
|
||||
- a1dc547: Added support for camel case CSP directives in app-config. For example:
|
||||
|
||||
```yaml
|
||||
backend:
|
||||
csp:
|
||||
upgradeInsecureRequests: false
|
||||
```
|
||||
|
||||
- 329cc34: Added logging of all plugins being initialized, periodic status, and completion.
|
||||
- Updated dependencies
|
||||
- @backstage/backend-common@0.22.0
|
||||
- @backstage/backend-plugin-api@0.6.18
|
||||
- @backstage/backend-tasks@0.5.23
|
||||
- @backstage/plugin-auth-node@0.4.13
|
||||
- @backstage/plugin-permission-node@0.7.29
|
||||
|
||||
## 0.7.2-next.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -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'
|
||||
@@ -326,19 +326,19 @@ export const rootLoggerServiceFactory: () => ServiceFactory<
|
||||
'root'
|
||||
>;
|
||||
|
||||
// @public (undocumented)
|
||||
// @public @deprecated (undocumented)
|
||||
export const schedulerServiceFactory: () => ServiceFactory<
|
||||
SchedulerService,
|
||||
'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)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@backstage/backend-app-api",
|
||||
"version": "0.7.3-next.1",
|
||||
"version": "0.7.3",
|
||||
"description": "Core API used by Backstage backend apps",
|
||||
"backstage": {
|
||||
"role": "node-library"
|
||||
@@ -83,6 +83,7 @@
|
||||
"path-to-regexp": "^6.2.1",
|
||||
"selfsigned": "^2.0.0",
|
||||
"stoppable": "^1.1.0",
|
||||
"triple-beam": "^1.4.1",
|
||||
"uuid": "^9.0.0",
|
||||
"winston": "^3.2.1",
|
||||
"winston-transport": "^4.5.0"
|
||||
|
||||
@@ -47,7 +47,8 @@ describe('readHelmetOptions', () => {
|
||||
csp: {
|
||||
key: ['value'],
|
||||
'img-src': false,
|
||||
'script-src-attr': ['custom'],
|
||||
scriptSrcAttr: ['custom'],
|
||||
'object-src': ['asd'],
|
||||
},
|
||||
});
|
||||
expect(readHelmetOptions(config)).toEqual({
|
||||
@@ -58,7 +59,7 @@ describe('readHelmetOptions', () => {
|
||||
'base-uri': ["'self'"],
|
||||
'font-src': ["'self'", 'https:', 'data:'],
|
||||
'frame-ancestors': ["'self'"],
|
||||
'object-src': ["'none'"],
|
||||
'object-src': ['asd'],
|
||||
'script-src': ["'self'", "'unsafe-eval'"],
|
||||
'style-src': ["'self'", 'https:', "'unsafe-inline'"],
|
||||
'script-src-attr': ['custom'],
|
||||
|
||||
@@ -18,6 +18,7 @@ import { Config } from '@backstage/config';
|
||||
import helmet from 'helmet';
|
||||
import { HelmetOptions } from 'helmet';
|
||||
import { ContentSecurityPolicyOptions } from 'helmet/dist/types/middlewares/content-security-policy';
|
||||
import kebabCase from 'lodash/kebabCase';
|
||||
|
||||
/**
|
||||
* Attempts to read Helmet options from the backend configuration object.
|
||||
@@ -97,10 +98,11 @@ export function applyCspDirectives(
|
||||
|
||||
if (directives) {
|
||||
for (const [key, value] of Object.entries(directives)) {
|
||||
const kebabCaseKey = kebabCase(key);
|
||||
if (value === false) {
|
||||
delete result[key];
|
||||
delete result[kebabCaseKey];
|
||||
} else {
|
||||
result[key] = value;
|
||||
result[kebabCaseKey] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,12 +14,10 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { TransformableInfo } from 'logform';
|
||||
import { format } from 'logform';
|
||||
import { WinstonLogger } from './WinstonLogger';
|
||||
|
||||
function msg(info: TransformableInfo): TransformableInfo {
|
||||
return { message: info.message, level: info.level, stack: info.stack };
|
||||
}
|
||||
import Transport from 'winston-transport';
|
||||
import { MESSAGE } from 'triple-beam';
|
||||
|
||||
describe('WinstonLogger', () => {
|
||||
it('creates a winston logger instance with default options', () => {
|
||||
@@ -33,49 +31,66 @@ describe('WinstonLogger', () => {
|
||||
expect(childLogger).toBeInstanceOf(WinstonLogger);
|
||||
});
|
||||
|
||||
it('redacter should redact and escape regex', () => {
|
||||
const redacter = WinstonLogger.redacter();
|
||||
const log = {
|
||||
level: 'error',
|
||||
message: 'hello (world)',
|
||||
stack: 'hello (world) from this file',
|
||||
};
|
||||
expect(redacter.format.transform(msg(log))).toEqual(msg(log));
|
||||
redacter.add(['hello\n']);
|
||||
expect(redacter.format.transform(msg(log))).toEqual(
|
||||
msg({
|
||||
...log,
|
||||
message: '[REDACTED] (world)',
|
||||
stack: '[REDACTED] (world) from this file',
|
||||
}),
|
||||
);
|
||||
redacter.add(['(world']);
|
||||
expect(redacter.format.transform(msg(log))).toEqual(
|
||||
msg({
|
||||
...log,
|
||||
message: '[REDACTED] [REDACTED])',
|
||||
stack: '[REDACTED] [REDACTED]) from this file',
|
||||
it('should redact and escape regex', () => {
|
||||
const mockTransport = new Transport({
|
||||
log: jest.fn(),
|
||||
logv: jest.fn(),
|
||||
});
|
||||
|
||||
const logger = WinstonLogger.create({
|
||||
format: format.json(),
|
||||
transports: [mockTransport],
|
||||
});
|
||||
|
||||
logger.addRedactions(['hello (world']);
|
||||
|
||||
logger.error('hello (world) from this file');
|
||||
|
||||
expect(mockTransport.log).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
[MESSAGE]: JSON.stringify({
|
||||
level: 'error',
|
||||
message: '***) from this file',
|
||||
}),
|
||||
}),
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
it('redacter should redact nested object', () => {
|
||||
const redacter = WinstonLogger.redacter();
|
||||
const log = {
|
||||
level: 'error',
|
||||
message: {
|
||||
nested: 'hello (world) from nested object',
|
||||
},
|
||||
};
|
||||
it('should redact nested object', () => {
|
||||
const mockTransport = new Transport({
|
||||
log: jest.fn(),
|
||||
logv: jest.fn(),
|
||||
});
|
||||
|
||||
redacter.add(['hello']);
|
||||
expect(redacter.format.transform(msg(log))).toEqual(
|
||||
msg({
|
||||
...log,
|
||||
message: {
|
||||
nested: '[REDACTED] (world) from nested object',
|
||||
},
|
||||
const logger = WinstonLogger.create({
|
||||
format: format.json(),
|
||||
transports: [mockTransport],
|
||||
});
|
||||
|
||||
logger.addRedactions(['hello']);
|
||||
|
||||
logger.error('something went wrong', {
|
||||
null: null,
|
||||
nested: 'hello (world) from nested object',
|
||||
nullProto: Object.create(null, {
|
||||
foo: { value: 'hello foo', enumerable: true },
|
||||
}),
|
||||
});
|
||||
|
||||
expect(mockTransport.log).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
[MESSAGE]: JSON.stringify({
|
||||
level: 'error',
|
||||
message: 'something went wrong',
|
||||
nested: '*** (world) from nested object',
|
||||
null: null,
|
||||
nullProto: {
|
||||
foo: '*** foo',
|
||||
},
|
||||
}),
|
||||
}),
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
transports,
|
||||
transport as Transport,
|
||||
} from 'winston';
|
||||
import { MESSAGE } from 'triple-beam';
|
||||
import { escapeRegExp } from '../lib/escapeRegExp';
|
||||
|
||||
/**
|
||||
@@ -61,8 +62,8 @@ export class WinstonLogger implements RootLoggerService {
|
||||
let logger = createLogger({
|
||||
level: process.env.LOG_LEVEL || options.level || 'info',
|
||||
format: format.combine(
|
||||
redacter.format,
|
||||
options.format ?? defaultFormatter,
|
||||
redacter.format,
|
||||
),
|
||||
transports: options.transports ?? new transports.Console(),
|
||||
});
|
||||
@@ -85,20 +86,16 @@ export class WinstonLogger implements RootLoggerService {
|
||||
|
||||
let redactionPattern: RegExp | undefined = undefined;
|
||||
|
||||
const replace = (obj: TransformableInfo) => {
|
||||
for (const key in obj) {
|
||||
if (obj.hasOwnProperty(key)) {
|
||||
if (typeof obj[key] === 'object') {
|
||||
obj[key] = replace(obj[key] as TransformableInfo);
|
||||
} else if (typeof obj[key] === 'string') {
|
||||
obj[key] = obj[key]?.replace(redactionPattern, '[REDACTED]');
|
||||
}
|
||||
}
|
||||
}
|
||||
return obj;
|
||||
};
|
||||
return {
|
||||
format: format(replace)(),
|
||||
format: format((obj: TransformableInfo) => {
|
||||
if (!redactionPattern || !obj) {
|
||||
return obj;
|
||||
}
|
||||
|
||||
obj[MESSAGE] = obj[MESSAGE]?.replace?.(redactionPattern, '***');
|
||||
|
||||
return obj;
|
||||
})(),
|
||||
add(newRedactions) {
|
||||
let added = 0;
|
||||
for (const redactionToTrim of newRedactions) {
|
||||
|
||||
@@ -14,11 +14,14 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { DatabaseService, LoggerService } from '@backstage/backend-plugin-api';
|
||||
import {
|
||||
DatabaseService,
|
||||
LoggerService,
|
||||
resolvePackagePath,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import { DateTime } from 'luxon';
|
||||
import { Knex } from 'knex';
|
||||
import { JsonObject } from '@backstage/types';
|
||||
import { resolvePackagePath } from '@backstage/backend-common';
|
||||
import { KeyStore } from './types';
|
||||
|
||||
const MIGRATIONS_TABLE = 'backstage_backend_public_keys__knex_migrations';
|
||||
|
||||
+7
-3
@@ -20,15 +20,19 @@ 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: {
|
||||
config: coreServices.rootConfig,
|
||||
logger: coreServices.rootLogger,
|
||||
plugin: coreServices.pluginMetadata,
|
||||
},
|
||||
async createRootContext({ config }) {
|
||||
return CacheManager.fromConfig(config);
|
||||
async createRootContext({ config, logger }) {
|
||||
return CacheManager.fromConfig(config, { logger });
|
||||
},
|
||||
async factory({ plugin }, manager) {
|
||||
return manager.forPlugin(plugin.getId()).getClient();
|
||||
|
||||
+8
-2
@@ -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,
|
||||
|
||||
+4
-1
@@ -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: {
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2024 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 * from './scheduler';
|
||||
@@ -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 {
|
||||
/**
|
||||
|
||||
+4
-1
@@ -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: {
|
||||
|
||||
+9
-3
@@ -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,
|
||||
|
||||
@@ -28,7 +28,8 @@ export * from './permissions';
|
||||
export * from './rootHttpRouter';
|
||||
export * from './rootLifecycle';
|
||||
export * from './rootLogger';
|
||||
export * from './scheduler';
|
||||
export * from './tokenManager';
|
||||
export * from './urlReader';
|
||||
export * from './userInfo';
|
||||
|
||||
export * from './deprecated';
|
||||
|
||||
+6
-1
@@ -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,
|
||||
|
||||
+4
-1
@@ -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: {
|
||||
|
||||
+5
-5
@@ -14,12 +14,12 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { getVoidLogger } from '@backstage/backend-common';
|
||||
import { BackendLifecycleImpl } from './rootLifecycleServiceFactory';
|
||||
import { mockServices } from '@backstage/backend-test-utils';
|
||||
|
||||
describe('lifecycleService', () => {
|
||||
it('should execute registered shutdown hook', async () => {
|
||||
const service = new BackendLifecycleImpl(getVoidLogger());
|
||||
const service = new BackendLifecycleImpl(mockServices.logger.mock());
|
||||
const hook = jest.fn();
|
||||
service.addShutdownHook(() => hook());
|
||||
// should not execute the hook more than once.
|
||||
@@ -30,7 +30,7 @@ describe('lifecycleService', () => {
|
||||
});
|
||||
|
||||
it('should not throw errors', async () => {
|
||||
const service = new BackendLifecycleImpl(getVoidLogger());
|
||||
const service = new BackendLifecycleImpl(mockServices.logger.mock());
|
||||
service.addShutdownHook(() => {
|
||||
throw new Error('oh no');
|
||||
});
|
||||
@@ -38,7 +38,7 @@ describe('lifecycleService', () => {
|
||||
});
|
||||
|
||||
it('should not throw async errors', async () => {
|
||||
const service = new BackendLifecycleImpl(getVoidLogger());
|
||||
const service = new BackendLifecycleImpl(mockServices.logger.mock());
|
||||
service.addShutdownHook(async () => {
|
||||
throw new Error('oh no');
|
||||
});
|
||||
@@ -46,7 +46,7 @@ describe('lifecycleService', () => {
|
||||
});
|
||||
|
||||
it('should reject hooks after trigger', async () => {
|
||||
const service = new BackendLifecycleImpl(getVoidLogger());
|
||||
const service = new BackendLifecycleImpl(mockServices.logger.mock());
|
||||
await service.startup();
|
||||
expect(() => {
|
||||
service.addStartupHook(() => {});
|
||||
|
||||
+5
-1
@@ -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,
|
||||
|
||||
+4
-1
@@ -20,7 +20,10 @@ import {
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import { TaskScheduler } from '@backstage/backend-tasks';
|
||||
|
||||
/** @public */
|
||||
/**
|
||||
* @public
|
||||
* @deprecated Please import from `@backstage/backend-defaults/scheduler` instead.
|
||||
*/
|
||||
export const schedulerServiceFactory = createServiceFactory({
|
||||
service: coreServices.scheduler,
|
||||
deps: {
|
||||
|
||||
+4
-1
@@ -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: {
|
||||
|
||||
+4
-1
@@ -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
|
||||
@@ -31,6 +31,7 @@ import { ForwardedError, ConflictError } from '@backstage/errors';
|
||||
import { featureDiscoveryServiceRef } from '@backstage/backend-plugin-api/alpha';
|
||||
import { DependencyGraph } from '../lib/DependencyGraph';
|
||||
import { ServiceRegistry } from './ServiceRegistry';
|
||||
import { createInitializationLogger } from './createInitializationLogger';
|
||||
|
||||
export interface BackendRegisterInit {
|
||||
consumes: Set<ServiceOrExtensionPoint>;
|
||||
@@ -226,6 +227,11 @@ export class BackendInitializer {
|
||||
|
||||
const allPluginIds = [...pluginInits.keys()];
|
||||
|
||||
const initLogger = createInitializationLogger(
|
||||
allPluginIds,
|
||||
await this.#serviceRegistry.get(coreServices.rootLogger, 'root'),
|
||||
);
|
||||
|
||||
// All plugins are initialized in parallel
|
||||
await Promise.all(
|
||||
allPluginIds.map(async pluginId => {
|
||||
@@ -289,6 +295,8 @@ export class BackendInitializer {
|
||||
});
|
||||
}
|
||||
|
||||
initLogger.onPluginStarted(pluginId);
|
||||
|
||||
// Once the plugin and all modules have been initialized, we can signal that the plugin has stared up successfully
|
||||
const lifecycleService = await this.#getPluginLifecycleImpl(pluginId);
|
||||
await lifecycleService.startup();
|
||||
@@ -299,6 +307,8 @@ export class BackendInitializer {
|
||||
const lifecycleService = await this.#getRootLifecycleImpl();
|
||||
await lifecycleService.startup();
|
||||
|
||||
initLogger.onAllStarted();
|
||||
|
||||
// Once the backend is started, any uncaught errors or unhandled rejections are caught
|
||||
// and logged, in order to avoid crashing the entire backend on local failures.
|
||||
if (process.env.NODE_ENV !== 'test') {
|
||||
@@ -335,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,79 @@
|
||||
/*
|
||||
* Copyright 2024 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 { RootLoggerService } from '@backstage/backend-plugin-api';
|
||||
|
||||
const LOGGER_INTERVAL_MAX = 60_000;
|
||||
|
||||
function joinIds(ids: Iterable<string>): string {
|
||||
return [...ids].map(id => `'${id}'`).join(', ');
|
||||
}
|
||||
|
||||
export function createInitializationLogger(
|
||||
pluginIds: string[],
|
||||
rootLogger?: RootLoggerService,
|
||||
): {
|
||||
onPluginStarted(pluginId: string): void;
|
||||
onAllStarted(): void;
|
||||
} {
|
||||
const logger = rootLogger?.child({ type: 'initialization' });
|
||||
const starting = new Set(pluginIds);
|
||||
const started = new Set<string>();
|
||||
|
||||
logger?.info(`Plugin initialization started: ${joinIds(pluginIds)}`);
|
||||
|
||||
const getInitStatus = () => {
|
||||
let status = '';
|
||||
if (started.size > 0) {
|
||||
status = `, newly initialized: ${joinIds(started)}`;
|
||||
started.clear();
|
||||
}
|
||||
if (starting.size > 0) {
|
||||
status += `, still initializing: ${joinIds(starting)}`;
|
||||
}
|
||||
return status;
|
||||
};
|
||||
|
||||
// Periodically log the initialization status with a fibonacci backoff
|
||||
let interval = 1000;
|
||||
let prevInterval = 0;
|
||||
let timeout: NodeJS.Timeout | undefined;
|
||||
const onTimeout = () => {
|
||||
logger?.info(`Plugin initialization in progress${getInitStatus()}`);
|
||||
|
||||
const nextInterval = Math.min(interval + prevInterval, LOGGER_INTERVAL_MAX);
|
||||
prevInterval = interval;
|
||||
interval = nextInterval;
|
||||
|
||||
timeout = setTimeout(onTimeout, nextInterval);
|
||||
};
|
||||
timeout = setTimeout(onTimeout, interval);
|
||||
|
||||
return {
|
||||
onPluginStarted(pluginId: string) {
|
||||
starting.delete(pluginId);
|
||||
started.add(pluginId);
|
||||
},
|
||||
onAllStarted() {
|
||||
logger?.info(`Plugin initialization complete${getInitStatus()}`);
|
||||
|
||||
if (timeout) {
|
||||
clearTimeout(timeout);
|
||||
timeout = undefined;
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user