Merge branch '19099-singularity-container-shell'
[arvados.git] / lib / crunchrun / singularity.go
index 921f58ff0a3d7fd8a4ed1992aef0fc3bfcc1bbb7..1da401f859f94b36655f771e5fea5af750b0cbe7 100644 (file)
@@ -12,9 +12,11 @@ import (
        "net"
        "os"
        "os/exec"
+       "os/user"
        "regexp"
        "sort"
        "strconv"
+       "strings"
        "syscall"
        "time"
 
@@ -24,6 +26,7 @@ import (
 
 type singularityExecutor struct {
        logf          func(string, ...interface{})
+       fakeroot      bool // use --fakeroot flag, allow --network=bridge when non-root (currently only used by tests)
        spec          containerSpec
        tmpdir        string
        child         *exec.Cmd
@@ -41,7 +44,13 @@ func newSingularityExecutor(logf func(string, ...interface{})) (*singularityExec
        }, nil
 }
 
-func (e *singularityExecutor) Runtime() string { return "singularity" }
+func (e *singularityExecutor) Runtime() string {
+       buf, err := exec.Command("singularity", "--version").CombinedOutput()
+       if err != nil {
+               return "singularity (unknown version)"
+       }
+       return strings.TrimSuffix(string(buf), "\n")
+}
 
 func (e *singularityExecutor) getOrCreateProject(ownerUuid string, name string, containerClient *arvados.Client) (*arvados.Group, error) {
        var gp arvados.GroupList
@@ -247,9 +256,21 @@ func (e *singularityExecutor) Create(spec containerSpec) error {
 }
 
 func (e *singularityExecutor) execCmd(path string) *exec.Cmd {
-       args := []string{path, "exec", "--containall", "--cleanenv", "--pwd", e.spec.WorkingDir, "--net"}
+       args := []string{path, "exec", "--containall", "--cleanenv", "--pwd=" + e.spec.WorkingDir}
+       if e.fakeroot {
+               args = append(args, "--fakeroot")
+       }
        if !e.spec.EnableNetwork {
-               args = append(args, "--network=none")
+               args = append(args, "--net", "--network=none")
+       } else if u, err := user.Current(); err == nil && u.Uid == "0" || e.fakeroot {
+               // Specifying --network=bridge fails unless (a) we are
+               // root, (b) we are using --fakeroot, or (c)
+               // singularity has been configured to allow our
+               // uid/gid to use it like so:
+               //
+               // singularity config global --set 'allow net networks' bridge
+               // singularity config global --set 'allow net groups' mygroup
+               args = append(args, "--net", "--network=bridge")
        }
        if e.spec.CUDADeviceCount != 0 {
                args = append(args, "--nv")
@@ -267,7 +288,7 @@ func (e *singularityExecutor) execCmd(path string) *exec.Cmd {
        for _, path := range binds {
                mount := e.spec.BindMounts[path]
                if path == e.spec.Env["HOME"] {
-                       // Singularity treates $HOME as special case
+                       // Singularity treats $HOME as special case
                        args = append(args, "--home", mount.HostPath+":"+path)
                } else {
                        args = append(args, "--bind", mount.HostPath+":"+path+":"+readonlyflag[mount.ReadOnly])
@@ -281,8 +302,8 @@ func (e *singularityExecutor) execCmd(path string) *exec.Cmd {
        env := make([]string, 0, len(e.spec.Env))
        for k, v := range e.spec.Env {
                if k == "HOME" {
-                       // Singularity treates $HOME as special case, this is handled
-                       // with --home above
+                       // Singularity treats $HOME as special case,
+                       // this is handled with --home above
                        continue
                }
                env = append(env, "SINGULARITYENV_"+k+"="+v)
@@ -296,6 +317,14 @@ func (e *singularityExecutor) execCmd(path string) *exec.Cmd {
                // us to select specific devices we need to propagate that.
                env = append(env, "SINGULARITYENV_CUDA_VISIBLE_DEVICES="+cudaVisibleDevices)
        }
+       // Singularity's default behavior is to evaluate each
+       // SINGULARITYENV_* env var with a shell as a double-quoted
+       // string and pass the result to the contained
+       // process. Singularity 3.10+ has an option to pass env vars
+       // through literally without evaluating, which is what we
+       // want. See https://github.com/sylabs/singularity/pull/704
+       // and https://dev.arvados.org/issues/19081
+       env = append(env, "SINGULARITY_NO_EVAL=1")
 
        args = append(args, e.imageFilename)
        args = append(args, e.spec.Command...)