API,daemon: support `type` URL parameter to /system/df

Let clients choose object types to compute disk usage of.

Signed-off-by: Roman Volosatovs <roman.volosatovs@docker.com>
Co-authored-by: Sebastiaan van Stijn <github@gone.nl>
This commit is contained in:
Roman Volosatovs 2021-06-23 15:26:54 +02:00
parent 12f1b3ce43
commit 47ad2f3dd6
No known key found for this signature in database
GPG Key ID: 216DD5F8CA6618A1
11 changed files with 446 additions and 65 deletions

View File

@ -10,12 +10,24 @@ import (
"github.com/docker/docker/api/types/swarm" "github.com/docker/docker/api/types/swarm"
) )
// DiskUsageOptions holds parameters for system disk usage query.
type DiskUsageOptions struct {
// Containers controls whether container disk usage should be computed.
Containers bool
// Images controls whether image disk usage should be computed.
Images bool
// Volumes controls whether volume disk usage should be computed.
Volumes bool
}
// Backend is the methods that need to be implemented to provide // Backend is the methods that need to be implemented to provide
// system specific functionality. // system specific functionality.
type Backend interface { type Backend interface {
SystemInfo() *types.Info SystemInfo() *types.Info
SystemVersion() types.Version SystemVersion() types.Version
SystemDiskUsage(ctx context.Context) (*types.DiskUsage, error) SystemDiskUsage(ctx context.Context, opts DiskUsageOptions) (*types.DiskUsage, error)
SubscribeToEvents(since, until time.Time, ef filters.Args) ([]events.Message, chan interface{}) SubscribeToEvents(since, until time.Time, ef filters.Args) ([]events.Message, chan interface{})
UnsubscribeFromEvents(chan interface{}) UnsubscribeFromEvents(chan interface{})
AuthenticateToRegistry(ctx context.Context, authConfig *types.AuthConfig) (string, string, error) AuthenticateToRegistry(ctx context.Context, authConfig *types.AuthConfig) (string, string, error)

View File

@ -16,7 +16,7 @@ import (
timetypes "github.com/docker/docker/api/types/time" timetypes "github.com/docker/docker/api/types/time"
"github.com/docker/docker/api/types/versions" "github.com/docker/docker/api/types/versions"
"github.com/docker/docker/pkg/ioutils" "github.com/docker/docker/pkg/ioutils"
pkgerrors "github.com/pkg/errors" "github.com/pkg/errors"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"golang.org/x/sync/errgroup" "golang.org/x/sync/errgroup"
) )
@ -90,44 +90,83 @@ func (s *systemRouter) getVersion(ctx context.Context, w http.ResponseWriter, r
} }
func (s *systemRouter) getDiskUsage(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { func (s *systemRouter) getDiskUsage(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
if err := httputils.ParseForm(r); err != nil {
return err
}
var getContainers, getImages, getVolumes, getBuildCache bool
if typeStrs, ok := r.Form["type"]; !ok {
getContainers, getImages, getVolumes, getBuildCache = true, true, true, true
} else {
for _, typ := range typeStrs {
switch types.DiskUsageObject(typ) {
case types.ContainerObject:
getContainers = true
case types.ImageObject:
getImages = true
case types.VolumeObject:
getVolumes = true
case types.BuildCacheObject:
getBuildCache = true
default:
return invalidRequestError{Err: fmt.Errorf("unknown object type: %s", typ)}
}
}
}
eg, ctx := errgroup.WithContext(ctx) eg, ctx := errgroup.WithContext(ctx)
var du *types.DiskUsage var systemDiskUsage *types.DiskUsage
eg.Go(func() error { if getContainers || getImages || getVolumes {
var err error eg.Go(func() error {
du, err = s.backend.SystemDiskUsage(ctx) var err error
return err systemDiskUsage, err = s.backend.SystemDiskUsage(ctx, DiskUsageOptions{
}) Containers: getContainers,
Images: getImages,
Volumes: getVolumes,
})
return err
})
}
var buildCache []*types.BuildCache var buildCache []*types.BuildCache
eg.Go(func() error { if getBuildCache {
var err error eg.Go(func() error {
buildCache, err = s.builder.DiskUsage(ctx) var err error
if err != nil { buildCache, err = s.builder.DiskUsage(ctx)
return pkgerrors.Wrap(err, "error getting build cache usage") if err != nil {
} return errors.Wrap(err, "error getting build cache usage")
return nil }
}) if buildCache == nil {
// Ensure empty `BuildCache` field is represented as empty JSON array(`[]`)
// instead of `null` to be consistent with `Images`, `Containers` etc.
buildCache = []*types.BuildCache{}
}
return nil
})
}
if err := eg.Wait(); err != nil { if err := eg.Wait(); err != nil {
return err return err
} }
var builderSize int64
if versions.LessThan(httputils.VersionFromContext(ctx), "1.42") { if versions.LessThan(httputils.VersionFromContext(ctx), "1.42") {
var builderSize int64
for _, b := range buildCache { for _, b := range buildCache {
builderSize += b.Size builderSize += b.Size
} }
du.BuilderSize = builderSize
} }
du.BuildCache = buildCache du := types.DiskUsage{
if buildCache == nil { BuildCache: buildCache,
// Ensure empty `BuildCache` field is represented as empty JSON array(`[]`) BuilderSize: builderSize,
// instead of `null` to be consistent with `Images`, `Containers` etc. }
du.BuildCache = []*types.BuildCache{} if systemDiskUsage != nil {
du.LayersSize = systemDiskUsage.LayersSize
du.Images = systemDiskUsage.Images
du.Containers = systemDiskUsage.Containers
du.Volumes = systemDiskUsage.Volumes
} }
return httputils.WriteJSON(w, http.StatusOK, du) return httputils.WriteJSON(w, http.StatusOK, du)
} }

View File

@ -8371,6 +8371,16 @@ paths:
description: "server error" description: "server error"
schema: schema:
$ref: "#/definitions/ErrorResponse" $ref: "#/definitions/ErrorResponse"
parameters:
- name: "type"
in: "query"
description: |
Object types, for which to compute and return data.
type: "array"
collectionFormat: multi
items:
type: "string"
enum: ["container", "image", "volume", "build-cache"]
tags: ["System"] tags: ["System"]
/images/{name}/get: /images/{name}/get:
get: get:

View File

@ -535,6 +535,27 @@ type ShimConfig struct {
Opts interface{} Opts interface{}
} }
// DiskUsageObject represents an object type used for disk usage query filtering.
type DiskUsageObject string
const (
// ContainerObject represents a container DiskUsageObject.
ContainerObject DiskUsageObject = "container"
// ImageObject represents an image DiskUsageObject.
ImageObject DiskUsageObject = "image"
// VolumeObject represents a volume DiskUsageObject.
VolumeObject DiskUsageObject = "volume"
// BuildCacheObject represents a build-cache DiskUsageObject.
BuildCacheObject DiskUsageObject = "build-cache"
)
// DiskUsageOptions holds parameters for system disk usage query.
type DiskUsageOptions struct {
// Types specifies what object types to include in the response. If empty,
// all object types are returned.
Types []DiskUsageObject
}
// DiskUsage contains response of Engine API: // DiskUsage contains response of Engine API:
// GET "/system/df" // GET "/system/df"
type DiskUsage struct { type DiskUsage struct {

View File

@ -4,23 +4,30 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"net/url"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
) )
// DiskUsage requests the current data usage from the daemon // DiskUsage requests the current data usage from the daemon
func (cli *Client) DiskUsage(ctx context.Context) (types.DiskUsage, error) { func (cli *Client) DiskUsage(ctx context.Context, options types.DiskUsageOptions) (types.DiskUsage, error) {
var du types.DiskUsage var query url.Values
if len(options.Types) > 0 {
query = url.Values{}
for _, t := range options.Types {
query.Add("type", string(t))
}
}
serverResp, err := cli.get(ctx, "/system/df", nil, nil) serverResp, err := cli.get(ctx, "/system/df", query, nil)
defer ensureReaderClosed(serverResp) defer ensureReaderClosed(serverResp)
if err != nil { if err != nil {
return du, err return types.DiskUsage{}, err
} }
var du types.DiskUsage
if err := json.NewDecoder(serverResp.body).Decode(&du); err != nil { if err := json.NewDecoder(serverResp.body).Decode(&du); err != nil {
return du, fmt.Errorf("Error retrieving disk usage: %v", err) return types.DiskUsage{}, fmt.Errorf("Error retrieving disk usage: %v", err)
} }
return du, nil return du, nil
} }

View File

@ -18,7 +18,7 @@ func TestDiskUsageError(t *testing.T) {
client := &Client{ client := &Client{
client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")),
} }
_, err := client.DiskUsage(context.Background()) _, err := client.DiskUsage(context.Background(), types.DiskUsageOptions{})
if !errdefs.IsSystem(err) { if !errdefs.IsSystem(err) {
t.Fatalf("expected a Server Error, got %[1]T: %[1]v", err) t.Fatalf("expected a Server Error, got %[1]T: %[1]v", err)
} }
@ -50,7 +50,7 @@ func TestDiskUsage(t *testing.T) {
}, nil }, nil
}), }),
} }
if _, err := client.DiskUsage(context.Background()); err != nil { if _, err := client.DiskUsage(context.Background(), types.DiskUsageOptions{}); err != nil {
t.Fatal(err) t.Fatal(err)
} }
} }

