[NBS 1.0] move more implementations from app-api to defaults

Signed-off-by: Fredrik Adelöw <freben@gmail.com>
This commit is contained in:
Fredrik Adelöw
2024-06-18 10:27:04 +02:00
parent 6fa100a902
commit 6c11f6e7e7
73 changed files with 1775 additions and 774 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/backend-test-utils': patch
---
Use imports from backend-defaults instead of the deprecated ones from backend-app-api
+1 -1
View File
@@ -17,7 +17,7 @@
"build:all": "backstage-cli repo build --all",
"build:api-docs": "LANG=en_EN yarn build:api-reports --docs --exclude 'plugins/@(adr|adr-backend|adr-common|airbrake|airbrake-backend|allure|analytics-module-ga|analytics-module-ga4|analytics-module-newrelic-browser|apache-airflow|api-docs|api-docs-module-protoc-gen-doc|apollo-explorer|app-visualizer|azure-devops|azure-devops-backend|azure-devops-common|azure-sites|azure-sites-backend|azure-sites-common|badges|badges-backend|bazaar|bazaar-backend|bitbucket-cloud-common|bitrise|catalog-graph|catalog-graphql|catalog-import|catalog-unprocessed-entities|cicd-statistics|cicd-statistics-module-gitlab|circleci|cloudbuild|code-climate|code-coverage|code-coverage-backend|codescene|config-schema|cost-insights|cost-insights-common|dynatrace|entity-feedback|entity-feedback-backend|entity-feedback-common|entity-validation|example-todo-list|example-todo-list-backend|example-todo-list-common|firehydrant|fossa|gcalendar|gcp-projects|git-release-manager|github-actions|github-deployments|github-issues|github-pull-requests-board|gitops-profiles|gocd|graphiql|graphql-backend|graphql-voyager|ilert|jenkins|jenkins-backend|jenkins-common|kafka|kafka-backend|lighthouse|lighthouse-backend|lighthouse-common|linguist|linguist-backend|linguist-common|microsoft-calendar|newrelic|newrelic-dashboard|nomad|nomad-backend|octopus-deploy|opencost|pagerduty|periskop|periskop-backend|playlist|playlist-backend|playlist-common|proxy-backend|puppetdb|rollbar|rollbar-backend|sentry|shortcuts|splunk-on-call|stack-overflow|stack-overflow-backend|stackstorm|tech-radar|tech-radar-2|todo|todo-backend|xcmetrics)'",
"build:api-reports": "yarn build:api-reports:only --tsc",
"build:api-reports:only": "NODE_OPTIONS=--max-old-space-size=8192 backstage-repo-tools api-reports --allow-warnings 'packages/backend-common,packages/core-components,plugins/+(catalog|catalog-import|git-release-manager|jenkins|kubernetes)' -o ae-wrong-input-file-type --validate-release-tags",
"build:api-reports:only": "NODE_OPTIONS=--max-old-space-size=8192 backstage-repo-tools api-reports --allow-warnings 'packages/backend-app-api,packages/backend-common,packages/core-components,plugins/+(catalog|catalog-import|git-release-manager|jenkins|kubernetes)' -o ae-wrong-input-file-type --validate-release-tags",
"build:backend": "yarn workspace example-backend build",
"build:knip-reports": "backstage-repo-tools knip-reports",
"build:plugins-report": "node ./scripts/build-plugins-report",
+81 -134
View File
@@ -68,26 +68,20 @@ export interface Backend {
// @public @deprecated (undocumented)
export const cacheServiceFactory: () => ServiceFactory<CacheService, 'plugin'>;
// @public (undocumented)
export function createConfigSecretEnumerator(options: {
logger: LoggerService;
dir?: string;
schema?: ConfigSchema;
}): Promise<(config: Config) => Iterable<string>>;
// Warning: (ae-forgotten-export) The symbol "createConfigSecretEnumerator_2" needs to be exported by the entry point index.d.ts
//
// @public @deprecated (undocumented)
export const createConfigSecretEnumerator: typeof createConfigSecretEnumerator_2;
// @public
export function createHttpServer(
listener: RequestListener,
options: HttpServerOptions,
deps: {
logger: LoggerService;
},
): Promise<ExtendedHttpServer>;
// Warning: (ae-forgotten-export) The symbol "createHttpServer_2" needs to be exported by the entry point index.d.ts
//
// @public @deprecated (undocumented)
export const createHttpServer: typeof createHttpServer_2;
// @public
export function createLifecycleMiddleware(
options: LifecycleMiddlewareOptions,
): RequestHandler;
// Warning: (ae-forgotten-export) The symbol "createLifecycleMiddleware_2" needs to be exported by the entry point index.d.ts
//
// @public @deprecated
export const createLifecycleMiddleware: typeof createLifecycleMiddleware_2;
// @public (undocumented)
export function createSpecializedBackend(
@@ -106,7 +100,7 @@ export const databaseServiceFactory: () => ServiceFactory<
'plugin'
>;
// @public
// @public @deprecated
export class DefaultRootHttpRouter implements RootHttpRouterService {
// (undocumented)
static create(options?: DefaultRootHttpRouterOptions): DefaultRootHttpRouter;
@@ -116,10 +110,10 @@ export class DefaultRootHttpRouter implements RootHttpRouterService {
use(path: string, handler: Handler): void;
}
// @public
export interface DefaultRootHttpRouterOptions {
indexPath?: string | false;
}
// Warning: (ae-forgotten-export) The symbol "DefaultRootHttpRouterOptions_2" needs to be exported by the entry point index.d.ts
//
// @public @deprecated
export type DefaultRootHttpRouterOptions = DefaultRootHttpRouterOptions_2;
// @public @deprecated (undocumented)
export const discoveryServiceFactory: () => ServiceFactory<
@@ -127,15 +121,10 @@ export const discoveryServiceFactory: () => ServiceFactory<
'plugin'
>;
// @public
export interface ExtendedHttpServer extends http.Server {
// (undocumented)
port(): number;
// (undocumented)
start(): Promise<void>;
// (undocumented)
stop(): Promise<void>;
}
// Warning: (ae-forgotten-export) The symbol "ExtendedHttpServer_2" needs to be exported by the entry point index.d.ts
//
// @public @deprecated (undocumented)
export type ExtendedHttpServer = ExtendedHttpServer_2;
// @public @deprecated
export class HostDiscovery implements DiscoveryService {
@@ -157,38 +146,25 @@ export const httpAuthServiceFactory: () => ServiceFactory<
'plugin'
>;
// @public (undocumented)
export interface HttpRouterFactoryOptions {
getPath?(pluginId: string): string;
}
// Warning: (ae-forgotten-export) The symbol "HttpRouterFactoryOptions_2" needs to be exported by the entry point index.d.ts
//
// @public @deprecated (undocumented)
export type HttpRouterFactoryOptions = HttpRouterFactoryOptions_2;
// @public
// @public @deprecated
export const httpRouterServiceFactory: (
options?: HttpRouterFactoryOptions | undefined,
options?: HttpRouterFactoryOptions_2 | undefined,
) => ServiceFactory<HttpRouterService, 'plugin'>;
// @public
export type HttpServerCertificateOptions =
| {
type: 'pem';
key: string;
cert: string;
}
| {
type: 'generated';
hostname: string;
};
// Warning: (ae-forgotten-export) The symbol "HttpServerCertificateOptions_2" needs to be exported by the entry point index.d.ts
//
// @public @deprecated (undocumented)
export type HttpServerCertificateOptions = HttpServerCertificateOptions_2;
// @public
export type HttpServerOptions = {
listen: {
port: number;
host: string;
};
https?: {
certificate: HttpServerCertificateOptions;
};
};
// Warning: (ae-forgotten-export) The symbol "HttpServerOptions_2" needs to be exported by the entry point index.d.ts
//
// @public @deprecated (undocumented)
export type HttpServerOptions = HttpServerOptions_2;
// @public @deprecated
export type IdentityFactoryOptions = {
@@ -201,12 +177,10 @@ export const identityServiceFactory: (
options?: IdentityFactoryOptions | undefined,
) => ServiceFactory<IdentityService, 'plugin'>;
// @public
export interface LifecycleMiddlewareOptions {
// (undocumented)
lifecycle: LifecycleService;
startupRequestPauseTimeout?: HumanDuration;
}
// Warning: (ae-forgotten-export) The symbol "LifecycleMiddlewareOptions_2" needs to be exported by the entry point index.d.ts
//
// @public @deprecated
export type LifecycleMiddlewareOptions = LifecycleMiddlewareOptions_2;
// @public @deprecated
export const lifecycleServiceFactory: () => ServiceFactory<
@@ -214,7 +188,7 @@ export const lifecycleServiceFactory: () => ServiceFactory<
'plugin'
>;
// @public
// @public @deprecated
export function loadBackendConfig(options: {
remote?: LoadConfigOptionsRemote;
argv: string[];
@@ -224,36 +198,26 @@ export function loadBackendConfig(options: {
config: Config;
}>;
// @public
// @public @deprecated
export const loggerServiceFactory: () => ServiceFactory<
LoggerService,
'plugin'
>;
// @public
export class MiddlewareFactory {
compression(): RequestHandler;
cors(): RequestHandler;
static create(options: MiddlewareFactoryOptions): MiddlewareFactory;
error(options?: MiddlewareFactoryErrorOptions): ErrorRequestHandler;
helmet(): RequestHandler;
logging(): RequestHandler;
notFound(): RequestHandler;
}
// Warning: (ae-forgotten-export) The symbol "MiddlewareFactory_2" needs to be exported by the entry point index.d.ts
//
// @public @deprecated (undocumented)
export const MiddlewareFactory: typeof MiddlewareFactory_2;
// @public
export interface MiddlewareFactoryErrorOptions {
logAllErrors?: boolean;
showStackTraces?: boolean;
}
// Warning: (ae-forgotten-export) The symbol "MiddlewareFactoryErrorOptions_2" needs to be exported by the entry point index.d.ts
//
// @public @deprecated (undocumented)
export type MiddlewareFactoryErrorOptions = MiddlewareFactoryErrorOptions_2;
// @public
export interface MiddlewareFactoryOptions {
// (undocumented)
config: RootConfigService;
// (undocumented)
logger: LoggerService;
}
// Warning: (ae-forgotten-export) The symbol "MiddlewareFactoryOptions_2" needs to be exported by the entry point index.d.ts
//
// @public @deprecated (undocumented)
export type MiddlewareFactoryOptions = MiddlewareFactoryOptions_2;
// @public @deprecated (undocumented)
export const permissionsServiceFactory: () => ServiceFactory<
@@ -261,14 +225,20 @@ export const permissionsServiceFactory: () => ServiceFactory<
'plugin'
>;
// @public
export function readCorsOptions(config?: Config): CorsOptions;
// Warning: (ae-forgotten-export) The symbol "readCorsOptions_2" needs to be exported by the entry point index.d.ts
//
// @public @deprecated (undocumented)
export const readCorsOptions: typeof readCorsOptions_2;
// @public
export function readHelmetOptions(config?: Config): HelmetOptions;
// Warning: (ae-forgotten-export) The symbol "readHelmetOptions_2" needs to be exported by the entry point index.d.ts
//
// @public @deprecated (undocumented)
export const readHelmetOptions: typeof readHelmetOptions_2;
// @public
export function readHttpServerOptions(config?: Config): HttpServerOptions;
// Warning: (ae-forgotten-export) The symbol "readHttpServerOptions_2" needs to be exported by the entry point index.d.ts
//
// @public @deprecated (undocumented)
export const readHttpServerOptions: typeof readHttpServerOptions_2;
// @public @deprecated (undocumented)
export interface RootConfigFactoryOptions {
@@ -283,35 +253,19 @@ export const rootConfigServiceFactory: (
options?: RootConfigFactoryOptions | undefined,
) => ServiceFactory<RootConfigService, 'root'>;
// @public (undocumented)
export interface RootHttpRouterConfigureContext {
// (undocumented)
app: Express_2;
// (undocumented)
applyDefaults: () => void;
// (undocumented)
config: RootConfigService;
// (undocumented)
lifecycle: LifecycleService;
// (undocumented)
logger: LoggerService;
// (undocumented)
middleware: MiddlewareFactory;
// (undocumented)
routes: RequestHandler;
// (undocumented)
server: Server;
}
// Warning: (ae-forgotten-export) The symbol "RootHttpRouterConfigureContext_2" needs to be exported by the entry point index.d.ts
//
// @public @deprecated (undocumented)
export type RootHttpRouterConfigureContext = RootHttpRouterConfigureContext_2;
// @public
export type RootHttpRouterFactoryOptions = {
indexPath?: string | false;
configure?(context: RootHttpRouterConfigureContext): void;
};
// Warning: (ae-forgotten-export) The symbol "RootHttpRouterFactoryOptions_2" needs to be exported by the entry point index.d.ts
//
// @public @deprecated
export type RootHttpRouterFactoryOptions = RootHttpRouterFactoryOptions_2;
// @public (undocumented)
// @public @deprecated (undocumented)
export const rootHttpRouterServiceFactory: (
options?: RootHttpRouterFactoryOptions | undefined,
options?: RootHttpRouterFactoryOptions_2 | undefined,
) => ServiceFactory<RootHttpRouterService, 'root'>;
// @public @deprecated
@@ -320,7 +274,7 @@ export const rootLifecycleServiceFactory: () => ServiceFactory<
'root'
>;
// @public
// @public @deprecated
export const rootLoggerServiceFactory: () => ServiceFactory<
RootLoggerService,
'root'
@@ -350,7 +304,7 @@ export const userInfoServiceFactory: () => ServiceFactory<
'plugin'
>;
// @public
// @public @deprecated
export class WinstonLogger implements RootLoggerService {
// (undocumented)
addRedactions(redactions: Iterable<string>): void;
@@ -372,15 +326,8 @@ export class WinstonLogger implements RootLoggerService {
warn(message: string, meta?: JsonObject): void;
}
// @public (undocumented)
export interface WinstonLoggerOptions {
// (undocumented)
format?: Format;
// (undocumented)
level?: string;
// (undocumented)
meta?: JsonObject;
// (undocumented)
transports?: transport[];
}
// Warning: (ae-forgotten-export) The symbol "WinstonLoggerOptions_2" needs to be exported by the entry point index.d.ts
//
// @public @deprecated (undocumented)
export type WinstonLoggerOptions = WinstonLoggerOptions_2;
```
+1
View File
@@ -90,6 +90,7 @@
"winston-transport": "^4.5.0"
},
"devDependencies": {
"@backstage/backend-defaults": "workspace:^",
"@backstage/backend-test-utils": "workspace:^",
"@backstage/cli": "workspace:^",
"@types/compression": "^1.7.0",
+9 -37
View File
@@ -14,56 +14,27 @@
* limitations under the License.
*/
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
import { createConfigSecretEnumerator as _createConfigSecretEnumerator } from '../../../backend-defaults/src/entrypoints/rootConfig/createConfigSecretEnumerator';
import { resolve as resolvePath } from 'path';
import parseArgs from 'minimist';
import { LoggerService } from '@backstage/backend-plugin-api';
import { findPaths } from '@backstage/cli-common';
import {
loadConfigSchema,
loadConfig,
ConfigTarget,
LoadConfigOptionsRemote,
ConfigSchema,
} from '@backstage/config-loader';
import { ConfigReader } from '@backstage/config';
import type { Config, AppConfig } from '@backstage/config';
import { getPackages } from '@manypkg/get-packages';
import { ObservableConfigProxy } from './ObservableConfigProxy';
import { isValidUrl } from '../lib/urls';
/** @public */
export async function createConfigSecretEnumerator(options: {
logger: LoggerService;
dir?: string;
schema?: ConfigSchema;
}): Promise<(config: Config) => Iterable<string>> {
const { logger, dir = process.cwd() } = options;
const { packages } = await getPackages(dir);
const schema =
options.schema ??
(await loadConfigSchema({
dependencies: packages.map(p => p.packageJson.name),
}));
return (config: Config) => {
const [secretsData] = schema.process(
[{ data: config.getOptional() ?? {}, context: 'schema-enumerator' }],
{
visibility: ['secret'],
ignoreSchemaErrors: true,
},
);
const secrets = new Set<string>();
JSON.parse(
JSON.stringify(secretsData.data),
(_, v) => typeof v === 'string' && secrets.add(v),
);
logger.info(
`Found ${secrets.size} new secrets in config that will be redacted`,
);
return secrets;
};
}
/**
* @public
* @deprecated Please import from `@backstage/backend-defaults/rootConfig` instead.
*/
export const createConfigSecretEnumerator = _createConfigSecretEnumerator;
/**
* Load configuration for a Backend.
@@ -71,6 +42,7 @@ export async function createConfigSecretEnumerator(options: {
* This function should only be called once, during the initialization of the backend.
*
* @public
* @deprecated Please migrate to the new backend system and use `coreServices.rootConfig` instead, or the {@link @backstage/config-loader#ConfigSources} facilities if required.
*/
export async function loadBackendConfig(options: {
remote?: LoadConfigOptionsRemote;
+64 -14
View File
@@ -14,17 +14,67 @@
* limitations under the License.
*/
export { readHttpServerOptions } from './config';
export { createHttpServer } from './createHttpServer';
export { MiddlewareFactory } from './MiddlewareFactory';
export type {
MiddlewareFactoryErrorOptions,
MiddlewareFactoryOptions,
} from './MiddlewareFactory';
export { readCorsOptions } from './readCorsOptions';
export { readHelmetOptions } from './readHelmetOptions';
export type {
ExtendedHttpServer,
HttpServerCertificateOptions,
HttpServerOptions,
} from './types';
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
import {
readHttpServerOptions as _readHttpServerOptions,
createHttpServer as _createHttpServer,
MiddlewareFactory as _MiddlewareFactory,
readCorsOptions as _readCorsOptions,
readHelmetOptions as _readHelmetOptions,
type MiddlewareFactoryErrorOptions as _MiddlewareFactoryErrorOptions,
type MiddlewareFactoryOptions as _MiddlewareFactoryOptions,
type ExtendedHttpServer as _ExtendedHttpServer,
type HttpServerCertificateOptions as _HttpServerCertificateOptions,
type HttpServerOptions as _HttpServerOptions,
} from '../../../backend-defaults/src/entrypoints/rootHttpRouter/http';
/**
* @public
* @deprecated Please import from `@backstage/backend-defaults/rootHttpRouter` instead.
*/
export const readHttpServerOptions = _readHttpServerOptions;
/**
* @public
* @deprecated Please import from `@backstage/backend-defaults/rootHttpRouter` instead.
*/
export const createHttpServer = _createHttpServer;
/**
* @public
* @deprecated Please import from `@backstage/backend-defaults/rootHttpRouter` instead.
*/
export const readCorsOptions = _readCorsOptions;
/**
* @public
* @deprecated Please import from `@backstage/backend-defaults/rootHttpRouter` instead.
*/
export const readHelmetOptions = _readHelmetOptions;
/**
* @public
* @deprecated Please import from `@backstage/backend-defaults/rootHttpRouter` instead.
*/
export const MiddlewareFactory = _MiddlewareFactory;
/**
* @public
* @deprecated Please import from `@backstage/backend-defaults/rootHttpRouter` instead.
*/
export type MiddlewareFactoryErrorOptions = _MiddlewareFactoryErrorOptions;
/**
* @public
* @deprecated Please import from `@backstage/backend-defaults/rootHttpRouter` instead.
*/
export type MiddlewareFactoryOptions = _MiddlewareFactoryOptions;
/**
* @public
* @deprecated Please import from `@backstage/backend-defaults/rootHttpRouter` instead.
*/
export type ExtendedHttpServer = _ExtendedHttpServer;
/**
* @public
* @deprecated Please import from `@backstage/backend-defaults/rootHttpRouter` instead.
*/
export type HttpServerCertificateOptions = _HttpServerCertificateOptions;
/**
* @public
* @deprecated Please import from `@backstage/backend-defaults/rootHttpRouter` instead.
*/
export type HttpServerOptions = _HttpServerOptions;
@@ -14,65 +14,37 @@
* limitations under the License.
*/
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
import {
WinstonLogger as _WinstonLogger,
type WinstonLoggerOptions as _WinstonLoggerOptions,
} from '../../../backend-defaults/src/entrypoints/rootLogger';
import {
LoggerService,
RootLoggerService,
} from '@backstage/backend-plugin-api';
import { JsonObject } from '@backstage/types';
import { Format, TransformableInfo } from 'logform';
import {
Logger,
format,
createLogger,
transports,
transport as Transport,
} from 'winston';
import { MESSAGE } from 'triple-beam';
import { escapeRegExp } from '../lib/escapeRegExp';
import { Format } from 'logform';
/**
* @public
* @deprecated Please import from `@backstage/backend-defaults/rootLogger` instead.
*/
export interface WinstonLoggerOptions {
meta?: JsonObject;
level?: string;
format?: Format;
transports?: Transport[];
}
export type WinstonLoggerOptions = _WinstonLoggerOptions;
/**
* A {@link @backstage/backend-plugin-api#LoggerService} implementation based on winston.
*
* @public
* @deprecated Please import from `@backstage/backend-defaults/rootLogger` instead.
*/
export class WinstonLogger implements RootLoggerService {
#winston: Logger;
#addRedactions?: (redactions: Iterable<string>) => void;
/**
* Creates a {@link WinstonLogger} instance.
*/
static create(options: WinstonLoggerOptions): WinstonLogger {
const redacter = WinstonLogger.redacter();
const defaultFormatter =
process.env.NODE_ENV === 'production'
? format.json()
: WinstonLogger.colorFormat();
let logger = createLogger({
level: process.env.LOG_LEVEL || options.level || 'info',
format: format.combine(
options.format ?? defaultFormatter,
redacter.format,
),
transports: options.transports ?? new transports.Console(),
});
if (options.meta) {
logger = logger.child(options.meta);
}
return new WinstonLogger(logger, redacter.add);
return new WinstonLogger(_WinstonLogger.create(options));
}
/**
@@ -82,111 +54,39 @@ export class WinstonLogger implements RootLoggerService {
format: Format;
add: (redactions: Iterable<string>) => void;
} {
const redactionSet = new Set<string>();
let redactionPattern: RegExp | undefined = undefined;
return {
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) {
// Trimming the string ensures that we don't accdentally get extra
// newlines or other whitespace interfering with the redaction; this
// can happen for example when using string literals in yaml
const redaction = redactionToTrim.trim();
// Exclude secrets that are empty or just one character in length. These
// typically mean that you are running local dev or tests, or using the
// --lax flag which sets things to just 'x'.
if (redaction.length <= 1) {
continue;
}
if (!redactionSet.has(redaction)) {
redactionSet.add(redaction);
added += 1;
}
}
if (added > 0) {
const redactions = Array.from(redactionSet)
.map(r => escapeRegExp(r))
.join('|');
redactionPattern = new RegExp(`(${redactions})`, 'g');
}
},
};
return _WinstonLogger.redacter();
}
/**
* Creates a pretty printed winston log formatter.
*/
static colorFormat(): Format {
const colorizer = format.colorize();
return format.combine(
format.timestamp(),
format.colorize({
colors: {
timestamp: 'dim',
prefix: 'blue',
field: 'cyan',
debug: 'grey',
},
}),
format.printf((info: TransformableInfo) => {
const { timestamp, level, message, plugin, service, ...fields } = info;
const prefix = plugin || service;
const timestampColor = colorizer.colorize('timestamp', timestamp);
const prefixColor = colorizer.colorize('prefix', prefix);
const extraFields = Object.entries(fields)
.map(
([key, value]) =>
`${colorizer.colorize('field', `${key}`)}=${value}`,
)
.join(' ');
return `${timestampColor} ${prefixColor} ${level} ${message} ${extraFields}`;
}),
);
return _WinstonLogger.colorFormat();
}
private constructor(
winston: Logger,
addRedactions?: (redactions: Iterable<string>) => void,
) {
this.#winston = winston;
this.#addRedactions = addRedactions;
}
private constructor(private readonly impl: _WinstonLogger) {}
error(message: string, meta?: JsonObject): void {
this.#winston.error(message, meta);
this.impl.error(message, meta);
}
warn(message: string, meta?: JsonObject): void {
this.#winston.warn(message, meta);
this.impl.warn(message, meta);
}
info(message: string, meta?: JsonObject): void {
this.#winston.info(message, meta);
this.impl.info(message, meta);
}
debug(message: string, meta?: JsonObject): void {
this.#winston.debug(message, meta);
this.impl.debug(message, meta);
}
child(meta: JsonObject): LoggerService {
return new WinstonLogger(this.#winston.child(meta));
return this.impl.child(meta);
}
addRedactions(redactions: Iterable<string>) {
this.#addRedactions?.(redactions);
this.impl.addRedactions(redactions);
}
}
@@ -16,5 +16,9 @@
export * from './auth';
export * from './httpAuth';
export * from './httpRouter';
export * from './logger';
export * from './rootHttpRouter';
export * from './rootLogger';
export * from './scheduler';
export * from './userInfo';
@@ -15,10 +15,9 @@
*/
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 };
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
import { HostDiscovery as _HostDiscovery } from '../../../../../backend-defaults/src/entrypoints/discovery';
/**
* HostDiscovery is a basic PluginEndpointDiscovery implementation
@@ -57,77 +56,16 @@ export class HostDiscovery implements DiscoveryService {
* 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'),
);
return new HostDiscovery(_HostDiscovery.fromConfig(config, options));
}
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),
);
}
private constructor(private readonly impl: _HostDiscovery) {}
async getBaseUrl(pluginId: string): Promise<string> {
return this.getTargetFromConfig(pluginId, 'internal');
return this.impl.getBaseUrl(pluginId);
}
async getExternalBaseUrl(pluginId: string): Promise<string> {
return this.getTargetFromConfig(pluginId, 'external');
return this.impl.getExternalBaseUrl(pluginId);
}
}
@@ -14,26 +14,18 @@
* limitations under the License.
*/
import { LifecycleService } from '@backstage/backend-plugin-api';
import { ServiceUnavailableError } from '@backstage/errors';
import { HumanDuration, durationToMilliseconds } from '@backstage/types';
import { RequestHandler } from 'express';
export const DEFAULT_TIMEOUT = { seconds: 5 };
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
import {
createLifecycleMiddleware as _createLifecycleMiddleware,
type LifecycleMiddlewareOptions as _LifecycleMiddlewareOptions,
} from '../../../../../backend-defaults/src/entrypoints/httpRouter/createLifecycleMiddleware';
/**
* Options for {@link createLifecycleMiddleware}.
* @public
* @deprecated Please import from `@backstage/backend-defaults/httpRouter` instead.
*/
export interface LifecycleMiddlewareOptions {
lifecycle: LifecycleService;
/**
* The maximum time that paused requests will wait for the service to start, before returning an error.
*
* Defaults to 5 seconds.
*/
startupRequestPauseTimeout?: HumanDuration;
}
export type LifecycleMiddlewareOptions = _LifecycleMiddlewareOptions;
/**
* Creates a middleware that pauses requests until the service has started.
@@ -48,59 +40,6 @@ export interface LifecycleMiddlewareOptions {
* {@link @backstage/errors#ServiceUnavailableError}.
*
* @public
* @deprecated Please import from `@backstage/backend-defaults/httpRouter` instead.
*/
export function createLifecycleMiddleware(
options: LifecycleMiddlewareOptions,
): RequestHandler {
const { lifecycle, startupRequestPauseTimeout = DEFAULT_TIMEOUT } = options;
let state: 'init' | 'up' | 'down' = 'init';
const waiting = new Set<{
next: (err?: Error) => void;
timeout: NodeJS.Timeout;
}>();
lifecycle.addStartupHook(async () => {
if (state === 'init') {
state = 'up';
for (const item of waiting) {
clearTimeout(item.timeout);
item.next();
}
waiting.clear();
}
});
lifecycle.addShutdownHook(async () => {
state = 'down';
for (const item of waiting) {
clearTimeout(item.timeout);
item.next(new ServiceUnavailableError('Service is shutting down'));
}
waiting.clear();
});
const timeoutMs = durationToMilliseconds(startupRequestPauseTimeout);
return (_req, _res, next) => {
if (state === 'up') {
next();
return;
} else if (state === 'down') {
next(new ServiceUnavailableError('Service is shutting down'));
return;
}
const item = {
next,
timeout: setTimeout(() => {
if (waiting.delete(item)) {
next(new ServiceUnavailableError('Service has not started up yet'));
}
}, timeoutMs),
};
waiting.add(item);
};
}
export const createLifecycleMiddleware = _createLifecycleMiddleware;
@@ -14,27 +14,17 @@
* limitations under the License.
*/
import { Handler } from 'express';
import PromiseRouter from 'express-promise-router';
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
import {
coreServices,
createServiceFactory,
HttpRouterServiceAuthPolicy,
} from '@backstage/backend-plugin-api';
import { createLifecycleMiddleware } from './createLifecycleMiddleware';
import { createCredentialsBarrier } from './createCredentialsBarrier';
import { createAuthIntegrationRouter } from './createAuthIntegrationRouter';
import { createCookieAuthRefreshMiddleware } from './createCookieAuthRefreshMiddleware';
httpRouterServiceFactory as _httpRouterServiceFactory,
type HttpRouterFactoryOptions as _HttpRouterFactoryOptions,
} from '../../../../../backend-defaults/src/entrypoints/httpRouter/httpRouterServiceFactory';
/**
* @public
* @deprecated Please import from `@backstage/backend-defaults/httpRouter` instead.
*/
export interface HttpRouterFactoryOptions {
/**
* A callback used to generate the path for each plugin, defaults to `/api/{pluginId}`.
*/
getPath?(pluginId: string): string;
}
export type HttpRouterFactoryOptions = _HttpRouterFactoryOptions;
/**
* HTTP route registration for plugins.
@@ -44,58 +34,6 @@ export interface HttpRouterFactoryOptions {
* for more information.
*
* @public
* @deprecated Please import from `@backstage/backend-defaults/httpRouter` instead.
*/
export const httpRouterServiceFactory = createServiceFactory(
(options?: HttpRouterFactoryOptions) => ({
service: coreServices.httpRouter,
initialization: 'always',
deps: {
plugin: coreServices.pluginMetadata,
config: coreServices.rootConfig,
logger: coreServices.logger,
lifecycle: coreServices.lifecycle,
rootHttpRouter: coreServices.rootHttpRouter,
auth: coreServices.auth,
httpAuth: coreServices.httpAuth,
},
async factory({
auth,
httpAuth,
config,
logger,
plugin,
rootHttpRouter,
lifecycle,
}) {
if (options?.getPath) {
logger.warn(
`DEPRECATION WARNING: The 'getPath' option for HttpRouterService is deprecated. The ability to reconfigure the '/api/' path prefix for plugins will be removed in the future.`,
);
}
const getPath = options?.getPath ?? (id => `/api/${id}`);
const path = getPath(plugin.getId());
const router = PromiseRouter();
rootHttpRouter.use(path, router);
const credentialsBarrier = createCredentialsBarrier({
httpAuth,
config,
});
router.use(createAuthIntegrationRouter({ auth }));
router.use(createLifecycleMiddleware({ lifecycle }));
router.use(credentialsBarrier.middleware);
router.use(createCookieAuthRefreshMiddleware({ auth, httpAuth }));
return {
use(handler: Handler): void {
router.use(handler);
},
addAuthPolicy(policy: HttpRouterServiceAuthPolicy): void {
credentialsBarrier.addAuthPolicy(policy);
},
};
},
}),
);
export const httpRouterServiceFactory = _httpRouterServiceFactory;
@@ -18,14 +18,10 @@ export * from './cache';
export * from './config';
export * from './database';
export * from './discovery';
export * from './httpRouter';
export * from './identity';
export * from './lifecycle';
export * from './logger';
export * from './permissions';
export * from './rootHttpRouter';
export * from './rootLifecycle';
export * from './rootLogger';
export * from './tokenManager';
export * from './urlReader';
@@ -14,10 +14,8 @@
* limitations under the License.
*/
import {
createServiceFactory,
coreServices,
} from '@backstage/backend-plugin-api';
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
import { loggerServiceFactory as _loggerServiceFactory } from '../../../../../backend-defaults/src/entrypoints/logger/loggerServiceFactory';
/**
* Plugin-level logging.
@@ -27,14 +25,6 @@ import {
* for more information.
*
* @public
* @deprecated Please import from `@backstage/backend-defaults/logger` instead.
*/
export const loggerServiceFactory = createServiceFactory({
service: coreServices.logger,
deps: {
rootLogger: coreServices.rootLogger,
plugin: coreServices.pluginMetadata,
},
factory({ rootLogger, plugin }) {
return rootLogger.child({ plugin: plugin.getId() });
},
});
export const loggerServiceFactory = _loggerServiceFactory;
@@ -15,101 +15,40 @@
*/
import { RootHttpRouterService } from '@backstage/backend-plugin-api';
import { Handler, Router } from 'express';
import trimEnd from 'lodash/trimEnd';
function normalizePath(path: string): string {
return `${trimEnd(path, '/')}/`;
}
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
import {
DefaultRootHttpRouter as _DefaultRootHttpRouter,
DefaultRootHttpRouterOptions as _DefaultRootHttpRouterOptions,
} from '../../../../../backend-defaults/src/entrypoints/rootHttpRouter/DefaultRootHttpRouter';
import { Handler } from 'express';
/**
* Options for the {@link DefaultRootHttpRouter} class.
*
* @public
* @deprecated Please import from `@backstage/backend-defaults/rootHttpRouter` instead.
*/
export interface DefaultRootHttpRouterOptions {
/**
* The path to forward all unmatched requests to. Defaults to '/api/app' if
* not given. Disables index path behavior if false is given.
*/
indexPath?: string | false;
}
export type DefaultRootHttpRouterOptions = _DefaultRootHttpRouterOptions;
/**
* The default implementation of the {@link @backstage/backend-plugin-api#RootHttpRouterService} interface for
* {@link @backstage/backend-plugin-api#coreServices.rootHttpRouter}.
*
* @public
* @deprecated Please import from `@backstage/backend-defaults/rootHttpRouter` instead.
*/
export class DefaultRootHttpRouter implements RootHttpRouterService {
#indexPath?: string;
#router = Router();
#namedRoutes = Router();
#indexRouter = Router();
#existingPaths = new Array<string>();
static create(options?: DefaultRootHttpRouterOptions) {
let indexPath;
if (options?.indexPath === false) {
indexPath = undefined;
} else if (options?.indexPath === undefined) {
indexPath = '/api/app';
} else if (options?.indexPath === '') {
throw new Error('indexPath option may not be an empty string');
} else {
indexPath = options.indexPath;
}
return new DefaultRootHttpRouter(indexPath);
return new DefaultRootHttpRouter(_DefaultRootHttpRouter.create(options));
}
private constructor(indexPath?: string) {
this.#indexPath = indexPath;
this.#router.use(this.#namedRoutes);
// Any request with a /api/ prefix will skip the index router, even if no named router matches
this.#router.use('/api/', (_req, _res, next) => {
next('router');
});
if (this.#indexPath) {
this.#router.use(this.#indexRouter);
}
}
private constructor(private readonly impl: RootHttpRouterService) {}
use(path: string, handler: Handler) {
if (path.match(/^[/\s]*$/)) {
throw new Error(`Root router path may not be empty`);
}
const conflictingPath = this.#findConflictingPath(path);
if (conflictingPath) {
throw new Error(
`Path ${path} conflicts with the existing path ${conflictingPath}`,
);
}
this.#existingPaths.push(path);
this.#namedRoutes.use(path, handler);
if (this.#indexPath === path) {
this.#indexRouter.use(handler);
}
this.impl.use(path, handler);
}
handler(): Handler {
return this.#router;
}
#findConflictingPath(newPath: string): string | undefined {
const normalizedNewPath = normalizePath(newPath);
for (const path of this.#existingPaths) {
const normalizedPath = normalizePath(path);
if (normalizedPath.startsWith(normalizedNewPath)) {
return path;
}
if (normalizedNewPath.startsWith(normalizedPath)) {
return path;
}
}
return undefined;
return (this.impl as any).handler();
}
}
@@ -14,35 +14,18 @@
* limitations under the License.
*/
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
import {
RootConfigService,
coreServices,
createServiceFactory,
LifecycleService,
LoggerService,
} from '@backstage/backend-plugin-api';
import express, { RequestHandler, Express } from 'express';
import type { Server } from 'node:http';
import {
createHttpServer,
MiddlewareFactory,
readHttpServerOptions,
} from '../../../http';
import { DefaultRootHttpRouter } from './DefaultRootHttpRouter';
rootHttpRouterServiceFactory as _rootHttpRouterServiceFactory,
RootHttpRouterFactoryOptions as _RootHttpRouterFactoryOptions,
RootHttpRouterConfigureContext as _RootHttpRouterConfigureContext,
} from '../../../../../backend-defaults/src/entrypoints/rootHttpRouter/rootHttpRouterServiceFactory';
/**
* @public
* @deprecated Please import from `@backstage/backend-defaults/rootHttpRouter` instead.
*/
export interface RootHttpRouterConfigureContext {
app: Express;
server: Server;
middleware: MiddlewareFactory;
routes: RequestHandler;
config: RootConfigService;
logger: LoggerService;
lifecycle: LifecycleService;
applyDefaults: () => void;
}
export type RootHttpRouterConfigureContext = _RootHttpRouterConfigureContext;
/**
* HTTP route registration for root services.
@@ -52,68 +35,12 @@ export interface RootHttpRouterConfigureContext {
* for more information.
*
* @public
* @deprecated Please import from `@backstage/backend-defaults/rootHttpRouter` instead.
*/
export type RootHttpRouterFactoryOptions = {
/**
* The path to forward all unmatched requests to. Defaults to '/api/app' if
* not given. Disables index path behavior if false is given.
*/
indexPath?: string | false;
export type RootHttpRouterFactoryOptions = _RootHttpRouterFactoryOptions;
configure?(context: RootHttpRouterConfigureContext): void;
};
function defaultConfigure({ applyDefaults }: RootHttpRouterConfigureContext) {
applyDefaults();
}
/** @public */
export const rootHttpRouterServiceFactory = createServiceFactory(
(options?: RootHttpRouterFactoryOptions) => ({
service: coreServices.rootHttpRouter,
deps: {
config: coreServices.rootConfig,
rootLogger: coreServices.rootLogger,
lifecycle: coreServices.rootLifecycle,
},
async factory({ config, rootLogger, lifecycle }) {
const { indexPath, configure = defaultConfigure } = options ?? {};
const logger = rootLogger.child({ service: 'rootHttpRouter' });
const app = express();
const router = DefaultRootHttpRouter.create({ indexPath });
const middleware = MiddlewareFactory.create({ config, logger });
const routes = router.handler();
const server = await createHttpServer(
app,
readHttpServerOptions(config.getOptionalConfig('backend')),
{ logger },
);
configure({
app,
server,
routes,
middleware,
config,
logger,
lifecycle,
applyDefaults() {
app.use(middleware.helmet());
app.use(middleware.cors());
app.use(middleware.compression());
app.use(middleware.logging());
app.use(routes);
app.use(middleware.notFound());
app.use(middleware.error());
},
});
lifecycle.addShutdownHook(() => server.stop());
await server.start();
return router;
},
}),
);
/**
* @public
* @deprecated Please import from `@backstage/backend-defaults/rootHttpRouter` instead.
*/
export const rootHttpRouterServiceFactory = _rootHttpRouterServiceFactory;
@@ -14,13 +14,8 @@
* limitations under the License.
*/
import {
createServiceFactory,
coreServices,
} from '@backstage/backend-plugin-api';
import { WinstonLogger } from '../../../logging';
import { transports, format } from 'winston';
import { createConfigSecretEnumerator } from '../../../config';
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
import { rootLoggerServiceFactory as _rootLoggerServiceFactory } from '../../../../../backend-defaults/src/entrypoints/rootLogger/rootLoggerServiceFactory';
/**
* Root-level logging.
@@ -30,29 +25,6 @@ import { createConfigSecretEnumerator } from '../../../config';
* for more information.
*
* @public
* @deprecated Please import from `@backstage/backend-defaults/rootLogger` instead.
*/
export const rootLoggerServiceFactory = createServiceFactory({
service: coreServices.rootLogger,
deps: {
config: coreServices.rootConfig,
},
async factory({ config }) {
const logger = WinstonLogger.create({
meta: {
service: 'backstage',
},
level: process.env.LOG_LEVEL || 'info',
format:
process.env.NODE_ENV === 'production'
? format.json()
: WinstonLogger.colorFormat(),
transports: [new transports.Console()],
});
const secretEnumerator = await createConfigSecretEnumerator({ logger });
logger.addRedactions(secretEnumerator(config));
config.subscribe?.(() => logger.addRedactions(secretEnumerator(config)));
return logger;
},
});
export const rootLoggerServiceFactory = _rootLoggerServiceFactory;
@@ -14,6 +14,9 @@
* limitations under the License.
*/
import { rootLifecycleServiceFactory } from '@backstage/backend-defaults/rootLifecycle';
import { lifecycleServiceFactory } from '@backstage/backend-defaults/lifecycle';
import { loggerServiceFactory } from '@backstage/backend-defaults/logger';
import {
createServiceRef,
createServiceFactory,
@@ -24,12 +27,6 @@ import {
} from '@backstage/backend-plugin-api';
import { BackendInitializer } from './BackendInitializer';
import {
lifecycleServiceFactory,
loggerServiceFactory,
rootLifecycleServiceFactory,
} from '../services/implementations';
class MockLogger {
debug() {}
info() {}
@@ -18,7 +18,7 @@ import { ErrorRequestHandler } from 'express';
import { LoggerService } from '@backstage/backend-plugin-api';
import { ConfigReader } from '@backstage/config';
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
import { MiddlewareFactory } from '../../../../backend-app-api/src/http/MiddlewareFactory';
import { MiddlewareFactory } from '../../../../backend-defaults/src/entrypoints/rootHttpRouter/http/MiddlewareFactory';
import { getRootLogger } from '../logging';
/**
@@ -15,7 +15,7 @@
*/
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
import { MiddlewareFactory } from '../../../../backend-app-api/src/http/MiddlewareFactory';
import { MiddlewareFactory } from '../../../../backend-defaults/src/entrypoints/rootHttpRouter/http/MiddlewareFactory';
import { ConfigReader } from '@backstage/config';
import { RequestHandler } from 'express';
import { getRootLogger } from '../logging';
@@ -15,7 +15,7 @@
*/
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
import { MiddlewareFactory } from '../../../../backend-app-api/src/http/MiddlewareFactory';
import { MiddlewareFactory } from '../../../../backend-defaults/src/entrypoints/rootHttpRouter/http/MiddlewareFactory';
import { RequestHandler } from 'express';
import { ConfigReader } from '@backstage/config';
import { LoggerService } from '@backstage/backend-plugin-api';
@@ -37,7 +37,7 @@ import {
readHttpServerOptions,
HttpServerOptions,
createHttpServer,
} from '../../../../../backend-app-api/src/http';
} from '../../../../../backend-defaults/src/entrypoints/rootHttpRouter/http';
export type CspOptions = Record<string, string[]>;
@@ -0,0 +1,35 @@
## 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 { HttpRouterService } from '@backstage/backend-plugin-api';
import { HumanDuration } from '@backstage/types';
import { LifecycleService } from '@backstage/backend-plugin-api';
import { RequestHandler } from 'express';
import { ServiceFactory } from '@backstage/backend-plugin-api';
// @public
export function createLifecycleMiddleware(
options: LifecycleMiddlewareOptions,
): RequestHandler;
// @public (undocumented)
export interface HttpRouterFactoryOptions {
getPath?(pluginId: string): string;
}
// @public
export const httpRouterServiceFactory: (
options?: HttpRouterFactoryOptions | undefined,
) => ServiceFactory<HttpRouterService, 'plugin'>;
// @public
export interface LifecycleMiddlewareOptions {
// (undocumented)
lifecycle: LifecycleService;
startupRequestPauseTimeout?: HumanDuration;
}
// (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 { LoggerService } from '@backstage/backend-plugin-api';
import { ServiceFactory } from '@backstage/backend-plugin-api';
// @public
export const loggerServiceFactory: () => ServiceFactory<
LoggerService,
'plugin'
>;
// (No @packageDocumentation comment for this package)
```
@@ -3,10 +3,20 @@
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import type { Config } from '@backstage/config';
import { ConfigSchema } from '@backstage/config-loader';
import { LoggerService } from '@backstage/backend-plugin-api';
import { RemoteConfigSourceOptions } from '@backstage/config-loader';
import { RootConfigService } from '@backstage/backend-plugin-api';
import { ServiceFactory } from '@backstage/backend-plugin-api';
// @public (undocumented)
export function createConfigSecretEnumerator(options: {
logger: LoggerService;
dir?: string;
schema?: ConfigSchema;
}): Promise<(config: Config) => Iterable<string>>;
// @public
export interface RootConfigFactoryOptions {
argv?: string[];
@@ -0,0 +1,147 @@
## 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
/// <reference types="node" />
import { Config } from '@backstage/config';
import { CorsOptions } from 'cors';
import { ErrorRequestHandler } from 'express';
import { Express as Express_2 } from 'express';
import { Handler } from 'express';
import { HelmetOptions } from 'helmet';
import * as http from 'http';
import { LifecycleService } from '@backstage/backend-plugin-api';
import { LoggerService } from '@backstage/backend-plugin-api';
import { RequestHandler } from 'express';
import { RequestListener } from 'http';
import { RootConfigService } from '@backstage/backend-plugin-api';
import { RootHttpRouterService } from '@backstage/backend-plugin-api';
import type { Server } from 'node:http';
import { ServiceFactory } from '@backstage/backend-plugin-api';
// @public
export function createHttpServer(
listener: RequestListener,
options: HttpServerOptions,
deps: {
logger: LoggerService;
},
): Promise<ExtendedHttpServer>;
// @public
export class DefaultRootHttpRouter implements RootHttpRouterService {
// (undocumented)
static create(options?: DefaultRootHttpRouterOptions): DefaultRootHttpRouter;
// (undocumented)
handler(): Handler;
// (undocumented)
use(path: string, handler: Handler): void;
}
// @public
export interface DefaultRootHttpRouterOptions {
indexPath?: string | false;
}
// @public
export interface ExtendedHttpServer extends http.Server {
// (undocumented)
port(): number;
// (undocumented)
start(): Promise<void>;
// (undocumented)
stop(): Promise<void>;
}
// @public
export type HttpServerCertificateOptions =
| {
type: 'pem';
key: string;
cert: string;
}
| {
type: 'generated';
hostname: string;
};
// @public
export type HttpServerOptions = {
listen: {
port: number;
host: string;
};
https?: {
certificate: HttpServerCertificateOptions;
};
};
// @public
export class MiddlewareFactory {
compression(): RequestHandler;
cors(): RequestHandler;
static create(options: MiddlewareFactoryOptions): MiddlewareFactory;
error(options?: MiddlewareFactoryErrorOptions): ErrorRequestHandler;
helmet(): RequestHandler;
logging(): RequestHandler;
notFound(): RequestHandler;
}
// @public
export interface MiddlewareFactoryErrorOptions {
logAllErrors?: boolean;
showStackTraces?: boolean;
}
// @public
export interface MiddlewareFactoryOptions {
// (undocumented)
config: RootConfigService;
// (undocumented)
logger: LoggerService;
}
// @public
export function readCorsOptions(config?: Config): CorsOptions;
// @public
export function readHelmetOptions(config?: Config): HelmetOptions;
// @public
export function readHttpServerOptions(config?: Config): HttpServerOptions;
// @public (undocumented)
export interface RootHttpRouterConfigureContext {
// (undocumented)
app: Express_2;
// (undocumented)
applyDefaults: () => void;
// (undocumented)
config: RootConfigService;
// (undocumented)
lifecycle: LifecycleService;
// (undocumented)
logger: LoggerService;
// (undocumented)
middleware: MiddlewareFactory;
// (undocumented)
routes: RequestHandler;
// (undocumented)
server: Server;
}
// @public
export type RootHttpRouterFactoryOptions = {
indexPath?: string | false;
configure?(context: RootHttpRouterConfigureContext): void;
};
// @public (undocumented)
export const rootHttpRouterServiceFactory: (
options?: RootHttpRouterFactoryOptions | undefined,
) => ServiceFactory<RootHttpRouterService, 'root'>;
// (No @packageDocumentation comment for this package)
```
@@ -0,0 +1,54 @@
## 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 { Format } from 'logform';
import { JsonObject } from '@backstage/types';
import { LoggerService } from '@backstage/backend-plugin-api';
import { RootLoggerService } from '@backstage/backend-plugin-api';
import { ServiceFactory } from '@backstage/backend-plugin-api';
import { transport } from 'winston';
// @public
export const rootLoggerServiceFactory: () => ServiceFactory<
RootLoggerService,
'root'
>;
// @public
export class WinstonLogger implements RootLoggerService {
// (undocumented)
addRedactions(redactions: Iterable<string>): void;
// (undocumented)
child(meta: JsonObject): LoggerService;
static colorFormat(): Format;
static create(options: WinstonLoggerOptions): WinstonLogger;
// (undocumented)
debug(message: string, meta?: JsonObject): void;
// (undocumented)
error(message: string, meta?: JsonObject): void;
// (undocumented)
info(message: string, meta?: JsonObject): void;
static redacter(): {
format: Format;
add: (redactions: Iterable<string>) => void;
};
// (undocumented)
warn(message: string, meta?: JsonObject): void;
}
// @public (undocumented)
export interface WinstonLoggerOptions {
// (undocumented)
format?: Format;
// (undocumented)
level?: string;
// (undocumented)
meta?: JsonObject;
// (undocumented)
transports?: transport[];
}
// (No @packageDocumentation comment for this package)
```
+40
View File
@@ -25,10 +25,14 @@
"./database": "./src/entrypoints/database/index.ts",
"./discovery": "./src/entrypoints/discovery/index.ts",
"./httpAuth": "./src/entrypoints/httpAuth/index.ts",
"./httpRouter": "./src/entrypoints/httpRouter/index.ts",
"./lifecycle": "./src/entrypoints/lifecycle/index.ts",
"./logger": "./src/entrypoints/logger/index.ts",
"./permissions": "./src/entrypoints/permissions/index.ts",
"./rootConfig": "./src/entrypoints/rootConfig/index.ts",
"./rootHttpRouter": "./src/entrypoints/rootHttpRouter/index.ts",
"./rootLifecycle": "./src/entrypoints/rootLifecycle/index.ts",
"./rootLogger": "./src/entrypoints/rootLogger/index.ts",
"./scheduler": "./src/entrypoints/scheduler/index.ts",
"./urlReader": "./src/entrypoints/urlReader/index.ts",
"./userInfo": "./src/entrypoints/userInfo/index.ts",
@@ -53,18 +57,30 @@
"httpAuth": [
"src/entrypoints/httpAuth/index.ts"
],
"httpRouter": [
"src/entrypoints/httpRouter/index.ts"
],
"lifecycle": [
"src/entrypoints/lifecycle/index.ts"
],
"logger": [
"src/entrypoints/logger/index.ts"
],
"permissions": [
"src/entrypoints/permissions/index.ts"
],
"rootConfig": [
"src/entrypoints/rootConfig/index.ts"
],
"rootHttpRouter": [
"src/entrypoints/rootHttpRouter/index.ts"
],
"rootLifecycle": [
"src/entrypoints/rootLifecycle/index.ts"
],
"rootLogger": [
"src/entrypoints/rootLogger/index.ts"
],
"scheduler": [
"src/entrypoints/scheduler/index.ts"
],
@@ -103,6 +119,7 @@
"@backstage/backend-common": "workspace:^",
"@backstage/backend-dev-utils": "workspace:^",
"@backstage/backend-plugin-api": "workspace:^",
"@backstage/cli-common": "workspace:^",
"@backstage/config": "workspace:^",
"@backstage/config-loader": "workspace:^",
"@backstage/errors": "workspace:^",
@@ -115,32 +132,49 @@
"@google-cloud/storage": "^7.0.0",
"@keyv/memcache": "^1.3.5",
"@keyv/redis": "^2.5.3",
"@manypkg/get-packages": "^1.1.3",
"@octokit/rest": "^19.0.3",
"@opentelemetry/api": "^1.3.0",
"@types/cors": "^2.8.6",
"@types/express": "^4.17.6",
"archiver": "^6.0.0",
"base64-stream": "^1.0.0",
"better-sqlite3": "^9.0.0",
"compression": "^1.7.4",
"concat-stream": "^2.0.0",
"cookie": "^0.6.0",
"cors": "^2.8.5",
"cron": "^3.0.0",
"express": "^4.17.1",
"express-promise-router": "^4.1.0",
"fs-extra": "^11.2.0",
"git-url-parse": "^14.0.0",
"helmet": "^6.0.0",
"isomorphic-git": "^1.23.0",
"jose": "^5.0.0",
"keyv": "^4.5.2",
"knex": "^3.0.0",
"lodash": "^4.17.21",
"logform": "^2.3.2",
"luxon": "^3.0.0",
"minimatch": "^9.0.0",
"minimist": "^1.2.5",
"morgan": "^1.10.0",
"mysql2": "^3.0.0",
"node-fetch": "^2.6.7",
"node-forge": "^1.3.1",
"p-limit": "^3.1.0",
"path-to-regexp": "^6.2.1",
"pg": "^8.11.3",
"pg-connection-string": "^2.3.0",
"raw-body": "^2.4.1",
"selfsigned": "^2.0.0",
"stoppable": "^1.1.0",
"tar": "^6.1.12",
"triple-beam": "^1.4.1",
"uuid": "^9.0.0",
"winston": "^3.2.1",
"winston-transport": "^4.5.0",
"yauzl": "^3.0.0",
"yn": "^4.0.0",
"zod": "^3.22.4"
@@ -150,8 +184,14 @@
"@backstage/backend-plugin-api": "workspace:^",
"@backstage/backend-test-utils": "workspace:^",
"@backstage/cli": "workspace:^",
"@types/http-errors": "^2.0.0",
"@types/morgan": "^1.9.0",
"@types/node-forge": "^1.3.0",
"@types/stoppable": "^1.1.0",
"aws-sdk-client-mock": "^4.0.0",
"http-errors": "^2.0.0",
"msw": "^1.0.0",
"supertest": "^6.1.3",
"wait-for-expect": "^3.0.2"
},
"configSchema": "config.d.ts"
@@ -17,11 +17,7 @@
import {
Backend,
createSpecializedBackend,
httpRouterServiceFactory,
identityServiceFactory,
loggerServiceFactory,
rootHttpRouterServiceFactory,
rootLoggerServiceFactory,
tokenManagerServiceFactory,
} from '@backstage/backend-app-api';
import { authServiceFactory } from '@backstage/backend-defaults/auth';
@@ -29,10 +25,14 @@ import { cacheServiceFactory } from '@backstage/backend-defaults/cache';
import { databaseServiceFactory } from '@backstage/backend-defaults/database';
import { discoveryServiceFactory } from '@backstage/backend-defaults/discovery';
import { httpAuthServiceFactory } from '@backstage/backend-defaults/httpAuth';
import { httpRouterServiceFactory } from '@backstage/backend-defaults/httpRouter';
import { lifecycleServiceFactory } from '@backstage/backend-defaults/lifecycle';
import { loggerServiceFactory } from '@backstage/backend-defaults/logger';
import { permissionsServiceFactory } from '@backstage/backend-defaults/permissions';
import { rootConfigServiceFactory } from '@backstage/backend-defaults/rootConfig';
import { rootHttpRouterServiceFactory } from '@backstage/backend-defaults/rootHttpRouter';
import { rootLifecycleServiceFactory } from '@backstage/backend-defaults/rootLifecycle';
import { rootLoggerServiceFactory } from '@backstage/backend-defaults/rootLogger';
import { schedulerServiceFactory } from '@backstage/backend-defaults/scheduler';
import { urlReaderServiceFactory } from '@backstage/backend-defaults/urlReader';
import { userInfoServiceFactory } from '@backstage/backend-defaults/userInfo';
@@ -15,8 +15,8 @@
*/
import { Config } from '@backstage/config';
import { readHttpServerOptions } from '@backstage/backend-app-api';
import { DiscoveryService } from '@backstage/backend-plugin-api';
import { readHttpServerOptions } from '../rootHttpRouter/http/config';
type Target = string | { internal: string; external: string };
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { AuthService } from '@backstage/backend-plugin-api';
import express from 'express';
import Router from 'express-promise-router';
@@ -20,7 +20,7 @@ import express from 'express';
import request from 'supertest';
import { createCredentialsBarrier } from './createCredentialsBarrier';
import { mockCredentials, mockServices } from '@backstage/backend-test-utils';
import { MiddlewareFactory } from '../../../http';
import { MiddlewareFactory } from '../rootHttpRouter/http';
const errorMiddleware = MiddlewareFactory.create({
config: mockServices.rootConfig(),
@@ -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 } from '@backstage/backend-plugin-api';
import { ServiceUnavailableError } from '@backstage/errors';
import { HumanDuration, durationToMilliseconds } from '@backstage/types';
import { RequestHandler } from 'express';
export const DEFAULT_TIMEOUT = { seconds: 5 };
/**
* Options for {@link createLifecycleMiddleware}.
* @public
*/
export interface LifecycleMiddlewareOptions {
lifecycle: LifecycleService;
/**
* The maximum time that paused requests will wait for the service to start, before returning an error.
*
* Defaults to 5 seconds.
*/
startupRequestPauseTimeout?: HumanDuration;
}
/**
* Creates a middleware that pauses requests until the service has started.
*
* @remarks
*
* Requests that arrive before the service has started will be paused until startup is complete.
* If the service does not start within the provided timeout, the request will be rejected with a
* {@link @backstage/errors#ServiceUnavailableError}.
*
* If the service is shutting down, all requests will be rejected with a
* {@link @backstage/errors#ServiceUnavailableError}.
*
* @public
*/
export function createLifecycleMiddleware(
options: LifecycleMiddlewareOptions,
): RequestHandler {
const { lifecycle, startupRequestPauseTimeout = DEFAULT_TIMEOUT } = options;
let state: 'init' | 'up' | 'down' = 'init';
const waiting = new Set<{
next: (err?: Error) => void;
timeout: NodeJS.Timeout;
}>();
lifecycle.addStartupHook(async () => {
if (state === 'init') {
state = 'up';
for (const item of waiting) {
clearTimeout(item.timeout);
item.next();
}
waiting.clear();
}
});
lifecycle.addShutdownHook(async () => {
state = 'down';
for (const item of waiting) {
clearTimeout(item.timeout);
item.next(new ServiceUnavailableError('Service is shutting down'));
}
waiting.clear();
});
const timeoutMs = durationToMilliseconds(startupRequestPauseTimeout);
return (_req, _res, next) => {
if (state === 'up') {
next();
return;
} else if (state === 'down') {
next(new ServiceUnavailableError('Service is shutting down'));
return;
}
const item = {
next,
timeout: setTimeout(() => {
if (waiting.delete(item)) {
next(new ServiceUnavailableError('Service has not started up yet'));
}
}, timeoutMs),
};
waiting.add(item);
};
}
@@ -0,0 +1,101 @@
/*
* 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 { Handler } from 'express';
import PromiseRouter from 'express-promise-router';
import {
coreServices,
createServiceFactory,
HttpRouterServiceAuthPolicy,
} from '@backstage/backend-plugin-api';
import { createLifecycleMiddleware } from './createLifecycleMiddleware';
import { createCredentialsBarrier } from './createCredentialsBarrier';
import { createAuthIntegrationRouter } from './createAuthIntegrationRouter';
import { createCookieAuthRefreshMiddleware } from './createCookieAuthRefreshMiddleware';
/**
* @public
*/
export interface HttpRouterFactoryOptions {
/**
* A callback used to generate the path for each plugin, defaults to `/api/{pluginId}`.
*/
getPath?(pluginId: string): string;
}
/**
* HTTP route registration for plugins.
*
* See {@link @backstage/code-plugin-api#HttpRouterService}
* and {@link https://backstage.io/docs/backend-system/core-services/http-router | the service docs}
* for more information.
*
* @public
*/
export const httpRouterServiceFactory = createServiceFactory(
(options?: HttpRouterFactoryOptions) => ({
service: coreServices.httpRouter,
initialization: 'always',
deps: {
plugin: coreServices.pluginMetadata,
config: coreServices.rootConfig,
logger: coreServices.logger,
lifecycle: coreServices.lifecycle,
rootHttpRouter: coreServices.rootHttpRouter,
auth: coreServices.auth,
httpAuth: coreServices.httpAuth,
},
async factory({
auth,
httpAuth,
config,
logger,
plugin,
rootHttpRouter,
lifecycle,
}) {
if (options?.getPath) {
logger.warn(
`DEPRECATION WARNING: The 'getPath' option for HttpRouterService is deprecated. The ability to reconfigure the '/api/' path prefix for plugins will be removed in the future.`,
);
}
const getPath = options?.getPath ?? (id => `/api/${id}`);
const path = getPath(plugin.getId());
const router = PromiseRouter();
rootHttpRouter.use(path, router);
const credentialsBarrier = createCredentialsBarrier({
httpAuth,
config,
});
router.use(createAuthIntegrationRouter({ auth }));
router.use(createLifecycleMiddleware({ lifecycle }));
router.use(credentialsBarrier.middleware);
router.use(createCookieAuthRefreshMiddleware({ auth, httpAuth }));
return {
use(handler: Handler): void {
router.use(handler);
},
addAuthPolicy(policy: HttpRouterServiceAuthPolicy): void {
credentialsBarrier.addAuthPolicy(policy);
},
};
},
}),
);
@@ -0,0 +1,20 @@
/*
* 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 { httpRouterServiceFactory } from './httpRouterServiceFactory';
export type { HttpRouterFactoryOptions } from './httpRouterServiceFactory';
export { createLifecycleMiddleware } from './createLifecycleMiddleware';
export type { LifecycleMiddlewareOptions } from './createLifecycleMiddleware';
@@ -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 { loggerServiceFactory } from './loggerServiceFactory';
@@ -0,0 +1,40 @@
/*
* 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,
} from '@backstage/backend-plugin-api';
/**
* Plugin-level logging.
*
* See {@link @backstage/code-plugin-api#LoggerService}
* and {@link https://backstage.io/docs/backend-system/core-services/logger | the service docs}
* for more information.
*
* @public
*/
export const loggerServiceFactory = createServiceFactory({
service: coreServices.logger,
deps: {
rootLogger: coreServices.rootLogger,
plugin: coreServices.pluginMetadata,
},
factory({ rootLogger, plugin }) {
return rootLogger.child({ plugin: plugin.getId() });
},
});
@@ -0,0 +1,74 @@
/*
* 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 { loadConfigSchema } from '@backstage/config-loader';
import { createConfigSecretEnumerator } from './createConfigSecretEnumerator';
import { mockServices } from '@backstage/backend-test-utils';
describe('createConfigSecretEnumerator', () => {
it('should enumerate secrets', async () => {
const logger = mockServices.logger.mock();
const enumerate = await createConfigSecretEnumerator({
logger,
});
const secrets = enumerate(
mockServices.rootConfig({
data: {
backend: { auth: { keys: [{ secret: 'my-secret-password' }] } },
},
}),
);
expect(Array.from(secrets)).toEqual(['my-secret-password']);
}, 20_000); // Bit higher timeout since we're loading all config schemas in the repo
it('should enumerate secrets with explicit schema', async () => {
const logger = mockServices.logger.mock();
const enumerate = await createConfigSecretEnumerator({
logger,
schema: await loadConfigSchema({
serialized: {
schemas: [
{
value: {
type: 'object',
properties: {
secret: {
visibility: 'secret',
type: 'string',
},
},
},
path: '/mock',
},
],
backstageConfigSchemaVersion: 1,
},
}),
});
const secrets = enumerate(
mockServices.rootConfig({
data: {
secret: 'my-secret',
other: 'not-secret',
},
}),
);
expect(Array.from(secrets)).toEqual(['my-secret']);
});
});
@@ -0,0 +1,54 @@
/*
* 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 { LoggerService } from '@backstage/backend-plugin-api';
import type { Config } from '@backstage/config';
import { ConfigSchema, loadConfigSchema } from '@backstage/config-loader';
import { getPackages } from '@manypkg/get-packages';
/** @public */
export async function createConfigSecretEnumerator(options: {
logger: LoggerService;
dir?: string;
schema?: ConfigSchema;
}): Promise<(config: Config) => Iterable<string>> {
const { logger, dir = process.cwd() } = options;
const { packages } = await getPackages(dir);
const schema =
options.schema ??
(await loadConfigSchema({
dependencies: packages.map(p => p.packageJson.name),
}));
return (config: Config) => {
const [secretsData] = schema.process(
[{ data: config.getOptional() ?? {}, context: 'schema-enumerator' }],
{
visibility: ['secret'],
ignoreSchemaErrors: true,
},
);
const secrets = new Set<string>();
JSON.parse(
JSON.stringify(secretsData.data),
(_, v) => typeof v === 'string' && secrets.add(v),
);
logger.info(
`Found ${secrets.size} new secrets in config that will be redacted`,
);
return secrets;
};
}
@@ -14,5 +14,8 @@
* limitations under the License.
*/
export { rootConfigServiceFactory } from './rootConfigServiceFactory';
export type { RootConfigFactoryOptions } from './rootConfigServiceFactory';
export { createConfigSecretEnumerator } from './createConfigSecretEnumerator';
export {
rootConfigServiceFactory,
type RootConfigFactoryOptions,
} from './rootConfigServiceFactory';
@@ -0,0 +1,124 @@
/*
* 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 express from 'express';
import request from 'supertest';
import { DefaultRootHttpRouter } from './DefaultRootHttpRouter';
describe('DefaultRootHttpRouter', () => {
it.each([
[['/b'], '/a'],
[['/a'], '/aa/b'],
[['/aa'], '/a/b'],
[['/a/b'], '/aa'],
[['/b/a'], '/a'],
[['/a'], '/aa'],
])(`with existing paths %s, adds %s without conflict`, (existing, added) => {
const router = DefaultRootHttpRouter.create();
for (const path of existing) {
router.use(path, () => {});
}
expect(() => router.use(added, () => {})).not.toThrow();
});
it.each([
[['/a'], '/a', '/a'],
[['/a'], '/a/b', '/a'],
[['/a/b'], '/a', '/a/b'],
])(
`find conflict when existing paths %s, adds %s`,
(existing, added, conflict) => {
const router = DefaultRootHttpRouter.create();
for (const path of existing) {
router.use(path, () => {});
}
expect(() => router.use(added, () => {})).toThrow(
`Path ${added} conflicts with the existing path ${conflict}`,
);
},
);
it('should not be possible to supply an empty indexPath', () => {
expect(() => DefaultRootHttpRouter.create({ indexPath: '' })).toThrow(
'indexPath option may not be an empty string',
);
});
it('will always prioritize non-index paths', async () => {
const router = DefaultRootHttpRouter.create({ indexPath: '/x' });
const app = express();
app.use(router.handler());
const routerX = express.Router();
routerX.get('/a', (_req, res) => res.status(201).end());
const routerY = express.Router();
routerY.get('/a', (_req, res) => res.status(202).end());
await request(app).get('/').expect(404);
await request(app).get('/a').expect(404);
await request(app).get('/x/a').expect(404);
await request(app).get('/y/a').expect(404);
router.use('/x', routerX);
await request(app).get('/').expect(404);
await request(app).get('/a').expect(201);
await request(app).get('/x/a').expect(201);
await request(app).get('/y/a').expect(404);
router.use('/y', routerY);
await request(app).get('/').expect(404);
await request(app).get('/a').expect(201);
await request(app).get('/x/a').expect(201);
await request(app).get('/y/a').expect(202);
expect('test').toBe('test');
});
it('should treat unknown /api/ routes as 404', async () => {
const router = DefaultRootHttpRouter.create();
const app = express();
app.use(router.handler());
router.use('/api/app', (_req, res) => res.status(201).end());
router.use('/api/catalog', (_req, res) => res.status(202).end());
await request(app).get('/').expect(201);
await request(app).get('/api/catalog').expect(202);
await request(app).get('/unknown').expect(201);
await request(app).get('/api/unknown').expect(404);
expect('test').toBe('test');
});
it('should treat unknown /api/ routes as 404 without an index path', async () => {
const router = DefaultRootHttpRouter.create({ indexPath: false });
const app = express();
app.use(router.handler());
router.use('/api/app', (_req, res) => res.status(201).end());
router.use('/api/catalog', (_req, res) => res.status(202).end());
await request(app).get('/').expect(404);
await request(app).get('/api/catalog').expect(202);
await request(app).get('/unknown').expect(404);
await request(app).get('/api/unknown').expect(404);
expect('test').toBe('test');
});
});
@@ -0,0 +1,115 @@
/*
* 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.
*/
import { RootHttpRouterService } from '@backstage/backend-plugin-api';
import { Handler, Router } from 'express';
import trimEnd from 'lodash/trimEnd';
function normalizePath(path: string): string {
return `${trimEnd(path, '/')}/`;
}
/**
* Options for the {@link DefaultRootHttpRouter} class.
*
* @public
*/
export interface DefaultRootHttpRouterOptions {
/**
* The path to forward all unmatched requests to. Defaults to '/api/app' if
* not given. Disables index path behavior if false is given.
*/
indexPath?: string | false;
}
/**
* The default implementation of the {@link @backstage/backend-plugin-api#RootHttpRouterService} interface for
* {@link @backstage/backend-plugin-api#coreServices.rootHttpRouter}.
*
* @public
*/
export class DefaultRootHttpRouter implements RootHttpRouterService {
#indexPath?: string;
#router = Router();
#namedRoutes = Router();
#indexRouter = Router();
#existingPaths = new Array<string>();
static create(options?: DefaultRootHttpRouterOptions) {
let indexPath;
if (options?.indexPath === false) {
indexPath = undefined;
} else if (options?.indexPath === undefined) {
indexPath = '/api/app';
} else if (options?.indexPath === '') {
throw new Error('indexPath option may not be an empty string');
} else {
indexPath = options.indexPath;
}
return new DefaultRootHttpRouter(indexPath);
}
private constructor(indexPath?: string) {
this.#indexPath = indexPath;
this.#router.use(this.#namedRoutes);
// Any request with a /api/ prefix will skip the index router, even if no named router matches
this.#router.use('/api/', (_req, _res, next) => {
next('router');
});
if (this.#indexPath) {
this.#router.use(this.#indexRouter);
}
}
use(path: string, handler: Handler) {
if (path.match(/^[/\s]*$/)) {
throw new Error(`Root router path may not be empty`);
}
const conflictingPath = this.#findConflictingPath(path);
if (conflictingPath) {
throw new Error(
`Path ${path} conflicts with the existing path ${conflictingPath}`,
);
}
this.#existingPaths.push(path);
this.#namedRoutes.use(path, handler);
if (this.#indexPath === path) {
this.#indexRouter.use(handler);
}
}
handler(): Handler {
return this.#router;
}
#findConflictingPath(newPath: string): string | undefined {
const normalizedNewPath = normalizePath(newPath);
for (const path of this.#existingPaths) {
const normalizedPath = normalizePath(path);
if (normalizedPath.startsWith(normalizedNewPath)) {
return path;
}
if (normalizedNewPath.startsWith(normalizedPath)) {
return path;
}
}
return undefined;
}
}
@@ -0,0 +1,30 @@
/*
* 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 { readHttpServerOptions } from './config';
export { createHttpServer } from './createHttpServer';
export { MiddlewareFactory } from './MiddlewareFactory';
export type {
MiddlewareFactoryErrorOptions,
MiddlewareFactoryOptions,
} from './MiddlewareFactory';
export { readCorsOptions } from './readCorsOptions';
export { readHelmetOptions } from './readHelmetOptions';
export type {
ExtendedHttpServer,
HttpServerCertificateOptions,
HttpServerOptions,
} from './types';
@@ -0,0 +1,26 @@
/*
* 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 {
DefaultRootHttpRouter,
type DefaultRootHttpRouterOptions,
} from './DefaultRootHttpRouter';
export * from './http';
export {
rootHttpRouterServiceFactory,
type RootHttpRouterConfigureContext,
type RootHttpRouterFactoryOptions,
} from './rootHttpRouterServiceFactory';
@@ -0,0 +1,119 @@
/*
* 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 {
RootConfigService,
coreServices,
createServiceFactory,
LifecycleService,
LoggerService,
} from '@backstage/backend-plugin-api';
import express, { RequestHandler, Express } from 'express';
import type { Server } from 'node:http';
import {
createHttpServer,
MiddlewareFactory,
readHttpServerOptions,
} from './http';
import { DefaultRootHttpRouter } from './DefaultRootHttpRouter';
/**
* @public
*/
export interface RootHttpRouterConfigureContext {
app: Express;
server: Server;
middleware: MiddlewareFactory;
routes: RequestHandler;
config: RootConfigService;
logger: LoggerService;
lifecycle: LifecycleService;
applyDefaults: () => void;
}
/**
* HTTP route registration for root services.
*
* See {@link @backstage/code-plugin-api#RootHttpRouterService}
* and {@link https://backstage.io/docs/backend-system/core-services/root-http-router | the service docs}
* for more information.
*
* @public
*/
export type RootHttpRouterFactoryOptions = {
/**
* The path to forward all unmatched requests to. Defaults to '/api/app' if
* not given. Disables index path behavior if false is given.
*/
indexPath?: string | false;
configure?(context: RootHttpRouterConfigureContext): void;
};
function defaultConfigure({ applyDefaults }: RootHttpRouterConfigureContext) {
applyDefaults();
}
/** @public */
export const rootHttpRouterServiceFactory = createServiceFactory(
(options?: RootHttpRouterFactoryOptions) => ({
service: coreServices.rootHttpRouter,
deps: {
config: coreServices.rootConfig,
rootLogger: coreServices.rootLogger,
lifecycle: coreServices.rootLifecycle,
},
async factory({ config, rootLogger, lifecycle }) {
const { indexPath, configure = defaultConfigure } = options ?? {};
const logger = rootLogger.child({ service: 'rootHttpRouter' });
const app = express();
const router = DefaultRootHttpRouter.create({ indexPath });
const middleware = MiddlewareFactory.create({ config, logger });
const routes = router.handler();
const server = await createHttpServer(
app,
readHttpServerOptions(config.getOptionalConfig('backend')),
{ logger },
);
configure({
app,
server,
routes,
middleware,
config,
logger,
lifecycle,
applyDefaults() {
app.use(middleware.helmet());
app.use(middleware.cors());
app.use(middleware.compression());
app.use(middleware.logging());
app.use(routes);
app.use(middleware.notFound());
app.use(middleware.error());
},
});
lifecycle.addShutdownHook(() => server.stop());
await server.start();
return router;
},
}),
);
@@ -0,0 +1,192 @@
/*
* 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.
*/
import {
LoggerService,
RootLoggerService,
} from '@backstage/backend-plugin-api';
import { JsonObject } from '@backstage/types';
import { Format, TransformableInfo } from 'logform';
import {
Logger,
format,
createLogger,
transports,
transport as Transport,
} from 'winston';
import { MESSAGE } from 'triple-beam';
import { escapeRegExp } from '../../lib/escapeRegExp';
/**
* @public
*/
export interface WinstonLoggerOptions {
meta?: JsonObject;
level?: string;
format?: Format;
transports?: Transport[];
}
/**
* A {@link @backstage/backend-plugin-api#LoggerService} implementation based on winston.
*
* @public
*/
export class WinstonLogger implements RootLoggerService {
#winston: Logger;
#addRedactions?: (redactions: Iterable<string>) => void;
/**
* Creates a {@link WinstonLogger} instance.
*/
static create(options: WinstonLoggerOptions): WinstonLogger {
const redacter = WinstonLogger.redacter();
const defaultFormatter =
process.env.NODE_ENV === 'production'
? format.json()
: WinstonLogger.colorFormat();
let logger = createLogger({
level: process.env.LOG_LEVEL || options.level || 'info',
format: format.combine(
options.format ?? defaultFormatter,
redacter.format,
),
transports: options.transports ?? new transports.Console(),
});
if (options.meta) {
logger = logger.child(options.meta);
}
return new WinstonLogger(logger, redacter.add);
}
/**
* Creates a winston log formatter for redacting secrets.
*/
static redacter(): {
format: Format;
add: (redactions: Iterable<string>) => void;
} {
const redactionSet = new Set<string>();
let redactionPattern: RegExp | undefined = undefined;
return {
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) {
// Trimming the string ensures that we don't accdentally get extra
// newlines or other whitespace interfering with the redaction; this
// can happen for example when using string literals in yaml
const redaction = redactionToTrim.trim();
// Exclude secrets that are empty or just one character in length. These
// typically mean that you are running local dev or tests, or using the
// --lax flag which sets things to just 'x'.
if (redaction.length <= 1) {
continue;
}
if (!redactionSet.has(redaction)) {
redactionSet.add(redaction);
added += 1;
}
}
if (added > 0) {
const redactions = Array.from(redactionSet)
.map(r => escapeRegExp(r))
.join('|');
redactionPattern = new RegExp(`(${redactions})`, 'g');
}
},
};
}
/**
* Creates a pretty printed winston log formatter.
*/
static colorFormat(): Format {
const colorizer = format.colorize();
return format.combine(
format.timestamp(),
format.colorize({
colors: {
timestamp: 'dim',
prefix: 'blue',
field: 'cyan',
debug: 'grey',
},
}),
format.printf((info: TransformableInfo) => {
const { timestamp, level, message, plugin, service, ...fields } = info;
const prefix = plugin || service;
const timestampColor = colorizer.colorize('timestamp', timestamp);
const prefixColor = colorizer.colorize('prefix', prefix);
const extraFields = Object.entries(fields)
.map(
([key, value]) =>
`${colorizer.colorize('field', `${key}`)}=${value}`,
)
.join(' ');
return `${timestampColor} ${prefixColor} ${level} ${message} ${extraFields}`;
}),
);
}
private constructor(
winston: Logger,
addRedactions?: (redactions: Iterable<string>) => void,
) {
this.#winston = winston;
this.#addRedactions = addRedactions;
}
error(message: string, meta?: JsonObject): void {
this.#winston.error(message, meta);
}
warn(message: string, meta?: JsonObject): void {
this.#winston.warn(message, meta);
}
info(message: string, meta?: JsonObject): void {
this.#winston.info(message, meta);
}
debug(message: string, meta?: JsonObject): void {
this.#winston.debug(message, meta);
}
child(meta: JsonObject): LoggerService {
return new WinstonLogger(this.#winston.child(meta));
}
addRedactions(redactions: Iterable<string>) {
this.#addRedactions?.(redactions);
}
}
@@ -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 { rootLoggerServiceFactory } from './rootLoggerServiceFactory';
export { WinstonLogger, type WinstonLoggerOptions } from './WinstonLogger';
@@ -0,0 +1,58 @@
/*
* 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,
} from '@backstage/backend-plugin-api';
import { transports, format } from 'winston';
import { WinstonLogger } from '../rootLogger/WinstonLogger';
import { createConfigSecretEnumerator } from '../rootConfig/createConfigSecretEnumerator';
/**
* Root-level logging.
*
* See {@link @backstage/code-plugin-api#RootLoggerService}
* and {@link https://backstage.io/docs/backend-system/core-services/root-logger | the service docs}
* for more information.
*
* @public
*/
export const rootLoggerServiceFactory = createServiceFactory({
service: coreServices.rootLogger,
deps: {
config: coreServices.rootConfig,
},
async factory({ config }) {
const logger = WinstonLogger.create({
meta: {
service: 'backstage',
},
level: process.env.LOG_LEVEL || 'info',
format:
process.env.NODE_ENV === 'production'
? format.json()
: WinstonLogger.colorFormat(),
transports: [new transports.Console()],
});
const secretEnumerator = await createConfigSecretEnumerator({ logger });
logger.addRedactions(secretEnumerator(config));
config.subscribe?.(() => logger.addRedactions(secretEnumerator(config)));
return logger;
},
});
@@ -0,0 +1,34 @@
/*
* Copyright 2021 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 { isValidUrl } from './urls';
describe('isValidUrl', () => {
it('should return true for url', () => {
const validUrl = isValidUrl('http://some.valid.url');
expect(validUrl).toBe(true);
});
it('should return false for absolute path', () => {
const validUrl = isValidUrl('/some/absolute/path');
expect(validUrl).toBe(false);
});
it('should return false for relative path', () => {
const validUrl = isValidUrl('../some/relative/path');
expect(validUrl).toBe(false);
});
});
+25
View File
@@ -0,0 +1,25 @@
/*
* Copyright 2021 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 function isValidUrl(url: string): boolean {
try {
// eslint-disable-next-line no-new
new URL(url);
return true;
} catch {
return false;
}
}
+2 -2
View File
@@ -22,7 +22,7 @@ import { EventsService } from '@backstage/plugin-events-node';
import { ExtendedHttpServer } from '@backstage/backend-app-api';
import { ExtensionPoint } from '@backstage/backend-plugin-api';
import { HttpAuthService } from '@backstage/backend-plugin-api';
import { HttpRouterFactoryOptions } from '@backstage/backend-app-api';
import { HttpRouterFactoryOptions } from '@backstage/backend-defaults/httpRouter';
import { HttpRouterService } from '@backstage/backend-plugin-api';
import { IdentityService } from '@backstage/backend-plugin-api';
import { JsonObject } from '@backstage/types';
@@ -32,7 +32,7 @@ import { LifecycleService } from '@backstage/backend-plugin-api';
import { LoggerService } from '@backstage/backend-plugin-api';
import { PermissionsService } from '@backstage/backend-plugin-api';
import { RootConfigService } from '@backstage/backend-plugin-api';
import { RootHttpRouterFactoryOptions } from '@backstage/backend-app-api';
import { RootHttpRouterFactoryOptions } from '@backstage/backend-defaults/rootHttpRouter';
import { RootHttpRouterService } from '@backstage/backend-plugin-api';
import { RootLifecycleService } from '@backstage/backend-plugin-api';
import { RootLoggerService } from '@backstage/backend-plugin-api';
+1
View File
@@ -46,6 +46,7 @@
},
"dependencies": {
"@backstage/backend-app-api": "workspace:^",
"@backstage/backend-defaults": "workspace:^",
"@backstage/backend-plugin-api": "workspace:^",
"@backstage/config": "workspace:^",
"@backstage/errors": "workspace:^",
@@ -14,48 +14,48 @@
* limitations under the License.
*/
import { cacheServiceFactory } from '@backstage/backend-defaults/cache';
import { databaseServiceFactory } from '@backstage/backend-defaults/database';
import {
RootConfigService,
coreServices,
createServiceFactory,
HostDiscovery,
discoveryServiceFactory,
} from '@backstage/backend-defaults/discovery';
import { httpRouterServiceFactory } from '@backstage/backend-defaults/httpRouter';
import { lifecycleServiceFactory } from '@backstage/backend-defaults/lifecycle';
import { loggerServiceFactory } from '@backstage/backend-defaults/logger';
import { permissionsServiceFactory } from '@backstage/backend-defaults/permissions';
import { rootHttpRouterServiceFactory } from '@backstage/backend-defaults/rootHttpRouter';
import { rootLifecycleServiceFactory } from '@backstage/backend-defaults/rootLifecycle';
import { schedulerServiceFactory } from '@backstage/backend-defaults/scheduler';
import { urlReaderServiceFactory } from '@backstage/backend-defaults/urlReader';
import {
AuthService,
BackstageCredentials,
BackstageUserInfo,
DiscoveryService,
HttpAuthService,
IdentityService,
LoggerService,
RootConfigService,
ServiceFactory,
ServiceRef,
TokenManagerService,
AuthService,
DiscoveryService,
HttpAuthService,
BackstageCredentials,
BackstageUserInfo,
UserInfoService,
coreServices,
createServiceFactory,
} from '@backstage/backend-plugin-api';
import {
cacheServiceFactory,
databaseServiceFactory,
httpRouterServiceFactory,
lifecycleServiceFactory,
loggerServiceFactory,
permissionsServiceFactory,
rootHttpRouterServiceFactory,
rootLifecycleServiceFactory,
schedulerServiceFactory,
urlReaderServiceFactory,
discoveryServiceFactory,
HostDiscovery,
} from '@backstage/backend-app-api';
import { ConfigReader } from '@backstage/config';
import { JsonObject } from '@backstage/types';
import { MockIdentityService } from './MockIdentityService';
import { MockRootLoggerService } from './MockRootLoggerService';
import { MockAuthService } from './MockAuthService';
import { MockHttpAuthService } from './MockHttpAuthService';
import { mockCredentials } from './mockCredentials';
import { MockUserInfoService } from './MockUserInfoService';
import {
eventsServiceFactory,
eventsServiceRef,
} from '@backstage/plugin-events-node';
import { JsonObject } from '@backstage/types';
import { MockAuthService } from './MockAuthService';
import { MockHttpAuthService } from './MockHttpAuthService';
import { MockIdentityService } from './MockIdentityService';
import { MockRootLoggerService } from './MockRootLoggerService';
import { MockUserInfoService } from './MockUserInfoService';
import { mockCredentials } from './mockCredentials';
/** @internal */
function createLoggerMock() {
+26
View File
@@ -3457,6 +3457,7 @@ __metadata:
resolution: "@backstage/backend-app-api@workspace:packages/backend-app-api"
dependencies:
"@backstage/backend-common": "workspace:^"
"@backstage/backend-defaults": "workspace:^"
"@backstage/backend-plugin-api": "workspace:^"
"@backstage/backend-tasks": "workspace:^"
"@backstage/backend-test-utils": "workspace:^"
@@ -3611,6 +3612,7 @@ __metadata:
"@backstage/backend-plugin-api": "workspace:^"
"@backstage/backend-test-utils": "workspace:^"
"@backstage/cli": "workspace:^"
"@backstage/cli-common": "workspace:^"
"@backstage/config": "workspace:^"
"@backstage/config-loader": "workspace:^"
"@backstage/errors": "workspace:^"
@@ -3623,35 +3625,58 @@ __metadata:
"@google-cloud/storage": ^7.0.0
"@keyv/memcache": ^1.3.5
"@keyv/redis": ^2.5.3
"@manypkg/get-packages": ^1.1.3
"@octokit/rest": ^19.0.3
"@opentelemetry/api": ^1.3.0
"@types/cors": ^2.8.6
"@types/express": ^4.17.6
"@types/http-errors": ^2.0.0
"@types/morgan": ^1.9.0
"@types/node-forge": ^1.3.0
"@types/stoppable": ^1.1.0
archiver: ^6.0.0
aws-sdk-client-mock: ^4.0.0
base64-stream: ^1.0.0
better-sqlite3: ^9.0.0
compression: ^1.7.4
concat-stream: ^2.0.0
cookie: ^0.6.0
cors: ^2.8.5
cron: ^3.0.0
express: ^4.17.1
express-promise-router: ^4.1.0
fs-extra: ^11.2.0
git-url-parse: ^14.0.0
helmet: ^6.0.0
http-errors: ^2.0.0
isomorphic-git: ^1.23.0
jose: ^5.0.0
keyv: ^4.5.2
knex: ^3.0.0
lodash: ^4.17.21
logform: ^2.3.2
luxon: ^3.0.0
minimatch: ^9.0.0
minimist: ^1.2.5
morgan: ^1.10.0
msw: ^1.0.0
mysql2: ^3.0.0
node-fetch: ^2.6.7
node-forge: ^1.3.1
p-limit: ^3.1.0
path-to-regexp: ^6.2.1
pg: ^8.11.3
pg-connection-string: ^2.3.0
raw-body: ^2.4.1
selfsigned: ^2.0.0
stoppable: ^1.1.0
supertest: ^6.1.3
tar: ^6.1.12
triple-beam: ^1.4.1
uuid: ^9.0.0
wait-for-expect: ^3.0.2
winston: ^3.2.1
winston-transport: ^4.5.0
yauzl: ^3.0.0
yn: ^4.0.0
zod: ^3.22.4
@@ -3771,6 +3796,7 @@ __metadata:
resolution: "@backstage/backend-test-utils@workspace:packages/backend-test-utils"
dependencies:
"@backstage/backend-app-api": "workspace:^"
"@backstage/backend-defaults": "workspace:^"
"@backstage/backend-plugin-api": "workspace:^"
"@backstage/cli": "workspace:^"
"@backstage/config": "workspace:^"