blob: 87727afb5f7a779930a434f66dd1648f4fa52035 [file] [log] [blame]
// Package buildbucket provides tools for interacting with the buildbucket API.
package buildbucket
import (
"context"
"net/http"
"github.com/google/uuid"
buildbucketpb "go.chromium.org/luci/buildbucket/proto"
"google.golang.org/grpc/metadata"
"google.golang.org/protobuf/types/known/fieldmaskpb"
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"
headerToken = "x-buildbucket-token"
)
var (
DEFAULT_SCOPES = []string{
"https://www.googleapis.com/auth/userinfo.email",
"https://www.googleapis.com/auth/userinfo.profile",
}
)
type BuildBucketInterface interface {
// CancelBuild cancels the specified build with the specified
// SummaryMarkdown.
CancelBuild(ctx context.Context, buildID int64, summaryMarkdown 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)
// GetBuild retrieves the build with the given ID.
GetBuild(ctx context.Context, buildId int64) (*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)
// Search retrieves Builds which match the given criteria.
Search(ctx context.Context, pred *buildbucketpb.BuildPredicate) ([]*buildbucketpb.Build, error)
// UpdateBuild sends an update for the given build.
UpdateBuild(ctx context.Context, build *buildbucketpb.Build, token string) error
// StartBuild notifies Buildbucket that the build has started. Returns the
// token which should be passed to UpdateBuild for subsequent calls.
StartBuild(ctx context.Context, buildId int64, taskId, token string) (string, 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, skerr.Wrap(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
}
// CancelBuild implements BuildbucketInterface.
func (c *Client) CancelBuild(ctx context.Context, buildID int64, summaryMarkdown string) (*buildbucketpb.Build, error) {
build, err := c.bc.CancelBuild(ctx, &buildbucketpb.CancelBuildRequest{
Id: buildID,
SummaryMarkdown: summaryMarkdown,
})
if err != nil {
return nil, skerr.Wrap(err)
}
return build, 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, skerr.Wrap(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, skerr.Wrap(err)
}
return c.Search(ctx, pred)
}
func contextWithTokenMetadata(ctx context.Context, token string) context.Context {
return metadata.NewOutgoingContext(ctx, map[string][]string{
headerToken: {token},
})
}
// StartBuild implements BuildbucketInterface.
func (c *Client) StartBuild(ctx context.Context, buildId int64, taskId, token string) (string, error) {
resp, err := c.bc.StartBuild(contextWithTokenMetadata(ctx, token), &buildbucketpb.StartBuildRequest{
RequestId: uuid.New().String(),
BuildId: buildId,
TaskId: taskId,
})
if err != nil {
return "", skerr.Wrap(err)
}
if resp.UpdateBuildToken == "" {
return "", skerr.Fmt("StartBuild returned an empty UpdateBuildToken")
}
return resp.UpdateBuildToken, nil
}
// UpdateBuild implements BuildbucketInterface.
func (c *Client) UpdateBuild(ctx context.Context, build *buildbucketpb.Build, token string) error {
var updatePaths []string
if build.Output != nil {
if build.Output.Properties != nil {
updatePaths = append(updatePaths, "build.output.properties")
}
if build.Output.GitilesCommit != nil {
updatePaths = append(updatePaths, "build.output.gitiles_commit")
}
if build.Output.Status != buildbucketpb.Status_STATUS_UNSPECIFIED {
updatePaths = append(updatePaths, "build.output.status")
}
if build.Output.StatusDetails != nil {
updatePaths = append(updatePaths, "build.output.status_details")
}
if build.Output.SummaryMarkdown != "" {
updatePaths = append(updatePaths, "build.output.summary_markdown")
}
}
if build.Status != buildbucketpb.Status_STATUS_UNSPECIFIED {
updatePaths = append(updatePaths, "build.status")
}
if build.StatusDetails != nil {
updatePaths = append(updatePaths, "build.status_details")
}
if len(build.Steps) > 0 {
updatePaths = append(updatePaths, "build.steps")
}
if build.SummaryMarkdown != "" {
updatePaths = append(updatePaths, "build.summary_markdown")
}
if len(build.Tags) > 0 {
updatePaths = append(updatePaths, "build.tags")
}
if build.Infra != nil && build.Infra.Buildbucket != nil && build.Infra.Buildbucket.Agent != nil {
if build.Infra.Buildbucket.Agent.Output != nil {
updatePaths = append(updatePaths, "build.infra.buildbucket.agent.output")
}
if build.Infra.Buildbucket.Agent.Purposes != nil {
updatePaths = append(updatePaths, "build.infra.buildbucket.agent.purposes")
}
}
_, err := c.bc.UpdateBuild(contextWithTokenMetadata(ctx, token), &buildbucketpb.UpdateBuildRequest{
Build: build,
UpdateMask: &fieldmaskpb.FieldMask{
Paths: updatePaths,
},
})
return skerr.Wrap(err)
}
// Make sure Client fulfills the BuildBucketInterface interface.
var _ BuildBucketInterface = (*Client)(nil)