Skip to content

Commit

Permalink
oci: support --overlay (sylabs#1659)
Browse files Browse the repository at this point in the history
* oci: support --overlay

* support for multiple overlays, other revisions

* fixup: deduplicate RunWrapped logic

* lots of refactoring and cleanup

* remove leftover debug-related panic call

* cleanup comments, fix small issues w/erroring

---------

Co-authored-by: David Trudgian <[email protected]>
  • Loading branch information
preminger and dtrudg authored May 11, 2023
1 parent adb9010 commit 64844dc
Show file tree
Hide file tree
Showing 15 changed files with 463 additions and 101 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@
`--fakeroot`, for example).
- The `remote status` command will now print the username, realname, and email
of the logged-in user, if available.
- OCI-mode now supports `--overlay <dir>` flag, allowing writes to the
filesystem to persist across runs of the OCI container. If specified dir does
not exist, Singularity will attempt to create it. Multiple overlays can be
specified, but all but one must be read-only (`--overlay <dir>:ro`).

## 3.11.3 \[2023-05-04\]

Expand Down
12 changes: 12 additions & 0 deletions cmd/internal/cli/oci_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,17 @@ var ociBundleFlag = cmdline.Flag{
EnvKeys: []string{"BUNDLE"},
}

// -o|--overlay
var ociOverlayFlag = cmdline.Flag{
ID: "ociOverlayFlag",
Value: &ociArgs.OverlayPaths,
DefaultValue: []string{},
Name: "overlay",
ShortHand: "o",
Usage: "specify an overlay dir to use in lieu of a writable tmpfs",
Tag: "<path>",
}

// -l|--log-path
var ociLogPathFlag = cmdline.Flag{
ID: "ociLogPathFlag",
Expand Down Expand Up @@ -126,6 +137,7 @@ func init() {
cmdManager.RegisterFlagForCmd(&ociLogPathFlag, createRunCmd...)
cmdManager.RegisterFlagForCmd(&ociLogFormatFlag, createRunCmd...)
cmdManager.RegisterFlagForCmd(&ociPidFileFlag, createRunCmd...)
cmdManager.RegisterFlagForCmd(&ociOverlayFlag, OciRunWrappedCmd)
cmdManager.RegisterFlagForCmd(&ociKillForceFlag, OciKillCmd)
cmdManager.RegisterFlagForCmd(&ociKillSignalFlag, OciKillCmd)
cmdManager.RegisterFlagForCmd(&ociUpdateFromFileFlag, OciUpdateCmd)
Expand Down
2 changes: 1 addition & 1 deletion docs/content.go
Original file line number Diff line number Diff line change
Expand Up @@ -977,7 +977,7 @@ Enterprise Performance Computing (EPC)`
$ singularity oci delete mycontainer`

// Internal oci launcher use only - no user-facing docs
OciRunWrappedUse string = `run-wrapped -b <bundle_path> [run options...] <container_ID>`
OciRunWrappedUse string = `run-wrapped -b <bundle_path> [-o <overlay_dir>] [run options...] <container_ID>`

OciUpdateUse string = `update [update options...] <container_ID>`
OciUpdateShort string = `Update container cgroups resources (root user only)`
Expand Down
1 change: 1 addition & 0 deletions e2e/actions/actions.go
Original file line number Diff line number Diff line change
Expand Up @@ -2592,5 +2592,6 @@ func E2ETests(env e2e.TestEnv) testhelper.Tests {
"ociCdi": c.actionOciCdi, // singularity exec --oci --cdi
"ociIDMaps": c.actionOciIDMaps, // check uid/gid mapping on host for --oci as user / --fakeroot
"ociCompat": np(c.actionOciCompat), // --oci equivalence to native mode --compat
"ociOverlay": (c.actionOciOverlay), // --overlay in OCI mode
}
}
99 changes: 97 additions & 2 deletions e2e/actions/oci.go
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,7 @@ func (c actionTests) actionOciExec(t *testing.T) {
argv: []string{"--home", "/tmp", imageRef, "cat", "/etc/passwd"},
exit: 0,
wantOutputs: []e2e.SingularityCmdResultOp{
e2e.ExpectOutput(e2e.RegexMatch, `^root:x:0:0:root:[^:]*:/bin/sh\n`),
e2e.ExpectOutput(e2e.RegexMatch, `^root:x:0:0:root:[^:]*:/bin/ash\n`),
},
},
}
Expand Down Expand Up @@ -811,7 +811,7 @@ func (c actionTests) actionOciCdi(t *testing.T) {

// Generate the command to be executed in the container
// Start by printing all environment variables, to test using e2e.ContainMatch conditions later
execCmd := "/bin/env"
execCmd := "/usr/bin/env"

// Add commands to test the presence of mapped devices.
for _, d := range tt.DeviceNodes {
Expand Down Expand Up @@ -974,3 +974,98 @@ func (c actionTests) actionOciCompat(t *testing.T) {
)
}
}

