Merge pull request #1299 from docker/remove

introduce compose rm command
This commit is contained in:
Nicolas De loof 2021-02-15 15:47:50 +01:00 committed by GitHub
commit 9063c138ba
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 247 additions and 11 deletions

View File

@ -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) { func (cs *aciComposeService) RunOneOffContainer(ctx context.Context, project *types.Project, opts compose.RunOptions) (int, error) {
return 0, errdefs.ErrNotImplemented return 0, errdefs.ErrNotImplemented
} }
func (cs *aciComposeService) Remove(ctx context.Context, project *types.Project, options compose.RemoveOptions) ([]string, error) {
return nil, errdefs.ErrNotImplemented
}

View File

@ -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) { func (c *composeService) RunOneOffContainer(ctx context.Context, project *types.Project, opts compose.RunOptions) (int, error) {
return 0, errdefs.ErrNotImplemented return 0, errdefs.ErrNotImplemented
} }
func (c *composeService) Remove(ctx context.Context, project *types.Project, options compose.RemoveOptions) ([]string, error) {
return nil, errdefs.ErrNotImplemented
}

View File

@ -53,6 +53,8 @@ type Service interface {
Kill(ctx context.Context, project *types.Project, options KillOptions) error Kill(ctx context.Context, project *types.Project, options KillOptions) error
// RunOneOffContainer creates a service oneoff container and starts its dependencies // RunOneOffContainer creates a service oneoff container and starts its dependencies
RunOneOffContainer(ctx context.Context, project *types.Project, opts RunOptions) (int, error) 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 // CreateOptions group options of the Create API
@ -97,6 +99,16 @@ type KillOptions struct {
Signal string 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 // RunOptions options to execute compose run
type RunOptions struct { type RunOptions struct {
Service string Service string

View File

@ -116,6 +116,7 @@ func Command(contextType string) *cobra.Command {
convertCommand(&opts), convertCommand(&opts),
killCommand(&opts), killCommand(&opts),
runCommand(&opts), runCommand(&opts),
removeCommand(&opts),
) )
if contextType == store.LocalContextType || contextType == store.DefaultContextType { if contextType == store.LocalContextType || contextType == store.DefaultContextType {

116
cli/cmd/compose/remove.go Normal file
View File

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

View File

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

View File

@ -28,3 +28,7 @@ import (
func (b *ecsAPIService) RunOneOffContainer(ctx context.Context, project *types.Project, opts compose.RunOptions) (int, error) { func (b *ecsAPIService) RunOneOffContainer(ctx context.Context, project *types.Project, opts compose.RunOptions) (int, error) {
return 0, errdefs.ErrNotImplemented return 0, errdefs.ErrNotImplemented
} }
func (b *ecsAPIService) Remove(ctx context.Context, project *types.Project, options compose.RemoveOptions) ([]string, error) {
return nil, errdefs.ErrNotImplemented
}

View File

@ -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) { func (s *composeService) RunOneOffContainer(ctx context.Context, project *types.Project, opts compose.RunOptions) (int, error) {
return 0, errdefs.ErrNotImplemented return 0, errdefs.ErrNotImplemented
} }
func (s *composeService) Remove(ctx context.Context, project *types.Project, options compose.RemoveOptions) ([]string, error) {
return nil, errdefs.ErrNotImplemented
}

View File

@ -31,7 +31,7 @@ import (
) )
func (s *composeService) attach(ctx context.Context, project *types.Project, consumer compose.ContainerEventListener) (Containers, error) { 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 { if err != nil {
return nil, err return nil, err
} }

View File

@ -23,18 +23,19 @@ import (
"strings" "strings"
"github.com/docker/compose-cli/api/compose" "github.com/docker/compose-cli/api/compose"
"github.com/docker/compose-cli/api/errdefs"
"github.com/compose-spec/compose-go/types" "github.com/compose-spec/compose-go/types"
moby "github.com/docker/docker/api/types" moby "github.com/docker/docker/api/types"
"github.com/docker/docker/client" "github.com/docker/docker/client"
"github.com/sanathkr/go-yaml" "github.com/sanathkr/go-yaml"
errdefs2 "github.com/docker/compose-cli/api/errdefs"
) )
// NewComposeService create a local implementation of the compose.Service API // NewComposeService create a local implementation of the compose.Service API
func NewComposeService(apiClient client.APIClient) compose.Service { func NewComposeService(apiClient client.APIClient) compose.Service {
return &composeService{apiClient: apiClient} return &composeService{
apiClient: apiClient,
}
} }
type composeService struct { type composeService struct {
@ -42,7 +43,7 @@ type composeService struct {
} }
func (s *composeService) Up(ctx context.Context, project *types.Project, options compose.UpOptions) error { 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 { func getCanonicalContainerName(c moby.Container) string {

View File

@ -18,6 +18,7 @@ package compose
import ( import (
"context" "context"
"fmt"
"sort" "sort"
"github.com/compose-spec/compose-go/types" "github.com/compose-spec/compose-go/types"
@ -28,13 +29,29 @@ import (
// Containers is a set of moby Container // Containers is a set of moby Container
type Containers []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 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{ containers, err := s.apiClient.ContainerList(ctx, moby.ContainerListOptions{
Filters: filters.NewArgs( Filters: f,
projectFilter(project.Name), All: true,
),
All: true,
}) })
if err != nil { if err != nil {
return nil, err return nil, err

View File

@ -334,6 +334,8 @@ func (s *composeService) startService(ctx context.Context, project *types.Projec
if err != nil { if err != nil {
return err return err
} }
w := progress.ContextWriter(ctx)
eg, ctx := errgroup.WithContext(ctx) eg, ctx := errgroup.WithContext(ctx)
for _, c := range containers { for _, c := range containers {
container := c container := c
@ -341,7 +343,6 @@ func (s *composeService) startService(ctx context.Context, project *types.Projec
continue continue
} }
eg.Go(func() error { eg.Go(func() error {
w := progress.ContextWriter(ctx)
eventName := getContainerProgressName(container) eventName := getContainerProgressName(container)
w.Event(progress.StartingEvent(eventName)) w.Event(progress.StartingEvent(eventName))
err := s.apiClient.ContainerStart(ctx, container.ID, moby.ContainerStartOptions{}) err := s.apiClient.ContainerStart(ctx, container.ID, moby.ContainerStartOptions{})

68
local/compose/remove.go Normal file
View File

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