github.com/abayer/test-infra@v0.0.5/prow/plugins/trigger/trigger.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 trigger
    18  
    19  import (
    20  	"fmt"
    21  	"strings"
    22  
    23  	"github.com/sirupsen/logrus"
    24  
    25  	"k8s.io/apimachinery/pkg/util/sets"
    26  
    27  	"k8s.io/test-infra/prow/config"
    28  	"k8s.io/test-infra/prow/github"
    29  	"k8s.io/test-infra/prow/kube"
    30  	"k8s.io/test-infra/prow/pjutil"
    31  	"k8s.io/test-infra/prow/pluginhelp"
    32  	"k8s.io/test-infra/prow/plugins"
    33  )
    34  
    35  const (
    36  	pluginName = "trigger"
    37  	lgtmLabel  = "lgtm"
    38  )
    39  
    40  func init() {
    41  	plugins.RegisterIssueCommentHandler(pluginName, handleIssueComment, helpProvider)
    42  	plugins.RegisterPullRequestHandler(pluginName, handlePullRequest, helpProvider)
    43  	plugins.RegisterPushEventHandler(pluginName, handlePush, helpProvider)
    44  }
    45  
    46  func helpProvider(config *plugins.Configuration, enabledRepos []string) (*pluginhelp.PluginHelp, error) {
    47  	configInfo := map[string]string{}
    48  	for _, orgRepo := range enabledRepos {
    49  		parts := strings.Split(orgRepo, "/")
    50  		if len(parts) != 2 {
    51  			return nil, fmt.Errorf("invalid repo in enabledRepos: %q", orgRepo)
    52  		}
    53  		org, repoName := parts[0], parts[1]
    54  		if trigger := config.TriggerFor(org, repoName); trigger != nil && trigger.TrustedOrg != "" {
    55  			org = trigger.TrustedOrg
    56  		}
    57  		configInfo[orgRepo] = fmt.Sprintf("The trusted Github organization for this repository is %q.", org)
    58  	}
    59  	pluginHelp := &pluginhelp.PluginHelp{
    60  		Description: `The trigger plugin starts tests in reaction to commands and pull request events. It is responsible for ensuring that test jobs are only run on trusted PRs. A PR is considered trusted if the author is a member of the 'trusted organization' for the repository or if such a member has left an '/ok-to-test' command on the PR.
    61  <br>Trigger starts jobs automatically when a new trusted PR is created or when an untrusted PR becomes trusted, but it can also be used to start jobs manually via the '/test' command.
    62  <br>The '/retest' command can be used to rerun jobs that have reported failure.`,
    63  		Config: configInfo,
    64  	}
    65  	pluginHelp.AddCommand(pluginhelp.Command{
    66  		Usage:       "/ok-to-test",
    67  		Description: "Marks a PR as 'trusted' and starts tests.",
    68  		Featured:    false,
    69  		WhoCanUse:   "Members of the trusted organization for the repo.",
    70  		Examples:    []string{"/ok-to-test"},
    71  	})
    72  	pluginHelp.AddCommand(pluginhelp.Command{
    73  		Usage:       "/test (<job name>|all)",
    74  		Description: "Manually starts a/all test job(s).",
    75  		Featured:    true,
    76  		WhoCanUse:   "Anyone can trigger this command on a trusted PR.",
    77  		Examples:    []string{"/test all", "/test pull-bazel-test"},
    78  	})
    79  	pluginHelp.AddCommand(pluginhelp.Command{
    80  		Usage:       "/retest",
    81  		Description: "Rerun test jobs that have failed.",
    82  		Featured:    true,
    83  		WhoCanUse:   "Anyone can trigger this command on a trusted PR.",
    84  		Examples:    []string{"/retest"},
    85  	})
    86  	return pluginHelp, nil
    87  }
    88  
    89  type githubClient interface {
    90  	AddLabel(org, repo string, number int, label string) error
    91  	BotName() (string, error)
    92  	IsCollaborator(org, repo, user string) (bool, error)
    93  	IsMember(org, user string) (bool, error)
    94  	GetPullRequest(org, repo string, number int) (*github.PullRequest, error)
    95  	GetRef(org, repo, ref string) (string, error)
    96  	CreateComment(owner, repo string, number int, comment string) error
    97  	ListIssueComments(owner, repo string, issue int) ([]github.IssueComment, error)
    98  	CreateStatus(owner, repo, ref string, status github.Status) error
    99  	GetCombinedStatus(org, repo, ref string) (*github.CombinedStatus, error)
   100  	GetPullRequestChanges(org, repo string, number int) ([]github.PullRequestChange, error)
   101  	RemoveLabel(org, repo string, number int, label string) error
   102  	DeleteStaleComments(org, repo string, number int, comments []github.IssueComment, isStale func(github.IssueComment) bool) error
   103  }
   104  
   105  type kubeClient interface {
   106  	CreateProwJob(kube.ProwJob) (kube.ProwJob, error)
   107  }
   108  
   109  type client struct {
   110  	GitHubClient githubClient
   111  	KubeClient   kubeClient
   112  	Config       *config.Config
   113  	Logger       *logrus.Entry
   114  }
   115  
   116  func getClient(pc plugins.PluginClient) client {
   117  	return client{
   118  		GitHubClient: pc.GitHubClient,
   119  		Config:       pc.Config,
   120  		KubeClient:   pc.KubeClient,
   121  		Logger:       pc.Logger,
   122  	}
   123  }
   124  
   125  func handlePullRequest(pc plugins.PluginClient, pr github.PullRequestEvent) error {
   126  	org, repo, _ := orgRepoAuthor(pr.PullRequest)
   127  	return handlePR(getClient(pc), pc.PluginConfig.TriggerFor(org, repo), pr)
   128  }
   129  
   130  func handleIssueComment(pc plugins.PluginClient, ic github.IssueCommentEvent) error {
   131  	return handleIC(getClient(pc), pc.PluginConfig.TriggerFor(ic.Repo.Owner.Login, ic.Repo.Name), ic)
   132  }
   133  
   134  func handlePush(pc plugins.PluginClient, pe github.PushEvent) error {
   135  	return handlePE(getClient(pc), pe)
   136  }
   137  
   138  // trustedUser returns true if user is trusted in repo.
   139  //
   140  // Trusted users are either repo collaborators, org members or trusted org members.
   141  // Whether repo collaborators and/or a second org is trusted is configured by trigger.
   142  func trustedUser(ghc githubClient, trigger *plugins.Trigger, user, org, repo string) (bool, error) {
   143  	// First check if user is a collaborator, assuming this is allowed
   144  	allowCollaborators := trigger == nil || !trigger.OnlyOrgMembers
   145  	if allowCollaborators {
   146  		if ok, err := ghc.IsCollaborator(org, repo, user); err != nil {
   147  			return false, fmt.Errorf("error in IsCollaborator: %v", err)
   148  		} else if ok {
   149  			return true, nil
   150  		}
   151  	}
   152  
   153  	// TODO(fejta): consider dropping support for org checks in the future.
   154  
   155  	// Next see if the user is an org member
   156  	if member, err := ghc.IsMember(org, user); err != nil {
   157  		return false, fmt.Errorf("error in IsMember(%s): %v", org, err)
   158  	} else if member {
   159  		return true, nil
   160  	}
   161  
   162  	// Determine if there is a second org to check
   163  	if trigger == nil || trigger.TrustedOrg == "" || trigger.TrustedOrg == org {
   164  		return false, nil // No trusted org and/or it is the same
   165  	}
   166  
   167  	// Check the second trusted org.
   168  	member, err := ghc.IsMember(trigger.TrustedOrg, user)
   169  	if err != nil {
   170  		return false, fmt.Errorf("error in IsMember(%s): %v", trigger.TrustedOrg, err)
   171  	}
   172  	return member, nil
   173  }
   174  
   175  func fileChangesGetter(ghc githubClient, org, repo string, num int) func() ([]string, error) {
   176  	var changedFiles []string
   177  	return func() ([]string, error) {
   178  		// Fetch the changed files from github at most once.
   179  		if changedFiles == nil {
   180  			changes, err := ghc.GetPullRequestChanges(org, repo, num)
   181  			if err != nil {
   182  				return nil, fmt.Errorf("error getting pull request changes: %v", err)
   183  			}
   184  			changedFiles = []string{}
   185  			for _, change := range changes {
   186  				changedFiles = append(changedFiles, change.Filename)
   187  			}
   188  		}
   189  		return changedFiles, nil
   190  	}
   191  }
   192  
   193  func allContexts(parent config.Presubmit) []string {
   194  	contexts := []string{parent.Context}
   195  	for _, child := range parent.RunAfterSuccess {
   196  		contexts = append(contexts, allContexts(child)...)
   197  	}
   198  	return contexts
   199  }
   200  
   201  func runOrSkipRequested(c client, pr *github.PullRequest, requestedJobs []config.Presubmit, forceRunContexts map[string]bool, body, eventGUID string) error {
   202  	org := pr.Base.Repo.Owner.Login
   203  	repo := pr.Base.Repo.Name
   204  	number := pr.Number
   205  
   206  	baseSHA, err := c.GitHubClient.GetRef(org, repo, "heads/"+pr.Base.Ref)
   207  	if err != nil {
   208  		return err
   209  	}
   210  
   211  	// Use a closure to lazily retrieve the file changes only if they are needed.
   212  	// We only have to fetch the changes if there is at least one RunIfChanged
   213  	// job that is not being force run (due to a `/retest` after a failure or
   214  	// because it is explicitly triggered with `/test foo`).
   215  	getChanges := fileChangesGetter(c.GitHubClient, org, repo, number)
   216  	// shouldRun indicates if a job should actually run.
   217  	shouldRun := func(j config.Presubmit) (bool, error) {
   218  		if !j.RunsAgainstBranch(pr.Base.Ref) {
   219  			return false, nil
   220  		}
   221  		if j.RunIfChanged == "" || forceRunContexts[j.Context] || j.TriggerMatches(body) {
   222  			return true, nil
   223  		}
   224  		changes, err := getChanges()
   225  		if err != nil {
   226  			return false, err
   227  		}
   228  		return j.RunsAgainstChanges(changes), nil
   229  	}
   230  
   231  	// For each job determine if any sharded version of the job runs.
   232  	// This in turn determines which jobs to run and which contexts to mark as "Skipped".
   233  	//
   234  	// Note: Job sharding is achieved with presubmit configurations that overlap on
   235  	// name, but run under disjoint circumstances. For example, a job 'foo' can be
   236  	// sharded to have different pod specs for different branches by
   237  	// creating 2 presubmit configurations with the name foo, but different pod
   238  	// specs, and specifying different branches for each job.
   239  	var toRunJobs []config.Presubmit
   240  	toRun := sets.NewString()
   241  	toSkip := sets.NewString()
   242  	for _, job := range requestedJobs {
   243  		runs, err := shouldRun(job)
   244  		if err != nil {
   245  			return err
   246  		}
   247  		if runs {
   248  			toRunJobs = append(toRunJobs, job)
   249  			toRun.Insert(job.Context)
   250  		} else if !job.SkipReport {
   251  			// we need to post context statuses for all jobs; if a job is slated to
   252  			// run after the success of a parent job that is skipped, it must be
   253  			// skipped as well
   254  			toSkip.Insert(allContexts(job)...)
   255  		}
   256  	}
   257  	// 'Skip' any context that is requested, but doesn't have any job shards that
   258  	// will run.
   259  	for _, context := range toSkip.Difference(toRun).List() {
   260  		if err := c.GitHubClient.CreateStatus(org, repo, pr.Head.SHA, github.Status{
   261  			State:       github.StatusSuccess,
   262  			Context:     context,
   263  			Description: "Skipped",
   264  		}); err != nil {
   265  			return err
   266  		}
   267  	}
   268  
   269  	var errors []error
   270  	for _, job := range toRunJobs {
   271  		c.Logger.Infof("Starting %s build.", job.Name)
   272  		pj := pjutil.NewPresubmit(*pr, baseSHA, job, eventGUID)
   273  		c.Logger.WithFields(pjutil.ProwJobFields(&pj)).Info("Creating a new prowjob.")
   274  		if _, err := c.KubeClient.CreateProwJob(pj); err != nil {
   275  			errors = append(errors, err)
   276  		}
   277  	}
   278  	if len(errors) > 0 {
   279  		return fmt.Errorf("errors starting jobs: %v", errors)
   280  	}
   281  	return nil
   282  }