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  }