18700: Add workbench2 to arvados-boot.
[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         ctrlHost, _, err := net.SplitHostPort(super.cluster.Services.Controller.ExternalURL.Host)
44         if err != nil {
45                 return fmt.Errorf("SplitHostPort(Controller.ExternalURL.Host): %w", err)
46         }
47         if f, err := os.Open("/var/lib/acme/live/" + ctrlHost + "/privkey"); err == nil {
48                 f.Close()
49                 vars["SSLCERT"] = "/var/lib/acme/live/" + ctrlHost + "/cert"
50                 vars["SSLKEY"] = "/var/lib/acme/live/" + ctrlHost + "/privkey"
51         }
52         for _, cmpt := range []struct {
53                 varname string
54                 svc     arvados.Service
55         }{
56                 {"CONTROLLER", super.cluster.Services.Controller},
57                 {"KEEPWEB", super.cluster.Services.WebDAV},
58                 {"KEEPWEBDL", super.cluster.Services.WebDAVDownload},
59                 {"KEEPPROXY", super.cluster.Services.Keepproxy},
60                 {"GIT", super.cluster.Services.GitHTTP},
61                 {"HEALTH", super.cluster.Services.Health},
62                 {"WORKBENCH1", super.cluster.Services.Workbench1},
63                 {"WORKBENCH2", super.cluster.Services.Workbench2},
64                 {"WS", super.cluster.Services.Websocket},
65         } {
66                 var host, port string
67                 if len(cmpt.svc.InternalURLs) == 0 {
68                         // We won't run this service, but we need an
69                         // upstream port to write in our templated
70                         // nginx config. Choose a port that will
71                         // return 502 Bad Gateway.
72                         port = "9"
73                 } else if host, port, err = internalPort(cmpt.svc); err != nil {
74                         return fmt.Errorf("%s internal port: %w (%v)", cmpt.varname, err, cmpt.svc)
75                 } else if ok, err := addrIsLocal(net.JoinHostPort(host, port)); !ok || err != nil {
76                         return fmt.Errorf("%s addrIsLocal() failed for host %q port %q: %v", cmpt.varname, host, port, err)
77                 }
78                 vars[cmpt.varname+"PORT"] = port
79
80                 port, err = externalPort(cmpt.svc)
81                 if err != nil {
82                         return fmt.Errorf("%s external port: %w (%v)", cmpt.varname, err, cmpt.svc)
83                 }
84                 listenAddr := net.JoinHostPort(super.ListenHost, port)
85                 if ok, err := addrIsLocal(listenAddr); !ok || err != nil {
86                         return fmt.Errorf("%s addrIsLocal(%q) failed: %w", cmpt.varname, listenAddr, err)
87                 }
88                 vars[cmpt.varname+"SSLPORT"] = port
89         }
90         var conftemplate string
91         if super.ClusterType == "production" {
92                 conftemplate = "/var/lib/arvados/share/nginx.conf"
93         } else {
94                 conftemplate = filepath.Join(super.SourcePath, "sdk", "python", "tests", "nginx.conf")
95         }
96         tmpl, err := ioutil.ReadFile(conftemplate)
97         if err != nil {
98                 return err
99         }
100         conf := regexp.MustCompile(`{{.*?}}`).ReplaceAllStringFunc(string(tmpl), func(src string) string {
101                 if len(src) < 4 {
102                         return src
103                 }
104                 return vars[src[2:len(src)-2]]
105         })
106         conffile := filepath.Join(super.tempdir, "nginx.conf")
107         err = ioutil.WriteFile(conffile, []byte(conf), 0755)
108         if err != nil {
109                 return err
110         }
111         nginx := "nginx"
112         if _, err := exec.LookPath(nginx); err != nil {
113                 for _, dir := range []string{"/sbin", "/usr/sbin", "/usr/local/sbin"} {
114                         if _, err = os.Stat(dir + "/nginx"); err == nil {
115                                 nginx = dir + "/nginx"
116                                 break
117                         }
118                 }
119         }
120
121         args := []string{
122                 "-g", "error_log stderr info;",
123                 "-g", "pid " + filepath.Join(super.wwwtempdir, "nginx.pid") + ";",
124                 "-c", conffile,
125         }
126         // Nginx ignores "user www-data;" when running as a non-root
127         // user... except that it causes it to ignore our other -g
128         // options. So we still have to decide for ourselves whether
129         // it's needed.
130         if u, err := user.Current(); err != nil {
131                 return fmt.Errorf("user.Current(): %w", err)
132         } else if u.Uid == "0" {
133                 args = append([]string{"-g", "user www-data;"}, args...)
134         }
135
136         super.waitShutdown.Add(1)
137         go func() {
138                 defer super.waitShutdown.Done()
139                 fail(super.RunProgram(ctx, ".", runOptions{}, nginx, args...))
140         }()
141         // Choose one of the ports where Nginx should listen, and wait
142         // here until we can connect. If ExternalURL is https://foo (with no port) then we connect to "foo:https"
143         testurl := url.URL(super.cluster.Services.Controller.ExternalURL)
144         if testurl.Port() == "" {
145                 testurl.Host = net.JoinHostPort(testurl.Host, testurl.Scheme)
146         }
147         return waitForConnect(ctx, testurl.Host)
148 }