github.com/shashidharatd/test-infra@v0.0.0-20171006011030-71304e1ca560/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  	"bufio"
    21  	"bytes"
    22  	"encoding/json"
    23  	"flag"
    24  	"fmt"
    25  	"io"
    26  	"io/ioutil"
    27  	"net/http"
    28  	"net/url"
    29  	"regexp"
    30  	"strconv"
    31  
    32  	"github.com/NYTimes/gziphandler"
    33  	"github.com/ghodss/yaml"
    34  	"github.com/sirupsen/logrus"
    35  
    36  	"k8s.io/test-infra/prow/config"
    37  	"k8s.io/test-infra/prow/jenkins"
    38  	"k8s.io/test-infra/prow/kube"
    39  	"k8s.io/test-infra/prow/pjutil"
    40  )
    41  
    42  var (
    43  	configPath   = flag.String("config-path", "/etc/config/config", "Path to config.yaml.")
    44  	buildCluster = flag.String("build-cluster", "", "Path to file containing a YAML-marshalled kube.Cluster object. If empty, uses the local cluster.")
    45  
    46  	jenkinsURL             = flag.String("jenkins-url", "", "Jenkins URL")
    47  	jenkinsUserName        = flag.String("jenkins-user", "jenkins-trigger", "Jenkins username")
    48  	jenkinsTokenFile       = flag.String("jenkins-token-file", "", "Path to the file containing the Jenkins API token.")
    49  	jenkinsBearerTokenFile = flag.String("jenkins-bearer-token-file", "", "Path to the file containing the Jenkins API bearer token.")
    50  )
    51  
    52  // Matches letters, numbers, hyphens, and underscores.
    53  var objReg = regexp.MustCompile(`^[\w-]+$`)
    54  
    55  func main() {
    56  	flag.Parse()
    57  	logrus.SetFormatter(&logrus.JSONFormatter{})
    58  
    59  	configAgent := &config.Agent{}
    60  	if err := configAgent.Start(*configPath); err != nil {
    61  		logrus.WithError(err).Fatal("Error starting config agent.")
    62  	}
    63  
    64  	kc, err := kube.NewClientInCluster(configAgent.Config().ProwJobNamespace)
    65  	if err != nil {
    66  		logrus.WithError(err).Fatal("Error getting client.")
    67  	}
    68  	var pkc *kube.Client
    69  	if *buildCluster == "" {
    70  		pkc = kc.Namespace(configAgent.Config().PodNamespace)
    71  	} else {
    72  		pkc, err = kube.NewClientFromFile(*buildCluster, configAgent.Config().PodNamespace)
    73  		if err != nil {
    74  			logrus.WithError(err).Fatal("Error getting kube client to build cluster.")
    75  		}
    76  	}
    77  
    78  	ac := &jenkins.AuthConfig{}
    79  	var jc *jenkins.Client
    80  	if *jenkinsURL != "" {
    81  		if *jenkinsTokenFile != "" {
    82  			if token, err := loadToken(*jenkinsTokenFile); err != nil {
    83  				logrus.WithError(err).Fatalf("Could not read token file.")
    84  			} else {
    85  				ac.Basic = &jenkins.BasicAuthConfig{
    86  					User:  *jenkinsUserName,
    87  					Token: token,
    88  				}
    89  			}
    90  		} else if *jenkinsBearerTokenFile != "" {
    91  			if token, err := loadToken(*jenkinsBearerTokenFile); err != nil {
    92  				logrus.WithError(err).Fatalf("Could not read token file.")
    93  			} else {
    94  				ac.BearerToken = &jenkins.BearerTokenAuthConfig{
    95  					Token: token,
    96  				}
    97  			}
    98  		} else {
    99  			logrus.Fatal("An auth token for basic or bearer token auth must be supplied.")
   100  		}
   101  		jc = jenkins.NewClient(*jenkinsURL, ac)
   102  	}
   103  
   104  	ja := &JobAgent{
   105  		kc:  kc,
   106  		pkc: pkc,
   107  		jc:  jc,
   108  	}
   109  	ja.Start()
   110  
   111  	http.Handle("/", gziphandler.GzipHandler(http.FileServer(http.Dir("/static"))))
   112  	http.Handle("/data.js", gziphandler.GzipHandler(handleData(ja)))
   113  	http.Handle("/log", gziphandler.GzipHandler(handleLog(ja)))
   114  	http.Handle("/rerun", gziphandler.GzipHandler(handleRerun(kc)))
   115  
   116  	logrus.WithError(http.ListenAndServe(":8080", nil)).Fatal("ListenAndServe returned.")
   117  }
   118  
   119  func loadToken(file string) (string, error) {
   120  	raw, err := ioutil.ReadFile(file)
   121  	if err != nil {
   122  		return "", err
   123  	}
   124  	return string(bytes.TrimSpace(raw)), nil
   125  }
   126  
   127  func handleData(ja *JobAgent) http.HandlerFunc {
   128  	return func(w http.ResponseWriter, r *http.Request) {
   129  		w.Header().Set("Cache-Control", "no-cache")
   130  		jobs := ja.Jobs()
   131  		jd, err := json.Marshal(jobs)
   132  		if err != nil {
   133  			logrus.WithError(err).Error("Error marshaling jobs.")
   134  			jd = []byte("[]")
   135  		}
   136  		// If we have a "var" query, then write out "var value = {...};".
   137  		// Otherwise, just write out the JSON.
   138  		if v := r.URL.Query().Get("var"); v != "" {
   139  			fmt.Fprintf(w, "var %s = %s;", v, string(jd))
   140  		} else {
   141  			fmt.Fprintf(w, string(jd))
   142  		}
   143  	}
   144  }
   145  
   146  type logClient interface {
   147  	GetJobLog(job, id string) ([]byte, error)
   148  	// Add ability to stream logs with options enabled. This call is used to follow logs
   149  	// using kubernetes client API. All other options on the Kubernetes log api can
   150  	// also be enabled.
   151  	GetJobLogStream(job, id string, options map[string]string) (io.ReadCloser, error)
   152  }
   153  
   154  func httpChunking(log io.ReadCloser, w http.ResponseWriter) {
   155  	flusher, ok := w.(http.Flusher)
   156  	if !ok {
   157  		logrus.Warning("Error getting flusher.")
   158  	}
   159  	reader := bufio.NewReader(log)
   160  	for {
   161  		line, err := reader.ReadBytes('\n')
   162  		if err != nil {
   163  			// TODO(rmmh): The log stops streaming after 30s.
   164  			// This seems to be an apiserver limitation-- investigate?
   165  			// logrus.WithError(err).Error("chunk failed to read!")
   166  			break
   167  		}
   168  		w.Write(line)
   169  		if flusher != nil {
   170  			flusher.Flush()
   171  		}
   172  	}
   173  }
   174  
   175  func getOptions(values url.Values) map[string]string {
   176  	options := make(map[string]string)
   177  	for k, v := range values {
   178  		if k != "pod" && k != "job" && k != "id" {
   179  			options[k] = v[0]
   180  		}
   181  	}
   182  	return options
   183  }
   184  
   185  // TODO(spxtr): Cache, rate limit.
   186  func handleLog(lc logClient) http.HandlerFunc {
   187  	return func(w http.ResponseWriter, r *http.Request) {
   188  		w.Header().Set("Cache-Control", "no-cache")
   189  		w.Header().Set("Access-Control-Allow-Origin", "*")
   190  		job := r.URL.Query().Get("job")
   191  		id := r.URL.Query().Get("id")
   192  		stream := r.URL.Query().Get("follow")
   193  		var logStreamRequested bool
   194  		if ok, _ := strconv.ParseBool(stream); ok {
   195  			// get http chunked responses to the client
   196  			w.Header().Set("Connection", "Keep-Alive")
   197  			w.Header().Set("Transfer-Encoding", "chunked")
   198  			logStreamRequested = true
   199  		}
   200  		if job != "" && id != "" {
   201  			if !objReg.MatchString(job) {
   202  				http.Error(w, "Invalid job query", http.StatusBadRequest)
   203  				return
   204  			}
   205  			if !objReg.MatchString(id) {
   206  				http.Error(w, "Invalid ID query", http.StatusBadRequest)
   207  				return
   208  			}
   209  			if !logStreamRequested {
   210  				log, err := lc.GetJobLog(job, id)
   211  				if err != nil {
   212  					http.Error(w, fmt.Sprintf("Log not found: %v", err), http.StatusNotFound)
   213  					logrus.WithError(err).Warning("Error returned.")
   214  					return
   215  				}
   216  				if _, err = w.Write(log); err != nil {
   217  					logrus.WithError(err).Warning("Error writing log.")
   218  				}
   219  			} else {
   220  				//run http chunking
   221  				options := getOptions(r.URL.Query())
   222  				log, err := lc.GetJobLogStream(job, id, options)
   223  				if err != nil {
   224  					http.Error(w, fmt.Sprintf("Log stream caused: %v", err), http.StatusNotFound)
   225  					logrus.WithError(err).Warning("Error returned.")
   226  					return
   227  				}
   228  				httpChunking(log, w)
   229  			}
   230  		} else {
   231  			http.Error(w, "Missing job and ID query", http.StatusBadRequest)
   232  			return
   233  		}
   234  	}
   235  }
   236  
   237  type pjClient interface {
   238  	GetProwJob(string) (kube.ProwJob, error)
   239  }
   240  
   241  func handleRerun(kc pjClient) http.HandlerFunc {
   242  	return func(w http.ResponseWriter, r *http.Request) {
   243  		name := r.URL.Query().Get("prowjob")
   244  		if !objReg.MatchString(name) {
   245  			http.Error(w, "Invalid ProwJob query", http.StatusBadRequest)
   246  			return
   247  		}
   248  		pj, err := kc.GetProwJob(name)
   249  		if err != nil {
   250  			http.Error(w, fmt.Sprintf("ProwJob not found: %v", err), http.StatusNotFound)
   251  			logrus.WithError(err).Warning("Error returned.")
   252  			return
   253  		}
   254  		pjutil := pjutil.NewProwJob(pj.Spec)
   255  		b, err := yaml.Marshal(&pjutil)
   256  		if err != nil {
   257  			http.Error(w, fmt.Sprintf("Error marshaling: %v", err), http.StatusInternalServerError)
   258  			logrus.WithError(err).Error("Error marshaling jobs.")
   259  			return
   260  		}
   261  		if _, err := w.Write(b); err != nil {
   262  			logrus.WithError(err).Error("Error writing log.")
   263  		}
   264  	}
   265  }