github.com/munnerz/test-infra@v0.0.0-20190108210205-ce3d181dc989/prow/cmd/deck/pr_history.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 main 18 19 import ( 20 "fmt" 21 "net/url" 22 "path" 23 "regexp" 24 "sort" 25 "strconv" 26 "strings" 27 "time" 28 29 "cloud.google.com/go/storage" 30 "github.com/sirupsen/logrus" 31 "k8s.io/apimachinery/pkg/util/sets" 32 v1 "k8s.io/test-infra/prow/apis/prowjobs/v1" 33 "k8s.io/test-infra/prow/config" 34 "k8s.io/test-infra/prow/gcsupload" 35 "k8s.io/test-infra/prow/pod-utils/downwardapi" 36 ) 37 38 var pullCommitRe = regexp.MustCompile(`^[-\w]+:\w{40},\d+:(\w{40})$`) 39 40 type prHistoryTemplate struct { 41 Link string 42 Name string 43 Jobs []prJobData 44 Commits []commitData 45 } 46 47 type prJobData struct { 48 Name string 49 Link string 50 Builds []buildData 51 } 52 53 type jobBuilds struct { 54 name string 55 buildPrefixes []string 56 } 57 58 type commitData struct { 59 Hash string 60 HashPrefix string // used only for display purposes, so don't worry about uniqueness 61 Link string 62 MaxWidth int 63 latest time.Time // time stamp of the job most recently started 64 } 65 66 type latestCommit []commitData 67 68 func (a latestCommit) Len() int { return len(a) } 69 func (a latestCommit) Swap(i, j int) { a[i], a[j] = a[j], a[i] } 70 func (a latestCommit) Less(i, j int) bool { 71 if len(a[i].Hash) != 40 { 72 return true 73 } 74 if len(a[j].Hash) != 40 { 75 return false 76 } 77 return a[i].latest.Before(a[j].latest) 78 } 79 80 type byStarted []buildData 81 82 func (a byStarted) Len() int { return len(a) } 83 func (a byStarted) Swap(i, j int) { a[i], a[j] = a[j], a[i] } 84 func (a byStarted) Less(i, j int) bool { return a[i].Started.Before(a[j].Started) } 85 86 func githubPRLink(org, repo string, pr int) string { 87 return fmt.Sprintf("https://github.com/%s/%s/pull/%d", org, repo, pr) 88 } 89 90 func githubCommitLink(org, repo, commitHash string) string { 91 return fmt.Sprintf("https://github.com/%s/%s/commit/%s", org, repo, commitHash) 92 } 93 94 func jobHistLink(bucketName, jobName string) string { 95 return fmt.Sprintf("/job-history/%s/pr-logs/directory/%s", bucketName, jobName) 96 } 97 98 // gets the pull commit hash from metadata 99 func getPullCommitHash(pull string) (string, error) { 100 match := pullCommitRe.FindStringSubmatch(pull) 101 if len(match) != 2 { 102 expected := "branch:hash,pullNumber:hash" 103 return "", fmt.Errorf("unable to parse pull %q (expected %q)", pull, expected) 104 } 105 return match[1], nil 106 } 107 108 // listJobBuilds concurrently lists builds for the given job prefixes that have been run on a PR 109 func listJobBuilds(bucket storageBucket, jobPrefixes []string) []jobBuilds { 110 jobch := make(chan jobBuilds) 111 defer close(jobch) 112 for i, jobPrefix := range jobPrefixes { 113 go func(i int, jobPrefix string) { 114 buildPrefixes, err := bucket.listSubDirs(jobPrefix) 115 if err != nil { 116 logrus.WithError(err).Warningf("Error getting builds for job %s", jobPrefix) 117 } 118 jobch <- jobBuilds{ 119 name: path.Base(jobPrefix), 120 buildPrefixes: buildPrefixes, 121 } 122 }(i, jobPrefix) 123 } 124 jobs := []jobBuilds{} 125 for range jobPrefixes { 126 job := <-jobch 127 jobs = append(jobs, job) 128 } 129 return jobs 130 } 131 132 // getPRBuildData concurrently fetches metadata on each build of each job run on a PR 133 func getPRBuildData(bucket storageBucket, jobs []jobBuilds) []buildData { 134 buildch := make(chan buildData) 135 defer close(buildch) 136 expected := 0 137 for _, job := range jobs { 138 for j, buildPrefix := range job.buildPrefixes { 139 go func(j int, jobName, buildPrefix string) { 140 build, err := getBuildData(bucket, buildPrefix) 141 if err != nil { 142 logrus.WithError(err).Warningf("build %s information incomplete", buildPrefix) 143 } 144 split := strings.Split(strings.TrimSuffix(buildPrefix, "/"), "/") 145 build.SpyglassLink = path.Join(spyglassPrefix, bucket.getName(), buildPrefix) 146 build.ID = split[len(split)-1] 147 build.jobName = jobName 148 build.prefix = buildPrefix 149 build.index = j 150 buildch <- build 151 }(j, job.name, buildPrefix) 152 expected++ 153 } 154 } 155 builds := []buildData{} 156 for k := 0; k < expected; k++ { 157 build := <-buildch 158 builds = append(builds, build) 159 } 160 return builds 161 } 162 163 func updateCommitData(commits map[string]*commitData, org, repo, hash string, buildTime time.Time, width int) { 164 commit, ok := commits[hash] 165 if !ok { 166 commits[hash] = &commitData{ 167 Hash: hash, 168 HashPrefix: hash, 169 } 170 commit = commits[hash] 171 if len(hash) == 40 { 172 commit.HashPrefix = hash[:7] 173 commit.Link = githubCommitLink(org, repo, hash) 174 } 175 } 176 if buildTime.After(commit.latest) { 177 commit.latest = buildTime 178 } 179 if width > commit.MaxWidth { 180 commit.MaxWidth = width 181 } 182 } 183 184 func parsePullKey(key string) (org, repo string, pr int, err error) { 185 parts := strings.Split(key, "/") 186 if len(parts) != 3 { 187 err = fmt.Errorf("malformed PR key: %s", key) 188 return 189 } 190 pr, err = strconv.Atoi(parts[2]) 191 if err != nil { 192 return 193 } 194 return parts[0], parts[1], pr, nil 195 } 196 197 // getGCSDirsForPR returns a map from bucket names -> set of "directories" containing presubmit data 198 func getGCSDirsForPR(config *config.Config, org, repo string, pr int) (map[string]sets.String, error) { 199 toSearch := make(map[string]sets.String) 200 fullRepo := org + "/" + repo 201 presubmits, ok := config.Presubmits[fullRepo] 202 if !ok { 203 return toSearch, fmt.Errorf("couldn't find presubmits for %q in config", fullRepo) 204 } 205 206 for _, presubmit := range presubmits { 207 var gcsConfig *v1.GCSConfiguration 208 if presubmit.DecorationConfig != nil && presubmit.DecorationConfig.GCSConfiguration != nil { 209 gcsConfig = presubmit.DecorationConfig.GCSConfiguration 210 } else { 211 // for undecorated jobs assume the default 212 gcsConfig = config.Plank.DefaultDecorationConfig.GCSConfiguration 213 } 214 215 gcsPath, _, _ := gcsupload.PathsForJob(gcsConfig, &downwardapi.JobSpec{ 216 Type: v1.PresubmitJob, 217 Job: presubmit.Name, 218 Refs: &v1.Refs{ 219 Repo: repo, 220 Org: org, 221 Pulls: []v1.Pull{ 222 {Number: pr}, 223 }, 224 }, 225 }, "") 226 gcsPath, _ = path.Split(path.Clean(gcsPath)) 227 if _, ok := toSearch[gcsConfig.Bucket]; !ok { 228 toSearch[gcsConfig.Bucket] = sets.String{} 229 } 230 toSearch[gcsConfig.Bucket].Insert(gcsPath) 231 } 232 return toSearch, nil 233 } 234 235 func getPRHistory(url *url.URL, config *config.Config, gcsClient *storage.Client) (prHistoryTemplate, error) { 236 start := time.Now() 237 template := prHistoryTemplate{} 238 239 key := strings.TrimPrefix(url.Path, "/pr-history/") 240 org, repo, pr, err := parsePullKey(key) 241 if err != nil { 242 return template, fmt.Errorf("failed to parse URL: %v", err) 243 } 244 template.Name = fmt.Sprintf("%s/%s #%d", org, repo, pr) 245 template.Link = githubPRLink(org, repo, pr) 246 247 toSearch, err := getGCSDirsForPR(config, org, repo, pr) 248 if err != nil { 249 return template, fmt.Errorf("failed to list GCS directories for PR %s: %v", template.Name, err) 250 } 251 252 builds := []buildData{} 253 // job name -> commit hash -> list of builds 254 jobCommitBuilds := make(map[string]map[string][]buildData) 255 256 for bucketName, gcsPaths := range toSearch { 257 bucket := gcsBucket{bucketName, gcsClient.Bucket(bucketName)} 258 for gcsPath := range gcsPaths { 259 jobPrefixes, err := bucket.listSubDirs(gcsPath) 260 if err != nil { 261 return template, fmt.Errorf("failed to get job names: %v", err) 262 } 263 // We assume job names to be unique, as enforced during config validation. 264 for _, jobPrefix := range jobPrefixes { 265 jobName := path.Base(jobPrefix) 266 jobData := prJobData{ 267 Name: jobName, 268 Link: jobHistLink(bucketName, jobName), 269 } 270 template.Jobs = append(template.Jobs, jobData) 271 jobCommitBuilds[jobName] = make(map[string][]buildData) 272 } 273 jobs := listJobBuilds(bucket, jobPrefixes) 274 builds = append(builds, getPRBuildData(bucket, jobs)...) 275 } 276 } 277 278 commits := make(map[string]*commitData) 279 for _, build := range builds { 280 jobName := build.jobName 281 hash := build.commitHash 282 jobCommitBuilds[jobName][hash] = append(jobCommitBuilds[jobName][hash], build) 283 updateCommitData(commits, org, repo, hash, build.Started, len(jobCommitBuilds[jobName][hash])) 284 } 285 for _, commit := range commits { 286 template.Commits = append(template.Commits, *commit) 287 } 288 // builds are grouped by commit, then sorted by build start time (newest-first) 289 sort.Sort(sort.Reverse(latestCommit(template.Commits))) 290 for i, job := range template.Jobs { 291 for _, commit := range template.Commits { 292 builds := jobCommitBuilds[job.Name][commit.Hash] 293 sort.Sort(sort.Reverse(byStarted(builds))) 294 template.Jobs[i].Builds = append(template.Jobs[i].Builds, builds...) 295 // pad empty spaces 296 for k := len(builds); k < commit.MaxWidth; k++ { 297 template.Jobs[i].Builds = append(template.Jobs[i].Builds, buildData{}) 298 } 299 } 300 } 301 302 elapsed := time.Now().Sub(start) 303 logrus.Infof("loaded %s in %v", url.Path, elapsed) 304 305 return template, nil 306 }