Browse code

Allow additional image stream build triggers on BuildConfig

Cesar Wong authored on 2015/07/15 00:10:43
Showing 22 changed files
... ...
@@ -12959,6 +12959,43 @@
12959 12959
      "lastTriggeredImageID": {
12960 12960
       "type": "string",
12961 12961
       "description": "used internally to save last used image ID for build"
12962
+     },
12963
+     "from": {
12964
+      "$ref": "v1.ObjectReference",
12965
+      "description": "reference to an ImageStreamTag that will trigger the build"
12966
+     }
12967
+    }
12968
+   },
12969
+   "v1.ObjectReference": {
12970
+    "id": "v1.ObjectReference",
12971
+    "properties": {
12972
+     "kind": {
12973
+      "type": "string",
12974
+      "description": "kind of the referent; see http://releases.k8s.io/v1.0.0/docs/api-conventions.md#types-kinds"
12975
+     },
12976
+     "namespace": {
12977
+      "type": "string",
12978
+      "description": "namespace of the referent; see http://releases.k8s.io/v1.0.0/docs/namespaces.md"
12979
+     },
12980
+     "name": {
12981
+      "type": "string",
12982
+      "description": "name of the referent; see http://releases.k8s.io/v1.0.0/docs/identifiers.md#names"
12983
+     },
12984
+     "uid": {
12985
+      "type": "string",
12986
+      "description": "uid of the referent; see http://releases.k8s.io/v1.0.0/docs/identifiers.md#uids"
12987
+     },
12988
+     "apiVersion": {
12989
+      "type": "string",
12990
+      "description": "API version of the referent"
12991
+     },
12992
+     "resourceVersion": {
12993
+      "type": "string",
12994
+      "description": "specific resourceVersion to which this reference is made, if any: http://releases.k8s.io/v1.0.0/docs/api-conventions.md#concurrency-control-and-consistency"
12995
+     },
12996
+     "fieldPath": {
12997
+      "type": "string",
12998
+      "description": "if referring to a piece of an object instead of an entire object, this string should contain a valid JSON/Go field access statement, such as desiredState.manifest.containers[2]"
12962 12999
      }
12963 13000
     }
12964 13001
    },
... ...
@@ -13121,39 +13158,6 @@
13121 13121
      }
13122 13122
     }
13123 13123
    },
13124
-   "v1.ObjectReference": {
13125
-    "id": "v1.ObjectReference",
13126
-    "properties": {
13127
-     "kind": {
13128
-      "type": "string",
13129
-      "description": "kind of the referent; see http://releases.k8s.io/v1.0.0/docs/api-conventions.md#types-kinds"
13130
-     },
13131
-     "namespace": {
13132
-      "type": "string",
13133
-      "description": "namespace of the referent; see http://releases.k8s.io/v1.0.0/docs/namespaces.md"
13134
-     },
13135
-     "name": {
13136
-      "type": "string",
13137
-      "description": "name of the referent; see http://releases.k8s.io/v1.0.0/docs/identifiers.md#names"
13138
-     },
13139
-     "uid": {
13140
-      "type": "string",
13141
-      "description": "uid of the referent; see http://releases.k8s.io/v1.0.0/docs/identifiers.md#uids"
13142
-     },
13143
-     "apiVersion": {
13144
-      "type": "string",
13145
-      "description": "API version of the referent"
13146
-     },
13147
-     "resourceVersion": {
13148
-      "type": "string",
13149
-      "description": "specific resourceVersion to which this reference is made, if any: http://releases.k8s.io/v1.0.0/docs/api-conventions.md#concurrency-control-and-consistency"
13150
-     },
13151
-     "fieldPath": {
13152
-      "type": "string",
13153
-      "description": "if referring to a piece of an object instead of an entire object, this string should contain a valid JSON/Go field access statement, such as desiredState.manifest.containers[2]"
13154
-     }
13155
-    }
13156
-   },
13157 13124
    "v1.EnvVar": {
13158 13125
     "id": "v1.EnvVar",
13159 13126
     "required": [
... ...
@@ -13446,6 +13450,10 @@
13446 13446
      "triggeredByImage": {
13447 13447
       "$ref": "v1.ObjectReference",
13448 13448
       "description": "image that triggered this build"
13449
+     },
13450
+     "from": {
13451
+      "$ref": "v1.ObjectReference",
13452
+      "description": "ImageStreamTag that triggered this build"
13449 13453
      }
13450 13454
     }
13451 13455
    },
... ...
@@ -822,6 +822,15 @@ func deepCopy_api_BuildRequest(in buildapi.BuildRequest, out *buildapi.BuildRequ
822 822
 	} else {
823 823
 		out.TriggeredByImage = nil
824 824
 	}
825
+	if in.From != nil {
826
+		if newVal, err := c.DeepCopy(in.From); err != nil {
827
+			return err
828
+		} else {
829
+			out.From = newVal.(*api.ObjectReference)
830
+		}
831
+	} else {
832
+		out.From = nil
833
+	}
825 834
 	return nil
826 835
 }
827 836
 
... ...
@@ -1058,6 +1067,15 @@ func deepCopy_api_GitSourceRevision(in buildapi.GitSourceRevision, out *buildapi
1058 1058
 
1059 1059
 func deepCopy_api_ImageChangeTrigger(in buildapi.ImageChangeTrigger, out *buildapi.ImageChangeTrigger, c *conversion.Cloner) error {
1060 1060
 	out.LastTriggeredImageID = in.LastTriggeredImageID
1061
+	if in.From != nil {
1062
+		if newVal, err := c.DeepCopy(in.From); err != nil {
1063
+			return err
1064
+		} else {
1065
+			out.From = newVal.(*api.ObjectReference)
1066
+		}
1067
+	} else {
1068
+		out.From = nil
1069
+	}
1061 1070
 	return nil
1062 1071
 }
1063 1072
 
... ...
@@ -1034,6 +1034,14 @@ func convert_api_BuildRequest_To_v1_BuildRequest(in *buildapi.BuildRequest, out
1034 1034
 	} else {
1035 1035
 		out.TriggeredByImage = nil
1036 1036
 	}
1037
+	if in.From != nil {
1038
+		out.From = new(v1.ObjectReference)
1039
+		if err := convert_api_ObjectReference_To_v1_ObjectReference(in.From, out.From, s); err != nil {
1040
+			return err
1041
+		}
1042
+	} else {
1043
+		out.From = nil
1044
+	}
1037 1045
 	return nil
1038 1046
 }
1039 1047
 
... ...
@@ -1183,6 +1191,14 @@ func convert_api_ImageChangeTrigger_To_v1_ImageChangeTrigger(in *buildapi.ImageC
1183 1183
 		defaulting.(func(*buildapi.ImageChangeTrigger))(in)
1184 1184
 	}
1185 1185
 	out.LastTriggeredImageID = in.LastTriggeredImageID
1186
+	if in.From != nil {
1187
+		out.From = new(v1.ObjectReference)
1188
+		if err := convert_api_ObjectReference_To_v1_ObjectReference(in.From, out.From, s); err != nil {
1189
+			return err
1190
+		}
1191
+	} else {
1192
+		out.From = nil
1193
+	}
1186 1194
 	return nil
1187 1195
 }
1188 1196
 
... ...
@@ -1382,6 +1398,14 @@ func convert_v1_BuildRequest_To_api_BuildRequest(in *buildapiv1.BuildRequest, ou
1382 1382
 	} else {
1383 1383
 		out.TriggeredByImage = nil
1384 1384
 	}
1385
+	if in.From != nil {
1386
+		out.From = new(api.ObjectReference)
1387
+		if err := convert_v1_ObjectReference_To_api_ObjectReference(in.From, out.From, s); err != nil {
1388
+			return err
1389
+		}
1390
+	} else {
1391
+		out.From = nil
1392
+	}
1385 1393
 	return nil
1386 1394
 }
1387 1395
 
... ...
@@ -1531,6 +1555,14 @@ func convert_v1_ImageChangeTrigger_To_api_ImageChangeTrigger(in *buildapiv1.Imag
1531 1531
 		defaulting.(func(*buildapiv1.ImageChangeTrigger))(in)
1532 1532
 	}
1533 1533
 	out.LastTriggeredImageID = in.LastTriggeredImageID
1534
+	if in.From != nil {
1535
+		out.From = new(api.ObjectReference)
1536
+		if err := convert_v1_ObjectReference_To_api_ObjectReference(in.From, out.From, s); err != nil {
1537
+			return err
1538
+		}
1539
+	} else {
1540
+		out.From = nil
1541
+	}
1534 1542
 	return nil
1535 1543
 }
1536 1544
 
... ...
@@ -803,6 +803,15 @@ func deepCopy_v1_BuildRequest(in buildapiv1.BuildRequest, out *buildapiv1.BuildR
803 803
 	} else {
804 804
 		out.TriggeredByImage = nil
805 805
 	}
806
+	if in.From != nil {
807
+		if newVal, err := c.DeepCopy(in.From); err != nil {
808
+			return err
809
+		} else {
810
+			out.From = newVal.(*v1.ObjectReference)
811
+		}
812
+	} else {
813
+		out.From = nil
814
+	}
806 815
 	return nil
807 816
 }
808 817
 
... ...
@@ -1039,6 +1048,15 @@ func deepCopy_v1_GitSourceRevision(in buildapiv1.GitSourceRevision, out *buildap
1039 1039
 
1040 1040
 func deepCopy_v1_ImageChangeTrigger(in buildapiv1.ImageChangeTrigger, out *buildapiv1.ImageChangeTrigger, c *conversion.Cloner) error {
1041 1041
 	out.LastTriggeredImageID = in.LastTriggeredImageID
1042
+	if in.From != nil {
1043
+		if newVal, err := c.DeepCopy(in.From); err != nil {
1044
+			return err
1045
+		} else {
1046
+			out.From = newVal.(*v1.ObjectReference)
1047
+		}
1048
+	} else {
1049
+		out.From = nil
1050
+	}
1042 1051
 	return nil
1043 1052
 }
1044 1053
 
... ...
@@ -1072,6 +1072,14 @@ func convert_api_BuildRequest_To_v1beta3_BuildRequest(in *buildapi.BuildRequest,
1072 1072
 	} else {
1073 1073
 		out.TriggeredByImage = nil
1074 1074
 	}
1075
+	if in.From != nil {
1076
+		out.From = new(v1beta3.ObjectReference)
1077
+		if err := convert_api_ObjectReference_To_v1beta3_ObjectReference(in.From, out.From, s); err != nil {
1078
+			return err
1079
+		}
1080
+	} else {
1081
+		out.From = nil
1082
+	}
1075 1083
 	return nil
1076 1084
 }
1077 1085
 
... ...
@@ -1221,6 +1229,14 @@ func convert_api_ImageChangeTrigger_To_v1beta3_ImageChangeTrigger(in *buildapi.I
1221 1221
 		defaulting.(func(*buildapi.ImageChangeTrigger))(in)
1222 1222
 	}
1223 1223
 	out.LastTriggeredImageID = in.LastTriggeredImageID
1224
+	if in.From != nil {
1225
+		out.From = new(v1beta3.ObjectReference)
1226
+		if err := convert_api_ObjectReference_To_v1beta3_ObjectReference(in.From, out.From, s); err != nil {
1227
+			return err
1228
+		}
1229
+	} else {
1230
+		out.From = nil
1231
+	}
1224 1232
 	return nil
1225 1233
 }
1226 1234
 
... ...
@@ -1420,6 +1436,14 @@ func convert_v1beta3_BuildRequest_To_api_BuildRequest(in *buildapiv1beta3.BuildR
1420 1420
 	} else {
1421 1421
 		out.TriggeredByImage = nil
1422 1422
 	}
1423
+	if in.From != nil {
1424
+		out.From = new(api.ObjectReference)
1425
+		if err := convert_v1beta3_ObjectReference_To_api_ObjectReference(in.From, out.From, s); err != nil {
1426
+			return err
1427
+		}
1428
+	} else {
1429
+		out.From = nil
1430
+	}
1423 1431
 	return nil
1424 1432
 }
1425 1433
 
... ...
@@ -1569,6 +1593,14 @@ func convert_v1beta3_ImageChangeTrigger_To_api_ImageChangeTrigger(in *buildapiv1
1569 1569
 		defaulting.(func(*buildapiv1beta3.ImageChangeTrigger))(in)
1570 1570
 	}
1571 1571
 	out.LastTriggeredImageID = in.LastTriggeredImageID
1572
+	if in.From != nil {
1573
+		out.From = new(api.ObjectReference)
1574
+		if err := convert_v1beta3_ObjectReference_To_api_ObjectReference(in.From, out.From, s); err != nil {
1575
+			return err
1576
+		}
1577
+	} else {
1578
+		out.From = nil
1579
+	}
1572 1580
 	return nil
1573 1581
 }
1574 1582
 
... ...
@@ -811,6 +811,15 @@ func deepCopy_v1beta3_BuildRequest(in buildapiv1beta3.BuildRequest, out *buildap
811 811
 	} else {
812 812
 		out.TriggeredByImage = nil
813 813
 	}
814
+	if in.From != nil {
815
+		if newVal, err := c.DeepCopy(in.From); err != nil {
816
+			return err
817
+		} else {
818
+			out.From = newVal.(*v1beta3.ObjectReference)
819
+		}
820
+	} else {
821
+		out.From = nil
822
+	}
814 823
 	return nil
815 824
 }
816 825
 
