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  }