go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/buildbucket/appengine/rpc/set_builder_health.go (about) 1 // Copyright 2023 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 rpc 16 17 import ( 18 "context" 19 "strings" 20 21 "google.golang.org/genproto/googleapis/rpc/status" 22 "google.golang.org/grpc/codes" 23 "google.golang.org/protobuf/proto" 24 "google.golang.org/protobuf/reflect/protoreflect" 25 "google.golang.org/protobuf/types/descriptorpb" 26 "google.golang.org/protobuf/types/known/emptypb" 27 "google.golang.org/protobuf/types/known/timestamppb" 28 29 "go.chromium.org/luci/common/data/stringset" 30 "go.chromium.org/luci/common/errors" 31 "go.chromium.org/luci/common/logging" 32 "go.chromium.org/luci/common/proto/protowalk" 33 "go.chromium.org/luci/gae/service/datastore" 34 "go.chromium.org/luci/grpc/appstatus" 35 "go.chromium.org/luci/server/auth" 36 37 "go.chromium.org/luci/buildbucket/appengine/internal/perm" 38 "go.chromium.org/luci/buildbucket/appengine/model" 39 "go.chromium.org/luci/buildbucket/bbperms" 40 pb "go.chromium.org/luci/buildbucket/proto" 41 ) 42 43 type SetBuilderHealthChecker struct{} 44 45 var _ protowalk.FieldProcessor = (*SetBuilderHealthChecker)(nil) 46 47 func (*SetBuilderHealthChecker) Process(field protoreflect.FieldDescriptor, msg protoreflect.Message) (data protowalk.ResultData, applied bool) { 48 return protowalk.ResultData{Message: "required", IsErr: true}, true 49 } 50 51 func init() { 52 protowalk.RegisterFieldProcessor(&SetBuilderHealthChecker{}, func(field protoreflect.FieldDescriptor) protowalk.ProcessAttr { 53 if fo := field.Options().(*descriptorpb.FieldOptions); fo != nil { 54 required := proto.GetExtension(fo, pb.E_RequiredByRpc).([]string) 55 for _, r := range required { 56 if r == "SetBuilderHealth" { 57 return protowalk.ProcessIfUnset 58 } 59 } 60 } 61 return protowalk.ProcessNever 62 }) 63 } 64 65 // createErrorResponse creates an errored response entry based on the error and code. 66 func createErrorResponse(err error, code codes.Code) *pb.SetBuilderHealthResponse_Response { 67 return &pb.SetBuilderHealthResponse_Response{ 68 Response: &pb.SetBuilderHealthResponse_Response_Error{ 69 Error: &status.Status{ 70 Code: int32(code), 71 Message: err.Error(), 72 }, 73 }, 74 } 75 } 76 77 func annotateErrorWithBuilder(err error, builder *pb.BuilderID) error { 78 return errors.Annotate(err, "Builder: %s/%s/%s", builder.Project, builder.Bucket, builder.Builder).Err() 79 } 80 81 // validateRequest validates if the given request is valid or not. It also modifies 82 // resp to add any new errors that arise from the request validation. 83 func validateRequest(ctx context.Context, req *pb.SetBuilderHealthRequest, errs map[int]error, resp []*pb.SetBuilderHealthResponse_Response) error { 84 if procRes := protowalk.Fields(req, &protowalk.RequiredProcessor{}, &SetBuilderHealthChecker{}); procRes != nil { 85 if resStrs := procRes.Strings(); len(resStrs) > 0 { 86 logging.Infof(ctx, strings.Join(resStrs, ". ")) 87 } 88 if err := procRes.Err(); err != nil { 89 return err 90 } 91 } 92 seen := stringset.New(len(req.Health)) 93 for i, msg := range req.Health { 94 fullBldrID := strings.Join([]string{msg.Id.Project, msg.Id.Bucket, msg.Id.Builder}, "/") 95 if seen.Has(fullBldrID) { 96 return errors.Reason("The following builder has multiple entries: %s", fullBldrID).Err() 97 } 98 seen.Add(fullBldrID) 99 if errs[i] == nil && (msg.Health.GetHealthScore() < 0 || msg.Health.GetHealthScore() > 10) { 100 err := annotateErrorWithBuilder(errors.Reason("HealthScore should be between 0 and 10").Err(), msg.Id) 101 errs[i] = err 102 resp[i] = createErrorResponse(err, codes.InvalidArgument) 103 } 104 } 105 return nil 106 } 107 108 // updateBuilderEntityWithHealth performs a read operation on the builder datastore model, updates 109 // the metadata, then saves the builder back to datastore. 110 func updateBuilderEntityWithHealth(ctx context.Context, bldr *pb.SetBuilderHealthRequest_BuilderHealth) error { 111 bktKey := model.BucketKey(ctx, bldr.Id.Project, bldr.Id.Bucket) 112 builder := &model.Builder{ 113 ID: bldr.Id.Builder, 114 Parent: bktKey, 115 } 116 txErr := datastore.RunInTransaction(ctx, func(ctx context.Context) error { 117 err := datastore.Get(ctx, builder) 118 if err != nil { 119 if _, isAppStatusErr := appstatus.Get(err); isAppStatusErr { 120 return err 121 } 122 return appstatus.Errorf(codes.Internal, "failed to get builder %s: %s", bldr.Id.Builder, err) 123 } 124 // If the reporter did not provide data and doc links, use the default ones from the builder config. 125 if bldr.Health.DataLinks == nil { 126 bldr.Health.DataLinks = builder.Config.GetBuilderHealthMetricsLinks().GetDataLinks() 127 } 128 if bldr.Health.DocLinks == nil { 129 bldr.Health.DocLinks = builder.Config.GetBuilderHealthMetricsLinks().GetDocLinks() 130 } 131 if bldr.Health.ContactTeamEmail == "" { 132 bldr.Health.ContactTeamEmail = builder.Config.GetContactTeamEmail() 133 } 134 bldr.Health.ReportedTime = timestamppb.Now() 135 bldr.Health.Reporter = auth.CurrentIdentity(ctx).Value() 136 if builder.Metadata != nil { 137 builder.Metadata.Health = bldr.Health 138 } else { 139 builder.Metadata = &pb.BuilderMetadata{ 140 Health: bldr.Health, 141 } 142 } 143 return datastore.Put(ctx, builder) 144 }, nil) 145 return txErr 146 } 147 148 // SetBuilderHealth implements pb.Builds.SetBuilderHealth. 149 func (*Builders) SetBuilderHealth(ctx context.Context, req *pb.SetBuilderHealthRequest) (*pb.SetBuilderHealthResponse, error) { 150 // Create and populate resp with empty protos 151 resp := &pb.SetBuilderHealthResponse{} 152 if len(req.GetHealth()) == 0 { 153 return resp, nil 154 } 155 resp.Responses = make([]*pb.SetBuilderHealthResponse_Response, len(req.Health)) 156 for i := 0; i < len(req.Health); i++ { 157 resp.Responses[i] = &pb.SetBuilderHealthResponse_Response{ 158 Response: &pb.SetBuilderHealthResponse_Response_Result{ 159 Result: &emptypb.Empty{}, 160 }, 161 } 162 } 163 // Only want to store health for builders that the requestor has permission 164 // to store health for. 165 errs := make(map[int]error, len(req.Health)) 166 for i, msg := range req.Health { 167 err := perm.HasInBuilder(ctx, bbperms.BuildersSetHealth, msg.Id) 168 if err != nil { 169 err := annotateErrorWithBuilder(err, msg.Id) 170 errs[i] = err 171 resp.Responses[i] = createErrorResponse(err, codes.PermissionDenied) 172 } 173 } 174 // Returning early if there are no builders that the user is allowed to update. 175 if len(errs) == len(req.Health) { 176 return resp, nil 177 } 178 if err := validateRequest(ctx, req, errs, resp.Responses); err != nil { 179 return nil, appstatus.Errorf(codes.InvalidArgument, "%s", err.Error()) 180 } 181 // Finally update builder health for builders that did not have a permission 182 // or validation error. 183 for i, msg := range req.Health { 184 if errs[i] == nil { 185 if err := updateBuilderEntityWithHealth(ctx, msg); err != nil { 186 resp.Responses[i] = createErrorResponse(err, codes.Internal) 187 } 188 } 189 } 190 return resp, nil 191 }