17344: Fix available-port check.
[arvados.git] / lib / install / init.go
index 3eeac5c54984c101587e4df93d72b2a3adf643ab..2992b6677e78c8f5154510787aeb7e6461205601 100644 (file)
@@ -13,6 +13,8 @@ import (
        "flag"
        "fmt"
        "io"
+       "net"
+       "net/http"
        "net/url"
        "os"
        "os/exec"
@@ -20,7 +22,9 @@ import (
        "regexp"
        "strconv"
        "strings"
+       "sync/atomic"
        "text/template"
+       "time"
 
        "git.arvados.org/arvados.git/lib/cmd"
        "git.arvados.org/arvados.git/lib/config"
@@ -111,6 +115,20 @@ func (initcmd *initCommand) RunCommand(prog string, args []string, stdin io.Read
                return 1
        }
 
+       ports := []int{443}
+       for i := 4440; i < 4460; i++ {
+               ports = append(ports, i)
+       }
+       if initcmd.TLS == "acme" {
+               ports = append(ports, 80)
+       }
+       for _, port := range ports {
+               err = initcmd.checkPort(ctx, fmt.Sprintf("%d", port))
+               if err != nil {
+                       return 1
+               }
+       }
+
        // Do the "create extension" thing early. This way, if there's
        // no local postgresql server (a likely failure mode), we can
        // bail out without any side effects, and the user can start
@@ -361,7 +379,7 @@ func (initcmd *initCommand) RunCommand(prog string, args []string, stdin io.Read
                        err = fmt.Errorf("%v: %w", cmd.Args, err)
                        return 1
                }
-               cmd = exec.CommandContext(ctx, "arv", "root", "keep", "docker", "alpine")
+               cmd = exec.CommandContext(ctx, "arv", "sudo", "keep", "docker", "alpine")
                cmd.Stdout = stderr
                cmd.Stderr = stderr
                err = cmd.Run()
@@ -417,3 +435,85 @@ func (initcmd *initCommand) createDB(ctx context.Context, dbconn arvados.Postgre
        }
        return nil
 }
+
+// Confirm that http://{initcmd.Domain}:{port} reaches a server that
+// we run on {port}.
+//
+// If port is "80", listening fails, and Nginx appears to be using the
+// debian-packaged default configuration that listens on port 80,
+// disable that Nginx config and try again.
+//
+// (Typically, the reason Nginx is installed is so that Arvados can
+// run an Nginx child process; the default Nginx service using config
+// from /etc/nginx is just an unfortunate side effect of installing
+// Nginx by way of the Debian package.)
+func (initcmd *initCommand) checkPort(ctx context.Context, port string) error {
+       err := initcmd.checkPortOnce(ctx, port)
+       if err == nil || port != "80" {
+               // success, or poking Nginx in the eye won't help
+               return err
+       }
+       d, err2 := os.Open("/etc/nginx/sites-enabled/.")
+       if err2 != nil {
+               return err
+       }
+       fis, err2 := d.Readdir(-1)
+       if err2 != nil || len(fis) != 1 {
+               return err
+       }
+       if target, err2 := os.Readlink("/etc/nginx/sites-enabled/default"); err2 != nil || target != "/etc/nginx/sites-available/default" {
+               return err
+       }
+       err2 = os.Remove("/etc/nginx/sites-enabled/default")
+       if err2 != nil {
+               return err
+       }
+       exec.CommandContext(ctx, "nginx", "-s", "reload").Run()
+       time.Sleep(time.Second)
+       return initcmd.checkPortOnce(ctx, port)
+}
+
+// Start an http server on 0.0.0.0:{port} and confirm that
+// http://{initcmd.Domain}:{port} reaches that server.
+func (initcmd *initCommand) checkPortOnce(ctx context.Context, port string) error {
+       b := make([]byte, 128)
+       _, err := rand.Read(b)
+       if err != nil {
+               return err
+       }
+       token := fmt.Sprintf("%x", b)
+
+       srv := http.Server{
+               Addr: net.JoinHostPort("", port),
+               Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+                       fmt.Fprint(w, token)
+               })}
+       var errServe atomic.Value
+       go func() {
+               errServe.Store(srv.ListenAndServe())
+       }()
+       defer srv.Close()
+       url := "http://" + net.JoinHostPort(initcmd.Domain, port) + "/probe"
+       req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
+       if err != nil {
+               return err
+       }
+       resp, err := http.DefaultClient.Do(req)
+       if err == nil {
+               defer resp.Body.Close()
+       }
+       if errServe, _ := errServe.Load().(error); errServe != nil {
+               // If server already exited, return that error
+               // (probably "can't listen"), not the request error.
+               return errServe
+       }
+       if err != nil {
+               return err
+       }
+       buf := make([]byte, len(token))
+       n, err := io.ReadFull(resp.Body, buf)
+       if string(buf[:n]) != token {
+               return fmt.Errorf("listened on port %s but %s connected to something else, returned %q, err %v", port, url, buf[:n], err)
+       }
+       return nil
+}