16053: Use setuidgid instead of sudo to drop privileges.
[arvados.git] / lib / install / deps.go
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 package install
6
7 import (
8         "bufio"
9         "bytes"
10         "context"
11         "flag"
12         "fmt"
13         "io"
14         "os"
15         "os/exec"
16         "strconv"
17         "strings"
18
19         "git.arvados.org/arvados.git/lib/cmd"
20         "git.arvados.org/arvados.git/sdk/go/ctxlog"
21 )
22
23 var Command cmd.Handler = installCommand{}
24
25 type installCommand struct{}
26
27 func (installCommand) RunCommand(prog string, args []string, stdin io.Reader, stdout, stderr io.Writer) int {
28         logger := ctxlog.New(stderr, "text", "info")
29         ctx := ctxlog.Context(context.Background(), logger)
30         ctx, cancel := context.WithCancel(ctx)
31         defer cancel()
32
33         var err error
34         defer func() {
35                 if err != nil {
36                         logger.WithError(err).Info("exiting")
37                 }
38         }()
39
40         flags := flag.NewFlagSet(prog, flag.ContinueOnError)
41         flags.SetOutput(stderr)
42         versionFlag := flags.Bool("version", false, "Write version information to stdout and exit 0")
43         clusterType := flags.String("type", "production", "cluster `type`: development, test, or production")
44         err = flags.Parse(args)
45         if err == flag.ErrHelp {
46                 err = nil
47                 return 0
48         } else if err != nil {
49                 return 2
50         } else if *versionFlag {
51                 return cmd.Version.RunCommand(prog, args, stdin, stdout, stderr)
52         }
53
54         var dev, test, prod bool
55         switch *clusterType {
56         case "development":
57                 dev = true
58         case "test":
59                 test = true
60         case "production":
61                 prod = true
62         default:
63                 err = fmt.Errorf("cluster type must be 'development', 'test', or 'production'")
64                 return 2
65         }
66
67         osv, err := identifyOS()
68         if err != nil {
69                 return 1
70         }
71
72         listdir, err := os.Open("/var/lib/apt/lists")
73         if err != nil {
74                 logger.Warnf("error while checking whether to run apt-get update: %s", err)
75         } else if names, _ := listdir.Readdirnames(1); len(names) == 0 {
76                 // Special case for a base docker image where the
77                 // package cache has been deleted and all "apt-get
78                 // install" commands will fail unless we fetch repos.
79                 cmd := exec.CommandContext(ctx, "apt-get", "update")
80                 cmd.Stdout = stdout
81                 cmd.Stderr = stderr
82                 err = cmd.Run()
83                 if err != nil {
84                         return 1
85                 }
86         }
87
88         if dev || test {
89                 debs := []string{
90                         "bison",
91                         "bsdmainutils",
92                         "build-essential",
93                         "cadaver",
94                         "curl",
95                         "cython",
96                         "daemontools", // lib/boot uses setuidgid to drop privileges when running as root
97                         "fuse",
98                         "gettext",
99                         "git",
100                         "gitolite3",
101                         "graphviz",
102                         "haveged",
103                         "iceweasel",
104                         "libattr1-dev",
105                         "libcrypt-ssleay-perl",
106                         "libcrypt-ssleay-perl",
107                         "libcurl3-gnutls",
108                         "libcurl4-openssl-dev",
109                         "libfuse-dev",
110                         "libgnutls28-dev",
111                         "libjson-perl",
112                         "libjson-perl",
113                         "libpam-dev",
114                         "libpcre3-dev",
115                         "libpq-dev",
116                         "libpython2.7-dev",
117                         "libreadline-dev",
118                         "libssl-dev",
119                         "libwww-perl",
120                         "libxml2-dev",
121                         "libxslt1.1",
122                         "linkchecker",
123                         "lsof",
124                         "net-tools",
125                         "nginx",
126                         "pandoc",
127                         "perl-modules",
128                         "pkg-config",
129                         "postgresql",
130                         "postgresql-contrib",
131                         "python",
132                         "python3-dev",
133                         "python-epydoc",
134                         "r-base",
135                         "r-cran-testthat",
136                         "sudo",
137                         "virtualenv",
138                         "wget",
139                         "xvfb",
140                         "zlib1g-dev",
141                 }
142                 switch {
143                 case osv.Debian && osv.Major >= 10:
144                         debs = append(debs, "libcurl4")
145                 default:
146                         debs = append(debs, "libcurl3")
147                 }
148                 cmd := exec.CommandContext(ctx, "apt-get", "install", "--yes", "--no-install-recommends")
149                 cmd.Args = append(cmd.Args, debs...)
150                 cmd.Env = append(os.Environ(), "DEBIAN_FRONTEND=noninteractive")
151                 cmd.Stdout = stdout
152                 cmd.Stderr = stderr
153                 err = cmd.Run()
154                 if err != nil {
155                         return 1
156                 }
157         }
158
159         os.Mkdir("/var/lib/arvados", 0755)
160         rubyversion := "2.5.7"
161         if haverubyversion, err := exec.Command("/var/lib/arvados/bin/ruby", "-v").CombinedOutput(); err == nil && bytes.HasPrefix(haverubyversion, []byte("ruby "+rubyversion)) {
162                 logger.Print("ruby " + rubyversion + " already installed")
163         } else {
164                 err = runBash(`
165 mkdir -p /var/lib/arvados/src
166 cd /var/lib/arvados/src
167 wget -c https://cache.ruby-lang.org/pub/ruby/2.5/ruby-`+rubyversion+`.tar.gz
168 tar xzf ruby-`+rubyversion+`.tar.gz
169 cd ruby-`+rubyversion+`
170 ./configure --disable-install-doc --prefix /var/lib/arvados
171 make -j4
172 make install
173 /var/lib/arvados/bin/gem install bundler
174 cd ..
175 rm -r ruby-`+rubyversion+` ruby-`+rubyversion+`.tar.gz
176 `, stdout, stderr)
177                 if err != nil {
178                         return 1
179                 }
180         }
181
182         if !prod {
183                 goversion := "1.14"
184                 if havegoversion, err := exec.Command("/usr/local/bin/go", "version").CombinedOutput(); err == nil && bytes.HasPrefix(havegoversion, []byte("go version go"+goversion+" ")) {
185                         logger.Print("go " + goversion + " already installed")
186                 } else {
187                         err = runBash(`
188 cd /tmp
189 wget -O- https://storage.googleapis.com/golang/go`+goversion+`.linux-amd64.tar.gz | tar -C /var/lib/arvados -xzf -
190 ln -sf /var/lib/arvados/go/bin/* /usr/local/bin/
191 `, stdout, stderr)
192                         if err != nil {
193                                 return 1
194                         }
195                 }
196
197                 pjsversion := "1.9.8"
198                 if havepjsversion, err := exec.Command("/usr/local/bin/phantomjs", "--version").CombinedOutput(); err == nil && string(havepjsversion) == "1.9.8\n" {
199                         logger.Print("phantomjs " + pjsversion + " already installed")
200                 } else {
201                         err = runBash(`
202 PJS=phantomjs-`+pjsversion+`-linux-x86_64
203 wget -O- https://bitbucket.org/ariya/phantomjs/downloads/$PJS.tar.bz2 | tar -C /var/lib/arvados -xjf -
204 ln -sf /var/lib/arvados/$PJS/bin/phantomjs /usr/local/bin/
205 `, stdout, stderr)
206                         if err != nil {
207                                 return 1
208                         }
209                 }
210
211                 geckoversion := "0.24.0"
212                 if havegeckoversion, err := exec.Command("/usr/local/bin/geckodriver", "--version").CombinedOutput(); err == nil && strings.Contains(string(havegeckoversion), " "+geckoversion+" ") {
213                         logger.Print("geckodriver " + geckoversion + " already installed")
214                 } else {
215                         err = runBash(`
216 GD=v`+geckoversion+`
217 wget -O- https://github.com/mozilla/geckodriver/releases/download/$GD/geckodriver-$GD-linux64.tar.gz | tar -C /var/lib/arvados/bin -xzf - geckodriver
218 ln -sf /var/lib/arvados/bin/geckodriver /usr/local/bin/
219 `, stdout, stderr)
220                         if err != nil {
221                                 return 1
222                         }
223                 }
224
225                 nodejsversion := "v8.15.1"
226                 if havenodejsversion, err := exec.Command("/usr/local/bin/node", "--version").CombinedOutput(); err == nil && string(havenodejsversion) == nodejsversion+"\n" {
227                         logger.Print("nodejs " + nodejsversion + " already installed")
228                 } else {
229                         err = runBash(`
230 NJS=`+nodejsversion+`
231 wget -O- https://nodejs.org/dist/${NJS}/node-${NJS}-linux-x64.tar.xz | sudo tar -C /var/lib/arvados -xJf -
232 ln -sf /var/lib/arvados/node-${NJS}-linux-x64/bin/{node,npm} /usr/local/bin/
233 `, stdout, stderr)
234                         if err != nil {
235                                 return 1
236                         }
237                 }
238         }
239
240         return 0
241 }
242
243 type osversion struct {
244         Debian bool
245         Ubuntu bool
246         Major  int
247 }
248
249 func identifyOS() (osversion, error) {
250         var osv osversion
251         f, err := os.Open("/etc/os-release")
252         if err != nil {
253                 return osv, err
254         }
255         defer f.Close()
256
257         kv := map[string]string{}
258         scanner := bufio.NewScanner(f)
259         for scanner.Scan() {
260                 line := strings.TrimSpace(scanner.Text())
261                 if strings.HasPrefix(line, "#") {
262                         continue
263                 }
264                 toks := strings.SplitN(line, "=", 2)
265                 if len(toks) != 2 {
266                         return osv, fmt.Errorf("invalid line in /etc/os-release: %q", line)
267                 }
268                 k := toks[0]
269                 v := strings.Trim(toks[1], `"`)
270                 if v == toks[1] {
271                         v = strings.Trim(v, `'`)
272                 }
273                 kv[k] = v
274         }
275         if err = scanner.Err(); err != nil {
276                 return osv, err
277         }
278         switch kv["ID"] {
279         case "ubuntu":
280                 osv.Ubuntu = true
281         case "debian":
282                 osv.Debian = true
283         default:
284                 return osv, fmt.Errorf("unsupported ID in /etc/os-release: %q", kv["ID"])
285         }
286         vstr := kv["VERSION_ID"]
287         if i := strings.Index(vstr, "."); i > 0 {
288                 vstr = vstr[:i]
289         }
290         osv.Major, err = strconv.Atoi(vstr)
291         if err != nil {
292                 return osv, fmt.Errorf("incomprehensible VERSION_ID in /etc/os/release: %q", kv["VERSION_ID"])
293         }
294         return osv, nil
295 }
296
297 func runBash(script string, stdout, stderr io.Writer) error {
298         cmd := exec.Command("bash", "-")
299         cmd.Stdin = bytes.NewBufferString("set -ex -o pipefail\n" + script)
300         cmd.Stdout = stdout
301         cmd.Stderr = stderr
302         return cmd.Run()
303 }