Browse code

reject build requests for binary builds if not providing binary inputs

Ben Parees authored on 2016/07/02 06:56:44
Showing 9 changed files
... ...
@@ -142,7 +142,7 @@ func validateCommonSpec(spec *buildapi.CommonSpec, fldPath *field.Path) field.Er
142 142
 	s := spec.Strategy
143 143
 
144 144
 	if s.DockerStrategy != nil && s.JenkinsPipelineStrategy == nil && spec.Source.Git == nil && spec.Source.Binary == nil && spec.Source.Dockerfile == nil && spec.Source.Images == nil {
145
-		allErrs = append(allErrs, field.Invalid(fldPath.Child("source"), spec.Source, "must provide a value for at least one of source, binary, images, or dockerfile"))
145
+		allErrs = append(allErrs, field.Invalid(fldPath.Child("source"), "", "must provide a value for at least one source input(git, binary, dockerfile, images)."))
146 146
 	}
147 147
 
148 148
 	allErrs = append(allErrs,
... ...
@@ -202,7 +202,6 @@ func validateSource(input *buildapi.BuildSource, isCustomStrategy, isDockerStrat
202 202
 			allErrs = append(allErrs, validateImageSource(image, fldPath.Child("images").Index(i))...)
203 203
 		}
204 204
 	}
205
-
206 205
 	if isJenkinsPipelineStrategyFromRepo && input.Git == nil {
207 206
 		allErrs = append(allErrs, field.Invalid(fldPath.Child("git"), "", "must be set when using Jenkins Pipeline strategy with Jenkinsfile from a git repo"))
208 207
 	}
... ...
@@ -46,7 +46,7 @@ func TestBuildValidationSuccess(t *testing.T) {
46 46
 
47 47
 func checkDockerStrategyEmptySourceError(result field.ErrorList) bool {
48 48
 	for _, err := range result {
49
-		if err.Type == field.ErrorTypeInvalid && strings.Contains(err.Field, "spec.source") && strings.Contains(err.Detail, "must provide a value for at least one of source, binary, images, or dockerfile") {
49
+		if err.Type == field.ErrorTypeInvalid && strings.Contains(err.Field, "spec.source") && strings.Contains(err.Detail, "must provide a value for at least one source input(git, binary, dockerfile, images).") {
50 50
 			return true
51 51
 		}
52 52
 	}
... ...
@@ -1632,7 +1632,7 @@ func TestValidateCommonSpec(t *testing.T) {
1632 1632
 				},
1633 1633
 			},
1634 1634
 		},
1635
-		// 17
1635
+		// 18
1636 1636
 		// dockerfilePath can't equal ..
1637 1637
 		{
1638 1638
 			string(field.ErrorTypeInvalid) + "strategy.dockerStrategy.dockerfilePath",
... ...
@@ -1656,7 +1656,7 @@ func TestValidateCommonSpec(t *testing.T) {
1656 1656
 				},
1657 1657
 			},
1658 1658
 		},
1659
-		// 18
1659
+		// 19
1660 1660
 		{
1661 1661
 			string(field.ErrorTypeInvalid) + "postCommit",
1662 1662
 			buildapi.CommonSpec{
... ...
@@ -1674,7 +1674,7 @@ func TestValidateCommonSpec(t *testing.T) {
1674 1674
 				},
1675 1675
 			},
1676 1676
 		},
1677
-		// 19
1677
+		// 20
1678 1678
 		{
1679 1679
 			string(field.ErrorTypeInvalid) + "source.git",
1680 1680
 			buildapi.CommonSpec{
... ...
@@ -1683,7 +1683,7 @@ func TestValidateCommonSpec(t *testing.T) {
1683 1683
 				},
1684 1684
 			},
1685 1685
 		},
1686
-		// 20
1686
+		// 21
1687 1687
 		{
1688 1688
 			string(field.ErrorTypeInvalid) + "source.git",
1689 1689
 			buildapi.CommonSpec{
... ...
@@ -1694,7 +1694,7 @@ func TestValidateCommonSpec(t *testing.T) {
1694 1694
 				},
1695 1695
 			},
1696 1696
 		},
1697
-		// 21
1697
+		// 22
1698 1698
 		// jenkinsfilePath can't be an absolute path
