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/singularity#598

Signed-off-by: Edita Kizinevic <[email protected]>
  • Loading branch information
dtrudg authored and edytuk committed May 24, 2023
1 parent 7b7f594 commit 7fe09bc
Show file tree
Hide file tree
Showing 16 changed files with 240 additions and 117 deletions.
6 changes: 3 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,7 @@ jobs:
go-version: 1.19.5

- name: Fetch deps
run: sudo apt-get -q update && sudo DEBIAN_FRONTEND=noninteractive apt-get install -y build-essential squashfs-tools squashfuse fuse-overlayfs fakeroot fuse2fs libseccomp-dev cryptsetup dbus-user-session conmon runc
run: sudo apt-get -q update && sudo DEBIAN_FRONTEND=noninteractive apt-get install -y build-essential squashfs-tools squashfuse fuse-overlayfs fakeroot fuse2fs libseccomp-dev cryptsetup dbus-user-session conmon crun

- name: Build and install Apptainer
run: |
Expand Down Expand Up @@ -271,7 +271,7 @@ jobs:
go-version: 1.19.5

- name: Fetch deps
run: sudo apt-get -q update && sudo DEBIAN_FRONTEND=noninteractive apt-get install -y build-essential squashfs-tools libseccomp-dev cryptsetup dbus-user-session conmon runc
run: sudo apt-get -q update && sudo DEBIAN_FRONTEND=noninteractive apt-get install -y build-essential squashfs-tools libseccomp-dev cryptsetup dbus-user-session conmon crun

- name: Build and install Apptainer
run: |
Expand Down Expand Up @@ -317,7 +317,7 @@ jobs:

- name: Fetch deps
if: env.run_tests
run: sudo apt-get -q update && sudo DEBIAN_FRONTEND=noninteractive apt-get install -y build-essential uidmap squashfs-tools squashfuse fuse-overlayfs fakeroot fuse2fs libseccomp-dev cryptsetup dbus-user-session conmon runc
run: sudo apt-get -q update && sudo DEBIAN_FRONTEND=noninteractive apt-get install -y build-essential uidmap squashfs-tools squashfuse fuse-overlayfs fakeroot fuse2fs libseccomp-dev cryptsetup dbus-user-session conmon crun

- name: Fetch gocryptfs
run: wget -O gocryptfs.tar.gz https://github.com/rfjakob/gocryptfs/releases/download/v2.3/gocryptfs_v2.3_linux-static_amd64.tar.gz && sudo tar xzvf gocryptfs.tar.gz -C /usr/local/bin gocryptfs
Expand Down
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ For older changes see the [archived Singularity change log](https://github.com/a
installations. This is an increase from 16 MiB in prior versions.
- Show standard output of yum bootstrap if log level is verbose or higher.
- Add architecture aware support for apptainer cache.
- The `apptainer oci` command group now uses `runc` to manage containers.
- The `apptainer oci` command group now uses `crun`, when available, or otherwise
`runc` to manage containers.
- The `apptainer oci` flags `--sync-socket`, `--empty-process`, and
`--timeout` have been removed.

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 @@ -195,13 +195,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:]...)
// apptainer 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 @@ -223,13 +231,21 @@ var ShellCmd = &cobra.Command{
sylog.Warningf("Parameters to shell command are ignored")
}

a := []string{"/.singularity.d/actions/shell"}
// apptainer 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 @@ -247,13 +263,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:]...)
// apptainer 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 @@ -271,13 +294,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)
// apptainer 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 @@ -288,7 +313,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 @@ -380,5 +405,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)
}
5 changes: 3 additions & 2 deletions cmd/internal/cli/checkpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -166,8 +166,9 @@ var CheckpointInstanceCmd = &cobra.Command{

sylog.Infof("Using checkpoint %q", e.Name())

a := append([]string{"/.singularity.d/actions/exec"}, dmtcp.CheckpointArgs(port)...)
if err := launchContainer(cmd, "instance://"+args[0], a, ""); err != nil {
containerCmd := "/.singularity.d/actions/exec"
containerArgs := dmtcp.CheckpointArgs(port)
if err := launchContainer(cmd, "instance://"+args[0], containerCmd, containerArgs, ""); err != nil {
sylog.Fatalf("%s", err)
}
},
Expand Down
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 @@ -45,14 +45,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 @@ -32,7 +32,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 @@ -46,8 +46,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 @@ -2776,24 +2776,6 @@ func (c actionTests) actionFakerootHome(t *testing.T) {
}
}

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

for _, p := range []e2e.Profile{e2e.OCIUserProfile, e2e.OCIRootProfile} {
c.env.RunApptainer(
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 @@ -2839,8 +2821,11 @@ func E2ETests(env e2e.TestEnv) testhelper.Tests {
"no-mount": c.actionNoMount, // test --no-mount
"compat": np(c.actionCompat), // test --compat
"umask": np(c.actionUmask), // test umask propagation
"ociRuntime": c.ociRuntime, // test --oci (unimplemented)
"invalidRemote": np(c.invalidRemote), // GHSA-5mv9-q7fq-9394
"fakeroot home": c.actionFakerootHome, // test home dir in fakeroot
//
// OCI Runtime Mode
//
"ociRun": c.actionOciRun, // apptainer run --oci
}
}
82 changes: 82 additions & 0 deletions e2e/actions/oci.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// Copyright (c) Contributors to the Apptainer project, established as
// Apptainer a Series of LF Projects LLC.
// For website terms of use, trademark policy, privacy policy and other
// project policies see https://lfprojects.org/policies
// 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/apptainer/apptainer/e2e/internal/e2e"
"github.com/apptainer/apptainer/internal/pkg/test/tool/require"
"github.com/pkg/errors"
)

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.RunApptainer(
t,
e2e.WithProfile(e2e.RootProfile),
e2e.WithCommand("oci mount"),
e2e.WithArgs(c.env.ImagePath, bundleDir),
e2e.ExpectExit(0),
)

cleanup := func() {
c.env.RunApptainer(
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.RunApptainer(
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 @@ -29,5 +29,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 @@ -106,7 +106,7 @@ 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

var fakerootPath string
Expand Down Expand Up @@ -181,6 +181,9 @@ func (l *Launcher) Exec(ctx context.Context, image string, args []string, instan
}
}

// 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 @@ -20,6 +20,7 @@ import (

"github.com/apptainer/apptainer/internal/pkg/buildcfg"
"github.com/apptainer/apptainer/internal/pkg/runtime/launcher"
"github.com/google/uuid"
)

var (
Expand Down Expand Up @@ -233,7 +234,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, "")
}
Loading

0 comments on commit 7fe09bc

Please sign in to comment.