go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/buildbucket/appengine/rpc/get_build_status.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 "fmt" 20 "strings" 21 22 "go.chromium.org/luci/common/errors" 23 "go.chromium.org/luci/gae/service/datastore" 24 "go.chromium.org/luci/grpc/appstatus" 25 26 "go.chromium.org/luci/buildbucket/appengine/common" 27 "go.chromium.org/luci/buildbucket/appengine/internal/perm" 28 "go.chromium.org/luci/buildbucket/appengine/model" 29 "go.chromium.org/luci/buildbucket/bbperms" 30 pb "go.chromium.org/luci/buildbucket/proto" 31 "go.chromium.org/luci/buildbucket/protoutil" 32 ) 33 34 func validateGetBuildStatusRequest(req *pb.GetBuildStatusRequest) error { 35 switch { 36 case req.GetId() != 0: 37 if req.Builder != nil || req.BuildNumber != 0 { 38 return errors.Reason("id is mutually exclusive with (builder + build_number)").Err() 39 } 40 case req.GetBuilder() != nil && req.BuildNumber != 0: 41 if err := protoutil.ValidateRequiredBuilderID(req.Builder); err != nil { 42 return errors.Annotate(err, "builder").Err() 43 } 44 default: 45 return errors.Reason("either id or (builder + build_number) is required").Err() 46 } 47 return nil 48 } 49 50 // GetBuildStatus handles a request to retrieve a build's status. Implements pb.BuildsServer. 51 func (*Builds) GetBuildStatus(ctx context.Context, req *pb.GetBuildStatusRequest) (*pb.Build, error) { 52 if err := validateGetBuildStatusRequest(req); err != nil { 53 return nil, appstatus.BadRequest(err) 54 } 55 56 var bs *model.BuildStatus 57 var err error 58 bldr := req.Builder 59 if req.GetId() != 0 { 60 bs = &model.BuildStatus{Build: datastore.MakeKey(ctx, model.BuildKind, req.Id)} 61 err = datastore.Get(ctx, bs) 62 } else { 63 // Get BuildStatus by builder + build_number. 64 bldAddr := fmt.Sprintf("%s/%s/%s/%d", bldr.Project, bldr.Bucket, bldr.Builder, req.BuildNumber) 65 q := datastore.NewQuery(model.BuildStatusKind).Eq("build_address", bldAddr) 66 err = datastore.Run(ctx, q, func(bse *model.BuildStatus) error { 67 bs = bse 68 return nil 69 }) 70 } 71 if err != nil && err != datastore.ErrNoSuchEntity { 72 return nil, err 73 } 74 75 var buildStatus pb.Status 76 if bs != nil && bs.Status != pb.Status_STATUS_UNSPECIFIED { 77 buildStatus = bs.Status 78 } else { 79 // BuildStatus not found, fall back to Build. 80 bID := req.Id 81 if bID == 0 { 82 bID, err = getBuildIDByBuildNumber(ctx, req.Builder, req.BuildNumber) 83 if err != nil { 84 return nil, err 85 } 86 } 87 bld, err := common.GetBuild(ctx, bID) 88 if err != nil { 89 return nil, err 90 } 91 buildStatus = bld.Proto.Status 92 bldr = bld.Proto.Builder 93 } 94 95 // Check user permission on the builder. 96 if bldr == nil { 97 parts := strings.Split(bs.BuildAddress, "/") 98 if len(parts) != 4 { 99 return nil, errors.Reason("failed to parse build_address of build %d", req.Id).Err() 100 } 101 bldr = &pb.BuilderID{ 102 Project: parts[0], 103 Bucket: parts[1], 104 Builder: parts[2], 105 } 106 } 107 // User needs BuildsGet or BuildsGetLimited permission to call this endpoint. 108 _, err = perm.GetFirstAvailablePerm(ctx, bldr, bbperms.BuildsGet, bbperms.BuildsGetLimited) 109 if err != nil { 110 return nil, err 111 } 112 113 return &pb.Build{ 114 Id: req.Id, 115 Builder: req.Builder, 116 Number: req.BuildNumber, 117 Status: buildStatus, 118 }, nil 119 }