1699 1699
 		{
1700 1700
 			string(field.ErrorTypeInvalid) + "strategy.jenkinsPipelineStrategy.jenkinsfilePath",
... ...
@@ -1711,7 +1711,7 @@ func TestValidateCommonSpec(t *testing.T) {
1711 1711
 				},
1712 1712
 			},
1713 1713
 		},
1714
-		// 22
1714
+		// 23
1715 1715
 		// jenkinsfilePath can't start with ../
1716 1716
 		{
1717 1717
 			string(field.ErrorTypeInvalid) + "strategy.jenkinsPipelineStrategy.jenkinsfilePath",
... ...
@@ -1728,7 +1728,7 @@ func TestValidateCommonSpec(t *testing.T) {
1728 1728
 				},
1729 1729
 			},
1730 1730
 		},
1731
-		// 23
1731
+		// 24
1732 1732
 		// jenkinsfilePath can't be a reference a path outside of the dir
1733 1733
 		{
1734 1734
 			string(field.ErrorTypeInvalid) + "strategy.jenkinsPipelineStrategy.jenkinsfilePath",
... ...
@@ -1745,7 +1745,7 @@ func TestValidateCommonSpec(t *testing.T) {
1745 1745
 				},
1746 1746
 			},
1747 1747
 		},
1748
-		// 24
1748
+		// 25
1749 1749
 		// jenkinsfilePath can't be equal to ..
1750 1750
 		{
1751 1751
 			string(field.ErrorTypeInvalid) + "strategy.jenkinsPipelineStrategy.jenkinsfilePath",
... ...
@@ -1762,7 +1762,7 @@ func TestValidateCommonSpec(t *testing.T) {
1762 1762
 				},
1763 1763
 			},
1764 1764
 		},
1765
-		// 25
1765
+		// 26
1766 1766
 		// path must be shorter than 100k
