Merge pull request #51310 from thaJeztah/volume_output_structs

client: VolumeRemove, VolumeUpdate: add output struct
This commit is contained in:
Austin Vazquez
2025-10-28 18:14:24 -05:00
committed by GitHub
15 changed files with 99 additions and 65 deletions

View File

@@ -10,7 +10,6 @@ import (
"github.com/moby/moby/api/types/events"
"github.com/moby/moby/api/types/network"
"github.com/moby/moby/api/types/registry"
"github.com/moby/moby/api/types/swarm"
"github.com/moby/moby/api/types/system"
)
@@ -189,9 +188,9 @@ type VolumeAPIClient interface {
VolumeCreate(ctx context.Context, options VolumeCreateOptions) (VolumeCreateResult, error)
VolumeInspect(ctx context.Context, volumeID string, options VolumeInspectOptions) (VolumeInspectResult, error)
VolumeList(ctx context.Context, options VolumeListOptions) (VolumeListResult, error)
VolumeRemove(ctx context.Context, volumeID string, options VolumeRemoveOptions) error
VolumesPrune(ctx context.Context, opts VolumePruneOptions) (VolumePruneResult, error)
VolumeUpdate(ctx context.Context, volumeID string, version swarm.Version, options VolumeUpdateOptions) error
VolumeRemove(ctx context.Context, volumeID string, options VolumeRemoveOptions) (VolumeRemoveResult, error)
VolumesPrune(ctx context.Context, options VolumePruneOptions) (VolumePruneResult, error)
VolumeUpdate(ctx context.Context, volumeID string, options VolumeUpdateOptions) (VolumeUpdateResult, error)
}
// SecretAPIClient defines API client methods for secrets

View File

