Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 13 additions & 5 deletions pkg/image/source.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"github.com/google/go-containerregistry/pkg/name"
"github.com/mitchellh/go-homedir"
"github.com/spf13/afero"
"github.com/sylabs/sif/v2/pkg/sif"
)

const (
Expand Down Expand Up @@ -198,12 +199,19 @@ func detectSourceFromPath(fs afero.Fs, imgPath string) (Source, error) {
return UnknownSource, nil
}

// assume this is an archive...
archive, err := fs.Open(imgPath)
f, err := fs.Open(imgPath)
if err != nil {
return UnknownSource, fmt.Errorf("unable to open archive=%s: %w", imgPath, err)
return UnknownSource, fmt.Errorf("unable to open file=%s: %w", imgPath, err)
}
defer f.Close()

// Check for Singularity container.
fi, err := sif.LoadContainer(f, sif.OptLoadWithCloseOnUnload(false))
if err == nil {
return SingularitySource, fi.UnloadContainer()
}

// assume this is an archive...
for _, pair := range []struct {
path string
source Source
Expand All @@ -217,12 +225,12 @@ func detectSourceFromPath(fs afero.Fs, imgPath string) (Source, error) {
OciTarballSource,
},
} {
if _, err = archive.Seek(0, io.SeekStart); err != nil {
if _, err = f.Seek(0, io.SeekStart); err != nil {
return UnknownSource, fmt.Errorf("unable to seek archive=%s: %w", imgPath, err)
}

var fileErr *file.ErrFileNotFound
_, err = file.ReaderFromTar(archive, pair.path)
_, err = file.ReaderFromTar(f, pair.path)
if err == nil {
return pair.source, nil
} else if !errors.As(err, &fileErr) {
Expand Down
213 changes: 123 additions & 90 deletions pkg/image/source_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,16 @@ import (

"github.com/mitchellh/go-homedir"
"github.com/spf13/afero"
"github.com/sylabs/sif/v2/pkg/sif"
)

func TestDetectSource(t *testing.T) {
cases := []struct {
name string
getFS func(*testing.T, afero.Fs)
input string
source Source
expectedLocation string
tarPath string
tarPaths []string
}{
{
name: "podman-engine",
Expand Down Expand Up @@ -104,57 +104,67 @@ func TestDetectSource(t *testing.T) {
},
{
name: "oci-tar-path",
getFS: getDummyTar("a-potential/path", "oci-layout"),
input: "a-potential/path",
source: OciTarballSource,
expectedLocation: "a-potential/path",
tarPath: "a-potential/path",
tarPaths: []string{"oci-layout"},
},
{
name: "unparsable-existing-path",
getFS: getDummyTar("a-potential/path"),
input: "a-potential/path",
source: UnknownSource,
expectedLocation: "",
tarPath: "a-potential/path",
tarPaths: []string{},
},
// honor tilde expansion
{
name: "oci-tar-path",
getFS: getDummyTar("~/a-potential/path", "oci-layout"),
input: "~/a-potential/path",
source: OciTarballSource,
expectedLocation: "~/a-potential/path",
tarPath: "~/a-potential/path",
tarPaths: []string{"oci-layout"},
},
{
name: "oci-tar-path-explicit",
getFS: getDummyTar("~/a-potential/path", "oci-layout"),
input: "oci-archive:~/a-potential/path",
source: OciTarballSource,
expectedLocation: "~/a-potential/path",
tarPath: "~/a-potential/path",
tarPaths: []string{"oci-layout"},
},
{
name: "oci-tar-path-with-scheme-separator",
getFS: getDummyTar("a-potential/path:version", "oci-layout"),
input: "a-potential/path:version",
source: OciTarballSource,
expectedLocation: "a-potential/path:version",
tarPath: "a-potential/path:version",
tarPaths: []string{"oci-layout"},
},
{
name: "singularity-path",
input: "singularity:~/path/to/file.sif",
getFS: getDummySIF("~/a-potential/path.sif"),
input: "singularity:~/a-potential/path.sif",
source: SingularitySource,
expectedLocation: "~/path/to/file.sif",
expectedLocation: "~/a-potential/path.sif",
},
{
name: "singularity-path-tilde",
getFS: getDummySIF("~/a-potential/path.sif"),
input: "~/a-potential/path.sif",
source: SingularitySource,
expectedLocation: "~/a-potential/path.sif",
},
{
name: "singularity-path-explicit",
getFS: getDummySIF("~/a-potential/path.sif"),
input: "singularity:~/a-potential/path.sif",
source: SingularitySource,
expectedLocation: "~/a-potential/path.sif",
},
Comment thread
kzantow marked this conversation as resolved.
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
fs := afero.NewMemMapFs()
if c.tarPath != "" {
getDummyTar(t, fs.(*afero.MemMapFs), c.tarPath, c.tarPaths...)
if c.getFS != nil {
c.getFS(t, fs)
}

source, location, err := detectSource(fs, c.input)
Expand Down Expand Up @@ -282,82 +292,81 @@ func TestParseScheme(t *testing.T) {
func TestDetectSourceFromPath(t *testing.T) {
tests := []struct {
name string
paths []string
path string
getFS func(*testing.T, afero.Fs)
expectedSource Source
sourceType string
expectedErr bool
}{
{
name: "no tar paths",
paths: []string{},
sourceType: "tar",
path: "image.tar",
getFS: getDummyTar("image.tar"),
Comment thread
tri-adam marked this conversation as resolved.
Outdated
expectedSource: UnknownSource,
},
{
name: "dummy tar paths",
paths: []string{"manifest", "index", "oci_layout"},
sourceType: "tar",
path: "image.tar",
getFS: getDummyTar("image.tar", "manifest", "index", "oci_layout"),
expectedSource: UnknownSource,
},
{
name: "oci-layout tar path",
paths: []string{"oci-layout"},
sourceType: "tar",
path: "image.tar",
getFS: getDummyTar("image.tar", "oci-layout"),
expectedSource: OciTarballSource,
},
{
name: "index.json tar path",
paths: []string{"index.json"}, // this is an optional OCI file...
sourceType: "tar",
expectedSource: UnknownSource, // ...which we should not respond to as primary evidence
path: "image.tar",
getFS: getDummyTar("image.tar", "index.json"), // this is an optional OCI file...
expectedSource: UnknownSource, // ...which we should not respond to as primary evidence
},
{
name: "docker tar path",
paths: []string{"manifest.json"},
sourceType: "tar",
path: "image.tar",
getFS: getDummyTar("image.tar", "manifest.json"),
expectedSource: DockerTarballSource,
},
{
name: "no dir paths",
paths: []string{},
sourceType: "dir",
path: "image",
getFS: getDummyDir("image"),
expectedSource: UnknownSource,
},
{
name: "oci-layout path",
paths: []string{"oci-layout"},
sourceType: "dir",
path: "image",
getFS: getDummyDir("image", "oci-layout"),
expectedSource: OciDirectorySource,
},
{
name: "dummy dir paths",
paths: []string{"manifest", "index", "oci_layout"},
sourceType: "dir",
path: "image",
getFS: getDummyDir("image", "manifest", "index", "oci_layout"),
expectedSource: UnknownSource,
},
{
name: "no path given",
sourceType: "none",
path: "/does-not-exist",
expectedSource: UnknownSource,
expectedErr: false,
},
{
name: "singularity-path",
path: "image.sif",
getFS: getDummySIF("image.sif"),
expectedSource: SingularitySource,
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
fs := afero.NewMemMapFs()
Comment thread
tri-adam marked this conversation as resolved.
Outdated
var testPath string
switch test.sourceType {
case "tar":
testPath = getDummyTar(t, fs.(*afero.MemMapFs), "image.tar", test.paths...)
case "dir":
testPath = getDummyPath(t, fs.(*afero.MemMapFs), "image", test.paths...)
case "none":
testPath = "/does-not-exist"
default:
t.Fatalf("unknown source type: %+v", test.sourceType)
if test.getFS != nil {
test.getFS(t, fs)
}
actual, err := detectSourceFromPath(fs, testPath)

actual, err := detectSourceFromPath(fs, test.path)
if err != nil && !test.expectedErr {
t.Fatalf("unexpected error: %+v", err)
} else if err == nil && test.expectedErr {
Expand All @@ -370,70 +379,94 @@ func TestDetectSourceFromPath(t *testing.T) {
}
}

// note: we do not pass the afero.Fs interface since we are writing out to the root of the filesystem, something we never want to do with an OS filesystem. This type is more explicit.
func getDummyTar(t *testing.T, fs *afero.MemMapFs, archivePath string, paths ...string) string {
t.Helper()
// getDummyTar returns a function that adds a TAR archive at archivePath populated with paths to fs.
func getDummyTar(archivePath string, paths ...string) func(*testing.T, afero.Fs) {
return func(t *testing.T, fs afero.Fs) {
t.Helper()

archivePath, err := homedir.Expand(archivePath)
if err != nil {
t.Fatalf("unable to expand home path=%q: %+v", archivePath, err)
}
archivePath, err := homedir.Expand(archivePath)
if err != nil {
t.Fatalf("unable to expand home path=%q: %+v", archivePath, err)
}

testFile, err := fs.Create(archivePath)
if err != nil {
t.Fatalf("failed to create dummy tar: %+v", err)
}
testFile, err := fs.Create(archivePath)
if err != nil {
t.Fatalf("failed to create dummy tar: %+v", err)
}

tarWriter := tar.NewWriter(testFile)
defer tarWriter.Close()
tarWriter := tar.NewWriter(testFile)
defer tarWriter.Close()

for _, filePath := range paths {
header := &tar.Header{
Name: filePath,
Size: 13,
}

err = tarWriter.WriteHeader(header)
if err != nil {
t.Fatalf("could not write dummy header: %+v", err)
}

for _, filePath := range paths {
header := &tar.Header{
Name: filePath,
Size: 13,
_, err = io.Copy(tarWriter, strings.NewReader("hello, world!"))
if err != nil {
t.Fatalf("could not write dummy file: %+v", err)
}
}
}
}

err = tarWriter.WriteHeader(header)
// getDummyDir returns a function that adds a directory dirPath populated with paths to fs.
func getDummyDir(dirPath string, paths ...string) func(*testing.T, afero.Fs) {
return func(t *testing.T, fs afero.Fs) {
t.Helper()

dirPath, err := homedir.Expand(dirPath)
if err != nil {
t.Fatalf("could not write dummy header: %+v", err)
t.Fatalf("unable to expand home dir=%q: %+v", dirPath, err)
}

_, err = io.Copy(tarWriter, strings.NewReader("hello, world!"))
if err != nil {
t.Fatalf("could not write dummy file: %+v", err)
if err = fs.Mkdir(dirPath, os.ModePerm); err != nil {
t.Fatalf("failed to create dummy tar: %+v", err)
}
}

return archivePath
}
for _, filePath := range paths {
f, err := fs.Create(path.Join(dirPath, filePath))
if err != nil {
t.Fatalf("unable to create file: %+v", err)
}

// note: we do not pass the afero.Fs interface since we are writing out to the root of the filesystem, something we never want to do with an OS filesystem. This type is more explicit.
func getDummyPath(t *testing.T, fs *afero.MemMapFs, dirPath string, paths ...string) string {
t.Helper()
if _, err = f.WriteString("hello, world!"); err != nil {
t.Fatalf("unable to write file")
}

dirPath, err := homedir.Expand(dirPath)
if err != nil {
t.Fatalf("unable to expand home dir=%q: %+v", dirPath, err)
if err = f.Close(); err != nil {
t.Fatalf("unable to close file")
}
}
}
}

if err = fs.Mkdir(dirPath, os.ModePerm); err != nil {
t.Fatalf("failed to create dummy tar: %+v", err)
}
// getDummySIF returns a function that adds a SIF at path to fs.
func getDummySIF(path string, opts ...sif.CreateOpt) func(*testing.T, afero.Fs) {
return func(t *testing.T, fs afero.Fs) {
t.Helper()

for _, filePath := range paths {
f, err := fs.Create(path.Join(dirPath, filePath))
path, err := homedir.Expand(path)
if err != nil {
t.Fatalf("unable to create file: %+v", err)
t.Fatalf("unable to expand home dir=%q: %+v", path, err)
}

if _, err = f.WriteString("hello, world!"); err != nil {
t.Fatalf("unable to write file")
f, err := fs.Create(path)
if err != nil {
t.Fatalf("failed to create file: %+v", err)
}
defer f.Close()

if err = f.Close(); err != nil {
t.Fatalf("unable to close file")
fi, err := sif.CreateContainer(f, opts...)
if err != nil {
t.Fatalf("failed to create container: %+v", err)
}
defer fi.UnloadContainer()
}

return dirPath
}