1767 1767
 		{
1768 1768
 			string(field.ErrorTypeInvalid) + "strategy.jenkinsPipelineStrategy.jenkinsfile",
... ...
@@ -302,12 +302,16 @@ func (m *mockBuildConfigUpdater) Update(buildcfg *buildapi.BuildConfig) error {
302 302
 }
303 303
 
304 304
 func mockBuildConfig(baseImage, triggerImage, repoName, repoTag string) *buildapi.BuildConfig {
305
+	dockerfile := "FROM foo"
305 306
 	return &buildapi.BuildConfig{
306 307
 		ObjectMeta: kapi.ObjectMeta{
307 308
 			Name: "testBuildCfg",
308 309
 		},
309 310
 		Spec: buildapi.BuildConfigSpec{
310 311
 			CommonSpec: buildapi.CommonSpec{
312
+				Source: buildapi.BuildSource{
313
+					Dockerfile: &dockerfile,
314
+				},
311 315
 				Strategy: buildapi.BuildStrategy{
312 316
 					DockerStrategy: &buildapi.DockerBuildStrategy{
313 317
 						From: &kapi.ObjectReference{
... ...
@@ -218,9 +218,8 @@ func (g *BuildGenerator) Instantiate(ctx kapi.Context, request *buildapi.BuildRe
218 218
 	if err != nil {
219 219
 		return nil, err
220 220
 	}
221
-
222 221
 	if buildutil.IsPaused(bc) {
223
-		return nil, errors.NewInternalError(&GeneratorFatalError{fmt.Sprintf("can't instantiate from BuildConfig %s/%s: BuildConfig is paused", bc.Namespace, bc.Name)})
222
+		return nil, errors.NewBadRequest(fmt.Sprintf("can't instantiate from BuildConfig %s/%s: BuildConfig is paused", bc.Namespace, bc.Name))
224 223
 	}
225 224
 
226 225
 	if err := g.checkLastVersion(bc, request.LastVersion); err != nil {
... ...
@@ -333,7 +332,6 @@ func (g *BuildGenerator) Clone(ctx kapi.Context, request *buildapi.BuildRequest)
333 333
 		if err != nil && !errors.IsNotFound(err) {
334 334
 			return nil, err
335 335
 		}
336
-
337 336
 		if buildutil.IsPaused(buildConfig) {
338 337
 			return nil, errors.NewInternalError(&GeneratorFatalError{fmt.Sprintf("can't instantiate from BuildConfig %s/%s: BuildConfig is paused", buildConfig.Namespace, buildConfig.Name)})
339 338
 		}
... ...
@@ -366,7 +364,6 @@ func (g *BuildGenerator) createBuild(ctx kapi.Context, build *buildapi.Build) (*
366 366
 		return nil, errors.NewConflict(buildapi.Resource("build"), build.Namespace, fmt.Errorf("Build.Namespace does not match the provided context"))
367 367
 	}
368 368
 	kapi.FillObjectMetaSystemFields(ctx, &build.ObjectMeta)
369
-
370 369
 	err := g.Client.CreateBuild(ctx, build)
371 370
 	if err != nil {
372 371
 		return nil, err
... ...
@@ -416,13 +413,15 @@ func (g *BuildGenerator) generateBuildFromConfig(ctx kapi.Context, bc *buildapi.
416 416
 			},
417 417
 		},
418 418
 	}
419
-
420 419
 	if binary != nil {
421 420
 		build.Spec.Source.Git = nil
422 421
 		build.Spec.Source.Binary = binary
423 422
 		if build.Spec.Source.Dockerfile != nil && binary.AsFile == "Dockerfile" {
424 423
 			build.Spec.Source.Dockerfile = nil
425 424
 		}
425
+	} else {
426
+		// must explicitly set this because we copied the source values from the buildconfig.
427
+		build.Spec.Source.Binary = nil
426 428
 	}
427 429
 
428 430
 	build.Name = getNextBuildName(bc)
... ...
@@ -698,6 +697,8 @@ func generateBuildFromBuild(build *buildapi.Build, buildConfig *buildapi.BuildCo
698 698
 			Config: buildCopy.Status.Config,
699 699
 		},
700 700
 	}
701
+	// TODO remove/update this when we support cloning binary builds
702
+	newBuild.Spec.Source.Binary = nil
701 703
 	if newBuild.Annotations == nil {
702 704
 		newBuild.Annotations = make(map[string]string)
703 705
 	}
... ...
@@ -48,6 +48,25 @@ func TestInstantiate(t *testing.T) {
48 48
 	}
49 49
 }
50 50
 
51
+func TestInstantiateBinary(t *testing.T) {
52
+	generator := mockBuildGenerator()
53
+	build, err := generator.Instantiate(kapi.NewDefaultContext(), &buildapi.BuildRequest{Binary: &buildapi.BinaryBuildSource{}})
54
+	if err != nil {
55
+		t.Errorf("Unexpected error %v", err)
56
+	}
57
+	if build.Spec.Source.Binary == nil {
58
+		t.Errorf("build should have a binary source value, has nil")
59
+	}
60
+	build, err = generator.Clone(kapi.NewDefaultContext(), &buildapi.BuildRequest{Binary: &buildapi.BinaryBuildSource{}})
61
+	if err != nil {
62
+		t.Errorf("Unexpected error %v", err)
63
+	}
64
+	// TODO: we should enable this flow.
65
+	if build.Spec.Source.Binary != nil {
66
+		t.Errorf("build should not have a binary source value, has %v", build.Spec.Source.Binary)
67
+	}
68
+}
69
+
51 70
 // TODO(agoldste): I'm not sure the intent of this test. Using the previous logic for
52 71
 // the generator, which would try to update the build config before creating
53 72
 // the build, I can see why the UpdateBuildConfigFunc is set up to return an
... ...
@@ -83,6 +102,7 @@ func TestInstantiateRetry(t *testing.T) {
83 83
 */
84 84
 
85 85
 func TestInstantiateDeletingError(t *testing.T) {
86
+	source := mocks.MockSource()
86 87
 	generator := BuildGenerator{Client: Client{
87 88
 		GetBuildConfigFunc: func(ctx kapi.Context, name string) (*buildapi.BuildConfig, error) {
88 89
 			bc := &buildapi.BuildConfig{
... ...
@@ -91,11 +111,31 @@ func TestInstantiateDeletingError(t *testing.T) {
91 91
 						buildapi.BuildConfigPausedAnnotation: "true",
92 92
 					},
93 93
 				},
94
+				Spec: buildapi.BuildConfigSpec{
95
+					CommonSpec: buildapi.CommonSpec{
96
+						Source: source,
97
+						Revision: &buildapi.SourceRevision{
98
+							Git: &buildapi.GitSourceRevision{
99
+								Commit: "1234",
100
+							},
101
+						},
102
+					},
103
+				},
94 104
 			}
95 105
 			return bc, nil
96 106
 		},
97 107
 		GetBuildFunc: func(ctx kapi.Context, name string) (*buildapi.Build, error) {
98 108
 			build := &buildapi.Build{
109
+				Spec: buildapi.BuildSpec{
110
+					CommonSpec: buildapi.CommonSpec{
111
+						Source: source,
112
+						Revision: &buildapi.SourceRevision{
113
+							Git: &buildapi.GitSourceRevision{
114
+								Commit: "1234",
115
+							},
116
+						},
117
+					},
118
+				},
99 119
 				Status: buildapi.BuildStatus{
100 120
 					Config: &kapi.ObjectReference{
101 121
 						Name: "buildconfig",
... ...
@@ -115,6 +155,61 @@ func TestInstantiateDeletingError(t *testing.T) {
115 115
 	}
116 116
 }
117 117
 
118
+// TestInstantiateBinaryClear ensures that when instantiating or cloning from a buildconfig/build
119
+// that has a binary source value, the resulting build does not have a binary source value
120
+// (because the request did not include one)
121
+func TestInstantiateBinaryRemoved(t *testing.T) {
122
+	generator := mockBuildGenerator()
123
+	client := generator.Client.(Client)
124
+	client.GetBuildConfigFunc = func(ctx kapi.Context, name string) (*buildapi.BuildConfig, error) {
125
+		bc := &buildapi.BuildConfig{
126
+			ObjectMeta: kapi.ObjectMeta{
127
+				Annotations: map[string]string{},
128
+			},
129
+			Spec: buildapi.BuildConfigSpec{
130
+				CommonSpec: buildapi.CommonSpec{
131
+					Source: buildapi.BuildSource{
132
+						Binary: &buildapi.BinaryBuildSource{},
133
+					},
134
+				},
135
+			},
136
+		}
137
+		return bc, nil
138
+	}
139
+	client.GetBuildFunc = func(ctx kapi.Context, name string) (*buildapi.Build, error) {
140
+		build := &buildapi.Build{
141
+			Spec: buildapi.BuildSpec{
142
+				CommonSpec: buildapi.CommonSpec{
143
+					Source: buildapi.BuildSource{
144
+						Binary: &buildapi.BinaryBuildSource{},
145
+					},
146
+				},
147
+			},
148
+			Status: buildapi.BuildStatus{
149
+				Config: &kapi.ObjectReference{
150
+					Name: "buildconfig",
151
+				},
152
+			},
153
+		}
154
+		return build, nil
155
+	}
156
+
157
+	build, err := generator.Instantiate(kapi.NewDefaultContext(), &buildapi.BuildRequest{})
158
+	if err != nil {
159
+		t.Errorf("Unexpected error %v", err)
160
+	}
161
+	if build.Spec.Source.Binary != nil {
162
+		t.Errorf("build should not have a binary source value, has %v", build.Spec.Source.Binary)
163
+	}
164
+	build, err = generator.Clone(kapi.NewDefaultContext(), &buildapi.BuildRequest{})
165
+	if err != nil {
166
+		t.Errorf("Unexpected error %v", err)
167
+	}
168
+	if build.Spec.Source.Binary != nil {
169
+		t.Errorf("build should not have a binary source value, has %v", build.Spec.Source.Binary)
170
+	}
171
+}
172
+
118 173
 func TestInstantiateGetBuildConfigError(t *testing.T) {
119 174
 	generator := BuildGenerator{Client: Client{
120 175
 		GetBuildConfigFunc: func(ctx kapi.Context, name string) (*buildapi.BuildConfig, error) {
... ...
@@ -240,6 +335,7 @@ func TestInstantiateWithImageTrigger(t *testing.T) {
240 240
 		},
241 241
 	}
242 242
 
243
+	source := mocks.MockSource()
243 244
 	for _, tc := range tests {
244 245
 		bc := &buildapi.BuildConfig{
245 246
 			Spec: buildapi.BuildConfigSpec{
... ...
@@ -252,6 +348,12 @@ func TestInstantiateWithImageTrigger(t *testing.T) {
252 252
 							},
253 253
 						},
254 254
 					},
255
+					Source: source,
256
+					Revision: &buildapi.SourceRevision{
257
+						Git: &buildapi.GitSourceRevision{
258
+							Commit: "1234",
259
+						},
260
+					},
255 261
 				},
256 262
 				Triggers: tc.triggers,
257 263
 			},
... ...
@@ -19,6 +19,7 @@ import (
19 19
 	"github.com/spf13/cobra"
20 20
 
21 21
 	kapi "k8s.io/kubernetes/pkg/api"
22
+	kerrors "k8s.io/kubernetes/pkg/api/errors"
22 23
 	"k8s.io/kubernetes/pkg/api/unversioned"
23 24
 	"k8s.io/kubernetes/pkg/client/restclient"
24 25
 	kclientcmd "k8s.io/kubernetes/pkg/client/unversioned/clientcmd"
... ...
@@ -30,7 +31,7 @@ import (
30 30
 	cmdutil "github.com/openshift/origin/pkg/cmd/util"
31 31
 	"github.com/openshift/origin/pkg/cmd/util/clientcmd"
32 32
 	"github.com/openshift/origin/pkg/generate/git"
33
-	"github.com/openshift/origin/pkg/util/errors"
33
+	oerrors "github.com/openshift/origin/pkg/util/errors"
34 34
 	"github.com/openshift/source-to-image/pkg/tar"
35 35
 )
36 36
 
... ...
@@ -165,6 +166,11 @@ func (o *StartBuildOptions) Complete(f *clientcmd.Factory, in io.Reader, out io.
165 165
 		return kcmdutil.UsageError(cmd, "Must pass a name of a build config or specify build name with '--from-build' flag")
166 166
 	}
167 167
 
168
+	if len(buildName) != 0 && (len(fromFile) != 0 || len(fromDir) != 0 || len(fromRepo) != 0) {
169
+		// TODO: we should support this, it should be possible to clone a build to run again with new uploaded artifacts.
170
+		// Doing so requires introducing a new clonebinary endpoint.
171
+		return kcmdutil.UsageError(cmd, "Cannot use '--from-build' flag with binary builds")
172
+	}
168 173
 	o.AsBinary = len(fromFile) > 0 || len(fromDir) > 0 || len(fromRepo) > 0
169 174
 
170 175
 	namespace, _, err := f.DefaultNamespace()
... ...
@@ -282,10 +288,16 @@ func (o *StartBuildOptions) Run() error {
282 282
 		}
283 283
 	case len(o.FromBuild) > 0:
284 284
 		if newBuild, err = o.Client.Builds(o.Namespace).Clone(request); err != nil {
285
+			if isInvalidSourceInputsError(err) {
286
+				return fmt.Errorf("Build %s/%s has no valid source inputs and '--from-build' cannot be used for binary builds", o.Namespace, o.Name)
287
+			}
285 288
 			return err
286 289
 		}
287 290
 	default:
288 291
 		if newBuild, err = o.Client.BuildConfigs(o.Namespace).Instantiate(request); err != nil {
292
+			if isInvalidSourceInputsError(err) {
293
+				return fmt.Errorf("Build configuration %s/%s has no valid source inputs, if this is a binary build you must specify one of '--from-dir', '--from-repo', or '--from-file'", o.Namespace, o.Name)
294
+			}
289 295
 			return err
290 296
 		}
291 297
 	}
... ...
@@ -327,7 +339,7 @@ func (o *StartBuildOptions) Run() error {
327 327
 				if err != nil {
328 328
 					// if --wait options is set, then retry the connection to build logs
329 329
 					// when we hit the timeout.
330
-					if o.WaitForComplete && errors.IsTimeoutErr(err) {
330
+					if o.WaitForComplete && oerrors.IsTimeoutErr(err) {
331 331
 						continue
332 332
 					}
333 333
 					fmt.Fprintf(o.ErrOut, "error getting logs: %v\n", err)
... ...
@@ -717,3 +729,18 @@ func WaitForBuildComplete(c osclient.BuildInterface, name string) error {
717 717
 		}
718 718
 	}
719 719
 }
720
+
721
+func isInvalidSourceInputsError(err error) bool {
722
+	if err != nil {
723
+		if statusErr, ok := err.(*kerrors.StatusError); ok {
724
+			if kerrors.IsInvalid(statusErr) {
725
+				for _, cause := range statusErr.ErrStatus.Details.Causes {
726
+					if cause.Field == "spec.source" {
727
+						return true
728
+					}
729
+				}
730
+			}
731
+		}
732
+	}
733
+	return false
734
+}
... ...
@@ -163,6 +163,9 @@ started=$(oc start-build ruby-sample-build-invalidtag)
163 163
 os::cmd::expect_success_and_text "oc describe build ${started}" 'centos/ruby-22-centos7$'
164 164
 frombuild=$(oc start-build --from-build="${started}")
165 165
 os::cmd::expect_success_and_text "oc describe build ${frombuild}" 'centos/ruby-22-centos7$'
166
+os::cmd::expect_failure_and_text "oc start-build ruby-sample-build-invalid-tag --from-dir=. --from-build=${started}" "Cannot use '--from-build' flag with binary builds"
167
+os::cmd::expect_failure_and_text "oc start-build ruby-sample-build-invalid-tag --from-file=. --from-build=${started}" "Cannot use '--from-build' flag with binary builds"
168
+os::cmd::expect_failure_and_text "oc start-build ruby-sample-build-invalid-tag --from-repo=. --from-build=${started}" "Cannot use '--from-build' flag with binary builds"
166 169
 echo "start-build: ok"
167 170
 os::test::junit::declare_suite_end
168 171
 
... ...
@@ -155,6 +155,34 @@ var _ = g.Describe("[builds][Slow] starting a build using CLI", func() {
155 155
 			}
156 156
 			o.Expect(err).NotTo(o.HaveOccurred())
157 157
 		})
158
+
159
+		// run one valid binary build so we can do --from-build later
160
+		g.It("should reject binary build requests without a --from-xxxx value", func() {
161
+			g.By("starting a valid build with a directory")
162
+			out, err := oc.Run("start-build").Args("sample-build-binary", "--follow", "--wait", fmt.Sprintf("--from-dir=%s", exampleBuild)).Output()
163
+			g.By(fmt.Sprintf("verifying the build %q status", out))
164
+			o.Expect(err).NotTo(o.HaveOccurred())
165
+			o.Expect(out).To(o.ContainSubstring("Uploading directory"))
166
+			o.Expect(out).To(o.ContainSubstring("as binary input for the build ..."))
167
+			o.Expect(out).To(o.ContainSubstring("Your bundle is complete"))
168
+
169
+			err = exutil.WaitForABuild(oc.REST().Builds(oc.Namespace()), "sample-build-binary-1", exutil.CheckBuildSuccessFn, exutil.CheckBuildFailedFn)
170
+			if err != nil {
171
+				exutil.DumpBuildLogs("sample-build-binary", oc)
172
+			}
173
+			o.Expect(err).NotTo(o.HaveOccurred())
174
+
175
+			g.By("starting a build without a --from-xxxx value")
176
+			out, err = oc.Run("start-build").Args("sample-build-binary").Output()
177
+			o.Expect(err).To(o.HaveOccurred())
178
+			o.Expect(out).To(o.ContainSubstring("has no valid source inputs"))
179
+
180
+			g.By("starting a build from an existing binary build")
181
+			out, err = oc.Run("start-build").Args("sample-build-binary", fmt.Sprintf("--from-build=%s", "sample-build-binary-1")).Output()
182
+			o.Expect(err).To(o.HaveOccurred())
183
+			o.Expect(out).To(o.ContainSubstring("has no valid source inputs"))
184
+
185
+		})
158 186
 	})
159 187
 
160 188
 	g.Describe("cancelling build started by oc start-build --wait", func() {
... ...
@@ -53,6 +53,43 @@
53 53
       "status": {
54 54
         "lastVersion": 0
55 55
       }
56
+    },
57
+    {
58
+      "kind": "BuildConfig",
59
+      "apiVersion": "v1",
60
+      "metadata": {
61
+        "name": "sample-build-binary",
62
+        "creationTimestamp": null
63
+      },
64
+      "spec": {
65
+        "triggers": [
66
+          {
67
+            "type": "imageChange",
68
+            "imageChange": {}
69
+          }
70
+        ],
71
+        "source": {
72
+          "type": "Binary",
73
+          "binary": {}
74
+        },
75
+        "strategy": {
76
+          "type": "Docker",
77
+          "dockerStrategy": {
78
+            "env": [
79
+              { "name": "FOO", "value": "test" },
80
+              { "name": "BAR", "value": "test" }
81
+            ],
82
+            "from": {
83
+              "kind": "DockerImage",
84
+              "name": "centos/ruby-22-centos7"
85
+            }
86
+          }
87
+        },
88
+        "resources": {}
89
+      },
90
+      "status": {
91
+        "lastVersion": 0
92
+      }
56 93
     }
57 94
   ]
58 95
 }