From 709cc4004d7585990013603b97b41ffbfaa10adb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Mon, 8 Jun 2020 13:57:38 +0200 Subject: [PATCH] feat(backend-common): add common code for service shell --- packages/backend-common/package.json | 5 + packages/backend-common/src/index.ts | 1 + .../src/service/ServiceBuilderImpl.ts | 117 ++++++++++++++++++ .../src/service/createServiceBuilder.ts | 24 ++++ packages/backend-common/src/service/index.ts | 18 +++ packages/backend-common/src/service/types.ts | 65 ++++++++++ packages/backend/package.json | 5 - packages/backend/src/index.ts | 57 +++------ plugins/catalog-backend/package.json | 3 - .../src/service/standaloneApplication.ts | 71 ----------- .../src/service/standaloneServer.ts | 33 +++-- 11 files changed, 263 insertions(+), 136 deletions(-) create mode 100644 packages/backend-common/src/service/ServiceBuilderImpl.ts create mode 100644 packages/backend-common/src/service/createServiceBuilder.ts create mode 100644 packages/backend-common/src/service/index.ts create mode 100644 packages/backend-common/src/service/types.ts delete mode 100644 plugins/catalog-backend/src/service/standaloneApplication.ts diff --git a/packages/backend-common/package.json b/packages/backend-common/package.json index 94dd222c41..54202975c3 100644 --- a/packages/backend-common/package.json +++ b/packages/backend-common/package.json @@ -27,12 +27,17 @@ "clean": "backstage-cli clean" }, "dependencies": { + "compression": "^1.7.4", + "cors": "^2.8.5", "express": "^4.17.1", + "helmet": "^3.22.0", "morgan": "^1.10.0", "winston": "^3.2.1" }, "devDependencies": { "@backstage/cli": "^0.1.1-alpha.7", + "@types/compression": "^1.7.0", + "@types/cors": "^2.8.6", "@types/express": "^4.17.6", "@types/http-errors": "^1.6.3", "@types/morgan": "^1.9.0", diff --git a/packages/backend-common/src/index.ts b/packages/backend-common/src/index.ts index b2c38ab506..11689aafff 100644 --- a/packages/backend-common/src/index.ts +++ b/packages/backend-common/src/index.ts @@ -17,3 +17,4 @@ export * from './errors'; export * from './logging'; export * from './middleware'; +export * from './service'; diff --git a/packages/backend-common/src/service/ServiceBuilderImpl.ts b/packages/backend-common/src/service/ServiceBuilderImpl.ts new file mode 100644 index 0000000000..ac35d52112 --- /dev/null +++ b/packages/backend-common/src/service/ServiceBuilderImpl.ts @@ -0,0 +1,117 @@ +/* + * 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 compression from 'compression'; +import cors from 'cors'; +import express, { Router } from 'express'; +import helmet from 'helmet'; +import { Server } from 'http'; +import { Logger } from 'winston'; +import { getRootLogger } from '../logging'; +import { + errorHandler, + notFoundHandler, + requestLoggingHandler, +} from '../middleware'; +import { ServiceBuilder } from './types'; + +const DEFAULT_PORT = 7000; + +export class ServiceBuilderImpl implements ServiceBuilder { + private port: number | undefined; + private logger: Logger | undefined; + private corsOptions: cors.CorsOptions | undefined; + private routers: [string, Router][]; + + constructor() { + this.routers = []; + } + + setPort(port: number): ServiceBuilder { + this.port = port; + return this; + } + + setLogger(logger: Logger): ServiceBuilder { + this.logger = logger; + return this; + } + + enableCors(options: cors.CorsOptions): ServiceBuilder { + this.corsOptions = options; + return this; + } + + addRouter(root: string, router: Router): ServiceBuilder { + this.routers.push([root, router]); + return this; + } + + start(): Promise { + const app = express(); + const { port, logger, corsOptions } = this.getOptions(); + + app.use(helmet()); + if (corsOptions) { + app.use(cors(corsOptions)); + } + app.use(compression()); + app.use(express.json()); + app.use(requestLoggingHandler()); + for (const [root, route] of this.routers) { + app.use(root, route); + } + app.use(notFoundHandler()); + app.use(errorHandler()); + + return new Promise((resolve, reject) => { + app.on('error', e => { + logger.error(`Failed to start up on port ${port}, ${e}`); + reject(e); + }); + const server = app.listen(port, () => { + logger.info(`Listening on port ${port}`); + }); + resolve(server); + }); + } + + private getOptions(): { + port: number; + logger: Logger; + corsOptions?: cors.CorsOptions; + } { + let port: number; + if (this.port !== undefined) { + port = this.port; + } else { + port = parseInt(process.env.PORT ?? '', 10) || DEFAULT_PORT; + } + + let logger: Logger; + if (this.logger) { + logger = this.logger; + } else { + logger = getRootLogger(); + } + + return { + port, + logger, + corsOptions: this.corsOptions, + }; + } +} diff --git a/packages/backend-common/src/service/createServiceBuilder.ts b/packages/backend-common/src/service/createServiceBuilder.ts new file mode 100644 index 0000000000..ffd8901def --- /dev/null +++ b/packages/backend-common/src/service/createServiceBuilder.ts @@ -0,0 +1,24 @@ +/* + * 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 { ServiceBuilderImpl } from './ServiceBuilderImpl'; + +/** + * Creates a new service builder. + */ +export function createServiceBuilder() { + return new ServiceBuilderImpl(); +} diff --git a/packages/backend-common/src/service/index.ts b/packages/backend-common/src/service/index.ts new file mode 100644 index 0000000000..9bba9d6baf --- /dev/null +++ b/packages/backend-common/src/service/index.ts @@ -0,0 +1,18 @@ +/* + * 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 { createServiceBuilder } from './createServiceBuilder'; +export type { ServiceBuilder } from './types'; diff --git a/packages/backend-common/src/service/types.ts b/packages/backend-common/src/service/types.ts new file mode 100644 index 0000000000..306f0d587e --- /dev/null +++ b/packages/backend-common/src/service/types.ts @@ -0,0 +1,65 @@ +/* + * 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 cors from 'cors'; +import { Router } from 'express'; +import { Server } from 'http'; +import { Logger } from 'winston'; + +export type ServiceBuilder = { + /** + * Sets the port to listen on. + * + * If no port is specified, the service will first look for an environment + * variable named PORT and use that if present, otherwise it picks a default + * port (7000). + * + * @param port The port to listen on + */ + setPort(port: number): ServiceBuilder; + + /** + * Sets the logger to use for service-specific logging. + * + * If no logger is given, the default root logger is used. + * + * @param logger A winston logger + */ + setLogger(logger: Logger): ServiceBuilder; + + /** + * Enables CORS handling using the given settings. + * + * If this method is not called, the resulting service will not have any + * built in CORS handling. + * + * @param options Standard CORS options + */ + enableCors(options: cors.CorsOptions): ServiceBuilder; + + /** + * Adds a router (similar to the express .use call) to the service. + * + * @param root The root URL to bind to (e.g. "/api/function1") + * @param router An express router + */ + addRouter(root: string, router: Router): ServiceBuilder; + + /** + * Starts the server using the given settings. + */ + start(): Promise; +}; diff --git a/packages/backend/package.json b/packages/backend/package.json index 65e2976489..4e2610380c 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -24,19 +24,14 @@ "@backstage/plugin-identity-backend": "^0.1.1-alpha.7", "@backstage/plugin-scaffolder-backend": "^0.1.1-alpha.7", "@backstage/plugin-sentry-backend": "^0.1.1-alpha.7", - "compression": "^1.7.4", - "cors": "^2.8.5", "esm": "^3.2.25", "express": "^4.17.1", - "helmet": "^3.22.0", "knex": "^0.21.1", "sqlite3": "^4.2.0", "winston": "^3.2.1" }, "devDependencies": { "@backstage/cli": "^0.1.1-alpha.7", - "@types/compression": "^1.7.0", - "@types/cors": "^2.8.6", "@types/express": "^4.17.6", "@types/express-serve-static-core": "^4.17.5", "@types/helmet": "^0.0.47", diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 5b1fc54330..0f0733a8ec 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -22,27 +22,15 @@ * Happy hacking! */ -import { - errorHandler, - getRootLogger, - notFoundHandler, - requestLoggingHandler, -} from '@backstage/backend-common'; -import compression from 'compression'; -import cors from 'cors'; -import express from 'express'; -import helmet from 'helmet'; +import { createServiceBuilder, getRootLogger } from '@backstage/backend-common'; import knex from 'knex'; +import auth from './plugins/auth'; import catalog from './plugins/catalog'; +import identity from './plugins/identity'; import scaffolder from './plugins/scaffolder'; import sentry from './plugins/sentry'; -import auth from './plugins/auth'; -import identity from './plugins/identity'; import { PluginEnvironment } from './types'; -const DEFAULT_PORT = 7000; -const PORT = parseInt(process.env.PORT ?? '', 10) || DEFAULT_PORT; - function createEnv(plugin: string): PluginEnvironment { const logger = getRootLogger().child({ type: 'plugin', plugin }); const database = knex({ @@ -57,30 +45,23 @@ function createEnv(plugin: string): PluginEnvironment { } async function main() { - const app = express(); - const corsOptions: cors.CorsOptions = { - origin: 'http://localhost:3000', - credentials: true, - }; + const service = createServiceBuilder() + .enableCors({ + origin: 'http://localhost:3000', + credentials: true, + }) + .addRouter('/catalog', await catalog(createEnv('catalog'))) + .addRouter('/scaffolder', await scaffolder(createEnv('scaffolder'))) + .addRouter( + '/sentry', + await sentry(getRootLogger().child({ type: 'plugin', plugin: 'sentry' })), + ) + .addRouter('/auth', await auth(createEnv('auth'))) + .addRouter('/identity', await identity(createEnv('identity'))); - app.use(helmet()); - app.use(cors(corsOptions)); - app.use(compression()); - app.use(express.json()); - app.use(requestLoggingHandler()); - app.use('/catalog', await catalog(createEnv('catalog'))); - app.use('/scaffolder', await scaffolder(createEnv('scaffolder'))); - app.use( - '/sentry', - await sentry(getRootLogger().child({ type: 'plugin', plugin: 'sentry' })), - ); - app.use('/auth', await auth(createEnv('auth'))); - app.use('/identity', await identity(createEnv('identity'))); - app.use(notFoundHandler()); - app.use(errorHandler()); - - app.listen(PORT, () => { - getRootLogger().info(`Listening on port ${PORT}`); + await service.start().catch(err => { + console.log(err); + process.exit(1); }); } diff --git a/plugins/catalog-backend/package.json b/plugins/catalog-backend/package.json index 7490d1221a..761c042723 100644 --- a/plugins/catalog-backend/package.json +++ b/plugins/catalog-backend/package.json @@ -18,13 +18,10 @@ "dependencies": { "@backstage/backend-common": "^0.1.1-alpha.7", "@backstage/catalog-model": "^0.1.1-alpha.7", - "compression": "^1.7.4", - "cors": "^2.8.5", "esm": "^3.2.25", "express": "^4.17.1", "express-promise-router": "^3.0.3", "fs-extra": "^9.0.0", - "helmet": "^3.22.0", "knex": "^0.21.1", "lodash": "^4.17.15", "morgan": "^1.10.0", diff --git a/plugins/catalog-backend/src/service/standaloneApplication.ts b/plugins/catalog-backend/src/service/standaloneApplication.ts deleted file mode 100644 index e8c1a226f0..0000000000 --- a/plugins/catalog-backend/src/service/standaloneApplication.ts +++ /dev/null @@ -1,71 +0,0 @@ -/* - * 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 { - errorHandler, - notFoundHandler, - requestLoggingHandler, -} from '@backstage/backend-common'; -import compression from 'compression'; -import cors from 'cors'; -import express from 'express'; -import helmet from 'helmet'; -import { Logger } from 'winston'; -import { EntitiesCatalog, LocationsCatalog } from '../catalog'; -import { HigherOrderOperation } from '../ingestion'; -import { createRouter } from './router'; - -export interface ApplicationOptions { - enableCors: boolean; - entitiesCatalog: EntitiesCatalog; - locationsCatalog?: LocationsCatalog; - higherOrderOperation?: HigherOrderOperation; - logger: Logger; -} - -export async function createStandaloneApplication( - options: ApplicationOptions, -): Promise { - const { - enableCors, - entitiesCatalog, - locationsCatalog, - higherOrderOperation, - logger, - } = options; - const app = express(); - - app.use(helmet()); - if (enableCors) { - app.use(cors()); - } - app.use(compression()); - app.use(express.json()); - app.use(requestLoggingHandler()); - app.use( - '/catalog', - await createRouter({ - entitiesCatalog, - locationsCatalog, - higherOrderOperation, - logger, - }), - ); - app.use(notFoundHandler()); - app.use(errorHandler()); - - return app; -} diff --git a/plugins/catalog-backend/src/service/standaloneServer.ts b/plugins/catalog-backend/src/service/standaloneServer.ts index 11fbd0b445..f19dfaf282 100644 --- a/plugins/catalog-backend/src/service/standaloneServer.ts +++ b/plugins/catalog-backend/src/service/standaloneServer.ts @@ -14,13 +14,15 @@ * limitations under the License. */ +import { createServiceBuilder } from '@backstage/backend-common'; import { Server } from 'http'; import { Logger } from 'winston'; +import { HigherOrderOperations } from '..'; import { DatabaseEntitiesCatalog } from '../catalog/DatabaseEntitiesCatalog'; import { DatabaseLocationsCatalog } from '../catalog/DatabaseLocationsCatalog'; import { DatabaseManager } from '../database/DatabaseManager'; -import { HigherOrderOperations, LocationReaders } from '../ingestion'; -import { createStandaloneApplication } from './standaloneApplication'; +import { createRouter } from './router'; +import { LocationReaders } from '../ingestion'; export interface ServerOptions { port: number; @@ -33,11 +35,11 @@ export async function startStandaloneServer( ): Promise { const logger = options.logger.child({ service: 'catalog-backend' }); + logger.debug('Creating application...'); const db = await DatabaseManager.createInMemoryDatabase(logger); - const entitiesCatalog = new DatabaseEntitiesCatalog(db); const locationsCatalog = new DatabaseLocationsCatalog(db); - const locationReader = new LocationReaders(options.logger); + const locationReader = new LocationReaders(); const higherOrderOperation = new HigherOrderOperations( entitiesCatalog, locationsCatalog, @@ -45,25 +47,18 @@ export async function startStandaloneServer( logger, ); - logger.debug('Creating application...'); - const app = await createStandaloneApplication({ - enableCors: options.enableCors, + logger.debug('Starting application server...'); + const router = await createRouter({ entitiesCatalog, locationsCatalog, higherOrderOperation, logger, }); - - logger.debug('Starting application server...'); - return await new Promise((resolve, reject) => { - const server = app.listen(options.port, (err?: Error) => { - if (err) { - reject(err); - return; - } - - logger.info(`Listening on port ${options.port}`); - resolve(server); - }); + const service = createServiceBuilder() + .enableCors({ origin: 'http://localhost:3000' }) + .addRouter('/catalog', router); + return await service.start().catch(err => { + logger.error(err); + process.exit(1); }); }