Skip to content

Commit 66096db

Browse files
Peaorltekton-robot
authored andcommitted
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 #1690
1 parent fccced4 commit 66096db

18 files changed

+342
-60
lines changed

cmd/entrypoint/main.go

+2
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ var (
4141
terminationPath = flag.String("termination_path", "/tekton/termination", "If specified, file to write upon termination")
4242
results = flag.String("results", "", "If specified, list of file names that might contain task results")
4343
waitPollingInterval = time.Second
44+
timeout = flag.Duration("timeout", time.Duration(0), "If specified, sets timeout for step")
4445
)
4546

4647
func cp(src, dst string) error {
@@ -103,6 +104,7 @@ func main() {
103104
Runner: &realRunner{},
104105
PostWriter: &realPostWriter{},
105106
Results: strings.Split(*results, ","),
107+
Timeout: timeout,
106108
}
107109

108110
// Copy any creds injected by the controller into the $HOME directory of the current

cmd/entrypoint/runner.go

+9-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
@@ -42,6 +43,9 @@ func (rr *realRunner) Run(args ...string) error {
4243

4344
// Start defined command
4445
if err := cmd.Start(); err != nil {
46+
if ctx.Err() == context.DeadlineExceeded {
47+
return context.DeadlineExceeded
48+
}
4549
return err
4650
}
4751

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

5862
// Wait for command to exit
5963
if err := cmd.Wait(); err != nil {
64+
if ctx.Err() == context.DeadlineExceeded {
65+
return context.DeadlineExceeded
66+
}
6067
return err
6168
}
6269

cmd/entrypoint/runner_test.go

+18-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,24 @@ 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 a millisecond even though it's supposed to sleep for 10 milliseconds.
27+
func TestRealRunnerTimeout(t *testing.T) {
28+
rr := realRunner{}
29+
timeout := time.Millisecond
30+
ctx, cancel := context.WithTimeout(context.Background(), timeout)
31+
defer cancel()
32+
if err := rr.Run(ctx, "sleep", "0.01"); err != nil {
33+
if err != context.DeadlineExceeded {
34+
t.Fatalf("unexpected error received: %v", err)
35+
}
36+
} else {
37+
t.Fatalf("step didn't timeout")
38+
}
39+
}

docs/tasks.md

+21
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,27 @@ steps:
241242
#!/usr/bin/env bash
242243
/bin/my-binary
243244
```
245+
#### Specifying a timeout
244246

247+
A `Step` can specify a `timeout` field.
248+
If the `Step` execution time exceeds the specified timeout, the `Step` kills
249+
its running process and any subsequent `Steps` in the `TaskRun` will not be
250+
executed. The `TaskRun` is placed into a `Failed` condition. An accompanying log
251+
describing which `Step` timed out is written as the `Failed` condition's message.
252+
253+
The timeout specification follows the duration format as specified in the [Go time package](https://golang.org/pkg/time/#ParseDuration) (e.g. 1s or 1ms).
254+
255+
The example `Step` below is supposed to sleep for 60 seconds but will be canceled by the specified 5 second timeout.
256+
```yaml
257+
steps:
258+
- name: sleep-then-timeout
259+
image: ubuntu
260+
script: |
261+
#!/usr/bin/env bash
262+
echo "I am supposed to sleep for 60 seconds!"
263+
sleep 60
264+
timeout: 5s
265+
```
245266
### Specifying `Parameters`
246267

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

examples/v1beta1/taskruns/workspace-in-sidecar.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ apiVersion: tekton.dev/v1beta1
1010
metadata:
1111
generateName: workspace-in-sidecar-
1212
spec:
13-
timeout: 30s
13+
timeout: 60s
1414
workspaces:
1515
- name: signals
1616
emptyDir: {}

pkg/apis/pipeline/v1beta1/task_types.go

+4-2
Original file line numberDiff line numberDiff line change
@@ -125,10 +125,12 @@ 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+
// Timeout is the time after which the step times out. Defaults to never.
129+
// Refer to Go's ParseDuration documentation for expected format: https://golang.org/pkg/time/#ParseDuration
130+
Timeout *metav1.Duration `json:"timeout,omitempty"`
128131
}
129132

130-
// Sidecar embeds the Container type, which allows it to include fields not
131-
// provided by Container.
133+
// 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.
132134
type Sidecar struct {
133135
corev1.Container `json:",inline"`
134136

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"
@@ -159,6 +160,12 @@ func validateStep(s Step, names sets.String) (errs *apis.FieldError) {
159160
names.Insert(s.Name)
160161
}
161162

163+
if s.Timeout != nil {
164+
if s.Timeout.Duration < time.Duration(0) {
165+
return apis.ErrInvalidValue(s.Timeout.Duration, "negative timeout")
166+
}
167+
}
168+
162169
for j, vm := range s.VolumeMounts {
163170
if strings.HasPrefix(vm.MountPath, "/tekton/") &&
164171
!strings.HasPrefix(vm.MountPath, "/tekton/home") {

pkg/apis/pipeline/v1beta1/task_validation_test.go

+13
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,14 @@ package v1beta1_test
1919
import (
2020
"context"
2121
"testing"
22+
"time"
2223

2324
"github.com/google/go-cmp/cmp"
2425
"github.com/google/go-cmp/cmp/cmpopts"
2526
"github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1"
2627
"github.com/tektoncd/pipeline/test/diff"
2728
corev1 "k8s.io/api/core/v1"
29+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2830
"knative.dev/pkg/apis"
2931
)
3032

@@ -930,6 +932,17 @@ func TestTaskSpecValidateError(t *testing.T) {
930932
Message: `non-existent variable in "\n\t\t\t\t#!/usr/bin/env bash\n\t\t\t\thello \"$(context.task.missing)\""`,
931933
Paths: []string{"steps[0].script"},
932934
},
935+
}, {
936+
name: "negative timeout string",
937+
fields: fields{
938+
Steps: []v1beta1.Step{{
939+
Timeout: &metav1.Duration{Duration: -10 * time.Second},
940+
}},
941+
},
942+
expectedError: apis.FieldError{
943+
Message: "invalid value: -10s",
944+
Paths: []string{"steps[0].negative timeout"},
945+
},
933946
}}
934947
for _, tt := range tests {
935948
t.Run(tt.name, func(t *testing.T) {

pkg/apis/pipeline/v1beta1/zz_generated.deepcopy.go

+5
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/entrypoint/entrypointer.go

+26-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"
@@ -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 duration within which the Step must complete
68+
Timeout *time.Duration
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(ctx context.Context, args ...string) error
7780
}
7881

7982
// PostWriter encapsulates writing a file when complete.
@@ -106,21 +109,41 @@ func (e Entrypointer) Go() error {
106109
Value: time.Now().Format(timeFormat),
107110
ResultType: v1beta1.InternalTektonResultType,
108111
})
109-
110112
return err
111113
}
112114
}
113115

114116
if e.Entrypoint != "" {
115117
e.Args = append([]string{e.Entrypoint}, e.Args...)
116118
}
119+
117120
output = append(output, v1beta1.PipelineResourceResult{
118121
Key: "StartedAt",
119122
Value: time.Now().Format(timeFormat),
120123
ResultType: v1beta1.InternalTektonResultType,
121124
})
122125

123-
err := e.Runner.Run(e.Args...)
126+
var err error
127+
if e.Timeout != nil && *e.Timeout < time.Duration(0) {
128+
err = fmt.Errorf("negative timeout specified")
129+
}
130+
131+
if err == nil {
132+
ctx := context.Background()
133+
var cancel context.CancelFunc
134+
if e.Timeout != nil && *e.Timeout != time.Duration(0) {
135+
ctx, cancel = context.WithTimeout(ctx, *e.Timeout)
136+
defer cancel()
137+
}
138+
err = e.Runner.Run(ctx, e.Args...)
139+
if err == context.DeadlineExceeded {
140+
output = append(output, v1beta1.PipelineResourceResult{
141+
Key: "Reason",
142+
Value: "TimeoutExceeded",
143+
ResultType: v1beta1.InternalTektonResultType,
144+
})
145+
}
146+
}
124147

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

0 commit comments

Comments
 (0)