Skip to content
This repository was archived by the owner on Jul 18, 2025. It is now read-only.

Commit ebe7816

Browse files
raynatopedrajetapetermetz
authored andcommitted
test(test tooling): add DamlTestLedger implementation
Primary Changes --------------- 1. Create a test tooling class for DAML AIO Image Fixes hyperledger-cacti#3435 Signed-off-by: raynato.c.pedrajeta <raynato.c.pedrajeta@accenture.com>
1 parent c5bbb33 commit ebe7816

5 files changed

Lines changed: 359 additions & 8 deletions

File tree

.cspell.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
"Crpc",
5555
"CSDE",
5656
"csdetemplate",
57+
"daml",
5758
"data",
5859
"davecgh",
5960
"dclm",
Lines changed: 345 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,345 @@
1+
import Docker, { Container, ContainerInfo } from "dockerode";
2+
import Joi from "joi";
3+
import tar from "tar-stream";
4+
import { EventEmitter } from "events";
5+
import {
6+
LogLevelDesc,
7+
Logger,
8+
LoggerProvider,
9+
Bools,
10+
} from "@hyperledger/cactus-common";
11+
import { ITestLedger } from "../i-test-ledger";
12+
import { Streams } from "../common/streams";
13+
import { Containers } from "../common/containers";
14+
15+
export interface IDamlTestLedgerOptions {
16+
imageVersion?: string;
17+
imageName?: string;
18+
rpcApiHttpPort?: number;
19+
logLevel?: LogLevelDesc;
20+
emitContainerLogs?: boolean;
21+
}
22+
23+
const DEFAULTS = Object.freeze({
24+
imageVersion: "2024-09-08T07-40-07-dev-2cc217b7a",
25+
imageName: "ghcr.io/hyperledger/cacti-daml-all-in-one",
26+
rpcApiHttpPort: 7575,
27+
});
28+
29+
export const DAML_TEST_LEDGER_DEFAULT_OPTIONS = DEFAULTS;
30+
31+
export const DAML_TEST_LEDGER_OPTIONS_JOI_SCHEMA: Joi.Schema =
32+
Joi.object().keys({
33+
imageVersion: Joi.string().min(5).required(),
34+
imageName: Joi.string().min(1).required(),
35+
rpcApiHttpPort: Joi.number()
36+
.integer()
37+
.positive()
38+
.min(1024)
39+
.max(65535)
40+
.required(),
41+
});
42+
43+
export class DamlTestLedger implements ITestLedger {
44+
public readonly imageVersion: string;
45+
public readonly imageName: string;
46+
public readonly rpcApiHttpPort: number;
47+
public readonly emitContainerLogs: boolean;
48+
49+
private readonly log: Logger;
50+
private container: Container | undefined;
51+
private containerId: string | undefined;
52+
53+
constructor(public readonly opts?: IDamlTestLedgerOptions) {
54+
if (!opts) {
55+
throw new TypeError(`DAMLTestLedger#ctor options was falsy.`);
56+
}
57+
this.imageVersion = opts.imageVersion || DEFAULTS.imageVersion;
58+
this.imageName = opts.imageName || DEFAULTS.imageName;
59+
this.rpcApiHttpPort = opts.rpcApiHttpPort || DEFAULTS.rpcApiHttpPort;
60+
61+
this.emitContainerLogs = Bools.isBooleanStrict(opts.emitContainerLogs)
62+
? (opts.emitContainerLogs as boolean)
63+
: true;
64+
65+
this.validateConstructorOptions();
66+
const label = "daml-test-ledger";
67+
const level = opts.logLevel || "INFO";
68+
this.log = LoggerProvider.getOrCreate({ level, label });
69+
}
70+
71+
public getContainer(): Container {
72+
const fnTag = "DAMLTestLedger#getContainer()";
73+
if (!this.container) {
74+
throw new Error(`${fnTag} container not yet started by this instance.`);
75+
} else {
76+
return this.container;
77+
}
78+
}
79+
80+
public getContainerImageName(): string {
81+
return `${this.imageName}:${this.imageVersion}`;
82+
}
83+
84+
public async getRpcApiHttpHost(): Promise<string> {
85+
const ipAddress = "127.0.0.1";
86+
const hostPort: number = await this.getRpcApiPublicPort();
87+
return `http://${ipAddress}:${hostPort}`;
88+
}
89+
90+
public async getFileContents(filePath: string): Promise<string> {
91+
const response = await this.getContainer().getArchive({
92+
path: filePath,
93+
});
94+
const extract: tar.Extract = tar.extract({ autoDestroy: true });
95+
96+
return new Promise((resolve, reject) => {
97+
let fileContents = "";
98+
extract.on("entry", async (header: unknown, stream, next) => {
99+
stream.on("error", (err: Error) => {
100+
reject(err);
101+
});
102+
const chunks: string[] = await Streams.aggregate<string>(stream);
103+
fileContents += chunks.join("");
104+
stream.resume();
105+
next();
106+
});
107+
108+
extract.on("finish", () => {
109+
resolve(fileContents);
110+
});
111+
112+
response.pipe(extract);
113+
});
114+
}
115+
116+
public async start(omitPull = false): Promise<Container> {
117+
const imageFqn = this.getContainerImageName();
118+
119+
if (this.container) {
120+
await this.container.stop();
121+
await this.container.remove();
122+
}
123+
const docker = new Docker();
124+
125+
if (!omitPull) {
126+
this.log.debug(`Pulling container image ${imageFqn} ...`);
127+
await this.pullContainerImage(imageFqn);
128+
this.log.debug(`Pulled ${imageFqn} OK. Starting container...`);
129+
}
130+
131+
return new Promise<Container>((resolve, reject) => {
132+
const eventEmitter: EventEmitter = docker.run(
133+
imageFqn,
134+
[],
135+
[],
136+
{
137+
NetworkMode: "host",
138+
ExposedPorts: {
139+
"7575/tcp": {}, // DAML http endpoint
140+
},
141+
HostConfig: {
142+
PublishAllPorts: true,
143+
PortBindings: {
144+
"7575/tcp": [
145+
{
146+
HostPort: "7575", //change the default port of docker back to 7575
147+
},
148+
],
149+
},
150+
},
151+
},
152+
{},
153+
(err: unknown) => {
154+
if (err) {
155+
reject(err);
156+
}
157+
},
158+
);
159+
160+
eventEmitter.once("start", async (container: Container) => {
161+
this.log.debug(`Started container OK. Waiting for healthcheck...`);
162+
this.container = container;
163+
this.containerId = container.id;
164+
165+
if (this.emitContainerLogs) {
166+
const fnTag = `[${this.getContainerImageName()}]`;
167+
await Containers.streamLogs({
168+
container: this.getContainer(),
169+
tag: fnTag,
170+
log: this.log,
171+
});
172+
}
173+
174+
try {
175+
await this.waitForHealthCheck();
176+
this.log.debug(`Healthcheck passing OK.`);
177+
resolve(container);
178+
} catch (ex) {
179+
reject(ex);
180+
}
181+
});
182+
});
183+
}
184+
185+
public async waitForHealthCheck(timeoutMs = 360000): Promise<void> {
186+
const fnTag = "DAMLTestLedger#waitForHealthCheck()";
187+
const startedAt = Date.now();
188+
let isHealthy = false;
189+
do {
190+
if (Date.now() >= startedAt + timeoutMs) {
191+
throw new Error(`${fnTag} timed out (${timeoutMs}ms)`);
192+
}
193+
const { Status, State } = await this.getContainerInfo();
194+
this.log.debug(`ContainerInfo.Status=%o, State=O%`, Status, State);
195+
isHealthy = Status.endsWith("(healthy)");
196+
if (!isHealthy) {
197+
await new Promise((resolve2) => setTimeout(resolve2, 1000));
198+
}
199+
} while (!isHealthy);
200+
}
201+
202+
public stop(): Promise<unknown> {
203+
const fnTag = "DAMLTestLedger#stop()";
204+
return new Promise((resolve, reject) => {
205+
if (this.container) {
206+
this.container.stop({}, (err: unknown, result: unknown) => {
207+
if (err) {
208+
reject(err);
209+
} else {
210+
resolve(result);
211+
}
212+
});
213+
} else {
214+
return reject(new Error(`${fnTag} Container was not running.`));
215+
}
216+
});
217+
}
218+
219+
public destroy(): Promise<unknown> {
220+
const fnTag = "DAMLTestLedger#destroy()";
221+
if (this.container) {
222+
return this.container.remove();
223+
} else {
224+
const ex = new Error(`${fnTag} Container not found, nothing to destroy.`);
225+
return Promise.reject(ex);
226+
}
227+
}
228+
229+
protected async getContainerInfo(): Promise<ContainerInfo> {
230+
const docker = new Docker();
231+
const image = this.getContainerImageName();
232+
const containerInfos = await docker.listContainers({});
233+
234+
let aContainerInfo;
235+
if (this.containerId !== undefined) {
236+
aContainerInfo = containerInfos.find((ci) => ci.Id === this.containerId);
237+
}
238+
239+
if (aContainerInfo) {
240+
return aContainerInfo;
241+
} else {
242+
throw new Error(`DAMLTestLedger#getContainerInfo() no image "${image}"`);
243+
}
244+
}
245+
246+
public async getRpcApiPublicPort(): Promise<number> {
247+
const fnTag = "DAMLTestLedger#getRpcApiPublicPort()";
248+
const aContainerInfo = await this.getContainerInfo();
249+
const { rpcApiHttpPort: thePort } = this;
250+
const { Ports: ports } = aContainerInfo;
251+
252+
if (ports.length < 1) {
253+
throw new Error(`${fnTag} no ports exposed or mapped at all`);
254+
}
255+
const mapping = ports.find((x) => x.PrivatePort === thePort);
256+
if (mapping) {
257+
if (!mapping.PublicPort) {
258+
throw new Error(`${fnTag} port ${thePort} mapped but not public`);
259+
} else if (mapping.IP !== "0.0.0.0") {
260+
throw new Error(`${fnTag} port ${thePort} mapped to 127.0.0.1`);
261+
} else {
262+
return mapping.PublicPort;
263+
}
264+
} else {
265+
throw new Error(`${fnTag} no mapping found for ${thePort}`);
266+
}
267+
}
268+
public async getDamlAuthorizationToken(): Promise<string> {
269+
const docker = new Docker();
270+
const aContainerInfo = await this.getContainerInfo();
271+
const containerId = aContainerInfo.Id;
272+
const exec = await docker.getContainer(containerId).exec({
273+
AttachStdin: false,
274+
AttachStdout: true,
275+
AttachStderr: true,
276+
Tty: true,
277+
Cmd: ["/bin/bash", "-c", "cat jwt"], // Command to execute
278+
});
279+
const stream = await exec.start({});
280+
281+
return new Promise<string>((resolve, reject) => {
282+
let output = "";
283+
stream.on("data", (data: Buffer) => {
284+
output += data.toString(); // Accumulate the output
285+
resolve(output);
286+
});
287+
stream.on("error", (err: Error) => {
288+
reject(err);
289+
});
290+
});
291+
}
292+
293+
public async getContainerIpAddress(): Promise<string> {
294+
const fnTag = "DAMLTestLedger#getContainerIpAddress()";
295+
const aContainerInfo = await this.getContainerInfo();
296+
297+
if (aContainerInfo) {
298+
const { NetworkSettings } = aContainerInfo;
299+
const networkNames: string[] = Object.keys(NetworkSettings.Networks);
300+
if (networkNames.length < 1) {
301+
throw new Error(`${fnTag} container not connected to any networks`);
302+
} else {
303+
return NetworkSettings.Networks[networkNames[0]].IPAddress;
304+
}
305+
} else {
306+
throw new Error(`${fnTag} cannot find image: ${this.imageName}`);
307+
}
308+
}
309+
310+
private pullContainerImage(containerNameAndTag: string): Promise<unknown[]> {
311+
return new Promise((resolve, reject) => {
312+
const docker = new Docker();
313+
docker.pull(containerNameAndTag, (pullError: unknown, stream: never) => {
314+
if (pullError) {
315+
reject(pullError);
316+
} else {
317+
docker.modem.followProgress(
318+
stream,
319+
(progressError: unknown, output: unknown[]) => {
320+
if (progressError) {
321+
reject(progressError);
322+
} else {
323+
resolve(output);
324+
}
325+
},
326+
);
327+
}
328+
});
329+
});
330+
}
331+
332+
private validateConstructorOptions(): void {
333+
const validationResult = DAML_TEST_LEDGER_OPTIONS_JOI_SCHEMA.validate({
334+
imageVersion: this.imageVersion,
335+
imageName: this.imageName,
336+
rpcApiHttpPort: this.rpcApiHttpPort,
337+
});
338+
339+
if (validationResult.error) {
340+
throw new Error(
341+
`DAMLTestLedger#ctor ${validationResult.error.annotate()}`,
342+
);
343+
}
344+
}
345+
}

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,12 @@ export {
1313
IBesuMpTestLedgerOptions,
1414
} from "./besu/besu-mp-test-ledger";
1515

16+
export {
17+
DamlTestLedger,
18+
DAML_TEST_LEDGER_DEFAULT_OPTIONS,
19+
IDamlTestLedgerOptions,
20+
} from "./daml/daml-test-ledger";
21+
1622
export {
1723
CordaTestLedger,
1824
ICordaTestLedgerConstructorOptions,

0 commit comments

Comments
 (0)