浏览代码

Merge pull request #1106 from docker/orphans

introduce --remove-orphans option
Nicolas De loof 4 年之前
父节点
当前提交
1cf48318a4

+ 1 - 1
aci/compose.go

@@ -56,7 +56,7 @@ func (cs *aciComposeService) Pull(ctx context.Context, project *types.Project) e
 	return errdefs.ErrNotImplemented
 }
 
-func (cs *aciComposeService) Create(ctx context.Context, project *types.Project) error {
+func (cs *aciComposeService) Create(ctx context.Context, project *types.Project, opts compose.CreateOptions) error {
 	return errdefs.ErrNotImplemented
 }
 

+ 1 - 1
api/client/compose.go

@@ -40,7 +40,7 @@ func (c *composeService) Pull(ctx context.Context, project *types.Project) error
 	return errdefs.ErrNotImplemented
 }
 
-func (c *composeService) Create(ctx context.Context, project *types.Project) error {
+func (c *composeService) Create(ctx context.Context, project *types.Project, opts compose.CreateOptions) error {
 	return errdefs.ErrNotImplemented
 }
 

+ 7 - 1
api/compose/api.go

@@ -32,7 +32,7 @@ type Service interface {
 	// Pull executes the equivalent of a `compose pull`
 	Pull(ctx context.Context, project *types.Project) error
 	// Create executes the equivalent to a `compose create`
-	Create(ctx context.Context, project *types.Project) error
+	Create(ctx context.Context, project *types.Project, opts CreateOptions) error
 	// Start executes the equivalent to a `compose start`
 	Start(ctx context.Context, project *types.Project, consumer LogConsumer) error
 	// Up executes the equivalent to a `compose up`
@@ -51,6 +51,12 @@ type Service interface {
 	RunOneOffContainer(ctx context.Context, project *types.Project, opts RunOptions) error
 }
 
+// CreateOptions group options of the Create API
+type CreateOptions struct {
+	// Remove legacy containers for services that are not defined in the project
+	RemoveOrphans bool
+}
+
 // UpOptions group options of the Up API
 type UpOptions struct {
 	// Detach will create services and return immediately

+ 1 - 1
cli/cmd/compose/run.go

@@ -104,7 +104,7 @@ func startDependencies(ctx context.Context, c *client.Client, project *types.Pro
 		}
 	}
 	project.Services = dependencies
-	if err := c.ComposeService().Create(ctx, project); err != nil {
+	if err := c.ComposeService().Create(ctx, project, compose.CreateOptions{}); err != nil {
 		return err
 	}
 	if err := c.ComposeService().Start(ctx, project, nil); err != nil {

+ 14 - 6
cli/cmd/compose/up.go

@@ -33,8 +33,13 @@ import (
 	"github.com/spf13/cobra"
 )
 
+type upOptions struct {
+	composeOptions
+	removeOrphans bool
+}
+
 func upCommand(contextType string) *cobra.Command {
-	opts := composeOptions{}
+	opts := upOptions{}
 	upCmd := &cobra.Command{
 		Use:   "up [SERVICE...]",
 		Short: "Create and start containers",
@@ -53,6 +58,7 @@ func upCommand(contextType string) *cobra.Command {
 	upCmd.Flags().StringArrayVarP(&opts.Environment, "environment", "e", []string{}, "Environment variables")
 	upCmd.Flags().BoolVarP(&opts.Detach, "detach", "d", false, "Detached mode: Run containers in the background")
 	upCmd.Flags().BoolVar(&opts.Build, "build", false, "Build images before starting containers.")
+	upCmd.Flags().BoolVar(&opts.removeOrphans, "remove-orphans", false, "Remove containers for services not defined in the Compose file.")
 
 	if contextType == store.AciContextType {
 		upCmd.Flags().StringVar(&opts.DomainName, "domainname", "", "Container NIS domain name")
@@ -61,8 +67,8 @@ func upCommand(contextType string) *cobra.Command {
 	return upCmd
 }
 
-func runUp(ctx context.Context, opts composeOptions, services []string) error {
-	c, project, err := setup(ctx, opts, services)
+func runUp(ctx context.Context, opts upOptions, services []string) error {
+	c, project, err := setup(ctx, opts.composeOptions, services)
 	if err != nil {
 		return err
 	}
@@ -75,14 +81,16 @@ func runUp(ctx context.Context, opts composeOptions, services []string) error {
 	return err
 }
 
-func runCreateStart(ctx context.Context, opts composeOptions, services []string) error {
-	c, project, err := setup(ctx, opts, services)
+func runCreateStart(ctx context.Context, opts upOptions, services []string) error {
+	c, project, err := setup(ctx, opts.composeOptions, services)
 	if err != nil {
 		return err
 	}
 
 	_, err = progress.Run(ctx, func(ctx context.Context) (string, error) {
-		return "", c.ComposeService().Create(ctx, project)
+		return "", c.ComposeService().Create(ctx, project, compose.CreateOptions{
+			RemoveOrphans: opts.removeOrphans,
+		})
 	})
 	if err != nil {
 		return err

+ 2 - 2
ecs/local/compose.go

@@ -43,13 +43,13 @@ func (e ecsLocalSimulation) Pull(ctx context.Context, project *types.Project) er
 	return e.compose.Pull(ctx, project)
 }
 
-func (e ecsLocalSimulation) Create(ctx context.Context, project *types.Project) error {
+func (e ecsLocalSimulation) Create(ctx context.Context, project *types.Project, opts compose.CreateOptions) error {
 	enhanced, err := e.enhanceForLocalSimulation(project)
 	if err != nil {
 		return err
 	}
 
-	return e.compose.Create(ctx, enhanced)
+	return e.compose.Create(ctx, enhanced, opts)
 }
 
 func (e ecsLocalSimulation) Start(ctx context.Context, project *types.Project, consumer compose.LogConsumer) error {

+ 1 - 1
ecs/up.go

@@ -43,7 +43,7 @@ func (b *ecsAPIService) Pull(ctx context.Context, project *types.Project) error
 	return errdefs.ErrNotImplemented
 }
 
-func (b *ecsAPIService) Create(ctx context.Context, project *types.Project) error {
+func (b *ecsAPIService) Create(ctx context.Context, project *types.Project, opts compose.CreateOptions) error {
 	return errdefs.ErrNotImplemented
 }
 

+ 1 - 1
example/backend.go

@@ -150,7 +150,7 @@ func (cs *composeService) Pull(ctx context.Context, project *types.Project) erro
 	return errdefs.ErrNotImplemented
 }
 
-func (cs *composeService) Create(ctx context.Context, project *types.Project) error {
+func (cs *composeService) Create(ctx context.Context, project *types.Project, opts compose.CreateOptions) error {
 	return errdefs.ErrNotImplemented
 }
 

+ 72 - 0
local/compose/containers.go

@@ -0,0 +1,72 @@
+/*
+   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 moby "github.com/docker/docker/api/types"
+
+// Containers is a set of moby Container
+type Containers []moby.Container
+
+// containerPredicate define a predicate we want container to satisfy for filtering operations
+type containerPredicate func(c moby.Container) bool
+
+func isService(services ...string) containerPredicate {
+	return func(c moby.Container) bool {
+		service := c.Labels[serviceLabel]
+		return contains(services, service)
+	}
+}
+
+func isNotService(services ...string) containerPredicate {
+	return func(c moby.Container) bool {
+		service := c.Labels[serviceLabel]
+		return !contains(services, service)
+	}
+}
+
+// filter return Containers with elements to match predicate
+func (containers Containers) filter(predicate containerPredicate) Containers {
+	var filtered Containers
+	for _, c := range containers {
+		if predicate(c) {
+			filtered = append(filtered, c)
+		}
+	}
+	return filtered
+}
+
+// split return Containers with elements to match and those not to match predicate
+func (containers Containers) split(predicate containerPredicate) (Containers, Containers) {
+	var right Containers
+	var left Containers
+	for _, c := range containers {
+		if predicate(c) {
+			right = append(right, c)
+		} else {
+			left = append(left, c)
+		}
+	}
+	return right, left
+}
+
+func (containers Containers) names() []string {
+	var names []string
+	for _, c := range containers {
+		names = append(names, getContainerName(c))
+	}
+	return names
+}

+ 2 - 12
local/compose/convergence.go

@@ -37,19 +37,9 @@ const (
 	forceRecreate = "force_recreate"
 )
 
-func (s *composeService) ensureService(ctx context.Context, project *types.Project, service types.ServiceConfig) error {
-	actual, err := s.apiClient.ContainerList(ctx, moby.ContainerListOptions{
-		Filters: filters.NewArgs(
-			projectFilter(project.Name),
-			serviceFilter(service.Name),
-		),
-		All: true,
-	})
-	if err != nil {
-		return err
-	}
-
+func (s *composeService) ensureService(ctx context.Context, observedState Containers, project *types.Project, service types.ServiceConfig) error {
 	scale := getScale(service)
+	actual := observedState.filter(isService(service.Name))
 
 	eg, _ := errgroup.WithContext(ctx)
 	if len(actual) < scale {

+ 37 - 5
local/compose/create.go

@@ -23,12 +23,10 @@ import (
 	"strconv"
 	"strings"
 
-	convert "github.com/docker/compose-cli/local/moby"
-	"github.com/docker/compose-cli/progress"
-
 	"github.com/compose-spec/compose-go/types"
 	moby "github.com/docker/docker/api/types"
 	"github.com/docker/docker/api/types/container"
+	"github.com/docker/docker/api/types/filters"
 	"github.com/docker/docker/api/types/mount"
 	"github.com/docker/docker/api/types/network"
 	"github.com/docker/docker/api/types/strslice"
@@ -36,9 +34,15 @@ import (
 	"github.com/docker/docker/errdefs"
 	"github.com/docker/go-connections/nat"
 	"github.com/pkg/errors"
+	"github.com/sirupsen/logrus"
+	"golang.org/x/sync/errgroup"
+
+	"github.com/docker/compose-cli/api/compose"
+	convert "github.com/docker/compose-cli/local/moby"
+	"github.com/docker/compose-cli/progress"
 )
 
-func (s *composeService) Create(ctx context.Context, project *types.Project) error {
+func (s *composeService) Create(ctx context.Context, project *types.Project, opts compose.CreateOptions) error {
 	err := s.ensureImagesExists(ctx, project)
 	if err != nil {
 		return err
@@ -52,8 +56,36 @@ func (s *composeService) Create(ctx context.Context, project *types.Project) err
 		return err
 	}
 
+	var observedState Containers
+	observedState, err = s.apiClient.ContainerList(ctx, moby.ContainerListOptions{
+		Filters: filters.NewArgs(
+			projectFilter(project.Name),
+		),
+		All: true,
+	})
+	if err != nil {
+		return err
+	}
+
+	orphans := observedState.filter(isNotService(project.ServiceNames()...))
+	if len(orphans) > 0 {
+		if opts.RemoveOrphans {
+			eg, _ := errgroup.WithContext(ctx)
+			w := progress.ContextWriter(ctx)
+			s.removeContainers(ctx, w, eg, orphans)
+			if eg.Wait() != nil {
+				return err
+			}
+		} else {
+			logrus.Warnf("Found orphan containers (%s) for this project. If "+
+				"you removed or renamed this service in your compose "+
+				"file, you can run this command with the "+
+				"--remove-orphans flag to clean it up.", orphans.names())
+		}
+	}
+
 	return InDependencyOrder(ctx, project, func(c context.Context, service types.ServiceConfig) error {
-		return s.ensureService(c, project, service)
+		return s.ensureService(c, observedState, project, service)
 	})
 }
 

+ 6 - 28
local/compose/down.go

@@ -41,7 +41,8 @@ func (s *composeService) Down(ctx context.Context, projectName string, options c
 		return err
 	}
 
-	containers, err := s.apiClient.ContainerList(ctx, moby.ContainerListOptions{
+	var containers Containers
+	containers, err = s.apiClient.ContainerList(ctx, moby.ContainerListOptions{
 		Filters: filters.NewArgs(projectFilter(project.Name)),
 		All:     true,
 	})
@@ -50,14 +51,14 @@ func (s *composeService) Down(ctx context.Context, projectName string, options c
 	}
 
 	err = InReverseDependencyOrder(ctx, project, func(c context.Context, service types.ServiceConfig) error {
-		serviceContainers, others := split(containers, isService(service.Name))
-		err := s.removeContainers(ctx, w, eg, serviceContainers)
+		serviceContainers, others := containers.split(isService(service.Name))
+		s.removeContainers(ctx, w, eg, serviceContainers)
 		containers = others
 		return err
 	})
 
 	if options.RemoveOrphans {
-		err := s.removeContainers(ctx, w, eg, containers)
+		s.removeContainers(ctx, w, eg, containers)
 		if err != nil {
 			return err
 		}
@@ -89,7 +90,7 @@ func (s *composeService) Down(ctx context.Context, projectName string, options c
 	return eg.Wait()
 }
 
-func (s *composeService) removeContainers(ctx context.Context, w progress.Writer, eg *errgroup.Group, containers []moby.Container) error {
+func (s *composeService) removeContainers(ctx context.Context, w progress.Writer, eg *errgroup.Group, containers []moby.Container) {
 	for _, container := range containers {
 		toDelete := container
 		eg.Go(func() error {
@@ -110,7 +111,6 @@ func (s *composeService) removeContainers(ctx context.Context, w progress.Writer
 			return nil
 		})
 	}
-	return nil
 }
 
 func (s *composeService) projectFromContainerLabels(ctx context.Context, projectName string) (*types.Project, error) {
@@ -160,25 +160,3 @@ func loadProjectOptionsFromLabels(c moby.Container) (*cli.ProjectOptions, error)
 		cli.WithWorkingDirectory(c.Labels[workingDirLabel]),
 		cli.WithName(c.Labels[projectLabel]))
 }
-
-type containerPredicate func(c moby.Container) bool
-
-func isService(service string) containerPredicate {
-	return func(c moby.Container) bool {
-		return c.Labels[serviceLabel] == service
-	}
-}
-
-// split return a container slice with elements to match predicate
-func split(containers []moby.Container, predicate containerPredicate) ([]moby.Container, []moby.Container) {
-	var right []moby.Container
-	var left []moby.Container
-	for _, c := range containers {
-		if predicate(c) {
-			right = append(right, c)
-		} else {
-			left = append(left, c)
-		}
-	}
-	return right, left
-}