diff --git a/config/300-crds/300-pipelinerun.yaml b/config/300-crds/300-pipelinerun.yaml
index 3516d547b08..dbc534c0138 100644
--- a/config/300-crds/300-pipelinerun.yaml
+++ b/config/300-crds/300-pipelinerun.yaml
@@ -4474,6 +4474,11 @@ spec:
description: The name of the Step to override.
type: string
x-kubernetes-list-type: atomic
+ timeout:
+ description: |-
+ Time after which the TaskRun times out.
+ Refer Go's ParseDuration documentation for expected format: https://golang.org/pkg/time/#ParseDuration
+ type: string
x-kubernetes-list-type: atomic
taskRunTemplate:
description: TaskRunTemplate represent template of taskrun
diff --git a/docs/pipeline-api.md b/docs/pipeline-api.md
index 6342223af64..6cf3875e8b1 100644
--- a/docs/pipeline-api.md
+++ b/docs/pipeline-api.md
@@ -3322,6 +3322,21 @@ Kubernetes core/v1.ResourceRequirements
Compute resources to use for this TaskRun
+
+
+timeout
+
+
+Kubernetes meta/v1.Duration
+
+
+ |
+
+(Optional)
+ Time after which the TaskRun times out.
+Refer Go’s ParseDuration documentation for expected format: https://golang.org/pkg/time/#ParseDuration
+ |
+
PipelineTaskRunTemplate
diff --git a/examples/v1/pipelineruns/pipelinerun-with-task-timeout-override.yaml b/examples/v1/pipelineruns/pipelinerun-with-task-timeout-override.yaml
new file mode 100644
index 00000000000..176f2c2ebd9
--- /dev/null
+++ b/examples/v1/pipelineruns/pipelinerun-with-task-timeout-override.yaml
@@ -0,0 +1,45 @@
+apiVersion: tekton.dev/v1
+kind: Pipeline
+metadata:
+ name: pipeline-with-timeouts
+spec:
+ tasks:
+ - name: task-with-timeout
+ timeout: "1h0m0s"
+ taskSpec:
+ steps:
+ - name: sleep
+ image: ubuntu
+ script: |
+ echo "Starting task..."
+ sleep 30
+ echo "Task completed!"
+
+ - name: another-task-with-timeout
+ timeout: "30m0s"
+ runAfter:
+ - task-with-timeout
+ taskSpec:
+ steps:
+ - name: sleep
+ image: ubuntu
+ script: |
+ echo "Starting another task..."
+ sleep 30
+ echo "Another task completed!"
+
+---
+apiVersion: tekton.dev/v1
+kind: PipelineRun
+metadata:
+ name: pipeline-timeout-override-run
+spec:
+ pipelineRef:
+ name: pipeline-with-timeouts
+ timeouts:
+ pipeline: "3h0m0s" # overall pipeline timeout
+ taskRunSpecs:
+ - pipelineTaskName: task-with-timeout
+ timeout: "2h0m0s" # override timeout to 2 hours
+ - pipelineTaskName: another-task-with-timeout
+ timeout: "1h0m0s" # override timeout to 1 hour
diff --git a/pkg/apis/pipeline/v1/openapi_generated.go b/pkg/apis/pipeline/v1/openapi_generated.go
index 2d7d6f57880..2c7d060eced 100644
--- a/pkg/apis/pipeline/v1/openapi_generated.go
+++ b/pkg/apis/pipeline/v1/openapi_generated.go
@@ -2232,11 +2232,17 @@ func schema_pkg_apis_pipeline_v1_PipelineTaskRunSpec(ref common.ReferenceCallbac
Ref: ref("k8s.io/api/core/v1.ResourceRequirements"),
},
},
+ "timeout": {
+ SchemaProps: spec.SchemaProps{
+ Description: "Time after which the TaskRun times out. Refer Go's ParseDuration documentation for expected format: https://golang.org/pkg/time/#ParseDuration",
+ Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.Duration"),
+ },
+ },
},
},
},
Dependencies: []string{
- "github.com/tektoncd/pipeline/pkg/apis/pipeline/pod.Template", "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1.PipelineTaskMetadata", "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1.TaskRunSidecarSpec", "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1.TaskRunStepSpec", "k8s.io/api/core/v1.ResourceRequirements"},
+ "github.com/tektoncd/pipeline/pkg/apis/pipeline/pod.Template", "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1.PipelineTaskMetadata", "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1.TaskRunSidecarSpec", "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1.TaskRunStepSpec", "k8s.io/api/core/v1.ResourceRequirements", "k8s.io/apimachinery/pkg/apis/meta/v1.Duration"},
}
}
diff --git a/pkg/apis/pipeline/v1/pipeline_validation_test.go b/pkg/apis/pipeline/v1/pipeline_validation_test.go
index 2dd9b22d55b..6758eb07eed 100644
--- a/pkg/apis/pipeline/v1/pipeline_validation_test.go
+++ b/pkg/apis/pipeline/v1/pipeline_validation_test.go
@@ -1370,7 +1370,7 @@ func TestPipelineSpec_Validate_Failure(t *testing.T) {
if err == nil {
t.Errorf("PipelineSpec.Validate() did not return error for invalid pipelineSpec")
}
- if d := cmp.Diff(tt.expectedError.Error(), err.Error(), cmpopts.IgnoreUnexported(apis.FieldError{})); d != "" {
+ if d := cmp.Diff(tt.expectedError.Error(), err.Error()); d != "" {
t.Errorf("PipelineSpec.Validate() errors diff %s", diff.PrintWantGot(d))
}
})
diff --git a/pkg/apis/pipeline/v1/pipelinerun_types.go b/pkg/apis/pipeline/v1/pipelinerun_types.go
index 83511e74e9c..976a8b58f86 100644
--- a/pkg/apis/pipeline/v1/pipelinerun_types.go
+++ b/pkg/apis/pipeline/v1/pipelinerun_types.go
@@ -656,6 +656,11 @@ type PipelineTaskRunSpec struct {
// Compute resources to use for this TaskRun
ComputeResources *corev1.ResourceRequirements `json:"computeResources,omitempty"`
+
+ // Time after which the TaskRun times out.
+ // Refer Go's ParseDuration documentation for expected format: https://golang.org/pkg/time/#ParseDuration
+ // +optional
+ Timeout *metav1.Duration `json:"timeout,omitempty"`
}
// GetTaskRunSpec returns the task specific spec for a given
@@ -678,6 +683,7 @@ func (pr *PipelineRun) GetTaskRunSpec(pipelineTaskName string) PipelineTaskRunSp
s.SidecarSpecs = task.SidecarSpecs
s.Metadata = task.Metadata
s.ComputeResources = task.ComputeResources
+ s.Timeout = task.Timeout
}
}
return s
diff --git a/pkg/apis/pipeline/v1/pipelinerun_validation.go b/pkg/apis/pipeline/v1/pipelinerun_validation.go
index 16330aa2153..4cdddf19be0 100644
--- a/pkg/apis/pipeline/v1/pipelinerun_validation.go
+++ b/pkg/apis/pipeline/v1/pipelinerun_validation.go
@@ -87,6 +87,14 @@ func (ps *PipelineRunSpec) Validate(ctx context.Context) (errs *apis.FieldError)
// Validate propagated parameters
errs = errs.Also(ps.validateInlineParameters(ctx))
+ // Validate task run specs first
+ for idx, trs := range ps.TaskRunSpecs {
+ if trs.Timeout != nil && trs.Timeout.Duration < 0 {
+ errs = errs.Also(apis.ErrInvalidValue(trs.Timeout.Duration.String()+" should be >= 0", "taskRunSpecs["+trs.PipelineTaskName+"].timeout"))
+ }
+ errs = errs.Also(validateTaskRunSpec(ctx, trs).ViaIndex(idx).ViaField("taskRunSpecs"))
+ }
+
if ps.Timeouts != nil {
// tasks timeout should be a valid duration of at least 0.
errs = errs.Also(validateTimeoutDuration("tasks", ps.Timeouts.Tasks))
@@ -103,6 +111,20 @@ func (ps *PipelineRunSpec) Validate(ctx context.Context) (errs *apis.FieldError)
defaultTimeout := time.Duration(config.FromContextOrDefaults(ctx).Defaults.DefaultTimeoutMinutes)
errs = errs.Also(ps.validatePipelineTimeout(defaultTimeout, "should be <= default timeout duration"))
}
+
+ if len(ps.TaskRunSpecs) > 0 {
+ pipelineTimeout := ps.Timeouts.Pipeline
+ if pipelineTimeout == nil {
+ pipelineTimeout = &metav1.Duration{Duration: time.Duration(config.FromContextOrDefaults(ctx).Defaults.DefaultTimeoutMinutes) * time.Minute}
+ }
+ for _, taskRunSpec := range ps.TaskRunSpecs {
+ if taskRunSpec.Timeout != nil {
+ if taskRunSpec.Timeout.Duration > pipelineTimeout.Duration && pipelineTimeout.Duration != config.NoTimeoutDuration {
+ errs = errs.Also(apis.ErrInvalidValue(taskRunSpec.Timeout.Duration.String()+" should be <= pipeline duration", "taskRunSpecs["+taskRunSpec.PipelineTaskName+"].timeout"))
+ }
+ }
+ }
+ }
}
errs = errs.Also(validateSpecStatus(ps.Status))
diff --git a/pkg/apis/pipeline/v1/pipelinerun_validation_test.go b/pkg/apis/pipeline/v1/pipelinerun_validation_test.go
index 42a38ed7c3f..faec8b856b9 100644
--- a/pkg/apis/pipeline/v1/pipelinerun_validation_test.go
+++ b/pkg/apis/pipeline/v1/pipelinerun_validation_test.go
@@ -805,6 +805,28 @@ func TestPipelineRun_Validate(t *testing.T) {
},
},
wc: cfgtesting.EnableAlphaAPIFields,
+ }, {
+ name: "valid task-specific timeouts",
+ pr: v1.PipelineRun{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "pipelinelinename",
+ },
+ Spec: v1.PipelineRunSpec{
+ PipelineRef: &v1.PipelineRef{
+ Name: "prname",
+ },
+ Timeouts: &v1.TimeoutFields{
+ Pipeline: &metav1.Duration{Duration: 1 * time.Hour},
+ },
+ TaskRunSpecs: []v1.PipelineTaskRunSpec{{
+ PipelineTaskName: "task1",
+ Timeout: &metav1.Duration{Duration: 30 * time.Minute},
+ }, {
+ PipelineTaskName: "task2",
+ Timeout: &metav1.Duration{Duration: 45 * time.Minute},
+ }},
+ },
+ },
}}
for _, ts := range tests {
@@ -1104,7 +1126,7 @@ func TestPipelineRunSpec_Invalidate(t *testing.T) {
ctx = ps.withContext(ctx)
}
err := ps.spec.Validate(ctx)
- if d := cmp.Diff(ps.wantErr.Error(), err.Error()); d != "" {
+ if d := cmp.Diff(ps.wantErr.Error(), err.Error(), cmpopts.IgnoreUnexported(apis.FieldError{})); d != "" {
t.Error(diff.PrintWantGot(d))
}
})
@@ -1202,6 +1224,23 @@ func TestPipelineRun_InvalidTimeouts(t *testing.T) {
},
},
want: apis.ErrInvalidValue("-48h0m0s should be >= 0", "spec.timeouts.pipeline"),
+ }, {
+ name: "negative task-specific timeout",
+ pr: v1.PipelineRun{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "pipelinelinename",
+ },
+ Spec: v1.PipelineRunSpec{
+ PipelineRef: &v1.PipelineRef{
+ Name: "prname",
+ },
+ TaskRunSpecs: []v1.PipelineTaskRunSpec{{
+ PipelineTaskName: "task1",
+ Timeout: &metav1.Duration{Duration: -1 * time.Hour},
+ }},
+ },
+ },
+ want: apis.ErrInvalidValue("-1h0m0s should be >= 0", "spec.taskRunSpecs[task1].timeout"),
}, {
name: "negative pipeline tasks Timeout",
pr: v1.PipelineRun{
@@ -1352,6 +1391,26 @@ func TestPipelineRun_InvalidTimeouts(t *testing.T) {
},
},
want: apis.ErrInvalidValue(`0s (no timeout) should be <= pipeline duration`, "spec.timeouts.finally"),
+ }, {
+ name: "task-specific timeout exceeds pipeline timeout",
+ pr: v1.PipelineRun{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "pipelinelinename",
+ },
+ Spec: v1.PipelineRunSpec{
+ PipelineRef: &v1.PipelineRef{
+ Name: "prname",
+ },
+ Timeouts: &v1.TimeoutFields{
+ Pipeline: &metav1.Duration{Duration: 1 * time.Hour},
+ },
+ TaskRunSpecs: []v1.PipelineTaskRunSpec{{
+ PipelineTaskName: "task1",
+ Timeout: &metav1.Duration{Duration: 2 * time.Hour},
+ }},
+ },
+ },
+ want: apis.ErrInvalidValue("2h0m0s should be <= pipeline duration", "spec.taskRunSpecs[task1].timeout"),
}}
for _, tc := range tests {
diff --git a/pkg/apis/pipeline/v1/swagger.json b/pkg/apis/pipeline/v1/swagger.json
index a459512f45e..3bd01244688 100644
--- a/pkg/apis/pipeline/v1/swagger.json
+++ b/pkg/apis/pipeline/v1/swagger.json
@@ -1111,6 +1111,10 @@
"$ref": "#/definitions/v1.TaskRunStepSpec"
},
"x-kubernetes-list-type": "atomic"
+ },
+ "timeout": {
+ "description": "Time after which the TaskRun times out. Refer Go's ParseDuration documentation for expected format: https://golang.org/pkg/time/#ParseDuration",
+ "$ref": "#/definitions/v1.Duration"
}
}
},
diff --git a/pkg/apis/pipeline/v1/zz_generated.deepcopy.go b/pkg/apis/pipeline/v1/zz_generated.deepcopy.go
index c3057d33561..d136443930b 100644
--- a/pkg/apis/pipeline/v1/zz_generated.deepcopy.go
+++ b/pkg/apis/pipeline/v1/zz_generated.deepcopy.go
@@ -997,6 +997,11 @@ func (in *PipelineTaskRunSpec) DeepCopyInto(out *PipelineTaskRunSpec) {
*out = new(corev1.ResourceRequirements)
(*in).DeepCopyInto(*out)
}
+ if in.Timeout != nil {
+ in, out := &in.Timeout, &out.Timeout
+ *out = new(metav1.Duration)
+ **out = **in
+ }
return
}