18676: Merge branch 'main' into 18676-anon-token-support-v2-in-config
authorWard Vandewege <ward@curii.com>
Fri, 18 Feb 2022 14:42:15 +0000 (09:42 -0500)
committerWard Vandewege <ward@curii.com>
Fri, 18 Feb 2022 14:42:15 +0000 (09:42 -0500)
Arvados-DCO-1.1-Signed-off-by: Ward Vandewege <ward@curii.com>

24 files changed:
doc/_config.yml
doc/admin/config-migration.html.textile.liquid [deleted file]
doc/admin/config.html.textile.liquid
doc/admin/maintenance-and-upgrading.html.textile.liquid [new file with mode: 0644]
doc/admin/upgrading.html.textile.liquid
doc/install/salt-multi-host.html.textile.liquid
doc/install/salt-single-host.html.textile.liquid
lib/crunchrun/crunchrun_test.go
lib/mount/fs.go
sdk/cwl/setup.py
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/keep-web/s3.go
services/keep-web/s3_test.go
tools/arvbox/bin/arvbox
tools/arvbox/lib/arvbox/docker/common.sh
tools/arvbox/lib/arvbox/docker/service/ready/run-service

index eb0f173dfbcefae5c4c609026363ec1c93127b38..f2ddd7f58c11a246accc2fec6c01c72d86d9a633 100644 (file)
@@ -218,12 +218,13 @@ navbar:
     - Manual installation:
       - install/install-manual-prerequisites.html.textile.liquid
       - install/packages.html.textile.liquid
-      - admin/upgrading.html.textile.liquid
     - Configuration:
       - install/config.html.textile.liquid
-      - admin/config-migration.html.textile.liquid
-      - admin/config.html.textile.liquid
       - admin/config-urls.html.textile.liquid
+      - admin/config.html.textile.liquid
+    - Maintenance and upgrading:
+      - admin/upgrading.html.textile.liquid
+      - admin/maintenance-and-upgrading.html.textile.liquid
     - Core:
       - install/install-api-server.html.textile.liquid
     - Keep:
