sigs.k8s.io/prow@v0.0.0-20240503223140-c5e374dc7eb1/cmd/pipeline/controller.go (about) 1 /* 2 Copyright 2019 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 "context" 21 "errors" 22 "fmt" 23 "sort" 24 "strconv" 25 "strings" 26 "time" 27 28 prowjobv1 "sigs.k8s.io/prow/pkg/apis/prowjobs/v1" 29 prowjobset "sigs.k8s.io/prow/pkg/client/clientset/versioned" 30 prowjobscheme "sigs.k8s.io/prow/pkg/client/clientset/versioned/scheme" 31 prowjobinfov1 "sigs.k8s.io/prow/pkg/client/informers/externalversions/prowjobs/v1" 32 prowjoblisters "sigs.k8s.io/prow/pkg/client/listers/prowjobs/v1" 33 "sigs.k8s.io/prow/pkg/config" 34 "sigs.k8s.io/prow/pkg/kube" 35 "sigs.k8s.io/prow/pkg/pjutil" 36 "sigs.k8s.io/prow/pkg/pod-utils/decorate" 37 "sigs.k8s.io/prow/pkg/pod-utils/downwardapi" 38 39 "github.com/sirupsen/logrus" 40 pipelinev1beta1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1" 41 untypedcorev1 "k8s.io/api/core/v1" 42 apierrors "k8s.io/apimachinery/pkg/api/errors" 43 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 44 "k8s.io/apimachinery/pkg/labels" 45 "k8s.io/apimachinery/pkg/util/runtime" 46 "k8s.io/apimachinery/pkg/util/sets" 47 "k8s.io/apimachinery/pkg/util/wait" 48 "k8s.io/client-go/kubernetes" 49 "k8s.io/client-go/kubernetes/scheme" 50 corev1 "k8s.io/client-go/kubernetes/typed/core/v1" 51 "k8s.io/client-go/tools/cache" 52 "k8s.io/client-go/tools/record" 53 "k8s.io/client-go/util/workqueue" 54 "knative.dev/pkg/apis" 55 ) 56 57 const ( 58 controllerName = "prow-pipeline-crd" 59 ) 60 61 type controller struct { 62 config config.Getter 63 pjc prowjobset.Interface 64 pipelines map[string]pipelineConfig 65 totURL string 66 67 pjLister prowjoblisters.ProwJobLister 68 pjInformer cache.SharedIndexInformer 69 70 workqueue workqueue.RateLimitingInterface 71 72 recorder record.EventRecorder 73 74 prowJobsDone bool 75 pipelinesDone map[string]bool 76 wait string 77 } 78 79 type controllerOptions struct { 80 kc kubernetes.Interface 81 pjc prowjobset.Interface 82 pji prowjobinfov1.ProwJobInformer 83 pipelineConfigs map[string]pipelineConfig 84 totURL string 85 prowConfig config.Getter 86 rl workqueue.RateLimitingInterface 87 } 88 89 // pjNamespace retruns the prow namespace from configuration 90 func (c *controller) pjNamespace() string { 91 return c.config().ProwJobNamespace 92 } 93 94 // hasSynced returns true when every prowjob and pipeline informer has synced. 95 func (c *controller) hasSynced() bool { 96 if !c.pjInformer.HasSynced() { 97 if c.wait != "prowjobs" { 98 c.wait = "prowjobs" 99 ns := c.pjNamespace() 100 if ns == "" { 101 ns = "controllers" 102 } 103 logrus.Infof("Waiting on prowjobs in %s namespace...", ns) 104 } 105 return false // still syncing prowjobs 106 } 107 if !c.prowJobsDone { 108 c.prowJobsDone = true 109 logrus.Info("Synced prow jobs") 110 } 111 if c.pipelinesDone == nil { 112 c.pipelinesDone = map[string]bool{} 113 } 114 for n, cfg := range c.pipelines { 115 if !cfg.informer.Informer().HasSynced() { 116 if c.wait != n { 117 c.wait = n 118 logrus.Infof("Waiting on %s pipelines...", n) 119 } 120 return false // still syncing pipelines in at least one cluster 121 } else if !c.pipelinesDone[n] { 122 c.pipelinesDone[n] = true 123 logrus.Infof("Synced %s pipelines", n) 124 } 125 } 126 return true // Everyone is synced 127 } 128 129 func newController(opts controllerOptions) (*controller, error) { 130 if err := prowjobscheme.AddToScheme(scheme.Scheme); err != nil { 131 return nil, err 132 } 133 134 // Log to events 135 eventBroadcaster := record.NewBroadcaster() 136 eventBroadcaster.StartLogging(logrus.Infof) 137 eventBroadcaster.StartRecordingToSink(&corev1.EventSinkImpl{Interface: opts.kc.CoreV1().Events("")}) 138 recorder := eventBroadcaster.NewRecorder(scheme.Scheme, untypedcorev1.EventSource{Component: controllerName}) 139 140 c := &controller{ 141 config: opts.prowConfig, 142 pjc: opts.pjc, 143 pipelines: opts.pipelineConfigs, 144 pjLister: opts.pji.Lister(), 145 pjInformer: opts.pji.Informer(), 146 workqueue: opts.rl, 147 recorder: recorder, 148 totURL: opts.totURL, 149 } 150 151 logrus.Info("Setting up event handlers") 152 153 // Reconcile whenever a prowjob changes 154 opts.pji.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ 155 AddFunc: func(obj interface{}) { 156 pj, ok := obj.(*prowjobv1.ProwJob) 157 if !ok { 158 logrus.Warnf("Ignoring bad prowjob add: %v", obj) 159 return 160 } 161 c.enqueueKey(pjutil.ClusterToCtx(pj.Spec.Cluster), pj) 162 }, 163 UpdateFunc: func(old, new interface{}) { 164 pj, ok := new.(*prowjobv1.ProwJob) 165 if !ok { 166 logrus.Warnf("Ignoring bad prowjob update: %v", new) 167 return 168 } 169 c.enqueueKey(pjutil.ClusterToCtx(pj.Spec.Cluster), pj) 170 }, 171 DeleteFunc: func(obj interface{}) { 172 pj, ok := obj.(*prowjobv1.ProwJob) 173 if !ok { 174 logrus.Warnf("Ignoring bad prowjob delete: %v", obj) 175 return 176 } 177 c.enqueueKey(pjutil.ClusterToCtx(pj.Spec.Cluster), pj) 178 }, 179 }) 180 181 for ctx, cfg := range opts.pipelineConfigs { 182 // Reconcile whenever a pipelinerun changes. 183 ctx := ctx // otherwise it will change 184 cfg.informer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ 185 AddFunc: func(obj interface{}) { 186 c.enqueueKey(ctx, obj) 187 }, 188 UpdateFunc: func(old, new interface{}) { 189 c.enqueueKey(ctx, new) 190 }, 191 DeleteFunc: func(obj interface{}) { 192 c.enqueueKey(ctx, obj) 193 }, 194 }) 195 } 196 197 return c, nil 198 } 199 200 // Run starts threads workers, returning after receiving a stop signal. 201 func (c *controller) Run(threads int, stop <-chan struct{}) error { 202 defer runtime.HandleCrash() 203 defer c.workqueue.ShutDown() 204 205 logrus.Info("Starting Pipeline controller") 206 logrus.Info("Waiting for informer caches to sync") 207 if ok := cache.WaitForCacheSync(stop, c.hasSynced); !ok { 208 return fmt.Errorf("failed to wait for caches to sync") 209 } 210 211 logrus.Info("Starting workers") 212 for i := 0; i < threads; i++ { 213 go wait.Until(c.runWorker, time.Second, stop) 214 } 215 216 logrus.Info("Started workers") 217 <-stop 218 logrus.Info("Shutting down workers") 219 return nil 220 } 221 222 // runWorker dequeues to reconcile, until the queue has closed. 223 func (c *controller) runWorker() { 224 for { 225 key, shutdown := c.workqueue.Get() 226 if shutdown { 227 return 228 } 229 func() { 230 defer c.workqueue.Done(key) 231 232 if err := reconcile(c, key.(string)); err != nil { 233 runtime.HandleError(fmt.Errorf("failed to reconcile %s: %w", key, err)) 234 return // Do not forget so we retry later. 235 } 236 c.workqueue.Forget(key) 237 }() 238 } 239 } 240 241 // toKey returns context/namespace/name 242 func toKey(ctx, namespace, name string) string { 243 return strings.Join([]string{ctx, namespace, name}, "/") 244 } 245 246 // fromKey converts toKey back into its parts 247 func fromKey(key string) (string, string, string, error) { 248 parts := strings.Split(key, "/") 249 if len(parts) != 3 { 250 return "", "", "", fmt.Errorf("bad key: %q", key) 251 } 252 return parts[0], parts[1], parts[2], nil 253 } 254 255 // enqueueKey schedules an item for reconciliation 256 func (c *controller) enqueueKey(ctx string, obj interface{}) { 257 switch o := obj.(type) { 258 case *prowjobv1.ProwJob: 259 ns := o.Spec.Namespace 260 if ns == "" { 261 ns = o.Namespace 262 } 263 c.workqueue.AddRateLimited(toKey(ctx, ns, o.Name)) 264 case *pipelinev1beta1.PipelineRun: 265 c.workqueue.AddRateLimited(toKey(ctx, o.Namespace, o.Name)) 266 default: 267 logrus.Warnf("cannot enqueue unknown type %T: %v", o, obj) 268 return 269 } 270 } 271 272 type reconciler interface { 273 getProwJob(name string) (*prowjobv1.ProwJob, error) 274 listProwJobs(namespace string) ([]*prowjobv1.ProwJob, error) 275 patchProwJob(pj *prowjobv1.ProwJob, newpj *prowjobv1.ProwJob) (*prowjobv1.ProwJob, error) 276 getPipelineRun(context, namespace, name string) (*pipelinev1beta1.PipelineRun, error) 277 cancelPipelineRun(context string, pr *pipelinev1beta1.PipelineRun) error 278 deletePipelineRun(context, namespace, name string) error 279 createPipelineRun(context, namespace string, b *pipelinev1beta1.PipelineRun) (*pipelinev1beta1.PipelineRun, error) 280 pipelineID(prowjobv1.ProwJob) (string, string, error) 281 now() metav1.Time 282 } 283 284 func (c *controller) getPipelineConfig(ctx string) (pipelineConfig, error) { 285 cfg, ok := c.pipelines[ctx] 286 if !ok { 287 defaultCtx := kube.DefaultClusterAlias 288 defaultCfg, ok := c.pipelines[defaultCtx] 289 if !ok { 290 return pipelineConfig{}, fmt.Errorf("no cluster configuration found for default context %q", defaultCtx) 291 } 292 return defaultCfg, nil 293 } 294 return cfg, nil 295 } 296 297 func (c *controller) getProwJob(name string) (*prowjobv1.ProwJob, error) { 298 return c.pjLister.ProwJobs(c.pjNamespace()).Get(name) 299 } 300 301 func (c *controller) patchProwJob(pj *prowjobv1.ProwJob, newpj *prowjobv1.ProwJob) (*prowjobv1.ProwJob, error) { 302 logrus.Debugf("patchProwJob(%s)", pj.Name) 303 return pjutil.PatchProwjob(context.TODO(), c.pjc.ProwV1().ProwJobs(c.pjNamespace()), logrus.NewEntry(logrus.StandardLogger()), *pj, *newpj) 304 } 305 306 func (c *controller) listProwJobs(namespace string) ([]*prowjobv1.ProwJob, error) { 307 pjnl := c.pjLister.ProwJobs(namespace) 308 return pjnl.List(labels.NewSelector()) 309 } 310 311 func (c *controller) getPipelineRun(context, namespace, name string) (*pipelinev1beta1.PipelineRun, error) { 312 p, err := c.getPipelineConfig(context) 313 if err != nil { 314 return nil, err 315 } 316 return p.informer.Lister().PipelineRuns(namespace).Get(name) 317 } 318 319 func (c *controller) deletePipelineRun(pContext, namespace, name string) error { 320 logrus.Debugf("deletePipeline(%s,%s,%s)", pContext, namespace, name) 321 p, err := c.getPipelineConfig(pContext) 322 if err != nil { 323 return err 324 } 325 return p.client.TektonV1beta1().PipelineRuns(namespace).Delete(context.TODO(), name, metav1.DeleteOptions{}) 326 } 327 328 func (c *controller) cancelPipelineRun(pContext string, pipeline *pipelinev1beta1.PipelineRun) error { 329 p, err := c.getPipelineConfig(pContext) 330 if err != nil { 331 return err 332 } 333 if pipeline.Spec.Status == pipelinev1beta1.PipelineRunSpecStatusCancelledRunFinally { 334 return nil 335 } 336 pipeline.Spec.Status = pipelinev1beta1.PipelineRunSpecStatusCancelledRunFinally 337 _, err = p.client.TektonV1beta1().PipelineRuns(pipeline.Namespace).Update(context.TODO(), pipeline, metav1.UpdateOptions{}) 338 return err 339 } 340 341 func (c *controller) createPipelineRun(pContext, namespace string, p *pipelinev1beta1.PipelineRun) (*pipelinev1beta1.PipelineRun, error) { 342 logrus.Debugf("createPipelineRun(%s,%s,%s)", pContext, namespace, p.Name) 343 pc, err := c.getPipelineConfig(pContext) 344 if err != nil { 345 return nil, err 346 } 347 p, err = pc.client.TektonV1beta1().PipelineRuns(namespace).Create(context.TODO(), p, metav1.CreateOptions{}) 348 if err != nil { 349 return p, err 350 } 351 // Block until the pipelinerun is in the lister, otherwise we may attempt to create it again 352 var errOut error 353 wait.Poll(time.Second, 10*time.Second, func() (bool, error) { 354 _, errOut = c.getPipelineRun(pContext, namespace, p.Name) 355 return errOut == nil, nil 356 }) 357 return p, errOut 358 } 359 360 func (c *controller) now() metav1.Time { 361 return metav1.Now() 362 } 363 364 func (c *controller) pipelineID(pj prowjobv1.ProwJob) (string, string, error) { 365 id, err := pjutil.GetBuildID(pj.Spec.Job, c.totURL) 366 if err != nil { 367 return "", "", err 368 } 369 pj.Status.BuildID = id 370 url, err := pjutil.JobURL(c.config().Plank, pj, logrus.NewEntry(logrus.StandardLogger())) 371 if err != nil { 372 logrus.WithFields(pjutil.ProwJobFields(&pj)).WithError(err).Error("Error calculating job status url") 373 } 374 return id, url, nil 375 } 376 377 func createProwJobIdentifier(pj *prowjobv1.ProwJob) string { 378 var additionalIdentifier string 379 if pj.Spec.Refs != nil { 380 var pulls []int 381 for _, pull := range pj.Spec.Refs.Pulls { 382 pulls = append(pulls, pull.Number) 383 } 384 sort.Ints(pulls) 385 additionalIdentifier = fmt.Sprintf("%s/%s@%s %v", pj.Spec.Refs.Org, pj.Spec.Refs.Repo, pj.Spec.Refs.BaseRef, pulls) 386 } 387 return fmt.Sprintf("%s %s", pj.Spec.Job, additionalIdentifier) 388 } 389 390 func getFilteredProwJobs(id string, pjsToFilter []*prowjobv1.ProwJob) []*prowjobv1.ProwJob { 391 pjs := []*prowjobv1.ProwJob{} 392 for _, p := range pjsToFilter { 393 if runningState(p.Status.State) && id == createProwJobIdentifier(p) { 394 pjs = append(pjs, p) 395 } 396 } 397 sort.Slice(pjs, func(i, j int) bool { 398 return pjs[i].Status.StartTime.Before(&pjs[j].Status.StartTime) 399 }) 400 return pjs 401 } 402 403 // abortDuplicatedProwJobs aborts all prowjobs with the same identifier as the given prowjob, 404 // it can also abort the given if it is running and has the same identifier as the detected 405 // newer prowjob. The function returns the updated prowjob if it was aborted, otherwise the 406 // original prowjob is returned. 407 func abortDuplicatedProwJobs(c reconciler, pj *prowjobv1.ProwJob) (*prowjobv1.ProwJob, error) { 408 if !runningState(pj.Status.State) || pj.Spec.Agent != prowjobv1.TektonAgent { 409 return pj, nil 410 } 411 id := createProwJobIdentifier(pj) 412 prowJobsToFilter, err := c.listProwJobs(pj.Namespace) 413 if err != nil { 414 return nil, err 415 } 416 pjs := getFilteredProwJobs(id, prowJobsToFilter) 417 // do not abort the newest prowjob 418 for i := 0; i < len(pjs)-1; i++ { 419 newpj := pjs[i].DeepCopy() 420 now := c.now() 421 newpj.Status.State = prowjobv1.AbortedState 422 newpj.Status.Description = descAborted 423 newpj.Status.CompletionTime = &now 424 newpj, err = c.patchProwJob(pjs[i], newpj) 425 if err != nil { 426 logrus.WithError(err).Error("failed to abort prowJob") 427 continue 428 } 429 if pj.Name == pjs[i].Name { 430 pj = newpj.DeepCopy() 431 } 432 } 433 return pj, nil 434 } 435 436 // reconcile ensures a tekton prowjob has a corresponding pipeline, updating the prowjob's status as the pipeline progresses. 437 func reconcile(c reconciler, key string) error { 438 logrus.Debugf("reconcile: %s\n", key) 439 440 ctx, namespace, name, err := fromKey(key) 441 if err != nil { 442 runtime.HandleError(err) 443 return nil 444 } 445 var wantPipelineRun bool 446 pj, err := c.getProwJob(name) 447 switch { 448 case apierrors.IsNotFound(err): 449 // Do not want pipeline 450 case err != nil: 451 return fmt.Errorf("get prowjob: %w", err) 452 case pj.Spec.Agent != prowjobv1.TektonAgent: 453 // Do not want a pipeline for this job 454 // We could look for a pipeline to remove, but it is more efficient to 455 // assume this field is immutable. 456 return nil 457 case pjutil.ClusterToCtx(pj.Spec.Cluster) != ctx: 458 // Build is in wrong cluster, we do not want this build 459 logrus.Warnf("%s found in context %s not %s", key, ctx, pjutil.ClusterToCtx(pj.Spec.Cluster)) 460 case pj.DeletionTimestamp == nil: 461 wantPipelineRun = true 462 } 463 if !apierrors.IsNotFound(err) { 464 pj, err = abortDuplicatedProwJobs(c, pj) 465 if err != nil { 466 return fmt.Errorf("abort duplicated prowjobs: %w", err) 467 } 468 } 469 newpj := pj.DeepCopy() 470 471 var havePipelineRun bool 472 p, err := c.getPipelineRun(ctx, namespace, name) 473 switch { 474 case apierrors.IsNotFound(err): 475 // Do not have a pipeline 476 case err != nil: 477 return fmt.Errorf("get pipelinerun %s: %w", key, err) 478 case p.DeletionTimestamp == nil: 479 havePipelineRun = true 480 } 481 482 var newPipelineRun bool 483 switch { 484 case !wantPipelineRun: 485 if !havePipelineRun { 486 if pj != nil && pj.Spec.Agent == prowjobv1.TektonAgent { 487 logrus.Infof("Observed deleted: %s", key) 488 } 489 return nil 490 } 491 492 // Skip deleting if the pipeline run is not created by prow 493 switch v, ok := p.Labels[kube.CreatedByProw]; { 494 case !ok, v != "true": 495 return nil 496 } 497 logrus.Infof("Delete PipelineRun/%s", key) 498 if err = c.deletePipelineRun(ctx, namespace, name); err != nil { 499 return fmt.Errorf("delete pipelinerun: %w", err) 500 } 501 return nil 502 case finalState(pj.Status.State): 503 logrus.Infof("Observed finished: %s", key) 504 return nil 505 case cancelledState(pj.Status.State): 506 if p != nil && p.Spec.Status != pipelinev1beta1.PipelineRunSpecStatusCancelledRunFinally { 507 if err = c.cancelPipelineRun(ctx, p); err != nil { 508 return fmt.Errorf("failed to cancel pipelineRun: %w", err) 509 } 510 } 511 return nil 512 case wantPipelineRun && !pj.Spec.HasPipelineRunSpec(): 513 return fmt.Errorf("nil PipelineRunSpec in ProwJob/%s", key) 514 case wantPipelineRun && !havePipelineRun && !cancelledState(pj.Status.State): 515 id, url, err := c.pipelineID(*newpj) 516 if err != nil { 517 return fmt.Errorf("failed to get pipeline id: %w", err) 518 } 519 newpj.Status.BuildID = id 520 newpj.Status.URL = url 521 newPipelineRun = true 522 pipelineRun, err := makePipelineRun(*newpj) 523 if err != nil { 524 return fmt.Errorf("error preparing resources: %w", err) 525 } 526 527 logrus.Infof("Create PipelineRun/%s", key) 528 p, err = c.createPipelineRun(ctx, namespace, pipelineRun) 529 if err != nil { 530 jerr := fmt.Errorf("start pipeline: %w", err) 531 // Set the prow job in error state to avoid an endless loop when 532 // the pipeline cannot be executed (e.g. referenced pipeline does not exist) 533 return updateProwJobState(c, key, newPipelineRun, pj, newpj, prowjobv1.ErrorState, jerr.Error()) 534 } 535 } 536 537 if p == nil { 538 return fmt.Errorf("no pipelinerun found or created for %q, wantPipelineRun was %t", key, wantPipelineRun) 539 } 540 wantState, wantMsg := prowJobStatus(p.Status) 541 return updateProwJobState(c, key, newPipelineRun, pj, newpj, wantState, wantMsg) 542 } 543 544 func updateProwJobState(c reconciler, key string, newPipelineRun bool, pj *prowjobv1.ProwJob, newpj *prowjobv1.ProwJob, state prowjobv1.ProwJobState, msg string) error { 545 haveState := newpj.Status.State 546 haveMsg := newpj.Status.Description 547 if newPipelineRun || haveState != state || haveMsg != msg { 548 if haveState != state && state == prowjobv1.PendingState { 549 now := c.now() 550 newpj.Status.PendingTime = &now 551 } 552 if newpj.Status.StartTime.IsZero() { 553 newpj.Status.StartTime = c.now() 554 } 555 if newpj.Status.CompletionTime.IsZero() && !runningState(state) { 556 now := c.now() 557 newpj.Status.CompletionTime = &now 558 } 559 newpj.Status.State = state 560 newpj.Status.Description = msg 561 logrus.Infof("Update ProwJob/%s: %s -> %s: %s", key, haveState, state, msg) 562 563 if _, err := c.patchProwJob(pj, newpj); err != nil { 564 return fmt.Errorf("update prow status: %w", err) 565 } 566 } 567 return nil 568 } 569 570 // finalState returns true if the prowjob has already finished 571 func finalState(status prowjobv1.ProwJobState) bool { 572 switch status { 573 case prowjobv1.SuccessState, prowjobv1.FailureState, prowjobv1.ErrorState: 574 return true 575 } 576 return false 577 } 578 579 // runningState returns true if the prowjob has running or pending state 580 func runningState(status prowjobv1.ProwJobState) bool { 581 switch status { 582 case prowjobv1.PendingState, prowjobv1.TriggeredState: 583 return true 584 } 585 return false 586 } 587 588 // cancelledState returns true if the prowjob aborted or errored 589 func cancelledState(status prowjobv1.ProwJobState) bool { 590 return status == prowjobv1.AbortedState 591 } 592 593 // description computes the ProwJobStatus description for this condition or falling back to a default if none is provided. 594 func description(cond apis.Condition, fallback string) string { 595 switch { 596 case cond.Message != "": 597 return cond.Message 598 case cond.Reason != "": 599 return cond.Reason 600 } 601 return fallback 602 } 603 604 const ( 605 descAborted = "aborted" 606 descScheduling = "scheduling" 607 descInitializing = "initializing" 608 descRunning = "running" 609 descSucceeded = "succeeded" 610 descFailed = "failed" 611 descUnknown = "unknown status" 612 descMissingCondition = "missing end condition" 613 ) 614 615 // prowJobStatus returns the desired state and description based on the pipeline status 616 func prowJobStatus(ps pipelinev1beta1.PipelineRunStatus) (prowjobv1.ProwJobState, string) { 617 started := ps.StartTime 618 finished := ps.CompletionTime 619 pcond := ps.GetCondition(apis.ConditionSucceeded) 620 if pcond == nil { 621 if !finished.IsZero() { 622 return prowjobv1.ErrorState, descMissingCondition 623 } 624 return prowjobv1.PendingState, descScheduling 625 } 626 cond := *pcond 627 switch { 628 case cond.Status == untypedcorev1.ConditionTrue: 629 return prowjobv1.SuccessState, description(cond, descSucceeded) 630 case cond.Status == untypedcorev1.ConditionFalse: 631 return prowjobv1.FailureState, description(cond, descFailed) 632 case started.IsZero(): 633 return prowjobv1.PendingState, description(cond, descInitializing) 634 case cond.Status == untypedcorev1.ConditionUnknown, finished.IsZero(): 635 return prowjobv1.PendingState, description(cond, descRunning) 636 } 637 638 logrus.Warnf("Unknown condition %#v", cond) 639 return prowjobv1.ErrorState, description(cond, descUnknown) // shouldn't happen 640 } 641 642 // pipelineMeta builds the pipeline metadata from prow job definition 643 func pipelineMeta(name string, pj prowjobv1.ProwJob) metav1.ObjectMeta { 644 labels, annotations := decorate.LabelsAndAnnotationsForJob(pj) 645 return metav1.ObjectMeta{ 646 Annotations: annotations, 647 Name: name, 648 Namespace: pj.Spec.Namespace, 649 Labels: labels, 650 } 651 } 652 653 // makePipelineGitTask creates a pipeline git resource from prow job 654 func makePipelineGitTask(name string, refs prowjobv1.Refs, pj prowjobv1.ProwJob) pipelinev1beta1.PipelineTask { 655 // Pick source URL 656 var sourceURL string 657 switch { 658 case refs.CloneURI != "": 659 sourceURL = refs.CloneURI 660 case refs.RepoLink != "": 661 sourceURL = fmt.Sprintf("%s.git", refs.RepoLink) 662 default: 663 sourceURL = fmt.Sprintf("https://github.com/%s/%s.git", refs.Org, refs.Repo) 664 } 665 666 // Pick revision 667 var revision string 668 switch { 669 case len(refs.Pulls) > 0: 670 if refs.Pulls[0].SHA != "" { 671 revision = refs.Pulls[0].SHA 672 } else { 673 revision = fmt.Sprintf("pull/%d/head", refs.Pulls[0].Number) 674 } 675 case refs.BaseSHA != "": 676 revision = refs.BaseSHA 677 default: 678 revision = refs.BaseRef 679 } 680 681 return pipelinev1beta1.PipelineTask{ 682 TaskRef: &pipelinev1beta1.TaskRef{ 683 Name: "git-clone", 684 }, 685 Params: []pipelinev1beta1.Param{ 686 { 687 Name: "url", 688 Value: pipelinev1beta1.ParamValue{StringVal: sourceURL}, 689 }, 690 { 691 Name: "revision", 692 Value: pipelinev1beta1.ParamValue{StringVal: revision}, 693 }, 694 }, 695 } 696 } 697 698 // makePipelineRun creates a pipeline run from prow job 699 func makePipelineRun(pj prowjobv1.ProwJob) (*pipelinev1beta1.PipelineRun, error) { 700 // First validate. 701 spec, err := pj.Spec.GetPipelineRunSpec() 702 if err != nil { 703 return nil, err 704 } 705 if spec == nil { 706 return nil, errors.New("no PipelineSpec defined") 707 } 708 buildID := pj.Status.BuildID 709 if buildID == "" { 710 return nil, errors.New("empty BuildID in status") 711 } 712 if err := config.ValidatePipelineRunSpec(pj.Spec.Type, pj.Spec.ExtraRefs, spec); err != nil { 713 return nil, fmt.Errorf("invalid pipeline_run_spec: %w", err) 714 } 715 716 p := pipelinev1beta1.PipelineRun{ 717 ObjectMeta: pipelineMeta(pj.Name, pj), 718 Spec: *spec.DeepCopy(), 719 } 720 721 // Add parameters instead of env vars. 722 env, err := downwardapi.EnvForSpec(downwardapi.NewJobSpec(pj.Spec, buildID, pj.Name)) 723 if err != nil { 724 return nil, err 725 } 726 for _, key := range sets.List(sets.KeySet[string](env)) { 727 val := env[key] 728 // TODO: make this handle existing values/substitutions. 729 p.Spec.Params = append(p.Spec.Params, pipelinev1beta1.Param{ 730 Name: key, 731 Value: pipelinev1beta1.ParamValue{ 732 Type: pipelinev1beta1.ParamTypeString, 733 StringVal: val, 734 }, 735 }) 736 } 737 738 if p.Spec.PipelineSpec != nil { 739 for i, task := range p.Spec.PipelineSpec.Tasks { 740 taskName := task.TaskRef.Name 741 var refs prowjobv1.Refs 742 var suffix string 743 if taskName == config.ProwImplicitGitResource { 744 if pj.Spec.Refs == nil { 745 return nil, fmt.Errorf("%q requested on a ProwJob without an implicit git ref", config.ProwImplicitGitResource) 746 } 747 refs = *pj.Spec.Refs 748 suffix = "-implicit-ref" 749 } else if match := config.ReProwExtraRef.FindStringSubmatch(taskName); len(match) == 2 { 750 index, _ := strconv.Atoi(match[1]) // We can't error because the regexp only matches digits. 751 refs = pj.Spec.ExtraRefs[index] // ValidatePipelineRunSpec made sure this is safe. 752 suffix = fmt.Sprintf("-extra-ref-%d", index) 753 } else { 754 continue 755 } 756 757 gitTask := makePipelineGitTask(pj.Name+suffix, refs, pj) 758 p.Spec.PipelineSpec.Tasks[i] = gitTask 759 } 760 } 761 762 return &p, nil 763 }