golang.org/x/build@v0.0.0-20240506185731-218518f32b70/cmd/coordinator/internal/legacydash/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  //go:build linux || darwin
     6  
     7  package legacydash
     8  
     9  import (
    10  	"crypto/hmac"
    11  	"crypto/md5"
    12  	"encoding/json"
    13  	"errors"
    14  	"fmt"
    15  	"log"
    16  	"net/http"
    17  	"strconv"
    18  	"strings"
    19  	"unicode/utf8"
    20  
    21  	"cloud.google.com/go/datastore"
    22  )
    23  
    24  const (
    25  	commitsPerPage = 30
    26  	builderVersion = 1 // must match x/build/cmd/coordinator/dash.go's value
    27  )
    28  
    29  // resultHandler records a build result.
    30  // It reads a JSON-encoded Result value from the request body,
    31  // creates a new Result entity, and creates or updates the relevant Commit entity.
    32  // If the Log field is not empty, resultHandler creates a new Log entity
    33  // and updates the LogHash field before putting the Commit entity.
    34  func (h handler) resultHandler(r *http.Request) (interface{}, error) {
    35  	if r.Method != "POST" {
    36  		return nil, errBadMethod(r.Method)
    37  	}
    38  
    39  	v, _ := strconv.Atoi(r.FormValue("version"))
    40  	if v != builderVersion {
    41  		return nil, fmt.Errorf("rejecting POST from builder; need version %v instead of %v",
    42  			builderVersion, v)
    43  	}
    44  
    45  	ctx := r.Context()
    46  	res := new(Result)
    47  	defer r.Body.Close()
    48  	if err := json.NewDecoder(r.Body).Decode(res); err != nil {
    49  		return nil, fmt.Errorf("decoding Body: %v", err)
    50  	}
    51  	if err := res.Valid(); err != nil {
    52  		return nil, fmt.Errorf("validating Result: %v", err)
    53  	}
    54  	// store the Log text if supplied
    55  	if len(res.Log) > 0 {
    56  		hash, err := h.putLog(ctx, res.Log)
    57  		if err != nil {
    58  			return nil, fmt.Errorf("putting Log: %v", err)
    59  		}
    60  		res.LogHash = hash
    61  	}
    62  	tx := func(tx *datastore.Transaction) error {
    63  		if _, err := getOrMakePackageInTx(ctx, tx, res.PackagePath); err != nil {
    64  			return fmt.Errorf("GetPackage: %v", err)
    65  		}
    66  		// put Result
    67  		if _, err := tx.Put(res.Key(), res); err != nil {
    68  			return fmt.Errorf("putting Result: %v", err)
    69  		}
    70  		// add Result to Commit
    71  		com := &Commit{PackagePath: res.PackagePath, Hash: res.Hash}
    72  		if err := com.AddResult(tx, res); err != nil {
    73  			return fmt.Errorf("AddResult: %v", err)
    74  		}
    75  		return nil
    76  	}
    77  	_, err := h.datastoreCl.RunInTransaction(ctx, tx)
    78  	return nil, err
    79  }
    80  
    81  // logHandler displays log text for a given hash.
    82  // It handles paths like "/log/hash".
    83  func (h handler) logHandler(w http.ResponseWriter, r *http.Request) {
    84  	if h.datastoreCl == nil {
    85  		http.Error(w, "no datastore client", http.StatusNotFound)
    86  		return
    87  	}
    88  	c := r.Context()
    89  	hash := r.URL.Path[strings.LastIndex(r.URL.Path, "/")+1:]
    90  	key := dsKey("Log", hash, nil)
    91  	l := new(Log)
    92  	if err := h.datastoreCl.Get(c, key, l); err != nil {
    93  		if err == datastore.ErrNoSuchEntity {
    94  			// Fall back to default namespace;
    95  			// maybe this was on the old dashboard.
    96  			key.Namespace = ""
    97  			err = h.datastoreCl.Get(c, key, l)
    98  		}
    99  		if err != nil {
   100  			log.Printf("Error: %v", err)
   101  			http.Error(w, "Error: "+err.Error(), http.StatusInternalServerError)
   102  			return
   103  		}
   104  	}
   105  	b, err := l.Text()
   106  	if err != nil {
   107  		log.Printf("Error: %v", err)
   108  		http.Error(w, "Error: "+err.Error(), http.StatusInternalServerError)
   109  		return
   110  	}
   111  	w.Header().Set("Content-type", "text/plain; charset=utf-8")
   112  	w.Write(b)
   113  }
   114  
   115  // clearResultsHandler purge a single build failure from the dashboard.
   116  // It currently only supports the main Go repo.
   117  func (h handler) clearResultsHandler(r *http.Request) (interface{}, error) {
   118  	if r.Method != "POST" {
   119  		return nil, errBadMethod(r.Method)
   120  	}
   121  	builder := r.FormValue("builder")
   122  	hash := r.FormValue("hash")
   123  	if builder == "" {
   124  		return nil, errors.New("missing 'builder'")
   125  	}
   126  	if hash == "" {
   127  		return nil, errors.New("missing 'hash'")
   128  	}
   129  
   130  	ctx := r.Context()
   131  
   132  	_, err := h.datastoreCl.RunInTransaction(ctx, func(tx *datastore.Transaction) error {
   133  		c := &Commit{
   134  			PackagePath: "", // TODO(adg): support clearing sub-repos
   135  			Hash:        hash,
   136  		}
   137  		err := tx.Get(c.Key(), c)
   138  		err = filterDatastoreError(err)
   139  		if err == datastore.ErrNoSuchEntity {
   140  			// Doesn't exist, so no build to clear.
   141  			return nil
   142  		}
   143  		if err != nil {
   144  			return err
   145  		}
   146  
   147  		r := c.Result(builder, "")
   148  		if r == nil {
   149  			// No result, so nothing to clear.
   150  			return nil
   151  		}
   152  		c.RemoveResult(r)
   153  		_, err = tx.Put(c.Key(), c)
   154  		if err != nil {
   155  			return err
   156  		}
   157  		return tx.Delete(r.Key())
   158  	})
   159  	return nil, err
   160  }
   161  
   162  type dashHandler func(*http.Request) (interface{}, error)
   163  
   164  type dashResponse struct {
   165  	Response interface{}
   166  	Error    string
   167  }
   168  
   169  // errBadMethod is returned by a dashHandler when
   170  // the request has an unsuitable method.
   171  type errBadMethod string
   172  
   173  func (e errBadMethod) Error() string {
   174  	return "bad method: " + string(e)
   175  }
   176  
   177  func builderKeyRevoked(builder string) bool {
   178  	switch builder {
   179  	case "plan9-amd64-mischief":
   180  		// Broken and unmaintained for months.
   181  		// It's polluting the dashboard.
   182  		return true
   183  	case "linux-arm-onlinenet":
   184  		// Requested to be revoked by Dave Cheney.
   185  		// The machine is in a fail+report loop
   186  		// and can't be accessed. Revoke it for now.
   187  		return true
   188  	}
   189  	return false
   190  }
   191  
   192  // authHandler wraps an http.HandlerFunc with a handler that validates the
   193  // supplied key and builder query parameters with the provided key checker.
   194  type authHandler struct {
   195  	kc keyCheck
   196  	h  dashHandler
   197  }
   198  
   199  func (a authHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
   200  	{ // Block to improve diff readability. Can be unnested later.
   201  		// Put the URL Query values into r.Form to avoid parsing the
   202  		// request body when calling r.FormValue.
   203  		r.Form = r.URL.Query()
   204  
   205  		var err error
   206  		var resp interface{}
   207  
   208  		// Validate key query parameter for POST requests only.
   209  		key := r.FormValue("key")
   210  		builder := r.FormValue("builder")
   211  		if r.Method == "POST" && !a.kc.ValidKey(key, builder) {
   212  			err = fmt.Errorf("invalid key %q for builder %q", key, builder)
   213  		}
   214  
   215  		// Call the original HandlerFunc and return the response.
   216  		if err == nil {
   217  			resp, err = a.h(r)
   218  		}
   219  
   220  		// Write JSON response.
   221  		dashResp := &dashResponse{Response: resp}
   222  		if err != nil {
   223  			log.Printf("%v", err)
   224  			dashResp.Error = err.Error()
   225  		}
   226  		w.Header().Set("Content-Type", "application/json")
   227  		if err = json.NewEncoder(w).Encode(dashResp); err != nil {
   228  			log.Printf("encoding response: %v", err)
   229  		}
   230  	}
   231  }
   232  
   233  // validHash reports whether hash looks like a valid git commit hash.
   234  func validHash(hash string) bool {
   235  	// TODO: correctly validate a hash: check that it's exactly 40
   236  	// lowercase hex digits. But this is what we historically did:
   237  	return hash != ""
   238  }
   239  
   240  type keyCheck struct {
   241  	// The builder master key.
   242  	masterKey string
   243  }
   244  
   245  func (kc keyCheck) ValidKey(key, builder string) bool {
   246  	if kc.isMasterKey(key) {
   247  		return true
   248  	}
   249  	if builderKeyRevoked(builder) {
   250  		return false
   251  	}
   252  	return key == kc.builderKey(builder)
   253  }
   254  
   255  func (kc keyCheck) isMasterKey(k string) bool {
   256  	return k == kc.masterKey
   257  }
   258  
   259  func (kc keyCheck) builderKey(builder string) string {
   260  	h := hmac.New(md5.New, []byte(kc.masterKey))
   261  	h.Write([]byte(builder))
   262  	return fmt.Sprintf("%x", h.Sum(nil))
   263  }
   264  
   265  // limitStringLength essentially does return s[:max],
   266  // but it ensures that we dot not split UTF-8 rune in half.
   267  // Otherwise appengine python scripts will break badly.
   268  func limitStringLength(s string, max int) string {
   269  	if len(s) <= max {
   270  		return s
   271  	}
   272  	for {
   273  		s = s[:max]
   274  		r, size := utf8.DecodeLastRuneInString(s)
   275  		if r != utf8.RuneError || size != 1 {
   276  			return s
   277  		}
   278  		max--
   279  	}
   280  }