From 92feb76252647f0a5fa1681b11f3fc15d08b55fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Thu, 30 Apr 2020 14:36:25 +0200 Subject: [PATCH] Make the inventory backend standalone runnable And also make a proposed split of how the loggers are handled. Now logs look like ``` { "type": "incomingRequest", "service": "backstage", "timestamp": "2020-04-30T14:29:04.002Z", "message": "...", "level": "info" } ``` for requests, and then if the plugin logs something properly ``` { "type": "plugin", "service": "backstage", "plugin": "inventory", "timestamp": "2020-04-30T14:29:04.002Z", "message": "...", "level": "info" } ``` --- packages/backend-common/README.md | 2 +- packages/backend/src/index.ts | 42 +++++++------ plugins/inventory-backend/package.json | 18 +++++- .../src/{plugin.test.ts => index.test.ts} | 0 plugins/inventory-backend/src/index.ts | 2 +- .../src/inventory/AggregatorInventory.ts | 35 +++++++++++ .../src/inventory/StaticInventory.ts | 29 +++++++++ .../inventory-backend/src/inventory/index.ts | 19 ++++++ .../src/{plugin.ts => inventory/types.ts} | 20 +++--- plugins/inventory-backend/src/run.ts | 34 +++++++++++ .../src/service/application.ts | 51 ++++++++++++++++ .../inventory-backend/src/service/router.ts | 61 +++++++++++++++++++ .../inventory-backend/src/service/server.ts | 48 +++++++++++++++ plugins/inventory-backend/src/setupTests.ts | 2 + plugins/inventory-backend/tsconfig.json | 1 + 15 files changed, 328 insertions(+), 36 deletions(-) rename plugins/inventory-backend/src/{plugin.test.ts => index.test.ts} (100%) create mode 100644 plugins/inventory-backend/src/inventory/AggregatorInventory.ts create mode 100644 plugins/inventory-backend/src/inventory/StaticInventory.ts create mode 100644 plugins/inventory-backend/src/inventory/index.ts rename plugins/inventory-backend/src/{plugin.ts => inventory/types.ts} (69%) create mode 100644 plugins/inventory-backend/src/run.ts create mode 100644 plugins/inventory-backend/src/service/application.ts create mode 100644 plugins/inventory-backend/src/service/router.ts create mode 100644 plugins/inventory-backend/src/service/server.ts diff --git a/packages/backend-common/README.md b/packages/backend-common/README.md index 32bdd3d2a8..82db0454d2 100644 --- a/packages/backend-common/README.md +++ b/packages/backend-common/README.md @@ -24,8 +24,8 @@ import { const app = express(); app.use(requestLoggingHandler()); app.use('/home', myHomeRouter); -app.use(errorHandler()); app.use(notFoundHandler()); +app.use(errorHandler()); app.listen(PORT, () => { getRootLogger().info(`Listening on port ${PORT}`); diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 1a2c7b70e6..e9fc15f1c1 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -28,32 +28,38 @@ import { notFoundHandler, requestLoggingHandler, } from '@backstage/backend-common'; -import { router as inventoryRouter } from '@backstage/plugin-inventory-backend'; +import { createRouter as inventoryRouter } from '@backstage/plugin-inventory-backend'; +import { createScaffolder } from '@backstage/plugin-scaffolder-backend'; import compression from 'compression'; import cors from 'cors'; import express from 'express'; import helmet from 'helmet'; import { testRouter } from './test'; -import { createScaffolder } from '@backstage/plugin-scaffolder-backend'; const DEFAULT_PORT = 7000; - const PORT = parseInt(process.env.PORT ?? '', 10) || DEFAULT_PORT; -const app = express(); +const logger = getRootLogger().child({ type: 'plugin' }); -const scaffolder = createScaffolder(); +async function main() { + const inventory = await inventoryRouter({ logger }); + const scaffolder = createScaffolder(); -app.use(helmet()); -app.use(cors()); -app.use(compression()); -app.use(express.json()); -app.use(requestLoggingHandler()); -app.use('/test', testRouter); -app.use('/inventory', inventoryRouter); -app.use('/scaffolder', scaffolder); -app.use(errorHandler()); -app.use(notFoundHandler()); + const app = express(); -app.listen(PORT, () => { - getRootLogger().info(`Listening on port ${PORT}`); -}); + app.use(helmet()); + app.use(cors()); + app.use(compression()); + app.use(express.json()); + app.use(requestLoggingHandler()); + app.use('/test', testRouter); + app.use('/inventory', inventory); + app.use('/scaffolder', scaffolder); + app.use(notFoundHandler()); + app.use(errorHandler()); + + app.listen(PORT, () => { + getRootLogger().info(`Listening on port ${PORT}`); + }); +} + +main(); diff --git a/plugins/inventory-backend/package.json b/plugins/inventory-backend/package.json index 37f3f27e78..4b7fd0ecc7 100644 --- a/plugins/inventory-backend/package.json +++ b/plugins/inventory-backend/package.json @@ -5,18 +5,30 @@ "license": "Apache-2.0", "private": true, "scripts": { + "start": "tsc-watch --onFirstSuccess \"nodemon dist/run.js\"", "build": "tsc", "lint": "backstage-cli lint", "test": "backstage-cli test", "clean": "backstage-cli clean" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.4" + "@backstage/cli": "^0.1.1-alpha.4", + "jest-fetch-mock": "^3.0.3", + "tsc-watch": "^4.2.3" }, "dependencies": { - "express": "^4.17.1" + "@backstage/backend-common": "0.1.1-alpha.4", + "compression": "^1.7.4", + "cors": "^2.8.5", + "express": "^4.17.1", + "helmet": "^3.22.0", + "morgan": "^1.10.0", + "winston": "^3.2.1" }, "files": [ "dist" - ] + ], + "nodemonConfig": { + "watch": "./dist" + } } diff --git a/plugins/inventory-backend/src/plugin.test.ts b/plugins/inventory-backend/src/index.test.ts similarity index 100% rename from plugins/inventory-backend/src/plugin.test.ts rename to plugins/inventory-backend/src/index.test.ts diff --git a/plugins/inventory-backend/src/index.ts b/plugins/inventory-backend/src/index.ts index 92c9090376..7612c392a2 100644 --- a/plugins/inventory-backend/src/index.ts +++ b/plugins/inventory-backend/src/index.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export { router } from './plugin'; +export * from './service/router'; diff --git a/plugins/inventory-backend/src/inventory/AggregatorInventory.ts b/plugins/inventory-backend/src/inventory/AggregatorInventory.ts new file mode 100644 index 0000000000..3f1ca0f5de --- /dev/null +++ b/plugins/inventory-backend/src/inventory/AggregatorInventory.ts @@ -0,0 +1,35 @@ +/* + * 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 { Component, Inventory } from './types'; + +export class AggregatorInventory implements Inventory { + inventories: Inventory[] = []; + + list(): Promise> { + return Promise.all(this.inventories.map(i => i.list())).then(lists => + lists.flat(), + ); + } + + item(id: string): Promise { + return this.list().then(items => items.find(i => i.id === id)); + } + + enlist(inventory: Inventory) { + this.inventories.push(inventory); + } +} diff --git a/plugins/inventory-backend/src/inventory/StaticInventory.ts b/plugins/inventory-backend/src/inventory/StaticInventory.ts new file mode 100644 index 0000000000..cd4fa2b7d5 --- /dev/null +++ b/plugins/inventory-backend/src/inventory/StaticInventory.ts @@ -0,0 +1,29 @@ +/* + * 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 { Component, Inventory } from './types'; + +export class StaticInventory implements Inventory { + constructor(private components: Component[]) {} + + list(): Promise> { + return Promise.resolve([...this.components]); + } + + item(id: string): Promise { + return this.list().then(items => items.find(i => i.id === id)); + } +} diff --git a/plugins/inventory-backend/src/inventory/index.ts b/plugins/inventory-backend/src/inventory/index.ts new file mode 100644 index 0000000000..3affa1109a --- /dev/null +++ b/plugins/inventory-backend/src/inventory/index.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. + */ + +export * from './AggregatorInventory'; +export * from './StaticInventory'; +export * from './types'; diff --git a/plugins/inventory-backend/src/plugin.ts b/plugins/inventory-backend/src/inventory/types.ts similarity index 69% rename from plugins/inventory-backend/src/plugin.ts rename to plugins/inventory-backend/src/inventory/types.ts index 88299a5c0f..3d30b11881 100644 --- a/plugins/inventory-backend/src/plugin.ts +++ b/plugins/inventory-backend/src/inventory/types.ts @@ -14,17 +14,11 @@ * limitations under the License. */ -import express from 'express'; +export type Component = { + id: string; +}; -export const router = express.Router(); - -router.get('/', async (_, res) => { - res - .status(200) - .send([ - { id: 'component1' }, - { id: 'component2' }, - { id: 'component3' }, - { id: 'component4' }, - ]); -}); +export type Inventory = { + list: () => Promise; + item: (id: string) => Promise; +}; diff --git a/plugins/inventory-backend/src/run.ts b/plugins/inventory-backend/src/run.ts new file mode 100644 index 0000000000..979827c40f --- /dev/null +++ b/plugins/inventory-backend/src/run.ts @@ -0,0 +1,34 @@ +/* + * 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 { startServer } from './service/server'; + +startServer({ + port: process.env.PLUGIN_PORT ? Number(process.env.PLUGIN_PORT) : 3003, + enableCors: process.env.PLUGIN_CORS + ? Boolean(process.env.PLUGIN_CORS) + : false, + logger: getRootLogger(), +}).catch(err => { + getRootLogger().error(err); + process.exit(1); +}); + +process.on('SIGINT', () => { + getRootLogger().info('CTRL+C pressed; exiting.'); + process.exit(0); +}); diff --git a/plugins/inventory-backend/src/service/application.ts b/plugins/inventory-backend/src/service/application.ts new file mode 100644 index 0000000000..e5cf151050 --- /dev/null +++ b/plugins/inventory-backend/src/service/application.ts @@ -0,0 +1,51 @@ +/* + * 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 { createRouter } from './router'; + +export interface ApplicationOptions { + enableCors: boolean; + logger: Logger; +} + +export async function createApplication( + options: ApplicationOptions, +): Promise { + const app = express(); + + app.use(helmet()); + if (options.enableCors) { + app.use(cors()); + } + app.use(compression()); + app.use(express.json()); + app.use(requestLoggingHandler()); + app.use('/', await createRouter({ logger: options.logger })); + app.use(notFoundHandler()); + app.use(errorHandler()); + + return app; +} diff --git a/plugins/inventory-backend/src/service/router.ts b/plugins/inventory-backend/src/service/router.ts new file mode 100644 index 0000000000..afa4a543b3 --- /dev/null +++ b/plugins/inventory-backend/src/service/router.ts @@ -0,0 +1,61 @@ +/* + * 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 express from 'express'; +import { Logger } from 'winston'; +import { AggregatorInventory, StaticInventory } from '../inventory'; + +export interface RouterOptions { + logger: Logger; +} + +export async function createRouter( + options: RouterOptions, +): Promise { + const logger = options.logger.child({ plugin: 'inventory' }); + + const inventory = new AggregatorInventory(); + inventory.enlist( + new StaticInventory([ + { id: 'component1' }, + { id: 'component2' }, + { id: 'component3' }, + { id: 'component4' }, + ]), + ); + + const router = express.Router(); + router + .get('/', async (req, res) => { + const components = await inventory.list(); + res.status(200).send(components); + }) + .get('/:id', async (req, res) => { + const { id } = req.params; + const component = await inventory.item(id); + if (component) { + res.status(200).send(component); + } else { + res.status(404).send(); + } + }); + + const app = express(); + app.set('logger', logger); + app.use('/', router); + + return app; +} diff --git a/plugins/inventory-backend/src/service/server.ts b/plugins/inventory-backend/src/service/server.ts new file mode 100644 index 0000000000..9715a72fef --- /dev/null +++ b/plugins/inventory-backend/src/service/server.ts @@ -0,0 +1,48 @@ +/* + * 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 { Server } from 'http'; +import { Logger } from 'winston'; +import { createApplication } from './application'; + +export interface ServerOptions { + port: number; + enableCors: boolean; + logger: Logger; +} + +export async function startServer(options: ServerOptions): Promise { + const logger = options.logger.child({ service: 'inventory-backend' }); + + logger.debug('Creating application...'); + const app = await createApplication({ + enableCors: options.enableCors, + 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); + }); + }); +} diff --git a/plugins/inventory-backend/src/setupTests.ts b/plugins/inventory-backend/src/setupTests.ts index f3b69cc361..3fa7cb04b4 100644 --- a/plugins/inventory-backend/src/setupTests.ts +++ b/plugins/inventory-backend/src/setupTests.ts @@ -13,3 +13,5 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + +require('jest-fetch-mock').enableMocks(); diff --git a/plugins/inventory-backend/tsconfig.json b/plugins/inventory-backend/tsconfig.json index b463ac102f..1a3f7ca819 100644 --- a/plugins/inventory-backend/tsconfig.json +++ b/plugins/inventory-backend/tsconfig.json @@ -10,6 +10,7 @@ "target": "es5", "module": "commonjs", "esModuleInterop": true, + "lib": ["es2019"], "types": ["node", "jest"] } }