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 }