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 |
+} |