Skip to content

Commit

Permalink
Add a new api field Timeouts, a dict of timeout fields:
Browse files Browse the repository at this point in the history
i.e.

timeouts:
  pipeline: "0h0m60s"
  tasks: "0h0m30s"
  finally: "0h0m20s"

All three subfields are optional
  • Loading branch information
souleb committed May 7, 2021
1 parent e30527c commit 72458c6
Show file tree
Hide file tree
Showing 12 changed files with 415 additions and 68 deletions.
70 changes: 54 additions & 16 deletions docs/pipelineruns.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,23 @@ weight: 4
-->
# PipelineRuns

- [Overview](#overview)
- [Configuring a `PipelineRun`](#configuring-a-pipelinerun)
- [Specifying the target `Pipeline`](#specifying-the-target-pipeline)
- [Tekton Bundles](#tekton-bundles)
- [PipelineRuns](#pipelineruns)
- [Overview](#overview)
- [Configuring a `PipelineRun`](#configuring-a-pipelinerun)
- [Specifying the target `Pipeline`](#specifying-the-target-pipeline)
- [Tekton Bundles](#tekton-bundles)
- [Specifying `Resources`](#specifying-resources)
- [Specifying `Parameters`](#specifying-parameters)
- [Specifying custom `ServiceAccount` credentials](#specifying-custom-serviceaccount-credentials)
- [Mapping `ServiceAccount` credentials to `Tasks`](#mapping-serviceaccount-credentials-to-tasks)
- [Specifying a `Pod` template](#specifying-a-pod-template)
- [Specifying `TaskRunSpecs`](#specifying-taskrunspecs)
- [Specifying `Workspaces`](#specifying-workspaces)
- [Specifying `LimitRange` values](#specifying-limitrange-values)
- [Configuring a failure timeout](#configuring-a-failure-timeout)
- [Monitoring execution status](#monitoring-execution-status)
- [Cancelling a `PipelineRun`](#cancelling-a-pipelinerun)
- [Pending `PipelineRuns`](#pending-pipelineruns)
- [Events](events.md#pipelineruns)
- [Specifying `Parameters`](#specifying-parameters)
- [Specifying custom `ServiceAccount` credentials](#specifying-custom-serviceaccount-credentials)
- [Mapping `ServiceAccount` credentials to `Tasks`](#mapping-serviceaccount-credentials-to-tasks)
- [Specifying a `Pod` template](#specifying-a-pod-template)
- [Specifying taskRunSpecs](#specifying-taskrunspecs)
- [Specifying `Workspaces`](#specifying-workspaces)
- [Specifying `LimitRange` values](#specifying-limitrange-values)
- [Configuring a failure timeout](#configuring-a-failure-timeout)
- [Monitoring execution status](#monitoring-execution-status)
- [Cancelling a `PipelineRun`](#cancelling-a-pipelinerun)
- [Pending `PipelineRuns`](#pending-pipelineruns)



Expand Down Expand Up @@ -414,6 +414,44 @@ You can use the `timeout` field to set the `PipelineRun's` desired timeout value
If you do not specify this value in the `PipelineRun`, the global default timeout value applies.
If you set the timeout to 0, the `PipelineRun` fails immediately upon encountering an error.

> :warning: ** `timeout`will be deprecated in future versions. Consider using `timeouts` instead.

You can use the `timeouts` field to set the `PipelineRun's` desired timeout value in minutes. There are three sub-fields than can be used to specify failures timeout for the entire pipeline, for tasks, and for finally tasks.

```yaml
timeouts:
pipeline: "0h0m60s"
tasks: "0h0m40s"
finally: "0h0m20s"
```
All three sub-fields are optional, and will be automatically processed according to the following constraint:
* `timeouts.pipeline >= timeouts.tasks + timeouts.finally`

You may combine the timeouts as follow:

Combination 1: Set the timeout for the entire `pipeline` and reserve a portion of it for `tasks`.

```yaml
kind: PipelineRun
spec:
timeouts:
pipeline: "0h4m0s"
tasks: "0h1m0s"
```

Combination 2: Set the timeout for the entire `pipeline` and reserve a portion of it for `finally`.

```yaml
kind: PipelineRun
spec:
timeouts:
pipeline: "0h4m0s"
finally: "0h3m0s"
```

If you do not specify the `timeout` value or `timeouts.pipeline` in the `PipelineRun`, the global default timeout value applies.
If you set the `timeout` value or `timeouts.pipeline` to 0, the `PipelineRun` fails immediately upon encountering an error.

The global default timeout is set to 60 minutes when you first install Tekton. You can set
a different global default timeout value using the `default-timeout-minutes` field in
[`config/config-defaults.yaml`](./../config/config-defaults.yaml).
Expand Down
60 changes: 60 additions & 0 deletions examples/v1beta1/pipelineruns/no-ci/pipeline-timeout.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,63 @@ spec:
value: "Good Morning, Bob!"
- name: NIGHT_GREETINGS
value: "Good Night, Bob!"

---

apiVersion: tekton.dev/v1beta1
kind: PipelineRun
metadata:
name: pipelinerun-timeouts
spec:
# 1 hour and half timeout for the pipeline
# 1 hour and fifteen minutes for the pipeline tasks
# 15 minutes for the finally tasks
timeouts:
pipeline: 1h30m
tasks: 1h15m
pipelineSpec:
params:
- name: MORNING_GREETINGS
description: "morning greetings, default is Good Morning!"
type: string
default: "Good Morning!"
- name: NIGHT_GREETINGS
description: "Night greetings, default is Good Night!"
type: string
default: "Good Night!"
tasks:
# Task to display morning greetings
- name: echo-good-morning
taskRef:
name: task-echo-message
params:
- name: MESSAGE
value: $(params.MORNING_GREETINGS)
# Task to display night greetings
- name: echo-good-night
taskRef:
name: task-echo-message
params:
- name: MESSAGE
value: $(params.NIGHT_GREETINGS)
finally:
- name: echo-status
params:
- name: echoStatus
value: "$(tasks.echo-good-night.status)"
taskSpec:
params:
- name: echoStatus
steps:
- name: verify-status
image: ubuntu
script: |
if [ $(params.echoStatus) == "Succeeded" ]
then
echo " Good night! echoed successfully"
fi
params:
- name: MORNING_GREETINGS
value: "Good Morning, Bob!"
- name: NIGHT_GREETINGS
value: "Good Night, Bob!"
25 changes: 24 additions & 1 deletion internal/builder/v1beta1/pipeline.go
Original file line number Diff line number Diff line change
Expand Up @@ -532,7 +532,30 @@ func PipelineRunNilTimeout(prs *v1beta1.PipelineRunSpec) {
// PipelineRunTasksTimeout sets the timeout to the PipelineRunSpec.
func PipelineRunTasksTimeout(duration time.Duration) PipelineRunSpecOp {
return func(prs *v1beta1.PipelineRunSpec) {
prs.TasksTimeout = &metav1.Duration{Duration: duration}
if prs.Timeouts == nil {
prs.Timeouts = &v1beta1.TimeoutFields{}
}
prs.Timeouts.Tasks = &metav1.Duration{Duration: duration}
}
}

// PipelineRunFinallyTimeout sets the timeout to the PipelineRunSpec.
func PipelineRunFinallyTimeout(duration time.Duration) PipelineRunSpecOp {
return func(prs *v1beta1.PipelineRunSpec) {
if prs.Timeouts == nil {
prs.Timeouts = &v1beta1.TimeoutFields{}
}
prs.Timeouts.Finally = &metav1.Duration{Duration: duration}
}
}

// PipelineRunPipelineTimeout sets the timeout to the PipelineRunSpec.
func PipelineRunPipelineTimeout(duration time.Duration) PipelineRunSpecOp {
return func(prs *v1beta1.PipelineRunSpec) {
if prs.Timeouts == nil {
prs.Timeouts = &v1beta1.TimeoutFields{}
}
prs.Timeouts.Pipeline = &metav1.Duration{Duration: duration}
}
}

Expand Down
30 changes: 24 additions & 6 deletions internal/builder/v1beta1/pipeline_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,9 @@ func TestPipelineRun(t *testing.T) {
tb.PipelineRunParam("first-param-string", "first-value"),
tb.PipelineRunParam("second-param-array", "some", "array"),
tb.PipelineRunTimeout(1*time.Hour),
tb.PipelineRunPipelineTimeout(100*time.Minute),
tb.PipelineRunTasksTimeout(50*time.Minute),
tb.PipelineRunFinallyTimeout(50*time.Minute),
tb.PipelineRunResourceBinding("some-resource", tb.PipelineResourceBindingRef("my-special-resource")),
tb.PipelineRunServiceAccountNameTask("foo", "sa-2"),
tb.PipelineRunPipelineRefBundle("/some/registry"),
Expand Down Expand Up @@ -210,8 +212,12 @@ func TestPipelineRun(t *testing.T) {
Name: "second-param-array",
Value: *v1beta1.NewArrayOrString("some", "array"),
}},
Timeout: &metav1.Duration{Duration: 1 * time.Hour},
TasksTimeout: &metav1.Duration{Duration: 50 * time.Minute},
Timeout: &metav1.Duration{Duration: 1 * time.Hour},
Timeouts: &v1beta1.TimeoutFields{
Pipeline: &metav1.Duration{Duration: 100 * time.Minute},
Tasks: &metav1.Duration{Duration: 50 * time.Minute},
Finally: &metav1.Duration{Duration: 50 * time.Minute},
},
Resources: []v1beta1.PipelineResourceBinding{{
Name: "some-resource",
ResourceRef: &v1beta1.PipelineResourceRef{
Expand Down Expand Up @@ -246,7 +252,9 @@ func TestPipelineRunWithPodTemplate(t *testing.T) {
tb.PipelineRunParam("first-param-string", "first-value"),
tb.PipelineRunParam("second-param-array", "some", "array"),
tb.PipelineRunTimeout(1*time.Hour),
tb.PipelineRunPipelineTimeout(50*time.Minute),
tb.PipelineRunTasksTimeout(50*time.Minute),
tb.PipelineRunFinallyTimeout(50*time.Minute),
tb.PipelineRunResourceBinding("some-resource", tb.PipelineResourceBindingRef("my-special-resource")),
tb.PipelineRunServiceAccountNameTask("foo", "sa-2"),
tb.PipelineRunNodeSelector(map[string]string{
Expand Down Expand Up @@ -279,8 +287,12 @@ func TestPipelineRunWithPodTemplate(t *testing.T) {
Name: "second-param-array",
Value: *v1beta1.NewArrayOrString("some", "array"),
}},
Timeout: &metav1.Duration{Duration: 1 * time.Hour},
TasksTimeout: &metav1.Duration{Duration: 50 * time.Minute},
Timeout: &metav1.Duration{Duration: 1 * time.Hour},
Timeouts: &v1beta1.TimeoutFields{
Pipeline: &metav1.Duration{Duration: 50 * time.Minute},
Tasks: &metav1.Duration{Duration: 50 * time.Minute},
Finally: &metav1.Duration{Duration: 50 * time.Minute},
},
Resources: []v1beta1.PipelineResourceBinding{{
Name: "some-resource",
ResourceRef: &v1beta1.PipelineResourceRef{
Expand Down Expand Up @@ -320,7 +332,9 @@ func TestPipelineRunWithResourceSpec(t *testing.T) {
tb.PipelineRunParam("first-param-string", "first-value"),
tb.PipelineRunParam("second-param-array", "some", "array"),
tb.PipelineRunTimeout(1*time.Hour),
tb.PipelineRunPipelineTimeout(50*time.Minute),
tb.PipelineRunTasksTimeout(50*time.Minute),
tb.PipelineRunFinallyTimeout(50*time.Minute),
tb.PipelineRunResourceBinding("some-resource",
tb.PipelineResourceBindingResourceSpec(&resource.PipelineResourceSpec{
Type: v1beta1.PipelineResourceTypeGit,
Expand Down Expand Up @@ -356,8 +370,12 @@ func TestPipelineRunWithResourceSpec(t *testing.T) {
Name: "second-param-array",
Value: *v1beta1.NewArrayOrString("some", "array"),
}},
Timeout: &metav1.Duration{Duration: 1 * time.Hour},
TasksTimeout: &metav1.Duration{Duration: 50 * time.Minute},
Timeout: &metav1.Duration{Duration: 1 * time.Hour},
Timeouts: &v1beta1.TimeoutFields{
Pipeline: &metav1.Duration{Duration: 50 * time.Minute},
Tasks: &metav1.Duration{Duration: 50 * time.Minute},
Finally: &metav1.Duration{Duration: 50 * time.Minute},
},
Resources: []v1beta1.PipelineResourceBinding{{
Name: "some-resource",
ResourceSpec: &resource.PipelineResourceSpec{
Expand Down
6 changes: 5 additions & 1 deletion pkg/apis/pipeline/v1beta1/pipelinerun_defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,14 @@ func (pr *PipelineRun) SetDefaults(ctx context.Context) {

func (prs *PipelineRunSpec) SetDefaults(ctx context.Context) {
cfg := config.FromContextOrDefaults(ctx)
if prs.Timeout == nil {
if prs.Timeout == nil && prs.Timeouts == nil {
prs.Timeout = &metav1.Duration{Duration: time.Duration(cfg.Defaults.DefaultTimeoutMinutes) * time.Minute}
}

if prs.Timeouts != nil && prs.Timeouts.Pipeline == nil {
prs.Timeouts.Pipeline = &metav1.Duration{Duration: time.Duration(cfg.Defaults.DefaultTimeoutMinutes) * time.Minute}
}

defaultSA := cfg.Defaults.DefaultServiceAccount
if prs.ServiceAccountName == "" && defaultSA != "" {
prs.ServiceAccountName = defaultSA
Expand Down
25 changes: 19 additions & 6 deletions pkg/apis/pipeline/v1beta1/pipelinerun_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,12 +89,17 @@ func (pr *PipelineRun) IsCancelled() bool {
}

func (pr *PipelineRun) GetTimeout(ctx context.Context) time.Duration {
// Use the platform default is no timeout is set
if pr.Spec.Timeout == nil {
// Use the platform default if no timeout is set
if pr.Spec.Timeout == nil && pr.Spec.Timeouts == nil {
defaultTimeout := time.Duration(config.FromContextOrDefaults(ctx).Defaults.DefaultTimeoutMinutes)
return defaultTimeout * time.Minute
}
return pr.Spec.Timeout.Duration

if pr.Spec.Timeout != nil {
return pr.Spec.Timeout.Duration
}

return pr.Spec.Timeouts.Pipeline.Duration
}

// IsPending returns true if the PipelineRun's spec status is set to Pending state
Expand Down Expand Up @@ -174,10 +179,12 @@ type PipelineRunSpec struct {
// Used for cancelling a pipelinerun (and maybe more later on)
// +optional
Status PipelineRunSpecStatus `json:"status,omitempty"`
// Time after which the Pipeline tasks time out.
// Finally tasks can run beyond this as they are bound to the pipeline timeout.
// Time after which the Pipeline times out.
// Currently three keys are accepted in the map
// pipeline, tasks and finally
// with Timeouts.pipeline >= Timeouts.tasks + Timeouts.finally
// +optional
TasksTimeout *metav1.Duration `json:"tasksTimeout,omitempty"`
Timeouts *TimeoutFields `json:"timeouts,omitempty"`
// Time after which the Pipeline times out. Defaults to never.
// Refer to Go's ParseDuration documentation for expected format: https://golang.org/pkg/time/#ParseDuration
// +optional
Expand All @@ -193,6 +200,12 @@ type PipelineRunSpec struct {
TaskRunSpecs []PipelineTaskRunSpec `json:"taskRunSpecs,omitempty"`
}

type TimeoutFields struct {
Pipeline *metav1.Duration `json:"pipeline,omitempty"`
Tasks *metav1.Duration `json:"tasks,omitempty"`
Finally *metav1.Duration `json:"finally,omitempty"`
}

// PipelineRunSpecStatus defines the pipelinerun spec status the user can provide
type PipelineRunSpecStatus string

Expand Down
51 changes: 41 additions & 10 deletions pkg/apis/pipeline/v1beta1/pipelinerun_validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,21 +83,34 @@ func (ps *PipelineRunSpec) Validate(ctx context.Context) (errs *apis.FieldError)
}
}

if ps.TasksTimeout != nil {
// tasksTimeout should be a valid duration of at least 0.
if ps.TasksTimeout.Duration < 0 {
errs = errs.Also(apis.ErrInvalidValue(fmt.Sprintf("%s should be >= 0", ps.TasksTimeout.Duration.String()), "tasksTimeout"))
if ps.Timeouts != nil {
if ps.Timeout != nil {
// can't have both at the same time
errs = errs.Also(apis.ErrDisallowedFields("timeout", "timeouts"))
}

if ps.Timeout != nil {
if ps.TasksTimeout.Duration > ps.Timeout.Duration {
errs = errs.Also(apis.ErrInvalidValue(fmt.Sprintf("%s should be <= timeout duration", ps.TasksTimeout.Duration.String()), "tasksTimeout"))
// tasks timeout should be a valid duration of at least 0.
if ps.Timeouts.Tasks != nil && ps.Timeouts.Tasks.Duration < 0 {
errs = errs.Also(apis.ErrInvalidValue(fmt.Sprintf("%s should be >= 0", ps.Timeouts.Tasks.Duration.String()), "timeouts.tasks"))
}

// finally timeout should be a valid duration of at least 0.
if ps.Timeouts.Finally != nil && ps.Timeouts.Finally.Duration < 0 {
errs = errs.Also(apis.ErrInvalidValue(fmt.Sprintf("%s should be >= 0", ps.Timeouts.Finally.Duration.String()), "timeouts.finally"))
}

if ps.Timeouts.Pipeline != nil {
// pipeline timeout should be a valid duration of at least 0.
if ps.Timeouts.Pipeline.Duration < 0 {
errs = errs.Also(apis.ErrInvalidValue(fmt.Sprintf("%s should be >= 0", ps.Timeouts.Pipeline.Duration.String()), "timeouts.pipeline"))
}

errs = errs.Also(ps.validatePipelineTimeout(ps.Timeouts.Pipeline.Duration, "should be <= pipeline duration"))

} else {
defaultTimeout := time.Duration(config.FromContextOrDefaults(ctx).Defaults.DefaultTimeoutMinutes)
if ps.TasksTimeout.Duration > defaultTimeout {
errs = errs.Also(apis.ErrInvalidValue(fmt.Sprintf("%s should be <= default timeout duration", ps.TasksTimeout.Duration.String()), "tasksTimeout"))
}
errs = errs.Also(ps.validatePipelineTimeout(defaultTimeout, "should be <= default timeout duration"))

}
}

Expand All @@ -120,3 +133,21 @@ func (ps *PipelineRunSpec) Validate(ctx context.Context) (errs *apis.FieldError)

return errs
}

func (ps *PipelineRunSpec) validatePipelineTimeout(timeout time.Duration, errorMsg string) (errs *apis.FieldError) {
if ps.Timeouts.Tasks != nil && ps.Timeouts.Tasks.Duration > timeout {
errs = errs.Also(apis.ErrInvalidValue(fmt.Sprintf("%s "+errorMsg, ps.Timeouts.Tasks.Duration.String()), "timeouts.tasks"))
}

if ps.Timeouts.Finally != nil && ps.Timeouts.Finally.Duration > timeout {
errs = errs.Also(apis.ErrInvalidValue(fmt.Sprintf("%s "+errorMsg, ps.Timeouts.Finally.Duration.String()), "timeouts.finally"))
}

if ps.Timeouts.Tasks != nil && ps.Timeouts.Finally != nil {
if ps.Timeouts.Tasks.Duration+ps.Timeouts.Finally.Duration > timeout {
errs = errs.Also(apis.ErrInvalidValue(fmt.Sprintf("%s + %s "+errorMsg, ps.Timeouts.Tasks.Duration.String(), ps.Timeouts.Finally.Duration.String()), "timeouts.tasks"))
errs = errs.Also(apis.ErrInvalidValue(fmt.Sprintf("%s + %s "+errorMsg, ps.Timeouts.Tasks.Duration.String(), ps.Timeouts.Finally.Duration.String()), "timeouts.finally"))
}
}
return errs
}
Loading

0 comments on commit 72458c6

Please sign in to comment.