Skip to content

Commit 0c2ff5c

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 than wait for the TaskRun timeout to abort the TaskRun. Closes tektoncd#1690
1 parent f0954c7 commit 0c2ff5c

File tree

17 files changed

+262
-32
lines changed

17 files changed

+262
-32
lines changed

cmd/entrypoint/main.go

+12-1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ limitations under the License.
1717
package main
1818

1919
import (
20+
"context"
2021
"flag"
2122
"log"
2223
"os"
@@ -40,6 +41,7 @@ var (
4041
terminationPath = flag.String("termination_path", "/tekton/termination", "If specified, file to write upon termination")
4142
results = flag.String("results", "", "If specified, list of file names that might contain task results")
4243
waitPollingInterval = time.Second
44+
timeout = flag.String("timeout", "", "If specified, sets timeout for step")
4345
)
4446

4547
func main() {
@@ -78,7 +80,16 @@ func main() {
7880
log.Printf("non-fatal error copying credentials: %q", err)
7981
}
8082

81-
if err := e.Go(); err != nil {
83+
// Add timeout to context if a non-zero timeout is specified for a step
84+
ctx := context.Background()
85+
timeoutContext, _ := time.ParseDuration(*timeout)
86+
var cancel context.CancelFunc
87+
if timeoutContext != time.Duration(0) {
88+
ctx, cancel = context.WithTimeout(ctx, timeoutContext)
89+
defer cancel()
90+
}
91+
92+
if err := e.Go(ctx); err != nil {
8293
switch t := err.(type) {
8394
case skipError:
8495
log.Print("Skipping step because a previous step failed")

cmd/entrypoint/runner.go

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

33
import (
4+
"context"
45
"os"
56
"os/exec"
67
"os/signal"
@@ -19,7 +20,7 @@ type realRunner struct {
1920

2021
var _ entrypoint.Runner = (*realRunner)(nil)
2122

22-
func (rr *realRunner) Run(args ...string) error {
23+
func (rr *realRunner) Run(ctx context.Context, args ...string) error {
2324
if len(args) == 0 {
2425
return nil
2526
}
@@ -33,7 +34,7 @@ func (rr *realRunner) Run(args ...string) error {
3334
signal.Notify(rr.signals)
3435
defer signal.Reset()
3536

36-
cmd := exec.Command(name, args...)
37+
cmd := exec.CommandContext(ctx, name, args...)
3738
cmd.Stdout = os.Stdout
3839
cmd.Stderr = os.Stderr
3940
// dedicated PID group used to forward signals to

cmd/entrypoint/runner_test.go

+17-1
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
package main
22

33
import (
4+
"context"
45
"os"
56
"syscall"
67
"testing"
8+
"time"
79
)
810

911
// TestRealRunnerSignalForwarding will artificially put an interrupt signal (SIGINT) in the rr.signals chan.
@@ -14,9 +16,23 @@ func TestRealRunnerSignalForwarding(t *testing.T) {
1416
rr := realRunner{}
1517
rr.signals = make(chan os.Signal, 1)
1618
rr.signals <- syscall.SIGINT
17-
if err := rr.Run("sleep", "3600"); err.Error() == "signal: interrupt" {
19+
if err := rr.Run(context.Background(), "sleep", "3600"); err.Error() == "signal: interrupt" {
1820
t.Logf("SIGINT forwarded to Entrypoint")
1921
} else {
2022
t.Fatalf("Unexpected error received: %v", err)
2123
}
2224
}
25+
26+
// TestRealRunnerTimeout tests whether cmd is killed after time.Second even though it's supposed to sleep for an hour.
27+
func TestRealRunnerTimeout(t *testing.T) {
28+
rr := realRunner{}
29+
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
30+
defer cancel()
31+
// For seconds and higher time units, err.Error() returns "signal: killed".
32+
// For microseconds and lower time units, err.Error() returns "context deadline exceeded".
33+
if err := rr.Run(ctx, "sleep", "3600"); err.Error() == "signal: killed" || err.Error() == "context deadline exceeded" {
34+
t.Logf("Timeout observed")
35+
} else {
36+
t.Fatalf("Unexpected error received: %v", err)
37+
}
38+
}

docs/tasks.md

+17
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,24 @@ steps:
241241
#!/usr/bin/env bash
242242
/bin/my-binary
243243
```
244+
#### Specifying a timeout
244245

246+
A step can specify a `timeout` field. If the `Step` execution time exceeds the specified timeout, the `Step` and consequently the entire `TaskRun` is canceled.
247+
An accompanying error message is output under `status.conditions.message`.
248+
249+
The format for a timeout is a duration string as specified in the [Go time package](https://golang.org/src/time/format.go?s=40541:40587#L1364) (e.g. 1s or 1ms).
250+
251+
The example `Step` below is supposed to sleep for 60 seconds but will be canceled by the specified 5 second timeout.
252+
```yaml
253+
steps:
254+
- name: willTimeout
255+
image: ubuntu
256+
script: |
257+
#!/usr/bin/env bash
258+
echo "I am supposed to sleep for 60 seconds!"
259+
sleep 60
260+
timeout: 5s
261+
```
245262
### Specifying `Parameters`
246263

247264
You can specify parameters, such as compilation flags or artifact names, that you want to supply to the `Task` at execution time.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
kind: TaskRun
2+
apiVersion: tekton.dev/v1beta1
3+
metadata:
4+
generateName: timeout-test-
5+
spec:
6+
taskSpec:
7+
steps:
8+
- name: wait
9+
image: ubuntu
10+
script: |
11+
#!/usr/bin/env bash
12+
echo "I am supposed to sleep for 60 seconds!"
13+
sleep 60
14+
timeout: 5s

pkg/apis/pipeline/v1beta1/task_types.go

+5-2
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ const (
2626
TaskRunResultType ResultType = "TaskRunResult"
2727
// PipelineResourceResultType default pipeline result value
2828
PipelineResourceResultType ResultType = "PipelineResourceResult"
29+
// InternalTektonResultType default internal tekton result value
30+
InternalTektonResultType ResultType = "InternalTektonResult"
2931
// UnknownResultType default unknown result type value
3032
UnknownResultType ResultType = ""
3133
)
@@ -123,10 +125,11 @@ type Step struct {
123125
//
124126
// If Script is not empty, the Step cannot have an Command or Args.
125127
Script string `json:"script,omitempty"`
128+
// If step times out after Timeout, pod is terminated
129+
Timeout string `json:"timeout,omitempty"`
126130
}
127131

128-
// Sidecar embeds the Container type, which allows it to include fields not
129-
// 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.
130133
type Sidecar struct {
131134
corev1.Container `json:",inline"`
132135

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

+11-3
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"
@@ -73,7 +74,7 @@ type Waiter interface {
7374

7475
// Runner encapsulates running commands.
7576
type Runner interface {
76-
Run(args ...string) error
77+
Run(ctx context.Context, args ...string) error
7778
}
7879

7980
// PostWriter encapsulates writing a file when complete.
@@ -84,7 +85,7 @@ type PostWriter interface {
8485

8586
// Go optionally waits for a file, runs the command, and writes a
8687
// post file.
87-
func (e Entrypointer) Go() error {
88+
func (e Entrypointer) Go(ctx context.Context) error {
8889
prod, _ := zap.NewProduction()
8990
logger := prod.Sugar()
9091

@@ -118,7 +119,14 @@ func (e Entrypointer) Go() error {
118119
Value: time.Now().Format(timeFormat),
119120
})
120121

121-
err := e.Runner.Run(e.Args...)
122+
err := e.Runner.Run(ctx, e.Args...)
123+
if ctx.Err() == context.DeadlineExceeded {
124+
output = append(output, v1beta1.PipelineResourceResult{
125+
Key: "Reason",
126+
Value: "TimeoutExceeded",
127+
ResultType: v1beta1.InternalTektonResultType,
128+
})
129+
}
122130

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

pkg/entrypoint/entrypointer_test.go

+5-4
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
"encoding/json"
2122
"errors"
2223
"io/ioutil"
@@ -76,7 +77,7 @@ func TestEntrypointerFailures(t *testing.T) {
7677
Runner: fr,
7778
PostWriter: fpw,
7879
TerminationPath: "termination",
79-
}.Go()
80+
}.Go(context.Background())
8081
if err == nil {
8182
t.Fatalf("Entrypointer didn't fail")
8283
}
@@ -139,7 +140,7 @@ func TestEntrypointer(t *testing.T) {
139140
Runner: fr,
140141
PostWriter: fpw,
141142
TerminationPath: "termination",
142-
}.Go()
143+
}.Go(context.Background())
143144
if err != nil {
144145
t.Fatalf("Entrypointer failed: %v", err)
145146
}
@@ -214,7 +215,7 @@ func (f *fakeWaiter) Wait(file string, _ bool) error {
214215

215216
type fakeRunner struct{ args *[]string }
216217

217-
func (f *fakeRunner) Run(args ...string) error {
218+
func (f *fakeRunner) Run(ctx context.Context, args ...string) error {
218219
f.args = &args
219220
return nil
220221
}
@@ -232,7 +233,7 @@ func (f *fakeErrorWaiter) Wait(file string, expectContent bool) error {
232233

233234
type fakeErrorRunner struct{ args *[]string }
234235

235-
func (f *fakeErrorRunner) Run(args ...string) error {
236+
func (f *fakeErrorRunner) Run(ctx context.Context, args ...string) error {
236237
f.args = &args
237238
return errors.New("runner failed")
238239
}

pkg/pod/entrypoint.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ var (
8686
// method, using entrypoint_lookup.go.
8787
//
8888
// 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) {
89+
func orderContainers(entrypointImage string, extraEntrypointArgs []string, steps []corev1.Container, stepTimeouts []string, results []v1beta1.TaskResult) (corev1.Container, []corev1.Container, error) {
9090
initContainer := corev1.Container{
9191
Name: "place-tools",
9292
Image: entrypointImage,
@@ -120,6 +120,7 @@ func orderContainers(entrypointImage string, extraEntrypointArgs []string, steps
120120
}
121121
argsForEntrypoint = append(argsForEntrypoint, extraEntrypointArgs...)
122122
argsForEntrypoint = append(argsForEntrypoint, resultArgument(steps, results)...)
123+
argsForEntrypoint = append(argsForEntrypoint, "-timeout", stepTimeouts[i])
123124

124125
cmd, args := s.Command, s.Args
125126
if len(cmd) == 0 {

pkg/pod/entrypoint_test.go

+20-4
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ func TestOrderContainers(t *testing.T) {
5656
"-wait_file_content",
5757
"-post_file", "/tekton/tools/0",
5858
"-termination_path", "/tekton/termination",
59+
"-timeout",
60+
"",
5961
"-entrypoint", "cmd", "--",
6062
"arg1", "arg2",
6163
},
@@ -68,6 +70,8 @@ func TestOrderContainers(t *testing.T) {
6870
"-wait_file", "/tekton/tools/0",
6971
"-post_file", "/tekton/tools/1",
7072
"-termination_path", "/tekton/termination",
73+
"-timeout",
74+
"",
7175
"-entrypoint", "cmd1", "--",
7276
"cmd2", "cmd3",
7377
"arg1", "arg2",
@@ -81,13 +85,15 @@ func TestOrderContainers(t *testing.T) {
8185
"-wait_file", "/tekton/tools/1",
8286
"-post_file", "/tekton/tools/2",
8387
"-termination_path", "/tekton/termination",
88+
"-timeout",
89+
"",
8490
"-entrypoint", "cmd", "--",
8591
"arg1", "arg2",
8692
},
8793
VolumeMounts: []corev1.VolumeMount{toolsMount},
8894
TerminationMessagePath: "/tekton/termination",
8995
}}
90-
gotInit, got, err := orderContainers(images.EntrypointImage, []string{}, steps, nil)
96+
gotInit, got, err := orderContainers(images.EntrypointImage, []string{}, steps, make([]string, len(steps)), nil)
9197
if err != nil {
9298
t.Fatalf("orderContainers: %v", err)
9399
}
@@ -138,6 +144,8 @@ func TestEntryPointResults(t *testing.T) {
138144
"-post_file", "/tekton/tools/0",
139145
"-termination_path", "/tekton/termination",
140146
"-results", "sum,sub",
147+
"-timeout",
148+
"",
141149
"-entrypoint", "cmd", "--",
142150
"arg1", "arg2",
143151
},
@@ -151,6 +159,8 @@ func TestEntryPointResults(t *testing.T) {
151159
"-post_file", "/tekton/tools/1",
152160
"-termination_path", "/tekton/termination",
153161
"-results", "sum,sub",
162+
"-timeout",
163+
"",
154164
"-entrypoint", "cmd1", "--",
155165
"cmd2", "cmd3",
156166
"arg1", "arg2",
@@ -165,13 +175,15 @@ func TestEntryPointResults(t *testing.T) {
165175
"-post_file", "/tekton/tools/2",
166176
"-termination_path", "/tekton/termination",
167177
"-results", "sum,sub",
178+
"-timeout",
179+
"",
168180
"-entrypoint", "cmd", "--",
169181
"arg1", "arg2",
170182
},
171183
VolumeMounts: []corev1.VolumeMount{toolsMount},
172184
TerminationMessagePath: "/tekton/termination",
173185
}}
174-
_, got, err := orderContainers(images.EntrypointImage, []string{}, steps, results)
186+
_, got, err := orderContainers(images.EntrypointImage, []string{}, steps, make([]string, len(steps)), results)
175187
if err != nil {
176188
t.Fatalf("orderContainers: %v", err)
177189
}
@@ -203,13 +215,15 @@ func TestEntryPointResultsSingleStep(t *testing.T) {
203215
"-post_file", "/tekton/tools/0",
204216
"-termination_path", "/tekton/termination",
205217
"-results", "sum,sub",
218+
"-timeout",
219+
"",
206220
"-entrypoint", "cmd", "--",
207221
"arg1", "arg2",
208222
},
209223
VolumeMounts: []corev1.VolumeMount{toolsMount, downwardMount},
210224
TerminationMessagePath: "/tekton/termination",
211225
}}
212-
_, got, err := orderContainers(images.EntrypointImage, []string{}, steps, results)
226+
_, got, err := orderContainers(images.EntrypointImage, []string{}, steps, make([]string, len(steps)), results)
213227
if err != nil {
214228
t.Fatalf("orderContainers: %v", err)
215229
}
@@ -237,13 +251,15 @@ func TestEntryPointSingleResultsSingleStep(t *testing.T) {
237251
"-post_file", "/tekton/tools/0",
238252
"-termination_path", "/tekton/termination",
239253
"-results", "sum",
254+
"-timeout",
255+
"",
240256
"-entrypoint", "cmd", "--",
241257
"arg1", "arg2",
242258
},
243259
VolumeMounts: []corev1.VolumeMount{toolsMount, downwardMount},
244260
TerminationMessagePath: "/tekton/termination",
245261
}}
246-
_, got, err := orderContainers(images.EntrypointImage, []string{}, steps, results)
262+
_, got, err := orderContainers(images.EntrypointImage, []string{}, steps, make([]string, len(steps)), results)
247263
if err != nil {
248264
t.Fatalf("orderContainers: %v", err)
249265
}

0 commit comments

Comments
 (0)