[ARVADOS] created: 1.3.0-2114-ge20ed15ea

Git user git at public.arvados.org
Tue Jan 28 19:36:28 UTC 2020


        at  e20ed15ea23390163ea262b38eff8403b7c9edf5 (commit)


commit e20ed15ea23390163ea262b38eff8403b7c9edf5
Author: Tom Clegg <tom at tomclegg.ca>
Date:   Tue Jan 28 14:35:47 2020 -0500

    15954: Fix missing return.
    
    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 d0a566d6b..6b4200a72 100644
--- a/lib/boot/cmd.go
+++ b/lib/boot/cmd.go
@@ -388,11 +388,11 @@ func (cmpt *component) Run(ctx context.Context, boot *bootCommand, stdout, stder
 				}()
 			}
 			wg.Wait()
-			return nil
 		} else {
 			// Just run one
 			boot.RunProgram(ctx, boot.tempdir, nil, nil, basename)
 		}
+		return nil
 	}
 	if cmpt.runFunc != nil {
 		return cmpt.runFunc(ctx, boot, stdout, stderr)

commit 0359ad44cb9bece93d0a9b1f131ec1fac7916d63
Author: Tom Clegg <tom at tomclegg.ca>
Date:   Tue Jan 28 11:54:17 2020 -0500

    15954: Avoid modifying caller's global environment.
    
    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 cfa6b4258..d0a566d6b 100644
--- a/lib/boot/cmd.go
+++ b/lib/boot/cmd.go
@@ -44,7 +44,9 @@ type bootCommand struct {
 	stdout  io.Writer
 	stderr  io.Writer
 
-	tempdir string
+	tempdir    string
+	configfile string
+	environ    []string // for child processes
 
 	setupRubyOnce sync.Once
 	setupRubyErr  error
@@ -127,9 +129,12 @@ func (boot *bootCommand) RunCommand(prog string, args []string, stdin io.Reader,
 	if err != nil {
 		return 1
 	}
-	os.Setenv("ARVADOS_CONFIG", conffile.Name())
-	arvados.DefaultConfigFile = conffile.Name()
-	os.Setenv("RAILS_ENV", boot.clusterType)
+	boot.configfile = conffile.Name()
+
+	boot.environ = os.Environ()
+	boot.setEnv("ARVADOS_CONFIG", boot.configfile)
+	boot.setEnv("RAILS_ENV", boot.clusterType)
+	boot.prependEnv("PATH", filepath.Join(boot.libPath, "bin")+":")
 
 	// Now that we have the config, replace the bootstrap logger
 	// with a new one according to the logging config.
@@ -146,7 +151,7 @@ func (boot *bootCommand) RunCommand(prog string, args []string, stdin io.Reader,
 	defer cancel()
 
 	ch := make(chan os.Signal)
-	signal.Notify(ch, syscall.SIGINT)
+	signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM)
 	go func() {
 		for sig := range ch {
 			logger.WithField("signal", sig).Info("caught signal")
@@ -164,12 +169,14 @@ func (boot *bootCommand) RunCommand(prog string, args []string, stdin io.Reader,
 			return 1
 		}
 	}
-	os.Setenv("PATH", filepath.Join(boot.libPath, "bin")+":"+os.Getenv("PATH"))
-
 	err = boot.installGoProgram(ctx, "cmd/arvados-server")
 	if err != nil {
 		return 1
 	}
+	err = boot.setupRubyEnv()
+	if err != nil {
+		return 1
+	}
 
 	var wg sync.WaitGroup
 	for _, cmpt := range []component{
@@ -228,6 +235,26 @@ func (boot *bootCommand) waitUntilReady(ctx context.Context) bool {
 	return true
 }
 
+func (boot *bootCommand) 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)
+}
+
+func (boot *bootCommand) 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)
+}
+
 func (boot *bootCommand) installGoProgram(ctx context.Context, srcpath string) error {
 	boot.goMutex.Lock()
 	defer boot.goMutex.Unlock()
@@ -235,17 +262,29 @@ func (boot *bootCommand) installGoProgram(ctx context.Context, srcpath string) e
 }
 
 func (boot *bootCommand) setupRubyEnv() error {
-	boot.setupRubyOnce.Do(func() {
-		buf, err := exec.Command("gem", "env", "gempath").Output() // /var/lib/arvados/.gem/ruby/2.5.0/bin:...
-		if err != nil || len(buf) == 0 {
-			boot.setupRubyErr = fmt.Errorf("gem env gempath: %v", err)
+	buf, err := exec.Command("gem", "env", "gempath").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)
+	return nil
+}
+
+func (boot *bootCommand) 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
+				}
+			}
 		}
-		gempath := string(bytes.Split(buf, []byte{':'})[0])
-		os.Setenv("PATH", gempath+"/bin:"+os.Getenv("PATH"))
-		os.Setenv("GEM_HOME", gempath)
-		os.Setenv("GEM_PATH", gempath)
-	})
-	return boot.setupRubyErr
+	}
+	return prog
 }
 
 // Run prog with args, using dir as working directory. If ctx is
@@ -259,7 +298,7 @@ func (boot *bootCommand) setupRubyEnv() error {
 func (boot *bootCommand) 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)
