8311: Add git_tree mount type.
authorTom Clegg <tclegg@veritasgenetics.com>
Tue, 12 Dec 2017 23:56:07 +0000 (18:56 -0500)
committerTom Clegg <tclegg@veritasgenetics.com>
Thu, 14 Dec 2017 16:07:27 +0000 (11:07 -0500)
Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tclegg@veritasgenetics.com>

build/run-tests.sh
sdk/go/arvados/client.go
sdk/go/arvados/container.go
services/api/app/controllers/arvados/v1/schema_controller.rb
services/api/test/test.git.tar
services/crunch-run/crunchrun.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..45f603d4abfdf9e75347f14e2a627b184bc9dc6a 100755 (executable)
@@ -323,6 +323,9 @@ start_api() {
 }
 
 start_nginx_proxy_services() {
+    if [[ -n "$ARVADOS_TEST_PROXY_SERVICES" ]]; then
+        return
+    fi
     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 \
@@ -821,6 +824,13 @@ 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 \
+            || return 1
+
     cd "$WORKSPACE/services/api" \
         && RAILS_ENV=test bundle exec rake db:drop \
         && RAILS_ENV=test bundle exec rake db:setup \
@@ -904,7 +914,7 @@ if [ ! -z "$only" ] && [ "$only" == "services/api" ]; then
   exit_cleanly
 fi
 
-start_api || { stop_services; fatal "start_api"; }
+start_api && start_nginx_proxy_services || { stop_services; fatal "start_api"; }
 
 test_ruby_sdk() {
     cd "$WORKSPACE/sdk/ruby" \
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..16726b76d2bd249bb6e773278c9f91f66719dfc3 100644 (file)
@@ -32,6 +32,7 @@ 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"
 }
 
 // RuntimeConstraints specify a container's compute resources (RAM,
index a237829ec7b4f06f6d0aae693a7853e318777b7f..f4137558386743df03e13ccb559f98dd2397a5c8 100644 (file)
@@ -60,6 +60,7 @@ 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: Rails.application.config.git_repo_https_base,
         parameters: {
           alt: {
             type: "string",
index faa0d656d392c1862349c69234ae408ee8dbe738..93cff81893fec9d598908246ecabc045c1306db8 100644 (file)
Binary files a/services/api/test/test.git.tar and b/services/api/test/test.git.tar differ
index f3f754b59d227c3d410ad1255a9a38da1dd9400b..d262b3d122b98fe0f8c2de2e7ebd5cfed1d59f3b 100644 (file)
@@ -538,6 +538,22 @@ 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)
+                       if err != nil {
+                               return err
+                       }
+                       bind := tmpdir + ":" + bind
+                       if !mnt.Writable {
+                               bind = bind + ":ro"
+                       }
+                       runner.Binds = append(runner.Binds, bind)
                }
        }
 
diff --git a/services/crunch-run/git_mount.go b/services/crunch-run/git_mount.go
new file mode 100644 (file)
index 0000000..a792741
--- /dev/null
@@ -0,0 +1,75 @@
+package main
+
+import (
+       "fmt"
+       "net/url"
+
+       "git.curoverse.com/arvados.git/sdk/go/arvados"
+       "git.curoverse.com/arvados.git/sdk/go/arvadostest"
+       "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
+
+func (gm gitMount) validate() error {
+       if gm.Path != "/" {
+               return fmt.Errorf("cannot mount git_tree path %q -- only \"/\" is supported", gm.Path)
+       }
+       return nil
+}
+
+// ExtractTree extracts the specified tree into dir, which is an
+// existing empty local directory.
+func (gm gitMount) extractTree(ac IArvadosClient, dir 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)
+       }
+       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", arvadostest.ActiveToken),
+       })
+       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)
+       }
+       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..4c6ce7f
--- /dev/null
@@ -0,0 +1,164 @@
+// 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) SetUpSuite(c *check.C) {
+       git_client.InstallProtocol("https", git_http.NewClient(arvados.InsecureHTTPClient))
+}
+
+func (s *GitMountSuite) SetUpTest(c *check.C) {
+       port, err := ioutil.ReadFile("../../tmp/arv-git-httpd-ssl.port")
+       c.Assert(err, check.IsNil)
+       discoveryMap["gitUrl"] = "https://localhost:" + string(port)
+
+       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)
+       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"})
+}
+
+// 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{
+               Path:   "/",
+               UUID:   arvadostest.Repository2UUID,
+               Commit: "5ebfab0522851df01fec11ec55a6d0f4877b542e",
+       }
+       err := gm.extractTree(&ArvTestClient{}, s.tmpdir)
+       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)
+       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)
+       c.Check(err, check.NotNil)
+       c.Check(err, check.ErrorMatches, ".*object not found.*")
+
+       s.checkTmpdirContents(c, []string{})
+}
+
+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:   "/dir1/",
+                               UUID:   arvadostest.Repository2UUID,
+                               Commit: "5ebfab0522851df01fec11ec55a6d0f4877b542e",
+                       },
+                       matcher: ".*path.*",
+               },
+               {
+                       gm: gitMount{
+                               Path:   "/",
+                               Commit: "5ebfab0522851df01fec11ec55a6d0f4877b542e",
+                       },
+                       matcher: ".*UUID.*",
+               },
+       } {
+               err := trial.gm.extractTree(&ArvTestClient{}, s.tmpdir)
+               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)
+}