blob: 0283bd6f1797a2258e09530a401b0d17ac6feed6 [file] [log] [blame]
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package main
import (
"context"
"crypto/hmac"
"crypto/md5"
"encoding/json"
"errors"
"fmt"
"html"
"log"
"net/http"
"strconv"
"strings"
"unicode/utf8"
"cloud.google.com/go/datastore"
"golang.org/x/build/app/key"
)
const (
commitsPerPage = 30
builderVersion = 1 // must match x/build/cmd/coordinator/dash.go's value
)
// resultHandler records a build result.
// It reads a JSON-encoded Result value from the request body,
// creates a new Result entity, and creates or updates the relevant Commit entity.
// If the Log field is not empty, resultHandler creates a new Log entity
// and updates the LogHash field before putting the Commit entity.
func resultHandler(r *http.Request) (interface{}, error) {
if r.Method != "POST" {
return nil, errBadMethod(r.Method)
}
v, _ := strconv.Atoi(r.FormValue("version"))
if v != builderVersion {
return nil, fmt.Errorf("rejecting POST from builder; need version %v instead of %v",
builderVersion, v)
}
ctx := r.Context()
res := new(Result)
defer r.Body.Close()
if err := json.NewDecoder(r.Body).Decode(res); err != nil {
return nil, fmt.Errorf("decoding Body: %v", err)
}
if err := res.Valid(); err != nil {
return nil, fmt.Errorf("validating Result: %v", err)
}
// store the Log text if supplied
if len(res.Log) > 0 {
hash, err := PutLog(ctx, res.Log)
if err != nil {
return nil, fmt.Errorf("putting Log: %v", err)
}
res.LogHash = hash
}
tx := func(tx *datastore.Transaction) error {
if _, err := getOrMakePackageInTx(ctx, tx, res.PackagePath); err != nil {
return fmt.Errorf("GetPackage: %v", err)
}
// put Result
if _, err := tx.Put(res.Key(), res); err != nil {
return fmt.Errorf("putting Result: %v", err)
}
// add Result to Commit
com := &Commit{PackagePath: res.PackagePath, Hash: res.Hash}
if err := com.AddResult(tx, res); err != nil {
return fmt.Errorf("AddResult: %v", err)
}
return nil
}
_, err := datastoreClient.RunInTransaction(ctx, tx)
return nil, err
}
// logHandler displays log text for a given hash.
// It handles paths like "/log/hash".
func logHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-type", "text/plain; charset=utf-8")
c := r.Context()
hash := r.URL.Path[strings.LastIndex(r.URL.Path, "/")+1:]
key := dsKey("Log", hash, nil)
l := new(Log)
if err := datastoreClient.Get(c, key, l); err != nil {
if err == datastore.ErrNoSuchEntity {
// Fall back to default namespace;
// maybe this was on the old dashboard.
key.Namespace = ""
err = datastoreClient.Get(c, key, l)
}
if err != nil {
logErr(w, r, err)
return
}
}
b, err := l.Text()
if err != nil {
logErr(w, r, err)
return
}
w.Write(b)
}
// clearResultsHandler purge a single build failure from the dashboard.
// It currently only supports the main Go repo.
func clearResultsHandler(r *http.Request) (interface{}, error) {
if r.Method != "POST" {
return nil, errBadMethod(r.Method)
}
builder := r.FormValue("builder")
hash := r.FormValue("hash")
if builder == "" {
return nil, errors.New("missing 'builder'")
}
if hash == "" {
return nil, errors.New("missing 'hash'")
}
ctx := r.Context()
_, err := datastoreClient.RunInTransaction(ctx, func(tx *datastore.Transaction) error {
c := &Commit{
PackagePath: "", // TODO(adg): support clearing sub-repos
Hash: hash,
}
err := tx.Get(c.Key(), c)
err = filterDatastoreError(err)
if err == datastore.ErrNoSuchEntity {
// Doesn't exist, so no build to clear.
return nil
}
if err != nil {
return err
}
r := c.Result(builder, "")
if r == nil {
// No result, so nothing to clear.
return nil
}
c.RemoveResult(r)
_, err = tx.Put(c.Key(), c)
if err != nil {
return err
}
return tx.Delete(r.Key())
})
return nil, err
}
type dashHandler func(*http.Request) (interface{}, error)
type dashResponse struct {
Response interface{}
Error string
}
// errBadMethod is returned by a dashHandler when
// the request has an unsuitable method.
type errBadMethod string
func (e errBadMethod) Error() string {
return "bad method: " + string(e)
}
func builderKeyRevoked(builder string) bool {
switch builder {
case "plan9-amd64-mischief":
// Broken and unmaintained for months.
// It's polluting the dashboard.
return true
case "linux-arm-onlinenet":
// Requested to be revoked by Dave Cheney.
// The machine is in a fail+report loop
// and can't be accessed. Revoke it for now.
return true
}
return false
}
// AuthHandler wraps a http.HandlerFunc with a handler that validates the
// supplied key and builder query parameters.
func AuthHandler(h dashHandler) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
c := r.Context()
// Put the URL Query values into r.Form to avoid parsing the
// request body when calling r.FormValue.
r.Form = r.URL.Query()
var err error
var resp interface{}
// Validate key query parameter for POST requests only.
key := r.FormValue("key")
builder := r.FormValue("builder")
if r.Method == "POST" && !validKey(c, key, builder) {
err = fmt.Errorf("invalid key %q for builder %q", key, builder)
}
// Call the original HandlerFunc and return the response.
if err == nil {
resp, err = h(r)
}
// Write JSON response.
dashResp := &dashResponse{Response: resp}
if err != nil {
log.Printf("%v", err)
dashResp.Error = err.Error()
}
w.Header().Set("Content-Type", "application/json")
if err = json.NewEncoder(w).Encode(dashResp); err != nil {
log.Printf("encoding response: %v", err)
}
}
}
// validHash reports whether hash looks like a valid git commit hash.
func validHash(hash string) bool {
// TODO: correctly validate a hash: check that it's exactly 40
// lowercase hex digits. But this is what we historically did:
return hash != ""
}
func validKey(c context.Context, key, builder string) bool {
if isMasterKey(c, key) {
return true
}
if builderKeyRevoked(builder) {
return false
}
return key == builderKey(c, builder)
}
var devModeMasterKey string
func masterKey(ctx context.Context) string {
if *dev {
return devModeMasterKey
}
return key.Secret(ctx, datastoreClient)
}
func isMasterKey(ctx context.Context, k string) bool {
return k == masterKey(ctx)
}
func builderKey(ctx context.Context, builder string) string {
h := hmac.New(md5.New, []byte(masterKey(ctx)))
h.Write([]byte(builder))
return fmt.Sprintf("%x", h.Sum(nil))
}
func logErr(w http.ResponseWriter, r *http.Request, err error) {
log.Printf("Error: %v", err)
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprint(w, "Error: ", html.EscapeString(err.Error()))
}
// limitStringLength essentially does return s[:max],
// but it ensures that we dot not split UTF-8 rune in half.
// Otherwise appengine python scripts will break badly.
func limitStringLength(s string, max int) string {
if len(s) <= max {
return s
}
for {
s = s[:max]
r, size := utf8.DecodeLastRuneInString(s)
if r != utf8.RuneError || size != 1 {
return s
}
max--
}
}