Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
36 changes: 36 additions & 0 deletions docs/taskruns.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ weight: 202
- [Monitoring `Steps`](#monitoring-steps)
- [Steps](#steps)
- [Monitoring `Results`](#monitoring-results)
- [Pending `TaskRun`s](#pending-taskruns)
- [Cancelling a `TaskRun`](#cancelling-a-taskrun)
- [Debugging a `TaskRun`](#debugging-a-taskrun)
- [Breakpoint on Failure](#breakpoint-on-failure)
Expand Down Expand Up @@ -872,6 +873,41 @@ Status:

```

## Pending `TaskRun`s

You can create a `TaskRun` in a pending state so that it does not start execution immediately.
This is useful for:

- **External scheduling**: Create TaskRuns in advance and start them based on external triggers
- **Approval workflows**: Require manual approval before task execution
- **Resource management**: Queue tasks and start them when resources are available
- **Batch operations**: Create multiple TaskRuns and start them simultaneously

When pending:

- The `TaskRun` is created but no Pod is created
- `status.startTime` is not set
- The condition is set to `Unknown` with reason `TaskRunPending`
- Clearing `spec.status` (or setting it to empty) starts execution
- Setting `spec.status: TaskRunCancelled` cancels without running

**Note:** A `TaskRun` can only be marked "pending" before it has started; this setting is invalid after the `TaskRun` has started.

To create a pending `TaskRun`, set `.spec.status` to `TaskRunPending`:

```yaml
apiVersion: tekton.dev/v1 # or tekton.dev/v1beta1
kind: TaskRun
metadata:
name: my-taskrun
spec:
taskRef:
name: my-task
status: "TaskRunPending"
```

To start the TaskRun, clear the `.spec.status` field. Alternatively, update the value to `TaskRunCancelled` to cancel it.

## Cancelling a `TaskRun`

To cancel a `TaskRun` that's currently executing, update its status to mark it as cancelled.
Expand Down
10 changes: 10 additions & 0 deletions pkg/apis/pipeline/v1/taskrun_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,9 @@ const (
// TaskRunSpecStatusCancelled indicates that the user wants to cancel the task,
// if not already cancelled or terminated
TaskRunSpecStatusCancelled = "TaskRunCancelled"
// TaskRunSpecStatusPending indicates that the user wants to postpone starting the task.
// When pending, no Pod is created and StartTime is not set.
TaskRunSpecStatusPending = "TaskRunPending"
)

// TaskRunSpecStatusMessage defines human readable status messages for the TaskRun.
Expand Down Expand Up @@ -237,6 +240,8 @@ const (
// TaskRunReasonFailureIgnored is the reason set when the Taskrun has failed due to pod execution error and the failure is ignored for the owning PipelineRun.
// TaskRuns failed due to reconciler/validation error should not use this reason.
TaskRunReasonFailureIgnored TaskRunReason = "FailureIgnored"
// TaskRunReasonPending is the reason set when the TaskRun is in the pending state
TaskRunReasonPending TaskRunReason = "TaskRunPending"
)

func (t TaskRunReason) String() string {
Expand Down Expand Up @@ -491,6 +496,11 @@ func (tr *TaskRun) IsCancelled() bool {
return tr.Spec.Status == TaskRunSpecStatusCancelled
}

// IsPending returns true if the TaskRun's spec status is set to Pending state.
func (tr *TaskRun) IsPending() bool {
return tr.Spec.Status == TaskRunSpecStatusPending
}

// IsRetriable returns true if the TaskRun's Retries is not exhausted.
func (tr *TaskRun) IsRetriable() bool {
return len(tr.Status.RetriesStatus) < tr.Spec.Retries
Expand Down
9 changes: 7 additions & 2 deletions pkg/apis/pipeline/v1/taskrun_validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@ func (tr *TaskRun) SupportedVerbs() []admissionregistrationv1.OperationType {
// Validate taskrun
func (tr *TaskRun) Validate(ctx context.Context) *apis.FieldError {
errs := validate.ObjectMetadata(tr.GetObjectMeta()).ViaField("metadata")

if tr.IsPending() && tr.HasStarted() {
errs = errs.Also(apis.ErrInvalidValue("TaskRun cannot be Pending after it is started", "spec.status"))
}

return errs.Also(tr.Spec.Validate(apis.WithinSpec(ctx)).ViaField("spec"))
}

Expand Down Expand Up @@ -97,8 +102,8 @@ func (ts *TaskRunSpec) Validate(ctx context.Context) (errs *apis.FieldError) {
}

if ts.Status != "" {
if ts.Status != TaskRunSpecStatusCancelled {
errs = errs.Also(apis.ErrInvalidValue(fmt.Sprintf("%s should be %s", ts.Status, TaskRunSpecStatusCancelled), "status"))
if ts.Status != TaskRunSpecStatusCancelled && ts.Status != TaskRunSpecStatusPending {
errs = errs.Also(apis.ErrInvalidValue(fmt.Sprintf("%s should be %s or %s", ts.Status, TaskRunSpecStatusCancelled, TaskRunSpecStatusPending), "status"))
}
}
if ts.Status == "" {
Expand Down
29 changes: 28 additions & 1 deletion pkg/apis/pipeline/v1/taskrun_validation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,24 @@ func TestTaskRun_Invalidate(t *testing.T) {
Paths: []string{"spec.task-words.properties"},
},
wc: cfgtesting.EnableAlphaAPIFields,
}, {
name: "taskrun pending while running",
taskRun: &v1.TaskRun{
ObjectMeta: metav1.ObjectMeta{Name: "tr"},
Spec: v1.TaskRunSpec{
TaskRef: &v1.TaskRef{Name: "mytask"},
Status: v1.TaskRunSpecStatusPending,
},
Status: v1.TaskRunStatus{
TaskRunStatusFields: v1.TaskRunStatusFields{
StartTime: &metav1.Time{Time: time.Now()},
},
},
},
want: &apis.FieldError{
Message: "invalid value: TaskRun cannot be Pending after it is started",
Paths: []string{"spec.status"},
},
}}
for _, ts := range tests {
t.Run(ts.name, func(t *testing.T) {
Expand All @@ -144,6 +162,15 @@ func TestTaskRun_Validate(t *testing.T) {
taskRun *v1.TaskRun
wc func(context.Context) context.Context
}{{
name: "valid pending taskrun",
taskRun: &v1.TaskRun{
ObjectMeta: metav1.ObjectMeta{Name: "tr"},
Spec: v1.TaskRunSpec{
TaskRef: &v1.TaskRef{Name: "mytask"},
Status: v1.TaskRunSpecStatusPending,
},
},
}, {
name: "propagating params with taskrun",
taskRun: &v1.TaskRun{
ObjectMeta: metav1.ObjectMeta{Name: "tr"},
Expand Down Expand Up @@ -614,7 +641,7 @@ func TestTaskRunSpec_Invalidate(t *testing.T) {
},
Status: "TaskRunCancell",
},
wantErr: apis.ErrInvalidValue("TaskRunCancell should be TaskRunCancelled", "status"),
wantErr: apis.ErrInvalidValue("TaskRunCancell should be TaskRunCancelled or TaskRunPending", "status"),
}, {
name: "incorrectly set statusMesage",
spec: v1.TaskRunSpec{
Expand Down
10 changes: 10 additions & 0 deletions pkg/apis/pipeline/v1beta1/taskrun_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,9 @@ const (
// TaskRunSpecStatusCancelled indicates that the user wants to cancel the task,
// if not already cancelled or terminated
TaskRunSpecStatusCancelled = "TaskRunCancelled"
// TaskRunSpecStatusPending indicates that the user wants to postpone starting the task.
// When pending, no Pod is created and StartTime is not set.
TaskRunSpecStatusPending = "TaskRunPending"
)

// TaskRunSpecStatusMessage defines human readable status messages for the TaskRun.
Expand Down Expand Up @@ -230,6 +233,8 @@ const (
TaskRunReasonResultLargerThanAllowedLimit TaskRunReason = "TaskRunResultLargerThanAllowedLimit"
// TaskRunReasonStopSidecarFailed indicates that the sidecar is not properly stopped.
TaskRunReasonStopSidecarFailed = "TaskRunStopSidecarFailed"
// TaskRunReasonPending is the reason set when the TaskRun is in the pending state
TaskRunReasonPending TaskRunReason = "TaskRunPending"
)

func (t TaskRunReason) String() string {
Expand Down Expand Up @@ -534,6 +539,11 @@ func (tr *TaskRun) IsCancelled() bool {
return tr.Spec.Status == TaskRunSpecStatusCancelled
}

// IsPending returns true if the TaskRun's spec status is set to Pending state.
func (tr *TaskRun) IsPending() bool {
return tr.Spec.Status == TaskRunSpecStatusPending
}

// IsTaskRunResultVerified returns true if the TaskRun's results have been validated by spire.
func (tr *TaskRun) IsTaskRunResultVerified() bool {
return tr.Status.GetCondition(apis.ConditionType(TaskRunConditionResultsVerified.String())).IsTrue()
Expand Down
9 changes: 7 additions & 2 deletions pkg/apis/pipeline/v1beta1/taskrun_validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@ func (tr *TaskRun) SupportedVerbs() []admissionregistrationv1.OperationType {
// Validate taskrun
func (tr *TaskRun) Validate(ctx context.Context) *apis.FieldError {
errs := validate.ObjectMetadata(tr.GetObjectMeta()).ViaField("metadata")

if tr.IsPending() && tr.HasStarted() {
errs = errs.Also(apis.ErrInvalidValue("TaskRun cannot be Pending after it is started", "spec.status"))
}

return errs.Also(tr.Spec.Validate(apis.WithinSpec(ctx)).ViaField("spec"))
}

Expand Down Expand Up @@ -97,8 +102,8 @@ func (ts *TaskRunSpec) Validate(ctx context.Context) (errs *apis.FieldError) {
}

if ts.Status != "" {
if ts.Status != TaskRunSpecStatusCancelled {
errs = errs.Also(apis.ErrInvalidValue(fmt.Sprintf("%s should be %s", ts.Status, TaskRunSpecStatusCancelled), "status"))
if ts.Status != TaskRunSpecStatusCancelled && ts.Status != TaskRunSpecStatusPending {
errs = errs.Also(apis.ErrInvalidValue(fmt.Sprintf("%s should be %s or %s", ts.Status, TaskRunSpecStatusCancelled, TaskRunSpecStatusPending), "status"))
}
}
if ts.Status == "" {
Expand Down
29 changes: 28 additions & 1 deletion pkg/apis/pipeline/v1beta1/taskrun_validation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,24 @@ func TestTaskRun_Invalidate(t *testing.T) {
Message: `missing field(s)`,
Paths: []string{"spec.task-words.properties"},
},
}, {
name: "taskrun pending while running",
taskRun: &v1beta1.TaskRun{
ObjectMeta: metav1.ObjectMeta{Name: "tr"},
Spec: v1beta1.TaskRunSpec{
TaskRef: &v1beta1.TaskRef{Name: "mytask"},
Status: v1beta1.TaskRunSpecStatusPending,
},
Status: v1beta1.TaskRunStatus{
TaskRunStatusFields: v1beta1.TaskRunStatusFields{
StartTime: &metav1.Time{Time: time.Now()},
},
},
},
want: &apis.FieldError{
Message: "invalid value: TaskRun cannot be Pending after it is started",
Paths: []string{"spec.status"},
},
}, {
name: "uses bundle (deprecated) on creation is disallowed",
taskRun: &v1beta1.TaskRun{
Expand Down Expand Up @@ -183,6 +201,15 @@ func TestTaskRun_Validate(t *testing.T) {
taskRun *v1beta1.TaskRun
wc func(context.Context) context.Context
}{{
name: "valid pending taskrun",
taskRun: &v1beta1.TaskRun{
ObjectMeta: metav1.ObjectMeta{Name: "tr"},
Spec: v1beta1.TaskRunSpec{
TaskRef: &v1beta1.TaskRef{Name: "mytask"},
Status: v1beta1.TaskRunSpecStatusPending,
},
},
}, {
name: "propagating params with taskrun",
taskRun: &v1beta1.TaskRun{
ObjectMeta: metav1.ObjectMeta{Name: "tr"},
Expand Down Expand Up @@ -574,7 +601,7 @@ func TestTaskRunSpec_Invalidate(t *testing.T) {
},
Status: "TaskRunCancell",
},
wantErr: apis.ErrInvalidValue("TaskRunCancell should be TaskRunCancelled", "status"),
wantErr: apis.ErrInvalidValue("TaskRunCancell should be TaskRunCancelled or TaskRunPending", "status"),
}, {
name: "incorrectly set statusMesage",
spec: v1beta1.TaskRunSpec{
Expand Down
8 changes: 7 additions & 1 deletion pkg/reconciler/taskrun/taskrun.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ func (c *Reconciler) ReconcileKind(ctx context.Context, tr *v1.TaskRun) pkgrecon

// If the TaskRun is just starting, this will also set the starttime,
// from which the timeout will immediately begin counting down.
if !tr.HasStarted() {
if !tr.HasStarted() && !tr.IsPending() {
tr.Status.InitializeConditions()
// In case node time was not synchronized, when controller has been scheduled to other nodes.
if tr.Status.StartTime.Sub(tr.CreationTimestamp.Time) < 0 {
Expand Down Expand Up @@ -191,6 +191,12 @@ func (c *Reconciler) ReconcileKind(ctx context.Context, tr *v1.TaskRun) pkgrecon
return c.finishReconcileUpdateEmitEvents(ctx, tr, before, err)
}

// When TaskRun is pending, do not create a Pod. Set condition and return.
if tr.IsPending() {
tr.Status.MarkResourceOngoing(v1.TaskRunReasonPending, fmt.Sprintf("TaskRun %q is pending", tr.Name))
return c.finishReconcileUpdateEmitEvents(ctx, tr, before, nil)
}

// Check if the TaskRun has timed out; if it is, this will set its status
// accordingly.
if tr.HasTimedOut(ctx, c.Clock) {
Expand Down
42 changes: 42 additions & 0 deletions pkg/reconciler/taskrun/taskrun_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1372,6 +1372,48 @@ status:
}
}

func TestReconcileOnPendingTaskRun(t *testing.T) {
taskRun := parse.MustParseV1TaskRun(t, `
metadata:
name: test-taskrun-pending
namespace: foo
spec:
taskRef:
name: test-task
status: TaskRunPending
`)
d := test.Data{
TaskRuns: []*v1.TaskRun{taskRun},
Tasks: []*v1.Task{simpleTask},
}
testAssets, cancel := getTaskRunController(t, d)
defer cancel()
createServiceAccount(t, testAssets, "default", "foo")

if err := testAssets.Controller.Reconciler.Reconcile(testAssets.Ctx, getRunName(taskRun)); err != nil {
t.Errorf("expected no error reconciling pending TaskRun but got %v", err)
}

updatedTR, err := testAssets.Clients.Pipeline.TektonV1().TaskRuns(taskRun.Namespace).Get(testAssets.Ctx, taskRun.Name, metav1.GetOptions{})
if err != nil {
t.Fatalf("Expected TaskRun %s to exist but instead got error when getting it: %v", taskRun.Name, err)
}

condition := updatedTR.Status.GetCondition(apis.ConditionSucceeded)
if condition == nil || condition.Status != corev1.ConditionUnknown {
t.Errorf("Expected pending TaskRun to have condition status Unknown, but had %v", condition)
}
if condition != nil && condition.Reason != v1.TaskRunReasonPending.String() {
t.Errorf("Expected reason %q but was %q", v1.TaskRunReasonPending, condition.Reason)
}
if updatedTR.Status.StartTime != nil {
t.Errorf("Start time should be nil for pending TaskRun, not: %s", updatedTR.Status.StartTime)
}
if updatedTR.Status.PodName != "" {
t.Errorf("Pod should not be created for pending TaskRun, but PodName was %q", updatedTR.Status.PodName)
}
}

func TestReconcileInvalidTaskRuns(t *testing.T) {
noTaskRun := parse.MustParseV1TaskRun(t, `
metadata:
Expand Down
Loading