diff --git a/prow/cluster/deck_rbac.yaml b/prow/cluster/deck_rbac.yaml index 512cac99adc9..da7097b58119 100644 --- a/prow/cluster/deck_rbac.yaml +++ b/prow/cluster/deck_rbac.yaml @@ -17,6 +17,10 @@ rules: verbs: - get - list + # Required when deck runs with `--rerun-creates-job=true` + # **Warning:** Only use this for non-public deck instances, this allows + # anyone with access to your Deck instance to create new Prowjobs + # - create --- kind: Role apiVersion: rbac.authorization.k8s.io/v1beta1 diff --git a/prow/cluster/starter.yaml b/prow/cluster/starter.yaml index 155fabc03a03..b6b966005218 100644 --- a/prow/cluster/starter.yaml +++ b/prow/cluster/starter.yaml @@ -533,6 +533,10 @@ rules: verbs: - get - list + # Required when deck runs with `--rerun-creates-job=true` + # **Warning:** Only use this for non-public deck instances, this allows + # anyone with access to your Deck instance to create new Prowjobs + # - create --- kind: Role apiVersion: rbac.authorization.k8s.io/v1beta1 diff --git a/prow/cmd/deck/main.go b/prow/cmd/deck/main.go index 2ad4da012260..bd68aef5643f 100644 --- a/prow/cmd/deck/main.go +++ b/prow/cmd/deck/main.go @@ -91,6 +91,7 @@ type options struct { spyglass bool spyglassFilesLocation string gcsCredentialsFile string + rerunCreatesJob bool } func (o *options) Validate() error { @@ -136,6 +137,7 @@ func gatherOptions(fs *flag.FlagSet, args ...string) options { fs.StringVar(&o.staticFilesLocation, "static-files-location", "/static", "Path to the static files") fs.StringVar(&o.templateFilesLocation, "template-files-location", "/template", "Path to the template files") fs.StringVar(&o.gcsCredentialsFile, "gcs-credentials-file", "", "Path to the GCS credentials file") + fs.BoolVar(&o.rerunCreatesJob, "rerun-creates-job", false, "Change the re-run option in Deck to actually create the job. **WARNING:** Only use this with non-public deck instances, otherwise strangers can DOS your Prow instance") o.kubernetes.AddFlags(fs) fs.Parse(args) o.configPath = config.ConfigPath(o.configPath) @@ -267,7 +269,7 @@ func main() { mux.Handle("/tide-history", gziphandler.GzipHandler(handleSimpleTemplate(o, cfg, "tide-history.html", nil))) mux.Handle("/plugins", gziphandler.GzipHandler(handleSimpleTemplate(o, cfg, "plugins.html", nil))) - indexHandler := handleSimpleTemplate(o, cfg, "index.html", struct{ SpyglassEnabled bool }{o.spyglass}) + indexHandler := handleSimpleTemplate(o, cfg, "index.html", struct{ SpyglassEnabled, ReRunCreatesJob bool }{o.spyglass, o.rerunCreatesJob}) runLocal := o.pregeneratedData != "" @@ -392,7 +394,7 @@ func prodOnlyMain(cfg config.Getter, o options, mux *http.ServeMux) *http.ServeM mux.Handle("/prowjobs.js", gziphandler.GzipHandler(handleProwJobs(ja))) mux.Handle("/badge.svg", gziphandler.GzipHandler(handleBadge(ja))) mux.Handle("/log", gziphandler.GzipHandler(handleLog(ja))) - mux.Handle("/rerun", gziphandler.GzipHandler(handleRerun(prowJobClient))) + mux.Handle("/rerun", gziphandler.GzipHandler(handleRerun(prowJobClient, o.rerunCreatesJob))) mux.Handle("/prowjob", gziphandler.GzipHandler(handleProwJob(prowJobClient))) if o.spyglass { @@ -1147,7 +1149,7 @@ func handleProwJob(prowJobClient prowv1.ProwJobInterface) http.HandlerFunc { } } -func handleRerun(prowJobClient prowv1.ProwJobInterface) http.HandlerFunc { +func handleRerun(prowJobClient prowv1.ProwJobInterface, createProwJob bool) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { name := r.URL.Query().Get("prowjob") if name == "" { @@ -1163,8 +1165,26 @@ func handleRerun(prowJobClient prowv1.ProwJobInterface) http.HandlerFunc { } return } - pjutil := pjutil.NewProwJob(pj.Spec, pj.ObjectMeta.Labels) - b, err := yaml.Marshal(&pjutil) + newPJ := pjutil.NewProwJob(pj.Spec, pj.ObjectMeta.Labels) + // Be very careful about this on publicly accessible Prow instances. Even after we have authentication + // for the handler, we need CSRF protection. + // On Prow instances that require auth even for viewing Deck this is okayish, because the Prowjob UUID + // is hard to guess + // Ref: https://github.com/kubernetes/test-infra/pull/12827#issuecomment-502850414 + if createProwJob { + if r.Method != http.MethodPost { + http.Error(w, "request must be of type POST", http.StatusMethodNotAllowed) + return + } + if _, err := prowJobClient.Create(&newPJ); err != nil { + logrus.WithError(err).Error("Error creating job") + http.Error(w, fmt.Sprintf("Error creating job: %v", err), http.StatusInternalServerError) + return + } + w.WriteHeader(http.StatusNoContent) + return + } + b, err := yaml.Marshal(&newPJ) if err != nil { http.Error(w, fmt.Sprintf("Error marshaling: %v", err), http.StatusInternalServerError) logrus.WithError(err).Error("Error marshaling jobs.") diff --git a/prow/cmd/deck/main_test.go b/prow/cmd/deck/main_test.go index 9cf124d8b641..1174fa733d93 100644 --- a/prow/cmd/deck/main_test.go +++ b/prow/cmd/deck/main_test.go @@ -256,51 +256,84 @@ func TestProwJob(t *testing.T) { // TestRerun just checks that the result can be unmarshaled properly, has an // updated status, and has equal spec. func TestRerun(t *testing.T) { - fakeProwJobClient := fake.NewSimpleClientset(&prowapi.ProwJob{ - ObjectMeta: metav1.ObjectMeta{ - Name: "wowsuch", - Namespace: "prowjobs", - }, - Spec: prowapi.ProwJobSpec{ - Job: "whoa", - Type: prowapi.PresubmitJob, - Refs: &prowapi.Refs{ - Org: "org", - Repo: "repo", - Pulls: []prowapi.Pull{ - {Number: 1}, - }, - }, + testCases := []struct { + name string + shouldCreateProwjob bool + }{ + { + name: "Handler returns ProwJob", + shouldCreateProwjob: false, }, - Status: prowapi.ProwJobStatus{ - State: prowapi.PendingState, + { + name: "Handler creates ProwJob", + shouldCreateProwjob: true, }, - }) - handler := handleRerun(fakeProwJobClient.ProwV1().ProwJobs("prowjobs")) - req, err := http.NewRequest(http.MethodGet, "/rerun?prowjob=wowsuch", nil) - if err != nil { - t.Fatalf("Error making request: %v", err) - } - rr := httptest.NewRecorder() - handler.ServeHTTP(rr, req) - if rr.Code != http.StatusOK { - t.Fatalf("Bad error code: %d", rr.Code) } - resp := rr.Result() - defer resp.Body.Close() - body, err := ioutil.ReadAll(resp.Body) - if err != nil { - t.Fatalf("Error reading response body: %v", err) - } - var res prowapi.ProwJob - if err := yaml.Unmarshal(body, &res); err != nil { - t.Fatalf("Error unmarshaling: %v", err) - } - if res.Spec.Job != "whoa" { - t.Errorf("Wrong job, expected \"whoa\", got \"%s\"", res.Spec.Job) - } - if res.Status.State != prowapi.TriggeredState { - t.Errorf("Wrong state, expected \"%v\", got \"%v\"", prowapi.TriggeredState, res.Status.State) + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + fakeProwJobClient := fake.NewSimpleClientset(&prowapi.ProwJob{ + ObjectMeta: metav1.ObjectMeta{ + Name: "wowsuch", + Namespace: "prowjobs", + }, + Spec: prowapi.ProwJobSpec{ + Job: "whoa", + Type: prowapi.PresubmitJob, + Refs: &prowapi.Refs{ + Org: "org", + Repo: "repo", + Pulls: []prowapi.Pull{ + {Number: 1}, + }, + }, + }, + Status: prowapi.ProwJobStatus{ + State: prowapi.PendingState, + }, + }) + handler := handleRerun(fakeProwJobClient.ProwV1().ProwJobs("prowjobs"), tc.shouldCreateProwjob) + req, err := http.NewRequest(http.MethodPost, "/rerun?prowjob=wowsuch", nil) + if err != nil { + t.Fatalf("Error making request: %v", err) + } + rr := httptest.NewRecorder() + handler.ServeHTTP(rr, req) + + if tc.shouldCreateProwjob { + if rr.Code != http.StatusNoContent { + t.Fatalf("Unexpected http status code: %d, expected 204", rr.Code) + } + pjs, err := fakeProwJobClient.ProwV1().ProwJobs("prowjobs").List(metav1.ListOptions{}) + if err != nil { + t.Fatalf("failed to list prowjobs: %v", err) + } + if numPJs := len(pjs.Items); numPJs != 2 { + t.Errorf("expected to get two prowjobs, got %d", numPJs) + } + + } else { + if rr.Code != http.StatusOK { + t.Fatalf("Bad error code: %d", rr.Code) + } + resp := rr.Result() + defer resp.Body.Close() + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + t.Fatalf("Error reading response body: %v", err) + } + var res prowapi.ProwJob + if err := yaml.Unmarshal(body, &res); err != nil { + t.Fatalf("Error unmarshaling: %v", err) + } + if res.Spec.Job != "whoa" { + t.Errorf("Wrong job, expected \"whoa\", got \"%s\"", res.Spec.Job) + } + if res.Status.State != prowapi.TriggeredState { + t.Errorf("Wrong state, expected \"%v\", got \"%v\"", prowapi.TriggeredState, res.Status.State) + } + } + }) } } diff --git a/prow/cmd/deck/static/prow/prow.ts b/prow/cmd/deck/static/prow/prow.ts index a66f01910e50..36ecdd3e300b 100644 --- a/prow/cmd/deck/static/prow/prow.ts +++ b/prow/cmd/deck/static/prow/prow.ts @@ -6,6 +6,7 @@ import {JobHistogram, JobSample} from './histogram'; declare const allBuilds: Job[]; declare const spyglass: boolean; +declare const rerunCreatesJob: boolean; // http://stackoverflow.com/a/5158301/3694 function getParameterByName(name: string): string | null { @@ -653,8 +654,19 @@ function redraw(fz: FuzzySearch): void { function createRerunCell(modal: HTMLElement, rerunElement: HTMLElement, prowjob: string): HTMLTableDataCellElement { const url = `https://${window.location.hostname}/rerun?prowjob=${prowjob}`; const c = document.createElement("td"); - const i = icon.create("refresh", "Show instructions for rerunning this job"); - i.onclick = () => { + let i; + if (rerunCreatesJob) { + i = icon.create("refresh", "Rerun this job"); + i.onclick = () => { + const form = document.createElement('form'); + form.method = 'POST'; + form.action = `${url}`; + c.appendChild(form); + form.submit(); + }; + } else { + i = icon.create("refresh", "Show instructions for rerunning this job"); + i.onclick = () => { modal.style.display = "block"; rerunElement.innerHTML = `kubectl create -f "${url}"`; const copyButton = document.createElement('a'); @@ -662,7 +674,8 @@ function createRerunCell(modal: HTMLElement, rerunElement: HTMLElement, prowjob: copyButton.onclick = () => copyToClipboardWithToast(`kubectl create -f "${url}"`); copyButton.innerHTML = "file_copy"; rerunElement.appendChild(copyButton); - }; + }; + } c.appendChild(i); c.classList.add("icon-cell"); return c; diff --git a/prow/cmd/deck/template/index.html b/prow/cmd/deck/template/index.html index c17500fbdc68..5543409143e7 100644 --- a/prow/cmd/deck/template/index.html +++ b/prow/cmd/deck/template/index.html @@ -5,6 +5,7 @@ {{end}}