From 20b83aa237438ff893d71268fdf78769ec007dc5 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof Date: Mon, 15 Feb 2021 09:13:41 +0100 Subject: [PATCH 1/2] introduce compose rm command Signed-off-by: Nicolas De Loof --- aci/compose.go | 4 ++ api/client/compose.go | 4 ++ api/compose/api.go | 10 +++++ cli/cmd/compose/compose.go | 1 + cli/cmd/compose/remove.go | 84 ++++++++++++++++++++++++++++++++++++ ecs/local/compose.go | 4 ++ ecs/run.go | 4 ++ kube/compose.go | 4 ++ local/compose/attach.go | 2 +- local/compose/compose.go | 7 ++- local/compose/containers.go | 27 +++++++++--- local/compose/convergence.go | 3 +- local/compose/remove.go | 83 +++++++++++++++++++++++++++++++++++ 13 files changed, 229 insertions(+), 8 deletions(-) create mode 100644 cli/cmd/compose/remove.go create mode 100644 local/compose/remove.go diff --git a/aci/compose.go b/aci/compose.go index f9248f12a..6d3f12c8a 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) error { + return errdefs.ErrNotImplemented +} diff --git a/api/client/compose.go b/api/client/compose.go index 5681e7947..970008e89 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) error { + return errdefs.ErrNotImplemented +} diff --git a/api/compose/api.go b/api/compose/api.go index 5efeb3700..6d1a9a6b9 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) error } // CreateOptions group options of the Create API @@ -97,6 +99,14 @@ type KillOptions struct { Signal string } +// RemoveOptions group options of the Remove API +type RemoveOptions struct { + // 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..ba796ce60 --- /dev/null +++ b/cli/cmd/compose/remove.go @@ -0,0 +1,84 @@ +/* + 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/client" + "github.com/docker/compose-cli/api/compose" + "github.com/docker/compose-cli/api/progress" + + "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 + } + + _, err = progress.Run(ctx, func(ctx context.Context) (string, error) { + if opts.stop { + err := c.ComposeService().Stop(ctx, project) + if err != nil { + return "", err + } + } + return "", c.ComposeService().Remove(ctx, project, compose.RemoveOptions{ + Volumes: opts.volumes, + Force: opts.force, + }) + }) + return err +} diff --git a/ecs/local/compose.go b/ecs/local/compose.go index c285e02cc..06cbbf1fd 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) error { + return e.compose.Remove(ctx, project, options) +} diff --git a/ecs/run.go b/ecs/run.go index f2e7811a1..8bd3a762f 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) error { + return errdefs.ErrNotImplemented +} diff --git a/kube/compose.go b/kube/compose.go index a31c8c221..1b06e03e5 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) error { + return 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..cc9bed3b3 100644 --- a/local/compose/compose.go +++ b/local/compose/compose.go @@ -23,6 +23,7 @@ import ( "strings" "github.com/docker/compose-cli/api/compose" + "github.com/docker/compose-cli/utils/prompt" "github.com/compose-spec/compose-go/types" moby "github.com/docker/docker/api/types" @@ -34,11 +35,15 @@ import ( // 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, + ui: prompt.User{}, + } } type composeService struct { apiClient client.APIClient + ui prompt.UI } func (s *composeService) Up(ctx context.Context, project *types.Project, options compose.UpOptions) error { 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..c06fca78c --- /dev/null +++ b/local/compose/remove.go @@ -0,0 +1,83 @@ +/* + 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/compose-spec/compose-go/types" + moby "github.com/docker/docker/api/types" + "golang.org/x/sync/errgroup" + + "github.com/docker/compose-cli/api/compose" + "github.com/docker/compose-cli/api/progress" + status "github.com/docker/compose-cli/local/moby" +) + +func (s *composeService) Remove(ctx context.Context, project *types.Project, options compose.RemoveOptions) error { + containers, err := s.getContainers(ctx, project, oneOffInclude) + if err != nil { + return 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 len(stoppedContainers) == 0 { + fmt.Println("No stopped containers") + return nil + } + msg := fmt.Sprintf("Going to remove %s", strings.Join(names, ", ")) + if options.Force { + fmt.Println(msg) + } else { + confirm, err := s.ui.Confirm(msg, false) + if err != nil { + return err + } + if !confirm { + return 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 eg.Wait() +} From 5cb2533faa5b8c8a4f12662d42cadaf57e9158a6 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof Date: Mon, 15 Feb 2021 10:46:56 +0100 Subject: [PATCH 2/2] dryrun to collect resources to be removed, then remove Signed-off-by: Nicolas De Loof --- aci/compose.go | 4 ++-- api/client/compose.go | 4 ++-- api/compose/api.go | 4 +++- cli/cmd/compose/remove.go | 44 +++++++++++++++++++++++++++++++++------ ecs/local/compose.go | 2 +- ecs/run.go | 4 ++-- kube/compose.go | 4 ++-- local/compose/compose.go | 8 ++----- local/compose/remove.go | 33 ++++++++--------------------- 9 files changed, 61 insertions(+), 46 deletions(-) diff --git a/aci/compose.go b/aci/compose.go index 6d3f12c8a..c7f82aba8 100644 --- a/aci/compose.go +++ b/aci/compose.go @@ -211,6 +211,6 @@ func (cs *aciComposeService) RunOneOffContainer(ctx context.Context, project *ty return 0, errdefs.ErrNotImplemented } -func (cs *aciComposeService) Remove(ctx context.Context, project *types.Project, options compose.RemoveOptions) error { - return 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 970008e89..063da2081 100644 --- a/api/client/compose.go +++ b/api/client/compose.go @@ -84,6 +84,6 @@ func (c *composeService) RunOneOffContainer(ctx context.Context, project *types. return 0, errdefs.ErrNotImplemented } -func (c *composeService) Remove(ctx context.Context, project *types.Project, options compose.RemoveOptions) error { - return 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 6d1a9a6b9..65cbbbe7f 100644 --- a/api/compose/api.go +++ b/api/compose/api.go @@ -54,7 +54,7 @@ type Service interface { // 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) error + Remove(ctx context.Context, project *types.Project, options RemoveOptions) ([]string, error) } // CreateOptions group options of the Create API @@ -101,6 +101,8 @@ type KillOptions struct { // 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 diff --git a/cli/cmd/compose/remove.go b/cli/cmd/compose/remove.go index ba796ce60..af5040136 100644 --- a/cli/cmd/compose/remove.go +++ b/cli/cmd/compose/remove.go @@ -18,10 +18,13 @@ 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" ) @@ -68,17 +71,46 @@ func runRemove(ctx context.Context, opts removeOptions, services []string) error return err } - _, err = progress.Run(ctx, func(ctx context.Context) (string, error) { - if opts.stop { + if opts.stop { + _, err = progress.Run(ctx, func(ctx context.Context) (string, error) { err := c.ComposeService().Stop(ctx, project) - if err != nil { - return "", err - } + return "", err + }) + if err != nil { + return err } - return "", c.ComposeService().Remove(ctx, project, compose.RemoveOptions{ + } + + 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 06cbbf1fd..1cd0c9b60 100644 --- a/ecs/local/compose.go +++ b/ecs/local/compose.go @@ -176,6 +176,6 @@ func (e ecsLocalSimulation) RunOneOffContainer(ctx context.Context, project *typ return 0, errors.Wrap(errdefs.ErrNotImplemented, "use docker-compose run") } -func (e ecsLocalSimulation) Remove(ctx context.Context, project *types.Project, options compose.RemoveOptions) error { +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 8bd3a762f..597f80783 100644 --- a/ecs/run.go +++ b/ecs/run.go @@ -29,6 +29,6 @@ func (b *ecsAPIService) RunOneOffContainer(ctx context.Context, project *types.P return 0, errdefs.ErrNotImplemented } -func (b *ecsAPIService) Remove(ctx context.Context, project *types.Project, options compose.RemoveOptions) error { - return 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 1b06e03e5..77f1eceea 100644 --- a/kube/compose.go +++ b/kube/compose.go @@ -198,6 +198,6 @@ func (s *composeService) RunOneOffContainer(ctx context.Context, project *types. return 0, errdefs.ErrNotImplemented } -func (s *composeService) Remove(ctx context.Context, project *types.Project, options compose.RemoveOptions) error { - return 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/compose.go b/local/compose/compose.go index cc9bed3b3..038ac615c 100644 --- a/local/compose/compose.go +++ b/local/compose/compose.go @@ -23,31 +23,27 @@ import ( "strings" "github.com/docker/compose-cli/api/compose" - "github.com/docker/compose-cli/utils/prompt" + "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, - ui: prompt.User{}, } } type composeService struct { apiClient client.APIClient - ui prompt.UI } 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/remove.go b/local/compose/remove.go index c06fca78c..76444eeb6 100644 --- a/local/compose/remove.go +++ b/local/compose/remove.go @@ -18,22 +18,20 @@ package compose import ( "context" - "fmt" - "strings" - - "github.com/compose-spec/compose-go/types" - moby "github.com/docker/docker/api/types" - "golang.org/x/sync/errgroup" "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) error { +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 err + return nil, err } stoppedContainers := containers.filter(func(c moby.Container) bool { @@ -45,21 +43,8 @@ func (s *composeService) Remove(ctx context.Context, project *types.Project, opt names = append(names, getCanonicalContainerName(c)) }) - if len(stoppedContainers) == 0 { - fmt.Println("No stopped containers") - return nil - } - msg := fmt.Sprintf("Going to remove %s", strings.Join(names, ", ")) - if options.Force { - fmt.Println(msg) - } else { - confirm, err := s.ui.Confirm(msg, false) - if err != nil { - return err - } - if !confirm { - return nil - } + if options.DryRun { + return names, nil } w := progress.ContextWriter(ctx) @@ -79,5 +64,5 @@ func (s *composeService) Remove(ctx context.Context, project *types.Project, opt return err }) } - return eg.Wait() + return nil, eg.Wait() }