feat(backend-common): base backend config
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
+50
-10
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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