Skip to content

Commit

Permalink
Add timeout setting to Steps
Browse files Browse the repository at this point in the history
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 #1690
  • Loading branch information
Peaorl committed Aug 11, 2020
1 parent f0954c7 commit 0c2ff5c
Show file tree
Hide file tree
Showing 17 changed files with 262 additions and 32 deletions.
13 changes: 12 additions & 1 deletion cmd/entrypoint/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ limitations under the License.
package main

import (
"context"
"flag"
"log"
"os"
Expand All @@ -40,6 +41,7 @@ var (
terminationPath = flag.String("termination_path", "/tekton/termination", "If specified, file to write upon termination")
results = flag.String("results", "", "If specified, list of file names that might contain task results")
waitPollingInterval = time.Second
timeout = flag.String("timeout", "", "If specified, sets timeout for step")
)

func main() {
Expand Down Expand Up @@ -78,7 +80,16 @@ func main() {
log.Printf("non-fatal error copying credentials: %q", err)
}

if err := e.Go(); err != nil {
// Add timeout to context if a non-zero timeout is specified for a step
ctx := context.Background()
timeoutContext, _ := time.ParseDuration(*timeout)
var cancel context.CancelFunc
if timeoutContext != time.Duration(0) {
ctx, cancel = context.WithTimeout(ctx, timeoutContext)
defer cancel()
}

if err := e.Go(ctx); err != nil {
switch t := err.(type) {
case skipError:
log.Print("Skipping step because a previous step failed")
Expand Down
5 changes: 3 additions & 2 deletions cmd/entrypoint/runner.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package main

import (
"context"
"os"
"os/exec"
"os/signal"
Expand All @@ -19,7 +20,7 @@ type realRunner struct {

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

func (rr *realRunner) Run(args ...string) error {
func (rr *realRunner) Run(ctx context.Context, args ...string) error {
if len(args) == 0 {
return nil
}
Expand All @@ -33,7 +34,7 @@ func (rr *realRunner) Run(args ...string) error {
signal.Notify(rr.signals)
defer signal.Reset()

cmd := exec.Command(name, args...)
cmd := exec.CommandContext(ctx, name, args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
// dedicated PID group used to forward signals to
Expand Down
18 changes: 17 additions & 1 deletion cmd/entrypoint/runner_test.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package main

import (
"context"
"os"
"syscall"
"testing"
"time"
)

// TestRealRunnerSignalForwarding will artificially put an interrupt signal (SIGINT) in the rr.signals chan.
Expand All @@ -14,9 +16,23 @@ func TestRealRunnerSignalForwarding(t *testing.T) {
rr := realRunner{}
rr.signals = make(chan os.Signal, 1)
rr.signals <- syscall.SIGINT
if err := rr.Run("sleep", "3600"); err.Error() == "signal: interrupt" {
if err := rr.Run(context.Background(), "sleep", "3600"); err.Error() == "signal: interrupt" {
t.Logf("SIGINT forwarded to Entrypoint")
} else {
t.Fatalf("Unexpected error received: %v", err)
}
}

// TestRealRunnerTimeout tests whether cmd is killed after time.Second even though it's supposed to sleep for an hour.
func TestRealRunnerTimeout(t *testing.T) {
rr := realRunner{}
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
// For seconds and higher time units, err.Error() returns "signal: killed".
// For microseconds and lower time units, err.Error() returns "context deadline exceeded".
if err := rr.Run(ctx, "sleep", "3600"); err.Error() == "signal: killed" || err.Error() == "context deadline exceeded" {
t.Logf("Timeout observed")
} else {
t.Fatalf("Unexpected error received: %v", err)
}
}
17 changes: 17 additions & 0 deletions docs/tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,24 @@ steps:
#!/usr/bin/env bash
/bin/my-binary
```
#### Specifying a timeout

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.
An accompanying error message is output under `status.conditions.message`.

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).

The example `Step` below is supposed to sleep for 60 seconds but will be canceled by the specified 5 second timeout.
```yaml
steps:
- name: willTimeout
image: ubuntu
script: |
#!/usr/bin/env bash
echo "I am supposed to sleep for 60 seconds!"
sleep 60
timeout: 5s
```
### Specifying `Parameters`

You can specify parameters, such as compilation flags or artifact names, that you want to supply to the `Task` at execution time.
Expand Down
14 changes: 14 additions & 0 deletions examples/v1beta1/taskruns/step-timeout.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
kind: TaskRun
apiVersion: tekton.dev/v1beta1
metadata:
generateName: timeout-test-
spec:
taskSpec:
steps:
- name: wait
image: ubuntu
script: |
#!/usr/bin/env bash
echo "I am supposed to sleep for 60 seconds!"
sleep 60
timeout: 5s
7 changes: 5 additions & 2 deletions pkg/apis/pipeline/v1beta1/task_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ const (
TaskRunResultType ResultType = "TaskRunResult"
// PipelineResourceResultType default pipeline result value
PipelineResourceResultType ResultType = "PipelineResourceResult"
// InternalTektonResultType default internal tekton result value
InternalTektonResultType ResultType = "InternalTektonResult"
// UnknownResultType default unknown result type value
UnknownResultType ResultType = ""
)
Expand Down Expand Up @@ -123,10 +125,11 @@ type Step struct {
//
// If Script is not empty, the Step cannot have an Command or Args.
Script string `json:"script,omitempty"`
// If step times out after Timeout, pod is terminated
Timeout string `json:"timeout,omitempty"`
}

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

Expand Down
7 changes: 7 additions & 0 deletions pkg/apis/pipeline/v1beta1/task_validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"fmt"
"path/filepath"
"strings"
"time"

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

if s.Timeout != "" {
if _, err := time.ParseDuration(s.Timeout); err != nil {
return apis.ErrInvalidValue(s.Timeout, "timeout")
}
}

for _, vm := range s.VolumeMounts {
if strings.HasPrefix(vm.MountPath, "/tekton/") &&
!strings.HasPrefix(vm.MountPath, "/tekton/home") {
Expand Down
14 changes: 11 additions & 3 deletions pkg/entrypoint/entrypointer.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ limitations under the License.
package entrypoint

import (
"context"
"fmt"
"io/ioutil"
"os"
Expand Down Expand Up @@ -73,7 +74,7 @@ type Waiter interface {

// Runner encapsulates running commands.
type Runner interface {
Run(args ...string) error
Run(ctx context.Context, args ...string) error
}

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

// Go optionally waits for a file, runs the command, and writes a
// post file.
func (e Entrypointer) Go() error {
func (e Entrypointer) Go(ctx context.Context) error {
prod, _ := zap.NewProduction()
logger := prod.Sugar()

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

err := e.Runner.Run(e.Args...)
err := e.Runner.Run(ctx, e.Args...)
if ctx.Err() == context.DeadlineExceeded {
output = append(output, v1beta1.PipelineResourceResult{
Key: "Reason",
Value: "TimeoutExceeded",
ResultType: v1beta1.InternalTektonResultType,
})
}

// Write the post file *no matter what*
e.WritePostFile(e.PostFile, err)
Expand Down
9 changes: 5 additions & 4 deletions pkg/entrypoint/entrypointer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ limitations under the License.
package entrypoint

import (
"context"
"encoding/json"
"errors"
"io/ioutil"
Expand Down Expand Up @@ -76,7 +77,7 @@ func TestEntrypointerFailures(t *testing.T) {
Runner: fr,
PostWriter: fpw,
TerminationPath: "termination",
}.Go()
}.Go(context.Background())
if err == nil {
t.Fatalf("Entrypointer didn't fail")
}
Expand Down Expand Up @@ -139,7 +140,7 @@ func TestEntrypointer(t *testing.T) {
Runner: fr,
PostWriter: fpw,
TerminationPath: "termination",
}.Go()
}.Go(context.Background())
if err != nil {
t.Fatalf("Entrypointer failed: %v", err)
}
Expand Down Expand Up @@ -214,7 +215,7 @@ func (f *fakeWaiter) Wait(file string, _ bool) error {

type fakeRunner struct{ args *[]string }

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

type fakeErrorRunner struct{ args *[]string }

func (f *fakeErrorRunner) Run(args ...string) error {
func (f *fakeErrorRunner) Run(ctx context.Context, args ...string) error {
f.args = &args
return errors.New("runner failed")
}
3 changes: 2 additions & 1 deletion pkg/pod/entrypoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ var (
// method, using entrypoint_lookup.go.
//
// TODO(#1605): Also use entrypoint injection to order sidecar start/stop.
func orderContainers(entrypointImage string, extraEntrypointArgs []string, steps []corev1.Container, results []v1beta1.TaskResult) (corev1.Container, []corev1.Container, error) {
func orderContainers(entrypointImage string, extraEntrypointArgs []string, steps []corev1.Container, stepTimeouts []string, results []v1beta1.TaskResult) (corev1.Container, []corev1.Container, error) {
initContainer := corev1.Container{
Name: "place-tools",
Image: entrypointImage,
Expand Down Expand Up @@ -120,6 +120,7 @@ func orderContainers(entrypointImage string, extraEntrypointArgs []string, steps
}
argsForEntrypoint = append(argsForEntrypoint, extraEntrypointArgs...)
argsForEntrypoint = append(argsForEntrypoint, resultArgument(steps, results)...)
argsForEntrypoint = append(argsForEntrypoint, "-timeout", stepTimeouts[i])

cmd, args := s.Command, s.Args
if len(cmd) == 0 {
Expand Down
24 changes: 20 additions & 4 deletions pkg/pod/entrypoint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ func TestOrderContainers(t *testing.T) {
"-wait_file_content",
"-post_file", "/tekton/tools/0",
"-termination_path", "/tekton/termination",
"-timeout",
"",
"-entrypoint", "cmd", "--",
"arg1", "arg2",
},
Expand All @@ -68,6 +70,8 @@ func TestOrderContainers(t *testing.T) {
"-wait_file", "/tekton/tools/0",
"-post_file", "/tekton/tools/1",
"-termination_path", "/tekton/termination",
"-timeout",
"",
"-entrypoint", "cmd1", "--",
"cmd2", "cmd3",
"arg1", "arg2",
Expand All @@ -81,13 +85,15 @@ func TestOrderContainers(t *testing.T) {
"-wait_file", "/tekton/tools/1",
"-post_file", "/tekton/tools/2",
"-termination_path", "/tekton/termination",
"-timeout",
"",
"-entrypoint", "cmd", "--",
"arg1", "arg2",
},
VolumeMounts: []corev1.VolumeMount{toolsMount},
TerminationMessagePath: "/tekton/termination",
}}
gotInit, got, err := orderContainers(images.EntrypointImage, []string{}, steps, nil)
gotInit, got, err := orderContainers(images.EntrypointImage, []string{}, steps, make([]string, len(steps)), nil)
if err != nil {
t.Fatalf("orderContainers: %v", err)
}
Expand Down Expand Up @@ -138,6 +144,8 @@ func TestEntryPointResults(t *testing.T) {
"-post_file", "/tekton/tools/0",
"-termination_path", "/tekton/termination",
"-results", "sum,sub",
"-timeout",
"",
"-entrypoint", "cmd", "--",
"arg1", "arg2",
},
Expand All @@ -151,6 +159,8 @@ func TestEntryPointResults(t *testing.T) {
"-post_file", "/tekton/tools/1",
"-termination_path", "/tekton/termination",
"-results", "sum,sub",
"-timeout",
"",
"-entrypoint", "cmd1", "--",
"cmd2", "cmd3",
"arg1", "arg2",
Expand All @@ -165,13 +175,15 @@ func TestEntryPointResults(t *testing.T) {
"-post_file", "/tekton/tools/2",
"-termination_path", "/tekton/termination",
"-results", "sum,sub",
"-timeout",
"",
"-entrypoint", "cmd", "--",
"arg1", "arg2",
},
VolumeMounts: []corev1.VolumeMount{toolsMount},
TerminationMessagePath: "/tekton/termination",
}}
_, got, err := orderContainers(images.EntrypointImage, []string{}, steps, results)
_, got, err := orderContainers(images.EntrypointImage, []string{}, steps, make([]string, len(steps)), results)
if err != nil {
t.Fatalf("orderContainers: %v", err)
}
Expand Down Expand Up @@ -203,13 +215,15 @@ func TestEntryPointResultsSingleStep(t *testing.T) {
"-post_file", "/tekton/tools/0",
"-termination_path", "/tekton/termination",
"-results", "sum,sub",
"-timeout",
"",
"-entrypoint", "cmd", "--",
"arg1", "arg2",
},
VolumeMounts: []corev1.VolumeMount{toolsMount, downwardMount},
TerminationMessagePath: "/tekton/termination",
}}
_, got, err := orderContainers(images.EntrypointImage, []string{}, steps, results)
_, got, err := orderContainers(images.EntrypointImage, []string{}, steps, make([]string, len(steps)), results)
if err != nil {
t.Fatalf("orderContainers: %v", err)
}
Expand Down Expand Up @@ -237,13 +251,15 @@ func TestEntryPointSingleResultsSingleStep(t *testing.T) {
"-post_file", "/tekton/tools/0",
"-termination_path", "/tekton/termination",
"-results", "sum",
"-timeout",
"",
"-entrypoint", "cmd", "--",
"arg1", "arg2",
},
VolumeMounts: []corev1.VolumeMount{toolsMount, downwardMount},
TerminationMessagePath: "/tekton/termination",
}}
_, got, err := orderContainers(images.EntrypointImage, []string{}, steps, results)
_, got, err := orderContainers(images.EntrypointImage, []string{}, steps, make([]string, len(steps)), results)
if err != nil {
t.Fatalf("orderContainers: %v", err)
}
Expand Down
Loading

0 comments on commit 0c2ff5c

Please sign in to comment.