15954: Merge branch 'master'
authorTom Clegg <tom@tomclegg.ca>
Tue, 11 Feb 2020 16:39:44 +0000 (11:39 -0500)
committerTom Clegg <tom@tomclegg.ca>
Tue, 11 Feb 2020 16:39:44 +0000 (11:39 -0500)
Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tom@tomclegg.ca>

12 files changed:
cmd/arvados-server/cmd.go
doc/examples/config/zzzzz.yml [new file with mode: 0644]
go.mod
lib/boot/cmd.go [new file with mode: 0644]
lib/boot/nginx.go [new file with mode: 0644]
lib/config/config.default.yml
lib/config/generated_config.go
lib/service/cmd.go
sdk/go/health/aggregator.go
sdk/python/tests/nginx.conf
sdk/python/tests/run_test_server.py
services/keepproxy/keepproxy.go

index d93a8e78fd3f216788584872a7d1bd3f3fd675d2..a9d927d8734401f76fa173bff7214e0038fc4c68 100644 (file)
@@ -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 (file)
index 0000000..9e3d718
--- /dev/null
@@ -0,0 +1,19 @@
+Clusters:
+  zzzzz:
+    PostgreSQL:
+      Connection:
+        client_encoding: utf8
+        host: localhost
+        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: /
diff --git a/go.mod b/go.mod
index 033723d23680982c09763f2db7c0ebd6917953f5..2e16e5a0fbec65656d6871e76579a38f24d29eb4 100644 (file)
--- 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 (file)
index 0000000..4d2c01f
--- /dev/null
@@ -0,0 +1,613 @@
+// 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"
+       "time"
+
+       "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"
+       "git.arvados.org/arvados.git/sdk/go/health"
+       "github.com/sirupsen/logrus"
+)
+
+var Command cmd.Handler = bootCommand{}
+
+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()
+
+       ch := make(chan os.Signal)
+       signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM)
+       go func() {
+               for sig := range ch {
+                       boot.logger.WithField("signal", sig).Info("caught signal")
+                       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")
+       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
+       }
+
+       boot.Start(ctx, loader)
+       defer boot.Stop()
+       if boot.WaitReady() {
+               fmt.Fprintln(stdout, boot.cluster.Services.Controller.ExternalURL)
+               <-ctx.Done() // wait for signal
+               return 0
+       } else {
+               return 1
+       }
+}
+
+type Booter struct {
+       SourcePath  string // e.g., /home/username/src/arvados
+       LibPath     string // e.g., /var/lib/arvados
+       ClusterType string // e.g., production
+       Stderr      io.Writer
+
+       logger  logrus.FieldLogger
+       cluster *arvados.Cluster
+
+       ctx           context.Context
+       cancel        context.CancelFunc
+       done          chan struct{}
+       healthChecker *health.Aggregator
+
+       tempdir    string
+       configfile string
+       environ    []string // for child processes
+
+       setupRubyOnce sync.Once
+       setupRubyErr  error
+       goMutex       sync.Mutex
+}
+
+func (boot *Booter) Start(ctx context.Context, loader *config.Loader) {
+       boot.ctx, boot.cancel = context.WithCancel(ctx)
+       boot.done = make(chan struct{})
+       go func() {
+               err := boot.run(loader)
+               if err != nil {
+                       fmt.Fprintln(boot.Stderr, err)
+               }
+               close(boot.done)
+       }()
+}
+
+func (boot *Booter) run(loader *config.Loader) 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)
+
+       loader.SkipAPICalls = true
+       cfg, err := loader.Load()
+       if 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.setEnv("ARVADOS_CONFIG", boot.configfile)
+       boot.setEnv("RAILS_ENV", boot.ClusterType)
+       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.
+       boot.logger = ctxlog.New(boot.Stderr, boot.cluster.SystemLogs.Format, boot.cluster.SystemLogs.LogLevel).WithFields(logrus.Fields{
+               "PID": os.Getpid(),
+       })
+       boot.healthChecker = &health.Aggregator{Cluster: boot.cluster}
+
+       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
+       }
+
+       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: "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: "workbench1", svc: boot.cluster.Services.Workbench1, railsApp: "apps/workbench"},
+               {name: "ws", goProg: "services/ws"},
+       } {
+               cmpt := cmpt
+               wg.Add(1)
+               go func() {
+                       defer wg.Done()
+                       defer boot.cancel()
+                       boot.logger.WithField("component", cmpt.name).Info("starting")
+                       err := cmpt.Run(boot.ctx, boot)
+                       if err != nil && err != context.Canceled {
+                               boot.logger.WithError(err).WithField("component", cmpt.name).Error("exited")
+                       }
+               }()
+       }
+       wg.Wait()
+       return nil
+}
+
+func (boot *Booter) Stop() {
+       boot.cancel()
+       <-boot.done
+}
+
+func (boot *Booter) WaitReady() bool {
+       for waiting := true; waiting; {
+               time.Sleep(time.Second)
+               if boot.ctx.Err() != nil {
+                       return 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 _, check := range resp.Checks {
+                       if check.Health != "OK" {
+                               waiting = true
+                       }
+               }
+       }
+       return 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)
+}
+
+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)
+}
+
+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")
+}
+
+func (boot *Booter) setupRubyEnv() error {
+       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 *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...))
+       fmt.Fprintf(boot.Stderr, "%s executing in %s\n", cmdline, dir)
+       cmd := exec.Command(boot.lookPath(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)
+       }
+       cmd.Env = append(env, boot.environ...)
+
+       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 {
+                                       log.WithField("PID", cmd.Process.Pid).Warn("still waiting for child process to exit 5s after SIGTERM")
+                               }
+                       }
+               }
+       }()
+
+       err := cmd.Run()
+       if err != nil && ctx.Err() == nil {
+               // Only report errors that happen before the context ends.
+               return fmt.Errorf("%s: error: %v", cmdline, err)
+       }
+       return nil
+}
+
+type component struct {
+       name       string
+       svc        arvados.Service
+       cmdHandler cmd.Handler
+       runFunc    func(ctx context.Context, boot *Booter) 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
+}
+
+func (cmpt *component) Run(ctx context.Context, boot *Booter) error {
+       if cmpt.notIfTest && boot.ClusterType == "test" {
+               fmt.Fprintf(boot.Stderr, "skipping component %q in %s mode\n", cmpt.name, boot.ClusterType)
+               <-ctx.Done()
+               return nil
+       }
+       fmt.Fprintf(boot.Stderr, "starting component %q\n", cmpt.name)
+       if cmpt.cmdHandler != nil {
+               errs := make(chan error, 1)
+               go func() {
+                       defer close(errs)
+                       exitcode := cmpt.cmdHandler.RunCommand(cmpt.name, []string{"-config", boot.configfile}, bytes.NewBuffer(nil), boot.Stderr, boot.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
+               }
+       }
+       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
+                       for u := range cmpt.svc.InternalURLs {
+                               u := u
+                               wg.Add(1)
+                               go func() {
+                                       defer wg.Done()
+                                       boot.RunProgram(ctx, boot.tempdir, nil, []string{"ARVADOS_SERVICE_INTERNAL_URL=" + u.String()}, basename)
+                               }()
+                       }
+                       wg.Wait()
+               } else {
+                       // Just run one
+                       boot.RunProgram(ctx, boot.tempdir, nil, nil, basename)
+               }
+               return nil
+       }
+       if cmpt.runFunc != nil {
+               return cmpt.runFunc(ctx, boot)
+       }
+       if cmpt.railsApp != "" {
+               port, err := internalPort(cmpt.svc)
+               if err != nil {
+                       return fmt.Errorf("bug: no InternalURLs for component %q: %v", cmpt.name, cmpt.svc.InternalURLs)
+               }
+               var buf bytes.Buffer
+               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.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.railsApp, nil, nil, "bundle", "install", "--jobs", "4", "--path", filepath.Join(os.Getenv("HOME"), ".gem"))
+               if err != nil {
+                       return err
+               }
+               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.railsApp, nil, nil, "bundle", "exec", "passenger-config", "install-standalone-runtime")
+               if err != nil {
+                       return err
+               }
+               err = boot.RunProgram(ctx, cmpt.railsApp, nil, nil, "bundle", "exec", "passenger-config", "validate-install")
+               if err != nil {
+                       return err
+               }
+               err = boot.RunProgram(ctx, cmpt.railsApp, nil, nil, "bundle", "exec", "passenger", "start", "-p", port)
+               if err != nil {
+                       return err
+               }
+               return nil
+       }
+       return fmt.Errorf("bug: component %q has nothing to run", cmpt.name)
+}
+
+func (boot *Booter) autofillConfig(cfg *arvados.Config, log logrus.FieldLogger) error {
+       cluster, err := cfg.GetCluster("")
+       if err != nil {
+               return err
+       }
+       port := 9000
+       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 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 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) {
+                       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)
+       }
+       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.
+               port++
+               cluster.Services.Keepstore.InternalURLs[arvados.URL{Scheme: "http", Host: fmt.Sprintf("localhost:%d", port)}] = 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("zzzzz-nyw5e-%015d", volnum)] = arvados.Volume{
+                               Driver:           "Directory",
+                               DriverParameters: json.RawMessage(fmt.Sprintf(`{"Root":%q}`, datadir)),
+                               AccessViaHosts: map[arvados.URL]arvados.VolumeAccess{
+                                       url: {},
+                               },
+                       }
+               }
+       }
+       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
+       }
+}
diff --git a/lib/boot/nginx.go b/lib/boot/nginx.go
new file mode 100644 (file)
index 0000000..1b361dd
--- /dev/null
@@ -0,0 +1,76 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+package boot
+
+import (
+       "context"
+       "fmt"
+       "io/ioutil"
+       "os"
+       "os/exec"
+       "path/filepath"
+       "regexp"
+
+       "git.arvados.org/arvados.git/sdk/go/arvados"
+)
+
+func runNginx(ctx context.Context, boot *Booter) 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)
+}
index 41af15073281b51d7a9087195a7ad9aacb9c0cc2..59dabbb26d6f6e093bc1ad94b0559771269c6ed5 100644 (file)
@@ -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.
index 25fa89394a0ba9132b23a29126866c94f2d7bc08..2d8a487b7db2e8cd55f915404f2544351e909b1e 100644 (file)
@@ -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.
index f1f3fd91dbe211a0742db03fec3052853c255b75..48912b8898c4cce944c43f3879c4a03ac25a87f8 100644 (file)
@@ -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)
index a1ef5e0beb76d8c95cef9c0b9ec5a2dbe8df9eae..a0284e8f247a60f8d2fd57b752f37a800d54c222 100644 (file)
@@ -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)
        }
@@ -103,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),
index 6010ee4bf73e0fc0278c672b41a20c0ecaa35532..e9be122354c933dbea9b828b093a1ca295c087e7 100644 (file)
@@ -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}}";
index 9e9b12f98ca6d09ed1fb65eb5c768acf23454bf5..5c05c124c642c7467d6252b8bcb420c8ca271767 100644 (file)
@@ -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')
index 58e4a85347ed3cb84428f19bb7788532157deeb0..2b15d79940844285bbb57c1f771bb31a4c15ff78 100644 (file)
@@ -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)))
 }