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 }