18702: Merge branch 'main' into 18702-document-upgrades
authorWard Vandewege <ward@curii.com>
Thu, 17 Feb 2022 19:05:36 +0000 (14:05 -0500)
committerWard Vandewege <ward@curii.com>
Thu, 17 Feb 2022 19:05:36 +0000 (14:05 -0500)
Arvados-DCO-1.1-Signed-off-by: Ward Vandewege <ward@curii.com>

36 files changed:
build/package-build-dockerfiles/ubuntu1804/Dockerfile
build/run-build-packages.sh
build/run-library.sh
doc/admin/upgrading.html.textile.liquid
doc/install/install-keep-web.html.textile.liquid
lib/boot/seed.go
lib/config/config.default.yml
lib/config/load.go
lib/crunchrun/crunchrun_test.go
lib/install/deps.go
lib/mount/fs.go
sdk/cwl/arvados_cwl/__init__.py
sdk/cwl/arvados_cwl/runner.py
sdk/cwl/setup.py
sdk/cwl/tests/arvados-tests.yml
sdk/dev-jobs.dockerfile
sdk/go/arvados/fs_base.go
sdk/go/arvados/fs_collection.go
sdk/go/arvados/fs_deferred.go
sdk/go/arvados/fs_filehandle.go
sdk/go/arvados/fs_getternode.go
sdk/go/arvados/fs_lookup.go
sdk/go/arvados/fs_project_test.go
sdk/go/arvados/fs_site.go
sdk/go/arvados/fs_site_test.go
services/api/app/models/api_client_authorization.rb
services/api/app/models/database_seeds.rb
services/api/lib/current_api_client.rb
services/api/script/get_anonymous_user_token.rb [deleted file]
services/keep-web/s3.go
services/keep-web/s3_test.go
tools/arvbox/lib/arvbox/docker/api-setup.sh
tools/arvbox/lib/arvbox/docker/common.sh
tools/arvbox/lib/arvbox/docker/service/doc/run-service
tools/arvbox/lib/arvbox/docker/service/gitolite/run-service
tools/arvbox/lib/arvbox/docker/service/workbench/run-service

index d3727849268ef16d5ce322f58e86e136bf3d98f3..ed2ca495410d2ce2b4f1de63fb87540b950c8d58 100644 (file)
@@ -29,7 +29,7 @@ ENV DEBIAN_FRONTEND noninteractive
 
 SHELL ["/bin/bash", "-c"]
 # Install dependencies.
-RUN /usr/bin/apt-get update && /usr/bin/apt-get install -q -y python3 python3-pip libcurl4-gnutls-dev libgnutls28-dev curl git libattr1-dev libfuse-dev libpq-dev unzip tzdata python3-venv python3-dev libpam-dev equivs
+RUN /usr/bin/apt-get update && /usr/bin/apt-get install -q -y python3.8 python3-pip libcurl4-gnutls-dev libgnutls28-dev curl git libattr1-dev libfuse-dev libpq-dev unzip tzdata python3.8-venv python3.8-dev libpam-dev equivs
 
 # Install virtualenv
 RUN /usr/bin/pip3 install 'virtualenv<20'
index 0cfe54cd139bb1540bc14ae4db0fceafaf6bb0a3..23cf81bd70246a473d12751ba99ee028ec5ec6a3 100755 (executable)
@@ -108,7 +108,8 @@ fi
 
 declare -a PYTHON3_BACKPORTS
 
-PYTHON3_VERSION=$(python3 -c 'import sys; print("{v.major}.{v.minor}".format(v=sys.version_info))')
+PYTHON3_EXECUTABLE=python3
+PYTHON3_VERSION=$($PYTHON3_EXECUTABLE -c 'import sys; print("{v.major}.{v.minor}".format(v=sys.version_info))')
 
 ## These defaults are suitable for any Debian-based distribution.
 # You can customize them as needed in distro sections below.
@@ -122,6 +123,13 @@ case "$TARGET" in
     debian*)
         FORMAT=deb
         ;;
+    ubuntu1804)
+        FORMAT=deb
+        PYTHON3_EXECUTABLE=python3.8
+        PYTHON3_VERSION=$($PYTHON3_EXECUTABLE -c 'import sys; print("{v.major}.{v.minor}".format(v=sys.version_info))')
+        PYTHON3_PACKAGE=python$PYTHON3_VERSION
+        PYTHON3_INSTALL_LIB=lib/python$PYTHON3_VERSION/dist-packages
+        ;;
     ubuntu*)
         FORMAT=deb
         ;;
