Skip to content

Commit 91baf88

Browse files
jhrotkondeloof
authored andcommitted
Recreate container on volume configuration change
Signed-off-by: Joana Hrotko <[email protected]> Signed-off-by: Nicolas De Loof <[email protected]>
1 parent 6243032 commit 91baf88

File tree

7 files changed

+108
-15
lines changed

7 files changed

+108
-15
lines changed

pkg/compose/convergence.go

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import (
3535
"github.com/docker/compose/v2/internal/tracing"
3636
moby "github.com/docker/docker/api/types"
3737
containerType "github.com/docker/docker/api/types/container"
38+
mmount "github.com/docker/docker/api/types/mount"
3839
"github.com/docker/docker/api/types/versions"
3940
specs "github.com/opencontainers/image-spec/specs-go/v1"
4041
"github.com/sirupsen/logrus"
@@ -60,6 +61,7 @@ type convergence struct {
6061
service *composeService
6162
services map[string]Containers
6263
networks map[string]string
64+
volumes map[string]string
6365
stateMutex sync.Mutex
6466
}
6567

@@ -75,7 +77,7 @@ func (c *convergence) setObservedState(serviceName string, containers Containers
7577
c.services[serviceName] = containers
7678
}
7779

78-
func newConvergence(services []string, state Containers, networks map[string]string, s *composeService) *convergence {
80+
func newConvergence(services []string, state Containers, networks map[string]string, volumes map[string]string, s *composeService) *convergence {
7981
observedState := map[string]Containers{}
8082
for _, s := range services {
8183
observedState[s] = Containers{}
@@ -88,6 +90,7 @@ func newConvergence(services []string, state Containers, networks map[string]str
8890
service: s,
8991
services: observedState,
9092
networks: networks,
93+
volumes: volumes,
9194
}
9295
}
9396

@@ -362,6 +365,27 @@ func (c *convergence) mustRecreate(expected types.ServiceConfig, actual moby.Con
362365
}
363366
}
364367

368+
if c.volumes != nil {
369+
// check the networks container is connected to are the expected ones
370+
for _, vol := range expected.Volumes {
371+
id := c.volumes[vol.Source]
372+
found := false
373+
for _, mount := range actual.Mounts {
374+
if mount.Type != mmount.TypeVolume {
375+
continue
376+
}
377+
if mount.Name == id {
378+
found = true
379+
break
380+
}
381+
}
382+
if !found {
383+
// config is up-to-date but container is not connected to network - maybe recreated ?
384+
return true, nil
385+
}
386+
}
387+
}
388+
365389
return false, nil
366390
}
367391

pkg/compose/create.go

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,8 @@ func (s *composeService) create(ctx context.Context, project *types.Project, opt
9292
return err
9393
}
9494

95-
if err := s.ensureProjectVolumes(ctx, project); err != nil {
95+
volumes, err := s.ensureProjectVolumes(ctx, project)
96+
if err != nil {
9697
return err
9798
}
9899

@@ -115,7 +116,7 @@ func (s *composeService) create(ctx context.Context, project *types.Project, opt
115116
"--remove-orphans flag to clean it up.", orphans.names())
116117
}
117118
}
118-
return newConvergence(options.Services, observedState, networks, s).apply(ctx, project, options)
119+
return newConvergence(options.Services, observedState, networks, volumes, s).apply(ctx, project, options)
119120
}
120121

