Skip to content

Commit

Permalink
feat(core-api): decouple web service install & registration #771
Browse files Browse the repository at this point in the history
Primary change:
==============

Divide the endpoint installation method into two smaller
ones where the first one only instantiates the endpoints
and the other one "registers" them as-in it hooks the
endpoint up onto the ExpressJS web app object so that
it will actually be responded to as HTTP requests come
in.

Why though?
We'll need this for the authorization MVP which will
need to be able to introspect the endpoints **prior**
to them getting registered so that it can determine
authz specific parameters like "is this endpoint
secured or non-secure?" or what scopes are necessary
for this endpoint? Etc.

Additional changes:
==================

Everything else that was needed to make the project
compile again and the tests passnig (which is a lot
because this is a very central, heavily used
interface that we just modified).

Fixes #771

Signed-off-by: Peter Somogyvari <[email protected]>
  • Loading branch information
petermetz authored and kikoncuo committed Apr 8, 2021
1 parent 77ac399 commit b50e148
Show file tree
Hide file tree
Showing 21 changed files with 179 additions and 95 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { Optional } from "typescript-optional";
import { Express } from "express";

import {
Logger,
Checks,
Expand All @@ -18,6 +20,7 @@ import {
import { DefaultApi as BesuApi } from "@hyperledger/cactus-plugin-ledger-connector-besu";
import { InsertBambooHarvestEndpoint } from "./web-services/insert-bamboo-harvest-endpoint";
import { DefaultApi as FabricApi } from "@hyperledger/cactus-plugin-ledger-connector-fabric";

import { ListBambooHarvestEndpoint } from "./web-services/list-bamboo-harvest-endpoint";
import { ISupplyChainContractDeploymentInfo } from "../i-supply-chain-contract-deployment-info";
import { InsertBookshelfEndpoint } from "./web-services/insert-bookshelf-endpoint";
Expand Down Expand Up @@ -51,6 +54,8 @@ export class SupplyChainCactusPlugin
private readonly log: Logger;
private readonly instanceId: string;

private endpoints: IWebServiceEndpoint[] | undefined;

public get className(): string {
return SupplyChainCactusPlugin.CLASS_NAME;
}
Expand All @@ -73,9 +78,16 @@ export class SupplyChainCactusPlugin
this.instanceId = options.instanceId;
}

public async installWebServices(
expressApp: any,
): Promise<IWebServiceEndpoint[]> {
async registerWebServices(app: Express): Promise<IWebServiceEndpoint[]> {
const webServices = await this.getOrCreateWebServices();
webServices.forEach((ws) => ws.registerExpress(app));
return webServices;
}

public async getOrCreateWebServices(): Promise<IWebServiceEndpoint[]> {
if (Array.isArray(this.endpoints)) {
return this.endpoints;
}
const insertBambooHarvest = new InsertBambooHarvestEndpoint({
contractAddress: this.options.contracts.bambooHarvestRepository.address,
contractAbi: this.options.contracts.bambooHarvestRepository.abi,
Expand All @@ -84,15 +96,13 @@ export class SupplyChainCactusPlugin
.web3SigningCredential as Web3SigningCredential,
logLevel: this.options.logLevel,
});
insertBambooHarvest.registerExpress(expressApp);

const listBambooHarvest = new ListBambooHarvestEndpoint({
contractAddress: this.options.contracts.bambooHarvestRepository.address,
contractAbi: this.options.contracts.bambooHarvestRepository.abi,
apiClient: this.options.quorumApiClient,
logLevel: this.options.logLevel,
});
listBambooHarvest.registerExpress(expressApp);

const insertBookshelf = new InsertBookshelfEndpoint({
contractAddress: this.options.contracts.bookshelfRepository.address,
Expand All @@ -102,36 +112,33 @@ export class SupplyChainCactusPlugin
.web3SigningCredential as Web3SigningCredential,
logLevel: this.options.logLevel,
});
insertBookshelf.registerExpress(expressApp);

const listBookshelf = new ListBookshelfEndpoint({
contractAddress: this.options.contracts.bookshelfRepository.address,
contractAbi: this.options.contracts.bookshelfRepository.abi,
besuApi: this.options.besuApiClient,
logLevel: this.options.logLevel,
});
listBookshelf.registerExpress(expressApp);

const insertShipment = new InsertShipmentEndpoint({
logLevel: this.options.logLevel,
fabricApi: this.options.fabricApiClient,
});
insertShipment.registerExpress(expressApp);

const listShipment = new ListShipmentEndpoint({
logLevel: this.options.logLevel,
fabricApi: this.options.fabricApiClient,
});

listShipment.registerExpress(expressApp);
return [
this.endpoints = [
insertBambooHarvest,
listBambooHarvest,
insertBookshelf,
listBookshelf,
insertShipment,
listShipment,
];
return this.endpoints;
}

public getHttpServer(): Optional<any> {
Expand Down
9 changes: 9 additions & 0 deletions packages/cactus-cmd-api-server/package-lock.json

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

3 changes: 2 additions & 1 deletion packages/cactus-cmd-api-server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@
"@types/node-forge": "0.9.3",
"@types/npm": "2.0.31",
"@types/semver": "7.3.1",
"@types/uuid": "7.0.2"
"@types/uuid": "7.0.2",
"@types/xml2js": "0.4.8"
}
}
46 changes: 28 additions & 18 deletions packages/cactus-cmd-api-server/src/main/typescript/api-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -372,20 +372,12 @@ export class ApiServer {
return addressInfo;
}

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

const apiCorsDomainCsv = this.options.config.apiCorsDomainCsv;
const allowedDomains = apiCorsDomainCsv.split(",");
const corsMiddleware = this.createCorsMiddleware(allowedDomains);
app.use(corsMiddleware);

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

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

/**
* Installs the own endpoints of the API server such as the ones providing
* healthcheck and monitoring information.
* @param app
*/
async getOrCreateWebServices(app: express.Application): Promise<void> {
const healthcheckHandler = (req: Request, res: Response) => {
res.json({
success: true,
Expand Down Expand Up @@ -420,16 +412,34 @@ export class ApiServer {
httpPathPrometheus,
prometheusExporterHandler,
);
}

const registry = await this.getOrInitPluginRegistry();
async startApiServer(): Promise<AddressInfo> {
const pluginRegistry = await this.getOrInitPluginRegistry();

const app: Application = express();
app.use(compression());

const apiCorsDomainCsv = this.options.config.apiCorsDomainCsv;
const allowedDomains = apiCorsDomainCsv.split(",");
const corsMiddleware = this.createCorsMiddleware(allowedDomains);
app.use(corsMiddleware);

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

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

this.getOrCreateWebServices(app); // The API server's own endpoints

this.log.info(`Starting to install web services...`);

const webServicesInstalled = registry
const webServicesInstalled = pluginRegistry
.getPlugins()
.filter((pluginInstance) => isIPluginWebService(pluginInstance))
.map((pluginInstance: ICactusPlugin) => {
return (pluginInstance as IPluginWebService).installWebServices(app);
.map(async (plugin: ICactusPlugin) => {
await (plugin as IPluginWebService).getOrCreateWebServices();
return (plugin as IPluginWebService).registerWebServices(app);
});

const endpoints2D = await Promise.all(webServicesInstalled);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { IWebServiceEndpoint } from "./i-web-service-endpoint";
import { ICactusPlugin } from "../i-cactus-plugin";
import { Server } from "http";
import { Server as SecureServer } from "https";
import { Optional } from "typescript-optional";
import { Application } from "express";
import { IWebServiceEndpoint } from "./i-web-service-endpoint";
import { ICactusPlugin } from "../i-cactus-plugin";

export interface IPluginWebService extends ICactusPlugin {
installWebServices(expressApp: any): Promise<IWebServiceEndpoint[]>;
getOrCreateWebServices(): Promise<IWebServiceEndpoint[]>;
registerWebServices(expressApp: Application): Promise<IWebServiceEndpoint[]>;
getHttpServer(): Optional<Server | SecureServer>;
shutdown(): Promise<void>;
}
Expand All @@ -15,7 +17,9 @@ export function isIPluginWebService(
): pluginInstance is IPluginWebService {
return (
pluginInstance &&
typeof (pluginInstance as IPluginWebService).installWebServices ===
typeof (pluginInstance as IPluginWebService).registerWebServices ===
"function" &&
typeof (pluginInstance as IPluginWebService).getOrCreateWebServices ===
"function" &&
typeof (pluginInstance as IPluginWebService).getHttpServer === "function" &&
typeof (pluginInstance as IPluginWebService).getPackageName ===
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export class PluginConsortiumManual
public prometheusExporter: PrometheusExporter;
private readonly log: Logger;
private readonly instanceId: string;
private endpoints: IWebServiceEndpoint[] | undefined;
private httpServer: Server | SecureServer | null = null;

constructor(public readonly options: IPluginConsortiumManualOptions) {
Expand Down Expand Up @@ -126,16 +127,11 @@ export class PluginConsortiumManual
}
}

public async installWebServices(
expressApp: Express,
public async registerWebServices(
app: Express,
): Promise<IWebServiceEndpoint[]> {
const { log } = this;

log.info(`Installing web services for plugin ${this.getPackageName()}...`);
const webApp: Express = this.options.webAppOptions ? express() : expressApp;
const webApp: Express = this.options.webAppOptions ? express() : app;

// presence of webAppOptions implies that caller wants the plugin to configure it's own express instance on a custom
// host/port to listen on
if (this.options.webAppOptions) {
this.log.info(`Creating dedicated HTTP server...`);
const { port, hostname } = this.options.webAppOptions;
Expand All @@ -157,6 +153,22 @@ export class PluginConsortiumManual
this.log.info(`Creation of HTTP server OK`, { address });
}

const webServices = await this.getOrCreateWebServices();
webServices.forEach((ws) => ws.registerExpress(webApp));
return webServices;
}

public async getOrCreateWebServices(): Promise<IWebServiceEndpoint[]> {
const { log } = this;
const pkgName = this.getPackageName();

if (this.endpoints) {
return this.endpoints;
}
log.info(`Creating web services for plugin ${pkgName}...`);
// presence of webAppOptions implies that caller wants the plugin to configure it's own express instance on a custom
// host/port to listen on

const { consortiumDatabase, keyPairPem } = this.options;
const consortiumRepo = new ConsortiumRepository({
db: consortiumDatabase,
Expand All @@ -166,32 +178,30 @@ export class PluginConsortiumManual
{
const options = { keyPairPem, consortiumRepo };
const endpoint = new GetConsortiumEndpointV1(options);
const path = endpoint.getPath();
webApp.get(path, endpoint.getExpressRequestHandler());
endpoints.push(endpoint);
this.log.info(`Registered GetConsortiumEndpointV1 at ${path}`);
const path = endpoint.getPath();
this.log.info(`Instantiated GetConsortiumEndpointV1 at ${path}`);
}
{
const options = { keyPairPem, consortiumRepo, plugin: this };
const endpoint = new GetNodeJwsEndpoint(options);
const path = endpoint.getPath();
webApp.get(path, endpoint.getExpressRequestHandler());
endpoints.push(endpoint);
this.log.info(`Registered GetNodeJwsEndpoint at ${path}`);
this.log.info(`Instantiated GetNodeJwsEndpoint at ${path}`);
}
{
const opts: IGetPrometheusExporterMetricsEndpointV1Options = {
plugin: this,
logLevel: this.options.logLevel,
};
const endpoint = new GetPrometheusExporterMetricsEndpointV1(opts);
endpoint.registerExpress(expressApp);
const path = endpoint.getPath();
endpoints.push(endpoint);
this.log.info(`Instantiated GetNodeJwsEndpoint at ${path}`);
}
this.endpoints = endpoints;

log.info(`Installed web svcs for plugin ${this.getPackageName()} OK`, {
endpoints,
});
log.info(`Instantiated web svcs for plugin ${pkgName} OK`, { endpoints });
return endpoints;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,8 @@ test("Can provide JWS", async (t: Test) => {
);
const apiClient = new ConsortiumManualApi({ basePath: apiHost });

await pluginConsortiumManual.installWebServices(expressApp);
await pluginConsortiumManual.getOrCreateWebServices();
await pluginConsortiumManual.registerWebServices(expressApp);

const epOpts: IGetNodeJwsEndpointOptions = {
plugin: pluginConsortiumManual,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ export class PluginKeychainMemory {
return res;
}

public async installWebServices(
public async getOrCreateWebServices(
expressApp: Express,
): Promise<IWebServiceEndpoint[]> {
const { log } = this;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ test("PluginKeychainMemory", (t1: Test) => {
);
const apiClient = new KeychainMemoryApi({ basePath: apiHost });

await plugin.installWebServices(expressApp);
await plugin.getOrCreateWebServices(expressApp);

t.equal(plugin.getKeychainId(), options.keychainId, "Keychain ID set OK");
t.equal(plugin.getInstanceId(), options.instanceId, "Instance ID set OK");
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Server } from "http";
import { Server as SecureServer } from "https";

import { Express } from "express";
import { Optional } from "typescript-optional";

import {
Expand Down Expand Up @@ -79,10 +80,15 @@ export class PluginKeychainVaultRemoteAdapter
*
* @param _expressApp
*/
public async installWebServices(): Promise<IWebServiceEndpoint[]> {
public async getOrCreateWebServices(): Promise<IWebServiceEndpoint[]> {
return [];
}

/* eslint-disable-next-line @typescript-eslint/no-unused-vars */
public registerWebServices(app: Express): Promise<IWebServiceEndpoint[]> {
return this.getOrCreateWebServices();
}

public getHttpServer(): Optional<Server | SecureServer> {
return Optional.empty();
}
Expand Down
Loading

0 comments on commit b50e148

Please sign in to comment.