Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 62 additions & 5 deletions cmd/compose/down.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import (
"os"
"time"

composecli "github.com/compose-spec/compose-go/v2/cli"
"github.com/compose-spec/compose-go/v2/types"
"github.com/docker/cli/cli/command"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
Expand All @@ -34,6 +36,7 @@ import (

type downOptions struct {
*ProjectOptions
all bool
removeOrphans bool
timeChanged bool
timeout int
Expand All @@ -48,13 +51,34 @@ func downCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *Backe
downCmd := &cobra.Command{
Use: "down [OPTIONS] [SERVICES]",
Short: "Stop and remove containers, networks",
Long: `Stops containers and removes containers, networks, volumes, and images created by up.

By default, the only things removed are:

- Containers for services defined in the Compose file.
- Networks defined in the networks section of the Compose file.
- The default network, if one is used.

Networks and volumes defined as external are never removed.

Anonymous volumes are not removed by default. However, as they don't have a stable name, they are not automatically
mounted by a subsequent up. For data that needs to persist between updates, use explicit paths as bind mounts or
named volumes.

Use --all to remove every resource for the project, including services from inactive profiles and orphan containers.`,
Example: `docker compose down
docker compose down -v --remove-orphans
docker compose down --all -v`,
PreRunE: AdaptCmd(func(ctx context.Context, cmd *cobra.Command, args []string) error {
opts.timeChanged = cmd.Flags().Changed("timeout")
if opts.images != "" {
if opts.images != "all" && opts.images != "local" {
return fmt.Errorf("invalid value for --rmi: %q", opts.images)
}
}
if opts.all && len(args) > 0 {
return fmt.Errorf("cannot combine --all with service arguments")
}
return nil
}),
RunE: Adapt(func(ctx context.Context, args []string) error {
Expand All @@ -63,6 +87,7 @@ func downCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *Backe
ValidArgsFunction: completeServiceNames(dockerCli, p),
}
flags := downCmd.Flags()
flags.BoolVar(&opts.all, "all", false, "Remove all resources for the project, including inactive profile services and orphan containers")
removeOrphans := utils.StringToBool(os.Getenv(ComposeRemoveOrphans))
flags.BoolVar(&opts.removeOrphans, "remove-orphans", removeOrphans, "Remove containers for services not defined in the Compose file")
flags.IntVarP(&opts.timeout, "timeout", "t", 0, "Specify a shutdown timeout in seconds")
Expand All @@ -79,7 +104,12 @@ func downCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *Backe
}

func runDown(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, opts downOptions, services []string) error {
project, name, err := opts.projectOrName(ctx, dockerCli, services...)
backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
if err != nil {
return err
}

project, name, err := getDownProjectOrName(ctx, dockerCli, backend, opts, services)
if err != nil {
return err
}
Expand All @@ -89,11 +119,9 @@ func runDown(ctx context.Context, dockerCli command.Cli, backendOptions *Backend
timeoutValue := time.Duration(opts.timeout) * time.Second
timeout = &timeoutValue
}
backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
if err != nil {
return err
}

return backend.Down(ctx, name, api.DownOptions{
All: opts.all,
RemoveOrphans: opts.removeOrphans,
Project: project,
Timeout: timeout,
Expand All @@ -102,3 +130,32 @@ func runDown(ctx context.Context, dockerCli command.Cli, backendOptions *Backend
Services: services,
})
}

func getDownProjectOrName(ctx context.Context, dockerCli command.Cli, backend api.Compose, opts downOptions, services []string) (*types.Project, string, error) {
if !opts.all {
return opts.projectOrName(ctx, dockerCli, services...)
}

allProjectOpts := *opts.ProjectOptions
allProjectOpts.Profiles = []string{"*"}
allProjectOpts.All = true

project, _, err := allProjectOpts.ToProject(ctx, dockerCli, backend, nil, composecli.WithDiscardEnvFile, composecli.WithoutEnvironmentResolution)
if err == nil {
return project, project.Name, nil
}

if len(allProjectOpts.ConfigPaths) > 0 {
return nil, "", err
}

name := allProjectOpts.ProjectName
if name == "" {
name = os.Getenv(ComposeProjectName)
}
if name != "" {
return nil, name, nil
}

return nil, "", err
}
31 changes: 31 additions & 0 deletions cmd/compose/down_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package compose

import (
"testing"

"gotest.tools/v3/assert"
)

func TestDownCommandHasAllFlag(t *testing.T) {
cmd := downCommand(&ProjectOptions{}, nil, &BackendOptions{})

flag := cmd.Flags().Lookup("all")
assert.Assert(t, flag != nil)
assert.Equal(t, flag.Usage, "Remove all resources for the project, including inactive profile services and orphan containers")
}
13 changes: 13 additions & 0 deletions docs/reference/compose_down.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,21 @@ Anonymous volumes are not removed by default. However, as they don’t have a st
mounted by a subsequent `up`. For data that needs to persist between updates, use explicit paths as bind mounts or
named volumes.

Use `--all` to remove every resource for the project, including services from inactive profiles and orphan containers.

### Examples

```console
$ docker compose down
$ docker compose down -v --remove-orphans
$ docker compose down --all -v
```

### Options

| Name | Type | Default | Description |
|:-------------------|:---------|:--------|:------------------------------------------------------------------------------------------------------------------------|
| `--all` | `bool` | | Remove all resources for the project, including inactive profile services and orphan containers |
| `--dry-run` | `bool` | | Execute command in dry run mode |
| `--remove-orphans` | `bool` | | Remove containers for services not defined in the Compose file |
| `--rmi` | `string` | | Remove images used by services. "local" remove only images that don't have a custom tag ("local"\|"all") |
Expand All @@ -43,3 +54,5 @@ Networks and volumes defined as external are never removed.
Anonymous volumes are not removed by default. However, as they don’t have a stable name, they are not automatically
mounted by a subsequent `up`. For data that needs to persist between updates, use explicit paths as bind mounts or
named volumes.

Use `--all` to remove every resource for the project, including services from inactive profiles and orphan containers.
17 changes: 16 additions & 1 deletion docs/reference/docker_compose_down.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,26 @@ long: |-
Anonymous volumes are not removed by default. However, as they don’t have a stable name, they are not automatically
mounted by a subsequent `up`. For data that needs to persist between updates, use explicit paths as bind mounts or
named volumes.
Use `--all` to remove every resource for the project, including services from inactive profiles and orphan containers.
usage: docker compose down [OPTIONS] [SERVICES]
pname: docker compose
plink: docker_compose.yaml
examples: |-
docker compose down
docker compose down -v --remove-orphans
docker compose down --all -v
options:
- option: all
value_type: bool
default_value: "false"
description: Remove all resources for the project, including inactive profile services and orphan containers
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: remove-orphans
value_type: bool
default_value: "false"
Expand Down Expand Up @@ -78,4 +94,3 @@ experimental: false
experimentalcli: false
kubernetes: false
swarm: false

2 changes: 2 additions & 0 deletions pkg/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,8 @@ type UpOptions struct {

// DownOptions group options of the Down API
type DownOptions struct {
// All performs a complete project cleanup, including inactive profile services and orphan containers
All bool
// RemoveOrphans will cleanup containers that are not declared on the compose model but own the same labels
RemoveOrphans bool
// Project is the compose project used to define this app. Might be nil if user ran `down` just with project name
Expand Down
11 changes: 11 additions & 0 deletions pkg/compose/down.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ func (s *composeService) Down(ctx context.Context, projectName string, options a
func (s *composeService) down(ctx context.Context, projectName string, options api.DownOptions) error { //nolint:gocyclo
resourceToRemove := false

if options.All {
options.RemoveOrphans = true
options.Services = nil
}

include := oneOffExclude
if options.RemoveOrphans {
include = oneOffInclude
Expand All @@ -60,6 +65,12 @@ func (s *composeService) down(ctx context.Context, projectName string, options a
return err
}
}
if options.All {
project, err = project.WithProfiles([]string{"*"})
if err != nil {
return err
}
}

// Check requested services exists in model
services, err := checkSelectedServices(options, project)
Expand Down
76 changes: 76 additions & 0 deletions pkg/compose/down_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,82 @@ func TestDownRemoveVolumes(t *testing.T) {
assert.NilError(t, err)
}

func TestDownAllRemovesInactiveProfileServicesAndOrphans(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()

api, cli := prepareMocks(mockCtrl)
tested, err := NewComposeService(cli)
assert.NilError(t, err)

projectName := strings.ToLower(testProject)
project := &types.Project{
Name: projectName,
Services: types.Services{
"service1": {
Name: "service1",
},
},
DisabledServices: types.Services{
"service2": {
Name: "service2",
Profiles: []string{"manual"},
},
},
Networks: types.Networks{
"default": {
Name: fmt.Sprintf("%s_default", projectName),
},
},
Volumes: types.Volumes{
"data": {
Name: fmt.Sprintf("%s_data", projectName),
},
},
}

api.EXPECT().ContainerList(gomock.Any(), projectFilterListOpt(true)).Return(
client.ContainerListResult{
Items: []container.Summary{
testContainer("service1", "123", false),
testContainer("service2", "456", false),
testContainer("service_orphan", "321", true),
},
}, nil)

stopOptions := client.ContainerStopOptions{}
api.EXPECT().ContainerStop(gomock.Any(), "123", stopOptions).Return(client.ContainerStopResult{}, nil)
api.EXPECT().ContainerStop(gomock.Any(), "456", stopOptions).Return(client.ContainerStopResult{}, nil)
api.EXPECT().ContainerStop(gomock.Any(), "321", stopOptions).Return(client.ContainerStopResult{}, nil)

api.EXPECT().ContainerRemove(gomock.Any(), "123", client.ContainerRemoveOptions{Force: true, RemoveVolumes: true}).Return(client.ContainerRemoveResult{}, nil)
api.EXPECT().ContainerRemove(gomock.Any(), "456", client.ContainerRemoveOptions{Force: true, RemoveVolumes: true}).Return(client.ContainerRemoveResult{}, nil)
api.EXPECT().ContainerRemove(gomock.Any(), "321", client.ContainerRemoveOptions{Force: true}).Return(client.ContainerRemoveResult{}, nil)

api.EXPECT().NetworkList(gomock.Any(), client.NetworkListOptions{
Filters: projectFilter(projectName).Add("label", networkFilter("default")),
}).Return(client.NetworkListResult{
Items: []network.Summary{{Network: network.Network{ID: "abc123", Name: fmt.Sprintf("%s_default", projectName)}}},
}, nil)
api.EXPECT().NetworkInspect(gomock.Any(), "abc123", gomock.Any()).Return(client.NetworkInspectResult{
Network: network.Inspect{Network: network.Network{ID: "abc123"}},
}, nil)
api.EXPECT().NetworkRemove(gomock.Any(), "abc123", gomock.Any()).Return(client.NetworkRemoveResult{}, nil)

api.EXPECT().VolumeInspect(gomock.Any(), fmt.Sprintf("%s_data", projectName), gomock.Any()).
Return(client.VolumeInspectResult{}, nil)
api.EXPECT().VolumeRemove(gomock.Any(), fmt.Sprintf("%s_data", projectName), client.VolumeRemoveOptions{Force: true}).
Return(client.VolumeRemoveResult{}, nil)

err = tested.Down(t.Context(), projectName, compose.DownOptions{
All: true,
Project: project,
Volumes: true,
Services: []string{"service1"},
})
assert.NilError(t, err)
}

func TestDownRemoveImages(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
Expand Down
28 changes: 28 additions & 0 deletions pkg/e2e/fixtures/down-all/compose.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
services:
foo:
image: alpine
command: tail -f /dev/null
stop_grace_period: 1s
networks:
- wipe
volumes:
- shared:/data

bar:
profiles:
- manual
depends_on:
- foo
image: alpine
command: tail -f /dev/null
stop_grace_period: 1s
networks:
- wipe
volumes:
- shared:/data

volumes:
shared:

networks:
wipe:
36 changes: 36 additions & 0 deletions pkg/e2e/profiles_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -205,3 +205,39 @@ func TestDotEnvProfileUsage(t *testing.T) {
res.Assert(t, icmd.Expected{Out: profiledService})
})
}

func TestDownAllRemovesInactiveProfileResources(t *testing.T) {
c := NewParallelCLI(t)
const projectName = "compose-e2e-down-all"
const composeFile = "./fixtures/down-all/compose.yaml"

t.Cleanup(func() {
_ = c.RunDockerComposeCmdNoCheck(t, "-f", composeFile, "-p", projectName, "down", "--all", "-v", "-t", "0")
})

c.RunDockerComposeCmd(t, "-f", composeFile, "-p", projectName, "up", "-d", "bar")

c.RunDockerComposeCmd(t, "-f", composeFile, "-p", projectName, "down", "--remove-orphans", "-v", "-t", "0")

res := c.RunDockerCmd(t, "ps", "--all", "--format", "{{.Names}}")
assert.Assert(t, !strings.Contains(res.Combined(), projectName+"-foo-1"), res.Combined())
assert.Assert(t, strings.Contains(res.Combined(), projectName+"-bar-1"), res.Combined())

res = c.RunDockerCmd(t, "network", "ls")
assert.Assert(t, strings.Contains(res.Combined(), projectName+"_wipe"), res.Combined())

res = c.RunDockerCmd(t, "volume", "ls")
assert.Assert(t, strings.Contains(res.Combined(), projectName+"_shared"), res.Combined())

c.RunDockerComposeCmd(t, "-f", composeFile, "-p", projectName, "down", "--all", "-v", "-t", "0")

res = c.RunDockerCmd(t, "ps", "--all", "--format", "{{.Names}}")
assert.Assert(t, !strings.Contains(res.Combined(), projectName+"-foo-1"), res.Combined())
assert.Assert(t, !strings.Contains(res.Combined(), projectName+"-bar-1"), res.Combined())

res = c.RunDockerCmd(t, "network", "ls")
assert.Assert(t, !strings.Contains(res.Combined(), projectName+"_wipe"), res.Combined())

res = c.RunDockerCmd(t, "volume", "ls")
assert.Assert(t, !strings.Contains(res.Combined(), projectName+"_shared"), res.Combined())
}