From b44eb68bcb009fe0c193570135cebc67616b572d Mon Sep 17 00:00:00 2001 From: Brian Fletcher Date: Tue, 20 Dec 2022 16:05:24 +0000 Subject: [PATCH 01/55] add examples of scaffolder action inputs to docs Signed-off-by: Brian Fletcher --- .changeset/.changeset/rude-gifts-pay.md | 5 ++ .changeset/metal-nails-punch.md | 5 ++ plugins/scaffolder-backend/api-report.md | 4 ++ .../actions/builtin/catalog/register.ts | 25 +++++++++- .../actions/builtin/catalog/write.ts | 35 +++++++++++++- .../scaffolder/actions/builtin/debug/log.ts | 24 +++++++++- .../src/scaffolder/actions/types.ts | 1 + .../scaffolder-backend/src/service/router.ts | 1 + plugins/scaffolder/api-report.md | 26 +++++++---- .../components/ActionsPage/ActionsPage.tsx | 46 +++++++++++++++++++ plugins/scaffolder/src/index.ts | 2 + plugins/scaffolder/src/types.ts | 24 ++++++++-- 12 files changed, 184 insertions(+), 14 deletions(-) create mode 100644 .changeset/.changeset/rude-gifts-pay.md create mode 100644 .changeset/metal-nails-punch.md diff --git a/.changeset/.changeset/rude-gifts-pay.md b/.changeset/.changeset/rude-gifts-pay.md new file mode 100644 index 0000000000..4440f2ffc1 --- /dev/null +++ b/.changeset/.changeset/rude-gifts-pay.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder': patch +--- + +Show action example yaml on the scaffolder actions documentation page. diff --git a/.changeset/metal-nails-punch.md b/.changeset/metal-nails-punch.md new file mode 100644 index 0000000000..07265d1343 --- /dev/null +++ b/.changeset/metal-nails-punch.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend': patch +--- + +Changes to provide examples alongside scaffolder task actions. diff --git a/plugins/scaffolder-backend/api-report.md b/plugins/scaffolder-backend/api-report.md index 69ef893dc3..73b6177c6a 100644 --- a/plugins/scaffolder-backend/api-report.md +++ b/plugins/scaffolder-backend/api-report.md @@ -843,6 +843,10 @@ export class TaskWorker { export type TemplateAction = { id: string; description?: string; + examples?: { + description: string; + example: string; + }[]; supportsDryRun?: boolean; schema?: { input?: Schema; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/register.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/register.ts index 4b9f883706..e31f3355a6 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/register.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/register.ts @@ -19,6 +19,28 @@ import { ScmIntegrations } from '@backstage/integration'; import { CatalogApi } from '@backstage/catalog-client'; import { stringifyEntityRef } from '@backstage/catalog-model'; import { createTemplateAction } from '../../createTemplateAction'; +import yaml from 'yaml'; + +const id = 'catalog:register'; + +const examples = [ + { + description: 'Register With the catalog', + example: yaml.stringify({ + steps: [ + { + action: id, + id: 'register-with-catalog', + name: 'Register with the catalog', + input: { + catalogInfoUrl: + 'http://github.com/backstage/backstage/blob/master/catalog-info.yaml', + }, + }, + ], + }), + }, +]; /** * Registers entities from a catalog descriptor file in the workspace into the software catalog. @@ -34,9 +56,10 @@ export function createCatalogRegisterAction(options: { | { catalogInfoUrl: string; optional?: boolean } | { repoContentsUrl: string; catalogInfoPath?: string; optional?: boolean } >({ - id: 'catalog:register', + id, description: 'Registers entities from a catalog descriptor file in the workspace into the software catalog.', + examples, schema: { input: { oneOf: [ diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/write.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/write.ts index da758898dc..5257839f2d 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/write.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/write.ts @@ -20,14 +20,47 @@ import * as yaml from 'yaml'; import { Entity } from '@backstage/catalog-model'; import { resolveSafeChildPath } from '@backstage/backend-common'; +const id = 'catalog:write'; + +const examples = [ + { + description: 'Write a catalog yaml file', + example: yaml.stringify({ + steps: [ + { + action: id, + id: 'create-catalog-info-file', + name: 'Create catalog file', + input: { + entity: { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'test', + annotations: {}, + }, + spec: { + type: 'service', + lifecycle: 'production', + owner: 'default/owner', + }, + }, + }, + }, + ], + }), + }, +]; + /** * Writes a catalog descriptor file containing the provided entity to a path in the workspace. * @public */ export function createCatalogWriteAction() { return createTemplateAction<{ filePath?: string; entity: Entity }>({ - id: 'catalog:write', + id, description: 'Writes the catalog-info.yaml for your template', + examples, schema: { input: { type: 'object', diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/log.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/log.ts index 99d030b772..79e555eb05 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/log.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/log.ts @@ -17,6 +17,27 @@ import { readdir, stat } from 'fs-extra'; import { relative, join } from 'path'; import { createTemplateAction } from '../../createTemplateAction'; +import yaml from 'yaml'; + +const id = 'debug:log'; + +const examples = [ + { + description: 'Write a debug message', + example: yaml.stringify({ + steps: [ + { + action: id, + id: 'write-debug-line', + name: 'Write log line', + input: { + message: 'Hello, there', + }, + }, + ], + }), + }, +]; /** * Writes a message into the log or lists all files in the workspace @@ -30,9 +51,10 @@ import { createTemplateAction } from '../../createTemplateAction'; */ export function createDebugLogAction() { return createTemplateAction<{ message?: string; listWorkspace?: boolean }>({ - id: 'debug:log', + id, description: 'Writes a message into the log or lists all files in the workspace.', + examples, schema: { input: { type: 'object', diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/types.ts b/plugins/scaffolder-backend/src/scaffolder/actions/types.ts index 666f7a0d50..0912cc038b 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/types.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/types.ts @@ -66,6 +66,7 @@ export type ActionContext = { export type TemplateAction = { id: string; description?: string; + examples?: { description: string; example: string }[]; supportsDryRun?: boolean; schema?: { input?: Schema; diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts index e741961ddc..9ff8d43625 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -292,6 +292,7 @@ export async function createRouter( return { id: action.id, description: action.description, + examples: action.examples, schema: action.schema, }; }); diff --git a/plugins/scaffolder/api-report.md b/plugins/scaffolder/api-report.md index 3fe1208ba9..6a029af6bf 100644 --- a/plugins/scaffolder/api-report.md +++ b/plugins/scaffolder/api-report.md @@ -38,6 +38,23 @@ import { UIOptionsType } from '@rjsf/utils'; import { UiSchema } from '@rjsf/utils'; import { z } from 'zod'; +// @public +export type Action = { + id: string; + description?: string; + schema?: { + input?: JSONSchema7; + output?: JSONSchema7; + }; + examples?: ActionExample[]; +}; + +// @public +export type ActionExample = { + description: string; + example: string; +}; + // @alpha export function createNextScaffolderFieldExtension< TReturnValue = unknown, @@ -188,14 +205,7 @@ export interface LayoutOptions

{ export type LayoutTemplate = FormProps_2['ObjectFieldTemplate']; // @public -export type ListActionsResponse = Array<{ - id: string; - description?: string; - schema?: { - input?: JSONSchema7; - output?: JSONSchema7; - }; -}>; +export type ListActionsResponse = Array; // @public export type LogEvent = { diff --git a/plugins/scaffolder/src/components/ActionsPage/ActionsPage.tsx b/plugins/scaffolder/src/components/ActionsPage/ActionsPage.tsx index 29951d7cb1..7a16337d09 100644 --- a/plugins/scaffolder/src/components/ActionsPage/ActionsPage.tsx +++ b/plugins/scaffolder/src/components/ActionsPage/ActionsPage.tsx @@ -27,6 +27,7 @@ import { TableContainer, TableHead, TableRow, + Grid, makeStyles, } from '@material-ui/core'; import { JSONSchema7, JSONSchema7Definition } from 'json-schema'; @@ -39,7 +40,11 @@ import { Header, Page, ErrorPage, + Table as BackstageTable, + TableColumn, + CodeSnippet, } from '@backstage/core-components'; +import { ActionExample } from '../../types'; const useStyles = makeStyles(theme => ({ code: { @@ -67,6 +72,29 @@ const useStyles = makeStyles(theme => ({ }, })); +const examplesColumns: TableColumn[] = [ + { + title: 'Description', + field: 'description', + width: '20%', + }, + { + title: 'Example', + render: ({ example }) => { + return ( + + + + ); + }, + }, +]; + export const ActionsPage = () => { const api = useApi(scaffolderApiRef); const classes = useStyles(); @@ -180,6 +208,24 @@ export const ActionsPage = () => { {renderTable(action.schema.output)} )} + {action.examples && ( + + Examples + + + )} ); }); diff --git a/plugins/scaffolder/src/index.ts b/plugins/scaffolder/src/index.ts index 1ed1346176..ae994191ce 100644 --- a/plugins/scaffolder/src/index.ts +++ b/plugins/scaffolder/src/index.ts @@ -22,6 +22,8 @@ export { scaffolderApiRef, ScaffolderClient } from './api'; export type { + Action, + ActionExample, ListActionsResponse, LogEvent, ScaffolderApi, diff --git a/plugins/scaffolder/src/types.ts b/plugins/scaffolder/src/types.ts index acb0f0e114..422661b896 100644 --- a/plugins/scaffolder/src/types.ts +++ b/plugins/scaffolder/src/types.ts @@ -43,18 +43,36 @@ export type ScaffolderTask = { }; /** - * The response shape for the `listActions` call to the `scaffolder-backend` + * A single action example * * @public */ -export type ListActionsResponse = Array<{ +export type ActionExample = { + description: string; + example: string; +}; + +/** + * The response shape for a single action in the `listActions` call to the `scaffolder-backend` + * + * @public + */ +export type Action = { id: string; description?: string; schema?: { input?: JSONSchema7; output?: JSONSchema7; }; -}>; + examples?: ActionExample[]; +}; + +/** + * The response shape for the `listActions` call to the `scaffolder-backend` + * + * @public + */ +export type ListActionsResponse = Array; /** @public */ export type ScaffolderOutputLink = { From 81c94a919f960a5939e6b5bb5f9356fc5df4e90e Mon Sep 17 00:00:00 2001 From: Brian Fletcher Date: Tue, 20 Dec 2022 16:30:17 +0000 Subject: [PATCH 02/55] add a test for the example Signed-off-by: Brian Fletcher --- .../actions/builtin/debug/log.test.ts | 40 +++++++++++++++++++ .../scaffolder/actions/builtin/debug/log.ts | 17 +++++++- 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/log.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/log.test.ts index c53caff018..6b7ad8816f 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/log.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/log.test.ts @@ -20,6 +20,7 @@ import os from 'os'; import { Writable } from 'stream'; import { createDebugLogAction } from './log'; import { join } from 'path'; +import yaml from 'yaml'; describe('debug:log', () => { const logStream = { @@ -91,4 +92,43 @@ describe('debug:log', () => { expect.stringContaining('Hello Backstage!'), ); }); + + it('should log the workspace content from an example, if active', async () => { + const example = action.examples?.find( + sample => sample.description === 'List the workspace directory', + )?.example as string; + expect(typeof example).toEqual('string'); + const context = { + ...mockContext, + ...yaml.parse(example).steps[0], + }; + + await action.handler(context); + + expect(logStream.write).toHaveBeenCalledTimes(1); + expect(logStream.write).toHaveBeenCalledWith( + expect.stringContaining('README.md'), + ); + expect(logStream.write).toHaveBeenCalledWith( + expect.stringContaining(join('a-directory', 'index.md')), + ); + }); + + it('should log message from an example', async () => { + const example = action.examples?.find( + sample => sample.description === 'Write a debug message', + )?.example as string; + + const context = { + ...mockContext, + ...yaml.parse(example).steps[0], + }; + + await action.handler(context); + + expect(logStream.write).toHaveBeenCalledTimes(1); + expect(logStream.write).toHaveBeenCalledWith( + expect.stringContaining('Hello Backstage!'), + ); + }); }); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/log.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/log.ts index 79e555eb05..fe16e4759c 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/log.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/log.ts @@ -31,7 +31,22 @@ const examples = [ id: 'write-debug-line', name: 'Write log line', input: { - message: 'Hello, there', + message: 'Hello Backstage!', + }, + }, + ], + }), + }, + { + description: 'List the workspace directory', + example: yaml.stringify({ + steps: [ + { + action: id, + id: 'write-debug-line', + name: 'Write log line', + input: { + listWorkspace: true, }, }, ], From afcf23243620829136766c7e9ea10304c7aafc71 Mon Sep 17 00:00:00 2001 From: Brian Fletcher Date: Tue, 20 Dec 2022 16:43:19 +0000 Subject: [PATCH 03/55] fix casing Signed-off-by: Brian Fletcher --- .../src/scaffolder/actions/builtin/catalog/register.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/register.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/register.ts index e31f3355a6..48fec3db03 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/register.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/register.ts @@ -25,7 +25,7 @@ const id = 'catalog:register'; const examples = [ { - description: 'Register With the catalog', + description: 'Register with the catalog', example: yaml.stringify({ steps: [ { From 6977ed06d79a401f5741a567969864b1952154af Mon Sep 17 00:00:00 2001 From: Brian Fletcher Date: Thu, 22 Dec 2022 09:02:33 +0000 Subject: [PATCH 04/55] expand changeset and use accordian for examples Signed-off-by: Brian Fletcher --- .changeset/metal-nails-punch.md | 66 ++++++++++++++++++- .../components/ActionsPage/ActionsPage.tsx | 42 +++++++----- 2 files changed, 91 insertions(+), 17 deletions(-) diff --git a/.changeset/metal-nails-punch.md b/.changeset/metal-nails-punch.md index 07265d1343..a34c340c0a 100644 --- a/.changeset/metal-nails-punch.md +++ b/.changeset/metal-nails-punch.md @@ -2,4 +2,68 @@ '@backstage/plugin-scaffolder-backend': patch --- -Changes to provide examples alongside scaffolder task actions. +This patch adds changes to provide examples alongside scaffolder task actions. + +The `createTemplateAction` function now takes a list of examples e.g. + +```typescript +const actionExamples = [ + { + description: 'Example 1', + example: yaml.stringify({ + steps: [ + { + action: 'test:action', + id: 'test', + input: { + input1: 'value', + }, + }, + ], + }), + }, +]; + +export function createTestAction() { + return createTemplateAction({ + id: 'test:action', + examples: [ + { + description: 'Example 1', + examples: actionExamples + } + ], + ..., + }); +``` + +These examples can be retrieved later from the api. + +```bash +curl http://localhost:7007/api/scaffolder/v2/actions +``` + +```json +[ + { + "id": "test:action", + "examples": [ + { + "description": "Example 1", + "example": "steps:\n - action: test:action\n id: test\n input:\n input1: value\n" + } + ], + "schema": { + "input": { + "type": "object", + "properties": { + "input1": { + "title": "Input 1", + "type": "string" + } + } + } + } + } +] +``` diff --git a/plugins/scaffolder/src/components/ActionsPage/ActionsPage.tsx b/plugins/scaffolder/src/components/ActionsPage/ActionsPage.tsx index 7a16337d09..234ba1951f 100644 --- a/plugins/scaffolder/src/components/ActionsPage/ActionsPage.tsx +++ b/plugins/scaffolder/src/components/ActionsPage/ActionsPage.tsx @@ -29,9 +29,13 @@ import { TableRow, Grid, makeStyles, + Accordion, + AccordionSummary, + AccordionDetails, } from '@material-ui/core'; import { JSONSchema7, JSONSchema7Definition } from 'json-schema'; import classNames from 'classnames'; +import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; import { useApi } from '@backstage/core-plugin-api'; import { @@ -209,22 +213,28 @@ export const ActionsPage = () => { )} {action.examples && ( - - Examples - - + + }> + Examples + + + + + + + )} ); From 489935d625b590b350a94d7c7695cac7725db765 Mon Sep 17 00:00:00 2001 From: Brian Fletcher Date: Tue, 3 Jan 2023 21:12:32 +0000 Subject: [PATCH 05/55] addressing code review comments Signed-off-by: Brian Fletcher --- .changeset/{.changeset => }/rude-gifts-pay.md | 0 .../scaffolder/actions/builtin/debug/log.ts | 6 +- .../components/ActionsPage/ActionsPage.tsx | 64 ++++++++----------- 3 files changed, 31 insertions(+), 39 deletions(-) rename .changeset/{.changeset => }/rude-gifts-pay.md (100%) diff --git a/.changeset/.changeset/rude-gifts-pay.md b/.changeset/rude-gifts-pay.md similarity index 100% rename from .changeset/.changeset/rude-gifts-pay.md rename to .changeset/rude-gifts-pay.md diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/log.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/log.ts index fe16e4759c..3863623563 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/log.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/log.ts @@ -29,7 +29,7 @@ const examples = [ { action: id, id: 'write-debug-line', - name: 'Write log line', + name: 'Write "Hello Backstage!" log line', input: { message: 'Hello Backstage!', }, @@ -43,8 +43,8 @@ const examples = [ steps: [ { action: id, - id: 'write-debug-line', - name: 'Write log line', + id: 'write-workspace-directory', + name: 'List the workspace directory', input: { listWorkspace: true, }, diff --git a/plugins/scaffolder/src/components/ActionsPage/ActionsPage.tsx b/plugins/scaffolder/src/components/ActionsPage/ActionsPage.tsx index 234ba1951f..c079ee29b5 100644 --- a/plugins/scaffolder/src/components/ActionsPage/ActionsPage.tsx +++ b/plugins/scaffolder/src/components/ActionsPage/ActionsPage.tsx @@ -44,7 +44,6 @@ import { Header, Page, ErrorPage, - Table as BackstageTable, TableColumn, CodeSnippet, } from '@backstage/core-components'; @@ -76,28 +75,33 @@ const useStyles = makeStyles(theme => ({ }, })); -const examplesColumns: TableColumn[] = [ - { - title: 'Description', - field: 'description', - width: '20%', - }, - { - title: 'Example', - render: ({ example }) => { - return ( - - - - ); - }, - }, -]; +const ExamplesTable = (props: { examples: ActionExample[] }) => { + return ( + + {props.examples.map(example => { + return ( + + + + {example.description} + + + + + + + + + ); + })} + + ); +}; export const ActionsPage = () => { const api = useApi(scaffolderApiRef); @@ -219,19 +223,7 @@ export const ActionsPage = () => { - + From 75d36681830483acecfea191d075ee8f4941bd77 Mon Sep 17 00:00:00 2001 From: Brian Fletcher Date: Tue, 3 Jan 2023 21:42:57 +0000 Subject: [PATCH 06/55] fixes import Signed-off-by: Brian Fletcher --- plugins/scaffolder/src/components/ActionsPage/ActionsPage.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/scaffolder/src/components/ActionsPage/ActionsPage.tsx b/plugins/scaffolder/src/components/ActionsPage/ActionsPage.tsx index c079ee29b5..7d0137c9bc 100644 --- a/plugins/scaffolder/src/components/ActionsPage/ActionsPage.tsx +++ b/plugins/scaffolder/src/components/ActionsPage/ActionsPage.tsx @@ -44,7 +44,6 @@ import { Header, Page, ErrorPage, - TableColumn, CodeSnippet, } from '@backstage/core-components'; import { ActionExample } from '../../types'; From b4db2862a41197e07183e87c04d7a92ebc832239 Mon Sep 17 00:00:00 2001 From: Morgan Bentell Date: Wed, 4 Jan 2023 09:51:30 +0100 Subject: [PATCH 07/55] Add cli option --preview-app-bundle-path Signed-off-by: Morgan Bentell --- packages/techdocs-cli/serve.sh | 19 +++++++++++++++++++ packages/techdocs-cli/src/commands/index.ts | 4 ++++ .../techdocs-cli/src/commands/serve/serve.ts | 8 +++++++- 3 files changed, 30 insertions(+), 1 deletion(-) create mode 100755 packages/techdocs-cli/serve.sh diff --git a/packages/techdocs-cli/serve.sh b/packages/techdocs-cli/serve.sh new file mode 100755 index 0000000000..743b49cd8f --- /dev/null +++ b/packages/techdocs-cli/serve.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +set -e + +## build app +cd ~/kod/backstage-frontend/packages/techdocs-cli-embedded-app-4-spotify/ +yarn build + +## run cli +REPO_ROOT=~/kod/backstage + +cd $REPO_ROOT/packages/techdocs-cli/src/ +rm -rf dist +cp -r ~/kod/backstage-frontend/packages/techdocs-cli-embedded-app-4-spotify/dist . + +cd $REPO_ROOT +yarn workspace @techdocs/cli build +cd ~/kod/docs/ +~/kod/backstage/packages/techdocs-cli/bin/techdocs-cli serve --preview-app-bundle-path ~/kod/backstage/packages/techdocs-cli/src/dist diff --git a/packages/techdocs-cli/src/commands/index.ts b/packages/techdocs-cli/src/commands/index.ts index 03772d09d7..a164a5e28f 100644 --- a/packages/techdocs-cli/src/commands/index.ts +++ b/packages/techdocs-cli/src/commands/index.ts @@ -251,6 +251,10 @@ export function registerCommands(program: Command) { ) .option('--mkdocs-port ', 'Port for MkDocs server to use', '8000') .option('-v --verbose', 'Enable verbose output.', false) + .option( + '--preview-app-bundle-path ', + 'Preview documentation using another web app', + ) .action(lazy(() => import('./serve/serve').then(m => m.default))); } diff --git a/packages/techdocs-cli/src/commands/serve/serve.ts b/packages/techdocs-cli/src/commands/serve/serve.ts index 68d0bbb277..60207b8daf 100644 --- a/packages/techdocs-cli/src/commands/serve/serve.ts +++ b/packages/techdocs-cli/src/commands/serve/serve.ts @@ -42,9 +42,15 @@ function findPreviewBundlePath(): string { } } +function getPreviewAppPath(opts: OptionValues): string { + return opts.previewAppBundlePath ?? findPreviewBundlePath(); +} + export default async function serve(opts: OptionValues) { const logger = createLogger({ verbose: opts.verbose }); + console.log(opts.previewAppBundlePath); + // Determine if we want to run in local dev mode or not // This will run the backstage http server on a different port and only used // for proxying mkdocs to the backstage app running locally (e.g. with webpack-dev-server) @@ -117,7 +123,7 @@ export default async function serve(opts: OptionValues) { const port = isDevMode ? backstageBackendPort : backstagePort; const httpServer = new HTTPServer( - findPreviewBundlePath(), + getPreviewAppPath(opts), port, opts.mkdocsPort, opts.verbose, From bc18c902a27cc3f17eaa8096993ca943521aaad0 Mon Sep 17 00:00:00 2001 From: Morgan Bentell Date: Wed, 4 Jan 2023 15:16:39 +0100 Subject: [PATCH 08/55] clean up and update documentation, changeset and cli-report Signed-off-by: Morgan Bentell --- .changeset/dry-camels-scream.md | 5 ++++ docs/features/techdocs/cli.md | 24 ++++++++++++------- packages/techdocs-cli/cli-report.md | 1 + packages/techdocs-cli/serve.sh | 19 --------------- packages/techdocs-cli/src/commands/index.ts | 2 +- .../techdocs-cli/src/commands/serve/serve.ts | 2 -- 6 files changed, 23 insertions(+), 30 deletions(-) create mode 100644 .changeset/dry-camels-scream.md delete mode 100755 packages/techdocs-cli/serve.sh diff --git a/.changeset/dry-camels-scream.md b/.changeset/dry-camels-scream.md new file mode 100644 index 0000000000..ea3a7f47b8 --- /dev/null +++ b/.changeset/dry-camels-scream.md @@ -0,0 +1,5 @@ +--- +'@techdocs/cli': minor +--- + +Add `--preview-app-bundle-path` option to the `serve` command enabling previewing with apps other than the provided one diff --git a/docs/features/techdocs/cli.md b/docs/features/techdocs/cli.md index 06eb83dfca..9d8d2b24d9 100644 --- a/docs/features/techdocs/cli.md +++ b/docs/features/techdocs/cli.md @@ -1,7 +1,9 @@ --- id: cli title: TechDocs CLI + # prettier-ignore + description: TechDocs CLI - a utility command line interface for managing TechDocs sites in Backstage. --- @@ -70,6 +72,11 @@ a Backstage app server on port 3000. The Backstage app has a custom TechDocs API implementation, which uses the MkDocs preview server as a proxy to fetch the generated documentation files and assets. +Backstage instances might differ from the provided preview app in appearance and +behavior. To preview documentation with a different app, use +`--preview-app-bundle-path` with a path to the bundle of the app to use instead. +Typically, a `dist` or `build` directory. + NOTE: When using a custom `techdocs` docker image, make sure the entry point is also `ENTRYPOINT ["mkdocs"]` or override with `--docker-entrypoint`. @@ -81,14 +88,15 @@ Usage: techdocs-cli serve [options] Serve a documentation project locally in a Backstage app-like environment Options: - -i, --docker-image The mkdocs docker container to use (default: "spotify/techdocs") - --docker-entrypoint Override the image entrypoint - --docker-option Extra options to pass to the docker run command, e.g. "--add-host=internal.host:192.168.11.12" - (can be added multiple times). - --no-docker Do not use Docker, use MkDocs executable in current user environment. - --mkdocs-port Port for MkDocs server to use (default: "8000") - -v --verbose Enable verbose output. (default: false) - -h, --help display help for command + -i, --docker-image The mkdocs docker container to use (default: "spotify/techdocs") + --docker-entrypoint Override the image entrypoint + --docker-option Extra options to pass to the docker run command, e.g. "--add-host=internal.host:192.168.11.12" + (can be added multiple times). + --no-docker Do not use Docker, use MkDocs executable in current user environment. + --mkdocs-port Port for MkDocs server to use (default: "8000") + --preview-app-bundle-path Preview documentation using a web app other than the included one. + -v --verbose Enable verbose output. (default: false) + -h, --help display help for command ``` ### Generate TechDocs site from a documentation project diff --git a/packages/techdocs-cli/cli-report.md b/packages/techdocs-cli/cli-report.md index cf5b2caf0f..ad41041dfb 100644 --- a/packages/techdocs-cli/cli-report.md +++ b/packages/techdocs-cli/cli-report.md @@ -99,6 +99,7 @@ Options: --docker-option --no-docker --mkdocs-port + --preview-app-bundle-path -v --verbose -h, --help ``` diff --git a/packages/techdocs-cli/serve.sh b/packages/techdocs-cli/serve.sh deleted file mode 100755 index 743b49cd8f..0000000000 --- a/packages/techdocs-cli/serve.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/bin/bash - -set -e - -## build app -cd ~/kod/backstage-frontend/packages/techdocs-cli-embedded-app-4-spotify/ -yarn build - -## run cli -REPO_ROOT=~/kod/backstage - -cd $REPO_ROOT/packages/techdocs-cli/src/ -rm -rf dist -cp -r ~/kod/backstage-frontend/packages/techdocs-cli-embedded-app-4-spotify/dist . - -cd $REPO_ROOT -yarn workspace @techdocs/cli build -cd ~/kod/docs/ -~/kod/backstage/packages/techdocs-cli/bin/techdocs-cli serve --preview-app-bundle-path ~/kod/backstage/packages/techdocs-cli/src/dist diff --git a/packages/techdocs-cli/src/commands/index.ts b/packages/techdocs-cli/src/commands/index.ts index a164a5e28f..e6c82ba7f8 100644 --- a/packages/techdocs-cli/src/commands/index.ts +++ b/packages/techdocs-cli/src/commands/index.ts @@ -252,7 +252,7 @@ export function registerCommands(program: Command) { .option('--mkdocs-port ', 'Port for MkDocs server to use', '8000') .option('-v --verbose', 'Enable verbose output.', false) .option( - '--preview-app-bundle-path ', + '--preview-app-bundle-path ', 'Preview documentation using another web app', ) .action(lazy(() => import('./serve/serve').then(m => m.default))); diff --git a/packages/techdocs-cli/src/commands/serve/serve.ts b/packages/techdocs-cli/src/commands/serve/serve.ts index 60207b8daf..ebb984782c 100644 --- a/packages/techdocs-cli/src/commands/serve/serve.ts +++ b/packages/techdocs-cli/src/commands/serve/serve.ts @@ -49,8 +49,6 @@ function getPreviewAppPath(opts: OptionValues): string { export default async function serve(opts: OptionValues) { const logger = createLogger({ verbose: opts.verbose }); - console.log(opts.previewAppBundlePath); - // Determine if we want to run in local dev mode or not // This will run the backstage http server on a different port and only used // for proxying mkdocs to the backstage app running locally (e.g. with webpack-dev-server) From a00234fef7271555466907f2f7b947ba1668715d Mon Sep 17 00:00:00 2001 From: Brian Fletcher Date: Thu, 5 Jan 2023 14:12:17 +0000 Subject: [PATCH 09/55] switch language to yaml in code snippet Signed-off-by: Brian Fletcher --- plugins/scaffolder/src/components/ActionsPage/ActionsPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/scaffolder/src/components/ActionsPage/ActionsPage.tsx b/plugins/scaffolder/src/components/ActionsPage/ActionsPage.tsx index 80ac519ca6..035f44db8c 100644 --- a/plugins/scaffolder/src/components/ActionsPage/ActionsPage.tsx +++ b/plugins/scaffolder/src/components/ActionsPage/ActionsPage.tsx @@ -92,7 +92,7 @@ const ExamplesTable = (props: { examples: ActionExample[] }) => { text={example.example} showLineNumbers showCopyCodeButton - language="JavaScript" + language="yaml" /> From af22bbb64743ca84718fc25b828a36bddb0ee01e Mon Sep 17 00:00:00 2001 From: Brian Fletcher Date: Thu, 5 Jan 2023 14:20:14 +0000 Subject: [PATCH 10/55] remove container within container Signed-off-by: Brian Fletcher --- plugins/scaffolder/src/components/ActionsPage/ActionsPage.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/scaffolder/src/components/ActionsPage/ActionsPage.tsx b/plugins/scaffolder/src/components/ActionsPage/ActionsPage.tsx index 035f44db8c..d727923d68 100644 --- a/plugins/scaffolder/src/components/ActionsPage/ActionsPage.tsx +++ b/plugins/scaffolder/src/components/ActionsPage/ActionsPage.tsx @@ -80,7 +80,7 @@ const ExamplesTable = (props: { examples: ActionExample[] }) => { {props.examples.map(example => { return ( - + <> {example.description} @@ -96,7 +96,7 @@ const ExamplesTable = (props: { examples: ActionExample[] }) => { /> - + ); })} From 38cdc28135497212f0733f32b02e6df1ebf70211 Mon Sep 17 00:00:00 2001 From: Morgan Bentell Date: Thu, 5 Jan 2023 17:28:26 +0100 Subject: [PATCH 11/55] add --preview-app-port option to serve command Signed-off-by: Morgan Bentell --- packages/techdocs-cli/serve.sh | 0 packages/techdocs-cli/src/commands/index.ts | 15 +++++++++++++++ packages/techdocs-cli/src/commands/serve/serve.ts | 10 ++++------ 3 files changed, 19 insertions(+), 6 deletions(-) create mode 100644 packages/techdocs-cli/serve.sh diff --git a/packages/techdocs-cli/serve.sh b/packages/techdocs-cli/serve.sh new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/techdocs-cli/src/commands/index.ts b/packages/techdocs-cli/src/commands/index.ts index e6c82ba7f8..b951699104 100644 --- a/packages/techdocs-cli/src/commands/index.ts +++ b/packages/techdocs-cli/src/commands/index.ts @@ -255,6 +255,21 @@ export function registerCommands(program: Command) { '--preview-app-bundle-path ', 'Preview documentation using another web app', ) + .option( + '--preview-app-port ', + 'Port for the preview app to be served on', + '3000', + ) + .hook('preAction', command => { + if ( + command.opts().previewAppPort && + !command.opts().previewAppBundlePath + ) { + command.error( + '--preview-app-port can only be used together with --preview-app-bundle-path', + ); + } + }) .action(lazy(() => import('./serve/serve').then(m => m.default))); } diff --git a/packages/techdocs-cli/src/commands/serve/serve.ts b/packages/techdocs-cli/src/commands/serve/serve.ts index ebb984782c..c99ab178f0 100644 --- a/packages/techdocs-cli/src/commands/serve/serve.ts +++ b/packages/techdocs-cli/src/commands/serve/serve.ts @@ -56,10 +56,6 @@ export default async function serve(opts: OptionValues) { ? true : false; - // TODO: Backstage app port should also be configurable as a CLI option. However, since we bundle - // a backstage app, we define app.baseUrl in the app-config.yaml. - // Hence, it is complicated to make this configurable. - const backstagePort = 3000; const backstageBackendPort = 7007; const mkdocsDockerAddr = `http://0.0.0.0:${opts.mkdocsPort}`; @@ -119,9 +115,11 @@ export default async function serve(opts: OptionValues) { ); } - const port = isDevMode ? backstageBackendPort : backstagePort; + const port = isDevMode ? backstageBackendPort : opts.previewAppPort; + const previewAppPath = getPreviewAppPath(opts); + console.log({ previewAppPath }); const httpServer = new HTTPServer( - getPreviewAppPath(opts), + previewAppPath, port, opts.mkdocsPort, opts.verbose, From 7c85508a02abef046acec64eca8f41dd8e93c456 Mon Sep 17 00:00:00 2001 From: Paulo Eduardo Peixoto Date: Thu, 5 Jan 2023 20:18:45 -0300 Subject: [PATCH 12/55] docs(docs/features/software-templates/writing-custom-field-extensions.md): update doc with valid example. After bug at React. Signed-off-by: Paulo Eduardo Peixoto --- .../writing-custom-field-extensions.md | 57 ++++++++++++------- 1 file changed, 37 insertions(+), 20 deletions(-) diff --git a/docs/features/software-templates/writing-custom-field-extensions.md b/docs/features/software-templates/writing-custom-field-extensions.md index 1d28bd5db9..a2c40a01b9 100644 --- a/docs/features/software-templates/writing-custom-field-extensions.md +++ b/docs/features/software-templates/writing-custom-field-extensions.md @@ -23,18 +23,19 @@ the `Scaffolder` frontend plugin in your own `App.tsx`. You can create your own Field Extension by using the [`createScaffolderFieldExtension`](https://backstage.io/docs/reference/plugin-scaffolder.createscaffolderfieldextension) -`API` like below: +`API` like below. + +As an example, we will create a component that validates whether a string is in the "Kehab case" pattern: ```tsx -//packages/app/src/scaffolder/MyCustomExtension/MyCustomExtension.tsx +//packages/app/src/scaffolder/ValidateKehabCase/ValidateKehabCaseExtension.tsx import React from 'react'; import { FieldProps, FieldValidation } from '@rjsf/core'; import FormControl from '@material-ui/core/FormControl'; -import { KubernetesValidatorFunctions } from '@backstage/catalog-model'; /* This is the actual component that will get rendered in the form */ -export const MyCustomExtension = ({ +export const ValidateKehabCaseExtension = ({ onChange, rawErrors, required, @@ -45,8 +46,17 @@ export const MyCustomExtension = ({ margin="normal" required={required} error={rawErrors?.length > 0 && !formData} - onChange={onChange} - /> + > + Name + onChange(e.target?.value)} + /> + + Use only letters, numbers, hyphens and underscores + + ); }; @@ -55,20 +65,22 @@ export const MyCustomExtension = ({ You will get the value from the `onChange` handler before as the value here to make sure that the types are aligned\ */ -export const myCustomValidation = ( +export const validateKehabCaseValidation = ( value: string, validation: FieldValidation, ) => { - if (!KubernetesValidatorFunctions.isValidObjectName(value)) { + const kehabCase = /^[a-z0-9-_]+$/g.test(value); + + if (kehabCase === false) { validation.addError( - 'must start and end with an alphanumeric character, and contain only alphanumeric characters, hyphens, underscores, and periods. Maximum length is 63 characters.', + `Only use letters, numbers, hyphen ("-") and underscore ("_").`, ); } }; ``` ```tsx -// packages/app/src/scaffolder/MyCustomExtension/extensions.ts +// packages/app/src/scaffolder/ValidateKehabCase/extensions.ts /* This is where the magic happens and creates the custom field extension. @@ -81,21 +93,24 @@ import { scaffolderPlugin, createScaffolderFieldExtension, } from '@backstage/plugin-scaffolder'; -import { MyCustomExtension, myCustomValidation } from './MyCustomExtension'; +import { + ValidateKehabCase, + validateKehabCaseValidation, +} from './ValidateKehabCase'; -export const MyCustomFieldExtension = scaffolderPlugin.provide( +export const ValidateKehabCaseFieldExtension = scaffolderPlugin.provide( createScaffolderFieldExtension({ - name: 'MyCustomExtension', - component: MyCustomExtension, - validation: myCustomValidation, + name: 'ValidateKehabCase', + component: ValidateKehabCase, + validation: validateKehabCaseValidation, }), ); ``` ```tsx -// packages/app/src/scaffolder/MyCustomExtension/index.ts +// packages/app/src/scaffolder/ValidateKehabCase/index.ts -export { MyCustomFieldExtension } from './extensions'; +export { ValidateKehabCaseFieldExtension } from './extensions'; ``` Once all these files are in place, you then need to provide your custom @@ -117,7 +132,7 @@ const routes = ( Should look something like this instead: ```tsx -import { MyCustomFieldExtension } from './scaffolder/MyCustomExtension'; +import { ValidateKehabCaseFieldExtension } from './scaffolder/ValidateKehabCase'; import { ScaffolderFieldExtensions } from '@backstage/plugin-scaffolder'; const routes = ( @@ -125,7 +140,7 @@ const routes = ( ... }> - + ... @@ -158,7 +173,9 @@ spec: title: Name type: string description: My custom name for the component - ui:field: MyCustomExtension + ui:field: ValidateKehabCaseExtension + steps: + [...] ``` ## Access Data from other Fields From 9f2b786fc9a55bb2133aaf3482b17b54cc991911 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 7 Jan 2023 14:15:17 +0100 Subject: [PATCH 13/55] Avoid directly logging error objects Signed-off-by: Patrik Oldsberg --- .changeset/tame-spoons-exercise.md | 16 ++++++++++++++++ .../techdocs-cli/src/commands/serve/serve.ts | 2 +- .../src/providers/AwsS3EntityProvider.ts | 2 +- .../src/providers/AzureDevOpsEntityProvider.ts | 2 +- .../src/BitbucketCloudEntityProvider.ts | 2 +- .../providers/BitbucketServerEntityProvider.ts | 2 +- .../src/providers/GerritEntityProvider.ts | 2 +- .../src/providers/GithubEntityProvider.ts | 2 +- .../src/providers/GithubOrgEntityProvider.ts | 2 +- .../providers/GitlabDiscoveryEntityProvider.ts | 2 +- .../src/engine/IncrementalIngestionEngine.ts | 5 ++++- .../src/processors/LdapOrgEntityProvider.ts | 2 +- .../MicrosoftGraphOrgEntityProvider.ts | 2 +- .../publisher/AwsSqsConsumingEventPublisher.ts | 2 +- 14 files changed, 32 insertions(+), 13 deletions(-) create mode 100644 .changeset/tame-spoons-exercise.md diff --git a/.changeset/tame-spoons-exercise.md b/.changeset/tame-spoons-exercise.md new file mode 100644 index 0000000000..37c489cb32 --- /dev/null +++ b/.changeset/tame-spoons-exercise.md @@ -0,0 +1,16 @@ +--- +'@backstage/plugin-catalog-backend-module-incremental-ingestion': patch +'@backstage/plugin-catalog-backend-module-bitbucket-server': patch +'@backstage/plugin-catalog-backend-module-bitbucket-cloud': patch +'@backstage/plugin-catalog-backend-module-msgraph': patch +'@backstage/plugin-catalog-backend-module-gerrit': patch +'@backstage/plugin-catalog-backend-module-github': patch +'@backstage/plugin-catalog-backend-module-gitlab': patch +'@backstage/plugin-events-backend-module-aws-sqs': patch +'@backstage/plugin-catalog-backend-module-azure': patch +'@backstage/plugin-catalog-backend-module-ldap': patch +'@backstage/plugin-catalog-backend-module-aws': patch +'@techdocs/cli': patch +--- + +Provide context for logged errors. diff --git a/packages/techdocs-cli/src/commands/serve/serve.ts b/packages/techdocs-cli/src/commands/serve/serve.ts index 68d0bbb277..fd1febfb6c 100644 --- a/packages/techdocs-cli/src/commands/serve/serve.ts +++ b/packages/techdocs-cli/src/commands/serve/serve.ts @@ -126,7 +126,7 @@ export default async function serve(opts: OptionValues) { httpServer .serve() .catch(err => { - logger.error(err); + logger.error('Failed to start HTTP server', err); mkdocsChildProcess.kill(); process.exit(1); }) diff --git a/plugins/catalog-backend-module-aws/src/providers/AwsS3EntityProvider.ts b/plugins/catalog-backend-module-aws/src/providers/AwsS3EntityProvider.ts index 80bc4d459b..b92fcfc022 100644 --- a/plugins/catalog-backend-module-aws/src/providers/AwsS3EntityProvider.ts +++ b/plugins/catalog-backend-module-aws/src/providers/AwsS3EntityProvider.ts @@ -131,7 +131,7 @@ export class AwsS3EntityProvider implements EntityProvider { try { await this.refresh(logger); } catch (error) { - logger.error(error); + logger.error(`${this.getProviderName()} refresh failed`, error); } }, }); diff --git a/plugins/catalog-backend-module-azure/src/providers/AzureDevOpsEntityProvider.ts b/plugins/catalog-backend-module-azure/src/providers/AzureDevOpsEntityProvider.ts index b824d176ee..98b951bd46 100644 --- a/plugins/catalog-backend-module-azure/src/providers/AzureDevOpsEntityProvider.ts +++ b/plugins/catalog-backend-module-azure/src/providers/AzureDevOpsEntityProvider.ts @@ -113,7 +113,7 @@ export class AzureDevOpsEntityProvider implements EntityProvider { try { await this.refresh(logger); } catch (error) { - logger.error(error); + logger.error(`${this.getProviderName()} refresh failed`, error); } }, }); diff --git a/plugins/catalog-backend-module-bitbucket-cloud/src/BitbucketCloudEntityProvider.ts b/plugins/catalog-backend-module-bitbucket-cloud/src/BitbucketCloudEntityProvider.ts index 83b6f447f2..a139bd7d8d 100644 --- a/plugins/catalog-backend-module-bitbucket-cloud/src/BitbucketCloudEntityProvider.ts +++ b/plugins/catalog-backend-module-bitbucket-cloud/src/BitbucketCloudEntityProvider.ts @@ -152,7 +152,7 @@ export class BitbucketCloudEntityProvider try { await this.refresh(logger); } catch (error) { - logger.error(error); + logger.error(`${this.getProviderName()} refresh failed`, error); } }, }); diff --git a/plugins/catalog-backend-module-bitbucket-server/src/providers/BitbucketServerEntityProvider.ts b/plugins/catalog-backend-module-bitbucket-server/src/providers/BitbucketServerEntityProvider.ts index 447b71ee58..2ecdd79019 100644 --- a/plugins/catalog-backend-module-bitbucket-server/src/providers/BitbucketServerEntityProvider.ts +++ b/plugins/catalog-backend-module-bitbucket-server/src/providers/BitbucketServerEntityProvider.ts @@ -130,7 +130,7 @@ export class BitbucketServerEntityProvider implements EntityProvider { try { await this.refresh(logger); } catch (error) { - logger.error(error); + logger.error(`${this.getProviderName()} refresh failed`, error); } }, }); diff --git a/plugins/catalog-backend-module-gerrit/src/providers/GerritEntityProvider.ts b/plugins/catalog-backend-module-gerrit/src/providers/GerritEntityProvider.ts index ec08dbe4be..986f8ae60c 100644 --- a/plugins/catalog-backend-module-gerrit/src/providers/GerritEntityProvider.ts +++ b/plugins/catalog-backend-module-gerrit/src/providers/GerritEntityProvider.ts @@ -129,7 +129,7 @@ export class GerritEntityProvider implements EntityProvider { try { await this.refresh(logger); } catch (error) { - logger.error(error); + logger.error(`${this.getProviderName()} refresh failed`, error); } }, }); diff --git a/plugins/catalog-backend-module-github/src/providers/GithubEntityProvider.ts b/plugins/catalog-backend-module-github/src/providers/GithubEntityProvider.ts index 8816d8ee80..e979d733d4 100644 --- a/plugins/catalog-backend-module-github/src/providers/GithubEntityProvider.ts +++ b/plugins/catalog-backend-module-github/src/providers/GithubEntityProvider.ts @@ -156,7 +156,7 @@ export class GithubEntityProvider implements EntityProvider, EventSubscriber { try { await this.refresh(logger); } catch (error) { - logger.error(error); + logger.error(`${this.getProviderName()} refresh failed`, error); } }, }); diff --git a/plugins/catalog-backend-module-github/src/providers/GithubOrgEntityProvider.ts b/plugins/catalog-backend-module-github/src/providers/GithubOrgEntityProvider.ts index 46ad089625..3093c2a897 100644 --- a/plugins/catalog-backend-module-github/src/providers/GithubOrgEntityProvider.ts +++ b/plugins/catalog-backend-module-github/src/providers/GithubOrgEntityProvider.ts @@ -565,7 +565,7 @@ export class GithubOrgEntityProvider try { await this.read({ logger }); } catch (error) { - logger.error(error); + logger.error(`${this.getProviderName()} refresh failed`, error); } }, }); diff --git a/plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.ts b/plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.ts index cb4629b820..1cc818c811 100644 --- a/plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.ts +++ b/plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.ts @@ -133,7 +133,7 @@ export class GitlabDiscoveryEntityProvider implements EntityProvider { try { await this.refresh(logger); } catch (error) { - logger.error(error); + logger.error(`${this.getProviderName()} refresh failed`, error); } }, }); diff --git a/plugins/catalog-backend-module-incremental-ingestion/src/engine/IncrementalIngestionEngine.ts b/plugins/catalog-backend-module-incremental-ingestion/src/engine/IncrementalIngestionEngine.ts index e2000c609e..e94bc9c53f 100644 --- a/plugins/catalog-backend-module-incremental-ingestion/src/engine/IncrementalIngestionEngine.ts +++ b/plugins/catalog-backend-module-incremental-ingestion/src/engine/IncrementalIngestionEngine.ts @@ -111,7 +111,10 @@ export class IncrementalIngestionEngine implements IterationEngine { ); const backoffLength = currentBackoff.as('milliseconds'); - this.options.logger.error(error); + this.options.logger.error( + `incremental-engine: Ingestion '${ingestionId}' failed`, + error, + ); const truncatedError = stringifyError(error).substring(0, 700); this.options.logger.error( diff --git a/plugins/catalog-backend-module-ldap/src/processors/LdapOrgEntityProvider.ts b/plugins/catalog-backend-module-ldap/src/processors/LdapOrgEntityProvider.ts index b38ea51cc0..64d56cc8bc 100644 --- a/plugins/catalog-backend-module-ldap/src/processors/LdapOrgEntityProvider.ts +++ b/plugins/catalog-backend-module-ldap/src/processors/LdapOrgEntityProvider.ts @@ -225,7 +225,7 @@ export class LdapOrgEntityProvider implements EntityProvider { try { await this.read({ logger }); } catch (error) { - logger.error(error); + logger.error(`${this.getProviderName()} refresh failed`, error); } }, }); diff --git a/plugins/catalog-backend-module-msgraph/src/processors/MicrosoftGraphOrgEntityProvider.ts b/plugins/catalog-backend-module-msgraph/src/processors/MicrosoftGraphOrgEntityProvider.ts index 32892e7201..5c1a7f9e1c 100644 --- a/plugins/catalog-backend-module-msgraph/src/processors/MicrosoftGraphOrgEntityProvider.ts +++ b/plugins/catalog-backend-module-msgraph/src/processors/MicrosoftGraphOrgEntityProvider.ts @@ -351,7 +351,7 @@ export class MicrosoftGraphOrgEntityProvider implements EntityProvider { try { await this.read({ logger }); } catch (error) { - logger.error(error); + logger.error(`${this.getProviderName()} refresh failed`, error); } }, }); diff --git a/plugins/events-backend-module-aws-sqs/src/publisher/AwsSqsConsumingEventPublisher.ts b/plugins/events-backend-module-aws-sqs/src/publisher/AwsSqsConsumingEventPublisher.ts index 4638769c45..e649e2ee83 100644 --- a/plugins/events-backend-module-aws-sqs/src/publisher/AwsSqsConsumingEventPublisher.ts +++ b/plugins/events-backend-module-aws-sqs/src/publisher/AwsSqsConsumingEventPublisher.ts @@ -100,7 +100,7 @@ export class AwsSqsConsumingEventPublisher implements EventPublisher { await this.sleep(this.waitTimeAfterEmptyReceiveMs); } } catch (error) { - logger.error(error); + logger.error('Failed to consume AWS SQS messages', error); } }, }); From 1a3dd5f8435fe164c6adab5b08c1b1d8fee1a295 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 7 Jan 2023 14:42:06 +0000 Subject: [PATCH 14/55] Update dependency @swc/core to v1.3.25 Signed-off-by: Renovate Bot --- storybook/yarn.lock | 88 ++++++++++++++++++++++----------------------- yarn.lock | 88 ++++++++++++++++++++++----------------------- 2 files changed, 86 insertions(+), 90 deletions(-) diff --git a/storybook/yarn.lock b/storybook/yarn.lock index 42c2bf1ad0..2a7fd49da3 100644 --- a/storybook/yarn.lock +++ b/storybook/yarn.lock @@ -2966,90 +2966,90 @@ __metadata: languageName: node linkType: hard -"@swc/core-darwin-arm64@npm:1.3.24": - version: 1.3.24 - resolution: "@swc/core-darwin-arm64@npm:1.3.24" +"@swc/core-darwin-arm64@npm:1.3.25": + version: 1.3.25 + resolution: "@swc/core-darwin-arm64@npm:1.3.25" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@swc/core-darwin-x64@npm:1.3.24": - version: 1.3.24 - resolution: "@swc/core-darwin-x64@npm:1.3.24" +"@swc/core-darwin-x64@npm:1.3.25": + version: 1.3.25 + resolution: "@swc/core-darwin-x64@npm:1.3.25" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@swc/core-linux-arm-gnueabihf@npm:1.3.24": - version: 1.3.24 - resolution: "@swc/core-linux-arm-gnueabihf@npm:1.3.24" +"@swc/core-linux-arm-gnueabihf@npm:1.3.25": + version: 1.3.25 + resolution: "@swc/core-linux-arm-gnueabihf@npm:1.3.25" conditions: os=linux & cpu=arm languageName: node linkType: hard -"@swc/core-linux-arm64-gnu@npm:1.3.24": - version: 1.3.24 - resolution: "@swc/core-linux-arm64-gnu@npm:1.3.24" +"@swc/core-linux-arm64-gnu@npm:1.3.25": + version: 1.3.25 + resolution: "@swc/core-linux-arm64-gnu@npm:1.3.25" conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard -"@swc/core-linux-arm64-musl@npm:1.3.24": - version: 1.3.24 - resolution: "@swc/core-linux-arm64-musl@npm:1.3.24" +"@swc/core-linux-arm64-musl@npm:1.3.25": + version: 1.3.25 + resolution: "@swc/core-linux-arm64-musl@npm:1.3.25" conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard -"@swc/core-linux-x64-gnu@npm:1.3.24": - version: 1.3.24 - resolution: "@swc/core-linux-x64-gnu@npm:1.3.24" +"@swc/core-linux-x64-gnu@npm:1.3.25": + version: 1.3.25 + resolution: "@swc/core-linux-x64-gnu@npm:1.3.25" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard -"@swc/core-linux-x64-musl@npm:1.3.24": - version: 1.3.24 - resolution: "@swc/core-linux-x64-musl@npm:1.3.24" +"@swc/core-linux-x64-musl@npm:1.3.25": + version: 1.3.25 + resolution: "@swc/core-linux-x64-musl@npm:1.3.25" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard -"@swc/core-win32-arm64-msvc@npm:1.3.24": - version: 1.3.24 - resolution: "@swc/core-win32-arm64-msvc@npm:1.3.24" +"@swc/core-win32-arm64-msvc@npm:1.3.25": + version: 1.3.25 + resolution: "@swc/core-win32-arm64-msvc@npm:1.3.25" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@swc/core-win32-ia32-msvc@npm:1.3.24": - version: 1.3.24 - resolution: "@swc/core-win32-ia32-msvc@npm:1.3.24" +"@swc/core-win32-ia32-msvc@npm:1.3.25": + version: 1.3.25 + resolution: "@swc/core-win32-ia32-msvc@npm:1.3.25" conditions: os=win32 & cpu=ia32 languageName: node linkType: hard -"@swc/core-win32-x64-msvc@npm:1.3.24": - version: 1.3.24 - resolution: "@swc/core-win32-x64-msvc@npm:1.3.24" +"@swc/core-win32-x64-msvc@npm:1.3.25": + version: 1.3.25 + resolution: "@swc/core-win32-x64-msvc@npm:1.3.25" conditions: os=win32 & cpu=x64 languageName: node linkType: hard "@swc/core@npm:^1.3.9": - version: 1.3.24 - resolution: "@swc/core@npm:1.3.24" + version: 1.3.25 + resolution: "@swc/core@npm:1.3.25" dependencies: - "@swc/core-darwin-arm64": 1.3.24 - "@swc/core-darwin-x64": 1.3.24 - "@swc/core-linux-arm-gnueabihf": 1.3.24 - "@swc/core-linux-arm64-gnu": 1.3.24 - "@swc/core-linux-arm64-musl": 1.3.24 - "@swc/core-linux-x64-gnu": 1.3.24 - "@swc/core-linux-x64-musl": 1.3.24 - "@swc/core-win32-arm64-msvc": 1.3.24 - "@swc/core-win32-ia32-msvc": 1.3.24 - "@swc/core-win32-x64-msvc": 1.3.24 + "@swc/core-darwin-arm64": 1.3.25 + "@swc/core-darwin-x64": 1.3.25 + "@swc/core-linux-arm-gnueabihf": 1.3.25 + "@swc/core-linux-arm64-gnu": 1.3.25 + "@swc/core-linux-arm64-musl": 1.3.25 + "@swc/core-linux-x64-gnu": 1.3.25 + "@swc/core-linux-x64-musl": 1.3.25 + "@swc/core-win32-arm64-msvc": 1.3.25 + "@swc/core-win32-ia32-msvc": 1.3.25 + "@swc/core-win32-x64-msvc": 1.3.25 dependenciesMeta: "@swc/core-darwin-arm64": optional: true @@ -3071,9 +3071,7 @@ __metadata: optional: true "@swc/core-win32-x64-msvc": optional: true - bin: - swcx: run_swcx.js - checksum: a27b842be129b83c116f804e63deaa51dbd5d9b77d6260888d549f6408df1dd05aeef20046ceacc9fd7458e6afda6723545249bd77f77086b98bd9bf84738c19 + checksum: de45a7dd871cc9497ad998d6a320d3c95cb9c74fdcb70590ff1f631e75001820d021bbfd5c463e9172afcb5ee47bffaa8fb893230e1329538c9f7afbd5ed45cf languageName: node linkType: hard diff --git a/yarn.lock b/yarn.lock index eed39c16fa..be7c3fc018 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13503,90 +13503,90 @@ __metadata: languageName: node linkType: hard -"@swc/core-darwin-arm64@npm:1.3.24": - version: 1.3.24 - resolution: "@swc/core-darwin-arm64@npm:1.3.24" +"@swc/core-darwin-arm64@npm:1.3.25": + version: 1.3.25 + resolution: "@swc/core-darwin-arm64@npm:1.3.25" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@swc/core-darwin-x64@npm:1.3.24": - version: 1.3.24 - resolution: "@swc/core-darwin-x64@npm:1.3.24" +"@swc/core-darwin-x64@npm:1.3.25": + version: 1.3.25 + resolution: "@swc/core-darwin-x64@npm:1.3.25" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@swc/core-linux-arm-gnueabihf@npm:1.3.24": - version: 1.3.24 - resolution: "@swc/core-linux-arm-gnueabihf@npm:1.3.24" +"@swc/core-linux-arm-gnueabihf@npm:1.3.25": + version: 1.3.25 + resolution: "@swc/core-linux-arm-gnueabihf@npm:1.3.25" conditions: os=linux & cpu=arm languageName: node linkType: hard -"@swc/core-linux-arm64-gnu@npm:1.3.24": - version: 1.3.24 - resolution: "@swc/core-linux-arm64-gnu@npm:1.3.24" +"@swc/core-linux-arm64-gnu@npm:1.3.25": + version: 1.3.25 + resolution: "@swc/core-linux-arm64-gnu@npm:1.3.25" conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard -"@swc/core-linux-arm64-musl@npm:1.3.24": - version: 1.3.24 - resolution: "@swc/core-linux-arm64-musl@npm:1.3.24" +"@swc/core-linux-arm64-musl@npm:1.3.25": + version: 1.3.25 + resolution: "@swc/core-linux-arm64-musl@npm:1.3.25" conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard -"@swc/core-linux-x64-gnu@npm:1.3.24": - version: 1.3.24 - resolution: "@swc/core-linux-x64-gnu@npm:1.3.24" +"@swc/core-linux-x64-gnu@npm:1.3.25": + version: 1.3.25 + resolution: "@swc/core-linux-x64-gnu@npm:1.3.25" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard -"@swc/core-linux-x64-musl@npm:1.3.24": - version: 1.3.24 - resolution: "@swc/core-linux-x64-musl@npm:1.3.24" +"@swc/core-linux-x64-musl@npm:1.3.25": + version: 1.3.25 + resolution: "@swc/core-linux-x64-musl@npm:1.3.25" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard -"@swc/core-win32-arm64-msvc@npm:1.3.24": - version: 1.3.24 - resolution: "@swc/core-win32-arm64-msvc@npm:1.3.24" +"@swc/core-win32-arm64-msvc@npm:1.3.25": + version: 1.3.25 + resolution: "@swc/core-win32-arm64-msvc@npm:1.3.25" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@swc/core-win32-ia32-msvc@npm:1.3.24": - version: 1.3.24 - resolution: "@swc/core-win32-ia32-msvc@npm:1.3.24" +"@swc/core-win32-ia32-msvc@npm:1.3.25": + version: 1.3.25 + resolution: "@swc/core-win32-ia32-msvc@npm:1.3.25" conditions: os=win32 & cpu=ia32 languageName: node linkType: hard -"@swc/core-win32-x64-msvc@npm:1.3.24": - version: 1.3.24 - resolution: "@swc/core-win32-x64-msvc@npm:1.3.24" +"@swc/core-win32-x64-msvc@npm:1.3.25": + version: 1.3.25 + resolution: "@swc/core-win32-x64-msvc@npm:1.3.25" conditions: os=win32 & cpu=x64 languageName: node linkType: hard "@swc/core@npm:^1.3.9": - version: 1.3.24 - resolution: "@swc/core@npm:1.3.24" + version: 1.3.25 + resolution: "@swc/core@npm:1.3.25" dependencies: - "@swc/core-darwin-arm64": 1.3.24 - "@swc/core-darwin-x64": 1.3.24 - "@swc/core-linux-arm-gnueabihf": 1.3.24 - "@swc/core-linux-arm64-gnu": 1.3.24 - "@swc/core-linux-arm64-musl": 1.3.24 - "@swc/core-linux-x64-gnu": 1.3.24 - "@swc/core-linux-x64-musl": 1.3.24 - "@swc/core-win32-arm64-msvc": 1.3.24 - "@swc/core-win32-ia32-msvc": 1.3.24 - "@swc/core-win32-x64-msvc": 1.3.24 + "@swc/core-darwin-arm64": 1.3.25 + "@swc/core-darwin-x64": 1.3.25 + "@swc/core-linux-arm-gnueabihf": 1.3.25 + "@swc/core-linux-arm64-gnu": 1.3.25 + "@swc/core-linux-arm64-musl": 1.3.25 + "@swc/core-linux-x64-gnu": 1.3.25 + "@swc/core-linux-x64-musl": 1.3.25 + "@swc/core-win32-arm64-msvc": 1.3.25 + "@swc/core-win32-ia32-msvc": 1.3.25 + "@swc/core-win32-x64-msvc": 1.3.25 dependenciesMeta: "@swc/core-darwin-arm64": optional: true @@ -13608,9 +13608,7 @@ __metadata: optional: true "@swc/core-win32-x64-msvc": optional: true - bin: - swcx: run_swcx.js - checksum: a27b842be129b83c116f804e63deaa51dbd5d9b77d6260888d549f6408df1dd05aeef20046ceacc9fd7458e6afda6723545249bd77f77086b98bd9bf84738c19 + checksum: de45a7dd871cc9497ad998d6a320d3c95cb9c74fdcb70590ff1f631e75001820d021bbfd5c463e9172afcb5ee47bffaa8fb893230e1329538c9f7afbd5ed45cf languageName: node linkType: hard From 29113e760af682145b11eed7de2c13ff8d284bd2 Mon Sep 17 00:00:00 2001 From: Morgan Bentell Date: Mon, 9 Jan 2023 10:03:39 +0100 Subject: [PATCH 15/55] update docs and clean up Signed-off-by: Morgan Bentell --- .changeset/dry-camels-scream.md | 2 +- docs/features/techdocs/cli.md | 2 ++ packages/techdocs-cli/cli-report.md | 1 + packages/techdocs-cli/src/commands/serve/serve.ts | 1 - 4 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.changeset/dry-camels-scream.md b/.changeset/dry-camels-scream.md index ea3a7f47b8..ea35cece7a 100644 --- a/.changeset/dry-camels-scream.md +++ b/.changeset/dry-camels-scream.md @@ -2,4 +2,4 @@ '@techdocs/cli': minor --- -Add `--preview-app-bundle-path` option to the `serve` command enabling previewing with apps other than the provided one +Add `--preview-app-bundle-path` and `--preview-app-port` options to the `serve` command enabling previewing with apps other than the provided one diff --git a/docs/features/techdocs/cli.md b/docs/features/techdocs/cli.md index 9d8d2b24d9..1c01441db3 100644 --- a/docs/features/techdocs/cli.md +++ b/docs/features/techdocs/cli.md @@ -95,6 +95,8 @@ Options: --no-docker Do not use Docker, use MkDocs executable in current user environment. --mkdocs-port Port for MkDocs server to use (default: "8000") --preview-app-bundle-path Preview documentation using a web app other than the included one. + --preview-app-port Port where the preview will be served. + Can only be used with "--preview-app-bundle-path". (default: "3000") -v --verbose Enable verbose output. (default: false) -h, --help display help for command ``` diff --git a/packages/techdocs-cli/cli-report.md b/packages/techdocs-cli/cli-report.md index ad41041dfb..ada61f2542 100644 --- a/packages/techdocs-cli/cli-report.md +++ b/packages/techdocs-cli/cli-report.md @@ -100,6 +100,7 @@ Options: --no-docker --mkdocs-port --preview-app-bundle-path + --preview-app-port -v --verbose -h, --help ``` diff --git a/packages/techdocs-cli/src/commands/serve/serve.ts b/packages/techdocs-cli/src/commands/serve/serve.ts index c99ab178f0..2aea8e4e80 100644 --- a/packages/techdocs-cli/src/commands/serve/serve.ts +++ b/packages/techdocs-cli/src/commands/serve/serve.ts @@ -117,7 +117,6 @@ export default async function serve(opts: OptionValues) { const port = isDevMode ? backstageBackendPort : opts.previewAppPort; const previewAppPath = getPreviewAppPath(opts); - console.log({ previewAppPath }); const httpServer = new HTTPServer( previewAppPath, port, From a3497ad62128297abbdbf598904c359fc3353e7e Mon Sep 17 00:00:00 2001 From: Paulo Eduardo Peixoto Date: Mon, 9 Jan 2023 08:18:16 -0300 Subject: [PATCH 16/55] style(docs/features/software-templates/writing-custom-field.extension.md): change style at line 28. Signed-off-by: Paulo Eduardo Peixoto --- .../software-templates/writing-custom-field-extensions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/features/software-templates/writing-custom-field-extensions.md b/docs/features/software-templates/writing-custom-field-extensions.md index a2c40a01b9..24861936c0 100644 --- a/docs/features/software-templates/writing-custom-field-extensions.md +++ b/docs/features/software-templates/writing-custom-field-extensions.md @@ -25,7 +25,7 @@ You can create your own Field Extension by using the [`createScaffolderFieldExtension`](https://backstage.io/docs/reference/plugin-scaffolder.createscaffolderfieldextension) `API` like below. -As an example, we will create a component that validates whether a string is in the "Kehab case" pattern: +As an example, we will create a component that validates whether a string is in the `Kehab-case` pattern: ```tsx //packages/app/src/scaffolder/ValidateKehabCase/ValidateKehabCaseExtension.tsx From f7ed480ffa9a06fd6668d9e62afa2324e597732e Mon Sep 17 00:00:00 2001 From: Heikki Hellgren Date: Mon, 9 Jan 2023 15:03:11 +0200 Subject: [PATCH 17/55] Add Toolbox plugin Closes #3758 Signed-off-by: Heikki Hellgren --- microsite/data/plugins/toolbox.yaml | 10 ++++++++++ microsite/static/img/toolbox-logo.png | Bin 0 -> 32360 bytes 2 files changed, 10 insertions(+) create mode 100644 microsite/data/plugins/toolbox.yaml create mode 100644 microsite/static/img/toolbox-logo.png diff --git a/microsite/data/plugins/toolbox.yaml b/microsite/data/plugins/toolbox.yaml new file mode 100644 index 0000000000..9d9adf0224 --- /dev/null +++ b/microsite/data/plugins/toolbox.yaml @@ -0,0 +1,10 @@ +--- +title: Toolbox +author: drodil +authorUrl: https://github.com/drodil +category: Tools +description: Development related tools within Backstage +documentation: https://github.com/drodil/backstage-plugin-toolbox +iconUrl: img/toolbox-logo.png +npmPackageName: '@drodil/backstage-plugin-toolbox' +addedDate: '2023-01-09' diff --git a/microsite/static/img/toolbox-logo.png b/microsite/static/img/toolbox-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..33a16f5f49243e0d83c8060c97be0c79ec9b7195 GIT binary patch literal 32360 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%0D$3)%{mP%d$PK@TVlWr{**q}t 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 HcJF@ydrES& literal 0 HcmV?d00001 From ecc6bfe4c979f8febad105be7303435ed5f6e956 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 3 Jan 2023 13:45:50 +0100 Subject: [PATCH 18/55] backend-plugin-api: add ServiceFactoryOrFunction type Signed-off-by: Patrik Oldsberg --- .changeset/cuddly-fishes-switch.md | 6 ++++++ .changeset/eight-mugs-draw.md | 5 +++++ packages/backend-app-api/api-report.md | 3 ++- packages/backend-app-api/src/wiring/types.ts | 4 ++-- packages/backend-defaults/api-report.md | 4 ++-- packages/backend-defaults/src/CreateBackend.ts | 4 ++-- packages/backend-plugin-api/api-report.md | 7 ++++++- .../backend-plugin-api/src/services/system/index.ts | 1 + .../backend-plugin-api/src/services/system/types.ts | 11 ++++++++++- 9 files changed, 36 insertions(+), 9 deletions(-) create mode 100644 .changeset/cuddly-fishes-switch.md create mode 100644 .changeset/eight-mugs-draw.md diff --git a/.changeset/cuddly-fishes-switch.md b/.changeset/cuddly-fishes-switch.md new file mode 100644 index 0000000000..75f09c16b7 --- /dev/null +++ b/.changeset/cuddly-fishes-switch.md @@ -0,0 +1,6 @@ +--- +'@backstage/backend-defaults': patch +'@backstage/backend-app-api': patch +--- + +Use new `ServiceFactoryOrFunction` type. diff --git a/.changeset/eight-mugs-draw.md b/.changeset/eight-mugs-draw.md new file mode 100644 index 0000000000..9d530e2a6d --- /dev/null +++ b/.changeset/eight-mugs-draw.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-plugin-api': patch +--- + +Added `ServiceFactoryOrFunction` type, for use when either a `ServiceFactory` or `() => ServiceFactory` can be used. diff --git a/packages/backend-app-api/api-report.md b/packages/backend-app-api/api-report.md index 1ff40c8b5d..a402150755 100644 --- a/packages/backend-app-api/api-report.md +++ b/packages/backend-app-api/api-report.md @@ -19,6 +19,7 @@ import { RootLifecycleService } from '@backstage/backend-plugin-api'; import { RootLoggerService } from '@backstage/backend-plugin-api'; import { SchedulerService } from '@backstage/backend-plugin-api'; import { ServiceFactory } from '@backstage/backend-plugin-api'; +import { ServiceFactoryOrFunction } from '@backstage/backend-plugin-api'; import { ServiceRef } from '@backstage/backend-plugin-api'; import { TokenManagerService } from '@backstage/backend-plugin-api'; import { UrlReader } from '@backstage/backend-common'; @@ -51,7 +52,7 @@ export function createSpecializedBackend( // @public (undocumented) export interface CreateSpecializedBackendOptions { // (undocumented) - services: (ServiceFactory | (() => ServiceFactory))[]; + services: ServiceFactoryOrFunction[]; } // @public (undocumented) diff --git a/packages/backend-app-api/src/wiring/types.ts b/packages/backend-app-api/src/wiring/types.ts index 115488d596..d3c4fc48c7 100644 --- a/packages/backend-app-api/src/wiring/types.ts +++ b/packages/backend-app-api/src/wiring/types.ts @@ -17,8 +17,8 @@ import { BackendFeature, ExtensionPoint, - ServiceFactory, ServiceRef, + ServiceFactoryOrFunction, } from '@backstage/backend-plugin-api'; /** @@ -42,7 +42,7 @@ export interface BackendRegisterInit { * @public */ export interface CreateSpecializedBackendOptions { - services: (ServiceFactory | (() => ServiceFactory))[]; + services: ServiceFactoryOrFunction[]; } export interface ServiceHolder { diff --git a/packages/backend-defaults/api-report.md b/packages/backend-defaults/api-report.md index 050ebde759..6cc58de663 100644 --- a/packages/backend-defaults/api-report.md +++ b/packages/backend-defaults/api-report.md @@ -4,7 +4,7 @@ ```ts import { Backend } from '@backstage/backend-app-api'; -import { ServiceFactory } from '@backstage/backend-plugin-api'; +import { ServiceFactoryOrFunction } from '@backstage/backend-plugin-api'; // @public (undocumented) export function createBackend(options?: CreateBackendOptions): Backend; @@ -12,6 +12,6 @@ export function createBackend(options?: CreateBackendOptions): Backend; // @public (undocumented) export interface CreateBackendOptions { // (undocumented) - services?: (ServiceFactory | (() => ServiceFactory))[]; + services?: ServiceFactoryOrFunction[]; } ``` diff --git a/packages/backend-defaults/src/CreateBackend.ts b/packages/backend-defaults/src/CreateBackend.ts index 6f9e8e39b6..bef80e4f2b 100644 --- a/packages/backend-defaults/src/CreateBackend.ts +++ b/packages/backend-defaults/src/CreateBackend.ts @@ -32,7 +32,7 @@ import { tokenManagerFactory, urlReaderFactory, } from '@backstage/backend-app-api'; -import { ServiceFactory } from '@backstage/backend-plugin-api'; +import { ServiceFactoryOrFunction } from '@backstage/backend-plugin-api'; export const defaultServiceFactories = [ cacheFactory(), @@ -55,7 +55,7 @@ export const defaultServiceFactories = [ * @public */ export interface CreateBackendOptions { - services?: (ServiceFactory | (() => ServiceFactory))[]; + services?: ServiceFactoryOrFunction[]; } /** diff --git a/packages/backend-plugin-api/api-report.md b/packages/backend-plugin-api/api-report.md index 2dcd0dccb4..6dc36c83b3 100644 --- a/packages/backend-plugin-api/api-report.md +++ b/packages/backend-plugin-api/api-report.md @@ -347,6 +347,11 @@ export interface ServiceFactoryConfig< service: ServiceRef; } +// @public +export type ServiceFactoryOrFunction = + | ServiceFactory + | (() => ServiceFactory); + // @public export type ServiceRef< TService, @@ -364,7 +369,7 @@ export interface ServiceRefConfig { // (undocumented) defaultFactory?: ( service: ServiceRef, - ) => Promise | (() => ServiceFactory)>; + ) => Promise>; // (undocumented) id: string; // (undocumented) diff --git a/packages/backend-plugin-api/src/services/system/index.ts b/packages/backend-plugin-api/src/services/system/index.ts index d00c2da4bd..cd15d8127d 100644 --- a/packages/backend-plugin-api/src/services/system/index.ts +++ b/packages/backend-plugin-api/src/services/system/index.ts @@ -20,5 +20,6 @@ export type { TypesToServiceRef, ServiceFactory, ServiceFactoryConfig, + ServiceFactoryOrFunction, } from './types'; export { createServiceRef, createServiceFactory } from './types'; diff --git a/packages/backend-plugin-api/src/services/system/types.ts b/packages/backend-plugin-api/src/services/system/types.ts index 7c3a9547b6..ffbbdb5f0c 100644 --- a/packages/backend-plugin-api/src/services/system/types.ts +++ b/packages/backend-plugin-api/src/services/system/types.ts @@ -71,13 +71,22 @@ export type ServiceFactory = >; }; +/** + * Represents either a {@link ServiceFactory} or a function that returns one. + * + * @public + */ +export type ServiceFactoryOrFunction = + | ServiceFactory + | (() => ServiceFactory); + /** @public */ export interface ServiceRefConfig { id: string; scope?: TScope; defaultFactory?: ( service: ServiceRef, - ) => Promise | (() => ServiceFactory)>; + ) => Promise>; } /** From 0939f56fd17f81e6a90fd57c1d0ea0a0576ed610 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 3 Jan 2023 17:17:03 +0100 Subject: [PATCH 19/55] backend-app-api: move http router factory implementation into subfolder Signed-off-by: Patrik Oldsberg --- .../httpRouterFactory.test.ts} | 2 +- .../httpRouterFactory.ts} | 0 .../implementations/httpRouter/index.ts | 18 ++++++++++++++++++ .../src/services/implementations/index.ts | 4 ++-- 4 files changed, 21 insertions(+), 3 deletions(-) rename packages/backend-app-api/src/services/implementations/{httpRouterService.test.ts => httpRouter/httpRouterFactory.test.ts} (97%) rename packages/backend-app-api/src/services/implementations/{httpRouterService.ts => httpRouter/httpRouterFactory.ts} (100%) create mode 100644 packages/backend-app-api/src/services/implementations/httpRouter/index.ts diff --git a/packages/backend-app-api/src/services/implementations/httpRouterService.test.ts b/packages/backend-app-api/src/services/implementations/httpRouter/httpRouterFactory.test.ts similarity index 97% rename from packages/backend-app-api/src/services/implementations/httpRouterService.test.ts rename to packages/backend-app-api/src/services/implementations/httpRouter/httpRouterFactory.test.ts index 89f6db2ac8..7717f4116e 100644 --- a/packages/backend-app-api/src/services/implementations/httpRouterService.test.ts +++ b/packages/backend-app-api/src/services/implementations/httpRouter/httpRouterFactory.test.ts @@ -18,7 +18,7 @@ import { HttpRouterService, ServiceFactory, } from '@backstage/backend-plugin-api'; -import { httpRouterFactory } from './httpRouterService'; +import { httpRouterFactory } from './httpRouterFactory'; describe('httpRouterFactory', () => { it('should register plugin paths', async () => { diff --git a/packages/backend-app-api/src/services/implementations/httpRouterService.ts b/packages/backend-app-api/src/services/implementations/httpRouter/httpRouterFactory.ts similarity index 100% rename from packages/backend-app-api/src/services/implementations/httpRouterService.ts rename to packages/backend-app-api/src/services/implementations/httpRouter/httpRouterFactory.ts diff --git a/packages/backend-app-api/src/services/implementations/httpRouter/index.ts b/packages/backend-app-api/src/services/implementations/httpRouter/index.ts new file mode 100644 index 0000000000..f2db660414 --- /dev/null +++ b/packages/backend-app-api/src/services/implementations/httpRouter/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 { httpRouterFactory } from './httpRouterFactory'; +export type { HttpRouterFactoryOptions } from './httpRouterFactory'; diff --git a/packages/backend-app-api/src/services/implementations/index.ts b/packages/backend-app-api/src/services/implementations/index.ts index 2365ff36f4..de5350e72d 100644 --- a/packages/backend-app-api/src/services/implementations/index.ts +++ b/packages/backend-app-api/src/services/implementations/index.ts @@ -14,6 +14,8 @@ * limitations under the License. */ +export * from './httpRouter'; + export { cacheFactory } from './cacheService'; export { configFactory } from './configService'; export { databaseFactory } from './databaseService'; @@ -24,9 +26,7 @@ export { permissionsFactory } from './permissionsService'; export { schedulerFactory } from './schedulerService'; export { tokenManagerFactory } from './tokenManagerService'; export { urlReaderFactory } from './urlReaderService'; -export { httpRouterFactory } from './httpRouterService'; export { rootHttpRouterFactory } from './rootHttpRouterService'; export { lifecycleFactory } from './lifecycleService'; export { rootLifecycleFactory } from './rootLifecycleService'; -export type { HttpRouterFactoryOptions } from './httpRouterService'; export type { RootHttpRouterFactoryOptions } from './rootHttpRouterService'; From 6eb86a13af4a32245714e77c7e19842aafcf0f6b Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 3 Jan 2023 17:18:42 +0100 Subject: [PATCH 20/55] backend-app-api: move rootHttpRouter into subfolder Signed-off-by: Patrik Oldsberg --- .../src/services/implementations/index.ts | 3 +-- .../implementations/rootHttpRouter/index.ts | 18 ++++++++++++++++++ .../rootHttpRouterFactory.test.ts} | 2 +- .../rootHttpRouterFactory.ts} | 0 4 files changed, 20 insertions(+), 3 deletions(-) create mode 100644 packages/backend-app-api/src/services/implementations/rootHttpRouter/index.ts rename packages/backend-app-api/src/services/implementations/{rootHttpRouterService.test.ts => rootHttpRouter/rootHttpRouterFactory.test.ts} (95%) rename packages/backend-app-api/src/services/implementations/{rootHttpRouterService.ts => rootHttpRouter/rootHttpRouterFactory.ts} (100%) diff --git a/packages/backend-app-api/src/services/implementations/index.ts b/packages/backend-app-api/src/services/implementations/index.ts index de5350e72d..73c3418090 100644 --- a/packages/backend-app-api/src/services/implementations/index.ts +++ b/packages/backend-app-api/src/services/implementations/index.ts @@ -15,6 +15,7 @@ */ export * from './httpRouter'; +export * from './rootHttpRouter'; export { cacheFactory } from './cacheService'; export { configFactory } from './configService'; @@ -26,7 +27,5 @@ export { permissionsFactory } from './permissionsService'; export { schedulerFactory } from './schedulerService'; export { tokenManagerFactory } from './tokenManagerService'; export { urlReaderFactory } from './urlReaderService'; -export { rootHttpRouterFactory } from './rootHttpRouterService'; export { lifecycleFactory } from './lifecycleService'; export { rootLifecycleFactory } from './rootLifecycleService'; -export type { RootHttpRouterFactoryOptions } from './rootHttpRouterService'; diff --git a/packages/backend-app-api/src/services/implementations/rootHttpRouter/index.ts b/packages/backend-app-api/src/services/implementations/rootHttpRouter/index.ts new file mode 100644 index 0000000000..9b72f17060 --- /dev/null +++ b/packages/backend-app-api/src/services/implementations/rootHttpRouter/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 { rootHttpRouterFactory } from './rootHttpRouterFactory'; +export type { RootHttpRouterFactoryOptions } from './rootHttpRouterFactory'; diff --git a/packages/backend-app-api/src/services/implementations/rootHttpRouterService.test.ts b/packages/backend-app-api/src/services/implementations/rootHttpRouter/rootHttpRouterFactory.test.ts similarity index 95% rename from packages/backend-app-api/src/services/implementations/rootHttpRouterService.test.ts rename to packages/backend-app-api/src/services/implementations/rootHttpRouter/rootHttpRouterFactory.test.ts index fea99b0df9..67b5066fb1 100644 --- a/packages/backend-app-api/src/services/implementations/rootHttpRouterService.test.ts +++ b/packages/backend-app-api/src/services/implementations/rootHttpRouter/rootHttpRouterFactory.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { findConflictingPath } from './rootHttpRouterService'; +import { findConflictingPath } from './rootHttpRouterFactory'; describe('findConflictingPath', () => { it('finds conflicts when present', () => { diff --git a/packages/backend-app-api/src/services/implementations/rootHttpRouterService.ts b/packages/backend-app-api/src/services/implementations/rootHttpRouter/rootHttpRouterFactory.ts similarity index 100% rename from packages/backend-app-api/src/services/implementations/rootHttpRouterService.ts rename to packages/backend-app-api/src/services/implementations/rootHttpRouter/rootHttpRouterFactory.ts From b77786901197004a7bb3431be0ba6773730ebda4 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 3 Jan 2023 17:40:45 +0100 Subject: [PATCH 21/55] backend-app-api: extract RestrictedIndexedRouter out of root router Signed-off-by: Patrik Oldsberg --- .../RestrictedIndexedRouter.test.ts | 51 +++++++++++++ .../rootHttpRouter/RestrictedIndexedRouter.ts | 73 +++++++++++++++++++ .../rootHttpRouterFactory.test.ts | 31 -------- .../rootHttpRouter/rootHttpRouterFactory.ts | 54 ++------------ 4 files changed, 130 insertions(+), 79 deletions(-) create mode 100644 packages/backend-app-api/src/services/implementations/rootHttpRouter/RestrictedIndexedRouter.test.ts create mode 100644 packages/backend-app-api/src/services/implementations/rootHttpRouter/RestrictedIndexedRouter.ts delete mode 100644 packages/backend-app-api/src/services/implementations/rootHttpRouter/rootHttpRouterFactory.test.ts diff --git a/packages/backend-app-api/src/services/implementations/rootHttpRouter/RestrictedIndexedRouter.test.ts b/packages/backend-app-api/src/services/implementations/rootHttpRouter/RestrictedIndexedRouter.test.ts new file mode 100644 index 0000000000..b72e87f77b --- /dev/null +++ b/packages/backend-app-api/src/services/implementations/rootHttpRouter/RestrictedIndexedRouter.test.ts @@ -0,0 +1,51 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { RestrictedIndexedRouter } from './RestrictedIndexedRouter'; + +describe('RestrictedIndexedRouter', () => { + 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 = new RestrictedIndexedRouter(false); + 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 = new RestrictedIndexedRouter(false); + for (const path of existing) { + router.use(path, () => {}); + } + expect(() => router.use(added, () => {})).toThrow( + `Path ${added} conflicts with the existing path ${conflict}`, + ); + }, + ); +}); diff --git a/packages/backend-app-api/src/services/implementations/rootHttpRouter/RestrictedIndexedRouter.ts b/packages/backend-app-api/src/services/implementations/rootHttpRouter/RestrictedIndexedRouter.ts new file mode 100644 index 0000000000..961277f34d --- /dev/null +++ b/packages/backend-app-api/src/services/implementations/rootHttpRouter/RestrictedIndexedRouter.ts @@ -0,0 +1,73 @@ +/* + * 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'; + +function normalizePath(path: string): string { + return path.replace(/\/*$/, '/'); +} + +export class RestrictedIndexedRouter implements RootHttpRouterService { + #indexPath?: false | string; + + #router = Router(); + #namedRoutes = Router(); + #indexRouter = Router(); + #existingPaths = new Array(); + + constructor(indexPath?: false | string) { + this.#indexPath = indexPath; + this.#router.use(this.#namedRoutes); + 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/services/implementations/rootHttpRouter/rootHttpRouterFactory.test.ts b/packages/backend-app-api/src/services/implementations/rootHttpRouter/rootHttpRouterFactory.test.ts deleted file mode 100644 index 67b5066fb1..0000000000 --- a/packages/backend-app-api/src/services/implementations/rootHttpRouter/rootHttpRouterFactory.test.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* - * 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 { findConflictingPath } from './rootHttpRouterFactory'; - -describe('findConflictingPath', () => { - it('finds conflicts when present', () => { - expect(findConflictingPath(['/a'], '/a')).toBe('/a'); - expect(findConflictingPath(['/b'], '/a')).toBe(undefined); - expect(findConflictingPath(['/a'], '/a/b')).toBe('/a'); - expect(findConflictingPath(['/a'], '/aa/b')).toBe(undefined); - expect(findConflictingPath(['/aa'], '/a/b')).toBe(undefined); - expect(findConflictingPath(['/a/b'], '/a')).toBe('/a/b'); - expect(findConflictingPath(['/a/b'], '/aa')).toBe(undefined); - expect(findConflictingPath(['/b/a'], '/a')).toBe(undefined); - expect(findConflictingPath(['/a'], '/aa')).toBe(undefined); - }); -}); diff --git a/packages/backend-app-api/src/services/implementations/rootHttpRouter/rootHttpRouterFactory.ts b/packages/backend-app-api/src/services/implementations/rootHttpRouter/rootHttpRouterFactory.ts index dae7474793..ffe345f0a0 100644 --- a/packages/backend-app-api/src/services/implementations/rootHttpRouter/rootHttpRouterFactory.ts +++ b/packages/backend-app-api/src/services/implementations/rootHttpRouter/rootHttpRouterFactory.ts @@ -18,9 +18,9 @@ import { createServiceFactory, coreServices, } from '@backstage/backend-plugin-api'; -import Router from 'express-promise-router'; import { Handler } from 'express'; import { createServiceBuilder } from '@backstage/backend-common'; +import { RestrictedIndexedRouter } from './RestrictedIndexedRouter'; /** * @public @@ -45,10 +45,9 @@ export const rootHttpRouterFactory = createServiceFactory({ lifecycle: coreServices.rootLifecycle, }, async factory({ config, lifecycle }, options?: RootHttpRouterFactoryOptions) { - const indexPath = options?.indexPath ?? '/api/app'; - - const namedRouter = Router(); - const indexRouter = Router(); + const router = new RestrictedIndexedRouter( + options?.indexPath ?? '/api/app', + ); const service = createServiceBuilder(module).loadConfig(config); @@ -56,7 +55,7 @@ export const rootHttpRouterFactory = createServiceFactory({ service.addRouter('', middleware); } - service.addRouter('', namedRouter).addRouter('', indexRouter); + service.addRouter('', router.handler()); const server = await service.start(); // Stop method isn't part of the public API, let's fix that once we move the implementation here. @@ -79,47 +78,6 @@ export const rootHttpRouterFactory = createServiceFactory({ labels: { service: 'rootHttpRouter' }, }); - const existingPaths = new Array(); - - return { - use: (path: string, handler: Handler) => { - if (path.match(/^[/\s]*$/)) { - throw new Error(`Root router path may not be empty`); - } - const conflictingPath = findConflictingPath(existingPaths, path); - if (conflictingPath) { - throw new Error( - `Path ${path} conflicts with the existing path ${conflictingPath}`, - ); - } - existingPaths.push(path); - namedRouter.use(path, handler); - - if (indexPath === path) { - indexRouter.use(handler); - } - }, - }; + return router; }, }); - -function normalizePath(path: string): string { - return path.replace(/\/*$/, '/'); -} - -export function findConflictingPath( - paths: string[], - newPath: string, -): string | undefined { - const normalizedNewPath = normalizePath(newPath); - for (const path of paths) { - const normalizedPath = normalizePath(path); - if (normalizedPath.startsWith(normalizedNewPath)) { - return path; - } - if (normalizedNewPath.startsWith(normalizedPath)) { - return path; - } - } - return undefined; -} From 7695c5a44c35619e14353b8cb2cf489ff8a837dd Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 4 Jan 2023 15:09:30 +0100 Subject: [PATCH 22/55] backend-app-api: forklift sevrice factory config from backend-common Signed-off-by: Patrik Oldsberg --- packages/backend-app-api/package.json | 11 ++++++++++- .../implementations/rootHttpRouter}/config.test.ts | 0 .../implementations/rootHttpRouter}/config.ts | 0 .../implementations/rootHttpRouter}/hostFactory.ts | 0 yarn.lock | 9 +++++++++ 5 files changed, 19 insertions(+), 1 deletion(-) rename packages/{backend-common/src/service/lib => backend-app-api/src/services/implementations/rootHttpRouter}/config.test.ts (100%) rename packages/{backend-common/src/service/lib => backend-app-api/src/services/implementations/rootHttpRouter}/config.ts (100%) rename packages/{backend-common/src/service/lib => backend-app-api/src/services/implementations/rootHttpRouter}/hostFactory.ts (100%) diff --git a/packages/backend-app-api/package.json b/packages/backend-app-api/package.json index 80598ec93d..20e196c4b6 100644 --- a/packages/backend-app-api/package.json +++ b/packages/backend-app-api/package.json @@ -36,15 +36,24 @@ "@backstage/backend-common": "workspace:^", "@backstage/backend-plugin-api": "workspace:^", "@backstage/backend-tasks": "workspace:^", + "@backstage/config": "workspace:^", "@backstage/errors": "workspace:^", "@backstage/plugin-permission-node": "workspace:^", + "@types/cors": "^2.8.6", "@types/express": "^4.17.6", + "cors": "^2.8.5", "express": "^4.17.1", "express-promise-router": "^4.1.0", + "fs-extra": "10.1.0", + "minimatch": "^5.0.0", + "node-forge": "^1.3.1", + "selfsigned": "^2.0.0", "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "workspace:^" + "@backstage/cli": "workspace:^", + "@types/fs-extra": "^9.0.3", + "@types/node-forge": "^1.3.0" }, "files": [ "dist", diff --git a/packages/backend-common/src/service/lib/config.test.ts b/packages/backend-app-api/src/services/implementations/rootHttpRouter/config.test.ts similarity index 100% rename from packages/backend-common/src/service/lib/config.test.ts rename to packages/backend-app-api/src/services/implementations/rootHttpRouter/config.test.ts diff --git a/packages/backend-common/src/service/lib/config.ts b/packages/backend-app-api/src/services/implementations/rootHttpRouter/config.ts similarity index 100% rename from packages/backend-common/src/service/lib/config.ts rename to packages/backend-app-api/src/services/implementations/rootHttpRouter/config.ts diff --git a/packages/backend-common/src/service/lib/hostFactory.ts b/packages/backend-app-api/src/services/implementations/rootHttpRouter/hostFactory.ts similarity index 100% rename from packages/backend-common/src/service/lib/hostFactory.ts rename to packages/backend-app-api/src/services/implementations/rootHttpRouter/hostFactory.ts diff --git a/yarn.lock b/yarn.lock index 588d41c776..ff351c8957 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3381,11 +3381,20 @@ __metadata: "@backstage/backend-plugin-api": "workspace:^" "@backstage/backend-tasks": "workspace:^" "@backstage/cli": "workspace:^" + "@backstage/config": "workspace:^" "@backstage/errors": "workspace:^" "@backstage/plugin-permission-node": "workspace:^" + "@types/cors": ^2.8.6 "@types/express": ^4.17.6 + "@types/fs-extra": ^9.0.3 + "@types/node-forge": ^1.3.0 + cors: ^2.8.5 express: ^4.17.1 express-promise-router: ^4.1.0 + fs-extra: 10.1.0 + minimatch: ^5.0.0 + node-forge: ^1.3.1 + selfsigned: ^2.0.0 winston: ^3.2.1 languageName: unknown linkType: soft From 31f20f50297c3834da67542c19ee0c5106aaad2c Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 4 Jan 2023 15:10:47 +0100 Subject: [PATCH 23/55] backend-app-api: move http options reading to lib + refactor Signed-off-by: Patrik Oldsberg --- .../backend-app-api/src/lib/http/config.ts | 94 ++++++++++++++++++ .../backend-app-api/src/lib/http/index.ts | 18 ++++ .../backend-app-api/src/lib/http/types.ts | 46 +++++++++ .../implementations/rootHttpRouter/config.ts | 99 ------------------- 4 files changed, 158 insertions(+), 99 deletions(-) create mode 100644 packages/backend-app-api/src/lib/http/config.ts create mode 100644 packages/backend-app-api/src/lib/http/index.ts create mode 100644 packages/backend-app-api/src/lib/http/types.ts diff --git a/packages/backend-app-api/src/lib/http/config.ts b/packages/backend-app-api/src/lib/http/config.ts new file mode 100644 index 0000000000..933bab42a0 --- /dev/null +++ b/packages/backend-app-api/src/lib/http/config.ts @@ -0,0 +1,94 @@ +/* + * 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 { Config } from '@backstage/config'; +import { HttpServerOptions } from './types'; + +const DEFAULT_PORT = 7007; +const DEFAULT_HOST = ''; + +/** + * Reads {@link HttpServerOptions} from a {@link @backstage/config#Config} object. + * + * @public + * @remarks + * + * The provided configuration object should contain the `listen` and + * additional keys directly. + * + * @example + * ```ts + * const opts = readHttpServerOptions(config.getConfig('backend')); + * ``` + */ +export function readHttpServerOptions(config: Config): HttpServerOptions { + return { + listen: readHttpListenOptions(config), + https: readHttpsOptions(config), + }; +} + +function readHttpListenOptions(config: Config): HttpServerOptions['listen'] { + const listen = config.get('listen'); + if (typeof listen === 'string') { + const parts = listen.split(':'); + const port = parseInt(parts[parts.length - 1], 10); + if (!isNaN(port)) { + if (parts.length === 1) { + return { port, host: DEFAULT_HOST }; + } + if (parts.length === 2) { + return { host: parts[0], port }; + } + } + throw new Error( + `Unable to parse listen address ${listen}, expected or :`, + ); + } + + return { + port: config.getOptionalNumber('listen.port') ?? DEFAULT_PORT, + host: config.getOptionalString('listen.host') ?? DEFAULT_HOST, + }; +} + +function readHttpsOptions(config: Config): HttpServerOptions['https'] { + const https = config.getOptional('https'); + if (https === true) { + const baseUrl = config.getString('baseUrl'); + let hostname; + try { + hostname = new URL(baseUrl).hostname; + } catch (error) { + throw new Error(`Invalid baseUrl "${baseUrl}"`); + } + + return { certificate: { type: 'generated', hostname } }; + } + + const cc = config.getOptionalConfig('https'); + if (!cc) { + return undefined; + } + + return { + certificate: { + type: 'plain', + cert: cc.getString('certificate.cert'), + key: cc.getString('certificate.key'), + }, + }; +} diff --git a/packages/backend-app-api/src/lib/http/index.ts b/packages/backend-app-api/src/lib/http/index.ts new file mode 100644 index 0000000000..0717194f75 --- /dev/null +++ b/packages/backend-app-api/src/lib/http/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 { readHttpServerOptions } from './config'; +export type { HttpServerOptions, HttpServerCertificateOptions } from './types'; diff --git a/packages/backend-app-api/src/lib/http/types.ts b/packages/backend-app-api/src/lib/http/types.ts new file mode 100644 index 0000000000..a33415ec83 --- /dev/null +++ b/packages/backend-app-api/src/lib/http/types.ts @@ -0,0 +1,46 @@ +/* + * 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. + */ + +/** + * Options for starting up an HTTP server. + * + * @public + */ +export type HttpServerOptions = { + listen: { + port: number; + host: string; + }; + https?: { + certificate: HttpServerCertificateOptions; + }; +}; + +/** + * Options for configuring HTTPS for an HTTP server. + * + * @public + */ +export type HttpServerCertificateOptions = + | { + type: 'plain'; + key: string; + cert: string; + } + | { + type: 'generated'; + hostname: string; + }; diff --git a/packages/backend-app-api/src/services/implementations/rootHttpRouter/config.ts b/packages/backend-app-api/src/services/implementations/rootHttpRouter/config.ts index 93cc12b7d4..a147b67ea1 100644 --- a/packages/backend-app-api/src/services/implementations/rootHttpRouter/config.ts +++ b/packages/backend-app-api/src/services/implementations/rootHttpRouter/config.ts @@ -52,49 +52,6 @@ type CustomOrigin = ( callback: (err: Error | null, origin?: StaticOrigin) => void, ) => void; -/** - * Reads some base options out of a config object. - * - * @param config - The root of a backend config object - * @returns A base options object - * - * @example - * ```json - * { - * baseUrl: "http://localhost:7007", - * listen: "0.0.0.0:7007" - * } - * ``` - */ -export function readBaseOptions(config: Config): BaseOptions { - if (typeof config.get('listen') === 'string') { - // TODO(freben): Expand this to support more addresses and perhaps optional - const { host, port } = parseListenAddress(config.getString('listen')); - - return removeUnknown({ - listenPort: port, - listenHost: host, - }); - } - - const port = config.getOptional('listen.port'); - if ( - typeof port !== 'undefined' && - typeof port !== 'number' && - typeof port !== 'string' - ) { - throw new Error( - `Invalid type in config for key 'backend.listen.port', got ${typeof port}, wanted string or number`, - ); - } - - return removeUnknown({ - listenPort: port, - listenHost: config.getOptionalString('listen.host'), - baseUrl: config.getOptionalString('baseUrl'), - }); -} - /** * Attempts to read a CORS options object from the root of a config object. * @@ -165,49 +122,6 @@ export function readCspOptions( return result; } -/** - * Attempts to read a https settings object from the root of a config object. - * - * @param config - The root of a backend config object - * @returns A https settings object, or undefined if not specified - * - * @example - * ```json - * { - * https: { - * certificate: ... - * } - * } - * ``` - */ -export function readHttpsSettings(config: Config): HttpsSettings | undefined { - const https = config.getOptional('https'); - if (https === true) { - const baseUrl = config.getString('baseUrl'); - let hostname; - try { - hostname = new URL(baseUrl).hostname; - } catch (error) { - throw new Error(`Invalid backend.baseUrl "${baseUrl}"`); - } - - return { certificate: { hostname } }; - } - - const cc = config.getOptionalConfig('https'); - if (!cc) { - return undefined; - } - - const certificateConfig = cc.get('certificate'); - - const cfg = { - certificate: certificateConfig, - }; - - return removeUnknown(cfg as HttpsSettings); -} - function getOptionalStringOrStrings( config: Config, key: string, @@ -269,16 +183,3 @@ function removeUnknown(obj: T): T { Object.entries(obj).filter(([, v]) => v !== undefined), ) as T; } - -function parseListenAddress(value: string): { host?: string; port?: number } { - const parts = value.split(':'); - if (parts.length === 1) { - return { port: parseInt(parts[0], 10) }; - } - if (parts.length === 2) { - return { host: parts[0], port: parseInt(parts[1], 10) }; - } - throw new Error( - `Unable to parse listen address ${value}, expected or :`, - ); -} From 64f40724d5e9a9caf084d846da8236eeab226b56 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 4 Jan 2023 16:14:59 +0100 Subject: [PATCH 24/55] backend-app-api: tests and fixes for readHttpServerOptions Signed-off-by: Patrik Oldsberg --- .../src/lib/http/config.test.ts | 81 +++++++++++++++++++ .../backend-app-api/src/lib/http/config.ts | 13 ++- 2 files changed, 91 insertions(+), 3 deletions(-) create mode 100644 packages/backend-app-api/src/lib/http/config.test.ts diff --git a/packages/backend-app-api/src/lib/http/config.test.ts b/packages/backend-app-api/src/lib/http/config.test.ts new file mode 100644 index 0000000000..3ab5eda5e9 --- /dev/null +++ b/packages/backend-app-api/src/lib/http/config.test.ts @@ -0,0 +1,81 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ConfigReader } from '@backstage/config'; +import { readHttpServerOptions } from './config'; + +describe('readHttpServerOptions', () => { + it.each([ + [{}, { listen: { host: '', port: 7007 } }], + [{ listen: ':80' }, { listen: { host: '', port: 80 } }], + [{ listen: '80' }, { listen: { host: '', port: 80 } }], + [{ listen: '1.2.3.4:80' }, { listen: { host: '1.2.3.4', port: 80 } }], + [{ listen: { host: '' } }, { listen: { host: '', port: 7007 } }], + [ + { listen: { host: '0.0.0.0' } }, + { listen: { host: '0.0.0.0', port: 7007 } }, + ], + [ + { listen: { host: '0.0.0.0', port: '80' } }, + { listen: { host: '0.0.0.0', port: 80 } }, + ], + [{ listen: { port: '80' } }, { listen: { host: '', port: 80 } }], + [{ listen: { port: 80 } }, { listen: { host: '', port: 80 } }], + [{ listen: { port: 80 } }, { listen: { host: '', port: 80 } }], + [ + { baseUrl: 'http://example.com:8080', https: true }, + { + listen: { host: '', port: 7007 }, + https: { certificate: { type: 'generated', hostname: 'example.com' } }, + }, + ], + [ + { https: { certificate: { cert: 'my-cert', key: 'my-key' } } }, + { + listen: { host: '', port: 7007 }, + https: { + certificate: { type: 'plain', cert: 'my-cert', key: 'my-key' }, + }, + }, + ], + ])('should read http server options %#', (input, output) => { + expect(readHttpServerOptions(new ConfigReader(input))).toEqual(output); + }); + + it.each([ + [ + { listen: { port: 'not-a-number' } }, + "Unable to convert config value for key 'listen.port' in 'mock-config' to a number", + ], + [ + { listen: { port: {} } }, + "Invalid type in config for key 'listen.port' in 'mock-config', got object, wanted number", + ], + [ + { listen: { host: false } }, + "Invalid type in config for key 'listen.host' in 'mock-config', got boolean, wanted string", + ], + [{ https: {} }, "Missing required config value at 'https.certificate.cert"], + [ + { https: { certificate: { cert: 'x' } } }, + "Missing required config value at 'https.certificate.key", + ], + ])('should throw on bad options %#', (input, message) => { + expect(() => readHttpServerOptions(new ConfigReader(input))).toThrow( + message, + ); + }); +}); diff --git a/packages/backend-app-api/src/lib/http/config.ts b/packages/backend-app-api/src/lib/http/config.ts index 933bab42a0..3c5acf0ffd 100644 --- a/packages/backend-app-api/src/lib/http/config.ts +++ b/packages/backend-app-api/src/lib/http/config.ts @@ -42,9 +42,9 @@ export function readHttpServerOptions(config: Config): HttpServerOptions { } function readHttpListenOptions(config: Config): HttpServerOptions['listen'] { - const listen = config.get('listen'); + const listen = config.getOptional('listen'); if (typeof listen === 'string') { - const parts = listen.split(':'); + const parts = String(listen).split(':'); const port = parseInt(parts[parts.length - 1], 10); if (!isNaN(port)) { if (parts.length === 1) { @@ -59,9 +59,16 @@ function readHttpListenOptions(config: Config): HttpServerOptions['listen'] { ); } + // Workaround to allow empty string + const host = config.getOptional('listen.host') ?? DEFAULT_HOST; + if (typeof host !== 'string') { + config.getOptionalString('listen.host'); // will throw + throw new Error('unreachable'); + } + return { port: config.getOptionalNumber('listen.port') ?? DEFAULT_PORT, - host: config.getOptionalString('listen.host') ?? DEFAULT_HOST, + host, }; } From 950e0392aac27b5d4a31317cea99fe2157b5c8ca Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 4 Jan 2023 17:52:38 +0100 Subject: [PATCH 25/55] backend-app-api: refactor hostFactory into createHttpServer Signed-off-by: Patrik Oldsberg --- packages/backend-app-api/package.json | 4 +- .../src/lib/http/createHttpServer.ts | 98 +++++++++++++++++ .../http/getGeneratedCertificate.ts} | 102 ++++-------------- .../backend-app-api/src/lib/http/types.ts | 15 +++ yarn.lock | 2 + 5 files changed, 137 insertions(+), 84 deletions(-) create mode 100644 packages/backend-app-api/src/lib/http/createHttpServer.ts rename packages/backend-app-api/src/{services/implementations/rootHttpRouter/hostFactory.ts => lib/http/getGeneratedCertificate.ts} (55%) diff --git a/packages/backend-app-api/package.json b/packages/backend-app-api/package.json index 20e196c4b6..20c723bd51 100644 --- a/packages/backend-app-api/package.json +++ b/packages/backend-app-api/package.json @@ -48,12 +48,14 @@ "minimatch": "^5.0.0", "node-forge": "^1.3.1", "selfsigned": "^2.0.0", + "stoppable": "^1.1.0", "winston": "^3.2.1" }, "devDependencies": { "@backstage/cli": "workspace:^", "@types/fs-extra": "^9.0.3", - "@types/node-forge": "^1.3.0" + "@types/node-forge": "^1.3.0", + "@types/stoppable": "^1.1.0" }, "files": [ "dist", diff --git a/packages/backend-app-api/src/lib/http/createHttpServer.ts b/packages/backend-app-api/src/lib/http/createHttpServer.ts new file mode 100644 index 0000000000..f72269c079 --- /dev/null +++ b/packages/backend-app-api/src/lib/http/createHttpServer.ts @@ -0,0 +1,98 @@ +/* + * 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 * as http from 'http'; +import * as https from 'https'; +import stoppableServer from 'stoppable'; +import { RequestListener } from 'http'; +import { LoggerService } from '@backstage/backend-plugin-api'; +import { HttpServerOptions, ExtendedHttpServer } from './types'; +import { getGeneratedCertificate } from './getGeneratedCertificate'; + +/** + * Creates a Node.js HTTP or HTTPS server instance. + * + * @public + */ +export async function createHttpServer( + listener: RequestListener, + options: HttpServerOptions, + deps: { logger: LoggerService }, +): Promise { + const server = await createServer(listener, options, deps); + + const stopper = stoppableServer(server, 0); + + return Object.assign(server, { + start() { + return new Promise((resolve, reject) => { + const handleStartupError = (error: Error) => { + server.close(); + reject(error); + }; + + server.on('error', handleStartupError); + + const { host, port } = options.listen; + server.listen(port, host, () => { + server.off('error', handleStartupError); + deps.logger.info(`Listening on ${host}:${port}`); + resolve(); + }); + }); + }, + + stop() { + return new Promise((resolve, reject) => { + stopper.stop((error?: Error) => { + if (error) { + reject(error); + } else { + resolve(); + } + }); + }); + }, + + port() { + const address = server.address(); + if (typeof address === 'string' || address === null) { + throw new Error(`Unexpected server address '${address}'`); + } + return address.port; + }, + }); +} + +async function createServer( + listener: RequestListener, + options: HttpServerOptions, + deps: { logger: LoggerService }, +): Promise { + if (options.https) { + const { certificate } = options.https; + if (certificate.type === 'generated') { + const credentials = await getGeneratedCertificate( + certificate.hostname, + deps.logger, + ); + return https.createServer(credentials, listener); + } + return https.createServer(certificate, listener); + } + + return http.createServer(listener); +} diff --git a/packages/backend-app-api/src/services/implementations/rootHttpRouter/hostFactory.ts b/packages/backend-app-api/src/lib/http/getGeneratedCertificate.ts similarity index 55% rename from packages/backend-app-api/src/services/implementations/rootHttpRouter/hostFactory.ts rename to packages/backend-app-api/src/lib/http/getGeneratedCertificate.ts index db4343cae5..b5cd420aff 100644 --- a/packages/backend-app-api/src/services/implementations/rootHttpRouter/hostFactory.ts +++ b/packages/backend-app-api/src/lib/http/getGeneratedCertificate.ts @@ -16,86 +16,16 @@ import fs from 'fs-extra'; import { resolve as resolvePath, dirname } from 'path'; -import express from 'express'; -import * as http from 'http'; -import * as https from 'https'; import { LoggerService } from '@backstage/backend-plugin-api'; -import { HttpsSettings } from './config'; import forge from 'node-forge'; const FIVE_DAYS_IN_MS = 5 * 24 * 60 * 60 * 1000; const IP_HOSTNAME_REGEX = /:|^\d+\.\d+\.\d+\.\d+$/; -/** - * Creates a Http server instance based on an Express application. - * - * @param app - The Express application object - * @param logger - Optional Winston logger object - * @returns A Http server instance - * - */ -export function createHttpServer( - app: express.Express, - logger?: LoggerService, -): http.Server { - logger?.info('Initializing http server'); - - return http.createServer(app); -} - -/** - * Creates a Https server instance based on an Express application. - * - * @param app - The Express application object - * @param httpsSettings - HttpsSettings for self-signed certificate generation - * @param logger - Optional Winston logger object - * @returns A Https server instance - * - */ -export async function createHttpsServer( - app: express.Express, - httpsSettings: HttpsSettings, - logger?: LoggerService, -): Promise { - logger?.info('Initializing https server'); - - let credentials: { key: string | Buffer; cert: string | Buffer }; - - if ('hostname' in httpsSettings?.certificate) { - credentials = await getGeneratedCertificate( - httpsSettings.certificate.hostname, - logger, - ); - } else { - logger?.info('Loading certificate from config'); - - credentials = { - key: httpsSettings?.certificate?.key, - cert: httpsSettings?.certificate?.cert, - }; - } - - if (!credentials.key || !credentials.cert) { - throw new Error('Invalid HTTPS credentials'); - } - - return https.createServer(credentials, app) as http.Server; -} - -function getCertificateExpiration(cert: string, logger?: LoggerService) { - try { - const crt = forge.pki.certificateFromPem(cert); - return crt.validity.notAfter.getTime() - Date.now(); - } catch (error) { - logger?.warn(`Unable to parse self-signed certificate. ${error}`); - return 0; - } -} - -async function getGeneratedCertificate( +export async function getGeneratedCertificate( hostname: string, - logger?: LoggerService, + logger: LoggerService, ) { const hasModules = await fs.pathExists('node_modules'); let certPath; @@ -109,24 +39,30 @@ async function getGeneratedCertificate( } if (await fs.pathExists(certPath)) { - const cert = await fs.readFile(certPath); - const remainingMs = getCertificateExpiration(cert.toString(), logger); - if (remainingMs > FIVE_DAYS_IN_MS) { - logger?.info('Using existing self-signed certificate'); - return { - key: cert, - cert, - }; + try { + const cert = await fs.readFile(certPath); + + const crt = forge.pki.certificateFromPem(cert.toString()); + const remainingMs = crt.validity.notAfter.getTime() - Date.now(); + if (remainingMs > FIVE_DAYS_IN_MS) { + logger.info('Using existing self-signed certificate'); + return { + key: cert, + cert, + }; + } + } catch (error) { + logger.warn(`Unable to use existing self-signed certificate, ${error}`); } } - logger?.info('Generating new self-signed certificate'); - const newCert = await createCertificate(hostname); + logger.info('Generating new self-signed certificate'); + const newCert = await generateCertificate(hostname); await fs.writeFile(certPath, newCert.cert + newCert.key, 'utf8'); return newCert; } -async function createCertificate(hostname: string) { +async function generateCertificate(hostname: string) { const attributes = [ { name: 'commonName', diff --git a/packages/backend-app-api/src/lib/http/types.ts b/packages/backend-app-api/src/lib/http/types.ts index a33415ec83..ff96f69bff 100644 --- a/packages/backend-app-api/src/lib/http/types.ts +++ b/packages/backend-app-api/src/lib/http/types.ts @@ -14,6 +14,21 @@ * limitations under the License. */ +import * as http from 'http'; + +/** + * An HTTP server extended with utility methods. + * + * @public + */ +export interface ExtendedHttpServer extends http.Server { + start(): Promise; + + stop(): Promise; + + port(): number; +} + /** * Options for starting up an HTTP server. * diff --git a/yarn.lock b/yarn.lock index ff351c8957..53e68b28f8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3388,6 +3388,7 @@ __metadata: "@types/express": ^4.17.6 "@types/fs-extra": ^9.0.3 "@types/node-forge": ^1.3.0 + "@types/stoppable": ^1.1.0 cors: ^2.8.5 express: ^4.17.1 express-promise-router: ^4.1.0 @@ -3395,6 +3396,7 @@ __metadata: minimatch: ^5.0.0 node-forge: ^1.3.1 selfsigned: ^2.0.0 + stoppable: ^1.1.0 winston: ^3.2.1 languageName: unknown linkType: soft From 1fed5327c37827106eab88ee76855113b8d329e7 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 5 Jan 2023 01:08:34 +0100 Subject: [PATCH 26/55] backend-app-api: split out readCorsOptions Signed-off-by: Patrik Oldsberg --- .../implementations/rootHttpRouter/config.ts | 105 ------------------ .../rootHttpRouter/readCorsOptions.test.ts | 81 ++++++++++++++ .../rootHttpRouter/readCorsOptions.ts | 81 ++++++++++++++ 3 files changed, 162 insertions(+), 105 deletions(-) create mode 100644 packages/backend-app-api/src/services/implementations/rootHttpRouter/readCorsOptions.test.ts create mode 100644 packages/backend-app-api/src/services/implementations/rootHttpRouter/readCorsOptions.ts diff --git a/packages/backend-app-api/src/services/implementations/rootHttpRouter/config.ts b/packages/backend-app-api/src/services/implementations/rootHttpRouter/config.ts index a147b67ea1..931fb48a63 100644 --- a/packages/backend-app-api/src/services/implementations/rootHttpRouter/config.ts +++ b/packages/backend-app-api/src/services/implementations/rootHttpRouter/config.ts @@ -15,8 +15,6 @@ */ import { Config } from '@backstage/config'; -import { CorsOptions } from 'cors'; -import { Minimatch } from 'minimatch'; export type BaseOptions = { listenPort?: string | number; @@ -45,47 +43,6 @@ export type CertificateAttributes = { */ export type CspOptions = Record; -type StaticOrigin = boolean | string | RegExp | (boolean | string | RegExp)[]; - -type CustomOrigin = ( - requestOrigin: string | undefined, - callback: (err: Error | null, origin?: StaticOrigin) => void, -) => void; - -/** - * Attempts to read a CORS options object from the root of a config object. - * - * @param config - The root of a backend config object - * @returns A CORS options object, or undefined if not specified - * - * @example - * ```json - * { - * cors: { - * origin: "http://localhost:3000", - * credentials: true - * } - * } - * ``` - */ -export function readCorsOptions(config: Config): CorsOptions | undefined { - const cc = config.getOptionalConfig('cors'); - if (!cc) { - return undefined; - } - - return removeUnknown({ - origin: createCorsOriginMatcher(getOptionalStringOrStrings(cc, 'origin')), - methods: getOptionalStringOrStrings(cc, 'methods'), - allowedHeaders: getOptionalStringOrStrings(cc, 'allowedHeaders'), - exposedHeaders: getOptionalStringOrStrings(cc, 'exposedHeaders'), - credentials: cc.getOptionalBoolean('credentials'), - maxAge: cc.getOptionalNumber('maxAge'), - preflightContinue: cc.getOptionalBoolean('preflightContinue'), - optionsSuccessStatus: cc.getOptionalNumber('optionsSuccessStatus'), - }); -} - /** * Attempts to read a CSP options object from the root of a config object. * @@ -121,65 +78,3 @@ export function readCspOptions( return result; } - -function getOptionalStringOrStrings( - config: Config, - key: string, -): string | string[] | undefined { - const value = config.getOptional(key); - if (value === undefined || isStringOrStrings(value)) { - return value; - } - throw new Error(`Expected string or array of strings, got ${typeof value}`); -} - -function createCorsOriginMatcher( - originValue: string | string[] | undefined, -): CustomOrigin | undefined { - if (originValue === undefined) { - return originValue; - } - - if (!isStringOrStrings(originValue)) { - throw new Error( - `Expected string or array of strings, got ${typeof originValue}`, - ); - } - - const allowedOrigin = - typeof originValue === 'string' ? [originValue] : originValue; - - const allowedOriginPatterns = - allowedOrigin?.map( - pattern => new Minimatch(pattern, { nocase: true, noglobstar: true }), - ) ?? []; - - return (origin, callback) => { - return callback( - null, - allowedOriginPatterns.some(pattern => pattern.match(origin ?? '')), - ); - }; -} - -function isStringOrStrings(value: any): value is string | string[] { - return typeof value === 'string' || isStringArray(value); -} - -function isStringArray(value: any): value is string[] { - if (!Array.isArray(value)) { - return false; - } - for (const v of value) { - if (typeof v !== 'string') { - return false; - } - } - return true; -} - -function removeUnknown(obj: T): T { - return Object.fromEntries( - Object.entries(obj).filter(([, v]) => v !== undefined), - ) as T; -} diff --git a/packages/backend-app-api/src/services/implementations/rootHttpRouter/readCorsOptions.test.ts b/packages/backend-app-api/src/services/implementations/rootHttpRouter/readCorsOptions.test.ts new file mode 100644 index 0000000000..1c70d020c4 --- /dev/null +++ b/packages/backend-app-api/src/services/implementations/rootHttpRouter/readCorsOptions.test.ts @@ -0,0 +1,81 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ConfigReader } from '@backstage/config'; +import { readCorsOptions } from './readCorsOptions'; + +describe('readCorsOptions', () => { + it('reads single string', () => { + const mockCallback = jest.fn(); + const config = new ConfigReader({ cors: { origin: 'https://*.value*' } }); + const cors = readCorsOptions(config); + expect(cors).toEqual( + expect.objectContaining({ + origin: expect.any(Function), + }), + ); + const origin = cors?.origin as Function; + origin('https://a.value', mockCallback); // valid origin + origin('http://a.value', mockCallback); // invalid origin + origin(undefined, mockCallback); // when not origin needs to reject the call + + expect(mockCallback.mock.calls[0][0]).toBe(null); + expect(mockCallback.mock.calls[1][0]).toBe(null); + + expect(mockCallback.mock.calls[0][1]).toBe(true); + expect(mockCallback.mock.calls[1][1]).toBe(false); + expect(mockCallback.mock.calls[2][1]).toBe(false); + }); + + it('reads string array', () => { + const mockCallback = jest.fn(); + const config = new ConfigReader({ + cors: { + origin: ['http?(s)://*.value?(-+([0-9])).com', 'http://*.value'], + }, + }); + const cors = readCorsOptions(config); + expect(cors).toEqual( + expect.objectContaining({ + origin: expect.any(Function), + }), + ); + const origin = cors?.origin as Function; + origin('https://a.b.c.value-9.com', mockCallback); + origin('http://a.value-999.com', mockCallback); + origin('http://a.value', mockCallback); + origin('http://a.valuex', mockCallback); + + expect(mockCallback.mock.calls[0][0]).toBe(null); + expect(mockCallback.mock.calls[1][0]).toBe(null); + expect(mockCallback.mock.calls[2][0]).toBe(null); + expect(mockCallback.mock.calls[3][0]).toBe(null); + + expect(mockCallback.mock.calls[0][1]).toBe(true); + expect(mockCallback.mock.calls[1][1]).toBe(true); + expect(mockCallback.mock.calls[2][1]).toBe(true); + expect(mockCallback.mock.calls[3][1]).toBe(false); + }); + + it('reads undefined origin', () => { + const config = new ConfigReader({ + cors: {}, + }); + const cors = readCorsOptions(config); + expect(cors).toEqual(expect.objectContaining({})); + expect(cors?.origin).toBeUndefined(); + }); +}); diff --git a/packages/backend-app-api/src/services/implementations/rootHttpRouter/readCorsOptions.ts b/packages/backend-app-api/src/services/implementations/rootHttpRouter/readCorsOptions.ts new file mode 100644 index 0000000000..17c3157ebd --- /dev/null +++ b/packages/backend-app-api/src/services/implementations/rootHttpRouter/readCorsOptions.ts @@ -0,0 +1,81 @@ +/* + * 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 { Config } from '@backstage/config'; +import { CorsOptions } from 'cors'; +import { Minimatch } from 'minimatch'; + +/** + * Attempts to read a CORS options object from the backend configuration object. + * + * @param config - The backend configuration object. + * @returns A CORS options object, or undefined if no cors configuration is present. + * + * @example + * ```ts + * const corsOptions = readCorsOptions(config.getConfig('backend')); + * ``` + */ +export function readCorsOptions(config: Config): CorsOptions | undefined { + const cc = config.getOptionalConfig('cors'); + if (!cc) { + return undefined; + } + + return { + origin: createCorsOriginMatcher(readStringArray(cc, 'origin')), + methods: readStringArray(cc, 'methods'), + allowedHeaders: readStringArray(cc, 'allowedHeaders'), + exposedHeaders: readStringArray(cc, 'exposedHeaders'), + credentials: cc.getOptionalBoolean('credentials'), + maxAge: cc.getOptionalNumber('maxAge'), + preflightContinue: cc.getOptionalBoolean('preflightContinue'), + optionsSuccessStatus: cc.getOptionalNumber('optionsSuccessStatus'), + }; +} + +function readStringArray(config: Config, key: string): string[] | undefined { + const value = config.getOptional(key); + if (typeof value === 'string') { + return [value]; + } else if (!value) { + return undefined; + } + return config.getStringArray(key); +} + +function createCorsOriginMatcher(allowedOriginPatterns: string[] | undefined) { + if (!allowedOriginPatterns) { + return undefined; + } + + const allowedOriginMatchers = allowedOriginPatterns.map( + pattern => new Minimatch(pattern, { nocase: true, noglobstar: true }), + ); + + return ( + origin: string | undefined, + callback: ( + err: Error | null, + origin: boolean | string | RegExp | (boolean | string | RegExp)[], + ) => void, + ) => { + return callback( + null, + allowedOriginMatchers.some(pattern => pattern.match(origin ?? '')), + ); + }; +} From 4097435eeb483e379c347bba4c29b6298f158c24 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 5 Jan 2023 11:22:54 +0100 Subject: [PATCH 27/55] backend-app-api: split out readHelmetOptions Signed-off-by: Patrik Oldsberg --- packages/backend-app-api/package.json | 1 + .../rootHttpRouter/config.test.ts | 108 ------------------ .../implementations/rootHttpRouter/config.ts | 80 ------------- .../rootHttpRouter/readHelmetOptions.test.ts | 57 +++++++++ .../rootHttpRouter/readHelmetOptions.ts | 108 ++++++++++++++++++ yarn.lock | 1 + 6 files changed, 167 insertions(+), 188 deletions(-) delete mode 100644 packages/backend-app-api/src/services/implementations/rootHttpRouter/config.test.ts delete mode 100644 packages/backend-app-api/src/services/implementations/rootHttpRouter/config.ts create mode 100644 packages/backend-app-api/src/services/implementations/rootHttpRouter/readHelmetOptions.test.ts create mode 100644 packages/backend-app-api/src/services/implementations/rootHttpRouter/readHelmetOptions.ts diff --git a/packages/backend-app-api/package.json b/packages/backend-app-api/package.json index 20c723bd51..7d56da10f1 100644 --- a/packages/backend-app-api/package.json +++ b/packages/backend-app-api/package.json @@ -45,6 +45,7 @@ "express": "^4.17.1", "express-promise-router": "^4.1.0", "fs-extra": "10.1.0", + "helmet": "^6.0.0", "minimatch": "^5.0.0", "node-forge": "^1.3.1", "selfsigned": "^2.0.0", diff --git a/packages/backend-app-api/src/services/implementations/rootHttpRouter/config.test.ts b/packages/backend-app-api/src/services/implementations/rootHttpRouter/config.test.ts deleted file mode 100644 index b80d475517..0000000000 --- a/packages/backend-app-api/src/services/implementations/rootHttpRouter/config.test.ts +++ /dev/null @@ -1,108 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { ConfigReader } from '@backstage/config'; -import { readCorsOptions, readCspOptions } from './config'; - -describe('config', () => { - describe('readCspOptions', () => { - it('reads valid values', () => { - const config = new ConfigReader({ csp: { key: ['value'] } }); - expect(readCspOptions(config)).toEqual( - expect.objectContaining({ - key: ['value'], - }), - ); - }); - - it('accepts false', () => { - const config = new ConfigReader({ csp: { key: false } }); - expect(readCspOptions(config)).toEqual( - expect.objectContaining({ - key: false, - }), - ); - }); - - it('rejects invalid value types', () => { - const config = new ConfigReader({ csp: { key: [4] } }); - expect(() => readCspOptions(config)).toThrow(/wanted string-array/); - }); - }); - - describe('readCorsOptions', () => { - it('reads single string', () => { - const mockCallback = jest.fn(); - const config = new ConfigReader({ cors: { origin: 'https://*.value*' } }); - const cors = readCorsOptions(config); - expect(cors).toEqual( - expect.objectContaining({ - origin: expect.any(Function), - }), - ); - const origin = cors?.origin as Function; - origin('https://a.value', mockCallback); // valid origin - origin('http://a.value', mockCallback); // invalid origin - origin(undefined, mockCallback); // when not origin needs to reject the call - - expect(mockCallback.mock.calls[0][0]).toBe(null); - expect(mockCallback.mock.calls[1][0]).toBe(null); - - expect(mockCallback.mock.calls[0][1]).toBe(true); - expect(mockCallback.mock.calls[1][1]).toBe(false); - expect(mockCallback.mock.calls[2][1]).toBe(false); - }); - - it('reads string array', () => { - const mockCallback = jest.fn(); - const config = new ConfigReader({ - cors: { - origin: ['http?(s)://*.value?(-+([0-9])).com', 'http://*.value'], - }, - }); - const cors = readCorsOptions(config); - expect(cors).toEqual( - expect.objectContaining({ - origin: expect.any(Function), - }), - ); - const origin = cors?.origin as Function; - origin('https://a.b.c.value-9.com', mockCallback); - origin('http://a.value-999.com', mockCallback); - origin('http://a.value', mockCallback); - origin('http://a.valuex', mockCallback); - - expect(mockCallback.mock.calls[0][0]).toBe(null); - expect(mockCallback.mock.calls[1][0]).toBe(null); - expect(mockCallback.mock.calls[2][0]).toBe(null); - expect(mockCallback.mock.calls[3][0]).toBe(null); - - expect(mockCallback.mock.calls[0][1]).toBe(true); - expect(mockCallback.mock.calls[1][1]).toBe(true); - expect(mockCallback.mock.calls[2][1]).toBe(true); - expect(mockCallback.mock.calls[3][1]).toBe(false); - }); - - it('reads undefined origin', () => { - const config = new ConfigReader({ - cors: {}, - }); - const cors = readCorsOptions(config); - expect(cors).toEqual(expect.objectContaining({})); - expect(cors?.origin).toBeUndefined(); - }); - }); -}); diff --git a/packages/backend-app-api/src/services/implementations/rootHttpRouter/config.ts b/packages/backend-app-api/src/services/implementations/rootHttpRouter/config.ts deleted file mode 100644 index 931fb48a63..0000000000 --- a/packages/backend-app-api/src/services/implementations/rootHttpRouter/config.ts +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { Config } from '@backstage/config'; - -export type BaseOptions = { - listenPort?: string | number; - listenHost?: string; -}; - -export type HttpsSettings = { - certificate: CertificateGenerationOptions | CertificateReferenceOptions; -}; - -export type CertificateReferenceOptions = { - key: string; - cert: string; -}; - -export type CertificateGenerationOptions = { - hostname: string; -}; - -export type CertificateAttributes = { - commonName: string; -}; - -/** - * A map from CSP directive names to their values. - */ -export type CspOptions = Record; - -/** - * Attempts to read a CSP options object from the root of a config object. - * - * @param config - The root of a backend config object - * @returns A CSP options object, or undefined if not specified. Values can be - * false as well, which means to remove the default behavior for that - * key. - * - * @example - * ```yaml - * backend: - * csp: - * connect-src: ["'self'", 'http:', 'https:'] - * upgrade-insecure-requests: false - * ``` - */ -export function readCspOptions( - config: Config, -): Record | undefined { - const cc = config.getOptionalConfig('csp'); - if (!cc) { - return undefined; - } - - const result: Record = {}; - for (const key of cc.keys()) { - if (cc.get(key) === false) { - result[key] = false; - } else { - result[key] = cc.getStringArray(key); - } - } - - return result; -} diff --git a/packages/backend-app-api/src/services/implementations/rootHttpRouter/readHelmetOptions.test.ts b/packages/backend-app-api/src/services/implementations/rootHttpRouter/readHelmetOptions.test.ts new file mode 100644 index 0000000000..d5e0e29264 --- /dev/null +++ b/packages/backend-app-api/src/services/implementations/rootHttpRouter/readHelmetOptions.test.ts @@ -0,0 +1,57 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ConfigReader } from '@backstage/config'; +import { readHelmetOptions } from './readHelmetOptions'; + +describe('readHelmetOptions', () => { + it('should add additional directives', () => { + const config = new ConfigReader({ + csp: { + key: ['value'], + 'img-src': false, + 'script-src-attr': ['custom'], + }, + }); + expect(readHelmetOptions(config)).toEqual({ + contentSecurityPolicy: { + useDefaults: false, + directives: { + 'default-src': ["'self'"], + 'base-uri': ["'self'"], + 'font-src': ["'self'", 'https:', 'data:'], + 'frame-ancestors': ["'self'"], + // 'img-src': ["'self'", 'data:'], + 'object-src': ["'none'"], + 'script-src': ["'self'", "'unsafe-eval'"], + 'style-src': ["'self'", 'https:', "'unsafe-inline'"], + 'script-src-attr': ['custom'], + 'upgrade-insecure-requests': [], + key: ['value'], + }, + }, + crossOriginEmbedderPolicy: false, + crossOriginOpenerPolicy: false, + crossOriginResourcePolicy: false, + originAgentCluster: false, + }); + }); + + it('rejects invalid value types', () => { + const config = new ConfigReader({ csp: { key: [4] } }); + expect(() => readHelmetOptions(config)).toThrow(/wanted string-array/); + }); +}); diff --git a/packages/backend-app-api/src/services/implementations/rootHttpRouter/readHelmetOptions.ts b/packages/backend-app-api/src/services/implementations/rootHttpRouter/readHelmetOptions.ts new file mode 100644 index 0000000000..e86997d685 --- /dev/null +++ b/packages/backend-app-api/src/services/implementations/rootHttpRouter/readHelmetOptions.ts @@ -0,0 +1,108 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Config } from '@backstage/config'; +import helmet from 'helmet'; +import { HelmetOptions } from 'helmet'; +import { ContentSecurityPolicyOptions } from 'helmet/dist/types/middlewares/content-security-policy'; + +/** + * Attempts to read Helmet options from the backend configuration object. + * + * @param config - The backend configuration object. + * @returns A Helmet options object, or undefined if no Helmet configuration is present. + * + * @example + * ```ts + * const helmetOptions = readHelmetOptions(config.getConfig('backend')); + * ``` + */ +export function readHelmetOptions(config: Config): HelmetOptions { + const cspOptions = readCspDirectives(config); + return { + contentSecurityPolicy: { + useDefaults: false, + directives: applyCspDirectives(cspOptions), + }, + // These are all disabled in order to maintain backwards compatibility + // when bumping helmet v5. We can't enable these by default because + // there is no way for users to configure them. + // TODO(Rugvip): We should give control of this setup to consumers + crossOriginEmbedderPolicy: false, + crossOriginOpenerPolicy: false, + crossOriginResourcePolicy: false, + originAgentCluster: false, + }; +} + +type CspDirectives = Record | undefined; + +/** + * Attempts to read a CSP directives from the backend configuration object. + * + * @example + * ```yaml + * backend: + * csp: + * connect-src: ["'self'", 'http:', 'https:'] + * upgrade-insecure-requests: false + * ``` + */ +function readCspDirectives(config: Config): CspDirectives { + const cc = config.getOptionalConfig('csp'); + if (!cc) { + return undefined; + } + + const result: Record = {}; + for (const key of cc.keys()) { + if (cc.get(key) === false) { + result[key] = false; + } else { + result[key] = cc.getStringArray(key); + } + } + + return result; +} + +export function applyCspDirectives( + directives: CspDirectives, +): ContentSecurityPolicyOptions['directives'] { + const result: ContentSecurityPolicyOptions['directives'] = + helmet.contentSecurityPolicy.getDefaultDirectives(); + + // TODO(Rugvip): We currently use non-precompiled AJV for validation in the frontend, which uses eval. + // It should be replaced by any other solution that doesn't require unsafe-eval. + result['script-src'] = ["'self'", "'unsafe-eval'"]; + + // TODO(Rugvip): This is removed so that we maintained backwards compatibility + // when bumping to helmet v5, we could remove this as well as + // skip setting `useDefaults: false` in the future. + delete result['form-action']; + + if (directives) { + for (const [key, value] of Object.entries(directives)) { + if (value === false) { + delete result[key]; + } else { + result[key] = value; + } + } + } + + return result; +} diff --git a/yarn.lock b/yarn.lock index 53e68b28f8..0179e179e2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3393,6 +3393,7 @@ __metadata: express: ^4.17.1 express-promise-router: ^4.1.0 fs-extra: 10.1.0 + helmet: ^6.0.0 minimatch: ^5.0.0 node-forge: ^1.3.1 selfsigned: ^2.0.0 From e8d2de592dd933195be0508c3f3fab87d035c833 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 5 Jan 2023 13:33:13 +0100 Subject: [PATCH 28/55] backend-app-api: readCorsOptions now disables cors by default Signed-off-by: Patrik Oldsberg --- .../implementations/rootHttpRouter/readCorsOptions.test.ts | 6 ++++++ .../implementations/rootHttpRouter/readCorsOptions.ts | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/backend-app-api/src/services/implementations/rootHttpRouter/readCorsOptions.test.ts b/packages/backend-app-api/src/services/implementations/rootHttpRouter/readCorsOptions.test.ts index 1c70d020c4..113b2a8ee6 100644 --- a/packages/backend-app-api/src/services/implementations/rootHttpRouter/readCorsOptions.test.ts +++ b/packages/backend-app-api/src/services/implementations/rootHttpRouter/readCorsOptions.test.ts @@ -18,6 +18,12 @@ import { ConfigReader } from '@backstage/config'; import { readCorsOptions } from './readCorsOptions'; describe('readCorsOptions', () => { + it('should be disabled by default', () => { + expect(readCorsOptions(new ConfigReader({}))).toEqual({ + origin: false, + }); + }); + it('reads single string', () => { const mockCallback = jest.fn(); const config = new ConfigReader({ cors: { origin: 'https://*.value*' } }); diff --git a/packages/backend-app-api/src/services/implementations/rootHttpRouter/readCorsOptions.ts b/packages/backend-app-api/src/services/implementations/rootHttpRouter/readCorsOptions.ts index 17c3157ebd..809ee9b527 100644 --- a/packages/backend-app-api/src/services/implementations/rootHttpRouter/readCorsOptions.ts +++ b/packages/backend-app-api/src/services/implementations/rootHttpRouter/readCorsOptions.ts @@ -29,10 +29,10 @@ import { Minimatch } from 'minimatch'; * const corsOptions = readCorsOptions(config.getConfig('backend')); * ``` */ -export function readCorsOptions(config: Config): CorsOptions | undefined { +export function readCorsOptions(config: Config): CorsOptions { const cc = config.getOptionalConfig('cors'); if (!cc) { - return undefined; + return { origin: false }; // Disable CORS } return { From f56886014e551577ed0d3e8b99ebe8437e1572ea Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 5 Jan 2023 13:39:42 +0100 Subject: [PATCH 29/55] backend-app-api: make config optional for all config readers Signed-off-by: Patrik Oldsberg --- .../src/lib/http/config.test.ts | 6 +++++ .../backend-app-api/src/lib/http/config.ts | 20 +++++++-------- .../rootHttpRouter/readCorsOptions.test.ts | 2 +- .../rootHttpRouter/readCorsOptions.ts | 4 +-- .../rootHttpRouter/readHelmetOptions.test.ts | 25 ++++++++++++++++++- .../rootHttpRouter/readHelmetOptions.ts | 6 ++--- 6 files changed, 46 insertions(+), 17 deletions(-) diff --git a/packages/backend-app-api/src/lib/http/config.test.ts b/packages/backend-app-api/src/lib/http/config.test.ts index 3ab5eda5e9..f54ba927ca 100644 --- a/packages/backend-app-api/src/lib/http/config.test.ts +++ b/packages/backend-app-api/src/lib/http/config.test.ts @@ -18,6 +18,12 @@ import { ConfigReader } from '@backstage/config'; import { readHttpServerOptions } from './config'; describe('readHttpServerOptions', () => { + it('should return defaults', () => { + expect(readHttpServerOptions()).toEqual({ + listen: { host: '', port: 7007 }, + }); + }); + it.each([ [{}, { listen: { host: '', port: 7007 } }], [{ listen: ':80' }, { listen: { host: '', port: 80 } }], diff --git a/packages/backend-app-api/src/lib/http/config.ts b/packages/backend-app-api/src/lib/http/config.ts index 3c5acf0ffd..e3b41c63de 100644 --- a/packages/backend-app-api/src/lib/http/config.ts +++ b/packages/backend-app-api/src/lib/http/config.ts @@ -34,15 +34,15 @@ const DEFAULT_HOST = ''; * const opts = readHttpServerOptions(config.getConfig('backend')); * ``` */ -export function readHttpServerOptions(config: Config): HttpServerOptions { +export function readHttpServerOptions(config?: Config): HttpServerOptions { return { listen: readHttpListenOptions(config), https: readHttpsOptions(config), }; } -function readHttpListenOptions(config: Config): HttpServerOptions['listen'] { - const listen = config.getOptional('listen'); +function readHttpListenOptions(config?: Config): HttpServerOptions['listen'] { + const listen = config?.getOptional('listen'); if (typeof listen === 'string') { const parts = String(listen).split(':'); const port = parseInt(parts[parts.length - 1], 10); @@ -60,22 +60,22 @@ function readHttpListenOptions(config: Config): HttpServerOptions['listen'] { } // Workaround to allow empty string - const host = config.getOptional('listen.host') ?? DEFAULT_HOST; + const host = config?.getOptional('listen.host') ?? DEFAULT_HOST; if (typeof host !== 'string') { - config.getOptionalString('listen.host'); // will throw + config?.getOptionalString('listen.host'); // will throw throw new Error('unreachable'); } return { - port: config.getOptionalNumber('listen.port') ?? DEFAULT_PORT, + port: config?.getOptionalNumber('listen.port') ?? DEFAULT_PORT, host, }; } -function readHttpsOptions(config: Config): HttpServerOptions['https'] { - const https = config.getOptional('https'); +function readHttpsOptions(config?: Config): HttpServerOptions['https'] { + const https = config?.getOptional('https'); if (https === true) { - const baseUrl = config.getString('baseUrl'); + const baseUrl = config!.getString('baseUrl'); let hostname; try { hostname = new URL(baseUrl).hostname; @@ -86,7 +86,7 @@ function readHttpsOptions(config: Config): HttpServerOptions['https'] { return { certificate: { type: 'generated', hostname } }; } - const cc = config.getOptionalConfig('https'); + const cc = config?.getOptionalConfig('https'); if (!cc) { return undefined; } diff --git a/packages/backend-app-api/src/services/implementations/rootHttpRouter/readCorsOptions.test.ts b/packages/backend-app-api/src/services/implementations/rootHttpRouter/readCorsOptions.test.ts index 113b2a8ee6..af490a26f9 100644 --- a/packages/backend-app-api/src/services/implementations/rootHttpRouter/readCorsOptions.test.ts +++ b/packages/backend-app-api/src/services/implementations/rootHttpRouter/readCorsOptions.test.ts @@ -19,7 +19,7 @@ import { readCorsOptions } from './readCorsOptions'; describe('readCorsOptions', () => { it('should be disabled by default', () => { - expect(readCorsOptions(new ConfigReader({}))).toEqual({ + expect(readCorsOptions()).toEqual({ origin: false, }); }); diff --git a/packages/backend-app-api/src/services/implementations/rootHttpRouter/readCorsOptions.ts b/packages/backend-app-api/src/services/implementations/rootHttpRouter/readCorsOptions.ts index 809ee9b527..98b548d02e 100644 --- a/packages/backend-app-api/src/services/implementations/rootHttpRouter/readCorsOptions.ts +++ b/packages/backend-app-api/src/services/implementations/rootHttpRouter/readCorsOptions.ts @@ -29,8 +29,8 @@ import { Minimatch } from 'minimatch'; * const corsOptions = readCorsOptions(config.getConfig('backend')); * ``` */ -export function readCorsOptions(config: Config): CorsOptions { - const cc = config.getOptionalConfig('cors'); +export function readCorsOptions(config?: Config): CorsOptions { + const cc = config?.getOptionalConfig('cors'); if (!cc) { return { origin: false }; // Disable CORS } diff --git a/packages/backend-app-api/src/services/implementations/rootHttpRouter/readHelmetOptions.test.ts b/packages/backend-app-api/src/services/implementations/rootHttpRouter/readHelmetOptions.test.ts index d5e0e29264..bc31404634 100644 --- a/packages/backend-app-api/src/services/implementations/rootHttpRouter/readHelmetOptions.test.ts +++ b/packages/backend-app-api/src/services/implementations/rootHttpRouter/readHelmetOptions.test.ts @@ -18,6 +18,30 @@ import { ConfigReader } from '@backstage/config'; import { readHelmetOptions } from './readHelmetOptions'; describe('readHelmetOptions', () => { + it('should return defaults', () => { + expect(readHelmetOptions()).toEqual({ + contentSecurityPolicy: { + useDefaults: false, + directives: { + 'default-src': ["'self'"], + 'base-uri': ["'self'"], + 'font-src': ["'self'", 'https:', 'data:'], + 'frame-ancestors': ["'self'"], + 'img-src': ["'self'", 'data:'], + 'object-src': ["'none'"], + 'script-src': ["'self'", "'unsafe-eval'"], + 'style-src': ["'self'", 'https:', "'unsafe-inline'"], + 'script-src-attr': ["'none'"], + 'upgrade-insecure-requests': [], + }, + }, + crossOriginEmbedderPolicy: false, + crossOriginOpenerPolicy: false, + crossOriginResourcePolicy: false, + originAgentCluster: false, + }); + }); + it('should add additional directives', () => { const config = new ConfigReader({ csp: { @@ -34,7 +58,6 @@ describe('readHelmetOptions', () => { 'base-uri': ["'self'"], 'font-src': ["'self'", 'https:', 'data:'], 'frame-ancestors': ["'self'"], - // 'img-src': ["'self'", 'data:'], 'object-src': ["'none'"], 'script-src': ["'self'", "'unsafe-eval'"], 'style-src': ["'self'", 'https:', "'unsafe-inline'"], diff --git a/packages/backend-app-api/src/services/implementations/rootHttpRouter/readHelmetOptions.ts b/packages/backend-app-api/src/services/implementations/rootHttpRouter/readHelmetOptions.ts index e86997d685..9cdf339844 100644 --- a/packages/backend-app-api/src/services/implementations/rootHttpRouter/readHelmetOptions.ts +++ b/packages/backend-app-api/src/services/implementations/rootHttpRouter/readHelmetOptions.ts @@ -30,7 +30,7 @@ import { ContentSecurityPolicyOptions } from 'helmet/dist/types/middlewares/cont * const helmetOptions = readHelmetOptions(config.getConfig('backend')); * ``` */ -export function readHelmetOptions(config: Config): HelmetOptions { +export function readHelmetOptions(config?: Config): HelmetOptions { const cspOptions = readCspDirectives(config); return { contentSecurityPolicy: { @@ -61,8 +61,8 @@ type CspDirectives = Record | undefined; * upgrade-insecure-requests: false * ``` */ -function readCspDirectives(config: Config): CspDirectives { - const cc = config.getOptionalConfig('csp'); +function readCspDirectives(config?: Config): CspDirectives { + const cc = config?.getOptionalConfig('csp'); if (!cc) { return undefined; } From d56bd5dfef0985f363027b157b36fb300f73ea47 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 5 Jan 2023 14:32:21 +0100 Subject: [PATCH 30/55] backend-app-api: add startHttpServer helper Signed-off-by: Patrik Oldsberg --- .../backend-app-api/src/lib/http/index.ts | 9 ++- .../src/lib/http/startHttpServer.ts | 62 +++++++++++++++++++ 2 files changed, 70 insertions(+), 1 deletion(-) create mode 100644 packages/backend-app-api/src/lib/http/startHttpServer.ts diff --git a/packages/backend-app-api/src/lib/http/index.ts b/packages/backend-app-api/src/lib/http/index.ts index 0717194f75..94ab61f6c8 100644 --- a/packages/backend-app-api/src/lib/http/index.ts +++ b/packages/backend-app-api/src/lib/http/index.ts @@ -14,5 +14,12 @@ * limitations under the License. */ +export { createHttpServer } from './createHttpServer'; +export { startHttpServer } from './startHttpServer'; +export type { StartHttpServerOptions } from './startHttpServer'; export { readHttpServerOptions } from './config'; -export type { HttpServerOptions, HttpServerCertificateOptions } from './types'; +export type { + HttpServerOptions, + HttpServerCertificateOptions, + ExtendedHttpServer, +} from './types'; diff --git a/packages/backend-app-api/src/lib/http/startHttpServer.ts b/packages/backend-app-api/src/lib/http/startHttpServer.ts new file mode 100644 index 0000000000..64d0be3f1e --- /dev/null +++ b/packages/backend-app-api/src/lib/http/startHttpServer.ts @@ -0,0 +1,62 @@ +/* + * 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 { + ConfigService, + RootLifecycleService, + RootLoggerService, +} from '@backstage/backend-plugin-api'; +import { RequestListener } from 'http'; +import { readHttpServerOptions } from './config'; +import { createHttpServer } from './createHttpServer'; + +/** + * Options for {@link startHttpServer}. + * + * @public + */ +export interface StartHttpServerOptions { + config: ConfigService; + logger: RootLoggerService; + lifecycle: RootLifecycleService; +} + +/** + * Starts up an HTTP server, as well as registers a shutdown handler that stops the server. + * + * @public + */ +export async function startHttpServer( + listener: RequestListener, + options: StartHttpServerOptions, +) { + const { config, logger, lifecycle } = options; + + const server = await createHttpServer( + listener, + readHttpServerOptions(config.getOptionalConfig('backend')), + { logger }, + ); + + lifecycle.addShutdownHook({ + async fn() { + await server.stop(); + }, + labels: { type: 'httpServer' }, + }); + + await server.start(); +} From 15f15e4a7121d7eff8d8ae18ec3ef7ac26c0b5f5 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 5 Jan 2023 14:32:58 +0100 Subject: [PATCH 31/55] backend-app-api: reimplement rootHttpRouterFactory with internal API Signed-off-by: Patrik Oldsberg --- packages/backend-app-api/package.json | 2 + .../rootHttpRouter/rootHttpRouterFactory.ts | 64 ++++++++----------- yarn.lock | 2 + 3 files changed, 31 insertions(+), 37 deletions(-) diff --git a/packages/backend-app-api/package.json b/packages/backend-app-api/package.json index 7d56da10f1..8c29b08c9e 100644 --- a/packages/backend-app-api/package.json +++ b/packages/backend-app-api/package.json @@ -41,6 +41,7 @@ "@backstage/plugin-permission-node": "workspace:^", "@types/cors": "^2.8.6", "@types/express": "^4.17.6", + "compression": "^1.7.4", "cors": "^2.8.5", "express": "^4.17.1", "express-promise-router": "^4.1.0", @@ -54,6 +55,7 @@ }, "devDependencies": { "@backstage/cli": "workspace:^", + "@types/compression": "^1.7.0", "@types/fs-extra": "^9.0.3", "@types/node-forge": "^1.3.0", "@types/stoppable": "^1.1.0" diff --git a/packages/backend-app-api/src/services/implementations/rootHttpRouter/rootHttpRouterFactory.ts b/packages/backend-app-api/src/services/implementations/rootHttpRouter/rootHttpRouterFactory.ts index ffe345f0a0..d491663ff8 100644 --- a/packages/backend-app-api/src/services/implementations/rootHttpRouter/rootHttpRouterFactory.ts +++ b/packages/backend-app-api/src/services/implementations/rootHttpRouter/rootHttpRouterFactory.ts @@ -18,9 +18,19 @@ import { createServiceFactory, coreServices, } from '@backstage/backend-plugin-api'; -import { Handler } from 'express'; -import { createServiceBuilder } from '@backstage/backend-common'; +import express from 'express'; +import compression from 'compression'; +import cors from 'cors'; +import helmet from 'helmet'; import { RestrictedIndexedRouter } from './RestrictedIndexedRouter'; +import { readCorsOptions } from './readCorsOptions'; +import { startHttpServer } from '../../../lib/http'; +import { readHelmetOptions } from './readHelmetOptions'; +import { + errorHandler, + notFoundHandler, + requestLoggingHandler, +} from '@backstage/backend-common'; /** * @public @@ -30,11 +40,6 @@ export type RootHttpRouterFactoryOptions = { * The path to forward all unmatched requests to. Defaults to '/api/app' */ indexPath?: string | false; - - /** - * Middlewares that are added before all other routes. - */ - middleware?: Handler[]; }; /** @public */ @@ -42,41 +47,26 @@ export const rootHttpRouterFactory = createServiceFactory({ service: coreServices.rootHttpRouter, deps: { config: coreServices.config, + logger: coreServices.rootLogger, lifecycle: coreServices.rootLifecycle, }, - async factory({ config, lifecycle }, options?: RootHttpRouterFactoryOptions) { - const router = new RestrictedIndexedRouter( - options?.indexPath ?? '/api/app', - ); + async factory( + { config, logger, lifecycle }, + { indexPath }: RootHttpRouterFactoryOptions = {}, + ) { + const router = new RestrictedIndexedRouter(indexPath ?? '/api/app'); - const service = createServiceBuilder(module).loadConfig(config); + const app = express(); - for (const middleware of options?.middleware ?? []) { - service.addRouter('', middleware); - } + app.use(helmet(readHelmetOptions(config.getOptionalConfig('backend')))); + app.use(cors(readCorsOptions(config.getOptionalConfig('backend')))); + app.use(compression()); + app.use(requestLoggingHandler(logger)); + app.use(router.handler()); + app.use(notFoundHandler()); + app.use(errorHandler({ logger })); - service.addRouter('', router.handler()); - - const server = await service.start(); - // Stop method isn't part of the public API, let's fix that once we move the implementation here. - const stoppableServer = server as typeof server & { - stop: (cb: (error?: Error) => void) => void; - }; - - lifecycle.addShutdownHook({ - async fn() { - await new Promise((resolve, reject) => { - stoppableServer.stop((error?: Error) => { - if (error) { - reject(error); - } else { - resolve(); - } - }); - }); - }, - labels: { service: 'rootHttpRouter' }, - }); + await startHttpServer(app, { config, logger, lifecycle }); return router; }, diff --git a/yarn.lock b/yarn.lock index 0179e179e2..6409539d39 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3384,11 +3384,13 @@ __metadata: "@backstage/config": "workspace:^" "@backstage/errors": "workspace:^" "@backstage/plugin-permission-node": "workspace:^" + "@types/compression": ^1.7.0 "@types/cors": ^2.8.6 "@types/express": ^4.17.6 "@types/fs-extra": ^9.0.3 "@types/node-forge": ^1.3.0 "@types/stoppable": ^1.1.0 + compression: ^1.7.4 cors: ^2.8.5 express: ^4.17.1 express-promise-router: ^4.1.0 From eb62b5be7481697b172a3696d51db45e0da80abb Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 9 Jan 2023 15:51:32 +0100 Subject: [PATCH 32/55] backend-app-api: add MiddlewareFactory + configure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: blam Co-authored-by: Fredrik Adelöw Co-authored-by: Johan Haals Signed-off-by: Patrik Oldsberg --- packages/backend-app-api/package.json | 7 +- .../src/lib/http/MiddlewareFactory.test.ts | 213 ++++++++++++++ .../src/lib/http/MiddlewareFactory.ts | 260 ++++++++++++++++++ .../backend-app-api/src/lib/http/index.ts | 2 + .../rootHttpRouter/rootHttpRouterFactory.ts | 66 +++-- yarn.lock | 5 + 6 files changed, 531 insertions(+), 22 deletions(-) create mode 100644 packages/backend-app-api/src/lib/http/MiddlewareFactory.test.ts create mode 100644 packages/backend-app-api/src/lib/http/MiddlewareFactory.ts diff --git a/packages/backend-app-api/package.json b/packages/backend-app-api/package.json index 8c29b08c9e..ed1d2bca40 100644 --- a/packages/backend-app-api/package.json +++ b/packages/backend-app-api/package.json @@ -48,6 +48,7 @@ "fs-extra": "10.1.0", "helmet": "^6.0.0", "minimatch": "^5.0.0", + "morgan": "^1.10.0", "node-forge": "^1.3.1", "selfsigned": "^2.0.0", "stoppable": "^1.1.0", @@ -57,8 +58,12 @@ "@backstage/cli": "workspace:^", "@types/compression": "^1.7.0", "@types/fs-extra": "^9.0.3", + "@types/http-errors": "^2.0.0", + "@types/morgan": "^1.9.0", "@types/node-forge": "^1.3.0", - "@types/stoppable": "^1.1.0" + "@types/stoppable": "^1.1.0", + "http-errors": "^2.0.0", + "supertest": "^6.1.3" }, "files": [ "dist", diff --git a/packages/backend-app-api/src/lib/http/MiddlewareFactory.test.ts b/packages/backend-app-api/src/lib/http/MiddlewareFactory.test.ts new file mode 100644 index 0000000000..64812564ca --- /dev/null +++ b/packages/backend-app-api/src/lib/http/MiddlewareFactory.test.ts @@ -0,0 +1,213 @@ +/* + * 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 { + AuthenticationError, + ConflictError, + InputError, + NotAllowedError, + NotFoundError, + NotModifiedError, +} from '@backstage/errors'; +import express from 'express'; +import createError from 'http-errors'; +import request from 'supertest'; +import { MiddlewareFactory } from './MiddlewareFactory'; +import { ConfigReader } from '@backstage/config'; + +describe('MiddlewareFactory', () => { + describe('middleware.error', () => { + const childLogger = { + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + debug: jest.fn(), + child: jest.fn(), + }; + + const logger = { + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + debug: jest.fn(), + child: () => childLogger, + }; + + const middleware = MiddlewareFactory.create({ + logger, + config: new ConfigReader({}), + }); + + beforeEach(() => { + jest.resetAllMocks(); + }); + + it('gives default code and message', async () => { + const app = express(); + app.use('/breaks', () => { + throw new Error('some message'); + }); + app.use(middleware.error()); + + const response = await request(app).get('/breaks'); + + expect(response.status).toBe(500); + expect(response.body).toEqual({ + error: expect.objectContaining({ + name: 'Error', + message: 'some message', + }), + request: { method: 'GET', url: '/breaks' }, + response: { statusCode: 500 }, + }); + }); + + it('does not try to send the response again if its already been sent', async () => { + const app = express(); + const mockSend = jest.fn(); + + app.use('/works_with_async_fail', (_, res) => { + res.status(200).send('hello'); + + // mutate the response object to test the middleware. + // it's hard to catch errors inside middleware from the outside. + res.send = mockSend; + throw new Error('some message'); + }); + + app.use(middleware.error()); + const response = await request(app).get('/works_with_async_fail'); + + expect(response.status).toBe(200); + expect(response.text).toBe('hello'); + + expect(mockSend).not.toHaveBeenCalled(); + }); + + it('takes code from http-errors library errors', async () => { + const app = express(); + app.use('/breaks', () => { + throw createError(432, 'Some Message'); + }); + app.use(middleware.error()); + + const response = await request(app).get('/breaks'); + + expect(response.status).toBe(432); + expect(response.body).toEqual({ + error: { + expose: true, + name: 'BadRequestError', + message: 'Some Message', + status: 432, + statusCode: 432, + }, + request: { + method: 'GET', + url: '/breaks', + }, + response: { statusCode: 432 }, + }); + }); + + it('handles well-known error classes', async () => { + const app = express(); + app.use('/NotModifiedError', () => { + throw new NotModifiedError(); + }); + app.use('/InputError', () => { + throw new InputError(); + }); + app.use('/AuthenticationError', () => { + throw new AuthenticationError(); + }); + app.use('/NotAllowedError', () => { + throw new NotAllowedError(); + }); + app.use('/NotFoundError', () => { + throw new NotFoundError(); + }); + app.use('/ConflictError', () => { + throw new ConflictError(); + }); + app.use(middleware.error()); + + const r = request(app); + expect((await r.get('/NotModifiedError')).status).toBe(304); + expect((await r.get('/InputError')).status).toBe(400); + expect((await r.get('/InputError')).body.error.name).toBe('InputError'); + expect((await r.get('/AuthenticationError')).status).toBe(401); + expect((await r.get('/AuthenticationError')).body.error.name).toBe( + 'AuthenticationError', + ); + expect((await r.get('/NotAllowedError')).status).toBe(403); + expect((await r.get('/NotAllowedError')).body.error.name).toBe( + 'NotAllowedError', + ); + expect((await r.get('/NotFoundError')).status).toBe(404); + expect((await r.get('/NotFoundError')).body.error.name).toBe( + 'NotFoundError', + ); + expect((await r.get('/ConflictError')).status).toBe(409); + expect((await r.get('/ConflictError')).body.error.name).toBe( + 'ConflictError', + ); + }); + + it('logs all 500 errors', async () => { + const app = express(); + const thrownError = new Error('some error'); + + app.use('/breaks', () => { + throw thrownError; + }); + app.use(middleware.error()); + + await request(app).get('/breaks'); + + expect(childLogger.error).toHaveBeenCalledWith( + 'Request failed with status 500', + thrownError, + ); + }); + + it('does not log 400 errors', async () => { + const app = express(); + + app.use('/NotFound', () => { + throw new NotFoundError(); + }); + app.use(middleware.error()); + + await request(app).get('/NotFound'); + + expect(childLogger.error).not.toHaveBeenCalled(); + }); + + it('log 400 errors when logAllErrors is true', async () => { + const app = express(); + + app.use('/NotFound', () => { + throw new NotFoundError(); + }); + app.use(middleware.error({ logAllErrors: true })); + + await request(app).get('/NotFound'); + + expect(childLogger.error).toHaveBeenCalled(); + }); + }); +}); diff --git a/packages/backend-app-api/src/lib/http/MiddlewareFactory.ts b/packages/backend-app-api/src/lib/http/MiddlewareFactory.ts new file mode 100644 index 0000000000..ccc94779b0 --- /dev/null +++ b/packages/backend-app-api/src/lib/http/MiddlewareFactory.ts @@ -0,0 +1,260 @@ +/* + * 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 { ConfigService, LoggerService } from '@backstage/backend-plugin-api'; +import { Request, Response, ErrorRequestHandler, NextFunction } from 'express'; +import cors from 'cors'; +import helmet from 'helmet'; +import morgan from 'morgan'; +import compression from 'compression'; +import { readHelmetOptions } from '../../services/implementations/rootHttpRouter/readHelmetOptions'; +import { readCorsOptions } from '../../services/implementations/rootHttpRouter/readCorsOptions'; +import { + AuthenticationError, + ConflictError, + ErrorResponseBody, + InputError, + NotAllowedError, + NotFoundError, + NotModifiedError, + serializeError, +} from '@backstage/errors'; + +/** + * Options used to create a {@link MiddlewareFactory}. + * + * @public + */ +export interface MiddlewareFactoryOptions { + config: ConfigService; + logger: LoggerService; +} + +/** + * Options passed to the {@link errorHandler} middleware. + * + * @public + */ +export interface MiddlewareFactoryErrorOptions { + /** + * Whether error response bodies should show error stack traces or not. + * + * If not specified, by default shows stack traces only in development mode. + */ + showStackTraces?: boolean; + + /** + * Whether any 4xx errors should be logged or not. + * + * If not specified, default to only logging 5xx errors. + */ + logAllErrors?: boolean; +} + +/** + * A utility to configure common middleware. + * + * @public + */ +export class MiddlewareFactory { + #config: ConfigService; + #logger: LoggerService; + + /** + * Creates a new {@link MiddlewareFactory}. + */ + static create(options: MiddlewareFactoryOptions) { + return new MiddlewareFactory(options); + } + + private constructor(options: MiddlewareFactoryOptions) { + this.#config = options.config; + this.#logger = options.logger; + } + + /** + * Returns a middleware that unconditionally produces a 404 error response. + * + * @remarks + * + * Typically you want to place this middleware at the end of the chain, such + * that it's the last one attempted after no other routes matched. + * + * @returns An Express request handler + */ + notFound() { + return (_req: Request, res: Response) => { + res.status(404).end(); + }; + } + + /** + * Returns the compression middleware. + * + * @remarks + * + * The middleware will attempt to compress response bodies for all requests + * that traverse through the middleware. + */ + compression() { + return compression(); + } + + /** + * Returns a request logging middleware. + * + * @remarks + * + * Typically you want to place this middleware at the start of the chain, such + * that it always logs requests whether they are "caught" by handlers farther + * down or not. + * + * @returns An Express request handler + */ + logging() { + const logger = this.#logger.child({ + type: 'incomingRequest', + }); + + return morgan('combined', { + stream: { + write(message: string) { + logger.info(message.trimEnd()); + }, + }, + }); + } + + /** + * Returns a middleware that implements the helmet library. + * + * @remarks + * + * This middleware applies security policies to incoming requests and outgoing + * responses. It is configured using config keys such as `backend.csp`. + * + * @see {@link https://helmetjs.github.io/} + * + * @returns An Express request handler + */ + helmet() { + return helmet(readHelmetOptions(this.#config.getOptionalConfig('backend'))); + } + + /** + * Returns a middleware that implements the cors library. + * + * @remarks + * + * This middleware handles CORS. It is configured using the config key + * `backend.cors`. + * + * @see {@link https://github.com/expressjs/cors} + * + * @returns An Express request handler + */ + cors() { + return cors(readCorsOptions(this.#config.getOptionalConfig('backend'))); + } + + /** + * Express middleware to handle errors during request processing. + * + * @remarks + * + * This is commonly the very last middleware in the chain. + * + * Its primary purpose is not to do translation of business logic exceptions, + * but rather to be a global catch-all for uncaught "fatal" errors that are + * expected to result in a 500 error. However, it also does handle some common + * error types (such as http-error exceptions, and the well-known error types + * in the `@backstage/errors` package) and returns the enclosed status code + * accordingly. + * + * It will also produce a response body with a serialized form of the error, + * unless a previous handler already did send a body. See + * {@link @backstage/errors#ErrorResponseBody} for the response shape used. + * + * @returns An Express error request handler + */ + error(options: MiddlewareFactoryErrorOptions = {}): ErrorRequestHandler { + const showStackTraces = + options.showStackTraces ?? process.env.NODE_ENV === 'development'; + + const logger = this.#logger.child({ + type: 'errorHandler', + }); + + return (error: Error, req: Request, res: Response, next: NextFunction) => { + const statusCode = getStatusCode(error); + if (options.logAllErrors || statusCode >= 500) { + logger.error(`Request failed with status ${statusCode}`, error); + } + + if (res.headersSent) { + // If the headers have already been sent, do not send the response again + // as this will throw an error in the backend. + next(error); + return; + } + + const body: ErrorResponseBody = { + error: serializeError(error, { includeStack: showStackTraces }), + request: { method: req.method, url: req.url }, + response: { statusCode }, + }; + + res.status(statusCode).json(body); + }; + } +} + +function getStatusCode(error: Error): number { + // Look for common http library status codes + const knownStatusCodeFields = ['statusCode', 'status']; + for (const field of knownStatusCodeFields) { + const statusCode = (error as any)[field]; + if ( + typeof statusCode === 'number' && + (statusCode | 0) === statusCode && // is whole integer + statusCode >= 100 && + statusCode <= 599 + ) { + return statusCode; + } + } + + // Handle well-known error types + switch (error.name) { + case NotModifiedError.name: + return 304; + case InputError.name: + return 400; + case AuthenticationError.name: + return 401; + case NotAllowedError.name: + return 403; + case NotFoundError.name: + return 404; + case ConflictError.name: + return 409; + default: + break; + } + + // Fall back to internal server error + return 500; +} diff --git a/packages/backend-app-api/src/lib/http/index.ts b/packages/backend-app-api/src/lib/http/index.ts index 94ab61f6c8..db579d1b44 100644 --- a/packages/backend-app-api/src/lib/http/index.ts +++ b/packages/backend-app-api/src/lib/http/index.ts @@ -23,3 +23,5 @@ export type { HttpServerCertificateOptions, ExtendedHttpServer, } from './types'; +export { MiddlewareFactory } from './MiddlewareFactory'; +export type { MiddlewareFactoryOptions } from './MiddlewareFactory'; diff --git a/packages/backend-app-api/src/services/implementations/rootHttpRouter/rootHttpRouterFactory.ts b/packages/backend-app-api/src/services/implementations/rootHttpRouter/rootHttpRouterFactory.ts index d491663ff8..f82cfe4a69 100644 --- a/packages/backend-app-api/src/services/implementations/rootHttpRouter/rootHttpRouterFactory.ts +++ b/packages/backend-app-api/src/services/implementations/rootHttpRouter/rootHttpRouterFactory.ts @@ -15,22 +15,24 @@ */ import { - createServiceFactory, + ConfigService, coreServices, + createServiceFactory, + LifecycleService, + LoggerService, } from '@backstage/backend-plugin-api'; -import express from 'express'; -import compression from 'compression'; -import cors from 'cors'; -import helmet from 'helmet'; +import express, { RequestHandler, Express } from 'express'; +import { MiddlewareFactory, startHttpServer } from '../../../lib/http'; import { RestrictedIndexedRouter } from './RestrictedIndexedRouter'; -import { readCorsOptions } from './readCorsOptions'; -import { startHttpServer } from '../../../lib/http'; -import { readHelmetOptions } from './readHelmetOptions'; -import { - errorHandler, - notFoundHandler, - requestLoggingHandler, -} from '@backstage/backend-common'; + +interface RootHttpRouterConfigureOptions { + app: Express; + middleware: MiddlewareFactory; + routes: RequestHandler; + config: ConfigService; + logger: LoggerService; + lifecycle: LifecycleService; +} /** * @public @@ -40,8 +42,24 @@ export type RootHttpRouterFactoryOptions = { * The path to forward all unmatched requests to. Defaults to '/api/app' */ indexPath?: string | false; + + configure?(options: RootHttpRouterConfigureOptions): void; }; +function defaultConfigure({ + app, + routes, + middleware, +}: RootHttpRouterConfigureOptions) { + 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()); +} + /** @public */ export const rootHttpRouterFactory = createServiceFactory({ service: coreServices.rootHttpRouter, @@ -52,19 +70,25 @@ export const rootHttpRouterFactory = createServiceFactory({ }, async factory( { config, logger, lifecycle }, - { indexPath }: RootHttpRouterFactoryOptions = {}, + { + indexPath, + configure = defaultConfigure, + }: RootHttpRouterFactoryOptions = {}, ) { const router = new RestrictedIndexedRouter(indexPath ?? '/api/app'); const app = express(); - app.use(helmet(readHelmetOptions(config.getOptionalConfig('backend')))); - app.use(cors(readCorsOptions(config.getOptionalConfig('backend')))); - app.use(compression()); - app.use(requestLoggingHandler(logger)); - app.use(router.handler()); - app.use(notFoundHandler()); - app.use(errorHandler({ logger })); + const middleware = MiddlewareFactory.create({ config, logger }); + + configure({ + app, + routes: router.handler(), + middleware, + config, + logger, + lifecycle, + }); await startHttpServer(app, { config, logger, lifecycle }); diff --git a/yarn.lock b/yarn.lock index 6409539d39..ed60523406 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3388,6 +3388,8 @@ __metadata: "@types/cors": ^2.8.6 "@types/express": ^4.17.6 "@types/fs-extra": ^9.0.3 + "@types/http-errors": ^2.0.0 + "@types/morgan": ^1.9.0 "@types/node-forge": ^1.3.0 "@types/stoppable": ^1.1.0 compression: ^1.7.4 @@ -3396,10 +3398,13 @@ __metadata: express-promise-router: ^4.1.0 fs-extra: 10.1.0 helmet: ^6.0.0 + http-errors: ^2.0.0 minimatch: ^5.0.0 + morgan: ^1.10.0 node-forge: ^1.3.1 selfsigned: ^2.0.0 stoppable: ^1.1.0 + supertest: ^6.1.3 winston: ^3.2.1 languageName: unknown linkType: soft From 5de909129fa2c52896ed940de5f5f6a83407810c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 9 Jan 2023 15:02:18 +0000 Subject: [PATCH 33/55] fix(deps): update dependency js-base64 to v3.7.4 Signed-off-by: Renovate Bot --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 588d41c776..2edc4fdbbf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -26786,9 +26786,9 @@ __metadata: linkType: hard "js-base64@npm:^3.6.0": - version: 3.7.3 - resolution: "js-base64@npm:3.7.3" - checksum: ee19bed9ba21693e4583a47773dd701938de63db82e7c324d4c19093046157f0d87905cc20540f194f1d0ec88d6e1bfc4056aea916bcb873ebcf95942d26aa5a + version: 3.7.4 + resolution: "js-base64@npm:3.7.4" + checksum: b8f6b10033b57bbb87fa34629f44bc665cb8b7bcba49610a29b296592616e633e9c186afaa02de5a54d60c08aaa36c8c8ad585db095a96fb331fe022f54792a2 languageName: node linkType: hard From c44dc6225ffb0eefa55571e3a5c7f7bc0bd2228d Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 9 Jan 2023 16:04:25 +0100 Subject: [PATCH 34/55] backend-common: reimplement middleware using MiddlewareFactory MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fredrik Adelöw Co-authored-by: blam Co-authored-by: Johan Haals Signed-off-by: Patrik Oldsberg --- packages/backend-app-api/src/index.ts | 1 + packages/backend-common/package.json | 1 + .../src/middleware/errorHandler.ts | 84 ++----------------- .../src/middleware/notFoundHandler.ts | 13 +-- .../src/middleware/requestLoggingHandler.ts | 18 ++-- yarn.lock | 1 + 6 files changed, 26 insertions(+), 92 deletions(-) diff --git a/packages/backend-app-api/src/index.ts b/packages/backend-app-api/src/index.ts index 02633f3732..a8cc079d47 100644 --- a/packages/backend-app-api/src/index.ts +++ b/packages/backend-app-api/src/index.ts @@ -20,5 +20,6 @@ * @packageDocumentation */ +export * from './lib/http'; export * from './wiring'; export * from './services/implementations'; diff --git a/packages/backend-common/package.json b/packages/backend-common/package.json index aff5e56abb..9c16472263 100644 --- a/packages/backend-common/package.json +++ b/packages/backend-common/package.json @@ -34,6 +34,7 @@ "test:kubernetes": "backstage-cli package test -t KubernetesContainerRunner --no-watch" }, "dependencies": { + "@backstage/backend-app-api": "workspace:^", "@backstage/backend-plugin-api": "workspace:^", "@backstage/cli-common": "workspace:^", "@backstage/config": "workspace:^", diff --git a/packages/backend-common/src/middleware/errorHandler.ts b/packages/backend-common/src/middleware/errorHandler.ts index 04b3f92e26..99ede75ab0 100644 --- a/packages/backend-common/src/middleware/errorHandler.ts +++ b/packages/backend-common/src/middleware/errorHandler.ts @@ -14,19 +14,11 @@ * limitations under the License. */ -import { - AuthenticationError, - ConflictError, - ErrorResponseBody, - InputError, - NotAllowedError, - NotFoundError, - NotModifiedError, - serializeError, -} from '@backstage/errors'; -import { ErrorRequestHandler, NextFunction, Request, Response } from 'express'; +import { ErrorRequestHandler } from 'express'; import { LoggerService } from '@backstage/backend-plugin-api'; import { getRootLogger } from '../logging'; +import { ConfigReader } from '@backstage/config'; +import { MiddlewareFactory } from '@backstage/backend-app-api'; /** * Options passed to the {@link errorHandler} middleware. @@ -73,69 +65,11 @@ export type ErrorHandlerOptions = { export function errorHandler( options: ErrorHandlerOptions = {}, ): ErrorRequestHandler { - const showStackTraces = - options.showStackTraces ?? process.env.NODE_ENV === 'development'; - - const logger = (options.logger || getRootLogger()).child({ - type: 'errorHandler', + return MiddlewareFactory.create({ + config: new ConfigReader({}), + logger: options.logger ?? getRootLogger(), + }).error({ + logAllErrors: options.logClientErrors, + showStackTraces: options.showStackTraces, }); - - return (error: Error, req: Request, res: Response, next: NextFunction) => { - const statusCode = getStatusCode(error); - if (options.logClientErrors || statusCode >= 500) { - logger.error(`Request failed with status ${statusCode}`, error); - } - - if (res.headersSent) { - // If the headers have already been sent, do not send the response again - // as this will throw an error in the backend. - next(error); - return; - } - - const body: ErrorResponseBody = { - error: serializeError(error, { includeStack: showStackTraces }), - request: { method: req.method, url: req.url }, - response: { statusCode }, - }; - - res.status(statusCode).json(body); - }; -} - -function getStatusCode(error: Error): number { - // Look for common http library status codes - const knownStatusCodeFields = ['statusCode', 'status']; - for (const field of knownStatusCodeFields) { - const statusCode = (error as any)[field]; - if ( - typeof statusCode === 'number' && - (statusCode | 0) === statusCode && // is whole integer - statusCode >= 100 && - statusCode <= 599 - ) { - return statusCode; - } - } - - // Handle well-known error types - switch (error.name) { - case NotModifiedError.name: - return 304; - case InputError.name: - return 400; - case AuthenticationError.name: - return 401; - case NotAllowedError.name: - return 403; - case NotFoundError.name: - return 404; - case ConflictError.name: - return 409; - default: - break; - } - - // Fall back to internal server error - return 500; } diff --git a/packages/backend-common/src/middleware/notFoundHandler.ts b/packages/backend-common/src/middleware/notFoundHandler.ts index 2d8b0ef3d0..4f29b4f824 100644 --- a/packages/backend-common/src/middleware/notFoundHandler.ts +++ b/packages/backend-common/src/middleware/notFoundHandler.ts @@ -14,7 +14,10 @@ * limitations under the License. */ -import { NextFunction, Request, RequestHandler, Response } from 'express'; +import { MiddlewareFactory } from '@backstage/backend-app-api'; +import { ConfigReader } from '@backstage/config'; +import { RequestHandler } from 'express'; +import { getRootLogger } from '../logging'; /** * Express middleware to handle requests for missing routes. @@ -26,8 +29,8 @@ import { NextFunction, Request, RequestHandler, Response } from 'express'; * @returns An Express request handler */ export function notFoundHandler(): RequestHandler { - /* eslint-disable @typescript-eslint/no-unused-vars */ - return (_request: Request, response: Response, _next: NextFunction) => { - response.status(404).end(); - }; + return MiddlewareFactory.create({ + config: new ConfigReader({}), + logger: getRootLogger(), + }).notFound(); } diff --git a/packages/backend-common/src/middleware/requestLoggingHandler.ts b/packages/backend-common/src/middleware/requestLoggingHandler.ts index 1618f43ced..a03e3c31f6 100644 --- a/packages/backend-common/src/middleware/requestLoggingHandler.ts +++ b/packages/backend-common/src/middleware/requestLoggingHandler.ts @@ -14,10 +14,11 @@ * limitations under the License. */ +import { MiddlewareFactory } from '@backstage/backend-app-api'; import { RequestHandler } from 'express'; import { LoggerService } from '@backstage/backend-plugin-api'; -import morgan from 'morgan'; import { getRootLogger } from '../logging'; +import { ConfigReader } from '@backstage/config'; /** * Logs incoming requests. @@ -27,15 +28,8 @@ import { getRootLogger } from '../logging'; * @returns An Express request handler */ export function requestLoggingHandler(logger?: LoggerService): RequestHandler { - const actualLogger = (logger || getRootLogger()).child({ - type: 'incomingRequest', - }); - - return morgan('combined', { - stream: { - write(message: string) { - actualLogger.info(message.trimEnd()); - }, - }, - }); + return MiddlewareFactory.create({ + config: new ConfigReader({}), + logger: logger ?? getRootLogger(), + }).logging(); } diff --git a/yarn.lock b/yarn.lock index ed60523406..fe2bc0bcca 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3413,6 +3413,7 @@ __metadata: version: 0.0.0-use.local resolution: "@backstage/backend-common@workspace:packages/backend-common" dependencies: + "@backstage/backend-app-api": "workspace:^" "@backstage/backend-plugin-api": "workspace:^" "@backstage/backend-test-utils": "workspace:^" "@backstage/cli": "workspace:^" From 011c8a8ff16014b4e5bbdbd4b16d6d995e996efd Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 9 Jan 2023 16:04:57 +0100 Subject: [PATCH 35/55] backend-common: use readHttpServerOptions to read SingleHostDiscovery options MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fredrik Adelöw Co-authored-by: blam Co-authored-by: Johan Haals Signed-off-by: Patrik Oldsberg --- .../backend-common/src/discovery/SingleHostDiscovery.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/backend-common/src/discovery/SingleHostDiscovery.ts b/packages/backend-common/src/discovery/SingleHostDiscovery.ts index cc23a2f744..ded23b3374 100644 --- a/packages/backend-common/src/discovery/SingleHostDiscovery.ts +++ b/packages/backend-common/src/discovery/SingleHostDiscovery.ts @@ -16,8 +16,7 @@ import { Config } from '@backstage/config'; import { PluginEndpointDiscovery } from './types'; -import { readBaseOptions } from '../service/lib/config'; -import { DEFAULT_PORT } from '../service/lib/ServiceBuilderImpl'; +import { readHttpServerOptions } from '@backstage/backend-app-api'; /** * SingleHostDiscovery is a basic PluginEndpointDiscovery implementation @@ -43,9 +42,9 @@ export class SingleHostDiscovery implements PluginEndpointDiscovery { const basePath = options?.basePath ?? '/api'; const externalBaseUrl = config.getString('backend.baseUrl'); - const { listenHost = '::', listenPort = DEFAULT_PORT } = readBaseOptions( - config.getConfig('backend'), - ); + 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 From ee36ee8d00de4ecb27bd1bff16eae69d2a806f8a Mon Sep 17 00:00:00 2001 From: Morgan Bentell Date: Mon, 9 Jan 2023 16:26:29 +0100 Subject: [PATCH 36/55] fix logic Signed-off-by: Morgan Bentell --- packages/techdocs-cli/src/commands/index.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/techdocs-cli/src/commands/index.ts b/packages/techdocs-cli/src/commands/index.ts index b951699104..d4303ad306 100644 --- a/packages/techdocs-cli/src/commands/index.ts +++ b/packages/techdocs-cli/src/commands/index.ts @@ -18,6 +18,7 @@ import { Command } from 'commander'; import { TechdocsGenerator } from '@backstage/plugin-techdocs-node'; const defaultDockerImage = TechdocsGenerator.defaultDockerImage; +const defaultPreviewAppPort = '3000'; export function registerCommands(program: Command) { program @@ -258,11 +259,11 @@ export function registerCommands(program: Command) { .option( '--preview-app-port ', 'Port for the preview app to be served on', - '3000', + defaultPreviewAppPort, ) .hook('preAction', command => { if ( - command.opts().previewAppPort && + command.opts().previewAppPort !== defaultPreviewAppPort && !command.opts().previewAppBundlePath ) { command.error( From 0c6323ae8fd7f4daebdff613aeef2b6e0cff5712 Mon Sep 17 00:00:00 2001 From: Morgan Bentell Date: Mon, 9 Jan 2023 16:42:56 +0100 Subject: [PATCH 37/55] fix picky cli-report Signed-off-by: Morgan Bentell --- packages/techdocs-cli/cli-report.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/techdocs-cli/cli-report.md b/packages/techdocs-cli/cli-report.md index ada61f2542..9230fe9f1c 100644 --- a/packages/techdocs-cli/cli-report.md +++ b/packages/techdocs-cli/cli-report.md @@ -99,9 +99,9 @@ Options: --docker-option --no-docker --mkdocs-port + -v --verbose --preview-app-bundle-path --preview-app-port - -v --verbose -h, --help ``` From c95291d14c12ad10ffd7910ad931a2663d147ffb Mon Sep 17 00:00:00 2001 From: Brian Fletcher Date: Mon, 9 Jan 2023 16:02:57 +0000 Subject: [PATCH 38/55] address review comments Signed-off-by: Brian Fletcher --- .../src/components/ActionsPage/ActionsPage.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/plugins/scaffolder/src/components/ActionsPage/ActionsPage.tsx b/plugins/scaffolder/src/components/ActionsPage/ActionsPage.tsx index d727923d68..34cf58441f 100644 --- a/plugins/scaffolder/src/components/ActionsPage/ActionsPage.tsx +++ b/plugins/scaffolder/src/components/ActionsPage/ActionsPage.tsx @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React from 'react'; +import React, { Fragment } from 'react'; import useAsync from 'react-use/lib/useAsync'; import { scaffolderApiRef } from '../../api'; import { @@ -80,13 +80,13 @@ const ExamplesTable = (props: { examples: ActionExample[] }) => { {props.examples.map(example => { return ( - <> - + + {example.description} - + { /> - + ); })} From a70b64b4889c66be46556386b5cb7d2c91f6882c Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 9 Jan 2023 17:22:14 +0100 Subject: [PATCH 39/55] backend-app-api: move cors and helmet options reader to http folder Signed-off-by: Patrik Oldsberg --- packages/backend-app-api/src/lib/http/MiddlewareFactory.ts | 4 ++-- packages/backend-app-api/src/lib/http/index.ts | 2 ++ .../rootHttpRouter => lib/http}/readCorsOptions.test.ts | 0 .../rootHttpRouter => lib/http}/readCorsOptions.ts | 0 .../rootHttpRouter => lib/http}/readHelmetOptions.test.ts | 0 .../rootHttpRouter => lib/http}/readHelmetOptions.ts | 0 6 files changed, 4 insertions(+), 2 deletions(-) rename packages/backend-app-api/src/{services/implementations/rootHttpRouter => lib/http}/readCorsOptions.test.ts (100%) rename packages/backend-app-api/src/{services/implementations/rootHttpRouter => lib/http}/readCorsOptions.ts (100%) rename packages/backend-app-api/src/{services/implementations/rootHttpRouter => lib/http}/readHelmetOptions.test.ts (100%) rename packages/backend-app-api/src/{services/implementations/rootHttpRouter => lib/http}/readHelmetOptions.ts (100%) diff --git a/packages/backend-app-api/src/lib/http/MiddlewareFactory.ts b/packages/backend-app-api/src/lib/http/MiddlewareFactory.ts index ccc94779b0..4be8467db3 100644 --- a/packages/backend-app-api/src/lib/http/MiddlewareFactory.ts +++ b/packages/backend-app-api/src/lib/http/MiddlewareFactory.ts @@ -20,8 +20,8 @@ import cors from 'cors'; import helmet from 'helmet'; import morgan from 'morgan'; import compression from 'compression'; -import { readHelmetOptions } from '../../services/implementations/rootHttpRouter/readHelmetOptions'; -import { readCorsOptions } from '../../services/implementations/rootHttpRouter/readCorsOptions'; +import { readHelmetOptions } from './readHelmetOptions'; +import { readCorsOptions } from './readCorsOptions'; import { AuthenticationError, ConflictError, diff --git a/packages/backend-app-api/src/lib/http/index.ts b/packages/backend-app-api/src/lib/http/index.ts index db579d1b44..8f1a8c5d92 100644 --- a/packages/backend-app-api/src/lib/http/index.ts +++ b/packages/backend-app-api/src/lib/http/index.ts @@ -24,4 +24,6 @@ export type { ExtendedHttpServer, } from './types'; export { MiddlewareFactory } from './MiddlewareFactory'; +export { readHelmetOptions } from './readHelmetOptions'; +export { readCorsOptions } from './readCorsOptions'; export type { MiddlewareFactoryOptions } from './MiddlewareFactory'; diff --git a/packages/backend-app-api/src/services/implementations/rootHttpRouter/readCorsOptions.test.ts b/packages/backend-app-api/src/lib/http/readCorsOptions.test.ts similarity index 100% rename from packages/backend-app-api/src/services/implementations/rootHttpRouter/readCorsOptions.test.ts rename to packages/backend-app-api/src/lib/http/readCorsOptions.test.ts diff --git a/packages/backend-app-api/src/services/implementations/rootHttpRouter/readCorsOptions.ts b/packages/backend-app-api/src/lib/http/readCorsOptions.ts similarity index 100% rename from packages/backend-app-api/src/services/implementations/rootHttpRouter/readCorsOptions.ts rename to packages/backend-app-api/src/lib/http/readCorsOptions.ts diff --git a/packages/backend-app-api/src/services/implementations/rootHttpRouter/readHelmetOptions.test.ts b/packages/backend-app-api/src/lib/http/readHelmetOptions.test.ts similarity index 100% rename from packages/backend-app-api/src/services/implementations/rootHttpRouter/readHelmetOptions.test.ts rename to packages/backend-app-api/src/lib/http/readHelmetOptions.test.ts diff --git a/packages/backend-app-api/src/services/implementations/rootHttpRouter/readHelmetOptions.ts b/packages/backend-app-api/src/lib/http/readHelmetOptions.ts similarity index 100% rename from packages/backend-app-api/src/services/implementations/rootHttpRouter/readHelmetOptions.ts rename to packages/backend-app-api/src/lib/http/readHelmetOptions.ts From 56d7066e8588d8b7ce3a9c3928dff6f0a0c74a45 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 9 Jan 2023 17:23:53 +0100 Subject: [PATCH 40/55] backend-common: reimplement service builder using backend-app-api MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: blam Co-authored-by: Johan Haals Co-authored-by: Fredrik Adelöw Signed-off-by: Patrik Oldsberg --- .../src/service/lib/ServiceBuilderImpl.ts | 152 ++++++------------ 1 file changed, 53 insertions(+), 99 deletions(-) diff --git a/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts b/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts index a3551416dd..f74dcd9b64 100644 --- a/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts +++ b/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts @@ -18,10 +18,9 @@ import { Config } from '@backstage/config'; import compression from 'compression'; import cors from 'cors'; import express, { Router, ErrorRequestHandler } from 'express'; -import helmet from 'helmet'; +import helmet, { HelmetOptions } from 'helmet'; import { ContentSecurityPolicyOptions } from 'helmet/dist/types/middlewares/content-security-policy'; import * as http from 'http'; -import stoppable from 'stoppable'; import { LoggerService } from '@backstage/backend-plugin-api'; import { useHotCleanup } from '../../hot'; import { getRootLogger } from '../../logging'; @@ -32,26 +31,20 @@ import { } from '../../middleware'; import { RequestLoggingHandlerFactory, ServiceBuilder } from '../types'; import { - CspOptions, - HttpsSettings, - readBaseOptions, readCorsOptions, - readCspOptions, - readHttpsSettings, -} from './config'; -import { createHttpServer, createHttpsServer } from './hostFactory'; + readHelmetOptions, + readHttpServerOptions, + HttpServerOptions, + createHttpServer, +} from '@backstage/backend-app-api'; -export const DEFAULT_PORT = 7007; -// '' is express default, which listens to all interfaces -const DEFAULT_HOST = ''; +export type CspOptions = Record; export class ServiceBuilderImpl implements ServiceBuilder { - private port: number | undefined; - private host: string | undefined; private logger: LoggerService | undefined; - private corsOptions: cors.CorsOptions | undefined; - private cspOptions: Record | undefined; - private httpsSettings: HttpsSettings | undefined; + private serverOptions: HttpServerOptions; + private helmetOptions: HelmetOptions; + private corsOptions: cors.CorsOptions; private routers: [string, Router][]; private requestLoggingHandler: RequestLoggingHandlerFactory | undefined; private errorHandler: ErrorRequestHandler | undefined; @@ -64,50 +57,29 @@ export class ServiceBuilderImpl implements ServiceBuilder { this.routers = []; this.module = moduleRef; this.useDefaultErrorHandler = true; + + this.serverOptions = readHttpServerOptions(); + this.corsOptions = readCorsOptions(); + this.helmetOptions = readHelmetOptions(); } loadConfig(config: Config): ServiceBuilder { const backendConfig = config.getOptionalConfig('backend'); - if (!backendConfig) { - return this; - } - const baseOptions = readBaseOptions(backendConfig); - if (baseOptions.listenPort) { - this.port = - typeof baseOptions.listenPort === 'string' - ? parseInt(baseOptions.listenPort, 10) - : baseOptions.listenPort; - } - if (baseOptions.listenHost) { - this.host = baseOptions.listenHost; - } - - const corsOptions = readCorsOptions(backendConfig); - if (corsOptions) { - this.corsOptions = corsOptions; - } - - const cspOptions = readCspOptions(backendConfig); - if (cspOptions) { - this.cspOptions = cspOptions; - } - - const httpsSettings = readHttpsSettings(backendConfig); - if (httpsSettings) { - this.httpsSettings = httpsSettings; - } + this.serverOptions = readHttpServerOptions(backendConfig); + this.corsOptions = readCorsOptions(backendConfig); + this.helmetOptions = readHelmetOptions(backendConfig); return this; } setPort(port: number): ServiceBuilder { - this.port = port; + this.serverOptions.listen.port = port; return this; } setHost(host: string): ServiceBuilder { - this.host = host; + this.serverOptions.listen.host = host; return this; } @@ -116,8 +88,24 @@ export class ServiceBuilderImpl implements ServiceBuilder { return this; } - setHttpsSettings(settings: HttpsSettings): ServiceBuilder { - this.httpsSettings = settings; + setHttpsSettings(settings: { + certificate: { key: string; cert: string } | { hostname: string }; + }): ServiceBuilder { + if ('hostname' in settings.certificate) { + this.serverOptions.https = { + certificate: { + ...settings.certificate, + type: 'generated', + }, + }; + } else { + this.serverOptions.https = { + certificate: { + ...settings.certificate, + type: 'plain', + }, + }; + } return this; } @@ -127,7 +115,11 @@ export class ServiceBuilderImpl implements ServiceBuilder { } setCsp(options: CspOptions): ServiceBuilder { - this.cspOptions = options; + const csp = this.helmetOptions.contentSecurityPolicy; + this.helmetOptions.contentSecurityPolicy = { + ...(typeof csp === 'object' ? csp : {}), + directives: applyCspDirectives(options), + }; return this; } @@ -155,13 +147,10 @@ export class ServiceBuilderImpl implements ServiceBuilder { async start(): Promise { const app = express(); - const { port, host, logger, corsOptions, httpsSettings, helmetOptions } = - this.getOptions(); + const logger = this.logger ?? getRootLogger(); - app.use(helmet(helmetOptions)); - if (corsOptions) { - app.use(cors(corsOptions)); - } + app.use(helmet(this.helmetOptions)); + app.use(cors(this.corsOptions)); app.use(compression()); app.use( (this.requestLoggingHandler ?? defaultRequestLoggingHandler)(logger), @@ -179,58 +168,23 @@ export class ServiceBuilderImpl implements ServiceBuilder { app.use(defaultErrorHandler()); } - const server: http.Server = httpsSettings - ? await createHttpsServer(app, httpsSettings, logger) - : createHttpServer(app, logger); - const stoppableServer = stoppable(server, 0); + const server = await createHttpServer(app, this.serverOptions, { logger }); useHotCleanup(this.module, () => - stoppableServer.stop((e: any) => { - if (e) console.error(e); + server.stop().catch(error => { + console.error(error); }), ); - return new Promise((resolve, reject) => { - function handleStartupError(e: unknown) { - server.close(); - reject(e); - } + await server.start(); - server.on('error', handleStartupError); - - server.listen(port, host, () => { - server.off('error', handleStartupError); - logger.info(`Listening on ${host}:${port}`); - resolve(stoppableServer); - }); - }); - } - - private getOptions() { - return { - port: this.port ?? DEFAULT_PORT, - host: this.host ?? DEFAULT_HOST, - logger: this.logger ?? getRootLogger(), - corsOptions: this.corsOptions, - httpsSettings: this.httpsSettings, - helmetOptions: { - contentSecurityPolicy: { - useDefaults: false, - directives: applyCspDirectives(this.cspOptions), - }, - // These are all disabled in order to maintain backwards compatibility - // when bumping helmet v5. We can't enable these by default because - // there is no way for users to configure them. - // TODO(Rugvip): We should give control of this setup to consumers - crossOriginEmbedderPolicy: false, - crossOriginOpenerPolicy: false, - crossOriginResourcePolicy: false, - originAgentCluster: false, - }, - }; + return server; } } +// TODO(Rugvip): This is a duplicate of the same logic over in backend-app-api. +// It's needed as we don't want to export this helper from there, but need +// It to implement the setCsp method here. export function applyCspDirectives( directives: Record | undefined, ): ContentSecurityPolicyOptions['directives'] { From 243f38de5a14ca01f5d5b4d44e3bc242939d6cdb Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 9 Jan 2023 17:24:33 +0100 Subject: [PATCH 41/55] backend-app-api: move http dir out of lib Signed-off-by: Patrik Oldsberg --- .../src/{lib => }/http/MiddlewareFactory.test.ts | 0 .../backend-app-api/src/{lib => }/http/MiddlewareFactory.ts | 0 packages/backend-app-api/src/{lib => }/http/config.test.ts | 0 packages/backend-app-api/src/{lib => }/http/config.ts | 0 packages/backend-app-api/src/{lib => }/http/createHttpServer.ts | 0 .../src/{lib => }/http/getGeneratedCertificate.ts | 0 packages/backend-app-api/src/{lib => }/http/index.ts | 0 .../backend-app-api/src/{lib => }/http/readCorsOptions.test.ts | 0 packages/backend-app-api/src/{lib => }/http/readCorsOptions.ts | 0 .../src/{lib => }/http/readHelmetOptions.test.ts | 0 .../backend-app-api/src/{lib => }/http/readHelmetOptions.ts | 0 packages/backend-app-api/src/{lib => }/http/startHttpServer.ts | 0 packages/backend-app-api/src/{lib => }/http/types.ts | 0 packages/backend-app-api/src/index.ts | 2 +- .../implementations/rootHttpRouter/rootHttpRouterFactory.ts | 2 +- 15 files changed, 2 insertions(+), 2 deletions(-) rename packages/backend-app-api/src/{lib => }/http/MiddlewareFactory.test.ts (100%) rename packages/backend-app-api/src/{lib => }/http/MiddlewareFactory.ts (100%) rename packages/backend-app-api/src/{lib => }/http/config.test.ts (100%) rename packages/backend-app-api/src/{lib => }/http/config.ts (100%) rename packages/backend-app-api/src/{lib => }/http/createHttpServer.ts (100%) rename packages/backend-app-api/src/{lib => }/http/getGeneratedCertificate.ts (100%) rename packages/backend-app-api/src/{lib => }/http/index.ts (100%) rename packages/backend-app-api/src/{lib => }/http/readCorsOptions.test.ts (100%) rename packages/backend-app-api/src/{lib => }/http/readCorsOptions.ts (100%) rename packages/backend-app-api/src/{lib => }/http/readHelmetOptions.test.ts (100%) rename packages/backend-app-api/src/{lib => }/http/readHelmetOptions.ts (100%) rename packages/backend-app-api/src/{lib => }/http/startHttpServer.ts (100%) rename packages/backend-app-api/src/{lib => }/http/types.ts (100%) diff --git a/packages/backend-app-api/src/lib/http/MiddlewareFactory.test.ts b/packages/backend-app-api/src/http/MiddlewareFactory.test.ts similarity index 100% rename from packages/backend-app-api/src/lib/http/MiddlewareFactory.test.ts rename to packages/backend-app-api/src/http/MiddlewareFactory.test.ts diff --git a/packages/backend-app-api/src/lib/http/MiddlewareFactory.ts b/packages/backend-app-api/src/http/MiddlewareFactory.ts similarity index 100% rename from packages/backend-app-api/src/lib/http/MiddlewareFactory.ts rename to packages/backend-app-api/src/http/MiddlewareFactory.ts diff --git a/packages/backend-app-api/src/lib/http/config.test.ts b/packages/backend-app-api/src/http/config.test.ts similarity index 100% rename from packages/backend-app-api/src/lib/http/config.test.ts rename to packages/backend-app-api/src/http/config.test.ts diff --git a/packages/backend-app-api/src/lib/http/config.ts b/packages/backend-app-api/src/http/config.ts similarity index 100% rename from packages/backend-app-api/src/lib/http/config.ts rename to packages/backend-app-api/src/http/config.ts diff --git a/packages/backend-app-api/src/lib/http/createHttpServer.ts b/packages/backend-app-api/src/http/createHttpServer.ts similarity index 100% rename from packages/backend-app-api/src/lib/http/createHttpServer.ts rename to packages/backend-app-api/src/http/createHttpServer.ts diff --git a/packages/backend-app-api/src/lib/http/getGeneratedCertificate.ts b/packages/backend-app-api/src/http/getGeneratedCertificate.ts similarity index 100% rename from packages/backend-app-api/src/lib/http/getGeneratedCertificate.ts rename to packages/backend-app-api/src/http/getGeneratedCertificate.ts diff --git a/packages/backend-app-api/src/lib/http/index.ts b/packages/backend-app-api/src/http/index.ts similarity index 100% rename from packages/backend-app-api/src/lib/http/index.ts rename to packages/backend-app-api/src/http/index.ts diff --git a/packages/backend-app-api/src/lib/http/readCorsOptions.test.ts b/packages/backend-app-api/src/http/readCorsOptions.test.ts similarity index 100% rename from packages/backend-app-api/src/lib/http/readCorsOptions.test.ts rename to packages/backend-app-api/src/http/readCorsOptions.test.ts diff --git a/packages/backend-app-api/src/lib/http/readCorsOptions.ts b/packages/backend-app-api/src/http/readCorsOptions.ts similarity index 100% rename from packages/backend-app-api/src/lib/http/readCorsOptions.ts rename to packages/backend-app-api/src/http/readCorsOptions.ts diff --git a/packages/backend-app-api/src/lib/http/readHelmetOptions.test.ts b/packages/backend-app-api/src/http/readHelmetOptions.test.ts similarity index 100% rename from packages/backend-app-api/src/lib/http/readHelmetOptions.test.ts rename to packages/backend-app-api/src/http/readHelmetOptions.test.ts diff --git a/packages/backend-app-api/src/lib/http/readHelmetOptions.ts b/packages/backend-app-api/src/http/readHelmetOptions.ts similarity index 100% rename from packages/backend-app-api/src/lib/http/readHelmetOptions.ts rename to packages/backend-app-api/src/http/readHelmetOptions.ts diff --git a/packages/backend-app-api/src/lib/http/startHttpServer.ts b/packages/backend-app-api/src/http/startHttpServer.ts similarity index 100% rename from packages/backend-app-api/src/lib/http/startHttpServer.ts rename to packages/backend-app-api/src/http/startHttpServer.ts diff --git a/packages/backend-app-api/src/lib/http/types.ts b/packages/backend-app-api/src/http/types.ts similarity index 100% rename from packages/backend-app-api/src/lib/http/types.ts rename to packages/backend-app-api/src/http/types.ts diff --git a/packages/backend-app-api/src/index.ts b/packages/backend-app-api/src/index.ts index a8cc079d47..9527e35919 100644 --- a/packages/backend-app-api/src/index.ts +++ b/packages/backend-app-api/src/index.ts @@ -20,6 +20,6 @@ * @packageDocumentation */ -export * from './lib/http'; +export * from './http'; export * from './wiring'; export * from './services/implementations'; diff --git a/packages/backend-app-api/src/services/implementations/rootHttpRouter/rootHttpRouterFactory.ts b/packages/backend-app-api/src/services/implementations/rootHttpRouter/rootHttpRouterFactory.ts index f82cfe4a69..ebd2945545 100644 --- a/packages/backend-app-api/src/services/implementations/rootHttpRouter/rootHttpRouterFactory.ts +++ b/packages/backend-app-api/src/services/implementations/rootHttpRouter/rootHttpRouterFactory.ts @@ -22,7 +22,7 @@ import { LoggerService, } from '@backstage/backend-plugin-api'; import express, { RequestHandler, Express } from 'express'; -import { MiddlewareFactory, startHttpServer } from '../../../lib/http'; +import { MiddlewareFactory, startHttpServer } from '../../../http'; import { RestrictedIndexedRouter } from './RestrictedIndexedRouter'; interface RootHttpRouterConfigureOptions { From e85e61c15435af2d681bb517e0c918dbb0fa2847 Mon Sep 17 00:00:00 2001 From: Paulo Eduardo Peixoto Date: Mon, 9 Jan 2023 14:16:44 -0300 Subject: [PATCH 42/55] docs(docs/features/software-templates/writing-custom-field-extensions.md): change "kehab" to "kebab". Signed-off-by: Paulo Eduardo Peixoto --- .../writing-custom-field-extensions.md | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/docs/features/software-templates/writing-custom-field-extensions.md b/docs/features/software-templates/writing-custom-field-extensions.md index 24861936c0..be00453795 100644 --- a/docs/features/software-templates/writing-custom-field-extensions.md +++ b/docs/features/software-templates/writing-custom-field-extensions.md @@ -25,17 +25,17 @@ You can create your own Field Extension by using the [`createScaffolderFieldExtension`](https://backstage.io/docs/reference/plugin-scaffolder.createscaffolderfieldextension) `API` like below. -As an example, we will create a component that validates whether a string is in the `Kehab-case` pattern: +As an example, we will create a component that validates whether a string is in the `Kebab-case` pattern: ```tsx -//packages/app/src/scaffolder/ValidateKehabCase/ValidateKehabCaseExtension.tsx +//packages/app/src/scaffolder/ValidateKebabCase/ValidateKebabCaseExtension.tsx import React from 'react'; import { FieldProps, FieldValidation } from '@rjsf/core'; import FormControl from '@material-ui/core/FormControl'; /* This is the actual component that will get rendered in the form */ -export const ValidateKehabCaseExtension = ({ +export const ValidateKebabCaseExtension = ({ onChange, rawErrors, required, @@ -65,13 +65,13 @@ export const ValidateKehabCaseExtension = ({ You will get the value from the `onChange` handler before as the value here to make sure that the types are aligned\ */ -export const validateKehabCaseValidation = ( +export const validateKebabCaseValidation = ( value: string, validation: FieldValidation, ) => { - const kehabCase = /^[a-z0-9-_]+$/g.test(value); + const kebabCase = /^[a-z0-9-_]+$/g.test(value); - if (kehabCase === false) { + if (kebabCase === false) { validation.addError( `Only use letters, numbers, hyphen ("-") and underscore ("_").`, ); @@ -80,7 +80,7 @@ export const validateKehabCaseValidation = ( ``` ```tsx -// packages/app/src/scaffolder/ValidateKehabCase/extensions.ts +// packages/app/src/scaffolder/ValidateKebabCase/extensions.ts /* This is where the magic happens and creates the custom field extension. @@ -94,23 +94,23 @@ import { createScaffolderFieldExtension, } from '@backstage/plugin-scaffolder'; import { - ValidateKehabCase, - validateKehabCaseValidation, -} from './ValidateKehabCase'; + ValidateKebabCase, + validateKebabCaseValidation, +} from './ValidateKebabCase'; -export const ValidateKehabCaseFieldExtension = scaffolderPlugin.provide( +export const ValidateKebabCaseFieldExtension = scaffolderPlugin.provide( createScaffolderFieldExtension({ - name: 'ValidateKehabCase', - component: ValidateKehabCase, - validation: validateKehabCaseValidation, + name: 'ValidateKebabCase', + component: ValidateKebabCase, + validation: validateKebabCaseValidation, }), ); ``` ```tsx -// packages/app/src/scaffolder/ValidateKehabCase/index.ts +// packages/app/src/scaffolder/ValidateKebabCase/index.ts -export { ValidateKehabCaseFieldExtension } from './extensions'; +export { ValidateKebabCaseFieldExtension } from './extensions'; ``` Once all these files are in place, you then need to provide your custom @@ -132,7 +132,7 @@ const routes = ( Should look something like this instead: ```tsx -import { ValidateKehabCaseFieldExtension } from './scaffolder/ValidateKehabCase'; +import { ValidateKebabCaseFieldExtension } from './scaffolder/ValidateKebabCase'; import { ScaffolderFieldExtensions } from '@backstage/plugin-scaffolder'; const routes = ( @@ -140,7 +140,7 @@ const routes = ( ... }> - + ... @@ -173,7 +173,7 @@ spec: title: Name type: string description: My custom name for the component - ui:field: ValidateKehabCaseExtension + ui:field: ValidateKebabCaseExtension steps: [...] ``` From 38fd519fc10207de46c418c2cd16cafc22401792 Mon Sep 17 00:00:00 2001 From: Patrick Knight Date: Mon, 9 Jan 2023 14:36:31 -0500 Subject: [PATCH 43/55] add highlighting of legend item Signed-off-by: Patrick Knight --- .changeset/metal-ducks-promise.md | 5 +++++ plugins/tech-radar/src/components/Radar/utils.ts | 1 + .../src/components/RadarLegend/RadarLegend.tsx | 8 ++++++++ .../src/components/RadarLegend/RadarLegendLink.tsx | 10 ++++++++-- .../src/components/RadarLegend/RadarLegendRing.tsx | 1 + 5 files changed, 23 insertions(+), 2 deletions(-) create mode 100644 .changeset/metal-ducks-promise.md diff --git a/.changeset/metal-ducks-promise.md b/.changeset/metal-ducks-promise.md new file mode 100644 index 0000000000..7d39edff44 --- /dev/null +++ b/.changeset/metal-ducks-promise.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-tech-radar': minor +--- + +Add highlighting of legend item and show bubble on hover within the Tech Radar diff --git a/plugins/tech-radar/src/components/Radar/utils.ts b/plugins/tech-radar/src/components/Radar/utils.ts index 7fff038023..5c32207268 100644 --- a/plugins/tech-radar/src/components/Radar/utils.ts +++ b/plugins/tech-radar/src/components/Radar/utils.ts @@ -141,6 +141,7 @@ export const adjustEntries = ( activeEntry && entry.id === activeEntry?.id ? entry.ring.color : color(entry.ring.color).desaturate(0.5).lighten(0.1).string(), + active: activeEntry && entry.id === activeEntry?.id ? true : false, }; }); diff --git a/plugins/tech-radar/src/components/RadarLegend/RadarLegend.tsx b/plugins/tech-radar/src/components/RadarLegend/RadarLegend.tsx index 6177041c70..4ca0350e7b 100644 --- a/plugins/tech-radar/src/components/RadarLegend/RadarLegend.tsx +++ b/plugins/tech-radar/src/components/RadarLegend/RadarLegend.tsx @@ -68,8 +68,16 @@ const useStyles = makeStyles(theme => ({ userSelect: 'none', fontSize: '11px', }, + activeEntry: { + pointerEvents: 'visiblePainted', + userSelect: 'none', + fontSize: '11px', + background: '#6f6f6f', + color: '#FFF', + }, entryLink: { pointerEvents: 'visiblePainted', + cursor: 'pointer', }, })); diff --git a/plugins/tech-radar/src/components/RadarLegend/RadarLegendLink.tsx b/plugins/tech-radar/src/components/RadarLegend/RadarLegendLink.tsx index 3ed1b7574a..f75f23c12b 100644 --- a/plugins/tech-radar/src/components/RadarLegend/RadarLegendLink.tsx +++ b/plugins/tech-radar/src/components/RadarLegend/RadarLegendLink.tsx @@ -23,6 +23,7 @@ type RadarLegendLinkProps = { description?: string; title?: string; classes: ClassNameMap; + active?: boolean; }; export const RadarLegendLink = ({ @@ -30,6 +31,7 @@ export const RadarLegendLink = ({ description, title, classes, + active, }: RadarLegendLinkProps) => { const [open, setOpen] = React.useState(false); @@ -55,7 +57,9 @@ export const RadarLegendLink = ({ tabIndex={0} onKeyPress={toggle} > - {title} + + {title} + {open && ( - {title} + + {title} + ); }; diff --git a/plugins/tech-radar/src/components/RadarLegend/RadarLegendRing.tsx b/plugins/tech-radar/src/components/RadarLegend/RadarLegendRing.tsx index f54e4ab3b3..9ad8126ac4 100644 --- a/plugins/tech-radar/src/components/RadarLegend/RadarLegendRing.tsx +++ b/plugins/tech-radar/src/components/RadarLegend/RadarLegendRing.tsx @@ -57,6 +57,7 @@ export const RadarLegendRing = ({ url={entry.url} title={entry.title} description={entry.description} + active={entry.active} /> ))} From d92cbd02050fcff15d45bcc053a8484f6c1f7cfd Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 9 Jan 2023 21:44:41 +0100 Subject: [PATCH 44/55] backend-app-api: update API report + fixes Signed-off-by: Patrik Oldsberg --- packages/backend-app-api/api-report.md | 151 +++++++++++++++++- .../src/http/MiddlewareFactory.ts | 2 +- packages/backend-app-api/src/http/index.ts | 17 +- .../src/http/readCorsOptions.ts | 1 + .../src/http/readHelmetOptions.ts | 1 + .../implementations/rootHttpRouter/index.ts | 5 +- .../rootHttpRouter/rootHttpRouterFactory.ts | 5 +- .../src/database/migrateBackendTasks.ts | 10 +- 8 files changed, 175 insertions(+), 17 deletions(-) diff --git a/packages/backend-app-api/api-report.md b/packages/backend-app-api/api-report.md index a402150755..24499f6c86 100644 --- a/packages/backend-app-api/api-report.md +++ b/packages/backend-app-api/api-report.md @@ -3,21 +3,38 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +/// +/// + import { BackendFeature } from '@backstage/backend-plugin-api'; +import { Config } from '@backstage/config'; import { ConfigService } from '@backstage/backend-plugin-api'; +import cors from 'cors'; +import { CorsOptions } from 'cors'; +import { ErrorRequestHandler } from 'express'; +import { Express as Express_2 } from 'express'; import { ExtensionPoint } from '@backstage/backend-plugin-api'; -import { Handler } from 'express'; +import { HelmetOptions } from 'helmet'; +import * as http from 'http'; import { HttpRouterService } from '@backstage/backend-plugin-api'; +import { IncomingMessage } from 'http'; import { LifecycleService } from '@backstage/backend-plugin-api'; import { LoggerService } from '@backstage/backend-plugin-api'; +import { ParamsDictionary } from 'express-serve-static-core'; +import { ParsedQs } from 'qs'; import { PermissionsService } from '@backstage/backend-plugin-api'; import { PluginCacheManager } from '@backstage/backend-common'; import { PluginDatabaseManager } from '@backstage/backend-common'; import { PluginEndpointDiscovery } from '@backstage/backend-common'; +import { Request as Request_2 } from 'express'; +import { RequestHandler } from 'express'; +import { RequestListener } from 'http'; +import { Response as Response_2 } from 'express'; import { RootHttpRouterService } from '@backstage/backend-plugin-api'; import { RootLifecycleService } from '@backstage/backend-plugin-api'; import { RootLoggerService } from '@backstage/backend-plugin-api'; import { SchedulerService } from '@backstage/backend-plugin-api'; +import { ServerResponse } from 'http'; import { ServiceFactory } from '@backstage/backend-plugin-api'; import { ServiceFactoryOrFunction } from '@backstage/backend-plugin-api'; import { ServiceRef } from '@backstage/backend-plugin-api'; @@ -44,6 +61,15 @@ export const configFactory: ( options?: undefined, ) => ServiceFactory; +// @public +export function createHttpServer( + listener: RequestListener, + options: HttpServerOptions, + deps: { + logger: LoggerService; + }, +): Promise; + // @public (undocumented) export function createSpecializedBackend( options: CreateSpecializedBackendOptions, @@ -65,6 +91,16 @@ export const discoveryFactory: ( options?: undefined, ) => ServiceFactory; +// @public +export interface ExtendedHttpServer extends http.Server { + // (undocumented) + port(): number; + // (undocumented) + start(): Promise; + // (undocumented) + stop(): Promise; +} + // @public (undocumented) export const httpRouterFactory: ( options?: HttpRouterFactoryOptions | undefined, @@ -75,6 +111,29 @@ export type HttpRouterFactoryOptions = { getPath(pluginId: string): string; }; +// @public +export type HttpServerCertificateOptions = + | { + type: 'plain'; + key: string; + cert: string; + } + | { + type: 'generated'; + hostname: string; + }; + +// @public +export type HttpServerOptions = { + listen: { + port: number; + host: string; + }; + https?: { + certificate: HttpServerCertificateOptions; + }; +}; + // @public export const lifecycleFactory: ( options?: undefined, @@ -85,11 +144,83 @@ export const loggerFactory: ( options?: undefined, ) => ServiceFactory; +// @public +export class MiddlewareFactory { + compression(): RequestHandler< + ParamsDictionary, + any, + any, + ParsedQs, + Record + >; + cors(): ( + req: cors.CorsRequest, + res: { + statusCode?: number | undefined; + setHeader(key: string, value: string): any; + end(): any; + }, + next: (err?: any) => any, + ) => void; + static create(options: MiddlewareFactoryOptions): MiddlewareFactory; + error(options?: MiddlewareFactoryErrorOptions): ErrorRequestHandler; + helmet(): ( + req: IncomingMessage, + res: ServerResponse, + next: (err?: unknown) => void, + ) => void; + logging(): ( + req: IncomingMessage, + res: ServerResponse, + callback: (err?: Error | undefined) => void, + ) => void; + notFound(): (_req: Request_2, res: Response_2) => void; +} + +// @public +export interface MiddlewareFactoryErrorOptions { + logAllErrors?: boolean; + showStackTraces?: boolean; +} + +// @public +export interface MiddlewareFactoryOptions { + // (undocumented) + config: ConfigService; + // (undocumented) + logger: LoggerService; +} + // @public (undocumented) export const permissionsFactory: ( options?: undefined, ) => ServiceFactory; +// @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 RootHttpRouterConfigureOptions { + // (undocumented) + app: Express_2; + // (undocumented) + config: ConfigService; + // (undocumented) + lifecycle: LifecycleService; + // (undocumented) + logger: LoggerService; + // (undocumented) + middleware: MiddlewareFactory; + // (undocumented) + routes: RequestHandler; +} + // @public (undocumented) export const rootHttpRouterFactory: ( options?: RootHttpRouterFactoryOptions | undefined, @@ -98,7 +229,7 @@ export const rootHttpRouterFactory: ( // @public (undocumented) export type RootHttpRouterFactoryOptions = { indexPath?: string | false; - middleware?: Handler[]; + configure?(options: RootHttpRouterConfigureOptions): void; }; // @public @@ -121,6 +252,22 @@ export type ServiceOrExtensionPoint = | ExtensionPoint | ServiceRef; +// @public +export function startHttpServer( + listener: RequestListener, + options: StartHttpServerOptions, +): Promise; + +// @public +export interface StartHttpServerOptions { + // (undocumented) + config: ConfigService; + // (undocumented) + lifecycle: RootLifecycleService; + // (undocumented) + logger: RootLoggerService; +} + // @public (undocumented) export const tokenManagerFactory: ( options?: undefined, diff --git a/packages/backend-app-api/src/http/MiddlewareFactory.ts b/packages/backend-app-api/src/http/MiddlewareFactory.ts index 4be8467db3..09e6709793 100644 --- a/packages/backend-app-api/src/http/MiddlewareFactory.ts +++ b/packages/backend-app-api/src/http/MiddlewareFactory.ts @@ -44,7 +44,7 @@ export interface MiddlewareFactoryOptions { } /** - * Options passed to the {@link errorHandler} middleware. + * Options passed to the {@link MiddlewareFactory.error} middleware. * * @public */ diff --git a/packages/backend-app-api/src/http/index.ts b/packages/backend-app-api/src/http/index.ts index 8f1a8c5d92..5bd54ec4a5 100644 --- a/packages/backend-app-api/src/http/index.ts +++ b/packages/backend-app-api/src/http/index.ts @@ -14,16 +14,19 @@ * 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 { startHttpServer } from './startHttpServer'; export type { StartHttpServerOptions } from './startHttpServer'; -export { readHttpServerOptions } from './config'; export type { - HttpServerOptions, - HttpServerCertificateOptions, ExtendedHttpServer, + HttpServerCertificateOptions, + HttpServerOptions, } from './types'; -export { MiddlewareFactory } from './MiddlewareFactory'; -export { readHelmetOptions } from './readHelmetOptions'; -export { readCorsOptions } from './readCorsOptions'; -export type { MiddlewareFactoryOptions } from './MiddlewareFactory'; diff --git a/packages/backend-app-api/src/http/readCorsOptions.ts b/packages/backend-app-api/src/http/readCorsOptions.ts index 98b548d02e..b58cae5ded 100644 --- a/packages/backend-app-api/src/http/readCorsOptions.ts +++ b/packages/backend-app-api/src/http/readCorsOptions.ts @@ -21,6 +21,7 @@ import { Minimatch } from 'minimatch'; /** * Attempts to read a CORS options object from the backend configuration object. * + * @public * @param config - The backend configuration object. * @returns A CORS options object, or undefined if no cors configuration is present. * diff --git a/packages/backend-app-api/src/http/readHelmetOptions.ts b/packages/backend-app-api/src/http/readHelmetOptions.ts index 9cdf339844..0555bdef00 100644 --- a/packages/backend-app-api/src/http/readHelmetOptions.ts +++ b/packages/backend-app-api/src/http/readHelmetOptions.ts @@ -22,6 +22,7 @@ import { ContentSecurityPolicyOptions } from 'helmet/dist/types/middlewares/cont /** * Attempts to read Helmet options from the backend configuration object. * + * @public * @param config - The backend configuration object. * @returns A Helmet options object, or undefined if no Helmet configuration is present. * diff --git a/packages/backend-app-api/src/services/implementations/rootHttpRouter/index.ts b/packages/backend-app-api/src/services/implementations/rootHttpRouter/index.ts index 9b72f17060..1dfd72273d 100644 --- a/packages/backend-app-api/src/services/implementations/rootHttpRouter/index.ts +++ b/packages/backend-app-api/src/services/implementations/rootHttpRouter/index.ts @@ -15,4 +15,7 @@ */ export { rootHttpRouterFactory } from './rootHttpRouterFactory'; -export type { RootHttpRouterFactoryOptions } from './rootHttpRouterFactory'; +export type { + RootHttpRouterFactoryOptions, + RootHttpRouterConfigureOptions, +} from './rootHttpRouterFactory'; diff --git a/packages/backend-app-api/src/services/implementations/rootHttpRouter/rootHttpRouterFactory.ts b/packages/backend-app-api/src/services/implementations/rootHttpRouter/rootHttpRouterFactory.ts index ebd2945545..284b019a2e 100644 --- a/packages/backend-app-api/src/services/implementations/rootHttpRouter/rootHttpRouterFactory.ts +++ b/packages/backend-app-api/src/services/implementations/rootHttpRouter/rootHttpRouterFactory.ts @@ -25,7 +25,10 @@ import express, { RequestHandler, Express } from 'express'; import { MiddlewareFactory, startHttpServer } from '../../../http'; import { RestrictedIndexedRouter } from './RestrictedIndexedRouter'; -interface RootHttpRouterConfigureOptions { +/** + * @public + */ +export interface RootHttpRouterConfigureOptions { app: Express; middleware: MiddlewareFactory; routes: RequestHandler; diff --git a/packages/backend-tasks/src/database/migrateBackendTasks.ts b/packages/backend-tasks/src/database/migrateBackendTasks.ts index cc0f4349ac..9530668e3e 100644 --- a/packages/backend-tasks/src/database/migrateBackendTasks.ts +++ b/packages/backend-tasks/src/database/migrateBackendTasks.ts @@ -18,12 +18,12 @@ import { resolvePackagePath } from '@backstage/backend-common'; import { Knex } from 'knex'; import { DB_MIGRATIONS_TABLE } from './tables'; -const migrationsDir = resolvePackagePath( - '@backstage/backend-tasks', - 'migrations', -); - export async function migrateBackendTasks(knex: Knex): Promise { + const migrationsDir = resolvePackagePath( + '@backstage/backend-tasks', + 'migrations', + ); + await knex.migrate.latest({ directory: migrationsDir, tableName: DB_MIGRATIONS_TABLE, From b99c030f1b369f0b8fb7c66692f95f38c88fff20 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 9 Jan 2023 21:48:21 +0100 Subject: [PATCH 45/55] added changesets for the root API refactor and move from backend-common Signed-off-by: Patrik Oldsberg --- .changeset/fair-seals-fry.md | 5 +++++ .changeset/kind-badgers-know.md | 5 +++++ .changeset/orange-spoons-approve.md | 5 +++++ 3 files changed, 15 insertions(+) create mode 100644 .changeset/fair-seals-fry.md create mode 100644 .changeset/kind-badgers-know.md create mode 100644 .changeset/orange-spoons-approve.md diff --git a/.changeset/fair-seals-fry.md b/.changeset/fair-seals-fry.md new file mode 100644 index 0000000000..09b8970357 --- /dev/null +++ b/.changeset/fair-seals-fry.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-app-api': patch +--- + +Moved over implementation of the root HTTP service from `@backstage/backend-common`, and replaced the `middleware` option with a `configure` callback option. diff --git a/.changeset/kind-badgers-know.md b/.changeset/kind-badgers-know.md new file mode 100644 index 0000000000..e01351cb5e --- /dev/null +++ b/.changeset/kind-badgers-know.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-tasks': patch +--- + +Minor internal refactor to avoid import cycle issue. diff --git a/.changeset/orange-spoons-approve.md b/.changeset/orange-spoons-approve.md new file mode 100644 index 0000000000..96b1eda6fc --- /dev/null +++ b/.changeset/orange-spoons-approve.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-common': patch +--- + +Refactor to rely on `@backstage/backend-app-api` for the implementation of `createServiceBuilder`. From c028fa124d654a29b421fc83988b13bb4c401f3b Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 9 Jan 2023 22:09:39 +0100 Subject: [PATCH 46/55] backend-app-api: explicit request handler types for MiddlewareFactory Signed-off-by: Patrik Oldsberg --- packages/backend-app-api/api-report.md | 40 +++---------------- .../src/http/MiddlewareFactory.ts | 18 ++++++--- 2 files changed, 17 insertions(+), 41 deletions(-) diff --git a/packages/backend-app-api/api-report.md b/packages/backend-app-api/api-report.md index 24499f6c86..18c2c52370 100644 --- a/packages/backend-app-api/api-report.md +++ b/packages/backend-app-api/api-report.md @@ -4,12 +4,10 @@ ```ts /// -/// import { BackendFeature } from '@backstage/backend-plugin-api'; import { Config } from '@backstage/config'; import { ConfigService } from '@backstage/backend-plugin-api'; -import cors from 'cors'; import { CorsOptions } from 'cors'; import { ErrorRequestHandler } from 'express'; import { Express as Express_2 } from 'express'; @@ -17,24 +15,18 @@ import { ExtensionPoint } from '@backstage/backend-plugin-api'; import { HelmetOptions } from 'helmet'; import * as http from 'http'; import { HttpRouterService } from '@backstage/backend-plugin-api'; -import { IncomingMessage } from 'http'; import { LifecycleService } from '@backstage/backend-plugin-api'; import { LoggerService } from '@backstage/backend-plugin-api'; -import { ParamsDictionary } from 'express-serve-static-core'; -import { ParsedQs } from 'qs'; import { PermissionsService } from '@backstage/backend-plugin-api'; import { PluginCacheManager } from '@backstage/backend-common'; import { PluginDatabaseManager } from '@backstage/backend-common'; import { PluginEndpointDiscovery } from '@backstage/backend-common'; -import { Request as Request_2 } from 'express'; import { RequestHandler } from 'express'; import { RequestListener } from 'http'; -import { Response as Response_2 } from 'express'; import { RootHttpRouterService } from '@backstage/backend-plugin-api'; import { RootLifecycleService } from '@backstage/backend-plugin-api'; import { RootLoggerService } from '@backstage/backend-plugin-api'; import { SchedulerService } from '@backstage/backend-plugin-api'; -import { ServerResponse } from 'http'; import { ServiceFactory } from '@backstage/backend-plugin-api'; import { ServiceFactoryOrFunction } from '@backstage/backend-plugin-api'; import { ServiceRef } from '@backstage/backend-plugin-api'; @@ -146,35 +138,13 @@ export const loggerFactory: ( // @public export class MiddlewareFactory { - compression(): RequestHandler< - ParamsDictionary, - any, - any, - ParsedQs, - Record - >; - cors(): ( - req: cors.CorsRequest, - res: { - statusCode?: number | undefined; - setHeader(key: string, value: string): any; - end(): any; - }, - next: (err?: any) => any, - ) => void; + compression(): RequestHandler; + cors(): RequestHandler; static create(options: MiddlewareFactoryOptions): MiddlewareFactory; error(options?: MiddlewareFactoryErrorOptions): ErrorRequestHandler; - helmet(): ( - req: IncomingMessage, - res: ServerResponse, - next: (err?: unknown) => void, - ) => void; - logging(): ( - req: IncomingMessage, - res: ServerResponse, - callback: (err?: Error | undefined) => void, - ) => void; - notFound(): (_req: Request_2, res: Response_2) => void; + helmet(): RequestHandler; + logging(): RequestHandler; + notFound(): RequestHandler; } // @public diff --git a/packages/backend-app-api/src/http/MiddlewareFactory.ts b/packages/backend-app-api/src/http/MiddlewareFactory.ts index 09e6709793..dc9fe9aa5f 100644 --- a/packages/backend-app-api/src/http/MiddlewareFactory.ts +++ b/packages/backend-app-api/src/http/MiddlewareFactory.ts @@ -15,7 +15,13 @@ */ import { ConfigService, LoggerService } from '@backstage/backend-plugin-api'; -import { Request, Response, ErrorRequestHandler, NextFunction } from 'express'; +import { + Request, + Response, + ErrorRequestHandler, + NextFunction, + RequestHandler, +} from 'express'; import cors from 'cors'; import helmet from 'helmet'; import morgan from 'morgan'; @@ -95,7 +101,7 @@ export class MiddlewareFactory { * * @returns An Express request handler */ - notFound() { + notFound(): RequestHandler { return (_req: Request, res: Response) => { res.status(404).end(); }; @@ -109,7 +115,7 @@ export class MiddlewareFactory { * The middleware will attempt to compress response bodies for all requests * that traverse through the middleware. */ - compression() { + compression(): RequestHandler { return compression(); } @@ -124,7 +130,7 @@ export class MiddlewareFactory { * * @returns An Express request handler */ - logging() { + logging(): RequestHandler { const logger = this.#logger.child({ type: 'incomingRequest', }); @@ -150,7 +156,7 @@ export class MiddlewareFactory { * * @returns An Express request handler */ - helmet() { + helmet(): RequestHandler { return helmet(readHelmetOptions(this.#config.getOptionalConfig('backend'))); } @@ -166,7 +172,7 @@ export class MiddlewareFactory { * * @returns An Express request handler */ - cors() { + cors(): RequestHandler { return cors(readCorsOptions(this.#config.getOptionalConfig('backend'))); } From 5677fdb75d9a1178cbabea979159ebc7227d83c2 Mon Sep 17 00:00:00 2001 From: Alex Crome Date: Mon, 9 Jan 2023 21:31:23 +0000 Subject: [PATCH 47/55] Minor grammar fix to CONTRIBUTING.md Signed-off-by: Alex Crome --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f658799a85..f3de7bf0e5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -142,7 +142,7 @@ Changesets **are** needed for new packages, as that is what triggers the package 6. Push the commit with your changeset to the branch associated with your PR 7. Accept our gratitude for making the release process easier on the maintainers -For more information, checkout [adding a changeset](https://github.com/atlassian/changesets/blob/master/docs/adding-a-changeset.md) documentation in the changesets repository. +For more information, check out [adding a changeset](https://github.com/atlassian/changesets/blob/master/docs/adding-a-changeset.md) documentation in the changesets repository. ## Merging to Master From 90f4eb12d59efad97c91e9c906c9937bb421b87b Mon Sep 17 00:00:00 2001 From: Brian Fletcher Date: Tue, 10 Jan 2023 08:05:48 +0000 Subject: [PATCH 48/55] use index for key in react fragment Signed-off-by: Brian Fletcher --- plugins/scaffolder/src/components/ActionsPage/ActionsPage.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/scaffolder/src/components/ActionsPage/ActionsPage.tsx b/plugins/scaffolder/src/components/ActionsPage/ActionsPage.tsx index 34cf58441f..0c16d0bd03 100644 --- a/plugins/scaffolder/src/components/ActionsPage/ActionsPage.tsx +++ b/plugins/scaffolder/src/components/ActionsPage/ActionsPage.tsx @@ -78,9 +78,9 @@ const useStyles = makeStyles(theme => ({ const ExamplesTable = (props: { examples: ActionExample[] }) => { return ( - {props.examples.map(example => { + {props.examples.map((example, index) => { return ( - + {example.description} From 5b54a5654ffe02c4c36bf9b3bb0cd1b1aaaac783 Mon Sep 17 00:00:00 2001 From: Morgan Bentell Date: Tue, 10 Jan 2023 09:44:24 +0100 Subject: [PATCH 49/55] remove dev file Signed-off-by: Morgan Bentell --- packages/techdocs-cli/serve.sh | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 packages/techdocs-cli/serve.sh diff --git a/packages/techdocs-cli/serve.sh b/packages/techdocs-cli/serve.sh deleted file mode 100644 index e69de29bb2..0000000000 From 794a87c36c00954fb52a5c96aa5c3065f96333b1 Mon Sep 17 00:00:00 2001 From: Morgan Bentell Date: Tue, 10 Jan 2023 09:52:45 +0100 Subject: [PATCH 50/55] remove autoformatting changes Signed-off-by: Morgan Bentell --- docs/features/techdocs/cli.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/features/techdocs/cli.md b/docs/features/techdocs/cli.md index 1c01441db3..682c6a8f8f 100644 --- a/docs/features/techdocs/cli.md +++ b/docs/features/techdocs/cli.md @@ -1,9 +1,7 @@ --- id: cli title: TechDocs CLI - # prettier-ignore - description: TechDocs CLI - a utility command line interface for managing TechDocs sites in Backstage. --- From bd20e8b925a190576bdd8ccb4441b47dd0804dfd Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 10 Jan 2023 10:16:52 +0100 Subject: [PATCH 51/55] backend-app-api: fix infinite loop in http server stop Signed-off-by: Patrik Oldsberg --- packages/backend-app-api/src/http/createHttpServer.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/backend-app-api/src/http/createHttpServer.ts b/packages/backend-app-api/src/http/createHttpServer.ts index f72269c079..5c526ba1e2 100644 --- a/packages/backend-app-api/src/http/createHttpServer.ts +++ b/packages/backend-app-api/src/http/createHttpServer.ts @@ -35,6 +35,10 @@ export async function createHttpServer( const server = await createServer(listener, options, deps); const stopper = stoppableServer(server, 0); + // The stopper here is actually the server itself, so if we try + // to call stopper.stop() down in the stop implementation, we'll + // be calling ourselves. + const stopServer = stopper.stop.bind(stopper); return Object.assign(server, { start() { @@ -57,7 +61,7 @@ export async function createHttpServer( stop() { return new Promise((resolve, reject) => { - stopper.stop((error?: Error) => { + stopServer((error?: Error) => { if (error) { reject(error); } else { From b8329b92df454978438b05140c261992e27e1ad1 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 10 Jan 2023 09:21:31 +0000 Subject: [PATCH 52/55] fix(deps): update dependency @rjsf/utils to v5.0.0-beta.16 Signed-off-by: Renovate Bot --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index b5d82d8162..0834d7d079 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12875,8 +12875,8 @@ __metadata: linkType: hard "@rjsf/utils@npm:^5.0.0-beta.14": - version: 5.0.0-beta.15 - resolution: "@rjsf/utils@npm:5.0.0-beta.15" + version: 5.0.0-beta.16 + resolution: "@rjsf/utils@npm:5.0.0-beta.16" dependencies: json-schema-merge-allof: ^0.8.1 jsonpointer: ^5.0.1 @@ -12885,7 +12885,7 @@ __metadata: react-is: ^18.2.0 peerDependencies: react: ^16.14.0 || >=17 - checksum: e22b1cc13183d6a6a57b900bb80825eff1c4680edadb554adb6737c21237c7862b2a249e5b67c0c6971094b5d21224daf108ad1f400286be924bbc5c29784a00 + checksum: 7a0f04f20b1b12eff33322bef322ee96e222fc621142fb34906cb18386a2a0dd97224fd97851899644994a1adaab33c6f107076f344b66108219fcb809132703 languageName: node linkType: hard From bff790af9c4a8c0f96bd3ecd3150062c7e66e3d8 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 10 Jan 2023 12:19:06 +0100 Subject: [PATCH 53/55] backend-app-api: remove startHttpServer Signed-off-by: Patrik Oldsberg --- packages/backend-app-api/api-report.md | 16 ----- packages/backend-app-api/src/http/index.ts | 2 - .../src/http/startHttpServer.ts | 62 ------------------- .../rootHttpRouter/rootHttpRouterFactory.ts | 21 ++++++- 4 files changed, 19 insertions(+), 82 deletions(-) delete mode 100644 packages/backend-app-api/src/http/startHttpServer.ts diff --git a/packages/backend-app-api/api-report.md b/packages/backend-app-api/api-report.md index 18c2c52370..cc84309a2f 100644 --- a/packages/backend-app-api/api-report.md +++ b/packages/backend-app-api/api-report.md @@ -222,22 +222,6 @@ export type ServiceOrExtensionPoint = | ExtensionPoint | ServiceRef; -// @public -export function startHttpServer( - listener: RequestListener, - options: StartHttpServerOptions, -): Promise; - -// @public -export interface StartHttpServerOptions { - // (undocumented) - config: ConfigService; - // (undocumented) - lifecycle: RootLifecycleService; - // (undocumented) - logger: RootLoggerService; -} - // @public (undocumented) export const tokenManagerFactory: ( options?: undefined, diff --git a/packages/backend-app-api/src/http/index.ts b/packages/backend-app-api/src/http/index.ts index 5bd54ec4a5..4a9ec14cf8 100644 --- a/packages/backend-app-api/src/http/index.ts +++ b/packages/backend-app-api/src/http/index.ts @@ -23,8 +23,6 @@ export type { } from './MiddlewareFactory'; export { readCorsOptions } from './readCorsOptions'; export { readHelmetOptions } from './readHelmetOptions'; -export { startHttpServer } from './startHttpServer'; -export type { StartHttpServerOptions } from './startHttpServer'; export type { ExtendedHttpServer, HttpServerCertificateOptions, diff --git a/packages/backend-app-api/src/http/startHttpServer.ts b/packages/backend-app-api/src/http/startHttpServer.ts deleted file mode 100644 index 64d0be3f1e..0000000000 --- a/packages/backend-app-api/src/http/startHttpServer.ts +++ /dev/null @@ -1,62 +0,0 @@ -/* - * 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 { - ConfigService, - RootLifecycleService, - RootLoggerService, -} from '@backstage/backend-plugin-api'; -import { RequestListener } from 'http'; -import { readHttpServerOptions } from './config'; -import { createHttpServer } from './createHttpServer'; - -/** - * Options for {@link startHttpServer}. - * - * @public - */ -export interface StartHttpServerOptions { - config: ConfigService; - logger: RootLoggerService; - lifecycle: RootLifecycleService; -} - -/** - * Starts up an HTTP server, as well as registers a shutdown handler that stops the server. - * - * @public - */ -export async function startHttpServer( - listener: RequestListener, - options: StartHttpServerOptions, -) { - const { config, logger, lifecycle } = options; - - const server = await createHttpServer( - listener, - readHttpServerOptions(config.getOptionalConfig('backend')), - { logger }, - ); - - lifecycle.addShutdownHook({ - async fn() { - await server.stop(); - }, - labels: { type: 'httpServer' }, - }); - - await server.start(); -} diff --git a/packages/backend-app-api/src/services/implementations/rootHttpRouter/rootHttpRouterFactory.ts b/packages/backend-app-api/src/services/implementations/rootHttpRouter/rootHttpRouterFactory.ts index 284b019a2e..4b021b977c 100644 --- a/packages/backend-app-api/src/services/implementations/rootHttpRouter/rootHttpRouterFactory.ts +++ b/packages/backend-app-api/src/services/implementations/rootHttpRouter/rootHttpRouterFactory.ts @@ -22,7 +22,11 @@ import { LoggerService, } from '@backstage/backend-plugin-api'; import express, { RequestHandler, Express } from 'express'; -import { MiddlewareFactory, startHttpServer } from '../../../http'; +import { + createHttpServer, + MiddlewareFactory, + readHttpServerOptions, +} from '../../../http'; import { RestrictedIndexedRouter } from './RestrictedIndexedRouter'; /** @@ -93,7 +97,20 @@ export const rootHttpRouterFactory = createServiceFactory({ lifecycle, }); - await startHttpServer(app, { config, logger, lifecycle }); + const server = await createHttpServer( + app, + readHttpServerOptions(config.getOptionalConfig('backend')), + { logger }, + ); + + lifecycle.addShutdownHook({ + async fn() { + await server.stop(); + }, + labels: { service: 'rootHttpRouter' }, + }); + + await server.start(); return router; }, From c45be22cc031171511da4fd07d0f30ff15834ca9 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 10 Jan 2023 12:52:55 +0100 Subject: [PATCH 54/55] backend-app-api: create a labelled logger for rootHttpRouter Signed-off-by: Patrik Oldsberg --- .../implementations/rootHttpRouter/rootHttpRouterFactory.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/backend-app-api/src/services/implementations/rootHttpRouter/rootHttpRouterFactory.ts b/packages/backend-app-api/src/services/implementations/rootHttpRouter/rootHttpRouterFactory.ts index 4b021b977c..d8cedda0d9 100644 --- a/packages/backend-app-api/src/services/implementations/rootHttpRouter/rootHttpRouterFactory.ts +++ b/packages/backend-app-api/src/services/implementations/rootHttpRouter/rootHttpRouterFactory.ts @@ -72,17 +72,18 @@ export const rootHttpRouterFactory = createServiceFactory({ service: coreServices.rootHttpRouter, deps: { config: coreServices.config, - logger: coreServices.rootLogger, + rootLogger: coreServices.rootLogger, lifecycle: coreServices.rootLifecycle, }, async factory( - { config, logger, lifecycle }, + { config, rootLogger, lifecycle }, { indexPath, configure = defaultConfigure, }: RootHttpRouterFactoryOptions = {}, ) { const router = new RestrictedIndexedRouter(indexPath ?? '/api/app'); + const logger = rootLogger.child({ service: 'rootHttpRouter' }); const app = express(); From e3fca10038856268dcaa4ddf058ead79ffe15e45 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 10 Jan 2023 13:01:21 +0100 Subject: [PATCH 55/55] backend-app-api: switch plugin log label pluginId->plugin Signed-off-by: Patrik Oldsberg --- .changeset/polite-books-suffer.md | 5 +++++ .../src/services/implementations/loggerService.ts | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 .changeset/polite-books-suffer.md diff --git a/.changeset/polite-books-suffer.md b/.changeset/polite-books-suffer.md new file mode 100644 index 0000000000..b2a3c88e35 --- /dev/null +++ b/.changeset/polite-books-suffer.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-app-api': patch +--- + +Tweaked the plugin logger to use `plugin` as the label for the plugin ID, rather than `pluginId`. diff --git a/packages/backend-app-api/src/services/implementations/loggerService.ts b/packages/backend-app-api/src/services/implementations/loggerService.ts index 52b391249d..5cefe808f8 100644 --- a/packages/backend-app-api/src/services/implementations/loggerService.ts +++ b/packages/backend-app-api/src/services/implementations/loggerService.ts @@ -28,7 +28,7 @@ export const loggerFactory = createServiceFactory({ }, async factory({ rootLogger }) { return async ({ plugin }) => { - return rootLogger.child({ pluginId: plugin.getId() }); + return rootLogger.child({ plugin: plugin.getId() }); }; }, });