golang.org/x/build@v0.0.0-20240506185731-218518f32b70/cmd/coordinator/dash.go (about)

     1  // 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  //go:build linux || darwin
     6  
     7  // Code interacting with build.golang.org ("the dashboard").
     8  
     9  package main
    10  
    11  import (
    12  	"bytes"
    13  	"context"
    14  	"crypto/hmac"
    15  	"crypto/md5"
    16  	"encoding/json"
    17  	"errors"
    18  	"fmt"
    19  	"io"
    20  	"log"
    21  	"net/http"
    22  	"net/url"
    23  	"os"
    24  	"strings"
    25  	"sync"
    26  	"time"
    27  
    28  	"cloud.google.com/go/compute/metadata"
    29  	"golang.org/x/build/internal/buildgo"
    30  	"golang.org/x/build/internal/coordinator/pool"
    31  	"golang.org/x/build/internal/secret"
    32  )
    33  
    34  // dash is copied from the builder binary. It runs the given method and command on the dashboard.
    35  //
    36  // TODO(bradfitz,adg): unify this somewhere?
    37  //
    38  // If args is non-nil it is encoded as the URL query string.
    39  // If req is non-nil it is JSON-encoded and passed as the body of the HTTP POST.
    40  // If resp is non-nil the server's response is decoded into the value pointed
    41  // to by resp (resp must be a pointer).
    42  func dash(meth, cmd string, args url.Values, req, resp interface{}) error {
    43  	const builderVersion = 1 // keep in sync with cmd/coordinator/internal/legacydash/handler.go
    44  	argsCopy := url.Values{"version": {fmt.Sprint(builderVersion)}}
    45  	for k, v := range args {
    46  		if k == "version" {
    47  			panic(`dash: reserved args key: "version"`)
    48  		}
    49  		argsCopy[k] = v
    50  	}
    51  	var r *http.Response
    52  	var err error
    53  	cmd = pool.NewGCEConfiguration().BuildEnv().DashBase() + cmd + "?" + argsCopy.Encode()
    54  	switch meth {
    55  	case "GET":
    56  		if req != nil {
    57  			log.Panicf("%s to %s with req", meth, cmd)
    58  		}
    59  		r, err = http.Get(cmd)
    60  	case "POST":
    61  		var body io.Reader
    62  		if req != nil {
    63  			b, err := json.Marshal(req)
    64  			if err != nil {
    65  				return err
    66  			}
    67  			body = bytes.NewBuffer(b)
    68  		}
    69  		r, err = http.Post(cmd, "text/json", body)
    70  	default:
    71  		log.Panicf("%s: invalid method %q", cmd, meth)
    72  		panic("invalid method: " + meth)
    73  	}
    74  	if err != nil {
    75  		return err
    76  	}
    77  	defer r.Body.Close()
    78  	if r.StatusCode != http.StatusOK {
    79  		return fmt.Errorf("bad http response: %v", r.Status)
    80  	}
    81  	body := new(bytes.Buffer)
    82  	if _, err := body.ReadFrom(r.Body); err != nil {
    83  		return err
    84  	}
    85  
    86  	// Read JSON-encoded Response into provided resp
    87  	// and return an error if present.
    88  	if err = json.Unmarshal(body.Bytes(), resp); err != nil {
    89  		log.Printf("json unmarshal %#q: %s\n", body.Bytes(), err)
    90  		return err
    91  	}
    92  
    93  	return nil
    94  }
    95  
    96  // recordResult sends build results to the dashboard.
    97  // This is not used for trybot runs; only those after commit.
    98  // The URLs end up looking like https://build.golang.org/log/$HEXDIGEST
    99  func recordResult(br buildgo.BuilderRev, ok bool, buildLog string, runTime time.Duration) error {
   100  	req := map[string]interface{}{
   101  		"Builder":     br.Name,
   102  		"PackagePath": "",
   103  		"Hash":        br.Rev,
   104  		"GoHash":      "",
   105  		"OK":          ok,
   106  		"Log":         buildLog,
   107  		"RunTime":     runTime,
   108  	}
   109  	if br.IsSubrepo() {
   110  		req["PackagePath"] = importPathOfRepo(br.SubName)
   111  		req["Hash"] = br.SubRev
   112  		req["GoHash"] = br.Rev
   113  	}
   114  	args := url.Values{"key": {builderKey(br.Name)}, "builder": {br.Name}}
   115  	if *mode == "dev" {
   116  		log.Printf("In dev mode, not recording result: %v", req)
   117  		return nil
   118  	}
   119  	var result struct {
   120  		Response interface{}
   121  		Error    string
   122  	}
   123  	if err := dash("POST", "result", args, req, &result); err != nil {
   124  		return err
   125  	}
   126  	if result.Error != "" {
   127  		return errors.New(result.Error)
   128  	}
   129  	return nil
   130  }
   131  
   132  var (
   133  	keyOnce        sync.Once
   134  	masterKeyCache []byte
   135  )
   136  
   137  // mustInitMasterKeyCache populates the masterKeyCache
   138  // with the master key. If no master key is found it will
   139  // log an error and exit. If `masterKey()` is called before
   140  // the key is initialized then it will log an error and exit.
   141  func mustInitMasterKeyCache(sc *secret.Client) {
   142  	keyOnce.Do(func() { loadKey(sc) })
   143  }
   144  
   145  func masterKey() []byte {
   146  	if len(masterKeyCache) == 0 {
   147  		log.Fatalf("No builder master key available")
   148  	}
   149  	return masterKeyCache
   150  }
   151  
   152  func builderKey(builder string) string {
   153  	master := masterKey()
   154  	if len(master) == 0 {
   155  		return ""
   156  	}
   157  	h := hmac.New(md5.New, master)
   158  	io.WriteString(h, builder)
   159  	return fmt.Sprintf("%x", h.Sum(nil))
   160  }
   161  
   162  func loadKey(sc *secret.Client) {
   163  	if *masterKeyFile != "" {
   164  		b, err := os.ReadFile(*masterKeyFile)
   165  		if err != nil {
   166  			log.Fatal(err)
   167  		}
   168  		masterKeyCache = bytes.TrimSpace(b)
   169  		return
   170  	}
   171  	if *mode == "dev" {
   172  		masterKeyCache = []byte("gophers rule")
   173  		return
   174  	}
   175  
   176  	if !metadata.OnGCE() {
   177  		log.Fatalf("No builder master key provided")
   178  	}
   179  
   180  	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
   181  	defer cancel()
   182  
   183  	masterKey, err := sc.Retrieve(ctx, secret.NameBuilderMasterKey)
   184  	if err != nil {
   185  		log.Fatalf("No builder master key available: %v", err)
   186  	}
   187  	masterKeyCache = []byte(strings.TrimSpace(masterKey))
   188  }