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 }