View File

@ -168,7 +168,7 @@ type SystemAPIClient interface {
Events(ctx context.Context, options types.EventsOptions) (<-chan events.Message, <-chan error) Events(ctx context.Context, options types.EventsOptions) (<-chan events.Message, <-chan error)
Info(ctx context.Context) (types.Info, error) Info(ctx context.Context) (types.Info, error)
RegistryLogin(ctx context.Context, auth types.AuthConfig) (registry.AuthenticateOKBody, error) RegistryLogin(ctx context.Context, auth types.AuthConfig) (registry.AuthenticateOKBody, error)
DiskUsage(ctx context.Context) (types.DiskUsage, error) DiskUsage(ctx context.Context, options types.DiskUsageOptions) (types.DiskUsage, error)
Ping(ctx context.Context) (types.Ping, error) Ping(ctx context.Context) (types.Ping, error)
} }

View File

@ -5,50 +5,64 @@ import (
"fmt" "fmt"
"sync/atomic" "sync/atomic"
"github.com/docker/docker/api/server/router/system"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/filters"
) )
// SystemDiskUsage returns information about the daemon data disk usage // SystemDiskUsage returns information about the daemon data disk usage
func (daemon *Daemon) SystemDiskUsage(ctx context.Context) (*types.DiskUsage, error) { func (daemon *Daemon) SystemDiskUsage(ctx context.Context, opts system.DiskUsageOptions) (*types.DiskUsage, error) {
if !atomic.CompareAndSwapInt32(&daemon.diskUsageRunning, 0, 1) { if !atomic.CompareAndSwapInt32(&daemon.diskUsageRunning, 0, 1) {
return nil, fmt.Errorf("a disk usage operation is already running") return nil, fmt.Errorf("a disk usage operation is already running")
} }
defer atomic.StoreInt32(&daemon.diskUsageRunning, 0) defer atomic.StoreInt32(&daemon.diskUsageRunning, 0)
// Retrieve container list var err error
allContainers, err := daemon.Containers(&types.ContainerListOptions{
Size: true, var containers []*types.Container
All: true, if opts.Containers {
}) // Retrieve container list
if err != nil { containers, err = daemon.Containers(&types.ContainerListOptions{
return nil, fmt.Errorf("failed to retrieve container list: %v", err) Size: true,
All: true,
})
if err != nil {
return nil, fmt.Errorf("failed to retrieve container list: %v", err)
}
} }
// Get all top images with extra attributes var (
allImages, err := daemon.imageService.Images(ctx, types.ImageListOptions{ images []*types.ImageSummary
Filters: filters.NewArgs(), layersSize int64
SharedSize: true, )
ContainerCount: true, if opts.Images {
}) // Get all top images with extra attributes
if err != nil { images, err = daemon.imageService.Images(ctx, types.ImageListOptions{
return nil, fmt.Errorf("failed to retrieve image list: %v", err) Filters: filters.NewArgs(),
SharedSize: true,
ContainerCount: true,
})
if err != nil {
return nil, fmt.Errorf("failed to retrieve image list: %v", err)
}
layersSize, err = daemon.imageService.LayerDiskUsage(ctx)
if err != nil {
return nil, err
}
} }
localVolumes, err := daemon.volumes.LocalVolumesSize(ctx) var volumes []*types.Volume
if err != nil { if opts.Volumes {
return nil, err volumes, err = daemon.volumes.LocalVolumesSize(ctx)
if err != nil {
return nil, err
}
} }
allLayersSize, err := daemon.imageService.LayerDiskUsage(ctx)
if err != nil {
return nil, err
}
return &types.DiskUsage{ return &types.DiskUsage{
LayersSize: allLayersSize, LayersSize: layersSize,
Containers: allContainers, Containers: containers,
Volumes: localVolumes, Volumes: volumes,
Images: allImages, Images: images,
}, nil }, nil
} }

