golang.org/x/build@v0.0.0-20240506185731-218518f32b70/cmd/coordinator/internal/legacydash/ui.go (about) 1 // Copyright 2011 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 legacydash 8 9 import ( 10 "bytes" 11 "context" 12 _ "embed" 13 "encoding/json" 14 "errors" 15 "fmt" 16 "html/template" 17 "log" 18 "net/http" 19 "os" 20 "sort" 21 "strconv" 22 "strings" 23 "time" 24 25 "cloud.google.com/go/datastore" 26 bbpb "go.chromium.org/luci/buildbucket/proto" 27 "golang.org/x/build/cmd/coordinator/internal/lucipoll" 28 "golang.org/x/build/dashboard" 29 "golang.org/x/build/internal/releasetargets" 30 "golang.org/x/build/maintner/maintnerd/apipb" 31 "golang.org/x/build/repos" 32 "golang.org/x/build/types" 33 "golang.org/x/sync/errgroup" 34 "google.golang.org/grpc" 35 "google.golang.org/grpc/codes" 36 ) 37 38 // uiHandler is the HTTP handler for the https://build.golang.org/. 39 func (h handler) uiHandler(w http.ResponseWriter, r *http.Request) { 40 view, err := viewForRequest(r) 41 if err != nil { 42 http.Error(w, err.Error(), http.StatusBadRequest) 43 return 44 } 45 46 dashReq, err := dashboardRequest(view, r) 47 if err != nil { 48 http.Error(w, err.Error(), http.StatusBadRequest) 49 return 50 } 51 52 ctx := r.Context() 53 tb := &uiTemplateDataBuilder{ 54 view: view, 55 req: dashReq, 56 } 57 var rpcs errgroup.Group 58 rpcs.Go(func() error { 59 var err error 60 tb.res, err = h.maintnerCl.GetDashboard(ctx, dashReq) 61 return err 62 }) 63 if view.ShowsActiveBuilds() { 64 rpcs.Go(func() error { 65 tb.activeBuilds = getActiveBuilds(ctx) 66 return nil 67 }) 68 } 69 if err := rpcs.Wait(); err != nil { 70 http.Error(w, "maintner.GetDashboard: "+err.Error(), httpStatusOfErr(err)) 71 return 72 } 73 var luci lucipoll.Snapshot 74 if h.LUCI != nil && tb.showLUCI() { 75 luci = h.LUCI.PostSubmitSnapshot() 76 } 77 data, err := tb.buildTemplateData(ctx, h.datastoreCl, luci) 78 if err != nil { 79 http.Error(w, err.Error(), http.StatusInternalServerError) 80 return 81 } 82 view.ServeDashboard(w, r, data) 83 } 84 85 // dashboardView is something that can render uiTemplateData. 86 // See viewForRequest. 87 type dashboardView interface { 88 ServeDashboard(w http.ResponseWriter, r *http.Request, data *uiTemplateData) 89 // ShowsActiveBuilds reports whether this view uses 90 // information about the currently active builds. 91 ShowsActiveBuilds() bool 92 } 93 94 // viewForRequest selects the dashboardView based on the HTTP 95 // request's "mode" parameter. Any error should be considered 96 // an HTTP 400 Bad Request. 97 func viewForRequest(r *http.Request) (dashboardView, error) { 98 if r.Method != "GET" && r.Method != "HEAD" { 99 return nil, errors.New("unsupported method") 100 } 101 switch r.FormValue("mode") { 102 case "failures": 103 return failuresView{}, nil 104 case "json": 105 return jsonView{}, nil 106 case "": 107 var showLUCI = true 108 if legacyOnly, _ := strconv.ParseBool(r.URL.Query().Get("legacyonly")); legacyOnly { 109 showLUCI = false 110 } 111 return htmlView{ShowLUCI: showLUCI}, nil 112 } 113 return nil, errors.New("unsupported mode argument") 114 } 115 116 type commitInPackage struct { 117 packagePath string // "" for Go, else package import path 118 commit string // git commit hash 119 } 120 121 // uiTemplateDataBuilder builds the uiTemplateData used by the various 122 // dashboardViews. That is, it maps the maintner protobuf response to 123 // the data structure needed by the dashboardView/template. 124 type uiTemplateDataBuilder struct { 125 view dashboardView 126 req *apipb.DashboardRequest 127 res *apipb.DashboardResponse 128 activeBuilds []types.ActivePostSubmitBuild // optional; for blue gopher links 129 130 // testCommitData, if non-nil, provides an alternate data 131 // source to use for testing instead of making real datastore 132 // calls. The keys are stringified datastore.Keys. 133 testCommitData map[string]*Commit 134 } 135 136 // getCommitsToLoad returns a set (all values are true) of which commits to load 137 // from the datastore. If fakeResults is on, it returns an empty set. 138 func (tb *uiTemplateDataBuilder) getCommitsToLoad() map[commitInPackage]bool { 139 if fakeResults { 140 return nil 141 } 142 m := make(map[commitInPackage]bool) 143 add := func(packagePath, commit string) { 144 m[commitInPackage{packagePath: packagePath, commit: commit}] = true 145 } 146 147 for _, dc := range tb.res.Commits { 148 add(tb.req.Repo, dc.Commit) 149 } 150 // We also want to load the Commits for the x/repo heads. 151 if tb.showXRepoSection() { 152 for _, rh := range tb.res.RepoHeads { 153 if path := repoImportPath(rh); path != "" { 154 add(path, rh.Commit.Commit) 155 } 156 } 157 } 158 return m 159 } 160 161 // loadDatastoreCommits loads the commits given in the keys of the 162 // want map. The returned map is keyed by the git hash and may not 163 // contain items that didn't exist in the datastore. (It is not an 164 // error if 1 or all don't exist.) 165 func (tb *uiTemplateDataBuilder) loadDatastoreCommits(ctx context.Context, cl *datastore.Client, want map[commitInPackage]bool) (map[string]*Commit, error) { 166 var keys []*datastore.Key 167 for k := range want { 168 key := (&Commit{ 169 PackagePath: k.packagePath, 170 Hash: k.commit, 171 }).Key() 172 keys = append(keys, key) 173 } 174 commits, err := fetchCommits(ctx, cl, keys) 175 if err != nil { 176 return nil, fmt.Errorf("fetchCommits: %v", err) 177 } 178 var ret = make(map[string]*Commit) 179 for _, c := range commits { 180 ret[c.Hash] = c 181 } 182 return ret, nil 183 } 184 185 // formatGitAuthor formats the git author name and email (as split by 186 // maintner) back into the unified string how they're stored in a git 187 // commit, so the shortUser func (used by the HTML template) can parse 188 // back out the email part's username later. Maybe we could plumb down 189 // the parsed proto into the template later. 190 func formatGitAuthor(name, email string) string { 191 name = strings.TrimSpace(name) 192 email = strings.TrimSpace(email) 193 if name != "" && email != "" { 194 return fmt.Sprintf("%s <%s>", name, email) 195 } 196 if name != "" { 197 return name 198 } 199 return "<" + email + ">" 200 } 201 202 // newCommitInfo returns a new CommitInfo populated for the template 203 // data given a repo name and a dashboard commit from that repo, using 204 // previously loaded datastore commit info in tb. 205 func (tb *uiTemplateDataBuilder) newCommitInfo(dsCommits map[string]*Commit, repo string, dc *apipb.DashCommit) *CommitInfo { 206 branch := dc.Branch 207 if branch == "" { 208 branch = "master" 209 } 210 ci := &CommitInfo{ 211 Hash: dc.Commit, 212 PackagePath: repo, 213 User: formatGitAuthor(dc.AuthorName, dc.AuthorEmail), 214 Desc: cleanTitle(dc.Title, tb.branch()), 215 Time: time.Unix(dc.CommitTimeSec, 0), 216 Branch: branch, 217 } 218 if dsc, ok := dsCommits[dc.Commit]; ok { 219 // Remove extra dsc.ResultData capacity to make it 220 // okay to append additional data to ci.ResultData. 221 ci.ResultData = dsc.ResultData[:len(dsc.ResultData):len(dsc.ResultData)] 222 } 223 // For non-go repos, add the rows for the Go commits that were 224 // at HEAD overlapping in time with dc.Commit. 225 if !tb.isGoRepo() { 226 if dc.GoCommitAtTime != "" { 227 ci.addEmptyResultGoHash(dc.GoCommitAtTime) 228 } 229 if dc.GoCommitLatest != "" && dc.GoCommitLatest != dc.GoCommitAtTime { 230 ci.addEmptyResultGoHash(dc.GoCommitLatest) 231 } 232 } 233 return ci 234 } 235 236 func (tb *uiTemplateDataBuilder) showLUCI() bool { 237 v, ok := tb.view.(htmlView) 238 return ok && v == htmlView{ShowLUCI: true} && 239 // Only show LUCI build results for the default top-level view. 240 tb.req.Page == 0 && 241 tb.req.Repo == "" && 242 tb.branch() == "master" 243 } 244 245 // showXRepoSection reports whether the dashboard should show the state of the x/foo repos at the bottom of 246 // the page in the three branches (master, latest release branch, two releases ago). 247 func (tb *uiTemplateDataBuilder) showXRepoSection() bool { 248 return tb.req.Page == 0 && 249 (tb.branch() == "master" || tb.req.Branch == "mixed") && 250 tb.isGoRepo() 251 } 252 253 func (tb *uiTemplateDataBuilder) isGoRepo() bool { return tb.req.Repo == "" || tb.req.Repo == "go" } 254 255 // repoGerritProj returns the Gerrit project name on go.googlesource.com for 256 // the repo requested, or empty if unknown. 257 func (tb *uiTemplateDataBuilder) repoGerritProj() string { 258 if tb.isGoRepo() { 259 return "go" 260 } 261 if r, ok := repos.ByImportPath[tb.req.Repo]; ok { 262 return r.GoGerritProject 263 } 264 return "" 265 } 266 267 // branch returns the request branch, or "master" if empty. 268 func (tb *uiTemplateDataBuilder) branch() string { 269 if tb.req.Branch == "" { 270 return "master" 271 } 272 return tb.req.Branch 273 } 274 275 // repoImportPath returns the import path for rh, unless rh is the 276 // main "go" repo or is configured to be hidden from the dashboard, in 277 // which case it returns the empty string. 278 func repoImportPath(rh *apipb.DashRepoHead) string { 279 if rh.GerritProject == "go" { 280 return "" 281 } 282 ri, ok := repos.ByGerritProject[rh.GerritProject] 283 if !ok || !ri.ShowOnDashboard() { 284 return "" 285 } 286 return ri.ImportPath 287 } 288 289 // appendLUCIResults appends result data polled from LUCI to commits. 290 func appendLUCIResults(luci lucipoll.Snapshot, commits []*CommitInfo, repo string) { 291 commitBuilds, ok := luci.RepoCommitBuilds[repo] 292 if !ok { 293 return 294 } 295 for _, c := range commits { 296 builds, ok := commitBuilds[c.Hash] 297 if !ok { 298 // No builds for this commit. 299 continue 300 } 301 for _, b := range builds { 302 builder := luci.Builders[b.BuilderName] 303 if builder.Repo != repo || builder.GoBranch != "master" { 304 // TODO: Support builder.GoBranch != master for x/ repos. 305 // Or maybe it's fine to leave that to appendLUCIResultsXRepo only, 306 // and have this appendLUCIResults function deal repo == "go" only. 307 // Hmm, maybe it needs to handle repo != "go" for when viewing the 308 // individual repo commit views... I'll see later. 309 continue 310 } 311 switch b.Status { 312 case bbpb.Status_STARTED, bbpb.Status_SUCCESS, bbpb.Status_FAILURE, bbpb.Status_INFRA_FAILURE: 313 default: 314 continue 315 } 316 tagFriendly := builder.Name 317 if after, ok := strings.CutPrefix(tagFriendly, fmt.Sprintf("gotip-%s-%s_", builder.Target.GOOS, builder.Target.GOARCH)); ok { 318 // Convert os-arch_osversion-mod1-mod2 (an underscore at start of "_osversion") 319 // to have os-arch-osversion-mod1-mod2 (a dash at start of "-osversion") form. 320 // The tag computation below uses this to find both "osversion-mod1" or "mod1". 321 tagFriendly = fmt.Sprintf("gotip-%s-%s-", builder.Target.GOOS, builder.Target.GOARCH) + after 322 } 323 builderName := strings.TrimPrefix(tagFriendly, "gotip-") + "-🐇" 324 // Note: goHash is empty for the main Go repo. 325 goHash := "" 326 if repo != "go" { 327 // TODO: Generalize goHash for non-go builds. 328 // 329 // If the non-go build was triggered by an x/ repo trigger, 330 // its b.GetInput().GetGitilesCommit().GetId() will be the 331 // x/ repo commit, and the Go repo commit will be selected 332 // at runtime... 333 // 334 // Can get it out of output properties but that would mean 335 // not being able to support STARTED builds. 336 } 337 switch b.Status { 338 case bbpb.Status_STARTED: 339 if c.BuildingURLs == nil { 340 c.BuildingURLs = make(map[builderAndGoHash]string) 341 } 342 c.BuildingURLs[builderAndGoHash{builderName, goHash}] = buildURL(b.ID) 343 case bbpb.Status_SUCCESS, bbpb.Status_FAILURE: 344 c.ResultData = append(c.ResultData, fmt.Sprintf("%s|%t|%s|%s", 345 builderName, 346 b.Status == bbpb.Status_SUCCESS, 347 buildURL(b.ID), 348 goHash, 349 )) 350 case bbpb.Status_INFRA_FAILURE: 351 c.ResultData = append(c.ResultData, fmt.Sprintf("%s|%s|%s|%s", 352 builderName, 353 "infra_failure", 354 buildURL(b.ID), 355 goHash, 356 )) 357 } 358 } 359 } 360 } 361 362 func appendLUCIResultsXRepo(luci lucipoll.Snapshot, c *CommitInfo, repo, goBranch, goHash string) { 363 commitBuilds, ok := luci.RepoCommitBuilds[repo] 364 if !ok { 365 return 366 } 367 builds, ok := commitBuilds[c.Hash] 368 if !ok { 369 // No builds for this commit. 370 return 371 } 372 for _, b := range builds { 373 builder := luci.Builders[b.BuilderName] 374 if builder.Repo != repo || builder.GoBranch != goBranch { 375 continue 376 } 377 switch b.Status { 378 case bbpb.Status_STARTED, bbpb.Status_SUCCESS, bbpb.Status_FAILURE, bbpb.Status_INFRA_FAILURE: 379 default: 380 continue 381 } 382 // TODO: Maybe dedup builder name calculation with addLUCIBuilders and more places. 383 // That said, those few places have slightly different details, so maybe it's fine. 384 shortGoBranch := "tip" 385 if after, ok := strings.CutPrefix(goBranch, "release-branch.go"); ok { 386 shortGoBranch = after 387 } 388 tagFriendly := builder.Name 389 if after, ok := strings.CutPrefix(tagFriendly, fmt.Sprintf("x_%s-go%s-%s-%s_", repo, shortGoBranch, builder.Target.GOOS, builder.Target.GOARCH)); ok { 390 // Convert os-arch_osversion-mod1-mod2 (an underscore at start of "_osversion") 391 // to have os-arch-osversion-mod1-mod2 (a dash at start of "-osversion") form. 392 // The tag computation below uses this to find both "osversion-mod1" or "mod1". 393 tagFriendly = fmt.Sprintf("x_%s-go%s-%s-%s-", repo, shortGoBranch, builder.Target.GOOS, builder.Target.GOARCH) + after 394 } 395 // The builder name is "os-arch{-suffix}-🐇". The "🐇" is used to avoid collisions 396 // in builder names between LUCI builders and coordinator builders, and to make it 397 // possible to tell LUCI builders apart by their name. 398 builderName := strings.TrimPrefix(tagFriendly, fmt.Sprintf("x_%s-go%s-", repo, shortGoBranch)) + "-🐇" 399 switch b.Status { 400 case bbpb.Status_STARTED: 401 if c.BuildingURLs == nil { 402 c.BuildingURLs = make(map[builderAndGoHash]string) 403 } 404 c.BuildingURLs[builderAndGoHash{builderName, goHash}] = buildURL(b.ID) 405 case bbpb.Status_SUCCESS, bbpb.Status_FAILURE: 406 c.ResultData = append(c.ResultData, fmt.Sprintf("%s|%t|%s|%s", 407 builderName, 408 b.Status == bbpb.Status_SUCCESS, 409 buildURL(b.ID), 410 goHash, 411 )) 412 case bbpb.Status_INFRA_FAILURE: 413 c.ResultData = append(c.ResultData, fmt.Sprintf("%s|%s|%s|%s", 414 builderName, 415 "infra_failure", 416 buildURL(b.ID), 417 goHash, 418 )) 419 } 420 } 421 } 422 423 func buildURL(buildID int64) string { 424 return fmt.Sprintf("https://ci.chromium.org/b/%d", buildID) 425 } 426 427 func (tb *uiTemplateDataBuilder) buildTemplateData(ctx context.Context, datastoreCl *datastore.Client, luci lucipoll.Snapshot) (*uiTemplateData, error) { 428 wantCommits := tb.getCommitsToLoad() 429 var dsCommits map[string]*Commit 430 if datastoreCl != nil { 431 var err error 432 dsCommits, err = tb.loadDatastoreCommits(ctx, datastoreCl, wantCommits) 433 if err != nil { 434 return nil, err 435 } 436 } else if m := tb.testCommitData; m != nil { 437 // Allow tests to fake what the datastore would've loaded, and 438 // thus also allow tests to be run without a real (or fake) datastore. 439 dsCommits = make(map[string]*Commit) 440 for k := range wantCommits { 441 if c, ok := m[k.commit]; ok { 442 dsCommits[k.commit] = c 443 } 444 } 445 } 446 447 var commits []*CommitInfo 448 for _, dc := range tb.res.Commits { 449 ci := tb.newCommitInfo(dsCommits, tb.req.Repo, dc) 450 commits = append(commits, ci) 451 } 452 appendLUCIResults(luci, commits, tb.repoGerritProj()) 453 454 // x/ repo sections at bottom (each is a "TagState", for historical reasons) 455 var xRepoSections []*TagState 456 if tb.showXRepoSection() { 457 for _, gorel := range tb.res.Releases { 458 ts := &TagState{ 459 Name: gorel.BranchName, 460 Tag: &CommitInfo{ // only a minimally populated version is needed by the template 461 Hash: gorel.BranchCommit, 462 }, 463 } 464 for _, rh := range tb.res.RepoHeads { 465 path := repoImportPath(rh) 466 if path == "" { 467 continue 468 } 469 ci := tb.newCommitInfo(dsCommits, path, rh.Commit) 470 appendLUCIResultsXRepo(luci, ci, rh.GerritProject, ts.Branch(), gorel.BranchCommit) 471 ts.Packages = append(ts.Packages, &PackageState{ 472 Package: &Package{ 473 Name: rh.GerritProject, 474 Path: path, 475 }, 476 Commit: ci, 477 }) 478 } 479 builders := map[string]bool{} 480 for _, pkg := range ts.Packages { 481 addBuilders(builders, pkg.Package.Name, ts.Branch()) 482 } 483 if len(luci.Builders) > 0 { 484 for name := range builders { 485 if dashboard.BuildersPortedToLUCI[name] { 486 // Don't display old builders that have been ported 487 // to LUCI if willing to show LUCI builders as well. 488 delete(builders, name) 489 } 490 } 491 } 492 addLUCIBuilders(luci, builders, ts.Packages, gorel.BranchName) 493 ts.Builders = builderKeys(builders) 494 495 sort.Slice(ts.Packages, func(i, j int) bool { 496 return ts.Packages[i].Package.Name < ts.Packages[j].Package.Name 497 }) 498 xRepoSections = append(xRepoSections, ts) 499 } 500 } 501 502 gerritProject := "go" 503 if repo := repos.ByImportPath[tb.req.Repo]; repo != nil { 504 gerritProject = repo.GoGerritProject 505 } 506 507 data := &uiTemplateData{ 508 Dashboard: goDash, 509 Package: goDash.packageWithPath(tb.req.Repo), 510 Commits: commits, 511 TagState: xRepoSections, 512 Pagination: &Pagination{}, 513 Branches: tb.res.Branches, 514 Branch: tb.branch(), 515 Repo: gerritProject, 516 } 517 518 builders := buildersOfCommits(commits) 519 if tb.branch() == "mixed" { 520 for _, gr := range tb.res.Releases { 521 addBuilders(builders, tb.repoGerritProj(), gr.BranchName) 522 } 523 } else { 524 addBuilders(builders, tb.repoGerritProj(), tb.branch()) 525 } 526 if len(luci.Builders) > 0 { 527 for name := range builders { 528 if dashboard.BuildersPortedToLUCI[name] { 529 // Don't display old builders that have been ported 530 // to LUCI if willing to show LUCI builders as well. 531 delete(builders, name) 532 } 533 } 534 } 535 data.Builders = builderKeys(builders) 536 537 if tb.res.CommitsTruncated { 538 data.Pagination.Next = int(tb.req.Page) + 1 539 } 540 if tb.req.Page > 0 { 541 data.Pagination.Prev = int(tb.req.Page) - 1 542 data.Pagination.HasPrev = true 543 } 544 545 if tb.view.ShowsActiveBuilds() { 546 // Populate building URLs for the HTML UI only. 547 data.populateBuildingURLs(ctx, tb.activeBuilds) 548 549 // Appending LUCI active builds can be done here, but it's done 550 // earlier in appendLUCIResults instead because it's convenient 551 // to do there in the case of LUCI. 552 } 553 554 return data, nil 555 } 556 557 // htmlView renders the HTML (default) form of https://build.golang.org/ with no mode parameter. 558 type htmlView struct { 559 // ShowLUCI controls whether to show build results from the LUCI post-submit dashboard 560 // in addition to coordinator-backed build results from Datastore. 561 // 562 // It has no effect if there's no LUCI client. 563 ShowLUCI bool 564 } 565 566 func (htmlView) ShowsActiveBuilds() bool { return true } 567 func (htmlView) ServeDashboard(w http.ResponseWriter, r *http.Request, data *uiTemplateData) { 568 var buf bytes.Buffer 569 if err := uiTemplate.Execute(&buf, data); err != nil { 570 log.Printf("Error: %v", err) 571 http.Error(w, "Error: "+err.Error(), http.StatusInternalServerError) 572 return 573 } 574 w.Header().Set("Content-Type", "text/html; charset=utf-8") 575 buf.WriteTo(w) 576 } 577 578 // dashboardRequest is a pure function that maps the provided HTTP 579 // request to a maintner DashboardRequest and lightly validates the 580 // HTTP request for the root dashboard handler. (It does not validate 581 // that, say, branches or repos are valid.) 582 // Any returned error is an HTTP 400 Bad Request. 583 func dashboardRequest(view dashboardView, r *http.Request) (*apipb.DashboardRequest, error) { 584 page := 0 585 if s := r.FormValue("page"); s != "" { 586 var err error 587 page, err = strconv.Atoi(r.FormValue("page")) 588 if err != nil { 589 return nil, fmt.Errorf("invalid page value %q", s) 590 } 591 if page < 0 { 592 return nil, errors.New("negative page") 593 } 594 } 595 596 repo := r.FormValue("repo") // empty for main go repo, else e.g. "golang.org/x/net" 597 branch := r.FormValue("branch") // empty means "master" 598 maxCommits := commitsPerPage 599 if branch == "mixed" { 600 maxCommits = 0 // let maintner decide 601 } 602 return &apipb.DashboardRequest{ 603 Page: int32(page), 604 Repo: repo, 605 Branch: branch, 606 MaxCommits: int32(maxCommits), 607 }, nil 608 } 609 610 // cleanTitle returns a cleaned version of the provided title for 611 // users viewing the provided viewBranch. 612 func cleanTitle(title, viewBranch string) string { 613 // Don't rewrite anything for master and mixed. 614 if viewBranch == "master" || viewBranch == "mixed" { 615 return title 616 } 617 // Strip the "[release-branch.go1.n]" prefixes from commit messages 618 // when looking at a branch. 619 if strings.HasPrefix(title, "[") { 620 if i := strings.IndexByte(title, ']'); i != -1 { 621 return strings.TrimSpace(title[i+1:]) 622 } 623 } 624 return title 625 } 626 627 // failuresView renders https://build.golang.org/?mode=failures, where it outputs 628 // one line per failure on the front page, in the form: 629 // 630 // hash builder failure-url 631 type failuresView struct{} 632 633 func (failuresView) ShowsActiveBuilds() bool { return false } 634 func (failuresView) ServeDashboard(w http.ResponseWriter, r *http.Request, data *uiTemplateData) { 635 w.Header().Set("Content-Type", "text/plain") 636 for _, c := range data.Commits { 637 for _, b := range data.Builders { 638 res := c.Result(b, "") 639 if res == nil || res.OK || res.LogHash == "" { 640 continue 641 } 642 url := fmt.Sprintf("https://%v/log/%v", r.Host, res.LogHash) 643 fmt.Fprintln(w, c.Hash, b, url) 644 } 645 } 646 // TODO: this doesn't include the TagState commit. It would be 647 // needed if we want to do golang.org/issue/36131, to permit 648 // the retrybuilds command to wipe flaky non-go builds. 649 } 650 651 // jsonView renders https://build.golang.org/?mode=json. 652 // The output is a types.BuildStatus JSON object. 653 type jsonView struct{} 654 655 func (jsonView) ShowsActiveBuilds() bool { return false } 656 func (jsonView) ServeDashboard(w http.ResponseWriter, r *http.Request, data *uiTemplateData) { 657 res := toBuildStatus(r.Host, data) 658 v, _ := json.MarshalIndent(res, "", "\t") 659 w.Header().Set("Content-Type", "text/json; charset=utf-8") 660 w.Write(v) 661 } 662 663 func toBuildStatus(host string, data *uiTemplateData) types.BuildStatus { 664 // cell returns one of "" (no data), "ok", or a failure URL. 665 cell := func(res *DisplayResult) string { 666 switch { 667 case res == nil: 668 return "" 669 case res.OK: 670 return "ok" 671 } 672 return fmt.Sprintf("https://%v/log/%v", host, res.LogHash) 673 } 674 675 builders := data.allBuilders() 676 677 var res types.BuildStatus 678 res.Builders = builders 679 680 // First the commits from the main section (the requested repo) 681 for _, c := range data.Commits { 682 // The logic below works for both the go repo and other subrepos: if c is 683 // in the main go repo, ResultGoHashes returns a slice of length 1 684 // containing the empty string. 685 for _, h := range c.ResultGoHashes() { 686 rev := types.BuildRevision{ 687 Repo: data.Repo, 688 Results: make([]string, len(res.Builders)), 689 GoRevision: h, 690 } 691 commitToBuildRevision(c, &rev) 692 for i, b := range res.Builders { 693 rev.Results[i] = cell(c.Result(b, h)) 694 } 695 res.Revisions = append(res.Revisions, rev) 696 } 697 } 698 699 // Then the one commit each for the subrepos for each of the tracked tags. 700 // (tip, Go 1.4, etc) 701 for _, ts := range data.TagState { 702 for _, pkgState := range ts.Packages { 703 goRev := ts.Tag.Hash 704 goBranch := ts.Name 705 if goBranch == "tip" { 706 // Normalize old hg terminology into 707 // our git branch name. 708 goBranch = "master" 709 } 710 rev := types.BuildRevision{ 711 Repo: pkgState.Package.Name, 712 GoRevision: goRev, 713 Results: make([]string, len(res.Builders)), 714 GoBranch: goBranch, 715 } 716 commitToBuildRevision(pkgState.Commit, &rev) 717 for i, b := range res.Builders { 718 rev.Results[i] = cell(pkgState.Commit.Result(b, goRev)) 719 } 720 res.Revisions = append(res.Revisions, rev) 721 } 722 } 723 return res 724 } 725 726 // commitToBuildRevision fills in the fields of BuildRevision rev that 727 // are derived from Commit c. 728 func commitToBuildRevision(c *CommitInfo, rev *types.BuildRevision) { 729 rev.Revision = c.Hash 730 rev.Date = c.Time.Format(time.RFC3339) 731 rev.Author = c.User 732 rev.Desc = c.Desc 733 rev.Branch = c.Branch 734 } 735 736 type Pagination struct { 737 Next, Prev int 738 HasPrev bool 739 } 740 741 // fetchCommits loads any commits that exist given by keys. 742 // It is not an error if a commit doesn't exist. 743 // Only commits that were found in datastore are returned, 744 // in an unspecified order. 745 func fetchCommits(ctx context.Context, cl *datastore.Client, keys []*datastore.Key) ([]*Commit, error) { 746 if len(keys) == 0 { 747 return nil, nil 748 } 749 out := make([]*Commit, len(keys)) 750 for i := range keys { 751 out[i] = new(Commit) 752 } 753 754 err := cl.GetMulti(ctx, keys, out) 755 err = filterDatastoreError(err) 756 err = filterNoSuchEntity(err) 757 if err != nil { 758 return nil, err 759 } 760 filtered := out[:0] 761 for _, c := range out { 762 if c.Valid() { // that is, successfully loaded 763 filtered = append(filtered, c) 764 } 765 } 766 return filtered, nil 767 } 768 769 // buildersOfCommits returns the set of builders that provided 770 // Results for the provided commits. 771 func buildersOfCommits(commits []*CommitInfo) map[string]bool { 772 m := make(map[string]bool) 773 for _, commit := range commits { 774 for _, r := range commit.Results() { 775 if r.Builder != "" { 776 m[r.Builder] = true 777 } 778 } 779 } 780 return m 781 } 782 783 // addBuilders adds builders to the provided map that should be active for 784 // the named Gerrit project & branch. (Issue 19930) 785 func addBuilders(builders map[string]bool, gerritProj, branch string) { 786 for name, bc := range dashboard.Builders { 787 if bc.BuildsRepoPostSubmit(gerritProj, branch, branch) { 788 builders[name] = true 789 } 790 } 791 } 792 793 // addLUCIBuilders adds LUCI builders that match the given xRepos and goBranch 794 // to the provided builders map. 795 func addLUCIBuilders(luci lucipoll.Snapshot, builders map[string]bool, xRepos []*PackageState, goBranch string) { 796 for _, r := range xRepos { 797 repoName := r.Package.Name 798 for _, b := range luci.Builders { 799 if b.Repo != repoName || b.GoBranch != goBranch { 800 // Filter out builders whose repo or Go branch doesn't match. 801 continue 802 } 803 shortGoBranch := "tip" 804 if after, ok := strings.CutPrefix(b.GoBranch, "release-branch.go"); ok { 805 shortGoBranch = after 806 } 807 tagFriendly := b.Name 808 if after, ok := strings.CutPrefix(tagFriendly, fmt.Sprintf("x_%s-go%s-%s-%s_", b.Repo, shortGoBranch, b.Target.GOOS, b.Target.GOARCH)); ok { 809 // Convert os-arch_osversion-mod1-mod2 (an underscore at start of "_osversion") 810 // to have os-arch-osversion-mod1-mod2 (a dash at start of "-osversion") form. 811 // The tag computation below uses this to find both "osversion-mod1" or "mod1". 812 tagFriendly = fmt.Sprintf("x_%s-go%s-%s-%s-", b.Repo, shortGoBranch, b.Target.GOOS, b.Target.GOARCH) + after 813 } 814 // The builder name is "os-arch{-suffix}-🐇". The "🐇" is used to avoid collisions 815 // in builder names between LUCI builders and coordinator builders, and to make to 816 // possible to tell LUCI builders apart by their name. 817 builderName := strings.TrimPrefix(tagFriendly, fmt.Sprintf("x_%s-go%s-", b.Repo, shortGoBranch)) + "-🐇" 818 builders[builderName] = true 819 } 820 } 821 } 822 823 func builderKeys(m map[string]bool) (s []string) { 824 s = make([]string, 0, len(m)) 825 for k := range m { 826 s = append(s, k) 827 } 828 sort.Sort(builderOrder(s)) 829 return 830 } 831 832 // builderOrder implements sort.Interface, sorting builder names 833 // ("darwin-amd64", etc) first by builderPriority and then alphabetically. 834 type builderOrder []string 835 836 func (s builderOrder) Len() int { return len(s) } 837 func (s builderOrder) Swap(i, j int) { s[i], s[j] = s[j], s[i] } 838 func (s builderOrder) Less(i, j int) bool { 839 pi, pj := builderPriority(s[i]), builderPriority(s[j]) 840 if pi == pj { 841 return s[i] < s[j] 842 } 843 return pi < pj 844 } 845 846 func builderPriority(builder string) (p int) { 847 // Group race builders together. 848 if isRace(builder) { 849 return 2 850 } 851 // If the OS has a specified priority, use it. 852 if p, ok := osPriority[builderOS(builder)]; ok { 853 return p 854 } 855 // The rest. 856 return 10 857 } 858 859 func isRace(s string) bool { 860 return strings.Contains(s, "-race-") || strings.HasSuffix(s, "-race") 861 } 862 863 func unsupported(builder string) bool { 864 return !releasetargets.IsFirstClass(builderOS(builder), builderArch(builder)) 865 } 866 867 // osPriority encodes priorities for specific operating systems. 868 var osPriority = map[string]int{ 869 "all": 0, 870 "darwin": 1, "linux": 1, "windows": 1, 871 // race == 2 872 "freebsd": 3, "openbsd": 3, "netbsd": 3, 873 "android": 4, "ios": 4, 874 } 875 876 // TagState represents the state of all Packages at a branch. 877 type TagState struct { 878 Name string // Go branch name: "master", "release-branch.go1.4", etc 879 Tag *CommitInfo // current Go commit on the Name branch 880 Packages []*PackageState 881 Builders []string 882 } 883 884 // Branch returns the git branch name, converting from the old 885 // terminology we used from Go's hg days into git terminology. 886 func (ts *TagState) Branch() string { 887 if ts.Name == "tip" { 888 return "master" 889 } 890 return ts.Name 891 } 892 893 // PackageState represents the state of a Package (x/foo repo) for given Go branch. 894 type PackageState struct { 895 Package *Package 896 Commit *CommitInfo 897 } 898 899 // A CommitInfo is a struct for use by html/template package. 900 // It is not stored in the datastore. 901 type CommitInfo struct { 902 Hash string 903 904 // ResultData is a copy of the [Commit.ResultData] field from datastore, 905 // with an additional rule that the second '|'-separated value may be "infra_failure" 906 // to indicate a problem with the infrastructure rather than the code being tested. 907 ResultData []string 908 909 // BuildingURLs contains the status URL values for builds that 910 // are currently in progress for this commit. 911 BuildingURLs map[builderAndGoHash]string 912 913 PackagePath string // (empty for main repo commits) 914 User string // "Foo Bar <foo@bar.com>" 915 Desc string // git commit title 916 Time time.Time // commit time 917 Branch string // "master", "release-branch.go1.14" 918 } 919 920 // addEmptyResultGoHash adds an empty result containing goHash to 921 // ci.ResultData, unless ci already contains a result for that hash. 922 // This is used for non-go repos to show the go commits (both earliest 923 // and latest) that correspond to this repo's commit time. We add an 924 // empty result so it shows up on the dashboard (both for humans, and 925 // in JSON form for the coordinator to pick up as work). Once the 926 // coordinator does that work and posts its result, then ResultData 927 // will be populate and this turns into a no-op. 928 func (ci *CommitInfo) addEmptyResultGoHash(goHash string) { 929 for _, exist := range ci.ResultData { 930 if strings.Contains(exist, goHash) { 931 return 932 } 933 } 934 ci.ResultData = append(ci.ResultData, (&Result{GoHash: goHash}).Data()) 935 } 936 937 type uiTemplateData struct { 938 Dashboard *Dashboard 939 Package *Package 940 Commits []*CommitInfo 941 Builders []string // builders for just the main section; not the "TagState" sections 942 TagState []*TagState // x/foo repo overviews at master + last two releases 943 Pagination *Pagination 944 Branches []string 945 Branch string 946 Repo string // the repo gerrit project name. "go" if unspecified in the request. 947 } 948 949 // getActiveBuilds returns the builds that coordinator is currently doing. 950 // This isn't critical functionality so errors are logged but otherwise ignored for now. 951 // Once this is merged into the coordinator we won't need to make an RPC to get 952 // this info. See https://github.com/golang/go/issues/34744#issuecomment-563398753. 953 func getActiveBuilds(ctx context.Context) (builds []types.ActivePostSubmitBuild) { 954 ctx, cancel := context.WithTimeout(ctx, 3*time.Second) 955 defer cancel() 956 req, _ := http.NewRequest("GET", "https://farmer.golang.org/status/post-submit-active.json", nil) 957 req = req.WithContext(ctx) 958 res, err := http.DefaultClient.Do(req) 959 if err != nil { 960 log.Printf("getActiveBuilds: Do: %v", err) 961 return 962 } 963 defer res.Body.Close() 964 if res.StatusCode != 200 { 965 log.Printf("getActiveBuilds: %v", res.Status) 966 return 967 } 968 if err := json.NewDecoder(res.Body).Decode(&builds); err != nil { 969 log.Printf("getActiveBuilds: JSON decode: %v", err) 970 } 971 return builds 972 } 973 974 // populateBuildingURLs populates each commit in Commits' buildingURLs map with the 975 // URLs of builds which are currently in progress. 976 func (td *uiTemplateData) populateBuildingURLs(ctx context.Context, activeBuilds []types.ActivePostSubmitBuild) { 977 // active maps from a build record with its status URL zeroed 978 // out to the actual value of that status URL. 979 active := map[types.ActivePostSubmitBuild]string{} 980 for _, rec := range activeBuilds { 981 statusURL := rec.StatusURL 982 rec.StatusURL = "" 983 active[rec] = statusURL 984 } 985 986 condAdd := func(c *CommitInfo, rec types.ActivePostSubmitBuild) { 987 su, ok := active[rec] 988 if !ok { 989 return 990 } 991 if c.BuildingURLs == nil { 992 c.BuildingURLs = make(map[builderAndGoHash]string) 993 } 994 c.BuildingURLs[builderAndGoHash{rec.Builder, rec.GoCommit}] = su 995 } 996 997 for _, b := range td.Builders { 998 for _, c := range td.Commits { 999 condAdd(c, types.ActivePostSubmitBuild{Builder: b, Commit: c.Hash}) 1000 } 1001 } 1002 1003 // Gather pending commits for sub-repos. 1004 for _, ts := range td.TagState { 1005 goHash := ts.Tag.Hash 1006 for _, b := range td.Builders { 1007 for _, pkg := range ts.Packages { 1008 c := pkg.Commit 1009 condAdd(c, types.ActivePostSubmitBuild{ 1010 Builder: b, 1011 Commit: c.Hash, 1012 GoCommit: goHash, 1013 }) 1014 } 1015 } 1016 } 1017 } 1018 1019 // allBuilders returns the list of builders, unified over the main 1020 // section and any x/foo branch overview (TagState) sections. 1021 func (td *uiTemplateData) allBuilders() []string { 1022 m := map[string]bool{} 1023 for _, b := range td.Builders { 1024 m[b] = true 1025 } 1026 for _, ts := range td.TagState { 1027 for _, b := range ts.Builders { 1028 m[b] = true 1029 } 1030 } 1031 return builderKeys(m) 1032 } 1033 1034 var uiTemplate = template.Must( 1035 template.New("ui.html").Funcs(tmplFuncs).Parse(uiHTML), 1036 ) 1037 1038 //go:embed ui.html 1039 var uiHTML string 1040 1041 var tmplFuncs = template.FuncMap{ 1042 "builderSpans": builderSpans, 1043 "builderSubheading": builderSubheading, 1044 "builderSubheading2": builderSubheading2, 1045 "shortDesc": shortDesc, 1046 "shortHash": shortHash, 1047 "shortUser": shortUser, 1048 "unsupported": unsupported, 1049 "isUntested": isUntested, 1050 "knownIssue": knownIssue, 1051 "formatTime": formatTime, 1052 } 1053 1054 func formatTime(t time.Time) string { 1055 if t.Year() != time.Now().Year() { 1056 return t.Format("02 Jan 06") 1057 } 1058 return t.Format("02 Jan 15:04") 1059 } 1060 1061 func splitDash(s string) (string, string) { 1062 i := strings.Index(s, "-") 1063 if i >= 0 { 1064 return s[:i], s[i+1:] 1065 } 1066 return s, "" 1067 } 1068 1069 // builderOS returns the os tag for a builder string 1070 func builderOS(s string) string { 1071 os, _ := splitDash(s) 1072 return os 1073 } 1074 1075 // builderOSOrRace returns the builder OS or, if it is a race builder, "race". 1076 func builderOSOrRace(s string) string { 1077 if isRace(s) { 1078 return "race" 1079 } 1080 return builderOS(s) 1081 } 1082 1083 // builderArch returns the arch tag for a builder string 1084 func builderArch(s string) string { 1085 _, arch := splitDash(s) 1086 arch, _ = splitDash(arch) // chop third part 1087 return arch 1088 } 1089 1090 // builderSubheading returns a short arch tag for a builder string 1091 // or, if it is a race builder, the builder OS. 1092 func builderSubheading(s string) string { 1093 if isRace(s) { 1094 return builderOS(s) 1095 } 1096 return builderArch(s) 1097 } 1098 1099 // builderSubheading2 returns any third part of a hyphenated builder name. 1100 // For instance, for "linux-amd64-nocgo", it returns "nocgo". 1101 // For race builders it returns the empty string. 1102 func builderSubheading2(s string) string { 1103 if isRace(s) { 1104 // Remove "race" and just take whatever the third component is after. 1105 var split []string 1106 for _, sc := range strings.Split(s, "-") { 1107 if sc == "race" { 1108 continue 1109 } 1110 split = append(split, sc) 1111 } 1112 s = strings.Join(split, "-") 1113 } 1114 _, secondThird := splitDash(s) 1115 _, third := splitDash(secondThird) 1116 return third 1117 } 1118 1119 type builderSpan struct { 1120 N int // Total number of builders. 1121 FirstN int // Number of builders for a first-class port. 1122 OS string 1123 Unsupported bool // Unsupported means the entire span has no builders for a first-class port. 1124 } 1125 1126 // builderSpans creates a list of tags showing 1127 // the builder's operating system names, spanning 1128 // the appropriate number of columns. 1129 func builderSpans(s []string) []builderSpan { 1130 var sp []builderSpan 1131 for len(s) > 0 { 1132 i := 1 1133 os := builderOSOrRace(s[0]) 1134 for i < len(s) && builderOSOrRace(s[i]) == os { 1135 i++ 1136 } 1137 var f int // First-class ports. 1138 for _, b := range s[:i] { 1139 if unsupported(b) { 1140 continue 1141 } 1142 f++ 1143 } 1144 sp = append(sp, builderSpan{i, f, os, f == 0}) 1145 s = s[i:] 1146 } 1147 return sp 1148 } 1149 1150 // shortDesc returns the first line of a description. 1151 func shortDesc(desc string) string { 1152 if i := strings.Index(desc, "\n"); i != -1 { 1153 desc = desc[:i] 1154 } 1155 return limitStringLength(desc, 100) 1156 } 1157 1158 // shortHash returns a short version of a hash. 1159 func shortHash(hash string) string { 1160 if len(hash) > 7 { 1161 hash = hash[:7] 1162 } 1163 return hash 1164 } 1165 1166 // shortUser returns a shortened version of a user string. 1167 func shortUser(user string) string { 1168 if i, j := strings.Index(user, "<"), strings.Index(user, ">"); 0 <= i && i < j { 1169 user = user[i+1 : j] 1170 } 1171 if i := strings.Index(user, "@"); i >= 0 { 1172 return user[:i] 1173 } 1174 return user 1175 } 1176 1177 func httpStatusOfErr(err error) int { 1178 fmt.Fprintf(os.Stderr, "Got error: %#v, code %v\n", err, grpc.Code(err)) 1179 switch grpc.Code(err) { 1180 case codes.NotFound: 1181 return http.StatusNotFound 1182 case codes.InvalidArgument: 1183 return http.StatusBadRequest 1184 default: 1185 return http.StatusInternalServerError 1186 } 1187 }