Merge branch '21504-arv-mount-reference'
[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 *arvados.Client, dir string, token string) error {
52         err := gm.validate()
53         if err != nil {
54                 return err
55         }
56         dd, err := ac.DiscoveryDocument()
57         if err != nil {
58                 return fmt.Errorf("error getting discovery document: %w", err)
59         }
60         u, err := url.Parse(dd.GitURL)
61         if err != nil {
62                 return fmt.Errorf("parse gitUrl %q: %s", dd.GitURL, err)
63         }
64         u, err = u.Parse("/" + gm.UUID + ".git")
65         if err != nil {
66                 return fmt.Errorf("build git url from %q, %q: %s", dd.GitURL, gm.UUID, err)
67         }
68         store := memory.NewStorage()
69         repo, err := git.Init(store, osfs.New(dir))
70         if err != nil {
71                 return fmt.Errorf("init repo: %s", err)
72         }
73         _, err = repo.CreateRemote(&git_config.RemoteConfig{
74                 Name: "origin",
75                 URLs: []string{u.String()},
76         })
77         if err != nil {
78                 return fmt.Errorf("create remote %q: %s", u.String(), err)
79         }
80         err = repo.Fetch(&git.FetchOptions{
81                 RemoteName: "origin",
82                 Auth: &git_http.BasicAuth{
83                         Username: "none",
84                         Password: token,
85                 },
86         })
87         if err != nil {
88                 return fmt.Errorf("git fetch %q: %s", u.String(), err)
89         }
90         wt, err := repo.Worktree()
91         if err != nil {
92                 return fmt.Errorf("worktree failed: %s", err)
93         }
94         err = wt.Checkout(&git.CheckoutOptions{
95                 Hash: git_plumbing.NewHash(gm.Commit),
96         })
97         if err != nil {
98                 return fmt.Errorf("checkout failed: %s", err)
99         }
100         err = filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
101                 if err != nil {
102                         return err
103                 }
104                 // copy user rx bits to group and other, in case
105                 // prevailing umask is more restrictive than 022
106                 mode := info.Mode()
107                 mode = mode | ((mode >> 3) & 050) | ((mode >> 6) & 5)
108                 return os.Chmod(path, mode)
109         })
110         if err != nil {
111                 return fmt.Errorf("chmod -R %q: %s", dir, err)
112         }
113         return nil
114 }