go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/milo/rpc/query_recent_builds.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 rpc 16 17 import ( 18 "context" 19 20 "go.chromium.org/luci/auth/identity" 21 "go.chromium.org/luci/buildbucket/bbperms" 22 buildbucketpb "go.chromium.org/luci/buildbucket/proto" 23 "go.chromium.org/luci/buildbucket/protoutil" 24 "go.chromium.org/luci/common/errors" 25 "go.chromium.org/luci/common/pagination" 26 "go.chromium.org/luci/common/pagination/dscursor" 27 "go.chromium.org/luci/gae/service/datastore" 28 "go.chromium.org/luci/grpc/appstatus" 29 "go.chromium.org/luci/milo/internal/model" 30 "go.chromium.org/luci/milo/internal/utils" 31 milopb "go.chromium.org/luci/milo/proto/v1" 32 "go.chromium.org/luci/server/auth" 33 "go.chromium.org/luci/server/auth/realms" 34 "google.golang.org/grpc/codes" 35 "google.golang.org/protobuf/types/known/timestamppb" 36 ) 37 38 var queryRecentBuildsPageTokenVault = dscursor.NewVault([]byte("luci.milo.v1.MiloInternal.QueryRecentBuilds")) 39 40 var queryRecentBuildsPageSize = PageSizeLimiter{ 41 Max: 100, 42 Default: 25, 43 } 44 45 // QueryRecentBuilds implements milopb.MiloInternal service 46 func (s *MiloInternalService) QueryRecentBuilds(ctx context.Context, req *milopb.QueryRecentBuildsRequest) (_ *milopb.QueryRecentBuildsResponse, err error) { 47 // Validate request. 48 err = validatesQueryRecentBuildsRequest(req) 49 if err != nil { 50 return nil, appstatus.BadRequest(err) 51 } 52 53 // Perform ACL check. 54 realm := realms.Join(req.Builder.Project, req.Builder.Bucket) 55 allowed, err := auth.HasPermission(ctx, bbperms.BuildsList, realm, nil) 56 if err != nil { 57 return nil, err 58 } 59 if !allowed { 60 if auth.CurrentIdentity(ctx) == identity.AnonymousIdentity { 61 return nil, appstatus.Error(codes.Unauthenticated, "not logged in") 62 } 63 return nil, appstatus.Error(codes.PermissionDenied, "no access to the bucket") 64 } 65 66 // Decode cursor from page token. 67 cur, err := queryRecentBuildsPageTokenVault.Cursor(ctx, req.PageToken) 68 switch err { 69 case pagination.ErrInvalidPageToken: 70 return nil, appstatus.Error(codes.InvalidArgument, "invalid page token") 71 case nil: 72 // Continue 73 default: 74 return nil, err 75 } 76 77 pageSize := int(queryRecentBuildsPageSize.Adjust(req.PageSize)) 78 79 // Construct query. 80 legacyBuilderID := utils.LegacyBuilderIDString(req.Builder) 81 q := datastore.NewQuery("BuildSummary"). 82 Eq("BuilderID", legacyBuilderID). 83 Order("-Created"). 84 Start(cur) 85 86 // Query recent builds. 87 recentBuilds := make([]*buildbucketpb.Build, 0, pageSize) 88 var nextCursor datastore.Cursor 89 err = datastore.Run(ctx, q, func(b *model.BuildSummary, getCursor datastore.CursorCB) error { 90 if !b.Summary.Status.Terminal() { 91 return nil 92 } 93 94 var buildID int64 = 0 95 _, buildNum, err := utils.ParseLegacyBuildbucketBuildID(b.BuildID) 96 if err != nil { 97 // If the BuildID is not the legacy build ID, trying parsing it as 98 // the new build ID. 99 buildID, err = utils.ParseBuildbucketBuildID(b.BuildID) 100 if err != nil { 101 return err 102 } 103 } 104 105 recentBuilds = append(recentBuilds, &buildbucketpb.Build{ 106 Id: buildID, 107 Number: buildNum, 108 Builder: req.Builder, 109 Status: b.Summary.Status.ToBuildbucket(), 110 CreateTime: timestamppb.New(b.Created), 111 }) 112 113 if len(recentBuilds) == pageSize { 114 nextCursor, err = getCursor() 115 if err != nil { 116 return err 117 } 118 119 return datastore.Stop 120 } 121 return nil 122 }) 123 if err != nil { 124 return nil, err 125 } 126 127 // Construct the next page token. 128 nextPageToken, err := queryRecentBuildsPageTokenVault.PageToken(ctx, nextCursor) 129 if err != nil { 130 return nil, err 131 } 132 133 return &milopb.QueryRecentBuildsResponse{ 134 Builds: recentBuilds, 135 NextPageToken: nextPageToken, 136 }, nil 137 } 138 139 func validatesQueryRecentBuildsRequest(req *milopb.QueryRecentBuildsRequest) error { 140 if req.PageSize < 0 { 141 return errors.Reason("page_size can not be negative").Err() 142 } 143 if err := protoutil.ValidateRequiredBuilderID(req.Builder); err != nil { 144 return errors.Annotate(err, "builder").Err() 145 } 146 return nil 147 }