From e4224f86c085c609303cdd72fef39384237af430 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Gronowski?= Date: Mon, 1 Sep 2025 16:03:16 +0200 Subject: [PATCH 1/4] integration/internal: Handle Buildkit in GetImageIDFromBody MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BuildKit emits some additional events during build and they are not `build.Result` so don't fail if we encounter one. Signed-off-by: Paweł Gronowski (cherry picked from commit ad830a47af5e495f9b76f6457e80ceefcf7a7daf) Signed-off-by: Paweł Gronowski --- integration/internal/build/build.go | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/integration/internal/build/build.go b/integration/internal/build/build.go index 1be988cd51..2b69b0bcfd 100644 --- a/integration/internal/build/build.go +++ b/integration/internal/build/build.go @@ -30,12 +30,10 @@ func Do(ctx context.Context, t *testing.T, client client.APIClient, buildCtx *fa // GetImageIDFromBody reads the image ID from the build response body. func GetImageIDFromBody(t *testing.T, body io.Reader) string { - var ( - jm jsonmessage.JSONMessage - br build.Result - dec = json.NewDecoder(body) - ) + var id string + dec := json.NewDecoder(body) for { + var jm jsonmessage.JSONMessage err := dec.Decode(&jm) if err == io.EOF { break @@ -44,10 +42,18 @@ func GetImageIDFromBody(t *testing.T, body io.Reader) string { if jm.Aux == nil { continue } - assert.NilError(t, json.Unmarshal(*jm.Aux, &br)) - assert.Assert(t, br.ID != "", "could not read image ID from build output") - break + + var br build.Result + if err := json.Unmarshal(*jm.Aux, &br); err == nil { + if br.ID == "" { + continue + } + id = br.ID + break + } } - io.Copy(io.Discard, body) - return br.ID + _, _ = io.Copy(io.Discard, body) + + assert.Assert(t, id != "", "could not read image ID from build output") + return id } From 687b206c6b642608f0d61bda090a4169311dca0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Gronowski?= Date: Mon, 1 Sep 2025 16:03:34 +0200 Subject: [PATCH 2/4] c8d/history: Fix non-native platforms MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When building a non-native platform, it's not unpacked by default. History tries to read the disk usage of all the layer and it doesn't handle missing snapshots gracefully. This patch fixes this. Signed-off-by: Paweł Gronowski (cherry picked from commit 27fca93b653f7503515d9a3436290a36631c209b) Signed-off-by: Paweł Gronowski --- daemon/containerd/image_history.go | 21 +++++- integration/image/history_test.go | 109 +++++++++++++++++++++++++++++ 2 files changed, 127 insertions(+), 3 deletions(-) diff --git a/daemon/containerd/image_history.go b/daemon/containerd/image_history.go index 32afe7d640..079e7d738b 100644 --- a/daemon/containerd/image_history.go +++ b/daemon/containerd/image_history.go @@ -2,9 +2,11 @@ package containerd import ( "context" + "fmt" "time" c8dimages "github.com/containerd/containerd/v2/core/images" + cerrdefs "github.com/containerd/errdefs" "github.com/containerd/log" "github.com/containerd/platforms" "github.com/distribution/reference" @@ -44,20 +46,33 @@ func (i *ImageService) ImageHistory(ctx context.Context, name string, platform * var ( history []*imagetype.HistoryResponseItem - sizes []int64 ) s := i.client.SnapshotService(i.snapshotter) diffIDs := ociImage.RootFS.DiffIDs + + sizes := make([]int64, len(diffIDs)) for i := range diffIDs { chainID := identity.ChainID(diffIDs[0 : i+1]).String() use, err := s.Usage(ctx, chainID) if err != nil { - return nil, err + if !cerrdefs.IsNotFound(err) { + return nil, fmt.Errorf("%w: failed to calculate disk usage of chain: %w", cerrdefs.ErrInternal, err) + } + + log.G(ctx).WithFields(log.Fields{ + "error": err, + "chainID": chainID, + "name": name, + "platform": platform, + }).Warn("failed to calculate disk usage of chain - snapshot not found") + + sizes[i] = 0 + continue } - sizes = append(sizes, use.Size) + sizes[i] = use.Size } for _, h := range ociImage.History { diff --git a/integration/image/history_test.go b/integration/image/history_test.go index 9dd7ebf3bf..9cb9be7b08 100644 --- a/integration/image/history_test.go +++ b/integration/image/history_test.go @@ -1,11 +1,21 @@ package image import ( + "context" + "io" "testing" + "github.com/containerd/platforms" + buildtypes "github.com/docker/docker/api/types/build" + imagetypes "github.com/docker/docker/api/types/image" + "github.com/docker/docker/client" "github.com/docker/docker/integration/internal/build" + "github.com/docker/docker/testutil" "github.com/docker/docker/testutil/fakecontext" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" "gotest.tools/v3/assert" + is "gotest.tools/v3/assert/cmp" + "gotest.tools/v3/skip" ) func TestAPIImagesHistory(t *testing.T) { @@ -31,3 +41,102 @@ func TestAPIImagesHistory(t *testing.T) { assert.Assert(t, found) } + +// TestAPIImageHistoryCrossPlatform tests the image history functionality +// when dealing with cross-platform image builds. +// This is a regression test for https://github.com/moby/moby/issues/50851 +// where `docker history` fails with "snapshot does not exist" error for +// images built for non-native platforms. +func TestAPIImageHistoryCrossPlatform(t *testing.T) { + skip.If(t, testEnv.DaemonInfo.OSType == "windows") + + ctx := setupTest(t) + apiClient := testEnv.APIClient() + + // Determine the non-native platform to use for testing + nonNativePlatform := ocispec.Platform{OS: testEnv.DaemonInfo.OSType, Architecture: "amd64"} + if testEnv.DaemonInfo.Architecture == "amd64" { + nonNativePlatform = ocispec.Platform{OS: testEnv.DaemonInfo.OSType, Architecture: "arm64"} + } + + // We need to pull the image for the non-native platform + // TODO: Make sure we have a multi-platform frozen image we could use + pullImageForPlatform(t, ctx, apiClient, "alpine", nonNativePlatform) + + dockerfile := "FROM alpine\nRUN true" + + buildCtx := fakecontext.New(t, t.TempDir(), fakecontext.WithDockerfile(dockerfile)) + defer buildCtx.Close() + + // Build the image for a non-native platform + resp, err := apiClient.ImageBuild(ctx, buildCtx.AsTarReader(t), buildtypes.ImageBuildOptions{ + Version: buildtypes.BuilderBuildKit, + Tags: []string{"cross-platform-test"}, + Platform: platforms.FormatAll(nonNativePlatform), + }) + assert.NilError(t, err) + defer resp.Body.Close() + + imgID := build.GetImageIDFromBody(t, resp.Body) + t.Cleanup(func() { + apiClient.ImageRemove(ctx, imgID, imagetypes.RemoveOptions{Force: true}) + }) + + testCases := []struct { + name string + imageRef string + options []client.ImageHistoryOption + }{ + { + name: "without explicit platform", + imageRef: imgID, + options: nil, + }, + { + name: "with explicit platform", + imageRef: imgID, + options: []client.ImageHistoryOption{client.ImageHistoryWithPlatform(nonNativePlatform)}, + }, + { + name: "using image reference", + imageRef: "cross-platform-test", + options: nil, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + ctx := testutil.StartSpan(ctx, t) + + hist, err := apiClient.ImageHistory(ctx, tc.imageRef, tc.options...) + + assert.NilError(t, err) + found := false + for _, layer := range hist { + if layer.ID == imgID { + found = true + break + } + } + assert.Assert(t, found, "History should contain the built image ID") + assert.Assert(t, is.Len(hist, 3)) + + for i, layer := range hist { + assert.Assert(t, layer.Size >= 0, "Layer %d should not have negative size", i) + } + }) + } +} + +func pullImageForPlatform(t *testing.T, ctx context.Context, apiClient client.APIClient, ref string, platform ocispec.Platform) { + pullResp, err := apiClient.ImagePull(ctx, ref, imagetypes.PullOptions{Platform: platforms.FormatAll(platform)}) + assert.NilError(t, err) + _, _ = io.Copy(io.Discard, pullResp) + + _, err = apiClient.ImageInspect(ctx, ref) + assert.NilError(t, err) + + t.Cleanup(func() { + _, _ = apiClient.ImageRemove(ctx, ref, imagetypes.RemoveOptions{Force: true}) + }) +} From d70382e4424e19cf65d9837a6d8ed8fcb9a5f566 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Gronowski?= Date: Mon, 1 Sep 2025 18:23:46 +0200 Subject: [PATCH 3/4] integration/internal: Print Buildkit logs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Paweł Gronowski (cherry picked from commit ce338dec81b34f7edc51890c0e2c64ead4e92c74) Signed-off-by: Paweł Gronowski --- integration/internal/build/build.go | 57 ++++++++++++++++++++++++++++- 1 file changed, 56 insertions(+), 1 deletion(-) diff --git a/integration/internal/build/build.go b/integration/internal/build/build.go index 2b69b0bcfd..91fb084711 100644 --- a/integration/internal/build/build.go +++ b/integration/internal/build/build.go @@ -1,16 +1,19 @@ package build import ( + "bytes" "context" "encoding/json" "io" "testing" + "github.com/containerd/containerd/v2/pkg/protobuf/proto" "github.com/docker/docker/api/types/build" "github.com/docker/docker/api/types/image" "github.com/docker/docker/client" "github.com/docker/docker/pkg/jsonmessage" "github.com/docker/docker/testutil/fakecontext" + controlapi "github.com/moby/buildkit/api/services/control" "gotest.tools/v3/assert" ) @@ -31,6 +34,7 @@ func Do(ctx context.Context, t *testing.T, client client.APIClient, buildCtx *fa // GetImageIDFromBody reads the image ID from the build response body. func GetImageIDFromBody(t *testing.T, body io.Reader) string { var id string + buf := bytes.NewBuffer(nil) dec := json.NewDecoder(body) for { var jm jsonmessage.JSONMessage @@ -39,6 +43,19 @@ func GetImageIDFromBody(t *testing.T, body io.Reader) string { break } assert.NilError(t, err) + + if handled := processBuildkitAux(t, &jm, &id); handled { + continue + } + + buf.Reset() + jm.Display(buf, false) + if buf.Len() == 0 { + continue + } + + t.Log(buf.String()) + if jm.Aux == nil { continue } @@ -49,11 +66,49 @@ func GetImageIDFromBody(t *testing.T, body io.Reader) string { continue } id = br.ID - break + continue } + + t.Log("Raw Aux", string(*jm.Aux)) } _, _ = io.Copy(io.Discard, body) assert.Assert(t, id != "", "could not read image ID from build output") return id } + +func processBuildkitAux(t *testing.T, jm *jsonmessage.JSONMessage, id *string) bool { + if jm.ID == "moby.buildkit.trace" { + var dt []byte + if err := json.Unmarshal(*jm.Aux, &dt); err != nil { + t.Log("Error unmarshalling buildkit trace", err) + return true + } + var sr controlapi.StatusResponse + if err := proto.Unmarshal(dt, &sr); err != nil { + t.Log("Error unmarshalling buildkit trace proto", err) + return true + } + for _, vtx := range sr.GetVertexes() { + t.Log(vtx.String()) + } + for _, vtx := range sr.GetStatuses() { + t.Log(vtx.String()) + } + for _, vtx := range sr.GetLogs() { + t.Log(vtx.String()) + } + for _, vtx := range sr.GetWarnings() { + t.Log(vtx.String()) + } + return true + } + if jm.ID == "moby.image.id" { + var br build.Result + if err := json.Unmarshal(*jm.Aux, &br); err == nil { + *id = br.ID + return true + } + } + return false +} From cfa70d073ec486c561dd2024050f66d5192a5a34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Gronowski?= Date: Tue, 2 Sep 2025 11:38:24 +0200 Subject: [PATCH 4/4] gha/arm64: Setup qemu MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Make it possible to build non-native images like the other test-integration workflows Signed-off-by: Paweł Gronowski (cherry picked from commit 8031b077bcee3c3cc7e438adc0f4dd05ec621757) Signed-off-by: Paweł Gronowski --- .github/workflows/arm64.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/arm64.yml b/.github/workflows/arm64.yml index fee8e545aa..7077fb8793 100644 --- a/.github/workflows/arm64.yml +++ b/.github/workflows/arm64.yml @@ -109,6 +109,9 @@ jobs: version: ${{ env.SETUP_BUILDX_VERSION }} driver-opts: image=${{ env.SETUP_BUILDKIT_IMAGE }} buildkitd-flags: --debug + - + name: Set up QEMU + uses: docker/setup-qemu-action@v3 - name: Build dev image uses: docker/bake-action@v6 @@ -198,6 +201,9 @@ jobs: version: ${{ env.SETUP_BUILDX_VERSION }} driver-opts: image=${{ env.SETUP_BUILDKIT_IMAGE }} buildkitd-flags: --debug + - + name: Set up QEMU + uses: docker/setup-qemu-action@v3 - name: Build dev image uses: docker/bake-action@v6