Merge pull request #1194 from spotify/freben/backend-common-service

feat(backend-common): add common code for service shell
This commit is contained in:
Fredrik Adelöw
2020-06-10 11:33:24 +02:00
committed by GitHub
11 changed files with 263 additions and 136 deletions
+5
View File
@@ -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",
+1
View File
@@ -17,3 +17,4 @@
export * from './errors';
export * from './logging';
export * from './middleware';
export * from './service';
@@ -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<Server> {
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,
};
}
}
@@ -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();
}
@@ -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';
@@ -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<Server>;
};
-5
View File
@@ -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",
+19 -38
View File
@@ -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);
});
}
-3
View File
@@ -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",
@@ -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<express.Application> {
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;
}
@@ -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<Server> {
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);
});
}