diff --git a/doc/admin/config-migration.html.textile.liquid b/doc/admin/config-migration.html.textile.liquid
deleted file mode 100644 (file)
index e36655a..0000000
+++ /dev/null
@@ -1,106 +0,0 @@
----
-layout: default
-navsection: installguide
-title: Migrating Configuration from v1.4 to v2.0
-...
-
-{% comment %}
-Copyright (C) The Arvados Authors. All rights reserved.
-
-SPDX-License-Identifier: CC-BY-SA-3.0
-{% endcomment %}
-
-{% include 'notebox_begin_warning' %}
-_New installations of Arvados 2.0+ can skip this section_
-{% include 'notebox_end' %}
-
-Arvados 2.0 migrates to a centralized configuration file for all components.  The centralized Arvados configuration is @/etc/arvados/config.yml@.  Components that support the new centralized configuration are listed below.  During the migration period, legacy configuration files are still loaded and take precedence over the centralized configuration file.
-
-h2. API server
-
-The legacy API server configuration is stored in @config/application.yml@ and @config/database.yml@.  After migration to @/etc/arvados/config.yml@, both of these files should be moved out of the way and/or deleted.
-
-Change to the API server directory and use the following commands:
-
-<pre>
-$ RAILS_ENV=production bin/rake config:migrate > config.yml
-$ cp config.yml /etc/arvados/config.yml
-</pre>
-
-This will print the contents of @config.yml@ after merging the legacy @application.yml@ and @database.yml@ into the existing systemwide @config.yml@.  It may be redirected to a file and copied to @/etc/arvados/config.yml@ (it is safe to copy over, all configuration items from the existing @/etc/arvados/config.yml@ will be included in the migrated output).
-
-If you wish to update @config.yml@ configuration by hand, or check that everything has been migrated, use @config:diff@ to print configuration items that differ between @application.yml@ and the system @config.yml@.
-
-<pre>
-$ RAILS_ENV=production bin/rake config:diff
-</pre>
-
-This command will also report if no migrations are required.
-
-h2. Workbench
-
-The legacy workbench configuration is stored in @config/application.yml@.  After migration to @/etc/arvados/config.yml@, this file should be moved out of the way and/or deleted.
-
-Change to the workbench server directory and use the following commands:
-
-<pre>
-$ RAILS_ENV=production bin/rake config:migrate > config.yml
-$ cp config.yml /etc/arvados/config.yml
-</pre>
-
-This will print the contents of @config.yml@ after merging the legacy @application.yml@ into the existing systemwide @config.yml@.  It may be redirected to a file and copied to @/etc/arvados/config.yml@ (it is safe to copy over, all configuration items from the existing @/etc/arvados/config.yml@ will be included in the migrated output).
-
-If you wish to update @config.yml@ configuration by hand, or check that everything has been migrated, use @config:diff@ to print configuration items that differ between @application.yml@ and the system @config.yml@.
-
-<pre>
-$ RAILS_ENV=production bin/rake config:diff
-</pre>
-
-This command will also report if no migrations are required.
-
-h2. keepstore, keep-web, crunch-dispatch-slurm, arvados-ws, keepproxy, arv-git-httpd, keep-balance
-
-The legacy config for each component (loaded from @/etc/arvados/component/component.yml@ or a different location specified via the -legacy-component-config command line argument) takes precedence over the centralized config. After you migrate everything from the legacy config to the centralized config, you should delete @/etc/arvados/component/component.yml@ and/or stop using the corresponding -legacy-component-config argument.
-
-To migrate a component configuration, do this on each node that runs an Arvados service:
-
-# Ensure that the latest @config.yml@ is installed on the current node
-# Install @arvados-server@ using @apt-get@ or @yum@.
-# Run @arvados-server config-check@, review and apply the recommended changes to @/etc/arvados/config.yml@
-# After applying changes, re-run @arvados-server config-check@ again to check for additional warnings and recommendations.
-# When you are satisfied, delete the legacy config file, restart the service, and check its startup logs.
-# Copy the updated @config.yml@ file to your next node, and repeat the process there.
-# When you have a @config.yml@ file that includes all volumes on all keepstores, it is important to add a 'Rendezvous' parameter to the InternalURLs entries to make sure the old volume identifiers line up with the new config. If you don't do this, @keep-balance@ will want to shuffle all the existing data around to match the new volume order. The 'Rendezvous' value should be the last 15 characters of the keepstore's UUID in the old configuration. Here's an example:
-
-<notextile>
-<pre><code>Clusters:
-  xxxxx:
-    Services:
-      Keepstore:
-        InternalURLs:
-          "http://keep1.xxxxx.arvadosapi.com:25107": {Rendezvous: "eim6eefaibesh3i"}
-          "http://keep2.xxxxx.arvadosapi.com:25107": {Rendezvous: "yequoodalai7ahg"}
-          "http://keep3.xxxxx.arvadosapi.com:25107": {Rendezvous: "eipheho6re1shou"}
-          "http://keep4.xxxxx.arvadosapi.com:25107": {Rendezvous: "ahk7chahthae3oo"}
-</code></pre>
-</notextile>
-
-In this example, the keepstore with the name `keep1` had the uuid `xxxxx-bi6l4-eim6eefaibesh3i` in the old configuration.
-
-After migrating and removing all legacy config files, make sure the @/etc/arvados/config.yml@ file is identical across all system nodes -- API server, keepstore, etc. -- and restart all services to make sure they are using the latest configuration.
-
-h2. Cloud installations only: node manager
-
-Node manager is deprecated and replaced by @arvados-dispatch-cloud@.  No automated config migration is available.  Follow the instructions to "install the cloud dispatcher":../install/crunch2-cloud/install-dispatch-cloud.html
-
-*Only one dispatch process should be running at a time.* If you are migrating a system that currently runs Node manager and @crunch-dispatch-slurm@, it is safest to remove the @crunch-dispatch-slurm@ service entirely before installing @arvados-dispatch-cloud@.
-
-<notextile>
-<pre><code>~$ <span class="userinput">sudo systemctl --now disable crunch-dispatch-slurm</span>
-~$ <span class="userinput">sudo apt-get remove crunch-dispatch-slurm</span>
-</code></pre>
-</notextile>
-
-h2. arvados-controller, arvados-dispatch-cloud
-
-Already uses centralized config exclusively.  No migration needed.
index 745cd2853265a096ad99b55bd6c53124feb22871..a043d6d1c09be1326f7a65b5e8f131db40b08ae9 100644 (file)
@@ -12,8 +12,6 @@ SPDX-License-Identifier: CC-BY-SA-3.0
 
 The Arvados configuration is stored at @/etc/arvados/config.yml@
 
