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  }