... ...
@@ -1047,6 +1056,15 @@ func deepCopy_v1beta3_GitSourceRevision(in buildapiv1beta3.GitSourceRevision, ou
1047 1047
 
1048 1048
 func deepCopy_v1beta3_ImageChangeTrigger(in buildapiv1beta3.ImageChangeTrigger, out *buildapiv1beta3.ImageChangeTrigger, c *conversion.Cloner) error {
1049 1049
 	out.LastTriggeredImageID = in.LastTriggeredImageID
1050
+	if in.From != nil {
1051
+		if newVal, err := c.DeepCopy(in.From); err != nil {
1052
+			return err
1053
+		} else {
1054
+			out.From = newVal.(*v1beta3.ObjectReference)
1055
+		}
1056
+	} else {
1057
+		out.From = nil
1058
+	}
1050 1059
 	return nil
1051 1060
 }
1052 1061
 
... ...
@@ -356,6 +356,12 @@ type ImageChangeTrigger struct {
356 356
 	// LastTriggeredImageID is used internally by the ImageChangeController to save last
357 357
 	// used image ID for build
358 358
 	LastTriggeredImageID string
359
+
360
+	// From is a reference to an ImageStreamTag that will trigger a build when updated
361
+	// It is optional. If no From is specified, the From image from the build strategy
362
+	// will be used. Only one ImageChangeTrigger with an empty From reference is allowed in
363
+	// a build configuration.
364
+	From *kapi.ObjectReference
359 365
 }
360 366
 
361 367
 // BuildTriggerPolicy describes a policy for a single trigger that results in a new Build.
... ...
@@ -453,6 +459,9 @@ type BuildRequest struct {
453 453
 
454 454
 	// TriggeredByImage is the Image that triggered this build.
455 455
 	TriggeredByImage *kapi.ObjectReference
456
+
457
+	// From is the reference to the ImageStreamTag that triggered the build.
458
+	From *kapi.ObjectReference
456 459
 }
457 460
 
458 461
 // BuildLogOptions is the REST options for a build log
... ...
@@ -341,6 +341,12 @@ type ImageChangeTrigger struct {
341 341
 	// LastTriggeredImageID is used internally by the ImageChangeController to save last
342 342
 	// used image ID for build
343 343
 	LastTriggeredImageID string `json:"lastTriggeredImageID,omitempty" description:"used internally to save last used image ID for build"`
344
+
345
+	// From is a reference to an ImageStreamTag that will trigger a build when updated
346
+	// It is optional. If no From is specified, the From image from the build strategy
347
+	// will be used. Only one ImageChangeTrigger with an empty From reference is allowed in
348
+	// a build configuration.
349
+	From *kapi.ObjectReference `json:"from,omitempty" description:"reference to an ImageStreamTag that will trigger the build"`
344 350
 }
345 351
 
346 352
 // BuildTriggerPolicy describes a policy for a single trigger that results in a new Build.
... ...
@@ -427,6 +433,9 @@ type BuildRequest struct {
427 427
 
428 428
 	// TriggeredByImage is the Image that triggered this build.
429 429
 	TriggeredByImage *kapi.ObjectReference `json:"triggeredByImage,omitempty" description:"image that triggered this build"`
430
+
431
+	// From is the reference to the ImageStreamTag that triggered the build.
432
+	From *kapi.ObjectReference `json:"from,omitempty" description:"ImageStreamTag that triggered this build"`
430 433
 }
431 434
 
432 435
 // BuildLogOptions is the REST options for a build log
... ...
@@ -330,6 +330,12 @@ type ImageChangeTrigger struct {
330 330
 	// LastTriggeredImageID is used internally by the ImageChangeController to save last
331 331
 	// used image ID for build
332 332
 	LastTriggeredImageID string `json:"lastTriggeredImageID,omitempty"`
333
+
334
+	// From is a reference to an ImageStreamTag that will trigger a build when updated
335
+	// It is optional. If no From is specified, the From image from the build strategy
336
+	// will be used. Only one ImageChangeTrigger with an empty From reference is allowed in
337
+	// a build configuration.
338
+	From *kapi.ObjectReference `json:"from,omitempty" description:"reference to an ImageStreamTag that will trigger the build"`
333 339
 }
334 340
 
335 341
 // BuildTriggerPolicy describes a policy for a single trigger that results in a new Build.
... ...
@@ -409,6 +415,9 @@ type BuildRequest struct {
409 409
 
410 410
 	// TriggeredByImage is the Image that triggered this build.
411 411
 	TriggeredByImage *kapi.ObjectReference `json:"triggeredByImage,omitempty"`
412
+
413
+	// From is the reference to the ImageStreamTag that triggered the build.
414
+	From *kapi.ObjectReference `json:"from,omitempty" description:"ImageStreamTag that triggered this build"`
412 415
 }
413 416
 
414 417
 // BuildLogOptions is the REST options for a build log
... ...
@@ -11,6 +11,7 @@ import (
11 11
 
12 12
 	oapi "github.com/openshift/origin/pkg/api"
13 13
 	buildapi "github.com/openshift/origin/pkg/build/api"
14
+	buildutil "github.com/openshift/origin/pkg/build/util"
14 15
 	imageapi "github.com/openshift/origin/pkg/image/api"
15 16
 )
16 17
 
... ...
@@ -36,22 +37,43 @@ func ValidateBuildUpdate(build *buildapi.Build, older *buildapi.Build) fielderro
36 36
 	return allErrs
37 37
 }
38 38
 
39
+// refKey returns a key for the given ObjectReference. If the ObjectReference
40
+// doesn't include a namespace, the passed in namespace is used for the reference
41
+func refKey(namespace string, ref *kapi.ObjectReference) string {
42
+	if ref == nil || ref.Kind != "ImageStreamTag" {
43
+		return "nil"
44
+	}
45
+	ns := ref.Namespace
46
+	if ns == "" {
47
+		ns = namespace
48
+	}
49
+	return fmt.Sprintf("%s/%s", ns, ref.Name)
50
+}
51
+
39 52
 // ValidateBuildConfig tests required fields for a Build.
40 53
 func ValidateBuildConfig(config *buildapi.BuildConfig) fielderrors.ValidationErrorList {
41 54
 	allErrs := fielderrors.ValidationErrorList{}
42 55
 	allErrs = append(allErrs, validation.ValidateObjectMeta(&config.ObjectMeta, true, validation.NameIsDNSSubdomain).Prefix("metadata")...)
43 56
 
44
-	// allow only one ImageChangeTrigger for now
45
-	ictCount := 0
57
+	// image change triggers that refer
58
+	fromRefs := map[string]struct{}{}
46 59
 	for i, trg := range config.Spec.Triggers {
47 60
 		allErrs = append(allErrs, validateTrigger(&trg).PrefixIndex(i).Prefix("triggers")...)
48
-		if trg.Type == buildapi.ImageChangeBuildTriggerType {
49
-			if ictCount++; ictCount > 1 {
50
-				allErrs = append(allErrs, fielderrors.NewFieldInvalid("triggers", config.Spec.Triggers, "only one ImageChange trigger is allowed"))
51
-				break
52
-			}
61
+		if trg.Type != buildapi.ImageChangeBuildTriggerType || trg.ImageChange == nil {
62
+			continue
63
+		}
64
+		from := trg.ImageChange.From
65
+		if from == nil {
66
+			from = buildutil.GetImageStreamForStrategy(config.Spec.Strategy)
53 67
 		}
68
+		fromKey := refKey(config.Namespace, from)
69
+		_, exists := fromRefs[fromKey]
70
+		if exists {
71
+			allErrs = append(allErrs, fielderrors.NewFieldInvalid("triggers", config.Spec.Triggers, "multiple ImageChange triggers refer to the same image stream tag"))
72
+		}
73
+		fromRefs[fromKey] = struct{}{}
54 74
 	}
75
+
55 76
 	allErrs = append(allErrs, validateBuildSpec(&config.Spec.BuildSpec).Prefix("spec")...)
56 77
 	return allErrs
57 78
 }
... ...
@@ -311,7 +333,20 @@ func validateTrigger(trigger *buildapi.BuildTriggerPolicy) fielderrors.Validatio
311 311
 	case buildapi.ImageChangeBuildTriggerType:
312 312
 		if trigger.ImageChange == nil {
313 313
 			allErrs = append(allErrs, fielderrors.NewFieldRequired("imageChange"))
314
+			break
315
+		}
316
+		if trigger.ImageChange.From == nil {
317
+			break
318
+		}
319
+		if kind := trigger.ImageChange.From.Kind; kind != "ImageStreamTag" {
320
+			invalidKindErr := fielderrors.NewFieldInvalid(
321
+				"imageChange.from.kind",
322
+				kind,
323
+				"only an ImageStreamTag type of reference is allowed in an ImageChange trigger.")
324
+			allErrs = append(allErrs, invalidKindErr)
325
+			break
314 326
 		}
327
+		allErrs = append(allErrs, validateFromImageReference(trigger.ImageChange.From).Prefix("from")...)
315 328
 	default:
316 329
 		allErrs = append(allErrs, fielderrors.NewFieldInvalid("type", trigger.Type, "invalid trigger type"))
317 330
 	}
... ...
@@ -1,7 +1,6 @@
1 1
 package validation
2 2
 
3 3
 import (
4
-	"strings"
5 4
 	"testing"
6 5
 
7 6
 	kapi "github.com/GoogleCloudPlatform/kubernetes/pkg/api"
... ...
@@ -168,6 +167,12 @@ func TestBuildConfigValidationSuccess(t *testing.T) {
168 168
 					},
169 169
 				},
170 170
 			},
171
+			Triggers: []buildapi.BuildTriggerPolicy{
172
+				{
173
+					Type:        buildapi.ImageChangeBuildTriggerType,
174
+					ImageChange: &buildapi.ImageChangeTrigger{},
175
+				},
176
+			},
171 177
 		},
172 178
 	}
173 179
 	if result := ValidateBuildConfig(buildConfig); len(result) > 0 {
... ...
@@ -213,54 +218,253 @@ func TestBuildConfigValidationFailureRequiredName(t *testing.T) {
213 213
 	}
214 214
 }
215 215
 
