github.com/zppinho/prow@v0.0.0-20240510014325-1738badeb017/pkg/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 "context" 23 "errors" 24 "fmt" 25 stdio "io" 26 "net/http" 27 "sort" 28 "sync" 29 "time" 30 31 "github.com/sirupsen/logrus" 32 "k8s.io/apimachinery/pkg/labels" 33 "k8s.io/apimachinery/pkg/util/sets" 34 ctrlruntimeclient "sigs.k8s.io/controller-runtime/pkg/client" 35 36 prowapi "sigs.k8s.io/prow/pkg/apis/prowjobs/v1" 37 "sigs.k8s.io/prow/pkg/config" 38 ) 39 40 const ( 41 period = 30 * time.Second 42 ) 43 44 var ( 45 errProwjobNotFound = errors.New("prowjob not found") 46 ) 47 48 func IsErrProwJobNotFound(err error) bool { 49 return err == errProwjobNotFound 50 } 51 52 // Job holds information about a job prow is running/has run. 53 // TODO(#5216): Remove this, and all associated machinery. 54 type Job struct { 55 Type string `json:"type"` 56 Refs prowapi.Refs `json:"refs"` 57 RefsKey string `json:"refs_key"` 58 Job string `json:"job"` 59 BuildID string `json:"build_id"` 60 Context string `json:"context"` 61 Started string `json:"started"` 62 Finished string `json:"finished"` 63 Duration string `json:"duration"` 64 State string `json:"state"` 65 Description string `json:"description"` 66 URL string `json:"url"` 67 PodName string `json:"pod_name"` 68 Agent prowapi.ProwJobAgent `json:"agent"` 69 ProwJob string `json:"prow_job"` 70 71 st time.Time 72 ft time.Time 73 } 74 75 type serviceClusterClient interface { 76 ListProwJobs(selector string) ([]prowapi.ProwJob, error) 77 } 78 79 // PodLogClient is an interface for interacting with the pod logs. 80 type PodLogClient interface { 81 GetLogs(name, container string) ([]byte, error) 82 } 83 84 // PJListingClient is an interface to list ProwJobs 85 type PJListingClient interface { 86 List(context.Context, *prowapi.ProwJobList, ...ctrlruntimeclient.ListOption) error 87 } 88 89 // NewJobAgent is a JobAgent constructor. 90 func NewJobAgent(ctx context.Context, pjLister PJListingClient, hiddenOnly, showHidden bool, tenantIDs []string, plClients map[string]PodLogClient, cfg config.Getter) *JobAgent { 91 return &JobAgent{ 92 kc: &filteringProwJobLister{ 93 ctx: ctx, 94 client: pjLister, 95 hiddenRepos: func() sets.Set[string] { return sets.New[string](cfg().Deck.HiddenRepos...) }, 96 hiddenOnly: hiddenOnly, 97 showHidden: showHidden, 98 tenantIDs: tenantIDs, 99 cfg: cfg, 100 }, 101 pkcs: plClients, 102 config: cfg, 103 } 104 } 105 106 type filteringProwJobLister struct { 107 ctx context.Context 108 client PJListingClient 109 cfg config.Getter 110 hiddenRepos func() sets.Set[string] 111 hiddenOnly bool 112 showHidden bool 113 tenantIDs []string 114 } 115 116 func (c *filteringProwJobLister) TenantIDMatch(pj prowapi.ProwJob) bool { 117 if pj.Spec.ProwJobDefault == nil { 118 return false 119 } 120 for _, id := range c.tenantIDs { 121 if id == pj.Spec.ProwJobDefault.TenantID { 122 return true 123 } 124 } 125 return false 126 } 127 128 func tenantIDMissingOrDefault(pj prowapi.ProwJob) bool { 129 return pj.Spec.ProwJobDefault == nil || pj.Spec.ProwJobDefault.TenantID == "" || pj.Spec.ProwJobDefault.TenantID == config.DefaultTenantID 130 } 131 132 func (c *filteringProwJobLister) ListProwJobs(selector string) ([]prowapi.ProwJob, error) { 133 prowJobList := &prowapi.ProwJobList{} 134 parsedSelector, err := labels.Parse(selector) 135 if err != nil { 136 return nil, fmt.Errorf("failed to parse selector: %w", err) 137 } 138 listOpts := &ctrlruntimeclient.ListOptions{LabelSelector: parsedSelector, Namespace: c.cfg().ProwJobNamespace} 139 if err := c.client.List(c.ctx, prowJobList, listOpts); err != nil { 140 return nil, err 141 } 142 143 var filtered []prowapi.ProwJob 144 for _, item := range prowJobList.Items { 145 if len(c.tenantIDs) != 0 { 146 if c.TenantIDMatch(item) { 147 // Deck has tenantID and it matches Prowjob 148 filtered = append(filtered, item) 149 } 150 } else if len(c.tenantIDs) == 0 { 151 // Deck has no tenantID 152 shouldHide := item.Spec.Hidden || c.pjHasHiddenRefs(item) 153 if shouldHide && (c.showHidden || c.hiddenOnly) { 154 // If Hidden and we are showing Hidden we add it 155 filtered = append(filtered, item) 156 } else if !shouldHide && !c.hiddenOnly && tenantIDMissingOrDefault(item) { 157 // If not Hidden then show if not hiddenOnly AND if no tenantID 158 filtered = append(filtered, item) 159 } 160 } 161 } 162 163 return filtered, nil 164 } 165 166 func (c *filteringProwJobLister) pjHasHiddenRefs(pj prowapi.ProwJob) bool { 167 allRefs := pj.Spec.ExtraRefs 168 if pj.Spec.Refs != nil { 169 allRefs = append(allRefs, *pj.Spec.Refs) 170 } 171 for _, refs := range allRefs { 172 if c.hiddenRepos().HasAny(fmt.Sprintf("%s/%s", refs.Org, refs.Repo), refs.Org) { 173 return true 174 } 175 } 176 177 return false 178 } 179 180 // JobAgent creates lists of jobs, updates their status and returns their run logs. 181 type JobAgent struct { 182 kc serviceClusterClient 183 pkcs map[string]PodLogClient 184 config config.Getter 185 prowJobs []prowapi.ProwJob 186 jobs []Job 187 jobsMap map[string]Job // pod name -> Job 188 jobsIDMap map[string]map[string]prowapi.ProwJob // job name -> id -> ProwJob 189 mut sync.Mutex 190 } 191 192 // Start will start the job and periodically update it. 193 func (ja *JobAgent) Start() { 194 ja.tryUpdate() 195 go func() { 196 t := time.Tick(period) 197 for range t { 198 ja.tryUpdate() 199 } 200 }() 201 } 202 203 // Jobs returns a thread-safe snapshot of the current job state. 204 func (ja *JobAgent) Jobs() []Job { 205 ja.mut.Lock() 206 defer ja.mut.Unlock() 207 res := make([]Job, len(ja.jobs)) 208 copy(res, ja.jobs) 209 return res 210 } 211 212 // ProwJobs returns a thread-safe snapshot of the current prow jobs. 213 func (ja *JobAgent) ProwJobs() []prowapi.ProwJob { 214 ja.mut.Lock() 215 defer ja.mut.Unlock() 216 res := make([]prowapi.ProwJob, len(ja.prowJobs)) 217 copy(res, ja.prowJobs) 218 return res 219 } 220 221 // GetProwJob finds the corresponding Prowjob resource from the provided job name and build ID 222 func (ja *JobAgent) GetProwJob(job, id string) (prowapi.ProwJob, error) { 223 if ja == nil { 224 return prowapi.ProwJob{}, fmt.Errorf("Prow job agent doesn't exist (are you running locally?)") 225 } 226 var j prowapi.ProwJob 227 ja.mut.Lock() 228 idMap, ok := ja.jobsIDMap[job] 229 if ok { 230 j, ok = idMap[id] 231 } 232 ja.mut.Unlock() 233 if !ok { 234 return prowapi.ProwJob{}, errProwjobNotFound 235 } 236 return j, nil 237 } 238 239 // GetJobLog returns the job logs, works for both kubernetes and jenkins agent types. 240 func (ja *JobAgent) GetJobLog(job, id string, container string) ([]byte, error) { 241 j, err := ja.GetProwJob(job, id) 242 if err != nil { 243 return nil, fmt.Errorf("error getting prowjob: %w", err) 244 } 245 if j.Spec.Agent == prowapi.KubernetesAgent { 246 client, ok := ja.pkcs[j.ClusterAlias()] 247 if !ok { 248 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()) 249 } 250 return client.GetLogs(j.Status.PodName, container) 251 } 252 for _, agentToTmpl := range ja.config().Deck.ExternalAgentLogs { 253 if agentToTmpl.Agent != string(j.Spec.Agent) { 254 continue 255 } 256 if !agentToTmpl.Selector.Matches(labels.Set(j.ObjectMeta.Labels)) { 257 continue 258 } 259 var b bytes.Buffer 260 if err := agentToTmpl.URLTemplate.Execute(&b, &j); err != nil { 261 return nil, fmt.Errorf("cannot execute URL template for prowjob %q with agent %q: %w", j.ObjectMeta.Name, j.Spec.Agent, err) 262 } 263 resp, err := http.Get(b.String()) 264 if err != nil { 265 return nil, err 266 } 267 defer resp.Body.Close() 268 return stdio.ReadAll(resp.Body) 269 270 } 271 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) 272 } 273 274 func (ja *JobAgent) tryUpdate() { 275 if err := ja.update(); err != nil { 276 logrus.WithError(err).Warning("Error updating job list.") 277 } 278 } 279 280 type byPJStartTime []prowapi.ProwJob 281 282 func (a byPJStartTime) Len() int { return len(a) } 283 func (a byPJStartTime) Swap(i, j int) { a[i], a[j] = a[j], a[i] } 284 func (a byPJStartTime) Less(i, j int) bool { 285 if a[i].Status.StartTime.Time != a[j].Status.StartTime.Time { 286 return a[i].Status.StartTime.Time.After(a[j].Status.StartTime.Time) 287 } 288 // Start time only has second granularity and we often start many jobs in the 289 // same second. Use the name as tie breaker. 290 return a[i].Spec.Job < a[j].Spec.Job 291 } 292 293 func (ja *JobAgent) update() error { 294 pjs, err := ja.kc.ListProwJobs(labels.Everything().String()) 295 if err != nil { 296 return err 297 } 298 var njs []Job 299 njsMap := make(map[string]Job) 300 njsIDMap := make(map[string]map[string]prowapi.ProwJob) 301 302 sort.Sort(byPJStartTime(pjs)) 303 304 for _, j := range pjs { 305 ft := time.Time{} 306 if j.Status.CompletionTime != nil { 307 ft = j.Status.CompletionTime.Time 308 } 309 buildID := j.Status.BuildID 310 nj := Job{ 311 Type: string(j.Spec.Type), 312 Job: j.Spec.Job, 313 Context: j.Spec.Context, 314 Agent: j.Spec.Agent, 315 ProwJob: j.ObjectMeta.Name, 316 BuildID: buildID, 317 318 Started: fmt.Sprintf("%d", j.Status.StartTime.Time.Unix()), 319 State: string(j.Status.State), 320 Description: j.Status.Description, 321 PodName: j.Status.PodName, 322 URL: j.Status.URL, 323 324 st: j.Status.StartTime.Time, 325 ft: ft, 326 } 327 if !nj.ft.IsZero() { 328 nj.Finished = nj.ft.Format(time.RFC3339Nano) 329 duration := nj.ft.Sub(nj.st) 330 duration -= duration % time.Second // strip fractional seconds 331 nj.Duration = duration.String() 332 } 333 if j.Spec.Refs != nil { 334 nj.Refs = *j.Spec.Refs 335 nj.RefsKey = j.Spec.Refs.String() 336 } 337 njs = append(njs, nj) 338 if nj.PodName != "" { 339 njsMap[nj.PodName] = nj 340 } 341 if _, ok := njsIDMap[j.Spec.Job]; !ok { 342 njsIDMap[j.Spec.Job] = make(map[string]prowapi.ProwJob) 343 } 344 njsIDMap[j.Spec.Job][buildID] = j 345 } 346 347 ja.mut.Lock() 348 defer ja.mut.Unlock() 349 ja.prowJobs = pjs 350 ja.jobs = njs 351 ja.jobsMap = njsMap 352 ja.jobsIDMap = njsIDMap 353 return nil 354 }