Skip to content

Commit

Permalink
feat(fabric-all-in-one): runs-a-Fabric-Network-in-one-docker-container
Browse files Browse the repository at this point in the history
Fix #132

Signed-off-by: Roy,Sownak <sownak.roy@accenture.com>
  • Loading branch information
sownak authored and petermetz committed Aug 18, 2020
1 parent a51684c commit 703bc61
Show file tree
Hide file tree
Showing 15 changed files with 770 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,266 @@
import Docker, { Container, ContainerInfo } from "dockerode";
import axios from "axios";
import Joi from "joi";
import { EventEmitter } from "events";
import { ITestLedger } from "../i-test-ledger";

/*
* Contains options for Fabric container
*/
export interface IFabricTestLedgerV1ConstructorOptions {
imageVersion?: string;
imageName?: string;
opsApiHttpPort?: number;
}

/*
* Provides default options for Fabric container
*/
const DEFAULT_OPTS = Object.freeze({
imageVersion: "latest",
imageName: "hyperledger/cactus-fabric-all-in-one",
opsApiHttpPort: 9443,
});
export const FABRIC_TEST_LEDGER_DEFAULT_OPTIONS = DEFAULT_OPTS;

/*
* Provides validations for the Fabric container's options
*/
const OPTS_JOI_SCHEMA: Joi.Schema = Joi.object().keys({
imageVersion: Joi.string().min(5).required(),
imageName: Joi.string().min(1).required(),
opsApiHttpPort: Joi.number().integer().min(1024).max(65535).required(),
});

export const FABRIC_TEST_LEDGER_OPTIONS_JOI_SCHEMA = OPTS_JOI_SCHEMA;

export class FabricTestLedgerV1 implements ITestLedger {
public readonly imageVersion: string;
public readonly imageName: string;
public readonly opsApiHttpPort: number;

private container: Container | undefined;

constructor(
public readonly options: IFabricTestLedgerV1ConstructorOptions = {}
) {
if (!options) {
throw new TypeError(`FabricTestLedgerV1#ctor options was falsy.`);
}
this.imageVersion = options.imageVersion || DEFAULT_OPTS.imageVersion;
this.imageName = options.imageName || DEFAULT_OPTS.imageName;
this.opsApiHttpPort = options.opsApiHttpPort || DEFAULT_OPTS.opsApiHttpPort;

this.validateConstructorOptions();
}

public getContainer(): Container {
const fnTag = "FabricTestLedgerV1#getContainer()";
if (!this.container) {
throw new Error(`${fnTag} container not yet started by this instance.`);
} else {
return this.container;
}
}

public getContainerImageName(): string {
return `${this.imageName}:${this.imageVersion}`;
}

public async getOpsApiHttpHost(): Promise<string> {
const ipAddress: string = "127.0.0.1";
const hostPort: number = await this.getOpsApiPublicPort();
return `http://${ipAddress}:${hostPort}/version`;
}

public async start(): Promise<Container> {
const containerNameAndTag = this.getContainerImageName();

if (this.container) {
await this.container.stop();
await this.container.remove();
}
const docker = new Docker();

await this.pullContainerImage(containerNameAndTag);

return new Promise<Container>((resolve, reject) => {
const eventEmitter: EventEmitter = docker.run(
containerNameAndTag,
[],
[],
{
ExposedPorts: {
[`${this.opsApiHttpPort}/tcp`]: {}, // Fabric Peer GRPC - HTTP
"7050/tcp": {}, // Orderer GRPC - HTTP
"7051/tcp": {}, // Peer additional - HTTP
"7052/tcp": {}, // Peer Chaincode - HTTP
"7053/tcp": {}, // Peer additional - HTTP
"7054/tcp": {}, // Fabric CA - HTTP
"9001/tcp": {}, // supervisord - HTTP
},
// This is a workaround needed for macOS which has issues with routing
// to docker container's IP addresses directly...
// https://stackoverflow.com/a/39217691
PublishAllPorts: true,
},
{},
(err: any) => {
if (err) {
reject(err);
}
}
);

eventEmitter.once("start", async (container: Container) => {
this.container = container;
try {
await this.waitForHealthCheck();
resolve(container);
} catch (ex) {
reject(ex);
}
});
});
}

public async waitForHealthCheck(timeoutMs: number = 120000): Promise<void> {
const fnTag = "FabricTestLedgerV1#waitForHealthCheck()";
const httpUrl = await this.getOpsApiHttpHost();
const startedAt = Date.now();
let reachable: boolean = false;
do {
try {
const res = await axios.get(httpUrl);
reachable = res.status > 199 && res.status < 300;
} catch (ex) {
reachable = false;
if (Date.now() >= startedAt + timeoutMs) {
throw new Error(`${fnTag} timed out (${timeoutMs}ms) -> ${ex.stack}`);
}
}
await new Promise((resolve2) => setTimeout(resolve2, 100));
} while (!reachable);
}

public stop(): Promise<any> {
const fnTag = "FabricTestLedgerV1#stop()";
return new Promise((resolve, reject) => {
if (this.container) {
this.container.stop({}, (err: any, result: any) => {
if (err) {
reject(err);
} else {
resolve(result);
}
});
} else {
return reject(new Error(`${fnTag} Container was not running.`));
}
});
}

public async destroy(): Promise<any> {
const fnTag = "FabricTestLedgerV1#destroy()";
if (this.container) {
return this.container.remove();
} else {
throw new Error(`${fnTag} Containernot found, nothing to destroy.`);
}
}

protected async getContainerInfo(): Promise<ContainerInfo> {
const fnTag = "FabricTestLedgerV1#getContainerInfo()";
const docker = new Docker();
const image = this.getContainerImageName();
const containerInfos = await docker.listContainers({});

const aContainerInfo = containerInfos.find((ci) => ci.Image === image);

if (aContainerInfo) {
return aContainerInfo;
} else {
throw new Error(`${fnTag} no image "${image}"`);
}
}

public async getOpsApiPublicPort(): Promise<number> {
const fnTag = "FabricTestLedgerV1#getOpsApiPublicPort()";
const aContainerInfo = await this.getContainerInfo();
const { opsApiHttpPort: thePort } = this;
const { Ports: ports } = aContainerInfo;

if (ports.length < 1) {
throw new Error(`${fnTag} no ports exposed or mapped at all`);
}
const mapping = ports.find((x) => x.PrivatePort === thePort);
if (mapping) {
if (!mapping.PublicPort) {
throw new Error(`${fnTag} port ${thePort} mapped but not public`);
} else if (mapping.IP !== "0.0.0.0") {
throw new Error(`${fnTag} port ${thePort} mapped to localhost`);
} else {
return mapping.PublicPort;
}
} else {
throw new Error(`${fnTag} no mapping found for ${thePort}`);
}
}

public async getContainerIpAddress(): Promise<string> {
const fnTag = "FabricTestLedgerV1#getContainerIpAddress()";
const aContainerInfo = await this.getContainerInfo();

if (aContainerInfo) {
const { NetworkSettings } = aContainerInfo;
const networkNames: string[] = Object.keys(NetworkSettings.Networks);
if (networkNames.length < 1) {
throw new Error(`${fnTag} container not connected to any networks`);
} else {
// return IP address of container on the first network that we found it connected to. Make this configurable?
return NetworkSettings.Networks[networkNames[0]].IPAddress;
}
} else {
throw new Error(`${fnTag} cannot find docker image ${this.imageName}`);
}
}

private pullContainerImage(containerNameAndTag: string): Promise<any[]> {
return new Promise((resolve, reject) => {
const docker = new Docker();
docker.pull(containerNameAndTag, (pullError: any, stream: any) => {
if (pullError) {
reject(pullError);
} else {
docker.modem.followProgress(
stream,
(progressError: any, output: any[]) => {
if (progressError) {
reject(progressError);
} else {
resolve(output);
}
},
(event: any) => null // ignore the spammy docker download log, we get it in the output variable anyway
);
}
});
});
}

private validateConstructorOptions(): void {
const fnTag = "FabricTestLedgerV1#validateConstructorOptions()";
const result = Joi.validate<IFabricTestLedgerV1ConstructorOptions>(
{
imageVersion: this.imageVersion,
imageName: this.imageName,
opsApiHttpPort: this.opsApiHttpPort,
},
OPTS_JOI_SCHEMA
);

if (result.error) {
throw new Error(`${fnTag} ${result.error.annotate()}`);
}
}
}
10 changes: 10 additions & 0 deletions packages/cactus-test-tooling/src/main/typescript/public-api.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
export { ITestLedger } from "./i-test-ledger";
export { IKeyPair, isIKeyPair } from "./i-key-pair";

