github.com/munnerz/test-infra@v0.0.0-20190108210205-ce3d181dc989/prow/deck/jobs/jobs.go (about)

     1  /*
     2  Copyright 2018 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 jobs implements methods on job information used by Prow component deck
    18  package jobs
    19  
    20  import (
    21  	"bytes"
    22  	"errors"
    23  	"fmt"
    24  	"io/ioutil"
    25  	"net/http"
    26  	"regexp"
    27  	"sort"
    28  	"sync"
    29  	"time"
    30  
    31  	"github.com/sirupsen/logrus"
    32  
    33  	"k8s.io/apimachinery/pkg/labels"
    34  	"k8s.io/test-infra/prow/config"
    35  	"k8s.io/test-infra/prow/kube"
    36  )
    37  
    38  const (
    39  	period = 30 * time.Second
    40  )
    41  
    42  var (
    43  	errProwjobNotFound = errors.New("prowjob not found")
    44  )
    45  
    46  // Job holds information about a job prow is running/has run.
    47  // TODO(#5216): Remove this, and all associated machinery.
    48  type Job struct {
    49  	Type        string            `json:"type"`
    50  	Repo        string            `json:"repo"`
    51  	Refs        string            `json:"refs"`
    52  	BaseRef     string            `json:"base_ref"`
    53  	BaseSHA     string            `json:"base_sha"`
    54  	PullSHA     string            `json:"pull_sha"`
    55  	Number      int               `json:"number"`
    56  	Author      string            `json:"author"`
    57  	Job         string            `json:"job"`
    58  	BuildID     string            `json:"build_id"`
    59  	Context     string            `json:"context"`
    60  	Started     string            `json:"started"`
    61  	Finished    string            `json:"finished"`
    62  	Duration    string            `json:"duration"`
    63  	State       string            `json:"state"`
    64  	Description string            `json:"description"`
    65  	URL         string            `json:"url"`
    66  	PodName     string            `json:"pod_name"`
    67  	Agent       kube.ProwJobAgent `json:"agent"`
    68  	ProwJob     string            `json:"prow_job"`
    69  
    70  	st time.Time
    71  	ft time.Time
    72  }
    73  
    74  type serviceClusterClient interface {
    75  	GetLog(pod string) ([]byte, error)
    76  	ListPods(selector string) ([]kube.Pod, error)
    77  	ListProwJobs(selector string) ([]kube.ProwJob, error)
    78  }
    79  
    80  // PodLogClient is an interface for interacting with the pod logs.
    81  type PodLogClient interface {
    82  	// GetContainerLog returns the pod log of the specified container
    83  	GetContainerLog(pod, container string) ([]byte, error)
    84  	// GetLogTail returns the last n bytes of the pod log of the specified container
    85  	GetLogTail(pod, container string, n int64) ([]byte, error)
    86  }
    87  
    88  // ConfigAgent is an interface to get the agent Config.
    89  type ConfigAgent interface {
    90  	Config() *config.Config
    91  }
    92  
    93  // NewJobAgent is a JobAgent constructor.
    94  func NewJobAgent(kc serviceClusterClient, plClients map[string]PodLogClient, ca ConfigAgent) *JobAgent {
    95  	return &JobAgent{
    96  		kc:   kc,
    97  		pkcs: plClients,
    98  		c:    ca,
    99  	}
   100  }
   101  
   102  // JobAgent creates lists of jobs, updates their status and returns their run logs.
   103  type JobAgent struct {
   104  	kc        serviceClusterClient
   105  	pkcs      map[string]PodLogClient
   106  	c         ConfigAgent
   107  	prowJobs  []kube.ProwJob
   108  	jobs      []Job
   109  	jobsMap   map[string]Job                     // pod name -> Job
   110  	jobsIDMap map[string]map[string]kube.ProwJob // job name -> id -> ProwJob
   111  	mut       sync.Mutex
   112  }
   113  
   114  // Start will start the job and periodically update it.
   115  func (ja *JobAgent) Start() {
   116  	ja.tryUpdate()
   117  	go func() {
   118  		t := time.Tick(period)
   119  		for range t {
   120  			ja.tryUpdate()
   121  		}
   122  	}()
   123  }
   124  
   125  // Jobs returns a thread-safe snapshot of the current job state.
   126  func (ja *JobAgent) Jobs() []Job {
   127  	ja.mut.Lock()
   128  	defer ja.mut.Unlock()
   129  	res := make([]Job, len(ja.jobs))
   130  	copy(res, ja.jobs)
   131  	return res
   132  }
   133  
   134  // ProwJobs returns a thread-safe snapshot of the current prow jobs.
   135  func (ja *JobAgent) ProwJobs() []kube.ProwJob {
   136  	ja.mut.Lock()
   137  	defer ja.mut.Unlock()
   138  	res := make([]kube.ProwJob, len(ja.prowJobs))
   139  	copy(res, ja.prowJobs)
   140  	return res
   141  }
   142  
   143  var jobNameRE = regexp.MustCompile(`^([\w-]+)-(\d+)$`)
   144  
   145  // GetProwJob finds the corresponding Prowjob resource from the provided job name and build ID
   146  func (ja *JobAgent) GetProwJob(job, id string) (kube.ProwJob, error) {
   147  	if ja == nil {
   148  		return kube.ProwJob{}, fmt.Errorf("Prow job agent doesn't exist (are you running locally?)")
   149  	}
   150  	var j kube.ProwJob
   151  	ja.mut.Lock()
   152  	idMap, ok := ja.jobsIDMap[job]
   153  	if ok {
   154  		j, ok = idMap[id]
   155  	}
   156  	ja.mut.Unlock()
   157  	if !ok {
   158  		return kube.ProwJob{}, errProwjobNotFound
   159  	}
   160  	return j, nil
   161  }
   162  
   163  // GetJobLogTail returns the last n bytes of the job logs, works for both kubernetes and jenkins agent types.
   164  func (ja *JobAgent) GetJobLogTail(job, id string, n int64) ([]byte, error) {
   165  	j, err := ja.GetProwJob(job, id)
   166  	if err != nil {
   167  		return nil, fmt.Errorf("error getting prowjob: %v", err)
   168  	}
   169  	if j.Spec.Agent == kube.KubernetesAgent {
   170  		client, ok := ja.pkcs[j.ClusterAlias()]
   171  		if !ok {
   172  			return nil, fmt.Errorf("cannot get logs for prowjob %q with agent %q: unknown cluster alias %q", j.ObjectMeta.Name, j.Spec.Agent, j.ClusterAlias())
   173  		}
   174  		return client.GetLogTail(j.Status.PodName, kube.TestContainerName, n)
   175  	}
   176  	for _, agentToTmpl := range ja.c.Config().Deck.ExternalAgentLogs {
   177  		if agentToTmpl.Agent != string(j.Spec.Agent) {
   178  			continue
   179  		}
   180  		if !agentToTmpl.Selector.Matches(labels.Set(j.ObjectMeta.Labels)) {
   181  			continue
   182  		}
   183  		var b bytes.Buffer
   184  		if err := agentToTmpl.URLTemplate.Execute(&b, &j); err != nil {
   185  			return nil, fmt.Errorf("cannot execute URL template for prowjob %q with agent %q: %v", j.ObjectMeta.Name, j.Spec.Agent, err)
   186  		}
   187  		resp, err := http.Get(b.String())
   188  		if err != nil {
   189  			return nil, err
   190  		}
   191  		defer resp.Body.Close()
   192  		content, err := ioutil.ReadAll(resp.Body)
   193  		if err != nil {
   194  			return nil, err
   195  		}
   196  		lenContent := int64(len(content))
   197  		bytesToRead := n
   198  		if lenContent < bytesToRead {
   199  			bytesToRead = lenContent
   200  			logrus.WithField("contentLen", lenContent).Warn("Tried to read more pod logs than exist, reading all instead")
   201  		}
   202  		cr := bytes.NewReader(content)
   203  		contentTail := make([]byte, bytesToRead)
   204  		bytesRead, err := cr.ReadAt(contentTail, lenContent-bytesToRead)
   205  		if int64(bytesRead) < bytesToRead {
   206  			logrus.WithFields(logrus.Fields{"prowjob": j.ObjectMeta.Name, "bytesRead": bytesRead, "bytesIntended": bytesToRead}).Error("Read fewer bytes than intended")
   207  		}
   208  		return contentTail, err
   209  	}
   210  	return nil, fmt.Errorf("cannot get logs for prowjob %q with agent %q: the agent is missing from the prow config file", j.ObjectMeta.Name, j.Spec.Agent)
   211  }
   212  
   213  // GetJobLog returns the job logs, works for both kubernetes and jenkins agent types.
   214  func (ja *JobAgent) GetJobLog(job, id string) ([]byte, error) {
   215  	j, err := ja.GetProwJob(job, id)
   216  	if err != nil {
   217  		return nil, fmt.Errorf("error getting prowjob: %v", err)
   218  	}
   219  	if j.Spec.Agent == kube.KubernetesAgent {
   220  		client, ok := ja.pkcs[j.ClusterAlias()]
   221  		if !ok {
   222  			return nil, fmt.Errorf("cannot get logs for prowjob %q with agent %q: unknown cluster alias %q", j.ObjectMeta.Name, j.Spec.Agent, j.ClusterAlias())
   223  		}
   224  		return client.GetContainerLog(j.Status.PodName, kube.TestContainerName)
   225  	}
   226  	for _, agentToTmpl := range ja.c.Config().Deck.ExternalAgentLogs {
   227  		if agentToTmpl.Agent != string(j.Spec.Agent) {
   228  			continue
   229  		}
   230  		if !agentToTmpl.Selector.Matches(labels.Set(j.ObjectMeta.Labels)) {
   231  			continue
   232  		}
   233  		var b bytes.Buffer
   234  		if err := agentToTmpl.URLTemplate.Execute(&b, &j); err != nil {
   235  			return nil, fmt.Errorf("cannot execute URL template for prowjob %q with agent %q: %v", j.ObjectMeta.Name, j.Spec.Agent, err)
   236  		}
   237  		resp, err := http.Get(b.String())
   238  		if err != nil {
   239  			return nil, err
   240  		}
   241  		defer resp.Body.Close()
   242  		return ioutil.ReadAll(resp.Body)
   243  
   244  	}
   245  	return nil, fmt.Errorf("cannot get logs for prowjob %q with agent %q: the agent is missing from the prow config file", j.ObjectMeta.Name, j.Spec.Agent)
   246  }
   247  
   248  func (ja *JobAgent) tryUpdate() {
   249  	if err := ja.update(); err != nil {
   250  		logrus.WithError(err).Warning("Error updating job list.")
   251  	}
   252  }
   253  
   254  type byStartTime []Job
   255  
   256  func (a byStartTime) Len() int           { return len(a) }
   257  func (a byStartTime) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }
   258  func (a byStartTime) Less(i, j int) bool { return a[i].st.After(a[j].st) }
   259  
   260  func (ja *JobAgent) update() error {
   261  	pjs, err := ja.kc.ListProwJobs(kube.EmptySelector)
   262  	if err != nil {
   263  		return err
   264  	}
   265  	var njs []Job
   266  	njsMap := make(map[string]Job)
   267  	njsIDMap := make(map[string]map[string]kube.ProwJob)
   268  	for _, j := range pjs {
   269  		ft := time.Time{}
   270  		if j.Status.CompletionTime != nil {
   271  			ft = j.Status.CompletionTime.Time
   272  		}
   273  		buildID := j.Status.BuildID
   274  		nj := Job{
   275  			Type:    string(j.Spec.Type),
   276  			Job:     j.Spec.Job,
   277  			Context: j.Spec.Context,
   278  			Agent:   j.Spec.Agent,
   279  			ProwJob: j.ObjectMeta.Name,
   280  			BuildID: buildID,
   281  
   282  			Started:     fmt.Sprintf("%d", j.Status.StartTime.Time.Unix()),
   283  			State:       string(j.Status.State),
   284  			Description: j.Status.Description,
   285  			PodName:     j.Status.PodName,
   286  			URL:         j.Status.URL,
   287  
   288  			st: j.Status.StartTime.Time,
   289  			ft: ft,
   290  		}
   291  		if !nj.ft.IsZero() {
   292  			nj.Finished = nj.ft.Format(time.RFC3339Nano)
   293  			duration := nj.ft.Sub(nj.st)
   294  			duration -= duration % time.Second // strip fractional seconds
   295  			nj.Duration = duration.String()
   296  		}
   297  		if j.Spec.Refs != nil {
   298  			nj.Repo = fmt.Sprintf("%s/%s", j.Spec.Refs.Org, j.Spec.Refs.Repo)
   299  			nj.Refs = j.Spec.Refs.String()
   300  			nj.BaseRef = j.Spec.Refs.BaseRef
   301  			nj.BaseSHA = j.Spec.Refs.BaseSHA
   302  			if len(j.Spec.Refs.Pulls) == 1 {
   303  				nj.Number = j.Spec.Refs.Pulls[0].Number
   304  				nj.Author = j.Spec.Refs.Pulls[0].Author
   305  				nj.PullSHA = j.Spec.Refs.Pulls[0].SHA
   306  			}
   307  		}
   308  		njs = append(njs, nj)
   309  		if nj.PodName != "" {
   310  			njsMap[nj.PodName] = nj
   311  		}
   312  		if _, ok := njsIDMap[j.Spec.Job]; !ok {
   313  			njsIDMap[j.Spec.Job] = make(map[string]kube.ProwJob)
   314  		}
   315  		njsIDMap[j.Spec.Job][buildID] = j
   316  	}
   317  	sort.Sort(byStartTime(njs))
   318  
   319  	ja.mut.Lock()
   320  	defer ja.mut.Unlock()
   321  	ja.prowJobs = pjs
   322  	ja.jobs = njs
   323  	ja.jobsMap = njsMap
   324  	ja.jobsIDMap = njsIDMap
   325  	return nil
   326  }