2016-05-18 00:19:28 +03:00
|
|
|
// 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"
|
2016-08-30 01:58:31 +03:00
|
|
|
"io"
|
2017-03-14 10:48:32 +03:00
|
|
|
"log"
|
2016-05-18 00:19:28 +03:00
|
|
|
"net/http"
|
|
|
|
"strings"
|
2017-03-14 00:02:35 +03:00
|
|
|
"sync/atomic"
|
2016-05-18 00:19:28 +03:00
|
|
|
"time"
|
|
|
|
|
|
|
|
"golang.org/x/build/gerrit"
|
|
|
|
"golang.org/x/build/godash"
|
|
|
|
"golang.org/x/net/context"
|
|
|
|
)
|
|
|
|
|
|
|
|
const entityPrefix = "DevApp"
|
|
|
|
|
2017-01-08 08:05:57 +03:00
|
|
|
var gerritTransport http.RoundTripper
|
|
|
|
|
2016-05-18 00:19:28 +03:00
|
|
|
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) }))
|
|
|
|
}
|
2016-05-28 01:38:52 +03:00
|
|
|
http.Handle("/dash", hstsHandler(showDash))
|
2016-08-30 01:58:31 +03:00
|
|
|
http.Handle("/update", ctxHandler(update))
|
|
|
|
// Defined in stats.go
|
2016-08-30 03:29:46 +03:00
|
|
|
http.HandleFunc("/stats/raw", rawHandler)
|
|
|
|
http.HandleFunc("/stats/svg", svgHandler)
|
2016-09-28 21:01:32 +03:00
|
|
|
http.Handle("/stats/release", ctxHandler(release))
|
|
|
|
http.Handle("/stats/release/data.js", ctxHandler(releaseData))
|
2016-08-30 01:58:31 +03:00
|
|
|
http.Handle("/update/stats", ctxHandler(updateStats))
|
2016-05-18 00:19:28 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2016-08-30 01:58:31 +03:00
|
|
|
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) {
|
2017-01-08 08:05:57 +03:00
|
|
|
ctx := getContext(r)
|
2016-08-30 01:58:31 +03:00
|
|
|
if err := fn(ctx, w, r); err != nil {
|
2017-03-14 10:48:32 +03:00
|
|
|
logger.Criticalf(ctx, "handler failed: %v", err)
|
2016-08-30 01:58:31 +03:00
|
|
|
http.Error(w, err.Error(), 500)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
func logFn(ctx context.Context, w io.Writer) func(string, ...interface{}) {
|
2017-03-14 10:48:32 +03:00
|
|
|
stdLogger := log.New(w, "", log.Lmicroseconds)
|
2016-08-30 01:58:31 +03:00
|
|
|
return func(format string, args ...interface{}) {
|
2017-03-14 10:48:32 +03:00
|
|
|
stdLogger.Printf(format, args...)
|
|
|
|
logger.Infof(ctx, format, args...)
|
2016-08-30 01:58:31 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-05-18 00:19:28 +03:00
|
|
|
type Page struct {
|
|
|
|
// Content is the complete HTML of the page.
|
2017-03-14 10:48:32 +03:00
|
|
|
Content []byte `datastore:"content,noindex"`
|
2016-05-18 00:19:28 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
func servePage(w http.ResponseWriter, r *http.Request, page string) {
|
2017-03-14 00:27:46 +03:00
|
|
|
ctx := getContext(r)
|
2017-01-08 08:05:57 +03:00
|
|
|
entity, err := getPage(ctx, page)
|
|
|
|
if err != nil {
|
2016-05-18 00:19:28 +03:00
|
|
|
http.Error(w, "page not found", 404)
|
|
|
|
return
|
|
|
|
}
|
2017-01-08 08:05:57 +03:00
|
|
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
2016-05-18 00:19:28 +03:00
|
|
|
w.Write(entity.Content)
|
|
|
|
}
|
|
|
|
|
2017-03-14 00:02:35 +03:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
2016-08-30 01:58:31 +03:00
|
|
|
func update(ctx context.Context, w http.ResponseWriter, _ *http.Request) error {
|
2017-01-08 08:05:57 +03:00
|
|
|
token, err := getToken(ctx)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
gzdata, _ := getCache(ctx, "gzdata")
|
2017-03-14 00:02:35 +03:00
|
|
|
ct := &countTransport{newTransport(ctx), 0}
|
|
|
|
gh := godash.NewGitHubClient("golang/go", token, ct)
|
|
|
|
defer func() {
|
2017-03-14 10:48:32 +03:00
|
|
|
logger.Infof(ctx, "Sent %d requests to GitHub", ct.Count())
|
2017-03-14 00:02:35 +03:00
|
|
|
}()
|
2016-08-30 01:58:31 +03:00
|
|
|
ger := gerrit.NewClient("https://go-review.googlesource.com", gerrit.NoAuth)
|
2016-05-18 00:19:28 +03:00
|
|
|
// Without a deadline, urlfetch will use a 5s timeout which is too slow for Gerrit.
|
2016-08-30 01:58:31 +03:00
|
|
|
gerctx, cancel := context.WithTimeout(ctx, 9*time.Minute)
|
2016-05-18 00:19:28 +03:00
|
|
|
defer cancel()
|
2017-03-14 00:02:35 +03:00
|
|
|
ger.HTTPClient = &http.Client{Transport: newTransport(gerctx)}
|
2016-08-30 01:58:31 +03:00
|
|
|
|
2017-01-08 08:05:57 +03:00
|
|
|
data, err := parseData(gzdata)
|
2016-08-30 01:58:31 +03:00
|
|
|
if err != nil {
|
|
|
|
return err
|
2016-05-18 00:19:28 +03:00
|
|
|
}
|
|
|
|
|
2017-02-21 01:12:04 +03:00
|
|
|
if err := data.Reviewers.LoadGithub(ctx, gh); err != nil {
|
2017-03-14 10:48:32 +03:00
|
|
|
logger.Criticalf(ctx, "failed to load reviewers: %v", err)
|
2016-05-18 00:19:28 +03:00
|
|
|
return err
|
|
|
|
}
|
2016-08-30 01:58:31 +03:00
|
|
|
l := logFn(ctx, w)
|
2017-01-08 08:05:57 +03:00
|
|
|
if err := data.FetchData(gerctx, gh, ger, l, 7, false, false); err != nil {
|
2017-03-14 10:48:32 +03:00
|
|
|
logger.Criticalf(ctx, "failed to fetch data: %v", err)
|
2016-05-18 00:19:28 +03:00
|
|
|
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 {
|
|
|
|
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
|
|
|
|
}
|
|
|
|
}
|
2016-08-30 01:58:31 +03:00
|
|
|
return writeCache(ctx, "gzdata", &data)
|
2016-05-18 00:19:28 +03:00
|
|
|
}
|
2017-03-14 10:48:32 +03:00
|
|
|
|
|
|
|
// 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) {
|
2017-04-29 07:44:23 +03:00
|
|
|
// Need to specify content type for consistent tests, without this it's
|
|
|
|
// determined from mime.types on the box the test is running on
|
|
|
|
w.Header().Set("Content-Type", "image/x-icon")
|
2017-03-14 10:48:32 +03:00
|
|
|
http.ServeFile(w, r, "./static/favicon.ico")
|
|
|
|
}
|