Skip to content

Commit f5eb400

Browse files
authored
Merge pull request moby#2318 from aaronlehmann/follow-links-includedpaths
Follow links in includedPaths to resolve incorrect caching when source path is behind symlink
2 parents 91d2f2d + e9e6cec commit f5eb400

File tree

2 files changed

+176
-27
lines changed

2 files changed

+176
-27
lines changed

cache/contenthash/checksum.go

Lines changed: 135 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -516,21 +516,44 @@ func (cc *cacheContext) includedPaths(ctx context.Context, m *mount, p string, o
516516
txn := cc.tree.Txn()
517517
root = txn.Root()
518518
var (
519-
updated bool
520-
iter *iradix.Iterator
521-
k []byte
522-
kOk bool
519+
updated bool
520+
iter *iradix.Iterator
521+
k []byte
522+
kOk bool
523+
origPrefix string
524+
resolvedPrefix string
523525
)
524526

525527
iter = root.Iterator()
526528

527529
if opts.Wildcard {
528-
k, _, kOk = iter.Next()
530+
origPrefix, k, kOk, err = wildcardPrefix(root, p)
531+
if err != nil {
532+
return nil, err
533+
}
529534
} else {
530-
k = convertPathToKey([]byte(p))
531-
if _, kOk = root.Get(k); kOk {
535+
origPrefix = p
536+
k = convertPathToKey([]byte(origPrefix))
537+
538+
// We need to resolve symlinks here, in case the base path
539+
// involves a symlink. That will match fsutil behavior of
540+
// calling functions such as stat and walk.
541+
var cr *CacheRecord
542+
k, cr, err = getFollowLinks(root, k, true)
543+
if err != nil {
544+
return nil, err
545+
}
546+
kOk = (cr != nil)
547+
}
548+
549+
if origPrefix != "" {
550+
if kOk {
532551
iter.SeekLowerBound(append(append([]byte{}, k...), 0))
533552
}
553+
554+
resolvedPrefix = string(convertKeyToPath(k))
555+
} else {
556+
k, _, kOk = iter.Next()
534557
}
535558

536559
var (
@@ -541,6 +564,20 @@ func (cc *cacheContext) includedPaths(ctx context.Context, m *mount, p string, o
541564
for kOk {
542565
fn := string(convertKeyToPath(k))
543566

567+
// Convert the path prefix from what we found in the prefix
568+
// tree to what the argument specified.
569+
//
570+
// For example, if the original 'p' argument was /a/b and there
571+
// is a symlink a->c, we want fn to be /a/b/foo rather than
572+
// /c/b/foo. This is necessary to ensure correct pattern
573+
// matching.
574+
//
575+
// When wildcards are enabled, this translation applies to the
576+
// portion of 'p' before any wildcards.
577+
if strings.HasPrefix(fn, resolvedPrefix) {
578+
fn = origPrefix + strings.TrimPrefix(fn, resolvedPrefix)
579+
}
580+
544581
for len(parentDirHeaders) != 0 {
545582
lastParentDir := parentDirHeaders[len(parentDirHeaders)-1]
546583
if strings.HasPrefix(fn, lastParentDir.path+"/") {
@@ -591,8 +628,7 @@ func (cc *cacheContext) includedPaths(ctx context.Context, m *mount, p string, o
591628
}
592629
} else {
593630
if !strings.HasPrefix(fn+"/", p+"/") {
594-
k, _, kOk = iter.Next()
595-
continue
631+
break
596632
}
597633

598634
shouldInclude, err = shouldIncludePath(
@@ -692,6 +728,82 @@ func shouldIncludePath(
692728
return true, nil
693729
}
694730

731+
func wildcardPrefix(root *iradix.Node, p string) (string, []byte, bool, error) {
732+
// For consistency with what the copy implementation in fsutil
733+
// does: split pattern into non-wildcard prefix and rest of
734+
// pattern, then follow symlinks when resolving the non-wildcard
735+
// prefix.
736+
737+
d1, d2 := splitWildcards(p)
738+
if d1 == "/" {
739+
return "", nil, false, nil
740+
}
741+
742+
linksWalked := 0
743+
k, cr, err := getFollowLinksWalk(root, convertPathToKey([]byte(d1)), true, &linksWalked)
744+
if err != nil {
745+
return "", k, false, err
746+
}
747+
748+
if d2 != "" && cr != nil && cr.Type == CacheRecordTypeSymlink {
749+
// getFollowLinks only handles symlinks in path
750+
// components before the last component, so
751+
// handle last component in d1 specially.
752+
resolved := string(convertKeyToPath(k))
753+
for {
754+
v, ok := root.Get(k)
755+
756+
if !ok {
757+
return d1, k, false, nil
758+
}
759+
if v.(*CacheRecord).Type != CacheRecordTypeSymlink {
760+
break
761+
}
762+
763+
linksWalked++
764+
if linksWalked > 255 {
765+
return "", k, false, errors.Errorf("too many links")
766+
}
767+
768+
resolved := cleanLink(resolved, v.(*CacheRecord).Linkname)
769+
k = convertPathToKey([]byte(resolved))
770+
}
771+
}
772+
return d1, k, cr != nil, nil
773+
}
774+
775+
func splitWildcards(p string) (d1, d2 string) {
776+
parts := strings.Split(path.Join(p), "/")
777+
var p1, p2 []string
778+
var found bool
779+
for _, p := range parts {
780+
if !found && containsWildcards(p) {
781+
found = true
782+
}
783+
if p == "" {
784+
p = "/"
785+
}
786+
if !found {
787+
p1 = append(p1, p)
788+
} else {
789+
p2 = append(p2, p)
790+
}
791+
}
792+
return filepath.Join(p1...), filepath.Join(p2...)
793+
}
794+
795+
func containsWildcards(name string) bool {
796+
for i := 0; i < len(name); i++ {
797+
ch := name[i]
798+
if ch == '\\' {
799+
i++
800+
} else if ch == '*' || ch == '?' || ch == '[' {
801+
return true
802+
}
803+
}
804+
return false
805+
}
806+
695807
func (cc *cacheContext) checksumNoFollow(ctx context.Context, m *mount, p string) (*CacheRecord, error) {
696808
p = keyPath(p)
697809

@@ -973,14 +1085,8 @@ func getFollowLinksWalk(root *iradix.Node, k []byte, follow bool, linksWalked *i
9731085
if *linksWalked > 255 {
9741086
return nil, nil, errors.Errorf("too many links")
9751087
}
976-
dirPath := path.Clean(string(convertKeyToPath(dir)))
977-
if dirPath == "." || dirPath == "/" {
978-
dirPath = ""
979-
}
980-
link := path.Clean(parent.Linkname)
981-
if !path.IsAbs(link) {
982-
link = path.Join("/", path.Join(path.Dir(dirPath), link))
983-
}
1088+
1089+
link := cleanLink(string(convertKeyToPath(dir)), parent.Linkname)
9841090
return getFollowLinksWalk(root, append(convertPathToKey([]byte(link)), file...), follow, linksWalked)
9851091
}
9861092
}
@@ -992,6 +1098,18 @@ func getFollowLinksWalk(root *iradix.Node, k []byte, follow bool, linksWalked *i
9921098
return k, nil, nil
9931099
}
9941100

1101+
func cleanLink(dir, linkname string) string {
1102+
dirPath := path.Clean(dir)
1103+
if dirPath == "." || dirPath == "/" {
1104+
dirPath = ""
1105+
}
1106+
link := path.Clean(linkname)
1107+
if !path.IsAbs(link) {
1108+
return path.Join("/", path.Join(path.Dir(dirPath), link))
1109+
}
1110+
return link
1111+
}
1112+
9951113
func prepareDigest(fp, p string, fi os.FileInfo) (digest.Digest, error) {
9961114
h, err := NewFileHash(fp, fi)
9971115
if err != nil {

cache/contenthash/checksum_test.go

Lines changed: 41 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -649,8 +649,6 @@ func TestChecksumIncludeDoubleStar(t *testing.T) {
649649
}
650650

651651
func TestChecksumIncludeSymlink(t *testing.T) {
652-
t.Skip("Test will fail until https://github.com/moby/buildkit/issues/2300 is fixed")
653-
654652
t.Parallel()
655653
tmpdir, err := ioutil.TempDir("", "buildkit-state")
656654
require.NoError(t, err)
@@ -663,31 +661,64 @@ func TestChecksumIncludeSymlink(t *testing.T) {
663661

664662
ch := []string{
665663
"ADD data dir",
666-
"ADD data/d1 dir",
664+
"ADD data/d0 dir",
665+
"ADD data/d0/d1 dir",
666+
"ADD data/d0/d1/d2 dir",
667667
"ADD mnt dir",
668668
"ADD mnt/data symlink ../data",
669-
"ADD data/d1/foo file abc",
669+
"ADD data/d0/d1/d2/foo file abc",
670+
"ADD data/symlink-to-d0 symlink d0",
670671
}
671672

672673
ref := createRef(t, cm, ch)
673674

674675
cc, err := newCacheContext(ref)
675676
require.NoError(t, err)
676677

677-
dgst, err := cc.Checksum(context.TODO(), ref, "data/d1", ChecksumOpts{IncludePatterns: []string{"**/foo"}}, nil)
678+
dgstD0, err := cc.Checksum(context.TODO(), ref, "data/d0", ChecksumOpts{IncludePatterns: []string{"**/foo"}}, nil)
678679
require.NoError(t, err)
679680
// File should be included
680-
require.NotEqual(t, digest.FromBytes([]byte{}), dgst)
681+
require.NotEqual(t, digest.FromBytes([]byte{}), dgstD0)
681682

682-
dgst, err = cc.Checksum(context.TODO(), ref, "mnt/data/d1", ChecksumOpts{IncludePatterns: []string{"**/foo"}}, nil)
683+
dgstMntD0, err := cc.Checksum(context.TODO(), ref, "mnt/data/d0", ChecksumOpts{IncludePatterns: []string{"**/foo"}}, nil)
683684
require.NoError(t, err)
684685
// File should be included despite symlink
685-
require.NotEqual(t, digest.FromBytes([]byte{}), dgst)
686+
require.Equal(t, dgstD0, dgstMntD0)
687+
688+
dgstD2, err := cc.Checksum(context.TODO(), ref, "data/d0/d1/d2", ChecksumOpts{IncludePatterns: []string{"**/foo"}}, nil)
689+
require.NoError(t, err)
690+
// File should be included
691+
require.NotEqual(t, digest.FromBytes([]byte{}), dgstD2)
692+
693+
dgstMntD2, err := cc.Checksum(context.TODO(), ref, "mnt/data/d0/d1/d2", ChecksumOpts{IncludePatterns: []string{"**/foo"}}, nil)
694+
require.NoError(t, err)
695+
// File should be included despite symlink
696+
require.Equal(t, dgstD2, dgstMntD2)
686697

687698
// Same, with Wildcard = true
688-
dgst, err = cc.Checksum(context.TODO(), ref, "mnt/data/d1", ChecksumOpts{IncludePatterns: []string{"**/foo"}, Wildcard: true}, nil)
699+
dgstMntD0Wildcard, err := cc.Checksum(context.TODO(), ref, "mnt/data/d0", ChecksumOpts{IncludePatterns: []string{"**/foo"}, Wildcard: true}, nil)
700+
require.NoError(t, err)
701+
require.Equal(t, dgstD0, dgstMntD0Wildcard)
702+
703+
dgstMntD0Wildcard2, err := cc.Checksum(context.TODO(), ref, "mnt/data/d*", ChecksumOpts{IncludePatterns: []string{"**/foo"}, Wildcard: true}, nil)
704+
require.NoError(t, err)
705+
require.Equal(t, dgstD0, dgstMntD0Wildcard2)
706+
707+
dgstMntD2Wildcard, err := cc.Checksum(context.TODO(), ref, "mnt/data/d0/d1/d2", ChecksumOpts{IncludePatterns: []string{"**/foo"}, Wildcard: true}, nil)
708+
require.NoError(t, err)
709+
require.Equal(t, dgstD2, dgstMntD2Wildcard)
710+
711+
dgstMntD2Wildcard2, err := cc.Checksum(context.TODO(), ref, "mnt/data/d0/d1/d*", ChecksumOpts{IncludePatterns: []string{"**/foo"}, Wildcard: true}, nil)
712+
require.NoError(t, err)
713+
require.Equal(t, dgstD2, dgstMntD2Wildcard2)
714+
715+
dgstMntInnerWildcard, err := cc.Checksum(context.TODO(), ref, "mnt/data/d0/d*/d2", ChecksumOpts{IncludePatterns: []string{"**/foo"}, Wildcard: true}, nil)
716+
require.NoError(t, err)
717+
require.Equal(t, dgstD2, dgstMntInnerWildcard)
718+
719+
dgstMntInnerWildcard2, err := cc.Checksum(context.TODO(), ref, "mnt/data/symlink-to-d0/d*/d2", ChecksumOpts{IncludePatterns: []string{"**/foo"}, Wildcard: true}, nil)
689720
require.NoError(t, err)
690-
require.NotEqual(t, digest.FromBytes([]byte{}), dgst)
721+
require.Equal(t, dgstD2, dgstMntInnerWildcard2)
691722
}
692723

693724
func TestHandleChange(t *testing.T) {

0 commit comments

Comments
 (0)