go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/buildbucket/cmd/bbagent/build_client.go (about)

     1  // Copyright 2019 The LUCI Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package main
    16  
    17  import (
    18  	"context"
    19  	"time"
    20  
    21  	"golang.org/x/time/rate"
    22  	"google.golang.org/genproto/protobuf/field_mask"
    23  	"google.golang.org/grpc"
    24  	"google.golang.org/grpc/codes"
    25  	"google.golang.org/grpc/metadata"
    26  	"google.golang.org/protobuf/types/known/fieldmaskpb"
    27  
    28  	"go.chromium.org/luci/auth"
    29  	"go.chromium.org/luci/common/errors"
    30  	"go.chromium.org/luci/common/lhttp"
    31  	"go.chromium.org/luci/common/logging"
    32  	"go.chromium.org/luci/common/proto/reflectutil"
    33  	"go.chromium.org/luci/common/retry"
    34  	"go.chromium.org/luci/common/retry/transient"
    35  	"go.chromium.org/luci/common/sync/dispatcher"
    36  	"go.chromium.org/luci/common/sync/dispatcher/buffer"
    37  	"go.chromium.org/luci/grpc/grpcutil"
    38  	"go.chromium.org/luci/grpc/prpc"
    39  	"go.chromium.org/luci/lucictx"
    40  
    41  	"go.chromium.org/luci/buildbucket"
    42  	bbpb "go.chromium.org/luci/buildbucket/proto"
    43  )
    44  
    45  // BuildsClient is a trimmed version of `bbpb.BuildsClient` which only
    46  // contains the required RPC methods for bbagent.
    47  //
    48  // The live implementation automatically binds the "x-buildbucket-token" key with
    49  // a token where necessary.
    50  //
    51  // Note: The dummy implementation will always return an EMPTY Build message;
    52  // Make sure any code using BuildsClient can handle this scenario.
    53  type BuildsClient interface {
    54  	UpdateBuild(ctx context.Context, in *bbpb.UpdateBuildRequest, opts ...grpc.CallOption) (*bbpb.Build, error)
    55  	StartBuild(ctx context.Context, in *bbpb.StartBuildRequest, opts ...grpc.CallOption) (*bbpb.StartBuildResponse, error)
    56  }
    57  
    58  var _ BuildsClient = dummyBBClient{}
    59  
    60  var readMask = &bbpb.BuildMask{
    61  	Fields: &fieldmaskpb.FieldMask{
    62  		Paths: []string{
    63  			"id",
    64  			"status",
    65  			"cancel_time",
    66  			"start_time",
    67  			"update_time",
    68  		},
    69  	},
    70  }
    71  
    72  type dummyBBClient struct{}
    73  
    74  func (dummyBBClient) UpdateBuild(ctx context.Context, in *bbpb.UpdateBuildRequest, opts ...grpc.CallOption) (*bbpb.Build, error) {
    75  	return &bbpb.Build{}, nil
    76  }
    77  
    78  func (dummyBBClient) StartBuild(ctx context.Context, in *bbpb.StartBuildRequest, opts ...grpc.CallOption) (*bbpb.StartBuildResponse, error) {
    79  	return &bbpb.StartBuildResponse{}, nil
    80  }
    81  
    82  type liveBBClient struct {
    83  	// A BUILD token for agent to call UpdateBuild.
    84  	buildToken string
    85  	// A START_BUILD token for agent to call StartBuild.
    86  	startBuildToken string
    87  	c               bbpb.BuildsClient
    88  	retryF          retry.Factory
    89  }
    90  
    91  func retryRPC(ctx context.Context, retryF retry.Factory, funcName string, f func() error) error {
    92  	return retry.Retry(ctx, transient.Only(retryF), f, func(err error, sleepTime time.Duration) {
    93  		logging.Fields{
    94  			logging.ErrorKey: err,
    95  			"sleepTime":      sleepTime,
    96  		}.Warningf(ctx, "%s will retry in %s", funcName, sleepTime)
    97  	})
    98  }
    99  
   100  func (bb *liveBBClient) UpdateBuild(ctx context.Context, in *bbpb.UpdateBuildRequest, opts ...grpc.CallOption) (build *bbpb.Build, err error) {
   101  	if bb.buildToken == "" {
   102  		return nil, errors.New("update build token not found.")
   103  	}
   104  	ctx = metadata.NewOutgoingContext(ctx, metadata.Pairs(buildbucket.BuildbucketTokenHeader, bb.buildToken))
   105  	err = retryRPC(ctx, bb.retryF, "UpdateBuild", func() (err error) {
   106  		build, err = bb.c.UpdateBuild(ctx, in)
   107  		return grpcutil.WrapIfTransientOr(err, codes.DeadlineExceeded, codes.NotFound)
   108  	})
   109  	return
   110  }
   111  
   112  func (bb *liveBBClient) StartBuild(ctx context.Context, in *bbpb.StartBuildRequest, opts ...grpc.CallOption) (res *bbpb.StartBuildResponse, err error) {
   113  	if bb.startBuildToken == "" {
   114  		return nil, errors.New("start build token not found.")
   115  	}
   116  	ctx = metadata.NewOutgoingContext(ctx, metadata.Pairs(buildbucket.BuildbucketTokenHeader, bb.startBuildToken))
   117  	err = retryRPC(ctx, bb.retryF, "StartBuild", func() (err error) {
   118  		res, err = bb.c.StartBuild(ctx, in)
   119  		return grpcutil.WrapIfTransientOr(err, codes.DeadlineExceeded, codes.NotFound)
   120  	})
   121  	if err == nil {
   122  		bb.buildToken = res.UpdateBuildToken
   123  	}
   124  	return
   125  }
   126  
   127  // Reads the build secrets from the environment and constructs a BuildsClient
   128  // which can be used to update the build state.
   129  //
   130  // retryEnabled allows us to switch retries for this client on and off
   131  func newBuildsClient(ctx context.Context, bbagentCtx *bbpb.BuildbucketAgentContext, hostname string, retryF retry.Factory) (BuildsClient, error) {
   132  	if hostname == "" {
   133  		logging.Infof(ctx, "No buildbucket hostname set; making dummy buildbucket client.")
   134  		return dummyBBClient{}, nil
   135  	}
   136  
   137  	bc, err := newBuildsClientWithSecrets(ctx, hostname, retryF, bbagentCtx.Secrets)
   138  	if err != nil {
   139  		return nil, err
   140  	}
   141  	return bc, nil
   142  }
   143  
   144  // Reads the provided build secrets and constructs a BuildsClient
   145  // which can be used to update the build state.
   146  //
   147  // retryEnabled allows us to switch retries for this client on and off
   148  func newBuildsClientWithSecrets(ctx context.Context, hostname string, retryF retry.Factory, secrets *bbpb.BuildSecrets) (BuildsClient, error) {
   149  	if hostname == "" {
   150  		logging.Infof(ctx, "No buildbucket hostname set; making dummy buildbucket client.")
   151  		return dummyBBClient{}, nil
   152  	}
   153  
   154  	prpcClient := &prpc.Client{
   155  		Host: hostname,
   156  		Options: &prpc.Options{
   157  			Insecure:      lhttp.IsLocalHost(hostname),
   158  			PerRPCTimeout: 30 * time.Second,
   159  			Debug:         true,
   160  		},
   161  	}
   162  
   163  	// Use "system" account to call UpdateBuild RPCs.
   164  	sctx, err := lucictx.SwitchLocalAccount(ctx, "system")
   165  	if err != nil {
   166  		return nil, errors.Annotate(err, "could not switch to 'system' account in LUCI_CONTEXT").Err()
   167  	}
   168  	prpcClient.C, err = auth.NewAuthenticator(sctx, auth.SilentLogin, auth.Options{
   169  		MonitorAs: "bbagent/buildbucket",
   170  	}).Client()
   171  	if err != nil {
   172  		return nil, err
   173  	}
   174  
   175  	startBuildToken := secrets.StartBuildToken
   176  	if startBuildToken == "" {
   177  		// TODO(crbug.com/1416971): remove this.
   178  		startBuildToken = secrets.BuildToken
   179  	}
   180  
   181  	return &liveBBClient{
   182  		buildToken:      secrets.BuildToken,
   183  		startBuildToken: startBuildToken,
   184  		c:               bbpb.NewBuildsPRPCClient(prpcClient),
   185  		retryF:          retryF,
   186  	}, nil
   187  }
   188  
   189  // options for the dispatcher.Channel
   190  func channelOpts(ctx context.Context) (*dispatcher.Options, <-chan error) {
   191  	errorFn, errCh := dispatcher.ErrorFnReport(10, func(failedBatch *buffer.Batch, err error) bool {
   192  		return transient.Tag.In(err)
   193  	})
   194  	opts := &dispatcher.Options{
   195  		QPSLimit: rate.NewLimiter(rate.Every(3*time.Second), 1),
   196  		MinQPS:   rate.Every(buildbucket.MinUpdateBuildInterval),
   197  		Buffer: buffer.Options{
   198  			MaxLeases:     1,
   199  			BatchItemsMax: 1,
   200  			FullBehavior:  &buffer.DropOldestBatch{MaxLiveItems: 1},
   201  			Retry: func() retry.Iterator {
   202  				return &retry.ExponentialBackoff{
   203  					Limited: retry.Limited{
   204  						Delay:    200 * time.Millisecond, // initial delay
   205  						Retries:  -1,
   206  						MaxTotal: 5 * time.Minute,
   207  					},
   208  					Multiplier: 1.2,
   209  					MaxDelay:   30 * time.Second,
   210  				}
   211  			},
   212  		},
   213  		DropFn:  dispatcher.DropFnSummarized(ctx, rate.NewLimiter(rate.Every(10*time.Second), 1)),
   214  		ErrorFn: errorFn,
   215  	}
   216  	return opts, errCh
   217  }
   218  
   219  func mkSendFn(ctx context.Context, client BuildsClient, bID int64, canceledBuildCh *closeOnceCh) dispatcher.SendFn {
   220  	return func(b *buffer.Batch) error {
   221  		var req *bbpb.UpdateBuildRequest
   222  
   223  		// Nil batch. Synthesize a UpdateBuild request.
   224  		if b == nil {
   225  			req = &bbpb.UpdateBuildRequest{
   226  				Build: &bbpb.Build{
   227  					Id: bID,
   228  				},
   229  				Mask: readMask,
   230  			}
   231  		} else if b.Meta != nil {
   232  			req = b.Meta.(*bbpb.UpdateBuildRequest)
   233  		} else {
   234  			build := b.Data[0].Item.(*bbpb.Build)
   235  			adaptedTags := buildbucket.WithoutDisallowedTagKeys(build.Tags)
   236  			if len(build.Tags) != len(adaptedTags) {
   237  				build = reflectutil.ShallowCopy(build).(*bbpb.Build)
   238  				build.Tags = adaptedTags
   239  			}
   240  			req = &bbpb.UpdateBuildRequest{
   241  				Build: build,
   242  				UpdateMask: &field_mask.FieldMask{
   243  					Paths: []string{
   244  						"build.steps",
   245  						"build.output",
   246  						"build.summary_markdown",
   247  					},
   248  				},
   249  				Mask: readMask,
   250  			}
   251  			if len(build.Tags) > 0 {
   252  				req.UpdateMask.Paths = append(req.UpdateMask.Paths, "build.tags")
   253  			}
   254  			b.Meta = req
   255  			b.Data[0].Item = nil
   256  		}
   257  
   258  		updatedBuild, err := client.UpdateBuild(ctx, req)
   259  		if err != nil {
   260  			return err
   261  		}
   262  		if updatedBuild.CancelTime != nil {
   263  			logging.Infof(ctx, "The build is in the cancel process, cancel time is %s.", updatedBuild.CancelTime.AsTime().String())
   264  			canceledBuildCh.close()
   265  		}
   266  		return nil
   267  	}
   268  }
   269  
   270  // defaultRetryStrategy defines a default build client retry strategy in bbagent.
   271  func defaultRetryStrategy() retry.Iterator {
   272  	return &retry.ExponentialBackoff{
   273  		Limited: retry.Limited{
   274  			Delay:   200 * time.Millisecond,
   275  			Retries: 10,
   276  		},
   277  		MaxDelay:   80 * time.Second,
   278  		Multiplier: 2,
   279  	}
   280  }