Browse code

daemon: container ArchivePath and ExtractToDir

The following methods will deprecate the Copy method and introduce
two new, well-behaved methods for creating a tar archive of a resource
in a container and for extracting a tar archive into a directory in a
container.

Docker-DCO-1.1-Signed-off-by: Josh Hawn <josh.hawn@docker.com> (github: jlhawn)

Josh Hawn authored on 2015/05/13 10:21:26
Showing 5 changed files
... ...
@@ -1,6 +1,7 @@
1 1
 package types
2 2
 
3 3
 import (
4
+	"os"
4 5
 	"time"
5 6
 
6 7
 	"github.com/docker/docker/daemon/network"
... ...
@@ -127,6 +128,16 @@ type CopyConfig struct {
127 127
 	Resource string
128 128
 }
129 129
 
130
+// ContainerPathStat is used to encode the response from
131
+// 	GET /containers/{name:.*}/stat-path
132
+type ContainerPathStat struct {
133
+	Name  string      `json:"name"`
134
+	Path  string      `json:"path"`
135
+	Size  int64       `json:"size"`
136
+	Mode  os.FileMode `json:"mode"`
137
+	Mtime time.Time   `json:"mtime"`
138
+}
139
+
130 140
 // GET "/containers/{name:.*}/top"
131 141
 type ContainerProcessList struct {
132 142
 	Processes [][]string
133 143
new file mode 100644
... ...
@@ -0,0 +1,297 @@
0
+package daemon
1
+
2
+import (
3
+	"errors"
4
+	"io"
5
+	"os"
6
+	"path/filepath"
7
+
8
+	"github.com/docker/docker/api/types"
9
+	"github.com/docker/docker/pkg/archive"
10
+	"github.com/docker/docker/pkg/chrootarchive"
11
+	"github.com/docker/docker/pkg/ioutils"
12
+)
13
+
14
+// ErrExtractPointNotDirectory is used to convey that the operation to extract
15
+// a tar archive to a directory in a container has failed because the specified
16
+// path does not refer to a directory.
17
+var ErrExtractPointNotDirectory = errors.New("extraction point is not a directory")
18
+
19
+// ContainerCopy performs a depracated operation of archiving the resource at
20
+// the specified path in the conatiner identified by the given name.
21
+func (daemon *Daemon) ContainerCopy(name string, res string) (io.ReadCloser, error) {
22
+	container, err := daemon.Get(name)
23
+	if err != nil {
24
+		return nil, err
25
+	}
26
+
27
+	if res[0] == '/' {
28
+		res = res[1:]
29
+	}
30
+
31
+	return container.Copy(res)
32
+}
33
+
34
+// ContainerStatPath stats the filesystem resource at the specified path in the
35
+// container identified by the given name.
36
+func (daemon *Daemon) ContainerStatPath(name string, path string) (stat *types.ContainerPathStat, err error) {
37
+	container, err := daemon.Get(name)
38
+	if err != nil {
39
+		return nil, err
40
+	}
41
+
42
+	return container.StatPath(path)
43
+}
44
+
45
+// ContainerArchivePath creates an archive of the filesystem resource at the
46
+// specified path in the container identified by the given name. Returns a
47
+// tar archive of the resource and whether it was a directory or a single file.
48
+func (daemon *Daemon) ContainerArchivePath(name string, path string) (content io.ReadCloser, stat *types.ContainerPathStat, err error) {
49
+	container, err := daemon.Get(name)
50
+	if err != nil {
51
+		return nil, nil, err
52
+	}
53
+
54
+	return container.ArchivePath(path)
55
+}
56
+
57
+// ContainerExtractToDir extracts the given archive to the specified location
58
+// in the filesystem of the container identified by the given name. The given
59
+// path must be of a directory in the container. If it is not, the error will
60
+// be ErrExtractPointNotDirectory. If noOverwriteDirNonDir is true then it will
61
+// be an error if unpacking the given content would cause an existing directory
62
+// to be replaced with a non-directory and vice versa.
63
+func (daemon *Daemon) ContainerExtractToDir(name, path string, noOverwriteDirNonDir bool, content io.Reader) error {
64
+	container, err := daemon.Get(name)
65
+	if err != nil {
66
+		return err
67
+	}
68
+
69
+	return container.ExtractToDir(path, noOverwriteDirNonDir, content)
70
+}
71
+
72
+// StatPath stats the filesystem resource at the specified path in this
73
+// container. Returns stat info about the resource.
74
+func (container *Container) StatPath(path string) (stat *types.ContainerPathStat, err error) {
75
+	container.Lock()
76
+	defer container.Unlock()
77
+
78
+	if err = container.Mount(); err != nil {
79
+		return nil, err
80
+	}
81
+	defer container.Unmount()
82
+
83
+	err = container.mountVolumes()
84
+	defer container.UnmountVolumes(true)
85
+	if err != nil {
86
+		return nil, err
87
+	}
88
+
89
+	// Consider the given path as an absolute path in the container.
90
+	absPath := path
91
+	if !filepath.IsAbs(absPath) {
92
+		absPath = archive.PreserveTrailingDotOrSeparator(filepath.Join("/", path), path)
93
+	}
94
+
95
+	resolvedPath, err := container.GetResourcePath(absPath)
96
+	if err != nil {
97
+		return nil, err
98
+	}
99
+
100
+	// A trailing "." or separator has important meaning. For example, if
101
+	// `"foo"` is a symlink to some directory `"dir"`, then `os.Lstat("foo")`
102
+	// will stat the link itself, while `os.Lstat("foo/")` will stat the link
103
+	// target. If the basename of the path is ".", it means to archive the
104
+	// contents of the directory with "." as the first path component rather
105
+	// than the name of the directory. This would cause extraction of the
106
+	// archive to *not* make another directory, but instead use the current
107
+	// directory.
108
+	resolvedPath = archive.PreserveTrailingDotOrSeparator(resolvedPath, absPath)
109
+
110
+	lstat, err := os.Lstat(resolvedPath)
111
+	if err != nil {
112
+		return nil, err
113
+	}
114
+
115
+	return &types.ContainerPathStat{
116
+		Name:  lstat.Name(),
117
+		Path:  absPath,
118
+		Size:  lstat.Size(),
119
+		Mode:  lstat.Mode(),
120
+		Mtime: lstat.ModTime(),
121
+	}, nil
122
+}
123
+
124
+// ArchivePath creates an archive of the filesystem resource at the specified
125
+// path in this container. Returns a tar archive of the resource and stat info
126
+// about the resource.
127
+func (container *Container) ArchivePath(path string) (content io.ReadCloser, stat *types.ContainerPathStat, err error) {
128
+	container.Lock()
129
+
130
+	defer func() {
131
+		if err != nil {
132
+			// Wait to unlock the container until the archive is fully read
133
+			// (see the ReadCloseWrapper func below) or if there is an error
134
+			// before that occurs.
135
+			container.Unlock()
136
+		}
137
+	}()
138
+
139
+	if err = container.Mount(); err != nil {
140
+		return nil, nil, err
141
+	}
142
+
143
+	defer func() {
144
+		if err != nil {
145
+			// unmount any volumes
146
+			container.UnmountVolumes(true)
147
+			// unmount the container's rootfs
148
+			container.Unmount()
149
+		}
150
+	}()
151
+
152
+	if err = container.mountVolumes(); err != nil {
153
+		return nil, nil, err
154
+	}
155
+
156
+	// Consider the given path as an absolute path in the container.
157
+	absPath := path
158
+	if !filepath.IsAbs(absPath) {
159
+		absPath = archive.PreserveTrailingDotOrSeparator(filepath.Join("/", path), path)
160
+	}
161
+
162
+	resolvedPath, err := container.GetResourcePath(absPath)
163
+	if err != nil {
164
+		return nil, nil, err
165
+	}
166
+
167
+	// A trailing "." or separator has important meaning. For example, if
168
+	// `"foo"` is a symlink to some directory `"dir"`, then `os.Lstat("foo")`
169
+	// will stat the link itself, while `os.Lstat("foo/")` will stat the link
170
+	// target. If the basename of the path is ".", it means to archive the
171
+	// contents of the directory with "." as the first path component rather
172
+	// than the name of the directory. This would cause extraction of the
173
+	// archive to *not* make another directory, but instead use the current
174
+	// directory.
175
+	resolvedPath = archive.PreserveTrailingDotOrSeparator(resolvedPath, absPath)
176
+
177
+	lstat, err := os.Lstat(resolvedPath)
178
+	if err != nil {
179
+		return nil, nil, err
180
+	}
181
+
182
+	stat = &types.ContainerPathStat{
183
+		Name:  lstat.Name(),
184
+		Path:  absPath,
185
+		Size:  lstat.Size(),
186
+		Mode:  lstat.Mode(),
187
+		Mtime: lstat.ModTime(),
188
+	}
189
+
190
+	data, err := archive.TarResource(resolvedPath)
191
+	if err != nil {
192
+		return nil, nil, err
193
+	}
194
+
195
+	content = ioutils.NewReadCloserWrapper(data, func() error {
196
+		err := data.Close()
197
+		container.UnmountVolumes(true)
198
+		container.Unmount()
199
+		container.Unlock()
200
+		return err
201
+	})
202
+
203
+	container.LogEvent("archive-path")
204
+
205
+	return content, stat, nil
206
+}
207
+
208
+// ExtractToDir extracts the given tar archive to the specified location in the
209
+// filesystem of this container. The given path must be of a directory in the
210
+// container. If it is not, the error will be ErrExtractPointNotDirectory. If
211
+// noOverwriteDirNonDir is true then it will be an error if unpacking the
212
+// given content would cause an existing directory to be replaced with a non-
213
+// directory and vice versa.
214
+func (container *Container) ExtractToDir(path string, noOverwriteDirNonDir bool, content io.Reader) (err error) {
215
+	container.Lock()
216
+	defer container.Unlock()
217
+
218
+	if err = container.Mount(); err != nil {
219
+		return err
220
+	}
221
+	defer container.Unmount()
222
+
223
+	err = container.mountVolumes()
224
+	defer container.UnmountVolumes(true)
225
+	if err != nil {
226
+		return err
227
+	}
228
+
229
+	// Consider the given path as an absolute path in the container.
230
+	absPath := path
231
+	if !filepath.IsAbs(absPath) {
232
+		absPath = archive.PreserveTrailingDotOrSeparator(filepath.Join("/", path), path)
233
+	}
234
+
235
+	resolvedPath, err := container.GetResourcePath(absPath)
236
+	if err != nil {
237
+		return err
238
+	}
239
+
240
+	// A trailing "." or separator has important meaning. For example, if
241
+	// `"foo"` is a symlink to some directory `"dir"`, then `os.Lstat("foo")`
242
+	// will stat the link itself, while `os.Lstat("foo/")` will stat the link
243
+	// target. If the basename of the path is ".", it means to archive the
244
+	// contents of the directory with "." as the first path component rather
245
+	// than the name of the directory. This would cause extraction of the
246
+	// archive to *not* make another directory, but instead use the current
247
+	// directory.
248
+	resolvedPath = archive.PreserveTrailingDotOrSeparator(resolvedPath, absPath)
249
+
250
+	stat, err := os.Lstat(resolvedPath)
251
+	if err != nil {
252
+		return err
253
+	}
254
+
255
+	if !stat.IsDir() {
256
+		return ErrExtractPointNotDirectory
257
+	}
258
+
259
+	baseRel, err := filepath.Rel(container.basefs, resolvedPath)
260
+	if err != nil {
261
+		return err
262
+	}
263
+	absPath = filepath.Join("/", baseRel)
264
+
265
+	// Need to check if the path is in a volume. If it is, it cannot be in a
266
+	// read-only volume. If it is not in a volume, the container cannot be
267
+	// configured with a read-only rootfs.
268
+	var toVolume bool
269
+	for _, mnt := range container.MountPoints {
270
+		if toVolume = mnt.hasResource(absPath); toVolume {
271
+			if mnt.RW {
272
+				break
273
+			}
274
+			return ErrVolumeReadonly
275
+		}
276
+	}
277
+
278
+	if !toVolume && container.hostConfig.ReadonlyRootfs {
279
+		return ErrContainerRootfsReadonly
280
+	}
281
+
282
+	options := &archive.TarOptions{
283
+		ChownOpts: &archive.TarChownOptions{
284
+			UID: 0, GID: 0, // TODO: use config.User? Remap to userns root?
285
+		},
286
+		NoOverwriteDirNonDir: noOverwriteDirNonDir,
287
+	}
288
+
289
+	if err := chrootarchive.Untar(content, resolvedPath, options); err != nil {
290
+		return err
291
+	}
292
+
293
+	container.LogEvent("extract-to-dir")
294
+
295
+	return nil
296
+}
... ...
@@ -35,10 +35,11 @@ import (
35 35
 )
36 36
 
37 37
 var (
38
-	ErrNotATTY               = errors.New("The PTY is not a file")
39
-	ErrNoTTY                 = errors.New("No PTY found")
40
-	ErrContainerStart        = errors.New("The container failed to start. Unknown error")
41
-	ErrContainerStartTimeout = errors.New("The container failed to start due to timed out.")
38
+	ErrNotATTY                 = errors.New("The PTY is not a file")
39
+	ErrNoTTY                   = errors.New("No PTY found")
40
+	ErrContainerStart          = errors.New("The container failed to start. Unknown error")
41
+	ErrContainerStartTimeout   = errors.New("The container failed to start due to timed out.")
42
+	ErrContainerRootfsReadonly = errors.New("container rootfs is marked read-only")
42 43
 )
43 44
 
44 45
 type StreamConfig struct {
... ...
@@ -616,13 +617,22 @@ func validateID(id string) error {
616 616
 	return nil
617 617
 }
618 618
 
619
-func (container *Container) Copy(resource string) (io.ReadCloser, error) {
619
+func (container *Container) Copy(resource string) (rc io.ReadCloser, err error) {
620 620
 	container.Lock()
621
-	defer container.Unlock()
622
-	var err error
621
+
622
+	defer func() {
623
+		if err != nil {
624
+			// Wait to unlock the container until the archive is fully read
625
+			// (see the ReadCloseWrapper func below) or if there is an error
626
+			// before that occurs.
627
+			container.Unlock()
628
+		}
629
+	}()
630
+
623 631
 	if err := container.Mount(); err != nil {
624 632
 		return nil, err
625 633
 	}
634
+
626 635
 	defer func() {
627 636
 		if err != nil {
628 637
 			// unmount any volumes
... ...
@@ -631,28 +641,11 @@ func (container *Container) Copy(resource string) (io.ReadCloser, error) {
631 631
 			container.Unmount()
632 632
 		}
633 633
 	}()
634
-	mounts, err := container.setupMounts()
635
-	if err != nil {
634
+
635
+	if err := container.mountVolumes(); err != nil {
636 636
 		return nil, err
637 637
 	}
638
-	for _, m := range mounts {
639
-		var dest string
640
-		dest, err = container.GetResourcePath(m.Destination)
641
-		if err != nil {
642
-			return nil, err
643
-		}
644
-		var stat os.FileInfo
645
-		stat, err = os.Stat(m.Source)
646
-		if err != nil {
647
-			return nil, err
648
-		}
649
-		if err = fileutils.CreateIfNotExists(dest, stat.IsDir()); err != nil {
650
-			return nil, err
651
-		}
652
-		if err = mount.Mount(m.Source, dest, "bind", "rbind,ro"); err != nil {
653
-			return nil, err
654
-		}
655
-	}
638
+
656 639
 	basePath, err := container.GetResourcePath(resource)
657 640
 	if err != nil {
658 641
 		return nil, err
... ...
@@ -688,6 +681,7 @@ func (container *Container) Copy(resource string) (io.ReadCloser, error) {
688 688
 		container.CleanupStorage()
689 689
 		container.UnmountVolumes(true)
690 690
 		container.Unmount()
691
+		container.Unlock()
691 692
 		return err
692 693
 	})
693 694
 	container.LogEvent("copy")
... ...
@@ -1190,6 +1184,40 @@ func (container *Container) shouldRestart() bool {
1190 1190
 		(container.hostConfig.RestartPolicy.Name == "on-failure" && container.ExitCode != 0)
1191 1191
 }
1192 1192
 
1193
+func (container *Container) mountVolumes() error {
1194
+	mounts, err := container.setupMounts()
1195
+	if err != nil {
1196
+		return err
1197
+	}
1198
+
1199
+	for _, m := range mounts {
1200
+		dest, err := container.GetResourcePath(m.Destination)
1201
+		if err != nil {
1202
+			return err
1203
+		}
1204
+
1205
+		var stat os.FileInfo
1206
+		stat, err = os.Stat(m.Source)
1207
+		if err != nil {
1208
+			return err
1209
+		}
1210
+		if err = fileutils.CreateIfNotExists(dest, stat.IsDir()); err != nil {
1211
+			return err
1212
+		}
1213
+
1214
+		opts := "rbind,ro"
1215
+		if m.Writable {
1216
+			opts = "rbind,rw"
1217
+		}
1218
+
1219
+		if err := mount.Mount(m.Source, dest, "bind", opts); err != nil {
1220
+			return err
1221
+		}
1222
+	}
1223
+
1224
+	return nil
1225
+}
1226
+
1193 1227
 func (container *Container) copyImagePathContent(v volume.Volume, destination string) error {
1194 1228
 	rootfs, err := symlink.FollowSymlinkInScope(filepath.Join(container.basefs, destination), container.basefs)
1195 1229
 	if err != nil {
1196 1230
deleted file mode 100644
... ...
@@ -1,16 +0,0 @@
1
-package daemon
2
-
3
-import "io"
4
-
5
-func (daemon *Daemon) ContainerCopy(name string, res string) (io.ReadCloser, error) {
6
-	container, err := daemon.Get(name)
7
-	if err != nil {
8
-		return nil, err
9
-	}
10
-
11
-	if res[0] == '/' {
12
-		res = res[1:]
13
-	}
14
-
15
-	return container.Copy(res)
16
-}
... ...
@@ -1,6 +1,7 @@
1 1
 package daemon
2 2
 
3 3
 import (
4
+	"errors"
4 5
 	"fmt"
5 6
 	"io/ioutil"
6 7
 	"os"
... ...
@@ -17,6 +18,10 @@ import (
17 17
 	"github.com/opencontainers/runc/libcontainer/label"
18 18
 )
19 19
 
20
+// ErrVolumeReadonly is used to signal an error when trying to copy data into
21
+// a volume mount that is not writable.
22
+var ErrVolumeReadonly = errors.New("mounted volume is marked read-only")
23
+
20 24
 type mountPoint struct {
21 25
 	Name        string
22 26
 	Destination string
... ...
@@ -47,6 +52,16 @@ func (m *mountPoint) Setup() (string, error) {
47 47
 	return "", fmt.Errorf("Unable to setup mount point, neither source nor volume defined")
48 48
 }
49 49
 
50
+// hasResource checks whether the given absolute path for a container is in
51
+// this mount point. If the relative path starts with `../` then the resource
52
+// is outside of this mount point, but we can't simply check for this prefix
53
+// because it misses `..` which is also outside of the mount, so check both.
54
+func (m *mountPoint) hasResource(absolutePath string) bool {
55
+	relPath, err := filepath.Rel(m.Destination, absolutePath)
56
+
57
+	return err == nil && relPath != ".." && !strings.HasPrefix(relPath, fmt.Sprintf("..%c", filepath.Separator))
58
+}
59
+
50 60
 func (m *mountPoint) Path() string {
51 61
 	if m.Volume != nil {
52 62
 		return m.Volume.Path()