Skip to content

Commit 9571310

Browse files
committed
dashboard/buildlet: start of the buildlet
This is the basics: untar a tar.gz file to a directory, and execute a command. Update golang/go#8639 Update golang/go#8640 Update golang/go#8642 Change-Id: I5917ed8bd0e4c2fdb4b3fab34ca929caca95cc8a Reviewed-on: https://go-review.googlesource.com/2180 Reviewed-by: Andrew Gerrand <adg@golang.org>
1 parent d6c6cd3 commit 9571310

File tree

2 files changed

+256
-0
lines changed

2 files changed

+256
-0
lines changed

README

+4
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@
55
The files in this directory constitute the continuous builder:
66

77
app/: an AppEngine server. The code that runs http://build.golang.org/
8+
buildlet/: HTTP server that runs on a VM and is told what to write to disk
9+
and what command to run. This is cross-compiled to all architectures
10+
and is the first program run when a builder VM comes up. It then
11+
is contacted by the coordinator to do a build.
812
builder/: gobuilder, a Go continuous build client
913
coordinator/: daemon that runs on CoreOS on Google Compute Engine and manages
1014
builds (using the builder in single-shot mode) in Docker containers.

buildlet/buildlet.go

+252
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
// Copyright 2014 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+
5+
// The buildlet is an HTTP server that untars content to disk and runs
6+
// commands it has untarred, streaming their output back over HTTP.
7+
// It is part of Go's continuous build system.
8+
//
9+
// This program intentionally allows remote code execution, and
10+
// provides no security of its own. It is assumed that any user uses
11+
// it with an appropriately-configured firewall between their VM
12+
// instances.
13+
package main // import "golang.org/x/tools/dashboard/buildlet"
14+
15+
/* Notes:
16+
17+
https://go.googlesource.com/go/+archive/3b76b017cabb.tar.gz
18+
curl -X PUT --data-binary "@go-3b76b017cabb.tar.gz" http://127.0.0.1:5937/writetgz
19+
20+
curl -d "cmd=src/make.bash" http://127.0.0.1:5937/exec
21+
22+
*/
23+
24+
import (
25+
"archive/tar"
26+
"compress/gzip"
27+
"flag"
28+
"fmt"
29+
"io"
30+
"io/ioutil"
31+
"log"
32+
"net"
33+
"net/http"
34+
"os"
35+
"os/exec"
36+
"path/filepath"
37+
"runtime"
38+
"strings"
39+
"sync"
40+
"time"
41+
)
42+
43+
var (
44+
scratchDir = flag.String("scratchdir", "", "Temporary directory to use. The contents of this directory may be deleted at any time. If empty, TempDir is used to create one.")
45+
listenAddr = flag.String("listen", defaultListenAddr(), "address to listen on. Warning: this service is inherently insecure and offers no protection of its own. Do not expose this port to the world.")
46+
)
47+
48+
func defaultListenAddr() string {
49+
if OnGCE() {
50+
// In production, default to
51+
return ":80"
52+
}
53+
return "localhost:5936"
54+
}
55+
56+
func main() {
57+
flag.Parse()
58+
if !OnGCE() && !strings.HasPrefix(*listenAddr, "localhost:") {
59+
log.Printf("** WARNING *** This server is unsafe and offers no security. Be careful.")
60+
}
61+
if *scratchDir == "" {
62+
dir, err := ioutil.TempDir("", "buildlet-scatch")
63+
if err != nil {
64+
log.Fatalf("error creating scratchdir with ioutil.TempDir: %v", err)
65+
}
66+
*scratchDir = dir
67+
}
68+
if _, err := os.Lstat(*scratchDir); err != nil {
69+
log.Fatalf("invalid --scratchdir %q: %v", *scratchDir, err)
70+
}
71+
http.HandleFunc("/writetgz", handleWriteTGZ)
72+
http.HandleFunc("/exec", handleExec)
73+
http.HandleFunc("/", handleRoot)
74+
// TODO: removeall
75+
log.Printf("Listening on %s ...", *listenAddr)
76+
log.Fatalf("ListenAndServe: %v", http.ListenAndServe(*listenAddr, nil))
77+
}
78+
79+
func handleRoot(w http.ResponseWriter, r *http.Request) {
80+
fmt.Fprintf(w, "buildlet running on %s-%s", runtime.GOOS, runtime.GOARCH)
81+
}
82+
83+
func handleWriteTGZ(w http.ResponseWriter, r *http.Request) {
84+
if r.Method != "PUT" {
85+
http.Error(w, "requires PUT method", http.StatusBadRequest)
86+
return
87+
}
88+
err := untar(r.Body, *scratchDir)
89+
if err != nil {
90+
status := http.StatusInternalServerError
91+
if he, ok := err.(httpStatuser); ok {
92+
status = he.httpStatus()
93+
}
94+
http.Error(w, err.Error(), status)
95+
return
96+
}
97+
io.WriteString(w, "OK")
98+
}
99+
100+
// untar reads the gzip-compressed tar file from r and writes it into dir.
101+
func untar(r io.Reader, dir string) error {
102+
zr, err := gzip.NewReader(r)
103+
if err != nil {
104+
return badRequest("requires gzip-compressed body: " + err.Error())
105+
}
106+
tr := tar.NewReader(zr)
107+
for {
108+
f, err := tr.Next()
109+
if err == io.EOF {
110+
break
111+
}
112+
if err != nil {
113+
log.Printf("tar reading error: %v", err)
114+
return badRequest("tar error: " + err.Error())
115+
}
116+
if !validRelPath(f.Name) {
117+
return badRequest(fmt.Sprintf("tar file contained invalid name %q", f.Name))
118+
}
119+
rel := filepath.FromSlash(f.Name)
120+
abs := filepath.Join(dir, rel)
121+
122+
fi := f.FileInfo()
123+
mode := fi.Mode()
124+
switch {
125+
case mode.IsRegular():
126+
// Make the directory. This is redundant because it should
127+
// already be made by a directory entry in the tar
128+
// beforehand. Thus, don't check for errors; the next
129+
// write will fail with the same error.
130+
os.MkdirAll(filepath.Dir(abs), 0755)
131+
wf, err := os.OpenFile(abs, os.O_RDWR|os.O_CREATE|os.O_TRUNC, mode.Perm())
132+
if err != nil {
133+
return err
134+
}
135+
n, err := io.Copy(wf, tr)
136+
if closeErr := wf.Close(); closeErr != nil && err == nil {
137+
err = closeErr
138+
}
139+
if err != nil {
140+
return fmt.Errorf("error writing to %s: %v", abs, err)
141+
}
142+
if n != f.Size {
143+
return fmt.Errorf("only wrote %d bytes to %s; expected %d", n, abs, f.Size)
144+
}
145+
log.Printf("wrote %s", abs)
146+
case mode.IsDir():
147+
if err := os.MkdirAll(abs, 0755); err != nil {
148+
return err
149+
}
150+
default:
151+
return badRequest(fmt.Sprintf("tar file entry %s contained unsupported file type %v", f.Name, mode))
152+
}
153+
}
154+
return nil
155+
}
156+
157+
func handleExec(w http.ResponseWriter, r *http.Request) {
158+
if r.Method != "POST" {
159+
http.Error(w, "requires POST method", http.StatusBadRequest)
160+
return
161+
}
162+
cmdPath := r.FormValue("cmd") // required
163+
if !validRelPath(cmdPath) {
164+
http.Error(w, "requires 'cmd' parameter", http.StatusBadRequest)
165+
return
166+
}
167+
absCmd := filepath.Join(*scratchDir, filepath.FromSlash(cmdPath))
168+
cmd := exec.Command(absCmd, r.PostForm["cmdArg"]...)
169+
cmd.Dir = filepath.Dir(absCmd)
170+
cmdOutput := &flushWriter{w: w}
171+
cmd.Stdout = cmdOutput
172+
cmd.Stderr = cmdOutput
173+
err := cmd.Run()
174+
log.Printf("Run = %v", err)
175+
// TODO: put the exit status in the HTTP trailer,
176+
// once https://golang.org/issue/7759 is fixed.
177+
}
178+
179+
// flushWriter is an io.Writer wrapper that writes to w and
180+
// Flushes the output immediately, if w is an http.Flusher.
181+
type flushWriter struct {
182+
mu sync.Mutex
183+
w http.ResponseWriter
184+
}
185+
186+
func (hw *flushWriter) Write(p []byte) (n int, err error) {
187+
hw.mu.Lock()
188+
defer hw.mu.Unlock()
189+
n, err = hw.w.Write(p)
190+
if f, ok := hw.w.(http.Flusher); ok {
191+
f.Flush()
192+
}
193+
return
194+
}
195+
196+
func validRelPath(p string) bool {
197+
if p == "" || strings.Contains(p, `\`) || strings.HasPrefix(p, "/") || strings.Contains(p, "../") {
198+
return false
199+
}
200+
return true
201+
}
202+
203+
type httpStatuser interface {
204+
error
205+
httpStatus() int
206+
}
207+
208+
type httpError struct {
209+
statusCode int
210+
msg string
211+
}
212+
213+
func (he httpError) Error() string { return he.msg }
214+
func (he httpError) httpStatus() int { return he.statusCode }
215+
216+
func badRequest(msg string) error {
217+
return httpError{http.StatusBadRequest, msg}
218+
}
219+
220+
// metaClient to fetch GCE metadata values.
221+
var metaClient = &http.Client{
222+
Transport: &http.Transport{
223+
Dial: (&net.Dialer{
224+
Timeout: 750 * time.Millisecond,
225+
KeepAlive: 30 * time.Second,
226+
}).Dial,
227+
ResponseHeaderTimeout: 750 * time.Millisecond,
228+
},
229+
}
230+
231+
var onGCE struct {
232+
sync.Mutex
233+
set bool
234+
v bool
235+
}
236+
237+
// OnGCE reports whether this process is running on Google Compute Engine.
238+
func OnGCE() bool {
239+
defer onGCE.Unlock()
240+
onGCE.Lock()
241+
if onGCE.set {
242+
return onGCE.v
243+
}
244+
onGCE.set = true
245+
246+
res, err := metaClient.Get("http://metadata.google.internal")
247+
if err != nil {
248+
return false
249+
}
250+
onGCE.v = res.Header.Get("Metadata-Flavor") == "Google"
251+
return onGCE.v
252+
}

0 commit comments

Comments
 (0)