Merge branch 'master' of git.curoverse.com:arvados into 11876-r-sdk
authorFuad Muhic <fmuhic@capeannenterprises.com>
Fri, 15 Dec 2017 12:01:48 +0000 (13:01 +0100)
committerFuad Muhic <fmuhic@capeannenterprises.com>
Fri, 15 Dec 2017 12:01:48 +0000 (13:01 +0100)
Arvados-DCO-1.1-Signed-off-by: Fuad Muhic <fmuhic@capeannenterprises.com>

15 files changed:
build/run-tests.sh
doc/_includes/_mount_types.liquid
sdk/go/arvados/client.go
sdk/go/arvados/container.go
sdk/go/arvadostest/fixtures.go
services/api/app/controllers/arvados/v1/schema_controller.rb
services/api/app/models/commit.rb
services/api/test/helpers/git_test_helper.rb
services/api/test/test.git.tar
services/arv-git-httpd/auth_handler.go
services/arv-git-httpd/auth_handler_test.go
services/crunch-run/crunchrun.go
services/crunch-run/crunchrun_test.go
services/crunch-run/git_mount.go [new file with mode: 0644]
services/crunch-run/git_mount_test.go [new file with mode: 0644]

index 7d1d4c9e6b29cc7783a40b992fed3773457b1341..7d6cb9ec8b81dc20dc9094cd33a564805a8a6f16 100755 (executable)
@@ -304,8 +304,8 @@ do
     esac
 done
 
