blob: 6f2735b3f4bdea9d3116a4c9abab0894955d1fcd [file] [log] [blame]
// Copyright 2020 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build go1.13 && (linux || darwin)
// +build go1.13
// +build linux darwin
// Package dashboard contains the implementation of the build dashboard for the Coordinator.
package dashboard
import (
"bytes"
"context"
"fmt"
"html/template"
"log"
"net/http"
"sort"
"strings"
"time"
"cloud.google.com/go/datastore"
"golang.org/x/build/cmd/coordinator/internal"
"golang.org/x/build/dashboard"
"golang.org/x/build/maintner/maintnerd/apipb"
grpc4 "grpc.go4.org"
)
var firstClassPorts = map[string]bool{
"darwin-amd64": true,
"linux-386": true,
"linux-amd64": true,
"linux-arm": true,
"linux-arm64": true,
"windows-386": true,
"windows-amd64": true,
}
type data struct {
Branch string
Builders []*builder
Commits []*commit
Dashboard struct {
Name string
}
Package dashPackage
Pagination *struct{}
TagState []struct{}
}
// MaintnerClient is a subset of apipb.MaintnerServiceClient.
type MaintnerClient interface {
// GetDashboard is extracted from apipb.MaintnerServiceClient.
GetDashboard(ctx context.Context, in *apipb.DashboardRequest, opts ...grpc4.CallOption) (*apipb.DashboardResponse, error)
}
type Handler struct {
// Datastore is a client used for fetching build status. If nil, it uses in-memory storage of build status.
Datastore *datastore.Client
// Maintner is a client for Maintner, used for fetching lists of commits.
Maintner MaintnerClient
// memoryResults is an in-memory storage of CI results. Used in development and testing for datastore data.
memoryResults map[string][]string
}
func (d *Handler) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
dd := &data{
Builders: d.getBuilders(dashboard.Builders),
Commits: d.commits(r.Context()),
Package: dashPackage{Name: "Go"},
}
var buf bytes.Buffer
if err := templ.Execute(&buf, dd); err != nil {
log.Printf("handleDashboard: error rendering template: %v", err)
http.Error(rw, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
buf.WriteTo(rw)
}
func (d *Handler) commits(ctx context.Context) []*commit {
var commits []*commit
resp, err := d.Maintner.GetDashboard(ctx, &apipb.DashboardRequest{})
if err != nil {
log.Printf("handleDashboard: error fetching from maintner: %v", err)
return commits
}
for _, c := range resp.GetCommits() {
commits = append(commits, &commit{
Desc: c.Title,
Hash: c.Commit,
Time: time.Unix(c.CommitTimeSec, 0).Format("02 Jan 15:04"),
User: formatGitAuthor(c.AuthorName, c.AuthorEmail),
})
}
d.getResults(ctx, commits)
return commits
}
// getResults populates result data on commits, fetched from Datastore or in-memory storage.
func (d *Handler) getResults(ctx context.Context, commits []*commit) {
if d.Datastore == nil {
for _, c := range commits {
if result, ok := d.memoryResults[c.Hash]; ok {
c.ResultData = result
}
}
return
}
getDatastoreResults(ctx, d.Datastore, commits, "go")
}
func (d *Handler) getBuilders(conf map[string]*dashboard.BuildConfig) []*builder {
bm := make(map[string]builder)
for _, b := range conf {
if !b.BuildsRepoPostSubmit("go", "master", "master") {
continue
}
db := bm[b.GOOS()]
db.OS = b.GOOS()
db.Archs = append(db.Archs, &arch{
Arch: b.GOARCH(),
Name: b.Name,
Tag: strings.TrimPrefix(b.Name, fmt.Sprintf("%s-%s-", b.GOOS(), b.GOARCH())),
})
bm[b.GOOS()] = db
}
var builders builderSlice
for _, db := range bm {
db := db
sort.Sort(&db.Archs)
builders = append(builders, &db)
}
sort.Sort(builders)
return builders
}
type arch struct {
Arch string
Name string
Tag string
}
func (a arch) FirstClass() bool {
segs := strings.SplitN(a.Name, "-", 3)
if len(segs) < 2 {
return false
}
if fc, ok := firstClassPorts[strings.Join(segs[0:2], "-")]; ok {
return fc
}
return false
}
type archSlice []*arch
func (d archSlice) Len() int {
return len(d)
}
// Less sorts first-class ports first, then it sorts by name.
func (d archSlice) Less(i, j int) bool {
iFirst, jFirst := d[i].FirstClass(), d[j].FirstClass()
if iFirst && !jFirst {
return true
}
if !iFirst && jFirst {
return false
}
return d[i].Name < d[j].Name
}
func (d archSlice) Swap(i, j int) {
d[i], d[j] = d[j], d[i]
}
type builder struct {
Active bool
Archs archSlice
OS string
Unsupported bool
}
func (b *builder) FirstClass() bool {
for _, a := range b.Archs {
if a.FirstClass() {
return true
}
}
return false
}
func (b *builder) FirstClassArchs() archSlice {
var as archSlice
for _, a := range b.Archs {
if a.FirstClass() {
as = append(as, a)
}
}
return as
}
type builderSlice []*builder
func (d builderSlice) Len() int {
return len(d)
}
// Less sorts first-class ports first, then it sorts by name.
func (d builderSlice) Less(i, j int) bool {
iFirst, jFirst := d[i].FirstClass(), d[j].FirstClass()
if iFirst && !jFirst {
return true
}
if !iFirst && jFirst {
return false
}
return d[i].OS < d[j].OS
}
func (d builderSlice) Swap(i, j int) {
d[i], d[j] = d[j], d[i]
}
type dashPackage struct {
Name string
Path string
}
type commit struct {
Desc string
Hash string
ResultData []string
Time string
User string
}
// shortUser returns a shortened version of a user string.
func (c *commit) ShortUser() string {
user := c.User
if i, j := strings.Index(user, "<"), strings.Index(user, ">"); 0 <= i && i < j {
user = user[i+1 : j]
}
if i := strings.Index(user, "@"); i >= 0 {
return user[:i]
}
return user
}
func (c *commit) ResultForBuilder(builder string) result {
for _, rd := range c.ResultData {
segs := strings.Split(rd, "|")
if len(segs) < 4 {
continue
}
if segs[0] == builder {
return result{
OK: segs[1] == "true",
LogHash: segs[2],
}
}
}
return result{}
}
type result struct {
BuildingURL string
OK bool
LogHash string
}
// formatGitAuthor formats the git author name and email (as split by
// maintner) back into the unified string how they're stored in a git
// commit, so the shortUser func (used by the HTML template) can parse
// back out the email part's username later. Maybe we could plumb down
// the parsed proto into the template later.
func formatGitAuthor(name, email string) string {
name = strings.TrimSpace(name)
email = strings.TrimSpace(email)
if name != "" && email != "" {
return fmt.Sprintf("%s <%s>", name, email)
}
if name != "" {
return name
}
return "<" + email + ">"
}
var templ = template.Must(
template.New("dashboard.html").ParseFiles(
internal.FilePath("dashboard.html", "internal/dashboard", "cmd/coordinator/internal/dashboard"),
),
)
// A Commit describes an individual commit in a package.
//
// Each Commit entity is a descendant of its associated Package entity.
// In other words, all Commits with the same PackagePath belong to the same
// datastore entity group.
type Commit struct {
PackagePath string // (empty for main repo commits)
Hash string
// ResultData is the Data string of each build Result for this Commit.
// For non-Go commits, only the Results for the current Go tip, weekly,
// and release Tags are stored here. This is purely de-normalized data.
// The complete data set is stored in Result entities.
//
// Each string is formatted as builder|OK|LogHash|GoHash.
ResultData []string `datastore:",noindex"`
}