From 497ea3226631fdcad763e6281ee058d91ca01988 Mon Sep 17 00:00:00 2001 From: Peter Somogyvari Date: Tue, 16 Jul 2024 18:59:08 -0700 Subject: [PATCH] test(test-tooling): add container image builder utilities 1. Currently our integration tests depend on pre-published container images to be on the official registry (ghcr.io). This has pros and cons. The pro is that we can pin the tests to a specific ledger version and then have confidence that the test code works with that specific image. On the other hand if the image itself has problems we won't know it until after it was published and then tests were executed with it (unless we perform manual testing which is a lot of effrot as it requires the manual modification of the test cases). 2. In order to gives us the ability to test against the container image definitions as they are in the current revision of the source code, we are adding here a couple of utility functions to streamline writing test cases that build the container images for themselves as part of the test case. An example of how to use it in a test case: ```typescript const imgConnectorJvm = await buildImageConnectorCordaServer({ logLevel, }); // ... connector = new CordaConnectorContainer({ logLevel, imageName: imgConnectorJvm.imageName, imageVersion: imgConnectorJvm.imageVersion, envVars: [envVarSpringAppJson], }); ``` Signed-off-by: Peter Somogyvari --- .../common/build-container-image.ts | 64 +++++++++ .../build-image-connector-corda-server.ts | 52 +++++++ .../build-image-corda-all-in-one-v4-12.ts | 49 +++++++ .../src/main/typescript/public-api.ts | 17 +++ tools/docker/corda-all-in-one/README.md | 128 ++++++++++++++++++ 5 files changed, 310 insertions(+) create mode 100644 packages/cactus-test-tooling/src/main/typescript/common/build-container-image.ts create mode 100644 packages/cactus-test-tooling/src/main/typescript/corda/build-image-connector-corda-server.ts create mode 100644 packages/cactus-test-tooling/src/main/typescript/corda/build-image-corda-all-in-one-v4-12.ts create mode 100644 tools/docker/corda-all-in-one/README.md diff --git a/packages/cactus-test-tooling/src/main/typescript/common/build-container-image.ts b/packages/cactus-test-tooling/src/main/typescript/common/build-container-image.ts new file mode 100644 index 0000000000..e73e16a515 --- /dev/null +++ b/packages/cactus-test-tooling/src/main/typescript/common/build-container-image.ts @@ -0,0 +1,64 @@ +import Docker, { ImageBuildContext, ImageBuildOptions } from "dockerode"; + +import { LoggerProvider, LogLevelDesc } from "@hyperledger/cactus-common"; + +export interface IBuildContainerImageRequest { + readonly logLevel: LogLevelDesc; + readonly buildDir: Readonly; + readonly imageFile: Readonly; + readonly imageTag: Readonly; + readonly dockerEngine?: Readonly; + readonly dockerodeImageBuildOptions?: Partial; + readonly dockerodeImageBuildContext?: Partial; +} + +export async function buildContainerImage( + req: Readonly, +): Promise { + if (!req) { + throw new Error("Expected arg req to be truthy."); + } + if (!req.buildDir) { + throw new Error("Expected arg req.buildDir to be truthy."); + } + if (!req.imageFile) { + throw new Error("Expected arg req.imageFile to be truthy."); + } + const logLevel: LogLevelDesc = req.logLevel || "INFO"; + const dockerEngine = req.dockerEngine || new Docker(); + + const log = LoggerProvider.getOrCreate({ + label: "build-container-image", + level: logLevel, + }); + + const imageBuildOptions: ImageBuildOptions = { + ...req.dockerodeImageBuildOptions, + t: req.imageTag, + }; + log.debug("imageBuildOptions=%o", imageBuildOptions); + + const imageBuildContext: ImageBuildContext = { + context: req.buildDir, + src: [req.imageFile, "."], + ...req.dockerodeImageBuildContext, + }; + log.debug("imageBuildContext=%o", imageBuildContext); + + const stream = await dockerEngine.buildImage( + imageBuildContext, + imageBuildOptions, + ); + + stream.on("data", (data: unknown) => { + if (data instanceof Buffer) { + log.debug("[Build]: ", data.toString("utf-8")); + } + }); + + await new Promise((resolve, reject) => { + dockerEngine.modem.followProgress(stream, (err, res) => + err ? reject(err) : resolve(res), + ); + }); +} diff --git a/packages/cactus-test-tooling/src/main/typescript/corda/build-image-connector-corda-server.ts b/packages/cactus-test-tooling/src/main/typescript/corda/build-image-connector-corda-server.ts new file mode 100644 index 0000000000..1bd8862518 --- /dev/null +++ b/packages/cactus-test-tooling/src/main/typescript/corda/build-image-connector-corda-server.ts @@ -0,0 +1,52 @@ +import path from "node:path"; +import { buildContainerImage } from "../public-api"; +import { LoggerProvider, LogLevelDesc } from "@hyperledger/cactus-common"; + +export interface IBuildImageConnectorCordaServerResponse { + readonly imageName: Readonly; + readonly imageVersion: Readonly; + /** + * The concatenation of `imageName` a colon character and `imageVersion`. + */ + readonly imageTag: Readonly; +} + +export interface IBuildImageConnectorCordaServerRequest { + readonly logLevel?: Readonly; +} + +export async function buildImageConnectorCordaServer( + req: IBuildImageConnectorCordaServerRequest, +): Promise { + if (!req) { + throw new Error("Expected arg req to be truthy."); + } + const logLevel: LogLevelDesc = req.logLevel || "WARN"; + const log = LoggerProvider.getOrCreate({ + level: logLevel, + label: "build-image-connector-corda-server.ts", + }); + const projectRoot = path.join(__dirname, "../../../../../../../"); + + const buildDirRel = + "./packages/cactus-plugin-ledger-connector-corda/src/main-server/"; + + const buildDirAbs = path.join(projectRoot, buildDirRel); + + log.info("Invoking container build with build dir: %s", buildDirAbs); + + const imageName = "cccs"; + const imageVersion = "latest"; + const imageTag = `${imageName}:${imageVersion}`; + + await buildContainerImage({ + buildDir: buildDirAbs, + imageFile: "Dockerfile", + imageTag, + logLevel: logLevel, + }); + + log.info("Building Corda v4 JVM Connector finished OK"); + + return { imageName, imageVersion, imageTag }; +} diff --git a/packages/cactus-test-tooling/src/main/typescript/corda/build-image-corda-all-in-one-v4-12.ts b/packages/cactus-test-tooling/src/main/typescript/corda/build-image-corda-all-in-one-v4-12.ts new file mode 100644 index 0000000000..55226c427b --- /dev/null +++ b/packages/cactus-test-tooling/src/main/typescript/corda/build-image-corda-all-in-one-v4-12.ts @@ -0,0 +1,49 @@ +import path from "node:path"; +import { buildContainerImage } from "../public-api"; +import { LoggerProvider, LogLevelDesc } from "@hyperledger/cactus-common"; + +export interface IBuildImageCordaAllInOneV412Response { + readonly imageName: Readonly; + readonly imageVersion: Readonly; + /** + * The concatenation of `imageName` a colon character and `imageVersion`. + */ + readonly imageTag: Readonly; +} + +export interface IBuildImageCordaAllInOneV412Request { + readonly logLevel?: Readonly; +} + +export async function buildImageCordaAllInOneV412( + req: IBuildImageCordaAllInOneV412Request, +): Promise { + if (!req) { + throw new Error("Expected arg req to be truthy."); + } + const logLevel: LogLevelDesc = req.logLevel || "WARN"; + const log = LoggerProvider.getOrCreate({ + level: logLevel, + label: "build-image-connector-corda-server.ts", + }); + const projectRoot = path.join(__dirname, "../../../../../../../"); + + const buildDirRel = "./tools/docker/corda-all-in-one/corda-v4_12/"; + + const buildDirAbs = path.join(projectRoot, buildDirRel); + + log.info("Invoking container build with build dir: %s", buildDirAbs); + + const imageName = "caio412"; + const imageVersion = "latest"; + const imageTag = `${imageName}:${imageVersion}`; + + await buildContainerImage({ + buildDir: buildDirAbs, + imageFile: "Dockerfile", + imageTag, + logLevel: logLevel, + }); + + return { imageName, imageVersion, imageTag }; +} diff --git a/packages/cactus-test-tooling/src/main/typescript/public-api.ts b/packages/cactus-test-tooling/src/main/typescript/public-api.ts index 1145088bbb..6b9dee61f3 100755 --- a/packages/cactus-test-tooling/src/main/typescript/public-api.ts +++ b/packages/cactus-test-tooling/src/main/typescript/public-api.ts @@ -194,3 +194,20 @@ export { FABRIC_25_LTS_FABRIC_SAMPLES__ORDERER_TLS_ROOTCERT_FILE_ORG_2, IFabricOrgEnvInfo, } from "./fabric/fabric-samples-env-constants"; + +export { + IBuildContainerImageRequest, + buildContainerImage, +} from "./common/build-container-image"; + +export { + IBuildImageConnectorCordaServerRequest, + IBuildImageConnectorCordaServerResponse, + buildImageConnectorCordaServer, +} from "./corda/build-image-connector-corda-server"; + +export { + IBuildImageCordaAllInOneV412Request, + IBuildImageCordaAllInOneV412Response, + buildImageCordaAllInOneV412, +} from "./corda/build-image-corda-all-in-one-v4-12"; diff --git a/tools/docker/corda-all-in-one/README.md b/tools/docker/corda-all-in-one/README.md new file mode 100644 index 0000000000..7ba5dd1331 --- /dev/null +++ b/tools/docker/corda-all-in-one/README.md @@ -0,0 +1,128 @@ +# cactus-corda-all-in-one + +> This docker image is for `testing` and `development` only. +> Do NOT use in production! + +## Usage + +### Build and Run Image Locally + +```sh +DOCKER_BUILDKIT=1 docker build ./tools/docker/corda-all-in-one/ -t caio +docker run --rm --privileged caio +``` + +# cactus-corda-4-8-all-in-one + +> This docker image is for `testing` and `development` only. +> Do NOT use in production! + +## Usage + +### Build and Run Image Locally + +```sh +DOCKER_BUILDKIT=1 docker build ./tools/docker/corda-all-in-one/ -f ./tools/docker/corda-all-in-one/corda-v4_8/Dockerfile -t caio48 +docker run --rm --privileged caio48 +``` + +# cactus-corda-4-8-all-in-one-flowdb + +> This docker image is for `testing` and `development` only. +> Do NOT use in production! + +## Customization + +`build.gradle` file from this sample has defined a single node called PartyA. It was modified to deploy the same nodes as in the obligation sample to make it work with our CordaTestLedger: +- Notary +- ParticipantA +- ParticipantB +- ParticipantC + +## Usage + +### Build and Run Image Locally + +```sh +DOCKER_BUILDKIT=1 docker build ./tools/docker/corda-all-in-one/corda-v4_8-flowdb/ -t caio48-flowdb +docker run --rm --privileged caio48-flowdb +``` + +# cactus-corda-4-12-all-in-one + +> This docker image is for `testing` and `development` only. +> Do NOT use in production! + +## Usage + +### Build and Run Image Locally + +```sh +DOCKER_BUILDKIT=1 docker build ./tools/docker/corda-all-in-one/corda-v4_12/ -f ./tools/docker/corda-all-in-one/corda-v4_12/Dockerfile -t caio412 +docker run --rm --privileged caio412 +``` + + +# cactus-corda-5-all-in-one-solar + +> This docker image is for `testing` and `development` only. +> Do NOT use in production! + +## Usage + +### Build and Run Image Locally + +```sh +DOCKER_BUILDKIT=1 docker build ./tools/docker/corda-all-in-one/corda-v5/ -f ./tools/docker/corda-all-in-one/corda-v5/Dockerfile -t caio5 +docker run --privileged caio5 +``` + +### Install Application and Testing + +Open container CLI: + +```sh +docker exec -it /bin/sh +``` + +In container CLI, run this command to install the sample application on the network: + +```sh +/root/bin/corda-cli/bin/corda-cli package install -n solar-system /corda5-solarsystem-contracts-demo/solar-system.cpb +``` + +To check that everything works correctly, start a flow with the following curl command: + +```sh +curl -u earthling:password --insecure -X POST "https://localhost:12112/api/v1/flowstarter/startflow" -H "accept: application/json" -H "Content-Type: application/json" -d "{\"rpcStartFlowRequest\":{\"clientId\":\"launchpad-1\",\"flowName\":\"net.corda.solarsystem.flows.LaunchProbeFlow\",\"parameters\":{\"parametersInJson\":\"{\\\"message\\\": \\\"Hello Mars\\\", \\\"target\\\": \\\"C=GB, L=FOURTH, O=MARS, OU=PLANET\\\", \\\"planetaryOnly\\\":\\\"true\\\"}\"}}}" +``` +If the command is successful, it returns a 200 response, including the flowId (a uuid) and the clientId, like the following: +```json +{ + "flowId":{ + "uuid":"9c8d5b46-be92-4be8-9569-76cb3e41cde9" + }, + "clientId":"launchpad-1" +} +``` +Using the field value ```flowId``` from the answer above, you can check the flow status: +```sh +curl -u earthling:password --insecure -X GET "https://localhost:12112/api/v1/flowstarter/flowoutcome/" -H "accept: application/json" +``` +It returns a 200 response, which includes these items in the response body: + +- Flow status +- Signatures of both parties +- ID of the state + +Sample of response: +```json +{ + "status":"COMPLETED", + "resultJson":"{ \n \"txId\" : \"SHA-256:882FCCFA0CE08FEC4F90A8BBC8B8FBC1DE3CBDA8DBED4D6562E0922234B87E4F\",\n \"outputStates\" : [\"{\\\"message\\\":\\\"Hello Mars\\\",\\\"planetaryOnly\\\":true,\\\"launcher\\\":\\\"OU\\u003dPLANET, O\\u003dEARTH, L\\u003dTHIRD, C\\u003dIE\\\",\\\"target\\\":\\\"OU\\u003dPLANET, O\\u003dMARS, L\\u003dFOURTH, C\\u003dGB\\\",\\\"linearId\\\":\\\"31800d11-b518-4fb7-a18e-18cc1c64a4ff\\\"}\"], \n \"signatures\": [\"ijMOjsLWxihWLnfxw7DoIv1gpHFaSAs+VfGSS5qaI1Z4cZu96riAo1uEFSbeskZTt2eGNwv05IP3dS08AjLRCA==\", \"2yRNwdrqKU6/lrUfgmaiXxdPYHjXxfXIYlEL8RHU2aNGQPUVXmc+jbsaNxbcig7Fs0kck28JreuUwn1lJOZODw==\"]\n}", + "exceptionDigest":null +} +``` + + +