Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Step timeout #3087

Merged
merged 1 commit into from
Oct 7, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions cmd/entrypoint/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,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.Duration("timeout", time.Duration(0), "If specified, sets timeout for step")
)

func cp(src, dst string) error {
Expand Down Expand Up @@ -103,6 +104,7 @@ func main() {
Runner: &realRunner{},
PostWriter: &realPostWriter{},
Results: strings.Split(*results, ","),
Timeout: timeout,
}

// Copy any creds injected by the controller into the $HOME directory of the current
Expand Down
11 changes: 9 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 All @@ -42,6 +43,9 @@ func (rr *realRunner) Run(args ...string) error {

// Start defined command
if err := cmd.Start(); err != nil {
if ctx.Err() == context.DeadlineExceeded {
return context.DeadlineExceeded
}
return err
}

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

// Wait for command to exit
if err := cmd.Wait(); err != nil {
if ctx.Err() == context.DeadlineExceeded {
return context.DeadlineExceeded
}
return err
}

Expand Down
19 changes: 18 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,24 @@ 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 a millisecond even though it's supposed to sleep for 10 milliseconds.
func TestRealRunnerTimeout(t *testing.T) {
rr := realRunner{}
timeout := time.Millisecond
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
if err := rr.Run(ctx, "sleep", "0.01"); err != nil {
if err != context.DeadlineExceeded {
t.Fatalf("unexpected error received: %v", err)
}
} else {
t.Fatalf("step didn't timeout")
}
}
21 changes: 21 additions & 0 deletions docs/tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ weight: 1
- [Defining `Steps`](#defining-steps)
- [Reserved directories](#reserved-directories)
- [Running scripts within `Steps`](#running-scripts-within-steps)
- [Specifying a timeout](#specifying-a-timeout)
- [Specifying `Parameters`](#specifying-parameters)
- [Specifying `Resources`](#specifying-resources)
- [Specifying `Workspaces`](#specifying-workspaces)
Expand Down Expand Up @@ -241,7 +242,27 @@ 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` kills
its running process and any subsequent `Steps` in the `TaskRun` will not be
executed. The `TaskRun` is placed into a `Failed` condition. An accompanying log
describing which `Step` timed out is written as the `Failed` condition's message.

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

The example `Step` below is supposed to sleep for 60 seconds but will be canceled by the specified 5 second timeout.
```yaml
steps:
- name: sleep-then-timeout
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
2 changes: 1 addition & 1 deletion examples/v1beta1/taskruns/workspace-in-sidecar.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ apiVersion: tekton.dev/v1beta1
metadata:
generateName: workspace-in-sidecar-
spec:
timeout: 30s
timeout: 60s
workspaces:
- name: signals
emptyDir: {}
Expand Down
6 changes: 4 additions & 2 deletions pkg/apis/pipeline/v1beta1/task_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,10 +125,12 @@ type Step struct {
//
// If Script is not empty, the Step cannot have an Command or Args.
Script string `json:"script,omitempty"`
// Timeout is the time after which the step times out. Defaults to never.
// Refer to Go's ParseDuration documentation for expected format: https://golang.org/pkg/time/#ParseDuration
Timeout *metav1.Duration `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 @@ -159,6 +160,12 @@ func validateStep(s Step, names sets.String) (errs *apis.FieldError) {
names.Insert(s.Name)
}

if s.Timeout != nil {
if s.Timeout.Duration < time.Duration(0) {
return apis.ErrInvalidValue(s.Timeout.Duration, "negative timeout")
}
}

for j, vm := range s.VolumeMounts {
if strings.HasPrefix(vm.MountPath, "/tekton/") &&
!strings.HasPrefix(vm.MountPath, "/tekton/home") {
Expand Down
13 changes: 13 additions & 0 deletions pkg/apis/pipeline/v1beta1/task_validation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,14 @@ package v1beta1_test
import (
"context"
"testing"
"time"

"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1"
"github.com/tektoncd/pipeline/test/diff"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"knative.dev/pkg/apis"
)

Expand Down Expand Up @@ -930,6 +932,17 @@ func TestTaskSpecValidateError(t *testing.T) {
Message: `non-existent variable in "\n\t\t\t\t#!/usr/bin/env bash\n\t\t\t\thello \"$(context.task.missing)\""`,
Paths: []string{"steps[0].script"},
},
}, {
name: "negative timeout string",
fields: fields{
Steps: []v1beta1.Step{{
Timeout: &metav1.Duration{Duration: -10 * time.Second},
}},
},
expectedError: apis.FieldError{
Message: "invalid value: -10s",
Paths: []string{"steps[0].negative timeout"},
},
}}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
Expand Down
5 changes: 5 additions & 0 deletions pkg/apis/pipeline/v1beta1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

29 changes: 26 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 @@ -63,6 +64,8 @@ type Entrypointer struct {

// Results is the set of files that might contain task results
Results []string
// Timeout is an optional user-specified duration within which the Step must complete
Timeout *time.Duration
}

// Waiter encapsulates waiting for files to exist.
Expand All @@ -73,7 +76,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 Down Expand Up @@ -106,21 +109,41 @@ func (e Entrypointer) Go() error {
Value: time.Now().Format(timeFormat),
ResultType: v1beta1.InternalTektonResultType,
})

return err
}
}

if e.Entrypoint != "" {
e.Args = append([]string{e.Entrypoint}, e.Args...)
}

output = append(output, v1beta1.PipelineResourceResult{
Key: "StartedAt",
Value: time.Now().Format(timeFormat),
ResultType: v1beta1.InternalTektonResultType,
})

err := e.Runner.Run(e.Args...)
var err error
if e.Timeout != nil && *e.Timeout < time.Duration(0) {
err = fmt.Errorf("negative timeout specified")
}

if err == nil {
ctx := context.Background()
var cancel context.CancelFunc
if e.Timeout != nil && *e.Timeout != time.Duration(0) {
ctx, cancel = context.WithTimeout(ctx, *e.Timeout)
defer cancel()
}
err = e.Runner.Run(ctx, e.Args...)
if 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
Loading