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