Skip to content

Commit

Permalink
Merge pull request #195 from tri-adam/mount
Browse files Browse the repository at this point in the history
Experimental Mount Command
  • Loading branch information
tri-adam committed Apr 5, 2022
2 parents d7a38b5 + 389f08f commit 6346972
Show file tree
Hide file tree
Showing 24 changed files with 325 additions and 26 deletions.
14 changes: 12 additions & 2 deletions cmd/siftool/siftool.go
@@ -1,4 +1,4 @@
// Copyright (c) 2018-2021, Sylabs Inc. All rights reserved.
// Copyright (c) 2018-2022, Sylabs Inc. All rights reserved.
// Copyright (c) 2017, SingularityWare, LLC. All rights reserved.
// Copyright (c) 2017, Yannick Cote <yhcote@gmail.com> All rights reserved.
// This software is licensed under a 3-clause BSD license. Please consult the
Expand All @@ -12,6 +12,7 @@ import (
"io"
"os"
"runtime"
"strconv"
"text/tabwriter"

"github.com/spf13/cobra"
Expand Down Expand Up @@ -79,7 +80,16 @@ possible to modify a SIF file via this tool via the add/del commands.`,

root.AddCommand(getVersion())

if err := siftool.AddCommands(&root); err != nil {
var experimental bool
if val, ok := os.LookupEnv("SIFTOOL_EXPERIMENTAL"); ok {
b, err := strconv.ParseBool(val)
if err != nil {
fmt.Fprintln(os.Stderr, "Error: failed to parse SIFTOOL_EXPERIMENTAL environment variable:", err)
}
experimental = b
}

if err := siftool.AddCommands(&root, siftool.OptWithExperimental(experimental)); err != nil {
fmt.Fprintln(os.Stderr, "Error:", err)
os.Exit(1)
}
Expand Down
15 changes: 14 additions & 1 deletion internal/app/siftool/app.go
@@ -1,4 +1,4 @@
// Copyright (c) 2021, Sylabs Inc. All rights reserved.
// Copyright (c) 2021-2022, Sylabs Inc. All rights reserved.
// This software is licensed under a 3-clause BSD license. Please consult the
// LICENSE file distributed with the sources of this project regarding your
// rights to use or distribute this software.
Expand All @@ -13,6 +13,7 @@ import (
// appOpts contains configured options.
type appOpts struct {
out io.Writer
err io.Writer
}

// AppOpt are used to configure optional behavior.
Expand All @@ -31,11 +32,23 @@ func OptAppOutput(w io.Writer) AppOpt {
}
}

// OptAppError specifies that errors should be written to w.
func OptAppError(w io.Writer) AppOpt {
return func(o *appOpts) error {
o.err = w
return nil
}
}

// New creates a new App configured with opts.
//
// By default, application output and errors are written to os.Stdout and os.Stderr respectively.
// To modify this behavior, consider using OptAppOutput and/or OptAppError.
func New(opts ...AppOpt) (*App, error) {
a := App{
opts: appOpts{
out: os.Stdout,
err: os.Stderr,
},
}

Expand Down
20 changes: 20 additions & 0 deletions internal/app/siftool/mount.go
@@ -0,0 +1,20 @@
// Copyright (c) 2022, Sylabs Inc. All rights reserved.
// This software is licensed under a 3-clause BSD license. Please consult the
// LICENSE file distributed with the sources of this project regarding your
// rights to use or distribute this software.

package siftool

import (
"context"

"github.com/sylabs/sif/v2/internal/pkg/exp"
)

// Mount mounts the primary system partition of the SIF file at path into mountPath.
func (a *App) Mount(ctx context.Context, path, mountPath string) error {
return exp.Mount(ctx, path, mountPath,
exp.OptMountStdout(a.opts.out),
exp.OptMountStderr(a.opts.err),
)
}
104 changes: 104 additions & 0 deletions internal/pkg/exp/mount.go
@@ -0,0 +1,104 @@
// Copyright (c) 2022, Sylabs Inc. All rights reserved.
// This software is licensed under a 3-clause BSD license. Please consult the
// LICENSE file distributed with the sources of this project regarding your
// rights to use or distribute this software.

// Package exp contains experimental functionality that is not sufficiently mature to be exported
// as part of the module API.
package exp

import (
"context"
"errors"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"

"github.com/sylabs/sif/v2/pkg/sif"
)

// mountSquashFS mounts the SquashFS filesystem from path at offset into mountPath.
func mountSquashFS(ctx context.Context, offset int64, path, mountPath string, mo mountOpts) error {
args := []string{
"-o", fmt.Sprintf("ro,offset=%d", offset),
filepath.Clean(path),
filepath.Clean(mountPath),
}

cmd := exec.CommandContext(ctx, "squashfuse", args...)
cmd.Stdout = mo.stdout
cmd.Stderr = mo.stderr

if err := cmd.Run(); err != nil {
return fmt.Errorf("failed to mount: %w", err)
}

return nil
}

// mountOpts accumulates mount options.
type mountOpts struct {
stdout io.Writer
stderr io.Writer
}

// MountOpt are used to specify mount options.
type MountOpt func(*mountOpts) error

// OptMountStdout writes standard output to w.
func OptMountStdout(w io.Writer) MountOpt {
return func(mo *mountOpts) error {
mo.stdout = w
return nil
}
}

// OptMountStderr writes standard error to w.
func OptMountStderr(w io.Writer) MountOpt {
return func(mo *mountOpts) error {
mo.stderr = w
return nil
}
}

var errUnsupportedFSType = errors.New("unrecognized filesystem type")

// Mount mounts the primary system partition of the SIF file at path into mountPath.
//
// Mount may start one or more underlying processes. By default, stdout and stderr of these
// processes is discarded. To modify this behavior, consider using OptMountStdout and/or
// OptMountStderr.
func Mount(ctx context.Context, path, mountPath string, opts ...MountOpt) error {
mo := mountOpts{}

for _, opt := range opts {
if err := opt(&mo); err != nil {
return fmt.Errorf("%w", err)
}
}

f, err := sif.LoadContainerFromPath(path, sif.OptLoadWithFlag(os.O_RDONLY))
if err != nil {
return fmt.Errorf("failed to load image: %w", err)
}
defer func() { _ = f.UnloadContainer() }()

d, err := f.GetDescriptor(sif.WithPartitionType(sif.PartPrimSys))
if err != nil {
return fmt.Errorf("failed to get partition descriptor: %w", err)
}

fs, _, _, err := d.PartitionMetadata()
if err != nil {
return fmt.Errorf("failed to get partition metadata: %w", err)
}

switch fs {
case sif.FsSquash:
return mountSquashFS(ctx, d.Offset(), path, mountPath, mo)
default:
return errUnsupportedFSType
}
}
4 changes: 2 additions & 2 deletions pkg/siftool/add_test.go
@@ -1,4 +1,4 @@
// Copyright (c) 2021, Sylabs Inc. All rights reserved.
// Copyright (c) 2021-2022, Sylabs Inc. All rights reserved.
// This software is licensed under a 3-clause BSD license. Please consult the
// LICENSE file distributed with the sources of this project regarding your
// rights to use or distribute this software.
Expand Down Expand Up @@ -46,7 +46,7 @@ func Test_command_getAdd(t *testing.T) {
}
args = append(args, tt.flags...)

runCommand(t, cmd, args)
runCommand(t, cmd, args, nil)
})
}
}
4 changes: 2 additions & 2 deletions pkg/siftool/del_test.go
@@ -1,4 +1,4 @@
// Copyright (c) 2021, Sylabs Inc. All rights reserved.
// Copyright (c) 2021-2022, Sylabs Inc. All rights reserved.
// This software is licensed under a 3-clause BSD license. Please consult the
// LICENSE file distributed with the sources of this project regarding your
// rights to use or distribute this software.
Expand All @@ -24,7 +24,7 @@ func Test_command_getDel(t *testing.T) {

cmd := c.getDel()

runCommand(t, cmd, []string{"1", makeTestSIF(t, true)})
runCommand(t, cmd, []string{"1", makeTestSIF(t, true)}, nil)
})
}
}
4 changes: 2 additions & 2 deletions pkg/siftool/dump_test.go
@@ -1,4 +1,4 @@
// Copyright (c) 2021, Sylabs Inc. All rights reserved.
// Copyright (c) 2021-2022, Sylabs Inc. All rights reserved.
// This software is licensed under a 3-clause BSD license. Please consult the
// LICENSE file distributed with the sources of this project regarding your
// rights to use or distribute this software.
Expand Down Expand Up @@ -39,7 +39,7 @@ func Test_command_getDump(t *testing.T) {

cmd := c.getDump()

runCommand(t, cmd, []string{tt.id, tt.path})
runCommand(t, cmd, []string{tt.id, tt.path}, nil)
})
}
}
4 changes: 2 additions & 2 deletions pkg/siftool/header_test.go
@@ -1,4 +1,4 @@
// Copyright (c) 2021, Sylabs Inc. All rights reserved.
// Copyright (c) 2021-2022, Sylabs Inc. All rights reserved.
// This software is licensed under a 3-clause BSD license. Please consult the
// LICENSE file distributed with the sources of this project regarding your
// rights to use or distribute this software.
Expand Down Expand Up @@ -68,7 +68,7 @@ func Test_command_getHeader(t *testing.T) {

cmd := c.getHeader()

runCommand(t, cmd, []string{tt.path})
runCommand(t, cmd, []string{tt.path}, nil)
})
}
}
4 changes: 2 additions & 2 deletions pkg/siftool/info_test.go
@@ -1,4 +1,4 @@
// Copyright (c) 2021, Sylabs Inc. All rights reserved.
// Copyright (c) 2021-2022, Sylabs Inc. All rights reserved.
// This software is licensed under a 3-clause BSD license. Please consult the
// LICENSE file distributed with the sources of this project regarding your
// rights to use or distribute this software.
Expand Down Expand Up @@ -39,7 +39,7 @@ func Test_command_getInfo(t *testing.T) {

cmd := c.getInfo()

runCommand(t, cmd, []string{tt.id, tt.path})
runCommand(t, cmd, []string{tt.id, tt.path}, nil)
})
}
}
4 changes: 2 additions & 2 deletions pkg/siftool/list_test.go
@@ -1,4 +1,4 @@
// Copyright (c) 2021, Sylabs Inc. All rights reserved.
// Copyright (c) 2021-2022, Sylabs Inc. All rights reserved.
// This software is licensed under a 3-clause BSD license. Please consult the
// LICENSE file distributed with the sources of this project regarding your
// rights to use or distribute this software.
Expand Down Expand Up @@ -68,7 +68,7 @@ func Test_command_getList(t *testing.T) {

cmd := c.getList()

runCommand(t, cmd, []string{tt.path})
runCommand(t, cmd, []string{tt.path}, nil)
})
}
}
27 changes: 27 additions & 0 deletions pkg/siftool/mount.go
@@ -0,0 +1,27 @@
// Copyright (c) 2022, Sylabs Inc. All rights reserved.
// This software is licensed under a 3-clause BSD license. Please consult the
// LICENSE file distributed with the sources of this project regarding your
// rights to use or distribute this software.

package siftool

import (
"github.com/spf13/cobra"
)

// getMount returns a command that mounts the primary system partition of a SIF image.
func (c *command) getMount() *cobra.Command {
return &cobra.Command{
Use: "mount <sif_path> <mount_path>",
Short: "Mount primary system partition",
Long: "Mount the primary system partition of a SIF image",
Example: c.opts.rootPath + " mount image.sif path/",
Args: cobra.ExactArgs(2),
PreRunE: c.initApp,
RunE: func(cmd *cobra.Command, args []string) error {
return c.app.Mount(cmd.Context(), args[0], args[1])
},
DisableFlagsInUseLine: true,
Hidden: true, // hide while command is experimental
}
}
61 changes: 61 additions & 0 deletions pkg/siftool/mount_test.go
@@ -0,0 +1,61 @@
// Copyright (c) 2022, Sylabs Inc. All rights reserved.
// This software is licensed under a 3-clause BSD license. Please consult the
// LICENSE file distributed with the sources of this project regarding your
// rights to use or distribute this software.

package siftool

import (
"os"
"os/exec"
"path/filepath"
"testing"

"github.com/sylabs/sif/v2/pkg/sif"
)

func Test_command_getMount(t *testing.T) {
if _, err := exec.LookPath("squashfuse"); err != nil {
t.Skip("squashfuse not found, skipping mount tests")
}

tests := []struct {
name string
opts commandOpts
path string
wantErr error
}{
{
name: "Empty",
path: filepath.Join(corpus, "empty.sif"),
wantErr: sif.ErrNoObjects,
},
{
name: "OneGroup",
path: filepath.Join(corpus, "one-group.sif"),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
path, err := os.MkdirTemp("", "siftool-mount-*")
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
cmd := exec.Command("fusermount", "-u", path)

if err := cmd.Run(); err != nil {
t.Log(err)
}

os.RemoveAll(path)
})

c := &command{opts: tt.opts}

cmd := c.getMount()

runCommand(t, cmd, []string{tt.path, path}, tt.wantErr)
})
}
}
4 changes: 2 additions & 2 deletions pkg/siftool/new_test.go
@@ -1,4 +1,4 @@
// Copyright (c) 2021, Sylabs Inc. All rights reserved.
// Copyright (c) 2021-2022, Sylabs Inc. All rights reserved.
// This software is licensed under a 3-clause BSD license. Please consult the
// LICENSE file distributed with the sources of this project regarding your
// rights to use or distribute this software.
Expand Down Expand Up @@ -32,7 +32,7 @@ func Test_command_getNew(t *testing.T) {

cmd := c.getNew()

runCommand(t, cmd, []string{tf.Name()})
runCommand(t, cmd, []string{tf.Name()}, nil)
})
}
}

0 comments on commit 6346972

Please sign in to comment.