Resolve symlinks when fetching file contents (#782)

This commit is contained in:
Alex Goodman
2022-02-24 10:01:59 -05:00
committed by GitHub
parent 7eea98fcc5
commit 99bb93d0fe
41 changed files with 947 additions and 138 deletions
+17 -1
View File
@@ -126,7 +126,7 @@ func (r *allLayersResolver) FilesByGlob(patterns ...string) ([]Location, error)
for _, pattern := range patterns {
for idx, layerIdx := range r.layers {
results, err := r.img.Layers[layerIdx].Tree.FilesByGlob(pattern, filetree.DoNotFollowDeadBasenameLinks)
results, err := r.img.Layers[layerIdx].Tree.FilesByGlob(pattern, filetree.FollowBasenameLinks, filetree.DoNotFollowDeadBasenameLinks)
if err != nil {
return nil, fmt.Errorf("failed to resolve files by glob (%s): %w", pattern, err)
}
@@ -184,6 +184,22 @@ func (r *allLayersResolver) RelativeFileByPath(location Location, path string) *
// FileContentsByLocation fetches file contents for a single file reference, irregardless of the source layer.
// If the path does not exist an error is returned.
func (r *allLayersResolver) FileContentsByLocation(location Location) (io.ReadCloser, error) {
entry, err := r.img.FileCatalog.Get(location.ref)
if err != nil {
return nil, fmt.Errorf("unable to get metadata for path=%q from file catalog: %w", location.RealPath, err)
}
switch entry.Metadata.TypeFlag {
case tar.TypeSymlink, tar.TypeLink:
// the location we are searching may be a symlink, we should always work with the resolved file
newLocation := r.RelativeFileByPath(location, location.VirtualPath)
if newLocation == nil {
// this is a dead link
return nil, fmt.Errorf("no contents for location=%q", location.VirtualPath)
}
location = *newLocation
}
return r.img.FileContentsByRef(location.ref)
}
+240
View File
@@ -1,6 +1,8 @@
package source
import (
"github.com/stretchr/testify/require"
"io"
"testing"
"github.com/stretchr/testify/assert"
@@ -301,3 +303,241 @@ func Test_imageAllLayersResolver_hasFilesystemIDInLocation(t *testing.T) {
}
}
func TestAllLayersImageResolver_FilesContents(t *testing.T) {
tests := []struct {
name string
fixture string
contents []string
}{
{
name: "one degree",
fixture: "link-2",
contents: []string{
"file 2!", // from the first resolved layer's perspective
"NEW file override!", // from the second resolved layers perspective
},
},
{
name: "two degrees",
fixture: "link-indirect",
contents: []string{
"file 2!",
"NEW file override!",
},
},
{
name: "dead link",
fixture: "link-dead",
contents: []string{},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
img := imagetest.GetFixtureImage(t, "docker-archive", "image-symlinks")
resolver, err := newAllLayersResolver(img)
assert.NoError(t, err)
refs, err := resolver.FilesByPath(test.fixture)
require.NoError(t, err)
// the given path should have an overridden file
require.Len(t, refs, len(test.contents))
for idx, loc := range refs {
reader, err := resolver.FileContentsByLocation(loc)
require.NoError(t, err)
actual, err := io.ReadAll(reader)
require.NoError(t, err)
assert.Equal(t, test.contents[idx], string(actual))
}
})
}
}
func Test_imageAllLayersResolver_resolvesLinks(t *testing.T) {
tests := []struct {
name string
runner func(FileResolver) []Location
expected []Location
}{
{
name: "by mimetype",
runner: func(resolver FileResolver) []Location {
// links should not show up when searching mimetype
actualLocations, err := resolver.FilesByMIMEType("text/plain")
assert.NoError(t, err)
return actualLocations
},
expected: []Location{
{
Coordinates: Coordinates{
RealPath: "/etc/group",
},
VirtualPath: "/etc/group",
},
{
Coordinates: Coordinates{
RealPath: "/etc/passwd",
},
VirtualPath: "/etc/passwd",
},
{
Coordinates: Coordinates{
RealPath: "/etc/shadow",
},
VirtualPath: "/etc/shadow",
},
{
Coordinates: Coordinates{
RealPath: "/file-1.txt",
},
VirtualPath: "/file-1.txt",
},
// copy 1
{
Coordinates: Coordinates{
RealPath: "/file-2.txt",
},
VirtualPath: "/file-2.txt",
},
{
Coordinates: Coordinates{
RealPath: "/file-3.txt",
},
VirtualPath: "/file-3.txt",
},
// copy 2
{
Coordinates: Coordinates{
RealPath: "/file-2.txt",
},
VirtualPath: "/file-2.txt",
},
// copy 1
{
Coordinates: Coordinates{
RealPath: "/parent/file-4.txt",
},
VirtualPath: "/parent/file-4.txt",
},
// copy 2
{
Coordinates: Coordinates{
RealPath: "/parent/file-4.txt",
},
VirtualPath: "/parent/file-4.txt",
},
},
},
{
name: "by glob",
runner: func(resolver FileResolver) []Location {
// links are searched, but resolve to the real files
actualLocations, err := resolver.FilesByGlob("*ink-*")
assert.NoError(t, err)
return actualLocations
},
expected: []Location{
{
Coordinates: Coordinates{
RealPath: "/file-1.txt",
},
VirtualPath: "/link-1",
},
// copy 1
{
Coordinates: Coordinates{
RealPath: "/file-2.txt",
},
VirtualPath: "/link-2",
},
// copy 2
{
Coordinates: Coordinates{
RealPath: "/file-2.txt",
},
VirtualPath: "/link-2",
},
{
Coordinates: Coordinates{
RealPath: "/file-3.txt",
},
VirtualPath: "/link-within",
},
},
},
{
name: "by path to degree 1 link",
runner: func(resolver FileResolver) []Location {
// links resolve to the final file
actualLocations, err := resolver.FilesByPath("/link-2")
assert.NoError(t, err)
return actualLocations
},
expected: []Location{
// we have multiple copies across layers
{
Coordinates: Coordinates{
RealPath: "/file-2.txt",
},
VirtualPath: "/link-2",
},
{
Coordinates: Coordinates{
RealPath: "/file-2.txt",
},
VirtualPath: "/link-2",
},
},
},
{
name: "by path to degree 2 link",
runner: func(resolver FileResolver) []Location {
// multiple links resolves to the final file
actualLocations, err := resolver.FilesByPath("/link-indirect")
assert.NoError(t, err)
return actualLocations
},
expected: []Location{
// we have multiple copies across layers
{
Coordinates: Coordinates{
RealPath: "/file-2.txt",
},
VirtualPath: "/link-indirect",
},
{
Coordinates: Coordinates{
RealPath: "/file-2.txt",
},
VirtualPath: "/link-indirect",
},
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
img := imagetest.GetFixtureImage(t, "docker-archive", "image-symlinks")
resolver, err := newAllLayersResolver(img)
assert.NoError(t, err)
actualLocations := test.runner(resolver)
assert.Len(t, actualLocations, len(test.expected))
for i, actual := range actualLocations {
assert.Equal(t, test.expected[i].RealPath, actual.RealPath)
assert.Equal(t, test.expected[i].VirtualPath, actual.VirtualPath)
}
})
}
}
+47 -16
View File
@@ -48,24 +48,35 @@ type directoryResolver struct {
}
func newDirectoryResolver(root string, pathFilters ...pathFilterFn) (*directoryResolver, error) {
currentWd, err := os.Getwd()
currentWD, err := os.Getwd()
if err != nil {
return nil, fmt.Errorf("could not create directory resolver: %w", err)
return nil, fmt.Errorf("could not gret CWD: %w", err)
}
// we have to account for the root being accessed through a symlink path and always resolve the real path. Otherwise
// we will not be able to normalize given paths that fall under the resolver
cleanCWD, err := filepath.EvalSymlinks(currentWD)
if err != nil {
return nil, fmt.Errorf("could not evaluate CWD symlinks: %w", err)
}
cleanRoot, err := filepath.EvalSymlinks(root)
if err != nil {
return nil, fmt.Errorf("could not evaluate root=%q symlinks: %w", root, err)
}
var currentWdRelRoot string
if path.IsAbs(root) {
currentWdRelRoot, err = filepath.Rel(currentWd, root)
if path.IsAbs(cleanRoot) {
currentWdRelRoot, err = filepath.Rel(cleanCWD, cleanRoot)
if err != nil {
return nil, fmt.Errorf("could not create directory resolver: %w", err)
return nil, fmt.Errorf("could not determine given root path to CWD: %w", err)
}
} else {
currentWdRelRoot = filepath.Clean(root)
currentWdRelRoot = filepath.Clean(cleanRoot)
}
resolver := directoryResolver{
path: root,
currentWd: currentWd,
path: cleanRoot,
currentWd: cleanCWD,
currentWdRelativeToRoot: currentWdRelRoot,
fileTree: filetree.NewFileTree(),
metadata: make(map[file.ID]FileMetadata),
@@ -74,7 +85,7 @@ func newDirectoryResolver(root string, pathFilters ...pathFilterFn) (*directoryR
errPaths: make(map[string]error),
}
return &resolver, indexAllRoots(root, resolver.indexTree)
return &resolver, indexAllRoots(cleanRoot, resolver.indexTree)
}
func (r *directoryResolver) indexTree(root string, stager *progress.Stage) ([]string, error) {
@@ -233,7 +244,9 @@ func (r directoryResolver) addSymlinkToIndex(p string, info os.FileInfo) (string
}
location := NewLocationFromDirectory(p, *ref)
location.VirtualPath = p
metadata := fileMetadataFromPath(p, usedInfo, r.isInIndex(location))
metadata.LinkDestination = linkTarget
r.addFileMetadataToIndex(ref, metadata)
return targetAbsPath, nil
@@ -305,8 +318,15 @@ func (r directoryResolver) FilesByPath(userPaths ...string) ([]Location, error)
continue
}
// we should be resolving symlinks and preserving this information as a VirtualPath to the real file
evaluatedPath, err := filepath.EvalSymlinks(userStrPath)
if err != nil {
log.Warnf("directory resolver unable to evaluate symlink for path=%q : %+v", userPath, err)
continue
}
// TODO: why not use stored metadata?
fileMeta, err := os.Stat(userStrPath)
fileMeta, err := os.Stat(evaluatedPath)
if errors.Is(err, os.ErrNotExist) {
// note: there are other kinds of errors other than os.ErrNotExist that may be given that is platform
// specific, but essentially hints at the same overall problem (that the path does not exist). Such an
@@ -317,7 +337,7 @@ func (r directoryResolver) FilesByPath(userPaths ...string) ([]Location, error)
// invalid paths. This logging statement is meant to raise IO or permissions related problems.
var pathErr *os.PathError
if !errors.As(err, &pathErr) {
log.Warnf("path is not valid (%s): %+v", userStrPath, err)
log.Warnf("path is not valid (%s): %+v", evaluatedPath, err)
}
continue
}
@@ -331,9 +351,14 @@ func (r directoryResolver) FilesByPath(userPaths ...string) ([]Location, error)
userStrPath = windowsToPosix(userStrPath)
}
exists, ref, err := r.fileTree.File(file.Path(userStrPath))
exists, ref, err := r.fileTree.File(file.Path(userStrPath), filetree.FollowBasenameLinks)
if err == nil && exists {
references = append(references, NewLocationFromDirectory(r.responsePath(userStrPath), *ref))
loc := NewVirtualLocationFromDirectory(
r.responsePath(string(ref.RealPath)), // the actual path relative to the resolver root
r.responsePath(userStrPath), // the path used to access this file, relative to the resolver root
*ref,
)
references = append(references, loc)
}
}
@@ -345,12 +370,17 @@ func (r directoryResolver) FilesByGlob(patterns ...string) ([]Location, error) {
result := make([]Location, 0)
for _, pattern := range patterns {
globResults, err := r.fileTree.FilesByGlob(pattern)
globResults, err := r.fileTree.FilesByGlob(pattern, filetree.FollowBasenameLinks)
if err != nil {
return nil, err
}
for _, globResult := range globResults {
result = append(result, NewLocationFromDirectory(r.responsePath(string(globResult.MatchPath)), globResult.Reference))
loc := NewVirtualLocationFromDirectory(
r.responsePath(string(globResult.Reference.RealPath)), // the actual path relative to the resolver root
r.responsePath(string(globResult.MatchPath)), // the path used to access this file, relative to the resolver root
globResult.Reference,
)
result = append(result, loc)
}
}
@@ -404,7 +434,8 @@ func (r *directoryResolver) AllLocations() <-chan Location {
results := make(chan Location)
go func() {
defer close(results)
for _, ref := range r.fileTree.AllFiles() {
// this should be all non-directory types
for _, ref := range r.fileTree.AllFiles(file.TypeReg, file.TypeSymlink, file.TypeHardLink, file.TypeBlockDevice, file.TypeCharacterDevice, file.TypeFifo) {
results <- NewLocationFromDirectory(r.responsePath(string(ref.RealPath)), ref)
}
}()
+99 -21
View File
@@ -4,6 +4,7 @@
package source
import (
"io"
"io/fs"
"io/ioutil"
"os"
@@ -259,6 +260,45 @@ func TestDirectoryResolver_FilesByGlobSingle(t *testing.T) {
assert.Equal(t, "image-symlinks/file-1.txt", refs[0].RealPath)
}
func TestDirectoryResolver_FilesByPath_ResolvesSymlinks(t *testing.T) {
tests := []struct {
name string
fixture string
}{
{
name: "one degree",
fixture: "link_to_new_readme",
},
{
name: "two degrees",
fixture: "link_to_link_to_new_readme",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
resolver, err := newDirectoryResolver("./test-fixtures/symlinks-simple")
assert.NoError(t, err)
refs, err := resolver.FilesByPath(test.fixture)
require.NoError(t, err)
assert.Len(t, refs, 1)
reader, err := resolver.FileContentsByLocation(refs[0])
require.NoError(t, err)
actual, err := io.ReadAll(reader)
require.NoError(t, err)
expected, err := os.ReadFile("test-fixtures/symlinks-simple/readme")
require.NoError(t, err)
assert.Equal(t, string(expected), string(actual))
})
}
}
func TestDirectoryResolverDoesNotIgnoreRelativeSystemPaths(t *testing.T) {
// let's make certain that "dev/place" is not ignored, since it is not "/dev/place"
resolver, err := newDirectoryResolver("test-fixtures/system_paths/target")
@@ -583,7 +623,7 @@ func Test_directoryResolver_FilesByMIMEType(t *testing.T) {
func Test_IndexingNestedSymLinks(t *testing.T) {
resolver, err := newDirectoryResolver("./test-fixtures/symlinks-simple")
assert.NoError(t, err)
require.NoError(t, err)
// check that we can get the real path
locations, err := resolver.FilesByPath("./readme")
@@ -593,12 +633,41 @@ func Test_IndexingNestedSymLinks(t *testing.T) {
// check that we can access the same file via 1 symlink
locations, err = resolver.FilesByPath("./link_to_new_readme")
require.NoError(t, err)
assert.Len(t, locations, 1)
require.Len(t, locations, 1)
assert.Equal(t, "readme", locations[0].RealPath)
assert.Equal(t, "link_to_new_readme", locations[0].VirtualPath)
// check that we can access the same file via 2 symlinks
locations, err = resolver.FilesByPath("./link_to_link_to_new_readme")
require.NoError(t, err)
assert.Len(t, locations, 1)
require.Len(t, locations, 1)
assert.Equal(t, "readme", locations[0].RealPath)
assert.Equal(t, "link_to_link_to_new_readme", locations[0].VirtualPath)
// check that we can access the same file via 2 symlinks
locations, err = resolver.FilesByGlob("**/link_*")
require.NoError(t, err)
require.Len(t, locations, 2)
// returned locations can be in any order
expectedVirtualPaths := []string{
"link_to_link_to_new_readme",
"link_to_new_readme",
}
expectedRealPaths := []string{
"readme",
}
actualRealPaths := strset.New()
actualVirtualPaths := strset.New()
for _, a := range locations {
actualVirtualPaths.Add(a.VirtualPath)
actualRealPaths.Add(a.RealPath)
}
assert.ElementsMatch(t, expectedVirtualPaths, actualVirtualPaths.List())
assert.ElementsMatch(t, expectedRealPaths, actualRealPaths.List())
}
func Test_IndexingNestedSymLinks_ignoredIndexes(t *testing.T) {
@@ -607,38 +676,27 @@ func Test_IndexingNestedSymLinks_ignoredIndexes(t *testing.T) {
}
resolver, err := newDirectoryResolver("./test-fixtures/symlinks-simple", filterFn)
assert.NoError(t, err)
var testingLocations []Location
require.NoError(t, err)
// the path to the real file is PRUNED from the index, so we should NOT expect a location returned
locations, err := resolver.FilesByPath("./readme")
require.NoError(t, err)
assert.Empty(t, locations)
// check that we can access the same file via 1 symlink
// check that we cannot access the file even via symlink
locations, err = resolver.FilesByPath("./link_to_new_readme")
require.NoError(t, err)
assert.Len(t, locations, 1)
testingLocations = append(testingLocations, locations...)
assert.Empty(t, locations)
// check that we can access the same file via 2 symlinks
// check that we still cannot access the same file via 2 symlinks
locations, err = resolver.FilesByPath("./link_to_link_to_new_readme")
require.NoError(t, err)
assert.Len(t, locations, 1)
testingLocations = append(testingLocations, locations...)
// check that we CANNOT get contents from any of the link locations
for _, location := range testingLocations {
contentReader, err := resolver.FileContentsByLocation(location)
assert.Errorf(t, err, "expected an error for getting content from a location not in the index")
assert.Nil(t, contentReader)
}
assert.Empty(t, locations)
}
func Test_IndexingNestedSymLinksOutsideOfRoot(t *testing.T) {
resolver, err := newDirectoryResolver("./test-fixtures/symlinks-roots/root")
assert.NoError(t, err)
resolver, err := newDirectoryResolver("./test-fixtures/symlinks-multiple-roots/root")
require.NoError(t, err)
// check that we can get the real path
locations, err := resolver.FilesByPath("./readme")
@@ -649,6 +707,26 @@ func Test_IndexingNestedSymLinksOutsideOfRoot(t *testing.T) {
locations, err = resolver.FilesByPath("./link_to_link_to_readme")
require.NoError(t, err)
assert.Len(t, locations, 1)
// something looks wrong here
t.Failed()
}
func Test_RootViaSymlink(t *testing.T) {
resolver, err := newDirectoryResolver("./test-fixtures/symlinked-root/nested/link-root")
require.NoError(t, err)
locations, err := resolver.FilesByPath("./file1.txt")
require.NoError(t, err)
assert.Len(t, locations, 1)
locations, err = resolver.FilesByPath("./nested/file2.txt")
require.NoError(t, err)
assert.Len(t, locations, 1)
locations, err = resolver.FilesByPath("./nested/linked-file1.txt")
require.NoError(t, err)
assert.Len(t, locations, 1)
}
func Test_directoryResolver_FileContentsByLocation(t *testing.T) {
+1
View File
@@ -63,6 +63,7 @@ func fileMetadataFromPath(path string, info os.FileInfo, withMIMEType bool) File
// unsupported across platforms
UserID: uid,
GroupID: gid,
Size: info.Size(),
MIMEType: mimeType,
}
}
+25 -1
View File
@@ -1,6 +1,7 @@
package source
import (
"archive/tar"
"fmt"
"io"
@@ -82,7 +83,7 @@ func (r *imageSquashResolver) FilesByGlob(patterns ...string) ([]Location, error
uniqueLocations := make([]Location, 0)
for _, pattern := range patterns {
results, err := r.img.SquashedTree().FilesByGlob(pattern)
results, err := r.img.SquashedTree().FilesByGlob(pattern, filetree.FollowBasenameLinks)
if err != nil {
return nil, fmt.Errorf("failed to resolve files by glob (%s): %w", pattern, err)
}
@@ -137,6 +138,29 @@ func (r *imageSquashResolver) RelativeFileByPath(_ Location, path string) *Locat
// FileContentsByLocation fetches file contents for a single file reference, irregardless of the source layer.
// If the path does not exist an error is returned.
func (r *imageSquashResolver) FileContentsByLocation(location Location) (io.ReadCloser, error) {
entry, err := r.img.FileCatalog.Get(location.ref)
if err != nil {
return nil, fmt.Errorf("unable to get metadata for path=%q from file catalog: %w", location.RealPath, err)
}
switch entry.Metadata.TypeFlag {
case tar.TypeSymlink, tar.TypeLink:
// the location we are searching may be a symlink, we should always work with the resolved file
locations, err := r.FilesByPath(location.RealPath)
if err != nil {
return nil, fmt.Errorf("failed to resolve content location at location=%+v: %w", location, err)
}
switch len(locations) {
case 0:
return nil, fmt.Errorf("link resolution failed while resolving content location: %+v", location)
case 1:
location = locations[0]
default:
return nil, fmt.Errorf("link resolution resulted in multiple results while resolving content location: %+v", location)
}
}
return r.img.FileContentsByRef(location.ref)
}
+210 -1
View File
@@ -1,6 +1,8 @@
package source
import (
"github.com/stretchr/testify/require"
"io"
"testing"
"github.com/scylladb/go-set/strset"
@@ -239,7 +241,7 @@ func Test_imageSquashResolver_FilesByMIMEType(t *testing.T) {
{
fixtureName: "image-simple",
mimeType: "text/plain",
expectedPaths: strset.New("/somefile-1.txt", "/somefile-2.txt", "/really/nested/file-3.txt", "/really/nested", "/really"),
expectedPaths: strset.New("/somefile-1.txt", "/somefile-2.txt", "/really/nested/file-3.txt"),
},
}
@@ -289,3 +291,210 @@ func Test_imageSquashResolver_hasFilesystemIDInLocation(t *testing.T) {
}
}
func TestSquashImageResolver_FilesContents(t *testing.T) {
tests := []struct {
name string
fixture string
contents []string
}{
{
name: "one degree",
fixture: "link-2",
contents: []string{
"NEW file override!", // always from the squashed perspective
},
},
{
name: "two degrees",
fixture: "link-indirect",
contents: []string{
"NEW file override!", // always from the squashed perspective
},
},
{
name: "dead link",
fixture: "link-dead",
contents: []string{},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
img := imagetest.GetFixtureImage(t, "docker-archive", "image-symlinks")
resolver, err := newImageSquashResolver(img)
assert.NoError(t, err)
refs, err := resolver.FilesByPath(test.fixture)
require.NoError(t, err)
assert.Len(t, refs, len(test.contents))
for idx, loc := range refs {
reader, err := resolver.FileContentsByLocation(loc)
require.NoError(t, err)
actual, err := io.ReadAll(reader)
require.NoError(t, err)
assert.Equal(t, test.contents[idx], string(actual))
}
})
}
}
func Test_imageSquashResolver_resolvesLinks(t *testing.T) {
tests := []struct {
name string
runner func(FileResolver) []Location
expected []Location
}{
{
name: "by mimetype",
runner: func(resolver FileResolver) []Location {
// links should not show up when searching mimetype
actualLocations, err := resolver.FilesByMIMEType("text/plain")
assert.NoError(t, err)
return actualLocations
},
expected: []Location{
{
Coordinates: Coordinates{
RealPath: "/etc/group",
},
VirtualPath: "/etc/group",
},
{
Coordinates: Coordinates{
RealPath: "/etc/passwd",
},
VirtualPath: "/etc/passwd",
},
{
Coordinates: Coordinates{
RealPath: "/etc/shadow",
},
VirtualPath: "/etc/shadow",
},
{
Coordinates: Coordinates{
RealPath: "/file-1.txt",
},
VirtualPath: "/file-1.txt",
},
{
Coordinates: Coordinates{
RealPath: "/file-3.txt",
},
VirtualPath: "/file-3.txt",
},
{
Coordinates: Coordinates{
RealPath: "/file-2.txt",
},
VirtualPath: "/file-2.txt",
},
{
Coordinates: Coordinates{
RealPath: "/parent/file-4.txt",
},
VirtualPath: "/parent/file-4.txt",
},
},
},
{
name: "by glob",
runner: func(resolver FileResolver) []Location {
// links are searched, but resolve to the real files
actualLocations, err := resolver.FilesByGlob("*ink-*")
assert.NoError(t, err)
return actualLocations
},
expected: []Location{
{
Coordinates: Coordinates{
RealPath: "/file-3.txt",
},
VirtualPath: "/link-within",
},
{
Coordinates: Coordinates{
RealPath: "/file-2.txt",
},
VirtualPath: "/link-2",
},
{
Coordinates: Coordinates{
RealPath: "/file-1.txt",
},
VirtualPath: "/link-1",
},
},
},
{
name: "by path to degree 1 link",
runner: func(resolver FileResolver) []Location {
// links resolve to the final file
actualLocations, err := resolver.FilesByPath("/link-2")
assert.NoError(t, err)
return actualLocations
},
expected: []Location{
// we have multiple copies across layers
{
Coordinates: Coordinates{
RealPath: "/file-2.txt",
},
VirtualPath: "/link-2",
},
},
},
{
name: "by path to degree 2 link",
runner: func(resolver FileResolver) []Location {
// multiple links resolves to the final file
actualLocations, err := resolver.FilesByPath("/link-indirect")
assert.NoError(t, err)
return actualLocations
},
expected: []Location{
// we have multiple copies across layers
{
Coordinates: Coordinates{
RealPath: "/file-2.txt",
},
VirtualPath: "/link-indirect",
},
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
img := imagetest.GetFixtureImage(t, "docker-archive", "image-symlinks")
resolver, err := newImageSquashResolver(img)
assert.NoError(t, err)
actualLocations := test.runner(resolver)
require.Len(t, actualLocations, len(test.expected))
// some operations on this resolver do not return stable results (order may be different across runs)
expectedMap := make(map[string]string)
for _, e := range test.expected {
expectedMap[e.VirtualPath] = e.RealPath
}
actualMap := make(map[string]string)
for _, a := range test.expected {
actualMap[a.VirtualPath] = a.RealPath
}
assert.Equal(t, expectedMap, actualMap)
})
}
}
+14
View File
@@ -78,6 +78,20 @@ func NewLocationFromDirectory(responsePath string, ref file.Reference) Location
}
}
// NewVirtualLocationFromDirectory creates a new Location representing the given path (extracted from the ref) relative to the given directory with a separate virtual access path.
func NewVirtualLocationFromDirectory(responsePath, virtualResponsePath string, ref file.Reference) Location {
if responsePath == virtualResponsePath {
return NewLocationFromDirectory(responsePath, ref)
}
return Location{
Coordinates: Coordinates{
RealPath: responsePath,
},
VirtualPath: virtualResponsePath,
ref: ref,
}
}
func (l Location) String() string {
str := ""
if l.ref.ID() != 0 {
+1 -1
View File
@@ -246,7 +246,7 @@ func (s *Source) FileResolver(scope Scope) (FileResolver, error) {
}
resolver, err := newDirectoryResolver(s.path, exclusionFunctions...)
if err != nil {
return nil, err
return nil, fmt.Errorf("unable to create directory resolver: %w", err)
}
s.directoryResolver = resolver
}
+34 -29
View File
@@ -47,55 +47,60 @@ func TestNewFromImage(t *testing.T) {
func TestNewFromDirectory(t *testing.T) {
testCases := []struct {
desc string
input string
expString string
inputPaths []string
expRefs int
desc string
input string
expString string
inputPaths []string
expectedRefs int
expectedErr bool
}{
{
desc: "no paths exist",
input: "foobar/",
inputPaths: []string{"/opt/", "/other"},
desc: "no paths exist",
input: "foobar/",
inputPaths: []string{"/opt/", "/other"},
expectedErr: true,
},
{
desc: "path detected",
input: "test-fixtures",
inputPaths: []string{"path-detected/.vimrc"},
expRefs: 1,
desc: "path detected",
input: "test-fixtures",
inputPaths: []string{"path-detected/.vimrc"},
expectedRefs: 1,
},
{
desc: "directory ignored",
input: "test-fixtures",
inputPaths: []string{"path-detected"},
expRefs: 0,
desc: "directory ignored",
input: "test-fixtures",
inputPaths: []string{"path-detected"},
expectedRefs: 0,
},
{
desc: "no files-by-path detected",
input: "test-fixtures",
inputPaths: []string{"no-path-detected"},
expRefs: 0,
desc: "no files-by-path detected",
input: "test-fixtures",
inputPaths: []string{"no-path-detected"},
expectedRefs: 0,
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
src, err := NewFromDirectory(test.input)
require.NoError(t, err)
assert.Equal(t, test.input, src.Metadata.Path)
if err != nil {
t.Errorf("could not create NewDirScope: %+v", err)
}
if src.Metadata.Path != test.input {
t.Errorf("mismatched stringer: '%s' != '%s'", src.Metadata.Path, test.input)
}
resolver, err := src.FileResolver(SquashedScope)
assert.NoError(t, err)
if test.expectedErr {
if err == nil {
t.Fatal("expected an error when making the resolver but got none")
}
return
} else {
require.NoError(t, err)
}
refs, err := resolver.FilesByPath(test.inputPaths...)
if err != nil {
t.Errorf("FilesByPath call produced an error: %+v", err)
}
if len(refs) != test.expRefs {
t.Errorf("unexpected number of refs returned: %d != %d", len(refs), test.expRefs)
if len(refs) != test.expectedRefs {
t.Errorf("unexpected number of refs returned: %d != %d", len(refs), test.expectedRefs)
}
@@ -3,6 +3,7 @@ FROM busybox:latest
# LAYER 1:
ADD file-1.txt .
# LAYER 2: link with previous data
RUN ln -s ./file-1.txt link-1
@@ -25,6 +26,7 @@ RUN ln -s ./i-dont-exist.txt link-dead
# LAYER 9: add the parent dir
ADD parent /parent
# LAYER 10: parent is a symlink
RUN ln -s /parent parent-link
@@ -0,0 +1 @@
../real-root
@@ -0,0 +1 @@
contents!
@@ -0,0 +1 @@
more contents!
@@ -0,0 +1 @@
../file1.txt