mirror of
https://github.com/moby/moby.git
synced 2022-11-09 12:21:53 -05:00
builder: add prune options to the API
Signed-off-by: Tibor Vass <tibor@docker.com>
This commit is contained in:
parent
a005332346
commit
8ff7847d1c
9 changed files with 190 additions and 27 deletions
|
@ -88,7 +88,7 @@ func (b *Backend) Build(ctx context.Context, config backend.BuildConfig) (string
|
||||||
}
|
}
|
||||||
|
|
||||||
// PruneCache removes all cached build sources
|
// PruneCache removes all cached build sources
|
||||||
func (b *Backend) PruneCache(ctx context.Context) (*types.BuildCachePruneReport, error) {
|
func (b *Backend) PruneCache(ctx context.Context, opts types.BuildCachePruneOptions) (*types.BuildCachePruneReport, error) {
|
||||||
eg, ctx := errgroup.WithContext(ctx)
|
eg, ctx := errgroup.WithContext(ctx)
|
||||||
|
|
||||||
var fsCacheSize uint64
|
var fsCacheSize uint64
|
||||||
|
@ -102,9 +102,10 @@ func (b *Backend) PruneCache(ctx context.Context) (*types.BuildCachePruneReport,
|
||||||
})
|
})
|
||||||
|
|
||||||
var buildCacheSize int64
|
var buildCacheSize int64
|
||||||
|
var cacheIDs []string
|
||||||
eg.Go(func() error {
|
eg.Go(func() error {
|
||||||
var err error
|
var err error
|
||||||
buildCacheSize, err = b.buildkit.Prune(ctx)
|
buildCacheSize, cacheIDs, err = b.buildkit.Prune(ctx, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "failed to prune build cache")
|
return errors.Wrap(err, "failed to prune build cache")
|
||||||
}
|
}
|
||||||
|
@ -115,7 +116,7 @@ func (b *Backend) PruneCache(ctx context.Context) (*types.BuildCachePruneReport,
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return &types.BuildCachePruneReport{SpaceReclaimed: fsCacheSize + uint64(buildCacheSize)}, nil
|
return &types.BuildCachePruneReport{SpaceReclaimed: fsCacheSize + uint64(buildCacheSize), CachesDeleted: cacheIDs}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cancel cancels the build by ID
|
// Cancel cancels the build by ID
|
||||||
|
|
|
@ -14,7 +14,7 @@ type Backend interface {
|
||||||
Build(context.Context, backend.BuildConfig) (string, error)
|
Build(context.Context, backend.BuildConfig) (string, error)
|
||||||
|
|
||||||
// Prune build cache
|
// Prune build cache
|
||||||
PruneCache(context.Context) (*types.BuildCachePruneReport, error)
|
PruneCache(context.Context, types.BuildCachePruneOptions) (*types.BuildCachePruneReport, error)
|
||||||
|
|
||||||
Cancel(context.Context, string) error
|
Cancel(context.Context, string) error
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@ import (
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types"
|
||||||
"github.com/docker/docker/api/types/backend"
|
"github.com/docker/docker/api/types/backend"
|
||||||
"github.com/docker/docker/api/types/container"
|
"github.com/docker/docker/api/types/container"
|
||||||
|
"github.com/docker/docker/api/types/filters"
|
||||||
"github.com/docker/docker/api/types/versions"
|
"github.com/docker/docker/api/types/versions"
|
||||||
"github.com/docker/docker/errdefs"
|
"github.com/docker/docker/errdefs"
|
||||||
"github.com/docker/docker/pkg/ioutils"
|
"github.com/docker/docker/pkg/ioutils"
|
||||||
|
@ -161,7 +162,26 @@ func parseVersion(s string) (types.BuilderVersion, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (br *buildRouter) postPrune(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
func (br *buildRouter) postPrune(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
||||||
report, err := br.backend.PruneCache(ctx)
|
if err := httputils.ParseForm(r); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
filters, err := filters.FromJSON(r.Form.Get("filters"))
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "could not parse filters")
|
||||||
|
}
|
||||||
|
ksfv := r.FormValue("keep-storage")
|
||||||
|
ks, err := strconv.Atoi(ksfv)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, "keep-storage is in bytes and expects an integer, got %v", ksfv)
|
||||||
|
}
|
||||||
|
|
||||||
|
opts := types.BuildCachePruneOptions{
|
||||||
|
All: httputils.BoolValue(r, "all"),
|
||||||
|
Filters: filters,
|
||||||
|
KeepStorage: int64(ks),
|
||||||
|
}
|
||||||
|
|
||||||
|
report, err := br.backend.PruneCache(ctx, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -1513,6 +1513,31 @@ definitions:
|
||||||
aux:
|
aux:
|
||||||
$ref: "#/definitions/ImageID"
|
$ref: "#/definitions/ImageID"
|
||||||
|
|
||||||
|
BuildCache:
|
||||||
|
type: "object"
|
||||||
|
properties:
|
||||||
|
ID:
|
||||||
|
type: "string"
|
||||||
|
Parent:
|
||||||
|
type: "string"
|
||||||
|
Type:
|
||||||
|
type: "string"
|
||||||
|
Description:
|
||||||
|
type: "string"
|
||||||
|
InUse:
|
||||||
|
type: "boolean"
|
||||||
|
Shared:
|
||||||
|
type: "boolean"
|
||||||
|
Size:
|
||||||
|
type: "integer"
|
||||||
|
CreatedAt:
|
||||||
|
type: "integer"
|
||||||
|
LastUsedAt:
|
||||||
|
type: "integer"
|
||||||
|
x-nullable: true
|
||||||
|
UsageCount:
|
||||||
|
type: "integer"
|
||||||
|
|
||||||
ImageID:
|
ImageID:
|
||||||
type: "object"
|
type: "object"
|
||||||
description: "Image ID or Digest"
|
description: "Image ID or Digest"
|
||||||
|
@ -6358,6 +6383,29 @@ paths:
|
||||||
produces:
|
produces:
|
||||||
- "application/json"
|
- "application/json"
|
||||||
operationId: "BuildPrune"
|
operationId: "BuildPrune"
|
||||||
|
parameters:
|
||||||
|
- name: "keep-storage"
|
||||||
|
in: "query"
|
||||||
|
description: "Amount of disk space in bytes to keep for cache"
|
||||||
|
type: "integer"
|
||||||
|
format: "int64"
|
||||||
|
- name: "all"
|
||||||
|
in: "query"
|
||||||
|
type: "boolean"
|
||||||
|
description: "Remove all types of build cache"
|
||||||
|
- name: "filters"
|
||||||
|
in: "query"
|
||||||
|
type: "string"
|
||||||
|
description: |
|
||||||
|
A JSON encoded value of the filters (a `map[string][]string`) to process on the list of build cache objects. Available filters:
|
||||||
|
- `unused-for=<duration>`: duration relative to daemon's time, during which build cache was not used, in Go's duration format (e.g., '24h')
|
||||||
|
- `id=<id>`
|
||||||
|
- `parent=<id>`
|
||||||
|
- `type=<string>`
|
||||||
|
- `description=<string>`
|
||||||
|
- `inuse`
|
||||||
|
- `shared`
|
||||||
|
- `private`
|
||||||
responses:
|
responses:
|
||||||
200:
|
200:
|
||||||
description: "No error"
|
description: "No error"
|
||||||
|
@ -6365,6 +6413,11 @@ paths:
|
||||||
type: "object"
|
type: "object"
|
||||||
title: "BuildPruneResponse"
|
title: "BuildPruneResponse"
|
||||||
properties:
|
properties:
|
||||||
|
CachesDeleted:
|
||||||
|
type: "array"
|
||||||
|
items:
|
||||||
|
description: "ID of build cache object"
|
||||||
|
type: "string"
|
||||||
SpaceReclaimed:
|
SpaceReclaimed:
|
||||||
description: "Disk space reclaimed in bytes"
|
description: "Disk space reclaimed in bytes"
|
||||||
type: "integer"
|
type: "integer"
|
||||||
|
@ -7199,6 +7252,10 @@ paths:
|
||||||
type: "array"
|
type: "array"
|
||||||
items:
|
items:
|
||||||
$ref: "#/definitions/Volume"
|
$ref: "#/definitions/Volume"
|
||||||
|
BuildCache:
|
||||||
|
type: "array"
|
||||||
|
items:
|
||||||
|
$ref: "#/definitions/BuildCache"
|
||||||
example:
|
example:
|
||||||
LayersSize: 1092588
|
LayersSize: 1092588
|
||||||
Images:
|
Images:
|
||||||
|
|
|
@ -543,6 +543,7 @@ type ImagesPruneReport struct {
|
||||||
// BuildCachePruneReport contains the response for Engine API:
|
// BuildCachePruneReport contains the response for Engine API:
|
||||||
// POST "/build/prune"
|
// POST "/build/prune"
|
||||||
type BuildCachePruneReport struct {
|
type BuildCachePruneReport struct {
|
||||||
|
CachesDeleted []string
|
||||||
SpaceReclaimed uint64
|
SpaceReclaimed uint64
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -592,14 +593,21 @@ type BuildResult struct {
|
||||||
|
|
||||||
// BuildCache contains information about a build cache record
|
// BuildCache contains information about a build cache record
|
||||||
type BuildCache struct {
|
type BuildCache struct {
|
||||||
ID string
|
ID string
|
||||||
Mutable bool
|
Parent string
|
||||||
InUse bool
|
Type string
|
||||||
Size int64
|
Description string
|
||||||
|
InUse bool
|
||||||
|
Shared bool
|
||||||
|
Size int64
|
||||||
CreatedAt time.Time
|
CreatedAt time.Time
|
||||||
LastUsedAt *time.Time
|
LastUsedAt *time.Time
|
||||||
UsageCount int
|
UsageCount int
|
||||||
Parent string
|
}
|
||||||
Description string
|
|
||||||
|
// BuildCachePruneOptions hold parameters to prune the build cache
|
||||||
|
type BuildCachePruneOptions struct {
|
||||||
|
All bool
|
||||||
|
KeepStorage int64
|
||||||
|
Filters filters.Args
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,6 +29,21 @@ import (
|
||||||
grpcmetadata "google.golang.org/grpc/metadata"
|
grpcmetadata "google.golang.org/grpc/metadata"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var errMultipleFilterValues = errors.New("filters expect only one value")
|
||||||
|
|
||||||
|
var cacheFields = map[string]bool{
|
||||||
|
"id": true,
|
||||||
|
"parent": true,
|
||||||
|
"type": true,
|
||||||
|
"description": true,
|
||||||
|
"inuse": true,
|
||||||
|
"shared": true,
|
||||||
|
"private": true,
|
||||||
|
// fields from buildkit that are not exposed
|
||||||
|
"mutable": false,
|
||||||
|
"immutable": false,
|
||||||
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
llbsolver.AllowNetworkHostUnstable = true
|
llbsolver.AllowNetworkHostUnstable = true
|
||||||
}
|
}
|
||||||
|
@ -87,48 +102,94 @@ func (b *Builder) DiskUsage(ctx context.Context) ([]*types.BuildCache, error) {
|
||||||
var items []*types.BuildCache
|
var items []*types.BuildCache
|
||||||
for _, r := range duResp.Record {
|
for _, r := range duResp.Record {
|
||||||
items = append(items, &types.BuildCache{
|
items = append(items, &types.BuildCache{
|
||||||
ID: r.ID,
|
ID: r.ID,
|
||||||
Mutable: r.Mutable,
|
Parent: r.Parent,
|
||||||
InUse: r.InUse,
|
Type: r.RecordType,
|
||||||
Size: r.Size_,
|
Description: r.Description,
|
||||||
|
InUse: r.InUse,
|
||||||
|
Shared: r.Shared,
|
||||||
|
Size: r.Size_,
|
||||||
CreatedAt: r.CreatedAt,
|
CreatedAt: r.CreatedAt,
|
||||||
LastUsedAt: r.LastUsedAt,
|
LastUsedAt: r.LastUsedAt,
|
||||||
UsageCount: int(r.UsageCount),
|
UsageCount: int(r.UsageCount),
|
||||||
Parent: r.Parent,
|
|
||||||
Description: r.Description,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return items, nil
|
return items, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prune clears all reclaimable build cache
|
// Prune clears all reclaimable build cache
|
||||||
func (b *Builder) Prune(ctx context.Context) (int64, error) {
|
func (b *Builder) Prune(ctx context.Context, opts types.BuildCachePruneOptions) (int64, []string, error) {
|
||||||
ch := make(chan *controlapi.UsageRecord)
|
ch := make(chan *controlapi.UsageRecord)
|
||||||
|
|
||||||
eg, ctx := errgroup.WithContext(ctx)
|
eg, ctx := errgroup.WithContext(ctx)
|
||||||
|
|
||||||
|
validFilters := make(map[string]bool, 1+len(cacheFields))
|
||||||
|
validFilters["unused-for"] = true
|
||||||
|
for k, v := range cacheFields {
|
||||||
|
validFilters[k] = v
|
||||||
|
}
|
||||||
|
if err := opts.Filters.Validate(validFilters); err != nil {
|
||||||
|
return 0, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var unusedFor time.Duration
|
||||||
|
unusedForValues := opts.Filters.Get("unused-for")
|
||||||
|
|
||||||
|
switch len(unusedForValues) {
|
||||||
|
case 0:
|
||||||
|
|
||||||
|
case 1:
|
||||||
|
var err error
|
||||||
|
unusedFor, err = time.ParseDuration(unusedForValues[0])
|
||||||
|
if err != nil {
|
||||||
|
return 0, nil, errors.Wrap(err, "unused-for filter expects a duration (e.g., '24h')")
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return 0, nil, errMultipleFilterValues
|
||||||
|
}
|
||||||
|
|
||||||
|
bkFilter := make([]string, 0, opts.Filters.Len())
|
||||||
|
for cacheField := range cacheFields {
|
||||||
|
values := opts.Filters.Get(cacheField)
|
||||||
|
switch len(values) {
|
||||||
|
case 0:
|
||||||
|
bkFilter = append(bkFilter, cacheField)
|
||||||
|
case 1:
|
||||||
|
bkFilter = append(bkFilter, cacheField+"=="+values[0])
|
||||||
|
default:
|
||||||
|
return 0, nil, errMultipleFilterValues
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
eg.Go(func() error {
|
eg.Go(func() error {
|
||||||
defer close(ch)
|
defer close(ch)
|
||||||
return b.controller.Prune(&controlapi.PruneRequest{}, &pruneProxy{
|
return b.controller.Prune(&controlapi.PruneRequest{
|
||||||
|
All: opts.All,
|
||||||
|
KeepDuration: int64(unusedFor),
|
||||||
|
KeepBytes: opts.KeepStorage,
|
||||||
|
Filter: bkFilter,
|
||||||
|
}, &pruneProxy{
|
||||||
streamProxy: streamProxy{ctx: ctx},
|
streamProxy: streamProxy{ctx: ctx},
|
||||||
ch: ch,
|
ch: ch,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
var size int64
|
var size int64
|
||||||
|
var cacheIDs []string
|
||||||
eg.Go(func() error {
|
eg.Go(func() error {
|
||||||
for r := range ch {
|
for r := range ch {
|
||||||
size += r.Size_
|
size += r.Size_
|
||||||
|
cacheIDs = append(cacheIDs, r.ID)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
if err := eg.Wait(); err != nil {
|
if err := eg.Wait(); err != nil {
|
||||||
return 0, err
|
return 0, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return size, nil
|
return size, cacheIDs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build executes a build request
|
// Build executes a build request
|
||||||
|
|
|
@ -4,19 +4,34 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types"
|
||||||
|
"github.com/docker/docker/api/types/filters"
|
||||||
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
// BuildCachePrune requests the daemon to delete unused cache data
|
// BuildCachePrune requests the daemon to delete unused cache data
|
||||||
func (cli *Client) BuildCachePrune(ctx context.Context) (*types.BuildCachePruneReport, error) {
|
func (cli *Client) BuildCachePrune(ctx context.Context, opts types.BuildCachePruneOptions) (*types.BuildCachePruneReport, error) {
|
||||||
if err := cli.NewVersionError("1.31", "build prune"); err != nil {
|
if err := cli.NewVersionError("1.31", "build prune"); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
report := types.BuildCachePruneReport{}
|
report := types.BuildCachePruneReport{}
|
||||||
|
|
||||||
serverResp, err := cli.post(ctx, "/build/prune", nil, nil, nil)
|
query := url.Values{}
|
||||||
|
if opts.All {
|
||||||
|
query.Set("all", "1")
|
||||||
|
}
|
||||||
|
query.Set("keep-storage", fmt.Sprintf("%d", opts.KeepStorage))
|
||||||
|
filters, err := filters.ToJSON(opts.Filters)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "prune could not marshal filters option")
|
||||||
|
}
|
||||||
|
query.Set("filters", filters)
|
||||||
|
|
||||||
|
serverResp, err := cli.post(ctx, "/build/prune", query, nil, nil)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
|
@ -86,7 +86,7 @@ type DistributionAPIClient interface {
|
||||||
// ImageAPIClient defines API client methods for the images
|
// ImageAPIClient defines API client methods for the images
|
||||||
type ImageAPIClient interface {
|
type ImageAPIClient interface {
|
||||||
ImageBuild(ctx context.Context, context io.Reader, options types.ImageBuildOptions) (types.ImageBuildResponse, error)
|
ImageBuild(ctx context.Context, context io.Reader, options types.ImageBuildOptions) (types.ImageBuildResponse, error)
|
||||||
BuildCachePrune(ctx context.Context) (*types.BuildCachePruneReport, error)
|
BuildCachePrune(ctx context.Context, opts types.BuildCachePruneOptions) (*types.BuildCachePruneReport, error)
|
||||||
BuildCancel(ctx context.Context, id string) error
|
BuildCancel(ctx context.Context, id string) error
|
||||||
ImageCreate(ctx context.Context, parentReference string, options types.ImageCreateOptions) (io.ReadCloser, error)
|
ImageCreate(ctx context.Context, parentReference string, options types.ImageCreateOptions) (io.ReadCloser, error)
|
||||||
ImageHistory(ctx context.Context, image string) ([]image.HistoryResponseItem, error)
|
ImageHistory(ctx context.Context, image string) ([]image.HistoryResponseItem, error)
|
||||||
|
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/docker/docker/api/types"
|
||||||
dclient "github.com/docker/docker/client"
|
dclient "github.com/docker/docker/client"
|
||||||
"github.com/docker/docker/internal/test/fakecontext"
|
"github.com/docker/docker/internal/test/fakecontext"
|
||||||
"github.com/docker/docker/internal/test/request"
|
"github.com/docker/docker/internal/test/request"
|
||||||
|
@ -76,7 +77,7 @@ func TestBuildWithSession(t *testing.T) {
|
||||||
assert.Check(t, is.Contains(string(outBytes), "Successfully built"))
|
assert.Check(t, is.Contains(string(outBytes), "Successfully built"))
|
||||||
assert.Check(t, is.Equal(strings.Count(string(outBytes), "Using cache"), 4))
|
assert.Check(t, is.Equal(strings.Count(string(outBytes), "Using cache"), 4))
|
||||||
|
|
||||||
_, err = client.BuildCachePrune(context.TODO())
|
_, 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())
|
||||||
|
|
Loading…
Reference in a new issue