diff --git a/local/compose/create.go b/local/compose/create.go index 56b8adad..6deefa78 100644 --- a/local/compose/create.go +++ b/local/compose/create.go @@ -938,7 +938,7 @@ func (s *composeService) ensureNetwork(ctx context.Context, n types.NetworkConfi return nil } -func (s *composeService) ensureNetworkDown(ctx context.Context, networkID string, networkName string) error { +func (s *composeService) removeNetwork(ctx context.Context, networkID string, networkName string) error { w := progress.ContextWriter(ctx) eventName := fmt.Sprintf("Network %s", networkName) w.Event(progress.RemovingEvent(eventName)) diff --git a/local/compose/down.go b/local/compose/down.go index 98184733..73582c36 100644 --- a/local/compose/down.go +++ b/local/compose/down.go @@ -33,6 +33,8 @@ import ( "github.com/docker/compose-cli/api/progress" ) +type downOp func() error + func (s *composeService) Down(ctx context.Context, projectName string, options compose.DownOptions) error { w := progress.ContextWriter(ctx) resourceToRemove := false @@ -73,37 +75,76 @@ func (s *composeService) Down(ctx context.Context, projectName string, options c } } - networks, err := s.apiClient.NetworkList(ctx, moby.NetworkListOptions{Filters: filters.NewArgs(projectFilter(projectName))}) + ops, err := s.ensureNetwoksDown(ctx, projectName) if err != nil { return err } - eg, _ := errgroup.WithContext(ctx) - for _, n := range networks { - resourceToRemove = true - networkID := n.ID - networkName := n.Name - eg.Go(func() error { - return s.ensureNetworkDown(ctx, networkID, networkName) - }) - } - if options.Images != "" { - for image := range s.getServiceImages(options, projectName) { - image := image - eg.Go(func() error { - resourceToRemove = true - return s.removeImage(ctx, image, w) - }) - } + ops = append(ops, s.ensureImagesDown(ctx, projectName, options, w)...) } - if !resourceToRemove { + if options.Volumes { + rm, err := s.ensureVolumesDown(ctx, projectName, w) + if err != nil { + return err + } + ops = append(ops, rm...) + } + + if !resourceToRemove && len(ops) == 0 { w.Event(progress.NewEvent(projectName, progress.Done, "Warning: No resource found to remove")) } + + eg, _ := errgroup.WithContext(ctx) + for _, op := range ops { + eg.Go(op) + } return eg.Wait() } +func (s *composeService) ensureVolumesDown(ctx context.Context, projectName string, w progress.Writer) ([]downOp, error) { + var ops []downOp + volumes, err := s.apiClient.VolumeList(ctx, filters.NewArgs(projectFilter(projectName))) + if err != nil { + return ops, err + } + for _, vol := range volumes.Volumes { + id := vol.Name + ops = append(ops, func() error { + return s.removeVolume(ctx, id, w) + }) + } + return ops, nil +} + +func (s *composeService) ensureImagesDown(ctx context.Context, projectName string, options compose.DownOptions, w progress.Writer) []downOp { + var ops []downOp + for image := range s.getServiceImages(options, projectName) { + image := image + ops = append(ops, func() error { + return s.removeImage(ctx, image, w) + }) + } + return ops +} + +func (s *composeService) ensureNetwoksDown(ctx context.Context, projectName string) ([]downOp, error) { + var ops []downOp + networks, err := s.apiClient.NetworkList(ctx, moby.NetworkListOptions{Filters: filters.NewArgs(projectFilter(projectName))}) + if err != nil { + return ops, err + } + for _, n := range networks { + networkID := n.ID + networkName := n.Name + ops = append(ops, func() error { + return s.removeNetwork(ctx, networkID, networkName) + }) + } + return ops, nil +} + func (s *composeService) getServiceImages(options compose.DownOptions, projectName string) map[string]struct{} { images := map[string]struct{}{} for _, service := range options.Project.Services { @@ -134,6 +175,21 @@ func (s *composeService) removeImage(ctx context.Context, image string, w progre return err } +func (s *composeService) removeVolume(ctx context.Context, id string, w progress.Writer) error { + resource := fmt.Sprintf("Volume %s", id) + w.Event(progress.NewEvent(resource, progress.Working, "Removing")) + err := s.apiClient.VolumeRemove(ctx, id, true) + if err == nil { + w.Event(progress.NewEvent(resource, progress.Done, "Removed")) + return nil + } + if errdefs.IsNotFound(err) { + w.Event(progress.NewEvent(resource, progress.Done, "Warning: No resource found to remove")) + return nil + } + return err +} + func (s *composeService) stopContainers(ctx context.Context, w progress.Writer, containers []moby.Container, timeout *time.Duration) error { for _, container := range containers { toStop := container diff --git a/local/compose/down_test.go b/local/compose/down_test.go index 00282b02..b4bc37b0 100644 --- a/local/compose/down_test.go +++ b/local/compose/down_test.go @@ -20,12 +20,12 @@ import ( "context" "testing" - "github.com/docker/docker/api/types/filters" - "github.com/docker/compose-cli/api/compose" "github.com/docker/compose-cli/local/mocks" apitypes "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/filters" + "github.com/docker/docker/api/types/volume" "github.com/golang/mock/gomock" "gotest.tools/v3/assert" ) @@ -79,3 +79,24 @@ func TestDownRemoveOrphans(t *testing.T) { err := tested.Down(context.Background(), testProject, compose.DownOptions{RemoveOrphans: true}) assert.NilError(t, err) } + +func TestDownRemoveVolumes(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + api := mocks.NewMockAPIClient(mockCtrl) + tested.apiClient = api + + api.EXPECT().ContainerList(gomock.Any(), projectFilterListOpt()).Return( + []apitypes.Container{testContainer("service1", "123")}, nil) + + api.EXPECT().ContainerStop(gomock.Any(), "123", nil).Return(nil) + api.EXPECT().ContainerRemove(gomock.Any(), "123", apitypes.ContainerRemoveOptions{Force: true}).Return(nil) + + api.EXPECT().NetworkList(gomock.Any(), apitypes.NetworkListOptions{Filters: filters.NewArgs(projectFilter(testProject))}).Return(nil, nil) + + api.EXPECT().VolumeList(gomock.Any(), filters.NewArgs(projectFilter(testProject))).Return(volume.VolumeListOKBody{Volumes: []*apitypes.Volume{{Name: "myProject_volume"}}}, nil) + api.EXPECT().VolumeRemove(gomock.Any(), "myProject_volume", true).Return(nil) + + err := tested.Down(context.Background(), testProject, compose.DownOptions{Volumes: true}) + assert.NilError(t, err) +} diff --git a/local/e2e/compose/volumes_test.go b/local/e2e/compose/volumes_test.go index ecf0bfd8..93557736 100644 --- a/local/e2e/compose/volumes_test.go +++ b/local/e2e/compose/volumes_test.go @@ -72,7 +72,9 @@ func TestLocalComposeVolume(t *testing.T) { }) t.Run("cleanup volume project", func(t *testing.T) { - c.RunDockerCmd("compose", "--project-name", projectName, "down") - c.RunDockerCmd("volume", "rm", projectName+"_staticVol") + c.RunDockerCmd("compose", "--project-name", projectName, "down", "--volumes") + res := c.RunDockerCmd("volume", "ls") + assert.Assert(t, !strings.Contains(res.Stdout(), projectName+"_staticVol")) + assert.Assert(t, !strings.Contains(res.Stdout(), "myvolume")) }) }