Merge pull request #1373 from spotify/freben/backend-config
feat(backend-common): base backend config
This commit is contained in:
@@ -4,6 +4,11 @@ app:
|
||||
|
||||
backend:
|
||||
baseUrl: http://localhost:7000
|
||||
listen: 0.0.0.0:7000
|
||||
cors:
|
||||
origin: http://localhost:3000
|
||||
methods: [GET, POST, PUT, DELETE]
|
||||
credentials: true
|
||||
|
||||
organization:
|
||||
name: Spotify
|
||||
|
||||
@@ -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.
|
||||
|
||||
+46
-27
@@ -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.listenPort) {
|
||||
this.port = baseOptions.listenPort;
|
||||
}
|
||||
if (baseOptions.listenHost) {
|
||||
this.host = baseOptions.listenHost;
|
||||
}
|
||||
|
||||
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,9 +119,10 @@ export class ServiceBuilderImpl implements ServiceBuilder {
|
||||
logger.error(`Failed to start up on port ${port}, ${e}`);
|
||||
reject(e);
|
||||
});
|
||||
|
||||
const server = stoppable(
|
||||
app.listen(port, () => {
|
||||
logger.info(`Listening on port ${port}`);
|
||||
app.listen(port, host, () => {
|
||||
logger.info(`Listening on ${host}:${port}`);
|
||||
}),
|
||||
0,
|
||||
);
|
||||
@@ -108,26 +139,14 @@ export class ServiceBuilderImpl implements ServiceBuilder {
|
||||
|
||||
private getOptions(): {
|
||||
port: number;
|
||||
host: string;
|
||||
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,
|
||||
port: this.port ?? DEFAULT_PORT,
|
||||
host: this.host ?? DEFAULT_HOST,
|
||||
logger: this.logger ?? getRootLogger(),
|
||||
corsOptions: this.corsOptions,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
/*
|
||||
* 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 = {
|
||||
listenPort?: number;
|
||||
listenHost?: 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",
|
||||
* listen: "0.0.0.0:7000"
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export function readBaseOptions(config: ConfigReader): BaseOptions {
|
||||
// TODO(freben): Expand this to support more addresses and perhaps optional
|
||||
const { host, port } = parseListenAddress(config.getString('listen'));
|
||||
return removeUnknown({
|
||||
listenPort: port,
|
||||
listenHost: host,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
function parseListenAddress(value: string): { host?: string; port?: number } {
|
||||
const parts = value.split(':');
|
||||
if (parts.length === 1) {
|
||||
return { port: parseInt(parts[0], 10) };
|
||||
}
|
||||
if (parts.length === 2) {
|
||||
return { host: parts[0], port: parseInt(parts[1], 10) };
|
||||
}
|
||||
throw new Error(
|
||||
`Unable to parse listen address ${value}, expected <port> or <host>:<port>`,
|
||||
);
|
||||
}
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user