diff --git a/aci/compose.go b/aci/compose.go index b3fe0210b..2820e192f 100644 --- a/aci/compose.go +++ b/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 } diff --git a/api/client/compose.go b/api/client/compose.go index 8f4c199a0..0c6211230 100644 --- a/api/client/compose.go +++ b/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 } diff --git a/api/compose/api.go b/api/compose/api.go index 6777594b5..72f3517e4 100644 --- a/api/compose/api.go +++ b/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 diff --git a/cli/cmd/compose/run.go b/cli/cmd/compose/run.go index 9f0140e74..3c019cb12 100644 --- a/cli/cmd/compose/run.go +++ b/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 { diff --git a/cli/cmd/compose/up.go b/cli/cmd/compose/up.go index b1b0b8189..c84b9dd57 100644 --- a/cli/cmd/compose/up.go +++ b/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 diff --git a/ecs/local/compose.go b/ecs/local/compose.go index 959f1c587..d5c9728fd 100644 --- a/ecs/local/compose.go +++ b/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 { diff --git a/ecs/up.go b/ecs/up.go index f649f36f6..13b33ab5a 100644 --- a/ecs/up.go +++ b/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 } diff --git a/example/backend.go b/example/backend.go index e0c62a6b7..58cf7a6c8 100644 --- a/example/backend.go +++ b/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 } diff --git a/local/compose/containers.go b/local/compose/containers.go new file mode 100644 index 000000000..4159045c4 --- /dev/null +++ b/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 +} diff --git a/local/compose/convergence.go b/local/compose/convergence.go index 8e23d4e9e..4721a8ab0 100644 --- a/local/compose/convergence.go +++ b/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 { diff --git a/local/compose/create.go b/local/compose/create.go index b97a8173f..57c174dbf 100644 --- a/local/compose/create.go +++ b/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) }) } diff --git a/local/compose/down.go b/local/compose/down.go index 704f832c0..d522cb51a 100644 --- a/local/compose/down.go +++ b/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 -}