Files
concourse-concourse/atc/exec/task_step_test.go
T

1189 lines
34 KiB
Go

package exec_test
import (
"context"
"errors"
"fmt"
"io"
"strings"
"time"
"github.com/concourse/concourse/atc"
"github.com/concourse/concourse/atc/db"
"github.com/concourse/concourse/atc/exec"
"github.com/concourse/concourse/atc/exec/build"
"github.com/concourse/concourse/atc/exec/execfakes"
"github.com/concourse/concourse/atc/runtime"
"github.com/concourse/concourse/atc/runtime/runtimetest"
"github.com/concourse/concourse/atc/worker"
"github.com/concourse/concourse/tracing"
"github.com/concourse/concourse/vars"
"github.com/concourse/concourse/worker/baggageclaim"
"github.com/onsi/gomega/gbytes"
"go.opentelemetry.io/otel/sdk/trace"
"go.opentelemetry.io/otel/sdk/trace/tracetest"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
var _ = Describe("TaskStep", func() {
var (
ctx context.Context
cancel func()
stdoutBuf *gbytes.Buffer
stderrBuf *gbytes.Buffer
fakePool *execfakes.FakePool
fakeStreamer *execfakes.FakeStreamer
fakeDelegate *execfakes.FakeTaskDelegate
fakeDelegateFactory *execfakes.FakeTaskDelegateFactory
taskPlan *atc.TaskPlan
state exec.RunState
repo *build.Repository
taskStep exec.Step
stepOk bool
stepErr error
cpuLimit = atc.CPULimit(1024)
memoryLimit = atc.MemoryLimit(1024)
containerMetadata = db.ContainerMetadata{
WorkingDirectory: "some-artifact-root",
Type: db.ContainerTypeTask,
StepName: "some-step",
}
stepMetadata = exec.StepMetadata{
TeamID: 123,
BuildID: 1234,
JobID: 12345,
ExternalURL: "http://foo.bar",
}
planID = atc.PlanID("42")
expectedOwner = db.NewBuildStepContainerOwner(stepMetadata.BuildID, planID, stepMetadata.TeamID)
defaultTaskTimeout time.Duration = 0
defaultTaskCacheTTL time.Duration = 0
)
BeforeEach(func() {
ctx, cancel = context.WithCancel(context.Background())
stdoutBuf = gbytes.NewBuffer()
stderrBuf = gbytes.NewBuffer()
fakeStreamer = new(execfakes.FakeStreamer)
fakeDelegate = new(execfakes.FakeTaskDelegate)
fakeDelegate.StdoutReturns(stdoutBuf)
fakeDelegate.StderrReturns(stderrBuf)
fakeDelegate.StartSpanReturns(ctx, tracing.NoopSpan)
fakeDelegateFactory = new(execfakes.FakeTaskDelegateFactory)
fakeDelegateFactory.TaskDelegateReturns(fakeDelegate)
state = exec.NewRunState(noopStepper, vars.StaticVariables{"source-param": "super-secret-source"})
repo = state.ArtifactRepository()
taskPlan = &atc.TaskPlan{
Name: "some-task",
Privileged: false,
ResourceTypes: atc.ResourceTypes{
{
Name: "custom-resource",
Type: "custom-type",
Source: atc.Source{"some-custom": "((source-param))"},
Params: atc.Params{"some-custom": "param"},
},
},
}
})
JustBeforeEach(func() {
plan := atc.Plan{
ID: planID,
Task: taskPlan,
}
// stuff stored on task step still
taskStep = exec.NewTaskStep(
plan.ID,
*plan.Task,
atc.ContainerLimits{},
stepMetadata,
containerMetadata,
nil,
fakePool,
fakeStreamer,
fakeDelegateFactory,
defaultTaskTimeout,
defaultTaskCacheTTL,
)
stepOk, stepErr = taskStep.Run(ctx, state)
})
expectWorkerSpecResourceTypeUnset := func() {
Expect(fakePool.FindOrSelectWorkerCallCount()).To(Equal(1))
_, _, _, workerSpec, _, _ := fakePool.FindOrSelectWorkerArgsForCall(0)
Expect(workerSpec.ResourceType).To(Equal(""))
}
Context("when the plan has a config", func() {
var chosenWorker *runtimetest.Worker
var chosenContainer *runtimetest.WorkerContainer
BeforeEach(func() {
taskPlan.Config = &atc.TaskConfig{
Platform: "some-platform",
Limits: &atc.ContainerLimits{
CPU: &cpuLimit,
Memory: &memoryLimit,
},
Params: atc.TaskEnv{
"SECURE": "secret-task-param",
},
Run: atc.TaskRunConfig{
Path: "ls",
Args: []string{"some", "args"},
},
}
chosenWorker = runtimetest.NewWorker("worker").
WithContainer(
expectedOwner,
runtimetest.NewContainer().WithProcess(
runtime.ProcessSpec{
ID: "task",
Path: "ls",
Args: []string{"some", "args"},
Dir: "some-artifact-root",
TTY: &runtime.TTYSpec{
WindowSize: runtime.WindowSize{
Columns: 500,
Rows: 500,
},
},
},
runtimetest.ProcessStub{Attachable: true},
),
nil,
)
chosenContainer = chosenWorker.Containers[0]
fakePool = new(execfakes.FakePool)
fakePool.FindOrSelectWorkerReturns(chosenWorker, nil)
})
It("Task env includes atc external url", func() {
Expect(chosenContainer.Spec.Env).To(ConsistOf("ATC_EXTERNAL_URL=http://foo.bar", "SECURE=secret-task-param"))
})
Context("before running the task", func() {
BeforeEach(func() {
chosenContainer.ProcessDefs[0].Stub.Do = func(_ context.Context, _ *runtimetest.Process) error {
defer GinkgoRecover()
Expect(fakeDelegate.InitializingCallCount()).To(Equal(1))
return nil
}
})
It("invokes the delegate's Initializing callback", func() {
// validate the process actually ran
Expect(chosenContainer.RunningProcesses()).To(HaveLen(1))
})
})
Describe("worker selection", func() {
var ctx context.Context
var workerSpec worker.Spec
JustBeforeEach(func() {
Expect(fakePool.FindOrSelectWorkerCallCount()).To(Equal(1))
ctx, _, _, workerSpec, _, _ = fakePool.FindOrSelectWorkerArgsForCall(0)
})
It("doesn't enforce a timeout", func() {
_, ok := ctx.Deadline()
Expect(ok).To(BeFalse())
})
It("emits a BeforeSelectWorker event", func() {
Expect(fakeDelegate.BeforeSelectWorkerCallCount()).To(Equal(1))
})
It("emits a SelectedWorker event", func() {
Expect(fakeDelegate.SelectedWorkerCallCount()).To(Equal(1))
_, workerName := fakeDelegate.SelectedWorkerArgsForCall(0)
Expect(workerName).To(Equal("worker"))
})
Context("when tags are configured", func() {
BeforeEach(func() {
taskPlan.Tags = atc.Tags{"plan", "tags"}
})
It("creates a worker spec with the tags", func() {
Expect(workerSpec.Tags).To(Equal([]string{"plan", "tags"}))
})
})
Context("when selecting a worker fails", func() {
BeforeEach(func() {
fakePool.FindOrSelectWorkerReturns(nil, errors.New("nope"))
})
It("returns an err", func() {
Expect(stepErr).To(MatchError(ContainSubstring("nope")))
})
})
})
It("sets the config on the TaskDelegate", func() {
Expect(fakeDelegate.SetTaskConfigCallCount()).To(Equal(1))
actualTaskConfig := fakeDelegate.SetTaskConfigArgsForCall(0)
Expect(actualTaskConfig).To(Equal(*taskPlan.Config))
})
Context("when privileged", func() {
BeforeEach(func() {
taskPlan.Privileged = true
})
It("marks the container's image spec as privileged", func() {
Expect(chosenContainer.Spec.ImageSpec.Privileged).To(BeTrue())
})
})
It("uses the correct container limits", func() {
Expect(atc.CPULimit(*chosenContainer.Spec.Limits.CPU)).To(Equal(atc.CPULimit(1024)))
Expect(atc.MemoryLimit(*chosenContainer.Spec.Limits.Memory)).To(Equal(atc.MemoryLimit(1024)))
})
Context("when toplevel limits are set", func() {
BeforeEach(func() {
cpu := atc.CPULimit(2048)
memory := atc.MemoryLimit(2048)
taskPlan.Limits = &atc.ContainerLimits{
CPU: &cpu,
Memory: &memory,
}
})
It("overrides the limits from the config", func() {
Expect(atc.CPULimit(*chosenContainer.Spec.Limits.CPU)).To(Equal(atc.CPULimit(2048)))
Expect(atc.MemoryLimit(*chosenContainer.Spec.Limits.Memory)).To(Equal(atc.MemoryLimit(2048)))
})
})
Context("when hermetic is configured", func() {
BeforeEach(func() {
taskPlan.Hermetic = true
})
It("returns the context.Canceled error", func() {
Expect(chosenContainer.Spec.Hermetic).To(Equal(true))
})
})
Context("when a user is configured", func() {
BeforeEach(func() {
taskPlan.Config.Run.User = "some-user"
})
It("sets the user on the container spec", func() {
Expect(chosenContainer.Spec.User).To(Equal("some-user"))
})
})
Context("when a timeout is configured", func() {
BeforeEach(func() {
taskPlan.Timeout = "1ms"
chosenContainer.ProcessDefs[0].Stub.Do = func(ctx context.Context, _ *runtimetest.Process) error {
select {
case <-ctx.Done():
return fmt.Errorf("wrapped: %w", ctx.Err())
case <-time.After(100 * time.Millisecond):
return nil
}
}
})
It("fails without error", func() {
Expect(stepOk).To(BeFalse())
Expect(stepErr).To(BeNil())
})
It("emits an Errored event", func() {
Expect(fakeDelegate.ErroredCallCount()).To(Equal(1))
_, status := fakeDelegate.ErroredArgsForCall(0)
Expect(status).To(Equal(exec.TimeoutLogMessage))
})
Context("when the timeout is bogus", func() {
BeforeEach(func() {
taskPlan.Timeout = "bogus"
})
It("fails miserably", func() {
Expect(stepErr).To(MatchError("parse timeout: time: invalid duration \"bogus\""))
})
})
})
Context("when there is default task timeout", func() {
BeforeEach(func() {
defaultTaskTimeout = time.Minute * 30
})
It("enforces it on the task", func() {
t, ok := chosenContainer.ContextOfRun().Deadline()
Expect(ok).To(BeTrue())
Expect(t).To(BeTemporally("~", time.Now().Add(time.Minute*30), time.Minute))
})
})
Context("when there is default task timeout and the plan specifies a timeout also", func() {
BeforeEach(func() {
defaultTaskTimeout = time.Minute * 30
taskPlan.Timeout = "1h"
})
It("enforces the plan's timeout on the task step", func() {
t, ok := chosenContainer.ContextOfRun().Deadline()
Expect(ok).To(BeTrue())
Expect(t).To(BeTemporally("~", time.Now().Add(time.Hour), time.Minute))
})
})
Context("when rootfs uri is set instead of image resource", func() {
BeforeEach(func() {
taskPlan.Config.RootfsURI = "some-image"
})
It("correctly sets up the image spec", func() {
Expect(chosenContainer.Spec.ImageSpec).To(Equal(runtime.ImageSpec{
ImageURL: "some-image",
Privileged: false,
}))
})
})
Context("when tracing is enabled", func() {
BeforeEach(func() {
defaultTaskTimeout = 0
exporter := tracetest.NewInMemoryExporter()
tp := trace.NewTracerProvider(trace.WithSyncer(exporter))
tracing.ConfigureTraceProvider(tp)
spanCtx, buildSpan := tracing.StartSpan(ctx, "build", nil)
fakeDelegate.StartSpanReturns(spanCtx, buildSpan)
chosenContainer.ProcessDefs[0].Stub.Do = func(ctx context.Context, _ *runtimetest.Process) error {
defer GinkgoRecover()
// Properly propagates span context
Expect(tracing.FromContext(ctx)).To(Equal(buildSpan))
return nil
}
})
AfterEach(func() {
tracing.Configured = false
})
It("populates the TRACEPARENT env var", func() {
Expect(chosenContainer.Spec.Env).To(ContainElement(MatchRegexp(`TRACEPARENT=.+`)))
})
})
Context("when the configuration specifies paths for inputs", func() {
var input1 *runtimetest.Volume
var input2 *runtimetest.Volume
BeforeEach(func() {
input1 = runtimetest.NewVolume("input1")
input2 = runtimetest.NewVolume("input2")
taskPlan.Config.Inputs = []atc.TaskInputConfig{
{Name: "some-input", Path: "some-input-configured-path"},
{Name: "some-other-input"},
}
})
Context("when all inputs are present", func() {
var input3 *runtimetest.Volume
var input4 *runtimetest.Volume
BeforeEach(func() {
input3 = runtimetest.NewVolume("input3")
input4 = runtimetest.NewVolume("input4")
// If or not framCache when RegisterArtifact, that should not impact task execution.
repo.RegisterArtifact("some-input", input1, false)
repo.RegisterArtifact("some-other-input", input2, true)
repo.RegisterArtifact("absolute-input", input3, false)
repo.RegisterArtifact("parent-dirs-input", input4, false)
taskPlan.Config.Inputs = []atc.TaskInputConfig{
{Name: "some-input", Path: "some-input-configured-path"},
{Name: "some-other-input"},
{Name: "absolute-input", Path: "/absolute-input"},
{Name: "parent-dirs-input", Path: "../parent-dirs/../input"},
}
})
It("configures the inputs for the containerSpec correctly", func() {
Expect(chosenContainer.Spec.Inputs).To(ConsistOf([]runtime.Input{
{
Artifact: input1,
DestinationPath: "some-artifact-root/some-input-configured-path",
FromCache: false,
},
{
Artifact: input2,
DestinationPath: "some-artifact-root/some-other-input",
FromCache: true,
},
{
Artifact: input3,
DestinationPath: "/absolute-input",
FromCache: false,
},
{
Artifact: input4,
DestinationPath: "some-artifact-root/parent-dirs/input",
FromCache: false,
},
}))
Expect(stepErr).ToNot(HaveOccurred())
})
})
Context("when any of the inputs are missing", func() {
BeforeEach(func() {
repo.RegisterArtifact("some-input", input1, false)
})
It("returns a MissingInputsError", func() {
Expect(stepErr).To(BeAssignableToTypeOf(exec.MissingInputsError{}))
Expect(stepErr.(exec.MissingInputsError).Inputs).To(ConsistOf("some-other-input"))
})
})
Context("when only inputs are cache", func() {
BeforeEach(func() {
// If or not framCache when RegisterArtifact, that should not impact task execution.
repo.RegisterArtifact("some-input", input1, true)
repo.RegisterArtifact("some-other-input", input2, true)
})
It("configures the inputs for the containerSpec correctly", func() {
Expect(chosenContainer.Spec.Inputs).To(ConsistOf([]runtime.Input{
{
Artifact: input1,
DestinationPath: "some-artifact-root/some-input-configured-path",
FromCache: true,
},
{
Artifact: input2,
DestinationPath: "some-artifact-root/some-other-input",
FromCache: true,
},
}))
Expect(stepErr).ToNot(HaveOccurred())
})
})
})
Context("when input is remapped", func() {
var remappedInputArtifact *runtimetest.Volume
BeforeEach(func() {
remappedInputArtifact = runtimetest.NewVolume("input1")
taskPlan.InputMapping = map[string]string{"remapped-input": "remapped-input-src"}
taskPlan.Config.Inputs = []atc.TaskInputConfig{
{Name: "remapped-input"},
}
})
Context("when all inputs are present in the in artifact repository", func() {
BeforeEach(func() {
repo.RegisterArtifact("remapped-input-src", remappedInputArtifact, false)
})
It("uses remapped input", func() {
Expect(chosenContainer.Spec.Inputs).To(ConsistOf([]runtime.Input{
{
Artifact: remappedInputArtifact,
DestinationPath: "some-artifact-root/remapped-input",
},
}))
Expect(stepErr).ToNot(HaveOccurred())
})
})
})
Context("when some inputs are optional", func() {
var (
optionalInputArtifact, optionalInput2Artifact, requiredInputArtifact *runtimetest.Volume
)
BeforeEach(func() {
optionalInputArtifact = runtimetest.NewVolume("optional1")
optionalInput2Artifact = runtimetest.NewVolume("optional2")
requiredInputArtifact = runtimetest.NewVolume("required1")
taskPlan.Config.Inputs = []atc.TaskInputConfig{
{Name: "optional-input", Optional: true},
{Name: "optional-input-2", Optional: true},
{Name: "required-input"},
}
})
Context("when an optional input is missing", func() {
BeforeEach(func() {
repo.RegisterArtifact("required-input", requiredInputArtifact, false)
repo.RegisterArtifact("optional-input-2", optionalInput2Artifact, false)
})
It("runs successfully without the optional input", func() {
Expect(chosenContainer.Spec.Inputs).To(ConsistOf([]runtime.Input{
{
Artifact: requiredInputArtifact,
DestinationPath: "some-artifact-root/required-input",
},
{
Artifact: optionalInput2Artifact,
DestinationPath: "some-artifact-root/optional-input-2",
},
}))
Expect(stepErr).ToNot(HaveOccurred())
})
})
Context("when a required input is missing", func() {
BeforeEach(func() {
repo.RegisterArtifact("optional-input", optionalInputArtifact, false)
repo.RegisterArtifact("optional-input-2", optionalInput2Artifact, false)
})
It("returns a MissingInputsError", func() {
Expect(stepErr).To(BeAssignableToTypeOf(exec.MissingInputsError{}))
Expect(stepErr.(exec.MissingInputsError).Inputs).To(ConsistOf("required-input"))
})
})
})
Context("when the configuration specifies paths for caches", func() {
var (
volume1 *runtimetest.Volume
volume2 *runtimetest.Volume
)
BeforeEach(func() {
taskPlan.Config.Caches = []atc.TaskCacheConfig{
{Path: "some-path-1"},
{Path: "some-path-2"},
}
volume1 = runtimetest.NewVolume("volume1")
volume2 = runtimetest.NewVolume("volume2")
chosenContainer.Mounts = []runtime.VolumeMount{
{
Volume: volume1,
MountPath: "some-artifact-root/some-path-1",
},
{
Volume: volume2,
MountPath: "some-artifact-root/some-path-2",
},
}
})
It("creates the containerSpec with the caches", func() {
Expect(chosenContainer.Spec.Caches).To(ConsistOf("some-path-1", "some-path-2"))
})
itRegistersCaches := func(didRegister bool) {
It("registers cache volumes as task caches", func() {
Expect(volume1.TaskCacheInitialized).To(Equal(didRegister))
Expect(volume2.TaskCacheInitialized).To(Equal(didRegister))
})
}
Context("when task belongs to a job", func() {
BeforeEach(func() {
stepMetadata.JobID = 12
})
Context("when the task succeeds", func() {
itRegistersCaches(true)
})
Context("when the task exits nonzero", func() {
BeforeEach(func() {
chosenContainer.ProcessDefs[0].Stub.ExitStatus = 1
})
itRegistersCaches(true)
})
Context("when the task errors", func() {
BeforeEach(func() {
chosenContainer.ProcessDefs[0].Stub.Err = "blah"
})
itRegistersCaches(true)
})
})
Context("when task does not belong to job (one-off build)", func() {
BeforeEach(func() {
stepMetadata.JobID = 0
})
It("does not error", func() {
Expect(stepErr).ToNot(HaveOccurred())
})
itRegistersCaches(false)
})
})
Context("when task config's cache TTL is not set", func() {
var volume1 *runtimetest.Volume
BeforeEach(func() {
stepMetadata.JobID = 12
taskPlan.Config.Caches = []atc.TaskCacheConfig{
{Path: "some-path-1"},
}
volume1 = runtimetest.NewVolume("volume1")
chosenContainer.Mounts = []runtime.VolumeMount{
{
Volume: volume1,
MountPath: "some-artifact-root/some-path-1",
},
}
})
Context("when defaultTaskCacheTTL is set", func() {
BeforeEach(func() {
defaultTaskCacheTTL = 24 * time.Hour
DeferCleanup(func() {
defaultTaskCacheTTL = 0
})
})
It("uses the defaultTaskCacheTTL", func() {
Expect(volume1.TaskCacheInitialized).To(BeTrue())
Expect(volume1.TaskCacheTTL).To(Equal(24 * time.Hour))
})
})
Context("when defaultTaskCacheTTL is not set", func() {
It("uses zero TTL", func() {
Expect(volume1.TaskCacheInitialized).To(BeTrue())
Expect(volume1.TaskCacheTTL).To(Equal(time.Duration(0)))
})
})
})
Context("when task config's cache TTL is set", func() {
var volume1 *runtimetest.Volume
BeforeEach(func() {
stepMetadata.JobID = 12
taskPlan.Config.Caches = []atc.TaskCacheConfig{
{Path: "some-path-1", TTL: "1h"},
}
volume1 = runtimetest.NewVolume("volume1")
chosenContainer.Mounts = []runtime.VolumeMount{
{
Volume: volume1,
MountPath: "some-artifact-root/some-path-1",
},
}
})
Context("when defaultTaskCacheTTL is also set", func() {
BeforeEach(func() {
defaultTaskCacheTTL = 24 * time.Hour
DeferCleanup(func() {
defaultTaskCacheTTL = 0
})
})
It("uses task config's cache TTL over defaultTaskCacheTTL", func() {
Expect(volume1.TaskCacheInitialized).To(BeTrue())
Expect(volume1.TaskCacheTTL).To(Equal(1 * time.Hour))
})
})
Context("when defaultTaskCacheTTL is not set", func() {
It("uses cacheConfig.TTL", func() {
Expect(volume1.TaskCacheInitialized).To(BeTrue())
Expect(volume1.TaskCacheTTL).To(Equal(1 * time.Hour))
})
})
})
Context("when the configuration specifies paths for outputs", func() {
var outputVolume1, outputVolume2, outputVolume3,
outputVolume4, outputVolume5 *runtimetest.Volume
BeforeEach(func() {
taskPlan.Config.Outputs = []atc.TaskOutputConfig{
{Name: "some-output", Path: "some-output-configured-path"},
{Name: "some-other-output"},
{Name: "some-trailing-slash-output", Path: "some-output-configured-path-with-trailing-slash/"},
{Name: "absolute-output", Path: "/absolute/output"},
{Name: "parent-dir-output", Path: "../parent/dir/../output"},
}
taskPlan.OutputMapping = map[string]string{
"some-other-output": "some-remapped-output",
}
outputVolume1 = runtimetest.NewVolume("output1")
outputVolume2 = runtimetest.NewVolume("output2")
outputVolume3 = runtimetest.NewVolume("output3")
outputVolume4 = runtimetest.NewVolume("output4")
outputVolume5 = runtimetest.NewVolume("output5")
chosenContainer.Mounts = []runtime.VolumeMount{
{
Volume: outputVolume1,
MountPath: "some-artifact-root/some-output-configured-path/",
},
{
Volume: outputVolume2,
MountPath: "some-artifact-root/some-other-output/",
},
{
Volume: outputVolume3,
MountPath: "some-artifact-root/some-output-configured-path-with-trailing-slash/",
},
{
Volume: outputVolume4,
MountPath: "/absolute/output/",
},
{
Volume: outputVolume5,
MountPath: "some-artifact-root/parent/dir/output/",
},
}
})
It("configures them appropriately in the container spec", func() {
Expect(chosenContainer.Spec.Outputs).To(Equal(runtime.OutputPaths{
"some-output": "some-artifact-root/some-output-configured-path/",
"some-other-output": "some-artifact-root/some-other-output/",
"some-trailing-slash-output": "some-artifact-root/some-output-configured-path-with-trailing-slash/",
"absolute-output": "/absolute/output/",
"parent-dir-output": "some-artifact-root/parent/dir/output/",
}))
})
It("registers the outputs in the build repo", func() {
Expect(repo.AsMap()).To(Equal(map[build.ArtifactName]build.ArtifactEntry{
"some-output": {
Artifact: outputVolume1,
FromCache: false,
},
"some-remapped-output": {
Artifact: outputVolume2,
FromCache: false,
},
"some-trailing-slash-output": {
Artifact: outputVolume3,
FromCache: false,
},
"absolute-output": {
Artifact: outputVolume4,
FromCache: false,
},
"parent-dir-output": {
Artifact: outputVolume5,
FromCache: false,
},
}))
})
})
Context("when missing the platform", func() {
BeforeEach(func() {
taskPlan.Config.Platform = ""
})
It("returns the error", func() {
Expect(stepErr).To(HaveOccurred())
})
It("is not successful", func() {
Expect(stepOk).To(BeFalse())
})
})
Context("when missing the path to the executable and ENTRYPOINT/CMD is not defined", func() {
BeforeEach(func() {
taskPlan.Config.Run.Path = ""
})
It("returns the error", func() {
Expect(stepErr).To(HaveOccurred())
Expect(stepErr.Error()).To(ContainSubstring("No executable defined for task. Specify an executable to run in either the task config (run.path) or as an ENTRYPOINT/CMD in the container image."))
})
It("is not successful", func() {
Expect(stepOk).To(BeFalse())
})
Context("when the image's metadata.json is malformed", func() {
BeforeEach(func() {
taskPlan.Config.Run.Path = ""
fakeStreamer.StreamFileReturns(io.NopCloser(strings.NewReader("definitely not json")), nil)
taskPlan.ImageArtifactName = "some-image-artifact"
imageVolume := runtimetest.NewVolume("image-volume")
repo.RegisterArtifact("some-image-artifact", imageVolume, false)
})
It("returns the error", func() {
Expect(stepErr).To(HaveOccurred())
Expect(stepErr.Error()).To(ContainSubstring("error parsing metadata.json from rootfs"))
})
It("is not successful", func() {
Expect(stepOk).To(BeFalse())
})
})
Context("when the image's metadata.json is not present in the image artifact", func() {
BeforeEach(func() {
taskPlan.Config.Run.Path = ""
fakeStreamer.StreamFileReturns(nil, baggageclaim.ErrFileNotFound)
taskPlan.ImageArtifactName = "some-image-artifact"
imageVolume := runtimetest.NewVolume("image-volume")
repo.RegisterArtifact("some-image-artifact", imageVolume, false)
})
It("returns the error", func() {
Expect(stepErr).To(HaveOccurred())
Expect(stepErr.Error()).To(ContainSubstring("file 'metadata.json' not found within artifact 'image-volume'"))
})
It("is not successful", func() {
Expect(stepOk).To(BeFalse())
})
})
})
Context("the task container has an ENTRYPOINT/CMD defined", func() {
BeforeEach(func() {
taskPlan.Config.Run.Path = ""
taskPlan.Config.Run.Args = []string{}
rootfsMetadata := `{
"entrypoint": [
"/bin/sh",
"-c"
],
"cmd": [
"echo hello world"
]
}`
fakeStreamer.StreamFileReturns(io.NopCloser(strings.NewReader(rootfsMetadata)), nil)
taskPlan.ImageArtifactName = "some-image-artifact"
imageVolume := runtimetest.NewVolume("image-volume")
repo.RegisterArtifact("some-image-artifact", imageVolume, false)
})
It("uses the ENTRYPOINT/CMD and passes it back to the task delegate", func() {
Expect(fakeDelegate.SetTaskConfigCallCount()).To(Equal(1))
actualTaskConfig := fakeDelegate.SetTaskConfigArgsForCall(0)
Expect(actualTaskConfig.Run.Path).To(Equal("/bin/sh"))
Expect(actualTaskConfig.Run.Args).To(Equal([]string{"-c", "echo hello world"}))
})
Context("there are args in the task config", func() {
BeforeEach(func() {
taskPlan.Config.Run.Args = []string{"task", "args"}
})
It("they are appended to the ENTRYPOINT/CMD args", func() {
Expect(fakeDelegate.SetTaskConfigCallCount()).To(Equal(1))
actualTaskConfig := fakeDelegate.SetTaskConfigArgsForCall(0)
Expect(actualTaskConfig.Run.Path).To(Equal("/bin/sh"))
Expect(actualTaskConfig.Run.Args).To(Equal([]string{"-c", "echo hello world", "task", "args"}))
})
})
Context("only ENTRYPOINT is specified", func() {
BeforeEach(func() {
taskPlan.Config.Run.Path = ""
taskPlan.Config.Run.Args = []string{}
rootfsMetadata := `{ "entrypoint": [ "some-program", "some-arg" ] }`
fakeStreamer.StreamFileReturns(io.NopCloser(strings.NewReader(rootfsMetadata)), nil)
})
It("is parsed correctly", func() {
Expect(fakeDelegate.SetTaskConfigCallCount()).To(Equal(1))
actualTaskConfig := fakeDelegate.SetTaskConfigArgsForCall(0)
Expect(actualTaskConfig.Run.Path).To(Equal("some-program"))
Expect(actualTaskConfig.Run.Args).To(Equal([]string{"some-arg"}))
})
})
Context("only CMD is specified", func() {
BeforeEach(func() {
taskPlan.Config.Run.Path = ""
taskPlan.Config.Run.Args = []string{}
rootfsMetadata := `{ "cmd": [ "cmd-program", "cmd-arg" ] }`
fakeStreamer.StreamFileReturns(io.NopCloser(strings.NewReader(rootfsMetadata)), nil)
})
It("is parsed correctly", func() {
Expect(fakeDelegate.SetTaskConfigCallCount()).To(Equal(1))
actualTaskConfig := fakeDelegate.SetTaskConfigArgsForCall(0)
Expect(actualTaskConfig.Run.Path).To(Equal("cmd-program"))
Expect(actualTaskConfig.Run.Args).To(Equal([]string{"cmd-arg"}))
})
})
})
Context("when an image artifact name is specified", func() {
BeforeEach(func() {
taskPlan.ImageArtifactName = "some-image-artifact"
})
Context("when the image artifact is registered in the artifact repo", func() {
var imageVolume *runtimetest.Volume
BeforeEach(func() {
imageVolume = runtimetest.NewVolume("image-volume")
repo.RegisterArtifact("some-image-artifact", imageVolume, false)
})
It("configures it in the containerSpec's ImageSpec", func() {
Expect(chosenContainer.Spec.ImageSpec).To(Equal(runtime.ImageSpec{
ImageArtifact: imageVolume,
}))
expectWorkerSpecResourceTypeUnset()
})
Describe("when task config specifies image and/or image resource as well as image artifact", func() {
Context("when streaming the metadata from the worker succeeds", func() {
JustBeforeEach(func() {
Expect(stepErr).ToNot(HaveOccurred())
})
Context("when the task config also specifies a rootfs_uri", func() {
BeforeEach(func() {
taskPlan.Config.RootfsURI = "some-image"
})
It("still uses the image artifact", func() {
Expect(chosenContainer.Spec.ImageSpec).To(Equal(runtime.ImageSpec{
ImageArtifact: imageVolume,
}))
expectWorkerSpecResourceTypeUnset()
})
})
Context("when the task config also specifies image_resource", func() {
BeforeEach(func() {
taskPlan.Config.ImageResource = &atc.ImageResource{
Type: "docker",
Source: atc.Source{"some": "super-secret-source"},
Params: atc.Params{"some": "params"},
Version: atc.Version{"some": "version"},
}
})
It("still uses the image artifact", func() {
Expect(chosenContainer.Spec.ImageSpec).To(Equal(runtime.ImageSpec{
ImageArtifact: imageVolume,
}))
expectWorkerSpecResourceTypeUnset()
})
})
})
})
})
Context("when the image artifact is NOT registered in the artifact repo", func() {
It("returns a MissingTaskImageSourceError", func() {
Expect(stepErr).To(Equal(exec.MissingTaskImageSourceError{"some-image-artifact"}))
})
It("is not successful", func() {
Expect(stepOk).To(BeFalse())
})
})
})
Context("when the image_resource is specified (even if rootfs_uri is configured)", func() {
var fetchedImageSpec runtime.ImageSpec
BeforeEach(func() {
taskPlan.Config.RootfsURI = "some-image"
taskPlan.Config.ImageResource = &atc.ImageResource{
Type: "docker",
Source: atc.Source{"some": "super-secret-source"},
Params: atc.Params{"some": "params"},
}
taskPlan.Tags = atc.Tags{"some", "tags"}
fetchedImageSpec = runtime.ImageSpec{
ImageArtifact: runtimetest.NewVolume("some-volume"),
}
fakeDelegate.FetchImageReturns(fetchedImageSpec, nil)
})
It("succeeds", func() {
Expect(stepErr).ToNot(HaveOccurred())
Expect(stepOk).To(BeTrue())
})
It("fetches the image", func() {
Expect(fakeDelegate.FetchImageCallCount()).To(Equal(1))
_, imageResource, types, privileged, tags, skipInterval := fakeDelegate.FetchImageArgsForCall(0)
Expect(imageResource).To(Equal(atc.ImageResource{
Type: "docker",
Source: atc.Source{"some": "super-secret-source"},
Params: atc.Params{"some": "params"},
}))
Expect(types).To(Equal(taskPlan.ResourceTypes))
Expect(privileged).To(BeFalse())
Expect(tags).To(Equal(atc.Tags{"some", "tags"}))
Expect(skipInterval).To(Equal(false))
})
It("creates the specs with the fetched image", func() {
Expect(chosenContainer.Spec.ImageSpec).To(Equal(fetchedImageSpec))
})
Context("when privileged", func() {
BeforeEach(func() {
taskPlan.Privileged = true
})
It("fetches a privileged image", func() {
Expect(fakeDelegate.FetchImageCallCount()).To(Equal(1))
_, _, _, privileged, _, _ := fakeDelegate.FetchImageArgsForCall(0)
Expect(privileged).To(BeTrue())
})
})
Context("when check skip interval is true", func() {
BeforeEach(func() {
taskPlan.CheckSkipInterval = true
})
It("fetches an image with forced check", func() {
Expect(fakeDelegate.FetchImageCallCount()).To(Equal(1))
_, _, _, _, _, skipInterval := fakeDelegate.FetchImageArgsForCall(0)
Expect(skipInterval).To(BeTrue())
})
})
})
Context("when a run dir and user are specified", func() {
BeforeEach(func() {
taskPlan.Config.Run.Dir = "/some/dir"
taskPlan.Config.Run.User = "some-user"
chosenContainer.ProcessDefs[0].Spec.Dir = "/some/dir"
chosenContainer.ProcessDefs[0].Spec.User = "some-user"
})
It("runs with the specified dir and user", func() {
Expect(stepErr).ToNot(HaveOccurred())
Expect(chosenContainer.RunningProcesses()).To(HaveLen(1))
})
})
Context("when running the task exits with a non-zero status", func() {
BeforeEach(func() {
chosenContainer.ProcessDefs[0].Stub.ExitStatus = 1
})
It("doesn't error", func() {
Expect(stepErr).ToNot(HaveOccurred())
})
It("is not successful", func() {
Expect(stepOk).To(BeFalse())
})
It("finishes the step", func() {
Expect(fakeDelegate.FinishedCallCount()).To(Equal(1))
_, status := fakeDelegate.FinishedArgsForCall(0)
Expect(status).To(Equal(exec.ExitStatus(1)))
})
})
Context("when running the task fails", func() {
BeforeEach(func() {
chosenContainer.ProcessDefs[0].Stub.Err = "failed to run the task"
})
It("returns the error", func() {
Expect(stepErr).To(HaveOccurred())
})
It("is not successful", func() {
Expect(stepOk).To(BeFalse())
})
})
Context("when the task step is interrupted", func() {
BeforeEach(func() {
cancel()
chosenContainer.ProcessDefs[0].Stub.Do = func(ctx context.Context, _ *runtimetest.Process) error {
select {
case <-ctx.Done():
return ctx.Err()
case <-time.After(1 * time.Second):
Fail("didn't return context error")
panic("unreachable")
}
}
})
It("returns the context.Canceled error", func() {
Expect(stepErr).To(Equal(context.Canceled))
})
It("is not successful", func() {
Expect(stepOk).To(BeFalse())
})
It("doesn't register a artifact", func() {
Expect(repo.AsMap()).To(BeEmpty())
})
})
})
})