@@ -26,19 +26,19 @@ type VolumePruneResult struct {
}
// VolumesPrune requests the daemon to delete unused data
func (cli *Client) VolumesPrune(ctx context.Context, opts VolumePruneOptions) (VolumePruneResult, error) {
if opts.All {
if _, ok := opts.Filters["all"]; ok {
func (cli *Client) VolumesPrune(ctx context.Context, options VolumePruneOptions) (VolumePruneResult, error) {
if options.All {
if _, ok := options.Filters["all"]; ok {
return VolumePruneResult{}, errdefs.ErrInvalidArgument.WithMessage(`conflicting options: cannot specify both "all" and "all" filter`)
}
if opts.Filters == nil {
opts.Filters = Filters{}
if options.Filters == nil {
options.Filters = Filters{}
}
opts.Filters.Add("all", "true")
options.Filters.Add("all", "true")
}
query := url.Values{}
opts.Filters.updateURLValues(query)
options.Filters.updateURLValues(query)
resp, err := cli.post(ctx, "/volumes/prune", query, nil, nil)
defer ensureReaderClosed(resp)
@@ -48,7 +48,7 @@ func (cli *Client) VolumesPrune(ctx context.Context, opts VolumePruneOptions) (V
var report volume.PruneReport
if err := json.NewDecoder(resp.Body).Decode(&report); err != nil {
return VolumePruneResult{}, fmt.Errorf("Error retrieving volume prune report: %v", err)
return VolumePruneResult{}, fmt.Errorf("error retrieving volume prune report: %v", err)
}
return VolumePruneResult{Report: report}, nil

View File

@@ -5,17 +5,22 @@ import (
"net/url"
)
// VolumeRemoveOptions holds optional parameters for volume removal.
// VolumeRemoveOptions holds options for [Client.VolumeRemove].
type VolumeRemoveOptions struct {
// Force the removal of the volume
Force bool
}
// VolumeRemoveResult holds the result of [Client.VolumeRemove],
type VolumeRemoveResult struct {
// Add future fields here.
}
// VolumeRemove removes a volume from the docker host.
func (cli *Client) VolumeRemove(ctx context.Context, volumeID string, options VolumeRemoveOptions) error {
func (cli *Client) VolumeRemove(ctx context.Context, volumeID string, options VolumeRemoveOptions) (VolumeRemoveResult, error) {
volumeID, err := trimID("volume", volumeID)
if err != nil {
return err
return VolumeRemoveResult{}, err
}
query := url.Values{}
@@ -24,5 +29,8 @@ func (cli *Client) VolumeRemove(ctx context.Context, volumeID string, options Vo
}
resp, err := cli.delete(ctx, "/volumes/"+volumeID, query, nil)
defer ensureReaderClosed(resp)
return err
if err != nil {
return VolumeRemoveResult{}, err
}
return VolumeRemoveResult{}, nil
}

View File

@@ -1,7 +1,6 @@
package client
import (
"context"
"fmt"
"net/http"
"testing"
@@ -15,14 +14,14 @@ func TestVolumeRemoveError(t *testing.T) {
client, err := NewClientWithOpts(WithMockClient(errorMock(http.StatusInternalServerError, "Server error")))
assert.NilError(t, err)
err = client.VolumeRemove(context.Background(), "volume_id", VolumeRemoveOptions{})
_, err = client.VolumeRemove(t.Context(), "volume_id", VolumeRemoveOptions{})
assert.Check(t, is.ErrorType(err, cerrdefs.IsInternal))
err = client.VolumeRemove(context.Background(), "", VolumeRemoveOptions{})
_, err = client.VolumeRemove(t.Context(), "", VolumeRemoveOptions{})
assert.Check(t, is.ErrorType(err, cerrdefs.IsInvalidArgument))
assert.Check(t, is.ErrorContains(err, "value is empty"))
err = client.VolumeRemove(context.Background(), " ", VolumeRemoveOptions{})
_, err = client.VolumeRemove(t.Context(), " ", VolumeRemoveOptions{})
assert.Check(t, is.ErrorType(err, cerrdefs.IsInvalidArgument))
assert.Check(t, is.ErrorContains(err, "value is empty"))
}
@@ -35,7 +34,7 @@ func TestVolumeRemoveConnectionError(t *testing.T) {
client, err := NewClientWithOpts(WithAPIVersionNegotiation(), WithHost("tcp://no-such-host.invalid"))
assert.NilError(t, err)
err = client.VolumeRemove(context.Background(), "volume_id", VolumeRemoveOptions{})
_, err = client.VolumeRemove(t.Context(), "volume_id", VolumeRemoveOptions{})
assert.Check(t, is.ErrorType(err, IsErrConnectionFailed))
}
@@ -54,6 +53,6 @@ func TestVolumeRemove(t *testing.T) {
}))
assert.NilError(t, err)
err = client.VolumeRemove(context.Background(), "volume_id", VolumeRemoveOptions{Force: true})
_, err = client.VolumeRemove(t.Context(), "volume_id", VolumeRemoveOptions{Force: true})
assert.NilError(t, err)
}

View File

@@ -8,23 +8,33 @@ import (
"github.com/moby/moby/api/types/volume"
)
// VolumeUpdateOptions holds options for [Client.VolumeUpdate].
type VolumeUpdateOptions struct {
Version swarm.Version
// Spec is the ClusterVolumeSpec to update the volume to.
Spec *volume.ClusterVolumeSpec `json:"Spec,omitempty"`
}
// VolumeUpdateResult holds the result of [Client.VolumeUpdate],
type VolumeUpdateResult struct {
// Add future fields here.
}
// VolumeUpdate updates a volume. This only works for Cluster Volumes, and
// only some fields can be updated.
func (cli *Client) VolumeUpdate(ctx context.Context, volumeID string, version swarm.Version, options VolumeUpdateOptions) error {
func (cli *Client) VolumeUpdate(ctx context.Context, volumeID string, options VolumeUpdateOptions) (VolumeUpdateResult, error) {
volumeID, err := trimID("volume", volumeID)
if err != nil {
return err
return VolumeUpdateResult{}, err
}
query := url.Values{}
query.Set("version", version.String())
query.Set("version", options.Version.String())
resp, err := cli.put(ctx, "/volumes/"+volumeID, query, options, nil)
defer ensureReaderClosed(resp)
return err
if err != nil {
return VolumeUpdateResult{}, err
}
return VolumeUpdateResult{}, nil
}

View File

@@ -1,7 +1,6 @@
package client
import (
"context"
"fmt"
"net/http"
"strings"
@@ -17,14 +16,14 @@ func TestVolumeUpdateError(t *testing.T) {
client, err := NewClientWithOpts(WithMockClient(errorMock(http.StatusInternalServerError, "Server error")))
assert.NilError(t, err)
err = client.VolumeUpdate(context.Background(), "volume", swarm.Version{}, VolumeUpdateOptions{})
_, err = client.VolumeUpdate(t.Context(), "volume", VolumeUpdateOptions{})
assert.Check(t, is.ErrorType(err, cerrdefs.IsInternal))
err = client.VolumeUpdate(context.Background(), "", swarm.Version{}, VolumeUpdateOptions{})
_, err = client.VolumeUpdate(t.Context(), "", VolumeUpdateOptions{})
assert.Check(t, is.ErrorType(err, cerrdefs.IsInvalidArgument))
assert.Check(t, is.ErrorContains(err, "value is empty"))
err = client.VolumeUpdate(context.Background(), " ", swarm.Version{}, VolumeUpdateOptions{})
_, err = client.VolumeUpdate(t.Context(), " ", VolumeUpdateOptions{})
assert.Check(t, is.ErrorType(err, cerrdefs.IsInvalidArgument))
assert.Check(t, is.ErrorContains(err, "value is empty"))
}
@@ -46,6 +45,8 @@ func TestVolumeUpdate(t *testing.T) {
}))
assert.NilError(t, err)
err = client.VolumeUpdate(context.Background(), "test1", swarm.Version{Index: uint64(10)}, VolumeUpdateOptions{})
_, err = client.VolumeUpdate(t.Context(), "test1", VolumeUpdateOptions{
Version: swarm.Version{Index: uint64(10)},
})
assert.NilError(t, err)
}

View File

@@ -566,7 +566,7 @@ func testLiveRestoreVolumeReferences(t *testing.T) {
d.Restart(t, "--live-restore", "--iptables=false", "--ip6tables=false")
// Try to remove the volume
err = c.VolumeRemove(ctx, volName, client.VolumeRemoveOptions{})
_, err = c.VolumeRemove(ctx, volName, client.VolumeRemoveOptions{})
assert.ErrorContains(t, err, "volume is in use")
_, err = c.VolumeInspect(ctx, volName, client.VolumeInspectOptions{})
@@ -626,7 +626,7 @@ func testLiveRestoreVolumeReferences(t *testing.T) {
// Try to remove the volume
// This should fail since its used by a container
err = c.VolumeRemove(ctx, v.Name, client.VolumeRemoveOptions{})
_, err = c.VolumeRemove(ctx, v.Name, client.VolumeRemoveOptions{})
assert.ErrorContains(t, err, "volume is in use")
t.Run("volume still mounted", func(t *testing.T) {
@@ -660,7 +660,7 @@ func testLiveRestoreVolumeReferences(t *testing.T) {
assert.NilError(t, err)
// Now we should be able to remove the volume
err = c.VolumeRemove(ctx, v.Name, client.VolumeRemoveOptions{})
_, err = c.VolumeRemove(ctx, v.Name, client.VolumeRemoveOptions{})
assert.NilError(t, err)
})

View File

@@ -101,7 +101,7 @@ func TestAuthZPluginV2RejectVolumeRequests(t *testing.T) {
assert.ErrorContains(t, err, fmt.Sprintf("Error response from daemon: plugin %s failed with error:", authzPluginNameWithTag))
// The plugin will block the command before it can determine the volume does not exist
err = c.VolumeRemove(ctx, "test", client.VolumeRemoveOptions{})
_, err = c.VolumeRemove(ctx, "test", client.VolumeRemoveOptions{})
assert.Assert(t, err != nil)
assert.ErrorContains(t, err, fmt.Sprintf("Error response from daemon: plugin %s failed with error:", authzPluginNameWithTag))

View File

@@ -249,7 +249,7 @@ func setupTestVolume(t *testing.T, apiClient client.APIClient) string {
volumeName := t.Name() + "-volume"
err := apiClient.VolumeRemove(ctx, volumeName, client.VolumeRemoveOptions{Force: true})
_, err := apiClient.VolumeRemove(ctx, volumeName, client.VolumeRemoveOptions{Force: true})
assert.NilError(t, err, "failed to clean volume")
_, err = apiClient.VolumeCreate(ctx, client.VolumeCreateOptions{

View File

@@ -76,7 +76,7 @@ func TestVolumesRemove(t *testing.T) {
vname := inspect.Container.Mounts[0].Name
t.Run("volume in use", func(t *testing.T) {
err = apiClient.VolumeRemove(ctx, vname, client.VolumeRemoveOptions{})
_, err = apiClient.VolumeRemove(ctx, vname, client.VolumeRemoveOptions{})
assert.Check(t, is.ErrorType(err, cerrdefs.IsConflict))
assert.Check(t, is.ErrorContains(err, "volume is in use"))
})
@@ -87,17 +87,17 @@ func TestVolumesRemove(t *testing.T) {
})
assert.NilError(t, err)
err = apiClient.VolumeRemove(ctx, vname, client.VolumeRemoveOptions{})
_, err = apiClient.VolumeRemove(ctx, vname, client.VolumeRemoveOptions{})
assert.NilError(t, err)
})
t.Run("non-existing volume", func(t *testing.T) {
err = apiClient.VolumeRemove(ctx, "no_such_volume", client.VolumeRemoveOptions{})
_, err = apiClient.VolumeRemove(ctx, "no_such_volume", client.VolumeRemoveOptions{})
assert.Check(t, is.ErrorType(err, cerrdefs.IsNotFound))
})
t.Run("non-existing volume force", func(t *testing.T) {
err = apiClient.VolumeRemove(ctx, "no_such_volume", client.VolumeRemoveOptions{Force: true})
_, err = apiClient.VolumeRemove(ctx, "no_such_volume", client.VolumeRemoveOptions{Force: true})
assert.NilError(t, err)
})
}
@@ -128,7 +128,7 @@ func TestVolumesRemoveSwarmEnabled(t *testing.T) {
vname := inspect.Container.Mounts[0].Name
t.Run("volume in use", func(t *testing.T) {
err = apiClient.VolumeRemove(ctx, vname, client.VolumeRemoveOptions{})
_, err = apiClient.VolumeRemove(ctx, vname, client.VolumeRemoveOptions{})
assert.Check(t, is.ErrorType(err, cerrdefs.IsConflict))
assert.Check(t, is.ErrorContains(err, "volume is in use"))
})
@@ -139,17 +139,17 @@ func TestVolumesRemoveSwarmEnabled(t *testing.T) {
})
assert.NilError(t, err)
err = apiClient.VolumeRemove(ctx, vname, client.VolumeRemoveOptions{})
_, err = apiClient.VolumeRemove(ctx, vname, client.VolumeRemoveOptions{})
assert.NilError(t, err)
})
t.Run("non-existing volume", func(t *testing.T) {
err = apiClient.VolumeRemove(ctx, "no_such_volume", client.VolumeRemoveOptions{})
_, err = apiClient.VolumeRemove(ctx, "no_such_volume", client.VolumeRemoveOptions{})
assert.Check(t, is.ErrorType(err, cerrdefs.IsNotFound))
})
t.Run("non-existing volume force", func(t *testing.T) {
err = apiClient.VolumeRemove(ctx, "no_such_volume", client.VolumeRemoveOptions{Force: true})
_, err = apiClient.VolumeRemove(ctx, "no_such_volume", client.VolumeRemoveOptions{Force: true})
assert.NilError(t, err)
})
}

View File

@@ -134,7 +134,7 @@ func deleteAllVolumes(ctx context.Context, t testing.TB, c client.VolumeAPIClien
if _, ok := protectedVolumes[v.Name]; ok {
continue
}
err := c.VolumeRemove(ctx, v.Name, client.VolumeRemoveOptions{
_, err := c.VolumeRemove(ctx, v.Name, client.VolumeRemoveOptions{
Force: true,
})
assert.Check(t, err, "failed to remove volume %s", v.Name)

View File

@@ -10,7 +10,6 @@ import (
"github.com/moby/moby/api/types/events"
"github.com/moby/moby/api/types/network"
"github.com/moby/moby/api/types/registry"
"github.com/moby/moby/api/types/swarm"
"github.com/moby/moby/api/types/system"
)
@@ -189,9 +188,9 @@ type VolumeAPIClient interface {
VolumeCreate(ctx context.Context, options VolumeCreateOptions) (VolumeCreateResult, error)
VolumeInspect(ctx context.Context, volumeID string, options VolumeInspectOptions) (VolumeInspectResult, error)
VolumeList(ctx context.Context, options VolumeListOptions) (VolumeListResult, error)
VolumeRemove(ctx context.Context, volumeID string, options VolumeRemoveOptions) error
VolumesPrune(ctx context.Context, opts VolumePruneOptions) (VolumePruneResult, error)
VolumeUpdate(ctx context.Context, volumeID string, version swarm.Version, options VolumeUpdateOptions) error
VolumeRemove(ctx context.Context, volumeID string, options VolumeRemoveOptions) (VolumeRemoveResult, error)
VolumesPrune(ctx context.Context, options VolumePruneOptions) (VolumePruneResult, error)
VolumeUpdate(ctx context.Context, volumeID string, options VolumeUpdateOptions) (VolumeUpdateResult, error)
}
// SecretAPIClient defines API client methods for secrets

View File

@@ -26,19 +26,19 @@ type VolumePruneResult struct {
}
// VolumesPrune requests the daemon to delete unused data
func (cli *Client) VolumesPrune(ctx context.Context, opts VolumePruneOptions) (VolumePruneResult, error) {
if opts.All {
if _, ok := opts.Filters["all"]; ok {
func (cli *Client) VolumesPrune(ctx context.Context, options VolumePruneOptions) (VolumePruneResult, error) {
if options.All {
if _, ok := options.Filters["all"]; ok {
return VolumePruneResult{}, errdefs.ErrInvalidArgument.WithMessage(`conflicting options: cannot specify both "all" and "all" filter`)
}
if opts.Filters == nil {
opts.Filters = Filters{}
if options.Filters == nil {
options.Filters = Filters{}
}
opts.Filters.Add("all", "true")
options.Filters.Add("all", "true")
}
query := url.Values{}
opts.Filters.updateURLValues(query)
options.Filters.updateURLValues(query)
resp, err := cli.post(ctx, "/volumes/prune", query, nil, nil)
defer ensureReaderClosed(resp)
@@ -48,7 +48,7 @@ func (cli *Client) VolumesPrune(ctx context.Context, opts VolumePruneOptions) (V
var report volume.PruneReport
if err := json.NewDecoder(resp.Body).Decode(&report); err != nil {
return VolumePruneResult{}, fmt.Errorf("Error retrieving volume prune report: %v", err)
return VolumePruneResult{}, fmt.Errorf("error retrieving volume prune report: %v", err)
}
return VolumePruneResult{Report: report}, nil

View File

@@ -5,17 +5,22 @@ import (
"net/url"
)
// VolumeRemoveOptions holds optional parameters for volume removal.
// VolumeRemoveOptions holds options for [Client.VolumeRemove].
type VolumeRemoveOptions struct {
// Force the removal of the volume
Force bool
}
// VolumeRemoveResult holds the result of [Client.VolumeRemove],
type VolumeRemoveResult struct {
// Add future fields here.
}
// VolumeRemove removes a volume from the docker host.
func (cli *Client) VolumeRemove(ctx context.Context, volumeID string, options VolumeRemoveOptions) error {
func (cli *Client) VolumeRemove(ctx context.Context, volumeID string, options VolumeRemoveOptions) (VolumeRemoveResult, error) {
volumeID, err := trimID("volume", volumeID)
if err != nil {
return err
return VolumeRemoveResult{}, err
}
query := url.Values{}
@@ -24,5 +29,8 @@ func (cli *Client) VolumeRemove(ctx context.Context, volumeID string, options Vo
}
resp, err := cli.delete(ctx, "/volumes/"+volumeID, query, nil)
defer ensureReaderClosed(resp)
return err
if err != nil {
return VolumeRemoveResult{}, err
}
return VolumeRemoveResult{}, nil
}

View File

@@ -8,23 +8,33 @@ import (
"github.com/moby/moby/api/types/volume"
)
// VolumeUpdateOptions holds options for [Client.VolumeUpdate].
type VolumeUpdateOptions struct {
Version swarm.Version
// Spec is the ClusterVolumeSpec to update the volume to.
Spec *volume.ClusterVolumeSpec `json:"Spec,omitempty"`
}
// VolumeUpdateResult holds the result of [Client.VolumeUpdate],
type VolumeUpdateResult struct {
// Add future fields here.
}
// VolumeUpdate updates a volume. This only works for Cluster Volumes, and
// only some fields can be updated.
func (cli *Client) VolumeUpdate(ctx context.Context, volumeID string, version swarm.Version, options VolumeUpdateOptions) error {
func (cli *Client) VolumeUpdate(ctx context.Context, volumeID string, options VolumeUpdateOptions) (VolumeUpdateResult, error) {
volumeID, err := trimID("volume", volumeID)
if err != nil {
return err
return VolumeUpdateResult{}, err
}
query := url.Values{}
query.Set("version", version.String())
query.Set("version", options.Version.String())
resp, err := cli.put(ctx, "/volumes/"+volumeID, query, options, nil)
defer ensureReaderClosed(resp)
return err
if err != nil {
return VolumeUpdateResult{}, err
}
return VolumeUpdateResult{}, nil
}