Skip to content

Commit

Permalink
oci: support for SCIF, incl. --app (#2348)
Browse files Browse the repository at this point in the history
* oci: support --app w/"run" command

* oci: extend SCIF --app support to all "action" cmds

* move Go template execution for tests into gen-purpose pkg

* e2e: initial infrastructure for testing OCI-mode SCIF

* e2e: fill out e2e tests for docker scif

* CHANGELOG entry

* address review comments
  • Loading branch information
preminger authored Nov 14, 2023
1 parent 360736b commit e5d8644
Show file tree
Hide file tree
Showing 10 changed files with 332 additions and 43 deletions.
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

0 comments on commit e5d8644

Please sign in to comment.