--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+package boot
+
+import (
+ "context"
+ "io/ioutil"
+ "path/filepath"
+)
+
+func createCertificates(ctx context.Context, boot *Booter, ready chan<- bool) 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
+ }
+
+ close(ready)
+ <-ctx.Done()
+ return nil
+}
"os/exec"
"os/signal"
"path/filepath"
+ "strconv"
"strings"
"sync"
"syscall"
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.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
}
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
+ SourcePath string // e.g., /home/username/src/arvados
+ LibPath string // e.g., /var/lib/arvados
+ ClusterType string // e.g., production
+ OwnTemporaryDatabase bool
+ Stderr io.Writer
logger logrus.FieldLogger
cluster *arvados.Cluster
}
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
+ components := map[string]*component{
+ "certificates": &component{runFunc: createCertificates},
+ "database": &component{runFunc: runPostgres, depends: []string{"certificates"}},
+ "nginx": &component{runFunc: runNginx},
+ "controller": &component{cmdHandler: controller.Command, depends: []string{"database"}},
+ "dispatchcloud": &component{cmdHandler: dispatchcloud.Command, notIfTest: true},
+ "git-httpd": &component{goProg: "services/arv-git-httpd"},
+ "health": &component{goProg: "services/health"},
+ "keep-balance": &component{goProg: "services/keep-balance", notIfTest: true},
+ "keepproxy": &component{goProg: "services/keepproxy"},
+ "keepstore": &component{goProg: "services/keepstore", svc: boot.cluster.Services.Keepstore},
+ "keep-web": &component{goProg: "services/keep-web"},
+ "railsAPI": &component{svc: boot.cluster.Services.RailsAPI, railsApp: "services/api", depends: []string{"database"}},
+ "workbench1": &component{svc: boot.cluster.Services.Workbench1, railsApp: "apps/workbench"},
+ "ws": &component{goProg: "services/ws", depends: []string{"database"}},
+ }
+ for _, cmpt := range components {
+ cmpt.ready = make(chan bool)
+ }
+ for name, cmpt := range components {
+ name, cmpt := name, 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)
+ for _, dep := range cmpt.depends {
+ boot.logger.WithField("component", name).WithField("dependency", dep).Info("waiting")
+ select {
+ case <-components[dep].ready:
+ case <-boot.ctx.Done():
+ return
+ }
+ }
+ boot.logger.WithField("component", name).Info("starting")
+ err := cmpt.Run(boot.ctx, name, boot)
if err != nil && err != context.Canceled {
- boot.logger.WithError(err).WithField("component", cmpt.name).Error("exited")
+ boot.logger.WithError(err).WithField("component", name).Error("exited")
}
}()
}
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
+ runFunc func(ctx context.Context, boot *Booter, ready chan<- bool) 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
+ depends []string // don't start until all of these components are ready
+
+ ready chan bool
}
-func (cmpt *component) Run(ctx context.Context, boot *Booter) error {
+func (cmpt *component) Run(ctx context.Context, name string, boot *Booter) error {
if cmpt.notIfTest && boot.ClusterType == "test" {
- fmt.Fprintf(boot.Stderr, "skipping component %q in %s mode\n", cmpt.name, boot.ClusterType)
+ fmt.Fprintf(boot.Stderr, "skipping component %q in %s mode\n", name, boot.ClusterType)
<-ctx.Done()
return nil
}
- fmt.Fprintf(boot.Stderr, "starting component %q\n", cmpt.name)
+ fmt.Fprintf(boot.Stderr, "starting component %q\n", 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)
+ exitcode := cmpt.cmdHandler.RunCommand(name, []string{"-config", boot.configfile}, bytes.NewBuffer(nil), boot.Stderr, boot.Stderr)
if exitcode != 0 {
errs <- fmt.Errorf("exit code %d", exitcode)
}
return nil
}
if cmpt.runFunc != nil {
- return cmpt.runFunc(ctx, boot)
+ return cmpt.runFunc(ctx, boot, cmpt.ready)
}
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)
+ return fmt.Errorf("bug: no InternalURLs for component %q: %v", name, cmpt.svc.InternalURLs)
}
var buf bytes.Buffer
err = boot.RunProgram(ctx, cmpt.railsApp, &buf, nil, "gem", "list", "--details", "bundler")
}
return nil
}
- return fmt.Errorf("bug: component %q has nothing to run", cmpt.name)
+ return fmt.Errorf("bug: component %q has nothing to run", name)
}
func (boot *Booter) autofillConfig(cfg *arvados.Config, log logrus.FieldLogger) error {
}
}
}
+ if boot.OwnTemporaryDatabase {
+ p, err := availablePort()
+ if err != nil {
+ return err
+ }
+ cluster.PostgreSQL.Connection = arvados.PostgreSQLConnection{
+ "client_encoding": "utf8",
+ "host": "localhost",
+ "port": strconv.Itoa(p),
+ "dbname": "arvados_test",
+ "user": "arvados",
+ "password": "insecure_arvados_test",
+ }
+ }
+
cfg.Clusters[cluster.ClusterID] = *cluster
return nil
}
return "80", nil
}
}
+
+func availablePort() (int, error) {
+ ln, err := net.Listen("tcp", ":0")
+ if err != nil {
+ return 0, err
+ }
+ defer ln.Close()
+ _, p, err := net.SplitHostPort(ln.Addr().String())
+ if err != nil {
+ return 0, err
+ }
+ return strconv.Atoi(p)
+}
+
+// Try to connect to addr until it works, then close ch. Give up if
+// ctx cancels.
+func connectAndClose(ctx context.Context, addr string, ch chan<- bool) {
+ 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()
+ close(ch)
+ return
+ }
+}
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+package boot
+
+import (
+ "bytes"
+ "context"
+ "database/sql"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "strings"
+ "time"
+
+ "git.arvados.org/arvados.git/sdk/go/arvados"
+ "github.com/lib/pq"
+)
+
+func runPostgres(ctx context.Context, boot *Booter, ready chan<- bool) error {
+ 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"]
+
+ ctx, cancel := context.WithCancel(ctx)
+ defer cancel()
+
+ go func() {
+ for {
+ if ctx.Err() != nil {
+ return
+ }
+ 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 {
+ boot.logger.WithError(err).Error("db open failed")
+ cancel()
+ return
+ }
+ defer db.Close()
+ conn, err := db.Conn(ctx)
+ if err != nil {
+ boot.logger.WithError(err).Error("db conn failed")
+ cancel()
+ return
+ }
+ 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 {
+ boot.logger.WithError(err).Error("createuser failed")
+ cancel()
+ return
+ }
+ _, err = conn.ExecContext(ctx, `CREATE DATABASE `+pq.QuoteIdentifier(boot.cluster.PostgreSQL.Connection["dbname"]))
+ if err != nil {
+ boot.logger.WithError(err).Error("createdb failed")
+ cancel()
+ return
+ }
+ close(ready)
+ return
+ }()
+
+ return 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"],
+ )
+}