Merge branch 'master' into 15803-unsetup
authorPeter Amstutz <pamstutz@veritasgenetics.com>
Mon, 18 Nov 2019 21:56:40 +0000 (16:56 -0500)
committerPeter Amstutz <pamstutz@veritasgenetics.com>
Mon, 18 Nov 2019 21:56:40 +0000 (16:56 -0500)
Arvados-DCO-1.1-Signed-off-by: Peter Amstutz <pamstutz@veritasgenetics.com>

55 files changed:
apps/workbench/app/views/layouts/application.html.erb
build/run-build-packages.sh
build/run-library.sh
build/run-tests.sh
doc/_config.yml
doc/_includes/_wb2_vocabulary_example.liquid [new file with mode: 0644]
doc/admin/workbench2-vocabulary.html.textile.liquid [new file with mode: 0644]
doc/install/install-workbench2-app.html.textile.liquid
go.mod [new file with mode: 0644]
go.sum [new file with mode: 0644]
lib/config/config.default.yml
lib/config/deprecated.go
lib/config/deprecated_test.go
lib/config/export.go
lib/config/generated_config.go
lib/controller/federation/conn.go
lib/controller/federation/list_test.go
lib/controller/federation/login_test.go [new file with mode: 0644]
lib/controller/handler.go
lib/controller/handler_test.go
lib/controller/localdb/conn.go [new file with mode: 0644]
lib/controller/localdb/login.go [new file with mode: 0644]
lib/controller/localdb/login_test.go [new file with mode: 0644]
lib/controller/railsproxy/railsproxy.go
lib/controller/router/response.go
lib/controller/router/router.go
lib/controller/rpc/conn.go
lib/controller/rpc/conn_test.go
sdk/go/arvados/api.go
sdk/go/arvados/client.go
sdk/go/arvados/config.go
sdk/go/arvados/login.go [new file with mode: 0644]
sdk/go/arvadostest/api.go
sdk/go/arvadostest/proxy.go [new file with mode: 0644]
sdk/java-v2/src/main/java/org/arvados/client/api/client/BaseApiClient.java
sdk/java-v2/src/main/java/org/arvados/client/api/client/factory/OkHttpClientFactory.java
sdk/java-v2/src/main/java/org/arvados/client/facade/ArvadosFacade.java
sdk/java-v2/src/test/java/org/arvados/client/api/client/factory/OkHttpClientFactoryTest.java
services/api/app/controllers/database_controller.rb
services/api/app/controllers/user_sessions_controller.rb
services/api/app/models/api_client.rb
services/api/app/models/user.rb
services/api/config/application.rb
services/api/config/arvados_config.rb
services/api/test/functional/user_sessions_controller_test.rb
services/api/test/unit/api_client_test.rb
services/dockercleaner/arvados-docker-cleaner.service
services/keep-balance/balance.go
services/keep-balance/balance_test.go
services/keep-web/handler_test.go
services/keep-web/server_test.go
services/keepstore/mounts_test.go
tools/arvbox/lib/arvbox/docker/go-setup.sh
vendor/.gitignore [deleted file]
vendor/vendor.json [deleted file]

index bd3afbb681f098a1f3fe726e7d9bc49f671240ac..4fc7da9949cc7ebdaca5bf3cf24d54456ae8d5b6 100644 (file)
@@ -20,7 +20,7 @@ SPDX-License-Identifier: AGPL-3.0 %>
   <meta name="author" content="">
   <% if current_user %>
     <% content_for :js do %>
-      window.defaultSession = <%=raw({baseURL: Rails.configuration.Services.Controller.ExternalURL.to_s, token: Thread.current[:arvados_api_token], user: current_user}.to_json)%>
+      window.defaultSession = <%=raw({baseURL: Rails.configuration.Services.Controller.ExternalURL.to_s.gsub(/\/?$/,'/'), token: Thread.current[:arvados_api_token], user: current_user}.to_json)%>
     <% end %>
   <% end %>
   <% if current_user and $arvados_api_client.discovery[:websocketUrl] %>
index fd313cf10ef8801d822245a7dcbd2876b5cf80e8..a07b308179990a7ac96b1185ab1f42e7d2e45350 100755 (executable)
@@ -281,7 +281,6 @@ debug_echo -e "\nPython packages\n"
 # Go binaries
 cd $WORKSPACE/packages/$TARGET
 export GOPATH=$(mktemp -d)
-go get github.com/kardianos/govendor
 package_go_binary cmd/arvados-client arvados-client \
     "Arvados command line tool (beta)"
 package_go_binary cmd/arvados-server arvados-server \
