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