Browse code

retry build errors

Ben Parees authored on 2015/02/25 11:01:53
Showing 8 changed files
... ...
@@ -8,7 +8,6 @@ import (
8 8
 	kapi "github.com/GoogleCloudPlatform/kubernetes/pkg/api"
9 9
 	errors "github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors"
10 10
 	"github.com/GoogleCloudPlatform/kubernetes/pkg/client/cache"
11
-	"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
12 11
 
13 12
 	buildapi "github.com/openshift/origin/pkg/build/api"
14 13
 	buildclient "github.com/openshift/origin/pkg/build/client"
... ...
@@ -17,9 +16,6 @@ import (
17 17
 
18 18
 // BuildController watches build resources and manages their state
19 19
 type BuildController struct {
20
-	BuildStore    cache.Store
21
-	NextBuild     func() *buildapi.Build
22
-	NextPod       func() *kapi.Pod
23 20
 	BuildUpdater  buildclient.BuildUpdater
24 21
 	PodManager    podManager
25 22
 	BuildStrategy BuildStrategy
... ...
@@ -41,32 +37,30 @@ type imageRepositoryClient interface {
41 41
 	GetImageRepository(namespace, name string) (*imageapi.ImageRepository, error)
42 42
 }
43 43
 
44
-// Run begins watching and syncing build jobs onto the cluster.
45
-func (bc *BuildController) Run() {
46
-	go util.Forever(func() { bc.HandleBuild(bc.NextBuild()) }, 0)
47
-	go util.Forever(func() { bc.HandlePod(bc.NextPod()) }, 0)
48
-}
49
-
50
-func (bc *BuildController) HandleBuild(build *buildapi.Build) {
44
+func (bc *BuildController) HandleBuild(build *buildapi.Build) error {
51 45
 	glog.V(4).Infof("Handling build %s", build.Name)
52 46
 
53 47
 	// We only deal with new builds here
54 48
 	if build.Status != buildapi.BuildStatusNew {
55
-		return
49
+		return nil
56 50
 	}
57 51
 
58 52
 	if err := bc.nextBuildStatus(build); err != nil {
59 53
 		// TODO: all build errors should be retried, and build error should not be a permanent status change.
60 54
 		// Instead, we should requeue this build request using the same backoff logic as the scheduler.
61
-		// BuildStatusError should be reserved for meaning "permanently errored, no way to try again".
62
-		glog.V(4).Infof("Build failed with error %s/%s: %#v", build.Namespace, build.Name, err)
63
-		build.Status = buildapi.BuildStatusError
64
-		build.Message = err.Error()
55
+		//build.Status = buildapi.BuildStatusError
56
+		//build.Message = err.Error()
57
+		return fmt.Errorf("Build failed with error %s/%s: %#v", build.Namespace, build.Name, err)
65 58
 	}
66 59
 
67 60
 	if err := bc.BuildUpdater.Update(build.Namespace, build); err != nil {
61
+		// This is not a retryable error because the build has been created.  The worst case
62
+		// outcome of not updating the buildconfig is that we might rerun a build for the
63
+		// same "new" imageid change in the future, which is better than guaranteeing we
64
+		// run the build 2+ times by retrying it here.
68 65
 		glog.V(2).Infof("Failed to record changes to build %s/%s: %#v", build.Namespace, build.Name, err)
69 66
 	}
67
+	return nil
70 68
 }
71 69
 
72 70
 // nextBuildStatus updates build with any appropriate changes, or returns an error if
... ...
@@ -134,7 +128,14 @@ func (bc *BuildController) nextBuildStatus(build *buildapi.Build) error {
134 134
 	return nil
135 135
 }
136 136
 
137
-func (bc *BuildController) HandlePod(pod *kapi.Pod) {
137
+// BuildPodController watches pods running builds and manages the build state
138
+type BuildPodController struct {
139
+	BuildStore   cache.Store
140
+	BuildUpdater buildclient.BuildUpdater
141
+	PodManager   podManager
142
+}
143
+
144
+func (bc *BuildPodController) HandlePod(pod *kapi.Pod) error {
138 145
 	// Find the build for this pod
139 146
 	var build *buildapi.Build
140 147
 	for _, obj := range bc.BuildStore.List() {
... ...
@@ -146,7 +147,7 @@ func (bc *BuildController) HandlePod(pod *kapi.Pod) {
146 146
 	}
147 147
 
148 148
 	if build == nil {
149
-		return
149
+		return nil
150 150
 	}
151 151
 
152 152
 	// A cancelling event was triggered for the build, delete its pod and update build status.
... ...
@@ -154,9 +155,9 @@ func (bc *BuildController) HandlePod(pod *kapi.Pod) {
154 154
 		glog.V(2).Infof("Cancelling build %s.", build.Name)
155 155
 
156 156
 		if err := bc.CancelBuild(build, pod); err != nil {
157
-			glog.Errorf("Failed to cancel build %s: %#v", build.Name, err)
157
+			return fmt.Errorf("Failed to cancel build %s: %#v, will retry", build.Name, err)
158 158
 		}
159
-		return
159
+		return nil
160 160
 	}
161 161
 
162 162
 	nextStatus := build.Status
... ...
@@ -180,13 +181,14 @@ func (bc *BuildController) HandlePod(pod *kapi.Pod) {
180 180
 		glog.V(4).Infof("Updating build %s status %s -> %s", build.Name, build.Status, nextStatus)
181 181
 		build.Status = nextStatus
182 182
 		if err := bc.BuildUpdater.Update(build.Namespace, build); err != nil {
183
-			glog.Errorf("Failed to update build %s: %#v", build.Name, err)
183
+			return fmt.Errorf("Failed to update build %s: %#v", build.Name, err)
184 184
 		}
185 185
 	}
186
+	return nil
186 187
 }
187 188
 
188
-// CancelBuild updates a build status to Cancelled, after its associated pod is associated.
189
-func (bc *BuildController) CancelBuild(build *buildapi.Build, pod *kapi.Pod) error {
189
+// CancelBuild updates a build status to Cancelled, after its associated pod is deleted.
190
+func (bc *BuildPodController) CancelBuild(build *buildapi.Build, pod *kapi.Pod) error {
190 191
 	if !isBuildCancellable(build) {
191 192
 		glog.V(2).Infof("The build can be cancelled only if it has pending/running status, not %s.", build.Status)
192 193
 		return nil
... ...
@@ -2,6 +2,7 @@ package controller
2 2
 
3 3
 import (
4 4
 	"errors"
5
+	"reflect"
5 6
 	"testing"
6 7
 
7 8
 	kapi "github.com/GoogleCloudPlatform/kubernetes/pkg/api"
... ...
@@ -93,8 +94,8 @@ func (_ *errNotFoundImageRepositoryClient) GetImageRepository(namespace, name st
93 93
 	return nil, kerrors.NewNotFound("ImageRepository", name)
94 94
 }
95 95
 
96
-func mockBuildAndController(status buildapi.BuildStatus, output buildapi.BuildOutput) (build *buildapi.Build, controller *BuildController) {
97
-	build = &buildapi.Build{
96
+func mockBuild(status buildapi.BuildStatus, output buildapi.BuildOutput) *buildapi.Build {
97
+	return &buildapi.Build{
98 98
 		ObjectMeta: kapi.ObjectMeta{
99 99
 			Name:      "data-build",
100 100
 			Namespace: "namespace",
... ...
@@ -119,16 +120,23 @@ func mockBuildAndController(status buildapi.BuildStatus, output buildapi.BuildOu
119 119
 		Status:  status,
120 120
 		PodName: "-the-pod-id",
121 121
 	}
122
-	controller = &BuildController{
123
-		BuildStore:            buildtest.NewFakeBuildStore(build),
122
+}
123
+
124
+func mockBuildController() *BuildController {
125
+	return &BuildController{
124 126
 		BuildUpdater:          &okBuildUpdater{},
125 127
 		PodManager:            &okPodManager{},
126
-		NextBuild:             func() *buildapi.Build { return nil },
127
-		NextPod:               func() *kapi.Pod { return nil },
128 128
 		BuildStrategy:         &okStrategy{},
129 129
 		ImageRepositoryClient: &okImageRepositoryClient{},
130 130
 	}
131
-	return
131
+}
132
+
133
+func mockBuildPodController(build *buildapi.Build) *BuildPodController {
134
+	return &BuildPodController{
135
+		BuildStore:   buildtest.NewFakeBuildStore(build),
136
+		BuildUpdater: &okBuildUpdater{},
137
+		PodManager:   &okPodManager{},
138
+	}
132 139
 }
133 140
 
134 141
 func mockPod(status kapi.PodPhase, exitCode int) *kapi.Pod {
... ...
@@ -257,7 +265,7 @@ func TestHandleBuild(t *testing.T) {
257 257
 		},
258 258
 		{ // 12
259 259
 			inStatus:    buildapi.BuildStatusNew,
260
-			outStatus:   buildapi.BuildStatusError, // TODO: this should be a retry
260
+			outStatus:   buildapi.BuildStatusError,
261 261
 			imageClient: &errNotFoundImageRepositoryClient{},
262 262
 			buildOutput: buildapi.BuildOutput{
263 263
 				To: &kapi.ObjectReference{
... ...
@@ -275,10 +283,24 @@ func TestHandleBuild(t *testing.T) {
275 275
 				},
276 276
 			},
277 277
 		},
278
+		{ // 14
279
+			inStatus:  buildapi.BuildStatusNew,
280
+			outStatus: buildapi.BuildStatusPending,
281
+			buildOutput: buildapi.BuildOutput{
282
+				To: &kapi.ObjectReference{
283
+					Name:      "foo",
284
+					Namespace: "bar",
285
+				},
286
+			},
287
+			outputSpec: "image/repo",
288
+			// an error updating the build is not reported as an error.
289
+			buildUpdater: &errBuildUpdater{},
290
+		},
278 291
 	}
279 292
 
280 293
 	for i, tc := range tests {
281
-		build, ctrl := mockBuildAndController(tc.inStatus, tc.buildOutput)
294
+		build := mockBuild(tc.inStatus, tc.buildOutput)
295
+		ctrl := mockBuildController()
282 296
 		if tc.buildStrategy != nil {
283 297
 			ctrl.BuildStrategy = tc.buildStrategy
284 298
 		}
... ...
@@ -292,8 +314,20 @@ func TestHandleBuild(t *testing.T) {
292 292
 			ctrl.ImageRepositoryClient = tc.imageClient
293 293
 		}
294 294
 
295
-		ctrl.HandleBuild(build)
295
+		err := ctrl.HandleBuild(build)
296
+
297
+		// ensure we return an error for cases where expected output is an error.
298
+		// these will be retried by the retrycontroller
299
+		if tc.inStatus != buildapi.BuildStatusError && tc.outStatus == buildapi.BuildStatusError {
300
+			if err == nil {
301
+				t.Errorf("(%d) Expected an error from HandleBuild, got none!", i)
302
+			}
303
+			continue
304
+		}
296 305
 
306
+		if err != nil {
307
+			t.Errorf("(%d) Unexpected error %v", err)
308
+		}
297 309
 		if build.Status != tc.outStatus {
298 310
 			t.Errorf("(%d) Expected %s, got %s!", i, tc.outStatus, build.Status)
299 311
 		}
... ...
@@ -317,6 +351,7 @@ func TestHandlePod(t *testing.T) {
317 317
 		podStatus    kapi.PodPhase
318 318
 		exitCode     int
319 319
 		buildUpdater buildclient.BuildUpdater
320
+		podManager   podManager
320 321
 	}
321 322
 
322 323
 	tests := []handlePodTest{
... ...
@@ -366,7 +401,8 @@ func TestHandlePod(t *testing.T) {
366 366
 	}
367 367
 
368 368
 	for i, tc := range tests {
369
-		build, ctrl := mockBuildAndController(tc.inStatus, buildapi.BuildOutput{})
369
+		build := mockBuild(tc.inStatus, buildapi.BuildOutput{})
370
+		ctrl := mockBuildPodController(build)
370 371
 		pod := mockPod(tc.podStatus, tc.exitCode)
371 372
 		if tc.matchID {
372 373
 			build.PodName = pod.Name
... ...
@@ -375,8 +411,16 @@ func TestHandlePod(t *testing.T) {
375 375
 			ctrl.BuildUpdater = tc.buildUpdater
376 376
 		}
377 377
 
378
-		ctrl.HandlePod(pod)
378
+		err := ctrl.HandlePod(pod)
379 379
 
380
+		if tc.buildUpdater != nil && reflect.TypeOf(tc.buildUpdater).Elem().Name() == "errBuildUpdater" {
381
+			if err == nil {
382
+				t.Errorf("(%d) Expected error, got none", i)
383
+			}
384
+			// can't check tc.outStatus because the local build object does get updated
385
+			// in this test (but would not updated in etcd)
386
+			continue
387
+		}
380 388
 		if build.Status != tc.outStatus {
381 389
 			t.Errorf("(%d) Expected %s, got %s!", i, tc.outStatus, build.Status)
382 390
 		}
... ...
@@ -385,10 +429,12 @@ func TestHandlePod(t *testing.T) {
385 385
 
386 386
 func TestCancelBuild(t *testing.T) {
387 387
 	type handleCancelBuildTest struct {
388
-		inStatus  buildapi.BuildStatus
389
-		outStatus buildapi.BuildStatus
390
-		podStatus kapi.PodPhase
391
-		exitCode  int
388
+		inStatus     buildapi.BuildStatus
389
+		outStatus    buildapi.BuildStatus
390
+		podStatus    kapi.PodPhase
391
+		exitCode     int
392
+		buildUpdater buildclient.BuildUpdater
393
+		podManager   podManager
392 394
 	}
393 395
 
394 396
 	tests := []handleCancelBuildTest{
... ...
@@ -421,13 +467,48 @@ func TestCancelBuild(t *testing.T) {
421 421
 			podStatus: kapi.PodFailed,
422 422
 			exitCode:  1,
423 423
 		},
424
+		{ // 5
425
+			inStatus:   buildapi.BuildStatusNew,
426
+			outStatus:  buildapi.BuildStatusNew,
427
+			podStatus:  kapi.PodFailed,
428
+			exitCode:   1,
429
+			podManager: &errPodManager{},
430
+		},
431
+		{ // 6
432
+			inStatus:     buildapi.BuildStatusNew,
433
+			outStatus:    buildapi.BuildStatusNew,
434
+			podStatus:    kapi.PodFailed,
435
+			exitCode:     1,
436
+			buildUpdater: &errBuildUpdater{},
437
+		},
424 438
 	}
425 439
 
426 440
 	for i, tc := range tests {
427
-		build, ctrl := mockBuildAndController(tc.inStatus, buildapi.BuildOutput{})
441
+		build := mockBuild(tc.inStatus, buildapi.BuildOutput{})
442
+		ctrl := mockBuildPodController(build)
428 443
 		pod := mockPod(tc.podStatus, tc.exitCode)
444
+		if tc.buildUpdater != nil {
445
+			ctrl.BuildUpdater = tc.buildUpdater
446
+		}
447
+		if tc.podManager != nil {
448
+			ctrl.PodManager = tc.podManager
449
+		}
429 450
 
430
-		ctrl.CancelBuild(build, pod)
451
+		err := ctrl.CancelBuild(build, pod)
452
+
453
+		if tc.podManager != nil && reflect.TypeOf(tc.podManager).Elem().Name() == "errPodManager" {
454
+			if err == nil {
455
+				t.Errorf("(%d) Expected error, got none", i)
456
+			}
457
+		}
458
+		if tc.buildUpdater != nil && reflect.TypeOf(tc.buildUpdater).Elem().Name() == "errBuildUpdater" {
459
+			if err == nil {
460
+				t.Errorf("(%d) Expected error, got none", i)
461
+			}
462
+			// can't check tc.outStatus because the local build object does get updated
463
+			// in this test (but would not updated in etcd)
464
+			continue
465
+		}
431 466
 
432 467
 		if build.Status != tc.outStatus {
433 468
 			t.Errorf("(%d) Expected %s, got %s!", i, tc.outStatus, build.Status)
... ...
@@ -11,16 +11,19 @@ import (
11 11
 	"github.com/GoogleCloudPlatform/kubernetes/pkg/client/cache"
12 12
 	"github.com/GoogleCloudPlatform/kubernetes/pkg/labels"
13 13
 	"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
14
+	kutil "github.com/GoogleCloudPlatform/kubernetes/pkg/util"
14 15
 	"github.com/GoogleCloudPlatform/kubernetes/pkg/watch"
15 16
 
16 17
 	buildapi "github.com/openshift/origin/pkg/build/api"
17 18
 	buildclient "github.com/openshift/origin/pkg/build/client"
18
-	controller "github.com/openshift/origin/pkg/build/controller"
19
+	buildcontroller "github.com/openshift/origin/pkg/build/controller"
19 20
 	strategy "github.com/openshift/origin/pkg/build/controller/strategy"
20 21
 	osclient "github.com/openshift/origin/pkg/client"
22
+	controller "github.com/openshift/origin/pkg/controller"
21 23
 	imageapi "github.com/openshift/origin/pkg/image/api"
22 24
 )
23 25
 
26
+// BuildControllerFactory constructs BuildController objects
24 27
 type BuildControllerFactory struct {
25 28
 	OSClient            osclient.Interface
26 29
 	KubeClient          kclient.Interface
... ...
@@ -30,16 +33,55 @@ type BuildControllerFactory struct {
30 30
 	CustomBuildStrategy *strategy.CustomBuildStrategy
31 31
 	// Stop may be set to allow controllers created by this factory to be terminated.
32 32
 	Stop <-chan struct{}
33
+}
34
+
35
+// Create constructs a BuildController
36
+func (factory *BuildControllerFactory) Create() controller.RunnableController {
37
+	queue := cache.NewFIFO(cache.MetaNamespaceKeyFunc)
38
+	cache.NewReflector(&buildLW{client: factory.OSClient}, &buildapi.Build{}, queue).Run()
39
+
40
+	client := ControllerClient{factory.KubeClient, factory.OSClient}
41
+	buildController := &buildcontroller.BuildController{
42
+		BuildUpdater:          factory.BuildUpdater,
43
+		ImageRepositoryClient: client,
44
+		PodManager:            client,
45
+		BuildStrategy: &typeBasedFactoryStrategy{
46
+			DockerBuildStrategy: factory.DockerBuildStrategy,
47
+			STIBuildStrategy:    factory.STIBuildStrategy,
48
+			CustomBuildStrategy: factory.CustomBuildStrategy,
49
+		},
50
+	}
51
+
52
+	return &controller.RetryController{
53
+		Queue:        queue,
54
+		RetryManager: controller.NewQueueRetryManager(queue, cache.MetaNamespaceKeyFunc, -1),
55
+		ShouldRetry: func(obj interface{}, err error) bool {
56
+			kutil.HandleError(err)
57
+			// BuildController currently has no fatal errors.
58
+			return true
59
+		},
60
+		Handle: func(obj interface{}) error {
61
+			build := obj.(*buildapi.Build)
62
+			return buildController.HandleBuild(build)
63
+		},
64
+	}
65
+}
66
+
67
+// BuildPodControllerFactory construct BuildPodController objects
68
+type BuildPodControllerFactory struct {
69
+	OSClient     osclient.Interface
70
+	KubeClient   kclient.Interface
71
+	BuildUpdater buildclient.BuildUpdater
72
+	// Stop may be set to allow controllers created by this factory to be terminated.
73
+	Stop <-chan struct{}
33 74
 
34 75
 	buildStore cache.Store
35 76
 }
36 77
 
37
-func (factory *BuildControllerFactory) Create() *controller.BuildController {
78
+// Create constructs a BuildPodController
79
+func (factory *BuildPodControllerFactory) Create() controller.RunnableController {
38 80
 	factory.buildStore = cache.NewStore(cache.MetaNamespaceKeyFunc)
39
-	cache.NewReflector(&buildLW{client: factory.OSClient}, &buildapi.Build{}, factory.buildStore).RunUntil(factory.Stop)
40
-
41
-	buildQueue := cache.NewFIFO(cache.MetaNamespaceKeyFunc)
42
-	cache.NewReflector(&buildLW{client: factory.OSClient}, &buildapi.Build{}, buildQueue).RunUntil(factory.Stop)
81
+	cache.NewReflector(&buildLW{client: factory.OSClient}, &buildapi.Build{}, factory.buildStore).Run()
43 82
 
44 83
 	// Kubernetes does not currently synchronize Pod status in storage with a Pod's container
45 84
 	// states. Because of this, we can't receive events related to container (and thus Pod)
... ...
@@ -49,29 +91,27 @@ func (factory *BuildControllerFactory) Create() *controller.BuildController {
49 49
 	//
50 50
 	// TODO: Find a way to get watch events for Pod/container status updates. The polling
51 51
 	// strategy is horribly inefficient and should be addressed upstream somehow.
52
-	podQueue := cache.NewFIFO(cache.MetaNamespaceKeyFunc)
53
-	cache.NewPoller(factory.pollPods, 10*time.Second, podQueue).RunUntil(factory.Stop)
52
+	queue := cache.NewFIFO(cache.MetaNamespaceKeyFunc)
53
+	cache.NewPoller(factory.pollPods, 10*time.Second, queue).RunUntil(factory.Stop)
54 54
 
55 55
 	client := ControllerClient{factory.KubeClient, factory.OSClient}
56
-	return &controller.BuildController{
57
-		BuildStore:            factory.buildStore,
58
-		BuildUpdater:          factory.BuildUpdater,
59
-		ImageRepositoryClient: client,
60
-		PodManager:            client,
61
-		NextBuild: func() *buildapi.Build {
62
-			build := buildQueue.Pop().(*buildapi.Build)
63
-			panicIfStopped(factory.Stop, "build controller stopped")
64
-			return build
65
-		},
66
-		NextPod: func() *kapi.Pod {
67
-			pod := podQueue.Pop().(*kapi.Pod)
68
-			panicIfStopped(factory.Stop, "build controller stopped")
69
-			return pod
56
+	buildPodController := &buildcontroller.BuildPodController{
57
+		BuildStore:   factory.buildStore,
58
+		BuildUpdater: factory.BuildUpdater,
59
+		PodManager:   client,
60
+	}
61
+
62
+	return &controller.RetryController{
63
+		Queue:        queue,
64
+		RetryManager: controller.NewQueueRetryManager(queue, cache.MetaNamespaceKeyFunc, -1),
65
+		ShouldRetry: func(obj interface{}, err error) bool {
66
+			kutil.HandleError(err)
67
+			// BuildPodController currently has no fatal errors.
68
+			return true
70 69
 		},
71
-		BuildStrategy: &typeBasedFactoryStrategy{
72
-			DockerBuildStrategy: factory.DockerBuildStrategy,
73
-			STIBuildStrategy:    factory.STIBuildStrategy,
74
-			CustomBuildStrategy: factory.CustomBuildStrategy,
70
+		Handle: func(obj interface{}) error {
71
+			pod := obj.(*kapi.Pod)
72
+			return buildPodController.HandlePod(pod)
75 73
 		},
76 74
 	}
77 75
 }
... ...
@@ -88,29 +128,40 @@ type ImageChangeControllerFactory struct {
88 88
 
89 89
 // Create creates a new ImageChangeController which is used to trigger builds when a new
90 90
 // image is available
91
-func (factory *ImageChangeControllerFactory) Create() *controller.ImageChangeController {
91
+func (factory *ImageChangeControllerFactory) Create() controller.RunnableController {
92 92
 	queue := cache.NewFIFO(cache.MetaNamespaceKeyFunc)
93
-	cache.NewReflector(&imageRepositoryLW{factory.Client}, &imageapi.ImageRepository{}, queue).RunUntil(factory.Stop)
93
+	cache.NewReflector(&imageRepositoryLW{factory.Client}, &imageapi.ImageRepository{}, queue).Run()
94 94
 
95 95
 	store := cache.NewStore(cache.MetaNamespaceKeyFunc)
96
-	cache.NewReflector(&buildConfigLW{client: factory.Client}, &buildapi.BuildConfig{}, store).RunUntil(factory.Stop)
96
+	cache.NewReflector(&buildConfigLW{client: factory.Client}, &buildapi.BuildConfig{}, store).Run()
97 97
 
98
-	return &controller.ImageChangeController{
98
+	imageChangeController := &buildcontroller.ImageChangeController{
99 99
 		BuildConfigStore:   store,
100 100
 		BuildConfigUpdater: factory.BuildConfigUpdater,
101 101
 		BuildCreator:       factory.BuildCreator,
102
-		NextImageRepository: func() *imageapi.ImageRepository {
103
-			repo := queue.Pop().(*imageapi.ImageRepository)
104
-			panicIfStopped(factory.Stop, "build image change controller stopped")
105
-			return repo
102
+		Stop:               factory.Stop,
103
+	}
104
+
105
+	return &controller.RetryController{
106
+		Queue:        queue,
107
+		RetryManager: controller.NewQueueRetryManager(queue, cache.MetaNamespaceKeyFunc, -1),
108
+		ShouldRetry: func(obj interface{}, err error) bool {
109
+			kutil.HandleError(err)
110
+			if _, isFatal := err.(buildcontroller.ImageChangeControllerFatalError); isFatal {
111
+				return false
112
+			}
113
+			return true
114
+		},
115
+		Handle: func(obj interface{}) error {
116
+			imageRepo := obj.(*imageapi.ImageRepository)
117
+			return imageChangeController.HandleImageRepo(imageRepo)
106 118
 		},
107
-		Stop: factory.Stop,
108 119
 	}
109 120
 }
110 121
 
111 122
 // pollPods lists pods for all builds in the buildStore which are pending or running and
112 123
 // returns an enumerator for cache.Poller. The poll scope is narrowed for efficiency.
113
-func (factory *BuildControllerFactory) pollPods() (cache.Enumerator, error) {
124
+func (factory *BuildPodControllerFactory) pollPods() (cache.Enumerator, error) {
114 125
 	list := &kapi.PodList{}
115 126
 
116 127
 	for _, obj := range factory.buildStore.List() {
... ...
@@ -1,10 +1,10 @@
1 1
 package controller
2 2
 
3 3
 import (
4
-	"github.com/golang/glog"
4
+	"fmt"
5 5
 
6 6
 	"github.com/GoogleCloudPlatform/kubernetes/pkg/client/cache"
7
-	"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
7
+	"github.com/golang/glog"
8 8
 
9 9
 	buildapi "github.com/openshift/origin/pkg/build/api"
10 10
 	buildclient "github.com/openshift/origin/pkg/build/client"
... ...
@@ -12,27 +12,28 @@ import (
12 12
 	imageapi "github.com/openshift/origin/pkg/image/api"
13 13
 )
14 14
 
15
+type ImageChangeControllerFatalError struct {
16
+	Reason string
17
+	Err    error
18
+}
19
+
20
+func (e ImageChangeControllerFatalError) Error() string {
21
+	return "fatal error handling ImageRepository change: " + e.Reason
22
+}
23
+
15 24
 // ImageChangeController watches for changes to ImageRepositories and triggers
16 25
 // builds when a new version of a tag referenced by a BuildConfig
17 26
 // is available.
18 27
 type ImageChangeController struct {
19
-	NextImageRepository func() *imageapi.ImageRepository
20
-	BuildConfigStore    cache.Store
21
-	BuildCreator        buildclient.BuildCreator
22
-	BuildConfigUpdater  buildclient.BuildConfigUpdater
28
+	BuildConfigStore   cache.Store
29
+	BuildCreator       buildclient.BuildCreator
30
+	BuildConfigUpdater buildclient.BuildConfigUpdater
23 31
 	// Stop is an optional channel that controls when the controller exits
24 32
 	Stop <-chan struct{}
25 33
 }
26 34
 
27
-// Run processes ImageRepository events one by one.
28
-func (c *ImageChangeController) Run() {
29
-	go util.Until(c.HandleImageRepo, 0, c.Stop)
30
-}
31
-
32 35
 // HandleImageRepo processes the next ImageRepository event.
33
-func (c *ImageChangeController) HandleImageRepo() {
34
-	glog.V(4).Infof("Waiting for imagerepo change")
35
-	imageRepo := c.NextImageRepository()
36
+func (c *ImageChangeController) HandleImageRepo(imageRepo *imageapi.ImageRepository) error {
36 37
 	glog.V(4).Infof("Build image change controller detected imagerepo change %s", imageRepo.DockerImageRepository)
37 38
 	imageSubstitutions := make(map[string]string)
38 39
 
... ...
@@ -78,12 +79,18 @@ func (c *ImageChangeController) HandleImageRepo() {
78 78
 			glog.V(4).Infof("Running build for buildConfig %s in namespace %s", config.Name, config.Namespace)
79 79
 			b := buildutil.GenerateBuildFromConfig(config, nil, imageSubstitutions)
80 80
 			if err := c.BuildCreator.Create(config.Namespace, b); err != nil {
81
-				glog.V(2).Infof("Error starting build for buildConfig %v: %v", config.Name, err)
81
+				return fmt.Errorf("Error starting build for buildConfig %s: %v", config.Name, err)
82 82
 			} else {
83 83
 				if err := c.BuildConfigUpdater.Update(config); err != nil {
84
+					// This is not a retryable error because the build has been created.  The worst case
85
+					// outcome of not updating the buildconfig is that we might rerun a build for the
86
+					// same "new" imageid change in the future, which is better than guaranteeing we
87
+					// run the build 2+ times by retrying it here.
84 88
 					glog.V(2).Infof("Error updating buildConfig %v: %v", config.Name, err)
89
+					return ImageChangeControllerFatalError{Reason: fmt.Sprintf("Error updating buildConfig %s with new LastTriggeredImageID", config.Name), Err: err}
85 90
 				}
86 91
 			}
87 92
 		}
88 93
 	}
94
+	return nil
89 95
 }
... ...
@@ -13,11 +13,12 @@ import (
13 13
 
14 14
 type mockBuildConfigUpdater struct {
15 15
 	buildcfg *buildapi.BuildConfig
16
+	err      error
16 17
 }
17 18
 
18 19
 func (m *mockBuildConfigUpdater) Update(buildcfg *buildapi.BuildConfig) error {
19 20
 	m.buildcfg = buildcfg
20
-	return nil
21
+	return m.err
21 22
 }
22 23
 
23 24
 type mockBuildCreator struct {
... ...
@@ -71,8 +72,8 @@ func appendTrigger(buildcfg *buildapi.BuildConfig, triggerImage, repoName, repoT
71 71
 	})
72 72
 }
73 73
 
74
-func mockImageChangeController(buildcfg *buildapi.BuildConfig, repoName, dockerImageRepo string, tags map[string]string) *ImageChangeController {
75
-	imageRepo := imageapi.ImageRepository{
74
+func mockImageRepo(repoName, dockerImageRepo string, tags map[string]string) *imageapi.ImageRepository {
75
+	return &imageapi.ImageRepository{
76 76
 		ObjectMeta: kapi.ObjectMeta{
77 77
 			Name: repoName,
78 78
 		},
... ...
@@ -81,23 +82,28 @@ func mockImageChangeController(buildcfg *buildapi.BuildConfig, repoName, dockerI
81 81
 		},
82 82
 		Tags: tags,
83 83
 	}
84
+}
84 85
 
86
+func mockImageChangeController(buildcfg *buildapi.BuildConfig) *ImageChangeController {
85 87
 	return &ImageChangeController{
86
-		NextImageRepository: func() *imageapi.ImageRepository { return &imageRepo },
87
-		BuildConfigStore:    buildtest.NewFakeBuildConfigStore(buildcfg),
88
-		BuildCreator:        &mockBuildCreator{},
89
-		BuildConfigUpdater:  &mockBuildConfigUpdater{},
88
+		BuildConfigStore:   buildtest.NewFakeBuildConfigStore(buildcfg),
89
+		BuildCreator:       &mockBuildCreator{},
90
+		BuildConfigUpdater: &mockBuildConfigUpdater{},
90 91
 	}
91 92
 }
92 93
 
93 94
 func TestNewImageID(t *testing.T) {
94 95
 	// valid configuration, new build should be triggered.
95 96
 	buildcfg := mockBuildConfig("registry.com/namespace/imagename", "registry.com/namespace/imagename", "testImageRepo", "testTag")
96
-	controller := mockImageChangeController(buildcfg, "testImageRepo", "registry.com/namespace/imagename", map[string]string{"testTag": "newImageID123"})
97
-	controller.HandleImageRepo()
97
+	imagerepo := mockImageRepo("testImageRepo", "registry.com/namespace/imagename", map[string]string{"testTag": "newImageID123"})
98
+	controller := mockImageChangeController(buildcfg)
99
+	err := controller.HandleImageRepo(imagerepo)
98 100
 	buildCreator := controller.BuildCreator.(*mockBuildCreator)
99 101
 	buildConfigUpdater := controller.BuildConfigUpdater.(*mockBuildConfigUpdater)
100 102
 
103
+	if err != nil {
104
+		t.Errorf("Unexpected error %v from HandleImageRepo", err)
105
+	}
101 106
 	if buildCreator.build == nil {
102 107
 		t.Error("Expected new build when new image was created!")
103 108
 	}
... ...
@@ -115,11 +121,15 @@ func TestNewImageID(t *testing.T) {
115 115
 func TestNewImageIDDefaultTag(t *testing.T) {
116 116
 	// valid configuration using default tag, new build should be triggered.
117 117
 	buildcfg := mockBuildConfig("registry.com/namespace/imagename", "registry.com/namespace/imagename", "testImageRepo", "")
118
-	controller := mockImageChangeController(buildcfg, "testImageRepo", "registry.com/namespace/imagename", map[string]string{buildapi.DefaultImageTag: "newImageID123"})
119
-	controller.HandleImageRepo()
118
+	imagerepo := mockImageRepo("testImageRepo", "registry.com/namespace/imagename", map[string]string{buildapi.DefaultImageTag: "newImageID123"})
119
+	controller := mockImageChangeController(buildcfg)
120
+	err := controller.HandleImageRepo(imagerepo)
120 121
 	buildCreator := controller.BuildCreator.(*mockBuildCreator)
121 122
 	buildConfigUpdater := controller.BuildConfigUpdater.(*mockBuildConfigUpdater)
122 123
 
124
+	if err != nil {
125
+		t.Errorf("Unexpected error %v from HandleImageRepo", err)
126
+	}
123 127
 	if buildCreator.build == nil {
124 128
 		t.Error("Expected new build when new image was created!")
125 129
 	}
... ...
@@ -138,11 +148,15 @@ func TestNonExistentImageRepository(t *testing.T) {
138 138
 	// this buildconfig references a non-existent imagerepo, so an update to the real imagerepo should not
139 139
 	// trigger a build here.
140 140
 	buildcfg := mockBuildConfig("registry.com/namespace/imagename", "registry.com/namespace/imagename", "testImageRepo", "testTag")
141
-	controller := mockImageChangeController(buildcfg, "otherImageRepo", "registry.com/namespace/imagename", map[string]string{"testTag": "newImageID123"})
142
-	controller.HandleImageRepo()
141
+	imagerepo := mockImageRepo("otherImageRepo", "registry.com/namespace/imagename", map[string]string{"testTag": "newImageID123"})
142
+	controller := mockImageChangeController(buildcfg)
143
+	err := controller.HandleImageRepo(imagerepo)
143 144
 	buildCreator := controller.BuildCreator.(*mockBuildCreator)
144 145
 	buildConfigUpdater := controller.BuildConfigUpdater.(*mockBuildConfigUpdater)
145 146
 
147
+	if err != nil {
148
+		t.Errorf("Unexpected error %v from HandleImageRepo", err)
149
+	}
146 150
 	if buildCreator.build != nil {
147 151
 		t.Error("New build created when a different repository was updated!")
148 152
 	}
... ...
@@ -154,11 +168,15 @@ func TestNonExistentImageRepository(t *testing.T) {
154 154
 func TestNewImageDifferentTagUpdate(t *testing.T) {
155 155
 	// this buildconfig references a different tag than the one that will be updated
156 156
 	buildcfg := mockBuildConfig("registry.com/namespace/imagename", "registry.com/namespace/imagename", "testImageRepo", "testTag")
157
-	controller := mockImageChangeController(buildcfg, "testImageRepo", "registry.com/namespace/imagename", map[string]string{"otherTag": "newImageID123"})
158
-	controller.HandleImageRepo()
157
+	imagerepo := mockImageRepo("testImageRepo", "registry.com/namespace/imagename", map[string]string{"otherTag": "newImageID123"})
158
+	controller := mockImageChangeController(buildcfg)
159
+	err := controller.HandleImageRepo(imagerepo)
159 160
 	buildCreator := controller.BuildCreator.(*mockBuildCreator)
160 161
 	buildConfigUpdater := controller.BuildConfigUpdater.(*mockBuildConfigUpdater)
161 162
 
163
+	if err != nil {
164
+		t.Errorf("Unexpected error %v from HandleImageRepo", err)
165
+	}
162 166
 	if buildCreator.build != nil {
163 167
 		t.Error("New build created when a different repository was updated!")
164 168
 	}
... ...
@@ -172,12 +190,15 @@ func TestNewImageDifferentTagUpdate2(t *testing.T) {
172 172
 	// it has previously run a build for the testTagID123 tag.
173 173
 	buildcfg := mockBuildConfig("registry.com/namespace/imagename", "registry.com/namespace/imagename", "testImageRepo", "testTag")
174 174
 	buildcfg.Triggers[0].ImageChange.LastTriggeredImageID = "testTagID123"
175
-	controller := mockImageChangeController(buildcfg, "testImageRepo", "registry.com/namespace/imagename",
176
-		map[string]string{"otherTag": "newImageID123", "testTag": "testTagID123"})
177
-	controller.HandleImageRepo()
175
+	imagerepo := mockImageRepo("testImageRepo", "registry.com/namespace/imagename", map[string]string{"otherTag": "newImageID123", "testTag": "testTagID123"})
176
+	controller := mockImageChangeController(buildcfg)
177
+	err := controller.HandleImageRepo(imagerepo)
178 178
 	buildCreator := controller.BuildCreator.(*mockBuildCreator)
179 179
 	buildConfigUpdater := controller.BuildConfigUpdater.(*mockBuildConfigUpdater)
180 180
 
181
+	if err != nil {
182
+		t.Errorf("Unexpected error %v from HandleImageRepo", err)
183
+	}
181 184
 	if buildCreator.build != nil {
182 185
 		t.Error("New build created when a different repository was updated!")
183 186
 	}
... ...
@@ -189,11 +210,15 @@ func TestNewImageDifferentTagUpdate2(t *testing.T) {
189 189
 func TestNewDifferentImageUpdate(t *testing.T) {
190 190
 	// this buildconfig references a different image than the one that will be updated
191 191
 	buildcfg := mockBuildConfig("registry.com/namespace/imagename1", "registry.com/namespace/imagename1", "testImageRepo1", "testTag1")
192
-	controller := mockImageChangeController(buildcfg, "testImageRepo2", "registry.com/namespace/imagename2", map[string]string{"testTag2": "newImageID123"})
193
-	controller.HandleImageRepo()
192
+	imagerepo := mockImageRepo("testImageRepo2", "registry.com/namespace/imagename2", map[string]string{"testTag2": "newImageID123"})
193
+	controller := mockImageChangeController(buildcfg)
194
+	err := controller.HandleImageRepo(imagerepo)
194 195
 	buildCreator := controller.BuildCreator.(*mockBuildCreator)
195 196
 	buildConfigUpdater := controller.BuildConfigUpdater.(*mockBuildConfigUpdater)
196 197
 
198
+	if err != nil {
199
+		t.Errorf("Unexpected error %v from HandleImageRepo", err)
200
+	}
197 201
 	if buildCreator.build != nil {
198 202
 		t.Error("New build created when a different repository was updated!")
199 203
 	}
... ...
@@ -206,11 +231,15 @@ func TestMultipleTriggers(t *testing.T) {
206 206
 	// this buildconfig references multiple images
207 207
 	buildcfg := mockBuildConfig("registry.com/namespace/imagename1", "registry.com/namespace/imagename1", "testImageRepo1", "testTag1")
208 208
 	appendTrigger(buildcfg, "registry.com/namespace/imagename2", "testImageRepo2", "testTag2")
209
-	controller := mockImageChangeController(buildcfg, "testImageRepo2", "registry.com/namespace/imagename2", map[string]string{"testTag2": "newImageID123"})
210
-	controller.HandleImageRepo()
209
+	imagerepo := mockImageRepo("testImageRepo2", "registry.com/namespace/imagename2", map[string]string{"testTag2": "newImageID123"})
210
+	controller := mockImageChangeController(buildcfg)
211
+	err := controller.HandleImageRepo(imagerepo)
211 212
 	buildCreator := controller.BuildCreator.(*mockBuildCreator)
212 213
 	buildConfigUpdater := controller.BuildConfigUpdater.(*mockBuildConfigUpdater)
213 214
 
215
+	if err != nil {
216
+		t.Errorf("Unexpected error %v from HandleImageRepo", err)
217
+	}
214 218
 	if buildCreator.build == nil {
215 219
 		t.Error("Expected new build when new image was created!")
216 220
 	}
... ...
@@ -230,11 +259,15 @@ func TestBuildConfigWithDifferentTriggerType(t *testing.T) {
230 230
 	// this buildconfig has different (than ImageChangeTrigger) trigger defined
231 231
 	buildcfg := mockBuildConfig("registry.com/namespace/imagename1", "", "", "")
232 232
 	buildcfg.Triggers[0].Type = buildapi.GenericWebHookBuildTriggerType
233
-	controller := mockImageChangeController(buildcfg, "testImageRepo2", "registry.com/namespace/imagename2", map[string]string{"testTag2": "newImageID123"})
234
-	controller.HandleImageRepo()
233
+	imagerepo := mockImageRepo("testImageRepo2", "registry.com/namespace/imagename2", map[string]string{"testTag2": "newImageID123"})
234
+	controller := mockImageChangeController(buildcfg)
235
+	err := controller.HandleImageRepo(imagerepo)
235 236
 	buildCreator := controller.BuildCreator.(*mockBuildCreator)
236 237
 	buildConfigUpdater := controller.BuildConfigUpdater.(*mockBuildConfigUpdater)
237 238
 
239
+	if err != nil {
240
+		t.Errorf("Unexpected error %v from HandleImageRepo", err)
241
+	}
238 242
 	if buildCreator.build != nil {
239 243
 		t.Error("New build created when a different trigger type was defined!")
240 244
 	}
... ...
@@ -248,11 +281,15 @@ func TestNoImageIDChange(t *testing.T) {
248 248
 	// startup when we're checking all the imageRepos
249 249
 	buildcfg := mockBuildConfig("registry.com/namespace/imagename", "registry.com/namespace/imagename", "testImageRepo", "testTag")
250 250
 	buildcfg.Triggers[0].ImageChange.LastTriggeredImageID = "imageID123"
251
-	controller := mockImageChangeController(buildcfg, "testImageRepo", "registry.com/namespace/imagename", map[string]string{"testTag": "imageID123"})
252
-	controller.HandleImageRepo()
251
+	imagerepo := mockImageRepo("testImageRepo", "registry.com/namespace/imagename", map[string]string{"testTag": "imageID123"})
252
+	controller := mockImageChangeController(buildcfg)
253
+	err := controller.HandleImageRepo(imagerepo)
253 254
 	buildCreator := controller.BuildCreator.(*mockBuildCreator)
254 255
 	buildConfigUpdater := controller.BuildConfigUpdater.(*mockBuildConfigUpdater)
255 256
 
257
+	if err != nil {
258
+		t.Errorf("Unexpected error %v from HandleImageRepo", err)
259
+	}
256 260
 	if buildCreator.build != nil {
257 261
 		t.Error("New build created when no change happened!")
258 262
 	}
... ...
@@ -264,12 +301,19 @@ func TestNoImageIDChange(t *testing.T) {
264 264
 func TestBuildCreateError(t *testing.T) {
265 265
 	// valid configuration, but build creation fails, in that situation the buildconfig should not be updated
266 266
 	buildcfg := mockBuildConfig("registry.com/namespace/imagename", "registry.com/namespace/imagename", "testImageRepo", "testTag")
267
-	controller := mockImageChangeController(buildcfg, "testImageRepo", "registry.com/namespace/imagename", map[string]string{"testTag": "newImageID123"})
267
+	imagerepo := mockImageRepo("testImageRepo", "registry.com/namespace/imagename", map[string]string{"testTag": "newImageID123"})
268
+	controller := mockImageChangeController(buildcfg)
268 269
 	buildCreator := controller.BuildCreator.(*mockBuildCreator)
269 270
 	buildCreator.err = fmt.Errorf("error")
270
-	controller.HandleImageRepo()
271
+	err := controller.HandleImageRepo(imagerepo)
271 272
 	buildConfigUpdater := controller.BuildConfigUpdater.(*mockBuildConfigUpdater)
272 273
 
274
+	if err == nil {
275
+		t.Error("Expected error from HandleImageRepo")
276
+	}
277
+	if _, ok := err.(ImageChangeControllerFatalError); ok {
278
+		t.Error("Expected retryable error from HandleImageRepo")
279
+	}
273 280
 	if buildCreator.build == nil {
274 281
 		t.Error("Expected new build when new image was created!")
275 282
 	}
... ...
@@ -281,14 +325,39 @@ func TestBuildCreateError(t *testing.T) {
281 281
 	}
282 282
 }
283 283
 
284
+func TestBuildUpdateError(t *testing.T) {
285
+	// valid configuration, but build creation fails, in that situation the buildconfig should not be updated
286
+	buildcfg := mockBuildConfig("registry.com/namespace/imagename", "registry.com/namespace/imagename", "testImageRepo", "testTag")
287
+	imagerepo := mockImageRepo("testImageRepo", "registry.com/namespace/imagename", map[string]string{"testTag": "newImageID123"})
288
+	controller := mockImageChangeController(buildcfg)
289
+	buildCreator := controller.BuildCreator.(*mockBuildCreator)
290
+	buildConfigUpdater := controller.BuildConfigUpdater.(*mockBuildConfigUpdater)
291
+	buildConfigUpdater.err = fmt.Errorf("error")
292
+	err := controller.HandleImageRepo(imagerepo)
293
+
294
+	if _, ok := err.(ImageChangeControllerFatalError); !ok {
295
+		t.Error("Expected fatal error from HandleImageRepo")
296
+	}
297
+	if buildCreator.build == nil {
298
+		t.Error("Expected new build when new image was created!")
299
+	}
300
+	if buildCreator.build.Parameters.Strategy.DockerStrategy.Image != "registry.com/namespace/imagename:newImageID123" {
301
+		t.Errorf("Image substitutions not properly setup for new build.  Expected %s, got %s |", "registry.com/namespace/imagename:newImageID123", buildCreator.build.Parameters.Strategy.DockerStrategy.Image)
302
+	}
303
+}
304
+
284 305
 func TestNewImageIDNoDockerRepo(t *testing.T) {
285 306
 	// No docker repository associated with the imagerepo, so no build can be created
286 307
 	buildcfg := mockBuildConfig("registry.com/namespace/imagename", "registry.com/namespace/imagename", "testImageRepo", "testTag")
287
-	controller := mockImageChangeController(buildcfg, "testImageRepo", "", map[string]string{"testTag": "newImageID123"})
288
-	controller.HandleImageRepo()
308
+	imagerepo := mockImageRepo("testImageRepo", "", map[string]string{"testTag": "newImageID123"})
309
+	controller := mockImageChangeController(buildcfg)
310
+	err := controller.HandleImageRepo(imagerepo)
289 311
 	buildCreator := controller.BuildCreator.(*mockBuildCreator)
290 312
 	buildConfigUpdater := controller.BuildConfigUpdater.(*mockBuildConfigUpdater)
291 313
 
314
+	if err != nil {
315
+		t.Errorf("Unexpected error %v from HandleImageRepo", err)
316
+	}
292 317
 	if buildCreator.build != nil {
293 318
 		t.Error("New build created when no change happened!")
294 319
 	}
... ...
@@ -596,6 +596,18 @@ func (c *MasterConfig) RunBuildController() {
596 596
 	controller.Run()
597 597
 }
598 598
 
599
+// RunBuildPodController starts the build/pod status sync loop for build status
600
+func (c *MasterConfig) RunBuildPodController() {
601
+	osclient, kclient := c.BuildControllerClients()
602
+	factory := buildcontrollerfactory.BuildPodControllerFactory{
603
+		OSClient:     osclient,
604
+		KubeClient:   kclient,
605
+		BuildUpdater: buildclient.NewOSClientBuildClient(osclient),
606
+	}
607
+	controller := factory.Create()
608
+	controller.Run()
609
+}
610
+
599 611
 // RunDeploymentController starts the build image change trigger controller process.
600 612
 func (c *MasterConfig) RunBuildImageChangeTriggerController() {
601 613
 	bcClient, _ := c.BuildControllerClients()
... ...
@@ -98,6 +98,7 @@ func (cfg Config) startMaster() error {
98 98
 
99 99
 	openshiftConfig.RunAssetServer()
100 100
 	openshiftConfig.RunBuildController()
101
+	openshiftConfig.RunBuildPodController()
101 102
 	openshiftConfig.RunBuildImageChangeTriggerController()
102 103
 	openshiftConfig.RunDeploymentController()
103 104
 	openshiftConfig.RunDeployerPodController()
... ...
@@ -203,7 +203,7 @@ func NewTestBuildOpenshift(t *testing.T) *testBuildOpenshift {
203 203
 			"github": github.New(),
204 204
 		})))
205 205
 
206
-	factory := buildcontrollerfactory.BuildControllerFactory{
206
+	bcFactory := buildcontrollerfactory.BuildControllerFactory{
207 207
 		OSClient:     osClient,
208 208
 		KubeClient:   kubeClient,
209 209
 		BuildUpdater: buildclient.NewOSClientBuildClient(osClient),
... ...
@@ -219,7 +219,16 @@ func NewTestBuildOpenshift(t *testing.T) *testBuildOpenshift {
219 219
 		Stop: openshift.stop,
220 220
 	}
221 221
 
222
-	factory.Create().Run()
222
+	bcFactory.Create().Run()
223
+
224
+	bpcFactory := buildcontrollerfactory.BuildPodControllerFactory{
225
+		OSClient:     osClient,
226
+		KubeClient:   kubeClient,
227
+		BuildUpdater: buildclient.NewOSClientBuildClient(osClient),
228
+		Stop:         openshift.stop,
229
+	}
230
+
231
+	bpcFactory.Create().Run()
223 232
 
224 233
 	return openshift
225 234
 }