go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/analysis/internal/testresults/gerritchangelists/fetch_owner_kinds.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 gerritchangelists
    16  
    17  import (
    18  	"context"
    19  	"regexp"
    20  	"strings"
    21  
    22  	"cloud.google.com/go/spanner"
    23  	"google.golang.org/grpc/codes"
    24  	"google.golang.org/grpc/status"
    25  
    26  	"go.chromium.org/luci/common/errors"
    27  	"go.chromium.org/luci/common/logging"
    28  	gerritpb "go.chromium.org/luci/common/proto/gerrit"
    29  	rdbpb "go.chromium.org/luci/resultdb/proto/v1"
    30  	"go.chromium.org/luci/server/span"
    31  
    32  	"go.chromium.org/luci/analysis/internal/gerrit"
    33  	"go.chromium.org/luci/analysis/pbutil"
    34  	pb "go.chromium.org/luci/analysis/proto/v1"
    35  )
    36  
    37  var (
    38  	// Automation service accounts.
    39  	automationAccountRE = regexp.MustCompile(`^.*@.*\.gserviceaccount\.com$`)
    40  )
    41  
    42  // LookupRequest represents parameters to a request to lookup up the
    43  // owner kind of a gerrit changelist.
    44  type LookupRequest struct {
    45  	// Gerrit project in which the changelist is. Optional, but
    46  	// gerrit prefers this be set to speed up lookups.
    47  	GerritProject string
    48  }
    49  
    50  // FetchOwnerKinds retrieves the owner kind of each of the nominated
    51  // gerrit changelists.
    52  //
    53  // For each changelist for which the owner kind is not cached in Spanner,
    54  // this method will make an RPC to gerrit.
    55  //
    56  // This method must NOT be called within a Spanner transaction
    57  // context, as it will create its own transactions to access
    58  // the changelist cache.
    59  func FetchOwnerKinds(ctx context.Context, reqs map[Key]LookupRequest) (map[Key]pb.ChangelistOwnerKind, error) {
    60  	cacheResult := make(map[Key]*GerritChangelist)
    61  	if len(reqs) > 0 {
    62  		keys := make(map[Key]struct{})
    63  		for key := range reqs {
    64  			keys[key] = struct{}{}
    65  		}
    66  
    67  		// Try to retrieve changelist details from the Spanner cache.
    68  		var err error
    69  		cacheResult, err = Read(span.Single(ctx), keys)
    70  		if err != nil {
    71  			return nil, errors.Annotate(err, "read changelist cache").Err()
    72  		}
    73  	}
    74  
    75  	var ms []*spanner.Mutation
    76  	for key, req := range reqs {
    77  		if _, ok := cacheResult[key]; ok {
    78  			continue
    79  		}
    80  
    81  		// Retrieve the changelist details from Gerrit.
    82  		ownerKind, err := retrieveChangelistOwnerKind(ctx, key, req.GerritProject)
    83  		if err != nil {
    84  			return nil, errors.Annotate(err, "retrieve owner kind from gerrit").Err()
    85  		}
    86  		cl := &GerritChangelist{
    87  			Project:   key.Project,
    88  			Host:      key.Host,
    89  			Change:    key.Change,
    90  			OwnerKind: ownerKind,
    91  		}
    92  
    93  		// Prepare a mutation to create/replace the Spanner cache entry for
    94  		// this changelist. (Replacement may occur if multiple calls to
    95  		// this method for the same changelist race.)
    96  		m, err := CreateOrUpdate(cl)
    97  		if err != nil {
    98  			return nil, err
    99  		}
   100  		ms = append(ms, m)
   101  
   102  		// Combine the fetched changelist details with those retrieved
   103  		// from the cache earlier.
   104  		cacheResult[key] = cl
   105  	}
   106  
   107  	if len(ms) > 0 {
   108  		// Apply pending updates to the cache.
   109  		_, err := span.ReadWriteTransaction(ctx, func(ctx context.Context) error {
   110  			span.BufferWrite(ctx, ms...)
   111  			return nil
   112  		})
   113  		if err != nil {
   114  			return nil, errors.Annotate(err, "update changelist cache").Err()
   115  		}
   116  	}
   117  
   118  	result := make(map[Key]pb.ChangelistOwnerKind)
   119  	for key, entry := range cacheResult {
   120  		result[key] = entry.OwnerKind
   121  	}
   122  	return result, nil
   123  }
   124  
   125  // PopulateOwnerKinds augments the given sources information to include
   126  // the owner kind of each changelist.
   127  //
   128  // For each changelist for which the owner kind is not cached in Spanner,
   129  // this method will make an RPC to gerrit.
   130  //
   131  // This method must NOT be called within a Spanner transaction
   132  // context, as it will create its own transactions to access
   133  // the changelist cache.
   134  func PopulateOwnerKinds(ctx context.Context, project string, sourcesByID map[string]*rdbpb.Sources) (map[string]*pb.Sources, error) {
   135  	if sourcesByID == nil {
   136  		return make(map[string]*pb.Sources), nil
   137  	}
   138  
   139  	// Create the lookup requests.
   140  	reqs := make(map[Key]LookupRequest)
   141  	for _, sources := range sourcesByID {
   142  		for _, cl := range sources.Changelists {
   143  			key := Key{
   144  				Project: project,
   145  				Host:    cl.Host,
   146  				Change:  cl.Change,
   147  			}
   148  			reqs[key] = LookupRequest{
   149  				GerritProject: cl.Project,
   150  			}
   151  		}
   152  	}
   153  
   154  	kinds, err := FetchOwnerKinds(ctx, reqs)
   155  	if err != nil {
   156  		return nil, err
   157  	}
   158  
   159  	// Augmenting the original ResultDB sources with the owner kind
   160  	// of each changelist, as retrieved from gerrit or the cache.
   161  	result := make(map[string]*pb.Sources)
   162  	for id, sources := range sourcesByID {
   163  		augmentedSources := pbutil.SourcesFromResultDB(sources)
   164  
   165  		for _, augmentedCL := range augmentedSources.Changelists {
   166  			key := Key{
   167  				Project: project,
   168  				Host:    augmentedCL.Host,
   169  				Change:  augmentedCL.Change,
   170  			}
   171  
   172  			// Augment each changelist with the owner kind.
   173  			augmentedCL.OwnerKind = kinds[key]
   174  		}
   175  		result[id] = augmentedSources
   176  	}
   177  	return result, nil
   178  }
   179  
   180  // retrieveChangelistOwnerKind retrieves the owner kind for the
   181  // given CL, using the given gerrit project hint (e.g. "chromium/src").
   182  func retrieveChangelistOwnerKind(ctx context.Context, clKey Key, gerritProjectHint string) (pb.ChangelistOwnerKind, error) {
   183  	if !strings.HasSuffix(clKey.Host, "-review.googlesource.com") {
   184  		// Do not try and retrieve CL information from a gerrit host other
   185  		// than those hosted on .googlesource.com. The CL hostname
   186  		// could come from an untrusted source, and we don't want to leak
   187  		// our authentication tokens to arbitrary hosts on the internet.
   188  		return pb.ChangelistOwnerKind_CHANGELIST_OWNER_UNSPECIFIED, nil
   189  	}
   190  
   191  	client, err := gerrit.NewClient(ctx, clKey.Host, clKey.Project)
   192  	if err != nil {
   193  		return pb.ChangelistOwnerKind_CHANGELIST_OWNER_UNSPECIFIED, err
   194  	}
   195  	req := &gerritpb.GetChangeRequest{
   196  		Number: clKey.Change,
   197  		Options: []gerritpb.QueryOption{
   198  			gerritpb.QueryOption_DETAILED_ACCOUNTS,
   199  		},
   200  		// Project hint, e.g. "chromium/src".
   201  		// Reduces work on Gerrit server side.
   202  		Project: gerritProjectHint,
   203  	}
   204  	fullChange, err := client.GetChange(ctx, req)
   205  	code := status.Code(err)
   206  	if code == codes.NotFound {
   207  		logging.Warningf(ctx, "Changelist %s/%v for project %s not found.",
   208  			clKey.Host, clKey.Change, clKey.Project)
   209  		return pb.ChangelistOwnerKind_CHANGELIST_OWNER_UNSPECIFIED, nil
   210  	}
   211  	if code == codes.PermissionDenied {
   212  		logging.Warningf(ctx, "LUCI Analysis does not have permission to read changelist %s/%v for project %s.",
   213  			clKey.Host, clKey.Change, clKey.Project)
   214  		return pb.ChangelistOwnerKind_CHANGELIST_OWNER_UNSPECIFIED, nil
   215  	}
   216  	if err != nil {
   217  		return pb.ChangelistOwnerKind_CHANGELIST_OWNER_UNSPECIFIED, err
   218  	}
   219  	ownerEmail := fullChange.Owner.GetEmail()
   220  	if automationAccountRE.MatchString(ownerEmail) {
   221  		return pb.ChangelistOwnerKind_AUTOMATION, nil
   222  	} else if ownerEmail != "" {
   223  		return pb.ChangelistOwnerKind_HUMAN, nil
   224  	}
   225  	return pb.ChangelistOwnerKind_CHANGELIST_OWNER_UNSPECIFIED, nil
   226  }