index b0d4daa722bd2440a4d471ea5b2a62cb1cbbe289..2a869553d1ef0acfcf996c9a6415396ddef87283 100755 (executable)
@@ -787,7 +787,7 @@ fpm_build_virtualenv_worker () {
     ARVADOS_BUILDING_ITERATION=1
   fi
 
-  local python=python3
+  local python=$PYTHON3_EXECUTABLE
   pip=pip3
   PACKAGE_PREFIX=$PYTHON3_PKG_PREFIX
 
index cf5ef55c1f39c20c8188284c26ac27ddd03310e3..5c2a36f4fc693e221056ce609075c7f8475326cb 100644 (file)
@@ -28,10 +28,14 @@ TODO: extract this information based on git commit messages and generate changel
 <div class="releasenotes">
 </notextile>
 
-h2(#main). development main (as of 2021-11-10)
+h2(#main). development main (as of 2022-02-10)
 
 "previous: Upgrading to 2.3.0":#v2_3_0
 
+h3. Anonymous token changes
+
+The anonymous token configured in @Users.AnonymousUserToken@ must now be 32 characters or longer. This was already the suggestion in the documentation, now it is enforced. The @script/get_anonymous_user_token.rb@ script that was needed to register the anonymous user token in the database has been removed. Registration of the anonymous token is no longer necessary.
+
 h3. Preemptible instance types are used automatically, if any are configured
 
 The default behavior for selecting "preemptible instances":{{site.baseurl}}/admin/spot-instances.html has changed. If your configuration lists any instance types with @Preemptible: true@, all child (non-top-level) containers will automatically be scheduled on preemptible instances. To avoid using preemptible instances except when explicitly requested by clients, add @AlwaysUsePreemptibleInstances: false@ in the @Containers@ config section. (Previously, preemptible instance types were never used unless the configuration specified @UsePreemptibleInstances: true@. That flag has been removed.)
index ea2ffb5e4889a4329fa6cc94f0d9a474722aa7ba..1ba9fc522fab9dbf69207d3fa67061e586843079 100644 (file)
@@ -11,7 +11,7 @@ SPDX-License-Identifier: CC-BY-SA-3.0
 
 # "Introduction":#introduction
 # "Configure DNS":#introduction
-# "Configure anonymous user token.yml":#update-config
+# "Configure anonymous user token":#update-config
 # "Update nginx configuration":#update-nginx
 # "Install keep-web package":#install-packages
 # "Start the service":#start-service
@@ -105,16 +105,13 @@ h2. Set InternalURLs
 
 h2(#update-config). Configure anonymous user token
 
-{% assign railscmd = "bin/bundle exec ./script/get_anonymous_user_token.rb --get" %}
-{% assign railsout = "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz" %}
 If you intend to use Keep-web to serve public data to anonymous clients, configure it with an anonymous token.
 
-# First, generate a long random string and put it in the @config.yml@ file, in the @AnonymousUserToken@ field.
-# Then, use the following command on the <strong>API server</strong> to register the anonymous user token in the database. {% include 'install_rails_command' %}
+Generate a random string (>= 32 characters long) and put it in the @config.yml@ file, in the @AnonymousUserToken@ field.
 
 <notextile>
 <pre><code>    Users:
-      AnonymousUserToken: <span class="userinput">"{{railsout}}"</span>
+      AnonymousUserToken: <span class="userinput">"zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz"</span>
 </code></pre>
 </notextile>
 
index bd1e942658e9f50fba873d3de4f3a1c971dd54dc..b43d907201d47a44c013ec7d201955f7f5145377 100644 (file)
@@ -27,9 +27,5 @@ func (seedDatabase) Run(ctx context.Context, fail func(error), super *Supervisor
        if err != nil {
                return err
        }
-       err = super.RunProgram(ctx, "services/api", runOptions{env: railsEnv}, "bundle", "exec", "./script/get_anonymous_user_token.rb")
-       if err != nil {
-               return err
-       }
        return nil
 }
index 17bba5410bb4f31efceb8b1b6ed74eb372d183b8..a7ce9828573fb4b5832f49837de19413af4aa6d4 100644 (file)
@@ -294,9 +294,7 @@ Clusters:
       NewInactiveUserNotificationRecipients: {}
 
       # Set AnonymousUserToken to enable anonymous user access. Populate this
-      # field with a long random string. Then run "bundle exec
-      # ./script/get_anonymous_user_token.rb" in the directory where your API
-      # server is running to record the token in the database.
+      # field with a random string at least 50 characters long.
       AnonymousUserToken: ""
 
       # If a new user has an alternate email address (local@domain)
index c2eb55554488f465a7dc990b87ad6da450459a4d..aa7520ca29e7d6fe2d4040e50ae11f3416b98c77 100644 (file)
@@ -299,9 +299,10 @@ func (ldr *Loader) Load() (*arvados.Config, error) {
                for _, err = range []error{
                        ldr.checkClusterID(fmt.Sprintf("Clusters.%s", id), id, false),
                        ldr.checkClusterID(fmt.Sprintf("Clusters.%s.Login.LoginCluster", id), cc.Login.LoginCluster, true),
-                       ldr.checkToken(fmt.Sprintf("Clusters.%s.ManagementToken", id), cc.ManagementToken),
-                       ldr.checkToken(fmt.Sprintf("Clusters.%s.SystemRootToken", id), cc.SystemRootToken),
-                       ldr.checkToken(fmt.Sprintf("Clusters.%s.Collections.BlobSigningKey", id), cc.Collections.BlobSigningKey),
+                       ldr.checkToken(fmt.Sprintf("Clusters.%s.ManagementToken", id), cc.ManagementToken, true),
+                       ldr.checkToken(fmt.Sprintf("Clusters.%s.SystemRootToken", id), cc.SystemRootToken, true),
+                       ldr.checkToken(fmt.Sprintf("Clusters.%s.Users.AnonymousUserToken", id), cc.Users.AnonymousUserToken, false),
+                       ldr.checkToken(fmt.Sprintf("Clusters.%s.Collections.BlobSigningKey", id), cc.Collections.BlobSigningKey, true),
                        checkKeyConflict(fmt.Sprintf("Clusters.%s.PostgreSQL.Connection", id), cc.PostgreSQL.Connection),
                        ldr.checkEnum("Containers.LocalKeepLogsToContainerLog", cc.Containers.LocalKeepLogsToContainerLog, "none", "all", "errors"),
                        ldr.checkEmptyKeepstores(cc),
@@ -333,10 +334,15 @@ func (ldr *Loader) checkClusterID(label, clusterID string, emptyStringOk bool) e
 var acceptableTokenRe = regexp.MustCompile(`^[a-zA-Z0-9]+$`)
 var acceptableTokenLength = 32
 
-func (ldr *Loader) checkToken(label, token string) error {
-       if token == "" {
-               if ldr.Logger != nil {
-                       ldr.Logger.Warnf("%s: secret token is not set (use %d+ random characters from a-z, A-Z, 0-9)", label, acceptableTokenLength)
+func (ldr *Loader) checkToken(label, token string, mandatory bool) error {
+       if len(token) == 0 {
+               if !mandatory {
+                       // when a token is not mandatory, the acceptable length and content is only checked if its length is non-zero
+                       return nil
+               } else {
+                       if ldr.Logger != nil {
+                               ldr.Logger.Warnf("%s: secret token is not set (use %d+ random characters from a-z, A-Z, 0-9)", label, acceptableTokenLength)
+                       }
                }
        } else if !acceptableTokenRe.MatchString(token) {
                return fmt.Errorf("%s: unacceptable characters in token (only a-z, A-Z, 0-9 are acceptable)", label)
index 806a83eef9aa789beeefaf5849051281428e8d81..26f78d2bf7d537c7a44f0b8dd57eda4355211b5b 100644 (file)
@@ -375,6 +375,14 @@ func (fw FileWrapper) Sync() error {
        return errors.New("not implemented")
 }
 
+func (fw FileWrapper) Snapshot() (*arvados.Subtree, error) {
+       return nil, errors.New("not implemented")
+}
+
+func (fw FileWrapper) Splice(*arvados.Subtree) error {
+       return errors.New("not implemented")
+}
+
 func (client *KeepTestClient) ManifestFileReader(m manifest.Manifest, filename string) (arvados.File, error) {
        if filename == hwImageID+".tar" {
                rdr := ioutil.NopCloser(&bytes.Buffer{})
index 1d3cc09275feb688929d9ab01ca4dd73de42aa5d..483ce9c933a87814bc0ada4e6252a06d6d269f7e 100644 (file)
@@ -262,7 +262,7 @@ ln -sf /var/lib/arvados/go/bin/* /usr/local/bin/
                } else {
                        err = inst.runBash(`
 PJS=phantomjs-`+pjsversion+`-linux-x86_64
-wget --progress=dot:giga -O- https://bitbucket.org/ariya/phantomjs/downloads/$PJS.tar.bz2 | tar -C /var/lib/arvados -xjf -
+wget --progress=dot:giga -O- https://cache.arvados.org/$PJS.tar.bz2 | tar -C /var/lib/arvados -xjf -
 ln -sf /var/lib/arvados/$PJS/bin/phantomjs /usr/local/bin/
 `, stdout, stderr)
                        if err != nil {
index c008b96af664ffa605592a1f61e14e30eb4677d3..3c2e628d0115e361f58150f589060ee14bc57f1b 100644 (file)
@@ -5,6 +5,7 @@
 package mount
 
 import (
+       "errors"
        "io"
        "log"
        "os"
@@ -121,23 +122,25 @@ func (fs *keepFS) Utimens(path string, tmsp []fuse.Timespec) int {
 }
 
 func (fs *keepFS) errCode(err error) int {
-       if os.IsNotExist(err) {
+       if err == nil {
+               return 0
+       }
+       if errors.Is(err, os.ErrNotExist) {
                return -fuse.ENOENT
        }
-       switch err {
-       case os.ErrExist:
+       if errors.Is(err, os.ErrExist) {
                return -fuse.EEXIST
-       case arvados.ErrInvalidArgument:
+       }
+       if errors.Is(err, arvados.ErrInvalidArgument) {
                return -fuse.EINVAL
-       case arvados.ErrInvalidOperation:
+       }
+       if errors.Is(err, arvados.ErrInvalidOperation) {
                return -fuse.ENOSYS
-       case arvados.ErrDirectoryNotEmpty:
+       }
+       if errors.Is(err, arvados.ErrDirectoryNotEmpty) {
                return -fuse.ENOTEMPTY
-       case nil:
-               return 0
-       default:
-               return -fuse.EIO
        }
+       return -fuse.EIO
 }
 
 func (fs *keepFS) Mkdir(path string, mode uint32) int {
index c3848b2629188c9cf687e8b69f054237b9e5b77e..734945c0f7ccb707258ab60be52107195afff0e0 100644 (file)
@@ -258,7 +258,11 @@ def exit_signal_handler(sigcode, frame):
     logger.error(str(u"Caught signal {}, exiting.").format(sigcode))
     sys.exit(-sigcode)
 
-def main(args, stdout, stderr, api_client=None, keep_client=None,
+def main(args=sys.argv[1:],
+         stdout=sys.stdout,
+         stderr=sys.stderr,
+         api_client=None,
+         keep_client=None,
          install_sig_handlers=True):
     parser = arg_parser()
 
index 145f1ad7f9d7572f1f4074da514cd0ea0f771d3e..7d6d287a207455d04e2a1d5469e053ea57f7cf6e 100644 (file)
@@ -283,9 +283,10 @@ def upload_dependencies(arvrunner, name, document_loader,
     metadata = scanobj
 
     sc_result = scandeps(uri, scanobj,
-                  loadref_fields,
-                  set(("$include", "$schemas", "location")),
-                  loadref, urljoin=document_loader.fetcher.urljoin)
+                         loadref_fields,
+                         set(("$include", "$schemas", "location")),
+                         loadref, urljoin=document_loader.fetcher.urljoin,
+                         nestdirs=False)
 
     sc = []
     uuids = {}
index f034ca5ab2f1ed82a385431fb1e32acea7c17fc7..e739235953cf799e07e9f13440ef7e05963241c9 100644 (file)
@@ -31,15 +31,12 @@ setup(name='arvados-cwl-runner',
       license='Apache 2.0',
       packages=find_packages(),
       package_data={'arvados_cwl': ['arv-cwl-schema-v1.0.yml', 'arv-cwl-schema-v1.1.yml', 'arv-cwl-schema-v1.2.yml']},
-      scripts=[
-          'bin/cwl-runner',
-          'bin/arvados-cwl-runner',
-      ],
+      entry_points={"console_scripts": ["cwl-runner=arvados_cwl:main", "arvados-cwl-runner=arvados_cwl:main"]},
       # Note that arvados/build/run-build-packages.sh looks at this
       # file to determine what version of cwltool and schema-salad to
       # build.
       install_requires=[
-          'cwltool==3.1.20211107152837',
+          'cwltool==3.1.20220210171524',
           'schema-salad==8.2.20211116214159',
           'arvados-python-client{}'.format(pysdk_dep),
           'setuptools',
index ae22d65f4db22f3a2b285320d1501a79d0a2e139..5282e93929fd19c932c521ac3e004950f66c533f 100644 (file)
     "outstr": "foo woble bar"
   tool: 17879-ignore-sbg-fields.cwl
   doc: "Test issue 17879 - ignores sbg fields"
+
+- job: chipseq/chip-seq-single.json
+  output: {}
+  tool: chipseq/cwl-packed.json
+  doc: "Test issue 18723 - correctly upload two directories with the same basename"
index 1e0068ffd48250375c60b8dc1331668a930fb809..b55b056b2d38339fe4bb42ddd15ba267099975ea 100644 (file)
@@ -23,9 +23,7 @@ ARG pipcmd=pip3
 
 RUN apt-get update -q && apt-get install -qy --no-install-recommends \
     git ${pythoncmd}-pip ${pythoncmd}-virtualenv ${pythoncmd}-dev libcurl4-gnutls-dev \
-    libgnutls28-dev nodejs ${pythoncmd}-pyasn1-modules build-essential
-
-RUN $pipcmd install -U setuptools six requests
+    libgnutls28-dev nodejs ${pythoncmd}-pyasn1-modules build-essential ${pythoncmd}-setuptools
 
 ARG sdk
 ARG runner
@@ -39,7 +37,7 @@ ADD cwl/dist/$runner /tmp/
 
 RUN cd /tmp/arvados-python-client-* && $pipcmd install .
 RUN if test -d /tmp/schema-salad-* ; then cd /tmp/schema-salad-* && $pipcmd install . ; fi
-RUN if test -d /tmp/cwltool-* ; then cd /tmp/cwltool-* && $pipcmd install networkx==2.2 && $pipcmd install . ; fi
+RUN if test -d /tmp/cwltool-* ; then cd /tmp/cwltool-* && $pipcmd install . ; fi
 RUN cd /tmp/arvados-cwl-runner-* && $pipcmd install .
 
 # Install dependencies and set up system.
index 5f2747ac9ada9225455331daee0f10640202907f..80b803729349dd500de0e7832288f593c41ab60c 100644 (file)
@@ -77,6 +77,24 @@ type File interface {
        Stat() (os.FileInfo, error)
        Truncate(int64) error
        Sync() error
+       // Create a snapshot of a file or directory tree, which can
+       // then be spliced onto a different path or a different
+       // collection.
+       Snapshot() (*Subtree, error)
+       // Replace this file or directory with the given snapshot.
+       // The target must be inside a collection: Splice returns an
+       // error if the File is a virtual file or directory like
+       // by_id, a project directory, .arvados#collection,
+       // etc. Splice can replace directories with regular files and
+       // vice versa, except it cannot replace the root directory of
+       // a collection with a regular file.
+       Splice(snapshot *Subtree) error
+}
+
+// A Subtree is a detached part of a filesystem tree that can be
+// spliced into a filesystem via (File)Splice().
+type Subtree struct {
+       inode inode
 }
 
 // A FileSystem is an http.Filesystem plus Stat() and support for
@@ -152,6 +170,12 @@ type inode interface {
        Readdir() ([]os.FileInfo, error)
        Size() int64
        FileInfo() os.FileInfo
+       // Create a snapshot of this node and its descendants.
+       Snapshot() (inode, error)
+       // Replace this node with a copy of the provided snapshot.
+       // Caller may provide the same snapshot to multiple Splice
+       // calls, but must not modify the snapshot concurrently.
+       Splice(inode) error
 
        // Child() performs lookups and updates of named child nodes.
        //
@@ -270,6 +294,14 @@ func (*nullnode) MemorySize() int64 {
        return 64
 }
 
+func (*nullnode) Snapshot() (inode, error) {
+       return nil, ErrInvalidOperation
+}
+
+func (*nullnode) Splice(inode) error {
+       return ErrInvalidOperation
+}
+
 type treenode struct {
        fs       FileSystem
        parent   inode
@@ -559,7 +591,7 @@ func (fs *fileSystem) Rename(oldname, newname string) error {
                // supported. Locking inodes from different
                // filesystems could deadlock, so we must error out
                // now.
-               return ErrInvalidArgument
+               return ErrInvalidOperation
        }
 
        // To ensure we can test reliably whether we're about to move
@@ -697,3 +729,32 @@ func rlookup(start inode, path string) (node inode, err error) {
 func permittedName(name string) bool {
        return name != "" && name != "." && name != ".." && !strings.Contains(name, "/")
 }
+
+// Snapshot returns a Subtree that's a copy of the given path. It
+// returns an error if the path is not inside a collection.
+func Snapshot(fs FileSystem, path string) (*Subtree, error) {
+       f, err := fs.OpenFile(path, os.O_RDONLY, 0)
+       if err != nil {
+               return nil, err
+       }
+       defer f.Close()
+       return f.Snapshot()
+}
+
+// Splice inserts newsubtree at the indicated target path.
+//
+// Splice returns an error if target is not inside a collection.
+//
+// Splice returns an error if target is the root of a collection and
+// newsubtree is a snapshot of a file.
+func Splice(fs FileSystem, target string, newsubtree *Subtree) error {
+       f, err := fs.OpenFile(target, os.O_WRONLY, 0)
+       if os.IsNotExist(err) {
+               f, err = fs.OpenFile(target, os.O_CREATE|os.O_WRONLY, 0700)
+       }
+       if err != nil {
+               return err
+       }
+       defer f.Close()
+       return f.Splice(newsubtree)
+}
index d087fd09441bc3938411d803de007541f506ba9f..0c5819721e0cb4e8762d6c1e1e3b70427a46a7da 100644 (file)
@@ -457,6 +457,14 @@ func (fs *collectionFileSystem) Size() int64 {
        return fs.fileSystem.root.(*dirnode).TreeSize()
 }
 
+func (fs *collectionFileSystem) Snapshot() (inode, error) {
+       return fs.fileSystem.root.Snapshot()
+}
+
+func (fs *collectionFileSystem) Splice(r inode) error {
+       return fs.fileSystem.root.Splice(r)
+}
+
 // filenodePtr is an offset into a file that is (usually) efficient to
 // seek to. Specifically, if filenode.repacked==filenodePtr.repacked
 // then
@@ -876,6 +884,47 @@ func (fn *filenode) waitPrune() {
        }
 }
 
+func (fn *filenode) Snapshot() (inode, error) {
+       fn.RLock()
+       defer fn.RUnlock()
+       segments := make([]segment, 0, len(fn.segments))
+       for _, seg := range fn.segments {
+               segments = append(segments, seg.Slice(0, seg.Len()))
+       }
+       return &filenode{
+               fileinfo: fn.fileinfo,
+               segments: segments,
+       }, nil
+}
+
+func (fn *filenode) Splice(repl inode) error {
+       repl, err := repl.Snapshot()
+       if err != nil {
+               return err
+       }
+       fn.parent.Lock()
+       defer fn.parent.Unlock()
+       fn.Lock()
+       defer fn.Unlock()
+       _, err = fn.parent.Child(fn.fileinfo.name, func(inode) (inode, error) { return repl, nil })
+       if err != nil {
+               return err
+       }
+       switch repl := repl.(type) {
+       case *dirnode:
+               repl.parent = fn.parent
+               repl.fileinfo.name = fn.fileinfo.name
+               repl.setTreeFS(fn.fs)
+       case *filenode:
+               repl.parent = fn.parent
+               repl.fileinfo.name = fn.fileinfo.name
+               repl.fs = fn.fs
+       default:
+               return fmt.Errorf("cannot splice snapshot containing %T: %w", repl, ErrInvalidArgument)
+       }
+       return nil
+}
+
 type dirnode struct {
        fs *collectionFileSystem
        treenode
@@ -1489,6 +1538,86 @@ func (dn *dirnode) TreeSize() (bytes int64) {
        return
 }
 
+func (dn *dirnode) Snapshot() (inode, error) {
+       return dn.snapshot()
+}
+
+func (dn *dirnode) snapshot() (*dirnode, error) {
+       dn.RLock()
+       defer dn.RUnlock()
+       snap := &dirnode{
+               treenode: treenode{
+                       inodes:   make(map[string]inode, len(dn.inodes)),
+                       fileinfo: dn.fileinfo,
+               },
+       }
+       for name, child := range dn.inodes {
+               dupchild, err := child.Snapshot()
+               if err != nil {
+                       return nil, err
+               }
+               snap.inodes[name] = dupchild
+               dupchild.SetParent(snap, name)
+       }
+       return snap, nil
+}
+
+func (dn *dirnode) Splice(repl inode) error {
+       repl, err := repl.Snapshot()
+       if err != nil {
+               return err
+       }
+       switch repl := repl.(type) {
+       default:
+               return fmt.Errorf("cannot splice snapshot containing %T: %w", repl, ErrInvalidArgument)
+       case *dirnode:
+               dn.Lock()
+               defer dn.Unlock()
+               dn.inodes = repl.inodes
+               dn.setTreeFS(dn.fs)
+       case *filenode:
+               dn.parent.Lock()
+               defer dn.parent.Unlock()
+               removing, err := dn.parent.Child(dn.fileinfo.name, nil)
+               if err != nil {
+                       return fmt.Errorf("cannot use Splice to replace a top-level directory with a file: %w", ErrInvalidOperation)
+               } else if removing != dn {
+                       // If ../thisdirname is not this dirnode, it
+                       // must be an inode that wraps a dirnode, like
+                       // a collectionFileSystem or deferrednode.
+                       if deferred, ok := removing.(*deferrednode); ok {
+                               // More useful to report the type of
+                               // the wrapped node rather than just
+                               // *deferrednode. (We know the real
+                               // inode is already loaded because dn
+                               // is inside it.)
+                               removing = deferred.realinode()
+                       }
+                       return fmt.Errorf("cannot use Splice to attach a file at top level of %T: %w", removing, ErrInvalidOperation)
+               }
+               dn.Lock()
+               defer dn.Unlock()
+               _, err = dn.parent.Child(dn.fileinfo.name, func(inode) (inode, error) { return repl, nil })
+               if err != nil {
+                       return err
+               }
+               repl.fs = dn.fs
+       }
+       return nil
+}
+
+func (dn *dirnode) setTreeFS(fs *collectionFileSystem) {
+       dn.fs = fs
+       for _, child := range dn.inodes {
+               switch child := child.(type) {
+               case *dirnode:
+                       child.setTreeFS(fs)
+               case *filenode:
+                       child.fs = fs
+               }
+       }
+}
+
 type segment interface {
        io.ReaderAt
        Len() int
index bb6c7a26263e23b1cdb733d6d1d00a9ee129175a..66a126a39c12a45620a93c59a9047ac3d4ae1fe8 100644 (file)
@@ -113,3 +113,5 @@ func (dn *deferrednode) RUnlock()                        { dn.realinode().RUnloc
 func (dn *deferrednode) FS() FileSystem                  { return dn.currentinode().FS() }
 func (dn *deferrednode) Parent() inode                   { return dn.currentinode().Parent() }
 func (dn *deferrednode) MemorySize() int64               { return dn.currentinode().MemorySize() }
+func (dn *deferrednode) Snapshot() (inode, error)        { return dn.realinode().Snapshot() }
+func (dn *deferrednode) Splice(repl inode) error         { return dn.realinode().Splice(repl) }
index 9af8d0ad405828b0c8e9906575cb1b77752eaa63..4530a7b06a4d58231f2b4d95287ee792c977431a 100644 (file)
@@ -110,3 +110,18 @@ func (f *filehandle) Sync() error {
        // Sync the containing filesystem.
        return f.FS().Sync()
 }
+
+func (f *filehandle) Snapshot() (*Subtree, error) {
+       if !f.readable {
+               return nil, ErrInvalidOperation
+       }
+       node, err := f.inode.Snapshot()
+       return &Subtree{inode: node}, err
+}
+
+func (f *filehandle) Splice(r *Subtree) error {
+       if !f.writable {
+               return ErrReadOnlyFile
+       }
+       return f.inode.Splice(r.inode)
+}
index 966fe9d5c04187829ebab9f065ea765355386fff..ddff0c52e709b50ba00db31225c5bd549866ea9c 100644 (file)
@@ -24,7 +24,7 @@ func (*getternode) IsDir() bool {
 }
 
 func (*getternode) Child(string, func(inode) (inode, error)) (inode, error) {
-       return nil, ErrInvalidArgument
+       return nil, ErrInvalidOperation
 }
 
 func (gn *getternode) get() error {
index 021e8241cfeda647334781499ac60396e0961ac1..471dc69c82f75318d290eaccfa7b8d4a542540f1 100644 (file)
@@ -68,7 +68,7 @@ func (ln *lookupnode) Readdir() ([]os.FileInfo, error) {
        return ln.treenode.Readdir()
 }
 
-// Child rejects (with ErrInvalidArgument) calls to add/replace
+// Child rejects (with ErrInvalidOperation) calls to add/replace
 // children, instead calling loadOne when a non-existing child is
 // looked up.
 func (ln *lookupnode) Child(name string, replace func(inode) (inode, error)) (inode, error) {
@@ -97,12 +97,12 @@ func (ln *lookupnode) Child(name string, replace func(inode) (inode, error)) (in
        if replace != nil {
                // Let the callback try to delete or replace the
                // existing node; if it does, return
-               // ErrInvalidArgument.
+               // ErrInvalidOperation.
                if tryRepl, err := replace(existing); err != nil {
                        // Propagate error from callback
                        return existing, err
                } else if tryRepl != existing {
-                       return existing, ErrInvalidArgument
+                       return existing, ErrInvalidOperation
                }
        }
        // Return original error from ln.treenode.Child() (it might be
index 89435132713b0a14a8438d1ebe07390beb4a8893..8e7f58815629478f243396472fb92b258fe3ac98 100644 (file)
@@ -7,6 +7,7 @@ package arvados
 import (
        "bytes"
        "encoding/json"
+       "errors"
        "io"
        "os"
        "path/filepath"
@@ -311,17 +312,37 @@ func (s *SiteFSSuite) TestProjectUnsupportedOperations(c *check.C) {
        s.fs.MountProject("home", "")
 
        _, err := s.fs.OpenFile("/home/A Project/newfilename", os.O_CREATE|os.O_RDWR, 0)
-       c.Check(err, check.ErrorMatches, "invalid argument")
+       c.Check(err, ErrorIs, ErrInvalidOperation)
 
        err = s.fs.Mkdir("/home/A Project/newdirname", 0)
-       c.Check(err, check.ErrorMatches, "invalid argument")
+       c.Check(err, ErrorIs, ErrInvalidOperation)
 
        err = s.fs.Mkdir("/by_id/newdirname", 0)
-       c.Check(err, check.ErrorMatches, "invalid argument")
+       c.Check(err, ErrorIs, ErrInvalidOperation)
 
        err = s.fs.Mkdir("/by_id/"+fixtureAProjectUUID+"/newdirname", 0)
-       c.Check(err, check.ErrorMatches, "invalid argument")
+       c.Check(err, ErrorIs, ErrInvalidOperation)
 
        _, err = s.fs.OpenFile("/home/A Project", 0, 0)
        c.Check(err, check.IsNil)
 }
+
+type errorIsChecker struct {
+       *check.CheckerInfo
+}
+
+var ErrorIs check.Checker = errorIsChecker{
+       &check.CheckerInfo{Name: "ErrorIs", Params: []string{"value", "target"}},
+}
+
+func (checker errorIsChecker) Check(params []interface{}, names []string) (result bool, errStr string) {
+       err, ok := params[0].(error)
+       if !ok {
+               return false, ""
+       }
+       target, ok := params[1].(error)
+       if !ok {
+               return false, ""
+       }
+       return errors.Is(err, target), ""
+}
index 5225df59ee58ec4c2017054f59ca68ee7936e41e..3892be1e9a97610522a1fc219d5c0fb807788e4c 100644 (file)
@@ -133,7 +133,7 @@ func (fs *customFileSystem) Stale(t time.Time) bool {
 }
 
 func (fs *customFileSystem) newNode(name string, perm os.FileMode, modTime time.Time) (node inode, err error) {
-       return nil, ErrInvalidArgument
+       return nil, ErrInvalidOperation
 }
 
 func (fs *customFileSystem) mountByID(parent inode, id string) inode {
@@ -179,7 +179,7 @@ func (fs *customFileSystem) newProjectNode(root inode, name, uuid string) inode
        }
 }
 
-// vdirnode wraps an inode by rejecting (with ErrInvalidArgument)
+// vdirnode wraps an inode by rejecting (with ErrInvalidOperation)
 // calls that add/replace children directly, instead calling a
 // create() func when a non-existing child is looked up.
 //
@@ -204,7 +204,7 @@ func (vn *vdirnode) Child(name string, replace func(inode) (inode, error)) (inod
                } else if tryRepl, err := replace(existing); err != nil {
                        return existing, err
                } else if tryRepl != existing {
-                       return existing, ErrInvalidArgument
+                       return existing, ErrInvalidOperation
                } else {
                        return existing, nil
                }
index 51ca88764e6625dd25428a526f325e6acf138af8..59fa5fc17616f692e29a7565972c63b4cbfb88cc 100644 (file)
@@ -5,8 +5,12 @@
 package arvados
 
 import (
+       "fmt"
+       "io"
+       "io/ioutil"
        "net/http"
        "os"
+       "syscall"
        "time"
 
        check "gopkg.in/check.v1"
@@ -131,16 +135,236 @@ func (s *SiteFSSuite) TestByUUIDAndPDH(c *check.C) {
        c.Check(names, check.DeepEquals, []string{"baz"})
 
        _, err = s.fs.OpenFile("/by_id/"+fixtureNonexistentCollection, os.O_RDWR|os.O_CREATE, 0755)
-       c.Check(err, check.Equals, ErrInvalidArgument)
+       c.Check(err, ErrorIs, ErrInvalidOperation)
        err = s.fs.Rename("/by_id/"+fixtureFooCollection, "/by_id/beep")
-       c.Check(err, check.Equals, ErrInvalidArgument)
+       c.Check(err, ErrorIs, ErrInvalidOperation)
        err = s.fs.Rename("/by_id/"+fixtureFooCollection+"/foo", "/by_id/beep")
-       c.Check(err, check.Equals, ErrInvalidArgument)
+       c.Check(err, ErrorIs, ErrInvalidOperation)
        _, err = s.fs.Stat("/by_id/beep")
        c.Check(err, check.Equals, os.ErrNotExist)
        err = s.fs.Rename("/by_id/"+fixtureFooCollection+"/foo", "/by_id/"+fixtureFooCollection+"/bar")
        c.Check(err, check.IsNil)
 
        err = s.fs.Rename("/by_id", "/beep")
-       c.Check(err, check.Equals, ErrInvalidArgument)
+       c.Check(err, ErrorIs, ErrInvalidOperation)
+}
+
+// Copy subtree from OS src to dst path inside fs. If src is a
+// directory, dst must exist and be a directory.
+func copyFromOS(fs FileSystem, dst, src string) error {
+       inf, err := os.Open(src)
+       if err != nil {
+               return err
+       }
+       defer inf.Close()
+       dirents, err := inf.Readdir(-1)
+       if e, ok := err.(*os.PathError); ok {
+               if e, ok := e.Err.(syscall.Errno); ok {
+                       if e == syscall.ENOTDIR {
+                               err = syscall.ENOTDIR
+                       }
+               }
+       }
+       if err == syscall.ENOTDIR {
+               outf, err := fs.OpenFile(dst, os.O_CREATE|os.O_EXCL|os.O_TRUNC|os.O_WRONLY, 0700)
+               if err != nil {
+                       return fmt.Errorf("open %s: %s", dst, err)
+               }
+               defer outf.Close()
+               _, err = io.Copy(outf, inf)
+               if err != nil {
+                       return fmt.Errorf("%s: copying data from %s: %s", dst, src, err)
+               }
+               err = outf.Close()
+               if err != nil {
+                       return err
+               }
+       } else if err != nil {
+               return fmt.Errorf("%s: readdir: %T %s", src, err, err)
+       } else {
+               {
+                       d, err := fs.Open(dst)
+                       if err != nil {
+                               return fmt.Errorf("opendir(%s): %s", dst, err)
+                       }
+                       d.Close()
+               }
+               for _, ent := range dirents {
+                       if ent.Name() == "." || ent.Name() == ".." {
+                               continue
+                       }
+                       dstname := dst + "/" + ent.Name()
+                       if ent.IsDir() {
+                               err = fs.Mkdir(dstname, 0700)
+                               if err != nil {
+                                       return fmt.Errorf("mkdir %s: %s", dstname, err)
+                               }
+                       }
+                       err = copyFromOS(fs, dstname, src+"/"+ent.Name())
+                       if err != nil {
+                               return err
+                       }
+               }
+       }
+       return nil
+}
+
+func (s *SiteFSSuite) TestSnapshotSplice(c *check.C) {
+       s.fs.MountProject("home", "")
+       thisfile, err := ioutil.ReadFile("fs_site_test.go")
+       c.Assert(err, check.IsNil)
+
+       var src1 Collection
+       err = s.client.RequestAndDecode(&src1, "POST", "arvados/v1/collections", nil, map[string]interface{}{
+               "collection": map[string]string{
+                       "name":       "TestSnapshotSplice src1",
+                       "owner_uuid": fixtureAProjectUUID,
+               },
+       })
+       c.Assert(err, check.IsNil)
+       defer s.client.RequestAndDecode(nil, "DELETE", "arvados/v1/collections/"+src1.UUID, nil, nil)
+       err = s.fs.Sync()
+       c.Assert(err, check.IsNil)
+       err = copyFromOS(s.fs, "/home/A Project/TestSnapshotSplice src1", "..") // arvados.git/sdk/go
+       c.Assert(err, check.IsNil)
+
+       var src2 Collection
+       err = s.client.RequestAndDecode(&src2, "POST", "arvados/v1/collections", nil, map[string]interface{}{
+               "collection": map[string]string{
+                       "name":       "TestSnapshotSplice src2",
+                       "owner_uuid": fixtureAProjectUUID,
+               },
+       })
+       c.Assert(err, check.IsNil)
+       defer s.client.RequestAndDecode(nil, "DELETE", "arvados/v1/collections/"+src2.UUID, nil, nil)
+       err = s.fs.Sync()
+       c.Assert(err, check.IsNil)
+       err = copyFromOS(s.fs, "/home/A Project/TestSnapshotSplice src2", "..") // arvados.git/sdk/go
+       c.Assert(err, check.IsNil)
+
+       var dst Collection
+       err = s.client.RequestAndDecode(&dst, "POST", "arvados/v1/collections", nil, map[string]interface{}{
+               "collection": map[string]string{
+                       "name":       "TestSnapshotSplice dst",
+                       "owner_uuid": fixtureAProjectUUID,
+               },
+       })
+       c.Assert(err, check.IsNil)
+       defer s.client.RequestAndDecode(nil, "DELETE", "arvados/v1/collections/"+dst.UUID, nil, nil)
+       err = s.fs.Sync()
+       c.Assert(err, check.IsNil)
+
+       dstPath := "/home/A Project/TestSnapshotSplice dst"
+       err = copyFromOS(s.fs, dstPath, "..") // arvados.git/sdk/go
+       c.Assert(err, check.IsNil)
+
+       // Snapshot directory
+       snap1, err := Snapshot(s.fs, "/home/A Project/TestSnapshotSplice src1/ctxlog")
+       c.Check(err, check.IsNil)
+       // Attach same snapshot twice, at paths that didn't exist before
+       err = Splice(s.fs, dstPath+"/ctxlog-copy", snap1)
+       c.Check(err, check.IsNil)
+       err = Splice(s.fs, dstPath+"/ctxlog-copy2", snap1)
+       c.Check(err, check.IsNil)
+       // Splicing a snapshot twice results in two independent copies
+       err = s.fs.Rename(dstPath+"/ctxlog-copy2/log.go", dstPath+"/ctxlog-copy/log2.go")
+       c.Check(err, check.IsNil)
+       _, err = s.fs.Open(dstPath + "/ctxlog-copy2/log.go")
+       c.Check(err, check.Equals, os.ErrNotExist)
+       f, err := s.fs.Open(dstPath + "/ctxlog-copy/log.go")
+       if c.Check(err, check.IsNil) {
+               buf, err := ioutil.ReadAll(f)
+               c.Check(err, check.IsNil)
+               c.Check(string(buf), check.Not(check.Equals), "")
+               f.Close()
+       }
+
+       // Snapshot regular file
+       snapFile, err := Snapshot(s.fs, "/home/A Project/TestSnapshotSplice src1/arvados/fs_site_test.go")
+       c.Check(err, check.IsNil)
+       // Replace dir with file
+       err = Splice(s.fs, dstPath+"/ctxlog-copy2", snapFile)
+       c.Check(err, check.IsNil)
+       if f, err := s.fs.Open(dstPath + "/ctxlog-copy2"); c.Check(err, check.IsNil) {
+               buf, err := ioutil.ReadAll(f)
+               c.Check(err, check.IsNil)
+               c.Check(string(buf), check.Equals, string(thisfile))
+       }
+
+       // Cannot splice a file onto a collection root, or anywhere
+       // outside a collection
+       for _, badpath := range []string{
+               dstPath,
+               "/home/A Project/newnodename",
+               "/home/A Project",
+               "/home/newnodename",
+               "/home",
+               "/newnodename",
+       } {
+               err = Splice(s.fs, badpath, snapFile)
+               c.Check(err, check.NotNil)
+               c.Check(err, ErrorIs, ErrInvalidOperation, check.Commentf("badpath %s"))
+               if badpath == dstPath {
+                       c.Check(err, check.ErrorMatches, `cannot use Splice to attach a file at top level of \*arvados.collectionFileSystem: invalid operation`, check.Commentf("badpath: %s", badpath))
+                       continue
+               }
+               err = Splice(s.fs, badpath, snap1)
+               c.Check(err, ErrorIs, ErrInvalidOperation, check.Commentf("badpath %s"))
+       }
+
+       // Destination cannot have trailing slash
+       for _, badpath := range []string{
+               dstPath + "/ctxlog/",
+               dstPath + "/",
+               "/home/A Project/",
+               "/home/",
+               "/",
+               "",
+       } {
+               err = Splice(s.fs, badpath, snap1)
+               c.Check(err, ErrorIs, ErrInvalidArgument, check.Commentf("badpath %s", badpath))
+               err = Splice(s.fs, badpath, snapFile)
+               c.Check(err, ErrorIs, ErrInvalidArgument, check.Commentf("badpath %s", badpath))
+       }
+
+       // Destination's parent must already exist
+       for _, badpath := range []string{
+               dstPath + "/newdirname/",
+               dstPath + "/newdirname/foobar",
+               "/foo/bar",
+       } {
+               err = Splice(s.fs, badpath, snap1)
+               c.Check(err, ErrorIs, os.ErrNotExist, check.Commentf("badpath %s", badpath))
+               err = Splice(s.fs, badpath, snapFile)
+               c.Check(err, ErrorIs, os.ErrNotExist, check.Commentf("badpath %s", badpath))
+       }
+
+       snap2, err := Snapshot(s.fs, dstPath+"/ctxlog-copy")
+       c.Check(err, check.IsNil)
+       err = Splice(s.fs, dstPath+"/ctxlog-copy-copy", snap2)
+       c.Check(err, check.IsNil)
+
+       // Snapshot entire collection, splice into same collection at
+       // a new path, remove file from original location, verify
+       // spliced content survives
+       snapDst, err := Snapshot(s.fs, dstPath+"")
+       c.Check(err, check.IsNil)
+       err = Splice(s.fs, dstPath+"", snapDst)
+       c.Check(err, check.IsNil)
+       err = Splice(s.fs, dstPath+"/copy1", snapDst)
+       c.Check(err, check.IsNil)
+       err = Splice(s.fs, dstPath+"/copy2", snapDst)
+       c.Check(err, check.IsNil)
+       err = s.fs.RemoveAll(dstPath + "/arvados/fs_site_test.go")
+       c.Check(err, check.IsNil)
+       err = s.fs.RemoveAll(dstPath + "/arvados")
+       c.Check(err, check.IsNil)
+       _, err = s.fs.Open(dstPath + "/arvados/fs_site_test.go")
+       c.Check(err, check.Equals, os.ErrNotExist)
+       f, err = s.fs.Open(dstPath + "/copy2/arvados/fs_site_test.go")
+       c.Check(err, check.IsNil)
+       defer f.Close()
+       buf, err := ioutil.ReadAll(f)
+       c.Check(err, check.IsNil)
+       c.Check(string(buf), check.Equals, string(thisfile))
 }
index 7c7ed759c60058b5915ad1d56505dba6b56d84dd..f8454029d6b8cf2561505080ac5b74b8d57b8c70 100644 (file)
@@ -111,6 +111,31 @@ class ApiClientAuthorization < ArvadosModel
     clnt
   end
 
+  def self.check_anonymous_user_token token
+    case token[0..2]
+    when 'v2/'
+      _, token_uuid, secret, optional = token.split('/')
+      unless token_uuid.andand.length == 27 && secret.andand.length.andand > 0 &&
+             token_uuid == Rails.configuration.ClusterID+"-gj3su-anonymouspublic"
+        # invalid v2 token, or v2 token for another user
+        return nil
+      end
+    else
+      # v1 token
+      secret = token
+    end
+
+    # The anonymous token content and minimum length is verified in lib/config
+    if secret.length >= 0 && secret == Rails.configuration.Users.AnonymousUserToken
+      return ApiClientAuthorization.new(user: User.find_by_uuid(anonymous_user_uuid),
+                                        uuid: Rails.configuration.ClusterID+"-gj3su-anonymouspublic",
+                                        api_token: token,
+                                        api_client: anonymous_user_token_api_client)
+    else
+      return nil
+    end
+  end
+
   def self.check_system_root_token token
     if token == Rails.configuration.SystemRootToken
       return ApiClientAuthorization.new(user: User.find_by_uuid(system_user_uuid),
@@ -126,6 +151,11 @@ class ApiClientAuthorization < ArvadosModel
     return nil if token.nil? or token.empty?
     remote ||= Rails.configuration.ClusterID
 
+    auth = self.check_anonymous_user_token(token)
+    if !auth.nil?
+      return auth
+    end
+
     auth = self.check_system_root_token(token)
     if !auth.nil?
       return auth
index 67bd3d10d78975cd942a32acc7bb49306d31e0cc..e0ae850ae7b10412eaa5a7578601de5da0f5e064 100644 (file)
@@ -14,6 +14,7 @@ class DatabaseSeeds
       anonymous_group
       anonymous_group_read_permission
       anonymous_user
+      anonymous_user_token_api_client
       system_root_token_api_client
       public_project_group
       public_project_read_permission
index 37e86976c1d9c5032d1948b415290069def7e1b3..ee666b77ab78632f843211fc9e510f9dd11f564c 100644 (file)
@@ -225,6 +225,16 @@ module CurrentApiClient
     end
   end
 
+  def anonymous_user_token_api_client
+    $anonymous_user_token_api_client = check_cache $anonymous_user_token_api_client do
+      act_as_system_user do
+        ActiveRecord::Base.transaction do
+          ApiClient.find_or_create_by!(is_trusted: false, url_prefix: "", name: "AnonymousUserToken")
+        end
+      end
+    end
+  end
+
   def system_root_token_api_client
     $system_root_token_api_client = check_cache $system_root_token_api_client do
       act_as_system_user do
diff --git a/services/api/script/get_anonymous_user_token.rb b/services/api/script/get_anonymous_user_token.rb
deleted file mode 100755 (executable)
index 4c3ca34..0000000
+++ /dev/null
@@ -1,85 +0,0 @@
-#!/usr/bin/env ruby
-# Copyright (C) The Arvados Authors. All rights reserved.
-#
-# SPDX-License-Identifier: AGPL-3.0
-
-# Get or Create an anonymous user token.
-# If get option is used, an existing anonymous user token is returned. If none exist, one is created.
-# If the get option is omitted, a new token is created and returned.
-
-require 'optimist'
-
-opts = Optimist::options do
-  banner ''
-  banner "Usage: get_anonymous_user_token "
-  banner ''
-  opt :get, <<-eos
-Get an existing anonymous user token. If no such token exists \
-or if this option is omitted, a new token is created and returned.
-  eos
-  opt :token, "token to create (optional)", :type => :string
-end
-
-get_existing = opts[:get]
-supplied_token = opts[:token]
-
-require File.dirname(__FILE__) + '/../config/environment'
-
-include ApplicationHelper
-act_as_system_user
-
-def create_api_client_auth(supplied_token=nil)
-  supplied_token = Rails.configuration.Users["AnonymousUserToken"]
-
-  if supplied_token.nil? or supplied_token.empty?
-    puts "Users.AnonymousUserToken is empty.  Destroying tokens that belong to anonymous."
-    # Token is empty.  Destroy any anonymous tokens.
-    ApiClientAuthorization.where(user: anonymous_user).destroy_all
-    return nil
-  end
-
-  attr = {user: anonymous_user,
-          api_client_id: 0,
-          scopes: ['GET /']}
-
-  secret = supplied_token
-
-  if supplied_token[0..2] == 'v2/'
-    _, token_uuid, secret, optional = supplied_token.split('/')
-    if token_uuid[0..4] != Rails.configuration.ClusterID
-      # Belongs to a different cluster.
-      puts supplied_token
-      return nil
-    end
-    attr[:uuid] = token_uuid
-  end
-
-  attr[:api_token] = secret
-
-  api_client_auth = ApiClientAuthorization.where(attr).first
-  if !api_client_auth
-    # The anonymous user token should never expire but we are not allowed to
-    # set :expires_at to nil, so we set it to 1000 years in the future.
-    attr[:expires_at] = Time.now + 1000.years
-    api_client_auth = ApiClientAuthorization.create!(attr)
-  end
-  api_client_auth
-end
-
-if get_existing
-  api_client_auth = ApiClientAuthorization.
-    where('user_id=?', anonymous_user.id.to_i).
-    where('expires_at>?', Time.now).
-    select { |auth| auth.scopes == ['GET /'] }.
-    first
-end
-
-# either not a get or no api_client_auth was found
-if !api_client_auth
-  api_client_auth = create_api_client_auth(supplied_token)
-end
-
-# print it to the console
-if api_client_auth
-  puts "v2/#{api_client_auth.uuid}/#{api_client_auth.api_token}"
-end
index e6262374d640f9ed526ef38c816fa785bce1d3d5..5af7ebb5d52cfe17022ce46ed1e50fed507ca93f 100644 (file)
@@ -471,7 +471,7 @@ func (h *handler) serveS3(w http.ResponseWriter, r *http.Request) bool {
                                        return true
                                }
                                err = fs.Mkdir(dir, 0755)
-                               if err == arvados.ErrInvalidArgument {
+                               if errors.Is(err, arvados.ErrInvalidArgument) || errors.Is(err, arvados.ErrInvalidOperation) {
                                        // Cannot create a directory
                                        // here.
                                        err = fmt.Errorf("mkdir %q failed: %w", dir, err)
index f411fde871cdba0cc9bbb2584be9a9bb878c6c1e..966f39c28dece0a4e3b5807d44f24e33a2cbe7c9 100644 (file)
@@ -332,7 +332,7 @@ func (s *IntegrationSuite) TestS3ProjectPutObjectNotSupported(c *check.C) {
                err = bucket.PutReader(trial.path, bytes.NewReader(buf), int64(len(buf)), trial.contentType, s3.Private, s3.Options{})
                c.Check(err.(*s3.Error).StatusCode, check.Equals, 400)
                c.Check(err.(*s3.Error).Code, check.Equals, `InvalidArgument`)
-               c.Check(err, check.ErrorMatches, `(mkdir "/by_id/zzzzz-j7d0g-[a-z0-9]{15}/newdir2?"|open "/zzzzz-j7d0g-[a-z0-9]{15}/newfile") failed: invalid argument`)
+               c.Check(err, check.ErrorMatches, `(mkdir "/by_id/zzzzz-j7d0g-[a-z0-9]{15}/newdir2?"|open "/zzzzz-j7d0g-[a-z0-9]{15}/newfile") failed: invalid (argument|operation)`)
 
                _, err = bucket.GetReader(trial.path)
                c.Check(err.(*s3.Error).StatusCode, check.Equals, 404)
index 4af0cca0ed51ccade4f03353d3be450949029542..29cea1ecbe3ee2a5e39e04b66c64dab65f010a3a 100755 (executable)
@@ -56,16 +56,16 @@ EOF
 fi
 
 if ! test -f $ARVADOS_CONTAINER_PATH/api_database_setup ; then
-   bin/bundle exec rake db:setup
+   flock $GEMLOCK bin/bundle exec rake db:setup
    touch $ARVADOS_CONTAINER_PATH/api_database_setup
 fi
 
 if ! test -s $ARVADOS_CONTAINER_PATH/superuser_token ; then
-    superuser_tok=$(bin/bundle exec ./script/create_superuser_token.rb)
+    superuser_tok=$(flock $GEMLOCK bin/bundle exec ./script/create_superuser_token.rb)
     echo "$superuser_tok" > $ARVADOS_CONTAINER_PATH/superuser_token
 fi
 
 rm -rf tmp
 mkdir -p tmp/cache
 
-bin/bundle exec rake db:migrate
+flock $GEMLOCK bin/bundle exec rake db:migrate
index f391376f39b8f6cd1aea68c4f3e1389fb45d2462..c44e7c410151d772291e25b267b368439f6867a3 100644 (file)
@@ -12,6 +12,7 @@ export npm_config_cache_min=Infinity
 export R_LIBS=/var/lib/Rlibs
 export HOME=$(getent passwd arvbox | cut -d: -f6)
 export ARVADOS_CONTAINER_PATH=/var/lib/arvados-arvbox
+GEMLOCK=/var/lib/arvados/lib/ruby/gems/gems.lock
 
 defaultdev=$(/sbin/ip route|awk '/default/ { print $5 }')
 dockerip=$(/sbin/ip route | grep default | awk '{ print $3 }')
@@ -62,7 +63,6 @@ else
 fi
 
 run_bundler() {
-    GEMLOCK=/var/lib/arvados/lib/ruby/gems/gems.lock
     flock $GEMLOCK /var/lib/arvados/bin/gem install --no-document bundler:$BUNDLER_VERSION
     if test -f Gemfile.lock ; then
         frozen=--frozen
index aa7e0822e2bb67480cef6a405ddac86086c60ab6..e9092f37022c7020e376fbb4f6c95b07e1af0804 100755 (executable)
@@ -29,4 +29,4 @@ if test "$1" = "--only-deps" ; then
 fi
 
 cd /usr/src/arvados/doc
-bundle exec rake generate baseurl=http://$localip:${services[doc]} arvados_api_host=$localip:${services[controller-ssl]} arvados_workbench_host=http://$localip
+flock $GEMLOCK bundle exec rake generate baseurl=http://$localip:${services[doc]} arvados_api_host=$localip:${services[controller-ssl]} arvados_workbench_host=http://$localip
index 55aff375fc73ea0f72469433727fda9f226b6996..5f2cbc8825f9fe94307491aca748cc26e4f8f1c5 100755 (executable)
@@ -125,6 +125,6 @@ $RAILS_ENV:
 EOF
 
 while true ; do
-    bundle exec script/arvados-git-sync.rb $RAILS_ENV
+    flock $GEMLOCK bundle exec script/arvados-git-sync.rb $RAILS_ENV
     sleep 120
 done
index 20ef9fcaf6397d0829d6a40d712879cc992c3946..d8332eba93c2b591c4106f1bd31dc19dc8a56214 100755 (executable)
@@ -34,7 +34,7 @@ cat >config/application.yml <<EOF
 $RAILS_ENV:
   keep_web_url: https://example.com/c=%{uuid_or_pdh}
 EOF
-   RAILS_GROUPS=assets bin/bundle exec rake npm:install
+   RAILS_GROUPS=assets flock $GEMLOCK bin/bundle exec rake npm:install
    rm config/application.yml
    exit
 fi
@@ -43,5 +43,5 @@ set -u
 
 secret_token=$(cat $ARVADOS_CONTAINER_PATH/workbench_secret_token)
 
-RAILS_GROUPS=assets bin/bundle exec rake npm:install
-bin/bundle exec rake assets:precompile
+RAILS_GROUPS=assets flock $GEMLOCK bin/bundle exec rake npm:install
+flock $GEMLOCK bin/bundle exec rake assets:precompile