blob: 6aaa862f35d6316589b0fff737f6747d4672bbab [file] [log] [blame]
/*
Funnels data from Google-internal sources into the buildbot and task scheduler databases.
Android builds continuously in tradefed at the master branch with the latest roll of Skia. There
is also another branch 'git_master-skia' which contains the HEAD of Skia instead of the last roll
of Skia. Using the Android Build APIs, this application continuously ingests builds from
git_master-skia and pushes them into the buildbot database so they appear on status.skia.org.
Since the target names may contain sensitive information they are obfuscated when pushed to the
buildbot database.
Build info can also be POSTed to this application to directly ingest builds from Google3.
This application also contains a redirector that will take links to the obfuscated target name and
build and return a link to the internal page with the detailed build info.
*/
package main
import (
"encoding/json"
"flag"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"path"
"reflect"
"strconv"
"strings"
"time"
"github.com/gorilla/mux"
"github.com/syndtr/goleveldb/leveldb"
"github.com/syndtr/goleveldb/leveldb/errors"
"go.skia.org/infra/go/androidbuild"
androidbuildinternal "go.skia.org/infra/go/androidbuildinternal/v2beta1"
"go.skia.org/infra/go/auth"
"go.skia.org/infra/go/buildbot"
"go.skia.org/infra/go/common"
"go.skia.org/infra/go/git/repograph"
"go.skia.org/infra/go/httputils"
"go.skia.org/infra/go/login"
"go.skia.org/infra/go/metadata"
"go.skia.org/infra/go/metrics2"
"go.skia.org/infra/go/sklog"
"go.skia.org/infra/go/util"
"go.skia.org/infra/go/vcsinfo"
"go.skia.org/infra/go/webhook"
"go.skia.org/infra/task_scheduler/go/db"
"go.skia.org/infra/task_scheduler/go/db/remote_db"
storage "google.golang.org/api/storage/v1"
)
const (
FAKE_MASTER = "client.skia.fake_internal"
FAKE_BUILDSLAVE = "fake_internal_buildslave"
// SKIA_BRANCH is the name of the git branch we sync Skia to regularly.
SKIA_BRANCH = "git_master-skia"
// MASTER is the git master branch we may check builds against.
MASTER = "git_master"
GOOGLE3_AUTOROLLER_TARGET_NAME = "Google3-Autoroller"
)
// flags
var (
buildbotDbHost = flag.String("buildbot_db_host", "skia-datahopper2:8000", "Where the Skia buildbot database is hosted.")
codenameDbDir = flag.String("codename_db_dir", "codenames", "The location of the leveldb database that holds the mappings between targets and their codenames.")
local = flag.Bool("local", false, "Running locally if true. As opposed to in production.")
period = flag.Duration("period", 5*time.Minute, "The time between ingestion runs.")
port = flag.String("port", ":8000", "HTTP service address (e.g., ':8000')")
promPort = flag.String("prom_port", ":20000", "Metrics service address (e.g., ':10110')")
targetList = flag.String("targets", "", "The targets to monitor, a space separated list.")
taskSchedulerDbUrl = flag.String("task_db_url", "http://skia-task-scheduler:8008/db/", "Where the Skia task scheduler database is hosted.")
taskSchedulerUrl = flag.String("task_scheduler_url", "http://skia-task-scheduler:8000/json/task", "URL for the task scheduler JSON API POST/PUT handlers.")
workdir = flag.String("workdir", ".", "Working directory used by data processors.")
)
var (
// terminal_build_status are the tradefed build status's that mean the build is done.
terminal_build_status = []string{"complete", "error"}
// codenameDB is a leveldb to store codenames and their deobfuscated counterparts.
codenameDB *leveldb.DB
// buildbotDB is a buildbot.DB instance used for ingesting build data.
buildbotDB buildbot.DB
// taskDB is a remote db.TaskReader used for looking up previously-added tasks by ID.
taskDB db.TaskReader
// repos provides information about the Git repositories in workdir.
repos repograph.Map
// tradefedLiveness is a metric for the time since last successful run through step().
tradefedLiveness = metrics2.NewLiveness("android-internal-ingest", nil)
// noCodenameTargets is a set of targets that do not need to be obfuscated.
noCodenameTargets = map[string]bool{
GOOGLE3_AUTOROLLER_TARGET_NAME: true,
}
// ingestBuildWebhookCodenames is the set of codenames we expect in ingestBuildHandler.
ingestBuildWebhookCodenames = map[string]bool{
GOOGLE3_AUTOROLLER_TARGET_NAME: true,
}
// ingestBuildWebhookLiveness maps a target codename to a metric for the time since last
// successful build ingestion.
ingestBuildWebhookLiveness = map[string]metrics2.Liveness{}
httpClient = httputils.NewTimeoutClient()
)
// isFinished returns true if the Build has finished running.
func isFinished(b *androidbuildinternal.Build) bool {
return util.In(b.BuildAttemptStatus, terminal_build_status)
}
// buildFromCommit builds a buildbot.Build from the commit and the info
// returned from the Apiary API. It also returns a key that uniqely identifies
// this build.
func buildFromCommit(build *androidbuildinternal.Build, commit *vcsinfo.ShortCommit) (string, *buildbot.Build) {
codename := util.StringToCodeName(build.Target.Name)
key := build.Branch + ":" + build.Target.Name + ":" + build.BuildId
b := &buildbot.Build{
Builder: codename,
Master: FAKE_MASTER,
Number: 0,
BuildSlave: FAKE_BUILDSLAVE,
Branch: "master",
Commits: nil,
GotRevision: commit.Hash,
Properties: [][]interface{}{
[]interface{}{"androidinternal_buildid", build.BuildId, "tradefed"},
[]interface{}{"buildbotURL", "https://internal.skia.org/", "tradefed"},
},
PropertiesStr: "",
Results: buildbot.BUILDBOT_FAILURE,
Steps: nil,
Started: util.UnixMillisToTime(build.CreationTimestamp),
Comments: nil,
Repository: common.REPO_SKIA,
}
// Fill in PropertiesStr based on Properties.
props, err := json.Marshal(b.Properties)
if err == nil {
b.PropertiesStr = string(props)
} else {
sklog.Errorf("Failed to encode properties: %s", err)
}
if build.Successful {
b.Results = buildbot.BUILDBOT_SUCCESS
}
// Only fill in Finished if the build has completed.
if isFinished(build) {
b.Finished = time.Now().UTC()
}
return key, b
}
// brokenOnMaster returns true if recent builds on master near the given buildID are unsuccessful.
func brokenOnMaster(buildService *androidbuildinternal.Service, target, buildID string) bool {
r, err := buildService.Build.List().Branch(MASTER).BuildType("submitted").StartBuildId(buildID).Target(target).MaxResults(4).Do()
if err != nil {
return false
}
for _, b := range r.Builds {
if isFinished(b) && !b.Successful {
return true
}
}
return false
}
// ingestBuild encapsulates many of the steps of ingesting a build:
// - Record the mapping between the codename (build.Builder) and the internal target name.
// - If no matching build exists, assign a new build number for this build and insert it.
// - Otherwise, update the existing build to match the given build.
func ingestBuild(build *buildbot.Build, commitHash, target string) error {
// Store build.Builder (the codename) with its pair build.Target.Name in a local leveldb to serve redirects.
if err := codenameDB.Put([]byte(build.Builder), []byte(target), nil); err != nil {
sklog.Errorf("Failed to write codename to data store: %s", err)
}
buildNumber, err := buildbotDB.GetBuildNumberForCommit(build.Master, build.Builder, commitHash)
if err != nil {
return fmt.Errorf("Failed to find the build in the database: %s", err)
}
sklog.Infof("GetBuildNumberForCommit at hash: %s returned %d", commitHash, buildNumber)
var existingBuild *buildbot.Build
if buildNumber != -1 {
existingBuild, err = buildbotDB.GetBuildFromDB(build.Master, build.Builder, buildNumber)
if err != nil {
return fmt.Errorf("Failed to retrieve build from database: %s", err)
}
}
taskId := ""
if existingBuild == nil {
// This is a new build we've never seen before, so add it to the buildbot database.
// TODO(benjaminwagner): This logic won't work well for concurrent requests. Revisit
// after borenet's "giant datahopper change."
// First calculate a new unique build.Number.
number, err := buildbotDB.GetMaxBuildNumber(build.Master, build.Builder)
if err != nil {
return fmt.Errorf("Failed to find next build number: %s", err)
}
build.Number = number + 1
sklog.Infof("Writing new build to the database: %s %d", build.Builder, build.Number)
} else {
// If the state of the build has changed then write it to the buildbot database.
build.Number = buildNumber
// Retrieve Task ID from existingBuild.
if id, err := existingBuild.GetStringProperty("taskId"); err == nil {
taskId = id
}
sklog.Infof("Writing updated build to the database: %s %d", build.Builder, build.Number)
}
if taskId == "" {
taskId, err = addTask(build)
if err != nil {
return err
}
} else {
if err := updateTask(taskId, build); err != nil {
return err
}
}
// Save task ID in build.Properties.
build.Properties = append(build.Properties, []interface{}{"taskId", taskId, "datahopper_internal"})
props, err := json.Marshal(build.Properties)
if err == nil {
build.PropertiesStr = string(props)
} else {
sklog.Errorf("Failed to encode properties: %s", err)
}
if err := buildbot.IngestBuild(buildbotDB, build, repos); err != nil {
return fmt.Errorf("Failed to ingest build: %s", err)
}
return nil
}
// taskStatus determines a db.TaskStatus equivalent to build's current status.
func taskStatus(build *buildbot.Build) db.TaskStatus {
if build.IsFinished() {
switch build.Results {
case buildbot.BUILDBOT_SUCCESS, buildbot.BUILDBOT_WARNINGS:
return db.TASK_STATUS_SUCCESS
case buildbot.BUILDBOT_FAILURE:
return db.TASK_STATUS_FAILURE
case buildbot.BUILDBOT_EXCEPTION:
return db.TASK_STATUS_MISHAP
}
} else if build.IsStarted() {
return db.TASK_STATUS_RUNNING
}
return db.TASK_STATUS_PENDING
}
// doTaskRequest adds/updates task using a POST/PUT request to Task Scheduler.
func doTaskRequest(method string, task *db.Task) error {
data, err := json.Marshal(task)
if err != nil {
return fmt.Errorf("Failed to marshal task %v: %s", task, err)
}
req, err := webhook.NewRequest(method, *taskSchedulerUrl, data)
if err != nil {
return fmt.Errorf("Could not create HTTP request: %s", err)
}
resp, err := httpClient.Do(req)
if err != nil {
return fmt.Errorf("%s request failed: %s", method, err)
}
defer util.Close(resp.Body)
if resp.StatusCode != 200 {
response, _ := ioutil.ReadAll(resp.Body)
return fmt.Errorf("Request Failed; response status code was %d: %s", resp.StatusCode, response)
}
var newTask *db.Task
if err := json.NewDecoder(resp.Body).Decode(&newTask); err != nil {
return fmt.Errorf("Unable to parse response from %s: %s", *taskSchedulerUrl, err)
}
*task = *newTask
return nil
}
// setTaskFromBuild sets relevant fields of task based on build.
func setTaskFromBuild(task *db.Task, build *buildbot.Build) {
task.Finished = build.Finished
task.Name = build.Builder
task.Properties = map[string]string{
"buildbotBuilder": build.Builder,
"buildbotMaster": build.Master,
"buildbotNumber": strconv.Itoa(build.Number),
"url": fmt.Sprintf("https://internal.skia.org/builders/%s/builds/%d", build.Builder, build.Number),
}
task.Repo = build.Repository
task.Revision = build.GotRevision
task.Started = build.Started
task.Status = taskStatus(build)
}
// addTask adds a task corresponding to build to the Task Scheduler DB, and returns the task ID.
func addTask(build *buildbot.Build) (string, error) {
task := &db.Task{}
setTaskFromBuild(task, build)
sklog.Infof("Adding task corresponding to build %s %d: %v", build.Builder, build.Number, task)
if err := doTaskRequest(http.MethodPost, task); err != nil {
return "", err
}
return task.Id, nil
}
// updateTask modifies the task with the given id based on build.
func updateTask(id string, build *buildbot.Build) error {
task, err := taskDB.GetTaskById(id)
if err != nil {
return err
} else if task == nil {
return fmt.Errorf("Can not find task %s for build %s %d!", id, build.Builder, build.Number)
}
orig := task.Copy()
setTaskFromBuild(task, build)
if reflect.DeepEqual(orig, task) {
sklog.Infof("No changes for task %s corresponding to build %s %d", id, build.Builder, build.Number)
return nil
}
sklog.Infof("Updating task %s corresponding to build %s %d", id, build.Builder, build.Number)
return doTaskRequest(http.MethodPut, task)
}
// step does a single step in ingesting builds from tradefed and pushing the results into the buildbot database.
func step(targets []string, buildService *androidbuildinternal.Service) {
sklog.Infof("step: Begin")
if err := repos.Update(); err != nil {
sklog.Errorf("Failed to update repos: %s", err)
return
}
// Loop over every target and look for skia commits in the builds.
for _, target := range targets {
r, err := buildService.Build.List().Branch(SKIA_BRANCH).BuildType("submitted").Target(target).ExtraFields("changeInfo").MaxResults(40).Do()
if err != nil {
sklog.Errorf("Failed to load internal builds: %v", err)
continue
}
// Iterate over the builds in reverse order so we ingest the earlier Git
// hashes first and the more recent Git hashes later.
for i := len(r.Builds) - 1; i >= 0; i-- {
b := r.Builds[i]
commits := androidbuild.CommitsFromChanges(b.Changes)
sklog.Infof("Commits: %#v", commits)
if len(commits) > 0 {
// Only look at the first commit in the list. The commits always appear in reverse chronological order, so
// the 0th entry is the most recent commit.
c := commits[0]
// Create a buildbot.Build from the build info.
key, build := buildFromCommit(b, c)
sklog.Infof("Key: %s Hash: %s", key, c.Hash)
// If this was a failure then we need to check that there is a
// mirror failure on the main branch, at which point we will say
// that this is a warning (appears green on status).
if build.Results == buildbot.BUILDBOT_FAILURE && brokenOnMaster(buildService, target, b.BuildId) {
build.Results = buildbot.BUILDBOT_WARNINGS
}
if err := ingestBuild(build, c.Hash, target); err != nil {
sklog.Error(err)
}
}
tradefedLiveness.Reset()
}
}
}
// indexHandler handles the GET of the main page.
func indexHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain")
if _, err := w.Write([]byte("Nothing to see here.")); err != nil {
sklog.Errorf("Failed to write response: %s", err)
}
}
const (
LAUNCH_CONTROL_BUILD_REDIRECT_TEMPLATE = `<!DOCTYPE html>
<html>
<head><title>Redirect</title></head>
<body>
<p>You are being redirected to Launch Control. The un-obfuscated target name for <b>%s</b> is <b>%s</b>.</p>
<p><a href='https://android-build-uber.corp.google.com/builds.html?branch=git_master-skia&lower_limit=%s&upper_limit=%s'>master-skia:%s</a></p>
<p><a href='https://android-build-uber.corp.google.com/builds.html?branch=git_master&lower_limit=%s&upper_limit=%s'>master:%s</a></p>
</body>
</html>
`
LAUNCH_CONTROL_BUILDER_REDIRECT_TEMPLATE = `<!DOCTYPE html>
<html>
<head><title>Redirect</title></head>
<body>
<p>You are being redirected to Launch Control. The un-obfuscated target name for <b>%s</b> is <b>%s</b>.</p>
<p><a href='https://android-build-uber.corp.google.com/builds.html?branch=git_master-skia'>master-skia:%s</a></p>
<p><a href='https://android-build-uber.corp.google.com/builds.html?branch=git_master'>master:%s</a></p>
</body>
</html>
`
GOOGLE3_AUTOROLLER_BORGCRON_REDIRECT_TEMPLATE = `<!DOCTYPE html>
<html>
<head><title>Redirect</title></head>
<body>
<p>You are being redirected to the docs for the Google3 Autoroller.</p>
<p><a href='https://sites.google.com/a/google.com/skia-infrastructure/docs/google3-autoroller'>Google3 Autoroller</a></p>
</body>
</html>
`
TEST_RESULTS_REDIRECT_TEMPLATE = `<!DOCTYPE html>
<html>
<head><title>Redirect</title></head>
<body>
<p>You are being redirected to the test results for <b>%s</b>.</p>
<p><a href='%s'>%s</a></p>
</body>
</html>
`
)
// redirectHandler handles redirecting to the correct internal build page.
func redirectHandler(w http.ResponseWriter, r *http.Request) {
if login.LoggedInAs(r) == "" {
r.Header.Set("Referer", r.URL.String())
http.Redirect(w, r, login.LoginURL(w, r), 302)
return
} else if !login.IsGoogler(r) {
errStr := "Cannot view; user is not a logged-in Googler."
httputils.ReportError(w, r, fmt.Errorf(errStr), errStr)
return
}
vars := mux.Vars(r)
codename := vars["codename"]
buildNumberStr := vars["buildNumber"]
target, err := codenameDB.Get([]byte(codename), nil)
if err != nil {
httputils.ReportError(w, r, err, "Not a valid target codename.")
return
}
buildNumber, err := strconv.Atoi(buildNumberStr)
if err != nil {
httputils.ReportError(w, r, err, "Not a valid build number.")
return
}
build, err := buildbotDB.GetBuildFromDB(FAKE_MASTER, string(codename), buildNumber)
if err != nil {
httputils.ReportError(w, r, err, "Could not find a matching build.")
}
result := ""
if id, err := build.GetStringProperty("androidinternal_buildid"); err == nil {
result = fmt.Sprintf(LAUNCH_CONTROL_BUILD_REDIRECT_TEMPLATE, codename, target, id, id, target, id, id, target)
} else if link, err := build.GetStringProperty("testResultsLink"); err == nil {
result = fmt.Sprintf(TEST_RESULTS_REDIRECT_TEMPLATE, target, link, link)
} else if cl, err := build.GetStringProperty("changeListNumber"); err == nil {
link = fmt.Sprintf("http://cl/%s", cl)
result = fmt.Sprintf(TEST_RESULTS_REDIRECT_TEMPLATE, target, link, link)
}
if result == "" {
sklog.Errorf("No redirect for %#v", build)
httputils.ReportError(w, r, nil, "No redirect for this build.")
return
}
w.Header().Set("Content-Type", "text/html")
if _, err := w.Write([]byte(result)); err != nil {
httputils.ReportError(w, r, err, "Failed to write response")
}
}
type Mapping struct {
Codename string
Target string
}
// mappingHandler displays all codename to target mappings.
func mappingHandler(w http.ResponseWriter, r *http.Request) {
if login.LoggedInAs(r) == "" {
r.Header.Set("Referer", r.URL.String())
http.Redirect(w, r, login.LoginURL(w, r), 302)
return
} else if !login.IsGoogler(r) {
errStr := "Cannot view; user is not a logged-in Googler."
httputils.ReportError(w, r, fmt.Errorf(errStr), errStr)
return
}
iter := codenameDB.NewIterator(nil, nil)
allMappings := []Mapping{}
for iter.Next() {
m := Mapping{Codename: string(iter.Key()), Target: string(iter.Value())}
allMappings = append(allMappings, m)
}
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(allMappings); err != nil {
httputils.ReportError(w, r, err, "Failed to marshal mappings.")
return
}
}
// builderRedirectHandler handles redirecting to the correct internal builder page.
func builderRedirectHandler(w http.ResponseWriter, r *http.Request) {
if login.LoggedInAs(r) == "" {
r.Header.Set("Referer", r.URL.String())
http.Redirect(w, r, login.LoginURL(w, r), 302)
return
} else if !login.IsGoogler(r) {
errStr := "Cannot view; user is not a logged-in Googler."
httputils.ReportError(w, r, fmt.Errorf(errStr), errStr)
return
}
vars := mux.Vars(r)
codename := vars["codename"]
target, err := codenameDB.Get([]byte(codename), nil)
if err != nil {
httputils.ReportError(w, r, err, "Not a valid target codename.")
return
}
w.Header().Set("Content-Type", "text/html")
response := ""
if string(target) == GOOGLE3_AUTOROLLER_TARGET_NAME {
response = GOOGLE3_AUTOROLLER_BORGCRON_REDIRECT_TEMPLATE
} else {
response = fmt.Sprintf(LAUNCH_CONTROL_BUILDER_REDIRECT_TEMPLATE, codename, target, target, target)
}
if _, err := w.Write([]byte(response)); err != nil {
sklog.Errorf("Failed to write response: %s", err)
}
}
// ingestBuildHandler parses the JSON body as a build and ingests it. The request must be
// authenticated via the protocol implemented in the webhook package. The client should retry this
// request several times, because some errors may be temporary.
func ingestBuildHandler(w http.ResponseWriter, r *http.Request) {
data, err := webhook.AuthenticateRequest(r)
if err != nil {
sklog.Errorf("Failed authentication in ingestBuildHandler: %s", err)
httputils.ReportError(w, r, nil, "Failed authentication.")
return
}
vars := map[string]string{}
if err := json.Unmarshal(data, &vars); err != nil {
sklog.Errorf("Failed to parse request: %s", err)
httputils.ReportError(w, r, nil, "Failed to parse request.")
return
}
target := vars["target"]
commitHash := vars["commitHash"]
status := vars["status"]
if target == "" || commitHash == "" || status == "" {
httputils.ReportError(w, r, nil, "Missing parameter.")
return
}
cl := vars["changeListNumber"]
link := vars["testResultsLink"]
startTimeStr := vars["startTime"]
finishTimeStr := vars["finishTime"]
codename := ""
if noCodenameTargets[target] {
codename = target
} else {
codename = util.StringToCodeName(target)
}
if !ingestBuildWebhookCodenames[codename] {
httputils.ReportError(w, r, nil, fmt.Sprintf("Unrecognized target (mapped to codename %s)", codename))
return
}
buildbotResults, err := buildbot.ParseResultsString(status)
if err != nil {
sklog.Errorf("Invalid status parameter: %s", err)
httputils.ReportError(w, r, nil, "Invalid status parameter.")
return
}
startTime := time.Now().UTC()
if startTimeStr != "" {
if t, err := strconv.ParseInt(startTimeStr, 10, 64); err == nil {
startTime = time.Unix(t, 0).UTC()
} else {
sklog.Errorf("Invalid startTime parameter: %s", err)
httputils.ReportError(w, r, nil, "Invalid startTime parameter.")
return
}
}
finishTime := time.Now().UTC()
if finishTimeStr != "" {
if t, err := strconv.ParseInt(finishTimeStr, 10, 64); err == nil {
finishTime = time.Unix(t, 0).UTC()
} else {
sklog.Errorf("Invalid finishTime parameter: %s", err)
httputils.ReportError(w, r, nil, "Invalid finishTime parameter.")
return
}
}
b := &buildbot.Build{
Builder: codename,
Master: FAKE_MASTER,
Number: 0,
BuildSlave: FAKE_BUILDSLAVE,
Branch: "master",
Commits: nil,
GotRevision: commitHash,
Properties: [][]interface{}{
[]interface{}{"buildbotURL", "https://internal.skia.org/", "datahopper_internal"},
},
PropertiesStr: "",
Results: buildbotResults,
Steps: nil,
Started: startTime,
Finished: finishTime,
Comments: nil,
Repository: common.REPO_SKIA,
}
if cl != "" {
if clNum, err := strconv.Atoi(cl); err == nil {
b.Properties = append(b.Properties, []interface{}{"changeListNumber", strconv.Itoa(clNum), "datahopper_internal"})
} else {
sklog.Errorf("Invalid changeListNumber parameter: %s", err)
httputils.ReportError(w, r, nil, "Invalid changeListNumber parameter.")
return
}
}
if link != "" {
if url, err := url.Parse(link); err == nil {
b.Properties = append(b.Properties, []interface{}{"testResultsLink", url.String(), "datahopper_internal"})
} else {
sklog.Errorf("Invalid testResultsLink parameter: %s", err)
httputils.ReportError(w, r, nil, "Invalid testResultsLink parameter.")
return
}
}
// Fill in PropertiesStr based on Properties.
props, err := json.Marshal(b.Properties)
if err == nil {
b.PropertiesStr = string(props)
} else {
sklog.Errorf("Failed to encode properties: %s", err)
}
if err := repos.Update(); err != nil {
httputils.ReportError(w, r, err, "Failed to update repos.")
return
}
if err := ingestBuild(b, commitHash, target); err != nil {
sklog.Errorf("Failed to ingest build: %s", err)
httputils.ReportError(w, r, nil, "Failed to ingest build.")
return
}
if metric, present := ingestBuildWebhookLiveness[codename]; present {
metric.Reset()
}
}
// updateWebhookMetrics updates "oldest-untested-commit-age" metrics for all
// ingestBuildWebhookCodenames. This metric records the age of the oldest commit for which we have
// not ingested a build. Returns an error if any metric was not updated.
func updateWebhookMetrics() error {
if err := repos.Update(); err != nil {
return err
}
repo, ok := repos[common.REPO_SKIA]
if !ok {
return fmt.Errorf("Unknown repo: %s", common.REPO_SKIA)
}
for codename, _ := range ingestBuildWebhookCodenames {
var untestedCommitInfo *repograph.Commit = nil
if err := repo.Get("master").Recurse(func(c *repograph.Commit) (bool, error) {
buildNumber, err := buildbotDB.GetBuildNumberForCommit(FAKE_MASTER, codename, c.Hash)
if err != nil {
return false, err
}
if buildNumber == -1 {
untestedCommitInfo = c
// No tests for this commit, check older.
return true, nil
}
return false, nil
}); err != nil {
return err
}
metric := metrics2.GetInt64Metric("datahopper_internal.ingest-build-webhook.oldest-untested-commit-age", map[string]string{"codename": codename})
if untestedCommitInfo == nil {
// There are no untested commits.
metric.Update(0)
} else {
metric.Update(int64(time.Since(untestedCommitInfo.Timestamp).Seconds()))
}
break
}
return nil
}
// startWebhookMetrics starts a goroutine to run updateWebhookMetrics.
func startWebhookMetrics() {
// A metric to ensure the other metrics are being updated.
metricLiveness := metrics2.NewLiveness("ingest-build-webhook-oldest-untested-commit-age-metric", nil)
go func() {
for _ = range time.Tick(common.SAMPLE_PERIOD) {
if err := updateWebhookMetrics(); err != nil {
sklog.Errorf("Failed to update metrics: %s", err)
continue
}
metricLiveness.Reset()
}
}()
}
func main() {
defer common.LogPanic()
var err error
common.InitWithMust(
"datahopper_internal",
common.PrometheusOpt(promPort),
common.CloudLoggingOpt(),
)
if !*local {
*targetList = metadata.Must(metadata.ProjectGet("datahopper_internal_targets"))
}
targets := strings.Split(*targetList, " ")
sklog.Infof("Targets: %#v", targets)
codenameDB, err = leveldb.OpenFile(*codenameDbDir, nil)
if err != nil && errors.IsCorrupted(err) {
codenameDB, err = leveldb.RecoverFile(*codenameDbDir, nil)
}
if err != nil {
sklog.Fatalf("Failed to open codename leveldb at %s: %s", *codenameDbDir, err)
}
// Initialize the buildbot database.
if *local {
buildbotDB, err = buildbot.NewLocalDB(path.Join(*workdir, "buildbot.db"))
if err != nil {
sklog.Fatal(err)
}
} else {
buildbotDB, err = buildbot.NewRemoteDB(*buildbotDbHost)
if err != nil {
sklog.Fatal(err)
}
}
// Create remote Tasks DB.
taskDB, err = remote_db.NewClient(*taskSchedulerDbUrl)
if err != nil {
sklog.Fatal(err)
}
login.SimpleInitMust(*port, *local)
if *local {
webhook.InitRequestSaltForTesting()
} else {
webhook.MustInitRequestSaltFromMetadata()
}
// Ensure Skia repo is cloned and updated.
repos, err = repograph.NewMap([]string{common.REPO_SKIA}, *workdir)
if err != nil {
sklog.Fatal(err)
}
// Initialize and start metrics.
for codename, _ := range ingestBuildWebhookCodenames {
ingestBuildWebhookLiveness[codename] = metrics2.NewLiveness("ingest-build-webhook.", map[string]string{"codename": codename})
}
startWebhookMetrics()
// Ingest Android framework builds.
go func() {
sklog.Infof("Starting.")
// In this case we don't want a backoff transport since the Apiary backend
// seems to fail a lot, so we basically want to fall back to polling if a
// call fails.
client, err := auth.NewJWTServiceAccountClient("", "", &http.Transport{Dial: httputils.DialTimeout}, androidbuildinternal.AndroidbuildInternalScope, storage.CloudPlatformScope)
if err != nil {
sklog.Fatalf("Unable to create authenticated client: %s", err)
}
buildService, err := androidbuildinternal.New(client)
if err != nil {
sklog.Fatalf("Failed to obtain Android build service: %v", err)
}
sklog.Infof("Ready to start loop.")
step(targets, buildService)
for _ = range time.Tick(*period) {
step(targets, buildService)
}
}()
r := mux.NewRouter()
r.HandleFunc("/", indexHandler)
r.HandleFunc("/builders/{codename}/builds/{buildNumber}", redirectHandler)
r.HandleFunc("/builders/{codename}", builderRedirectHandler)
// Note: The unobfuscate-status extension relies on the below handler and its contents.
r.HandleFunc("/mapping/", mappingHandler)
r.HandleFunc("/ingestBuild", ingestBuildHandler)
r.HandleFunc("/loginstatus/", login.StatusHandler)
r.HandleFunc("/logout/", login.LogoutHandler)
r.HandleFunc("/oauth2callback/", login.OAuth2CallbackHandler)
http.Handle("/", httputils.LoggingGzipRequestResponse(r))
sklog.Fatal(http.ListenAndServe(*port, nil))
}