From c0f0b82fa2355535c347ef5c7e271c3ad62a70e7 Mon Sep 17 00:00:00 2001 From: Nick Craig-Wood Date: Thu, 5 Mar 2026 17:57:09 +0000 Subject: [PATCH] docker serve: make Create idempotent to avoid "volume already exists" after restart Docker may re-send Create requests for volumes that already exist, especially after a plugin restart. Previously this returned ErrVolumeExists which Docker surfaced as "volume name must be unique". Now if a volume with the same name already exists, Create returns success (no-op), matching the Docker volume plugin protocol's expectation of idempotent operations. --- cmd/serve/docker/docker_test.go | 8 +++++--- cmd/serve/docker/driver.go | 11 ++++++++--- cmd/serve/docker/volume.go | 1 - 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/cmd/serve/docker/docker_test.go b/cmd/serve/docker/docker_test.go index e5df97676..f57382711 100644 --- a/cmd/serve/docker/docker_test.go +++ b/cmd/serve/docker/docker_test.go @@ -94,7 +94,8 @@ func TestDockerPluginLogic(t *testing.T) { assert.NoError(t, drv.Create(volReq)) path1 := filepath.Join(testDir, "vol1") - assert.ErrorIs(t, drv.Create(volReq), docker.ErrVolumeExists) + // Create is idempotent - creating the same volume again should succeed + assert.NoError(t, drv.Create(volReq)) getReq := &docker.GetRequest{Name: "vol1"} getRes, err := drv.Get(getReq) @@ -373,8 +374,9 @@ func testMountAPI(t *testing.T, sockAddr string) { } cli.request("Create", createReq, &res, false) assert.Equal(t, "{}", res) - cli.request("Create", createReq, &res, true) - assert.Contains(t, res, "volume already exists") + // Create is idempotent - creating the same volume again should succeed + cli.request("Create", createReq, &res, false) + assert.Equal(t, "{}", res) mountReq := docker.MountRequest{Name: "vol1", ID: "id1"} var mountRes docker.MountResponse diff --git a/cmd/serve/docker/driver.go b/cmd/serve/docker/driver.go index 8a4d3b9bb..ebb7ca6f8 100644 --- a/cmd/serve/docker/driver.go +++ b/cmd/serve/docker/driver.go @@ -182,8 +182,12 @@ func reportErr(err error) { } } -// Create volume -// To use subpath we are limited to defining a new volume definition via alias +// Create volume. +// +// If the volume already exists, the request is treated as a no-op +// (idempotent) so that Docker can safely re-send Create requests +// after a plugin restart without getting "volume already exists". +// To use subpath we are limited to defining a new volume definition via alias. func (drv *Driver) Create(req *CreateRequest) error { ctx := context.Background() drv.mu.Lock() @@ -193,7 +197,8 @@ func (drv *Driver) Create(req *CreateRequest) error { fs.Debugf(nil, "Create volume %q", name) if vol, _ := drv.getVolume(name); vol != nil { - return ErrVolumeExists + fs.Debugf(nil, "Volume %q already exists, treating Create as no-op", name) + return nil } vol, err := newVolume(ctx, name, req.Options, drv) diff --git a/cmd/serve/docker/volume.go b/cmd/serve/docker/volume.go index fc4034850..4f69cb627 100644 --- a/cmd/serve/docker/volume.go +++ b/cmd/serve/docker/volume.go @@ -20,7 +20,6 @@ import ( // Errors var ( ErrVolumeNotFound = errors.New("volume not found") - ErrVolumeExists = errors.New("volume already exists") ErrMountpointExists = errors.New("non-empty mountpoint already exists") )