Skip to content
This repository has been archived by the owner on Nov 27, 2023. It is now read-only.

introduce compose rm command #1299

Merged
merged 2 commits into from
Feb 15, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions aci/compose.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 changes: 4 additions & 0 deletions api/client/compose.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 changes: 12 additions & 0 deletions api/compose/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions cli/cmd/compose/compose.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
116 changes: 116 additions & 0 deletions cli/cmd/compose/remove.go
Original file line number Diff line number Diff line change
@@ -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...]",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we add remove as alias?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there's no such thing as a remove alias on docker rm nor docker-compose rm, so to prevent heterogeneous UX I'd prefer we keep such "UX improvement" a separate debate

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 changes: 4 additions & 0 deletions ecs/local/compose.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 changes: 4 additions & 0 deletions ecs/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 changes: 4 additions & 0 deletions kube/compose.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
2 changes: 1 addition & 1 deletion local/compose/attach.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
9 changes: 5 additions & 4 deletions local/compose/compose.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,26 +23,27 @@ 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 {
apiClient client.APIClient
}

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 {
Expand Down
27 changes: 22 additions & 5 deletions local/compose/containers.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package compose

import (
"context"
"fmt"
"sort"

"github.com/compose-spec/compose-go/types"
Expand All @@ -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
Expand Down
3 changes: 2 additions & 1 deletion local/compose/convergence.go
Original file line number Diff line number Diff line change
Expand Up @@ -334,14 +334,15 @@ 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
if container.State == status.ContainerRunning {
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{})
Expand Down
68 changes: 68 additions & 0 deletions local/compose/remove.go
Original file line number Diff line number Diff line change
@@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove this?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

required. go iteration re-assigning c variable. such a stupid idiom (ㆆ _ ㆆ)

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