| // Copyright 2022 The Tint Authors. | 
 | // | 
 | // Licensed under the Apache License, Version 2.0 (the "License"); | 
 | // you may not use this file except in compliance with the License. | 
 | // You may obtain a copy of the License at | 
 | // | 
 | //     http://www.apache.org/licenses/LICENSE-2.0 | 
 | // | 
 | // Unless required by applicable law or agreed to in writing, software | 
 | // distributed under the License is distributed on an "AS IS" BASIS, | 
 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 
 | // See the License for the specific language governing permissions and | 
 | // limitations under the License. | 
 |  | 
 | // 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 | 
 | } |