[ARVADOS] updated: 1.3.0-2298-g37ab0eede

Git user git at public.arvados.org
Thu Feb 27 14:26:13 UTC 2020


Summary of changes:
 .../app/controllers/virtual_machines_controller.rb |   4 +-
 doc/api/methods.html.textile.liquid                |   1 +
 go.mod                                             |   6 +-
 go.sum                                             |   9 +-
 lib/boot/cert.go                                   |  20 +-
 lib/boot/cmd.go                                    | 648 +--------------------
 lib/boot/nginx.go                                  |  58 +-
 lib/boot/passenger.go                              |  52 +-
 lib/boot/postgresql.go                             |  34 +-
 lib/boot/seed.go                                   |   7 +-
 lib/boot/service.go                                |  87 ++-
 lib/boot/{cmd.go => supervisor.go}                 | 427 +++++++-------
 lib/controller/federation/conn.go                  |  51 +-
 lib/controller/federation/login_test.go            |  34 ++
 lib/controller/handler.go                          |   1 +
 lib/controller/handler_test.go                     |  47 ++
 lib/controller/integration_test.go                 | 145 +++--
 lib/controller/localdb/conn.go                     |   9 +
 lib/controller/localdb/login.go                    |  12 +
 lib/controller/localdb/login_test.go               |   6 +
 lib/controller/router/router.go                    |   7 +
 lib/controller/rpc/conn.go                         |   8 +
 lib/controller/rpc/conn_test.go                    |  10 +
 lib/service/cmd.go                                 |   2 +-
 lib/{boot => service}/log.go                       |   6 +-
 sdk/go/arvados/api.go                              |   7 +
 sdk/go/arvados/collection.go                       |  27 +
 sdk/go/arvados/login.go                            |   9 +
 sdk/go/arvadostest/api.go                          |   4 +
 .../controllers/arvados/v1/schema_controller.rb    |   2 +-
 .../app/controllers/arvados/v1/users_controller.rb |   5 +-
 .../api/app/models/api_client_authorization.rb     |   2 +-
 services/api/lib/record_filters.rb                 |   6 +-
 services/api/test/fixtures/collections.yml         |  49 +-
 .../api/test/functional/arvados/v1/filters_test.rb |   7 +
 tools/arvbox/lib/arvbox/docker/common.sh           |   4 +-
 tools/arvbox/lib/arvbox/docker/service/nginx/run   |   7 +-
 .../lib/arvbox/docker/service/sdk/run-service      |   6 +-
 38 files changed, 795 insertions(+), 1031 deletions(-)
 copy lib/boot/{cmd.go => supervisor.go} (52%)
 rename lib/{boot => service}/log.go (84%)

       via  37ab0eedec5eaf99c27b6b64fd04cc9248081713 (commit)
       via  8b1b770d208c2886c7b46a502f0d67d8fdcc919a (commit)
       via  13c4aa05bce66154212061414836fac7732ebc64 (commit)
       via  e20adcd07cefcf4f35d6b778567fcfb46e197246 (commit)
       via  ae47f5fc8d181bc26e96218286e8801a20bfe8d7 (commit)
       via  f26e039288e744510a6478b7b2597d3c494fb2d1 (commit)
       via  05c56c61a16b35a896f45fa949334a23cb4a3355 (commit)
       via  cfdbd3cf6f137d642f26664c935fdb33a0eb6b8e (commit)
       via  8975e5ccaa3d39f611dec459f066181277f03454 (commit)
       via  7b826c628bf2f327f327540651a49af89ce045a1 (commit)
       via  168fedbe65526ff3eabf155039d6e55a8f5eadf6 (commit)
       via  dbc875430e4329862d780f12b804b716a90fc651 (commit)
       via  e470a947218ecf042ca75c69d4579a052199e570 (commit)
       via  7caabd9c19c46ebc218d10b2c048e36d6e8cb2a4 (commit)
       via  9acd9d8cdb6425b0ed40ed1800f3fe2d932c5d03 (commit)
       via  f1a81ab9bb1da527e977f1a5667f86e37976bd4a (commit)
       via  348abbad014278d061b9f93ecbb5c5bedc03ae86 (commit)
       via  8dedaac5a1758fab15243f82acab2092fc24e2df (commit)
       via  feb290061b91fa059aefd251ed3c3532b32620ea (commit)
       via  4f88adddbe3a15dd0cfd88b0f939f4e6d1e16611 (commit)
       via  75fa01ce18ee8de9ad689b0aa20e9a9c485526a7 (commit)
       via  79e21a2dffa00d854631627a2a07a6bd8e130b51 (commit)
       via  ce014b06b594d9d368187189ac01b41a238e54e1 (commit)
       via  8ebed6625b925e1ae5c18b162560f37308335bad (commit)
       via  d89876219e668a3a97a6c61f92320bad0c0527c8 (commit)
       via  b374dbf6b4ec13421570fa13ecebc6b23e19dbab (commit)
       via  494226025d5464d7cdef70d99094fe26feddbb4d (commit)
       via  edefa841e564018ebf99320b6596898ba3f9c63e (commit)
       via  67a252633f9e0f7e55cc9feff1964221dd818cfe (commit)
       via  e15c73164eaedc121420b328a7b26da3c35bc145 (commit)
       via  64e387b2f4f0fe6c4c7bf16232706c7cf194caf0 (commit)
       via  1d922bf219f74efcd5fda01ed127f7ccae0790c4 (commit)
       via  e459f4e2d40762f67ffedafbe988c8da6f4f04d4 (commit)
       via  8141f4c40e11415b0b37ef30f351364f50c7760e (commit)
       via  dd7da09fe29a98b2af2c3d8fd428d47e0d1e5d74 (commit)
       via  8a88ceb33a00a83708fab9a0e3f5ad5e95bac2a6 (commit)
       via  f497a5625ccfc1c359e17fa88ac6edb80181244d (commit)
       via  1fa3a92ed6100aac7ac8af68095d199effeea344 (commit)
       via  fb5491f94ace7bbba4666a0a2aa719acd7cbfb7b (commit)
       via  1672501e88972c94bdcab5f681229eb9e9f4a4f6 (commit)
      from  71b3aadcbf0d8a3c0b283fc27a17773951d417c7 (commit)

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


commit 37ab0eedec5eaf99c27b6b64fd04cc9248081713
Merge: 8b1b770d2 e20adcd07
Author: Tom Clegg <tom at tomclegg.ca>
Date:   Thu Feb 27 09:25:20 2020 -0500

    15954: Merge branch 'master'
    
    Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tom at tomclegg.ca>


commit 8b1b770d208c2886c7b46a502f0d67d8fdcc919a
Author: Tom Clegg <tom at tomclegg.ca>
Date:   Thu Feb 27 09:23:24 2020 -0500

    15954: Remove unused LibPath.
    
    Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tom at tomclegg.ca>

