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  }