[ARVADOS] created: 1.1.4-495-g20bef384d

Git user git at public.curoverse.com
Thu Jun 21 16:56:53 EDT 2018


        at  20bef384d2b71b8357d701e52ec6b46e44b1b617 (commit)


commit 20bef384d2b71b8357d701e52ec6b46e44b1b617
Author: Tom Clegg <tclegg at veritasgenetics.com>
Date:   Thu Jun 21 16:56:15 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..cb3e39f66
--- /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 = &testLogger{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/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