121122
func prepareNetworks(project *types.Project) {
@@ -141,15 +142,17 @@ func (s *composeService) ensureNetworks(ctx context.Context, project *types.Proj
141142
return networks, nil
142143
}
143144

144-
func (s *composeService) ensureProjectVolumes(ctx context.Context, project *types.Project) error {
145+
func (s *composeService) ensureProjectVolumes(ctx context.Context, project *types.Project) (map[string]string, error) {
146+
ids := map[string]string{}
145147
for k, volume := range project.Volumes {
146148
volume.Labels = volume.Labels.Add(api.VolumeLabel, k)
147149
volume.Labels = volume.Labels.Add(api.ProjectLabel, project.Name)
148150
volume.Labels = volume.Labels.Add(api.VersionLabel, api.ComposeVersion)
149-
err := s.ensureVolume(ctx, volume, project.Name)
151+
id, err := s.ensureVolume(ctx, volume, project.Name)
150152
if err != nil {
151-
return err
153+
return nil, err
152154
}
155+
ids[k] = id
153156
}
154157

155158
err := func() error {
@@ -205,7 +208,7 @@ func (s *composeService) ensureProjectVolumes(ctx context.Context, project *type
205208
if err != nil {
206209
progress.ContextWriter(ctx).TailMsgf("Failed to prepare Synchronized file shares: %v", err)
207210
}
208-
return nil
211+
return ids, nil
209212
}
210213

211214
func (s *composeService) getCreateConfigs(ctx context.Context,
@@ -1426,21 +1429,21 @@ func (s *composeService) resolveExternalNetwork(ctx context.Context, n *types.Ne
14261429
}
14271430
}
14281431

1429-
func (s *composeService) ensureVolume(ctx context.Context, volume types.VolumeConfig, project string) error {
1432+
func (s *composeService) ensureVolume(ctx context.Context, volume types.VolumeConfig, project string) (string, error) {
14301433
inspected, err := s.apiClient().VolumeInspect(ctx, volume.Name)
14311434
if err != nil {
14321435
if !errdefs.IsNotFound(err) {
1433-
return err
1436+
return "", err
14341437
}
14351438
if volume.External {
1436-
return fmt.Errorf("external volume %q not found", volume.Name)
1439+
return "", fmt.Errorf("external volume %q not found", volume.Name)
14371440
}
1438-
err := s.createVolume(ctx, volume)
1439-
return err
1441+
err = s.createVolume(ctx, volume)
1442+
return "", err
14401443
}
14411444

14421445
if volume.External {
1443-
return nil
1446+
return volume.Name, nil
14441447
}
14451448

14461449
// Volume exists with name, but let's double-check this is the expected one
@@ -1451,7 +1454,16 @@ func (s *composeService) ensureVolume(ctx context.Context, volume types.VolumeCo
14511454
if ok && p != project {
14521455
logrus.Warnf("volume %q already exists but was created for project %q (expected %q). Use `external: true` to use an existing volume", volume.Name, p, project)
14531456
}
1454-
return nil
1457+
1458+
expected, err := VolumeHash(volume)
1459+
if err != nil {
1460+
return "", err
1461+
}
1462+
actual, ok := inspected.Labels[api.ConfigHashLabel]
1463+
if ok && actual != expected {
1464+
logrus.Warnf("volume %q exists but doesn't match configuration in compose file. You should remove it so it get recreated", volume.Name)
1465+
}
1466+
return inspected.Name, nil
14551467
}
14561468

14571469
func (s *composeService) createVolume(ctx context.Context, volume types.VolumeConfig) error {

pkg/compose/hash.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,23 @@ func ServiceHash(o types.ServiceConfig) (string, error) {
4242
return digest.SHA256.FromBytes(bytes).Encoded(), nil
4343
}
4444

45+
// NetworkHash computes the configuration hash for a network.
4546
func NetworkHash(o *types.NetworkConfig) (string, error) {
4647
bytes, err := json.Marshal(o)
4748
if err != nil {
4849
return "", err
4950
}
5051
return digest.SHA256.FromBytes(bytes).Encoded(), nil
5152
}
53+
54+
// VolumeHash computes the configuration hash for a volume.
55+
func VolumeHash(o types.VolumeConfig) (string, error) {
56+
if o.Driver == "" { // (TODO: jhrotko) This probably should be fixed in compose-go
57+
o.Driver = "local"
58+
}
59+
bytes, err := json.Marshal(o)
60+
if err != nil {
61+
return "", err
62+
}
63+
return digest.SHA256.FromBytes(bytes).Encoded(), nil
64+
}

pkg/compose/run.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ func (s *composeService) prepareRun(ctx context.Context, project *types.Project,
104104
Labels: mergeLabels(service.Labels, service.CustomLabels),
105105
}
106106

107-
err = newConvergence(project.ServiceNames(), observedState, nil, s).resolveServiceReferences(&service)
107+
err = newConvergence(project.ServiceNames(), observedState, nil, nil, s).resolveServiceReferences(&service)
108108
if err != nil {
109109
return "", err
110110
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
services:
2+
app:
3+
image: alpine
4+
volumes:
5+
- my_vol:/my_vol
6+
7+
volumes:
8+
my_vol:
9+
external: true
10+
name: test_external_volume
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
services:
2+
app:
3+
image: alpine
4+
volumes:
5+
- my_vol:/my_vol
6+
7+
volumes:
8+
my_vol:
9+
external: true
10+
name: test_external_volume_2

pkg/e2e/volumes_test.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package e2e
1818

1919
import (
20+
"fmt"
2021
"net/http"
2122
"os"
2223
"path/filepath"
@@ -121,3 +122,26 @@ func TestProjectVolumeBind(t *testing.T) {
121122
assert.Assert(t, strings.Contains(ret.Stdout(), "SUCCESS"))
122123
})
123124
}
125+
126+
func TestUpRecreateVolumes(t *testing.T) {
127+
c := NewCLI(t)
128+
const projectName = "compose-e2e-recreate-volumes"
129+
t.Cleanup(func() {
130+
c.cleanupWithDown(t, projectName)
131+
c.RunDockerCmd(t, "volume", "rm", "-f", "test_external_volume")
132+
c.RunDockerCmd(t, "volume", "rm", "-f", "test_external_volume_2")
133+
})
134+
135+
c.RunDockerCmd(t, "volume", "create", "test_external_volume")
136+
c.RunDockerCmd(t, "volume", "create", "test_external_volume_2")
137+
138+
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/recreate-volumes/compose.yaml", "--project-name", projectName, "up", "-d")
139+
assert.NilError(t, res.Error)
140+
141+
res = c.RunDockerCmd(t, "inspect", fmt.Sprintf("%s-app-1", projectName), "-f", "{{ (index .Mounts 0).Name }}")
142+
res.Assert(t, icmd.Expected{Out: "test_external_volume"})
143+
144+
res = c.RunDockerComposeCmd(t, "-f", "./fixtures/recreate-volumes/compose2.yaml", "--project-name", projectName, "up", "-d")
145+
res = c.RunDockerCmd(t, "inspect", fmt.Sprintf("%s-app-1", projectName), "-f", "{{ (index .Mounts 0).Name }}")
146+
res.Assert(t, icmd.Expected{Out: "test_external_volume_2"})
147+
}

0 commit comments

Comments
 (0)