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 }