Skip to content

Commit

Permalink
test(test-tooling): add container image builder utilities
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
petermetz committed Jul 17, 2024
1 parent c8b33c8 commit 38903d5
Show file tree
Hide file tree
Showing 5 changed files with 310 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -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<string>;
readonly imageFile: Readonly<string>;
readonly imageTag: Readonly<string>;
readonly dockerEngine?: Readonly<Docker>;
readonly dockerodeImageBuildOptions?: Partial<ImageBuildOptions>;
readonly dockerodeImageBuildContext?: Partial<ImageBuildContext>;
}

export async function buildContainerImage(
req: Readonly<IBuildContainerImageRequest>,
): Promise<void> {
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),
);
});
}
Original file line number Diff line number Diff line change
@@ -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<string>;
readonly imageVersion: Readonly<string>;
/**
* The concatenation of `imageName` a colon character and `imageVersion`.
*/
readonly imageTag: Readonly<string>;
}

export interface IBuildImageConnectorCordaServerRequest {
readonly logLevel?: Readonly<LogLevelDesc>;
}

export async function buildImageConnectorCordaServer(
req: IBuildImageConnectorCordaServerRequest,
): Promise<IBuildImageConnectorCordaServerResponse> {
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 };
}
Original file line number Diff line number Diff line change
@@ -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<string>;
readonly imageVersion: Readonly<string>;
/**
* The concatenation of `imageName` a colon character and `imageVersion`.
*/
readonly imageTag: Readonly<string>;
}

export interface IBuildImageCordaAllInOneV412Request {
readonly logLevel?: Readonly<LogLevelDesc>;
}

export async function buildImageCordaAllInOneV412(
req: IBuildImageCordaAllInOneV412Request,
): Promise<IBuildImageCordaAllInOneV412Response> {
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 };
}
17 changes: 17 additions & 0 deletions packages/cactus-test-tooling/src/main/typescript/public-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
128 changes: 128 additions & 0 deletions tools/docker/corda-all-in-one/README.md
Original file line number Diff line number Diff line change
@@ -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 <id_docker> /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/<flowId>" -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
}
```



0 comments on commit 38903d5

Please sign in to comment.