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  }