17813: Handle setting $HOME as a special case
[arvados.git] / lib / crunchrun / singularity.go
index 4bec8c3ebed11970c9f0c0734e625c5c32df2523..be2d1887153dfea20540db4b24626d4676351769 100644 (file)
@@ -5,20 +5,29 @@
 package crunchrun
 
 import (
+       "fmt"
+       "io"
        "io/ioutil"
        "os"
        "os/exec"
+       "sort"
+       "strings"
        "syscall"
 
+       "git.arvados.org/arvados.git/sdk/go/arvados"
        "golang.org/x/net/context"
 )
 
 type singularityExecutor struct {
-       logf          func(string, ...interface{})
-       spec          containerSpec
-       tmpdir        string
-       child         *exec.Cmd
-       imageFilename string // "sif" image
+       logf            func(string, ...interface{})
+       spec            containerSpec
+       tmpdir          string
+       child           *exec.Cmd
+       imageFilename   string // "sif" image
+       containerClient *arvados.Client
+       container       arvados.Container
+       keepClient      IKeepClient
+       keepMount       string
 }
 
 func newSingularityExecutor(logf func(string, ...interface{})) (*singularityExecutor, error) {
@@ -32,13 +41,97 @@ func newSingularityExecutor(logf func(string, ...interface{})) (*singularityExec
        }, nil
 }
 