-start_api() {
-    echo 'Starting API server...'
+start_services() {
+    echo 'Starting API, keepproxy, keep-web, ws, arv-git-httpd, and nginx ssl proxy...'
     if [[ ! -d "$WORKSPACE/services/api/log" ]]; then
        mkdir -p "$WORKSPACE/services/api/log"
     fi
@@ -317,39 +317,26 @@ start_api() {
         && eval $(python sdk/python/tests/run_test_server.py start --auth admin) \
         && export ARVADOS_TEST_API_HOST="$ARVADOS_API_HOST" \
         && export ARVADOS_TEST_API_INSTALLED="$$" \
-        && python sdk/python/tests/run_test_server.py start_ws \
-        && python sdk/python/tests/run_test_server.py start_nginx \
-        && (env | egrep ^ARVADOS)
-}
-
-start_nginx_proxy_services() {
-    echo 'Starting keepproxy, keep-web, ws, arv-git-httpd, and nginx ssl proxy...'
-    cd "$WORKSPACE" \
         && python sdk/python/tests/run_test_server.py start_keep_proxy \
         && python sdk/python/tests/run_test_server.py start_keep-web \
         && python sdk/python/tests/run_test_server.py start_arv-git-httpd \
         && python sdk/python/tests/run_test_server.py start_ws \
         && python sdk/python/tests/run_test_server.py start_nginx \
-        && export ARVADOS_TEST_PROXY_SERVICES=1
+        && (env | egrep ^ARVADOS)
 }
 
 stop_services() {
-    if [[ -n "$ARVADOS_TEST_PROXY_SERVICES" ]]; then
-        unset ARVADOS_TEST_PROXY_SERVICES
-        cd "$WORKSPACE" \
-            && python sdk/python/tests/run_test_server.py stop_nginx \
-            && python sdk/python/tests/run_test_server.py stop_arv-git-httpd \
-            && python sdk/python/tests/run_test_server.py stop_ws \
-            && python sdk/python/tests/run_test_server.py stop_keep-web \
-            && python sdk/python/tests/run_test_server.py stop_keep_proxy
-    fi
-    if [[ -n "$ARVADOS_TEST_API_HOST" ]]; then
-        unset ARVADOS_TEST_API_HOST
-        cd "$WORKSPACE" \
-            && python sdk/python/tests/run_test_server.py stop_nginx \
-            && python sdk/python/tests/run_test_server.py stop_ws \
-            && python sdk/python/tests/run_test_server.py stop
+    if [[ -z "$ARVADOS_TEST_API_HOST" ]]; then
+        return
     fi
+    unset ARVADOS_TEST_API_HOST
+    cd "$WORKSPACE" \
+        && python sdk/python/tests/run_test_server.py stop_nginx \
+        && python sdk/python/tests/run_test_server.py stop_arv-git-httpd \
+        && python sdk/python/tests/run_test_server.py stop_ws \
+        && python sdk/python/tests/run_test_server.py stop_keep-web \
+        && python sdk/python/tests/run_test_server.py stop_keep_proxy \
+        && python sdk/python/tests/run_test_server.py stop
 }
 
 interrupt() {
@@ -821,6 +808,15 @@ install_apiserver() {
         ) || return 1
     fi
 
+    cd "$WORKSPACE/services/api" \
+        && rm -rf tmp/git \
+        && mkdir -p tmp/git \
+        && cd tmp/git \
+        && tar xf ../../test/test.git.tar \
+        && mkdir -p internal.git \
+        && git --git-dir internal.git init \
+            || return 1
+
     cd "$WORKSPACE/services/api" \
         && RAILS_ENV=test bundle exec rake db:drop \
         && RAILS_ENV=test bundle exec rake db:setup \
@@ -904,7 +900,7 @@ if [ ! -z "$only" ] && [ "$only" == "services/api" ]; then
   exit_cleanly
 fi
 
-start_api || { stop_services; fatal "start_api"; }
+start_services || { stop_services; fatal "start_services"; }
 
 test_ruby_sdk() {
     cd "$WORKSPACE/sdk/ruby" \
@@ -951,37 +947,32 @@ do
 done
 
 test_workbench_units() {
-    start_nginx_proxy_services \
-        && cd "$WORKSPACE/apps/workbench" \
+    cd "$WORKSPACE/apps/workbench" \
         && env RAILS_ENV=test ${short:+RAILS_TEST_SHORT=1} bundle exec rake test:units TESTOPTS=-v ${testargs[apps/workbench]}
 }
 do_test apps/workbench_units workbench_units
 
 test_workbench_functionals() {
-    start_nginx_proxy_services \
-        && cd "$WORKSPACE/apps/workbench" \
+    cd "$WORKSPACE/apps/workbench" \
         && env RAILS_ENV=test ${short:+RAILS_TEST_SHORT=1} bundle exec rake test:functionals TESTOPTS=-v ${testargs[apps/workbench]}
 }
 do_test apps/workbench_functionals workbench_functionals
 
 test_workbench_integration() {
-    start_nginx_proxy_services \
-        && cd "$WORKSPACE/apps/workbench" \
+    cd "$WORKSPACE/apps/workbench" \
         && env RAILS_ENV=test ${short:+RAILS_TEST_SHORT=1} bundle exec rake test:integration TESTOPTS=-v ${testargs[apps/workbench]}
 }
 do_test apps/workbench_integration workbench_integration
 
 
 test_workbench_benchmark() {
-    start_nginx_proxy_services \
-        && cd "$WORKSPACE/apps/workbench" \
+    cd "$WORKSPACE/apps/workbench" \
         && env RAILS_ENV=test ${short:+RAILS_TEST_SHORT=1} bundle exec rake test:benchmark ${testargs[apps/workbench_benchmark]}
 }
 do_test apps/workbench_benchmark workbench_benchmark
 
 test_workbench_profile() {
-    start_nginx_proxy_services \
-        && cd "$WORKSPACE/apps/workbench" \
+    cd "$WORKSPACE/apps/workbench" \
         && env RAILS_ENV=test ${short:+RAILS_TEST_SHORT=1} bundle exec rake test:profile ${testargs[apps/workbench_profile]}
 }
 do_test apps/workbench_profile workbench_profile
index 4b0508db5cc6d3b80fedef8a69f11f2306fb6c81..734b07c8b7970c00bdd6e99be3815e9d8d31f1f0 100644 (file)
@@ -24,25 +24,15 @@ At container startup, the target path will have the same directory structure as
  "kind":"collection",
  "uuid":"..."
 }</code></pre>|
-|Git tree|@git_tree@|One of { @"git-url"@, @"repository_name"@, @"uuid"@ } must be provided.
-One of { @"commit"@, @"revisions"@ } must be provided.
-"path" may be provided. The default path is "/".
-At container startup, the target path will have the source tree indicated by the given revision. The @.git@ metadata directory _will not_ be available: typically the system will use @git-archive@ rather than @git-checkout@ to prepare the target directory.
-- If a value is given for @"revisions"@, it will be resolved to a set of commits (as desribed in the "ranges" section of git-revisions(1)) and the container request will be satisfiable by any commit in that set.
-- If a value is given for @"commit"@, it will be resolved to a single commit, and the tree resulting from that commit will be used.
-- @"path"@ can be used to select a subdirectory or a single file from the tree indicated by the selected commit.
-- Multiple commits can resolve to the same tree: for example, the file/directory given in @"path"@ might not have changed between commits A and B.
-- The resolved mount (found in the Container record) will have only the "kind" key and a "blob" or "tree" key indicating the 40-character hash of the git tree/blob used.|<pre><code>{
+|Git tree|@git_tree@|@"uuid"@ must be the UUID of an Arvados-hosted git repository.
+@"commit"@ must be a full 40-character commit hash.
+@"path"@, if provided, must be "/".
+At container startup, the target path will have the source tree indicated by the given commit. The @.git@ metadata directory _will not_ be available.|<pre><code>{
  "kind":"git_tree",
  "uuid":"zzzzz-s0uqq-xxxxxxxxxxxxxxx",
- "commit":"master"
+ "commit":"f315c59f90934cccae6381e72bba59d27ba42099"
 }
-{
- "kind":"git_tree",
- "uuid":"zzzzz-s0uqq-xxxxxxxxxxxxxxx",
- "commit_range":"bugfix^..master",
- "path":"/crunch_scripts/grep"
-}</code></pre>|
+</code></pre>|
 |Temporary directory|@tmp@|@"capacity"@: capacity (in bytes) of the storage device.
 @"device_type"@ (optional, default "network"): one of @{"ram", "ssd", "disk", "network"}@ indicating the acceptable level of performance.
 At container startup, the target path will be empty. When the container finishes, the content will be discarded. This will be backed by a storage mechanism no slower than the specified type.|<pre><code>{
index a38d95c2e68ee90e1d9f0d41bdef2b341127fd27..24f3faac16053fd6b40457a6111a7ac4d954f994 100644 (file)
@@ -245,6 +245,7 @@ type DiscoveryDocument struct {
        BasePath                     string              `json:"basePath"`
        DefaultCollectionReplication int                 `json:"defaultCollectionReplication"`
        BlobSignatureTTL             int64               `json:"blobSignatureTtl"`
+       GitURL                       string              `json:"gitUrl"`
        Schemas                      map[string]Schema   `json:"schemas"`
        Resources                    map[string]Resource `json:"resources"`
 }
index 7e588be17bb16c04cdbd6098b8dbff8f7c599d18..a541a8dca77fb03b9d6728fd8c9c13c5836414c8 100644 (file)
@@ -32,6 +32,9 @@ type Mount struct {
        Content           interface{} `json:"content"`
        ExcludeFromOutput bool        `json:"exclude_from_output"`
        Capacity          int64       `json:"capacity"`
+       Commit            string      `json:"commit"`          // only if kind=="git_tree"
+       RepositoryName    string      `json:"repository_name"` // only if kind=="git_tree"
+       GitURL            string      `json:"git_url"`         // only if kind=="git_tree"
 }
 
 // RuntimeConstraints specify a container's compute resources (RAM,
index 7858fa02861826e0f7f8e836f87f949d274a744a..eab7a2708c48f16fb9df967c9a88912e5b0f4f4f 100644 (file)
@@ -30,6 +30,13 @@ const (
        Dispatch1AuthUUID = "zzzzz-gj3su-k9dvestay1plssr"
 
        QueuedContainerUUID = "zzzzz-dz642-queuedcontainer"
+
+       ArvadosRepoUUID = "zzzzz-s0uqq-arvadosrepo0123"
+       ArvadosRepoName = "arvados"
+       FooRepoUUID     = "zzzzz-s0uqq-382brsig8rp3666"
+       FooRepoName     = "active/foo"
+       Repository2UUID = "zzzzz-s0uqq-382brsig8rp3667"
+       Repository2Name = "active/foo2"
 )
 
 // PathologicalManifest : A valid manifest designed to test
index a237829ec7b4f06f6d0aae693a7853e318777b7f..d4be3c8093fee71692d5b1ed7b2d5fd57c96e44d 100644 (file)
@@ -60,6 +60,14 @@ class Arvados::V1::SchemaController < ApplicationController
         websocketUrl: Rails.application.config.websocket_address,
         workbenchUrl: Rails.application.config.workbench_address,
         keepWebServiceUrl: Rails.application.config.keep_web_service_url,
+        gitUrl: case Rails.application.config.git_repo_https_base
+                when false
+                  ''
+                when true
+                  'https://git.%s.arvadosapi.com/' % Rails.configuration.uuid_prefix
+                else
+                  Rails.application.config.git_repo_https_base
+                end,
         parameters: {
           alt: {
             type: "string",
index b0efbc7cb0c2c84c5ed4fb705b1e7dc5e88b5138..19254ce8846a326d747d49928d4b7a21baaa9011 100644 (file)
@@ -219,7 +219,7 @@ class Commit < ActiveRecord::Base
   end
 
   def self.cache_dir_base
-    Rails.root.join 'tmp', 'git'
+    Rails.root.join 'tmp', 'git-cache'
   end
 
   def self.fetch_remote_repository gitdir, git_url
index 673e0e248fd0e5e63fbe95a97f85a6d7a2cd3b79..170b59ee1e10d833fd3a0cb0fb6a6aef87bbc123 100644 (file)
@@ -26,19 +26,13 @@ module GitTestHelper
       FileUtils.mkdir_p @tmpdir
       system("tar", "-xC", @tmpdir.to_s, "-f", "test/test.git.tar")
       Rails.configuration.git_repositories_dir = "#{@tmpdir}/test"
-
-      # Initialize an empty internal git repo.
-      intdir =
-        Rails.configuration.git_internal_dir =
-        Rails.root.join(@tmpdir, 'internal.git').to_s
-      FileUtils.mkdir_p intdir
-      IO.read("|git --git-dir #{intdir.shellescape} init")
-      assert $?.success?
+      Rails.configuration.git_internal_dir = "#{@tmpdir}/internal.git"
     end
 
     base.teardown do
-      FileUtils.remove_entry @tmpdir, true
       FileUtils.remove_entry Commit.cache_dir_base, true
+      FileUtils.mkdir_p @tmpdir
+      system("tar", "-xC", @tmpdir.to_s, "-f", "test/test.git.tar")
     end
   end
 
index faa0d656d392c1862349c69234ae408ee8dbe738..8f6a48d98a9c265932f5f203f8ab96f46fcb67d2 100644 (file)
Binary files a/services/api/test/test.git.tar and b/services/api/test/test.git.tar differ
index 617c73282f633ac6ddbca83c2094c1acfe8f3f18..b4dc58b24fc1cb1436cbb1db9dbc6b73deec373c 100644 (file)
@@ -5,9 +5,11 @@
 package main
 
 import (
+       "errors"
        "log"
        "net/http"
        "os"
+       "regexp"
        "strings"
        "sync"
        "time"
@@ -29,7 +31,6 @@ func (h *authHandler) setup() {
                log.Fatal(err)
        }
        h.clientPool = &arvadosclient.ClientPool{Prototype: ac}
-       log.Printf("%+v", h.clientPool.Prototype)
 }
 
 func (h *authHandler) ServeHTTP(wOrig http.ResponseWriter, r *http.Request) {
@@ -71,7 +72,9 @@ func (h *authHandler) ServeHTTP(wOrig http.ResponseWriter, r *http.Request) {
                        // Nobody has called WriteHeader yet: that
                        // must be our job.
                        w.WriteHeader(statusCode)
-                       w.Write([]byte(statusText))
+                       if statusCode >= 400 {
+                               w.Write([]byte(statusText))
+                       }
                }
 
                // If the given password is a valid token, log the first 10 characters of the token.
@@ -117,27 +120,17 @@ func (h *authHandler) ServeHTTP(wOrig http.ResponseWriter, r *http.Request) {
        // Ask API server whether the repository is readable using
        // this token (by trying to read it!)
        arv.ApiToken = apiToken
-       reposFound := arvadosclient.Dict{}
-       if err := arv.List("repositories", arvadosclient.Dict{
-               "filters": [][]string{{"name", "=", repoName}},
-       }, &reposFound); err != nil {
+       repoUUID, err := h.lookupRepo(arv, repoName)
+       if err != nil {
                statusCode, statusText = http.StatusInternalServerError, err.Error()
                return
        }
        validApiToken = true
-       if avail, ok := reposFound["items_available"].(float64); !ok {
-               statusCode, statusText = http.StatusInternalServerError, "bad list response from API"
-               return
-       } else if avail < 1 {
+       if repoUUID == "" {
                statusCode, statusText = http.StatusNotFound, "not found"
                return
-       } else if avail > 1 {
-               statusCode, statusText = http.StatusInternalServerError, "name collision"
-               return
        }
 
-       repoUUID := reposFound["items"].([]interface{})[0].(map[string]interface{})["uuid"].(string)
-
        isWrite := strings.HasSuffix(r.URL.Path, "/git-receive-pack")
        if !isWrite {
                statusText = "read"
@@ -190,3 +183,28 @@ func (h *authHandler) ServeHTTP(wOrig http.ResponseWriter, r *http.Request) {
 
        h.handler.ServeHTTP(w, r)
 }
+
+var uuidRegexp = regexp.MustCompile(`^[0-9a-z]{5}-s0uqq-[0-9a-z]{15}$`)
+
+func (h *authHandler) lookupRepo(arv *arvadosclient.ArvadosClient, repoName string) (string, error) {
+       reposFound := arvadosclient.Dict{}
+       var column string
+       if uuidRegexp.MatchString(repoName) {
+               column = "uuid"
+       } else {
+               column = "name"
+       }
+       err := arv.List("repositories", arvadosclient.Dict{
+               "filters": [][]string{{column, "=", repoName}},
+       }, &reposFound)
+       if err != nil {
+               return "", err
+       } else if avail, ok := reposFound["items_available"].(float64); !ok {
+               return "", errors.New("bad list response from API")
+       } else if avail < 1 {
+               return "", nil
+       } else if avail > 1 {
+               return "", errors.New("name collision")
+       }
+       return reposFound["items"].([]interface{})[0].(map[string]interface{})["uuid"].(string), nil
+}
index df64999405ed2a91a31b24abb6292e1ed76be7ec..05fde03e72c7366ebafdf7e1fc04209e86c5868e 100644 (file)
@@ -5,10 +5,16 @@
 package main
 
 import (
+       "io"
+       "log"
        "net/http"
        "net/http/httptest"
        "net/url"
+       "path/filepath"
+       "strings"
 
+       "git.curoverse.com/arvados.git/sdk/go/arvados"
+       "git.curoverse.com/arvados.git/sdk/go/arvadostest"
        check "gopkg.in/check.v1"
 )
 
@@ -16,6 +22,116 @@ var _ = check.Suite(&AuthHandlerSuite{})
 
 type AuthHandlerSuite struct{}
 
+func (s *AuthHandlerSuite) SetUpSuite(c *check.C) {
+       arvadostest.StartAPI()
+}
+
+func (s *AuthHandlerSuite) TearDownSuite(c *check.C) {
+       arvadostest.StopAPI()
+}
+
+func (s *AuthHandlerSuite) SetUpTest(c *check.C) {
+       arvadostest.ResetEnv()
+       repoRoot, err := filepath.Abs("../api/tmp/git/test")
+       c.Assert(err, check.IsNil)
+       theConfig = &Config{
+               Client: arvados.Client{
+                       APIHost:  arvadostest.APIHost(),
+                       Insecure: true,
+               },
+               Listen:          ":0",
+               GitCommand:      "/usr/bin/git",
+               RepoRoot:        repoRoot,
+               ManagementToken: arvadostest.ManagementToken,
+       }
+}
+
+func (s *AuthHandlerSuite) TestPermission(c *check.C) {
+       h := &authHandler{handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+               log.Printf("%v", r.URL)
+               io.WriteString(w, r.URL.Path)
+       })}
+       baseURL, err := url.Parse("http://git.example/")
+       c.Assert(err, check.IsNil)
+       for _, trial := range []struct {
+               label   string
+               token   string
+               pathIn  string
+               pathOut string
+               status  int
+       }{
+               {
+                       label:   "read repo by name",
+                       token:   arvadostest.ActiveToken,
+                       pathIn:  arvadostest.Repository2Name + ".git/git-upload-pack",
+                       pathOut: arvadostest.Repository2UUID + ".git/git-upload-pack",
+               },
+               {
+                       label:   "read repo by uuid",
+                       token:   arvadostest.ActiveToken,
+                       pathIn:  arvadostest.Repository2UUID + ".git/git-upload-pack",
+                       pathOut: arvadostest.Repository2UUID + ".git/git-upload-pack",
+               },
+               {
+                       label:   "write repo by name",
+                       token:   arvadostest.ActiveToken,
+                       pathIn:  arvadostest.Repository2Name + ".git/git-receive-pack",
+                       pathOut: arvadostest.Repository2UUID + ".git/git-receive-pack",
+               },
+               {
+                       label:   "write repo by uuid",
+                       token:   arvadostest.ActiveToken,
+                       pathIn:  arvadostest.Repository2UUID + ".git/git-receive-pack",
+                       pathOut: arvadostest.Repository2UUID + ".git/git-receive-pack",
+               },
+               {
+                       label:  "uuid not found",
+                       token:  arvadostest.ActiveToken,
+                       pathIn: strings.Replace(arvadostest.Repository2UUID, "6", "z", -1) + ".git/git-upload-pack",
+                       status: http.StatusNotFound,
+               },
+               {
+                       label:  "name not found",
+                       token:  arvadostest.ActiveToken,
+                       pathIn: "nonexistent-bogus.git/git-upload-pack",
+                       status: http.StatusNotFound,
+               },
+               {
+                       label:   "read read-only repo",
+                       token:   arvadostest.SpectatorToken,
+                       pathIn:  arvadostest.FooRepoName + ".git/git-upload-pack",
+                       pathOut: arvadostest.FooRepoUUID + "/.git/git-upload-pack",
+               },
+               {
+                       label:  "write read-only repo",
+                       token:  arvadostest.SpectatorToken,
+                       pathIn: arvadostest.FooRepoName + ".git/git-receive-pack",
+                       status: http.StatusForbidden,
+               },
+       } {
+               c.Logf("trial label: %q", trial.label)
+               u, err := baseURL.Parse(trial.pathIn)
+               c.Assert(err, check.IsNil)
+               resp := httptest.NewRecorder()
+               req := &http.Request{
+                       Method: "POST",
+                       URL:    u,
+                       Header: http.Header{
+                               "Authorization": {"Bearer " + trial.token}}}
+               h.ServeHTTP(resp, req)
+               if trial.status == 0 {
+                       trial.status = http.StatusOK
+               }
+               c.Check(resp.Code, check.Equals, trial.status)
+               if trial.status < 400 {
+                       if trial.pathOut != "" && !strings.HasPrefix(trial.pathOut, "/") {
+                               trial.pathOut = "/" + trial.pathOut
+                       }
+                       c.Check(resp.Body.String(), check.Equals, trial.pathOut)
+               }
+       }
+}
+
 func (s *AuthHandlerSuite) TestCORS(c *check.C) {
        h := &authHandler{}
 
index f3f754b59d227c3d410ad1255a9a38da1dd9400b..8fd5801d236015b28ae125d56f787b3a236064cf 100644 (file)
@@ -390,6 +390,11 @@ func (runner *ContainerRunner) SetupMounts() (err error) {
                return fmt.Errorf("While creating keep mount temp dir: %v", err)
        }
 
+       token, err := runner.ContainerToken()
+       if err != nil {
+               return fmt.Errorf("could not get container token: %s", err)
+       }
+
        pdhOnly := true
        tmpcount := 0
        arvMountCmd := []string{
@@ -538,6 +543,18 @@ func (runner *ContainerRunner) SetupMounts() (err error) {
                                return fmt.Errorf("writing temp file: %v", err)
                        }
                        runner.Binds = append(runner.Binds, fmt.Sprintf("%s:%s:ro", tmpfn, bind))
+
+               case mnt.Kind == "git_tree":
+                       tmpdir, err := runner.MkTempDir("", "")
+                       if err != nil {
+                               return fmt.Errorf("creating temp dir: %v", err)
+                       }
+                       runner.CleanupTempDir = append(runner.CleanupTempDir, tmpdir)
+                       err = gitMount(mnt).extractTree(runner.ArvClient, tmpdir, token)
+                       if err != nil {
+                               return err
+                       }
+                       runner.Binds = append(runner.Binds, tmpdir+":"+bind+":ro")
                }
        }
 
@@ -562,11 +579,6 @@ func (runner *ContainerRunner) SetupMounts() (err error) {
        }
        arvMountCmd = append(arvMountCmd, runner.ArvMountPoint)
 
-       token, err := runner.ContainerToken()
-       if err != nil {
-               return fmt.Errorf("could not get container token: %s", err)
-       }
-
        runner.ArvMount, err = runner.RunArvMount(arvMountCmd, token)
        if err != nil {
                return fmt.Errorf("While trying to start arv-mount: %v", err)
index e1d9fed730ea5ace0393b07e8b1fd8f1eaff65e4..652b50d1798ac42fa5a2df59731b6059ba8959e6 100644 (file)
@@ -28,6 +28,7 @@ import (
 
        "git.curoverse.com/arvados.git/sdk/go/arvados"
        "git.curoverse.com/arvados.git/sdk/go/arvadosclient"
+       "git.curoverse.com/arvados.git/sdk/go/arvadostest"
        "git.curoverse.com/arvados.git/sdk/go/manifest"
 
        dockertypes "github.com/docker/docker/api/types"
@@ -1263,6 +1264,64 @@ func (s *TestSuite) TestSetupMounts(c *C) {
                cr.CleanupDirs()
                checkEmpty()
        }
+
+       // git_tree mounts
+       {
+               i = 0
+               cr.ArvMountPoint = ""
+               (*GitMountSuite)(nil).useTestGitServer(c)
+               cr.token = arvadostest.ActiveToken
+               cr.Container.Mounts = make(map[string]arvados.Mount)
+               cr.Container.Mounts = map[string]arvados.Mount{
+                       "/tip": {
+                               Kind:   "git_tree",
+                               UUID:   arvadostest.Repository2UUID,
+                               Commit: "fd3531f42995344f36c30b79f55f27b502f3d344",
+                               Path:   "/",
+                       },
+                       "/non-tip": {
+                               Kind:   "git_tree",
+                               UUID:   arvadostest.Repository2UUID,
+                               Commit: "5ebfab0522851df01fec11ec55a6d0f4877b542e",
+                               Path:   "/",
+                       },
+               }
+               cr.OutputPath = "/tmp"
+
+               err := cr.SetupMounts()
+               c.Check(err, IsNil)
+
+               // dirMap[mountpoint] == tmpdir
+               dirMap := make(map[string]string)
+               for _, bind := range cr.Binds {
+                       tokens := strings.Split(bind, ":")
+                       dirMap[tokens[1]] = tokens[0]
+
+                       if cr.Container.Mounts[tokens[1]].Writable {
+                               c.Check(len(tokens), Equals, 2)
+                       } else {
+                               c.Check(len(tokens), Equals, 3)
+                               c.Check(tokens[2], Equals, "ro")
+                       }
+               }
+
+               data, err := ioutil.ReadFile(dirMap["/tip"] + "/dir1/dir2/file with mode 0644")
+               c.Check(err, IsNil)
+               c.Check(string(data), Equals, "\000\001\002\003")
+               _, err = ioutil.ReadFile(dirMap["/tip"] + "/file only on testbranch")
+               c.Check(err, FitsTypeOf, &os.PathError{})
+               c.Check(os.IsNotExist(err), Equals, true)
+
+               data, err = ioutil.ReadFile(dirMap["/non-tip"] + "/dir1/dir2/file with mode 0644")
+               c.Check(err, IsNil)
+               c.Check(string(data), Equals, "\000\001\002\003")
+               data, err = ioutil.ReadFile(dirMap["/non-tip"] + "/file only on testbranch")
+               c.Check(err, IsNil)
+               c.Check(string(data), Equals, "testfile\n")
+
+               cr.CleanupDirs()
+               checkEmpty()
+       }
 }
 
 func (s *TestSuite) TestStdout(c *C) {
diff --git a/services/crunch-run/git_mount.go b/services/crunch-run/git_mount.go
new file mode 100644 (file)
index 0000000..92b8371
--- /dev/null
@@ -0,0 +1,110 @@
+package main
+
+import (
+       "fmt"
+       "net/url"
+       "os"
+       "path/filepath"
+       "regexp"
+
+       "git.curoverse.com/arvados.git/sdk/go/arvados"
+       "gopkg.in/src-d/go-billy.v3/osfs"
+       git "gopkg.in/src-d/go-git.v4"
+       git_config "gopkg.in/src-d/go-git.v4/config"
+       git_plumbing "gopkg.in/src-d/go-git.v4/plumbing"
+       git_http "gopkg.in/src-d/go-git.v4/plumbing/transport/http"
+       "gopkg.in/src-d/go-git.v4/storage/memory"
+)
+
+type gitMount arvados.Mount
+
+var (
+       sha1re     = regexp.MustCompile(`^[0-9a-f]{40}$`)
+       repoUUIDre = regexp.MustCompile(`^[0-9a-z]{5}-s0uqq-[0-9a-z]{15}$`)
+)
+
+func (gm gitMount) validate() error {
+       if gm.Path != "" && gm.Path != "/" {
+               return fmt.Errorf("cannot mount git_tree with path %q -- only \"/\" is supported", gm.Path)
+       }
+       if !sha1re.MatchString(gm.Commit) {
+               return fmt.Errorf("cannot mount git_tree with commit %q -- must be a 40-char SHA1", gm.Commit)
+       }
+       if gm.RepositoryName != "" || gm.GitURL != "" {
+               return fmt.Errorf("cannot mount git_tree -- repository_name and git_url must be empty")
+       }
+       if !repoUUIDre.MatchString(gm.UUID) {
+               return fmt.Errorf("cannot mount git_tree with uuid %q -- must be a repository UUID", gm.UUID)
+       }
+       if gm.Writable {
+               return fmt.Errorf("writable git_tree mount is not supported")
+       }
+       return nil
+}
+
+// ExtractTree extracts the specified tree into dir, which is an
+// existing empty local directory.
+func (gm gitMount) extractTree(ac IArvadosClient, dir string, token string) error {
+       err := gm.validate()
+       if err != nil {
+               return err
+       }
+       baseURL, err := ac.Discovery("gitUrl")
+       if err != nil {
+               return fmt.Errorf("discover gitUrl from API: %s", err)
+       } else if _, ok := baseURL.(string); !ok {
+               return fmt.Errorf("discover gitUrl from API: expected string, found %T", baseURL)
+       }
+
+       u, err := url.Parse(baseURL.(string))
+       if err != nil {
+               return fmt.Errorf("parse gitUrl %q: %s", baseURL, err)
+       }
+       u, err = u.Parse("/" + gm.UUID + ".git")
+       if err != nil {
+               return fmt.Errorf("build git url from %q, %q: %s", baseURL, gm.UUID, err)
+       }
+       store := memory.NewStorage()
+       repo, err := git.Init(store, osfs.New(dir))
+       if err != nil {
+               return fmt.Errorf("init repo: %s", err)
+       }
+       _, err = repo.CreateRemote(&git_config.RemoteConfig{
+               Name: "origin",
+               URLs: []string{u.String()},
+       })
+       if err != nil {
+               return fmt.Errorf("create remote %q: %s", u.String(), err)
+       }
+       err = repo.Fetch(&git.FetchOptions{
+               RemoteName: "origin",
+               Auth:       git_http.NewBasicAuth("none", token),
+       })
+       if err != nil {
+               return fmt.Errorf("git fetch %q: %s", u.String(), err)
+       }
+       wt, err := repo.Worktree()
+       if err != nil {
+               return fmt.Errorf("worktree failed: %s", err)
+       }
+       err = wt.Checkout(&git.CheckoutOptions{
+               Hash: git_plumbing.NewHash(gm.Commit),
+       })
+       if err != nil {
+               return fmt.Errorf("checkout failed: %s", err)
+       }
+       err = filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
+               if err != nil {
+                       return err
+               }
+               // copy user rx bits to group and other, in case
+               // prevailing umask is more restrictive than 022
+               mode := info.Mode()
+               mode = mode | ((mode >> 3) & 050) | ((mode >> 6) & 5)
+               return os.Chmod(path, mode)
+       })
+       if err != nil {
+               return fmt.Errorf("chmod -R %q: %s", dir, err)
+       }
+       return nil
+}
diff --git a/services/crunch-run/git_mount_test.go b/services/crunch-run/git_mount_test.go
new file mode 100644 (file)
index 0000000..4dc95bc
--- /dev/null
@@ -0,0 +1,209 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+package main
+
+import (
+       "io/ioutil"
+       "os"
+       "path/filepath"
+
+       "git.curoverse.com/arvados.git/sdk/go/arvados"
+       "git.curoverse.com/arvados.git/sdk/go/arvadostest"
+       check "gopkg.in/check.v1"
+       git_client "gopkg.in/src-d/go-git.v4/plumbing/transport/client"
+       git_http "gopkg.in/src-d/go-git.v4/plumbing/transport/http"
+)
+
+type GitMountSuite struct {
+       tmpdir string
+}
+
+var _ = check.Suite(&GitMountSuite{})
+
+func (s *GitMountSuite) SetUpTest(c *check.C) {
+       s.useTestGitServer(c)
+
+       var err error
+       s.tmpdir, err = ioutil.TempDir("", "")
+       c.Assert(err, check.IsNil)
+}
+
+func (s *GitMountSuite) TearDownTest(c *check.C) {
+       err := os.RemoveAll(s.tmpdir)
+       c.Check(err, check.IsNil)
+}
+
+// Commit fd3531f is crunch-run-tree-test
+func (s *GitMountSuite) TestextractTree(c *check.C) {
+       gm := gitMount{
+               Path:   "/",
+               UUID:   arvadostest.Repository2UUID,
+               Commit: "fd3531f42995344f36c30b79f55f27b502f3d344",
+       }
+       err := gm.extractTree(&ArvTestClient{}, s.tmpdir, arvadostest.ActiveToken)
+       c.Check(err, check.IsNil)
+
+       fnm := filepath.Join(s.tmpdir, "dir1/dir2/file with mode 0644")
+       data, err := ioutil.ReadFile(fnm)
+       c.Check(err, check.IsNil)
+       c.Check(data, check.DeepEquals, []byte{0, 1, 2, 3})
+       fi, err := os.Stat(fnm)
+       c.Check(err, check.IsNil)
+       if err == nil {
+               c.Check(fi.Mode(), check.Equals, os.FileMode(0644))
+       }
+
+       fnm = filepath.Join(s.tmpdir, "dir1/dir2/file with mode 0755")
+       data, err = ioutil.ReadFile(fnm)
+       c.Check(err, check.IsNil)
+       c.Check(string(data), check.DeepEquals, "#!/bin/sh\nexec echo OK\n")
+       fi, err = os.Stat(fnm)
+       c.Check(err, check.IsNil)
+       if err == nil {
+               c.Check(fi.Mode(), check.Equals, os.FileMode(0755))
+       }
+
+       // Ensure there's no extra stuff like a ".git" dir
+       s.checkTmpdirContents(c, []string{"dir1"})
+
+       // Ensure tmpdir is world-readable and world-executable so the
+       // UID inside the container can use it.
+       fi, err = os.Stat(s.tmpdir)
+       c.Check(err, check.IsNil)
+       c.Check(fi.Mode()&os.ModePerm, check.Equals, os.FileMode(0755))
+}
+
+// Commit 5ebfab0 is not the tip of any branch or tag, but is
+// reachable in branch "crunch-run-non-tip-test".
+func (s *GitMountSuite) TestExtractNonTipCommit(c *check.C) {
+       gm := gitMount{
+               UUID:   arvadostest.Repository2UUID,
+               Commit: "5ebfab0522851df01fec11ec55a6d0f4877b542e",
+       }
+       err := gm.extractTree(&ArvTestClient{}, s.tmpdir, arvadostest.ActiveToken)
+       c.Check(err, check.IsNil)
+
+       fnm := filepath.Join(s.tmpdir, "file only on testbranch")
+       data, err := ioutil.ReadFile(fnm)
+       c.Check(err, check.IsNil)
+       c.Check(string(data), check.DeepEquals, "testfile\n")
+}
+
+func (s *GitMountSuite) TestNonexistentRepository(c *check.C) {
+       gm := gitMount{
+               Path:   "/",
+               UUID:   "zzzzz-s0uqq-nonexistentrepo",
+               Commit: "5ebfab0522851df01fec11ec55a6d0f4877b542e",
+       }
+       err := gm.extractTree(&ArvTestClient{}, s.tmpdir, arvadostest.ActiveToken)
+       c.Check(err, check.NotNil)
+       c.Check(err, check.ErrorMatches, ".*repository not found.*")
+
+       s.checkTmpdirContents(c, []string{})
+}
+
+func (s *GitMountSuite) TestNonexistentCommit(c *check.C) {
+       gm := gitMount{
+               Path:   "/",
+               UUID:   arvadostest.Repository2UUID,
+               Commit: "bb66b6bb6b6bbb6b6b6b66b6b6b6b6b6b6b6b66b",
+       }
+       err := gm.extractTree(&ArvTestClient{}, s.tmpdir, arvadostest.ActiveToken)
+       c.Check(err, check.NotNil)
+       c.Check(err, check.ErrorMatches, ".*object not found.*")
+
+       s.checkTmpdirContents(c, []string{})
+}
+
+func (s *GitMountSuite) TestGitUrlDiscoveryFails(c *check.C) {
+       delete(discoveryMap, "gitUrl")
+       gm := gitMount{
+               Path:   "/",
+               UUID:   arvadostest.Repository2UUID,
+               Commit: "5ebfab0522851df01fec11ec55a6d0f4877b542e",
+       }
+       err := gm.extractTree(&ArvTestClient{}, s.tmpdir, arvadostest.ActiveToken)
+       c.Check(err, check.ErrorMatches, ".*gitUrl.*")
+}
+
+func (s *GitMountSuite) TestInvalid(c *check.C) {
+       for _, trial := range []struct {
+               gm      gitMount
+               matcher string
+       }{
+               {
+                       gm: gitMount{
+                               Path:   "/",
+                               UUID:   arvadostest.Repository2UUID,
+                               Commit: "abc123",
+                       },
+                       matcher: ".*SHA1.*",
+               },
+               {
+                       gm: gitMount{
+                               Path:           "/",
+                               UUID:           arvadostest.Repository2UUID,
+                               RepositoryName: arvadostest.Repository2Name,
+                               Commit:         "5ebfab0522851df01fec11ec55a6d0f4877b542e",
+                       },
+                       matcher: ".*repository_name.*",
+               },
+               {
+                       gm: gitMount{
+                               Path:   "/",
+                               GitURL: "https://localhost:0/" + arvadostest.Repository2Name + ".git",
+                               Commit: "5ebfab0522851df01fec11ec55a6d0f4877b542e",
+                       },
+                       matcher: ".*git_url.*",
+               },
+               {
+                       gm: gitMount{
+                               Path:   "/dir1/",
+                               UUID:   arvadostest.Repository2UUID,
+                               Commit: "5ebfab0522851df01fec11ec55a6d0f4877b542e",
+                       },
+                       matcher: ".*path.*",
+               },
+               {
+                       gm: gitMount{
+                               Path:   "/",
+                               Commit: "5ebfab0522851df01fec11ec55a6d0f4877b542e",
+                       },
+                       matcher: ".*UUID.*",
+               },
+               {
+                       gm: gitMount{
+                               Path:     "/",
+                               UUID:     arvadostest.Repository2UUID,
+                               Commit:   "5ebfab0522851df01fec11ec55a6d0f4877b542e",
+                               Writable: true,
+                       },
+                       matcher: ".*writable.*",
+               },
+       } {
+               err := trial.gm.extractTree(&ArvTestClient{}, s.tmpdir, arvadostest.ActiveToken)
+               c.Check(err, check.NotNil)
+               s.checkTmpdirContents(c, []string{})
+
+               err = trial.gm.validate()
+               c.Check(err, check.ErrorMatches, trial.matcher)
+       }
+}
+
+func (s *GitMountSuite) checkTmpdirContents(c *check.C, expect []string) {
+       f, err := os.Open(s.tmpdir)
+       c.Check(err, check.IsNil)
+       names, err := f.Readdirnames(-1)
+       c.Check(err, check.IsNil)
+       c.Check(names, check.DeepEquals, expect)
+}
+
+func (*GitMountSuite) useTestGitServer(c *check.C) {
+       git_client.InstallProtocol("https", git_http.NewClient(arvados.InsecureHTTPClient))
+
+       port, err := ioutil.ReadFile("../../tmp/arv-git-httpd-ssl.port")
+       c.Assert(err, check.IsNil)
+       discoveryMap["gitUrl"] = "https://localhost:" + string(port)
+}