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