From 58738cdee327f5de481dcf7d3d377374cbb5f13a Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Wed, 7 Dec 2016 14:02:13 -0800 Subject: [PATCH] Add `--filter until=` for `docker container/image prune` This fix is a follow up for comment https://github.com/docker/docker/pull/28535#issuecomment-263215225 This fix provides `--filter until=` for `docker container/image prune`. This fix adds `--filter until=` to `docker container/image prune` so that it is possible to specify a timestamp and prune those containers/images that are earlier than the timestamp. Related docs has been updated Several integration tests have been added to cover changes. This fix fixes #28497. This fix is related to #28535. Signed-off-by: Yong Tang --- api/server/router/network/network_routes.go | 7 +- api/swagger.yaml | 5 +- cli/command/container/prune.go | 16 ++- cli/command/image/prune.go | 16 +-- cli/command/network/prune.go | 16 ++- cli/command/prune/prune.go | 15 +-- cli/command/system/prune.go | 21 ++-- client/container_prune_test.go | 111 ++++++++++++++++++ client/image_prune_test.go | 106 +++++++++++++++++ client/network_prune_test.go | 99 ++++++++++++++++ daemon/prune.go | 58 ++++++++- docs/reference/commandline/container_prune.md | 63 +++++++++- docs/reference/commandline/image_prune.md | 88 +++++++++++++- docs/reference/commandline/network_prune.md | 50 +++++++- docs/reference/commandline/system_prune.md | 28 ++++- integration-cli/docker_cli_prune_unix_test.go | 21 ++++ 16 files changed, 671 insertions(+), 49 deletions(-) create mode 100644 client/container_prune_test.go create mode 100644 client/image_prune_test.go create mode 100644 client/network_prune_test.go diff --git a/api/server/router/network/network_routes.go b/api/server/router/network/network_routes.go index 2390aac5d4..3e45c146c8 100644 --- a/api/server/router/network/network_routes.go +++ b/api/server/router/network/network_routes.go @@ -312,7 +312,12 @@ func (n *networkRouter) postNetworksPrune(ctx context.Context, w http.ResponseWr return err } - pruneReport, err := n.backend.NetworksPrune(filters.Args{}) + pruneFilters, err := filters.FromParam(r.Form.Get("filters")) + if err != nil { + return err + } + + pruneReport, err := n.backend.NetworksPrune(pruneFilters) if err != nil { return err } diff --git a/api/swagger.yaml b/api/swagger.yaml index 6f4e180638..c1c6338ee2 100644 --- a/api/swagger.yaml +++ b/api/swagger.yaml @@ -4191,6 +4191,7 @@ paths: Filters to process on the prune list, encoded as JSON (a `map[string][]string`). Available filters: + - `until=` Prune containers created before this timestamp. The `` can be Unix timestamps, date formatted timestamps, or Go duration strings (e.g. `10m`, `1h30m`) computed relative to the daemon machine’s time. type: "string" responses: 200: @@ -4866,6 +4867,7 @@ paths: - `dangling=` When set to `true` (or `1`), prune only unused *and* untagged images. When set to `false` (or `0`), all unused images are pruned. + - `until=` Prune images created before this timestamp. The `` can be Unix timestamps, date formatted timestamps, or Go duration strings (e.g. `10m`, `1h30m`) computed relative to the daemon machine’s time. type: "string" responses: 200: @@ -6295,8 +6297,6 @@ paths: /networks/prune: post: summary: "Delete unused networks" - consumes: - - "application/json" produces: - "application/json" operationId: "NetworkPrune" @@ -6307,6 +6307,7 @@ paths: Filters to process on the prune list, encoded as JSON (a `map[string][]string`). Available filters: + - `until=` Prune networks created before this timestamp. The `` can be Unix timestamps, date formatted timestamps, or Go duration strings (e.g. `10m`, `1h30m`) computed relative to the daemon machine’s time. type: "string" responses: 200: diff --git a/cli/command/container/prune.go b/cli/command/container/prune.go index 0aad66e6ee..ca50e2e159 100644 --- a/cli/command/container/prune.go +++ b/cli/command/container/prune.go @@ -3,21 +3,22 @@ package container import ( "fmt" - "github.com/docker/docker/api/types/filters" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" + "github.com/docker/docker/opts" units "github.com/docker/go-units" "github.com/spf13/cobra" "golang.org/x/net/context" ) type pruneOptions struct { - force bool + force bool + filter opts.FilterOpt } // NewPruneCommand returns a new cobra prune command for containers func NewPruneCommand(dockerCli *command.DockerCli) *cobra.Command { - var opts pruneOptions + opts := pruneOptions{filter: opts.NewFilterOpt()} cmd := &cobra.Command{ Use: "prune [OPTIONS]", @@ -39,6 +40,7 @@ func NewPruneCommand(dockerCli *command.DockerCli) *cobra.Command { flags := cmd.Flags() flags.BoolVarP(&opts.force, "force", "f", false, "Do not prompt for confirmation") + flags.Var(&opts.filter, "filter", "Provide filter values (e.g. 'until=')") return cmd } @@ -47,11 +49,13 @@ const warning = `WARNING! This will remove all stopped containers. Are you sure you want to continue?` func runPrune(dockerCli *command.DockerCli, opts pruneOptions) (spaceReclaimed uint64, output string, err error) { + pruneFilters := opts.filter.Value() + if !opts.force && !command.PromptForConfirmation(dockerCli.In(), dockerCli.Out(), warning) { return } - report, err := dockerCli.Client().ContainersPrune(context.Background(), filters.Args{}) + report, err := dockerCli.Client().ContainersPrune(context.Background(), pruneFilters) if err != nil { return } @@ -69,6 +73,6 @@ func runPrune(dockerCli *command.DockerCli, opts pruneOptions) (spaceReclaimed u // RunPrune calls the Container Prune API // This returns the amount of space reclaimed and a detailed output string -func RunPrune(dockerCli *command.DockerCli) (uint64, string, error) { - return runPrune(dockerCli, pruneOptions{force: true}) +func RunPrune(dockerCli *command.DockerCli, filter opts.FilterOpt) (uint64, string, error) { + return runPrune(dockerCli, pruneOptions{force: true, filter: filter}) } diff --git a/cli/command/image/prune.go b/cli/command/image/prune.go index 82c28fcf49..f17aed7410 100644 --- a/cli/command/image/prune.go +++ b/cli/command/image/prune.go @@ -5,21 +5,22 @@ import ( "golang.org/x/net/context" - "github.com/docker/docker/api/types/filters" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" + "github.com/docker/docker/opts" units "github.com/docker/go-units" "github.com/spf13/cobra" ) type pruneOptions struct { - force bool - all bool + force bool + all bool + filter opts.FilterOpt } // NewPruneCommand returns a new cobra prune command for images func NewPruneCommand(dockerCli *command.DockerCli) *cobra.Command { - var opts pruneOptions + opts := pruneOptions{filter: opts.NewFilterOpt()} cmd := &cobra.Command{ Use: "prune [OPTIONS]", @@ -42,6 +43,7 @@ func NewPruneCommand(dockerCli *command.DockerCli) *cobra.Command { flags := cmd.Flags() flags.BoolVarP(&opts.force, "force", "f", false, "Do not prompt for confirmation") flags.BoolVarP(&opts.all, "all", "a", false, "Remove all unused images, not just dangling ones") + flags.Var(&opts.filter, "filter", "Provide filter values (e.g. 'until=')") return cmd } @@ -54,7 +56,7 @@ Are you sure you want to continue?` ) func runPrune(dockerCli *command.DockerCli, opts pruneOptions) (spaceReclaimed uint64, output string, err error) { - pruneFilters := filters.NewArgs() + pruneFilters := opts.filter.Value() pruneFilters.Add("dangling", fmt.Sprintf("%v", !opts.all)) warning := danglingWarning @@ -87,6 +89,6 @@ func runPrune(dockerCli *command.DockerCli, opts pruneOptions) (spaceReclaimed u // RunPrune calls the Image Prune API // This returns the amount of space reclaimed and a detailed output string -func RunPrune(dockerCli *command.DockerCli, all bool) (uint64, string, error) { - return runPrune(dockerCli, pruneOptions{force: true, all: all}) +func RunPrune(dockerCli *command.DockerCli, all bool, filter opts.FilterOpt) (uint64, string, error) { + return runPrune(dockerCli, pruneOptions{force: true, all: all, filter: filter}) } diff --git a/cli/command/network/prune.go b/cli/command/network/prune.go index 9f1979e6b5..c5c5359926 100644 --- a/cli/command/network/prune.go +++ b/cli/command/network/prune.go @@ -5,19 +5,20 @@ import ( "golang.org/x/net/context" - "github.com/docker/docker/api/types/filters" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" + "github.com/docker/docker/opts" "github.com/spf13/cobra" ) type pruneOptions struct { - force bool + force bool + filter opts.FilterOpt } // NewPruneCommand returns a new cobra prune command for networks func NewPruneCommand(dockerCli *command.DockerCli) *cobra.Command { - var opts pruneOptions + opts := pruneOptions{filter: opts.NewFilterOpt()} cmd := &cobra.Command{ Use: "prune [OPTIONS]", @@ -38,6 +39,7 @@ func NewPruneCommand(dockerCli *command.DockerCli) *cobra.Command { flags := cmd.Flags() flags.BoolVarP(&opts.force, "force", "f", false, "Do not prompt for confirmation") + flags.Var(&opts.filter, "filter", "Provide filter values (e.g. 'until=')") return cmd } @@ -46,11 +48,13 @@ const warning = `WARNING! This will remove all networks not used by at least one Are you sure you want to continue?` func runPrune(dockerCli *command.DockerCli, opts pruneOptions) (output string, err error) { + pruneFilters := opts.filter.Value() + if !opts.force && !command.PromptForConfirmation(dockerCli.In(), dockerCli.Out(), warning) { return } - report, err := dockerCli.Client().NetworksPrune(context.Background(), filters.Args{}) + report, err := dockerCli.Client().NetworksPrune(context.Background(), pruneFilters) if err != nil { return } @@ -67,7 +71,7 @@ func runPrune(dockerCli *command.DockerCli, opts pruneOptions) (output string, e // RunPrune calls the Network Prune API // This returns the amount of space reclaimed and a detailed output string -func RunPrune(dockerCli *command.DockerCli) (uint64, string, error) { - output, err := runPrune(dockerCli, pruneOptions{force: true}) +func RunPrune(dockerCli *command.DockerCli, filter opts.FilterOpt) (uint64, string, error) { + output, err := runPrune(dockerCli, pruneOptions{force: true, filter: filter}) return 0, output, err } diff --git a/cli/command/prune/prune.go b/cli/command/prune/prune.go index a022487fd6..6314718c69 100644 --- a/cli/command/prune/prune.go +++ b/cli/command/prune/prune.go @@ -6,6 +6,7 @@ import ( "github.com/docker/docker/cli/command/image" "github.com/docker/docker/cli/command/network" "github.com/docker/docker/cli/command/volume" + "github.com/docker/docker/opts" "github.com/spf13/cobra" ) @@ -30,21 +31,21 @@ func NewNetworkPruneCommand(dockerCli *command.DockerCli) *cobra.Command { } // RunContainerPrune executes a prune command for containers -func RunContainerPrune(dockerCli *command.DockerCli) (uint64, string, error) { - return container.RunPrune(dockerCli) +func RunContainerPrune(dockerCli *command.DockerCli, filter opts.FilterOpt) (uint64, string, error) { + return container.RunPrune(dockerCli, filter) } // RunVolumePrune executes a prune command for volumes -func RunVolumePrune(dockerCli *command.DockerCli) (uint64, string, error) { +func RunVolumePrune(dockerCli *command.DockerCli, filter opts.FilterOpt) (uint64, string, error) { return volume.RunPrune(dockerCli) } // RunImagePrune executes a prune command for images -func RunImagePrune(dockerCli *command.DockerCli, all bool) (uint64, string, error) { - return image.RunPrune(dockerCli, all) +func RunImagePrune(dockerCli *command.DockerCli, all bool, filter opts.FilterOpt) (uint64, string, error) { + return image.RunPrune(dockerCli, all, filter) } // RunNetworkPrune executes a prune command for networks -func RunNetworkPrune(dockerCli *command.DockerCli) (uint64, string, error) { - return network.RunPrune(dockerCli) +func RunNetworkPrune(dockerCli *command.DockerCli, filter opts.FilterOpt) (uint64, string, error) { + return network.RunPrune(dockerCli, filter) } diff --git a/cli/command/system/prune.go b/cli/command/system/prune.go index 92dddbdca6..46e4316f4a 100644 --- a/cli/command/system/prune.go +++ b/cli/command/system/prune.go @@ -6,18 +6,20 @@ import ( "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" "github.com/docker/docker/cli/command/prune" + "github.com/docker/docker/opts" units "github.com/docker/go-units" "github.com/spf13/cobra" ) type pruneOptions struct { - force bool - all bool + force bool + all bool + filter opts.FilterOpt } // NewPruneCommand creates a new cobra.Command for `docker prune` func NewPruneCommand(dockerCli *command.DockerCli) *cobra.Command { - var opts pruneOptions + opts := pruneOptions{filter: opts.NewFilterOpt()} cmd := &cobra.Command{ Use: "prune [OPTIONS]", @@ -32,6 +34,7 @@ func NewPruneCommand(dockerCli *command.DockerCli) *cobra.Command { flags := cmd.Flags() flags.BoolVarP(&opts.force, "force", "f", false, "Do not prompt for confirmation") flags.BoolVarP(&opts.all, "all", "a", false, "Remove all unused images not just dangling ones") + flags.Var(&opts.filter, "filter", "Provide filter values (e.g. 'until=')") return cmd } @@ -48,27 +51,27 @@ Are you sure you want to continue?` allImageDesc = `- all images without at least one container associated to them` ) -func runPrune(dockerCli *command.DockerCli, opts pruneOptions) error { +func runPrune(dockerCli *command.DockerCli, options pruneOptions) error { var message string - if opts.all { + if options.all { message = fmt.Sprintf(warning, allImageDesc) } else { message = fmt.Sprintf(warning, danglingImageDesc) } - if !opts.force && !command.PromptForConfirmation(dockerCli.In(), dockerCli.Out(), message) { + if !options.force && !command.PromptForConfirmation(dockerCli.In(), dockerCli.Out(), message) { return nil } var spaceReclaimed uint64 - for _, pruneFn := range []func(dockerCli *command.DockerCli) (uint64, string, error){ + for _, pruneFn := range []func(dockerCli *command.DockerCli, filter opts.FilterOpt) (uint64, string, error){ prune.RunContainerPrune, prune.RunVolumePrune, prune.RunNetworkPrune, } { - spc, output, err := pruneFn(dockerCli) + spc, output, err := pruneFn(dockerCli, options.filter) if err != nil { return err } @@ -78,7 +81,7 @@ func runPrune(dockerCli *command.DockerCli, opts pruneOptions) error { } } - spc, output, err := prune.RunImagePrune(dockerCli, opts.all) + spc, output, err := prune.RunImagePrune(dockerCli, options.all, options.filter) if err != nil { return err } diff --git a/client/container_prune_test.go b/client/container_prune_test.go new file mode 100644 index 0000000000..5f06ea0664 --- /dev/null +++ b/client/container_prune_test.go @@ -0,0 +1,111 @@ +package client + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "strings" + "testing" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/filters" + "github.com/docker/docker/pkg/testutil/assert" + "golang.org/x/net/context" +) + +func TestContainersPruneError(t *testing.T) { + client := &Client{ + client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), + version: "1.25", + } + + filters := filters.NewArgs() + + _, err := client.ContainersPrune(context.Background(), filters) + assert.Error(t, err, "Error response from daemon: Server error") +} + +func TestContainersPrune(t *testing.T) { + expectedURL := "/v1.25/containers/prune" + + danglingFilters := filters.NewArgs() + danglingFilters.Add("dangling", "true") + + noDanglingFilters := filters.NewArgs() + noDanglingFilters.Add("dangling", "false") + + danglingUntilFilters := filters.NewArgs() + danglingUntilFilters.Add("dangling", "true") + danglingUntilFilters.Add("until", "2016-12-15T14:00") + + listCases := []struct { + filters filters.Args + expectedQueryParams map[string]string + }{ + { + filters: filters.Args{}, + expectedQueryParams: map[string]string{ + "until": "", + "filter": "", + "filters": "", + }, + }, + { + filters: danglingFilters, + expectedQueryParams: map[string]string{ + "until": "", + "filter": "", + "filters": `{"dangling":{"true":true}}`, + }, + }, + { + filters: danglingUntilFilters, + expectedQueryParams: map[string]string{ + "until": "", + "filter": "", + "filters": `{"dangling":{"true":true},"until":{"2016-12-15T14:00":true}}`, + }, + }, + { + filters: noDanglingFilters, + expectedQueryParams: map[string]string{ + "until": "", + "filter": "", + "filters": `{"dangling":{"false":true}}`, + }, + }, + } + for _, listCase := range listCases { + client := &Client{ + client: newMockClient(func(req *http.Request) (*http.Response, error) { + if !strings.HasPrefix(req.URL.Path, expectedURL) { + return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) + } + query := req.URL.Query() + for key, expected := range listCase.expectedQueryParams { + actual := query.Get(key) + assert.Equal(t, actual, expected) + } + content, err := json.Marshal(types.ContainersPruneReport{ + ContainersDeleted: []string{"container_id1", "container_id2"}, + SpaceReclaimed: 9999, + }) + if err != nil { + return nil, err + } + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader(content)), + }, nil + }), + version: "1.25", + } + + report, err := client.ContainersPrune(context.Background(), listCase.filters) + assert.NilError(t, err) + assert.Equal(t, len(report.ContainersDeleted), 2) + assert.Equal(t, report.SpaceReclaimed, uint64(9999)) + } +} diff --git a/client/image_prune_test.go b/client/image_prune_test.go new file mode 100644 index 0000000000..61cf18ef35 --- /dev/null +++ b/client/image_prune_test.go @@ -0,0 +1,106 @@ +package client + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "strings" + "testing" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/filters" + "github.com/docker/docker/pkg/testutil/assert" + "golang.org/x/net/context" +) + +func TestImagesPruneError(t *testing.T) { + client := &Client{ + client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), + version: "1.25", + } + + filters := filters.NewArgs() + + _, err := client.ImagesPrune(context.Background(), filters) + assert.Error(t, err, "Error response from daemon: Server error") +} + +func TestImagesPrune(t *testing.T) { + expectedURL := "/v1.25/images/prune" + + danglingFilters := filters.NewArgs() + danglingFilters.Add("dangling", "true") + + noDanglingFilters := filters.NewArgs() + noDanglingFilters.Add("dangling", "false") + + listCases := []struct { + filters filters.Args + expectedQueryParams map[string]string + }{ + { + filters: filters.Args{}, + expectedQueryParams: map[string]string{ + "until": "", + "filter": "", + "filters": "", + }, + }, + { + filters: danglingFilters, + expectedQueryParams: map[string]string{ + "until": "", + "filter": "", + "filters": `{"dangling":{"true":true}}`, + }, + }, + { + filters: noDanglingFilters, + expectedQueryParams: map[string]string{ + "until": "", + "filter": "", + "filters": `{"dangling":{"false":true}}`, + }, + }, + } + for _, listCase := range listCases { + client := &Client{ + client: newMockClient(func(req *http.Request) (*http.Response, error) { + if !strings.HasPrefix(req.URL.Path, expectedURL) { + return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) + } + query := req.URL.Query() + for key, expected := range listCase.expectedQueryParams { + actual := query.Get(key) + assert.Equal(t, actual, expected) + } + content, err := json.Marshal(types.ImagesPruneReport{ + ImagesDeleted: []types.ImageDelete{ + { + Deleted: "image_id1", + }, + { + Deleted: "image_id2", + }, + }, + SpaceReclaimed: 9999, + }) + if err != nil { + return nil, err + } + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader(content)), + }, nil + }), + version: "1.25", + } + + report, err := client.ImagesPrune(context.Background(), listCase.filters) + assert.NilError(t, err) + assert.Equal(t, len(report.ImagesDeleted), 2) + assert.Equal(t, report.SpaceReclaimed, uint64(9999)) + } +} diff --git a/client/network_prune_test.go b/client/network_prune_test.go new file mode 100644 index 0000000000..07a5d41f20 --- /dev/null +++ b/client/network_prune_test.go @@ -0,0 +1,99 @@ +package client + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "strings" + "testing" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/filters" + "github.com/docker/docker/pkg/testutil/assert" + "golang.org/x/net/context" +) + +func TestNetworksPruneError(t *testing.T) { + client := &Client{ + client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), + version: "1.25", + } + + filters := filters.NewArgs() + + _, err := client.NetworksPrune(context.Background(), filters) + if err == nil || err.Error() != "Error response from daemon: Server error" { + t.Fatalf("expected a Server Error, got %v", err) + } +} + +func TestNetworksPrune(t *testing.T) { + expectedURL := "/v1.25/networks/prune" + + danglingFilters := filters.NewArgs() + danglingFilters.Add("dangling", "true") + + noDanglingFilters := filters.NewArgs() + noDanglingFilters.Add("dangling", "false") + + listCases := []struct { + filters filters.Args + expectedQueryParams map[string]string + }{ + { + filters: filters.Args{}, + expectedQueryParams: map[string]string{ + "until": "", + "filter": "", + "filters": "", + }, + }, + { + filters: danglingFilters, + expectedQueryParams: map[string]string{ + "until": "", + "filter": "", + "filters": `{"dangling":{"true":true}}`, + }, + }, + { + filters: noDanglingFilters, + expectedQueryParams: map[string]string{ + "until": "", + "filter": "", + "filters": `{"dangling":{"false":true}}`, + }, + }, + } + for _, listCase := range listCases { + client := &Client{ + client: newMockClient(func(req *http.Request) (*http.Response, error) { + if !strings.HasPrefix(req.URL.Path, expectedURL) { + return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL) + } + query := req.URL.Query() + for key, expected := range listCase.expectedQueryParams { + actual := query.Get(key) + assert.Equal(t, actual, expected) + } + content, err := json.Marshal(types.NetworksPruneReport{ + NetworksDeleted: []string{"network_id1", "network_id2"}, + }) + if err != nil { + return nil, err + } + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader(content)), + }, nil + }), + version: "1.25", + } + + report, err := client.NetworksPrune(context.Background(), listCase.filters) + assert.NilError(t, err) + assert.Equal(t, len(report.NetworksDeleted), 2) + } +} diff --git a/daemon/prune.go b/daemon/prune.go index a693beb4e1..b0663b9433 100644 --- a/daemon/prune.go +++ b/daemon/prune.go @@ -3,11 +3,13 @@ package daemon import ( "fmt" "regexp" + "time" "github.com/Sirupsen/logrus" "github.com/docker/distribution/digest" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/filters" + timetypes "github.com/docker/docker/api/types/time" "github.com/docker/docker/image" "github.com/docker/docker/layer" "github.com/docker/docker/pkg/directory" @@ -21,9 +23,17 @@ import ( func (daemon *Daemon) ContainersPrune(pruneFilters filters.Args) (*types.ContainersPruneReport, error) { rep := &types.ContainersPruneReport{} + until, err := getUntilFromPruneFilters(pruneFilters) + if err != nil { + return nil, err + } + allContainers := daemon.List() for _, c := range allContainers { if !c.IsRunning() { + if !until.IsZero() && c.Created.After(until) { + continue + } cSize, _ := daemon.getSize(c) // TODO: sets RmLink to true? err := daemon.ContainerRm(c.ID, &types.ContainerRmConfig{}) @@ -84,6 +94,11 @@ func (daemon *Daemon) ImagesPrune(pruneFilters filters.Args) (*types.ImagesPrune } } + until, err := getUntilFromPruneFilters(pruneFilters) + if err != nil { + return nil, err + } + var allImages map[image.ID]*image.Image if danglingOnly { allImages = daemon.imageStore.Heads() @@ -104,6 +119,9 @@ func (daemon *Daemon) ImagesPrune(pruneFilters filters.Args) (*types.ImagesPrune if len(daemon.referenceStore.References(dgst)) == 0 && len(daemon.imageStore.Children(id)) != 0 { continue } + if !until.IsZero() && img.Created.After(until) { + continue + } topImages[id] = img } @@ -169,9 +187,17 @@ func (daemon *Daemon) ImagesPrune(pruneFilters filters.Args) (*types.ImagesPrune // localNetworksPrune removes unused local networks func (daemon *Daemon) localNetworksPrune(pruneFilters filters.Args) (*types.NetworksPruneReport, error) { rep := &types.NetworksPruneReport{} - var err error + + until, err := getUntilFromPruneFilters(pruneFilters) + if err != nil { + return rep, err + } + // When the function returns true, the walk will stop. l := func(nw libnetwork.Network) bool { + if !until.IsZero() && nw.Info().Created().After(until) { + return false + } nwName := nw.Name() predefined := runconfig.IsPreDefinedNetwork(nwName) if !predefined && len(nw.Endpoints()) == 0 { @@ -190,6 +216,12 @@ func (daemon *Daemon) localNetworksPrune(pruneFilters filters.Args) (*types.Netw // clusterNetworksPrune removes unused cluster networks func (daemon *Daemon) clusterNetworksPrune(pruneFilters filters.Args) (*types.NetworksPruneReport, error) { rep := &types.NetworksPruneReport{} + + until, err := getUntilFromPruneFilters(pruneFilters) + if err != nil { + return nil, err + } + cluster := daemon.GetCluster() networks, err := cluster.GetNetworks() if err != nil { @@ -200,6 +232,9 @@ func (daemon *Daemon) clusterNetworksPrune(pruneFilters filters.Args) (*types.Ne if nw.Name == "ingress" { continue } + if !until.IsZero() && nw.Created.After(until) { + continue + } // https://github.com/docker/docker/issues/24186 // `docker network inspect` unfortunately displays ONLY those containers that are local to that node. // So we try to remove it anyway and check the error @@ -234,3 +269,24 @@ func (daemon *Daemon) NetworksPrune(pruneFilters filters.Args) (*types.NetworksP } return rep, err } + +func getUntilFromPruneFilters(pruneFilters filters.Args) (time.Time, error) { + until := time.Time{} + if !pruneFilters.Include("until") { + return until, nil + } + untilFilters := pruneFilters.Get("until") + if len(untilFilters) > 1 { + return until, fmt.Errorf("more than one until filter specified") + } + ts, err := timetypes.GetTimestamp(untilFilters[0], time.Now()) + if err != nil { + return until, err + } + seconds, nanoseconds, err := timetypes.ParseTimestamps(ts, 0) + if err != nil { + return until, err + } + until = time.Unix(seconds, nanoseconds) + return until, nil +} diff --git a/docs/reference/commandline/container_prune.md b/docs/reference/commandline/container_prune.md index 43156406ec..f50e75f7a3 100644 --- a/docs/reference/commandline/container_prune.md +++ b/docs/reference/commandline/container_prune.md @@ -21,8 +21,10 @@ Usage: docker container prune [OPTIONS] Remove all stopped containers Options: - -f, --force Do not prompt for confirmation - --help Print usage +Options: + --filter filter Provide filter values (e.g. 'until=') + -f, --force Do not prompt for confirmation + --help Print usage ``` ## Examples @@ -38,6 +40,63 @@ f98f9c2aa1eaf727e4ec9c0283bc7d4aa4762fbdba7f26191f26c97f64090360 Total reclaimed space: 212 B ``` +## Filtering + +The filtering flag (`-f` or `--filter`) format is of "key=value". If there is more +than one filter, then pass multiple flags (e.g., `--filter "foo=bar" --filter "bif=baz"`) + +The currently supported filters are: + +* until (``) - only remove containers created before given timestamp + +The `until` filter can be Unix timestamps, date formatted +timestamps, or Go duration strings (e.g. `10m`, `1h30m`) computed +relative to the daemon machine’s time. Supported formats for date +formatted time stamps include RFC3339Nano, RFC3339, `2006-01-02T15:04:05`, +`2006-01-02T15:04:05.999999999`, `2006-01-02Z07:00`, and `2006-01-02`. The local +timezone on the daemon will be used if you do not provide either a `Z` or a +`+-00:00` timezone offset at the end of the timestamp. When providing Unix +timestamps enter seconds[.nanoseconds], where seconds is the number of seconds +that have elapsed since January 1, 1970 (midnight UTC/GMT), not counting leap +seconds (aka Unix epoch or Unix time), and the optional .nanoseconds field is a +fraction of a second no more than nine digits long. + +The following removes containers created more than 5 minutes ago: +```bash +$ docker ps -a --format 'table {{.ID}}\t{{.Image}}\t{{.Command}}\t{{.CreatedAt}}\t{{.Status}}' +CONTAINER ID IMAGE COMMAND CREATED AT STATUS +61b9efa71024 busybox "sh" 2017-01-04 13:23:33 -0800 PST Exited (0) 41 seconds ago +53a9bc23a516 busybox "sh" 2017-01-04 13:11:59 -0800 PST Exited (0) 12 minutes ago + +$ docker container prune --force --filter "until=5m" +Deleted Containers: +53a9bc23a5168b6caa2bfbefddf1b30f93c7ad57f3dec271fd32707497cb9369 + +Total reclaimed space: 25 B + +$ docker ps -a --format 'table {{.ID}}\t{{.Image}}\t{{.Command}}\t{{.CreatedAt}}\t{{.Status}}' +CONTAINER ID IMAGE COMMAND CREATED AT STATUS +61b9efa71024 busybox "sh" 2017-01-04 13:23:33 -0800 PST Exited (0) 44 seconds ago +``` + +The following removes containers created before `2017-01-04T13:10:00`: +```bash +$ docker ps -a --format 'table {{.ID}}\t{{.Image}}\t{{.Command}}\t{{.CreatedAt}}\t{{.Status}}' +CONTAINER ID IMAGE COMMAND CREATED AT STATUS +53a9bc23a516 busybox "sh" 2017-01-04 13:11:59 -0800 PST Exited (0) 7 minutes ago +4a75091a6d61 busybox "sh" 2017-01-04 13:09:53 -0800 PST Exited (0) 9 minutes ago + +$ docker container prune --force --filter "until=2017-01-04T13:10:00" +Deleted Containers: +4a75091a6d618526fcd8b33ccd6e5928ca2a64415466f768a6180004b0c72c6c + +Total reclaimed space: 27 B + +$ docker ps -a --format 'table {{.ID}}\t{{.Image}}\t{{.Command}}\t{{.CreatedAt}}\t{{.Status}}' +CONTAINER ID IMAGE COMMAND CREATED AT STATUS +53a9bc23a516 busybox "sh" 2017-01-04 13:11:59 -0800 PST Exited (0) 9 minutes ago +``` + ## Related information * [system df](system_df.md) diff --git a/docs/reference/commandline/image_prune.md b/docs/reference/commandline/image_prune.md index a80b8d38c8..e6450d3c5c 100644 --- a/docs/reference/commandline/image_prune.md +++ b/docs/reference/commandline/image_prune.md @@ -21,9 +21,10 @@ Usage: docker image prune [OPTIONS] Remove unused images Options: - -a, --all Remove all unused images, not just dangling ones - -f, --force Do not prompt for confirmation - --help Print usage + -a, --all Remove all unused images, not just dangling ones + --filter filter Provide filter values (e.g. 'until=') + -f, --force Do not prompt for confirmation + --help Print usage ``` Remove all dangling images. If `-a` is specified, will also remove all images not referenced by any container. @@ -62,6 +63,87 @@ deleted: sha256:2c675ee9ed53425e31a13e3390bf3f539bf8637000e4bcfbb85ee03ef4d910a1 Total reclaimed space: 16.43 MB ``` +## Filtering + +The filtering flag (`-f` or `--filter`) format is of "key=value". If there is more +than one filter, then pass multiple flags (e.g., `--filter "foo=bar" --filter "bif=baz"`) + +The currently supported filters are: + +* until (``) - only remove images created before given timestamp + +The `until` filter can be Unix timestamps, date formatted +timestamps, or Go duration strings (e.g. `10m`, `1h30m`) computed +relative to the daemon machine’s time. Supported formats for date +formatted time stamps include RFC3339Nano, RFC3339, `2006-01-02T15:04:05`, +`2006-01-02T15:04:05.999999999`, `2006-01-02Z07:00`, and `2006-01-02`. The local +timezone on the daemon will be used if you do not provide either a `Z` or a +`+-00:00` timezone offset at the end of the timestamp. When providing Unix +timestamps enter seconds[.nanoseconds], where seconds is the number of seconds +that have elapsed since January 1, 1970 (midnight UTC/GMT), not counting leap +seconds (aka Unix epoch or Unix time), and the optional .nanoseconds field is a +fraction of a second no more than nine digits long. + +The following removes images created before `2017-01-04T00:00:00`: +```bash +$ docker images --format 'table {{.Repository}}\t{{.Tag}}\t{{.ID}}\t{{.CreatedAt}}\t{{.Size}}' +REPOSITORY TAG IMAGE ID CREATED AT SIZE +foo latest 2f287ac753da 2017-01-04 13:42:23 -0800 PST 3.98 MB +alpine latest 88e169ea8f46 2016-12-27 10:17:25 -0800 PST 3.98 MB +busybox latest e02e811dd08f 2016-10-07 14:03:58 -0700 PDT 1.09 MB + +$ docker image prune -a --force --filter "until=2017-01-04T00:00:00" +Deleted Images: +untagged: alpine:latest +untagged: alpine@sha256:dfbd4a3a8ebca874ebd2474f044a0b33600d4523d03b0df76e5c5986cb02d7e8 +untagged: busybox:latest +untagged: busybox@sha256:29f5d56d12684887bdfa50dcd29fc31eea4aaf4ad3bec43daf19026a7ce69912 +deleted: sha256:e02e811dd08fd49e7f6032625495118e63f597eb150403d02e3238af1df240ba +deleted: sha256:e88b3f82283bc59d5e0df427c824e9f95557e661fcb0ea15fb0fb6f97760f9d9 + +Total reclaimed space: 1.093 MB + +$ docker images --format 'table {{.Repository}}\t{{.Tag}}\t{{.ID}}\t{{.CreatedAt}}\t{{.Size}}' +REPOSITORY TAG IMAGE ID CREATED AT SIZE +foo latest 2f287ac753da 2017-01-04 13:42:23 -0800 PST 3.98 MB +``` + +The following removes images created more than 10 days (`240h`) ago: +```bash +$ docker images +REPOSITORY TAG IMAGE ID CREATED SIZE +foo latest 2f287ac753da 14 seconds ago 3.98 MB +alpine latest 88e169ea8f46 8 days ago 3.98 MB +debian jessie 7b0a06c805e8 2 months ago 123 MB +busybox latest e02e811dd08f 2 months ago 1.09 MB +golang 1.7.0 138c2e655421 4 months ago 670 MB + +$ docker image prune -a --force --filter "until=240h" +Deleted Images: +untagged: golang:1.7.0 +untagged: golang@sha256:6765038c2b8f407fd6e3ecea043b44580c229ccfa2a13f6d85866cf2b4a9628e +deleted: sha256:138c2e6554219de65614d88c15521bfb2da674cbb0bf840de161f89ff4264b96 +deleted: sha256:ec353c2e1a673f456c4b78906d0d77f9d9456cfb5229b78c6a960bfb7496b76a +deleted: sha256:fe22765feaf3907526b4921c73ea6643ff9e334497c9b7e177972cf22f68ee93 +deleted: sha256:ff845959c80148421a5c3ae11cc0e6c115f950c89bc949646be55ed18d6a2912 +deleted: sha256:a4320831346648c03db64149eafc83092e2b34ab50ca6e8c13112388f25899a7 +deleted: sha256:4c76020202ee1d9709e703b7c6de367b325139e74eebd6b55b30a63c196abaf3 +deleted: sha256:d7afd92fb07236c8a2045715a86b7d5f0066cef025018cd3ca9a45498c51d1d6 +deleted: sha256:9e63c5bce4585dd7038d830a1f1f4e44cb1a1515b00e620ac718e934b484c938 +untagged: debian:jessie +untagged: debian@sha256:c1af755d300d0c65bb1194d24bce561d70c98a54fb5ce5b1693beb4f7988272f +deleted: sha256:7b0a06c805e8f23807fb8856621c60851727e85c7bcb751012c813f122734c8d +deleted: sha256:f96222d75c5563900bc4dd852179b720a0885de8f7a0619ba0ac76e92542bbc8 + +Total reclaimed space: 792.6 MB + +$ docker images +REPOSITORY TAG IMAGE ID CREATED SIZE +foo latest 2f287ac753da About a minute ago 3.98 MB +alpine latest 88e169ea8f46 8 days ago 3.98 MB +busybox latest e02e811dd08f 2 months ago 1.09 MB +``` + ## Related information * [system df](system_df.md) diff --git a/docs/reference/commandline/network_prune.md b/docs/reference/commandline/network_prune.md index 5b65465600..a3878c2977 100644 --- a/docs/reference/commandline/network_prune.md +++ b/docs/reference/commandline/network_prune.md @@ -12,8 +12,9 @@ Usage: docker network prune [OPTIONS] Remove all unused networks Options: - -f, --force Do not prompt for confirmation - --help Print usage + --filter filter Provide filter values (e.g. 'until=') + -f, --force Do not prompt for confirmation + --help Print usage ``` Remove all unused networks. Unused networks are those which are not referenced by any containers. @@ -29,6 +30,51 @@ n1 n2 ``` +## Filtering + +The filtering flag (`-f` or `--filter`) format is of "key=value". If there is more +than one filter, then pass multiple flags (e.g., `--filter "foo=bar" --filter "bif=baz"`) + +The currently supported filters are: + +* until (``) - only remove networks created before given timestamp + +The `until` filter can be Unix timestamps, date formatted +timestamps, or Go duration strings (e.g. `10m`, `1h30m`) computed +relative to the daemon machine’s time. Supported formats for date +formatted time stamps include RFC3339Nano, RFC3339, `2006-01-02T15:04:05`, +`2006-01-02T15:04:05.999999999`, `2006-01-02Z07:00`, and `2006-01-02`. The local +timezone on the daemon will be used if you do not provide either a `Z` or a +`+-00:00` timezone offset at the end of the timestamp. When providing Unix +timestamps enter seconds[.nanoseconds], where seconds is the number of seconds +that have elapsed since January 1, 1970 (midnight UTC/GMT), not counting leap +seconds (aka Unix epoch or Unix time), and the optional .nanoseconds field is a +fraction of a second no more than nine digits long. + +The following removes networks created more than 5 minutes ago. Note that +system networks such as `bridge`, `host`, and `none` will never be pruned: + +```bash +$ docker network ls +NETWORK ID NAME DRIVER SCOPE +7430df902d7a bridge bridge local +ea92373fd499 foo-1-day-ago bridge local +ab53663ed3c7 foo-1-min-ago bridge local +97b91972bc3b host host local +f949d337b1f5 none null local + +$ docker network prune --force --filter until=5m +Deleted Networks: +foo-1-day-ago + +$ docker network ls +NETWORK ID NAME DRIVER SCOPE +7430df902d7a bridge bridge local +ab53663ed3c7 foo-1-min-ago bridge local +97b91972bc3b host host local +f949d337b1f5 none null local +``` + ## Related information * [network disconnect ](network_disconnect.md) diff --git a/docs/reference/commandline/system_prune.md b/docs/reference/commandline/system_prune.md index 46f8c4364a..b6764a779f 100644 --- a/docs/reference/commandline/system_prune.md +++ b/docs/reference/commandline/system_prune.md @@ -21,9 +21,10 @@ Usage: docker system prune [OPTIONS] Delete unused data Options: - -a, --all Remove all unused data not just dangling ones - -f, --force Do not prompt for confirmation - --help Print usage + -a, --all Remove all unused images not just dangling ones + --filter filter Provide filter values (e.g. 'until=') + -f, --force Do not prompt for confirmation + --help Print usage ``` Remove all unused containers, volumes, networks and images (both dangling and unreferenced). @@ -64,6 +65,27 @@ deleted: sha256:3a88a5c81eb5c283e72db2dbc6d65cbfd8e80b6c89bb6e714cfaaa0eed99c548 Total reclaimed space: 13.5 MB ``` +## Filtering + +The filtering flag (`-f` or `--filter`) format is of "key=value". If there is more +than one filter, then pass multiple flags (e.g., `--filter "foo=bar" --filter "bif=baz"`) + +The currently supported filters are: + +* until (``) - only remove containers, images, and networks created before given timestamp + +The `until` filter can be Unix timestamps, date formatted +timestamps, or Go duration strings (e.g. `10m`, `1h30m`) computed +relative to the daemon machine’s time. Supported formats for date +formatted time stamps include RFC3339Nano, RFC3339, `2006-01-02T15:04:05`, +`2006-01-02T15:04:05.999999999`, `2006-01-02Z07:00`, and `2006-01-02`. The local +timezone on the daemon will be used if you do not provide either a `Z` or a +`+-00:00` timezone offset at the end of the timestamp. When providing Unix +timestamps enter seconds[.nanoseconds], where seconds is the number of seconds +that have elapsed since January 1, 1970 (midnight UTC/GMT), not counting leap +seconds (aka Unix epoch or Unix time), and the optional .nanoseconds field is a +fraction of a second no more than nine digits long. + ## Related information * [volume create](volume_create.md) diff --git a/integration-cli/docker_cli_prune_unix_test.go b/integration-cli/docker_cli_prune_unix_test.go index 842a18c084..79d7d3cf5b 100644 --- a/integration-cli/docker_cli_prune_unix_test.go +++ b/integration-cli/docker_cli_prune_unix_test.go @@ -5,6 +5,7 @@ package main import ( "strconv" "strings" + "time" "github.com/docker/docker/integration-cli/checker" "github.com/docker/docker/integration-cli/daemon" @@ -90,3 +91,23 @@ func (s *DockerDaemonSuite) TestPruneImageDangling(c *check.C) { c.Assert(err, checker.IsNil) c.Assert(strings.TrimSpace(out), checker.Not(checker.Contains), id) } + +func (s *DockerSuite) TestPruneContainerUntil(c *check.C) { + out, _ := dockerCmd(c, "run", "-d", "busybox") + id1 := strings.TrimSpace(out) + c.Assert(waitExited(id1, 5*time.Second), checker.IsNil) + + until := daemonUnixTime(c) + + out, _ = dockerCmd(c, "run", "-d", "busybox") + id2 := strings.TrimSpace(out) + c.Assert(waitExited(id2, 5*time.Second), checker.IsNil) + + out, _ = dockerCmd(c, "container", "prune", "--force", "--filter", "until="+until) + c.Assert(strings.TrimSpace(out), checker.Contains, id1) + c.Assert(strings.TrimSpace(out), checker.Not(checker.Contains), id2) + + out, _ = dockerCmd(c, "ps", "-a", "-q", "--no-trunc") + c.Assert(strings.TrimSpace(out), checker.Not(checker.Contains), id1) + c.Assert(strings.TrimSpace(out), checker.Contains, id2) +}