Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Experimental Mount Command #195

Merged
merged 4 commits into from Apr 5, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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)
})
}
}