From c19714c3c42b84ecc46cdfd605c9219bacc1c74b Mon Sep 17 00:00:00 2001 From: Vibhav Bobade Date: Sat, 29 Mar 2025 05:58:13 +0530 Subject: [PATCH] feat: override task timeouts in pipelineruns --- config/300-crds/300-pipelinerun.yaml | 5 ++ docs/pipeline-api.md | 15 +++++ ...ipelinerun-with-task-timeout-override.yaml | 45 ++++++++++++++ pkg/apis/pipeline/v1/openapi_generated.go | 8 ++- .../pipeline/v1/pipeline_validation_test.go | 2 +- pkg/apis/pipeline/v1/pipelinerun_types.go | 6 ++ .../pipeline/v1/pipelinerun_validation.go | 22 +++++++ .../v1/pipelinerun_validation_test.go | 61 ++++++++++++++++++- pkg/apis/pipeline/v1/swagger.json | 4 ++ pkg/apis/pipeline/v1/zz_generated.deepcopy.go | 5 ++ 10 files changed, 170 insertions(+), 3 deletions(-) create mode 100644 examples/v1/pipelineruns/pipelinerun-with-task-timeout-override.yaml 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 }