|
1 | 1 | import { |
2 | 2 | Bools, |
| 3 | + ILoggerOptions, |
3 | 4 | Logger, |
4 | 5 | LoggerProvider, |
5 | 6 | LogLevelDesc, |
6 | 7 | } from "@hyperledger/cactus-common"; |
7 | 8 | import Joi from "joi"; |
8 | 9 | import Docker, { Container, ContainerInfo } from "dockerode"; |
| 10 | +import Dockerode from "dockerode"; |
9 | 11 | import EventEmitter from "events"; |
10 | 12 | import { SSHExecCommandResponse } from "node-ssh"; |
11 | 13 | import { streamLogs } from "./containers"; |
| 14 | +import pRetry from "p-retry"; |
| 15 | +import throttle from "lodash/throttle"; |
12 | 16 |
|
13 | 17 | export const CC_COMPILER_DEFAULT_OPTIONS = Object.freeze({ |
14 | 18 | containerImageVersion: "2025-08-12-d5365bf", |
15 | 19 | containerImageName: "ghcr.io/hyperledger-cacti/cactus-connector-fabric-cli", |
16 | 20 | dockerNetworkName: "bridge", |
17 | 21 | }); |
18 | 22 |
|
| 23 | +export interface IDockerPullProgressDetail { |
| 24 | + readonly current: number; |
| 25 | + readonly total: number; |
| 26 | +} |
| 27 | + |
| 28 | +export interface IDockerPullProgress { |
| 29 | + readonly status: "Downloading"; |
| 30 | + readonly progressDetail: IDockerPullProgressDetail; |
| 31 | + readonly progress: string; |
| 32 | + readonly id: string; |
| 33 | +} |
| 34 | + |
19 | 35 | export interface ICompilerToolsOptions { |
20 | 36 | containerImageVersion?: string; |
21 | 37 | containerImageName?: string; |
@@ -43,6 +59,7 @@ export class CompilerTools { |
43 | 59 | public readonly emitContainerLogs: boolean; |
44 | 60 |
|
45 | 61 | private readonly log: Logger; |
| 62 | + private readonly level: LogLevelDesc; |
46 | 63 | private container: Container | undefined; |
47 | 64 | private containerId: string | undefined; |
48 | 65 |
|
@@ -71,8 +88,8 @@ export class CompilerTools { |
71 | 88 | } |
72 | 89 |
|
73 | 90 | const label = "fabric-cc-compiler"; |
74 | | - const level = options.logLevel || "INFO"; |
75 | | - this.log = LoggerProvider.getOrCreate({ level, label }); |
| 91 | + this.level = options.logLevel || "INFO"; |
| 92 | + this.log = LoggerProvider.getOrCreate({ level: this.level, label }); |
76 | 93 | } |
77 | 94 |
|
78 | 95 | public getContainerImageName(): string { |
@@ -160,7 +177,7 @@ export class CompilerTools { |
160 | 177 |
|
161 | 178 | if (!omitPull) { |
162 | 179 | this.log.debug(`Pulling container image ${imageFqn} ...`); |
163 | | - await this.pullContainerImage(imageFqn); |
| 180 | + await CompilerTools.pullImage(imageFqn, undefined, this.level); |
164 | 181 | this.log.debug(`Pulled ${imageFqn} OK. Starting container...`); |
165 | 182 | } |
166 | 183 |
|
@@ -299,25 +316,70 @@ export class CompilerTools { |
299 | 316 | }; |
300 | 317 | } |
301 | 318 |
|
302 | | - private pullContainerImage(containerNameAndTag: string): Promise<unknown[]> { |
| 319 | + public static pullImage( |
| 320 | + imageFqn: string, |
| 321 | + options: Record<string, unknown> = {}, |
| 322 | + logLevel?: LogLevelDesc, |
| 323 | + ): Promise<unknown[]> { |
| 324 | + const defaultLoggerOptions: ILoggerOptions = { |
| 325 | + label: "containers#pullImage()", |
| 326 | + level: logLevel || "INFO", |
| 327 | + }; |
| 328 | + const log = LoggerProvider.getOrCreate(defaultLoggerOptions); |
| 329 | + const task = () => CompilerTools.tryPullImage(imageFqn, options, logLevel); |
| 330 | + const retryOptions: pRetry.Options & { retries: number } = { |
| 331 | + retries: 6, |
| 332 | + onFailedAttempt: async (ex) => { |
| 333 | + log.debug(`Failed attempt at pulling container image ${imageFqn}`, ex); |
| 334 | + }, |
| 335 | + }; |
| 336 | + return pRetry(task, retryOptions); |
| 337 | + } |
| 338 | + |
| 339 | + public static tryPullImage( |
| 340 | + imageFqn: string, |
| 341 | + options: Record<string, unknown> = {}, |
| 342 | + logLevel?: LogLevelDesc, |
| 343 | + ): Promise<unknown[]> { |
303 | 344 | return new Promise((resolve, reject) => { |
304 | | - const docker = new Docker(); |
305 | | - docker.pull(containerNameAndTag, (pullError: unknown, stream: never) => { |
| 345 | + const loggerOptions: ILoggerOptions = { |
| 346 | + label: "containers#tryPullImage()", |
| 347 | + level: logLevel || "INFO", |
| 348 | + }; |
| 349 | + const log = LoggerProvider.getOrCreate(loggerOptions); |
| 350 | + |
| 351 | + const docker = new Dockerode(); |
| 352 | + |
| 353 | + const progressPrinter = throttle((msg: IDockerPullProgress): void => { |
| 354 | + log.debug(JSON.stringify(msg.progress || msg.status)); |
| 355 | + }, 1000); |
| 356 | + |
| 357 | + const pullStreamStartedHandler = ( |
| 358 | + pullError: unknown, |
| 359 | + stream: NodeJS.ReadableStream, |
| 360 | + ) => { |
306 | 361 | if (pullError) { |
| 362 | + log.error(`Could not even start ${imageFqn} pull:`, pullError); |
307 | 363 | reject(pullError); |
308 | 364 | } else { |
| 365 | + log.debug(`Started ${imageFqn} pull progress stream OK`); |
309 | 366 | docker.modem.followProgress( |
310 | 367 | stream, |
311 | 368 | (progressError: unknown, output: unknown[]) => { |
312 | 369 | if (progressError) { |
| 370 | + log.error(`Failed to finish ${imageFqn} pull:`, progressError); |
313 | 371 | reject(progressError); |
314 | 372 | } else { |
| 373 | + log.debug(`Finished ${imageFqn} pull completely OK`); |
315 | 374 | resolve(output); |
316 | 375 | } |
317 | 376 | }, |
| 377 | + (msg: IDockerPullProgress): void => progressPrinter(msg), |
318 | 378 | ); |
319 | 379 | } |
320 | | - }); |
| 380 | + }; |
| 381 | + |
| 382 | + docker.pull(imageFqn, options, pullStreamStartedHandler); |
321 | 383 | }); |
322 | 384 | } |
323 | 385 | } |
0 commit comments