fix(docker): reject failed image pulls instead of treating them as success

Every Docker pull went through `followProgress(pullStream, resolve)`, passing
the Promise's resolve as dockerode's onFinished(err, output) callback — so the
error argument was ignored and a failed pull (dropped/metered connection, bad
manifest, registry error, disk full mid-pull) resolved as if it had succeeded.
The code then tried to create/start a container from a missing or partial
image, surfacing a confusing downstream error rather than the real cause. (#790)

Add a DockerService.pullImage() helper that rejects when followProgress reports
an error, and route all five pull sites through it:
  - service install
  - AMD ROCm image pull
  - service update
  - force-reinstall / recreate (forcePull)
  - sysbench benchmark image pull

Closes #790

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Chris Sherwood 2026-06-06 16:03:17 -07:00 committed by jakeaturner
parent 36068c645e
commit 98d235679e
No known key found for this signature in database
GPG Key ID: B1072EBDEECE328D
2 changed files with 26 additions and 10 deletions

View File

@ -614,8 +614,7 @@ export class BenchmarkService {
await this.dockerService.docker.getImage(SYSBENCH_IMAGE).inspect()
} catch {
this._updateStatus('starting', `Pulling sysbench image...`)
const pullStream = await this.dockerService.docker.pull(SYSBENCH_IMAGE)
await new Promise((resolve) => this.dockerService.docker.modem.followProgress(pullStream, resolve))
await this.dockerService.pullImage(SYSBENCH_IMAGE)
}
}

View File

@ -58,6 +58,27 @@ export class DockerService {
}
}
/**
* Pull a Docker image and resolve only when the pull genuinely completes.
*
* dockerode's `followProgress(stream, onFinished)` reports failures via the
* first argument of onFinished. Every call site used to pass the Promise's
* `resolve` directly as that callback, so a failed pull (dropped/metered
* connection, bad manifest, registry error, disk full mid-pull) resolved as
* if it had succeeded and the code then tried to create/start a container
* from a missing or partial image, surfacing a confusing downstream error.
* Rejecting on that error here lets callers fail fast with the real cause (#790).
*/
async pullImage(imageName: string): Promise<void> {
const pullStream = await this.docker.pull(imageName)
await new Promise<void>((resolve, reject) => {
this.docker.modem.followProgress(pullStream, (error: Error | null) => {
if (error) reject(error)
else resolve()
})
})
}
async affectContainer(
serviceName: string,
action: 'start' | 'stop' | 'restart'
@ -632,13 +653,12 @@ export class DockerService {
)
} else {
// Start pulling the Docker image and wait for it to complete
const pullStream = await this.docker.pull(service.container_image)
this._broadcast(
service.service_name,
'pulling',
`Pulling Docker image ${service.container_image}...`
)
await new Promise((res) => this.docker.modem.followProgress(pullStream, res))
await this.pullImage(service.container_image)
}
if (service.service_name === SERVICE_NAMES.KIWIX) {
@ -728,8 +748,7 @@ export class DockerService {
'pulling',
`Pulling Docker image ${finalImage}...`
)
const rocmPullStream = await this.docker.pull(finalImage)
await new Promise((res) => this.docker.modem.followProgress(rocmPullStream, res))
await this.pullImage(finalImage)
}
const amdDevices = await this._discoverAMDDevices()
@ -1523,8 +1542,7 @@ export class DockerService {
// Step 1: Pull new image (runtimeImage diverges from newImage for AMD, see above)
this._broadcast(serviceName, 'update-pulling', `Pulling image ${runtimeImage}...`)
const pullStream = await this.docker.pull(runtimeImage)
await new Promise((res) => this.docker.modem.followProgress(pullStream, res))
await this.pullImage(runtimeImage)
// Step 2: Find and stop existing container
this._broadcast(serviceName, 'update-stopping', `Stopping current container...`)
@ -1996,8 +2014,7 @@ export class DockerService {
// Pull the image if it's missing locally, or always when forcePull (e.g. :latest updates).
if (opts.forcePull || !(await this._checkImageExists(service.container_image))) {
const pullStream = await this.docker.pull(service.container_image)
await new Promise((res) => this.docker.modem.followProgress(pullStream, res))
await this.pullImage(service.container_image)
}
const newContainer = await this.docker.createContainer({