18700: Add workbench2 to arvados-boot.
[arvados.git] / lib / boot / nginx.go
index c5bfd605afac4485bd391c6d87cc5016eddfc0de..44bcbc395432e6ba328adfc9090212acd5ed58cd 100644 (file)
@@ -8,51 +8,92 @@ import (
        "context"
        "fmt"
        "io/ioutil"
+       "net"
+       "net/url"
        "os"
        "os/exec"
+       "os/user"
        "path/filepath"
        "regexp"
 
        "git.arvados.org/arvados.git/sdk/go/arvados"
 )
 
+// Run an Nginx process that proxies the supervisor's configured
+// ExternalURLs to the appropriate InternalURLs.
 type runNginx struct{}
 
 func (runNginx) String() string {
        return "nginx"
 }
 
-func (runNginx) Run(ctx context.Context, fail func(error), boot *Booter) error {
+func (runNginx) Run(ctx context.Context, fail func(error), super *Supervisor) error {
+       err := super.wait(ctx, createCertificates{})
+       if err != nil {
+               return err
+       }
        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,
+               "LISTENHOST": super.ListenHost,
+               "SSLCERT":    filepath.Join(super.tempdir, "server.crt"),
+               "SSLKEY":     filepath.Join(super.tempdir, "server.key"),
+               "ACCESSLOG":  filepath.Join(super.tempdir, "nginx_access.log"),
+               "ERRORLOG":   filepath.Join(super.tempdir, "nginx_error.log"),
+               "TMPDIR":     super.wwwtempdir,
+       }
+       ctrlHost, _, err := net.SplitHostPort(super.cluster.Services.Controller.ExternalURL.Host)
+       if err != nil {
+               return fmt.Errorf("SplitHostPort(Controller.ExternalURL.Host): %w", err)
+       }
+       if f, err := os.Open("/var/lib/acme/live/" + ctrlHost + "/privkey"); err == nil {
+               f.Close()
+               vars["SSLCERT"] = "/var/lib/acme/live/" + ctrlHost + "/cert"
+               vars["SSLKEY"] = "/var/lib/acme/live/" + ctrlHost + "/privkey"
        }
-       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},
+               {"CONTROLLER", super.cluster.Services.Controller},
+               {"KEEPWEB", super.cluster.Services.WebDAV},
+               {"KEEPWEBDL", super.cluster.Services.WebDAVDownload},
+               {"KEEPPROXY", super.cluster.Services.Keepproxy},
+               {"GIT", super.cluster.Services.GitHTTP},
+               {"HEALTH", super.cluster.Services.Health},
+               {"WORKBENCH1", super.cluster.Services.Workbench1},
+               {"WORKBENCH2", super.cluster.Services.Workbench2},
+               {"WS", super.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)
+               var host, port string
+               if len(cmpt.svc.InternalURLs) == 0 {
+                       // We won't run this service, but we need an
+                       // upstream port to write in our templated
+                       // nginx config. Choose a port that will
+                       // return 502 Bad Gateway.
+                       port = "9"
+               } else if host, port, err = internalPort(cmpt.svc); err != nil {
+                       return fmt.Errorf("%s internal port: %w (%v)", cmpt.varname, err, cmpt.svc)
+               } else if ok, err := addrIsLocal(net.JoinHostPort(host, port)); !ok || err != nil {
+                       return fmt.Errorf("%s addrIsLocal() failed for host %q port %q: %v", cmpt.varname, host, port, err)
                }
-               vars[cmpt.varname+"SSLPORT"], err = externalPort(cmpt.svc)
+               vars[cmpt.varname+"PORT"] = port
+
+               port, err = externalPort(cmpt.svc)
                if err != nil {
-                       return fmt.Errorf("%s external port: %s (%v)", cmpt.varname, err, cmpt.svc)
+                       return fmt.Errorf("%s external port: %w (%v)", cmpt.varname, err, cmpt.svc)
                }
+               listenAddr := net.JoinHostPort(super.ListenHost, port)
+               if ok, err := addrIsLocal(listenAddr); !ok || err != nil {
+                       return fmt.Errorf("%s addrIsLocal(%q) failed: %w", cmpt.varname, listenAddr, err)
+               }
+               vars[cmpt.varname+"SSLPORT"] = port
+       }
+       var conftemplate string
+       if super.ClusterType == "production" {
+               conftemplate = "/var/lib/arvados/share/nginx.conf"
+       } else {
+               conftemplate = filepath.Join(super.SourcePath, "sdk", "python", "tests", "nginx.conf")
        }
-       tmpl, err := ioutil.ReadFile(filepath.Join(boot.SourcePath, "sdk", "python", "tests", "nginx.conf"))
+       tmpl, err := ioutil.ReadFile(conftemplate)
        if err != nil {
                return err
        }
@@ -62,7 +103,7 @@ func (runNginx) Run(ctx context.Context, fail func(error), boot *Booter) error {
                }
                return vars[src[2:len(src)-2]]
        })
-       conffile := filepath.Join(boot.tempdir, "nginx.conf")
+       conffile := filepath.Join(super.tempdir, "nginx.conf")
        err = ioutil.WriteFile(conffile, []byte(conf), 0755)
        if err != nil {
                return err
@@ -76,11 +117,32 @@ func (runNginx) Run(ctx context.Context, fail func(error), boot *Booter) error {
                        }
                }
        }
+
+       args := []string{
+               "-g", "error_log stderr info;",
+               "-g", "pid " + filepath.Join(super.wwwtempdir, "nginx.pid") + ";",
+               "-c", conffile,
+       }
+       // Nginx ignores "user www-data;" when running as a non-root
+       // user... except that it causes it to ignore our other -g
+       // options. So we still have to decide for ourselves whether
+       // it's needed.
+       if u, err := user.Current(); err != nil {
+               return fmt.Errorf("user.Current(): %w", err)
+       } else if u.Uid == "0" {
+               args = append([]string{"-g", "user www-data;"}, args...)
+       }
+
+       super.waitShutdown.Add(1)
        go func() {
-               fail(boot.RunProgram(ctx, ".", nil, nil, nginx,
-                       "-g", "error_log stderr info;",
-                       "-g", "pid "+filepath.Join(boot.tempdir, "nginx.pid")+";",
-                       "-c", conffile))
+               defer super.waitShutdown.Done()
+               fail(super.RunProgram(ctx, ".", runOptions{}, nginx, args...))
        }()
-       return waitForConnect(ctx, boot.cluster.Services.Controller.ExternalURL.Host)
+       // Choose one of the ports where Nginx should listen, and wait
+       // here until we can connect. If ExternalURL is https://foo (with no port) then we connect to "foo:https"
+       testurl := url.URL(super.cluster.Services.Controller.ExternalURL)
+       if testurl.Port() == "" {
+               testurl.Host = net.JoinHostPort(testurl.Host, testurl.Scheme)
+       }
+       return waitForConnect(ctx, testurl.Host)
 }