introduce --remove-orphans option

Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
This commit is contained in:
Nicolas De Loof 2021-01-06 14:23:37 +01:00
parent 849fbed1ac
commit 4de01936f8
No known key found for this signature in database
GPG Key ID: 9858809D6F8F6E7E
12 changed files with 145 additions and 59 deletions

View File

@ -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
}

View File

@ -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
}

View File

@ -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

View File

@ -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 {

View File

@ -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

View File

@ -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 {

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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 {

View File

@ -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)
})
}

View File

@ -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
}