Merge branch '18376-nfs-readdirent'
authorTom Clegg <tom@curii.com>
Fri, 3 Dec 2021 16:30:23 +0000 (11:30 -0500)
committerTom Clegg <tom@curii.com>
Fri, 3 Dec 2021 16:30:23 +0000 (11:30 -0500)
refs #18376

Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tom@curii.com>

54 files changed:
doc/_includes/_install_debian_key.liquid
doc/admin/collection-versioning.html.textile.liquid
doc/admin/keep-recovering-data.html.textile.liquid
doc/admin/upgrading.html.textile.liquid
doc/install/configure-s3-object-storage.html.textile.liquid
go.mod
go.sum
lib/config/config.default.yml
lib/config/generated_config.go
lib/controller/auth_test.go
lib/controller/cmd.go
lib/controller/dblock/dblock.go [new file with mode: 0644]
lib/controller/federation.go
lib/controller/federation/conn.go
lib/controller/federation_test.go
lib/controller/handler.go
lib/controller/handler_test.go
lib/controller/integration_test.go
lib/controller/localdb/container_gateway_test.go
lib/controller/localdb/login.go
lib/controller/localdb/login_oidc.go
lib/controller/router/response.go
lib/controller/router/router.go
lib/controller/rpc/conn.go
lib/controller/server_test.go
lib/controller/trash.go [new file with mode: 0644]
lib/crunchrun/crunchrun.go
lib/lsf/dispatch.go
lib/lsf/dispatch_test.go
lib/lsf/lsfcli.go
lib/lsf/lsfqueue.go
sdk/go/arvados/api.go
sdk/go/arvados/api_client_authorization.go
sdk/go/arvados/client.go
sdk/go/arvadostest/api.go
sdk/go/arvadostest/api_test.go [new file with mode: 0644]
sdk/java-v2/src/main/java/org/arvados/client/api/model/Group.java
sdk/java-v2/src/main/java/org/arvados/client/api/model/Link.java
sdk/java-v2/src/test/java/org/arvados/client/api/client/LinkApiClientTest.java [new file with mode: 0644]
sdk/java-v2/src/test/resources/org/arvados/client/api/client/links-create.json [new file with mode: 0644]
sdk/java-v2/src/test/resources/org/arvados/client/api/client/links-get.json [new file with mode: 0644]
sdk/java-v2/src/test/resources/org/arvados/client/api/client/links-list.json [new file with mode: 0644]
sdk/python/arvados/commands/put.py
sdk/python/tests/test_arv_put.py
services/api/app/controllers/arvados/v1/schema_controller.rb
services/api/app/controllers/sys_controller.rb [moved from services/api/lib/sweep_trashed_objects.rb with 55% similarity]
services/api/app/models/collection.rb
services/api/config/routes.rb
services/api/lib/update_permissions.rb
services/api/test/functional/sys_controller_test.rb [new file with mode: 0644]
services/api/test/integration/errors_test.rb
services/api/test/unit/api_client_authorization_test.rb
services/api/test/unit/collection_test.rb
services/api/test/unit/group_test.rb

index b25674c8ce4db0af2d1a2aac3c181693b418d86c..91b24a8a8d02bac853a64c6615438b40d6776cd5 100644 (file)
@@ -5,8 +5,8 @@ SPDX-License-Identifier: CC-BY-SA-3.0
 {% endcomment %}
 
 <notextile>
-<pre><code># <span class="userinput">apt-get --no-install-recommends install curl gnupg2</span>
-# <span class="userinput">curl -s https://apt.arvados.org/pubkey.gpg -o /etc/apt/trusted.gpg.d/arvados.asc</span>
+<pre><code># <span class="userinput">apt-get --no-install-recommends install curl gnupg2 ca-certificates</span>
+# <span class="userinput">curl https://apt.arvados.org/pubkey.gpg -o /etc/apt/trusted.gpg.d/arvados.asc</span>
 </code></pre>
 </notextile>
 
index 29b0bcd506a81ea186d87fb96cf0a2553b744f3e..7b25f7b395d3f9fb10ed2f477cfcc68a785c8cbc 100644 (file)
@@ -23,13 +23,13 @@ There are 2 configuration settings in the @Collections@ section of @config.yml@
       # is older than the amount of seconds defined on PreserveVersionIfIdle,
       # a snapshot of the collection's previous state is created and linked to
       # the current collection.
-      CollectionVersioning: false
+      CollectionVersioning: true
 
       # This setting control the auto-save aspect of collection versioning, and can be set to:
       #   0s = auto-create a new version on every update.
       #  -1s = never auto-create new versions.
       # > 0s = auto-create a new version when older than the specified number of seconds.
-      PreserveVersionIfIdle: -1s
+      PreserveVersionIfIdle: 10s
 </pre>
 
 Note that if you set @CollectionVersioning@ to @false@ after being enabled, old versions will still be accessible, but further changes will not be versioned.
