blob: 198d01387d764723f24dd31d846f061df8fa4017 [file] [log] [blame]
/*
Android Compile Server Frontend.
*/
package main
import (
"context"
"encoding/json"
"flag"
"fmt"
"html/template"
"net/http"
"path/filepath"
"time"
"cloud.google.com/go/datastore"
"github.com/gorilla/mux"
"github.com/unrolled/secure"
"go.skia.org/infra/android_compile/go/util"
"go.skia.org/infra/go/allowed"
"go.skia.org/infra/go/auth"
"go.skia.org/infra/go/baseapp"
"go.skia.org/infra/go/cleanup"
"go.skia.org/infra/go/httputils"
"go.skia.org/infra/go/login"
"go.skia.org/infra/go/sklog"
)
const (
forceSyncPostUrl = "/_/force_sync"
pendingTasksPostUrl = "/_/pending_tasks"
compileInstancesPostUrl = "/_/compile_instances"
)
var (
// Flags
host = flag.String("host", "skia-android-compile.corp.goog", "HTTP service host")
// Datastore params
namespace = flag.String("namespace", "android-compile-staging", "The Cloud Datastore namespace, such as 'android-compile'.")
projectName = flag.String("project_name", "google.com:skia-corp", "The Google Cloud project name.")
serverURL string
)
// Server is the state of the server.
type Server struct {
templates *template.Template
}
func (srv *Server) loadTemplates() {
srv.templates = template.Must(template.New("").Delims("{%", "%}").ParseFiles(
filepath.Join(*baseapp.ResourcesDir, "index.html"),
))
}
func (srv *Server) indexHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")
if err := srv.templates.ExecuteTemplate(w, "index.html", map[string]string{
// Look in webpack.config.js for where the nonce templates are injected.
"Nonce": secure.CSPNonce(r.Context()),
}); err != nil {
httputils.ReportError(w, err, "Failed to expand template", http.StatusInternalServerError)
return
}
}
func (srv *Server) compileInstancesHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
compileInstances, err := util.GetAllCompileInstances(context.Background())
if err != nil {
httputils.ReportError(w, err, "Failed to get android compile instances", http.StatusInternalServerError)
return
}
if err := json.NewEncoder(w).Encode(compileInstances); err != nil {
httputils.ReportError(w, err, fmt.Sprintf("Failed to encode JSON: %v", err), http.StatusInternalServerError)
return
}
}
func (srv *Server) pendingTasksHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
unownedPendingTasks, ownedPendingTasks, err := util.GetPendingCompileTasks("" /* ownedByInstance */)
if err != nil {
httputils.ReportError(w, err, "Failed to get unowned/owned compile tasks", http.StatusInternalServerError)
return
}
var pendingTasks = struct {
UnOwnedPendingTasks []*util.CompileTask `json:"unowned_pending_tasks"`
OwnedPendingTasks []*util.CompileTask `json:"owned_pending_tasks"`
}{
UnOwnedPendingTasks: unownedPendingTasks,
OwnedPendingTasks: ownedPendingTasks,
}
if err := json.NewEncoder(w).Encode(pendingTasks); err != nil {
httputils.ReportError(w, err, fmt.Sprintf("Failed to encode JSON: %v", err), http.StatusInternalServerError)
return
}
}
func (srv *Server) forceSyncHandler(w http.ResponseWriter, r *http.Request) {
if err := util.SetForceMirrorUpdateOnAllInstances(context.Background()); err != nil {
httputils.ReportError(w, err, "Failed to set force mirror update on all instances", http.StatusInternalServerError)
return
}
sklog.Infof("Force sync button has been pressed by %s", login.LoggedInAs(r))
compileInstances, err := util.GetAllCompileInstances(context.Background())
if err != nil {
httputils.ReportError(w, err, "Failed to get android compile instances", http.StatusInternalServerError)
return
}
if err := json.NewEncoder(w).Encode(compileInstances); err != nil {
httputils.ReportError(w, err, fmt.Sprintf("Failed to encode JSON: %v", err), http.StatusInternalServerError)
return
}
return
}
// See baseapp.App.
func (srv *Server) AddHandlers(r *mux.Router) {
// For login/logout.
r.HandleFunc(login.DEFAULT_OAUTH2_CALLBACK, login.OAuth2CallbackHandler)
r.HandleFunc("/logout/", login.LogoutHandler)
r.HandleFunc("/loginstatus/", login.StatusHandler)
// The main page.
r.HandleFunc("/", srv.indexHandler).Methods("GET")
// POST handlers.
r.HandleFunc(forceSyncPostUrl, srv.forceSyncHandler).Methods("POST")
r.HandleFunc(pendingTasksPostUrl, srv.pendingTasksHandler).Methods("POST")
r.HandleFunc(compileInstancesPostUrl, srv.compileInstancesHandler).Methods("POST")
// Healthz.
r.HandleFunc("/healthz", httputils.ReadyHandleFunc)
}
// See baseapp.Constructor
func New() (baseapp.App, error) {
serverURL := "https://" + *host
if *baseapp.Local {
serverURL = "http://" + *host + *baseapp.Port
}
login.InitWithAllow(serverURL+login.DEFAULT_OAUTH2_CALLBACK, allowed.Googlers(), allowed.Googlers(), nil)
// Create token source.
ts, err := auth.NewDefaultTokenSource(*baseapp.Local, auth.SCOPE_READ_WRITE, auth.SCOPE_USERINFO_EMAIL, auth.SCOPE_GERRIT, datastore.ScopeDatastore)
if err != nil {
sklog.Fatalf("Problem setting up default token source: %s", err)
}
// Initialize cloud datastore.
if err := util.DatastoreInit(*projectName, *namespace, ts); err != nil {
sklog.Fatalf("Failed to init cloud datastore: %s", err)
}
// Start updater for the queue length metrics.
cleanup.Repeat(time.Minute, func(ctx context.Context) {
unownedPendingTasks, ownedPendingTasks, err := util.GetPendingCompileTasks("" /* ownedByInstance */)
if err != nil {
sklog.Errorf("Failed to get unowned/owned compile tasks: %s", err)
} else {
util.QueueLengthMetric.Update(int64(len(unownedPendingTasks)))
util.RunningLengthMetric.Update(int64(len(ownedPendingTasks)))
}
}, nil)
srv := &Server{}
srv.loadTemplates()
return srv, nil
}
// See baseapp.App.
func (srv *Server) AddMiddleware() []mux.MiddlewareFunc {
ret := []mux.MiddlewareFunc{}
if !*baseapp.Local {
ret = append(ret, login.ForceAuthMiddleware(login.DEFAULT_OAUTH2_CALLBACK), login.RestrictViewer)
}
return ret
}
func main() {
baseapp.Serve(New, []string{*host})
}