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

oci: support for SCIF, incl. --app #2348

Merged
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@
builds, `--authfile` and other authentication options, and more. See the [user
guide](https://docs.sylabs.io/guides/latest/user-guide/build_a_container.html#dockerfile)
for more information.
- Docker-style SCIF containers
([https://sci-f.github.io/tutorial-preview-install](https://sci-f.github.io/tutorial-preview-install))
are now supported. If the entrypoint of an OCI container is the `scif`
executable, then the `run` / `exec` / `shell` commands in `--oci` mode can be
given the `--app <appname>` flag, and will automatically invoke the relevant
SCIF command.

### Bug Fixes

Expand Down
155 changes: 155 additions & 0 deletions e2e/docker/docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import (
"github.com/sylabs/singularity/v4/e2e/internal/e2e"
"github.com/sylabs/singularity/v4/e2e/internal/testhelper"
"github.com/sylabs/singularity/v4/internal/pkg/test/tool/require"
"github.com/sylabs/singularity/v4/internal/pkg/test/tool/tmpl"
"github.com/sylabs/singularity/v4/internal/pkg/util/fs"
"golang.org/x/sys/unix"
"gotest.tools/assert"
Expand Down Expand Up @@ -1571,6 +1572,159 @@ func verifyImgArch(t *testing.T, imgPath, arch string) {
assert.Equal(t, arch, cg.Architecture)
}

// Test support for SCIF containers in OCI mode
func (c ctx) testDockerSCIF(t *testing.T) {
tmpdir, tmpdirCleanup := e2e.MakeTempDir(t, "", "docker-scif-", "dir")
t.Cleanup(func() {
if !t.Failed() {
tmpdirCleanup(t)
}
})

scifRecipeFilename := "local_scif_recipe"
scifRecipeFullpath := filepath.Join(tmpdir, scifRecipeFilename)
scifRecipeSource := filepath.Join("..", "test", "defs", "scif_recipe")
if err := fs.CopyFile(scifRecipeSource, scifRecipeFullpath, 0o755); err != nil {
t.Fatalf("While trying to copy %q to %q: %v", scifRecipeSource, scifRecipeFullpath, err)
}

tmplValues := struct{ SCIFRecipeFilename string }{SCIFRecipeFilename: scifRecipeFilename}
scifDockerfile := tmpl.Execute(t, tmpdir, "Dockerfile-", filepath.Join("..", "test", "defs", "Dockerfile.scif.tmpl"), tmplValues)
scifImageFilename := "scif-image.oci.sif"
scifImageFullpath := filepath.Join(tmpdir, scifImageFilename)

// Uncomment when `singularity inspect --oci` for Docker-style SCIF
// containers is enabled.
// See: https://github.com/sylabs/singularity/pull/2360
// scifInspectOutAllPath := filepath.Join("..", "test", "defs", "scif_recipe.inspect_output.all")
// scifInspectOutAllBytes, err := os.ReadFile(scifInspectOutAllPath)
// if err != nil {
// t.Fatalf("While trying to read contents of %s: %v", scifInspectOutAllPath, err)
// }
// scifInspectOutOnePath := filepath.Join("..", "test", "defs", "scif_recipe.inspect_output.one")
// scifInspectOutOneBytes, err := os.ReadFile(scifInspectOutOnePath)
// if err != nil {
// t.Fatalf("While trying to read contents of %s: %v", scifInspectOutOnePath, err)
// }

// testInspectOutput := func(bytes []byte) func(t *testing.T, r *e2e.SingularityCmdResult) {
// return func(t *testing.T, r *e2e.SingularityCmdResult) {
// got := string(r.Stdout)
// assert.Equal(t, got, string(bytes))
// }
// }

c.env.RunSingularity(
t,
e2e.AsSubtest("build"),
e2e.WithProfile(e2e.OCIUserProfile),
e2e.WithCommand("build"),
e2e.WithDir(tmpdir),
e2e.WithArgs("--oci", scifImageFilename, scifDockerfile),
e2e.ExpectExit(0),
)

tests := []struct {
name string
cmd string
app string
preArgs []string
args []string
expects []e2e.SingularityCmdResultOp
expectExit int
}{
{
name: "run echo",
cmd: "run",
app: "hello-world-echo",
expects: []e2e.SingularityCmdResultOp{
e2e.ExpectOutput(e2e.ContainMatch, "The best app is hello-world-echo"),
},
expectExit: 0,
},
{
name: "exec echo",
cmd: "exec",
app: "hello-world-echo",
args: []string{"echo", "This is different text that should still include [e]SCIF_APPNAME"},
expects: []e2e.SingularityCmdResultOp{
e2e.ExpectOutput(e2e.ContainMatch, "This is different text that should still include hello-world-echo"),
},
expectExit: 0,
},
{
name: "run script",
cmd: "run",
app: "hello-world-script",
expects: []e2e.SingularityCmdResultOp{
e2e.ExpectOutput(e2e.ContainMatch, "Hello World!"),
},
expectExit: 0,
},
{
name: "exec script",
cmd: "exec",
app: "hello-world-script",
args: []string{"echo", "This is different text that should still include [e]SCIF_APPNAME"},
expects: []e2e.SingularityCmdResultOp{
e2e.ExpectOutput(e2e.ContainMatch, "This is different text that should still include hello-world-script"),
},
expectExit: 0,
},
{
name: "exec script2",
cmd: "exec",
app: "hello-world-script",
args: []string{"/bin/bash hello-world.sh"},
expects: []e2e.SingularityCmdResultOp{
e2e.ExpectOutput(e2e.ContainMatch, "Hello World!"),
},
expectExit: 0,
},
// Uncomment when `singularity inspect --oci` for Docker-style SCIF
// containers is enabled.
// See: https://github.com/sylabs/singularity/pull/2360
// {
// name: "insp all",
// cmd: "inspect",
// preArgs: []string{"--oci", "--list-apps"},
// expects: []e2e.SingularityCmdResultOp{
// testInspectOutput(scifInspectOutAllBytes),
// },
// expectExit: 0,
// }, {
// name: "insp one",
// cmd: "inspect",
// app: "hello-world-script",
// preArgs: []string{"--oci"},
// expects: []e2e.SingularityCmdResultOp{
// testInspectOutput(scifInspectOutOneBytes),
// },
// expectExit: 0,
// },
}

for _, tt := range tests {
args := tt.preArgs[:]
if tt.app != "" {
args = append(args, "--app", tt.app)
}
args = append(args, scifImageFullpath)
if len(tt.args) > 0 {
args = append(args, tt.args...)
}

c.env.RunSingularity(
t,
e2e.AsSubtest(tt.name),
e2e.WithProfile(e2e.OCIUserProfile),
e2e.WithCommand(tt.cmd),
e2e.WithArgs(args...),
e2e.ExpectExit(0, tt.expects...),
)
}
}

// E2ETests is the main func to trigger the test suite
func E2ETests(env e2e.TestEnv) testhelper.Tests {
c := ctx{
Expand All @@ -1597,6 +1751,7 @@ func E2ETests(env e2e.TestEnv) testhelper.Tests {
t.Run("user", c.testDockerUSER)
t.Run("platform", c.testDockerPlatform)
t.Run("crossarch buildkit", c.testDockerCrossArchBk)
t.Run("scif", c.testDockerSCIF)
// Regressions
t.Run("issue 4524", c.issue4524)
t.Run("issue 1286", c.issue1286)
Expand Down
46 changes: 8 additions & 38 deletions e2e/imgbuild/imgbuild.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,13 @@ import (
"path/filepath"
"strings"
"testing"
"text/template"
"time"

"github.com/sylabs/singularity/v4/e2e/ecl"
"github.com/sylabs/singularity/v4/e2e/internal/e2e"
"github.com/sylabs/singularity/v4/e2e/internal/testhelper"
"github.com/sylabs/singularity/v4/internal/pkg/test/tool/require"
"github.com/sylabs/singularity/v4/internal/pkg/test/tool/tmpl"
"github.com/sylabs/singularity/v4/internal/pkg/util/fs"
)

Expand Down Expand Up @@ -1962,7 +1962,7 @@ func (c imgBuildTests) buildUseExistingBuildkitd(t *testing.T) {
imageNoPrefix := strings.TrimPrefix(c.env.TestRegistryImage, "docker://")

tmplValues := struct{ Source string }{Source: imageNoPrefix}
dockerfileSimple := c.createDockerfileFromTmpl(t, tmpdir, filepath.Join("..", "test", "defs", "Dockerfile.simple.tmpl"), tmplValues)
dockerfileSimple := tmpl.Execute(t, tmpdir, "Dockerfile-", filepath.Join("..", "test", "defs", "Dockerfile.simple.tmpl"), tmplValues)
outputImgPath := filepath.Join(tmpdir, "image.oci.sif")

buildkitd, err := exec.LookPath("buildkitd")
Expand Down Expand Up @@ -2091,12 +2091,12 @@ func (c imgBuildTests) buildDockerfile(t *testing.T) {
Source: imageNoPrefix,
AddFile: "/this_should_not_exist/this_should_not_exist_either",
}
dockerfileSimple := c.createDockerfileFromTmpl(t, tmpdir, filepath.Join("..", "test", "defs", "Dockerfile.simple.tmpl"), tmplValues)
dockerfileBroken := c.createDockerfileFromTmpl(t, tmpdir, filepath.Join("..", "test", "defs", "Dockerfile.broken.tmpl"), tmplValues)
dockerfileBuildArgs := c.createDockerfileFromTmpl(t, tmpdir, filepath.Join("..", "test", "defs", "Dockerfile.buildargs.tmpl"), tmplValues)
dockerfileBuildArgsNoDef := c.createDockerfileFromTmpl(t, tmpdir, filepath.Join("..", "test", "defs", "Dockerfile.buildargs-nodefault.tmpl"), tmplValues)
dockerfileAdd := c.createDockerfileFromTmpl(t, tmpdir, filepath.Join("..", "test", "defs", "Dockerfile.add.tmpl"), tmplValues)
dockerfileAddBad := c.createDockerfileFromTmpl(t, tmpdir, filepath.Join("..", "test", "defs", "Dockerfile.add.tmpl"), badAddValues)
dockerfileSimple := tmpl.Execute(t, tmpdir, "Dockerfile-", filepath.Join("..", "test", "defs", "Dockerfile.simple.tmpl"), tmplValues)
dockerfileBroken := tmpl.Execute(t, tmpdir, "Dockerfile-", filepath.Join("..", "test", "defs", "Dockerfile.broken.tmpl"), tmplValues)
dockerfileBuildArgs := tmpl.Execute(t, tmpdir, "Dockerfile-", filepath.Join("..", "test", "defs", "Dockerfile.buildargs.tmpl"), tmplValues)
dockerfileBuildArgsNoDef := tmpl.Execute(t, tmpdir, "Dockerfile-", filepath.Join("..", "test", "defs", "Dockerfile.buildargs-nodefault.tmpl"), tmplValues)
dockerfileAdd := tmpl.Execute(t, tmpdir, "Dockerfile-", filepath.Join("..", "test", "defs", "Dockerfile.add.tmpl"), tmplValues)
dockerfileAddBad := tmpl.Execute(t, tmpdir, "Dockerfile-", filepath.Join("..", "test", "defs", "Dockerfile.add.tmpl"), badAddValues)

outputImgPath := filepath.Join(tmpdir, "image.oci.sif")

Expand Down Expand Up @@ -2286,36 +2286,6 @@ func (c imgBuildTests) buildDockerfile(t *testing.T) {
}
}

func (c imgBuildTests) createDockerfileFromTmpl(t *testing.T, tmpdir, tmplPath string, values any) string {
dockerfile, err := os.CreateTemp(tmpdir, "Dockerfile-")
if err != nil {
t.Fatalf("failed to open temp file: %v", err)
}
dockerfileName := dockerfile.Name()
t.Cleanup(func() {
if !t.Failed() {
os.Remove(dockerfileName)
}
})
defer dockerfile.Close()

tmplBytes, err := os.ReadFile(tmplPath)
if err != nil {
t.Fatalf("While trying to read template file %q: %v", tmplPath, err)
}
tmpl, err := template.New(filepath.Base(dockerfileName)).Parse(string(tmplBytes))
if err != nil {
t.Fatalf("While trying to parse template file %q: %v", tmplPath, err)
}

err = tmpl.Execute(dockerfile, values)
if err != nil {
t.Fatalf("While trying to execute template %q: %v", tmplPath, err)
}

return dockerfileName
}

func (c imgBuildTests) buildWithAuth(t *testing.T) {
e2e.EnsureImage(t, c.env)

Expand Down
4 changes: 0 additions & 4 deletions internal/pkg/runtime/launcher/oci/launcher_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -192,10 +192,6 @@ func checkOpts(lo launcher.Options) error {
badOpt = append(badOpt, "ContainAll")
}

if lo.AppName != "" {
badOpt = append(badOpt, "AppName")
}

if lo.KeyInfo != nil {
badOpt = append(badOpt, "KeyInfo")
}
Expand Down
43 changes: 42 additions & 1 deletion internal/pkg/runtime/launcher/oci/process_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"context"
"fmt"
"os"
"path/filepath"
"strings"
"syscall"

Expand All @@ -25,7 +26,10 @@ import (
"golang.org/x/term"
)

const singularityLibs = "/.singularity.d/libs"
const (
singularityLibs = "/.singularity.d/libs"
scifExecutableName = "scif"
)

// Script that can be run by /bin/sh to emulate native mode shell behavior.
// Set Singularity> prompt, try bash --norc, fall back to sh.
Expand Down Expand Up @@ -87,6 +91,20 @@ func (l *Launcher) getProcess(ctx context.Context, imgSpec imgspecv1.Image, bund
return nil, nil, fmt.Errorf("while getting ProcessArgs: %w", err)
}
sylog.Debugf("Native SIF container process/args: %v", args)
case l.cfg.AppName != "":
sylog.Debugf("SCIF app %q requested", l.cfg.AppName)
specArgs := getSpecArgs(imgSpec)
if len(specArgs) < 1 {
return nil, nil, fmt.Errorf("could not determine executable for container")
}
if filepath.Base(specArgs[0]) != scifExecutableName {
sylog.Warningf("OCI mode: SCIF app requested (%q) but container entrypoint does not seem to be a %s executable (container command-line: %q)", l.cfg.AppName, scifExecutableName, strings.Join(specArgs, " "))
}
args, err = l.argsForSCIF(specArgs, ep)
if err != nil {
return nil, nil, err
}
sylog.Debugf("args after prepareArgsForSCIF(): %v", args)
case ep.Action == "shell":
// OCI-SIF shell handling to emulate native runtime shell
args = []string{"/bin/sh", "-c", ociShellScript}
Expand All @@ -108,6 +126,23 @@ func (l *Launcher) getProcess(ctx context.Context, imgSpec imgspecv1.Image, bund
return &p, rtEnv, nil
}

func (l *Launcher) argsForSCIF(specArgs []string, ep launcher.ExecParams) ([]string, error) {
switch ep.Action {
case "run", "exec", "shell":
args := []string{specArgs[0], ep.Action, l.cfg.AppName}
args = append(args, specArgs[1:]...)
if ep.Process != "" {
args = append(args, ep.Process)
}
if len(ep.Args) > 0 {
args = append(args, ep.Args...)
}
return args, nil
}

return []string{}, fmt.Errorf("unrecognized action %q", ep.Action)
}

// getProcessTerminal determines whether the container process should run with a terminal.
func getProcessTerminal() bool {
// Sets the default Process.Terminal to false if our stdin is not a terminal.
Expand Down Expand Up @@ -135,6 +170,12 @@ func getProcessArgs(imageSpec imgspecv1.Image, ep launcher.ExecParams) []string
return processArgs
}

// getSpecArgs attempts to get the command-line args that the OCI container was
// built to run.
func getSpecArgs(imageSpec imgspecv1.Image) []string {
return append(imageSpec.Config.Entrypoint, imageSpec.Config.Cmd...)
}

// getProcessCwd computes the Cwd that the container process should start in.
// Default in OCI mode, like native --compat, is $HOME.
// In native emulation (--no-compat), we use the CWD.
Expand Down
Loading