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 }