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 }