Repository: docker/compose

Issue:
Having a `volume`   command
### Description

Not much needs to be said I suppose. Having a `volume` command, context-aware of the services running in the compose file would make it a lot easier to delete, prune, and inspect volumes related to the services. The main usage for this is *not having to type the whole project name - service name - volume name, and not having to browse amidst all volumes you have available on a host.

If pointed in the right direction I can do my best to provide a patch for this. 

Hints from issue comments:
@ndeloof: Sounds reasonable to have such a command, you can look at the [list](https://github.com/docker/compose/blob/main/cmd/compose/list.go) command as an illustration of such "introspection" commands@batcity: @pedrolucasp (Just wondering) are you still planning on working on this feature?@pedrolucasp: > @pedrolucasp (Just wondering) are you still planning on working on this feature?

Yeah, I plan to, but got caught up with work stuff. Wen't as far as having the command available, but got stuck with connecting the backend part of it.@batcity: ah sounds good!, thanks for the update!

Solution patch:
diff --git a/cmd/compose/compose.go b/cmd/compose/compose.go
index 10b53e53..2be3ab15 100644
--- a/cmd/compose/compose.go
+++ b/cmd/compose/compose.go
@@ -636,6 +636,7 @@ func RootCommand(dockerCli command.Cli, backend Backend) *cobra.Command { //noli
 		publishCommand(&opts, dockerCli, backend),
 		alphaCommand(&opts, dockerCli, backend),
 		bridgeCommand(&opts, dockerCli),
+		volumesCommand(&opts, dockerCli, backend),
 	)
 
 	c.Flags().SetInterspersed(false)
diff --git a/cmd/compose/volumes.go b/cmd/compose/volumes.go
new file mode 100644
index 00000000..d98f71db
--- /dev/null
+++ b/cmd/compose/volumes.go
@@ -0,0 +1,95 @@
+/*
+   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"
+	"slices"
+
+	"github.com/docker/cli/cli/command"
+	"github.com/docker/cli/cli/command/formatter"
+	"github.com/docker/cli/cli/flags"
+	"github.com/docker/compose/v2/pkg/api"
+	"github.com/spf13/cobra"
+)
+
+type volumesOptions struct {
+	*ProjectOptions
+	Quiet  bool
+	Format string
+}
+
+func volumesCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
+	options := volumesOptions{
+		ProjectOptions: p,
+	}
+
+	cmd := &cobra.Command{
+		Use:   "volumes [OPTIONS] [SERVICE...]",
+		Short: "List volumes",
+		RunE: Adapt(func(ctx context.Context, args []string) error {
+			return runVol(ctx, dockerCli, backend, args, options)
+		}),
+		ValidArgsFunction: completeServiceNames(dockerCli, p),
+	}
+
+	cmd.Flags().BoolVarP(&options.Quiet, "quiet", "q", false, "Only display volume names")
+	cmd.Flags().StringVar(&options.Format, "format", "table", flags.FormatHelp)
+
+	return cmd
+}
+
+func runVol(ctx context.Context, dockerCli command.Cli, backend api.Service, services []string, options volumesOptions) error {
+	project, _, err := options.projectOrName(ctx, dockerCli, services...)
+	if err != nil {
+		return err
+	}
+
+	names := project.ServiceNames()
+
+	if len(services) == 0 {
+		services = names
+	}
+
+	for _, service := range services {
+		if !slices.Contains(names, service) {
+			return fmt.Errorf("no such service: %s", service)
+		}
+	}
+
+	volumes, err := backend.Volumes(ctx, project, api.VolumesOptions{
+		Services: services,
+	})
+	if err != nil {
+		return err
+	}
+
+	if options.Quiet {
+		for _, v := range volumes {
+			_, _ = fmt.Fprintln(dockerCli.Out(), v.Name)
+		}
+		return nil
+	}
+
+	volumeCtx := formatter.Context{
+		Output: dockerCli.Out(),
+		Format: formatter.NewVolumeFormat(options.Format, options.Quiet),
+	}
+
+	return formatter.VolumeWrite(volumeCtx, volumes)
+}
diff --git a/docs/reference/compose.md b/docs/reference/compose.md
index 391bf1a9..74d129d8 100644
--- a/docs/reference/compose.md
+++ b/docs/reference/compose.md
@@ -43,6 +43,7 @@ Define and run multi-container applications with Docker
 | [`unpause`](compose_unpause.md) | Unpause services                                                                        |
 | [`up`](compose_up.md)           | Create and start containers                                                             |
 | [`version`](compose_version.md) | Show the Docker Compose version information                                             |
+| [`volumes`](compose_volumes.md) | List volumes                                                                            |
 | [`wait`](compose_wait.md)       | Block until containers of all (or specified) services stop.                             |
 | [`watch`](compose_watch.md)     | Watch build context for service and rebuild/refresh containers when files are updated   |
 
diff --git a/docs/reference/compose_volumes.md b/docs/reference/compose_volumes.md
new file mode 100644
index 00000000..6bad874f
--- /dev/null
+++ b/docs/reference/compose_volumes.md
@@ -0,0 +1,16 @@
+# docker compose volumes
+
+<!---MARKER_GEN_START-->
+List volumes
+
+### Options
+
+| Name            | Type     | Default | Description                                                                                                                                                                                                                                                                                                                                                                                                                          |
+|:----------------|:---------|:--------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| `--dry-run`     | `bool`   |         | Execute command in dry run mode                                                                                                                                                                                                                                                                                                                                                                                                      |
+| `--format`      | `string` | `table` | Format output using a custom template:<br>'table':            Print output in table format with column headers (default)<br>'table TEMPLATE':   Print output in table format using the given Go template<br>'json':             Print in JSON format<br>'TEMPLATE':         Print output using the given Go template.<br>Refer to https://docs.docker.com/go/formatting/ for more information about formatting output with templates |
+| `-q`, `--quiet` | `bool`   |         | Only display volume names                                                                                                                                                                                                                                                                                                                                                                                                            |
+
+
+<!---MARKER_GEN_END-->
+
diff --git a/docs/reference/docker_compose.yaml b/docs/reference/docker_compose.yaml
index 93332702..02a39d93 100644
--- a/docs/reference/docker_compose.yaml
+++ b/docs/reference/docker_compose.yaml
@@ -37,6 +37,7 @@ cname:
     - docker compose unpause
     - docker compose up
     - docker compose version
+    - docker compose volumes
     - docker compose wait
     - docker compose watch
 clink:
@@ -72,6 +73,7 @@ clink:
     - docker_compose_unpause.yaml
     - docker_compose_up.yaml
     - docker_compose_version.yaml
+    - docker_compose_volumes.yaml
     - docker_compose_wait.yaml
     - docker_compose_watch.yaml
 options:
diff --git a/docs/reference/docker_compose_volumes.yaml b/docs/reference/docker_compose_volumes.yaml
new file mode 100644
index 00000000..20516db7
--- /dev/null
+++ b/docs/reference/docker_compose_volumes.yaml
@@ -0,0 +1,52 @@
+command: docker compose volumes
+short: List volumes
+long: List volumes
+usage: docker compose volumes [OPTIONS] [SERVICE...]
+pname: docker compose
+plink: docker_compose.yaml
+options:
+    - option: format
+      value_type: string
+      default_value: table
+      description: |-
+        Format output using a custom template:
+        'table':            Print output in table format with column headers (default)
+        'table TEMPLATE':   Print output in table format using the given Go template
+        'json':             Print in JSON format
+        'TEMPLATE':         Print output using the given Go template.
+        Refer to https://docs.docker.com/go/formatting/ for more information about formatting output with templates
+      deprecated: false
+      hidden: false
+      experimental: false
+      experimentalcli: false
+      kubernetes: false
+      swarm: false
+    - option: quiet
+      shorthand: q
+      value_type: bool
+      default_value: "false"
+      description: Only display volume names
+      deprecated: false
+      hidden: false
+      experimental: false
+      experimentalcli: false
+      kubernetes: false
+      swarm: false
+inherited_options:
+    - option: dry-run
+      value_type: bool
+      default_value: "false"
+      description: Execute command in dry run mode
+      deprecated: false
+      hidden: false
+      experimental: false
+      experimentalcli: false
+      kubernetes: false
+      swarm: false
+deprecated: false
+hidden: false
+experimental: false
+experimentalcli: false
+kubernetes: false
+swarm: false
+
diff --git a/pkg/api/api.go b/pkg/api/api.go
index 1be88389..d3e8f0e9 100644
--- a/pkg/api/api.go
+++ b/pkg/api/api.go
@@ -26,6 +26,7 @@ import (
 	"github.com/compose-spec/compose-go/v2/types"
 	"github.com/containerd/platforms"
 	"github.com/docker/cli/opts"
+	"github.com/docker/docker/api/types/volume"
 )
 
 // Service manages a compose project
@@ -98,8 +99,16 @@ type Service interface {
 	Commit(ctx context.Context, projectName string, options CommitOptions) error
 	// Generate generates a Compose Project from existing containers
 	Generate(ctx context.Context, options GenerateOptions) (*types.Project, error)
+	// Volumes executes the equivalent to a `docker volume ls`
+	Volumes(ctx context.Context, project *types.Project, options VolumesOptions) ([]VolumesSummary, error)
 }
 
+type VolumesOptions struct {
+	Services []string
+}
+
+type VolumesSummary = *volume.Volume
+
 type ScaleOptions struct {
 	Services []string
 }
diff --git a/pkg/compose/volumes.go b/pkg/compose/volumes.go
new file mode 100644
index 00000000..8c7bbed2
--- /dev/null
+++ b/pkg/compose/volumes.go
@@ -0,0 +1,85 @@
+/*
+   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"
+	"slices"
+
+	"github.com/compose-spec/compose-go/v2/types"
+	"github.com/docker/compose/v2/pkg/api"
+	"github.com/docker/docker/api/types/container"
+	"github.com/docker/docker/api/types/filters"
+	"github.com/docker/docker/api/types/volume"
+)
+
+func (s *composeService) Volumes(ctx context.Context, project *types.Project, options api.VolumesOptions) ([]api.VolumesSummary, error) {
+	projectName := project.Name
+
+	allContainers, err := s.apiClient().ContainerList(ctx, container.ListOptions{
+		Filters: filters.NewArgs(projectFilter(projectName)),
+	})
+	if err != nil {
+		return nil, err
+	}
+
+	var containers []container.Summary
+
+	if len(options.Services) > 0 {
+		// filter service containers
+		for _, c := range allContainers {
+			if slices.Contains(options.Services, c.Labels[api.ServiceLabel]) {
+				containers = append(containers, c)
+			}
+		}
+	} else {
+		containers = allContainers
+	}
+
+	volumesResponse, err := s.apiClient().VolumeList(ctx, volume.ListOptions{
+		Filters: filters.NewArgs(projectFilter(projectName)),
+	})
+	if err != nil {
+		return nil, err
+	}
+
+	projectVolumes := volumesResponse.Volumes
+
+	if len(options.Services) == 0 {
+		return projectVolumes, nil
+	}
+
+	var volumes []api.VolumesSummary
+
+	// create a name lookup of volumes used by containers
+	serviceVolumes := make(map[string]bool)
+
+	for _, container := range containers {
+		for _, mount := range container.Mounts {
+			serviceVolumes[mount.Name] = true
+		}
+	}
+
+	// append if volumes in this project are in serviceVolumes
+	for _, v := range projectVolumes {
+		if serviceVolumes[v.Name] {
+			volumes = append(volumes, v)
+		}
+	}
+
+	return volumes, nil
+}
diff --git a/pkg/mocks/mock_docker_compose_api.go b/pkg/mocks/mock_docker_compose_api.go
index 811187ea..6bb065f3 100644
--- a/pkg/mocks/mock_docker_compose_api.go
+++ b/pkg/mocks/mock_docker_compose_api.go
@@ -497,6 +497,21 @@ func (mr *MockServiceMockRecorder) Viz(ctx, project, options any) *gomock.Call {
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Viz", reflect.TypeOf((*MockService)(nil).Viz), ctx, project, options)
 }
 
+// Volumes mocks base method.
+func (m *MockService) Volumes(ctx context.Context, project *types.Project, options api.VolumesOptions) ([]api.VolumesSummary, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "Volumes", ctx, project, options)
+	ret0, _ := ret[0].([]api.VolumesSummary)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// Volumes indicates an expected call of Volumes.
+func (mr *MockServiceMockRecorder) Volumes(ctx, project, options any) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Volumes", reflect.TypeOf((*MockService)(nil).Volumes), ctx, project, options)
+}
+
 // Wait mocks base method.
 func (m *MockService) Wait(ctx context.Context, projectName string, options api.WaitOptions) (int64, error) {
 	m.ctrl.T.Helper()



Test patch:
diff --git a/pkg/compose/volumes_test.go b/pkg/compose/volumes_test.go
new file mode 100644
index 00000000..8e149159
--- /dev/null
+++ b/pkg/compose/volumes_test.go
@@ -0,0 +1,89 @@
+/*
+   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"
+	"testing"
+
+	"github.com/compose-spec/compose-go/v2/types"
+	"github.com/docker/compose/v2/pkg/api"
+	"github.com/docker/docker/api/types/container"
+	"github.com/docker/docker/api/types/filters"
+	"github.com/docker/docker/api/types/volume"
+	"go.uber.org/mock/gomock"
+	"gotest.tools/v3/assert"
+)
+
+func TestVolumes(t *testing.T) {
+	mockCtrl := gomock.NewController(t)
+	defer mockCtrl.Finish()
+
+	mockApi, mockCli := prepareMocks(mockCtrl)
+	tested := composeService{
+		dockerCli: mockCli,
+	}
+
+	// Create test volumes
+	vol1 := &volume.Volume{Name: testProject + "_vol1"}
+	vol2 := &volume.Volume{Name: testProject + "_vol2"}
+	vol3 := &volume.Volume{Name: testProject + "_vol3"}
+
+	// Create test containers with volume mounts
+	c1 := container.Summary{
+		Labels: map[string]string{api.ServiceLabel: "service1"},
+		Mounts: []container.MountPoint{
+			{Name: testProject + "_vol1"},
+			{Name: testProject + "_vol2"},
+		},
+	}
+	c2 := container.Summary{
+		Labels: map[string]string{api.ServiceLabel: "service2"},
+		Mounts: []container.MountPoint{
+			{Name: testProject + "_vol3"},
+		},
+	}
+
+	ctx := context.Background()
+	project := &types.Project{Name: testProject}
+	args := filters.NewArgs(projectFilter(testProject))
+	listOpts := container.ListOptions{Filters: args}
+	volumeListArgs := filters.NewArgs(projectFilter(testProject))
+	volumeListOpts := volume.ListOptions{Filters: volumeListArgs}
+	volumeReturn := volume.ListResponse{
+		Volumes: []*volume.Volume{vol1, vol2, vol3},
+	}
+	containerReturn := []container.Summary{c1, c2}
+
+	// Mock API calls
+	mockApi.EXPECT().ContainerList(ctx, listOpts).Times(2).Return(containerReturn, nil)
+	mockApi.EXPECT().VolumeList(ctx, volumeListOpts).Times(2).Return(volumeReturn, nil)
+
+	// Test without service filter - should return all project volumes
+	volumeOptions := api.VolumesOptions{}
+	volumes, err := tested.Volumes(ctx, project, volumeOptions)
+	expected := []api.VolumesSummary{vol1, vol2, vol3}
+	assert.NilError(t, err)
+	assert.DeepEqual(t, volumes, expected)
+
+	// Test with service filter - should only return volumes used by service1
+	volumeOptions = api.VolumesOptions{Services: []string{"service1"}}
+	volumes, err = tested.Volumes(ctx, project, volumeOptions)
+	expected = []api.VolumesSummary{vol1, vol2}
+	assert.NilError(t, err)
+	assert.DeepEqual(t, volumes, expected)
+}