| package image | |
| import ( | |
| "strings" | |
| "testing" | |
| "github.com/docker/docker/api/types" | |
| containertypes "github.com/docker/docker/api/types/container" | |
| "github.com/docker/docker/api/types/filters" | |
| "github.com/docker/docker/api/types/image" | |
| "github.com/docker/docker/client" | |
| "github.com/docker/docker/integration/internal/container" | |
| "github.com/docker/docker/internal/testutils/specialimage" | |
| "github.com/docker/docker/testutil" | |
| "github.com/docker/docker/testutil/daemon" | |
| "gotest.tools/v3/assert" | |
| is "gotest.tools/v3/assert/cmp" | |
| "gotest.tools/v3/skip" | |
| ) | |
| // Regression test for: https://github.com/moby/moby/issues/45732 | |
| func TestPruneDontDeleteUsedDangling(t *testing.T) { | |
| skip.If(t, testEnv.DaemonInfo.OSType == "windows", "cannot start multiple daemons on windows") | |
| skip.If(t, testEnv.IsRemoteDaemon, "cannot run daemon when remote daemon") | |
| ctx := setupTest(t) | |
| d := daemon.New(t) | |
| d.Start(t) | |
| defer d.Stop(t) | |
| client := d.NewClientT(t) | |
| defer client.Close() | |
| danglingID := specialimage.Load(ctx, t, client, specialimage.Dangling) | |
| _, _, err := client.ImageInspectWithRaw(ctx, danglingID) | |
| assert.NilError(t, err, "Test dangling image doesn't exist") | |
| container.Create(ctx, t, client, | |
| container.WithImage(danglingID), | |
| container.WithCmd("sleep", "60")) | |
| pruned, err := client.ImagesPrune(ctx, filters.NewArgs(filters.Arg("dangling", "true"))) | |
| assert.NilError(t, err) | |
| for _, deleted := range pruned.ImagesDeleted { | |
| if strings.Contains(deleted.Deleted, danglingID) || strings.Contains(deleted.Untagged, danglingID) { | |
| t.Errorf("used dangling image %s shouldn't be deleted", danglingID) | |
| } | |
| } | |
| _, _, err = client.ImageInspectWithRaw(ctx, danglingID) | |
| assert.NilError(t, err, "Test dangling image should still exist") | |
| } | |
| func TestPruneLexographicalOrder(t *testing.T) { | |
| skip.If(t, testEnv.DaemonInfo.OSType == "windows", "cannot start multiple daemons on windows") | |
| skip.If(t, testEnv.IsRemoteDaemon, "cannot run daemon when remote daemon") | |
| ctx := setupTest(t) | |
| d := daemon.New(t) | |
| d.Start(t) | |
| defer d.Stop(t) | |
| apiClient := d.NewClientT(t) | |
| defer apiClient.Close() | |
| d.LoadBusybox(ctx, t) | |
| inspect, _, err := apiClient.ImageInspectWithRaw(ctx, "busybox:latest") | |
| assert.NilError(t, err) | |
| id := inspect.ID | |
| var tags = []string{"h", "a", "j", "o", "s", "q", "w", "e", "r", "t"} | |
| for _, tag := range tags { | |
| err = apiClient.ImageTag(ctx, id, "busybox:"+tag) | |
| assert.NilError(t, err) | |
| } | |
| err = apiClient.ImageTag(ctx, id, "busybox:z") | |
| assert.NilError(t, err) | |
| _, err = apiClient.ImageRemove(ctx, "busybox:latest", image.RemoveOptions{Force: true}) | |
| assert.NilError(t, err) | |
| // run container | |
| cid := container.Create(ctx, t, apiClient, container.WithImage(id)) | |
| defer container.Remove(ctx, t, apiClient, cid, containertypes.RemoveOptions{Force: true}) | |
| pruned, err := apiClient.ImagesPrune(ctx, filters.NewArgs(filters.Arg("dangling", "false"))) | |
| assert.NilError(t, err) | |
| assert.Check(t, is.Len(pruned.ImagesDeleted, len(tags))) | |
| for _, p := range pruned.ImagesDeleted { | |
| assert.Check(t, is.Equal(p.Deleted, "")) | |
| assert.Check(t, p.Untagged != "busybox:z") | |
| } | |
| } | |
| // Regression test for https://github.com/moby/moby/issues/48063 | |
| func TestPruneDontDeleteUsedImage(t *testing.T) { | |
| skip.If(t, testEnv.DaemonInfo.OSType == "windows", "cannot start multiple daemons on windows") | |
| skip.If(t, testEnv.IsRemoteDaemon, "cannot run daemon when remote daemon") | |
| ctx := setupTest(t) | |
| for _, env := range []struct { | |
| name string | |
| prepare func(t *testing.T, client *daemon.Daemon, apiClient *client.Client) error | |
| check func(t *testing.T, apiClient *client.Client, pruned image.PruneReport) | |
| }{ | |
| { | |
| // Container uses the busybox:latest image and it's the only image | |
| // tag with the same target. | |
| name: "single tag", | |
| check: func(t *testing.T, apiClient *client.Client, pruned image.PruneReport) { | |
| assert.Check(t, is.Len(pruned.ImagesDeleted, 0)) | |
| _, _, err := apiClient.ImageInspectWithRaw(ctx, "busybox:latest") | |
| assert.NilError(t, err, "Busybox image should still exist") | |
| }, | |
| }, | |
| { | |
| // Container uses the busybox:latest image and there's also a second | |
| // busybox:other tag pointing to the same image. | |
| name: "two tags", | |
| prepare: func(t *testing.T, d *daemon.Daemon, apiClient *client.Client) error { | |
| return apiClient.ImageTag(ctx, "busybox:latest", "busybox:a") | |
| }, | |
| check: func(t *testing.T, apiClient *client.Client, pruned image.PruneReport) { | |
| if assert.Check(t, is.Len(pruned.ImagesDeleted, 1)) { | |
| assert.Check(t, is.Equal(pruned.ImagesDeleted[0].Deleted, "")) | |
| assert.Check(t, is.Equal(pruned.ImagesDeleted[0].Untagged, "busybox:a")) | |
| } | |
| _, _, err := apiClient.ImageInspectWithRaw(ctx, "busybox:a") | |
| assert.Check(t, err != nil, "Busybox:a image should be deleted") | |
| _, _, err = apiClient.ImageInspectWithRaw(ctx, "busybox:latest") | |
| assert.Check(t, err == nil, "Busybox:latest image should still exist") | |
| }, | |
| }, | |
| } { | |
| for _, tc := range []struct { | |
| name string | |
| imageID func(t *testing.T, inspect types.ImageInspect) string | |
| }{ | |
| { | |
| name: "full id", | |
| imageID: func(t *testing.T, inspect types.ImageInspect) string { | |
| return inspect.ID | |
| }, | |
| }, | |
| { | |
| name: "full id without sha256 prefix", | |
| imageID: func(t *testing.T, inspect types.ImageInspect) string { | |
| return strings.TrimPrefix(inspect.ID, "sha256:") | |
| }, | |
| }, | |
| { | |
| name: "truncated id (without sha256 prefix)", | |
| imageID: func(t *testing.T, inspect types.ImageInspect) string { | |
| return strings.TrimPrefix(inspect.ID, "sha256:")[:8] | |
| }, | |
| }, | |
| { | |
| name: "repo and digest without tag", | |
| imageID: func(t *testing.T, inspect types.ImageInspect) string { | |
| skip.If(t, !testEnv.UsingSnapshotter()) | |
| return "busybox@" + inspect.ID | |
| }, | |
| }, | |
| { | |
| name: "tagged and digested", | |
| imageID: func(t *testing.T, inspect types.ImageInspect) string { | |
| skip.If(t, !testEnv.UsingSnapshotter()) | |
| return "busybox:latest@" + inspect.ID | |
| }, | |
| }, | |
| { | |
| name: "repo digest", | |
| imageID: func(t *testing.T, inspect types.ImageInspect) string { | |
| // graphdriver won't have a repo digest | |
| skip.If(t, len(inspect.RepoDigests) == 0, "no repo digest") | |
| return inspect.RepoDigests[0] | |
| }, | |
| }, | |
| } { | |
| tc := tc | |
| t.Run(env.name+"/"+tc.name, func(t *testing.T) { | |
| ctx := testutil.StartSpan(ctx, t) | |
| d := daemon.New(t) | |
| d.Start(t) | |
| defer d.Stop(t) | |
| apiClient := d.NewClientT(t) | |
| defer apiClient.Close() | |
| d.LoadBusybox(ctx, t) | |
| if env.prepare != nil { | |
| err := env.prepare(t, d, apiClient) | |
| assert.NilError(t, err, "prepare failed") | |
| } | |
| inspect, _, err := apiClient.ImageInspectWithRaw(ctx, "busybox:latest") | |
| assert.NilError(t, err) | |
| image := tc.imageID(t, inspect) | |
| t.Log(image) | |
| cid := container.Run(ctx, t, apiClient, | |
| container.WithImage(image), | |
| container.WithCmd("sleep", "60")) | |
| defer container.Remove(ctx, t, apiClient, cid, containertypes.RemoveOptions{Force: true}) | |
| // dangling=false also prunes unused images | |
| pruned, err := apiClient.ImagesPrune(ctx, filters.NewArgs(filters.Arg("dangling", "false"))) | |
| assert.NilError(t, err) | |
| env.check(t, apiClient, pruned) | |
| }) | |
| } | |
| } | |
| } |