Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(cmd-api-server): add gRPC plugin auto-registration support #3173

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {
PluginImport,
Constants,
PluginImportAction,
isIPluginGrpcService,
} from "@hyperledger/cactus-core-api";

import {
Expand Down Expand Up @@ -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;
Expand All @@ -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,
)
Expand Down Expand Up @@ -462,20 +466,24 @@ export class ApiServer {
}

if (this.grpcServer) {
this.log.info(`Closing Cacti gRPC server ...`);
await new Promise<void>((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 =
"Failed to shut down gRPC server of the Cacti API server.";
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`);
}
}

Expand Down Expand Up @@ -648,6 +656,11 @@ export class ApiServer {
}

async startGrpcServer(): Promise<AddressInfo> {
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)
Expand All @@ -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 });
},
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
);
}
Original file line number Diff line number Diff line change
@@ -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);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Loading