diff --git a/e2e/example/example.go b/e2e/example/example.go new file mode 100644 index 0000000000..003f6ea91f --- /dev/null +++ b/e2e/example/example.go @@ -0,0 +1,189 @@ +// Copyright (c) 2019, 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 example + +import ( + "io/ioutil" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/sylabs/singularity/e2e/internal/e2e" + "github.com/sylabs/singularity/internal/pkg/test" +) + +type testingEnv struct { + // base env for running tests + CmdPath string `split_words:"true"` + TestDir string `split_words:"true"` + RunDisabled bool `default:"false"` + // base image for tests + ImagePath string `split_words:"true"` +} + +var testenv testingEnv + +// exec tests min fuctionality for singularity exec +func testSingularityGeneric(t *testing.T, name string, privileged bool, action string, options e2e.ExecOpts, image string) { + testFn := test.WithoutPrivilege + if privileged { + testFn = test.WithPrivilege + } + + const ( + testfile = "testSingularityExec.tmp" + testdir = "testSingularityExec.dir" + ) + + t.Run(name, testFn(func(t *testing.T) { + if options.Userns { + // check if user namespace is supported and skip test if not + } + + workdir, err := e2e.MakeTmpDir(testenv.TestDir, "d-", 0755) + defer os.RemoveAll(workdir) + + if err := os.MkdirAll(filepath.Join(workdir, "tmp", testdir), 0755); err != nil { + t.Fatal(err) + } + if err := ioutil.WriteFile(filepath.Join(workdir, testfile), []byte{}, 0755); err != nil { + t.Fatal(err) + } + + // running parallel test could have side effects while + // setting current working directory because it would affect + // other Go threads too, it's safer to set cmd.Dir instead + pwd, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + defer os.Chdir(pwd) + + // required when running as root with user namespace enabled + // because generally current working directory is located in + // user home directory, so even if running as root and user + // namespace enabled, root will get a permission denied while + // mounting current working directory. Change this to temporary + // workdir + if err := os.Chdir(workdir); err != nil { + t.Fatal(err) + } + + tests := []struct { + name string + argv []string + e2e.ExecOpts + exitCode int + searchOutput string + }{ + {"true", []string{"true"}, e2e.ExecOpts{}, 0, ""}, + {"trueAbsPAth", []string{"/bin/true"}, e2e.ExecOpts{}, 0, ""}, + {"false", []string{"false"}, e2e.ExecOpts{}, 1, ""}, + {"falseAbsPath", []string{"/bin/false"}, e2e.ExecOpts{}, 1, ""}, + // Scif apps tests + {"ScifTestAppGood", []string{"testapp.sh"}, e2e.ExecOpts{App: "testapp"}, 0, ""}, + {"ScifTestAppBad", []string{"testapp.sh"}, e2e.ExecOpts{App: "fakeapp"}, 1, ""}, + {"ScifTestfolderOrg", []string{"test", "-d", "/scif"}, e2e.ExecOpts{}, 0, ""}, + {"ScifTestfolderOrg", []string{"test", "-d", "/scif/apps"}, e2e.ExecOpts{}, 0, ""}, + {"ScifTestfolderOrg", []string{"test", "-d", "/scif/data"}, e2e.ExecOpts{}, 0, ""}, + {"ScifTestfolderOrg", []string{"test", "-d", "/scif/apps/foo"}, e2e.ExecOpts{}, 0, ""}, + {"ScifTestfolderOrg", []string{"test", "-d", "/scif/apps/bar"}, e2e.ExecOpts{}, 0, ""}, + // blocked by issue [scif-apps] Files created at install step fall into an unexpected path #2404 + {"ScifTestfolderOrg", []string{"test", "-f", "/scif/apps/foo/filefoo.exec"}, e2e.ExecOpts{}, 0, ""}, + {"ScifTestfolderOrg", []string{"test", "-f", "/scif/apps/bar/filebar.exec"}, e2e.ExecOpts{}, 0, ""}, + {"ScifTestfolderOrg", []string{"test", "-d", "/scif/data/foo/output"}, e2e.ExecOpts{}, 0, ""}, + {"ScifTestfolderOrg", []string{"test", "-d", "/scif/data/foo/input"}, e2e.ExecOpts{}, 0, ""}, + {"WorkdirContain", []string{"test", "-d", "/tmp/" + testdir}, e2e.ExecOpts{Workdir: workdir, Contain: true}, 0, ""}, + {"Workdir", []string{"test", "-d", "/tmp/" + testdir}, e2e.ExecOpts{Workdir: workdir}, 1, ""}, + {"pwdGood", []string{"true"}, e2e.ExecOpts{Pwd: "/etc"}, 0, ""}, + {"home", []string{"test", "-f", filepath.Join(workdir, testfile)}, e2e.ExecOpts{Home: workdir}, 0, ""}, + {"noHome", []string{"test", "-f", "/home/" + testfile}, e2e.ExecOpts{Home: workdir + ":/home", NoHome: true}, 1, ""}, + {"homePath", []string{"test", "-f", "/home/" + testfile}, e2e.ExecOpts{Home: workdir + ":/home"}, 0, ""}, + {"homeTmp", []string{"true"}, e2e.ExecOpts{Home: "/tmp"}, 0, ""}, + {"homeTmpExplicit", []string{"true"}, e2e.ExecOpts{Home: "/tmp:/home"}, 0, ""}, + {"ScifTestAppGood", []string{"testapp.sh"}, e2e.ExecOpts{App: "testapp"}, 0, ""}, + {"ScifTestAppBad", []string{"testapp.sh"}, e2e.ExecOpts{App: "fakeapp"}, 1, ""}, + {"userBind", []string{"test", "-f", "/mnt/" + testfile}, e2e.ExecOpts{Binds: []string{workdir + ":/mnt"}}, 0, ""}, + {"PwdGood", []string{"pwd"}, e2e.ExecOpts{Pwd: "/etc"}, 0, "/etc"}, + } + + for _, tt := range tests { + // note that we need to run tests with the same + // privileges wrapper function otherwise tests + // will be always executed as root + t.Run(tt.name, testFn(func(t *testing.T) { + if options.Userns { + tt.ExecOpts.Userns = true + } + stdout, stderr, exitCode, err := e2e.ImageExec(t, testenv.CmdPath, action, tt.ExecOpts, image, tt.argv) + if stdout != "" && !strings.Contains(stdout, tt.searchOutput) { + t.Log(stdout) + t.Fatalf("unexpected output returned running '%v': %v", strings.Join(tt.argv, " "), err) + } else if tt.exitCode >= 0 && exitCode == tt.exitCode { + // PASS + return + } else if tt.exitCode == 0 && exitCode != 0 { + // FAIL + t.Log(stderr) + t.Fatalf("unexpected failure running '%v': %v", strings.Join(tt.argv, " "), err) + } else if tt.exitCode != 0 && exitCode == 0 { + // FAIL + t.Log(stderr) + t.Fatalf("unexpected success running '%v'", strings.Join(tt.argv, " ")) + } else if err != nil { + // FAIL + t.Log(stderr) + t.Fatalf("unexpected error running '%v': %s", strings.Join(tt.argv, " "), err) + } + })) + } + })) +} + +// RunE2ETests is the main func to trigger the test suite +func RunE2ETests(t *testing.T) { + e2e.LoadEnv(t, &testenv) + e2e.EnsureImage(t) + + // world writable to allow unprivileged build to write + // sandbox image + sandboxUnpriv, err := e2e.MakeTmpDir(testenv.TestDir, "d-", 0777) + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(sandboxUnpriv) + + // need to change how SINGULARITY_CACHEDIR is set for parallel tests + test.DropPrivilege(t) + if output, err := e2e.ImageBuild(testenv.CmdPath, e2e.BuildOpts{Force: true, Sandbox: true}, sandboxUnpriv, testenv.ImagePath); err != nil { + t.Fatalf("%s: %s", err, string(output)) + } + test.ResetPrivilege(t) + + sandboxPriv, err := e2e.MakeTmpDir(testenv.TestDir, "d-", 0755) + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(sandboxPriv) + + if output, err := e2e.ImageBuild(testenv.CmdPath, e2e.BuildOpts{Force: true, Sandbox: true}, sandboxPriv, testenv.ImagePath); err != nil { + t.Fatalf("%s: %s", err, string(output)) + } + + testSingularityGeneric(t, "Exec", false, "exec", e2e.ExecOpts{}, testenv.ImagePath) + testSingularityGeneric(t, "ExecPriv", true, "exec", e2e.ExecOpts{}, testenv.ImagePath) + testSingularityGeneric(t, "Run", false, "run", e2e.ExecOpts{}, testenv.ImagePath) + testSingularityGeneric(t, "RunPriv", true, "run", e2e.ExecOpts{}, testenv.ImagePath) + testSingularityGeneric(t, "ExecUsernsWithSIF", false, "exec", e2e.ExecOpts{Userns: true}, testenv.ImagePath) + testSingularityGeneric(t, "ExecUsernsPrivWithSIF", true, "exec", e2e.ExecOpts{Userns: true}, testenv.ImagePath) + testSingularityGeneric(t, "RunUsernsWithSIF", false, "run", e2e.ExecOpts{Userns: true}, testenv.ImagePath) + testSingularityGeneric(t, "RunUsernsPrivWithSIF", true, "run", e2e.ExecOpts{Userns: true}, testenv.ImagePath) + testSingularityGeneric(t, "ExecUsernsWithSandbox", false, "exec", e2e.ExecOpts{Userns: true}, sandboxUnpriv) + testSingularityGeneric(t, "ExecUsernsPrivWithSandbox", true, "exec", e2e.ExecOpts{Userns: true}, sandboxPriv) + testSingularityGeneric(t, "RunUsernsWithSandbox", false, "run", e2e.ExecOpts{Userns: true}, sandboxUnpriv) + testSingularityGeneric(t, "RunUsernsPrivWithSandbox", true, "run", e2e.ExecOpts{Userns: true}, sandboxPriv) +} diff --git a/e2e/scripts/example/example.test b/e2e/scripts/example/example.test new file mode 100644 index 0000000000..c206e170e5 --- /dev/null +++ b/e2e/scripts/example/example.test @@ -0,0 +1,102 @@ + +source common.func + +test-singularity-generic() { + local name="$1" + local privileged="$2" + local command="$3" + local options="$4" + local img="$5" + local oldcwd="$PWD" + + case "${options}" in + *-u*|*--userns*) + if ! check-namespace "user"; then + test-skip "${name}" "user namespace not supported" + return + fi + ;; + esac + + # create test file/dir + local testfile="testSingularityExec.tmp" + local testdir="testSingularityExec.dir" + + # workdir + if [ $privileged = "true" ]; then + local workdir=`mktmpdir 0755 root:root` + sudo mkdir -p ${workdir}/tmp/${testdir} + sudo touch ${workdir}/${testfile} + + set sudo singularity ${command} ${options} + else + local workdir=`mktmpdir` + mkdir -p ${workdir}/tmp/${testdir} + touch ${workdir}/${testfile} + + set singularity ${command} ${options} + fi + + # go into workdir to not trigger error with user namespace execution + cd ${workdir} + + expect-exit 1 "${name}/NoImage" $@ + expect-exit 0 "${name}/True" $@ ${img} true + expect-exit 0 "${name}/TrueAbsPath" $@ ${img} /bin/true + expect-exit 1 "${name}/False" $@ ${img} false + expect-exit 1 "${name}/FalseAbsPath" $@ ${img} /bin/false + + # Scif apps tests + expect-exit 0 "${name}/ScifTestAppGood" $@ --app testapp ${img} testapp.sh + expect-exit 1 "${name}/ScifTestAppBad" $@ --app fakeapp ${img} testapp.sh + expect-exit 0 "${name}/ScifTestfolderOrg" $@ ${img} test -d /scif + expect-exit 0 "${name}/ScifTestfolderOrg" $@ ${img} test -d /scif/apps + expect-exit 0 "${name}/ScifTestfolderOrg" $@ ${img} test -d /scif/data + expect-exit 0 "${name}/ScifTestfolderOrg" $@ ${img} test -d /scif/apps/foo + expect-exit 0 "${name}/ScifTestfolderOrg" $@ ${img} test -d /scif/apps/bar + + # blocked by issue [scif-apps] Files created at install step fall into an unexpected path #2404 + expect-exit 0 "${name}/ScifTestfolderOrg" $@ ${img} test -f /scif/apps/foo/filefoo.exec + expect-exit 0 "${name}/ScifTestfolderOrg" $@ ${img} test -f /scif/apps/bar/filebar.exec + expect-exit 0 "${name}/ScifTestfolderOrg" $@ ${img} test -d /scif/data/foo/output + expect-exit 0 "${name}/ScifTestfolderOrg" $@ ${img} test -d /scif/data/foo/input + + # --workdir + expect-exit 0 "${name}/WorkdirContain" $@ --contain --workdir ${workdir} ${img} test -d /tmp/${testdir} + expect-exit 1 "${name}/Workdir" $@ --workdir ${workdir} ${img} test -d /tmp/${testdir} + + # --home + expect-exit 0 "${name}/Home" $@ --home ${workdir} ${img} test -f ${workdir}/${testfile} + expect-exit 1 "${name}/NoHome" $@ --home ${workdir}:/home --no-home ${img} test -f /home/${testfile} + expect-exit 0 "${name}/HomePath" $@ --home ${workdir}:/home ${img} test -f /home/${testfile} + expect-exit 0 "${name}/HomeTmp" $@ --home /tmp ${img} true + expect-exit 0 "${name}/HomeTmpExplicit" $@ --home /tmp:/home ${img} true + + expect-exit 0 "${name}/UserBind" $@ --bind ${workdir}:/mnt ${img} test -f /mnt/${testfile} + + stdout-contains "${name}/PwdGood" "/etc" $@ --pwd /etc ${img} pwd + + cd ${oldcwd} +} + +image=`get-test-image` + +sandbox_unpriv=`mktmpdir` +run-command singularity build -F -s ${sandbox_unpriv} ${image} + +sandbox_priv=`mktmpdir 0700 root:root` +run-command sudo singularity build -F -s ${sandbox_priv} ${image} + +######################## NAME RUN-PRIVILEGED COMMAND CUSTOM_OPTIONS IMAGE +test-singularity-generic "Exec" false exec "" ${image} +test-singularity-generic "ExecPriv" true exec "" ${image} +test-singularity-generic "Run" false run "" ${image} +test-singularity-generic "RunPriv" true run "" ${image} +test-singularity-generic "ExecUsernsWithSIF" false exec "-u" ${image} +test-singularity-generic "ExecUsernsPrivWithSIF" true exec "-u" ${image} +test-singularity-generic "RunUsernsWithSIF" false run "-u" ${image} +test-singularity-generic "RunUsernsPrivWithSIF" true run "-u" ${image} +test-singularity-generic "ExecUsernsWithSandbox" false exec "-u" ${sandbox_unpriv} +test-singularity-generic "ExecUsernsPrivWithSandbox" true exec "-u" ${sandbox_priv} +test-singularity-generic "RunUsernsWithSandbox" false run "-u" ${sandbox_unpriv} +test-singularity-generic "RunUsernsPrivWithSandbox" true run "-u" ${sandbox_priv} \ No newline at end of file diff --git a/e2e/suite.go b/e2e/suite.go index 5758ecdf1e..ebf243a040 100644 --- a/e2e/suite.go +++ b/e2e/suite.go @@ -19,10 +19,10 @@ import ( "github.com/sylabs/singularity/e2e/actions" "github.com/sylabs/singularity/e2e/docker" singularityenv "github.com/sylabs/singularity/e2e/env" + "github.com/sylabs/singularity/e2e/example" "github.com/sylabs/singularity/e2e/help" "github.com/sylabs/singularity/e2e/imgbuild" "github.com/sylabs/singularity/e2e/instance" - singularitye2e "github.com/sylabs/singularity/e2e/internal/e2e" "github.com/sylabs/singularity/e2e/pull" "github.com/sylabs/singularity/e2e/remote" version "github.com/sylabs/singularity/e2e/version" @@ -91,8 +91,8 @@ func Run(t *testing.T) { defer os.Remove(imagePath) // Start registry for tests - singularitye2e.PrepRegistry(t) - defer singularitye2e.KillRegistry(t) + //singularitye2e.PrepRegistry(t) + //defer singularitye2e.KillRegistry(t) // RunE2ETests by functionality @@ -113,4 +113,6 @@ func Run(t *testing.T) { t.Run("ENV", singularityenv.RunE2ETests) t.Run("VERSION", version.RunE2ETests) + + t.Run("EXAMPLE", example.RunE2ETests) }