github.com/abayer/test-infra@v0.0.5/prow/plugins/override/override.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 override supports the /override context command.
    18  package override
    19  
    20  import (
    21  	"fmt"
    22  	"regexp"
    23  	"strings"
    24  
    25  	"github.com/sirupsen/logrus"
    26  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    27  	"k8s.io/apimachinery/pkg/util/sets"
    28  
    29  	"k8s.io/test-infra/prow/config"
    30  	"k8s.io/test-infra/prow/github"
    31  	"k8s.io/test-infra/prow/kube"
    32  	"k8s.io/test-infra/prow/pjutil"
    33  	"k8s.io/test-infra/prow/pluginhelp"
    34  	"k8s.io/test-infra/prow/plugins"
    35  )
    36  
    37  const pluginName = "override"
    38  
    39  var (
    40  	overrideRe = regexp.MustCompile(`(?mi)^/override (.+?)\s*$`)
    41  )
    42  
    43  type githubClient interface {
    44  	CreateComment(owner, repo string, number int, comment string) error
    45  	CreateStatus(org, repo, ref string, s github.Status) error
    46  	GetPullRequest(org, repo string, number int) (*github.PullRequest, error)
    47  	GetRef(org, repo, ref string) (string, error)
    48  	HasPermission(org, repo, user string, role ...string) (bool, error)
    49  	ListStatuses(org, repo, ref string) ([]github.Status, error)
    50  }
    51  
    52  type kubeClient interface {
    53  	CreateProwJob(kube.ProwJob) (kube.ProwJob, error)
    54  }
    55  
    56  type overrideClient interface {
    57  	githubClient
    58  	kubeClient
    59  	presubmitForContext(org, repo, context string) *config.Presubmit
    60  }
    61  
    62  type client struct {
    63  	gc githubClient
    64  	jc config.JobConfig
    65  	kc kubeClient
    66  }
    67  
    68  func (c client) CreateComment(owner, repo string, number int, comment string) error {
    69  	return c.gc.CreateComment(owner, repo, number, comment)
    70  }
    71  func (c client) CreateStatus(org, repo, ref string, s github.Status) error {
    72  	return c.gc.CreateStatus(org, repo, ref, s)
    73  }
    74  
    75  func (c client) GetRef(org, repo, ref string) (string, error) {
    76  	return c.gc.GetRef(org, repo, ref)
    77  }
    78  
    79  func (c client) GetPullRequest(org, repo string, number int) (*github.PullRequest, error) {
    80  	return c.gc.GetPullRequest(org, repo, number)
    81  }
    82  func (c client) ListStatuses(org, repo, ref string) ([]github.Status, error) {
    83  	return c.gc.ListStatuses(org, repo, ref)
    84  }
    85  func (c client) HasPermission(org, repo, user string, role ...string) (bool, error) {
    86  	return c.gc.HasPermission(org, repo, user, role...)
    87  }
    88  
    89  func (c client) CreateProwJob(pj kube.ProwJob) (kube.ProwJob, error) {
    90  	return c.kc.CreateProwJob(pj)
    91  }
    92  
    93  func (c client) presubmitForContext(org, repo, context string) *config.Presubmit {
    94  	for _, p := range c.jc.AllPresubmits([]string{org + "/" + repo}) {
    95  		if p.Context == context {
    96  			return &p
    97  		}
    98  	}
    99  	return nil
   100  }
   101  
   102  func init() {
   103  	plugins.RegisterGenericCommentHandler(pluginName, handleGenericComment, helpProvider)
   104  }
   105  
   106  func helpProvider(config *plugins.Configuration, enabledRepos []string) (*pluginhelp.PluginHelp, error) {
   107  	pluginHelp := &pluginhelp.PluginHelp{
   108  		Description: "The override plugin allows repo admins to force a github status context to pass",
   109  	}
   110  	pluginHelp.AddCommand(pluginhelp.Command{
   111  		Usage:       "/override [context]",
   112  		Description: "Forces a github status context to green (one per line).",
   113  		Featured:    false,
   114  		WhoCanUse:   "Repo administrators",
   115  		Examples:    []string{"/override pull-repo-whatever", "/override continuous-integration/travis\n/override deleted-job"},
   116  	})
   117  	return pluginHelp, nil
   118  }
   119  
   120  func handleGenericComment(pc plugins.PluginClient, e github.GenericCommentEvent) error {
   121  	c := client{
   122  		gc: pc.GitHubClient,
   123  		jc: pc.Config.JobConfig,
   124  		kc: pc.KubeClient,
   125  	}
   126  	return handle(c, pc.Logger, &e)
   127  }
   128  
   129  func authorized(gc githubClient, log *logrus.Entry, org, repo, user string) bool {
   130  	ok, err := gc.HasPermission(org, repo, user, github.RoleAdmin)
   131  	if err != nil {
   132  		log.WithError(err).Warnf("cannot determine whether %s is an admin of %s/%s", user, org, repo)
   133  		return false
   134  	}
   135  	return ok
   136  }
   137  
   138  func description(user string) string {
   139  	return fmt.Sprintf("Overridden by %s", user)
   140  }
   141  
   142  func handle(oc overrideClient, log *logrus.Entry, e *github.GenericCommentEvent) error {
   143  
   144  	if !e.IsPR || e.IssueState != "open" || e.Action != github.GenericCommentActionCreated {
   145  		return nil
   146  	}
   147  
   148  	mat := overrideRe.FindAllStringSubmatch(e.Body, -1)
   149  	if len(mat) == 0 {
   150  		return nil
   151  	}
   152  
   153  	overrides := sets.String{}
   154  
   155  	for _, m := range mat {
   156  		overrides.Insert(m[1])
   157  	}
   158  
   159  	org := e.Repo.Owner.Login
   160  	repo := e.Repo.Name
   161  	number := e.Number
   162  	user := e.User.Login
   163  
   164  	if !authorized(oc, log, org, repo, user) {
   165  		resp := fmt.Sprintf("%s unauthorized: /override is restricted to repo administrators", user)
   166  		log.Warn(resp)
   167  		return oc.CreateComment(org, repo, number, plugins.FormatResponseRaw(e.Body, e.HTMLURL, user, resp))
   168  	}
   169  
   170  	pr, err := oc.GetPullRequest(org, repo, number)
   171  	if err != nil {
   172  		resp := fmt.Sprintf("Cannot get PR #%d in %s/%s", number, org, repo)
   173  		log.WithError(err).Warn(resp)
   174  		return oc.CreateComment(org, repo, number, plugins.FormatResponseRaw(e.Body, e.HTMLURL, user, resp))
   175  	}
   176  
   177  	sha := pr.Head.SHA
   178  	statuses, err := oc.ListStatuses(org, repo, sha)
   179  	if err != nil {
   180  		resp := fmt.Sprintf("Cannot get commit statuses for PR #%d in %s/%s", number, org, repo)
   181  		log.WithError(err).Warn(resp)
   182  		return oc.CreateComment(org, repo, number, plugins.FormatResponseRaw(e.Body, e.HTMLURL, user, resp))
   183  	}
   184  
   185  	done := sets.String{}
   186  
   187  	defer func() {
   188  		if len(done) == 0 {
   189  			return
   190  		}
   191  		msg := fmt.Sprintf("Overrode contexts on behalf of %s: %s", user, strings.Join(done.List(), ", "))
   192  		log.Info(msg)
   193  		oc.CreateComment(org, repo, number, plugins.FormatResponseRaw(e.Body, e.HTMLURL, user, msg))
   194  	}()
   195  
   196  	for _, status := range statuses {
   197  		if status.State == github.StatusSuccess || !overrides.Has(status.Context) {
   198  			continue
   199  		}
   200  		// First create the overridden prow result if necessary
   201  		if pre := oc.presubmitForContext(org, repo, status.Context); pre != nil {
   202  			baseSHA, err := oc.GetRef(org, repo, "heads/"+pr.Base.Ref)
   203  			if err != nil {
   204  				resp := fmt.Sprintf("Cannot get base ref of PR")
   205  				log.WithError(err).Warn(resp)
   206  				return oc.CreateComment(org, repo, number, plugins.FormatResponseRaw(e.Body, e.HTMLURL, user, resp))
   207  			}
   208  
   209  			pj := pjutil.NewPresubmit(*pr, baseSHA, *pre, e.GUID)
   210  			now := metav1.Now()
   211  			pj.Status = kube.ProwJobStatus{
   212  				StartTime:      now,
   213  				CompletionTime: &now,
   214  				State:          kube.SuccessState,
   215  				Description:    description(user),
   216  				URL:            e.HTMLURL,
   217  			}
   218  			log.WithFields(pjutil.ProwJobFields(&pj)).Info("Creating a new prowjob.")
   219  			if _, err := oc.CreateProwJob(pj); err != nil {
   220  				resp := fmt.Sprintf("Failed to create override job for %s", status.Context)
   221  				log.WithError(err).Warn(resp)
   222  				return oc.CreateComment(org, repo, number, plugins.FormatResponseRaw(e.Body, e.HTMLURL, user, resp))
   223  			}
   224  		}
   225  		status.State = github.StatusSuccess
   226  		status.Description = description(user)
   227  		if err := oc.CreateStatus(org, repo, sha, status); err != nil {
   228  			resp := fmt.Sprintf("Cannot update PR status for context %s", status.Context)
   229  			log.WithError(err).Warn(resp)
   230  			return oc.CreateComment(org, repo, number, plugins.FormatResponseRaw(e.Body, e.HTMLURL, user, resp))
   231  		}
   232  		done.Insert(status.Context)
   233  	}
   234  	return nil
   235  }