[ARVADOS] updated: 1.1.4-499-ge1633d0a8

Git user git at public.curoverse.com
Thu Jun 28 14:55:12 EDT 2018


Summary of changes:
 lib/controller/federation.go        |  90 ++++++++++++++++++++++++++
 lib/controller/federation_test.go   |  61 ++++++++++++++++--
 lib/controller/handler.go           | 123 ++++++++----------------------------
 lib/controller/handler_test.go      |   5 +-
 lib/controller/proxy.go             |  79 +++++++++++++++++++++++
 lib/controller/server_test.go       |  53 ++++++++++++++++
 sdk/go/arvados/config.go            |   7 +-
 sdk/go/auth/auth.go                 |  24 +++++--
 sdk/go/auth/salt.go                 |  45 +++++++++++++
 sdk/python/tests/run_test_server.py |   1 +
 10 files changed, 379 insertions(+), 109 deletions(-)
 create mode 100644 lib/controller/federation.go
 create mode 100644 lib/controller/proxy.go
 create mode 100644 lib/controller/server_test.go
 create mode 100644 sdk/go/auth/salt.go

  discards  20bef384d2b71b8357d701e52ec6b46e44b1b617 (commit)
       via  e1633d0a8365f63db56777524b1281d90bd9ee46 (commit)
       via  890195dc78d25d9a8f1e513197e4adc71be4f146 (commit)
       via  967821ec0ac00aef677f943e709ea079ebc30f78 (commit)
       via  59cf374273bfae0baec8370526a747bbb8e5edb7 (commit)
       via  a6ab70e907a47e8e28aae2dd3eb357aa72c8b673 (commit)

This update added new revisions after undoing existing revisions.  That is
to say, the old revision is not a strict subset of the new revision.  This
situation occurs when you --force push a change and generate a repository
containing something like this:

 * -- * -- B -- O -- O -- O (20bef384d2b71b8357d701e52ec6b46e44b1b617)
            \
             N -- N -- N (e1633d0a8365f63db56777524b1281d90bd9ee46)

When this happens we assume that you've already had alert emails for all
of the O revisions, and so we here report only the revisions in the N
branch from the common base, B.

Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.


commit e1633d0a8365f63db56777524b1281d90bd9ee46
Author: Tom Clegg <tclegg at veritasgenetics.com>
Date:   Thu Jun 28 14:53:08 2018 -0400

    13493: Verify TLS unless config specifies insecure.
    
    Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tclegg at veritasgenetics.com>

diff --git a/lib/controller/federation.go b/lib/controller/federation.go
index 008e50687..3571de624 100644
--- a/lib/controller/federation.go
+++ b/lib/controller/federation.go
@@ -45,7 +45,11 @@ func (h *Handler) proxyRemoteCluster(w http.ResponseWriter, req *http.Request, n
 		httpserver.Error(w, err.Error(), http.StatusBadRequest)
 		return
 	}
-	h.proxy(w, req, urlOut)
+	client := h.secureClient
+	if remote.Insecure {
+		client = h.insecureClient
+	}
+	h.proxy.Do(w, req, urlOut, client)
 }
 
 // Extract the auth token supplied in req, and replace it with a
