github.com/xushiwei/go@v0.0.0-20130601165731-2b9d83f45bc9/misc/dashboard/app/build/handler.go (about) 1 // Copyright 2011 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 package build 6 7 import ( 8 "crypto/hmac" 9 "crypto/md5" 10 "encoding/json" 11 "errors" 12 "fmt" 13 "net/http" 14 15 "appengine" 16 "appengine/datastore" 17 "cache" 18 ) 19 20 const commitsPerPage = 30 21 22 // commitHandler retrieves commit data or records a new commit. 23 // 24 // For GET requests it returns a Commit value for the specified 25 // packagePath and hash. 26 // 27 // For POST requests it reads a JSON-encoded Commit value from the request 28 // body and creates a new Commit entity. It also updates the "tip" Tag for 29 // each new commit at tip. 30 // 31 // This handler is used by a gobuilder process in -commit mode. 32 func commitHandler(r *http.Request) (interface{}, error) { 33 c := appengine.NewContext(r) 34 com := new(Commit) 35 36 if r.Method == "GET" { 37 com.PackagePath = r.FormValue("packagePath") 38 com.Hash = r.FormValue("hash") 39 if err := datastore.Get(c, com.Key(c), com); err != nil { 40 return nil, fmt.Errorf("getting Commit: %v", err) 41 } 42 return com, nil 43 } 44 if r.Method != "POST" { 45 return nil, errBadMethod(r.Method) 46 } 47 48 // POST request 49 defer r.Body.Close() 50 if err := json.NewDecoder(r.Body).Decode(com); err != nil { 51 return nil, fmt.Errorf("decoding Body: %v", err) 52 } 53 if len(com.Desc) > maxDatastoreStringLen { 54 com.Desc = com.Desc[:maxDatastoreStringLen] 55 } 56 if err := com.Valid(); err != nil { 57 return nil, fmt.Errorf("validating Commit: %v", err) 58 } 59 defer cache.Tick(c) 60 tx := func(c appengine.Context) error { 61 return addCommit(c, com) 62 } 63 return nil, datastore.RunInTransaction(c, tx, nil) 64 } 65 66 // addCommit adds the Commit entity to the datastore and updates the tip Tag. 67 // It must be run inside a datastore transaction. 68 func addCommit(c appengine.Context, com *Commit) error { 69 var tc Commit // temp value so we don't clobber com 70 err := datastore.Get(c, com.Key(c), &tc) 71 if err != datastore.ErrNoSuchEntity { 72 // if this commit is already in the datastore, do nothing 73 if err == nil { 74 return nil 75 } 76 return fmt.Errorf("getting Commit: %v", err) 77 } 78 // get the next commit number 79 p, err := GetPackage(c, com.PackagePath) 80 if err != nil { 81 return fmt.Errorf("GetPackage: %v", err) 82 } 83 com.Num = p.NextNum 84 p.NextNum++ 85 if _, err := datastore.Put(c, p.Key(c), p); err != nil { 86 return fmt.Errorf("putting Package: %v", err) 87 } 88 // if this isn't the first Commit test the parent commit exists 89 if com.Num > 0 { 90 n, err := datastore.NewQuery("Commit"). 91 Filter("Hash =", com.ParentHash). 92 Ancestor(p.Key(c)). 93 Count(c) 94 if err != nil { 95 return fmt.Errorf("testing for parent Commit: %v", err) 96 } 97 if n == 0 { 98 return errors.New("parent commit not found") 99 } 100 } 101 // update the tip Tag if this is the Go repo 102 if p.Path == "" { 103 t := &Tag{Kind: "tip", Hash: com.Hash} 104 if _, err = datastore.Put(c, t.Key(c), t); err != nil { 105 return fmt.Errorf("putting Tag: %v", err) 106 } 107 } 108 // put the Commit 109 if _, err = datastore.Put(c, com.Key(c), com); err != nil { 110 return fmt.Errorf("putting Commit: %v", err) 111 } 112 return nil 113 } 114 115 // tagHandler records a new tag. It reads a JSON-encoded Tag value from the 116 // request body and updates the Tag entity for the Kind of tag provided. 117 // 118 // This handler is used by a gobuilder process in -commit mode. 119 func tagHandler(r *http.Request) (interface{}, error) { 120 if r.Method != "POST" { 121 return nil, errBadMethod(r.Method) 122 } 123 124 t := new(Tag) 125 defer r.Body.Close() 126 if err := json.NewDecoder(r.Body).Decode(t); err != nil { 127 return nil, err 128 } 129 if err := t.Valid(); err != nil { 130 return nil, err 131 } 132 c := appengine.NewContext(r) 133 defer cache.Tick(c) 134 _, err := datastore.Put(c, t.Key(c), t) 135 return nil, err 136 } 137 138 // Todo is a todoHandler response. 139 type Todo struct { 140 Kind string // "build-go-commit" or "build-package" 141 Data interface{} 142 } 143 144 // todoHandler returns the next action to be performed by a builder. 145 // It expects "builder" and "kind" query parameters and returns a *Todo value. 146 // Multiple "kind" parameters may be specified. 147 func todoHandler(r *http.Request) (interface{}, error) { 148 c := appengine.NewContext(r) 149 now := cache.Now(c) 150 key := "build-todo-" + r.Form.Encode() 151 var todo *Todo 152 if cache.Get(r, now, key, &todo) { 153 return todo, nil 154 } 155 var err error 156 builder := r.FormValue("builder") 157 for _, kind := range r.Form["kind"] { 158 var data interface{} 159 switch kind { 160 case "build-go-commit": 161 data, err = buildTodo(c, builder, "", "") 162 case "build-package": 163 packagePath := r.FormValue("packagePath") 164 goHash := r.FormValue("goHash") 165 data, err = buildTodo(c, builder, packagePath, goHash) 166 } 167 if data != nil || err != nil { 168 todo = &Todo{Kind: kind, Data: data} 169 break 170 } 171 } 172 if err == nil { 173 cache.Set(r, now, key, todo) 174 } 175 return todo, err 176 } 177 178 // buildTodo returns the next Commit to be built (or nil if none available). 179 // 180 // If packagePath and goHash are empty, it scans the first 20 Go Commits in 181 // Num-descending order and returns the first one it finds that doesn't have a 182 // Result for this builder. 183 // 184 // If provided with non-empty packagePath and goHash args, it scans the first 185 // 20 Commits in Num-descending order for the specified packagePath and 186 // returns the first that doesn't have a Result for this builder and goHash. 187 func buildTodo(c appengine.Context, builder, packagePath, goHash string) (interface{}, error) { 188 p, err := GetPackage(c, packagePath) 189 if err != nil { 190 return nil, err 191 } 192 193 t := datastore.NewQuery("Commit"). 194 Ancestor(p.Key(c)). 195 Limit(commitsPerPage). 196 Order("-Num"). 197 Run(c) 198 for { 199 com := new(Commit) 200 if _, err := t.Next(com); err == datastore.Done { 201 break 202 } else if err != nil { 203 return nil, err 204 } 205 if com.Result(builder, goHash) == nil { 206 return com, nil 207 } 208 } 209 210 // Nothing left to do if this is a package (not the Go tree). 211 if packagePath != "" { 212 return nil, nil 213 } 214 215 // If there are no Go tree commits left to build, 216 // see if there are any subrepo commits that need to be built at tip. 217 // If so, ask the builder to build a go tree at the tip commit. 218 // TODO(adg): do the same for "weekly" and "release" tags. 219 220 tag, err := GetTag(c, "tip") 221 if err != nil { 222 return nil, err 223 } 224 225 // Check that this Go commit builds OK for this builder. 226 // If not, don't re-build as the subrepos will never get built anyway. 227 com, err := tag.Commit(c) 228 if err != nil { 229 return nil, err 230 } 231 if r := com.Result(builder, ""); r != nil && !r.OK { 232 return nil, nil 233 } 234 235 pkgs, err := Packages(c, "subrepo") 236 if err != nil { 237 return nil, err 238 } 239 for _, pkg := range pkgs { 240 com, err := pkg.LastCommit(c) 241 if err != nil { 242 c.Warningf("%v: no Commit found: %v", pkg, err) 243 continue 244 } 245 if com.Result(builder, tag.Hash) == nil { 246 return tag.Commit(c) 247 } 248 } 249 250 return nil, nil 251 } 252 253 // packagesHandler returns a list of the non-Go Packages monitored 254 // by the dashboard. 255 func packagesHandler(r *http.Request) (interface{}, error) { 256 kind := r.FormValue("kind") 257 c := appengine.NewContext(r) 258 now := cache.Now(c) 259 key := "build-packages-" + kind 260 var p []*Package 261 if cache.Get(r, now, key, &p) { 262 return p, nil 263 } 264 p, err := Packages(c, kind) 265 if err != nil { 266 return nil, err 267 } 268 cache.Set(r, now, key, p) 269 return p, nil 270 } 271 272 // resultHandler records a build result. 273 // It reads a JSON-encoded Result value from the request body, 274 // creates a new Result entity, and updates the relevant Commit entity. 275 // If the Log field is not empty, resultHandler creates a new Log entity 276 // and updates the LogHash field before putting the Commit entity. 277 func resultHandler(r *http.Request) (interface{}, error) { 278 if r.Method != "POST" { 279 return nil, errBadMethod(r.Method) 280 } 281 282 c := appengine.NewContext(r) 283 res := new(Result) 284 defer r.Body.Close() 285 if err := json.NewDecoder(r.Body).Decode(res); err != nil { 286 return nil, fmt.Errorf("decoding Body: %v", err) 287 } 288 if err := res.Valid(); err != nil { 289 return nil, fmt.Errorf("validating Result: %v", err) 290 } 291 defer cache.Tick(c) 292 // store the Log text if supplied 293 if len(res.Log) > 0 { 294 hash, err := PutLog(c, res.Log) 295 if err != nil { 296 return nil, fmt.Errorf("putting Log: %v", err) 297 } 298 res.LogHash = hash 299 } 300 tx := func(c appengine.Context) error { 301 // check Package exists 302 if _, err := GetPackage(c, res.PackagePath); err != nil { 303 return fmt.Errorf("GetPackage: %v", err) 304 } 305 // put Result 306 if _, err := datastore.Put(c, res.Key(c), res); err != nil { 307 return fmt.Errorf("putting Result: %v", err) 308 } 309 // add Result to Commit 310 com := &Commit{PackagePath: res.PackagePath, Hash: res.Hash} 311 if err := com.AddResult(c, res); err != nil { 312 return fmt.Errorf("AddResult: %v", err) 313 } 314 // Send build failure notifications, if necessary. 315 // Note this must run after the call AddResult, which 316 // populates the Commit's ResultData field. 317 return notifyOnFailure(c, com, res.Builder) 318 } 319 return nil, datastore.RunInTransaction(c, tx, nil) 320 } 321 322 // logHandler displays log text for a given hash. 323 // It handles paths like "/log/hash". 324 func logHandler(w http.ResponseWriter, r *http.Request) { 325 w.Header().Set("Content-type", "text/plain; charset=utf-8") 326 c := appengine.NewContext(r) 327 hash := r.URL.Path[len("/log/"):] 328 key := datastore.NewKey(c, "Log", hash, 0, nil) 329 l := new(Log) 330 if err := datastore.Get(c, key, l); err != nil { 331 logErr(w, r, err) 332 return 333 } 334 b, err := l.Text() 335 if err != nil { 336 logErr(w, r, err) 337 return 338 } 339 w.Write(b) 340 } 341 342 type dashHandler func(*http.Request) (interface{}, error) 343 344 type dashResponse struct { 345 Response interface{} 346 Error string 347 } 348 349 // errBadMethod is returned by a dashHandler when 350 // the request has an unsuitable method. 351 type errBadMethod string 352 353 func (e errBadMethod) Error() string { 354 return "bad method: " + string(e) 355 } 356 357 // AuthHandler wraps a http.HandlerFunc with a handler that validates the 358 // supplied key and builder query parameters. 359 func AuthHandler(h dashHandler) http.HandlerFunc { 360 return func(w http.ResponseWriter, r *http.Request) { 361 c := appengine.NewContext(r) 362 363 // Put the URL Query values into r.Form to avoid parsing the 364 // request body when calling r.FormValue. 365 r.Form = r.URL.Query() 366 367 var err error 368 var resp interface{} 369 370 // Validate key query parameter for POST requests only. 371 key := r.FormValue("key") 372 builder := r.FormValue("builder") 373 if r.Method == "POST" && !validKey(c, key, builder) { 374 err = errors.New("invalid key: " + key) 375 } 376 377 // Call the original HandlerFunc and return the response. 378 if err == nil { 379 resp, err = h(r) 380 } 381 382 // Write JSON response. 383 dashResp := &dashResponse{Response: resp} 384 if err != nil { 385 c.Errorf("%v", err) 386 dashResp.Error = err.Error() 387 } 388 w.Header().Set("Content-Type", "application/json") 389 if err = json.NewEncoder(w).Encode(dashResp); err != nil { 390 c.Criticalf("encoding response: %v", err) 391 } 392 } 393 } 394 395 func keyHandler(w http.ResponseWriter, r *http.Request) { 396 builder := r.FormValue("builder") 397 if builder == "" { 398 logErr(w, r, errors.New("must supply builder in query string")) 399 return 400 } 401 c := appengine.NewContext(r) 402 fmt.Fprint(w, builderKey(c, builder)) 403 } 404 405 func init() { 406 // admin handlers 407 http.HandleFunc("/init", initHandler) 408 http.HandleFunc("/key", keyHandler) 409 410 // authenticated handlers 411 http.HandleFunc("/commit", AuthHandler(commitHandler)) 412 http.HandleFunc("/packages", AuthHandler(packagesHandler)) 413 http.HandleFunc("/result", AuthHandler(resultHandler)) 414 http.HandleFunc("/tag", AuthHandler(tagHandler)) 415 http.HandleFunc("/todo", AuthHandler(todoHandler)) 416 417 // public handlers 418 http.HandleFunc("/log/", logHandler) 419 } 420 421 func validHash(hash string) bool { 422 // TODO(adg): correctly validate a hash 423 return hash != "" 424 } 425 426 func validKey(c appengine.Context, key, builder string) bool { 427 if appengine.IsDevAppServer() { 428 return true 429 } 430 if key == secretKey(c) { 431 return true 432 } 433 return key == builderKey(c, builder) 434 } 435 436 func builderKey(c appengine.Context, builder string) string { 437 h := hmac.New(md5.New, []byte(secretKey(c))) 438 h.Write([]byte(builder)) 439 return fmt.Sprintf("%x", h.Sum(nil)) 440 } 441 442 func logErr(w http.ResponseWriter, r *http.Request, err error) { 443 appengine.NewContext(r).Errorf("Error: %v", err) 444 w.WriteHeader(http.StatusInternalServerError) 445 fmt.Fprint(w, "Error: ", err) 446 }