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

     1  // Copyright 2020 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 dashboard contains the implementation of the build dashboard for the Coordinator.
     8  package dashboard
     9  
    10  import (
    11  	"bytes"
    12  	"context"
    13  	_ "embed"
    14  	"fmt"
    15  	"html/template"
    16  	"log"
    17  	"net/http"
    18  	"sort"
    19  	"strconv"
    20  	"strings"
    21  	"time"
    22  
    23  	"cloud.google.com/go/datastore"
    24  	"golang.org/x/build/cmd/coordinator/internal/lucipoll"
    25  	"golang.org/x/build/dashboard"
    26  	"golang.org/x/build/internal/releasetargets"
    27  	"golang.org/x/build/maintner/maintnerd/apipb"
    28  	"google.golang.org/grpc"
    29  )
    30  
    31  type data struct {
    32  	Branch    string
    33  	Builders  []*builder
    34  	Commits   []*commit
    35  	Dashboard struct {
    36  		Name string
    37  	}
    38  	Package    dashPackage
    39  	Pagination *struct{}
    40  	TagState   []struct{}
    41  }
    42  
    43  // MaintnerClient is a subset of apipb.MaintnerServiceClient.
    44  type MaintnerClient interface {
    45  	// GetDashboard is extracted from apipb.MaintnerServiceClient.
    46  	GetDashboard(ctx context.Context, in *apipb.DashboardRequest, opts ...grpc.CallOption) (*apipb.DashboardResponse, error)
    47  }
    48  
    49  type luciClient interface {
    50  	PostSubmitSnapshot() lucipoll.Snapshot
    51  }
    52  
    53  type Handler struct {
    54  	// Datastore is a client used for fetching build status. If nil, it uses in-memory storage of build status.
    55  	Datastore *datastore.Client
    56  	// Maintner is a client for Maintner, used for fetching lists of commits.
    57  	Maintner MaintnerClient
    58  	// LUCI is a client for LUCI, used for fetching build results from there.
    59  	LUCI luciClient
    60  
    61  	// memoryResults is an in-memory storage of CI results. Used in development and testing for datastore data.
    62  	memoryResults map[string][]string
    63  }
    64  
    65  func (d *Handler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    66  	var showLUCI = true
    67  	if legacyOnly, _ := strconv.ParseBool(req.URL.Query().Get("legacyonly")); legacyOnly {
    68  		showLUCI = false
    69  	}
    70  
    71  	var luci lucipoll.Snapshot
    72  	if d.LUCI != nil && showLUCI {
    73  		luci = d.LUCI.PostSubmitSnapshot()
    74  	}
    75  
    76  	dd := &data{
    77  		Builders: d.getBuilders(dashboard.Builders, luci),
    78  		Commits:  d.commits(req.Context(), luci),
    79  		Package:  dashPackage{Name: "Go"},
    80  	}
    81  
    82  	var buf bytes.Buffer
    83  	if err := templ.Execute(&buf, dd); err != nil {
    84  		log.Printf("handleDashboard: error rendering template: %v", err)
    85  		http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
    86  		return
    87  	}
    88  	buf.WriteTo(w)
    89  }
    90  
    91  func (d *Handler) commits(ctx context.Context, luci lucipoll.Snapshot) []*commit {
    92  	resp, err := d.Maintner.GetDashboard(ctx, &apipb.DashboardRequest{})
    93  	if err != nil {
    94  		log.Printf("handleDashboard: error fetching from maintner: %v", err)
    95  		return nil
    96  	}
    97  	var commits []*commit
    98  	for _, c := range resp.GetCommits() {
    99  		commits = append(commits, &commit{
   100  			Desc: c.Title,
   101  			Hash: c.Commit,
   102  			Time: time.Unix(c.CommitTimeSec, 0).Format("02 Jan 15:04"),
   103  			User: formatGitAuthor(c.AuthorName, c.AuthorEmail),
   104  		})
   105  	}
   106  	d.getResults(ctx, commits, luci)
   107  	return commits
   108  }
   109  
   110  // getResults populates result data on commits, fetched from Datastore or in-memory storage
   111  // and, if luci is non-zero, also from LUCI.
   112  func (d *Handler) getResults(ctx context.Context, commits []*commit, luci lucipoll.Snapshot) {
   113  	if d.Datastore != nil {
   114  		getDatastoreResults(ctx, d.Datastore, commits, "go")
   115  	} else {
   116  		for _, c := range commits {
   117  			if result, ok := d.memoryResults[c.Hash]; ok {
   118  				c.ResultData = result
   119  			}
   120  		}
   121  	}
   122  	appendLUCIResults(luci, commits, "go")
   123  }
   124  
   125  func (d *Handler) getBuilders(conf map[string]*dashboard.BuildConfig, luci lucipoll.Snapshot) []*builder {
   126  	bm := make(map[string]builder)
   127  	for _, b := range conf {
   128  		if !b.BuildsRepoPostSubmit("go", "master", "master") {
   129  			continue
   130  		}
   131  		if dashboard.BuildersPortedToLUCI[b.Name] && len(luci.Builders) > 0 {
   132  			// Don't display old builders that have been ported
   133  			// to LUCI if willing to show LUCI builders as well.
   134  			continue
   135  		}
   136  		db := bm[b.GOOS()]
   137  		db.OS = b.GOOS()
   138  		db.Archs = append(db.Archs, &arch{
   139  			os: b.GOOS(), Arch: b.GOARCH(),
   140  			Name: b.Name,
   141  			// Tag is the part after "os-arch", if any, without leading dash.
   142  			Tag: strings.TrimPrefix(strings.TrimPrefix(b.Name, fmt.Sprintf("%s-%s", b.GOOS(), b.GOARCH())), "-"),
   143  		})
   144  		bm[b.GOOS()] = db
   145  	}
   146  
   147  	for _, b := range luci.Builders {
   148  		if b.Repo != "go" || b.GoBranch != "master" {
   149  			continue
   150  		}
   151  		db := bm[b.Target.GOOS]
   152  		db.OS = b.Target.GOOS
   153  		tagFriendly := b.Name + "-🐇"
   154  		if after, ok := strings.CutPrefix(tagFriendly, fmt.Sprintf("gotip-%s-%s_", b.Target.GOOS, b.Target.GOARCH)); ok {
   155  			// Convert os-arch_osversion-mod1-mod2 (an underscore at start of "_osversion")
   156  			// to have os-arch-osversion-mod1-mod2 (a dash at start of "-osversion") form.
   157  			// The tag computation below uses this to find both "osversion-mod1" or "mod1".
   158  			tagFriendly = fmt.Sprintf("gotip-%s-%s-", b.Target.GOOS, b.Target.GOARCH) + after
   159  		}
   160  		db.Archs = append(db.Archs, &arch{
   161  			os: b.Target.GOOS, Arch: b.Target.GOARCH,
   162  			Name: b.Name,
   163  			// Tag is the part after "os-arch", if any, without leading dash.
   164  			Tag: strings.TrimPrefix(strings.TrimPrefix(tagFriendly, fmt.Sprintf("gotip-%s-%s", b.Target.GOOS, b.Target.GOARCH)), "-"),
   165  		})
   166  		bm[b.Target.GOOS] = db
   167  	}
   168  
   169  	var builders builderSlice
   170  	for _, db := range bm {
   171  		db := db
   172  		sort.Sort(&db.Archs)
   173  		builders = append(builders, &db)
   174  	}
   175  	sort.Sort(builders)
   176  	return builders
   177  }
   178  
   179  type arch struct {
   180  	os, Arch string
   181  	Name     string
   182  	Tag      string
   183  }
   184  
   185  func (a arch) FirstClass() bool { return releasetargets.IsFirstClass(a.os, a.Arch) }
   186  
   187  type archSlice []*arch
   188  
   189  func (d archSlice) Len() int {
   190  	return len(d)
   191  }
   192  
   193  // Less sorts first-class ports first, then it sorts by name.
   194  func (d archSlice) Less(i, j int) bool {
   195  	iFirst, jFirst := d[i].FirstClass(), d[j].FirstClass()
   196  	if iFirst && !jFirst {
   197  		return true
   198  	}
   199  	if !iFirst && jFirst {
   200  		return false
   201  	}
   202  	return d[i].Name < d[j].Name
   203  }
   204  
   205  func (d archSlice) Swap(i, j int) {
   206  	d[i], d[j] = d[j], d[i]
   207  }
   208  
   209  type builder struct {
   210  	Active      bool
   211  	Archs       archSlice
   212  	OS          string
   213  	Unsupported bool
   214  }
   215  
   216  func (b *builder) FirstClass() bool {
   217  	for _, a := range b.Archs {
   218  		if a.FirstClass() {
   219  			return true
   220  		}
   221  	}
   222  	return false
   223  }
   224  
   225  func (b *builder) FirstClassArchs() archSlice {
   226  	var as archSlice
   227  	for _, a := range b.Archs {
   228  		if a.FirstClass() {
   229  			as = append(as, a)
   230  		}
   231  	}
   232  	return as
   233  }
   234  
   235  type builderSlice []*builder
   236  
   237  func (d builderSlice) Len() int {
   238  	return len(d)
   239  }
   240  
   241  // Less sorts first-class ports first, then it sorts by name.
   242  func (d builderSlice) Less(i, j int) bool {
   243  	iFirst, jFirst := d[i].FirstClass(), d[j].FirstClass()
   244  	if iFirst && !jFirst {
   245  		return true
   246  	}
   247  	if !iFirst && jFirst {
   248  		return false
   249  	}
   250  	return d[i].OS < d[j].OS
   251  }
   252  
   253  func (d builderSlice) Swap(i, j int) {
   254  	d[i], d[j] = d[j], d[i]
   255  }
   256  
   257  type dashPackage struct {
   258  	Name string
   259  	Path string
   260  }
   261  
   262  type commit struct {
   263  	Desc string
   264  	Hash string
   265  	// ResultData is a copy of the [Commit.ResultData] field from datastore,
   266  	// with an additional rule that the second '|'-separated value may be "infra_failure"
   267  	// to indicate a problem with the infrastructure rather than the code being tested.
   268  	//
   269  	// It can also have the form of "builder|BuildingURL" for in progress builds.
   270  	ResultData []string
   271  	Time       string
   272  	User       string
   273  }
   274  
   275  // ShortUser returns a shortened version of a user string.
   276  func (c *commit) ShortUser() string {
   277  	user := c.User
   278  	if i, j := strings.Index(user, "<"), strings.Index(user, ">"); 0 <= i && i < j {
   279  		user = user[i+1 : j]
   280  	}
   281  	if i := strings.Index(user, "@"); i >= 0 {
   282  		return user[:i]
   283  	}
   284  	return user
   285  }
   286  
   287  func (c *commit) ResultForBuilder(builder string) *result {
   288  	for _, rd := range c.ResultData {
   289  		segs := strings.Split(rd, "|")
   290  		if len(segs) == 2 && segs[0] == builder {
   291  			return &result{
   292  				BuildingURL: segs[1],
   293  			}
   294  		}
   295  		if len(segs) < 4 {
   296  			continue
   297  		}
   298  		if segs[0] == builder {
   299  			return &result{
   300  				OK:      segs[1] == "true",
   301  				Noise:   segs[1] == "infra_failure",
   302  				LogHash: segs[2],
   303  			}
   304  		}
   305  	}
   306  	return nil
   307  }
   308  
   309  type result struct {
   310  	BuildingURL string
   311  	OK          bool
   312  	Noise       bool
   313  	LogHash     string
   314  }
   315  
   316  func (r result) LogURL() string {
   317  	if strings.HasPrefix(r.LogHash, "https://") {
   318  		return r.LogHash
   319  	} else {
   320  		return "https://build.golang.org/log/" + r.LogHash
   321  	}
   322  }
   323  
   324  // formatGitAuthor formats the git author name and email (as split by
   325  // maintner) back into the unified string how they're stored in a git
   326  // commit, so the shortUser func (used by the HTML template) can parse
   327  // back out the email part's username later. Maybe we could plumb down
   328  // the parsed proto into the template later.
   329  func formatGitAuthor(name, email string) string {
   330  	name = strings.TrimSpace(name)
   331  	email = strings.TrimSpace(email)
   332  	if name != "" && email != "" {
   333  		return fmt.Sprintf("%s <%s>", name, email)
   334  	}
   335  	if name != "" {
   336  		return name
   337  	}
   338  	return "<" + email + ">"
   339  }
   340  
   341  //go:embed dashboard.html
   342  var dashboardTemplate string
   343  
   344  var templ = template.Must(
   345  	template.New("dashboard.html").Funcs(template.FuncMap{
   346  		"shortHash": shortHash,
   347  	}).Parse(dashboardTemplate),
   348  )
   349  
   350  // shortHash returns a short version of a hash.
   351  func shortHash(hash string) string {
   352  	if len(hash) > 7 {
   353  		hash = hash[:7]
   354  	}
   355  	return hash
   356  }
   357  
   358  // A Commit describes an individual commit in a package.
   359  //
   360  // Each Commit entity is a descendant of its associated Package entity.
   361  // In other words, all Commits with the same PackagePath belong to the same
   362  // datastore entity group.
   363  type Commit struct {
   364  	PackagePath string // (empty for main repo commits)
   365  	Hash        string
   366  
   367  	// ResultData is the Data string of each build Result for this Commit.
   368  	// For non-Go commits, only the Results for the current Go tip, weekly,
   369  	// and release Tags are stored here. This is purely de-normalized data.
   370  	// The complete data set is stored in Result entities.
   371  	//
   372  	// Each string is formatted as builder|OK|LogHash|GoHash.
   373  	ResultData []string `datastore:",noindex"`
   374  }