-See "Migrating Configuration":config-migration.html for information about migrating from legacy component-specific configuration files.
-
 {% codeblock as yaml %}
 {% include 'config_default_yml' %}
 {% endcodeblock %}
diff --git a/doc/admin/maintenance-and-upgrading.html.textile.liquid b/doc/admin/maintenance-and-upgrading.html.textile.liquid
new file mode 100644 (file)
index 0000000..0d517fe
--- /dev/null
@@ -0,0 +1,64 @@
+---
+layout: default
+navsection: installguide
+title: Maintenance and upgrading
+...
+{% comment %}
+Copyright (C) The Arvados Authors. All rights reserved.
+
+SPDX-License-Identifier: CC-BY-SA-3.0
+{% endcomment %}
+
+# "Commercial support":#commercial_support
+# "Maintaining Arvados":#maintaining
+## "Modification of the config.yml file":#configuration
+## "Distributing the configuration file":#distribution
+## "Restart the services affected by the change":#restart
+# "Upgrading Arvados":#upgrading
+
+h2(#commercial_support). Commercial support
+
+Arvados is "100% open source software":{{site.baseurl}}/user/copying/copying.html. Anyone can download, install, maintain and upgrade it. However, if this is not something you want to spend your time and energy doing, "Curii Corporation":https://curii.com provides managed Arvados installations as well as commercial support for Arvados. Please contact "info@curii.com":mailto:info@curii.com for more information.
+
+If you'd prefer to do things yourself, a few starting points for maintaining and upgrading Arvados can be found below.
+
+h2(#maintaining). Maintaining Arvados
+
+After Arvados is installed, periodic configuration changes may be required to adapt the software to your needs. Arvados uses a unified configuration file, which is normally found at @/etc/arvados/config.yml@.
+
+Making a configuration change to Arvados typically involves three steps:
+
+* modification of the @config.yml@ file
+* distribution of the modified file to the machines in the cluster
+* restarting of the services affected by the change
+
+h3(#configchange). Modification of the @config.yml@ file
+
+Consult the "configuration reference":{{site.baseurl}}/admin/config.html or another part of the documentation to identify the change to be made.
+
+Preserve a copy of your existing configuration file as a backup, and make the desired modification.
+
+Run @arvados-server config-check@ to make sure the configuration file has no errors and no warnings.
+
+h3(#distribution). Distribute the configuration file
+
+We recommend to keep the @config.yml@ file in sync between all the Arvados system nodes, to avoid issues with services running on different versions of the configuration.
+
+Distribution of the configuration file can be done in many ways, e.g. scp, configuration management software, etc.
+
+h3(#restart). Restart the services affected by the change
+
+If you know which Arvados service uses the specific configuration that was modified, restart those services. When in doubt, restart all Arvados system services.
+
+h2(#upgrading). Upgrading Arvados
+
+Upgrading Arvados typically involves the following steps:
+
+# consult the "upgrade notes":{{site.baseurl}}/admin/upgrading.html and the "release notes":https://arvados.org/releases/ for the release you want to upgrade to
+# Wait for the cluster to be idle and stop Arvados services.
+# Make a backup of your database, as a precaution.
+# update the configuration file for the new release, if necessary (see "Maintaining Arvados":#maintaining above)
+# rebuild and deploy the "compute node image":{{site.baseurl}}/install/crunch2-cloud/install-compute-node.html (cloud only)
+# Install new packages using @apt-get upgrade@ or @yum upgrade@.
+# Wait for package installation scripts as they perform any necessary data migrations.
+# Verify that the Arvados services were restarted as part of the package upgrades.
index 61068ca24d0a2e92eeca4858eb4fee04ef47ef9a..943bc3e0ee7f9a420c1dfac438f755373ac52aae 100644 (file)
@@ -1,7 +1,7 @@
 ---
 layout: default
 navsection: installguide
-title: "Upgrading Arvados and Release notes"
+title: "Arvados upgrade notes"
 ...
 
 {% comment %}
@@ -10,20 +10,13 @@ Copyright (C) The Arvados Authors. All rights reserved.
 SPDX-License-Identifier: CC-BY-SA-3.0
 {% endcomment %}
 
-For Arvados administrators, this page will cover what you need to know and do in order to ensure a smooth upgrade of your Arvados installation.  For general release notes covering features added and bugs fixed, see "Arvados releases":https://arvados.org/releases .
+For Arvados administrators, this page will cover what you need to know and do in order to ensure a smooth upgrade of your Arvados installation.  For general release notes covering features added and bugs fixed, see "Arvados releases":https://arvados.org/releases.
 
-h2. General process
-
-# Consult upgrade notes below to see if any manual configuration updates are necessary.
-# Wait for the cluster to be idle and stop Arvados services.
-# Make a backup of your database, as a precaution.
-# Install new packages using @apt-get upgrade@ or @yum upgrade@.
-# Wait for package installation scripts as they perform any necessary data migrations.
-# Restart Arvados services.
+Upgrade instructions can be found at "Maintenance and upgrading":{{site.baseurl}}/admin/maintenance-and-upgrading.html#upgrading.
 
 h2. Upgrade notes
 
-Some versions introduce changes that require special attention when upgrading: e.g., there is a new service to install, or there is a change to the default configuration that you might need to override in order to preserve the old behavior.
+Some versions introduce changes that require special attention when upgrading: e.g., there is a new service to install, or there is a change to the default configuration that you might need to override in order to preserve the old behavior. These notes are listed below, organized by release version. Scroll down to the version number you are upgrading to.
 
 {% comment %}
 Note to developers: Add new items at the top. Include the date, issue number, commit, and considerations/instructions for those about to upgrade.
@@ -37,7 +30,7 @@ TODO: extract this information based on git commit messages and generate changel
 
 h2(#main). development main (as of 2022-02-10)
 
-"previous: Upgrading from 2.3.0":#v2_3_0
+"previous: Upgrading to 2.3.0":#v2_3_0
 
 h3. Anonymous token changes
 
@@ -238,7 +231,7 @@ Arvados 2.0 is a major upgrade, with many changes.  Please read these upgrade no
 
 h3. Migrating to centralized config.yml
 
-See "Migrating Configuration":config-migration.html for notes on migrating legacy per-component configuration files to the new centralized @/etc/arvados/config.yml@.
+See "Migrating Configuration":https://doc.arvados.org/v2.1/admin/config-migration.html for notes on migrating legacy per-component configuration files to the new centralized @/etc/arvados/config.yml@.
 
 To ensure a smooth transition, the per-component config files continue to be read, and take precedence over the centralized configuration.  Your cluster should continue to function after upgrade but before doing the full configuration migration.  However, several services (keepstore, keep-web, keepproxy) require a minimal `/etc/arvados/config.yml` to start:
 
@@ -258,11 +251,11 @@ You can no longer specify types of keep services to balance via the @KeepService
 
 You can no longer specify individual keep services to balance via the @config.KeepServiceList@ command line option or @KeepServiceList@ legacy config option. Instead, keep-balance will operate on all keepstore servers with @service_type:disk@ as reported by the @arv keep_service list@ command. If you are still using the legacy config, @KeepServiceList@ should be removed or keep-balance will produce an error.
 
-Please see the "config migration guide":{{site.baseurl}}/admin/config-migration.html and "keep-balance install guide":{{site.baseurl}}/install/install-keep-balance.html for more details.
+Please see the "config migration guide":https://doc.arvados.org/v2.1/admin/config-migration.html and "keep-balance install guide":{{site.baseurl}}/install/install-keep-balance.html for more details.
 
 h3. Arv-git-httpd configuration migration
 
-(feature "#14712":https://dev.arvados.org/issues/14712 ) The arv-git-httpd package can now be configured using the centralized configuration file at @/etc/arvados/config.yml@. Configuration via individual command line arguments is no longer available. Please see "arv-git-httpd's config migration guide":{{site.baseurl}}/admin/config-migration.html#arv-git-httpd for more details.
+(feature "#14712":https://dev.arvados.org/issues/14712 ) The arv-git-httpd package can now be configured using the centralized configuration file at @/etc/arvados/config.yml@. Configuration via individual command line arguments is no longer available. Please see "arv-git-httpd's config migration guide":https://doc.arvados.org/v2.1/admin/config-migration.html#arv-git-httpd for more details.
 
 h3. Keepstore and keep-web configuration migration
 
@@ -270,11 +263,11 @@ keepstore and keep-web no longer support configuration via (previously deprecate
 
 keep-web now supports the legacy @keep-web.yml@ config format (used by Arvados 1.4) and the new cluster config file format. Please check "keep-web's install guide":{{site.baseurl}}/install/install-keep-web.html for more details.
 
-keepstore now supports the legacy @keepstore.yml@ config format (used by Arvados 1.4) and the new cluster config file format. Please check the "keepstore config migration notes":{{site.baseurl}}/admin/config-migration.html#keepstore and "keepstore install guide":{{site.baseurl}}/install/install-keepstore.html for more details.
+keepstore now supports the legacy @keepstore.yml@ config format (used by Arvados 1.4) and the new cluster config file format. Please check the "keepstore config migration notes":https://doc.arvados.org/v2.1/admin/config-migration.html#keepstore and "keepstore install guide":{{site.baseurl}}/install/install-keepstore.html for more details.
 
 h3. Keepproxy configuration migration
 
-(feature "#14715":https://dev.arvados.org/issues/14715 ) Keepproxy can now be configured using the centralized config at @/etc/arvados/config.yml@. Configuration via individual command line arguments is no longer available and the @DisableGet@, @DisablePut@, and @PIDFile@ configuration options are no longer supported. If you are still using the legacy config and @DisableGet@ or @DisablePut@ are set to true or @PIDFile@ has a value, keepproxy will produce an error and fail to start. Please see "keepproxy's config migration guide":{{site.baseurl}}/admin/config-migration.html#keepproxy for more details.
+(feature "#14715":https://dev.arvados.org/issues/14715 ) Keepproxy can now be configured using the centralized config at @/etc/arvados/config.yml@. Configuration via individual command line arguments is no longer available and the @DisableGet@, @DisablePut@, and @PIDFile@ configuration options are no longer supported. If you are still using the legacy config and @DisableGet@ or @DisablePut@ are set to true or @PIDFile@ has a value, keepproxy will produce an error and fail to start. Please see "keepproxy's config migration guide":https://doc.arvados.org/v2.1/admin/config-migration.html#keepproxy for more details.
 
 h3. Delete "keep_services" records
 
@@ -471,7 +464,7 @@ As part of story "#9945":https://dev.arvados.org/issues/9945, it was discovered
 
 h3. New configuration
 
-Arvados is migrating to a centralized configuration file for all components.  During the migration, legacy configuration files will continue to be loaded.  See "Migrating Configuration":config-migration.html for details.
+Arvados is migrating to a centralized configuration file for all components.  During the migration, legacy configuration files will continue to be loaded.  See "Migrating Configuration":https://doc.arvados.org/v2.1/admin/config-migration.html for details.
 
 h2(#v1_3_3). v1.3.3 (2019-05-14)
 
index c3d6a92b5d9f019e28c26b622acd52c7ac2a9b89..10f2e32ef134569591940ffa8f44bdbdae593d70 100644 (file)
@@ -20,8 +20,7 @@ SPDX-License-Identifier: CC-BY-SA-3.0
 # "Run the provision.sh script":#run_provision_script
 # "Initial user and login":#initial_user
 # "Test the installed cluster running a simple workflow":#test_install
-
-
+# "After the installation":#post_install
 
 h2(#introduction). Introduction
 
@@ -290,3 +289,9 @@ INFO Final output collection d6c69a88147dde9d52a418d50ef788df+123
 INFO Final process status is success
 </code></pre>
 </notextile>
+
+h2(#post_install). After the installation
+
+Once the installation is complete, it is recommended to keep a copy of your local configuration files. Committing them to version control is a good idea.
+
+Re-running the Salt-based installer is not recommended for maintaining and upgrading Arvados, please see "Maintenance and upgrading":{{site.baseurl}}/admin/maintenance-and-upgrading.html for more information.
index ce70a30d467bc7f38bb75e1f4803e04df699cb86..0f06412f9103c12fb3c46d8fa9f2c8a826e7eb89 100644 (file)
@@ -20,6 +20,7 @@ SPDX-License-Identifier: CC-BY-SA-3.0
 ## "DNS configuration (single host / multiple hostnames)":#single_host_multiple_hostnames_dns_configuration
 # "Initial user and login":#initial_user
 # "Test the installed cluster running a simple workflow":#test_install
+# "After the installation":#post_install
 
 h2(#single_host). Single host install using the provision.sh script
 
@@ -266,3 +267,9 @@ INFO Final output collection d6c69a88147dde9d52a418d50ef788df+123
 INFO Final process status is success
 </code></pre>
 </notextile>
+
+h2(#post_install). After the installation
+
+Once the installation is complete, it is recommended to keep a copy of your local configuration files. Committing them to version control is a good idea.
+
+Re-running the Salt-based installer is not recommended for maintaining and upgrading Arvados, please see "Maintenance and upgrading":{{site.baseurl}}/admin/maintenance-and-upgrading.html for more information.
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 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 e739235953cf799e07e9f13440ef7e05963241c9..e1883d737036578bdd3e2d319fee93a48b41c314 100644 (file)
@@ -36,7 +36,7 @@ setup(name='arvados-cwl-runner',
       # file to determine what version of cwltool and schema-salad to
       # build.
       install_requires=[
-          'cwltool==3.1.20220210171524',
+          'cwltool==3.1.20220217190813',
           'schema-salad==8.2.20211116214159',
           'arvados-python-client{}'.format(pysdk_dep),
           'setuptools',
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 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 e7d03677eac190a3b4f6cb10a056973811765366..e021b442f1d7a0db09848c4364c0dcca45c7d0f0 100755 (executable)
@@ -143,7 +143,7 @@ docker_run_dev() {
            "--volume=$PG_DATA:/var/lib/postgresql:rw" \
            "--volume=$VAR_DATA:$ARVADOS_CONTAINER_PATH:rw" \
            "--volume=$PASSENGER:/var/lib/passenger:rw" \
-          "--volume=$GEMS:/var/lib/arvados/lib/ruby/gems:rw" \
+          "--volume=$GEMS:/var/lib/arvados-arvbox/.gem:rw" \
            "--volume=$PIPCACHE:/var/lib/pip:rw" \
            "--volume=$NPMCACHE:/var/lib/npm:rw" \
            "--volume=$GOSTUFF:/var/lib/gopath:rw" \
index c44e7c410151d772291e25b267b368439f6867a3..5e16264c8aaa754f1221f743019c6b655cc65481 100644 (file)
@@ -12,7 +12,8 @@ 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
+export GEM_HOME=$HOME/.gem
+GEMLOCK=$GEM_HOME/gems.lock
 
 defaultdev=$(/sbin/ip route|awk '/default/ { print $5 }')
 dockerip=$(/sbin/ip route | grep default | awk '{ print $3 }')
@@ -63,7 +64,7 @@ else
 fi
 
 run_bundler() {
-    flock $GEMLOCK /var/lib/arvados/bin/gem install --no-document bundler:$BUNDLER_VERSION
+    flock $GEMLOCK /var/lib/arvados/bin/gem install --no-document --user bundler:$BUNDLER_VERSION
     if test -f Gemfile.lock ; then
         frozen=--frozen
     else
index 6ec788589f59d624a3ab2e1b9c47bc72d76e84dc..5007fe0be3e8e459fdd107246b5987b324051bfd 100755 (executable)
@@ -63,7 +63,7 @@ fi
 
 if ! [[ -z "$waiting" ]] ; then
     if ps x | grep -v grep | grep "bundle install" > /dev/null; then
-        gemcount=$(ls /var/lib/arvados/lib/ruby/gems/*/gems 2>/dev/null | wc -l)
+        gemcount=$(ls /var/lib/arvados/lib/ruby/gems/*/gems /var/lib/arvados-arvbox/.gem/ruby/*/gems 2>/dev/null | wc -l)
 
         gemlockcount=0
         for l in /usr/src/arvados/services/api/Gemfile.lock \