Merge branch '17755-singularity-race' into main refs #17755
[arvados.git] / lib / crunchrun / git_mount.go
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 package crunchrun
6
7 import (
8         "fmt"
9         "net/url"
10         "os"
11         "path/filepath"
12         "regexp"
13
14         "git.arvados.org/arvados.git/sdk/go/arvados"
15         "gopkg.in/src-d/go-billy.v4/osfs"
16         git "gopkg.in/src-d/go-git.v4"
17         git_config "gopkg.in/src-d/go-git.v4/config"
18         git_plumbing "gopkg.in/src-d/go-git.v4/plumbing"
19         git_http "gopkg.in/src-d/go-git.v4/plumbing/transport/http"
20         "gopkg.in/src-d/go-git.v4/storage/memory"
21 )
22
23 type gitMount arvados.Mount
24
25 var (
26         sha1re     = regexp.MustCompile(`^[0-9a-f]{40}$`)
27         repoUUIDre = regexp.MustCompile(`^[0-9a-z]{5}-s0uqq-[0-9a-z]{15}$`)
28 )
29
30 func (gm gitMount) validate() error {
31         if gm.Path != "" && gm.Path != "/" {
32                 return fmt.Errorf("cannot mount git_tree with path %q -- only \"/\" is supported", gm.Path)
33         }
34         if !sha1re.MatchString(gm.Commit) {
35                 return fmt.Errorf("cannot mount git_tree with commit %q -- must be a 40-char SHA1", gm.Commit)
36         }
37         if gm.RepositoryName != "" || gm.GitURL != "" {
38                 return fmt.Errorf("cannot mount git_tree -- repository_name and git_url must be empty")
39         }
40         if !repoUUIDre.MatchString(gm.UUID) {
41                 return fmt.Errorf("cannot mount git_tree with uuid %q -- must be a repository UUID", gm.UUID)
42         }
43         if gm.Writable {
44                 return fmt.Errorf("writable git_tree mount is not supported")
45         }
46         return nil
47 }
48
49 // ExtractTree extracts the specified tree into dir, which is an
50 // existing empty local directory.
51 func (gm gitMount) extractTree(ac IArvadosClient, dir string, token string) error {
52         err := gm.validate()
53         if err != nil {
54                 return err
55         }
56         baseURL, err := ac.Discovery("gitUrl")
57         if err != nil {
58                 return fmt.Errorf("discover gitUrl from API: %s", err)
59         } else if _, ok := baseURL.(string); !ok {
60                 return fmt.Errorf("discover gitUrl from API: expected string, found %T", baseURL)
61         }
62
63         u, err := url.Parse(baseURL.(string))
64         if err != nil {
65                 return fmt.Errorf("parse gitUrl %q: %s", baseURL, err)
66         }
67         u, err = u.Parse("/" + gm.UUID + ".git")
68         if err != nil {
69                 return fmt.Errorf("build git url from %q, %q: %s", baseURL, gm.UUID, err)
70         }
71         store := memory.NewStorage()
72         repo, err := git.Init(store, osfs.New(dir))
73         if err != nil {
74                 return fmt.Errorf("init repo: %s", err)
75         }
76         _, err = repo.CreateRemote(&git_config.RemoteConfig{
77                 Name: "origin",
78                 URLs: []string{u.String()},
79         })
80         if err != nil {
81                 return fmt.Errorf("create remote %q: %s", u.String(), err)
82         }
83         err = repo.Fetch(&git.FetchOptions{
84                 RemoteName: "origin",
85                 Auth: &git_http.BasicAuth{
86                         Username: "none",
87                         Password: token,
88                 },
89         })
90         if err != nil {
91                 return fmt.Errorf("git fetch %q: %s", u.String(), err)
92         }
93         wt, err := repo.Worktree()
94         if err != nil {
95                 return fmt.Errorf("worktree failed: %s", err)
96         }
97         err = wt.Checkout(&git.CheckoutOptions{
98                 Hash: git_plumbing.NewHash(gm.Commit),
99         })
100         if err != nil {
101                 return fmt.Errorf("checkout failed: %s", err)
102         }
103         err = filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
104                 if err != nil {
105                         return err
106                 }
107                 // copy user rx bits to group and other, in case
108                 // prevailing umask is more restrictive than 022
109                 mode := info.Mode()
110                 mode = mode | ((mode >> 3) & 050) | ((mode >> 6) & 5)
111                 return os.Chmod(path, mode)
112         })
113         if err != nil {
114                 return fmt.Errorf("chmod -R %q: %s", dir, err)
115         }
116         return nil
117 }