diff --git a/packages/cactus-cmd-api-server/src/main/typescript/api-server.ts b/packages/cactus-cmd-api-server/src/main/typescript/api-server.ts index b89bf3e88a..9a6756020f 100644 --- a/packages/cactus-cmd-api-server/src/main/typescript/api-server.ts +++ b/packages/cactus-cmd-api-server/src/main/typescript/api-server.ts @@ -34,6 +34,7 @@ import { PluginImport, Constants, PluginImportAction, + isIPluginGrpcService, } from "@hyperledger/cactus-core-api"; import { @@ -105,6 +106,7 @@ export class ApiServer { private readonly enableShutdownHook: boolean; public prometheusExporter: PrometheusExporter; + public boundGrpcHostPort: string; public get className(): string { return ApiServer.CLASS_NAME; @@ -118,6 +120,8 @@ export class ApiServer { throw new Error(`ApiServer#ctor options.config was falsy`); } + this.boundGrpcHostPort = "127.0.0.1:-1"; + this.enableShutdownHook = Bools.isBooleanStrict( options.config.enableShutdownHook, ) @@ -462,8 +466,12 @@ export class ApiServer { } if (this.grpcServer) { - this.log.info(`Closing Cacti gRPC server ...`); await new Promise((resolve, reject) => { + this.log.info(`Draining Cacti gRPC server ...`); + this.grpcServer.drain(this.boundGrpcHostPort, 5000); + this.log.info(`Drained Cacti gRPC server OK`); + + this.log.info(`Trying to shut down Cacti gRPC server ...`); this.grpcServer.tryShutdown((ex?: Error) => { if (ex) { const eMsg = @@ -471,11 +479,11 @@ export class ApiServer { this.log.debug(eMsg, ex); reject(newRex(eMsg, ex)); } else { + this.log.info(`Shut down Cacti gRPC server OK`); resolve(); } }); }); - this.log.info(`Close gRPC server OK`); } } @@ -648,6 +656,11 @@ export class ApiServer { } async startGrpcServer(): Promise { + const fnTag = `${this.className}#startGrpcServer()`; + const { log } = this; + const { logLevel } = this.options.config; + const pluginRegistry = await this.getOrInitPluginRegistry(); + return new Promise((resolve, reject) => { // const grpcHost = "0.0.0.0"; // FIXME - make this configurable (config-service.ts) const grpcHost = "127.0.0.1"; // FIXME - make this configurable (config-service.ts) @@ -672,15 +685,46 @@ export class ApiServer { new GrpcServerApiServer(), ); + log.debug("Installing gRPC services of IPluginGrpcService instances..."); + pluginRegistry.getPlugins().forEach(async (x: ICactusPlugin) => { + if (!isIPluginGrpcService(x)) { + this.log.debug("%s skipping %s instance", fnTag, x.getPackageName()); + return; + } + const opts = { logLevel }; + log.info("%s Creating gRPC service of: %s", fnTag, x.getPackageName()); + + const svcPairs = await x.createGrpcSvcDefAndImplPairs(opts); + log.debug("%s Obtained %o gRPC svc pairs OK", fnTag, svcPairs.length); + + svcPairs.forEach(({ definition, implementation }) => { + const svcNames = Object.values(definition).map((x) => x.originalName); + const svcPaths = Object.values(definition).map((x) => x.path); + log.debug("%s Adding gRPC svc names %o ...", fnTag, svcNames); + log.debug("%s Adding gRPC svc paths %o ...", fnTag, svcPaths); + this.grpcServer.addService(definition, implementation); + log.debug("%s Added gRPC svc OK ...", fnTag); + }); + + log.info("%s Added gRPC service of: %s OK", fnTag, x.getPackageName()); + }); + log.debug("%s Installed all IPluginGrpcService instances OK", fnTag); + this.grpcServer.bindAsync( grpcHostAndPort, grpcTlsCredentials, (error: Error | null, port: number) => { if (error) { - this.log.error("Binding gRPC failed: ", error); - return reject(new RuntimeError("Binding gRPC failed: ", error)); + this.log.error("%s Binding gRPC failed: ", fnTag, error); + return reject(new RuntimeError(fnTag + " gRPC bindAsync:", error)); + } else { + this.log.info("%s gRPC server bound to port %o OK", fnTag, port); } - this.grpcServer.start(); + + const portStr = port.toString(10); + this.boundGrpcHostPort = grpcHost.concat(":").concat(portStr); + log.info("%s boundGrpcHostPort=%s", fnTag, this.boundGrpcHostPort); + const family = determineAddressFamily(grpcHost); resolve({ address: grpcHost, port, family }); }, diff --git a/packages/cactus-cmd-api-server/src/main/typescript/grpc/grpc-credentials-factory.ts b/packages/cactus-cmd-api-server/src/main/typescript/grpc/grpc-credentials-factory.ts new file mode 100644 index 0000000000..f587e18285 --- /dev/null +++ b/packages/cactus-cmd-api-server/src/main/typescript/grpc/grpc-credentials-factory.ts @@ -0,0 +1,91 @@ +import * as grpc from "@grpc/grpc-js"; + +/** + * Re-exports the underlying `grpc.ServerCredentials.createInsecure()` call + * verbatim. + * Why though? This is necessary because the {grpc.Server} object does an `instanceof` + * validation on credential objects that are passed to it and this check comes back + * negative if you've constructed the credentials object with a different instance + * of the library, **even** if the versions of the library instances are the **same**. + * + * Therefore this is a workaround that allows callers to construct credentials + * objects with the same import of the `@grpc/grpc-js` library that the {ApiServer} + * of this package is using. + * + * @returns {grpc.ServerCredentials} + */ +export function createGrpcInsecureServerCredentials(): grpc.ServerCredentials { + return grpc.ServerCredentials.createInsecure(); +} + +/** + * Re-exports the underlying `grpc.ServerCredentials.createInsecure()` call + * verbatim. + * Why though? This is necessary because the {grpc.Server} object does an `instanceof` + * validation on credential objects that are passed to it and this check comes back + * negative if you've constructed the credentials object with a different instance + * of the library, **even** if the versions of the library instances are the **same**. + * + * Therefore this is a workaround that allows callers to construct credentials + * objects with the same import of the `@grpc/grpc-js` library that the {ApiServer} + * of this package is using. + * + * @returns {grpc.ServerCredentials} + */ +export function createGrpcSslServerCredentials( + rootCerts: Buffer | null, + keyCertPairs: grpc.KeyCertPair[], + checkClientCertificate?: boolean, +): grpc.ServerCredentials { + return grpc.ServerCredentials.createSsl( + rootCerts, + keyCertPairs, + checkClientCertificate, + ); +} + +/** + * Re-exports the underlying `grpc.ServerCredentials.createInsecure()` call + * verbatim. + * Why though? This is necessary because the {grpc.Server} object does an `instanceof` + * validation on credential objects that are passed to it and this check comes back + * negative if you've constructed the credentials object with a different instance + * of the library, **even** if the versions of the library instances are the **same**. + * + * Therefore this is a workaround that allows callers to construct credentials + * objects with the same import of the `@grpc/grpc-js` library that the {ApiServer} + * of this package is using. + * + * @returns {grpc.ChannelCredentials} + */ +export function createGrpcInsecureChannelCredentials(): grpc.ChannelCredentials { + return grpc.ChannelCredentials.createInsecure(); +} + +/** + * Re-exports the underlying `grpc.ServerCredentials.createInsecure()` call + * verbatim. + * Why though? This is necessary because the {grpc.Server} object does an `instanceof` + * validation on credential objects that are passed to it and this check comes back + * negative if you've constructed the credentials object with a different instance + * of the library, **even** if the versions of the library instances are the **same**. + * + * Therefore this is a workaround that allows callers to construct credentials + * objects with the same import of the `@grpc/grpc-js` library that the {ApiServer} + * of this package is using. + * + * @returns {grpc.ChannelCredentials} + */ +export function createGrpcSslChannelCredentials( + rootCerts?: Buffer | null, + privateKey?: Buffer | null, + certChain?: Buffer | null, + verifyOptions?: grpc.VerifyOptions, +): grpc.ChannelCredentials { + return grpc.ChannelCredentials.createSsl( + rootCerts, + privateKey, + certChain, + verifyOptions, + ); +} diff --git a/packages/cactus-cmd-api-server/src/main/typescript/grpc/grpc-server-factory.ts b/packages/cactus-cmd-api-server/src/main/typescript/grpc/grpc-server-factory.ts new file mode 100644 index 0000000000..288bd7ea85 --- /dev/null +++ b/packages/cactus-cmd-api-server/src/main/typescript/grpc/grpc-server-factory.ts @@ -0,0 +1,21 @@ +import * as grpc from "@grpc/grpc-js"; + +/** + * Re-exports the underlying `new grpc.Server()` call verbatim. + * + * Why though? This is necessary because the {grpc.Server} object does an `instanceof` + * validation on credential objects that are passed to it and this check comes back + * negative if you've constructed the credentials object with a different instance + * of the library, **even** if the versions of the library instances are the **same**. + * + * Therefore this is a workaround that allows callers to construct credentials + * objects/servers with the same import of the `@grpc/grpc-js` library that the + * {ApiServer} of this package is using internally. + * + * @returns {grpc.Server} + */ +export function createGrpcServer( + options?: grpc.ServerOptions | undefined, +): grpc.Server { + return new grpc.Server(options); +} diff --git a/packages/cactus-cmd-api-server/src/main/typescript/public-api.ts b/packages/cactus-cmd-api-server/src/main/typescript/public-api.ts index 34a44578a1..911a7e60fe 100755 --- a/packages/cactus-cmd-api-server/src/main/typescript/public-api.ts +++ b/packages/cactus-cmd-api-server/src/main/typescript/public-api.ts @@ -39,3 +39,12 @@ export { } from "./authzn/authorizer-factory"; export { IAuthorizationConfig } from "./authzn/i-authorization-config"; export { AuthorizationProtocol } from "./config/authorization-protocol"; + +export { + createGrpcInsecureChannelCredentials, + createGrpcInsecureServerCredentials, + createGrpcSslChannelCredentials, + createGrpcSslServerCredentials, +} from "./grpc/grpc-credentials-factory"; + +export { createGrpcServer } from "./grpc/grpc-server-factory";