github.com/yrj2011/jx-test-infra@v0.0.0-20190529031832-7a2065ee98eb/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  	"encoding/base64"
    22  	"encoding/json"
    23  	"errors"
    24  	"flag"
    25  	"fmt"
    26  	"io/ioutil"
    27  	"net/http"
    28  	"net/url"
    29  	"path"
    30  	"regexp"
    31  	"time"
    32  
    33  	"github.com/NYTimes/gziphandler"
    34  	"github.com/ghodss/yaml"
    35  	"github.com/gorilla/sessions"
    36  	"github.com/sirupsen/logrus"
    37  
    38  	"golang.org/x/oauth2"
    39  	"golang.org/x/oauth2/github"
    40  	"k8s.io/test-infra/prow/config"
    41  	"k8s.io/test-infra/prow/deck/jobs"
    42  	"k8s.io/test-infra/prow/githuboauth"
    43  	"k8s.io/test-infra/prow/kube"
    44  	"k8s.io/test-infra/prow/logrusutil"
    45  	"k8s.io/test-infra/prow/pjutil"
    46  	"k8s.io/test-infra/prow/pluginhelp"
    47  	"k8s.io/test-infra/prow/prstatus"
    48  )
    49  
    50  type options struct {
    51  	configPath            string
    52  	jobConfigPath         string
    53  	buildCluster          string
    54  	tideURL               string
    55  	hookURL               string
    56  	oauthURL              string
    57  	githubOAuthConfigFile string
    58  	cookieSecretFile      string
    59  	redirectHTTPTo        string
    60  	hiddenOnly            bool
    61  	runLocal              bool
    62  }
    63  
    64  func (o *options) Validate() error {
    65  	if o.configPath == "" {
    66  		return errors.New("required flag --config-path was unset")
    67  	}
    68  	if o.oauthURL != "" {
    69  		if o.githubOAuthConfigFile == "" {
    70  			return errors.New("an OAuth URL was provided but required flag --github-oauth-config-file was unset")
    71  		}
    72  		if o.cookieSecretFile == "" {
    73  			return errors.New("an OAuth URL was provided but required flag --cookie-secret was unset")
    74  		}
    75  	}
    76  	return nil
    77  }
    78  
    79  func gatherOptions() options {
    80  	o := options{}
    81  	flag.StringVar(&o.configPath, "config-path", "/etc/config/config.yaml", "Path to config.yaml.")
    82  	flag.StringVar(&o.jobConfigPath, "job-config-path", "", "Path to prow job configs.")
    83  	flag.StringVar(&o.buildCluster, "build-cluster", "", "Path to file containing a YAML-marshalled kube.Cluster object. If empty, uses the local cluster.")
    84  	flag.StringVar(&o.tideURL, "tide-url", "", "Path to tide. If empty, do not serve tide data.")
    85  	flag.StringVar(&o.hookURL, "hook-url", "", "Path to hook plugin help endpoint.")
    86  	flag.StringVar(&o.oauthURL, "oauth-url", "", "Path to deck user dashboard endpoint.")
    87  	flag.StringVar(&o.githubOAuthConfigFile, "github-oauth-config-file", "/etc/github/secret", "Path to the file containing the GitHub App Client secret.")
    88  	flag.StringVar(&o.cookieSecretFile, "cookie-secret", "/etc/cookie/secret", "Path to the file containing the cookie secret key.")
    89  	// use when behind a load balancer
    90  	flag.StringVar(&o.redirectHTTPTo, "redirect-http-to", "", "Host to redirect http->https to based on x-forwarded-proto == http.")
    91  	// use when behind an oauth proxy
    92  	flag.BoolVar(&o.hiddenOnly, "hidden-only", false, "Show only hidden jobs. Useful for serving hidden jobs behind an oauth proxy.")
    93  	flag.BoolVar(&o.runLocal, "run-local", false, "Serve a local copy of the UI, used by the prow/cmd/deck/runlocal script")
    94  	flag.Parse()
    95  	return o
    96  }
    97  
    98  var (
    99  	// Matches letters, numbers, hyphens, and underscores.
   100  	objReg              = regexp.MustCompile(`^[\w-]+$`)
   101  	staticFilesLocation = "./static"
   102  )
   103  
   104  func main() {
   105  	o := gatherOptions()
   106  	if err := o.Validate(); err != nil {
   107  		logrus.Fatalf("Invalid options: %v", err)
   108  	}
   109  
   110  	logrus.SetFormatter(
   111  		logrusutil.NewDefaultFieldsFormatter(nil, logrus.Fields{"component": "deck"}),
   112  	)
   113  
   114  	mux := http.NewServeMux()
   115  
   116  	staticHandlerFromDir := func(dir string) http.Handler {
   117  		return defaultExtension(".html",
   118  			gziphandler.GzipHandler(handleCached(http.FileServer(http.Dir(dir)))))
   119  	}
   120  
   121  	// setup config agent, pod log clients etc.
   122  	configAgent := &config.Agent{}
   123  	if err := configAgent.Start(o.configPath, o.jobConfigPath); err != nil {
   124  		logrus.WithError(err).Fatal("Error starting config agent.")
   125  	}
   126  
   127  	// setup common handlers for local and deployed runs
   128  	mux.Handle("/", staticHandlerFromDir(staticFilesLocation))
   129  	mux.Handle("/config", gziphandler.GzipHandler(handleConfig(configAgent)))
   130  	mux.Handle("/branding.js", gziphandler.GzipHandler(handleBranding(configAgent)))
   131  	mux.Handle("/favicon.ico", gziphandler.GzipHandler(handleFavicon(configAgent)))
   132  
   133  	// when deployed, do the full main
   134  	if !o.runLocal {
   135  		mux = prodOnlyMain(configAgent, o, mux)
   136  	}
   137  
   138  	// setup done, actually start the server
   139  	logrus.WithError(http.ListenAndServe(":8080", mux)).Fatal("ListenAndServe returned.")
   140  }
   141  
   142  // prodOnlyMain contains logic only used when running deployed, not locally
   143  func prodOnlyMain(configAgent *config.Agent, o options, mux *http.ServeMux) *http.ServeMux {
   144  
   145  	kc, err := kube.NewClientInCluster(configAgent.Config().ProwJobNamespace)
   146  	if err != nil {
   147  		logrus.WithError(err).Fatal("Error getting client.")
   148  	}
   149  	kc.SetHiddenReposProvider(func() []string { return configAgent.Config().Deck.HiddenRepos }, o.hiddenOnly)
   150  
   151  	var pkcs map[string]*kube.Client
   152  	if o.buildCluster == "" {
   153  		pkcs = map[string]*kube.Client{kube.DefaultClusterAlias: kc.Namespace(configAgent.Config().PodNamespace)}
   154  	} else {
   155  		pkcs, err = kube.ClientMapFromFile(o.buildCluster, configAgent.Config().PodNamespace)
   156  		if err != nil {
   157  			logrus.WithError(err).Fatal("Error getting kube client to build cluster.")
   158  		}
   159  	}
   160  	plClients := map[string]jobs.PodLogClient{}
   161  	for alias, client := range pkcs {
   162  		plClients[alias] = client
   163  	}
   164  
   165  	ja := jobs.NewJobAgent(kc, plClients, configAgent)
   166  	ja.Start()
   167  
   168  	// setup prod only handlers
   169  	mux.Handle("/data.js", gziphandler.GzipHandler(handleData(ja)))
   170  	mux.Handle("/prowjobs.js", gziphandler.GzipHandler(handleProwJobs(ja)))
   171  	mux.Handle("/badge.svg", gziphandler.GzipHandler(handleBadge(ja)))
   172  	mux.Handle("/log", gziphandler.GzipHandler(handleLog(ja)))
   173  	mux.Handle("/rerun", gziphandler.GzipHandler(handleRerun(kc)))
   174  
   175  	if o.hookURL != "" {
   176  		mux.Handle("/plugin-help.js",
   177  			gziphandler.GzipHandler(handlePluginHelp(newHelpAgent(o.hookURL))))
   178  	}
   179  
   180  	if o.tideURL != "" {
   181  		ta := &tideAgent{
   182  			log:  logrus.WithField("agent", "tide"),
   183  			path: o.tideURL,
   184  			updatePeriod: func() time.Duration {
   185  				return configAgent.Config().Deck.TideUpdatePeriod
   186  			},
   187  			hiddenRepos: configAgent.Config().Deck.HiddenRepos,
   188  			hiddenOnly:  o.hiddenOnly,
   189  		}
   190  		ta.start()
   191  		mux.Handle("/tide.js", gziphandler.GzipHandler(handleTide(configAgent, ta)))
   192  	}
   193  
   194  	// Enable Git OAuth feature if oauthURL is provided.
   195  	if o.oauthURL != "" {
   196  		githubOAuthConfigRaw, err := loadToken(o.githubOAuthConfigFile)
   197  		if err != nil {
   198  			logrus.WithError(err).Fatal("Could not read github oauth config file.")
   199  		}
   200  
   201  		cookieSecretRaw, err := loadToken(o.cookieSecretFile)
   202  		if err != nil {
   203  			logrus.WithError(err).Fatal("Could not read cookie secret file.")
   204  		}
   205  
   206  		var githubOAuthConfig config.GithubOAuthConfig
   207  		if err := yaml.Unmarshal(githubOAuthConfigRaw, &githubOAuthConfig); err != nil {
   208  			logrus.WithError(err).Fatal("Error unmarshalling github oauth config")
   209  		}
   210  		if !isValidatedGitOAuthConfig(&githubOAuthConfig) {
   211  			logrus.Fatal("Error invalid github oauth config")
   212  		}
   213  
   214  		decodedSecret, err := base64.StdEncoding.DecodeString(string(cookieSecretRaw))
   215  		if err != nil {
   216  			logrus.WithError(err).Fatal("Error decoding cookie secret")
   217  		}
   218  		if len(decodedSecret) == 0 {
   219  			logrus.Fatal("Cookie secret should not be empty")
   220  		}
   221  		cookie := sessions.NewCookieStore(decodedSecret)
   222  		githubOAuthConfig.InitGithubOAuthConfig(cookie)
   223  
   224  		goa := githuboauth.NewGithubOAuthAgent(&githubOAuthConfig, logrus.WithField("client", "githuboauth"))
   225  		oauthClient := &oauth2.Config{
   226  			ClientID:     githubOAuthConfig.ClientID,
   227  			ClientSecret: githubOAuthConfig.ClientSecret,
   228  			RedirectURL:  githubOAuthConfig.RedirectURL,
   229  			Scopes:       githubOAuthConfig.Scopes,
   230  			Endpoint:     github.Endpoint,
   231  		}
   232  
   233  		repoSet := make(map[string]bool)
   234  		for r := range configAgent.Config().Presubmits {
   235  			repoSet[r] = true
   236  		}
   237  		for _, q := range configAgent.Config().Tide.Queries {
   238  			for _, v := range q.Repos {
   239  				repoSet[v] = true
   240  			}
   241  		}
   242  		var repos []string
   243  		for k, v := range repoSet {
   244  			if v {
   245  				repos = append(repos, k)
   246  			}
   247  		}
   248  
   249  		prStatusAgent := prstatus.NewDashboardAgent(
   250  			repos,
   251  			&githubOAuthConfig,
   252  			logrus.WithField("client", "pr-status"))
   253  
   254  		mux.Handle("/pr-data.js", handleNotCached(
   255  			prStatusAgent.HandlePrStatus(prStatusAgent)))
   256  		// Handles login request.
   257  		mux.Handle("/github-login", goa.HandleLogin(oauthClient))
   258  		// Handles redirect from Github OAuth server.
   259  		mux.Handle("/github-login/redirect", goa.HandleRedirect(oauthClient, githuboauth.NewGithubClientGetter()))
   260  	}
   261  
   262  	// optionally inject http->https redirect handler when behind loadbalancer
   263  	if o.redirectHTTPTo != "" {
   264  		redirectMux := http.NewServeMux()
   265  		redirectMux.Handle("/", func(oldMux *http.ServeMux, host string) http.HandlerFunc {
   266  			return func(w http.ResponseWriter, r *http.Request) {
   267  				if r.Header.Get("x-forwarded-proto") == "http" {
   268  					redirectURL, err := url.Parse(r.URL.String())
   269  					if err != nil {
   270  						logrus.Errorf("Failed to parse URL: %s.", r.URL.String())
   271  						http.Error(w, "Failed to perform https redirect.", http.StatusInternalServerError)
   272  						return
   273  					}
   274  					redirectURL.Scheme = "https"
   275  					redirectURL.Host = host
   276  					http.Redirect(w, r, redirectURL.String(), http.StatusMovedPermanently)
   277  				} else {
   278  					oldMux.ServeHTTP(w, r)
   279  				}
   280  			}
   281  		}(mux, o.redirectHTTPTo))
   282  		mux = redirectMux
   283  	}
   284  	return mux
   285  }
   286  
   287  func loadToken(file string) ([]byte, error) {
   288  	raw, err := ioutil.ReadFile(file)
   289  	if err != nil {
   290  		return []byte{}, err
   291  	}
   292  	return bytes.TrimSpace(raw), nil
   293  }
   294  
   295  // copy a http.Request
   296  // see: https://go-review.googlesource.com/c/go/+/36483/3/src/net/http/server.go
   297  func dupeRequest(original *http.Request) *http.Request {
   298  	r2 := new(http.Request)
   299  	*r2 = *original
   300  	r2.URL = new(url.URL)
   301  	*r2.URL = *original.URL
   302  	return r2
   303  }
   304  
   305  // serve with handler but map extensionless URLs to the default
   306  func defaultExtension(extension string, h http.Handler) http.Handler {
   307  	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   308  		if len(r.URL.Path) > 0 &&
   309  			r.URL.Path[len(r.URL.Path)-1] != '/' && path.Ext(r.URL.Path) == "" {
   310  			r2 := dupeRequest(r)
   311  			r2.URL.Path = r.URL.Path + extension
   312  			h.ServeHTTP(w, r2)
   313  		} else {
   314  			h.ServeHTTP(w, r)
   315  		}
   316  	})
   317  }
   318  
   319  func handleCached(next http.Handler) http.Handler {
   320  	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   321  		// This looks ridiculous but actually no-cache means "revalidate" and
   322  		// "max-age=0" just means there is no time in which it can skip
   323  		// revalidation. We also need to set must-revalidate because no-cache
   324  		// doesn't imply must-revalidate when using the back button
   325  		// https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.1
   326  		// TODO(bentheelder): consider setting a longer max-age
   327  		// setting it this way means the content is always revalidated
   328  		w.Header().Set("Cache-Control", "public, max-age=0, no-cache, must-revalidate")
   329  		next.ServeHTTP(w, r)
   330  	})
   331  }
   332  
   333  func setHeadersNoCaching(w http.ResponseWriter) {
   334  	// Note that we need to set both no-cache and no-store because only some
   335  	// broswers decided to (incorrectly) treat no-cache as "never store"
   336  	// IE "no-store". for good measure to cover older browsers we also set
   337  	// expires and pragma: https://stackoverflow.com/a/2068407
   338  	w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
   339  	w.Header().Set("Pragma", "no-cache")
   340  	w.Header().Set("Expires", "0")
   341  }
   342  
   343  func handleNotCached(next http.Handler) http.HandlerFunc {
   344  	return func(w http.ResponseWriter, r *http.Request) {
   345  		setHeadersNoCaching(w)
   346  		next.ServeHTTP(w, r)
   347  	}
   348  }
   349  
   350  func handleProwJobs(ja *jobs.JobAgent) http.HandlerFunc {
   351  	return func(w http.ResponseWriter, r *http.Request) {
   352  		setHeadersNoCaching(w)
   353  		jobs := ja.ProwJobs()
   354  		if v := r.URL.Query().Get("omit"); v == "pod_spec" {
   355  			for i := range jobs {
   356  				jobs[i].Spec.PodSpec = nil
   357  			}
   358  		}
   359  		jd, err := json.Marshal(struct {
   360  			Items []kube.ProwJob `json:"items"`
   361  		}{jobs})
   362  		if err != nil {
   363  			logrus.WithError(err).Error("Error marshaling jobs.")
   364  			jd = []byte("{}")
   365  		}
   366  		// If we have a "var" query, then write out "var value = {...};".
   367  		// Otherwise, just write out the JSON.
   368  		if v := r.URL.Query().Get("var"); v != "" {
   369  			fmt.Fprintf(w, "var %s = %s;", v, string(jd))
   370  		} else {
   371  			fmt.Fprint(w, string(jd))
   372  		}
   373  	}
   374  }
   375  
   376  func handleData(ja *jobs.JobAgent) http.HandlerFunc {
   377  	return func(w http.ResponseWriter, r *http.Request) {
   378  		setHeadersNoCaching(w)
   379  		jobs := ja.Jobs()
   380  		jd, err := json.Marshal(jobs)
   381  		if err != nil {
   382  			logrus.WithError(err).Error("Error marshaling jobs.")
   383  			jd = []byte("[]")
   384  		}
   385  		// If we have a "var" query, then write out "var value = {...};".
   386  		// Otherwise, just write out the JSON.
   387  		if v := r.URL.Query().Get("var"); v != "" {
   388  			fmt.Fprintf(w, "var %s = %s;", v, string(jd))
   389  		} else {
   390  			fmt.Fprint(w, string(jd))
   391  		}
   392  	}
   393  }
   394  
   395  func handleBadge(ja *jobs.JobAgent) http.HandlerFunc {
   396  	return func(w http.ResponseWriter, r *http.Request) {
   397  		setHeadersNoCaching(w)
   398  		wantJobs := r.URL.Query().Get("jobs")
   399  		if wantJobs == "" {
   400  			http.Error(w, "missing jobs query parameter", http.StatusBadRequest)
   401  			return
   402  		}
   403  		w.Header().Set("Content-Type", "image/svg+xml")
   404  
   405  		allJobs := ja.ProwJobs()
   406  		_, _, svg := renderBadge(pickLatestJobs(allJobs, wantJobs))
   407  		w.Write(svg)
   408  	}
   409  }
   410  
   411  func handleTide(ca *config.Agent, ta *tideAgent) http.HandlerFunc {
   412  	return func(w http.ResponseWriter, r *http.Request) {
   413  		setHeadersNoCaching(w)
   414  		queryConfigs := ca.Config().Tide.Queries
   415  
   416  		ta.Lock()
   417  		defer ta.Unlock()
   418  		pools := ta.pools
   419  		queryConfigs, pools = ta.filterHidden(queryConfigs, pools)
   420  		queries := make([]string, 0, len(queryConfigs))
   421  		for _, qc := range queryConfigs {
   422  			queries = append(queries, qc.Query())
   423  		}
   424  
   425  		payload := tideData{
   426  			Queries:     queries,
   427  			TideQueries: queryConfigs,
   428  			Pools:       pools,
   429  		}
   430  		pd, err := json.Marshal(payload)
   431  		if err != nil {
   432  			logrus.WithError(err).Error("Error marshaling payload.")
   433  			pd = []byte("{}")
   434  		}
   435  		// If we have a "var" query, then write out "var value = {...};".
   436  		// Otherwise, just write out the JSON.
   437  		if v := r.URL.Query().Get("var"); v != "" {
   438  			fmt.Fprintf(w, "var %s = %s;", v, string(pd))
   439  		} else {
   440  			fmt.Fprint(w, string(pd))
   441  		}
   442  
   443  	}
   444  }
   445  
   446  func handlePluginHelp(ha *helpAgent) http.HandlerFunc {
   447  	return func(w http.ResponseWriter, r *http.Request) {
   448  		setHeadersNoCaching(w)
   449  		help, err := ha.getHelp()
   450  		if err != nil {
   451  			logrus.WithError(err).Error("Getting plugin help from hook.")
   452  			help = &pluginhelp.Help{}
   453  		}
   454  		b, err := json.Marshal(*help)
   455  		if err != nil {
   456  			logrus.WithError(err).Error("Marshaling plugin help.")
   457  			b = []byte("[]")
   458  		}
   459  		// If we have a "var" query, then write out "var value = [...];".
   460  		// Otherwise, just write out the JSON.
   461  		if v := r.URL.Query().Get("var"); v != "" {
   462  			fmt.Fprintf(w, "var %s = %s;", v, string(b))
   463  		} else {
   464  			fmt.Fprint(w, string(b))
   465  		}
   466  	}
   467  }
   468  
   469  type logClient interface {
   470  	GetJobLog(job, id string) ([]byte, error)
   471  }
   472  
   473  // TODO(spxtr): Cache, rate limit.
   474  func handleLog(lc logClient) http.HandlerFunc {
   475  	return func(w http.ResponseWriter, r *http.Request) {
   476  		setHeadersNoCaching(w)
   477  		w.Header().Set("Access-Control-Allow-Origin", "*")
   478  		job := r.URL.Query().Get("job")
   479  		id := r.URL.Query().Get("id")
   480  		if err := validateLogRequest(r); err != nil {
   481  			http.Error(w, err.Error(), http.StatusBadRequest)
   482  			return
   483  		}
   484  		log, err := lc.GetJobLog(job, id)
   485  		if err != nil {
   486  			http.Error(w, fmt.Sprintf("Log not found: %v", err), http.StatusNotFound)
   487  			logrus.WithError(err).Warning("Log not found.")
   488  			return
   489  		}
   490  		if _, err = w.Write(log); err != nil {
   491  			logrus.WithError(err).Warning("Error writing log.")
   492  		}
   493  	}
   494  }
   495  
   496  func validateLogRequest(r *http.Request) error {
   497  	job := r.URL.Query().Get("job")
   498  	id := r.URL.Query().Get("id")
   499  
   500  	if job == "" {
   501  		return errors.New("Missing job query")
   502  	}
   503  	if id == "" {
   504  		return errors.New("Missing ID query")
   505  	}
   506  	if !objReg.MatchString(job) {
   507  		return fmt.Errorf("Invalid job query: %s", job)
   508  	}
   509  	if !objReg.MatchString(id) {
   510  		return fmt.Errorf("Invalid ID query: %s", id)
   511  	}
   512  	return nil
   513  }
   514  
   515  type pjClient interface {
   516  	GetProwJob(string) (kube.ProwJob, error)
   517  }
   518  
   519  func handleRerun(kc pjClient) http.HandlerFunc {
   520  	return func(w http.ResponseWriter, r *http.Request) {
   521  		name := r.URL.Query().Get("prowjob")
   522  		if !objReg.MatchString(name) {
   523  			http.Error(w, "Invalid ProwJob query", http.StatusBadRequest)
   524  			return
   525  		}
   526  		pj, err := kc.GetProwJob(name)
   527  		if err != nil {
   528  			http.Error(w, fmt.Sprintf("ProwJob not found: %v", err), http.StatusNotFound)
   529  			logrus.WithError(err).Warning("ProwJob not found.")
   530  			return
   531  		}
   532  		pjutil := pjutil.NewProwJob(pj.Spec, pj.ObjectMeta.Labels)
   533  		b, err := yaml.Marshal(&pjutil)
   534  		if err != nil {
   535  			http.Error(w, fmt.Sprintf("Error marshaling: %v", err), http.StatusInternalServerError)
   536  			logrus.WithError(err).Error("Error marshaling jobs.")
   537  			return
   538  		}
   539  		if _, err := w.Write(b); err != nil {
   540  			logrus.WithError(err).Error("Error writing log.")
   541  		}
   542  	}
   543  }
   544  
   545  func handleConfig(ca jobs.ConfigAgent) http.HandlerFunc {
   546  	return func(w http.ResponseWriter, r *http.Request) {
   547  		// TODO(bentheelder): add the ability to query for portions of the config?
   548  		setHeadersNoCaching(w)
   549  		config := ca.Config()
   550  		b, err := yaml.Marshal(config)
   551  		if err != nil {
   552  			logrus.WithError(err).Error("Error marshaling config.")
   553  			http.Error(w, "Failed to marhshal config.", http.StatusInternalServerError)
   554  			return
   555  		}
   556  		buff := bytes.NewBuffer(b)
   557  		_, err = buff.WriteTo(w)
   558  		if err != nil {
   559  			logrus.WithError(err).Error("Error writing config.")
   560  			http.Error(w, "Failed to write config.", http.StatusInternalServerError)
   561  		}
   562  	}
   563  }
   564  
   565  func handleBranding(ca jobs.ConfigAgent) http.HandlerFunc {
   566  	return func(w http.ResponseWriter, r *http.Request) {
   567  		setHeadersNoCaching(w)
   568  		config := ca.Config()
   569  		b, err := json.Marshal(config.Deck.Branding)
   570  		if err != nil {
   571  			logrus.WithError(err).Error("Error marshaling branding config.")
   572  			http.Error(w, "Failed to marshal branding config.", http.StatusInternalServerError)
   573  			return
   574  		}
   575  		// If we have a "var" query, then write out "var value = [...];".
   576  		// Otherwise, just write out the JSON.
   577  		if v := r.URL.Query().Get("var"); v != "" {
   578  			fmt.Fprintf(w, "var %s = %s;", v, string(b))
   579  		} else {
   580  			fmt.Fprint(w, string(b))
   581  		}
   582  	}
   583  }
   584  
   585  func handleFavicon(ca jobs.ConfigAgent) http.HandlerFunc {
   586  	return func(w http.ResponseWriter, r *http.Request) {
   587  		config := ca.Config()
   588  		if config.Deck.Branding != nil && config.Deck.Branding.Favicon != "" {
   589  			http.ServeFile(w, r, staticFilesLocation+"/"+config.Deck.Branding.Favicon)
   590  		} else {
   591  			http.ServeFile(w, r, staticFilesLocation+"/favicon.ico")
   592  		}
   593  	}
   594  }
   595  
   596  func isValidatedGitOAuthConfig(githubOAuthConfig *config.GithubOAuthConfig) bool {
   597  	return githubOAuthConfig.ClientID != "" && githubOAuthConfig.ClientSecret != "" &&
   598  		githubOAuthConfig.RedirectURL != "" &&
   599  		githubOAuthConfig.FinalRedirectURL != ""
   600  }