blob: a79aa164d8b773106d6535761f25b36ec60750aa [file] [log] [blame]
Brad Fitzpatrick7b6d1b12015-07-05 16:22:16 -07001// 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
5// Code related to remote buildlets. See x/build/remote-buildlet.txt
6
7package main // import "golang.org/x/build/cmd/coordinator"
8
9import (
10 "bytes"
11 "encoding/json"
12 "fmt"
13 "html"
14 "log"
15 "net/http"
16 "net/http/httputil"
17 "sort"
18 "strings"
19 "sync"
20 "time"
21
22 "golang.org/x/build/buildlet"
23 "golang.org/x/build/dashboard"
Evan Brown956434c2015-10-02 15:38:22 -070024 "golang.org/x/net/context"
Brad Fitzpatrick7b6d1b12015-07-05 16:22:16 -070025)
26
27var (
28 remoteBuildlets = struct {
29 sync.Mutex
30 m map[string]*remoteBuildlet // keyed by buildletName
31 }{m: map[string]*remoteBuildlet{}}
32
33 cleanTimer *time.Timer
34)
35
36const (
37 remoteBuildletIdleTimeout = 30 * time.Minute
38 remoteBuildletCleanInterval = time.Minute
39)
40
41func init() {
42 cleanTimer = time.AfterFunc(remoteBuildletCleanInterval, expireBuildlets)
43}
44
45type remoteBuildlet struct {
46 User string // "user-foo" build key
47 Name string // dup of key
48 Type string
49 Created time.Time
50 Expires time.Time
51
52 buildlet *buildlet.Client
53}
54
55func addRemoteBuildlet(rb *remoteBuildlet) (name string) {
56 remoteBuildlets.Lock()
57 defer remoteBuildlets.Unlock()
58 n := 0
59 for {
60 name = fmt.Sprintf("%s-%s-%d", rb.User, rb.Type, n)
61 if _, ok := remoteBuildlets.m[name]; ok {
62 n++
63 } else {
64 remoteBuildlets.m[name] = rb
65 return name
66 }
67 }
68}
69
Brad Fitzpatrick7b6d1b12015-07-05 16:22:16 -070070func expireBuildlets() {
71 defer cleanTimer.Reset(remoteBuildletCleanInterval)
72 remoteBuildlets.Lock()
73 defer remoteBuildlets.Unlock()
74 now := time.Now()
75 for name, rb := range remoteBuildlets.m {
76 if !rb.Expires.IsZero() && rb.Expires.Before(now) {
Brad Fitzpatrick8bad2a82015-09-15 22:18:37 +000077 go rb.buildlet.Close()
Brad Fitzpatrick7b6d1b12015-07-05 16:22:16 -070078 delete(remoteBuildlets.m, name)
79 }
80 }
81}
82
83// always wrapped in requireBuildletProxyAuth.
84func handleBuildletCreate(w http.ResponseWriter, r *http.Request) {
85 if r.Method != "POST" {
86 http.Error(w, "POST required", 400)
87 return
88 }
89 typ := r.FormValue("type")
90 if typ == "" {
91 http.Error(w, "missing 'type' parameter", 400)
92 return
93 }
94 bconf, ok := dashboard.Builders[typ]
95 if !ok {
96 http.Error(w, "unknown builder type in 'type' parameter", 400)
97 return
98 }
99 user, _, _ := r.BasicAuth()
100 pool := poolForConf(bconf)
101
102 // rev is just a comment basically. The GCE pool uses it for
103 // naming.
Brad Fitzpatrickabd5ab32015-07-14 11:34:25 -0700104 rev := user // includes "user-" prefix.
Brad Fitzpatrick7b6d1b12015-07-05 16:22:16 -0700105
106 var closeNotify <-chan bool
107 if cn, ok := w.(http.CloseNotifier); ok {
108 closeNotify = cn.CloseNotify()
109 }
Evan Brown956434c2015-10-02 15:38:22 -0700110
111 ctx, cancel := context.WithCancel(context.Background())
Brad Fitzpatrick7b6d1b12015-07-05 16:22:16 -0700112
113 resc := make(chan *buildlet.Client)
114 errc := make(chan error)
115 go func() {
Evan Brown956434c2015-10-02 15:38:22 -0700116 bc, err := pool.GetBuildlet(ctx, typ, rev, eventTimeLoggerFunc(func(event string, optText ...string) {
Brad Fitzpatrick7b6d1b12015-07-05 16:22:16 -0700117 var extra string
118 if len(optText) > 0 {
119 extra = " " + optText[0]
120 }
121 log.Printf("creating buildlet %s for %s: %s%s", typ, user, event, extra)
122 }))
123 if bc != nil {
124 resc <- bc
125 return
126 }
127 errc <- err
128 }()
129 for {
130 select {
131 case bc := <-resc:
132 rb := &remoteBuildlet{
133 User: user,
134 Type: typ,
135 buildlet: bc,
136 Created: time.Now(),
137 Expires: time.Now().Add(remoteBuildletIdleTimeout),
138 }
139 rb.Name = addRemoteBuildlet(rb)
140 jenc, err := json.MarshalIndent(rb, "", " ")
141 if err != nil {
142 http.Error(w, err.Error(), 500)
143 log.Print(err)
144 return
145 }
146 log.Printf("created buildlet %v for %v", rb.Name, rb.User)
147 w.Header().Set("Content-Type", "application/json; charset=utf-8")
148 jenc = append(jenc, '\n')
149 w.Write(jenc)
150 return
151 case err := <-errc:
152 log.Printf("error creating buildlet: %v", err)
153 http.Error(w, err.Error(), 500)
154 return
155 case <-closeNotify:
156 log.Printf("client went away during buildlet create request")
Evan Brown956434c2015-10-02 15:38:22 -0700157 cancel()
Brad Fitzpatrick7b6d1b12015-07-05 16:22:16 -0700158 closeNotify = nil // unnecessary, but habit.
159 }
160 }
161}
162
163// always wrapped in requireBuildletProxyAuth.
164func handleBuildletList(w http.ResponseWriter, r *http.Request) {
165 if r.Method != "GET" {
166 http.Error(w, "POST required", 400)
167 return
168 }
169 res := make([]*remoteBuildlet, 0) // so it's never JSON "null"
170 remoteBuildlets.Lock()
171 defer remoteBuildlets.Unlock()
172 user, _, _ := r.BasicAuth()
173 for _, rb := range remoteBuildlets.m {
174 if rb.User == user {
175 res = append(res, rb)
176 }
177 }
178 sort.Sort(byBuildletName(res))
179 jenc, err := json.MarshalIndent(res, "", " ")
180 if err != nil {
181 http.Error(w, err.Error(), 500)
182 return
183 }
184 jenc = append(jenc, '\n')
185 w.Header().Set("Content-Type", "application/json; charset=utf-8")
186 w.Write(jenc)
187}
188
189type byBuildletName []*remoteBuildlet
190
191func (s byBuildletName) Len() int { return len(s) }
192func (s byBuildletName) Less(i, j int) bool { return s[i].Name < s[j].Name }
193func (s byBuildletName) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
194
195func remoteBuildletStatus() string {
196 remoteBuildlets.Lock()
197 defer remoteBuildlets.Unlock()
198
199 if len(remoteBuildlets.m) == 0 {
200 return "<i>(none)</i>"
201 }
202
203 var buf bytes.Buffer
204 var all []*remoteBuildlet
205 for _, rb := range remoteBuildlets.m {
206 all = append(all, rb)
207 }
208 sort.Sort(byBuildletName(all))
209
210 buf.WriteString("<ul>")
211 for _, rb := range all {
212 fmt.Fprintf(&buf, "<li><b>%s</b>, created %v ago, expires in %v</li>\n",
213 html.EscapeString(rb.Name),
214 time.Since(rb.Created), rb.Expires.Sub(time.Now()))
215 }
216 buf.WriteString("</ul>")
217
218 return buf.String()
219}
220
221// httpRouter separates out HTTP traffic being proxied
222// to buildlets on behalf of remote clients from traffic
223// destined for the coordiantor itself (the default).
224type httpRouter struct{}
225
226func (httpRouter) ServeHTTP(w http.ResponseWriter, r *http.Request) {
227 if r.Header.Get("X-Buildlet-Proxy") != "" {
228 requireBuildletProxyAuth(http.HandlerFunc(proxyBuildletHTTP)).ServeHTTP(w, r)
229 } else {
230 http.DefaultServeMux.ServeHTTP(w, r)
231 }
232}
233
234func proxyBuildletHTTP(w http.ResponseWriter, r *http.Request) {
235 if r.TLS == nil {
236 http.Error(w, "https required", http.StatusBadRequest)
237 return
238 }
239 buildletName := r.Header.Get("X-Buildlet-Proxy")
240 if buildletName == "" {
241 http.Error(w, "missing X-Buildlet-Proxy; server misconfig", http.StatusInternalServerError)
242 return
243 }
244 remoteBuildlets.Lock()
245 rb, ok := remoteBuildlets.m[buildletName]
246 if ok {
247 rb.Expires = time.Now().Add(remoteBuildletIdleTimeout)
248 }
249 remoteBuildlets.Unlock()
250 if !ok {
251 http.Error(w, "unknown or expired buildlet", http.StatusBadGateway)
252 return
253 }
254 user, _, _ := r.BasicAuth()
255 if rb.User != user {
256 http.Error(w, "you don't own that buildlet", http.StatusUnauthorized)
257 return
258 }
259
260 if r.Method == "POST" && r.URL.Path == "/halt" {
Brad Fitzpatrick8bad2a82015-09-15 22:18:37 +0000261 err := rb.buildlet.Close()
Brad Fitzpatrick7b6d1b12015-07-05 16:22:16 -0700262 if err != nil {
263 http.Error(w, err.Error(), http.StatusInternalServerError)
264 }
265 rb.buildlet.Close()
266 remoteBuildlets.Lock()
267 delete(remoteBuildlets.m, buildletName)
268 remoteBuildlets.Unlock()
269 return
270 }
271
272 outReq, err := http.NewRequest(r.Method, rb.buildlet.URL()+r.URL.Path+"?"+r.URL.RawQuery, r.Body)
273 if err != nil {
274 log.Printf("bad proxy request: %v", err)
275 http.Error(w, err.Error(), http.StatusInternalServerError)
276 return
277 }
278 outReq.Header = r.Header
279 proxy := &httputil.ReverseProxy{
280 Director: func(*http.Request) {}, // nothing
281 Transport: rb.buildlet.ProxyRoundTripper(),
282 FlushInterval: 500 * time.Millisecond,
283 }
284 proxy.ServeHTTP(w, outReq)
285}
286
287func requireBuildletProxyAuth(h http.Handler) http.Handler {
288 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
289 user, pass, ok := r.BasicAuth()
290 if !ok {
291 http.Error(w, "missing required authentication", 400)
292 return
293 }
294 if !strings.HasPrefix(user, "user-") || builderKey(user) != pass {
295 http.Error(w, "bad username or password", 401)
296 return
297 }
298 h.ServeHTTP(w, r)
299 })
300}