Browse Source

Merge pull request #1299 from docker/remove

introduce compose rm command
Nicolas De loof 4 years ago
parent
commit
9063c138ba

+ 4 - 0
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
+}

+ 4 - 0
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
+}

+ 12 - 0
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

+ 1 - 0
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 {

+ 116 - 0
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
+}

+ 4 - 0
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)
+}

+ 4 - 0
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
+}

+ 4 - 0
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
+}

+ 1 - 1
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
 	}

+ 5 - 4
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 {

+ 22 - 5
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

+ 2 - 1
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{})

+ 68 - 0
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()
+}