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  }