-	cmd := exec.Command(prog, args...)
+	cmd := exec.Command(boot.lookPath(prog), args...)
 	if output == nil {
 		cmd.Stdout = boot.stderr
 	} else {
@@ -271,9 +310,7 @@ func (boot *bootCommand) RunProgram(ctx context.Context, dir string, output io.W
 	} else {
 		cmd.Dir = filepath.Join(boot.sourcePath, dir)
 	}
-	if env != nil {
-		cmd.Env = append(env, os.Environ()...)
-	}
+	cmd.Env = append(env, boot.environ...)
 	go func() {
 		<-ctx.Done()
 		log := ctxlog.FromContext(ctx).WithFields(logrus.Fields{"dir": dir, "cmdline": cmdline})
@@ -318,7 +355,7 @@ func (cmpt *component) Run(ctx context.Context, boot *bootCommand, stdout, stder
 		errs := make(chan error, 1)
 		go func() {
 			defer close(errs)
-			exitcode := cmpt.cmdHandler.RunCommand(cmpt.name, nil, bytes.NewBuffer(nil), stdout, stderr)
+			exitcode := cmpt.cmdHandler.RunCommand(cmpt.name, []string{"-config", boot.configfile}, bytes.NewBuffer(nil), stdout, stderr)
 			if exitcode != 0 {
 				errs <- fmt.Errorf("exit code %d", exitcode)
 			}
@@ -365,10 +402,6 @@ func (cmpt *component) Run(ctx context.Context, boot *bootCommand, stdout, stder
 		if err != nil {
 			return fmt.Errorf("bug: no InternalURLs for component %q: %v", cmpt.name, cmpt.svc.InternalURLs)
 		}
-		err = boot.setupRubyEnv()
-		if err != nil {
-			return err
-		}
 		var buf bytes.Buffer
 		err = boot.RunProgram(ctx, cmpt.railsApp, &buf, nil, "gem", "list", "--details", "bundler")
 		if err != nil {

commit 071208be8ec20519c8fb05fe7ac0563e55e55b5b
Author: Tom Clegg <tom at tomclegg.ca>
Date:   Tue Jan 28 11:03:08 2020 -0500

    15954: Write controller URL to stdout when cluster is ready.
    
    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 992c7c6e5..cfa6b4258 100644
--- a/lib/boot/cmd.go
+++ b/lib/boot/cmd.go
@@ -29,6 +29,7 @@ import (
 	"git.arvados.org/arvados.git/lib/dispatchcloud"
 	"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"
 )
 
@@ -196,11 +197,37 @@ func (boot *bootCommand) RunCommand(prog string, args []string, stdin io.Reader,
 			}
 		}()
 	}
+	if boot.waitUntilReady(ctx) {
+		fmt.Fprintln(stdout, boot.cluster.Services.Controller.ExternalURL)
+	}
 	<-ctx.Done()
 	wg.Wait()
 	return 0
 }
 
+func (boot *bootCommand) waitUntilReady(ctx context.Context) bool {
+	agg := health.Aggregator{Cluster: boot.cluster}
+	for waiting := true; waiting; {
+		time.Sleep(time.Second)
+		if ctx.Err() != nil {
+			return false
+		}
+		resp := agg.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 _, check := range resp.Checks {
+			if check.Health != "OK" {
+				waiting = true
+			}
+		}
+	}
+	return true
+}
+
 func (boot *bootCommand) installGoProgram(ctx context.Context, srcpath string) error {
 	boot.goMutex.Lock()
 	defer boot.goMutex.Unlock()
diff --git a/sdk/go/health/aggregator.go b/sdk/go/health/aggregator.go
index 90823b38b..a0284e8f2 100644
--- a/sdk/go/health/aggregator.go
+++ b/sdk/go/health/aggregator.go
@@ -106,6 +106,7 @@ type ServiceHealth struct {
 }
 
 func (agg *Aggregator) ClusterHealth() ClusterHealthResponse {
+	agg.setupOnce.Do(agg.setup)
 	resp := ClusterHealthResponse{
 		Health:   "OK",
 		Checks:   make(map[string]CheckResult),

commit 3d18d409e0bfe9ed4571ae6f43191c215a692dab
Author: Tom Clegg <tom at tomclegg.ca>
Date:   Tue Jan 28 10:56:17 2020 -0500

    15954: Add comments.
    
    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 7f513310d..992c7c6e5 100644
--- a/lib/boot/cmd.go
+++ b/lib/boot/cmd.go
@@ -221,6 +221,14 @@ func (boot *bootCommand) setupRubyEnv() error {
 	return boot.setupRubyErr
 }
 
+// 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 *bootCommand) 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)
@@ -243,6 +251,7 @@ func (boot *bootCommand) RunProgram(ctx context.Context, dir string, output io.W
 		<-ctx.Done()
 		log := ctxlog.FromContext(ctx).WithFields(logrus.Fields{"dir": dir, "cmdline": cmdline})
 		for cmd.ProcessState == nil {
+			// Child hasn't exited yet
 			if cmd.Process == nil {
 				log.Infof("waiting for child process to start")
 				time.Sleep(time.Second)
@@ -431,6 +440,7 @@ func (boot *bootCommand) autofillConfig(cfg *arvados.Config, log logrus.FieldLog
 		cluster.TLS.Insecure = true
 	}
 	if boot.clusterType == "test" {
+		// Add a second keepstore process.
 		port++
 		cluster.Services.Keepstore.InternalURLs[arvados.URL{Scheme: "http", Host: fmt.Sprintf("localhost:%d", port)}] = arvados.ServiceInstance{}
 

commit d81316e711dd9966e62ac6b5444e8d2aa12edfaa
Author: Tom Clegg <tom at tomclegg.ca>
Date:   Tue Jan 28 10:51:55 2020 -0500

    15954: Fix test-mode volume config.
    
    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 633dbdb0b..7f513310d 100644
--- a/lib/boot/cmd.go
+++ b/lib/boot/cmd.go
@@ -434,23 +434,23 @@ func (boot *bootCommand) autofillConfig(cfg *arvados.Config, log logrus.FieldLog
 		port++
 		cluster.Services.Keepstore.InternalURLs[arvados.URL{Scheme: "http", Host: fmt.Sprintf("localhost:%d", port)}] = arvados.ServiceInstance{}
 
-		n := -1
+		// Create a directory-backed volume for each keepstore
+		// process.
+		cluster.Volumes = map[string]arvados.Volume{}
 		for url := range cluster.Services.Keepstore.InternalURLs {
-			n++
-			datadir := fmt.Sprintf("%s/keep%d.data", boot.tempdir, n)
+			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 = map[string]arvados.Volume{
-				fmt.Sprintf("zzzzz-nyw5e-%015d", n): arvados.Volume{
-					Driver:           "Directory",
-					DriverParameters: json.RawMessage(fmt.Sprintf(`{"Root":%q}`, datadir)),
-					AccessViaHosts: map[arvados.URL]arvados.VolumeAccess{
-						url: {},
-					},
+			cluster.Volumes[fmt.Sprintf("zzzzz-nyw5e-%015d", volnum)] = arvados.Volume{
+				Driver:           "Directory",
+				DriverParameters: json.RawMessage(fmt.Sprintf(`{"Root":%q}`, datadir)),
+				AccessViaHosts: map[arvados.URL]arvados.VolumeAccess{
+					url: {},
 				},
 			}
 		}

commit f7a22b6b70839d2fd4b49ef1ad1d96701f30ff6c
Author: Tom Clegg <tom at tomclegg.ca>
Date:   Mon Jan 27 16:10:52 2020 -0500

    15954: Eliminate intermediate go process.
    
    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 e0e357e1e..633dbdb0b 100644
--- a/lib/boot/cmd.go
+++ b/lib/boot/cmd.go
@@ -247,8 +247,9 @@ func (boot *bootCommand) RunProgram(ctx context.Context, dir string, output io.W
 				log.Infof("waiting for child process to start")
 				time.Sleep(time.Second)
 			} else {
+				log.WithField("PID", cmd.Process.Pid).Info("sending SIGTERM")
 				cmd.Process.Signal(syscall.SIGTERM)
-				log.WithField("PID", cmd.Process.Pid).Infof("waiting for child process to exit after SIGTERM")
+				log.WithField("PID", cmd.Process.Pid).Info("waiting for child process to exit after SIGTERM")
 				time.Sleep(5 * time.Second)
 			}
 		}
@@ -297,6 +298,11 @@ func (cmpt *component) Run(ctx context.Context, boot *bootCommand, stdout, stder
 		}
 	}
 	if cmpt.goProg != "" {
+		boot.RunProgram(ctx, cmpt.goProg, nil, nil, "go", "install")
+		if ctx.Err() != nil {
+			return nil
+		}
+		_, basename := filepath.Split(cmpt.goProg)
 		if len(cmpt.svc.InternalURLs) > 0 {
 			// Run one for each URL
 			var wg sync.WaitGroup
@@ -305,14 +311,14 @@ func (cmpt *component) Run(ctx context.Context, boot *bootCommand, stdout, stder
 				wg.Add(1)
 				go func() {
 					defer wg.Done()
-					boot.RunProgram(ctx, cmpt.goProg, nil, []string{"ARVADOS_SERVICE_INTERNAL_URL=" + u.String()}, "go", "run", ".")
+					boot.RunProgram(ctx, boot.tempdir, nil, []string{"ARVADOS_SERVICE_INTERNAL_URL=" + u.String()}, basename)
 				}()
 			}
 			wg.Wait()
 			return nil
 		} else {
 			// Just run one
-			return boot.RunProgram(ctx, cmpt.goProg, nil, nil, "go", "run", ".")
+			boot.RunProgram(ctx, boot.tempdir, nil, nil, basename)
 		}
 	}
 	if cmpt.runFunc != nil {

commit 570a9f8e5504d518b118952098b95f11761dda18
Author: Tom Clegg <tom at tomclegg.ca>
Date:   Mon Jan 27 16:10:22 2020 -0500

    15954: Start websocket and keepstore.
    
    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 361f64505..e0e357e1e 100644
--- a/lib/boot/cmd.go
+++ b/lib/boot/cmd.go
@@ -179,8 +179,10 @@ func (boot *bootCommand) RunCommand(prog string, args []string, stdin io.Reader,
 		{name: "health", goProg: "services/health"},
 		{name: "keep-balance", goProg: "services/keep-balance", notIfTest: true},
 		{name: "keepproxy", goProg: "services/keepproxy"},
+		{name: "keepstore", goProg: "services/keepstore", svc: boot.cluster.Services.Keepstore},
 		{name: "keep-web", goProg: "services/keep-web"},
 		{name: "railsAPI", svc: boot.cluster.Services.RailsAPI, railsApp: "services/api"},
+		{name: "ws", goProg: "services/ws"},
 	} {
 		cmpt := cmpt
 		wg.Add(1)
@@ -202,8 +204,7 @@ func (boot *bootCommand) RunCommand(prog string, args []string, stdin io.Reader,
 func (boot *bootCommand) installGoProgram(ctx context.Context, srcpath string) error {
 	boot.goMutex.Lock()
 	defer boot.goMutex.Unlock()
-	env := append([]string{"GOPATH=" + boot.libPath}, os.Environ()...)
-	return boot.RunProgram(ctx, filepath.Join(boot.sourcePath, srcpath), nil, env, "go", "install")
+	return boot.RunProgram(ctx, filepath.Join(boot.sourcePath, srcpath), nil, []string{"GOPATH=" + boot.libPath}, "go", "install")
 }
 
 func (boot *bootCommand) setupRubyEnv() error {
@@ -236,7 +237,7 @@ func (boot *bootCommand) RunProgram(ctx context.Context, dir string, output io.W
 		cmd.Dir = filepath.Join(boot.sourcePath, dir)
 	}
 	if env != nil {
-		cmd.Env = env
+		cmd.Env = append(env, os.Environ()...)
 	}
 	go func() {
 		<-ctx.Done()
@@ -296,7 +297,23 @@ func (cmpt *component) Run(ctx context.Context, boot *bootCommand, stdout, stder
 		}
 	}
 	if cmpt.goProg != "" {
-		return boot.RunProgram(ctx, cmpt.goProg, nil, nil, "go", "run", ".")
+		if len(cmpt.svc.InternalURLs) > 0 {
+			// Run one for each URL
+			var wg sync.WaitGroup
+			for u := range cmpt.svc.InternalURLs {
+				u := u
+				wg.Add(1)
+				go func() {
+					defer wg.Done()
+					boot.RunProgram(ctx, cmpt.goProg, nil, []string{"ARVADOS_SERVICE_INTERNAL_URL=" + u.String()}, "go", "run", ".")
+				}()
+			}
+			wg.Wait()
+			return nil
+		} else {
+			// Just run one
+			return boot.RunProgram(ctx, cmpt.goProg, nil, nil, "go", "run", ".")
+		}
 	}
 	if cmpt.runFunc != nil {
 		return cmpt.runFunc(ctx, boot, stdout, stderr)
@@ -407,12 +424,29 @@ func (boot *bootCommand) autofillConfig(cfg *arvados.Config, log logrus.FieldLog
 	if boot.clusterType != "production" {
 		cluster.TLS.Insecure = true
 	}
-	if boot.clusterType == "test" && len(cluster.Volumes) == 0 {
-		cluster.Volumes = map[string]arvados.Volume{
-			"zzzzz-nyw5e-000000000000000": arvados.Volume{
-				Driver:           "Directory",
-				DriverParameters: json.RawMessage(fmt.Sprintf(`{"Root":%q}`, boot.tempdir+"/keep0")),
-			},
+	if boot.clusterType == "test" {
+		port++
+		cluster.Services.Keepstore.InternalURLs[arvados.URL{Scheme: "http", Host: fmt.Sprintf("localhost:%d", port)}] = arvados.ServiceInstance{}
+
+		n := -1
+		for url := range cluster.Services.Keepstore.InternalURLs {
+			n++
+			datadir := fmt.Sprintf("%s/keep%d.data", boot.tempdir, n)
+			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 = map[string]arvados.Volume{
+				fmt.Sprintf("zzzzz-nyw5e-%015d", n): arvados.Volume{
+					Driver:           "Directory",
+					DriverParameters: json.RawMessage(fmt.Sprintf(`{"Root":%q}`, datadir)),
+					AccessViaHosts: map[arvados.URL]arvados.VolumeAccess{
+						url: {},
+					},
+				},
+			}
 		}
 	}
 	cfg.Clusters[cluster.ClusterID] = *cluster
diff --git a/lib/service/cmd.go b/lib/service/cmd.go
index f1f3fd91d..48912b889 100644
--- a/lib/service/cmd.go
+++ b/lib/service/cmd.go
@@ -12,6 +12,7 @@ import (
 	"io"
 	"net"
 	"net/http"
+	"net/url"
 	"os"
 	"strings"
 
@@ -164,6 +165,14 @@ func getListenAddr(svcs arvados.Services, prog arvados.ServiceName, log logrus.F
 	if !ok {
 		return arvados.URL{}, fmt.Errorf("unknown service name %q", prog)
 	}
+
+	if want := os.Getenv("ARVADOS_SERVICE_INTERNAL_URL"); want == "" {
+	} else if url, err := url.Parse(want); err != nil {
+		return arvados.URL{}, fmt.Errorf("$ARVADOS_SERVICE_INTERNAL_URL (%q): %s", want, err)
+	} else {
+		return arvados.URL(*url), nil
+	}
+
 	errors := []string{}
 	for url := range svc.InternalURLs {
 		listener, err := net.Listen("tcp", url.Host)

commit 59b55da81d54d287a39e24aa7d5187371bf0001b
Author: Tom Clegg <tom at tomclegg.ca>
Date:   Mon Jan 27 11:39:34 2020 -0500

    15954: Add ping check for health aggregator.
    
    Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tom at tomclegg.ca>

diff --git a/sdk/go/health/aggregator.go b/sdk/go/health/aggregator.go
index a1ef5e0be..90823b38b 100644
--- a/sdk/go/health/aggregator.go
+++ b/sdk/go/health/aggregator.go
@@ -62,11 +62,14 @@ func (agg *Aggregator) ServeHTTP(resp http.ResponseWriter, req *http.Request) {
 		sendErr(http.StatusUnauthorized, errUnauthorized)
 		return
 	}
-	if req.URL.Path != "/_health/all" {
+	if req.URL.Path == "/_health/all" {
+		json.NewEncoder(resp).Encode(agg.ClusterHealth())
+	} else if req.URL.Path == "/_health/ping" {
+		resp.Write(healthyBody)
+	} else {
 		sendErr(http.StatusNotFound, errNotFound)
 		return
 	}
-	json.NewEncoder(resp).Encode(agg.ClusterHealth())
 	if agg.Log != nil {
 		agg.Log(req, nil)
 	}

commit 1cb783ed951c9160f22680b2620c6f32b581582f
Author: Tom Clegg <tom at tomclegg.ca>
Date:   Mon Jan 27 10:22:02 2020 -0500

    15954: Don't assign ports for dispatchcloud in test mode.
    
    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 8d20e5f6c..361f64505 100644
--- a/lib/boot/cmd.go
+++ b/lib/boot/cmd.go
@@ -366,6 +366,9 @@ func (boot *bootCommand) autofillConfig(cfg *arvados.Config, log logrus.FieldLog
 		&cluster.Services.WebDAVDownload,
 		&cluster.Services.Websocket,
 	} {
+		if svc == &cluster.Services.DispatchCloud && boot.clusterType == "test" {
+			continue
+		}
 		if len(svc.InternalURLs) == 0 {
 			port++
 			svc.InternalURLs = map[arvados.URL]arvados.ServiceInstance{

commit 455040905b4e1ccbb107a1e06766043a9540b871
Author: Tom Clegg <tom at tomclegg.ca>
Date:   Mon Jan 27 10:21:06 2020 -0500

    15954: Fix wrong token expected at keepproxy health check endpoint.
    
    Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tom at tomclegg.ca>

diff --git a/services/keepproxy/keepproxy.go b/services/keepproxy/keepproxy.go
index 58e4a8534..2b15d7994 100644
--- a/services/keepproxy/keepproxy.go
+++ b/services/keepproxy/keepproxy.go
@@ -157,7 +157,7 @@ func run(logger log.FieldLogger, cluster *arvados.Cluster) error {
 	signal.Notify(term, syscall.SIGINT)
 
 	// Start serving requests.
-	router = MakeRESTRouter(kc, time.Duration(cluster.API.KeepServiceRequestTimeout), cluster.SystemRootToken)
+	router = MakeRESTRouter(kc, time.Duration(cluster.API.KeepServiceRequestTimeout), cluster.ManagementToken)
 	return http.Serve(listener, httpserver.AddRequestIDs(httpserver.LogRequests(router)))
 }
 

commit 10f4f6d203e7d7cacfe2d6620fa4515c2354c556
Author: Tom Clegg <tom at tomclegg.ca>
Date:   Fri Jan 24 15:28:28 2020 -0500

    15954: Add health/proxy/dav 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 4ac6d4a21..8d20e5f6c 100644
--- a/lib/boot/cmd.go
+++ b/lib/boot/cmd.go
@@ -39,8 +39,11 @@ type bootCommand struct {
 	libPath     string // e.g., /var/lib/arvados
 	clusterType string // e.g., production
 
-	stdout io.Writer
-	stderr io.Writer
+	cluster *arvados.Cluster
+	stdout  io.Writer
+	stderr  io.Writer
+
+	tempdir string
 
 	setupRubyOnce sync.Once
 	setupRubyErr  error
@@ -98,11 +101,11 @@ func (boot *bootCommand) RunCommand(prog string, args []string, stdin io.Reader,
 		return 1
 	}
 
-	tempdir, err := ioutil.TempDir("", "arvados-server-boot-")
+	boot.tempdir, err = ioutil.TempDir("", "arvados-server-boot-")
 	if err != nil {
 		return 1
 	}
-	defer os.RemoveAll(tempdir)
+	defer os.RemoveAll(boot.tempdir)
 
 	// Fill in any missing config keys, and write the resulting
 	// config in the temp dir for child services to use.
@@ -110,7 +113,7 @@ func (boot *bootCommand) RunCommand(prog string, args []string, stdin io.Reader,
 	if err != nil {
 		return 1
 	}
-	conffile, err := os.OpenFile(filepath.Join(tempdir, "config.yml"), os.O_CREATE|os.O_WRONLY, 0777)
+	conffile, err := os.OpenFile(filepath.Join(boot.tempdir, "config.yml"), os.O_CREATE|os.O_WRONLY, 0777)
 	if err != nil {
 		return 1
 	}
@@ -124,16 +127,16 @@ func (boot *bootCommand) RunCommand(prog string, args []string, stdin io.Reader,
 		return 1
 	}
 	os.Setenv("ARVADOS_CONFIG", conffile.Name())
-
+	arvados.DefaultConfigFile = conffile.Name()
 	os.Setenv("RAILS_ENV", boot.clusterType)
 
 	// Now that we have the config, replace the bootstrap logger
 	// with a new one according to the logging config.
-	cluster, err := cfg.GetCluster("")
+	boot.cluster, err = cfg.GetCluster("")
 	if err != nil {
 		return 1
 	}
-	log = ctxlog.New(stderr, cluster.SystemLogs.Format, cluster.SystemLogs.LogLevel)
+	log = ctxlog.New(stderr, boot.cluster.SystemLogs.Format, boot.cluster.SystemLogs.LogLevel)
 	logger := log.WithFields(logrus.Fields{
 		"PID": os.Getpid(),
 	})
@@ -169,10 +172,15 @@ func (boot *bootCommand) RunCommand(prog string, args []string, stdin io.Reader,
 
 	var wg sync.WaitGroup
 	for _, cmpt := range []component{
+		{name: "nginx", runFunc: runNginx},
 		{name: "controller", cmdHandler: controller.Command},
 		{name: "dispatchcloud", cmdHandler: dispatchcloud.Command, notIfTest: true},
+		{name: "git-httpd", goProg: "services/arv-git-httpd"},
+		{name: "health", goProg: "services/health"},
+		{name: "keep-balance", goProg: "services/keep-balance", notIfTest: true},
 		{name: "keepproxy", goProg: "services/keepproxy"},
-		{name: "railsAPI", svc: cluster.Services.RailsAPI, railsApp: "services/api"},
+		{name: "keep-web", goProg: "services/keep-web"},
+		{name: "railsAPI", svc: boot.cluster.Services.RailsAPI, railsApp: "services/api"},
 	} {
 		cmpt := cmpt
 		wg.Add(1)
@@ -233,13 +241,13 @@ func (boot *bootCommand) RunProgram(ctx context.Context, dir string, output io.W
 	go func() {
 		<-ctx.Done()
 		log := ctxlog.FromContext(ctx).WithFields(logrus.Fields{"dir": dir, "cmdline": cmdline})
-		for cmd.ProcessState != nil {
+		for cmd.ProcessState == nil {
 			if cmd.Process == nil {
 				log.Infof("waiting for child process to start")
 				time.Sleep(time.Second)
 			} else {
-				cmd.Process.Signal(syscall.SIGINT)
-				log.WithField("PID", cmd.Process.Pid).Infof("waiting for child process to exit after SIGINT")
+				cmd.Process.Signal(syscall.SIGTERM)
+				log.WithField("PID", cmd.Process.Pid).Infof("waiting for child process to exit after SIGTERM")
 				time.Sleep(5 * time.Second)
 			}
 		}
@@ -255,6 +263,7 @@ type component struct {
 	name       string
 	svc        arvados.Service
 	cmdHandler cmd.Handler
+	runFunc    func(ctx context.Context, boot *bootCommand, stdout, stderr io.Writer) error
 	railsApp   string // source dir in arvados tree, e.g., "services/api"
 	goProg     string // source dir in arvados tree, e.g., "services/keepstore"
 	notIfTest  bool   // don't run this component on a test cluster
@@ -262,7 +271,7 @@ type component struct {
 
 func (cmpt *component) Run(ctx context.Context, boot *bootCommand, stdout, stderr io.Writer) error {
 	if cmpt.notIfTest && boot.clusterType == "test" {
-		fmt.Fprintf(stderr, "skipping component %q\n", cmpt.name)
+		fmt.Fprintf(stderr, "skipping component %q in %s mode\n", cmpt.name, boot.clusterType)
 		<-ctx.Done()
 		return nil
 	}
@@ -289,25 +298,15 @@ func (cmpt *component) Run(ctx context.Context, boot *bootCommand, stdout, stder
 	if cmpt.goProg != "" {
 		return boot.RunProgram(ctx, cmpt.goProg, nil, nil, "go", "run", ".")
 	}
+	if cmpt.runFunc != nil {
+		return cmpt.runFunc(ctx, boot, stdout, stderr)
+	}
 	if cmpt.railsApp != "" {
-		port := "-"
-		for u := range cmpt.svc.InternalURLs {
-			if _, p, err := net.SplitHostPort(u.Host); err != nil {
-				return err
-			} else if p != "" {
-				port = p
-			} else if u.Scheme == "https" {
-				port = "443"
-			} else {
-				port = "80"
-			}
-			break
-		}
-		if port == "-" {
+		port, err := internalPort(cmpt.svc)
+		if err != nil {
 			return fmt.Errorf("bug: no InternalURLs for component %q: %v", cmpt.name, cmpt.svc.InternalURLs)
 		}
-
-		err := boot.setupRubyEnv()
+		err = boot.setupRubyEnv()
 		if err != nil {
 			return err
 		}
@@ -358,7 +357,14 @@ func (boot *bootCommand) autofillConfig(cfg *arvados.Config, log logrus.FieldLog
 	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,
 	} {
 		if len(svc.InternalURLs) == 0 {
 			port++
@@ -366,15 +372,22 @@ func (boot *bootCommand) autofillConfig(cfg *arvados.Config, log logrus.FieldLog
 				arvados.URL{Scheme: "http", Host: fmt.Sprintf("localhost:%d", port)}: arvados.ServiceInstance{},
 			}
 		}
-	}
-	if cluster.Services.Controller.ExternalURL.Host == "" {
-		for k := range cluster.Services.Controller.InternalURLs {
-			cluster.Services.Controller.ExternalURL = k
+		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) {
+			port++
+			svc.ExternalURL = arvados.URL{Scheme: "https", Host: fmt.Sprintf("localhost:%d", port)}
 		}
 	}
 	if cluster.SystemRootToken == "" {
 		cluster.SystemRootToken = randomHexString(64)
 	}
+	if cluster.ManagementToken == "" {
+		cluster.ManagementToken = randomHexString(64)
+	}
 	if cluster.API.RailsSessionSecretToken == "" {
 		cluster.API.RailsSessionSecretToken = randomHexString(64)
 	}
@@ -388,6 +401,17 @@ func (boot *bootCommand) autofillConfig(cfg *arvados.Config, log logrus.FieldLog
 		}
 		cluster.Containers.DispatchPrivateKey = string(buf)
 	}
+	if boot.clusterType != "production" {
+		cluster.TLS.Insecure = true
+	}
+	if boot.clusterType == "test" && len(cluster.Volumes) == 0 {
+		cluster.Volumes = map[string]arvados.Volume{
+			"zzzzz-nyw5e-000000000000000": arvados.Volume{
+				Driver:           "Directory",
+				DriverParameters: json.RawMessage(fmt.Sprintf(`{"Root":%q}`, boot.tempdir+"/keep0")),
+			},
+		}
+	}
 	cfg.Clusters[cluster.ClusterID] = *cluster
 	return nil
 }
@@ -400,3 +424,30 @@ func randomHexString(chars int) string {
 	}
 	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
+	}
+}
diff --git a/lib/boot/nginx.go b/lib/boot/nginx.go
new file mode 100644
index 000000000..11d823fc0
--- /dev/null
+++ b/lib/boot/nginx.go
@@ -0,0 +1,77 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+package boot
+
+import (
+	"context"
+	"fmt"
+	"io"
+	"io/ioutil"
+	"os"
+	"os/exec"
+	"path/filepath"
+	"regexp"
+
+	"git.arvados.org/arvados.git/sdk/go/arvados"
+)
+
+func runNginx(ctx context.Context, boot *bootCommand, stdout, stderr io.Writer) error {
+	vars := map[string]string{
+		"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,
+	}
+	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},
+		{"WS", boot.cluster.Services.Websocket},
+	} {
+		vars[cmpt.varname+"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 err != nil {
+			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"))
+	if err != nil {
+		return err
+	}
+	conf := regexp.MustCompile(`{{.*?}}`).ReplaceAllStringFunc(string(tmpl), func(src string) string {
+		if len(src) < 4 {
+			return src
+		}
+		return vars[src[2:len(src)-2]]
+	})
+	conffile := filepath.Join(boot.tempdir, "nginx.conf")
+	err = ioutil.WriteFile(conffile, []byte(conf), 0755)
+	if err != nil {
+		return err
+	}
+	nginx := "nginx"
+	if _, err := exec.LookPath(nginx); err != nil {
+		for _, dir := range []string{"/sbin", "/usr/sbin", "/usr/local/sbin"} {
+			if _, err = os.Stat(dir + "/nginx"); err == nil {
+				nginx = dir + "/nginx"
+				break
+			}
+		}
+	}
+	return boot.RunProgram(ctx, ".", nil, nil, nginx,
+		"-g", "error_log stderr info;",
+		"-g", "pid "+filepath.Join(boot.tempdir, "nginx.pid")+";",
+		"-c", conffile)
+}
diff --git a/sdk/python/tests/nginx.conf b/sdk/python/tests/nginx.conf
index 6010ee4bf..e9be12235 100644
--- a/sdk/python/tests/nginx.conf
+++ b/sdk/python/tests/nginx.conf
@@ -92,7 +92,7 @@ http {
     server localhost:{{WSPORT}};
   }
   server {
-    listen *:{{WSSPORT}} ssl default_server;
+    listen *:{{WSSSLPORT}} ssl default_server;
     server_name websocket;
     ssl_certificate "{{SSLCERT}}";
     ssl_certificate_key "{{SSLKEY}}";
diff --git a/sdk/python/tests/run_test_server.py b/sdk/python/tests/run_test_server.py
index 9e9b12f98..5c05c124c 100644
--- a/sdk/python/tests/run_test_server.py
+++ b/sdk/python/tests/run_test_server.py
@@ -616,7 +616,7 @@ def run_nginx():
     nginxconf['GITPORT'] = internal_port_from_config("GitHTTP")
     nginxconf['GITSSLPORT'] = external_port_from_config("GitHTTP")
     nginxconf['WSPORT'] = internal_port_from_config("Websocket")
-    nginxconf['WSSPORT'] = external_port_from_config("Websocket")
+    nginxconf['WSSSLPORT'] = external_port_from_config("Websocket")
     nginxconf['SSLCERT'] = os.path.join(SERVICES_SRC_DIR, 'api', 'tmp', 'self-signed.pem')
     nginxconf['SSLKEY'] = os.path.join(SERVICES_SRC_DIR, 'api', 'tmp', 'self-signed.key')
     nginxconf['ACCESSLOG'] = _logfilename('nginx_access')

commit 11c6d08cdb0bb78a7144d229ee6b884ae0618b15
Author: Tom Clegg <tom at tomclegg.ca>
Date:   Mon Jan 27 10:10:31 2020 -0500

    15954: Use test suite secrets in test config.
    
    Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tom at tomclegg.ca>

diff --git a/doc/examples/config/zzzzz.yml b/doc/examples/config/zzzzz.yml
index 16d449cf4..9e3d718ed 100644
--- a/doc/examples/config/zzzzz.yml
+++ b/doc/examples/config/zzzzz.yml
@@ -7,3 +7,13 @@ Clusters:
         dbname: arvados_test
         user: arvados
         password: insecure_arvados_test
+    ManagementToken: e687950a23c3a9bceec28c6223a06c79
+    SystemRootToken: systemusertesttoken1234567890aoeuidhtnsqjkxbmwvzpy
+    API:
+      RequestTimeout: 30s
+    TLS:
+      Insecure: true
+    Collections:
+      BlobSigningKey: zfhgfenhffzltr9dixws36j1yhksjoll2grmku38mi7yxd66h5j4q9w4jzanezacp8s6q0ro3hxakfye02152hncy6zml2ed0uc
+      TrustAllContent: true
+      ForwardSlashNameSubstitution: /

commit b532d3105faf4325b47e7bc8ccefd384f960574e
Author: Tom Clegg <tom at tomclegg.ca>
Date:   Fri Jan 24 15:07:32 2020 -0500

    15954: Remove unused cmdArgs.
    
    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 a8aa114de..4ac6d4a21 100644
--- a/lib/boot/cmd.go
+++ b/lib/boot/cmd.go
@@ -169,8 +169,8 @@ func (boot *bootCommand) RunCommand(prog string, args []string, stdin io.Reader,
 
 	var wg sync.WaitGroup
 	for _, cmpt := range []component{
-		{name: "controller", cmdArgs: []string{"-config", conffile.Name()}, cmdHandler: controller.Command},
-		{name: "dispatchcloud", cmdArgs: []string{"-config", conffile.Name()}, cmdHandler: dispatchcloud.Command, notIfTest: true},
+		{name: "controller", cmdHandler: controller.Command},
+		{name: "dispatchcloud", cmdHandler: dispatchcloud.Command, notIfTest: true},
 		{name: "keepproxy", goProg: "services/keepproxy"},
 		{name: "railsAPI", svc: cluster.Services.RailsAPI, railsApp: "services/api"},
 	} {
@@ -255,7 +255,6 @@ type component struct {
 	name       string
 	svc        arvados.Service
 	cmdHandler cmd.Handler
-	cmdArgs    []string
 	railsApp   string // source dir in arvados tree, e.g., "services/api"
 	goProg     string // source dir in arvados tree, e.g., "services/keepstore"
 	notIfTest  bool   // don't run this component on a test cluster
@@ -272,7 +271,7 @@ func (cmpt *component) Run(ctx context.Context, boot *bootCommand, stdout, stder
 		errs := make(chan error, 1)
 		go func() {
 			defer close(errs)
-			exitcode := cmpt.cmdHandler.RunCommand(cmpt.name, cmpt.cmdArgs, bytes.NewBuffer(nil), stdout, stderr)
+			exitcode := cmpt.cmdHandler.RunCommand(cmpt.name, nil, bytes.NewBuffer(nil), stdout, stderr)
 			if exitcode != 0 {
 				errs <- fmt.Errorf("exit code %d", exitcode)
 			}

commit 56cc681c50a1463d0128e9e07b5e55265727b567
Author: Tom Clegg <tom at tomclegg.ca>
Date:   Fri Jan 24 15:06:29 2020 -0500

    15954: Fix process start/stop race.
    
    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 de46ee674..a8aa114de 100644
--- a/lib/boot/cmd.go
+++ b/lib/boot/cmd.go
@@ -232,13 +232,16 @@ func (boot *bootCommand) RunProgram(ctx context.Context, dir string, output io.W
 	}
 	go func() {
 		<-ctx.Done()
-		cmd.Process.Signal(syscall.SIGINT)
-		for range time.Tick(5 * time.Second) {
-			if cmd.ProcessState != nil {
-				break
+		log := ctxlog.FromContext(ctx).WithFields(logrus.Fields{"dir": dir, "cmdline": cmdline})
+		for cmd.ProcessState != nil {
+			if cmd.Process == nil {
+				log.Infof("waiting for child process to start")
+				time.Sleep(time.Second)
+			} else {
+				cmd.Process.Signal(syscall.SIGINT)
+				log.WithField("PID", cmd.Process.Pid).Infof("waiting for child process to exit after SIGINT")
+				time.Sleep(5 * time.Second)
 			}
-			ctxlog.FromContext(ctx).WithField("process", cmd.Process).Infof("waiting for child process to exit after SIGINT")
-			cmd.Process.Signal(syscall.SIGINT)
 		}
 	}()
 	err := cmd.Run()

commit 0d97999fe3429b31031a09b563e39bf83fefad16
Author: Tom Clegg <tom at tomclegg.ca>
Date:   Fri Jan 24 14:45:35 2020 -0500

    15954: Run dispatchcloud and keepproxy.
    
    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 6762446b1..de46ee674 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/controller"
+	"git.arvados.org/arvados.git/lib/dispatchcloud"
 	"git.arvados.org/arvados.git/sdk/go/arvados"
 	"git.arvados.org/arvados.git/sdk/go/ctxlog"
 	"github.com/sirupsen/logrus"
@@ -105,7 +106,10 @@ func (boot *bootCommand) RunCommand(prog string, args []string, stdin io.Reader,
 
 	// Fill in any missing config keys, and write the resulting
 	// config in the temp dir for child services to use.
-	autofillConfig(cfg, log)
+	err = boot.autofillConfig(cfg, log)
+	if err != nil {
+		return 1
+	}
 	conffile, err := os.OpenFile(filepath.Join(tempdir, "config.yml"), os.O_CREATE|os.O_WRONLY, 0777)
 	if err != nil {
 		return 1
@@ -165,9 +169,10 @@ func (boot *bootCommand) RunCommand(prog string, args []string, stdin io.Reader,
 
 	var wg sync.WaitGroup
 	for _, cmpt := range []component{
-		{name: "controller", svc: cluster.Services.Controller, cmdArgs: []string{"-config", conffile.Name()}, cmdHandler: controller.Command},
-		// {name: "dispatchcloud", cmdArgs: []string{"-config", conffile.Name()}, cmdHandler: dispatchcloud.Command},
-		{name: "railsAPI", svc: cluster.Services.RailsAPI, src: "services/api"},
+		{name: "controller", cmdArgs: []string{"-config", conffile.Name()}, cmdHandler: controller.Command},
+		{name: "dispatchcloud", cmdArgs: []string{"-config", conffile.Name()}, cmdHandler: dispatchcloud.Command, notIfTest: true},
+		{name: "keepproxy", goProg: "services/keepproxy"},
+		{name: "railsAPI", svc: cluster.Services.RailsAPI, railsApp: "services/api"},
 	} {
 		cmpt := cmpt
 		wg.Add(1)
@@ -248,10 +253,17 @@ type component struct {
 	svc        arvados.Service
 	cmdHandler cmd.Handler
 	cmdArgs    []string
-	src        string // source dir in arvados tree, e.g., "services/keepstore"
+	railsApp   string // source dir in arvados tree, e.g., "services/api"
+	goProg     string // source dir in arvados tree, e.g., "services/keepstore"
+	notIfTest  bool   // don't run this component on a test cluster
 }
 
 func (cmpt *component) Run(ctx context.Context, boot *bootCommand, stdout, stderr io.Writer) error {
+	if cmpt.notIfTest && boot.clusterType == "test" {
+		fmt.Fprintf(stderr, "skipping component %q\n", cmpt.name)
+		<-ctx.Done()
+		return nil
+	}
 	fmt.Fprintf(stderr, "starting component %q\n", cmpt.name)
 	if cmpt.cmdHandler != nil {
 		errs := make(chan error, 1)
@@ -272,7 +284,10 @@ func (cmpt *component) Run(ctx context.Context, boot *bootCommand, stdout, stder
 			return nil
 		}
 	}
-	if cmpt.src != "" {
+	if cmpt.goProg != "" {
+		return boot.RunProgram(ctx, cmpt.goProg, nil, nil, "go", "run", ".")
+	}
+	if cmpt.railsApp != "" {
 		port := "-"
 		for u := range cmpt.svc.InternalURLs {
 			if _, p, err := net.SplitHostPort(u.Host); err != nil {
@@ -295,36 +310,36 @@ func (cmpt *component) Run(ctx context.Context, boot *bootCommand, stdout, stder
 			return err
 		}
 		var buf bytes.Buffer
-		err = boot.RunProgram(ctx, cmpt.src, &buf, nil, "gem", "list", "--details", "bundler")
+		err = boot.RunProgram(ctx, cmpt.railsApp, &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, cmpt.src, nil, nil, "gem", "install", "--user", "bundler:1.11", "bundler:1.17.3", "bundler:2.0.2")
+				err = boot.RunProgram(ctx, cmpt.railsApp, 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, cmpt.src, nil, nil, "bundle", "install", "--jobs", "4", "--path", filepath.Join(os.Getenv("HOME"), ".gem"))
+		err = boot.RunProgram(ctx, cmpt.railsApp, nil, nil, "bundle", "install", "--jobs", "4", "--path", filepath.Join(os.Getenv("HOME"), ".gem"))
 		if err != nil {
 			return err
 		}
-		err = boot.RunProgram(ctx, cmpt.src, nil, nil, "bundle", "exec", "passenger-config", "build-native-support")
+		err = boot.RunProgram(ctx, cmpt.railsApp, nil, nil, "bundle", "exec", "passenger-config", "build-native-support")
 		if err != nil {
 			return err
 		}
-		err = boot.RunProgram(ctx, cmpt.src, nil, nil, "bundle", "exec", "passenger-config", "install-standalone-runtime")
+		err = boot.RunProgram(ctx, cmpt.railsApp, nil, nil, "bundle", "exec", "passenger-config", "install-standalone-runtime")
 		if err != nil {
 			return err
 		}
-		err = boot.RunProgram(ctx, cmpt.src, nil, nil, "bundle", "exec", "passenger-config", "validate-install")
+		err = boot.RunProgram(ctx, cmpt.railsApp, nil, nil, "bundle", "exec", "passenger-config", "validate-install")
 		if err != nil {
 			return err
 		}
-		err = boot.RunProgram(ctx, cmpt.src, nil, nil, "bundle", "exec", "passenger", "start", "-p", port)
+		err = boot.RunProgram(ctx, cmpt.railsApp, nil, nil, "bundle", "exec", "passenger", "start", "-p", port)
 		if err != nil {
 			return err
 		}
@@ -332,10 +347,10 @@ func (cmpt *component) Run(ctx context.Context, boot *bootCommand, stdout, stder
 	return fmt.Errorf("bug: component %q has nothing to run", cmpt.name)
 }
 
-func autofillConfig(cfg *arvados.Config, log logrus.FieldLogger) {
+func (boot *bootCommand) autofillConfig(cfg *arvados.Config, log logrus.FieldLogger) error {
 	cluster, err := cfg.GetCluster("")
 	if err != nil {
-		panic(err)
+		return err
 	}
 	port := 9000
 	for _, svc := range []*arvados.Service{
@@ -364,7 +379,15 @@ func 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 err != nil {
+			return err
+		}
+		cluster.Containers.DispatchPrivateKey = string(buf)
+	}
 	cfg.Clusters[cluster.ClusterID] = *cluster
+	return nil
 }
 
 func randomHexString(chars int) string {
diff --git a/lib/config/config.default.yml b/lib/config/config.default.yml
index 50fee746b..91bb8550e 100644
--- a/lib/config/config.default.yml
+++ b/lib/config/config.default.yml
@@ -623,7 +623,7 @@ Clusters:
       # (experimental) cloud dispatcher for executing containers on
       # worker VMs. Begins with "-----BEGIN RSA PRIVATE KEY-----\n"
       # and ends with "\n-----END RSA PRIVATE KEY-----\n".
-      DispatchPrivateKey: none
+      DispatchPrivateKey: ""
 
       # Maximum time to wait for workers to come up before abandoning
       # stale locks from a previous dispatch process.
diff --git a/lib/config/generated_config.go b/lib/config/generated_config.go
index 2ee602507..eb5cc47f2 100644
--- a/lib/config/generated_config.go
+++ b/lib/config/generated_config.go
@@ -629,7 +629,7 @@ Clusters:
       # (experimental) cloud dispatcher for executing containers on
       # worker VMs. Begins with "-----BEGIN RSA PRIVATE KEY-----\n"
       # and ends with "\n-----END RSA PRIVATE KEY-----\n".
-      DispatchPrivateKey: none
+      DispatchPrivateKey: ""
 
       # Maximum time to wait for workers to come up before abandoning
       # stale locks from a previous dispatch process.

commit b4bc18e9418dc02d126ea949e4405f9f40bf043a
Author: Tom Clegg <tom at tomclegg.ca>
Date:   Fri Jan 24 14:02:00 2020 -0500

    15954: Try harder to shut down children before exiting.
    
    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 f2805ecf0..6762446b1 100644
--- a/lib/boot/cmd.go
+++ b/lib/boot/cmd.go
@@ -21,6 +21,7 @@ import (
 	"strings"
 	"sync"
 	"syscall"
+	"time"
 
 	"git.arvados.org/arvados.git/lib/cmd"
 	"git.arvados.org/arvados.git/lib/config"
@@ -161,22 +162,27 @@ func (boot *bootCommand) RunCommand(prog string, args []string, stdin io.Reader,
 	if err != nil {
 		return 1
 	}
+
+	var wg sync.WaitGroup
 	for _, cmpt := range []component{
 		{name: "controller", svc: cluster.Services.Controller, cmdArgs: []string{"-config", conffile.Name()}, cmdHandler: controller.Command},
 		// {name: "dispatchcloud", cmdArgs: []string{"-config", conffile.Name()}, cmdHandler: dispatchcloud.Command},
 		{name: "railsAPI", svc: cluster.Services.RailsAPI, src: "services/api"},
 	} {
 		cmpt := cmpt
+		wg.Add(1)
 		go func() {
+			defer wg.Done()
+			defer cancel()
 			logger.WithField("component", cmpt.name).Info("starting")
 			err := cmpt.Run(ctx, boot, stdout, stderr)
 			if err != nil {
 				logger.WithError(err).WithField("component", cmpt.name).Info("exited")
 			}
-			cancel()
 		}()
 	}
 	<-ctx.Done()
+	wg.Wait()
 	return 0
 }
 
@@ -222,6 +228,13 @@ func (boot *bootCommand) RunProgram(ctx context.Context, dir string, output io.W
 	go func() {
 		<-ctx.Done()
 		cmd.Process.Signal(syscall.SIGINT)
+		for range time.Tick(5 * time.Second) {
+			if cmd.ProcessState != nil {
+				break
+			}
+			ctxlog.FromContext(ctx).WithField("process", cmd.Process).Infof("waiting for child process to exit after SIGINT")
+			cmd.Process.Signal(syscall.SIGINT)
+		}
 	}()
 	err := cmd.Run()
 	if err != nil {
@@ -241,11 +254,23 @@ type component struct {
 func (cmpt *component) Run(ctx context.Context, boot *bootCommand, stdout, stderr io.Writer) error {
 	fmt.Fprintf(stderr, "starting component %q\n", cmpt.name)
 	if cmpt.cmdHandler != nil {
-		exitcode := cmpt.cmdHandler.RunCommand(cmpt.name, cmpt.cmdArgs, bytes.NewBuffer(nil), stdout, stderr)
-		if exitcode != 0 {
-			return fmt.Errorf("exit code %d", exitcode)
+		errs := make(chan error, 1)
+		go func() {
+			defer close(errs)
+			exitcode := cmpt.cmdHandler.RunCommand(cmpt.name, cmpt.cmdArgs, bytes.NewBuffer(nil), stdout, stderr)
+			if exitcode != 0 {
+				errs <- fmt.Errorf("exit code %d", exitcode)
+			}
+		}()
+		select {
+		case err := <-errs:
+			return err
+		case <-ctx.Done():
+			// cmpt.cmdHandler.RunCommand() doesn't have
+			// access to our context, so it won't shut
+			// down by itself. We just abandon it.
+			return nil
 		}
-		return nil
 	}
 	if cmpt.src != "" {
 		port := "-"

commit d0991b80b5b0b9a2169624a225ae6892ebe496d4
Author: Tom Clegg <tom at tomclegg.ca>
Date:   Fri Jan 24 13:37:07 2020 -0500

    15954: boot RailsAPI and controller.
    
    Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tom at tomclegg.ca>

diff --git a/cmd/arvados-server/cmd.go b/cmd/arvados-server/cmd.go
index d93a8e78f..a9d927d87 100644
--- a/cmd/arvados-server/cmd.go
+++ b/cmd/arvados-server/cmd.go
@@ -7,6 +7,7 @@ package main
 import (
 	"os"
 
+	"git.arvados.org/arvados.git/lib/boot"
 	"git.arvados.org/arvados.git/lib/cloud/cloudtest"
 	"git.arvados.org/arvados.git/lib/cmd"
 	"git.arvados.org/arvados.git/lib/config"
@@ -21,6 +22,7 @@ var (
 		"-version":  cmd.Version,
 		"--version": cmd.Version,
 
+		"boot":            boot.Command,
 		"cloudtest":       cloudtest.Command,
 		"config-check":    config.CheckCommand,
 		"config-dump":     config.DumpCommand,
diff --git a/doc/examples/config/zzzzz.yml b/doc/examples/config/zzzzz.yml
new file mode 100644
index 000000000..16d449cf4
--- /dev/null
+++ b/doc/examples/config/zzzzz.yml
@@ -0,0 +1,9 @@
+Clusters:
+  zzzzz:
+    PostgreSQL:
+      Connection:
+        client_encoding: utf8
+        host: localhost
+        dbname: arvados_test
+        user: arvados
+        password: insecure_arvados_test
diff --git a/go.mod b/go.mod
index 033723d23..2e16e5a0f 100644
--- a/go.mod
+++ b/go.mod
@@ -50,7 +50,7 @@ require (
 	golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2
 	golang.org/x/net v0.0.0-20190613194153-d28f0bde5980
 	golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45
-	golang.org/x/sys v0.0.0-20191105231009-c1f44814a5cd // indirect
+	golang.org/x/sys v0.0.0-20191105231009-c1f44814a5cd
 	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/lib/boot/cmd.go b/lib/boot/cmd.go
new file mode 100644
index 000000000..f2805ecf0
--- /dev/null
+++ b/lib/boot/cmd.go
@@ -0,0 +1,352 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+package boot
+
+import (
+	"bytes"
+	"context"
+	"crypto/rand"
+	"encoding/json"
+	"flag"
+	"fmt"
+	"io"
+	"io/ioutil"
+	"net"
+	"os"
+	"os/exec"
+	"os/signal"
+	"path/filepath"
+	"strings"
+	"sync"
+	"syscall"
+
+	"git.arvados.org/arvados.git/lib/cmd"
+	"git.arvados.org/arvados.git/lib/config"
+	"git.arvados.org/arvados.git/lib/controller"
+	"git.arvados.org/arvados.git/sdk/go/arvados"
+	"git.arvados.org/arvados.git/sdk/go/ctxlog"
+	"github.com/sirupsen/logrus"
+)
+
+var Command cmd.Handler = &bootCommand{}
+
+type bootCommand struct {
+	sourcePath  string // e.g., /home/username/src/arvados
+	libPath     string // e.g., /var/lib/arvados
+	clusterType string // e.g., production
+
+	stdout io.Writer
+	stderr io.Writer
+
+	setupRubyOnce sync.Once
+	setupRubyErr  error
+	goMutex       sync.Mutex
+}
+
+func (boot *bootCommand) RunCommand(prog string, args []string, stdin io.Reader, stdout, stderr io.Writer) int {
+	boot.stdout = stdout
+	boot.stderr = stderr
+	log := ctxlog.New(stderr, "json", "info")
+
+	var err error
+	defer func() {
+		if err != nil {
+			log.WithError(err).Info("exiting")
+		}
+	}()
+
+	flags := flag.NewFlagSet(prog, flag.ContinueOnError)
+	flags.SetOutput(stderr)
+	loader := config.NewLoader(stdin, log)
+	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")
+	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
+	}
+
+	cwd, err := os.Getwd()
+	if err != nil {
+		return 1
+	}
+	if !strings.HasPrefix(boot.sourcePath, "/") {
+		boot.sourcePath = filepath.Join(cwd, boot.sourcePath)
+	}
+	boot.sourcePath, err = filepath.EvalSymlinks(boot.sourcePath)
+	if err != nil {
+		return 1
+	}
+
+	loader.SkipAPICalls = true
+	cfg, err := loader.Load()
+	if err != nil {
+		return 1
+	}
+
+	tempdir, err := ioutil.TempDir("", "arvados-server-boot-")
+	if err != nil {
+		return 1
+	}
+	defer os.RemoveAll(tempdir)
+
+	// Fill in any missing config keys, and write the resulting
+	// config in the temp dir for child services to use.
+	autofillConfig(cfg, log)
+	conffile, err := os.OpenFile(filepath.Join(tempdir, "config.yml"), os.O_CREATE|os.O_WRONLY, 0777)
+	if err != nil {
+		return 1
+	}
+	defer conffile.Close()
+	err = json.NewEncoder(conffile).Encode(cfg)
+	if err != nil {
+		return 1
+	}
+	err = conffile.Close()
+	if err != nil {
+		return 1
+	}
+	os.Setenv("ARVADOS_CONFIG", conffile.Name())
+
+	os.Setenv("RAILS_ENV", boot.clusterType)
+
+	// Now that we have the config, replace the bootstrap logger
+	// with a new one according to the logging config.
+	cluster, err := cfg.GetCluster("")
+	if err != nil {
+		return 1
+	}
+	log = ctxlog.New(stderr, cluster.SystemLogs.Format, cluster.SystemLogs.LogLevel)
+	logger := log.WithFields(logrus.Fields{
+		"PID": os.Getpid(),
+	})
+	ctx := ctxlog.Context(context.Background(), logger)
+	ctx, cancel := context.WithCancel(ctx)
+	defer cancel()
+
+	ch := make(chan os.Signal)
+	signal.Notify(ch, syscall.SIGINT)
+	go func() {
+		for sig := range ch {
+			logger.WithField("signal", sig).Info("caught signal")
+			cancel()
+		}
+	}()
+
+	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 1
+			}
+		} else if err != nil {
+			return 1
+		}
+	}
+	os.Setenv("PATH", filepath.Join(boot.libPath, "bin")+":"+os.Getenv("PATH"))
+
+	err = boot.installGoProgram(ctx, "cmd/arvados-server")
+	if err != nil {
+		return 1
+	}
+	for _, cmpt := range []component{
+		{name: "controller", svc: cluster.Services.Controller, cmdArgs: []string{"-config", conffile.Name()}, cmdHandler: controller.Command},
+		// {name: "dispatchcloud", cmdArgs: []string{"-config", conffile.Name()}, cmdHandler: dispatchcloud.Command},
+		{name: "railsAPI", svc: cluster.Services.RailsAPI, src: "services/api"},
+	} {
+		cmpt := cmpt
+		go func() {
+			logger.WithField("component", cmpt.name).Info("starting")
+			err := cmpt.Run(ctx, boot, stdout, stderr)
+			if err != nil {
+				logger.WithError(err).WithField("component", cmpt.name).Info("exited")
+			}
+			cancel()
+		}()
+	}
+	<-ctx.Done()
+	return 0
+}
+
+func (boot *bootCommand) installGoProgram(ctx context.Context, srcpath string) error {
+	boot.goMutex.Lock()
+	defer boot.goMutex.Unlock()
+	env := append([]string{"GOPATH=" + boot.libPath}, os.Environ()...)
+	return boot.RunProgram(ctx, filepath.Join(boot.sourcePath, srcpath), nil, env, "go", "install")
+}
+
+func (boot *bootCommand) setupRubyEnv() error {
+	boot.setupRubyOnce.Do(func() {
+		buf, err := exec.Command("gem", "env", "gempath").Output() // /var/lib/arvados/.gem/ruby/2.5.0/bin:...
+		if err != nil || len(buf) == 0 {
+			boot.setupRubyErr = fmt.Errorf("gem env gempath: %v", err)
+		}
+		gempath := string(bytes.Split(buf, []byte{':'})[0])
+		os.Setenv("PATH", gempath+"/bin:"+os.Getenv("PATH"))
+		os.Setenv("GEM_HOME", gempath)
+		os.Setenv("GEM_PATH", gempath)
+	})
+	return boot.setupRubyErr
+}
+
+func (boot *bootCommand) 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)
+	cmd := exec.Command(prog, args...)
+	if output == nil {
+		cmd.Stdout = boot.stderr
+	} else {
+		cmd.Stdout = output
+	}
+	cmd.Stderr = boot.stderr
+	if strings.HasPrefix(dir, "/") {
+		cmd.Dir = dir
+	} else {
+		cmd.Dir = filepath.Join(boot.sourcePath, dir)
+	}
+	if env != nil {
+		cmd.Env = env
+	}
+	go func() {
+		<-ctx.Done()
+		cmd.Process.Signal(syscall.SIGINT)
+	}()
+	err := cmd.Run()
+	if err != nil {
+		return fmt.Errorf("%s: error: %v", cmdline, err)
+	}
+	return nil
+}
+
+type component struct {
+	name       string
+	svc        arvados.Service
+	cmdHandler cmd.Handler
+	cmdArgs    []string
+	src        string // source dir in arvados tree, e.g., "services/keepstore"
+}
+
+func (cmpt *component) Run(ctx context.Context, boot *bootCommand, stdout, stderr io.Writer) error {
+	fmt.Fprintf(stderr, "starting component %q\n", cmpt.name)
+	if cmpt.cmdHandler != nil {
+		exitcode := cmpt.cmdHandler.RunCommand(cmpt.name, cmpt.cmdArgs, bytes.NewBuffer(nil), stdout, stderr)
+		if exitcode != 0 {
+			return fmt.Errorf("exit code %d", exitcode)
+		}
+		return nil
+	}
+	if cmpt.src != "" {
+		port := "-"
+		for u := range cmpt.svc.InternalURLs {
+			if _, p, err := net.SplitHostPort(u.Host); err != nil {
+				return err
+			} else if p != "" {
+				port = p
+			} else if u.Scheme == "https" {
+				port = "443"
+			} else {
+				port = "80"
+			}
+			break
+		}
+		if port == "-" {
+			return fmt.Errorf("bug: no InternalURLs for component %q: %v", cmpt.name, cmpt.svc.InternalURLs)
+		}
+
+		err := boot.setupRubyEnv()
+		if err != nil {
+			return err
+		}
+		var buf bytes.Buffer
+		err = boot.RunProgram(ctx, cmpt.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, cmpt.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, cmpt.src, nil, nil, "bundle", "install", "--jobs", "4", "--path", filepath.Join(os.Getenv("HOME"), ".gem"))
+		if err != nil {
+			return err
+		}
+		err = boot.RunProgram(ctx, cmpt.src, nil, nil, "bundle", "exec", "passenger-config", "build-native-support")
+		if err != nil {
+			return err
+		}
+		err = boot.RunProgram(ctx, cmpt.src, nil, nil, "bundle", "exec", "passenger-config", "install-standalone-runtime")
+		if err != nil {
+			return err
+		}
+		err = boot.RunProgram(ctx, cmpt.src, nil, nil, "bundle", "exec", "passenger-config", "validate-install")
+		if err != nil {
+			return err
+		}
+		err = boot.RunProgram(ctx, cmpt.src, nil, nil, "bundle", "exec", "passenger", "start", "-p", port)
+		if err != nil {
+			return err
+		}
+	}
+	return fmt.Errorf("bug: component %q has nothing to run", cmpt.name)
+}
+
+func autofillConfig(cfg *arvados.Config, log logrus.FieldLogger) {
+	cluster, err := cfg.GetCluster("")
+	if err != nil {
+		panic(err)
+	}
+	port := 9000
+	for _, svc := range []*arvados.Service{
+		&cluster.Services.Controller,
+		&cluster.Services.DispatchCloud,
+		&cluster.Services.RailsAPI,
+	} {
+		if len(svc.InternalURLs) == 0 {
+			port++
+			svc.InternalURLs = map[arvados.URL]arvados.ServiceInstance{
+				arvados.URL{Scheme: "http", Host: fmt.Sprintf("localhost:%d", port)}: arvados.ServiceInstance{},
+			}
+		}
+	}
+	if cluster.Services.Controller.ExternalURL.Host == "" {
+		for k := range cluster.Services.Controller.InternalURLs {
+			cluster.Services.Controller.ExternalURL = k
+		}
+	}
+	if cluster.SystemRootToken == "" {
+		cluster.SystemRootToken = randomHexString(64)
+	}
+	if cluster.API.RailsSessionSecretToken == "" {
+		cluster.API.RailsSessionSecretToken = randomHexString(64)
+	}
+	if cluster.Collections.BlobSigningKey == "" {
+		cluster.Collections.BlobSigningKey = randomHexString(64)
+	}
+	cfg.Clusters[cluster.ClusterID] = *cluster
+}
+
+func randomHexString(chars int) string {
+	b := make([]byte, chars/2)
+	_, err := rand.Read(b)
+	if err != nil {
+		panic(err)
+	}
+	return fmt.Sprintf("%x", b)
+}

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


hooks/post-receive
-- 




More information about the arvados-commits mailing list