View File

@ -24,6 +24,10 @@ keywords: "API, Docker, rcli, REST, documentation"
* `GET /images/json` now accepts query parameter `shared-size`. When set `true`, * `GET /images/json` now accepts query parameter `shared-size`. When set `true`,
images returned will include `SharedSize`, which provides the size on disk shared images returned will include `SharedSize`, which provides the size on disk shared
with other images present on the system. with other images present on the system.
* `GET /system/df` now accepts query parameter `type`. When set,
computes and returns data only for the specified object type.
The parameter can be specified multiple times to select several object types.
Supported values are: `container`, `image`, `volume`, `build-cache`.
## v1.41 API changes ## v1.41 API changes

View File

@ -53,14 +53,14 @@ func TestBuildWithSession(t *testing.T) {
assert.Check(t, is.Equal(strings.Count(out, "Using cache"), 2)) assert.Check(t, is.Equal(strings.Count(out, "Using cache"), 2))
assert.Check(t, is.Contains(out, "contentcontent")) assert.Check(t, is.Contains(out, "contentcontent"))
du, err := client.DiskUsage(context.TODO()) du, err := client.DiskUsage(context.TODO(), types.DiskUsageOptions{})
assert.Check(t, err) assert.Check(t, err)
assert.Check(t, du.BuilderSize > 10) assert.Check(t, du.BuilderSize > 10)
out = testBuildWithSession(t, client, client.DaemonHost(), fctx.Dir, dockerfile) out = testBuildWithSession(t, client, client.DaemonHost(), fctx.Dir, dockerfile)
assert.Check(t, is.Equal(strings.Count(out, "Using cache"), 4)) assert.Check(t, is.Equal(strings.Count(out, "Using cache"), 4))
du2, err := client.DiskUsage(context.TODO()) du2, err := client.DiskUsage(context.TODO(), types.DiskUsageOptions{})
assert.Check(t, err) assert.Check(t, err)
assert.Check(t, is.Equal(du.BuilderSize, du2.BuilderSize)) assert.Check(t, is.Equal(du.BuilderSize, du2.BuilderSize))
@ -84,7 +84,7 @@ func TestBuildWithSession(t *testing.T) {
_, err = client.BuildCachePrune(context.TODO(), types.BuildCachePruneOptions{All: true}) _, err = client.BuildCachePrune(context.TODO(), types.BuildCachePruneOptions{All: true})
assert.Check(t, err) assert.Check(t, err)
du, err = client.DiskUsage(context.TODO()) du, err = client.DiskUsage(context.TODO(), types.DiskUsageOptions{})
assert.Check(t, err) assert.Check(t, err)
assert.Check(t, is.Equal(du.BuilderSize, int64(0))) assert.Check(t, is.Equal(du.BuilderSize, int64(0)))
} }

