<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] %>
# 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 \
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)"
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)
}
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"
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
;;
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
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
[[ -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" \
fi
do_test gofmt
- do_test govendor
do_test doc
do_test sdk/ruby
do_test sdk/R
- 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
--- /dev/null
+{
+ "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
--- /dev/null
+---
+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
=> 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
--- /dev/null
+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
--- /dev/null
+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=
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
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 {
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.")
+ }
}
}
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 {
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
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 {
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))
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(""))
"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,
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
"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"
}
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 {
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
return conn.chooseBackend(options.UUID).APIClientAuthorizationCurrent(ctx, options)
}
-type backend interface{ arvados.API }
+type backend interface {
+ arvados.API
+ BaseURL() url.URL
+}
type notFoundError struct{}
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)
--- /dev/null
+// 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)
+ }
+}
if h.Cluster.EnableBetaController14287 {
mux.Handle("/arvados/v1/collections", rtr)
mux.Handle("/arvados/v1/collections/", rtr)
+ mux.Handle("/login", rtr)
}
hs := http.NotFoundHandler()
}
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) {
--- /dev/null
+// 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)
+ }
+}
--- /dev/null
+// 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)
+}
--- /dev/null
+// 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
+}
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
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)
}
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)
}
}
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) {
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{} },
rtr.sendError(w, err)
return
}
- rtr.sendResponse(w, resp, respOpts)
+ rtr.sendResponse(w, req, resp, respOpts)
})
}
}
type Conn struct {
+ SendHeader http.Header
clusterID string
httpClient http.Client
baseURL url.URL
}
}
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,
}
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 {
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
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
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
+}
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,
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", ""}
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)
// arvadosclient.ArvadosClient.)
KeepServiceURIs []string `json:",omitempty"`
+ // HTTP headers to add/override in outgoing requests.
+ SendHeader http.Header
+
dd *DiscoveryDocument
ctx context.Context
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 {
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,
}
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)
}
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
--- /dev/null
+// 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())
+ }
+}
"context"
"encoding/json"
"errors"
+ "net/url"
"reflect"
"runtime"
"sync"
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
--- /dev/null
+// 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
+}
BaseApiClient(ConfigProvider config) {
this.config = config;
- client = OkHttpClientFactory.builder()
- .build()
- .create(config.isApiHostInsecure());
+ this.client = OkHttpClientFactory.INSTANCE.create(config.isApiHostInsecure());
}
Request.Builder getRequestBuilder() {
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;
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");
// 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);
}
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()";
- }
- }
}
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.
*
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);
@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);
# 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',
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
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
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
# alternate_emails
# identity_url
- info = info.with_indifferent_access
-
primary_user = nil
# local database
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
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
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
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
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
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
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 {
// 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
}
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
// 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,
}
}
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
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
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 {
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 {
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)
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("===")
}
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 {
shouldPullMounts []string
shouldTrashMounts []string
- expectResult balanceResult
+ expectBlockState *balancedBlockState
+ expectClassState map[string]balancedBlockState
}
func (bal *balancerSuite) SetUpSuite(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) {
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) {
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{
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) {
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.
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.
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) {
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)
}
}
} 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)
}
}
}
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
Value string
}
Summary struct {
- SampleCount string `json:"sample_count"`
- SampleSum float64 `json:"sample_sum"`
- Quantile []struct {
- Quantile float64
- Value float64
- }
+ SampleCount string
+ SampleSum float64
}
}
}
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
}
}
#
# 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
+++ /dev/null
-*
-!vendor.json
-!.gitignore
+++ /dev/null
-{
- "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"
-}