mirror of
https://github.com/moby/moby.git
synced 2026-08-06 16:11:48 +00:00
Merge pull request #50875 from vvoland/50867-28.x
[28.x backport] c8d/history: Fix non-native platforms
This commit is contained in:
6
.github/workflows/arm64.yml
vendored
6
.github/workflows/arm64.yml
vendored
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user