blob: 8d00ed08fd1b71d87aa259123760ce2bd44c8e8e [file] [log] [blame]
// Package buildbucket provides tools for interacting with the buildbucket API.
package buildbucket
import (
"context"
"net/http"
buildbucketpb "go.chromium.org/luci/buildbucket/proto"
structpb "google.golang.org/protobuf/types/known/structpb"
"go.chromium.org/luci/grpc/prpc"
"go.skia.org/infra/go/buildbucket/common"
"go.skia.org/infra/go/skerr"
)
const (
BUILD_URL_TMPL = "https://%s/build/%d"
DEFAULT_HOST = "cr-buildbucket.appspot.com"
)
var (
DEFAULT_SCOPES = []string{
"https://www.googleapis.com/auth/userinfo.email",
"https://www.googleapis.com/auth/userinfo.profile",
}
)
type BuildBucketInterface interface {
// GetBuild retrieves the build with the given ID.
GetBuild(ctx context.Context, buildId int64) (*buildbucketpb.Build, error)
// Search retrieves Builds which match the given criteria.
Search(ctx context.Context, pred *buildbucketpb.BuildPredicate) ([]*buildbucketpb.Build, error)
// GetTrybotsForCL retrieves trybot results for the given CL using the
// optional tags.
GetTrybotsForCL(ctx context.Context, issue, patchset int64, gerritUrl string, tags map[string]string) ([]*buildbucketpb.Build, error)
// ScheduleBuilds schedules the specified builds on the given CL. Builds are
// scheduled with one batch request to buildbucket.
// builds is the slice of which builds should be scheduled by buildbucket.
// Eg: ["Infra-PerCommit-Race", "Infra-PerCommit-Small"].
// buildsToTags is the map of which tags to use when scheduling
// some of the builds. Eg: {"Infra-PerCommit-Race": {"triggered_by": "skcq"}}
// means that the Infra-PerCommit-Race build should be scheduled with the
// "triggered_by: skcq" tag.
ScheduleBuilds(ctx context.Context, builds []string, buildsToTags map[string]map[string]string, issue, patchset int64, gerritUrl, repo, bbProject, bbBucket string) ([]*buildbucketpb.Build, error)
// CancelBuilds cancels the specified buildIDs with the specified
// summaryMarkdown. Builds are cancelled with one batch request
// to buildbucket.
CancelBuilds(ctx context.Context, buildIDs []int64, summaryMarkdown string) ([]*buildbucketpb.Build, error)
}
// Client is used for interacting with the BuildBucket API.
type Client struct {
bc buildbucketpb.BuildsClient
host string
}
// NewClient returns an authenticated Client instance.
func NewClient(c *http.Client) *Client {
host := DEFAULT_HOST
return &Client{
bc: buildbucketpb.NewBuildsPRPCClient(&prpc.Client{
C: c,
Host: host,
}),
host: host,
}
}
// NewTestingClient lets the MockClient inject a mock BuildsClient and host.
func NewTestingClient(bc buildbucketpb.BuildsClient, host string) *Client {
return &Client{
bc: bc,
host: host,
}
}
// GetBuild implements the BuildBucketInterface.
func (c *Client) GetBuild(ctx context.Context, buildId int64) (*buildbucketpb.Build, error) {
b, err := c.bc.GetBuild(ctx, &buildbucketpb.GetBuildRequest{
Id: buildId,
Fields: common.GetBuildFields,
})
return b, err
}
// ScheduleBuilds implements the BuildBucketInterface.
func (c *Client) ScheduleBuilds(ctx context.Context, builds []string, buildsToTags map[string]map[string]string, issue, patchset int64, gerritURL, repo, bbProject, bbBucket string) ([]*buildbucketpb.Build, error) {
requests := []*buildbucketpb.BatchRequest_Request{}
for _, b := range builds {
tagStringPairs := []*buildbucketpb.StringPair{}
tags, ok := buildsToTags[b]
if ok {
for n, v := range tags {
stringPair := &buildbucketpb.StringPair{
Key: n,
Value: v,
}
tagStringPairs = append(tagStringPairs, stringPair)
}
}
request := &buildbucketpb.BatchRequest_Request{
Request: &buildbucketpb.BatchRequest_Request_ScheduleBuild{
ScheduleBuild: &buildbucketpb.ScheduleBuildRequest{
Builder: &buildbucketpb.BuilderID{
Project: bbProject,
Bucket: bbBucket,
Builder: b,
},
GerritChanges: []*buildbucketpb.GerritChange{
{
Host: gerritURL,
Project: repo,
Change: issue,
Patchset: patchset,
},
},
Properties: &structpb.Struct{},
Tags: tagStringPairs,
Fields: common.GetBuildFields,
},
},
}
requests = append(requests, request)
}
resp, err := c.bc.Batch(ctx, &buildbucketpb.BatchRequest{
Requests: requests,
})
if err != nil {
return nil, skerr.Fmt("Could not schedule builds on buildbucket: %s", err)
}
if len(resp.Responses) != len(builds) {
return nil, skerr.Fmt("Buildbucket gave %d responses for %d builders", len(resp.Responses), len(builds))
}
respBuilds := []*buildbucketpb.Build{}
for _, r := range resp.Responses {
respBuilds = append(respBuilds, r.GetScheduleBuild())
}
return respBuilds, nil
}
// CancelBuilds implements the BuildBucketInterface.
func (c *Client) CancelBuilds(ctx context.Context, buildIDs []int64, summaryMarkdown string) ([]*buildbucketpb.Build, error) {
requests := []*buildbucketpb.BatchRequest_Request{}
for _, bID := range buildIDs {
request := &buildbucketpb.BatchRequest_Request{
Request: &buildbucketpb.BatchRequest_Request_CancelBuild{
CancelBuild: &buildbucketpb.CancelBuildRequest{
Id: bID,
SummaryMarkdown: summaryMarkdown,
},
},
}
requests = append(requests, request)
}
resp, err := c.bc.Batch(ctx, &buildbucketpb.BatchRequest{
Requests: requests,
})
if err != nil {
return nil, skerr.Wrapf(err, "Could not cancel builds on buildbucket")
}
if len(resp.Responses) != len(buildIDs) {
return nil, skerr.Fmt("Buildbucket gave %d responses for %d builders", len(resp.Responses), len(buildIDs))
}
respBuilds := []*buildbucketpb.Build{}
for _, r := range resp.Responses {
respBuilds = append(respBuilds, r.GetCancelBuild())
}
return respBuilds, nil
}
// GetBuild implements the BuildBucketInterface.
func (c *Client) Search(ctx context.Context, pred *buildbucketpb.BuildPredicate) ([]*buildbucketpb.Build, error) {
rv := []*buildbucketpb.Build{}
cursor := ""
for {
req := &buildbucketpb.SearchBuildsRequest{
Fields: common.SearchBuildsFields,
PageToken: cursor,
Predicate: pred,
}
resp, err := c.bc.SearchBuilds(ctx, req)
if err != nil {
return nil, err
}
if resp == nil {
break
}
rv = append(rv, resp.Builds...)
cursor = resp.NextPageToken
if cursor == "" {
break
}
}
return rv, nil
}
// GetTrybotsForCL implements the BuildBucketInterface.
func (c *Client) GetTrybotsForCL(ctx context.Context, issue, patchset int64, gerritUrl string, tags map[string]string) ([]*buildbucketpb.Build, error) {
pred, err := common.GetTrybotsForCLPredicate(issue, patchset, gerritUrl, tags)
if err != nil {
return nil, err
}
return c.Search(ctx, pred)
}
// Make sure Client fulfills the BuildBucketInterface interface.
var _ BuildBucketInterface = (*Client)(nil)