diff --git a/lib/controller/handler.go b/lib/controller/handler.go
index feccbe608..78ad89e70 100644
--- a/lib/controller/handler.go
+++ b/lib/controller/handler.go
@@ -10,6 +10,7 @@ import (
 	"net/url"
 	"strings"
 	"sync"
+	"time"
 
 	"git.curoverse.com/arvados.git/sdk/go/arvados"
 	"git.curoverse.com/arvados.git/sdk/go/health"
@@ -20,9 +21,11 @@ type Handler struct {
 	Cluster     *arvados.Cluster
 	NodeProfile *arvados.NodeProfile
 
-	setupOnce    sync.Once
-	handlerStack http.Handler
-	proxyClient  *arvados.Client
+	setupOnce      sync.Once
+	handlerStack   http.Handler
+	proxy          *proxy
+	secureClient   *http.Client
+	insecureClient *http.Client
 }
 
 func (h *Handler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
@@ -32,7 +35,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
 
 func (h *Handler) CheckHealth() error {
 	h.setupOnce.Do(h.setup)
-	_, err := findRailsAPI(h.Cluster, h.NodeProfile)
+	_, _, err := findRailsAPI(h.Cluster, h.NodeProfile)
 	return err
 }
 
@@ -47,6 +50,19 @@ func (h *Handler) setup() {
 	hs = prepend(hs, h.proxyRemoteCluster)
 	mux.Handle("/", hs)
 	h.handlerStack = mux
+
+	sc := *arvados.DefaultSecureClient
+	sc.Timeout = time.Duration(h.Cluster.HTTPRequestTimeout)
+	h.secureClient = &sc
+
+	ic := *arvados.InsecureHTTPClient
+	ic.Timeout = time.Duration(h.Cluster.HTTPRequestTimeout)
+	h.insecureClient = &ic
+
+	h.proxy = &proxy{
+		Name:           "arvados-controller",
+		RequestTimeout: time.Duration(h.Cluster.HTTPRequestTimeout),
+	}
 }
 
 type middlewareFunc func(http.ResponseWriter, *http.Request, http.Handler)
@@ -58,7 +74,7 @@ func prepend(next http.Handler, middleware middlewareFunc) http.Handler {
 }
 
 func (h *Handler) proxyRailsAPI(w http.ResponseWriter, req *http.Request, next http.Handler) {
-	urlOut, err := findRailsAPI(h.Cluster, h.NodeProfile)
+	urlOut, insecure, err := findRailsAPI(h.Cluster, h.NodeProfile)
 	if err != nil {
 		httpserver.Error(w, err.Error(), http.StatusInternalServerError)
 		return
@@ -70,12 +86,16 @@ func (h *Handler) proxyRailsAPI(w http.ResponseWriter, req *http.Request, next h
 		RawPath:  req.URL.RawPath,
 		RawQuery: req.URL.RawQuery,
 	}
-	h.proxy(w, req, urlOut)
+	client := h.secureClient
+	if insecure {
+		client = h.insecureClient
+	}
+	h.proxy.Do(w, req, urlOut, client)
 }
 
 // For now, findRailsAPI always uses the rails API running on this
 // node.
-func findRailsAPI(cluster *arvados.Cluster, np *arvados.NodeProfile) (*url.URL, error) {
+func findRailsAPI(cluster *arvados.Cluster, np *arvados.NodeProfile) (*url.URL, bool, error) {
 	hostport := np.RailsAPI.Listen
 	if len(hostport) > 1 && hostport[0] == ':' && strings.TrimRight(hostport[1:], "0123456789") == "" {
 		// ":12345" => connect to indicated port on localhost
@@ -83,11 +103,12 @@ func findRailsAPI(cluster *arvados.Cluster, np *arvados.NodeProfile) (*url.URL,
 	} else if _, _, err := net.SplitHostPort(hostport); err == nil {
 		// "[::1]:12345" => connect to indicated address & port
 	} else {
-		return nil, err
+		return nil, false, err
 	}
 	proto := "http"
 	if np.RailsAPI.TLS {
 		proto = "https"
 	}
-	return url.Parse(proto + "://" + hostport)
+	url, err := url.Parse(proto + "://" + hostport)
+	return url, np.RailsAPI.Insecure, err
 }
diff --git a/lib/controller/handler_test.go b/lib/controller/handler_test.go
index 4829fe0e4..c132b659d 100644
--- a/lib/controller/handler_test.go
+++ b/lib/controller/handler_test.go
@@ -38,7 +38,7 @@ func (s *HandlerSuite) SetUpTest(c *check.C) {
 		NodeProfiles: map[string]arvados.NodeProfile{
 			"*": {
 				Controller: arvados.SystemServiceInstance{Listen: ":"},
-				RailsAPI:   arvados.SystemServiceInstance{Listen: os.Getenv("ARVADOS_TEST_API_HOST"), TLS: true},
+				RailsAPI:   arvados.SystemServiceInstance{Listen: os.Getenv("ARVADOS_TEST_API_HOST"), TLS: true, Insecure: true},
 			},
 		},
 	}
@@ -70,7 +70,7 @@ func (s *HandlerSuite) TestRequestTimeout(c *check.C) {
 	err := json.Unmarshal(resp.Body.Bytes(), &jresp)
 	c.Check(err, check.IsNil)
 	c.Assert(len(jresp.Errors), check.Equals, 1)
-	c.Check(jresp.Errors[0], check.Matches, `.*context deadline exceeded`)
+	c.Check(jresp.Errors[0], check.Matches, `.*context deadline exceeded.*`)
 }
 
 func (s *HandlerSuite) TestProxyWithoutToken(c *check.C) {
diff --git a/lib/controller/proxy.go b/lib/controller/proxy.go
index 91949f0fa..7905f91d0 100644
--- a/lib/controller/proxy.go
+++ b/lib/controller/proxy.go
@@ -11,10 +11,14 @@ import (
 	"net/url"
 	"time"
 
-	"git.curoverse.com/arvados.git/sdk/go/arvados"
 	"git.curoverse.com/arvados.git/sdk/go/httpserver"
 )
 
+type proxy struct {
+	Name           string // to use in Via header
+	RequestTimeout time.Duration
+}
+
 // headers that shouldn't be forwarded when proxying. See
 // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers
 var dropHeaders = map[string]bool{
@@ -28,7 +32,7 @@ var dropHeaders = map[string]bool{
 	"Upgrade":           true,
 }
 
-func (h *Handler) proxy(w http.ResponseWriter, reqIn *http.Request, urlOut *url.URL) {
+func (p *proxy) Do(w http.ResponseWriter, reqIn *http.Request, urlOut *url.URL, client *http.Client) {
 	// Copy headers from incoming request, then add/replace proxy
 	// headers like Via and X-Forwarded-For.
 	hdrOut := http.Header{}
@@ -45,9 +49,9 @@ func (h *Handler) proxy(w http.ResponseWriter, reqIn *http.Request, urlOut *url.
 	hdrOut.Add("Via", reqIn.Proto+" arvados-controller")
 
 	ctx := reqIn.Context()
-	if timeout := h.Cluster.HTTPRequestTimeout; timeout > 0 {
+	if p.RequestTimeout > 0 {
 		var cancel context.CancelFunc
-		ctx, cancel = context.WithDeadline(ctx, time.Now().Add(time.Duration(timeout)))
+		ctx, cancel = context.WithDeadline(ctx, time.Now().Add(time.Duration(p.RequestTimeout)))
 		defer cancel()
 	}
 
@@ -57,7 +61,7 @@ func (h *Handler) proxy(w http.ResponseWriter, reqIn *http.Request, urlOut *url.
 		Header: hdrOut,
 		Body:   reqIn.Body,
 	}).WithContext(ctx)
-	resp, err := arvados.InsecureHTTPClient.Do(reqOut)
+	resp, err := client.Do(reqOut)
 	if err != nil {
 		httpserver.Error(w, err.Error(), http.StatusInternalServerError)
 		return
diff --git a/lib/controller/server_test.go b/lib/controller/server_test.go
index 55329656c..2ad922244 100644
--- a/lib/controller/server_test.go
+++ b/lib/controller/server_test.go
@@ -34,7 +34,7 @@ func newServerFromIntegrationTestEnv(c *check.C) *httpserver.Server {
 
 	nodeProfile := arvados.NodeProfile{
 		Controller: arvados.SystemServiceInstance{Listen: ":"},
-		RailsAPI:   arvados.SystemServiceInstance{Listen: os.Getenv("ARVADOS_TEST_API_HOST"), TLS: true},
+		RailsAPI:   arvados.SystemServiceInstance{Listen: os.Getenv("ARVADOS_TEST_API_HOST"), TLS: true, Insecure: true},
 	}
 	handler := &Handler{Cluster: &arvados.Cluster{
 		ClusterID: "zzzzz",
diff --git a/sdk/go/arvados/config.go b/sdk/go/arvados/config.go
index 83bf6292a..dfe2e7b31 100644
--- a/sdk/go/arvados/config.go
+++ b/sdk/go/arvados/config.go
@@ -65,6 +65,8 @@ type RemoteCluster struct {
 	Proxy bool
 	// Scheme, default "https". Can be set to "http" for testing.
 	Scheme string
+	// Disable TLS verify. Can be set to true for testing.
+	Insecure bool
 }
 
 type InstanceType struct {
@@ -141,6 +143,7 @@ func (np *NodeProfile) ServicePorts() map[ServiceName]string {
 }
 
 type SystemServiceInstance struct {
-	Listen string
-	TLS    bool
+	Listen   string
+	TLS      bool
+	Insecure bool
 }
diff --git a/sdk/python/tests/run_test_server.py b/sdk/python/tests/run_test_server.py
index f7ca6daf6..bad3738db 100644
--- a/sdk/python/tests/run_test_server.py
+++ b/sdk/python/tests/run_test_server.py
@@ -415,6 +415,7 @@ Clusters:
         "arvados-api-server":
           Listen: ":{}"
           TLS: true
+          Insecure: true
         """.format(port, rails_api_port))
     logf = open(_logfilename('controller'), 'a')
     controller = subprocess.Popen(

commit 890195dc78d25d9a8f1e513197e4adc71be4f146
Author: Tom Clegg <tclegg at veritasgenetics.com>
Date:   Thu Jun 28 10:02:36 2018 -0400

    13493: Move proxy and federation code to their own source files.
    
    Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tclegg at veritasgenetics.com>

diff --git a/lib/controller/federation.go b/lib/controller/federation.go
index 5ea305d03..008e50687 100644
--- a/lib/controller/federation.go
+++ b/lib/controller/federation.go
@@ -8,10 +8,46 @@ import (
 	"bytes"
 	"io/ioutil"
 	"net/http"
+	"net/url"
+	"regexp"
 
 	"git.curoverse.com/arvados.git/sdk/go/auth"
+	"git.curoverse.com/arvados.git/sdk/go/httpserver"
 )
 
+var wfRe = regexp.MustCompile(`^/arvados/v1/workflows/([0-9a-z]{5})-[^/]+$`)
+
+func (h *Handler) proxyRemoteCluster(w http.ResponseWriter, req *http.Request, next http.Handler) {
+	m := wfRe.FindStringSubmatch(req.URL.Path)
+	if len(m) < 2 || m[1] == h.Cluster.ClusterID {
+		next.ServeHTTP(w, req)
+		return
+	}
+	remoteID := m[1]
+	remote, ok := h.Cluster.RemoteClusters[remoteID]
+	if !ok {
+		httpserver.Error(w, "no proxy available for cluster "+remoteID, http.StatusNotFound)
+		return
+	}
+	scheme := remote.Scheme
+	if scheme == "" {
+		scheme = "https"
+	}
+	urlOut := &url.URL{
+		Scheme:   scheme,
+		Host:     remote.Host,
+		Path:     req.URL.Path,
+		RawPath:  req.URL.RawPath,
+		RawQuery: req.URL.RawQuery,
+	}
+	err := h.saltAuthToken(req, remoteID)
+	if err != nil {
+		httpserver.Error(w, err.Error(), http.StatusBadRequest)
+		return
+	}
+	h.proxy(w, req, urlOut)
+}
+
 // Extract the auth token supplied in req, and replace it with a
 // salted token for the remote cluster.
 func (h *Handler) saltAuthToken(req *http.Request, remote string) error {
diff --git a/lib/controller/handler.go b/lib/controller/handler.go
index ab6c72735..feccbe608 100644
--- a/lib/controller/handler.go
+++ b/lib/controller/handler.go
@@ -5,15 +5,11 @@
 package controller
 
 import (
-	"context"
-	"io"
 	"net"
 	"net/http"
 	"net/url"
-	"regexp"
 	"strings"
 	"sync"
-	"time"
 
 	"git.curoverse.com/arvados.git/sdk/go/arvados"
 	"git.curoverse.com/arvados.git/sdk/go/health"
@@ -53,19 +49,6 @@ func (h *Handler) setup() {
 	h.handlerStack = mux
 }
 
-// headers that shouldn't be forwarded when proxying. See
-// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers
-var dropHeaders = map[string]bool{
-	"Connection":          true,
-	"Keep-Alive":          true,
-	"Proxy-Authenticate":  true,
-	"Proxy-Authorization": true,
-	"TE":                true,
-	"Trailer":           true,
-	"Transfer-Encoding": true,
-	"Upgrade":           true,
-}
-
 type middlewareFunc func(http.ResponseWriter, *http.Request, http.Handler)
 
 func prepend(next http.Handler, middleware middlewareFunc) http.Handler {
@@ -74,39 +57,6 @@ func prepend(next http.Handler, middleware middlewareFunc) http.Handler {
 	})
 }
 
-var wfRe = regexp.MustCompile(`^/arvados/v1/workflows/([0-9a-z]{5})-[^/]+$`)
-
-func (h *Handler) proxyRemoteCluster(w http.ResponseWriter, req *http.Request, next http.Handler) {
-	m := wfRe.FindStringSubmatch(req.URL.Path)
-	if len(m) < 2 || m[1] == h.Cluster.ClusterID {
-		next.ServeHTTP(w, req)
-		return
-	}
-	remoteID := m[1]
-	remote, ok := h.Cluster.RemoteClusters[remoteID]
-	if !ok {
-		httpserver.Error(w, "no proxy available for cluster "+remoteID, http.StatusNotFound)
-		return
-	}
-	scheme := remote.Scheme
-	if scheme == "" {
-		scheme = "https"
-	}
-	urlOut := &url.URL{
-		Scheme:   scheme,
-		Host:     remote.Host,
-		Path:     req.URL.Path,
-		RawPath:  req.URL.RawPath,
-		RawQuery: req.URL.RawQuery,
-	}
-	err := h.saltAuthToken(req, remoteID)
-	if err != nil {
-		httpserver.Error(w, err.Error(), http.StatusBadRequest)
-		return
-	}
-	h.proxy(w, req, urlOut)
-}
-
 func (h *Handler) proxyRailsAPI(w http.ResponseWriter, req *http.Request, next http.Handler) {
 	urlOut, err := findRailsAPI(h.Cluster, h.NodeProfile)
 	if err != nil {
@@ -123,52 +73,6 @@ func (h *Handler) proxyRailsAPI(w http.ResponseWriter, req *http.Request, next h
 	h.proxy(w, req, urlOut)
 }
 
-func (h *Handler) proxy(w http.ResponseWriter, reqIn *http.Request, urlOut *url.URL) {
-	// Copy headers from incoming request, then add/replace proxy
-	// headers like Via and X-Forwarded-For.
-	hdrOut := http.Header{}
-	for k, v := range reqIn.Header {
-		if !dropHeaders[k] {
-			hdrOut[k] = v
-		}
-	}
-	xff := reqIn.RemoteAddr
-	if xffIn := reqIn.Header.Get("X-Forwarded-For"); xffIn != "" {
-		xff = xffIn + "," + xff
-	}
-	hdrOut.Set("X-Forwarded-For", xff)
-	hdrOut.Add("Via", reqIn.Proto+" arvados-controller")
-
-	ctx := reqIn.Context()
-	if timeout := h.Cluster.HTTPRequestTimeout; timeout > 0 {
-		var cancel context.CancelFunc
-		ctx, cancel = context.WithDeadline(ctx, time.Now().Add(time.Duration(timeout)))
-		defer cancel()
-	}
-
-	reqOut := (&http.Request{
-		Method: reqIn.Method,
-		URL:    urlOut,
-		Header: hdrOut,
-		Body:   reqIn.Body,
-	}).WithContext(ctx)
-	resp, err := arvados.InsecureHTTPClient.Do(reqOut)
-	if err != nil {
-		httpserver.Error(w, err.Error(), http.StatusInternalServerError)
-		return
-	}
-	for k, v := range resp.Header {
-		for _, v := range v {
-			w.Header().Add(k, v)
-		}
-	}
-	w.WriteHeader(resp.StatusCode)
-	n, err := io.Copy(w, resp.Body)
-	if err != nil {
-		httpserver.Logger(reqIn).WithError(err).WithField("bytesCopied", n).Error("error copying response body")
-	}
-}
-
 // For now, findRailsAPI always uses the rails API running on this
 // node.
 func findRailsAPI(cluster *arvados.Cluster, np *arvados.NodeProfile) (*url.URL, error) {
diff --git a/lib/controller/proxy.go b/lib/controller/proxy.go
new file mode 100644
index 000000000..91949f0fa
--- /dev/null
+++ b/lib/controller/proxy.go
@@ -0,0 +1,75 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+package controller
+
+import (
+	"context"
+	"io"
+	"net/http"
+	"net/url"
+	"time"
+
+	"git.curoverse.com/arvados.git/sdk/go/arvados"
+	"git.curoverse.com/arvados.git/sdk/go/httpserver"
+)
+
+// headers that shouldn't be forwarded when proxying. See
+// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers
+var dropHeaders = map[string]bool{
+	"Connection":          true,
+	"Keep-Alive":          true,
+	"Proxy-Authenticate":  true,
+	"Proxy-Authorization": true,
+	"TE":                true,
+	"Trailer":           true,
+	"Transfer-Encoding": true,
+	"Upgrade":           true,
+}
+
+func (h *Handler) proxy(w http.ResponseWriter, reqIn *http.Request, urlOut *url.URL) {
+	// Copy headers from incoming request, then add/replace proxy
+	// headers like Via and X-Forwarded-For.
+	hdrOut := http.Header{}
+	for k, v := range reqIn.Header {
+		if !dropHeaders[k] {
+			hdrOut[k] = v
+		}
+	}
+	xff := reqIn.RemoteAddr
+	if xffIn := reqIn.Header.Get("X-Forwarded-For"); xffIn != "" {
+		xff = xffIn + "," + xff
+	}
+	hdrOut.Set("X-Forwarded-For", xff)
+	hdrOut.Add("Via", reqIn.Proto+" arvados-controller")
+
+	ctx := reqIn.Context()
+	if timeout := h.Cluster.HTTPRequestTimeout; timeout > 0 {
+		var cancel context.CancelFunc
+		ctx, cancel = context.WithDeadline(ctx, time.Now().Add(time.Duration(timeout)))
+		defer cancel()
+	}
+
+	reqOut := (&http.Request{
+		Method: reqIn.Method,
+		URL:    urlOut,
+		Header: hdrOut,
+		Body:   reqIn.Body,
+	}).WithContext(ctx)
+	resp, err := arvados.InsecureHTTPClient.Do(reqOut)
+	if err != nil {
+		httpserver.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+	for k, v := range resp.Header {
+		for _, v := range v {
+			w.Header().Add(k, v)
+		}
+	}
+	w.WriteHeader(resp.StatusCode)
+	n, err := io.Copy(w, resp.Body)
+	if err != nil {
+		httpserver.Logger(reqIn).WithError(err).WithField("bytesCopied", n).Error("error copying response body")
+	}
+}

commit 967821ec0ac00aef677f943e709ea079ebc30f78
Author: Tom Clegg <tclegg at veritasgenetics.com>
Date:   Wed Jun 27 16:43:34 2018 -0400

    13493: Salt tokens when forwarding requests to remote clusters.
    
    Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tclegg at veritasgenetics.com>

diff --git a/lib/controller/federation.go b/lib/controller/federation.go
new file mode 100644
index 000000000..5ea305d03
--- /dev/null
+++ b/lib/controller/federation.go
@@ -0,0 +1,50 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+package controller
+
+import (
+	"bytes"
+	"io/ioutil"
+	"net/http"
+
+	"git.curoverse.com/arvados.git/sdk/go/auth"
+)
+
+// Extract the auth token supplied in req, and replace it with a
+// salted token for the remote cluster.
+func (h *Handler) saltAuthToken(req *http.Request, remote string) error {
+	creds := auth.NewCredentials()
+	creds.LoadTokensFromHTTPRequest(req)
+	if len(creds.Tokens) == 0 && req.Header.Get("Content-Type") == "application/x-www-form-encoded" {
+		// Override ParseForm's 10MiB limit by ensuring
+		// req.Body is a *http.maxBytesReader.
+		req.Body = http.MaxBytesReader(nil, req.Body, 1<<28) // 256MiB. TODO: use MaxRequestSize from discovery doc or config.
+		if err := creds.LoadTokensFromHTTPRequestBody(req); err != nil {
+			return err
+		}
+		// Replace req.Body with a buffer that re-encodes the
+		// form without api_token, in case we end up
+		// forwarding the request to RailsAPI.
+		if req.PostForm != nil {
+			req.PostForm.Del("api_token")
+		}
+		req.Body = ioutil.NopCloser(bytes.NewBufferString(req.PostForm.Encode()))
+	}
+	if len(creds.Tokens) == 0 {
+		return nil
+	}
+	token, err := auth.SaltToken(creds.Tokens[0], remote)
+	if err == auth.ErrObsoleteToken {
+		// FIXME: If the token exists in our own database,
+		// salt it for the remote. Otherwise, assume it was
+		// issued by the remote, and pass it through
+		// unmodified.
+		token = creds.Tokens[0]
+	} else if err != nil {
+		return err
+	}
+	req.Header.Set("Authorization", "Bearer "+token)
+	return nil
+}
diff --git a/lib/controller/federation_test.go b/lib/controller/federation_test.go
index 6c54fc8d3..156b8d5f2 100644
--- a/lib/controller/federation_test.go
+++ b/lib/controller/federation_test.go
@@ -94,7 +94,7 @@ func (s *FederationSuite) TestNoAuth(c *check.C) {
 
 func (s *FederationSuite) TestBadAuth(c *check.C) {
 	req := httptest.NewRequest("GET", "/arvados/v1/workflows/"+arvadostest.WorkflowWithDefinitionYAMLUUID, nil)
-	req.Header.Set("Authorization", "Bearer aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
+	req.Header.Set("Authorization", "Bearer aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
 	resp := httptest.NewRecorder()
 	s.handler.ServeHTTP(resp, req)
 	c.Check(resp.Code, check.Equals, http.StatusUnauthorized)
@@ -145,14 +145,23 @@ func (s *FederationSuite) TestGetRemoteWorkflow(c *check.C) {
 }
 
 func (s *FederationSuite) TestUpdateRemoteWorkflow(c *check.C) {
-	req := httptest.NewRequest("PATCH", "/arvados/v1/workflows/"+arvadostest.WorkflowWithDefinitionYAMLUUID, strings.NewReader(url.Values{
-		"workflow": {`{"description":"updated by TestUpdateRemoteWorkflow"}`},
-	}.Encode()))
-	req.Header.Set("Content-type", "application/x-www-form-urlencoded")
-	req.Header.Set("Authorization", "Bearer "+arvadostest.ActiveToken)
-	resp := httptest.NewRecorder()
-	s.handler.ServeHTTP(resp, req)
-	s.checkResponseOK(c, resp)
+	updateDescription := func(descr string) *httptest.ResponseRecorder {
+		req := httptest.NewRequest("PATCH", "/arvados/v1/workflows/"+arvadostest.WorkflowWithDefinitionYAMLUUID, strings.NewReader(url.Values{
+			"workflow": {`{"description":"` + descr + `"}`},
+		}.Encode()))
+		req.Header.Set("Content-type", "application/x-www-form-urlencoded")
+		req.Header.Set("Authorization", "Bearer "+arvadostest.ActiveToken)
+		resp := httptest.NewRecorder()
+		s.handler.ServeHTTP(resp, req)
+		s.checkResponseOK(c, resp)
+		return resp
+	}
+
+	// Update description twice so running this test twice in a
+	// row still causes ModifiedAt to change
+	updateDescription("updated once by TestUpdateRemoteWorkflow")
+	resp := updateDescription("updated twice by TestUpdateRemoteWorkflow")
+
 	var wf arvados.Workflow
 	c.Check(json.Unmarshal(resp.Body.Bytes(), &wf), check.IsNil)
 	c.Check(wf.UUID, check.Equals, arvadostest.WorkflowWithDefinitionYAMLUUID)
diff --git a/lib/controller/handler.go b/lib/controller/handler.go
index 7f4376e6f..ab6c72735 100644
--- a/lib/controller/handler.go
+++ b/lib/controller/handler.go
@@ -82,9 +82,10 @@ func (h *Handler) proxyRemoteCluster(w http.ResponseWriter, req *http.Request, n
 		next.ServeHTTP(w, req)
 		return
 	}
-	remote, ok := h.Cluster.RemoteClusters[m[1]]
+	remoteID := m[1]
+	remote, ok := h.Cluster.RemoteClusters[remoteID]
 	if !ok {
-		httpserver.Error(w, "no proxy available for cluster "+m[1], http.StatusNotFound)
+		httpserver.Error(w, "no proxy available for cluster "+remoteID, http.StatusNotFound)
 		return
 	}
 	scheme := remote.Scheme
@@ -98,6 +99,11 @@ func (h *Handler) proxyRemoteCluster(w http.ResponseWriter, req *http.Request, n
 		RawPath:  req.URL.RawPath,
 		RawQuery: req.URL.RawQuery,
 	}
+	err := h.saltAuthToken(req, remoteID)
+	if err != nil {
+		httpserver.Error(w, err.Error(), http.StatusBadRequest)
+		return
+	}
 	h.proxy(w, req, urlOut)
 }
 
diff --git a/sdk/go/auth/auth.go b/sdk/go/auth/auth.go
index ea492430e..ad1d398c7 100644
--- a/sdk/go/auth/auth.go
+++ b/sdk/go/auth/auth.go
@@ -34,7 +34,7 @@ var EncodeTokenCookie func([]byte) string = base64.URLEncoding.EncodeToString
 // token.
 var DecodeTokenCookie func(string) ([]byte, error) = base64.URLEncoding.DecodeString
 
-// LoadTokensFromHttpRequest loads all tokens it can find in the
+// LoadTokensFromHTTPRequest loads all tokens it can find in the
 // headers and query string of an http query.
 func (a *Credentials) LoadTokensFromHTTPRequest(r *http.Request) {
 	// Load plain token from "Authorization: OAuth2 ..." header
@@ -83,7 +83,21 @@ func (a *Credentials) loadTokenFromCookie(r *http.Request) {
 	a.Tokens = append(a.Tokens, string(token))
 }
 
-// TODO: LoadTokensFromHttpRequestBody(). We can't assume in
-// LoadTokensFromHttpRequest() that [or how] we should read and parse
-// the request body. This has to be requested explicitly by the
-// application.
+// LoadTokensFromHTTPRequestBody() loads credentials from the request
+// body.
+//
+// This is separate from LoadTokensFromHTTPRequest() because it's not
+// always desirable to read the request body. This has to be requested
+// explicitly by the application.
+func (a *Credentials) LoadTokensFromHTTPRequestBody(r *http.Request) error {
+	if r.Header.Get("Content-Type") != "application/x-www-form-urlencoded" {
+		return nil
+	}
+	if err := r.ParseForm(); err != nil {
+		return err
+	}
+	if t := r.PostFormValue("api_token"); t != "" {
+		a.Tokens = append(a.Tokens, t)
+	}
+	return nil
+}
diff --git a/sdk/go/auth/salt.go b/sdk/go/auth/salt.go
new file mode 100644
index 000000000..f669eb276
--- /dev/null
+++ b/sdk/go/auth/salt.go
@@ -0,0 +1,45 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: Apache-2.0
+
+package auth
+
+import (
+	"crypto/hmac"
+	"crypto/sha1"
+	"errors"
+	"fmt"
+	"regexp"
+	"strings"
+)
+
+var (
+	reObsoleteToken  = regexp.MustCompile(`^[0-9a-z]{41,}$`)
+	ErrObsoleteToken = errors.New("obsolete token format")
+	ErrTokenFormat   = errors.New("badly formatted token")
+	ErrSalted        = errors.New("token already salted")
+)
+
+func SaltToken(token, remote string) (string, error) {
+	parts := strings.Split(token, "/")
+	if len(parts) < 3 || parts[0] != "v2" {
+		if reObsoleteToken.MatchString(token) {
+			return "", ErrObsoleteToken
+		} else {
+			return "", ErrTokenFormat
+		}
+	}
+	uuid := parts[1]
+	secret := parts[2]
+	if len(secret) != 40 {
+		// not already salted
+		secret = fmt.Sprintf("%x", hmac.New(sha1.New, []byte(secret)).Sum([]byte(remote)))
+		return "v2/" + uuid + "/" + secret, nil
+	} else if strings.HasPrefix(uuid, remote) {
+		// already salted for the desired remote
+		return token, nil
+	} else {
+		// salted for a different remote, can't be used
+		return "", ErrSalted
+	}
+}

commit 59cf374273bfae0baec8370526a747bbb8e5edb7
Author: Tom Clegg <tclegg at veritasgenetics.com>
Date:   Fri Jun 22 16:40:17 2018 -0400

    13493: Test PATCH request.
    
    Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tclegg at veritasgenetics.com>

diff --git a/lib/controller/federation_test.go b/lib/controller/federation_test.go
index 983aa7196..6c54fc8d3 100644
--- a/lib/controller/federation_test.go
+++ b/lib/controller/federation_test.go
@@ -8,7 +8,9 @@ import (
 	"encoding/json"
 	"net/http"
 	"net/http/httptest"
+	"net/url"
 	"strings"
+	"time"
 
 	"git.curoverse.com/arvados.git/sdk/go/arvados"
 	"git.curoverse.com/arvados.git/sdk/go/arvadostest"
@@ -66,10 +68,18 @@ func (s *FederationSuite) TearDownTest(c *check.C) {
 	}
 }
 
-func (s *FederationSuite) TestLocalRequestError(c *check.C) {
+func (s *FederationSuite) TestLocalRequest(c *check.C) {
 	req := httptest.NewRequest("GET", "/arvados/v1/workflows/"+strings.Replace(arvadostest.WorkflowWithDefinitionYAMLUUID, "zzzzz-", "zhome-", 1), nil)
 	resp := httptest.NewRecorder()
 	s.handler.ServeHTTP(resp, req)
+	s.checkHandledLocally(c, resp)
+}
+
+func (s *FederationSuite) checkHandledLocally(c *check.C, resp *httptest.ResponseRecorder) {
+	// Our "home" controller can't handle local requests because
+	// it doesn't have its own stub/test Rails API, so we rely on
+	// "connection refused" to indicate the controller tried to
+	// proxy the request to its local Rails API.
 	c.Check(resp.Code, check.Equals, http.StatusInternalServerError)
 	s.checkJSONErrorMatches(c, resp, `.*connection refused`)
 }
@@ -109,7 +119,17 @@ func (s *FederationSuite) TestGetUnknownRemote(c *check.C) {
 	s.checkJSONErrorMatches(c, resp, `.*no proxy available for cluster zz404`)
 }
 
-func (s *FederationSuite) TestRemoteDown(c *check.C) {
+func (s *FederationSuite) TestRemoteError(c *check.C) {
+	rc := s.handler.Cluster.RemoteClusters["zzzzz"]
+	rc.Scheme = "https"
+	s.handler.Cluster.RemoteClusters["zzzzz"] = rc
+
+	req := httptest.NewRequest("GET", "/arvados/v1/workflows/"+arvadostest.WorkflowWithDefinitionYAMLUUID, nil)
+	req.Header.Set("Authorization", "Bearer "+arvadostest.ActiveToken)
+	resp := httptest.NewRecorder()
+	s.handler.ServeHTTP(resp, req)
+	c.Check(resp.Code, check.Equals, http.StatusInternalServerError)
+	s.checkJSONErrorMatches(c, resp, `.*HTTP response to HTTPS client`)
 }
 
 func (s *FederationSuite) TestGetRemoteWorkflow(c *check.C) {
@@ -124,6 +144,30 @@ func (s *FederationSuite) TestGetRemoteWorkflow(c *check.C) {
 	c.Check(wf.OwnerUUID, check.Equals, arvadostest.ActiveUserUUID)
 }
 
+func (s *FederationSuite) TestUpdateRemoteWorkflow(c *check.C) {
+	req := httptest.NewRequest("PATCH", "/arvados/v1/workflows/"+arvadostest.WorkflowWithDefinitionYAMLUUID, strings.NewReader(url.Values{
+		"workflow": {`{"description":"updated by TestUpdateRemoteWorkflow"}`},
+	}.Encode()))
+	req.Header.Set("Content-type", "application/x-www-form-urlencoded")
+	req.Header.Set("Authorization", "Bearer "+arvadostest.ActiveToken)
+	resp := httptest.NewRecorder()
+	s.handler.ServeHTTP(resp, req)
+	s.checkResponseOK(c, resp)
+	var wf arvados.Workflow
+	c.Check(json.Unmarshal(resp.Body.Bytes(), &wf), check.IsNil)
+	c.Check(wf.UUID, check.Equals, arvadostest.WorkflowWithDefinitionYAMLUUID)
+	c.Assert(wf.ModifiedAt, check.NotNil)
+	c.Logf("%s", *wf.ModifiedAt)
+	c.Check(time.Since(*wf.ModifiedAt) < time.Minute, check.Equals, true)
+}
+
+func (s *FederationSuite) checkResponseOK(c *check.C, resp *httptest.ResponseRecorder) {
+	c.Check(resp.Code, check.Equals, http.StatusOK)
+	if resp.Code != http.StatusOK {
+		c.Logf("... response body = %s\n", resp.Body.String())
+	}
+}
+
 func (s *FederationSuite) checkJSONErrorMatches(c *check.C, resp *httptest.ResponseRecorder, re string) {
 	var jresp httpserver.ErrorResponse
 	err := json.Unmarshal(resp.Body.Bytes(), &jresp)
diff --git a/lib/controller/handler_test.go b/lib/controller/handler_test.go
index 981ad7ab9..4829fe0e4 100644
--- a/lib/controller/handler_test.go
+++ b/lib/controller/handler_test.go
@@ -101,6 +101,7 @@ func (s *HandlerSuite) TestProxyWithTokenInRequestBody(c *check.C) {
 		"_method":   {"GET"},
 		"api_token": {arvadostest.ActiveToken},
 	}.Encode()))
+	req.Header.Set("Content-type", "application/x-www-form-urlencoded")
 	resp := httptest.NewRecorder()
 	s.handler.ServeHTTP(resp, req)
 	c.Check(resp.Code, check.Equals, http.StatusOK)

commit a6ab70e907a47e8e28aae2dd3eb357aa72c8b673
Author: Tom Clegg <tclegg at veritasgenetics.com>
Date:   Fri Jun 22 14:32:07 2018 -0400

    13493: Proxy requests to remote clusters.
    
    Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tclegg at veritasgenetics.com>

diff --git a/lib/controller/federation_test.go b/lib/controller/federation_test.go
new file mode 100644
index 000000000..983aa7196
--- /dev/null
+++ b/lib/controller/federation_test.go
@@ -0,0 +1,133 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+package controller
+
+import (
+	"encoding/json"
+	"net/http"
+	"net/http/httptest"
+	"strings"
+
+	"git.curoverse.com/arvados.git/sdk/go/arvados"
+	"git.curoverse.com/arvados.git/sdk/go/arvadostest"
+	"git.curoverse.com/arvados.git/sdk/go/httpserver"
+	"github.com/Sirupsen/logrus"
+	check "gopkg.in/check.v1"
+)
+
+// Gocheck boilerplate
+var _ = check.Suite(&FederationSuite{})
+
+type FederationSuite struct {
+	log          *logrus.Logger
+	localServer  *httpserver.Server
+	remoteServer *httpserver.Server
+	handler      *Handler
+}
+
+func (s *FederationSuite) SetUpTest(c *check.C) {
+	s.log = logrus.New()
+	s.log.Formatter = &logrus.JSONFormatter{}
+	s.log.Out = &logWriter{c.Log}
+
+	s.remoteServer = newServerFromIntegrationTestEnv(c)
+	c.Assert(s.remoteServer.Start(), check.IsNil)
+
+	nodeProfile := arvados.NodeProfile{
+		Controller: arvados.SystemServiceInstance{Listen: ":"},
+		RailsAPI:   arvados.SystemServiceInstance{Listen: ":1"}, // local reqs will error "connection refused"
+	}
+	s.handler = &Handler{Cluster: &arvados.Cluster{
+		ClusterID: "zhome",
+		NodeProfiles: map[string]arvados.NodeProfile{
+			"*": nodeProfile,
+		},
+	}, NodeProfile: &nodeProfile}
+	s.localServer = newServerFromIntegrationTestEnv(c)
+	s.localServer.Server.Handler = httpserver.AddRequestIDs(httpserver.LogRequests(s.log, s.handler))
+	s.handler.Cluster.RemoteClusters = map[string]arvados.RemoteCluster{
+		"zzzzz": {
+			Host:   s.remoteServer.Addr,
+			Proxy:  true,
+			Scheme: "http",
+		},
+	}
+	c.Assert(s.localServer.Start(), check.IsNil)
+}
+
+func (s *FederationSuite) TearDownTest(c *check.C) {
+	if s.remoteServer != nil {
+		s.remoteServer.Close()
+	}
+	if s.localServer != nil {
+		s.localServer.Close()
+	}
+}
+
+func (s *FederationSuite) TestLocalRequestError(c *check.C) {
+	req := httptest.NewRequest("GET", "/arvados/v1/workflows/"+strings.Replace(arvadostest.WorkflowWithDefinitionYAMLUUID, "zzzzz-", "zhome-", 1), nil)
+	resp := httptest.NewRecorder()
+	s.handler.ServeHTTP(resp, req)
+	c.Check(resp.Code, check.Equals, http.StatusInternalServerError)
+	s.checkJSONErrorMatches(c, resp, `.*connection refused`)
+}
+
+func (s *FederationSuite) TestNoAuth(c *check.C) {
+	req := httptest.NewRequest("GET", "/arvados/v1/workflows/"+arvadostest.WorkflowWithDefinitionYAMLUUID, nil)
+	resp := httptest.NewRecorder()
+	s.handler.ServeHTTP(resp, req)
+	c.Check(resp.Code, check.Equals, http.StatusUnauthorized)
+	s.checkJSONErrorMatches(c, resp, `Not logged in`)
+}
+
+func (s *FederationSuite) TestBadAuth(c *check.C) {
+	req := httptest.NewRequest("GET", "/arvados/v1/workflows/"+arvadostest.WorkflowWithDefinitionYAMLUUID, nil)
+	req.Header.Set("Authorization", "Bearer aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
+	resp := httptest.NewRecorder()
+	s.handler.ServeHTTP(resp, req)
+	c.Check(resp.Code, check.Equals, http.StatusUnauthorized)
+	s.checkJSONErrorMatches(c, resp, `Not logged in`)
+}
+
+func (s *FederationSuite) TestNoAccess(c *check.C) {
+	req := httptest.NewRequest("GET", "/arvados/v1/workflows/"+arvadostest.WorkflowWithDefinitionYAMLUUID, nil)
+	req.Header.Set("Authorization", "Bearer "+arvadostest.SpectatorToken)
+	resp := httptest.NewRecorder()
+	s.handler.ServeHTTP(resp, req)
+	c.Check(resp.Code, check.Equals, http.StatusNotFound)
+	s.checkJSONErrorMatches(c, resp, `.*not found`)
+}
+
+func (s *FederationSuite) TestGetUnknownRemote(c *check.C) {
+	req := httptest.NewRequest("GET", "/arvados/v1/workflows/"+strings.Replace(arvadostest.WorkflowWithDefinitionYAMLUUID, "zzzzz-", "zz404-", 1), nil)
+	req.Header.Set("Authorization", "Bearer "+arvadostest.ActiveToken)
+	resp := httptest.NewRecorder()
+	s.handler.ServeHTTP(resp, req)
+	c.Check(resp.Code, check.Equals, http.StatusNotFound)
+	s.checkJSONErrorMatches(c, resp, `.*no proxy available for cluster zz404`)
+}
+
+func (s *FederationSuite) TestRemoteDown(c *check.C) {
+}
+
+func (s *FederationSuite) TestGetRemoteWorkflow(c *check.C) {
+	req := httptest.NewRequest("GET", "/arvados/v1/workflows/"+arvadostest.WorkflowWithDefinitionYAMLUUID, nil)
+	req.Header.Set("Authorization", "Bearer "+arvadostest.ActiveToken)
+	resp := httptest.NewRecorder()
+	s.handler.ServeHTTP(resp, req)
+	c.Check(resp.Code, check.Equals, http.StatusOK)
+	var wf arvados.Workflow
+	c.Check(json.Unmarshal(resp.Body.Bytes(), &wf), check.IsNil)
+	c.Check(wf.UUID, check.Equals, arvadostest.WorkflowWithDefinitionYAMLUUID)
+	c.Check(wf.OwnerUUID, check.Equals, arvadostest.ActiveUserUUID)
+}
+
+func (s *FederationSuite) checkJSONErrorMatches(c *check.C, resp *httptest.ResponseRecorder, re string) {
+	var jresp httpserver.ErrorResponse
+	err := json.Unmarshal(resp.Body.Bytes(), &jresp)
+	c.Check(err, check.IsNil)
+	c.Assert(len(jresp.Errors), check.Equals, 1)
+	c.Check(jresp.Errors[0], check.Matches, re)
+}
diff --git a/lib/controller/handler.go b/lib/controller/handler.go
index 59c2f2a61..7f4376e6f 100644
--- a/lib/controller/handler.go
+++ b/lib/controller/handler.go
@@ -10,6 +10,7 @@ import (
 	"net"
 	"net/http"
 	"net/url"
+	"regexp"
 	"strings"
 	"sync"
 	"time"
@@ -45,7 +46,10 @@ func (h *Handler) setup() {
 		Token:  h.Cluster.ManagementToken,
 		Prefix: "/_health/",
 	})
-	mux.Handle("/", http.HandlerFunc(h.proxyRailsAPI))
+	hs := http.NotFoundHandler()
+	hs = prepend(hs, h.proxyRailsAPI)
+	hs = prepend(hs, h.proxyRemoteCluster)
+	mux.Handle("/", hs)
 	h.handlerStack = mux
 }
 
@@ -62,7 +66,42 @@ var dropHeaders = map[string]bool{
 	"Upgrade":           true,
 }
 
-func (h *Handler) proxyRailsAPI(w http.ResponseWriter, reqIn *http.Request) {
+type middlewareFunc func(http.ResponseWriter, *http.Request, http.Handler)
+
+func prepend(next http.Handler, middleware middlewareFunc) http.Handler {
+	return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
+		middleware(w, req, next)
+	})
+}
+
+var wfRe = regexp.MustCompile(`^/arvados/v1/workflows/([0-9a-z]{5})-[^/]+$`)
+
+func (h *Handler) proxyRemoteCluster(w http.ResponseWriter, req *http.Request, next http.Handler) {
+	m := wfRe.FindStringSubmatch(req.URL.Path)
+	if len(m) < 2 || m[1] == h.Cluster.ClusterID {
+		next.ServeHTTP(w, req)
+		return
+	}
+	remote, ok := h.Cluster.RemoteClusters[m[1]]
+	if !ok {
+		httpserver.Error(w, "no proxy available for cluster "+m[1], http.StatusNotFound)
+		return
+	}
+	scheme := remote.Scheme
+	if scheme == "" {
+		scheme = "https"
+	}
+	urlOut := &url.URL{
+		Scheme:   scheme,
+		Host:     remote.Host,
+		Path:     req.URL.Path,
+		RawPath:  req.URL.RawPath,
+		RawQuery: req.URL.RawQuery,
+	}
+	h.proxy(w, req, urlOut)
+}
+
+func (h *Handler) proxyRailsAPI(w http.ResponseWriter, req *http.Request, next http.Handler) {
 	urlOut, err := findRailsAPI(h.Cluster, h.NodeProfile)
 	if err != nil {
 		httpserver.Error(w, err.Error(), http.StatusInternalServerError)
@@ -71,11 +110,14 @@ func (h *Handler) proxyRailsAPI(w http.ResponseWriter, reqIn *http.Request) {
 	urlOut = &url.URL{
 		Scheme:   urlOut.Scheme,
 		Host:     urlOut.Host,
-		Path:     reqIn.URL.Path,
-		RawPath:  reqIn.URL.RawPath,
-		RawQuery: reqIn.URL.RawQuery,
+		Path:     req.URL.Path,
+		RawPath:  req.URL.RawPath,
+		RawQuery: req.URL.RawQuery,
 	}
+	h.proxy(w, req, urlOut)
+}
 
+func (h *Handler) proxy(w http.ResponseWriter, reqIn *http.Request, urlOut *url.URL) {
 	// Copy headers from incoming request, then add/replace proxy
 	// headers like Via and X-Forwarded-For.
 	hdrOut := http.Header{}
diff --git a/lib/controller/server_test.go b/lib/controller/server_test.go
new file mode 100644
index 000000000..55329656c
--- /dev/null
+++ b/lib/controller/server_test.go
@@ -0,0 +1,53 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+package controller
+
+import (
+	"net/http"
+	"os"
+
+	"git.curoverse.com/arvados.git/sdk/go/arvados"
+	"git.curoverse.com/arvados.git/sdk/go/httpserver"
+	"github.com/Sirupsen/logrus"
+	check "gopkg.in/check.v1"
+)
+
+// logWriter is an io.Writer that writes by calling a "write log"
+// function, typically (*check.C)Log().
+type logWriter struct {
+	logfunc func(...interface{})
+}
+
+func (tl *logWriter) Write(buf []byte) (int, error) {
+	tl.logfunc(string(buf))
+	return len(buf), nil
+}
+
+// Return a new unstarted controller server, using the Rails API
+// provided by the integration-testing environment.
+func newServerFromIntegrationTestEnv(c *check.C) *httpserver.Server {
+	log := logrus.New()
+	log.Formatter = &logrus.JSONFormatter{}
+	log.Out = &logWriter{c.Log}
+
+	nodeProfile := arvados.NodeProfile{
+		Controller: arvados.SystemServiceInstance{Listen: ":"},
+		RailsAPI:   arvados.SystemServiceInstance{Listen: os.Getenv("ARVADOS_TEST_API_HOST"), TLS: true},
+	}
+	handler := &Handler{Cluster: &arvados.Cluster{
+		ClusterID: "zzzzz",
+		NodeProfiles: map[string]arvados.NodeProfile{
+			"*": nodeProfile,
+		},
+	}, NodeProfile: &nodeProfile}
+
+	srv := &httpserver.Server{
+		Server: http.Server{
+			Handler: httpserver.AddRequestIDs(httpserver.LogRequests(log, handler)),
+		},
+		Addr: nodeProfile.Controller.Listen,
+	}
+	return srv
+}
diff --git a/sdk/go/arvados/config.go b/sdk/go/arvados/config.go
index 182cf8433..83bf6292a 100644
--- a/sdk/go/arvados/config.go
+++ b/sdk/go/arvados/config.go
@@ -54,6 +54,17 @@ type Cluster struct {
 	NodeProfiles       map[string]NodeProfile
 	InstanceTypes      []InstanceType
 	HTTPRequestTimeout Duration
+	RemoteClusters     map[string]RemoteCluster
+}
+
+type RemoteCluster struct {
+	// API endpoint host or host:port; default is {id}.arvadosapi.com
+	Host string
+	// Perform a proxy request when a local client requests an
+	// object belonging to this remote.
+	Proxy bool
+	// Scheme, default "https". Can be set to "http" for testing.
+	Scheme string
 }
 
 type InstanceType struct {
diff --git a/sdk/go/arvadostest/fixtures.go b/sdk/go/arvadostest/fixtures.go
index a43469077..6a4b6232a 100644
--- a/sdk/go/arvadostest/fixtures.go
+++ b/sdk/go/arvadostest/fixtures.go
@@ -46,6 +46,8 @@ const (
 
 	FooCollectionSharingTokenUUID = "zzzzz-gj3su-gf02tdm4g1z3e3u"
 	FooCollectionSharingToken     = "iknqgmunrhgsyfok8uzjlwun9iscwm3xacmzmg65fa1j1lpdss"
+
+	WorkflowWithDefinitionYAMLUUID = "zzzzz-7fd4e-validworkfloyml"
 )
 
 // PathologicalManifest : A valid manifest designed to test

-----------------------------------------------------------------------


hooks/post-receive
-- 




More information about the arvados-commits mailing list