sigs.k8s.io/prow@v0.0.0-20240503223140-c5e374dc7eb1/cmd/deck/rerun.go (about)

     1  /*
     2  Copyright 2022 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  	"context"
    21  	"errors"
    22  	"fmt"
    23  	"net/http"
    24  
    25  	"github.com/sirupsen/logrus"
    26  	kerrors "k8s.io/apimachinery/pkg/api/errors"
    27  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    28  	utilerrors "k8s.io/apimachinery/pkg/util/errors"
    29  	"k8s.io/apimachinery/pkg/util/sets"
    30  	prowapi "sigs.k8s.io/prow/pkg/apis/prowjobs/v1"
    31  	prowv1 "sigs.k8s.io/prow/pkg/client/clientset/versioned/typed/prowjobs/v1"
    32  	"sigs.k8s.io/prow/pkg/config"
    33  	gerritsource "sigs.k8s.io/prow/pkg/gerrit/source"
    34  	"sigs.k8s.io/prow/pkg/github"
    35  	"sigs.k8s.io/prow/pkg/githuboauth"
    36  	"sigs.k8s.io/prow/pkg/kube"
    37  	"sigs.k8s.io/prow/pkg/pjutil"
    38  	"sigs.k8s.io/prow/pkg/plugins"
    39  	"sigs.k8s.io/prow/pkg/plugins/trigger"
    40  )
    41  
    42  var (
    43  	// Stores the annotations and labels that are generated
    44  	// and specified within components.
    45  	ComponentSpecifiedAnnotationsAndLabels = sets.New[string](
    46  		// Labels
    47  		kube.GerritRevision,
    48  		kube.GerritPatchset,
    49  		kube.GerritReportLabel,
    50  		github.EventGUID,
    51  		kube.CreatedByTideLabel,
    52  		// Annotations
    53  		kube.GerritID,
    54  		kube.GerritInstance,
    55  	)
    56  )
    57  
    58  func verifyRerunRefs(refs *prowapi.Refs) error {
    59  	var errs []error
    60  	if refs == nil {
    61  		return errors.New("Refs must be supplied")
    62  	}
    63  	if len(refs.Org) == 0 {
    64  		errs = append(errs, errors.New("org must be supplied"))
    65  	}
    66  	if len(refs.Repo) == 0 {
    67  		errs = append(errs, errors.New("repo must be supplied"))
    68  	}
    69  	if len(refs.BaseRef) == 0 {
    70  		errs = append(errs, errors.New("baseRef must be supplied"))
    71  	}
    72  	return utilerrors.NewAggregate(errs)
    73  }
    74  
    75  func setRerunOrgRepo(refs *prowapi.Refs, labels map[string]string) string {
    76  	org, repo := refs.Org, refs.Repo
    77  	orgRepo := org + "/" + repo
    78  	// Normalize prefix to orgRepo if this is a gerrit job.
    79  	// (Unfortunately gerrit jobs use the full repo URL as the identifier.)
    80  	if labels[kube.GerritRevision] != "" && !gerritsource.IsGerritOrg(refs.Org) {
    81  		orgRepo = gerritsource.CloneURIFromOrgRepo(refs.Org, refs.Repo)
    82  	}
    83  	return orgRepo
    84  }
    85  
    86  type preOrPostsubmit interface {
    87  	GetName() string
    88  	CouldRun(string) bool
    89  	GetLabels() map[string]string
    90  	GetAnnotations() map[string]string
    91  }
    92  
    93  func getPreOrPostSpec[p preOrPostsubmit](jobGetter func(string) []p, creator func(p, prowapi.Refs) prowapi.ProwJobSpec, name string, refs *prowapi.Refs, labels map[string]string) (*prowapi.ProwJobSpec, map[string]string, map[string]string, error) {
    94  	if err := verifyRerunRefs(refs); err != nil {
    95  		return nil, nil, nil, err
    96  	}
    97  	var result *p
    98  	branch := refs.BaseRef
    99  	orgRepo := setRerunOrgRepo(refs, labels)
   100  	nameFound := false
   101  	for _, job := range jobGetter(orgRepo) {
   102  		job := job
   103  		if job.GetName() != name {
   104  			continue
   105  		}
   106  		nameFound = true
   107  		if job.CouldRun(branch) { // filter out jobs that are not branch matching
   108  			if result != nil {
   109  				return nil, nil, nil, fmt.Errorf("%s matches multiple prow jobs from orgRepo %q", name, orgRepo)
   110  			}
   111  			result = &job
   112  		}
   113  	}
   114  	if result == nil {
   115  		if nameFound {
   116  			return nil, nil, nil, fmt.Errorf("found job %q, but not allowed to run for orgRepo %q", name, orgRepo)
   117  		} else {
   118  			return nil, nil, nil, fmt.Errorf("failed to find job %q for orgRepo %q", name, orgRepo)
   119  		}
   120  	}
   121  
   122  	prowJobSpec := creator(*result, *refs)
   123  	return &prowJobSpec, (*result).GetLabels(), (*result).GetAnnotations(), nil
   124  }
   125  
   126  func getPresubmitSpec(cfg config.Getter, name string, refs *prowapi.Refs, labels map[string]string) (*prowapi.ProwJobSpec, map[string]string, map[string]string, error) {
   127  	return getPreOrPostSpec(cfg().GetPresubmitsStatic, pjutil.PresubmitSpec, name, refs, labels)
   128  }
   129  
   130  func getPostsubmitSpec(cfg config.Getter, name string, refs *prowapi.Refs, labels map[string]string) (*prowapi.ProwJobSpec, map[string]string, map[string]string, error) {
   131  	return getPreOrPostSpec(cfg().GetPostsubmitsStatic, pjutil.PostsubmitSpec, name, refs, labels)
   132  }
   133  
   134  func getPeriodicSpec(cfg config.Getter, name string) (*prowapi.ProwJobSpec, map[string]string, map[string]string, error) {
   135  	var periodicJob *config.Periodic
   136  	for _, job := range cfg().AllPeriodics() {
   137  		if job.Name == name {
   138  			// Directly followed by break, so this is ok
   139  			// nolint: exportloopref
   140  			periodicJob = &job
   141  			break
   142  		}
   143  	}
   144  	if periodicJob == nil {
   145  		return nil, nil, nil, fmt.Errorf("failed to find associated periodic job %q", name)
   146  	}
   147  	prowJobSpec := pjutil.PeriodicSpec(*periodicJob)
   148  	return &prowJobSpec, periodicJob.Labels, periodicJob.Annotations, nil
   149  }
   150  
   151  func getProwJobSpec(pjType prowapi.ProwJobType, cfg config.Getter, name string, refs *prowapi.Refs, labels map[string]string) (*prowapi.ProwJobSpec, map[string]string, map[string]string, error) {
   152  	switch pjType {
   153  	case prowapi.PeriodicJob:
   154  		return getPeriodicSpec(cfg, name)
   155  	case prowapi.PresubmitJob:
   156  		return getPresubmitSpec(cfg, name, refs, labels)
   157  	case prowapi.PostsubmitJob:
   158  		return getPostsubmitSpec(cfg, name, refs, labels)
   159  	default:
   160  		return nil, nil, nil, fmt.Errorf("Could not create new prowjob: Invalid prowjob type: %q", pjType)
   161  	}
   162  }
   163  
   164  type pluginsCfg func() *plugins.Configuration
   165  
   166  // canTriggerJob determines whether the given user can trigger any job.
   167  func canTriggerJob(user string, pj prowapi.ProwJob, cfg *prowapi.RerunAuthConfig, cli deckGitHubClient, pluginsCfg pluginsCfg, log *logrus.Entry) (bool, error) {
   168  	var org string
   169  	if pj.Spec.Refs != nil {
   170  		org = pj.Spec.Refs.Org
   171  	} else if len(pj.Spec.ExtraRefs) > 0 {
   172  		org = pj.Spec.ExtraRefs[0].Org
   173  	}
   174  
   175  	// Then check config-level rerun auth config.
   176  	if auth, err := cfg.IsAuthorized(org, user, cli); err != nil {
   177  		return false, err
   178  	} else if auth {
   179  		return true, err
   180  	}
   181  
   182  	// Check job-level rerun auth config.
   183  	if auth, err := pj.Spec.RerunAuthConfig.IsAuthorized(org, user, cli); err != nil {
   184  		return false, err
   185  	} else if auth {
   186  		return true, nil
   187  	}
   188  
   189  	if cli == nil {
   190  		log.Warning("No GitHub token was provided, so we cannot retrieve GitHub teams")
   191  		return false, nil
   192  	}
   193  
   194  	// If the job is a presubmit and has an associated PR, and a plugin config is provided,
   195  	// do the same checks as for /test
   196  	if pj.Spec.Type == prowapi.PresubmitJob && pj.Spec.Refs != nil && len(pj.Spec.Refs.Pulls) > 0 {
   197  		if pluginsCfg == nil {
   198  			log.Info("No plugin config was provided so we cannot check if the user would be allowed to use /test.")
   199  		} else {
   200  			pcfg := pluginsCfg()
   201  			pull := pj.Spec.Refs.Pulls[0]
   202  			org := pj.Spec.Refs.Org
   203  			repo := pj.Spec.Refs.Repo
   204  			_, allowed, err := trigger.TrustedPullRequest(cli, pcfg.TriggerFor(org, repo), user, org, repo, pull.Number, nil)
   205  			return allowed, err
   206  		}
   207  	}
   208  	return false, nil
   209  }
   210  
   211  func isAllowedToRerun(r *http.Request, acfg authCfgGetter, goa *githuboauth.Agent, ghc githuboauth.AuthenticatedUserIdentifier, pj prowapi.ProwJob, cli deckGitHubClient, pluginAgent *plugins.ConfigAgent, log *logrus.Entry) (bool, string, error, int) {
   212  	authConfig := acfg(&pj.Spec)
   213  	var allowed bool
   214  	var login string
   215  	if pj.Spec.RerunAuthConfig.IsAllowAnyone() || authConfig.IsAllowAnyone() {
   216  		// Skip getting the users login via GH oauth if anyone is allowed to rerun
   217  		// jobs so that GH oauth doesn't need to be set up for private Prows.
   218  		allowed = true
   219  	} else {
   220  		if goa == nil {
   221  			return allowed, "", errors.New("GitHub oauth must be configured to rerun jobs unless 'allow_anyone: true' is specified."), http.StatusInternalServerError
   222  		}
   223  		var err error
   224  		login, err = goa.GetLogin(r, ghc)
   225  		if err != nil {
   226  			return allowed, "", errors.New("Error retrieving GitHub login."), http.StatusUnauthorized
   227  		}
   228  		log = log.WithField("user", login)
   229  		allowed, err = canTriggerJob(login, pj, authConfig, cli, pluginAgent.Config, log)
   230  		if err != nil {
   231  			return allowed, "", err, http.StatusInternalServerError
   232  		}
   233  	}
   234  	return allowed, login, nil, http.StatusOK
   235  }
   236  
   237  // Valid value for query parameter mode in rerun route
   238  const (
   239  	LATEST = "latest"
   240  )
   241  
   242  // handleRerun triggers a rerun of the given job if that features is enabled, it receives a
   243  // POST request, and the user has the necessary permissions. Otherwise, it writes the config
   244  // for a new job but does not trigger it.
   245  func handleRerun(cfg config.Getter, prowJobClient prowv1.ProwJobInterface, createProwJob bool, acfg authCfgGetter, goa *githuboauth.Agent, ghc githuboauth.AuthenticatedUserIdentifier, cli deckGitHubClient, pluginAgent *plugins.ConfigAgent, log *logrus.Entry) http.HandlerFunc {
   246  	return func(w http.ResponseWriter, r *http.Request) {
   247  		name := r.URL.Query().Get("prowjob")
   248  		mode := r.URL.Query().Get("mode")
   249  		l := log.WithField("prowjob", name)
   250  		if name == "" {
   251  			http.Error(w, "request did not provide the 'prowjob' query parameter", http.StatusBadRequest)
   252  			return
   253  		}
   254  		pj, err := prowJobClient.Get(context.TODO(), name, metav1.GetOptions{})
   255  		if err != nil {
   256  			http.Error(w, fmt.Sprintf("ProwJob not found: %v", err), http.StatusNotFound)
   257  			if !kerrors.IsNotFound(err) {
   258  				// admins only care about errors other than not found
   259  				l.WithError(err).Warning("ProwJob not found.")
   260  			}
   261  			return
   262  		}
   263  		enableScheduling := cfg().Scheduler.Enabled
   264  		var newPJ prowapi.ProwJob
   265  		if mode == LATEST {
   266  			prowJobSpec, labels, annotations, err := getProwJobSpec(pj.Spec.Type, cfg, pj.Spec.Job, pj.Spec.Refs, pj.Labels)
   267  			if err != nil {
   268  				// These are user errors, i.e. missing fields, requested prowjob doesn't exist etc.
   269  				// These errors are already surfaced to user via pubsub two lines below.
   270  				http.Error(w, fmt.Sprintf("Could not create new prowjob: Failed getting prowjob spec: %v", err), http.StatusBadRequest)
   271  				l.WithError(err).Debug("Could not create new prowjob")
   272  				return
   273  			}
   274  
   275  			// Add component specified labels and annotations from original prowjob
   276  			for k, v := range pj.ObjectMeta.Labels {
   277  				if ComponentSpecifiedAnnotationsAndLabels.Has(k) {
   278  					if labels == nil {
   279  						labels = make(map[string]string)
   280  					}
   281  					labels[k] = v
   282  				}
   283  			}
   284  			for k, v := range pj.ObjectMeta.Annotations {
   285  				if ComponentSpecifiedAnnotationsAndLabels.Has(k) {
   286  					if annotations == nil {
   287  						annotations = make(map[string]string)
   288  					}
   289  					annotations[k] = v
   290  				}
   291  			}
   292  
   293  			newPJ = pjutil.NewProwJob(*prowJobSpec, labels, annotations, pjutil.RequireScheduling(enableScheduling))
   294  		} else {
   295  			newPJ = pjutil.NewProwJob(pj.Spec, pj.ObjectMeta.Labels, pj.ObjectMeta.Annotations, pjutil.RequireScheduling(enableScheduling))
   296  		}
   297  		l = l.WithField("job", newPJ.Spec.Job)
   298  		switch r.Method {
   299  		case http.MethodGet:
   300  			handleSerialize(w, "prowjob", newPJ, l)
   301  		case http.MethodPost:
   302  			if !createProwJob {
   303  				http.Error(w, "Direct rerun feature is not enabled. Enable with the '--rerun-creates-job' flag.", http.StatusMethodNotAllowed)
   304  				return
   305  			}
   306  			allowed, user, err, code := isAllowedToRerun(r, acfg, goa, ghc, newPJ, cli, pluginAgent, l)
   307  			if err != nil {
   308  				http.Error(w, fmt.Sprintf("Could not verify if allowed to rerun: %v.", err), code)
   309  				l.WithError(err).Debug("Could not verify if allowed to rerun.")
   310  			}
   311  			l = l.WithField("allowed", allowed).WithField("user", user).WithField("code", code)
   312  			l.Info("Attempted rerun")
   313  			if !allowed {
   314  				if _, err = w.Write([]byte("You don't have permission to rerun that job.")); err != nil {
   315  					l.WithError(err).Error("Error writing to rerun response.")
   316  				}
   317  				return
   318  			}
   319  			var rerunDescription string
   320  			if len(user) > 0 {
   321  				rerunDescription = fmt.Sprintf("%v successfully reran %v.", user, name)
   322  			} else {
   323  				rerunDescription = fmt.Sprintf("Successfully reran %v.", name)
   324  			}
   325  			newPJ.Status.Description = rerunDescription
   326  			created, err := prowJobClient.Create(context.TODO(), &newPJ, metav1.CreateOptions{})
   327  			if err != nil {
   328  				l.WithError(err).Error("Error creating job.")
   329  				http.Error(w, fmt.Sprintf("Error creating job: %v", err), http.StatusInternalServerError)
   330  				return
   331  			}
   332  			l = l.WithField("new-prowjob", created.Name)
   333  			if len(user) > 0 {
   334  				l.Info(fmt.Sprintf("%v successfully created a rerun of %v.", user, name))
   335  			} else {
   336  				l.Info(fmt.Sprintf("Successfully created a rerun of %v.", name))
   337  			}
   338  			if _, err = w.Write([]byte("Job successfully triggered. Wait 30 seconds and refresh the page for the job to show up.")); err != nil {
   339  				l.WithError(err).Error("Error writing to rerun response.")
   340  			}
   341  			return
   342  		default:
   343  			http.Error(w, fmt.Sprintf("bad verb %v", r.Method), http.StatusMethodNotAllowed)
   344  			return
   345  		}
   346  	}
   347  }