index 95f2ff1452c4a4477d5c62be391148802899383f..a4cebbc8a71d07de545a8382dd001a736802a548 100755 (executable)
@@ -110,11 +110,8 @@ calculate_go_package_version() {
   local -n __returnvar="$1"; shift
   local src_path="$1"; shift
 
-  mkdir -p "$GOPATH/src/git.curoverse.com"
-  ln -sfn "$WORKSPACE" "$GOPATH/src/git.curoverse.com/arvados.git"
-  (cd "$GOPATH/src/git.curoverse.com/arvados.git" && "$GOPATH/bin/govendor" sync -v)
-
-  cd "$GOPATH/src/git.curoverse.com/arvados.git/$src_path"
+  cd "$WORKSPACE/$src_path"
+  go mod download
   local version="$(version_from_git)"
   local timestamp="$(timestamp_from_git)"
 
@@ -126,7 +123,7 @@ calculate_go_package_version() {
       checkdirs+=(sdk/go lib)
   fi
   for dir in ${checkdirs[@]}; do
-      cd "$GOPATH/src/git.curoverse.com/arvados.git/$dir"
+      cd "$WORKSPACE/$dir"
       ts="$(timestamp_from_git)"
       if [[ "$ts" -gt "$timestamp" ]]; then
           version=$(version_from_git)
index 0014547ce5448ed2abdc727b3eb8bccf0f75820a..38005070c70454dc3b07045f2d0d9d7feaa35458 100755 (executable)
@@ -631,26 +631,8 @@ initialize() {
 }
 
 install_env() {
-    (
-        set -e
-        mkdir -p "$GOPATH/src/git.curoverse.com"
-        if [[ ! -h "$GOPATH/src/git.curoverse.com/arvados.git" ]]; then
-            for d in \
-                "$GOPATH/src/git.curoverse.com/arvados.git/tmp/GOPATH" \
-                    "$GOPATH/src/git.curoverse.com/arvados.git/tmp" \
-                    "$GOPATH/src/git.curoverse.com/arvados.git/arvados" \
-                    "$GOPATH/src/git.curoverse.com/arvados.git"; do
-                [[ -h "$d" ]] && rm "$d"
-                [[ -d "$d" ]] && rmdir "$d"
-            done
-        fi
-        ln -vsfT "$WORKSPACE" "$GOPATH/src/git.curoverse.com/arvados.git"
-        go get -v github.com/kardianos/govendor
-        cd "$GOPATH/src/git.curoverse.com/arvados.git"
-        go get -v -d ...
-        "$GOPATH/bin/govendor" sync
-        which goimports >/dev/null || go get golang.org/x/tools/cmd/goimports
-    ) || fatal "Go setup failed"
+    go mod download || fatal "Go deps failed"
+    which goimports >/dev/null || go get golang.org/x/tools/cmd/goimports || fatal "Go setup failed"
 
     setup_virtualenv "$VENVDIR" --python python2.7
     . "$VENVDIR/bin/activate"
@@ -735,7 +717,7 @@ do_test() {
             stop_services
             check_arvados_config "$1"
             ;;
-        gofmt | govendor | doc | lib/cli | lib/cloud/azure | lib/cloud/ec2 | lib/cloud/cloudtest | lib/cmd | lib/dispatchcloud/ssh_executor | lib/dispatchcloud/worker)
+        gofmt | doc | lib/cli | lib/cloud/azure | lib/cloud/ec2 | lib/cloud/cloudtest | lib/cmd | lib/dispatchcloud/ssh_executor | lib/dispatchcloud/worker)
             check_arvados_config "$1"
             # don't care whether services are running
             ;;
@@ -771,12 +753,12 @@ do_test_once() {
     then
         covername="coverage-$(echo "$1" | sed -e 's/\//_/g')"
         coverflags=("-covermode=count" "-coverprofile=$WORKSPACE/tmp/.$covername.tmp")
-        # We do "go get -t" here to catch compilation errors
+        # We do "go install" here to catch compilation errors
         # before trying "go test". Otherwise, coverage-reporting
         # mode makes Go show the wrong line numbers when reporting
         # compilation errors.
-        go get -ldflags "$(go_ldflags)" -t "git.curoverse.com/arvados.git/$1" && \
-            cd "$GOPATH/src/git.curoverse.com/arvados.git/$1" && \
+        go install -ldflags "$(go_ldflags)" "$WORKSPACE/$1" && \
+            cd "$WORKSPACE/$1" && \
             if [[ -n "${testargs[$1]}" ]]
         then
             # "go test -check.vv giturl" doesn't work, but this
@@ -863,7 +845,7 @@ do_install_once() {
         result=1
     elif [[ "$2" == "go" ]]
     then
-        go get -ldflags "$(go_ldflags)" -t "git.curoverse.com/arvados.git/$1"
+        go install -ldflags "$(go_ldflags)" "$WORKSPACE/$1"
     elif [[ "$2" == "pip" ]]
     then
         # $3 can name a path directory for us to use, including trailing
@@ -1037,27 +1019,6 @@ test_gofmt() {
     [[ -z "$(gofmt -e -d $dirs | tee -a /dev/stderr)" ]]
 }
 
-test_govendor() {
-    (
-        set -e
-        cd "$GOPATH/src/git.curoverse.com/arvados.git"
-        # Remove cached source dirs in workdir. Otherwise, they will
-        # not qualify as +missing or +external below, and we won't be
-        # able to detect that they're missing from vendor/vendor.json.
-        rm -rf vendor/*/
-        go get -v -d ...
-        "$GOPATH/bin/govendor" sync
-        if [[ -n $("$GOPATH/bin/govendor" list +unused +missing +external | tee /dev/stderr) ]]; then
-            echo >&2 "vendor/vendor.json has unused or missing dependencies -- try:
-
-(export GOPATH=\"${GOPATH}\"; cd \$GOPATH/src/git.curoverse.com/arvados.git && \$GOPATH/bin/govendor add +missing +external && \$GOPATH/bin/govendor remove +unused)
-
-"
-            return 1
-        fi
-    )
-}
-
 test_services/api() {
     rm -f "$WORKSPACE/services/api/git-commit.version"
     cd "$WORKSPACE/services/api" \
@@ -1183,7 +1144,6 @@ test_all() {
     fi
 
     do_test gofmt
-    do_test govendor
     do_test doc
     do_test sdk/ruby
     do_test sdk/R
index da7635c1f4c96813f0aa0473252d1e91ca57da57..404d2f6c63a90606ea9b8099cd329da2e764ecd1 100644 (file)
@@ -178,6 +178,7 @@ navbar:
       - admin/troubleshooting.html.textile.liquid
       - install/migrate-docker19.html.textile.liquid
       - admin/upgrade-crunch2.html.textile.liquid
+      - admin/workbench2-vocabulary.html.textile.liquid
   installguide:
     - Overview:
       - install/index.html.textile.liquid
diff --git a/doc/_includes/_wb2_vocabulary_example.liquid b/doc/_includes/_wb2_vocabulary_example.liquid
new file mode 100644 (file)
index 0000000..ee2ac97
--- /dev/null
@@ -0,0 +1,27 @@
+{
+    "strict_tags": false,
+    "tags": {
+        "IDTAGANIMALS": {
+            "strict": false,
+            "labels": [{"label": "Animal" }, {"label": "Creature"}, {"label": "Species"}],
+            "values": {
+                "IDVALANIMALS1": { "labels": [{"label": "Human"}, {"label": "Homo sapiens"}] },
+                "IDVALANIMALS2": { "labels": [{"label": "Dog"}, {"label": "Canis lupus familiaris"}] },
+                "IDVALANIMALS3": { "labels": [{"label": "Elephant"}, {"label": "Loxodonta"}] },
+                "IDVALANIMALS4": { "labels": [{"label": "Eagle"}, {"label": "Haliaeetus leucocephalus"}] }
+            }
+        },
+        "IDTAGCOMMENT": {
+            "labels": [{"label": "Comment"}, {"label": "Suggestion"}]
+        },
+        "IDTAGIMPORTANCES": {
+            "strict": true,
+            "labels": [{"label": "Importance"}, {"label": "Priority"}],
+            "values": {
+                "IDVALIMPORTANCES1": { "labels": [{"label": "Critical"}, {"label": "Urgent"}, {"label": "High"}] },
+                "IDVALIMPORTANCES2": { "labels": [{"label": "Normal"}, {"label": "Moderate"}] },
+                "IDVALIMPORTANCES3": { "labels": [{"label": "Low"}] }
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/doc/admin/workbench2-vocabulary.html.textile.liquid b/doc/admin/workbench2-vocabulary.html.textile.liquid
new file mode 100644 (file)
index 0000000..82c384c
--- /dev/null
@@ -0,0 +1,51 @@
+---
+layout: default
+navsection: admin
+title: Workbench2 Vocabulary Format
+...
+
+{% comment %}
+Copyright (C) The Arvados Authors. All rights reserved.
+
+SPDX-License-Identifier: CC-BY-SA-3.0
+{% endcomment %}
+
+Many Arvados objects (like collections and projects) can store metadata as properties that in turn can be used in searches allowing a flexible way of organizing data inside the system.
+
+The Workbench2 user interface enables the site adminitrator to set up a properties vocabulary formal definition so that users can select from predefined key/value pairs of properties, offering the possibility to add different terms for the same concept.
+
+h2. Workbench2 configuration
+
+Workbench2 retrieves the vocabulary file URL from the cluster config as shown:
+
+<notextile>
+<pre><code>Cluster:
+  zzzzz:
+    Workbench:
+      VocabularyURL: <span class="userinput">https://site.example.com/vocabulary.json</span>
+</code></pre>
+</notextile>
+
+h2. Vocabulary definition format
+
+The JSON file describes the available keys and values and if the user is allowed to enter free text not defined by the vocabulary.
+
+Keys and values are indexed by identifiers so that the concept of a term is preserved even if vocabulary labels are changed.
+
+The following is an example of a vocabulary definition:
+
+{% codeblock as json %}
+{% include 'wb2_vocabulary_example' %}
+{% endcodeblock %}
+
+If the @strict_tags@ flag at the root level is @true@, it will restrict the users from saving property keys other than the ones defined in the vocabulary. Take notice that this restriction is at the client level on Workbench2, it doesn't limit the user's ability to set any arbitrary property via other means (e.g. Python SDK or CLI commands)
+
+Inside the @tags@ member, IDs are defined (@IDTAGANIMALS@, @IDTAGCOMMENT@, @IDTAGIMPORTANCES@) and can have any format that the current application requires. Every key will declare at least a @labels@ list with zero or more label objects.
+
+The @strict@ flag inside a tag definition operates the same as the @strict_tags@ root member, but at the individual tag level. When @strict@ is @true@, a tag’s value options are limited to those defined by the vocabulary.
+
+The @values@ member is optional and is used to define valid key/label pairs when applicable. In the example above, @IDTAGCOMMENT@ allows open-ended text by only defining the tag's ID and labels and leaving out @values@.
+
+When any key or value has more than one label option, Workbench2's user interface will allow the user to select any of the options. But because only the IDs are saved in the system, when the property is displayed in the user interface, the label shown will be the first of each group defined in the vocabulary file. For example, the user could select the property key @Species@ and @Homo sapiens@ as its value, but the user interface will display it as @Animal: Human@ because those labels are the first in the vocabulary definition.
+
+Internally, Workbench2 uses the IDs to do property based searches, so if the user searches by @Animal: Human@ or @Species: Homo sapiens@, both will return the same results.
\ No newline at end of file
index 6b94c8f2b39111d0cdaa15ed54aa7f9bdeb7e392..b5bdcd42cc06e77b8a946d47281d40253afc0728 100644 (file)
@@ -91,3 +91,7 @@ irb(main):003:0&gt; <span class="userinput">act_as_system_user do wb.update_attr
 =&gt; true
 </code></pre>
 </notextile>
+
+h2. Vocabulary configuration (optional)
+
+To configure the property vocabulary definition, please visit the "Workbench2 Vocabulary Format":{{site.baseurl}}/admin/workbench2-vocabulary.html page in the Admin section.
\ No newline at end of file
diff --git a/go.mod b/go.mod
new file mode 100644 (file)
index 0000000..efea57d
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,65 @@
+module git.curoverse.com/arvados.git
+
+go 1.13
+
+require (
+       github.com/AdRoll/goamz v0.0.0-20170825154802-2731d20f46f4
+       github.com/Azure/azure-sdk-for-go v19.1.0+incompatible
+       github.com/Azure/go-autorest v10.15.2+incompatible
+       github.com/Microsoft/go-winio v0.4.5 // indirect
+       github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7 // indirect
+       github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 // indirect
+       github.com/aws/aws-sdk-go v1.25.30
+       github.com/coreos/go-oidc v2.1.0+incompatible
+       github.com/coreos/go-systemd v0.0.0-20180108085132-cc4f39464dc7
+       github.com/dgrijalva/jwt-go v3.1.0+incompatible // indirect
+       github.com/dimchansky/utfbom v1.0.0 // indirect
+       github.com/dnaeon/go-vcr v1.0.1 // indirect
+       github.com/docker/distribution v2.6.0-rc.1.0.20180105232752-277ed486c948+incompatible // indirect
+       github.com/docker/docker v1.4.2-0.20180109013817-94b8a116fbf1
+       github.com/docker/go-connections v0.3.0 // indirect
+       github.com/docker/go-units v0.3.3-0.20171221200356-d59758554a3d // indirect
+       github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 // indirect
+       github.com/ghodss/yaml v1.0.0
+       github.com/gliderlabs/ssh v0.2.2 // indirect
+       github.com/gogo/protobuf v1.1.1
+       github.com/gorilla/context v1.1.1 // indirect
+       github.com/gorilla/mux v1.6.1-0.20180107155708-5bbbb5b2b572
+       github.com/hashicorp/golang-lru v0.5.1
+       github.com/imdario/mergo v0.3.8-0.20190415133143-5ef87b449ca7
+       github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
+       github.com/jmcvetta/randutil v0.0.0-20150817122601-2bb1b664bcff
+       github.com/julienschmidt/httprouter v1.2.0
+       github.com/kevinburke/ssh_config v0.0.0-20171013211458-802051befeb5 // indirect
+       github.com/lib/pq v0.0.0-20171126050459-83612a56d3dd
+       github.com/marstr/guid v1.1.1-0.20170427235115-8bdf7d1a087c // indirect
+       github.com/mitchellh/go-homedir v0.0.0-20161203194507-b8bc1bf76747 // indirect
+       github.com/opencontainers/go-digest v1.0.0-rc1 // indirect
+       github.com/opencontainers/image-spec v1.0.1-0.20171125024018-577479e4dc27 // indirect
+       github.com/pelletier/go-buffruneio v0.2.0 // indirect
+       github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 // indirect
+       github.com/prometheus/client_golang v1.2.1
+       github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4
+       github.com/prometheus/common v0.7.0
+       github.com/satori/go.uuid v1.2.1-0.20180103174451-36e9d2ebbde5 // indirect
+       github.com/sergi/go-diff v1.0.0 // indirect
+       github.com/sirupsen/logrus v1.4.2
+       github.com/src-d/gcfg v1.3.0 // indirect
+       github.com/stretchr/testify v1.4.0 // indirect
+       github.com/xanzy/ssh-agent v0.1.0 // indirect
+       golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2
+       golang.org/x/net v0.0.0-20190613194153-d28f0bde5980
+       golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45
+       golang.org/x/sys v0.0.0-20191105231009-c1f44814a5cd // indirect
+       google.golang.org/api v0.13.0
+       gopkg.in/check.v1 v1.0.0-20161208181325-20d25e280405
+       gopkg.in/square/go-jose.v2 v2.3.1
+       gopkg.in/src-d/go-billy.v4 v4.0.1
+       gopkg.in/src-d/go-git-fixtures.v3 v3.5.0 // indirect
+       gopkg.in/src-d/go-git.v4 v4.0.0
+       gopkg.in/warnings.v0 v0.1.2 // indirect
+       gopkg.in/yaml.v2 v2.2.4 // indirect
+       rsc.io/getopt v0.0.0-20170811000552-20be20937449
+)
+
+replace github.com/AdRoll/goamz => github.com/curoverse/goamz v0.0.0-20190905141525-1bba09f407ef
diff --git a/go.sum b/go.sum
new file mode 100644 (file)
index 0000000..45ba0b1
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,256 @@
+cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+cloud.google.com/go v0.38.0 h1:ROfEUZz+Gh5pa62DJWXSaonyu3StP6EA6lPEXPI6mCo=
+cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
+github.com/Azure/azure-sdk-for-go v19.1.0+incompatible h1:ysqLW+tqZjJWOTE74heH/pDRbr4vlN3yV+dqQYgpyxw=
+github.com/Azure/azure-sdk-for-go v19.1.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
+github.com/Azure/go-autorest v10.15.2+incompatible h1:oZpnRzZie83xGV5txbT1aa/7zpCPvURGhV6ThJij2bs=
+github.com/Azure/go-autorest v10.15.2+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
+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/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/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=
+github.com/aws/aws-sdk-go v1.25.30 h1:I9qj6zW3mMfsg91e+GMSN/INcaX9tTFvr/l/BAHKaIY=
+github.com/aws/aws-sdk-go v1.25.30/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
+github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
+github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
+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/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/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=
+github.com/coreos/go-systemd v0.0.0-20180108085132-cc4f39464dc7 h1:e3u8KWFMR3irlDo1Z/tL8Hsz1MJmCLkSoX5AZRMKZkg=
+github.com/coreos/go-systemd v0.0.0-20180108085132-cc4f39464dc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
+github.com/curoverse/goamz v0.0.0-20190905141525-1bba09f407ef h1:k3Q9m06dbTShrR4phl/QNi15ZSPkIwgyQmNvJRcXR3Y=
+github.com/curoverse/goamz v0.0.0-20190905141525-1bba09f407ef/go.mod h1:NUkr+hZ9k+l0cEXg9S7EW8+UIfPkP/hNy2Ga0QVPZ88=
+github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
+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.1.0+incompatible h1:FFziAwDQQ2dz1XClWMkwvukur3evtZx7x/wMHKM1i20=
+github.com/dgrijalva/jwt-go v3.1.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
+github.com/dimchansky/utfbom v1.0.0 h1:fGC2kkf4qOoKqZ4q7iIh+Vef4ubC1c38UDsEyZynZPc=
+github.com/dimchansky/utfbom v1.0.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQvIirEdv+8=
+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=
+github.com/docker/distribution v2.6.0-rc.1.0.20180105232752-277ed486c948+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
+github.com/docker/docker v1.4.2-0.20180109013817-94b8a116fbf1 h1:0NaIDWeMBQIQACbThhJaL8lts6EMPSTCMLeDstJ6gU8=
+github.com/docker/docker v1.4.2-0.20180109013817-94b8a116fbf1/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
+github.com/docker/go-connections v0.3.0 h1:3lOnM9cSzgGwx8VfK/NGOW5fLQ0GjIlCkaktF+n1M6o=
+github.com/docker/go-connections v0.3.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
+github.com/docker/go-units v0.3.3-0.20171221200356-d59758554a3d h1:dVaNRYvaGV23AdNdsm+4y1mPN0tj3/1v6taqKMmM6Ko=
+github.com/docker/go-units v0.3.3-0.20171221200356-d59758554a3d/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
+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/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
+github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
+github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0=
+github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
+github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
+github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
+github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
+github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
+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/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/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 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg=
+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/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 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=
+github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
+github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
+github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
+github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM=
+github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
+github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
+github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
+github.com/gorilla/mux v1.6.1-0.20180107155708-5bbbb5b2b572 h1:eWMpQtfzS3D63EI50baSfP/zjyqFM9tDfvVyAlCIMic=
+github.com/gorilla/mux v1.6.1-0.20180107155708-5bbbb5b2b572/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
+github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
+github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU=
+github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
+github.com/imdario/mergo v0.3.8-0.20190415133143-5ef87b449ca7 h1:kUGMXUVH7IU1rKA3TZu9ROUE61dVv2SSgSsdeYKm0mg=
+github.com/imdario/mergo v0.3.8-0.20190415133143-5ef87b449ca7/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
+github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
+github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
+github.com/jmcvetta/randutil v0.0.0-20150817122601-2bb1b664bcff h1:6NvhExg4omUC9NfA+l4Oq3ibNNeJUdiAF3iBVB0PlDk=
+github.com/jmcvetta/randutil v0.0.0-20150817122601-2bb1b664bcff/go.mod h1:ddfPX8Z28YMjiqoaJhNBzWHapTHXejnB5cDCUWDwriw=
+github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM=
+github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
+github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
+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/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/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
+github.com/lib/pq v0.0.0-20171126050459-83612a56d3dd h1:2RDaVc4/izhWyAvYxNm8c9saSyCDIxefNwOcqaH7pcU=
+github.com/lib/pq v0.0.0-20171126050459-83612a56d3dd/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
+github.com/marstr/guid v1.1.1-0.20170427235115-8bdf7d1a087c h1:ouxemItv3B/Zh008HJkEXDYCN3BIRyNHxtUN7ThJ5Js=
+github.com/marstr/guid v1.1.1-0.20170427235115-8bdf7d1a087c/go.mod h1:74gB1z2wpxxInTG6yaqA7KrtM0NZ+RbrcqDvYHefzho=
+github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
+github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
+github.com/mitchellh/go-homedir v0.0.0-20161203194507-b8bc1bf76747 h1:eQox4Rh4ewJF+mqYPxCkmBAirRnPaHEB26UkNuPyjlk=
+github.com/mitchellh/go-homedir v0.0.0-20161203194507-b8bc1bf76747/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/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/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+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=
+github.com/opencontainers/image-spec v1.0.1-0.20171125024018-577479e4dc27 h1:8Q+VFspwMHwvVvpSS8xpuFQR7RpGX8G8ECXwgc/05sg=
+github.com/opencontainers/image-spec v1.0.1-0.20171125024018-577479e4dc27/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
+github.com/pelletier/go-buffruneio v0.2.0 h1:U4t4R6YkofJ5xHm3dJzuRpPZ0mr5MMCoAWooScCR7aA=
+github.com/pelletier/go-buffruneio v0.2.0/go.mod h1:JkE26KsDizTr40EUHkXVtNPvgGtbSNq5BcowyYOWdKo=
+github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
+github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 h1:J9b7z+QKAmPf4YLrFg6oQUotqHQeUNWwkvo7jZp1GLU=
+github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA=
+github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
+github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
+github.com/prometheus/client_golang v1.2.1 h1:JnMpQc6ppsNgw9QPAGF6Dod479itz7lvlsMzzNayLOI=
+github.com/prometheus/client_golang v1.2.1/go.mod h1:XMU6Z2MjaRKVu/dC1qupJI9SiNkDYzz3xecMgSW/F+U=
+github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
+github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4 h1:gQz4mCbXsO+nc9n1hCxHcGA3Zx3Eo+UHZoInFGUIXNM=
+github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
+github.com/prometheus/common v0.7.0 h1:L+1lyG48J1zAQXA3RBX/nG/B3gjlHq0zTt2tlbJLyCY=
+github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA=
+github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
+github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
+github.com/prometheus/procfs v0.0.5 h1:3+auTFlqw+ZaQYJARz6ArODtkaIwtvBTx3N2NehQlL8=
+github.com/prometheus/procfs v0.0.5/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ=
+github.com/satori/go.uuid v1.2.1-0.20180103174451-36e9d2ebbde5 h1:Jw7W4WMfQDxsXvfeFSaS2cHlY7bAF4MGrgnbd0+Uo78=
+github.com/satori/go.uuid v1.2.1-0.20180103174451-36e9d2ebbde5/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
+github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
+github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
+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/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/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=
+github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
+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=
+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 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+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/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+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=
+golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c h1:uOCk1iQW6Vc18bnC13MfzScl+wdKBmM9Y9kU7Z83/lw=
+golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190613194153-d28f0bde5980 h1:dfGZHvZk057jK2MCeWus/TowKpJ8y4AmooUzdBSR9GU=
+golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+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=
+golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+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/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=
+golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191105231009-c1f44814a5cd h1:3x5uuvBgE6oaXJjCOvpCC1IpgJogqQ+PqGGU3ZxAgII=
+golang.org/x/sys v0.0.0-20191105231009-c1f44814a5cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+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 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
+golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
+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=
+golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
+golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+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=
+google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
+google.golang.org/api v0.13.0 h1:Q3Ui3V3/CVinFWFiW39Iw0kMuVrRzYX0wN6OPFp0lTA=
+google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
+google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
+google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/appengine v1.5.0 h1:KxkO13IPW4Lslp2bz+KHP2E3gtFlrIGNThxkZQ3g+4c=
+google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
+google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873 h1:nfPFGzJkUDX6uBmpN/pSw7MbOAWegH5QDQuoXFHedLg=
+google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+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/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
+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/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=
+gopkg.in/src-d/go-billy.v4 v4.0.1 h1:iMxwQPj2cuKRyaIZ985zxClkcdTtT5VpXYf4PTJc0Ek=
+gopkg.in/src-d/go-billy.v4 v4.0.1/go.mod h1:ZHSF0JP+7oD97194otDUCD7Ofbk63+xFcfWP5bT6h+Q=
+gopkg.in/src-d/go-git-fixtures.v3 v3.5.0 h1:ivZFOIltbce2Mo8IjzUHAFoq/IylO9WHhNOAJK+LsJg=
+gopkg.in/src-d/go-git-fixtures.v3 v3.5.0/go.mod h1:dLBcvytrw/TYZsNTWCnkNF2DSIlzWYqTe3rJR56Ac7g=
+gopkg.in/src-d/go-git.v4 v4.0.0 h1:9ZRNKHuhaTaJRGcGaH6Qg7uUORO2X0MNB5WL/CDdqto=
+gopkg.in/src-d/go-git.v4 v4.0.0/go.mod h1:CzbUWqMn4pvmvndg3gnh5iZFmSsbhyhUWdI0IQ60AQo=
+gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
+gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
+gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
+gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+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/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 9f473e361931aff39bc7a5f8a276a6e626c97bc5..81c36b9bfbe2b00626403cfde5cbdfd2894c34f9 100644 (file)
@@ -493,8 +493,29 @@ Clusters:
     Login:
       # These settings are provided by your OAuth2 provider (eg
       # Google) used to perform upstream authentication.
-      ProviderAppSecret: ""
       ProviderAppID: ""
+      ProviderAppSecret: ""
+
+      # (Experimental) Authenticate with Google, bypassing the
+      # SSO-provider gateway service. Use the Google Cloud console to
+      # enable the People API (APIs and Services > Enable APIs and
+      # services > Google People API > Enable), generate a Client ID
+      # and secret (APIs and Services > Credentials > Create
+      # credentials > OAuth client ID > Web application) and add your
+      # controller's /login URL (e.g.,
+      # "https://zzzzz.example.com/login") as an authorized redirect
+      # URL.
+      #
+      # Requires EnableBetaController14287. ProviderAppID must be
+      # blank.
+      GoogleClientID: ""
+      GoogleClientSecret: ""
+
+      # Allow users to log in to existing accounts using any verified
+      # email address listed by their Google account. If true, the
+      # Google People API must be enabled in order for Google login to
+      # work. If false, only the primary email address will be used.
+      GoogleAlternateEmailAddresses: true
 
       # The cluster ID to delegate the user database.  When set,
       # logins on this cluster will be redirected to the login cluster
index 7b11e090eeee7479effd5c37b8c834798041c0a7..95116ec2e50c292a3fd4036d17f6d7603dbe7b5f 100644 (file)
@@ -370,27 +370,27 @@ const defaultKeepWebConfigPath = "/etc/arvados/keep-web/keep-web.yml"
 type oldKeepWebConfig struct {
        Client *arvados.Client
 
-       Listen string
+       Listen *string
 
-       AnonymousTokens    []string
-       AttachmentOnlyHost string
-       TrustAllContent    bool
+       AnonymousTokens    *[]string
+       AttachmentOnlyHost *string
+       TrustAllContent    *bool
 
        Cache struct {
-               TTL                  arvados.Duration
-               UUIDTTL              arvados.Duration
-               MaxCollectionEntries int
-               MaxCollectionBytes   int64
-               MaxPermissionEntries int
-               MaxUUIDEntries       int
+               TTL                  *arvados.Duration
+               UUIDTTL              *arvados.Duration
+               MaxCollectionEntries *int
+               MaxCollectionBytes   *int64
+               MaxPermissionEntries *int
+               MaxUUIDEntries       *int
        }
 
        // Hack to support old command line flag, which is a bool
        // meaning "get actual token from environment".
-       deprecatedAllowAnonymous bool
+       deprecatedAllowAnonymous *bool
 
        // Authorization token to be included in all health check requests.
-       ManagementToken string
+       ManagementToken *string
 }
 
 func (ldr *Loader) loadOldKeepWebConfig(cfg *arvados.Config) error {
@@ -412,22 +412,43 @@ func (ldr *Loader) loadOldKeepWebConfig(cfg *arvados.Config) error {
 
        loadOldClientConfig(cluster, oc.Client)
 
-       cluster.Services.WebDAV.InternalURLs[arvados.URL{Host: oc.Listen}] = arvados.ServiceInstance{}
-       cluster.Services.WebDAVDownload.InternalURLs[arvados.URL{Host: oc.Listen}] = arvados.ServiceInstance{}
-       cluster.Services.WebDAVDownload.ExternalURL = arvados.URL{Host: oc.AttachmentOnlyHost}
-       cluster.TLS.Insecure = oc.Client.Insecure
-       cluster.ManagementToken = oc.ManagementToken
-       cluster.Collections.TrustAllContent = oc.TrustAllContent
-       cluster.Collections.WebDAVCache.TTL = oc.Cache.TTL
-       cluster.Collections.WebDAVCache.UUIDTTL = oc.Cache.UUIDTTL
-       cluster.Collections.WebDAVCache.MaxCollectionEntries = oc.Cache.MaxCollectionEntries
-       cluster.Collections.WebDAVCache.MaxCollectionBytes = oc.Cache.MaxCollectionBytes
-       cluster.Collections.WebDAVCache.MaxPermissionEntries = oc.Cache.MaxPermissionEntries
-       cluster.Collections.WebDAVCache.MaxUUIDEntries = oc.Cache.MaxUUIDEntries
-       if len(oc.AnonymousTokens) > 0 {
-               cluster.Users.AnonymousUserToken = oc.AnonymousTokens[0]
-               if len(oc.AnonymousTokens) > 1 {
-                       ldr.Logger.Warn("More than 1 anonymous tokens configured, using only the first and discarding the rest.")
+       if oc.Listen != nil {
+               cluster.Services.WebDAV.InternalURLs[arvados.URL{Host: *oc.Listen}] = arvados.ServiceInstance{}
+               cluster.Services.WebDAVDownload.InternalURLs[arvados.URL{Host: *oc.Listen}] = arvados.ServiceInstance{}
+       }
+       if oc.AttachmentOnlyHost != nil {
+               cluster.Services.WebDAVDownload.ExternalURL = arvados.URL{Host: *oc.AttachmentOnlyHost}
+       }
+       if oc.ManagementToken != nil {
+               cluster.ManagementToken = *oc.ManagementToken
+       }
+       if oc.TrustAllContent != nil {
+               cluster.Collections.TrustAllContent = *oc.TrustAllContent
+       }
+       if oc.Cache.TTL != nil {
+               cluster.Collections.WebDAVCache.TTL = *oc.Cache.TTL
+       }
+       if oc.Cache.UUIDTTL != nil {
+               cluster.Collections.WebDAVCache.UUIDTTL = *oc.Cache.UUIDTTL
+       }
+       if oc.Cache.MaxCollectionEntries != nil {
+               cluster.Collections.WebDAVCache.MaxCollectionEntries = *oc.Cache.MaxCollectionEntries
+       }
+       if oc.Cache.MaxCollectionBytes != nil {
+               cluster.Collections.WebDAVCache.MaxCollectionBytes = *oc.Cache.MaxCollectionBytes
+       }
+       if oc.Cache.MaxPermissionEntries != nil {
+               cluster.Collections.WebDAVCache.MaxPermissionEntries = *oc.Cache.MaxPermissionEntries
+       }
+       if oc.Cache.MaxUUIDEntries != nil {
+               cluster.Collections.WebDAVCache.MaxUUIDEntries = *oc.Cache.MaxUUIDEntries
+       }
+       if oc.AnonymousTokens != nil {
+               if len(*oc.AnonymousTokens) > 0 {
+                       cluster.Users.AnonymousUserToken = (*oc.AnonymousTokens)[0]
+                       if len(*oc.AnonymousTokens) > 1 {
+                               ldr.Logger.Warn("More than 1 anonymous tokens configured, using only the first and discarding the rest.")
+                       }
                }
        }
 
@@ -439,11 +460,11 @@ const defaultGitHttpdConfigPath = "/etc/arvados/git-httpd/git-httpd.yml"
 
 type oldGitHttpdConfig struct {
        Client          *arvados.Client
-       Listen          string
-       GitCommand      string
-       GitoliteHome    string
-       RepoRoot        string
-       ManagementToken string
+       Listen          *string
+       GitCommand      *string
+       GitoliteHome    *string
+       RepoRoot        *string
+       ManagementToken *string
 }
 
 func (ldr *Loader) loadOldGitHttpdConfig(cfg *arvados.Config) error {
@@ -465,12 +486,21 @@ func (ldr *Loader) loadOldGitHttpdConfig(cfg *arvados.Config) error {
 
        loadOldClientConfig(cluster, oc.Client)
 
-       cluster.Services.GitHTTP.InternalURLs[arvados.URL{Host: oc.Listen}] = arvados.ServiceInstance{}
-       cluster.TLS.Insecure = oc.Client.Insecure
-       cluster.ManagementToken = oc.ManagementToken
-       cluster.Git.GitCommand = oc.GitCommand
-       cluster.Git.GitoliteHome = oc.GitoliteHome
-       cluster.Git.Repositories = oc.RepoRoot
+       if oc.Listen != nil {
+               cluster.Services.GitHTTP.InternalURLs[arvados.URL{Host: *oc.Listen}] = arvados.ServiceInstance{}
+       }
+       if oc.ManagementToken != nil {
+               cluster.ManagementToken = *oc.ManagementToken
+       }
+       if oc.GitCommand != nil {
+               cluster.Git.GitCommand = *oc.GitCommand
+       }
+       if oc.GitoliteHome != nil {
+               cluster.Git.GitoliteHome = *oc.GitoliteHome
+       }
+       if oc.RepoRoot != nil {
+               cluster.Git.Repositories = *oc.RepoRoot
+       }
 
        cfg.Clusters[cluster.ClusterID] = *cluster
        return nil
index ff1bb9434a42c8babc3cedef9165e7ad3d16d949..845c73c053629f6bceb77af9f317524d435e4ec3 100644 (file)
@@ -15,6 +15,9 @@ import (
        check "gopkg.in/check.v1"
 )
 
+// Configured at: sdk/python/tests/run_test_server.py
+const TestServerManagementToken = "e687950a23c3a9bceec28c6223a06c79"
+
 func testLoadLegacyConfig(content []byte, mungeFlag string, c *check.C) (*arvados.Cluster, error) {
        tmpfile, err := ioutil.TempFile("", "example")
        if err != nil {
@@ -133,6 +136,23 @@ func (s *LoadSuite) TestLegacyKeepWebConfig(c *check.C) {
        c.Check(cluster.ManagementToken, check.Equals, "xyzzy")
 }
 
+// Tests fix for https://dev.arvados.org/issues/15642
+func (s *LoadSuite) TestLegacyKeepWebConfigDoesntDisableMissingItems(c *check.C) {
+       content := []byte(`
+{
+       "Client": {
+               "Scheme": "",
+               "APIHost": "example.com",
+               "AuthToken": "abcdefg",
+       }
+}
+`)
+       cluster, err := testLoadLegacyConfig(content, "-legacy-keepweb-config", c)
+       c.Check(err, check.IsNil)
+       // The resulting ManagementToken should be the one set up on the test server.
+       c.Check(cluster.ManagementToken, check.Equals, TestServerManagementToken)
+}
+
 func (s *LoadSuite) TestLegacyKeepproxyConfig(c *check.C) {
        f := "-legacy-keepproxy-config"
        content := []byte(fmtKeepproxyConfig("", true))
@@ -217,6 +237,23 @@ func (s *LoadSuite) TestLegacyArvGitHttpdConfig(c *check.C) {
        c.Check(cluster.Services.Keepproxy.InternalURLs[arvados.URL{Host: ":9000"}], check.Equals, arvados.ServiceInstance{})
 }
 
+// Tests fix for https://dev.arvados.org/issues/15642
+func (s *LoadSuite) TestLegacyArvGitHttpdConfigDoesntDisableMissingItems(c *check.C) {
+       content := []byte(`
+{
+       "Client": {
+               "Scheme": "",
+               "APIHost": "example.com",
+               "AuthToken": "abcdefg",
+       }
+}
+`)
+       cluster, err := testLoadLegacyConfig(content, "-legacy-git-httpd-config", c)
+       c.Check(err, check.IsNil)
+       // The resulting ManagementToken should be the one set up on the test server.
+       c.Check(cluster.ManagementToken, check.Equals, TestServerManagementToken)
+}
+
 func (s *LoadSuite) TestLegacyKeepBalanceConfig(c *check.C) {
        f := "-legacy-keepbalance-config"
        content := []byte(fmtKeepBalanceConfig(""))
index 5f17b345944e612af899b7b15e370e4df8f3e5ca..7adacab4c8df392ef35f57c6ecb6d1cbe8f7d749 100644 (file)
@@ -130,8 +130,11 @@ var whitelist = map[string]bool{
        "InstanceTypes.*":                              true,
        "InstanceTypes.*.*":                            true,
        "Login":                                        true,
-       "Login.ProviderAppSecret":                      false,
+       "Login.GoogleClientID":                         false,
+       "Login.GoogleClientSecret":                     false,
+       "Login.GoogleAlternateEmailAddresses":          false,
        "Login.ProviderAppID":                          false,
+       "Login.ProviderAppSecret":                      false,
        "Login.LoginCluster":                           true,
        "Login.RemoteTokenRefresh":                     true,
        "Mail":                                         false,
index dbf11569f65fac975b8fdfd3c54f4d4e48a7413d..68dea169f843072aa33c4f6905e6651011c57fe4 100644 (file)
@@ -499,8 +499,29 @@ Clusters:
     Login:
       # These settings are provided by your OAuth2 provider (eg
       # Google) used to perform upstream authentication.
-      ProviderAppSecret: ""
       ProviderAppID: ""
+      ProviderAppSecret: ""
+
+      # (Experimental) Authenticate with Google, bypassing the
+      # SSO-provider gateway service. Use the Google Cloud console to
+      # enable the People API (APIs and Services > Enable APIs and
+      # services > Google People API > Enable), generate a Client ID
+      # and secret (APIs and Services > Credentials > Create
+      # credentials > OAuth client ID > Web application) and add your
+      # controller's /login URL (e.g.,
+      # "https://zzzzz.example.com/login") as an authorized redirect
+      # URL.
+      #
+      # Requires EnableBetaController14287. ProviderAppID must be
+      # blank.
+      GoogleClientID: ""
+      GoogleClientSecret: ""
+
+      # Allow users to log in to existing accounts using any verified
+      # email address listed by their Google account. If true, the
+      # Google People API must be enabled in order for Google login to
+      # work. If false, only the primary email address will be used.
+      GoogleAlternateEmailAddresses: true
 
       # The cluster ID to delegate the user database.  When set,
       # logins on this cluster will be redirected to the login cluster
index 3bcafacd2c8bc47bdce87bb6908169c710662c6d..3829d0a40adab273f7ea126ac8ff40d92527fc46 100644 (file)
@@ -17,7 +17,7 @@ import (
        "strings"
 
        "git.curoverse.com/arvados.git/lib/config"
-       "git.curoverse.com/arvados.git/lib/controller/railsproxy"
+       "git.curoverse.com/arvados.git/lib/controller/localdb"
        "git.curoverse.com/arvados.git/lib/controller/rpc"
        "git.curoverse.com/arvados.git/sdk/go/arvados"
        "git.curoverse.com/arvados.git/sdk/go/auth"
@@ -31,7 +31,7 @@ type Conn struct {
 }
 
 func New(cluster *arvados.Cluster) *Conn {
-       local := railsproxy.NewConn(cluster)
+       local := localdb.NewConn(cluster)
        remotes := map[string]backend{}
        for id, remote := range cluster.RemoteClusters {
                if !remote.Proxy {
@@ -185,6 +185,30 @@ func (conn *Conn) ConfigGet(ctx context.Context) (json.RawMessage, error) {
        return json.RawMessage(buf.Bytes()), err
 }
 
+func (conn *Conn) Login(ctx context.Context, options arvados.LoginOptions) (arvados.LoginResponse, error) {
+       if id := conn.cluster.Login.LoginCluster; id != "" && id != conn.cluster.ClusterID {
+               // defer entire login procedure to designated cluster
+               remote, ok := conn.remotes[id]
+               if !ok {
+                       return arvados.LoginResponse{}, fmt.Errorf("configuration problem: designated login cluster %q is not defined", id)
+               }
+               baseURL := remote.BaseURL()
+               target, err := baseURL.Parse(arvados.EndpointLogin.Path)
+               if err != nil {
+                       return arvados.LoginResponse{}, fmt.Errorf("internal error getting redirect target: %s", err)
+               }
+               target.RawQuery = url.Values{
+                       "return_to": []string{options.ReturnTo},
+                       "remote":    []string{options.Remote},
+               }.Encode()
+               return arvados.LoginResponse{
+                       RedirectLocation: target.String(),
+               }, nil
+       } else {
+               return conn.local.Login(ctx, options)
+       }
+}
+
 func (conn *Conn) CollectionGet(ctx context.Context, options arvados.GetOptions) (arvados.Collection, error) {
        if len(options.UUID) == 27 {
                // UUID is really a UUID
@@ -291,7 +315,10 @@ func (conn *Conn) APIClientAuthorizationCurrent(ctx context.Context, options arv
        return conn.chooseBackend(options.UUID).APIClientAuthorizationCurrent(ctx, options)
 }
 
-type backend interface{ arvados.API }
+type backend interface {
+       arvados.API
+       BaseURL() url.URL
+}
 
 type notFoundError struct{}
 
index e9e8950b992e498d072d87be18cfd8737c312f39..c9b981fc15fed27d5fb75131894a2cff6cd77a41 100644 (file)
@@ -59,14 +59,14 @@ func (s *FederationSuite) SetUpTest(c *check.C) {
        s.fed = New(s.cluster)
 }
 
-func (s *FederationSuite) addDirectRemote(c *check.C, id string, backend arvados.API) {
+func (s *FederationSuite) addDirectRemote(c *check.C, id string, backend backend) {
        s.cluster.RemoteClusters[id] = arvados.RemoteCluster{
                Host: "in-process.local",
        }
        s.fed.remotes[id] = backend
 }
 
-func (s *FederationSuite) addHTTPRemote(c *check.C, id string, backend arvados.API) {
+func (s *FederationSuite) addHTTPRemote(c *check.C, id string, backend backend) {
        srv := httpserver.Server{Addr: ":"}
        srv.Handler = router.New(backend)
        c.Check(srv.Start(), check.IsNil)
diff --git a/lib/controller/federation/login_test.go b/lib/controller/federation/login_test.go
new file mode 100644 (file)
index 0000000..e001014
--- /dev/null
@@ -0,0 +1,32 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+package federation
+
+import (
+       "context"
+       "net/url"
+
+       "git.curoverse.com/arvados.git/sdk/go/arvados"
+       "git.curoverse.com/arvados.git/sdk/go/arvadostest"
+       check "gopkg.in/check.v1"
+)
+
+func (s *FederationSuite) TestDeferToLoginCluster(c *check.C) {
+       s.addHTTPRemote(c, "zhome", &arvadostest.APIStub{})
+       s.cluster.Login.LoginCluster = "zhome"
+
+       returnTo := "https://app.example.com/foo?bar"
+       for _, remote := range []string{"", "ccccc"} {
+               resp, err := s.fed.Login(context.Background(), arvados.LoginOptions{Remote: remote, ReturnTo: returnTo})
+               c.Check(err, check.IsNil)
+               c.Logf("remote %q -- RedirectLocation %q", remote, resp.RedirectLocation)
+               target, err := url.Parse(resp.RedirectLocation)
+               c.Check(err, check.IsNil)
+               c.Check(target.Host, check.Equals, s.cluster.RemoteClusters["zhome"].Host)
+               c.Check(target.Scheme, check.Equals, "http")
+               c.Check(target.Query().Get("remote"), check.Equals, remote)
+               c.Check(target.Query().Get("return_to"), check.Equals, returnTo)
+       }
+}
index f7b2362f371e71b99a196d4b795e54a927e81919..f925233ba36ddfdddab0b26b3ea16db772e2877a 100644 (file)
@@ -83,6 +83,7 @@ func (h *Handler) setup() {
        if h.Cluster.EnableBetaController14287 {
                mux.Handle("/arvados/v1/collections", rtr)
                mux.Handle("/arvados/v1/collections/", rtr)
+               mux.Handle("/login", rtr)
        }
 
        hs := http.NotFoundHandler()
index 5dc0b1e86f8f1ff66d689f19ad6ab7d7b699a3de..ebadc5d0213a25f1fd21123a48b4456b042d4b52 100644 (file)
@@ -165,11 +165,18 @@ func (s *HandlerSuite) TestProxyNotFound(c *check.C) {
 }
 
 func (s *HandlerSuite) TestProxyRedirect(c *check.C) {
+       s.cluster.Login.ProviderAppID = "test"
+       s.cluster.Login.ProviderAppSecret = "test"
        req := httptest.NewRequest("GET", "https://0.0.0.0:1/login?return_to=foo", nil)
        resp := httptest.NewRecorder()
        s.handler.ServeHTTP(resp, req)
-       c.Check(resp.Code, check.Equals, http.StatusFound)
-       c.Check(resp.Header().Get("Location"), check.Matches, `https://0.0.0.0:1/auth/joshid\?return_to=%2Cfoo&?`)
+       if !c.Check(resp.Code, check.Equals, http.StatusFound) {
+               c.Log(resp.Body.String())
+       }
+       // Old "proxy entire request" code path returns an absolute
+       // URL. New lib/controller/federation code path returns a
+       // relative URL.
+       c.Check(resp.Header().Get("Location"), check.Matches, `(https://0.0.0.0:1)?/auth/joshid\?return_to=%2Cfoo&?`)
 }
 
 func (s *HandlerSuite) TestValidateV1APIToken(c *check.C) {
diff --git a/lib/controller/localdb/conn.go b/lib/controller/localdb/conn.go
new file mode 100644 (file)
index 0000000..835ab43
--- /dev/null
@@ -0,0 +1,43 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+package localdb
+
+import (
+       "context"
+       "errors"
+
+       "git.curoverse.com/arvados.git/lib/controller/railsproxy"
+       "git.curoverse.com/arvados.git/lib/controller/rpc"
+       "git.curoverse.com/arvados.git/sdk/go/arvados"
+)
+
+type railsProxy = rpc.Conn
+
+type Conn struct {
+       cluster     *arvados.Cluster
+       *railsProxy // handles API methods that aren't defined on Conn itself
+
+       googleLoginController
+}
+
+func NewConn(cluster *arvados.Cluster) *Conn {
+       return &Conn{
+               cluster:    cluster,
+               railsProxy: railsproxy.NewConn(cluster),
+       }
+}
+
+func (conn *Conn) Login(ctx context.Context, opts arvados.LoginOptions) (arvados.LoginResponse, error) {
+       wantGoogle := conn.cluster.Login.GoogleClientID != ""
+       wantSSO := conn.cluster.Login.ProviderAppID != ""
+       if wantGoogle == wantSSO {
+               return arvados.LoginResponse{}, errors.New("configuration problem: exactly one of Login.GoogleClientID and Login.ProviderAppID must be configured")
+       } else if wantGoogle {
+               return conn.googleLoginController.Login(ctx, conn.cluster, conn.railsProxy, opts)
+       } else {
+               // Proxy to RailsAPI, which hands off to sso-provider.
+               return conn.railsProxy.Login(ctx, opts)
+       }
+}
diff --git a/lib/controller/localdb/login.go b/lib/controller/localdb/login.go
new file mode 100644 (file)
index 0000000..13ae366
--- /dev/null
@@ -0,0 +1,275 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+package localdb
+
+import (
+       "bytes"
+       "context"
+       "crypto/hmac"
+       "crypto/sha256"
+       "encoding/base64"
+       "errors"
+       "fmt"
+       "net/url"
+       "strings"
+       "sync"
+       "text/template"
+       "time"
+
+       "git.curoverse.com/arvados.git/lib/controller/rpc"
+       "git.curoverse.com/arvados.git/sdk/go/arvados"
+       "git.curoverse.com/arvados.git/sdk/go/auth"
+       "git.curoverse.com/arvados.git/sdk/go/ctxlog"
+       "github.com/coreos/go-oidc"
+       "golang.org/x/oauth2"
+       "google.golang.org/api/option"
+       "google.golang.org/api/people/v1"
+)
+
+type googleLoginController struct {
+       issuer            string // override OIDC issuer URL (normally https://accounts.google.com) for testing
+       peopleAPIBasePath string // override Google People API base URL (normally set by google pkg to https://people.googleapis.com/)
+       provider          *oidc.Provider
+       mu                sync.Mutex
+}
+
+func (ctrl *googleLoginController) getProvider() (*oidc.Provider, error) {
+       ctrl.mu.Lock()
+       defer ctrl.mu.Unlock()
+       if ctrl.provider == nil {
+               issuer := ctrl.issuer
+               if issuer == "" {
+                       issuer = "https://accounts.google.com"
+               }
+               provider, err := oidc.NewProvider(context.Background(), issuer)
+               if err != nil {
+                       return nil, err
+               }
+               ctrl.provider = provider
+       }
+       return ctrl.provider, nil
+}
+
+func (ctrl *googleLoginController) Login(ctx context.Context, cluster *arvados.Cluster, railsproxy *railsProxy, opts arvados.LoginOptions) (arvados.LoginResponse, error) {
+       provider, err := ctrl.getProvider()
+       if err != nil {
+               return ctrl.loginError(fmt.Errorf("error setting up OpenID Connect provider: %s", err))
+       }
+       redirURL, err := (*url.URL)(&cluster.Services.Controller.ExternalURL).Parse("/login")
+       if err != nil {
+               return ctrl.loginError(fmt.Errorf("error making redirect URL: %s", err))
+       }
+       conf := &oauth2.Config{
+               ClientID:     cluster.Login.GoogleClientID,
+               ClientSecret: cluster.Login.GoogleClientSecret,
+               Endpoint:     provider.Endpoint(),
+               Scopes:       []string{oidc.ScopeOpenID, "profile", "email"},
+               RedirectURL:  redirURL.String(),
+       }
+       verifier := provider.Verifier(&oidc.Config{
+               ClientID: conf.ClientID,
+       })
+       if opts.State == "" {
+               // Initiate Google sign-in.
+               if opts.ReturnTo == "" {
+                       return ctrl.loginError(errors.New("missing return_to parameter"))
+               }
+               me := url.URL(cluster.Services.Controller.ExternalURL)
+               callback, err := me.Parse("/" + arvados.EndpointLogin.Path)
+               if err != nil {
+                       return ctrl.loginError(err)
+               }
+               conf.RedirectURL = callback.String()
+               state := ctrl.newOAuth2State([]byte(cluster.SystemRootToken), opts.Remote, opts.ReturnTo)
+               return arvados.LoginResponse{
+                       RedirectLocation: conf.AuthCodeURL(state.String(),
+                               // prompt=select_account tells Google
+                               // to show the "choose which Google
+                               // account" page, even if the client
+                               // is currently logged in to exactly
+                               // one Google account.
+                               oauth2.SetAuthURLParam("prompt", "select_account")),
+               }, nil
+       } else {
+               // Callback after Google sign-in.
+               state := ctrl.parseOAuth2State(opts.State)
+               if !state.verify([]byte(cluster.SystemRootToken)) {
+                       return ctrl.loginError(errors.New("invalid OAuth2 state"))
+               }
+               oauth2Token, err := conf.Exchange(ctx, opts.Code)
+               if err != nil {
+                       return ctrl.loginError(fmt.Errorf("error in OAuth2 exchange: %s", err))
+               }
+               rawIDToken, ok := oauth2Token.Extra("id_token").(string)
+               if !ok {
+                       return ctrl.loginError(errors.New("error in OAuth2 exchange: no ID token in OAuth2 token"))
+               }
+               idToken, err := verifier.Verify(ctx, rawIDToken)
+               if err != nil {
+                       return ctrl.loginError(fmt.Errorf("error verifying ID token: %s", err))
+               }
+               authinfo, err := ctrl.getAuthInfo(ctx, cluster, conf, oauth2Token, idToken)
+               if err != nil {
+                       return ctrl.loginError(err)
+               }
+               ctxRoot := auth.NewContext(ctx, &auth.Credentials{Tokens: []string{cluster.SystemRootToken}})
+               return railsproxy.UserSessionCreate(ctxRoot, rpc.UserSessionCreateOptions{
+                       ReturnTo: state.Remote + "," + state.ReturnTo,
+                       AuthInfo: *authinfo,
+               })
+       }
+}
+
+// Use a person's token to get all of their email addresses, with the
+// primary address at index 0. The provided defaultAddr is always
+// included in the returned slice, and is used as the primary if the
+// Google API does not indicate one.
+func (ctrl *googleLoginController) getAuthInfo(ctx context.Context, cluster *arvados.Cluster, conf *oauth2.Config, token *oauth2.Token, idToken *oidc.IDToken) (*rpc.UserSessionAuthInfo, error) {
+       var ret rpc.UserSessionAuthInfo
+       defer ctxlog.FromContext(ctx).WithField("ret", &ret).Debug("getAuthInfo returned")
+
+       var claims struct {
+               Name     string `json:"name"`
+               Email    string `json:"email"`
+               Verified bool   `json:"email_verified"`
+       }
+       if err := idToken.Claims(&claims); err != nil {
+               return nil, fmt.Errorf("error extracting claims from ID token: %s", err)
+       } else if claims.Verified {
+               // Fall back to this info if the People API call
+               // (below) doesn't return a primary && verified email.
+               if names := strings.Fields(strings.TrimSpace(claims.Name)); len(names) > 1 {
+                       ret.FirstName = strings.Join(names[0:len(names)-1], " ")
+                       ret.LastName = names[len(names)-1]
+               } else {
+                       ret.FirstName = names[0]
+               }
+               ret.Email = claims.Email
+       }
+
+       if !cluster.Login.GoogleAlternateEmailAddresses {
+               if ret.Email == "" {
+                       return nil, fmt.Errorf("cannot log in with unverified email address %q", claims.Email)
+               }
+               return &ret, nil
+       }
+
+       svc, err := people.NewService(ctx, option.WithTokenSource(conf.TokenSource(ctx, token)), option.WithScopes(people.UserEmailsReadScope))
+       if err != nil {
+               return nil, fmt.Errorf("error setting up People API: %s", err)
+       }
+       if p := ctrl.peopleAPIBasePath; p != "" {
+               // Override normal API endpoint (for testing)
+               svc.BasePath = p
+       }
+       person, err := people.NewPeopleService(svc).Get("people/me").PersonFields("emailAddresses,names").Do()
+       if err != nil {
+               if strings.Contains(err.Error(), "Error 403") && strings.Contains(err.Error(), "accessNotConfigured") {
+                       // Log the original API error, but display
+                       // only the "fix config" advice to the user.
+                       ctxlog.FromContext(ctx).WithError(err).WithField("email", ret.Email).Error("People API is not enabled")
+                       return nil, errors.New("configuration error: Login.GoogleAlternateEmailAddresses is true, but Google People API is not enabled")
+               } else {
+                       return nil, fmt.Errorf("error getting profile info from People API: %s", err)
+               }
+       }
+
+       // The given/family names returned by the People API and
+       // flagged as "primary" (if any) take precedence over the
+       // split-by-whitespace result from above.
+       for _, name := range person.Names {
+               if name.Metadata != nil && name.Metadata.Primary {
+                       ret.FirstName = name.GivenName
+                       ret.LastName = name.FamilyName
+                       break
+               }
+       }
+
+       altEmails := map[string]bool{}
+       if ret.Email != "" {
+               altEmails[ret.Email] = true
+       }
+       for _, ea := range person.EmailAddresses {
+               if ea.Metadata == nil || !ea.Metadata.Verified {
+                       ctxlog.FromContext(ctx).WithField("address", ea.Value).Info("skipping unverified email address")
+                       continue
+               }
+               altEmails[ea.Value] = true
+               if ea.Metadata.Primary || ret.Email == "" {
+                       ret.Email = ea.Value
+               }
+       }
+       if len(altEmails) == 0 {
+               return nil, errors.New("cannot log in without a verified email address")
+       }
+       for ae := range altEmails {
+               if ae != ret.Email {
+                       ret.AlternateEmails = append(ret.AlternateEmails, ae)
+               }
+       }
+       return &ret, nil
+}
+
+func (ctrl *googleLoginController) loginError(sendError error) (resp arvados.LoginResponse, err error) {
+       tmpl, err := template.New("error").Parse(`<h2>Login error:</h2><p>{{.}}</p>`)
+       if err != nil {
+               return
+       }
+       err = tmpl.Execute(&resp.HTML, sendError.Error())
+       return
+}
+
+func (ctrl *googleLoginController) newOAuth2State(key []byte, remote, returnTo string) oauth2State {
+       s := oauth2State{
+               Time:     time.Now().Unix(),
+               Remote:   remote,
+               ReturnTo: returnTo,
+       }
+       s.HMAC = s.computeHMAC(key)
+       return s
+}
+
+type oauth2State struct {
+       HMAC     []byte // hash of other fields; see computeHMAC()
+       Time     int64  // creation time (unix timestamp)
+       Remote   string // remote cluster if requesting a salted token, otherwise blank
+       ReturnTo string // redirect target
+}
+
+func (ctrl *googleLoginController) parseOAuth2State(encoded string) (s oauth2State) {
+       // Errors are not checked. If decoding/parsing fails, the
+       // token will be rejected by verify().
+       decoded, _ := base64.RawURLEncoding.DecodeString(encoded)
+       f := strings.Split(string(decoded), "\n")
+       if len(f) != 4 {
+               return
+       }
+       fmt.Sscanf(f[0], "%x", &s.HMAC)
+       fmt.Sscanf(f[1], "%x", &s.Time)
+       fmt.Sscanf(f[2], "%s", &s.Remote)
+       fmt.Sscanf(f[3], "%s", &s.ReturnTo)
+       return
+}
+
+func (s oauth2State) verify(key []byte) bool {
+       if delta := time.Now().Unix() - s.Time; delta < 0 || delta > 300 {
+               return false
+       }
+       return hmac.Equal(s.computeHMAC(key), s.HMAC)
+}
+
+func (s oauth2State) String() string {
+       var buf bytes.Buffer
+       enc := base64.NewEncoder(base64.RawURLEncoding, &buf)
+       fmt.Fprintf(enc, "%x\n%x\n%s\n%s", s.HMAC, s.Time, s.Remote, s.ReturnTo)
+       enc.Close()
+       return buf.String()
+}
+
+func (s oauth2State) computeHMAC(key []byte) []byte {
+       mac := hmac.New(sha256.New, key)
+       fmt.Fprintf(mac, "%x %s %s", s.Time, s.Remote, s.ReturnTo)
+       return mac.Sum(nil)
+}
diff --git a/lib/controller/localdb/login_test.go b/lib/controller/localdb/login_test.go
new file mode 100644 (file)
index 0000000..c5b9ee0
--- /dev/null
@@ -0,0 +1,450 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+package localdb
+
+import (
+       "bytes"
+       "context"
+       "crypto/rand"
+       "crypto/rsa"
+       "encoding/json"
+       "fmt"
+       "net/http"
+       "net/http/httptest"
+       "net/url"
+       "sort"
+       "strings"
+       "testing"
+       "time"
+
+       "git.curoverse.com/arvados.git/lib/config"
+       "git.curoverse.com/arvados.git/lib/controller/rpc"
+       "git.curoverse.com/arvados.git/sdk/go/arvados"
+       "git.curoverse.com/arvados.git/sdk/go/arvadostest"
+       "git.curoverse.com/arvados.git/sdk/go/auth"
+       "git.curoverse.com/arvados.git/sdk/go/ctxlog"
+       check "gopkg.in/check.v1"
+       jose "gopkg.in/square/go-jose.v2"
+)
+
+// Gocheck boilerplate
+func Test(t *testing.T) {
+       check.TestingT(t)
+}
+
+var _ = check.Suite(&LoginSuite{})
+
+type LoginSuite struct {
+       cluster               *arvados.Cluster
+       ctx                   context.Context
+       localdb               *Conn
+       railsSpy              *arvadostest.Proxy
+       fakeIssuer            *httptest.Server
+       fakePeopleAPI         *httptest.Server
+       fakePeopleAPIResponse map[string]interface{}
+       issuerKey             *rsa.PrivateKey
+
+       // expected token request
+       validCode string
+       // desired response from token endpoint
+       authEmail         string
+       authEmailVerified bool
+       authName          string
+}
+
+func (s *LoginSuite) TearDownSuite(c *check.C) {
+       // Undo any changes/additions to the user database so they
+       // don't affect subsequent tests.
+       arvadostest.ResetEnv()
+       c.Check(arvados.NewClientFromEnv().RequestAndDecode(nil, "POST", "database/reset", nil, nil), check.IsNil)
+}
+
+func (s *LoginSuite) SetUpTest(c *check.C) {
+       var err error
+       s.issuerKey, err = rsa.GenerateKey(rand.Reader, 2048)
+       c.Assert(err, check.IsNil)
+
+       s.authEmail = "active-user@arvados.local"
+       s.authEmailVerified = true
+       s.authName = "Fake User Name"
+       s.fakeIssuer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
+               req.ParseForm()
+               c.Logf("fakeIssuer: got req: %s %s %s", req.Method, req.URL, req.Form)
+               w.Header().Set("Content-Type", "application/json")
+               switch req.URL.Path {
+               case "/.well-known/openid-configuration":
+                       json.NewEncoder(w).Encode(map[string]interface{}{
+                               "issuer":                 s.fakeIssuer.URL,
+                               "authorization_endpoint": s.fakeIssuer.URL + "/auth",
+                               "token_endpoint":         s.fakeIssuer.URL + "/token",
+                               "jwks_uri":               s.fakeIssuer.URL + "/jwks",
+                               "userinfo_endpoint":      s.fakeIssuer.URL + "/userinfo",
+                       })
+               case "/token":
+                       if req.Form.Get("code") != s.validCode || s.validCode == "" {
+                               w.WriteHeader(http.StatusUnauthorized)
+                               return
+                       }
+                       idToken, _ := json.Marshal(map[string]interface{}{
+                               "iss":            s.fakeIssuer.URL,
+                               "aud":            []string{"test%client$id"},
+                               "sub":            "fake-user-id",
+                               "exp":            time.Now().UTC().Add(time.Minute).UnixNano(),
+                               "iat":            time.Now().UTC().UnixNano(),
+                               "nonce":          "fake-nonce",
+                               "email":          s.authEmail,
+                               "email_verified": s.authEmailVerified,
+                               "name":           s.authName,
+                       })
+                       json.NewEncoder(w).Encode(struct {
+                               AccessToken  string `json:"access_token"`
+                               TokenType    string `json:"token_type"`
+                               RefreshToken string `json:"refresh_token"`
+                               ExpiresIn    int32  `json:"expires_in"`
+                               IDToken      string `json:"id_token"`
+                       }{
+                               AccessToken:  s.fakeToken(c, []byte("fake access token")),
+                               TokenType:    "Bearer",
+                               RefreshToken: "test-refresh-token",
+                               ExpiresIn:    30,
+                               IDToken:      s.fakeToken(c, idToken),
+                       })
+               case "/jwks":
+                       json.NewEncoder(w).Encode(jose.JSONWebKeySet{
+                               Keys: []jose.JSONWebKey{
+                                       {Key: s.issuerKey.Public(), Algorithm: string(jose.RS256), KeyID: ""},
+                               },
+                       })
+               case "/auth":
+                       w.WriteHeader(http.StatusInternalServerError)
+               case "/userinfo":
+                       w.WriteHeader(http.StatusInternalServerError)
+               default:
+                       w.WriteHeader(http.StatusNotFound)
+               }
+       }))
+       s.validCode = fmt.Sprintf("abcdefgh-%d", time.Now().Unix())
+
+       s.fakePeopleAPI = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
+               req.ParseForm()
+               c.Logf("fakePeopleAPI: got req: %s %s %s", req.Method, req.URL, req.Form)
+               w.Header().Set("Content-Type", "application/json")
+               switch req.URL.Path {
+               case "/v1/people/me":
+                       if f := req.Form.Get("personFields"); f != "emailAddresses,names" {
+                               w.WriteHeader(http.StatusBadRequest)
+                               break
+                       }
+                       json.NewEncoder(w).Encode(s.fakePeopleAPIResponse)
+               default:
+                       w.WriteHeader(http.StatusNotFound)
+               }
+       }))
+       s.fakePeopleAPIResponse = map[string]interface{}{}
+
+       cfg, err := config.NewLoader(nil, ctxlog.TestLogger(c)).Load()
+       s.cluster, err = cfg.GetCluster("")
+       s.cluster.Login.GoogleClientID = "test%client$id"
+       s.cluster.Login.GoogleClientSecret = "test#client/secret"
+       c.Assert(err, check.IsNil)
+
+       s.localdb = NewConn(s.cluster)
+       s.localdb.googleLoginController.issuer = s.fakeIssuer.URL
+       s.localdb.googleLoginController.peopleAPIBasePath = s.fakePeopleAPI.URL
+
+       s.railsSpy = arvadostest.NewProxy(c, s.cluster.Services.RailsAPI)
+       s.localdb.railsProxy = rpc.NewConn(s.cluster.ClusterID, s.railsSpy.URL, true, rpc.PassthroughTokenProvider)
+}
+
+func (s *LoginSuite) TearDownTest(c *check.C) {
+       s.railsSpy.Close()
+}
+
+func (s *LoginSuite) TestGoogleLogin_Start_Bogus(c *check.C) {
+       resp, err := s.localdb.Login(context.Background(), arvados.LoginOptions{})
+       c.Check(err, check.IsNil)
+       c.Check(resp.RedirectLocation, check.Equals, "")
+       c.Check(resp.HTML.String(), check.Matches, `.*missing return_to parameter.*`)
+}
+
+func (s *LoginSuite) TestGoogleLogin_Start(c *check.C) {
+       for _, remote := range []string{"", "zzzzz"} {
+               resp, err := s.localdb.Login(context.Background(), arvados.LoginOptions{Remote: remote, ReturnTo: "https://app.example.com/foo?bar"})
+               c.Check(err, check.IsNil)
+               target, err := url.Parse(resp.RedirectLocation)
+               c.Check(err, check.IsNil)
+               issuerURL, _ := url.Parse(s.fakeIssuer.URL)
+               c.Check(target.Host, check.Equals, issuerURL.Host)
+               q := target.Query()
+               c.Check(q.Get("client_id"), check.Equals, "test%client$id")
+               state := s.localdb.googleLoginController.parseOAuth2State(q.Get("state"))
+               c.Check(state.verify([]byte(s.cluster.SystemRootToken)), check.Equals, true)
+               c.Check(state.Time, check.Not(check.Equals), 0)
+               c.Check(state.Remote, check.Equals, remote)
+               c.Check(state.ReturnTo, check.Equals, "https://app.example.com/foo?bar")
+       }
+}
+
+func (s *LoginSuite) TestGoogleLogin_InvalidCode(c *check.C) {
+       state := s.startLogin(c)
+       resp, err := s.localdb.Login(context.Background(), arvados.LoginOptions{
+               Code:  "first-try-a-bogus-code",
+               State: state,
+       })
+       c.Check(err, check.IsNil)
+       c.Check(resp.RedirectLocation, check.Equals, "")
+       c.Check(resp.HTML.String(), check.Matches, `(?ms).*error in OAuth2 exchange.*cannot fetch token.*`)
+}
+
+func (s *LoginSuite) TestGoogleLogin_InvalidState(c *check.C) {
+       s.startLogin(c)
+       resp, err := s.localdb.Login(context.Background(), arvados.LoginOptions{
+               Code:  s.validCode,
+               State: "bogus-state",
+       })
+       c.Check(err, check.IsNil)
+       c.Check(resp.RedirectLocation, check.Equals, "")
+       c.Check(resp.HTML.String(), check.Matches, `(?ms).*invalid OAuth2 state.*`)
+}
+
+func (s *LoginSuite) setupPeopleAPIError(c *check.C) {
+       s.fakePeopleAPI = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
+               w.WriteHeader(http.StatusForbidden)
+               fmt.Fprintln(w, `Error 403: accessNotConfigured`)
+       }))
+       s.localdb.googleLoginController.peopleAPIBasePath = s.fakePeopleAPI.URL
+}
+
+func (s *LoginSuite) TestGoogleLogin_PeopleAPIDisabled(c *check.C) {
+       s.cluster.Login.GoogleAlternateEmailAddresses = false
+       s.authEmail = "joe.smith@primary.example.com"
+       s.setupPeopleAPIError(c)
+       state := s.startLogin(c)
+       _, err := s.localdb.Login(context.Background(), arvados.LoginOptions{
+               Code:  s.validCode,
+               State: state,
+       })
+       c.Check(err, check.IsNil)
+       authinfo := s.getCallbackAuthInfo(c)
+       c.Check(authinfo.Email, check.Equals, "joe.smith@primary.example.com")
+}
+
+func (s *LoginSuite) TestGoogleLogin_PeopleAPIError(c *check.C) {
+       s.setupPeopleAPIError(c)
+       state := s.startLogin(c)
+       resp, err := s.localdb.Login(context.Background(), arvados.LoginOptions{
+               Code:  s.validCode,
+               State: state,
+       })
+       c.Check(err, check.IsNil)
+       c.Check(resp.RedirectLocation, check.Equals, "")
+}
+
+func (s *LoginSuite) TestGoogleLogin_Success(c *check.C) {
+       state := s.startLogin(c)
+       resp, err := s.localdb.Login(context.Background(), arvados.LoginOptions{
+               Code:  s.validCode,
+               State: state,
+       })
+       c.Check(err, check.IsNil)
+       c.Check(resp.HTML.String(), check.Equals, "")
+       target, err := url.Parse(resp.RedirectLocation)
+       c.Check(err, check.IsNil)
+       c.Check(target.Host, check.Equals, "app.example.com")
+       c.Check(target.Path, check.Equals, "/foo")
+       token := target.Query().Get("api_token")
+       c.Check(token, check.Matches, `v2/zzzzz-gj3su-.{15}/.{32,50}`)
+
+       authinfo := s.getCallbackAuthInfo(c)
+       c.Check(authinfo.FirstName, check.Equals, "Fake User")
+       c.Check(authinfo.LastName, check.Equals, "Name")
+       c.Check(authinfo.Email, check.Equals, "active-user@arvados.local")
+       c.Check(authinfo.AlternateEmails, check.HasLen, 0)
+
+       // Try using the returned Arvados token.
+       c.Logf("trying an API call with new token %q", token)
+       ctx := auth.NewContext(context.Background(), &auth.Credentials{Tokens: []string{token}})
+       cl, err := s.localdb.CollectionList(ctx, arvados.ListOptions{Limit: -1})
+       c.Check(cl.ItemsAvailable, check.Not(check.Equals), 0)
+       c.Check(cl.Items, check.Not(check.HasLen), 0)
+       c.Check(err, check.IsNil)
+
+       // Might as well check that bogus tokens aren't accepted.
+       badtoken := token + "plussomeboguschars"
+       c.Logf("trying an API call with mangled token %q", badtoken)
+       ctx = auth.NewContext(context.Background(), &auth.Credentials{Tokens: []string{badtoken}})
+       cl, err = s.localdb.CollectionList(ctx, arvados.ListOptions{Limit: -1})
+       c.Check(cl.Items, check.HasLen, 0)
+       c.Check(err, check.NotNil)
+       c.Check(err, check.ErrorMatches, `.*401 Unauthorized: Not logged in.*`)
+}
+
+func (s *LoginSuite) TestGoogleLogin_RealName(c *check.C) {
+       s.authEmail = "joe.smith@primary.example.com"
+       s.fakePeopleAPIResponse = map[string]interface{}{
+               "names": []map[string]interface{}{
+                       {
+                               "metadata":   map[string]interface{}{"primary": false},
+                               "givenName":  "Joe",
+                               "familyName": "Smith",
+                       },
+                       {
+                               "metadata":   map[string]interface{}{"primary": true},
+                               "givenName":  "Joseph",
+                               "familyName": "Psmith",
+                       },
+               },
+       }
+       state := s.startLogin(c)
+       s.localdb.Login(context.Background(), arvados.LoginOptions{
+               Code:  s.validCode,
+               State: state,
+       })
+
+       authinfo := s.getCallbackAuthInfo(c)
+       c.Check(authinfo.FirstName, check.Equals, "Joseph")
+       c.Check(authinfo.LastName, check.Equals, "Psmith")
+}
+
+func (s *LoginSuite) TestGoogleLogin_OIDCRealName(c *check.C) {
+       s.authName = "Joe P. Smith"
+       s.authEmail = "joe.smith@primary.example.com"
+       state := s.startLogin(c)
+       s.localdb.Login(context.Background(), arvados.LoginOptions{
+               Code:  s.validCode,
+               State: state,
+       })
+
+       authinfo := s.getCallbackAuthInfo(c)
+       c.Check(authinfo.FirstName, check.Equals, "Joe P.")
+       c.Check(authinfo.LastName, check.Equals, "Smith")
+}
+
+// People API returns some additional email addresses.
+func (s *LoginSuite) TestGoogleLogin_AlternateEmailAddresses(c *check.C) {
+       s.authEmail = "joe.smith@primary.example.com"
+       s.fakePeopleAPIResponse = map[string]interface{}{
+               "emailAddresses": []map[string]interface{}{
+                       {
+                               "metadata": map[string]interface{}{"verified": true},
+                               "value":    "joe.smith@work.example.com",
+                       },
+                       {
+                               "value": "joe.smith@unverified.example.com", // unverified, so this one will be ignored
+                       },
+                       {
+                               "metadata": map[string]interface{}{"verified": true},
+                               "value":    "joe.smith@home.example.com",
+                       },
+               },
+       }
+       state := s.startLogin(c)
+       s.localdb.Login(context.Background(), arvados.LoginOptions{
+               Code:  s.validCode,
+               State: state,
+       })
+
+       authinfo := s.getCallbackAuthInfo(c)
+       c.Check(authinfo.Email, check.Equals, "joe.smith@primary.example.com")
+       c.Check(authinfo.AlternateEmails, check.DeepEquals, []string{"joe.smith@home.example.com", "joe.smith@work.example.com"})
+}
+
+// Primary address is not the one initially returned by oidc.
+func (s *LoginSuite) TestGoogleLogin_AlternateEmailAddresses_Primary(c *check.C) {
+       s.authEmail = "joe.smith@alternate.example.com"
+       s.fakePeopleAPIResponse = map[string]interface{}{
+               "emailAddresses": []map[string]interface{}{
+                       {
+                               "metadata": map[string]interface{}{"verified": true, "primary": true},
+                               "value":    "joe.smith@primary.example.com",
+                       },
+                       {
+                               "metadata": map[string]interface{}{"verified": true},
+                               "value":    "joe.smith@alternate.example.com",
+                       },
+               },
+       }
+       state := s.startLogin(c)
+       s.localdb.Login(context.Background(), arvados.LoginOptions{
+               Code:  s.validCode,
+               State: state,
+       })
+       authinfo := s.getCallbackAuthInfo(c)
+       c.Check(authinfo.Email, check.Equals, "joe.smith@primary.example.com")
+       c.Check(authinfo.AlternateEmails, check.DeepEquals, []string{"joe.smith@alternate.example.com"})
+}
+
+func (s *LoginSuite) TestGoogleLogin_NoPrimaryEmailAddress(c *check.C) {
+       s.authEmail = "joe.smith@unverified.example.com"
+       s.authEmailVerified = false
+       s.fakePeopleAPIResponse = map[string]interface{}{
+               "emailAddresses": []map[string]interface{}{
+                       {
+                               "metadata": map[string]interface{}{"verified": true},
+                               "value":    "joe.smith@work.example.com",
+                       },
+                       {
+                               "metadata": map[string]interface{}{"verified": true},
+                               "value":    "joe.smith@home.example.com",
+                       },
+               },
+       }
+       state := s.startLogin(c)
+       s.localdb.Login(context.Background(), arvados.LoginOptions{
+               Code:  s.validCode,
+               State: state,
+       })
+
+       authinfo := s.getCallbackAuthInfo(c)
+       c.Check(authinfo.Email, check.Equals, "joe.smith@work.example.com") // first verified email in People response
+       c.Check(authinfo.AlternateEmails, check.DeepEquals, []string{"joe.smith@home.example.com"})
+}
+
+func (s *LoginSuite) getCallbackAuthInfo(c *check.C) (authinfo rpc.UserSessionAuthInfo) {
+       for _, dump := range s.railsSpy.RequestDumps {
+               c.Logf("spied request: %q", dump)
+               split := bytes.Split(dump, []byte("\r\n\r\n"))
+               c.Assert(split, check.HasLen, 2)
+               hdr, body := string(split[0]), string(split[1])
+               if strings.Contains(hdr, "POST /auth/controller/callback") {
+                       vs, err := url.ParseQuery(body)
+                       c.Check(json.Unmarshal([]byte(vs.Get("auth_info")), &authinfo), check.IsNil)
+                       c.Check(err, check.IsNil)
+                       sort.Strings(authinfo.AlternateEmails)
+                       return
+               }
+       }
+       c.Error("callback not found")
+       return
+}
+
+func (s *LoginSuite) startLogin(c *check.C) (state string) {
+       // Initiate login, but instead of following the redirect to
+       // the provider, just grab state from the redirect URL.
+       resp, err := s.localdb.Login(context.Background(), arvados.LoginOptions{ReturnTo: "https://app.example.com/foo?bar"})
+       c.Check(err, check.IsNil)
+       target, err := url.Parse(resp.RedirectLocation)
+       c.Check(err, check.IsNil)
+       state = target.Query().Get("state")
+       c.Check(state, check.Not(check.Equals), "")
+       return
+}
+
+func (s *LoginSuite) fakeToken(c *check.C, payload []byte) string {
+       signer, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.RS256, Key: s.issuerKey}, nil)
+       if err != nil {
+               c.Error(err)
+       }
+       object, err := signer.Sign(payload)
+       if err != nil {
+               c.Error(err)
+       }
+       t, err := object.CompactSerialize()
+       if err != nil {
+               c.Error(err)
+       }
+       c.Logf("fakeToken(%q) == %q", payload, t)
+       return t
+}
index 576e603eedd758f8ff53f2556e1161b6957b0691..ba1c323ba67a377e7eabc40be6eb5fa4762ef381 100644 (file)
@@ -7,15 +7,12 @@
 package railsproxy
 
 import (
-       "context"
-       "errors"
        "fmt"
        "net/url"
        "strings"
 
        "git.curoverse.com/arvados.git/lib/controller/rpc"
        "git.curoverse.com/arvados.git/sdk/go/arvados"
-       "git.curoverse.com/arvados.git/sdk/go/auth"
 )
 
 // For now, FindRailsAPI always uses the rails API running on this
@@ -40,13 +37,5 @@ func NewConn(cluster *arvados.Cluster) *rpc.Conn {
        if err != nil {
                panic(err)
        }
-       return rpc.NewConn(cluster.ClusterID, url, insecure, provideIncomingToken)
-}
-
-func provideIncomingToken(ctx context.Context) ([]string, error) {
-       incoming, ok := auth.FromContext(ctx)
-       if !ok {
-               return nil, errors.New("no token provided")
-       }
-       return incoming.Tokens, nil
+       return rpc.NewConn(cluster.ClusterID, url, insecure, rpc.PassthroughTokenProvider)
 }
index aa3af1f64c45194c5d5b3cc2e1996ed212941e5e..e3ec37a6ea842ac36e6b8d766cf91cf0210f05f9 100644 (file)
@@ -52,9 +52,16 @@ func applySelectParam(selectParam []string, orig map[string]interface{}) map[str
        return selected
 }
 
-func (rtr *router) sendResponse(w http.ResponseWriter, resp interface{}, opts responseOptions) {
+func (rtr *router) sendResponse(w http.ResponseWriter, req *http.Request, resp interface{}, opts responseOptions) {
        var tmp map[string]interface{}
 
+       if resp, ok := resp.(http.Handler); ok {
+               // resp knows how to write its own http response
+               // header and body.
+               resp.ServeHTTP(w, req)
+               return
+       }
+
        err := rtr.transcode(resp, &tmp)
        if err != nil {
                rtr.sendError(w, err)
@@ -121,7 +128,9 @@ func (rtr *router) sendResponse(w http.ResponseWriter, resp interface{}, opts re
                }
        }
        w.Header().Set("Content-Type", "application/json")
-       json.NewEncoder(w).Encode(tmp)
+       enc := json.NewEncoder(w)
+       enc.SetEscapeHTML(false)
+       enc.Encode(tmp)
 }
 
 func (rtr *router) sendError(w http.ResponseWriter, err error) {
index 5d5602df523b672d6e8f6d84346ed5255a20ae76..d3bdce527211e1b26245a820dcbc9cd174f3dc62 100644 (file)
@@ -47,6 +47,13 @@ func (rtr *router) addRoutes() {
                                return rtr.fed.ConfigGet(ctx)
                        },
                },
+               {
+                       arvados.EndpointLogin,
+                       func() interface{} { return &arvados.LoginOptions{} },
+                       func(ctx context.Context, opts interface{}) (interface{}, error) {
+                               return rtr.fed.Login(ctx, *opts.(*arvados.LoginOptions))
+                       },
+               },
                {
                        arvados.EndpointCollectionCreate,
                        func() interface{} { return &arvados.CreateOptions{} },
@@ -263,7 +270,7 @@ func (rtr *router) addRoute(endpoint arvados.APIEndpoint, defaultOpts func() int
                        rtr.sendError(w, err)
                        return
                }
-               rtr.sendResponse(w, resp, respOpts)
+               rtr.sendResponse(w, req, resp, respOpts)
        })
 }
 
index 1028da829fbdb0361fc5b041e98fba5e30c81c6c..7d7cb486f4f742d57411751254cf7b8dd6ab22ad 100644 (file)
@@ -32,6 +32,7 @@ func PassthroughTokenProvider(ctx context.Context) ([]string, error) {
 }
 
 type Conn struct {
+       SendHeader    http.Header
        clusterID     string
        httpClient    http.Client
        baseURL       url.URL
@@ -61,8 +62,11 @@ func NewConn(clusterID string, url *url.URL, insecure bool, tp TokenProvider) *C
                }
        }
        return &Conn{
-               clusterID:     clusterID,
-               httpClient:    http.Client{Transport: transport},
+               clusterID: clusterID,
+               httpClient: http.Client{
+                       CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse },
+                       Transport:     transport,
+               },
                baseURL:       *url,
                tokenProvider: tp,
        }
@@ -70,9 +74,10 @@ func NewConn(clusterID string, url *url.URL, insecure bool, tp TokenProvider) *C
 
 func (conn *Conn) requestAndDecode(ctx context.Context, dst interface{}, ep arvados.APIEndpoint, body io.Reader, opts interface{}) error {
        aClient := arvados.Client{
-               Client:  &conn.httpClient,
-               Scheme:  conn.baseURL.Scheme,
-               APIHost: conn.baseURL.Host,
+               Client:     &conn.httpClient,
+               Scheme:     conn.baseURL.Scheme,
+               APIHost:    conn.baseURL.Host,
+               SendHeader: conn.SendHeader,
        }
        tokens, err := conn.tokenProvider(ctx)
        if err != nil {
@@ -121,6 +126,10 @@ func (conn *Conn) requestAndDecode(ctx context.Context, dst interface{}, ep arva
        return aClient.RequestAndDecodeContext(ctx, dst, ep.Method, path, body, params)
 }
 
+func (conn *Conn) BaseURL() url.URL {
+       return conn.baseURL
+}
+
 func (conn *Conn) ConfigGet(ctx context.Context) (json.RawMessage, error) {
        ep := arvados.EndpointConfigGet
        var resp json.RawMessage
@@ -128,6 +137,30 @@ func (conn *Conn) ConfigGet(ctx context.Context) (json.RawMessage, error) {
        return resp, err
 }
 
+func (conn *Conn) Login(ctx context.Context, options arvados.LoginOptions) (arvados.LoginResponse, error) {
+       ep := arvados.EndpointLogin
+       var resp arvados.LoginResponse
+       err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
+       resp.RedirectLocation = conn.relativeToBaseURL(resp.RedirectLocation)
+       return resp, err
+}
+
+// If the given location is a valid URL and its origin is the same as
+// conn.baseURL, return it as a relative URL. Otherwise, return it
+// unmodified.
+func (conn *Conn) relativeToBaseURL(location string) string {
+       u, err := url.Parse(location)
+       if err == nil && u.Scheme == conn.baseURL.Scheme && strings.ToLower(u.Host) == strings.ToLower(conn.baseURL.Host) {
+               u.Opaque = ""
+               u.Scheme = ""
+               u.User = nil
+               u.Host = ""
+               return u.String()
+       } else {
+               return location
+       }
+}
+
 func (conn *Conn) CollectionCreate(ctx context.Context, options arvados.CreateOptions) (arvados.Collection, error) {
        ep := arvados.EndpointCollectionCreate
        var resp arvados.Collection
@@ -281,3 +314,22 @@ func (conn *Conn) APIClientAuthorizationCurrent(ctx context.Context, options arv
        err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
        return resp, err
 }
+
+type UserSessionAuthInfo struct {
+       Email           string   `json:"email"`
+       AlternateEmails []string `json:"alternate_emails"`
+       FirstName       string   `json:"first_name"`
+       LastName        string   `json:"last_name"`
+}
+
+type UserSessionCreateOptions struct {
+       AuthInfo UserSessionAuthInfo `json:"auth_info"`
+       ReturnTo string              `json:"return_to"`
+}
+
+func (conn *Conn) UserSessionCreate(ctx context.Context, options UserSessionCreateOptions) (arvados.LoginResponse, error) {
+       ep := arvados.APIEndpoint{Method: "POST", Path: "auth/controller/callback"}
+       var resp arvados.LoginResponse
+       err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
+       return resp, err
+}
index 80e90a043f23e66c85d2c49f49280de84a061ee8..7a5403e930edb3ec197191d6319487a7ae2f5eda 100644 (file)
@@ -36,10 +36,21 @@ func (s *RPCSuite) SetUpTest(c *check.C) {
        ctx := ctxlog.Context(context.Background(), ctxlog.TestLogger(c))
        s.ctx = context.WithValue(ctx, contextKeyTestTokens, []string{arvadostest.ActiveToken})
        s.conn = NewConn("zzzzz", &url.URL{Scheme: "https", Host: os.Getenv("ARVADOS_TEST_API_HOST")}, true, func(ctx context.Context) ([]string, error) {
-               return ctx.Value(contextKeyTestTokens).([]string), nil
+               tokens, _ := ctx.Value(contextKeyTestTokens).([]string)
+               return tokens, nil
        })
 }
 
+func (s *RPCSuite) TestLogin(c *check.C) {
+       s.ctx = context.Background()
+       opts := arvados.LoginOptions{
+               ReturnTo: "https://foo.example.com/bar",
+       }
+       resp, err := s.conn.Login(s.ctx, opts)
+       c.Check(err, check.IsNil)
+       c.Check(resp.RedirectLocation, check.Equals, "/auth/joshid?return_to="+url.QueryEscape(","+opts.ReturnTo))
+}
+
 func (s *RPCSuite) TestCollectionCreate(c *check.C) {
        coll, err := s.conn.CollectionCreate(s.ctx, arvados.CreateOptions{Attrs: map[string]interface{}{
                "owner_uuid":         arvadostest.ActiveUserUUID,
index 772f8da9719ae874d3f392782fb6388d3e74a488..5531cf71d344cbb795caaa2cac670d7a3ff88ca1 100644 (file)
@@ -18,6 +18,7 @@ type APIEndpoint struct {
 
 var (
        EndpointConfigGet                     = APIEndpoint{"GET", "arvados/v1/config", ""}
+       EndpointLogin                         = APIEndpoint{"GET", "login", ""}
        EndpointCollectionCreate              = APIEndpoint{"POST", "arvados/v1/collections", "collection"}
        EndpointCollectionUpdate              = APIEndpoint{"PATCH", "arvados/v1/collections/:uuid", "collection"}
        EndpointCollectionGet                 = APIEndpoint{"GET", "arvados/v1/collections/:uuid", ""}
@@ -83,8 +84,16 @@ type DeleteOptions struct {
        UUID string `json:"uuid"`
 }
 
+type LoginOptions struct {
+       ReturnTo string `json:"return_to"`        // On success, redirect to this target with api_token=xxx query param
+       Remote   string `json:"remote,omitempty"` // Salt token for remote Cluster ID
+       Code     string `json:"code,omitempty"`   // OAuth2 callback code
+       State    string `json:"state,omitempty"`  // OAuth2 callback state
+}
+
 type API interface {
        ConfigGet(ctx context.Context) (json.RawMessage, error)
+       Login(ctx context.Context, options LoginOptions) (LoginResponse, error)
        CollectionCreate(ctx context.Context, options CreateOptions) (Collection, error)
        CollectionUpdate(ctx context.Context, options UpdateOptions) (Collection, error)
        CollectionGet(ctx context.Context, options GetOptions) (Collection, error)
index a5815987b192a86c9ee646205bcc9ea0f7986dcc..8545cb969d92f8fbc716175d5c9820f47ed6680a 100644 (file)
@@ -54,6 +54,9 @@ type Client struct {
        // arvadosclient.ArvadosClient.)
        KeepServiceURIs []string `json:",omitempty"`
 
+       // HTTP headers to add/override in outgoing requests.
+       SendHeader http.Header
+
        dd *DiscoveryDocument
 
        ctx context.Context
@@ -144,9 +147,22 @@ func (c *Client) Do(req *http.Request) (*http.Response, error) {
        return c.httpClient().Do(req)
 }
 
+func isRedirectStatus(code int) bool {
+       switch code {
+       case http.StatusMovedPermanently, http.StatusFound, http.StatusSeeOther, http.StatusTemporaryRedirect, http.StatusPermanentRedirect:
+               return true
+       default:
+               return false
+       }
+}
+
 // DoAndDecode performs req and unmarshals the response (which must be
 // JSON) into dst. Use this instead of RequestAndDecode if you need
 // more control of the http.Request object.
+//
+// If the response status indicates an HTTP redirect, the Location
+// header value is unmarshalled to dst as a RedirectLocation
+// key/field.
 func (c *Client) DoAndDecode(dst interface{}, req *http.Request) error {
        resp, err := c.Do(req)
        if err != nil {
@@ -157,13 +173,28 @@ func (c *Client) DoAndDecode(dst interface{}, req *http.Request) error {
        if err != nil {
                return err
        }
-       if resp.StatusCode != 200 {
-               return newTransactionError(req, resp, buf)
-       }
-       if dst == nil {
+       switch {
+       case resp.StatusCode == http.StatusOK && dst == nil:
                return nil
+       case resp.StatusCode == http.StatusOK:
+               return json.Unmarshal(buf, dst)
+
+       // If the caller uses a client with a custom CheckRedirect
+       // func, Do() might return the 3xx response instead of
+       // following it.
+       case isRedirectStatus(resp.StatusCode) && dst == nil:
+               return nil
+       case isRedirectStatus(resp.StatusCode):
+               // Copy the redirect target URL to dst.RedirectLocation.
+               buf, err := json.Marshal(map[string]string{"RedirectLocation": resp.Header.Get("Location")})
+               if err != nil {
+                       return err
+               }
+               return json.Unmarshal(buf, dst)
+
+       default:
+               return newTransactionError(req, resp, buf)
        }
-       return json.Unmarshal(buf, dst)
 }
 
 // Convert an arbitrary struct to url.Values. For example,
@@ -268,6 +299,9 @@ func (c *Client) RequestAndDecodeContext(ctx context.Context, dst interface{}, m
        }
        req = req.WithContext(ctx)
        req.Header.Set("Content-type", "application/x-www-form-urlencoded")
+       for k, v := range c.SendHeader {
+               req.Header[k] = v
+       }
        return c.DoAndDecode(dst, req)
 }
 
index 952fde5d1cb9d01c9a398b76fc86c5cd3ef02b1d..805efb7db287d61a9049cf0abf69ac61236989d4 100644 (file)
@@ -132,10 +132,13 @@ type Cluster struct {
                Repositories string
        }
        Login struct {
-               ProviderAppSecret  string
-               ProviderAppID      string
-               LoginCluster       string
-               RemoteTokenRefresh Duration
+               GoogleClientID                string
+               GoogleClientSecret            string
+               GoogleAlternateEmailAddresses bool
+               ProviderAppID                 string
+               ProviderAppSecret             string
+               LoginCluster                  string
+               RemoteTokenRefresh            Duration
        }
        Mail struct {
                MailchimpAPIKey                string
diff --git a/sdk/go/arvados/login.go b/sdk/go/arvados/login.go
new file mode 100644 (file)
index 0000000..7107ac5
--- /dev/null
@@ -0,0 +1,26 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: Apache-2.0
+
+package arvados
+
+import (
+       "bytes"
+       "net/http"
+)
+
+type LoginResponse struct {
+       RedirectLocation string
+       HTML             bytes.Buffer
+}
+
+func (resp LoginResponse) ServeHTTP(w http.ResponseWriter, req *http.Request) {
+       w.Header().Set("Cache-Control", "no-store")
+       if resp.RedirectLocation != "" {
+               w.Header().Set("Location", resp.RedirectLocation)
+               w.WriteHeader(http.StatusFound)
+       } else {
+               w.Header().Set("Content-Type", "text/html")
+               w.Write(resp.HTML.Bytes())
+       }
+}
index 850bd0639dcaa856c5b6dfa69b09309a1ead1de5..24e9f190865b0456a8be431449239e2ec3aba6db 100644 (file)
@@ -8,6 +8,7 @@ import (
        "context"
        "encoding/json"
        "errors"
+       "net/url"
        "reflect"
        "runtime"
        "sync"
@@ -24,10 +25,18 @@ type APIStub struct {
        mtx   sync.Mutex
 }
 
+// BaseURL implements federation.backend
+func (as *APIStub) BaseURL() url.URL {
+       return url.URL{Scheme: "https", Host: "apistub.example.com"}
+}
 func (as *APIStub) ConfigGet(ctx context.Context) (json.RawMessage, error) {
        as.appendCall(as.ConfigGet, ctx, nil)
        return nil, as.Error
 }
+func (as *APIStub) Login(ctx context.Context, options arvados.LoginOptions) (arvados.LoginResponse, error) {
+       as.appendCall(as.Login, ctx, options)
+       return arvados.LoginResponse{}, as.Error
+}
 func (as *APIStub) CollectionCreate(ctx context.Context, options arvados.CreateOptions) (arvados.Collection, error) {
        as.appendCall(as.CollectionCreate, ctx, options)
        return arvados.Collection{}, as.Error
diff --git a/sdk/go/arvadostest/proxy.go b/sdk/go/arvadostest/proxy.go
new file mode 100644 (file)
index 0000000..015061a
--- /dev/null
@@ -0,0 +1,72 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: Apache-2.0
+
+package arvadostest
+
+import (
+       "crypto/tls"
+       "net"
+       "net/http"
+       "net/http/httptest"
+       "net/http/httputil"
+       "net/url"
+       "time"
+
+       "git.curoverse.com/arvados.git/sdk/go/arvados"
+       "gopkg.in/check.v1"
+)
+
+type Proxy struct {
+       *httptest.Server
+
+       // URL where the proxy is listening. Same as Server.URL, but
+       // with parsing already done for you.
+       URL *url.URL
+
+       // A dump of each request that has been proxied.
+       RequestDumps [][]byte
+}
+
+// NewProxy returns a new Proxy that saves a dump of each reqeust
+// before forwarding to the indicated service.
+func NewProxy(c *check.C, svc arvados.Service) *Proxy {
+       var target url.URL
+       c.Assert(svc.InternalURLs, check.HasLen, 1)
+       for u := range svc.InternalURLs {
+               target = url.URL(u)
+               break
+       }
+       rp := httputil.NewSingleHostReverseProxy(&target)
+       rp.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) {
+               dump, _ := httputil.DumpRequest(r, false)
+               c.Logf("arvadostest.Proxy ErrorHandler(%s): %s\n%s", r.URL, err, dump)
+               http.Error(w, err.Error(), http.StatusBadGateway)
+       }
+       rp.Transport = &http.Transport{
+               DialContext: (&net.Dialer{
+                       Timeout:   30 * time.Second,
+                       KeepAlive: 30 * time.Second,
+                       DualStack: true,
+               }).DialContext,
+               MaxIdleConns:          100,
+               IdleConnTimeout:       90 * time.Second,
+               TLSHandshakeTimeout:   10 * time.Second,
+               ExpectContinueTimeout: 1 * time.Second,
+               TLSClientConfig:       &tls.Config{InsecureSkipVerify: true},
+       }
+       srv := httptest.NewServer(rp)
+       u, err := url.Parse(srv.URL)
+       c.Assert(err, check.IsNil)
+       proxy := &Proxy{
+               Server: srv,
+               URL:    u,
+       }
+       rp.Director = func(r *http.Request) {
+               dump, _ := httputil.DumpRequest(r, true)
+               proxy.RequestDumps = append(proxy.RequestDumps, dump)
+               r.URL.Scheme = target.Scheme
+               r.URL.Host = target.Host
+       }
+       return proxy
+}
index 7e8a2979befaee7af09007bd51e3d0fbc878d312..a8d1a08cb09643262bf657498aefc53b727f168c 100644 (file)
@@ -34,9 +34,7 @@ abstract class BaseApiClient {
 
     BaseApiClient(ConfigProvider config) {
         this.config = config;
-        client = OkHttpClientFactory.builder()
-                .build()
-                .create(config.isApiHostInsecure());
+        this.client = OkHttpClientFactory.INSTANCE.create(config.isApiHostInsecure());
     }
 
     Request.Builder getRequestBuilder() {
index 0e95e661e7fccd1b24f433e2ace298effa9064c1..f9041c9281a0f4026e6136741dba7da04901fc05 100644 (file)
@@ -7,6 +7,7 @@
 
 package org.arvados.client.api.client.factory;
 
+import com.google.common.base.Suppliers;
 import okhttp3.OkHttpClient;
 import org.arvados.client.exception.ArvadosClientException;
 import org.slf4j.Logger;
@@ -19,31 +20,60 @@ import java.security.KeyManagementException;
 import java.security.NoSuchAlgorithmException;
 import java.security.SecureRandom;
 import java.security.cert.X509Certificate;
+import java.util.function.Supplier;
 
-public class OkHttpClientFactory {
-
+/**
+ * {@link OkHttpClient} instance factory that builds and configures client instances sharing
+ * the common resource pool: this is the recommended approach to optimize resource usage.
+ */
+public final class OkHttpClientFactory {
+    public static final OkHttpClientFactory INSTANCE = new OkHttpClientFactory();
     private final Logger log = org.slf4j.LoggerFactory.getLogger(OkHttpClientFactory.class);
+    private final OkHttpClient clientSecure = new OkHttpClient();
+    private final Supplier<OkHttpClient> clientUnsecure =
+            Suppliers.memoize(this::getDefaultClientAcceptingAllCertificates);
+
+    private OkHttpClientFactory() { /* singleton */}
 
-    OkHttpClientFactory() {
+    public OkHttpClient create(boolean apiHostInsecure) {
+        return apiHostInsecure ? getDefaultUnsecureClient() : getDefaultClient();
     }
 
-    public static OkHttpClientFactoryBuilder builder() {
-        return new OkHttpClientFactoryBuilder();
+    /**
+     * @return default secure {@link OkHttpClient} with shared resource pool.
+     */
+    public OkHttpClient getDefaultClient() {
+        return clientSecure;
     }
 
-    public OkHttpClient create(boolean apiHostInsecure) {
-        OkHttpClient.Builder builder = new OkHttpClient.Builder();
-        if (apiHostInsecure) {
-            trustAllCertificates(builder);
-        }
-        return builder.build();
+    /**
+     * @return default {@link OkHttpClient} with shared resource pool
+     * that will accept all SSL certificates by default.
+     */
+    public OkHttpClient getDefaultUnsecureClient() {
+        return clientUnsecure.get();
+    }
+
+    /**
+     * @return default {@link OkHttpClient.Builder} with shared resource pool.
+     */
+    public OkHttpClient.Builder getDefaultClientBuilder() {
+        return clientSecure.newBuilder();
+    }
+
+    /**
+     * @return default {@link OkHttpClient.Builder} with shared resource pool
+     * that is preconfigured to accept all SSL certificates.
+     */
+    public OkHttpClient.Builder getDefaultUnsecureClientBuilder() {
+        return clientUnsecure.get().newBuilder();
     }
 
-    private void trustAllCertificates(OkHttpClient.Builder builder) {
+    private OkHttpClient getDefaultClientAcceptingAllCertificates() {
         log.warn("Creating unsafe OkHttpClient. All SSL certificates will be accepted.");
         try {
             // Create a trust manager that does not validate certificate chains
-            final TrustManager[] trustAllCerts = new TrustManager[] { createX509TrustManager() };
+            final TrustManager[] trustAllCerts = {createX509TrustManager()};
 
             // Install the all-trusting trust manager
             SSLContext sslContext = SSLContext.getInstance("SSL");
@@ -51,8 +81,11 @@ public class OkHttpClientFactory {
             // Create an ssl socket factory with our all-trusting manager
             final SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();
 
+            // Create the OkHttpClient.Builder with shared resource pool
+            final OkHttpClient.Builder builder = clientSecure.newBuilder();
             builder.sslSocketFactory(sslSocketFactory, (X509TrustManager) trustAllCerts[0]);
             builder.hostnameVerifier((hostname, session) -> true);
+            return builder.build();
         } catch (NoSuchAlgorithmException | KeyManagementException e) {
             throw new ArvadosClientException("Error establishing SSL context", e);
         }
@@ -60,30 +93,19 @@ public class OkHttpClientFactory {
 
     private static X509TrustManager createX509TrustManager() {
         return new X509TrustManager() {
-            
+
             @Override
-            public void checkClientTrusted(X509Certificate[] chain, String authType) {}
+            public void checkClientTrusted(X509Certificate[] chain, String authType) {
+            }
 
             @Override
-            public void checkServerTrusted(X509Certificate[] chain, String authType) {}
+            public void checkServerTrusted(X509Certificate[] chain, String authType) {
+            }
 
             @Override
             public X509Certificate[] getAcceptedIssuers() {
-                return new X509Certificate[] {};
+                return new X509Certificate[]{};
             }
         };
     }
-
-    public static class OkHttpClientFactoryBuilder {
-        OkHttpClientFactoryBuilder() {
-        }
-
-        public OkHttpClientFactory build() {
-            return new OkHttpClientFactory();
-        }
-
-        public String toString() {
-            return "OkHttpClientFactory.OkHttpClientFactoryBuilder()";
-        }
-    }
 }
index 858edf598b41e558951c6fd0c01ef02cad5eef3c..571cb2590906f9d041a342dbf26d95724184e3b0 100644 (file)
@@ -276,6 +276,18 @@ public class ArvadosFacade {
         return collectionsApiClient.list(listArgument);
     }
 
+    /**
+     * Gets project details by uuid.
+     *
+     * @param projectUuid uuid of project
+     * @return Group object containing information about project
+     */
+    public Group getProjectByUuid(String projectUuid) {
+        Group project = groupsApiClient.get(projectUuid);
+        log.debug("Retrieved " + project.getName() + " with UUID: " + project.getUuid());
+        return project;
+    }
+
     /**
      * Creates new project that will be a subproject of "home" for current user.
      *
index f7e18132941715a4340684e23183054730f3646c..f485d3bb02aff3e3faca87c77ef8af1f42c5e06d 100644 (file)
@@ -32,7 +32,7 @@ public class OkHttpClientFactoryTest extends ArvadosClientMockedWebServerTest {
     public void secureOkHttpClientIsCreated() throws Exception {
 
         // given
-        OkHttpClientFactory factory = OkHttpClientFactory.builder().build();
+        OkHttpClientFactory factory = OkHttpClientFactory.INSTANCE;
         // * configure HTTPS server
         SSLSocketFactory sf = getSSLSocketFactoryWithSelfSignedCertificate();
         server.useHttps(sf, false);
@@ -50,7 +50,7 @@ public class OkHttpClientFactoryTest extends ArvadosClientMockedWebServerTest {
     @Test
     public void insecureOkHttpClientIsCreated() throws Exception {
         // given
-        OkHttpClientFactory factory = OkHttpClientFactory.builder().build();
+        OkHttpClientFactory factory = OkHttpClientFactory.INSTANCE;
         // * configure HTTPS server
         SSLSocketFactory sf = getSSLSocketFactoryWithSelfSignedCertificate();
         server.useHttps(sf, false);
index b618a321e5d7578665141df4876a98a5bf872d59..d6045a5dcbf35a3c786bb6db5105d49e9636cc39 100644 (file)
@@ -14,7 +14,7 @@ class DatabaseController < ApplicationController
     # use @example.com email addresses when creating user records, so
     # we can tell they're not valuable.
     user_uuids = User.
-      where('email is null or email not like ?', '%@example.com').
+      where('email is null or (email not like ? and email not like ?)', '%@example.com', '%.example.com').
       collect(&:uuid)
     fixture_uuids =
       YAML::load_file(File.expand_path('../../../test/fixtures/users.yml',
index 4364229b77284e9dbaab975c626ec9e5e52c3e33..0a03399d1f0607b5412ef6b45a47a4ac230b7e8e 100644 (file)
@@ -17,10 +17,21 @@ class UserSessionsController < ApplicationController
       raise "Local login disabled when LoginCluster is set"
     end
 
-    omniauth = request.env['omniauth.auth']
+    if params[:provider] == 'controller'
+      if request.headers['Authorization'] != 'Bearer ' + Rails.configuration.SystemRootToken
+        return send_error('Invalid authorization header', status: 401)
+      end
+      # arvados-controller verified the user and is passing auth_info
+      # in request params.
+      authinfo = SafeJSON.load(params[:auth_info])
+    else
+      # omniauth middleware verified the user and is passing auth_info
+      # in request.env.
+      authinfo = request.env['omniauth.auth']['info'].with_indifferent_access
+    end
 
     begin
-      user = User.register omniauth['info']
+      user = User.register(authinfo)
     rescue => e
       Rails.logger.warn e
       return redirect_to login_failure_url
@@ -45,8 +56,6 @@ class UserSessionsController < ApplicationController
 
     user.save or raise Exception.new(user.errors.messages)
 
-    omniauth.delete('extra')
-
     # Give the authenticated user a cookie for direct API access
     session[:user_id] = user.id
     session[:api_client_uuid] = nil
index 1f95d78c0c2446201c79e47f9200e3ae8123d5a4..8ed693f820d5eac0eff9389ac851166e800d6516 100644 (file)
@@ -13,4 +13,25 @@ class ApiClient < ArvadosModel
     t.add :url_prefix
     t.add :is_trusted
   end
+
+  def is_trusted
+    norm(self.url_prefix) == norm(Rails.configuration.Services.Workbench1.ExternalURL) ||
+      norm(self.url_prefix) == norm(Rails.configuration.Services.Workbench2.ExternalURL) ||
+      super
+  end
+
+  protected
+
+  def norm url
+    # normalize URL for comparison
+    url = URI(url)
+    if url.scheme == "https"
+      url.port == "443"
+    end
+    if url.scheme == "http"
+      url.port == "80"
+    end
+    url.path = "/"
+    url
+  end
 end
index 6a369303467989ccdc10061718bbedea1c4903c9..8a7c71f00a53f9874c26039f6a660027880b13f0 100644 (file)
@@ -433,8 +433,6 @@ class User < ArvadosModel
     #   alternate_emails
     #   identity_url
 
-    info = info.with_indifferent_access
-
     primary_user = nil
 
     # local database
index 9a4270ad9df4384d88c97cf100ae83790a1a35ba..f211ec9e0cde5c67160bda1bde97e20cdb7861a8 100644 (file)
@@ -76,6 +76,11 @@ module Server
 
     config.action_dispatch.perform_deep_munge = false
 
+    # force_ssl's redirect-to-https feature doesn't work when the
+    # client supplies a port number, and prevents arvados-controller
+    # from connecting to Rails internally via plain http.
+    config.ssl_options = {redirect: false}
+
     I18n.enforce_available_locales = false
 
     # Before using the filesystem backend for Rails.cache, check
index 5546e8e406de5ec5c3c44e9ab889786391bcd4c1..f82f6e5f371490c070e8b13486208a349b28047a 100644 (file)
@@ -85,6 +85,7 @@ end
 arvcfg = ConfigLoader.new
 arvcfg.declare_config "ClusterID", NonemptyString, :uuid_prefix
 arvcfg.declare_config "ManagementToken", String, :ManagementToken
+arvcfg.declare_config "SystemRootToken", String
 arvcfg.declare_config "Git.Repositories", String, :git_repositories_dir
 arvcfg.declare_config "API.DisabledAPIs", Hash, :disable_api_methods, ->(cfg, k, v) { arrayToHash cfg, "API.DisabledAPIs", v }
 arvcfg.declare_config "API.MaxRequestSize", Integer, :max_request_size
@@ -105,8 +106,8 @@ arvcfg.declare_config "Users.EmailSubjectPrefix", String, :email_subject_prefix
 arvcfg.declare_config "Users.UserNotifierEmailFrom", String, :user_notifier_email_from
 arvcfg.declare_config "Users.NewUserNotificationRecipients", Hash, :new_user_notification_recipients, ->(cfg, k, v) { arrayToHash cfg, "Users.NewUserNotificationRecipients", v }
 arvcfg.declare_config "Users.NewInactiveUserNotificationRecipients", Hash, :new_inactive_user_notification_recipients, method(:arrayToHash)
-arvcfg.declare_config "Login.ProviderAppSecret", NonemptyString, :sso_app_secret
-arvcfg.declare_config "Login.ProviderAppID", NonemptyString, :sso_app_id
+arvcfg.declare_config "Login.ProviderAppSecret", String, :sso_app_secret
+arvcfg.declare_config "Login.ProviderAppID", String, :sso_app_id
 arvcfg.declare_config "Login.LoginCluster", String
 arvcfg.declare_config "Login.RemoteTokenRefresh", ActiveSupport::Duration
 arvcfg.declare_config "TLS.Insecure", Boolean, :sso_insecure
index d96ccb0903fc72026297a412ac474a8ac895af04..fc9475692a5933c2ed01f77e7871f4fd3942d7ec 100644 (file)
@@ -64,4 +64,23 @@ class UserSessionsControllerTest < ActionController::TestCase
     assert_nil assigns(:api_client)
   end
 
+  test "controller cannot create session without SystemRootToken" do
+    get :create, params: {provider: 'controller', auth_info: {email: "foo@bar.com"}, return_to: ',https://app.example'}
+    assert_response 401
+  end
+
+  test "controller cannot create session with wrong SystemRootToken" do
+    @request.headers['Authorization'] = 'Bearer blah'
+    get :create, params: {provider: 'controller', auth_info: {email: "foo@bar.com"}, return_to: ',https://app.example'}
+    assert_response 401
+  end
+
+  test "controller can create session using SystemRootToken" do
+    @request.headers['Authorization'] = 'Bearer '+Rails.configuration.SystemRootToken
+    get :create, params: {provider: 'controller', auth_info: {email: "foo@bar.com"}, return_to: ',https://app.example'}
+    assert_response :redirect
+    api_client_auth = assigns(:api_client_auth)
+    assert_not_nil api_client_auth
+    assert_includes(@response.redirect_url, 'api_token='+api_client_auth.token)
+  end
 end
index fc7d1ee2f429ffa30f885016d147b089889daf7b..df082c27fd8c35f7a8d1011bcd3faeba3d4bd4d8 100644 (file)
@@ -5,7 +5,27 @@
 require 'test_helper'
 
 class ApiClientTest < ActiveSupport::TestCase
-  # test "the truth" do
-  #   assert true
-  # end
+  include CurrentApiClient
+
+  test "configured workbench is trusted" do
+    Rails.configuration.Services.Workbench1.ExternalURL = URI("http://wb1.example.com")
+    Rails.configuration.Services.Workbench2.ExternalURL = URI("https://wb2.example.com:443")
+
+    act_as_system_user do
+      [["http://wb0.example.com", false],
+       ["http://wb1.example.com", true],
+       ["http://wb2.example.com", false],
+       ["https://wb2.example.com", true],
+       ["https://wb2.example.com/", true],
+      ].each do |pfx, result|
+        a = ApiClient.create(url_prefix: pfx, is_trusted: false)
+        assert_equal result, a.is_trusted
+      end
+
+      a = ApiClient.create(url_prefix: "http://example.com", is_trusted: true)
+      a.save!
+      a.reload
+      assert a.is_trusted
+    end
+  end
 end
index fca8d8b1266eab91aa81ba47b5c06744c6a38e84..7e049144ae1ef49a700ad71580d50a48b0df144f 100644 (file)
@@ -6,7 +6,6 @@
 Description=Arvados Docker Image Cleaner
 Documentation=https://doc.arvados.org/
 After=network.target
-#AssertPathExists=/etc/arvados/docker-cleaner/docker-cleaner.json
 
 # systemd==229 (ubuntu:xenial) obeys StartLimitInterval in the [Unit] section
 StartLimitInterval=0
index e50b0b505aee471f30918772f66f18ec0838e31d..1f27bda426bcdc66fedb50648fbfbb18387d4d9a 100644 (file)
@@ -547,25 +547,32 @@ var changeName = map[int]string{
        changeNone:  "none",
 }
 
+type balancedBlockState struct {
+       needed       int
+       unneeded     int
+       pulling      int
+       unachievable bool
+}
+
 type balanceResult struct {
        blk        *BlockState
        blkid      arvados.SizedDigest
-       have       int
-       want       int
+       lost       bool
+       blockState balancedBlockState
        classState map[string]balancedBlockState
 }
 
+type slot struct {
+       mnt  *KeepMount // never nil
+       repl *Replica   // replica already stored here (or nil)
+       want bool       // we should pull/leave a replica here
+}
+
 // balanceBlock compares current state to desired state for a single
 // block, and makes the appropriate ChangeSet calls.
 func (bal *Balancer) balanceBlock(blkid arvados.SizedDigest, blk *BlockState) balanceResult {
        bal.Logger.Debugf("balanceBlock: %v %+v", blkid, blk)
 
-       type slot struct {
-               mnt  *KeepMount // never nil
-               repl *Replica   // replica already stored here (or nil)
-               want bool       // we should pull/leave a replica here
-       }
-
        // Build a list of all slots (one per mounted volume).
        slots := make([]slot, 0, bal.mounts)
        for _, srv := range bal.KeepServices {
@@ -601,26 +608,9 @@ func (bal *Balancer) balanceBlock(blkid arvados.SizedDigest, blk *BlockState) ba
        // won't want to trash any replicas.
        underreplicated := false
 
-       classState := make(map[string]balancedBlockState, len(bal.classes))
        unsafeToDelete := make(map[int64]bool, len(slots))
        for _, class := range bal.classes {
                desired := blk.Desired[class]
-
-               countedDev := map[string]bool{}
-               have := 0
-               for _, slot := range slots {
-                       if slot.repl != nil && bal.mountsByClass[class][slot.mnt] && !countedDev[slot.mnt.DeviceID] {
-                               have += slot.mnt.Replication
-                               if slot.mnt.DeviceID != "" {
-                                       countedDev[slot.mnt.DeviceID] = true
-                               }
-                       }
-               }
-               classState[class] = balancedBlockState{
-                       desired: desired,
-                       surplus: have - desired,
-               }
-
                if desired == 0 {
                        continue
                }
@@ -733,16 +723,6 @@ func (bal *Balancer) balanceBlock(blkid arvados.SizedDigest, blk *BlockState) ba
                        underreplicated = safe < desired
                }
 
-               // set the unachievable flag if there aren't enough
-               // slots offering the relevant storage class. (This is
-               // as easy as checking slots[desired] because we
-               // already sorted the qualifying slots to the front.)
-               if desired >= len(slots) || !bal.mountsByClass[class][slots[desired].mnt] {
-                       cs := classState[class]
-                       cs.unachievable = true
-                       classState[class] = cs
-               }
-
                // Avoid deleting wanted replicas from devices that
                // are mounted on multiple servers -- even if they
                // haven't already been added to unsafeToDelete
@@ -758,36 +738,40 @@ func (bal *Balancer) balanceBlock(blkid arvados.SizedDigest, blk *BlockState) ba
        // replica that doesn't have a timestamp collision with
        // others.
 
-       countedDev := map[string]bool{}
-       var have, want int
-       for _, slot := range slots {
-               if countedDev[slot.mnt.DeviceID] {
-                       continue
-               }
-               if slot.want {
-                       want += slot.mnt.Replication
-               }
-               if slot.repl != nil {
-                       have += slot.mnt.Replication
-               }
-               if slot.mnt.DeviceID != "" {
-                       countedDev[slot.mnt.DeviceID] = true
+       for i, slot := range slots {
+               // Don't trash (1) any replicas of an underreplicated
+               // block, even if they're in the wrong positions, or
+               // (2) any replicas whose Mtimes are identical to
+               // needed replicas (in case we're really seeing the
+               // same copy via different mounts).
+               if slot.repl != nil && (underreplicated || unsafeToDelete[slot.repl.Mtime]) {
+                       slots[i].want = true
                }
        }
 
+       classState := make(map[string]balancedBlockState, len(bal.classes))
+       for _, class := range bal.classes {
+               classState[class] = computeBlockState(slots, bal.mountsByClass[class], len(blk.Replicas), blk.Desired[class])
+       }
+       blockState := computeBlockState(slots, nil, len(blk.Replicas), 0)
+
+       var lost bool
        var changes []string
        for _, slot := range slots {
                // TODO: request a Touch if Mtime is duplicated.
                var change int
                switch {
-               case !underreplicated && !slot.want && slot.repl != nil && slot.repl.Mtime < bal.MinMtime && !unsafeToDelete[slot.repl.Mtime]:
+               case !slot.want && slot.repl != nil && slot.repl.Mtime < bal.MinMtime:
                        slot.mnt.KeepService.AddTrash(Trash{
                                SizedDigest: blkid,
                                Mtime:       slot.repl.Mtime,
                                From:        slot.mnt,
                        })
                        change = changeTrash
-               case len(blk.Replicas) > 0 && slot.repl == nil && slot.want && !slot.mnt.ReadOnly:
+               case slot.repl == nil && slot.want && len(blk.Replicas) == 0:
+                       lost = true
+                       change = changeNone
+               case slot.repl == nil && slot.want && !slot.mnt.ReadOnly:
                        slot.mnt.KeepService.AddPull(Pull{
                                SizedDigest: blkid,
                                From:        blk.Replicas[0].KeepMount.KeepService,
@@ -809,17 +793,48 @@ func (bal *Balancer) balanceBlock(blkid arvados.SizedDigest, blk *BlockState) ba
                }
        }
        if bal.Dumper != nil {
-               bal.Dumper.Printf("%s refs=%d have=%d want=%v %v %v", blkid, blk.RefCount, have, want, blk.Desired, changes)
+               bal.Dumper.Printf("%s refs=%d needed=%d unneeded=%d pulling=%v %v %v", blkid, blk.RefCount, blockState.needed, blockState.unneeded, blockState.pulling, blk.Desired, changes)
        }
        return balanceResult{
                blk:        blk,
                blkid:      blkid,
-               have:       have,
-               want:       want,
+               lost:       lost,
+               blockState: blockState,
                classState: classState,
        }
 }
 
+func computeBlockState(slots []slot, onlyCount map[*KeepMount]bool, have, needRepl int) (bbs balancedBlockState) {
+       repl := 0
+       countedDev := map[string]bool{}
+       for _, slot := range slots {
+               if onlyCount != nil && !onlyCount[slot.mnt] {
+                       continue
+               }
+               if countedDev[slot.mnt.DeviceID] {
+                       continue
+               }
+               switch {
+               case slot.repl != nil && slot.want:
+                       bbs.needed++
+                       repl += slot.mnt.Replication
+               case slot.repl != nil && !slot.want:
+                       bbs.unneeded++
+                       repl += slot.mnt.Replication
+               case slot.repl == nil && slot.want && have > 0:
+                       bbs.pulling++
+                       repl += slot.mnt.Replication
+               }
+               if slot.mnt.DeviceID != "" {
+                       countedDev[slot.mnt.DeviceID] = true
+               }
+       }
+       if repl < needRepl {
+               bbs.unachievable = true
+       }
+       return
+}
+
 type blocksNBytes struct {
        replicas int
        blocks   int
@@ -830,6 +845,13 @@ func (bb blocksNBytes) String() string {
        return fmt.Sprintf("%d replicas (%d blocks, %d bytes)", bb.replicas, bb.blocks, bb.bytes)
 }
 
+type replicationStats struct {
+       needed       blocksNBytes
+       unneeded     blocksNBytes
+       pulling      blocksNBytes
+       unachievable blocksNBytes
+}
+
 type balancerStats struct {
        lost          blocksNBytes
        overrep       blocksNBytes
@@ -866,25 +888,11 @@ func (s *balancerStats) dedupBlockRatio() float64 {
        return float64(s.collectionBlockRefs) / float64(s.collectionBlocks)
 }
 
-type replicationStats struct {
-       desired      blocksNBytes
-       surplus      blocksNBytes
-       short        blocksNBytes
-       unachievable blocksNBytes
-}
-
-type balancedBlockState struct {
-       desired      int
-       surplus      int
-       unachievable bool
-}
-
 func (bal *Balancer) collectStatistics(results <-chan balanceResult) {
        var s balancerStats
        s.replHistogram = make([]int, 2)
        s.classStats = make(map[string]replicationStats, len(bal.classes))
        for result := range results {
-               surplus := result.have - result.want
                bytes := result.blkid.Size()
 
                if rc := int64(result.blk.RefCount); rc > 0 {
@@ -897,41 +905,51 @@ func (bal *Balancer) collectStatistics(results <-chan balanceResult) {
                for class, state := range result.classState {
                        cs := s.classStats[class]
                        if state.unachievable {
+                               cs.unachievable.replicas++
                                cs.unachievable.blocks++
                                cs.unachievable.bytes += bytes
                        }
-                       if state.desired > 0 {
-                               cs.desired.replicas += state.desired
-                               cs.desired.blocks++
-                               cs.desired.bytes += bytes * int64(state.desired)
+                       if state.needed > 0 {
+                               cs.needed.replicas += state.needed
+                               cs.needed.blocks++
+                               cs.needed.bytes += bytes * int64(state.needed)
                        }
-                       if state.surplus > 0 {
-                               cs.surplus.replicas += state.surplus
-                               cs.surplus.blocks++
-                               cs.surplus.bytes += bytes * int64(state.surplus)
-                       } else if state.surplus < 0 {
-                               cs.short.replicas += -state.surplus
-                               cs.short.blocks++
-                               cs.short.bytes += bytes * int64(-state.surplus)
+                       if state.unneeded > 0 {
+                               cs.unneeded.replicas += state.unneeded
+                               cs.unneeded.blocks++
+                               cs.unneeded.bytes += bytes * int64(state.unneeded)
+                       }
+                       if state.pulling > 0 {
+                               cs.pulling.replicas += state.pulling
+                               cs.pulling.blocks++
+                               cs.pulling.bytes += bytes * int64(state.pulling)
                        }
                        s.classStats[class] = cs
                }
 
+               bs := result.blockState
                switch {
-               case result.have == 0 && result.want > 0:
-                       s.lost.replicas -= surplus
+               case result.lost:
+                       s.lost.replicas++
                        s.lost.blocks++
-                       s.lost.bytes += bytes * int64(-surplus)
+                       s.lost.bytes += bytes
                        fmt.Fprintf(bal.lostBlocks, "%s", strings.SplitN(string(result.blkid), "+", 2)[0])
                        for pdh := range result.blk.Refs {
                                fmt.Fprintf(bal.lostBlocks, " %s", pdh)
                        }
                        fmt.Fprint(bal.lostBlocks, "\n")
-               case surplus < 0:
-                       s.underrep.replicas -= surplus
+               case bs.pulling > 0:
+                       s.underrep.replicas += bs.pulling
+                       s.underrep.blocks++
+                       s.underrep.bytes += bytes * int64(bs.pulling)
+               case bs.unachievable:
+                       s.underrep.replicas++
                        s.underrep.blocks++
-                       s.underrep.bytes += bytes * int64(-surplus)
-               case surplus > 0 && result.want == 0:
+                       s.underrep.bytes += bytes
+               case bs.unneeded > 0 && bs.needed == 0:
+                       // Count as "garbage" if all replicas are old
+                       // enough to trash, otherwise count as
+                       // "unref".
                        counter := &s.garbage
                        for _, r := range result.blk.Replicas {
                                if r.Mtime >= bal.MinMtime {
@@ -939,34 +957,34 @@ func (bal *Balancer) collectStatistics(results <-chan balanceResult) {
                                        break
                                }
                        }
-                       counter.replicas += surplus
+                       counter.replicas += bs.unneeded
                        counter.blocks++
-                       counter.bytes += bytes * int64(surplus)
-               case surplus > 0:
-                       s.overrep.replicas += surplus
+                       counter.bytes += bytes * int64(bs.unneeded)
+               case bs.unneeded > 0:
+                       s.overrep.replicas += bs.unneeded
                        s.overrep.blocks++
-                       s.overrep.bytes += bytes * int64(result.have-result.want)
+                       s.overrep.bytes += bytes * int64(bs.unneeded)
                default:
-                       s.justright.replicas += result.want
+                       s.justright.replicas += bs.needed
                        s.justright.blocks++
-                       s.justright.bytes += bytes * int64(result.want)
+                       s.justright.bytes += bytes * int64(bs.needed)
                }
 
-               if result.want > 0 {
-                       s.desired.replicas += result.want
+               if bs.needed > 0 {
+                       s.desired.replicas += bs.needed
                        s.desired.blocks++
-                       s.desired.bytes += bytes * int64(result.want)
+                       s.desired.bytes += bytes * int64(bs.needed)
                }
-               if result.have > 0 {
-                       s.current.replicas += result.have
+               if bs.needed+bs.unneeded > 0 {
+                       s.current.replicas += bs.needed + bs.unneeded
                        s.current.blocks++
-                       s.current.bytes += bytes * int64(result.have)
+                       s.current.bytes += bytes * int64(bs.needed+bs.unneeded)
                }
 
-               for len(s.replHistogram) <= result.have {
+               for len(s.replHistogram) <= bs.needed+bs.unneeded {
                        s.replHistogram = append(s.replHistogram, 0)
                }
-               s.replHistogram[result.have]++
+               s.replHistogram[bs.needed+bs.unneeded]++
        }
        for _, srv := range bal.KeepServices {
                s.pulls += len(srv.ChangeSet.Pulls)
@@ -990,9 +1008,9 @@ func (bal *Balancer) PrintStatistics() {
        for _, class := range bal.classes {
                cs := bal.stats.classStats[class]
                bal.logf("===")
-               bal.logf("storage class %q: %s desired", class, cs.desired)
-               bal.logf("storage class %q: %s short", class, cs.short)
-               bal.logf("storage class %q: %s surplus", class, cs.surplus)
+               bal.logf("storage class %q: %s needed", class, cs.needed)
+               bal.logf("storage class %q: %s unneeded", class, cs.unneeded)
+               bal.logf("storage class %q: %s pulling", class, cs.pulling)
                bal.logf("storage class %q: %s unachievable", class, cs.unachievable)
        }
        bal.logf("===")
@@ -1008,7 +1026,7 @@ func (bal *Balancer) PrintStatistics() {
 }
 
 func (bal *Balancer) printHistogram(hashColumns int) {
-       bal.logf("Replication level distribution (counting N replicas on a single server as N):")
+       bal.logf("Replication level distribution:")
        maxCount := 0
        for _, count := range bal.stats.replHistogram {
                if maxCount < count {
index 6cffa8ded4dbad6975225949e871852e5ca2d50e..26987724114e75377d59a747f3b59fa7ad79ef33 100644 (file)
@@ -50,7 +50,8 @@ type tester struct {
        shouldPullMounts  []string
        shouldTrashMounts []string
 
-       expectResult balanceResult
+       expectBlockState *balancedBlockState
+       expectClassState map[string]balancedBlockState
 }
 
 func (bal *balancerSuite) SetUpSuite(c *check.C) {
@@ -101,28 +102,42 @@ func (bal *balancerSuite) TestPerfect(c *check.C) {
                desired:     map[string]int{"default": 2},
                current:     slots{0, 1},
                shouldPull:  nil,
-               shouldTrash: nil})
+               shouldTrash: nil,
+               expectBlockState: &balancedBlockState{
+                       needed: 2,
+               }})
 }
 
 func (bal *balancerSuite) TestDecreaseRepl(c *check.C) {
        bal.try(c, tester{
                desired:     map[string]int{"default": 2},
                current:     slots{0, 2, 1},
-               shouldTrash: slots{2}})
+               shouldTrash: slots{2},
+               expectBlockState: &balancedBlockState{
+                       needed:   2,
+                       unneeded: 1,
+               }})
 }
 
 func (bal *balancerSuite) TestDecreaseReplToZero(c *check.C) {
        bal.try(c, tester{
                desired:     map[string]int{"default": 0},
                current:     slots{0, 1, 3},
-               shouldTrash: slots{0, 1, 3}})
+               shouldTrash: slots{0, 1, 3},
+               expectBlockState: &balancedBlockState{
+                       unneeded: 3,
+               }})
 }
 
 func (bal *balancerSuite) TestIncreaseRepl(c *check.C) {
        bal.try(c, tester{
                desired:    map[string]int{"default": 4},
                current:    slots{0, 1},
-               shouldPull: slots{2, 3}})
+               shouldPull: slots{2, 3},
+               expectBlockState: &balancedBlockState{
+                       needed:  2,
+                       pulling: 2,
+               }})
 }
 
 func (bal *balancerSuite) TestSkipReadonly(c *check.C) {
@@ -130,7 +145,11 @@ func (bal *balancerSuite) TestSkipReadonly(c *check.C) {
        bal.try(c, tester{
                desired:    map[string]int{"default": 4},
                current:    slots{0, 1},
-               shouldPull: slots{2, 4}})
+               shouldPull: slots{2, 4},
+               expectBlockState: &balancedBlockState{
+                       needed:  2,
+                       pulling: 2,
+               }})
 }
 
 func (bal *balancerSuite) TestMultipleViewsReadOnly(c *check.C) {
@@ -310,13 +329,10 @@ func (bal *balancerSuite) TestDecreaseReplBlockTooNew(c *check.C) {
                desired:    map[string]int{"default": 2},
                current:    slots{0, 1, 2},
                timestamps: []int64{oldTime, newTime, newTime + 1},
-               expectResult: balanceResult{
-                       have: 3,
-                       want: 2,
-                       classState: map[string]balancedBlockState{"default": {
-                               desired:      2,
-                               surplus:      1,
-                               unachievable: false}}}})
+               expectBlockState: &balancedBlockState{
+                       needed:   2,
+                       unneeded: 1,
+               }})
        // The best replicas are too new to delete, but the excess
        // replica is old enough.
        bal.try(c, tester{
@@ -349,104 +365,121 @@ func (bal *balancerSuite) TestVolumeReplication(c *check.C) {
                known:      0,
                desired:    map[string]int{"default": 2},
                current:    slots{1},
-               shouldPull: slots{0}})
+               shouldPull: slots{0},
+               expectBlockState: &balancedBlockState{
+                       needed:  1,
+                       pulling: 1,
+               }})
        bal.try(c, tester{
                known:      0,
                desired:    map[string]int{"default": 2},
                current:    slots{0, 1},
-               shouldPull: nil})
+               shouldPull: nil,
+               expectBlockState: &balancedBlockState{
+                       needed: 2,
+               }})
        bal.try(c, tester{
                known:       0,
                desired:     map[string]int{"default": 2},
                current:     slots{0, 1, 2},
-               shouldTrash: slots{2}})
+               shouldTrash: slots{2},
+               expectBlockState: &balancedBlockState{
+                       needed:   2,
+                       unneeded: 1,
+               }})
        bal.try(c, tester{
                known:       0,
                desired:     map[string]int{"default": 3},
                current:     slots{0, 2, 3, 4},
                shouldPull:  slots{1},
                shouldTrash: slots{4},
-               expectResult: balanceResult{
-                       have: 4,
-                       want: 3,
-                       classState: map[string]balancedBlockState{"default": {
-                               desired:      3,
-                               surplus:      1,
-                               unachievable: false}}}})
+               expectBlockState: &balancedBlockState{
+                       needed:   3,
+                       unneeded: 1,
+                       pulling:  1,
+               }})
        bal.try(c, tester{
                known:       0,
                desired:     map[string]int{"default": 3},
                current:     slots{0, 1, 2, 3, 4},
-               shouldTrash: slots{2, 3, 4}})
+               shouldTrash: slots{2, 3, 4},
+               expectBlockState: &balancedBlockState{
+                       needed:   2,
+                       unneeded: 3,
+               }})
        bal.try(c, tester{
                known:       0,
                desired:     map[string]int{"default": 4},
                current:     slots{0, 1, 2, 3, 4},
                shouldTrash: slots{3, 4},
-               expectResult: balanceResult{
-                       have: 6,
-                       want: 4,
-                       classState: map[string]balancedBlockState{"default": {
-                               desired:      4,
-                               surplus:      2,
-                               unachievable: false}}}})
+               expectBlockState: &balancedBlockState{
+                       needed:   3,
+                       unneeded: 2,
+               }})
        // block 1 rendezvous is 0,9,7 -- so slot 0 has repl=2
        bal.try(c, tester{
                known:   1,
                desired: map[string]int{"default": 2},
                current: slots{0},
-               expectResult: balanceResult{
-                       have: 2,
-                       want: 2,
-                       classState: map[string]balancedBlockState{"default": {
-                               desired:      2,
-                               surplus:      0,
-                               unachievable: false}}}})
+               expectBlockState: &balancedBlockState{
+                       needed: 1,
+               }})
        bal.try(c, tester{
                known:      1,
                desired:    map[string]int{"default": 3},
                current:    slots{0},
-               shouldPull: slots{1}})
+               shouldPull: slots{1},
+               expectBlockState: &balancedBlockState{
+                       needed:  1,
+                       pulling: 1,
+               }})
        bal.try(c, tester{
                known:      1,
                desired:    map[string]int{"default": 4},
                current:    slots{0},
-               shouldPull: slots{1, 2}})
+               shouldPull: slots{1, 2},
+               expectBlockState: &balancedBlockState{
+                       needed:  1,
+                       pulling: 2,
+               }})
        bal.try(c, tester{
                known:      1,
                desired:    map[string]int{"default": 4},
                current:    slots{2},
-               shouldPull: slots{0, 1}})
+               shouldPull: slots{0, 1},
+               expectBlockState: &balancedBlockState{
+                       needed:  1,
+                       pulling: 2,
+               }})
        bal.try(c, tester{
                known:      1,
                desired:    map[string]int{"default": 4},
                current:    slots{7},
                shouldPull: slots{0, 1, 2},
-               expectResult: balanceResult{
-                       have: 1,
-                       want: 4,
-                       classState: map[string]balancedBlockState{"default": {
-                               desired:      4,
-                               surplus:      -3,
-                               unachievable: false}}}})
+               expectBlockState: &balancedBlockState{
+                       needed:  1,
+                       pulling: 3,
+               }})
        bal.try(c, tester{
                known:       1,
                desired:     map[string]int{"default": 2},
                current:     slots{1, 2, 3, 4},
                shouldPull:  slots{0},
-               shouldTrash: slots{3, 4}})
+               shouldTrash: slots{3, 4},
+               expectBlockState: &balancedBlockState{
+                       needed:   2,
+                       unneeded: 2,
+                       pulling:  1,
+               }})
        bal.try(c, tester{
                known:       1,
                desired:     map[string]int{"default": 2},
                current:     slots{0, 1, 2},
                shouldTrash: slots{1, 2},
-               expectResult: balanceResult{
-                       have: 4,
-                       want: 2,
-                       classState: map[string]balancedBlockState{"default": {
-                               desired:      2,
-                               surplus:      2,
-                               unachievable: false}}}})
+               expectBlockState: &balancedBlockState{
+                       needed:   1,
+                       unneeded: 2,
+               }})
 }
 
 func (bal *balancerSuite) TestDeviceRWMountedByMultipleServers(c *check.C) {
@@ -483,12 +516,10 @@ func (bal *balancerSuite) TestDeviceRWMountedByMultipleServers(c *check.C) {
                desired:    map[string]int{"default": 2},
                current:    slots{1, 9},
                shouldPull: slots{0},
-               expectResult: balanceResult{
-                       have: 1,
-                       classState: map[string]balancedBlockState{"default": {
-                               desired:      2,
-                               surplus:      -1,
-                               unachievable: false}}}})
+               expectBlockState: &balancedBlockState{
+                       needed:  1,
+                       pulling: 1,
+               }})
        // block 0 is overreplicated, but the second and third
        // replicas are the same replica according to DeviceID
        // (despite different Mtimes). Don't trash the third replica.
@@ -496,12 +527,9 @@ func (bal *balancerSuite) TestDeviceRWMountedByMultipleServers(c *check.C) {
                known:   0,
                desired: map[string]int{"default": 2},
                current: slots{0, 1, 9},
-               expectResult: balanceResult{
-                       have: 2,
-                       classState: map[string]balancedBlockState{"default": {
-                               desired:      2,
-                               surplus:      0,
-                               unachievable: false}}}})
+               expectBlockState: &balancedBlockState{
+                       needed: 2,
+               }})
        // block 0 is overreplicated; the third and fifth replicas are
        // extra, but the fourth is another view of the second and
        // shouldn't be trashed.
@@ -510,12 +538,10 @@ func (bal *balancerSuite) TestDeviceRWMountedByMultipleServers(c *check.C) {
                desired:     map[string]int{"default": 2},
                current:     slots{0, 1, 5, 9, 12},
                shouldTrash: slots{5, 12},
-               expectResult: balanceResult{
-                       have: 4,
-                       classState: map[string]balancedBlockState{"default": {
-                               desired:      2,
-                               surplus:      2,
-                               unachievable: false}}}})
+               expectBlockState: &balancedBlockState{
+                       needed:   2,
+                       unneeded: 2,
+               }})
 }
 
 func (bal *balancerSuite) TestChangeStorageClasses(c *check.C) {
@@ -682,14 +708,11 @@ func (bal *balancerSuite) try(c *check.C, t tester) {
                sort.Strings(didTrashMounts)
                c.Check(didTrashMounts, check.DeepEquals, t.shouldTrashMounts)
        }
-       if t.expectResult.have > 0 {
-               c.Check(result.have, check.Equals, t.expectResult.have)
-       }
-       if t.expectResult.want > 0 {
-               c.Check(result.want, check.Equals, t.expectResult.want)
+       if t.expectBlockState != nil {
+               c.Check(result.blockState, check.Equals, *t.expectBlockState)
        }
-       if t.expectResult.classState != nil {
-               c.Check(result.classState, check.DeepEquals, t.expectResult.classState)
+       if t.expectClassState != nil {
+               c.Check(result.classState, check.DeepEquals, t.expectClassState)
        }
 }
 
index aefd0fd08dd20c24a03182c6e967acfccbdcc6ae..14389d191721acd2e3fd941167712b0e15fa1244 100644 (file)
@@ -811,7 +811,12 @@ func (s *IntegrationSuite) testDirectoryListing(c *check.C) {
                } else {
                        c.Check(resp.Code, check.Equals, http.StatusMultiStatus, comment)
                        for _, e := range trial.expect {
-                               c.Check(resp.Body.String(), check.Matches, `(?ms).*<D:href>`+filepath.Join(u.Path, e)+`</D:href>.*`, comment)
+                               if strings.HasSuffix(e, "/") {
+                                       e = filepath.Join(u.Path, e) + "/"
+                               } else {
+                                       e = filepath.Join(u.Path, e)
+                               }
+                               c.Check(resp.Body.String(), check.Matches, `(?ms).*<D:href>`+e+`</D:href>.*`, comment)
                        }
                }
        }
index 0d2e776f1ab7869cd5a77cbea4d61f503ae9db23..ad694395094bf02b6634a5798b4f40ba7219f6bc 100644 (file)
@@ -342,12 +342,8 @@ func (s *IntegrationSuite) TestMetrics(c *check.C) {
        c.Assert(err, check.IsNil)
        c.Check(resp.StatusCode, check.Equals, http.StatusOK)
        type summary struct {
-               SampleCount string  `json:"sample_count"`
-               SampleSum   float64 `json:"sample_sum"`
-               Quantile    []struct {
-                       Quantile float64
-                       Value    float64
-               }
+               SampleCount string
+               SampleSum   float64
        }
        type counter struct {
                Value int64
index 9b5606b5c405fe2fe264fb2a66a7bf8701f72997..dd6247f8bb2824879669f6bb7d9c21eae2d61df2 100644 (file)
@@ -107,12 +107,8 @@ func (s *HandlerSuite) TestMetrics(c *check.C) {
                                Value string
                        }
                        Summary struct {
-                               SampleCount string  `json:"sample_count"`
-                               SampleSum   float64 `json:"sample_sum"`
-                               Quantile    []struct {
-                                       Quantile float64
-                                       Value    float64
-                               }
+                               SampleCount string
+                               SampleSum   float64
                        }
                }
        }
@@ -124,8 +120,6 @@ func (s *HandlerSuite) TestMetrics(c *check.C) {
                for _, m := range g.Metric {
                        if len(m.Label) == 2 && m.Label[0].Name == "code" && m.Label[0].Value == "200" && m.Label[1].Name == "method" && m.Label[1].Value == "put" {
                                c.Check(m.Summary.SampleCount, check.Equals, "2")
-                               c.Check(len(m.Summary.Quantile), check.Not(check.Equals), 0)
-                               c.Check(m.Summary.Quantile[0].Value, check.Not(check.Equals), float64(0))
                                found[g.Name] = true
                        }
                }
index cdd7298da000376a7755258edf38f9de01438de8..15a11b05805b15a54edf37fedeeaa5a9c4b62d4d 100644 (file)
@@ -3,17 +3,15 @@
 #
 # SPDX-License-Identifier: AGPL-3.0
 
-mkdir -p /var/lib/gopath
-cd /var/lib/gopath
+export GOPATH=/var/lib/gopath
+mkdir -p $GOPATH
 
-export GOPATH=$PWD
-mkdir -p "$GOPATH/src/git.curoverse.com"
-ln -sfn "/usr/src/arvados" "$GOPATH/src/git.curoverse.com/arvados.git"
-
-flock /var/lib/gopath/gopath.lock go get -t github.com/kardianos/govendor
-cd "$GOPATH/src/git.curoverse.com/arvados.git"
-flock /var/lib/gopath/gopath.lock go get -v -d ...
-flock /var/lib/gopath/gopath.lock "$GOPATH/bin/govendor" sync
-
-flock /var/lib/gopath/gopath.lock go get -t "git.curoverse.com/arvados.git/cmd/arvados-server"
+cd /usr/src/arvados
+if [[ $UID = 0 ]] ; then
+    /usr/local/lib/arvbox/runsu.sh flock /var/lib/gopath/gopath.lock go mod download
+    /usr/local/lib/arvbox/runsu.sh flock /var/lib/gopath/gopath.lock go get ./cmd/arvados-server
+else
+    flock /var/lib/gopath/gopath.lock go mod download
+    flock /var/lib/gopath/gopath.lock go get ./cmd/arvados-server
+fi
 install $GOPATH/bin/arvados-server /usr/local/bin
diff --git a/vendor/.gitignore b/vendor/.gitignore
deleted file mode 100644 (file)
index f902f86..0000000
+++ /dev/null
@@ -1,3 +0,0 @@
-*
-!vendor.json
-!.gitignore
diff --git a/vendor/vendor.json b/vendor/vendor.json
deleted file mode 100644 (file)
index c146a00..0000000
+++ /dev/null
@@ -1,1185 +0,0 @@
-{
-       "comment": "",
-       "ignore": "test",
-       "package": [
-               {
-                       "checksumSHA1": "jfYWZyRWLMfG0J5K7G2K8a9AKfs=",
-                       "origin": "github.com/curoverse/goamz/aws",
-                       "path": "github.com/AdRoll/goamz/aws",
-                       "revision": "1bba09f407ef1d02c90bc37eff7e91e2231fa587",
-                       "revisionTime": "2019-09-05T14:15:25Z"
-               },
-               {
-                       "checksumSHA1": "lqoARtBgwnvhEhLyIjR3GLnR5/c=",
-                       "origin": "github.com/curoverse/goamz/s3",
-                       "path": "github.com/AdRoll/goamz/s3",
-                       "revision": "1bba09f407ef1d02c90bc37eff7e91e2231fa587",
-                       "revisionTime": "2019-09-05T14:15:25Z"
-               },
-               {
-                       "checksumSHA1": "tvxbsTkdjB0C/uxEglqD6JfVnMg=",
-                       "origin": "github.com/curoverse/goamz/s3/s3test",
-                       "path": "github.com/AdRoll/goamz/s3/s3test",
-                       "revision": "1bba09f407ef1d02c90bc37eff7e91e2231fa587",
-                       "revisionTime": "2019-09-05T14:15:25Z"
-               },
-               {
-                       "checksumSHA1": "KF4DsRUpZ+h+qRQ/umRAQZfVvw0=",
-                       "path": "github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2018-06-01/compute",
-                       "revision": "4e8cbbfb1aeab140cd0fa97fd16b64ee18c3ca6a",
-                       "revisionTime": "2018-07-27T22:05:59Z"
-               },
-               {
-                       "checksumSHA1": "IZNzp1cYx+xYHd4gzosKpG6Jr/k=",
-                       "path": "github.com/Azure/azure-sdk-for-go/services/network/mgmt/2018-06-01/network",
-                       "revision": "4e8cbbfb1aeab140cd0fa97fd16b64ee18c3ca6a",
-                       "revisionTime": "2018-07-27T22:05:59Z"
-               },
-               {
-                       "checksumSHA1": "W4c2uTDJlwhfryWg9esshmJANo0=",
-                       "path": "github.com/Azure/azure-sdk-for-go/services/storage/mgmt/2018-02-01/storage",
-                       "revision": "4e8cbbfb1aeab140cd0fa97fd16b64ee18c3ca6a",
-                       "revisionTime": "2018-07-27T22:05:59Z"
-               },
-               {
-                       "checksumSHA1": "xHZe/h/tyrqmS9qiR03bLfRv5FI=",
-                       "path": "github.com/Azure/azure-sdk-for-go/storage",
-                       "revision": "f8eeb65a1a1f969696b49aada9d24073f2c2acd1",
-                       "revisionTime": "2018-02-15T19:19:13Z"
-               },
-               {
-                       "checksumSHA1": "PfyfOXsPbGEWmdh54cguqzdwloY=",
-                       "path": "github.com/Azure/azure-sdk-for-go/version",
-                       "revision": "471256ff7c6c93b96131845cef5309d20edd313d",
-                       "revisionTime": "2018-02-14T01:17:07Z"
-               },
-               {
-                       "checksumSHA1": "1Y2+bSzYrdPHQqRjR1OrBMHAvxY=",
-                       "path": "github.com/Azure/go-autorest/autorest",
-                       "revision": "39013ecb48eaf6ced3f4e3e1d95515140ce6b3cf",
-                       "revisionTime": "2018-08-09T20:19:59Z"
-               },
-               {
-                       "checksumSHA1": "GxL0HHpZDj2milPhR3SPV6MWLPc=",
-                       "path": "github.com/Azure/go-autorest/autorest/adal",
-                       "revision": "39013ecb48eaf6ced3f4e3e1d95515140ce6b3cf",
-                       "revisionTime": "2018-08-09T20:19:59Z"
-               },
-               {
-                       "checksumSHA1": "ZNgwJOdHZmm4k/HJIbT1L5giO6M=",
-                       "path": "github.com/Azure/go-autorest/autorest/azure",
-                       "revision": "39013ecb48eaf6ced3f4e3e1d95515140ce6b3cf",
-                       "revisionTime": "2018-08-09T20:19:59Z"
-               },
-               {
-                       "checksumSHA1": "6i7kwcXGTn55WqfubQs21swgr34=",
-                       "path": "github.com/Azure/go-autorest/autorest/azure/auth",
-                       "revision": "39013ecb48eaf6ced3f4e3e1d95515140ce6b3cf",
-                       "revisionTime": "2018-08-09T20:19:59Z"
-               },
-               {
-                       "checksumSHA1": "9nXCi9qQsYjxCeajJKWttxgEt0I=",
-                       "path": "github.com/Azure/go-autorest/autorest/date",
-                       "revision": "39013ecb48eaf6ced3f4e3e1d95515140ce6b3cf",
-                       "revisionTime": "2018-08-09T20:19:59Z"
-               },
-               {
-                       "checksumSHA1": "SbBb2GcJNm5GjuPKGL2777QywR4=",
-                       "path": "github.com/Azure/go-autorest/autorest/to",
-                       "revision": "39013ecb48eaf6ced3f4e3e1d95515140ce6b3cf",
-                       "revisionTime": "2018-08-09T20:19:59Z"
-               },
-               {
-                       "checksumSHA1": "HjdLfAF3oA2In8F3FKh/Y+BPyXk=",
-                       "path": "github.com/Azure/go-autorest/autorest/validation",
-                       "revision": "39013ecb48eaf6ced3f4e3e1d95515140ce6b3cf",
-                       "revisionTime": "2018-08-09T20:19:59Z"
-               },
-               {
-                       "checksumSHA1": "b2lrPJRxf+MEfmMafN40wepi5WM=",
-                       "path": "github.com/Azure/go-autorest/logger",
-                       "revision": "39013ecb48eaf6ced3f4e3e1d95515140ce6b3cf",
-                       "revisionTime": "2018-08-09T20:19:59Z"
-               },
-               {
-                       "checksumSHA1": "UtAIMAsMWLBJ6yO1qZ0soFnb0sI=",
-                       "path": "github.com/Azure/go-autorest/version",
-                       "revision": "39013ecb48eaf6ced3f4e3e1d95515140ce6b3cf",
-                       "revisionTime": "2018-08-09T20:19:59Z"
-               },
-               {
-                       "checksumSHA1": "o/3cn04KAiwC7NqNVvmfVTD+hgA=",
-                       "path": "github.com/Microsoft/go-winio",
-                       "revision": "78439966b38d69bf38227fbf57ac8a6fee70f69a",
-                       "revisionTime": "2017-08-04T20:09:54Z"
-               },
-               {
-                       "checksumSHA1": "k59wLJfyqGB04o238WhKSAzSz9M=",
-                       "path": "github.com/aws/aws-sdk-go/aws",
-                       "revision": "d496c5aab9b8ba36936e457a488e971b4f9fd891",
-                       "revisionTime": "2019-03-06T20:18:39Z"
-               },
-               {
-                       "checksumSHA1": "Y9W+4GimK4Fuxq+vyIskVYFRnX4=",
-                       "path": "github.com/aws/aws-sdk-go/aws/awserr",
-                       "revision": "d496c5aab9b8ba36936e457a488e971b4f9fd891",
-                       "revisionTime": "2019-03-06T20:18:39Z"
-               },
-               {
-                       "checksumSHA1": "PEDqMAEPxlh9Y8/dIbHlE6A7LEA=",
-                       "path": "github.com/aws/aws-sdk-go/aws/awsutil",
-                       "revision": "d496c5aab9b8ba36936e457a488e971b4f9fd891",
-                       "revisionTime": "2019-03-06T20:18:39Z"
-               },
-               {
-                       "checksumSHA1": "KpW2B6W3J1yB/7QJWjjtsKz1Xbc=",
-                       "path": "github.com/aws/aws-sdk-go/aws/client",
-                       "revision": "d496c5aab9b8ba36936e457a488e971b4f9fd891",
-                       "revisionTime": "2019-03-06T20:18:39Z"
-               },
-               {
-                       "checksumSHA1": "uEJU4I6dTKaraQKvrljlYKUZwoc=",
-                       "path": "github.com/aws/aws-sdk-go/aws/client/metadata",
-                       "revision": "d496c5aab9b8ba36936e457a488e971b4f9fd891",
-                       "revisionTime": "2019-03-06T20:18:39Z"
-               },
-               {
-                       "checksumSHA1": "GvmthjOyNZGOKmXK4XVrbT5+K9I=",
-                       "path": "github.com/aws/aws-sdk-go/aws/corehandlers",
-                       "revision": "d496c5aab9b8ba36936e457a488e971b4f9fd891",
-                       "revisionTime": "2019-03-06T20:18:39Z"
-               },
-               {
-                       "checksumSHA1": "QHizt8XKUpuslIZv6EH6ENiGpGA=",
-                       "path": "github.com/aws/aws-sdk-go/aws/credentials",
-                       "revision": "d496c5aab9b8ba36936e457a488e971b4f9fd891",
-                       "revisionTime": "2019-03-06T20:18:39Z"
-               },
-               {
-                       "checksumSHA1": "JTilCBYWVAfhbKSnrxCNhE8IFns=",
-                       "path": "github.com/aws/aws-sdk-go/aws/credentials/ec2rolecreds",
-                       "revision": "d496c5aab9b8ba36936e457a488e971b4f9fd891",
-                       "revisionTime": "2019-03-06T20:18:39Z"
-               },
-               {
-                       "checksumSHA1": "1pENtl2K9hG7qoB7R6J7dAHa82g=",
-                       "path": "github.com/aws/aws-sdk-go/aws/credentials/endpointcreds",
-                       "revision": "d496c5aab9b8ba36936e457a488e971b4f9fd891",
-                       "revisionTime": "2019-03-06T20:18:39Z"
-               },
-               {
-                       "checksumSHA1": "sPtOSV32SZr2xN7vZlF4FXo43/o=",
-                       "path": "github.com/aws/aws-sdk-go/aws/credentials/processcreds",
-                       "revision": "d496c5aab9b8ba36936e457a488e971b4f9fd891",
-                       "revisionTime": "2019-03-06T20:18:39Z"
-               },
-               {
-                       "checksumSHA1": "JEYqmF83O5n5bHkupAzA6STm0no=",
-                       "path": "github.com/aws/aws-sdk-go/aws/credentials/stscreds",
-                       "revision": "d496c5aab9b8ba36936e457a488e971b4f9fd891",
-                       "revisionTime": "2019-03-06T20:18:39Z"
-               },
-               {
-                       "checksumSHA1": "3pJft1H34eTYK6s6p3ijj3mGtc4=",
-                       "path": "github.com/aws/aws-sdk-go/aws/csm",
-                       "revision": "d496c5aab9b8ba36936e457a488e971b4f9fd891",
-                       "revisionTime": "2019-03-06T20:18:39Z"
-               },
-               {
-                       "checksumSHA1": "7AmyyJXVkMdmy8dphC3Nalx5XkI=",
-                       "path": "github.com/aws/aws-sdk-go/aws/defaults",
-                       "revision": "d496c5aab9b8ba36936e457a488e971b4f9fd891",
-                       "revisionTime": "2019-03-06T20:18:39Z"
-               },
-               {
-                       "checksumSHA1": "47hnR1KYqZDBT3xmHuS7cNtqHP8=",
-                       "path": "github.com/aws/aws-sdk-go/aws/ec2metadata",
-                       "revision": "d496c5aab9b8ba36936e457a488e971b4f9fd891",
-                       "revisionTime": "2019-03-06T20:18:39Z"
-               },
-               {
-                       "checksumSHA1": "pcWH1AkR7sUs84cN/XTD9Jexf2Q=",
-                       "path": "github.com/aws/aws-sdk-go/aws/endpoints",
-                       "revision": "d496c5aab9b8ba36936e457a488e971b4f9fd891",
-                       "revisionTime": "2019-03-06T20:18:39Z"
-               },
-               {
-                       "checksumSHA1": "nhavXPspOdqm5iAvIGgmZmXk4aI=",
-                       "path": "github.com/aws/aws-sdk-go/aws/request",
-                       "revision": "d496c5aab9b8ba36936e457a488e971b4f9fd891",
-                       "revisionTime": "2019-03-06T20:18:39Z"
-               },
-               {
-                       "checksumSHA1": "w4tSwNFNJ4cGgjYEdAgsDnikqec=",
-                       "path": "github.com/aws/aws-sdk-go/aws/session",
-                       "revision": "d496c5aab9b8ba36936e457a488e971b4f9fd891",
-                       "revisionTime": "2019-03-06T20:18:39Z"
-               },
-               {
-                       "checksumSHA1": "C9uAu9gsLIpJGIX6/5P+n3s9wQo=",
-                       "path": "github.com/aws/aws-sdk-go/aws/signer/v4",
-                       "revision": "d496c5aab9b8ba36936e457a488e971b4f9fd891",
-                       "revisionTime": "2019-03-06T20:18:39Z"
-               },
-               {
-                       "checksumSHA1": "Fe2TPw9X2UvlkRaOS7LPJlpkuTo=",
-                       "path": "github.com/aws/aws-sdk-go/internal/ini",
-                       "revision": "d496c5aab9b8ba36936e457a488e971b4f9fd891",
-                       "revisionTime": "2019-03-06T20:18:39Z"
-               },
-               {
-                       "checksumSHA1": "wjxQlU1PYxrDRFoL1Vek8Wch7jk=",
-                       "path": "github.com/aws/aws-sdk-go/internal/sdkio",
-                       "revision": "d496c5aab9b8ba36936e457a488e971b4f9fd891",
-                       "revisionTime": "2019-03-06T20:18:39Z"
-               },
-               {
-                       "checksumSHA1": "MYLldFRnsZh21TfCkgkXCT3maPU=",
-                       "path": "github.com/aws/aws-sdk-go/internal/sdkrand",
-                       "revision": "d496c5aab9b8ba36936e457a488e971b4f9fd891",
-                       "revisionTime": "2019-03-06T20:18:39Z"
-               },
-               {
-                       "checksumSHA1": "tQVg7Sz2zv+KkhbiXxPH0mh9spg=",
-                       "path": "github.com/aws/aws-sdk-go/internal/sdkuri",
-                       "revision": "d496c5aab9b8ba36936e457a488e971b4f9fd891",
-                       "revisionTime": "2019-03-06T20:18:39Z"
-               },
-               {
-                       "checksumSHA1": "sXiZ5x6j2FvlIO57pboVnRTm7QA=",
-                       "path": "github.com/aws/aws-sdk-go/internal/shareddefaults",
-                       "revision": "d496c5aab9b8ba36936e457a488e971b4f9fd891",
-                       "revisionTime": "2019-03-06T20:18:39Z"
-               },
-               {
-                       "checksumSHA1": "NtXXi501Kou3laVAsJfcbKSkNI8=",
-                       "path": "github.com/aws/aws-sdk-go/private/protocol",
-                       "revision": "d496c5aab9b8ba36936e457a488e971b4f9fd891",
-                       "revisionTime": "2019-03-06T20:18:39Z"
-               },
-               {
-                       "checksumSHA1": "0cZnOaE1EcFUuiu4bdHV2k7slQg=",
-                       "path": "github.com/aws/aws-sdk-go/private/protocol/ec2query",
-                       "revision": "d496c5aab9b8ba36936e457a488e971b4f9fd891",
-                       "revisionTime": "2019-03-06T20:18:39Z"
-               },
-               {
-                       "checksumSHA1": "lj56XJFI2OSp+hEOrFZ+eiEi/yM=",
-                       "path": "github.com/aws/aws-sdk-go/private/protocol/query",
-                       "revision": "d496c5aab9b8ba36936e457a488e971b4f9fd891",
-                       "revisionTime": "2019-03-06T20:18:39Z"
-               },
-               {
-                       "checksumSHA1": "+O6A945eTP9plLpkEMZB0lwBAcg=",
-                       "path": "github.com/aws/aws-sdk-go/private/protocol/query/queryutil",
-                       "revision": "d496c5aab9b8ba36936e457a488e971b4f9fd891",
-                       "revisionTime": "2019-03-06T20:18:39Z"
-               },
-               {
-                       "checksumSHA1": "RDOk9se2S83/HAYmWnpoW3bgQfQ=",
-                       "path": "github.com/aws/aws-sdk-go/private/protocol/rest",
-                       "revision": "d496c5aab9b8ba36936e457a488e971b4f9fd891",
-                       "revisionTime": "2019-03-06T20:18:39Z"
-               },
-               {
-                       "checksumSHA1": "B8unEuOlpQfnig4cMyZtXLZVVOs=",
-                       "path": "github.com/aws/aws-sdk-go/private/protocol/xml/xmlutil",
-                       "revision": "d496c5aab9b8ba36936e457a488e971b4f9fd891",
-                       "revisionTime": "2019-03-06T20:18:39Z"
-               },
-               {
-                       "checksumSHA1": "uvEbLM/ZodhtEUVTEoC+Lbc9PHg=",
-                       "path": "github.com/aws/aws-sdk-go/service/ec2",
-                       "revision": "d496c5aab9b8ba36936e457a488e971b4f9fd891",
-                       "revisionTime": "2019-03-06T20:18:39Z"
-               },
-               {
-                       "checksumSHA1": "HMY+b4YBLVvWoKm5vB+H7tpKiTI=",
-                       "path": "github.com/aws/aws-sdk-go/service/sts",
-                       "revision": "d496c5aab9b8ba36936e457a488e971b4f9fd891",
-                       "revisionTime": "2019-03-06T20:18:39Z"
-               },
-               {
-                       "checksumSHA1": "spyv5/YFBjYyZLZa1U2LBfDR8PM=",
-                       "path": "github.com/beorn7/perks/quantile",
-                       "revision": "4c0e84591b9aa9e6dcfdf3e020114cd81f89d5f9",
-                       "revisionTime": "2016-08-04T10:47:26Z"
-               },
-               {
-                       "checksumSHA1": "+Zz+leZHHC9C0rx8DoRuffSRPso=",
-                       "path": "github.com/coreos/go-systemd/daemon",
-                       "revision": "cc4f39464dc797b91c8025330de585294c2a6950",
-                       "revisionTime": "2018-01-08T08:51:32Z"
-               },
-               {
-                       "checksumSHA1": "+TKtBzv23ywvmmqRiGEjUba4YmI=",
-                       "path": "github.com/dgrijalva/jwt-go",
-                       "revision": "dbeaa9332f19a944acb5736b4456cfcc02140e29",
-                       "revisionTime": "2017-10-19T21:57:19Z"
-               },
-               {
-                       "checksumSHA1": "7EjxkAUND/QY/sN+2fNKJ52v1Rc=",
-                       "path": "github.com/dimchansky/utfbom",
-                       "revision": "5448fe645cb1964ba70ac8f9f2ffe975e61a536c",
-                       "revisionTime": "2018-07-13T13:37:17Z"
-               },
-               {
-                       "checksumSHA1": "Gj+xR1VgFKKmFXYOJMnAczC3Znk=",
-                       "path": "github.com/docker/distribution/digestset",
-                       "revision": "277ed486c948042cab91ad367c379524f3b25e18",
-                       "revisionTime": "2018-01-05T23:27:52Z"
-               },
-               {
-                       "checksumSHA1": "2Fe4D6PGaVE2he4fUeenLmhC1lE=",
-                       "path": "github.com/docker/distribution/reference",
-                       "revision": "277ed486c948042cab91ad367c379524f3b25e18",
-                       "revisionTime": "2018-01-05T23:27:52Z"
-               },
-               {
-                       "checksumSHA1": "QKCQfrTv4wTL0KBDMHpWM/jHl9I=",
-                       "path": "github.com/docker/docker/api",
-                       "revision": "94b8a116fbf1cd90e68d8f5361b520d326a66f9b",
-                       "revisionTime": "2018-01-09T01:38:17Z"
-               },
-               {
-                       "checksumSHA1": "b91BIyJbqy05pXpEh1eGCJkdjYc=",
-                       "path": "github.com/docker/docker/api/types",
-                       "revision": "94b8a116fbf1cd90e68d8f5361b520d326a66f9b",
-                       "revisionTime": "2018-01-09T01:38:17Z"
-               },
-               {
-                       "checksumSHA1": "jVJDbe0IcyjoKc2xbohwzQr+FF0=",
-                       "path": "github.com/docker/docker/api/types/blkiodev",
-                       "revision": "94b8a116fbf1cd90e68d8f5361b520d326a66f9b",
-                       "revisionTime": "2018-01-09T01:38:17Z"
-               },
-               {
-                       "checksumSHA1": "DuOqFTQ95vKSuSE/Va88yRN/wb8=",
-                       "path": "github.com/docker/docker/api/types/container",
-                       "revision": "94b8a116fbf1cd90e68d8f5361b520d326a66f9b",
-                       "revisionTime": "2018-01-09T01:38:17Z"
-               },
-               {
-                       "checksumSHA1": "XDP7i6sMYGnUKeFzgt+mFBJwjjw=",
-                       "path": "github.com/docker/docker/api/types/events",
-                       "revision": "94b8a116fbf1cd90e68d8f5361b520d326a66f9b",
-                       "revisionTime": "2018-01-09T01:38:17Z"
-               },
-               {
-                       "checksumSHA1": "S4SWOa0XduRd8ene8Alwih2Nwcw=",
-                       "path": "github.com/docker/docker/api/types/filters",
-                       "revision": "94b8a116fbf1cd90e68d8f5361b520d326a66f9b",
-                       "revisionTime": "2018-01-09T01:38:17Z"
-               },
-               {
-                       "checksumSHA1": "KuC0C6jo1t7tlvIqb7G3u1FIaZU=",
-                       "path": "github.com/docker/docker/api/types/image",
-                       "revision": "94b8a116fbf1cd90e68d8f5361b520d326a66f9b",
-                       "revisionTime": "2018-01-09T01:38:17Z"
-               },
-               {
-                       "checksumSHA1": "uJeLBKpHZXP+bWhXP4HhpyUTWYI=",
-                       "path": "github.com/docker/docker/api/types/mount",
-                       "revision": "94b8a116fbf1cd90e68d8f5361b520d326a66f9b",
-                       "revisionTime": "2018-01-09T01:38:17Z"
-               },
-               {
-                       "checksumSHA1": "Gskp+nvbVe8Gk1xPLHylZvNmqTg=",
-                       "path": "github.com/docker/docker/api/types/network",
-                       "revision": "94b8a116fbf1cd90e68d8f5361b520d326a66f9b",
-                       "revisionTime": "2018-01-09T01:38:17Z"
-               },
-               {
-                       "checksumSHA1": "r2vWq7Uc3ExKzMqYgH0b4AKjLKY=",
-                       "path": "github.com/docker/docker/api/types/registry",
-                       "revision": "94b8a116fbf1cd90e68d8f5361b520d326a66f9b",
-                       "revisionTime": "2018-01-09T01:38:17Z"
-               },
-               {
-                       "checksumSHA1": "VTxWyFud/RedrpllGdQonVtGM/A=",
-                       "path": "github.com/docker/docker/api/types/strslice",
-                       "revision": "94b8a116fbf1cd90e68d8f5361b520d326a66f9b",
-                       "revisionTime": "2018-01-09T01:38:17Z"
-               },
-               {
-                       "checksumSHA1": "Q0U3queMsCw+rPPztXnRHwAxQEc=",
-                       "path": "github.com/docker/docker/api/types/swarm",
-                       "revision": "94b8a116fbf1cd90e68d8f5361b520d326a66f9b",
-                       "revisionTime": "2018-01-09T01:38:17Z"
-               },
-               {
-                       "checksumSHA1": "kVfD1e4Gak7k6tqDX5nrgQ57EYY=",
-                       "path": "github.com/docker/docker/api/types/swarm/runtime",
-                       "revision": "94b8a116fbf1cd90e68d8f5361b520d326a66f9b",
-                       "revisionTime": "2018-01-09T01:38:17Z"
-               },
-               {
-                       "checksumSHA1": "77axKFOjRx1nGrzIggGXfTxUYVQ=",
-                       "path": "github.com/docker/docker/api/types/time",
-                       "revision": "94b8a116fbf1cd90e68d8f5361b520d326a66f9b",
-                       "revisionTime": "2018-01-09T01:38:17Z"
-               },
-               {
-                       "checksumSHA1": "uDPQ3nHsrvGQc9tg/J9OSC4N5dQ=",
-                       "path": "github.com/docker/docker/api/types/versions",
-                       "revision": "94b8a116fbf1cd90e68d8f5361b520d326a66f9b",
-                       "revisionTime": "2018-01-09T01:38:17Z"
-               },
-               {
-                       "checksumSHA1": "IBJy2zPEnYmcFJ3lM1eiRWnCxTA=",
-                       "path": "github.com/docker/docker/api/types/volume",
-                       "revision": "94b8a116fbf1cd90e68d8f5361b520d326a66f9b",
-                       "revisionTime": "2018-01-09T01:38:17Z"
-               },
-               {
-                       "checksumSHA1": "zQvx3WYTAwbPZEaVPjAsrmW7V00=",
-                       "path": "github.com/docker/docker/client",
-                       "revision": "94b8a116fbf1cd90e68d8f5361b520d326a66f9b",
-                       "revisionTime": "2018-01-09T01:38:17Z"
-               },
-               {
-                       "checksumSHA1": "JbiWTzH699Sqz25XmDlsARpMN9w=",
-                       "path": "github.com/docker/go-connections/nat",
-                       "revision": "3ede32e2033de7505e6500d6c868c2b9ed9f169d",
-                       "revisionTime": "2017-06-23T20:36:43Z"
-               },
-               {
-                       "checksumSHA1": "jUfDG3VQsA2UZHvvIXncgiddpYA=",
-                       "path": "github.com/docker/go-connections/sockets",
-                       "revision": "3ede32e2033de7505e6500d6c868c2b9ed9f169d",
-                       "revisionTime": "2017-06-23T20:36:43Z"
-               },
-               {
-                       "checksumSHA1": "c6lDGNwTm5mYq18IHP+lqYpk8xU=",
-                       "path": "github.com/docker/go-connections/tlsconfig",
-                       "revision": "3ede32e2033de7505e6500d6c868c2b9ed9f169d",
-                       "revisionTime": "2017-06-23T20:36:43Z"
-               },
-               {
-                       "checksumSHA1": "kP4hqQGUNNXhgYxgB4AMWfNvmnA=",
-                       "path": "github.com/docker/go-units",
-                       "revision": "d59758554a3d3911fa25c0269de1ebe2f1912c39",
-                       "revisionTime": "2017-12-21T20:03:56Z"
-               },
-               {
-                       "checksumSHA1": "ImX1uv6O09ggFeBPUJJ2nu7MPSA=",
-                       "path": "github.com/ghodss/yaml",
-                       "revision": "0ca9ea5df5451ffdf184b4428c902747c2c11cd7",
-                       "revisionTime": "2017-03-27T23:54:44Z"
-               },
-               {
-                       "checksumSHA1": "8UEp6v0Dczw/SlasE0DivB0mAHA=",
-                       "path": "github.com/gogo/protobuf/jsonpb",
-                       "revision": "30cf7ac33676b5786e78c746683f0d4cd64fa75b",
-                       "revisionTime": "2018-05-09T16:24:41Z"
-               },
-               {
-                       "checksumSHA1": "wn2shNJMwRZpvuvkf1s7h0wvqHI=",
-                       "path": "github.com/gogo/protobuf/proto",
-                       "revision": "160de10b2537169b5ae3e7e221d28269ef40d311",
-                       "revisionTime": "2018-01-04T10:21:28Z"
-               },
-               {
-                       "checksumSHA1": "HPVQZu059/Rfw2bAWM538bVTcUc=",
-                       "path": "github.com/gogo/protobuf/sortkeys",
-                       "revision": "30cf7ac33676b5786e78c746683f0d4cd64fa75b",
-                       "revisionTime": "2018-05-09T16:24:41Z"
-               },
-               {
-                       "checksumSHA1": "SkxU1+wPGUJyLyQENrZtr2/OUBs=",
-                       "path": "github.com/gogo/protobuf/types",
-                       "revision": "30cf7ac33676b5786e78c746683f0d4cd64fa75b",
-                       "revisionTime": "2018-05-09T16:24:41Z"
-               },
-               {
-                       "checksumSHA1": "yqF125xVSkmfLpIVGrLlfE05IUk=",
-                       "path": "github.com/golang/protobuf/proto",
-                       "revision": "1e59b77b52bf8e4b449a57e6f79f21226d571845",
-                       "revisionTime": "2017-11-13T18:07:20Z"
-               },
-               {
-                       "checksumSHA1": "iIUYZyoanCQQTUaWsu8b+iOSPt4=",
-                       "origin": "github.com/docker/docker/vendor/github.com/gorilla/context",
-                       "path": "github.com/gorilla/context",
-                       "revision": "94b8a116fbf1cd90e68d8f5361b520d326a66f9b",
-                       "revisionTime": "2018-01-09T01:38:17Z"
-               },
-               {
-                       "checksumSHA1": "fSs1WcPh2F5JJtxqYC+Jt8yCkYc=",
-                       "path": "github.com/gorilla/mux",
-                       "revision": "5bbbb5b2b5729b132181cc7f4aa3b3c973e9a0ed",
-                       "revisionTime": "2018-01-07T15:57:08Z"
-               },
-               {
-                       "checksumSHA1": "d9PxF1XQGLMJZRct2R8qVM/eYlE=",
-                       "path": "github.com/hashicorp/golang-lru",
-                       "revision": "0a025b7e63adc15a622f29b0b2c4c3848243bbf6",
-                       "revisionTime": "2016-08-13T22:13:03Z"
-               },
-               {
-                       "checksumSHA1": "9hffs0bAIU6CquiRhKQdzjHnKt0=",
-                       "path": "github.com/hashicorp/golang-lru/simplelru",
-                       "revision": "0a025b7e63adc15a622f29b0b2c4c3848243bbf6",
-                       "revisionTime": "2016-08-13T22:13:03Z"
-               },
-               {
-                       "checksumSHA1": "x7IEwuVYTztOJItr3jtePGyFDWA=",
-                       "path": "github.com/imdario/mergo",
-                       "revision": "5ef87b449ca75fbed1bc3765b749ca8f73f1fa69",
-                       "revisionTime": "2019-04-15T13:31:43Z"
-               },
-               {
-                       "checksumSHA1": "iCsyavJDnXC9OY//p52IWJWy7PY=",
-                       "path": "github.com/jbenet/go-context/io",
-                       "revision": "d14ea06fba99483203c19d92cfcd13ebe73135f4",
-                       "revisionTime": "2015-07-11T00:45:18Z"
-               },
-               {
-                       "checksumSHA1": "khL6oKjx81rAZKW+36050b7f5As=",
-                       "path": "github.com/jmcvetta/randutil",
-                       "revision": "2bb1b664bcff821e02b2a0644cd29c7e824d54f8",
-                       "revisionTime": "2015-08-17T12:26:01Z"
-               },
-               {
-                       "checksumSHA1": "blwbl9vPvRLtL5QlZgfpLvsFiZ4=",
-                       "origin": "github.com/aws/aws-sdk-go/vendor/github.com/jmespath/go-jmespath",
-                       "path": "github.com/jmespath/go-jmespath",
-                       "revision": "d496c5aab9b8ba36936e457a488e971b4f9fd891",
-                       "revisionTime": "2019-03-06T20:18:39Z"
-               },
-               {
-                       "checksumSHA1": "X7g98YfLr+zM7aN76AZvAfpZyfk=",
-                       "path": "github.com/julienschmidt/httprouter",
-                       "revision": "adbc77eec0d91467376ca515bc3a14b8434d0f18",
-                       "revisionTime": "2018-04-11T15:45:01Z"
-               },
-               {
-                       "checksumSHA1": "oX6jFQD74oOApvDIhOzW2dXpg5Q=",
-                       "path": "github.com/kevinburke/ssh_config",
-                       "revision": "802051befeb51da415c46972b5caf36e7c33c53d",
-                       "revisionTime": "2017-10-13T21:14:58Z"
-               },
-               {
-                       "checksumSHA1": "IfZcD4U1dtllJKlPNeD2aU4Jn98=",
-                       "path": "github.com/lib/pq",
-                       "revision": "83612a56d3dd153a94a629cd64925371c9adad78",
-                       "revisionTime": "2017-11-26T05:04:59Z"
-               },
-               {
-                       "checksumSHA1": "AU3fA8Sm33Vj9PBoRPSeYfxLRuE=",
-                       "path": "github.com/lib/pq/oid",
-                       "revision": "83612a56d3dd153a94a629cd64925371c9adad78",
-                       "revisionTime": "2017-11-26T05:04:59Z"
-               },
-               {
-                       "checksumSHA1": "T9E+5mKBQ/BX4wlNxgaPfetxdeI=",
-                       "path": "github.com/marstr/guid",
-                       "revision": "8bdf7d1a087ccc975cf37dd6507da50698fd19ca",
-                       "revisionTime": "2017-04-27T23:51:15Z"
-               },
-               {
-                       "checksumSHA1": "bKMZjd2wPw13VwoE7mBeSv5djFA=",
-                       "path": "github.com/matttproud/golang_protobuf_extensions/pbutil",
-                       "revision": "c12348ce28de40eed0136aa2b644d0ee0650e56c",
-                       "revisionTime": "2016-04-24T11:30:07Z"
-               },
-               {
-                       "checksumSHA1": "V/quM7+em2ByJbWBLOsEwnY3j/Q=",
-                       "path": "github.com/mitchellh/go-homedir",
-                       "revision": "b8bc1bf767474819792c23f32d8286a45736f1c6",
-                       "revisionTime": "2016-12-03T19:45:07Z"
-               },
-               {
-                       "checksumSHA1": "OFNit1Qx2DdWhotfREKodDNUwCM=",
-                       "path": "github.com/opencontainers/go-digest",
-                       "revision": "279bed98673dd5bef374d3b6e4b09e2af76183bf",
-                       "revisionTime": "2017-06-07T19:53:33Z"
-               },
-               {
-                       "checksumSHA1": "ZGlIwSRjdLYCUII7JLE++N4w7Xc=",
-                       "path": "github.com/opencontainers/image-spec/specs-go",
-                       "revision": "577479e4dc273d3779f00c223c7e0dba4cd6b8b0",
-                       "revisionTime": "2017-11-25T02:40:18Z"
-               },
-               {
-                       "checksumSHA1": "jdbXRRzeu0njLE9/nCEZG+Yg/Jk=",
-                       "path": "github.com/opencontainers/image-spec/specs-go/v1",
-                       "revision": "577479e4dc273d3779f00c223c7e0dba4cd6b8b0",
-                       "revisionTime": "2017-11-25T02:40:18Z"
-               },
-               {
-                       "checksumSHA1": "F1IYMLBLAZaTOWnmXsgaxTGvrWI=",
-                       "path": "github.com/pelletier/go-buffruneio",
-                       "revision": "c37440a7cf42ac63b919c752ca73a85067e05992",
-                       "revisionTime": "2017-02-27T22:03:11Z"
-               },
-               {
-                       "checksumSHA1": "xCv4GBFyw07vZkVtKF/XrUnkHRk=",
-                       "path": "github.com/pkg/errors",
-                       "revision": "e881fd58d78e04cf6d0de1217f8707c8cc2249bc",
-                       "revisionTime": "2017-12-16T07:03:16Z"
-               },
-               {
-                       "checksumSHA1": "Ajt29IHVbX99PUvzn8Gc/lMCXBY=",
-                       "path": "github.com/prometheus/client_golang/prometheus",
-                       "revision": "9bb6ab929dcbe1c8393cd9ef70387cb69811bd1c",
-                       "revisionTime": "2018-02-03T14:28:15Z"
-               },
-               {
-                       "checksumSHA1": "c3Ui7nnLiJ4CAGWZ8dGuEgqHd8s=",
-                       "path": "github.com/prometheus/client_golang/prometheus/promhttp",
-                       "revision": "9bb6ab929dcbe1c8393cd9ef70387cb69811bd1c",
-                       "revisionTime": "2018-02-03T14:28:15Z"
-               },
-               {
-                       "checksumSHA1": "DvwvOlPNAgRntBzt3b3OSRMS2N4=",
-                       "path": "github.com/prometheus/client_model/go",
-                       "revision": "99fa1f4be8e564e8a6b613da7fa6f46c9edafc6c",
-                       "revisionTime": "2017-11-17T10:05:41Z"
-               },
-               {
-                       "checksumSHA1": "xfnn0THnqNwjwimeTClsxahYrIo=",
-                       "path": "github.com/prometheus/common/expfmt",
-                       "revision": "89604d197083d4781071d3c65855d24ecfb0a563",
-                       "revisionTime": "2018-01-10T21:49:58Z"
-               },
-               {
-                       "checksumSHA1": "GWlM3d2vPYyNATtTFgftS10/A9w=",
-                       "path": "github.com/prometheus/common/internal/bitbucket.org/ww/goautoneg",
-                       "revision": "89604d197083d4781071d3c65855d24ecfb0a563",
-                       "revisionTime": "2018-01-10T21:49:58Z"
-               },
-               {
-                       "checksumSHA1": "YU+/K48IMawQnToO4ETE6a+hhj4=",
-                       "path": "github.com/prometheus/common/model",
-                       "revision": "89604d197083d4781071d3c65855d24ecfb0a563",
-                       "revisionTime": "2018-01-10T21:49:58Z"
-               },
-               {
-                       "checksumSHA1": "lolK0h7LSVERIX8zLyVQ/+7wEyA=",
-                       "path": "github.com/prometheus/procfs",
-                       "revision": "cb4147076ac75738c9a7d279075a253c0cc5acbd",
-                       "revisionTime": "2018-01-25T13:30:57Z"
-               },
-               {
-                       "checksumSHA1": "lv9rIcjbVEGo8AT1UCUZXhXrfQc=",
-                       "path": "github.com/prometheus/procfs/internal/util",
-                       "revision": "cb4147076ac75738c9a7d279075a253c0cc5acbd",
-                       "revisionTime": "2018-01-25T13:30:57Z"
-               },
-               {
-                       "checksumSHA1": "BXJH5h2ri8SU5qC6kkDvTIGCky4=",
-                       "path": "github.com/prometheus/procfs/nfs",
-                       "revision": "cb4147076ac75738c9a7d279075a253c0cc5acbd",
-                       "revisionTime": "2018-01-25T13:30:57Z"
-               },
-               {
-                       "checksumSHA1": "yItvTQLUVqm/ArLEbvEhqG0T5a0=",
-                       "path": "github.com/prometheus/procfs/xfs",
-                       "revision": "cb4147076ac75738c9a7d279075a253c0cc5acbd",
-                       "revisionTime": "2018-01-25T13:30:57Z"
-               },
-               {
-                       "checksumSHA1": "eDQ6f1EsNf+frcRO/9XukSEchm8=",
-                       "path": "github.com/satori/go.uuid",
-                       "revision": "36e9d2ebbde5e3f13ab2e25625fd453271d6522e",
-                       "revisionTime": "2018-01-03T17:44:51Z"
-               },
-               {
-                       "checksumSHA1": "UwtyqB7CaUWPlw0DVJQvw0IFQZs=",
-                       "path": "github.com/sergi/go-diff/diffmatchpatch",
-                       "revision": "1744e2970ca51c86172c8190fadad617561ed6e7",
-                       "revisionTime": "2017-11-10T11:01:46Z"
-               },
-               {
-                       "checksumSHA1": "ySaT8G3I3y4MmnoXOYAAX0rC+p8=",
-                       "path": "github.com/sirupsen/logrus",
-                       "revision": "d682213848ed68c0a260ca37d6dd5ace8423f5ba",
-                       "revisionTime": "2017-12-05T20:32:29Z"
-               },
-               {
-                       "checksumSHA1": "8QeSG127zQqbA+YfkO1WkKx/iUI=",
-                       "path": "github.com/src-d/gcfg",
-                       "revision": "f187355171c936ac84a82793659ebb4936bc1c23",
-                       "revisionTime": "2016-10-26T10:01:55Z"
-               },
-               {
-                       "checksumSHA1": "yf5NBT8BofPfGYCXoLnj7BIA1wo=",
-                       "path": "github.com/src-d/gcfg/scanner",
-                       "revision": "f187355171c936ac84a82793659ebb4936bc1c23",
-                       "revisionTime": "2016-10-26T10:01:55Z"
-               },
-               {
-                       "checksumSHA1": "C5Z8YVyNTuvupM9AUr9KbPlps4Q=",
-                       "path": "github.com/src-d/gcfg/token",
-                       "revision": "f187355171c936ac84a82793659ebb4936bc1c23",
-                       "revisionTime": "2016-10-26T10:01:55Z"
-               },
-               {
-                       "checksumSHA1": "mDkN3UpR7auuFbwUuIwExz4DZgY=",
-                       "path": "github.com/src-d/gcfg/types",
-                       "revision": "f187355171c936ac84a82793659ebb4936bc1c23",
-                       "revisionTime": "2016-10-26T10:01:55Z"
-               },
-               {
-                       "checksumSHA1": "iHiMTBffQvWYlOLu3130JXuQpgQ=",
-                       "path": "github.com/xanzy/ssh-agent",
-                       "revision": "ba9c9e33906f58169366275e3450db66139a31a9",
-                       "revisionTime": "2015-12-15T15:34:51Z"
-               },
-               {
-                       "checksumSHA1": "TT1rac6kpQp2vz24m5yDGUNQ/QQ=",
-                       "path": "golang.org/x/crypto/cast5",
-                       "revision": "0fcca4842a8d74bfddc2c96a073bd2a4d2a7a2e8",
-                       "revisionTime": "2017-11-25T19:00:56Z"
-               },
-               {
-                       "checksumSHA1": "IQkUIOnvlf0tYloFx9mLaXSvXWQ=",
-                       "path": "golang.org/x/crypto/curve25519",
-                       "revision": "0fcca4842a8d74bfddc2c96a073bd2a4d2a7a2e8",
-                       "revisionTime": "2017-11-25T19:00:56Z"
-               },
-               {
-                       "checksumSHA1": "1hwn8cgg4EVXhCpJIqmMbzqnUo0=",
-                       "path": "golang.org/x/crypto/ed25519",
-                       "revision": "0fcca4842a8d74bfddc2c96a073bd2a4d2a7a2e8",
-                       "revisionTime": "2017-11-25T19:00:56Z"
-               },
-               {
-                       "checksumSHA1": "LXFcVx8I587SnWmKycSDEq9yvK8=",
-                       "path": "golang.org/x/crypto/ed25519/internal/edwards25519",
-                       "revision": "0fcca4842a8d74bfddc2c96a073bd2a4d2a7a2e8",
-                       "revisionTime": "2017-11-25T19:00:56Z"
-               },
-               {
-                       "checksumSHA1": "ooU7jaiYSUKlg5BVllI8lsq+5Qk=",
-                       "path": "golang.org/x/crypto/openpgp",
-                       "revision": "0fcca4842a8d74bfddc2c96a073bd2a4d2a7a2e8",
-                       "revisionTime": "2017-11-25T19:00:56Z"
-               },
-               {
-                       "checksumSHA1": "olOKkhrdkYQHZ0lf1orrFQPQrv4=",
-                       "path": "golang.org/x/crypto/openpgp/armor",
-                       "revision": "0fcca4842a8d74bfddc2c96a073bd2a4d2a7a2e8",
-                       "revisionTime": "2017-11-25T19:00:56Z"
-               },
-               {
-                       "checksumSHA1": "eo/KtdjieJQXH7Qy+faXFcF70ME=",
-                       "path": "golang.org/x/crypto/openpgp/elgamal",
-                       "revision": "0fcca4842a8d74bfddc2c96a073bd2a4d2a7a2e8",
-                       "revisionTime": "2017-11-25T19:00:56Z"
-               },
-               {
-                       "checksumSHA1": "rlxVSaGgqdAgwblsErxTxIfuGfg=",
-                       "path": "golang.org/x/crypto/openpgp/errors",
-                       "revision": "0fcca4842a8d74bfddc2c96a073bd2a4d2a7a2e8",
-                       "revisionTime": "2017-11-25T19:00:56Z"
-               },
-               {
-                       "checksumSHA1": "Pq88+Dgh04UdXWZN6P+bLgYnbRc=",
-                       "path": "golang.org/x/crypto/openpgp/packet",
-                       "revision": "0fcca4842a8d74bfddc2c96a073bd2a4d2a7a2e8",
-                       "revisionTime": "2017-11-25T19:00:56Z"
-               },
-               {
-                       "checksumSHA1": "s2qT4UwvzBSkzXuiuMkowif1Olw=",
-                       "path": "golang.org/x/crypto/openpgp/s2k",
-                       "revision": "0fcca4842a8d74bfddc2c96a073bd2a4d2a7a2e8",
-                       "revisionTime": "2017-11-25T19:00:56Z"
-               },
-               {
-                       "checksumSHA1": "PJY7uCr3UnX4/Mf/RoWnbieSZ8o=",
-                       "path": "golang.org/x/crypto/pkcs12",
-                       "revision": "614d502a4dac94afa3a6ce146bd1736da82514c6",
-                       "revisionTime": "2018-07-28T08:01:47Z"
-               },
-               {
-                       "checksumSHA1": "p0GC51McIdA7JygoP223twJ1s0E=",
-                       "path": "golang.org/x/crypto/pkcs12/internal/rc2",
-                       "revision": "614d502a4dac94afa3a6ce146bd1736da82514c6",
-                       "revisionTime": "2018-07-28T08:01:47Z"
-               },
-               {
-                       "checksumSHA1": "NHjGg73p5iGZ+7tflJ4cVABNmKE=",
-                       "path": "golang.org/x/crypto/ssh",
-                       "revision": "0fcca4842a8d74bfddc2c96a073bd2a4d2a7a2e8",
-                       "revisionTime": "2017-11-25T19:00:56Z"
-               },
-               {
-                       "checksumSHA1": "NMRX0onGReaL9IfLr0XQ3kl5Id0=",
-                       "path": "golang.org/x/crypto/ssh/agent",
-                       "revision": "0fcca4842a8d74bfddc2c96a073bd2a4d2a7a2e8",
-                       "revisionTime": "2017-11-25T19:00:56Z"
-               },
-               {
-                       "checksumSHA1": "zBHtHvMj+MXa1qa4aglBt46uUck=",
-                       "path": "golang.org/x/crypto/ssh/knownhosts",
-                       "revision": "0fcca4842a8d74bfddc2c96a073bd2a4d2a7a2e8",
-                       "revisionTime": "2017-11-25T19:00:56Z"
-               },
-               {
-                       "checksumSHA1": "X1NTlfcau2XcV6WtAHF6b/DECOA=",
-                       "path": "golang.org/x/crypto/ssh/terminal",
-                       "revision": "0fcca4842a8d74bfddc2c96a073bd2a4d2a7a2e8",
-                       "revisionTime": "2017-11-25T19:00:56Z"
-               },
-               {
-                       "checksumSHA1": "Y+HGqEkYM15ir+J93MEaHdyFy0c=",
-                       "origin": "github.com/docker/docker/vendor/golang.org/x/net/context",
-                       "path": "golang.org/x/net/context",
-                       "revision": "94b8a116fbf1cd90e68d8f5361b520d326a66f9b",
-                       "revisionTime": "2018-01-09T01:38:17Z"
-               },
-               {
-                       "checksumSHA1": "WHc3uByvGaMcnSoI21fhzYgbOgg=",
-                       "path": "golang.org/x/net/context/ctxhttp",
-                       "revision": "434ec0c7fe3742c984919a691b2018a6e9694425",
-                       "revisionTime": "2017-09-25T09:26:47Z"
-               },
-               {
-                       "checksumSHA1": "r9l4r3H6FOLQ0c2JaoXpopFjpnw=",
-                       "path": "golang.org/x/net/proxy",
-                       "revision": "434ec0c7fe3742c984919a691b2018a6e9694425",
-                       "revisionTime": "2017-09-25T09:26:47Z"
-               },
-               {
-                       "checksumSHA1": "TBlnCuZUOzJHLu5DNY7XEj8TvbU=",
-                       "path": "golang.org/x/net/webdav",
-                       "revision": "434ec0c7fe3742c984919a691b2018a6e9694425",
-                       "revisionTime": "2017-09-25T09:26:47Z"
-               },
-               {
-                       "checksumSHA1": "XgtZlzd39qIkBHs6XYrq9dhTCog=",
-                       "path": "golang.org/x/net/webdav/internal/xml",
-                       "revision": "434ec0c7fe3742c984919a691b2018a6e9694425",
-                       "revisionTime": "2017-09-25T09:26:47Z"
-               },
-               {
-                       "checksumSHA1": "7EZyXN0EmZLgGxZxK01IJua4c8o=",
-                       "path": "golang.org/x/net/websocket",
-                       "revision": "434ec0c7fe3742c984919a691b2018a6e9694425",
-                       "revisionTime": "2017-09-25T09:26:47Z"
-               },
-               {
-                       "checksumSHA1": "znPq37/LZ4pJh7B4Lbu0ZuoMhNk=",
-                       "origin": "github.com/docker/docker/vendor/golang.org/x/sys/unix",
-                       "path": "golang.org/x/sys/unix",
-                       "revision": "94b8a116fbf1cd90e68d8f5361b520d326a66f9b",
-                       "revisionTime": "2018-01-09T01:38:17Z"
-               },
-               {
-                       "checksumSHA1": "8BcMOi8XTSigDtV2npDc8vMrS60=",
-                       "origin": "github.com/docker/docker/vendor/golang.org/x/sys/windows",
-                       "path": "golang.org/x/sys/windows",
-                       "revision": "94b8a116fbf1cd90e68d8f5361b520d326a66f9b",
-                       "revisionTime": "2018-01-09T01:38:17Z"
-               },
-               {
-                       "checksumSHA1": "ziMb9+ANGRJSSIuxYdRbA+cDRBQ=",
-                       "path": "golang.org/x/text/transform",
-                       "revision": "e19ae1496984b1c655b8044a65c0300a3c878dd3",
-                       "revisionTime": "2017-12-24T20:31:28Z"
-               },
-               {
-                       "checksumSHA1": "BCNYmf4Ek93G4lk5x3ucNi/lTwA=",
-                       "path": "golang.org/x/text/unicode/norm",
-                       "revision": "e19ae1496984b1c655b8044a65c0300a3c878dd3",
-                       "revisionTime": "2017-12-24T20:31:28Z"
-               },
-               {
-                       "checksumSHA1": "CEFTYXtWmgSh+3Ik1NmDaJcz4E0=",
-                       "path": "gopkg.in/check.v1",
-                       "revision": "20d25e2804050c1cd24a7eea1e7a6447dd0e74ec",
-                       "revisionTime": "2016-12-08T18:13:25Z"
-               },
-               {
-                       "checksumSHA1": "GdsHg+yOsZtdMvD9HJFovPsqKec=",
-                       "path": "gopkg.in/src-d/go-billy.v4",
-                       "revision": "053dbd006f81a230434f712314aacfb540b52cc5",
-                       "revisionTime": "2017-11-27T19:20:57Z"
-               },
-               {
-                       "checksumSHA1": "yscejfasrttJfPq91pn7gArFb5o=",
-                       "path": "gopkg.in/src-d/go-billy.v4/helper/chroot",
-                       "revision": "053dbd006f81a230434f712314aacfb540b52cc5",
-                       "revisionTime": "2017-11-27T19:20:57Z"
-               },
-               {
-                       "checksumSHA1": "B7HAyGfl+ONIAvlHzbvSsLisx9o=",
-                       "path": "gopkg.in/src-d/go-billy.v4/helper/polyfill",
-                       "revision": "053dbd006f81a230434f712314aacfb540b52cc5",
-                       "revisionTime": "2017-11-27T19:20:57Z"
-               },
-               {
-                       "checksumSHA1": "1CnG3JdmIQoa6mE0O98BfymLmuM=",
-                       "path": "gopkg.in/src-d/go-billy.v4/osfs",
-                       "revision": "053dbd006f81a230434f712314aacfb540b52cc5",
-                       "revisionTime": "2017-11-27T19:20:57Z"
-               },
-               {
-                       "checksumSHA1": "lo42NuhQJppy2ne/uwPR2T9BSPY=",
-                       "path": "gopkg.in/src-d/go-billy.v4/util",
-                       "revision": "053dbd006f81a230434f712314aacfb540b52cc5",
-                       "revisionTime": "2017-11-27T19:20:57Z"
-               },
-               {
-                       "checksumSHA1": "ydjzL2seh3M8h9svrSDV5y/KQJU=",
-                       "path": "gopkg.in/src-d/go-git.v4",
-                       "revision": "bf3b1f1fb9e0a04d0f87511a7ded2562b48a19d8",
-                       "revisionTime": "2018-01-08T13:05:52Z"
-               },
-               {
-                       "checksumSHA1": "TSoIlaADKlw3Zx0ysCCBn6kyXNE=",
-                       "path": "gopkg.in/src-d/go-git.v4/config",
-                       "revision": "bf3b1f1fb9e0a04d0f87511a7ded2562b48a19d8",
-                       "revisionTime": "2018-01-08T13:05:52Z"
-               },
-               {
-                       "checksumSHA1": "B2OLPJ4wnJIM2TMjTyzusYluUeI=",
-                       "path": "gopkg.in/src-d/go-git.v4/internal/revision",
-                       "revision": "bf3b1f1fb9e0a04d0f87511a7ded2562b48a19d8",
-                       "revisionTime": "2018-01-08T13:05:52Z"
-               },
-               {
-                       "checksumSHA1": "o9YH41kQMefVGUS7d3WWSLLhIRk=",
-                       "path": "gopkg.in/src-d/go-git.v4/plumbing",
-                       "revision": "bf3b1f1fb9e0a04d0f87511a7ded2562b48a19d8",
-                       "revisionTime": "2018-01-08T13:05:52Z"
-               },
-               {
-                       "checksumSHA1": "BrsKLhmB0BtaMY+ol1oglnHhvrs=",
-                       "path": "gopkg.in/src-d/go-git.v4/plumbing/cache",
-                       "revision": "bf3b1f1fb9e0a04d0f87511a7ded2562b48a19d8",
-                       "revisionTime": "2018-01-08T13:05:52Z"
-               },
-               {
-                       "checksumSHA1": "pHPMiAzXG/TJqTLEKj2SHjxX4zs=",
-                       "path": "gopkg.in/src-d/go-git.v4/plumbing/filemode",
-                       "revision": "bf3b1f1fb9e0a04d0f87511a7ded2562b48a19d8",
-                       "revisionTime": "2018-01-08T13:05:52Z"
-               },
-               {
-                       "checksumSHA1": "UGIM9BX7w3MhiadsuN6f8Bx0VZU=",
-                       "path": "gopkg.in/src-d/go-git.v4/plumbing/format/config",
-                       "revision": "bf3b1f1fb9e0a04d0f87511a7ded2562b48a19d8",
-                       "revisionTime": "2018-01-08T13:05:52Z"
-               },
-               {
-                       "checksumSHA1": "L1H7nPf65//6nQGt3Lzq16vLD8w=",
-                       "path": "gopkg.in/src-d/go-git.v4/plumbing/format/diff",
-                       "revision": "bf3b1f1fb9e0a04d0f87511a7ded2562b48a19d8",
-                       "revisionTime": "2018-01-08T13:05:52Z"
-               },
-               {
-                       "checksumSHA1": "87WhYdropmGA4peZOembY5hEgq8=",
-                       "path": "gopkg.in/src-d/go-git.v4/plumbing/format/gitignore",
-                       "revision": "bf3b1f1fb9e0a04d0f87511a7ded2562b48a19d8",
-                       "revisionTime": "2018-01-08T13:05:52Z"
-               },
-               {
-                       "checksumSHA1": "G0TX3efLdk7noo/n1Dt9Tzempig=",
-                       "path": "gopkg.in/src-d/go-git.v4/plumbing/format/idxfile",
-                       "revision": "bf3b1f1fb9e0a04d0f87511a7ded2562b48a19d8",
-                       "revisionTime": "2018-01-08T13:05:52Z"
-               },
-               {
-                       "checksumSHA1": "q7HtzrSzVE9qN5N3QOxkLFcZI1U=",
-                       "path": "gopkg.in/src-d/go-git.v4/plumbing/format/index",
-                       "revision": "bf3b1f1fb9e0a04d0f87511a7ded2562b48a19d8",
-                       "revisionTime": "2018-01-08T13:05:52Z"
-               },
-               {
-                       "checksumSHA1": "0IxJpGMfdnr3cuuVE59u+1B5n9o=",
-                       "path": "gopkg.in/src-d/go-git.v4/plumbing/format/objfile",
-                       "revision": "bf3b1f1fb9e0a04d0f87511a7ded2562b48a19d8",
-                       "revisionTime": "2018-01-08T13:05:52Z"
-               },
-               {
-                       "checksumSHA1": "LJnyldAM69WmMXW5avaEeSScKTU=",
-                       "path": "gopkg.in/src-d/go-git.v4/plumbing/format/packfile",
-                       "revision": "bf3b1f1fb9e0a04d0f87511a7ded2562b48a19d8",
-                       "revisionTime": "2018-01-08T13:05:52Z"
-               },
-               {
-                       "checksumSHA1": "T8efjPxCKp23RvSBI51qugHzgxw=",
-                       "path": "gopkg.in/src-d/go-git.v4/plumbing/format/pktline",
-                       "revision": "bf3b1f1fb9e0a04d0f87511a7ded2562b48a19d8",
-                       "revisionTime": "2018-01-08T13:05:52Z"
-               },
-               {
-                       "checksumSHA1": "97LEL3gxgDWPP/UlRHMfKb5I0RA=",
-                       "path": "gopkg.in/src-d/go-git.v4/plumbing/object",
-                       "revision": "bf3b1f1fb9e0a04d0f87511a7ded2562b48a19d8",
-                       "revisionTime": "2018-01-08T13:05:52Z"
-               },
-               {
-                       "checksumSHA1": "PQmY1mHiPdNBNrh3lESZe3QH36c=",
-                       "path": "gopkg.in/src-d/go-git.v4/plumbing/protocol/packp",
-                       "revision": "bf3b1f1fb9e0a04d0f87511a7ded2562b48a19d8",
-                       "revisionTime": "2018-01-08T13:05:52Z"
-               },
-               {
-                       "checksumSHA1": "JjHHYoWDYf0H//nP2FIS05ZLgj8=",
-                       "path": "gopkg.in/src-d/go-git.v4/plumbing/protocol/packp/capability",
-                       "revision": "bf3b1f1fb9e0a04d0f87511a7ded2562b48a19d8",
-                       "revisionTime": "2018-01-08T13:05:52Z"
-               },
-               {
-                       "checksumSHA1": "wVfbzV5BNhjW/HFFJuTCjkPSJ5M=",
-                       "path": "gopkg.in/src-d/go-git.v4/plumbing/protocol/packp/sideband",
-                       "revision": "bf3b1f1fb9e0a04d0f87511a7ded2562b48a19d8",
-                       "revisionTime": "2018-01-08T13:05:52Z"
-               },
-               {
-                       "checksumSHA1": "m8nTTRFD7kmX9nT5Yfr9lqabR4s=",
-                       "path": "gopkg.in/src-d/go-git.v4/plumbing/revlist",
-                       "revision": "bf3b1f1fb9e0a04d0f87511a7ded2562b48a19d8",
-                       "revisionTime": "2018-01-08T13:05:52Z"
-               },
-               {
-                       "checksumSHA1": "Xito+BwVCMpKrhcvgz5wU+MRmEo=",
-                       "path": "gopkg.in/src-d/go-git.v4/plumbing/storer",
-                       "revision": "bf3b1f1fb9e0a04d0f87511a7ded2562b48a19d8",
-                       "revisionTime": "2018-01-08T13:05:52Z"
-               },
-               {
-                       "checksumSHA1": "AVSX04sTj3cBv1muAmIbPE9D9FY=",
-                       "path": "gopkg.in/src-d/go-git.v4/plumbing/transport",
-                       "revision": "bf3b1f1fb9e0a04d0f87511a7ded2562b48a19d8",
-                       "revisionTime": "2018-01-08T13:05:52Z"
-               },
-               {
-                       "checksumSHA1": "cmOntUALmiRvvblEXAQXNO4Oous=",
-                       "path": "gopkg.in/src-d/go-git.v4/plumbing/transport/client",
-                       "revision": "bf3b1f1fb9e0a04d0f87511a7ded2562b48a19d8",
-                       "revisionTime": "2018-01-08T13:05:52Z"
-               },
-               {
-                       "checksumSHA1": "gaKy+c/OjPQFLhENnSAFEZUngok=",
-                       "path": "gopkg.in/src-d/go-git.v4/plumbing/transport/file",
-                       "revision": "bf3b1f1fb9e0a04d0f87511a7ded2562b48a19d8",
-                       "revisionTime": "2018-01-08T13:05:52Z"
-               },
-               {
-                       "checksumSHA1": "chcAwbm6J5uXXn6IV58+G6RKCjU=",
-                       "path": "gopkg.in/src-d/go-git.v4/plumbing/transport/git",
-                       "revision": "bf3b1f1fb9e0a04d0f87511a7ded2562b48a19d8",
-                       "revisionTime": "2018-01-08T13:05:52Z"
-               },
-               {
-                       "checksumSHA1": "m9TNeIIGUBdZ0qdSl5Xa/0TIvfo=",
-                       "path": "gopkg.in/src-d/go-git.v4/plumbing/transport/http",
-                       "revision": "bf3b1f1fb9e0a04d0f87511a7ded2562b48a19d8",
-                       "revisionTime": "2018-01-08T13:05:52Z"
-               },
-               {
-                       "checksumSHA1": "6asrmcjb98FpRr83ICCODXdGWdE=",
-                       "path": "gopkg.in/src-d/go-git.v4/plumbing/transport/internal/common",
-                       "revision": "bf3b1f1fb9e0a04d0f87511a7ded2562b48a19d8",
-                       "revisionTime": "2018-01-08T13:05:52Z"
-               },
-               {
-                       "checksumSHA1": "MGiWWrsy8iQ5ZdCXEN2Oc4oprCk=",
-                       "path": "gopkg.in/src-d/go-git.v4/plumbing/transport/server",
-                       "revision": "bf3b1f1fb9e0a04d0f87511a7ded2562b48a19d8",
-                       "revisionTime": "2018-01-08T13:05:52Z"
-               },
-               {
-                       "checksumSHA1": "vat8YhxXGXNcg8HvCDfHAR6BcL0=",
-                       "path": "gopkg.in/src-d/go-git.v4/plumbing/transport/ssh",
-                       "revision": "bf3b1f1fb9e0a04d0f87511a7ded2562b48a19d8",
-                       "revisionTime": "2018-01-08T13:05:52Z"
-               },
-               {
-                       "checksumSHA1": "FlVLBdu4cjlXj9zjRRNDurRLABU=",
-                       "path": "gopkg.in/src-d/go-git.v4/storage",
-                       "revision": "bf3b1f1fb9e0a04d0f87511a7ded2562b48a19d8",
-                       "revisionTime": "2018-01-08T13:05:52Z"
-               },
-               {
-                       "checksumSHA1": "IpSxC31PynwJBajOaHR7gtnVc7I=",
-                       "path": "gopkg.in/src-d/go-git.v4/storage/filesystem",
-                       "revision": "bf3b1f1fb9e0a04d0f87511a7ded2562b48a19d8",
-                       "revisionTime": "2018-01-08T13:05:52Z"
-               },
-               {
-                       "checksumSHA1": "OaZO6dgvn6PMvezw0bYQUGLSrF0=",
-                       "path": "gopkg.in/src-d/go-git.v4/storage/filesystem/internal/dotgit",
-                       "revision": "bf3b1f1fb9e0a04d0f87511a7ded2562b48a19d8",
-                       "revisionTime": "2018-01-08T13:05:52Z"
-               },
-               {
-                       "checksumSHA1": "jPRm9YqpcJzx4oasd6PBdD33Dgo=",
-                       "path": "gopkg.in/src-d/go-git.v4/storage/memory",
-                       "revision": "bf3b1f1fb9e0a04d0f87511a7ded2562b48a19d8",
-                       "revisionTime": "2018-01-08T13:05:52Z"
-               },
-               {
-                       "checksumSHA1": "AzdUpuGqSNnNK6DgdNjWrn99i3o=",
-                       "path": "gopkg.in/src-d/go-git.v4/utils/binary",
-                       "revision": "bf3b1f1fb9e0a04d0f87511a7ded2562b48a19d8",
-                       "revisionTime": "2018-01-08T13:05:52Z"
-               },
-               {
-                       "checksumSHA1": "vniUxB6bbDYazl21cOfmhdZZiY8=",
-                       "path": "gopkg.in/src-d/go-git.v4/utils/diff",
-                       "revision": "bf3b1f1fb9e0a04d0f87511a7ded2562b48a19d8",
-                       "revisionTime": "2018-01-08T13:05:52Z"
-               },
-               {
-                       "checksumSHA1": "cspCXRxvzvoNOEUB7wRgOKYrVjQ=",
-                       "path": "gopkg.in/src-d/go-git.v4/utils/ioutil",
-                       "revision": "bf3b1f1fb9e0a04d0f87511a7ded2562b48a19d8",
-                       "revisionTime": "2018-01-08T13:05:52Z"
-               },
-               {
-                       "checksumSHA1": "shsY2I1OFbnjopNWF21Tkfx+tac=",
-                       "path": "gopkg.in/src-d/go-git.v4/utils/merkletrie",
-                       "revision": "bf3b1f1fb9e0a04d0f87511a7ded2562b48a19d8",
-                       "revisionTime": "2018-01-08T13:05:52Z"
-               },
-               {
-                       "checksumSHA1": "QiHHx1Qb/Vv4W6uQb+mJU2zMqLo=",
-                       "path": "gopkg.in/src-d/go-git.v4/utils/merkletrie/filesystem",
-                       "revision": "bf3b1f1fb9e0a04d0f87511a7ded2562b48a19d8",
-                       "revisionTime": "2018-01-08T13:05:52Z"
-               },
-               {
-                       "checksumSHA1": "M+6y9mdBFksksEGBceBh9Se3W7Y=",
-                       "path": "gopkg.in/src-d/go-git.v4/utils/merkletrie/index",
-                       "revision": "bf3b1f1fb9e0a04d0f87511a7ded2562b48a19d8",
-                       "revisionTime": "2018-01-08T13:05:52Z"
-               },
-               {
-                       "checksumSHA1": "7eEw/xsSrFLfSppRf/JIt9u7lbU=",
-                       "path": "gopkg.in/src-d/go-git.v4/utils/merkletrie/internal/frame",
-                       "revision": "bf3b1f1fb9e0a04d0f87511a7ded2562b48a19d8",
-                       "revisionTime": "2018-01-08T13:05:52Z"
-               },
-               {
-                       "checksumSHA1": "qCb9d3cwnPHVLqS/U9NAzK+1Ptg=",
-                       "path": "gopkg.in/src-d/go-git.v4/utils/merkletrie/noder",
-                       "revision": "bf3b1f1fb9e0a04d0f87511a7ded2562b48a19d8",
-                       "revisionTime": "2018-01-08T13:05:52Z"
-               },
-               {
-                       "checksumSHA1": "I4c3qsEX8KAUTeB9+2pwVX/2ojU=",
-                       "path": "gopkg.in/warnings.v0",
-                       "revision": "ec4a0fea49c7b46c2aeb0b51aac55779c607e52b",
-                       "revisionTime": "2017-11-15T19:30:34Z"
-               },
-               {
-                       "checksumSHA1": "qOmvuDm+F+2nQQecUZBVkZrTn6Y=",
-                       "path": "gopkg.in/yaml.v2",
-                       "revision": "d670f9405373e636a5a2765eea47fac0c9bc91a4",
-                       "revisionTime": "2018-01-09T11:43:31Z"
-               },
-               {
-                       "checksumSHA1": "rBIcwbUjE9w1aV0qh7lAL1hcxCQ=",
-                       "path": "rsc.io/getopt",
-                       "revision": "20be20937449f18bb9967c10d732849fb4401e63",
-                       "revisionTime": "2017-08-11T00:05:52Z"
-               }
-       ],
-       "rootPath": "git.curoverse.com/arvados.git"
-}