View File

@ -0,0 +1,274 @@
package system // import "github.com/docker/docker/integration/system"
import (
"context"
"testing"
"github.com/docker/docker/api/types"
"github.com/docker/docker/integration/internal/container"
"github.com/docker/docker/testutil/daemon"
"gotest.tools/v3/assert"
"gotest.tools/v3/skip"
)
func TestDiskUsage(t *testing.T) {
skip.If(t, testEnv.OSType == "windows") // d.Start fails on Windows with `protocol not available`
t.Parallel()
d := daemon.New(t)
defer d.Cleanup(t)
d.Start(t, "--iptables=false")
defer d.Stop(t)
client := d.NewClientT(t)
ctx := context.Background()
var stepDU types.DiskUsage
for _, step := range []struct {
doc string
next func(t *testing.T, prev types.DiskUsage) types.DiskUsage
}{
{
doc: "empty",
next: func(t *testing.T, _ types.DiskUsage) types.DiskUsage {
du, err := client.DiskUsage(ctx, types.DiskUsageOptions{})
assert.NilError(t, err)
assert.DeepEqual(t, du, types.DiskUsage{
Images: []*types.ImageSummary{},
Containers: []*types.Container{},
Volumes: []*types.Volume{},
BuildCache: []*types.BuildCache{},
})
return du
},
},
{
doc: "after LoadBusybox",
next: func(t *testing.T, _ types.DiskUsage) types.DiskUsage {
d.LoadBusybox(t)
du, err := client.DiskUsage(ctx, types.DiskUsageOptions{})
assert.NilError(t, err)
assert.Assert(t, du.LayersSize > 0)
assert.Equal(t, len(du.Images), 1)
assert.DeepEqual(t, du, types.DiskUsage{
LayersSize: du.LayersSize,
Images: []*types.ImageSummary{
{
Created: du.Images[0].Created,
ID: du.Images[0].ID,
RepoTags: []string{"busybox:latest"},
Size: du.LayersSize,
VirtualSize: du.LayersSize,
},
},
Containers: []*types.Container{},
Volumes: []*types.Volume{},
BuildCache: []*types.BuildCache{},
})
return du
},
},
{
doc: "after container.Run",
next: func(t *testing.T, prev types.DiskUsage) types.DiskUsage {
cID := container.Run(ctx, t, client)
du, err := client.DiskUsage(ctx, types.DiskUsageOptions{})
assert.NilError(t, err)
assert.Equal(t, len(du.Containers), 1)
assert.Equal(t, len(du.Containers[0].Names), 1)
assert.Assert(t, du.Containers[0].Created >= prev.Images[0].Created)
assert.DeepEqual(t, du, types.DiskUsage{
LayersSize: prev.LayersSize,
Images: []*types.ImageSummary{
func() *types.ImageSummary {
sum := *prev.Images[0]
sum.Containers++
return &sum
}(),
},
Containers: []*types.Container{
{
ID: cID,
Names: du.Containers[0].Names,
Image: "busybox",
ImageID: prev.Images[0].ID,
Command: du.Containers[0].Command, // not relevant for the test
Created: du.Containers[0].Created,
Ports: du.Containers[0].Ports, // not relevant for the test
SizeRootFs: prev.Images[0].Size,
Labels: du.Containers[0].Labels, // not relevant for the test
State: du.Containers[0].State, // not relevant for the test
Status: du.Containers[0].Status, // not relevant for the test
HostConfig: du.Containers[0].HostConfig, // not relevant for the test
NetworkSettings: du.Containers[0].NetworkSettings, // not relevant for the test
Mounts: du.Containers[0].Mounts, // not relevant for the test
},
},
Volumes: []*types.Volume{},
BuildCache: []*types.BuildCache{},
})
return du
},
},
} {
t.Run(step.doc, func(t *testing.T) {
stepDU = step.next(t, stepDU)
for _, tc := range []struct {
doc string
options types.DiskUsageOptions
expected types.DiskUsage
}{
{
doc: "container types",
options: types.DiskUsageOptions{
Types: []types.DiskUsageObject{
types.ContainerObject,
},
},
expected: types.DiskUsage{
Containers: stepDU.Containers,
},
},
{
doc: "image types",
options: types.DiskUsageOptions{
Types: []types.DiskUsageObject{
types.ImageObject,
},
},
expected: types.DiskUsage{
LayersSize: stepDU.LayersSize,
Images: stepDU.Images,
},
},
{
doc: "volume types",
options: types.DiskUsageOptions{
Types: []types.DiskUsageObject{
types.VolumeObject,
},
},
expected: types.DiskUsage{
Volumes: stepDU.Volumes,
},
},
{
doc: "build-cache types",
options: types.DiskUsageOptions{
Types: []types.DiskUsageObject{
types.BuildCacheObject,
},
},
expected: types.DiskUsage{
BuildCache: stepDU.BuildCache,
},
},
{
doc: "container, volume types",
options: types.DiskUsageOptions{
Types: []types.DiskUsageObject{
types.ContainerObject,
types.VolumeObject,
},
},
expected: types.DiskUsage{
Containers: stepDU.Containers,
Volumes: stepDU.Volumes,
},
},
{
doc: "image, build-cache types",
options: types.DiskUsageOptions{
Types: []types.DiskUsageObject{
types.ImageObject,
types.BuildCacheObject,
},
},
expected: types.DiskUsage{
LayersSize: stepDU.LayersSize,
Images: stepDU.Images,
BuildCache: stepDU.BuildCache,
},
},
{
doc: "container, volume, build-cache types",
options: types.DiskUsageOptions{
Types: []types.DiskUsageObject{
types.ContainerObject,
types.VolumeObject,
types.BuildCacheObject,
},
},
expected: types.DiskUsage{
Containers: stepDU.Containers,
Volumes: stepDU.Volumes,
BuildCache: stepDU.BuildCache,
},
},
{
doc: "image, volume, build-cache types",
options: types.DiskUsageOptions{
Types: []types.DiskUsageObject{
types.ImageObject,
types.VolumeObject,
types.BuildCacheObject,
},
},
expected: types.DiskUsage{
LayersSize: stepDU.LayersSize,
Images: stepDU.Images,
Volumes: stepDU.Volumes,
BuildCache: stepDU.BuildCache,
},
},
{
doc: "container, image, volume types",
options: types.DiskUsageOptions{
Types: []types.DiskUsageObject{
types.ContainerObject,
types.ImageObject,
types.VolumeObject,
},
},
expected: types.DiskUsage{
LayersSize: stepDU.LayersSize,
Containers: stepDU.Containers,
Images: stepDU.Images,
Volumes: stepDU.Volumes,
},
},
{
doc: "container, image, volume, build-cache types",
options: types.DiskUsageOptions{
Types: []types.DiskUsageObject{
types.ContainerObject,
types.ImageObject,
types.VolumeObject,
types.BuildCacheObject,
},
},
expected: types.DiskUsage{
LayersSize: stepDU.LayersSize,
Containers: stepDU.Containers,
Images: stepDU.Images,
Volumes: stepDU.Volumes,
BuildCache: stepDU.BuildCache,
},
},
} {
tc := tc
t.Run(tc.doc, func(t *testing.T) {
// TODO: Run in parallel once https://github.com/moby/moby/pull/42560 is merged.
du, err := client.DiskUsage(ctx, tc.options)
assert.NilError(t, err)
assert.DeepEqual(t, du, tc.expected)
})
}
})
}
}