export {
BesuTestLedger,
IBesuTestLedgerConstructorOptions,
BESU_TEST_LEDGER_DEFAULT_OPTIONS,
BESU_TEST_LEDGER_OPTIONS_JOI_SCHEMA,
} from "./besu/besu-test-ledger";

export {
QuorumTestLedger,
IQuorumTestLedgerConstructorOptions,
Expand All @@ -14,9 +16,17 @@ export {
} from "./quorum/quorum-test-ledger";
export * from "./quorum/i-quorum-genesis-options";
export { Containers } from "./common/containers";

export {
HttpEchoContainer,
IHttpEchoContainerConstructorOptions,
HTTP_ECHO_CONTAINER_CTOR_DEFAULTS,
HTTP_ECHO_CONTAINER_OPTS_SCHEMA,
} from "./http-echo/http-echo-container";

export {
FabricTestLedgerV1,
IFabricTestLedgerV1ConstructorOptions,
FABRIC_TEST_LEDGER_DEFAULT_OPTIONS,
FABRIC_TEST_LEDGER_OPTIONS_JOI_SCHEMA,
} from "./fabric/fabric-test-ledger-v1";
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// tslint:disable-next-line: no-var-requires
const tap = require("tap");
import isPortReachable from "is-port-reachable";
import { Container } from "dockerode";
import { FabricTestLedgerV1 } from "../../../../../main/typescript/public-api";

tap.test("constructor throws if invalid input is provided", (assert: any) => {
assert.ok(FabricTestLedgerV1);
assert.throws(() => new FabricTestLedgerV1({ imageVersion: "nope" }));
assert.end();
});

tap.test(
"constructor does not throw if valid input is provided",
(assert: any) => {
assert.ok(FabricTestLedgerV1);
assert.doesNotThrow(() => new FabricTestLedgerV1());
assert.end();
}
);

tap.test("starts/stops/destroys a docker container", async (assert: any) => {
const fabricTestLedger = new FabricTestLedgerV1();
assert.tearDown(() => fabricTestLedger.stop());
assert.tearDown(() => fabricTestLedger.destroy());

const container: Container = await fabricTestLedger.start();
assert.ok(container);
const ipAddress: string = await fabricTestLedger.getContainerIpAddress();
assert.ok(ipAddress);
assert.ok(ipAddress.length);

const hostPort: number = await fabricTestLedger.getOpsApiPublicPort();
assert.ok(hostPort, "getOpsApiPublicPort() returns truthy OK");
assert.ok(isFinite(hostPort), "getOpsApiPublicPort() returns finite OK");

const isReachable = await isPortReachable(hostPort, { host: "localhost" });
assert.ok(isReachable, `HostPort ${hostPort} is reachable via localhost`);

assert.end();
});
Loading

0 comments on commit 703bc61

Please sign in to comment.