github.com/munnerz/test-infra@v0.0.0-20190108210205-ce3d181dc989/prow/gerrit/adapter/adapter.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 adapter implements a controller that interacts with gerrit instances 18 package adapter 19 20 import ( 21 "errors" 22 "fmt" 23 "io" 24 "io/ioutil" 25 "net/url" 26 "os" 27 "path/filepath" 28 "strconv" 29 "time" 30 31 "github.com/sirupsen/logrus" 32 33 "k8s.io/test-infra/prow/config" 34 "k8s.io/test-infra/prow/gerrit/client" 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 gerritClient interface { 44 QueryChanges(lastUpdate time.Time, rateLimit int) map[string][]client.ChangeInfo 45 GetBranchRevision(instance, project, branch string) (string, error) 46 SetReview(instance, id, revision, message string, labels map[string]string) error 47 } 48 49 type configAgent interface { 50 Config() *config.Config 51 } 52 53 // Controller manages gerrit changes. 54 type Controller struct { 55 ca configAgent 56 kc kubeClient 57 gc gerritClient 58 59 lastSyncFallback string 60 61 lastUpdate time.Time 62 } 63 64 // NewController returns a new gerrit controller client 65 func NewController(lastSyncFallback, cookiefilePath string, projects map[string][]string, kc *kube.Client, ca *config.Agent) (*Controller, error) { 66 if lastSyncFallback == "" { 67 return nil, errors.New("empty lastSyncFallback") 68 } 69 70 var lastUpdate time.Time 71 if buf, err := ioutil.ReadFile(lastSyncFallback); err == nil { 72 unix, err := strconv.ParseInt(string(buf), 10, 64) 73 if err != nil { 74 return nil, err 75 } 76 lastUpdate = time.Unix(unix, 0) 77 } else if err != nil && !os.IsNotExist(err) { 78 return nil, fmt.Errorf("failed to read lastSyncFallback: %v", err) 79 } else { 80 logrus.Warnf("lastSyncFallback not found: %s", lastSyncFallback) 81 lastUpdate = time.Now() 82 } 83 84 c, err := client.NewClient(projects) 85 if err != nil { 86 return nil, err 87 } 88 c.Start(cookiefilePath) 89 90 return &Controller{ 91 kc: kc, 92 ca: ca, 93 gc: c, 94 lastUpdate: lastUpdate, 95 lastSyncFallback: lastSyncFallback, 96 }, nil 97 } 98 99 func copyFile(srcPath, destPath string) error { 100 // fallback to copying the file instead 101 src, err := os.Open(srcPath) 102 if err != nil { 103 return err 104 } 105 dst, err := os.OpenFile(destPath, os.O_WRONLY, 0666) 106 if err != nil { 107 return err 108 } 109 _, err = io.Copy(dst, src) 110 if err != nil { 111 return err 112 } 113 dst.Sync() 114 dst.Close() 115 src.Close() 116 return nil 117 } 118 119 // SaveLastSync saves last sync time in Unix to a volume 120 func (c *Controller) SaveLastSync(lastSync time.Time) error { 121 if c.lastSyncFallback == "" { 122 return nil 123 } 124 125 lastSyncUnix := strconv.FormatInt(lastSync.Unix(), 10) 126 logrus.Infof("Writing last sync: %s", lastSyncUnix) 127 128 tempFile, err := ioutil.TempFile(filepath.Dir(c.lastSyncFallback), "temp") 129 if err != nil { 130 return err 131 } 132 defer os.Remove(tempFile.Name()) 133 134 err = ioutil.WriteFile(tempFile.Name(), []byte(lastSyncUnix), 0644) 135 if err != nil { 136 return err 137 } 138 139 err = os.Rename(tempFile.Name(), c.lastSyncFallback) 140 if err != nil { 141 logrus.WithError(err).Info("Rename failed, fallback to copyfile") 142 return copyFile(tempFile.Name(), c.lastSyncFallback) 143 } 144 return nil 145 } 146 147 // Sync looks for newly made gerrit changes 148 // and creates prowjobs according to specs 149 func (c *Controller) Sync() error { 150 // gerrit timestamp only has second precision 151 syncTime := time.Now().Truncate(time.Second) 152 153 for instance, changes := range c.gc.QueryChanges(c.lastUpdate, c.ca.Config().Gerrit.RateLimit) { 154 for _, change := range changes { 155 if err := c.ProcessChange(instance, change); err != nil { 156 logrus.WithError(err).Errorf("Failed process change %v", change.CurrentRevision) 157 } 158 } 159 160 logrus.Infof("Processed %d changes for instance %s", len(changes), instance) 161 } 162 163 c.lastUpdate = syncTime 164 if err := c.SaveLastSync(syncTime); err != nil { 165 logrus.WithError(err).Errorf("last sync %v, cannot save to path %v", syncTime, c.lastSyncFallback) 166 } 167 168 return nil 169 } 170 171 func makeCloneURI(instance, project string) (*url.URL, error) { 172 u, err := url.Parse(instance) 173 if err != nil { 174 return nil, fmt.Errorf("instance %s is not a url: %v", instance, err) 175 } 176 if u.Host == "" { 177 return nil, errors.New("instance does not set host") 178 } 179 if u.Path != "" { 180 return nil, errors.New("instance cannot set path (this is set by project)") 181 } 182 u.Path = project 183 return u, nil 184 } 185 186 // listChangedFiles lists (in lexicographic order) the files changed as part of a Gerrit patchset 187 func listChangedFiles(changeInfo client.ChangeInfo) []string { 188 changed := []string{} 189 revision := changeInfo.Revisions[changeInfo.CurrentRevision] 190 for file := range revision.Files { 191 changed = append(changed, file) 192 } 193 return changed 194 } 195 196 // ProcessChange creates new presubmit prowjobs base off the gerrit changes 197 func (c *Controller) ProcessChange(instance string, change client.ChangeInfo) error { 198 rev, ok := change.Revisions[change.CurrentRevision] 199 if !ok { 200 return fmt.Errorf("cannot find current revision for change %v", change.ID) 201 } 202 203 logger := logrus.WithField("gerrit change", change.Number) 204 205 cloneURI, err := makeCloneURI(instance, change.Project) 206 if err != nil { 207 return fmt.Errorf("failed to create clone uri: %v", err) 208 } 209 210 baseSHA, err := c.gc.GetBranchRevision(instance, change.Project, change.Branch) 211 if err != nil { 212 return fmt.Errorf("failed to get SHA from base branch: %v", err) 213 } 214 215 triggeredJobs := []string{} 216 217 kr := kube.Refs{ 218 Org: cloneURI.Host, // Something like android.googlesource.com 219 Repo: change.Project, // Something like platform/build 220 BaseRef: change.Branch, 221 BaseSHA: baseSHA, 222 CloneURI: cloneURI.String(), // Something like https://android.googlesource.com/platform/build 223 Pulls: []kube.Pull{ 224 { 225 Number: change.Number, 226 Author: rev.Commit.Author.Name, 227 SHA: change.CurrentRevision, 228 Ref: rev.Ref, 229 }, 230 }, 231 } 232 233 type jobSpec struct { 234 spec kube.ProwJobSpec 235 labels map[string]string 236 } 237 238 var jobSpecs []jobSpec 239 240 changedFiles := listChangedFiles(change) 241 242 switch change.Status { 243 case client.Merged: 244 postsubmits := c.ca.Config().Postsubmits[cloneURI.String()] 245 postsubmits = append(postsubmits, c.ca.Config().Postsubmits[cloneURI.Host+"/"+cloneURI.Path]...) 246 for _, postsubmit := range postsubmits { 247 if postsubmit.RunsAgainstChanges(changedFiles) { 248 jobSpecs = append(jobSpecs, jobSpec{ 249 spec: pjutil.PostsubmitSpec(postsubmit, kr), 250 labels: postsubmit.Labels, 251 }) 252 } 253 } 254 case client.New: 255 presubmits := c.ca.Config().Presubmits[cloneURI.String()] 256 presubmits = append(presubmits, c.ca.Config().Presubmits[cloneURI.Host+"/"+cloneURI.Path]...) 257 for _, presubmit := range presubmits { 258 if presubmit.RunsAgainstChanges(changedFiles) { 259 jobSpecs = append(jobSpecs, jobSpec{ 260 spec: pjutil.PresubmitSpec(presubmit, kr), 261 labels: presubmit.Labels, 262 }) 263 } 264 } 265 } 266 267 annotations := map[string]string{ 268 client.GerritID: change.ID, 269 client.GerritInstance: instance, 270 } 271 272 for _, jSpec := range jobSpecs { 273 labels := make(map[string]string) 274 for k, v := range jSpec.labels { 275 labels[k] = v 276 } 277 labels[client.GerritRevision] = change.CurrentRevision 278 279 pj := pjutil.NewProwJobWithAnnotation(jSpec.spec, labels, annotations) 280 if _, err := c.kc.CreateProwJob(pj); err != nil { 281 logger.WithError(err).Errorf("fail to create prowjob %v", pj) 282 } else { 283 triggeredJobs = append(triggeredJobs, jSpec.spec.Job) 284 } 285 } 286 287 if len(triggeredJobs) > 0 { 288 // comment back to gerrit 289 message := fmt.Sprintf("Triggered %d prow jobs:", len(triggeredJobs)) 290 for _, job := range triggeredJobs { 291 message += fmt.Sprintf("\n * Name: %s", job) 292 } 293 294 if err := c.gc.SetReview(instance, change.ID, change.CurrentRevision, message, nil); err != nil { 295 return err 296 } 297 } 298 299 return nil 300 }