github.com/abayer/test-infra@v0.0.5/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  	"fmt"
    23  	"io/ioutil"
    24  	"net/http"
    25  	"regexp"
    26  	"sort"
    27  	"sync"
    28  	"time"
    29  
    30  	"github.com/sirupsen/logrus"
    31  	"k8s.io/apimachinery/pkg/labels"
    32  	"k8s.io/test-infra/prow/config"
    33  	"k8s.io/test-infra/prow/kube"
    34  )
    35  
    36  const (
    37  	period = 30 * time.Second
    38  )
    39  
    40  // Job holds information about a job prow is running/has run.
    41  // TODO(#5216): Remove this, and all associated machinery.
    42  type Job struct {
    43  	Type        string            `json:"type"`
    44  	Repo        string            `json:"repo"`
    45  	Refs        string            `json:"refs"`
    46  	BaseRef     string            `json:"base_ref"`
    47  	BaseSHA     string            `json:"base_sha"`
    48  	PullSHA     string            `json:"pull_sha"`
    49  	Number      int               `json:"number"`
    50  	Author      string            `json:"author"`
    51  	Job         string            `json:"job"`
    52  	BuildID     string            `json:"build_id"`
    53  	Context     string            `json:"context"`
    54  	Started     string            `json:"started"`
    55  	Finished    string            `json:"finished"`
    56  	Duration    string            `json:"duration"`
    57  	State       string            `json:"state"`
    58  	Description string            `json:"description"`
    59  	URL         string            `json:"url"`
    60  	PodName     string            `json:"pod_name"`
    61  	Agent       kube.ProwJobAgent `json:"agent"`
    62  	ProwJob     string            `json:"prow_job"`
    63  
    64  	st time.Time
    65  	ft time.Time
    66  }
    67  
    68  type serviceClusterClient interface {
    69  	GetLog(pod string) ([]byte, error)
    70  	ListPods(selector string) ([]kube.Pod, error)
    71  	ListProwJobs(selector string) ([]kube.ProwJob, error)
    72  }
    73  
    74  type PodLogClient interface {
    75  	GetContainerLog(pod, container string) ([]byte, error)
    76  }
    77  
    78  type ConfigAgent interface {
    79  	Config() *config.Config
    80  }
    81  
    82  // NewJobAgent is a JobAgent constructor
    83  func NewJobAgent(kc serviceClusterClient, plClients map[string]PodLogClient, ca ConfigAgent) *JobAgent {
    84  	return &JobAgent{
    85  		kc:   kc,
    86  		pkcs: plClients,
    87  		c:    ca,
    88  	}
    89  }
    90  
    91  // JobAgent creates lists of jobs, updates their status and returns their run logs.
    92  type JobAgent struct {
    93  	kc        serviceClusterClient
    94  	pkcs      map[string]PodLogClient
    95  	c         ConfigAgent
    96  	prowJobs  []kube.ProwJob
    97  	jobs      []Job
    98  	jobsMap   map[string]Job                     // pod name -> Job
    99  	jobsIDMap map[string]map[string]kube.ProwJob // job name -> id -> ProwJob
   100  	mut       sync.Mutex
   101  }
   102  
   103  // Start will start the job and periodically update it.
   104  func (ja *JobAgent) Start() {
   105  	ja.tryUpdate()
   106  	go func() {
   107  		t := time.Tick(period)
   108  		for range t {
   109  			ja.tryUpdate()
   110  		}
   111  	}()
   112  }
   113  
   114  // Jobs returns a thread-safe snapshot of the current job state.
   115  func (ja *JobAgent) Jobs() []Job {
   116  	ja.mut.Lock()
   117  	defer ja.mut.Unlock()
   118  	res := make([]Job, len(ja.jobs))
   119  	copy(res, ja.jobs)
   120  	return res
   121  }
   122  
   123  // ProwJobs returns a thread-safe snapshot of the current prow jobs.
   124  func (ja *JobAgent) ProwJobs() []kube.ProwJob {
   125  	ja.mut.Lock()
   126  	defer ja.mut.Unlock()
   127  	res := make([]kube.ProwJob, len(ja.prowJobs))
   128  	copy(res, ja.prowJobs)
   129  	return res
   130  }
   131  
   132  var jobNameRE = regexp.MustCompile(`^([\w-]+)-(\d+)$`)
   133  
   134  // GetJobLog returns the job logs, works for both kubernetes and jenkins agent types.
   135  func (ja *JobAgent) GetJobLog(job, id string) ([]byte, error) {
   136  	var j kube.ProwJob
   137  	ja.mut.Lock()
   138  	idMap, ok := ja.jobsIDMap[job]
   139  	if ok {
   140  		j, ok = idMap[id]
   141  	}
   142  	ja.mut.Unlock()
   143  	if !ok {
   144  		return nil, fmt.Errorf("no such job found: %s (id: %s)", job, id)
   145  	}
   146  	if j.Spec.Agent == kube.KubernetesAgent {
   147  		client, ok := ja.pkcs[j.ClusterAlias()]
   148  		if !ok {
   149  			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())
   150  		}
   151  		return client.GetContainerLog(j.Status.PodName, kube.TestContainerName)
   152  	}
   153  	for _, agentToTmpl := range ja.c.Config().Deck.ExternalAgentLogs {
   154  		if agentToTmpl.Agent != string(j.Spec.Agent) {
   155  			continue
   156  		}
   157  		if !agentToTmpl.Selector.Matches(labels.Set(j.ObjectMeta.Labels)) {
   158  			continue
   159  		}
   160  		var b bytes.Buffer
   161  		if err := agentToTmpl.URLTemplate.Execute(&b, &j); err != nil {
   162  			return nil, fmt.Errorf("cannot execute URL template for prowjob %q with agent %q: %v", j.ObjectMeta.Name, j.Spec.Agent, err)
   163  		}
   164  		resp, err := http.Get(b.String())
   165  		if err != nil {
   166  			return nil, err
   167  		}
   168  		defer resp.Body.Close()
   169  		return ioutil.ReadAll(resp.Body)
   170  	}
   171  	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)
   172  }
   173  
   174  func (ja *JobAgent) tryUpdate() {
   175  	if err := ja.update(); err != nil {
   176  		logrus.WithError(err).Warning("Error updating job list.")
   177  	}
   178  }
   179  
   180  type byStartTime []Job
   181  
   182  func (a byStartTime) Len() int           { return len(a) }
   183  func (a byStartTime) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }
   184  func (a byStartTime) Less(i, j int) bool { return a[i].st.After(a[j].st) }
   185  
   186  func (ja *JobAgent) update() error {
   187  	pjs, err := ja.kc.ListProwJobs(kube.EmptySelector)
   188  	if err != nil {
   189  		return err
   190  	}
   191  	var njs []Job
   192  	njsMap := make(map[string]Job)
   193  	njsIDMap := make(map[string]map[string]kube.ProwJob)
   194  	for _, j := range pjs {
   195  		ft := time.Time{}
   196  		if j.Status.CompletionTime != nil {
   197  			ft = j.Status.CompletionTime.Time
   198  		}
   199  		buildID := j.Status.BuildID
   200  		nj := Job{
   201  			Type:    string(j.Spec.Type),
   202  			Job:     j.Spec.Job,
   203  			Context: j.Spec.Context,
   204  			Agent:   j.Spec.Agent,
   205  			ProwJob: j.ObjectMeta.Name,
   206  			BuildID: buildID,
   207  
   208  			Started:     fmt.Sprintf("%d", j.Status.StartTime.Time.Unix()),
   209  			State:       string(j.Status.State),
   210  			Description: j.Status.Description,
   211  			PodName:     j.Status.PodName,
   212  			URL:         j.Status.URL,
   213  
   214  			st: j.Status.StartTime.Time,
   215  			ft: ft,
   216  		}
   217  		if !nj.ft.IsZero() {
   218  			nj.Finished = nj.ft.Format(time.RFC3339Nano)
   219  			duration := nj.ft.Sub(nj.st)
   220  			duration -= duration % time.Second // strip fractional seconds
   221  			nj.Duration = duration.String()
   222  		}
   223  		if j.Spec.Refs != nil {
   224  			nj.Repo = fmt.Sprintf("%s/%s", j.Spec.Refs.Org, j.Spec.Refs.Repo)
   225  			nj.Refs = j.Spec.Refs.String()
   226  			nj.BaseRef = j.Spec.Refs.BaseRef
   227  			nj.BaseSHA = j.Spec.Refs.BaseSHA
   228  			if len(j.Spec.Refs.Pulls) == 1 {
   229  				nj.Number = j.Spec.Refs.Pulls[0].Number
   230  				nj.Author = j.Spec.Refs.Pulls[0].Author
   231  				nj.PullSHA = j.Spec.Refs.Pulls[0].SHA
   232  			}
   233  		}
   234  		njs = append(njs, nj)
   235  		if nj.PodName != "" {
   236  			njsMap[nj.PodName] = nj
   237  		}
   238  		if _, ok := njsIDMap[j.Spec.Job]; !ok {
   239  			njsIDMap[j.Spec.Job] = make(map[string]kube.ProwJob)
   240  		}
   241  		njsIDMap[j.Spec.Job][buildID] = j
   242  	}
   243  	sort.Sort(byStartTime(njs))
   244  
   245  	ja.mut.Lock()
   246  	defer ja.mut.Unlock()
   247  	ja.prowJobs = pjs
   248  	ja.jobs = njs
   249  	ja.jobsMap = njsMap
   250  	ja.jobsIDMap = njsIDMap
   251  	return nil
   252  }