index 14e25dd11c5bd17da6808f40553033e813246b09..8d71a765e6a416bc8239c7e87ab8b745e2c7f2ea 100644 (file)
@@ -24,7 +24,7 @@ Multiple collections may share a _portable data hash_, i.e. have the same conten
 
 h2(#check_collection_versioning). Consider collection versioning
 
-Arvados supports collection versioning. If it has been "enabled":{{ site.baseurl }}/admin/collection-versioning.html on your cluster, the deleted collection may be recoverable from an older version. See "Using collection versioning":{{ site.baseurl }}/user/topics/collection-versioning.html for details.
+Arvados supports collection versioning. If it has not been "disabled":{{ site.baseurl }}/admin/collection-versioning.html on your cluster, the deleted collection may be recoverable from an older version. See "Using collection versioning":{{ site.baseurl }}/user/topics/collection-versioning.html for details.
 
 h2(#recover_collection). Recovering collections
 
index c1a7ae87dec28d0c9a439334eabc4c42a98f6180..517c2dd0cde0014f44bd03e077048c459a191ef2 100644 (file)
@@ -39,6 +39,14 @@ h2(#main). development main (as of 2021-11-10)
 
 "previous: Upgrading from 2.3.0":#v2_3_0
 
+h3. Default LSF arguments have changed
+
+If you use LSF and your configuration specifies @Containers.LSF.BsubArgumentsList@, you should update it to include the new arguments (@"-R", "select[mem>=%MMB]", ...@, see "configuration reference":{{site.baseurl}}/admin/config.html). Otherwise, containers that are too big to run on any LSF host will remain in the LSF queue instead of being cancelled.
+
+h3. Previously trashed role groups will be deleted
+
+Due to a bug in previous versions, the @DELETE@ operation on a role group caused the group to be flagged as trash in the database, but continue to grant permissions regardless. After upgrading, any role groups that had been trashed this way will be deleted. This might surprise some users if they were relying on permissions that were still in effect due to this bug. Future @DELETE@ operations on a role group will immediately delete the group and revoke the associated permissions.
+
 h3. Users are visible to other users by default
 
 When a new user is set up (either via @AutoSetupNewUsers@ config or via Workbench admin interface) the user immediately becomes visible to other users. To revert to the previous behavior, where the administrator must add two users to the same group using the Workbench admin interface in order for the users to see each other, change the new @Users.ActivatedUsersAreVisibleToOthers@ config to @false@.
index e6b1e095ed8e09b4ee6052196a29b23ec87d7b66..e9866d510344da4e9a58ba7c8a5deee268540da1 100644 (file)
@@ -48,7 +48,7 @@ Volumes are configured in the @Volumes@ section of the cluster configuration fil
 
           # Storage provider region. For Google Cloud Storage, use ""
           # or omit.
-          Region: <span class="userinput">us-east-1a</span>
+          Region: <span class="userinput">us-east-1</span>
 
           # Storage provider endpoint. For Amazon S3, use "" or
           # omit. For Google Cloud Storage, use
diff --git a/go.mod b/go.mod
index adca449b7143ff3b01d2e3a9e037eac2114320a8..0ea3dd9169db9ad970a4a985ade5fb525878ee9f 100644 (file)
--- a/go.mod
+++ b/go.mod
@@ -5,8 +5,10 @@ go 1.13
 require (
        github.com/AdRoll/goamz v0.0.0-20170825154802-2731d20f46f4
        github.com/Azure/azure-sdk-for-go v45.1.0+incompatible
-       github.com/Azure/go-autorest/autorest v0.11.3
-       github.com/Azure/go-autorest/autorest/azure/auth v0.5.1
+       github.com/Azure/go-autorest/autorest v0.11.22
+       github.com/Azure/go-autorest/autorest/adal v0.9.17 // indirect
+       github.com/Azure/go-autorest/autorest/azure/auth v0.5.9
+       github.com/Azure/go-autorest/autorest/azure/cli v0.4.4 // indirect
        github.com/Azure/go-autorest/autorest/to v0.4.0
        github.com/Azure/go-autorest/autorest/validation v0.3.0 // indirect
        github.com/Microsoft/go-winio v0.4.5 // indirect
@@ -33,6 +35,7 @@ require (
        github.com/go-asn1-ber/asn1-ber v1.4.1 // indirect
        github.com/go-ldap/ldap v3.0.3+incompatible
        github.com/gogo/protobuf v1.1.1
+       github.com/golang-jwt/jwt/v4 v4.1.0 // indirect
        github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
        github.com/gorilla/context v1.1.1 // indirect
        github.com/gorilla/mux v1.6.1-0.20180107155708-5bbbb5b2b572
@@ -58,11 +61,11 @@ require (
        github.com/sirupsen/logrus v1.8.1
        github.com/src-d/gcfg v1.3.0 // indirect
        github.com/xanzy/ssh-agent v0.1.0 // indirect
-       golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9
-       golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4
+       golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871
+       golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2
        golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45
-       golang.org/x/sys v0.0.0-20210603125802-9665404d3644
-       golang.org/x/tools v0.1.2 // indirect
+       golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e
+       golang.org/x/tools v0.1.7 // indirect
        google.golang.org/api v0.13.0
        gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d // indirect
        gopkg.in/check.v1 v1.0.0-20161208181325-20d25e280405
diff --git a/go.sum b/go.sum
index 2f575eae919cf129009ad887c5e91ada2448edcb..28ff3dadc9b9fe03785932b474a865e7e09075db 100644 (file)
--- a/go.sum
+++ b/go.sum
@@ -6,40 +6,39 @@ github.com/Azure/azure-sdk-for-go v45.1.0+incompatible h1:kxtaPD8n2z5Za+9e3sKsYG
 github.com/Azure/azure-sdk-for-go v45.1.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
 github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs=
 github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
-github.com/Azure/go-autorest/autorest v0.11.0/go.mod h1:JFgpikqFJ/MleTTxwepExTKnFUKKszPS8UavbQYUMuw=
-github.com/Azure/go-autorest/autorest v0.11.3 h1:fyYnmYujkIXUgv88D9/Wo2ybE4Zwd/TmQd5sSI5u2Ws=
-github.com/Azure/go-autorest/autorest v0.11.3/go.mod h1:JFgpikqFJ/MleTTxwepExTKnFUKKszPS8UavbQYUMuw=
-github.com/Azure/go-autorest/autorest/adal v0.9.0/go.mod h1:/c022QCutn2P7uY+/oQWWNcK9YU+MH96NgK+jErpbcg=
-github.com/Azure/go-autorest/autorest/adal v0.9.2 h1:Aze/GQeAN1RRbGmnUJvUj+tFGBzFdIg3293/A9rbxC4=
-github.com/Azure/go-autorest/autorest/adal v0.9.2/go.mod h1:/3SMAM86bP6wC9Ev35peQDUeqFZBMH07vvUOmg4z/fE=
-github.com/Azure/go-autorest/autorest/azure/auth v0.5.1 h1:bvUhZciHydpBxBmCheUgxxbSwJy7xcfjkUsjUcqSojc=
-github.com/Azure/go-autorest/autorest/azure/auth v0.5.1/go.mod h1:ea90/jvmnAwDrSooLH4sRIehEPtG/EPUXavDh31MnA4=
-github.com/Azure/go-autorest/autorest/azure/cli v0.4.0 h1:Ml+UCrnlKD+cJmSzrZ/RDcDw86NjkRUpnFh7V5JUhzU=
-github.com/Azure/go-autorest/autorest/azure/cli v0.4.0/go.mod h1:JljT387FplPzBA31vUcvsetLKF3pec5bdAxjVU4kI2s=
+github.com/Azure/go-autorest/autorest v0.11.19/go.mod h1:dSiJPy22c3u0OtOKDNttNgqpNFY/GeWa7GH/Pz56QRA=
+github.com/Azure/go-autorest/autorest v0.11.22 h1:bXiQwDjrRmBQOE67bwlvUKAC1EU1yZTPQ38c+bstZws=
+github.com/Azure/go-autorest/autorest v0.11.22/go.mod h1:BAWYUWGPEtKPzjVkp0Q6an0MJcJDsoh5Z1BFAEFs4Xs=
+github.com/Azure/go-autorest/autorest/adal v0.9.5/go.mod h1:B7KF7jKIeC9Mct5spmyCB/A8CG/sEz1vwIRGv/bbw7A=
+github.com/Azure/go-autorest/autorest/adal v0.9.13/go.mod h1:W/MM4U6nLxnIskrw4UwWzlHfGjwUS50aOsc/I3yuU8M=
+github.com/Azure/go-autorest/autorest/adal v0.9.14/go.mod h1:W/MM4U6nLxnIskrw4UwWzlHfGjwUS50aOsc/I3yuU8M=
+github.com/Azure/go-autorest/autorest/adal v0.9.17 h1:esOPl2dhcz9P3jqBSJ8tPGEj2EqzPPT6zfyuloiogKY=
+github.com/Azure/go-autorest/autorest/adal v0.9.17/go.mod h1:XVVeme+LZwABT8K5Lc3hA4nAe8LDBVle26gTrguhhPQ=
+github.com/Azure/go-autorest/autorest/azure/auth v0.5.9 h1:Y2CgdzitFDsdMwYMzf9LIZWrrTFysqbRc7b94XVVJ78=
+github.com/Azure/go-autorest/autorest/azure/auth v0.5.9/go.mod h1:hg3/1yw0Bq87O3KvvnJoAh34/0zbP7SFizX/qN5JvjU=
+github.com/Azure/go-autorest/autorest/azure/cli v0.4.2/go.mod h1:7qkJkT+j6b+hIpzMOwPChJhTqS8VbsqqgULzMNRugoM=
+github.com/Azure/go-autorest/autorest/azure/cli v0.4.4 h1:iuooz5cZL6VRcO7DVSFYxRcouqn6bFVE/e77Wts50Zk=
+github.com/Azure/go-autorest/autorest/azure/cli v0.4.4/go.mod h1:yAQ2b6eP/CmLPnmLvxtT1ALIY3OR1oFcCqVBi8vHiTc=
 github.com/Azure/go-autorest/autorest/date v0.3.0 h1:7gUk1U5M/CQbp9WoqinNzJar+8KY+LPI6wiWrP/myHw=
 github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74=
-github.com/Azure/go-autorest/autorest/mocks v0.4.0/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k=
 github.com/Azure/go-autorest/autorest/mocks v0.4.1 h1:K0laFcLE6VLTOwNgSxaGbUcLPuGXlNkbVvq4cW4nIHk=
 github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k=
 github.com/Azure/go-autorest/autorest/to v0.4.0 h1:oXVqrxakqqV1UZdSazDOPOLvOIz+XA683u8EctwboHk=
 github.com/Azure/go-autorest/autorest/to v0.4.0/go.mod h1:fE8iZBn7LQR7zH/9XU2NcPR4o9jEImooCeWJcYV/zLE=
 github.com/Azure/go-autorest/autorest/validation v0.3.0 h1:3I9AAI63HfcLtphd9g39ruUwRI+Ca+z/f36KHPFRUss=
 github.com/Azure/go-autorest/autorest/validation v0.3.0/go.mod h1:yhLgjC0Wda5DYXl6JAsWyUe4KVNffhoDhG0zVzUMo3E=
-github.com/Azure/go-autorest/logger v0.2.0 h1:e4RVHVZKC5p6UANLJHkM4OfR1UKZPj8Wt8Pcx+3oqrE=
-github.com/Azure/go-autorest/logger v0.2.0/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8=
+github.com/Azure/go-autorest/logger v0.2.1 h1:IG7i4p/mDa2Ce4TRyAO8IHnVhAVF3RFU+ZtXWSmf4Tg=
+github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8=
 github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo=
 github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU=
-github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 github.com/Microsoft/go-winio v0.4.5 h1:U2XsGR5dBg1yzwSEJoP2dE2/aAXpmad+CNG2hE9Pd5k=
 github.com/Microsoft/go-winio v0.4.5/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA=
 github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7 h1:uSoVVbwJiQipAclBbw+8quDsfcvFjOpI5iCf4p/cqCs=
 github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs=
 github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
-github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM=
 github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
 github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
-github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4 h1:Hs82Z41s6SdL1CELW+XaDYmOH4hkBN4/N9og/AsOv7E=
 github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
 github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA=
 github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
@@ -60,13 +59,11 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
 github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
 github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY=
 github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
-github.com/boltdb/bolt v1.3.1 h1:JQmyP4ZBrce+ZQu0dY660FMfatumYDLun9hBCUVIkF4=
 github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps=
 github.com/bradleypeabody/godap v0.0.0-20170216002349-c249933bc092 h1:0Di2onNnlN5PAyWPbqlPyN45eOQ+QW/J9eqLynt4IV4=
 github.com/bradleypeabody/godap v0.0.0-20170216002349-c249933bc092/go.mod h1:8IzBjZCRSnsvM6MJMG8HNNtnzMl48H22rbJL2kRUJ0Y=
 github.com/cespare/xxhash/v2 v2.1.0 h1:yTUvW7Vhb89inJ+8irsUqiWjh8iT6sQPZiQzI6ReGkA=
 github.com/cespare/xxhash/v2 v2.1.0/go.mod h1:dgIUBU3pDso/gPgZ1osOZ0iQf77oPR28Tjxl5dIMyVM=
-github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI=
 github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
 github.com/coreos/go-oidc v2.1.0+incompatible h1:sdJrfw8akMnCuUlaZU3tE/uYXFgfqom8DBE9so9EBsM=
 github.com/coreos/go-oidc v2.1.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc=
@@ -77,10 +74,9 @@ github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7Do
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
-github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
-github.com/dimchansky/utfbom v1.1.0 h1:FcM3g+nofKgUteL8dm/UpdRXNC9KmADgTpLKsu0TRo4=
 github.com/dimchansky/utfbom v1.1.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQvIirEdv+8=
+github.com/dimchansky/utfbom v1.1.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi/U=
+github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE=
 github.com/dnaeon/go-vcr v1.0.1 h1:r8L/HqC0Hje5AXMu1ooW8oyQyOFv4GxqpL0nRP7SLLY=
 github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E=
 github.com/docker/distribution v2.6.0-rc.1.0.20180105232752-277ed486c948+incompatible h1:PVtvnmmxSMUcT5AY6vG7sCCzRg3eyoW6vQvXtITC60c=
@@ -95,6 +91,7 @@ github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4
 github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
 github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ=
 github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
+github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=
 github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
 github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
 github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
@@ -104,40 +101,35 @@ github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aev
 github.com/go-asn1-ber/asn1-ber v1.4.1 h1:qP/QDxOtmMoJVgXHCXNzDpA0+wkgYB2x5QoLMVOciyw=
 github.com/go-asn1-ber/asn1-ber v1.4.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
 github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
-github.com/go-kit/kit v0.9.0 h1:wDJmvq38kDhkVxi50ni9ykkdUr1PKgqKOoi01fa0Mdk=
 github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
 github.com/go-ldap/ldap v3.0.3+incompatible h1:HTeSZO8hWMS1Rgb2Ziku6b8a7qRIZZMHjsvuZyatzwk=
 github.com/go-ldap/ldap v3.0.3+incompatible/go.mod h1:qfd9rJvER9Q0/D/Sqn1DfHRoBp40uXYvFoEVrNEPqRc=
 github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
-github.com/go-logfmt/logfmt v0.4.0 h1:MP4Eh7ZCb31lleYCFuwm0oe4/YGak+5l1vA2NOE80nA=
 github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
 github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
 github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
 github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
-github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=
 github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
 github.com/gogo/protobuf v1.1.1 h1:72R+M5VuhED/KujmZVcIquuo8mBgX4oVda//DQb3PXo=
 github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
+github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
+github.com/golang-jwt/jwt/v4 v4.1.0 h1:XUgk2Ex5veyVFVeLm0xhusUTQybEbexJXrvPNOKkSY0=
+github.com/golang-jwt/jwt/v4 v4.1.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
 github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
 github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
 github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
-github.com/golang/mock v1.2.0 h1:28o5sBqPkBsMGnC6b4MvE2TzSr5/AT4c/1fLqVGIwlk=
 github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
 github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
 github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
-github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c h1:964Od4U6p2jUkFxvCydnIczKteheJEzHRToSGK3Bnlw=
 github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
 github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
 github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
 github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
 github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw=
 github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
-github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no=
 github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
-github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57 h1:eqyIo2HjKhKe/mJzTG8n4VqvLXIOEG+SLdDqX7xGtkY=
 github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
@@ -164,21 +156,15 @@ github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhB
 github.com/johannesboyne/gofakes3 v0.0.0-20200716060623-6b2b4cb092cc h1:JJPhSHowepOF2+ElJVyb9jgt5ZyBkPMkPuhS0uODSFs=
 github.com/johannesboyne/gofakes3 v0.0.0-20200716060623-6b2b4cb092cc/go.mod h1:fNiSoOiEI5KlkWXn26OwKnNe58ilTIkpBlgOrt7Olu8=
 github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
-github.com/json-iterator/go v1.1.7 h1:KfgG9LzI+pYjr4xvmz/5H4FXjokeP+rlHLhv3iH62Fo=
 github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
-github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024 h1:rBMNdlhTLzJjJSDIjNEXX1Pz3Hmwmz91v+zycvx9PJc=
 github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
 github.com/julienschmidt/httprouter v1.2.0 h1:TDTW5Yz1mjftljbcKqRcrYhd4XeOoI98t+9HbQbYf7g=
 github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
 github.com/kevinburke/ssh_config v0.0.0-20171013211458-802051befeb5 h1:xXn0nBttYwok7DhU4RxqaADEpQn7fEMt5kKc3yoj/n0=
 github.com/kevinburke/ssh_config v0.0.0-20171013211458-802051befeb5/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
-github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
 github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
-github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515 h1:T+h1c/A9Gawja4Y9mFVWj2vyii2bbUNDw3kt9VxK2EY=
 github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
 github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
-github.com/lib/pq v1.3.0 h1:/qkRGz8zljWiDcFvgpwUpwIAPu3r07TDvs3Rws+o/pU=
-github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
 github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8=
 github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
 github.com/mattn/go-sqlite3 v1.9.0 h1:pDRiWfl+++eC2FEFRy6jXmQlvp4Yh3z1MJKg4UeYM/4=
@@ -188,14 +174,11 @@ github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5
 github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
 github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
 github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
-github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
 github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
-github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
 github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
 github.com/msteinert/pam v0.0.0-20190215180659-f29b9f28d6f9 h1:ZivaaKmjs9q90zi6I4gTLW6tbVGtlBjellr3hMYaly0=
 github.com/msteinert/pam v0.0.0-20190215180659-f29b9f28d6f9/go.mod h1:np1wUFZ6tyoke22qDJZY40URn9Ae51gX7ljIWXN5TJs=
-github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223 h1:F9x/1yl3T2AeKLr2AMdilSD8+f9bvMnNN8VS5iDtovc=
 github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
 github.com/opencontainers/go-digest v1.0.0-rc1 h1:WzifXhOVOEOuFYOJAW6aQqW0TooG2iki3E3Ii+WN7gQ=
 github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
@@ -234,16 +217,13 @@ github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAm
 github.com/shabbyrobe/gocovmerge v0.0.0-20180507124511-f6ea450bfb63 h1:J6qvD6rbmOil46orKqJaRPG+zTpoGlBTUdyv8ki63L0=
 github.com/shabbyrobe/gocovmerge v0.0.0-20180507124511-f6ea450bfb63/go.mod h1:n+VKSARF5y/tS9XFSP7vWDfS+GUC5vs/YT7M5XDTUEM=
 github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
-github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
 github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
 github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
 github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
-github.com/spf13/afero v1.2.1 h1:qgMbHoJbPbw579P+1zVY+6n4nIFuIchaIjzZ/I/Yq8M=
 github.com/spf13/afero v1.2.1/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
 github.com/src-d/gcfg v1.3.0 h1:2BEDr8r0I0b8h/fOqwtxCEiq2HJu8n2JGZJQFGXWLjg=
 github.com/src-d/gcfg v1.3.0/go.mod h1:p/UMsR43ujA89BJY9duynAwIpvqEujIH/jFlfL7jWoI=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
-github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A=
 github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
@@ -251,28 +231,22 @@ github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJy
 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
 github.com/xanzy/ssh-agent v0.1.0 h1:lOhdXLxtmYjaHc76ZtNmJWPg948y/RnT+3N3cvKWFzY=
 github.com/xanzy/ssh-agent v0.1.0/go.mod h1:0NyE30eGUDliuLEHJgYte/zncp2zdTStcOnWhgSqHD8=
-github.com/yuin/goldmark v1.2.1 h1:ruQGxdhGHe7FWOJPT0mKs5+pD2Xs1Bm/kdGlHO04FmM=
-github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
-github.com/yuin/goldmark v1.3.5 h1:dPmz1Snjq0kmkz159iL7S6WzdahUTHnHB5M56WFVifs=
-github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
+github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
 go.opencensus.io v0.21.0 h1:mU6zScU4U1YAFPHEHYk+3JC4SY7JxgkqS10ZOSyksNg=
 go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
 golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
-golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
-golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
-golang.org/x/exp v0.0.0-20190121172915-509febef88a4 h1:c2HOrn5iMezYjSlGPncknSEr/8x5LELb/ilJbXi9DEA=
+golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871 h1:/pEO3GD/ABYAjuakUS6xSEmmlyVS4kxBNkeA9tLJiTI=
+golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
 golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
 golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
 golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
-golang.org/x/lint v0.0.0-20190409202823-959b441ac422 h1:QzoH/1pFpZguR8NrRHLcO6jKqfv2zpuSqZLgdm7ZmjI=
 golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
-golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4=
-golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
-golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo=
 golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -286,10 +260,10 @@ golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn
 golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20201021035429-f5854403a974 h1:IX6qOQeG5uLjB/hjjwjedwfjND0hgjPMMyO1RoIXQNI=
-golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
-golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4 h1:4nGaVu0QrbjT/AK2PRLuQfQuh6DJve+pELhqTdAj3x0=
-golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
+golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 h1:CIJ76btIcR3eFI5EgSo6k1qKw9KJexJuRLI9G7Hp5wE=
+golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0=
@@ -299,9 +273,6 @@ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJ
 golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 h1:SQFwaSi55rU7vdNs9Yr0Z324VNlrF+0wMqRXT4St8ck=
-golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -314,23 +285,19 @@ golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4 h1:myAQVi0cGEoqQVR5POX+8RR2mrocKqNN1hmeMqhX27k=
-golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210510120138-977fb7262007 h1:gG67DSER+11cZvqIMb8S8bt0vZtiN6xWYARwirrOSfE=
-golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20210603125802-9665404d3644 h1:CA1DEQ4NdKphKeL70tvsWNdT5oFh1lOjihRcEDROi0I=
-golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e h1:WUoyKPm6nCo1BnNUvPGnFG3T5DUVem42yDJZZ4CNxMA=
+golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
-golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/time v0.0.0-20181108054448-85acf8d2951c h1:fqgJT0MGcGpPgpWU7VRdRjuArfcOvC4AoJmILihzhDg=
+golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
+golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -340,10 +307,8 @@ golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3
 golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
 golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
 golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.1.0 h1:po9/4sTYwZU9lPhi1tOrb4hCv3qrhiQ77LZfGa2OjwY=
-golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
-golang.org/x/tools v0.1.2 h1:kRBLX7v7Af8W7Gdbbc908OJcdgtK8bOz9Uaj8/F1ACA=
-golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
+golang.org/x/tools v0.1.7 h1:6j8CgantCy3yc8JGBqkDLMKWqZ0RDU2g1HVgacojGWQ=
+golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -364,14 +329,12 @@ google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRn
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
 google.golang.org/grpc v1.20.1 h1:Hz2g2wirWK7H0qIIhGIqRGTuMwTE8HEKFnDZZ7lm9NU=
 google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
-gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc=
 gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
 gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d h1:TxyelI5cVkbREznMhfzycHdkp5cLA7DpE+GKjSslYhM=
 gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d/go.mod h1:cuepJuh7vyXfUyUwEgHQXw849cJrilpS5NeIjOWESAw=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20161208181325-20d25e280405 h1:829vOVxxusYHC+IqBtkX5mbKtsY9fheQiQn0MZRVLfQ=
 gopkg.in/check.v1 v1.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce h1:xcEWjVhvbDy+nHP67nPDDpbYrY+ILlfndk4bRioVHaU=
 gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA=
 gopkg.in/square/go-jose.v2 v2.3.1 h1:SK5KegNXmKmqE342YYN2qPHEnUYeoMiXXl1poUlI+o4=
 gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
@@ -385,7 +348,6 @@ gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
 gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
 honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
-honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a h1:LJwr7TCTghdatWv40WobzlKXc9c4s8oGa7QKJUtHhWA=
 honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 rsc.io/getopt v0.0.0-20170811000552-20be20937449 h1:UukjJOsjQH0DIuyyrcod6CXHS6cdaMMuJmrt+SN1j4A=
 rsc.io/getopt v0.0.0-20170811000552-20be20937449/go.mod h1:dhCdeqAxkyt5u3/sKRkUXuHaMXUu1Pt13GTQAM2xnig=
index 411a79650b3421df477f32e0e96b574d068023ae..a84dc5d31673b58b152645f3b73f3efa514ea767 100644 (file)
@@ -497,12 +497,12 @@ Clusters:
       # is older than the amount of seconds defined on PreserveVersionIfIdle,
       # a snapshot of the collection's previous state is created and linked to
       # the current collection.
-      CollectionVersioning: false
+      CollectionVersioning: true
 
       #   0s = auto-create a new version on every update.
       #  -1s = never auto-create new versions.
       # > 0s = auto-create a new version when older than the specified number of seconds.
-      PreserveVersionIfIdle: -1s
+      PreserveVersionIfIdle: 10s
 
       # If non-empty, allow project and collection names to contain
       # the "/" character (slash/stroke/solidus), and replace "/" with
@@ -1089,7 +1089,7 @@ Clusters:
         # in /tmp on the compute node each time an Arvados container
         # runs. Ensure you have something in place to delete old files
         # from /tmp, or adjust the "-o" and "-e" arguments accordingly.
-        BsubArgumentsList: ["-o", "/tmp/crunch-run.%%J.out", "-e", "/tmp/crunch-run.%%J.err", "-J", "%U", "-n", "%C", "-D", "%MMB", "-R", "rusage[mem=%MMB:tmp=%TMB] span[hosts=1]"]
+        BsubArgumentsList: ["-o", "/tmp/crunch-run.%%J.out", "-e", "/tmp/crunch-run.%%J.err", "-J", "%U", "-n", "%C", "-D", "%MMB", "-R", "rusage[mem=%MMB:tmp=%TMB] span[hosts=1]", "-R", "select[mem>=%MMB]", "-R", "select[tmp>=%TMB]", "-R", "select[ncpus>=%C]"]
 
         # Use sudo to switch to this user account when submitting LSF
         # jobs.
@@ -1362,7 +1362,7 @@ Clusters:
           AccessKeyID: aaaaa
           SecretAccessKey: aaaaa
           Endpoint: ""
-          Region: us-east-1a
+          Region: us-east-1
           Bucket: aaaaa
           LocationConstraint: false
           V2Signature: false
index f8553c3eb758785edb6e023b10e8a9697085e74c..567ac30a9b7d855cfabdc10374dcf55b8f9694c7 100644 (file)
@@ -503,12 +503,12 @@ Clusters:
       # is older than the amount of seconds defined on PreserveVersionIfIdle,
       # a snapshot of the collection's previous state is created and linked to
       # the current collection.
-      CollectionVersioning: false
+      CollectionVersioning: true
 
       #   0s = auto-create a new version on every update.
       #  -1s = never auto-create new versions.
       # > 0s = auto-create a new version when older than the specified number of seconds.
-      PreserveVersionIfIdle: -1s
+      PreserveVersionIfIdle: 10s
 
       # If non-empty, allow project and collection names to contain
       # the "/" character (slash/stroke/solidus), and replace "/" with
@@ -1095,7 +1095,7 @@ Clusters:
         # in /tmp on the compute node each time an Arvados container
         # runs. Ensure you have something in place to delete old files
         # from /tmp, or adjust the "-o" and "-e" arguments accordingly.
-        BsubArgumentsList: ["-o", "/tmp/crunch-run.%%J.out", "-e", "/tmp/crunch-run.%%J.err", "-J", "%U", "-n", "%C", "-D", "%MMB", "-R", "rusage[mem=%MMB:tmp=%TMB] span[hosts=1]"]
+        BsubArgumentsList: ["-o", "/tmp/crunch-run.%%J.out", "-e", "/tmp/crunch-run.%%J.err", "-J", "%U", "-n", "%C", "-D", "%MMB", "-R", "rusage[mem=%MMB:tmp=%TMB] span[hosts=1]", "-R", "select[mem>=%MMB]", "-R", "select[tmp>=%TMB]", "-R", "select[ncpus>=%C]"]
 
         # Use sudo to switch to this user account when submitting LSF
         # jobs.
@@ -1368,7 +1368,7 @@ Clusters:
           AccessKeyID: aaaaa
           SecretAccessKey: aaaaa
           Endpoint: ""
-          Region: us-east-1a
+          Region: us-east-1
           Bucket: aaaaa
           LocationConstraint: false
           V2Signature: false
index 17524114671e840ecdac05e2457d9ebbb96c5635..5d477a7664b7266ec28e7696bd604171b6f7c70c 100644 (file)
@@ -98,7 +98,7 @@ func (s *AuthSuite) SetUpTest(c *check.C) {
        cluster.Login.OpenIDConnect.AcceptAccessToken = true
        cluster.Login.OpenIDConnect.AcceptAccessTokenScope = ""
 
-       s.testHandler = &Handler{Cluster: cluster}
+       s.testHandler = &Handler{Cluster: cluster, BackgroundContext: ctxlog.Context(context.Background(), s.log)}
        s.testServer = newServerFromIntegrationTestEnv(c)
        s.testServer.Server.BaseContext = func(net.Listener) context.Context {
                return ctxlog.Context(context.Background(), s.log)
index 7ab7f5305b4fe83113d1a47f499f7d3eb8298804..96972251a3d18af5758e37cd7961ed586504a10a 100644 (file)
@@ -16,6 +16,6 @@ import (
 // Command starts a controller service. See cmd/arvados-server/cmd.go
 var Command cmd.Handler = service.Command(arvados.ServiceNameController, newHandler)
 
-func newHandler(_ context.Context, cluster *arvados.Cluster, _ string, _ *prometheus.Registry) service.Handler {
-       return &Handler{Cluster: cluster}
+func newHandler(ctx context.Context, cluster *arvados.Cluster, _ string, _ *prometheus.Registry) service.Handler {
+       return &Handler{Cluster: cluster, BackgroundContext: ctx}
 }
diff --git a/lib/controller/dblock/dblock.go b/lib/controller/dblock/dblock.go
new file mode 100644 (file)
index 0000000..1a36822
--- /dev/null
@@ -0,0 +1,107 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+package dblock
+
+import (
+       "context"
+       "database/sql"
+       "sync"
+       "time"
+
+       "git.arvados.org/arvados.git/sdk/go/ctxlog"
+       "github.com/jmoiron/sqlx"
+)
+
+var (
+       TrashSweep = &DBLocker{key: 10001}
+       retryDelay = 5 * time.Second
+)
+
+// DBLocker uses pg_advisory_lock to maintain a cluster-wide lock for
+// a long-running task like "do X every N seconds".
+type DBLocker struct {
+       key   int
+       mtx   sync.Mutex
+       ctx   context.Context
+       getdb func(context.Context) (*sqlx.DB, error)
+       conn  *sql.Conn // != nil if advisory lock has been acquired
+}
+
+// Lock acquires the advisory lock, waiting/reconnecting if needed.
+func (dbl *DBLocker) Lock(ctx context.Context, getdb func(context.Context) (*sqlx.DB, error)) {
+       logger := ctxlog.FromContext(ctx)
+       for ; ; time.Sleep(retryDelay) {
+               dbl.mtx.Lock()
+               if dbl.conn != nil {
+                       // Another goroutine is already locked/waiting
+                       // on this lock. Wait for them to release.
+                       dbl.mtx.Unlock()
+                       continue
+               }
+               db, err := getdb(ctx)
+               if err != nil {
+                       logger.WithError(err).Infof("error getting database pool")
+                       dbl.mtx.Unlock()
+                       continue
+               }
+               conn, err := db.Conn(ctx)
+               if err != nil {
+                       logger.WithError(err).Info("error getting database connection")
+                       dbl.mtx.Unlock()
+                       continue
+               }
+               var locked bool
+               err = conn.QueryRowContext(ctx, `SELECT pg_try_advisory_lock($1)`, dbl.key).Scan(&locked)
+               if err != nil {
+                       logger.WithError(err).Infof("error getting pg_try_advisory_lock %d", dbl.key)
+                       conn.Close()
+                       dbl.mtx.Unlock()
+                       continue
+               }
+               if !locked {
+                       conn.Close()
+                       dbl.mtx.Unlock()
+                       continue
+               }
+               logger.Debugf("acquired pg_advisory_lock %d", dbl.key)
+               dbl.ctx, dbl.getdb, dbl.conn = ctx, getdb, conn
+               dbl.mtx.Unlock()
+               return
+       }
+}
+
+// Check confirms that the lock is still active (i.e., the session is
+// still alive), and re-acquires if needed. Panics if Lock is not
+// acquired first.
+func (dbl *DBLocker) Check() {
+       dbl.mtx.Lock()
+       err := dbl.conn.PingContext(dbl.ctx)
+       if err == nil {
+               ctxlog.FromContext(dbl.ctx).Debugf("pg_advisory_lock %d connection still alive", dbl.key)
+               dbl.mtx.Unlock()
+               return
+       }
+       ctxlog.FromContext(dbl.ctx).WithError(err).Info("database connection ping failed")
+       dbl.conn.Close()
+       dbl.conn = nil
+       ctx, getdb := dbl.ctx, dbl.getdb
+       dbl.mtx.Unlock()
+       dbl.Lock(ctx, getdb)
+}
+
+func (dbl *DBLocker) Unlock() {
+       dbl.mtx.Lock()
+       defer dbl.mtx.Unlock()
+       if dbl.conn != nil {
+               _, err := dbl.conn.ExecContext(context.Background(), `SELECT pg_advisory_unlock($1)`, dbl.key)
+               if err != nil {
+                       ctxlog.FromContext(dbl.ctx).WithError(err).Infof("error releasing pg_advisory_lock %d", dbl.key)
+               } else {
+                       ctxlog.FromContext(dbl.ctx).Debugf("released pg_advisory_lock %d", dbl.key)
+               }
+               dbl.conn.Close()
+               dbl.conn = nil
+       }
+}
index cd69727ecb5d2fac27f2777905ad4ba0b5bd4ef7..e7d6e29b88c1f683f981a1ee5df2b53cf7c862af 100644 (file)
@@ -214,10 +214,9 @@ VALUES ($1, $2, CURRENT_TIMESTAMP AT TIME ZONE 'UTC' + INTERVAL '2 weeks', $3,
        }
 
        return &arvados.APIClientAuthorization{
-               UUID:      uuid,
-               APIToken:  token,
-               ExpiresAt: "",
-               Scopes:    scopes}, nil
+               UUID:     uuid,
+               APIToken: token,
+               Scopes:   scopes}, nil
 }
 
 // Extract the auth token supplied in req, and replace it with a
index d1bf473d76856abd59bfb35f069e4f47f498e680..298c693b4e9939a930daefb6c332f3092c0eaaf3 100644 (file)
@@ -525,6 +525,10 @@ func (conn *Conn) SpecimenDelete(ctx context.Context, options arvados.DeleteOpti
        return conn.chooseBackend(options.UUID).SpecimenDelete(ctx, options)
 }
 
+func (conn *Conn) SysTrashSweep(ctx context.Context, options struct{}) (struct{}, error) {
+       return conn.local.SysTrashSweep(ctx, options)
+}
+
 var userAttrsCachedFromLoginCluster = map[string]bool{
        "created_at":  true,
        "email":       true,
@@ -725,6 +729,33 @@ func (conn *Conn) APIClientAuthorizationCurrent(ctx context.Context, options arv
        return conn.chooseBackend(options.UUID).APIClientAuthorizationCurrent(ctx, options)
 }
 
+func (conn *Conn) APIClientAuthorizationCreate(ctx context.Context, options arvados.CreateOptions) (arvados.APIClientAuthorization, error) {
+       if conn.cluster.Login.LoginCluster != "" {
+               return conn.chooseBackend(conn.cluster.Login.LoginCluster).APIClientAuthorizationCreate(ctx, options)
+       }
+       ownerUUID, ok := options.Attrs["owner_uuid"].(string)
+       if ok && ownerUUID != "" {
+               return conn.chooseBackend(ownerUUID).APIClientAuthorizationCreate(ctx, options)
+       }
+       return conn.local.APIClientAuthorizationCreate(ctx, options)
+}
+
+func (conn *Conn) APIClientAuthorizationUpdate(ctx context.Context, options arvados.UpdateOptions) (arvados.APIClientAuthorization, error) {
+       return conn.chooseBackend(options.UUID).APIClientAuthorizationUpdate(ctx, options)
+}
+
+func (conn *Conn) APIClientAuthorizationDelete(ctx context.Context, options arvados.DeleteOptions) (arvados.APIClientAuthorization, error) {
+       return conn.chooseBackend(options.UUID).APIClientAuthorizationDelete(ctx, options)
+}
+
+func (conn *Conn) APIClientAuthorizationList(ctx context.Context, options arvados.ListOptions) (arvados.APIClientAuthorizationList, error) {
+       return conn.local.APIClientAuthorizationList(ctx, options)
+}
+
+func (conn *Conn) APIClientAuthorizationGet(ctx context.Context, options arvados.GetOptions) (arvados.APIClientAuthorization, error) {
+       return conn.chooseBackend(options.UUID).APIClientAuthorizationGet(ctx, options)
+}
+
 type backend interface {
        arvados.API
        BaseURL() url.URL
index 211c7619809ed6a8855248915facef843da55081..a3b198ffc9788bfeb53eb38daeecb66c82af6161 100644 (file)
@@ -70,7 +70,7 @@ func (s *FederationSuite) SetUpTest(c *check.C) {
        cluster.Collections.BlobSigningTTL = arvados.Duration(time.Hour * 24 * 14)
        arvadostest.SetServiceURL(&cluster.Services.RailsAPI, "http://localhost:1/")
        arvadostest.SetServiceURL(&cluster.Services.Controller, "http://localhost:/")
-       s.testHandler = &Handler{Cluster: cluster}
+       s.testHandler = &Handler{Cluster: cluster, BackgroundContext: ctxlog.Context(context.Background(), s.log)}
        s.testServer = newServerFromIntegrationTestEnv(c)
        s.testServer.Server.BaseContext = func(net.Listener) context.Context {
                return ctxlog.Context(context.Background(), s.log)
@@ -721,7 +721,7 @@ func (s *FederationSuite) TestCreateRemoteContainerRequestCheckRuntimeToken(c *c
        var aca arvados.APIClientAuthorization
        c.Check(json.NewDecoder(resp.Body).Decode(&aca), check.IsNil)
        c.Check(aca.ExpiresAt, check.NotNil) // Time.Now()+BlobSigningTTL
-       t, _ := time.Parse(time.RFC3339Nano, aca.ExpiresAt)
+       t := aca.ExpiresAt
        c.Check(t.After(time.Now().Add(s.testHandler.Cluster.API.MaxTokenLifetime.Duration())), check.Equals, true)
        c.Check(t.Before(time.Now().Add(s.testHandler.Cluster.Collections.BlobSigningTTL.Duration())), check.Equals, true)
 }
index b51d909110827bf7d8470120a87f5e29db008a15..f5840b34ce72cd18da4d75c4d27dbb23920e53dd 100644 (file)
@@ -32,9 +32,11 @@ import (
 )
 
 type Handler struct {
-       Cluster *arvados.Cluster
+       Cluster           *arvados.Cluster
+       BackgroundContext context.Context
 
        setupOnce      sync.Once
+       federation     *federation.Conn
        handlerStack   http.Handler
        proxy          *proxy
        secureClient   *http.Client
@@ -103,7 +105,8 @@ func (h *Handler) setup() {
        healthFuncs := make(map[string]health.Func)
 
        oidcAuthorizer := localdb.OIDCAccessTokenAuthorizer(h.Cluster, h.db)
-       rtr := router.New(federation.New(h.Cluster, &healthFuncs), router.Config{
+       h.federation = federation.New(h.Cluster, &healthFuncs)
+       rtr := router.New(h.federation, router.Config{
                MaxRequestSize: h.Cluster.API.MaxRequestSize,
                WrapCalls:      api.ComposeWrappers(ctrlctx.WrapCallsInTransactions(h.db), oidcAuthorizer.WrapCalls),
        })
@@ -133,6 +136,8 @@ func (h *Handler) setup() {
        mux.Handle("/arvados/v1/links/", rtr)
        mux.Handle("/login", rtr)
        mux.Handle("/logout", rtr)
+       mux.Handle("/arvados/v1/api_client_authorizations", rtr)
+       mux.Handle("/arvados/v1/api_client_authorizations/", rtr)
 
        hs := http.NotFoundHandler()
        hs = prepend(hs, h.proxyRailsAPI)
@@ -152,6 +157,8 @@ func (h *Handler) setup() {
        h.proxy = &proxy{
                Name: "arvados-controller",
        }
+
+       go h.trashSweepWorker()
 }
 
 var errDBConnection = errors.New("database connection error")
index f854079f97d87376c9d6e3813b10b2872701d0f5..723e1011f9de62ac871566ec21c4488d1f55b360 100644 (file)
@@ -35,7 +35,7 @@ var _ = check.Suite(&HandlerSuite{})
 
 type HandlerSuite struct {
        cluster *arvados.Cluster
-       handler http.Handler
+       handler *Handler
        ctx     context.Context
        cancel  context.CancelFunc
 }
@@ -51,7 +51,7 @@ func (s *HandlerSuite) SetUpTest(c *check.C) {
        s.cluster.TLS.Insecure = true
        arvadostest.SetServiceURL(&s.cluster.Services.RailsAPI, "https://"+os.Getenv("ARVADOS_TEST_API_HOST"))
        arvadostest.SetServiceURL(&s.cluster.Services.Controller, "http://localhost:/")
-       s.handler = newHandler(s.ctx, s.cluster, "", prometheus.NewRegistry())
+       s.handler = newHandler(s.ctx, s.cluster, "", prometheus.NewRegistry()).(*Handler)
 }
 
 func (s *HandlerSuite) TearDownTest(c *check.C) {
@@ -276,7 +276,7 @@ func (s *HandlerSuite) TestLogoutGoogle(c *check.C) {
 
 func (s *HandlerSuite) TestValidateV1APIToken(c *check.C) {
        req := httptest.NewRequest("GET", "/arvados/v1/users/current", nil)
-       user, ok, err := s.handler.(*Handler).validateAPItoken(req, arvadostest.ActiveToken)
+       user, ok, err := s.handler.validateAPItoken(req, arvadostest.ActiveToken)
        c.Assert(err, check.IsNil)
        c.Check(ok, check.Equals, true)
        c.Check(user.Authorization.UUID, check.Equals, arvadostest.ActiveTokenUUID)
@@ -287,7 +287,7 @@ func (s *HandlerSuite) TestValidateV1APIToken(c *check.C) {
 
 func (s *HandlerSuite) TestValidateV2APIToken(c *check.C) {
        req := httptest.NewRequest("GET", "/arvados/v1/users/current", nil)
-       user, ok, err := s.handler.(*Handler).validateAPItoken(req, arvadostest.ActiveTokenV2)
+       user, ok, err := s.handler.validateAPItoken(req, arvadostest.ActiveTokenV2)
        c.Assert(err, check.IsNil)
        c.Check(ok, check.Equals, true)
        c.Check(user.Authorization.UUID, check.Equals, arvadostest.ActiveTokenUUID)
@@ -319,11 +319,11 @@ func (s *HandlerSuite) TestValidateRemoteToken(c *check.C) {
 
 func (s *HandlerSuite) TestCreateAPIToken(c *check.C) {
        req := httptest.NewRequest("GET", "/arvados/v1/users/current", nil)
-       auth, err := s.handler.(*Handler).createAPItoken(req, arvadostest.ActiveUserUUID, nil)
+       auth, err := s.handler.createAPItoken(req, arvadostest.ActiveUserUUID, nil)
        c.Assert(err, check.IsNil)
        c.Check(auth.Scopes, check.DeepEquals, []string{"all"})
 
-       user, ok, err := s.handler.(*Handler).validateAPItoken(req, auth.TokenV2())
+       user, ok, err := s.handler.validateAPItoken(req, auth.TokenV2())
        c.Assert(err, check.IsNil)
        c.Check(ok, check.Equals, true)
        c.Check(user.Authorization.UUID, check.Equals, auth.UUID)
@@ -392,10 +392,30 @@ func (s *HandlerSuite) TestGetObjects(c *check.C) {
        json.Unmarshal(resp.Body.Bytes(), &ksList)
        c.Assert(len(ksList.Items), check.Not(check.Equals), 0)
        ksUUID := ksList.Items[0].UUID
+       // Create a new token for the test user so that we're not comparing
+       // the ones from the fixtures.
+       req = httptest.NewRequest("POST", "/arvados/v1/api_client_authorizations",
+               strings.NewReader(`{
+                       "api_client_authorization": {
+                               "owner_uuid": "`+arvadostest.AdminUserUUID+`",
+                               "created_by_ip_address": "::1",
+                               "last_used_by_ip_address": "::1",
+                               "default_owner_uuid": "`+arvadostest.AdminUserUUID+`"
+                       }
+               }`))
+       req.Header.Set("Authorization", "Bearer "+arvadostest.SystemRootToken)
+       req.Header.Set("Content-type", "application/json")
+       resp = httptest.NewRecorder()
+       s.handler.ServeHTTP(resp, req)
+       c.Assert(resp.Code, check.Equals, http.StatusOK,
+               check.Commentf("%s", resp.Body.String()))
+       var auth arvados.APIClientAuthorization
+       json.Unmarshal(resp.Body.Bytes(), &auth)
+       c.Assert(auth.UUID, check.Not(check.Equals), "")
 
        testCases := map[string]map[string]bool{
                "api_clients/" + arvadostest.TrustedWorkbenchAPIClientUUID:     nil,
-               "api_client_authorizations/" + arvadostest.AdminTokenUUID:      nil,
+               "api_client_authorizations/" + auth.UUID:                       {"href": true, "modified_by_client_uuid": true, "modified_by_user_uuid": true},
                "authorized_keys/" + arvadostest.AdminAuthorizedKeysUUID:       nil,
                "collections/" + arvadostest.CollectionWithUniqueWordsUUID:     {"href": true},
                "containers/" + arvadostest.RunningContainerUUID:               nil,
@@ -411,7 +431,8 @@ func (s *HandlerSuite) TestGetObjects(c *check.C) {
                "workflows/" + arvadostest.WorkflowWithDefinitionYAMLUUID:      nil,
        }
        for url, skippedFields := range testCases {
-               s.CheckObjectType(c, "/arvados/v1/"+url, arvadostest.AdminToken, skippedFields)
+               c.Logf("Testing %q", url)
+               s.CheckObjectType(c, "/arvados/v1/"+url, auth.TokenV2(), skippedFields)
        }
 }
 
@@ -430,3 +451,30 @@ func (s *HandlerSuite) TestRedactRailsAPIHostFromErrors(c *check.C) {
        c.Check(jresp.Errors[0], check.Matches, `.*//railsapi\.internal/arvados/v1/collections/.*: 404 Not Found.*`)
        c.Check(jresp.Errors[0], check.Not(check.Matches), `(?ms).*127.0.0.1.*`)
 }
+
+func (s *HandlerSuite) TestTrashSweep(c *check.C) {
+       s.cluster.SystemRootToken = arvadostest.SystemRootToken
+       s.cluster.Collections.TrashSweepInterval = arvados.Duration(time.Second / 10)
+       s.handler.CheckHealth()
+       ctx := auth.NewContext(s.ctx, &auth.Credentials{Tokens: []string{arvadostest.ActiveTokenV2}})
+       coll, err := s.handler.federation.CollectionCreate(ctx, arvados.CreateOptions{Attrs: map[string]interface{}{"name": "test trash sweep"}, EnsureUniqueName: true})
+       c.Assert(err, check.IsNil)
+       defer s.handler.federation.CollectionDelete(ctx, arvados.DeleteOptions{UUID: coll.UUID})
+       db, err := s.handler.db(s.ctx)
+       c.Assert(err, check.IsNil)
+       _, err = db.ExecContext(s.ctx, `update collections set trash_at = $1, delete_at = $2 where uuid = $3`, time.Now().UTC().Add(time.Second/10), time.Now().UTC().Add(time.Hour), coll.UUID)
+       c.Assert(err, check.IsNil)
+       deadline := time.Now().Add(5 * time.Second)
+       for {
+               if time.Now().After(deadline) {
+                       c.Log("timed out")
+                       c.FailNow()
+               }
+               updated, err := s.handler.federation.CollectionGet(ctx, arvados.GetOptions{UUID: coll.UUID, IncludeTrash: true})
+               c.Assert(err, check.IsNil)
+               if updated.IsTrashed {
+                       break
+               }
+               time.Sleep(time.Second / 10)
+       }
+}
index 4cf6a683287ae211f58f13cdb664d087f2ceff00..1498da5a2193115ed7042572bf7485ea9afa05a1 100644 (file)
@@ -662,6 +662,48 @@ func (s *IntegrationSuite) TestIntermediateCluster(c *check.C) {
        }
 }
 
+// Test for #17785
+func (s *IntegrationSuite) TestFederatedApiClientAuthHandling(c *check.C) {
+       rootctx1, rootclnt1, _ := s.testClusters["z1111"].RootClients()
+       conn1 := s.testClusters["z1111"].Conn()
+
+       // Make sure LoginCluster is properly configured
+       for _, cls := range []string{"z1111", "z3333"} {
+               c.Check(
+                       s.testClusters[cls].Config.Clusters[cls].Login.LoginCluster,
+                       check.Equals, "z1111",
+                       check.Commentf("incorrect LoginCluster config on cluster %q", cls))
+       }
+       // Get user's UUID & attempt to create a token for it on the remote cluster
+       _, _, _, user := s.testClusters["z1111"].UserClients(rootctx1, c, conn1,
+               "user@example.com", true)
+       _, rootclnt3, _ := s.testClusters["z3333"].ClientsWithToken(rootclnt1.AuthToken)
+       var resp arvados.APIClientAuthorization
+       err := rootclnt3.RequestAndDecode(
+               &resp, "POST", "arvados/v1/api_client_authorizations", nil,
+               map[string]interface{}{
+                       "api_client_authorization": map[string]string{
+                               "owner_uuid": user.UUID,
+                       },
+               },
+       )
+       c.Assert(err, check.IsNil)
+       newTok := resp.TokenV2()
+       c.Assert(newTok, check.Not(check.Equals), "")
+
+       // Confirm the token is from z1111
+       c.Assert(strings.HasPrefix(newTok, "v2/z1111-gj3su-"), check.Equals, true)
+
+       // Confirm the token works and is from the correct user
+       _, rootclnt3bis, _ := s.testClusters["z3333"].ClientsWithToken(newTok)
+       var curUser arvados.User
+       err = rootclnt3bis.RequestAndDecode(
+               &curUser, "GET", "arvados/v1/users/current", nil, nil,
+       )
+       c.Assert(err, check.IsNil)
+       c.Assert(curUser.UUID, check.Equals, user.UUID)
+}
+
 // Test for bug #18076
 func (s *IntegrationSuite) TestStaleCachedUserRecord(c *check.C) {
        rootctx1, _, _ := s.testClusters["z1111"].RootClients()
@@ -670,13 +712,11 @@ func (s *IntegrationSuite) TestStaleCachedUserRecord(c *check.C) {
        conn3 := s.testClusters["z3333"].Conn()
 
        // Make sure LoginCluster is properly configured
-       for cls := range s.testClusters {
-               if cls == "z1111" || cls == "z3333" {
-                       c.Check(
-                               s.testClusters[cls].Config.Clusters[cls].Login.LoginCluster,
-                               check.Equals, "z1111",
-                               check.Commentf("incorrect LoginCluster config on cluster %q", cls))
-               }
+       for _, cls := range []string{"z1111", "z3333"} {
+               c.Check(
+                       s.testClusters[cls].Config.Clusters[cls].Login.LoginCluster,
+                       check.Equals, "z1111",
+                       check.Commentf("incorrect LoginCluster config on cluster %q", cls))
        }
 
        for testCaseNr, testCase := range []struct {
index 2a77357677b3d7f064832d471257866ef356b07a..70037cc501401375ee107d8e243f21e8f15c3cb5 100644 (file)
@@ -210,10 +210,9 @@ func (s *ContainerGatewaySuite) TestConnect(c *check.C) {
                // Receive binary
                _, err = io.ReadFull(sshconn.Conn, buf[:4])
                c.Check(err, check.IsNil)
-               c.Check(buf[:4], check.DeepEquals, []byte{0, 0, 1, 0xfc})
 
                // If we can get this far into an SSH handshake...
-               c.Log("success, tunnel is working")
+               c.Logf("was able to read %x -- success, tunnel is working", buf[:4])
        }()
        select {
        case <-done:
index 3c7b01baad1361735ebe37b4ef6df7157d1eb750..2b20491a04a426f50dbb354b9c8e0a7e86f833ea 100644 (file)
@@ -147,13 +147,13 @@ func (conn *Conn) CreateAPIClientAuthorization(ctx context.Context, rootToken st
                        tokensecret = tokenparts[2]
                }
        }
-       var exp sql.NullString
+       var exp sql.NullTime
        var scopes []byte
        err = tx.QueryRowxContext(ctx, "select uuid, api_token, expires_at, scopes from api_client_authorizations where api_token=$1", tokensecret).Scan(&resp.UUID, &resp.APIToken, &exp, &scopes)
        if err != nil {
                return
        }
-       resp.ExpiresAt = exp.String
+       resp.ExpiresAt = exp.Time
        if len(scopes) > 0 {
                err = json.Unmarshal(scopes, &resp.Scopes)
                if err != nil {
index 6182469ac378d58b1e1f864bf4d98a6b48a022fb..e076f7e1289c2b7ad48c6b7fb7e8782fd85ff1ce 100644 (file)
@@ -408,11 +408,8 @@ func (ta *oidcTokenAuthorizer) registerToken(ctx context.Context, tok string) er
                // cached positive result
                aca := cached.(arvados.APIClientAuthorization)
                var expiring bool
-               if aca.ExpiresAt != "" {
-                       t, err := time.Parse(time.RFC3339Nano, aca.ExpiresAt)
-                       if err != nil {
-                               return fmt.Errorf("error parsing expires_at value: %w", err)
-                       }
+               if !aca.ExpiresAt.IsZero() {
+                       t := aca.ExpiresAt
                        expiring = t.Before(time.Now().Add(time.Minute))
                }
                if !expiring {
@@ -505,7 +502,7 @@ func (ta *oidcTokenAuthorizer) registerToken(ctx context.Context, tok string) er
        if err != nil {
                return err
        }
-       aca.ExpiresAt = exp.Format(time.RFC3339Nano)
+       aca.ExpiresAt = exp
        ta.cache.Add(tok, aca)
        return nil
 }
index 01126bcb49a130440ec56bae76dbb78590dc9a3b..c0c599be8bcd4f1acb6fbbc5ed90e6541cf08dfe 100644 (file)
@@ -138,6 +138,7 @@ func (rtr *router) sendError(w http.ResponseWriter, err error) {
 }
 
 var infixMap = map[string]interface{}{
+       "gj3su": arvados.APIClientAuthorization{},
        "4zz18": arvados.Collection{},
        "xvhdp": arvados.ContainerRequest{},
        "dz642": arvados.Container{},
@@ -150,6 +151,11 @@ var infixMap = map[string]interface{}{
        "7fd4e": arvados.Workflow{},
 }
 
+var specialKindTransforms = map[string]string{
+       "arvados.APIClientAuthorization":     "arvados#apiClientAuthorization",
+       "arvados.APIClientAuthorizationList": "arvados#apiClientAuthorizationList",
+}
+
 var mungeKind = regexp.MustCompile(`\..`)
 
 func kind(resp interface{}) string {
@@ -157,6 +163,9 @@ func kind(resp interface{}) string {
        if !strings.HasPrefix(t, "arvados.") {
                return ""
        }
+       if k, ok := specialKindTransforms[t]; ok {
+               return k
+       }
        return mungeKind.ReplaceAllStringFunc(t, func(s string) string {
                // "arvados.CollectionList" => "arvados#collectionList"
                return "#" + strings.ToLower(s[1:])
index 02e06279f1168adca61999a543a9e82ad059e424..2cfcc4fc28287c8ee44166277ecaffe23d42e293 100644 (file)
@@ -384,6 +384,48 @@ func (rtr *router) addRoutes() {
                                return rtr.backend.SpecimenDelete(ctx, *opts.(*arvados.DeleteOptions))
                        },
                },
+               {
+                       arvados.EndpointAPIClientAuthorizationCreate,
+                       func() interface{} { return &arvados.CreateOptions{} },
+                       func(ctx context.Context, opts interface{}) (interface{}, error) {
+                               return rtr.backend.APIClientAuthorizationCreate(ctx, *opts.(*arvados.CreateOptions))
+                       },
+               },
+               {
+                       arvados.EndpointAPIClientAuthorizationUpdate,
+                       func() interface{} { return &arvados.UpdateOptions{} },
+                       func(ctx context.Context, opts interface{}) (interface{}, error) {
+                               return rtr.backend.APIClientAuthorizationUpdate(ctx, *opts.(*arvados.UpdateOptions))
+                       },
+               },
+               {
+                       arvados.EndpointAPIClientAuthorizationDelete,
+                       func() interface{} { return &arvados.DeleteOptions{} },
+                       func(ctx context.Context, opts interface{}) (interface{}, error) {
+                               return rtr.backend.APIClientAuthorizationDelete(ctx, *opts.(*arvados.DeleteOptions))
+                       },
+               },
+               {
+                       arvados.EndpointAPIClientAuthorizationList,
+                       func() interface{} { return &arvados.ListOptions{} },
+                       func(ctx context.Context, opts interface{}) (interface{}, error) {
+                               return rtr.backend.APIClientAuthorizationList(ctx, *opts.(*arvados.ListOptions))
+                       },
+               },
+               {
+                       arvados.EndpointAPIClientAuthorizationCurrent,
+                       func() interface{} { return &arvados.GetOptions{} },
+                       func(ctx context.Context, opts interface{}) (interface{}, error) {
+                               return rtr.backend.APIClientAuthorizationCurrent(ctx, *opts.(*arvados.GetOptions))
+                       },
+               },
+               {
+                       arvados.EndpointAPIClientAuthorizationGet,
+                       func() interface{} { return &arvados.GetOptions{} },
+                       func(ctx context.Context, opts interface{}) (interface{}, error) {
+                               return rtr.backend.APIClientAuthorizationGet(ctx, *opts.(*arvados.GetOptions))
+                       },
+               },
                {
                        arvados.EndpointUserCreate,
                        func() interface{} { return &arvados.CreateOptions{} },
index 25f47bc3bac4f801f2aa33b90e2ab935b0f651f9..1148068d70896c2ff4b16074c731fedf23bb5bbb 100644 (file)
@@ -572,6 +572,13 @@ func (conn *Conn) SpecimenDelete(ctx context.Context, options arvados.DeleteOpti
        return resp, err
 }
 
+func (conn *Conn) SysTrashSweep(ctx context.Context, options struct{}) (struct{}, error) {
+       ep := arvados.EndpointSysTrashSweep
+       var resp struct{}
+       err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
+       return resp, err
+}
+
 func (conn *Conn) UserCreate(ctx context.Context, options arvados.CreateOptions) (arvados.User, error) {
        ep := arvados.EndpointUserCreate
        var resp arvados.User
@@ -645,6 +652,36 @@ func (conn *Conn) APIClientAuthorizationCurrent(ctx context.Context, options arv
        err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
        return resp, err
 }
+func (conn *Conn) APIClientAuthorizationCreate(ctx context.Context, options arvados.CreateOptions) (arvados.APIClientAuthorization, error) {
+       ep := arvados.EndpointAPIClientAuthorizationCreate
+       var resp arvados.APIClientAuthorization
+       err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
+       return resp, err
+}
+func (conn *Conn) APIClientAuthorizationUpdate(ctx context.Context, options arvados.UpdateOptions) (arvados.APIClientAuthorization, error) {
+       ep := arvados.EndpointAPIClientAuthorizationUpdate
+       var resp arvados.APIClientAuthorization
+       err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
+       return resp, err
+}
+func (conn *Conn) APIClientAuthorizationDelete(ctx context.Context, options arvados.DeleteOptions) (arvados.APIClientAuthorization, error) {
+       ep := arvados.EndpointAPIClientAuthorizationDelete
+       var resp arvados.APIClientAuthorization
+       err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
+       return resp, err
+}
+func (conn *Conn) APIClientAuthorizationList(ctx context.Context, options arvados.ListOptions) (arvados.APIClientAuthorizationList, error) {
+       ep := arvados.EndpointAPIClientAuthorizationList
+       var resp arvados.APIClientAuthorizationList
+       err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
+       return resp, err
+}
+func (conn *Conn) APIClientAuthorizationGet(ctx context.Context, options arvados.GetOptions) (arvados.APIClientAuthorization, error) {
+       ep := arvados.EndpointAPIClientAuthorizationGet
+       var resp arvados.APIClientAuthorization
+       err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
+       return resp, err
+}
 
 type UserSessionAuthInfo struct {
        UserUUID        string    `json:"user_uuid"`
index b2b3365a2015b2ac899a3b62f45d563042267ac9..4f3d4a56834dad539363be635718ca3b0758d3b5 100644 (file)
@@ -35,11 +35,14 @@ func integrationTestCluster() *arvados.Cluster {
 // provided by the integration-testing environment.
 func newServerFromIntegrationTestEnv(c *check.C) *httpserver.Server {
        log := ctxlog.TestLogger(c)
-
-       handler := &Handler{Cluster: &arvados.Cluster{
-               ClusterID:  "zzzzz",
-               PostgreSQL: integrationTestCluster().PostgreSQL,
-       }}
+       ctx := ctxlog.Context(context.Background(), log)
+       handler := &Handler{
+               Cluster: &arvados.Cluster{
+                       ClusterID:  "zzzzz",
+                       PostgreSQL: integrationTestCluster().PostgreSQL,
+               },
+               BackgroundContext: ctx,
+       }
        handler.Cluster.TLS.Insecure = true
        handler.Cluster.Collections.BlobSigning = true
        handler.Cluster.Collections.BlobSigningKey = arvadostest.BlobSigningKey
@@ -49,10 +52,8 @@ func newServerFromIntegrationTestEnv(c *check.C) *httpserver.Server {
 
        srv := &httpserver.Server{
                Server: http.Server{
-                       BaseContext: func(net.Listener) context.Context {
-                               return ctxlog.Context(context.Background(), log)
-                       },
-                       Handler: httpserver.AddRequestIDs(httpserver.LogRequests(handler)),
+                       BaseContext: func(net.Listener) context.Context { return ctx },
+                       Handler:     httpserver.AddRequestIDs(httpserver.LogRequests(handler)),
                },
                Addr: ":",
        }
diff --git a/lib/controller/trash.go b/lib/controller/trash.go
new file mode 100644 (file)
index 0000000..551b2f9
--- /dev/null
@@ -0,0 +1,33 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+package controller
+
+import (
+       "time"
+
+       "git.arvados.org/arvados.git/lib/controller/dblock"
+       "git.arvados.org/arvados.git/sdk/go/auth"
+       "git.arvados.org/arvados.git/sdk/go/ctxlog"
+)
+
+func (h *Handler) trashSweepWorker() {
+       sleep := h.Cluster.Collections.TrashSweepInterval.Duration()
+       logger := ctxlog.FromContext(h.BackgroundContext).WithField("worker", "trash sweep")
+       ctx := ctxlog.Context(h.BackgroundContext, logger)
+       if sleep <= 0 {
+               logger.Debugf("Collections.TrashSweepInterval is %v, not running worker", sleep)
+               return
+       }
+       dblock.TrashSweep.Lock(ctx, h.db)
+       defer dblock.TrashSweep.Unlock()
+       for time.Sleep(sleep); ctx.Err() == nil; time.Sleep(sleep) {
+               dblock.TrashSweep.Check()
+               ctx := auth.NewContext(ctx, &auth.Credentials{Tokens: []string{h.Cluster.SystemRootToken}})
+               _, err := h.federation.SysTrashSweep(ctx, struct{}{})
+               if err != nil {
+                       logger.WithError(err).Info("trash sweep failed")
+               }
+       }
+}
index 63a0ada54ee733629b8bd44b7e3bff6df5f7b793..dd0169025bb3cba58049dcdc9151738ea7b48463 100644 (file)
@@ -1701,7 +1701,7 @@ func (command) RunCommand(prog string, args []string, stdin io.Reader, stdout, s
 
        if ok, code := cmd.ParseFlags(flags, prog, args, "container-uuid", stderr); !ok {
                return code
-       } else if flags.NArg() != 1 {
+       } else if !*list && flags.NArg() != 1 {
                fmt.Fprintf(stderr, "missing required argument: container-uuid (try -help)\n")
                return 2
        }
index 6e35b7de929f8843bdd7bdb848698ca96c62a123..537d52a072d6a503262b1a228c868afc8f28b151 100644 (file)
@@ -167,7 +167,7 @@ func (disp *dispatcher) runContainer(_ *dispatch.Dispatcher, ctr arvados.Contain
 
        if ctr.State != dispatch.Locked {
                // already started by prior invocation
-       } else if _, ok := disp.lsfqueue.JobID(ctr.UUID); !ok {
+       } else if _, ok := disp.lsfqueue.Lookup(ctr.UUID); !ok {
                disp.logger.Printf("Submitting container %s to LSF", ctr.UUID)
                cmd := []string{disp.Cluster.Containers.CrunchRunCommand}
                cmd = append(cmd, "--runtime-engine="+disp.Cluster.Containers.RuntimeEngine)
@@ -181,16 +181,38 @@ func (disp *dispatcher) runContainer(_ *dispatch.Dispatcher, ctr arvados.Contain
        disp.logger.Printf("Start monitoring container %v in state %q", ctr.UUID, ctr.State)
        defer disp.logger.Printf("Done monitoring container %s", ctr.UUID)
 
-       // If the container disappears from the lsf queue, there is
-       // no point in waiting for further dispatch updates: just
-       // clean up and return.
        go func(uuid string) {
+               cancelled := false
                for ctx.Err() == nil {
-                       if _, ok := disp.lsfqueue.JobID(uuid); !ok {
+                       qent, ok := disp.lsfqueue.Lookup(uuid)
+                       if !ok {
+                               // If the container disappears from
+                               // the lsf queue, there is no point in
+                               // waiting for further dispatch
+                               // updates: just clean up and return.
                                disp.logger.Printf("container %s job disappeared from LSF queue", uuid)
                                cancel()
                                return
                        }
+                       if !cancelled && qent.Stat == "PEND" && strings.Contains(qent.PendReason, "There are no suitable hosts for the job") {
+                               disp.logger.Printf("container %s: %s", uuid, qent.PendReason)
+                               err := disp.arvDispatcher.Arv.Update("containers", uuid, arvadosclient.Dict{
+                                       "container": map[string]interface{}{
+                                               "runtime_status": map[string]string{
+                                                       "error": qent.PendReason,
+                                               },
+                                       },
+                               }, nil)
+                               if err != nil {
+                                       disp.logger.Printf("error setting runtime_status on %s: %s", uuid, err)
+                                       continue // retry
+                               }
+                               err = disp.arvDispatcher.UpdateState(uuid, dispatch.Cancelled)
+                               if err != nil {
+                                       continue // retry (UpdateState() already logged the error)
+                               }
+                               cancelled = true
+                       }
                }
        }(ctr.UUID)
 
@@ -236,10 +258,10 @@ func (disp *dispatcher) runContainer(_ *dispatch.Dispatcher, ctr arvados.Contain
        // from the queue.
        ticker := time.NewTicker(5 * time.Second)
        defer ticker.Stop()
-       for jobid, ok := disp.lsfqueue.JobID(ctr.UUID); ok; _, ok = disp.lsfqueue.JobID(ctr.UUID) {
-               err := disp.lsfcli.Bkill(jobid)
+       for qent, ok := disp.lsfqueue.Lookup(ctr.UUID); ok; _, ok = disp.lsfqueue.Lookup(ctr.UUID) {
+               err := disp.lsfcli.Bkill(qent.ID)
                if err != nil {
-                       disp.logger.Warnf("%s: bkill(%d): %s", ctr.UUID, jobid, err)
+                       disp.logger.Warnf("%s: bkill(%s): %s", ctr.UUID, qent.ID, err)
                }
                <-ticker.C
        }
@@ -262,10 +284,10 @@ func (disp *dispatcher) submit(container arvados.Container, crunchRunCommand []s
 }
 
 func (disp *dispatcher) bkill(ctr arvados.Container) {
-       if jobid, ok := disp.lsfqueue.JobID(ctr.UUID); !ok {
+       if qent, ok := disp.lsfqueue.Lookup(ctr.UUID); !ok {
                disp.logger.Debugf("bkill(%s): redundant, job not in queue", ctr.UUID)
-       } else if err := disp.lsfcli.Bkill(jobid); err != nil {
-               disp.logger.Warnf("%s: bkill(%d): %s", ctr.UUID, jobid, err)
+       } else if err := disp.lsfcli.Bkill(qent.ID); err != nil {
+               disp.logger.Warnf("%s: bkill(%s): %s", ctr.UUID, qent.ID, err)
        }
 }
 
index 641453e5480ced43609efcb499d5dbff61383cae..c044df09f65d42f5f4aad7903b60e27160d5ec98 100644 (file)
@@ -6,6 +6,7 @@ package lsf
 
 import (
        "context"
+       "encoding/json"
        "fmt"
        "math/rand"
        "os/exec"
@@ -29,7 +30,8 @@ func Test(t *testing.T) {
 var _ = check.Suite(&suite{})
 
 type suite struct {
-       disp *dispatcher
+       disp     *dispatcher
+       crTooBig arvados.ContainerRequest
 }
 
 func (s *suite) TearDownTest(c *check.C) {
@@ -46,6 +48,22 @@ func (s *suite) SetUpTest(c *check.C) {
        s.disp.lsfcli.stubCommand = func(string, ...string) *exec.Cmd {
                return exec.Command("bash", "-c", "echo >&2 unimplemented stub; false")
        }
+       err = arvados.NewClientFromEnv().RequestAndDecode(&s.crTooBig, "POST", "arvados/v1/container_requests", nil, map[string]interface{}{
+               "container_request": map[string]interface{}{
+                       "runtime_constraints": arvados.RuntimeConstraints{
+                               RAM:   1000000000000,
+                               VCPUs: 1,
+                       },
+                       "container_image":     arvadostest.DockerImage112PDH,
+                       "command":             []string{"sleep", "1"},
+                       "mounts":              map[string]arvados.Mount{"/mnt/out": {Kind: "tmp", Capacity: 1000}},
+                       "output_path":         "/mnt/out",
+                       "state":               arvados.ContainerRequestStateCommitted,
+                       "priority":            1,
+                       "container_count_max": 1,
+               },
+       })
+       c.Assert(err, check.IsNil)
 }
 
 type lsfstub struct {
@@ -82,7 +100,10 @@ func (stub lsfstub) stubCommand(s *suite, c *check.C) func(prog string, args ...
                                        "-J", arvadostest.LockedContainerUUID,
                                        "-n", "4",
                                        "-D", "11701MB",
-                                       "-R", "rusage[mem=11701MB:tmp=0MB] span[hosts=1]"})
+                                       "-R", "rusage[mem=11701MB:tmp=0MB] span[hosts=1]",
+                                       "-R", "select[mem>=11701MB]",
+                                       "-R", "select[tmp>=0MB]",
+                                       "-R", "select[ncpus>=4]"})
                                mtx.Lock()
                                fakejobq[nextjobid] = args[1]
                                nextjobid++
@@ -92,7 +113,23 @@ func (stub lsfstub) stubCommand(s *suite, c *check.C) func(prog string, args ...
                                        "-J", arvadostest.QueuedContainerUUID,
                                        "-n", "4",
                                        "-D", "11701MB",
-                                       "-R", "rusage[mem=11701MB:tmp=45777MB] span[hosts=1]"})
+                                       "-R", "rusage[mem=11701MB:tmp=45777MB] span[hosts=1]",
+                                       "-R", "select[mem>=11701MB]",
+                                       "-R", "select[tmp>=45777MB]",
+                                       "-R", "select[ncpus>=4]"})
+                               mtx.Lock()
+                               fakejobq[nextjobid] = args[1]
+                               nextjobid++
+                               mtx.Unlock()
+                       case s.crTooBig.ContainerUUID:
+                               c.Check(args, check.DeepEquals, []string{
+                                       "-J", s.crTooBig.ContainerUUID,
+                                       "-n", "1",
+                                       "-D", "954187MB",
+                                       "-R", "rusage[mem=954187MB:tmp=256MB] span[hosts=1]",
+                                       "-R", "select[mem>=954187MB]",
+                                       "-R", "select[tmp>=256MB]",
+                                       "-R", "select[ncpus>=1]"})
                                mtx.Lock()
                                fakejobq[nextjobid] = args[1]
                                nextjobid++
@@ -103,13 +140,31 @@ func (stub lsfstub) stubCommand(s *suite, c *check.C) func(prog string, args ...
                        }
                        return exec.Command("echo", "submitted job")
                case "bjobs":
-                       c.Check(args, check.DeepEquals, []string{"-u", "all", "-noheader", "-o", "jobid stat job_name:30"})
-                       out := ""
+                       c.Check(args, check.DeepEquals, []string{"-u", "all", "-o", "jobid stat job_name pend_reason", "-json"})
+                       var records []map[string]interface{}
                        for jobid, uuid := range fakejobq {
-                               out += fmt.Sprintf(`%d %s %s\n`, jobid, "RUN", uuid)
+                               stat, reason := "RUN", ""
+                               if uuid == s.crTooBig.ContainerUUID {
+                                       // The real bjobs output includes a trailing ';' here:
+                                       stat, reason = "PEND", "There are no suitable hosts for the job;"
+                               }
+                               records = append(records, map[string]interface{}{
+                                       "JOBID":       fmt.Sprintf("%d", jobid),
+                                       "STAT":        stat,
+                                       "JOB_NAME":    uuid,
+                                       "PEND_REASON": reason,
+                               })
                        }
-                       c.Logf("bjobs out: %q", out)
-                       return exec.Command("printf", out)
+                       out, err := json.Marshal(map[string]interface{}{
+                               "COMMAND": "bjobs",
+                               "JOBS":    len(fakejobq),
+                               "RECORDS": records,
+                       })
+                       if err != nil {
+                               panic(err)
+                       }
+                       c.Logf("bjobs out: %s", out)
+                       return exec.Command("printf", string(out))
                case "bkill":
                        killid, _ := strconv.Atoi(args[0])
                        if uuid, ok := fakejobq[killid]; !ok {
@@ -137,6 +192,7 @@ func (s *suite) TestSubmit(c *check.C) {
                sudoUser:  s.disp.Cluster.Containers.LSF.BsubSudoUser,
        }.stubCommand(s, c)
        s.disp.Start()
+
        deadline := time.Now().Add(20 * time.Second)
        for range time.NewTicker(time.Second).C {
                if time.Now().After(deadline) {
@@ -144,23 +200,37 @@ func (s *suite) TestSubmit(c *check.C) {
                        break
                }
                // "queuedcontainer" should be running
-               if _, ok := s.disp.lsfqueue.JobID(arvadostest.QueuedContainerUUID); !ok {
+               if _, ok := s.disp.lsfqueue.Lookup(arvadostest.QueuedContainerUUID); !ok {
                        continue
                }
                // "lockedcontainer" should be cancelled because it
                // has priority 0 (no matching container requests)
-               if _, ok := s.disp.lsfqueue.JobID(arvadostest.LockedContainerUUID); ok {
+               if _, ok := s.disp.lsfqueue.Lookup(arvadostest.LockedContainerUUID); ok {
+                       continue
+               }
+               // "crTooBig" should be cancelled because lsf stub
+               // reports there is no suitable instance type
+               if _, ok := s.disp.lsfqueue.Lookup(s.crTooBig.ContainerUUID); ok {
                        continue
                }
                var ctr arvados.Container
                if err := s.disp.arvDispatcher.Arv.Get("containers", arvadostest.LockedContainerUUID, nil, &ctr); err != nil {
                        c.Logf("error getting container state for %s: %s", arvadostest.LockedContainerUUID, err)
                        continue
-               }
-               if ctr.State != arvados.ContainerStateQueued {
+               } else if ctr.State != arvados.ContainerStateQueued {
                        c.Logf("LockedContainer is not in the LSF queue but its arvados record has not been updated to state==Queued (state is %q)", ctr.State)
                        continue
                }
+
+               if err := s.disp.arvDispatcher.Arv.Get("containers", s.crTooBig.ContainerUUID, nil, &ctr); err != nil {
+                       c.Logf("error getting container state for %s: %s", s.crTooBig.ContainerUUID, err)
+                       continue
+               } else if ctr.State != arvados.ContainerStateCancelled {
+                       c.Logf("container %s is not in the LSF queue but its arvados record has not been updated to state==Cancelled (state is %q)", s.crTooBig.ContainerUUID, ctr.State)
+                       continue
+               } else {
+                       c.Check(ctr.RuntimeStatus["error"], check.Equals, "There are no suitable hosts for the job;")
+               }
                c.Log("reached desired state")
                break
        }
index 9d712ee97fa02013eff3035edc0cdb7cd43fab64..d17559568c13bbadbee2da66c6c443b99ef552f1 100644 (file)
@@ -6,6 +6,7 @@ package lsf
 
 import (
        "bytes"
+       "encoding/json"
        "fmt"
        "os"
        "os/exec"
@@ -16,9 +17,10 @@ import (
 )
 
 type bjobsEntry struct {
-       id   int
-       name string
-       stat string
+       ID         string `json:"JOBID"`
+       Name       string `json:"JOB_NAME"`
+       Stat       string `json:"STAT"`
+       PendReason string `json:"PEND_REASON"`
 }
 
 type lsfcli struct {
@@ -53,29 +55,21 @@ func (cli lsfcli) Bsub(script []byte, args []string, arv *arvados.Client) error
 
 func (cli lsfcli) Bjobs() ([]bjobsEntry, error) {
        cli.logger.Debugf("Bjobs()")
-       cmd := cli.command("bjobs", "-u", "all", "-noheader", "-o", "jobid stat job_name:30")
+       cmd := cli.command("bjobs", "-u", "all", "-o", "jobid stat job_name pend_reason", "-json")
        buf, err := cmd.Output()
        if err != nil {
                return nil, errWithStderr(err)
        }
-       var bjobs []bjobsEntry
-       for _, line := range strings.Split(string(buf), "\n") {
-               if line == "" {
-                       continue
-               }
-               var ent bjobsEntry
-               if _, err := fmt.Sscan(line, &ent.id, &ent.stat, &ent.name); err != nil {
-                       cli.logger.Warnf("ignoring unparsed line in bjobs output: %q", line)
-                       continue
-               }
-               bjobs = append(bjobs, ent)
+       var resp struct {
+               Records []bjobsEntry `json:"RECORDS"`
        }
-       return bjobs, nil
+       err = json.Unmarshal(buf, &resp)
+       return resp.Records, err
 }
 
-func (cli lsfcli) Bkill(id int) error {
-       cli.logger.Infof("Bkill(%d)", id)
-       cmd := cli.command("bkill", fmt.Sprintf("%d", id))
+func (cli lsfcli) Bkill(id string) error {
+       cli.logger.Infof("Bkill(%s)", id)
+       cmd := cli.command("bkill", id)
        buf, err := cmd.CombinedOutput()
        if err == nil || strings.Index(string(buf), "already finished") >= 0 {
                return nil
index 3c4fc4cb8cf6bc72e4cb1768041cd329e68eded1..3ed4d0c1820cfaad1340c1304902a7deabd0fcb7 100644 (file)
@@ -23,12 +23,12 @@ type lsfqueue struct {
        latest    map[string]bjobsEntry
 }
 
-// JobID waits for the next queue update (so even a job that was only
+// Lookup waits for the next queue update (so even a job that was only
 // submitted a nanosecond ago will show up) and then returns the LSF
-// job ID corresponding to the given container UUID.
-func (q *lsfqueue) JobID(uuid string) (int, bool) {
+// queue information corresponding to the given container UUID.
+func (q *lsfqueue) Lookup(uuid string) (bjobsEntry, bool) {
        ent, ok := q.getNext()[uuid]
-       return ent.id, ok
+       return ent, ok
 }
 
 // All waits for the next queue update, then returns the names of all
@@ -94,7 +94,7 @@ func (q *lsfqueue) init() {
                        }
                        next := make(map[string]bjobsEntry, len(ents))
                        for _, ent := range ents {
-                               next[ent.name] = ent
+                               next[ent.Name] = ent
                        }
                        // Replace q.latest and notify all the
                        // goroutines that the "next update" they
index 0fdc13d1985d085c28db23615dd9ce1c673781cd..7409b18132981932b2fce6e4be1b5e1ec06d1f16 100644 (file)
@@ -68,6 +68,7 @@ var (
        EndpointLinkGet                       = APIEndpoint{"GET", "arvados/v1/links/{uuid}", ""}
        EndpointLinkList                      = APIEndpoint{"GET", "arvados/v1/links", ""}
        EndpointLinkDelete                    = APIEndpoint{"DELETE", "arvados/v1/links/{uuid}", ""}
+       EndpointSysTrashSweep                 = APIEndpoint{"POST", "sys/trash_sweep", ""}
        EndpointUserActivate                  = APIEndpoint{"POST", "arvados/v1/users/{uuid}/activate", ""}
        EndpointUserCreate                    = APIEndpoint{"POST", "arvados/v1/users", "user"}
        EndpointUserCurrent                   = APIEndpoint{"GET", "arvados/v1/users/current", ""}
@@ -84,6 +85,11 @@ var (
        EndpointUserBatchUpdate               = APIEndpoint{"PATCH", "arvados/v1/users/batch_update", ""}
        EndpointUserAuthenticate              = APIEndpoint{"POST", "arvados/v1/users/authenticate", ""}
        EndpointAPIClientAuthorizationCurrent = APIEndpoint{"GET", "arvados/v1/api_client_authorizations/current", ""}
+       EndpointAPIClientAuthorizationCreate  = APIEndpoint{"POST", "arvados/v1/api_client_authorizations", "api_client_authorization"}
+       EndpointAPIClientAuthorizationUpdate  = APIEndpoint{"PUT", "arvados/v1/api_client_authorizations/{uuid}", "api_client_authorization"}
+       EndpointAPIClientAuthorizationList    = APIEndpoint{"GET", "arvados/v1/api_client_authorizations", ""}
+       EndpointAPIClientAuthorizationDelete  = APIEndpoint{"DELETE", "arvados/v1/api_client_authorizations/{uuid}", ""}
+       EndpointAPIClientAuthorizationGet     = APIEndpoint{"GET", "arvados/v1/api_client_authorizations/{uuid}", ""}
 )
 
 type ContainerSSHOptions struct {
@@ -269,6 +275,7 @@ type API interface {
        SpecimenGet(ctx context.Context, options GetOptions) (Specimen, error)
        SpecimenList(ctx context.Context, options ListOptions) (SpecimenList, error)
        SpecimenDelete(ctx context.Context, options DeleteOptions) (Specimen, error)
+       SysTrashSweep(ctx context.Context, options struct{}) (struct{}, error)
        UserCreate(ctx context.Context, options CreateOptions) (User, error)
        UserUpdate(ctx context.Context, options UpdateOptions) (User, error)
        UserMerge(ctx context.Context, options UserMergeOptions) (User, error)
@@ -283,4 +290,9 @@ type API interface {
        UserBatchUpdate(context.Context, UserBatchUpdateOptions) (UserList, error)
        UserAuthenticate(ctx context.Context, options UserAuthenticateOptions) (APIClientAuthorization, error)
        APIClientAuthorizationCurrent(ctx context.Context, options GetOptions) (APIClientAuthorization, error)
+       APIClientAuthorizationCreate(ctx context.Context, options CreateOptions) (APIClientAuthorization, error)
+       APIClientAuthorizationList(ctx context.Context, options ListOptions) (APIClientAuthorizationList, error)
+       APIClientAuthorizationDelete(ctx context.Context, options DeleteOptions) (APIClientAuthorization, error)
+       APIClientAuthorizationUpdate(ctx context.Context, options UpdateOptions) (APIClientAuthorization, error)
+       APIClientAuthorizationGet(ctx context.Context, options GetOptions) (APIClientAuthorization, error)
 }
index 7c17cdef04debdf4540b6c2de761673c34d01883..c920d2dc348ede29355f3f062a5e1b445e4e02c3 100644 (file)
@@ -4,12 +4,26 @@
 
 package arvados
 
+import "time"
+
 // APIClientAuthorization is an arvados#apiClientAuthorization resource.
 type APIClientAuthorization struct {
-       UUID      string   `json:"uuid"`
-       APIToken  string   `json:"api_token"`
-       ExpiresAt string   `json:"expires_at"`
-       Scopes    []string `json:"scopes"`
+       UUID                 string    `json:"uuid"`
+       APIClientID          int       `json:"api_client_id"`
+       APIToken             string    `json:"api_token"`
+       CreatedAt            time.Time `json:"created_at"`
+       CreatedByIPAddress   string    `json:"created_by_ip_address"`
+       DefaultOwnerUUID     string    `json:"default_owner_uuid"`
+       Etag                 string    `json:"etag"`
+       ExpiresAt            time.Time `json:"expires_at"`
+       LastUsedAt           time.Time `json:"last_used_at"`
+       LastUsedByIPAddress  string    `json:"last_used_by_ip_address"`
+       ModifiedAt           time.Time `json:"modified_at"`
+       ModifiedByClientUUID string    `json:"modified_by_client_uuid"`
+       ModifiedByUserUUID   string    `json:"modified_by_user_uuid"`
+       OwnerUUID            string    `json:"owner_uuid"`
+       Scopes               []string  `json:"scopes"`
+       UserID               int       `json:"user_id"`
 }
 
 // APIClientAuthorizationList is an arvados#apiClientAuthorizationList resource.
index 13bb3bf80de70c11e4567ab69ea56c9c03b28a8f..5ec828667fc940ace2c3f59b6cdc643139ae3b14 100644 (file)
@@ -217,6 +217,8 @@ func (c *Client) DoAndDecode(dst interface{}, req *http.Request) error {
                return err
        }
        switch {
+       case resp.StatusCode == http.StatusNoContent:
+               return nil
        case resp.StatusCode == http.StatusOK && dst == nil:
                return nil
        case resp.StatusCode == http.StatusOK:
index 0af477125b737a65f1fad46fce3009f5e27d1bcd..f49d29ce2b8cbbc7b9a8c564e20d86673588f58a 100644 (file)
@@ -209,6 +209,10 @@ func (as *APIStub) SpecimenDelete(ctx context.Context, options arvados.DeleteOpt
        as.appendCall(ctx, as.SpecimenDelete, options)
        return arvados.Specimen{}, as.Error
 }
+func (as *APIStub) SysTrashSweep(ctx context.Context, options struct{}) (struct{}, error) {
+       as.appendCall(ctx, as.SysTrashSweep, options)
+       return struct{}{}, as.Error
+}
 func (as *APIStub) UserCreate(ctx context.Context, options arvados.CreateOptions) (arvados.User, error) {
        as.appendCall(ctx, as.UserCreate, options)
        return arvados.User{}, as.Error
@@ -265,6 +269,26 @@ func (as *APIStub) APIClientAuthorizationCurrent(ctx context.Context, options ar
        as.appendCall(ctx, as.APIClientAuthorizationCurrent, options)
        return arvados.APIClientAuthorization{}, as.Error
 }
+func (as *APIStub) APIClientAuthorizationCreate(ctx context.Context, options arvados.CreateOptions) (arvados.APIClientAuthorization, error) {
+       as.appendCall(ctx, as.APIClientAuthorizationCreate, options)
+       return arvados.APIClientAuthorization{}, as.Error
+}
+func (as *APIStub) APIClientAuthorizationUpdate(ctx context.Context, options arvados.UpdateOptions) (arvados.APIClientAuthorization, error) {
+       as.appendCall(ctx, as.APIClientAuthorizationUpdate, options)
+       return arvados.APIClientAuthorization{}, as.Error
+}
+func (as *APIStub) APIClientAuthorizationDelete(ctx context.Context, options arvados.DeleteOptions) (arvados.APIClientAuthorization, error) {
+       as.appendCall(ctx, as.APIClientAuthorizationDelete, options)
+       return arvados.APIClientAuthorization{}, as.Error
+}
+func (as *APIStub) APIClientAuthorizationList(ctx context.Context, options arvados.ListOptions) (arvados.APIClientAuthorizationList, error) {
+       as.appendCall(ctx, as.APIClientAuthorizationList, options)
+       return arvados.APIClientAuthorizationList{}, as.Error
+}
+func (as *APIStub) APIClientAuthorizationGet(ctx context.Context, options arvados.GetOptions) (arvados.APIClientAuthorization, error) {
+       as.appendCall(ctx, as.APIClientAuthorizationGet, options)
+       return arvados.APIClientAuthorization{}, as.Error
+}
 
 func (as *APIStub) appendCall(ctx context.Context, method interface{}, options interface{}) {
        as.mtx.Lock()
diff --git a/sdk/go/arvadostest/api_test.go b/sdk/go/arvadostest/api_test.go
new file mode 100644 (file)
index 0000000..798d035
--- /dev/null
@@ -0,0 +1,10 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: Apache-2.0
+
+package arvadostest
+
+import "git.arvados.org/arvados.git/sdk/go/arvados"
+
+// Test that *APIStub implements arvados.API
+var _ arvados.API = &APIStub{}
index e9fbdb744d95b9ff2ac17261caf6de8bbffbedfb..6482215b1c29140e6f99d51a43c1b319f470301c 100644 (file)
@@ -28,7 +28,7 @@ public class Group extends Item {
     private String groupClass;
     @JsonProperty("description")
     private String description;
-    @JsonProperty("writable_by")
+    @JsonProperty(value = "writable_by", access = JsonProperty.Access.WRITE_ONLY)
     private List<String> writableBy;
     @JsonProperty("delete_at")
     private LocalDateTime deleteAt;
index a24f02a017473aeba273aa895f867801de9f55b9..1d1a20fc762b94ed95877e1d9106cfc1e986f530 100644 (file)
@@ -15,15 +15,19 @@ import com.fasterxml.jackson.annotation.JsonPropertyOrder;
 
 @JsonInclude(JsonInclude.Include.NON_NULL)
 @JsonIgnoreProperties(ignoreUnknown = true)
-@JsonPropertyOrder({ "name", "head_kind", "head_uuid", "link_class" })
+@JsonPropertyOrder({"name", "head_kind", "head_uuid", "link_class"})
 public class Link extends Item {
 
     @JsonProperty("name")
     private String name;
-    @JsonProperty("head_kind")
+    @JsonProperty(value = "head_kind", access = JsonProperty.Access.WRITE_ONLY)
     private String headKind;
     @JsonProperty("head_uuid")
     private String headUuid;
+    @JsonProperty("tail_uuid")
+    private String tailUuid;
+    @JsonProperty(value = "tail_kind", access = JsonProperty.Access.WRITE_ONLY)
+    private String tailKind;
     @JsonProperty("link_class")
     private String linkClass;
 
@@ -39,6 +43,14 @@ public class Link extends Item {
         return headUuid;
     }
 
+    public String getTailUuid() {
+        return tailUuid;
+    }
+
+    public String getTailKind() {
+        return tailKind;
+    }
+
     public String getLinkClass() {
         return linkClass;
     }
@@ -55,6 +67,14 @@ public class Link extends Item {
         this.headUuid = headUuid;
     }
 
+    public void setTailUuid(String tailUuid) {
+        this.tailUuid = tailUuid;
+    }
+
+    public void setTailKind(String tailKind) {
+        this.tailKind = tailKind;
+    }
+
     public void setLinkClass(String linkClass) {
         this.linkClass = linkClass;
     }
diff --git a/sdk/java-v2/src/test/java/org/arvados/client/api/client/LinkApiClientTest.java b/sdk/java-v2/src/test/java/org/arvados/client/api/client/LinkApiClientTest.java
new file mode 100644 (file)
index 0000000..f051b56
--- /dev/null
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) The Arvados Authors. All rights reserved.
+ *
+ * SPDX-License-Identifier: AGPL-3.0 OR Apache-2.0
+ *
+ */
+
+package org.arvados.client.api.client;
+
+import okhttp3.mockwebserver.RecordedRequest;
+import org.arvados.client.api.model.Link;
+import org.arvados.client.api.model.LinkList;
+import org.arvados.client.test.utils.ArvadosClientMockedWebServerTest;
+import org.arvados.client.test.utils.RequestMethod;
+import org.junit.Test;
+
+import static org.arvados.client.test.utils.ApiClientTestUtils.assertAuthorizationHeader;
+import static org.arvados.client.test.utils.ApiClientTestUtils.assertRequestMethod;
+import static org.arvados.client.test.utils.ApiClientTestUtils.assertRequestPath;
+import static org.arvados.client.test.utils.ApiClientTestUtils.getResponse;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.Assert.assertEquals;
+
+public class LinkApiClientTest extends ArvadosClientMockedWebServerTest {
+
+    private static final String RESOURCE = "links";
+
+    private final LinksApiClient client = new LinksApiClient(CONFIG);
+
+    @Test
+    public void listLinks() throws Exception {
+        // given
+        server.enqueue(getResponse("links-list"));
+
+        // when
+        LinkList actual = client.list();
+
+        // then
+        RecordedRequest request = server.takeRequest();
+        assertAuthorizationHeader(request);
+        assertRequestPath(request, RESOURCE);
+        assertRequestMethod(request, RequestMethod.GET);
+        assertThat(actual.getItemsAvailable()).isEqualTo(2);
+    }
+
+    @Test
+    public void getLink() throws Exception {
+        // given
+        server.enqueue(getResponse("links-get"));
+
+        String uuid = "arkau-o0j2j-huxuaxbi46s1yml";
+
+        // when
+        Link actual = client.get(uuid);
+
+        // then
+        RecordedRequest request = server.takeRequest();
+        assertAuthorizationHeader(request);
+        assertRequestPath(request, RESOURCE + "/" + uuid);
+        assertRequestMethod(request, RequestMethod.GET);
+        assertEquals(actual.getUuid(), uuid);
+        assertEquals(actual.getName(), "can_read");
+        assertEquals(actual.getHeadKind(), "arvados#group");
+        assertEquals(actual.getHeadUuid(), "arkau-j7d0g-fcedae2076pw56h");
+        assertEquals(actual.getTailUuid(), "ardev-tpzed-n3kzq4fvoks3uw4");
+        assertEquals(actual.getTailKind(), "arvados#user");
+        assertEquals(actual.getLinkClass(), "permission");
+    }
+
+    @Test
+    public void createLink() throws Exception {
+        // given
+        server.enqueue(getResponse("links-create"));
+
+        String name = "Star Link";
+
+        Link collection = new Link();
+        collection.setName(name);
+
+        // when
+        Link actual = client.create(collection);
+
+        // then
+        RecordedRequest request = server.takeRequest();
+        assertAuthorizationHeader(request);
+        assertRequestPath(request, RESOURCE);
+        assertRequestMethod(request, RequestMethod.POST);
+        assertThat(actual.getName()).isEqualTo(name);
+        assertEquals(actual.getName(), name);
+        assertEquals(actual.getUuid(), "arkau-o0j2j-huxuaxbi46s1yml");
+        assertEquals(actual.getHeadKind(), "arvados#group");
+        assertEquals(actual.getHeadUuid(), "arkau-j7d0g-fcedae2076pw56h");
+        assertEquals(actual.getTailUuid(), "ardev-tpzed-n3kzq4fvoks3uw4");
+        assertEquals(actual.getTailKind(), "arvados#user");
+        assertEquals(actual.getLinkClass(), "star");
+    }
+}
\ No newline at end of file
diff --git a/sdk/java-v2/src/test/resources/org/arvados/client/api/client/links-create.json b/sdk/java-v2/src/test/resources/org/arvados/client/api/client/links-create.json
new file mode 100644 (file)
index 0000000..0664d88
--- /dev/null
@@ -0,0 +1,18 @@
+{
+  "href": "/links/arkau-o0j2j-huxuaxbi46s1yml",
+  "kind": "arvados#link",
+  "etag": "zw1rlnbig0kpm9btw8us3pn9",
+  "uuid": "arkau-o0j2j-huxuaxbi46s1yml",
+  "owner_uuid": "arkau-tpzed-000000000000000",
+  "created_at": "2021-11-30T08:45:04.373354745Z",
+  "modified_by_client_uuid": null,
+  "modified_by_user_uuid": "ardev-tpzed-n3kzq4fvoks3uw4",
+  "modified_at": "2021-11-30T08:45:04.374489000Z",
+  "tail_uuid": "ardev-tpzed-n3kzq4fvoks3uw4",
+  "link_class": "star",
+  "name": "Star Link",
+  "head_uuid": "arkau-j7d0g-fcedae2076pw56h",
+  "head_kind": "arvados#group",
+  "tail_kind": "arvados#user",
+  "properties": {}
+}
\ No newline at end of file
diff --git a/sdk/java-v2/src/test/resources/org/arvados/client/api/client/links-get.json b/sdk/java-v2/src/test/resources/org/arvados/client/api/client/links-get.json
new file mode 100644 (file)
index 0000000..25f63bd
--- /dev/null
@@ -0,0 +1,18 @@
+{
+  "href": "/links/arkau-o0j2j-huxuaxbi46s1yml",
+  "kind": "arvados#link",
+  "etag": "zw1rlnbig0kpm9btw8us3pn9",
+  "uuid": "arkau-o0j2j-huxuaxbi46s1yml",
+  "owner_uuid": "arkau-tpzed-000000000000000",
+  "created_at": "2021-11-30T08:45:04.373354745Z",
+  "modified_by_client_uuid": null,
+  "modified_by_user_uuid": "ardev-tpzed-n3kzq4fvoks3uw4",
+  "modified_at": "2021-11-30T08:45:04.374489000Z",
+  "tail_uuid": "ardev-tpzed-n3kzq4fvoks3uw4",
+  "link_class": "permission",
+  "name": "can_read",
+  "head_uuid": "arkau-j7d0g-fcedae2076pw56h",
+  "head_kind": "arvados#group",
+  "tail_kind": "arvados#user",
+  "properties": {}
+}
\ No newline at end of file
diff --git a/sdk/java-v2/src/test/resources/org/arvados/client/api/client/links-list.json b/sdk/java-v2/src/test/resources/org/arvados/client/api/client/links-list.json
new file mode 100644 (file)
index 0000000..e720ecf
--- /dev/null
@@ -0,0 +1,46 @@
+{
+  "kind": "arvados#linkList",
+  "etag": "",
+  "self_link": "",
+  "offset": 0,
+  "limit": 100,
+  "items": [
+    {
+      "href": "/links/arkau-o0j2j-x2b4rdadxs2fizn",
+      "kind": "arvados#link",
+      "etag": "dkhtr9tvp9zfy0d90xjn7w1t7",
+      "uuid": "arkau-o0j2j-x2b4rdadxs2fizn",
+      "owner_uuid": "arkau-j7d0g-publicfavorites",
+      "created_at": "2021-10-27T12:00:06.607794000Z",
+      "modified_by_client_uuid": null,
+      "modified_by_user_uuid": "arlog-tpzed-fyiau9qwo7ytntu",
+      "modified_at": "2021-10-27T12:00:06.609840000Z",
+      "tail_uuid": "arkau-j7d0g-publicfavorites",
+      "link_class": "star",
+      "name": "pRED Data Commons Service - Open access",
+      "head_uuid": "arkau-j7d0g-sfhw8b1uson0hwh",
+      "head_kind": "arvados#group",
+      "tail_kind": "arvados#group",
+      "properties": {}
+    },
+    {
+      "href": "/links/arkau-o0j2j-r5am4lz9gnu488k",
+      "kind": "arvados#link",
+      "etag": "9nt0c2xn5oz1jzjzawlycmehz",
+      "uuid": "arkau-o0j2j-r5am4lz9gnu488k",
+      "owner_uuid": "arkau-j7d0g-publicfavorites",
+      "created_at": "2021-06-23T14:58:06.189520000Z",
+      "modified_by_client_uuid": null,
+      "modified_by_user_uuid": "arlog-tpzed-xzjyeljl6co7vlz",
+      "modified_at": "2021-06-23T14:58:06.196208000Z",
+      "tail_uuid": "arkau-j7d0g-publicfavorites",
+      "link_class": "star",
+      "name": "Open Targets Genetics",
+      "head_uuid": "arkau-j7d0g-pj5wysmpy5wn8yo",
+      "head_kind": "arvados#group",
+      "tail_kind": "arvados#group",
+      "properties": {}
+    }
+  ],
+  "items_available": 2
+}
\ No newline at end of file
index f6f85ba69619ba930cca9efd20d3b4f134f28527..be7cd629c98cfbac0ff36be2ce10c4de2c30cf2e 100644 (file)
@@ -576,6 +576,9 @@ class ArvPutUploadJob(object):
                     files.sort()
                     for f in files:
                         filepath = os.path.join(root, f)
+                        if not os.path.isfile(filepath):
+                            self.logger.warning("Skipping non-regular file '{}'".format(filepath))
+                            continue
                         # Add its size to the total bytes count (if applicable)
                         if self.follow_links or (not os.path.islink(filepath)):
                             if self.bytes_expected is not None:
index 2a71f3671a4f956cc7006acf97ee57a7e89bb47c..0e531dee314529534aa4b5ae14815756105f0e66 100644 (file)
@@ -14,10 +14,10 @@ from functools import partial
 import apiclient
 import ciso8601
 import datetime
-import hashlib
 import json
 import logging
 import mock
+import multiprocessing
 import os
 import pwd
 import random
@@ -31,7 +31,6 @@ import tempfile
 import time
 import unittest
 import uuid
-import yaml
 
 import arvados
 import arvados.commands.put as arv_put
@@ -294,6 +293,26 @@ class ArvPutUploadJobTest(run_test_server.TestCaseWithServers,
         shutil.rmtree(self.small_files_dir)
         shutil.rmtree(self.tempdir_with_symlink)
 
+    def test_non_regular_files_are_ignored_except_symlinks_to_dirs(self):
+        def pfunc(x):
+            with open(x, 'w') as f:
+                f.write('test')
+        fifo_filename = 'fifo-file'
+        fifo_path = os.path.join(self.tempdir_with_symlink, fifo_filename)
+        self.assertTrue(os.path.islink(os.path.join(self.tempdir_with_symlink, 'linkeddir')))
+        os.mkfifo(fifo_path)
+        producer = multiprocessing.Process(target=pfunc, args=(fifo_path,))
+        producer.start()
+        cwriter = arv_put.ArvPutUploadJob([self.tempdir_with_symlink])
+        cwriter.start(save_collection=False)
+        if producer.exitcode is None:
+            # If the producer is still running, kill it. This should always be
+            # before any assertion that may fail.
+            producer.terminate()
+            producer.join(1)
+        self.assertIn('linkeddir', cwriter.manifest_text())
+        self.assertNotIn(fifo_filename, cwriter.manifest_text())
+
     def test_symlinks_are_followed_by_default(self):
         self.assertTrue(os.path.islink(os.path.join(self.tempdir_with_symlink, 'linkeddir')))
         self.assertTrue(os.path.islink(os.path.join(self.tempdir_with_symlink, 'linkedfile')))
index c1d4b74d6dfab1d76b84cf680aaf50ad2487da30..59ac639baf929dd2c06c9352159f33288be1d792 100644 (file)
@@ -427,6 +427,27 @@ class Arvados::V1::SchemaController < ApplicationController
         }
       }
 
+      discovery[:resources]['sys'] = {
+        methods: {
+          get: {
+            id: "arvados.sys.trash_sweep",
+            path: "sys/trash_sweep",
+            httpMethod: "POST",
+            description: "apply scheduled trash and delete operations",
+            parameters: {
+            },
+            parameterOrder: [
+            ],
+            response: {
+            },
+            scopes: [
+              "https://api.arvados.org/auth/arvados",
+              "https://api.arvados.org/auth/arvados.readonly"
+            ]
+          },
+        }
+      }
+
       Rails.configuration.API.DisabledAPIs.each do |method, _|
         ctrl, action = method.to_s.split('.', 2)
         discovery[:resources][ctrl][:methods].delete(action.to_sym)
similarity index 55%
rename from services/api/lib/sweep_trashed_objects.rb
rename to services/api/app/controllers/sys_controller.rb
index c09896567f3ac1291d8cbe0632393ac60d2ac8fc..a67b124bd09ffc92834d3bd66a508ef96ffa6dc0 100644 (file)
@@ -2,33 +2,12 @@
 #
 # SPDX-License-Identifier: AGPL-3.0
 
-require 'current_api_client'
+class SysController < ApplicationController
+  skip_before_action :find_object_by_uuid
+  skip_before_action :render_404_if_no_object
+  before_action :admin_required
 
-module SweepTrashedObjects
-  extend CurrentApiClient
-
-  def self.delete_project_and_contents(p_uuid)
-    p = Group.find_by_uuid(p_uuid)
-    if !p || p.group_class != 'project'
-      raise "can't sweep group '#{p_uuid}', it may not exist or not be a project"
-    end
-    # First delete sub projects
-    Group.where({group_class: 'project', owner_uuid: p_uuid}).each do |sub_project|
-      delete_project_and_contents(sub_project.uuid)
-    end
-    # Next, iterate over all tables which have owner_uuid fields, with some
-    # exceptions, and delete records owned by this project
-    skipped_classes = ['Group', 'User']
-    ActiveRecord::Base.descendants.reject(&:abstract_class?).each do |klass|
-      if !skipped_classes.include?(klass.name) && klass.columns.collect(&:name).include?('owner_uuid')
-        klass.where({owner_uuid: p_uuid}).destroy_all
-      end
-    end
-    # Finally delete the project itself
-    p.destroy
-  end
-
-  def self.sweep_now
+  def trash_sweep
     act_as_system_user do
       # Sweep trashed collections
       Collection.
@@ -38,45 +17,43 @@ module SweepTrashedObjects
         where('is_trashed = false and trash_at < statement_timestamp()').
         update_all('is_trashed = true')
 
-      # Sweep trashed projects and their contents
+      # Sweep trashed projects and their contents (as well as role
+      # groups that were trashed before #18340 when that was
+      # disallowed)
       Group.
-        where({group_class: 'project'}).
         where('delete_at is not null and delete_at < statement_timestamp()').each do |project|
           delete_project_and_contents(project.uuid)
       end
       Group.
-        where({group_class: 'project'}).
         where('is_trashed = false and trash_at < statement_timestamp()').
         update_all('is_trashed = true')
 
       # Sweep expired tokens
       ActiveRecord::Base.connection.execute("DELETE from api_client_authorizations where expires_at <= statement_timestamp()")
     end
+    head :no_content
   end
 
-  def self.sweep_if_stale
-    return if Rails.configuration.Collections.TrashSweepInterval <= 0
-    exp = Rails.configuration.Collections.TrashSweepInterval.seconds
-    need = false
-    Rails.cache.fetch('SweepTrashedObjects', expires_in: exp) do
-      need = true
+  protected
+
+  def delete_project_and_contents(p_uuid)
+    p = Group.find_by_uuid(p_uuid)
+    if !p
+      raise "can't sweep group '#{p_uuid}', it may not exist"
+    end
+    # First delete sub projects
+    Group.where({group_class: 'project', owner_uuid: p_uuid}).each do |sub_project|
+      delete_project_and_contents(sub_project.uuid)
     end
-    if need
-      Thread.new do
-        Thread.current.abort_on_exception = false
-        begin
-          sweep_now
-        rescue => e
-          Rails.logger.error "#{e.class}: #{e}\n#{e.backtrace.join("\n\t")}"
-        ensure
-          # Rails 5.1+ makes test threads share a database connection, so we can't
-          # close a connection shared with other threads.
-          # https://github.com/rails/rails/commit/deba47799ff905f778e0c98a015789a1327d5087
-          if Rails.env != "test"
-            ActiveRecord::Base.connection.close
-          end
-        end
+    # Next, iterate over all tables which have owner_uuid fields, with some
+    # exceptions, and delete records owned by this project
+    skipped_classes = ['Group', 'User']
+    ActiveRecord::Base.descendants.reject(&:abstract_class?).each do |klass|
+      if !skipped_classes.include?(klass.name) && klass.columns.collect(&:name).include?('owner_uuid')
+        klass.where({owner_uuid: p_uuid}).destroy_all
       end
     end
+    # Finally delete the project itself
+    p.destroy
   end
 end
index a98cde4446d17e63e1e5e34db0bbc777f27f1903..b4660dbd355de72261d4584977b88533f77f829e 100644 (file)
@@ -3,7 +3,6 @@
 # SPDX-License-Identifier: AGPL-3.0
 
 require 'arvados/keep'
-require 'sweep_trashed_objects'
 require 'trashable'
 
 class Collection < ArvadosModel
@@ -616,11 +615,6 @@ class Collection < ArvadosModel
     super - ["manifest_text", "storage_classes_desired", "storage_classes_confirmed", "current_version_uuid"]
   end
 
-  def self.where *args
-    SweepTrashedObjects.sweep_if_stale
-    super
-  end
-
   protected
 
   # Although the defaults for these columns is already set up on the schema,
index 738426b1d8b06e007f2c62dbf0d91ba6311c8672..98f5788d6505d3525115f3b3a8e5622b8a059937 100644 (file)
@@ -92,6 +92,8 @@ Rails.application.routes.draw do
     end
   end
 
+  post '/sys/trash_sweep', to: 'sys#trash_sweep'
+
   if Rails.env == 'test'
     post '/database/reset', to: 'database#reset'
   end
index 23e60c8ed94733db647e3aafd911bbd272407646..b7e5476404869f6a89603302eafe828397acd1c5 100644 (file)
@@ -118,6 +118,10 @@ as select * from compute_permission_subgraph($1, $2, $3, $4)
 
     ActiveRecord::Base.connection.exec_query "SET LOCAL enable_mergejoin to true;"
 
+    # Now that we have recomputed a set of permissions, delete any
+    # rows from the materialized_permissions table where (target_uuid,
+    # user_uuid) is not present or has perm_level=0 in the recomputed
+    # set.
     ActiveRecord::Base.connection.exec_delete %{
 delete from #{PERMISSION_VIEW} where
   target_uuid in (select target_uuid from #{temptable_perms}) and
@@ -128,10 +132,18 @@ delete from #{PERMISSION_VIEW} where
 },
                                               "update_permissions.delete"
 
+    # Now insert-or-update permissions in the recomputed set.  The
+    # WHERE clause is important to avoid redundantly updating rows
+    # that haven't actually changed.
     ActiveRecord::Base.connection.exec_query %{
 insert into #{PERMISSION_VIEW} (user_uuid, target_uuid, perm_level, traverse_owned)
   select user_uuid, target_uuid, val as perm_level, traverse_owned from #{temptable_perms} where val>0
-on conflict (user_uuid, target_uuid) do update set perm_level=EXCLUDED.perm_level, traverse_owned=EXCLUDED.traverse_owned;
+on conflict (user_uuid, target_uuid) do update
+set perm_level=EXCLUDED.perm_level, traverse_owned=EXCLUDED.traverse_owned
+where #{PERMISSION_VIEW}.user_uuid=EXCLUDED.user_uuid and
+      #{PERMISSION_VIEW}.target_uuid=EXCLUDED.target_uuid and
+       (#{PERMISSION_VIEW}.perm_level != EXCLUDED.perm_level or
+        #{PERMISSION_VIEW}.traverse_owned != EXCLUDED.traverse_owned);
 },
                                              "update_permissions.insert"
 
diff --git a/services/api/test/functional/sys_controller_test.rb b/services/api/test/functional/sys_controller_test.rb
new file mode 100644 (file)
index 0000000..e13d702
--- /dev/null
@@ -0,0 +1,135 @@
+# Copyright (C) The Arvados Authors. All rights reserved.
+#
+# SPDX-License-Identifier: AGPL-3.0
+
+require 'test_helper'
+
+class SysControllerTest < ActionController::TestCase
+  include CurrentApiClient
+  include DbCurrentTime
+
+  test "trash_sweep - delete expired tokens" do
+    assert_not_empty ApiClientAuthorization.where(uuid: api_client_authorizations(:expired).uuid)
+    authorize_with :admin
+    post :trash_sweep
+    assert_response :success
+    assert_empty ApiClientAuthorization.where(uuid: api_client_authorizations(:expired).uuid)
+  end
+
+  test "trash_sweep - fail with non-admin token" do
+    authorize_with :active
+    post :trash_sweep
+    assert_response 403
+  end
+
+  test "trash_sweep - move collections to trash" do
+    c = collections(:trashed_on_next_sweep)
+    refute_empty Collection.where('uuid=? and is_trashed=false', c.uuid)
+    assert_raises(ActiveRecord::RecordNotUnique) do
+      act_as_user users(:active) do
+        Collection.create!(owner_uuid: c.owner_uuid,
+                           name: c.name)
+      end
+    end
+    authorize_with :admin
+    post :trash_sweep
+    assert_response :success
+    c = Collection.where('uuid=? and is_trashed=true', c.uuid).first
+    assert c
+    act_as_user users(:active) do
+      assert Collection.create!(owner_uuid: c.owner_uuid,
+                                name: c.name)
+    end
+  end
+
+  test "trash_sweep - delete collections" do
+    uuid = 'zzzzz-4zz18-3u1p5umicfpqszp' # deleted_on_next_sweep
+    assert_not_empty Collection.where(uuid: uuid)
+    authorize_with :admin
+    post :trash_sweep
+    assert_response :success
+    assert_empty Collection.where(uuid: uuid)
+  end
+
+  test "trash_sweep - delete referring links" do
+    uuid = collections(:trashed_on_next_sweep).uuid
+    act_as_system_user do
+      assert_raises ActiveRecord::RecordInvalid do
+        # Cannot create because :trashed_on_next_sweep is already trashed
+        Link.create!(head_uuid: uuid,
+                     tail_uuid: system_user_uuid,
+                     link_class: 'whatever',
+                     name: 'something')
+      end
+
+      # Bump trash_at to now + 1 minute
+      Collection.where(uuid: uuid).
+        update(trash_at: db_current_time + (1).minute)
+
+      # Not considered trashed now
+      Link.create!(head_uuid: uuid,
+                   tail_uuid: system_user_uuid,
+                   link_class: 'whatever',
+                   name: 'something')
+    end
+    past = db_current_time
+    Collection.where(uuid: uuid).
+      update_all(is_trashed: true, trash_at: past, delete_at: past)
+    assert_not_empty Collection.where(uuid: uuid)
+    authorize_with :admin
+    post :trash_sweep
+    assert_response :success
+    assert_empty Collection.where(uuid: uuid)
+  end
+
+  test "trash_sweep - move projects to trash" do
+    p = groups(:trashed_on_next_sweep)
+    assert_empty Group.where('uuid=? and is_trashed=true', p.uuid)
+    authorize_with :admin
+    post :trash_sweep
+    assert_response :success
+    assert_not_empty Group.where('uuid=? and is_trashed=true', p.uuid)
+  end
+
+  test "trash_sweep - delete projects and their contents" do
+    g_foo = groups(:trashed_project)
+    g_bar = groups(:trashed_subproject)
+    g_baz = groups(:trashed_subproject3)
+    col = collections(:collection_in_trashed_subproject)
+    job = jobs(:job_in_trashed_project)
+    cr = container_requests(:cr_in_trashed_project)
+    # Save how many objects were before the sweep
+    user_nr_was = User.all.length
+    coll_nr_was = Collection.all.length
+    group_nr_was = Group.where('group_class<>?', 'project').length
+    project_nr_was = Group.where(group_class: 'project').length
+    cr_nr_was = ContainerRequest.all.length
+    job_nr_was = Job.all.length
+    assert_not_empty Group.where(uuid: g_foo.uuid)
+    assert_not_empty Group.where(uuid: g_bar.uuid)
+    assert_not_empty Group.where(uuid: g_baz.uuid)
+    assert_not_empty Collection.where(uuid: col.uuid)
+    assert_not_empty Job.where(uuid: job.uuid)
+    assert_not_empty ContainerRequest.where(uuid: cr.uuid)
+
+    authorize_with :admin
+    post :trash_sweep
+    assert_response :success
+
+    assert_empty Group.where(uuid: g_foo.uuid)
+    assert_empty Group.where(uuid: g_bar.uuid)
+    assert_empty Group.where(uuid: g_baz.uuid)
+    assert_empty Collection.where(uuid: col.uuid)
+    assert_empty Job.where(uuid: job.uuid)
+    assert_empty ContainerRequest.where(uuid: cr.uuid)
+    # No unwanted deletions should have happened
+    assert_equal user_nr_was, User.all.length
+    assert_equal coll_nr_was-2,        # collection_in_trashed_subproject
+                 Collection.all.length # & deleted_on_next_sweep collections
+    assert_equal group_nr_was, Group.where('group_class<>?', 'project').length
+    assert_equal project_nr_was-3, Group.where(group_class: 'project').length
+    assert_equal cr_nr_was-1, ContainerRequest.all.length
+    assert_equal job_nr_was-1, Job.all.length
+  end
+
+end
index e3224f49127e83bf9b76f8887b83b65bf1733bc0..a2a1545cee93d7ffcdd5a63073881abc960caa90 100644 (file)
@@ -24,7 +24,7 @@ class ErrorsTest < ActionDispatch::IntegrationTest
       # Generally, new routes should appear under /arvados/v1/. If
       # they appear elsewhere, that might have been caused by default
       # rails generator behavior that we don't want.
-      assert_match(/^\/(|\*a|arvados\/v1\/.*|auth\/.*|login|logout|database\/reset|discovery\/.*|static\/.*|themes\/.*|assets|_health\/.*)(\(\.:format\))?$/,
+      assert_match(/^\/(|\*a|arvados\/v1\/.*|auth\/.*|login|logout|database\/reset|discovery\/.*|static\/.*|sys\/trash_sweep|themes\/.*|assets|_health\/.*)(\(\.:format\))?$/,
                    route.path.spec.to_s,
                    "Unexpected new route: #{route.path.spec}")
     end
index fb90418b8480be6507532a1e9f4baefd00922463..e043f8914a4f3aafccea19b51a8b692b7915b792 100644 (file)
@@ -3,7 +3,6 @@
 # SPDX-License-Identifier: AGPL-3.0
 
 require 'test_helper'
-require 'sweep_trashed_objects'
 
 class ApiClientAuthorizationTest < ActiveSupport::TestCase
   include CurrentApiClient
@@ -20,12 +19,6 @@ class ApiClientAuthorizationTest < ActiveSupport::TestCase
     end
   end
 
-  test "delete expired in SweepTrashedObjects" do
-    assert_not_empty ApiClientAuthorization.where(uuid: api_client_authorizations(:expired).uuid)
-    SweepTrashedObjects.sweep_now
-    assert_empty ApiClientAuthorization.where(uuid: api_client_authorizations(:expired).uuid)
-  end
-
   test "accepts SystemRootToken" do
     assert_nil ApiClientAuthorization.validate(token: "xxxSystemRootTokenxxx")
 
index de0f1d360cb8509a5aea5bea31bbd763eba46609..e7134a5be581f7b8efd69f1be04919631e7d98ed 100644 (file)
@@ -3,7 +3,6 @@
 # SPDX-License-Identifier: AGPL-3.0
 
 require 'test_helper'
-require 'sweep_trashed_objects'
 require 'fix_collection_versions_timestamps'
 
 class CollectionTest < ActiveSupport::TestCase
@@ -1058,60 +1057,6 @@ class CollectionTest < ActiveSupport::TestCase
     assert_includes(coll_uuids, collections(:docker_image).uuid)
   end
 
-  test "move collections to trash in SweepTrashedObjects" do
-    c = collections(:trashed_on_next_sweep)
-    refute_empty Collection.where('uuid=? and is_trashed=false', c.uuid)
-    assert_raises(ActiveRecord::RecordNotUnique) do
-      act_as_user users(:active) do
-        Collection.create!(owner_uuid: c.owner_uuid,
-                           name: c.name)
-      end
-    end
-    SweepTrashedObjects.sweep_now
-    c = Collection.where('uuid=? and is_trashed=true', c.uuid).first
-    assert c
-    act_as_user users(:active) do
-      assert Collection.create!(owner_uuid: c.owner_uuid,
-                                name: c.name)
-    end
-  end
-
-  test "delete collections in SweepTrashedObjects" do
-    uuid = 'zzzzz-4zz18-3u1p5umicfpqszp' # deleted_on_next_sweep
-    assert_not_empty Collection.where(uuid: uuid)
-    SweepTrashedObjects.sweep_now
-    assert_empty Collection.where(uuid: uuid)
-  end
-
-  test "delete referring links in SweepTrashedObjects" do
-    uuid = collections(:trashed_on_next_sweep).uuid
-    act_as_system_user do
-      assert_raises ActiveRecord::RecordInvalid do
-        # Cannot create because :trashed_on_next_sweep is already trashed
-        Link.create!(head_uuid: uuid,
-                     tail_uuid: system_user_uuid,
-                     link_class: 'whatever',
-                     name: 'something')
-      end
-
-      # Bump trash_at to now + 1 minute
-      Collection.where(uuid: uuid).
-        update(trash_at: db_current_time + (1).minute)
-
-      # Not considered trashed now
-      Link.create!(head_uuid: uuid,
-                   tail_uuid: system_user_uuid,
-                   link_class: 'whatever',
-                   name: 'something')
-    end
-    past = db_current_time
-    Collection.where(uuid: uuid).
-      update_all(is_trashed: true, trash_at: past, delete_at: past)
-    assert_not_empty Collection.where(uuid: uuid)
-    SweepTrashedObjects.sweep_now
-    assert_empty Collection.where(uuid: uuid)
-  end
-
   test "empty names are exempt from name uniqueness" do
     act_as_user users(:active) do
       c1 = Collection.new(name: nil, manifest_text: '', owner_uuid: groups(:aproject).uuid)
index 017916f48bee5fafd278800a143236eb7c6b609a..10932e116d7adbed60880f4fc84d0a55242039db 100644 (file)
@@ -228,50 +228,6 @@ class GroupTest < ActiveSupport::TestCase
     assert User.readable_by(users(:admin)).where(uuid:  u_bar.uuid).any?
   end
 
-  test "move projects to trash in SweepTrashedObjects" do
-    p = groups(:trashed_on_next_sweep)
-    assert_empty Group.where('uuid=? and is_trashed=true', p.uuid)
-    SweepTrashedObjects.sweep_now
-    assert_not_empty Group.where('uuid=? and is_trashed=true', p.uuid)
-  end
-
-  test "delete projects and their contents in SweepTrashedObjects" do
-    g_foo = groups(:trashed_project)
-    g_bar = groups(:trashed_subproject)
-    g_baz = groups(:trashed_subproject3)
-    col = collections(:collection_in_trashed_subproject)
-    job = jobs(:job_in_trashed_project)
-    cr = container_requests(:cr_in_trashed_project)
-    # Save how many objects were before the sweep
-    user_nr_was = User.all.length
-    coll_nr_was = Collection.all.length
-    group_nr_was = Group.where('group_class<>?', 'project').length
-    project_nr_was = Group.where(group_class: 'project').length
-    cr_nr_was = ContainerRequest.all.length
-    job_nr_was = Job.all.length
-    assert_not_empty Group.where(uuid: g_foo.uuid)
-    assert_not_empty Group.where(uuid: g_bar.uuid)
-    assert_not_empty Group.where(uuid: g_baz.uuid)
-    assert_not_empty Collection.where(uuid: col.uuid)
-    assert_not_empty Job.where(uuid: job.uuid)
-    assert_not_empty ContainerRequest.where(uuid: cr.uuid)
-    SweepTrashedObjects.sweep_now
-    assert_empty Group.where(uuid: g_foo.uuid)
-    assert_empty Group.where(uuid: g_bar.uuid)
-    assert_empty Group.where(uuid: g_baz.uuid)
-    assert_empty Collection.where(uuid: col.uuid)
-    assert_empty Job.where(uuid: job.uuid)
-    assert_empty ContainerRequest.where(uuid: cr.uuid)
-    # No unwanted deletions should have happened
-    assert_equal user_nr_was, User.all.length
-    assert_equal coll_nr_was-2,        # collection_in_trashed_subproject
-                 Collection.all.length # & deleted_on_next_sweep collections
-    assert_equal group_nr_was, Group.where('group_class<>?', 'project').length
-    assert_equal project_nr_was-3, Group.where(group_class: 'project').length
-    assert_equal cr_nr_was-1, ContainerRequest.all.length
-    assert_equal job_nr_was-1, Job.all.length
-  end
-
   test "project names must be displayable in a filesystem" do
     set_user_from_auth :active
     ["", "{SOLIDUS}"].each do |subst|