| // Copyright 2022 The Dawn & Tint Authors | 
 | // | 
 | // Redistribution and use in source and binary forms, with or without | 
 | // modification, are permitted provided that the following conditions are met: | 
 | // | 
 | // 1. Redistributions of source code must retain the above copyright notice, this | 
 | //    list of conditions and the following disclaimer. | 
 | // | 
 | // 2. Redistributions in binary form must reproduce the above copyright notice, | 
 | //    this list of conditions and the following disclaimer in the documentation | 
 | //    and/or other materials provided with the distribution. | 
 | // | 
 | // 3. Neither the name of the copyright holder nor the names of its | 
 | //    contributors may be used to endorse or promote products derived from | 
 | //    this software without specific prior written permission. | 
 | // | 
 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" | 
 | // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE | 
 | // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | 
 | // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE | 
 | // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL | 
 | // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR | 
 | // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER | 
 | // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, | 
 | // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | 
 | // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | 
 |  | 
 | // Package buildbucket provides helpers for interfacing with build-bucket | 
 | package buildbucket | 
 |  | 
 | import ( | 
 | 	"context" | 
 | 	"fmt" | 
 | 	"log" | 
 | 	"net/url" | 
 |  | 
 | 	"dawn.googlesource.com/dawn/tools/src/gerrit" | 
 | 	"dawn.googlesource.com/dawn/tools/src/utils" | 
 | 	"go.chromium.org/luci/auth" | 
 | 	bbpb "go.chromium.org/luci/buildbucket/proto" | 
 | 	"go.chromium.org/luci/grpc/prpc" | 
 | 	"go.chromium.org/luci/hardcoded/chromeinfra" | 
 | ) | 
 |  | 
 | // Buildbucket is the client to communicate with Buildbucket. | 
 | type Buildbucket struct { | 
 | 	client bbpb.BuildsClient | 
 | } | 
 |  | 
 | // Builder describes a buildbucket builder | 
 | type Builder struct { | 
 | 	Project string | 
 | 	Bucket  string | 
 | 	Builder string | 
 | } | 
 |  | 
 | // BuildID is a unique identifier of a build | 
 | type BuildID int64 | 
 |  | 
 | // Build describes a buildbucket build | 
 | type Build struct { | 
 | 	ID      BuildID | 
 | 	Status  BuildStatus | 
 | 	Builder Builder | 
 | } | 
 |  | 
 | // BuildStatus is the status of a build | 
 | type BuildStatus string | 
 |  | 
 | // Enumerator values for BuildStatus | 
 | const ( | 
 | 	// Unspecified state. Meaning depends on the context. | 
 | 	StatusUnknown BuildStatus = "unknown" | 
 | 	// Build was scheduled, but did not start or end yet. | 
 | 	StatusScheduled BuildStatus = "scheduled" | 
 | 	// Build/step has started. | 
 | 	StatusStarted BuildStatus = "started" | 
 | 	// A build/step ended successfully. | 
 | 	// This is a terminal status. It may not transition to another status. | 
 | 	StatusSuccess BuildStatus = "success" | 
 | 	// A build/step ended unsuccessfully due to its Build.Input, | 
 | 	// e.g. tests failed, and NOT due to a build infrastructure failure. | 
 | 	// This is a terminal status. It may not transition to another status. | 
 | 	StatusFailure BuildStatus = "failure" | 
 | 	// A build/step ended unsuccessfully due to a failure independent of the | 
 | 	// input, e.g. swarming failed, not enough capacity or the recipe was unable | 
 | 	// to read the patch from gerrit. | 
 | 	// start_time is not required for this status. | 
 | 	// This is a terminal status. It may not transition to another status. | 
 | 	StatusInfraFailure BuildStatus = "infra-failure" | 
 | 	// A build was cancelled explicitly, e.g. via an RPC. | 
 | 	// This is a terminal status. It may not transition to another status. | 
 | 	StatusCanceled BuildStatus = "canceled" | 
 | ) | 
 |  | 
 | // Running returns true if the build is still running | 
 | func (s BuildStatus) Running() bool { | 
 | 	switch s { | 
 | 	case StatusScheduled, StatusStarted: | 
 | 		return true | 
 | 	default: | 
 | 		return false | 
 | 	} | 
 | } | 
 |  | 
 | // pb returns a protobuf BuilderID constructed from the Builder | 
 | func (b Builder) pb() *bbpb.BuilderID { | 
 | 	return &bbpb.BuilderID{ | 
 | 		Project: b.Project, | 
 | 		Bucket:  b.Bucket, | 
 | 		Builder: b.Builder, | 
 | 	} | 
 | } | 
 |  | 
 | // toBuilder returns a Builder constructed from the protobuf BuilderID | 
 | func toBuilder(b *bbpb.BuilderID) Builder { | 
 | 	return Builder{ | 
 | 		Project: b.Project, | 
 | 		Bucket:  b.Bucket, | 
 | 		Builder: b.Builder, | 
 | 	} | 
 | } | 
 |  | 
 | // gerritChange returns the protobuf GerritChange from a gerrit.Patchset | 
 | func gerritChange(ps gerrit.Patchset) *bbpb.GerritChange { | 
 | 	host := ps.Host | 
 | 	if u, err := url.Parse(ps.Host); err == nil && u.Host != "" { | 
 | 		host = u.Host // Strip scheme from URL | 
 | 	} | 
 | 	return &bbpb.GerritChange{ | 
 | 		Host:     host, | 
 | 		Project:  ps.Project, | 
 | 		Change:   int64(ps.Change), | 
 | 		Patchset: int64(ps.Patchset), | 
 | 	} | 
 | } | 
 |  | 
 | // toBuildStatus returns a BuildStatus from a protobuf Status | 
 | func toBuildStatus(s bbpb.Status) BuildStatus { | 
 | 	switch s { | 
 | 	default: | 
 | 		return StatusUnknown | 
 | 	case bbpb.Status_SCHEDULED: | 
 | 		return StatusScheduled | 
 | 	case bbpb.Status_STARTED: | 
 | 		return StatusStarted | 
 | 	case bbpb.Status_SUCCESS: | 
 | 		return StatusSuccess | 
 | 	case bbpb.Status_FAILURE: | 
 | 		return StatusFailure | 
 | 	case bbpb.Status_INFRA_FAILURE: | 
 | 		return StatusInfraFailure | 
 | 	case bbpb.Status_CANCELED: | 
 | 		return StatusCanceled | 
 | 	} | 
 | } | 
 |  | 
 | // toBuild returns a Build from a protobuf Build | 
 | func toBuild(b *bbpb.Build) Build { | 
 | 	return Build{BuildID(b.Id), toBuildStatus(b.Status), toBuilder(b.Builder)} | 
 | } | 
 |  | 
 | // New creates a client to communicate with Buildbucket. | 
 | func New(ctx context.Context, credentials auth.Options) (*Buildbucket, error) { | 
 | 	http, err := auth.NewAuthenticator(ctx, auth.InteractiveLogin, credentials).Client() | 
 | 	if err != nil { | 
 | 		return nil, err | 
 | 	} | 
 | 	client, err := bbpb.NewBuildsClient( | 
 | 		&prpc.Client{ | 
 | 			C:       http, | 
 | 			Host:    chromeinfra.BuildbucketHost, | 
 | 			Options: prpc.DefaultOptions(), | 
 | 		}), nil | 
 | 	if err != nil { | 
 | 		return nil, err | 
 | 	} | 
 |  | 
 | 	return &Buildbucket{client}, nil | 
 | } | 
 |  | 
 | // SearchBuilds queries the list of builds performed for the given gerrit change. | 
 | func (r *Buildbucket) SearchBuilds(ctx context.Context, ps gerrit.Patchset, f func(Build) error) error { | 
 | 	pageToken := "" | 
 | 	for { | 
 | 		rsp, err := r.client.SearchBuilds(ctx, &bbpb.SearchBuildsRequest{ | 
 | 			Predicate: &bbpb.BuildPredicate{ | 
 | 				GerritChanges: []*bbpb.GerritChange{gerritChange(ps)}, | 
 | 			}, | 
 | 			PageSize:  1000, // Maximum page size. | 
 | 			PageToken: pageToken, | 
 | 		}) | 
 | 		if err != nil { | 
 | 			return err | 
 | 		} | 
 |  | 
 | 		for _, res := range rsp.Builds { | 
 | 			if err := f(toBuild(res)); err != nil { | 
 | 				return err | 
 | 			} | 
 | 		} | 
 |  | 
 | 		pageToken = rsp.GetNextPageToken() | 
 | 		if pageToken == "" { | 
 | 			// No more test variants with unexpected result. | 
 | 			break | 
 | 		} | 
 | 	} | 
 |  | 
 | 	return nil | 
 | } | 
 |  | 
 | // StartBuild starts a build. | 
 | func (r *Buildbucket) StartBuild( | 
 | 	ctx context.Context, | 
 | 	ps gerrit.Patchset, | 
 | 	builder Builder, | 
 | 	parentSwarmingRunID string, | 
 | 	forceBuild bool) (Build, error) { | 
 |  | 
 | 	id := "" | 
 | 	if !forceBuild { | 
 | 		id = utils.Hash(ps, builder) | 
 | 	} | 
 |  | 
 | 	req := &bbpb.ScheduleBuildRequest{ | 
 | 		RequestId:     id, | 
 | 		Builder:       builder.pb(), | 
 | 		GerritChanges: []*bbpb.GerritChange{gerritChange(ps)}, | 
 | 	} | 
 | 	if parentSwarmingRunID != "" { | 
 | 		req.Swarming = &bbpb.ScheduleBuildRequest_Swarming{ | 
 | 			ParentRunId: parentSwarmingRunID, | 
 | 		} | 
 | 	} | 
 |  | 
 | 	build, err := r.client.ScheduleBuild(ctx, req) | 
 | 	if err != nil { | 
 | 		return Build{}, fmt.Errorf("failed to start build for patchset %+v on builder %+v: %w", ps, builder, err) | 
 | 	} | 
 |  | 
 | 	if status := toBuildStatus(build.Status); !forceBuild && !status.Running() { | 
 | 		log.Printf("ScheduleBuild() returned with %v, attempting to force a retry...\n", status) | 
 | 		return r.StartBuild(ctx, ps, builder, parentSwarmingRunID, true) | 
 | 	} | 
 |  | 
 | 	return toBuild(build), nil | 
 | } | 
 |  | 
 | // QueryBuild queries the status of a build. | 
 | func (r *Buildbucket) QueryBuild(ctx context.Context, id BuildID) (Build, error) { | 
 | 	b, err := r.client.GetBuild(ctx, &bbpb.GetBuildRequest{Id: int64(id)}) | 
 | 	if err != nil { | 
 | 		return Build{}, fmt.Errorf("failed to query build with id %v: %w", id, err) | 
 | 	} | 
 | 	return toBuild(b), nil | 
 | } |