github.com/munnerz/test-infra@v0.0.0-20190108210205-ce3d181dc989/prow/gerrit/client/client.go (about) 1 /* 2 Copyright 2018 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 // Package client implements a client that can handle multiple gerrit instances 18 // derived from https://github.com/andygrunwald/go-gerrit 19 package client 20 21 import ( 22 "fmt" 23 "io/ioutil" 24 "net/url" 25 "strings" 26 "time" 27 28 "github.com/andygrunwald/go-gerrit" 29 "github.com/sirupsen/logrus" 30 ) 31 32 const ( 33 // LGTM means all presubmits passed, but need someone else to approve before merge. 34 LGTM = "+1" 35 // LBTM means some presubmits failed, perfer not merge. 36 LBTM = "-1" 37 // CodeReview is the default gerrit code review label 38 CodeReview = "Code-Review" 39 40 // GerritID identifies a gerrit change 41 GerritID = "prow.k8s.io/gerrit-id" 42 // GerritInstance is the gerrit host url 43 GerritInstance = "prow.k8s.io/gerrit-instance" 44 // GerritRevision is the SHA of current patchset from a gerrit change 45 GerritRevision = "prow.k8s.io/gerrit-revision" 46 // GerritReportLabel is the gerrit label prow will cast vote on, fallback to CodeReview label if unset 47 GerritReportLabel = "prow.k8s.io/gerrit-report-label" 48 49 // Merged status indicates a Gerrit change has been merged 50 Merged = "MERGED" 51 // New status indicates a Gerrit change is new (ie pending) 52 New = "NEW" 53 ) 54 55 // ProjectsFlag is the flag type for gerrit projects when initializing a gerrit client 56 type ProjectsFlag map[string][]string 57 58 func (p ProjectsFlag) String() string { 59 var hosts []string 60 for host, repos := range p { 61 hosts = append(hosts, host+"="+strings.Join(repos, ",")) 62 } 63 return strings.Join(hosts, " ") 64 } 65 66 // Set populates ProjectsFlag upon flag.Parse() 67 func (p ProjectsFlag) Set(value string) error { 68 parts := strings.SplitN(value, "=", 2) 69 if len(parts) != 2 { 70 return fmt.Errorf("%s not in the form of host=repo-a,repo-b,etc", value) 71 } 72 host := parts[0] 73 if _, ok := p[host]; ok { 74 return fmt.Errorf("duplicate host: %s", host) 75 } 76 repos := strings.Split(parts[1], ",") 77 p[host] = repos 78 return nil 79 } 80 81 type gerritAuthentication interface { 82 SetCookieAuth(name, value string) 83 } 84 85 type gerritAccount interface { 86 GetAccount(name string) (*gerrit.AccountInfo, *gerrit.Response, error) 87 SetUsername(accountID string, input *gerrit.UsernameInput) (*string, *gerrit.Response, error) 88 } 89 90 type gerritChange interface { 91 QueryChanges(opt *gerrit.QueryChangeOptions) (*[]gerrit.ChangeInfo, *gerrit.Response, error) 92 SetReview(changeID, revisionID string, input *gerrit.ReviewInput) (*gerrit.ReviewResult, *gerrit.Response, error) 93 } 94 95 type gerritProjects interface { 96 GetBranch(projectName, branchID string) (*gerrit.BranchInfo, *gerrit.Response, error) 97 } 98 99 // gerritInstanceHandler holds all actual gerrit handlers 100 type gerritInstanceHandler struct { 101 instance string 102 projects []string 103 104 authService gerritAuthentication 105 accountService gerritAccount 106 changeService gerritChange 107 projectService gerritProjects 108 } 109 110 // Client holds a instance:handler map 111 type Client struct { 112 handlers map[string]*gerritInstanceHandler 113 } 114 115 // ChangeInfo is a gerrit.ChangeInfo 116 type ChangeInfo = gerrit.ChangeInfo 117 118 // RevisionInfo is a gerrit.RevisionInfo 119 type RevisionInfo = gerrit.RevisionInfo 120 121 // FileInfo is a gerrit.FileInfo 122 type FileInfo = gerrit.FileInfo 123 124 // NewClient returns a new gerrit client 125 func NewClient(instances map[string][]string) (*Client, error) { 126 c := &Client{ 127 handlers: map[string]*gerritInstanceHandler{}, 128 } 129 for instance := range instances { 130 gc, err := gerrit.NewClient(instance, nil) 131 if err != nil { 132 return nil, err 133 } 134 135 c.handlers[instance] = &gerritInstanceHandler{ 136 instance: instance, 137 projects: instances[instance], 138 authService: gc.Authentication, 139 accountService: gc.Accounts, 140 changeService: gc.Changes, 141 projectService: gc.Projects, 142 } 143 } 144 145 return c, nil 146 } 147 148 func auth(c *Client, cookiefilePath string) { 149 logrus.Info("Starting auth loop...") 150 var previousToken string 151 wait := 10 * time.Minute 152 for { 153 raw, err := ioutil.ReadFile(cookiefilePath) 154 if err != nil { 155 logrus.WithError(err).Error("Failed to read auth cookie") 156 } 157 fields := strings.Fields(string(raw)) 158 token := fields[len(fields)-1] 159 160 if token == previousToken { 161 time.Sleep(wait) 162 continue 163 } 164 165 logrus.Info("New token, updating handlers...") 166 167 // update auth token for each instance 168 for _, handler := range c.handlers { 169 handler.authService.SetCookieAuth("o", token) 170 171 self, _, err := handler.accountService.GetAccount("self") 172 if err != nil { 173 logrus.WithError(err).Error("Failed to auth with token") 174 continue 175 } 176 177 logrus.Infof("Authentication to %s successful, Username: %s", handler.instance, self.Name) 178 } 179 previousToken = token 180 time.Sleep(wait) 181 } 182 } 183 184 // Start will authenticate the client with gerrit periodically 185 // Start must be called before user calls any client functions. 186 func (c *Client) Start(cookiefilePath string) { 187 if cookiefilePath != "" { 188 go auth(c, cookiefilePath) 189 } 190 } 191 192 // QueryChanges queries for all changes from all projects after lastUpdate time 193 // returns an instance:changes map 194 func (c *Client) QueryChanges(lastUpdate time.Time, rateLimit int) map[string][]ChangeInfo { 195 result := map[string][]ChangeInfo{} 196 for _, h := range c.handlers { 197 changes := h.queryAllChanges(lastUpdate, rateLimit) 198 if len(changes) > 0 { 199 result[h.instance] = []ChangeInfo{} 200 for _, change := range changes { 201 result[h.instance] = append(result[h.instance], change) 202 } 203 } 204 } 205 return result 206 } 207 208 // SetReview writes a review comment base on the change id + revision 209 func (c *Client) SetReview(instance, id, revision, message string, labels map[string]string) error { 210 h, ok := c.handlers[instance] 211 if !ok { 212 return fmt.Errorf("not activated gerrit instance: %s", instance) 213 } 214 215 if _, _, err := h.changeService.SetReview(id, revision, &gerrit.ReviewInput{ 216 Message: message, 217 Labels: labels, 218 }); err != nil { 219 return fmt.Errorf("cannot comment to gerrit: %v", err) 220 } 221 222 return nil 223 } 224 225 // GetBranchRevision returns SHA of HEAD of a branch 226 func (c *Client) GetBranchRevision(instance, project, branch string) (string, error) { 227 h, ok := c.handlers[instance] 228 if !ok { 229 return "", fmt.Errorf("not activated gerrit instance: %s", instance) 230 } 231 232 res, _, err := h.projectService.GetBranch(project, url.QueryEscape(branch)) 233 if err != nil { 234 return "", err 235 } 236 237 return res.Revision, nil 238 } 239 240 // private handler implementation details 241 242 func (h *gerritInstanceHandler) queryAllChanges(lastUpdate time.Time, rateLimit int) []gerrit.ChangeInfo { 243 result := []gerrit.ChangeInfo{} 244 for _, project := range h.projects { 245 changes, err := h.queryChangesForProject(project, lastUpdate, rateLimit) 246 if err != nil { 247 // don't halt on error from one project, log & continue 248 logrus.WithError(err).Errorf("fail to query changes for project %s", project) 249 continue 250 } 251 result = append(result, changes...) 252 } 253 254 return result 255 } 256 257 func (h *gerritInstanceHandler) queryChangesForProject(project string, lastUpdate time.Time, rateLimit int) ([]gerrit.ChangeInfo, error) { 258 pending := []gerrit.ChangeInfo{} 259 260 opt := &gerrit.QueryChangeOptions{} 261 opt.Query = append(opt.Query, "project:"+project) 262 opt.AdditionalFields = []string{"CURRENT_REVISION", "CURRENT_COMMIT", "CURRENT_FILES"} 263 264 start := 0 265 266 for { 267 opt.Limit = rateLimit 268 opt.Start = start 269 270 // The change output is sorted by the last update time, most recently updated to oldest updated. 271 // Gerrit API docs: https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#list-changes 272 changes, _, err := h.changeService.QueryChanges(opt) 273 if err != nil { 274 // should not happen? Let next sync loop catch up 275 return nil, fmt.Errorf("failed to query gerrit changes: %v", err) 276 } 277 278 if changes == nil || len(*changes) == 0 { 279 logrus.Infof("no more changes from query, returning...") 280 return pending, nil 281 } 282 283 logrus.Infof("Find %d changes from query %v", len(*changes), opt.Query) 284 285 start += len(*changes) 286 287 for _, change := range *changes { 288 // if we already processed this change, then we stop the current sync loop 289 const layout = "2006-01-02 15:04:05" 290 updated, err := time.Parse(layout, change.Updated) 291 if err != nil { 292 logrus.WithError(err).Errorf("Parse time %v failed", change.Updated) 293 continue 294 } 295 296 logrus.Infof("Change %d, last updated %s, status %s", change.Number, change.Updated, change.Status) 297 298 // process if updated later than last updated 299 // stop if update was stale 300 if !updated.Before(lastUpdate) { 301 switch change.Status { 302 case Merged: 303 submitted, err := time.Parse(layout, change.Submitted) 304 if err != nil { 305 logrus.WithError(err).Errorf("Parse time %v failed", change.Submitted) 306 continue 307 } 308 if submitted.Before(lastUpdate) { 309 logrus.Infof("Change %d, submitted %s before lastUpdate %s, skipping this patchset", change.Number, submitted, lastUpdate) 310 continue 311 } 312 pending = append(pending, change) 313 case New: 314 // we need to make sure the change update is from a fresh commit change 315 rev, ok := change.Revisions[change.CurrentRevision] 316 if !ok { 317 logrus.WithError(err).Errorf("(should not happen?)cannot find current revision for change %v", change.ID) 318 continue 319 } 320 321 created, err := time.Parse(layout, rev.Created) 322 if err != nil { 323 logrus.WithError(err).Errorf("Parse time %v failed", rev.Created) 324 continue 325 } 326 327 if created.Before(lastUpdate) { 328 // stale commit 329 logrus.Infof("Change %d, latest revision updated %s before lastUpdate %s, skipping this patchset", change.Number, created, lastUpdate) 330 continue 331 } 332 333 pending = append(pending, change) 334 default: 335 // change has been abandoned, do nothing 336 } 337 } else { 338 logrus.Infof("Change %d, updated %s before lastUpdate %s, return", change.Number, change.Updated, lastUpdate) 339 return pending, nil 340 } 341 } 342 } 343 }