This was removed in a clean-up
(060f4ae6179b10aeafa883670826159fdae8204a) but should not have been.
Fixes issues with volumes when upgrading from pre-1.7.0 daemons.
Signed-off-by: Brian Goff <cpuguy83@gmail.com>
... | ... |
@@ -160,6 +160,12 @@ func (daemon *Daemon) restore() error { |
160 | 160 |
continue |
161 | 161 |
} |
162 | 162 |
|
163 |
+ // verify that all volumes valid and have been migrated from the pre-1.7 layout |
|
164 |
+ if err := daemon.verifyVolumesInfo(c); err != nil { |
|
165 |
+ // don't skip the container due to error |
|
166 |
+ logrus.Errorf("Failed to verify volumes for container '%s': %v", c.ID, err) |
|
167 |
+ } |
|
168 |
+ |
|
163 | 169 |
// The LogConfig.Type is empty if the container was created before docker 1.12 with default log driver. |
164 | 170 |
// We should rewrite it to use the daemon defaults. |
165 | 171 |
// Fixes https://github.com/docker/docker/issues/22536 |
... | ... |
@@ -165,3 +165,10 @@ func rootFSToAPIType(rootfs *image.RootFS) types.RootFS { |
165 | 165 |
func setupDaemonProcess(config *Config) error { |
166 | 166 |
return nil |
167 | 167 |
} |
168 |
+ |
|
169 |
+// verifyVolumesInfo is a no-op on solaris. |
|
170 |
+// This is called during daemon initialization to migrate volumes from pre-1.7. |
|
171 |
+// Solaris was not supported on pre-1.7 daemons. |
|
172 |
+func (daemon *Daemon) verifyVolumesInfo(container *container.Container) error { |
|
173 |
+ return nil |
|
174 |
+} |
... | ... |
@@ -5,9 +5,14 @@ package daemon |
5 | 5 |
import ( |
6 | 6 |
"io/ioutil" |
7 | 7 |
"os" |
8 |
+ "path/filepath" |
|
8 | 9 |
"testing" |
9 | 10 |
|
10 | 11 |
"github.com/docker/docker/container" |
12 |
+ "github.com/docker/docker/volume" |
|
13 |
+ "github.com/docker/docker/volume/drivers" |
|
14 |
+ "github.com/docker/docker/volume/local" |
|
15 |
+ "github.com/docker/docker/volume/store" |
|
11 | 16 |
containertypes "github.com/docker/engine-api/types/container" |
12 | 17 |
) |
13 | 18 |
|
... | ... |
@@ -197,3 +202,82 @@ func TestNetworkOptions(t *testing.T) { |
197 | 197 |
t.Fatalf("Expected networkOptions error, got nil") |
198 | 198 |
} |
199 | 199 |
} |
200 |
+ |
|
201 |
+func TestMigratePre17Volumes(t *testing.T) { |
|
202 |
+ rootDir, err := ioutil.TempDir("", "test-daemon-volumes") |
|
203 |
+ if err != nil { |
|
204 |
+ t.Fatal(err) |
|
205 |
+ } |
|
206 |
+ defer os.RemoveAll(rootDir) |
|
207 |
+ |
|
208 |
+ volumeRoot := filepath.Join(rootDir, "volumes") |
|
209 |
+ err = os.MkdirAll(volumeRoot, 0755) |
|
210 |
+ if err != nil { |
|
211 |
+ t.Fatal(err) |
|
212 |
+ } |
|
213 |
+ |
|
214 |
+ containerRoot := filepath.Join(rootDir, "containers") |
|
215 |
+ cid := "1234" |
|
216 |
+ err = os.MkdirAll(filepath.Join(containerRoot, cid), 0755) |
|
217 |
+ |
|
218 |
+ vid := "5678" |
|
219 |
+ vfsPath := filepath.Join(rootDir, "vfs", "dir", vid) |
|
220 |
+ err = os.MkdirAll(vfsPath, 0755) |
|
221 |
+ if err != nil { |
|
222 |
+ t.Fatal(err) |
|
223 |
+ } |
|
224 |
+ |
|
225 |
+ config := []byte(` |
|
226 |
+ { |
|
227 |
+ "ID": "` + cid + `", |
|
228 |
+ "Volumes": { |
|
229 |
+ "/foo": "` + vfsPath + `", |
|
230 |
+ "/bar": "/foo", |
|
231 |
+ "/quux": "/quux" |
|
232 |
+ }, |
|
233 |
+ "VolumesRW": { |
|
234 |
+ "/foo": true, |
|
235 |
+ "/bar": true, |
|
236 |
+ "/quux": false |
|
237 |
+ } |
|
238 |
+ } |
|
239 |
+ `) |
|
240 |
+ |
|
241 |
+ volStore, err := store.New(volumeRoot) |
|
242 |
+ if err != nil { |
|
243 |
+ t.Fatal(err) |
|
244 |
+ } |
|
245 |
+ drv, err := local.New(volumeRoot, 0, 0) |
|
246 |
+ if err != nil { |
|
247 |
+ t.Fatal(err) |
|
248 |
+ } |
|
249 |
+ volumedrivers.Register(drv, volume.DefaultDriverName) |
|
250 |
+ |
|
251 |
+ daemon := &Daemon{root: rootDir, repository: containerRoot, volumes: volStore} |
|
252 |
+ err = ioutil.WriteFile(filepath.Join(containerRoot, cid, "config.v2.json"), config, 600) |
|
253 |
+ if err != nil { |
|
254 |
+ t.Fatal(err) |
|
255 |
+ } |
|
256 |
+ c, err := daemon.load(cid) |
|
257 |
+ if err != nil { |
|
258 |
+ t.Fatal(err) |
|
259 |
+ } |
|
260 |
+ if err := daemon.verifyVolumesInfo(c); err != nil { |
|
261 |
+ t.Fatal(err) |
|
262 |
+ } |
|
263 |
+ |
|
264 |
+ expected := map[string]volume.MountPoint{ |
|
265 |
+ "/foo": {Destination: "/foo", RW: true, Name: vid}, |
|
266 |
+ "/bar": {Source: "/foo", Destination: "/bar", RW: true}, |
|
267 |
+ "/quux": {Source: "/quux", Destination: "/quux", RW: false}, |
|
268 |
+ } |
|
269 |
+ for id, mp := range c.MountPoints { |
|
270 |
+ x, exists := expected[id] |
|
271 |
+ if !exists { |
|
272 |
+ t.Fatal("volume not migrated") |
|
273 |
+ } |
|
274 |
+ if mp.Source != x.Source || mp.Destination != x.Destination || mp.RW != x.RW || mp.Name != x.Name { |
|
275 |
+ t.Fatalf("got unexpected mountpoint, expected: %+v, got: %+v", x, mp) |
|
276 |
+ } |
|
277 |
+ } |
|
278 |
+} |
... | ... |
@@ -434,3 +434,10 @@ func rootFSToAPIType(rootfs *image.RootFS) types.RootFS { |
434 | 434 |
func setupDaemonProcess(config *Config) error { |
435 | 435 |
return nil |
436 | 436 |
} |
437 |
+ |
|
438 |
+// verifyVolumesInfo is a no-op on windows. |
|
439 |
+// This is called during daemon initialization to migrate volumes from pre-1.7. |
|
440 |
+// volumes were not supported on windows pre-1.7 |
|
441 |
+func (daemon *Daemon) verifyVolumesInfo(container *container.Container) error { |
|
442 |
+ return nil |
|
443 |
+} |
... | ... |
@@ -3,12 +3,18 @@ |
3 | 3 |
package daemon |
4 | 4 |
|
5 | 5 |
import ( |
6 |
+ "encoding/json" |
|
6 | 7 |
"os" |
8 |
+ "path/filepath" |
|
7 | 9 |
"sort" |
8 | 10 |
"strconv" |
11 |
+ "strings" |
|
9 | 12 |
|
10 | 13 |
"github.com/docker/docker/container" |
11 | 14 |
"github.com/docker/docker/volume" |
15 |
+ "github.com/docker/docker/volume/drivers" |
|
16 |
+ "github.com/docker/docker/volume/local" |
|
17 |
+ "github.com/pkg/errors" |
|
12 | 18 |
) |
13 | 19 |
|
14 | 20 |
// setupMounts iterates through each of the mount points for a container and |
... | ... |
@@ -84,3 +90,74 @@ func setBindModeIfNull(bind *volume.MountPoint) *volume.MountPoint { |
84 | 84 |
} |
85 | 85 |
return bind |
86 | 86 |
} |
87 |
+ |
|
88 |
+// migrateVolume links the contents of a volume created pre Docker 1.7 |
|
89 |
+// into the location expected by the local driver. |
|
90 |
+// It creates a symlink from DOCKER_ROOT/vfs/dir/VOLUME_ID to DOCKER_ROOT/volumes/VOLUME_ID/_container_data. |
|
91 |
+// It preserves the volume json configuration generated pre Docker 1.7 to be able to |
|
92 |
+// downgrade from Docker 1.7 to Docker 1.6 without losing volume compatibility. |
|
93 |
+func migrateVolume(id, vfs string) error { |
|
94 |
+ l, err := volumedrivers.GetDriver(volume.DefaultDriverName) |
|
95 |
+ if err != nil { |
|
96 |
+ return err |
|
97 |
+ } |
|
98 |
+ |
|
99 |
+ newDataPath := l.(*local.Root).DataPath(id) |
|
100 |
+ fi, err := os.Stat(newDataPath) |
|
101 |
+ if err != nil && !os.IsNotExist(err) { |
|
102 |
+ return err |
|
103 |
+ } |
|
104 |
+ |
|
105 |
+ if fi != nil && fi.IsDir() { |
|
106 |
+ return nil |
|
107 |
+ } |
|
108 |
+ |
|
109 |
+ return os.Symlink(vfs, newDataPath) |
|
110 |
+} |
|
111 |
+ |
|
112 |
+// verifyVolumesInfo ports volumes configured for the containers pre docker 1.7. |
|
113 |
+// It reads the container configuration and creates valid mount points for the old volumes. |
|
114 |
+func (daemon *Daemon) verifyVolumesInfo(container *container.Container) error { |
|
115 |
+ // Inspect old structures only when we're upgrading from old versions |
|
116 |
+ // to versions >= 1.7 and the MountPoints has not been populated with volumes data. |
|
117 |
+ type volumes struct { |
|
118 |
+ Volumes map[string]string |
|
119 |
+ VolumesRW map[string]bool |
|
120 |
+ } |
|
121 |
+ cfgPath, err := container.ConfigPath() |
|
122 |
+ if err != nil { |
|
123 |
+ return err |
|
124 |
+ } |
|
125 |
+ f, err := os.Open(cfgPath) |
|
126 |
+ if err != nil { |
|
127 |
+ return errors.Wrap(err, "could not open container config") |
|
128 |
+ } |
|
129 |
+ var cv volumes |
|
130 |
+ if err := json.NewDecoder(f).Decode(&cv); err != nil { |
|
131 |
+ return errors.Wrap(err, "could not decode container config") |
|
132 |
+ } |
|
133 |
+ |
|
134 |
+ if len(container.MountPoints) == 0 && len(cv.Volumes) > 0 { |
|
135 |
+ for destination, hostPath := range cv.Volumes { |
|
136 |
+ vfsPath := filepath.Join(daemon.root, "vfs", "dir") |
|
137 |
+ rw := cv.VolumesRW != nil && cv.VolumesRW[destination] |
|
138 |
+ |
|
139 |
+ if strings.HasPrefix(hostPath, vfsPath) { |
|
140 |
+ id := filepath.Base(hostPath) |
|
141 |
+ v, err := daemon.volumes.CreateWithRef(id, volume.DefaultDriverName, container.ID, nil, nil) |
|
142 |
+ if err != nil { |
|
143 |
+ return err |
|
144 |
+ } |
|
145 |
+ if err := migrateVolume(id, hostPath); err != nil { |
|
146 |
+ return err |
|
147 |
+ } |
|
148 |
+ container.AddMountPointWithVolume(destination, v, true) |
|
149 |
+ } else { // Bind mount |
|
150 |
+ m := volume.MountPoint{Source: hostPath, Destination: destination, RW: rw} |
|
151 |
+ container.MountPoints[destination] = &m |
|
152 |
+ } |
|
153 |
+ } |
|
154 |
+ return container.ToDisk() |
|
155 |
+ } |
|
156 |
+ return nil |
|
157 |
+} |
... | ... |
@@ -23,6 +23,7 @@ import ( |
23 | 23 |
"github.com/docker/docker/pkg/integration/checker" |
24 | 24 |
icmd "github.com/docker/docker/pkg/integration/cmd" |
25 | 25 |
"github.com/docker/docker/pkg/mount" |
26 |
+ "github.com/docker/docker/pkg/stringid" |
|
26 | 27 |
"github.com/docker/go-units" |
27 | 28 |
"github.com/docker/libnetwork/iptables" |
28 | 29 |
"github.com/docker/libtrust" |
... | ... |
@@ -2791,3 +2792,83 @@ func (s *DockerDaemonSuite) TestDaemonRestartSaveContainerExitCode(c *check.C) { |
2791 | 2791 |
c.Assert(err, checker.IsNil) |
2792 | 2792 |
c.Assert(out, checker.Equals, runError) |
2793 | 2793 |
} |
2794 |
+ |
|
2795 |
+func (s *DockerDaemonSuite) TestDaemonBackcompatPre17Volumes(c *check.C) { |
|
2796 |
+ testRequires(c, SameHostDaemon) |
|
2797 |
+ d := s.d |
|
2798 |
+ err := d.StartWithBusybox() |
|
2799 |
+ c.Assert(err, checker.IsNil) |
|
2800 |
+ |
|
2801 |
+ // hack to be able to side-load a container config |
|
2802 |
+ out, err := d.Cmd("create", "busybox:latest") |
|
2803 |
+ c.Assert(err, checker.IsNil, check.Commentf(out)) |
|
2804 |
+ id := strings.TrimSpace(out) |
|
2805 |
+ |
|
2806 |
+ out, err = d.Cmd("inspect", "--type=image", "--format={{.ID}}", "busybox:latest") |
|
2807 |
+ c.Assert(err, checker.IsNil, check.Commentf(out)) |
|
2808 |
+ c.Assert(d.Stop(), checker.IsNil) |
|
2809 |
+ <-d.wait |
|
2810 |
+ |
|
2811 |
+ imageID := strings.TrimSpace(out) |
|
2812 |
+ volumeID := stringid.GenerateNonCryptoID() |
|
2813 |
+ vfsPath := filepath.Join(d.root, "vfs", "dir", volumeID) |
|
2814 |
+ c.Assert(os.MkdirAll(vfsPath, 0755), checker.IsNil) |
|
2815 |
+ |
|
2816 |
+ config := []byte(` |
|
2817 |
+ { |
|
2818 |
+ "ID": "` + id + `", |
|
2819 |
+ "Name": "hello", |
|
2820 |
+ "Driver": "` + d.storageDriver + `", |
|
2821 |
+ "Image": "` + imageID + `", |
|
2822 |
+ "Config": {"Image": "busybox:latest"}, |
|
2823 |
+ "NetworkSettings": {}, |
|
2824 |
+ "Volumes": { |
|
2825 |
+ "/bar":"/foo", |
|
2826 |
+ "/foo": "` + vfsPath + `", |
|
2827 |
+ "/quux":"/quux" |
|
2828 |
+ }, |
|
2829 |
+ "VolumesRW": { |
|
2830 |
+ "/bar": true, |
|
2831 |
+ "/foo": true, |
|
2832 |
+ "/quux": false |
|
2833 |
+ } |
|
2834 |
+ } |
|
2835 |
+ `) |
|
2836 |
+ |
|
2837 |
+ configPath := filepath.Join(d.root, "containers", id, "config.v2.json") |
|
2838 |
+ err = ioutil.WriteFile(configPath, config, 600) |
|
2839 |
+ err = d.Start() |
|
2840 |
+ c.Assert(err, checker.IsNil) |
|
2841 |
+ |
|
2842 |
+ out, err = d.Cmd("inspect", "--type=container", "--format={{ json .Mounts }}", id) |
|
2843 |
+ c.Assert(err, checker.IsNil, check.Commentf(out)) |
|
2844 |
+ type mount struct { |
|
2845 |
+ Name string |
|
2846 |
+ Source string |
|
2847 |
+ Destination string |
|
2848 |
+ Driver string |
|
2849 |
+ RW bool |
|
2850 |
+ } |
|
2851 |
+ |
|
2852 |
+ ls := []mount{} |
|
2853 |
+ err = json.NewDecoder(strings.NewReader(out)).Decode(&ls) |
|
2854 |
+ c.Assert(err, checker.IsNil) |
|
2855 |
+ |
|
2856 |
+ expected := []mount{ |
|
2857 |
+ {Source: "/foo", Destination: "/bar", RW: true}, |
|
2858 |
+ {Name: volumeID, Destination: "/foo", RW: true}, |
|
2859 |
+ {Source: "/quux", Destination: "/quux", RW: false}, |
|
2860 |
+ } |
|
2861 |
+ c.Assert(ls, checker.HasLen, len(expected)) |
|
2862 |
+ |
|
2863 |
+ for _, m := range ls { |
|
2864 |
+ var matched bool |
|
2865 |
+ for _, x := range expected { |
|
2866 |
+ if m.Source == x.Source && m.Destination == x.Destination && m.RW == x.RW || m.Name != x.Name { |
|
2867 |
+ matched = true |
|
2868 |
+ break |
|
2869 |
+ } |
|
2870 |
+ } |
|
2871 |
+ c.Assert(matched, checker.True, check.Commentf("did find match for %+v", m)) |
|
2872 |
+ } |
|
2873 |
+} |