blob: adcb475b084322d96f9b3278c16def150d41f9f3 [file] [log] [blame]
Brad Fitzpatrickf3c01932015-01-15 16:29:16 -08001// Copyright 2015 The Go Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE file.
4
Brad Fitzpatrickf3c01932015-01-15 16:29:16 -08005// Package buildlet contains client tools for working with a buildlet
6// server.
Andrew Gerrandfa8373a2015-01-21 17:25:37 +11007package buildlet // import "golang.org/x/build/buildlet"
Brad Fitzpatrickf3c01932015-01-15 16:29:16 -08008
9import (
Brad Fitzpatrickde8994a2015-02-09 13:12:20 -080010 "bufio"
Brad Fitzpatrickf8c24842015-01-16 09:54:03 -080011 "errors"
Brad Fitzpatrickf3c01932015-01-15 16:29:16 -080012 "fmt"
13 "io"
14 "io/ioutil"
Brad Fitzpatrickf68e9f52015-02-03 11:09:50 +000015 "net"
Brad Fitzpatrickf3c01932015-01-15 16:29:16 -080016 "net/http"
Brad Fitzpatrickf8c24842015-01-16 09:54:03 -080017 "net/url"
Brad Fitzpatrickf3c01932015-01-15 16:29:16 -080018 "strings"
Andrew Gerrand376b01d2015-02-03 12:39:25 +000019 "time"
20
21 "golang.org/x/oauth2"
Brad Fitzpatrickf3c01932015-01-15 16:29:16 -080022)
23
Brad Fitzpatrickf3c01932015-01-15 16:29:16 -080024// NewClient returns a *Client that will manipulate ipPort,
25// authenticated using the provided keypair.
26//
27// This constructor returns immediately without testing the host or auth.
Brad Fitzpatrick874c0832015-01-16 12:59:14 -080028func NewClient(ipPort string, kp KeyPair) *Client {
Brad Fitzpatrickf3c01932015-01-15 16:29:16 -080029 return &Client{
Brad Fitzpatrick874c0832015-01-16 12:59:14 -080030 ipPort: ipPort,
31 tls: kp,
32 password: kp.Password(),
33 httpClient: &http.Client{
34 Transport: &http.Transport{
Brad Fitzpatrickf68e9f52015-02-03 11:09:50 +000035 Dial: defaultDialer(),
Brad Fitzpatrick874c0832015-01-16 12:59:14 -080036 DialTLS: kp.tlsDialer(),
37 },
38 },
Brad Fitzpatrickf3c01932015-01-15 16:29:16 -080039 }
40}
41
Brad Fitzpatrickf68e9f52015-02-03 11:09:50 +000042// defaultDialer returns the net/http package's default Dial function.
43// Notably, this sets TCP keep-alive values, so when we kill VMs
44// (whose TCP stacks stop replying, forever), we don't leak file
45// descriptors for otherwise forever-stalled TCP connections.
46func defaultDialer() func(network, addr string) (net.Conn, error) {
47 if fn := http.DefaultTransport.(*http.Transport).Dial; fn != nil {
48 return fn
49 }
50 return net.Dial
51}
52
Brad Fitzpatrickf3c01932015-01-15 16:29:16 -080053// A Client interacts with a single buildlet.
54type Client struct {
Brad Fitzpatrick874c0832015-01-16 12:59:14 -080055 ipPort string
56 tls KeyPair
57 password string // basic auth password or empty for none
58 httpClient *http.Client
Brad Fitzpatrickf3c01932015-01-15 16:29:16 -080059}
60
61// URL returns the buildlet's URL prefix, without a trailing slash.
62func (c *Client) URL() string {
Brad Fitzpatrick874c0832015-01-16 12:59:14 -080063 if !c.tls.IsZero() {
64 return "https://" + strings.TrimSuffix(c.ipPort, ":443")
Brad Fitzpatrickf3c01932015-01-15 16:29:16 -080065 }
Brad Fitzpatrick874c0832015-01-16 12:59:14 -080066 return "http://" + strings.TrimSuffix(c.ipPort, ":80")
67}
68
69func (c *Client) do(req *http.Request) (*http.Response, error) {
70 if c.password != "" {
71 req.SetBasicAuth("gomote", c.password)
72 }
73 return c.httpClient.Do(req)
Brad Fitzpatrickf3c01932015-01-15 16:29:16 -080074}
75
Brad Fitzpatrick0cc04612015-01-19 19:43:25 -080076// doOK sends the request and expects a 200 OK response.
77func (c *Client) doOK(req *http.Request) error {
Brad Fitzpatrick874c0832015-01-16 12:59:14 -080078 res, err := c.do(req)
Brad Fitzpatrickf3c01932015-01-15 16:29:16 -080079 if err != nil {
80 return err
81 }
82 defer res.Body.Close()
Brad Fitzpatrick0cc04612015-01-19 19:43:25 -080083 if res.StatusCode != http.StatusOK {
Brad Fitzpatrickf3c01932015-01-15 16:29:16 -080084 slurp, _ := ioutil.ReadAll(io.LimitReader(res.Body, 4<<10))
85 return fmt.Errorf("%v; body: %s", res.Status, slurp)
86 }
87 return nil
88}
Brad Fitzpatrickf8c24842015-01-16 09:54:03 -080089
Brad Fitzpatrick0cc04612015-01-19 19:43:25 -080090// PutTar writes files to the remote buildlet, rooted at the relative
91// directory dir.
92// If dir is empty, they're placed at the root of the buildlet's work directory.
93// The dir is created if necessary.
94// The Reader must be of a tar.gz file.
95func (c *Client) PutTar(r io.Reader, dir string) error {
96 req, err := http.NewRequest("PUT", c.URL()+"/writetgz?dir="+url.QueryEscape(dir), r)
97 if err != nil {
98 return err
99 }
100 return c.doOK(req)
101}
102
103// PutTarFromURL tells the buildlet to download the tar.gz file from tarURL
104// and write it to dir, a relative directory from the workdir.
105// If dir is empty, they're placed at the root of the buildlet's work directory.
106// The dir is created if necessary.
107// The url must be of a tar.gz file.
108func (c *Client) PutTarFromURL(tarURL, dir string) error {
109 form := url.Values{
110 "url": {tarURL},
111 }
112 req, err := http.NewRequest("POST", c.URL()+"/writetgz?dir="+url.QueryEscape(dir), strings.NewReader(form.Encode()))
113 if err != nil {
114 return err
115 }
116 req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
117 return c.doOK(req)
118}
119
Brad Fitzpatrickb35ba9f2015-01-19 20:53:34 -0800120// GetTar returns a .tar.gz stream of the given directory, relative to the buildlet's work dir.
121// The provided dir may be empty to get everything.
122func (c *Client) GetTar(dir string) (tgz io.ReadCloser, err error) {
123 req, err := http.NewRequest("GET", c.URL()+"/tgz?dir="+url.QueryEscape(dir), nil)
124 if err != nil {
125 return nil, err
126 }
127 res, err := c.do(req)
128 if err != nil {
129 return nil, err
130 }
131 if res.StatusCode != http.StatusOK {
132 slurp, _ := ioutil.ReadAll(io.LimitReader(res.Body, 4<<10))
133 res.Body.Close()
134 return nil, fmt.Errorf("%v; body: %s", res.Status, slurp)
135 }
136 return res.Body, nil
137}
138
Brad Fitzpatrickf8c24842015-01-16 09:54:03 -0800139// ExecOpts are options for a remote command invocation.
140type ExecOpts struct {
141 // Output is the output of stdout and stderr.
142 // If nil, the output is discarded.
143 Output io.Writer
144
Brad Fitzpatrickb35ba9f2015-01-19 20:53:34 -0800145 // Args are the arguments to pass to the cmd given to Client.Exec.
146 Args []string
147
Brad Fitzpatrick32d05202015-01-21 15:15:48 -0800148 // ExtraEnv are KEY=VALUE pairs to append to the buildlet
149 // process's environment.
150 ExtraEnv []string
151
Brad Fitzpatrickb35ba9f2015-01-19 20:53:34 -0800152 // SystemLevel controls whether the command is run outside of
153 // the buildlet's environment.
154 SystemLevel bool
155
Brad Fitzpatrickde8994a2015-02-09 13:12:20 -0800156 // Debug, if true, instructs to the buildlet to print extra debug
157 // info to the output before the command begins executing.
158 Debug bool
159
Brad Fitzpatrickf8c24842015-01-16 09:54:03 -0800160 // OnStartExec is an optional hook that runs after the 200 OK
161 // response from the buildlet, but before the output begins
162 // writing to Output.
163 OnStartExec func()
164}
165
166// Exec runs cmd on the buildlet.
167//
168// Two errors are returned: one is whether the command succeeded
169// remotely (remoteErr), and the second (execErr) is whether there
170// were system errors preventing the command from being started or
171// seen to completition. If execErr is non-nil, the remoteErr is
172// meaningless.
173func (c *Client) Exec(cmd string, opts ExecOpts) (remoteErr, execErr error) {
Brad Fitzpatrickb35ba9f2015-01-19 20:53:34 -0800174 var mode string
175 if opts.SystemLevel {
176 mode = "sys"
177 }
Brad Fitzpatrick874c0832015-01-16 12:59:14 -0800178 form := url.Values{
Brad Fitzpatrickb35ba9f2015-01-19 20:53:34 -0800179 "cmd": {cmd},
180 "mode": {mode},
181 "cmdArg": opts.Args,
Brad Fitzpatrick32d05202015-01-21 15:15:48 -0800182 "env": opts.ExtraEnv,
Brad Fitzpatrickde8994a2015-02-09 13:12:20 -0800183 "debug": {fmt.Sprint(opts.Debug)},
Brad Fitzpatrick874c0832015-01-16 12:59:14 -0800184 }
185 req, err := http.NewRequest("POST", c.URL()+"/exec", strings.NewReader(form.Encode()))
186 if err != nil {
187 return nil, err
188 }
189 req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
190 res, err := c.do(req)
Brad Fitzpatrickf8c24842015-01-16 09:54:03 -0800191 if err != nil {
192 return nil, err
193 }
194 defer res.Body.Close()
195 if res.StatusCode != http.StatusOK {
196 slurp, _ := ioutil.ReadAll(io.LimitReader(res.Body, 4<<10))
197 return nil, fmt.Errorf("buildlet: HTTP status %v: %s", res.Status, slurp)
198 }
199 condRun(opts.OnStartExec)
200
201 // Stream the output:
202 out := opts.Output
203 if out == nil {
204 out = ioutil.Discard
205 }
206 if _, err := io.Copy(out, res.Body); err != nil {
207 return nil, fmt.Errorf("error copying response: %v", err)
208 }
209
210 // Don't record to the dashboard unless we heard the trailer from
211 // the buildlet, otherwise it was probably some unrelated error
212 // (like the VM being killed, or the buildlet crashing due to
213 // e.g. https://golang.org/issue/9309, since we require a tip
214 // build of the buildlet to get Trailers support)
215 state := res.Trailer.Get("Process-State")
216 if state == "" {
217 return nil, errors.New("missing Process-State trailer from HTTP response; buildlet built with old (<= 1.4) Go?")
218 }
219 if state != "ok" {
220 return errors.New(state), nil
221 }
222 return nil, nil
223}
224
Brad Fitzpatrick874c0832015-01-16 12:59:14 -0800225// Destroy shuts down the buildlet, destroying all state immediately.
226func (c *Client) Destroy() error {
227 req, err := http.NewRequest("POST", c.URL()+"/halt", nil)
228 if err != nil {
229 return err
230 }
Andrew Gerrand212bff22015-02-02 12:04:07 +0000231 return c.doOK(req)
232}
233
234// RemoveAll deletes the provided paths, relative to the work directory.
235func (c *Client) RemoveAll(paths ...string) error {
Brad Fitzpatrickde8994a2015-02-09 13:12:20 -0800236 if len(paths) == 0 {
237 return nil
238 }
Andrew Gerrand212bff22015-02-02 12:04:07 +0000239 form := url.Values{"path": paths}
240 req, err := http.NewRequest("POST", c.URL()+"/removeall", strings.NewReader(form.Encode()))
Brad Fitzpatrick874c0832015-01-16 12:59:14 -0800241 if err != nil {
242 return err
243 }
Andrew Gerrand84aecb22015-02-04 15:54:43 +0000244 req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
Andrew Gerrand212bff22015-02-02 12:04:07 +0000245 return c.doOK(req)
Brad Fitzpatrick874c0832015-01-16 12:59:14 -0800246}
247
Andrew Gerrand376b01d2015-02-03 12:39:25 +0000248// DestroyVM shuts down the buildlet and destroys the VM instance.
249func (c *Client) DestroyVM(ts oauth2.TokenSource, proj, zone, instance string) error {
250 gceErrc := make(chan error, 1)
251 buildletErrc := make(chan error, 1)
252 go func() {
253 gceErrc <- DestroyVM(ts, proj, zone, instance)
254 }()
255 go func() {
256 buildletErrc <- c.Destroy()
257 }()
258 timeout := time.NewTimer(5 * time.Second)
259 defer timeout.Stop()
260
261 var retErr error
262 var gceDone, buildletDone bool
263 for !gceDone || !buildletDone {
264 select {
265 case err := <-gceErrc:
266 if err != nil {
267 retErr = err
268 }
269 gceDone = true
270 case err := <-buildletErrc:
271 if err != nil {
272 retErr = err
273 }
274 buildletDone = true
275 case <-timeout.C:
276 e := ""
277 if !buildletDone {
278 e = "timeout asking buildlet to shut down"
279 }
280 if !gceDone {
281 if e != "" {
282 e += " and "
283 }
284 e += "timeout asking GCE to delete builder VM"
285 }
286 return errors.New(e)
287 }
288 }
289 return retErr
290}
291
Andrew Gerrand84aecb22015-02-04 15:54:43 +0000292// WorkDir returns the absolute path to the buildlet work directory.
293func (c *Client) WorkDir() (string, error) {
294 req, err := http.NewRequest("GET", c.URL()+"/workdir", nil)
295 if err != nil {
296 return "", err
297 }
298 resp, err := c.do(req)
299 if err != nil {
300 return "", err
301 }
302 if resp.StatusCode != http.StatusOK {
303 return "", errors.New(resp.Status)
304 }
305 b, err := ioutil.ReadAll(resp.Body)
306 resp.Body.Close()
307 if err != nil {
308 return "", err
309 }
310 return string(b), nil
311}
312
Brad Fitzpatrickde8994a2015-02-09 13:12:20 -0800313// DirEntry is the information about a file on a buildlet.
314type DirEntry struct {
315 // line is of the form "drw-rw-rw\t<name>" and then if a regular file,
316 // also "\t<size>\t<modtime>". in either case, without trailing newline.
317 // TODO: break into parsed fields?
318 line string
319}
320
321func (de DirEntry) String() string {
322 return de.line
323}
324
325func (de DirEntry) Name() string {
326 f := strings.Split(de.line, "\t")
327 if len(f) < 2 {
328 return ""
329 }
330 return f[1]
331}
332
333func (de DirEntry) Digest() string {
334 f := strings.Split(de.line, "\t")
335 if len(f) < 5 {
336 return ""
337 }
338 return f[4]
339}
340
341// ListDirOpts are options for Client.ListDir.
342type ListDirOpts struct {
343 // Recursive controls whether the directory is listed
344 // recursively.
345 Recursive bool
346
347 // Skip are the directories to skip, relative to the directory
348 // passed to ListDir. Each item should contain only forward
349 // slashes and not start or end in slashes.
350 Skip []string
351
352 // Digest controls whether the SHA-1 digests of regular files
353 // are returned.
354 Digest bool
355}
356
357// ListDir lists the contents of a directory.
358// The fn callback is run for each entry.
359func (c *Client) ListDir(dir string, opts ListDirOpts, fn func(DirEntry)) error {
360 param := url.Values{
361 "dir": {dir},
362 "recursive": {fmt.Sprint(opts.Recursive)},
363 "skip": opts.Skip,
364 "digest": {fmt.Sprint(opts.Digest)},
365 }
366 req, err := http.NewRequest("GET", c.URL()+"/ls?"+param.Encode(), nil)
367 if err != nil {
368 return err
369 }
370 resp, err := c.do(req)
371 if err != nil {
372 return err
373 }
374 defer resp.Body.Close()
375 if resp.StatusCode != http.StatusOK {
376 return errors.New(resp.Status)
377 }
378 sc := bufio.NewScanner(resp.Body)
379 for sc.Scan() {
380 line := strings.TrimSpace(sc.Text())
381 fn(DirEntry{line: line})
382 }
383 return sc.Err()
384}
385
Brad Fitzpatrickf8c24842015-01-16 09:54:03 -0800386func condRun(fn func()) {
387 if fn != nil {
388 fn()
389 }
390}