blob: 26b7bd656e5ecc94dde2a6b0ce854573b05e69af [file] [log] [blame]
/*
Handlers and types specific to Chromium builds.
*/
package chromium_builds
import (
"bufio"
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"path/filepath"
"regexp"
"strconv"
"strings"
"text/template"
"time"
"cloud.google.com/go/datastore"
"github.com/gorilla/mux"
"go.skia.org/infra/ct/go/ctfe/task_common"
ctfeutil "go.skia.org/infra/ct/go/ctfe/util"
ctutil "go.skia.org/infra/ct/go/util"
"go.skia.org/infra/go/buildskia"
"go.skia.org/infra/go/common"
"go.skia.org/infra/go/ds"
"go.skia.org/infra/go/gitiles"
"go.skia.org/infra/go/httputils"
"go.skia.org/infra/go/sklog"
skutil "go.skia.org/infra/go/util"
"google.golang.org/api/iterator"
)
const (
// URL returning the GIT commit hash of the last known good release of Chromium.
CHROMIUM_LKGR_URL = "http://chromium-status.appspot.com/git-lkgr"
)
var (
// Base URL of the Chromium GIT repository, to be followed by commit hash.
CHROMIUM_GIT_REPO_URL = fmt.Sprintf(gitiles.CommitURL, common.REPO_CHROMIUM, "")
// URL of a base64-encoded file that includes the GIT commit hash last known good release of Skia.
CHROMIUM_DEPS_FILE = fmt.Sprintf(gitiles.DownloadURL, common.REPO_CHROMIUM, "master", "DEPS")
// Base URL of the Skia GIT repository, to be followed by commit hash.
SKIA_GIT_REPO_URL = fmt.Sprintf(gitiles.CommitURL, common.REPO_SKIA, "")
addTaskTemplate *template.Template = nil
runsHistoryTemplate *template.Template = nil
httpClient = httputils.NewTimeoutClient()
)
func ReloadTemplates(resourcesDir string) {
addTaskTemplate = template.Must(template.ParseFiles(
filepath.Join(resourcesDir, "templates/chromium_builds.html"),
filepath.Join(resourcesDir, "templates/header.html"),
filepath.Join(resourcesDir, "templates/titlebar.html"),
))
runsHistoryTemplate = template.Must(template.ParseFiles(
filepath.Join(resourcesDir, "templates/chromium_build_runs_history.html"),
filepath.Join(resourcesDir, "templates/header.html"),
filepath.Join(resourcesDir, "templates/titlebar.html"),
))
}
type DatastoreTask struct {
task_common.CommonCols
ChromiumRev string
ChromiumRevTs int64
SkiaRev string
}
func (task DatastoreTask) GetTaskName() string {
return "ChromiumBuild"
}
func (task DatastoreTask) GetDescription() string {
// This task does not support descriptions.
return ""
}
func (task DatastoreTask) GetResultsLink() string {
return ""
}
func (task DatastoreTask) GetPopulatedAddTaskVars() (task_common.AddTaskVars, error) {
taskVars := &AddTaskVars{}
taskVars.Username = task.Username
taskVars.TsAdded = ctutil.GetCurrentTs()
taskVars.RepeatAfterDays = strconv.FormatInt(task.RepeatAfterDays, 10)
taskVars.ChromiumRev = task.ChromiumRev
taskVars.ChromiumRevTs = strconv.FormatInt(task.ChromiumRevTs, 10)
taskVars.SkiaRev = task.SkiaRev
return taskVars, nil
}
func (task DatastoreTask) RunsOnGCEWorkers() bool {
// Unused for chromium_builds because it always runs on the GCE builders not
// the workers or bare-metal machines.
return false
}
func (task DatastoreTask) GetDatastoreKind() ds.Kind {
return ds.CHROMIUM_BUILD_TASKS
}
func (task DatastoreTask) Query(it *datastore.Iterator) (interface{}, error) {
tasks := []*DatastoreTask{}
for {
t := &DatastoreTask{}
_, err := it.Next(t)
if err == iterator.Done {
break
} else if err != nil {
return nil, fmt.Errorf("Failed to retrieve list of tasks: %s", err)
}
tasks = append(tasks, t)
}
return tasks, nil
}
func (task DatastoreTask) Get(c context.Context, key *datastore.Key) (task_common.Task, error) {
t := &DatastoreTask{}
if err := ds.DS.Get(c, key, t); err != nil {
return nil, err
}
return t, nil
}
func (task DatastoreTask) TriggerSwarmingTaskAndMail(ctx context.Context) error {
runID := task_common.GetRunID(&task)
emails := task_common.GetEmailRecipients(task.Username, nil)
isolateArgs := map[string]string{
"TARGET_PLATFORM": ctutil.PLATFORM_LINUX,
"CHROMIUM_HASH": task.ChromiumRev,
"SKIA_HASH": task.SkiaRev,
}
sTaskID, err := ctutil.TriggerMasterScriptSwarmingTask(ctx, runID, "build_chromium", ctutil.BUILD_CHROMIUM_MASTER_ISOLATE, task_common.ServiceAccountFile, ctutil.PLATFORM_LINUX, false, isolateArgs)
if err != nil {
return fmt.Errorf("Could not trigger master script for build_chromium with isolate args %v: %s", isolateArgs, err)
}
// Mark task as started in datastore.
if err := task_common.UpdateTaskSetStarted(ctx, runID, sTaskID, &task); err != nil {
return fmt.Errorf("Could not mark task as started in datastore: %s", err)
}
// Send start email.
skutil.LogErr(ctfeutil.SendTaskStartEmail(task.DatastoreKey.ID, emails, "Build chromium", runID, "", ""))
return nil
}
func (task DatastoreTask) SendCompletionEmail(ctx context.Context, completedSuccessfully bool) error {
runID := task_common.GetRunID(&task)
emails := task_common.GetEmailRecipients(task.Username, nil)
emailSubject := fmt.Sprintf("Chromium build task has completed (#%d)", task.DatastoreKey.ID)
failureHtml := ""
if !completedSuccessfully {
emailSubject += " with failures"
failureHtml = ctfeutil.GetFailureEmailHtml(runID)
}
bodyTemplate := `
The Cluster telemetry queued task to create a new chromium build has completed. %s.<br/>
%s
You can schedule more runs <a href="%s">here</a>.<br/><br/>
Thanks!
`
emailBody := fmt.Sprintf(bodyTemplate, ctfeutil.GetSwarmingLogsLink(runID), failureHtml, task_common.WebappURL+ctfeutil.CHROMIUM_BUILD_URI)
if err := ctfeutil.SendEmail(emails, emailSubject, emailBody); err != nil {
return fmt.Errorf("Error while sending email: %s", err)
}
return nil
}
func (task *DatastoreTask) SetCompleted(success bool) {
task.TsCompleted = ctutil.GetCurrentTsInt64()
task.Failure = !success
task.TaskDone = true
}
func addTaskView(w http.ResponseWriter, r *http.Request) {
ctfeutil.ExecuteSimpleTemplate(addTaskTemplate, w, r)
}
type AddTaskVars struct {
task_common.AddTaskCommonVars
ChromiumRev string `json:"chromium_rev"`
ChromiumRevTs string `json:"chromium_rev_ts"`
SkiaRev string `json:"skia_rev"`
}
func (task *AddTaskVars) GetDatastoreKind() ds.Kind {
return ds.CHROMIUM_BUILD_TASKS
}
func (task *AddTaskVars) GetPopulatedDatastoreTask(ctx context.Context) (task_common.Task, error) {
if task.ChromiumRev == "" ||
task.SkiaRev == "" ||
task.ChromiumRevTs == "" {
return nil, fmt.Errorf("Invalid parameters")
}
// Example timestamp format: "Wed Jul 15 13:42:19 2015"
var chromiumRevTs int64
if parsedTs, err := time.Parse(time.ANSIC, task.ChromiumRevTs); err != nil {
// ChromiumRevTs is likely already in the expected format.
chromiumRevTs, err = strconv.ParseInt(task.ChromiumRevTs, 10, 64)
if err != nil {
return nil, fmt.Errorf("%s is not int64: %s", task.ChromiumRevTs, err)
}
} else {
ts := parsedTs.UTC().Format("20060102150405")
chromiumRevTs, err = strconv.ParseInt(ts, 10, 64)
if err != nil {
return nil, fmt.Errorf("%s is not int64: %s", ts, err)
}
}
t := &DatastoreTask{
ChromiumRev: task.ChromiumRev,
SkiaRev: task.SkiaRev,
ChromiumRevTs: chromiumRevTs,
}
return t, nil
}
func addTaskHandler(w http.ResponseWriter, r *http.Request) {
task_common.AddTaskHandler(w, r, &AddTaskVars{})
}
func getRevDataHandler(getLkgr func() (string, error), gitRepoUrl string, w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
revString := r.FormValue("rev")
if revString == "" {
httputils.ReportError(w, nil, "No revision specified", http.StatusInternalServerError)
return
}
if strings.EqualFold(revString, "LKGR") {
lkgr, err := getLkgr()
if err != nil {
httputils.ReportError(w, nil, "Unable to retrieve LKGR revision", http.StatusInternalServerError)
}
sklog.Infof("Retrieved LKGR commit hash: %s", lkgr)
revString = lkgr
}
commitUrl := gitRepoUrl + revString + "?format=JSON"
sklog.Infof("Reading revision detail from %s", commitUrl)
resp, err := httpClient.Get(commitUrl)
if err != nil {
httputils.ReportError(w, err, "Unable to retrieve revision detail", http.StatusInternalServerError)
return
}
defer skutil.Close(resp.Body)
if resp.StatusCode == 404 {
// Return successful empty response, since the user could be typing.
if err := json.NewEncoder(w).Encode(map[string]interface{}{}); err != nil {
httputils.ReportError(w, err, fmt.Sprintf("Failed to encode JSON: %v", err), http.StatusInternalServerError)
return
}
return
}
if resp.StatusCode != 200 {
httputils.ReportError(w, nil, "Unable to retrieve revision detail", http.StatusInternalServerError)
return
}
// Remove junk in the first line. https://code.google.com/p/gitiles/issues/detail?id=31
bufBody := bufio.NewReader(resp.Body)
if _, err = bufBody.ReadString('\n'); err != nil {
httputils.ReportError(w, err, "Unable to retrieve revision detail", http.StatusInternalServerError)
return
}
if _, err = io.Copy(w, bufBody); err != nil {
httputils.ReportError(w, err, "Unable to retrieve revision detail", http.StatusInternalServerError)
return
}
}
// TODO(benjaminwagner): Seems to duplicate code in ct/go/util/chromium_builds.go.
func getChromiumLkgr() (string, error) {
sklog.Infof("Reading Chromium LKGR from %s", CHROMIUM_LKGR_URL)
resp, err := httpClient.Get(CHROMIUM_LKGR_URL)
if err != nil {
return "", err
}
defer skutil.Close(resp.Body)
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return "", err
}
return bytes.NewBuffer(body).String(), nil
}
func getChromiumRevDataHandler(w http.ResponseWriter, r *http.Request) {
getRevDataHandler(getChromiumLkgr, CHROMIUM_GIT_REPO_URL, w, r)
}
var skiaRevisionRegexp = regexp.MustCompile("'skia_revision': '([0-9a-fA-F]{2,40})'")
func getSkiaLkgr() (string, error) {
return buildskia.GetSkiaHash(httpClient)
}
func getSkiaRevDataHandler(w http.ResponseWriter, r *http.Request) {
getRevDataHandler(getSkiaLkgr, SKIA_GIT_REPO_URL, w, r)
}
func deleteTaskHandler(w http.ResponseWriter, r *http.Request) {
task_common.DeleteTaskHandler(&DatastoreTask{}, w, r)
}
func redoTaskHandler(w http.ResponseWriter, r *http.Request) {
task_common.RedoTaskHandler(&DatastoreTask{}, w, r)
}
func runsHistoryView(w http.ResponseWriter, r *http.Request) {
ctfeutil.ExecuteSimpleTemplate(runsHistoryTemplate, w, r)
}
func getTasksHandler(w http.ResponseWriter, r *http.Request) {
task_common.GetTasksHandler(&DatastoreTask{}, w, r)
}
// Validate that the given chromiumBuild exists in the Datastore.
func Validate(ctx context.Context, chromiumBuild DatastoreTask) error {
q := ds.NewQuery(chromiumBuild.GetDatastoreKind())
q = q.Filter("ChromiumRev =", chromiumBuild.ChromiumRev)
q = q.Filter("SkiaRev =", chromiumBuild.SkiaRev)
q = q.Filter("TaskDone =", true)
q = q.Filter("Failure =", false)
count, err := ds.DS.Count(ctx, q)
if err != nil {
sklog.Info(err)
return fmt.Errorf("Error when validating chromium build %v: %s", chromiumBuild, err)
}
if count == 0 {
return fmt.Errorf("Unable to validate chromium_build parameter %v", chromiumBuild)
}
return nil
}
func AddHandlers(externalRouter *mux.Router) {
externalRouter.HandleFunc("/"+ctfeutil.CHROMIUM_BUILD_URI, addTaskView).Methods("GET")
externalRouter.HandleFunc("/"+ctfeutil.CHROMIUM_BUILD_RUNS_URI, runsHistoryView).Methods("GET")
externalRouter.HandleFunc("/"+ctfeutil.CHROMIUM_REV_DATA_POST_URI, getChromiumRevDataHandler).Methods("POST")
externalRouter.HandleFunc("/"+ctfeutil.SKIA_REV_DATA_POST_URI, getSkiaRevDataHandler).Methods("POST")
externalRouter.HandleFunc("/"+ctfeutil.ADD_CHROMIUM_BUILD_TASK_POST_URI, addTaskHandler).Methods("POST")
externalRouter.HandleFunc("/"+ctfeutil.GET_CHROMIUM_BUILD_TASKS_POST_URI, getTasksHandler).Methods("POST")
externalRouter.HandleFunc("/"+ctfeutil.DELETE_CHROMIUM_BUILD_TASK_POST_URI, deleteTaskHandler).Methods("POST")
externalRouter.HandleFunc("/"+ctfeutil.REDO_CHROMIUM_BUILD_TASK_POST_URI, redoTaskHandler).Methods("POST")
}