diff --git a/aci/compose.go b/aci/compose.go index f9248f12a..c7f82aba8 100644 --- a/aci/compose.go +++ b/aci/compose.go @@ -210,3 +210,7 @@ func (cs *aciComposeService) Kill(ctx context.Context, project *types.Project, o func (cs *aciComposeService) RunOneOffContainer(ctx context.Context, project *types.Project, opts compose.RunOptions) (int, error) { return 0, errdefs.ErrNotImplemented } + +func (cs *aciComposeService) Remove(ctx context.Context, project *types.Project, options compose.RemoveOptions) ([]string, error) { + return nil, errdefs.ErrNotImplemented +} diff --git a/api/client/compose.go b/api/client/compose.go index 5681e7947..063da2081 100644 --- a/api/client/compose.go +++ b/api/client/compose.go @@ -83,3 +83,7 @@ func (c *composeService) Kill(ctx context.Context, project *types.Project, optio func (c *composeService) RunOneOffContainer(ctx context.Context, project *types.Project, opts compose.RunOptions) (int, error) { return 0, errdefs.ErrNotImplemented } + +func (c *composeService) Remove(ctx context.Context, project *types.Project, options compose.RemoveOptions) ([]string, error) { + return nil, errdefs.ErrNotImplemented +} diff --git a/api/compose/api.go b/api/compose/api.go index 5efeb3700..65cbbbe7f 100644 --- a/api/compose/api.go +++ b/api/compose/api.go @@ -53,6 +53,8 @@ type Service interface { Kill(ctx context.Context, project *types.Project, options KillOptions) error // RunOneOffContainer creates a service oneoff container and starts its dependencies RunOneOffContainer(ctx context.Context, project *types.Project, opts RunOptions) (int, error) + // Remove executes the equivalent to a `compose rm` + Remove(ctx context.Context, project *types.Project, options RemoveOptions) ([]string, error) } // CreateOptions group options of the Create API @@ -97,6 +99,16 @@ type KillOptions struct { Signal string } +// RemoveOptions group options of the Remove API +type RemoveOptions struct { + // DryRun just list removable resources + DryRun bool + // Volumes remove anonymous volumes + Volumes bool + // Force don't ask to confirm removal + Force bool +} + // RunOptions options to execute compose run type RunOptions struct { Service string diff --git a/cli/cmd/compose/compose.go b/cli/cmd/compose/compose.go index 360a11b03..4a5c612c6 100644 --- a/cli/cmd/compose/compose.go +++ b/cli/cmd/compose/compose.go @@ -116,6 +116,7 @@ func Command(contextType string) *cobra.Command { convertCommand(&opts), killCommand(&opts), runCommand(&opts), + removeCommand(&opts), ) if contextType == store.LocalContextType || contextType == store.DefaultContextType { diff --git a/cli/cmd/compose/remove.go b/cli/cmd/compose/remove.go new file mode 100644 index 000000000..af5040136 --- /dev/null +++ b/cli/cmd/compose/remove.go @@ -0,0 +1,116 @@ +/* + 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 ( + "context" + "fmt" + "strings" + + "github.com/docker/compose-cli/api/client" + "github.com/docker/compose-cli/api/compose" + "github.com/docker/compose-cli/api/progress" + "github.com/docker/compose-cli/utils/prompt" + + "github.com/spf13/cobra" +) + +type removeOptions struct { + *projectOptions + force bool + stop bool + volumes bool +} + +func removeCommand(p *projectOptions) *cobra.Command { + opts := removeOptions{ + projectOptions: p, + } + cmd := &cobra.Command{ + Use: "rm [SERVICE...]", + Short: "Removes stopped service containers", + Long: `Removes stopped service containers + +By default, anonymous volumes attached to containers will not be removed. You +can override this with -v. To list all volumes, use "docker volume ls". + +Any data which is not in a volume will be lost.`, + RunE: func(cmd *cobra.Command, args []string) error { + return runRemove(cmd.Context(), opts, args) + }, + } + f := cmd.Flags() + f.BoolVarP(&opts.force, "force", "f", false, "Don't ask to confirm removal") + f.BoolVarP(&opts.stop, "stop", "s", false, "Stop the containers, if required, before removing") + f.BoolVarP(&opts.volumes, "volumes", "v", false, "Remove any anonymous volumes attached to containers") + return cmd +} + +func runRemove(ctx context.Context, opts removeOptions, services []string) error { + c, err := client.NewWithDefaultLocalBackend(ctx) + if err != nil { + return err + } + + project, err := opts.toProject(services) + if err != nil { + return err + } + + if opts.stop { + _, err = progress.Run(ctx, func(ctx context.Context) (string, error) { + err := c.ComposeService().Stop(ctx, project) + return "", err + }) + if err != nil { + return err + } + } + + reosurces, err := c.ComposeService().Remove(ctx, project, compose.RemoveOptions{ + DryRun: true, + }) + if err != nil { + return err + } + + if len(reosurces) == 0 { + fmt.Println("No stopped containers") + return nil + } + msg := fmt.Sprintf("Going to remove %s", strings.Join(reosurces, ", ")) + if opts.force { + fmt.Println(msg) + } else { + confirm, err := prompt.User{}.Confirm(msg, false) + if err != nil { + return err + } + if !confirm { + return nil + } + } + + _, err = progress.Run(ctx, func(ctx context.Context) (string, error) { + _, err = c.ComposeService().Remove(ctx, project, compose.RemoveOptions{ + Volumes: opts.volumes, + Force: opts.force, + }) + return "", err + }) + return err +} diff --git a/ecs/local/compose.go b/ecs/local/compose.go index c285e02cc..1cd0c9b60 100644 --- a/ecs/local/compose.go +++ b/ecs/local/compose.go @@ -175,3 +175,7 @@ func (e ecsLocalSimulation) List(ctx context.Context) ([]compose.Stack, error) { func (e ecsLocalSimulation) RunOneOffContainer(ctx context.Context, project *types.Project, opts compose.RunOptions) (int, error) { return 0, errors.Wrap(errdefs.ErrNotImplemented, "use docker-compose run") } + +func (e ecsLocalSimulation) Remove(ctx context.Context, project *types.Project, options compose.RemoveOptions) ([]string, error) { + return e.compose.Remove(ctx, project, options) +} diff --git a/ecs/run.go b/ecs/run.go index f2e7811a1..597f80783 100644 --- a/ecs/run.go +++ b/ecs/run.go @@ -28,3 +28,7 @@ import ( func (b *ecsAPIService) RunOneOffContainer(ctx context.Context, project *types.Project, opts compose.RunOptions) (int, error) { return 0, errdefs.ErrNotImplemented } + +func (b *ecsAPIService) Remove(ctx context.Context, project *types.Project, options compose.RemoveOptions) ([]string, error) { + return nil, errdefs.ErrNotImplemented +} diff --git a/kube/compose.go b/kube/compose.go index a31c8c221..77f1eceea 100644 --- a/kube/compose.go +++ b/kube/compose.go @@ -197,3 +197,7 @@ func (s *composeService) Kill(ctx context.Context, project *types.Project, optio func (s *composeService) RunOneOffContainer(ctx context.Context, project *types.Project, opts compose.RunOptions) (int, error) { return 0, errdefs.ErrNotImplemented } + +func (s *composeService) Remove(ctx context.Context, project *types.Project, options compose.RemoveOptions) ([]string, error) { + return nil, errdefs.ErrNotImplemented +} diff --git a/local/compose/attach.go b/local/compose/attach.go index 3dd1c09a5..9a2ad1ada 100644 --- a/local/compose/attach.go +++ b/local/compose/attach.go @@ -31,7 +31,7 @@ import ( ) func (s *composeService) attach(ctx context.Context, project *types.Project, consumer compose.ContainerEventListener) (Containers, error) { - containers, err := s.getContainers(ctx, project) + containers, err := s.getContainers(ctx, project, oneOffExclude) if err != nil { return nil, err } diff --git a/local/compose/compose.go b/local/compose/compose.go index e05302f7c..038ac615c 100644 --- a/local/compose/compose.go +++ b/local/compose/compose.go @@ -23,18 +23,19 @@ import ( "strings" "github.com/docker/compose-cli/api/compose" + "github.com/docker/compose-cli/api/errdefs" "github.com/compose-spec/compose-go/types" moby "github.com/docker/docker/api/types" "github.com/docker/docker/client" "github.com/sanathkr/go-yaml" - - errdefs2 "github.com/docker/compose-cli/api/errdefs" ) // NewComposeService create a local implementation of the compose.Service API func NewComposeService(apiClient client.APIClient) compose.Service { - return &composeService{apiClient: apiClient} + return &composeService{ + apiClient: apiClient, + } } type composeService struct { @@ -42,7 +43,7 @@ type composeService struct { } func (s *composeService) Up(ctx context.Context, project *types.Project, options compose.UpOptions) error { - return errdefs2.ErrNotImplemented + return errdefs.ErrNotImplemented } func getCanonicalContainerName(c moby.Container) string { diff --git a/local/compose/containers.go b/local/compose/containers.go index c31cd4202..7628aac92 100644 --- a/local/compose/containers.go +++ b/local/compose/containers.go @@ -18,6 +18,7 @@ package compose import ( "context" + "fmt" "sort" "github.com/compose-spec/compose-go/types" @@ -28,13 +29,29 @@ import ( // Containers is a set of moby Container type Containers []moby.Container -func (s *composeService) getContainers(ctx context.Context, project *types.Project) (Containers, error) { +type oneOff int + +const ( + oneOffInclude = oneOff(iota) + oneOffExclude + oneOffOnly +) + +func (s *composeService) getContainers(ctx context.Context, project *types.Project, oneOff oneOff) (Containers, error) { var containers Containers + f := filters.NewArgs( + projectFilter(project.Name), + ) + switch oneOff { + case oneOffOnly: + f.Add("label", fmt.Sprintf("%s=%s", oneoffLabel, "True")) + case oneOffExclude: + f.Add("label", fmt.Sprintf("%s=%s", oneoffLabel, "False")) + case oneOffInclude: + } containers, err := s.apiClient.ContainerList(ctx, moby.ContainerListOptions{ - Filters: filters.NewArgs( - projectFilter(project.Name), - ), - All: true, + Filters: f, + All: true, }) if err != nil { return nil, err diff --git a/local/compose/convergence.go b/local/compose/convergence.go index e93221216..5ba389409 100644 --- a/local/compose/convergence.go +++ b/local/compose/convergence.go @@ -334,6 +334,8 @@ func (s *composeService) startService(ctx context.Context, project *types.Projec if err != nil { return err } + + w := progress.ContextWriter(ctx) eg, ctx := errgroup.WithContext(ctx) for _, c := range containers { container := c @@ -341,7 +343,6 @@ func (s *composeService) startService(ctx context.Context, project *types.Projec continue } eg.Go(func() error { - w := progress.ContextWriter(ctx) eventName := getContainerProgressName(container) w.Event(progress.StartingEvent(eventName)) err := s.apiClient.ContainerStart(ctx, container.ID, moby.ContainerStartOptions{}) diff --git a/local/compose/remove.go b/local/compose/remove.go new file mode 100644 index 000000000..76444eeb6 --- /dev/null +++ b/local/compose/remove.go @@ -0,0 +1,68 @@ +/* + 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 ( + "context" + + "github.com/docker/compose-cli/api/compose" + "github.com/docker/compose-cli/api/progress" + status "github.com/docker/compose-cli/local/moby" + + "github.com/compose-spec/compose-go/types" + moby "github.com/docker/docker/api/types" + "golang.org/x/sync/errgroup" +) + +func (s *composeService) Remove(ctx context.Context, project *types.Project, options compose.RemoveOptions) ([]string, error) { + containers, err := s.getContainers(ctx, project, oneOffInclude) + if err != nil { + return nil, err + } + + stoppedContainers := containers.filter(func(c moby.Container) bool { + return c.State != status.ContainerRunning + }) + + var names []string + stoppedContainers.forEach(func(c moby.Container) { + names = append(names, getCanonicalContainerName(c)) + }) + + if options.DryRun { + return names, nil + } + + w := progress.ContextWriter(ctx) + eg, ctx := errgroup.WithContext(ctx) + for _, c := range stoppedContainers { + c := c + eg.Go(func() error { + eventName := getContainerProgressName(c) + w.Event(progress.RemovingEvent(eventName)) + err = s.apiClient.ContainerRemove(ctx, c.ID, moby.ContainerRemoveOptions{ + RemoveVolumes: options.Volumes, + Force: options.Force, + }) + if err == nil { + w.Event(progress.RemovedEvent(eventName)) + } + return err + }) + } + return nil, eg.Wait() +}