e0eec5ee4a047a27fba930d3f2b60346caa1e19c
[arvados.git] / lib / boot / passenger.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         "bytes"
9         "context"
10         "fmt"
11         "os"
12         "path/filepath"
13         "strings"
14         "sync"
15
16         "git.arvados.org/arvados.git/sdk/go/arvados"
17 )
18
19 // Don't trust "passenger-config" (or "bundle install") to handle
20 // concurrent installs.
21 var passengerInstallMutex sync.Mutex
22
23 var railsEnv = []string{
24         "ARVADOS_RAILS_LOG_TO_STDOUT=1",
25         "ARVADOS_CONFIG_NOLEGACY=1", // don't load database.yml from source tree
26 }
27
28 // Install a Rails application's dependencies, including phusion
29 // passenger.
30 type installPassenger struct {
31         src       string // path to app in source tree
32         varlibdir string // path to app (relative to /var/lib/arvados) in OS package: "railsapi" or "workbench1"
33         depends   []supervisedTask
34 }
35
36 func (runner installPassenger) String() string {
37         return "installPassenger:" + runner.src
38 }
39
40 func (runner installPassenger) Run(ctx context.Context, fail func(error), super *Supervisor) error {
41         if super.ClusterType == "production" {
42                 // passenger has already been installed via package
43                 return nil
44         }
45         err := super.wait(ctx, runner.depends...)
46         if err != nil {
47                 return err
48         }
49
50         passengerInstallMutex.Lock()
51         defer passengerInstallMutex.Unlock()
52
53         appdir := runner.src
54         if super.ClusterType == "test" {
55                 // In the multi-cluster test setup, if we run multiple
56                 // Rails instances directly from the source tree, they
57                 // step on one another's files in {source}/tmp, log,
58                 // etc. So instead we copy the source directory into a
59                 // temp dir and run the Rails app from there.
60                 appdir = filepath.Join(super.tempdir, runner.varlibdir)
61                 err = super.RunProgram(ctx, super.tempdir, runOptions{}, "mkdir", "-p", appdir)
62                 if err != nil {
63                         return err
64                 }
65                 err = super.RunProgram(ctx, filepath.Join(super.SourcePath, runner.src), runOptions{}, "rsync",
66                         "-a", "--no-owner", "--no-group", "--delete-after", "--delete-excluded",
67                         "--exclude", "/coverage",
68                         "--exclude", "/log",
69                         "--exclude", "/node_modules",
70                         "--exclude", "/tmp",
71                         "--exclude", "/public/assets",
72                         "--exclude", "/vendor",
73                         "--exclude", "/config/environments",
74                         "./",
75                         appdir+"/")
76                 if err != nil {
77                         return err
78                 }
79         }
80
81         var buf bytes.Buffer
82         err = super.RunProgram(ctx, appdir, runOptions{output: &buf}, "gem", "list", "--details", "bundler")
83         if err != nil {
84                 return err
85         }
86         for _, version := range []string{"2.2.19"} {
87                 if !strings.Contains(buf.String(), "("+version+")") {
88                         err = super.RunProgram(ctx, appdir, runOptions{}, "gem", "install", "--user", "--conservative", "--no-document", "bundler:2.2.19")
89                         if err != nil {
90                                 return err
91                         }
92                         break
93                 }
94         }
95         err = super.RunProgram(ctx, appdir, runOptions{}, "bundle", "config", "--set", "local", "path", filepath.Join(os.Getenv("HOME"), ".gem"))
96         if err != nil {
97                 return err
98         }
99         err = super.RunProgram(ctx, appdir, runOptions{}, "bundle", "install", "--jobs", "4")
100         if err != nil {
101                 return err
102         }
103         err = super.RunProgram(ctx, appdir, runOptions{}, "bundle", "exec", "passenger-config", "build-native-support")
104         if err != nil {
105                 return err
106         }
107         err = super.RunProgram(ctx, appdir, runOptions{}, "bundle", "exec", "passenger-config", "install-standalone-runtime")
108         if err != nil {
109                 return err
110         }
111         err = super.RunProgram(ctx, appdir, runOptions{}, "bundle", "exec", "passenger-config", "validate-install")
112         if err != nil && !strings.Contains(err.Error(), "exit status 2") {
113                 // Exit code 2 indicates there were warnings (like
114                 // "other passenger installations have been detected",
115                 // which we can't expect to avoid) but no errors.
116                 // Other non-zero exit codes (1, 9) indicate errors.
117                 return err
118         }
119         return nil
120 }
121
122 type runPassenger struct {
123         src       string // path to app in source tree
124         varlibdir string // path to app (relative to /var/lib/arvados) in OS package: "railsapi" or "workbench1"
125         svc       arvados.Service
126         depends   []supervisedTask
127 }
128
129 func (runner runPassenger) String() string {
130         return "runPassenger:" + runner.src
131 }
132
133 func (runner runPassenger) Run(ctx context.Context, fail func(error), super *Supervisor) error {
134         err := super.wait(ctx, runner.depends...)
135         if err != nil {
136                 return err
137         }
138         host, port, err := internalPort(runner.svc)
139         if err != nil {
140                 return fmt.Errorf("bug: no internalPort for %q: %v (%#v)", runner, err, runner.svc)
141         }
142         var appdir string
143         switch super.ClusterType {
144         case "production":
145                 appdir = "/var/lib/arvados/" + runner.varlibdir
146         case "test":
147                 appdir = filepath.Join(super.tempdir, runner.varlibdir)
148         default:
149                 appdir = runner.src
150         }
151         loglevel := "4"
152         if lvl, ok := map[string]string{
153                 "debug":   "5",
154                 "info":    "4",
155                 "warn":    "2",
156                 "warning": "2",
157                 "error":   "1",
158                 "fatal":   "0",
159                 "panic":   "0",
160         }[super.cluster.SystemLogs.LogLevel]; ok {
161                 loglevel = lvl
162         }
163         super.waitShutdown.Add(1)
164         go func() {
165                 defer super.waitShutdown.Done()
166                 cmdline := []string{
167                         "bundle", "exec",
168                         "passenger", "start",
169                         "--address", host,
170                         "--port", port,
171                         "--log-level", loglevel,
172                         "--no-friendly-error-pages",
173                         "--disable-anonymous-telemetry",
174                         "--disable-security-update-check",
175                         "--no-compile-runtime",
176                         "--no-install-runtime",
177                         "--pid-file", filepath.Join(super.wwwtempdir, "passenger."+strings.Replace(appdir, "/", "_", -1)+".pid"),
178                 }
179                 opts := runOptions{
180                         env: append([]string{
181                                 "TMPDIR=" + super.wwwtempdir,
182                         }, railsEnv...),
183                 }
184                 if super.ClusterType == "production" {
185                         opts.user = "www-data"
186                         opts.env = append(opts.env, "HOME=/var/www")
187                 } else {
188                         // This would be desirable when changing uid
189                         // too, but it fails because /dev/stderr is a
190                         // symlink to a pty owned by root: "nginx:
191                         // [emerg] open() "/dev/stderr" failed (13:
192                         // Permission denied)"
193                         cmdline = append(cmdline, "--log-file", "/dev/stderr")
194                 }
195                 err = super.RunProgram(ctx, appdir, opts, cmdline[0], cmdline[1:]...)
196                 fail(err)
197         }()
198         return nil
199 }