github.com/munnerz/test-infra@v0.0.0-20190108210205-ce3d181dc989/prow/cmd/deck/main.go (about) 1 /* 2 Copyright 2016 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 "bytes" 21 "context" 22 "encoding/base64" 23 "encoding/json" 24 "errors" 25 "flag" 26 "fmt" 27 "html/template" 28 "io/ioutil" 29 "net/http" 30 "net/url" 31 "path" 32 "strings" 33 "time" 34 35 "cloud.google.com/go/storage" 36 "github.com/NYTimes/gziphandler" 37 "github.com/gorilla/sessions" 38 "github.com/sirupsen/logrus" 39 "google.golang.org/api/option" 40 "sigs.k8s.io/yaml" 41 42 "golang.org/x/oauth2" 43 "golang.org/x/oauth2/github" 44 "k8s.io/test-infra/prow/config" 45 "k8s.io/test-infra/prow/deck/jobs" 46 "k8s.io/test-infra/prow/githuboauth" 47 "k8s.io/test-infra/prow/kube" 48 "k8s.io/test-infra/prow/logrusutil" 49 "k8s.io/test-infra/prow/pjutil" 50 "k8s.io/test-infra/prow/pluginhelp" 51 "k8s.io/test-infra/prow/prstatus" 52 "k8s.io/test-infra/prow/spyglass" 53 54 // Import standard spyglass viewers 55 56 "k8s.io/test-infra/prow/spyglass/lenses" 57 _ "k8s.io/test-infra/prow/spyglass/lenses/buildlog" 58 _ "k8s.io/test-infra/prow/spyglass/lenses/junit" 59 _ "k8s.io/test-infra/prow/spyglass/lenses/metadata" 60 ) 61 62 type options struct { 63 configPath string 64 jobConfigPath string 65 buildCluster string 66 tideURL string 67 hookURL string 68 oauthURL string 69 githubOAuthConfigFile string 70 cookieSecretFile string 71 redirectHTTPTo string 72 hiddenOnly bool 73 pregeneratedData string 74 staticFilesLocation string 75 templateFilesLocation string 76 spyglass bool 77 spyglassFilesLocation string 78 gcsCredentialsFile string 79 } 80 81 func (o *options) Validate() error { 82 if o.configPath == "" { 83 return errors.New("required flag --config-path was unset") 84 } 85 if o.oauthURL != "" { 86 if o.githubOAuthConfigFile == "" { 87 return errors.New("an OAuth URL was provided but required flag --github-oauth-config-file was unset") 88 } 89 if o.cookieSecretFile == "" { 90 return errors.New("an OAuth URL was provided but required flag --cookie-secret was unset") 91 } 92 } 93 return nil 94 } 95 96 func gatherOptions() options { 97 o := options{} 98 flag.StringVar(&o.configPath, "config-path", "/etc/config/config.yaml", "Path to config.yaml.") 99 flag.StringVar(&o.jobConfigPath, "job-config-path", "", "Path to prow job configs.") 100 flag.StringVar(&o.buildCluster, "build-cluster", "", "Path to file containing a YAML-marshalled kube.Cluster object. If empty, uses the local cluster.") 101 flag.StringVar(&o.tideURL, "tide-url", "", "Path to tide. If empty, do not serve tide data.") 102 flag.StringVar(&o.hookURL, "hook-url", "", "Path to hook plugin help endpoint.") 103 flag.StringVar(&o.oauthURL, "oauth-url", "", "Path to deck user dashboard endpoint.") 104 flag.StringVar(&o.githubOAuthConfigFile, "github-oauth-config-file", "/etc/github/secret", "Path to the file containing the GitHub App Client secret.") 105 flag.StringVar(&o.cookieSecretFile, "cookie-secret", "/etc/cookie/secret", "Path to the file containing the cookie secret key.") 106 // use when behind a load balancer 107 flag.StringVar(&o.redirectHTTPTo, "redirect-http-to", "", "Host to redirect http->https to based on x-forwarded-proto == http.") 108 // use when behind an oauth proxy 109 flag.BoolVar(&o.hiddenOnly, "hidden-only", false, "Show only hidden jobs. Useful for serving hidden jobs behind an oauth proxy.") 110 flag.StringVar(&o.pregeneratedData, "pregenerated-data", "", "Use API output from another prow instance. Used by the prow/cmd/deck/runlocal script") 111 flag.BoolVar(&o.spyglass, "spyglass", false, "Use Prow built-in job viewing instead of Gubernator") 112 flag.StringVar(&o.spyglassFilesLocation, "spyglass-files-location", "/lenses", "Location of the static files for spyglass.") 113 flag.StringVar(&o.staticFilesLocation, "static-files-location", "/static", "Path to the static files") 114 flag.StringVar(&o.templateFilesLocation, "template-files-location", "/template", "Path to the template files") 115 flag.StringVar(&o.gcsCredentialsFile, "gcs-credentials-file", "", "Path to the GCS credentials file") 116 flag.Parse() 117 return o 118 } 119 120 func staticHandlerFromDir(dir string) http.Handler { 121 return gziphandler.GzipHandler(handleCached(http.FileServer(http.Dir(dir)))) 122 } 123 124 func main() { 125 o := gatherOptions() 126 if err := o.Validate(); err != nil { 127 logrus.Fatalf("Invalid options: %v", err) 128 } 129 130 logrus.SetFormatter( 131 logrusutil.NewDefaultFieldsFormatter(nil, logrus.Fields{"component": "deck"}), 132 ) 133 134 mux := http.NewServeMux() 135 136 // setup config agent, pod log clients etc. 137 configAgent := &config.Agent{} 138 if err := configAgent.Start(o.configPath, o.jobConfigPath); err != nil { 139 logrus.WithError(err).Fatal("Error starting config agent.") 140 } 141 142 // setup common handlers for local and deployed runs 143 mux.Handle("/static/", http.StripPrefix("/static", staticHandlerFromDir(o.staticFilesLocation))) 144 mux.Handle("/config", gziphandler.GzipHandler(handleConfig(configAgent))) 145 mux.Handle("/favicon.ico", gziphandler.GzipHandler(handleFavicon(o.staticFilesLocation, configAgent))) 146 147 // Set up handlers for template pages. 148 mux.Handle("/pr", gziphandler.GzipHandler(handleSimpleTemplate(o, configAgent, "pr.html", nil))) 149 mux.Handle("/command-help", gziphandler.GzipHandler(handleSimpleTemplate(o, configAgent, "command-help.html", nil))) 150 mux.Handle("/plugin-help", http.RedirectHandler("/command-help", http.StatusMovedPermanently)) 151 mux.Handle("/tide", gziphandler.GzipHandler(handleSimpleTemplate(o, configAgent, "tide.html", nil))) 152 mux.Handle("/tide-history", gziphandler.GzipHandler(handleSimpleTemplate(o, configAgent, "tide-history.html", nil))) 153 mux.Handle("/plugins", gziphandler.GzipHandler(handleSimpleTemplate(o, configAgent, "plugins.html", nil))) 154 155 indexHandler := handleSimpleTemplate(o, configAgent, "index.html", struct{ SpyglassEnabled bool }{o.spyglass}) 156 157 runLocal := o.pregeneratedData != "" 158 159 var fallbackHandler func(http.ResponseWriter, *http.Request) 160 if runLocal { 161 localDataHandler := staticHandlerFromDir(o.pregeneratedData) 162 fallbackHandler = localDataHandler.ServeHTTP 163 } else { 164 fallbackHandler = http.NotFound 165 } 166 167 mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 168 if r.URL.Path != "/" { 169 fallbackHandler(w, r) 170 return 171 } 172 indexHandler(w, r) 173 }) 174 175 if runLocal { 176 mux = localOnlyMain(configAgent, o, mux) 177 } else { 178 mux = prodOnlyMain(configAgent, o, mux) 179 } 180 181 // setup done, actually start the server 182 logrus.WithError(http.ListenAndServe(":8080", mux)).Fatal("ListenAndServe returned.") 183 } 184 185 // localOnlyMain contains logic used only when running locally, and is mutually exclusive with 186 // prodOnlyMain. 187 func localOnlyMain(configAgent *config.Agent, o options, mux *http.ServeMux) *http.ServeMux { 188 mux.Handle("/github-login", gziphandler.GzipHandler(handleSimpleTemplate(o, configAgent, "github-login.html", nil))) 189 190 if o.spyglass { 191 initSpyglass(configAgent, o, mux, nil) 192 } 193 194 return mux 195 } 196 197 // prodOnlyMain contains logic only used when running deployed, not locally 198 func prodOnlyMain(configAgent *config.Agent, o options, mux *http.ServeMux) *http.ServeMux { 199 kc, err := kube.NewClientInCluster(configAgent.Config().ProwJobNamespace) 200 if err != nil { 201 logrus.WithError(err).Fatal("Error getting client.") 202 } 203 kc.SetHiddenReposProvider(func() []string { return configAgent.Config().Deck.HiddenRepos }, o.hiddenOnly) 204 205 var pkcs map[string]*kube.Client 206 if o.buildCluster == "" { 207 pkcs = map[string]*kube.Client{kube.DefaultClusterAlias: kc.Namespace(configAgent.Config().PodNamespace)} 208 } else { 209 pkcs, err = kube.ClientMapFromFile(o.buildCluster, configAgent.Config().PodNamespace) 210 if err != nil { 211 logrus.WithError(err).Fatal("Error getting kube client to build cluster.") 212 } 213 } 214 plClients := map[string]jobs.PodLogClient{} 215 for alias, client := range pkcs { 216 plClients[alias] = client 217 } 218 219 ja := jobs.NewJobAgent(kc, plClients, configAgent) 220 ja.Start() 221 222 // setup prod only handlers 223 mux.Handle("/data.js", gziphandler.GzipHandler(handleData(ja))) 224 mux.Handle("/prowjobs.js", gziphandler.GzipHandler(handleProwJobs(ja))) 225 mux.Handle("/badge.svg", gziphandler.GzipHandler(handleBadge(ja))) 226 mux.Handle("/log", gziphandler.GzipHandler(handleLog(ja))) 227 mux.Handle("/rerun", gziphandler.GzipHandler(handleRerun(kc))) 228 229 if o.spyglass { 230 initSpyglass(configAgent, o, mux, ja) 231 } 232 233 if o.hookURL != "" { 234 mux.Handle("/plugin-help.js", 235 gziphandler.GzipHandler(handlePluginHelp(newHelpAgent(o.hookURL)))) 236 } 237 238 if o.tideURL != "" { 239 ta := &tideAgent{ 240 log: logrus.WithField("agent", "tide"), 241 path: o.tideURL, 242 updatePeriod: func() time.Duration { 243 return configAgent.Config().Deck.TideUpdatePeriod 244 }, 245 hiddenRepos: configAgent.Config().Deck.HiddenRepos, 246 hiddenOnly: o.hiddenOnly, 247 } 248 ta.start() 249 mux.Handle("/tide.js", gziphandler.GzipHandler(handleTidePools(configAgent, ta))) 250 mux.Handle("/tide-history.js", gziphandler.GzipHandler(handleTideHistory(ta))) 251 } 252 253 // Enable Git OAuth feature if oauthURL is provided. 254 if o.oauthURL != "" { 255 githubOAuthConfigRaw, err := loadToken(o.githubOAuthConfigFile) 256 if err != nil { 257 logrus.WithError(err).Fatal("Could not read github oauth config file.") 258 } 259 260 cookieSecretRaw, err := loadToken(o.cookieSecretFile) 261 if err != nil { 262 logrus.WithError(err).Fatal("Could not read cookie secret file.") 263 } 264 265 var githubOAuthConfig config.GithubOAuthConfig 266 if err := yaml.Unmarshal(githubOAuthConfigRaw, &githubOAuthConfig); err != nil { 267 logrus.WithError(err).Fatal("Error unmarshalling github oauth config") 268 } 269 if !isValidatedGitOAuthConfig(&githubOAuthConfig) { 270 logrus.Fatal("Error invalid github oauth config") 271 } 272 273 decodedSecret, err := base64.StdEncoding.DecodeString(string(cookieSecretRaw)) 274 if err != nil { 275 logrus.WithError(err).Fatal("Error decoding cookie secret") 276 } 277 if len(decodedSecret) == 0 { 278 logrus.Fatal("Cookie secret should not be empty") 279 } 280 cookie := sessions.NewCookieStore(decodedSecret) 281 githubOAuthConfig.InitGithubOAuthConfig(cookie) 282 283 goa := githuboauth.NewAgent(&githubOAuthConfig, logrus.WithField("client", "githuboauth")) 284 oauthClient := &oauth2.Config{ 285 ClientID: githubOAuthConfig.ClientID, 286 ClientSecret: githubOAuthConfig.ClientSecret, 287 RedirectURL: githubOAuthConfig.RedirectURL, 288 Scopes: githubOAuthConfig.Scopes, 289 Endpoint: github.Endpoint, 290 } 291 292 repoSet := make(map[string]bool) 293 for r := range configAgent.Config().Presubmits { 294 repoSet[r] = true 295 } 296 for _, q := range configAgent.Config().Tide.Queries { 297 for _, v := range q.Repos { 298 repoSet[v] = true 299 } 300 } 301 var repos []string 302 for k, v := range repoSet { 303 if v { 304 repos = append(repos, k) 305 } 306 } 307 308 prStatusAgent := prstatus.NewDashboardAgent( 309 repos, 310 &githubOAuthConfig, 311 logrus.WithField("client", "pr-status")) 312 313 mux.Handle("/pr-data.js", handleNotCached( 314 prStatusAgent.HandlePrStatus(prStatusAgent))) 315 // Handles login request. 316 mux.Handle("/github-login", goa.HandleLogin(oauthClient)) 317 // Handles redirect from Github OAuth server. 318 mux.Handle("/github-login/redirect", goa.HandleRedirect(oauthClient, githuboauth.NewGithubClientGetter())) 319 } 320 321 // optionally inject http->https redirect handler when behind loadbalancer 322 if o.redirectHTTPTo != "" { 323 redirectMux := http.NewServeMux() 324 redirectMux.Handle("/", func(oldMux *http.ServeMux, host string) http.HandlerFunc { 325 return func(w http.ResponseWriter, r *http.Request) { 326 if r.Header.Get("x-forwarded-proto") == "http" { 327 redirectURL, err := url.Parse(r.URL.String()) 328 if err != nil { 329 logrus.Errorf("Failed to parse URL: %s.", r.URL.String()) 330 http.Error(w, "Failed to perform https redirect.", http.StatusInternalServerError) 331 return 332 } 333 redirectURL.Scheme = "https" 334 redirectURL.Host = host 335 http.Redirect(w, r, redirectURL.String(), http.StatusMovedPermanently) 336 } else { 337 oldMux.ServeHTTP(w, r) 338 } 339 } 340 }(mux, o.redirectHTTPTo)) 341 mux = redirectMux 342 } 343 return mux 344 } 345 346 func initSpyglass(configAgent *config.Agent, o options, mux *http.ServeMux, ja *jobs.JobAgent) { 347 var c *storage.Client 348 var err error 349 if o.gcsCredentialsFile == "" { 350 c, err = storage.NewClient(context.Background(), option.WithoutAuthentication()) 351 } else { 352 c, err = storage.NewClient(context.Background(), option.WithCredentialsFile(o.gcsCredentialsFile)) 353 } 354 if err != nil { 355 logrus.WithError(err).Fatal("Error getting GCS client") 356 } 357 sg := spyglass.New(ja, configAgent, c) 358 359 mux.Handle("/spyglass/static/", http.StripPrefix("/spyglass/static", staticHandlerFromDir(o.spyglassFilesLocation))) 360 mux.Handle("/spyglass/lens/", gziphandler.GzipHandler(http.StripPrefix("/spyglass/lens/", handleArtifactView(o, sg, configAgent)))) 361 mux.Handle("/view/", gziphandler.GzipHandler(handleRequestJobViews(sg, configAgent, o))) 362 mux.Handle("/job-history/", gziphandler.GzipHandler(handleJobHistory(o, configAgent, c))) 363 mux.Handle("/pr-history/", gziphandler.GzipHandler(handlePRHistory(o, configAgent, c))) 364 } 365 366 func loadToken(file string) ([]byte, error) { 367 raw, err := ioutil.ReadFile(file) 368 if err != nil { 369 return []byte{}, err 370 } 371 return bytes.TrimSpace(raw), nil 372 } 373 374 // copy a http.Request 375 // see: https://go-review.googlesource.com/c/go/+/36483/3/src/net/http/server.go 376 func dupeRequest(original *http.Request) *http.Request { 377 r2 := new(http.Request) 378 *r2 = *original 379 r2.URL = new(url.URL) 380 *r2.URL = *original.URL 381 return r2 382 } 383 384 func handleCached(next http.Handler) http.Handler { 385 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 386 // This looks ridiculous but actually no-cache means "revalidate" and 387 // "max-age=0" just means there is no time in which it can skip 388 // revalidation. We also need to set must-revalidate because no-cache 389 // doesn't imply must-revalidate when using the back button 390 // https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.1 391 // TODO(bentheelder): consider setting a longer max-age 392 // setting it this way means the content is always revalidated 393 w.Header().Set("Cache-Control", "public, max-age=0, no-cache, must-revalidate") 394 next.ServeHTTP(w, r) 395 }) 396 } 397 398 func setHeadersNoCaching(w http.ResponseWriter) { 399 // Note that we need to set both no-cache and no-store because only some 400 // browsers decided to (incorrectly) treat no-cache as "never store" 401 // IE "no-store". for good measure to cover older browsers we also set 402 // expires and pragma: https://stackoverflow.com/a/2068407 403 w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate") 404 w.Header().Set("Pragma", "no-cache") 405 w.Header().Set("Expires", "0") 406 } 407 408 func handleNotCached(next http.Handler) http.HandlerFunc { 409 return func(w http.ResponseWriter, r *http.Request) { 410 setHeadersNoCaching(w) 411 next.ServeHTTP(w, r) 412 } 413 } 414 415 func handleProwJobs(ja *jobs.JobAgent) http.HandlerFunc { 416 return func(w http.ResponseWriter, r *http.Request) { 417 setHeadersNoCaching(w) 418 jobs := ja.ProwJobs() 419 if v := r.URL.Query().Get("omit"); v == "pod_spec" { 420 for i := range jobs { 421 jobs[i].Spec.PodSpec = nil 422 } 423 } 424 jd, err := json.Marshal(struct { 425 Items []kube.ProwJob `json:"items"` 426 }{jobs}) 427 if err != nil { 428 logrus.WithError(err).Error("Error marshaling jobs.") 429 jd = []byte("{}") 430 } 431 // If we have a "var" query, then write out "var value = {...};". 432 // Otherwise, just write out the JSON. 433 if v := r.URL.Query().Get("var"); v != "" { 434 fmt.Fprintf(w, "var %s = %s;", v, string(jd)) 435 } else { 436 fmt.Fprint(w, string(jd)) 437 } 438 } 439 } 440 441 func handleData(ja *jobs.JobAgent) http.HandlerFunc { 442 return func(w http.ResponseWriter, r *http.Request) { 443 setHeadersNoCaching(w) 444 jobs := ja.Jobs() 445 jd, err := json.Marshal(jobs) 446 if err != nil { 447 logrus.WithError(err).Error("Error marshaling jobs.") 448 jd = []byte("[]") 449 } 450 // If we have a "var" query, then write out "var value = {...};". 451 // Otherwise, just write out the JSON. 452 if v := r.URL.Query().Get("var"); v != "" { 453 fmt.Fprintf(w, "var %s = %s;", v, string(jd)) 454 } else { 455 fmt.Fprint(w, string(jd)) 456 } 457 } 458 } 459 460 func handleBadge(ja *jobs.JobAgent) http.HandlerFunc { 461 return func(w http.ResponseWriter, r *http.Request) { 462 setHeadersNoCaching(w) 463 wantJobs := r.URL.Query().Get("jobs") 464 if wantJobs == "" { 465 http.Error(w, "missing jobs query parameter", http.StatusBadRequest) 466 return 467 } 468 w.Header().Set("Content-Type", "image/svg+xml") 469 470 allJobs := ja.ProwJobs() 471 _, _, svg := renderBadge(pickLatestJobs(allJobs, wantJobs)) 472 w.Write(svg) 473 } 474 } 475 476 func handleJobHistory(o options, ca *config.Agent, gcsClient *storage.Client) http.HandlerFunc { 477 return func(w http.ResponseWriter, r *http.Request) { 478 setHeadersNoCaching(w) 479 tmpl, err := getJobHistory(r.URL, ca.Config(), gcsClient) 480 if err != nil { 481 msg := fmt.Sprintf("failed to get job history: %v", err) 482 logrus.WithField("url", r.URL).Error(msg) 483 http.Error(w, msg, http.StatusInternalServerError) 484 return 485 } 486 handleSimpleTemplate(o, ca, "job-history.html", tmpl)(w, r) 487 } 488 } 489 490 func handlePRHistory(o options, ca *config.Agent, gcsClient *storage.Client) http.HandlerFunc { 491 return func(w http.ResponseWriter, r *http.Request) { 492 setHeadersNoCaching(w) 493 tmpl, err := getPRHistory(r.URL, ca.Config(), gcsClient) 494 if err != nil { 495 msg := fmt.Sprintf("failed to get PR history: %v", err) 496 logrus.WithField("url", r.URL).Error(msg) 497 http.Error(w, msg, http.StatusInternalServerError) 498 return 499 } 500 handleSimpleTemplate(o, ca, "pr-history.html", tmpl)(w, r) 501 } 502 } 503 504 // handleRequestJobViews handles requests to get all available artifact views for a given job. 505 // The url must specify a storage key type, such as "prowjob" or "gcs": 506 // 507 // /view/<key-type>/<key> 508 // 509 // Examples: 510 // - /view/gcs/kubernetes-jenkins/pr-logs/pull/test-infra/9557/pull-test-infra-verify-gofmt/15688/ 511 // - /view/prowjob/echo-test/1046875594609922048 512 func handleRequestJobViews(sg *spyglass.Spyglass, ca *config.Agent, o options) http.HandlerFunc { 513 return func(w http.ResponseWriter, r *http.Request) { 514 start := time.Now() 515 setHeadersNoCaching(w) 516 src := strings.TrimPrefix(r.URL.Path, "/view/") 517 518 page, err := renderSpyglass(sg, ca, src, o) 519 if err != nil { 520 logrus.WithError(err).Error("error rendering spyglass page") 521 message := fmt.Sprintf("error rendering spyglass page: %v", err) 522 http.Error(w, message, http.StatusInternalServerError) 523 return 524 } 525 526 fmt.Fprint(w, page) 527 elapsed := time.Since(start) 528 logrus.WithFields(logrus.Fields{ 529 "duration": elapsed.String(), 530 "endpoint": r.URL.Path, 531 "source": src, 532 }).Info("Loading view completed.") 533 } 534 } 535 536 // renderSpyglass returns a pre-rendered Spyglass page from the given source string 537 func renderSpyglass(sg *spyglass.Spyglass, ca *config.Agent, src string, o options) (string, error) { 538 renderStart := time.Now() 539 artifactNames, err := sg.ListArtifacts(src) 540 if err != nil { 541 return "", fmt.Errorf("error listing artifacts: %v", err) 542 } 543 if len(artifactNames) == 0 { 544 return "", fmt.Errorf("found no artifacts for %s", src) 545 } 546 547 viewerCache := map[string][]string{} 548 viewersRegistry := ca.Config().Deck.Spyglass.Viewers 549 regexCache := ca.Config().Deck.Spyglass.RegexCache 550 551 for re, viewerNames := range viewersRegistry { 552 matches := []string{} 553 for _, a := range artifactNames { 554 if regexCache[re].MatchString(a) { 555 matches = append(matches, a) 556 } 557 } 558 if len(matches) > 0 { 559 for _, vName := range viewerNames { 560 viewerCache[vName] = matches 561 } 562 } 563 } 564 565 ls := sg.Lenses(viewerCache) 566 lensNames := []string{} 567 for _, l := range ls { 568 lensNames = append(lensNames, l.Name()) 569 } 570 571 jobHistLink := "" 572 jobPath, err := sg.JobPath(src) 573 if err == nil { 574 jobHistLink = path.Join("/job-history", jobPath) 575 } 576 logrus.Infof("job history link: %s", jobHistLink) 577 578 var viewBuf bytes.Buffer 579 type lensesTemplate struct { 580 Lenses []lenses.Lens 581 LensNames []string 582 Source string 583 LensArtifacts map[string][]string 584 JobHistLink string 585 } 586 lTmpl := lensesTemplate{ 587 Lenses: ls, 588 LensNames: lensNames, 589 Source: src, 590 LensArtifacts: viewerCache, 591 JobHistLink: jobHistLink, 592 } 593 t := template.New("spyglass.html") 594 595 if _, err := prepareBaseTemplate(o, ca, t); err != nil { 596 return "", fmt.Errorf("error preparing base template: %v", err) 597 } 598 t, err = t.ParseFiles(path.Join(o.templateFilesLocation, "spyglass.html")) 599 if err != nil { 600 return "", fmt.Errorf("error parsing template: %v", err) 601 } 602 603 if err = t.Execute(&viewBuf, lTmpl); err != nil { 604 return "", fmt.Errorf("error rendering template: %v", err) 605 } 606 renderElapsed := time.Since(renderStart) 607 logrus.WithFields(logrus.Fields{ 608 "duration": renderElapsed.String(), 609 "source": src, 610 }).Info("Rendered spyglass views.") 611 return viewBuf.String(), nil 612 } 613 614 // handleArtifactView handles requests to load a single view for a job. This is what viewers 615 // will use to call back to themselves. 616 // Query params: 617 // - name: required, specifies the name of the viewer to load 618 // - src: required, specifies the job source from which to fetch artifacts 619 func handleArtifactView(o options, sg *spyglass.Spyglass, ca *config.Agent) http.HandlerFunc { 620 return func(w http.ResponseWriter, r *http.Request) { 621 setHeadersNoCaching(w) 622 pathSegments := strings.Split(r.URL.Path, "/") 623 if len(pathSegments) != 2 { 624 http.NotFound(w, r) 625 return 626 } 627 lensName := pathSegments[0] 628 resource := pathSegments[1] 629 630 lens, err := lenses.GetLens(lensName) 631 if err != nil { 632 http.Error(w, fmt.Sprintf("No such template: %s (%v)", lensName, err), http.StatusNotFound) 633 return 634 } 635 636 lensResourcesDir := lenses.ResourceDirForLens(o.spyglassFilesLocation, lens.Name()) 637 638 reqString := r.URL.Query().Get("req") 639 var request spyglass.LensRequest 640 err = json.Unmarshal([]byte(reqString), &request) 641 if err != nil { 642 http.Error(w, fmt.Sprintf("Failed to parse request: %v", err), http.StatusBadRequest) 643 return 644 } 645 646 artifacts, err := sg.FetchArtifacts(request.Source, "", ca.Config().Deck.Spyglass.SizeLimit, request.Artifacts) 647 if err != nil { 648 http.Error(w, fmt.Sprintf("Failed to retrieve expected artifacts: %v", err), http.StatusInternalServerError) 649 return 650 } 651 652 switch resource { 653 case "iframe": 654 t, err := template.ParseFiles(path.Join(o.templateFilesLocation, "spyglass-lens.html")) 655 if err != nil { 656 http.Error(w, fmt.Sprintf("Failed to load template: %v", err), http.StatusInternalServerError) 657 return 658 } 659 660 w.Header().Set("Content-Type", "text/html; encoding=utf-8") 661 t.Execute(w, struct { 662 Title string 663 BaseURL string 664 Head template.HTML 665 Body template.HTML 666 }{ 667 lens.Title(), 668 "/spyglass/static/" + lensName + "/", 669 template.HTML(lens.Header(artifacts, lensResourcesDir)), 670 template.HTML(lens.Body(artifacts, lensResourcesDir, "")), 671 }) 672 case "rerender": 673 data, err := ioutil.ReadAll(r.Body) 674 if err != nil { 675 http.Error(w, fmt.Sprintf("Failed to read body: %v", err), http.StatusInternalServerError) 676 return 677 } 678 w.Header().Set("Content-Type", "text/html; encoding=utf-8") 679 w.Write([]byte(lens.Body(artifacts, lensResourcesDir, string(data)))) 680 case "callback": 681 data, err := ioutil.ReadAll(r.Body) 682 if err != nil { 683 http.Error(w, fmt.Sprintf("Failed to read body: %v", err), http.StatusInternalServerError) 684 return 685 } 686 w.Write([]byte(lens.Callback(artifacts, lensResourcesDir, string(data)))) 687 default: 688 http.NotFound(w, r) 689 } 690 } 691 } 692 693 func handleTidePools(ca *config.Agent, ta *tideAgent) http.HandlerFunc { 694 return func(w http.ResponseWriter, r *http.Request) { 695 setHeadersNoCaching(w) 696 queryConfigs := ta.filterHiddenQueries(ca.Config().Tide.Queries) 697 queries := make([]string, 0, len(queryConfigs)) 698 for _, qc := range queryConfigs { 699 queries = append(queries, qc.Query()) 700 } 701 702 ta.Lock() 703 pools := ta.pools 704 ta.Unlock() 705 706 payload := tidePools{ 707 Queries: queries, 708 TideQueries: queryConfigs, 709 Pools: pools, 710 } 711 pd, err := json.Marshal(payload) 712 if err != nil { 713 logrus.WithError(err).Error("Error marshaling payload.") 714 pd = []byte("{}") 715 } 716 // If we have a "var" query, then write out "var value = {...};". 717 // Otherwise, just write out the JSON. 718 if v := r.URL.Query().Get("var"); v != "" { 719 fmt.Fprintf(w, "var %s = %s;", v, string(pd)) 720 } else { 721 fmt.Fprint(w, string(pd)) 722 } 723 } 724 } 725 726 func handleTideHistory(ta *tideAgent) http.HandlerFunc { 727 return func(w http.ResponseWriter, r *http.Request) { 728 setHeadersNoCaching(w) 729 730 ta.Lock() 731 history := ta.history 732 ta.Unlock() 733 734 payload := tideHistory{ 735 History: history, 736 } 737 pd, err := json.Marshal(payload) 738 if err != nil { 739 logrus.WithError(err).Error("Error marshaling payload.") 740 pd = []byte("{}") 741 } 742 // If we have a "var" query, then write out "var value = {...};". 743 // Otherwise, just write out the JSON. 744 if v := r.URL.Query().Get("var"); v != "" { 745 fmt.Fprintf(w, "var %s = %s;", v, string(pd)) 746 } else { 747 fmt.Fprint(w, string(pd)) 748 } 749 } 750 } 751 752 func handlePluginHelp(ha *helpAgent) http.HandlerFunc { 753 return func(w http.ResponseWriter, r *http.Request) { 754 setHeadersNoCaching(w) 755 help, err := ha.getHelp() 756 if err != nil { 757 logrus.WithError(err).Error("Getting plugin help from hook.") 758 help = &pluginhelp.Help{} 759 } 760 b, err := json.Marshal(*help) 761 if err != nil { 762 logrus.WithError(err).Error("Marshaling plugin help.") 763 b = []byte("[]") 764 } 765 // If we have a "var" query, then write out "var value = [...];". 766 // Otherwise, just write out the JSON. 767 if v := r.URL.Query().Get("var"); v != "" { 768 fmt.Fprintf(w, "var %s = %s;", v, string(b)) 769 } else { 770 fmt.Fprint(w, string(b)) 771 } 772 } 773 } 774 775 type logClient interface { 776 GetJobLog(job, id string) ([]byte, error) 777 } 778 779 // TODO(spxtr): Cache, rate limit. 780 func handleLog(lc logClient) http.HandlerFunc { 781 return func(w http.ResponseWriter, r *http.Request) { 782 setHeadersNoCaching(w) 783 w.Header().Set("Access-Control-Allow-Origin", "*") 784 job := r.URL.Query().Get("job") 785 id := r.URL.Query().Get("id") 786 logger := logrus.WithFields(logrus.Fields{"job": job, "id": id}) 787 if err := validateLogRequest(r); err != nil { 788 http.Error(w, err.Error(), http.StatusBadRequest) 789 return 790 } 791 log, err := lc.GetJobLog(job, id) 792 if err != nil { 793 http.Error(w, fmt.Sprintf("Log not found: %v", err), http.StatusNotFound) 794 logger := logger.WithError(err) 795 msg := "Log not found." 796 if strings.Contains(err.Error(), "PodInitializing") { 797 // PodInitializing is really common and not something 798 // that has any actionable items for administrators 799 // monitoring logs, so we should log it as information 800 logger.Info(msg) 801 } else { 802 logger.Warning(msg) 803 } 804 return 805 } 806 if _, err = w.Write(log); err != nil { 807 logger.WithError(err).Warning("Error writing log.") 808 } 809 } 810 } 811 812 func validateLogRequest(r *http.Request) error { 813 job := r.URL.Query().Get("job") 814 id := r.URL.Query().Get("id") 815 816 if job == "" { 817 return errors.New("request did not provide the 'job' query parameter") 818 } 819 if id == "" { 820 return errors.New("request did not provide the 'id' query parameter") 821 } 822 return nil 823 } 824 825 type pjClient interface { 826 GetProwJob(string) (kube.ProwJob, error) 827 } 828 829 func handleRerun(kc pjClient) http.HandlerFunc { 830 return func(w http.ResponseWriter, r *http.Request) { 831 name := r.URL.Query().Get("prowjob") 832 if name == "" { 833 http.Error(w, "request did not provide the 'name' query parameter", http.StatusBadRequest) 834 return 835 } 836 pj, err := kc.GetProwJob(name) 837 if err != nil { 838 http.Error(w, fmt.Sprintf("ProwJob not found: %v", err), http.StatusNotFound) 839 logrus.WithError(err).Warning("ProwJob not found.") 840 return 841 } 842 pjutil := pjutil.NewProwJob(pj.Spec, pj.ObjectMeta.Labels) 843 b, err := yaml.Marshal(&pjutil) 844 if err != nil { 845 http.Error(w, fmt.Sprintf("Error marshaling: %v", err), http.StatusInternalServerError) 846 logrus.WithError(err).Error("Error marshaling jobs.") 847 return 848 } 849 if _, err := w.Write(b); err != nil { 850 logrus.WithError(err).Error("Error writing log.") 851 } 852 } 853 } 854 855 func handleConfig(ca jobs.ConfigAgent) http.HandlerFunc { 856 return func(w http.ResponseWriter, r *http.Request) { 857 // TODO(bentheelder): add the ability to query for portions of the config? 858 setHeadersNoCaching(w) 859 config := ca.Config() 860 b, err := yaml.Marshal(config) 861 if err != nil { 862 logrus.WithError(err).Error("Error marshaling config.") 863 http.Error(w, "Failed to marhshal config.", http.StatusInternalServerError) 864 return 865 } 866 buff := bytes.NewBuffer(b) 867 _, err = buff.WriteTo(w) 868 if err != nil { 869 logrus.WithError(err).Error("Error writing config.") 870 http.Error(w, "Failed to write config.", http.StatusInternalServerError) 871 } 872 } 873 } 874 875 func handleFavicon(staticFilesLocation string, ca jobs.ConfigAgent) http.HandlerFunc { 876 return func(w http.ResponseWriter, r *http.Request) { 877 config := ca.Config() 878 if config.Deck.Branding != nil && config.Deck.Branding.Favicon != "" { 879 http.ServeFile(w, r, staticFilesLocation+"/"+config.Deck.Branding.Favicon) 880 } else { 881 http.ServeFile(w, r, staticFilesLocation+"/favicon.ico") 882 } 883 } 884 } 885 886 func isValidatedGitOAuthConfig(githubOAuthConfig *config.GithubOAuthConfig) bool { 887 return githubOAuthConfig.ClientID != "" && githubOAuthConfig.ClientSecret != "" && 888 githubOAuthConfig.RedirectURL != "" && 889 githubOAuthConfig.FinalRedirectURL != "" 890 }