// actionOciOverlay checks that --overlay functions correctly in OCI mode.
func (c actionTests) actionOciOverlay(t *testing.T) {
e2e.EnsureOCIArchive(t, c.env)
imageRef := "oci-archive:" + c.env.OCIArchivePath

for _, profile := range []e2e.Profile{e2e.OCIRootProfile, e2e.OCIFakerootProfile} {
testDir, err := fs.MakeTmpDir(c.env.TestDir, "overlaytestdir", 0o755)
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
if !t.Failed() {
os.RemoveAll(testDir)
}
})

// Create a few read-only overlay subdirs under testDir
for i := 0; i < 3; i++ {
dirName := fmt.Sprintf("my_ro_ol_dir%d", i)
fullPath := filepath.Join(testDir, dirName)
if err = os.Mkdir(fullPath, 0o755); err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
if !t.Failed() {
os.RemoveAll(fullPath)
}
})
if err = os.WriteFile(
filepath.Join(fullPath, fmt.Sprintf("testfile.%d", i)),
[]byte(fmt.Sprintf("test_string_%d\n", i)),
0o644); err != nil {
t.Fatal(err)
}
if err = os.WriteFile(
filepath.Join(fullPath, "maskable_testfile"),
[]byte(fmt.Sprintf("maskable_string_%d\n", i)),
0o644); err != nil {
t.Fatal(err)
}
}

tests := []struct {
name string
args []string
exitCode int
wantOutputs []e2e.SingularityCmdResultOp
}{
{
name: "NewWritable",
args: []string{"--overlay", filepath.Join(testDir, "my_rw_ol_dir"), imageRef, "sh", "-c", "echo my_test_string > /my_test_file"},
exitCode: 0,
},
{
name: "ExistWritable",
args: []string{"--overlay", filepath.Join(testDir, "my_rw_ol_dir"), imageRef, "cat", "/my_test_file"},
exitCode: 0,
wantOutputs: []e2e.SingularityCmdResultOp{
e2e.ExpectOutput(e2e.ExactMatch, "my_test_string"),
},
},
{
name: "NonExistReadonly",
args: []string{"--overlay", filepath.Join(testDir, "my_ro_ol_dir_nonexistent:ro"), imageRef, "echo", "hi"},
exitCode: 255,
},
{
name: "SeveralReadonly",
args: []string{"--overlay", filepath.Join(testDir, "my_ro_ol_dir2:ro"), "--overlay", filepath.Join(testDir, "my_ro_ol_dir1:ro"), imageRef, "cat", "/testfile.1", "/maskable_testfile"},
exitCode: 0,
wantOutputs: []e2e.SingularityCmdResultOp{
e2e.ExpectOutput(e2e.ContainMatch, "test_string_1"),
e2e.ExpectOutput(e2e.ContainMatch, "maskable_string_2"),
},
},
}

