blob: b2c5fd387c78133a18fd856708050d1d34ee9013 [file] [log] [blame]
// Copyright 2020 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package main
import (
"context"
"errors"
"fmt"
"github.com/golang-collections/collections/set"
"go.chromium.org/luci/auth"
buildbucketpb "go.chromium.org/luci/buildbucket/proto"
"go.chromium.org/luci/grpc/prpc"
"google.golang.org/genproto/protobuf/field_mask"
)
// buildbucketClientWrapper provides utilities for interacting with Buildbucket.
type buildbucketClientWrapper struct {
client buildbucketpb.BuildsClient
}
// newBuildbucketClient returns an authenticated buildbucketClientWrapper.
func newBuildbucketClient(ctx context.Context, authOpts auth.Options, host string) (*buildbucketClientWrapper, error) {
authClient, err := auth.NewAuthenticator(ctx, auth.OptionalLogin, authOpts).Client()
if err != nil {
return nil, err
}
client := buildbucketpb.NewBuildsPRPCClient(&prpc.Client{
C: authClient,
Host: host,
})
return &buildbucketClientWrapper{client: client}, nil
}
// GetBuilds gets the latest n successful builds for each input builder, from most to least recent.
// The response is in the same order as the order of the input builders.
func (c *buildbucketClientWrapper) GetBuilds(ctx context.Context, builders []*buildbucketpb.BuilderID, mask *field_mask.FieldMask, n int) ([][]*buildbucketpb.Build, error) {
// Always grab input by default.
mask.Paths = append(mask.Paths, "builds.*.input")
// Construct requests to execute in batch.
reqs := make([]*buildbucketpb.BatchRequest_Request, len(builders))
for i, builder := range builders {
reqs[i] = &buildbucketpb.BatchRequest_Request{
Request: &buildbucketpb.BatchRequest_Request_SearchBuilds{
SearchBuilds: &buildbucketpb.SearchBuildsRequest{
Predicate: &buildbucketpb.BuildPredicate{
Builder: builder,
Status: buildbucketpb.Status_SUCCESS,
},
Fields: mask,
PageSize: int32(n),
},
},
}
}
batchResp, err := c.client.Batch(ctx, &buildbucketpb.BatchRequest{Requests: reqs})
if err != nil {
return nil, fmt.Errorf("failed to retrieve latest %d successful builds for %s: %w", n, builders, err)
}
builds := make([][]*buildbucketpb.Build, len(builders))
for i, resp := range batchResp.Responses {
switch resp.Response.(type) {
case *buildbucketpb.BatchResponse_Response_SearchBuilds:
builds[i] = resp.Response.(*buildbucketpb.BatchResponse_Response_SearchBuilds).SearchBuilds.Builds
case *buildbucketpb.BatchResponse_Response_Error:
return nil, fmt.Errorf("got batch response error: %s", resp.GetError().String())
default:
return nil, fmt.Errorf("unexpected response type: %T", resp.Response)
}
}
return builds, nil
}
// accessorFunc accesses a value from a Build.
type accessorFunc func(*buildbucketpb.Build) any
// getLastKnownGood gets the latest common attribute from the input slices of builds.
func getLastKnownGood(builds [][]*buildbucketpb.Build, gitilesRef string, f accessorFunc) (any, error) {
if len(builds) == 0 {
return nil, errors.New("input builds is of length 0")
}
// Compute common attributes to all slices but the first.
var commonAttrSet *set.Set
for _, buildSlice := range builds[1:] {
attrSet := set.New()
for _, build := range buildSlice {
buildAttr := f(build)
// Skip nil attributes.
if buildAttr != nil {
attrSet.Insert(buildAttr)
}
}
// Initialize set on first iteration.
if commonAttrSet == nil {
commonAttrSet = attrSet
// Otherwise, take the intersection between this set and the common set.
} else {
commonAttrSet = attrSet.Intersection(commonAttrSet)
}
}
// Iterate through the first slice, returning the first attribute match.
for _, build := range builds[0] {
// Skip non-matching refs.
if gitilesRef != "" && gitilesRef != build.Input.GitilesCommit.Ref {
continue
}
attr := f(build)
// For length-1 inputs, the set is nil, so return the first build.
if commonAttrSet == nil || commonAttrSet.Has(attr) {
return attr, nil
}
}
return nil, errors.New("no common last-known-good attribute found")
}