go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/buildbucket/appengine/internal/resultdb/resultdb.go (about)

     1  // Copyright 2021 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 resultdb
    16  
    17  import (
    18  	"context"
    19  	"crypto/sha256"
    20  	"encoding/hex"
    21  	"fmt"
    22  	"net/http"
    23  
    24  	"go.chromium.org/luci/common/clock"
    25  	"go.chromium.org/luci/common/errors"
    26  	"go.chromium.org/luci/common/logging"
    27  	"go.chromium.org/luci/common/retry/transient"
    28  	"go.chromium.org/luci/common/sync/parallel"
    29  	"go.chromium.org/luci/gae/service/datastore"
    30  	"go.chromium.org/luci/gae/service/info"
    31  	"go.chromium.org/luci/grpc/grpcutil"
    32  	"go.chromium.org/luci/grpc/prpc"
    33  	rdbPb "go.chromium.org/luci/resultdb/proto/v1"
    34  	"go.chromium.org/luci/server/auth"
    35  	"go.chromium.org/luci/server/tq"
    36  	"google.golang.org/grpc"
    37  	"google.golang.org/grpc/codes"
    38  	"google.golang.org/grpc/metadata"
    39  	"google.golang.org/protobuf/types/known/timestamppb"
    40  
    41  	"go.chromium.org/luci/buildbucket/appengine/model"
    42  	"go.chromium.org/luci/buildbucket/protoutil"
    43  )
    44  
    45  var mockRecorderClientKey = "used in tests only for setting the mock recorder client"
    46  
    47  // SetMockRecorder set the mock resultDB recorder client for testing purpose.
    48  func SetMockRecorder(ctx context.Context, mock rdbPb.RecorderClient) context.Context {
    49  	return context.WithValue(ctx, &mockRecorderClientKey, mock)
    50  }
    51  
    52  // CreateInvocations creates resultdb invocations for each build.
    53  // build.Proto.Infra.Resultdb must not be nil.
    54  //
    55  // Note: it will mutate the value of build.Proto.Infra.Resultdb.Invocation and build.ResultDBUpdateToken.
    56  func CreateInvocations(ctx context.Context, builds []*model.Build) errors.MultiError {
    57  	bbHost := info.AppID(ctx) + ".appspot.com"
    58  	merr := make(errors.MultiError, len(builds))
    59  	if len(builds) == 0 {
    60  		return nil
    61  	}
    62  	host := builds[0].Proto.GetInfra().GetResultdb().GetHostname()
    63  	if host == "" {
    64  		return nil
    65  	}
    66  	now := clock.Now(ctx).UTC()
    67  
    68  	_ = parallel.WorkPool(64, func(ch chan<- func() error) {
    69  		for i, b := range builds {
    70  			i := i
    71  			b := b
    72  			proj := b.Proto.Builder.Project
    73  			if !b.Proto.GetInfra().GetResultdb().GetEnable() {
    74  				continue
    75  			}
    76  			realm := b.Realm()
    77  			if realm == "" {
    78  				logging.Warningf(ctx, fmt.Sprintf("the builder %q has resultDB enabled while the build %d doesn't have realm", b.Proto.Builder.Builder, b.Proto.Id))
    79  				continue
    80  			}
    81  			ch <- func() error {
    82  				// TODO(crbug/1042991): After build scheduling flow also dedups number not just the id,
    83  				// we can combine build id invocation and number invocation into a Batch.
    84  
    85  				// Use per-project credential to create invocation.
    86  				recorderClient, err := newRecorderClient(ctx, host, proj)
    87  				if err != nil {
    88  					merr[i] = errors.Annotate(err, "failed to create resultDB recorder client for project: %s", proj).Err()
    89  					return nil
    90  				}
    91  
    92  				// Make a call to create build id invocation.
    93  				invID := fmt.Sprintf("build-%d", b.Proto.Id)
    94  				deadline := now.Add(b.Proto.ExecutionTimeout.AsDuration()).Add(b.Proto.SchedulingTimeout.AsDuration())
    95  				reqForBldID := &rdbPb.CreateInvocationRequest{
    96  					InvocationId: invID,
    97  					Invocation: &rdbPb.Invocation{
    98  						BigqueryExports:  b.Proto.GetInfra().GetResultdb().GetBqExports(),
    99  						Deadline:         timestamppb.New(deadline),
   100  						ProducerResource: fmt.Sprintf("//%s/builds/%d", bbHost, b.Proto.Id),
   101  						Realm:            realm,
   102  					},
   103  					RequestId: invID,
   104  				}
   105  				header := metadata.MD{}
   106  				if _, err = recorderClient.CreateInvocation(ctx, reqForBldID, grpc.Header(&header)); err != nil {
   107  					merr[i] = errors.Annotate(err, "failed to create the invocation for build id: %d", b.Proto.Id).Err()
   108  					return nil
   109  				}
   110  				token, ok := header["update-token"]
   111  				if !ok {
   112  					merr[i] = errors.Reason("CreateInvocation response doesn't have update-token header for build id: %d", b.Proto.Id).Err()
   113  					return nil
   114  				}
   115  				b.ResultDBUpdateToken = token[0]
   116  				b.Proto.Infra.Resultdb.Invocation = fmt.Sprintf("invocations/%s", reqForBldID.InvocationId)
   117  
   118  				// Create another invocation for the build number in which it includes the invocation for build id,
   119  				// If the build has the Number field populated.
   120  				if b.Proto.Number > 0 {
   121  					sha256Builder := sha256.Sum256([]byte(protoutil.FormatBuilderID(b.Proto.Builder)))
   122  					_, err = recorderClient.CreateInvocation(ctx, &rdbPb.CreateInvocationRequest{
   123  						InvocationId: fmt.Sprintf("build-%s-%d", hex.EncodeToString(sha256Builder[:]), b.Proto.Number),
   124  						Invocation: &rdbPb.Invocation{
   125  							State:               rdbPb.Invocation_FINALIZING,
   126  							ProducerResource:    reqForBldID.Invocation.ProducerResource,
   127  							Realm:               realm,
   128  							IncludedInvocations: []string{fmt.Sprintf("invocations/%s", reqForBldID.InvocationId)},
   129  						},
   130  						RequestId: fmt.Sprintf("build-%d-%d", b.Proto.Id, b.Proto.Number),
   131  					})
   132  					if err != nil {
   133  						merr[i] = errors.Annotate(err, "failed to create the invocation for build number: %d (build id: %d)", b.Proto.Number, b.Proto.Id).Err()
   134  						return nil
   135  					}
   136  				}
   137  				return nil
   138  			}
   139  		}
   140  	})
   141  
   142  	if merr.First() != nil {
   143  		return merr
   144  	}
   145  	return nil
   146  }
   147  
   148  // FinalizeInvocation calls ResultDB to finalize the build's invocation.
   149  func FinalizeInvocation(ctx context.Context, buildID int64) error {
   150  	b := &model.Build{ID: buildID}
   151  	infra := &model.BuildInfra{
   152  		Build: datastore.KeyForObj(ctx, b),
   153  	}
   154  	switch err := datastore.Get(ctx, b, infra); {
   155  	case errors.Contains(err, datastore.ErrNoSuchEntity):
   156  		return errors.Annotate(err, "build %d or buildInfra not found", buildID).Tag(tq.Fatal).Err()
   157  	case err != nil:
   158  		return errors.Annotate(err, "failed to fetch build %d or buildInfra", buildID).Tag(transient.Tag).Err()
   159  	}
   160  	rdb := infra.Proto.Resultdb
   161  	if rdb.Hostname == "" || rdb.Invocation == "" {
   162  		// If there's no hostname or no invocation, it means resultdb integration
   163  		// is not enabled for this build.
   164  		return nil
   165  	}
   166  
   167  	recorderClient, err := newRecorderClient(ctx, rdb.Hostname, b.Project)
   168  	if err != nil {
   169  		return errors.Annotate(err, "failed to create a recorder client for build %d", buildID).Tag(tq.Fatal).Err()
   170  	}
   171  
   172  	ctx = metadata.AppendToOutgoingContext(ctx, "update-token", b.ResultDBUpdateToken)
   173  	if _, err := recorderClient.FinalizeInvocation(ctx, &rdbPb.FinalizeInvocationRequest{Name: rdb.Invocation}); err != nil {
   174  		code := grpcutil.Code(err)
   175  		if code == codes.FailedPrecondition || code == codes.PermissionDenied {
   176  			return errors.Annotate(err, "Fatal rpc error when finalizing %s for build %d", rdb.Invocation, buildID).Tag(tq.Fatal).Err()
   177  		} else {
   178  			// Retry other errors.
   179  			return transient.Tag.Apply(err)
   180  		}
   181  	}
   182  	return nil
   183  }
   184  
   185  func newRecorderClient(ctx context.Context, host string, project string) (rdbPb.RecorderClient, error) {
   186  	if mockClient, ok := ctx.Value(&mockRecorderClientKey).(*rdbPb.MockRecorderClient); ok {
   187  		return mockClient, nil
   188  	}
   189  
   190  	t, err := auth.GetRPCTransport(ctx, auth.AsProject, auth.WithProject(project))
   191  	if err != nil {
   192  		return nil, err
   193  	}
   194  	return rdbPb.NewRecorderPRPCClient(
   195  		&prpc.Client{
   196  			C:    &http.Client{Transport: t},
   197  			Host: host,
   198  		}), nil
   199  }