t.Run(profile.String(), func(t *testing.T) {
for _, tt := range tests {
c.env.RunSingularity(
t,
e2e.AsSubtest(tt.name),
e2e.WithProfile(profile),
e2e.WithCommand("exec"),
e2e.WithArgs(tt.args...),
e2e.ExpectExit(
tt.exitCode,
tt.wantOutputs...,
),
)
}
})
}
}
12 changes: 6 additions & 6 deletions e2e/docker/docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -488,28 +488,28 @@ func (c ctx) testDockerRegistry(t *testing.T) {
dfd e2e.DefFileDetails
}{
{
name: "BusyBox",
name: "Alpine",
exit: 0,
dfd: e2e.DefFileDetails{
Bootstrap: "docker",
From: c.env.TestRegistry + "/my-busybox",
From: c.env.TestRegistry + "/my-alpine",
},
},
{
name: "BusyBoxRegistry",
name: "AlpineRegistry",
exit: 0,
dfd: e2e.DefFileDetails{
Bootstrap: "docker",
From: "my-busybox",
From: "my-alpine",
Registry: c.env.TestRegistry,
},
},
{
name: "BusyBoxNamespace",
name: "AlpineNamespace",
exit: 255,
dfd: e2e.DefFileDetails{
Bootstrap: "docker",
From: "my-busybox",
From: "my-alpine",
Registry: c.env.TestRegistry,
Namespace: "not-a-namespace",
},
Expand Down
2 changes: 1 addition & 1 deletion e2e/docker/regressions.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ func (c ctx) issue5172(t *testing.T) {
u := e2e.UserProfile.HostUser(t)

// create $HOME/.config/containers/registries.conf
regImage := "docker://" + c.env.TestRegistry + "/my-busybox"
regImage := "docker://" + c.env.TestRegistry + "/my-alpine"
regDir := filepath.Join(u.Dir, ".config", "containers")
regFile := filepath.Join(regDir, "registries.conf")
imagePath := filepath.Join(c.env.TestDir, "issue-5172")
Expand Down
2 changes: 1 addition & 1 deletion e2e/imgbuild/imgbuild.go
Original file line number Diff line number Diff line change
Expand Up @@ -1550,7 +1550,7 @@ func (c imgBuildTests) buildBindMount(t *testing.T) {
}
}

// testWritableTmpfs checks that we can run the build using a writeable tmpfs in the %test step
// testWritableTmpfs checks that we can run the build using a writable tmpfs in the %test step
func (c imgBuildTests) testWritableTmpfs(t *testing.T) {
e2e.EnsureImage(t, c.env)

Expand Down
6 changes: 3 additions & 3 deletions e2e/suite.go
Original file line number Diff line number Diff line change
Expand Up @@ -194,9 +194,9 @@ func Run(t *testing.T) {

// Provision local registry
testenv.TestRegistry = e2e.StartRegistry(t, testenv)
testenv.TestRegistryImage = fmt.Sprintf("docker://%s/my-busybox:latest", testenv.TestRegistry)
testenv.TestRegistryImage = fmt.Sprintf("docker://%s/my-alpine:latest", testenv.TestRegistry)

// Copy small test image (busybox:latest) into local registry from DockerHub
// Copy small test image (alpine:latest) into local registry from DockerHub
insecureSource := false
insecureValue := os.Getenv("E2E_DOCKER_MIRROR_INSECURE")
if insecureValue != "" {
Expand All @@ -205,7 +205,7 @@ func Run(t *testing.T) {
t.Fatalf("could not convert E2E_DOCKER_MIRROR_INSECURE=%s: %s", insecureValue, err)
}
}
e2e.CopyOCIImage(t, "docker://busybox:latest", testenv.TestRegistryImage, insecureSource, true)
e2e.CopyOCIImage(t, "docker://alpine:latest", testenv.TestRegistryImage, insecureSource, true)

// SIF base test path, built on demand by e2e.EnsureImage
imagePath := path.Join(name, "test.sif")
Expand Down
4 changes: 3 additions & 1 deletion internal/app/singularity/oci_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
// OciArgs contains CLI arguments
type OciArgs struct {
BundlePath string
OverlayPaths []string
LogPath string
LogFormat string
PidFile string
Expand All @@ -48,7 +49,8 @@ func OciRunWrapped(ctx context.Context, containerID string, args *OciArgs) error
if err != nil {
return err
}
return oci.RunWrapped(ctx, containerID, args.BundlePath, args.PidFile, systemdCgroups)

return oci.RunWrapped(ctx, containerID, args.BundlePath, args.PidFile, args.OverlayPaths, systemdCgroups)
}

// OciCreate creates a container from an OCI bundle
Expand Down
7 changes: 2 additions & 5 deletions internal/pkg/runtime/launcher/oci/launcher_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,6 @@ func checkOpts(lo launcher.Options) error {
if lo.WritableTmpfs {
sylog.Infof("--oci mode uses --writable-tmpfs by default")
}
if len(lo.OverlayPaths) > 0 {
badOpt = append(badOpt, "OverlayPaths")
}
if lo.WorkDir != "" {
badOpt = append(badOpt, "WorkDir")
}
Expand Down Expand Up @@ -473,11 +470,11 @@ func (l *Launcher) Exec(ctx context.Context, image string, process string, args

if os.Getuid() == 0 {
// Execution of runc/crun run, wrapped with prep / cleanup.
err = RunWrapped(ctx, id.String(), b.Path(), "", l.singularityConf.SystemdCgroups)
err = RunWrapped(ctx, id.String(), b.Path(), "", l.cfg.OverlayPaths, l.singularityConf.SystemdCgroups)
} else {
// Reexec singularity oci run in a userns with mappings.
// Note - the oci run command will pull out the SystemdCgroups setting from config.
err = RunWrappedNS(ctx, id.String(), b.Path(), "")
err = RunWrappedNS(ctx, id.String(), b.Path(), l.cfg.OverlayPaths)
}
var exitErr *exec.ExitError
if errors.As(err, &exitErr) {
Expand Down
108 changes: 108 additions & 0 deletions internal/pkg/runtime/launcher/oci/oci_overlay.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
// Copyright (c) 2018-2023, 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 oci

import (
"fmt"
"path/filepath"
"strings"

"github.com/sylabs/singularity/pkg/ocibundle/tools"
"github.com/sylabs/singularity/pkg/sylog"
"github.com/sylabs/singularity/pkg/util/singularityconf"
)

// WrapWithWritableTmpFs runs a function wrapped with prep / cleanup steps for a writable tmpfs.
func WrapWithWritableTmpFs(f func() error, bundleDir string) error {
// TODO: --oci mode always emulating --compat, which uses --writable-tmpfs.
// Provide a way of disabling this, for a read only rootfs.
overlayDir, err := prepareWritableTmpfs(bundleDir)
if err != nil {
return err
}

err = f()

// Cleanup actions log errors, but don't return - so we get as much cleanup done as possible.
if cleanupErr := cleanupWritableTmpfs(bundleDir, overlayDir); cleanupErr != nil {
sylog.Errorf("While cleaning up writable tmpfs: %v", cleanupErr)
}

// Return any error from the actual container payload - preserve exit code.
return err
}

// WrapWithOverlays runs a function wrapped with prep / cleanup steps for overlays.
func WrapWithOverlays(f func() error, bundleDir string, overlayPaths []string) error {
writableOverlayFound := false
ovs := tools.OverlaySet{}
for _, p := range overlayPaths {
writable := true
splitted := strings.SplitN(p, ":", 2)
barePath := splitted[0]
if len(splitted) > 1 {
if splitted[1] == "ro" {
writable = false
}
}

if writable && writableOverlayFound {
return fmt.Errorf("you can't specify more than one writable overlay; %#v has already been specified as a writable overlay; use '--overlay %s:ro' instead", ovs.WritableLoc, barePath)
}
if writable {
writableOverlayFound = true
ovs.WritableLoc = barePath
} else {
ovs.ReadonlyLocs = append(ovs.ReadonlyLocs, barePath)
}
}

rootFsDir := tools.RootFs(bundleDir).Path()
err := tools.ApplyOverlay(rootFsDir, ovs)
if err != nil {
return err
}

err = f()

// Cleanup actions log errors, but don't return - so we get as much cleanup done as possible.
if cleanupErr := tools.UnmountOverlay(rootFsDir); cleanupErr != nil {
sylog.Errorf("While unmounting rootfs overlay: %v", cleanupErr)
}

// Return any error from the actual container payload - preserve exit code.
return err
}

func prepareWritableTmpfs(bundleDir string) (string, error) {
sylog.Debugf("Configuring writable tmpfs overlay for %s", bundleDir)
c := singularityconf.GetCurrentConfig()
if c == nil {
return "", fmt.Errorf("singularity configuration is not initialized")
}
return tools.CreateOverlayTmpfs(bundleDir, int(c.SessiondirMaxSize))
}

func cleanupWritableTmpfs(bundleDir, overlayDir string) error {
sylog.Debugf("Cleaning up writable tmpfs overlay for %s", bundleDir)
return tools.DeleteOverlayTmpfs(bundleDir, overlayDir)
}

// absOverlay takes an overlay description string (a path, optionally followed by a colon with an option string, like ":ro" or ":rw"), and replaces any relative path in the description string with an absolute one.
func absOverlay(desc string) (string, error) {
splitted := strings.SplitN(desc, ":", 2)
barePath := splitted[0]
absBarePath, err := filepath.Abs(barePath)
if err != nil {
return "", err
}
absDesc := absBarePath
if len(splitted) > 1 {
absDesc += ":" + splitted[1]
}

return absDesc, nil
}
Loading

0 comments on commit 64844dc

Please sign in to comment.