16306: Fix inability to shutdown passenger processes.
[arvados.git] / lib / boot / nginx.go
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 package boot
6
7 import (
8         "context"
9         "fmt"
10         "io/ioutil"
11         "net"
12         "net/url"
13         "os"
14         "os/exec"
15         "os/user"
16         "path/filepath"
17         "regexp"
18
19         "git.arvados.org/arvados.git/sdk/go/arvados"
20 )
21
22 // Run an Nginx process that proxies the supervisor's configured
23 // ExternalURLs to the appropriate InternalURLs.
24 type runNginx struct{}
25
26 func (runNginx) String() string {
27         return "nginx"
28 }
29
30 func (runNginx) Run(ctx context.Context, fail func(error), super *Supervisor) error {
31         err := super.wait(ctx, createCertificates{})
32         if err != nil {
33                 return err
34         }
35         vars := map[string]string{
36                 "LISTENHOST": super.ListenHost,
37                 "SSLCERT":    filepath.Join(super.tempdir, "server.crt"),
38                 "SSLKEY":     filepath.Join(super.tempdir, "server.key"),
39                 "ACCESSLOG":  filepath.Join(super.tempdir, "nginx_access.log"),
40                 "ERRORLOG":   filepath.Join(super.tempdir, "nginx_error.log"),
41                 "TMPDIR":     super.wwwtempdir,
42         }
43         for _, cmpt := range []struct {
44                 varname string
45                 svc     arvados.Service
46         }{
47                 {"CONTROLLER", super.cluster.Services.Controller},
48                 {"KEEPWEB", super.cluster.Services.WebDAV},
49                 {"KEEPWEBDL", super.cluster.Services.WebDAVDownload},
50                 {"KEEPPROXY", super.cluster.Services.Keepproxy},
51                 {"GIT", super.cluster.Services.GitHTTP},
52                 {"HEALTH", super.cluster.Services.Health},
53                 {"WORKBENCH1", super.cluster.Services.Workbench1},
54                 {"WS", super.cluster.Services.Websocket},
55         } {
56                 port, err := internalPort(cmpt.svc)
57                 if err != nil {
58                         return fmt.Errorf("%s internal port: %w (%v)", cmpt.varname, err, cmpt.svc)
59                 }
60                 if ok, err := addrIsLocal(net.JoinHostPort(super.ListenHost, port)); !ok || err != nil {
61                         return fmt.Errorf("urlIsLocal() failed for host %q port %q: %v", super.ListenHost, port, err)
62                 }
63                 vars[cmpt.varname+"PORT"] = port
64
65                 port, err = externalPort(cmpt.svc)
66                 if err != nil {
67                         return fmt.Errorf("%s external port: %w (%v)", cmpt.varname, err, cmpt.svc)
68                 }
69                 if ok, err := addrIsLocal(net.JoinHostPort(super.ListenHost, port)); !ok || err != nil {
70                         return fmt.Errorf("urlIsLocal() failed for host %q port %q: %v", super.ListenHost, port, err)
71                 }
72                 vars[cmpt.varname+"SSLPORT"] = port
73         }
74         var conftemplate string
75         if super.ClusterType == "production" {
76                 conftemplate = "/var/lib/arvados/share/nginx.conf"
77         } else {
78                 conftemplate = filepath.Join(super.SourcePath, "sdk", "python", "tests", "nginx.conf")
79         }
80         tmpl, err := ioutil.ReadFile(conftemplate)
81         if err != nil {
82                 return err
83         }
84         conf := regexp.MustCompile(`{{.*?}}`).ReplaceAllStringFunc(string(tmpl), func(src string) string {
85                 if len(src) < 4 {
86                         return src
87                 }
88                 return vars[src[2:len(src)-2]]
89         })
90         conffile := filepath.Join(super.tempdir, "nginx.conf")
91         err = ioutil.WriteFile(conffile, []byte(conf), 0755)
92         if err != nil {
93                 return err
94         }
95         nginx := "nginx"
96         if _, err := exec.LookPath(nginx); err != nil {
97                 for _, dir := range []string{"/sbin", "/usr/sbin", "/usr/local/sbin"} {
98                         if _, err = os.Stat(dir + "/nginx"); err == nil {
99                                 nginx = dir + "/nginx"
100                                 break
101                         }
102                 }
103         }
104
105         args := []string{
106                 "-g", "error_log stderr info;",
107                 "-g", "pid " + filepath.Join(super.wwwtempdir, "nginx.pid") + ";",
108                 "-c", conffile,
109         }
110         // Nginx ignores "user www-data;" when running as a non-root
111         // user... except that it causes it to ignore our other -g
112         // options. So we still have to decide for ourselves whether
113         // it's needed.
114         if u, err := user.Current(); err != nil {
115                 return fmt.Errorf("user.Current(): %w", err)
116         } else if u.Uid == "0" {
117                 args = append([]string{"-g", "user www-data;"}, args...)
118         }
119
120         super.waitShutdown.Add(1)
121         go func() {
122                 defer super.waitShutdown.Done()
123                 fail(super.RunProgram(ctx, ".", runOptions{}, nginx, args...))
124         }()
125         // Choose one of the ports where Nginx should listen, and wait
126         // here until we can connect. If ExternalURL is https://foo (with no port) then we connect to "foo:https"
127         testurl := url.URL(super.cluster.Services.Controller.ExternalURL)
128         if testurl.Port() == "" {
129                 testurl.Host = net.JoinHostPort(testurl.Host, testurl.Scheme)
130         }
131         return waitForConnect(ctx, testurl.Host)
132 }