github.com/yrj2011/jx-test-infra@v0.0.0-20190529031832-7a2065ee98eb/prow/gerrit/gerrit.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 gerrit implements a gerrit-fetcher using https://github.com/andygrunwald/go-gerrit 18 package gerrit 19 20 import ( 21 "bytes" 22 "fmt" 23 "io/ioutil" 24 "os" 25 "os/exec" 26 "path/filepath" 27 "strconv" 28 "strings" 29 "time" 30 31 "github.com/andygrunwald/go-gerrit" 32 "github.com/sirupsen/logrus" 33 34 "k8s.io/test-infra/prow/config" 35 "k8s.io/test-infra/prow/kube" 36 "k8s.io/test-infra/prow/pjutil" 37 ) 38 39 type kubeClient interface { 40 CreateProwJob(kube.ProwJob) (kube.ProwJob, error) 41 } 42 43 type gerritAuthentication interface { 44 SetCookieAuth(name, value string) 45 } 46 47 type gerritAccount interface { 48 GetAccount(name string) (*gerrit.AccountInfo, *gerrit.Response, error) 49 SetUsername(accountID string, input *gerrit.UsernameInput) (*string, *gerrit.Response, error) 50 } 51 52 type gerritChange interface { 53 QueryChanges(opt *gerrit.QueryChangeOptions) (*[]gerrit.ChangeInfo, *gerrit.Response, error) 54 SetReview(changeID, revisionID string, input *gerrit.ReviewInput) (*gerrit.ReviewResult, *gerrit.Response, error) 55 } 56 57 type configAgent interface { 58 Config() *config.Config 59 } 60 61 // Controller manages gerrit changes. 62 type Controller struct { 63 ca configAgent 64 65 // go-gerrit change endpoint client 66 auth gerritAuthentication 67 account gerritAccount 68 gc gerritChange 69 instance string 70 storage string 71 projects []string 72 73 kc kubeClient 74 75 lastUpdate time.Time 76 } 77 78 // NewController returns a new gerrit controller client 79 func NewController(instance, storage string, projects []string, kc *kube.Client, ca *config.Agent) (*Controller, error) { 80 lastUpdate := time.Now() 81 if storage != "" { 82 buf, err := ioutil.ReadFile(storage) 83 if err == nil { 84 unix, err := strconv.ParseInt(string(buf), 10, 64) 85 if err != nil { 86 return nil, err 87 } else { 88 lastUpdate = time.Unix(unix, 0) 89 } 90 } else if !os.IsNotExist(err) { 91 return nil, err 92 } 93 // fallback to time.Now() if file does not exist yet 94 } 95 96 c, err := gerrit.NewClient(instance, nil) 97 if err != nil { 98 return nil, err 99 } 100 101 return &Controller{ 102 instance: instance, 103 projects: projects, 104 kc: kc, 105 ca: ca, 106 auth: c.Authentication, 107 account: c.Accounts, 108 gc: c.Changes, 109 lastUpdate: lastUpdate, 110 storage: storage, 111 }, nil 112 } 113 114 // Auth authenticates to gerrit server 115 // Token will expire, so we need to regenerate it once so often 116 func (c *Controller) Auth() error { 117 cmd := exec.Command("python", "./git-cookie-authdaemon") 118 if err := cmd.Run(); err != nil { 119 return fmt.Errorf("Fail to authenticate to gerrit using git-cookie-authdaemon : %v", err) 120 } 121 122 raw, err := ioutil.ReadFile(filepath.Join(os.Getenv("HOME"), ".git-credential-cache/cookie")) 123 if err != nil { 124 return err 125 } 126 fields := strings.Fields(string(raw)) 127 token := fields[len(fields)-1] 128 129 c.auth.SetCookieAuth("o", token) 130 131 self, _, err := c.account.GetAccount("self") 132 if err != nil { 133 logrus.WithError(err).Errorf("Fail to auth with token: %s", token) 134 return err 135 } 136 137 logrus.Infof("Authentication successful, Username: %s", self.Name) 138 139 return nil 140 } 141 142 // SaveLastSync saves last sync time in Unix to a volume 143 func (c *Controller) SaveLastSync(lastSync time.Time) error { 144 if c.storage == "" { 145 return nil 146 } 147 148 lastSyncUnix := strconv.FormatInt(lastSync.Unix(), 10) 149 logrus.Infof("Writing last sync: %s", lastSyncUnix) 150 151 err := ioutil.WriteFile(c.storage+".tmp", []byte(lastSyncUnix), 0644) 152 if err != nil { 153 return err 154 } 155 return os.Rename(c.storage+".tmp", c.storage) 156 } 157 158 // Sync looks for newly made gerrit changes 159 // and creates prowjobs according to presubmit specs 160 func (c *Controller) Sync() error { 161 syncTime := time.Now() 162 changes := c.QueryChanges() 163 164 for _, change := range changes { 165 if err := c.ProcessChange(change); err != nil { 166 logrus.WithError(err).Errorf("Failed process change %v", change.CurrentRevision) 167 } 168 } 169 170 c.lastUpdate = syncTime 171 if err := c.SaveLastSync(syncTime); err != nil { 172 logrus.WithError(err).Errorf("last sync %v, cannot save to path %v", syncTime, c.storage) 173 } 174 logrus.Infof("Processed %d changes", len(changes)) 175 return nil 176 } 177 178 func (c *Controller) queryProjectChanges(proj string) ([]gerrit.ChangeInfo, error) { 179 pending := []gerrit.ChangeInfo{} 180 181 opt := &gerrit.QueryChangeOptions{} 182 opt.Query = append(opt.Query, "project:"+proj+"+status:open") 183 opt.AdditionalFields = []string{"CURRENT_REVISION", "CURRENT_COMMIT"} 184 185 start := 0 186 187 for { 188 opt.Limit = c.ca.Config().Gerrit.RateLimit 189 opt.Start = start 190 191 // The change output is sorted by the last update time, most recently updated to oldest updated. 192 // Gerrit API docs: https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#list-changes 193 changes, _, err := c.gc.QueryChanges(opt) 194 if err != nil { 195 // should not happen? Let next sync loop catch up 196 return pending, fmt.Errorf("failed to query gerrit changes: %v", err) 197 } 198 199 logrus.Infof("Find %d changes from query %v", len(*changes), opt.Query) 200 201 if len(*changes) == 0 { 202 return pending, nil 203 } 204 start += len(*changes) 205 206 for _, change := range *changes { 207 // if we already processed this change, then we stop the current sync loop 208 const layout = "2006-01-02 15:04:05" 209 updated, err := time.Parse(layout, change.Updated) 210 if err != nil { 211 logrus.WithError(err).Errorf("Parse time %v failed", change.Updated) 212 continue 213 } 214 215 // process if updated later than last updated 216 // stop if update was stale 217 if updated.After(c.lastUpdate) { 218 // we need to make sure the change update is from a new commit change 219 rev, ok := change.Revisions[change.CurrentRevision] 220 if !ok { 221 logrus.WithError(err).Errorf("(should not happen?)cannot find current revision for change %v", change.ID) 222 continue 223 } 224 225 created, err := time.Parse(layout, rev.Created) 226 if err != nil { 227 logrus.WithError(err).Errorf("Parse time %v failed", rev.Created) 228 continue 229 } 230 231 if !created.After(c.lastUpdate) { 232 // stale commit 233 continue 234 } 235 236 pending = append(pending, change) 237 } else { 238 return pending, nil 239 } 240 } 241 } 242 } 243 244 // QueryChanges will query all valid gerrit changes since controller's last sync loop 245 func (c *Controller) QueryChanges() []gerrit.ChangeInfo { 246 // store a map of changeID:change 247 pending := []gerrit.ChangeInfo{} 248 249 // can only query against one project at a time :-( 250 for _, proj := range c.projects { 251 if res, err := c.queryProjectChanges(proj); err != nil { 252 logrus.WithError(err).Errorf("fail to query changes for project %s", proj) 253 } else { 254 pending = append(pending, res...) 255 } 256 } 257 258 return pending 259 } 260 261 // ProcessChange creates new presubmit prowjobs base off the gerrit changes 262 func (c *Controller) ProcessChange(change gerrit.ChangeInfo) error { 263 rev, ok := change.Revisions[change.CurrentRevision] 264 if !ok { 265 return fmt.Errorf("cannot find current revision for change %v", change.ID) 266 } 267 268 parentSHA := "" 269 if len(rev.Commit.Parents) > 0 { 270 parentSHA = rev.Commit.Parents[0].Commit 271 } 272 273 logger := logrus.WithField("gerrit change", change.Number) 274 275 type triggeredJob struct { 276 Name, URL string 277 } 278 triggeredJobs := []triggeredJob{} 279 280 for _, spec := range c.ca.Config().Presubmits[c.instance+"/"+change.Project] { 281 kr := kube.Refs{ 282 Org: c.instance, 283 Repo: change.Project, 284 BaseRef: change.Branch, 285 BaseSHA: parentSHA, 286 Pulls: []kube.Pull{ 287 { 288 Number: change.Number, 289 Author: rev.Commit.Author.Name, 290 SHA: change.CurrentRevision, 291 Ref: rev.Ref, 292 }, 293 }, 294 } 295 296 // TODO(krzyzacy): Support AlwaysRun and RunIfChanged 297 pj := pjutil.NewProwJob(pjutil.PresubmitSpec(spec, kr), map[string]string{}) 298 logger.WithFields(pjutil.ProwJobFields(&pj)).Infof("Creating a new prowjob for change %s.", change.Number) 299 if _, err := c.kc.CreateProwJob(pj); err != nil { 300 logger.WithError(err).Errorf("fail to create prowjob %v", pj) 301 } else { 302 var b bytes.Buffer 303 url := "" 304 template := c.ca.Config().Plank.JobURLTemplate 305 if template != nil { 306 if err := template.Execute(&b, &pj); err != nil { 307 logger.WithFields(pjutil.ProwJobFields(&pj)).Errorf("error executing URL template: %v", err) 308 } 309 // TODO(krzyzacy): We doesn't have buildID here yet - do a hack to get a proper URL to the PR 310 // Remove this once we have proper report interface. 311 312 // mangle 313 // https://gubernator.k8s.io/build/gob-prow/pr-logs/pull/some/repo/8940/pull-test-infra-presubmit// 314 // to 315 // https://gubernator.k8s.io/builds/gob-prow/pr-logs/pull/some_repo/8940/pull-test-infra-presubmit/ 316 url = b.String() 317 url = strings.Replace(url, "build", "builds", 1) 318 // TODO(krzyzacy): gerrit path can be foo.googlesource.com/bar/baz, which means we took bar/baz as the repo 319 // we are mangling the path in bootstrap.py, we need to handle this better in podutils 320 url = strings.Replace(url, change.Project, strings.Replace(change.Project, "/", "_", -1), 1) 321 url = strings.TrimSuffix(url, "//") 322 } 323 triggeredJobs = append(triggeredJobs, triggeredJob{Name: spec.Name, URL: url}) 324 } 325 } 326 327 if len(triggeredJobs) > 0 { 328 // comment back to gerrit 329 message := "Triggered presubmit:" 330 for _, job := range triggeredJobs { 331 if job.URL != "" { 332 message += fmt.Sprintf("\n * Name: %s, URL: %s", job.Name, job.URL) 333 } else { 334 message += fmt.Sprintf("\n * Name: %s", job.Name) 335 } 336 } 337 338 if _, _, err := c.gc.SetReview(change.ID, change.CurrentRevision, &gerrit.ReviewInput{ 339 Message: message, 340 }); err != nil { 341 return fmt.Errorf("cannot comment to gerrit: %v", err) 342 } 343 } 344 345 return nil 346 }