Skip to content

Commit

Permalink
feat(core-api): web service plugins
Browse files Browse the repository at this point in the history
Signed-off-by: Peter Somogyvari <peter.somogyvari@accenture.com>
  • Loading branch information
petermetz committed May 19, 2020
1 parent 9a0a9b4 commit 8eaeb45
Show file tree
Hide file tree
Showing 34 changed files with 3,217 additions and 101 deletions.
6 changes: 3 additions & 3 deletions packages/bif-cmd-api-server/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/bif-cmd-api-server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@
},
"devDependencies": {
"@types/compression": "1.7.0",
"@types/convict": "4.2.1",
"@types/convict": "5.2.0",
"@types/cors": "2.8.6",
"@types/express": "4.17.3",
"@types/joi": "14.3.4",
Expand Down
209 changes: 126 additions & 83 deletions packages/bif-cmd-api-server/src/main/typescript/api-server.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,28 @@
import path from 'path';
import { Server } from 'http';
import { Config } from 'convict';
import express, { Express, Request, Response, NextFunction, RequestHandler, Application } from 'express';
import { OpenApiValidator } from 'express-openapi-validator';
import compression from 'compression';
import bodyParser from 'body-parser';
import cors, { CorsOptions } from 'cors';
import { IPluginKVStorage, PluginFactory } from '@hyperledger-labs/bif-core-api';
import { IPluginKVStorage, PluginFactory, ICactusPlugin, PluginAspect } from '@hyperledger-labs/bif-core-api';
import { CreateConsortiumEndpointV1 } from './consortium/routes/create-consortium-endpoint-v1';
import { IBifApiServerOptions, ConfigService } from './config/config-service';
import { BIF_OPEN_API_JSON } from './openapi-spec';
import { Logger, LoggerProvider } from '@hyperledger-labs/bif-common';
import { Servers } from './common/servers';

export interface IApiServerConstructorOptions {
plugins: ICactusPlugin[];
config: Config<IBifApiServerOptions>;
}

export class ApiServer {

private readonly log: Logger;
private httpServerApi: Server | null = null;
private httpServerCockpit: Server | null = null;

constructor(public readonly options: IApiServerConstructorOptions) {
if (!options) {
Expand All @@ -30,111 +35,149 @@ export class ApiServer {
}

async start(): Promise<void> {
await this.startCockpitFileServer();
await this.startApiServer();
try {
await this.startCockpitFileServer();
await this.startApiServer();
} catch (ex) {
this.log.error(`Failed to start ApiServer: ${ex.stack}`);
this.log.error(`Attempting shutdown...`);
await this.shutdown();
}
}

async startCockpitFileServer(): Promise<void> {
const cockpitWwwRoot = this.options.config.get('cockpitWwwRoot');
this.log.info(`wwwRoot: ${cockpitWwwRoot}`);
public getHttpServerApi(): Server | null {
return this.httpServerApi;
}

const resolvedWwwRoot = path.resolve(process.cwd(), cockpitWwwRoot);
this.log.info(`resolvedWwwRoot: ${resolvedWwwRoot}`);
public getHttpServerCockpit(): Server | null {
return this.httpServerCockpit;
}

const resolvedIndexHtml = path.resolve(resolvedWwwRoot + '/index.html');
this.log.info(`resolvedIndexHtml: ${resolvedIndexHtml}`);
public async shutdown(): Promise<void> {

const app: Express = express();
app.use(compression());
app.use(express.static(resolvedWwwRoot));
app.get('/*', (_, res) => res.sendFile(resolvedIndexHtml));
if (this.httpServerApi) {
this.log.info(`Closing HTTP server of the API...`);
await Servers.shutdown(this.httpServerApi);
this.log.info(`Close HTTP server of the API OK`);
}

if (this.httpServerCockpit) {
this.log.info(`Closing HTTP server of the cockpit ...`);
await Servers.shutdown(this.httpServerCockpit);
this.log.info(`Close HTTP server of the cockpit OK`);
}
}

const cockpitPort: number = this.options.config.get('cockpitPort');
const cockpitHost: string = this.options.config.get('cockpitHost');
async startCockpitFileServer(): Promise < void> {
const cockpitWwwRoot = this.options.config.get('cockpitWwwRoot');
this.log.info(`wwwRoot: ${cockpitWwwRoot}`);

await new Promise<any>((resolve, reject) => {
const httpServer = app.listen(cockpitPort, cockpitHost, () => {
// tslint:disable-next-line: no-console
console.log(`BIF Cockpit UI reachable on port ${cockpitPort}`);
resolve({ cockpitPort });
});
httpServer.on('error', (err) => reject(err));
const resolvedWwwRoot = path.resolve(process.cwd(), cockpitWwwRoot);
this.log.info(`resolvedWwwRoot: ${resolvedWwwRoot}`);

const resolvedIndexHtml = path.resolve(resolvedWwwRoot + '/index.html');
this.log.info(`resolvedIndexHtml: ${resolvedIndexHtml}`);

const app: Express = express();
app.use(compression());
app.use(express.static(resolvedWwwRoot));
app.get('/*', (_, res) => res.sendFile(resolvedIndexHtml));

const cockpitPort: number = this.options.config.get('cockpitPort');
const cockpitHost: string = this.options.config.get('cockpitHost');

await new Promise<any>((resolve, reject) => {
this.httpServerCockpit = app.listen(cockpitPort, cockpitHost, () => {
this.log.info(`Cactus Cockpit UI reachable on port http://${cockpitHost}:${cockpitPort}`);
resolve({ cockpitPort });
});
}
this.httpServerCockpit.on('error', (err: any) => reject(err));
});
}

async startApiServer(): Promise<void> {
const app: Application = express();
app.use(compression());
async startApiServer(): Promise < void> {
const app: Application = express();
app.use(compression());

const corsMiddleware = this.createCorsMiddleware()
const corsMiddleware = this.createCorsMiddleware()
app.use(corsMiddleware);

app.use(bodyParser.json({ limit: '50mb' }));
app.use(bodyParser.json({ limit: '50mb' }));

const openApiValidator = this.createOpenApiValidator();
await openApiValidator.install(app);
const openApiValidator = this.createOpenApiValidator();
await openApiValidator.install(app);

app.get('/healthcheck', (req: Request, res: Response, next: NextFunction) => {
res.json({ 'success': true, timestamp: new Date() });
});
app.get('/healthcheck', (req: Request, res: Response, next: NextFunction) => {
res.json({ 'success': true, timestamp: new Date() });
});

const storage: IPluginKVStorage = await this.createStoragePlugin();
const configService = new ConfigService();
const config = configService.getOrCreate();
const storage: IPluginKVStorage = await this.createStoragePlugin();
const configService = new ConfigService();
const config = configService.getOrCreate();
{
const endpoint = new CreateConsortiumEndpointV1({ storage, config });
app.post(endpoint.getPath(), endpoint.handleRequest.bind(endpoint));
}
const endpoint = new CreateConsortiumEndpointV1({ storage, config });
app.post(endpoint.getPath(), endpoint.handleRequest.bind(endpoint));
}

// FIXME
// app.get('/api/v1/consortium/:consortiumId', (req: Request, res: Response, next: NextFunction) => {
// res.json({ swagger: 'TODO' });
// });

const apiPort: number = this.options.config.get('apiPort');
const apiHost: string = this.options.config.get('apiHost');
await new Promise<any>((resolve, reject) => {
const httpServer = app.listen(apiPort, apiHost, () => {
// tslint:disable-next-line: no-console
console.log(`BIF API reachable on port ${apiPort}`);
resolve({ port: apiPort });
});
httpServer.on('error', (err) => reject(err));
});
// FIXME
// app.get('/api/v1/consortium/:consortiumId', (req: Request, res: Response, next: NextFunction) => {
// res.json({ swagger: 'TODO' });
// });

const apiPort: number = this.options.config.get('apiPort');
const apiHost: string = this.options.config.get('apiHost');
this.log.info(`Binding Cactus API to port ${apiPort}...`);
await new Promise<any>((resolve, reject) => {
const httpServerApi = app.listen(apiPort, apiHost, () => {
const address: any = httpServerApi.address();
this.log.info(`Successfully bound API to port ${apiPort}`, { address });
if (address && address.port) {
resolve({ port: address.port });
} else {
resolve({ port: apiPort });
}
});
this.httpServerApi = httpServerApi;
this.httpServerApi.on('error', (err) => reject(err));
});
}

createOpenApiValidator(): OpenApiValidator {
return new OpenApiValidator({
apiSpec: BIF_OPEN_API_JSON,
validateRequests: true,
validateResponses: false
});
}
createOpenApiValidator(): OpenApiValidator {
return new OpenApiValidator({
apiSpec: BIF_OPEN_API_JSON,
validateRequests: true,
validateResponses: false
});
}

async createStoragePlugin(): Promise<IPluginKVStorage> {
const storagePluginPackage = this.options.config.get('storagePluginPackage');
const { PluginFactoryKVStorage } = await import(storagePluginPackage);
const storagePluginOptionsJson = this.options.config.get('storagePluginOptionsJson');
const storagePluginOptions = JSON.parse(storagePluginOptionsJson);
const pluginFactory: PluginFactory<IPluginKVStorage, unknown> = new PluginFactoryKVStorage();
const plugin = await pluginFactory.create(storagePluginOptions);
return plugin;
async createStoragePlugin(): Promise < IPluginKVStorage > {
const kvStoragePlugin = this.options.plugins.find((p) => p.getAspect() === PluginAspect.KV_STORAGE);
if(kvStoragePlugin) {
return kvStoragePlugin as IPluginKVStorage;
}
const storagePluginPackage = this.options.config.get('storagePluginPackage');
const { PluginFactoryKVStorage } = await import(storagePluginPackage);
const storagePluginOptionsJson = this.options.config.get('storagePluginOptionsJson');
const storagePluginOptions = JSON.parse(storagePluginOptionsJson);
const pluginFactory: PluginFactory<IPluginKVStorage, unknown> = new PluginFactoryKVStorage();
const plugin = await pluginFactory.create(storagePluginOptions);
return plugin;
}

createCorsMiddleware(): RequestHandler {
const apiCorsDomainCsv = this.options.config.get('apiCorsDomainCsv');
const allowedDomains = apiCorsDomainCsv.split(',');
const allDomainsAllowed = allowedDomains.includes('*');

const corsOptions: CorsOptions = {
origin: (origin: string | undefined, callback) => {
if (allDomainsAllowed || origin && allowedDomains.indexOf(origin) !== -1) {
callback(null, true);
} else {
callback(new Error(`CORS not allowed for Origin "${origin}".`));
}
createCorsMiddleware(): RequestHandler {
const apiCorsDomainCsv = this.options.config.get('apiCorsDomainCsv');
const allowedDomains = apiCorsDomainCsv.split(',');
const allDomainsAllowed = allowedDomains.includes('*');

const corsOptions: CorsOptions = {
origin: (origin: string | undefined, callback) => {
if (allDomainsAllowed || origin && allowedDomains.indexOf(origin) !== -1) {
callback(null, true);
} else {
callback(new Error(`CORS not allowed for Origin "${origin}".`));
}
}
return cors(corsOptions);
}
return cors(corsOptions);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const main = async () => {
} else {
const configService = new ConfigService();
const config = configService.getOrCreate();
const apiServer = new ApiServer({ config });
const apiServer = new ApiServer({ config, plugins: [] });
await apiServer.start();
}
};
Expand Down
29 changes: 29 additions & 0 deletions packages/bif-cmd-api-server/src/main/typescript/common/servers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Server } from 'http';
import { Server as SecureServer } from 'https';

/**
* Utility class for handling common tasks for NodeJS HTTP/S server objects.
*/
export class Servers {

/**
* Returns with a promise that resolves when the server has been shut down. Rejects if anything goes wrong of if the
* parameters are invalid.
*
* @param server The server object that will be shut down.
*/
public static async shutdown(server: Server | SecureServer): Promise<void> {
if (!server) {
throw new TypeError(`Servers#shutdown() server was falsy. Need object.`);
}
return new Promise<void>((resolve, reject) => {
server.close((err: any) => {
if (err) {
reject(new Error(`Servers#shutdown() Failed to shut down server: ${err.stack}`));
} else {
resolve();
}
});
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,43 @@ export class ConfigService {
}
}

generateExampleConfig(): IBifApiServerOptions {
/**
* Remaps the example config returned by `newExampleConfig()` into a similar object whose keys are the designated
* environment variable names. As an example it returns something like this:
*
* ```json
* {
* "HTTP_PORT": "3000"
* }
* ```
*
* Where the output of `newExampleConfig()` would be something like this (example)
*
* ```json
* {
* "httpPort": "3000"
* }
* ```
*/
public newExampleConfigEnv(bifApiServerOptions?: IBifApiServerOptions): { [key: string]: string } {
bifApiServerOptions = bifApiServerOptions || this.newExampleConfig();
const configSchema: any = ConfigService.getConfigSchema();
return Object
.entries(bifApiServerOptions)
.reduce((acc: any, [key, value]) => {
const schemaObj: any = configSchema[key];
acc[schemaObj.env] = value;
return acc;
}, {});
}

public newExampleConfigConvict(bifApiServerOptions?: IBifApiServerOptions): Config<IBifApiServerOptions> {
bifApiServerOptions = bifApiServerOptions || this.newExampleConfig();
const env = this.newExampleConfigEnv(bifApiServerOptions);
return this.getOrCreate({ env });
}

public newExampleConfig(): IBifApiServerOptions {
const schema = ConfigService.getConfigSchema();

// FIXME most of this lowever level crypto code should be in a commons package that's universal
Expand Down Expand Up @@ -228,10 +264,10 @@ export class ConfigService {
};
}

getOrCreate(): Config<IBifApiServerOptions> {
getOrCreate(options?: { env?: any, args?: string[] }): Config<IBifApiServerOptions> {
if (!ConfigService.config) {
const schema: Schema<IBifApiServerOptions> = ConfigService.getConfigSchema();
ConfigService.config = convict(schema);
ConfigService.config = (convict as any)(schema, options);
if (ConfigService.config.get('configFile')) {
const configFilePath = ConfigService.config.get('configFile');
ConfigService.config.loadFile(configFilePath);
Expand Down
4 changes: 2 additions & 2 deletions packages/bif-cmd-api-server/src/main/typescript/public-api.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export { ApiServer } from './api-server';
export { ConfigService } from './config/config-service';
export { ApiServer, IApiServerConstructorOptions } from './api-server';
export { ConfigService, IBifApiServerOptions } from './config/config-service';
export { CreateConsortiumEndpointV1, ICreateConsortiumEndpointOptions } from './consortium/routes/create-consortium-endpoint-v1';

0 comments on commit 8eaeb45

Please sign in to comment.