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. WARNING, recreate data will be lost possibly depending on the volume driver.
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
27 changes: 25 additions & 2 deletions 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 @@ -1623,8 +1624,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, volume.Name)
if err != nil {
return "", err
}
Expand All @@ -1639,6 +1639,29 @@ func (s *composeService) ensureVolume(ctx context.Context, name string, volume t
return inspected.Volume.Name, nil
}

func confirmVolumeRecreate(labels map[string]string, prompt Prompt, name string) (bool, error) {
recreate, ok := labels[api.VolumeRecreateWhenSpecUpdatedLabel]
msg := fmt.Sprintf("Volume %q exists but doesn't match configuration in compose file.", name)
if ok {
c := utils.StringToBool(recreate)
msg := fmt.Sprintf("%s The label %s is set to %s,", msg, api.VolumeRecreateWhenSpecUpdatedLabel, recreate)
if c {
msg = msg + " so the volume is being recreated."
} else {
msg = msg + " so it should never be recreated when the configuration is updated."
}
logrus.Warn(msg)
return c, nil
} else {
promptMsg := msg + " Recreate (data will be lost)?"
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, "volumeName")
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)
}
})
}
}
15 changes: 15 additions & 0 deletions pkg/e2e/fixtures/recreate-volumes/label-new.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
services:
app:
image: alpine
volumes:
- my_vol:/my_vol
- no_recreate_label:/no_recreate_label

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

volumes:
my_vol:
labels:
com.docker.compose.volume.recreate-when-spec-updated: "true"
foo: bar
no_recreate_label:
labels:
foo: bar
32 changes: 32 additions & 0 deletions pkg/e2e/volumes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,38 @@ 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")

checkVol := func(volumeName string, label string, expected string) {
res := c.RunDockerCmd(t, "volume", "inspect",
fmt.Sprintf("%s_%s", projectName, volumeName),
"-f", fmt.Sprintf("{{ index .Labels \"%s\" }}", label))
res.Assert(t, icmd.Expected{Out: expected})
}

checkVol("my_vol", "foo", "bar")
checkVol("no_recreate_label", "foo", "bar")

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

checkVol("my_vol", "foo", "zot")
// The no_recreate_label volume should not be recreated, so its label should remain unchanged
checkVol("no_recreate_label", "foo", "bar")

// --yes should recreate the no_recreate_label volume
_ = c.RunDockerComposeCmd(t, "-f", "./fixtures/recreate-volumes/label-new.yml", "--project-name", projectName, "up", "--yes", "-d")

checkVol("my_vol", "foo", "zot")
checkVol("no_recreate_label", "foo", "zot")
}

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