Merge pull request #50875 from vvoland/50867-28.x

[28.x backport] c8d/history: Fix non-native platforms
This commit is contained in:
Austin Vazquez
2025-09-02 07:16:20 -07:00
committed by GitHub
4 changed files with 204 additions and 13 deletions

View File

@@ -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

View File

@@ -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 {

View File

@@ -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})
})
}

View File

@@ -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"
)
@@ -30,24 +33,82 @@ 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
buf := bytes.NewBuffer(nil)
dec := json.NewDecoder(body)
for {
var jm jsonmessage.JSONMessage
err := dec.Decode(&jm)
if err == io.EOF {
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
}
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
continue
}
t.Log("Raw Aux", string(*jm.Aux))
}
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
}
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
}