github.com/abayer/test-infra@v0.0.5/prow/cmd/build/controller.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 main
    18  
    19  import (
    20  	"errors"
    21  	"flag"
    22  	"fmt"
    23  	"os"
    24  	"os/signal"
    25  	"reflect"
    26  	"syscall"
    27  	"time"
    28  
    29  	prowjobv1 "k8s.io/test-infra/prow/apis/prowjobs/v1"
    30  	prowjobset "k8s.io/test-infra/prow/client/clientset/versioned"
    31  	prowjobscheme "k8s.io/test-infra/prow/client/clientset/versioned/scheme"
    32  	prowjobinfo "k8s.io/test-infra/prow/client/informers/externalversions"
    33  	prowjobinfov1 "k8s.io/test-infra/prow/client/informers/externalversions/prowjobs/v1"
    34  	prowjoblisters "k8s.io/test-infra/prow/client/listers/prowjobs/v1"
    35  	"k8s.io/test-infra/prow/logrusutil"
    36  
    37  	buildv1alpha1 "github.com/knative/build/pkg/apis/build/v1alpha1"
    38  	buildset "github.com/knative/build/pkg/client/clientset/versioned"
    39  	buildinfo "github.com/knative/build/pkg/client/informers/externalversions"
    40  	buildinfov1alpha1 "github.com/knative/build/pkg/client/informers/externalversions/build/v1alpha1"
    41  	buildlisters "github.com/knative/build/pkg/client/listers/build/v1alpha1"
    42  
    43  	untypedcorev1 "k8s.io/api/core/v1"
    44  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    45  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    46  	"k8s.io/apimachinery/pkg/runtime/schema"
    47  	"k8s.io/apimachinery/pkg/util/runtime"
    48  	"k8s.io/apimachinery/pkg/util/wait"
    49  	"k8s.io/client-go/kubernetes"
    50  	"k8s.io/client-go/kubernetes/scheme"
    51  	corev1 "k8s.io/client-go/kubernetes/typed/core/v1"
    52  	"k8s.io/client-go/tools/cache"
    53  	"k8s.io/client-go/tools/clientcmd"
    54  	"k8s.io/client-go/tools/record"
    55  	"k8s.io/client-go/util/workqueue"
    56  
    57  	"github.com/sirupsen/logrus"
    58  )
    59  
    60  var (
    61  	masterURL      string
    62  	kubeconfig     string
    63  	controllerName = "prow-build-crd"
    64  )
    65  
    66  func init() {
    67  	flag.StringVar(&kubeconfig, "kubeconfig", "", "Path to kubeconfig. Only required if out of cluster")
    68  	flag.StringVar(&masterURL, "master", "", "The address of the kubernetes API server. Overrides any value in kubeconfig. Only required if out of cluster")
    69  }
    70  
    71  func stopper() chan struct{} {
    72  	stop := make(chan struct{})
    73  	c := make(chan os.Signal, 2)
    74  	signal.Notify(c, os.Interrupt, syscall.SIGTERM)
    75  	go func() {
    76  		<-c
    77  		close(stop)
    78  		<-c
    79  		os.Exit(1)
    80  	}()
    81  	return stop
    82  }
    83  
    84  func main() {
    85  	flag.Parse()
    86  	logrusutil.NewDefaultFieldsFormatter(nil, logrus.Fields{"component": "build"})
    87  	stop := stopper()
    88  
    89  	cfg, err := clientcmd.BuildConfigFromFlags(masterURL, kubeconfig)
    90  	if err != nil {
    91  		logrus.Fatalf("Error building kubeconfig: %v", err)
    92  	}
    93  
    94  	kc, err := kubernetes.NewForConfig(cfg)
    95  	if err != nil {
    96  		logrus.Fatalf("Error building kubernetes client: %v", err)
    97  	}
    98  
    99  	pjc, err := prowjobset.NewForConfig(cfg)
   100  	if err != nil {
   101  		logrus.Fatalf("Error building prowjob client: %v", err)
   102  	}
   103  
   104  	bc, err := buildset.NewForConfig(cfg)
   105  	if err != nil {
   106  		logrus.Fatalf("Error building build client: %v", err)
   107  	}
   108  
   109  	// Assume watches receive updates, but resync every 30m in case something wonky happens
   110  	pjif := prowjobinfo.NewSharedInformerFactory(pjc, time.Minute*30)
   111  	bif := buildinfo.NewSharedInformerFactory(bc, time.Minute*30)
   112  
   113  	controller := newController(kc, pjc, bc, pjif.Prow().V1().ProwJobs(), bif.Build().V1alpha1().Builds())
   114  
   115  	go pjif.Start(stop)
   116  	go bif.Start(stop)
   117  
   118  	if err = controller.Run(2, stop); err != nil {
   119  		logrus.Fatalf("Error running controller: %v", err)
   120  	}
   121  }
   122  
   123  type controller struct {
   124  	pjc prowjobset.Interface
   125  	bc  buildset.Interface
   126  
   127  	pjLister prowjoblisters.ProwJobLister
   128  	pjSynced cache.InformerSynced
   129  	bLister  buildlisters.BuildLister
   130  	bSynced  cache.InformerSynced
   131  
   132  	workqueue workqueue.RateLimitingInterface
   133  
   134  	recorder record.EventRecorder
   135  }
   136  
   137  func newController(kc kubernetes.Interface, pjc prowjobset.Interface, bc buildset.Interface, pji prowjobinfov1.ProwJobInformer, bi buildinfov1alpha1.BuildInformer) *controller {
   138  	// Log to events
   139  	prowjobscheme.AddToScheme(scheme.Scheme)
   140  	eventBroadcaster := record.NewBroadcaster()
   141  	eventBroadcaster.StartLogging(logrus.Infof)
   142  	eventBroadcaster.StartRecordingToSink(&corev1.EventSinkImpl{Interface: kc.CoreV1().Events("")})
   143  	recorder := eventBroadcaster.NewRecorder(scheme.Scheme, untypedcorev1.EventSource{Component: controllerName})
   144  
   145  	// Create struct
   146  	c := &controller{
   147  		pjc:       pjc,
   148  		bc:        bc,
   149  		pjLister:  pji.Lister(),
   150  		pjSynced:  pji.Informer().HasSynced,
   151  		bLister:   bi.Lister(),
   152  		bSynced:   bi.Informer().HasSynced,
   153  		workqueue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), controllerName),
   154  		recorder:  recorder,
   155  	}
   156  
   157  	logrus.Info("Setting up event handlers")
   158  
   159  	// Reconcile whenever a prowjob changes
   160  	pji.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
   161  		AddFunc: c.enqueueKey,
   162  		UpdateFunc: func(old, new interface{}) {
   163  			c.enqueueKey(new)
   164  		},
   165  		DeleteFunc: c.enqueueKey,
   166  	})
   167  
   168  	// Reconvile whenever a build changes.
   169  	bi.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
   170  		AddFunc: c.enqueueKey,
   171  		UpdateFunc: func(old, new interface{}) {
   172  			c.enqueueKey(new)
   173  		},
   174  		DeleteFunc: c.enqueueKey,
   175  	})
   176  
   177  	return c
   178  }
   179  
   180  // Run starts threads workers, returning after receiving a stop signal.
   181  func (c *controller) Run(threads int, stop <-chan struct{}) error {
   182  	defer runtime.HandleCrash()
   183  	defer c.workqueue.ShutDown()
   184  
   185  	logrus.Info("Starting Build controller")
   186  	logrus.Info("Waiting for informer caches to sync")
   187  	if ok := cache.WaitForCacheSync(stop, c.pjSynced, c.bSynced); !ok {
   188  		return fmt.Errorf("failed to wait for caches to sync")
   189  	}
   190  
   191  	logrus.Info("Starting workers")
   192  	for i := 0; i < threads; i++ {
   193  		go wait.Until(c.runWorker, time.Second, stop)
   194  	}
   195  
   196  	logrus.Info("Started workers")
   197  	<-stop
   198  	logrus.Info("Shutting down workers")
   199  	return nil
   200  }
   201  
   202  // runWorker dequeues to reconcile, until the queue has closed.
   203  func (c *controller) runWorker() {
   204  	for {
   205  		key, shutdown := c.workqueue.Get()
   206  		if shutdown {
   207  			return
   208  		}
   209  		defer c.workqueue.Done(key)
   210  
   211  		if err := reconcile(c, key.(string)); err != nil {
   212  			runtime.HandleError(fmt.Errorf("failed to reconcile %s: %v", key, err))
   213  			continue // Do not forget so we retry later.
   214  		}
   215  		c.workqueue.Forget(key)
   216  	}
   217  }
   218  
   219  // enqueueKey schedules an item for reconciliation.
   220  func (c *controller) enqueueKey(obj interface{}) {
   221  	key, err := cache.MetaNamespaceKeyFunc(obj)
   222  	if err != nil {
   223  		runtime.HandleError(err)
   224  		return
   225  	}
   226  	c.workqueue.AddRateLimited(key)
   227  }
   228  
   229  type reconciler interface {
   230  	getProwJob(namespace, name string) (*prowjobv1.ProwJob, error)
   231  	getBuild(namespace, name string) (*buildv1alpha1.Build, error)
   232  	deleteBuild(namespace, name string) error
   233  	createBuild(namespace string, b *buildv1alpha1.Build) (*buildv1alpha1.Build, error)
   234  	updateBuild(namespace string, b *buildv1alpha1.Build) (*buildv1alpha1.Build, error)
   235  	updateProwJob(namespace string, pj *prowjobv1.ProwJob) (*prowjobv1.ProwJob, error)
   236  	now() metav1.Time
   237  }
   238  
   239  func (c *controller) getProwJob(namespace, name string) (*prowjobv1.ProwJob, error) {
   240  	return c.pjLister.ProwJobs(namespace).Get(name)
   241  }
   242  
   243  func (c *controller) getBuild(namespace, name string) (*buildv1alpha1.Build, error) {
   244  	return c.bLister.Builds(namespace).Get(name)
   245  }
   246  func (c *controller) deleteBuild(namespace, name string) error {
   247  	return c.bc.BuildV1alpha1().Builds(namespace).Delete(name, &metav1.DeleteOptions{})
   248  }
   249  func (c *controller) createBuild(namespace string, b *buildv1alpha1.Build) (*buildv1alpha1.Build, error) {
   250  	return c.bc.BuildV1alpha1().Builds(namespace).Create(b)
   251  }
   252  func (c *controller) updateBuild(namespace string, b *buildv1alpha1.Build) (*buildv1alpha1.Build, error) {
   253  	return c.bc.BuildV1alpha1().Builds(namespace).Update(b)
   254  }
   255  func (c *controller) updateProwJob(namespace string, pj *prowjobv1.ProwJob) (*prowjobv1.ProwJob, error) {
   256  	return c.pjc.ProwV1().ProwJobs(namespace).Update(pj)
   257  }
   258  
   259  func (c *controller) now() metav1.Time {
   260  	return metav1.Now()
   261  }
   262  
   263  var (
   264  	groupVersionKind = schema.GroupVersionKind{
   265  		Group:   prowjobv1.SchemeGroupVersion.Group,
   266  		Version: prowjobv1.SchemeGroupVersion.Version,
   267  		Kind:    "ProwJob",
   268  	}
   269  )
   270  
   271  // reconcile ensures that the prowjob and build
   272  func reconcile(c reconciler, key string) error {
   273  	namespace, name, err := cache.SplitMetaNamespaceKey(key)
   274  	if err != nil {
   275  		runtime.HandleError(fmt.Errorf("invalid resource %s: %v", key, err))
   276  	}
   277  
   278  	var wantBuild bool
   279  
   280  	pj, err := c.getProwJob(namespace, name)
   281  	switch {
   282  	case apierrors.IsNotFound(err):
   283  		// Do not want build
   284  	case err != nil:
   285  		return fmt.Errorf("get prowjob: %v", err)
   286  	case pj.Spec.Agent != prowjobv1.BuildAgent:
   287  		// Do not want a build for this job
   288  	case pj.DeletionTimestamp == nil:
   289  		wantBuild = true
   290  	}
   291  
   292  	var haveBuild bool
   293  
   294  	b, err := c.getBuild(namespace, name)
   295  	switch {
   296  	case apierrors.IsNotFound(err):
   297  		// Do not have a build
   298  	case err != nil:
   299  		return fmt.Errorf("get build: %v", err)
   300  	case b.DeletionTimestamp == nil:
   301  		haveBuild = true
   302  	}
   303  
   304  	// Should we create or delete this build?
   305  	switch {
   306  	case !wantBuild:
   307  		if !haveBuild {
   308  			logrus.Infof("Deleted %s", key)
   309  			return nil
   310  		}
   311  		switch or := metav1.GetControllerOf(b); {
   312  		case or == nil, or.APIVersion != groupVersionKind.GroupVersion().String(), or.Kind != groupVersionKind.Kind:
   313  			return nil // Not controlled by this
   314  		}
   315  		logrus.Infof("Delete builds/%s", key)
   316  		if err = c.deleteBuild(namespace, name); err != nil {
   317  			return fmt.Errorf("delete build: %v", err)
   318  		}
   319  		return nil
   320  	case finalState(pj.Status.State):
   321  		logrus.Infof("Finished %s", key)
   322  		return nil
   323  	case wantBuild && pj.Spec.BuildSpec == nil:
   324  		return errors.New("nil BuildSpec")
   325  	case wantBuild && !haveBuild:
   326  		if b, err = makeBuild(*pj); err != nil {
   327  			return fmt.Errorf("make build: %v", err)
   328  		}
   329  		logrus.Infof("Create builds/%s", key)
   330  		if b, err = c.createBuild(namespace, b); err != nil {
   331  			return fmt.Errorf("create build: %v", err)
   332  		}
   333  	}
   334  
   335  	// Ensure spec is correct
   336  	pj.Spec.BuildSpec.Generation = b.Spec.Generation
   337  	if !reflect.DeepEqual(b.Spec, *pj.Spec.BuildSpec) {
   338  		logrus.Infof("\n%#v\n!=\n%#v", b.Spec, *pj.Spec.BuildSpec)
   339  		b2 := b.DeepCopy()
   340  		b2.Spec = *pj.Spec.BuildSpec
   341  		logrus.Infof("Update builds/%s", key)
   342  		if b, err = c.updateBuild(namespace, b2); err != nil {
   343  			return fmt.Errorf("update build spec: %v", err)
   344  		}
   345  	}
   346  
   347  	// Ensure prowjob status is correct
   348  	haveState := pj.Status.State
   349  	haveMsg := pj.Status.Description
   350  	wantState, wantMsg := prowJobStatus(b.Status)
   351  	if haveState != wantState || haveMsg != wantMsg {
   352  		npj := pj.DeepCopy()
   353  		if npj.Status.StartTime.IsZero() {
   354  			npj.Status.StartTime = c.now()
   355  		}
   356  		if npj.Status.CompletionTime.IsZero() && finalState(wantState) {
   357  			now := c.now()
   358  			npj.Status.CompletionTime = &now
   359  		}
   360  		npj.Status.State = wantState
   361  		npj.Status.Description = wantMsg
   362  		logrus.Infof("Update prowjobs/%s", key)
   363  		if _, err = c.updateProwJob(namespace, npj); err != nil {
   364  			return fmt.Errorf("update prow status: %v", err)
   365  		}
   366  	}
   367  	return nil
   368  }
   369  
   370  // finalState returns true if the prowjob has already finished
   371  func finalState(status prowjobv1.ProwJobState) bool {
   372  	switch status {
   373  	case "", prowjobv1.PendingState, prowjobv1.TriggeredState:
   374  		return false
   375  	}
   376  	return true
   377  }
   378  
   379  // description computes the ProwJobStatus description for this condition or falling back to a default if none is provided.
   380  func description(cond buildv1alpha1.BuildCondition, fallback string) string {
   381  	switch {
   382  	case cond.Message != "":
   383  		return cond.Message
   384  	case cond.Reason != "":
   385  		return cond.Reason
   386  	}
   387  	return fallback
   388  }
   389  
   390  const (
   391  	descScheduling       = "scheduling"
   392  	descInitializing     = "initializing"
   393  	descRunning          = "running"
   394  	descSucceeded        = "succeeded"
   395  	descFailed           = "failed"
   396  	descUnknown          = "unknown status"
   397  	descMissingCondition = "missing end condition"
   398  )
   399  
   400  // prowJobStatus returns the desired state and description based on the build status.
   401  func prowJobStatus(bs buildv1alpha1.BuildStatus) (prowjobv1.ProwJobState, string) {
   402  	started := bs.StartTime
   403  	finished := bs.CompletionTime
   404  	pcond := bs.GetCondition(buildv1alpha1.BuildSucceeded)
   405  	if pcond == nil {
   406  		if !finished.IsZero() {
   407  			return prowjobv1.ErrorState, descMissingCondition
   408  		}
   409  		return prowjobv1.PendingState, descScheduling
   410  	}
   411  	cond := *pcond
   412  	switch {
   413  	case cond.Status == untypedcorev1.ConditionTrue:
   414  		return prowjobv1.SuccessState, description(cond, descSucceeded)
   415  	case cond.Status == untypedcorev1.ConditionFalse:
   416  		return prowjobv1.FailureState, description(cond, descFailed)
   417  	case started.IsZero():
   418  		return prowjobv1.TriggeredState, description(cond, descInitializing)
   419  	case finished.IsZero():
   420  		return prowjobv1.PendingState, description(cond, descRunning)
   421  	}
   422  	return prowjobv1.ErrorState, description(cond, descUnknown) // shouldn't happen
   423  }
   424  
   425  // makeBuild creates a build from the prowjob, using the prowjob's buildspec.
   426  func makeBuild(pj prowjobv1.ProwJob) (*buildv1alpha1.Build, error) {
   427  	if pj.Spec.BuildSpec == nil {
   428  		return nil, errors.New("nil BuildSpec")
   429  	}
   430  	return &buildv1alpha1.Build{
   431  		ObjectMeta: metav1.ObjectMeta{
   432  			Name:      pj.Name,
   433  			Namespace: pj.Namespace,
   434  			OwnerReferences: []metav1.OwnerReference{
   435  				*metav1.NewControllerRef(&pj, groupVersionKind),
   436  			},
   437  		},
   438  		Spec: *pj.Spec.BuildSpec,
   439  	}, nil
   440  }