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  }