From: Tom Clegg Date: Tue, 18 Feb 2020 21:26:46 +0000 (-0500) Subject: 15954: Merge branch 'master' X-Git-Tag: 2.1.0~273^2~60 X-Git-Url: https://git.arvados.org/arvados.git/commitdiff_plain/63b1a5c12557bb11505f9dcfcc955f328b51e118?hp=a1d031db9dc26e2b2da06b612ddd81fbd7fcd9e9 15954: Merge branch 'master' Arvados-DCO-1.1-Signed-off-by: Tom Clegg --- diff --git a/build/run-tests.sh b/build/run-tests.sh index 891faca419..ad800dfd07 100755 --- a/build/run-tests.sh +++ b/build/run-tests.sh @@ -555,7 +555,7 @@ setup_ruby_environment() { export HOME=$GEMHOME ("$bundle" version | grep -q 2.0.2) \ || gem install --user bundler -v 2.0.2 - "$bundle" version | grep 2.0.2 + "$bundle" version | tee /dev/stderr | grep -q 'version 2' ) || fatal 'install bundler' fi } diff --git a/cmd/arvados-server/cmd.go b/cmd/arvados-server/cmd.go index d93a8e78fd..a9d927d873 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 0000000000..c63550edf7 --- /dev/null +++ b/doc/examples/config/zzzzz.yml @@ -0,0 +1,12 @@ +Clusters: + zzzzz: + 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 2f18527340..6d5f748d42 100644 --- a/go.mod +++ b/go.mod @@ -33,7 +33,7 @@ require ( github.com/julienschmidt/httprouter v1.2.0 github.com/karalabe/xgo v0.0.0-20191115072854-c5ccff8648a7 // indirect github.com/kevinburke/ssh_config v0.0.0-20171013211458-802051befeb5 // indirect - github.com/lib/pq v0.0.0-20171126050459-83612a56d3dd + github.com/lib/pq v1.3.0 github.com/marstr/guid v1.1.1-0.20170427235115-8bdf7d1a087c // indirect github.com/mitchellh/go-homedir v0.0.0-20161203194507-b8bc1bf76747 // indirect github.com/opencontainers/go-digest v1.0.0-rc1 // indirect @@ -52,7 +52,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/go.sum b/go.sum index 0a543fde90..253865d63d 100644 --- a/go.sum +++ b/go.sum @@ -113,6 +113,8 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxv github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/lib/pq v0.0.0-20171126050459-83612a56d3dd h1:2RDaVc4/izhWyAvYxNm8c9saSyCDIxefNwOcqaH7pcU= github.com/lib/pq v0.0.0-20171126050459-83612a56d3dd/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.3.0 h1:/qkRGz8zljWiDcFvgpwUpwIAPu3r07TDvs3Rws+o/pU= +github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/marstr/guid v1.1.1-0.20170427235115-8bdf7d1a087c h1:ouxemItv3B/Zh008HJkEXDYCN3BIRyNHxtUN7ThJ5Js= github.com/marstr/guid v1.1.1-0.20170427235115-8bdf7d1a087c/go.mod h1:74gB1z2wpxxInTG6yaqA7KrtM0NZ+RbrcqDvYHefzho= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= diff --git a/lib/boot/cert.go b/lib/boot/cert.go new file mode 100644 index 0000000000..560579b777 --- /dev/null +++ b/lib/boot/cert.go @@ -0,0 +1,58 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +package boot + +import ( + "context" + "io/ioutil" + "path/filepath" +) + +type createCertificates struct{} + +func (createCertificates) String() string { + return "certificates" +} + +func (createCertificates) Run(ctx context.Context, fail func(error), boot *Booter) error { + // Generate root key + err := boot.RunProgram(ctx, boot.tempdir, nil, nil, "openssl", "genrsa", "-out", "rootCA.key", "4096") + if err != nil { + return err + } + // Generate a self-signed root certificate + err = boot.RunProgram(ctx, boot.tempdir, nil, nil, "openssl", "req", "-x509", "-new", "-nodes", "-key", "rootCA.key", "-sha256", "-days", "3650", "-out", "rootCA.crt", "-subj", "/C=US/ST=MA/O=Example Org/CN=localhost") + if err != nil { + return err + } + // Generate server key + err = boot.RunProgram(ctx, boot.tempdir, nil, nil, "openssl", "genrsa", "-out", "server.key", "2048") + if err != nil { + return err + } + // Build config file for signing request + defaultconf, err := ioutil.ReadFile("/etc/ssl/openssl.cnf") + if err != nil { + return err + } + err = ioutil.WriteFile(filepath.Join(boot.tempdir, "server.cfg"), append(defaultconf, []byte(` +[SAN] +subjectAltName=DNS:localhost,DNS:localhost.localdomain +`)...), 0777) + if err != nil { + return err + } + // Generate signing request + err = boot.RunProgram(ctx, boot.tempdir, nil, nil, "openssl", "req", "-new", "-sha256", "-key", "server.key", "-subj", "/C=US/ST=MA/O=Example Org/CN=localhost", "-reqexts", "SAN", "-config", "server.cfg", "-out", "server.csr") + if err != nil { + return err + } + // Sign certificate + err = boot.RunProgram(ctx, boot.tempdir, nil, nil, "openssl", "x509", "-req", "-in", "server.csr", "-CA", "rootCA.crt", "-CAkey", "rootCA.key", "-CAcreateserial", "-out", "server.crt", "-days", "3650", "-sha256") + if err != nil { + return err + } + return nil +} diff --git a/lib/boot/cmd.go b/lib/boot/cmd.go new file mode 100644 index 0000000000..6cf44837ca --- /dev/null +++ b/lib/boot/cmd.go @@ -0,0 +1,642 @@ +// 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/sdk/go/arvados" + "git.arvados.org/arvados.git/sdk/go/ctxlog" + "git.arvados.org/arvados.git/sdk/go/health" + "github.com/sirupsen/logrus" +) + +var Command cmd.Handler = bootCommand{} + +type bootTask interface { + // Execute the task. Run should return nil when the task is + // done enough to satisfy a dependency relationship (e.g., the + // service is running and ready). If the task starts a + // goroutine that fails after Run returns (e.g., the service + // shuts down), it should call cancel. + Run(ctx context.Context, fail func(error), boot *Booter) error + String() string +} + +type bootCommand struct{} + +func (bootCommand) RunCommand(prog string, args []string, stdin io.Reader, stdout, stderr io.Writer) int { + boot := &Booter{ + Stderr: stderr, + logger: ctxlog.New(stderr, "json", "info"), + } + + ctx := ctxlog.Context(context.Background(), boot.logger) + ctx, cancel := context.WithCancel(ctx) + defer cancel() + + 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") + flags.StringVar(&boot.ListenHost, "listen-host", "localhost", "host name or interface address for service listeners") + flags.StringVar(&boot.ControllerAddr, "controller-address", ":0", "desired controller address, `host:port` or `:port`") + flags.BoolVar(&boot.OwnTemporaryDatabase, "own-temporary-database", false, "bring up a postgres server and create a temporary database") + err = flags.Parse(args) + if err == flag.ErrHelp { + err = nil + return 0 + } else if err != nil { + return 2 + } else if *versionFlag { + return cmd.Version.RunCommand(prog, args, stdin, stdout, stderr) + } else if boot.ClusterType != "development" && boot.ClusterType != "test" && boot.ClusterType != "production" { + err = fmt.Errorf("cluster type must be 'development', 'test', or 'production'") + return 2 + } + + 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 + ListenHost string // e.g., localhost + ControllerAddr string // e.g., 127.0.0.1:8000 + OwnTemporaryDatabase bool + Stderr io.Writer + + logger logrus.FieldLogger + cluster *arvados.Cluster + + ctx context.Context + cancel context.CancelFunc + done chan struct{} + healthChecker *health.Aggregator + tasksReady map[string]chan bool + + 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. + loglevel := boot.cluster.SystemLogs.LogLevel + if s := os.Getenv("ARVADOS_DEBUG"); s != "" && s != "0" { + loglevel = "debug" + } + boot.logger = ctxlog.New(boot.Stderr, boot.cluster.SystemLogs.Format, loglevel).WithFields(logrus.Fields{ + "PID": os.Getpid(), + }) + 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 + } + + tasks := []bootTask{ + createCertificates{}, + runPostgreSQL{}, + runNginx{}, + runServiceCommand{name: "controller", svc: boot.cluster.Services.Controller, depends: []bootTask{runPostgreSQL{}}}, + runGoProgram{src: "services/arv-git-httpd"}, + runGoProgram{src: "services/health"}, + runGoProgram{src: "services/keepproxy", depends: []bootTask{runPassenger{src: "services/api"}}}, + runGoProgram{src: "services/keepstore", svc: boot.cluster.Services.Keepstore}, + runGoProgram{src: "services/keep-web"}, + runGoProgram{src: "services/ws", depends: []bootTask{runPostgreSQL{}}}, + installPassenger{src: "services/api"}, + runPassenger{src: "services/api", svc: boot.cluster.Services.RailsAPI, depends: []bootTask{createCertificates{}, runPostgreSQL{}, installPassenger{src: "services/api"}}}, + installPassenger{src: "apps/workbench", depends: []bootTask{installPassenger{src: "services/api"}}}, // dependency ensures workbench doesn't delay api startup + runPassenger{src: "apps/workbench", svc: boot.cluster.Services.Workbench1, depends: []bootTask{installPassenger{src: "apps/workbench"}}}, + seedDatabase{}, + } + if boot.ClusterType != "test" { + tasks = append(tasks, + runServiceCommand{name: "dispatch-cloud", svc: boot.cluster.Services.Controller}, + runGoProgram{src: "services/keep-balance"}, + ) + } + boot.tasksReady = map[string]chan bool{} + for _, task := range tasks { + boot.tasksReady[task.String()] = make(chan bool) + } + for _, task := range tasks { + task := task + fail := func(err error) { + if boot.ctx.Err() != nil { + return + } + boot.cancel() + boot.logger.WithField("task", task.String()).WithError(err).Error("task failed") + } + go func() { + boot.logger.WithField("task", task.String()).Info("starting") + err := task.Run(boot.ctx, fail, boot) + if err != nil { + fail(err) + return + } + close(boot.tasksReady[task.String()]) + }() + } + err = boot.wait(boot.ctx, tasks...) + if err != nil { + return err + } + <-boot.ctx.Done() + return boot.ctx.Err() +} + +func (boot *Booter) wait(ctx context.Context, tasks ...bootTask) error { + for _, task := range tasks { + ch, ok := boot.tasksReady[task.String()] + if !ok { + return fmt.Errorf("no such task: %s", task) + } + boot.logger.WithField("task", task.String()).Info("waiting") + select { + case <-ch: + case <-ctx.Done(): + return ctx.Err() + } + } + 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 target, check := range resp.Checks { + if check.Health != "OK" { + waiting = true + boot.logger.WithField("target", target).Debug("waiting") + } + } + } + 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) + + logprefix := prog + if prog == "bundle" && len(args) > 2 && args[0] == "exec" { + logprefix = args[1] + } + if !strings.HasPrefix(dir, "/") { + logprefix = dir + ": " + logprefix + } + stderr := &logPrefixer{Writer: boot.Stderr, Prefix: []byte("[" + logprefix + "] ")} + + cmd := exec.Command(boot.lookPath(prog), args...) + if output == nil { + cmd.Stdout = stderr + } else { + cmd.Stdout = output + } + cmd.Stderr = 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 +} + +func (boot *Booter) autofillConfig(cfg *arvados.Config, log logrus.FieldLogger) error { + cluster, err := cfg.GetCluster("") + if err != nil { + return err + } + usedPort := map[string]bool{} + nextPort := func() string { + for { + port, err := availablePort(":0") + if err != nil { + panic(err) + } + if usedPort[port] { + continue + } + usedPort[port] = true + return port + } + } + if cluster.Services.Controller.ExternalURL.Host == "" { + h, p, err := net.SplitHostPort(boot.ControllerAddr) + if err != nil { + return err + } + if h == "" { + h = boot.ListenHost + } + if p == "0" { + p, err = availablePort(":0") + if err != nil { + return err + } + usedPort[p] = true + } + cluster.Services.Controller.ExternalURL = arvados.URL{Scheme: "https", Host: net.JoinHostPort(h, p)} + } + for _, svc := range []*arvados.Service{ + &cluster.Services.Controller, + &cluster.Services.DispatchCloud, + &cluster.Services.GitHTTP, + &cluster.Services.Health, + &cluster.Services.Keepproxy, + &cluster.Services.Keepstore, + &cluster.Services.RailsAPI, + &cluster.Services.WebDAV, + &cluster.Services.WebDAVDownload, + &cluster.Services.Websocket, + &cluster.Services.Workbench1, + } { + if svc == &cluster.Services.DispatchCloud && boot.ClusterType == "test" { + continue + } + if svc.ExternalURL.Host == "" && (svc == &cluster.Services.Controller || + svc == &cluster.Services.GitHTTP || + svc == &cluster.Services.Keepproxy || + svc == &cluster.Services.WebDAV || + svc == &cluster.Services.WebDAVDownload || + svc == &cluster.Services.Websocket || + svc == &cluster.Services.Workbench1) { + svc.ExternalURL = arvados.URL{Scheme: "https", Host: fmt.Sprintf("%s:%s", boot.ListenHost, nextPort())} + } + if len(svc.InternalURLs) == 0 { + svc.InternalURLs = map[arvados.URL]arvados.ServiceInstance{ + arvados.URL{Scheme: "http", Host: fmt.Sprintf("%s:%s", boot.ListenHost, nextPort())}: arvados.ServiceInstance{}, + } + } + } + if cluster.SystemRootToken == "" { + cluster.SystemRootToken = randomHexString(64) + } + if cluster.ManagementToken == "" { + cluster.ManagementToken = randomHexString(64) + } + if cluster.API.RailsSessionSecretToken == "" { + cluster.API.RailsSessionSecretToken = randomHexString(64) + } + if cluster.Collections.BlobSigningKey == "" { + cluster.Collections.BlobSigningKey = randomHexString(64) + } + if boot.ClusterType != "production" && cluster.Containers.DispatchPrivateKey == "" { + buf, err := ioutil.ReadFile(filepath.Join(boot.SourcePath, "lib", "dispatchcloud", "test", "sshkey_dispatch")) + if err != nil { + return err + } + cluster.Containers.DispatchPrivateKey = string(buf) + } + if boot.ClusterType != "production" { + cluster.TLS.Insecure = true + } + if boot.ClusterType == "test" { + // Add a second keepstore process. + cluster.Services.Keepstore.InternalURLs[arvados.URL{Scheme: "http", Host: fmt.Sprintf("%s:%s", boot.ListenHost, nextPort())}] = arvados.ServiceInstance{} + + // Create a directory-backed volume for each keepstore + // process. + cluster.Volumes = map[string]arvados.Volume{} + for url := range cluster.Services.Keepstore.InternalURLs { + volnum := len(cluster.Volumes) + datadir := fmt.Sprintf("%s/keep%d.data", boot.tempdir, volnum) + if _, err = os.Stat(datadir + "/."); err == nil { + } else if !os.IsNotExist(err) { + return err + } else if err = os.Mkdir(datadir, 0777); err != nil { + return err + } + cluster.Volumes[fmt.Sprintf(cluster.ClusterID+"-nyw5e-%015d", volnum)] = arvados.Volume{ + Driver: "Directory", + DriverParameters: json.RawMessage(fmt.Sprintf(`{"Root":%q}`, datadir)), + AccessViaHosts: map[arvados.URL]arvados.VolumeAccess{ + url: {}, + }, + } + } + } + if boot.OwnTemporaryDatabase { + cluster.PostgreSQL.Connection = arvados.PostgreSQLConnection{ + "client_encoding": "utf8", + "host": "localhost", + "port": nextPort(), + "dbname": "arvados_test", + "user": "arvados", + "password": "insecure_arvados_test", + } + } + + cfg.Clusters[cluster.ClusterID] = *cluster + return nil +} + +func randomHexString(chars int) string { + b := make([]byte, chars/2) + _, err := rand.Read(b) + if err != nil { + panic(err) + } + return fmt.Sprintf("%x", b) +} + +func internalPort(svc arvados.Service) (string, error) { + for u := range svc.InternalURLs { + if _, p, err := net.SplitHostPort(u.Host); err != nil { + return "", err + } else if p != "" { + return p, nil + } else if u.Scheme == "https" { + return "443", nil + } else { + return "80", nil + } + } + return "", fmt.Errorf("service has no InternalURLs") +} + +func externalPort(svc arvados.Service) (string, error) { + if _, p, err := net.SplitHostPort(svc.ExternalURL.Host); err != nil { + return "", err + } else if p != "" { + return p, nil + } else if svc.ExternalURL.Scheme == "https" { + return "443", nil + } else { + return "80", nil + } +} + +func availablePort(addr string) (string, error) { + ln, err := net.Listen("tcp", addr) + if err != nil { + return "", err + } + defer ln.Close() + _, port, err := net.SplitHostPort(ln.Addr().String()) + if err != nil { + return "", err + } + return port, nil +} + +// Try to connect to addr until it works, then close ch. Give up if +// ctx cancels. +func waitForConnect(ctx context.Context, addr string) error { + dialer := net.Dialer{Timeout: time.Second} + for ctx.Err() == nil { + conn, err := dialer.DialContext(ctx, "tcp", addr) + if err != nil { + time.Sleep(time.Second / 10) + continue + } + conn.Close() + return nil + } + return ctx.Err() +} diff --git a/lib/boot/log.go b/lib/boot/log.go new file mode 100644 index 0000000000..062a854a08 --- /dev/null +++ b/lib/boot/log.go @@ -0,0 +1,32 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +package boot + +import ( + "bytes" + "io" +) + +type logPrefixer struct { + io.Writer + Prefix []byte + did bool +} + +func (lp *logPrefixer) Write(p []byte) (int, error) { + if len(p) == 0 { + return 0, nil + } + if !lp.did { + lp.Writer.Write(lp.Prefix) + lp.did = p[len(p)-1] != '\n' + } + out := append(bytes.Replace(p[:len(p)-1], []byte("\n"), append([]byte("\n"), lp.Prefix...), -1), p[len(p)-1]) + _, err := lp.Writer.Write(out) + if err != nil { + return 0, err + } + return len(p), nil +} diff --git a/lib/boot/nginx.go b/lib/boot/nginx.go new file mode 100644 index 0000000000..5c1954c838 --- /dev/null +++ b/lib/boot/nginx.go @@ -0,0 +1,87 @@ +// 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" +) + +type runNginx struct{} + +func (runNginx) String() string { + return "nginx" +} + +func (runNginx) Run(ctx context.Context, fail func(error), boot *Booter) error { + vars := map[string]string{ + "LISTENHOST": boot.ListenHost, + "SSLCERT": filepath.Join(boot.SourcePath, "services", "api", "tmp", "self-signed.pem"), // TODO: root ca + "SSLKEY": filepath.Join(boot.SourcePath, "services", "api", "tmp", "self-signed.key"), // TODO: root ca + "ACCESSLOG": filepath.Join(boot.tempdir, "nginx_access.log"), + "ERRORLOG": filepath.Join(boot.tempdir, "nginx_error.log"), + "TMPDIR": boot.tempdir, + } + var err error + for _, cmpt := range []struct { + varname string + svc arvados.Service + }{ + {"CONTROLLER", boot.cluster.Services.Controller}, + {"KEEPWEB", boot.cluster.Services.WebDAV}, + {"KEEPWEBDL", boot.cluster.Services.WebDAVDownload}, + {"KEEPPROXY", boot.cluster.Services.Keepproxy}, + {"GIT", boot.cluster.Services.GitHTTP}, + {"WORKBENCH1", boot.cluster.Services.Workbench1}, + {"WS", boot.cluster.Services.Websocket}, + } { + 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 + } + } + } + go func() { + fail(boot.RunProgram(ctx, ".", nil, nil, nginx, + "-g", "error_log stderr info;", + "-g", "pid "+filepath.Join(boot.tempdir, "nginx.pid")+";", + "-c", conffile)) + }() + return waitForConnect(ctx, boot.cluster.Services.Controller.ExternalURL.Host) +} diff --git a/lib/boot/passenger.go b/lib/boot/passenger.go new file mode 100644 index 0000000000..61f97df526 --- /dev/null +++ b/lib/boot/passenger.go @@ -0,0 +1,93 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +package boot + +import ( + "bytes" + "context" + "fmt" + "os" + "path/filepath" + "strings" + + "git.arvados.org/arvados.git/sdk/go/arvados" +) + +type installPassenger struct { + src string + depends []bootTask +} + +func (runner installPassenger) String() string { + return "installPassenger:" + runner.src +} + +func (runner installPassenger) Run(ctx context.Context, fail func(error), boot *Booter) error { + err := boot.wait(ctx, runner.depends...) + if err != nil { + return err + } + var buf bytes.Buffer + err = boot.RunProgram(ctx, runner.src, &buf, nil, "gem", "list", "--details", "bundler") + if err != nil { + return err + } + for _, version := range []string{"1.11.0", "1.17.3", "2.0.2"} { + if !strings.Contains(buf.String(), "("+version+")") { + err = boot.RunProgram(ctx, runner.src, nil, nil, "gem", "install", "--user", "bundler:1.11", "bundler:1.17.3", "bundler:2.0.2") + if err != nil { + return err + } + break + } + } + err = boot.RunProgram(ctx, runner.src, nil, nil, "bundle", "install", "--jobs", "4", "--path", filepath.Join(os.Getenv("HOME"), ".gem")) + if err != nil { + return err + } + err = boot.RunProgram(ctx, runner.src, nil, nil, "bundle", "exec", "passenger-config", "build-native-support") + if err != nil { + return err + } + err = boot.RunProgram(ctx, runner.src, nil, nil, "bundle", "exec", "passenger-config", "install-standalone-runtime") + if err != nil { + return err + } + err = boot.RunProgram(ctx, runner.src, nil, nil, "bundle", "exec", "passenger-config", "validate-install") + if err != nil { + return err + } + return nil +} + +type runPassenger struct { + src string + svc arvados.Service + depends []bootTask +} + +func (runner runPassenger) String() string { + return "runPassenger:" + runner.src +} + +func (runner runPassenger) Run(ctx context.Context, fail func(error), boot *Booter) error { + err := boot.wait(ctx, runner.depends...) + if err != nil { + return err + } + port, err := internalPort(runner.svc) + if err != nil { + return fmt.Errorf("bug: no InternalURLs for component %q: %v", runner, runner.svc.InternalURLs) + } + go func() { + err = boot.RunProgram(ctx, runner.src, nil, nil, "bundle", "exec", + "passenger", "start", + "-p", port, + "--log-file", "/dev/null", + "--pid-file", filepath.Join(boot.tempdir, "passenger."+strings.Replace(runner.src, "/", "_", -1)+".pid")) + fail(err) + }() + return nil +} diff --git a/lib/boot/postgresql.go b/lib/boot/postgresql.go new file mode 100644 index 0000000000..48e24ffaec --- /dev/null +++ b/lib/boot/postgresql.go @@ -0,0 +1,100 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +package boot + +import ( + "bytes" + "context" + "database/sql" + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + "time" + + "git.arvados.org/arvados.git/sdk/go/arvados" + "github.com/lib/pq" +) + +type runPostgreSQL struct{} + +func (runPostgreSQL) String() string { + return "postgresql" +} + +func (runPostgreSQL) Run(ctx context.Context, fail func(error), boot *Booter) error { + err := boot.wait(ctx, createCertificates{}) + if err != nil { + return err + } + + buf := bytes.NewBuffer(nil) + err = boot.RunProgram(ctx, boot.tempdir, buf, nil, "pg_config", "--bindir") + if err != nil { + return err + } + datadir := filepath.Join(boot.tempdir, "pgdata") + + err = os.Mkdir(datadir, 0755) + if err != nil { + return err + } + bindir := strings.TrimSpace(buf.String()) + + err = boot.RunProgram(ctx, boot.tempdir, nil, nil, filepath.Join(bindir, "initdb"), "-D", datadir) + if err != nil { + return err + } + + err = boot.RunProgram(ctx, boot.tempdir, nil, nil, "cp", "server.crt", "server.key", datadir) + if err != nil { + return err + } + + port := boot.cluster.PostgreSQL.Connection["port"] + + go func() { + fail(boot.RunProgram(ctx, boot.tempdir, nil, nil, filepath.Join(bindir, "postgres"), + "-l", // enable ssl + "-D", datadir, // data dir + "-k", datadir, // socket dir + "-p", boot.cluster.PostgreSQL.Connection["port"], + )) + }() + + for { + if ctx.Err() != nil { + return ctx.Err() + } + if exec.CommandContext(ctx, "pg_isready", "--timeout=10", "--host="+boot.cluster.PostgreSQL.Connection["host"], "--port="+port).Run() == nil { + break + } + time.Sleep(time.Second / 2) + } + db, err := sql.Open("postgres", arvados.PostgreSQLConnection{ + "host": datadir, + "port": port, + "dbname": "postgres", + }.String()) + if err != nil { + return fmt.Errorf("db open failed: %s", err) + } + defer db.Close() + conn, err := db.Conn(ctx) + if err != nil { + return fmt.Errorf("db conn failed: %s", err) + } + defer conn.Close() + _, err = conn.ExecContext(ctx, `CREATE USER `+pq.QuoteIdentifier(boot.cluster.PostgreSQL.Connection["user"])+` WITH SUPERUSER ENCRYPTED PASSWORD `+pq.QuoteLiteral(boot.cluster.PostgreSQL.Connection["password"])) + if err != nil { + return fmt.Errorf("createuser failed: %s", err) + } + _, err = conn.ExecContext(ctx, `CREATE DATABASE `+pq.QuoteIdentifier(boot.cluster.PostgreSQL.Connection["dbname"])) + if err != nil { + return fmt.Errorf("createdb failed: %s", err) + } + return nil +} diff --git a/lib/boot/seed.go b/lib/boot/seed.go new file mode 100644 index 0000000000..9f086d5445 --- /dev/null +++ b/lib/boot/seed.go @@ -0,0 +1,27 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +package boot + +import ( + "context" +) + +type seedDatabase struct{} + +func (seedDatabase) String() string { + return "seedDatabase" +} + +func (seedDatabase) Run(ctx context.Context, fail func(error), boot *Booter) error { + err := boot.wait(ctx, runPostgreSQL{}, installPassenger{src: "services/api"}) + if err != nil { + return err + } + err = boot.RunProgram(ctx, "services/api", nil, nil, "bundle", "exec", "rake", "db:setup") + if err != nil { + return err + } + return nil +} diff --git a/lib/boot/service.go b/lib/boot/service.go new file mode 100644 index 0000000000..6edf78b3ce --- /dev/null +++ b/lib/boot/service.go @@ -0,0 +1,68 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +package boot + +import ( + "context" + "path/filepath" + + "git.arvados.org/arvados.git/sdk/go/arvados" +) + +type runServiceCommand struct { + name string + svc arvados.Service + depends []bootTask +} + +func (runner runServiceCommand) String() string { + return runner.name +} + +func (runner runServiceCommand) Run(ctx context.Context, fail func(error), boot *Booter) error { + boot.wait(ctx, runner.depends...) + go func() { + var u arvados.URL + for u = range runner.svc.InternalURLs { + } + fail(boot.RunProgram(ctx, boot.tempdir, nil, []string{"ARVADOS_SERVICE_INTERNAL_URL=" + u.String()}, "arvados-server", runner.name, "-config", boot.configfile)) + }() + return nil +} + +type runGoProgram struct { + src string + svc arvados.Service + depends []bootTask +} + +func (runner runGoProgram) String() string { + _, basename := filepath.Split(runner.src) + return basename +} + +func (runner runGoProgram) Run(ctx context.Context, fail func(error), boot *Booter) error { + boot.wait(ctx, runner.depends...) + boot.RunProgram(ctx, runner.src, nil, nil, "go", "install") + if ctx.Err() != nil { + return ctx.Err() + } + _, basename := filepath.Split(runner.src) + if len(runner.svc.InternalURLs) > 0 { + // Run one for each URL + for u := range runner.svc.InternalURLs { + u := u + go func() { + fail(boot.RunProgram(ctx, boot.tempdir, nil, []string{"ARVADOS_SERVICE_INTERNAL_URL=" + u.String()}, basename)) + }() + } + } else { + // Just run one + go func() { + fail(boot.RunProgram(ctx, boot.tempdir, nil, nil, basename)) + }() + } + return nil +} diff --git a/lib/config/config.default.yml b/lib/config/config.default.yml index 41af150732..59dabbb26d 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 25fa89394a..2d8a487b7d 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. diff --git a/lib/service/cmd.go b/lib/service/cmd.go index f1f3fd91db..48912b8898 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) diff --git a/sdk/go/health/aggregator.go b/sdk/go/health/aggregator.go index a1ef5e0beb..a0284e8f24 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) } @@ -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), diff --git a/sdk/python/tests/nginx.conf b/sdk/python/tests/nginx.conf index 6010ee4bf7..b10b3f0082 100644 --- a/sdk/python/tests/nginx.conf +++ b/sdk/python/tests/nginx.conf @@ -17,7 +17,7 @@ http { uwsgi_temp_path "{{TMPDIR}}"; scgi_temp_path "{{TMPDIR}}"; upstream arv-git-http { - server localhost:{{GITPORT}}; + server {{LISTENHOST}}:{{GITPORT}}; } server { listen *:{{GITSSLPORT}} ssl default_server; @@ -33,7 +33,7 @@ http { } } upstream keepproxy { - server localhost:{{KEEPPROXYPORT}}; + server {{LISTENHOST}}:{{KEEPPROXYPORT}}; } server { listen *:{{KEEPPROXYSSLPORT}} ssl default_server; @@ -52,7 +52,7 @@ http { } } upstream keep-web { - server localhost:{{KEEPWEBPORT}}; + server {{LISTENHOST}}:{{KEEPWEBPORT}}; } server { listen *:{{KEEPWEBSSLPORT}} ssl default_server; @@ -89,10 +89,10 @@ http { } } upstream ws { - server localhost:{{WSPORT}}; + server {{LISTENHOST}}:{{WSPORT}}; } server { - listen *:{{WSSPORT}} ssl default_server; + listen *:{{WSSSLPORT}} ssl default_server; server_name websocket; ssl_certificate "{{SSLCERT}}"; ssl_certificate_key "{{SSLKEY}}"; @@ -106,8 +106,24 @@ http { proxy_redirect off; } } + upstream workbench1 { + server {{LISTENHOST}}:{{WORKBENCH1PORT}}; + } + server { + listen *:{{WORKBENCH1SSLPORT}} ssl default_server; + server_name workbench1; + ssl_certificate "{{SSLCERT}}"; + ssl_certificate_key "{{SSLKEY}}"; + location / { + proxy_pass http://workbench1; + proxy_set_header Host $http_host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto https; + proxy_redirect off; + } + } upstream controller { - server localhost:{{CONTROLLERPORT}}; + server {{LISTENHOST}}:{{CONTROLLERPORT}}; } server { listen *:{{CONTROLLERSSLPORT}} ssl default_server; diff --git a/sdk/python/tests/run_test_server.py b/sdk/python/tests/run_test_server.py index 9e9b12f98c..9519c2b723 100644 --- a/sdk/python/tests/run_test_server.py +++ b/sdk/python/tests/run_test_server.py @@ -606,6 +606,7 @@ def run_nginx(): return stop_nginx() nginxconf = {} + nginxconf['LISTENHOST'] = 'localhost' nginxconf['CONTROLLERPORT'] = internal_port_from_config("Controller") nginxconf['CONTROLLERSSLPORT'] = external_port_from_config("Controller") nginxconf['KEEPWEBPORT'] = internal_port_from_config("WebDAV") @@ -616,7 +617,9 @@ 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['WORKBENCH1PORT'] = internal_port_from_config("Workbench1") + nginxconf['WORKBENCH1SSLPORT'] = external_port_from_config("Workbench1") 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') @@ -648,6 +651,8 @@ def setup_config(): controller_external_port = find_available_port() websocket_port = find_available_port() websocket_external_port = find_available_port() + workbench1_port = find_available_port() + workbench1_external_port = find_available_port() git_httpd_port = find_available_port() git_httpd_external_port = find_available_port() keepproxy_port = find_available_port() @@ -683,6 +688,12 @@ def setup_config(): "http://%s:%s"%(localhost, websocket_port): {}, }, }, + "Workbench1": { + "ExternalURL": "https://%s:%s/" % (localhost, workbench1_external_port), + "InternalURLs": { + "http://%s:%s"%(localhost, workbench1_port): {}, + }, + }, "GitHTTP": { "ExternalURL": "https://%s:%s" % (localhost, git_httpd_external_port), "InternalURLs": { @@ -712,6 +723,9 @@ def setup_config(): "http://%s:%s"%(localhost, keep_web_dl_port): {}, }, }, + "SSO": { + "ExternalURL": "http://localhost:3002", + }, } config = { @@ -721,6 +735,11 @@ def setup_config(): "SystemRootToken": auth_token('system_user'), "API": { "RequestTimeout": "30s", + "RailsSessionSecretToken": "e24205c490ac07e028fd5f8a692dcb398bcd654eff1aef5f9fe6891994b18483", + }, + "Login": { + "ProviderAppID": "arvados-server", + "ProviderAppSecret": "608dbf356a327e2d0d4932b60161e212c2d8d8f5e25690d7b622f850a990cd33", }, "SystemLogs": { "LogLevel": ('info' if os.environ.get('ARVADOS_DEBUG', '') in ['','0'] else 'debug'), @@ -734,14 +753,22 @@ def setup_config(): "Services": services, "Users": { "AnonymousUserToken": auth_token('anonymous'), + "UserProfileNotificationAddress": "arvados@example.com", }, "Collections": { "BlobSigningKey": "zfhgfenhffzltr9dixws36j1yhksjoll2grmku38mi7yxd66h5j4q9w4jzanezacp8s6q0ro3hxakfye02152hncy6zml2ed0uc", "TrustAllContent": True, "ForwardSlashNameSubstitution": "/", + "TrashSweepInterval": "-1s", }, "Git": { - "Repositories": "%s/test" % os.path.join(SERVICES_SRC_DIR, 'api', 'tmp', 'git'), + "Repositories": os.path.join(SERVICES_SRC_DIR, 'api', 'tmp', 'git', 'test'), + }, + "Containers": { + "JobsAPI": { + "GitInternalDir": os.path.join(SERVICES_SRC_DIR, 'api', 'tmp', 'internal.git'), + }, + "SupportedDockerImageFormats": {"v1": {}}, }, "Volumes": { "zzzzz-nyw5e-%015d"%n: { diff --git a/services/api/config/application.default.yml b/services/api/config/application.default.yml index 4e1936b771..9fd5368c0a 100644 --- a/services/api/config/application.default.yml +++ b/services/api/config/application.default.yml @@ -76,15 +76,3 @@ test: action_controller.allow_forgery_protection: false action_mailer.delivery_method: :test active_support.deprecation: :stderr - uuid_prefix: zzzzz - sso_app_id: arvados-server - sso_app_secret: <%= rand(2**512).to_s(36) %> - sso_provider_url: http://localhost:3002 - secret_token: <%= rand(2**512).to_s(36) %> - blob_signing_key: zfhgfenhffzltr9dixws36j1yhksjoll2grmku38mi7yxd66h5j4q9w4jzanezacp8s6q0ro3hxakfye02152hncy6zml2ed0uc - user_profile_notification_address: arvados@example.com - workbench_address: https://localhost:3001/ - git_repositories_dir: <%= Rails.root.join 'tmp', 'git', 'test' %> - git_internal_dir: <%= Rails.root.join 'tmp', 'internal.git' %> - trash_sweep_interval: -1 - docker_image_formats: ["v1"] diff --git a/services/api/lib/config_loader.rb b/services/api/lib/config_loader.rb index 522aa73b0a..cf16993ca5 100644 --- a/services/api/lib/config_loader.rb +++ b/services/api/lib/config_loader.rb @@ -180,8 +180,13 @@ class ConfigLoader end end - def self.parse_duration durstr, cfgkey: - duration_re = /-?(\d+(\.\d+)?)(s|m|h)/ + def self.parse_duration(durstr, cfgkey:) + sign = 1 + if durstr[0] == '-' + durstr = durstr[1..-1] + sign = -1 + end + duration_re = /(\d+(\.\d+)?)(s|m|h)/ dursec = 0 while durstr != "" mt = duration_re.match durstr @@ -189,7 +194,7 @@ class ConfigLoader raise "#{cfgkey} not a valid duration: '#{durstr}', accepted suffixes are s, m, h" end multiplier = {s: 1, m: 60, h: 3600} - dursec += (Float(mt[1]) * multiplier[mt[3].to_sym]) + dursec += (Float(mt[1]) * multiplier[mt[3].to_sym] * sign) durstr = durstr[mt[0].length..-1] end return dursec.seconds diff --git a/services/keepproxy/keepproxy.go b/services/keepproxy/keepproxy.go index 58e4a85347..2b15d79940 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))) }