diff --git a/.cspell.json b/.cspell.json index c8c7e27789..43cbe52804 100644 --- a/.cspell.json +++ b/.cspell.json @@ -21,6 +21,7 @@ "cids", "Corda", "Cordapp", + "couchdb", "dclm", "DHTAPI", "DockerOde", diff --git a/packages/cactus-test-tooling/package.json b/packages/cactus-test-tooling/package.json index 8a6f1c2af4..dd96f2e631 100644 --- a/packages/cactus-test-tooling/package.json +++ b/packages/cactus-test-tooling/package.json @@ -51,6 +51,16 @@ "email": "your.name@example.com", "url": "https://example.com" }, + { + "name": "Catarina Pedreira", + "email": "catarina.pedreira@tecnico.ulisboa.pt", + "url": "https://github.com/CatarinaPedreira" + }, + { + "name": "Rafael Belchior", + "email": "rafael.belchior@tecnico.ulisboa.pt", + "url": "https://rafaelapb.github.io/" + }, { "name": "Peter Somogyvari", "email": "peter.somogyvari@accenture.com", 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 871a57ca24..94df89df75 100755 --- a/packages/cactus-test-tooling/src/main/typescript/public-api.ts +++ b/packages/cactus-test-tooling/src/main/typescript/public-api.ts @@ -131,6 +131,11 @@ export { RustcContainer, } from "./rustc-container/rustc-container"; +export { + ISubstrateTestLedgerOptions, + SubstrateTestLedger, +} from "./substrate-test-ledger/substrate-test-ledger"; + export { RustcBuildCmd } from "./rustc-container/rustc-build-cmd"; export { Streams } from "./common/streams"; diff --git a/packages/cactus-test-tooling/src/main/typescript/substrate-test-ledger/substrate-test-ledger.ts b/packages/cactus-test-tooling/src/main/typescript/substrate-test-ledger/substrate-test-ledger.ts new file mode 100644 index 0000000000..f50fe9d6a6 --- /dev/null +++ b/packages/cactus-test-tooling/src/main/typescript/substrate-test-ledger/substrate-test-ledger.ts @@ -0,0 +1,202 @@ +import type { EventEmitter } from "events"; +import { Optional } from "typescript-optional"; +import { RuntimeError } from "run-time-error"; +import type { Container, ContainerInfo } from "dockerode"; +import Docker from "dockerode"; +import { Logger, Checks, Bools } from "@hyperledger/cactus-common"; +import type { LogLevelDesc } from "@hyperledger/cactus-common"; +import { LoggerProvider } from "@hyperledger/cactus-common"; +import { Containers } from "../common/containers"; + +export interface ISubstrateTestLedgerOptions { + readonly publishAllPorts: boolean; + readonly logLevel?: LogLevelDesc; + readonly imageName?: string; + readonly imageTag?: string; + readonly emitContainerLogs?: boolean; + readonly envVars?: Map; +} + +export class SubstrateTestLedger { + public static readonly CLASS_NAME = "SubstrateTestLedger"; + + public readonly logLevel: LogLevelDesc; + public readonly imageName: string; + public readonly imageTag: string; + public readonly imageFqn: string; + public readonly log: Logger; + public readonly emitContainerLogs: boolean; + public readonly publishAllPorts: boolean; + public readonly envVars: Map; + + private _containerId: Optional; + + public get containerId(): Optional { + return this._containerId; + } + + public get container(): Optional { + const docker = new Docker(); + return this.containerId.isPresent() + ? Optional.ofNonNull(docker.getContainer(this.containerId.get())) + : Optional.empty(); + } + + public get className(): string { + return SubstrateTestLedger.CLASS_NAME; + } + + constructor(public readonly opts: ISubstrateTestLedgerOptions) { + const fnTag = `${this.className}#constructor()`; + Checks.truthy(opts, `${fnTag} arg options`); + + this.publishAllPorts = opts.publishAllPorts; + this._containerId = Optional.empty(); + this.imageName = + opts.imageName || "ghcr.io/hyperledger/cactus-substrate-all-in-one"; + this.imageTag = opts.imageTag || "2021-09-24---feat-1274"; + this.imageFqn = `${this.imageName}:${this.imageTag}`; + this.envVars = opts.envVars || new Map(); + this.emitContainerLogs = Bools.isBooleanStrict(opts.emitContainerLogs) + ? (opts.emitContainerLogs as boolean) + : true; + + this.logLevel = opts.logLevel || "INFO"; + + const level = this.logLevel; + const label = this.className; + this.log = LoggerProvider.getOrCreate({ level, label }); + + this.log.debug(`Created instance of ${this.className} OK`); + } + public getContainerImageName(): string { + return `${this.imageName}:${this.imageTag}`; + } + public async start(omitPull = false): Promise { + const docker = new Docker(); + if (this.containerId.isPresent()) { + this.log.debug(`Container ID provided. Will not start new one.`); + const container = docker.getContainer(this.containerId.get()); + return container; + } + if (!omitPull) { + this.log.debug(`Pulling image ${this.imageFqn}...`); + await Containers.pullImage(this.imageFqn); + this.log.debug(`Pulled image ${this.imageFqn} OK`); + } + + const dockerEnvVars: string[] = new Array(...this.envVars).map( + (pairs) => `${pairs[0]}=${pairs[1]}`, + ); + + // TODO: dynamically expose ports for custom port mapping + const createOptions = { + Env: dockerEnvVars, + Healthcheck: { + Test: [ + "CMD-SHELL", + `rustup --version && rustc --version && cargo --version`, + ], + Interval: 1000000000, // 1 second + Timeout: 3000000000, // 3 seconds + Retries: 10, + StartPeriod: 1000000000, // 1 second + }, + ExposedPorts: { + "9944/tcp": {}, // OpenSSH Server - TCP + }, + HostConfig: { + AutoRemove: true, + PublishAllPorts: this.publishAllPorts, + Privileged: false, + PortBindings: { + "9944/tcp": [{ HostPort: "9944" }], + }, + }, + }; + + this.log.debug(`Starting ${this.imageFqn} with options: `, createOptions); + + return new Promise((resolve, reject) => { + const eventEmitter: EventEmitter = docker.run( + this.imageFqn, + [], + [], + createOptions, + {}, + (err: Error) => { + if (err) { + const errorMessage = `Failed to start container ${this.imageFqn}`; + const exception = new RuntimeError(errorMessage, err); + this.log.error(exception); + reject(exception); + } + }, + ); + + eventEmitter.once("start", async (container: Container) => { + const { id } = container; + this.log.debug(`Started ${this.imageFqn} successfully. ID=${id}`); + this._containerId = Optional.ofNonNull(id); + + if (this.emitContainerLogs) { + const logOptions = { follow: true, stderr: true, stdout: true }; + const logStream = await container.logs(logOptions); + logStream.on("data", (data: Buffer) => { + const fnTag = `[${this.imageFqn}]`; + this.log.debug(`${fnTag} %o`, data.toString("utf-8")); + }); + } + this.log.debug(`Registered container log stream callbacks OK`); + + try { + this.log.debug(`Starting to wait for healthcheck... `); + await Containers.waitForHealthCheck(this.containerId.get()); + this.log.debug(`Healthcheck passed OK`); + resolve(container); + } catch (ex) { + this.log.error(ex); + reject(ex); + } + }); + }); + } + + public async stop(): Promise { + return Containers.stop(this.container.get()); + } + + public async destroy(): Promise { + return this.container.get().remove(); + } + + public async getContainerIpAddress(): Promise { + const containerInfo = await this.getContainerInfo(); + return Containers.getContainerInternalIp(containerInfo); + } + + protected async getContainerInfo(): Promise { + const fnTag = "FabricTestLedgerV1#getContainerInfo()"; + const docker = new Docker(); + const image = this.getContainerImageName(); + const containerInfos = await docker.listContainers({}); + + let aContainerInfo; + if (this.containerId !== undefined) { + aContainerInfo = containerInfos.find( + (ci) => ci.Id == this.containerId.toString(), + ); + } + + if (aContainerInfo) { + return aContainerInfo; + } else { + throw new Error(`${fnTag} no image "${image}"`); + } + } + + // ./scripts/docker_run.sh ./target/release/node-template purge-chain --dev + protected async purgeDevChain(): Promise { + throw new Error("TODO"); + } +} diff --git a/packages/cactus-test-tooling/src/test/typescript/integration/substrate/substrate-test-ledger-constructor.test.ts b/packages/cactus-test-tooling/src/test/typescript/integration/substrate/substrate-test-ledger-constructor.test.ts new file mode 100644 index 0000000000..af6229c4c5 --- /dev/null +++ b/packages/cactus-test-tooling/src/test/typescript/integration/substrate/substrate-test-ledger-constructor.test.ts @@ -0,0 +1,40 @@ +import test, { Test } from "tape-promise/tape"; +import { LogLevelDesc } from "@hyperledger/cactus-common"; +import { SubstrateTestLedger } from "../../../../main/typescript/substrate-test-ledger/substrate-test-ledger"; +import { pruneDockerAllIfGithubAction } from "../../../../main/typescript/github-actions/prune-docker-all-if-github-action"; + +const testCase = "Instantiate plugin"; +const logLevel: LogLevelDesc = "TRACE"; + +test("BEFORE " + testCase, async (t: Test) => { + const pruning = pruneDockerAllIfGithubAction({ logLevel }); + await t.doesNotReject(pruning, "Pruning didn't throw OK"); + t.end(); +}); + +test(testCase, async (t: Test) => { + const options = { + publishAllPorts: true, + logLevel: logLevel, + emitContainerLogs: true, + envVars: new Map([ + ["WORKING_DIR", "/var/www/node-template"], + ["CONTAINER_NAME", "contracts-node-template-cactus"], + ["PORT", "9944"], + ["DOCKER_PORT", "9944"], + ["CARGO_HOME", "/var/www/node-template/.cargo"], + ]), + }; + + const ledger = new SubstrateTestLedger(options); + const tearDown = async () => { + await ledger.stop(); + await pruneDockerAllIfGithubAction({ logLevel }); + }; + + test.onFinish(tearDown); + await ledger.start(); + t.ok(ledger); + + t.end(); +}); diff --git a/tools/docker/substrate-all-in-one/Dockerfile b/tools/docker/substrate-all-in-one/Dockerfile new file mode 100644 index 0000000000..e5927c7ac2 --- /dev/null +++ b/tools/docker/substrate-all-in-one/Dockerfile @@ -0,0 +1,39 @@ +FROM paritytech/ci-linux:production +LABEL AUTHORS="Rafael Belchior, Catarina Pedreira" +LABEL VERSION="2021-09-10" +LABEL org.opencontainers.image.source=https://github.com/hyperledger/cactus + +WORKDIR / +ARG WORKING_DIR=/var/www/node-template +ARG CONTAINER_NAME=contracts-node-template-cactus +ARG PORT=9944 +ARG DOCKER_PORT=9944 +ARG CARGO_HOME=/var/www/node-template/.cargo + +ENV CARGO_HOME=${CARGO_HOME} +ENV CACTUS_CFG_PATH=/etc/hyperledger/cactus +VOLUME .:/var/www/node-template + +RUN apt update + +# Get ubuntu and rust packages +RUN apt install -y build-essential pkg-config git clang curl libssl-dev llvm libudev-dev + +ENV CACTUS_CFG_PATH=/etc/hyperledger/cactus +RUN mkdir -p $CACTUS_CFG_PATH + +RUN set -e + +RUN echo "*** Instaling Rust environment ***" +RUN curl https://sh.rustup.rs -y -sSf | sh +RUN echo 'source $HOME/.cargo/env' >> $HOME/.bashrc +RUN rustup default nightly + +RUN echo "*** Initializing WASM build environment" +RUN rustup target add wasm32-unknown-unknown --toolchain nightly + +RUN echo "*** Installing Substrate node environment ***" +RUN cargo install contracts-node --git https://github.com/paritytech/substrate-contracts-node.git --force --locked + +RUN echo "*** Start Substrate node template ***" +CMD [ "/var/www/node-template/.cargo/bin/substrate-contracts-node", "--dev"] \ No newline at end of file diff --git a/tools/docker/substrate-all-in-one/README.md b/tools/docker/substrate-all-in-one/README.md new file mode 100644 index 0000000000..3d997d8fa3 --- /dev/null +++ b/tools/docker/substrate-all-in-one/README.md @@ -0,0 +1,20 @@ +# @hyperledger/cactus-substrate-all-in-one + +A container image that can holds the default Substrate test ledger (and the corresponding front-end). +This image can be used for development of Substrate-based chains (including but not limited to pallets, smart contracts) and connectors. + +## Table of Contents + +- [Usage](#usage) +- [Build](#build) + +## Usage +```sh +docker run -t -p 9944:9944 --name substrate-contracts-node saio:latest +``` + +## Build + +```sh +DOCKER_BUILDKIT=1 docker build -f ./tools/docker/substrate-all-in-one/Dockerfile . --tag saio +``` diff --git a/tools/docker/substrate-all-in-one/hooks/post_push b/tools/docker/substrate-all-in-one/hooks/post_push new file mode 100755 index 0000000000..8f41b30ce4 --- /dev/null +++ b/tools/docker/substrate-all-in-one/hooks/post_push @@ -0,0 +1,18 @@ +#!/bin/bash + + +SHORTHASH="$(git rev-parse --short HEAD)" +TODAYS_DATE="$(date +%F)" + +# +# We tag every image with today's date and also the git short hash +# Today's date helps humans quickly intuit which version is older/newer +# And the short hash helps identify the exact git revision that the image was +# built from in case you are chasing some exotic bug that requires this sort of +# rabbithole diving where you are down to comparing the images at this level. +# +DOCKER_TAG="$TODAYS_DATE-$SHORTHASH" + + +docker tag $IMAGE_NAME $DOCKER_REPO:$DOCKER_TAG +docker push $DOCKER_REPO:$DOCKER_TAG diff --git a/yarn.lock b/yarn.lock index bb05c8a9cb..4642e7a68f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -24505,4 +24505,4 @@ zone.js@0.11.4: zone.js@~0.10.3: version "0.10.3" resolved "https://registry.yarnpkg.com/zone.js/-/zone.js-0.10.3.tgz#3e5e4da03c607c9dcd92e37dd35687a14a140c16" - integrity sha512-LXVLVEq0NNOqK/fLJo3d0kfzd4sxwn2/h67/02pjCjfKDxgx1i9QqpvtHD8CrBnSSwMw5+dy11O7FRX5mkO7Cg== + integrity sha512-LXVLVEq0NNOqK/fLJo3d0kfzd4sxwn2/h67/02pjCjfKDxgx1i9QqpvtHD8CrBnSSwMw5+dy11O7FRX5mkO7Cg== \ No newline at end of file