Skip to content

Commit 6ff1b98

Browse files
committed
fix(test-tooling): bind test ledgers to port zero for macOS
This makes the ledger container based integration tests pass on macOS that also leads to the CI script finally passing on Macs in general. Yaaay! Fixes hyperledger-cacti#186 Signed-off-by: Peter Somogyvari <[email protected]>
1 parent 92e7b0b commit 6ff1b98

File tree

5 files changed

+229
-119
lines changed

5 files changed

+229
-119
lines changed

packages/cactus-test-tooling/src/main/typescript/besu/besu-test-ledger.ts

+79-52
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import Docker, { Container } from "dockerode";
2-
import isPortReachable from "is-port-reachable";
1+
import Docker, { Container, ContainerInfo } from "dockerode";
2+
import axios from "axios";
33
import Joi from "joi";
44
import tar from "tar-stream";
55
import { EventEmitter } from "events";
@@ -56,10 +56,9 @@ export class BesuTestLedger implements ITestLedger {
5656
}
5757

5858
public getContainer(): Container {
59+
const fnTag = "BesuTestLedger#getContainer()";
5960
if (!this.container) {
60-
throw new Error(
61-
`BesuTestLedger#getBesuKeyPair() container wasn't started by this instance yet.`
62-
);
61+
throw new Error(`${fnTag} container not yet started by this instance.`);
6362
} else {
6463
return this.container;
6564
}
@@ -70,8 +69,9 @@ export class BesuTestLedger implements ITestLedger {
7069
}
7170

7271
public async getRpcApiHttpHost(): Promise<string> {
73-
const ipAddress: string = await this.getContainerIpAddress();
74-
return `http://${ipAddress}:${this.rpcApiHttpPort}`;
72+
const ipAddress: string = "127.0.0.1";
73+
const hostPort: number = await this.getRpcApiPublicPort();
74+
return `http://${ipAddress}:${hostPort}`;
7575
}
7676

7777
public async getFileContents(filePath: string): Promise<string> {
@@ -137,16 +137,10 @@ export class BesuTestLedger implements ITestLedger {
137137
"9001/tcp": {}, // supervisord - HTTP
138138
"9545/tcp": {}, // besu metrics
139139
},
140-
Hostconfig: {
141-
PortBindings: {
142-
// [`${this.rpcApiHttpPort}/tcp`]: [{ HostPort: '8545', }],
143-
// '8546/tcp': [{ HostPort: '8546', }],
144-
// '8080/tcp': [{ HostPort: '8080', }],
145-
// '8888/tcp': [{ HostPort: '8888', }],
146-
// '9001/tcp': [{ HostPort: '9001', }],
147-
// '9545/tcp': [{ HostPort: '9545', }],
148-
},
149-
},
140+
// This is a workaround needed for macOS which has issues with routing
141+
// to docker container's IP addresses directly...
142+
// https://stackoverflow.com/a/39217691
143+
PublishAllPorts: true,
150144
},
151145
{},
152146
(err: any) => {
@@ -158,15 +152,8 @@ export class BesuTestLedger implements ITestLedger {
158152

159153
eventEmitter.once("start", async (container: Container) => {
160154
this.container = container;
161-
// once the container has started, we wait until the the besu RPC API starts listening on the designated port
162-
// which we determine by continously trying to establish a socket until it actually works
163-
const host: string = await this.getContainerIpAddress();
164155
try {
165-
let reachable: boolean = false;
166-
do {
167-
reachable = await isPortReachable(this.rpcApiHttpPort, { host });
168-
await new Promise((resolve2) => setTimeout(resolve2, 100));
169-
} while (!reachable);
156+
await this.waitForHealthCheck();
170157
resolve(container);
171158
} catch (ex) {
172159
reject(ex);
@@ -175,7 +162,27 @@ export class BesuTestLedger implements ITestLedger {
175162
});
176163
}
177164

165+
public async waitForHealthCheck(timeoutMs: number = 120000): Promise<void> {
166+
const fnTag = "BesuTestLedger#waitForHealthCheck()";
167+
const httpUrl = await this.getRpcApiHttpHost();
168+
const startedAt = Date.now();
169+
let reachable: boolean = false;
170+
do {
171+
try {
172+
const res = await axios.get(httpUrl);
173+
reachable = res.status > 199 && res.status < 300;
174+
} catch (ex) {
175+
reachable = false;
176+
if (Date.now() >= startedAt + timeoutMs) {
177+
throw new Error(`${fnTag} timed out (${timeoutMs}ms) -> ${ex.stack}`);
178+
}
179+
}
180+
await new Promise((resolve2) => setTimeout(resolve2, 100));
181+
} while (!reachable);
182+
}
183+
178184
public stop(): Promise<any> {
185+
const fnTag = "BesuTestLedger#stop()";
179186
return new Promise((resolve, reject) => {
180187
if (this.container) {
181188
this.container.stop({}, (err: any, result: any) => {
@@ -186,54 +193,74 @@ export class BesuTestLedger implements ITestLedger {
186193
}
187194
});
188195
} else {
189-
return reject(
190-
new Error(
191-
`BesuTestLedger#stop() Container was not running to begin with.`
192-
)
193-
);
196+
return reject(new Error(`${fnTag} Container was not running.`));
194197
}
195198
});
196199
}
197200

198201
public destroy(): Promise<any> {
202+
const fnTag = "BesuTestLedger#destroy()";
199203
if (this.container) {
200204
return this.container.remove();
201205
} else {
202-
return Promise.reject(
203-
new Error(
204-
`BesuTestLedger#destroy() Container was never created, nothing to destroy.`
205-
)
206-
);
206+
const ex = new Error(`${fnTag} Container not found, nothing to destroy.`);
207+
return Promise.reject(ex);
207208
}
208209
}
209210

210-
public async getContainerIpAddress(): Promise<string> {
211+
protected async getContainerInfo(): Promise<ContainerInfo> {
211212
const docker = new Docker();
212-
const containerImageName = this.getContainerImageName();
213-
const containerInfos: Docker.ContainerInfo[] = await docker.listContainers(
214-
{}
215-
);
213+
const image = this.getContainerImageName();
214+
const containerInfos = await docker.listContainers({});
215+
216+
const aContainerInfo = containerInfos.find((ci) => ci.Image === image);
217+
218+
if (aContainerInfo) {
219+
return aContainerInfo;
220+
} else {
221+
throw new Error(`BesuTestLedger#getContainerInfo() no image "${image}"`);
222+
}
223+
}
224+
225+
public async getRpcApiPublicPort(): Promise<number> {
226+
const fnTag = "BesuTestLedger#getRpcApiPublicPort()";
227+
const aContainerInfo = await this.getContainerInfo();
228+
const { rpcApiHttpPort: thePort } = this;
229+
const { Ports: ports } = aContainerInfo;
230+
231+
if (ports.length < 1) {
232+
throw new Error(`${fnTag} no ports exposed or mapped at all`);
233+
}
234+
const mapping = ports.find((x) => x.PrivatePort === thePort);
235+
if (mapping) {
236+
if (!mapping.PublicPort) {
237+
throw new Error(`${fnTag} port ${thePort} mapped but not public`);
238+
} else if (mapping.IP !== "0.0.0.0") {
239+
throw new Error(`${fnTag} port ${thePort} mapped to localhost`);
240+
} else {
241+
return mapping.PublicPort;
242+
}
243+
} else {
244+
throw new Error(`${fnTag} no mapping found for ${thePort}`);
245+
}
246+
}
247+
248+
public async getContainerIpAddress(): Promise<string> {
249+
const fnTag = "BesuTestLedger#getContainerIpAddress()";
250+
const aContainerInfo = await this.getContainerInfo();
216251

217-
const aContainerInfo = containerInfos.find(
218-
(ci) => ci.Image === containerImageName
219-
);
220252
if (aContainerInfo) {
221253
const { NetworkSettings } = aContainerInfo;
222254
const networkNames: string[] = Object.keys(NetworkSettings.Networks);
223255
if (networkNames.length < 1) {
224-
throw new Error(
225-
`BesuTestLedger#getContainerIpAddress() no network found: ${JSON.stringify(
226-
NetworkSettings
227-
)}`
228-
);
256+
throw new Error(`${fnTag} container not connected to any networks`);
229257
} else {
230-
// return IP address of container on the first network that we found it connected to. Make this configurable?
258+
// return IP address of container on the first network that we found
259+
// it connected to. Make this configurable?
231260
return NetworkSettings.Networks[networkNames[0]].IPAddress;
232261
}
233262
} else {
234-
throw new Error(
235-
`BesuTestLedger#getContainerIpAddress() cannot find container image ${this.containerImageName}`
236-
);
263+
throw new Error(`${fnTag} cannot find image: ${this.containerImageName}`);
237264
}
238265
}
239266

packages/cactus-test-tooling/src/main/typescript/http-echo/http-echo-container.ts

+52-20
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import Docker, { Container } from "dockerode";
1+
import Docker, { Container, ContainerInfo } from "dockerode";
22
import isPortReachable from "is-port-reachable";
33
import Joi from "joi";
44
import { EventEmitter } from "events";
@@ -80,10 +80,13 @@ export class HttpEchoContainer implements ITestLedger {
8080
["--port", this.httpPort.toString(10)],
8181
[],
8282
{
83-
ExposedPorts: {},
84-
Hostconfig: {
85-
PortBindings: {},
83+
ExposedPorts: {
84+
[`${this.httpPort}/tcp`]: {},
8685
},
86+
// This is a workaround needed for macOS which has issues with routing
87+
// to docker container's IP addresses directly...
88+
// https://stackoverflow.com/a/39217691
89+
PublishAllPorts: true,
8790
},
8891
{},
8992
(err: any) => {
@@ -95,11 +98,12 @@ export class HttpEchoContainer implements ITestLedger {
9598

9699
eventEmitter.once("start", async (container: Container) => {
97100
this.container = container;
98-
const host: string = await this.getContainerIpAddress();
101+
const host: string = "127.0.0.1";
102+
const hostPort = await this.getPublicHttpPort();
99103
try {
100104
let reachable: boolean = false;
101105
do {
102-
reachable = await isPortReachable(this.httpPort, { host });
106+
reachable = await isPortReachable(hostPort, { host });
103107
await new Promise((resolve2) => setTimeout(resolve2, 100));
104108
} while (!reachable);
105109
resolve(container);
@@ -142,31 +146,59 @@ export class HttpEchoContainer implements ITestLedger {
142146
}
143147
}
144148

145-
public async getContainerIpAddress(): Promise<string> {
149+
protected async getContainerInfo(): Promise<ContainerInfo> {
150+
const fnTag = "HttpEchoContainer#getContainerInfo()";
146151
const docker = new Docker();
147-
const imageName = this.getImageName();
148-
const containerInfos: Docker.ContainerInfo[] = await docker.listContainers(
149-
{}
150-
);
152+
const image = this.getImageName();
153+
const containerInfos = await docker.listContainers({});
154+
155+
const aContainerInfo = containerInfos.find((ci) => ci.Image === image);
156+
157+
if (aContainerInfo) {
158+
return aContainerInfo;
159+
} else {
160+
throw new Error(`${fnTag} no image found: "${image}"`);
161+
}
162+
}
163+
164+
public async getPublicHttpPort(): Promise<number> {
165+
const fnTag = "HttpEchoContainer#getRpcApiPublicPort()";
166+
const aContainerInfo = await this.getContainerInfo();
167+
const { httpPort: thePort } = this;
168+
const { Ports: ports } = aContainerInfo;
169+
170+
if (ports.length < 1) {
171+
throw new Error(`${fnTag} no ports exposed or mapped at all`);
172+
}
173+
const mapping = ports.find((x) => x.PrivatePort === thePort);
174+
if (mapping) {
175+
if (!mapping.PublicPort) {
176+
throw new Error(`${fnTag} port ${thePort} mapped but not public`);
177+
} else if (mapping.IP !== "0.0.0.0") {
178+
throw new Error(`${fnTag} port ${thePort} mapped to localhost`);
179+
} else {
180+
return mapping.PublicPort;
181+
}
182+
} else {
183+
throw new Error(`${fnTag} no mapping found for ${thePort}`);
184+
}
185+
}
186+
187+
public async getContainerIpAddress(): Promise<string> {
188+
const fnTag = "HttpEchoContainer#getContainerIpAddress()";
189+
const aContainerInfo = await this.getContainerInfo();
151190

152-
const aContainerInfo = containerInfos.find((ci) => ci.Image === imageName);
153191
if (aContainerInfo) {
154192
const { NetworkSettings } = aContainerInfo;
155193
const networkNames: string[] = Object.keys(NetworkSettings.Networks);
156194
if (networkNames.length < 1) {
157-
throw new Error(
158-
`HttpEchoContainer#getContainerIpAddress() no network found: ${JSON.stringify(
159-
NetworkSettings
160-
)}`
161-
);
195+
throw new Error(`${fnTag} container not on any networks`);
162196
} else {
163197
// return IP address of container on the first network that we found it connected to. Make this configurable?
164198
return NetworkSettings.Networks[networkNames[0]].IPAddress;
165199
}
166200
} else {
167-
throw new Error(
168-
`HttpEchoContainer#getContainerIpAddress() cannot find container image ${this.imageName}`
169-
);
201+
throw new Error(`${fnTag} cannot find container image ${this.imageName}`);
170202
}
171203
}
172204

0 commit comments

Comments
 (0)