blob: 19e30e4df069d1a8cd46ae9d427fb2e58669efc7 [file]
package main
import (
"encoding/json"
"flag"
"fmt"
"io/ioutil"
"net/http"
"os"
"path/filepath"
"runtime/pprof"
"time"
"github.com/gorilla/mux"
"github.com/skia-dev/glog"
"go.skia.org/infra/go/auth"
"go.skia.org/infra/go/common"
"go.skia.org/infra/go/database"
"go.skia.org/infra/go/eventbus"
"go.skia.org/infra/go/login"
"go.skia.org/infra/go/metadata"
"go.skia.org/infra/go/redisutil"
"go.skia.org/infra/go/skiaversion"
"go.skia.org/infra/go/timer"
"go.skia.org/infra/go/util"
"go.skia.org/infra/golden/go/analysis"
"go.skia.org/infra/golden/go/blame"
"go.skia.org/infra/golden/go/db"
"go.skia.org/infra/golden/go/digeststore"
"go.skia.org/infra/golden/go/expstorage"
"go.skia.org/infra/golden/go/filediffstore"
"go.skia.org/infra/golden/go/history"
"go.skia.org/infra/golden/go/ignore"
"go.skia.org/infra/golden/go/status"
"go.skia.org/infra/golden/go/storage"
"go.skia.org/infra/golden/go/summary"
"go.skia.org/infra/golden/go/tally"
"go.skia.org/infra/golden/go/types"
pconfig "go.skia.org/infra/perf/go/config"
"go.skia.org/infra/perf/go/filetilestore"
)
// Command line flags.
var (
graphiteServer = flag.String("graphite_server", "skia-monitoring:2003", "Where is Graphite metrics ingestion server running.")
port = flag.String("port", ":9000", "HTTP service address (e.g., ':9000')")
local = flag.Bool("local", false, "Running locally if true. As opposed to in production.")
staticDir = flag.String("static_dir", "./app", "Directory with static content to serve")
tileStoreDir = flag.String("tile_store_dir", "/tmp/tileStore", "What directory to look for tiles in.")
imageDir = flag.String("image_dir", "/tmp/imagedir", "What directory to store test and diff images in.")
gsBucketName = flag.String("gs_bucket", "chromium-skia-gm", "Name of the google storage bucket that holds uploaded images.")
doOauth = flag.Bool("oauth", true, "Run through the OAuth 2.0 flow on startup, otherwise use a GCE service account.")
oauthCacheFile = flag.String("oauth_cache_file", "/home/perf/google_storage_token.data", "Path to the file where to cache cache the oauth credentials.")
memProfile = flag.Duration("memprofile", 0, "Duration for which to profile memory. After this duration the program writes the memory profile and exits.")
nCommits = flag.Int("n_commits", 50, "Number of recent commits to include in the analysis.")
resourcesDir = flag.String("resources_dir", "", "The directory to find templates, JS, and CSS files. If blank the directory relative to the source code files will be used.")
redirectURL = flag.String("redirect_url", "https://gold.skia.org/oauth2callback/", "OAuth2 redirect url. Only used when local=false.")
redisHost = flag.String("redis_host", "", "The host and port (e.g. 'localhost:6379') of the Redis data store that will be used for caching.")
redisDB = flag.Int("redis_db", 0, "The index of the Redis database we should use. Default will work fine in most cases.")
startAnalyzer = flag.Bool("start_analyzer", true, "Create an instance of the analyzer and start it running.")
startExperimental = flag.Bool("start_experimental", true, "Start experimental features.")
cpuProfile = flag.Duration("cpu_profile", 0, "Duration for which to profile the CPU usage. After this duration the program writes the CPU profile and exits.")
forceLogin = flag.Bool("force_login", false, "Force the user to be authenticated for all requests.")
authWhiteList = flag.String("auth_whitelist", login.DEFAULT_DOMAIN_WHITELIST, "White space separated list of domains and email addresses that are allowed to login.")
nTilesToBackfill = flag.Int("backfill_tiles", 0, "Number of tiles to backfill in our history of tiles.")
storageDir = flag.String("storage_dir", "/tmp/gold-storage", "Directory to store reproducible application data.")
)
const (
IMAGE_URL_PREFIX = "/img/"
// OAUTH2_CALLBACK_PATH is callback endpoint used for the Oauth2 flow.
OAUTH2_CALLBACK_PATH = "/oauth2callback/"
)
// TODO(stephana): Once the analyzer related code is removed, simplify
// the ResponseEnvelope and use it solely to wrap JSON arrays.
// Remove sendResponse and sendErrorResponse in favor of sendJsonResponse
// and util.ReportError.
// ResponseEnvelope wraps all responses. Some fields might be empty depending
// on context or whether there was an error or not.
type ResponseEnvelope struct {
Data *interface{} `json:"data"`
Err *string `json:"err"`
Status int `json:"status"`
Pagination *util.ResponsePagination `json:"pagination"`
}
// CommonEnv captures shared that affect the frontend as well as the backend.
// It is used in setting up endpoints as well as rendering HTML.
type CommonEnv struct {
// BaseURL is the base path of the application.
BaseURL string
}
var (
analyzer *analysis.Analyzer = nil
// Module level variables that need to be accessible to main2.go.
storages *storage.Storage
pathToURLConverter analysis.PathToURLConverter
tallies *tally.Tallies
summaries *summary.Summaries
statusWatcher *status.StatusWatcher
commonEnv CommonEnv
blamer *blame.Blamer
)
func listTestDetailsHandler(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query()
result, err := analyzer.ListTestDetails(query)
if err != nil {
sendErrorResponse(w, err.Error(), http.StatusInternalServerError)
return
}
sendResponse(w, result, http.StatusOK, nil)
}
// testDetailsHandler returns sufficient information about the given
// testName to triage digests.
func testDetailsHandler(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query()
testName := mux.Vars(r)["testname"]
result, err := analyzer.GetTestDetails(testName, query)
if err != nil {
sendErrorResponse(w, err.Error(), http.StatusInternalServerError)
return
}
sendResponse(w, result, http.StatusOK, nil)
}
// triageDigestsHandler handles triaging digests. It requires the user
// to be logged in and upon success returns the the test details in the
// same format as testDetailsHandler. That way it can be used by the
// frontend to incrementally triage digests for a specific test
// (or set of tests.)
// TODO (stephana): This is not finished and WIP.
func triageDigestsHandler(w http.ResponseWriter, r *http.Request) {
// Make sure the user is authenticated.
userId := login.LoggedInAs(r)
if userId == "" {
sendErrorResponse(w, "You must be logged in triage digests.", http.StatusForbidden)
return
}
// Parse input data in the body.
var tc map[string]types.TestClassification
if err := parseJson(r, &tc); err != nil {
sendErrorResponse(w, "Unable to parse JSON. Error: "+err.Error(), http.StatusBadRequest)
return
}
// Update the labeling of the given tests and digests.
result, err := analyzer.SetDigestLabels(tc, userId)
if err != nil {
sendErrorResponse(w, err.Error(), http.StatusBadRequest)
return
}
sendResponse(w, result, http.StatusOK, nil)
}
// blameHandler returns the blame list for the given test.
func blameHandler(w http.ResponseWriter, r *http.Request) {
testName := mux.Vars(r)["testname"]
result := analyzer.GetBlameList(testName)
sendResponse(w, result, http.StatusOK, nil)
}
// statusHandler returns the current status with respect to HEAD.
func statusHandler(w http.ResponseWriter, r *http.Request) {
result := analyzer.GetStatus()
sendResponse(w, result, http.StatusOK, nil)
}
// sendErrorResponse wraps an error in a response envelope and sends it to
// the client.
func sendErrorResponse(w http.ResponseWriter, errorMsg string, status int) {
resp := ResponseEnvelope{nil, &errorMsg, status, nil}
sendJson(w, &resp, status)
}
// sendResponse wraps the data of a succesful response in a response envelope
// and sends it to the client.
func sendResponse(w http.ResponseWriter, data interface{}, status int, pagination *util.ResponsePagination) {
resp := ResponseEnvelope{&data, nil, status, pagination}
sendJson(w, &resp, status)
}
// sendJson serializes the response envelope and sends ito the client.
func sendJson(w http.ResponseWriter, resp *ResponseEnvelope, status int) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
if err := json.NewEncoder(w).Encode(resp); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
// parseJson extracts the body from the request and parses it into the
// provided interface.
func parseJson(r *http.Request, v interface{}) error {
// TODO (stephana): validate the JSON against a schema. Might not be necessary !
defer util.Close(r.Body)
decoder := json.NewDecoder(r.Body)
return decoder.Decode(v)
}
// URLAwareFileServer wraps around a standard file server and allows to generate
// URLs for a given path that is contained in the root.
type URLAwareFileServer struct {
// baseDir is the root directory for all content served. All paths have to
// be contained somewhere in the directory tree below this.
baseDir string
// baseUrl is the URL prefix that maps to baseDir.
baseUrl string
// Handler is a standard FileServer handler.
Handler http.Handler
}
func NewURLAwareFileServer(baseDir, baseUrl string) *URLAwareFileServer {
absPath, err := filepath.Abs(baseDir)
if err != nil {
glog.Fatalf("Unable to get abs path of %s. Got error: %s", baseDir, err)
}
fileHandler := http.StripPrefix(baseUrl, http.FileServer(http.Dir(absPath)))
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Cache images for 12 hours.
w.Header().Set("Cache-control", "public, max-age=43200")
fileHandler.ServeHTTP(w, r)
})
return &URLAwareFileServer{
baseDir: absPath,
baseUrl: baseUrl,
Handler: handler,
}
}
// converToUrl returns the path component of a URL given the path
// contained within baseDir.
func (ug *URLAwareFileServer) GetURL(path string) string {
absPath, err := filepath.Abs(path)
if err != nil {
glog.Errorf("Unable to get absolute path of %s. Got error: %s", path, err)
return ""
}
relPath, err := filepath.Rel(ug.baseDir, absPath)
if err != nil {
glog.Errorf("Unable to find subpath got error %s", err)
return ""
}
ret := ug.baseUrl + relPath
return ret
}
// getOAuthClient returns an oauth client (either from cached credentials or
// via an authentication flow) or nil depending on whether doOauth is false.
func getOAuthClient(doOauth bool, cacheFilePath string) *http.Client {
if doOauth {
config := auth.DefaultOAuthConfig(cacheFilePath)
client, err := auth.RunFlow(config)
if err != nil {
glog.Fatalf("Failed to auth: %s", err)
}
return client
}
return nil
}
func main() {
var err error
t := timer.New("main init")
// Setup DB flags.
dbConf := database.ConfigFromFlags(db.PROD_DB_HOST, db.PROD_DB_PORT, database.USER_RW, db.PROD_DB_NAME, db.MigrationSteps())
// Global init to initialize
common.InitWithMetrics("skiacorrectness", graphiteServer)
v, err := skiaversion.GetVersion()
if err != nil {
glog.Fatalf("Unable to retrieve version: %s", err)
}
glog.Infof("Version %s, built at %s", v.Commit, v.Date)
// Enable the memory profiler if memProfile was set.
// TODO(stephana): This should be moved to a HTTP endpoint that
// only responds to internal IP addresses/ports.
if *memProfile > 0 {
time.AfterFunc(*memProfile, func() {
glog.Infof("Writing Memory Profile")
f, err := ioutil.TempFile("./", "memory-profile")
if err != nil {
glog.Fatalf("Unable to create memory profile file: %s", err)
}
if err := pprof.WriteHeapProfile(f); err != nil {
glog.Fatalf("Unable to write memory profile file: %v", err)
}
util.Close(f)
glog.Infof("Memory profile written to %s", f.Name())
os.Exit(0)
})
}
if *cpuProfile > 0 {
glog.Infof("Writing CPU Profile")
f, err := ioutil.TempFile("./", "cpu-profile")
if err != nil {
glog.Fatalf("Unable to create cpu profile file: %s", err)
}
if err := pprof.StartCPUProfile(f); err != nil {
glog.Fatalf("Unable to write cpu profile file: %v", err)
}
time.AfterFunc(*cpuProfile, func() {
pprof.StopCPUProfile()
util.Close(f)
glog.Infof("CPU profile written to %s", f.Name())
os.Exit(0)
})
}
// Init this module.
Init()
// Initialize submodules.
filediffstore.Init()
// Set up login
// TODO (stephana): Factor out to go/login/login.go and removed hard coded
// values.
var cookieSalt = "notverysecret"
var clientID = "31977622648-ubjke2f3staq6ouas64r31h8f8tcbiqp.apps.googleusercontent.com"
var clientSecret = "rK-kRY71CXmcg0v9I9KIgWci"
var useRedirectURL = fmt.Sprintf("http://localhost%s/oauth2callback/", *port)
if !*local {
cookieSalt = metadata.Must(metadata.ProjectGet(metadata.COOKIESALT))
clientID = metadata.Must(metadata.ProjectGet(metadata.CLIENT_ID))
clientSecret = metadata.Must(metadata.ProjectGet(metadata.CLIENT_SECRET))
useRedirectURL = *redirectURL
}
login.Init(clientID, clientSecret, useRedirectURL, cookieSalt, login.DEFAULT_SCOPE, *authWhiteList, *local)
// get the Oauthclient if necessary.
client := getOAuthClient(*doOauth, *oauthCacheFile)
// Set up the cache implementation to use.
cacheFactory := filediffstore.MemCacheFactory
if *redisHost != "" {
cacheFactory = func(uniqueId string, codec util.LRUCodec) util.LRUCache {
return redisutil.NewRedisLRUCache(*redisHost, *redisDB, uniqueId, codec)
}
}
// Get the expecations storage, the filediff storage and the tilestore.
diffStore, err := filediffstore.NewFileDiffStore(client, *imageDir, *gsBucketName, filediffstore.DEFAULT_GS_IMG_DIR_NAME, cacheFactory, filediffstore.RECOMMENDED_WORKER_POOL_SIZE)
if err != nil {
glog.Fatalf("Allocating DiffStore failed: %s", err)
}
if !*local {
if err := dbConf.GetPasswordFromMetadata(); err != nil {
glog.Fatal(err)
}
}
vdb, err := dbConf.NewVersionedDB()
if err != nil {
glog.Fatal(err)
}
digestStore, err := digeststore.New(*storageDir)
if err != nil {
glog.Fatal(err)
}
eventBus := eventbus.New()
storages = &storage.Storage{
DiffStore: diffStore,
ExpectationsStore: expstorage.NewCachingExpectationStore(expstorage.NewSQLExpectationStore(vdb), eventBus),
IgnoreStore: ignore.NewSQLIgnoreStore(vdb),
TileStore: filetilestore.NewFileTileStore(*tileStoreDir, pconfig.DATASET_GOLD, 2*time.Minute),
DigestStore: digestStore,
NCommits: *nCommits,
EventBus: eventBus,
}
if err := history.Init(storages, *nTilesToBackfill); err != nil {
glog.Fatalf("Unable to initialize history package: %s", err)
}
if blamer, err = blame.New(storages); err != nil {
glog.Fatalf("Unable to create blamer: %s", err)
}
if err := ignore.Init(storages.IgnoreStore); err != nil {
glog.Fatalf("Failed to start monitoring for expired ignore rules: %s", err)
}
// Enable the experimental features.
if *startExperimental {
tallies, err = tally.New(storages)
if err != nil {
glog.Fatalf("Failed to build tallies: %s", err)
}
summaries, err = summary.New(storages, tallies, blamer)
if err != nil {
glog.Fatalf("Failed to build summary: %s", err)
}
statusWatcher, err = status.New(storages)
if err != nil {
glog.Fatalf("Failed to initialize status watcher: %s", err)
}
}
// Initialize the Analyzer
imgFS := NewURLAwareFileServer(*imageDir, IMAGE_URL_PREFIX)
pathToURLConverter = imgFS.GetURL
if *startAnalyzer {
analyzer = analysis.NewAnalyzer(storages, imgFS.GetURL, cacheFactory, 5*time.Minute)
}
t.Stop()
router := mux.NewRouter()
// Wire up the resources. We use the 'rest' prefix to avoid any name
// clashes witht the static files being served.
router.HandleFunc("/rest/triage", listTestDetailsHandler).Methods("GET")
router.HandleFunc("/rest/triage/{testname}", testDetailsHandler).Methods("GET")
router.HandleFunc("/rest/triage", triageDigestsHandler).Methods("POST")
router.HandleFunc("/rest/status", util.CorsHandler(statusHandler)).Methods("GET")
router.HandleFunc("/rest/blame/{testname}", blameHandler).Methods("GET")
// Set up the login related resources.
// TODO (stephana): Clean up the URLs so they have the same prefix.
router.HandleFunc(OAUTH2_CALLBACK_PATH, login.OAuth2CallbackHandler)
router.HandleFunc("/rest/logout", login.LogoutHandler)
router.HandleFunc("/rest/loginstatus", login.StatusHandler)
// Set up the resource to serve the image files.
router.PathPrefix(IMAGE_URL_PREFIX).Handler(imgFS.Handler)
// New Polymer based UI endpoints.
router.PathPrefix("/res/").HandlerFunc(makeResourceHandler())
// All the handlers will be prefixed with poly to differentiate it from the
// angular code until the angular code is removed.
router.HandleFunc("/loginstatus/", login.StatusHandler)
router.HandleFunc("/logout/", login.LogoutHandler)
commonEnv.BaseURL = "/"
polyRouter := router.PathPrefix(commonEnv.BaseURL).Subrouter()
polyRouter.HandleFunc("/", polyMainHandler).Methods("GET")
polyRouter.HandleFunc("/ignores", polyIgnoresHandler).Methods("GET")
polyRouter.HandleFunc("/cmp/{test}", polyCompareHandler).Methods("GET")
polyRouter.HandleFunc("/detail", polySingleDigestHandler).Methods("GET")
polyRouter.HandleFunc("/diff", polyDiffDigestHandler).Methods("GET")
polyRouter.HandleFunc("/_/diff", polyDiffJSONDigestHandler).Methods("GET")
polyRouter.HandleFunc("/_/list", polyListTestsHandler).Methods("GET")
polyRouter.HandleFunc("/_/paramset", polyParamsHandler).Methods("GET")
polyRouter.HandleFunc("/_/ignores", polyIgnoresJSONHandler).Methods("GET")
polyRouter.HandleFunc("/_/ignores/del/{id}", polyIgnoresDeleteHandler).Methods("POST")
polyRouter.HandleFunc("/_/ignores/add/", polyIgnoresAddHandler).Methods("POST")
polyRouter.HandleFunc("/_/ignores/save/{id}", polyIgnoresUpdateHandler).Methods("POST")
polyRouter.HandleFunc("/_/test", polyTestHandler).Methods("POST")
polyRouter.HandleFunc("/_/details", polyDetailsHandler).Methods("GET")
polyRouter.HandleFunc("/_/triage", polyTriageHandler).Methods("POST")
polyRouter.HandleFunc("/_/status/{test}", polyTestStatusHandler).Methods("GET")
polyRouter.HandleFunc("/triagelog", polyTriageLogView).Methods("GET")
polyRouter.HandleFunc("/_/triagelog", polyTriageLogHandler).Methods("GET")
polyRouter.HandleFunc("/_/hashes", polyAllHashesHandler).Methods("GET")
polyRouter.HandleFunc("/_/status", polyStatusHandler).Methods("GET")
// Serve the legacy app from the static directory.
router.PathPrefix("/legacy/").Handler(http.StripPrefix("/legacy", http.FileServer(http.Dir(*staticDir))))
// Add the necessary middleware and have the router handle all requests.
// By structuring the middleware this way we only log requests that are
// authenticated.
rootHandler := util.LoggingGzipRequestResponse(router)
if *forceLogin {
rootHandler = login.ForceAuth(rootHandler, OAUTH2_CALLBACK_PATH)
}
http.Handle("/", rootHandler)
// Start the server
glog.Infoln("Serving on http://127.0.0.1" + *port)
glog.Fatal(http.ListenAndServe(*port, nil))
}