Skip to content

Commit

Permalink
feat: run action for OCI bundle
Browse files Browse the repository at this point in the history
As a first step toward run/shell/exec actions on native OCI images,
implement a minimal `singularity run --oci mybundle` which:

* Requires an on-disk bundle with appropriate `config.json`.
* Runs this bundle using `crun` or `runc`.
* Makes no attempt to handle any arguments or options.
* Does not modify the `config.json` - i.e. it must match namespace /
  mapping requirements for rootless execution etc.

At this stage, the functionality is essentially equivalent to
`singularity oci run` and is not yet useful.

The primary purpose of the PR is to refactor some of the code that
passes args for launching a container.

In addition, we now use `crun` in preference to `runc` if
available. `crun` supports e.g. single uid->uid mapping in a
usernamespace (without root mapping).

Closes sylabs#598
  • Loading branch information
dtrudg committed Oct 31, 2022
1 parent 7b5f599 commit 2dcdb8b
Show file tree
Hide file tree
Showing 15 changed files with 232 additions and 113 deletions.
2 changes: 1 addition & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ commands:
command: <<# parameters.sudo >>sudo <</ parameters.sudo >>apt-get -q update
- run:
name: Install dependencies
command: <<# parameters.sudo >>sudo <</ parameters.sudo >>apt-get -q install -y build-essential squashfs-tools libseccomp-dev libssl-dev uuid-dev cryptsetup-bin runc libglib2.0-dev squashfuse
command: <<# parameters.sudo >>sudo <</ parameters.sudo >>apt-get -q install -y build-essential squashfs-tools libseccomp-dev libssl-dev uuid-dev cryptsetup-bin crun libglib2.0-dev squashfuse
- run:
name: Install proot
command: |-
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
- When the kernel supports unprivileged overlay mounts in a user
namespace, the container will be constructed using an overlay
instead of underlay layout.
- `crun` will be used as the low-level OCI runtime, when available, rather than
`runc`. `runc` will not support all rootless OCI runtime functionality used by
Singularity.

### Development / Testing

