go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/buildbucket/appengine/rpc/list_builders.go (about) 1 // Copyright 2020 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/common/data/stringset" 21 "go.chromium.org/luci/common/errors" 22 "go.chromium.org/luci/common/pagination" 23 "go.chromium.org/luci/common/pagination/dscursor" 24 "go.chromium.org/luci/gae/service/datastore" 25 "go.chromium.org/luci/grpc/appstatus" 26 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 var listBuildersCursorVault = dscursor.NewVault([]byte("buildbucket.v2.Builders.ListBuilders")) 35 36 // validateListBuildersReq validates the given request. 37 func validateListBuildersReq(ctx context.Context, req *pb.ListBuildersRequest) error { 38 if req.Project == "" { 39 if req.Bucket != "" { 40 return errors.Reason("project must be specified when bucket is specified").Err() 41 } 42 } else { 43 err := protoutil.ValidateBuilderID(&pb.BuilderID{Project: req.Project, Bucket: req.Bucket}) 44 if err != nil { 45 return err 46 } 47 } 48 49 return validatePageSize(req.PageSize) 50 } 51 52 // ListBuilders handles a request to retrieve builders. Implements pb.BuildersServer. 53 func (*Builders) ListBuilders(ctx context.Context, req *pb.ListBuildersRequest) (*pb.ListBuildersResponse, error) { 54 if err := validateListBuildersReq(ctx, req); err != nil { 55 return nil, appstatus.BadRequest(err) 56 } 57 58 // Parse the cursor from the page token. 59 cur, err := listBuildersCursorVault.Cursor(ctx, req.PageToken) 60 switch err { 61 case pagination.ErrInvalidPageToken: 62 return nil, appstatus.BadRequest(err) 63 case nil: 64 // continue 65 default: 66 return nil, err 67 } 68 69 // ACL checks. 70 var key *datastore.Key 71 var allowedBuckets []string 72 if req.Bucket == "" { 73 if req.Project != "" { 74 key = model.ProjectKey(ctx, req.Project) 75 } 76 77 var err error 78 if allowedBuckets, err = perm.BucketsByPerm(ctx, bbperms.BuildersList, req.Project); err != nil { 79 return nil, err 80 } 81 } else { 82 key = model.BucketKey(ctx, req.Project, req.Bucket) 83 84 if err := perm.HasInBucket(ctx, bbperms.BuildersList, req.Project, req.Bucket); err != nil { 85 return nil, err 86 } 87 allowedBuckets = []string{protoutil.FormatBucketID(req.Project, req.Bucket)} 88 } 89 90 // Fetch the builders. 91 q := datastore.NewQuery(model.BuilderKind).Ancestor(key).Start(cur) 92 builders, nextCursor, err := fetchBuilders(ctx, q, allowedBuckets, req.PageSize) 93 if err != nil { 94 return nil, errors.Annotate(err, "failed to fetch builders").Err() 95 } 96 97 // Generate the next page token. 98 nextPageToken, err := listBuildersCursorVault.PageToken(ctx, nextCursor) 99 if err != nil { 100 return nil, err 101 } 102 103 // Compose the response. 104 res := &pb.ListBuildersResponse{ 105 Builders: make([]*pb.BuilderItem, len(builders)), 106 NextPageToken: nextPageToken, 107 } 108 for i, b := range builders { 109 res.Builders[i] = &pb.BuilderItem{ 110 Id: &pb.BuilderID{ 111 Project: b.Parent.Parent().StringID(), 112 Bucket: b.Parent.StringID(), 113 Builder: b.ID, 114 }, 115 Config: b.Config, 116 } 117 } 118 return res, nil 119 } 120 121 // fetchBuilders fetches a page of builders together with a cursor. 122 // 123 // buckets in allowedBuckets should use project/bucket format. 124 func fetchBuilders(ctx context.Context, q *datastore.Query, allowedBuckets []string, pageSize int32) (builders []*model.Builder, nextCursor datastore.Cursor, err error) { 125 // Note: this function is fairly generic, but the only reason it is currently 126 // Builder-specific is because datastore.Run does not accept callback 127 // signature func(any, CursorCB). 128 129 if pageSize <= 0 { 130 pageSize = 100 131 } 132 133 // Convert allowedBuckets to a set for faster lookup. 134 allowedBucketSet := stringset.NewFromSlice(allowedBuckets...) 135 136 // Fetch entities and the cursor if needed. 137 err = datastore.Run(ctx, q, func(builder *model.Builder, getCursor datastore.CursorCB) error { 138 // Check if the bucket is allowed. Use the fully qualified bucket ID 139 // instead of the bucket name so we don't have to assume that the query 140 // only returns builders from a single project. 141 bucketID := protoutil.FormatBucketID(builder.Parent.Parent().StringID(), builder.Parent.StringID()) 142 if !allowedBucketSet.Has(bucketID) { 143 return nil 144 } 145 146 builders = append(builders, builder) 147 if len(builders) == int(pageSize) { 148 var err error 149 if nextCursor, err = getCursor(); err != nil { 150 return err 151 } 152 return datastore.Stop 153 } 154 155 return nil 156 }) 157 158 return 159 }