github.com/shashidharatd/test-infra@v0.0.0-20171006011030-71304e1ca560/prow/cmd/splice/main.go (about)

     1  /*
     2  Copyright 2016 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  	"encoding/json"
    21  	"flag"
    22  	"fmt"
    23  	"io/ioutil"
    24  	"net/http"
    25  	"os"
    26  	"os/exec"
    27  	"strings"
    28  	"time"
    29  
    30  	log "github.com/sirupsen/logrus"
    31  
    32  	"k8s.io/test-infra/prow/config"
    33  	"k8s.io/test-infra/prow/kube"
    34  	"k8s.io/test-infra/prow/pjutil"
    35  )
    36  
    37  var (
    38  	submitQueueURL = flag.String("submit-queue-endpoint", "http://submit-queue.k8s.io/github-e2e-queue", "Submit Queue status URL")
    39  	remoteURL      = flag.String("remote-url", "https://github.com/kubernetes/kubernetes", "Remote Git URL")
    40  	orgName        = flag.String("org", "kubernetes", "Org name")
    41  	repoName       = flag.String("repo", "kubernetes", "Repo name")
    42  	configPath     = flag.String("config-path", "/etc/config/config", "Path to config.yaml.")
    43  	maxBatchSize   = flag.Int("batch-size", 5, "Maximum batch size")
    44  )
    45  
    46  // Call a binary and return its output and success status.
    47  func call(binary string, args ...string) (string, error) {
    48  	cmdout := "+ " + binary + " "
    49  	for _, arg := range args {
    50  		cmdout += arg + " "
    51  	}
    52  	log.Info(cmdout)
    53  
    54  	cmd := exec.Command(binary, args...)
    55  	output, err := cmd.CombinedOutput()
    56  	return string(output), err
    57  }
    58  
    59  // getQueuedPRs reads the list of queued PRs from the Submit Queue.
    60  func getQueuedPRs(url string) ([]int, error) {
    61  	resp, err := http.Get(url)
    62  	if err != nil {
    63  		return nil, err
    64  	}
    65  	defer resp.Body.Close()
    66  	body, err := ioutil.ReadAll(resp.Body)
    67  	if err != nil {
    68  		return nil, err
    69  	}
    70  
    71  	queue := struct {
    72  		E2EQueue []struct {
    73  			Number  int
    74  			BaseRef string
    75  		}
    76  	}{}
    77  	err = json.Unmarshal(body, &queue)
    78  	if err != nil {
    79  		return nil, err
    80  	}
    81  
    82  	ret := []int{}
    83  	for _, e := range queue.E2EQueue {
    84  		if e.BaseRef == "" || e.BaseRef == "master" {
    85  			ret = append(ret, e.Number)
    86  		}
    87  	}
    88  	return ret, nil
    89  }
    90  
    91  // Splicer manages a git repo in specific directory.
    92  type splicer struct {
    93  	dir string // The repository location.
    94  }
    95  
    96  // makeSplicer returns a splicer in a new temporary directory,
    97  // with an initial .git dir created.
    98  func makeSplicer() (*splicer, error) {
    99  	dir, err := ioutil.TempDir("", "splice_")
   100  	if err != nil {
   101  		return nil, err
   102  	}
   103  	s := &splicer{dir}
   104  	err = s.gitCalls([][]string{
   105  		{"init"},
   106  		{"config", "--local", "user.name", "K8S Prow Splice"},
   107  		{"config", "--local", "user.email", "splice@localhost"},
   108  	})
   109  	if err != nil {
   110  		s.cleanup()
   111  		return nil, err
   112  	}
   113  	log.Infof("Splicer created in %s.", dir)
   114  	return s, nil
   115  }
   116  
   117  // cleanup recurisvely deletes the repository
   118  func (s *splicer) cleanup() {
   119  	os.RemoveAll(s.dir)
   120  }
   121  
   122  // gitCall is a helper to call `git -C $path $args`.
   123  func (s *splicer) gitCall(args ...string) error {
   124  	fullArgs := append([]string{"-C", s.dir}, args...)
   125  	output, err := call("git", fullArgs...)
   126  	if len(output) > 0 {
   127  		log.Info(output)
   128  	}
   129  	return err
   130  }
   131  
   132  // gitCalls is a helper to chain repeated gitCall invocations,
   133  // returning the first failure, or nil if they all succeeded.
   134  func (s *splicer) gitCalls(argsList [][]string) error {
   135  	for _, args := range argsList {
   136  		err := s.gitCall(args...)
   137  		if err != nil {
   138  			return err
   139  		}
   140  	}
   141  	return nil
   142  }
   143  
   144  // findMergeable fetches given PRs from upstream, merges them locally,
   145  // and finally returns a list of PRs that can be merged without conflicts.
   146  func (s *splicer) findMergeable(remote string, prs []int) ([]int, error) {
   147  	args := []string{"fetch", "-f", remote, "master:master"}
   148  	for _, pr := range prs {
   149  		args = append(args, fmt.Sprintf("pull/%d/head:pr/%d", pr, pr))
   150  	}
   151  
   152  	err := s.gitCalls([][]string{
   153  		{"reset", "--hard"},
   154  		{"checkout", "--orphan", "blank"},
   155  		{"reset", "--hard"},
   156  		{"clean", "-fdx"},
   157  		args,
   158  		{"checkout", "-B", "batch", "master"},
   159  	})
   160  	if err != nil {
   161  		return nil, err
   162  	}
   163  
   164  	out := []int{}
   165  	for _, pr := range prs {
   166  		err := s.gitCall("merge", "--no-ff", "--no-stat",
   167  			"-m", fmt.Sprintf("merge #%d", pr),
   168  			fmt.Sprintf("pr/%d", pr))
   169  		if err != nil {
   170  			// merge conflict: cleanup and move on
   171  			err = s.gitCall("merge", "--abort")
   172  			if err != nil {
   173  				return nil, err
   174  			}
   175  			continue
   176  		}
   177  		out = append(out, pr)
   178  	}
   179  	return out, nil
   180  }
   181  
   182  // gitRef returns the SHA for the given git object-- a branch, generally.
   183  func (s *splicer) gitRef(ref string) string {
   184  	output, err := call("git", "-C", s.dir, "rev-parse", ref)
   185  	if err != nil {
   186  		return ""
   187  	}
   188  	return strings.TrimSpace(output)
   189  }
   190  
   191  // Produce a kube.Refs for the given pull requests. This involves computing the
   192  // git ref for master and the PRs.
   193  func (s *splicer) makeBuildRefs(org, repo string, prs []int) kube.Refs {
   194  	refs := kube.Refs{
   195  		Org:     org,
   196  		Repo:    repo,
   197  		BaseRef: "master",
   198  		BaseSHA: s.gitRef("master"),
   199  	}
   200  	for _, pr := range prs {
   201  		branch := fmt.Sprintf("pr/%d", pr)
   202  		refs.Pulls = append(refs.Pulls, kube.Pull{Number: pr, SHA: s.gitRef(branch)})
   203  	}
   204  	return refs
   205  }
   206  
   207  // Filters to the list of jobs which already passed this commit
   208  func completedJobs(currentJobs []kube.ProwJob, refs kube.Refs) []kube.ProwJob {
   209  	var skippable []kube.ProwJob
   210  	rs := refs.String()
   211  
   212  	for _, job := range currentJobs {
   213  		if job.Spec.Type != kube.BatchJob {
   214  			continue
   215  		}
   216  		if !job.Complete() {
   217  			continue
   218  		}
   219  		if job.Status.State != kube.SuccessState {
   220  			continue
   221  		}
   222  		if job.Spec.Refs.String() != rs {
   223  			continue
   224  		}
   225  		skippable = append(skippable, job)
   226  	}
   227  	return skippable
   228  }
   229  
   230  // Filters to the list of required presubmits that report
   231  func requiredPresubmits(presubmits []config.Presubmit) []config.Presubmit {
   232  	var out []config.Presubmit
   233  	for _, job := range presubmits {
   234  		if !job.AlwaysRun { // Ignore manual jobs as these do not block
   235  			continue
   236  		}
   237  		if job.SkipReport { // Ignore silent jobs as these do not block
   238  			continue
   239  		}
   240  		out = append(out, job)
   241  	}
   242  	return out
   243  }
   244  
   245  // Filters to the list of required presubmit which have not already passed this commit
   246  func neededPresubmits(presubmits []config.Presubmit, currentJobs []kube.ProwJob, refs kube.Refs) []config.Presubmit {
   247  	skippable := make(map[string]bool)
   248  	for _, job := range completedJobs(currentJobs, refs) {
   249  		skippable[job.Spec.Context] = true
   250  	}
   251  
   252  	var needed []config.Presubmit
   253  	for _, job := range requiredPresubmits(presubmits) {
   254  		if skippable[job.Context] {
   255  			continue
   256  		}
   257  		needed = append(needed, job)
   258  	}
   259  	return needed
   260  }
   261  
   262  func main() {
   263  	flag.Parse()
   264  	log.SetFormatter(&log.JSONFormatter{})
   265  
   266  	splicer, err := makeSplicer()
   267  	if err != nil {
   268  		log.WithError(err).Fatal("Could not make splicer.")
   269  	}
   270  	defer splicer.cleanup()
   271  
   272  	configAgent := &config.Agent{}
   273  	if err := configAgent.Start(*configPath); err != nil {
   274  		log.WithError(err).Fatal("Error starting config agent.")
   275  	}
   276  
   277  	kc, err := kube.NewClientInCluster(configAgent.Config().ProwJobNamespace)
   278  	if err != nil {
   279  		log.WithError(err).Fatal("Error getting kube client.")
   280  	}
   281  
   282  	cooldown := 0
   283  	// Loop endlessly, sleeping a minute between iterations
   284  	for range time.Tick(1 * time.Minute) {
   285  		start := time.Now()
   286  		// List batch jobs, only start a new one if none are active.
   287  		currentJobs, err := kc.ListProwJobs(nil)
   288  		if err != nil {
   289  			log.WithError(err).Error("Error listing prow jobs.")
   290  			continue
   291  		}
   292  
   293  		running := []string{}
   294  		for _, job := range currentJobs {
   295  			if job.Spec.Type != kube.BatchJob {
   296  				continue
   297  			}
   298  			if !job.Complete() {
   299  				running = append(running, job.Spec.Job)
   300  			}
   301  		}
   302  		if len(running) > 0 {
   303  			log.Infof("Waiting on %d jobs: %v", len(running), running)
   304  			continue
   305  		}
   306  
   307  		// Start a new batch if the cooldown is 0, otherwise wait. This gives
   308  		// the SQ some time to merge before we start a new batch.
   309  		if cooldown > 0 {
   310  			cooldown--
   311  			continue
   312  		}
   313  
   314  		queue, err := getQueuedPRs(*submitQueueURL)
   315  		if err != nil {
   316  			log.WithError(err).Warning("Error getting queued PRs. Is the submit queue down?")
   317  			continue
   318  		}
   319  		// No need to check for mergeable PRs if none is in the queue.
   320  		if len(queue) == 0 {
   321  			continue
   322  		}
   323  		log.Infof("PRs in queue: %v", queue)
   324  		batchPRs, err := splicer.findMergeable(*remoteURL, queue)
   325  		if err != nil {
   326  			log.WithError(err).Error("Error computing mergeable PRs.")
   327  			continue
   328  		}
   329  		// No need to start batches for single PRs
   330  		if len(batchPRs) <= 1 {
   331  			continue
   332  		}
   333  		// Trim down to the desired batch size.
   334  		if len(batchPRs) > *maxBatchSize {
   335  			batchPRs = batchPRs[:*maxBatchSize]
   336  		}
   337  		log.Infof("Starting a batch for the following PRs: %v", batchPRs)
   338  		refs := splicer.makeBuildRefs(*orgName, *repoName, batchPRs)
   339  		presubmits := configAgent.Config().Presubmits[fmt.Sprintf("%s/%s", *orgName, *repoName)]
   340  		for _, job := range neededPresubmits(presubmits, currentJobs, refs) {
   341  			if _, err := kc.CreateProwJob(pjutil.NewProwJob(pjutil.BatchSpec(job, refs))); err != nil {
   342  				log.WithError(err).WithField("job", job.Name).Error("Error starting batch job.")
   343  			}
   344  		}
   345  		cooldown = 5
   346  		log.Infof("Sync time: %v", time.Since(start))
   347  	}
   348  }