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  }