go.fuchsia.dev/infra@v0.0.0-20240507153436-9b593402251b/cmd/lkg/buildbucket.go (about)

     1  // Copyright 2020 The Fuchsia Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style license that can be
     3  // found in the LICENSE file.
     4  
     5  package main
     6  
     7  import (
     8  	"context"
     9  	"errors"
    10  	"fmt"
    11  
    12  	"github.com/golang-collections/collections/set"
    13  	"go.chromium.org/luci/auth"
    14  	buildbucketpb "go.chromium.org/luci/buildbucket/proto"
    15  	"go.chromium.org/luci/grpc/prpc"
    16  	"google.golang.org/genproto/protobuf/field_mask"
    17  )
    18  
    19  // buildbucketClientWrapper provides utilities for interacting with Buildbucket.
    20  type buildbucketClientWrapper struct {
    21  	client buildbucketpb.BuildsClient
    22  }
    23  
    24  // newBuildbucketClient returns an authenticated buildbucketClientWrapper.
    25  func newBuildbucketClient(ctx context.Context, authOpts auth.Options, host string) (*buildbucketClientWrapper, error) {
    26  	authClient, err := auth.NewAuthenticator(ctx, auth.OptionalLogin, authOpts).Client()
    27  	if err != nil {
    28  		return nil, err
    29  	}
    30  	client := buildbucketpb.NewBuildsPRPCClient(&prpc.Client{
    31  		C:    authClient,
    32  		Host: host,
    33  	})
    34  	return &buildbucketClientWrapper{client: client}, nil
    35  }
    36  
    37  // GetBuilds gets the latest n successful builds for each input builder, from most to least recent.
    38  // The response is in the same order as the order of the input builders.
    39  func (c *buildbucketClientWrapper) GetBuilds(ctx context.Context, builders []*buildbucketpb.BuilderID, mask *field_mask.FieldMask, n int) ([][]*buildbucketpb.Build, error) {
    40  	// Always grab input by default.
    41  	mask.Paths = append(mask.Paths, "builds.*.input")
    42  	// Construct requests to execute in batch.
    43  	reqs := make([]*buildbucketpb.BatchRequest_Request, len(builders))
    44  	for i, builder := range builders {
    45  		reqs[i] = &buildbucketpb.BatchRequest_Request{
    46  			Request: &buildbucketpb.BatchRequest_Request_SearchBuilds{
    47  				SearchBuilds: &buildbucketpb.SearchBuildsRequest{
    48  					Predicate: &buildbucketpb.BuildPredicate{
    49  						Builder: builder,
    50  						Status:  buildbucketpb.Status_SUCCESS,
    51  					},
    52  					Fields:   mask,
    53  					PageSize: int32(n),
    54  				},
    55  			},
    56  		}
    57  	}
    58  	batchResp, err := c.client.Batch(ctx, &buildbucketpb.BatchRequest{Requests: reqs})
    59  	if err != nil {
    60  		return nil, fmt.Errorf("failed to retrieve latest %d successful builds for %s: %w", n, builders, err)
    61  	}
    62  	builds := make([][]*buildbucketpb.Build, len(builders))
    63  	for i, resp := range batchResp.Responses {
    64  		switch resp.Response.(type) {
    65  		case *buildbucketpb.BatchResponse_Response_SearchBuilds:
    66  			builds[i] = resp.Response.(*buildbucketpb.BatchResponse_Response_SearchBuilds).SearchBuilds.Builds
    67  		case *buildbucketpb.BatchResponse_Response_Error:
    68  			return nil, fmt.Errorf("got batch response error: %s", resp.GetError().String())
    69  		default:
    70  			return nil, fmt.Errorf("unexpected response type: %T", resp.Response)
    71  		}
    72  	}
    73  	return builds, nil
    74  }
    75  
    76  // accessorFunc accesses a value from a Build.
    77  type accessorFunc func(*buildbucketpb.Build) any
    78  
    79  // getLastKnownGood gets the latest common attribute from the input slices of builds.
    80  func getLastKnownGood(builds [][]*buildbucketpb.Build, gitilesRef string, f accessorFunc) (any, error) {
    81  	if len(builds) == 0 {
    82  		return nil, errors.New("input builds is of length 0")
    83  	}
    84  	// Compute common attributes to all slices but the first.
    85  	var commonAttrSet *set.Set
    86  	for _, buildSlice := range builds[1:] {
    87  		attrSet := set.New()
    88  		for _, build := range buildSlice {
    89  			buildAttr := f(build)
    90  			// Skip nil attributes.
    91  			if buildAttr != nil {
    92  				attrSet.Insert(buildAttr)
    93  			}
    94  		}
    95  		// Initialize set on first iteration.
    96  		if commonAttrSet == nil {
    97  			commonAttrSet = attrSet
    98  			// Otherwise, take the intersection between this set and the common set.
    99  		} else {
   100  			commonAttrSet = attrSet.Intersection(commonAttrSet)
   101  		}
   102  	}
   103  
   104  	// Iterate through the first slice, returning the first attribute match.
   105  	for _, build := range builds[0] {
   106  		// Skip non-matching refs.
   107  		if gitilesRef != "" && gitilesRef != build.Input.GitilesCommit.Ref {
   108  			continue
   109  		}
   110  		attr := f(build)
   111  		// For length-1 inputs, the set is nil, so return the first build.
   112  		if commonAttrSet == nil || commonAttrSet.Has(attr) {
   113  			return attr, nil
   114  		}
   115  	}
   116  
   117  	return nil, errors.New("no common last-known-good attribute found")
   118  }