blob: 6920c8e177f641017c2481f881b72436d7eedda6 [file] [log] [blame]
// Copyright 2021 The Fuchsia Authors.
// 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/maruel/subcommands"
"log"
"strings"
"time"
"go.chromium.org/luci/auth"
"go.chromium.org/luci/auth/client/authcli"
buildbucketpb "go.chromium.org/luci/buildbucket/proto"
"go.chromium.org/luci/common/retry"
"go.chromium.org/luci/common/retry/transient"
"go.chromium.org/luci/grpc/prpc"
"go.fuchsia.dev/infra/buildbucket"
"google.golang.org/genproto/protobuf/field_mask"
)
type commonFlags struct {
subcommands.CommandRunBase
authFlags authcli.Flags
parsedAuthOpts auth.Options
bbHost string
gitilesRemote string
baseCommit string
builder buildbucket.BuilderIDFlag
jsonOutput string
}
func (c *commonFlags) Init(authOpts auth.Options) {
c.authFlags = authcli.Flags{}
c.authFlags.Register(&c.Flags, authOpts)
c.Flags.StringVar(&c.bbHost, "bb-host", "cr-buildbucket.appspot.com", "Buildbucket host to use.")
c.Flags.StringVar(&c.gitilesRemote, "gitiles-remote", "", "Gitiles remote for base commit.")
c.Flags.StringVar(&c.baseCommit, "base-commit", "", "Base commit as sha1.")
c.Flags.Var(&c.builder, "builder", "Fully-qualified Buildbucket CI builder name to inspect.")
c.Flags.StringVar(&c.jsonOutput, "json-output", "-", "Path to output results to.")
}
func (c *commonFlags) Parse() error {
var err error
c.parsedAuthOpts, err = c.authFlags.Options()
if err != nil {
return err
}
if c.gitilesRemote == "" {
return errors.New("-gitiles-remote is required")
}
c.gitilesRemote = strings.TrimPrefix(c.gitilesRemote, "https://")
if c.baseCommit == "" {
return errors.New("-base-commit is required")
}
if &c.builder == nil {
return errors.New("-builder is required")
}
if c.jsonOutput == "" {
return errors.New("-json-output is required")
}
return nil
}
// searchForBuild gets the CI build with gitiles commit matching the base
// commit.
func searchForBuild(ctx context.Context, buildsClient buildbucketpb.BuildsClient, builder *buildbucketpb.BuilderID, gitilesRemote, baseCommit string, fieldMaskPaths []string) (*buildbucketpb.Build, error) {
resp, err := buildsClient.SearchBuilds(ctx, &buildbucketpb.SearchBuildsRequest{
Predicate: &buildbucketpb.BuildPredicate{
Builder: builder,
Tags: []*buildbucketpb.StringPair{
{
Key: "buildset",
Value: fmt.Sprintf("commit/gitiles/%s/+/%s", gitilesRemote, baseCommit),
},
},
},
Fields: &field_mask.FieldMask{Paths: fieldMaskPaths},
PageSize: int32(1),
})
if err != nil {
return nil, fmt.Errorf("failed to query builds for %s: %w", builder.String(), err)
}
builds := resp.Builds
if len(builds) == 0 {
return nil, fmt.Errorf("no builds found for %s", builder.String())
}
if len(builds) != 1 {
return nil, fmt.Errorf("expected exactly one build, got %d", len(builds))
}
return builds[0], nil
}
// waitForBuildCompletion waits for the given build to complete, as CI could be
// on the cusp of completion.
func waitForBuildCompletion(ctx context.Context, buildsClient buildbucketpb.BuildsClient, buildId int64, buildLink string, fieldMaskPaths []string) (*buildbucketpb.Build, error) {
retryPolicy := transient.Only(func() retry.Iterator {
return &retry.ExponentialBackoff{
Limited: retry.Limited{
Delay: time.Minute,
Retries: 20,
},
Multiplier: 1,
}
})
var build *buildbucketpb.Build
if err := retry.Retry(ctx, retryPolicy, func() error {
var err error
build, err = buildsClient.GetBuild(ctx, &buildbucketpb.GetBuildRequest{
Id: buildId,
Fields: &field_mask.FieldMask{Paths: fieldMaskPaths},
})
if err != nil {
return fmt.Errorf("failed to get build %d: %w", buildId, err)
}
if build.Status == buildbucketpb.Status_STARTED {
err := buildNotSuccessfulError{
msg: fmt.Sprintf("a finished build is needed to attempt the size diff but got status %s, see %s", build.Status, buildLink),
status: build.Status,
}
log.Printf("%s is still status %s", buildLink, build.Status)
return transient.Tag.Apply(err)
}
return nil
}, nil); err != nil {
return nil, err
}
return build, nil
}
func getBuild(ctx context.Context, c commonFlags, fieldMaskPaths1 []string, fieldMaskPaths2 []string) (*buildbucketpb.Build, error) {
authClient, err := auth.NewAuthenticator(ctx, auth.OptionalLogin, c.parsedAuthOpts).Client()
if err != nil {
return nil, fmt.Errorf("failed to initialize auth client: %w", err)
}
buildsClient := buildbucketpb.NewBuildsPRPCClient(&prpc.Client{
C: authClient,
Host: c.bbHost,
})
builder, ok := c.builder.Get().(*buildbucketpb.BuilderID)
if !ok {
return nil, errors.New("builder input is not of the form project/bucket/builder")
}
build, err := searchForBuild(ctx, buildsClient, builder, c.gitilesRemote, c.baseCommit, fieldMaskPaths1)
if err != nil {
return nil, err
}
buildLink := fmt.Sprintf("https://%s/build/%d", c.bbHost, build.Id)
if build.Status == buildbucketpb.Status_STARTED {
build, err = waitForBuildCompletion(ctx, buildsClient, build.Id, buildLink, fieldMaskPaths2)
if err != nil {
return nil, err
}
}
return build, nil
}