go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/milo/internal/git/gerrit.go (about) 1 // Copyright 2018 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 git 16 17 import ( 18 "context" 19 "encoding/json" 20 "fmt" 21 "time" 22 23 "google.golang.org/grpc/codes" 24 "google.golang.org/grpc/status" 25 26 gerritpb "go.chromium.org/luci/common/proto/gerrit" 27 "go.chromium.org/luci/milo/internal/utils" 28 "go.chromium.org/luci/server/caching/layered" 29 ) 30 31 // errGRPCNotFound is what gRPC API would have returned for NotFound error. 32 var errGRPCNotFound = status.Errorf(codes.NotFound, "not found") 33 34 // CLEmail implements Client interface. 35 func (p *implementation) CLEmail(c context.Context, host string, changeNumber int64) (email string, err error) { 36 defer func() { err = utils.TagGRPC(c, err) }() 37 changeInfo, err := p.clEmailAndProjectNoACLs(c, host, changeNumber) 38 if err != nil { 39 return 40 } 41 allowed, err := p.acls.IsAllowed(c, host, changeInfo.Project) 42 switch { 43 case err != nil: 44 return 45 case allowed: 46 email = changeInfo.GetOwner().GetEmail() 47 default: 48 err = errGRPCNotFound 49 } 50 return 51 } 52 53 var gerritChangeInfoCache = layered.RegisterCache(layered.Parameters[*gerritpb.ChangeInfo]{ 54 ProcessCacheCapacity: 4096, 55 GlobalNamespace: "gerrit-change-info", 56 Marshal: func(item *gerritpb.ChangeInfo) ([]byte, error) { 57 return json.Marshal(item) 58 }, 59 Unmarshal: func(blob []byte) (*gerritpb.ChangeInfo, error) { 60 changeInfo := &gerritpb.ChangeInfo{} 61 err := json.Unmarshal(blob, changeInfo) 62 return changeInfo, err 63 }, 64 }) 65 66 // clEmailAndProjectNoACLs fetches and caches change owner email and project. 67 // 68 // Gerrit change owner and project are deemed immutable. 69 // Caveat: technically only owner's account id is immutable. Owner's email 70 // associated with this account id may change, but this is rare. 71 func (p *implementation) clEmailAndProjectNoACLs(c context.Context, host string, changeNumber int64) (*gerritpb.ChangeInfo, error) { 72 key := fmt.Sprintf("%s/%d", host, changeNumber) 73 return gerritChangeInfoCache.GetOrCreate(c, key, func() (v *gerritpb.ChangeInfo, exp time.Duration, err error) { 74 client, err := p.gerritClient(c, host) 75 if err != nil { 76 return nil, 0, err 77 } 78 79 info, err := client.GetChange(c, &gerritpb.GetChangeRequest{ 80 Number: changeNumber, 81 Options: []gerritpb.QueryOption{ 82 gerritpb.QueryOption_DETAILED_ACCOUNTS, 83 gerritpb.QueryOption_SKIP_MERGEABLE, 84 }, 85 }) 86 // We can't cache outcome of not found CL because 87 // * Milo may not at first have access to a CL, say while CL was hidden or 88 // because of bad ACLs. 89 // * Gerrit is known to return 404 flakes. 90 if err != nil { 91 return nil, 0, err 92 } 93 94 // Cache and return only email and project. 95 ret := &gerritpb.ChangeInfo{ 96 Project: info.Project, 97 Owner: &gerritpb.AccountInfo{Email: info.GetOwner().GetEmail()}, 98 } 99 100 return ret, 0, nil 101 }) 102 }