This fix is a follow up for comment
https://github.com/docker/docker/pull/28535#issuecomment-263215225
This fix provides `--filter until=<timestamp>` for `docker container/image prune`.
This fix adds `--filter until=<timestamp>` 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 <yong.tang.github@outlook.com>
| ... | ... |
@@ -312,7 +312,12 @@ func (n *networkRouter) postNetworksPrune(ctx context.Context, w http.ResponseWr |
| 312 | 312 |
return err |
| 313 | 313 |
} |
| 314 | 314 |
|
| 315 |
- pruneReport, err := n.backend.NetworksPrune(filters.Args{})
|
|
| 315 |
+ pruneFilters, err := filters.FromParam(r.Form.Get("filters"))
|
|
| 316 |
+ if err != nil {
|
|
| 317 |
+ return err |
|
| 318 |
+ } |
|
| 319 |
+ |
|
| 320 |
+ pruneReport, err := n.backend.NetworksPrune(pruneFilters) |
|
| 316 | 321 |
if err != nil {
|
| 317 | 322 |
return err |
| 318 | 323 |
} |
| ... | ... |
@@ -4191,6 +4191,7 @@ paths: |
| 4191 | 4191 |
Filters to process on the prune list, encoded as JSON (a `map[string][]string`). |
| 4192 | 4192 |
|
| 4193 | 4193 |
Available filters: |
| 4194 |
+ - `until=<timestamp>` Prune containers created before this timestamp. The `<timestamp>` can be Unix timestamps, date formatted timestamps, or Go duration strings (e.g. `10m`, `1h30m`) computed relative to the daemon machine’s time. |
|
| 4194 | 4195 |
type: "string" |
| 4195 | 4196 |
responses: |
| 4196 | 4197 |
200: |
| ... | ... |
@@ -4866,6 +4867,7 @@ paths: |
| 4866 | 4866 |
- `dangling=<boolean>` When set to `true` (or `1`), prune only |
| 4867 | 4867 |
unused *and* untagged images. When set to `false` |
| 4868 | 4868 |
(or `0`), all unused images are pruned. |
| 4869 |
+ - `until=<string>` Prune images created before this timestamp. The `<timestamp>` can be Unix timestamps, date formatted timestamps, or Go duration strings (e.g. `10m`, `1h30m`) computed relative to the daemon machine’s time. |
|
| 4869 | 4870 |
type: "string" |
| 4870 | 4871 |
responses: |
| 4871 | 4872 |
200: |
| ... | ... |
@@ -6295,8 +6297,6 @@ paths: |
| 6295 | 6295 |
/networks/prune: |
| 6296 | 6296 |
post: |
| 6297 | 6297 |
summary: "Delete unused networks" |
| 6298 |
- consumes: |
|
| 6299 |
- - "application/json" |
|
| 6300 | 6298 |
produces: |
| 6301 | 6299 |
- "application/json" |
| 6302 | 6300 |
operationId: "NetworkPrune" |
| ... | ... |
@@ -6307,6 +6307,7 @@ paths: |
| 6307 | 6307 |
Filters to process on the prune list, encoded as JSON (a `map[string][]string`). |
| 6308 | 6308 |
|
| 6309 | 6309 |
Available filters: |
| 6310 |
+ - `until=<timestamp>` Prune networks created before this timestamp. The `<timestamp>` can be Unix timestamps, date formatted timestamps, or Go duration strings (e.g. `10m`, `1h30m`) computed relative to the daemon machine’s time. |
|
| 6310 | 6311 |
type: "string" |
| 6311 | 6312 |
responses: |
| 6312 | 6313 |
200: |
| ... | ... |
@@ -3,21 +3,22 @@ package container |
| 3 | 3 |
import ( |
| 4 | 4 |
"fmt" |
| 5 | 5 |
|
| 6 |
- "github.com/docker/docker/api/types/filters" |
|
| 7 | 6 |
"github.com/docker/docker/cli" |
| 8 | 7 |
"github.com/docker/docker/cli/command" |
| 8 |
+ "github.com/docker/docker/opts" |
|
| 9 | 9 |
units "github.com/docker/go-units" |
| 10 | 10 |
"github.com/spf13/cobra" |
| 11 | 11 |
"golang.org/x/net/context" |
| 12 | 12 |
) |
| 13 | 13 |
|
| 14 | 14 |
type pruneOptions struct {
|
| 15 |
- force bool |
|
| 15 |
+ force bool |
|
| 16 |
+ filter opts.FilterOpt |
|
| 16 | 17 |
} |
| 17 | 18 |
|
| 18 | 19 |
// NewPruneCommand returns a new cobra prune command for containers |
| 19 | 20 |
func NewPruneCommand(dockerCli *command.DockerCli) *cobra.Command {
|
| 20 |
- var opts pruneOptions |
|
| 21 |
+ opts := pruneOptions{filter: opts.NewFilterOpt()}
|
|
| 21 | 22 |
|
| 22 | 23 |
cmd := &cobra.Command{
|
| 23 | 24 |
Use: "prune [OPTIONS]", |
| ... | ... |
@@ -39,6 +40,7 @@ func NewPruneCommand(dockerCli *command.DockerCli) *cobra.Command {
|
| 39 | 39 |
|
| 40 | 40 |
flags := cmd.Flags() |
| 41 | 41 |
flags.BoolVarP(&opts.force, "force", "f", false, "Do not prompt for confirmation") |
| 42 |
+ flags.Var(&opts.filter, "filter", "Provide filter values (e.g. 'until=<timestamp>')") |
|
| 42 | 43 |
|
| 43 | 44 |
return cmd |
| 44 | 45 |
} |
| ... | ... |
@@ -47,11 +49,13 @@ const warning = `WARNING! This will remove all stopped containers. |
| 47 | 47 |
Are you sure you want to continue?` |
| 48 | 48 |
|
| 49 | 49 |
func runPrune(dockerCli *command.DockerCli, opts pruneOptions) (spaceReclaimed uint64, output string, err error) {
|
| 50 |
+ pruneFilters := opts.filter.Value() |
|
| 51 |
+ |
|
| 50 | 52 |
if !opts.force && !command.PromptForConfirmation(dockerCli.In(), dockerCli.Out(), warning) {
|
| 51 | 53 |
return |
| 52 | 54 |
} |
| 53 | 55 |
|
| 54 |
- report, err := dockerCli.Client().ContainersPrune(context.Background(), filters.Args{})
|
|
| 56 |
+ report, err := dockerCli.Client().ContainersPrune(context.Background(), pruneFilters) |
|
| 55 | 57 |
if err != nil {
|
| 56 | 58 |
return |
| 57 | 59 |
} |
| ... | ... |
@@ -69,6 +73,6 @@ func runPrune(dockerCli *command.DockerCli, opts pruneOptions) (spaceReclaimed u |
| 69 | 69 |
|
| 70 | 70 |
// RunPrune calls the Container Prune API |
| 71 | 71 |
// This returns the amount of space reclaimed and a detailed output string |
| 72 |
-func RunPrune(dockerCli *command.DockerCli) (uint64, string, error) {
|
|
| 73 |
- return runPrune(dockerCli, pruneOptions{force: true})
|
|
| 72 |
+func RunPrune(dockerCli *command.DockerCli, filter opts.FilterOpt) (uint64, string, error) {
|
|
| 73 |
+ return runPrune(dockerCli, pruneOptions{force: true, filter: filter})
|
|
| 74 | 74 |
} |
| ... | ... |
@@ -5,21 +5,22 @@ import ( |
| 5 | 5 |
|
| 6 | 6 |
"golang.org/x/net/context" |
| 7 | 7 |
|
| 8 |
- "github.com/docker/docker/api/types/filters" |
|
| 9 | 8 |
"github.com/docker/docker/cli" |
| 10 | 9 |
"github.com/docker/docker/cli/command" |
| 10 |
+ "github.com/docker/docker/opts" |
|
| 11 | 11 |
units "github.com/docker/go-units" |
| 12 | 12 |
"github.com/spf13/cobra" |
| 13 | 13 |
) |
| 14 | 14 |
|
| 15 | 15 |
type pruneOptions struct {
|
| 16 |
- force bool |
|
| 17 |
- all bool |
|
| 16 |
+ force bool |
|
| 17 |
+ all bool |
|
| 18 |
+ filter opts.FilterOpt |
|
| 18 | 19 |
} |
| 19 | 20 |
|
| 20 | 21 |
// NewPruneCommand returns a new cobra prune command for images |
| 21 | 22 |
func NewPruneCommand(dockerCli *command.DockerCli) *cobra.Command {
|
| 22 |
- var opts pruneOptions |
|
| 23 |
+ opts := pruneOptions{filter: opts.NewFilterOpt()}
|
|
| 23 | 24 |
|
| 24 | 25 |
cmd := &cobra.Command{
|
| 25 | 26 |
Use: "prune [OPTIONS]", |
| ... | ... |
@@ -42,6 +43,7 @@ func NewPruneCommand(dockerCli *command.DockerCli) *cobra.Command {
|
| 42 | 42 |
flags := cmd.Flags() |
| 43 | 43 |
flags.BoolVarP(&opts.force, "force", "f", false, "Do not prompt for confirmation") |
| 44 | 44 |
flags.BoolVarP(&opts.all, "all", "a", false, "Remove all unused images, not just dangling ones") |
| 45 |
+ flags.Var(&opts.filter, "filter", "Provide filter values (e.g. 'until=<timestamp>')") |
|
| 45 | 46 |
|
| 46 | 47 |
return cmd |
| 47 | 48 |
} |
| ... | ... |
@@ -54,7 +56,7 @@ Are you sure you want to continue?` |
| 54 | 54 |
) |
| 55 | 55 |
|
| 56 | 56 |
func runPrune(dockerCli *command.DockerCli, opts pruneOptions) (spaceReclaimed uint64, output string, err error) {
|
| 57 |
- pruneFilters := filters.NewArgs() |
|
| 57 |
+ pruneFilters := opts.filter.Value() |
|
| 58 | 58 |
pruneFilters.Add("dangling", fmt.Sprintf("%v", !opts.all))
|
| 59 | 59 |
|
| 60 | 60 |
warning := danglingWarning |
| ... | ... |
@@ -87,6 +89,6 @@ func runPrune(dockerCli *command.DockerCli, opts pruneOptions) (spaceReclaimed u |
| 87 | 87 |
|
| 88 | 88 |
// RunPrune calls the Image Prune API |
| 89 | 89 |
// This returns the amount of space reclaimed and a detailed output string |
| 90 |
-func RunPrune(dockerCli *command.DockerCli, all bool) (uint64, string, error) {
|
|
| 91 |
- return runPrune(dockerCli, pruneOptions{force: true, all: all})
|
|
| 90 |
+func RunPrune(dockerCli *command.DockerCli, all bool, filter opts.FilterOpt) (uint64, string, error) {
|
|
| 91 |
+ return runPrune(dockerCli, pruneOptions{force: true, all: all, filter: filter})
|
|
| 92 | 92 |
} |
| ... | ... |
@@ -5,19 +5,20 @@ import ( |
| 5 | 5 |
|
| 6 | 6 |
"golang.org/x/net/context" |
| 7 | 7 |
|
| 8 |
- "github.com/docker/docker/api/types/filters" |
|
| 9 | 8 |
"github.com/docker/docker/cli" |
| 10 | 9 |
"github.com/docker/docker/cli/command" |
| 10 |
+ "github.com/docker/docker/opts" |
|
| 11 | 11 |
"github.com/spf13/cobra" |
| 12 | 12 |
) |
| 13 | 13 |
|
| 14 | 14 |
type pruneOptions struct {
|
| 15 |
- force bool |
|
| 15 |
+ force bool |
|
| 16 |
+ filter opts.FilterOpt |
|
| 16 | 17 |
} |
| 17 | 18 |
|
| 18 | 19 |
// NewPruneCommand returns a new cobra prune command for networks |
| 19 | 20 |
func NewPruneCommand(dockerCli *command.DockerCli) *cobra.Command {
|
| 20 |
- var opts pruneOptions |
|
| 21 |
+ opts := pruneOptions{filter: opts.NewFilterOpt()}
|
|
| 21 | 22 |
|
| 22 | 23 |
cmd := &cobra.Command{
|
| 23 | 24 |
Use: "prune [OPTIONS]", |
| ... | ... |
@@ -38,6 +39,7 @@ func NewPruneCommand(dockerCli *command.DockerCli) *cobra.Command {
|
| 38 | 38 |
|
| 39 | 39 |
flags := cmd.Flags() |
| 40 | 40 |
flags.BoolVarP(&opts.force, "force", "f", false, "Do not prompt for confirmation") |
| 41 |
+ flags.Var(&opts.filter, "filter", "Provide filter values (e.g. 'until=<timestamp>')") |
|
| 41 | 42 |
|
| 42 | 43 |
return cmd |
| 43 | 44 |
} |
| ... | ... |
@@ -46,11 +48,13 @@ const warning = `WARNING! This will remove all networks not used by at least one |
| 46 | 46 |
Are you sure you want to continue?` |
| 47 | 47 |
|
| 48 | 48 |
func runPrune(dockerCli *command.DockerCli, opts pruneOptions) (output string, err error) {
|
| 49 |
+ pruneFilters := opts.filter.Value() |
|
| 50 |
+ |
|
| 49 | 51 |
if !opts.force && !command.PromptForConfirmation(dockerCli.In(), dockerCli.Out(), warning) {
|
| 50 | 52 |
return |
| 51 | 53 |
} |
| 52 | 54 |
|
| 53 |
- report, err := dockerCli.Client().NetworksPrune(context.Background(), filters.Args{})
|
|
| 55 |
+ report, err := dockerCli.Client().NetworksPrune(context.Background(), pruneFilters) |
|
| 54 | 56 |
if err != nil {
|
| 55 | 57 |
return |
| 56 | 58 |
} |
| ... | ... |
@@ -67,7 +71,7 @@ func runPrune(dockerCli *command.DockerCli, opts pruneOptions) (output string, e |
| 67 | 67 |
|
| 68 | 68 |
// RunPrune calls the Network Prune API |
| 69 | 69 |
// This returns the amount of space reclaimed and a detailed output string |
| 70 |
-func RunPrune(dockerCli *command.DockerCli) (uint64, string, error) {
|
|
| 71 |
- output, err := runPrune(dockerCli, pruneOptions{force: true})
|
|
| 70 |
+func RunPrune(dockerCli *command.DockerCli, filter opts.FilterOpt) (uint64, string, error) {
|
|
| 71 |
+ output, err := runPrune(dockerCli, pruneOptions{force: true, filter: filter})
|
|
| 72 | 72 |
return 0, output, err |
| 73 | 73 |
} |
| ... | ... |
@@ -6,6 +6,7 @@ import ( |
| 6 | 6 |
"github.com/docker/docker/cli/command/image" |
| 7 | 7 |
"github.com/docker/docker/cli/command/network" |
| 8 | 8 |
"github.com/docker/docker/cli/command/volume" |
| 9 |
+ "github.com/docker/docker/opts" |
|
| 9 | 10 |
"github.com/spf13/cobra" |
| 10 | 11 |
) |
| 11 | 12 |
|
| ... | ... |
@@ -30,21 +31,21 @@ func NewNetworkPruneCommand(dockerCli *command.DockerCli) *cobra.Command {
|
| 30 | 30 |
} |
| 31 | 31 |
|
| 32 | 32 |
// RunContainerPrune executes a prune command for containers |
| 33 |
-func RunContainerPrune(dockerCli *command.DockerCli) (uint64, string, error) {
|
|
| 34 |
- return container.RunPrune(dockerCli) |
|
| 33 |
+func RunContainerPrune(dockerCli *command.DockerCli, filter opts.FilterOpt) (uint64, string, error) {
|
|
| 34 |
+ return container.RunPrune(dockerCli, filter) |
|
| 35 | 35 |
} |
| 36 | 36 |
|
| 37 | 37 |
// RunVolumePrune executes a prune command for volumes |
| 38 |
-func RunVolumePrune(dockerCli *command.DockerCli) (uint64, string, error) {
|
|
| 38 |
+func RunVolumePrune(dockerCli *command.DockerCli, filter opts.FilterOpt) (uint64, string, error) {
|
|
| 39 | 39 |
return volume.RunPrune(dockerCli) |
| 40 | 40 |
} |
| 41 | 41 |
|
| 42 | 42 |
// RunImagePrune executes a prune command for images |
| 43 |
-func RunImagePrune(dockerCli *command.DockerCli, all bool) (uint64, string, error) {
|
|
| 44 |
- return image.RunPrune(dockerCli, all) |
|
| 43 |
+func RunImagePrune(dockerCli *command.DockerCli, all bool, filter opts.FilterOpt) (uint64, string, error) {
|
|
| 44 |
+ return image.RunPrune(dockerCli, all, filter) |
|
| 45 | 45 |
} |
| 46 | 46 |
|
| 47 | 47 |
// RunNetworkPrune executes a prune command for networks |
| 48 |
-func RunNetworkPrune(dockerCli *command.DockerCli) (uint64, string, error) {
|
|
| 49 |
- return network.RunPrune(dockerCli) |
|
| 48 |
+func RunNetworkPrune(dockerCli *command.DockerCli, filter opts.FilterOpt) (uint64, string, error) {
|
|
| 49 |
+ return network.RunPrune(dockerCli, filter) |
|
| 50 | 50 |
} |
| ... | ... |
@@ -6,18 +6,20 @@ import ( |
| 6 | 6 |
"github.com/docker/docker/cli" |
| 7 | 7 |
"github.com/docker/docker/cli/command" |
| 8 | 8 |
"github.com/docker/docker/cli/command/prune" |
| 9 |
+ "github.com/docker/docker/opts" |
|
| 9 | 10 |
units "github.com/docker/go-units" |
| 10 | 11 |
"github.com/spf13/cobra" |
| 11 | 12 |
) |
| 12 | 13 |
|
| 13 | 14 |
type pruneOptions struct {
|
| 14 |
- force bool |
|
| 15 |
- all bool |
|
| 15 |
+ force bool |
|
| 16 |
+ all bool |
|
| 17 |
+ filter opts.FilterOpt |
|
| 16 | 18 |
} |
| 17 | 19 |
|
| 18 | 20 |
// NewPruneCommand creates a new cobra.Command for `docker prune` |
| 19 | 21 |
func NewPruneCommand(dockerCli *command.DockerCli) *cobra.Command {
|
| 20 |
- var opts pruneOptions |
|
| 22 |
+ opts := pruneOptions{filter: opts.NewFilterOpt()}
|
|
| 21 | 23 |
|
| 22 | 24 |
cmd := &cobra.Command{
|
| 23 | 25 |
Use: "prune [OPTIONS]", |
| ... | ... |
@@ -32,6 +34,7 @@ func NewPruneCommand(dockerCli *command.DockerCli) *cobra.Command {
|
| 32 | 32 |
flags := cmd.Flags() |
| 33 | 33 |
flags.BoolVarP(&opts.force, "force", "f", false, "Do not prompt for confirmation") |
| 34 | 34 |
flags.BoolVarP(&opts.all, "all", "a", false, "Remove all unused images not just dangling ones") |
| 35 |
+ flags.Var(&opts.filter, "filter", "Provide filter values (e.g. 'until=<timestamp>')") |
|
| 35 | 36 |
|
| 36 | 37 |
return cmd |
| 37 | 38 |
} |
| ... | ... |
@@ -48,27 +51,27 @@ Are you sure you want to continue?` |
| 48 | 48 |
allImageDesc = `- all images without at least one container associated to them` |
| 49 | 49 |
) |
| 50 | 50 |
|
| 51 |
-func runPrune(dockerCli *command.DockerCli, opts pruneOptions) error {
|
|
| 51 |
+func runPrune(dockerCli *command.DockerCli, options pruneOptions) error {
|
|
| 52 | 52 |
var message string |
| 53 | 53 |
|
| 54 |
- if opts.all {
|
|
| 54 |
+ if options.all {
|
|
| 55 | 55 |
message = fmt.Sprintf(warning, allImageDesc) |
| 56 | 56 |
} else {
|
| 57 | 57 |
message = fmt.Sprintf(warning, danglingImageDesc) |
| 58 | 58 |
} |
| 59 | 59 |
|
| 60 |
- if !opts.force && !command.PromptForConfirmation(dockerCli.In(), dockerCli.Out(), message) {
|
|
| 60 |
+ if !options.force && !command.PromptForConfirmation(dockerCli.In(), dockerCli.Out(), message) {
|
|
| 61 | 61 |
return nil |
| 62 | 62 |
} |
| 63 | 63 |
|
| 64 | 64 |
var spaceReclaimed uint64 |
| 65 | 65 |
|
| 66 |
- for _, pruneFn := range []func(dockerCli *command.DockerCli) (uint64, string, error){
|
|
| 66 |
+ for _, pruneFn := range []func(dockerCli *command.DockerCli, filter opts.FilterOpt) (uint64, string, error){
|
|
| 67 | 67 |
prune.RunContainerPrune, |
| 68 | 68 |
prune.RunVolumePrune, |
| 69 | 69 |
prune.RunNetworkPrune, |
| 70 | 70 |
} {
|
| 71 |
- spc, output, err := pruneFn(dockerCli) |
|
| 71 |
+ spc, output, err := pruneFn(dockerCli, options.filter) |
|
| 72 | 72 |
if err != nil {
|
| 73 | 73 |
return err |
| 74 | 74 |
} |
| ... | ... |
@@ -78,7 +81,7 @@ func runPrune(dockerCli *command.DockerCli, opts pruneOptions) error {
|
| 78 | 78 |
} |
| 79 | 79 |
} |
| 80 | 80 |
|
| 81 |
- spc, output, err := prune.RunImagePrune(dockerCli, opts.all) |
|
| 81 |
+ spc, output, err := prune.RunImagePrune(dockerCli, options.all, options.filter) |
|
| 82 | 82 |
if err != nil {
|
| 83 | 83 |
return err |
| 84 | 84 |
} |
| 85 | 85 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,111 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "bytes" |
|
| 4 |
+ "encoding/json" |
|
| 5 |
+ "fmt" |
|
| 6 |
+ "io/ioutil" |
|
| 7 |
+ "net/http" |
|
| 8 |
+ "strings" |
|
| 9 |
+ "testing" |
|
| 10 |
+ |
|
| 11 |
+ "github.com/docker/docker/api/types" |
|
| 12 |
+ "github.com/docker/docker/api/types/filters" |
|
| 13 |
+ "github.com/docker/docker/pkg/testutil/assert" |
|
| 14 |
+ "golang.org/x/net/context" |
|
| 15 |
+) |
|
| 16 |
+ |
|
| 17 |
+func TestContainersPruneError(t *testing.T) {
|
|
| 18 |
+ client := &Client{
|
|
| 19 |
+ client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), |
|
| 20 |
+ version: "1.25", |
|
| 21 |
+ } |
|
| 22 |
+ |
|
| 23 |
+ filters := filters.NewArgs() |
|
| 24 |
+ |
|
| 25 |
+ _, err := client.ContainersPrune(context.Background(), filters) |
|
| 26 |
+ assert.Error(t, err, "Error response from daemon: Server error") |
|
| 27 |
+} |
|
| 28 |
+ |
|
| 29 |
+func TestContainersPrune(t *testing.T) {
|
|
| 30 |
+ expectedURL := "/v1.25/containers/prune" |
|
| 31 |
+ |
|
| 32 |
+ danglingFilters := filters.NewArgs() |
|
| 33 |
+ danglingFilters.Add("dangling", "true")
|
|
| 34 |
+ |
|
| 35 |
+ noDanglingFilters := filters.NewArgs() |
|
| 36 |
+ noDanglingFilters.Add("dangling", "false")
|
|
| 37 |
+ |
|
| 38 |
+ danglingUntilFilters := filters.NewArgs() |
|
| 39 |
+ danglingUntilFilters.Add("dangling", "true")
|
|
| 40 |
+ danglingUntilFilters.Add("until", "2016-12-15T14:00")
|
|
| 41 |
+ |
|
| 42 |
+ listCases := []struct {
|
|
| 43 |
+ filters filters.Args |
|
| 44 |
+ expectedQueryParams map[string]string |
|
| 45 |
+ }{
|
|
| 46 |
+ {
|
|
| 47 |
+ filters: filters.Args{},
|
|
| 48 |
+ expectedQueryParams: map[string]string{
|
|
| 49 |
+ "until": "", |
|
| 50 |
+ "filter": "", |
|
| 51 |
+ "filters": "", |
|
| 52 |
+ }, |
|
| 53 |
+ }, |
|
| 54 |
+ {
|
|
| 55 |
+ filters: danglingFilters, |
|
| 56 |
+ expectedQueryParams: map[string]string{
|
|
| 57 |
+ "until": "", |
|
| 58 |
+ "filter": "", |
|
| 59 |
+ "filters": `{"dangling":{"true":true}}`,
|
|
| 60 |
+ }, |
|
| 61 |
+ }, |
|
| 62 |
+ {
|
|
| 63 |
+ filters: danglingUntilFilters, |
|
| 64 |
+ expectedQueryParams: map[string]string{
|
|
| 65 |
+ "until": "", |
|
| 66 |
+ "filter": "", |
|
| 67 |
+ "filters": `{"dangling":{"true":true},"until":{"2016-12-15T14:00":true}}`,
|
|
| 68 |
+ }, |
|
| 69 |
+ }, |
|
| 70 |
+ {
|
|
| 71 |
+ filters: noDanglingFilters, |
|
| 72 |
+ expectedQueryParams: map[string]string{
|
|
| 73 |
+ "until": "", |
|
| 74 |
+ "filter": "", |
|
| 75 |
+ "filters": `{"dangling":{"false":true}}`,
|
|
| 76 |
+ }, |
|
| 77 |
+ }, |
|
| 78 |
+ } |
|
| 79 |
+ for _, listCase := range listCases {
|
|
| 80 |
+ client := &Client{
|
|
| 81 |
+ client: newMockClient(func(req *http.Request) (*http.Response, error) {
|
|
| 82 |
+ if !strings.HasPrefix(req.URL.Path, expectedURL) {
|
|
| 83 |
+ return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
|
|
| 84 |
+ } |
|
| 85 |
+ query := req.URL.Query() |
|
| 86 |
+ for key, expected := range listCase.expectedQueryParams {
|
|
| 87 |
+ actual := query.Get(key) |
|
| 88 |
+ assert.Equal(t, actual, expected) |
|
| 89 |
+ } |
|
| 90 |
+ content, err := json.Marshal(types.ContainersPruneReport{
|
|
| 91 |
+ ContainersDeleted: []string{"container_id1", "container_id2"},
|
|
| 92 |
+ SpaceReclaimed: 9999, |
|
| 93 |
+ }) |
|
| 94 |
+ if err != nil {
|
|
| 95 |
+ return nil, err |
|
| 96 |
+ } |
|
| 97 |
+ return &http.Response{
|
|
| 98 |
+ StatusCode: http.StatusOK, |
|
| 99 |
+ Body: ioutil.NopCloser(bytes.NewReader(content)), |
|
| 100 |
+ }, nil |
|
| 101 |
+ }), |
|
| 102 |
+ version: "1.25", |
|
| 103 |
+ } |
|
| 104 |
+ |
|
| 105 |
+ report, err := client.ContainersPrune(context.Background(), listCase.filters) |
|
| 106 |
+ assert.NilError(t, err) |
|
| 107 |
+ assert.Equal(t, len(report.ContainersDeleted), 2) |
|
| 108 |
+ assert.Equal(t, report.SpaceReclaimed, uint64(9999)) |
|
| 109 |
+ } |
|
| 110 |
+} |
| 0 | 111 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,106 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "bytes" |
|
| 4 |
+ "encoding/json" |
|
| 5 |
+ "fmt" |
|
| 6 |
+ "io/ioutil" |
|
| 7 |
+ "net/http" |
|
| 8 |
+ "strings" |
|
| 9 |
+ "testing" |
|
| 10 |
+ |
|
| 11 |
+ "github.com/docker/docker/api/types" |
|
| 12 |
+ "github.com/docker/docker/api/types/filters" |
|
| 13 |
+ "github.com/docker/docker/pkg/testutil/assert" |
|
| 14 |
+ "golang.org/x/net/context" |
|
| 15 |
+) |
|
| 16 |
+ |
|
| 17 |
+func TestImagesPruneError(t *testing.T) {
|
|
| 18 |
+ client := &Client{
|
|
| 19 |
+ client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), |
|
| 20 |
+ version: "1.25", |
|
| 21 |
+ } |
|
| 22 |
+ |
|
| 23 |
+ filters := filters.NewArgs() |
|
| 24 |
+ |
|
| 25 |
+ _, err := client.ImagesPrune(context.Background(), filters) |
|
| 26 |
+ assert.Error(t, err, "Error response from daemon: Server error") |
|
| 27 |
+} |
|
| 28 |
+ |
|
| 29 |
+func TestImagesPrune(t *testing.T) {
|
|
| 30 |
+ expectedURL := "/v1.25/images/prune" |
|
| 31 |
+ |
|
| 32 |
+ danglingFilters := filters.NewArgs() |
|
| 33 |
+ danglingFilters.Add("dangling", "true")
|
|
| 34 |
+ |
|
| 35 |
+ noDanglingFilters := filters.NewArgs() |
|
| 36 |
+ noDanglingFilters.Add("dangling", "false")
|
|
| 37 |
+ |
|
| 38 |
+ listCases := []struct {
|
|
| 39 |
+ filters filters.Args |
|
| 40 |
+ expectedQueryParams map[string]string |
|
| 41 |
+ }{
|
|
| 42 |
+ {
|
|
| 43 |
+ filters: filters.Args{},
|
|
| 44 |
+ expectedQueryParams: map[string]string{
|
|
| 45 |
+ "until": "", |
|
| 46 |
+ "filter": "", |
|
| 47 |
+ "filters": "", |
|
| 48 |
+ }, |
|
| 49 |
+ }, |
|
| 50 |
+ {
|
|
| 51 |
+ filters: danglingFilters, |
|
| 52 |
+ expectedQueryParams: map[string]string{
|
|
| 53 |
+ "until": "", |
|
| 54 |
+ "filter": "", |
|
| 55 |
+ "filters": `{"dangling":{"true":true}}`,
|
|
| 56 |
+ }, |
|
| 57 |
+ }, |
|
| 58 |
+ {
|
|
| 59 |
+ filters: noDanglingFilters, |
|
| 60 |
+ expectedQueryParams: map[string]string{
|
|
| 61 |
+ "until": "", |
|
| 62 |
+ "filter": "", |
|
| 63 |
+ "filters": `{"dangling":{"false":true}}`,
|
|
| 64 |
+ }, |
|
| 65 |
+ }, |
|
| 66 |
+ } |
|
| 67 |
+ for _, listCase := range listCases {
|
|
| 68 |
+ client := &Client{
|
|
| 69 |
+ client: newMockClient(func(req *http.Request) (*http.Response, error) {
|
|
| 70 |
+ if !strings.HasPrefix(req.URL.Path, expectedURL) {
|
|
| 71 |
+ return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
|
|
| 72 |
+ } |
|
| 73 |
+ query := req.URL.Query() |
|
| 74 |
+ for key, expected := range listCase.expectedQueryParams {
|
|
| 75 |
+ actual := query.Get(key) |
|
| 76 |
+ assert.Equal(t, actual, expected) |
|
| 77 |
+ } |
|
| 78 |
+ content, err := json.Marshal(types.ImagesPruneReport{
|
|
| 79 |
+ ImagesDeleted: []types.ImageDelete{
|
|
| 80 |
+ {
|
|
| 81 |
+ Deleted: "image_id1", |
|
| 82 |
+ }, |
|
| 83 |
+ {
|
|
| 84 |
+ Deleted: "image_id2", |
|
| 85 |
+ }, |
|
| 86 |
+ }, |
|
| 87 |
+ SpaceReclaimed: 9999, |
|
| 88 |
+ }) |
|
| 89 |
+ if err != nil {
|
|
| 90 |
+ return nil, err |
|
| 91 |
+ } |
|
| 92 |
+ return &http.Response{
|
|
| 93 |
+ StatusCode: http.StatusOK, |
|
| 94 |
+ Body: ioutil.NopCloser(bytes.NewReader(content)), |
|
| 95 |
+ }, nil |
|
| 96 |
+ }), |
|
| 97 |
+ version: "1.25", |
|
| 98 |
+ } |
|
| 99 |
+ |
|
| 100 |
+ report, err := client.ImagesPrune(context.Background(), listCase.filters) |
|
| 101 |
+ assert.NilError(t, err) |
|
| 102 |
+ assert.Equal(t, len(report.ImagesDeleted), 2) |
|
| 103 |
+ assert.Equal(t, report.SpaceReclaimed, uint64(9999)) |
|
| 104 |
+ } |
|
| 105 |
+} |
| 0 | 106 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,99 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "bytes" |
|
| 4 |
+ "encoding/json" |
|
| 5 |
+ "fmt" |
|
| 6 |
+ "io/ioutil" |
|
| 7 |
+ "net/http" |
|
| 8 |
+ "strings" |
|
| 9 |
+ "testing" |
|
| 10 |
+ |
|
| 11 |
+ "github.com/docker/docker/api/types" |
|
| 12 |
+ "github.com/docker/docker/api/types/filters" |
|
| 13 |
+ "github.com/docker/docker/pkg/testutil/assert" |
|
| 14 |
+ "golang.org/x/net/context" |
|
| 15 |
+) |
|
| 16 |
+ |
|
| 17 |
+func TestNetworksPruneError(t *testing.T) {
|
|
| 18 |
+ client := &Client{
|
|
| 19 |
+ client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")), |
|
| 20 |
+ version: "1.25", |
|
| 21 |
+ } |
|
| 22 |
+ |
|
| 23 |
+ filters := filters.NewArgs() |
|
| 24 |
+ |
|
| 25 |
+ _, err := client.NetworksPrune(context.Background(), filters) |
|
| 26 |
+ if err == nil || err.Error() != "Error response from daemon: Server error" {
|
|
| 27 |
+ t.Fatalf("expected a Server Error, got %v", err)
|
|
| 28 |
+ } |
|
| 29 |
+} |
|
| 30 |
+ |
|
| 31 |
+func TestNetworksPrune(t *testing.T) {
|
|
| 32 |
+ expectedURL := "/v1.25/networks/prune" |
|
| 33 |
+ |
|
| 34 |
+ danglingFilters := filters.NewArgs() |
|
| 35 |
+ danglingFilters.Add("dangling", "true")
|
|
| 36 |
+ |
|
| 37 |
+ noDanglingFilters := filters.NewArgs() |
|
| 38 |
+ noDanglingFilters.Add("dangling", "false")
|
|
| 39 |
+ |
|
| 40 |
+ listCases := []struct {
|
|
| 41 |
+ filters filters.Args |
|
| 42 |
+ expectedQueryParams map[string]string |
|
| 43 |
+ }{
|
|
| 44 |
+ {
|
|
| 45 |
+ filters: filters.Args{},
|
|
| 46 |
+ expectedQueryParams: map[string]string{
|
|
| 47 |
+ "until": "", |
|
| 48 |
+ "filter": "", |
|
| 49 |
+ "filters": "", |
|
| 50 |
+ }, |
|
| 51 |
+ }, |
|
| 52 |
+ {
|
|
| 53 |
+ filters: danglingFilters, |
|
| 54 |
+ expectedQueryParams: map[string]string{
|
|
| 55 |
+ "until": "", |
|
| 56 |
+ "filter": "", |
|
| 57 |
+ "filters": `{"dangling":{"true":true}}`,
|
|
| 58 |
+ }, |
|
| 59 |
+ }, |
|
| 60 |
+ {
|
|
| 61 |
+ filters: noDanglingFilters, |
|
| 62 |
+ expectedQueryParams: map[string]string{
|
|
| 63 |
+ "until": "", |
|
| 64 |
+ "filter": "", |
|
| 65 |
+ "filters": `{"dangling":{"false":true}}`,
|
|
| 66 |
+ }, |
|
| 67 |
+ }, |
|
| 68 |
+ } |
|
| 69 |
+ for _, listCase := range listCases {
|
|
| 70 |
+ client := &Client{
|
|
| 71 |
+ client: newMockClient(func(req *http.Request) (*http.Response, error) {
|
|
| 72 |
+ if !strings.HasPrefix(req.URL.Path, expectedURL) {
|
|
| 73 |
+ return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
|
|
| 74 |
+ } |
|
| 75 |
+ query := req.URL.Query() |
|
| 76 |
+ for key, expected := range listCase.expectedQueryParams {
|
|
| 77 |
+ actual := query.Get(key) |
|
| 78 |
+ assert.Equal(t, actual, expected) |
|
| 79 |
+ } |
|
| 80 |
+ content, err := json.Marshal(types.NetworksPruneReport{
|
|
| 81 |
+ NetworksDeleted: []string{"network_id1", "network_id2"},
|
|
| 82 |
+ }) |
|
| 83 |
+ if err != nil {
|
|
| 84 |
+ return nil, err |
|
| 85 |
+ } |
|
| 86 |
+ return &http.Response{
|
|
| 87 |
+ StatusCode: http.StatusOK, |
|
| 88 |
+ Body: ioutil.NopCloser(bytes.NewReader(content)), |
|
| 89 |
+ }, nil |
|
| 90 |
+ }), |
|
| 91 |
+ version: "1.25", |
|
| 92 |
+ } |
|
| 93 |
+ |
|
| 94 |
+ report, err := client.NetworksPrune(context.Background(), listCase.filters) |
|
| 95 |
+ assert.NilError(t, err) |
|
| 96 |
+ assert.Equal(t, len(report.NetworksDeleted), 2) |
|
| 97 |
+ } |
|
| 98 |
+} |
| ... | ... |
@@ -3,11 +3,13 @@ package daemon |
| 3 | 3 |
import ( |
| 4 | 4 |
"fmt" |
| 5 | 5 |
"regexp" |
| 6 |
+ "time" |
|
| 6 | 7 |
|
| 7 | 8 |
"github.com/Sirupsen/logrus" |
| 8 | 9 |
"github.com/docker/distribution/digest" |
| 9 | 10 |
"github.com/docker/docker/api/types" |
| 10 | 11 |
"github.com/docker/docker/api/types/filters" |
| 12 |
+ timetypes "github.com/docker/docker/api/types/time" |
|
| 11 | 13 |
"github.com/docker/docker/image" |
| 12 | 14 |
"github.com/docker/docker/layer" |
| 13 | 15 |
"github.com/docker/docker/pkg/directory" |
| ... | ... |
@@ -21,9 +23,17 @@ import ( |
| 21 | 21 |
func (daemon *Daemon) ContainersPrune(pruneFilters filters.Args) (*types.ContainersPruneReport, error) {
|
| 22 | 22 |
rep := &types.ContainersPruneReport{}
|
| 23 | 23 |
|
| 24 |
+ until, err := getUntilFromPruneFilters(pruneFilters) |
|
| 25 |
+ if err != nil {
|
|
| 26 |
+ return nil, err |
|
| 27 |
+ } |
|
| 28 |
+ |
|
| 24 | 29 |
allContainers := daemon.List() |
| 25 | 30 |
for _, c := range allContainers {
|
| 26 | 31 |
if !c.IsRunning() {
|
| 32 |
+ if !until.IsZero() && c.Created.After(until) {
|
|
| 33 |
+ continue |
|
| 34 |
+ } |
|
| 27 | 35 |
cSize, _ := daemon.getSize(c) |
| 28 | 36 |
// TODO: sets RmLink to true? |
| 29 | 37 |
err := daemon.ContainerRm(c.ID, &types.ContainerRmConfig{})
|
| ... | ... |
@@ -84,6 +94,11 @@ func (daemon *Daemon) ImagesPrune(pruneFilters filters.Args) (*types.ImagesPrune |
| 84 | 84 |
} |
| 85 | 85 |
} |
| 86 | 86 |
|
| 87 |
+ until, err := getUntilFromPruneFilters(pruneFilters) |
|
| 88 |
+ if err != nil {
|
|
| 89 |
+ return nil, err |
|
| 90 |
+ } |
|
| 91 |
+ |
|
| 87 | 92 |
var allImages map[image.ID]*image.Image |
| 88 | 93 |
if danglingOnly {
|
| 89 | 94 |
allImages = daemon.imageStore.Heads() |
| ... | ... |
@@ -104,6 +119,9 @@ func (daemon *Daemon) ImagesPrune(pruneFilters filters.Args) (*types.ImagesPrune |
| 104 | 104 |
if len(daemon.referenceStore.References(dgst)) == 0 && len(daemon.imageStore.Children(id)) != 0 {
|
| 105 | 105 |
continue |
| 106 | 106 |
} |
| 107 |
+ if !until.IsZero() && img.Created.After(until) {
|
|
| 108 |
+ continue |
|
| 109 |
+ } |
|
| 107 | 110 |
topImages[id] = img |
| 108 | 111 |
} |
| 109 | 112 |
|
| ... | ... |
@@ -169,9 +187,17 @@ func (daemon *Daemon) ImagesPrune(pruneFilters filters.Args) (*types.ImagesPrune |
| 169 | 169 |
// localNetworksPrune removes unused local networks |
| 170 | 170 |
func (daemon *Daemon) localNetworksPrune(pruneFilters filters.Args) (*types.NetworksPruneReport, error) {
|
| 171 | 171 |
rep := &types.NetworksPruneReport{}
|
| 172 |
- var err error |
|
| 172 |
+ |
|
| 173 |
+ until, err := getUntilFromPruneFilters(pruneFilters) |
|
| 174 |
+ if err != nil {
|
|
| 175 |
+ return rep, err |
|
| 176 |
+ } |
|
| 177 |
+ |
|
| 173 | 178 |
// When the function returns true, the walk will stop. |
| 174 | 179 |
l := func(nw libnetwork.Network) bool {
|
| 180 |
+ if !until.IsZero() && nw.Info().Created().After(until) {
|
|
| 181 |
+ return false |
|
| 182 |
+ } |
|
| 175 | 183 |
nwName := nw.Name() |
| 176 | 184 |
predefined := runconfig.IsPreDefinedNetwork(nwName) |
| 177 | 185 |
if !predefined && len(nw.Endpoints()) == 0 {
|
| ... | ... |
@@ -190,6 +216,12 @@ func (daemon *Daemon) localNetworksPrune(pruneFilters filters.Args) (*types.Netw |
| 190 | 190 |
// clusterNetworksPrune removes unused cluster networks |
| 191 | 191 |
func (daemon *Daemon) clusterNetworksPrune(pruneFilters filters.Args) (*types.NetworksPruneReport, error) {
|
| 192 | 192 |
rep := &types.NetworksPruneReport{}
|
| 193 |
+ |
|
| 194 |
+ until, err := getUntilFromPruneFilters(pruneFilters) |
|
| 195 |
+ if err != nil {
|
|
| 196 |
+ return nil, err |
|
| 197 |
+ } |
|
| 198 |
+ |
|
| 193 | 199 |
cluster := daemon.GetCluster() |
| 194 | 200 |
networks, err := cluster.GetNetworks() |
| 195 | 201 |
if err != nil {
|
| ... | ... |
@@ -200,6 +232,9 @@ func (daemon *Daemon) clusterNetworksPrune(pruneFilters filters.Args) (*types.Ne |
| 200 | 200 |
if nw.Name == "ingress" {
|
| 201 | 201 |
continue |
| 202 | 202 |
} |
| 203 |
+ if !until.IsZero() && nw.Created.After(until) {
|
|
| 204 |
+ continue |
|
| 205 |
+ } |
|
| 203 | 206 |
// https://github.com/docker/docker/issues/24186 |
| 204 | 207 |
// `docker network inspect` unfortunately displays ONLY those containers that are local to that node. |
| 205 | 208 |
// So we try to remove it anyway and check the error |
| ... | ... |
@@ -234,3 +269,24 @@ func (daemon *Daemon) NetworksPrune(pruneFilters filters.Args) (*types.NetworksP |
| 234 | 234 |
} |
| 235 | 235 |
return rep, err |
| 236 | 236 |
} |
| 237 |
+ |
|
| 238 |
+func getUntilFromPruneFilters(pruneFilters filters.Args) (time.Time, error) {
|
|
| 239 |
+ until := time.Time{}
|
|
| 240 |
+ if !pruneFilters.Include("until") {
|
|
| 241 |
+ return until, nil |
|
| 242 |
+ } |
|
| 243 |
+ untilFilters := pruneFilters.Get("until")
|
|
| 244 |
+ if len(untilFilters) > 1 {
|
|
| 245 |
+ return until, fmt.Errorf("more than one until filter specified")
|
|
| 246 |
+ } |
|
| 247 |
+ ts, err := timetypes.GetTimestamp(untilFilters[0], time.Now()) |
|
| 248 |
+ if err != nil {
|
|
| 249 |
+ return until, err |
|
| 250 |
+ } |
|
| 251 |
+ seconds, nanoseconds, err := timetypes.ParseTimestamps(ts, 0) |
|
| 252 |
+ if err != nil {
|
|
| 253 |
+ return until, err |
|
| 254 |
+ } |
|
| 255 |
+ until = time.Unix(seconds, nanoseconds) |
|
| 256 |
+ return until, nil |
|
| 257 |
+} |
| ... | ... |
@@ -21,8 +21,10 @@ Usage: docker container prune [OPTIONS] |
| 21 | 21 |
Remove all stopped containers |
| 22 | 22 |
|
| 23 | 23 |
Options: |
| 24 |
- -f, --force Do not prompt for confirmation |
|
| 25 |
- --help Print usage |
|
| 24 |
+Options: |
|
| 25 |
+ --filter filter Provide filter values (e.g. 'until=<timestamp>') |
|
| 26 |
+ -f, --force Do not prompt for confirmation |
|
| 27 |
+ --help Print usage |
|
| 26 | 28 |
``` |
| 27 | 29 |
|
| 28 | 30 |
## Examples |
| ... | ... |
@@ -38,6 +40,63 @@ f98f9c2aa1eaf727e4ec9c0283bc7d4aa4762fbdba7f26191f26c97f64090360 |
| 38 | 38 |
Total reclaimed space: 212 B |
| 39 | 39 |
``` |
| 40 | 40 |
|
| 41 |
+## Filtering |
|
| 42 |
+ |
|
| 43 |
+The filtering flag (`-f` or `--filter`) format is of "key=value". If there is more |
|
| 44 |
+than one filter, then pass multiple flags (e.g., `--filter "foo=bar" --filter "bif=baz"`) |
|
| 45 |
+ |
|
| 46 |
+The currently supported filters are: |
|
| 47 |
+ |
|
| 48 |
+* until (`<timestamp>`) - only remove containers created before given timestamp |
|
| 49 |
+ |
|
| 50 |
+The `until` filter can be Unix timestamps, date formatted |
|
| 51 |
+timestamps, or Go duration strings (e.g. `10m`, `1h30m`) computed |
|
| 52 |
+relative to the daemon machine’s time. Supported formats for date |
|
| 53 |
+formatted time stamps include RFC3339Nano, RFC3339, `2006-01-02T15:04:05`, |
|
| 54 |
+`2006-01-02T15:04:05.999999999`, `2006-01-02Z07:00`, and `2006-01-02`. The local |
|
| 55 |
+timezone on the daemon will be used if you do not provide either a `Z` or a |
|
| 56 |
+`+-00:00` timezone offset at the end of the timestamp. When providing Unix |
|
| 57 |
+timestamps enter seconds[.nanoseconds], where seconds is the number of seconds |
|
| 58 |
+that have elapsed since January 1, 1970 (midnight UTC/GMT), not counting leap |
|
| 59 |
+seconds (aka Unix epoch or Unix time), and the optional .nanoseconds field is a |
|
| 60 |
+fraction of a second no more than nine digits long. |
|
| 61 |
+ |
|
| 62 |
+The following removes containers created more than 5 minutes ago: |
|
| 63 |
+```bash |
|
| 64 |
+$ docker ps -a --format 'table {{.ID}}\t{{.Image}}\t{{.Command}}\t{{.CreatedAt}}\t{{.Status}}'
|
|
| 65 |
+CONTAINER ID IMAGE COMMAND CREATED AT STATUS |
|
| 66 |
+61b9efa71024 busybox "sh" 2017-01-04 13:23:33 -0800 PST Exited (0) 41 seconds ago |
|
| 67 |
+53a9bc23a516 busybox "sh" 2017-01-04 13:11:59 -0800 PST Exited (0) 12 minutes ago |
|
| 68 |
+ |
|
| 69 |
+$ docker container prune --force --filter "until=5m" |
|
| 70 |
+Deleted Containers: |
|
| 71 |
+53a9bc23a5168b6caa2bfbefddf1b30f93c7ad57f3dec271fd32707497cb9369 |
|
| 72 |
+ |
|
| 73 |
+Total reclaimed space: 25 B |
|
| 74 |
+ |
|
| 75 |
+$ docker ps -a --format 'table {{.ID}}\t{{.Image}}\t{{.Command}}\t{{.CreatedAt}}\t{{.Status}}'
|
|
| 76 |
+CONTAINER ID IMAGE COMMAND CREATED AT STATUS |
|
| 77 |
+61b9efa71024 busybox "sh" 2017-01-04 13:23:33 -0800 PST Exited (0) 44 seconds ago |
|
| 78 |
+``` |
|
| 79 |
+ |
|
| 80 |
+The following removes containers created before `2017-01-04T13:10:00`: |
|
| 81 |
+```bash |
|
| 82 |
+$ docker ps -a --format 'table {{.ID}}\t{{.Image}}\t{{.Command}}\t{{.CreatedAt}}\t{{.Status}}'
|
|
| 83 |
+CONTAINER ID IMAGE COMMAND CREATED AT STATUS |
|
| 84 |
+53a9bc23a516 busybox "sh" 2017-01-04 13:11:59 -0800 PST Exited (0) 7 minutes ago |
|
| 85 |
+4a75091a6d61 busybox "sh" 2017-01-04 13:09:53 -0800 PST Exited (0) 9 minutes ago |
|
| 86 |
+ |
|
| 87 |
+$ docker container prune --force --filter "until=2017-01-04T13:10:00" |
|
| 88 |
+Deleted Containers: |
|
| 89 |
+4a75091a6d618526fcd8b33ccd6e5928ca2a64415466f768a6180004b0c72c6c |
|
| 90 |
+ |
|
| 91 |
+Total reclaimed space: 27 B |
|
| 92 |
+ |
|
| 93 |
+$ docker ps -a --format 'table {{.ID}}\t{{.Image}}\t{{.Command}}\t{{.CreatedAt}}\t{{.Status}}'
|
|
| 94 |
+CONTAINER ID IMAGE COMMAND CREATED AT STATUS |
|
| 95 |
+53a9bc23a516 busybox "sh" 2017-01-04 13:11:59 -0800 PST Exited (0) 9 minutes ago |
|
| 96 |
+``` |
|
| 97 |
+ |
|
| 41 | 98 |
## Related information |
| 42 | 99 |
|
| 43 | 100 |
* [system df](system_df.md) |
| ... | ... |
@@ -21,9 +21,10 @@ Usage: docker image prune [OPTIONS] |
| 21 | 21 |
Remove unused images |
| 22 | 22 |
|
| 23 | 23 |
Options: |
| 24 |
- -a, --all Remove all unused images, not just dangling ones |
|
| 25 |
- -f, --force Do not prompt for confirmation |
|
| 26 |
- --help Print usage |
|
| 24 |
+ -a, --all Remove all unused images, not just dangling ones |
|
| 25 |
+ --filter filter Provide filter values (e.g. 'until=<timestamp>') |
|
| 26 |
+ -f, --force Do not prompt for confirmation |
|
| 27 |
+ --help Print usage |
|
| 27 | 28 |
``` |
| 28 | 29 |
|
| 29 | 30 |
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 |
| 62 | 62 |
Total reclaimed space: 16.43 MB |
| 63 | 63 |
``` |
| 64 | 64 |
|
| 65 |
+## Filtering |
|
| 66 |
+ |
|
| 67 |
+The filtering flag (`-f` or `--filter`) format is of "key=value". If there is more |
|
| 68 |
+than one filter, then pass multiple flags (e.g., `--filter "foo=bar" --filter "bif=baz"`) |
|
| 69 |
+ |
|
| 70 |
+The currently supported filters are: |
|
| 71 |
+ |
|
| 72 |
+* until (`<timestamp>`) - only remove images created before given timestamp |
|
| 73 |
+ |
|
| 74 |
+The `until` filter can be Unix timestamps, date formatted |
|
| 75 |
+timestamps, or Go duration strings (e.g. `10m`, `1h30m`) computed |
|
| 76 |
+relative to the daemon machine’s time. Supported formats for date |
|
| 77 |
+formatted time stamps include RFC3339Nano, RFC3339, `2006-01-02T15:04:05`, |
|
| 78 |
+`2006-01-02T15:04:05.999999999`, `2006-01-02Z07:00`, and `2006-01-02`. The local |
|
| 79 |
+timezone on the daemon will be used if you do not provide either a `Z` or a |
|
| 80 |
+`+-00:00` timezone offset at the end of the timestamp. When providing Unix |
|
| 81 |
+timestamps enter seconds[.nanoseconds], where seconds is the number of seconds |
|
| 82 |
+that have elapsed since January 1, 1970 (midnight UTC/GMT), not counting leap |
|
| 83 |
+seconds (aka Unix epoch or Unix time), and the optional .nanoseconds field is a |
|
| 84 |
+fraction of a second no more than nine digits long. |
|
| 85 |
+ |
|
| 86 |
+The following removes images created before `2017-01-04T00:00:00`: |
|
| 87 |
+```bash |
|
| 88 |
+$ docker images --format 'table {{.Repository}}\t{{.Tag}}\t{{.ID}}\t{{.CreatedAt}}\t{{.Size}}'
|
|
| 89 |
+REPOSITORY TAG IMAGE ID CREATED AT SIZE |
|
| 90 |
+foo latest 2f287ac753da 2017-01-04 13:42:23 -0800 PST 3.98 MB |
|
| 91 |
+alpine latest 88e169ea8f46 2016-12-27 10:17:25 -0800 PST 3.98 MB |
|
| 92 |
+busybox latest e02e811dd08f 2016-10-07 14:03:58 -0700 PDT 1.09 MB |
|
| 93 |
+ |
|
| 94 |
+$ docker image prune -a --force --filter "until=2017-01-04T00:00:00" |
|
| 95 |
+Deleted Images: |
|
| 96 |
+untagged: alpine:latest |
|
| 97 |
+untagged: alpine@sha256:dfbd4a3a8ebca874ebd2474f044a0b33600d4523d03b0df76e5c5986cb02d7e8 |
|
| 98 |
+untagged: busybox:latest |
|
| 99 |
+untagged: busybox@sha256:29f5d56d12684887bdfa50dcd29fc31eea4aaf4ad3bec43daf19026a7ce69912 |
|
| 100 |
+deleted: sha256:e02e811dd08fd49e7f6032625495118e63f597eb150403d02e3238af1df240ba |
|
| 101 |
+deleted: sha256:e88b3f82283bc59d5e0df427c824e9f95557e661fcb0ea15fb0fb6f97760f9d9 |
|
| 102 |
+ |
|
| 103 |
+Total reclaimed space: 1.093 MB |
|
| 104 |
+ |
|
| 105 |
+$ docker images --format 'table {{.Repository}}\t{{.Tag}}\t{{.ID}}\t{{.CreatedAt}}\t{{.Size}}'
|
|
| 106 |
+REPOSITORY TAG IMAGE ID CREATED AT SIZE |
|
| 107 |
+foo latest 2f287ac753da 2017-01-04 13:42:23 -0800 PST 3.98 MB |
|
| 108 |
+``` |
|
| 109 |
+ |
|
| 110 |
+The following removes images created more than 10 days (`240h`) ago: |
|
| 111 |
+```bash |
|
| 112 |
+$ docker images |
|
| 113 |
+REPOSITORY TAG IMAGE ID CREATED SIZE |
|
| 114 |
+foo latest 2f287ac753da 14 seconds ago 3.98 MB |
|
| 115 |
+alpine latest 88e169ea8f46 8 days ago 3.98 MB |
|
| 116 |
+debian jessie 7b0a06c805e8 2 months ago 123 MB |
|
| 117 |
+busybox latest e02e811dd08f 2 months ago 1.09 MB |
|
| 118 |
+golang 1.7.0 138c2e655421 4 months ago 670 MB |
|
| 119 |
+ |
|
| 120 |
+$ docker image prune -a --force --filter "until=240h" |
|
| 121 |
+Deleted Images: |
|
| 122 |
+untagged: golang:1.7.0 |
|
| 123 |
+untagged: golang@sha256:6765038c2b8f407fd6e3ecea043b44580c229ccfa2a13f6d85866cf2b4a9628e |
|
| 124 |
+deleted: sha256:138c2e6554219de65614d88c15521bfb2da674cbb0bf840de161f89ff4264b96 |
|
| 125 |
+deleted: sha256:ec353c2e1a673f456c4b78906d0d77f9d9456cfb5229b78c6a960bfb7496b76a |
|
| 126 |
+deleted: sha256:fe22765feaf3907526b4921c73ea6643ff9e334497c9b7e177972cf22f68ee93 |
|
| 127 |
+deleted: sha256:ff845959c80148421a5c3ae11cc0e6c115f950c89bc949646be55ed18d6a2912 |
|
| 128 |
+deleted: sha256:a4320831346648c03db64149eafc83092e2b34ab50ca6e8c13112388f25899a7 |
|
| 129 |
+deleted: sha256:4c76020202ee1d9709e703b7c6de367b325139e74eebd6b55b30a63c196abaf3 |
|
| 130 |
+deleted: sha256:d7afd92fb07236c8a2045715a86b7d5f0066cef025018cd3ca9a45498c51d1d6 |
|
| 131 |
+deleted: sha256:9e63c5bce4585dd7038d830a1f1f4e44cb1a1515b00e620ac718e934b484c938 |
|
| 132 |
+untagged: debian:jessie |
|
| 133 |
+untagged: debian@sha256:c1af755d300d0c65bb1194d24bce561d70c98a54fb5ce5b1693beb4f7988272f |
|
| 134 |
+deleted: sha256:7b0a06c805e8f23807fb8856621c60851727e85c7bcb751012c813f122734c8d |
|
| 135 |
+deleted: sha256:f96222d75c5563900bc4dd852179b720a0885de8f7a0619ba0ac76e92542bbc8 |
|
| 136 |
+ |
|
| 137 |
+Total reclaimed space: 792.6 MB |
|
| 138 |
+ |
|
| 139 |
+$ docker images |
|
| 140 |
+REPOSITORY TAG IMAGE ID CREATED SIZE |
|
| 141 |
+foo latest 2f287ac753da About a minute ago 3.98 MB |
|
| 142 |
+alpine latest 88e169ea8f46 8 days ago 3.98 MB |
|
| 143 |
+busybox latest e02e811dd08f 2 months ago 1.09 MB |
|
| 144 |
+``` |
|
| 145 |
+ |
|
| 65 | 146 |
## Related information |
| 66 | 147 |
|
| 67 | 148 |
* [system df](system_df.md) |
| ... | ... |
@@ -12,8 +12,9 @@ Usage: docker network prune [OPTIONS] |
| 12 | 12 |
Remove all unused networks |
| 13 | 13 |
|
| 14 | 14 |
Options: |
| 15 |
- -f, --force Do not prompt for confirmation |
|
| 16 |
- --help Print usage |
|
| 15 |
+ --filter filter Provide filter values (e.g. 'until=<timestamp>') |
|
| 16 |
+ -f, --force Do not prompt for confirmation |
|
| 17 |
+ --help Print usage |
|
| 17 | 18 |
``` |
| 18 | 19 |
|
| 19 | 20 |
Remove all unused networks. Unused networks are those which are not referenced by any containers. |
| ... | ... |
@@ -29,6 +30,51 @@ n1 |
| 29 | 29 |
n2 |
| 30 | 30 |
``` |
| 31 | 31 |
|
| 32 |
+## Filtering |
|
| 33 |
+ |
|
| 34 |
+The filtering flag (`-f` or `--filter`) format is of "key=value". If there is more |
|
| 35 |
+than one filter, then pass multiple flags (e.g., `--filter "foo=bar" --filter "bif=baz"`) |
|
| 36 |
+ |
|
| 37 |
+The currently supported filters are: |
|
| 38 |
+ |
|
| 39 |
+* until (`<timestamp>`) - only remove networks created before given timestamp |
|
| 40 |
+ |
|
| 41 |
+The `until` filter can be Unix timestamps, date formatted |
|
| 42 |
+timestamps, or Go duration strings (e.g. `10m`, `1h30m`) computed |
|
| 43 |
+relative to the daemon machine’s time. Supported formats for date |
|
| 44 |
+formatted time stamps include RFC3339Nano, RFC3339, `2006-01-02T15:04:05`, |
|
| 45 |
+`2006-01-02T15:04:05.999999999`, `2006-01-02Z07:00`, and `2006-01-02`. The local |
|
| 46 |
+timezone on the daemon will be used if you do not provide either a `Z` or a |
|
| 47 |
+`+-00:00` timezone offset at the end of the timestamp. When providing Unix |
|
| 48 |
+timestamps enter seconds[.nanoseconds], where seconds is the number of seconds |
|
| 49 |
+that have elapsed since January 1, 1970 (midnight UTC/GMT), not counting leap |
|
| 50 |
+seconds (aka Unix epoch or Unix time), and the optional .nanoseconds field is a |
|
| 51 |
+fraction of a second no more than nine digits long. |
|
| 52 |
+ |
|
| 53 |
+The following removes networks created more than 5 minutes ago. Note that |
|
| 54 |
+system networks such as `bridge`, `host`, and `none` will never be pruned: |
|
| 55 |
+ |
|
| 56 |
+```bash |
|
| 57 |
+$ docker network ls |
|
| 58 |
+NETWORK ID NAME DRIVER SCOPE |
|
| 59 |
+7430df902d7a bridge bridge local |
|
| 60 |
+ea92373fd499 foo-1-day-ago bridge local |
|
| 61 |
+ab53663ed3c7 foo-1-min-ago bridge local |
|
| 62 |
+97b91972bc3b host host local |
|
| 63 |
+f949d337b1f5 none null local |
|
| 64 |
+ |
|
| 65 |
+$ docker network prune --force --filter until=5m |
|
| 66 |
+Deleted Networks: |
|
| 67 |
+foo-1-day-ago |
|
| 68 |
+ |
|
| 69 |
+$ docker network ls |
|
| 70 |
+NETWORK ID NAME DRIVER SCOPE |
|
| 71 |
+7430df902d7a bridge bridge local |
|
| 72 |
+ab53663ed3c7 foo-1-min-ago bridge local |
|
| 73 |
+97b91972bc3b host host local |
|
| 74 |
+f949d337b1f5 none null local |
|
| 75 |
+``` |
|
| 76 |
+ |
|
| 32 | 77 |
## Related information |
| 33 | 78 |
|
| 34 | 79 |
* [network disconnect ](network_disconnect.md) |
| ... | ... |
@@ -21,9 +21,10 @@ Usage: docker system prune [OPTIONS] |
| 21 | 21 |
Delete unused data |
| 22 | 22 |
|
| 23 | 23 |
Options: |
| 24 |
- -a, --all Remove all unused data not just dangling ones |
|
| 25 |
- -f, --force Do not prompt for confirmation |
|
| 26 |
- --help Print usage |
|
| 24 |
+ -a, --all Remove all unused images not just dangling ones |
|
| 25 |
+ --filter filter Provide filter values (e.g. 'until=<timestamp>') |
|
| 26 |
+ -f, --force Do not prompt for confirmation |
|
| 27 |
+ --help Print usage |
|
| 27 | 28 |
``` |
| 28 | 29 |
|
| 29 | 30 |
Remove all unused containers, volumes, networks and images (both dangling and unreferenced). |
| ... | ... |
@@ -64,6 +65,27 @@ deleted: sha256:3a88a5c81eb5c283e72db2dbc6d65cbfd8e80b6c89bb6e714cfaaa0eed99c548 |
| 64 | 64 |
Total reclaimed space: 13.5 MB |
| 65 | 65 |
``` |
| 66 | 66 |
|
| 67 |
+## Filtering |
|
| 68 |
+ |
|
| 69 |
+The filtering flag (`-f` or `--filter`) format is of "key=value". If there is more |
|
| 70 |
+than one filter, then pass multiple flags (e.g., `--filter "foo=bar" --filter "bif=baz"`) |
|
| 71 |
+ |
|
| 72 |
+The currently supported filters are: |
|
| 73 |
+ |
|
| 74 |
+* until (`<timestamp>`) - only remove containers, images, and networks created before given timestamp |
|
| 75 |
+ |
|
| 76 |
+The `until` filter can be Unix timestamps, date formatted |
|
| 77 |
+timestamps, or Go duration strings (e.g. `10m`, `1h30m`) computed |
|
| 78 |
+relative to the daemon machine’s time. Supported formats for date |
|
| 79 |
+formatted time stamps include RFC3339Nano, RFC3339, `2006-01-02T15:04:05`, |
|
| 80 |
+`2006-01-02T15:04:05.999999999`, `2006-01-02Z07:00`, and `2006-01-02`. The local |
|
| 81 |
+timezone on the daemon will be used if you do not provide either a `Z` or a |
|
| 82 |
+`+-00:00` timezone offset at the end of the timestamp. When providing Unix |
|
| 83 |
+timestamps enter seconds[.nanoseconds], where seconds is the number of seconds |
|
| 84 |
+that have elapsed since January 1, 1970 (midnight UTC/GMT), not counting leap |
|
| 85 |
+seconds (aka Unix epoch or Unix time), and the optional .nanoseconds field is a |
|
| 86 |
+fraction of a second no more than nine digits long. |
|
| 87 |
+ |
|
| 67 | 88 |
## Related information |
| 68 | 89 |
|
| 69 | 90 |
* [volume create](volume_create.md) |
| ... | ... |
@@ -5,6 +5,7 @@ package main |
| 5 | 5 |
import ( |
| 6 | 6 |
"strconv" |
| 7 | 7 |
"strings" |
| 8 |
+ "time" |
|
| 8 | 9 |
|
| 9 | 10 |
"github.com/docker/docker/integration-cli/checker" |
| 10 | 11 |
"github.com/docker/docker/integration-cli/daemon" |
| ... | ... |
@@ -90,3 +91,23 @@ func (s *DockerDaemonSuite) TestPruneImageDangling(c *check.C) {
|
| 90 | 90 |
c.Assert(err, checker.IsNil) |
| 91 | 91 |
c.Assert(strings.TrimSpace(out), checker.Not(checker.Contains), id) |
| 92 | 92 |
} |
| 93 |
+ |
|
| 94 |
+func (s *DockerSuite) TestPruneContainerUntil(c *check.C) {
|
|
| 95 |
+ out, _ := dockerCmd(c, "run", "-d", "busybox") |
|
| 96 |
+ id1 := strings.TrimSpace(out) |
|
| 97 |
+ c.Assert(waitExited(id1, 5*time.Second), checker.IsNil) |
|
| 98 |
+ |
|
| 99 |
+ until := daemonUnixTime(c) |
|
| 100 |
+ |
|
| 101 |
+ out, _ = dockerCmd(c, "run", "-d", "busybox") |
|
| 102 |
+ id2 := strings.TrimSpace(out) |
|
| 103 |
+ c.Assert(waitExited(id2, 5*time.Second), checker.IsNil) |
|
| 104 |
+ |
|
| 105 |
+ out, _ = dockerCmd(c, "container", "prune", "--force", "--filter", "until="+until) |
|
| 106 |
+ c.Assert(strings.TrimSpace(out), checker.Contains, id1) |
|
| 107 |
+ c.Assert(strings.TrimSpace(out), checker.Not(checker.Contains), id2) |
|
| 108 |
+ |
|
| 109 |
+ out, _ = dockerCmd(c, "ps", "-a", "-q", "--no-trunc") |
|
| 110 |
+ c.Assert(strings.TrimSpace(out), checker.Not(checker.Contains), id1) |
|
| 111 |
+ c.Assert(strings.TrimSpace(out), checker.Contains, id2) |
|
| 112 |
+} |