blob: 085b0b64707dd2a211243da0a929a2c1ae8ba9b3 [file] [log] [blame]
/*
Automatic DEPS rolls of Skia into Chrome.
*/
package main
import (
"context"
"encoding/json"
"flag"
"fmt"
"net/http"
"net/url"
"os/user"
"path"
"path/filepath"
"runtime"
"strings"
"text/template"
"time"
"github.com/gorilla/mux"
"go.skia.org/infra/go/depot_tools"
"go.skia.org/infra/go/metadata"
"go.skia.org/infra/go/sklog"
"go.skia.org/infra/autoroll/go/autorollerv2"
"go.skia.org/infra/autoroll/go/repo_manager"
"go.skia.org/infra/go/common"
"go.skia.org/infra/go/gerrit"
"go.skia.org/infra/go/httputils"
"go.skia.org/infra/go/login"
"go.skia.org/infra/go/metrics2"
"go.skia.org/infra/go/skiaversion"
"go.skia.org/infra/go/util"
)
var (
arb *autorollerv2.AutoRoller = nil
mainTemplate *template.Template = nil
)
// flags
var (
childName = flag.String("childName", "Skia", "Name of the project to roll.")
childPath = flag.String("childPath", "src/third_party/skia", "Path within parent repo of the project to roll.")
childBranch = flag.String("child_branch", "master", "Branch of the project we want to roll.")
cqExtraTrybots = flag.String("cqExtraTrybots", "", "Comma-separated list of trybots to run.")
host = flag.String("host", "localhost", "HTTP service host")
local = flag.Bool("local", false, "Running locally if true. As opposed to in production.")
parentRepo = flag.String("parent_repo", common.REPO_CHROMIUM, "Repo to roll into.")
parentBranch = flag.String("parent_branch", "master", "Branch of the parent repo we want to roll into.")
port = flag.String("port", ":8000", "HTTP service port (e.g., ':8000')")
promPort = flag.String("prom_port", ":20000", "Metrics service address (e.g., ':10110')")
resourcesDir = flag.String("resources_dir", "", "The directory to find templates, JS, and CSS files. If blank the current directory will be used.")
sheriff = flag.String("sheriff", "", "Email address to CC on rolls, or URL from which to obtain such an email address.")
strategy = flag.String("strategy", repo_manager.ROLL_STRATEGY_BATCH, "DEPS roll strategy; how many commits should be rolled at once.")
useMetadata = flag.Bool("use_metadata", true, "Load sensitive values from metadata not from flags.")
workdir = flag.String("workdir", ".", "Directory to use for scratch work.")
rollIntoAndroid = flag.Bool("roll_into_android", false, "Roll into Android; do not do a DEPS/Manifest roll.")
useManifest = flag.Bool("use_manifest", false, "Do a Manifest roll.")
gerritUrl = flag.String("gerrit_url", gerrit.GERRIT_CHROMIUM_URL, "Gerrit URL the roller will be uploading issues to.")
)
func getSheriff() ([]string, error) {
emails, err := getSheriffHelper()
if err != nil {
return nil, err
}
if strings.Contains(*parentRepo, "chromium") {
for i, s := range emails {
emails[i] = strings.Replace(s, "google.com", "chromium.org", 1)
}
}
return emails, nil
}
func getSheriffHelper() ([]string, error) {
// If the passed-in sheriff doesn't look like a URL, it's probably an
// email address. Use it directly.
if _, err := url.ParseRequestURI(*sheriff); err != nil {
if strings.Count(*sheriff, "@") == 1 {
return []string{*sheriff}, nil
} else {
return nil, fmt.Errorf("Sheriff must be an email address or a valid URL; %q doesn't look like either.", *sheriff)
}
}
// Hit the URL to get the email address. Expect JSON.
client := httputils.NewTimeoutClient()
resp, err := client.Get(*sheriff)
if err != nil {
return nil, err
}
defer util.Close(resp.Body)
var sheriff struct {
Username string `json:"username"`
}
if err := json.NewDecoder(resp.Body).Decode(&sheriff); err != nil {
return nil, err
}
return []string{sheriff.Username}, nil
}
func getCQExtraTrybots() string {
return *cqExtraTrybots
}
func reloadTemplates() {
// Change the current working directory to two directories up from this source file so that we
// can read templates and serve static (res/) files.
if *resourcesDir == "" {
_, filename, _, _ := runtime.Caller(0)
*resourcesDir = filepath.Join(filepath.Dir(filename), "../..")
}
mainTemplate = template.Must(template.ParseFiles(
filepath.Join(*resourcesDir, "templates/main.html"),
filepath.Join(*resourcesDir, "templates/header.html"),
))
}
func Init() {
reloadTemplates()
}
func modeJsonHandler(w http.ResponseWriter, r *http.Request) {
if !login.IsGoogler(r) {
httputils.ReportError(w, r, fmt.Errorf("User does not have edit rights."), "You must be logged in with an @google.com account to do that.")
return
}
var mode struct {
Message string `json:"message"`
Mode string `json:"mode"`
}
defer util.Close(r.Body)
if err := json.NewDecoder(r.Body).Decode(&mode); err != nil {
httputils.ReportError(w, r, err, "Failed to decode request body.")
return
}
if err := arb.SetMode(mode.Mode, login.LoggedInAs(r), mode.Message); err != nil {
httputils.ReportError(w, r, err, "Failed to set AutoRoll mode.")
return
}
// Return the ARB status.
statusJsonHandler(w, r)
}
func statusJsonHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
// Obtain the status info. Only display error messages if the user
// is a logged-in Googler.
status := arb.GetStatus(login.IsGoogler(r))
if err := json.NewEncoder(w).Encode(&status); err != nil {
sklog.Error(err)
}
}
func mainHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")
// Don't use cached templates in testing mode.
if *local {
reloadTemplates()
}
mainPage := struct {
ProjectName string
ProjectUser string
}{
ProjectName: *childName,
ProjectUser: arb.GetUser(),
}
if err := mainTemplate.Execute(w, mainPage); err != nil {
sklog.Errorln("Failed to expand template:", err)
}
}
func runServer() {
r := mux.NewRouter()
r.PathPrefix("/res/").HandlerFunc(httputils.MakeResourceHandler(*resourcesDir))
r.HandleFunc("/", mainHandler)
r.HandleFunc("/json/mode", modeJsonHandler).Methods("POST")
r.HandleFunc("/json/status", httputils.CorsHandler(statusJsonHandler))
r.HandleFunc("/json/version", skiaversion.JsonHandler)
r.HandleFunc("/oauth2callback/", login.OAuth2CallbackHandler)
r.HandleFunc("/logout/", login.LogoutHandler)
r.HandleFunc("/loginstatus/", login.StatusHandler)
http.Handle("/", httputils.LoggingGzipRequestResponse(r))
sklog.Fatal(http.ListenAndServe(*port, nil))
}
func main() {
common.InitWithMust(
"autoroll",
common.PrometheusOpt(promPort),
common.CloudLoggingOpt(),
)
Init()
v, err := skiaversion.GetVersion()
if err != nil {
sklog.Fatal(err)
}
sklog.Infof("Version %s, built at %s", v.Commit, v.Date)
if *local {
*useMetadata = false
}
user, err := user.Current()
if err != nil {
sklog.Fatal(err)
}
gitcookiesPath := path.Join(user.HomeDir, ".gitcookies")
androidInternalGerritUrl := *gerritUrl
if *useMetadata {
// If we are rolling into Android get the Gerrit Url from metadata.
androidInternalGerritUrl, err = metadata.ProjectGet("android_internal_gerrit_url")
if err != nil {
sklog.Fatal(err)
}
}
// Create the code review API client.
gUrl := *gerritUrl
if *rollIntoAndroid {
if !*local {
// Android roller uses the gitcookie created by gcompute-tools/git-cookie-authdaemon.
// TODO(rmistry): Turn this on via the GCE setup script so that it exists right when the instance comes up?
gitcookiesPath = filepath.Join(user.HomeDir, ".git-credential-cache", "cookie")
}
gUrl = androidInternalGerritUrl
} else {
if strings.Contains(*parentRepo, "skia") {
gUrl = gerrit.GERRIT_SKIA_URL
}
}
g, err := gerrit.NewGerrit(gUrl, gitcookiesPath, nil)
if err != nil {
sklog.Fatalf("Failed to create Gerrit client: %s", err)
}
g.TurnOnAuthenticatedGets()
// Retrieve the list of extra CQ trybots.
// TODO(borenet): Make this editable on the web front-end.
cqExtraTrybots := getCQExtraTrybots()
sklog.Infof("CQ extra trybots: %s", cqExtraTrybots)
// Retrieve the initial email list.
emails, err := getSheriff()
if err != nil {
sklog.Fatal(err)
}
sklog.Infof("Sheriff: %s", strings.Join(emails, ", "))
// Sync depot_tools.
var depotTools string
if !*rollIntoAndroid {
depotTools, err = depot_tools.Sync(*workdir)
if err != nil {
sklog.Fatal(err)
}
}
// Start the autoroller.
if *rollIntoAndroid {
arb, err = autorollerv2.NewAndroidAutoRoller(*workdir, *parentBranch, *childPath, *childBranch, cqExtraTrybots, emails, g, *strategy)
} else if *useManifest {
arb, err = autorollerv2.NewManifestAutoRoller(*workdir, *parentRepo, *parentBranch, *childPath, *childBranch, cqExtraTrybots, emails, g, depotTools, *strategy)
} else {
arb, err = autorollerv2.NewDEPSAutoRoller(*workdir, *parentRepo, *parentBranch, *childPath, *childBranch, cqExtraTrybots, emails, g, depotTools, *strategy)
}
if err != nil {
sklog.Fatal(err)
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
arb.Start(time.Minute /* tickFrequency */, 15*time.Minute /* repoFrequency */, ctx)
// Feed AutoRoll stats into metrics.
go func() {
for range time.Tick(time.Minute) {
status := arb.GetStatus(false)
v := int64(0)
if status.LastRoll != nil && status.LastRoll.Closed && status.LastRoll.Committed {
v = int64(1)
}
metrics2.GetInt64Metric("autoroll.last-roll-result", map[string]string{"child-path": *childPath}).Update(v)
}
}()
// Update the current sheriff in a loop.
go func() {
for range time.Tick(30 * time.Minute) {
emails, err := getSheriff()
if err != nil {
sklog.Errorf("Failed to retrieve current sheriff: %s", err)
} else {
arb.SetEmails(emails)
}
}
}()
login.SimpleInitMust(*port, *local)
runServer()
}