Browse code

When creating build, lookup "to" field if specified

Also change builds to go into "Error" status when a problem occurs
and record a message. Slightly refactor the build controller code.
If the build pod already exists, assume that there is a race in build
execution and stay in "pending" state.

This means that clients that want to create ImageRepositories and
BuildConfigs at the same time that are using the integrated registry
only need to specify:

POST /imageRepositories {
"metadata": {
"name": "myimage",
},
}
POST /buildConfigs {
"parameters": {
"output": {
"to": {
"name": "myimage",
}
}
}
}

and the build will be configured with the right registry name automatically.
The integrated registry must be configured properly and OpenShift must be
configured to default to it via OPENSHIFT_DEFAULT_REGISTRY.

Clayton Coleman authored on 2015/01/20 14:00:41
Showing 4 changed files
... ...
@@ -16,6 +16,9 @@ type Build struct {
16 16
 	// Status is the current status of the build.
17 17
 	Status BuildStatus `json:"status,omitempty"`
18 18
 
19
+	// A human readable message indicating details about why the build has this status
20
+	Message string `json:"message,omitempty"`
21
+
19 22
 	// PodName is the name of the pod that is used to execute the build
20 23
 	PodName string `json:"podName,omitempty"`
21 24
 
... ...
@@ -16,6 +16,9 @@ type Build struct {
16 16
 	// Status is the current status of the build.
17 17
 	Status BuildStatus `json:"status,omitempty"`
18 18
 
19
+	// A human readable message indicating details about why the build has this status
20
+	Message string `json:"message,omitempty"`
21
+
19 22
 	// PodName is the name of the pod that is used to execute the build
20 23
 	PodName string `json:"podName,omitempty"`
21 24
 
... ...
@@ -11,6 +11,7 @@ import (
11 11
 	"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
12 12
 
13 13
 	buildapi "github.com/openshift/origin/pkg/build/api"
14
+	image "github.com/openshift/origin/pkg/image/api"
14 15
 )
15 16
 
16 17
 // BuildController watches build resources and manages their state
... ...
@@ -21,6 +22,8 @@ type BuildController struct {
21 21
 	BuildUpdater  buildUpdater
22 22
 	PodManager    podManager
23 23
 	BuildStrategy BuildStrategy
24
+
25
+	ImageRepositoryClient imageRepositoryClient
24 26
 }
25 27
 
26 28
 // BuildStrategy knows how to create a pod spec for a pod which can execute a build.
... ...
@@ -37,6 +40,10 @@ type podManager interface {
37 37
 	DeletePod(namespace string, pod *kapi.Pod) error
38 38
 }
39 39
 
40
+type imageRepositoryClient interface {
41
+	GetImageRepository(namespace, name string) (*image.ImageRepository, error)
42
+}
43
+
40 44
 // Run begins watching and syncing build jobs onto the cluster.
41 45
 func (bc *BuildController) Run() {
42 46
 	go util.Forever(func() { bc.HandleBuild(bc.NextBuild()) }, 0)
... ...
@@ -51,39 +58,77 @@ func (bc *BuildController) HandleBuild(build *buildapi.Build) {
51 51
 		return
52 52
 	}
53 53
 
54
-	nextStatus := buildapi.BuildStatusFailed
55
-	build.PodName = fmt.Sprintf("build-%s", build.Name)
54
+	if err := bc.nextBuildStatus(build); err != nil {
55
+		glog.V(4).Infof("Build failed with error %s/%s: %#v", build.Namespace, build.Name, err)
56
+		build.Status = buildapi.BuildStatusError
57
+		build.Message = err.Error()
58
+	}
56 59
 
60
+	if _, err := bc.BuildUpdater.UpdateBuild(build.Namespace, build); err != nil {
61
+		glog.V(2).Infof("Failed to record changes to build %s/%s: %#v", build.Namespace, build.Name, err)
62
+	}
63
+}
64
+
65
+// nextBuildStatus updates build with any appropriate changes, or returns an error if
66
+// the change cannot occur. When returning nil, be sure to set build.Status and optionally
67
+// build.Message.
68
+func (bc *BuildController) nextBuildStatus(build *buildapi.Build) error {
57 69
 	// If a cancelling event was triggered for the build, update build status.
58 70
 	if build.Cancelled {
59
-		glog.V(2).Infof("Cancelling build %s.", build.Name)
60
-		nextStatus = buildapi.BuildStatusCancelled
61
-
62
-	} else {
63
-
64
-		var podSpec *kapi.Pod
65
-		var err error
66
-		if podSpec, err = bc.BuildStrategy.CreateBuildPod(build); err != nil {
67
-			glog.V(2).Infof("Strategy failed to create build pod definition: %v", err)
68
-			nextStatus = buildapi.BuildStatusFailed
69
-		} else {
70
-
71
-			if _, err := bc.PodManager.CreatePod(build.Namespace, podSpec); err != nil {
72
-				if !errors.IsAlreadyExists(err) {
73
-					glog.V(2).Infof("Failed to create pod for build %s: %#v", build.Name, err)
74
-					nextStatus = buildapi.BuildStatusFailed
75
-				}
76
-			} else {
77
-				glog.V(2).Infof("Created build pod: %#v", podSpec)
78
-				nextStatus = buildapi.BuildStatusPending
71
+		glog.V(4).Infof("Cancelling build %s.", build.Name)
72
+		build.Status = buildapi.BuildStatusCancelled
73
+		return nil
74
+	}
75
+
76
+	// lookup the destination from the referenced image repository
77
+	spec := build.Parameters.Output.DockerImageReference
78
+	if ref := build.Parameters.Output.To; ref != nil {
79
+		// TODO: security, ensure that the reference image stream is actually visible
80
+		namespace := ref.Namespace
81
+		if len(namespace) == 0 {
82
+			namespace = build.Namespace
83
+		}
84
+
85
+		repo, err := bc.ImageRepositoryClient.GetImageRepository(namespace, ref.Name)
86
+		if err != nil {
87
+			if errors.IsNotFound(err) {
88
+				build.Status = buildapi.BuildStatusFailed
89
+				build.Message = fmt.Sprintf("The referenced output image repository %s/%s does not exist", namespace, ref.Name)
90
+				return nil
79 91
 			}
92
+			return fmt.Errorf("the referenced output repo %s/%s could not be found by %s/%s: %v", namespace, ref.Name, build.Namespace, build.Name, err)
80 93
 		}
94
+		spec = repo.Status.DockerImageRepository
81 95
 	}
82 96
 
83
-	build.Status = nextStatus
84
-	if _, err := bc.BuildUpdater.UpdateBuild(build.Namespace, build); err != nil {
85
-		glog.V(2).Infof("Failed to update build %s: %#v", build.Name, err)
97
+	// set the expected build parameters, which will be saved if no error occurs
98
+	build.Status = buildapi.BuildStatusPending
99
+	build.PodName = fmt.Sprintf("build-%s", build.Name)
100
+
101
+	// override DockerImageReference in the strategy for the copy we send to the server
102
+	copy, err := kapi.Scheme.Copy(build)
103
+	if err != nil {
104
+		return fmt.Errorf("unable to copy build: %v", err)
105
+	}
106
+	buildCopy := copy.(*buildapi.Build)
107
+	buildCopy.Parameters.Output.DockerImageReference = spec
108
+
109
+	// invoke the strategy to get a build pod
110
+	podSpec, err := bc.BuildStrategy.CreateBuildPod(buildCopy)
111
+	if err != nil {
112
+		return fmt.Errorf("the strategy failed to create a build pod for %s/%s: %v", build.Namespace, build.Name, err)
113
+	}
114
+
115
+	if _, err := bc.PodManager.CreatePod(build.Namespace, podSpec); err != nil {
116
+		if errors.IsAlreadyExists(err) {
117
+			glog.V(4).Infof("Build pod already existed: %#v", podSpec)
118
+			return nil
119
+		}
120
+		return fmt.Errorf("failed to create pod for build %s/%s: s", build.Namespace, build.Name, err)
86 121
 	}
122
+
123
+	glog.V(4).Infof("Created pod for build: %#v", podSpec)
124
+	return nil
87 125
 }
88 126
 
89 127
 func (bc *BuildController) HandlePod(pod *kapi.Pod) {
... ...
@@ -9,6 +9,7 @@ import (
9 9
 
10 10
 	buildapi "github.com/openshift/origin/pkg/build/api"
11 11
 	buildtest "github.com/openshift/origin/pkg/build/controller/test"
12
+	imageapi "github.com/openshift/origin/pkg/image/api"
12 13
 )
13 14
 
14 15
 type okBuildUpdater struct{}
... ...
@@ -23,9 +24,12 @@ func (ebu *errBuildUpdater) UpdateBuild(namespace string, build *buildapi.Build)
23 23
 	return &buildapi.Build{}, errors.New("UpdateBuild error!")
24 24
 }
25 25
 
26
-type okStrategy struct{}
26
+type okStrategy struct {
27
+	build *buildapi.Build
28
+}
27 29
 
28 30
 func (os *okStrategy) CreateBuildPod(build *buildapi.Build) (*kapi.Pod, error) {
31
+	os.build = build
29 32
 	return &kapi.Pod{}, nil
30 33
 }
31 34
 
... ...
@@ -65,10 +69,34 @@ func (_ *errExistsPodManager) DeletePod(namespace string, pod *kapi.Pod) error {
65 65
 	return kerrors.NewNotFound("kind", "name")
66 66
 }
67 67
 
68
-func mockBuildAndController(status buildapi.BuildStatus) (build *buildapi.Build, controller *BuildController) {
68
+type okImageRepositoryClient struct{}
69
+
70
+func (_ *okImageRepositoryClient) GetImageRepository(namespace, name string) (*imageapi.ImageRepository, error) {
71
+	return &imageapi.ImageRepository{
72
+		ObjectMeta: kapi.ObjectMeta{Name: name, Namespace: namespace},
73
+		Status: imageapi.ImageRepositoryStatus{
74
+			DockerImageRepository: "image/repo",
75
+		},
76
+	}, nil
77
+}
78
+
79
+type errImageRepositoryClient struct{}
80
+
81
+func (_ *errImageRepositoryClient) GetImageRepository(namespace, name string) (*imageapi.ImageRepository, error) {
82
+	return nil, errors.New("GetImageRepository error!")
83
+}
84
+
85
+type errNotFoundImageRepositoryClient struct{}
86
+
87
+func (_ *errNotFoundImageRepositoryClient) GetImageRepository(namespace, name string) (*imageapi.ImageRepository, error) {
88
+	return nil, kerrors.NewNotFound("ImageRepository", name)
89
+}
90
+
91
+func mockBuildAndController(status buildapi.BuildStatus, output buildapi.BuildOutput) (build *buildapi.Build, controller *BuildController) {
69 92
 	build = &buildapi.Build{
70 93
 		ObjectMeta: kapi.ObjectMeta{
71
-			Name: "dataBuild",
94
+			Name:      "data-build",
95
+			Namespace: "namespace",
72 96
 			Labels: map[string]string{
73 97
 				"name": "dataBuild",
74 98
 			},
... ...
@@ -86,20 +114,19 @@ func mockBuildAndController(status buildapi.BuildStatus) (build *buildapi.Build,
86 86
 					ContextDir: "contextimage",
87 87
 				},
88 88
 			},
89
-			Output: buildapi.BuildOutput{
90
-				DockerImageReference: "repository/dataBuild",
91
-			},
89
+			Output: output,
92 90
 		},
93 91
 		Status:  status,
94 92
 		PodName: "-the-pod-id",
95 93
 	}
96 94
 	controller = &BuildController{
97
-		BuildStore:    buildtest.NewFakeBuildStore(build),
98
-		BuildUpdater:  &okBuildUpdater{},
99
-		PodManager:    &okPodManager{},
100
-		NextBuild:     func() *buildapi.Build { return nil },
101
-		NextPod:       func() *kapi.Pod { return nil },
102
-		BuildStrategy: &okStrategy{},
95
+		BuildStore:            buildtest.NewFakeBuildStore(build),
96
+		BuildUpdater:          &okBuildUpdater{},
97
+		PodManager:            &okPodManager{},
98
+		NextBuild:             func() *buildapi.Build { return nil },
99
+		NextPod:               func() *kapi.Pod { return nil },
100
+		BuildStrategy:         &okStrategy{},
101
+		ImageRepositoryClient: &okImageRepositoryClient{},
103 102
 	}
104 103
 	return
105 104
 }
... ...
@@ -124,60 +151,134 @@ func TestHandleBuild(t *testing.T) {
124 124
 	type handleBuildTest struct {
125 125
 		inStatus      buildapi.BuildStatus
126 126
 		outStatus     buildapi.BuildStatus
127
+		buildOutput   buildapi.BuildOutput
127 128
 		buildStrategy BuildStrategy
128 129
 		buildUpdater  buildUpdater
130
+		imageClient   imageRepositoryClient
129 131
 		podManager    podManager
132
+		outputSpec    string
130 133
 	}
131 134
 
132 135
 	tests := []handleBuildTest{
133 136
 		{ // 0
134 137
 			inStatus:  buildapi.BuildStatusNew,
135 138
 			outStatus: buildapi.BuildStatusPending,
139
+			buildOutput: buildapi.BuildOutput{
140
+				DockerImageReference: "repository/dataBuild",
141
+			},
136 142
 		},
137 143
 		{ // 1
138 144
 			inStatus:  buildapi.BuildStatusPending,
139 145
 			outStatus: buildapi.BuildStatusPending,
146
+			buildOutput: buildapi.BuildOutput{
147
+				DockerImageReference: "repository/dataBuild",
148
+			},
140 149
 		},
141 150
 		{ // 2
142 151
 			inStatus:  buildapi.BuildStatusRunning,
143 152
 			outStatus: buildapi.BuildStatusRunning,
153
+			buildOutput: buildapi.BuildOutput{
154
+				DockerImageReference: "repository/dataBuild",
155
+			},
144 156
 		},
145 157
 		{ // 3
146 158
 			inStatus:  buildapi.BuildStatusComplete,
147 159
 			outStatus: buildapi.BuildStatusComplete,
160
+			buildOutput: buildapi.BuildOutput{
161
+				DockerImageReference: "repository/dataBuild",
162
+			},
148 163
 		},
149 164
 		{ // 4
150 165
 			inStatus:  buildapi.BuildStatusFailed,
151 166
 			outStatus: buildapi.BuildStatusFailed,
167
+			buildOutput: buildapi.BuildOutput{
168
+				DockerImageReference: "repository/dataBuild",
169
+			},
152 170
 		},
153 171
 		{ // 5
154 172
 			inStatus:  buildapi.BuildStatusError,
155 173
 			outStatus: buildapi.BuildStatusError,
174
+			buildOutput: buildapi.BuildOutput{
175
+				DockerImageReference: "repository/dataBuild",
176
+			},
156 177
 		},
157 178
 		{ // 6
158 179
 			inStatus:      buildapi.BuildStatusNew,
159
-			outStatus:     buildapi.BuildStatusFailed,
180
+			outStatus:     buildapi.BuildStatusError,
160 181
 			buildStrategy: &errStrategy{},
182
+			buildOutput: buildapi.BuildOutput{
183
+				DockerImageReference: "repository/dataBuild",
184
+			},
161 185
 		},
162 186
 		{ // 7
163 187
 			inStatus:   buildapi.BuildStatusNew,
164
-			outStatus:  buildapi.BuildStatusFailed,
188
+			outStatus:  buildapi.BuildStatusError,
165 189
 			podManager: &errPodManager{},
190
+			buildOutput: buildapi.BuildOutput{
191
+				DockerImageReference: "repository/dataBuild",
192
+			},
166 193
 		},
167 194
 		{ // 8
168 195
 			inStatus:   buildapi.BuildStatusNew,
169
-			outStatus:  buildapi.BuildStatusFailed,
196
+			outStatus:  buildapi.BuildStatusPending,
170 197
 			podManager: &errExistsPodManager{},
198
+			buildOutput: buildapi.BuildOutput{
199
+				DockerImageReference: "repository/dataBuild",
200
+			},
171 201
 		},
172 202
 		{ // 9
173 203
 			inStatus:     buildapi.BuildStatusNew,
174 204
 			outStatus:    buildapi.BuildStatusPending,
175 205
 			buildUpdater: &errBuildUpdater{},
206
+			buildOutput: buildapi.BuildOutput{
207
+				DockerImageReference: "repository/dataBuild",
208
+			},
209
+		},
210
+		{ // 10
211
+			inStatus:  buildapi.BuildStatusNew,
212
+			outStatus: buildapi.BuildStatusPending,
213
+			buildOutput: buildapi.BuildOutput{
214
+				To: &kapi.ObjectReference{
215
+					Name: "foo",
216
+				},
217
+			},
218
+			outputSpec: "image/repo",
219
+		},
220
+		{ // 11
221
+			inStatus:  buildapi.BuildStatusNew,
222
+			outStatus: buildapi.BuildStatusPending,
223
+			buildOutput: buildapi.BuildOutput{
224
+				To: &kapi.ObjectReference{
225
+					Name:      "foo",
226
+					Namespace: "bar",
227
+				},
228
+			},
229
+			outputSpec: "image/repo",
230
+		},
231
+		{ // 12
232
+			inStatus:    buildapi.BuildStatusNew,
233
+			outStatus:   buildapi.BuildStatusFailed,
234
+			imageClient: &errNotFoundImageRepositoryClient{},
235
+			buildOutput: buildapi.BuildOutput{
236
+				To: &kapi.ObjectReference{
237
+					Name: "foo",
238
+				},
239
+			},
240
+		},
241
+		{ // 13
242
+			inStatus:    buildapi.BuildStatusNew,
243
+			outStatus:   buildapi.BuildStatusError,
244
+			imageClient: &errImageRepositoryClient{},
245
+			buildOutput: buildapi.BuildOutput{
246
+				To: &kapi.ObjectReference{
247
+					Name: "foo",
248
+				},
249
+			},
176 250
 		},
177 251
 	}
178 252
 
179 253
 	for i, tc := range tests {
180
-		build, ctrl := mockBuildAndController(tc.inStatus)
254
+		build, ctrl := mockBuildAndController(tc.inStatus, tc.buildOutput)
181 255
 		if tc.buildStrategy != nil {
182 256
 			ctrl.BuildStrategy = tc.buildStrategy
183 257
 		}
... ...
@@ -187,12 +288,24 @@ func TestHandleBuild(t *testing.T) {
187 187
 		if tc.podManager != nil {
188 188
 			ctrl.PodManager = tc.podManager
189 189
 		}
190
+		if tc.imageClient != nil {
191
+			ctrl.ImageRepositoryClient = tc.imageClient
192
+		}
190 193
 
191 194
 		ctrl.HandleBuild(build)
192 195
 
193 196
 		if build.Status != tc.outStatus {
194 197
 			t.Errorf("(%d) Expected %s, got %s!", i, tc.outStatus, build.Status)
195 198
 		}
199
+		if tc.inStatus != buildapi.BuildStatusError && build.Status == buildapi.BuildStatusError && len(build.Message) == 0 {
200
+			t.Errorf("(%d) errored build should set message: %#v", i, build)
201
+		}
202
+		if len(tc.outputSpec) != 0 {
203
+			build := ctrl.BuildStrategy.(*okStrategy).build
204
+			if build.Parameters.Output.DockerImageReference != tc.outputSpec {
205
+				t.Errorf("(%d) expected build sent to strategy to have docker spec %q: %#v", i, tc.outputSpec, build)
206
+			}
207
+		}
196 208
 	}
197 209
 }
198 210
 
... ...
@@ -253,7 +366,7 @@ func TestHandlePod(t *testing.T) {
253 253
 	}
254 254
 
255 255
 	for i, tc := range tests {
256
-		build, ctrl := mockBuildAndController(tc.inStatus)
256
+		build, ctrl := mockBuildAndController(tc.inStatus, buildapi.BuildOutput{})
257 257
 		pod := mockPod(tc.podStatus, tc.exitCode)
258 258
 		if tc.matchID {
259 259
 			build.PodName = pod.Name
... ...
@@ -311,7 +424,7 @@ func TestCancelBuild(t *testing.T) {
311 311
 	}
312 312
 
313 313
 	for i, tc := range tests {
314
-		build, ctrl := mockBuildAndController(tc.inStatus)
314
+		build, ctrl := mockBuildAndController(tc.inStatus, buildapi.BuildOutput{})
315 315
 		pod := mockPod(tc.podStatus, tc.exitCode)
316 316
 
317 317
 		ctrl.CancelBuild(build, pod)