Skip to content

Commit 8c746c3

Browse files
committed
feat(indy-test-ledger): add helper class for indy ledger
- Add `IndyTestLedger` class to setup test indy ledger for testing purposes. - Add functional tests for new test ledger class. - Minor fix in cleaning up sample indy test ledger. Peter's changes: 1. Rebased onto upstream/main as of 2024-01-21 which was a bit of a challenge on account of this being quite old (110 commits behind) 2. Meaning that there's a chance that I messed up some of the conflict resolutions in a way that is sub-optimal, please forgive if this is the case! Depends on #2861 Co-authored-by: Peter Somogyvari <[email protected]> Signed-off-by: Michal Bajer <[email protected]> Signed-off-by: Peter Somogyvari <[email protected]>
1 parent 0f7d7b7 commit 8c746c3

File tree

8 files changed

+902
-2
lines changed

8 files changed

+902
-2
lines changed

.cspell.json

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"minWordLength": 4,
55
"allowCompoundWords": true,
66
"words": [
7+
"outsh",
78
"adminpw",
89
"Albertirsa",
910
"ALLFORTX",

packages/cactus-test-tooling/package.json

+6
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,12 @@
9090
"web3-utils": "1.6.1"
9191
},
9292
"devDependencies": {
93+
"@aries-framework/askar": "0.5.0-alpha.58",
94+
"@aries-framework/core": "0.5.0-alpha.58",
95+
"@aries-framework/indy-vdr": "0.5.0-alpha.58",
96+
"@aries-framework/node": "0.5.0-alpha.58",
97+
"@hyperledger/aries-askar-nodejs": "0.2.0-dev.1",
98+
"@hyperledger/indy-vdr-nodejs": "0.2.0-dev.3",
9399
"@types/dockerode": "3.2.7",
94100
"@types/esm": "3.2.0",
95101
"@types/fs-extra": "9.0.13",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,368 @@
1+
import { EventEmitter } from "events";
2+
import Docker, { Container } from "dockerode";
3+
import { v4 as internalIpV4 } from "internal-ip";
4+
import type { IndyVdrPoolConfig } from "@aries-framework/indy-vdr";
5+
6+
import {
7+
Logger,
8+
Checks,
9+
LogLevelDesc,
10+
LoggerProvider,
11+
} from "@hyperledger/cactus-common";
12+
13+
import { Containers } from "../common/containers";
14+
15+
export interface IIndyTestLedgerOptions {
16+
readonly containerImageName?: string;
17+
readonly containerImageVersion?: string;
18+
readonly logLevel?: LogLevelDesc;
19+
readonly emitContainerLogs?: boolean;
20+
readonly envVars?: string[];
21+
// For test development, attach to ledger that is already running, don't spin up new one
22+
readonly useRunningLedger?: boolean;
23+
}
24+
25+
/**
26+
* Default values used by IndyTestLedger constructor.
27+
*/
28+
export const INDY_TEST_LEDGER_DEFAULT_OPTIONS = Object.freeze({
29+
containerImageName: "ghcr.io/outsh/cactus-indy-all-in-one",
30+
containerImageVersion: "0.1",
31+
logLevel: "info" as LogLevelDesc,
32+
emitContainerLogs: false,
33+
envVars: [],
34+
useRunningLedger: false,
35+
});
36+
37+
const INDY_ENDORSER_DID_SEED = "000000000000000000000000Steward1";
38+
const GENESIS_FILE_PATH = "/var/lib/indy/sandbox/pool_transactions_genesis";
39+
const DEFAULT_DID_INDY_NAMESPACE = "cacti:test";
40+
const DEFAULT_POOL_ADDRESS = "172.16.0.2";
41+
const DEFAULT_NODE1_PORT = "9701";
42+
const DEFAULT_NODE1_CLIENT_PORT = "9702";
43+
const DEFAULT_NODE2_PORT = "9703";
44+
const DEFAULT_NODE2_CLIENT_PORT = "9704";
45+
const DEFAULT_NODE3_PORT = "9705";
46+
const DEFAULT_NODE3_CLIENT_PORT = "9706";
47+
const DEFAULT_NODE4_PORT = "9707";
48+
const DEFAULT_NODE4_CLIENT_PORT = "9708";
49+
50+
export class IndyTestLedger {
51+
private readonly log: Logger;
52+
private readonly logLevel: LogLevelDesc;
53+
private readonly containerImageName: string;
54+
private readonly containerImageVersion: string;
55+
private readonly envVars: string[];
56+
private readonly emitContainerLogs: boolean;
57+
public readonly useRunningLedger: boolean;
58+
private _container: Container | undefined;
59+
60+
public get fullContainerImageName(): string {
61+
return [this.containerImageName, this.containerImageVersion].join(":");
62+
}
63+
64+
public get className(): string {
65+
return "IndyTestLedger";
66+
}
67+
68+
public get container(): Container {
69+
if (this._container) {
70+
return this._container;
71+
} else {
72+
throw new Error(`Invalid state: _container is not set. Called start()?`);
73+
}
74+
}
75+
76+
constructor(public readonly options: IIndyTestLedgerOptions) {
77+
Checks.truthy(options, `${this.className} arg options`);
78+
79+
this.logLevel =
80+
this.options.logLevel || INDY_TEST_LEDGER_DEFAULT_OPTIONS.logLevel;
81+
this.log = LoggerProvider.getOrCreate({
82+
level: this.logLevel,
83+
label: this.className,
84+
});
85+
86+
this.emitContainerLogs =
87+
options?.emitContainerLogs ??
88+
INDY_TEST_LEDGER_DEFAULT_OPTIONS.emitContainerLogs;
89+
this.useRunningLedger =
90+
options?.useRunningLedger ??
91+
INDY_TEST_LEDGER_DEFAULT_OPTIONS.useRunningLedger;
92+
this.containerImageName =
93+
this.options.containerImageName ||
94+
INDY_TEST_LEDGER_DEFAULT_OPTIONS.containerImageName;
95+
this.containerImageVersion =
96+
this.options.containerImageVersion ||
97+
INDY_TEST_LEDGER_DEFAULT_OPTIONS.containerImageVersion;
98+
this.envVars =
99+
this.options.envVars || INDY_TEST_LEDGER_DEFAULT_OPTIONS.envVars;
100+
101+
this.log.info(
102+
`Created ${this.className} OK. Image FQN: ${this.fullContainerImageName}`,
103+
);
104+
}
105+
106+
/**
107+
* Get container status.
108+
*
109+
* @returns status string
110+
*/
111+
public async getContainerStatus(): Promise<string> {
112+
if (!this.container) {
113+
throw new Error(
114+
"IndyTestLedger#getContainerStatus(): Container not started yet!",
115+
);
116+
}
117+
118+
const { Status } = await Containers.getById(this.container.id);
119+
return Status;
120+
}
121+
122+
/**
123+
* Start a test Indy ledger.
124+
*
125+
* @param omitPull Don't pull docker image from upstream if true.
126+
* @returns Promise<Container>
127+
*/
128+
public async start(omitPull = false): Promise<Container> {
129+
if (this.useRunningLedger) {
130+
this.log.info(
131+
"Search for already running Indy Test Ledger because 'useRunningLedger' flag is enabled.",
132+
);
133+
this.log.info(
134+
"Search criteria - image name: ",
135+
this.fullContainerImageName,
136+
", state: running",
137+
);
138+
const containerInfo = await Containers.getByPredicate(
139+
(ci) =>
140+
ci.Image === this.fullContainerImageName && ci.State === "running",
141+
);
142+
const docker = new Docker();
143+
this._container = docker.getContainer(containerInfo.Id);
144+
return this._container;
145+
}
146+
147+
if (this._container) {
148+
this.log.warn("Container was already running - restarting it...");
149+
await this.container.stop();
150+
await this.container.remove();
151+
this._container = undefined;
152+
}
153+
154+
if (!omitPull) {
155+
await Containers.pullImage(
156+
this.fullContainerImageName,
157+
{},
158+
this.logLevel,
159+
);
160+
}
161+
162+
return new Promise<Container>((resolve, reject) => {
163+
const docker = new Docker();
164+
const eventEmitter: EventEmitter = docker.run(
165+
this.fullContainerImageName,
166+
[],
167+
[],
168+
{
169+
ExposedPorts: {
170+
[`${DEFAULT_NODE1_PORT}/tcp`]: {},
171+
[`${DEFAULT_NODE1_CLIENT_PORT}/tcp`]: {},
172+
[`${DEFAULT_NODE2_PORT}/tcp`]: {},
173+
[`${DEFAULT_NODE2_CLIENT_PORT}/tcp`]: {},
174+
[`${DEFAULT_NODE3_PORT}/tcp`]: {},
175+
[`${DEFAULT_NODE3_CLIENT_PORT}/tcp`]: {},
176+
[`${DEFAULT_NODE4_PORT}/tcp`]: {},
177+
[`${DEFAULT_NODE4_CLIENT_PORT}/tcp`]: {},
178+
},
179+
Env: this.envVars,
180+
HostConfig: {
181+
PublishAllPorts: true,
182+
},
183+
},
184+
{},
185+
(err?: Error) => {
186+
if (err) {
187+
this.log.error(
188+
`Failed to start ${this.fullContainerImageName} container; `,
189+
err,
190+
);
191+
reject(err);
192+
}
193+
},
194+
);
195+
196+
eventEmitter.once("start", async (container: Container) => {
197+
this._container = container;
198+
199+
if (this.emitContainerLogs) {
200+
const fnTag = `[${this.fullContainerImageName}]`;
201+
await Containers.streamLogs({
202+
container: this.container,
203+
tag: fnTag,
204+
log: this.log,
205+
});
206+
}
207+
208+
try {
209+
await Containers.waitForHealthCheck(this.container.id);
210+
resolve(container);
211+
} catch (ex) {
212+
reject(ex);
213+
}
214+
});
215+
});
216+
}
217+
218+
/**
219+
* Stop a test Indy ledger.
220+
*
221+
* @returns Stop operation results.
222+
*/
223+
public async stop(): Promise<unknown> {
224+
if (this.useRunningLedger) {
225+
this.log.info("Ignore stop request because useRunningLedger is enabled.");
226+
return;
227+
} else if (this.container) {
228+
return Containers.stop(this.container);
229+
} else {
230+
throw new Error(
231+
`IndyTestLedger#stop() Container was never created, nothing to stop.`,
232+
);
233+
}
234+
}
235+
236+
/**
237+
* Destroy a test Indy ledger.
238+
*
239+
* @returns Destroy operation results.
240+
*/
241+
public async destroy(): Promise<unknown> {
242+
if (this.useRunningLedger) {
243+
this.log.info(
244+
"Ignore destroy request because useRunningLedger is enabled.",
245+
);
246+
return;
247+
} else if (this.container) {
248+
return this.container.remove();
249+
} else {
250+
throw new Error(
251+
`IndyTestLedger#destroy() Container was never created, nothing to destroy.`,
252+
);
253+
}
254+
}
255+
256+
/**
257+
* Get localhost mapping of specified container port.
258+
*
259+
* @param port port in container
260+
* @returns localhost port
261+
*/
262+
private async getHostPort(port: string): Promise<number> {
263+
const fnTag = `${this.className}#getHostPort()`;
264+
if (this.container) {
265+
const cInfo = await Containers.getById(this.container.id);
266+
return Containers.getPublicPort(parseInt(port, 10), cInfo);
267+
} else {
268+
throw new Error(`${fnTag} Container not set. Did you call start()?`);
269+
}
270+
}
271+
272+
/**
273+
* Read ledger `pool_transactions_genesis` file from container storage.
274+
* Patch the node IP and ports to match the ones exported to the localhost matchine.
275+
*
276+
* @returns pool_transactions_genesis contents
277+
*/
278+
public async readPoolTransactionsGenesis(): Promise<string> {
279+
if (!this.container) {
280+
throw new Error(
281+
"IndyTestLedger#readPoolTransactionsGenesis(): Container not started yet!",
282+
);
283+
}
284+
285+
// Read pool_transactions_genesis file
286+
this.log.debug("Get client config from path:", GENESIS_FILE_PATH);
287+
let genesisFile = await Containers.pullFile(
288+
this.container,
289+
GENESIS_FILE_PATH,
290+
"ascii",
291+
);
292+
// this.log.debug("Raw pool_transactions_genesis file:", genesisFile);
293+
294+
// Patch pool address
295+
const localhostIp = (await internalIpV4()) || "121.0.0.1";
296+
this.log.debug("localhost address found:", localhostIp);
297+
genesisFile = genesisFile.replace(
298+
new RegExp(DEFAULT_POOL_ADDRESS, "g"),
299+
localhostIp,
300+
);
301+
302+
// Patch ports
303+
genesisFile = genesisFile
304+
.replace(
305+
DEFAULT_NODE1_PORT,
306+
(await this.getHostPort(DEFAULT_NODE1_PORT)).toString(),
307+
)
308+
.replace(
309+
DEFAULT_NODE1_CLIENT_PORT,
310+
(await this.getHostPort(DEFAULT_NODE1_CLIENT_PORT)).toString(),
311+
)
312+
.replace(
313+
DEFAULT_NODE2_PORT,
314+
(await this.getHostPort(DEFAULT_NODE2_PORT)).toString(),
315+
)
316+
.replace(
317+
DEFAULT_NODE2_CLIENT_PORT,
318+
(await this.getHostPort(DEFAULT_NODE2_CLIENT_PORT)).toString(),
319+
)
320+
.replace(
321+
DEFAULT_NODE3_PORT,
322+
(await this.getHostPort(DEFAULT_NODE3_PORT)).toString(),
323+
)
324+
.replace(
325+
DEFAULT_NODE3_CLIENT_PORT,
326+
(await this.getHostPort(DEFAULT_NODE3_CLIENT_PORT)).toString(),
327+
)
328+
.replace(
329+
DEFAULT_NODE4_PORT,
330+
(await this.getHostPort(DEFAULT_NODE4_PORT)).toString(),
331+
)
332+
.replace(
333+
DEFAULT_NODE4_CLIENT_PORT,
334+
(await this.getHostPort(DEFAULT_NODE4_CLIENT_PORT)).toString(),
335+
);
336+
this.log.debug("Patched pool_transactions_genesis file:", genesisFile);
337+
338+
return genesisFile;
339+
}
340+
341+
/**
342+
* Get indy VDR pool configuration object.
343+
*
344+
* @param indyNamespace namespace to use (default: `cacti:test`)
345+
* @returns `IndyVdrPoolConfig`
346+
*/
347+
public async getIndyVdrPoolConfig(
348+
indyNamespace = DEFAULT_DID_INDY_NAMESPACE,
349+
): Promise<IndyVdrPoolConfig> {
350+
const genesisTransactions = await this.readPoolTransactionsGenesis();
351+
return {
352+
isProduction: false,
353+
genesisTransactions,
354+
indyNamespace,
355+
connectOnStartup: true,
356+
};
357+
}
358+
359+
/**
360+
* Get secret seed of already registered endorser did on indy ledger.
361+
* Can be imported into ledger and used to authenticate write operations on Indy VDR.
362+
*
363+
* @returns DID Seed
364+
*/
365+
public getEndorserDidSeed(): string {
366+
return INDY_ENDORSER_DID_SEED;
367+
}
368+
}

0 commit comments

Comments
 (0)