Skip to content

Commit e62b1b0

Browse files
committed
feat(test-tooling): go-ipfs test container
New utility class that can manage the life-cycle of a go-ipfs container. It uses the official go-ipfs container image under the hood not a custom one like the AIO images. The purpose of this is to help authoring test cases in the future related to IPFS of which a good example will be the IPFS object-store plugin's implementation. Signed-off-by: Peter Somogyvari <[email protected]>
1 parent f97f264 commit e62b1b0

File tree

2 files changed

+197
-0
lines changed

2 files changed

+197
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
import type { EventEmitter } from "events";
2+
import { Optional } from "typescript-optional";
3+
import { RuntimeError } from "run-time-error";
4+
import type { Container } from "dockerode";
5+
import Docker from "dockerode";
6+
import { Logger, Checks, Bools } from "@hyperledger/cactus-common";
7+
import type { LogLevelDesc } from "@hyperledger/cactus-common";
8+
import { LoggerProvider } from "@hyperledger/cactus-common";
9+
import { Containers } from "../common/containers";
10+
11+
export interface IGoIpfsTestContainerOptions {
12+
readonly logLevel?: LogLevelDesc;
13+
readonly imageName?: string;
14+
readonly imageTag?: string;
15+
readonly emitContainerLogs?: boolean;
16+
readonly envVars?: Map<string, string>;
17+
readonly containerId?: string;
18+
readonly apiPort?: number;
19+
readonly swarmPort?: number;
20+
readonly webGatewayPort?: number;
21+
}
22+
23+
export class GoIpfsTestContainer {
24+
public static readonly CLASS_NAME = "GoIpfsTestContainer";
25+
26+
public readonly logLevel: LogLevelDesc;
27+
public readonly imageName: string;
28+
public readonly imageTag: string;
29+
public readonly imageFqn: string;
30+
public readonly log: Logger;
31+
public readonly emitContainerLogs: boolean;
32+
public readonly envVars: Map<string, string>;
33+
public readonly apiPort: number;
34+
public readonly swarmPort: number;
35+
public readonly webGatewayPort: number;
36+
37+
private _containerId: Optional<string>;
38+
39+
public get containerId(): Optional<string> {
40+
return this._containerId;
41+
}
42+
43+
public get container(): Optional<Container> {
44+
const docker = new Docker();
45+
return this.containerId.isPresent()
46+
? Optional.ofNonNull(docker.getContainer(this.containerId.get()))
47+
: Optional.empty();
48+
}
49+
50+
public get className(): string {
51+
return GoIpfsTestContainer.CLASS_NAME;
52+
}
53+
54+
constructor(public readonly options: IGoIpfsTestContainerOptions) {
55+
const fnTag = `${this.className}#constructor()`;
56+
Checks.truthy(options, `${fnTag} arg options`);
57+
58+
this.swarmPort = options.swarmPort || 4001;
59+
this.apiPort = options.apiPort || 5001;
60+
this.webGatewayPort = options.webGatewayPort || 8080;
61+
this.imageName = options.imageName || "ipfs/go-ipfs";
62+
this.imageTag = options.imageTag || "v0.8.0";
63+
this.imageFqn = `${this.imageName}:${this.imageTag}`;
64+
this.envVars = options.envVars || new Map();
65+
this.emitContainerLogs = Bools.isBooleanStrict(options.emitContainerLogs)
66+
? (options.emitContainerLogs as boolean)
67+
: true;
68+
69+
this._containerId = Optional.ofNullable(options.containerId);
70+
71+
this.logLevel = options.logLevel || "INFO";
72+
73+
const level = this.logLevel;
74+
const label = this.className;
75+
this.log = LoggerProvider.getOrCreate({ level, label });
76+
77+
this.log.debug(`Created instance of ${this.className} OK`);
78+
}
79+
80+
public async start(omitPull = false): Promise<Container> {
81+
const docker = new Docker();
82+
if (this.containerId.isPresent()) {
83+
this.log.warn(`Container ID provided. Will not start new one.`);
84+
const container = docker.getContainer(this.containerId.get());
85+
return container;
86+
}
87+
// otherwise we carry on with launching the container
88+
89+
if (!omitPull) {
90+
await Containers.pullImage(this.imageFqn);
91+
}
92+
93+
const dockerEnvVars: string[] = new Array(...this.envVars).map(
94+
(pairs) => `${pairs[0]}=${pairs[1]}`,
95+
);
96+
97+
const createOptions = {
98+
ExposedPorts: {
99+
[`${this.swarmPort}/tcp`]: {},
100+
[`${this.apiPort}/tcp`]: {},
101+
[`${this.webGatewayPort}/tcp`]: {},
102+
},
103+
Env: dockerEnvVars,
104+
Healthcheck: {
105+
Test: [
106+
"CMD-SHELL",
107+
`wget -O- --post-data='' --header='Content-Type:application/json' 'http://127.0.0.1:5001/api/v0/commands'`,
108+
],
109+
Interval: 1000000000, // 1 second
110+
Timeout: 3000000000, // 3 seconds
111+
Retries: 99,
112+
StartPeriod: 1000000000, // 1 second
113+
},
114+
HostConfig: {
115+
PublishAllPorts: true,
116+
},
117+
};
118+
119+
this.log.debug(`Starting ${this.imageFqn} with options: `, createOptions);
120+
121+
return new Promise<Container>((resolve, reject) => {
122+
const eventEmitter: EventEmitter = docker.run(
123+
this.imageFqn,
124+
[],
125+
[],
126+
createOptions,
127+
{},
128+
(err: Error) => {
129+
if (err) {
130+
const errorMessage = `Failed to start container ${this.imageFqn}`;
131+
reject(new RuntimeError(errorMessage, err));
132+
}
133+
},
134+
);
135+
136+
eventEmitter.once("start", async (container: Container) => {
137+
const { id } = container;
138+
this.log.debug(`Started ${this.imageFqn} successfully. ID=${id}`);
139+
this._containerId = Optional.ofNonNull(id);
140+
141+
if (this.emitContainerLogs) {
142+
const logOptions = { follow: true, stderr: true, stdout: true };
143+
const logStream = await container.logs(logOptions);
144+
logStream.on("data", (data: Buffer) => {
145+
const fnTag = `[${this.imageFqn}]`;
146+
this.log.debug(`${fnTag} %o`, data.toString("utf-8"));
147+
});
148+
}
149+
150+
try {
151+
await Containers.waitForHealthCheck(this.containerId.get());
152+
resolve(container);
153+
} catch (ex) {
154+
reject(ex);
155+
}
156+
});
157+
});
158+
}
159+
160+
public async stop(): Promise<unknown> {
161+
return Containers.stop(this.container.get());
162+
}
163+
164+
public async destroy(): Promise<unknown> {
165+
return this.container.get().remove();
166+
}
167+
168+
public async getApiUrl(): Promise<string> {
169+
const port = await this.getApiPort();
170+
return `http://127.0.0.1:${port}`;
171+
}
172+
173+
public async getWebGatewayUrl(): Promise<string> {
174+
const port = await this.getWebGatewayPort();
175+
return `http://127.0.0.1:${port}`;
176+
}
177+
178+
public async getApiPort(): Promise<number> {
179+
const containerInfo = await Containers.getById(this.containerId.get());
180+
return Containers.getPublicPort(this.apiPort, containerInfo);
181+
}
182+
183+
public async getWebGatewayPort(): Promise<number> {
184+
const containerInfo = await Containers.getById(this.containerId.get());
185+
return Containers.getPublicPort(this.webGatewayPort, containerInfo);
186+
}
187+
188+
public async getSwarmPort(): Promise<number> {
189+
const containerInfo = await Containers.getById(this.containerId.get());
190+
return Containers.getPublicPort(this.swarmPort, containerInfo);
191+
}
192+
}

packages/cactus-test-tooling/src/main/typescript/public-api.ts

+5
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,11 @@ export {
9393
OpenEthereumTestLedger,
9494
} from "./openethereum/openethereum-test-ledger";
9595

96+
export {
97+
GoIpfsTestContainer,
98+
IGoIpfsTestContainerOptions,
99+
} from "./go-ipfs/go-ipfs-test-container";
100+
96101
export {
97102
SAMPLE_CORDAPP_ROOT_DIRS,
98103
SampleCordappEnum,

0 commit comments

Comments
 (0)