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 }