Skip to content
This repository was archived by the owner on Feb 8, 2021. It is now read-only.

Commit 47da59f

Browse files
committed
Fix symlink handling in builder ADD/COPY commands
Fixes #17290 Fixes following issues: - Cache checksums turning off while walking a broken symlink. - Cache checksums were taken from symlinks while targets were actually copied. - Copying a symlink pointing to a file to a directory used the basename of the target as a destination basename, instead of basename of the symlink. Signed-off-by: Tonis Tiigi <[email protected]>
1 parent 55711a2 commit 47da59f

File tree

6 files changed

+187
-45
lines changed

6 files changed

+187
-45
lines changed

builder/builder.go

+12-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ type Context interface {
3333
Close() error
3434
// Stat returns an entry corresponding to path if any.
3535
// It is recommended to return an error if path was not found.
36-
Stat(path string) (FileInfo, error)
36+
// If path is a symlink it also returns the path to the target file.
37+
Stat(path string) (string, FileInfo, error)
3738
// Open opens path from the context and returns a readable stream of it.
3839
Open(path string) (io.ReadCloser, error)
3940
// Walk walks the tree of the context with the function passed to it.
@@ -64,13 +65,23 @@ type PathFileInfo struct {
6465
os.FileInfo
6566
// FilePath holds the absolute path to the file.
6667
FilePath string
68+
// Name holds the basename for the file.
69+
FileName string
6770
}
6871

6972
// Path returns the absolute path to the file.
7073
func (fi PathFileInfo) Path() string {
7174
return fi.FilePath
7275
}
7376

77+
// Name returns the basename of the file.
78+
func (fi PathFileInfo) Name() string {
79+
if fi.FileName != "" {
80+
return fi.FileName
81+
}
82+
return fi.FileInfo.Name()
83+
}
84+
7485
// Hashed defines an extra method intended for implementations of os.FileInfo.
7586
type Hashed interface {
7687
// Hash returns the hash of a file.

builder/dockerfile/internals.go

+7-6
Original file line numberDiff line numberDiff line change
@@ -366,7 +366,7 @@ func (b *Builder) calcCopyInfo(cmdName, origPath string, allowLocalDecompression
366366

367367
// Must be a dir or a file
368368

369-
fi, err := b.context.Stat(origPath)
369+
statPath, fi, err := b.context.Stat(origPath)
370370
if err != nil {
371371
return nil, err
372372
}
@@ -383,18 +383,19 @@ func (b *Builder) calcCopyInfo(cmdName, origPath string, allowLocalDecompression
383383
hfi.SetHash("file:" + hfi.Hash())
384384
return copyInfos, nil
385385
}
386-
387386
// Must be a dir
388-
389387
var subfiles []string
390-
b.context.Walk(origPath, func(path string, info builder.FileInfo, err error) error {
388+
err = b.context.Walk(statPath, func(path string, info builder.FileInfo, err error) error {
391389
if err != nil {
392390
return err
393391
}
394392
// we already checked handleHash above
395393
subfiles = append(subfiles, info.(builder.Hashed).Hash())
396394
return nil
397395
})
396+
if err != nil {
397+
return nil, err
398+
}
398399

399400
sort.Strings(subfiles)
400401
hasher := sha256.New()
@@ -613,9 +614,9 @@ func (b *Builder) readDockerfile() error {
613614
// back to 'Dockerfile' and use that in the error message.
614615
if b.DockerfileName == "" {
615616
b.DockerfileName = api.DefaultDockerfileName
616-
if _, err := b.context.Stat(b.DockerfileName); os.IsNotExist(err) {
617+
if _, _, err := b.context.Stat(b.DockerfileName); os.IsNotExist(err) {
617618
lowercase := strings.ToLower(b.DockerfileName)
618-
if _, err := b.context.Stat(lowercase); err == nil {
619+
if _, _, err := b.context.Stat(lowercase); err == nil {
619620
b.DockerfileName = lowercase
620621
}
621622
}

builder/tarsum.go

+28-35
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import (
55
"io"
66
"os"
77
"path/filepath"
8-
"strings"
98

109
"github.com/docker/docker/pkg/archive"
1110
"github.com/docker/docker/pkg/chrootarchive"
@@ -43,26 +42,32 @@ func (c *tarSumContext) Open(path string) (io.ReadCloser, error) {
4342
return r, nil
4443
}
4544

46-
func (c *tarSumContext) Stat(path string) (fi FileInfo, err error) {
45+
func (c *tarSumContext) Stat(path string) (string, FileInfo, error) {
4746
cleanpath, fullpath, err := c.normalize(path)
4847
if err != nil {
49-
return nil, err
48+
return "", nil, err
5049
}
5150

5251
st, err := os.Lstat(fullpath)
5352
if err != nil {
54-
return nil, convertPathError(err, cleanpath)
53+
return "", nil, convertPathError(err, cleanpath)
54+
}
55+
56+
rel, err := filepath.Rel(c.root, fullpath)
57+
if err != nil {
58+
return "", nil, convertPathError(err, cleanpath)
5559
}
5660

57-
fi = PathFileInfo{st, fullpath}
58-
// we set sum to path by default for the case where GetFile returns nil.
59-
// The usual case is if cleanpath is empty.
61+
// We set sum to path by default for the case where GetFile returns nil.
62+
// The usual case is if relative path is empty.
6063
sum := path
61-
if tsInfo := c.sums.GetFile(cleanpath); tsInfo != nil {
64+
// Use the checksum of the followed path(not the possible symlink) because
65+
// this is the file that is actually copied.
66+
if tsInfo := c.sums.GetFile(rel); tsInfo != nil {
6267
sum = tsInfo.Sum()
6368
}
64-
fi = &HashedFileInfo{fi, sum}
65-
return fi, nil
69+
fi := &HashedFileInfo{PathFileInfo{st, fullpath, filepath.Base(cleanpath)}, sum}
70+
return rel, fi, nil
6671
}
6772

6873
// MakeTarSumContext returns a build Context from a tar stream.
@@ -114,46 +119,34 @@ func (c *tarSumContext) normalize(path string) (cleanpath, fullpath string, err
114119
if err != nil {
115120
return "", "", fmt.Errorf("Forbidden path outside the build context: %s (%s)", path, fullpath)
116121
}
117-
_, err = os.Stat(fullpath)
122+
_, err = os.Lstat(fullpath)
118123
if err != nil {
119124
return "", "", convertPathError(err, path)
120125
}
121126
return
122127
}
123128

124129
func (c *tarSumContext) Walk(root string, walkFn WalkFunc) error {
125-
for _, tsInfo := range c.sums {
126-
path := tsInfo.Name()
127-
path, fullpath, err := c.normalize(path)
130+
root = filepath.Join(c.root, filepath.Join(string(filepath.Separator), root))
131+
return filepath.Walk(root, func(fullpath string, info os.FileInfo, err error) error {
132+
rel, err := filepath.Rel(c.root, fullpath)
128133
if err != nil {
129134
return err
130135
}
131-
132-
// Any file in the context that starts with the given path will be
133-
// picked up and its hashcode used. However, we'll exclude the
134-
// root dir itself. We do this for a coupel of reasons:
135-
// 1 - ADD/COPY will not copy the dir itself, just its children
136-
// so there's no reason to include it in the hash calc
137-
// 2 - the metadata on the dir will change when any child file
138-
// changes. This will lead to a miss in the cache check if that
139-
// child file is in the .dockerignore list.
140-
if rel, err := filepath.Rel(root, path); err != nil {
141-
return err
142-
} else if rel == "." || strings.HasPrefix(rel, ".."+string(os.PathSeparator)) {
143-
continue
136+
if rel == "." {
137+
return nil
144138
}
145139

146-
info, err := os.Lstat(fullpath)
147-
if err != nil {
148-
return convertPathError(err, path)
140+
sum := rel
141+
if tsInfo := c.sums.GetFile(rel); tsInfo != nil {
142+
sum = tsInfo.Sum()
149143
}
150-
// TODO check context breakout?
151-
fi := &HashedFileInfo{PathFileInfo{info, fullpath}, tsInfo.Sum()}
152-
if err := walkFn(path, fi, nil); err != nil {
144+
fi := &HashedFileInfo{PathFileInfo{FileInfo: info, FilePath: fullpath}, sum}
145+
if err := walkFn(rel, fi, nil); err != nil {
153146
return err
154147
}
155-
}
156-
return nil
148+
return nil
149+
})
157150
}
158151

159152
func (c *tarSumContext) Remove(path string) error {

daemon/daemonbuilder/builder.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ func (d Docker) Copy(c *daemon.Container, destPath string, src builder.FileInfo,
183183

184184
// only needed for fixPermissions, but might as well put it before CopyFileWithTar
185185
if destExists && destStat.IsDir() {
186-
destPath = filepath.Join(destPath, filepath.Base(srcPath))
186+
destPath = filepath.Join(destPath, src.Name())
187187
}
188188

189189
if err := idtools.MkdirAllNewAs(filepath.Dir(destPath), 0755, rootUID, rootGID); err != nil {

integration-cli/docker_cli_build_test.go

+125
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121

2222
"github.com/docker/docker/builder/dockerfile/command"
2323
"github.com/docker/docker/pkg/archive"
24+
"github.com/docker/docker/pkg/integration/checker"
2425
"github.com/docker/docker/pkg/stringutils"
2526
"github.com/go-check/check"
2627
)
@@ -6277,3 +6278,127 @@ func (s *DockerSuite) TestBuildMultipleTags(c *check.C) {
62776278
c.Assert(err, check.IsNil)
62786279
c.Assert(id1, check.Equals, id2)
62796280
}
6281+
6282+
// #17290
6283+
func (s *DockerSuite) TestBuildCacheBrokenSymlink(c *check.C) {
6284+
testRequires(c, DaemonIsLinux)
6285+
name := "testbuildbrokensymlink"
6286+
ctx, err := fakeContext(`
6287+
FROM busybox
6288+
COPY . ./`,
6289+
map[string]string{
6290+
"foo": "bar",
6291+
})
6292+
c.Assert(err, checker.IsNil)
6293+
defer ctx.Close()
6294+
6295+
err = os.Symlink(filepath.Join(ctx.Dir, "nosuchfile"), filepath.Join(ctx.Dir, "asymlink"))
6296+
c.Assert(err, checker.IsNil)
6297+
6298+
// warm up cache
6299+
_, err = buildImageFromContext(name, ctx, true)
6300+
c.Assert(err, checker.IsNil)
6301+
6302+
// add new file to context, should invalidate cache
6303+
err = ioutil.WriteFile(filepath.Join(ctx.Dir, "newfile"), []byte("foo"), 0644)
6304+
c.Assert(err, checker.IsNil)
6305+
6306+
_, out, err := buildImageFromContextWithOut(name, ctx, true)
6307+
c.Assert(err, checker.IsNil)
6308+
6309+
c.Assert(out, checker.Not(checker.Contains), "Using cache")
6310+
6311+
}
6312+
6313+
func (s *DockerSuite) TestBuildFollowSymlinkToFile(c *check.C) {
6314+
testRequires(c, DaemonIsLinux)
6315+
name := "testbuildbrokensymlink"
6316+
ctx, err := fakeContext(`
6317+
FROM busybox
6318+
COPY asymlink target`,
6319+
map[string]string{
6320+
"foo": "bar",
6321+
})
6322+
c.Assert(err, checker.IsNil)
6323+
defer ctx.Close()
6324+
6325+
err = os.Symlink("foo", filepath.Join(ctx.Dir, "asymlink"))
6326+
c.Assert(err, checker.IsNil)
6327+
6328+
id, err := buildImageFromContext(name, ctx, true)
6329+
c.Assert(err, checker.IsNil)
6330+
6331+
out, _ := dockerCmd(c, "run", "--rm", id, "cat", "target")
6332+
c.Assert(out, checker.Matches, "bar")
6333+
6334+
// change target file should invalidate cache
6335+
err = ioutil.WriteFile(filepath.Join(ctx.Dir, "foo"), []byte("baz"), 0644)
6336+
c.Assert(err, checker.IsNil)
6337+
6338+
id, out, err = buildImageFromContextWithOut(name, ctx, true)
6339+
c.Assert(err, checker.IsNil)
6340+
c.Assert(out, checker.Not(checker.Contains), "Using cache")
6341+
6342+
out, _ = dockerCmd(c, "run", "--rm", id, "cat", "target")
6343+
c.Assert(out, checker.Matches, "baz")
6344+
}
6345+
6346+
func (s *DockerSuite) TestBuildFollowSymlinkToDir(c *check.C) {
6347+
testRequires(c, DaemonIsLinux)
6348+
name := "testbuildbrokensymlink"
6349+
ctx, err := fakeContext(`
6350+
FROM busybox
6351+
COPY asymlink /`,
6352+
map[string]string{
6353+
"foo/abc": "bar",
6354+
"foo/def": "baz",
6355+
})
6356+
c.Assert(err, checker.IsNil)
6357+
defer ctx.Close()
6358+
6359+
err = os.Symlink("foo", filepath.Join(ctx.Dir, "asymlink"))
6360+
c.Assert(err, checker.IsNil)
6361+
6362+
id, err := buildImageFromContext(name, ctx, true)
6363+
c.Assert(err, checker.IsNil)
6364+
6365+
out, _ := dockerCmd(c, "run", "--rm", id, "cat", "abc", "def")
6366+
c.Assert(out, checker.Matches, "barbaz")
6367+
6368+
// change target file should invalidate cache
6369+
err = ioutil.WriteFile(filepath.Join(ctx.Dir, "foo/def"), []byte("bax"), 0644)
6370+
c.Assert(err, checker.IsNil)
6371+
6372+
id, out, err = buildImageFromContextWithOut(name, ctx, true)
6373+
c.Assert(err, checker.IsNil)
6374+
c.Assert(out, checker.Not(checker.Contains), "Using cache")
6375+
6376+
out, _ = dockerCmd(c, "run", "--rm", id, "cat", "abc", "def")
6377+
c.Assert(out, checker.Matches, "barbax")
6378+
6379+
}
6380+
6381+
// TestBuildSymlinkBasename tests that target file gets basename from symlink,
6382+
// not from the target file.
6383+
func (s *DockerSuite) TestBuildSymlinkBasename(c *check.C) {
6384+
testRequires(c, DaemonIsLinux)
6385+
name := "testbuildbrokensymlink"
6386+
ctx, err := fakeContext(`
6387+
FROM busybox
6388+
COPY asymlink /`,
6389+
map[string]string{
6390+
"foo": "bar",
6391+
})
6392+
c.Assert(err, checker.IsNil)
6393+
defer ctx.Close()
6394+
6395+
err = os.Symlink("foo", filepath.Join(ctx.Dir, "asymlink"))
6396+
c.Assert(err, checker.IsNil)
6397+
6398+
id, err := buildImageFromContext(name, ctx, true)
6399+
c.Assert(err, checker.IsNil)
6400+
6401+
out, _ := dockerCmd(c, "run", "--rm", id, "cat", "asymlink")
6402+
c.Assert(out, checker.Matches, "bar")
6403+
6404+
}

integration-cli/docker_utils.go

+14-2
Original file line numberDiff line numberDiff line change
@@ -1234,6 +1234,14 @@ func buildImage(name, dockerfile string, useCache bool, buildFlags ...string) (s
12341234
}
12351235

12361236
func buildImageFromContext(name string, ctx *FakeContext, useCache bool, buildFlags ...string) (string, error) {
1237+
id, _, err := buildImageFromContextWithOut(name, ctx, useCache, buildFlags...)
1238+
if err != nil {
1239+
return "", err
1240+
}
1241+
return id, nil
1242+
}
1243+
1244+
func buildImageFromContextWithOut(name string, ctx *FakeContext, useCache bool, buildFlags ...string) (string, string, error) {
12371245
args := []string{"build", "-t", name}
12381246
if !useCache {
12391247
args = append(args, "--no-cache")
@@ -1244,9 +1252,13 @@ func buildImageFromContext(name string, ctx *FakeContext, useCache bool, buildFl
12441252
buildCmd.Dir = ctx.Dir
12451253
out, exitCode, err := runCommandWithOutput(buildCmd)
12461254
if err != nil || exitCode != 0 {
1247-
return "", fmt.Errorf("failed to build the image: %s", out)
1255+
return "", "", fmt.Errorf("failed to build the image: %s", out)
12481256
}
1249-
return getIDByName(name)
1257+
id, err := getIDByName(name)
1258+
if err != nil {
1259+
return "", "", err
1260+
}
1261+
return id, out, nil
12501262
}
12511263

12521264
func buildImageFromPath(name, path string, useCache bool, buildFlags ...string) (string, error) {

0 commit comments

Comments
 (0)