Skip to content

Commit 76c9cda

Browse files
committed
feat: add confirmation label when volume spec changes
close: #13807
1 parent 40a2262 commit 76c9cda

6 files changed

Lines changed: 140 additions & 1 deletion

File tree

pkg/api/labels.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ const (
3333
ContainerNumberLabel = "com.docker.compose.container-number"
3434
// VolumeLabel allow to track resource related to a compose volume
3535
VolumeLabel = "com.docker.compose.volume"
36+
// VolumeRecreateWhenSpecUpdatedLabel when set to true, volume will be recreated when spec updated
37+
VolumeRecreateWhenSpecUpdatedLabel = "com.docker.compose.volume.recreate-when-spec-updated"
3638
// NetworkLabel allow to track resource related to a compose network
3739
NetworkLabel = "com.docker.compose.network"
3840
// WorkingDirLabel stores absolute path to compose project working directory

pkg/compose/create.go

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ import (
4343
cdi "tags.cncf.io/container-device-interface/pkg/parser"
4444

4545
"github.com/docker/compose/v5/pkg/api"
46+
"github.com/docker/compose/v5/pkg/utils"
4647
)
4748

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

1643+
func confirmVolumeRecreate(labels map[string]string, prompt Prompt, promptMsg string) (bool, error) {
1644+
recreate, ok := labels[api.VolumeRecreateWhenSpecUpdatedLabel]
1645+
if ok {
1646+
return utils.StringToBool(recreate), nil
1647+
} else {
1648+
c, err := prompt(promptMsg, false)
1649+
if err != nil {
1650+
return false, err
1651+
}
1652+
return c, nil
1653+
}
1654+
}
1655+
16421656
func (s *composeService) removeDivergedVolume(ctx context.Context, name string, volume types.VolumeConfig, project *types.Project) error {
16431657
// Remove services mounting divergent volume
16441658
var services []string

pkg/compose/create_test.go

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,13 @@
1717
package compose
1818

1919
import (
20+
"io"
2021
"net"
2122
"net/netip"
2223
"os"
2324
"path/filepath"
2425
"sort"
26+
"strings"
2527
"testing"
2628

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

40+
"github.com/docker/cli/cli/streams"
41+
"github.com/docker/compose/v5/cmd/prompt"
3842
"github.com/docker/compose/v5/pkg/api"
3943
)
4044

@@ -484,3 +488,81 @@ volumes:
484488
})
485489
}
486490
}
491+
492+
func Test_composeService_confirmVolumeRecreate(t *testing.T) {
493+
tests := []struct {
494+
name string
495+
labels map[string]string
496+
input string
497+
want bool
498+
wantErr bool
499+
}{
500+
{
501+
name: "no labels no input",
502+
labels: nil,
503+
input: "",
504+
want: false,
505+
wantErr: false,
506+
},
507+
{
508+
name: "no labels and input is y",
509+
labels: nil,
510+
input: "y",
511+
want: true,
512+
wantErr: false,
513+
},
514+
{
515+
name: "no labels and input is true",
516+
labels: nil,
517+
input: "true",
518+
want: true,
519+
wantErr: false,
520+
},
521+
{
522+
name: "no labels and input is no",
523+
labels: nil,
524+
input: "no",
525+
want: false,
526+
wantErr: false,
527+
},
528+
{
529+
name: "no input, has labels recreate true",
530+
labels: map[string]string{api.VolumeRecreateWhenSpecUpdatedLabel: "true"},
531+
want: true,
532+
wantErr: false,
533+
},
534+
{
535+
name: "no input, has labels recreate TRUE",
536+
labels: map[string]string{api.VolumeRecreateWhenSpecUpdatedLabel: "TRUE"},
537+
want: true,
538+
wantErr: false,
539+
},
540+
{
541+
name: "no input, has labels recreate false",
542+
labels: map[string]string{api.VolumeRecreateWhenSpecUpdatedLabel: "false"},
543+
want: false,
544+
wantErr: false,
545+
},
546+
}
547+
for _, tt := range tests {
548+
t.Run(tt.name, func(t *testing.T) {
549+
prompt := prompt.NewPrompt(
550+
streams.NewIn(io.NopCloser(strings.NewReader(tt.input))),
551+
streams.NewOut(t.Output())).Confirm
552+
553+
got, gotErr := confirmVolumeRecreate(tt.labels, prompt, "promptMsg")
554+
if gotErr != nil {
555+
if !tt.wantErr {
556+
t.Errorf("confirmVolumeRecreate() failed: %v", gotErr)
557+
}
558+
return
559+
}
560+
if tt.wantErr {
561+
t.Fatal("confirmVolumeRecreate() succeeded unexpectedly")
562+
}
563+
if tt.want != got {
564+
t.Errorf("confirmVolumeRecreate() = %v, want %v", got, tt.want)
565+
}
566+
})
567+
}
568+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
services:
2+
app:
3+
image: alpine
4+
volumes:
5+
- my_vol:/my_vol
6+
7+
volumes:
8+
my_vol:
9+
labels:
10+
com.docker.compose.volume.recreate-when-spec-updated: "true"
11+
foo: zot
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
services:
2+
app:
3+
image: alpine
4+
volumes:
5+
- my_vol:/my_vol
6+
7+
volumes:
8+
my_vol:
9+
labels:
10+
com.docker.compose.volume.recreate-when-spec-updated: "true"
11+
foo: bar
12+

pkg/e2e/volumes_test.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,24 @@ func TestUpRecreateVolumes_IgnoreBinds(t *testing.T) {
172172
assert.Check(t, !strings.Contains(res.Combined(), "Recreated"))
173173
}
174174

175+
func TestUpRecreateVolumes_RecreateLabel(t *testing.T) {
176+
c := NewCLI(t)
177+
const projectName = "compose-e2e-recreate-volumes-recreate-label"
178+
t.Cleanup(func() {
179+
c.cleanupWithDown(t, projectName)
180+
})
181+
182+
c.RunDockerComposeCmd(t, "-f", "./fixtures/recreate-volumes/label-old.yml", "--project-name", projectName, "up", "-d")
183+
184+
res := c.RunDockerCmd(t, "volume", "inspect", fmt.Sprintf("%s_my_vol", projectName), "-f", "{{ index .Labels \"foo\" }}")
185+
res.Assert(t, icmd.Expected{Out: "bar"})
186+
187+
res = c.RunDockerComposeCmd(t, "-f", "./fixtures/recreate-volumes/label-new.yml", "--project-name", projectName, "up", "-d")
188+
189+
res = c.RunDockerCmd(t, "volume", "inspect", fmt.Sprintf("%s_my_vol", projectName), "-f", "{{ index .Labels \"foo\" }}")
190+
res.Assert(t, icmd.Expected{Out: "zot"})
191+
}
192+
175193
func TestImageVolume(t *testing.T) {
176194
c := NewCLI(t)
177195
const projectName = "compose-e2e-image-volume"

0 commit comments

Comments
 (0)