From 6c11f6e7e77542a8d9969f0f2fc9b1ffc48eab6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Tue, 18 Jun 2024 10:27:04 +0200 Subject: [PATCH] [NBS 1.0] move more implementations from app-api to defaults MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/tiny-peas-shop.md | 5 + package.json | 2 +- packages/backend-app-api/api-report.md | 215 +++++++----------- packages/backend-app-api/package.json | 1 + packages/backend-app-api/src/config/config.ts | 46 +--- packages/backend-app-api/src/http/index.ts | 78 +++++-- .../src/logging/WinstonLogger.ts | 140 ++---------- .../services/implementations/deprecated.ts | 4 + .../discovery/HostDiscovery.ts | 74 +----- .../httpRouter/createLifecycleMiddleware.ts | 79 +------ .../httpRouter/httpRouterServiceFactory.ts | 78 +------ .../src/services/implementations/index.ts | 4 - .../logger/loggerServiceFactory.ts | 18 +- .../rootHttpRouter/DefaultRootHttpRouter.ts | 87 ++----- .../rootHttpRouterServiceFactory.ts | 101 ++------ .../rootLogger/rootLoggerServiceFactory.ts | 36 +-- .../src/wiring/BackendInitializer.test.ts | 9 +- .../src/deprecated/middleware/errorHandler.ts | 2 +- .../deprecated/middleware/notFoundHandler.ts | 2 +- .../middleware/requestLoggingHandler.ts | 2 +- .../service/lib/ServiceBuilderImpl.ts | 2 +- .../backend-defaults/api-report-httpRouter.md | 35 +++ .../backend-defaults/api-report-logger.md | 16 ++ .../backend-defaults/api-report-rootConfig.md | 10 + .../api-report-rootHttpRouter.md | 147 ++++++++++++ .../backend-defaults/api-report-rootLogger.md | 54 +++++ packages/backend-defaults/package.json | 40 ++++ .../backend-defaults/src/CreateBackend.ts | 8 +- .../entrypoints/discovery/HostDiscovery.ts | 2 +- .../httpRouter/createAuthIntegrationRouter.ts | 1 + .../createCookieAuthRefreshMiddleware.test.ts | 0 .../createCookieAuthRefreshMiddleware.ts | 0 .../createCredentialsBarrier.test.ts | 2 +- .../httpRouter/createCredentialsBarrier.ts | 0 .../createLifecycleMiddleware.test.ts | 0 .../httpRouter/createLifecycleMiddleware.ts | 106 +++++++++ .../httpRouterServiceFactory.test.ts | 0 .../httpRouter/httpRouterServiceFactory.ts | 101 ++++++++ .../src/entrypoints/httpRouter/index.ts | 20 ++ .../src/entrypoints/logger/index.ts | 17 ++ .../logger/loggerServiceFactory.ts | 40 ++++ .../createConfigSecretEnumerator.test.ts | 74 ++++++ .../createConfigSecretEnumerator.ts | 54 +++++ .../src/entrypoints/rootConfig/index.ts | 7 +- .../DefaultRootHttpRouter.test.ts | 124 ++++++++++ .../rootHttpRouter/DefaultRootHttpRouter.ts | 115 ++++++++++ .../http/MiddlewareFactory.test.ts | 0 .../rootHttpRouter}/http/MiddlewareFactory.ts | 0 .../http/applyInternalErrorFilter.ts | 0 .../rootHttpRouter}/http/config.test.ts | 0 .../rootHttpRouter}/http/config.ts | 0 .../rootHttpRouter}/http/createHttpServer.ts | 0 .../http/getGeneratedCertificate.ts | 0 .../entrypoints/rootHttpRouter/http/index.ts | 30 +++ .../http/readCorsOptions.test.ts | 0 .../rootHttpRouter}/http/readCorsOptions.ts | 0 .../http/readHelmetOptions.test.ts | 0 .../rootHttpRouter}/http/readHelmetOptions.ts | 0 .../entrypoints/rootHttpRouter}/http/types.ts | 0 .../src/entrypoints/rootHttpRouter/index.ts | 26 +++ .../rootHttpRouterServiceFactory.ts | 119 ++++++++++ .../rootLogger}/WinstonLogger.test.ts | 0 .../entrypoints/rootLogger/WinstonLogger.ts | 192 ++++++++++++++++ .../src/entrypoints/rootLogger/index.ts | 18 ++ .../rootLogger/rootLoggerServiceFactory.ts | 58 +++++ .../src/lib/escapeRegExp.test.ts | 0 .../src/lib/escapeRegExp.ts | 0 .../backend-defaults/src/lib/urls.test.ts | 34 +++ packages/backend-defaults/src/lib/urls.ts | 25 ++ packages/backend-test-utils/api-report.md | 4 +- packages/backend-test-utils/package.json | 1 + .../src/next/services/mockServices.ts | 58 ++--- yarn.lock | 26 +++ 73 files changed, 1775 insertions(+), 774 deletions(-) create mode 100644 .changeset/tiny-peas-shop.md create mode 100644 packages/backend-defaults/api-report-httpRouter.md create mode 100644 packages/backend-defaults/api-report-logger.md create mode 100644 packages/backend-defaults/api-report-rootHttpRouter.md create mode 100644 packages/backend-defaults/api-report-rootLogger.md rename packages/{backend-app-api/src/services/implementations => backend-defaults/src/entrypoints}/httpRouter/createAuthIntegrationRouter.ts (99%) rename packages/{backend-app-api/src/services/implementations => backend-defaults/src/entrypoints}/httpRouter/createCookieAuthRefreshMiddleware.test.ts (100%) rename packages/{backend-app-api/src/services/implementations => backend-defaults/src/entrypoints}/httpRouter/createCookieAuthRefreshMiddleware.ts (100%) rename packages/{backend-app-api/src/services/implementations => backend-defaults/src/entrypoints}/httpRouter/createCredentialsBarrier.test.ts (98%) rename packages/{backend-app-api/src/services/implementations => backend-defaults/src/entrypoints}/httpRouter/createCredentialsBarrier.ts (100%) rename packages/{backend-app-api/src/services/implementations => backend-defaults/src/entrypoints}/httpRouter/createLifecycleMiddleware.test.ts (100%) create mode 100644 packages/backend-defaults/src/entrypoints/httpRouter/createLifecycleMiddleware.ts rename packages/{backend-app-api/src/services/implementations => backend-defaults/src/entrypoints}/httpRouter/httpRouterServiceFactory.test.ts (100%) create mode 100644 packages/backend-defaults/src/entrypoints/httpRouter/httpRouterServiceFactory.ts create mode 100644 packages/backend-defaults/src/entrypoints/httpRouter/index.ts create mode 100644 packages/backend-defaults/src/entrypoints/logger/index.ts create mode 100644 packages/backend-defaults/src/entrypoints/logger/loggerServiceFactory.ts create mode 100644 packages/backend-defaults/src/entrypoints/rootConfig/createConfigSecretEnumerator.test.ts create mode 100644 packages/backend-defaults/src/entrypoints/rootConfig/createConfigSecretEnumerator.ts create mode 100644 packages/backend-defaults/src/entrypoints/rootHttpRouter/DefaultRootHttpRouter.test.ts create mode 100644 packages/backend-defaults/src/entrypoints/rootHttpRouter/DefaultRootHttpRouter.ts rename packages/{backend-app-api/src => backend-defaults/src/entrypoints/rootHttpRouter}/http/MiddlewareFactory.test.ts (100%) rename packages/{backend-app-api/src => backend-defaults/src/entrypoints/rootHttpRouter}/http/MiddlewareFactory.ts (100%) rename packages/{backend-app-api/src => backend-defaults/src/entrypoints/rootHttpRouter}/http/applyInternalErrorFilter.ts (100%) rename packages/{backend-app-api/src => backend-defaults/src/entrypoints/rootHttpRouter}/http/config.test.ts (100%) rename packages/{backend-app-api/src => backend-defaults/src/entrypoints/rootHttpRouter}/http/config.ts (100%) rename packages/{backend-app-api/src => backend-defaults/src/entrypoints/rootHttpRouter}/http/createHttpServer.ts (100%) rename packages/{backend-app-api/src => backend-defaults/src/entrypoints/rootHttpRouter}/http/getGeneratedCertificate.ts (100%) create mode 100644 packages/backend-defaults/src/entrypoints/rootHttpRouter/http/index.ts rename packages/{backend-app-api/src => backend-defaults/src/entrypoints/rootHttpRouter}/http/readCorsOptions.test.ts (100%) rename packages/{backend-app-api/src => backend-defaults/src/entrypoints/rootHttpRouter}/http/readCorsOptions.ts (100%) rename packages/{backend-app-api/src => backend-defaults/src/entrypoints/rootHttpRouter}/http/readHelmetOptions.test.ts (100%) rename packages/{backend-app-api/src => backend-defaults/src/entrypoints/rootHttpRouter}/http/readHelmetOptions.ts (100%) rename packages/{backend-app-api/src => backend-defaults/src/entrypoints/rootHttpRouter}/http/types.ts (100%) create mode 100644 packages/backend-defaults/src/entrypoints/rootHttpRouter/index.ts create mode 100644 packages/backend-defaults/src/entrypoints/rootHttpRouter/rootHttpRouterServiceFactory.ts rename packages/{backend-app-api/src/logging => backend-defaults/src/entrypoints/rootLogger}/WinstonLogger.test.ts (100%) create mode 100644 packages/backend-defaults/src/entrypoints/rootLogger/WinstonLogger.ts create mode 100644 packages/backend-defaults/src/entrypoints/rootLogger/index.ts create mode 100644 packages/backend-defaults/src/entrypoints/rootLogger/rootLoggerServiceFactory.ts rename packages/{backend-app-api => backend-defaults}/src/lib/escapeRegExp.test.ts (100%) rename packages/{backend-app-api => backend-defaults}/src/lib/escapeRegExp.ts (100%) create mode 100644 packages/backend-defaults/src/lib/urls.test.ts create mode 100644 packages/backend-defaults/src/lib/urls.ts diff --git a/.changeset/tiny-peas-shop.md b/.changeset/tiny-peas-shop.md new file mode 100644 index 0000000000..7b1ea5dba1 --- /dev/null +++ b/.changeset/tiny-peas-shop.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-test-utils': patch +--- + +Use imports from backend-defaults instead of the deprecated ones from backend-app-api diff --git a/package.json b/package.json index 03b152d1e6..0f9107afc4 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/packages/backend-app-api/api-report.md b/packages/backend-app-api/api-report.md index 565757eac4..071fd6c1d3 100644 --- a/packages/backend-app-api/api-report.md +++ b/packages/backend-app-api/api-report.md @@ -68,26 +68,20 @@ export interface Backend { // @public @deprecated (undocumented) export const cacheServiceFactory: () => ServiceFactory; -// @public (undocumented) -export function createConfigSecretEnumerator(options: { - logger: LoggerService; - dir?: string; - schema?: ConfigSchema; -}): Promise<(config: Config) => Iterable>; +// 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; +// 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; - // (undocumented) - stop(): Promise; -} +// 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; -// @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; -// @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; -// @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; // @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): 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; ``` diff --git a/packages/backend-app-api/package.json b/packages/backend-app-api/package.json index 15227aaf71..c1a0c742ea 100644 --- a/packages/backend-app-api/package.json +++ b/packages/backend-app-api/package.json @@ -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", diff --git a/packages/backend-app-api/src/config/config.ts b/packages/backend-app-api/src/config/config.ts index 60d1e46e90..957997849e 100644 --- a/packages/backend-app-api/src/config/config.ts +++ b/packages/backend-app-api/src/config/config.ts @@ -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> { - 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(); - 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; diff --git a/packages/backend-app-api/src/http/index.ts b/packages/backend-app-api/src/http/index.ts index 4a9ec14cf8..ab1d134688 100644 --- a/packages/backend-app-api/src/http/index.ts +++ b/packages/backend-app-api/src/http/index.ts @@ -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; diff --git a/packages/backend-app-api/src/logging/WinstonLogger.ts b/packages/backend-app-api/src/logging/WinstonLogger.ts index 64e97b230c..e5403906ce 100644 --- a/packages/backend-app-api/src/logging/WinstonLogger.ts +++ b/packages/backend-app-api/src/logging/WinstonLogger.ts @@ -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) => 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) => void; } { - const redactionSet = new Set(); - - 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) => 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) { - this.#addRedactions?.(redactions); + this.impl.addRedactions(redactions); } } diff --git a/packages/backend-app-api/src/services/implementations/deprecated.ts b/packages/backend-app-api/src/services/implementations/deprecated.ts index 5bf022bb57..347a968bce 100644 --- a/packages/backend-app-api/src/services/implementations/deprecated.ts +++ b/packages/backend-app-api/src/services/implementations/deprecated.ts @@ -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'; diff --git a/packages/backend-app-api/src/services/implementations/discovery/HostDiscovery.ts b/packages/backend-app-api/src/services/implementations/discovery/HostDiscovery.ts index d337da997a..5909dd1ae6 100644 --- a/packages/backend-app-api/src/services/implementations/discovery/HostDiscovery.ts +++ b/packages/backend-app-api/src/services/implementations/discovery/HostDiscovery.ts @@ -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'); - - 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 { - return this.getTargetFromConfig(pluginId, 'internal'); + return this.impl.getBaseUrl(pluginId); } async getExternalBaseUrl(pluginId: string): Promise { - return this.getTargetFromConfig(pluginId, 'external'); + return this.impl.getExternalBaseUrl(pluginId); } } diff --git a/packages/backend-app-api/src/services/implementations/httpRouter/createLifecycleMiddleware.ts b/packages/backend-app-api/src/services/implementations/httpRouter/createLifecycleMiddleware.ts index 3f8ee3ea69..d4864a23a6 100644 --- a/packages/backend-app-api/src/services/implementations/httpRouter/createLifecycleMiddleware.ts +++ b/packages/backend-app-api/src/services/implementations/httpRouter/createLifecycleMiddleware.ts @@ -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; diff --git a/packages/backend-app-api/src/services/implementations/httpRouter/httpRouterServiceFactory.ts b/packages/backend-app-api/src/services/implementations/httpRouter/httpRouterServiceFactory.ts index 4a0dec49e6..9d2b74fb43 100644 --- a/packages/backend-app-api/src/services/implementations/httpRouter/httpRouterServiceFactory.ts +++ b/packages/backend-app-api/src/services/implementations/httpRouter/httpRouterServiceFactory.ts @@ -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; diff --git a/packages/backend-app-api/src/services/implementations/index.ts b/packages/backend-app-api/src/services/implementations/index.ts index b03031549f..3a2fe819ed 100644 --- a/packages/backend-app-api/src/services/implementations/index.ts +++ b/packages/backend-app-api/src/services/implementations/index.ts @@ -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'; diff --git a/packages/backend-app-api/src/services/implementations/logger/loggerServiceFactory.ts b/packages/backend-app-api/src/services/implementations/logger/loggerServiceFactory.ts index 4eff798e11..7e7b4b98b0 100644 --- a/packages/backend-app-api/src/services/implementations/logger/loggerServiceFactory.ts +++ b/packages/backend-app-api/src/services/implementations/logger/loggerServiceFactory.ts @@ -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; diff --git a/packages/backend-app-api/src/services/implementations/rootHttpRouter/DefaultRootHttpRouter.ts b/packages/backend-app-api/src/services/implementations/rootHttpRouter/DefaultRootHttpRouter.ts index 9d7d1de120..617d5055db 100644 --- a/packages/backend-app-api/src/services/implementations/rootHttpRouter/DefaultRootHttpRouter.ts +++ b/packages/backend-app-api/src/services/implementations/rootHttpRouter/DefaultRootHttpRouter.ts @@ -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(); - 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(); } } diff --git a/packages/backend-app-api/src/services/implementations/rootHttpRouter/rootHttpRouterServiceFactory.ts b/packages/backend-app-api/src/services/implementations/rootHttpRouter/rootHttpRouterServiceFactory.ts index 88cc863374..bef1913645 100644 --- a/packages/backend-app-api/src/services/implementations/rootHttpRouter/rootHttpRouterServiceFactory.ts +++ b/packages/backend-app-api/src/services/implementations/rootHttpRouter/rootHttpRouterServiceFactory.ts @@ -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; diff --git a/packages/backend-app-api/src/services/implementations/rootLogger/rootLoggerServiceFactory.ts b/packages/backend-app-api/src/services/implementations/rootLogger/rootLoggerServiceFactory.ts index e33de09297..af1931c438 100644 --- a/packages/backend-app-api/src/services/implementations/rootLogger/rootLoggerServiceFactory.ts +++ b/packages/backend-app-api/src/services/implementations/rootLogger/rootLoggerServiceFactory.ts @@ -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; diff --git a/packages/backend-app-api/src/wiring/BackendInitializer.test.ts b/packages/backend-app-api/src/wiring/BackendInitializer.test.ts index 910ef95bea..3bbef5a4c0 100644 --- a/packages/backend-app-api/src/wiring/BackendInitializer.test.ts +++ b/packages/backend-app-api/src/wiring/BackendInitializer.test.ts @@ -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() {} diff --git a/packages/backend-common/src/deprecated/middleware/errorHandler.ts b/packages/backend-common/src/deprecated/middleware/errorHandler.ts index 618e30ffa1..3664ed0e0a 100644 --- a/packages/backend-common/src/deprecated/middleware/errorHandler.ts +++ b/packages/backend-common/src/deprecated/middleware/errorHandler.ts @@ -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'; /** diff --git a/packages/backend-common/src/deprecated/middleware/notFoundHandler.ts b/packages/backend-common/src/deprecated/middleware/notFoundHandler.ts index f499418dba..bb9504a9ec 100644 --- a/packages/backend-common/src/deprecated/middleware/notFoundHandler.ts +++ b/packages/backend-common/src/deprecated/middleware/notFoundHandler.ts @@ -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'; diff --git a/packages/backend-common/src/deprecated/middleware/requestLoggingHandler.ts b/packages/backend-common/src/deprecated/middleware/requestLoggingHandler.ts index 74859b4741..f30f2de659 100644 --- a/packages/backend-common/src/deprecated/middleware/requestLoggingHandler.ts +++ b/packages/backend-common/src/deprecated/middleware/requestLoggingHandler.ts @@ -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'; diff --git a/packages/backend-common/src/deprecated/service/lib/ServiceBuilderImpl.ts b/packages/backend-common/src/deprecated/service/lib/ServiceBuilderImpl.ts index 9b5891a5bf..b338eb20b1 100644 --- a/packages/backend-common/src/deprecated/service/lib/ServiceBuilderImpl.ts +++ b/packages/backend-common/src/deprecated/service/lib/ServiceBuilderImpl.ts @@ -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; diff --git a/packages/backend-defaults/api-report-httpRouter.md b/packages/backend-defaults/api-report-httpRouter.md new file mode 100644 index 0000000000..a7ca2c8dd3 --- /dev/null +++ b/packages/backend-defaults/api-report-httpRouter.md @@ -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; + +// @public +export interface LifecycleMiddlewareOptions { + // (undocumented) + lifecycle: LifecycleService; + startupRequestPauseTimeout?: HumanDuration; +} + +// (No @packageDocumentation comment for this package) +``` diff --git a/packages/backend-defaults/api-report-logger.md b/packages/backend-defaults/api-report-logger.md new file mode 100644 index 0000000000..fa9018a40c --- /dev/null +++ b/packages/backend-defaults/api-report-logger.md @@ -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) +``` diff --git a/packages/backend-defaults/api-report-rootConfig.md b/packages/backend-defaults/api-report-rootConfig.md index 2d46370c27..317c451e1b 100644 --- a/packages/backend-defaults/api-report-rootConfig.md +++ b/packages/backend-defaults/api-report-rootConfig.md @@ -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>; + // @public export interface RootConfigFactoryOptions { argv?: string[]; diff --git a/packages/backend-defaults/api-report-rootHttpRouter.md b/packages/backend-defaults/api-report-rootHttpRouter.md new file mode 100644 index 0000000000..b753406202 --- /dev/null +++ b/packages/backend-defaults/api-report-rootHttpRouter.md @@ -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 +/// + +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; + +// @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; + // (undocumented) + stop(): Promise; +} + +// @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; + +// (No @packageDocumentation comment for this package) +``` diff --git a/packages/backend-defaults/api-report-rootLogger.md b/packages/backend-defaults/api-report-rootLogger.md new file mode 100644 index 0000000000..70dc0deee6 --- /dev/null +++ b/packages/backend-defaults/api-report-rootLogger.md @@ -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): 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) => 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) +``` diff --git a/packages/backend-defaults/package.json b/packages/backend-defaults/package.json index 06be729126..e3e0f69c62 100644 --- a/packages/backend-defaults/package.json +++ b/packages/backend-defaults/package.json @@ -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" diff --git a/packages/backend-defaults/src/CreateBackend.ts b/packages/backend-defaults/src/CreateBackend.ts index 834d54e9e5..9b3fa5d649 100644 --- a/packages/backend-defaults/src/CreateBackend.ts +++ b/packages/backend-defaults/src/CreateBackend.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'; diff --git a/packages/backend-defaults/src/entrypoints/discovery/HostDiscovery.ts b/packages/backend-defaults/src/entrypoints/discovery/HostDiscovery.ts index 180c8706d5..559dda2aa0 100644 --- a/packages/backend-defaults/src/entrypoints/discovery/HostDiscovery.ts +++ b/packages/backend-defaults/src/entrypoints/discovery/HostDiscovery.ts @@ -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 }; diff --git a/packages/backend-app-api/src/services/implementations/httpRouter/createAuthIntegrationRouter.ts b/packages/backend-defaults/src/entrypoints/httpRouter/createAuthIntegrationRouter.ts similarity index 99% rename from packages/backend-app-api/src/services/implementations/httpRouter/createAuthIntegrationRouter.ts rename to packages/backend-defaults/src/entrypoints/httpRouter/createAuthIntegrationRouter.ts index 94650199f3..ef36e7ba2d 100644 --- a/packages/backend-app-api/src/services/implementations/httpRouter/createAuthIntegrationRouter.ts +++ b/packages/backend-defaults/src/entrypoints/httpRouter/createAuthIntegrationRouter.ts @@ -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'; diff --git a/packages/backend-app-api/src/services/implementations/httpRouter/createCookieAuthRefreshMiddleware.test.ts b/packages/backend-defaults/src/entrypoints/httpRouter/createCookieAuthRefreshMiddleware.test.ts similarity index 100% rename from packages/backend-app-api/src/services/implementations/httpRouter/createCookieAuthRefreshMiddleware.test.ts rename to packages/backend-defaults/src/entrypoints/httpRouter/createCookieAuthRefreshMiddleware.test.ts diff --git a/packages/backend-app-api/src/services/implementations/httpRouter/createCookieAuthRefreshMiddleware.ts b/packages/backend-defaults/src/entrypoints/httpRouter/createCookieAuthRefreshMiddleware.ts similarity index 100% rename from packages/backend-app-api/src/services/implementations/httpRouter/createCookieAuthRefreshMiddleware.ts rename to packages/backend-defaults/src/entrypoints/httpRouter/createCookieAuthRefreshMiddleware.ts diff --git a/packages/backend-app-api/src/services/implementations/httpRouter/createCredentialsBarrier.test.ts b/packages/backend-defaults/src/entrypoints/httpRouter/createCredentialsBarrier.test.ts similarity index 98% rename from packages/backend-app-api/src/services/implementations/httpRouter/createCredentialsBarrier.test.ts rename to packages/backend-defaults/src/entrypoints/httpRouter/createCredentialsBarrier.test.ts index a5246c91b2..124acf277b 100644 --- a/packages/backend-app-api/src/services/implementations/httpRouter/createCredentialsBarrier.test.ts +++ b/packages/backend-defaults/src/entrypoints/httpRouter/createCredentialsBarrier.test.ts @@ -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(), diff --git a/packages/backend-app-api/src/services/implementations/httpRouter/createCredentialsBarrier.ts b/packages/backend-defaults/src/entrypoints/httpRouter/createCredentialsBarrier.ts similarity index 100% rename from packages/backend-app-api/src/services/implementations/httpRouter/createCredentialsBarrier.ts rename to packages/backend-defaults/src/entrypoints/httpRouter/createCredentialsBarrier.ts diff --git a/packages/backend-app-api/src/services/implementations/httpRouter/createLifecycleMiddleware.test.ts b/packages/backend-defaults/src/entrypoints/httpRouter/createLifecycleMiddleware.test.ts similarity index 100% rename from packages/backend-app-api/src/services/implementations/httpRouter/createLifecycleMiddleware.test.ts rename to packages/backend-defaults/src/entrypoints/httpRouter/createLifecycleMiddleware.test.ts diff --git a/packages/backend-defaults/src/entrypoints/httpRouter/createLifecycleMiddleware.ts b/packages/backend-defaults/src/entrypoints/httpRouter/createLifecycleMiddleware.ts new file mode 100644 index 0000000000..3f8ee3ea69 --- /dev/null +++ b/packages/backend-defaults/src/entrypoints/httpRouter/createLifecycleMiddleware.ts @@ -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); + }; +} diff --git a/packages/backend-app-api/src/services/implementations/httpRouter/httpRouterServiceFactory.test.ts b/packages/backend-defaults/src/entrypoints/httpRouter/httpRouterServiceFactory.test.ts similarity index 100% rename from packages/backend-app-api/src/services/implementations/httpRouter/httpRouterServiceFactory.test.ts rename to packages/backend-defaults/src/entrypoints/httpRouter/httpRouterServiceFactory.test.ts diff --git a/packages/backend-defaults/src/entrypoints/httpRouter/httpRouterServiceFactory.ts b/packages/backend-defaults/src/entrypoints/httpRouter/httpRouterServiceFactory.ts new file mode 100644 index 0000000000..4a0dec49e6 --- /dev/null +++ b/packages/backend-defaults/src/entrypoints/httpRouter/httpRouterServiceFactory.ts @@ -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); + }, + }; + }, + }), +); diff --git a/packages/backend-defaults/src/entrypoints/httpRouter/index.ts b/packages/backend-defaults/src/entrypoints/httpRouter/index.ts new file mode 100644 index 0000000000..928a232131 --- /dev/null +++ b/packages/backend-defaults/src/entrypoints/httpRouter/index.ts @@ -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'; diff --git a/packages/backend-defaults/src/entrypoints/logger/index.ts b/packages/backend-defaults/src/entrypoints/logger/index.ts new file mode 100644 index 0000000000..ca52bf5193 --- /dev/null +++ b/packages/backend-defaults/src/entrypoints/logger/index.ts @@ -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'; diff --git a/packages/backend-defaults/src/entrypoints/logger/loggerServiceFactory.ts b/packages/backend-defaults/src/entrypoints/logger/loggerServiceFactory.ts new file mode 100644 index 0000000000..4eff798e11 --- /dev/null +++ b/packages/backend-defaults/src/entrypoints/logger/loggerServiceFactory.ts @@ -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() }); + }, +}); diff --git a/packages/backend-defaults/src/entrypoints/rootConfig/createConfigSecretEnumerator.test.ts b/packages/backend-defaults/src/entrypoints/rootConfig/createConfigSecretEnumerator.test.ts new file mode 100644 index 0000000000..1efb12ad03 --- /dev/null +++ b/packages/backend-defaults/src/entrypoints/rootConfig/createConfigSecretEnumerator.test.ts @@ -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']); + }); +}); diff --git a/packages/backend-defaults/src/entrypoints/rootConfig/createConfigSecretEnumerator.ts b/packages/backend-defaults/src/entrypoints/rootConfig/createConfigSecretEnumerator.ts new file mode 100644 index 0000000000..421b07c396 --- /dev/null +++ b/packages/backend-defaults/src/entrypoints/rootConfig/createConfigSecretEnumerator.ts @@ -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> { + 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(); + 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; + }; +} diff --git a/packages/backend-defaults/src/entrypoints/rootConfig/index.ts b/packages/backend-defaults/src/entrypoints/rootConfig/index.ts index 1775ef2efc..66f5e8e15f 100644 --- a/packages/backend-defaults/src/entrypoints/rootConfig/index.ts +++ b/packages/backend-defaults/src/entrypoints/rootConfig/index.ts @@ -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'; diff --git a/packages/backend-defaults/src/entrypoints/rootHttpRouter/DefaultRootHttpRouter.test.ts b/packages/backend-defaults/src/entrypoints/rootHttpRouter/DefaultRootHttpRouter.test.ts new file mode 100644 index 0000000000..fc1de9b205 --- /dev/null +++ b/packages/backend-defaults/src/entrypoints/rootHttpRouter/DefaultRootHttpRouter.test.ts @@ -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'); + }); +}); diff --git a/packages/backend-defaults/src/entrypoints/rootHttpRouter/DefaultRootHttpRouter.ts b/packages/backend-defaults/src/entrypoints/rootHttpRouter/DefaultRootHttpRouter.ts new file mode 100644 index 0000000000..9d7d1de120 --- /dev/null +++ b/packages/backend-defaults/src/entrypoints/rootHttpRouter/DefaultRootHttpRouter.ts @@ -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(); + + 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; + } +} diff --git a/packages/backend-app-api/src/http/MiddlewareFactory.test.ts b/packages/backend-defaults/src/entrypoints/rootHttpRouter/http/MiddlewareFactory.test.ts similarity index 100% rename from packages/backend-app-api/src/http/MiddlewareFactory.test.ts rename to packages/backend-defaults/src/entrypoints/rootHttpRouter/http/MiddlewareFactory.test.ts diff --git a/packages/backend-app-api/src/http/MiddlewareFactory.ts b/packages/backend-defaults/src/entrypoints/rootHttpRouter/http/MiddlewareFactory.ts similarity index 100% rename from packages/backend-app-api/src/http/MiddlewareFactory.ts rename to packages/backend-defaults/src/entrypoints/rootHttpRouter/http/MiddlewareFactory.ts diff --git a/packages/backend-app-api/src/http/applyInternalErrorFilter.ts b/packages/backend-defaults/src/entrypoints/rootHttpRouter/http/applyInternalErrorFilter.ts similarity index 100% rename from packages/backend-app-api/src/http/applyInternalErrorFilter.ts rename to packages/backend-defaults/src/entrypoints/rootHttpRouter/http/applyInternalErrorFilter.ts diff --git a/packages/backend-app-api/src/http/config.test.ts b/packages/backend-defaults/src/entrypoints/rootHttpRouter/http/config.test.ts similarity index 100% rename from packages/backend-app-api/src/http/config.test.ts rename to packages/backend-defaults/src/entrypoints/rootHttpRouter/http/config.test.ts diff --git a/packages/backend-app-api/src/http/config.ts b/packages/backend-defaults/src/entrypoints/rootHttpRouter/http/config.ts similarity index 100% rename from packages/backend-app-api/src/http/config.ts rename to packages/backend-defaults/src/entrypoints/rootHttpRouter/http/config.ts diff --git a/packages/backend-app-api/src/http/createHttpServer.ts b/packages/backend-defaults/src/entrypoints/rootHttpRouter/http/createHttpServer.ts similarity index 100% rename from packages/backend-app-api/src/http/createHttpServer.ts rename to packages/backend-defaults/src/entrypoints/rootHttpRouter/http/createHttpServer.ts diff --git a/packages/backend-app-api/src/http/getGeneratedCertificate.ts b/packages/backend-defaults/src/entrypoints/rootHttpRouter/http/getGeneratedCertificate.ts similarity index 100% rename from packages/backend-app-api/src/http/getGeneratedCertificate.ts rename to packages/backend-defaults/src/entrypoints/rootHttpRouter/http/getGeneratedCertificate.ts diff --git a/packages/backend-defaults/src/entrypoints/rootHttpRouter/http/index.ts b/packages/backend-defaults/src/entrypoints/rootHttpRouter/http/index.ts new file mode 100644 index 0000000000..4a9ec14cf8 --- /dev/null +++ b/packages/backend-defaults/src/entrypoints/rootHttpRouter/http/index.ts @@ -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'; diff --git a/packages/backend-app-api/src/http/readCorsOptions.test.ts b/packages/backend-defaults/src/entrypoints/rootHttpRouter/http/readCorsOptions.test.ts similarity index 100% rename from packages/backend-app-api/src/http/readCorsOptions.test.ts rename to packages/backend-defaults/src/entrypoints/rootHttpRouter/http/readCorsOptions.test.ts diff --git a/packages/backend-app-api/src/http/readCorsOptions.ts b/packages/backend-defaults/src/entrypoints/rootHttpRouter/http/readCorsOptions.ts similarity index 100% rename from packages/backend-app-api/src/http/readCorsOptions.ts rename to packages/backend-defaults/src/entrypoints/rootHttpRouter/http/readCorsOptions.ts diff --git a/packages/backend-app-api/src/http/readHelmetOptions.test.ts b/packages/backend-defaults/src/entrypoints/rootHttpRouter/http/readHelmetOptions.test.ts similarity index 100% rename from packages/backend-app-api/src/http/readHelmetOptions.test.ts rename to packages/backend-defaults/src/entrypoints/rootHttpRouter/http/readHelmetOptions.test.ts diff --git a/packages/backend-app-api/src/http/readHelmetOptions.ts b/packages/backend-defaults/src/entrypoints/rootHttpRouter/http/readHelmetOptions.ts similarity index 100% rename from packages/backend-app-api/src/http/readHelmetOptions.ts rename to packages/backend-defaults/src/entrypoints/rootHttpRouter/http/readHelmetOptions.ts diff --git a/packages/backend-app-api/src/http/types.ts b/packages/backend-defaults/src/entrypoints/rootHttpRouter/http/types.ts similarity index 100% rename from packages/backend-app-api/src/http/types.ts rename to packages/backend-defaults/src/entrypoints/rootHttpRouter/http/types.ts diff --git a/packages/backend-defaults/src/entrypoints/rootHttpRouter/index.ts b/packages/backend-defaults/src/entrypoints/rootHttpRouter/index.ts new file mode 100644 index 0000000000..9c2d96dab2 --- /dev/null +++ b/packages/backend-defaults/src/entrypoints/rootHttpRouter/index.ts @@ -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'; diff --git a/packages/backend-defaults/src/entrypoints/rootHttpRouter/rootHttpRouterServiceFactory.ts b/packages/backend-defaults/src/entrypoints/rootHttpRouter/rootHttpRouterServiceFactory.ts new file mode 100644 index 0000000000..ea3dc42eb7 --- /dev/null +++ b/packages/backend-defaults/src/entrypoints/rootHttpRouter/rootHttpRouterServiceFactory.ts @@ -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; + }, + }), +); diff --git a/packages/backend-app-api/src/logging/WinstonLogger.test.ts b/packages/backend-defaults/src/entrypoints/rootLogger/WinstonLogger.test.ts similarity index 100% rename from packages/backend-app-api/src/logging/WinstonLogger.test.ts rename to packages/backend-defaults/src/entrypoints/rootLogger/WinstonLogger.test.ts diff --git a/packages/backend-defaults/src/entrypoints/rootLogger/WinstonLogger.ts b/packages/backend-defaults/src/entrypoints/rootLogger/WinstonLogger.ts new file mode 100644 index 0000000000..38d05f6511 --- /dev/null +++ b/packages/backend-defaults/src/entrypoints/rootLogger/WinstonLogger.ts @@ -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) => 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) => void; + } { + const redactionSet = new Set(); + + 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) => 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) { + this.#addRedactions?.(redactions); + } +} diff --git a/packages/backend-defaults/src/entrypoints/rootLogger/index.ts b/packages/backend-defaults/src/entrypoints/rootLogger/index.ts new file mode 100644 index 0000000000..96ed402520 --- /dev/null +++ b/packages/backend-defaults/src/entrypoints/rootLogger/index.ts @@ -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'; diff --git a/packages/backend-defaults/src/entrypoints/rootLogger/rootLoggerServiceFactory.ts b/packages/backend-defaults/src/entrypoints/rootLogger/rootLoggerServiceFactory.ts new file mode 100644 index 0000000000..8454cf07d3 --- /dev/null +++ b/packages/backend-defaults/src/entrypoints/rootLogger/rootLoggerServiceFactory.ts @@ -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; + }, +}); diff --git a/packages/backend-app-api/src/lib/escapeRegExp.test.ts b/packages/backend-defaults/src/lib/escapeRegExp.test.ts similarity index 100% rename from packages/backend-app-api/src/lib/escapeRegExp.test.ts rename to packages/backend-defaults/src/lib/escapeRegExp.test.ts diff --git a/packages/backend-app-api/src/lib/escapeRegExp.ts b/packages/backend-defaults/src/lib/escapeRegExp.ts similarity index 100% rename from packages/backend-app-api/src/lib/escapeRegExp.ts rename to packages/backend-defaults/src/lib/escapeRegExp.ts diff --git a/packages/backend-defaults/src/lib/urls.test.ts b/packages/backend-defaults/src/lib/urls.test.ts new file mode 100644 index 0000000000..c2a67fb849 --- /dev/null +++ b/packages/backend-defaults/src/lib/urls.test.ts @@ -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); + }); +}); diff --git a/packages/backend-defaults/src/lib/urls.ts b/packages/backend-defaults/src/lib/urls.ts new file mode 100644 index 0000000000..848cea25d9 --- /dev/null +++ b/packages/backend-defaults/src/lib/urls.ts @@ -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; + } +} diff --git a/packages/backend-test-utils/api-report.md b/packages/backend-test-utils/api-report.md index cc30dc8f56..019d398ea6 100644 --- a/packages/backend-test-utils/api-report.md +++ b/packages/backend-test-utils/api-report.md @@ -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'; diff --git a/packages/backend-test-utils/package.json b/packages/backend-test-utils/package.json index 40523b1e04..73390d75d6 100644 --- a/packages/backend-test-utils/package.json +++ b/packages/backend-test-utils/package.json @@ -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:^", diff --git a/packages/backend-test-utils/src/next/services/mockServices.ts b/packages/backend-test-utils/src/next/services/mockServices.ts index 57621a9209..24571b634c 100644 --- a/packages/backend-test-utils/src/next/services/mockServices.ts +++ b/packages/backend-test-utils/src/next/services/mockServices.ts @@ -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() { diff --git a/yarn.lock b/yarn.lock index 08c4010281..00817bfb22 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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:^"