[ARVADOS] created: 1.1.4-384-gf777c7488

Git user git at public.curoverse.com
Fri Jun 15 15:26:40 EDT 2018


        at  f777c74882e6b0f52b15f62d1d6251cd180979e4 (commit)


commit f777c74882e6b0f52b15f62d1d6251cd180979e4
Author: Tom Clegg <tclegg at veritasgenetics.com>
Date:   Fri Jun 15 15:26:24 2018 -0400

    13497: Rename SystemNodes to NodeProfiles in config.
    
    Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tclegg at veritasgenetics.com>

diff --git a/lib/controller/cmd.go b/lib/controller/cmd.go
index 02fb3afba..94eb2580b 100644
--- a/lib/controller/cmd.go
+++ b/lib/controller/cmd.go
@@ -12,6 +12,6 @@ import (
 
 var Command cmd.Handler = service.Command(arvados.ServiceNameController, newHandler)
 
-func newHandler(cluster *arvados.Cluster, node *arvados.SystemNode) service.Handler {
-	return &Handler{Cluster: cluster, Node: node}
+func newHandler(cluster *arvados.Cluster, np *arvados.NodeProfile) service.Handler {
+	return &Handler{Cluster: cluster, NodeProfile: np}
 }
diff --git a/lib/controller/handler.go b/lib/controller/handler.go
index 643a932a5..59c2f2a61 100644
--- a/lib/controller/handler.go
+++ b/lib/controller/handler.go
@@ -20,8 +20,8 @@ import (
 )
 
 type Handler struct {
-	Cluster *arvados.Cluster
-	Node    *arvados.SystemNode
+	Cluster     *arvados.Cluster
+	NodeProfile *arvados.NodeProfile
 
 	setupOnce    sync.Once
 	handlerStack http.Handler
@@ -63,7 +63,7 @@ var dropHeaders = map[string]bool{
 }
 
 func (h *Handler) proxyRailsAPI(w http.ResponseWriter, reqIn *http.Request) {
-	urlOut, err := findRailsAPI(h.Cluster, h.Node)
+	urlOut, err := findRailsAPI(h.Cluster, h.NodeProfile)
 	if err != nil {
 		httpserver.Error(w, err.Error(), http.StatusInternalServerError)
 		return
@@ -123,8 +123,8 @@ func (h *Handler) proxyRailsAPI(w http.ResponseWriter, reqIn *http.Request) {
 
 // For now, findRailsAPI always uses the rails API running on this
 // node.
-func findRailsAPI(cluster *arvados.Cluster, node *arvados.SystemNode) (*url.URL, error) {
-	hostport := node.RailsAPI.Listen
+func findRailsAPI(cluster *arvados.Cluster, np *arvados.NodeProfile) (*url.URL, error) {
+	hostport := np.RailsAPI.Listen
 	if len(hostport) > 1 && hostport[0] == ':' && strings.TrimRight(hostport[1:], "0123456789") == "" {
 		// ":12345" => connect to indicated port on localhost
 		hostport = "localhost" + hostport
@@ -134,7 +134,7 @@ func findRailsAPI(cluster *arvados.Cluster, node *arvados.SystemNode) (*url.URL,
 		return nil, err
 	}
 	proto := "http"
-	if node.RailsAPI.TLS {
+	if np.RailsAPI.TLS {
 		proto = "https"
 	}
 	return url.Parse(proto + "://" + hostport)
diff --git a/lib/controller/handler_test.go b/lib/controller/handler_test.go
index 70a337a6c..981ad7ab9 100644
--- a/lib/controller/handler_test.go
+++ b/lib/controller/handler_test.go
@@ -35,14 +35,14 @@ type HandlerSuite struct {
 func (s *HandlerSuite) SetUpTest(c *check.C) {
 	s.cluster = &arvados.Cluster{
 		ClusterID: "zzzzz",
-		SystemNodes: map[string]arvados.SystemNode{
+		NodeProfiles: map[string]arvados.NodeProfile{
 			"*": {
 				Controller: arvados.SystemServiceInstance{Listen: ":"},
 				RailsAPI:   arvados.SystemServiceInstance{Listen: os.Getenv("ARVADOS_TEST_API_HOST"), TLS: true},
 			},
 		},
 	}
-	node := s.cluster.SystemNodes["*"]
+	node := s.cluster.NodeProfiles["*"]
 	s.handler = newHandler(s.cluster, &node)
 }
 
diff --git a/lib/service/cmd.go b/lib/service/cmd.go
index a144c01a6..4584939f7 100644
--- a/lib/service/cmd.go
+++ b/lib/service/cmd.go
@@ -10,6 +10,7 @@ import (
 	"fmt"
 	"io"
 	"net/http"
+	"os"
 
 	"git.curoverse.com/arvados.git/lib/cmd"
 	"git.curoverse.com/arvados.git/sdk/go/arvados"
@@ -23,7 +24,7 @@ type Handler interface {
 	CheckHealth() error
 }
 
-type NewHandlerFunc func(*arvados.Cluster, *arvados.SystemNode) Handler
+type NewHandlerFunc func(*arvados.Cluster, *arvados.NodeProfile) Handler
 
 type command struct {
 	newHandler NewHandlerFunc
@@ -59,7 +60,7 @@ func (c *command) RunCommand(prog string, args []string, stdin io.Reader, stdout
 	flags := flag.NewFlagSet("", flag.ContinueOnError)
 	flags.SetOutput(stderr)
 	configFile := flags.String("config", arvados.DefaultConfigFile, "Site configuration `file`")
-	hostName := flags.String("host", "", "Host profile `name` to use in SystemNodes config (if blank, use hostname reported by OS)")
+	nodeProfile := flags.String("node-profile", "", "`Name` of NodeProfiles config entry to use (if blank, use $ARVADOS_NODE_PROFILE or hostname reported by OS)")
 	err = flags.Parse(args)
 	if err == flag.ErrHelp {
 		err = nil
@@ -75,16 +76,20 @@ func (c *command) RunCommand(prog string, args []string, stdin io.Reader, stdout
 	if err != nil {
 		return 1
 	}
-	node, err := cluster.GetSystemNode(*hostName)
+	profileName := *nodeProfile
+	if profileName == "" {
+		profileName = os.Getenv("ARVADOS_NODE_PROFILE")
+	}
+	profile, err := cluster.GetNodeProfile(profileName)
 	if err != nil {
 		return 1
 	}
-	listen := node.ServicePorts()[c.svcName]
+	listen := profile.ServicePorts()[c.svcName]
 	if listen == "" {
 		err = fmt.Errorf("configuration does not enable the %s service on this host", c.svcName)
 		return 1
 	}
-	handler := c.newHandler(cluster, node)
+	handler := c.newHandler(cluster, profile)
 	if err = handler.CheckHealth(); err != nil {
 		return 1
 	}
diff --git a/sdk/go/arvados/config.go b/sdk/go/arvados/config.go
index a74c6d8d6..8856c9295 100644
--- a/sdk/go/arvados/config.go
+++ b/sdk/go/arvados/config.go
@@ -51,7 +51,7 @@ func (sc *Config) GetCluster(clusterID string) (*Cluster, error) {
 type Cluster struct {
 	ClusterID          string `json:"-"`
 	ManagementToken    string
-	SystemNodes        map[string]SystemNode
+	NodeProfiles       map[string]NodeProfile
 	InstanceTypes      []InstanceType
 	HTTPRequestTimeout Duration
 }
@@ -65,17 +65,11 @@ type InstanceType struct {
 	Price        float64
 }
 
-// GetThisSystemNode returns a SystemNode for the node we're running
-// on right now.
-func (cc *Cluster) GetThisSystemNode() (*SystemNode, error) {
-	return cc.GetSystemNode("")
-}
-
-// GetSystemNode returns a SystemNode for the given hostname. An error
-// is returned if the appropriate configuration can't be determined
-// (e.g., this does not appear to be a system node). If node is empty,
-// use the OS-reported hostname.
-func (cc *Cluster) GetSystemNode(node string) (*SystemNode, error) {
+// GetNodeProfile returns a NodeProfile for the given hostname. An
+// error is returned if the appropriate configuration can't be
+// determined (e.g., this does not appear to be a system node). If
+// node is empty, use the OS-reported hostname.
+func (cc *Cluster) GetNodeProfile(node string) (*NodeProfile, error) {
 	if node == "" {
 		hostname, err := os.Hostname()
 		if err != nil {
@@ -83,18 +77,18 @@ func (cc *Cluster) GetSystemNode(node string) (*SystemNode, error) {
 		}
 		node = hostname
 	}
-	if cfg, ok := cc.SystemNodes[node]; ok {
+	if cfg, ok := cc.NodeProfiles[node]; ok {
 		return &cfg, nil
 	}
 	// If node is not listed, but "*" gives a default system node
 	// config, use the default config.
-	if cfg, ok := cc.SystemNodes["*"]; ok {
+	if cfg, ok := cc.NodeProfiles["*"]; ok {
 		return &cfg, nil
 	}
 	return nil, fmt.Errorf("config does not provision host %q as a system node", node)
 }
 
-type SystemNode struct {
+type NodeProfile struct {
 	Controller  SystemServiceInstance `json:"arvados-controller"`
 	Health      SystemServiceInstance `json:"arvados-health"`
 	Keepproxy   SystemServiceInstance `json:"keepproxy"`
@@ -121,16 +115,16 @@ const (
 
 // ServicePorts returns the configured listening address (or "" if
 // disabled) for each service on the node.
-func (sn *SystemNode) ServicePorts() map[ServiceName]string {
+func (np *NodeProfile) ServicePorts() map[ServiceName]string {
 	return map[ServiceName]string{
-		ServiceNameRailsAPI:    sn.RailsAPI.Listen,
-		ServiceNameController:  sn.Controller.Listen,
-		ServiceNameNodemanager: sn.Nodemanager.Listen,
-		ServiceNameWorkbench:   sn.Workbench.Listen,
-		ServiceNameWebsocket:   sn.Websocket.Listen,
-		ServiceNameKeepweb:     sn.Keepweb.Listen,
-		ServiceNameKeepproxy:   sn.Keepproxy.Listen,
-		ServiceNameKeepstore:   sn.Keepstore.Listen,
+		ServiceNameRailsAPI:    np.RailsAPI.Listen,
+		ServiceNameController:  np.Controller.Listen,
+		ServiceNameNodemanager: np.Nodemanager.Listen,
+		ServiceNameWorkbench:   np.Workbench.Listen,
+		ServiceNameWebsocket:   np.Websocket.Listen,
+		ServiceNameKeepweb:     np.Keepweb.Listen,
+		ServiceNameKeepproxy:   np.Keepproxy.Listen,
+		ServiceNameKeepstore:   np.Keepstore.Listen,
 	}
 }
 
diff --git a/sdk/go/health/aggregator.go b/sdk/go/health/aggregator.go
index 3f3a91800..a6cb8798a 100644
--- a/sdk/go/health/aggregator.go
+++ b/sdk/go/health/aggregator.go
@@ -113,8 +113,8 @@ func (agg *Aggregator) ClusterHealth(cluster *arvados.Cluster) ClusterHealthResp
 
 	mtx := sync.Mutex{}
 	wg := sync.WaitGroup{}
-	for node, nodeConfig := range cluster.SystemNodes {
-		for svc, addr := range nodeConfig.ServicePorts() {
+	for profileName, profile := range cluster.NodeProfiles {
+		for svc, addr := range profile.ServicePorts() {
 			// Ensure svc is listed in resp.Services.
 			mtx.Lock()
 			if _, ok := resp.Services[svc]; !ok {
@@ -128,10 +128,10 @@ func (agg *Aggregator) ClusterHealth(cluster *arvados.Cluster) ClusterHealthResp
 			}
 
 			wg.Add(1)
-			go func(node string, svc arvados.ServiceName, addr string) {
+			go func(profileName string, svc arvados.ServiceName, addr string) {
 				defer wg.Done()
 				var result CheckResult
-				url, err := agg.pingURL(node, addr)
+				url, err := agg.pingURL(profileName, addr)
 				if err != nil {
 					result = CheckResult{
 						Health: "ERROR",
@@ -152,7 +152,7 @@ func (agg *Aggregator) ClusterHealth(cluster *arvados.Cluster) ClusterHealthResp
 				} else {
 					resp.Health = "ERROR"
 				}
-			}(node, svc, addr)
+			}(profileName, svc, addr)
 		}
 	}
 	wg.Wait()
diff --git a/sdk/go/health/aggregator_test.go b/sdk/go/health/aggregator_test.go
index 4c652254b..a96ed136c 100644
--- a/sdk/go/health/aggregator_test.go
+++ b/sdk/go/health/aggregator_test.go
@@ -34,7 +34,7 @@ func (s *AggregatorSuite) SetUpTest(c *check.C) {
 		Clusters: map[string]arvados.Cluster{
 			"zzzzz": {
 				ManagementToken: arvadostest.ManagementToken,
-				SystemNodes:     map[string]arvados.SystemNode{},
+				NodeProfiles:    map[string]arvados.NodeProfile{},
 			},
 		},
 	}}
@@ -86,7 +86,7 @@ func (*unhealthyHandler) ServeHTTP(resp http.ResponseWriter, req *http.Request)
 func (s *AggregatorSuite) TestUnhealthy(c *check.C) {
 	srv, listen := s.stubServer(&unhealthyHandler{})
 	defer srv.Close()
-	s.handler.Config.Clusters["zzzzz"].SystemNodes["localhost"] = arvados.SystemNode{
+	s.handler.Config.Clusters["zzzzz"].NodeProfiles["localhost"] = arvados.NodeProfile{
 		Keepstore: arvados.SystemServiceInstance{Listen: listen},
 	}
 	s.handler.ServeHTTP(s.resp, s.req)
@@ -106,7 +106,7 @@ func (*healthyHandler) ServeHTTP(resp http.ResponseWriter, req *http.Request) {
 func (s *AggregatorSuite) TestHealthy(c *check.C) {
 	srv, listen := s.stubServer(&healthyHandler{})
 	defer srv.Close()
-	s.handler.Config.Clusters["zzzzz"].SystemNodes["localhost"] = arvados.SystemNode{
+	s.handler.Config.Clusters["zzzzz"].NodeProfiles["localhost"] = arvados.NodeProfile{
 		Controller:  arvados.SystemServiceInstance{Listen: listen},
 		Keepproxy:   arvados.SystemServiceInstance{Listen: listen},
 		Keepstore:   arvados.SystemServiceInstance{Listen: listen},
@@ -130,7 +130,7 @@ func (s *AggregatorSuite) TestHealthyAndUnhealthy(c *check.C) {
 	defer srvH.Close()
 	srvU, listenU := s.stubServer(&unhealthyHandler{})
 	defer srvU.Close()
-	s.handler.Config.Clusters["zzzzz"].SystemNodes["localhost"] = arvados.SystemNode{
+	s.handler.Config.Clusters["zzzzz"].NodeProfiles["localhost"] = arvados.NodeProfile{
 		Controller:  arvados.SystemServiceInstance{Listen: listenH},
 		Keepproxy:   arvados.SystemServiceInstance{Listen: listenH},
 		Keepstore:   arvados.SystemServiceInstance{Listen: listenH},
@@ -140,7 +140,7 @@ func (s *AggregatorSuite) TestHealthyAndUnhealthy(c *check.C) {
 		Websocket:   arvados.SystemServiceInstance{Listen: listenH},
 		Workbench:   arvados.SystemServiceInstance{Listen: listenH},
 	}
-	s.handler.Config.Clusters["zzzzz"].SystemNodes["127.0.0.1"] = arvados.SystemNode{
+	s.handler.Config.Clusters["zzzzz"].NodeProfiles["127.0.0.1"] = arvados.NodeProfile{
 		Keepstore: arvados.SystemServiceInstance{Listen: listenU},
 	}
 	s.handler.ServeHTTP(s.resp, s.req)
@@ -194,7 +194,7 @@ func (s *AggregatorSuite) TestPingTimeout(c *check.C) {
 	s.handler.timeout = arvados.Duration(100 * time.Millisecond)
 	srv, listen := s.stubServer(&slowHandler{})
 	defer srv.Close()
-	s.handler.Config.Clusters["zzzzz"].SystemNodes["localhost"] = arvados.SystemNode{
+	s.handler.Config.Clusters["zzzzz"].NodeProfiles["localhost"] = arvados.NodeProfile{
 		Keepstore: arvados.SystemServiceInstance{Listen: listen},
 	}
 	s.handler.ServeHTTP(s.resp, s.req)
diff --git a/sdk/python/tests/run_test_server.py b/sdk/python/tests/run_test_server.py
index dc4d721f1..f7ca6daf6 100644
--- a/sdk/python/tests/run_test_server.py
+++ b/sdk/python/tests/run_test_server.py
@@ -408,7 +408,7 @@ def run_controller():
         f.write("""
 Clusters:
   zzzzz:
-    SystemNodes:
+    NodeProfiles:
       "*":
         "arvados-controller":
           Listen: ":{}"
diff --git a/services/health/main.go b/services/health/main.go
index 376d4830b..1d2ec47a6 100644
--- a/services/health/main.go
+++ b/services/health/main.go
@@ -41,7 +41,7 @@ func main() {
 	if err != nil {
 		log.Fatal(err)
 	}
-	nodeCfg, err := clusterCfg.GetThisSystemNode()
+	nodeCfg, err := clusterCfg.GetNodeProfile("")
 	if err != nil {
 		log.Fatal(err)
 	}

commit 1b5156270c5cb8d7a4a1b095d981f1a84a98554f
Author: Tom Clegg <tclegg at veritasgenetics.com>
Date:   Fri Jun 15 15:08:59 2018 -0400

    13497: Abort startup if Rails API cannot be found.
    
    Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tclegg at veritasgenetics.com>

diff --git a/lib/controller/cmd.go b/lib/controller/cmd.go
index e006b6594..02fb3afba 100644
--- a/lib/controller/cmd.go
+++ b/lib/controller/cmd.go
@@ -5,8 +5,6 @@
 package controller
 
 import (
-	"net/http"
-
 	"git.curoverse.com/arvados.git/lib/cmd"
 	"git.curoverse.com/arvados.git/lib/service"
 	"git.curoverse.com/arvados.git/sdk/go/arvados"
@@ -14,6 +12,6 @@ import (
 
 var Command cmd.Handler = service.Command(arvados.ServiceNameController, newHandler)
 
-func newHandler(cluster *arvados.Cluster, node *arvados.SystemNode) http.Handler {
+func newHandler(cluster *arvados.Cluster, node *arvados.SystemNode) service.Handler {
 	return &Handler{Cluster: cluster, Node: node}
 }
diff --git a/lib/controller/handler.go b/lib/controller/handler.go
index ad765bafa..643a932a5 100644
--- a/lib/controller/handler.go
+++ b/lib/controller/handler.go
@@ -33,6 +33,12 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
 	h.handlerStack.ServeHTTP(w, req)
 }
 
+func (h *Handler) CheckHealth() error {
+	h.setupOnce.Do(h.setup)
+	_, err := findRailsAPI(h.Cluster, h.NodeProfile)
+	return err
+}
+
 func (h *Handler) setup() {
 	mux := http.NewServeMux()
 	mux.Handle("/_health/", &health.Handler{
diff --git a/lib/service/cmd.go b/lib/service/cmd.go
index ab7886066..a144c01a6 100644
--- a/lib/service/cmd.go
+++ b/lib/service/cmd.go
@@ -18,7 +18,12 @@ import (
 	"github.com/coreos/go-systemd/daemon"
 )
 
-type NewHandlerFunc func(*arvados.Cluster, *arvados.SystemNode) http.Handler
+type Handler interface {
+	http.Handler
+	CheckHealth() error
+}
+
+type NewHandlerFunc func(*arvados.Cluster, *arvados.SystemNode) Handler
 
 type command struct {
 	newHandler NewHandlerFunc
@@ -79,9 +84,13 @@ func (c *command) RunCommand(prog string, args []string, stdin io.Reader, stdout
 		err = fmt.Errorf("configuration does not enable the %s service on this host", c.svcName)
 		return 1
 	}
+	handler := c.newHandler(cluster, node)
+	if err = handler.CheckHealth(); err != nil {
+		return 1
+	}
 	srv := &httpserver.Server{
 		Server: http.Server{
-			Handler: httpserver.AddRequestIDs(httpserver.LogRequests(log, c.newHandler(cluster, node))),
+			Handler: httpserver.AddRequestIDs(httpserver.LogRequests(log, handler)),
 		},
 		Addr: listen,
 	}

commit b58c06e93fc1392aea0347ea099376b41ec4b7c3
Author: Tom Clegg <tclegg at veritasgenetics.com>
Date:   Fri Jun 15 15:06:02 2018 -0400

    13497: Support "run-tests.sh --only go".
    
    Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tclegg at veritasgenetics.com>

diff --git a/build/run-tests.sh b/build/run-tests.sh
index ad7960275..210f11f20 100755
--- a/build/run-tests.sh
+++ b/build/run-tests.sh
@@ -652,8 +652,9 @@ do_test() {
             ;;
     esac
     if [[ -z "${skip[$suite]}" && -z "${skip[$1]}" && \
-                (-z "${only}" || "${only}" == "${suite}" || \
-                 "${only}" == "${1}") ]]; then
+              (-z "${only}" || "${only}" == "${suite}" || \
+                   "${only}" == "${1}") ||
+                  "${only}" == "${2}" ]]; then
         retry do_test_once ${@}
     else
         title "Skipping ${1} tests"

commit a1f0e517f6b37ea987c0146a4ca93f50715f00f2
Author: Tom Clegg <tclegg at veritasgenetics.com>
Date:   Fri Jun 15 15:05:17 2018 -0400

    13497: Bump version numbers for Go packages when lib/ changes.
    
    Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tclegg at veritasgenetics.com>

diff --git a/build/run-library.sh b/build/run-library.sh
index fb4df6a79..4b18d037b 100755
--- a/build/run-library.sh
+++ b/build/run-library.sh
@@ -129,10 +129,7 @@ package_go_binary() {
     # Arvados SDK and the SDK has changed.
     declare -a checkdirs=(vendor)
     if grep -qr git.curoverse.com/arvados .; then
-        checkdirs+=(sdk/go)
-        if [[ "$prog" -eq "crunch-dispatch-slurm" ]]; then
-          checkdirs+=(lib/dispatchcloud)
-        fi
+        checkdirs+=(sdk/go lib)
     fi
     for dir in ${checkdirs[@]}; do
         cd "$GOPATH/src/git.curoverse.com/arvados.git/$dir"

commit 21c5372c6b670820e842e01336eb6b191d6e10b7
Author: Tom Clegg <tclegg at veritasgenetics.com>
Date:   Thu Jun 14 17:03:24 2018 -0400

    13497: Quote file paths in test suite nginx.conf.
    
    Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tclegg at veritasgenetics.com>

diff --git a/sdk/python/tests/nginx.conf b/sdk/python/tests/nginx.conf
index ce1b17062..ce1929fdf 100644
--- a/sdk/python/tests/nginx.conf
+++ b/sdk/python/tests/nginx.conf
@@ -7,7 +7,7 @@ error_log "{{ERRORLOG}}" info;          # Yes, must be specified here _and_ cmdl
 events {
 }
 http {
-  access_log {{ACCESSLOG}} combined;
+  access_log "{{ACCESSLOG}}" combined;
   client_body_temp_path "{{TMPDIR}}";
   upstream arv-git-http {
     server localhost:{{GITPORT}};
@@ -15,8 +15,8 @@ http {
   server {
     listen *:{{GITSSLPORT}} ssl default_server;
     server_name _;
-    ssl_certificate {{SSLCERT}};
-    ssl_certificate_key {{SSLKEY}};
+    ssl_certificate "{{SSLCERT}}";
+    ssl_certificate_key "{{SSLKEY}}";
     location  / {
       proxy_pass http://arv-git-http;
     }
@@ -27,8 +27,8 @@ http {
   server {
     listen *:{{KEEPPROXYSSLPORT}} ssl default_server;
     server_name _;
-    ssl_certificate {{SSLCERT}};
-    ssl_certificate_key {{SSLKEY}};
+    ssl_certificate "{{SSLCERT}}";
+    ssl_certificate_key "{{SSLKEY}}";
     location  / {
       proxy_pass http://keepproxy;
     }
@@ -39,8 +39,8 @@ http {
   server {
     listen *:{{KEEPWEBSSLPORT}} ssl default_server;
     server_name ~^(?<request_host>.*)$;
-    ssl_certificate {{SSLCERT}};
-    ssl_certificate_key {{SSLKEY}};
+    ssl_certificate "{{SSLCERT}}";
+    ssl_certificate_key "{{SSLKEY}}";
     location  / {
       proxy_pass http://keep-web;
       proxy_set_header Host $request_host:{{KEEPWEBPORT}};
@@ -50,8 +50,8 @@ http {
   server {
     listen *:{{KEEPWEBDLSSLPORT}} ssl default_server;
     server_name ~.*;
-    ssl_certificate {{SSLCERT}};
-    ssl_certificate_key {{SSLKEY}};
+    ssl_certificate "{{SSLCERT}}";
+    ssl_certificate_key "{{SSLKEY}}";
     location  / {
       proxy_pass http://keep-web;
       proxy_set_header Host download:{{KEEPWEBPORT}};
@@ -65,8 +65,8 @@ http {
   server {
     listen *:{{WSSPORT}} ssl default_server;
     server_name ~^(?<request_host>.*)$;
-    ssl_certificate {{SSLCERT}};
-    ssl_certificate_key {{SSLKEY}};
+    ssl_certificate "{{SSLCERT}}";
+    ssl_certificate_key "{{SSLKEY}}";
     location  / {
       proxy_pass http://ws;
       proxy_set_header Upgrade $http_upgrade;
@@ -81,8 +81,8 @@ http {
   server {
     listen *:{{CONTROLLERSSLPORT}} ssl default_server;
     server_name _;
-    ssl_certificate {{SSLCERT}};
-    ssl_certificate_key {{SSLKEY}};
+    ssl_certificate "{{SSLCERT}}";
+    ssl_certificate_key "{{SSLKEY}}";
     location  / {
       proxy_pass http://controller;
       proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

commit 4a98eba9ae08ffccb822842f74b1b805302a1ad1
Author: Tom Clegg <tclegg at veritasgenetics.com>
Date:   Thu Jun 14 17:02:10 2018 -0400

    13497: Suppress nginx "info" logs from console.
    
    (unless ARVADOS_DEBUG is set)
    
    Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tclegg at veritasgenetics.com>

diff --git a/sdk/python/tests/nginx.conf b/sdk/python/tests/nginx.conf
index bda67e630..ce1b17062 100644
--- a/sdk/python/tests/nginx.conf
+++ b/sdk/python/tests/nginx.conf
@@ -3,7 +3,7 @@
 # SPDX-License-Identifier: Apache-2.0
 
 daemon off;
-error_log stderr info;          # Yes, must be specified here _and_ cmdline
+error_log "{{ERRORLOG}}" info;          # Yes, must be specified here _and_ cmdline
 events {
 }
 http {
diff --git a/sdk/python/tests/run_test_server.py b/sdk/python/tests/run_test_server.py
index 258cc38d6..dc4d721f1 100644
--- a/sdk/python/tests/run_test_server.py
+++ b/sdk/python/tests/run_test_server.py
@@ -666,6 +666,7 @@ def run_nginx():
     nginxconf['SSLCERT'] = os.path.join(SERVICES_SRC_DIR, 'api', 'tmp', 'self-signed.pem')
     nginxconf['SSLKEY'] = os.path.join(SERVICES_SRC_DIR, 'api', 'tmp', 'self-signed.key')
     nginxconf['ACCESSLOG'] = _logfilename('nginx_access')
+    nginxconf['ERRORLOG'] = _logfilename('nginx_error')
     nginxconf['TMPDIR'] = TEST_TMPDIR
 
     conftemplatefile = os.path.join(MY_DIRNAME, 'nginx.conf')

commit e26dc8ebc182bec997624213c771f06e9b0179e8
Author: Tom Clegg <tclegg at veritasgenetics.com>
Date:   Thu Jun 14 16:29:11 2018 -0400

    13497: Send test server logs to {workspace}/tmp/*.log.
    
    Don't send them to console unless ARVADOS_DEBUG is set.
    
    Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tclegg at veritasgenetics.com>

diff --git a/build/run-tests.sh b/build/run-tests.sh
index 52269fd6e..ad7960275 100755
--- a/build/run-tests.sh
+++ b/build/run-tests.sh
@@ -413,6 +413,8 @@ do
     fi
 done
 
+rm -vf "${WORKSPACE}/tmp/*.log"
+
 setup_ruby_environment() {
     if [[ -s "$HOME/.rvm/scripts/rvm" ]] ; then
         source "$HOME/.rvm/scripts/rvm"
diff --git a/sdk/python/tests/run_test_server.py b/sdk/python/tests/run_test_server.py
index f0fbfe742..258cc38d6 100644
--- a/sdk/python/tests/run_test_server.py
+++ b/sdk/python/tests/run_test_server.py
@@ -202,14 +202,21 @@ def _wait_until_port_listens(port, timeout=10):
         format(port, timeout),
         file=sys.stderr)
 
-def _fifo2stderr(label):
-    """Create a fifo, and copy it to stderr, prepending label to each line.
+def _logfilename(label):
+    """Set up a labelled log file, and return a path to write logs to.
 
-    Return value is the path to the new FIFO.
+    Normally, the returned path is {tmpdir}/{label}.log.
+
+    In debug mode, logs are also written to stderr, with [label]
+    prepended to each line. The returned path is a FIFO.
 
     +label+ should contain only alphanumerics: it is also used as part
     of the FIFO filename.
+
     """
+    logfilename = os.path.join(TEST_TMPDIR, label+'.log')
+    if not os.environ.get('ARVADOS_DEBUG', ''):
+        return logfilename
     fifo = os.path.join(TEST_TMPDIR, label+'.fifo')
     try:
         os.remove(fifo)
@@ -217,8 +224,21 @@ def _fifo2stderr(label):
         if error.errno != errno.ENOENT:
             raise
     os.mkfifo(fifo, 0o700)
+    stdbuf = ['stdbuf', '-i0', '-oL', '-eL']
+    # open(fifo, 'r') would block waiting for someone to open the fifo
+    # for writing, so we need a separate cat process to open it for
+    # us.
+    cat = subprocess.Popen(
+        stdbuf+['cat', fifo],
+        stdin=open('/dev/null'),
+        stdout=subprocess.PIPE)
+    tee = subprocess.Popen(
+        stdbuf+['tee', '-a', logfilename],
+        stdin=cat.stdout,
+        stdout=subprocess.PIPE)
     subprocess.Popen(
-        ['stdbuf', '-i0', '-oL', '-eL', 'sed', '-e', 's/^/['+label+'] /', fifo],
+        stdbuf+['sed', '-e', 's/^/['+label+'] /'],
+        stdin=tee.stdout,
         stdout=sys.stderr)
     return fifo
 
@@ -396,7 +416,7 @@ Clusters:
           Listen: ":{}"
           TLS: true
         """.format(port, rails_api_port))
-    logf = open(_fifo2stderr('controller'), 'w')
+    logf = open(_logfilename('controller'), 'a')
     controller = subprocess.Popen(
         ["arvados-server", "controller", "-config", conf],
         stdin=open('/dev/null'), stdout=logf, stderr=logf, close_fds=True)
@@ -437,7 +457,7 @@ Postgres:
                    _dbconfig('database'),
                    _dbconfig('username'),
                    _dbconfig('password')))
-    logf = open(_fifo2stderr('ws'), 'w')
+    logf = open(_logfilename('ws'), 'a')
     ws = subprocess.Popen(
         ["ws", "-config", conf],
         stdin=open('/dev/null'), stdout=logf, stderr=logf, close_fds=True)
@@ -463,7 +483,7 @@ def _start_keep(n, keep_args):
     for arg, val in keep_args.items():
         keep_cmd.append("{}={}".format(arg, val))
 
-    logf = open(_fifo2stderr('keep{}'.format(n)), 'w')
+    logf = open(_logfilename('keep{}'.format(n)), 'a')
     kp0 = subprocess.Popen(
         keep_cmd, stdin=open('/dev/null'), stdout=logf, stderr=logf, close_fds=True)
 
@@ -547,7 +567,7 @@ def run_keep_proxy():
     port = find_available_port()
     env = os.environ.copy()
     env['ARVADOS_API_TOKEN'] = auth_token('anonymous')
-    logf = open(_fifo2stderr('keepproxy'), 'w')
+    logf = open(_logfilename('keepproxy'), 'a')
     kp = subprocess.Popen(
         ['keepproxy',
          '-pid='+_pidfile('keepproxy'),
@@ -586,7 +606,7 @@ def run_arv_git_httpd():
     gitport = find_available_port()
     env = os.environ.copy()
     env.pop('ARVADOS_API_TOKEN', None)
-    logf = open(_fifo2stderr('arv-git-httpd'), 'w')
+    logf = open(_logfilename('arv-git-httpd'), 'a')
     agh = subprocess.Popen(
         ['arv-git-httpd',
          '-repo-root='+gitdir+'/test',
@@ -610,7 +630,7 @@ def run_keep_web():
     keepwebport = find_available_port()
     env = os.environ.copy()
     env['ARVADOS_API_TOKEN'] = auth_token('anonymous')
-    logf = open(_fifo2stderr('keep-web'), 'w')
+    logf = open(_logfilename('keep-web'), 'a')
     keepweb = subprocess.Popen(
         ['keep-web',
          '-allow-anonymous',
@@ -645,7 +665,7 @@ def run_nginx():
     nginxconf['WSSPORT'] = _getport('wss')
     nginxconf['SSLCERT'] = os.path.join(SERVICES_SRC_DIR, 'api', 'tmp', 'self-signed.pem')
     nginxconf['SSLKEY'] = os.path.join(SERVICES_SRC_DIR, 'api', 'tmp', 'self-signed.key')
-    nginxconf['ACCESSLOG'] = _fifo2stderr('nginx_access_log')
+    nginxconf['ACCESSLOG'] = _logfilename('nginx_access')
     nginxconf['TMPDIR'] = TEST_TMPDIR
 
     conftemplatefile = os.path.join(MY_DIRNAME, 'nginx.conf')

commit 46434cfe5a053097440bcccc35d0ce7d00bbcfee
Author: Tom Clegg <tclegg at veritasgenetics.com>
Date:   Thu Jun 14 15:37:13 2018 -0400

    13497: Add systemd unit to arvados-controller package.
    
    Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tclegg at veritasgenetics.com>

diff --git a/cmd/arvados-server/arvados-controller.service b/cmd/arvados-server/arvados-controller.service
new file mode 100644
index 000000000..fbd73e334
--- /dev/null
+++ b/cmd/arvados-server/arvados-controller.service
@@ -0,0 +1,27 @@
+# Copyright (C) The Arvados Authors. All rights reserved.
+#
+# SPDX-License-Identifier: AGPL-3.0
+
+[Unit]
+Description=Arvados controller
+Documentation=https://doc.arvados.org/
+After=network.target
+AssertPathExists=/etc/arvados/config.yml
+
+# systemd==229 (ubuntu:xenial) obeys StartLimitInterval in the [Unit] section
+StartLimitInterval=0
+
+# systemd>=230 (debian:9) obeys StartLimitIntervalSec in the [Unit] section
+StartLimitIntervalSec=0
+
+[Service]
+Type=notify
+ExecStart=/usr/bin/arvados-controller
+Restart=always
+RestartSec=1
+
+# systemd<=219 (centos:7, debian:8, ubuntu:trusty) obeys StartLimitInterval in the [Service] section
+StartLimitInterval=0
+
+[Install]
+WantedBy=multi-user.target
diff --git a/lib/service/cmd.go b/lib/service/cmd.go
index 3447bfb16..ab7886066 100644
--- a/lib/service/cmd.go
+++ b/lib/service/cmd.go
@@ -15,6 +15,7 @@ import (
 	"git.curoverse.com/arvados.git/sdk/go/arvados"
 	"git.curoverse.com/arvados.git/sdk/go/httpserver"
 	"github.com/Sirupsen/logrus"
+	"github.com/coreos/go-systemd/daemon"
 )
 
 type NewHandlerFunc func(*arvados.Cluster, *arvados.SystemNode) http.Handler
@@ -92,6 +93,9 @@ func (c *command) RunCommand(prog string, args []string, stdin io.Reader, stdout
 		"Listen":  srv.Addr,
 		"Service": c.svcName,
 	}).Info("listening")
+	if _, err := daemon.SdNotify(false, "READY=1"); err != nil {
+		log.WithError(err).Errorf("error notifying init daemon")
+	}
 	err = srv.Wait()
 	if err != nil {
 		return 1

commit 18d6239d25924545ba91825011d467861cd5513c
Author: Tom Clegg <tclegg at veritasgenetics.com>
Date:   Thu Jun 14 15:23:08 2018 -0400

    13497: Accept host key on command line.
    
    Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tclegg at veritasgenetics.com>

diff --git a/lib/service/cmd.go b/lib/service/cmd.go
index e59ac486a..3447bfb16 100644
--- a/lib/service/cmd.go
+++ b/lib/service/cmd.go
@@ -53,6 +53,7 @@ func (c *command) RunCommand(prog string, args []string, stdin io.Reader, stdout
 	flags := flag.NewFlagSet("", flag.ContinueOnError)
 	flags.SetOutput(stderr)
 	configFile := flags.String("config", arvados.DefaultConfigFile, "Site configuration `file`")
+	hostName := flags.String("host", "", "Host profile `name` to use in SystemNodes config (if blank, use hostname reported by OS)")
 	err = flags.Parse(args)
 	if err == flag.ErrHelp {
 		err = nil
@@ -68,7 +69,7 @@ func (c *command) RunCommand(prog string, args []string, stdin io.Reader, stdout
 	if err != nil {
 		return 1
 	}
-	node, err := cluster.GetThisSystemNode()
+	node, err := cluster.GetSystemNode(*hostName)
 	if err != nil {
 		return 1
 	}
diff --git a/sdk/go/arvados/config.go b/sdk/go/arvados/config.go
index 16d93362a..a74c6d8d6 100644
--- a/sdk/go/arvados/config.go
+++ b/sdk/go/arvados/config.go
@@ -68,17 +68,21 @@ type InstanceType struct {
 // GetThisSystemNode returns a SystemNode for the node we're running
 // on right now.
 func (cc *Cluster) GetThisSystemNode() (*SystemNode, error) {
-	hostname, err := os.Hostname()
-	if err != nil {
-		return nil, err
-	}
-	return cc.GetSystemNode(hostname)
+	return cc.GetSystemNode("")
 }
 
 // GetSystemNode returns a SystemNode for the given hostname. An error
 // is returned if the appropriate configuration can't be determined
-// (e.g., this does not appear to be a system node).
+// (e.g., this does not appear to be a system node). If node is empty,
+// use the OS-reported hostname.
 func (cc *Cluster) GetSystemNode(node string) (*SystemNode, error) {
+	if node == "" {
+		hostname, err := os.Hostname()
+		if err != nil {
+			return nil, err
+		}
+		node = hostname
+	}
 	if cfg, ok := cc.SystemNodes[node]; ok {
 		return &cfg, nil
 	}

commit 48fd863c654325eefceb8dfd182c88a8149ca309
Author: Tom Clegg <tclegg at veritasgenetics.com>
Date:   Thu Jun 14 13:34:58 2018 -0400

    13497: Add controller to health check.
    
    Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tclegg at veritasgenetics.com>

diff --git a/sdk/go/health/aggregator_test.go b/sdk/go/health/aggregator_test.go
index 8a540371c..4c652254b 100644
--- a/sdk/go/health/aggregator_test.go
+++ b/sdk/go/health/aggregator_test.go
@@ -107,6 +107,7 @@ func (s *AggregatorSuite) TestHealthy(c *check.C) {
 	srv, listen := s.stubServer(&healthyHandler{})
 	defer srv.Close()
 	s.handler.Config.Clusters["zzzzz"].SystemNodes["localhost"] = arvados.SystemNode{
+		Controller:  arvados.SystemServiceInstance{Listen: listen},
 		Keepproxy:   arvados.SystemServiceInstance{Listen: listen},
 		Keepstore:   arvados.SystemServiceInstance{Listen: listen},
 		Keepweb:     arvados.SystemServiceInstance{Listen: listen},
@@ -130,6 +131,7 @@ func (s *AggregatorSuite) TestHealthyAndUnhealthy(c *check.C) {
 	srvU, listenU := s.stubServer(&unhealthyHandler{})
 	defer srvU.Close()
 	s.handler.Config.Clusters["zzzzz"].SystemNodes["localhost"] = arvados.SystemNode{
+		Controller:  arvados.SystemServiceInstance{Listen: listenH},
 		Keepproxy:   arvados.SystemServiceInstance{Listen: listenH},
 		Keepstore:   arvados.SystemServiceInstance{Listen: listenH},
 		Keepweb:     arvados.SystemServiceInstance{Listen: listenH},

commit 8666f138c10e2a201ee288770f29c5a20b9fc706
Author: Tom Clegg <tclegg at veritasgenetics.com>
Date:   Wed Jun 13 16:59:52 2018 -0400

    13497: Set usable path for nginx request body buffering.
    
    Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tclegg at veritasgenetics.com>

diff --git a/sdk/python/tests/nginx.conf b/sdk/python/tests/nginx.conf
index d818c5f9c..bda67e630 100644
--- a/sdk/python/tests/nginx.conf
+++ b/sdk/python/tests/nginx.conf
@@ -8,6 +8,7 @@ events {
 }
 http {
   access_log {{ACCESSLOG}} combined;
+  client_body_temp_path "{{TMPDIR}}";
   upstream arv-git-http {
     server localhost:{{GITPORT}};
   }
diff --git a/sdk/python/tests/run_test_server.py b/sdk/python/tests/run_test_server.py
index d8a21204a..f0fbfe742 100644
--- a/sdk/python/tests/run_test_server.py
+++ b/sdk/python/tests/run_test_server.py
@@ -646,6 +646,7 @@ def run_nginx():
     nginxconf['SSLCERT'] = os.path.join(SERVICES_SRC_DIR, 'api', 'tmp', 'self-signed.pem')
     nginxconf['SSLKEY'] = os.path.join(SERVICES_SRC_DIR, 'api', 'tmp', 'self-signed.key')
     nginxconf['ACCESSLOG'] = _fifo2stderr('nginx_access_log')
+    nginxconf['TMPDIR'] = TEST_TMPDIR
 
     conftemplatefile = os.path.join(MY_DIRNAME, 'nginx.conf')
     conffile = os.path.join(TEST_TMPDIR, 'nginx.conf')

commit 488bc59b2d90e0a9a23801b034c8a54525d83da4
Author: Tom Clegg <tclegg at veritasgenetics.com>
Date:   Wed Jun 13 16:43:41 2018 -0400

    13497: Remove alternate integration test glue in Workbench.
    
    Expect run-tests.sh to have set everything up correctly instead.
    
    Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tclegg at veritasgenetics.com>

diff --git a/apps/workbench/test/test_helper.rb b/apps/workbench/test/test_helper.rb
index 60dadec61..2fd926ff1 100644
--- a/apps/workbench/test/test_helper.rb
+++ b/apps/workbench/test/test_helper.rb
@@ -177,38 +177,14 @@ class ApiServerForTests
   end
 
   def run_test_server
-    env_script = nil
     Dir.chdir PYTHON_TESTS_DIR do
-      # These are no-ops if we're running within run-tests.sh (except
-      # that we do get a useful env_script back from "start", even
-      # though it doesn't need to start up a new server).
-      env_script = check_output %w(python ./run_test_server.py start --auth admin)
-      check_output %w(python ./run_test_server.py start_arv-git-httpd)
-      check_output %w(python ./run_test_server.py start_keep-web)
-      check_output %w(python ./run_test_server.py start_nginx)
-      # This one isn't a no-op, even under run-tests.sh.
       check_output %w(python ./run_test_server.py start_keep)
     end
-    test_env = {}
-    env_script.each_line do |line|
-      line = line.chomp
-      if 0 == line.index('export ')
-        toks = line.sub('export ', '').split '=', 2
-        $stderr.puts "run_test_server.py: #{toks[0]}=#{toks[1]}"
-        test_env[toks[0]] = toks[1]
-      end
-    end
-    test_env
   end
 
   def stop_test_server
     Dir.chdir PYTHON_TESTS_DIR do
       check_output %w(python ./run_test_server.py stop_keep)
-      # These are no-ops if we're running within run-tests.sh
-      check_output %w(python ./run_test_server.py stop_nginx)
-      check_output %w(python ./run_test_server.py stop_arv-git-httpd)
-      check_output %w(python ./run_test_server.py stop_keep-web)
-      check_output %w(python ./run_test_server.py stop)
     end
     @@server_is_running = false
   end
@@ -223,9 +199,9 @@ class ApiServerForTests
       stop_test_server
     end
 
-    test_env = run_test_server
-    $application_config['arvados_login_base'] = "https://#{test_env['ARVADOS_API_HOST']}/login"
-    $application_config['arvados_v1_base'] = "https://#{test_env['ARVADOS_API_HOST']}/arvados/v1"
+    run_test_server
+    $application_config['arvados_login_base'] = "https://#{ENV['ARVADOS_API_HOST']}/login"
+    $application_config['arvados_v1_base'] = "https://#{ENV['ARVADOS_API_HOST']}/arvados/v1"
     $application_config['arvados_insecure_host'] = true
     ActiveSupport::TestCase.reset_application_config
 

commit e2f03263c7c2496ff3ee84e43eb133fe171905f9
Author: Tom Clegg <tclegg at veritasgenetics.com>
Date:   Wed Jun 13 15:51:30 2018 -0400

    13497: Route API traffic through controller in test suites.
    
    Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tclegg at veritasgenetics.com>

diff --git a/build/run-tests.sh b/build/run-tests.sh
index 9bcc5ba05..52269fd6e 100755
--- a/build/run-tests.sh
+++ b/build/run-tests.sh
@@ -349,15 +349,19 @@ start_services() {
 	rm -f "$WORKSPACE/tmp/api.pid"
     fi
     cd "$WORKSPACE" \
-        && eval $(python sdk/python/tests/run_test_server.py start --auth admin) \
+        && eval $(python sdk/python/tests/run_test_server.py start --auth admin || echo fail=1) \
         && export ARVADOS_TEST_API_HOST="$ARVADOS_API_HOST" \
         && export ARVADOS_TEST_API_INSTALLED="$$" \
+        && python sdk/python/tests/run_test_server.py start_controller \
         && python sdk/python/tests/run_test_server.py start_keep_proxy \
         && python sdk/python/tests/run_test_server.py start_keep-web \
         && python sdk/python/tests/run_test_server.py start_arv-git-httpd \
         && python sdk/python/tests/run_test_server.py start_ws \
-        && python sdk/python/tests/run_test_server.py start_nginx \
+        && eval $(python sdk/python/tests/run_test_server.py start_nginx || echo fail=1) \
         && (env | egrep ^ARVADOS)
+    if [[ -n "$fail" ]]; then
+       return 1
+    fi
 }
 
 stop_services() {
@@ -371,6 +375,7 @@ stop_services() {
         && python sdk/python/tests/run_test_server.py stop_ws \
         && python sdk/python/tests/run_test_server.py stop_keep-web \
         && python sdk/python/tests/run_test_server.py stop_keep_proxy \
+        && python sdk/python/tests/run_test_server.py stop_controller \
         && python sdk/python/tests/run_test_server.py stop
 }
 
diff --git a/lib/controller/handler_test.go b/lib/controller/handler_test.go
index dcd4d26a3..70a337a6c 100644
--- a/lib/controller/handler_test.go
+++ b/lib/controller/handler_test.go
@@ -38,7 +38,7 @@ func (s *HandlerSuite) SetUpTest(c *check.C) {
 		SystemNodes: map[string]arvados.SystemNode{
 			"*": {
 				Controller: arvados.SystemServiceInstance{Listen: ":"},
-				RailsAPI:   arvados.SystemServiceInstance{Listen: os.Getenv("ARVADOS_API_HOST"), TLS: true},
+				RailsAPI:   arvados.SystemServiceInstance{Listen: os.Getenv("ARVADOS_TEST_API_HOST"), TLS: true},
 			},
 		},
 	}
diff --git a/sdk/python/tests/nginx.conf b/sdk/python/tests/nginx.conf
index 780968cb8..d818c5f9c 100644
--- a/sdk/python/tests/nginx.conf
+++ b/sdk/python/tests/nginx.conf
@@ -74,4 +74,17 @@ http {
       proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
     }
   }
+  upstream controller {
+    server localhost:{{CONTROLLERPORT}};
+  }
+  server {
+    listen *:{{CONTROLLERSSLPORT}} ssl default_server;
+    server_name _;
+    ssl_certificate {{SSLCERT}};
+    ssl_certificate_key {{SSLKEY}};
+    location  / {
+      proxy_pass http://controller;
+      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+    }
+  }
 }
diff --git a/sdk/python/tests/run_test_server.py b/sdk/python/tests/run_test_server.py
index 567b3b3bf..d8a21204a 100644
--- a/sdk/python/tests/run_test_server.py
+++ b/sdk/python/tests/run_test_server.py
@@ -377,6 +377,40 @@ def stop(force=False):
         kill_server_pid(_pidfile('api'))
         my_api_host = None
 
+def run_controller():
+    if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
+        return
+    stop_controller()
+    rails_api_port = int(string.split(os.environ.get('ARVADOS_TEST_API_HOST', my_api_host), ':')[-1])
+    port = find_available_port()
+    conf = os.path.join(TEST_TMPDIR, 'arvados.yml')
+    with open(conf, 'w') as f:
+        f.write("""
+Clusters:
+  zzzzz:
+    SystemNodes:
+      "*":
+        "arvados-controller":
+          Listen: ":{}"
+        "arvados-api-server":
+          Listen: ":{}"
+          TLS: true
+        """.format(port, rails_api_port))
+    logf = open(_fifo2stderr('controller'), 'w')
+    controller = subprocess.Popen(
+        ["arvados-server", "controller", "-config", conf],
+        stdin=open('/dev/null'), stdout=logf, stderr=logf, close_fds=True)
+    with open(_pidfile('controller'), 'w') as f:
+        f.write(str(controller.pid))
+    _wait_until_port_listens(port)
+    _setport('controller', port)
+    return port
+
+def stop_controller():
+    if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
+        return
+    kill_server_pid(_pidfile('controller'))
+
 def run_ws():
     if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
         return
@@ -598,6 +632,8 @@ def run_nginx():
         return
     stop_nginx()
     nginxconf = {}
+    nginxconf['CONTROLLERPORT'] = _getport('controller')
+    nginxconf['CONTROLLERSSLPORT'] = find_available_port()
     nginxconf['KEEPWEBPORT'] = _getport('keep-web')
     nginxconf['KEEPWEBDLSSLPORT'] = find_available_port()
     nginxconf['KEEPWEBSSLPORT'] = find_available_port()
@@ -628,6 +664,7 @@ def run_nginx():
          '-g', 'pid '+_pidfile('nginx')+';',
          '-c', conffile],
         env=env, stdin=open('/dev/null'), stdout=sys.stderr)
+    _setport('controller-ssl', nginxconf['CONTROLLERSSLPORT'])
     _setport('keep-web-dl-ssl', nginxconf['KEEPWEBDLSSLPORT'])
     _setport('keep-web-ssl', nginxconf['KEEPWEBSSLPORT'])
     _setport('keepproxy-ssl', nginxconf['KEEPPROXYSSLPORT'])
@@ -766,6 +803,7 @@ if __name__ == "__main__":
     actions = [
         'start', 'stop',
         'start_ws', 'stop_ws',
+        'start_controller', 'stop_controller',
         'start_keep', 'stop_keep',
         'start_keep_proxy', 'stop_keep_proxy',
         'start_keep-web', 'stop_keep-web',
@@ -802,6 +840,10 @@ if __name__ == "__main__":
         run_ws()
     elif args.action == 'stop_ws':
         stop_ws()
+    elif args.action == 'start_controller':
+        run_controller()
+    elif args.action == 'stop_controller':
+        stop_controller()
     elif args.action == 'start_keep':
         run_keep(enforce_permissions=args.keep_enforce_permissions, num_servers=args.num_keep_servers)
     elif args.action == 'stop_keep':
@@ -820,6 +862,7 @@ if __name__ == "__main__":
         stop_keep_web()
     elif args.action == 'start_nginx':
         run_nginx()
+        print("export ARVADOS_API_HOST=0.0.0.0:{}".format(_getport('controller-ssl')))
     elif args.action == 'stop_nginx':
         stop_nginx()
     else:

commit db7330822cb7dbdd1b61a34737d1b24158d8068d
Author: Tom Clegg <tclegg at veritasgenetics.com>
Date:   Wed Jun 13 15:51:07 2018 -0400

    13497: Don't propagate connection-oriented headers when proxying.
    
    Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tclegg at veritasgenetics.com>

diff --git a/lib/controller/handler.go b/lib/controller/handler.go
index 013d293f2..ad765bafa 100644
--- a/lib/controller/handler.go
+++ b/lib/controller/handler.go
@@ -43,6 +43,19 @@ 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,
+}
+
 func (h *Handler) proxyRailsAPI(w http.ResponseWriter, reqIn *http.Request) {
 	urlOut, err := findRailsAPI(h.Cluster, h.Node)
 	if err != nil {
@@ -61,7 +74,9 @@ func (h *Handler) proxyRailsAPI(w http.ResponseWriter, reqIn *http.Request) {
 	// headers like Via and X-Forwarded-For.
 	hdrOut := http.Header{}
 	for k, v := range reqIn.Header {
-		hdrOut[k] = v
+		if !dropHeaders[k] {
+			hdrOut[k] = v
+		}
 	}
 	xff := reqIn.RemoteAddr
 	if xffIn := reqIn.Header.Get("X-Forwarded-For"); xffIn != "" {

commit 2e0b7fcafcccc50602f8fd4df11b6312467e95fa
Author: Tom Clegg <tclegg at veritasgenetics.com>
Date:   Wed Jun 13 15:49:26 2018 -0400

    13497: Send request body when proxying.
    
    Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tclegg at veritasgenetics.com>

diff --git a/lib/controller/handler.go b/lib/controller/handler.go
index 6e4f0e3b4..013d293f2 100644
--- a/lib/controller/handler.go
+++ b/lib/controller/handler.go
@@ -81,6 +81,7 @@ func (h *Handler) proxyRailsAPI(w http.ResponseWriter, reqIn *http.Request) {
 		Method: reqIn.Method,
 		URL:    urlOut,
 		Header: hdrOut,
+		Body:   reqIn.Body,
 	}).WithContext(ctx)
 	resp, err := arvados.InsecureHTTPClient.Do(reqOut)
 	if err != nil {
diff --git a/lib/controller/handler_test.go b/lib/controller/handler_test.go
index a187ba443..dcd4d26a3 100644
--- a/lib/controller/handler_test.go
+++ b/lib/controller/handler_test.go
@@ -8,7 +8,9 @@ import (
 	"encoding/json"
 	"net/http"
 	"net/http/httptest"
+	"net/url"
 	"os"
+	"strings"
 	"testing"
 	"time"
 
@@ -94,6 +96,20 @@ func (s *HandlerSuite) TestProxyWithToken(c *check.C) {
 	c.Check(u.UUID, check.Equals, arvadostest.ActiveUserUUID)
 }
 
+func (s *HandlerSuite) TestProxyWithTokenInRequestBody(c *check.C) {
+	req := httptest.NewRequest("POST", "/arvados/v1/users/current", strings.NewReader(url.Values{
+		"_method":   {"GET"},
+		"api_token": {arvadostest.ActiveToken},
+	}.Encode()))
+	resp := httptest.NewRecorder()
+	s.handler.ServeHTTP(resp, req)
+	c.Check(resp.Code, check.Equals, http.StatusOK)
+	var u arvados.User
+	err := json.Unmarshal(resp.Body.Bytes(), &u)
+	c.Check(err, check.IsNil)
+	c.Check(u.UUID, check.Equals, arvadostest.ActiveUserUUID)
+}
+
 func (s *HandlerSuite) TestProxyNotFound(c *check.C) {
 	req := httptest.NewRequest("GET", "/arvados/v1/xyzzy", nil)
 	resp := httptest.NewRecorder()

commit fa8fd28e3ca22518a147cf34bf7146ef2a173257
Author: Tom Clegg <tclegg at veritasgenetics.com>
Date:   Tue Jun 12 16:39:41 2018 -0400

    13497: Add timeout for proxy requests.
    
    Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tclegg at veritasgenetics.com>

diff --git a/lib/controller/handler.go b/lib/controller/handler.go
index a1b3848e5..6e4f0e3b4 100644
--- a/lib/controller/handler.go
+++ b/lib/controller/handler.go
@@ -5,12 +5,14 @@
 package controller
 
 import (
+	"context"
 	"io"
 	"net"
 	"net/http"
 	"net/url"
 	"strings"
 	"sync"
+	"time"
 
 	"git.curoverse.com/arvados.git/sdk/go/arvados"
 	"git.curoverse.com/arvados.git/sdk/go/health"
@@ -44,7 +46,7 @@ func (h *Handler) setup() {
 func (h *Handler) proxyRailsAPI(w http.ResponseWriter, reqIn *http.Request) {
 	urlOut, err := findRailsAPI(h.Cluster, h.Node)
 	if err != nil {
-		http.Error(w, err.Error(), http.StatusInternalServerError)
+		httpserver.Error(w, err.Error(), http.StatusInternalServerError)
 		return
 	}
 	urlOut = &url.URL{
@@ -68,14 +70,21 @@ func (h *Handler) proxyRailsAPI(w http.ResponseWriter, reqIn *http.Request) {
 	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,
-	}).WithContext(reqIn.Context())
+	}).WithContext(ctx)
 	resp, err := arvados.InsecureHTTPClient.Do(reqOut)
 	if err != nil {
-		http.Error(w, err.Error(), http.StatusInternalServerError)
+		httpserver.Error(w, err.Error(), http.StatusInternalServerError)
 		return
 	}
 	for k, v := range resp.Header {
diff --git a/lib/controller/handler_test.go b/lib/controller/handler_test.go
index 57bb13d95..a187ba443 100644
--- a/lib/controller/handler_test.go
+++ b/lib/controller/handler_test.go
@@ -10,9 +10,11 @@ import (
 	"net/http/httptest"
 	"os"
 	"testing"
+	"time"
 
 	"git.curoverse.com/arvados.git/sdk/go/arvados"
 	"git.curoverse.com/arvados.git/sdk/go/arvadostest"
+	"git.curoverse.com/arvados.git/sdk/go/httpserver"
 	check "gopkg.in/check.v1"
 )
 
@@ -56,6 +58,19 @@ func (s *HandlerSuite) TestProxyDiscoveryDoc(c *check.C) {
 	c.Check(len(dd.Schemas), check.Not(check.Equals), 0)
 }
 
+func (s *HandlerSuite) TestRequestTimeout(c *check.C) {
+	s.cluster.HTTPRequestTimeout = arvados.Duration(time.Nanosecond)
+	req := httptest.NewRequest("GET", "/discovery/v1/apis/arvados/v1/rest", nil)
+	resp := httptest.NewRecorder()
+	s.handler.ServeHTTP(resp, req)
+	c.Check(resp.Code, check.Equals, http.StatusInternalServerError)
+	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, `.*context deadline exceeded`)
+}
+
 func (s *HandlerSuite) TestProxyWithoutToken(c *check.C) {
 	req := httptest.NewRequest("GET", "/arvados/v1/users/current", nil)
 	resp := httptest.NewRecorder()
diff --git a/sdk/go/arvados/config.go b/sdk/go/arvados/config.go
index e0a2b1d28..16d93362a 100644
--- a/sdk/go/arvados/config.go
+++ b/sdk/go/arvados/config.go
@@ -49,10 +49,11 @@ func (sc *Config) GetCluster(clusterID string) (*Cluster, error) {
 }
 
 type Cluster struct {
-	ClusterID       string `json:"-"`
-	ManagementToken string
-	SystemNodes     map[string]SystemNode
-	InstanceTypes   []InstanceType
+	ClusterID          string `json:"-"`
+	ManagementToken    string
+	SystemNodes        map[string]SystemNode
+	InstanceTypes      []InstanceType
+	HTTPRequestTimeout Duration
 }
 
 type InstanceType struct {
diff --git a/sdk/go/httpserver/error.go b/sdk/go/httpserver/error.go
new file mode 100644
index 000000000..398e61fcd
--- /dev/null
+++ b/sdk/go/httpserver/error.go
@@ -0,0 +1,21 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: Apache-2.0
+
+package httpserver
+
+import (
+	"encoding/json"
+	"net/http"
+)
+
+type ErrorResponse struct {
+	Errors []string `json:"errors"`
+}
+
+func Error(w http.ResponseWriter, error string, code int) {
+	w.Header().Set("Content-Type", "application/json")
+	w.Header().Set("X-Content-Type-Options", "nosniff")
+	w.WriteHeader(code)
+	json.NewEncoder(w).Encode(ErrorResponse{Errors: []string{error}})
+}

commit 36f8e449321e4fa02d88fee1fded14aa8ff81723
Author: Tom Clegg <tclegg at veritasgenetics.com>
Date:   Tue Jun 12 16:00:07 2018 -0400

    13497: Proxy requests to Rails API.
    
    Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tclegg at veritasgenetics.com>

diff --git a/lib/controller/cmd.go b/lib/controller/cmd.go
index 2bb68aed9..e006b6594 100644
--- a/lib/controller/cmd.go
+++ b/lib/controller/cmd.go
@@ -12,6 +12,8 @@ import (
 	"git.curoverse.com/arvados.git/sdk/go/arvados"
 )
 
-var Command cmd.Handler = service.Command(arvados.ServiceNameController, func(cluster *arvados.Cluster, _ *arvados.SystemNode) http.Handler {
-	return &Handler{Cluster: cluster}
-})
+var Command cmd.Handler = service.Command(arvados.ServiceNameController, newHandler)
+
+func newHandler(cluster *arvados.Cluster, node *arvados.SystemNode) http.Handler {
+	return &Handler{Cluster: cluster, Node: node}
+}
diff --git a/lib/controller/handler.go b/lib/controller/handler.go
index f0354d94d..a1b3848e5 100644
--- a/lib/controller/handler.go
+++ b/lib/controller/handler.go
@@ -6,16 +6,20 @@ package controller
 
 import (
 	"io"
+	"net"
 	"net/http"
 	"net/url"
+	"strings"
 	"sync"
 
 	"git.curoverse.com/arvados.git/sdk/go/arvados"
 	"git.curoverse.com/arvados.git/sdk/go/health"
+	"git.curoverse.com/arvados.git/sdk/go/httpserver"
 )
 
 type Handler struct {
 	Cluster *arvados.Cluster
+	Node    *arvados.SystemNode
 
 	setupOnce    sync.Once
 	handlerStack http.Handler
@@ -37,15 +41,39 @@ func (h *Handler) setup() {
 	h.handlerStack = mux
 }
 
-func (h *Handler) proxyRailsAPI(w http.ResponseWriter, incomingReq *http.Request) {
-	url, err := findRailsAPI(h.Cluster)
+func (h *Handler) proxyRailsAPI(w http.ResponseWriter, reqIn *http.Request) {
+	urlOut, err := findRailsAPI(h.Cluster, h.Node)
 	if err != nil {
 		http.Error(w, err.Error(), http.StatusInternalServerError)
 		return
 	}
-	req := *incomingReq
-	req.URL.Host = url.Host
-	resp, err := arvados.InsecureHTTPClient.Do(&req)
+	urlOut = &url.URL{
+		Scheme:   urlOut.Scheme,
+		Host:     urlOut.Host,
+		Path:     reqIn.URL.Path,
+		RawPath:  reqIn.URL.RawPath,
+		RawQuery: reqIn.URL.RawQuery,
+	}
+
+	// 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 {
+		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")
+
+	reqOut := (&http.Request{
+		Method: reqIn.Method,
+		URL:    urlOut,
+		Header: hdrOut,
+	}).WithContext(reqIn.Context())
+	resp, err := arvados.InsecureHTTPClient.Do(reqOut)
 	if err != nil {
 		http.Error(w, err.Error(), http.StatusInternalServerError)
 		return
@@ -56,15 +84,27 @@ func (h *Handler) proxyRailsAPI(w http.ResponseWriter, incomingReq *http.Request
 		}
 	}
 	w.WriteHeader(resp.StatusCode)
-	io.Copy(w, resp.Body)
+	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) (*url.URL, error) {
-	node, err := cluster.GetThisSystemNode()
-	if err != nil {
+func findRailsAPI(cluster *arvados.Cluster, node *arvados.SystemNode) (*url.URL, error) {
+	hostport := node.RailsAPI.Listen
+	if len(hostport) > 1 && hostport[0] == ':' && strings.TrimRight(hostport[1:], "0123456789") == "" {
+		// ":12345" => connect to indicated port on localhost
+		hostport = "localhost" + hostport
+	} else if _, _, err := net.SplitHostPort(hostport); err == nil {
+		// "[::1]:12345" => connect to indicated address & port
+	} else {
 		return nil, err
 	}
-	return url.Parse("http://" + node.RailsAPI.Listen)
+	proto := "http"
+	if node.RailsAPI.TLS {
+		proto = "https"
+	}
+	return url.Parse(proto + "://" + hostport)
 }
diff --git a/lib/controller/handler_test.go b/lib/controller/handler_test.go
new file mode 100644
index 000000000..57bb13d95
--- /dev/null
+++ b/lib/controller/handler_test.go
@@ -0,0 +1,91 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+package controller
+
+import (
+	"encoding/json"
+	"net/http"
+	"net/http/httptest"
+	"os"
+	"testing"
+
+	"git.curoverse.com/arvados.git/sdk/go/arvados"
+	"git.curoverse.com/arvados.git/sdk/go/arvadostest"
+	check "gopkg.in/check.v1"
+)
+
+// Gocheck boilerplate
+func Test(t *testing.T) {
+	check.TestingT(t)
+}
+
+var _ = check.Suite(&HandlerSuite{})
+
+type HandlerSuite struct {
+	cluster *arvados.Cluster
+	handler http.Handler
+}
+
+func (s *HandlerSuite) SetUpTest(c *check.C) {
+	s.cluster = &arvados.Cluster{
+		ClusterID: "zzzzz",
+		SystemNodes: map[string]arvados.SystemNode{
+			"*": {
+				Controller: arvados.SystemServiceInstance{Listen: ":"},
+				RailsAPI:   arvados.SystemServiceInstance{Listen: os.Getenv("ARVADOS_API_HOST"), TLS: true},
+			},
+		},
+	}
+	node := s.cluster.SystemNodes["*"]
+	s.handler = newHandler(s.cluster, &node)
+}
+
+func (s *HandlerSuite) TestProxyDiscoveryDoc(c *check.C) {
+	req := httptest.NewRequest("GET", "/discovery/v1/apis/arvados/v1/rest", nil)
+	resp := httptest.NewRecorder()
+	s.handler.ServeHTTP(resp, req)
+	c.Check(resp.Code, check.Equals, http.StatusOK)
+	var dd arvados.DiscoveryDocument
+	err := json.Unmarshal(resp.Body.Bytes(), &dd)
+	c.Check(err, check.IsNil)
+	c.Check(dd.BlobSignatureTTL, check.Not(check.Equals), int64(0))
+	c.Check(dd.BlobSignatureTTL > 0, check.Equals, true)
+	c.Check(len(dd.Resources), check.Not(check.Equals), 0)
+	c.Check(len(dd.Schemas), check.Not(check.Equals), 0)
+}
+
+func (s *HandlerSuite) TestProxyWithoutToken(c *check.C) {
+	req := httptest.NewRequest("GET", "/arvados/v1/users/current", nil)
+	resp := httptest.NewRecorder()
+	s.handler.ServeHTTP(resp, req)
+	c.Check(resp.Code, check.Equals, http.StatusUnauthorized)
+	jresp := map[string]interface{}{}
+	err := json.Unmarshal(resp.Body.Bytes(), &jresp)
+	c.Check(err, check.IsNil)
+	c.Check(jresp["errors"], check.FitsTypeOf, []interface{}{})
+}
+
+func (s *HandlerSuite) TestProxyWithToken(c *check.C) {
+	req := httptest.NewRequest("GET", "/arvados/v1/users/current", 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 u arvados.User
+	err := json.Unmarshal(resp.Body.Bytes(), &u)
+	c.Check(err, check.IsNil)
+	c.Check(u.UUID, check.Equals, arvadostest.ActiveUserUUID)
+}
+
+func (s *HandlerSuite) TestProxyNotFound(c *check.C) {
+	req := httptest.NewRequest("GET", "/arvados/v1/xyzzy", nil)
+	resp := httptest.NewRecorder()
+	s.handler.ServeHTTP(resp, req)
+	c.Check(resp.Code, check.Equals, http.StatusNotFound)
+	jresp := map[string]interface{}{}
+	err := json.Unmarshal(resp.Body.Bytes(), &jresp)
+	c.Check(err, check.IsNil)
+	c.Check(jresp["errors"], check.FitsTypeOf, []interface{}{})
+}
diff --git a/sdk/go/arvados/config.go b/sdk/go/arvados/config.go
index 875a274dc..e0a2b1d28 100644
--- a/sdk/go/arvados/config.go
+++ b/sdk/go/arvados/config.go
@@ -131,4 +131,5 @@ func (sn *SystemNode) ServicePorts() map[ServiceName]string {
 
 type SystemServiceInstance struct {
 	Listen string
+	TLS    bool
 }
diff --git a/sdk/go/httpserver/logger.go b/sdk/go/httpserver/logger.go
index ec3fa7fae..9577718c7 100644
--- a/sdk/go/httpserver/logger.go
+++ b/sdk/go/httpserver/logger.go
@@ -17,7 +17,10 @@ type contextKey struct {
 	name string
 }
 
-var requestTimeContextKey = contextKey{"requestTime"}
+var (
+	requestTimeContextKey = contextKey{"requestTime"}
+	loggerContextKey      = contextKey{"logger"}
+)
 
 // LogRequests wraps an http.Handler, logging each request and
 // response via logger.
@@ -27,7 +30,6 @@ func LogRequests(logger logrus.FieldLogger, h http.Handler) http.Handler {
 	}
 	return http.HandlerFunc(func(wrapped http.ResponseWriter, req *http.Request) {
 		w := &responseTimer{ResponseWriter: WrapResponseWriter(wrapped)}
-		req = req.WithContext(context.WithValue(req.Context(), &requestTimeContextKey, time.Now()))
 		lgr := logger.WithFields(logrus.Fields{
 			"RequestID":       req.Header.Get("X-Request-Id"),
 			"remoteAddr":      req.RemoteAddr,
@@ -38,12 +40,25 @@ func LogRequests(logger logrus.FieldLogger, h http.Handler) http.Handler {
 			"reqQuery":        req.URL.RawQuery,
 			"reqBytes":        req.ContentLength,
 		})
+		ctx := req.Context()
+		ctx = context.WithValue(ctx, &requestTimeContextKey, time.Now())
+		ctx = context.WithValue(ctx, &loggerContextKey, lgr)
+		req = req.WithContext(ctx)
+
 		logRequest(w, req, lgr)
 		defer logResponse(w, req, lgr)
 		h.ServeHTTP(w, req)
 	})
 }
 
+func Logger(req *http.Request) logrus.FieldLogger {
+	if lgr, ok := req.Context().Value(&loggerContextKey).(logrus.FieldLogger); ok {
+		return lgr
+	} else {
+		return logrus.StandardLogger()
+	}
+}
+
 func logRequest(w *responseTimer, req *http.Request, lgr *logrus.Entry) {
 	lgr.Info("request")
 }

commit 8f76037ba8a37c488612285ffe70d26d0d038124
Author: Tom Clegg <tclegg at veritasgenetics.com>
Date:   Tue Jun 12 11:10:12 2018 -0400

    13497: Move common system service code to lib/service.
    
    Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tclegg at veritasgenetics.com>

diff --git a/lib/cmd/cmd.go b/lib/cmd/cmd.go
index 0d3a07a61..353167e80 100644
--- a/lib/cmd/cmd.go
+++ b/lib/cmd/cmd.go
@@ -58,6 +58,11 @@ func (m Multi) RunCommand(prog string, args []string, stdin io.Reader, stdout, s
 		return 2
 	}
 	_, basename := filepath.Split(prog)
+	if strings.HasPrefix(basename, "arvados-") {
+		basename = basename[8:]
+	} else if strings.HasPrefix(basename, "crunch-") {
+		basename = basename[7:]
+	}
 	if cmd, ok := m[basename]; ok {
 		return cmd.RunCommand(prog, args, stdin, stdout, stderr)
 	} else if cmd, ok = m[args[0]]; ok {
diff --git a/lib/controller/cmd.go b/lib/controller/cmd.go
index c13d0fa07..2bb68aed9 100644
--- a/lib/controller/cmd.go
+++ b/lib/controller/cmd.go
@@ -5,75 +5,13 @@
 package controller
 
 import (
-	"flag"
-	"fmt"
-	"io"
 	"net/http"
 
 	"git.curoverse.com/arvados.git/lib/cmd"
+	"git.curoverse.com/arvados.git/lib/service"
 	"git.curoverse.com/arvados.git/sdk/go/arvados"
-	"git.curoverse.com/arvados.git/sdk/go/httpserver"
-	"github.com/Sirupsen/logrus"
 )
 
-const rfc3339NanoFixed = "2006-01-02T15:04:05.000000000Z07:00"
-
-var Command cmd.Handler = &command{}
-
-type command struct{}
-
-func (*command) RunCommand(prog string, args []string, stdin io.Reader, stdout, stderr io.Writer) int {
-	log := logrus.StandardLogger()
-	log.Formatter = &logrus.JSONFormatter{
-		TimestampFormat: rfc3339NanoFixed,
-	}
-	log.Out = stderr
-
-	var err error
-	defer func() {
-		if err != nil {
-			log.WithError(err).Info("exiting")
-		}
-	}()
-	flags := flag.NewFlagSet("", flag.ContinueOnError)
-	flags.SetOutput(stderr)
-	configFile := flags.String("config", arvados.DefaultConfigFile, "Site configuration `file`")
-	err = flags.Parse(args)
-	if err != nil {
-		return 2
-	}
-	cfg, err := arvados.GetConfig(*configFile)
-	if err != nil {
-		return 1
-	}
-	cluster, err := cfg.GetCluster("")
-	if err != nil {
-		return 1
-	}
-	node, err := cluster.GetThisSystemNode()
-	if err != nil {
-		return 1
-	}
-	if node.Controller.Listen == "" {
-		err = fmt.Errorf("configuration does not run a controller on this host: Clusters[%q].SystemNodes[`hostname` or *].Controller.Listen == \"\"", cluster.ClusterID)
-		return 1
-	}
-	srv := &httpserver.Server{
-		Server: http.Server{
-			Handler: httpserver.LogRequests(&Handler{
-				Cluster: cluster,
-			}),
-		},
-		Addr: node.Controller.Listen,
-	}
-	err = srv.Start()
-	if err != nil {
-		return 1
-	}
-	log.WithField("Listen", srv.Addr).Info("listening")
-	err = srv.Wait()
-	if err != nil {
-		return 1
-	}
-	return 0
-}
+var Command cmd.Handler = service.Command(arvados.ServiceNameController, func(cluster *arvados.Cluster, _ *arvados.SystemNode) http.Handler {
+	return &Handler{Cluster: cluster}
+})
diff --git a/lib/controller/handler.go b/lib/controller/handler.go
index c51f8668b..f0354d94d 100644
--- a/lib/controller/handler.go
+++ b/lib/controller/handler.go
@@ -12,14 +12,12 @@ import (
 
 	"git.curoverse.com/arvados.git/sdk/go/arvados"
 	"git.curoverse.com/arvados.git/sdk/go/health"
-	"git.curoverse.com/arvados.git/sdk/go/httpserver"
 )
 
 type Handler struct {
 	Cluster *arvados.Cluster
 
 	setupOnce    sync.Once
-	mux          http.ServeMux
 	handlerStack http.Handler
 	proxyClient  *arvados.Client
 }
@@ -30,12 +28,13 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
 }
 
 func (h *Handler) setup() {
-	h.mux.Handle("/_health/", &health.Handler{
+	mux := http.NewServeMux()
+	mux.Handle("/_health/", &health.Handler{
 		Token:  h.Cluster.ManagementToken,
 		Prefix: "/_health/",
 	})
-	h.mux.Handle("/", http.HandlerFunc(h.proxyRailsAPI))
-	h.handlerStack = httpserver.LogRequests(&h.mux)
+	mux.Handle("/", http.HandlerFunc(h.proxyRailsAPI))
+	h.handlerStack = mux
 }
 
 func (h *Handler) proxyRailsAPI(w http.ResponseWriter, incomingReq *http.Request) {
diff --git a/lib/service/cmd.go b/lib/service/cmd.go
new file mode 100644
index 000000000..e59ac486a
--- /dev/null
+++ b/lib/service/cmd.go
@@ -0,0 +1,101 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: Apache-2.0
+
+// package service provides a cmd.Handler that brings up a system service.
+package service
+
+import (
+	"flag"
+	"fmt"
+	"io"
+	"net/http"
+
+	"git.curoverse.com/arvados.git/lib/cmd"
+	"git.curoverse.com/arvados.git/sdk/go/arvados"
+	"git.curoverse.com/arvados.git/sdk/go/httpserver"
+	"github.com/Sirupsen/logrus"
+)
+
+type NewHandlerFunc func(*arvados.Cluster, *arvados.SystemNode) http.Handler
+
+type command struct {
+	newHandler NewHandlerFunc
+	svcName    arvados.ServiceName
+}
+
+// Command returns a cmd.Handler that loads site config, calls
+// newHandler with the current cluster and node configs, and brings up
+// an http server with the returned handler.
+//
+// The handler is wrapped with server middleware (adding X-Request-ID
+// headers, logging requests/responses, etc).
+func Command(svcName arvados.ServiceName, newHandler NewHandlerFunc) cmd.Handler {
+	return &command{
+		newHandler: newHandler,
+		svcName:    svcName,
+	}
+}
+
+func (c *command) RunCommand(prog string, args []string, stdin io.Reader, stdout, stderr io.Writer) int {
+	log := logrus.New()
+	log.Formatter = &logrus.JSONFormatter{
+		TimestampFormat: rfc3339NanoFixed,
+	}
+	log.Out = stderr
+
+	var err error
+	defer func() {
+		if err != nil {
+			log.WithError(err).Info("exiting")
+		}
+	}()
+	flags := flag.NewFlagSet("", flag.ContinueOnError)
+	flags.SetOutput(stderr)
+	configFile := flags.String("config", arvados.DefaultConfigFile, "Site configuration `file`")
+	err = flags.Parse(args)
+	if err == flag.ErrHelp {
+		err = nil
+		return 0
+	} else if err != nil {
+		return 2
+	}
+	cfg, err := arvados.GetConfig(*configFile)
+	if err != nil {
+		return 1
+	}
+	cluster, err := cfg.GetCluster("")
+	if err != nil {
+		return 1
+	}
+	node, err := cluster.GetThisSystemNode()
+	if err != nil {
+		return 1
+	}
+	listen := node.ServicePorts()[c.svcName]
+	if listen == "" {
+		err = fmt.Errorf("configuration does not enable the %s service on this host", c.svcName)
+		return 1
+	}
+	srv := &httpserver.Server{
+		Server: http.Server{
+			Handler: httpserver.AddRequestIDs(httpserver.LogRequests(log, c.newHandler(cluster, node))),
+		},
+		Addr: listen,
+	}
+	err = srv.Start()
+	if err != nil {
+		return 1
+	}
+	log.WithFields(logrus.Fields{
+		"Listen":  srv.Addr,
+		"Service": c.svcName,
+	}).Info("listening")
+	err = srv.Wait()
+	if err != nil {
+		return 1
+	}
+	return 0
+}
+
+const rfc3339NanoFixed = "2006-01-02T15:04:05.000000000Z07:00"
diff --git a/sdk/go/arvados/config.go b/sdk/go/arvados/config.go
index 2de13d784..875a274dc 100644
--- a/sdk/go/arvados/config.go
+++ b/sdk/go/arvados/config.go
@@ -101,18 +101,31 @@ type SystemNode struct {
 	Workbench   SystemServiceInstance `json:"arvados-workbench"`
 }
 
+type ServiceName string
+
+const (
+	ServiceNameRailsAPI    ServiceName = "arvados-api-server"
+	ServiceNameController  ServiceName = "arvados-controller"
+	ServiceNameNodemanager ServiceName = "arvados-node-manager"
+	ServiceNameWorkbench   ServiceName = "arvados-workbench"
+	ServiceNameWebsocket   ServiceName = "arvados-ws"
+	ServiceNameKeepweb     ServiceName = "keep-web"
+	ServiceNameKeepproxy   ServiceName = "keepproxy"
+	ServiceNameKeepstore   ServiceName = "keepstore"
+)
+
 // ServicePorts returns the configured listening address (or "" if
 // disabled) for each service on the node.
-func (sn *SystemNode) ServicePorts() map[string]string {
-	return map[string]string{
-		"arvados-api-server":   sn.RailsAPI.Listen,
-		"arvados-controller":   sn.Controller.Listen,
-		"arvados-node-manager": sn.Nodemanager.Listen,
-		"arvados-workbench":    sn.Workbench.Listen,
-		"arvados-ws":           sn.Websocket.Listen,
-		"keep-web":             sn.Keepweb.Listen,
-		"keepproxy":            sn.Keepproxy.Listen,
-		"keepstore":            sn.Keepstore.Listen,
+func (sn *SystemNode) ServicePorts() map[ServiceName]string {
+	return map[ServiceName]string{
+		ServiceNameRailsAPI:    sn.RailsAPI.Listen,
+		ServiceNameController:  sn.Controller.Listen,
+		ServiceNameNodemanager: sn.Nodemanager.Listen,
+		ServiceNameWorkbench:   sn.Workbench.Listen,
+		ServiceNameWebsocket:   sn.Websocket.Listen,
+		ServiceNameKeepweb:     sn.Keepweb.Listen,
+		ServiceNameKeepproxy:   sn.Keepproxy.Listen,
+		ServiceNameKeepstore:   sn.Keepstore.Listen,
 	}
 }
 
diff --git a/sdk/go/health/aggregator.go b/sdk/go/health/aggregator.go
index 5edb1f95c..3f3a91800 100644
--- a/sdk/go/health/aggregator.go
+++ b/sdk/go/health/aggregator.go
@@ -87,7 +87,7 @@ type ClusterHealthResponse struct {
 	// exposes problems that can't be expressed in Checks, like
 	// "service S is needed, but isn't configured to run
 	// anywhere."
-	Services map[string]ServiceHealth `json:"services"`
+	Services map[arvados.ServiceName]ServiceHealth `json:"services"`
 }
 
 type CheckResult struct {
@@ -108,7 +108,7 @@ func (agg *Aggregator) ClusterHealth(cluster *arvados.Cluster) ClusterHealthResp
 	resp := ClusterHealthResponse{
 		Health:   "OK",
 		Checks:   make(map[string]CheckResult),
-		Services: make(map[string]ServiceHealth),
+		Services: make(map[arvados.ServiceName]ServiceHealth),
 	}
 
 	mtx := sync.Mutex{}
@@ -128,7 +128,7 @@ func (agg *Aggregator) ClusterHealth(cluster *arvados.Cluster) ClusterHealthResp
 			}
 
 			wg.Add(1)
-			go func(node, svc, addr string) {
+			go func(node string, svc arvados.ServiceName, addr string) {
 				defer wg.Done()
 				var result CheckResult
 				url, err := agg.pingURL(node, addr)
@@ -143,7 +143,7 @@ func (agg *Aggregator) ClusterHealth(cluster *arvados.Cluster) ClusterHealthResp
 
 				mtx.Lock()
 				defer mtx.Unlock()
-				resp.Checks[svc+"+"+url] = result
+				resp.Checks[fmt.Sprintf("%s+%s", svc, url)] = result
 				if result.Health == "OK" {
 					h := resp.Services[svc]
 					h.N++
diff --git a/sdk/go/httpserver/logger.go b/sdk/go/httpserver/logger.go
index 1a4b7c559..ec3fa7fae 100644
--- a/sdk/go/httpserver/logger.go
+++ b/sdk/go/httpserver/logger.go
@@ -19,15 +19,16 @@ type contextKey struct {
 
 var requestTimeContextKey = contextKey{"requestTime"}
 
-var Logger logrus.FieldLogger = logrus.StandardLogger()
-
 // LogRequests wraps an http.Handler, logging each request and
-// response via logrus.
-func LogRequests(h http.Handler) http.Handler {
+// response via logger.
+func LogRequests(logger logrus.FieldLogger, h http.Handler) http.Handler {
+	if logger == nil {
+		logger = logrus.StandardLogger()
+	}
 	return http.HandlerFunc(func(wrapped http.ResponseWriter, req *http.Request) {
 		w := &responseTimer{ResponseWriter: WrapResponseWriter(wrapped)}
 		req = req.WithContext(context.WithValue(req.Context(), &requestTimeContextKey, time.Now()))
-		lgr := Logger.WithFields(logrus.Fields{
+		lgr := logger.WithFields(logrus.Fields{
 			"RequestID":       req.Header.Get("X-Request-Id"),
 			"remoteAddr":      req.RemoteAddr,
 			"reqForwardedFor": req.Header.Get("X-Forwarded-For"),
diff --git a/sdk/go/httpserver/logger_test.go b/sdk/go/httpserver/logger_test.go
index bbcafa143..bdde3303e 100644
--- a/sdk/go/httpserver/logger_test.go
+++ b/sdk/go/httpserver/logger_test.go
@@ -9,11 +9,10 @@ import (
 	"encoding/json"
 	"net/http"
 	"net/http/httptest"
-	"os"
 	"testing"
 	"time"
 
-	log "github.com/Sirupsen/logrus"
+	"github.com/Sirupsen/logrus"
 	check "gopkg.in/check.v1"
 )
 
@@ -26,12 +25,13 @@ var _ = check.Suite(&Suite{})
 type Suite struct{}
 
 func (s *Suite) TestLogRequests(c *check.C) {
-	defer log.SetOutput(os.Stdout)
 	captured := &bytes.Buffer{}
-	log.SetOutput(captured)
-	log.SetFormatter(&log.JSONFormatter{
+	log := logrus.New()
+	log.Out = captured
+	log.Formatter = &logrus.JSONFormatter{
 		TimestampFormat: time.RFC3339Nano,
-	})
+	}
+
 	h := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
 		w.Write([]byte("hello world"))
 	})
@@ -39,7 +39,7 @@ func (s *Suite) TestLogRequests(c *check.C) {
 	req.Header.Set("X-Forwarded-For", "1.2.3.4:12345")
 	c.Assert(err, check.IsNil)
 	resp := httptest.NewRecorder()
-	AddRequestIDs(LogRequests(h)).ServeHTTP(resp, req)
+	AddRequestIDs(LogRequests(log, h)).ServeHTTP(resp, req)
 
 	dec := json.NewDecoder(captured)
 
diff --git a/services/keep-web/server.go b/services/keep-web/server.go
index 2995bd30a..e51376c3b 100644
--- a/services/keep-web/server.go
+++ b/services/keep-web/server.go
@@ -14,7 +14,7 @@ type server struct {
 }
 
 func (srv *server) Start() error {
-	srv.Handler = httpserver.AddRequestIDs(httpserver.LogRequests(&handler{Config: srv.Config}))
+	srv.Handler = httpserver.AddRequestIDs(httpserver.LogRequests(nil, &handler{Config: srv.Config}))
 	srv.Addr = srv.Config.Listen
 	return srv.Server.Start()
 }
diff --git a/services/keepproxy/keepproxy.go b/services/keepproxy/keepproxy.go
index 07fc63b63..b6c8bd66a 100644
--- a/services/keepproxy/keepproxy.go
+++ b/services/keepproxy/keepproxy.go
@@ -182,7 +182,7 @@ func main() {
 
 	// Start serving requests.
 	router = MakeRESTRouter(!cfg.DisableGet, !cfg.DisablePut, kc, time.Duration(cfg.Timeout), cfg.ManagementToken)
-	http.Serve(listener, httpserver.AddRequestIDs(httpserver.LogRequests(router)))
+	http.Serve(listener, httpserver.AddRequestIDs(httpserver.LogRequests(nil, router)))
 
 	log.Println("shutting down")
 }
diff --git a/services/keepstore/handlers.go b/services/keepstore/handlers.go
index a84a84db3..fb327a386 100644
--- a/services/keepstore/handlers.go
+++ b/services/keepstore/handlers.go
@@ -92,7 +92,7 @@ func MakeRESTRouter() http.Handler {
 
 	mux := http.NewServeMux()
 	mux.Handle("/", theConfig.metrics.Instrument(
-		httpserver.AddRequestIDs(httpserver.LogRequests(rtr.limiter))))
+		httpserver.AddRequestIDs(httpserver.LogRequests(nil, rtr.limiter))))
 	mux.HandleFunc("/metrics.json", theConfig.metrics.exportJSON)
 	mux.Handle("/metrics", theConfig.metrics.exportProm)
 

commit 72bd39971753efa7e951b945d07e8d9704a07221
Author: Tom Clegg <tclegg at veritasgenetics.com>
Date:   Thu Jun 7 15:23:05 2018 -0400

    13497: Build arvados-server and arvados-controller packages.
    
    Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tclegg at veritasgenetics.com>

diff --git a/build/run-build-packages.sh b/build/run-build-packages.sh
index 63f81832f..61dc07e4a 100755
--- a/build/run-build-packages.sh
+++ b/build/run-build-packages.sh
@@ -291,6 +291,10 @@ 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 \
+    "Arvados server daemons"
+package_go_binary cmd/arvados-server arvados-controller \
+    "Arvados cluster controller daemon"
 package_go_binary sdk/go/crunchrunner crunchrunner \
     "Crunchrunner executes a command inside a container and uploads the output"
 package_go_binary services/arv-git-httpd arvados-git-httpd \

commit ceb5ad39a5b94ba26d4a4a059f7801b758ddcec8
Author: Tom Clegg <tclegg at veritasgenetics.com>
Date:   Wed Jun 6 15:25:45 2018 -0400

    13427: More symlink hack
    
    Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tclegg at veritasgenetics.com>

diff --git a/build/run-tests.sh b/build/run-tests.sh
index 40d37621e..9bcc5ba05 100755
--- a/build/run-tests.sh
+++ b/build/run-tests.sh
@@ -516,12 +516,17 @@ export GOPATH
     set -e
     mkdir -p "$GOPATH/src/git.curoverse.com"
     rmdir -v --parents --ignore-fail-on-non-empty "${temp}/GOPATH"
+    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"; do
+            [[ -d "$d" ]] && rmdir "$d"
+        done
+    fi
     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
-        [[ -d "$d" ]] && rmdir "$d"
         [[ -h "$d" ]] && rm "$d"
     done
     ln -vsfT "$WORKSPACE" "$GOPATH/src/git.curoverse.com/arvados.git"

commit 220778381f3a6aa6988c682f914fb9baeada85be
Author: Tom Clegg <tclegg at veritasgenetics.com>
Date:   Thu Jun 7 10:20:29 2018 -0400

    13497: Add arvados-server command.
    
    Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tclegg at veritasgenetics.com>

diff --git a/build/run-tests.sh b/build/run-tests.sh
index 17a4fbd35..40d37621e 100755
--- a/build/run-tests.sh
+++ b/build/run-tests.sh
@@ -70,6 +70,7 @@ apps/workbench_integration (*)
 apps/workbench_benchmark
 apps/workbench_profile
 cmd/arvados-client
+cmd/arvados-server
 doc
 lib/cli
 lib/cmd
@@ -891,6 +892,7 @@ do_install services/api apiserver
 declare -a gostuff
 gostuff=(
     cmd/arvados-client
+    cmd/arvados-server
     lib/cli
     lib/cmd
     lib/controller
diff --git a/cmd/arvados-client/cmd.go b/cmd/arvados-client/cmd.go
index b616b54bd..4550ae53a 100644
--- a/cmd/arvados-client/cmd.go
+++ b/cmd/arvados-client/cmd.go
@@ -5,24 +5,19 @@
 package main
 
 import (
-	"fmt"
-	"io"
 	"os"
-	"regexp"
-	"runtime"
 
 	"git.curoverse.com/arvados.git/lib/cli"
 	"git.curoverse.com/arvados.git/lib/cmd"
 )
 
 var (
-	version                = "dev"
-	cmdVersion cmd.Handler = versionCmd{}
-	handler                = cmd.Multi(map[string]cmd.Handler{
-		"-e":        cmdVersion,
-		"version":   cmdVersion,
-		"-version":  cmdVersion,
-		"--version": cmdVersion,
+	version = "dev"
+	handler = cmd.Multi(map[string]cmd.Handler{
+		"-e":        cmd.Version(version),
+		"version":   cmd.Version(version),
+		"-version":  cmd.Version(version),
+		"--version": cmd.Version(version),
 
 		"copy":     cli.Copy,
 		"create":   cli.Create,
@@ -61,14 +56,6 @@ var (
 	})
 )
 
-type versionCmd struct{}
-
-func (versionCmd) RunCommand(prog string, args []string, _ io.Reader, stdout, _ io.Writer) int {
-	prog = regexp.MustCompile(` -*version$`).ReplaceAllLiteralString(prog, "")
-	fmt.Fprintf(stdout, "%s %s (%s)\n", prog, version, runtime.Version())
-	return 0
-}
-
 func fixLegacyArgs(args []string) []string {
 	flags, _ := cli.LegacyFlagSet()
 	return cmd.SubcommandToFront(args, flags)
diff --git a/cmd/arvados-server/cmd.go b/cmd/arvados-server/cmd.go
new file mode 100644
index 000000000..1af3745df
--- /dev/null
+++ b/cmd/arvados-server/cmd.go
@@ -0,0 +1,27 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+package main
+
+import (
+	"os"
+
+	"git.curoverse.com/arvados.git/lib/cmd"
+	"git.curoverse.com/arvados.git/lib/controller"
+)
+
+var (
+	version = "dev"
+	handler = cmd.Multi(map[string]cmd.Handler{
+		"version":   cmd.Version(version),
+		"-version":  cmd.Version(version),
+		"--version": cmd.Version(version),
+
+		"controller": controller.Command,
+	})
+)
+
+func main() {
+	os.Exit(handler.RunCommand(os.Args[0], os.Args[1:], os.Stdin, os.Stdout, os.Stderr))
+}
diff --git a/lib/cmd/cmd.go b/lib/cmd/cmd.go
index 8b8427a70..0d3a07a61 100644
--- a/lib/cmd/cmd.go
+++ b/lib/cmd/cmd.go
@@ -12,6 +12,8 @@ import (
 	"io"
 	"io/ioutil"
 	"path/filepath"
+	"regexp"
+	"runtime"
 	"sort"
 	"strings"
 )
@@ -26,6 +28,14 @@ func (f HandlerFunc) RunCommand(prog string, args []string, stdin io.Reader, std
 	return f(prog, args, stdin, stdout, stderr)
 }
 
+type Version string
+
+func (v Version) RunCommand(prog string, args []string, stdin io.Reader, stdout, stderr io.Writer) int {
+	prog = regexp.MustCompile(` -*version$`).ReplaceAllLiteralString(prog, "")
+	fmt.Fprintf(stdout, "%s %s (%s)\n", prog, v, runtime.Version())
+	return 0
+}
+
 // Multi is a Handler that looks up its first argument in a map, and
 // invokes the resulting Handler with the remaining args.
 //

commit 25a80e9318880fbff91289ac8f70e1cae4c132a2
Author: Tom Clegg <tclegg at veritasgenetics.com>
Date:   Thu Jun 7 09:33:20 2018 -0400

    13497: Use basename($0) as subcommand, if it is one.
    
    Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tclegg at veritasgenetics.com>

diff --git a/lib/cmd/cmd.go b/lib/cmd/cmd.go
index 2cc71e68a..8b8427a70 100644
--- a/lib/cmd/cmd.go
+++ b/lib/cmd/cmd.go
@@ -11,6 +11,7 @@ import (
 	"fmt"
 	"io"
 	"io/ioutil"
+	"path/filepath"
 	"sort"
 	"strings"
 )
@@ -46,12 +47,15 @@ func (m Multi) RunCommand(prog string, args []string, stdin io.Reader, stdout, s
 		m.Usage(stderr)
 		return 2
 	}
-	if cmd, ok := m[args[0]]; !ok {
-		fmt.Fprintf(stderr, "unrecognized command %q\n", args[0])
+	_, basename := filepath.Split(prog)
+	if cmd, ok := m[basename]; ok {
+		return cmd.RunCommand(prog, args, stdin, stdout, stderr)
+	} else if cmd, ok = m[args[0]]; ok {
+		return cmd.RunCommand(prog+" "+args[0], args[1:], stdin, stdout, stderr)
+	} else {
+		fmt.Fprintf(stderr, "%s: unrecognized command %q\n", prog, args[0])
 		m.Usage(stderr)
 		return 2
-	} else {
-		return cmd.RunCommand(prog+" "+args[0], args[1:], stdin, stdout, stderr)
 	}
 }
 
diff --git a/lib/cmd/cmd_test.go b/lib/cmd/cmd_test.go
index d8a486157..2fc50985f 100644
--- a/lib/cmd/cmd_test.go
+++ b/lib/cmd/cmd_test.go
@@ -42,6 +42,16 @@ func (s *CmdSuite) TestHello(c *check.C) {
 	c.Check(stderr.String(), check.Equals, "")
 }
 
+func (s *CmdSuite) TestHelloViaProg(c *check.C) {
+	defer cmdtest.LeakCheck(c)()
+	stdout := bytes.NewBuffer(nil)
+	stderr := bytes.NewBuffer(nil)
+	exited := testCmd.RunCommand("/usr/local/bin/echo", []string{"hello", "world"}, bytes.NewReader(nil), stdout, stderr)
+	c.Check(exited, check.Equals, 0)
+	c.Check(stdout.String(), check.Equals, "hello world\n")
+	c.Check(stderr.String(), check.Equals, "")
+}
+
 func (s *CmdSuite) TestUsage(c *check.C) {
 	defer cmdtest.LeakCheck(c)()
 	stdout := bytes.NewBuffer(nil)
@@ -49,7 +59,7 @@ func (s *CmdSuite) TestUsage(c *check.C) {
 	exited := testCmd.RunCommand("prog", []string{"nosuchcommand", "hi"}, bytes.NewReader(nil), stdout, stderr)
 	c.Check(exited, check.Equals, 2)
 	c.Check(stdout.String(), check.Equals, "")
-	c.Check(stderr.String(), check.Matches, `(?ms)^unrecognized command "nosuchcommand"\n.*echo.*\n`)
+	c.Check(stderr.String(), check.Matches, `(?ms)^prog: unrecognized command "nosuchcommand"\n.*echo.*\n`)
 }
 
 func (s *CmdSuite) TestSubcommandToFront(c *check.C) {

commit a788135c352c36d1a905c7630423ba57b2ae072a
Author: Tom Clegg <tclegg at veritasgenetics.com>
Date:   Thu Jun 7 00:46:47 2018 -0400

    13497: Add controller, proxy to Rails API.
    
    Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tclegg at veritasgenetics.com>

diff --git a/build/run-tests.sh b/build/run-tests.sh
index 1b6e4f142..17a4fbd35 100755
--- a/build/run-tests.sh
+++ b/build/run-tests.sh
@@ -73,6 +73,7 @@ cmd/arvados-client
 doc
 lib/cli
 lib/cmd
+lib/controller
 lib/crunchstat
 lib/dispatchcloud
 services/api
@@ -892,6 +893,7 @@ gostuff=(
     cmd/arvados-client
     lib/cli
     lib/cmd
+    lib/controller
     lib/crunchstat
     lib/dispatchcloud
     sdk/go/arvados
diff --git a/lib/controller/cmd.go b/lib/controller/cmd.go
new file mode 100644
index 000000000..c13d0fa07
--- /dev/null
+++ b/lib/controller/cmd.go
@@ -0,0 +1,79 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+package controller
+
+import (
+	"flag"
+	"fmt"
+	"io"
+	"net/http"
+
+	"git.curoverse.com/arvados.git/lib/cmd"
+	"git.curoverse.com/arvados.git/sdk/go/arvados"
+	"git.curoverse.com/arvados.git/sdk/go/httpserver"
+	"github.com/Sirupsen/logrus"
+)
+
+const rfc3339NanoFixed = "2006-01-02T15:04:05.000000000Z07:00"
+
+var Command cmd.Handler = &command{}
+
+type command struct{}
+
+func (*command) RunCommand(prog string, args []string, stdin io.Reader, stdout, stderr io.Writer) int {
+	log := logrus.StandardLogger()
+	log.Formatter = &logrus.JSONFormatter{
+		TimestampFormat: rfc3339NanoFixed,
+	}
+	log.Out = stderr
+
+	var err error
+	defer func() {
+		if err != nil {
+			log.WithError(err).Info("exiting")
+		}
+	}()
+	flags := flag.NewFlagSet("", flag.ContinueOnError)
+	flags.SetOutput(stderr)
+	configFile := flags.String("config", arvados.DefaultConfigFile, "Site configuration `file`")
+	err = flags.Parse(args)
+	if err != nil {
+		return 2
+	}
+	cfg, err := arvados.GetConfig(*configFile)
+	if err != nil {
+		return 1
+	}
+	cluster, err := cfg.GetCluster("")
+	if err != nil {
+		return 1
+	}
+	node, err := cluster.GetThisSystemNode()
+	if err != nil {
+		return 1
+	}
+	if node.Controller.Listen == "" {
+		err = fmt.Errorf("configuration does not run a controller on this host: Clusters[%q].SystemNodes[`hostname` or *].Controller.Listen == \"\"", cluster.ClusterID)
+		return 1
+	}
+	srv := &httpserver.Server{
+		Server: http.Server{
+			Handler: httpserver.LogRequests(&Handler{
+				Cluster: cluster,
+			}),
+		},
+		Addr: node.Controller.Listen,
+	}
+	err = srv.Start()
+	if err != nil {
+		return 1
+	}
+	log.WithField("Listen", srv.Addr).Info("listening")
+	err = srv.Wait()
+	if err != nil {
+		return 1
+	}
+	return 0
+}
diff --git a/lib/controller/handler.go b/lib/controller/handler.go
new file mode 100644
index 000000000..c51f8668b
--- /dev/null
+++ b/lib/controller/handler.go
@@ -0,0 +1,71 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+package controller
+
+import (
+	"io"
+	"net/http"
+	"net/url"
+	"sync"
+
+	"git.curoverse.com/arvados.git/sdk/go/arvados"
+	"git.curoverse.com/arvados.git/sdk/go/health"
+	"git.curoverse.com/arvados.git/sdk/go/httpserver"
+)
+
+type Handler struct {
+	Cluster *arvados.Cluster
+
+	setupOnce    sync.Once
+	mux          http.ServeMux
+	handlerStack http.Handler
+	proxyClient  *arvados.Client
+}
+
+func (h *Handler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
+	h.setupOnce.Do(h.setup)
+	h.handlerStack.ServeHTTP(w, req)
+}
+
+func (h *Handler) setup() {
+	h.mux.Handle("/_health/", &health.Handler{
+		Token:  h.Cluster.ManagementToken,
+		Prefix: "/_health/",
+	})
+	h.mux.Handle("/", http.HandlerFunc(h.proxyRailsAPI))
+	h.handlerStack = httpserver.LogRequests(&h.mux)
+}
+
+func (h *Handler) proxyRailsAPI(w http.ResponseWriter, incomingReq *http.Request) {
+	url, err := findRailsAPI(h.Cluster)
+	if err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+	req := *incomingReq
+	req.URL.Host = url.Host
+	resp, err := arvados.InsecureHTTPClient.Do(&req)
+	if err != nil {
+		http.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)
+	io.Copy(w, resp.Body)
+}
+
+// For now, findRailsAPI always uses the rails API running on this
+// node.
+func findRailsAPI(cluster *arvados.Cluster) (*url.URL, error) {
+	node, err := cluster.GetThisSystemNode()
+	if err != nil {
+		return nil, err
+	}
+	return url.Parse("http://" + node.RailsAPI.Listen)
+}
diff --git a/sdk/go/arvados/config.go b/sdk/go/arvados/config.go
index 9ed0eacf2..2de13d784 100644
--- a/sdk/go/arvados/config.go
+++ b/sdk/go/arvados/config.go
@@ -90,6 +90,7 @@ func (cc *Cluster) GetSystemNode(node string) (*SystemNode, error) {
 }
 
 type SystemNode struct {
+	Controller  SystemServiceInstance `json:"arvados-controller"`
 	Health      SystemServiceInstance `json:"arvados-health"`
 	Keepproxy   SystemServiceInstance `json:"keepproxy"`
 	Keepstore   SystemServiceInstance `json:"keepstore"`
@@ -105,6 +106,7 @@ type SystemNode struct {
 func (sn *SystemNode) ServicePorts() map[string]string {
 	return map[string]string{
 		"arvados-api-server":   sn.RailsAPI.Listen,
+		"arvados-controller":   sn.Controller.Listen,
 		"arvados-node-manager": sn.Nodemanager.Listen,
 		"arvados-workbench":    sn.Workbench.Listen,
 		"arvados-ws":           sn.Websocket.Listen,

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


hooks/post-receive
-- 




More information about the arvados-commits mailing list