1package exec_test
2
3import (
4	"context"
5	"errors"
6
7	"code.cloudfoundry.org/lager"
8	"github.com/concourse/concourse/atc"
9	"github.com/concourse/concourse/atc/db"
10	"github.com/concourse/concourse/atc/db/lock/lockfakes"
11	"github.com/concourse/concourse/atc/exec"
12	"github.com/concourse/concourse/atc/exec/build"
13	"github.com/concourse/concourse/atc/exec/execfakes"
14	"github.com/concourse/concourse/atc/runtime"
15	"github.com/concourse/concourse/atc/runtime/runtimefakes"
16	"github.com/concourse/concourse/atc/worker"
17	"github.com/concourse/concourse/atc/worker/workerfakes"
18	"github.com/concourse/concourse/tracing"
19	"github.com/concourse/concourse/vars"
20	"github.com/onsi/gomega/gbytes"
21	"go.opentelemetry.io/otel/api/trace"
22	"go.opentelemetry.io/otel/api/trace/testtrace"
23
24	. "github.com/onsi/ginkgo"
25	. "github.com/onsi/gomega"
26)
27
28var _ = Describe("TaskStep", func() {
29	var (
30		ctx    context.Context
31		cancel func()
32
33		stdoutBuf *gbytes.Buffer
34		stderrBuf *gbytes.Buffer
35
36		fakeClient   *workerfakes.FakeClient
37		fakeStrategy *workerfakes.FakeContainerPlacementStrategy
38
39		fakeLockFactory *lockfakes.FakeLockFactory
40
41		fakeDelegate *execfakes.FakeTaskDelegate
42		taskPlan     *atc.TaskPlan
43
44		interpolatedResourceTypes atc.VersionedResourceTypes
45
46		repo  *build.Repository
47		state *execfakes.FakeRunState
48
49		taskStep exec.Step
50		stepErr  error
51
52		buildVars *vars.BuildVariables
53
54		containerMetadata = db.ContainerMetadata{
55			WorkingDirectory: "some-artifact-root",
56			Type:             db.ContainerTypeTask,
57			StepName:         "some-step",
58		}
59
60		stepMetadata = exec.StepMetadata{
61			TeamID:  123,
62			BuildID: 1234,
63			JobID:   12345,
64		}
65
66		planID = atc.PlanID("42")
67	)
68
69	BeforeEach(func() {
70		ctx, cancel = context.WithCancel(context.Background())
71
72		stdoutBuf = gbytes.NewBuffer()
73		stderrBuf = gbytes.NewBuffer()
74
75		fakeClient = new(workerfakes.FakeClient)
76		fakeStrategy = new(workerfakes.FakeContainerPlacementStrategy)
77
78		fakeLockFactory = new(lockfakes.FakeLockFactory)
79
80		credVars := vars.StaticVariables{"source-param": "super-secret-source"}
81		buildVars = vars.NewBuildVariables(credVars, true)
82
83		fakeDelegate = new(execfakes.FakeTaskDelegate)
84		fakeDelegate.VariablesReturns(buildVars)
85		fakeDelegate.StdoutReturns(stdoutBuf)
86		fakeDelegate.StderrReturns(stderrBuf)
87
88		repo = build.NewRepository()
89		state = new(execfakes.FakeRunState)
90		state.ArtifactRepositoryReturns(repo)
91
92		uninterpolatedResourceTypes := atc.VersionedResourceTypes{
93			{
94				ResourceType: atc.ResourceType{
95					Name:   "custom-resource",
96					Type:   "custom-type",
97					Source: atc.Source{"some-custom": "((source-param))"},
98					Params: atc.Params{"some-custom": "param"},
99				},
100				Version: atc.Version{"some-custom": "version"},
101			},
102		}
103
104		interpolatedResourceTypes = atc.VersionedResourceTypes{
105			{
106				ResourceType: atc.ResourceType{
107					Name:   "custom-resource",
108					Type:   "custom-type",
109					Source: atc.Source{"some-custom": "super-secret-source"},
110					Params: atc.Params{"some-custom": "param"},
111				},
112				Version: atc.Version{"some-custom": "version"},
113			},
114		}
115
116		taskPlan = &atc.TaskPlan{
117			Name:                   "some-task",
118			Privileged:             false,
119			Tags:                   []string{"step", "tags"},
120			VersionedResourceTypes: uninterpolatedResourceTypes,
121		}
122	})
123
124	JustBeforeEach(func() {
125		plan := atc.Plan{
126			ID:   planID,
127			Task: taskPlan,
128		}
129
130		// stuff stored on task step still
131		taskStep = exec.NewTaskStep(
132			plan.ID,
133			*plan.Task,
134			atc.ContainerLimits{},
135			stepMetadata,
136			containerMetadata,
137			fakeStrategy,
138			fakeClient,
139			fakeDelegate,
140			fakeLockFactory,
141		)
142
143		stepErr = taskStep.Run(ctx, state)
144	})
145
146	Context("when the plan has a config", func() {
147
148		BeforeEach(func() {
149			cpu := uint64(1024)
150			memory := uint64(1024)
151
152			taskPlan.Config = &atc.TaskConfig{
153				Platform: "some-platform",
154				ImageResource: &atc.ImageResource{
155					Type:    "docker",
156					Source:  atc.Source{"some": "secret-source-param"},
157					Params:  atc.Params{"some": "params"},
158					Version: atc.Version{"some": "version"},
159				},
160				Limits: &atc.ContainerLimits{
161					CPU:    &cpu,
162					Memory: &memory,
163				},
164				Params: atc.TaskEnv{
165					"SECURE": "secret-task-param",
166				},
167				Run: atc.TaskRunConfig{
168					Path: "ls",
169					Args: []string{"some", "args"},
170				},
171			}
172		})
173
174		Context("before running the task container", func() {
175			BeforeEach(func() {
176				fakeDelegate.InitializingStub = func(lager.Logger) {
177					defer GinkgoRecover()
178					Expect(fakeClient.RunTaskStepCallCount()).To(BeZero())
179				}
180			})
181
182			It("invoked the delegate's Initializing callback", func() {
183				Expect(fakeDelegate.InitializingCallCount()).To(Equal(1))
184			})
185
186			Context("when rootfs uri is set instead of image resource", func() {
187				BeforeEach(func() {
188					taskPlan.Config = &atc.TaskConfig{
189						Platform:  "some-platform",
190						RootfsURI: "some-image",
191						Params:    map[string]string{"SOME": "params"},
192						Run: atc.TaskRunConfig{
193							Path: "ls",
194							Args: []string{"some", "args"},
195						},
196					}
197				})
198
199				It("correctly sets up the image spec", func() {
200					Expect(fakeClient.RunTaskStepCallCount()).To(Equal(1))
201					_, _, _, containerSpec, _, _, _, _, _, _, _ := fakeClient.RunTaskStepArgsForCall(0)
202
203					Expect(containerSpec).To(Equal(worker.ContainerSpec{
204						Platform: "some-platform",
205						Tags:     []string{"step", "tags"},
206						TeamID:   stepMetadata.TeamID,
207						ImageSpec: worker.ImageSpec{
208							ImageURL:   "some-image",
209							Privileged: false,
210						},
211						Type: "task",
212						Dir:  "some-artifact-root",
213						Env:  []string{"SOME=params"},
214
215						ArtifactByPath: map[string]runtime.Artifact{},
216						Outputs:        worker.OutputPaths{},
217					}))
218
219				})
220			})
221
222			Context("when tracing is enabled", func() {
223				var buildSpan trace.Span
224
225				BeforeEach(func() {
226					tracing.ConfigureTraceProvider(testTraceProvider{})
227					ctx, buildSpan = tracing.StartSpan(ctx, "build", nil)
228				})
229
230				It("propagates span context to the worker client", func() {
231					ctx, _, _, _, _, _, _, _, _, _, _ := fakeClient.RunTaskStepArgsForCall(0)
232					span, ok := tracing.FromContext(ctx).(*testtrace.Span)
233					Expect(ok).To(BeTrue(), "no testtrace.Span in context")
234					Expect(span.ParentSpanID()).To(Equal(buildSpan.SpanContext().SpanID))
235				})
236
237				It("populates the TRACEPARENT env var", func() {
238					_, _, _, containerSpec, _, _, _, _, _, _, _ := fakeClient.RunTaskStepArgsForCall(0)
239
240					Expect(containerSpec.Env).To(ContainElement(MatchRegexp(`TRACEPARENT=.+`)))
241				})
242
243				AfterEach(func() {
244					tracing.Configured = false
245				})
246			})
247		})
248
249		It("secrets are tracked", func() {
250			mapit := vars.TrackedVarsMap{}
251			buildVars.IterateInterpolatedCreds(mapit)
252			Expect(mapit["source-param"]).To(Equal("super-secret-source"))
253		})
254
255		It("creates a containerSpec with the correct parameters", func() {
256			Expect(fakeClient.RunTaskStepCallCount()).To(Equal(1))
257
258			_, _, _, containerSpec, _, _, _, _, _, _, _ := fakeClient.RunTaskStepArgsForCall(0)
259
260			Expect(containerSpec.Dir).To(Equal("some-artifact-root"))
261			Expect(containerSpec.User).To(BeEmpty())
262		})
263
264		It("creates the task process spec with the correct parameters", func() {
265			Expect(fakeClient.RunTaskStepCallCount()).To(Equal(1))
266
267			_, _, _, _, _, _, _, _, taskProcessSpec, _, _ := fakeClient.RunTaskStepArgsForCall(0)
268			Expect(taskProcessSpec.StdoutWriter).To(Equal(stdoutBuf))
269			Expect(taskProcessSpec.StderrWriter).To(Equal(stderrBuf))
270			Expect(taskProcessSpec.Path).To(Equal("ls"))
271			Expect(taskProcessSpec.Args).To(Equal([]string{"some", "args"}))
272		})
273
274		It("sets the config on the TaskDelegate", func() {
275			Expect(fakeDelegate.SetTaskConfigCallCount()).To(Equal(1))
276			actualTaskConfig := fakeDelegate.SetTaskConfigArgsForCall(0)
277			Expect(actualTaskConfig).To(Equal(*taskPlan.Config))
278		})
279
280		Context("when privileged", func() {
281			BeforeEach(func() {
282				taskPlan.Privileged = true
283			})
284
285			It("marks the container's image spec as privileged", func() {
286				Expect(fakeClient.RunTaskStepCallCount()).To(Equal(1))
287				_, _, _, containerSpec, _, _, _, _, _, _, _ := fakeClient.RunTaskStepArgsForCall(0)
288				Expect(containerSpec.ImageSpec.Privileged).To(BeTrue())
289			})
290		})
291
292		Context("when the configuration specifies paths for inputs", func() {
293			var inputArtifact *runtimefakes.FakeArtifact
294			var otherInputArtifact *runtimefakes.FakeArtifact
295
296			BeforeEach(func() {
297				inputArtifact = new(runtimefakes.FakeArtifact)
298				otherInputArtifact = new(runtimefakes.FakeArtifact)
299
300				taskPlan.Config = &atc.TaskConfig{
301					Platform:  "some-platform",
302					RootfsURI: "some-image",
303					Params:    map[string]string{"SOME": "params"},
304					Run: atc.TaskRunConfig{
305						Path: "ls",
306						Args: []string{"some", "args"},
307					},
308					Inputs: []atc.TaskInputConfig{
309						{Name: "some-input", Path: "some-input-configured-path"},
310						{Name: "some-other-input"},
311					},
312				}
313			})
314
315			Context("when all inputs are present", func() {
316				BeforeEach(func() {
317					repo.RegisterArtifact("some-input", inputArtifact)
318					repo.RegisterArtifact("some-other-input", otherInputArtifact)
319				})
320
321				It("configures the inputs for the containerSpec correctly", func() {
322					Expect(fakeClient.RunTaskStepCallCount()).To(Equal(1))
323					_, _, _, actualContainerSpec, _, _, _, _, _, _, _ := fakeClient.RunTaskStepArgsForCall(0)
324					Expect(actualContainerSpec.ArtifactByPath).To(HaveLen(2))
325					Expect(actualContainerSpec.ArtifactByPath["some-artifact-root/some-input-configured-path"]).To(Equal(inputArtifact))
326					Expect(actualContainerSpec.ArtifactByPath["some-artifact-root/some-other-input"]).To(Equal(otherInputArtifact))
327				})
328			})
329
330			Context("when any of the inputs are missing", func() {
331				BeforeEach(func() {
332					repo.RegisterArtifact("some-input", inputArtifact)
333				})
334
335				It("returns a MissingInputsError", func() {
336					Expect(stepErr).To(BeAssignableToTypeOf(exec.MissingInputsError{}))
337					Expect(stepErr.(exec.MissingInputsError).Inputs).To(ConsistOf("some-other-input"))
338				})
339			})
340		})
341
342		Context("when input is remapped", func() {
343			var remappedInputArtifact *runtimefakes.FakeArtifact
344
345			BeforeEach(func() {
346				remappedInputArtifact = new(runtimefakes.FakeArtifact)
347				taskPlan.InputMapping = map[string]string{"remapped-input": "remapped-input-src"}
348				taskPlan.Config = &atc.TaskConfig{
349					Platform: "some-platform",
350					Run: atc.TaskRunConfig{
351						Path: "ls",
352						Args: []string{"some", "args"},
353					},
354					Inputs: []atc.TaskInputConfig{
355						{Name: "remapped-input"},
356					},
357				}
358			})
359
360			Context("when all inputs are present in the in artifact repository", func() {
361				BeforeEach(func() {
362					repo.RegisterArtifact("remapped-input-src", remappedInputArtifact)
363				})
364
365				It("uses remapped input", func() {
366					Expect(fakeClient.RunTaskStepCallCount()).To(Equal(1))
367					_, _, _, actualContainerSpec, _, _, _, _, _, _, _ := fakeClient.RunTaskStepArgsForCall(0)
368					Expect(actualContainerSpec.ArtifactByPath).To(HaveLen(1))
369					Expect(actualContainerSpec.ArtifactByPath["some-artifact-root/remapped-input"]).To(Equal(remappedInputArtifact))
370					Expect(stepErr).ToNot(HaveOccurred())
371				})
372			})
373
374			Context("when any of the inputs are missing", func() {
375				It("returns a MissingInputsError", func() {
376					Expect(stepErr).To(BeAssignableToTypeOf(exec.MissingInputsError{}))
377					Expect(stepErr.(exec.MissingInputsError).Inputs).To(ConsistOf("remapped-input-src"))
378				})
379			})
380		})
381
382		Context("when some inputs are optional", func() {
383			var (
384				optionalInputArtifact, optionalInput2Artifact, requiredInputArtifact *runtimefakes.FakeArtifact
385			)
386
387			BeforeEach(func() {
388				optionalInputArtifact = new(runtimefakes.FakeArtifact)
389				optionalInput2Artifact = new(runtimefakes.FakeArtifact)
390				requiredInputArtifact = new(runtimefakes.FakeArtifact)
391				taskPlan.Config = &atc.TaskConfig{
392					Platform: "some-platform",
393					Run: atc.TaskRunConfig{
394						Path: "ls",
395					},
396					Inputs: []atc.TaskInputConfig{
397						{Name: "optional-input", Optional: true},
398						{Name: "optional-input-2", Optional: true},
399						{Name: "required-input"},
400					},
401				}
402			})
403
404			Context("when an optional input is missing", func() {
405				BeforeEach(func() {
406					repo.RegisterArtifact("required-input", requiredInputArtifact)
407					repo.RegisterArtifact("optional-input-2", optionalInput2Artifact)
408				})
409
410				It("runs successfully without the optional input", func() {
411					Expect(stepErr).ToNot(HaveOccurred())
412					Expect(fakeClient.RunTaskStepCallCount()).To(Equal(1))
413					_, _, _, actualContainerSpec, _, _, _, _, _, _, _ := fakeClient.RunTaskStepArgsForCall(0)
414					Expect(actualContainerSpec.ArtifactByPath).To(HaveLen(2))
415					Expect(actualContainerSpec.ArtifactByPath["some-artifact-root/required-input"]).To(Equal(optionalInputArtifact))
416					Expect(actualContainerSpec.ArtifactByPath["some-artifact-root/optional-input-2"]).To(Equal(optionalInput2Artifact))
417				})
418			})
419
420			Context("when a required input is missing", func() {
421				BeforeEach(func() {
422					repo.RegisterArtifact("optional-input", optionalInputArtifact)
423					repo.RegisterArtifact("optional-input-2", optionalInput2Artifact)
424				})
425
426				It("returns a MissingInputsError", func() {
427					Expect(stepErr).To(BeAssignableToTypeOf(exec.MissingInputsError{}))
428					Expect(stepErr.(exec.MissingInputsError).Inputs).To(ConsistOf("required-input"))
429				})
430			})
431		})
432
433		Context("when the configuration specifies paths for caches", func() {
434			var (
435				fakeVolume1 *workerfakes.FakeVolume
436				fakeVolume2 *workerfakes.FakeVolume
437			)
438
439			BeforeEach(func() {
440				taskPlan.Config = &atc.TaskConfig{
441					Platform:  "some-platform",
442					RootfsURI: "some-image",
443					Run: atc.TaskRunConfig{
444						Path: "ls",
445					},
446					Caches: []atc.TaskCacheConfig{
447						{Path: "some-path-1"},
448						{Path: "some-path-2"},
449					},
450				}
451
452				fakeVolume1 = new(workerfakes.FakeVolume)
453				fakeVolume2 = new(workerfakes.FakeVolume)
454				taskResult := worker.TaskResult{
455					ExitStatus: 0,
456					VolumeMounts: []worker.VolumeMount{
457						{
458							Volume:    fakeVolume1,
459							MountPath: "some-artifact-root/some-path-1",
460						},
461						{
462							Volume:    fakeVolume2,
463							MountPath: "some-artifact-root/some-path-2",
464						},
465					},
466				}
467				fakeClient.RunTaskStepReturns(taskResult, nil)
468			})
469
470			It("creates the containerSpec with the caches in the inputs", func() {
471				_, _, _, containerSpec, _, _, _, _, _, _, _ := fakeClient.RunTaskStepArgsForCall(0)
472				Expect(containerSpec.ArtifactByPath).To(HaveLen(2))
473				Expect(containerSpec.ArtifactByPath["some-artifact-root/some-path-1"]).ToNot(BeNil())
474				Expect(containerSpec.ArtifactByPath["some-artifact-root/some-path-2"]).ToNot(BeNil())
475			})
476
477			Context("when task belongs to a job", func() {
478				BeforeEach(func() {
479					stepMetadata.JobID = 12
480				})
481
482				It("registers cache volumes as task caches", func() {
483					Expect(stepErr).ToNot(HaveOccurred())
484
485					Expect(fakeVolume1.InitializeTaskCacheCallCount()).To(Equal(1))
486					_, jID, stepName, cachePath, p := fakeVolume1.InitializeTaskCacheArgsForCall(0)
487					Expect(jID).To(Equal(stepMetadata.JobID))
488					Expect(stepName).To(Equal("some-task"))
489					Expect(cachePath).To(Equal("some-path-1"))
490					Expect(p).To(Equal(bool(taskPlan.Privileged)))
491
492					Expect(fakeVolume2.InitializeTaskCacheCallCount()).To(Equal(1))
493					_, jID, stepName, cachePath, p = fakeVolume2.InitializeTaskCacheArgsForCall(0)
494					Expect(jID).To(Equal(stepMetadata.JobID))
495					Expect(stepName).To(Equal("some-task"))
496					Expect(cachePath).To(Equal("some-path-2"))
497					Expect(p).To(Equal(bool(taskPlan.Privileged)))
498				})
499			})
500
501			Context("when task does not belong to job (one-off build)", func() {
502				BeforeEach(func() {
503					stepMetadata.JobID = 0
504				})
505
506				It("does not initialize caches", func() {
507					Expect(stepErr).ToNot(HaveOccurred())
508					Expect(fakeVolume1.InitializeTaskCacheCallCount()).To(Equal(0))
509					Expect(fakeVolume2.InitializeTaskCacheCallCount()).To(Equal(0))
510				})
511			})
512		})
513
514		Context("when the configuration specifies paths for outputs", func() {
515			BeforeEach(func() {
516				taskPlan.Config = &atc.TaskConfig{
517					Platform:  "some-platform",
518					RootfsURI: "some-image",
519					Params:    map[string]string{"SOME": "params"},
520					Run: atc.TaskRunConfig{
521						Path: "ls",
522						Args: []string{"some", "args"},
523					},
524					Outputs: []atc.TaskOutputConfig{
525						{Name: "some-output", Path: "some-output-configured-path"},
526						{Name: "some-other-output"},
527						{Name: "some-trailing-slash-output", Path: "some-output-configured-path-with-trailing-slash/"},
528					},
529				}
530			})
531
532			It("configures them appropriately in the container spec", func() {
533				_, _, _, containerSpec, _, _, _, _, _, _, _ := fakeClient.RunTaskStepArgsForCall(0)
534				Expect(containerSpec.Outputs).To(Equal(worker.OutputPaths{
535					"some-output":                "some-artifact-root/some-output-configured-path/",
536					"some-other-output":          "some-artifact-root/some-other-output/",
537					"some-trailing-slash-output": "some-artifact-root/some-output-configured-path-with-trailing-slash/",
538				}))
539			})
540		})
541
542		Context("when missing the platform", func() {
543
544			BeforeEach(func() {
545				taskPlan.Config.Platform = ""
546			})
547
548			It("returns the error", func() {
549				Expect(stepErr).To(HaveOccurred())
550			})
551
552			It("is not successful", func() {
553				Expect(taskStep.Succeeded()).To(BeFalse())
554			})
555		})
556
557		Context("when missing the path to the executable", func() {
558
559			BeforeEach(func() {
560				taskPlan.Config.Run.Path = ""
561			})
562
563			It("returns the error", func() {
564				Expect(stepErr).To(HaveOccurred())
565			})
566
567			It("is not successful", func() {
568				Expect(taskStep.Succeeded()).To(BeFalse())
569			})
570		})
571
572		Context("when an image artifact name is specified", func() {
573			BeforeEach(func() {
574				taskPlan.ImageArtifactName = "some-image-artifact"
575			})
576
577			Context("when the image artifact is registered in the artifact repo", func() {
578				var imageArtifact *runtimefakes.FakeArtifact
579
580				BeforeEach(func() {
581					imageArtifact = new(runtimefakes.FakeArtifact)
582					repo.RegisterArtifact("some-image-artifact", imageArtifact)
583				})
584
585				It("configures it in the containerSpec's ImageSpec", func() {
586					_, _, _, containerSpec, workerSpec, _, _, _, _, _, _ := fakeClient.RunTaskStepArgsForCall(0)
587					Expect(containerSpec.ImageSpec).To(Equal(worker.ImageSpec{
588						ImageArtifact: imageArtifact,
589					}))
590
591					Expect(workerSpec.ResourceType).To(Equal(""))
592				})
593
594				Describe("when task config specifies image and/or image resource as well as image artifact", func() {
595					Context("when streaming the metadata from the worker succeeds", func() {
596
597						JustBeforeEach(func() {
598							Expect(stepErr).ToNot(HaveOccurred())
599						})
600
601						Context("when the task config also specifies image", func() {
602							BeforeEach(func() {
603								taskPlan.Config = &atc.TaskConfig{
604									Platform:  "some-platform",
605									RootfsURI: "some-image",
606									Params:    map[string]string{"SOME": "params"},
607									Run: atc.TaskRunConfig{
608										Path: "ls",
609										Args: []string{"some", "args"},
610									},
611								}
612							})
613
614							It("still uses the image artifact", func() {
615								_, _, _, containerSpec, workerSpec, _, _, _, _, _, _ := fakeClient.RunTaskStepArgsForCall(0)
616								Expect(containerSpec.ImageSpec).To(Equal(worker.ImageSpec{
617									ImageArtifact: imageArtifact,
618								}))
619
620								Expect(workerSpec.ResourceType).To(Equal(""))
621							})
622						})
623
624						Context("when the task config also specifies image_resource", func() {
625							BeforeEach(func() {
626								taskPlan.Config = &atc.TaskConfig{
627									Platform: "some-platform",
628									ImageResource: &atc.ImageResource{
629										Type:    "docker",
630										Source:  atc.Source{"some": "super-secret-source"},
631										Params:  atc.Params{"some": "params"},
632										Version: atc.Version{"some": "version"},
633									},
634									Params: map[string]string{"SOME": "params"},
635									Run: atc.TaskRunConfig{
636										Path: "ls",
637										Args: []string{"some", "args"},
638									},
639								}
640							})
641
642							It("still uses the image artifact", func() {
643								_, _, _, containerSpec, workerSpec, _, _, _, _, _, _ := fakeClient.RunTaskStepArgsForCall(0)
644								Expect(containerSpec.ImageSpec).To(Equal(worker.ImageSpec{
645									ImageArtifact: imageArtifact,
646								}))
647
648								Expect(workerSpec.ResourceType).To(Equal(""))
649							})
650						})
651
652						Context("when the task config also specifies image and image_resource", func() {
653							BeforeEach(func() {
654								taskPlan.Config = &atc.TaskConfig{
655									Platform:  "some-platform",
656									RootfsURI: "some-image",
657									ImageResource: &atc.ImageResource{
658										Type:    "docker",
659										Source:  atc.Source{"some": "super-secret-source"},
660										Params:  atc.Params{"some": "params"},
661										Version: atc.Version{"some": "version"},
662									},
663									Params: map[string]string{"SOME": "params"},
664									Run: atc.TaskRunConfig{
665										Path: "ls",
666										Args: []string{"some", "args"},
667									},
668								}
669							})
670
671							It("still uses the image artifact", func() {
672								_, _, _, containerSpec, workerSpec, _, _, _, _, _, _ := fakeClient.RunTaskStepArgsForCall(0)
673								Expect(containerSpec.ImageSpec).To(Equal(worker.ImageSpec{
674									ImageArtifact: imageArtifact,
675								}))
676								Expect(workerSpec.ResourceType).To(Equal(""))
677							})
678						})
679					})
680				})
681			})
682
683			Context("when the image artifact is NOT registered in the artifact repo", func() {
684				It("returns a MissingTaskImageSourceError", func() {
685					Expect(stepErr).To(Equal(exec.MissingTaskImageSourceError{"some-image-artifact"}))
686				})
687
688				It("is not successful", func() {
689					Expect(taskStep.Succeeded()).To(BeFalse())
690				})
691			})
692		})
693
694		Context("when the image_resource is specified (even if RootfsURI is configured)", func() {
695			BeforeEach(func() {
696				taskPlan.Config = &atc.TaskConfig{
697					Platform:  "some-platform",
698					RootfsURI: "some-image",
699					ImageResource: &atc.ImageResource{
700						Type:    "docker",
701						Source:  atc.Source{"some": "super-secret-source"},
702						Params:  atc.Params{"some": "params"},
703						Version: atc.Version{"some": "version"},
704					},
705					Params: map[string]string{"SOME": "params"},
706					Run: atc.TaskRunConfig{
707						Path: "ls",
708						Args: []string{"some", "args"},
709					},
710				}
711			})
712
713			It("creates the specs with the image resource", func() {
714				_, _, _, containerSpec, workerSpec, _, _, _, _, _, _ := fakeClient.RunTaskStepArgsForCall(0)
715				Expect(containerSpec.ImageSpec.ImageResource).To(Equal(&worker.ImageResource{
716					Type:    "docker",
717					Source:  atc.Source{"some": "super-secret-source"},
718					Params:  atc.Params{"some": "params"},
719					Version: atc.Version{"some": "version"},
720				}))
721
722				Expect(workerSpec).To(Equal(worker.WorkerSpec{
723					TeamID:        123,
724					Platform:      "some-platform",
725					ResourceTypes: interpolatedResourceTypes,
726					Tags:          []string{"step", "tags"},
727					ResourceType:  "docker",
728				}))
729			})
730		})
731
732		Context("when the RootfsURI is configured", func() {
733			BeforeEach(func() {
734				taskPlan.Config = &atc.TaskConfig{
735					Platform:  "some-platform",
736					RootfsURI: "some-image",
737					Params:    map[string]string{"SOME": "params"},
738					Run: atc.TaskRunConfig{
739						Path: "ls",
740						Args: []string{"some", "args"},
741					},
742				}
743			})
744
745			It("creates the specs with the image resource", func() {
746				_, _, _, containerSpec, workerSpec, _, _, _, _, _, _ := fakeClient.RunTaskStepArgsForCall(0)
747				Expect(containerSpec.ImageSpec.ImageURL).To(Equal("some-image"))
748
749				Expect(workerSpec).To(Equal(worker.WorkerSpec{
750					TeamID:        123,
751					Platform:      "some-platform",
752					ResourceTypes: interpolatedResourceTypes,
753					Tags:          []string{"step", "tags"},
754				}))
755			})
756		})
757
758		Context("when a run dir is specified", func() {
759			var dir string
760			BeforeEach(func() {
761				dir = "/some/dir"
762				taskPlan.Config.Run.Dir = dir
763			})
764
765			It("specifies it in the process  spec", func() {
766				_, _, _, _, _, _, _, _, processSpec, _, _ := fakeClient.RunTaskStepArgsForCall(0)
767				Expect(processSpec.Dir).To(Equal(dir))
768			})
769		})
770
771		Context("when a run user is specified", func() {
772			BeforeEach(func() {
773				taskPlan.Config.Run.User = "some-user"
774			})
775
776			It("adds the user to the container spec", func() {
777				_, _, _, containerSpec, _, _, _, _, _, _, _ := fakeClient.RunTaskStepArgsForCall(0)
778				Expect(containerSpec.User).To(Equal("some-user"))
779			})
780
781			It("doesn't bother adding the user to the run spec", func() {
782				_, _, _, _, _, _, _, _, processSpec, _, _ := fakeClient.RunTaskStepArgsForCall(0)
783				Expect(processSpec.User).To(BeEmpty())
784			})
785		})
786
787		Context("when running the task succeeds", func() {
788			var taskStepStatus int
789			BeforeEach(func() {
790				taskPlan.Config = &atc.TaskConfig{
791					Platform:  "some-platform",
792					RootfsURI: "some-image",
793					Params:    map[string]string{"SOME": "params"},
794					Run: atc.TaskRunConfig{
795						Path: "ls",
796						Args: []string{"some", "args"},
797					},
798					Outputs: []atc.TaskOutputConfig{
799						{Name: "some-output", Path: "some-output-configured-path"},
800						{Name: "some-other-output"},
801						{Name: "some-trailing-slash-output", Path: "some-output-configured-path-with-trailing-slash/"},
802					},
803				}
804			})
805
806			It("returns successfully", func() {
807				Expect(stepErr).ToNot(HaveOccurred())
808			})
809
810			Context("when the task exits with zero status", func() {
811				BeforeEach(func() {
812					taskStepStatus = 0
813					taskResult := worker.TaskResult{
814						ExitStatus:   taskStepStatus,
815						VolumeMounts: []worker.VolumeMount{},
816					}
817					fakeClient.RunTaskStepReturns(taskResult, nil)
818				})
819				It("finishes the task via the delegate", func() {
820					Expect(fakeDelegate.FinishedCallCount()).To(Equal(1))
821					_, status := fakeDelegate.FinishedArgsForCall(0)
822					Expect(status).To(Equal(exec.ExitStatus(taskStepStatus)))
823				})
824
825				It("returns successfully", func() {
826					Expect(stepErr).ToNot(HaveOccurred())
827				})
828
829				Describe("the registered artifacts", func() {
830					var (
831						artifact1 runtime.Artifact
832						artifact2 runtime.Artifact
833						artifact3 runtime.Artifact
834
835						fakeMountPath1 string = "some-artifact-root/some-output-configured-path/"
836						fakeMountPath2 string = "some-artifact-root/some-other-output/"
837						fakeMountPath3 string = "some-artifact-root/some-output-configured-path-with-trailing-slash/"
838
839						fakeVolume1 *workerfakes.FakeVolume
840						fakeVolume2 *workerfakes.FakeVolume
841						fakeVolume3 *workerfakes.FakeVolume
842					)
843
844					BeforeEach(func() {
845
846						fakeVolume1 = new(workerfakes.FakeVolume)
847						fakeVolume1.HandleReturns("some-handle-1")
848						fakeVolume2 = new(workerfakes.FakeVolume)
849						fakeVolume2.HandleReturns("some-handle-2")
850						fakeVolume3 = new(workerfakes.FakeVolume)
851						fakeVolume3.HandleReturns("some-handle-3")
852
853						fakeTaskResult := worker.TaskResult{
854							ExitStatus: 0,
855							VolumeMounts: []worker.VolumeMount{
856								{
857									Volume:    fakeVolume1,
858									MountPath: fakeMountPath1,
859								},
860								{
861									Volume:    fakeVolume2,
862									MountPath: fakeMountPath2,
863								},
864								{
865									Volume:    fakeVolume3,
866									MountPath: fakeMountPath3,
867								},
868							},
869						}
870						fakeClient.RunTaskStepReturns(fakeTaskResult, nil)
871					})
872
873					JustBeforeEach(func() {
874						Expect(stepErr).ToNot(HaveOccurred())
875
876						var found bool
877						artifact1, found = repo.ArtifactFor("some-output")
878						Expect(found).To(BeTrue())
879
880						artifact2, found = repo.ArtifactFor("some-other-output")
881						Expect(found).To(BeTrue())
882
883						artifact3, found = repo.ArtifactFor("some-trailing-slash-output")
884						Expect(found).To(BeTrue())
885					})
886
887					It("does not register the task as a artifact", func() {
888						artifactMap := repo.AsMap()
889						Expect(artifactMap).To(ConsistOf(artifact1, artifact2, artifact3))
890					})
891
892					It("passes existing output volumes to the resource", func() {
893						_, _, _, containerSpec, _, _, _, _, _, _, _ := fakeClient.RunTaskStepArgsForCall(0)
894						Expect(containerSpec.Outputs).To(Equal(worker.OutputPaths{
895							"some-output":                "some-artifact-root/some-output-configured-path/",
896							"some-other-output":          "some-artifact-root/some-other-output/",
897							"some-trailing-slash-output": "some-artifact-root/some-output-configured-path-with-trailing-slash/",
898						}))
899					})
900				})
901			})
902
903			Context("when the task exits with nonzero status", func() {
904				BeforeEach(func() {
905					taskStepStatus = 5
906					taskResult := worker.TaskResult{ExitStatus: taskStepStatus, VolumeMounts: []worker.VolumeMount{}}
907					fakeClient.RunTaskStepReturns(taskResult, nil)
908				})
909				It("finishes the task via the delegate", func() {
910					Expect(fakeDelegate.FinishedCallCount()).To(Equal(1))
911					_, status := fakeDelegate.FinishedArgsForCall(0)
912					Expect(status).To(Equal(exec.ExitStatus(taskStepStatus)))
913				})
914
915				It("returns successfully", func() {
916					Expect(stepErr).ToNot(HaveOccurred())
917				})
918			})
919		})
920
921		Context("when running the task fails", func() {
922			disaster := errors.New("task run failed")
923
924			BeforeEach(func() {
925				taskResult := worker.TaskResult{ExitStatus: -1, VolumeMounts: []worker.VolumeMount{}}
926				fakeClient.RunTaskStepReturns(taskResult, disaster)
927			})
928
929			It("returns the error", func() {
930				Expect(stepErr).To(Equal(disaster))
931			})
932
933			It("is not successful", func() {
934				Expect(taskStep.Succeeded()).To(BeFalse())
935			})
936		})
937
938		Context("when the task step is interrupted", func() {
939			BeforeEach(func() {
940				fakeClient.RunTaskStepReturns(
941					worker.TaskResult{
942						ExitStatus:   -1,
943						VolumeMounts: []worker.VolumeMount{},
944					}, context.Canceled)
945				cancel()
946			})
947
948			It("returns the context.Canceled error", func() {
949				Expect(stepErr).To(Equal(context.Canceled))
950			})
951
952			It("is not successful", func() {
953				Expect(taskStep.Succeeded()).To(BeFalse())
954			})
955
956			It("waits for RunTaskStep to return", func() {
957				Expect(fakeClient.RunTaskStepCallCount()).To(Equal(1))
958			})
959
960			It("doesn't register a artifact", func() {
961				artifactMap := repo.AsMap()
962				Expect(artifactMap).To(BeEmpty())
963			})
964		})
965
966		Context("when RunTaskStep returns volume mounts", func() {
967			var (
968				fakeMountPath1 string = "some-artifact-root/some-output-configured-path/"
969				fakeMountPath2 string = "some-artifact-root/some-other-output/"
970				fakeMountPath3 string = "some-artifact-root/some-output-configured-path-with-trailing-slash/"
971
972				fakeVolume1 *workerfakes.FakeVolume
973				fakeVolume2 *workerfakes.FakeVolume
974				fakeVolume3 *workerfakes.FakeVolume
975
976				runTaskStepError error
977				taskResult       worker.TaskResult
978			)
979
980			BeforeEach(func() {
981				taskPlan.Config = &atc.TaskConfig{
982					Platform:  "some-platform",
983					RootfsURI: "some-image",
984					Params:    map[string]string{"SOME": "params"},
985					Run: atc.TaskRunConfig{
986						Path: "ls",
987						Args: []string{"some", "args"},
988					},
989					Outputs: []atc.TaskOutputConfig{
990						{Name: "some-output", Path: "some-output-configured-path"},
991						{Name: "some-other-output"},
992						{Name: "some-trailing-slash-output", Path: "some-output-configured-path-with-trailing-slash/"},
993					},
994				}
995
996				fakeVolume1 = new(workerfakes.FakeVolume)
997				fakeVolume1.HandleReturns("some-handle-1")
998				fakeVolume2 = new(workerfakes.FakeVolume)
999				fakeVolume2.HandleReturns("some-handle-2")
1000				fakeVolume3 = new(workerfakes.FakeVolume)
1001				fakeVolume3.HandleReturns("some-handle-3")
1002
1003				taskResult = worker.TaskResult{
1004					ExitStatus: 0,
1005					VolumeMounts: []worker.VolumeMount{
1006						{
1007							Volume:    fakeVolume1,
1008							MountPath: fakeMountPath1,
1009						},
1010						{
1011							Volume:    fakeVolume2,
1012							MountPath: fakeMountPath2,
1013						},
1014						{
1015							Volume:    fakeVolume3,
1016							MountPath: fakeMountPath3,
1017						},
1018					},
1019				}
1020			})
1021
1022			var outputsAreRegistered = func() {
1023				It("registers the outputs as artifacts", func() {
1024					artifact1, found := repo.ArtifactFor("some-output")
1025					Expect(found).To(BeTrue())
1026
1027					artifact2, found := repo.ArtifactFor("some-other-output")
1028					Expect(found).To(BeTrue())
1029
1030					artifact3, found := repo.ArtifactFor("some-trailing-slash-output")
1031					Expect(found).To(BeTrue())
1032
1033					artifactMap := repo.AsMap()
1034					Expect(artifactMap).To(ConsistOf(artifact1, artifact2, artifact3))
1035				})
1036
1037			}
1038
1039			Context("when RunTaskStep succeeds", func() {
1040				BeforeEach(func() {
1041					runTaskStepError = nil
1042					fakeClient.RunTaskStepReturns(taskResult, runTaskStepError)
1043				})
1044				outputsAreRegistered()
1045			})
1046
1047			Context("when RunTaskStep returns a context Canceled error", func() {
1048				BeforeEach(func() {
1049					runTaskStepError = context.Canceled
1050					fakeClient.RunTaskStepReturns(taskResult, runTaskStepError)
1051				})
1052				outputsAreRegistered()
1053			})
1054			Context("when RunTaskStep returns a context DeadlineExceeded error", func() {
1055				BeforeEach(func() {
1056					runTaskStepError = context.DeadlineExceeded
1057					fakeClient.RunTaskStepReturns(taskResult, runTaskStepError)
1058				})
1059				outputsAreRegistered()
1060			})
1061
1062			Context("when RunTaskStep returns a unexpected error", func() {
1063				BeforeEach(func() {
1064					runTaskStepError = errors.New("some unexpected error")
1065					fakeClient.RunTaskStepReturns(taskResult, runTaskStepError)
1066				})
1067				It("re-registers the outputs as artifacts", func() {
1068					artifactMap := repo.AsMap()
1069					Expect(artifactMap).To(BeEmpty())
1070				})
1071
1072			})
1073		})
1074
1075		Context("when output is remapped", func() {
1076			var (
1077				fakeMountPath string = "some-artifact-root/generic-remapped-output/"
1078			)
1079
1080			BeforeEach(func() {
1081				taskPlan.OutputMapping = map[string]string{"generic-remapped-output": "specific-remapped-output"}
1082				taskPlan.Config = &atc.TaskConfig{
1083					Platform: "some-platform",
1084					Run: atc.TaskRunConfig{
1085						Path: "ls",
1086					},
1087					Outputs: []atc.TaskOutputConfig{
1088						{Name: "generic-remapped-output"},
1089					},
1090				}
1091
1092				fakeVolume := new(workerfakes.FakeVolume)
1093				fakeVolume.HandleReturns("some-handle")
1094
1095				taskResult := worker.TaskResult{
1096					ExitStatus: 0,
1097					VolumeMounts: []worker.VolumeMount{
1098						{
1099							Volume:    fakeVolume,
1100							MountPath: fakeMountPath,
1101						},
1102					},
1103				}
1104				fakeClient.RunTaskStepReturns(taskResult, nil)
1105			})
1106
1107			JustBeforeEach(func() {
1108				Expect(stepErr).ToNot(HaveOccurred())
1109			})
1110
1111			It("registers the outputs as artifacts with specific name", func() {
1112				artifact, found := repo.ArtifactFor("specific-remapped-output")
1113				Expect(found).To(BeTrue())
1114
1115				artifactMap := repo.AsMap()
1116				Expect(artifactMap).To(ConsistOf(artifact))
1117			})
1118		})
1119
1120	})
1121})
1122