Files
moby/client/system_disk_usage_test.go
Austin Vazquez 46dba49ec8 Fix reclaimable image disk usage calculation for in-use images
This change reworks to build up the reclaimable image disk uage instead of setting it to total size and subtracting active images. This change also includes the image index size as reclaimable if it is included with the image summary.

Signed-off-by: Austin Vazquez <austin.vazquez@docker.com>
2026-03-19 17:47:26 -05:00

277 lines
7.6 KiB
Go

package client
import (
"fmt"
"net/http"
"testing"
cerrdefs "github.com/containerd/errdefs"
"github.com/moby/moby/api/types/image"
"github.com/moby/moby/api/types/system"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
)
func TestDiskUsageError(t *testing.T) {
client, err := New(WithMockClient(errorMock(http.StatusInternalServerError, "Server error")))
assert.NilError(t, err)
_, err = client.DiskUsage(t.Context(), DiskUsageOptions{})
assert.Check(t, is.ErrorType(err, cerrdefs.IsInternal))
}
func TestDiskUsage(t *testing.T) {
const expectedURL = "/system/df"
client, err := New(WithMockClient(func(req *http.Request) (*http.Response, error) {
if err := assertRequest(req, http.MethodGet, expectedURL); err != nil {
return nil, err
}
return mockJSONResponse(http.StatusOK, nil, system.DiskUsage{
ImageUsage: &image.DiskUsage{
ActiveCount: 0,
TotalCount: 0,
Reclaimable: 0,
TotalSize: 4096,
Items: []image.Summary{},
},
})(req)
}))
assert.NilError(t, err)
du, err := client.DiskUsage(t.Context(), DiskUsageOptions{})
assert.NilError(t, err)
assert.Equal(t, du.Images.ActiveCount, int64(0))
assert.Equal(t, du.Images.TotalCount, int64(0))
assert.Equal(t, du.Images.Reclaimable, int64(0))
assert.Equal(t, du.Images.TotalSize, int64(4096))
assert.Equal(t, len(du.Images.Items), 0)
}
func TestDiskUsageWithOptions(t *testing.T) {
const expectedURL = "/system/df"
tests := []struct {
options DiskUsageOptions
expectedQuery string
}{
{
options: DiskUsageOptions{
Containers: true,
},
expectedQuery: "type=container",
},
{
options: DiskUsageOptions{
Images: true,
},
expectedQuery: "type=image",
},
{
options: DiskUsageOptions{
Volumes: true,
},
expectedQuery: "type=volume",
},
{
options: DiskUsageOptions{
BuildCache: true,
},
expectedQuery: "type=build-cache",
},
{
options: DiskUsageOptions{
Containers: true,
Images: true,
},
expectedQuery: "type=container&type=image",
},
{
options: DiskUsageOptions{
Containers: true,
Images: true,
Volumes: true,
BuildCache: true,
},
expectedQuery: "type=container&type=image&type=volume&type=build-cache",
},
{
options: DiskUsageOptions{
Containers: true,
Verbose: true,
},
expectedQuery: "type=container&verbose=1",
},
{
options: DiskUsageOptions{
Containers: true,
Images: true,
Volumes: true,
BuildCache: true,
Verbose: true,
},
expectedQuery: "type=container&type=image&type=volume&type=build-cache&verbose=1",
},
}
for _, tt := range tests {
t.Run(fmt.Sprintf("options=%+v", tt.options), func(t *testing.T) {
client, err := New(WithMockClient(func(req *http.Request) (*http.Response, error) {
if err := assertRequestWithQuery(req, http.MethodGet, expectedURL, tt.expectedQuery); err != nil {
return nil, err
}
return mockJSONResponse(http.StatusOK, nil, system.DiskUsage{})(req)
}))
assert.NilError(t, err)
_, err = client.DiskUsage(t.Context(), tt.options)
assert.NilError(t, err)
})
}
}
func TestLegacyDiskUsage(t *testing.T) {
const legacyVersion = "1.51"
const expectedURL = "/system/df"
client, err := New(
WithAPIVersion(legacyVersion),
WithMockClient(func(req *http.Request) (*http.Response, error) {
if err := assertRequest(req, http.MethodGet, "/v"+legacyVersion+expectedURL); err != nil {
return nil, err
}
return mockJSONResponse(http.StatusOK, nil, &legacyDiskUsage{
LayersSize: 4096,
Images: []image.Summary{},
})(req)
}))
assert.NilError(t, err)
du, err := client.DiskUsage(t.Context(), DiskUsageOptions{})
assert.NilError(t, err)
assert.Equal(t, du.Images.ActiveCount, int64(0))
assert.Equal(t, du.Images.TotalCount, int64(0))
assert.Equal(t, du.Images.Reclaimable, int64(0))
assert.Equal(t, du.Images.TotalSize, int64(4096))
assert.Equal(t, len(du.Images.Items), 0)
}
func TestImageDiskUsageFromLegacyAPI(t *testing.T) {
const legacyVersion = "1.51"
const expectedURL = "/system/df"
tests := []struct {
name string
mockResponse *legacyDiskUsage
expectedActiveCount int64
expectedTotalCount int64
expectedReclaimable int64
expectedTotalSize int64
}{
{
name: "no images",
mockResponse: &legacyDiskUsage{
LayersSize: 0,
Images: []image.Summary{},
},
expectedActiveCount: 0,
expectedTotalCount: 0,
expectedReclaimable: 0,
expectedTotalSize: 0,
},
{
name: "images with no containers",
mockResponse: &legacyDiskUsage{
LayersSize: 8192,
Images: []image.Summary{
{ID: "image1", Size: 4096, SharedSize: 0, Containers: 0},
{ID: "image2", Size: 4096, SharedSize: 0, Containers: 0},
},
},
expectedActiveCount: 0,
expectedTotalCount: 2,
expectedReclaimable: 8192,
expectedTotalSize: 8192,
},
{
name: "images with containers",
mockResponse: &legacyDiskUsage{
LayersSize: 12288,
Images: []image.Summary{
{ID: "image1", Size: 4096, SharedSize: 0, Containers: 2},
{ID: "image2", Size: 2048, SharedSize: 0, Containers: 0},
{ID: "image3", Size: 6144, SharedSize: 0, Containers: 1},
},
},
expectedActiveCount: 2,
expectedTotalCount: 3,
expectedReclaimable: 2048,
expectedTotalSize: 12288,
},
{
name: "images with shared size",
mockResponse: &legacyDiskUsage{
LayersSize: 15360,
Images: []image.Summary{
{ID: "image1", Size: 4096, SharedSize: 1024, Containers: 1},
{ID: "image2", Size: 8192, SharedSize: 2048, Containers: 0},
{ID: "image3", Size: 3072, SharedSize: 1024, Containers: 0},
},
},
expectedActiveCount: 1,
expectedTotalCount: 3,
expectedReclaimable: 8192, // (8192-2048) + (3072-1024)
expectedTotalSize: 15360,
},
{
name: "multiplatform image with an image index",
mockResponse: &legacyDiskUsage{
LayersSize: 4096,
Images: []image.Summary{
{ID: "image1", Size: 4096, SharedSize: 0, Containers: 0, Descriptor: &ocispec.Descriptor{MediaType: ocispec.MediaTypeImageIndex, Size: 512}},
},
},
expectedActiveCount: 0,
expectedTotalCount: 1,
expectedReclaimable: 4096, // (4096 - 0)
expectedTotalSize: 4096,
},
{
name: "multiplatform image with a Docker distribution manifest",
mockResponse: &legacyDiskUsage{
LayersSize: 4096,
Images: []image.Summary{
{ID: "image1", Size: 4096, SharedSize: 0, Containers: 0, Descriptor: &ocispec.Descriptor{MediaType: "application/vnd.docker.distribution.manifest.v2+json", Size: 427}},
},
},
expectedActiveCount: 0,
expectedTotalCount: 1,
expectedReclaimable: 4096, // (4096 - 0)
expectedTotalSize: 4096,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
client, err := New(
WithAPIVersion(legacyVersion),
WithMockClient(func(req *http.Request) (*http.Response, error) {
if err := assertRequest(req, http.MethodGet, "/v"+legacyVersion+expectedURL); err != nil {
return nil, err
}
return mockJSONResponse(http.StatusOK, nil, tt.mockResponse)(req)
}))
assert.NilError(t, err)
du, err := client.DiskUsage(t.Context(), DiskUsageOptions{Images: true})
assert.NilError(t, err)
assert.Equal(t, du.Images.ActiveCount, tt.expectedActiveCount, tt.name)
assert.Equal(t, du.Images.TotalCount, tt.expectedTotalCount, tt.name)
assert.Equal(t, du.Images.Reclaimable, tt.expectedReclaimable, tt.name)
assert.Equal(t, du.Images.TotalSize, tt.expectedTotalSize, tt.name)
assert.Equal(t, len(du.Images.Items), len(tt.mockResponse.Images), tt.name)
})
}
}