go.fuchsia.dev/infra@v0.0.0-20240507153436-9b593402251b/cmd/size_diff/common.go (about) 1 // Copyright 2021 The Fuchsia Authors. 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 "log" 12 "strings" 13 "time" 14 15 "github.com/maruel/subcommands" 16 "go.chromium.org/luci/auth" 17 "go.chromium.org/luci/auth/client/authcli" 18 buildbucketpb "go.chromium.org/luci/buildbucket/proto" 19 "go.chromium.org/luci/common/proto/structmask" 20 "go.chromium.org/luci/common/retry" 21 "go.chromium.org/luci/common/retry/transient" 22 "go.chromium.org/luci/grpc/prpc" 23 "go.fuchsia.dev/infra/buildbucket" 24 "google.golang.org/protobuf/types/known/fieldmaskpb" 25 ) 26 27 // buildFieldMask identifies required fields and output properties that should 28 // be requested from the Buildbucket API. 29 func buildFieldMask(outputProps []string) *buildbucketpb.BuildMask { 30 return &buildbucketpb.BuildMask{ 31 Fields: &fieldmaskpb.FieldMask{Paths: []string{ 32 "id", 33 "status", 34 }}, 35 OutputProperties: []*structmask.StructMask{{Path: outputProps}}, 36 } 37 } 38 39 type commonFlags struct { 40 subcommands.CommandRunBase 41 authFlags authcli.Flags 42 43 parsedAuthOpts auth.Options 44 bbHost string 45 gitilesRemote string 46 baseCommit string 47 builder buildbucket.BuilderIDFlag 48 jsonOutput string 49 } 50 51 func (c *commonFlags) Init(authOpts auth.Options) { 52 c.authFlags = authcli.Flags{} 53 c.authFlags.Register(&c.Flags, authOpts) 54 c.Flags.StringVar(&c.bbHost, "bb-host", "cr-buildbucket.appspot.com", "Buildbucket host to use.") 55 c.Flags.StringVar(&c.gitilesRemote, "gitiles-remote", "", "Gitiles remote for base commit.") 56 c.Flags.StringVar(&c.baseCommit, "base-commit", "", "Base commit as sha1.") 57 c.Flags.Var(&c.builder, "builder", "Fully-qualified Buildbucket CI builder name to inspect.") 58 c.Flags.StringVar(&c.jsonOutput, "json-output", "-", "Path to output results to.") 59 } 60 61 func (c *commonFlags) Parse() error { 62 var err error 63 c.parsedAuthOpts, err = c.authFlags.Options() 64 if err != nil { 65 return err 66 } 67 if c.gitilesRemote == "" { 68 return errors.New("-gitiles-remote is required") 69 } 70 c.gitilesRemote = strings.TrimPrefix(c.gitilesRemote, "https://") 71 if c.baseCommit == "" { 72 return errors.New("-base-commit is required") 73 } 74 if &c.builder == nil { 75 return errors.New("-builder is required") 76 } 77 if c.jsonOutput == "" { 78 return errors.New("-json-output is required") 79 } 80 return nil 81 } 82 83 // searchForBuild gets the CI build with gitiles commit matching the base 84 // commit. 85 func searchForBuild(ctx context.Context, buildsClient buildbucketpb.BuildsClient, builder *buildbucketpb.BuilderID, gitilesRemote, baseCommit string, outputProps []string) (*buildbucketpb.Build, error) { 86 fullBuilderName := fmt.Sprintf("%s/%s/%s", builder.Project, builder.Bucket, builder.Builder) 87 resp, err := buildsClient.SearchBuilds(ctx, &buildbucketpb.SearchBuildsRequest{ 88 Predicate: &buildbucketpb.BuildPredicate{ 89 Builder: builder, 90 Tags: []*buildbucketpb.StringPair{ 91 { 92 Key: "buildset", 93 Value: fmt.Sprintf("commit/gitiles/%s/+/%s", gitilesRemote, baseCommit), 94 }, 95 }, 96 }, 97 Mask: buildFieldMask(outputProps), 98 PageSize: int32(1), 99 }) 100 if err != nil { 101 return nil, fmt.Errorf("failed to query builds for %s: %w", fullBuilderName, err) 102 } 103 builds := resp.Builds 104 if len(builds) == 0 { 105 return nil, fmt.Errorf("no builds found for %s", fullBuilderName) 106 } 107 if len(builds) != 1 { 108 return nil, fmt.Errorf("expected exactly one build, got %d", len(builds)) 109 } 110 return builds[0], nil 111 } 112 113 // waitForBuildCompletion waits for the given build to complete, as CI could be 114 // on the cusp of completion. 115 func waitForBuildCompletion(ctx context.Context, buildsClient buildbucketpb.BuildsClient, buildID int64, bbHost string, outputProps []string) (*buildbucketpb.Build, error) { 116 buildLink := fmt.Sprintf("https://%s/build/%d", bbHost, buildID) 117 retryPolicy := transient.Only(func() retry.Iterator { 118 return &retry.ExponentialBackoff{ 119 Limited: retry.Limited{ 120 Delay: time.Minute, 121 Retries: 20, 122 }, 123 Multiplier: 1, 124 } 125 }) 126 var build *buildbucketpb.Build 127 if err := retry.Retry(ctx, retryPolicy, func() error { 128 var err error 129 build, err = buildsClient.GetBuild(ctx, &buildbucketpb.GetBuildRequest{ 130 Id: buildID, 131 Mask: buildFieldMask(outputProps), 132 }) 133 if err != nil { 134 return fmt.Errorf("failed to get build %d: %w", buildID, err) 135 } 136 if !hasEnded(build) { 137 err := buildNotSuccessfulError{ 138 msg: fmt.Sprintf("a finished build is needed to attempt the size diff but got status %s, see %s", build.Status, buildLink), 139 status: build.Status, 140 } 141 log.Printf("%s is still status %s", buildLink, build.Status) 142 return transient.Tag.Apply(err) 143 } 144 return nil 145 }, nil); err != nil { 146 return nil, err 147 } 148 return build, nil 149 } 150 151 // hasEnded returns whether a build has completed. 152 func hasEnded(build *buildbucketpb.Build) bool { 153 bitmasked := build.Status & buildbucketpb.Status_ENDED_MASK 154 return bitmasked > 0 155 } 156 157 func getBuild(ctx context.Context, c commonFlags, outputProps []string) (*buildbucketpb.Build, error) { 158 authClient, err := auth.NewAuthenticator(ctx, auth.OptionalLogin, c.parsedAuthOpts).Client() 159 if err != nil { 160 return nil, fmt.Errorf("failed to initialize auth client: %w", err) 161 } 162 163 buildsClient := buildbucketpb.NewBuildsPRPCClient(&prpc.Client{ 164 C: authClient, 165 Host: c.bbHost, 166 }) 167 builder, ok := c.builder.Get().(*buildbucketpb.BuilderID) 168 if !ok { 169 return nil, errors.New("builder input is not of the form project/bucket/builder") 170 } 171 172 build, err := searchForBuild(ctx, buildsClient, builder, c.gitilesRemote, c.baseCommit, outputProps) 173 if err != nil { 174 return nil, err 175 } 176 177 if hasEnded(build) { 178 // The build has already finished, exit early to avoid an extra RPC 179 // request. 180 return build, err 181 } 182 return waitForBuildCompletion(ctx, buildsClient, build.Id, c.bbHost, outputProps) 183 }