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 " ;
3
3
import Joi from "joi" ;
4
4
import tar from "tar-stream" ;
5
5
import { EventEmitter } from "events" ;
@@ -56,10 +56,9 @@ export class BesuTestLedger implements ITestLedger {
56
56
}
57
57
58
58
public getContainer ( ) : Container {
59
+ const fnTag = "BesuTestLedger#getContainer()" ;
59
60
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.` ) ;
63
62
} else {
64
63
return this . container ;
65
64
}
@@ -70,8 +69,9 @@ export class BesuTestLedger implements ITestLedger {
70
69
}
71
70
72
71
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 } ` ;
75
75
}
76
76
77
77
public async getFileContents ( filePath : string ) : Promise < string > {
@@ -137,16 +137,10 @@ export class BesuTestLedger implements ITestLedger {
137
137
"9001/tcp" : { } , // supervisord - HTTP
138
138
"9545/tcp" : { } , // besu metrics
139
139
} ,
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 ,
150
144
} ,
151
145
{ } ,
152
146
( err : any ) => {
@@ -158,15 +152,8 @@ export class BesuTestLedger implements ITestLedger {
158
152
159
153
eventEmitter . once ( "start" , async ( container : Container ) => {
160
154
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 ( ) ;
164
155
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 ( ) ;
170
157
resolve ( container ) ;
171
158
} catch ( ex ) {
172
159
reject ( ex ) ;
@@ -175,7 +162,27 @@ export class BesuTestLedger implements ITestLedger {
175
162
} ) ;
176
163
}
177
164
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
+
178
184
public stop ( ) : Promise < any > {
185
+ const fnTag = "BesuTestLedger#stop()" ;
179
186
return new Promise ( ( resolve , reject ) => {
180
187
if ( this . container ) {
181
188
this . container . stop ( { } , ( err : any , result : any ) => {
@@ -186,54 +193,74 @@ export class BesuTestLedger implements ITestLedger {
186
193
}
187
194
} ) ;
188
195
} 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.` ) ) ;
194
197
}
195
198
} ) ;
196
199
}
197
200
198
201
public destroy ( ) : Promise < any > {
202
+ const fnTag = "BesuTestLedger#destroy()" ;
199
203
if ( this . container ) {
200
204
return this . container . remove ( ) ;
201
205
} 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 ) ;
207
208
}
208
209
}
209
210
210
- public async getContainerIpAddress ( ) : Promise < string > {
211
+ protected async getContainerInfo ( ) : Promise < ContainerInfo > {
211
212
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 ( ) ;
216
251
217
- const aContainerInfo = containerInfos . find (
218
- ( ci ) => ci . Image === containerImageName
219
- ) ;
220
252
if ( aContainerInfo ) {
221
253
const { NetworkSettings } = aContainerInfo ;
222
254
const networkNames : string [ ] = Object . keys ( NetworkSettings . Networks ) ;
223
255
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` ) ;
229
257
} 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?
231
260
return NetworkSettings . Networks [ networkNames [ 0 ] ] . IPAddress ;
232
261
}
233
262
} 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 } ` ) ;
237
264
}
238
265
}
239
266
0 commit comments