| package image |
| |
| import ( |
| "context" |
| "io" |
| "testing" |
| |
| "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) { |
| ctx := setupTest(t) |
| client := testEnv.APIClient() |
| |
| dockerfile := "FROM busybox\nENV FOO bar" |
| |
| imgID := build.Do(ctx, t, client, fakecontext.New(t, t.TempDir(), fakecontext.WithDockerfile(dockerfile))) |
| |
| historydata, err := client.ImageHistory(ctx, imgID) |
| assert.NilError(t, err) |
| |
| assert.Assert(t, len(historydata) != 0) |
| |
| var found bool |
| for _, imageLayer := range historydata { |
| if imageLayer.ID == imgID { |
| found = true |
| break |
| } |
| } |
| |
| 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}) |
| }) |
| } |