Skip to content

Commit ea1f523

Browse files
committed
Add timeout setting to Steps
This feature allows a Task author to specify a Step timeout in a Taskrun. An example use case is when a Task author would like to execute a Step for setting up an execution environment. One may expect this Step to execute within a few seconds. If the execution time takes longer than expected one may rather want to fail fast instead of waiting for the TaskRun timeout to abort the TaskRun. Closes tektoncd#1690
1 parent ce6b61a commit ea1f523

File tree

17 files changed

+257
-36
lines changed

17 files changed

+257
-36
lines changed

cmd/entrypoint/main.go

+5
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ var (
4040
terminationPath = flag.String("termination_path", "/tekton/termination", "If specified, file to write upon termination")
4141
results = flag.String("results", "", "If specified, list of file names that might contain task results")
4242
waitPollingInterval = time.Second
43+
timeout = flag.String("timeout", "", "If specified, sets timeout for step")
4344
)
4445

4546
func main() {
@@ -72,6 +73,10 @@ func main() {
7273
Results: strings.Split(*results, ","),
7374
}
7475

76+
if timeout != nil {
77+
e.Timeout = *timeout
78+
}
79+
7580
// Copy any creds injected by the controller into the $HOME directory of the current
7681
// user so that they're discoverable by git / ssh.
7782
if err := credentials.CopyCredsToHome(credentials.CredsInitCredentials); err != nil {

cmd/entrypoint/runner.go

+19-2
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
package main
22

33
import (
4+
"context"
45
"os"
56
"os/exec"
67
"os/signal"
78
"syscall"
9+
"time"
810

911
"github.com/tektoncd/pipeline/pkg/entrypoint"
1012
)
@@ -19,7 +21,7 @@ type realRunner struct {
1921

2022
var _ entrypoint.Runner = (*realRunner)(nil)
2123

22-
func (rr *realRunner) Run(args ...string) error {
24+
func (rr *realRunner) Run(timeout string, args ...string) error {
2325
if len(args) == 0 {
2426
return nil
2527
}
@@ -33,7 +35,16 @@ func (rr *realRunner) Run(args ...string) error {
3335
signal.Notify(rr.signals)
3436
defer signal.Reset()
3537

36-
cmd := exec.Command(name, args...)
38+
// Add timeout to context if a non-zero timeout is specified for a step
39+
ctx := context.Background()
40+
var cancel context.CancelFunc
41+
timeoutContext, _ := time.ParseDuration(timeout)
42+
if timeoutContext != time.Duration(0) {
43+
ctx, cancel = context.WithTimeout(ctx, timeoutContext)
44+
defer cancel()
45+
}
46+
47+
cmd := exec.CommandContext(ctx, name, args...)
3748
cmd.Stdout = os.Stdout
3849
cmd.Stderr = os.Stderr
3950
// dedicated PID group used to forward signals to
@@ -42,6 +53,9 @@ func (rr *realRunner) Run(args ...string) error {
4253

4354
// Start defined command
4455
if err := cmd.Start(); err != nil {
56+
if ctx.Err() == context.DeadlineExceeded {
57+
return context.DeadlineExceeded
58+
}
4559
return err
4660
}
4761

@@ -57,6 +71,9 @@ func (rr *realRunner) Run(args ...string) error {
5771

5872
// Wait for command to exit
5973
if err := cmd.Wait(); err != nil {
74+
if ctx.Err() == context.DeadlineExceeded {
75+
return context.DeadlineExceeded
76+
}
6077
return err
6178
}
6279

cmd/entrypoint/runner_test.go

+15-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package main
22

33
import (
4+
"context"
45
"os"
56
"syscall"
67
"testing"
@@ -14,9 +15,22 @@ func TestRealRunnerSignalForwarding(t *testing.T) {
1415
rr := realRunner{}
1516
rr.signals = make(chan os.Signal, 1)
1617
rr.signals <- syscall.SIGINT
17-
if err := rr.Run("sleep", "3600"); err.Error() == "signal: interrupt" {
18+
if err := rr.Run("", "sleep", "3600"); err.Error() == "signal: interrupt" {
1819
t.Logf("SIGINT forwarded to Entrypoint")
1920
} else {
2021
t.Fatalf("Unexpected error received: %v", err)
2122
}
2223
}
24+
25+
// TestRealRunnerTimeout tests whether cmd is killed after a millisecond even though it's supposed to sleep for 10 milliseconds.
26+
func TestRealRunnerTimeout(t *testing.T) {
27+
rr := realRunner{}
28+
timeout := "1ms"
29+
if err := rr.Run(timeout, "sleep", "0.01"); err != nil {
30+
if err != context.DeadlineExceeded {
31+
t.Fatalf("unexpected error received: %v", err)
32+
}
33+
} else {
34+
t.Fatalf("step didn't timeout")
35+
}
36+
}

docs/tasks.md

+19
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ weight: 1
1212
- [Defining `Steps`](#defining-steps)
1313
- [Reserved directories](#reserved-directories)
1414
- [Running scripts within `Steps`](#running-scripts-within-steps)
15+
- [Specifying a timeout](#specifying-a-timeout)
1516
- [Specifying `Parameters`](#specifying-parameters)
1617
- [Specifying `Resources`](#specifying-resources)
1718
- [Specifying `Workspaces`](#specifying-workspaces)
@@ -241,7 +242,25 @@ steps:
241242
#!/usr/bin/env bash
242243
/bin/my-binary
243244
```
245+
#### Specifying a timeout
244246

247+
A `Step` can specify a `timeout` field. If the `Step` execution time exceeds the specified timeout, this `Step` and any subsequent `Steps` are canceled.
248+
Specifically, the running process spawned by the `Step` is killed.
249+
An accompanying log is output under the `status.conditions.message` field of the `TaskRun` YAML.
250+
251+
The format for a timeout is a duration string as specified in the [Go time package](https://golang.org/pkg/time/#ParseDuration) (e.g. 1s or 1ms).
252+
253+
The example `Step` below is supposed to sleep for 60 seconds but will be canceled by the specified 5 second timeout.
254+
```yaml
255+
steps:
256+
- name: sleep-then-timeout
257+
image: ubuntu
258+
script: |
259+
#!/usr/bin/env bash
260+
echo "I am supposed to sleep for 60 seconds!"
261+
sleep 60
262+
timeout: 5s
263+
```
245264
### Specifying `Parameters`
246265

247266
You can specify parameters, such as compilation flags or artifact names, that you want to supply to the `Task` at execution time.

internal/builder/v1beta1/pod.go

+3
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ type PodSpecOp func(*corev1.PodSpec)
3333
// PodStatusOp is an operation which modifies a PodStatus struct.
3434
type PodStatusOp func(status *corev1.PodStatus)
3535

36+
// PodContainerStatusOp is an operation which modifies a ContainerStatus struct.
37+
type PodContainerStatusOp func(status *corev1.ContainerStatus)
38+
3639
// Pod creates a Pod with default values.
3740
// Any number of Pod modifiers can be passed to transform it.
3841
func Pod(name string, ops ...PodOp) *corev1.Pod {

pkg/apis/pipeline/v1beta1/task_types.go

+3-2
Original file line numberDiff line numberDiff line change
@@ -125,10 +125,11 @@ type Step struct {
125125
//
126126
// If Script is not empty, the Step cannot have an Command or Args.
127127
Script string `json:"script,omitempty"`
128+
// If step times out after Timeout, pod is terminated
129+
Timeout string `json:"timeout,omitempty"`
128130
}
129131

130-
// Sidecar embeds the Container type, which allows it to include fields not
131-
// provided by Container.
132+
// Sidecar has nearly the same data structure as Step, consisting of a Container and an optional Script, but does not have the ability to timeout.
132133
type Sidecar struct {
133134
corev1.Container `json:",inline"`
134135

pkg/apis/pipeline/v1beta1/task_validation.go

+7
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"fmt"
2222
"path/filepath"
2323
"strings"
24+
"time"
2425

2526
"github.com/tektoncd/pipeline/pkg/apis/validate"
2627
"github.com/tektoncd/pipeline/pkg/substitution"
@@ -189,6 +190,12 @@ func validateSteps(steps []Step) *apis.FieldError {
189190
names.Insert(s.Name)
190191
}
191192

193+
if s.Timeout != "" {
194+
if _, err := time.ParseDuration(s.Timeout); err != nil {
195+
return apis.ErrInvalidValue(s.Timeout, "timeout")
196+
}
197+
}
198+
192199
for _, vm := range s.VolumeMounts {
193200
if strings.HasPrefix(vm.MountPath, "/tekton/") &&
194201
!strings.HasPrefix(vm.MountPath, "/tekton/home") {

pkg/entrypoint/entrypointer.go

+13-2
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ limitations under the License.
1717
package entrypoint
1818

1919
import (
20+
"context"
2021
"fmt"
2122
"io/ioutil"
2223
"os"
@@ -63,6 +64,8 @@ type Entrypointer struct {
6364

6465
// Results is the set of files that might contain task results
6566
Results []string
67+
//Timeout is an optional user-specified flag for timing out Steps
68+
Timeout string
6669
}
6770

6871
// Waiter encapsulates waiting for files to exist.
@@ -73,7 +76,7 @@ type Waiter interface {
7376

7477
// Runner encapsulates running commands.
7578
type Runner interface {
76-
Run(args ...string) error
79+
Run(timeout string, args ...string) error
7780
}
7881

7982
// PostWriter encapsulates writing a file when complete.
@@ -114,13 +117,21 @@ func (e Entrypointer) Go() error {
114117
if e.Entrypoint != "" {
115118
e.Args = append([]string{e.Entrypoint}, e.Args...)
116119
}
120+
117121
output = append(output, v1beta1.PipelineResourceResult{
118122
Key: "StartedAt",
119123
Value: time.Now().Format(timeFormat),
120124
ResultType: v1beta1.InternalTektonResultType,
121125
})
122126

123-
err := e.Runner.Run(e.Args...)
127+
err := e.Runner.Run(e.Timeout, e.Args...)
128+
if err == context.DeadlineExceeded {
129+
output = append(output, v1beta1.PipelineResourceResult{
130+
Key: "Reason",
131+
Value: "TimeoutExceeded",
132+
ResultType: v1beta1.InternalTektonResultType,
133+
})
134+
}
124135

125136
// Write the post file *no matter what*
126137
e.WritePostFile(e.PostFile, err)

pkg/entrypoint/entrypointer_test.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,7 @@ func (f *fakeWaiter) Wait(file string, _ bool) error {
214214

215215
type fakeRunner struct{ args *[]string }
216216

217-
func (f *fakeRunner) Run(args ...string) error {
217+
func (f *fakeRunner) Run(timeout string, args ...string) error {
218218
f.args = &args
219219
return nil
220220
}
@@ -232,7 +232,7 @@ func (f *fakeErrorWaiter) Wait(file string, expectContent bool) error {
232232

233233
type fakeErrorRunner struct{ args *[]string }
234234

235-
func (f *fakeErrorRunner) Run(args ...string) error {
235+
func (f *fakeErrorRunner) Run(timeout string, args ...string) error {
236236
f.args = &args
237237
return errors.New("runner failed")
238238
}

pkg/pod/entrypoint.go

+6-5
Original file line numberDiff line numberDiff line change
@@ -84,9 +84,7 @@ var (
8484
// Containers must have Command specified; if the user didn't specify a
8585
// command, we must have fetched the image's ENTRYPOINT before calling this
8686
// method, using entrypoint_lookup.go.
87-
//
88-
// TODO(#1605): Also use entrypoint injection to order sidecar start/stop.
89-
func orderContainers(entrypointImage string, extraEntrypointArgs []string, steps []corev1.Container, results []v1beta1.TaskResult) (corev1.Container, []corev1.Container, error) {
87+
func orderContainers(entrypointImage string, commonExtraEntrypointArgs []string, extraEntrypointArgs [][]string, steps []corev1.Container, results []v1beta1.TaskResult) (corev1.Container, []corev1.Container, error) {
9088
initContainer := corev1.Container{
9189
Name: "place-tools",
9290
Image: entrypointImage,
@@ -118,7 +116,10 @@ func orderContainers(entrypointImage string, extraEntrypointArgs []string, steps
118116
"-termination_path", terminationPath,
119117
}
120118
}
121-
argsForEntrypoint = append(argsForEntrypoint, extraEntrypointArgs...)
119+
argsForEntrypoint = append(argsForEntrypoint, commonExtraEntrypointArgs...)
120+
if extraEntrypointArgs[i] != nil {
121+
argsForEntrypoint = append(argsForEntrypoint, extraEntrypointArgs[i]...)
122+
}
122123
argsForEntrypoint = append(argsForEntrypoint, resultArgument(steps, results)...)
123124

124125
cmd, args := s.Command, s.Args
@@ -236,6 +237,6 @@ func isContainerSidecar(name string) bool { return strings.HasPrefix(name, sidec
236237
// trimStepPrefix returns the container name, stripped of its step prefix.
237238
func trimStepPrefix(name string) string { return strings.TrimPrefix(name, stepPrefix) }
238239

239-
// trimSidecarPrefix returns the container name, stripped of its sidecar
240+
// TrimSidecarPrefix returns the container name, stripped of its sidecar
240241
// prefix.
241242
func TrimSidecarPrefix(name string) string { return strings.TrimPrefix(name, sidecarPrefix) }

pkg/pod/entrypoint_test.go

+4-4
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ func TestOrderContainers(t *testing.T) {
8787
VolumeMounts: []corev1.VolumeMount{toolsMount},
8888
TerminationMessagePath: "/tekton/termination",
8989
}}
90-
gotInit, got, err := orderContainers(images.EntrypointImage, []string{}, steps, nil)
90+
gotInit, got, err := orderContainers(images.EntrypointImage, []string{}, make([][]string, len(steps)), steps, nil)
9191
if err != nil {
9292
t.Fatalf("orderContainers: %v", err)
9393
}
@@ -171,7 +171,7 @@ func TestEntryPointResults(t *testing.T) {
171171
VolumeMounts: []corev1.VolumeMount{toolsMount},
172172
TerminationMessagePath: "/tekton/termination",
173173
}}
174-
_, got, err := orderContainers(images.EntrypointImage, []string{}, steps, results)
174+
_, got, err := orderContainers(images.EntrypointImage, []string{}, make([][]string, len(steps)), steps, results)
175175
if err != nil {
176176
t.Fatalf("orderContainers: %v", err)
177177
}
@@ -209,7 +209,7 @@ func TestEntryPointResultsSingleStep(t *testing.T) {
209209
VolumeMounts: []corev1.VolumeMount{toolsMount, downwardMount},
210210
TerminationMessagePath: "/tekton/termination",
211211
}}
212-
_, got, err := orderContainers(images.EntrypointImage, []string{}, steps, results)
212+
_, got, err := orderContainers(images.EntrypointImage, []string{}, make([][]string, len(steps)), steps, results)
213213
if err != nil {
214214
t.Fatalf("orderContainers: %v", err)
215215
}
@@ -243,7 +243,7 @@ func TestEntryPointSingleResultsSingleStep(t *testing.T) {
243243
VolumeMounts: []corev1.VolumeMount{toolsMount, downwardMount},
244244
TerminationMessagePath: "/tekton/termination",
245245
}}
246-
_, got, err := orderContainers(images.EntrypointImage, []string{}, steps, results)
246+
_, got, err := orderContainers(images.EntrypointImage, []string{}, make([][]string, len(steps)), steps, results)
247247
if err != nil {
248248
t.Fatalf("orderContainers: %v", err)
249249
}

pkg/pod/pod.go

+14-1
Original file line numberDiff line numberDiff line change
@@ -139,9 +139,11 @@ func (b *Builder) Build(ctx context.Context, taskRun *v1beta1.TaskRun, taskSpec
139139
return nil, err
140140
}
141141

142+
extraEntrypointArgs := returnTimeoutArgs(taskSpec.Steps)
143+
142144
// Rewrite steps with entrypoint binary. Append the entrypoint init
143145
// container to place the entrypoint binary.
144-
entrypointInit, stepContainers, err := orderContainers(b.Images.EntrypointImage, credEntrypointArgs, stepContainers, taskSpec.Results)
146+
entrypointInit, stepContainers, err := orderContainers(b.Images.EntrypointImage, credEntrypointArgs, extraEntrypointArgs, stepContainers, taskSpec.Results)
145147
if err != nil {
146148
return nil, err
147149
}
@@ -398,3 +400,14 @@ func shouldAddReadyAnnotationOnPodCreate(ctx context.Context, sidecars []v1beta1
398400
cfg := config.FromContextOrDefaults(ctx)
399401
return !cfg.FeatureFlags.RunningInEnvWithInjectedSidecars
400402
}
403+
404+
// returnTimeoutArgs returns a string array of timeout arguments for steps
405+
func returnTimeoutArgs(steps []v1beta1.Step) [][]string {
406+
stepTimeouts := make([][]string, len(steps))
407+
for i, s := range steps {
408+
if s.Timeout != "" {
409+
stepTimeouts[i] = append(stepTimeouts[i], "-timeout", s.Timeout)
410+
}
411+
}
412+
return stepTimeouts
413+
}

0 commit comments

Comments
 (0)