| // Copyright 2016 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. |
| |
| // Package devapp implements a simple App Engine app for generating |
| // and serving Go project release dashboards using the godash |
| // command/library. |
| package devapp |
| |
| import ( |
| "bytes" |
| "fmt" |
| "io" |
| "log" |
| "net/http" |
| "strings" |
| "sync/atomic" |
| "time" |
| |
| "golang.org/x/build/gerrit" |
| "golang.org/x/build/godash" |
| "golang.org/x/net/context" |
| ) |
| |
| const entityPrefix = "DevApp" |
| |
| var gerritTransport http.RoundTripper |
| |
| func init() { |
| for _, page := range []string{"release", "cl"} { |
| page := page |
| http.Handle("/"+page, hstsHandler(func(w http.ResponseWriter, r *http.Request) { servePage(w, r, page) })) |
| } |
| http.Handle("/dash", hstsHandler(showDash)) |
| http.Handle("/update", ctxHandler(update)) |
| // Defined in stats.go |
| http.HandleFunc("/stats/raw", rawHandler) |
| http.HandleFunc("/stats/svg", svgHandler) |
| http.Handle("/stats/release", ctxHandler(release)) |
| http.Handle("/stats/release/data.js", ctxHandler(releaseData)) |
| http.Handle("/update/stats", ctxHandler(updateStats)) |
| } |
| |
| // hstsHandler wraps an http.HandlerFunc such that it sets the HSTS header. |
| func hstsHandler(fn http.HandlerFunc) http.Handler { |
| return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| w.Header().Set("Strict-Transport-Security", "max-age=31536000; preload") |
| fn(w, r) |
| }) |
| } |
| |
| func ctxHandler(fn func(ctx context.Context, w http.ResponseWriter, r *http.Request) error) http.Handler { |
| return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| ctx := getContext(r) |
| if err := fn(ctx, w, r); err != nil { |
| logger.Criticalf(ctx, "handler failed: %v", err) |
| http.Error(w, err.Error(), 500) |
| } |
| }) |
| } |
| |
| func logFn(ctx context.Context, w io.Writer) func(string, ...interface{}) { |
| stdLogger := log.New(w, "", log.Lmicroseconds) |
| return func(format string, args ...interface{}) { |
| stdLogger.Printf(format, args...) |
| logger.Infof(ctx, format, args...) |
| } |
| } |
| |
| type Page struct { |
| // Content is the complete HTML of the page. |
| Content []byte `datastore:"content,noindex"` |
| } |
| |
| func servePage(w http.ResponseWriter, r *http.Request, page string) { |
| ctx := getContext(r) |
| entity, err := getPage(ctx, page) |
| if err != nil { |
| http.Error(w, "page not found", 404) |
| return |
| } |
| w.Header().Set("Content-Type", "text/html; charset=utf-8") |
| w.Write(entity.Content) |
| } |
| |
| type countTransport struct { |
| http.RoundTripper |
| count int64 |
| } |
| |
| func (ct *countTransport) RoundTrip(req *http.Request) (*http.Response, error) { |
| atomic.AddInt64(&ct.count, 1) |
| return ct.RoundTripper.RoundTrip(req) |
| } |
| |
| func (ct *countTransport) Count() int64 { |
| return atomic.LoadInt64(&ct.count) |
| } |
| |
| func update(ctx context.Context, w http.ResponseWriter, _ *http.Request) error { |
| token, err := getToken(ctx) |
| if err != nil { |
| return err |
| } |
| gzdata, _ := getCache(ctx, "gzdata") |
| ct := &countTransport{newTransport(ctx), 0} |
| gh := godash.NewGitHubClient("golang/go", token, ct) |
| defer func() { |
| logger.Infof(ctx, "Sent %d requests to GitHub", ct.Count()) |
| }() |
| ger := gerrit.NewClient("https://go-review.googlesource.com", gerrit.NoAuth) |
| // Without a deadline, urlfetch will use a 5s timeout which is too slow for Gerrit. |
| gerctx, cancel := context.WithTimeout(ctx, 9*time.Minute) |
| defer cancel() |
| ger.HTTPClient = &http.Client{Transport: newTransport(gerctx)} |
| |
| data, err := parseData(gzdata) |
| if err != nil { |
| return err |
| } |
| |
| if err := data.Reviewers.LoadGithub(ctx, gh); err != nil { |
| logger.Criticalf(ctx, "failed to load reviewers: %v", err) |
| return err |
| } |
| l := logFn(ctx, w) |
| if err := data.FetchData(gerctx, gh, ger, l, 7, false, false); err != nil { |
| logger.Criticalf(ctx, "failed to fetch data: %v", err) |
| return err |
| } |
| |
| for _, cls := range []bool{false, true} { |
| var output bytes.Buffer |
| kind := "release" |
| if cls { |
| kind = "CL" |
| } |
| fmt.Fprintf(&output, "Go %s dashboard\n", kind) |
| fmt.Fprintf(&output, "%v\n\n", time.Now().UTC().Format(time.UnixDate)) |
| fmt.Fprintf(&output, "HOWTO\n\n") |
| if cls { |
| data.PrintCLs(&output) |
| } else { |
| fmt.Fprintf(&output, fmt.Sprintf(`<a href="/stats/release?cycle=%d">Go 1.%d Issue Stats Dashboard</a>`, data.GoReleaseCycle, data.GoReleaseCycle)) |
| data.PrintIssues(&output) |
| } |
| var html bytes.Buffer |
| godash.PrintHTML(&html, output.String()) |
| |
| if err := writePage(ctx, strings.ToLower(kind), html.Bytes()); err != nil { |
| return err |
| } |
| } |
| return writeCache(ctx, "gzdata", &data) |
| } |
| |
| // POST /setToken |
| // |
| // Store a github token in the database, so we can use it for API calls. |
| // Necessary because the only available configuration method is the app.yaml |
| // file, which we want to check in, and can't store secrets. |
| func setTokenHandler(w http.ResponseWriter, r *http.Request) { |
| if r.Method != "POST" { |
| w.Header().Set("Allow", "POST") |
| http.Error(w, "Method not allowed", 405) |
| return |
| } |
| ctx := r.Context() |
| r.ParseForm() |
| if value := r.Form.Get("value"); value != "" { |
| var token Cache |
| token.Value = []byte(value) |
| if err := putCache(ctx, "github-token", &token); err != nil { |
| http.Error(w, err.Error(), 500) |
| } |
| } |
| } |
| |
| // GET /favicon.ico |
| func faviconHandler(w http.ResponseWriter, r *http.Request) { |
| http.ServeFile(w, r, "./static/favicon.ico") |
| } |