blob: 7ca7b22ad0834b930da6d31936085ed0e4cdcd5c [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"
David Crawshaw581ddd12015-04-06 08:09:20 -040011 "encoding/json"
Brad Fitzpatrickf8c24842015-01-16 09:54:03 -080012 "errors"
Brad Fitzpatrickf3c01932015-01-15 16:29:16 -080013 "fmt"
14 "io"
15 "io/ioutil"
Brad Fitzpatrickf68e9f52015-02-03 11:09:50 +000016 "net"
Brad Fitzpatrickf3c01932015-01-15 16:29:16 -080017 "net/http"
Brad Fitzpatrickf8c24842015-01-16 09:54:03 -080018 "net/url"
Andrew Gerrand91d984c2015-02-10 23:25:40 +110019 "os"
Brad Fitzpatrickf3c01932015-01-15 16:29:16 -080020 "strings"
Brad Fitzpatrick378fb292015-06-10 13:59:42 -070021 "sync"
Andrew Gerrand376b01d2015-02-03 12:39:25 +000022 "time"
23
24 "golang.org/x/oauth2"
Brad Fitzpatrickf3c01932015-01-15 16:29:16 -080025)
26
Brad Fitzpatrickf3c01932015-01-15 16:29:16 -080027// NewClient returns a *Client that will manipulate ipPort,
28// authenticated using the provided keypair.
29//
30// This constructor returns immediately without testing the host or auth.
Brad Fitzpatrick874c0832015-01-16 12:59:14 -080031func NewClient(ipPort string, kp KeyPair) *Client {
Brad Fitzpatrickf3c01932015-01-15 16:29:16 -080032 return &Client{
Brad Fitzpatrick874c0832015-01-16 12:59:14 -080033 ipPort: ipPort,
34 tls: kp,
35 password: kp.Password(),
Brad Fitzpatrickd4ea0142015-06-12 10:31:58 -070036 peerDead: make(chan struct{}),
Brad Fitzpatrick874c0832015-01-16 12:59:14 -080037 httpClient: &http.Client{
38 Transport: &http.Transport{
Brad Fitzpatrickf68e9f52015-02-03 11:09:50 +000039 Dial: defaultDialer(),
Brad Fitzpatrick874c0832015-01-16 12:59:14 -080040 DialTLS: kp.tlsDialer(),
41 },
42 },
Brad Fitzpatrickf3c01932015-01-15 16:29:16 -080043 }
44}
45
Brad Fitzpatrick7b2f9d72015-03-27 17:45:12 +010046// SetCloseFunc sets a function to be called when c.Close is called.
47// SetCloseFunc must not be called concurrently with Close.
48func (c *Client) SetCloseFunc(fn func() error) {
49 c.closeFunc = fn
50}
51
52func (c *Client) Close() error {
Brad Fitzpatrickd4ea0142015-06-12 10:31:58 -070053 c.setPeerDead(errors.New("Close called"))
Brad Fitzpatrick7b2f9d72015-03-27 17:45:12 +010054 var err error
55 if c.closeFunc != nil {
56 err = c.closeFunc()
57 c.closeFunc = nil
58 }
59 return err
60}
61
Brad Fitzpatrickd4ea0142015-06-12 10:31:58 -070062// To be called only via c.setPeerDeadOnce.Do(s.setPeerDead)
63func (c *Client) setPeerDead(err error) {
64 c.setPeerDeadOnce.Do(func() {
65 c.deadErr = err
66 close(c.peerDead)
67 })
68}
69
Brad Fitzpatrick7b2f9d72015-03-27 17:45:12 +010070// SetDescription sets a short description of where the buildlet
71// connection came from. This is used by the build coordinator status
72// page, mostly for debugging.
73func (c *Client) SetDescription(v string) {
74 c.desc = v
75}
76
David Crawshaw581ddd12015-04-06 08:09:20 -040077// SetHTTPClient replaces the underlying HTTP client.
Brad Fitzpatrickd4ea0142015-06-12 10:31:58 -070078// It should only be called before the Client is used.
David Crawshaw581ddd12015-04-06 08:09:20 -040079func (c *Client) SetHTTPClient(httpClient *http.Client) {
80 c.httpClient = httpClient
81}
82
Brad Fitzpatrickd4ea0142015-06-12 10:31:58 -070083// EnableHeartbeats enables background heartbeating
84// against the peer.
85// It should only be called before the Client is used.
86func (c *Client) EnableHeartbeats() {
87 // TODO(bradfitz): make this always enabled, once the
88 // reverse buildlet connection model supports
89 // multiple connections at once.
90 c.heartbeat = true
91}
92
Brad Fitzpatrickf68e9f52015-02-03 11:09:50 +000093// defaultDialer returns the net/http package's default Dial function.
94// Notably, this sets TCP keep-alive values, so when we kill VMs
95// (whose TCP stacks stop replying, forever), we don't leak file
96// descriptors for otherwise forever-stalled TCP connections.
97func defaultDialer() func(network, addr string) (net.Conn, error) {
98 if fn := http.DefaultTransport.(*http.Transport).Dial; fn != nil {
99 return fn
100 }
101 return net.Dial
102}
103
Brad Fitzpatrickf3c01932015-01-15 16:29:16 -0800104// A Client interacts with a single buildlet.
105type Client struct {
Brad Fitzpatrick874c0832015-01-16 12:59:14 -0800106 ipPort string
107 tls KeyPair
108 password string // basic auth password or empty for none
109 httpClient *http.Client
Brad Fitzpatrickd4ea0142015-06-12 10:31:58 -0700110 heartbeat bool // whether to heartbeat in the background
Brad Fitzpatrick7b2f9d72015-03-27 17:45:12 +0100111
112 closeFunc func() error
113 desc string
Brad Fitzpatrick378fb292015-06-10 13:59:42 -0700114
Brad Fitzpatrickd4ea0142015-06-12 10:31:58 -0700115 initHeartbeatOnce sync.Once
116 setPeerDeadOnce sync.Once
117 peerDead chan struct{} // closed on peer death
118 deadErr error // guarded by peerDead's close
119
Brad Fitzpatrick378fb292015-06-10 13:59:42 -0700120 mu sync.Mutex
121 broken bool // client is broken in some way
Brad Fitzpatrick7b2f9d72015-03-27 17:45:12 +0100122}
123
124func (c *Client) String() string {
125 if c == nil {
126 return "(nil *buildlet.Client)"
127 }
128 return strings.TrimSpace(c.URL() + " " + c.desc)
Brad Fitzpatrickf3c01932015-01-15 16:29:16 -0800129}
130
131// URL returns the buildlet's URL prefix, without a trailing slash.
132func (c *Client) URL() string {
Brad Fitzpatrick874c0832015-01-16 12:59:14 -0800133 if !c.tls.IsZero() {
134 return "https://" + strings.TrimSuffix(c.ipPort, ":443")
Brad Fitzpatrickf3c01932015-01-15 16:29:16 -0800135 }
Brad Fitzpatrick874c0832015-01-16 12:59:14 -0800136 return "http://" + strings.TrimSuffix(c.ipPort, ":80")
137}
138
Brad Fitzpatrick79f3fc02015-05-27 21:51:25 -0700139func (c *Client) IPPort() string { return c.ipPort }
140
Brad Fitzpatrick378fb292015-06-10 13:59:42 -0700141// MarkBroken marks this client as broken in some way.
142func (c *Client) MarkBroken() {
143 c.mu.Lock()
144 defer c.mu.Unlock()
145 c.broken = true
146}
147
148// IsBroken reports whether this client is broken in some way.
149func (c *Client) IsBroken() bool {
150 c.mu.Lock()
151 defer c.mu.Unlock()
152 return c.broken
153}
154
Brad Fitzpatrick874c0832015-01-16 12:59:14 -0800155func (c *Client) do(req *http.Request) (*http.Response, error) {
Brad Fitzpatrickd4ea0142015-06-12 10:31:58 -0700156 c.initHeartbeatOnce.Do(c.initHeartbeats)
Brad Fitzpatrick874c0832015-01-16 12:59:14 -0800157 if c.password != "" {
158 req.SetBasicAuth("gomote", c.password)
159 }
160 return c.httpClient.Do(req)
Brad Fitzpatrickf3c01932015-01-15 16:29:16 -0800161}
162
Brad Fitzpatrickd4ea0142015-06-12 10:31:58 -0700163func (c *Client) initHeartbeats() {
164 if !c.heartbeat {
165 // TODO(bradfitz): make this always enabled later, once
166 // reverse buildlets are fixed.
167 return
168 }
169 go c.heartbeatLoop()
170}
171
172func (c *Client) heartbeatLoop() {
173 for {
174 select {
175 case <-c.peerDead:
176 // Already dead by something else.
177 // Most likely: c.Close was called.
178 return
179 case <-time.After(10 * time.Second):
180 t0 := time.Now()
181 if _, err := c.Status(); err != nil {
182 err := fmt.Errorf("Buildlet %v failed heartbeat after %v; marking dead; err=%v", c, time.Since(t0), err)
183 c.MarkBroken()
184 c.setPeerDead(err)
185 return
186 }
187 }
188 }
189}
190
Brad Fitzpatrick08025222015-06-09 09:12:17 -0700191var errHeaderTimeout = errors.New("timeout waiting for headers")
192
193// doHeaderTimeout calls c.do(req) and returns its results, or
194// errHeaderTimeout if max elapses first.
195func (c *Client) doHeaderTimeout(req *http.Request, max time.Duration) (res *http.Response, err error) {
196 type resErr struct {
197 res *http.Response
198 err error
199 }
200 resErrc := make(chan resErr, 1)
201 go func() {
202 res, err := c.do(req)
203 resErrc <- resErr{res, err}
204 }()
205
206 timer := time.NewTimer(max)
207 defer timer.Stop()
208
Brad Fitzpatrickd4ea0142015-06-12 10:31:58 -0700209 cleanup := func() {
210 if re := <-resErrc; re.res != nil {
211 re.res.Body.Close()
212 }
213 }
214
Brad Fitzpatrick08025222015-06-09 09:12:17 -0700215 select {
216 case re := <-resErrc:
217 return re.res, re.err
Brad Fitzpatrickd4ea0142015-06-12 10:31:58 -0700218 case <-c.peerDead:
219 go cleanup()
220 return nil, c.deadErr
Brad Fitzpatrick08025222015-06-09 09:12:17 -0700221 case <-timer.C:
Brad Fitzpatrickd4ea0142015-06-12 10:31:58 -0700222 go cleanup()
Brad Fitzpatrick08025222015-06-09 09:12:17 -0700223 return nil, errHeaderTimeout
224 }
225}
226
Brad Fitzpatrick0cc04612015-01-19 19:43:25 -0800227// doOK sends the request and expects a 200 OK response.
228func (c *Client) doOK(req *http.Request) error {
Brad Fitzpatrick874c0832015-01-16 12:59:14 -0800229 res, err := c.do(req)
Brad Fitzpatrickf3c01932015-01-15 16:29:16 -0800230 if err != nil {
231 return err
232 }
233 defer res.Body.Close()
Brad Fitzpatrick0cc04612015-01-19 19:43:25 -0800234 if res.StatusCode != http.StatusOK {
Brad Fitzpatrickf3c01932015-01-15 16:29:16 -0800235 slurp, _ := ioutil.ReadAll(io.LimitReader(res.Body, 4<<10))
236 return fmt.Errorf("%v; body: %s", res.Status, slurp)
237 }
238 return nil
239}
Brad Fitzpatrickf8c24842015-01-16 09:54:03 -0800240
Brad Fitzpatrick0cc04612015-01-19 19:43:25 -0800241// PutTar writes files to the remote buildlet, rooted at the relative
242// directory dir.
243// If dir is empty, they're placed at the root of the buildlet's work directory.
244// The dir is created if necessary.
245// The Reader must be of a tar.gz file.
246func (c *Client) PutTar(r io.Reader, dir string) error {
247 req, err := http.NewRequest("PUT", c.URL()+"/writetgz?dir="+url.QueryEscape(dir), r)
248 if err != nil {
249 return err
250 }
251 return c.doOK(req)
252}
253
254// PutTarFromURL tells the buildlet to download the tar.gz file from tarURL
255// and write it to dir, a relative directory from the workdir.
256// If dir is empty, they're placed at the root of the buildlet's work directory.
257// The dir is created if necessary.
258// The url must be of a tar.gz file.
259func (c *Client) PutTarFromURL(tarURL, dir string) error {
260 form := url.Values{
261 "url": {tarURL},
262 }
263 req, err := http.NewRequest("POST", c.URL()+"/writetgz?dir="+url.QueryEscape(dir), strings.NewReader(form.Encode()))
264 if err != nil {
265 return err
266 }
267 req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
268 return c.doOK(req)
269}
270
Andrew Gerrand91d984c2015-02-10 23:25:40 +1100271// Put writes the provided file to path (relative to workdir) and sets mode.
272func (c *Client) Put(r io.Reader, path string, mode os.FileMode) error {
273 param := url.Values{
274 "path": {path},
275 "mode": {fmt.Sprint(int64(mode))},
276 }
277 req, err := http.NewRequest("PUT", c.URL()+"/write?"+param.Encode(), r)
278 if err != nil {
279 return err
280 }
281 return c.doOK(req)
282}
283
Brad Fitzpatrickb35ba9f2015-01-19 20:53:34 -0800284// GetTar returns a .tar.gz stream of the given directory, relative to the buildlet's work dir.
285// The provided dir may be empty to get everything.
286func (c *Client) GetTar(dir string) (tgz io.ReadCloser, err error) {
287 req, err := http.NewRequest("GET", c.URL()+"/tgz?dir="+url.QueryEscape(dir), nil)
288 if err != nil {
289 return nil, err
290 }
291 res, err := c.do(req)
292 if err != nil {
293 return nil, err
294 }
295 if res.StatusCode != http.StatusOK {
296 slurp, _ := ioutil.ReadAll(io.LimitReader(res.Body, 4<<10))
297 res.Body.Close()
298 return nil, fmt.Errorf("%v; body: %s", res.Status, slurp)
299 }
300 return res.Body, nil
301}
302
Brad Fitzpatrickf8c24842015-01-16 09:54:03 -0800303// ExecOpts are options for a remote command invocation.
304type ExecOpts struct {
305 // Output is the output of stdout and stderr.
306 // If nil, the output is discarded.
307 Output io.Writer
308
Andrew Gerrand60978122015-02-11 11:17:46 +1100309 // Dir is the directory from which to execute the command.
310 // It is optional. If not specified, it defaults to the directory of
311 // the command, or the work directory if SystemLevel is set.
312 Dir string
313
Brad Fitzpatrickb35ba9f2015-01-19 20:53:34 -0800314 // Args are the arguments to pass to the cmd given to Client.Exec.
315 Args []string
316
Brad Fitzpatrick32d05202015-01-21 15:15:48 -0800317 // ExtraEnv are KEY=VALUE pairs to append to the buildlet
318 // process's environment.
319 ExtraEnv []string
320
Andrew Gerrand1fc56ca2015-05-21 13:01:10 +1000321 // Path, if non-nil, specifies the PATH variable of the executed
322 // process's environment. A non-nil empty list clears the path.
323 // The following expansions apply:
324 // - the string "$PATH" expands to any existing PATH element(s)
325 // - the substring "$WORKDIR" expands to buildlet's temp workdir
326 // After expansions, the list is joined with an OS-specific list
327 // separator and supplied to the executed process as its PATH
328 // environment variable.
329 Path []string
330
Brad Fitzpatrickb35ba9f2015-01-19 20:53:34 -0800331 // SystemLevel controls whether the command is run outside of
332 // the buildlet's environment.
333 SystemLevel bool
334
Brad Fitzpatrickde8994a2015-02-09 13:12:20 -0800335 // Debug, if true, instructs to the buildlet to print extra debug
336 // info to the output before the command begins executing.
337 Debug bool
338
Brad Fitzpatrickf8c24842015-01-16 09:54:03 -0800339 // OnStartExec is an optional hook that runs after the 200 OK
340 // response from the buildlet, but before the output begins
341 // writing to Output.
342 OnStartExec func()
Brad Fitzpatrickd4ea0142015-06-12 10:31:58 -0700343
344 // Timeout is an optional duration before ErrTimeout is returned.
345 Timeout time.Duration
Brad Fitzpatrickf8c24842015-01-16 09:54:03 -0800346}
347
Brad Fitzpatrickd4ea0142015-06-12 10:31:58 -0700348var ErrTimeout = errors.New("buildlet: timeout waiting for command to complete")
349
Brad Fitzpatrickf8c24842015-01-16 09:54:03 -0800350// Exec runs cmd on the buildlet.
351//
352// Two errors are returned: one is whether the command succeeded
353// remotely (remoteErr), and the second (execErr) is whether there
354// were system errors preventing the command from being started or
355// seen to completition. If execErr is non-nil, the remoteErr is
356// meaningless.
357func (c *Client) Exec(cmd string, opts ExecOpts) (remoteErr, execErr error) {
Brad Fitzpatrickb35ba9f2015-01-19 20:53:34 -0800358 var mode string
359 if opts.SystemLevel {
360 mode = "sys"
361 }
Andrew Gerrand1fc56ca2015-05-21 13:01:10 +1000362 path := opts.Path
363 if len(path) == 0 && path != nil {
364 // url.Values doesn't distinguish between a nil slice and
365 // a non-nil zero-length slice, so use this sentinel value.
366 path = []string{"$EMPTY"}
367 }
Brad Fitzpatrick874c0832015-01-16 12:59:14 -0800368 form := url.Values{
Brad Fitzpatrickb35ba9f2015-01-19 20:53:34 -0800369 "cmd": {cmd},
370 "mode": {mode},
Andrew Gerrand60978122015-02-11 11:17:46 +1100371 "dir": {opts.Dir},
Brad Fitzpatrickb35ba9f2015-01-19 20:53:34 -0800372 "cmdArg": opts.Args,
Brad Fitzpatrick32d05202015-01-21 15:15:48 -0800373 "env": opts.ExtraEnv,
Andrew Gerrand1fc56ca2015-05-21 13:01:10 +1000374 "path": path,
Brad Fitzpatrickde8994a2015-02-09 13:12:20 -0800375 "debug": {fmt.Sprint(opts.Debug)},
Brad Fitzpatrick874c0832015-01-16 12:59:14 -0800376 }
377 req, err := http.NewRequest("POST", c.URL()+"/exec", strings.NewReader(form.Encode()))
378 if err != nil {
379 return nil, err
380 }
381 req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
Brad Fitzpatrick08025222015-06-09 09:12:17 -0700382
383 // The first thing the buildlet's exec handler does is flush the headers, so
Brad Fitzpatrickd4ea0142015-06-12 10:31:58 -0700384 // 10 seconds should be plenty of time, regardless of where on the planet
Brad Fitzpatrick08025222015-06-09 09:12:17 -0700385 // (Atlanta, Paris, etc) the reverse buildlet is:
Brad Fitzpatrickd4ea0142015-06-12 10:31:58 -0700386 res, err := c.doHeaderTimeout(req, 10*time.Second)
Brad Fitzpatrick08025222015-06-09 09:12:17 -0700387 if err == errHeaderTimeout {
Brad Fitzpatrick378fb292015-06-10 13:59:42 -0700388 c.MarkBroken()
Brad Fitzpatrick08025222015-06-09 09:12:17 -0700389 return nil, errors.New("buildlet: timeout waiting for exec header response")
390 }
Brad Fitzpatrickf8c24842015-01-16 09:54:03 -0800391 if err != nil {
392 return nil, err
393 }
394 defer res.Body.Close()
395 if res.StatusCode != http.StatusOK {
396 slurp, _ := ioutil.ReadAll(io.LimitReader(res.Body, 4<<10))
397 return nil, fmt.Errorf("buildlet: HTTP status %v: %s", res.Status, slurp)
398 }
399 condRun(opts.OnStartExec)
400
Brad Fitzpatrickd4ea0142015-06-12 10:31:58 -0700401 type errs struct {
402 remoteErr, execErr error
Brad Fitzpatrickf8c24842015-01-16 09:54:03 -0800403 }
Brad Fitzpatrickd4ea0142015-06-12 10:31:58 -0700404 resc := make(chan errs, 1)
405 go func() {
406 // Stream the output:
407 out := opts.Output
408 if out == nil {
409 out = ioutil.Discard
410 }
411 if _, err := io.Copy(out, res.Body); err != nil {
412 resc <- errs{execErr: fmt.Errorf("error copying response: %v", err)}
413 return
414 }
Brad Fitzpatrickf8c24842015-01-16 09:54:03 -0800415
Brad Fitzpatrickd4ea0142015-06-12 10:31:58 -0700416 // Don't record to the dashboard unless we heard the trailer from
417 // the buildlet, otherwise it was probably some unrelated error
418 // (like the VM being killed, or the buildlet crashing due to
419 // e.g. https://golang.org/issue/9309, since we require a tip
420 // build of the buildlet to get Trailers support)
421 state := res.Trailer.Get("Process-State")
422 if state == "" {
423 resc <- errs{execErr: errors.New("missing Process-State trailer from HTTP response; buildlet built with old (<= 1.4) Go?")}
424 return
425 }
426 if state != "ok" {
427 resc <- errs{remoteErr: errors.New(state)}
428 } else {
429 resc <- errs{} // success
430 }
431 }()
432 var timer <-chan time.Time
433 if opts.Timeout > 0 {
434 t := time.NewTimer(opts.Timeout)
435 defer t.Stop()
436 timer = t.C
Brad Fitzpatrickf8c24842015-01-16 09:54:03 -0800437 }
Brad Fitzpatrickd4ea0142015-06-12 10:31:58 -0700438 select {
439 case <-timer:
440 c.MarkBroken()
441 return nil, ErrTimeout
442 case res := <-resc:
443 return res.remoteErr, res.execErr
444 case <-c.peerDead:
445 return nil, c.deadErr
Brad Fitzpatrickf8c24842015-01-16 09:54:03 -0800446 }
Brad Fitzpatrickf8c24842015-01-16 09:54:03 -0800447}
448
Brad Fitzpatrick874c0832015-01-16 12:59:14 -0800449// Destroy shuts down the buildlet, destroying all state immediately.
450func (c *Client) Destroy() error {
451 req, err := http.NewRequest("POST", c.URL()+"/halt", nil)
452 if err != nil {
453 return err
454 }
Andrew Gerrand212bff22015-02-02 12:04:07 +0000455 return c.doOK(req)
456}
457
458// RemoveAll deletes the provided paths, relative to the work directory.
459func (c *Client) RemoveAll(paths ...string) error {
Brad Fitzpatrickde8994a2015-02-09 13:12:20 -0800460 if len(paths) == 0 {
461 return nil
462 }
Andrew Gerrand212bff22015-02-02 12:04:07 +0000463 form := url.Values{"path": paths}
464 req, err := http.NewRequest("POST", c.URL()+"/removeall", strings.NewReader(form.Encode()))
Brad Fitzpatrick874c0832015-01-16 12:59:14 -0800465 if err != nil {
466 return err
467 }
Andrew Gerrand84aecb22015-02-04 15:54:43 +0000468 req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
Andrew Gerrand212bff22015-02-02 12:04:07 +0000469 return c.doOK(req)
Brad Fitzpatrick874c0832015-01-16 12:59:14 -0800470}
471
Andrew Gerrand376b01d2015-02-03 12:39:25 +0000472// DestroyVM shuts down the buildlet and destroys the VM instance.
473func (c *Client) DestroyVM(ts oauth2.TokenSource, proj, zone, instance string) error {
474 gceErrc := make(chan error, 1)
475 buildletErrc := make(chan error, 1)
476 go func() {
477 gceErrc <- DestroyVM(ts, proj, zone, instance)
478 }()
479 go func() {
480 buildletErrc <- c.Destroy()
481 }()
482 timeout := time.NewTimer(5 * time.Second)
483 defer timeout.Stop()
484
485 var retErr error
486 var gceDone, buildletDone bool
487 for !gceDone || !buildletDone {
488 select {
489 case err := <-gceErrc:
490 if err != nil {
491 retErr = err
492 }
493 gceDone = true
494 case err := <-buildletErrc:
495 if err != nil {
496 retErr = err
497 }
498 buildletDone = true
499 case <-timeout.C:
500 e := ""
501 if !buildletDone {
502 e = "timeout asking buildlet to shut down"
503 }
504 if !gceDone {
505 if e != "" {
506 e += " and "
507 }
508 e += "timeout asking GCE to delete builder VM"
509 }
510 return errors.New(e)
511 }
512 }
513 return retErr
514}
515
David Crawshawa3dce2c2015-04-07 19:46:19 -0400516// Status provides status information about the buildlet.
David Crawshaw581ddd12015-04-06 08:09:20 -0400517//
518// A coordinator can use the provided information to decide what, if anything,
519// to do with a buildlet.
David Crawshawa3dce2c2015-04-07 19:46:19 -0400520type Status struct {
521 Version int // buildlet version, coordinator rejects any value less than 1.
David Crawshaw581ddd12015-04-06 08:09:20 -0400522}
523
David Crawshawa3dce2c2015-04-07 19:46:19 -0400524// Status returns an Status value describing this buildlet.
525func (c *Client) Status() (Status, error) {
526 req, err := http.NewRequest("GET", c.URL()+"/status", nil)
David Crawshaw581ddd12015-04-06 08:09:20 -0400527 if err != nil {
David Crawshawa3dce2c2015-04-07 19:46:19 -0400528 return Status{}, err
David Crawshaw581ddd12015-04-06 08:09:20 -0400529 }
Brad Fitzpatrickd4ea0142015-06-12 10:31:58 -0700530 resp, err := c.doHeaderTimeout(req, 10*time.Second) // plenty of time
David Crawshaw581ddd12015-04-06 08:09:20 -0400531 if err != nil {
David Crawshawa3dce2c2015-04-07 19:46:19 -0400532 return Status{}, err
David Crawshaw581ddd12015-04-06 08:09:20 -0400533 }
534 if resp.StatusCode != http.StatusOK {
David Crawshawa3dce2c2015-04-07 19:46:19 -0400535 return Status{}, errors.New(resp.Status)
David Crawshaw581ddd12015-04-06 08:09:20 -0400536 }
537 b, err := ioutil.ReadAll(resp.Body)
538 resp.Body.Close()
539 if err != nil {
David Crawshawa3dce2c2015-04-07 19:46:19 -0400540 return Status{}, err
David Crawshaw581ddd12015-04-06 08:09:20 -0400541 }
David Crawshawa3dce2c2015-04-07 19:46:19 -0400542 var status Status
543 if err := json.Unmarshal(b, &status); err != nil {
544 return Status{}, err
David Crawshaw581ddd12015-04-06 08:09:20 -0400545 }
David Crawshawa3dce2c2015-04-07 19:46:19 -0400546 return status, nil
David Crawshaw581ddd12015-04-06 08:09:20 -0400547}
548
Andrew Gerrand84aecb22015-02-04 15:54:43 +0000549// WorkDir returns the absolute path to the buildlet work directory.
550func (c *Client) WorkDir() (string, error) {
551 req, err := http.NewRequest("GET", c.URL()+"/workdir", nil)
552 if err != nil {
553 return "", err
554 }
Brad Fitzpatrickd4ea0142015-06-12 10:31:58 -0700555 resp, err := c.doHeaderTimeout(req, 10*time.Second) // plenty of time
Andrew Gerrand84aecb22015-02-04 15:54:43 +0000556 if err != nil {
557 return "", err
558 }
559 if resp.StatusCode != http.StatusOK {
560 return "", errors.New(resp.Status)
561 }
562 b, err := ioutil.ReadAll(resp.Body)
563 resp.Body.Close()
564 if err != nil {
565 return "", err
566 }
567 return string(b), nil
568}
569
Brad Fitzpatrickde8994a2015-02-09 13:12:20 -0800570// DirEntry is the information about a file on a buildlet.
571type DirEntry struct {
572 // line is of the form "drw-rw-rw\t<name>" and then if a regular file,
573 // also "\t<size>\t<modtime>". in either case, without trailing newline.
574 // TODO: break into parsed fields?
575 line string
576}
577
578func (de DirEntry) String() string {
579 return de.line
580}
581
582func (de DirEntry) Name() string {
583 f := strings.Split(de.line, "\t")
584 if len(f) < 2 {
585 return ""
586 }
587 return f[1]
588}
589
590func (de DirEntry) Digest() string {
591 f := strings.Split(de.line, "\t")
592 if len(f) < 5 {
593 return ""
594 }
595 return f[4]
596}
597
598// ListDirOpts are options for Client.ListDir.
599type ListDirOpts struct {
600 // Recursive controls whether the directory is listed
601 // recursively.
602 Recursive bool
603
604 // Skip are the directories to skip, relative to the directory
605 // passed to ListDir. Each item should contain only forward
606 // slashes and not start or end in slashes.
607 Skip []string
608
609 // Digest controls whether the SHA-1 digests of regular files
610 // are returned.
611 Digest bool
612}
613
614// ListDir lists the contents of a directory.
615// The fn callback is run for each entry.
616func (c *Client) ListDir(dir string, opts ListDirOpts, fn func(DirEntry)) error {
617 param := url.Values{
618 "dir": {dir},
619 "recursive": {fmt.Sprint(opts.Recursive)},
620 "skip": opts.Skip,
621 "digest": {fmt.Sprint(opts.Digest)},
622 }
623 req, err := http.NewRequest("GET", c.URL()+"/ls?"+param.Encode(), nil)
624 if err != nil {
625 return err
626 }
627 resp, err := c.do(req)
628 if err != nil {
629 return err
630 }
631 defer resp.Body.Close()
632 if resp.StatusCode != http.StatusOK {
633 return errors.New(resp.Status)
634 }
635 sc := bufio.NewScanner(resp.Body)
636 for sc.Scan() {
637 line := strings.TrimSpace(sc.Text())
638 fn(DirEntry{line: line})
639 }
640 return sc.Err()
641}
642
Brad Fitzpatrickf8c24842015-01-16 09:54:03 -0800643func condRun(fn func()) {
644 if fn != nil {
645 fn()
646 }
647}