diff --git a/app-config.yaml b/app-config.yaml index 79967842ef..cd8494613a 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -10,6 +10,13 @@ backend: methods: [GET, POST, PUT, DELETE] credentials: true +proxy: + '/circleci/api': + target: 'https://circleci.com/api/v1.1' + changeOrigin: true + pathRewrite: + '^/circleci/api/': '/' + organization: name: Spotify diff --git a/docs/getting-started/structure-of-a-plugin.md b/docs/getting-started/structure-of-a-plugin.md index fcfb2c0d46..a277628b6c 100644 --- a/docs/getting-started/structure-of-a-plugin.md +++ b/docs/getting-started/structure-of-a-plugin.md @@ -95,4 +95,13 @@ There are two things needed for a Backstage app to start making use of a plugin. Luckily these two steps happen automatically when you create a plugin with the Backstage CLI. +## Talking to the outside world + +If your plugin needs to communicate with services outside the backstage +environment you will probably face challenges like CORS policies and/or +backend-side authorization. To smooth this process out you can use proxy - +either the one you already have (like nginx/haproxy/etc) or the proxy-backend +plugin that we provide for the backstage backend. +[Read more](../../plugins/proxy-backend/README.md) + [Back to Getting Started](README.md) diff --git a/packages/app/src/apis.ts b/packages/app/src/apis.ts index e80f031bf3..011c549619 100644 --- a/packages/app/src/apis.ts +++ b/packages/app/src/apis.ts @@ -78,7 +78,10 @@ export const apis = (config: ConfigApi) => { ); builder.add(storageApiRef, WebStorage.create({ errorApi })); - builder.add(circleCIApiRef, new CircleCIApi()); + builder.add( + circleCIApiRef, + new CircleCIApi(`${backendUrl}/proxy/circleci/api`), + ); builder.add(githubActionsApiRef, new GithubActionsClient()); builder.add(featureFlagsApiRef, new FeatureFlags()); diff --git a/packages/backend/package.json b/packages/backend/package.json index 69b97b6335..2844f17744 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -18,6 +18,7 @@ "migrate:create": "knex migrate:make -x ts" }, "dependencies": { + "@backstage/plugin-proxy-backend": "^0.1.1-alpha.13", "@backstage/backend-common": "^0.1.1-alpha.13", "@backstage/catalog-model": "^0.1.1-alpha.13", "@backstage/config": "^0.1.1-alpha.13", diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 5562ffa242..ea5142da19 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -36,6 +36,7 @@ import identity from './plugins/identity'; import rollbar from './plugins/rollbar'; import scaffolder from './plugins/scaffolder'; import sentry from './plugins/sentry'; +import proxy from './plugins/proxy'; import techdocs from './plugins/techdocs'; import { PluginEnvironment } from './types'; @@ -65,6 +66,7 @@ async function main() { const scaffolderEnv = useHotMemoize(module, () => createEnv('scaffolder')); const authEnv = useHotMemoize(module, () => createEnv('auth')); const identityEnv = useHotMemoize(module, () => createEnv('identity')); + const proxyEnv = useHotMemoize(module, () => createEnv('proxy')); const techdocsEnv = useHotMemoize(module, () => createEnv('techdocs')); const service = createServiceBuilder(module) @@ -83,7 +85,8 @@ async function main() { ) .addRouter('/auth', await auth(authEnv)) .addRouter('/identity', await identity(identityEnv)) - .addRouter('/techdocs', await techdocs(techdocsEnv)); + .addRouter('/techdocs', await techdocs(techdocsEnv)) + .addRouter('/proxy', await proxy(proxyEnv)); await service.start().catch(err => { console.log(err); diff --git a/packages/backend/src/plugins/proxy.ts b/packages/backend/src/plugins/proxy.ts new file mode 100644 index 0000000000..4964de130e --- /dev/null +++ b/packages/backend/src/plugins/proxy.ts @@ -0,0 +1,25 @@ +/* + * Copyright 2020 Spotify AB + * + * 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. + */ +// @ts-ignore +import { createRouter } from '@backstage/plugin-proxy-backend'; +import { PluginEnvironment } from '../types'; + +export default async function createPlugin({ + logger, + config, +}: PluginEnvironment) { + return await createRouter({ logger, config }); +} diff --git a/plugins/circleci/package.json b/plugins/circleci/package.json index 00ad37bfb5..69c74da25e 100644 --- a/plugins/circleci/package.json +++ b/plugins/circleci/package.json @@ -10,15 +10,6 @@ "main": "dist/index.esm.js", "types": "dist/index.d.ts" }, - "proxy": { - "/circleci/api": { - "target": "https://circleci.com/api/v1.1", - "changeOrigin": true, - "pathRewrite": { - "^/circleci/api/": "/" - } - } - }, "scripts": { "build": "backstage-cli plugin:build", "lint": "backstage-cli lint", diff --git a/plugins/proxy-backend/.eslintrc.js b/plugins/proxy-backend/.eslintrc.js new file mode 100644 index 0000000000..16a033dbc6 --- /dev/null +++ b/plugins/proxy-backend/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: [require.resolve('@backstage/cli/config/eslint.backend')], +}; diff --git a/plugins/proxy-backend/README.md b/plugins/proxy-backend/README.md new file mode 100644 index 0000000000..315178dac6 --- /dev/null +++ b/plugins/proxy-backend/README.md @@ -0,0 +1,37 @@ +# Proxy backend plugin + +This is the backend plugin that enables proxy definitions to be declared in and read from app-config.yaml. + +Relies on the `http-proxy-middleware` package. + +## Getting Started + +This backend plugin can be started in a standalone mode from directly in this package +with `yarn start`. However, it will have limited functionality and that process is +most convenient when developing the plugin itself. + +To run it within the backend do: + +1. Register the router in `packages/backend/src/index.ts`: + +``` +const proxyEnv = useHotMemoize(module, () => createEnv('proxy')); + +const service = createServiceBuilder(module) + .loadConfig(configReader) + /** several different routers */ + .addRouter('/', await proxy(proxyEnv)); +``` + +2. Start the backend + +```bash +yarn workspace example-backend start +``` + +This will launch the full example backend. + +## Links + +- (http-proxy-middleware)[https://www.npmjs.com/package/http-proxy-middleware] +- (The Backstage homepage)[https://backstage.io] diff --git a/plugins/proxy-backend/package.json b/plugins/proxy-backend/package.json new file mode 100644 index 0000000000..9a9352ba83 --- /dev/null +++ b/plugins/proxy-backend/package.json @@ -0,0 +1,49 @@ +{ + "name": "@backstage/plugin-proxy-backend", + "version": "0.1.1-alpha.13", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "publishConfig": { + "access": "public", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" + }, + "scripts": { + "start": "backstage-cli backend:dev", + "build": "backstage-cli backend:build", + "lint": "backstage-cli lint", + "test": "backstage-cli test", + "prepack": "backstage-cli prepack", + "postpack": "backstage-cli postpack", + "clean": "backstage-cli clean" + }, + "dependencies": { + "@backstage/backend-common": "^0.1.1-alpha.13", + "@backstage/config": "^0.1.1-alpha.13", + "@backstage/config-loader": "^0.1.1-alpha.13", + "@types/express": "^4.17.6", + "express": "^4.17.1", + "express-promise-router": "^3.0.3", + "http-proxy-middleware": "^0.19.1", + "morgan": "^1.10.0", + "node-fetch": "^2.6.0", + "uuid": "^8.0.0", + "winston": "^3.2.1", + "yaml": "^1.9.2", + "yn": "^4.0.0", + "yup": "^0.29.1" + }, + "devDependencies": { + "@backstage/cli": "^0.1.1-alpha.13", + "@types/node-fetch": "^2.5.7", + "@types/supertest": "^2.0.8", + "@types/uuid": "^8.0.0", + "@types/yup": "^0.28.2", + "jest-fetch-mock": "^3.0.3", + "supertest": "^4.0.2" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/proxy-backend/src/index.ts b/plugins/proxy-backend/src/index.ts new file mode 100644 index 0000000000..7612c392a2 --- /dev/null +++ b/plugins/proxy-backend/src/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './service/router'; diff --git a/plugins/proxy-backend/src/run.ts b/plugins/proxy-backend/src/run.ts new file mode 100644 index 0000000000..b96989e4b8 --- /dev/null +++ b/plugins/proxy-backend/src/run.ts @@ -0,0 +1,33 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 { getRootLogger } from '@backstage/backend-common'; +import yn from 'yn'; +import { startStandaloneServer } from './service/standaloneServer'; + +const port = process.env.PLUGIN_PORT ? Number(process.env.PLUGIN_PORT) : 7000; +const enableCors = yn(process.env.PLUGIN_CORS, { default: false }); +const logger = getRootLogger(); + +startStandaloneServer({ port, enableCors, logger }).catch(err => { + logger.error(err); + process.exit(1); +}); + +process.on('SIGINT', () => { + logger.info('CTRL+C pressed; exiting.'); + process.exit(0); +}); diff --git a/plugins/proxy-backend/src/service/router.test.ts b/plugins/proxy-backend/src/service/router.test.ts new file mode 100644 index 0000000000..699ed9b99d --- /dev/null +++ b/plugins/proxy-backend/src/service/router.test.ts @@ -0,0 +1,32 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 { createRouter } from './router'; +import winston from 'winston'; +import { ConfigReader } from '@backstage/config'; +import { loadConfig } from '@backstage/config-loader'; + +describe('createRouter', () => { + it('works', async () => { + const logger = winston.createLogger(); + const config = ConfigReader.fromConfigs(await loadConfig()); + const router = await createRouter({ + config, + logger, + }); + expect(router).toBeDefined(); + }); +}); diff --git a/plugins/proxy-backend/src/service/router.ts b/plugins/proxy-backend/src/service/router.ts new file mode 100644 index 0000000000..2c8af574bd --- /dev/null +++ b/plugins/proxy-backend/src/service/router.ts @@ -0,0 +1,38 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 express from 'express'; +import Router from 'express-promise-router'; +import createProxyMiddleware from 'http-proxy-middleware'; +import { Logger } from 'winston'; + +export interface RouterOptions { + logger: Logger; + config: Config; +} + +export async function createRouter( + options: RouterOptions, +): Promise { + const router = Router(); + const proxyConfig = options.config.get('proxy') ?? {}; + Object.entries(proxyConfig).forEach(([route, proxyRouteConfig]) => { + router.use(createProxyMiddleware(route, proxyRouteConfig)); + }); + + return router; +} diff --git a/plugins/proxy-backend/src/service/standaloneServer.ts b/plugins/proxy-backend/src/service/standaloneServer.ts new file mode 100644 index 0000000000..84c803eeac --- /dev/null +++ b/plugins/proxy-backend/src/service/standaloneServer.ts @@ -0,0 +1,54 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 { createServiceBuilder } from '@backstage/backend-common'; +import { Server } from 'http'; +import { Logger } from 'winston'; +import { createRouter } from './router'; +import { ConfigReader } from '@backstage/config'; +import { loadConfig } from '@backstage/config-loader'; + +export interface ServerOptions { + port: number; + enableCors: boolean; + logger: Logger; +} + +export async function startStandaloneServer( + options: ServerOptions, +): Promise { + const logger = options.logger.child({ service: 'proxy-backend' }); + + logger.debug('Creating application...'); + + const config = ConfigReader.fromConfigs(await loadConfig()); + const router = await createRouter({ + config, + logger, + }); + const service = createServiceBuilder(module) + .enableCors({ origin: 'http://localhost:3000' }) + .addRouter('/proxy', router); + + logger.debug('Starting application server...'); + + return await service.start().catch(err => { + logger.error(err); + process.exit(1); + }); +} + +module.hot?.accept(); diff --git a/plugins/proxy-backend/src/setupTests.ts b/plugins/proxy-backend/src/setupTests.ts new file mode 100644 index 0000000000..f7b6ca962d --- /dev/null +++ b/plugins/proxy-backend/src/setupTests.ts @@ -0,0 +1,19 @@ +/* + * Copyright 2020 Spotify AB + * + * 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. + */ + +require('jest-fetch-mock').enableMocks(); + +export {}; diff --git a/yarn.lock b/yarn.lock index 0fb5368314..ca6bdad85b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10173,6 +10173,16 @@ http-proxy-middleware@0.19.1: lodash "^4.17.11" micromatch "^3.1.10" +http-proxy-middleware@^0.19.1: + version "0.19.2" + resolved "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-0.19.2.tgz#ee73dcc8348165afefe8de2ff717751d181608ee" + integrity sha512-aYk1rTKqLTus23X3L96LGNCGNgWpG4cG0XoZIT1GUPhhulEHX/QalnO6Vbo+WmKWi4AL2IidjuC0wZtbpg0yhQ== + dependencies: + http-proxy "^1.18.1" + is-glob "^4.0.0" + lodash "^4.17.11" + micromatch "^3.1.10" + http-proxy@^1.17.0: version "1.18.0" resolved "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.0.tgz#dbe55f63e75a347db7f3d99974f2692a314a6a3a"