| // trybot_updater is an application that updates a repo's buildbucket.config file. |
| package main |
| |
| import ( |
| "bytes" |
| "context" |
| "flag" |
| "net/http" |
| "sort" |
| "strings" |
| "text/template" |
| "time" |
| |
| "go.skia.org/infra/go/auth" |
| "go.skia.org/infra/go/common" |
| "go.skia.org/infra/go/gerrit" |
| "go.skia.org/infra/go/git" |
| "go.skia.org/infra/go/gitiles" |
| "go.skia.org/infra/go/httputils" |
| "go.skia.org/infra/go/rotations" |
| "go.skia.org/infra/go/skerr" |
| "go.skia.org/infra/go/sklog" |
| "go.skia.org/infra/go/util" |
| "go.skia.org/infra/task_scheduler/go/specs" |
| "golang.org/x/oauth2/google" |
| ) |
| |
| const ( |
| // The format of this file is that of a gerrit extension config (not a proto). |
| // The buildbucket extension parses the config like this: |
| // https://chromium.googlesource.com/infra/gerrit-plugins/buildbucket/+/refs/heads/main/src/main/java/com/googlesource/chromium/plugins/buildbucket/GetConfig.java |
| bbCfgFileName = "buildbucket.config" |
| // Branch buildbucket.config lies in. Hopefully this will change one day, see b/38258213. |
| bbCfgBranch = "refs/meta/config" |
| // Template used to create buildbucket.config content. |
| bbCfgTemplate = ` |
| # This file is generated by trybot-updater from {{.TasksCfgFile}}. DO NOT EDIT. |
| |
| {{range .EmptyBuckets}}[bucket "{{.}}"]{{end}} |
| [bucket "{{.BucketName}}"] |
| {{- range .Jobs}} |
| builder = {{.}} |
| {{- end}} |
| ` |
| ) |
| |
| var ( |
| // Flags. |
| repoUrl = flag.String("repo_url", common.REPO_SKIA, "Repo that needs buildbucket.config updated from it's tasks.json file.") |
| repoDefaultBranch = flag.String("branch", git.MainBranch, "The default branch of the specified repo.") |
| bucketName = flag.String("bucket_name", "luci.skia.skia.primary", "Name of the bucket to update in buildbucket.config.") |
| emptyBuckets = common.NewMultiStringFlag("empty_bucket", nil, "Empty buckets to specify in buildbucket.config. Eg: luci.chromium.try. See skbug.com/9639 for why these buckets are empty.") |
| pollingPeriod = flag.Duration("polling_period", 10*time.Minute, "How often to poll tasks.json.") |
| submit = flag.Bool("submit", false, "If set, automatically submit the Gerrit change to update buildbucket.config") |
| local = flag.Bool("local", false, "Running locally if true. As opposed to in production.") |
| promPort = flag.String("prom_port", ":20000", "Metrics service address (e.g., ':20000')") |
| |
| bbCfgTemplateParsed = template.Must(template.New("buildbucket_config").Parse(bbCfgTemplate)) |
| ) |
| |
| // getBuildbucketCfgFromJobs reads tasks.json from the specified repository and returns |
| // contents of what the new buildbucket.config file should be. |
| func getBuildbucketCfgFromJobs(ctx context.Context, repo *gitiles.Repo) (string, error) { |
| // Read tasks.json from the specified repository. |
| tasksContents, err := repo.ReadFileAtRef(ctx, specs.TASKS_CFG_FILE, *repoDefaultBranch) |
| if err != nil { |
| return "", skerr.Fmt("Could not read %s: %s", specs.TASKS_CFG_FILE, err) |
| } |
| tasksCfg, err := specs.ParseTasksCfg(string(tasksContents)) |
| if err != nil { |
| return "", skerr.Fmt("Could not parse %s: %s", specs.TASKS_CFG_FILE, err) |
| } |
| |
| // Create a sorted slice of jobs. |
| jobs := make([]string, 0, len(tasksCfg.Jobs)) |
| for j := range tasksCfg.Jobs { |
| jobs = append(jobs, j) |
| } |
| sort.Strings(jobs) |
| |
| // Use jobs to create content of buildbucket.config. |
| bbCfg := new(bytes.Buffer) |
| if err := bbCfgTemplateParsed.Execute(bbCfg, struct { |
| TasksCfgFile string |
| EmptyBuckets []string |
| BucketName string |
| Jobs []string |
| }{ |
| TasksCfgFile: specs.TASKS_CFG_FILE, |
| EmptyBuckets: *emptyBuckets, |
| BucketName: *bucketName, |
| Jobs: jobs, |
| }); err != nil { |
| return "", skerr.Fmt("Failed to execute bbCfg template: %s", err) |
| } |
| return bbCfg.String(), nil |
| } |
| |
| // getCurrentBuildbucketCfg returns the current contents of buildbucket.config for the |
| // specified repository. |
| func getCurrentBuildbucketCfg(ctx context.Context, repo *gitiles.Repo) (string, error) { |
| contents, err := repo.ReadFileAtRef(ctx, bbCfgFileName, bbCfgBranch) |
| if err != nil { |
| return "", skerr.Fmt("Could not read %s: %s", bbCfgFileName, err) |
| } |
| return string(contents), nil |
| } |
| |
| // updateBuildbucketCfg creates a Gerrit CL to update buildbucket.config. If submit flag is true then that CL |
| // is automatically self-approved and submitted. |
| func updateBuildbucketCfg(ctx context.Context, g *gerrit.Gerrit, repo *gitiles.Repo, cfgContents string, httpClient *http.Client) error { |
| commitMsg := `Update buildbucket.config |
| |
| Update is done by the trybot-updater bot. |
| Please contact the Skia Infra Gardener if this bot causes problems. |
| ` |
| repoSplit := strings.Split(*repoUrl, "/") |
| project := strings.TrimSuffix(repoSplit[len(repoSplit)-1], ".git") |
| baseCommitInfo, err := repo.Details(ctx, bbCfgBranch) |
| if err != nil { |
| return skerr.Fmt("Could not get details of %s: %s", bbCfgBranch, err) |
| } |
| baseCommit := baseCommitInfo.Hash |
| const baseChangeID = "" |
| ci, err := gerrit.CreateAndEditChange(ctx, g, project, bbCfgBranch, commitMsg, baseCommit, baseChangeID, func(ctx context.Context, g gerrit.GerritInterface, ci *gerrit.ChangeInfo) error { |
| if err := g.EditFile(ctx, ci, bbCfgFileName, cfgContents); err != nil { |
| return skerr.Fmt("Could not edit %s: %s", bbCfgFileName, err) |
| } |
| return nil |
| }) |
| if err != nil { |
| // If a change was created but had errors then abandon it. |
| if ci != nil { |
| if abandonErr := g.Abandon(ctx, ci, "Trybot updater CL creation had an error"); abandonErr != nil { |
| sklog.Errorf("Error when abandoning change %s: %s", ci.Id, abandonErr) |
| } |
| } |
| return skerr.Fmt("Could not create Gerrit change: %s", err) |
| } |
| sklog.Infof("Uploaded change https://skia-review.googlesource.com/c/%s/+/%d", project, ci.Issue) |
| |
| if *submit { |
| gardeners, err := rotations.FromURL(httpClient, rotations.InfraGardenerURL) |
| if err != nil { |
| return skerr.Fmt("Could not get infra gardener email: %s", err) |
| } |
| if err := g.SetReview(ctx, ci, "", gerrit.ConfigChromiumBotCommit.SelfApproveLabels, gardeners, "", nil, "", 0, nil); err != nil { |
| return abandonGerritChange(ctx, g, ci, err) |
| } |
| if err := g.Submit(ctx, ci); err != nil { |
| return abandonGerritChange(ctx, g, ci, err) |
| } |
| sklog.Infof("Submitted change https://skia-review.googlesource.com/c/%s/+/%d", project, ci.Issue) |
| } |
| |
| return nil |
| } |
| |
| // abandonGerritChange abandons the specified CL and returns the specified err after abandoning. |
| // If abandoning fails then that error is wrapped with the specified err. |
| func abandonGerritChange(ctx context.Context, g *gerrit.Gerrit, issue *gerrit.ChangeInfo, err error) error { |
| if abandonErr := g.Abandon(ctx, issue, ""); abandonErr != nil { |
| return skerr.Wrapf(err, "failed to abandon CL with %s", abandonErr) |
| } |
| return err |
| } |
| |
| func main() { |
| common.InitWithMust( |
| "trybot_updater", |
| common.PrometheusOpt(promPort), |
| ) |
| defer sklog.Flush() |
| ctx := context.Background() |
| |
| // OAuth2.0 TokenSource. |
| ts, err := google.DefaultTokenSource(ctx, auth.ScopeUserinfoEmail, auth.ScopeGerrit) |
| if err != nil { |
| sklog.Fatal(err) |
| } |
| // Authenticated HTTP client. |
| httpClient := httputils.DefaultClientConfig().WithTokenSource(ts).With2xxOnly().Client() |
| |
| // Instantiate Gerrit. |
| gUrl := strings.Split(*repoUrl, ".googlesource.com")[0] + "-review.googlesource.com" |
| g, err := gerrit.NewGerrit(gUrl, httpClient) |
| if err != nil { |
| sklog.Fatal(err) |
| } |
| |
| // Instantiate Gitiles using the specified repo URL. |
| repo := gitiles.NewRepo(*repoUrl, httpClient) |
| |
| // TODO(rmistry): Use pubsub instead of polling. |
| util.RepeatCtx(ctx, *pollingPeriod, func(ctx context.Context) { |
| existingCfg, err := getCurrentBuildbucketCfg(ctx, repo) |
| if err != nil { |
| sklog.Errorf("Could not get contents of buildbucket.config from %s: %s", repo, err) |
| return |
| } |
| |
| newCfg, err := getBuildbucketCfgFromJobs(ctx, repo) |
| if err != nil { |
| sklog.Errorf("Could not get list of jobs from %s: %s", repo, err) |
| return |
| } |
| |
| // Only update buildbucket.config if the config is different. |
| if newCfg != existingCfg { |
| if err := updateBuildbucketCfg(ctx, g, repo, newCfg, httpClient); err != nil { |
| sklog.Errorf("Could not update buildbucket.config: %s", err) |
| sklog.Info("Sleep for 10 mins since there was a error with Gerrit to give it time to recover.") |
| time.Sleep(10 * time.Minute) |
| } |
| } else { |
| sklog.Info("Config has not changed") |
| } |
| }) |
| } |