feat(backend-common): base backend config

This commit is contained in:
Fredrik Adelöw
2020-06-18 11:19:01 +02:00
parent 2156b06280
commit 324b0a767a
7 changed files with 182 additions and 16 deletions
+6
View File
@@ -4,6 +4,12 @@ app:
backend:
baseUrl: http://localhost:7000
bindPort: 7000
bindHost: localhost
cors:
origin: http://localhost:3000
methods: [GET, POST, PUT, DELETE]
credentials: true
organization:
name: Spotify
+1
View File
@@ -29,6 +29,7 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/config": "^0.1.1-alpha.9",
"@types/cors": "^2.8.6",
"@types/express": "^4.17.6",
"compression": "^1.7.4",
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { ServiceBuilderImpl } from './ServiceBuilderImpl';
import { ServiceBuilderImpl } from './lib/ServiceBuilderImpl';
/**
* Creates a new service builder.
@@ -14,6 +14,7 @@
* limitations under the License.
*/
import { ConfigReader } from '@backstage/config';
import compression from 'compression';
import cors from 'cors';
import express, { Router } from 'express';
@@ -21,37 +22,66 @@ import helmet from 'helmet';
import { Server } from 'http';
import stoppable from 'stoppable';
import { Logger } from 'winston';
import { getRootLogger } from '../logging';
import { useHotCleanup } from '../../hot';
import { getRootLogger } from '../../logging';
import {
errorHandler,
notFoundHandler,
requestLoggingHandler,
} from '../middleware';
import { ServiceBuilder } from './types';
import { useHotCleanup } from '../hot';
} from '../../middleware';
import { ServiceBuilder } from '../types';
import { readBaseOptions, readCorsOptions } from './config';
const DEFAULT_PORT = 7000;
const DEFAULT_HOST = 'localhost';
export class ServiceBuilderImpl implements ServiceBuilder {
private port: number | undefined;
private host: string | undefined;
private logger: Logger | undefined;
private corsOptions: cors.CorsOptions | undefined;
private routers: [string, Router][];
/**
* Reference to the module where builder is created
* Needed for the HMR
*/
// Reference to the module where builder is created - needed for hot module
// reloading
private module: NodeModule;
constructor(module: NodeModule) {
this.routers = [];
this.module = module;
}
loadConfig(config: ConfigReader): ServiceBuilder {
const backendConfig = config.getOptionalConfig('backend');
if (!backendConfig) {
return this;
}
const baseOptions = readBaseOptions(backendConfig);
if (baseOptions.bindPort) {
this.port = baseOptions.bindPort;
}
if (baseOptions.bindHost) {
this.host = baseOptions.bindHost;
}
const corsOptions = readCorsOptions(backendConfig);
if (corsOptions) {
this.corsOptions = corsOptions;
}
return this;
}
setPort(port: number): ServiceBuilder {
this.port = port;
return this;
}
setHost(host: string): ServiceBuilder {
this.host = host;
return this;
}
setLogger(logger: Logger): ServiceBuilder {
this.logger = logger;
return this;
@@ -69,7 +99,7 @@ export class ServiceBuilderImpl implements ServiceBuilder {
start(): Promise<Server> {
const app = express();
const { port, logger, corsOptions } = this.getOptions();
const { port, host, logger, corsOptions } = this.getOptions();
app.use(helmet());
if (corsOptions) {
@@ -89,8 +119,9 @@ export class ServiceBuilderImpl implements ServiceBuilder {
logger.error(`Failed to start up on port ${port}, ${e}`);
reject(e);
});
const server = stoppable(
app.listen(port, () => {
app.listen(port, host, () => {
logger.info(`Listening on port ${port}`);
}),
0,
@@ -108,6 +139,7 @@ export class ServiceBuilderImpl implements ServiceBuilder {
private getOptions(): {
port: number;
host: string;
logger: Logger;
corsOptions?: cors.CorsOptions;
} {
@@ -118,6 +150,13 @@ export class ServiceBuilderImpl implements ServiceBuilder {
port = parseInt(process.env.PORT ?? '', 10) || DEFAULT_PORT;
}
let host: string;
if (this.host !== undefined) {
host = this.host;
} else {
host = process.env.HOST || DEFAULT_HOST;
}
let logger: Logger;
if (this.logger) {
logger = this.logger;
@@ -127,6 +166,7 @@ export class ServiceBuilderImpl implements ServiceBuilder {
return {
port,
host,
logger,
corsOptions: this.corsOptions,
};
@@ -0,0 +1,112 @@
/*
* 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 { ConfigReader } from '@backstage/config';
import { CorsOptions } from 'cors';
export type BaseOptions = {
bindPort?: number;
bindHost?: string;
};
/**
* Reads some base options out of a config object.
*
* @param config The root of a backend config object
* @returns A base options object
*
* @example
* ```json
* {
* baseUrl: "http://localhost:7000"
* bindPort: 7000
* bindHost: "0.0.0.0"
* }
* ```
*/
export function readBaseOptions(config: ConfigReader): BaseOptions {
return removeUnknown({
bindPort: config.getOptionalNumber('bindPort'),
bindHost: config.getOptionalString('bindHost'),
});
}
/**
* Attempts to read a CORS options object from the root of a config object.
*
* @param config The root of a backend config object
* @returns A CORS options object, or undefined if not specified
*
* @example
* ```json
* {
* cors: {
* origin: "http://localhost:3000",
* credentials: true
* }
* }
* ```
*/
export function readCorsOptions(config: ConfigReader): CorsOptions | undefined {
const cc = config.getOptionalConfig('cors');
if (!cc) {
return undefined;
}
return removeUnknown({
origin: getOptionalStringOrStrings(cc, 'origin'),
methods: getOptionalStringOrStrings(cc, 'methods'),
allowedHeaders: getOptionalStringOrStrings(cc, 'allowedHeaders'),
exposedHeaders: getOptionalStringOrStrings(cc, 'exposedHeaders'),
credentials: cc.getOptionalBoolean('credentials'),
maxAge: cc.getOptionalNumber('maxAge'),
preflightContinue: cc.getOptionalBoolean('preflightContinue'),
optionsSuccessStatus: cc.getOptionalNumber('optionsSuccessStatus'),
});
}
function getOptionalStringOrStrings(
config: ConfigReader,
key: string,
): string | string[] | undefined {
const value = config.getOptional(key);
if (
value === undefined ||
typeof value === 'string' ||
isStringArray(value)
) {
return value;
}
throw new Error(`Expected string or array of strings, got ${typeof value}`);
}
function isStringArray(value: any): value is string[] {
if (!Array.isArray(value)) {
return false;
}
for (const v of value) {
if (typeof v !== 'string') {
return false;
}
}
return true;
}
function removeUnknown<T extends object>(obj: T): T {
return Object.fromEntries(
Object.entries(obj).filter(([, v]) => v !== undefined),
) as T;
}
@@ -14,12 +14,20 @@
* limitations under the License.
*/
import { ConfigReader } from '@backstage/config';
import cors from 'cors';
import { Router } from 'express';
import { Server } from 'http';
import { Logger } from 'winston';
export type ServiceBuilder = {
/**
* Sets the service parameters based on configuration.
*
* @param config The configuration to read
*/
loadConfig(config: ConfigReader): ServiceBuilder;
/**
* Sets the port to listen on.
*
+4 -5
View File
@@ -55,7 +55,9 @@ function makeCreateEnv(loadedConfigs: AppConfig[]) {
}
async function main() {
const createEnv = makeCreateEnv(await loadConfig());
const configs = await loadConfig();
const configReader = ConfigReader.fromConfigs(configs);
const createEnv = makeCreateEnv(configs);
const catalogEnv = useHotMemoize(module, () => createEnv('catalog'));
const scaffolderEnv = useHotMemoize(module, () => createEnv('scaffolder'));
@@ -63,10 +65,7 @@ async function main() {
const identityEnv = useHotMemoize(module, () => createEnv('identity'));
const service = createServiceBuilder(module)
.enableCors({
origin: 'http://localhost:3000',
credentials: true,
})
.loadConfig(configReader)
.addRouter('/catalog', await catalog(catalogEnv))
.addRouter('/scaffolder', await scaffolder(scaffolderEnv))
.addRouter(