From 37e7720f5619784eee7af165bce8589a0ac3fd66 Mon Sep 17 00:00:00 2001 From: Peter Somogyvari Date: Wed, 3 Apr 2024 22:23:37 -0700 Subject: [PATCH] feat(cmd-api-server): add gRPC plugin auto-registration support 1. The API server supports gRPC endpoints, but plugins are not yet able to register their own gRPC services to be exposed the same way that was already possible for HTTP endpoints to be registered dynamically. This was due to an oversight when the original contribution was made by Peter (who was the person making the oversight - good job Peter) 2. The functionality works largely the same as it does for the HTTP endpoints but it does so for gRPC services (which is the equivalent of endpoints in gRPC terminology, so service === endpoint in this context.) 3. There are new methods added to the public API surface of the API server package which can be used to construct gRPC credential and server objects using the instance of the library that is used by the API server. This is necessary because the validation logic built into grpc-js fails for these mentioned objects if the creds or the server was constructed with a different instance of the library than the one used by the API server. 4. Different instance in this context means just that the exact same version of the library was imported from a different path for example there could be the node_modules directory of the besu connector and also the node_modules directory of the API server. 5. Because of the problem outlined above, the only way we can have functioning test cases is if the API server exposes its own instance of grpc-js. Signed-off-by: Peter Somogyvari --- .../src/main/typescript/api-server.ts | 54 ++++++++++- .../grpc/grpc-credentials-factory.ts | 91 +++++++++++++++++++ .../typescript/grpc/grpc-server-factory.ts | 21 +++++ .../src/main/typescript/public-api.ts | 9 ++ 4 files changed, 170 insertions(+), 5 deletions(-) create mode 100644 packages/cactus-cmd-api-server/src/main/typescript/grpc/grpc-credentials-factory.ts create mode 100644 packages/cactus-cmd-api-server/src/main/typescript/grpc/grpc-server-factory.ts 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";