16552: Avoid port collision due to port assignment races in tests.
[arvados.git] / lib / boot / nginx.go
index a7c09a7227913ca66ed43d8040c6e0dd1919583f..b391c4dc8c93f61f50d221bb116cf71da3c10960 100644 (file)
@@ -9,10 +9,12 @@ import (
        "fmt"
        "io/ioutil"
        "net"
+       "net/url"
        "os"
        "os/exec"
        "path/filepath"
        "regexp"
+       "strings"
 
        "git.arvados.org/arvados.git/sdk/go/arvados"
 )
@@ -30,13 +32,36 @@ func (runNginx) Run(ctx context.Context, fail func(error), super *Supervisor) er
        if err != nil {
                return err
        }
+       extListenHost := "0.0.0.0"
+       if super.ClusterType == "test" {
+               // Our dynamic port number assignment strategy (choose
+               // an available port, write it in a config file, and
+               // have another process/goroutine bind to it) is prone
+               // to races when used by concurrent supervisors. In
+               // test mode we don't accept remote connections, so we
+               // can avoid collisions by using the per-cluster
+               // loopback address instead of 0.0.0.0.
+               extListenHost = super.ListenHost
+       }
        vars := map[string]string{
-               "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.tempdir,
+               "LISTENHOST":       extListenHost,
+               "UPSTREAMHOST":     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,
+               "ARVADOS_API_HOST": super.cluster.Services.Controller.ExternalURL.Host,
+       }
+       u := url.URL(super.cluster.Services.Controller.ExternalURL)
+       ctrlHost := u.Hostname()
+       if strings.HasPrefix(super.cluster.TLS.Certificate, "file:/") && strings.HasPrefix(super.cluster.TLS.Key, "file:/") {
+               vars["SSLCERT"] = filepath.Clean(super.cluster.TLS.Certificate[5:])
+               vars["SSLKEY"] = filepath.Clean(super.cluster.TLS.Key[5:])
+       } else 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"
        }
        for _, cmpt := range []struct {
                varname string
@@ -49,14 +74,20 @@ func (runNginx) Run(ctx context.Context, fail func(error), super *Supervisor) er
                {"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},
        } {
-               port, err := internalPort(cmpt.svc)
-               if err != nil {
+               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)
-               }
-               if ok, err := addrIsLocal(net.JoinHostPort(super.ListenHost, port)); !ok || err != nil {
-                       return fmt.Errorf("urlIsLocal() failed for host %q port %q: %v", super.ListenHost, port, err)
+               } 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+"PORT"] = port
 
@@ -64,8 +95,9 @@ func (runNginx) Run(ctx context.Context, fail func(error), super *Supervisor) er
                if err != nil {
                        return fmt.Errorf("%s external port: %w (%v)", cmpt.varname, err, cmpt.svc)
                }
-               if ok, err := addrIsLocal(net.JoinHostPort(super.ListenHost, port)); !ok || err != nil {
-                       return fmt.Errorf("urlIsLocal() failed for host %q port %q: %v", super.ListenHost, port, err)
+               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
        }
@@ -99,13 +131,22 @@ func (runNginx) Run(ctx context.Context, fail func(error), super *Supervisor) er
                        }
                }
        }
+
+       configs := "error_log stderr info; "
+       configs += "pid " + filepath.Join(super.wwwtempdir, "nginx.pid") + "; "
+       configs += "user www-data; "
+
        super.waitShutdown.Add(1)
        go func() {
                defer super.waitShutdown.Done()
-               fail(super.RunProgram(ctx, ".", nil, nil, nginx,
-                       "-g", "error_log stderr info;",
-                       "-g", "pid "+filepath.Join(super.tempdir, "nginx.pid")+";",
-                       "-c", conffile))
+               fail(super.RunProgram(ctx, ".", runOptions{}, nginx, "-g", configs, "-c", conffile))
        }()
-       return waitForConnect(ctx, super.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)
 }