github.com/graybobo/golang.org-package-offline-cache@v0.0.0-20200626051047-6608995c132f/x/tools/dashboard/coordinator/main.go (about) 1 // Copyright 2014 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 // The coordinator runs on GCE and coordinates builds in Docker containers. 6 package main // import "golang.org/x/tools/dashboard/coordinator" 7 8 import ( 9 "bytes" 10 "crypto/hmac" 11 "crypto/md5" 12 "encoding/json" 13 "flag" 14 "fmt" 15 "io" 16 "io/ioutil" 17 "log" 18 "net/http" 19 "os" 20 "os/exec" 21 "sort" 22 "strings" 23 "sync" 24 "time" 25 ) 26 27 var ( 28 masterKeyFile = flag.String("masterkey", "", "Path to builder master key. Else fetched using GCE project attribute 'builder-master-key'.") 29 maxBuilds = flag.Int("maxbuilds", 6, "Max concurrent builds") 30 31 // Debug flags: 32 addTemp = flag.Bool("temp", false, "Append -temp to all builders.") 33 just = flag.String("just", "", "If non-empty, run single build in the foreground. Requires rev.") 34 rev = flag.String("rev", "", "Revision to build.") 35 ) 36 37 var ( 38 startTime = time.Now() 39 builders = map[string]buildConfig{} // populated once at startup 40 watchers = map[string]watchConfig{} // populated once at startup 41 donec = make(chan builderRev) // reports of finished builders 42 43 statusMu sync.Mutex 44 status = map[builderRev]*buildStatus{} 45 ) 46 47 type imageInfo struct { 48 url string // of tar file 49 50 mu sync.Mutex 51 lastMod string 52 } 53 54 var images = map[string]*imageInfo{ 55 "go-commit-watcher": {url: "https://storage.googleapis.com/go-builder-data/docker-commit-watcher.tar.gz"}, 56 "gobuilders/linux-x86-base": {url: "https://storage.googleapis.com/go-builder-data/docker-linux.base.tar.gz"}, 57 "gobuilders/linux-x86-clang": {url: "https://storage.googleapis.com/go-builder-data/docker-linux.clang.tar.gz"}, 58 "gobuilders/linux-x86-gccgo": {url: "https://storage.googleapis.com/go-builder-data/docker-linux.gccgo.tar.gz"}, 59 "gobuilders/linux-x86-nacl": {url: "https://storage.googleapis.com/go-builder-data/docker-linux.nacl.tar.gz"}, 60 "gobuilders/linux-x86-sid": {url: "https://storage.googleapis.com/go-builder-data/docker-linux.sid.tar.gz"}, 61 } 62 63 type buildConfig struct { 64 name string // "linux-amd64-race" 65 image string // Docker image to use to build 66 cmd string // optional -cmd flag (relative to go/src/) 67 env []string // extra environment ("key=value") pairs 68 dashURL string // url of the build dashboard 69 tool string // the tool this configuration is for 70 } 71 72 type watchConfig struct { 73 repo string // "https://go.googlesource.com/go" 74 dash string // "https://build.golang.org/" (must end in /) 75 interval time.Duration // Polling interval 76 } 77 78 func main() { 79 flag.Parse() 80 addBuilder(buildConfig{name: "linux-386"}) 81 addBuilder(buildConfig{name: "linux-386-387", env: []string{"GO386=387"}}) 82 addBuilder(buildConfig{name: "linux-amd64"}) 83 addBuilder(buildConfig{name: "linux-amd64-nocgo", env: []string{"CGO_ENABLED=0", "USER=root"}}) 84 addBuilder(buildConfig{name: "linux-amd64-noopt", env: []string{"GO_GCFLAGS=-N -l"}}) 85 addBuilder(buildConfig{name: "linux-amd64-race"}) 86 addBuilder(buildConfig{name: "nacl-386"}) 87 addBuilder(buildConfig{name: "nacl-amd64p32"}) 88 addBuilder(buildConfig{ 89 name: "linux-amd64-gccgo", 90 image: "gobuilders/linux-x86-gccgo", 91 cmd: "make RUNTESTFLAGS=\"--target_board=unix/-m64\" check-go -j16", 92 dashURL: "https://build.golang.org/gccgo", 93 tool: "gccgo", 94 }) 95 addBuilder(buildConfig{ 96 name: "linux-386-gccgo", 97 image: "gobuilders/linux-x86-gccgo", 98 cmd: "make RUNTESTFLAGS=\"--target_board=unix/-m32\" check-go -j16", 99 dashURL: "https://build.golang.org/gccgo", 100 tool: "gccgo", 101 }) 102 addBuilder(buildConfig{name: "linux-386-sid", image: "gobuilders/linux-x86-sid"}) 103 addBuilder(buildConfig{name: "linux-amd64-sid", image: "gobuilders/linux-x86-sid"}) 104 addBuilder(buildConfig{name: "linux-386-clang", image: "gobuilders/linux-x86-clang"}) 105 addBuilder(buildConfig{name: "linux-amd64-clang", image: "gobuilders/linux-x86-clang"}) 106 107 addWatcher(watchConfig{repo: "https://go.googlesource.com/go", dash: "https://build.golang.org/"}) 108 // TODO(adg,cmang): fix gccgo watcher 109 // addWatcher(watchConfig{repo: "https://code.google.com/p/gofrontend", dash: "https://build.golang.org/gccgo/"}) 110 111 if (*just != "") != (*rev != "") { 112 log.Fatalf("--just and --rev must be used together") 113 } 114 if *just != "" { 115 conf, ok := builders[*just] 116 if !ok { 117 log.Fatalf("unknown builder %q", *just) 118 } 119 cmd := exec.Command("docker", append([]string{"run"}, conf.dockerRunArgs(*rev)...)...) 120 cmd.Stdout = os.Stdout 121 cmd.Stderr = os.Stderr 122 if err := cmd.Run(); err != nil { 123 log.Fatalf("Build failed: %v", err) 124 } 125 return 126 } 127 128 http.HandleFunc("/", handleStatus) 129 http.HandleFunc("/logs", handleLogs) 130 go http.ListenAndServe(":80", nil) 131 132 for _, watcher := range watchers { 133 if err := startWatching(watchers[watcher.repo]); err != nil { 134 log.Printf("Error starting watcher for %s: %v", watcher.repo, err) 135 } 136 } 137 138 workc := make(chan builderRev) 139 for name, builder := range builders { 140 go findWorkLoop(name, builder.dashURL, workc) 141 } 142 143 ticker := time.NewTicker(1 * time.Minute) 144 for { 145 select { 146 case work := <-workc: 147 log.Printf("workc received %+v; len(status) = %v, maxBuilds = %v; cur = %p", work, len(status), *maxBuilds, status[work]) 148 mayBuild := mayBuildRev(work) 149 if mayBuild { 150 if numBuilds() > *maxBuilds { 151 mayBuild = false 152 } 153 } 154 if mayBuild { 155 if st, err := startBuilding(builders[work.name], work.rev); err == nil { 156 setStatus(work, st) 157 log.Printf("%v now building in %v", work, st.container) 158 } else { 159 log.Printf("Error starting to build %v: %v", work, err) 160 } 161 } 162 case done := <-donec: 163 log.Printf("%v done", done) 164 setStatus(done, nil) 165 case <-ticker.C: 166 if numCurrentBuilds() == 0 && time.Now().After(startTime.Add(10*time.Minute)) { 167 // TODO: halt the whole machine to kill the VM or something 168 } 169 } 170 } 171 } 172 173 func numCurrentBuilds() int { 174 statusMu.Lock() 175 defer statusMu.Unlock() 176 return len(status) 177 } 178 179 func mayBuildRev(work builderRev) bool { 180 statusMu.Lock() 181 defer statusMu.Unlock() 182 return len(status) < *maxBuilds && status[work] == nil 183 } 184 185 func setStatus(work builderRev, st *buildStatus) { 186 statusMu.Lock() 187 defer statusMu.Unlock() 188 if st == nil { 189 delete(status, work) 190 } else { 191 status[work] = st 192 } 193 } 194 195 func getStatus(work builderRev) *buildStatus { 196 statusMu.Lock() 197 defer statusMu.Unlock() 198 return status[work] 199 } 200 201 type byAge []*buildStatus 202 203 func (s byAge) Len() int { return len(s) } 204 func (s byAge) Less(i, j int) bool { return s[i].start.Before(s[j].start) } 205 func (s byAge) Swap(i, j int) { s[i], s[j] = s[j], s[i] } 206 207 func handleStatus(w http.ResponseWriter, r *http.Request) { 208 var active []*buildStatus 209 statusMu.Lock() 210 for _, st := range status { 211 active = append(active, st) 212 } 213 statusMu.Unlock() 214 215 fmt.Fprintf(w, "<html><body><h1>Go build coordinator</h1>%d of max %d builds running:<p><pre>", len(status), *maxBuilds) 216 sort.Sort(byAge(active)) 217 for _, st := range active { 218 fmt.Fprintf(w, "%-22s hg %s in container <a href='/logs?name=%s&rev=%s'>%s</a>, %v ago\n", st.name, st.rev, st.name, st.rev, 219 st.container, time.Now().Sub(st.start)) 220 } 221 fmt.Fprintf(w, "</pre></body></html>") 222 } 223 224 func handleLogs(w http.ResponseWriter, r *http.Request) { 225 st := getStatus(builderRev{r.FormValue("name"), r.FormValue("rev")}) 226 if st == nil { 227 fmt.Fprintf(w, "<html><body><h1>not building</h1>") 228 return 229 } 230 out, err := exec.Command("docker", "logs", st.container).CombinedOutput() 231 if err != nil { 232 log.Print(err) 233 http.Error(w, "Error fetching logs. Already finished?", 500) 234 return 235 } 236 key := builderKey(st.name) 237 logs := strings.Replace(string(out), key, "BUILDERKEY", -1) 238 w.Header().Set("Content-Type", "text/plain; charset=utf-8") 239 io.WriteString(w, logs) 240 } 241 242 func findWorkLoop(builderName, dashURL string, work chan<- builderRev) { 243 // TODO: make this better 244 for { 245 rev, err := findWork(builderName, dashURL) 246 if err != nil { 247 log.Printf("Finding work for %s: %v", builderName, err) 248 } else if rev != "" { 249 work <- builderRev{builderName, rev} 250 } 251 time.Sleep(60 * time.Second) 252 } 253 } 254 255 func findWork(builderName, dashURL string) (rev string, err error) { 256 var jres struct { 257 Response struct { 258 Kind string 259 Data struct { 260 Hash string 261 PerfResults []string 262 } 263 } 264 } 265 res, err := http.Get(dashURL + "/todo?builder=" + builderName + "&kind=build-go-commit") 266 if err != nil { 267 return 268 } 269 defer res.Body.Close() 270 if res.StatusCode != 200 { 271 return "", fmt.Errorf("unexpected http status %d", res.StatusCode) 272 } 273 err = json.NewDecoder(res.Body).Decode(&jres) 274 if jres.Response.Kind == "build-go-commit" { 275 rev = jres.Response.Data.Hash 276 } 277 return rev, err 278 } 279 280 type builderRev struct { 281 name, rev string 282 } 283 284 // returns the part after "docker run" 285 func (conf buildConfig) dockerRunArgs(rev string) (args []string) { 286 if key := builderKey(conf.name); key != "" { 287 tmpKey := "/tmp/" + conf.name + ".buildkey" 288 if _, err := os.Stat(tmpKey); err != nil { 289 if err := ioutil.WriteFile(tmpKey, []byte(key), 0600); err != nil { 290 log.Fatal(err) 291 } 292 } 293 // Images may look for .gobuildkey in / or /root, so provide both. 294 // TODO(adg): fix images that look in the wrong place. 295 args = append(args, "-v", tmpKey+":/.gobuildkey") 296 args = append(args, "-v", tmpKey+":/root/.gobuildkey") 297 } 298 for _, pair := range conf.env { 299 args = append(args, "-e", pair) 300 } 301 args = append(args, 302 conf.image, 303 "/usr/local/bin/builder", 304 "-rev="+rev, 305 "-dashboard="+conf.dashURL, 306 "-tool="+conf.tool, 307 "-buildroot=/", 308 "-v", 309 ) 310 if conf.cmd != "" { 311 args = append(args, "-cmd", conf.cmd) 312 } 313 args = append(args, conf.name) 314 return 315 } 316 317 func addBuilder(c buildConfig) { 318 if c.name == "" { 319 panic("empty name") 320 } 321 if *addTemp { 322 c.name += "-temp" 323 } 324 if _, dup := builders[c.name]; dup { 325 panic("dup name") 326 } 327 if c.dashURL == "" { 328 c.dashURL = "https://build.golang.org" 329 } 330 if c.tool == "" { 331 c.tool = "go" 332 } 333 334 if strings.HasPrefix(c.name, "nacl-") { 335 if c.image == "" { 336 c.image = "gobuilders/linux-x86-nacl" 337 } 338 if c.cmd == "" { 339 c.cmd = "/usr/local/bin/build-command.pl" 340 } 341 } 342 if strings.HasPrefix(c.name, "linux-") && c.image == "" { 343 c.image = "gobuilders/linux-x86-base" 344 } 345 if c.image == "" { 346 panic("empty image") 347 } 348 builders[c.name] = c 349 } 350 351 // returns the part after "docker run" 352 func (conf watchConfig) dockerRunArgs() (args []string) { 353 log.Printf("Running watcher with master key %q", masterKey()) 354 if key := masterKey(); len(key) > 0 { 355 tmpKey := "/tmp/watcher.buildkey" 356 if _, err := os.Stat(tmpKey); err != nil { 357 if err := ioutil.WriteFile(tmpKey, key, 0600); err != nil { 358 log.Fatal(err) 359 } 360 } 361 // Images may look for .gobuildkey in / or /root, so provide both. 362 // TODO(adg): fix images that look in the wrong place. 363 args = append(args, "-v", tmpKey+":/.gobuildkey") 364 args = append(args, "-v", tmpKey+":/root/.gobuildkey") 365 } 366 args = append(args, 367 "go-commit-watcher", 368 "/usr/local/bin/watcher", 369 "-repo="+conf.repo, 370 "-dash="+conf.dash, 371 "-poll="+conf.interval.String(), 372 ) 373 return 374 } 375 376 func addWatcher(c watchConfig) { 377 if c.repo == "" { 378 c.repo = "https://go.googlesource.com/go" 379 } 380 if c.dash == "" { 381 c.dash = "https://build.golang.org/" 382 } 383 if c.interval == 0 { 384 c.interval = 10 * time.Second 385 } 386 watchers[c.repo] = c 387 } 388 389 func condUpdateImage(img string) error { 390 ii := images[img] 391 if ii == nil { 392 log.Fatalf("Image %q not described.", img) 393 } 394 ii.mu.Lock() 395 defer ii.mu.Unlock() 396 res, err := http.Head(ii.url) 397 if err != nil { 398 return fmt.Errorf("Error checking %s: %v", ii.url, err) 399 } 400 if res.StatusCode != 200 { 401 return fmt.Errorf("Error checking %s: %v", ii.url, res.Status) 402 } 403 if res.Header.Get("Last-Modified") == ii.lastMod { 404 return nil 405 } 406 407 res, err = http.Get(ii.url) 408 if err != nil || res.StatusCode != 200 { 409 return fmt.Errorf("Get after Head failed for %s: %v, %v", ii.url, err, res) 410 } 411 defer res.Body.Close() 412 413 log.Printf("Running: docker load of %s\n", ii.url) 414 cmd := exec.Command("docker", "load") 415 cmd.Stdin = res.Body 416 417 var out bytes.Buffer 418 cmd.Stdout = &out 419 cmd.Stderr = &out 420 421 if cmd.Run(); err != nil { 422 log.Printf("Failed to pull latest %s from %s and pipe into docker load: %v, %s", img, ii.url, err, out.Bytes()) 423 return err 424 } 425 ii.lastMod = res.Header.Get("Last-Modified") 426 return nil 427 } 428 429 // numBuilds finds the number of go builder instances currently running. 430 func numBuilds() int { 431 out, _ := exec.Command("docker", "ps").Output() 432 numBuilds := 0 433 ps := bytes.Split(out, []byte("\n")) 434 for _, p := range ps { 435 if bytes.HasPrefix(p, []byte("gobuilders/")) { 436 numBuilds++ 437 } 438 } 439 log.Printf("num current docker builds: %d", numBuilds) 440 return numBuilds 441 } 442 443 func startBuilding(conf buildConfig, rev string) (*buildStatus, error) { 444 if err := condUpdateImage(conf.image); err != nil { 445 log.Printf("Failed to setup container for %v %v: %v", conf.name, rev, err) 446 return nil, err 447 } 448 449 cmd := exec.Command("docker", append([]string{"run", "-d"}, conf.dockerRunArgs(rev)...)...) 450 all, err := cmd.CombinedOutput() 451 log.Printf("Docker run for %v %v = err:%v, output:%s", conf.name, rev, err, all) 452 if err != nil { 453 return nil, err 454 } 455 container := strings.TrimSpace(string(all)) 456 go func() { 457 all, err := exec.Command("docker", "wait", container).CombinedOutput() 458 log.Printf("docker wait %s/%s: %v, %s", container, rev, err, strings.TrimSpace(string(all))) 459 donec <- builderRev{conf.name, rev} 460 exec.Command("docker", "rm", container).Run() 461 }() 462 return &buildStatus{ 463 builderRev: builderRev{ 464 name: conf.name, 465 rev: rev, 466 }, 467 container: container, 468 start: time.Now(), 469 }, nil 470 } 471 472 type buildStatus struct { 473 builderRev 474 container string 475 start time.Time 476 477 mu sync.Mutex 478 // ... 479 } 480 481 func startWatching(conf watchConfig) (err error) { 482 defer func() { 483 if err != nil { 484 restartWatcherSoon(conf) 485 } 486 }() 487 log.Printf("Starting watcher for %v", conf.repo) 488 if err := condUpdateImage("go-commit-watcher"); err != nil { 489 log.Printf("Failed to setup container for commit watcher: %v", err) 490 return err 491 } 492 493 cmd := exec.Command("docker", append([]string{"run", "-d"}, conf.dockerRunArgs()...)...) 494 all, err := cmd.CombinedOutput() 495 if err != nil { 496 log.Printf("Docker run for commit watcher = err:%v, output: %s", err, all) 497 return err 498 } 499 container := strings.TrimSpace(string(all)) 500 // Start a goroutine to wait for the watcher to die. 501 go func() { 502 exec.Command("docker", "wait", container).Run() 503 exec.Command("docker", "rm", "-v", container).Run() 504 log.Printf("Watcher crashed. Restarting soon.") 505 restartWatcherSoon(conf) 506 }() 507 return nil 508 } 509 510 func restartWatcherSoon(conf watchConfig) { 511 time.AfterFunc(30*time.Second, func() { 512 startWatching(conf) 513 }) 514 } 515 516 func builderKey(builder string) string { 517 master := masterKey() 518 if len(master) == 0 { 519 return "" 520 } 521 h := hmac.New(md5.New, master) 522 io.WriteString(h, builder) 523 return fmt.Sprintf("%x", h.Sum(nil)) 524 } 525 526 func masterKey() []byte { 527 keyOnce.Do(loadKey) 528 return masterKeyCache 529 } 530 531 var ( 532 keyOnce sync.Once 533 masterKeyCache []byte 534 ) 535 536 func loadKey() { 537 if *masterKeyFile != "" { 538 b, err := ioutil.ReadFile(*masterKeyFile) 539 if err != nil { 540 log.Fatal(err) 541 } 542 masterKeyCache = bytes.TrimSpace(b) 543 return 544 } 545 req, _ := http.NewRequest("GET", "http://metadata.google.internal/computeMetadata/v1/project/attributes/builder-master-key", nil) 546 req.Header.Set("Metadata-Flavor", "Google") 547 res, err := http.DefaultClient.Do(req) 548 if err != nil { 549 log.Fatal("No builder master key available") 550 } 551 defer res.Body.Close() 552 if res.StatusCode != 200 { 553 log.Fatalf("No builder-master-key project attribute available.") 554 } 555 slurp, err := ioutil.ReadAll(res.Body) 556 if err != nil { 557 log.Fatal(err) 558 } 559 masterKeyCache = bytes.TrimSpace(slurp) 560 }