Expand Down
55 changes: 40 additions & 15 deletions cmd/internal/cli/actions.go
Original file line number Diff line number Diff line change
Expand Up @@ -179,13 +179,21 @@ var ExecCmd = &cobra.Command{
Args: cobra.MinimumNArgs(2),
PreRun: actionPreRun,
Run: func(cmd *cobra.Command, args []string) {
a := append([]string{"/.singularity.d/actions/exec"}, args[1:]...)
// singularity exec <image> <command> [args...]
image := args[0]
containerCmd := "/.singularity.d/actions/exec"
containerArgs := args[1:]
// OCI runtime does not use an action script
if ociRuntime {
containerCmd = args[1]
containerArgs = args[2:]
}
setVM(cmd)
if vm {
execVM(cmd, args[0], a)
execVM(cmd, image, containerCmd, containerArgs)
return
}
if err := launchContainer(cmd, args[0], a, ""); err != nil {
if err := launchContainer(cmd, image, containerCmd, containerArgs, ""); err != nil {
sylog.Fatalf("%s", err)
}
},
Expand All @@ -203,13 +211,21 @@ var ShellCmd = &cobra.Command{
Args: cobra.MinimumNArgs(1),
PreRun: actionPreRun,
Run: func(cmd *cobra.Command, args []string) {
a := []string{"/.singularity.d/actions/shell"}
// singularity shell <image>
image := args[0]
containerCmd := "/.singularity.d/actions/shell"
containerArgs := []string{}
// OCI runtime does not use an action script
if ociRuntime {
// TODO - needs to have bash -> sh fallback logic implemented somewhere.
containerCmd = "/bin/sh"
}
setVM(cmd)
if vm {
execVM(cmd, args[0], a)
execVM(cmd, image, containerCmd, containerArgs)
return
}
if err := launchContainer(cmd, args[0], a, ""); err != nil {
if err := launchContainer(cmd, image, containerCmd, containerArgs, ""); err != nil {
sylog.Fatalf("%s", err)
}
},
Expand All @@ -227,13 +243,20 @@ var RunCmd = &cobra.Command{
Args: cobra.MinimumNArgs(1),
PreRun: actionPreRun,
Run: func(cmd *cobra.Command, args []string) {
a := append([]string{"/.singularity.d/actions/run"}, args[1:]...)
// singularity run <image> [args...]
image := args[0]
containerCmd := "/.singularity.d/actions/run"
containerArgs := args[1:]
// OCI runtime does not use an action script
if ociRuntime {
containerCmd = ""
}
setVM(cmd)
if vm {
execVM(cmd, args[0], a)
execVM(cmd, args[0], containerCmd, containerArgs)
return
}
if err := launchContainer(cmd, args[0], a, ""); err != nil {
if err := launchContainer(cmd, image, containerCmd, containerArgs, ""); err != nil {
sylog.Fatalf("%s", err)
}
},
Expand All @@ -251,13 +274,15 @@ var TestCmd = &cobra.Command{
Args: cobra.MinimumNArgs(1),
PreRun: actionPreRun,
Run: func(cmd *cobra.Command, args []string) {
a := append([]string{"/.singularity.d/actions/test"}, args[1:]...)
setVM(cmd)
// singularity test <image> [args...]
image := args[0]
containerCmd := "/.singularity.d/actions/test"
containerArgs := args[1:]
if vm {
execVM(cmd, args[0], a)
execVM(cmd, image, containerCmd, containerArgs)
return
}
if err := launchContainer(cmd, args[0], a, ""); err != nil {
if err := launchContainer(cmd, image, containerCmd, containerArgs, ""); err != nil {
sylog.Fatalf("%s", err)
}
},
Expand All @@ -268,7 +293,7 @@ var TestCmd = &cobra.Command{
Example: docs.RunTestExample,
}

func launchContainer(cmd *cobra.Command, image string, args []string, instanceName string) error {
func launchContainer(cmd *cobra.Command, image string, containerCmd string, containerArgs []string, instanceName string) error {
ns := launcher.Namespaces{
User: userNamespace,
UTS: utsNamespace,
Expand Down Expand Up @@ -350,5 +375,5 @@ func launchContainer(cmd *cobra.Command, image string, args []string, instanceNa
}
}

return l.Exec(cmd.Context(), image, args, instanceName)
return l.Exec(cmd.Context(), image, containerCmd, containerArgs, instanceName)
}
8 changes: 4 additions & 4 deletions cmd/internal/cli/instance_start_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,14 @@ var instanceStartCmd = &cobra.Command{
Run: func(cmd *cobra.Command, args []string) {
image := args[0]
name := args[1]

a := append([]string{"/.singularity.d/actions/start"}, args[2:]...)
containerCmd := "/.singularity.d/actions/start"
containerArgs := args[2:]
setVM(cmd)
if vm {
execVM(cmd, image, a)
execVM(cmd, image, containerCmd, containerArgs)
return
}
if err := launchContainer(cmd, image, a, name); err != nil {
if err := launchContainer(cmd, image, containerCmd, containerArgs, name); err != nil {
sylog.Fatalf("%s", err)
}

Expand Down
6 changes: 3 additions & 3 deletions cmd/internal/cli/startvm.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ func getHypervisorArgs(sifImage, bzImage, initramfs, singAction, cliExtra string
return args
}

func execVM(cmd *cobra.Command, image string, args []string) {
func execVM(cmd *cobra.Command, image string, containerCmd string, containerArgs []string) {
// SIF image we are running
sifImage := image

Expand All @@ -42,8 +42,8 @@ func execVM(cmd *cobra.Command, image string, args []string) {
isInternal = true
} else {
// Get our "action" (run, exec, shell) based on the action script being called
singAction = filepath.Base(args[0])
cliExtra = strings.Join(args[1:], " ")
singAction = filepath.Base(containerCmd)
cliExtra = strings.Join(containerArgs, " ")
}

if err := startVM(sifImage, singAction, cliExtra, isInternal); err != nil {
Expand Down
23 changes: 4 additions & 19 deletions e2e/actions/actions.go
Original file line number Diff line number Diff line change
Expand Up @@ -2406,24 +2406,6 @@ func countSquashfuseMounts(t *testing.T) int {
return count
}

func (c actionTests) ociRuntime(t *testing.T) {
e2e.EnsureImage(t, c.env)

for _, p := range []e2e.Profile{e2e.OCIUserProfile, e2e.OCIRootProfile} {
c.env.RunSingularity(
t,
e2e.AsSubtest(p.String()),
e2e.WithProfile(p),
e2e.WithCommand("exec"),
e2e.WithArgs(c.env.ImagePath, "/bin/true"),
e2e.ExpectExit(
255,
e2e.ExpectError(e2e.ContainMatch, "not implemented"),
),
)
}
}

// E2ETests is the main func to trigger the test suite
func E2ETests(env e2e.TestEnv) testhelper.Tests {
c := actionTests{
Expand Down Expand Up @@ -2466,9 +2448,12 @@ func E2ETests(env e2e.TestEnv) testhelper.Tests {
"umask": c.actionUmask, // test umask propagation
"no-mount": c.actionNoMount, // test --no-mount
"compat": c.actionCompat, // test --compat
"ociRuntime": c.ociRuntime, // test --oci (unimplemented)
"invalidRemote": np(c.invalidRemote), // GHSA-5mv9-q7fq-9394
"SIFFUSE": np(c.actionSIFFUSE), // test --sif-fuse
"NoSIFFUSE": np(c.actionNoSIFFUSE), // test absence of squashfs and CleanupHost()
//
// OCI Runtime Mode
//
"ociRun": c.actionOciRun, // singularity run --oci
}
}
78 changes: 78 additions & 0 deletions e2e/actions/oci.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// Copyright (c) 2022, Sylabs Inc. All rights reserved.
// This software is licensed under a 3-clause BSD license. Please consult the
// LICENSE.md file distributed with the sources of this project regarding your
// rights to use or distribute this software.

package actions

import (
"os"
"testing"

"github.com/pkg/errors"
"github.com/sylabs/singularity/e2e/internal/e2e"
"github.com/sylabs/singularity/internal/pkg/test/tool/require"
)

func (c actionTests) ociBundle(t *testing.T) (string, func()) {
require.Seccomp(t)
require.Filesystem(t, "overlay")

bundleDir, err := os.MkdirTemp(c.env.TestDir, "bundle-")
if err != nil {
err = errors.Wrapf(err, "creating temporary bundle directory at %q", c.env.TestDir)
t.Fatalf("failed to create bundle directory: %+v", err)
}
c.env.RunSingularity(
t,
e2e.WithProfile(e2e.RootProfile),
e2e.WithCommand("oci mount"),
e2e.WithArgs(c.env.ImagePath, bundleDir),
e2e.ExpectExit(0),
)

cleanup := func() {
c.env.RunSingularity(
t,
e2e.WithProfile(e2e.RootProfile),
e2e.WithCommand("oci umount"),
e2e.WithArgs(bundleDir),
e2e.ExpectExit(0),
)
os.RemoveAll(bundleDir)
}

return bundleDir, cleanup
}

func (c actionTests) actionOciRun(t *testing.T) {
e2e.EnsureImage(t, c.env)

bundle, cleanup := c.ociBundle(t)
defer cleanup()

tests := []struct {
name string
argv []string
exit int
}{
{
name: "NoCommand",
argv: []string{bundle},
exit: 0,
},
}

for _, tt := range tests {
c.env.RunSingularity(
t,
e2e.AsSubtest(tt.name),
e2e.WithProfile(e2e.OCIRootProfile),
e2e.WithCommand("run"),
// While we don't support args we are entering a /bin/sh interactively, so we need to exit.
e2e.ConsoleRun(e2e.ConsoleSendLine("exit")),
e2e.WithArgs(tt.argv...),
e2e.ExpectExit(tt.exit),
)
}
}
2 changes: 1 addition & 1 deletion internal/pkg/runtime/launcher/launcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,5 @@ type Launcher interface {
// the container#s initial process. If instanceName is specified, the
// container must be launched as a background instance, otherwist it must
// run interactively, attached to the console.
Exec(ctx context.Context, image string, args []string, instanceName string) error
Exec(ctx context.Context, image string, cmd string, args []string, instanceName string) error
}
5 changes: 4 additions & 1 deletion internal/pkg/runtime/launcher/native/launcher_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,9 +93,12 @@ func NewLauncher(opts ...launcher.Option) (*Launcher, error) {
// This includes interactive containers, instances, and joining an existing instance.
//
//nolint:maintidx
func (l *Launcher) Exec(ctx context.Context, image string, args []string, instanceName string) error {
func (l *Launcher) Exec(ctx context.Context, image string, cmd string, args []string, instanceName string) error {
var err error

// Native runtime expects command to execute as arg[0]
args = append([]string{cmd}, args...)

// Set arguments to pass to contained process.
l.generator.SetProcessArgs(args)

Expand Down
23 changes: 20 additions & 3 deletions internal/pkg/runtime/launcher/oci/launcher_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"fmt"
"strings"

"github.com/google/uuid"
"github.com/sylabs/singularity/internal/pkg/buildcfg"
"github.com/sylabs/singularity/internal/pkg/runtime/launcher"
)
Expand Down Expand Up @@ -232,7 +233,23 @@ func checkOpts(lo launcher.Options) error {
return nil
}

// Exec is not yet implemented.
func (l *Launcher) Exec(ctx context.Context, image string, args []string, instanceName string) error {
return ErrNotImplemented
// Exec will interactively execute a container via the runc low-level runtime.
func (l *Launcher) Exec(ctx context.Context, image string, cmd string, args []string, instanceName string) error {
if instanceName != "" {
return fmt.Errorf("%w: instanceName", ErrNotImplemented)
}

if cmd != "" {
return fmt.Errorf("%w: cmd %v", ErrNotImplemented, cmd)
}

if len(args) > 0 {
return fmt.Errorf("%w: args %v", ErrNotImplemented, args)
}

id, err := uuid.NewRandom()
if err != nil {
return fmt.Errorf("while generating container id: %w", err)
}
return Run(ctx, id.String(), image, "")
}
12 changes: 0 additions & 12 deletions internal/pkg/runtime/launcher/oci/launcher_linux_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
package oci

import (
"context"
"reflect"
"testing"

Expand Down Expand Up @@ -54,14 +53,3 @@ func TestNewLauncher(t *testing.T) {
})
}
}

func TestExec(t *testing.T) {
l, err := NewLauncher([]launcher.Option{}...)
if err != nil {
t.Errorf("Couldn't initialize launcher: %s", err)
}

if err := l.Exec(context.Background(), "", []string{}, ""); err != ErrNotImplemented {
t.Errorf("Expected %v, got %v", ErrNotImplemented, err)
}
}
6 changes: 3 additions & 3 deletions internal/pkg/runtime/launcher/oci/oci_conmon_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ func Create(containerID, bundlePath string) error {
if err != nil {
return err
}
runc, err := bin.FindBin("runc")
runtimeBin, err := runtime()
if err != nil {
return err
}
Expand Down Expand Up @@ -92,12 +92,12 @@ func Create(containerID, bundlePath string) error {
"--cid", containerID,
"--name", containerID,
"--cuuid", containerUUID.String(),
"--runtime", runc,
"--runtime", runtimeBin,
"--conmon-pidfile", path.Join(sd, conmonPidFile),
"--container-pidfile", path.Join(sd, containerPidFile),
"--log-path", path.Join(sd, containerLogFile),
"--runtime-arg", "--root",
"--runtime-arg", runcStateDir,
"--runtime-arg", runtimeStateDir(),
"--runtime-arg", "--log",
"--runtime-arg", path.Join(sd, runcLogFile),
"--full-attach",
Expand Down
Loading

0 comments on commit 2dcdb8b

Please sign in to comment.