From 7daf65bfcfa1be447d9a48bd1c935e5227dcc06c Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 2 Aug 2023 16:25:19 +0200 Subject: [PATCH] proxy-backend: deprecate root proxy config, move to proxy.endpoints instead Signed-off-by: Patrik Oldsberg --- .changeset/rotten-rabbits-move.md | 5 + plugins/proxy-backend/config.d.ts | 100 +++++++++++--------- plugins/proxy-backend/src/plugin.ts | 51 +++++----- plugins/proxy-backend/src/service/router.ts | 59 +++++++++++- 4 files changed, 137 insertions(+), 78 deletions(-) create mode 100644 .changeset/rotten-rabbits-move.md diff --git a/.changeset/rotten-rabbits-move.md b/.changeset/rotten-rabbits-move.md new file mode 100644 index 0000000000..f2e1ffda08 --- /dev/null +++ b/.changeset/rotten-rabbits-move.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-proxy-backend': minor +--- + +Defining proxy endpoints directly under the root `proxy` configuration key is deprecated. Endpoints should now be declared under `proxy.endpoints` instead. The `skipInvalidProxies` and `reviveConsumedRequestBodies` can now also be configured through static configuration. diff --git a/plugins/proxy-backend/config.d.ts b/plugins/proxy-backend/config.d.ts index bc9cf7765a..79e1cfcba6 100644 --- a/plugins/proxy-backend/config.d.ts +++ b/plugins/proxy-backend/config.d.ts @@ -15,51 +15,63 @@ */ export interface Config { - /** - * A list of forwarding-proxies. Each key is a route to match, - * below the prefix that the proxy plugin is mounted on. It must - * start with a '/'. - */ proxy?: { - [key: string]: - | string - | { - /** - * Target of the proxy. Url string to be parsed with the url module. - */ - target: string; - /** - * Object with extra headers to be added to target requests. - */ - headers?: { - /** @visibility secret */ - Authorization?: string; - /** @visibility secret */ - authorization?: string; - /** @visibility secret */ - 'X-Api-Key'?: string; - /** @visibility secret */ - 'x-api-key'?: string; - [key: string]: string | undefined; + /** + * Rather than failing to start up, the proxy backend will instead just warn on invalid endpoints. + */ + skipInvalidProxies?: boolean; + + /** + * Revive request bodies that have already been consumed by earlier middleware. + */ + reviveConsumedRequestBodies?: boolean; + + /** + * A list of forwarding-proxies. Each key is a route to match, + * below the prefix that the proxy plugin is mounted on. It must + * start with a '/'. + */ + endpoints?: { + [key: string]: + | string + | { + /** + * Target of the proxy. Url string to be parsed with the url module. + */ + target: string; + /** + * Object with extra headers to be added to target requests. + */ + headers?: { + /** @visibility secret */ + Authorization?: string; + /** @visibility secret */ + authorization?: string; + /** @visibility secret */ + 'X-Api-Key'?: string; + /** @visibility secret */ + 'x-api-key'?: string; + [key: string]: string | undefined; + }; + /** + * Changes the origin of the host header to the target URL. Default: true. + */ + changeOrigin?: boolean; + /** + * Rewrite target's url path. Object-keys will be used as RegExp to match paths. + * If pathRewrite is not specified, it is set to a single rewrite that removes the entire prefix and route. + */ + pathRewrite?: { [regexp: string]: string }; + /** + * Limit the forwarded HTTP methods, for example allowedMethods: ['GET'] to enforce read-only access. + */ + allowedMethods?: string[]; + /** + * Limit the forwarded HTTP methods. By default, only the headers that are considered safe for CORS + * and headers that are set by the proxy will be forwarded. + */ + allowedHeaders?: string[]; }; - /** - * Changes the origin of the host header to the target URL. Default: true. - */ - changeOrigin?: boolean; - /** - * Rewrite target's url path. Object-keys will be used as RegExp to match paths. - * If pathRewrite is not specified, it is set to a single rewrite that removes the entire prefix and route. - */ - pathRewrite?: { [regexp: string]: string }; - /** - * Limit the forwarded HTTP methods, for example allowedMethods: ['GET'] to enforce read-only access. - */ - allowedMethods?: string[]; - /** - * Limit the forwarded HTTP methods. By default, only the headers that are considered safe for CORS - * and headers that are set by the proxy will be forwarded. - */ - allowedHeaders?: string[]; - }; + }; }; } diff --git a/plugins/proxy-backend/src/plugin.ts b/plugins/proxy-backend/src/plugin.ts index c8dbdb4a08..e4c14b0037 100644 --- a/plugins/proxy-backend/src/plugin.ts +++ b/plugins/proxy-backend/src/plugin.ts @@ -26,32 +26,25 @@ import { createRouter } from './service/router'; * * @alpha */ -export const proxyPlugin = createBackendPlugin( - (options?: { - skipInvalidProxies?: boolean; - reviveConsumedRequestBodies?: boolean; - }) => ({ - pluginId: 'proxy', - register(env) { - env.registerInit({ - deps: { - config: coreServices.rootConfig, - discovery: coreServices.discovery, - logger: coreServices.logger, - httpRouter: coreServices.httpRouter, - }, - async init({ config, discovery, logger, httpRouter }) { - httpRouter.use( - await createRouter({ - config, - discovery, - logger: loggerToWinstonLogger(logger), - skipInvalidProxies: options?.skipInvalidProxies, - reviveConsumedRequestBodies: options?.reviveConsumedRequestBodies, - }), - ); - }, - }); - }, - }), -); +export const proxyPlugin = createBackendPlugin({ + pluginId: 'proxy', + register(env) { + env.registerInit({ + deps: { + config: coreServices.rootConfig, + discovery: coreServices.discovery, + logger: coreServices.logger, + httpRouter: coreServices.httpRouter, + }, + async init({ config, discovery, logger, httpRouter }) { + httpRouter.use( + await createRouter({ + config, + discovery, + logger: loggerToWinstonLogger(logger), + }), + ); + }, + }); + }, +}); diff --git a/plugins/proxy-backend/src/service/router.ts b/plugins/proxy-backend/src/service/router.ts index 6912965659..f8f6a3fdab 100644 --- a/plugins/proxy-backend/src/service/router.ts +++ b/plugins/proxy-backend/src/service/router.ts @@ -191,6 +191,37 @@ export function buildMiddleware( return createProxyMiddleware(filter, fullConfig); } +function readProxyConfig(config: Config, logger: Logger): unknown { + const endpoints = config.getOptional('proxy.endpoints'); + if (endpoints) { + if (typeof endpoints !== 'object' || Array.isArray(endpoints)) { + throw new Error('proxy configuration must be an object'); + } + return endpoints; + } + + const root = config.getOptional('proxy'); + if (!root) { + return {}; + } + if (typeof root !== 'object' || Array.isArray(root)) { + throw new Error('deprecated proxy configuration must be an object'); + } + + const rootEndpoints = Object.fromEntries( + Object.entries(root).filter(([key]) => key.startsWith('/')), + ); + if (Object.keys(rootEndpoints).length === 0) { + return {}; + } + + logger.warn( + "Configuring proxy endpoints in the root 'proxy' configuration is deprecated. Move this configuration to 'proxy.endpoints' instead.", + ); + + return rootEndpoints; +} + /** * Creates a new {@link https://expressjs.com/en/api.html#router | "express router"} that proxy each target configured under the `proxy` key of the config * @example @@ -215,25 +246,39 @@ export async function createRouter( const router = Router(); let currentRouter = Router(); + const skipInvalidProxies = + options.skipInvalidProxies ?? + options.config.getOptionalBoolean('proxy.skipInvalidProxies') ?? + false; + const reviveConsumedRequestBodies = + options.reviveConsumedRequestBodies ?? + options.config.getOptionalBoolean('proxy.reviveConsumedRequestBodies') ?? + false; + const proxyOptions = { + skipInvalidProxies, + reviveConsumedRequestBodies, + logger: options.logger, + }; + const externalUrl = await options.discovery.getExternalBaseUrl('proxy'); const { pathname: pathPrefix } = new URL(externalUrl); - const proxyConfig = options.config.getOptional('proxy') ?? {}; - configureMiddlewares(options, currentRouter, pathPrefix, proxyConfig); + const proxyConfig = readProxyConfig(options.config, options.logger); + configureMiddlewares(proxyOptions, currentRouter, pathPrefix, proxyConfig); router.use((...args) => currentRouter(...args)); if (options.config.subscribe) { let currentKey = JSON.stringify(proxyConfig); options.config.subscribe(() => { - const newProxyConfig = options.config.getOptional('proxy') ?? {}; + const newProxyConfig = readProxyConfig(options.config, options.logger); const newKey = JSON.stringify(newProxyConfig); if (currentKey !== newKey) { currentKey = newKey; currentRouter = Router(); configureMiddlewares( - options, + proxyOptions, currentRouter, pathPrefix, newProxyConfig, @@ -246,7 +291,11 @@ export async function createRouter( } function configureMiddlewares( - options: RouterOptions, + options: { + reviveConsumedRequestBodies: boolean; + skipInvalidProxies: boolean; + logger: Logger; + }, router: express.Router, pathPrefix: string, proxyConfig: any,