diff --git a/lib/boot/cmd.go b/lib/boot/cmd.go
index f2266d6b5..60955dfa0 100644
--- a/lib/boot/cmd.go
+++ b/lib/boot/cmd.go
@@ -52,7 +52,6 @@ func (bootCommand) RunCommand(prog string, args []string, stdin io.Reader, stdou
 	loader.SetupFlags(flags)
 	versionFlag := flags.Bool("version", false, "Write version information to stdout and exit 0")
 	flags.StringVar(&super.SourcePath, "source", ".", "arvados source tree `directory`")
-	flags.StringVar(&super.LibPath, "lib", "/var/lib/arvados", "`directory` to install dependencies and library files")
 	flags.StringVar(&super.ClusterType, "type", "production", "cluster `type`: development, test, or production")
 	flags.StringVar(&super.ListenHost, "listen-host", "localhost", "host name or interface address for service listeners")
 	flags.StringVar(&super.ControllerAddr, "controller-address", ":0", "desired controller address, `host:port` or `:port`")
diff --git a/lib/boot/supervisor.go b/lib/boot/supervisor.go
index 3e297d84e..e10fa23fa 100644
--- a/lib/boot/supervisor.go
+++ b/lib/boot/supervisor.go
@@ -34,7 +34,6 @@ import (
 type Supervisor struct {
 	SourcePath           string // e.g., /home/username/src/arvados
 	SourceVersion        string // e.g., acbd1324...
-	LibPath              string // e.g., /var/lib/arvados
 	ClusterType          string // e.g., production
 	ListenHost           string // e.g., localhost
 	ControllerAddr       string // e.g., 127.0.0.1:8000
@@ -128,7 +127,6 @@ func (super *Supervisor) run(cfg *arvados.Config) error {
 	super.setEnv("RAILS_ENV", super.ClusterType)
 	super.setEnv("TMPDIR", super.tempdir)
 	super.prependEnv("PATH", filepath.Join(super.tempdir, "bin")+":")
-	super.prependEnv("PATH", filepath.Join(super.LibPath, "bin")+":")
 
 	super.cluster, err = cfg.GetCluster("")
 	if err != nil {
@@ -164,16 +162,7 @@ func (super *Supervisor) run(cfg *arvados.Config) error {
 	} else {
 		return errors.New("specifying a version to run is not yet supported")
 	}
-	for _, dir := range []string{super.LibPath, filepath.Join(super.LibPath, "bin")} {
-		if _, err = os.Stat(filepath.Join(dir, ".")); os.IsNotExist(err) {
-			err = os.Mkdir(dir, 0755)
-			if err != nil {
-				return err
-			}
-		} else if err != nil {
-			return err
-		}
-	}
+
 	_, err = super.installGoProgram(super.ctx, "cmd/arvados-server")
 	if err != nil {
 		return err
diff --git a/lib/controller/integration_test.go b/lib/controller/integration_test.go
index 3afdc0b3d..2adb5811e 100644
--- a/lib/controller/integration_test.go
+++ b/lib/controller/integration_test.go
@@ -105,7 +105,6 @@ func (s *IntegrationSuite) SetUpSuite(c *check.C) {
 		s.testClusters[id] = &testCluster{
 			super: boot.Supervisor{
 				SourcePath:           filepath.Join(cwd, "..", ".."),
-				LibPath:              filepath.Join(cwd, "..", "..", "tmp"),
 				ClusterType:          "test",
 				ListenHost:           "127.0.0." + id[3:],
 				ControllerAddr:       ":0",

commit 13c4aa05bce66154212061414836fac7732ebc64
Author: Tom Clegg <tom at tomclegg.ca>
Date:   Wed Feb 26 16:56:18 2020 -0500

    15954: Check addr/port for unsupported configs.
    
    Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tom at tomclegg.ca>

diff --git a/go.mod b/go.mod
index 6d5f748d4..9a139448a 100644
--- a/go.mod
+++ b/go.mod
@@ -49,10 +49,10 @@ require (
 	github.com/src-d/gcfg v1.3.0 // indirect
 	github.com/stretchr/testify v1.4.0 // indirect
 	github.com/xanzy/ssh-agent v0.1.0 // indirect
-	golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2
-	golang.org/x/net v0.0.0-20190613194153-d28f0bde5980
+	golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550
+	golang.org/x/net v0.0.0-20190620200207-3b0461eec859
 	golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45
-	golang.org/x/sys v0.0.0-20191105231009-c1f44814a5cd
+	golang.org/x/sys v0.0.0-20191105231009-c1f44814a5cd // indirect
 	google.golang.org/api v0.13.0
 	gopkg.in/check.v1 v1.0.0-20161208181325-20d25e280405
 	gopkg.in/square/go-jose.v2 v2.3.1
diff --git a/go.sum b/go.sum
index 253865d63..48b23d796 100644
--- a/go.sum
+++ b/go.sum
@@ -111,8 +111,6 @@ github.com/kevinburke/ssh_config v0.0.0-20171013211458-802051befeb5/go.mod h1:CT
 github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
 github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
 github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
-github.com/lib/pq v0.0.0-20171126050459-83612a56d3dd h1:2RDaVc4/izhWyAvYxNm8c9saSyCDIxefNwOcqaH7pcU=
-github.com/lib/pq v0.0.0-20171126050459-83612a56d3dd/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
 github.com/lib/pq v1.3.0 h1:/qkRGz8zljWiDcFvgpwUpwIAPu3r07TDvs3Rws+o/pU=
 github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
 github.com/marstr/guid v1.1.1-0.20170427235115-8bdf7d1a087c h1:ouxemItv3B/Zh008HJkEXDYCN3BIRyNHxtUN7ThJ5Js=
@@ -176,6 +174,8 @@ go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
 golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8=
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
 golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
@@ -188,10 +188,13 @@ golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73r
 golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c h1:uOCk1iQW6Vc18bnC13MfzScl+wdKBmM9Y9kU7Z83/lw=
 golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190613194153-d28f0bde5980 h1:dfGZHvZk057jK2MCeWus/TowKpJ8y4AmooUzdBSR9GU=
 golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0=
@@ -205,6 +208,7 @@ golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5h
 golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -220,6 +224,7 @@ golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGm
 golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
 golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
 golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c h1:97SnQk1GYRXJgvwZ8fadnxDOWfKvkNQHH3CtZntPSrM=
 golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
 google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
 google.golang.org/api v0.13.0 h1:Q3Ui3V3/CVinFWFiW39Iw0kMuVrRzYX0wN6OPFp0lTA=
diff --git a/lib/boot/nginx.go b/lib/boot/nginx.go
index 2d5c74594..6b2d6777f 100644
--- a/lib/boot/nginx.go
+++ b/lib/boot/nginx.go
@@ -8,6 +8,7 @@ import (
 	"context"
 	"fmt"
 	"io/ioutil"
+	"net"
 	"os"
 	"os/exec"
 	"path/filepath"
@@ -46,14 +47,23 @@ func (runNginx) Run(ctx context.Context, fail func(error), super *Supervisor) er
 		{"WORKBENCH1", super.cluster.Services.Workbench1},
 		{"WS", super.cluster.Services.Websocket},
 	} {
-		vars[cmpt.varname+"PORT"], err = internalPort(cmpt.svc)
+		port, err := internalPort(cmpt.svc)
 		if err != nil {
 			return fmt.Errorf("%s internal port: %s (%v)", cmpt.varname, err, cmpt.svc)
 		}
-		vars[cmpt.varname+"SSLPORT"], err = externalPort(cmpt.svc)
+		if ok, err := addrIsLocal(net.JoinHostPort(super.ListenHost, port)); !ok || err != nil {
+			return fmt.Errorf("urlIsLocal() failed for host %q port %q: %v", super.ListenHost, port, err)
+		}
+		vars[cmpt.varname+"PORT"] = port
+
+		port, err = externalPort(cmpt.svc)
 		if err != nil {
 			return fmt.Errorf("%s external port: %s (%v)", cmpt.varname, err, cmpt.svc)
 		}
+		if ok, err := addrIsLocal(net.JoinHostPort(super.ListenHost, port)); !ok || err != nil {
+			return fmt.Errorf("urlIsLocal() failed for host %q port %q: %v", super.ListenHost, port, err)
+		}
+		vars[cmpt.varname+"SSLPORT"] = port
 	}
 	tmpl, err := ioutil.ReadFile(filepath.Join(super.SourcePath, "sdk", "python", "tests", "nginx.conf"))
 	if err != nil {
diff --git a/lib/boot/passenger.go b/lib/boot/passenger.go
index 36be2f1a0..7ebb36e47 100644
--- a/lib/boot/passenger.go
+++ b/lib/boot/passenger.go
@@ -90,7 +90,7 @@ func (runner runPassenger) Run(ctx context.Context, fail func(error), super *Sup
 	}
 	port, err := internalPort(runner.svc)
 	if err != nil {
-		return fmt.Errorf("bug: no InternalURLs for component %q: %v", runner, runner.svc.InternalURLs)
+		return fmt.Errorf("bug: no internalPort for %q: %v (%#v)", runner, err, runner.svc)
 	}
 	loglevel := "4"
 	if lvl, ok := map[string]string{
diff --git a/lib/boot/service.go b/lib/boot/service.go
index 018e9f8bb..5afacfe71 100644
--- a/lib/boot/service.go
+++ b/lib/boot/service.go
@@ -38,6 +38,11 @@ func (runner runServiceCommand) Run(ctx context.Context, fail func(error), super
 	super.wait(ctx, runner.depends...)
 	for u := range runner.svc.InternalURLs {
 		u := u
+		if islocal, err := addrIsLocal(u.Host); err != nil {
+			return err
+		} else if !islocal {
+			continue
+		}
 		super.waitShutdown.Add(1)
 		go func() {
 			defer super.waitShutdown.Done()
@@ -80,6 +85,11 @@ func (runner runGoProgram) Run(ctx context.Context, fail func(error), super *Sup
 	super.wait(ctx, runner.depends...)
 	for u := range runner.svc.InternalURLs {
 		u := u
+		if islocal, err := addrIsLocal(u.Host); err != nil {
+			return err
+		} else if !islocal {
+			continue
+		}
 		super.waitShutdown.Add(1)
 		go func() {
 			defer super.waitShutdown.Done()
diff --git a/lib/boot/supervisor.go b/lib/boot/supervisor.go
index de570a952..3e297d84e 100644
--- a/lib/boot/supervisor.go
+++ b/lib/boot/supervisor.go
@@ -624,6 +624,19 @@ func (super *Supervisor) autofillConfig(cfg *arvados.Config, log logrus.FieldLog
 	return nil
 }
 
+func addrIsLocal(addr string) (bool, error) {
+	return true, nil
+	listener, err := net.Listen("tcp", addr)
+	if err == nil {
+		listener.Close()
+		return true, nil
+	} else if strings.Contains(err.Error(), "cannot assign requested address") {
+		return false, nil
+	} else {
+		return false, err
+	}
+}
+
 func randomHexString(chars int) string {
 	b := make([]byte, chars/2)
 	_, err := rand.Read(b)
@@ -634,6 +647,9 @@ func randomHexString(chars int) string {
 }
 
 func internalPort(svc arvados.Service) (string, error) {
+	if len(svc.InternalURLs) > 1 {
+		return "", errors.New("internalPort() doesn't work with multiple InternalURLs")
+	}
 	for u := range svc.InternalURLs {
 		if _, p, err := net.SplitHostPort(u.Host); err != nil {
 			return "", err

commit 05c56c61a16b35a896f45fa949334a23cb4a3355
Author: Tom Clegg <tom at tomclegg.ca>
Date:   Wed Feb 26 10:18:16 2020 -0500

    15954: Tidy up federation test.
    
    Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tom at tomclegg.ca>

diff --git a/lib/controller/integration_test.go b/lib/controller/integration_test.go
index e308f8726..3afdc0b3d 100644
--- a/lib/controller/integration_test.go
+++ b/lib/controller/integration_test.go
@@ -7,6 +7,7 @@ package controller
 import (
 	"bytes"
 	"context"
+	"io"
 	"net"
 	"net/url"
 	"os"
@@ -163,27 +164,21 @@ func (s *IntegrationSuite) userClients(c *check.C, conn *rpc.Conn, rootctx conte
 	redirURL, err := url.Parse(login.RedirectLocation)
 	c.Assert(err, check.IsNil)
 	userToken := redirURL.Query().Get("api_token")
-	c.Logf("userToken: %q", userToken)
+	c.Logf("user token: %q", userToken)
 	ctx, ac, kc := s.clientsWithToken(clusterID, userToken)
 	user, err := conn.UserGetCurrent(ctx, arvados.GetOptions{})
-	if err != nil {
-		panic(err)
-	}
+	c.Assert(err, check.IsNil)
 	_, err = conn.UserSetup(rootctx, arvados.UserSetupOptions{UUID: user.UUID})
-	if err != nil {
-		panic(err)
-	}
-	_, err = conn.UserActivate(rootctx, arvados.UserActivateOptions{UUID: user.UUID})
-	if err != nil {
-		panic(err)
-	}
-	user, err = conn.UserGetCurrent(ctx, arvados.GetOptions{})
-	if err != nil {
-		panic(err)
-	}
-	c.Logf("user: %#v", user)
-	if !user.IsActive {
-		c.Fatal("failed to activate user")
+	c.Assert(err, check.IsNil)
+	if activate {
+		_, err = conn.UserActivate(rootctx, arvados.UserActivateOptions{UUID: user.UUID})
+		c.Assert(err, check.IsNil)
+		user, err = conn.UserGetCurrent(ctx, arvados.GetOptions{})
+		c.Assert(err, check.IsNil)
+		c.Logf("user UUID: %q", user.UUID)
+		if !user.IsActive {
+			c.Fatalf("failed to activate user -- %#v", user)
+		}
 	}
 	return ctx, ac, kc
 }
@@ -192,30 +187,40 @@ func (s *IntegrationSuite) rootClients(clusterID string) (context.Context, *arva
 	return s.clientsWithToken(clusterID, s.testClusters[clusterID].config.Clusters[clusterID].SystemRootToken)
 }
 
-func (s *IntegrationSuite) TestLoopDetection(c *check.C) {
+func (s *IntegrationSuite) TestGetCollectionByPDH(c *check.C) {
 	conn1 := s.conn("z1111")
 	rootctx1, _, _ := s.rootClients("z1111")
 	conn3 := s.conn("z3333")
-	// rootctx3, _, _ := s.rootClients("z3333")
-
 	userctx1, ac1, kc1 := s.userClients(c, conn1, rootctx1, "z1111", true)
-	_, err := conn1.CollectionGet(userctx1, arvados.GetOptions{UUID: "1f4b0bc7583c2a7f9102c395f4ffc5e3+45"})
-	c.Assert(err, check.ErrorMatches, `.*404 Not Found.*`)
 
+	// Create the collection to find its PDH (but don't save it
+	// anywhere yet)
 	var coll1 arvados.Collection
 	fs1, err := coll1.FileSystem(ac1, kc1)
-	if err != nil {
-		c.Error(err)
-	}
-	f, err := fs1.OpenFile("foo", os.O_CREATE|os.O_RDWR, 0777)
-	f.Write([]byte("foo"))
-	f.Close()
+	c.Assert(err, check.IsNil)
+	f, err := fs1.OpenFile("test.txt", os.O_CREATE|os.O_RDWR, 0777)
+	c.Assert(err, check.IsNil)
+	_, err = io.WriteString(f, "IntegrationSuite.TestGetCollectionByPDH")
+	c.Assert(err, check.IsNil)
+	err = f.Close()
+	c.Assert(err, check.IsNil)
 	mtxt, err := fs1.MarshalManifest(".")
+	c.Assert(err, check.IsNil)
+	pdh := arvados.PortableDataHash(mtxt)
+
+	// Looking up the PDH before saving returns 404 if cycle
+	// detection is working.
+	_, err = conn1.CollectionGet(userctx1, arvados.GetOptions{UUID: pdh})
+	c.Assert(err, check.ErrorMatches, `.*404 Not Found.*`)
+
+	// Save the collection on cluster z1111.
 	coll1, err = conn1.CollectionCreate(userctx1, arvados.CreateOptions{Attrs: map[string]interface{}{
 		"manifest_text": mtxt,
 	}})
 	c.Assert(err, check.IsNil)
-	coll, err := conn3.CollectionGet(userctx1, arvados.GetOptions{UUID: "1f4b0bc7583c2a7f9102c395f4ffc5e3+45"})
+
+	// Retrieve the collection from cluster z3333.
+	coll, err := conn3.CollectionGet(userctx1, arvados.GetOptions{UUID: pdh})
 	c.Check(err, check.IsNil)
-	c.Check(coll.PortableDataHash, check.Equals, "1f4b0bc7583c2a7f9102c395f4ffc5e3+45")
+	c.Check(coll.PortableDataHash, check.Equals, pdh)
 }

commit cfdbd3cf6f137d642f26664c935fdb33a0eb6b8e
Author: Tom Clegg <tom at tomclegg.ca>
Date:   Wed Feb 26 10:17:32 2020 -0500

    15954: Move PortableDataHash func to sdk/go/arvados.
    
    Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tom at tomclegg.ca>

diff --git a/lib/controller/federation/conn.go b/lib/controller/federation/conn.go
index 3eca63705..3909b6cdd 100644
--- a/lib/controller/federation/conn.go
+++ b/lib/controller/federation/conn.go
@@ -7,7 +7,6 @@ package federation
 import (
 	"bytes"
 	"context"
-	"crypto/md5"
 	"encoding/json"
 	"errors"
 	"fmt"
@@ -169,26 +168,6 @@ func rewriteManifest(mt, remoteID string) string {
 	})
 }
 
-// this could be in sdk/go/arvados
-func portableDataHash(mt string) string {
-	h := md5.New()
-	blkRe := regexp.MustCompile(`^ [0-9a-f]{32}\+\d+`)
-	size := 0
-	_ = regexp.MustCompile(` ?[^ ]*`).ReplaceAllFunc([]byte(mt), func(tok []byte) []byte {
-		if m := blkRe.Find(tok); m != nil {
-			// write hash+size, ignore remaining block hints
-			tok = m
-		}
-		n, err := h.Write(tok)
-		if err != nil {
-			panic(err)
-		}
-		size += n
-		return nil
-	})
-	return fmt.Sprintf("%x+%d", h.Sum(nil), size)
-}
-
 func (conn *Conn) ConfigGet(ctx context.Context) (json.RawMessage, error) {
 	var buf bytes.Buffer
 	err := config.ExportJSON(&buf, conn.cluster)
@@ -243,7 +222,7 @@ func (conn *Conn) CollectionGet(ctx context.Context, options arvados.GetOptions)
 			// options.UUID is either hash+size or
 			// hash+size+hints; only hash+size need to
 			// match the computed PDH.
-			if pdh := portableDataHash(c.ManifestText); pdh != options.UUID && !strings.HasPrefix(options.UUID, pdh+"+") {
+			if pdh := arvados.PortableDataHash(c.ManifestText); pdh != options.UUID && !strings.HasPrefix(options.UUID, pdh+"+") {
 				err = httpErrorf(http.StatusBadGateway, "bad portable data hash %q received from remote %q (expected %q)", pdh, remoteID, options.UUID)
 				ctxlog.FromContext(ctx).Warn(err)
 				return err
diff --git a/sdk/go/arvados/collection.go b/sdk/go/arvados/collection.go
index 35fd3fd74..030665d77 100644
--- a/sdk/go/arvados/collection.go
+++ b/sdk/go/arvados/collection.go
@@ -6,7 +6,9 @@ package arvados
 
 import (
 	"bufio"
+	"crypto/md5"
 	"fmt"
+	"regexp"
 	"strings"
 	"time"
 
@@ -90,3 +92,28 @@ type CollectionList struct {
 	Offset         int          `json:"offset"`
 	Limit          int          `json:"limit"`
 }
+
+var (
+	blkRe = regexp.MustCompile(`^ [0-9a-f]{32}\+\d+`)
+	tokRe = regexp.MustCompile(` ?[^ ]*`)
+)
+
+// PortableDataHash computes the portable data hash of the given
+// manifest.
+func PortableDataHash(mt string) string {
+	h := md5.New()
+	size := 0
+	_ = tokRe.ReplaceAllFunc([]byte(mt), func(tok []byte) []byte {
+		if m := blkRe.Find(tok); m != nil {
+			// write hash+size, ignore remaining block hints
+			tok = m
+		}
+		n, err := h.Write(tok)
+		if err != nil {
+			panic(err)
+		}
+		size += n
+		return nil
+	})
+	return fmt.Sprintf("%x+%d", h.Sum(nil), size)
+}

commit 8975e5ccaa3d39f611dec459f066181277f03454
Author: Tom Clegg <tom at tomclegg.ca>
Date:   Wed Feb 26 10:16:38 2020 -0500

    15954: Fix up logging prefix.
    
    Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tom at tomclegg.ca>

diff --git a/lib/boot/supervisor.go b/lib/boot/supervisor.go
index 784428b35..de570a952 100644
--- a/lib/boot/supervisor.go
+++ b/lib/boot/supervisor.go
@@ -412,9 +412,9 @@ func (super *Supervisor) RunProgram(ctx context.Context, dir string, output io.W
 	super.logger.WithField("command", cmdline).WithField("dir", dir).Info("executing")
 
 	logprefix := strings.TrimPrefix(prog, super.tempdir+"/bin/")
-	if prog == "bundle" && len(args) > 2 && args[0] == "exec" {
+	if logprefix == "bundle" && len(args) > 2 && args[0] == "exec" {
 		logprefix = args[1]
-	} else if prog == "arvados-server" && len(args) > 1 {
+	} else if logprefix == "arvados-server" && len(args) > 1 {
 		logprefix = args[0]
 	}
 	if !strings.HasPrefix(dir, "/") {

commit 7b826c628bf2f327f327540651a49af89ce045a1
Author: Tom Clegg <tom at tomclegg.ca>
Date:   Wed Feb 26 02:01:33 2020 -0500

    15954: More careful about finding available ports.
    
    Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tom at tomclegg.ca>

diff --git a/lib/boot/supervisor.go b/lib/boot/supervisor.go
index 59e1ae352..784428b35 100644
--- a/lib/boot/supervisor.go
+++ b/lib/boot/supervisor.go
@@ -503,7 +503,7 @@ func (super *Supervisor) autofillConfig(cfg *arvados.Config, log logrus.FieldLog
 	usedPort := map[string]bool{}
 	nextPort := func() string {
 		for {
-			port, err := availablePort(super.ListenHost + ":0")
+			port, err := availablePort(super.ListenHost)
 			if err != nil {
 				panic(err)
 			}
@@ -523,7 +523,7 @@ func (super *Supervisor) autofillConfig(cfg *arvados.Config, log logrus.FieldLog
 			h = super.ListenHost
 		}
 		if p == "0" {
-			p, err = availablePort(":0")
+			p, err = availablePort(h)
 			if err != nil {
 				return err
 			}
@@ -660,8 +660,8 @@ func externalPort(svc arvados.Service) (string, error) {
 	}
 }
 
-func availablePort(addr string) (string, error) {
-	ln, err := net.Listen("tcp", addr)
+func availablePort(host string) (string, error) {
+	ln, err := net.Listen("tcp", net.JoinHostPort(host, "0"))
 	if err != nil {
 		return "", err
 	}

commit 168fedbe65526ff3eabf155039d6e55a8f5eadf6
Author: Tom Clegg <tom at tomclegg.ca>
Date:   Tue Feb 25 18:44:48 2020 -0500

    15954: Rename booter to supervisor, tidy up.
    
    Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tom at tomclegg.ca>

diff --git a/lib/boot/cert.go b/lib/boot/cert.go
index 560579b77..508605bb7 100644
--- a/lib/boot/cert.go
+++ b/lib/boot/cert.go
@@ -10,25 +10,31 @@ import (
 	"path/filepath"
 )
 
+// Create a root CA key and use it to make a new server
+// certificate+key pair.
+//
+// In future we'll make one root CA key per host instead of one per
+// cluster, so it only needs to be imported to a browser once for
+// ongoing dev/test usage.
 type createCertificates struct{}
 
 func (createCertificates) String() string {
 	return "certificates"
 }
 
-func (createCertificates) Run(ctx context.Context, fail func(error), boot *Booter) error {
+func (createCertificates) Run(ctx context.Context, fail func(error), super *Supervisor) error {
 	// Generate root key
-	err := boot.RunProgram(ctx, boot.tempdir, nil, nil, "openssl", "genrsa", "-out", "rootCA.key", "4096")
+	err := super.RunProgram(ctx, super.tempdir, nil, nil, "openssl", "genrsa", "-out", "rootCA.key", "4096")
 	if err != nil {
 		return err
 	}
 	// Generate a self-signed root certificate
-	err = boot.RunProgram(ctx, boot.tempdir, nil, nil, "openssl", "req", "-x509", "-new", "-nodes", "-key", "rootCA.key", "-sha256", "-days", "3650", "-out", "rootCA.crt", "-subj", "/C=US/ST=MA/O=Example Org/CN=localhost")
+	err = super.RunProgram(ctx, super.tempdir, nil, nil, "openssl", "req", "-x509", "-new", "-nodes", "-key", "rootCA.key", "-sha256", "-days", "3650", "-out", "rootCA.crt", "-subj", "/C=US/ST=MA/O=Example Org/CN=localhost")
 	if err != nil {
 		return err
 	}
 	// Generate server key
-	err = boot.RunProgram(ctx, boot.tempdir, nil, nil, "openssl", "genrsa", "-out", "server.key", "2048")
+	err = super.RunProgram(ctx, super.tempdir, nil, nil, "openssl", "genrsa", "-out", "server.key", "2048")
 	if err != nil {
 		return err
 	}
@@ -37,7 +43,7 @@ func (createCertificates) Run(ctx context.Context, fail func(error), boot *Boote
 	if err != nil {
 		return err
 	}
-	err = ioutil.WriteFile(filepath.Join(boot.tempdir, "server.cfg"), append(defaultconf, []byte(`
+	err = ioutil.WriteFile(filepath.Join(super.tempdir, "server.cfg"), append(defaultconf, []byte(`
 [SAN]
 subjectAltName=DNS:localhost,DNS:localhost.localdomain
 `)...), 0777)
@@ -45,12 +51,12 @@ subjectAltName=DNS:localhost,DNS:localhost.localdomain
 		return err
 	}
 	// Generate signing request
-	err = boot.RunProgram(ctx, boot.tempdir, nil, nil, "openssl", "req", "-new", "-sha256", "-key", "server.key", "-subj", "/C=US/ST=MA/O=Example Org/CN=localhost", "-reqexts", "SAN", "-config", "server.cfg", "-out", "server.csr")
+	err = super.RunProgram(ctx, super.tempdir, nil, nil, "openssl", "req", "-new", "-sha256", "-key", "server.key", "-subj", "/C=US/ST=MA/O=Example Org/CN=localhost", "-reqexts", "SAN", "-config", "server.cfg", "-out", "server.csr")
 	if err != nil {
 		return err
 	}
 	// Sign certificate
-	err = boot.RunProgram(ctx, boot.tempdir, nil, nil, "openssl", "x509", "-req", "-in", "server.csr", "-CA", "rootCA.crt", "-CAkey", "rootCA.key", "-CAcreateserial", "-out", "server.crt", "-days", "3650", "-sha256")
+	err = super.RunProgram(ctx, super.tempdir, nil, nil, "openssl", "x509", "-req", "-in", "server.csr", "-CA", "rootCA.crt", "-CAkey", "rootCA.key", "-CAcreateserial", "-out", "server.crt", "-days", "3650", "-sha256")
 	if err != nil {
 		return err
 	}
diff --git a/lib/boot/cmd.go b/lib/boot/cmd.go
index e8a86e6ba..f2266d6b5 100644
--- a/lib/boot/cmd.go
+++ b/lib/boot/cmd.go
@@ -5,77 +5,58 @@
 package boot
 
 import (
-	"bytes"
 	"context"
-	"crypto/rand"
-	"encoding/json"
-	"errors"
 	"flag"
 	"fmt"
 	"io"
-	"io/ioutil"
-	"net"
-	"os"
-	"os/exec"
-	"os/signal"
-	"os/user"
-	"path/filepath"
-	"strings"
-	"sync"
-	"syscall"
-	"time"
 
 	"git.arvados.org/arvados.git/lib/cmd"
 	"git.arvados.org/arvados.git/lib/config"
-	"git.arvados.org/arvados.git/lib/service"
-	"git.arvados.org/arvados.git/sdk/go/arvados"
 	"git.arvados.org/arvados.git/sdk/go/ctxlog"
-	"git.arvados.org/arvados.git/sdk/go/health"
-	"github.com/sirupsen/logrus"
 )
 
 var Command cmd.Handler = bootCommand{}
 
-type bootTask interface {
+type supervisedTask interface {
 	// Execute the task. Run should return nil when the task is
 	// done enough to satisfy a dependency relationship (e.g., the
 	// service is running and ready). If the task starts a
 	// goroutine that fails after Run returns (e.g., the service
 	// shuts down), it should call cancel.
-	Run(ctx context.Context, fail func(error), boot *Booter) error
+	Run(ctx context.Context, fail func(error), super *Supervisor) error
 	String() string
 }
 
 type bootCommand struct{}
 
 func (bootCommand) RunCommand(prog string, args []string, stdin io.Reader, stdout, stderr io.Writer) int {
-	boot := &Booter{
+	super := &Supervisor{
 		Stderr: stderr,
 		logger: ctxlog.New(stderr, "json", "info"),
 	}
 
-	ctx := ctxlog.Context(context.Background(), boot.logger)
+	ctx := ctxlog.Context(context.Background(), super.logger)
 	ctx, cancel := context.WithCancel(ctx)
 	defer cancel()
 
 	var err error
 	defer func() {
 		if err != nil {
-			boot.logger.WithError(err).Info("exiting")
+			super.logger.WithError(err).Info("exiting")
 		}
 	}()
 
 	flags := flag.NewFlagSet(prog, flag.ContinueOnError)
 	flags.SetOutput(stderr)
-	loader := config.NewLoader(stdin, boot.logger)
+	loader := config.NewLoader(stdin, super.logger)
 	loader.SetupFlags(flags)
 	versionFlag := flags.Bool("version", false, "Write version information to stdout and exit 0")
-	flags.StringVar(&boot.SourcePath, "source", ".", "arvados source tree `directory`")
-	flags.StringVar(&boot.LibPath, "lib", "/var/lib/arvados", "`directory` to install dependencies and library files")
-	flags.StringVar(&boot.ClusterType, "type", "production", "cluster `type`: development, test, or production")
-	flags.StringVar(&boot.ListenHost, "listen-host", "localhost", "host name or interface address for service listeners")
-	flags.StringVar(&boot.ControllerAddr, "controller-address", ":0", "desired controller address, `host:port` or `:port`")
-	flags.BoolVar(&boot.OwnTemporaryDatabase, "own-temporary-database", false, "bring up a postgres server and create a temporary database")
+	flags.StringVar(&super.SourcePath, "source", ".", "arvados source tree `directory`")
+	flags.StringVar(&super.LibPath, "lib", "/var/lib/arvados", "`directory` to install dependencies and library files")
+	flags.StringVar(&super.ClusterType, "type", "production", "cluster `type`: development, test, or production")
+	flags.StringVar(&super.ListenHost, "listen-host", "localhost", "host name or interface address for service listeners")
+	flags.StringVar(&super.ControllerAddr, "controller-address", ":0", "desired controller address, `host:port` or `:port`")
+	flags.BoolVar(&super.OwnTemporaryDatabase, "own-temporary-database", false, "bring up a postgres server and create a temporary database")
 	err = flags.Parse(args)
 	if err == flag.ErrHelp {
 		err = nil
@@ -84,7 +65,7 @@ func (bootCommand) RunCommand(prog string, args []string, stdin io.Reader, stdou
 		return 2
 	} else if *versionFlag {
 		return cmd.Version.RunCommand(prog, args, stdin, stdout, stderr)
-	} else if boot.ClusterType != "development" && boot.ClusterType != "test" && boot.ClusterType != "production" {
+	} else if super.ClusterType != "development" && super.ClusterType != "test" && super.ClusterType != "production" {
 		err = fmt.Errorf("cluster type must be 'development', 'test', or 'production'")
 		return 2
 	}
@@ -95,674 +76,14 @@ func (bootCommand) RunCommand(prog string, args []string, stdin io.Reader, stdou
 		return 1
 	}
 
-	boot.Start(ctx, cfg)
-	defer boot.Stop()
-	if url, ok := boot.WaitReady(); ok {
+	super.Start(ctx, cfg)
+	defer super.Stop()
+	if url, ok := super.WaitReady(); ok {
 		fmt.Fprintln(stdout, url)
 		// Wait for signal/crash + orderly shutdown
-		<-boot.done
+		<-super.done
 		return 0
 	} else {
 		return 1
 	}
 }
-
-type Booter struct {
-	SourcePath           string // e.g., /home/username/src/arvados
-	SourceVersion        string // e.g., acbd1324...
-	LibPath              string // e.g., /var/lib/arvados
-	ClusterType          string // e.g., production
-	ListenHost           string // e.g., localhost
-	ControllerAddr       string // e.g., 127.0.0.1:8000
-	OwnTemporaryDatabase bool
-	Stderr               io.Writer
-
-	logger  logrus.FieldLogger
-	cluster *arvados.Cluster
-
-	ctx           context.Context
-	cancel        context.CancelFunc
-	done          chan struct{}
-	healthChecker *health.Aggregator
-	tasksReady    map[string]chan bool
-	waitShutdown  sync.WaitGroup
-
-	tempdir    string
-	configfile string
-	environ    []string // for child processes
-
-	setupRubyOnce sync.Once
-	setupRubyErr  error
-	goMutex       sync.Mutex
-}
-
-func (boot *Booter) Start(ctx context.Context, cfg *arvados.Config) {
-	boot.ctx, boot.cancel = context.WithCancel(ctx)
-	boot.done = make(chan struct{})
-
-	go func() {
-		sigch := make(chan os.Signal)
-		signal.Notify(sigch, syscall.SIGINT, syscall.SIGTERM)
-		defer signal.Stop(sigch)
-		go func() {
-			for sig := range sigch {
-				boot.logger.WithField("signal", sig).Info("caught signal")
-				boot.cancel()
-			}
-		}()
-
-		err := boot.run(cfg)
-		if err != nil {
-			fmt.Fprintln(boot.Stderr, err)
-		}
-		close(boot.done)
-	}()
-}
-
-func (boot *Booter) run(cfg *arvados.Config) error {
-	cwd, err := os.Getwd()
-	if err != nil {
-		return err
-	}
-	if !strings.HasPrefix(boot.SourcePath, "/") {
-		boot.SourcePath = filepath.Join(cwd, boot.SourcePath)
-	}
-	boot.SourcePath, err = filepath.EvalSymlinks(boot.SourcePath)
-	if err != nil {
-		return err
-	}
-
-	boot.tempdir, err = ioutil.TempDir("", "arvados-server-boot-")
-	if err != nil {
-		return err
-	}
-	defer os.RemoveAll(boot.tempdir)
-	if err := os.Mkdir(filepath.Join(boot.tempdir, "bin"), 0777); err != nil {
-		return err
-	}
-
-	// Fill in any missing config keys, and write the resulting
-	// config in the temp dir for child services to use.
-	err = boot.autofillConfig(cfg, boot.logger)
-	if err != nil {
-		return err
-	}
-	conffile, err := os.OpenFile(filepath.Join(boot.tempdir, "config.yml"), os.O_CREATE|os.O_WRONLY, 0777)
-	if err != nil {
-		return err
-	}
-	defer conffile.Close()
-	err = json.NewEncoder(conffile).Encode(cfg)
-	if err != nil {
-		return err
-	}
-	err = conffile.Close()
-	if err != nil {
-		return err
-	}
-	boot.configfile = conffile.Name()
-
-	boot.environ = os.Environ()
-	boot.cleanEnv()
-	boot.setEnv("ARVADOS_CONFIG", boot.configfile)
-	boot.setEnv("RAILS_ENV", boot.ClusterType)
-	boot.setEnv("TMPDIR", boot.tempdir)
-	boot.prependEnv("PATH", filepath.Join(boot.tempdir, "bin")+":")
-	boot.prependEnv("PATH", filepath.Join(boot.LibPath, "bin")+":")
-
-	boot.cluster, err = cfg.GetCluster("")
-	if err != nil {
-		return err
-	}
-	// Now that we have the config, replace the bootstrap logger
-	// with a new one according to the logging config.
-	loglevel := boot.cluster.SystemLogs.LogLevel
-	if s := os.Getenv("ARVADOS_DEBUG"); s != "" && s != "0" {
-		loglevel = "debug"
-	}
-	boot.logger = ctxlog.New(boot.Stderr, boot.cluster.SystemLogs.Format, loglevel).WithFields(logrus.Fields{
-		"PID": os.Getpid(),
-	})
-
-	if boot.SourceVersion == "" {
-		// Find current source tree version.
-		var buf bytes.Buffer
-		err = boot.RunProgram(boot.ctx, ".", &buf, nil, "git", "diff", "--shortstat")
-		if err != nil {
-			return err
-		}
-		dirty := buf.Len() > 0
-		buf.Reset()
-		err = boot.RunProgram(boot.ctx, ".", &buf, nil, "git", "log", "-n1", "--format=%H")
-		if err != nil {
-			return err
-		}
-		boot.SourceVersion = strings.TrimSpace(buf.String())
-		if dirty {
-			boot.SourceVersion += "+uncommitted"
-		}
-	} else {
-		return errors.New("specifying a version to run is not yet supported")
-	}
-	for _, dir := range []string{boot.LibPath, filepath.Join(boot.LibPath, "bin")} {
-		if _, err = os.Stat(filepath.Join(dir, ".")); os.IsNotExist(err) {
-			err = os.Mkdir(dir, 0755)
-			if err != nil {
-				return err
-			}
-		} else if err != nil {
-			return err
-		}
-	}
-	_, err = boot.installGoProgram(boot.ctx, "cmd/arvados-server")
-	if err != nil {
-		return err
-	}
-	err = boot.setupRubyEnv()
-	if err != nil {
-		return err
-	}
-
-	tasks := []bootTask{
-		createCertificates{},
-		runPostgreSQL{},
-		runNginx{},
-		runServiceCommand{name: "controller", svc: boot.cluster.Services.Controller, depends: []bootTask{runPostgreSQL{}}},
-		runGoProgram{src: "services/arv-git-httpd"},
-		runGoProgram{src: "services/health"},
-		runGoProgram{src: "services/keepproxy", depends: []bootTask{runPassenger{src: "services/api"}}},
-		runGoProgram{src: "services/keepstore", svc: boot.cluster.Services.Keepstore},
-		runGoProgram{src: "services/keep-web"},
-		runGoProgram{src: "services/ws", depends: []bootTask{runPostgreSQL{}}},
-		installPassenger{src: "services/api"},
-		runPassenger{src: "services/api", svc: boot.cluster.Services.RailsAPI, depends: []bootTask{createCertificates{}, runPostgreSQL{}, installPassenger{src: "services/api"}}},
-		installPassenger{src: "apps/workbench", depends: []bootTask{installPassenger{src: "services/api"}}}, // dependency ensures workbench doesn't delay api startup
-		runPassenger{src: "apps/workbench", svc: boot.cluster.Services.Workbench1, depends: []bootTask{installPassenger{src: "apps/workbench"}}},
-		seedDatabase{},
-	}
-	if boot.ClusterType != "test" {
-		tasks = append(tasks,
-			runServiceCommand{name: "dispatch-cloud", svc: boot.cluster.Services.Controller},
-			runGoProgram{src: "services/keep-balance"},
-		)
-	}
-	boot.tasksReady = map[string]chan bool{}
-	for _, task := range tasks {
-		boot.tasksReady[task.String()] = make(chan bool)
-	}
-	for _, task := range tasks {
-		task := task
-		fail := func(err error) {
-			if boot.ctx.Err() != nil {
-				return
-			}
-			boot.cancel()
-			boot.logger.WithField("task", task.String()).WithError(err).Error("task failed")
-		}
-		go func() {
-			boot.logger.WithField("task", task.String()).Info("starting")
-			err := task.Run(boot.ctx, fail, boot)
-			if err != nil {
-				fail(err)
-				return
-			}
-			close(boot.tasksReady[task.String()])
-		}()
-	}
-	err = boot.wait(boot.ctx, tasks...)
-	if err != nil {
-		return err
-	}
-	boot.logger.Info("all startup tasks are complete; starting health checks")
-	boot.healthChecker = &health.Aggregator{Cluster: boot.cluster}
-	<-boot.ctx.Done()
-	boot.logger.Info("shutting down")
-	boot.waitShutdown.Wait()
-	return boot.ctx.Err()
-}
-
-func (boot *Booter) wait(ctx context.Context, tasks ...bootTask) error {
-	for _, task := range tasks {
-		ch, ok := boot.tasksReady[task.String()]
-		if !ok {
-			return fmt.Errorf("no such task: %s", task)
-		}
-		boot.logger.WithField("task", task.String()).Info("waiting")
-		select {
-		case <-ch:
-			boot.logger.WithField("task", task.String()).Info("ready")
-		case <-ctx.Done():
-			boot.logger.WithField("task", task.String()).Info("task was never ready")
-			return ctx.Err()
-		}
-	}
-	return nil
-}
-
-func (boot *Booter) Stop() {
-	boot.cancel()
-	<-boot.done
-}
-
-func (boot *Booter) WaitReady() (*arvados.URL, bool) {
-	ticker := time.NewTicker(time.Second)
-	defer ticker.Stop()
-	for waiting := true; waiting; {
-		select {
-		case <-ticker.C:
-		case <-boot.ctx.Done():
-			return nil, false
-		}
-		if boot.healthChecker == nil {
-			// not set up yet
-			continue
-		}
-		resp := boot.healthChecker.ClusterHealth()
-		// The overall health check (resp.Health=="OK") might
-		// never pass due to missing components (like
-		// arvados-dispatch-cloud in a test cluster), so
-		// instead we wait for all configured components to
-		// pass.
-		waiting = false
-		for target, check := range resp.Checks {
-			if check.Health != "OK" {
-				waiting = true
-				boot.logger.WithField("target", target).Debug("waiting")
-			}
-		}
-	}
-	u := boot.cluster.Services.Controller.ExternalURL
-	return &u, true
-}
-
-func (boot *Booter) prependEnv(key, prepend string) {
-	for i, s := range boot.environ {
-		if strings.HasPrefix(s, key+"=") {
-			boot.environ[i] = key + "=" + prepend + s[len(key)+1:]
-			return
-		}
-	}
-	boot.environ = append(boot.environ, key+"="+prepend)
-}
-
-var cleanEnvPrefixes = []string{
-	"GEM_HOME=",
-	"GEM_PATH=",
-	"ARVADOS_",
-}
-
-func (boot *Booter) cleanEnv() {
-	var cleaned []string
-	for _, s := range boot.environ {
-		drop := false
-		for _, p := range cleanEnvPrefixes {
-			if strings.HasPrefix(s, p) {
-				drop = true
-				break
-			}
-		}
-		if !drop {
-			cleaned = append(cleaned, s)
-		}
-	}
-	boot.environ = cleaned
-}
-
-func (boot *Booter) setEnv(key, val string) {
-	for i, s := range boot.environ {
-		if strings.HasPrefix(s, key+"=") {
-			boot.environ[i] = key + "=" + val
-			return
-		}
-	}
-	boot.environ = append(boot.environ, key+"="+val)
-}
-
-// Remove all but the first occurrence of each env var.
-func dedupEnv(in []string) []string {
-	saw := map[string]bool{}
-	var out []string
-	for _, kv := range in {
-		if split := strings.Index(kv, "="); split < 1 {
-			panic("invalid environment var: " + kv)
-		} else if saw[kv[:split]] {
-			continue
-		} else {
-			saw[kv[:split]] = true
-			out = append(out, kv)
-		}
-	}
-	return out
-}
-
-func (boot *Booter) installGoProgram(ctx context.Context, srcpath string) (string, error) {
-	_, basename := filepath.Split(srcpath)
-	bindir := filepath.Join(boot.tempdir, "bin")
-	binfile := filepath.Join(bindir, basename)
-	err := boot.RunProgram(ctx, filepath.Join(boot.SourcePath, srcpath), nil, []string{"GOBIN=" + bindir}, "go", "install", "-ldflags", "-X git.arvados.org/arvados.git/lib/cmd.version="+boot.SourceVersion+" -X main.version="+boot.SourceVersion)
-	return binfile, err
-}
-
-func (boot *Booter) setupRubyEnv() error {
-	cmd := exec.Command("gem", "env", "gempath")
-	cmd.Env = boot.environ
-	buf, err := cmd.Output() // /var/lib/arvados/.gem/ruby/2.5.0/bin:...
-	if err != nil || len(buf) == 0 {
-		return fmt.Errorf("gem env gempath: %v", err)
-	}
-	gempath := string(bytes.Split(buf, []byte{':'})[0])
-	boot.prependEnv("PATH", gempath+"/bin:")
-	boot.setEnv("GEM_HOME", gempath)
-	boot.setEnv("GEM_PATH", gempath)
-	// Passenger install doesn't work unless $HOME is ~user
-	u, err := user.Current()
-	if err != nil {
-		return err
-	}
-	boot.setEnv("HOME", u.HomeDir)
-	return nil
-}
-
-func (boot *Booter) lookPath(prog string) string {
-	for _, val := range boot.environ {
-		if strings.HasPrefix(val, "PATH=") {
-			for _, dir := range filepath.SplitList(val[5:]) {
-				path := filepath.Join(dir, prog)
-				if fi, err := os.Stat(path); err == nil && fi.Mode()&0111 != 0 {
-					return path
-				}
-			}
-		}
-	}
-	return prog
-}
-
-// Run prog with args, using dir as working directory. If ctx is
-// cancelled while the child is running, RunProgram terminates the
-// child, waits for it to exit, then returns.
-//
-// Child's environment will have our env vars, plus any given in env.
-//
-// Child's stdout will be written to output if non-nil, otherwise the
-// boot command's stderr.
-func (boot *Booter) RunProgram(ctx context.Context, dir string, output io.Writer, env []string, prog string, args ...string) error {
-	cmdline := fmt.Sprintf("%s", append([]string{prog}, args...))
-	boot.logger.WithField("command", cmdline).WithField("dir", dir).Info("executing")
-
-	logprefix := strings.TrimPrefix(prog, boot.tempdir+"/bin/")
-	if prog == "bundle" && len(args) > 2 && args[0] == "exec" {
-		logprefix = args[1]
-	} else if prog == "arvados-server" && len(args) > 1 {
-		logprefix = args[0]
-	}
-	if !strings.HasPrefix(dir, "/") {
-		logprefix = dir + ": " + logprefix
-	}
-
-	cmd := exec.Command(boot.lookPath(prog), args...)
-	stdout, err := cmd.StdoutPipe()
-	if err != nil {
-		return err
-	}
-	stderr, err := cmd.StderrPipe()
-	if err != nil {
-		return err
-	}
-	logwriter := &service.LogPrefixer{Writer: boot.Stderr, Prefix: []byte("[" + logprefix + "] ")}
-	var copiers sync.WaitGroup
-	copiers.Add(1)
-	go func() {
-		io.Copy(logwriter, stderr)
-		copiers.Done()
-	}()
-	copiers.Add(1)
-	go func() {
-		if output == nil {
-			io.Copy(logwriter, stdout)
-		} else {
-			io.Copy(output, stdout)
-		}
-		copiers.Done()
-	}()
-
-	if strings.HasPrefix(dir, "/") {
-		cmd.Dir = dir
-	} else {
-		cmd.Dir = filepath.Join(boot.SourcePath, dir)
-	}
-	env = append([]string(nil), env...)
-	env = append(env, boot.environ...)
-	cmd.Env = dedupEnv(env)
-
-	exited := false
-	defer func() { exited = true }()
-	go func() {
-		<-ctx.Done()
-		log := ctxlog.FromContext(ctx).WithFields(logrus.Fields{"dir": dir, "cmdline": cmdline})
-		for !exited {
-			if cmd.Process == nil {
-				log.Debug("waiting for child process to start")
-				time.Sleep(time.Second / 2)
-			} else {
-				log.WithField("PID", cmd.Process.Pid).Debug("sending SIGTERM")
-				cmd.Process.Signal(syscall.SIGTERM)
-				time.Sleep(5 * time.Second)
-				if !exited {
-					stdout.Close()
-					stderr.Close()
-					log.WithField("PID", cmd.Process.Pid).Warn("still waiting for child process to exit 5s after SIGTERM")
-				}
-			}
-		}
-	}()
-
-	err = cmd.Start()
-	if err != nil {
-		return err
-	}
-	copiers.Wait()
-	err = cmd.Wait()
-	if ctx.Err() != nil {
-		// Return "context canceled", instead of the "killed"
-		// error that was probably caused by the context being
-		// canceled.
-		return ctx.Err()
-	} else if err != nil {
-		return fmt.Errorf("%s: error: %v", cmdline, err)
-	}
-	return nil
-}
-
-func (boot *Booter) autofillConfig(cfg *arvados.Config, log logrus.FieldLogger) error {
-	cluster, err := cfg.GetCluster("")
-	if err != nil {
-		return err
-	}
-	usedPort := map[string]bool{}
-	nextPort := func() string {
-		for {
-			port, err := availablePort(boot.ListenHost + ":0")
-			if err != nil {
-				panic(err)
-			}
-			if usedPort[port] {
-				continue
-			}
-			usedPort[port] = true
-			return port
-		}
-	}
-	if cluster.Services.Controller.ExternalURL.Host == "" {
-		h, p, err := net.SplitHostPort(boot.ControllerAddr)
-		if err != nil {
-			return err
-		}
-		if h == "" {
-			h = boot.ListenHost
-		}
-		if p == "0" {
-			p, err = availablePort(":0")
-			if err != nil {
-				return err
-			}
-			usedPort[p] = true
-		}
-		cluster.Services.Controller.ExternalURL = arvados.URL{Scheme: "https", Host: net.JoinHostPort(h, p)}
-	}
-	for _, svc := range []*arvados.Service{
-		&cluster.Services.Controller,
-		&cluster.Services.DispatchCloud,
-		&cluster.Services.GitHTTP,
-		&cluster.Services.Health,
-		&cluster.Services.Keepproxy,
-		&cluster.Services.Keepstore,
-		&cluster.Services.RailsAPI,
-		&cluster.Services.WebDAV,
-		&cluster.Services.WebDAVDownload,
-		&cluster.Services.Websocket,
-		&cluster.Services.Workbench1,
-	} {
-		if svc == &cluster.Services.DispatchCloud && boot.ClusterType == "test" {
-			continue
-		}
-		if svc.ExternalURL.Host == "" && (svc == &cluster.Services.Controller ||
-			svc == &cluster.Services.GitHTTP ||
-			svc == &cluster.Services.Keepproxy ||
-			svc == &cluster.Services.WebDAV ||
-			svc == &cluster.Services.WebDAVDownload ||
-			svc == &cluster.Services.Websocket ||
-			svc == &cluster.Services.Workbench1) {
-			svc.ExternalURL = arvados.URL{Scheme: "https", Host: fmt.Sprintf("%s:%s", boot.ListenHost, nextPort())}
-		}
-		if len(svc.InternalURLs) == 0 {
-			svc.InternalURLs = map[arvados.URL]arvados.ServiceInstance{
-				arvados.URL{Scheme: "http", Host: fmt.Sprintf("%s:%s", boot.ListenHost, nextPort())}: arvados.ServiceInstance{},
-			}
-		}
-	}
-	if cluster.SystemRootToken == "" {
-		cluster.SystemRootToken = randomHexString(64)
-	}
-	if cluster.ManagementToken == "" {
-		cluster.ManagementToken = randomHexString(64)
-	}
-	if cluster.API.RailsSessionSecretToken == "" {
-		cluster.API.RailsSessionSecretToken = randomHexString(64)
-	}
-	if cluster.Collections.BlobSigningKey == "" {
-		cluster.Collections.BlobSigningKey = randomHexString(64)
-	}
-	if boot.ClusterType != "production" && cluster.Containers.DispatchPrivateKey == "" {
-		buf, err := ioutil.ReadFile(filepath.Join(boot.SourcePath, "lib", "dispatchcloud", "test", "sshkey_dispatch"))
-		if err != nil {
-			return err
-		}
-		cluster.Containers.DispatchPrivateKey = string(buf)
-	}
-	if boot.ClusterType != "production" {
-		cluster.TLS.Insecure = true
-	}
-	if boot.ClusterType == "test" {
-		// Add a second keepstore process.
-		cluster.Services.Keepstore.InternalURLs[arvados.URL{Scheme: "http", Host: fmt.Sprintf("%s:%s", boot.ListenHost, nextPort())}] = arvados.ServiceInstance{}
-
-		// Create a directory-backed volume for each keepstore
-		// process.
-		cluster.Volumes = map[string]arvados.Volume{}
-		for url := range cluster.Services.Keepstore.InternalURLs {
-			volnum := len(cluster.Volumes)
-			datadir := fmt.Sprintf("%s/keep%d.data", boot.tempdir, volnum)
-			if _, err = os.Stat(datadir + "/."); err == nil {
-			} else if !os.IsNotExist(err) {
-				return err
-			} else if err = os.Mkdir(datadir, 0777); err != nil {
-				return err
-			}
-			cluster.Volumes[fmt.Sprintf(cluster.ClusterID+"-nyw5e-%015d", volnum)] = arvados.Volume{
-				Driver:           "Directory",
-				DriverParameters: json.RawMessage(fmt.Sprintf(`{"Root":%q}`, datadir)),
-				AccessViaHosts: map[arvados.URL]arvados.VolumeAccess{
-					url: {},
-				},
-			}
-		}
-	}
-	if boot.OwnTemporaryDatabase {
-		cluster.PostgreSQL.Connection = arvados.PostgreSQLConnection{
-			"client_encoding": "utf8",
-			"host":            "localhost",
-			"port":            nextPort(),
-			"dbname":          "arvados_test",
-			"user":            "arvados",
-			"password":        "insecure_arvados_test",
-		}
-	}
-
-	cfg.Clusters[cluster.ClusterID] = *cluster
-	return nil
-}
-
-func randomHexString(chars int) string {
-	b := make([]byte, chars/2)
-	_, err := rand.Read(b)
-	if err != nil {
-		panic(err)
-	}
-	return fmt.Sprintf("%x", b)
-}
-
-func internalPort(svc arvados.Service) (string, error) {
-	for u := range svc.InternalURLs {
-		if _, p, err := net.SplitHostPort(u.Host); err != nil {
-			return "", err
-		} else if p != "" {
-			return p, nil
-		} else if u.Scheme == "https" {
-			return "443", nil
-		} else {
-			return "80", nil
-		}
-	}
-	return "", fmt.Errorf("service has no InternalURLs")
-}
-
-func externalPort(svc arvados.Service) (string, error) {
-	if _, p, err := net.SplitHostPort(svc.ExternalURL.Host); err != nil {
-		return "", err
-	} else if p != "" {
-		return p, nil
-	} else if svc.ExternalURL.Scheme == "https" {
-		return "443", nil
-	} else {
-		return "80", nil
-	}
-}
-
-func availablePort(addr string) (string, error) {
-	ln, err := net.Listen("tcp", addr)
-	if err != nil {
-		return "", err
-	}
-	defer ln.Close()
-	_, port, err := net.SplitHostPort(ln.Addr().String())
-	if err != nil {
-		return "", err
-	}
-	return port, nil
-}
-
-// Try to connect to addr until it works, then close ch. Give up if
-// ctx cancels.
-func waitForConnect(ctx context.Context, addr string) error {
-	dialer := net.Dialer{Timeout: time.Second}
-	for ctx.Err() == nil {
-		conn, err := dialer.DialContext(ctx, "tcp", addr)
-		if err != nil {
-			time.Sleep(time.Second / 10)
-			continue
-		}
-		conn.Close()
-		return nil
-	}
-	return ctx.Err()
-}
diff --git a/lib/boot/nginx.go b/lib/boot/nginx.go
index a06d3a700..2d5c74594 100644
--- a/lib/boot/nginx.go
+++ b/lib/boot/nginx.go
@@ -16,33 +16,35 @@ import (
 	"git.arvados.org/arvados.git/sdk/go/arvados"
 )
 
+// Run an Nginx process that proxies the supervisor's configured
+// ExternalURLs to the appropriate InternalURLs.
 type runNginx struct{}
 
 func (runNginx) String() string {
 	return "nginx"
 }
 
-func (runNginx) Run(ctx context.Context, fail func(error), boot *Booter) error {
+func (runNginx) Run(ctx context.Context, fail func(error), super *Supervisor) error {
 	vars := map[string]string{
-		"LISTENHOST": boot.ListenHost,
-		"SSLCERT":    filepath.Join(boot.SourcePath, "services", "api", "tmp", "self-signed.pem"), // TODO: root ca
-		"SSLKEY":     filepath.Join(boot.SourcePath, "services", "api", "tmp", "self-signed.key"), // TODO: root ca
-		"ACCESSLOG":  filepath.Join(boot.tempdir, "nginx_access.log"),
-		"ERRORLOG":   filepath.Join(boot.tempdir, "nginx_error.log"),
-		"TMPDIR":     boot.tempdir,
+		"LISTENHOST": super.ListenHost,
+		"SSLCERT":    filepath.Join(super.SourcePath, "services", "api", "tmp", "self-signed.pem"), // TODO: root ca
+		"SSLKEY":     filepath.Join(super.SourcePath, "services", "api", "tmp", "self-signed.key"), // TODO: root ca
+		"ACCESSLOG":  filepath.Join(super.tempdir, "nginx_access.log"),
+		"ERRORLOG":   filepath.Join(super.tempdir, "nginx_error.log"),
+		"TMPDIR":     super.tempdir,
 	}
 	var err error
 	for _, cmpt := range []struct {
 		varname string
 		svc     arvados.Service
 	}{
-		{"CONTROLLER", boot.cluster.Services.Controller},
-		{"KEEPWEB", boot.cluster.Services.WebDAV},
-		{"KEEPWEBDL", boot.cluster.Services.WebDAVDownload},
-		{"KEEPPROXY", boot.cluster.Services.Keepproxy},
-		{"GIT", boot.cluster.Services.GitHTTP},
-		{"WORKBENCH1", boot.cluster.Services.Workbench1},
-		{"WS", boot.cluster.Services.Websocket},
+		{"CONTROLLER", super.cluster.Services.Controller},
+		{"KEEPWEB", super.cluster.Services.WebDAV},
+		{"KEEPWEBDL", super.cluster.Services.WebDAVDownload},
+		{"KEEPPROXY", super.cluster.Services.Keepproxy},
+		{"GIT", super.cluster.Services.GitHTTP},
+		{"WORKBENCH1", super.cluster.Services.Workbench1},
+		{"WS", super.cluster.Services.Websocket},
 	} {
 		vars[cmpt.varname+"PORT"], err = internalPort(cmpt.svc)
 		if err != nil {
@@ -53,7 +55,7 @@ func (runNginx) Run(ctx context.Context, fail func(error), boot *Booter) error {
 			return fmt.Errorf("%s external port: %s (%v)", cmpt.varname, err, cmpt.svc)
 		}
 	}
-	tmpl, err := ioutil.ReadFile(filepath.Join(boot.SourcePath, "sdk", "python", "tests", "nginx.conf"))
+	tmpl, err := ioutil.ReadFile(filepath.Join(super.SourcePath, "sdk", "python", "tests", "nginx.conf"))
 	if err != nil {
 		return err
 	}
@@ -63,7 +65,7 @@ func (runNginx) Run(ctx context.Context, fail func(error), boot *Booter) error {
 		}
 		return vars[src[2:len(src)-2]]
 	})
-	conffile := filepath.Join(boot.tempdir, "nginx.conf")
+	conffile := filepath.Join(super.tempdir, "nginx.conf")
 	err = ioutil.WriteFile(conffile, []byte(conf), 0755)
 	if err != nil {
 		return err
@@ -77,13 +79,13 @@ func (runNginx) Run(ctx context.Context, fail func(error), boot *Booter) error {
 			}
 		}
 	}
-	boot.waitShutdown.Add(1)
+	super.waitShutdown.Add(1)
 	go func() {
-		defer boot.waitShutdown.Done()
-		fail(boot.RunProgram(ctx, ".", nil, nil, nginx,
+		defer super.waitShutdown.Done()
+		fail(super.RunProgram(ctx, ".", nil, nil, nginx,
 			"-g", "error_log stderr info;",
-			"-g", "pid "+filepath.Join(boot.tempdir, "nginx.pid")+";",
+			"-g", "pid "+filepath.Join(super.tempdir, "nginx.pid")+";",
 			"-c", conffile))
 	}()
-	return waitForConnect(ctx, boot.cluster.Services.Controller.ExternalURL.Host)
+	return waitForConnect(ctx, super.cluster.Services.Controller.ExternalURL.Host)
 }
diff --git a/lib/boot/passenger.go b/lib/boot/passenger.go
index 10581a697..36be2f1a0 100644
--- a/lib/boot/passenger.go
+++ b/lib/boot/passenger.go
@@ -20,17 +20,19 @@ import (
 // concurrent installs.
 var passengerInstallMutex sync.Mutex
 
+// Install a Rails application's dependencies, including phusion
+// passenger.
 type installPassenger struct {
 	src     string
-	depends []bootTask
+	depends []supervisedTask
 }
 
 func (runner installPassenger) String() string {
 	return "installPassenger:" + runner.src
 }
 
-func (runner installPassenger) Run(ctx context.Context, fail func(error), boot *Booter) error {
-	err := boot.wait(ctx, runner.depends...)
+func (runner installPassenger) Run(ctx context.Context, fail func(error), super *Supervisor) error {
+	err := super.wait(ctx, runner.depends...)
 	if err != nil {
 		return err
 	}
@@ -39,32 +41,32 @@ func (runner installPassenger) Run(ctx context.Context, fail func(error), boot *
 	defer passengerInstallMutex.Unlock()
 
 	var buf bytes.Buffer
-	err = boot.RunProgram(ctx, runner.src, &buf, nil, "gem", "list", "--details", "bundler")
+	err = super.RunProgram(ctx, runner.src, &buf, nil, "gem", "list", "--details", "bundler")
 	if err != nil {
 		return err
 	}
 	for _, version := range []string{"1.11.0", "1.17.3", "2.0.2"} {
 		if !strings.Contains(buf.String(), "("+version+")") {
-			err = boot.RunProgram(ctx, runner.src, nil, nil, "gem", "install", "--user", "bundler:1.11", "bundler:1.17.3", "bundler:2.0.2")
+			err = super.RunProgram(ctx, runner.src, nil, nil, "gem", "install", "--user", "bundler:1.11", "bundler:1.17.3", "bundler:2.0.2")
 			if err != nil {
 				return err
 			}
 			break
 		}
 	}
-	err = boot.RunProgram(ctx, runner.src, nil, nil, "bundle", "install", "--jobs", "4", "--path", filepath.Join(os.Getenv("HOME"), ".gem"))
+	err = super.RunProgram(ctx, runner.src, nil, nil, "bundle", "install", "--jobs", "4", "--path", filepath.Join(os.Getenv("HOME"), ".gem"))
 	if err != nil {
 		return err
 	}
-	err = boot.RunProgram(ctx, runner.src, nil, nil, "bundle", "exec", "passenger-config", "build-native-support")
+	err = super.RunProgram(ctx, runner.src, nil, nil, "bundle", "exec", "passenger-config", "build-native-support")
 	if err != nil {
 		return err
 	}
-	err = boot.RunProgram(ctx, runner.src, nil, nil, "bundle", "exec", "passenger-config", "install-standalone-runtime")
+	err = super.RunProgram(ctx, runner.src, nil, nil, "bundle", "exec", "passenger-config", "install-standalone-runtime")
 	if err != nil {
 		return err
 	}
-	err = boot.RunProgram(ctx, runner.src, nil, nil, "bundle", "exec", "passenger-config", "validate-install")
+	err = super.RunProgram(ctx, runner.src, nil, nil, "bundle", "exec", "passenger-config", "validate-install")
 	if err != nil {
 		return err
 	}
@@ -74,15 +76,15 @@ func (runner installPassenger) Run(ctx context.Context, fail func(error), boot *
 type runPassenger struct {
 	src     string
 	svc     arvados.Service
-	depends []bootTask
+	depends []supervisedTask
 }
 
 func (runner runPassenger) String() string {
 	return "runPassenger:" + runner.src
 }
 
-func (runner runPassenger) Run(ctx context.Context, fail func(error), boot *Booter) error {
-	err := boot.wait(ctx, runner.depends...)
+func (runner runPassenger) Run(ctx context.Context, fail func(error), super *Supervisor) error {
+	err := super.wait(ctx, runner.depends...)
 	if err != nil {
 		return err
 	}
@@ -99,19 +101,19 @@ func (runner runPassenger) Run(ctx context.Context, fail func(error), boot *Boot
 		"error":   "1",
 		"fatal":   "0",
 		"panic":   "0",
-	}[boot.cluster.SystemLogs.LogLevel]; ok {
+	}[super.cluster.SystemLogs.LogLevel]; ok {
 		loglevel = lvl
 	}
-	boot.waitShutdown.Add(1)
+	super.waitShutdown.Add(1)
 	go func() {
-		defer boot.waitShutdown.Done()
-		err = boot.RunProgram(ctx, runner.src, nil, nil, "bundle", "exec",
+		defer super.waitShutdown.Done()
+		err = super.RunProgram(ctx, runner.src, nil, nil, "bundle", "exec",
 			"passenger", "start",
 			"-p", port,
 			"--log-file", "/dev/stderr",
 			"--log-level", loglevel,
 			"--no-friendly-error-pages",
-			"--pid-file", filepath.Join(boot.tempdir, "passenger."+strings.Replace(runner.src, "/", "_", -1)+".pid"))
+			"--pid-file", filepath.Join(super.tempdir, "passenger."+strings.Replace(runner.src, "/", "_", -1)+".pid"))
 		fail(err)
 	}()
 	return nil
diff --git a/lib/boot/postgresql.go b/lib/boot/postgresql.go
index 9f0de2dd6..df9890415 100644
--- a/lib/boot/postgresql.go
+++ b/lib/boot/postgresql.go
@@ -19,50 +19,53 @@ import (
 	"github.com/lib/pq"
 )
 
+// Run a postgresql server in a private data directory. Set up a db
+// user, database, and TCP listener that match the supervisor's
+// configured database connection info.
 type runPostgreSQL struct{}
 
 func (runPostgreSQL) String() string {
 	return "postgresql"
 }
 
-func (runPostgreSQL) Run(ctx context.Context, fail func(error), boot *Booter) error {
-	err := boot.wait(ctx, createCertificates{})
+func (runPostgreSQL) Run(ctx context.Context, fail func(error), super *Supervisor) error {
+	err := super.wait(ctx, createCertificates{})
 	if err != nil {
 		return err
 	}
 
 	buf := bytes.NewBuffer(nil)
-	err = boot.RunProgram(ctx, boot.tempdir, buf, nil, "pg_config", "--bindir")
+	err = super.RunProgram(ctx, super.tempdir, buf, nil, "pg_config", "--bindir")
 	if err != nil {
 		return err
 	}
 	bindir := strings.TrimSpace(buf.String())
 
-	datadir := filepath.Join(boot.tempdir, "pgdata")
+	datadir := filepath.Join(super.tempdir, "pgdata")
 	err = os.Mkdir(datadir, 0755)
 	if err != nil {
 		return err
 	}
-	err = boot.RunProgram(ctx, boot.tempdir, nil, nil, filepath.Join(bindir, "initdb"), "-D", datadir)
+	err = super.RunProgram(ctx, super.tempdir, nil, nil, filepath.Join(bindir, "initdb"), "-D", datadir)
 	if err != nil {
 		return err
 	}
 
-	err = boot.RunProgram(ctx, boot.tempdir, nil, nil, "cp", "server.crt", "server.key", datadir)
+	err = super.RunProgram(ctx, super.tempdir, nil, nil, "cp", "server.crt", "server.key", datadir)
 	if err != nil {
 		return err
 	}
 
-	port := boot.cluster.PostgreSQL.Connection["port"]
+	port := super.cluster.PostgreSQL.Connection["port"]
 
-	boot.waitShutdown.Add(1)
+	super.waitShutdown.Add(1)
 	go func() {
-		defer boot.waitShutdown.Done()
-		fail(boot.RunProgram(ctx, boot.tempdir, nil, nil, filepath.Join(bindir, "postgres"),
+		defer super.waitShutdown.Done()
+		fail(super.RunProgram(ctx, super.tempdir, nil, nil, filepath.Join(bindir, "postgres"),
 			"-l",          // enable ssl
 			"-D", datadir, // data dir
 			"-k", datadir, // socket dir
-			"-p", boot.cluster.PostgreSQL.Connection["port"],
+			"-p", super.cluster.PostgreSQL.Connection["port"],
 		))
 	}()
 
@@ -70,7 +73,7 @@ func (runPostgreSQL) Run(ctx context.Context, fail func(error), boot *Booter) er
 		if ctx.Err() != nil {
 			return ctx.Err()
 		}
-		if exec.CommandContext(ctx, "pg_isready", "--timeout=10", "--host="+boot.cluster.PostgreSQL.Connection["host"], "--port="+port).Run() == nil {
+		if exec.CommandContext(ctx, "pg_isready", "--timeout=10", "--host="+super.cluster.PostgreSQL.Connection["host"], "--port="+port).Run() == nil {
 			break
 		}
 		time.Sleep(time.Second / 2)
@@ -89,11 +92,11 @@ func (runPostgreSQL) Run(ctx context.Context, fail func(error), boot *Booter) er
 		return fmt.Errorf("db conn failed: %s", err)
 	}
 	defer conn.Close()
-	_, err = conn.ExecContext(ctx, `CREATE USER `+pq.QuoteIdentifier(boot.cluster.PostgreSQL.Connection["user"])+` WITH SUPERUSER ENCRYPTED PASSWORD `+pq.QuoteLiteral(boot.cluster.PostgreSQL.Connection["password"]))
+	_, err = conn.ExecContext(ctx, `CREATE USER `+pq.QuoteIdentifier(super.cluster.PostgreSQL.Connection["user"])+` WITH SUPERUSER ENCRYPTED PASSWORD `+pq.QuoteLiteral(super.cluster.PostgreSQL.Connection["password"]))
 	if err != nil {
 		return fmt.Errorf("createuser failed: %s", err)
 	}
-	_, err = conn.ExecContext(ctx, `CREATE DATABASE `+pq.QuoteIdentifier(boot.cluster.PostgreSQL.Connection["dbname"]))
+	_, err = conn.ExecContext(ctx, `CREATE DATABASE `+pq.QuoteIdentifier(super.cluster.PostgreSQL.Connection["dbname"]))
 	if err != nil {
 		return fmt.Errorf("createdb failed: %s", err)
 	}
diff --git a/lib/boot/seed.go b/lib/boot/seed.go
index 9f086d544..650c83688 100644
--- a/lib/boot/seed.go
+++ b/lib/boot/seed.go
@@ -8,18 +8,19 @@ import (
 	"context"
 )
 
+// Populate a blank database with arvados tables and seed rows.
 type seedDatabase struct{}
 
 func (seedDatabase) String() string {
 	return "seedDatabase"
 }
 
-func (seedDatabase) Run(ctx context.Context, fail func(error), boot *Booter) error {
-	err := boot.wait(ctx, runPostgreSQL{}, installPassenger{src: "services/api"})
+func (seedDatabase) Run(ctx context.Context, fail func(error), super *Supervisor) error {
+	err := super.wait(ctx, runPostgreSQL{}, installPassenger{src: "services/api"})
 	if err != nil {
 		return err
 	}
-	err = boot.RunProgram(ctx, "services/api", nil, nil, "bundle", "exec", "rake", "db:setup")
+	err = super.RunProgram(ctx, "services/api", nil, nil, "bundle", "exec", "rake", "db:setup")
 	if err != nil {
 		return err
 	}
diff --git a/lib/boot/service.go b/lib/boot/service.go
index 0ebf647ad..018e9f8bb 100644
--- a/lib/boot/service.go
+++ b/lib/boot/service.go
@@ -6,41 +6,52 @@ package boot
 
 import (
 	"context"
+	"errors"
 	"path/filepath"
 
 	"git.arvados.org/arvados.git/sdk/go/arvados"
 )
 
+// Run a service using the arvados-server binary.
+//
+// In future this will bring up the service in the current process,
+// but for now (at least until the subcommand handlers get a shutdown
+// mechanism) it starts a child process using the arvados-server
+// binary, which the supervisor is assumed to have installed in
+// {super.tempdir}/bin/.
 type runServiceCommand struct {
-	name    string
-	svc     arvados.Service
-	depends []bootTask
+	name    string           // arvados-server subcommand, e.g., "controller"
+	svc     arvados.Service  // cluster.Services.* entry with the desired InternalURLs
+	depends []supervisedTask // wait for these tasks before starting
 }
 
 func (runner runServiceCommand) String() string {
 	return runner.name
 }
 
-func (runner runServiceCommand) Run(ctx context.Context, fail func(error), boot *Booter) error {
-	boot.wait(ctx, runner.depends...)
-	binfile := filepath.Join(boot.tempdir, "bin", "arvados-server")
-	err := boot.RunProgram(ctx, boot.tempdir, nil, nil, binfile, "-version")
+func (runner runServiceCommand) Run(ctx context.Context, fail func(error), super *Supervisor) error {
+	binfile := filepath.Join(super.tempdir, "bin", "arvados-server")
+	err := super.RunProgram(ctx, super.tempdir, nil, nil, binfile, "-version")
 	if err != nil {
 		return err
 	}
-	go func() {
-		var u arvados.URL
-		for u = range runner.svc.InternalURLs {
-		}
-		fail(boot.RunProgram(ctx, boot.tempdir, nil, []string{"ARVADOS_SERVICE_INTERNAL_URL=" + u.String()}, binfile, runner.name, "-config", boot.configfile))
-	}()
+	super.wait(ctx, runner.depends...)
+	for u := range runner.svc.InternalURLs {
+		u := u
+		super.waitShutdown.Add(1)
+		go func() {
+			defer super.waitShutdown.Done()
+			fail(super.RunProgram(ctx, super.tempdir, nil, []string{"ARVADOS_SERVICE_INTERNAL_URL=" + u.String()}, binfile, runner.name, "-config", super.configfile))
+		}()
+	}
 	return nil
 }
 
+// Run a Go service that isn't bundled in arvados-server.
 type runGoProgram struct {
-	src     string
-	svc     arvados.Service
-	depends []bootTask
+	src     string           // source dir, e.g., "services/keepproxy"
+	svc     arvados.Service  // cluster.Services.* entry with the desired InternalURLs
+	depends []supervisedTask // wait for these tasks before starting
 }
 
 func (runner runGoProgram) String() string {
@@ -48,9 +59,12 @@ func (runner runGoProgram) String() string {
 	return basename
 }
 
-func (runner runGoProgram) Run(ctx context.Context, fail func(error), boot *Booter) error {
-	boot.wait(ctx, runner.depends...)
-	binfile, err := boot.installGoProgram(ctx, runner.src)
+func (runner runGoProgram) Run(ctx context.Context, fail func(error), super *Supervisor) error {
+	if len(runner.svc.InternalURLs) == 0 {
+		return errors.New("bug: runGoProgram needs non-empty svc.InternalURLs")
+	}
+
+	binfile, err := super.installGoProgram(ctx, runner.src)
 	if err != nil {
 		return err
 	}
@@ -58,26 +72,18 @@ func (runner runGoProgram) Run(ctx context.Context, fail func(error), boot *Boot
 		return ctx.Err()
 	}
 
-	err = boot.RunProgram(ctx, boot.tempdir, nil, nil, binfile, "-version")
+	err = super.RunProgram(ctx, super.tempdir, nil, nil, binfile, "-version")
 	if err != nil {
 		return err
 	}
-	if len(runner.svc.InternalURLs) > 0 {
-		// Run one for each URL
-		for u := range runner.svc.InternalURLs {
-			u := u
-			boot.waitShutdown.Add(1)
-			go func() {
-				defer boot.waitShutdown.Done()
-				fail(boot.RunProgram(ctx, boot.tempdir, nil, []string{"ARVADOS_SERVICE_INTERNAL_URL=" + u.String()}, binfile))
-			}()
-		}
-	} else {
-		// Just run one
-		boot.waitShutdown.Add(1)
+
+	super.wait(ctx, runner.depends...)
+	for u := range runner.svc.InternalURLs {
+		u := u
+		super.waitShutdown.Add(1)
 		go func() {
-			defer boot.waitShutdown.Done()
-			fail(boot.RunProgram(ctx, boot.tempdir, nil, nil, binfile))
+			defer super.waitShutdown.Done()
+			fail(super.RunProgram(ctx, super.tempdir, nil, []string{"ARVADOS_SERVICE_INTERNAL_URL=" + u.String()}, binfile))
 		}()
 	}
 	return nil
diff --git a/lib/boot/cmd.go b/lib/boot/supervisor.go
similarity index 55%
copy from lib/boot/cmd.go
copy to lib/boot/supervisor.go
index e8a86e6ba..59e1ae352 100644
--- a/lib/boot/cmd.go
+++ b/lib/boot/supervisor.go
@@ -10,7 +10,6 @@ import (
 	"crypto/rand"
 	"encoding/json"
 	"errors"
-	"flag"
 	"fmt"
 	"io"
 	"io/ioutil"
@@ -25,8 +24,6 @@ import (
 	"syscall"
 	"time"
 
-	"git.arvados.org/arvados.git/lib/cmd"
-	"git.arvados.org/arvados.git/lib/config"
 	"git.arvados.org/arvados.git/lib/service"
 	"git.arvados.org/arvados.git/sdk/go/arvados"
 	"git.arvados.org/arvados.git/sdk/go/ctxlog"
@@ -34,80 +31,7 @@ import (
 	"github.com/sirupsen/logrus"
 )
 
-var Command cmd.Handler = bootCommand{}
-
-type bootTask interface {
-	// Execute the task. Run should return nil when the task is
-	// done enough to satisfy a dependency relationship (e.g., the
-	// service is running and ready). If the task starts a
-	// goroutine that fails after Run returns (e.g., the service
-	// shuts down), it should call cancel.
-	Run(ctx context.Context, fail func(error), boot *Booter) error
-	String() string
-}
-
-type bootCommand struct{}
-
-func (bootCommand) RunCommand(prog string, args []string, stdin io.Reader, stdout, stderr io.Writer) int {
-	boot := &Booter{
-		Stderr: stderr,
-		logger: ctxlog.New(stderr, "json", "info"),
-	}
-
-	ctx := ctxlog.Context(context.Background(), boot.logger)
-	ctx, cancel := context.WithCancel(ctx)
-	defer cancel()
-
-	var err error
-	defer func() {
-		if err != nil {
-			boot.logger.WithError(err).Info("exiting")
-		}
-	}()
-
-	flags := flag.NewFlagSet(prog, flag.ContinueOnError)
-	flags.SetOutput(stderr)
-	loader := config.NewLoader(stdin, boot.logger)
-	loader.SetupFlags(flags)
-	versionFlag := flags.Bool("version", false, "Write version information to stdout and exit 0")
-	flags.StringVar(&boot.SourcePath, "source", ".", "arvados source tree `directory`")
-	flags.StringVar(&boot.LibPath, "lib", "/var/lib/arvados", "`directory` to install dependencies and library files")
-	flags.StringVar(&boot.ClusterType, "type", "production", "cluster `type`: development, test, or production")
-	flags.StringVar(&boot.ListenHost, "listen-host", "localhost", "host name or interface address for service listeners")
-	flags.StringVar(&boot.ControllerAddr, "controller-address", ":0", "desired controller address, `host:port` or `:port`")
-	flags.BoolVar(&boot.OwnTemporaryDatabase, "own-temporary-database", false, "bring up a postgres server and create a temporary database")
-	err = flags.Parse(args)
-	if err == flag.ErrHelp {
-		err = nil
-		return 0
-	} else if err != nil {
-		return 2
-	} else if *versionFlag {
-		return cmd.Version.RunCommand(prog, args, stdin, stdout, stderr)
-	} else if boot.ClusterType != "development" && boot.ClusterType != "test" && boot.ClusterType != "production" {
-		err = fmt.Errorf("cluster type must be 'development', 'test', or 'production'")
-		return 2
-	}
-
-	loader.SkipAPICalls = true
-	cfg, err := loader.Load()
-	if err != nil {
-		return 1
-	}
-
-	boot.Start(ctx, cfg)
-	defer boot.Stop()
-	if url, ok := boot.WaitReady(); ok {
-		fmt.Fprintln(stdout, url)
-		// Wait for signal/crash + orderly shutdown
-		<-boot.done
-		return 0
-	} else {
-		return 1
-	}
-}
-
-type Booter struct {
+type Supervisor struct {
 	SourcePath           string // e.g., /home/username/src/arvados
 	SourceVersion        string // e.g., acbd1324...
 	LibPath              string // e.g., /var/lib/arvados
@@ -130,15 +54,11 @@ type Booter struct {
 	tempdir    string
 	configfile string
 	environ    []string // for child processes
-
-	setupRubyOnce sync.Once
-	setupRubyErr  error
-	goMutex       sync.Mutex
 }
 
-func (boot *Booter) Start(ctx context.Context, cfg *arvados.Config) {
-	boot.ctx, boot.cancel = context.WithCancel(ctx)
-	boot.done = make(chan struct{})
+func (super *Supervisor) Start(ctx context.Context, cfg *arvados.Config) {
+	super.ctx, super.cancel = context.WithCancel(ctx)
+	super.done = make(chan struct{})
 
 	go func() {
 		sigch := make(chan os.Signal)
@@ -146,48 +66,48 @@ func (boot *Booter) Start(ctx context.Context, cfg *arvados.Config) {
 		defer signal.Stop(sigch)
 		go func() {
 			for sig := range sigch {
-				boot.logger.WithField("signal", sig).Info("caught signal")
-				boot.cancel()
+				super.logger.WithField("signal", sig).Info("caught signal")
+				super.cancel()
 			}
 		}()
 
-		err := boot.run(cfg)
+		err := super.run(cfg)
 		if err != nil {
-			fmt.Fprintln(boot.Stderr, err)
+			fmt.Fprintln(super.Stderr, err)
 		}
-		close(boot.done)
+		close(super.done)
 	}()
 }
 
-func (boot *Booter) run(cfg *arvados.Config) error {
+func (super *Supervisor) run(cfg *arvados.Config) error {
 	cwd, err := os.Getwd()
 	if err != nil {
 		return err
 	}
-	if !strings.HasPrefix(boot.SourcePath, "/") {
-		boot.SourcePath = filepath.Join(cwd, boot.SourcePath)
+	if !strings.HasPrefix(super.SourcePath, "/") {
+		super.SourcePath = filepath.Join(cwd, super.SourcePath)
 	}
-	boot.SourcePath, err = filepath.EvalSymlinks(boot.SourcePath)
+	super.SourcePath, err = filepath.EvalSymlinks(super.SourcePath)
 	if err != nil {
 		return err
 	}
 
-	boot.tempdir, err = ioutil.TempDir("", "arvados-server-boot-")
+	super.tempdir, err = ioutil.TempDir("", "arvados-server-boot-")
 	if err != nil {
 		return err
 	}
-	defer os.RemoveAll(boot.tempdir)
-	if err := os.Mkdir(filepath.Join(boot.tempdir, "bin"), 0777); err != nil {
+	defer os.RemoveAll(super.tempdir)
+	if err := os.Mkdir(filepath.Join(super.tempdir, "bin"), 0777); err != nil {
 		return err
 	}
 
 	// Fill in any missing config keys, and write the resulting
 	// config in the temp dir for child services to use.
-	err = boot.autofillConfig(cfg, boot.logger)
+	err = super.autofillConfig(cfg, super.logger)
 	if err != nil {
 		return err
 	}
-	conffile, err := os.OpenFile(filepath.Join(boot.tempdir, "config.yml"), os.O_CREATE|os.O_WRONLY, 0777)
+	conffile, err := os.OpenFile(filepath.Join(super.tempdir, "config.yml"), os.O_CREATE|os.O_WRONLY, 0777)
 	if err != nil {
 		return err
 	}
@@ -200,51 +120,51 @@ func (boot *Booter) run(cfg *arvados.Config) error {
 	if err != nil {
 		return err
 	}
-	boot.configfile = conffile.Name()
+	super.configfile = conffile.Name()
 
-	boot.environ = os.Environ()
-	boot.cleanEnv()
-	boot.setEnv("ARVADOS_CONFIG", boot.configfile)
-	boot.setEnv("RAILS_ENV", boot.ClusterType)
-	boot.setEnv("TMPDIR", boot.tempdir)
-	boot.prependEnv("PATH", filepath.Join(boot.tempdir, "bin")+":")
-	boot.prependEnv("PATH", filepath.Join(boot.LibPath, "bin")+":")
+	super.environ = os.Environ()
+	super.cleanEnv()
+	super.setEnv("ARVADOS_CONFIG", super.configfile)
+	super.setEnv("RAILS_ENV", super.ClusterType)
+	super.setEnv("TMPDIR", super.tempdir)
+	super.prependEnv("PATH", filepath.Join(super.tempdir, "bin")+":")
+	super.prependEnv("PATH", filepath.Join(super.LibPath, "bin")+":")
 
-	boot.cluster, err = cfg.GetCluster("")
+	super.cluster, err = cfg.GetCluster("")
 	if err != nil {
 		return err
 	}
 	// Now that we have the config, replace the bootstrap logger
 	// with a new one according to the logging config.
-	loglevel := boot.cluster.SystemLogs.LogLevel
+	loglevel := super.cluster.SystemLogs.LogLevel
 	if s := os.Getenv("ARVADOS_DEBUG"); s != "" && s != "0" {
 		loglevel = "debug"
 	}
-	boot.logger = ctxlog.New(boot.Stderr, boot.cluster.SystemLogs.Format, loglevel).WithFields(logrus.Fields{
+	super.logger = ctxlog.New(super.Stderr, super.cluster.SystemLogs.Format, loglevel).WithFields(logrus.Fields{
 		"PID": os.Getpid(),
 	})
 
-	if boot.SourceVersion == "" {
+	if super.SourceVersion == "" {
 		// Find current source tree version.
 		var buf bytes.Buffer
-		err = boot.RunProgram(boot.ctx, ".", &buf, nil, "git", "diff", "--shortstat")
+		err = super.RunProgram(super.ctx, ".", &buf, nil, "git", "diff", "--shortstat")
 		if err != nil {
 			return err
 		}
 		dirty := buf.Len() > 0
 		buf.Reset()
-		err = boot.RunProgram(boot.ctx, ".", &buf, nil, "git", "log", "-n1", "--format=%H")
+		err = super.RunProgram(super.ctx, ".", &buf, nil, "git", "log", "-n1", "--format=%H")
 		if err != nil {
 			return err
 		}
-		boot.SourceVersion = strings.TrimSpace(buf.String())
+		super.SourceVersion = strings.TrimSpace(buf.String())
 		if dirty {
-			boot.SourceVersion += "+uncommitted"
+			super.SourceVersion += "+uncommitted"
 		}
 	} else {
 		return errors.New("specifying a version to run is not yet supported")
 	}
-	for _, dir := range []string{boot.LibPath, filepath.Join(boot.LibPath, "bin")} {
+	for _, dir := range []string{super.LibPath, filepath.Join(super.LibPath, "bin")} {
 		if _, err = os.Stat(filepath.Join(dir, ".")); os.IsNotExist(err) {
 			err = os.Mkdir(dir, 0755)
 			if err != nil {
@@ -254,135 +174,137 @@ func (boot *Booter) run(cfg *arvados.Config) error {
 			return err
 		}
 	}
-	_, err = boot.installGoProgram(boot.ctx, "cmd/arvados-server")
+	_, err = super.installGoProgram(super.ctx, "cmd/arvados-server")
 	if err != nil {
 		return err
 	}
-	err = boot.setupRubyEnv()
+	err = super.setupRubyEnv()
 	if err != nil {
 		return err
 	}
 
-	tasks := []bootTask{
+	tasks := []supervisedTask{
 		createCertificates{},
 		runPostgreSQL{},
 		runNginx{},
-		runServiceCommand{name: "controller", svc: boot.cluster.Services.Controller, depends: []bootTask{runPostgreSQL{}}},
-		runGoProgram{src: "services/arv-git-httpd"},
-		runGoProgram{src: "services/health"},
-		runGoProgram{src: "services/keepproxy", depends: []bootTask{runPassenger{src: "services/api"}}},
-		runGoProgram{src: "services/keepstore", svc: boot.cluster.Services.Keepstore},
-		runGoProgram{src: "services/keep-web"},
-		runGoProgram{src: "services/ws", depends: []bootTask{runPostgreSQL{}}},
+		runServiceCommand{name: "controller", svc: super.cluster.Services.Controller, depends: []supervisedTask{runPostgreSQL{}}},
+		runGoProgram{src: "services/arv-git-httpd", svc: super.cluster.Services.GitHTTP},
+		runGoProgram{src: "services/health", svc: super.cluster.Services.Health},
+		runGoProgram{src: "services/keepproxy", svc: super.cluster.Services.Keepproxy, depends: []supervisedTask{runPassenger{src: "services/api"}}},
+		runGoProgram{src: "services/keepstore", svc: super.cluster.Services.Keepstore},
+		runGoProgram{src: "services/keep-web", svc: super.cluster.Services.WebDAV},
+		runGoProgram{src: "services/ws", svc: super.cluster.Services.Websocket, depends: []supervisedTask{runPostgreSQL{}}},
 		installPassenger{src: "services/api"},
-		runPassenger{src: "services/api", svc: boot.cluster.Services.RailsAPI, depends: []bootTask{createCertificates{}, runPostgreSQL{}, installPassenger{src: "services/api"}}},
-		installPassenger{src: "apps/workbench", depends: []bootTask{installPassenger{src: "services/api"}}}, // dependency ensures workbench doesn't delay api startup
-		runPassenger{src: "apps/workbench", svc: boot.cluster.Services.Workbench1, depends: []bootTask{installPassenger{src: "apps/workbench"}}},
+		runPassenger{src: "services/api", svc: super.cluster.Services.RailsAPI, depends: []supervisedTask{createCertificates{}, runPostgreSQL{}, installPassenger{src: "services/api"}}},
+		installPassenger{src: "apps/workbench", depends: []supervisedTask{installPassenger{src: "services/api"}}}, // dependency ensures workbench doesn't delay api startup
+		runPassenger{src: "apps/workbench", svc: super.cluster.Services.Workbench1, depends: []supervisedTask{installPassenger{src: "apps/workbench"}}},
 		seedDatabase{},
 	}
-	if boot.ClusterType != "test" {
+	if super.ClusterType != "test" {
 		tasks = append(tasks,
-			runServiceCommand{name: "dispatch-cloud", svc: boot.cluster.Services.Controller},
+			runServiceCommand{name: "dispatch-cloud", svc: super.cluster.Services.Controller},
 			runGoProgram{src: "services/keep-balance"},
 		)
 	}
-	boot.tasksReady = map[string]chan bool{}
+	super.tasksReady = map[string]chan bool{}
 	for _, task := range tasks {
-		boot.tasksReady[task.String()] = make(chan bool)
+		super.tasksReady[task.String()] = make(chan bool)
 	}
 	for _, task := range tasks {
 		task := task
 		fail := func(err error) {
-			if boot.ctx.Err() != nil {
+			if super.ctx.Err() != nil {
 				return
 			}
-			boot.cancel()
-			boot.logger.WithField("task", task.String()).WithError(err).Error("task failed")
+			super.cancel()
+			super.logger.WithField("task", task.String()).WithError(err).Error("task failed")
 		}
 		go func() {
-			boot.logger.WithField("task", task.String()).Info("starting")
-			err := task.Run(boot.ctx, fail, boot)
+			super.logger.WithField("task", task.String()).Info("starting")
+			err := task.Run(super.ctx, fail, super)
 			if err != nil {
 				fail(err)
 				return
 			}
-			close(boot.tasksReady[task.String()])
+			close(super.tasksReady[task.String()])
 		}()
 	}
-	err = boot.wait(boot.ctx, tasks...)
+	err = super.wait(super.ctx, tasks...)
 	if err != nil {
 		return err
 	}
-	boot.logger.Info("all startup tasks are complete; starting health checks")
-	boot.healthChecker = &health.Aggregator{Cluster: boot.cluster}
-	<-boot.ctx.Done()
-	boot.logger.Info("shutting down")
-	boot.waitShutdown.Wait()
-	return boot.ctx.Err()
+	super.logger.Info("all startup tasks are complete; starting health checks")
+	super.healthChecker = &health.Aggregator{Cluster: super.cluster}
+	<-super.ctx.Done()
+	super.logger.Info("shutting down")
+	super.waitShutdown.Wait()
+	return super.ctx.Err()
 }
 
-func (boot *Booter) wait(ctx context.Context, tasks ...bootTask) error {
+func (super *Supervisor) wait(ctx context.Context, tasks ...supervisedTask) error {
 	for _, task := range tasks {
-		ch, ok := boot.tasksReady[task.String()]
+		ch, ok := super.tasksReady[task.String()]
 		if !ok {
 			return fmt.Errorf("no such task: %s", task)
 		}
-		boot.logger.WithField("task", task.String()).Info("waiting")
+		super.logger.WithField("task", task.String()).Info("waiting")
 		select {
 		case <-ch:
-			boot.logger.WithField("task", task.String()).Info("ready")
+			super.logger.WithField("task", task.String()).Info("ready")
 		case <-ctx.Done():
-			boot.logger.WithField("task", task.String()).Info("task was never ready")
+			super.logger.WithField("task", task.String()).Info("task was never ready")
 			return ctx.Err()
 		}
 	}
 	return nil
 }
 
-func (boot *Booter) Stop() {
-	boot.cancel()
-	<-boot.done
+func (super *Supervisor) Stop() {
+	super.cancel()
+	<-super.done
 }
 
-func (boot *Booter) WaitReady() (*arvados.URL, bool) {
+func (super *Supervisor) WaitReady() (*arvados.URL, bool) {
 	ticker := time.NewTicker(time.Second)
 	defer ticker.Stop()
-	for waiting := true; waiting; {
+	for waiting := "all"; waiting != ""; {
 		select {
 		case <-ticker.C:
-		case <-boot.ctx.Done():
+		case <-super.ctx.Done():
 			return nil, false
 		}
-		if boot.healthChecker == nil {
+		if super.healthChecker == nil {
 			// not set up yet
 			continue
 		}
-		resp := boot.healthChecker.ClusterHealth()
+		resp := super.healthChecker.ClusterHealth()
 		// The overall health check (resp.Health=="OK") might
 		// never pass due to missing components (like
 		// arvados-dispatch-cloud in a test cluster), so
 		// instead we wait for all configured components to
 		// pass.
-		waiting = false
+		waiting = ""
 		for target, check := range resp.Checks {
 			if check.Health != "OK" {
-				waiting = true
-				boot.logger.WithField("target", target).Debug("waiting")
+				waiting += " " + target
 			}
 		}
+		if waiting != "" {
+			super.logger.WithField("targets", waiting[1:]).Info("waiting")
+		}
 	}
-	u := boot.cluster.Services.Controller.ExternalURL
+	u := super.cluster.Services.Controller.ExternalURL
 	return &u, true
 }
 
-func (boot *Booter) prependEnv(key, prepend string) {
-	for i, s := range boot.environ {
+func (super *Supervisor) prependEnv(key, prepend string) {
+	for i, s := range super.environ {
 		if strings.HasPrefix(s, key+"=") {
-			boot.environ[i] = key + "=" + prepend + s[len(key)+1:]
+			super.environ[i] = key + "=" + prepend + s[len(key)+1:]
 			return
 		}
 	}
-	boot.environ = append(boot.environ, key+"="+prepend)
+	super.environ = append(super.environ, key+"="+prepend)
 }
 
 var cleanEnvPrefixes = []string{
@@ -391,9 +313,9 @@ var cleanEnvPrefixes = []string{
 	"ARVADOS_",
 }
 
-func (boot *Booter) cleanEnv() {
+func (super *Supervisor) cleanEnv() {
 	var cleaned []string
-	for _, s := range boot.environ {
+	for _, s := range super.environ {
 		drop := false
 		for _, p := range cleanEnvPrefixes {
 			if strings.HasPrefix(s, p) {
@@ -405,17 +327,17 @@ func (boot *Booter) cleanEnv() {
 			cleaned = append(cleaned, s)
 		}
 	}
-	boot.environ = cleaned
+	super.environ = cleaned
 }
 
-func (boot *Booter) setEnv(key, val string) {
-	for i, s := range boot.environ {
+func (super *Supervisor) setEnv(key, val string) {
+	for i, s := range super.environ {
 		if strings.HasPrefix(s, key+"=") {
-			boot.environ[i] = key + "=" + val
+			super.environ[i] = key + "=" + val
 			return
 		}
 	}
-	boot.environ = append(boot.environ, key+"="+val)
+	super.environ = append(super.environ, key+"="+val)
 }
 
 // Remove all but the first occurrence of each env var.
@@ -435,36 +357,36 @@ func dedupEnv(in []string) []string {
 	return out
 }
 
-func (boot *Booter) installGoProgram(ctx context.Context, srcpath string) (string, error) {
+func (super *Supervisor) installGoProgram(ctx context.Context, srcpath string) (string, error) {
 	_, basename := filepath.Split(srcpath)
-	bindir := filepath.Join(boot.tempdir, "bin")
+	bindir := filepath.Join(super.tempdir, "bin")
 	binfile := filepath.Join(bindir, basename)
-	err := boot.RunProgram(ctx, filepath.Join(boot.SourcePath, srcpath), nil, []string{"GOBIN=" + bindir}, "go", "install", "-ldflags", "-X git.arvados.org/arvados.git/lib/cmd.version="+boot.SourceVersion+" -X main.version="+boot.SourceVersion)
+	err := super.RunProgram(ctx, filepath.Join(super.SourcePath, srcpath), nil, []string{"GOBIN=" + bindir}, "go", "install", "-ldflags", "-X git.arvados.org/arvados.git/lib/cmd.version="+super.SourceVersion+" -X main.version="+super.SourceVersion)
 	return binfile, err
 }
 
-func (boot *Booter) setupRubyEnv() error {
+func (super *Supervisor) setupRubyEnv() error {
 	cmd := exec.Command("gem", "env", "gempath")
-	cmd.Env = boot.environ
+	cmd.Env = super.environ
 	buf, err := cmd.Output() // /var/lib/arvados/.gem/ruby/2.5.0/bin:...
 	if err != nil || len(buf) == 0 {
 		return fmt.Errorf("gem env gempath: %v", err)
 	}
 	gempath := string(bytes.Split(buf, []byte{':'})[0])
-	boot.prependEnv("PATH", gempath+"/bin:")
-	boot.setEnv("GEM_HOME", gempath)
-	boot.setEnv("GEM_PATH", gempath)
+	super.prependEnv("PATH", gempath+"/bin:")
+	super.setEnv("GEM_HOME", gempath)
+	super.setEnv("GEM_PATH", gempath)
 	// Passenger install doesn't work unless $HOME is ~user
 	u, err := user.Current()
 	if err != nil {
 		return err
 	}
-	boot.setEnv("HOME", u.HomeDir)
+	super.setEnv("HOME", u.HomeDir)
 	return nil
 }
 
-func (boot *Booter) lookPath(prog string) string {
-	for _, val := range boot.environ {
+func (super *Supervisor) lookPath(prog string) string {
+	for _, val := range super.environ {
 		if strings.HasPrefix(val, "PATH=") {
 			for _, dir := range filepath.SplitList(val[5:]) {
 				path := filepath.Join(dir, prog)
@@ -485,11 +407,11 @@ func (boot *Booter) lookPath(prog string) string {
 //
 // Child's stdout will be written to output if non-nil, otherwise the
 // boot command's stderr.
-func (boot *Booter) RunProgram(ctx context.Context, dir string, output io.Writer, env []string, prog string, args ...string) error {
+func (super *Supervisor) RunProgram(ctx context.Context, dir string, output io.Writer, env []string, prog string, args ...string) error {
 	cmdline := fmt.Sprintf("%s", append([]string{prog}, args...))
-	boot.logger.WithField("command", cmdline).WithField("dir", dir).Info("executing")
+	super.logger.WithField("command", cmdline).WithField("dir", dir).Info("executing")
 
-	logprefix := strings.TrimPrefix(prog, boot.tempdir+"/bin/")
+	logprefix := strings.TrimPrefix(prog, super.tempdir+"/bin/")
 	if prog == "bundle" && len(args) > 2 && args[0] == "exec" {
 		logprefix = args[1]
 	} else if prog == "arvados-server" && len(args) > 1 {
@@ -499,7 +421,7 @@ func (boot *Booter) RunProgram(ctx context.Context, dir string, output io.Writer
 		logprefix = dir + ": " + logprefix
 	}
 
-	cmd := exec.Command(boot.lookPath(prog), args...)
+	cmd := exec.Command(super.lookPath(prog), args...)
 	stdout, err := cmd.StdoutPipe()
 	if err != nil {
 		return err
@@ -508,7 +430,7 @@ func (boot *Booter) RunProgram(ctx context.Context, dir string, output io.Writer
 	if err != nil {
 		return err
 	}
-	logwriter := &service.LogPrefixer{Writer: boot.Stderr, Prefix: []byte("[" + logprefix + "] ")}
+	logwriter := &service.LogPrefixer{Writer: super.Stderr, Prefix: []byte("[" + logprefix + "] ")}
 	var copiers sync.WaitGroup
 	copiers.Add(1)
 	go func() {
@@ -528,10 +450,10 @@ func (boot *Booter) RunProgram(ctx context.Context, dir string, output io.Writer
 	if strings.HasPrefix(dir, "/") {
 		cmd.Dir = dir
 	} else {
-		cmd.Dir = filepath.Join(boot.SourcePath, dir)
+		cmd.Dir = filepath.Join(super.SourcePath, dir)
 	}
 	env = append([]string(nil), env...)
-	env = append(env, boot.environ...)
+	env = append(env, super.environ...)
 	cmd.Env = dedupEnv(env)
 
 	exited := false
@@ -573,7 +495,7 @@ func (boot *Booter) RunProgram(ctx context.Context, dir string, output io.Writer
 	return nil
 }
 
-func (boot *Booter) autofillConfig(cfg *arvados.Config, log logrus.FieldLogger) error {
+func (super *Supervisor) autofillConfig(cfg *arvados.Config, log logrus.FieldLogger) error {
 	cluster, err := cfg.GetCluster("")
 	if err != nil {
 		return err
@@ -581,7 +503,7 @@ func (boot *Booter) autofillConfig(cfg *arvados.Config, log logrus.FieldLogger)
 	usedPort := map[string]bool{}
 	nextPort := func() string {
 		for {
-			port, err := availablePort(boot.ListenHost + ":0")
+			port, err := availablePort(super.ListenHost + ":0")
 			if err != nil {
 				panic(err)
 			}
@@ -593,12 +515,12 @@ func (boot *Booter) autofillConfig(cfg *arvados.Config, log logrus.FieldLogger)
 		}
 	}
 	if cluster.Services.Controller.ExternalURL.Host == "" {
-		h, p, err := net.SplitHostPort(boot.ControllerAddr)
+		h, p, err := net.SplitHostPort(super.ControllerAddr)
 		if err != nil {
 			return err
 		}
 		if h == "" {
-			h = boot.ListenHost
+			h = super.ListenHost
 		}
 		if p == "0" {
 			p, err = availablePort(":0")
@@ -622,7 +544,7 @@ func (boot *Booter) autofillConfig(cfg *arvados.Config, log logrus.FieldLogger)
 		&cluster.Services.Websocket,
 		&cluster.Services.Workbench1,
 	} {
-		if svc == &cluster.Services.DispatchCloud && boot.ClusterType == "test" {
+		if svc == &cluster.Services.DispatchCloud && super.ClusterType == "test" {
 			continue
 		}
 		if svc.ExternalURL.Host == "" && (svc == &cluster.Services.Controller ||
@@ -632,11 +554,11 @@ func (boot *Booter) autofillConfig(cfg *arvados.Config, log logrus.FieldLogger)
 			svc == &cluster.Services.WebDAVDownload ||
 			svc == &cluster.Services.Websocket ||
 			svc == &cluster.Services.Workbench1) {
-			svc.ExternalURL = arvados.URL{Scheme: "https", Host: fmt.Sprintf("%s:%s", boot.ListenHost, nextPort())}
+			svc.ExternalURL = arvados.URL{Scheme: "https", Host: fmt.Sprintf("%s:%s", super.ListenHost, nextPort())}
 		}
 		if len(svc.InternalURLs) == 0 {
 			svc.InternalURLs = map[arvados.URL]arvados.ServiceInstance{
-				arvados.URL{Scheme: "http", Host: fmt.Sprintf("%s:%s", boot.ListenHost, nextPort())}: arvados.ServiceInstance{},
+				arvados.URL{Scheme: "http", Host: fmt.Sprintf("%s:%s", super.ListenHost, nextPort())}: arvados.ServiceInstance{},
 			}
 		}
 	}
@@ -652,26 +574,26 @@ func (boot *Booter) autofillConfig(cfg *arvados.Config, log logrus.FieldLogger)
 	if cluster.Collections.BlobSigningKey == "" {
 		cluster.Collections.BlobSigningKey = randomHexString(64)
 	}
-	if boot.ClusterType != "production" && cluster.Containers.DispatchPrivateKey == "" {
-		buf, err := ioutil.ReadFile(filepath.Join(boot.SourcePath, "lib", "dispatchcloud", "test", "sshkey_dispatch"))
+	if super.ClusterType != "production" && cluster.Containers.DispatchPrivateKey == "" {
+		buf, err := ioutil.ReadFile(filepath.Join(super.SourcePath, "lib", "dispatchcloud", "test", "sshkey_dispatch"))
 		if err != nil {
 			return err
 		}
 		cluster.Containers.DispatchPrivateKey = string(buf)
 	}
-	if boot.ClusterType != "production" {
+	if super.ClusterType != "production" {
 		cluster.TLS.Insecure = true
 	}
-	if boot.ClusterType == "test" {
+	if super.ClusterType == "test" {
 		// Add a second keepstore process.
-		cluster.Services.Keepstore.InternalURLs[arvados.URL{Scheme: "http", Host: fmt.Sprintf("%s:%s", boot.ListenHost, nextPort())}] = arvados.ServiceInstance{}
+		cluster.Services.Keepstore.InternalURLs[arvados.URL{Scheme: "http", Host: fmt.Sprintf("%s:%s", super.ListenHost, nextPort())}] = arvados.ServiceInstance{}
 
 		// Create a directory-backed volume for each keepstore
 		// process.
 		cluster.Volumes = map[string]arvados.Volume{}
 		for url := range cluster.Services.Keepstore.InternalURLs {
 			volnum := len(cluster.Volumes)
-			datadir := fmt.Sprintf("%s/keep%d.data", boot.tempdir, volnum)
+			datadir := fmt.Sprintf("%s/keep%d.data", super.tempdir, volnum)
 			if _, err = os.Stat(datadir + "/."); err == nil {
 			} else if !os.IsNotExist(err) {
 				return err
@@ -687,7 +609,7 @@ func (boot *Booter) autofillConfig(cfg *arvados.Config, log logrus.FieldLogger)
 			}
 		}
 	}
-	if boot.OwnTemporaryDatabase {
+	if super.OwnTemporaryDatabase {
 		cluster.PostgreSQL.Connection = arvados.PostgreSQLConnection{
 			"client_encoding": "utf8",
 			"host":            "localhost",
diff --git a/lib/controller/integration_test.go b/lib/controller/integration_test.go
index e679106dc..e308f8726 100644
--- a/lib/controller/integration_test.go
+++ b/lib/controller/integration_test.go
@@ -27,7 +27,7 @@ import (
 var _ = check.Suite(&IntegrationSuite{})
 
 type testCluster struct {
-	booter        boot.Booter
+	super         boot.Supervisor
 	config        arvados.Config
 	controllerURL *url.URL
 }
@@ -102,7 +102,7 @@ func (s *IntegrationSuite) SetUpSuite(c *check.C) {
 		cfg, err := loader.Load()
 		c.Assert(err, check.IsNil)
 		s.testClusters[id] = &testCluster{
-			booter: boot.Booter{
+			super: boot.Supervisor{
 				SourcePath:           filepath.Join(cwd, "..", ".."),
 				LibPath:              filepath.Join(cwd, "..", "..", "tmp"),
 				ClusterType:          "test",
@@ -113,10 +113,10 @@ func (s *IntegrationSuite) SetUpSuite(c *check.C) {
 			},
 			config: *cfg,
 		}
-		s.testClusters[id].booter.Start(context.Background(), &s.testClusters[id].config)
+		s.testClusters[id].super.Start(context.Background(), &s.testClusters[id].config)
 	}
 	for _, tc := range s.testClusters {
-		au, ok := tc.booter.WaitReady()
+		au, ok := tc.super.WaitReady()
 		c.Assert(ok, check.Equals, true)
 		u := url.URL(*au)
 		tc.controllerURL = &u
@@ -125,7 +125,7 @@ func (s *IntegrationSuite) SetUpSuite(c *check.C) {
 
 func (s *IntegrationSuite) TearDownSuite(c *check.C) {
 	for _, c := range s.testClusters {
-		c.booter.Stop()
+		c.super.Stop()
 	}
 }
 
diff --git a/lib/service/cmd.go b/lib/service/cmd.go
index 48912b889..7f2f78ee9 100644
--- a/lib/service/cmd.go
+++ b/lib/service/cmd.go
@@ -59,7 +59,7 @@ func (c *command) RunCommand(prog string, args []string, stdin io.Reader, stdout
 	var err error
 	defer func() {
 		if err != nil {
-			log.WithError(err).Info("exiting")
+			log.WithError(err).Error("exiting")
 		}
 	}()
 

commit 7caabd9c19c46ebc218d10b2c048e36d6e8cb2a4
Author: Tom Clegg <tom at tomclegg.ca>
Date:   Tue Feb 25 11:12:58 2020 -0500

    15954: Allow activating user using an admin token.
    
    This was intended to work already, but didn't.
    
    Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tom at tomclegg.ca>

diff --git a/services/api/app/controllers/arvados/v1/users_controller.rb b/services/api/app/controllers/arvados/v1/users_controller.rb
index 1cf3b9d78..224f2c0bd 100644
--- a/services/api/app/controllers/arvados/v1/users_controller.rb
+++ b/services/api/app/controllers/arvados/v1/users_controller.rb
@@ -45,8 +45,11 @@ class Arvados::V1::UsersController < ApplicationController
   end
 
   def activate
+    if params[:id] and params[:id].match(/\D/)
+      params[:uuid] = params.delete :id
+    end
     if current_user.andand.is_admin && params[:uuid]
-      @object = User.find params[:uuid]
+      @object = User.find_by_uuid params[:uuid]
     else
       @object = current_user
     end

commit 9acd9d8cdb6425b0ed40ed1800f3fe2d932c5d03
Author: Tom Clegg <tom at tomclegg.ca>
Date:   Tue Feb 25 11:11:47 2020 -0500

    15954: Fixup federation test.
    
    Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tom at tomclegg.ca>

diff --git a/lib/controller/integration_test.go b/lib/controller/integration_test.go
index 7cec2058c..e679106dc 100644
--- a/lib/controller/integration_test.go
+++ b/lib/controller/integration_test.go
@@ -81,16 +81,19 @@ func (s *IntegrationSuite) SetUpSuite(c *check.C) {
         Scheme: https
         Insecure: true
         Proxy: true
+        ActivateUsers: true
       z2222:
         Host: ` + hostport["z2222"] + `
         Scheme: https
         Insecure: true
         Proxy: true
+        ActivateUsers: true
       z3333:
         Host: ` + hostport["z3333"] + `
         Scheme: https
         Insecure: true
         Proxy: true
+        ActivateUsers: true
 `
 		loader := config.NewLoader(bytes.NewBufferString(yaml), ctxlog.TestLogger(c))
 		loader.Path = "-"
@@ -132,7 +135,7 @@ func (s *IntegrationSuite) conn(clusterID string) *rpc.Conn {
 
 func (s *IntegrationSuite) clientsWithToken(clusterID string, token string) (context.Context, *arvados.Client, *keepclient.KeepClient) {
 	cl := s.testClusters[clusterID].config.Clusters[clusterID]
-	rootctx := auth.NewContext(context.Background(), auth.NewCredentials(token))
+	ctx := auth.NewContext(context.Background(), auth.NewCredentials(token))
 	ac, err := arvados.NewClientFromConfig(&cl)
 	if err != nil {
 		panic(err)
@@ -143,7 +146,7 @@ func (s *IntegrationSuite) clientsWithToken(clusterID string, token string) (con
 		panic(err)
 	}
 	kc := keepclient.New(arv)
-	return rootctx, ac, kc
+	return ctx, ac, kc
 }
 
 func (s *IntegrationSuite) userClients(c *check.C, conn *rpc.Conn, rootctx context.Context, clusterID string, activate bool) (context.Context, *arvados.Client, *keepclient.KeepClient) {
@@ -160,6 +163,7 @@ func (s *IntegrationSuite) userClients(c *check.C, conn *rpc.Conn, rootctx conte
 	redirURL, err := url.Parse(login.RedirectLocation)
 	c.Assert(err, check.IsNil)
 	userToken := redirURL.Query().Get("api_token")
+	c.Logf("userToken: %q", userToken)
 	ctx, ac, kc := s.clientsWithToken(clusterID, userToken)
 	user, err := conn.UserGetCurrent(ctx, arvados.GetOptions{})
 	if err != nil {
@@ -169,10 +173,18 @@ func (s *IntegrationSuite) userClients(c *check.C, conn *rpc.Conn, rootctx conte
 	if err != nil {
 		panic(err)
 	}
+	_, err = conn.UserActivate(rootctx, arvados.UserActivateOptions{UUID: user.UUID})
+	if err != nil {
+		panic(err)
+	}
 	user, err = conn.UserGetCurrent(ctx, arvados.GetOptions{})
 	if err != nil {
 		panic(err)
 	}
+	c.Logf("user: %#v", user)
+	if !user.IsActive {
+		c.Fatal("failed to activate user")
+	}
 	return ctx, ac, kc
 }
 
@@ -187,8 +199,8 @@ func (s *IntegrationSuite) TestLoopDetection(c *check.C) {
 	// rootctx3, _, _ := s.rootClients("z3333")
 
 	userctx1, ac1, kc1 := s.userClients(c, conn1, rootctx1, "z1111", true)
-	_, err := conn1.CollectionGet(rootctx1, arvados.GetOptions{UUID: "1f4b0bc7583c2a7f9102c395f4ffc5e3+45"})
-	c.Check(err, check.ErrorMatches, `.*404 Not Found.*`)
+	_, err := conn1.CollectionGet(userctx1, arvados.GetOptions{UUID: "1f4b0bc7583c2a7f9102c395f4ffc5e3+45"})
+	c.Assert(err, check.ErrorMatches, `.*404 Not Found.*`)
 
 	var coll1 arvados.Collection
 	fs1, err := coll1.FileSystem(ac1, kc1)
@@ -202,6 +214,7 @@ func (s *IntegrationSuite) TestLoopDetection(c *check.C) {
 	coll1, err = conn1.CollectionCreate(userctx1, arvados.CreateOptions{Attrs: map[string]interface{}{
 		"manifest_text": mtxt,
 	}})
+	c.Assert(err, check.IsNil)
 	coll, err := conn3.CollectionGet(userctx1, arvados.GetOptions{UUID: "1f4b0bc7583c2a7f9102c395f4ffc5e3+45"})
 	c.Check(err, check.IsNil)
 	c.Check(coll.PortableDataHash, check.Equals, "1f4b0bc7583c2a7f9102c395f4ffc5e3+45")

commit f1a81ab9bb1da527e977f1a5667f86e37976bd4a
Author: Tom Clegg <tom at tomclegg.ca>
Date:   Tue Feb 25 11:11:21 2020 -0500

    15954: Report version when starting services.
    
    Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tom at tomclegg.ca>

diff --git a/lib/boot/cmd.go b/lib/boot/cmd.go
index e2bb98542..e8a86e6ba 100644
--- a/lib/boot/cmd.go
+++ b/lib/boot/cmd.go
@@ -9,6 +9,7 @@ import (
 	"context"
 	"crypto/rand"
 	"encoding/json"
+	"errors"
 	"flag"
 	"fmt"
 	"io"
@@ -108,6 +109,7 @@ func (bootCommand) RunCommand(prog string, args []string, stdin io.Reader, stdou
 
 type Booter struct {
 	SourcePath           string // e.g., /home/username/src/arvados
+	SourceVersion        string // e.g., acbd1324...
 	LibPath              string // e.g., /var/lib/arvados
 	ClusterType          string // e.g., production
 	ListenHost           string // e.g., localhost
@@ -222,6 +224,26 @@ func (boot *Booter) run(cfg *arvados.Config) error {
 		"PID": os.Getpid(),
 	})
 
+	if boot.SourceVersion == "" {
+		// Find current source tree version.
+		var buf bytes.Buffer
+		err = boot.RunProgram(boot.ctx, ".", &buf, nil, "git", "diff", "--shortstat")
+		if err != nil {
+			return err
+		}
+		dirty := buf.Len() > 0
+		buf.Reset()
+		err = boot.RunProgram(boot.ctx, ".", &buf, nil, "git", "log", "-n1", "--format=%H")
+		if err != nil {
+			return err
+		}
+		boot.SourceVersion = strings.TrimSpace(buf.String())
+		if dirty {
+			boot.SourceVersion += "+uncommitted"
+		}
+	} else {
+		return errors.New("specifying a version to run is not yet supported")
+	}
 	for _, dir := range []string{boot.LibPath, filepath.Join(boot.LibPath, "bin")} {
 		if _, err = os.Stat(filepath.Join(dir, ".")); os.IsNotExist(err) {
 			err = os.Mkdir(dir, 0755)
@@ -232,7 +254,7 @@ func (boot *Booter) run(cfg *arvados.Config) error {
 			return err
 		}
 	}
-	err = boot.installGoProgram(boot.ctx, "cmd/arvados-server")
+	_, err = boot.installGoProgram(boot.ctx, "cmd/arvados-server")
 	if err != nil {
 		return err
 	}
@@ -413,10 +435,12 @@ func dedupEnv(in []string) []string {
 	return out
 }
 
-func (boot *Booter) installGoProgram(ctx context.Context, srcpath string) error {
-	boot.goMutex.Lock()
-	defer boot.goMutex.Unlock()
-	return boot.RunProgram(ctx, filepath.Join(boot.SourcePath, srcpath), nil, []string{"GOBIN=" + boot.tempdir + "/bin"}, "go", "install")
+func (boot *Booter) installGoProgram(ctx context.Context, srcpath string) (string, error) {
+	_, basename := filepath.Split(srcpath)
+	bindir := filepath.Join(boot.tempdir, "bin")
+	binfile := filepath.Join(bindir, basename)
+	err := boot.RunProgram(ctx, filepath.Join(boot.SourcePath, srcpath), nil, []string{"GOBIN=" + bindir}, "go", "install", "-ldflags", "-X git.arvados.org/arvados.git/lib/cmd.version="+boot.SourceVersion+" -X main.version="+boot.SourceVersion)
+	return binfile, err
 }
 
 func (boot *Booter) setupRubyEnv() error {
@@ -557,7 +581,7 @@ func (boot *Booter) autofillConfig(cfg *arvados.Config, log logrus.FieldLogger)
 	usedPort := map[string]bool{}
 	nextPort := func() string {
 		for {
-			port, err := availablePort(":0")
+			port, err := availablePort(boot.ListenHost + ":0")
 			if err != nil {
 				panic(err)
 			}
diff --git a/lib/boot/service.go b/lib/boot/service.go
index 8cfea565a..0ebf647ad 100644
--- a/lib/boot/service.go
+++ b/lib/boot/service.go
@@ -23,11 +23,16 @@ func (runner runServiceCommand) String() string {
 
 func (runner runServiceCommand) Run(ctx context.Context, fail func(error), boot *Booter) error {
 	boot.wait(ctx, runner.depends...)
+	binfile := filepath.Join(boot.tempdir, "bin", "arvados-server")
+	err := boot.RunProgram(ctx, boot.tempdir, nil, nil, binfile, "-version")
+	if err != nil {
+		return err
+	}
 	go func() {
 		var u arvados.URL
 		for u = range runner.svc.InternalURLs {
 		}
-		fail(boot.RunProgram(ctx, boot.tempdir, nil, []string{"ARVADOS_SERVICE_INTERNAL_URL=" + u.String()}, "arvados-server", runner.name, "-config", boot.configfile))
+		fail(boot.RunProgram(ctx, boot.tempdir, nil, []string{"ARVADOS_SERVICE_INTERNAL_URL=" + u.String()}, binfile, runner.name, "-config", boot.configfile))
 	}()
 	return nil
 }
@@ -45,17 +50,18 @@ func (runner runGoProgram) String() string {
 
 func (runner runGoProgram) Run(ctx context.Context, fail func(error), boot *Booter) error {
 	boot.wait(ctx, runner.depends...)
-	bindir := filepath.Join(boot.tempdir, "bin")
-	err := boot.RunProgram(ctx, runner.src, nil, []string{"GOBIN=" + bindir}, "go", "install")
+	binfile, err := boot.installGoProgram(ctx, runner.src)
 	if err != nil {
 		return err
 	}
 	if ctx.Err() != nil {
 		return ctx.Err()
 	}
-	_, basename := filepath.Split(runner.src)
-	binfile := filepath.Join(bindir, basename)
 
+	err = boot.RunProgram(ctx, boot.tempdir, nil, nil, binfile, "-version")
+	if err != nil {
+		return err
+	}
 	if len(runner.svc.InternalURLs) > 0 {
 		// Run one for each URL
 		for u := range runner.svc.InternalURLs {

commit 348abbad014278d061b9f93ecbb5c5bedc03ae86
Author: Tom Clegg <tom at tomclegg.ca>
Date:   Tue Feb 25 10:09:01 2020 -0500

    15954: Propagate remote param.
    
    Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tom at tomclegg.ca>

diff --git a/sdk/go/arvados/api.go b/sdk/go/arvados/api.go
index aa670c539..62f9a95fd 100644
--- a/sdk/go/arvados/api.go
+++ b/sdk/go/arvados/api.go
@@ -63,6 +63,7 @@ type GetOptions struct {
 	Select       []string `json:"select"`
 	IncludeTrash bool     `json:"include_trash"`
 	ForwardedFor string   `json:"forwarded_for"`
+	Remote       string   `json:"remote"`
 }
 
 type UntrashOptions struct {

commit 8dedaac5a1758fab15243f82acab2092fc24e2df
Author: Tom Clegg <tom at tomclegg.ca>
Date:   Mon Feb 24 21:50:32 2020 -0500

    15954: Ensure stdout is copied before returning from RunProgram.
    
    Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tom at tomclegg.ca>

diff --git a/lib/boot/cmd.go b/lib/boot/cmd.go
index 62088a604..e2bb98542 100644
--- a/lib/boot/cmd.go
+++ b/lib/boot/cmd.go
@@ -485,12 +485,21 @@ func (boot *Booter) RunProgram(ctx context.Context, dir string, output io.Writer
 		return err
 	}
 	logwriter := &service.LogPrefixer{Writer: boot.Stderr, Prefix: []byte("[" + logprefix + "] ")}
-	go io.Copy(logwriter, stderr)
-	if output == nil {
-		go io.Copy(logwriter, stdout)
-	} else {
-		go io.Copy(output, stdout)
-	}
+	var copiers sync.WaitGroup
+	copiers.Add(1)
+	go func() {
+		io.Copy(logwriter, stderr)
+		copiers.Done()
+	}()
+	copiers.Add(1)
+	go func() {
+		if output == nil {
+			io.Copy(logwriter, stdout)
+		} else {
+			io.Copy(output, stdout)
+		}
+		copiers.Done()
+	}()
 
 	if strings.HasPrefix(dir, "/") {
 		cmd.Dir = dir
@@ -527,6 +536,7 @@ func (boot *Booter) RunProgram(ctx context.Context, dir string, output io.Writer
 	if err != nil {
 		return err
 	}
+	copiers.Wait()
 	err = cmd.Wait()
 	if ctx.Err() != nil {
 		// Return "context canceled", instead of the "killed"

commit 4f88adddbe3a15dd0cfd88b0f939f4e6d1e16611
Author: Tom Clegg <tom at tomclegg.ca>
Date:   Mon Feb 24 15:29:12 2020 -0500

    15954: Use user tokens for integration test.
    
    Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tom at tomclegg.ca>

diff --git a/lib/controller/integration_test.go b/lib/controller/integration_test.go
index c86126d3d..7cec2058c 100644
--- a/lib/controller/integration_test.go
+++ b/lib/controller/integration_test.go
@@ -72,7 +72,7 @@ func (s *IntegrationSuite) SetUpSuite(c *check.C) {
     TLS:
       Insecure: true
     Login:
-      LoginCluster: z1111
+      # LoginCluster: z1111
     SystemLogs:
       Format: text
     RemoteClusters:
@@ -126,43 +126,83 @@ func (s *IntegrationSuite) TearDownSuite(c *check.C) {
 	}
 }
 
-func (s *IntegrationSuite) conn(clusterID string) (*rpc.Conn, context.Context, *arvados.Client, *keepclient.KeepClient) {
+func (s *IntegrationSuite) conn(clusterID string) *rpc.Conn {
+	return rpc.NewConn(clusterID, s.testClusters[clusterID].controllerURL, true, rpc.PassthroughTokenProvider)
+}
+
+func (s *IntegrationSuite) clientsWithToken(clusterID string, token string) (context.Context, *arvados.Client, *keepclient.KeepClient) {
 	cl := s.testClusters[clusterID].config.Clusters[clusterID]
-	conn := rpc.NewConn(clusterID, s.testClusters[clusterID].controllerURL, true, rpc.PassthroughTokenProvider)
-	rootctx := auth.NewContext(context.Background(), auth.NewCredentials(cl.SystemRootToken))
+	rootctx := auth.NewContext(context.Background(), auth.NewCredentials(token))
 	ac, err := arvados.NewClientFromConfig(&cl)
 	if err != nil {
 		panic(err)
 	}
-	ac.AuthToken = cl.SystemRootToken
+	ac.AuthToken = token
 	arv, err := arvadosclient.New(ac)
 	if err != nil {
 		panic(err)
 	}
 	kc := keepclient.New(arv)
-	return conn, rootctx, ac, kc
+	return rootctx, ac, kc
+}
+
+func (s *IntegrationSuite) userClients(c *check.C, conn *rpc.Conn, rootctx context.Context, clusterID string, activate bool) (context.Context, *arvados.Client, *keepclient.KeepClient) {
+	login, err := conn.UserSessionCreate(rootctx, rpc.UserSessionCreateOptions{
+		ReturnTo: ",https://example.com",
+		AuthInfo: rpc.UserSessionAuthInfo{
+			Email:     "user at example.com",
+			FirstName: "Example",
+			LastName:  "User",
+			Username:  "example",
+		},
+	})
+	c.Assert(err, check.IsNil)
+	redirURL, err := url.Parse(login.RedirectLocation)
+	c.Assert(err, check.IsNil)
+	userToken := redirURL.Query().Get("api_token")
+	ctx, ac, kc := s.clientsWithToken(clusterID, userToken)
+	user, err := conn.UserGetCurrent(ctx, arvados.GetOptions{})
+	if err != nil {
+		panic(err)
+	}
+	_, err = conn.UserSetup(rootctx, arvados.UserSetupOptions{UUID: user.UUID})
+	if err != nil {
+		panic(err)
+	}
+	user, err = conn.UserGetCurrent(ctx, arvados.GetOptions{})
+	if err != nil {
+		panic(err)
+	}
+	return ctx, ac, kc
+}
+
+func (s *IntegrationSuite) rootClients(clusterID string) (context.Context, *arvados.Client, *keepclient.KeepClient) {
+	return s.clientsWithToken(clusterID, s.testClusters[clusterID].config.Clusters[clusterID].SystemRootToken)
 }
 
 func (s *IntegrationSuite) TestLoopDetection(c *check.C) {
-	conn1, rootctx1, _, _ := s.conn("z1111")
-	conn3, rootctx3, ac3, kc3 := s.conn("z3333")
+	conn1 := s.conn("z1111")
+	rootctx1, _, _ := s.rootClients("z1111")
+	conn3 := s.conn("z3333")
+	// rootctx3, _, _ := s.rootClients("z3333")
 
+	userctx1, ac1, kc1 := s.userClients(c, conn1, rootctx1, "z1111", true)
 	_, err := conn1.CollectionGet(rootctx1, arvados.GetOptions{UUID: "1f4b0bc7583c2a7f9102c395f4ffc5e3+45"})
 	c.Check(err, check.ErrorMatches, `.*404 Not Found.*`)
 
-	var coll3 arvados.Collection
-	fs3, err := coll3.FileSystem(ac3, kc3)
+	var coll1 arvados.Collection
+	fs1, err := coll1.FileSystem(ac1, kc1)
 	if err != nil {
 		c.Error(err)
 	}
-	f, err := fs3.OpenFile("foo", os.O_CREATE|os.O_RDWR, 0777)
+	f, err := fs1.OpenFile("foo", os.O_CREATE|os.O_RDWR, 0777)
 	f.Write([]byte("foo"))
 	f.Close()
-	mtxt, err := fs3.MarshalManifest(".")
-	coll3, err = conn3.CollectionCreate(rootctx3, arvados.CreateOptions{Attrs: map[string]interface{}{
+	mtxt, err := fs1.MarshalManifest(".")
+	coll1, err = conn1.CollectionCreate(userctx1, arvados.CreateOptions{Attrs: map[string]interface{}{
 		"manifest_text": mtxt,
 	}})
-	coll, err := conn1.CollectionGet(rootctx1, arvados.GetOptions{UUID: "1f4b0bc7583c2a7f9102c395f4ffc5e3+45"})
+	coll, err := conn3.CollectionGet(userctx1, arvados.GetOptions{UUID: "1f4b0bc7583c2a7f9102c395f4ffc5e3+45"})
 	c.Check(err, check.IsNil)
 	c.Check(coll.PortableDataHash, check.Equals, "1f4b0bc7583c2a7f9102c395f4ffc5e3+45")
 }

commit 75fa01ce18ee8de9ad689b0aa20e9a9c485526a7
Author: Tom Clegg <tom at tomclegg.ca>
Date:   Mon Feb 24 15:28:30 2020 -0500

    15954: Use a different loopback IP addr for each test cluster.
    
    Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tom at tomclegg.ca>

diff --git a/lib/controller/integration_test.go b/lib/controller/integration_test.go
index c78686ac8..c86126d3d 100644
--- a/lib/controller/integration_test.go
+++ b/lib/controller/integration_test.go
@@ -48,15 +48,19 @@ func (s *IntegrationSuite) SetUpSuite(c *check.C) {
 		"z2222": nil,
 		"z3333": nil,
 	}
-	port := map[string]string{}
+	hostport := map[string]string{}
 	for id := range s.testClusters {
-		port[id] = func() string {
-			ln, err := net.Listen("tcp", "localhost:0")
+		hostport[id] = func() string {
+			// TODO: Instead of expecting random ports on
+			// 127.0.0.11, 22, 33 to be race-safe, try
+			// different 127.x.y.z until finding one that
+			// isn't in use.
+			ln, err := net.Listen("tcp", ":0")
 			c.Assert(err, check.IsNil)
 			ln.Close()
 			_, port, err := net.SplitHostPort(ln.Addr().String())
 			c.Assert(err, check.IsNil)
-			return port
+			return "127.0.0." + id[3:] + ":" + port
 		}()
 	}
 	for id := range s.testClusters {
@@ -64,7 +68,7 @@ func (s *IntegrationSuite) SetUpSuite(c *check.C) {
   ` + id + `:
     Services:
       Controller:
-        ExternalURL: https://localhost:` + port[id] + `
+        ExternalURL: https://` + hostport[id] + `
     TLS:
       Insecure: true
     Login:
@@ -73,17 +77,20 @@ func (s *IntegrationSuite) SetUpSuite(c *check.C) {
       Format: text
     RemoteClusters:
       z1111:
-        Host: localhost:` + port["z1111"] + `
+        Host: ` + hostport["z1111"] + `
         Scheme: https
         Insecure: true
+        Proxy: true
       z2222:
-        Host: localhost:` + port["z2222"] + `
+        Host: ` + hostport["z2222"] + `
         Scheme: https
         Insecure: true
+        Proxy: true
       z3333:
-        Host: localhost:` + port["z3333"] + `
+        Host: ` + hostport["z3333"] + `
         Scheme: https
         Insecure: true
+        Proxy: true
 `
 		loader := config.NewLoader(bytes.NewBufferString(yaml), ctxlog.TestLogger(c))
 		loader.Path = "-"
@@ -96,7 +103,7 @@ func (s *IntegrationSuite) SetUpSuite(c *check.C) {
 				SourcePath:           filepath.Join(cwd, "..", ".."),
 				LibPath:              filepath.Join(cwd, "..", "..", "tmp"),
 				ClusterType:          "test",
-				ListenHost:           "localhost",
+				ListenHost:           "127.0.0." + id[3:],
 				ControllerAddr:       ":0",
 				OwnTemporaryDatabase: true,
 				Stderr:               &service.LogPrefixer{Writer: ctxlog.LogWriter(c.Log), Prefix: []byte("[" + id + "] ")},

commit 79e21a2dffa00d854631627a2a07a6bd8e130b51
Author: Tom Clegg <tom at tomclegg.ca>
Date:   Mon Feb 24 15:27:10 2020 -0500

    15954: Cleanup.
    
    Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tom at tomclegg.ca>

diff --git a/lib/boot/postgresql.go b/lib/boot/postgresql.go
index ed5aecd09..9f0de2dd6 100644
--- a/lib/boot/postgresql.go
+++ b/lib/boot/postgresql.go
@@ -36,14 +36,13 @@ func (runPostgreSQL) Run(ctx context.Context, fail func(error), boot *Booter) er
 	if err != nil {
 		return err
 	}
-	datadir := filepath.Join(boot.tempdir, "pgdata")
+	bindir := strings.TrimSpace(buf.String())
 
+	datadir := filepath.Join(boot.tempdir, "pgdata")
 	err = os.Mkdir(datadir, 0755)
 	if err != nil {
 		return err
 	}
-	bindir := strings.TrimSpace(buf.String())
-
 	err = boot.RunProgram(ctx, boot.tempdir, nil, nil, filepath.Join(bindir, "initdb"), "-D", datadir)
 	if err != nil {
 		return err

commit ce014b06b594d9d368187189ac01b41a238e54e1
Author: Tom Clegg <tom at tomclegg.ca>
Date:   Mon Feb 24 15:26:45 2020 -0500

    15954: Show passenger logs.
    
    Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tom at tomclegg.ca>

diff --git a/lib/boot/passenger.go b/lib/boot/passenger.go
index 21834dab2..10581a697 100644
--- a/lib/boot/passenger.go
+++ b/lib/boot/passenger.go
@@ -90,13 +90,27 @@ func (runner runPassenger) Run(ctx context.Context, fail func(error), boot *Boot
 	if err != nil {
 		return fmt.Errorf("bug: no InternalURLs for component %q: %v", runner, runner.svc.InternalURLs)
 	}
+	loglevel := "4"
+	if lvl, ok := map[string]string{
+		"debug":   "5",
+		"info":    "4",
+		"warn":    "2",
+		"warning": "2",
+		"error":   "1",
+		"fatal":   "0",
+		"panic":   "0",
+	}[boot.cluster.SystemLogs.LogLevel]; ok {
+		loglevel = lvl
+	}
 	boot.waitShutdown.Add(1)
 	go func() {
 		defer boot.waitShutdown.Done()
 		err = boot.RunProgram(ctx, runner.src, nil, nil, "bundle", "exec",
 			"passenger", "start",
 			"-p", port,
-			"--log-file", "/dev/null",
+			"--log-file", "/dev/stderr",
+			"--log-level", loglevel,
+			"--no-friendly-error-pages",
 			"--pid-file", filepath.Join(boot.tempdir, "passenger."+strings.Replace(runner.src, "/", "_", -1)+".pid"))
 		fail(err)
 	}()

commit 8ebed6625b925e1ae5c18b162560f37308335bad
Author: Tom Clegg <tom at tomclegg.ca>
Date:   Mon Feb 24 15:26:15 2020 -0500

    15954: Fix error reporting during shutdown.
    
    Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tom at tomclegg.ca>

diff --git a/lib/boot/cmd.go b/lib/boot/cmd.go
index bf78c42d7..62088a604 100644
--- a/lib/boot/cmd.go
+++ b/lib/boot/cmd.go
@@ -528,8 +528,12 @@ func (boot *Booter) RunProgram(ctx context.Context, dir string, output io.Writer
 		return err
 	}
 	err = cmd.Wait()
-	if err != nil && ctx.Err() == nil {
-		// Only report errors that happen before the context ends.
+	if ctx.Err() != nil {
+		// Return "context canceled", instead of the "killed"
+		// error that was probably caused by the context being
+		// canceled.
+		return ctx.Err()
+	} else if err != nil {
 		return fmt.Errorf("%s: error: %v", cmdline, err)
 	}
 	return nil

commit d89876219e668a3a97a6c61f92320bad0c0527c8
Author: Tom Clegg <tom at tomclegg.ca>
Date:   Mon Feb 24 15:24:00 2020 -0500

    15954: Fix UUID of transient SystemRootToken auth record.
    
    Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tom at tomclegg.ca>

diff --git a/services/api/app/models/api_client_authorization.rb b/services/api/app/models/api_client_authorization.rb
index 77fc0a45a..5386cb119 100644
--- a/services/api/app/models/api_client_authorization.rb
+++ b/services/api/app/models/api_client_authorization.rb
@@ -111,7 +111,7 @@ class ApiClientAuthorization < ArvadosModel
   def self.check_system_root_token token
     if token == Rails.configuration.SystemRootToken
       return ApiClientAuthorization.new(user: User.find_by_uuid(system_user_uuid),
-                                        uuid: uuid_prefix+"-gj3su-000000000000000",
+                                        uuid: Rails.configuration.ClusterID+"-gj3su-000000000000000",
                                         api_token: token,
                                         api_client: ApiClient.new(is_trusted: true, url_prefix: ""))
     else

commit b374dbf6b4ec13421570fa13ecebc6b23e19dbab
Author: Tom Clegg <tom at tomclegg.ca>
Date:   Mon Feb 24 10:42:07 2020 -0500

    15954: Trim log prefix.
    
    Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tom at tomclegg.ca>

diff --git a/lib/boot/cmd.go b/lib/boot/cmd.go
index a360a0b4e..bf78c42d7 100644
--- a/lib/boot/cmd.go
+++ b/lib/boot/cmd.go
@@ -465,7 +465,7 @@ func (boot *Booter) RunProgram(ctx context.Context, dir string, output io.Writer
 	cmdline := fmt.Sprintf("%s", append([]string{prog}, args...))
 	boot.logger.WithField("command", cmdline).WithField("dir", dir).Info("executing")
 
-	logprefix := prog
+	logprefix := strings.TrimPrefix(prog, boot.tempdir+"/bin/")
 	if prog == "bundle" && len(args) > 2 && args[0] == "exec" {
 		logprefix = args[1]
 	} else if prog == "arvados-server" && len(args) > 1 {

commit 494226025d5464d7cdef70d99094fe26feddbb4d
Author: Tom Clegg <tom at tomclegg.ca>
Date:   Mon Feb 24 10:24:28 2020 -0500

    15954: Ignore entry for own cluster ID in RemoteClusters config.
    
    Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tom at tomclegg.ca>

diff --git a/lib/controller/federation/conn.go b/lib/controller/federation/conn.go
index 42083cb83..3eca63705 100644
--- a/lib/controller/federation/conn.go
+++ b/lib/controller/federation/conn.go
@@ -35,7 +35,7 @@ func New(cluster *arvados.Cluster) *Conn {
 	local := localdb.NewConn(cluster)
 	remotes := map[string]backend{}
 	for id, remote := range cluster.RemoteClusters {
-		if !remote.Proxy {
+		if !remote.Proxy || id == cluster.ClusterID {
 			continue
 		}
 		conn := rpc.NewConn(id, &url.URL{Scheme: remote.Scheme, Host: remote.Host}, remote.Insecure, saltedTokenProvider(local, id))

commit edefa841e564018ebf99320b6596898ba3f9c63e
Author: Tom Clegg <tom at tomclegg.ca>
Date:   Mon Feb 24 10:23:53 2020 -0500

    15954: Install go binaries to tempdir.
    
    Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tom at tomclegg.ca>

diff --git a/lib/boot/cmd.go b/lib/boot/cmd.go
index 2c3103202..a360a0b4e 100644
--- a/lib/boot/cmd.go
+++ b/lib/boot/cmd.go
@@ -175,6 +175,9 @@ func (boot *Booter) run(cfg *arvados.Config) error {
 		return err
 	}
 	defer os.RemoveAll(boot.tempdir)
+	if err := os.Mkdir(filepath.Join(boot.tempdir, "bin"), 0777); err != nil {
+		return err
+	}
 
 	// Fill in any missing config keys, and write the resulting
 	// config in the temp dir for child services to use.
@@ -202,6 +205,7 @@ func (boot *Booter) run(cfg *arvados.Config) error {
 	boot.setEnv("ARVADOS_CONFIG", boot.configfile)
 	boot.setEnv("RAILS_ENV", boot.ClusterType)
 	boot.setEnv("TMPDIR", boot.tempdir)
+	boot.prependEnv("PATH", filepath.Join(boot.tempdir, "bin")+":")
 	boot.prependEnv("PATH", filepath.Join(boot.LibPath, "bin")+":")
 
 	boot.cluster, err = cfg.GetCluster("")
@@ -412,7 +416,7 @@ func dedupEnv(in []string) []string {
 func (boot *Booter) installGoProgram(ctx context.Context, srcpath string) error {
 	boot.goMutex.Lock()
 	defer boot.goMutex.Unlock()
-	return boot.RunProgram(ctx, filepath.Join(boot.SourcePath, srcpath), nil, []string{"GOPATH=" + boot.LibPath}, "go", "install")
+	return boot.RunProgram(ctx, filepath.Join(boot.SourcePath, srcpath), nil, []string{"GOBIN=" + boot.tempdir + "/bin"}, "go", "install")
 }
 
 func (boot *Booter) setupRubyEnv() error {
diff --git a/lib/boot/service.go b/lib/boot/service.go
index 4b35e1376..8cfea565a 100644
--- a/lib/boot/service.go
+++ b/lib/boot/service.go
@@ -45,7 +45,8 @@ func (runner runGoProgram) String() string {
 
 func (runner runGoProgram) Run(ctx context.Context, fail func(error), boot *Booter) error {
 	boot.wait(ctx, runner.depends...)
-	err := boot.RunProgram(ctx, runner.src, nil, nil, "go", "install")
+	bindir := filepath.Join(boot.tempdir, "bin")
+	err := boot.RunProgram(ctx, runner.src, nil, []string{"GOBIN=" + bindir}, "go", "install")
 	if err != nil {
 		return err
 	}
@@ -53,6 +54,8 @@ func (runner runGoProgram) Run(ctx context.Context, fail func(error), boot *Boot
 		return ctx.Err()
 	}
 	_, basename := filepath.Split(runner.src)
+	binfile := filepath.Join(bindir, basename)
+
 	if len(runner.svc.InternalURLs) > 0 {
 		// Run one for each URL
 		for u := range runner.svc.InternalURLs {
@@ -60,7 +63,7 @@ func (runner runGoProgram) Run(ctx context.Context, fail func(error), boot *Boot
 			boot.waitShutdown.Add(1)
 			go func() {
 				defer boot.waitShutdown.Done()
-				fail(boot.RunProgram(ctx, boot.tempdir, nil, []string{"ARVADOS_SERVICE_INTERNAL_URL=" + u.String()}, basename))
+				fail(boot.RunProgram(ctx, boot.tempdir, nil, []string{"ARVADOS_SERVICE_INTERNAL_URL=" + u.String()}, binfile))
 			}()
 		}
 	} else {
@@ -68,7 +71,7 @@ func (runner runGoProgram) Run(ctx context.Context, fail func(error), boot *Boot
 		boot.waitShutdown.Add(1)
 		go func() {
 			defer boot.waitShutdown.Done()
-			fail(boot.RunProgram(ctx, boot.tempdir, nil, nil, basename))
+			fail(boot.RunProgram(ctx, boot.tempdir, nil, nil, binfile))
 		}()
 	}
 	return nil

commit 67a252633f9e0f7e55cc9feff1964221dd818cfe
Author: Tom Clegg <tom at tomclegg.ca>
Date:   Mon Feb 24 10:17:39 2020 -0500

    15954: Logging and env var fixes.
    
    Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tom at tomclegg.ca>

diff --git a/lib/boot/cmd.go b/lib/boot/cmd.go
index 5f5bb1ee7..2c3103202 100644
--- a/lib/boot/cmd.go
+++ b/lib/boot/cmd.go
@@ -26,6 +26,7 @@ import (
 
 	"git.arvados.org/arvados.git/lib/cmd"
 	"git.arvados.org/arvados.git/lib/config"
+	"git.arvados.org/arvados.git/lib/service"
 	"git.arvados.org/arvados.git/sdk/go/arvados"
 	"git.arvados.org/arvados.git/sdk/go/ctxlog"
 	"git.arvados.org/arvados.git/sdk/go/health"
@@ -391,6 +392,23 @@ func (boot *Booter) setEnv(key, val string) {
 	boot.environ = append(boot.environ, key+"="+val)
 }
 
+// Remove all but the first occurrence of each env var.
+func dedupEnv(in []string) []string {
+	saw := map[string]bool{}
+	var out []string
+	for _, kv := range in {
+		if split := strings.Index(kv, "="); split < 1 {
+			panic("invalid environment var: " + kv)
+		} else if saw[kv[:split]] {
+			continue
+		} else {
+			saw[kv[:split]] = true
+			out = append(out, kv)
+		}
+	}
+	return out
+}
+
 func (boot *Booter) installGoProgram(ctx context.Context, srcpath string) error {
 	boot.goMutex.Lock()
 	defer boot.goMutex.Unlock()
@@ -441,11 +459,13 @@ func (boot *Booter) lookPath(prog string) string {
 // boot command's stderr.
 func (boot *Booter) RunProgram(ctx context.Context, dir string, output io.Writer, env []string, prog string, args ...string) error {
 	cmdline := fmt.Sprintf("%s", append([]string{prog}, args...))
-	fmt.Fprintf(boot.Stderr, "%s executing in %s\n", cmdline, dir)
+	boot.logger.WithField("command", cmdline).WithField("dir", dir).Info("executing")
 
 	logprefix := prog
 	if prog == "bundle" && len(args) > 2 && args[0] == "exec" {
 		logprefix = args[1]
+	} else if prog == "arvados-server" && len(args) > 1 {
+		logprefix = args[0]
 	}
 	if !strings.HasPrefix(dir, "/") {
 		logprefix = dir + ": " + logprefix
@@ -460,7 +480,7 @@ func (boot *Booter) RunProgram(ctx context.Context, dir string, output io.Writer
 	if err != nil {
 		return err
 	}
-	logwriter := &logPrefixer{Writer: boot.Stderr, Prefix: []byte("[" + logprefix + "] ")}
+	logwriter := &service.LogPrefixer{Writer: boot.Stderr, Prefix: []byte("[" + logprefix + "] ")}
 	go io.Copy(logwriter, stderr)
 	if output == nil {
 		go io.Copy(logwriter, stdout)
@@ -473,7 +493,9 @@ func (boot *Booter) RunProgram(ctx context.Context, dir string, output io.Writer
 	} else {
 		cmd.Dir = filepath.Join(boot.SourcePath, dir)
 	}
-	cmd.Env = append(env, boot.environ...)
+	env = append([]string(nil), env...)
+	env = append(env, boot.environ...)
+	cmd.Env = dedupEnv(env)
 
 	exited := false
 	defer func() { exited = true }()
diff --git a/lib/controller/integration_test.go b/lib/controller/integration_test.go
index 157998aa7..c78686ac8 100644
--- a/lib/controller/integration_test.go
+++ b/lib/controller/integration_test.go
@@ -15,6 +15,7 @@ import (
 	"git.arvados.org/arvados.git/lib/boot"
 	"git.arvados.org/arvados.git/lib/config"
 	"git.arvados.org/arvados.git/lib/controller/rpc"
+	"git.arvados.org/arvados.git/lib/service"
 	"git.arvados.org/arvados.git/sdk/go/arvados"
 	"git.arvados.org/arvados.git/sdk/go/arvadosclient"
 	"git.arvados.org/arvados.git/sdk/go/auth"
@@ -68,6 +69,8 @@ func (s *IntegrationSuite) SetUpSuite(c *check.C) {
       Insecure: true
     Login:
       LoginCluster: z1111
+    SystemLogs:
+      Format: text
     RemoteClusters:
       z1111:
         Host: localhost:` + port["z1111"] + `
@@ -96,7 +99,7 @@ func (s *IntegrationSuite) SetUpSuite(c *check.C) {
 				ListenHost:           "localhost",
 				ControllerAddr:       ":0",
 				OwnTemporaryDatabase: true,
-				Stderr:               ctxlog.LogWriter(c.Log),
+				Stderr:               &service.LogPrefixer{Writer: ctxlog.LogWriter(c.Log), Prefix: []byte("[" + id + "] ")},
 			},
 			config: *cfg,
 		}
diff --git a/lib/boot/log.go b/lib/service/log.go
similarity index 84%
rename from lib/boot/log.go
rename to lib/service/log.go
index eaaca8567..76278030b 100644
--- a/lib/boot/log.go
+++ b/lib/service/log.go
@@ -2,20 +2,20 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-package boot
+package service
 
 import (
 	"bytes"
 	"io"
 )
 
-type logPrefixer struct {
+type LogPrefixer struct {
 	io.Writer
 	Prefix []byte
 	did    bool
 }
 
-func (lp *logPrefixer) Write(p []byte) (int, error) {
+func (lp *LogPrefixer) Write(p []byte) (int, error) {
 	if len(p) == 0 {
 		return 0, nil
 	}

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


hooks/post-receive
-- 




More information about the arvados-commits mailing list