blob: d2f67d9b7f0499c6557580954468a5fb1b1b89a2 [file] [log] [blame]
// Package buildbucket provides tools for interacting with the buildbucket API.
package buildbucket
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"go.skia.org/infra/go/jsonutils"
"go.skia.org/infra/go/util"
)
const (
apiUrl = "https://cr-buildbucket.appspot.com/api/buildbucket/v1"
)
var (
DEFAULT_SCOPES = []string{
"https://www.googleapis.com/auth/userinfo.email",
"https://www.googleapis.com/auth/userinfo.profile",
}
)
const (
// Possible values for the Build.Status field.
// See: https://chromium.googlesource.com/infra/luci/luci-go/+/master/common/api/buildbucket/buildbucket/v1/buildbucket-gen.go#317
STATUS_COMPLETED = "COMPLETED"
STATUS_SCHEDULED = "SCHEDULED"
STATUS_STARTED = "STARTED"
// Possible values for the Build.Result field.
// See: https://chromium.googlesource.com/infra/luci/luci-go/+/master/common/api/buildbucket/buildbucket/v1/buildbucket-gen.go#305
RESULT_CANCELED = "CANCELED"
RESULT_FAILURE = "FAILURE"
RESULT_SUCCESS = "SUCCESS"
)
type Author struct {
Email string `json:"email"`
}
type Change struct {
Author *Author `json:"author"`
Repository string `json:"repo_url"`
Revision string `json:"revision"`
}
type Error struct {
Message string `json:"message"`
Reason string `json:"reason"`
}
type Properties struct {
AttemptStartTs float64 `json:"attempt_start_ts,omitempty"`
Category string `json:"category,omitempty"`
Gerrit string `json:"patch_gerrit_url,omitempty"`
GerritIssue jsonutils.Number `json:"patch_issue,omitempty"`
GerritPatchset string `json:"patch_ref,omitempty"`
Master string `json:"master,omitempty"`
PatchProject string `json:"patch_project,omitempty"`
PatchStorage string `json:"patch_storage,omitempty"`
Reason string `json:"reason,omitempty"`
Revision string `json:"revision,omitempty"`
TryJobRepo string `json:"try_job_repo,omitempty"`
}
type Parameters struct {
BuilderName string `json:"builder_name"`
Changes []*Change `json:"changes"`
Properties Properties `json:"properties"`
Swarming *swarming `json:"swarming,omitempty"`
}
type swarming struct {
OverrideBuilderCfg swarmingOverrides `json:"override_builder_cfg"`
}
type swarmingOverrides struct {
Dimensions []string `json:"dimensions"`
}
type buildBucketRequest struct {
Bucket string `json:"bucket"`
ParametersJSON string `json:"parameters_json"`
}
type buildBucketResponse struct {
Build *Build `json:"build"`
Error *Error `json:"error"`
Kind string `json:"kind"`
Etag string `json:"etag"`
}
// Build is a struct containing information about a build in BuildBucket.
type Build struct {
Bucket string `json:"bucket"`
Completed jsonutils.Time `json:"completed_ts"`
CreatedBy string `json:"created_by"`
Created jsonutils.Time `json:"created_ts"`
FailureReason string `json:"failure_reason"`
Id string `json:"id"`
Url string `json:"url"`
ParametersJson string `json:"parameters_json"`
Parameters *Parameters `json:"-"`
Result string `json:"result"`
ResultDetailsJson string `json:"result_details_json"`
Status string `json:"status"`
StatusChanged jsonutils.Time `json:"status_changed_ts"`
Updated jsonutils.Time `json:"updated_ts"`
UtcNow jsonutils.Time `json:"utcnow_ts"`
}
// Client is used for interacting with the BuildBucket API.
type Client struct {
*http.Client
}
// NewClient returns an authenticated Client instance.
func NewClient(c *http.Client) *Client {
return &Client{c}
}
// RequestBuild adds a request for the given build. The swarmingBotId parameter
// may be the empty string, in which case the build may run on any bot.
func (c *Client) RequestBuild(builder, master, commit, repo, author, swarmingBotId string) (*Build, error) {
p := Parameters{
BuilderName: builder,
Changes: []*Change{
{
Author: &Author{
Email: author,
},
Repository: repo,
Revision: commit,
},
},
Properties: Properties{
Reason: "Triggered by SkiaScheduler",
},
}
if swarmingBotId != "" {
p.Swarming = &swarming{
OverrideBuilderCfg: swarmingOverrides{
Dimensions: []string{
fmt.Sprintf("id:%s", swarmingBotId),
},
},
}
}
jsonParams, err := json.Marshal(p)
if err != nil {
return nil, err
}
body := buildBucketRequest{
Bucket: fmt.Sprintf("master.%s", master),
ParametersJSON: string(jsonParams),
}
jsonBody, err := json.Marshal(body)
if err != nil {
return nil, err
}
url := apiUrl + "/builds"
req, err := http.NewRequest("PUT", url, bytes.NewReader(jsonBody))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json; charset=utf-8")
resp, err := c.Do(req)
if err != nil {
return nil, err
}
defer util.Close(resp.Body)
if resp.StatusCode != http.StatusOK {
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("Failed to schedule build (code %s); couldn't read response body: %v", resp.Status, err)
}
return nil, fmt.Errorf("Response code is %s. Response body:\n%s", resp.Status, string(b))
}
var res buildBucketResponse
if err := json.NewDecoder(resp.Body).Decode(&res); err != nil {
return nil, fmt.Errorf("Failed to decode response body: %v", err)
}
if res.Error != nil {
return nil, fmt.Errorf("Failed to schedule build: %s", res.Error.Message)
}
return res.Build, nil
}
// GetBuild retrieves the build with the given ID.
func (c *Client) GetBuild(buildId string) (*Build, error) {
url := apiUrl + "/builds/" + buildId + "?alt=json"
resp, err := c.Get(url)
if err != nil {
return nil, err
}
defer util.Close(resp.Body)
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("Request got response %s", resp.Status)
}
var result struct {
Build *Build `json:"build"`
}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, err
}
return result.Build, nil
}
// getOnePage retrieves one page of search results.
func (c *Client) getOnePage(url string) ([]*Build, string, error) {
resp, err := c.Get(url)
if err != nil {
return nil, "", err
}
defer util.Close(resp.Body)
if resp.StatusCode != http.StatusOK {
return nil, "", fmt.Errorf("Request got response %s", resp.Status)
}
var result struct {
Builds []*Build `json:"builds"`
NextCursor string `json:"next_cursor"`
}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, "", err
}
return result.Builds, result.NextCursor, nil
}
// Search retrieves results based on the given criteria.
func (c *Client) Search(url string) ([]*Build, error) {
rv := []*Build{}
cursor := ""
for {
newUrl := url
if cursor != "" {
newUrl += fmt.Sprintf("&start_cursor=%s", cursor)
}
var builds []*Build
var err error
builds, cursor, err = c.getOnePage(newUrl)
if err != nil {
return nil, err
}
rv = append(rv, builds...)
if cursor == "" {
break
}
}
return rv, nil
}
// GetTrybotsForCL retrieves trybot results for the given CL.
func (c *Client) GetTrybotsForCL(issueID, patchsetID int64, patchStorage, crUrl string) ([]*Build, error) {
u, err := url.Parse(crUrl)
if err != nil {
return nil, err
}
host := u.Host
q := url.Values{"tag": []string{fmt.Sprintf("buildset:patch/%s/%s/%d/%d", patchStorage, host, issueID, patchsetID)}}
url := apiUrl + "/search?" + q.Encode()
builds, err := c.Search(url)
if err != nil {
return nil, err
}
// Parse the parameters.
for _, build := range builds {
build.Parameters = &Parameters{}
if err := json.Unmarshal([]byte(build.ParametersJson), build.Parameters); err != nil {
return nil, fmt.Errorf("Unable to decode parameters in build: %s", err)
}
}
return builds, nil
}