18995: Merge branch 'main' into 18995-code-cleanup-1
[arvados.git] / lib / boot / supervisor.go
index c75e7f146e3ce2b2734575804aa25c477839efdc..762581e54e09dcf11008f6f43d15edb03202c29b 100644 (file)
@@ -42,6 +42,9 @@ type Supervisor struct {
        ClusterType          string // e.g., production
        ListenHost           string // e.g., localhost
        ControllerAddr       string // e.g., 127.0.0.1:8000
+       Workbench2Source     string // e.g., /home/username/src/arvados-workbench2
+       NoWorkbench1         bool
+       NoWorkbench2         bool
        OwnTemporaryDatabase bool
        Stderr               io.Writer
 
@@ -57,8 +60,8 @@ type Supervisor struct {
        waitShutdown  sync.WaitGroup
 
        bindir     string
-       tempdir    string
-       wwwtempdir string
+       tempdir    string // in production mode, this is accessible only to root
+       wwwtempdir string // in production mode, this is accessible only to www-data
        configfile string
        environ    []string // for child processes
 }
@@ -243,16 +246,25 @@ func (super *Supervisor) run(cfg *arvados.Config) error {
                runServiceCommand{name: "controller", svc: super.cluster.Services.Controller, depends: []supervisedTask{seedDatabase{}}},
                runGoProgram{src: "services/arv-git-httpd", svc: super.cluster.Services.GitHTTP},
                runGoProgram{src: "services/health", svc: super.cluster.Services.Health},
-               runGoProgram{src: "services/keepproxy", svc: super.cluster.Services.Keepproxy, depends: []supervisedTask{runPassenger{src: "services/api"}}},
-               runGoProgram{src: "services/keepstore", svc: super.cluster.Services.Keepstore},
+               runServiceCommand{name: "keepproxy", svc: super.cluster.Services.Keepproxy, depends: []supervisedTask{runPassenger{src: "services/api"}}},
+               runServiceCommand{name: "keepstore", svc: super.cluster.Services.Keepstore},
                runGoProgram{src: "services/keep-web", svc: super.cluster.Services.WebDAV},
                runServiceCommand{name: "ws", svc: super.cluster.Services.Websocket, depends: []supervisedTask{seedDatabase{}}},
                installPassenger{src: "services/api"},
                runPassenger{src: "services/api", varlibdir: "railsapi", svc: super.cluster.Services.RailsAPI, depends: []supervisedTask{createCertificates{}, seedDatabase{}, installPassenger{src: "services/api"}}},
-               installPassenger{src: "apps/workbench", depends: []supervisedTask{seedDatabase{}}}, // dependency ensures workbench doesn't delay api install/startup
-               runPassenger{src: "apps/workbench", varlibdir: "workbench1", svc: super.cluster.Services.Workbench1, depends: []supervisedTask{installPassenger{src: "apps/workbench"}}},
                seedDatabase{},
        }
+       if !super.NoWorkbench1 {
+               tasks = append(tasks,
+                       installPassenger{src: "apps/workbench", depends: []supervisedTask{seedDatabase{}}}, // dependency ensures workbench doesn't delay api install/startup
+                       runPassenger{src: "apps/workbench", varlibdir: "workbench1", svc: super.cluster.Services.Workbench1, depends: []supervisedTask{installPassenger{src: "apps/workbench"}}},
+               )
+       }
+       if !super.NoWorkbench2 {
+               tasks = append(tasks,
+                       runWorkbench2{svc: super.cluster.Services.Workbench2},
+               )
+       }
        if super.ClusterType != "test" {
                tasks = append(tasks,
                        runServiceCommand{name: "dispatch-cloud", svc: super.cluster.Services.DispatchCloud},
@@ -443,7 +455,7 @@ func (super *Supervisor) setupRubyEnv() error {
                cmd.Env = super.environ
                buf, err := cmd.Output() // /var/lib/arvados/.gem/ruby/2.5.0/bin:...
                if err != nil || len(buf) == 0 {
-                       return fmt.Errorf("gem env gempath: %v", err)
+                       return fmt.Errorf("gem env gempath: %w", err)
                }
                gempath := string(bytes.Split(buf, []byte{':'})[0])
                super.prependEnv("PATH", gempath+"/bin:")
@@ -477,6 +489,7 @@ type runOptions struct {
        output io.Writer // attach stdout
        env    []string  // add/replace environment variables
        user   string    // run as specified user
+       stdin  io.Reader
 }
 
 // RunProgram runs prog with args, using dir as working directory. If ctx is
@@ -520,6 +533,7 @@ func (super *Supervisor) RunProgram(ctx context.Context, dir string, opts runOpt
        }
 
        cmd := exec.Command(super.lookPath(prog), args...)
+       cmd.Stdin = opts.stdin
        stdout, err := cmd.StdoutPipe()
        if err != nil {
                return err
@@ -623,32 +637,40 @@ func (super *Supervisor) autofillConfig(cfg *arvados.Config) error {
                return err
        }
        usedPort := map[string]bool{}
-       nextPort := func(host string) string {
+       nextPort := func(host string) (string, error) {
                for {
                        port, err := availablePort(host)
                        if err != nil {
-                               panic(err)
+                               port, err = availablePort(super.ListenHost)
+                       }
+                       if err != nil {
+                               return "", err
                        }
                        if usedPort[port] {
                                continue
                        }
                        usedPort[port] = true
-                       return port
+                       return port, nil
                }
        }
        if cluster.Services.Controller.ExternalURL.Host == "" {
                h, p, err := net.SplitHostPort(super.ControllerAddr)
                if err != nil {
-                       return err
+                       return fmt.Errorf("SplitHostPort(ControllerAddr): %w", err)
                }
                if h == "" {
                        h = super.ListenHost
                }
                if p == "0" {
-                       p = nextPort(h)
+                       p, err = nextPort(h)
+                       if err != nil {
+                               return err
+                       }
                }
                cluster.Services.Controller.ExternalURL = arvados.URL{Scheme: "https", Host: net.JoinHostPort(h, p), Path: "/"}
        }
+       u := url.URL(cluster.Services.Controller.ExternalURL)
+       defaultExtHost := u.Hostname()
        for _, svc := range []*arvados.Service{
                &cluster.Services.Controller,
                &cluster.Services.DispatchCloud,
@@ -661,26 +683,47 @@ func (super *Supervisor) autofillConfig(cfg *arvados.Config) error {
                &cluster.Services.WebDAVDownload,
                &cluster.Services.Websocket,
                &cluster.Services.Workbench1,
+               &cluster.Services.Workbench2,
        } {
                if svc == &cluster.Services.DispatchCloud && super.ClusterType == "test" {
                        continue
                }
                if svc.ExternalURL.Host == "" {
+                       port, err := nextPort(defaultExtHost)
+                       if err != nil {
+                               return err
+                       }
+                       host := net.JoinHostPort(defaultExtHost, port)
                        if svc == &cluster.Services.Controller ||
                                svc == &cluster.Services.GitHTTP ||
                                svc == &cluster.Services.Health ||
                                svc == &cluster.Services.Keepproxy ||
                                svc == &cluster.Services.WebDAV ||
                                svc == &cluster.Services.WebDAVDownload ||
-                               svc == &cluster.Services.Workbench1 {
-                               svc.ExternalURL = arvados.URL{Scheme: "https", Host: fmt.Sprintf("%s:%s", super.ListenHost, nextPort(super.ListenHost)), Path: "/"}
+                               svc == &cluster.Services.Workbench1 ||
+                               svc == &cluster.Services.Workbench2 {
+                               svc.ExternalURL = arvados.URL{Scheme: "https", Host: host, Path: "/"}
                        } else if svc == &cluster.Services.Websocket {
-                               svc.ExternalURL = arvados.URL{Scheme: "wss", Host: fmt.Sprintf("%s:%s", super.ListenHost, nextPort(super.ListenHost)), Path: "/websocket"}
+                               svc.ExternalURL = arvados.URL{Scheme: "wss", Host: host, Path: "/websocket"}
                        }
                }
+               if super.NoWorkbench1 && svc == &cluster.Services.Workbench1 ||
+                       super.NoWorkbench2 && svc == &cluster.Services.Workbench2 {
+                       // When workbench1 is disabled, it gets an
+                       // ExternalURL (so we have a valid listening
+                       // port to write in our Nginx config) but no
+                       // InternalURLs (so health checker doesn't
+                       // complain).
+                       continue
+               }
                if len(svc.InternalURLs) == 0 {
+                       port, err := nextPort(super.ListenHost)
+                       if err != nil {
+                               return err
+                       }
+                       host := net.JoinHostPort(super.ListenHost, port)
                        svc.InternalURLs = map[arvados.URL]arvados.ServiceInstance{
-                               {Scheme: "http", Host: fmt.Sprintf("%s:%s", super.ListenHost, nextPort(super.ListenHost)), Path: "/"}: {},
+                               {Scheme: "http", Host: host, Path: "/"}: {},
                        }
                }
        }
@@ -708,7 +751,12 @@ func (super *Supervisor) autofillConfig(cfg *arvados.Config) error {
        }
        if super.ClusterType == "test" {
                // Add a second keepstore process.
-               cluster.Services.Keepstore.InternalURLs[arvados.URL{Scheme: "http", Host: fmt.Sprintf("%s:%s", super.ListenHost, nextPort(super.ListenHost)), Path: "/"}] = arvados.ServiceInstance{}
+               port, err := nextPort(super.ListenHost)
+               if err != nil {
+                       return err
+               }
+               host := net.JoinHostPort(super.ListenHost, port)
+               cluster.Services.Keepstore.InternalURLs[arvados.URL{Scheme: "http", Host: host, Path: "/"}] = arvados.ServiceInstance{}
 
                // Create a directory-backed volume for each keepstore
                // process.
@@ -728,14 +776,28 @@ func (super *Supervisor) autofillConfig(cfg *arvados.Config) error {
                                AccessViaHosts: map[arvados.URL]arvados.VolumeAccess{
                                        url: {},
                                },
+                               StorageClasses: map[string]bool{
+                                       "default": true,
+                                       "foo":     true,
+                                       "bar":     true,
+                               },
                        }
                }
+               cluster.StorageClasses = map[string]arvados.StorageClassConfig{
+                       "default": {Default: true},
+                       "foo":     {},
+                       "bar":     {},
+               }
        }
        if super.OwnTemporaryDatabase {
+               port, err := nextPort("localhost")
+               if err != nil {
+                       return err
+               }
                cluster.PostgreSQL.Connection = arvados.PostgreSQLConnection{
                        "client_encoding": "utf8",
                        "host":            "localhost",
-                       "port":            nextPort(super.ListenHost),
+                       "port":            port,
                        "dbname":          "arvados_test",
                        "user":            "arvados",
                        "password":        "insecure_arvados_test",
@@ -747,7 +809,6 @@ func (super *Supervisor) autofillConfig(cfg *arvados.Config) error {
 }
 
 func addrIsLocal(addr string) (bool, error) {
-       return true, nil
        listener, err := net.Listen("tcp", addr)
        if err == nil {
                listener.Close()
@@ -768,21 +829,23 @@ func randomHexString(chars int) string {
        return fmt.Sprintf("%x", b)
 }
 
-func internalPort(svc arvados.Service) (string, error) {
+func internalPort(svc arvados.Service) (host, port string, err error) {
        if len(svc.InternalURLs) > 1 {
-               return "", errors.New("internalPort() doesn't work with multiple InternalURLs")
+               return "", "", errors.New("internalPort() doesn't work with multiple InternalURLs")
        }
        for u := range svc.InternalURLs {
                u := url.URL(u)
-               if p := u.Port(); p != "" {
-                       return p, nil
-               } else if u.Scheme == "https" || u.Scheme == "ws" {
-                       return "443", nil
-               } else {
-                       return "80", nil
+               host, port = u.Hostname(), u.Port()
+               switch {
+               case port != "":
+               case u.Scheme == "https", u.Scheme == "ws":
+                       port = "443"
+               default:
+                       port = "80"
                }
+               return
        }
-       return "", fmt.Errorf("service has no InternalURLs")
+       return "", "", fmt.Errorf("service has no InternalURLs")
 }
 
 func externalPort(svc arvados.Service) (string, error) {