Skip to content
Open
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
2 changes: 2 additions & 0 deletions pkg/api/labels.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ const (
ContainerNumberLabel = "com.docker.compose.container-number"
// VolumeLabel allow to track resource related to a compose volume
VolumeLabel = "com.docker.compose.volume"
// VolumeRecreateWhenSpecUpdatedLabel when set to true, volume will be recreated when spec updated
VolumeRecreateWhenSpecUpdatedLabel = "com.docker.compose.volume.recreate-when-spec-updated"
// NetworkLabel allow to track resource related to a compose network
NetworkLabel = "com.docker.compose.network"
// WorkingDirLabel stores absolute path to compose project working directory
Expand Down
16 changes: 15 additions & 1 deletion pkg/compose/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import (
cdi "tags.cncf.io/container-device-interface/pkg/parser"

"github.com/docker/compose/v5/pkg/api"
"github.com/docker/compose/v5/pkg/utils"
)

type createOptions struct {
Expand Down Expand Up @@ -1624,7 +1625,7 @@ func (s *composeService) ensureVolume(ctx context.Context, name string, volume t
actual, ok := inspected.Volume.Labels[api.ConfigHashLabel]
if ok && actual != expected {
msg := fmt.Sprintf("Volume %q exists but doesn't match configuration in compose file. Recreate (data will be lost)?", volume.Name)
confirm, err := s.prompt(msg, false)
confirm, err := confirmVolumeRecreate(inspected.Volume.Labels, s.prompt, msg)
if err != nil {
return "", err
}
Expand All @@ -1639,6 +1640,19 @@ func (s *composeService) ensureVolume(ctx context.Context, name string, volume t
return inspected.Volume.Name, nil
}

func confirmVolumeRecreate(labels map[string]string, prompt Prompt, promptMsg string) (bool, error) {
recreate, ok := labels[api.VolumeRecreateWhenSpecUpdatedLabel]
if ok {
return utils.StringToBool(recreate), nil
} else {
c, err := prompt(promptMsg, false)
if err != nil {
return false, err
}
return c, nil
}
}

func (s *composeService) removeDivergedVolume(ctx context.Context, name string, volume types.VolumeConfig, project *types.Project) error {
// Remove services mounting divergent volume
var services []string
Expand Down
82 changes: 82 additions & 0 deletions pkg/compose/create_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,13 @@
package compose

import (
"io"
"net"
"net/netip"
"os"
"path/filepath"
"sort"
"strings"
"testing"

composeloader "github.com/compose-spec/compose-go/v2/loader"
Expand All @@ -35,6 +37,8 @@ import (
"gotest.tools/v3/assert"
"gotest.tools/v3/assert/cmp"

"github.com/docker/cli/cli/streams"
"github.com/docker/compose/v5/cmd/prompt"
"github.com/docker/compose/v5/pkg/api"
)

Expand Down Expand Up @@ -484,3 +488,81 @@ volumes:
})
}
}

func Test_composeService_confirmVolumeRecreate(t *testing.T) {
tests := []struct {
name string
labels map[string]string
input string
want bool
wantErr bool
}{
{
name: "no labels no input",
labels: nil,
input: "",
want: false,
wantErr: false,
},
{
name: "no labels and input is y",
labels: nil,
input: "y",
want: true,
wantErr: false,
},
{
name: "no labels and input is true",
labels: nil,
input: "true",
want: true,
wantErr: false,
},
{
name: "no labels and input is no",
labels: nil,
input: "no",
want: false,
wantErr: false,
},
{
name: "no input, has labels recreate true",
labels: map[string]string{api.VolumeRecreateWhenSpecUpdatedLabel: "true"},
want: true,
wantErr: false,
},
{
name: "no input, has labels recreate TRUE",
labels: map[string]string{api.VolumeRecreateWhenSpecUpdatedLabel: "TRUE"},
want: true,
wantErr: false,
},
{
name: "no input, has labels recreate false",
labels: map[string]string{api.VolumeRecreateWhenSpecUpdatedLabel: "false"},
want: false,
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
prompt := prompt.NewPrompt(
streams.NewIn(io.NopCloser(strings.NewReader(tt.input))),
streams.NewOut(t.Output())).Confirm

got, gotErr := confirmVolumeRecreate(tt.labels, prompt, "promptMsg")
if gotErr != nil {
if !tt.wantErr {
t.Errorf("confirmVolumeRecreate() failed: %v", gotErr)
}
return
}
if tt.wantErr {
t.Fatal("confirmVolumeRecreate() succeeded unexpectedly")
}
if tt.want != got {
t.Errorf("confirmVolumeRecreate() = %v, want %v", got, tt.want)
}
})
}
}
11 changes: 11 additions & 0 deletions pkg/e2e/fixtures/recreate-volumes/label-new.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
services:
app:
image: alpine
volumes:
- my_vol:/my_vol

volumes:
my_vol:
labels:
com.docker.compose.volume.recreate-when-spec-updated: "true"
foo: zot
12 changes: 12 additions & 0 deletions pkg/e2e/fixtures/recreate-volumes/label-old.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
services:
app:
image: alpine
volumes:
- my_vol:/my_vol

volumes:
my_vol:
labels:
com.docker.compose.volume.recreate-when-spec-updated: "true"
foo: bar

18 changes: 18 additions & 0 deletions pkg/e2e/volumes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,24 @@ func TestUpRecreateVolumes_IgnoreBinds(t *testing.T) {
assert.Check(t, !strings.Contains(res.Combined(), "Recreated"))
}

func TestUpRecreateVolumes_RecreateLabel(t *testing.T) {
c := NewCLI(t)
const projectName = "compose-e2e-recreate-volumes-recreate-label"
t.Cleanup(func() {
c.cleanupWithDown(t, projectName)
})

c.RunDockerComposeCmd(t, "-f", "./fixtures/recreate-volumes/label-old.yml", "--project-name", projectName, "up", "-d")

res := c.RunDockerCmd(t, "volume", "inspect", fmt.Sprintf("%s_my_vol", projectName), "-f", "{{ index .Labels \"foo\" }}")
res.Assert(t, icmd.Expected{Out: "bar"})

res = c.RunDockerComposeCmd(t, "-f", "./fixtures/recreate-volumes/label-new.yml", "--project-name", projectName, "up", "-d")

res = c.RunDockerCmd(t, "volume", "inspect", fmt.Sprintf("%s_my_vol", projectName), "-f", "{{ index .Labels \"foo\" }}")
res.Assert(t, icmd.Expected{Out: "zot"})
}

func TestImageVolume(t *testing.T) {
c := NewCLI(t)
const projectName = "compose-e2e-image-volume"
Expand Down