216
-func TestBuildConfigValidationFailureTooManyICT(t *testing.T) {
217
-	buildConfig := &buildapi.BuildConfig{
218
-		ObjectMeta: kapi.ObjectMeta{Name: "bar", Namespace: "foo"},
219
-		Spec: buildapi.BuildConfigSpec{
220
-			BuildSpec: buildapi.BuildSpec{
221
-				Source: buildapi.BuildSource{
222
-					Type: buildapi.BuildSourceGit,
223
-					Git: &buildapi.GitBuildSource{
224
-						URI: "http://github.com/my/repository",
216
+func TestBuildConfigImageChangeTriggers(t *testing.T) {
217
+	tests := []struct {
218
+		name        string
219
+		triggers    []buildapi.BuildTriggerPolicy
220
+		expectError bool
221
+		errorType   fielderrors.ValidationErrorType
222
+	}{
223
+		{
224
+			name: "valid default trigger",
225
+			triggers: []buildapi.BuildTriggerPolicy{
226
+				{
227
+					Type:        buildapi.ImageChangeBuildTriggerType,
228
+					ImageChange: &buildapi.ImageChangeTrigger{},
229
+				},
230
+			},
231
+			expectError: false,
232
+		},
233
+		{
234
+			name: "more than one default trigger",
235
+			triggers: []buildapi.BuildTriggerPolicy{
236
+				{
237
+					Type:        buildapi.ImageChangeBuildTriggerType,
238
+					ImageChange: &buildapi.ImageChangeTrigger{},
239
+				},
240
+				{
241
+					Type:        buildapi.ImageChangeBuildTriggerType,
242
+					ImageChange: &buildapi.ImageChangeTrigger{},
243
+				},
244
+			},
245
+			expectError: true,
246
+			errorType:   fielderrors.ValidationErrorTypeInvalid,
247
+		},
248
+		{
249
+			name: "missing image change struct",
250
+			triggers: []buildapi.BuildTriggerPolicy{
251
+				{
252
+					Type: buildapi.ImageChangeBuildTriggerType,
253
+				},
254
+			},
255
+			expectError: true,
256
+			errorType:   fielderrors.ValidationErrorTypeRequired,
257
+		},
258
+		{
259
+			name: "only one default image change trigger",
260
+			triggers: []buildapi.BuildTriggerPolicy{
261
+				{
262
+					Type:        buildapi.ImageChangeBuildTriggerType,
263
+					ImageChange: &buildapi.ImageChangeTrigger{},
264
+				},
265
+				{
266
+					Type: buildapi.ImageChangeBuildTriggerType,
267
+					ImageChange: &buildapi.ImageChangeTrigger{
268
+						From: &kapi.ObjectReference{
269
+							Kind: "ImageStreamTag",
270
+							Name: "myimage:tag",
271
+						},
225 272
 					},
226
-					ContextDir: "context",
227 273
 				},
228
-				Strategy: buildapi.BuildStrategy{
229
-					Type:           buildapi.DockerBuildStrategyType,
230
-					DockerStrategy: &buildapi.DockerBuildStrategy{},
274
+			},
275
+			expectError: false,
276
+		},
277
+		{
278
+			name: "invalid reference kind for trigger",
279
+			triggers: []buildapi.BuildTriggerPolicy{
280
+				{
281
+					Type:        buildapi.ImageChangeBuildTriggerType,
282
+					ImageChange: &buildapi.ImageChangeTrigger{},
231 283
 				},
232
-				Output: buildapi.BuildOutput{
233
-					To: &kapi.ObjectReference{
234
-						Kind: "DockerImage",
235
-						Name: "repository/data",
284
+				{
285
+					Type: buildapi.ImageChangeBuildTriggerType,
286
+					ImageChange: &buildapi.ImageChangeTrigger{
287
+						From: &kapi.ObjectReference{
288
+							Kind: "DockerImage",
289
+							Name: "myimage:tag",
290
+						},
236 291
 					},
237 292
 				},
238 293
 			},
239
-			Triggers: []buildapi.BuildTriggerPolicy{
294
+			expectError: true,
295
+			errorType:   fielderrors.ValidationErrorTypeInvalid,
296
+		},
297
+		{
298
+			name: "empty reference kind for trigger",
299
+			triggers: []buildapi.BuildTriggerPolicy{
240 300
 				{
241 301
 					Type:        buildapi.ImageChangeBuildTriggerType,
242 302
 					ImageChange: &buildapi.ImageChangeTrigger{},
243 303
 				},
244 304
 				{
305
+					Type: buildapi.ImageChangeBuildTriggerType,
306
+					ImageChange: &buildapi.ImageChangeTrigger{
307
+						From: &kapi.ObjectReference{
308
+							Name: "myimage:tag",
309
+						},
310
+					},
311
+				},
312
+			},
313
+			expectError: true,
314
+			errorType:   fielderrors.ValidationErrorTypeInvalid,
315
+		},
316
+		{
317
+			name: "duplicate imagestreamtag references",
318
+			triggers: []buildapi.BuildTriggerPolicy{
319
+				{
320
+					Type: buildapi.ImageChangeBuildTriggerType,
321
+					ImageChange: &buildapi.ImageChangeTrigger{
322
+						From: &kapi.ObjectReference{
323
+							Kind: "ImageStreamTag",
324
+							Name: "myimage:tag",
325
+						},
326
+					},
327
+				},
328
+				{
329
+					Type: buildapi.ImageChangeBuildTriggerType,
330
+					ImageChange: &buildapi.ImageChangeTrigger{
331
+						From: &kapi.ObjectReference{
332
+							Kind: "ImageStreamTag",
333
+							Name: "myimage:tag",
334
+						},
335
+					},
336
+				},
337
+			},
338
+			expectError: true,
339
+			errorType:   fielderrors.ValidationErrorTypeInvalid,
340
+		},
341
+		{
342
+			name: "duplicate imagestreamtag - same as strategy ref",
343
+			triggers: []buildapi.BuildTriggerPolicy{
344
+				{
245 345
 					Type:        buildapi.ImageChangeBuildTriggerType,
246 346
 					ImageChange: &buildapi.ImageChangeTrigger{},
247 347
 				},
348
+				{
349
+					Type: buildapi.ImageChangeBuildTriggerType,
350
+					ImageChange: &buildapi.ImageChangeTrigger{
351
+						From: &kapi.ObjectReference{
352
+							Kind: "ImageStreamTag",
353
+							Name: "builderimage:latest",
354
+						},
355
+					},
356
+				},
248 357
 			},
358
+			expectError: true,
359
+			errorType:   fielderrors.ValidationErrorTypeInvalid,
360
+		},
361
+		{
362
+			name: "imagestreamtag references with same name, different ns",
363
+			triggers: []buildapi.BuildTriggerPolicy{
364
+				{
365
+					Type: buildapi.ImageChangeBuildTriggerType,
366
+					ImageChange: &buildapi.ImageChangeTrigger{
367
+						From: &kapi.ObjectReference{
368
+							Kind:      "ImageStreamTag",
369
+							Name:      "myimage:tag",
370
+							Namespace: "ns1",
371
+						},
372
+					},
373
+				},
374
+				{
375
+					Type: buildapi.ImageChangeBuildTriggerType,
376
+					ImageChange: &buildapi.ImageChangeTrigger{
377
+						From: &kapi.ObjectReference{
378
+							Kind:      "ImageStreamTag",
379
+							Name:      "myimage:tag",
380
+							Namespace: "ns2",
381
+						},
382
+					},
383
+				},
384
+			},
385
+			expectError: false,
386
+		},
387
+		{
388
+			name: "imagestreamtag references with same name, same ns",
389
+			triggers: []buildapi.BuildTriggerPolicy{
390
+				{
391
+					Type: buildapi.ImageChangeBuildTriggerType,
392
+					ImageChange: &buildapi.ImageChangeTrigger{
393
+						From: &kapi.ObjectReference{
394
+							Kind:      "ImageStreamTag",
395
+							Name:      "myimage:tag",
396
+							Namespace: "ns",
397
+						},
398
+					},
399
+				},
400
+				{
401
+					Type: buildapi.ImageChangeBuildTriggerType,
402
+					ImageChange: &buildapi.ImageChangeTrigger{
403
+						From: &kapi.ObjectReference{
404
+							Kind:      "ImageStreamTag",
405
+							Name:      "myimage:tag",
406
+							Namespace: "ns",
407
+						},
408
+					},
409
+				},
410
+			},
411
+			expectError: true,
412
+			errorType:   fielderrors.ValidationErrorTypeInvalid,
249 413
 		},
250 414
 	}
251
-	errors := ValidateBuildConfig(buildConfig)
252
-	if len(errors) != 1 {
253
-		t.Fatalf("Unexpected validation errors %v", errors)
254
-	}
255
-	err := errors[0].(*fielderrors.ValidationError)
256
-	if err.Type != fielderrors.ValidationErrorTypeInvalid {
257
-		t.Errorf("Unexpected error type, expected %s, got %s", fielderrors.ValidationErrorTypeInvalid, err.Type)
258
-	}
259
-	if err.Field != "triggers" {
260
-		t.Errorf("Unexpected field name expected triggers, got %s", err.Field)
261
-	}
262
-	if !strings.Contains(err.Detail, "only one ImageChange trigger is allowed") {
263
-		t.Errorf("Unexpected error details: %s", err.Detail)
415
+
416
+	for _, tc := range tests {
417
+		buildConfig := &buildapi.BuildConfig{
418
+			ObjectMeta: kapi.ObjectMeta{Name: "bar", Namespace: "foo"},
419
+			Spec: buildapi.BuildConfigSpec{
420
+				BuildSpec: buildapi.BuildSpec{
421
+					Source: buildapi.BuildSource{
422
+						Type: buildapi.BuildSourceGit,
423
+						Git: &buildapi.GitBuildSource{
424
+							URI: "http://github.com/my/repository",
425
+						},
426
+						ContextDir: "context",
427
+					},
428
+					Strategy: buildapi.BuildStrategy{
429
+						Type: buildapi.SourceBuildStrategyType,
430
+						SourceStrategy: &buildapi.SourceBuildStrategy{
431
+							From: kapi.ObjectReference{
432
+								Kind: "ImageStreamTag",
433
+								Name: "builderimage:latest",
434
+							},
435
+						},
436
+					},
437
+					Output: buildapi.BuildOutput{
438
+						To: &kapi.ObjectReference{
439
+							Kind: "DockerImage",
440
+							Name: "repository/data",
441
+						},
442
+					},
443
+				},
444
+				Triggers: tc.triggers,
445
+			},
446
+		}
447
+		errors := ValidateBuildConfig(buildConfig)
448
+		// Check whether an error was returned
449
+		if hasError := len(errors) > 0; hasError != tc.expectError {
450
+			t.Errorf("%s: did not get expected result: %#v", tc.name, errors)
451
+		}
452
+		// Check whether it's the expected error type
453
+		if len(errors) > 0 && tc.expectError && tc.errorType != "" {
454
+			verr, ok := errors[0].(*fielderrors.ValidationError)
455
+			if !ok {
456
+				t.Errorf("%s: unexpected error: %#v. Expected ValidationError of type: %s", tc.name, errors[0], verr.Type)
457
+				continue
458
+			}
459
+			if verr.Type != tc.errorType {
460
+				t.Errorf("%s: unexpected error type. Expected: %s. Got: %s", tc.name, tc.errorType, verr.Type)
461
+			}
462
+		}
264 463
 	}
265 464
 }
266 465
 
... ...
@@ -59,21 +59,32 @@ func (c *ImageChangeController) HandleImageRepo(repo *imageapi.ImageStream) erro
59 59
 	for _, bc := range c.BuildConfigStore.List() {
60 60
 		config := bc.(*buildapi.BuildConfig)
61 61
 
62
-		from := buildutil.GetImageStreamForStrategy(config.Spec.Strategy)
63
-		if from == nil || from.Kind != "ImageStreamTag" {
64
-			continue
65
-		}
66
-
67
-		shouldBuild := false
68
-		triggeredImage := ""
69
-		// For every ImageChange trigger find the latest tagged image from the image repository and replace that value
70
-		// throughout the build strategies. A new build is triggered only if the latest tagged image id or pull spec
71
-		// differs from the last triggered build recorded on the build config.
62
+		var (
63
+			from           *kapi.ObjectReference
64
+			shouldBuild    = false
65
+			triggeredImage = ""
66
+		)
67
+		// For every ImageChange trigger find the latest tagged image from the image repository and
68
+		// invoke a build using that image id. A new build is triggered only if the latest tagged image id or pull spec
69
+		// differs from the last triggered build recorded on the build config for that trigger
72 70
 		for _, trigger := range config.Spec.Triggers {
73 71
 			if trigger.Type != buildapi.ImageChangeBuildTriggerType {
74 72
 				continue
75 73
 			}
76
-			fromStreamName := getImageStreamNameFromReference(from)
74
+			if trigger.ImageChange.From != nil {
75
+				from = trigger.ImageChange.From
76
+			} else {
77
+				from = buildutil.GetImageStreamForStrategy(config.Spec.Strategy)
78
+			}
79
+
80
+			if from == nil || from.Kind != "ImageStreamTag" {
81
+				continue
82
+			}
83
+			fromStreamName, tag, ok := imageapi.SplitImageStreamTag(from.Name)
84
+			if !ok {
85
+				glog.Errorf("Invalid image stream tag: %s in build config %s/%s", from.Name, config.Name, config.Namespace)
86
+				continue
87
+			}
77 88
 
78 89
 			fromNamespace := from.Namespace
79 90
 			if len(fromNamespace) == 0 {
... ...
@@ -89,7 +100,6 @@ func (c *ImageChangeController) HandleImageRepo(repo *imageapi.ImageStream) erro
89 89
 
90 90
 			// This split is safe because ImageStreamTag names always have the form
91 91
 			// name:tag.
92
-			tag := strings.Split(from.Name, ":")[1]
93 92
 			latest := imageapi.LatestTaggedImage(repo, tag)
94 93
 			if latest == nil {
95 94
 				glog.V(4).Infof("unable to find tagged image: no image recorded for %s/%s:%s", repo.Namespace, repo.Name, tag)
... ...
@@ -122,6 +132,7 @@ func (c *ImageChangeController) HandleImageRepo(repo *imageapi.ImageStream) erro
122 122
 					Kind: "DockerImage",
123 123
 					Name: triggeredImage,
124 124
 				},
125
+				From: from,
125 126
 			}
126 127
 			if _, err := c.BuildConfigInstantiator.Instantiate(config.Namespace, request); err != nil {
127 128
 				if kerrors.IsConflict(err) {
... ...
@@ -13,6 +13,7 @@ import (
13 13
 	"github.com/GoogleCloudPlatform/kubernetes/pkg/credentialprovider"
14 14
 	"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
15 15
 	buildapi "github.com/openshift/origin/pkg/build/api"
16
+	buildutil "github.com/openshift/origin/pkg/build/util"
16 17
 	"github.com/openshift/origin/pkg/cmd/server/bootstrappolicy"
17 18
 	imageapi "github.com/openshift/origin/pkg/image/api"
18 19
 )
... ...
@@ -98,7 +99,7 @@ func (g *BuildGenerator) FetchServiceAccountSecrets(namespace, serviceAccount st
98 98
 	var result []kapi.Secret
99 99
 	sa, err := g.ServiceAccounts.ServiceAccounts(namespace).Get(serviceAccount)
100 100
 	if err != nil {
101
-		return result, fmt.Errorf("Error getting push/pull secrets for service account %q: %v", namespace, serviceAccount, err)
101
+		return result, fmt.Errorf("Error getting push/pull secrets for service account %s/%s: %v", namespace, serviceAccount, err)
102 102
 	}
103 103
 	for _, ref := range sa.Secrets {
104 104
 		secret, err := g.Secrets.Secrets(namespace).Get(ref.Name)
... ...
@@ -110,25 +111,62 @@ func (g *BuildGenerator) FetchServiceAccountSecrets(namespace, serviceAccount st
110 110
 	return result, nil
111 111
 }
112 112
 
113
+// findImageChangeTrigger finds an image change trigger that has a from that matches the passed in ref
114
+// if no match is found but there is an image change trigger with a null from, that trigger is returned
115
+func findImageChangeTrigger(bc *buildapi.BuildConfig, ref *kapi.ObjectReference) *buildapi.ImageChangeTrigger {
116
+	if ref == nil {
117
+		return nil
118
+	}
119
+	for _, trigger := range bc.Spec.Triggers {
120
+		if trigger.Type != buildapi.ImageChangeBuildTriggerType {
121
+			continue
122
+		}
123
+		imageChange := trigger.ImageChange
124
+		triggerRef := imageChange.From
125
+		if triggerRef == nil {
126
+			triggerRef = buildutil.GetImageStreamForStrategy(bc.Spec.Strategy)
127
+			if triggerRef == nil || triggerRef.Kind != "ImageStreamTag" {
128
+				continue
129
+			}
130
+		}
131
+		triggerNs := triggerRef.Namespace
132
+		if triggerNs == "" {
133
+			triggerNs = bc.Namespace
134
+		}
135
+		refNs := ref.Namespace
136
+		if refNs == "" {
137
+			refNs = bc.Namespace
138
+		}
139
+		if triggerRef.Name == ref.Name && triggerNs == refNs {
140
+			return imageChange
141
+		}
142
+	}
143
+	return nil
144
+}
145
+
146
+func describeBuildRequest(request *buildapi.BuildRequest) string {
147
+	desc := fmt.Sprintf("BuildConfig: %s/%s", request.Namespace, request.Name)
148
+	if request.Revision != nil {
149
+		desc += fmt.Sprintf(", Revision: %#v", request.Revision.Git)
150
+	}
151
+	if request.TriggeredByImage != nil {
152
+		desc += fmt.Sprintf(", TriggeredBy: %s/%s with stream: %s/%s",
153
+			request.TriggeredByImage.Kind, request.TriggeredByImage.Name,
154
+			request.From.Kind, request.From.Name)
155
+	}
156
+	return desc
157
+}
158
+
113 159
 // Instantiate returns new Build object based on a BuildRequest object
114 160
 func (g *BuildGenerator) Instantiate(ctx kapi.Context, request *buildapi.BuildRequest) (*buildapi.Build, error) {
115
-	glog.V(4).Infof("Generating Build from BuildConfig %s/%s", request.Namespace, request.Name)
161
+	glog.V(4).Infof("Generating Build from %s", describeBuildRequest(request))
116 162
 	bc, err := g.Client.GetBuildConfig(ctx, request.Name)
117 163
 	if err != nil {
118 164
 		return nil, err
119 165
 	}
120 166
 
121
-	if request.TriggeredByImage != nil {
122
-		for _, trigger := range bc.Spec.Triggers {
123
-			if trigger.Type != buildapi.ImageChangeBuildTriggerType {
124
-				continue
125
-			}
126
-			if trigger.ImageChange.LastTriggeredImageID == request.TriggeredByImage.Name {
127
-				glog.V(2).Infof("Aborting imageid triggered build for BuildConfig %s/%s with imageid %s because the BuildConfig already matches this imageid", bc.Namespace, bc.Name, request.TriggeredByImage)
128
-				return nil, fmt.Errorf("Build config %s/%s has already instantiated a build for imageid %s", bc.Namespace, bc.Name, request.TriggeredByImage.Name)
129
-			}
130
-			trigger.ImageChange.LastTriggeredImageID = request.TriggeredByImage.Name
131
-		}
167
+	if err := g.updateImageTriggers(ctx, bc, request.From, request.TriggeredByImage); err != nil {
168
+		return nil, err
132 169
 	}
133 170
 
134 171
 	newBuild, err := g.generateBuildFromConfig(ctx, bc, request.Revision)
... ...
@@ -150,6 +188,46 @@ func (g *BuildGenerator) Instantiate(ctx kapi.Context, request *buildapi.BuildRe
150 150
 	return g.createBuild(ctx, newBuild)
151 151
 }
152 152
 
153
+// updateImageTriggers sets the LastTriggeredImageID on all the ImageChangeTriggers on the BuildConfig and
154
+// updates the From reference of the strategy if the strategy uses an ImageStream or ImageStreamTag reference
155
+func (g *BuildGenerator) updateImageTriggers(ctx kapi.Context, bc *buildapi.BuildConfig, from, triggeredBy *kapi.ObjectReference) error {
156
+	var requestTrigger *buildapi.ImageChangeTrigger
157
+	if from != nil {
158
+		requestTrigger = findImageChangeTrigger(bc, from)
159
+	}
160
+	if requestTrigger != nil && requestTrigger.LastTriggeredImageID == triggeredBy.Name {
161
+		glog.V(2).Infof("Aborting imageid triggered build for BuildConfig %s/%s with imageid %s because the BuildConfig already matches this imageid", bc.Namespace, bc.Name, triggeredBy.Name)
162
+		return fmt.Errorf("build config %s/%s has already instantiated a build for imageid %s", bc.Namespace, bc.Name, triggeredBy.Name)
163
+	}
164
+	// Update last triggered image id for all image change triggers
165
+	for _, trigger := range bc.Spec.Triggers {
166
+		if trigger.Type != buildapi.ImageChangeBuildTriggerType {
167
+			continue
168
+		}
169
+		// Use the requested image id for the trigger that caused the build, otherwise resolve to the latest
170
+		if trigger.ImageChange == requestTrigger {
171
+			trigger.ImageChange.LastTriggeredImageID = triggeredBy.Name
172
+			continue
173
+		}
174
+
175
+		triggerImageRef := trigger.ImageChange.From
176
+		if triggerImageRef == nil {
177
+			triggerImageRef = buildutil.GetImageStreamForStrategy(bc.Spec.Strategy)
178
+		}
179
+		image, err := g.resolveImageStreamReference(ctx, *triggerImageRef, bc.Namespace)
180
+		if err != nil {
181
+			// If the trigger is for the strategy from ref, return an error
182
+			if trigger.ImageChange.From == nil {
183
+				return err
184
+			}
185
+			// Otherwise, warn that an error occurred, but continue
186
+			glog.Warningf("Could not resolve trigger reference for build config %s/%s: %#v", bc.Namespace, bc.Name, triggerImageRef)
187
+		}
188
+		trigger.ImageChange.LastTriggeredImageID = image
189
+	}
190
+	return nil
191
+}
192
+
153 193
 // Clone returns clone of a Build
154 194
 func (g *BuildGenerator) Clone(ctx kapi.Context, request *buildapi.BuildRequest) (*buildapi.Build, error) {
155 195
 	glog.V(4).Infof("Generating build from build %s/%s", request.Namespace, request.Name)
... ...
@@ -227,15 +305,22 @@ func (g *BuildGenerator) generateBuildFromConfig(ctx kapi.Context, bc *buildapi.
227 227
 	if build.Spec.Output.PushSecret == nil {
228 228
 		build.Spec.Output.PushSecret = g.resolveImageSecret(ctx, builderSecrets, build.Spec.Output.To, bc.Namespace)
229 229
 	}
230
+	strategyImageChangeTrigger := getStrategyImageChangeTrigger(bc)
230 231
 
231 232
 	// If the Build is using a From reference instead of a resolved image, we need to resolve that From
232 233
 	// reference to a valid image so we can run the build.  Builds do not consume ImageStream references,
233 234
 	// only image specs.
235
+	var image string
236
+	if strategyImageChangeTrigger != nil {
237
+		image = strategyImageChangeTrigger.LastTriggeredImageID
238
+	}
234 239
 	switch {
235 240
 	case build.Spec.Strategy.Type == buildapi.SourceBuildStrategyType:
236
-		image, err := g.resolveImageStreamReference(ctx, build.Spec.Strategy.SourceStrategy.From, build.Status.Config.Namespace)
237
-		if err != nil {
238
-			return nil, err
241
+		if image == "" {
242
+			image, err = g.resolveImageStreamReference(ctx, build.Spec.Strategy.SourceStrategy.From, build.Status.Config.Namespace)
243
+			if err != nil {
244
+				return nil, err
245
+			}
239 246
 		}
240 247
 		build.Spec.Strategy.SourceStrategy.From = kapi.ObjectReference{
241 248
 			Kind: "DockerImage",
... ...
@@ -246,9 +331,11 @@ func (g *BuildGenerator) generateBuildFromConfig(ctx kapi.Context, bc *buildapi.
246 246
 		}
247 247
 	case build.Spec.Strategy.Type == buildapi.DockerBuildStrategyType &&
248 248
 		build.Spec.Strategy.DockerStrategy.From != nil:
249
-		image, err := g.resolveImageStreamReference(ctx, *build.Spec.Strategy.DockerStrategy.From, build.Status.Config.Namespace)
250
-		if err != nil {
251
-			return nil, err
249
+		if image == "" {
250
+			image, err = g.resolveImageStreamReference(ctx, *build.Spec.Strategy.DockerStrategy.From, build.Status.Config.Namespace)
251
+			if err != nil {
252
+				return nil, err
253
+			}
252 254
 		}
253 255
 		build.Spec.Strategy.DockerStrategy.From = &kapi.ObjectReference{
254 256
 			Kind: "DockerImage",
... ...
@@ -258,9 +345,11 @@ func (g *BuildGenerator) generateBuildFromConfig(ctx kapi.Context, bc *buildapi.
258 258
 			build.Spec.Strategy.DockerStrategy.PullSecret = g.resolveImageSecret(ctx, builderSecrets, build.Spec.Strategy.DockerStrategy.From, bc.Namespace)
259 259
 		}
260 260
 	case build.Spec.Strategy.Type == buildapi.CustomBuildStrategyType:
261
-		image, err := g.resolveImageStreamReference(ctx, build.Spec.Strategy.CustomStrategy.From, build.Status.Config.Namespace)
262
-		if err != nil {
263
-			return nil, err
261
+		if image == "" {
262
+			image, err = g.resolveImageStreamReference(ctx, build.Spec.Strategy.CustomStrategy.From, build.Status.Config.Namespace)
263
+			if err != nil {
264
+				return nil, err
265
+			}
264 266
 		}
265 267
 		build.Spec.Strategy.CustomStrategy.From = kapi.ObjectReference{
266 268
 			Kind: "DockerImage",
... ...
@@ -445,3 +534,13 @@ func getNextBuildNameFromBuild(build *buildapi.Build) string {
445 445
 	}
446 446
 	return fmt.Sprintf("%s-%d", buildName, int32(util.Now().Unix()))
447 447
 }
448
+
449
+// getStrategyImageChangeTrigger returns the ImageChangeTrigger that corresponds to the BuildConfig's strategy
450
+func getStrategyImageChangeTrigger(bc *buildapi.BuildConfig) *buildapi.ImageChangeTrigger {
451
+	for _, trigger := range bc.Spec.Triggers {
452
+		if trigger.Type == buildapi.ImageChangeBuildTriggerType && trigger.ImageChange.From == nil {
453
+			return trigger.ImageChange
454
+		}
455
+	}
456
+	return nil
457
+}
... ...
@@ -15,6 +15,7 @@ import (
15 15
 
16 16
 	buildapi "github.com/openshift/origin/pkg/build/api"
17 17
 	mocks "github.com/openshift/origin/pkg/build/generator/test"
18
+	buildutil "github.com/openshift/origin/pkg/build/util"
18 19
 	imageapi "github.com/openshift/origin/pkg/image/api"
19 20
 )
20 21
 
... ...
@@ -122,6 +123,295 @@ func TestInstantiateGenerateBuildError(t *testing.T) {
122 122
 	}
123 123
 }
124 124
 
125
+func TestInstantiateWithImageTrigger(t *testing.T) {
126
+	imageID := "the-image-id-12345"
127
+	defaultTriggers := func() []buildapi.BuildTriggerPolicy {
128
+		return []buildapi.BuildTriggerPolicy{
129
+			{
130
+				Type: buildapi.GenericWebHookBuildTriggerType,
131
+			},
132
+			{
133
+				Type:        buildapi.ImageChangeBuildTriggerType,
134
+				ImageChange: &buildapi.ImageChangeTrigger{},
135
+			},
136
+			{
137
+				Type: buildapi.ImageChangeBuildTriggerType,
138
+				ImageChange: &buildapi.ImageChangeTrigger{
139
+					From: &kapi.ObjectReference{
140
+						Name: "image1:tag1",
141
+						Kind: "ImageStreamTag",
142
+					},
143
+				},
144
+			},
145
+			{
146
+				Type: buildapi.ImageChangeBuildTriggerType,
147
+				ImageChange: &buildapi.ImageChangeTrigger{
148
+					From: &kapi.ObjectReference{
149
+						Name:      "image2:tag2",
150
+						Namespace: "image2ns",
151
+						Kind:      "ImageStreamTag",
152
+					},
153
+				},
154
+			},
155
+		}
156
+	}
157
+	triggersWithImageID := func() []buildapi.BuildTriggerPolicy {
158
+		triggers := defaultTriggers()
159
+		triggers[2].ImageChange.LastTriggeredImageID = imageID
160
+		return triggers
161
+	}
162
+	tests := []struct {
163
+		name          string
164
+		reqFrom       *kapi.ObjectReference
165
+		triggerIndex  int // index of trigger that will be updated with the image id, if -1, no update expected
166
+		triggers      []buildapi.BuildTriggerPolicy
167
+		errorExpected bool
168
+	}{
169
+		{
170
+			name: "default trigger",
171
+			reqFrom: &kapi.ObjectReference{
172
+				Kind: "ImageStreamTag",
173
+				Name: "image3:tag3",
174
+			},
175
+			triggerIndex: 1,
176
+			triggers:     defaultTriggers(),
177
+		},
178
+		{
179
+			name: "trigger with from",
180
+			reqFrom: &kapi.ObjectReference{
181
+				Kind: "ImageStreamTag",
182
+				Name: "image1:tag1",
183
+			},
184
+			triggerIndex: 2,
185
+			triggers:     defaultTriggers(),
186
+		},
187
+		{
188
+			name: "trigger with from and namespace",
189
+			reqFrom: &kapi.ObjectReference{
190
+				Kind:      "ImageStreamTag",
191
+				Name:      "image2:tag2",
192
+				Namespace: "image2ns",
193
+			},
194
+			triggerIndex: 3,
195
+			triggers:     defaultTriggers(),
196
+		},
197
+		{
198
+			name: "existing image id",
199
+			reqFrom: &kapi.ObjectReference{
200
+				Kind: "ImageStreamTag",
201
+				Name: "image1:tag1",
202
+			},
203
+			triggers:      triggersWithImageID(),
204
+			errorExpected: true,
205
+		},
206
+	}
207
+
208
+	for _, tc := range tests {
209
+		bc := &buildapi.BuildConfig{
210
+			Spec: buildapi.BuildConfigSpec{
211
+				BuildSpec: buildapi.BuildSpec{
212
+					Strategy: buildapi.BuildStrategy{
213
+						Type: buildapi.SourceBuildStrategyType,
214
+						SourceStrategy: &buildapi.SourceBuildStrategy{
215
+							From: kapi.ObjectReference{
216
+								Name: "image3:tag3",
217
+								Kind: "ImageStreamTag",
218
+							},
219
+						},
220
+					},
221
+				},
222
+				Triggers: tc.triggers,
223
+			},
224
+		}
225
+		generator := mockBuildGeneratorForInstantiate()
226
+		client := generator.Client.(Client)
227
+		client.GetBuildConfigFunc =
228
+			func(ctx kapi.Context, name string) (*buildapi.BuildConfig, error) {
229
+				return bc, nil
230
+			}
231
+		client.UpdateBuildConfigFunc =
232
+			func(ctx kapi.Context, buildConfig *buildapi.BuildConfig) error {
233
+				bc = buildConfig
234
+				return nil
235
+			}
236
+		generator.Client = client
237
+
238
+		req := &buildapi.BuildRequest{
239
+			TriggeredByImage: &kapi.ObjectReference{
240
+				Kind: "DockerImage",
241
+				Name: imageID,
242
+			},
243
+			From: tc.reqFrom,
244
+		}
245
+		_, err := generator.Instantiate(kapi.NewDefaultContext(), req)
246
+		if err != nil && !tc.errorExpected {
247
+			t.Errorf("%s: unexpected error %v", tc.name, err)
248
+			continue
249
+		}
250
+		if err == nil && tc.errorExpected {
251
+			t.Errorf("%s: expected error but didn't get one", tc.name)
252
+			continue
253
+		}
254
+		if tc.errorExpected {
255
+			continue
256
+		}
257
+		for i := range bc.Spec.Triggers {
258
+			if i == tc.triggerIndex {
259
+				// Verify that the trigger got updated
260
+				if bc.Spec.Triggers[i].ImageChange.LastTriggeredImageID != imageID {
261
+					t.Errorf("%s: expeccted trigger at index %d to contain imageID %s", tc.name, i, imageID)
262
+				}
263
+				continue
264
+			}
265
+			// Ensure that other triggers are updated with the latest docker image ref
266
+			if bc.Spec.Triggers[i].Type == buildapi.ImageChangeBuildTriggerType {
267
+				from := bc.Spec.Triggers[i].ImageChange.From
268
+				if from == nil {
269
+					from = buildutil.GetImageStreamForStrategy(bc.Spec.Strategy)
270
+				}
271
+				if bc.Spec.Triggers[i].ImageChange.LastTriggeredImageID != ("ref@" + from.Name) {
272
+					t.Errorf("%s: expected LastTriggeredImageID for trigger at %d to be %s. Got: %s", tc.name, i, "ref@"+from.Name, bc.Spec.Triggers[i].ImageChange.LastTriggeredImageID)
273
+				}
274
+			}
275
+		}
276
+	}
277
+}
278
+
279
+func TestFindImageTrigger(t *testing.T) {
280
+	defaultTrigger := &buildapi.ImageChangeTrigger{}
281
+	image1Trigger := &buildapi.ImageChangeTrigger{
282
+		From: &kapi.ObjectReference{
283
+			Name: "image1:tag1",
284
+		},
285
+	}
286
+	image2Trigger := &buildapi.ImageChangeTrigger{
287
+		From: &kapi.ObjectReference{
288
+			Name:      "image2:tag2",
289
+			Namespace: "image2ns",
290
+		},
291
+	}
292
+	image4Trigger := &buildapi.ImageChangeTrigger{
293
+		From: &kapi.ObjectReference{
294
+			Name: "image4:tag4",
295
+		},
296
+	}
297
+	image5Trigger := &buildapi.ImageChangeTrigger{
298
+		From: &kapi.ObjectReference{
299
+			Name:      "image5:tag5",
300
+			Namespace: "bcnamespace",
301
+		},
302
+	}
303
+	bc := &buildapi.BuildConfig{
304
+		ObjectMeta: kapi.ObjectMeta{
305
+			Name:      "testbc",
306
+			Namespace: "bcnamespace",
307
+		},
308
+		Spec: buildapi.BuildConfigSpec{
309
+			BuildSpec: buildapi.BuildSpec{
310
+				Strategy: buildapi.BuildStrategy{
311
+					Type: buildapi.SourceBuildStrategyType,
312
+					SourceStrategy: &buildapi.SourceBuildStrategy{
313
+						From: kapi.ObjectReference{
314
+							Name: "image3:tag3",
315
+							Kind: "ImageStreamTag",
316
+						},
317
+					},
318
+				},
319
+			},
320
+			Triggers: []buildapi.BuildTriggerPolicy{
321
+				{
322
+					Type: buildapi.GenericWebHookBuildTriggerType,
323
+				},
324
+				{
325
+					Type:        buildapi.ImageChangeBuildTriggerType,
326
+					ImageChange: defaultTrigger,
327
+				},
328
+				{
329
+					Type:        buildapi.ImageChangeBuildTriggerType,
330
+					ImageChange: image1Trigger,
331
+				},
332
+				{
333
+					Type:        buildapi.ImageChangeBuildTriggerType,
334
+					ImageChange: image2Trigger,
335
+				},
336
+				{
337
+					Type:        buildapi.ImageChangeBuildTriggerType,
338
+					ImageChange: image4Trigger,
339
+				},
340
+				{
341
+					Type:        buildapi.ImageChangeBuildTriggerType,
342
+					ImageChange: image5Trigger,
343
+				},
344
+			},
345
+		},
346
+	}
347
+
348
+	tests := []struct {
349
+		name   string
350
+		input  *kapi.ObjectReference
351
+		expect *buildapi.ImageChangeTrigger
352
+	}{
353
+		{
354
+			name:   "nil reference",
355
+			input:  nil,
356
+			expect: nil,
357
+		},
358
+		{
359
+			name: "match name",
360
+			input: &kapi.ObjectReference{
361
+				Name: "image1:tag1",
362
+			},
363
+			expect: image1Trigger,
364
+		},
365
+		{
366
+			name: "mismatched namespace",
367
+			input: &kapi.ObjectReference{
368
+				Name:      "image1:tag1",
369
+				Namespace: "otherns",
370
+			},
371
+			expect: nil,
372
+		},
373
+		{
374
+			name: "match name and namespace",
375
+			input: &kapi.ObjectReference{
376
+				Name:      "image2:tag2",
377
+				Namespace: "image2ns",
378
+			},
379
+			expect: image2Trigger,
380
+		},
381
+		{
382
+			name: "match default trigger",
383
+			input: &kapi.ObjectReference{
384
+				Name: "image3:tag3",
385
+			},
386
+			expect: defaultTrigger,
387
+		},
388
+		{
389
+			name: "input includes bc namespace",
390
+			input: &kapi.ObjectReference{
391
+				Name:      "image4:tag4",
392
+				Namespace: "bcnamespace",
393
+			},
394
+			expect: image4Trigger,
395
+		},
396
+		{
397
+			name: "implied namespace in trigger input",
398
+			input: &kapi.ObjectReference{
399
+				Name: "image5:tag5",
400
+			},
401
+			expect: image5Trigger,
402
+		},
403
+	}
404
+
405
+	for _, tc := range tests {
406
+		result := findImageChangeTrigger(bc, tc.input)
407
+		if result != tc.expect {
408
+			t.Errorf("%s: unexpected trigger for %#v: %#v", tc.name, tc.input, result)
409
+		}
410
+	}
411
+
412
+}
413
+
125 414
 func TestClone(t *testing.T) {
126 415
 	generator := BuildGenerator{Client: Client{
127 416
 		CreateBuildFunc: func(ctx kapi.Context, build *buildapi.Build) error {
... ...
@@ -861,6 +1151,21 @@ func mockBuild(source buildapi.BuildSource, strategy buildapi.BuildStrategy, out
861 861
 	}
862 862
 }
863 863
 
864
+func mockBuildGeneratorForInstantiate() *BuildGenerator {
865
+	g := mockBuildGenerator()
866
+	c := g.Client.(Client)
867
+	c.GetImageStreamTagFunc = func(ctx kapi.Context, name string) (*imageapi.ImageStreamTag, error) {
868
+		return &imageapi.ImageStreamTag{
869
+			Image: imageapi.Image{
870
+				ObjectMeta:           kapi.ObjectMeta{Name: imageRepoName + ":" + newTag},
871
+				DockerImageReference: "ref@" + name,
872
+			},
873
+		}, nil
874
+	}
875
+	g.Client = c
876
+	return g
877
+}
878
+
864 879
 func mockBuildGenerator() *BuildGenerator {
865 880
 	fakeSecrets := []runtime.Object{}
866 881
 	for _, s := range mocks.MockBuilderSecrets() {
... ...
@@ -1,9 +1,11 @@
1 1
 package graph
2 2
 
3 3
 import (
4
+	kapi "github.com/GoogleCloudPlatform/kubernetes/pkg/api"
4 5
 	"github.com/gonum/graph"
5 6
 
6 7
 	osgraph "github.com/openshift/origin/pkg/api/graph"
8
+	buildapi "github.com/openshift/origin/pkg/build/api"
7 9
 	buildgraph "github.com/openshift/origin/pkg/build/graph/nodes"
8 10
 	buildutil "github.com/openshift/origin/pkg/build/util"
9 11
 	imageapi "github.com/openshift/origin/pkg/image/api"
... ...
@@ -11,14 +13,30 @@ import (
11 11
 )
12 12
 
13 13
 const (
14
+	// BuildTriggerImageEdgeKind is an edge from an ImageStream to a BuildConfig that
15
+	// represents a trigger connection. Changes to the ImageStream will trigger a new build
16
+	// from the BuildConfig.
17
+	BuildTriggerImageEdgeKind = "BuildTriggerImage"
18
+
19
+	// BuildInputImageEdgeKind is  an edge from an ImageStream to a BuildConfig, where the
20
+	// ImageStream is the source image for the build (builder in S2I builds, FROM in Docker builds,
21
+	// custom builder in Custom builds). The same ImageStream can also have a trigger
22
+	// relationship with the BuildConfig, but not necessarily.
14 23
 	BuildInputImageEdgeKind = "BuildInputImage"
15
-	BuildOutputEdgeKind     = "BuildOutput"
16
-	BuildInputEdgeKind      = "BuildInput"
24
+
25
+	// BuildOutputEdgeKind is an edge from a BuildConfig to an ImageStream. The ImageStream will hold
26
+	// the ouptut of the Builds created with that BuildConfig.
27
+	BuildOutputEdgeKind = "BuildOutput"
28
+
29
+	// BuildInputEdgeKind is an edge from a source repository to a BuildConfig. The source repository is the
30
+	// input source for the build.
31
+	BuildInputEdgeKind = "BuildInput"
17 32
 
18 33
 	// BuildEdgeKind goes from a BuildConfigNode to a BuildNode and indicates that the buildConfig owns the build
19 34
 	BuildEdgeKind = "Build"
20 35
 )
21 36
 
37
+// AddBuildEdges adds edges that connect a BuildConfig to Builds to the given graph
22 38
 func AddBuildEdges(g osgraph.MutableUniqueGraph, node *buildgraph.BuildConfigNode) {
23 39
 	for _, n := range g.(graph.Graph).Nodes() {
24 40
 		if buildNode, ok := n.(*buildgraph.BuildNode); ok {
... ...
@@ -29,6 +47,7 @@ func AddBuildEdges(g osgraph.MutableUniqueGraph, node *buildgraph.BuildConfigNod
29 29
 	}
30 30
 }
31 31
 
32
+// AddAllBuildEdges adds build edges to all BuildConfig nodes in the given graph
32 33
 func AddAllBuildEdges(g osgraph.MutableUniqueGraph) {
33 34
 	for _, node := range g.(graph.Graph).Nodes() {
34 35
 		if bcNode, ok := node.(*buildgraph.BuildConfigNode); ok {
... ...
@@ -37,48 +56,68 @@ func AddAllBuildEdges(g osgraph.MutableUniqueGraph) {
37 37
 	}
38 38
 }
39 39
 
40
-// AddInputOutputEdges links the build config to other nodes for the images and source repositories it depends on.
41
-func AddInputOutputEdges(g osgraph.MutableUniqueGraph, node *buildgraph.BuildConfigNode) *buildgraph.BuildConfigNode {
42
-	output := node.BuildConfig.Spec.Output
43
-	to := output.To
44
-	switch {
45
-	case to == nil:
46
-	case to.Kind == "DockerImage":
47
-		out := imagegraph.EnsureDockerRepositoryNode(g, to.Name, "")
48
-		g.AddEdge(node, out, BuildOutputEdgeKind)
49
-	case to.Kind == "ImageStreamTag":
50
-		out := imagegraph.FindOrCreateSyntheticImageStreamTagNode(g, imagegraph.MakeImageStreamTagObjectMeta2(defaultNamespace(to.Namespace, node.BuildConfig.Namespace), to.Name))
51
-		g.AddEdge(node, out, BuildOutputEdgeKind)
40
+func imageRefNode(g osgraph.MutableUniqueGraph, ref *kapi.ObjectReference, bc *buildapi.BuildConfig) graph.Node {
41
+	if ref == nil {
42
+		return nil
52 43
 	}
44
+	switch ref.Kind {
45
+	case "DockerImage":
46
+		if ref, err := imageapi.ParseDockerImageReference(ref.Name); err == nil {
47
+			tag := ref.Tag
48
+			ref.Tag = ""
49
+			return imagegraph.EnsureDockerRepositoryNode(g, ref.String(), tag)
50
+		}
51
+	case "ImageStream":
52
+		return imagegraph.FindOrCreateSyntheticImageStreamTagNode(g, imagegraph.MakeImageStreamTagObjectMeta(defaultNamespace(ref.Namespace, bc.Namespace), ref.Name, imageapi.DefaultImageTag))
53
+	case "ImageStreamTag":
54
+		return imagegraph.FindOrCreateSyntheticImageStreamTagNode(g, imagegraph.MakeImageStreamTagObjectMeta2(defaultNamespace(ref.Namespace, bc.Namespace), ref.Name))
55
+	case "ImageStreamImage":
56
+		return imagegraph.FindOrCreateSyntheticImageStreamImageNode(g, imagegraph.MakeImageStreamImageObjectMeta(defaultNamespace(ref.Namespace, bc.Namespace), ref.Name))
57
+	}
58
+	return nil
59
+}
60
+
61
+// AddOutputEdges links the build config to its output image node.
62
+func AddOutputEdges(g osgraph.MutableUniqueGraph, node *buildgraph.BuildConfigNode) {
63
+	out := imageRefNode(g, node.BuildConfig.Spec.Output.To, node.BuildConfig)
64
+	g.AddEdge(node, out, BuildOutputEdgeKind)
65
+}
53 66
 
67
+// AddInputEdges links the build config to its input image and source nodes.
68
+func AddInputEdges(g osgraph.MutableUniqueGraph, node *buildgraph.BuildConfigNode) {
54 69
 	if in := buildgraph.EnsureSourceRepositoryNode(g, node.BuildConfig.Spec.Source); in != nil {
55 70
 		g.AddEdge(in, node, BuildInputEdgeKind)
56 71
 	}
72
+	inputImage := buildutil.GetImageStreamForStrategy(node.BuildConfig.Spec.Strategy)
73
+	if input := imageRefNode(g, inputImage, node.BuildConfig); input != nil {
74
+		g.AddEdge(input, node, BuildInputImageEdgeKind)
75
+	}
76
+}
57 77
 
58
-	from := buildutil.GetImageStreamForStrategy(node.BuildConfig.Spec.Strategy)
59
-	if from != nil {
60
-		switch from.Kind {
61
-		case "DockerImage":
62
-			if ref, err := imageapi.ParseDockerImageReference(from.Name); err == nil {
63
-				tag := ref.Tag
64
-				ref.Tag = ""
65
-				in := imagegraph.EnsureDockerRepositoryNode(g, ref.String(), tag)
66
-				g.AddEdge(in, node, BuildInputImageEdgeKind)
67
-			}
68
-		case "ImageStream":
69
-			in := imagegraph.FindOrCreateSyntheticImageStreamTagNode(g, imagegraph.MakeImageStreamTagObjectMeta(defaultNamespace(from.Namespace, node.BuildConfig.Namespace), from.Name, imageapi.DefaultImageTag))
70
-			g.AddEdge(in, node, BuildInputImageEdgeKind)
71
-		case "ImageStreamTag":
72
-			in := imagegraph.FindOrCreateSyntheticImageStreamTagNode(g, imagegraph.MakeImageStreamTagObjectMeta2(defaultNamespace(from.Namespace, node.BuildConfig.Namespace), from.Name))
73
-			g.AddEdge(in, node, BuildInputImageEdgeKind)
74
-		case "ImageStreamImage":
75
-			in := imagegraph.FindOrCreateSyntheticImageStreamImageNode(g, imagegraph.MakeImageStreamImageObjectMeta(defaultNamespace(from.Namespace, node.BuildConfig.Namespace), from.Name))
76
-			g.AddEdge(in, node, BuildInputImageEdgeKind)
78
+// AddTriggerEdges links the build config to its trigger input image nodes.
79
+func AddTriggerEdges(g osgraph.MutableUniqueGraph, node *buildgraph.BuildConfigNode) {
80
+	for _, trigger := range node.BuildConfig.Spec.Triggers {
81
+		if trigger.Type != buildapi.ImageChangeBuildTriggerType {
82
+			continue
83
+		}
84
+		from := trigger.ImageChange.From
85
+		if trigger.ImageChange.From == nil {
86
+			from = buildutil.GetImageStreamForStrategy(node.BuildConfig.Spec.Strategy)
77 87
 		}
88
+		triggerNode := imageRefNode(g, from, node.BuildConfig)
89
+		g.AddEdge(triggerNode, node, BuildTriggerImageEdgeKind)
78 90
 	}
91
+}
92
+
93
+// AddInputOutputEdges links the build config to other nodes for the images and source repositories it depends on.
94
+func AddInputOutputEdges(g osgraph.MutableUniqueGraph, node *buildgraph.BuildConfigNode) *buildgraph.BuildConfigNode {
95
+	AddInputEdges(g, node)
96
+	AddTriggerEdges(g, node)
97
+	AddOutputEdges(g, node)
79 98
 	return node
80 99
 }
81 100
 
101
+// AddAllInputOutputEdges adds input and output edges for all BuildConfigs in the given graph
82 102
 func AddAllInputOutputEdges(g osgraph.MutableUniqueGraph) {
83 103
 	for _, node := range g.(graph.Graph).Nodes() {
84 104
 		if bcNode, ok := node.(*buildgraph.BuildConfigNode); ok {
... ...
@@ -2,6 +2,7 @@ package describe
2 2
 
3 3
 import (
4 4
 	"fmt"
5
+	"sort"
5 6
 	"strings"
6 7
 
7 8
 	kapi "github.com/GoogleCloudPlatform/kubernetes/pkg/api"
... ...
@@ -10,8 +11,8 @@ import (
10 10
 	"github.com/golang/glog"
11 11
 	"github.com/gonum/graph"
12 12
 	"github.com/gonum/graph/encoding/dot"
13
+	"github.com/gonum/graph/internal"
13 14
 	"github.com/gonum/graph/path"
14
-	"github.com/gonum/graph/traverse"
15 15
 
16 16
 	osgraph "github.com/openshift/origin/pkg/api/graph"
17 17
 	buildedges "github.com/openshift/origin/pkg/build/graph"
... ...
@@ -78,7 +79,7 @@ func (d *ChainDescriber) MakeGraph() (osgraph.Graph, error) {
78 78
 // image stream tag (name:tag) in namespace. Namespace is needed here
79 79
 // because image stream tags with the same name can be found across
80 80
 // different namespaces.
81
-func (d *ChainDescriber) Describe(ist *imageapi.ImageStreamTag) (string, error) {
81
+func (d *ChainDescriber) Describe(ist *imageapi.ImageStreamTag, includeInputImages bool) (string, error) {
82 82
 	g, err := d.MakeGraph()
83 83
 	if err != nil {
84 84
 		return "", err
... ...
@@ -90,8 +91,13 @@ func (d *ChainDescriber) Describe(ist *imageapi.ImageStreamTag) (string, error)
90 90
 		return "", NotFoundErr(fmt.Sprintf("%q", ist.Name))
91 91
 	}
92 92
 
93
+	buildInputEdgeKinds := []string{buildedges.BuildTriggerImageEdgeKind}
94
+	if includeInputImages {
95
+		buildInputEdgeKinds = append(buildInputEdgeKinds, buildedges.BuildInputImageEdgeKind)
96
+	}
97
+
93 98
 	// Partition down to the subgraph containing the ist of interest
94
-	partitioned := partition(g, istNode)
99
+	partitioned := partition(g, istNode, buildInputEdgeKinds)
95 100
 
96 101
 	switch strings.ToLower(d.outputFormat) {
97 102
 	case "dot":
... ...
@@ -108,11 +114,14 @@ func (d *ChainDescriber) Describe(ist *imageapi.ImageStreamTag) (string, error)
108 108
 }
109 109
 
110 110
 // partition the graph down to a subgraph starting from the given root
111
-func partition(g osgraph.Graph, root graph.Node) osgraph.Graph {
111
+func partition(g osgraph.Graph, root graph.Node, buildInputEdgeKinds []string) osgraph.Graph {
112 112
 	// Filter out all but BuildConfig and ImageStreamTag nodes
113 113
 	nodeFn := osgraph.NodesOfKind(buildgraph.BuildConfigNodeKind, imagegraph.ImageStreamTagNodeKind)
114 114
 	// Filter out all but BuildInputImage and BuildOutput edges
115
-	edgeFn := osgraph.EdgesOfKind(buildedges.BuildInputImageEdgeKind, buildedges.BuildOutputEdgeKind)
115
+	edgeKinds := []string{}
116
+	edgeKinds = append(edgeKinds, buildInputEdgeKinds...)
117
+	edgeKinds = append(edgeKinds, buildedges.BuildOutputEdgeKind)
118
+	edgeFn := osgraph.EdgesOfKind(edgeKinds...)
116 119
 	sub := g.Subgraph(nodeFn, edgeFn)
117 120
 
118 121
 	// Filter out inbound edges to the ist of interest
... ...
@@ -149,7 +158,7 @@ func (d *ChainDescriber) humanReadableOutput(g osgraph.Graph, root graph.Node) s
149 149
 	}
150 150
 	out := ""
151 151
 
152
-	dfs := &traverse.DepthFirst{
152
+	dfs := &DepthFirst{
153 153
 		Visit: func(u, v graph.Node) {
154 154
 			depth[v] = depth[u] + 1
155 155
 		},
... ...
@@ -189,3 +198,52 @@ func outputHelper(info, namespace string, singleNamespace bool) string {
189 189
 	}
190 190
 	return fmt.Sprintf("<%s %s>", namespace, info)
191 191
 }
192
+
193
+// DepthFirst implements stateful depth-first graph traversal.
194
+// Modifies behavior of visitor.DepthFirst to allow nodes to be visited multiple
195
+// times as long as they're not in the current stack
196
+type DepthFirst struct {
197
+	EdgeFilter func(graph.Edge) bool
198
+	Visit      func(u, v graph.Node)
199
+	stack      internal.NodeStack
200
+}
201
+
202
+// Walk performs a depth-first traversal of the graph g starting from the given node
203
+func (d *DepthFirst) Walk(g graph.Graph, from graph.Node, until func(graph.Node) bool) graph.Node {
204
+	return d.visit(g, from, until)
205
+}
206
+
207
+func (d *DepthFirst) visit(g graph.Graph, t graph.Node, until func(graph.Node) bool) graph.Node {
208
+	if until != nil && until(t) {
209
+		return t
210
+	}
211
+	d.stack.Push(t)
212
+	children := osgraph.ByID(g.From(t))
213
+	sort.Sort(children)
214
+	for _, n := range children {
215
+		if d.EdgeFilter != nil && !d.EdgeFilter(g.Edge(t, n)) {
216
+			continue
217
+		}
218
+		if d.visited(n.ID()) {
219
+			continue
220
+		}
221
+		if d.Visit != nil {
222
+			d.Visit(t, n)
223
+		}
224
+		result := d.visit(g, n, until)
225
+		if result != nil {
226
+			return result
227
+		}
228
+	}
229
+	d.stack.Pop()
230
+	return nil
231
+}
232
+
233
+func (d *DepthFirst) visited(id int) bool {
234
+	for _, n := range d.stack {
235
+		if n.ID() == id {
236
+			return true
237
+		}
238
+	}
239
+	return false
240
+}
... ...
@@ -7,6 +7,8 @@ import (
7 7
 	kapi "github.com/GoogleCloudPlatform/kubernetes/pkg/api"
8 8
 	ktestclient "github.com/GoogleCloudPlatform/kubernetes/pkg/client/testclient"
9 9
 	kutil "github.com/GoogleCloudPlatform/kubernetes/pkg/util"
10
+	"github.com/gonum/graph"
11
+	"github.com/gonum/graph/concrete"
10 12
 
11 13
 	"github.com/openshift/origin/pkg/client/testclient"
12 14
 	imagegraph "github.com/openshift/origin/pkg/image/graph/nodes"
... ...
@@ -21,9 +23,10 @@ func TestChainDescriber(t *testing.T) {
21 21
 		name             string
22 22
 		tag              string
23 23
 		path             string
24
-		humanReadable    map[string]struct{}
24
+		humanReadable    map[string]int
25 25
 		dot              []string
26 26
 		expectedErr      error
27
+		includeInputImg  bool
27 28
 	}{
28 29
 		{
29 30
 			testName:         "human readable test - single namespace",
... ...
@@ -33,12 +36,12 @@ func TestChainDescriber(t *testing.T) {
33 33
 			name:             "ruby-20-centos7",
34 34
 			tag:              "latest",
35 35
 			path:             "../../../../pkg/cmd/experimental/buildchain/test/single-namespace-bcs.yaml",
36
-			humanReadable: map[string]struct{}{
37
-				"imagestreamtag/ruby-20-centos7:latest":        {},
38
-				"\tbc/ruby-hello-world":                        {},
39
-				"\t\timagestreamtag/ruby-hello-world:latest":   {},
40
-				"\tbc/ruby-sample-build":                       {},
41
-				"\t\timagestreamtag/origin-ruby-sample:latest": {},
36
+			humanReadable: map[string]int{
37
+				"imagestreamtag/ruby-20-centos7:latest":        1,
38
+				"\tbc/ruby-hello-world":                        1,
39
+				"\t\timagestreamtag/ruby-hello-world:latest":   1,
40
+				"\tbc/ruby-sample-build":                       1,
41
+				"\t\timagestreamtag/origin-ruby-sample:latest": 1,
42 42
 			},
43 43
 			expectedErr: nil,
44 44
 		},
... ...
@@ -62,8 +65,8 @@ func TestChainDescriber(t *testing.T) {
62 62
 				"// Edge definitions.",
63 63
 				"[label=\"BuildOutput\"];",
64 64
 				"[label=\"BuildOutput\"];",
65
-				"[label=\"BuildInputImage\"];",
66
-				"[label=\"BuildInputImage\"];",
65
+				"[label=\"BuildInputImage,BuildTriggerImage\"];",
66
+				"[label=\"BuildInputImage,BuildTriggerImage\"];",
67 67
 				"}",
68 68
 			},
69 69
 			expectedErr: nil,
... ...
@@ -76,12 +79,12 @@ func TestChainDescriber(t *testing.T) {
76 76
 			name:             "ruby-20-centos7",
77 77
 			tag:              "latest",
78 78
 			path:             "../../../../pkg/cmd/experimental/buildchain/test/multiple-namespaces-bcs.yaml",
79
-			humanReadable: map[string]struct{}{
80
-				"<master imagestreamtag/ruby-20-centos7:latest>":         {},
81
-				"\t<default bc/ruby-hello-world>":                        {},
82
-				"\t\t<test imagestreamtag/ruby-hello-world:latest>":      {},
83
-				"\t<test bc/ruby-sample-build>":                          {},
84
-				"\t\t<another imagestreamtag/origin-ruby-sample:latest>": {},
79
+			humanReadable: map[string]int{
80
+				"<master imagestreamtag/ruby-20-centos7:latest>":         1,
81
+				"\t<default bc/ruby-hello-world>":                        1,
82
+				"\t\t<test imagestreamtag/ruby-hello-world:latest>":      1,
83
+				"\t<test bc/ruby-sample-build>":                          1,
84
+				"\t\t<another imagestreamtag/origin-ruby-sample:latest>": 1,
85 85
 			},
86 86
 			expectedErr: nil,
87 87
 		},
... ...
@@ -105,12 +108,59 @@ func TestChainDescriber(t *testing.T) {
105 105
 				"// Edge definitions.",
106 106
 				"[label=\"BuildOutput\"];",
107 107
 				"[label=\"BuildOutput\"];",
108
-				"[label=\"BuildInputImage\"];",
109
-				"[label=\"BuildInputImage\"];",
108
+				"[label=\"BuildInputImage,BuildTriggerImage\"];",
109
+				"[label=\"BuildInputImage,BuildTriggerImage\"];",
110 110
 				"}",
111 111
 			},
112 112
 			expectedErr: nil,
113 113
 		},
114
+		{
115
+			testName:         "human readable - multiple triggers - triggeronly",
116
+			name:             "ruby-20-centos7",
117
+			defaultNamespace: "test",
118
+			tag:              "latest",
119
+			path:             "../../../../pkg/cmd/experimental/buildchain/test/multiple-trigger-bcs.yaml",
120
+			namespaces:       kutil.NewStringSet("test"),
121
+			humanReadable: map[string]int{
122
+				"imagestreamtag/ruby-20-centos7:latest":   1,
123
+				"\tbc/parent1":                            1,
124
+				"\t\timagestreamtag/parent1img:latest":    1,
125
+				"\t\t\tbc/child2":                         2,
126
+				"\t\t\t\timagestreamtag/child2img:latest": 2,
127
+				"\tbc/parent2":                            1,
128
+				"\t\timagestreamtag/parent2img:latest":    1,
129
+				"\t\t\tbc/child3":                         2,
130
+				"\t\t\t\timagestreamtag/child3img:latest": 2,
131
+				"\t\t\tbc/child1":                         1,
132
+				"\t\t\t\timagestreamtag/child1img:latest": 1,
133
+				"\tbc/parent3":                            1,
134
+				"\t\timagestreamtag/parent3img:latest":    1,
135
+			},
136
+		},
137
+		{
138
+			testName:         "human readable - multiple triggers - trigger+input",
139
+			name:             "ruby-20-centos7",
140
+			defaultNamespace: "test",
141
+			tag:              "latest",
142
+			path:             "../../../../pkg/cmd/experimental/buildchain/test/multiple-trigger-bcs.yaml",
143
+			namespaces:       kutil.NewStringSet("test"),
144
+			includeInputImg:  true,
145
+			humanReadable: map[string]int{
146
+				"imagestreamtag/ruby-20-centos7:latest":   1,
147
+				"\tbc/parent1":                            1,
148
+				"\t\timagestreamtag/parent1img:latest":    1,
149
+				"\t\t\tbc/child1":                         2,
150
+				"\t\t\t\timagestreamtag/child1img:latest": 2,
151
+				"\t\t\tbc/child2":                         2,
152
+				"\t\t\t\timagestreamtag/child2img:latest": 2,
153
+				"\t\t\tbc/child3":                         3,
154
+				"\t\t\t\timagestreamtag/child3img:latest": 3,
155
+				"\tbc/parent2":                            1,
156
+				"\t\timagestreamtag/parent2img:latest":    1,
157
+				"\tbc/parent3":                            1,
158
+				"\t\timagestreamtag/parent3img:latest":    1,
159
+			},
160
+		},
114 161
 	}
115 162
 
116 163
 	for _, test := range tests {
... ...
@@ -124,7 +174,8 @@ func TestChainDescriber(t *testing.T) {
124 124
 		oc, _ := testclient.NewFixtureClients(o)
125 125
 		ist := imagegraph.MakeImageStreamTagObjectMeta(test.defaultNamespace, test.name, test.tag)
126 126
 
127
-		desc, err := NewChainDescriber(oc, test.namespaces, test.output).Describe(ist)
127
+		desc, err := NewChainDescriber(oc, test.namespaces, test.output).Describe(ist, test.includeInputImg)
128
+		t.Logf("%s: output:\n%s\n\n", test.testName, desc)
128 129
 		if err != test.expectedErr {
129 130
 			t.Fatalf("%s: error mismatch: expected %v, got %v", test.testName, test.expectedErr, err)
130 131
 		}
... ...
@@ -134,7 +185,7 @@ func TestChainDescriber(t *testing.T) {
134 134
 		switch test.output {
135 135
 		case "dot":
136 136
 			if len(test.dot) != len(got) {
137
-				t.Fatalf("%s: expected %d lines, got %d", test.testName, len(test.dot), len(got))
137
+				t.Fatalf("%s: expected %d lines, got %d:\n%s", test.testName, len(test.dot), len(got), desc)
138 138
 			}
139 139
 			for _, expected := range test.dot {
140 140
 				if !strings.Contains(desc, expected) {
... ...
@@ -142,14 +193,60 @@ func TestChainDescriber(t *testing.T) {
142 142
 				}
143 143
 			}
144 144
 		case "":
145
-			if len(test.humanReadable) != len(got) {
146
-				t.Fatalf("%s: expected %d lines, got %d", test.testName, len(test.humanReadable), len(got))
145
+			if lenReadable(test.humanReadable) != len(got) {
146
+				t.Fatalf("%s: expected %d lines, got %d:\n%s", test.testName, lenReadable(test.humanReadable), len(got), desc)
147 147
 			}
148 148
 			for _, line := range got {
149 149
 				if _, ok := test.humanReadable[line]; !ok {
150 150
 					t.Errorf("%s: unexpected line: %s", test.testName, line)
151 151
 				}
152
+				test.humanReadable[line]--
153
+			}
154
+			for line, cnt := range test.humanReadable {
155
+				if cnt != 0 {
156
+					t.Errorf("%s: unexpected number of lines for [%s]: %d", test.testName, line, cnt)
157
+				}
152 158
 			}
153 159
 		}
154 160
 	}
155 161
 }
162
+
163
+func lenReadable(value map[string]int) int {
164
+	length := 0
165
+	for _, cnt := range value {
166
+		length += cnt
167
+	}
168
+	return length
169
+}
170
+
171
+func TestDepthFirst(t *testing.T) {
172
+	g := concrete.NewDirectedGraph()
173
+
174
+	a := concrete.Node(g.NewNodeID())
175
+	b := concrete.Node(g.NewNodeID())
176
+
177
+	g.AddNode(a)
178
+	g.AddNode(b)
179
+	g.SetEdge(concrete.Edge{F: a, T: b}, 1)
180
+	g.SetEdge(concrete.Edge{F: b, T: a}, 1)
181
+
182
+	count := 0
183
+
184
+	df := &DepthFirst{
185
+		EdgeFilter: func(graph.Edge) bool {
186
+			return true
187
+		},
188
+		Visit: func(u, v graph.Node) {
189
+			count++
190
+			t.Logf("%d -> %d\n", u.ID(), v.ID())
191
+		},
192
+	}
193
+
194
+	df.Walk(g, a, func(n graph.Node) bool {
195
+		if count > 100 {
196
+			t.Fatalf("looped")
197
+			return true
198
+		}
199
+		return false
200
+	})
201
+}
... ...
@@ -24,7 +24,7 @@ const (
24 24
 Output the inputs and dependencies of your builds
25 25
 
26 26
 Supported formats for the generated graph are dot and a human-readable output.
27
-Tag and namespace are optional and if they are not specified, 'latest' and the 
27
+Tag and namespace are optional and if they are not specified, 'latest' and the
28 28
 default namespace will be used respectively.`
29 29
 
30 30
 	buildChainExample = `  // Build the dependency tree for the 'latest' tag in centos7
... ...
@@ -37,6 +37,7 @@ default namespace will be used respectively.`
37 37
   $ %[1]s centos7 -n test --all`
38 38
 )
39 39
 
40
+// BuildChainRecommendedCommandName is the recommended command name
40 41
 const BuildChainRecommendedCommandName = "build-chain"
41 42
 
42 43
 // BuildChainOptions contains all the options needed for build-chain
... ...
@@ -47,6 +48,7 @@ type BuildChainOptions struct {
47 47
 	defaultNamespace string
48 48
 	namespaces       kutil.StringSet
49 49
 	allNamespaces    bool
50
+	triggerOnly      bool
50 51
 
51 52
 	output string
52 53
 
... ...
@@ -74,6 +76,7 @@ func NewCmdBuildChain(name, fullName string, f *clientcmd.Factory, out io.Writer
74 74
 	}
75 75
 
76 76
 	cmd.Flags().BoolVar(&options.allNamespaces, "all", false, "Build dependency tree for the specified image stream tag across all namespaces")
77
+	cmd.Flags().BoolVar(&options.triggerOnly, "trigger-only", true, "If true, only include dependencies based on build triggers. If false, include all dependencies.")
77 78
 	cmd.Flags().StringVarP(&options.output, "output", "o", "", "Output format of dependency tree")
78 79
 	return cmd
79 80
 }
... ...
@@ -151,7 +154,7 @@ func (o *BuildChainOptions) Validate() error {
151 151
 // experimental build-chain command
152 152
 func (o *BuildChainOptions) RunBuildChain() error {
153 153
 	ist := imagegraph.MakeImageStreamTagObjectMeta(o.defaultNamespace, o.name, o.tag)
154
-	desc, err := describe.NewChainDescriber(o.c, o.namespaces, o.output).Describe(ist)
154
+	desc, err := describe.NewChainDescriber(o.c, o.namespaces, o.output).Describe(ist, !o.triggerOnly)
155 155
 	if err != nil {
156 156
 		if _, isNotFoundErr := err.(describe.NotFoundErr); isNotFoundErr {
157 157
 			// Try to get the imageStreamTag via a direct GET
158 158
new file mode 100644
... ...
@@ -0,0 +1,220 @@
0
+# Sets up a multi-parent build config tree
1
+# openshift/ruby-20-centos7:latest
2
+#   -> bc - parent1 (input, trigger)
3
+#      -> parent1img:latest
4
+#         -> bc - child1 (input)
5
+#         -> bc - child2 (input, trigger)
6
+#         -> bc - child3 (input)
7
+#   -> bc - parent2 (input, trigger)
8
+#      -> parent2img:latest
9
+#         -> bc - child1 (trigger)
10
+#         -> bc - child3 (trigger)
11
+#   -> bc - parent3 (input, trigger)
12
+#      -> parent3img:latest
13
+#         -> bc - child2 (trigger)
14
+#         -> bc - child3 (trigger)
15
+#
16
+#  bc child1 has  [input] parent1img, and [trigger] parent2img
17
+#  bc child2 has  [input, trigger] parent1img, and [trigger] parent3img
18
+#  bc child3 has  [input] parent1img, [trigger] parent2img, [trigger] parent3img
19
+#
20
+apiVersion: v1
21
+items:
22
+- apiVersion: v1
23
+  kind: BuildConfig
24
+  metadata:
25
+    name: parent1
26
+    namespace: test
27
+  spec:
28
+    output:
29
+      to:
30
+        kind: ImageStreamTag
31
+        name: parent1img:latest
32
+    resources: {}
33
+    source:
34
+      git:
35
+        uri: https://github.com/openshift/ruby-hello-world.git
36
+      type: Git
37
+    strategy:
38
+      dockerStrategy:
39
+        from:
40
+          kind: ImageStreamTag
41
+          name: ruby-20-centos7:latest
42
+      type: Docker
43
+    triggers:
44
+    - github:
45
+        secret: q_ZtlnBcu7ca48ie8dNi
46
+      type: GitHub
47
+    - generic:
48
+        secret: 3kYKtANjVRCOPoM0uLNp
49
+      type: Generic
50
+    - imageChange: {}
51
+      type: ImageChange
52
+- apiVersion: v1
53
+  kind: BuildConfig
54
+  metadata:
55
+    name: parent2
56
+    namespace: test
57
+  spec:
58
+    output:
59
+      to:
60
+        kind: ImageStreamTag
61
+        name: parent2img:latest
62
+    resources: {}
63
+    source:
64
+      git:
65
+        uri: https://github.com/openshift/ruby-hello-world.git
66
+      type: Git
67
+    strategy:
68
+      dockerStrategy:
69
+        from:
70
+          kind: ImageStreamTag
71
+          name: ruby-20-centos7:latest
72
+      type: Docker
73
+    triggers:
74
+    - github:
75
+        secret: q_ZtlnBcu7ca48ie8dNi
76
+      type: GitHub
77
+    - generic:
78
+        secret: 3kYKtANjVRCOPoM0uLNp
79
+      type: Generic
80
+    - imageChange: {}
81
+      type: ImageChange
82
+- apiVersion: v1
83
+  kind: BuildConfig
84
+  metadata:
85
+    name: parent3
86
+    namespace: test
87
+  spec:
88
+    output:
89
+      to:
90
+        kind: ImageStreamTag
91
+        name: parent3img:latest
92
+    resources: {}
93
+    source:
94
+      git:
95
+        uri: https://github.com/openshift/ruby-hello-world.git
96
+      type: Git
97
+    strategy:
98
+      dockerStrategy:
99
+        from:
100
+          kind: ImageStreamTag
101
+          name: ruby-20-centos7:latest
102
+      type: Docker
103
+    triggers:
104
+    - github:
105
+        secret: q_ZtlnBcu7ca48ie8dNi
106
+      type: GitHub
107
+    - generic:
108
+        secret: 3kYKtANjVRCOPoM0uLNp
109
+      type: Generic
110
+    - imageChange: {}
111
+      type: ImageChange
112
+- apiVersion: v1
113
+  kind: BuildConfig
114
+  metadata:
115
+    name: child1
116
+    namespace: test
117
+  spec:
118
+    output:
119
+      to:
120
+        kind: ImageStreamTag
121
+        name: child1img:latest
122
+    source:
123
+      git:
124
+        uri: https://github.com/openshift/ruby-hello-world.git
125
+      type: Git
126
+    strategy:
127
+      sourceStrategy:
128
+        from:
129
+          kind: ImageStreamTag
130
+          name: parent1img:latest
131
+      type: Source
132
+    triggers:
133
+    - github:
134
+        secret: secret101
135
+      type: GitHub
136
+    - generic:
137
+        secret: secret101
138
+      type: Generic
139
+    - imageChange:
140
+         from:
141
+           name: parent2img:latest
142
+           kind: ImageStreamTag
143
+      type: ImageChange
144
+- apiVersion: v1
145
+  kind: BuildConfig
146
+  metadata:
147
+    name: child2
148
+    namespace: test
149
+  spec:
150
+    output:
151
+      to:
152
+        kind: ImageStreamTag
153
+        name: child2img:latest
154
+    source:
155
+      git:
156
+        uri: https://github.com/openshift/ruby-hello-world.git
157
+      type: Git
158
+    strategy:
159
+      sourceStrategy:
160
+        from:
161
+          kind: DockerImage
162
+          name: openshift/ruby-20-centos7:latest
163
+      type: Source
164
+    triggers:
165
+    - github:
166
+        secret: secret101
167
+      type: GitHub
168
+    - generic:
169
+        secret: secret101
170
+      type: Generic
171
+    - imageChange:
172
+         from:
173
+           name: parent1img:latest
174
+           kind: ImageStreamTag
175
+      type: ImageChange
176
+    - imageChange:
177
+         from:
178
+           name: parent3img:latest
179
+           kind: ImageStreamTag
180
+      type: ImageChange
181
+- apiVersion: v1
182
+  kind: BuildConfig
183
+  metadata:
184
+    name: child3
185
+    namespace: test
186
+  spec:
187
+    output:
188
+      to:
189
+        kind: ImageStreamTag
190
+        name: child3img:latest
191
+    source:
192
+      git:
193
+        uri: https://github.com/openshift/ruby-hello-world.git
194
+      type: Git
195
+    strategy:
196
+      sourceStrategy:
197
+        from:
198
+          kind: ImageStreamTag
199
+          name: parent1img:latest
200
+      type: Source
201
+    triggers:
202
+    - github:
203
+        secret: secret101
204
+      type: GitHub
205
+    - generic:
206
+        secret: secret101
207
+      type: Generic
208
+    - imageChange:
209
+         from:
210
+           name: parent2img:latest
211
+           kind: ImageStreamTag
212
+      type: ImageChange
213
+    - imageChange:
214
+         from:
215
+           name: parent3img:latest
216
+           kind: ImageStreamTag
217
+      type: ImageChange
218
+kind: List
219
+metadata: {}
... ...
@@ -556,6 +556,7 @@ _oadm_build-chain()
556 556
     flags+=("-h")
557 557
     flags+=("--output=")
558 558
     two_word_flags+=("-o")
559
+    flags+=("--trigger-only")
559 560
 
560 561
     must_have_one_flag=()
561 562
     must_have_one_noun=()
... ...
@@ -934,6 +934,7 @@ _openshift_admin_build-chain()
934 934
     flags+=("-h")
935 935
     flags+=("--output=")
936 936
     two_word_flags+=("-o")
937
+    flags+=("--trigger-only")
937 938
 
938 939
     must_have_one_flag=()
939 940
     must_have_one_noun=()
... ...
@@ -4045,6 +4046,7 @@ _openshift_ex_build-chain()
4045 4045
     flags+=("-h")
4046 4046
     flags+=("--output=")
4047 4047
     two_word_flags+=("-o")
4048
+    flags+=("--trigger-only")
4048 4049
 
4049 4050
     must_have_one_flag=()
4050 4051
     must_have_one_noun=()
... ...
@@ -316,3 +316,169 @@ func runTest(t *testing.T, testname string, clusterAdminClient *client.Client, i
316 316
 		t.Errorf("unexpected trigger id: expected %v, got %v", e, a)
317 317
 	}
318 318
 }
319
+
320
+func TestMultipleImageChangeBuildTriggers(t *testing.T) {
321
+	mockImageStream := func(name, tag string) *imageapi.ImageStream {
322
+		return &imageapi.ImageStream{
323
+			ObjectMeta: kapi.ObjectMeta{Name: name},
324
+			Spec: imageapi.ImageStreamSpec{
325
+				DockerImageRepository: "registry:5000/openshift/" + name,
326
+				Tags: map[string]imageapi.TagReference{
327
+					tag: {
328
+						From: &kapi.ObjectReference{
329
+							Kind: "DockerImage",
330
+							Name: "registry:5000/openshift/" + name + ":" + tag,
331
+						},
332
+					},
333
+				},
334
+			},
335
+		}
336
+
337
+	}
338
+	mockStreamMapping := func(name, tag string) *imageapi.ImageStreamMapping {
339
+		return &imageapi.ImageStreamMapping{
340
+			ObjectMeta: kapi.ObjectMeta{Name: name},
341
+			Tag:        tag,
342
+			Image: imageapi.Image{
343
+				ObjectMeta: kapi.ObjectMeta{
344
+					Name: name,
345
+				},
346
+				DockerImageReference: "registry:5000/openshift/" + name + ":" + tag,
347
+			},
348
+		}
349
+
350
+	}
351
+	multipleImageChangeBuildConfig := func() *buildapi.BuildConfig {
352
+		strategy := stiStrategy("ImageStreamTag", "image1:tag1")
353
+		bc := imageChangeBuildConfig("multi-image-trigger", strategy)
354
+		bc.Spec.BuildSpec.Output.To.Name = "image1:outputtag"
355
+		bc.Spec.Triggers = []buildapi.BuildTriggerPolicy{
356
+			{
357
+				Type:        buildapi.ImageChangeBuildTriggerType,
358
+				ImageChange: &buildapi.ImageChangeTrigger{},
359
+			},
360
+			{
361
+				Type: buildapi.ImageChangeBuildTriggerType,
362
+				ImageChange: &buildapi.ImageChangeTrigger{
363
+					From: &kapi.ObjectReference{
364
+						Name: "image2:tag2",
365
+						Kind: "ImageStreamTag",
366
+					},
367
+				},
368
+			},
369
+			{
370
+				Type: buildapi.ImageChangeBuildTriggerType,
371
+				ImageChange: &buildapi.ImageChangeTrigger{
372
+					From: &kapi.ObjectReference{
373
+						Name: "image3:tag3",
374
+						Kind: "ImageStreamTag",
375
+					},
376
+				},
377
+			},
378
+		}
379
+		return bc
380
+	}
381
+	clusterAdminClient := setup(t)
382
+	config := multipleImageChangeBuildConfig()
383
+	triggersToTest := []struct {
384
+		triggerIndex int
385
+		name         string
386
+		tag          string
387
+	}{
388
+		{
389
+			triggerIndex: 0,
390
+			name:         "image1",
391
+			tag:          "tag1",
392
+		},
393
+		{
394
+			triggerIndex: 1,
395
+			name:         "image2",
396
+			tag:          "tag2",
397
+		},
398
+		{
399
+			triggerIndex: 2,
400
+			name:         "image3",
401
+			tag:          "tag3",
402
+		},
403
+	}
404
+
405
+	created, err := clusterAdminClient.BuildConfigs(testutil.Namespace()).Create(config)
406
+	if err != nil {
407
+		t.Fatalf("Couldn't create BuildConfig: %v", err)
408
+	}
409
+	watch, err := clusterAdminClient.Builds(testutil.Namespace()).Watch(labels.Everything(), fields.Everything(), created.ResourceVersion)
410
+	if err != nil {
411
+		t.Fatalf("Couldn't subscribe to Builds %v", err)
412
+	}
413
+	defer watch.Stop()
414
+
415
+	watch2, err := clusterAdminClient.BuildConfigs(testutil.Namespace()).Watch(labels.Everything(), fields.Everything(), created.ResourceVersion)
416
+	if err != nil {
417
+		t.Fatalf("Couldn't subscribe to BuildConfigs %v", err)
418
+	}
419
+	defer watch2.Stop()
420
+
421
+	for _, tc := range triggersToTest {
422
+		imageStream := mockImageStream(tc.name, tc.tag)
423
+		imageStreamMapping := mockStreamMapping(tc.name, tc.tag)
424
+		imageStream, err = clusterAdminClient.ImageStreams(testutil.Namespace()).Create(imageStream)
425
+		if err != nil {
426
+			t.Fatalf("Couldn't create ImageStream: %v", err)
427
+		}
428
+
429
+		err = clusterAdminClient.ImageStreamMappings(testutil.Namespace()).Create(imageStreamMapping)
430
+		if err != nil {
431
+			t.Fatalf("Couldn't create Image: %v", err)
432
+		}
433
+		// wait for initial build event from the creation of the imagerepo
434
+		event := <-watch.ResultChan()
435
+		if e, a := watchapi.Added, event.Type; e != a {
436
+			t.Fatalf("expected watch event type %s, got %s", e, a)
437
+		}
438
+		newBuild := event.Object.(*buildapi.Build)
439
+		trigger := config.Spec.Triggers[tc.triggerIndex]
440
+		if trigger.ImageChange.From == nil {
441
+			switch newBuild.Spec.Strategy.Type {
442
+			case buildapi.SourceBuildStrategyType:
443
+				if newBuild.Spec.Strategy.SourceStrategy.From.Name != "registry:5000/openshift/"+tc.name+":"+tc.tag {
444
+					i, _ := clusterAdminClient.ImageStreams(testutil.Namespace()).Get(imageStream.Name)
445
+					bc, _ := clusterAdminClient.BuildConfigs(testutil.Namespace()).Get(config.Name)
446
+					t.Fatalf("Expected build with base image %s, got %s\n, imagerepo is %v\ntrigger is %#v", "registry:5000/openshift/"+tc.name+":"+tc.tag, newBuild.Spec.Strategy.DockerStrategy.From.Name, i, bc.Spec.Triggers[tc.triggerIndex].ImageChange)
447
+				}
448
+			case buildapi.DockerBuildStrategyType:
449
+				if newBuild.Spec.Strategy.DockerStrategy.From.Name != "registry:8080/openshift/"+tc.name+":"+tc.tag {
450
+					i, _ := clusterAdminClient.ImageStreams(testutil.Namespace()).Get(imageStream.Name)
451
+					bc, _ := clusterAdminClient.BuildConfigs(testutil.Namespace()).Get(config.Name)
452
+					t.Fatalf("Expected build with base image %s, got %s\n, imagerepo is %v\ntrigger is %#v", "registry:5000/openshift/"+tc.name+":"+tag, newBuild.Spec.Strategy.DockerStrategy.From.Name, i, bc.Spec.Triggers[tc.triggerIndex].ImageChange)
453
+				}
454
+			case buildapi.CustomBuildStrategyType:
455
+				if newBuild.Spec.Strategy.CustomStrategy.From.Name != "registry:8080/openshift/"+tc.name+":"+tag {
456
+					i, _ := clusterAdminClient.ImageStreams(testutil.Namespace()).Get(imageStream.Name)
457
+					bc, _ := clusterAdminClient.BuildConfigs(testutil.Namespace()).Get(config.Name)
458
+					t.Fatalf("Expected build with base image %s, got %s\n, imagerepo is %v\ntrigger is %#v", "registry:5000/openshift/"+tc.name+":"+tag, newBuild.Spec.Strategy.DockerStrategy.From.Name, i, bc.Spec.Triggers[tc.triggerIndex].ImageChange)
459
+				}
460
+
461
+			}
462
+		}
463
+		event = <-watch.ResultChan()
464
+		if e, a := watchapi.Modified, event.Type; e != a {
465
+			t.Fatalf("expected watch event type %s, got %s", e, a)
466
+		}
467
+		newBuild = event.Object.(*buildapi.Build)
468
+		// Make sure the resolution of the build's docker image pushspec didn't mutate the persisted API object
469
+		if newBuild.Spec.Output.To.Name != "image1:outputtag" {
470
+			t.Fatalf("unexpected build output: %#v %#v", newBuild.Spec.Output.To, newBuild.Spec.Output)
471
+		}
472
+
473
+		// wait for build config to be updated
474
+		<-watch2.ResultChan()
475
+		updatedConfig, err := clusterAdminClient.BuildConfigs(testutil.Namespace()).Get(config.Name)
476
+		if err != nil {
477
+			t.Fatalf("Couldn't get BuildConfig: %v", err)
478
+		}
479
+		// the first tag did not have an image id, so the last trigger field is the pull spec
480
+		if updatedConfig.Spec.Triggers[tc.triggerIndex].ImageChange.LastTriggeredImageID != "registry:5000/openshift/"+tc.name+":"+tc.tag {
481
+			t.Fatalf("Expected imageID equal to pull spec, got %#v", updatedConfig.Spec.Triggers[0].ImageChange)
482
+		}
483
+	}
484
+}