-func (e *singularityExecutor) ImageLoaded(string) bool {
-       return false
+func (e *singularityExecutor) getOrCreateProject(ownerUuid string, name string, create bool) (*arvados.Group, error) {
+       var gp arvados.GroupList
+       err := e.containerClient.RequestAndDecode(&gp,
+               arvados.EndpointGroupList.Method,
+               arvados.EndpointGroupList.Path,
+               nil, arvados.ListOptions{Filters: []arvados.Filter{
+                       arvados.Filter{"owner_uuid", "=", ownerUuid},
+                       arvados.Filter{"name", "=", name},
+                       arvados.Filter{"group_class", "=", "project"},
+               },
+                       Limit: 1})
+       if err != nil {
+               return nil, err
+       }
+       if len(gp.Items) == 1 {
+               return &gp.Items[0], nil
+       }
+       if !create {
+               return nil, nil
+       }
+       var rgroup arvados.Group
+       err = e.containerClient.RequestAndDecode(&rgroup,
+               arvados.EndpointGroupCreate.Method,
+               arvados.EndpointGroupCreate.Path,
+               nil, map[string]interface{}{
+                       "group": map[string]string{
+                               "owner_uuid":  ownerUuid,
+                               "name":        name,
+                               "group_class": "project",
+                       },
+               })
+       if err != nil {
+               return nil, err
+       }
+       return &rgroup, nil
+}
+
+func (e *singularityExecutor) ImageLoaded(imageId string) bool {
+       // Check if docker image is cached in keep & if so set imageFilename
+
+       // Cache the image to keep
+       cacheGroup, err := e.getOrCreateProject(e.container.RuntimeUserUUID, ".cache", false)
+       if err != nil {
+               e.logf("error getting '.cache' project: %v", err)
+               return false
+       }
+       imageGroup, err := e.getOrCreateProject(cacheGroup.UUID, "auto-generated singularity images", false)
+       if err != nil {
+               e.logf("error getting 'auto-generated singularity images' project: %s", err)
+               return false
+       }
+
+       collectionName := fmt.Sprintf("singularity image for %v", imageId)
+       var cl arvados.CollectionList
+       err = e.containerClient.RequestAndDecode(&cl,
+               arvados.EndpointCollectionList.Method,
+               arvados.EndpointCollectionList.Path,
+               nil, arvados.ListOptions{Filters: []arvados.Filter{
+                       arvados.Filter{"owner_uuid", "=", imageGroup.UUID},
+                       arvados.Filter{"name", "=", collectionName},
+               },
+                       Limit: 1})
+       if err != nil {
+               e.logf("error getting collection '%v' project: %v", err)
+               return false
+       }
+       if len(cl.Items) == 0 {
+               e.logf("no cached image '%v' found", collectionName)
+               return false
+       }
+
+       path := fmt.Sprintf("%s/by_id/%s/image.sif", e.keepMount, cl.Items[0].PortableDataHash)
+       e.logf("Looking for %v", path)
+       if _, err = os.Stat(path); os.IsNotExist(err) {
+               return false
+       }
+       e.imageFilename = path
+
+       return true
 }
 
 // LoadImage will satisfy ContainerExecuter interface transforming
 // containerImage into a sif file for later use.
 func (e *singularityExecutor) LoadImage(imageTarballPath string) error {
+       if e.imageFilename != "" {
+               e.logf("using singularity image %v", e.imageFilename)
+
+               // was set by ImageLoaded
+               return nil
+       }
+
        e.logf("building singularity image")
        // "singularity build" does not accept a
        // docker-archive://... filename containing a ":" character,
@@ -65,6 +158,68 @@ func (e *singularityExecutor) LoadImage(imageTarballPath string) error {
        if err != nil {
                return err
        }
+
+       // Cache the image to keep
+       cacheGroup, err := e.getOrCreateProject(e.container.RuntimeUserUUID, ".cache", true)
+       if err != nil {
+               e.logf("error getting '.cache' project: %v", err)
+               return nil
+       }
+       imageGroup, err := e.getOrCreateProject(cacheGroup.UUID, "auto-generated singularity images", true)
+       if err != nil {
+               e.logf("error getting 'auto-generated singularity images' project: %v", err)
+               return nil
+       }
+
+       parts := strings.Split(imageTarballPath, "/")
+       imageId := parts[len(parts)-1]
+       if strings.HasSuffix(imageId, ".tar") {
+               imageId = imageId[0 : len(imageId)-4]
+       }
+
+       fs, err := (&arvados.Collection{ManifestText: ""}).FileSystem(e.containerClient, e.keepClient)
+       if err != nil {
+               e.logf("error creating FileSystem: %s", err)
+       }
+
+       dst, err := fs.OpenFile("image.sif", os.O_CREATE|os.O_WRONLY, 0666)
+       if err != nil {
+               e.logf("error creating opening collection file for writing: %s", err)
+       }
+
+       src, err := os.Open(e.imageFilename)
+       if err != nil {
+               dst.Close()
+               return nil
+       }
+       defer src.Close()
+       _, err = io.Copy(dst, src)
+       if err != nil {
+               dst.Close()
+               return nil
+       }
+
+       manifestText, err := fs.MarshalManifest(".")
+       if err != nil {
+               e.logf("error creating manifest text: %s", err)
+       }
+
+       var imageCollection arvados.Collection
+       collectionName := fmt.Sprintf("singularity image for %s", imageId)
+       err = e.containerClient.RequestAndDecode(&imageCollection,
+               arvados.EndpointCollectionCreate.Method,
+               arvados.EndpointCollectionCreate.Path,
+               nil, map[string]interface{}{
+                       "collection": map[string]string{
+                               "owner_uuid":    imageGroup.UUID,
+                               "name":          collectionName,
+                               "manifest_text": manifestText,
+                       },
+               })
+       if err != nil {
+               e.logf("error creating '%v' collection: %s", collectionName, err)
+       }
+
        return nil
 }
 
@@ -74,7 +229,7 @@ func (e *singularityExecutor) Create(spec containerSpec) error {
 }
 
 func (e *singularityExecutor) Start() error {
-       args := []string{"singularity", "exec", "--containall", "--no-home", "--cleanenv"}
+       args := []string{"singularity", "exec", "--containall", "--no-home", "--cleanenv", "--pwd", e.spec.WorkingDir}
        if !e.spec.EnableNetwork {
                args = append(args, "--net", "--network=none")
        }
@@ -82,11 +237,15 @@ func (e *singularityExecutor) Start() error {
                false: "rw",
                true:  "ro",
        }
-       for path, mount := range e.spec.BindMounts {
+       var binds []string
+       for path, _ := range e.spec.BindMounts {
+               binds = append(binds, path)
+       }
+       sort.Strings(binds)
+       for _, path := range binds {
+               mount := e.spec.BindMounts[path]
                args = append(args, "--bind", mount.HostPath+":"+path+":"+readonlyflag[mount.ReadOnly])
        }
-       args = append(args, e.imageFilename)
-       args = append(args, e.spec.Command...)
 
        // This is for singularity 3.5.2. There are some behaviors
        // that will change in singularity 3.6, please see:
@@ -94,9 +253,17 @@ func (e *singularityExecutor) Start() error {
        // https://sylabs.io/guides/3.5/user-guide/environment_and_metadata.html
        env := make([]string, 0, len(e.spec.Env))
        for k, v := range e.spec.Env {
-               env = append(env, "SINGULARITYENV_"+k+"="+v)
+               if k == "HOME" {
+                       // $HOME is a special case
+                       args = append(args, "--home="+v)
+               } else {
+                       env = append(env, "SINGULARITYENV_"+k+"="+v)
+               }
        }
 
+       args = append(args, e.imageFilename)
+       args = append(args, e.spec.Command...)
+
        path, err := exec.LookPath(args[0])
        if err != nil {
                return err
@@ -146,3 +313,10 @@ func (e *singularityExecutor) Close() {
                e.logf("error removing temp dir: %s", err)
        }
 }
+
+func (e *singularityExecutor) SetArvadoClient(containerClient *arvados.Client, keepClient IKeepClient, container arvados.Container, keepMount string) {
+       e.containerClient = containerClient
+       e.container = container
+       e.keepClient = keepClient
+       e.keepMount = keepMount
+}