mirror of
https://github.com/moby/moby.git
synced 2026-01-11 18:51:37 +00:00
c8d/history: Fix non-native platforms
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 <pawel.gronowski@docker.com>
This commit is contained in:
@@ -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,20 @@
|
||||
package image
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"github.com/moby/moby/v2/integration/internal/build"
|
||||
"github.com/containerd/platforms"
|
||||
buildtypes "github.com/moby/moby/api/types/build"
|
||||
"github.com/moby/moby/client"
|
||||
build "github.com/moby/moby/v2/integration/internal/build"
|
||||
"github.com/moby/moby/v2/testutil"
|
||||
"github.com/moby/moby/v2/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 +40,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, client.ImageRemoveOptions{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, client.ImagePullOptions{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, client.ImageRemoveOptions{Force: true})
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user