From a74bfc3ad7f79cdfd84a909944bc66b61ccb6665 Mon Sep 17 00:00:00 2001 From: Jvst Me Date: Fri, 29 May 2026 22:04:06 +0200 Subject: [PATCH] Fix image pull progress when reported in seconds Newer Docker daemon versions (tested with 29.5.2) report layer extraction progress using only elapsed seconds instead of current and total bytes. ```json {"status":"Extracting","progressDetail":{"current":3,"units":"s"},"id":"9cb31e2e37ea"} ``` In that case, we will now ignore the layer extraction progress (because we cannot display per-layer seconds, and they are not very useful) and only include the layer's full size in the `extracted_bytes` counter when the layer completes extraction. The pull progress indicator in the CLI still looks the same, but the extraction counter is less granular with newer Docker versions than with older ones. --- runner/internal/shim/docker.go | 37 +++++++++++++++++++---------- runner/internal/shim/docker_test.go | 28 ++++++++++++++++++++++ 2 files changed, 53 insertions(+), 12 deletions(-) diff --git a/runner/internal/shim/docker.go b/runner/internal/shim/docker.go index 49a11c7e7..604cc8354 100644 --- a/runner/internal/shim/docker.go +++ b/runner/internal/shim/docker.go @@ -56,17 +56,26 @@ const ( // https://github.com/moby/moby/blob/e77ff99ede5ee5952b3a9227863552ae6e5b6fb1/pkg/jsonmessage/jsonmessage.go#L144 // All fields are optional. type PullMessage struct { - Id string `json:"id"` // layer id - Status string `json:"status"` - ProgressDetail struct { - Current uint64 `json:"current"` // bytes - Total uint64 `json:"total"` // bytes - } `json:"progressDetail"` - ErrorDetail struct { + Id string `json:"id"` // layer id + Status string `json:"status"` + ProgressDetail ProgressDetail `json:"progressDetail"` + ErrorDetail struct { Message string `json:"message"` } `json:"errorDetail"` } +type ProgressDetail struct { + Current uint64 `json:"current"` + Total uint64 `json:"total"` + Units string `json:"units"` +} + +func (p *ProgressDetail) isUnitBytes() bool { + // > Units is the unit to print for progress. It defaults to "bytes" if empty + // https://github.com/moby/moby/blob/8151a55a776f5f83f68bcf0030c19031439ea357/api/types/jsonstream/progress.go#L9 + return p.Units == "bytes" || p.Units == "" +} + type layerProgress struct { Status string DownloadedBytes uint64 @@ -94,14 +103,18 @@ func (t *PullTracker) Update(msg PullMessage) { case "Pulling fs layer", "Waiting", "Verifying Checksum", "Already exists": // no bytes to update, just track status case "Downloading": - layer.DownloadedBytes = msg.ProgressDetail.Current - layer.TotalBytes = msg.ProgressDetail.Total + if msg.ProgressDetail.isUnitBytes() { + layer.DownloadedBytes = msg.ProgressDetail.Current + layer.TotalBytes = msg.ProgressDetail.Total + } case "Download complete": layer.DownloadedBytes = layer.TotalBytes case "Extracting": - layer.ExtractedBytes = msg.ProgressDetail.Current - layer.DownloadedBytes = msg.ProgressDetail.Total - layer.TotalBytes = msg.ProgressDetail.Total + if msg.ProgressDetail.isUnitBytes() { + layer.ExtractedBytes = msg.ProgressDetail.Current + layer.DownloadedBytes = msg.ProgressDetail.Total + layer.TotalBytes = msg.ProgressDetail.Total + } case "Pull complete": layer.ExtractedBytes = layer.TotalBytes layer.DownloadedBytes = layer.TotalBytes diff --git a/runner/internal/shim/docker_test.go b/runner/internal/shim/docker_test.go index 259c76dcd..e8dbdc350 100644 --- a/runner/internal/shim/docker_test.go +++ b/runner/internal/shim/docker_test.go @@ -268,3 +268,31 @@ func TestPullTracker_MixedLayerStatuses(t *testing.T) { assert.Equal(t, uint64(100+200), p.TotalBytes) assert.False(t, p.IsTotalBytesFinal) // layer-waiting size unknown } + +func TestPullTracker_NonBytesExtractingUnit(t *testing.T) { + tracker := newPullTracker() + tracker.Update(PullMessage{Id: "3.11", Status: "Pulling from library/python"}) + tracker.Update(PullMessage{Id: "aaa", Status: "Pulling fs layer"}) + tracker.Update(pullMsg("aaa", "Downloading", 100, 200)) + tracker.Update(pullMsg("aaa", "Downloading", 200, 200)) + tracker.Update(PullMessage{Id: "aaa", Status: "Download complete"}) + // Newer Docker daemons report extraction progress in seconds. Tested with 29.5.2 + tracker.Update(PullMessage{Id: "aaa", Status: "Extracting", ProgressDetail: ProgressDetail{Current: 1, Units: "s"}}) + tracker.Update(PullMessage{Id: "aaa", Status: "Extracting", ProgressDetail: ProgressDetail{Current: 2, Units: "s"}}) + + p := tracker.Progress() + require.NotNil(t, p) + assert.Equal(t, uint64(200), p.DownloadedBytes) + assert.Equal(t, uint64(0), p.ExtractedBytes) // reported in seconds, bytes unknown + assert.Equal(t, uint64(200), p.TotalBytes) + assert.True(t, p.IsTotalBytesFinal) + + tracker.Update(PullMessage{Id: "aaa", Status: "Pull complete"}) + + p = tracker.Progress() + require.NotNil(t, p) + assert.Equal(t, uint64(200), p.DownloadedBytes) + assert.Equal(t, uint64(200), p.ExtractedBytes) // pull complete => extracted == total + assert.Equal(t, uint64(200), p.TotalBytes) + assert.True(t, p.IsTotalBytesFinal) +}