Skip to content

Commit 23a2b91

Browse files
abayerknative-prow-robot
authored andcommitted
Add Pipeline-level timeouts, implement Task-level timeouts
Fixes tektoncd#222
1 parent ed1b898 commit 23a2b91

20 files changed

+525
-25
lines changed

docs/using.md

+12
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
- [How do I make Resources?](#creating-resources)
66
- [How do I run a Pipeline?](#running-a-pipeline)
77
- [How do I run a Task on its own?](#running-a-task)
8+
- [How do I ensure a Pipeline or Task stops if it runs for too long?](#timing-out-pipelines-and-tasks)
89
- [How do I troubleshoot a PipelineRun?](#troubleshooting)
910
- [How do I follow logs?](../test/logs/README.md)
1011

@@ -992,6 +993,17 @@ Below is an example on how to create a storage resource with service account.
992993
secretKey: service_account.json
993994
```
994995

996+
## Timing Out Pipelines and Tasks
997+
998+
If you want to ensure that your `Pipeline` or `Task` will be stopped if it runs
999+
past a certain duration, you can use the `Timeout` field on either `Pipeline`
1000+
or `Task`. In both cases, add the following to the `spec`:
1001+
1002+
```yaml
1003+
spec:
1004+
timeout: 5m
1005+
```
1006+
9951007
## Troubleshooting
9961008

9971009
All objects created by the build-pipeline controller show the lineage of where

pkg/apis/pipeline/v1alpha1/pipeline_types.go

+7-3
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,13 @@ import (
2323

2424
// PipelineSpec defines the desired state of PipeLine.
2525
type PipelineSpec struct {
26-
Resources []PipelineDeclaredResource `json:"resources"`
27-
Tasks []PipelineTask `json:"tasks"`
28-
Generation int64 `json:"generation,omitempty"`
26+
Resources []PipelineDeclaredResource `json:"resources"`
27+
Tasks []PipelineTask `json:"tasks"`
28+
// Time after which the Pipeline times out. Defaults to never.
29+
// Refer Go's ParseDuration documentation for expected format: https://golang.org/pkg/time/#ParseDuration
30+
// +optional
31+
Timeout *metav1.Duration `json:"timeout,omitempty"`
32+
Generation int64 `json:"generation,omitempty"`
2933
}
3034

3135
// PipelineStatus does not contain anything because Pipelines on their own

pkg/apis/pipeline/v1alpha1/pipeline_validation.go

+8
Original file line numberDiff line numberDiff line change
@@ -124,5 +124,13 @@ func (ps *PipelineSpec) Validate() *apis.FieldError {
124124
if err := validateFrom(ps.Tasks); err != nil {
125125
return apis.ErrInvalidValue(err.Error(), "spec.tasks.resources.inputs.from")
126126
}
127+
128+
if ps.Timeout != nil {
129+
// timeout should be a valid duration of at least 0.
130+
if ps.Timeout.Duration <= 0 {
131+
return apis.ErrInvalidValue(fmt.Sprintf("%s should be > 0", ps.Timeout.Duration.String()), "spec.timeout")
132+
}
133+
}
134+
127135
return nil
128136
}

pkg/apis/pipeline/v1alpha1/pipeline_validation_test.go

+18
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,11 @@ package v1alpha1_test
1818

1919
import (
2020
"testing"
21+
"time"
2122

2223
"github.com/knative/build-pipeline/pkg/apis/pipeline/v1alpha1"
2324
tb "github.com/knative/build-pipeline/test/builder"
25+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2426
)
2527

2628
func TestPipelineSpec_Validate_Error(t *testing.T) {
@@ -100,6 +102,14 @@ func TestPipelineSpec_Validate_Error(t *testing.T) {
100102
tb.PipelineTaskInputResource("wow-image", "wonderful-resource", tb.From("bar"))),
101103
)),
102104
},
105+
{
106+
name: "negative pipeline timeout",
107+
p: tb.Pipeline("pipeline", "namespace", tb.PipelineSpec(
108+
tb.PipelineTask("foo", "foo-task"),
109+
tb.PipelineTask("bar", "bar-task"),
110+
tb.PipelineTimeout(&metav1.Duration{Duration: -48 * time.Hour}),
111+
)),
112+
},
103113
}
104114
for _, tt := range tests {
105115
t.Run(tt.name, func(t *testing.T) {
@@ -144,6 +154,14 @@ func TestPipelineSpec_Validate_Valid(t *testing.T) {
144154
tb.PipelineTaskInputResource("wow-image", "wonderful-resource", tb.From("bar"))),
145155
)),
146156
},
157+
{
158+
name: "valid timeout",
159+
p: tb.Pipeline("pipeline", "namespace", tb.PipelineSpec(
160+
tb.PipelineTask("foo", "foo-task"),
161+
tb.PipelineTask("bar", "bar-task"),
162+
tb.PipelineTimeout(&metav1.Duration{Duration: 24 * time.Hour}),
163+
)),
164+
},
147165
}
148166
for _, tt := range tests {
149167
t.Run(tt.name, func(t *testing.T) {

pkg/apis/pipeline/v1alpha1/pipelinerun_types.go

+10
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package v1alpha1
1818

1919
import (
2020
"fmt"
21+
"time"
2122

2223
duckv1alpha1 "github.com/knative/pkg/apis/duck/v1alpha1"
2324
"github.com/knative/pkg/webhook"
@@ -107,6 +108,12 @@ type PipelineRunStatus struct {
107108
// In #107 should be updated to hold the location logs have been uploaded to
108109
// +optional
109110
Results *Results `json:"results,omitempty"`
111+
// StartTime is the time the PipelineRun is actually started.
112+
// +optional
113+
StartTime *metav1.Time `json:"startTime,omitempty"`
114+
// CompletionTime is the time the PipelineRun completed.
115+
// +optional
116+
CompletionTime *metav1.Time `json:"completionTime,omitempty"`
110117
// map of TaskRun Status with the taskRun name as the key
111118
//+optional
112119
TaskRuns map[string]TaskRunStatus `json:"taskRuns,omitempty"`
@@ -124,6 +131,9 @@ func (pr *PipelineRunStatus) InitializeConditions() {
124131
if pr.TaskRuns == nil {
125132
pr.TaskRuns = make(map[string]TaskRunStatus)
126133
}
134+
if pr.StartTime.IsZero() {
135+
pr.StartTime = &metav1.Time{time.Now()}
136+
}
127137
pipelineRunCondSet.Manage(pr).InitializeConditions()
128138
}
129139

pkg/apis/pipeline/v1alpha1/pipelinerun_types_test.go

+4
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,10 @@ func TestInitializeConditions(t *testing.T) {
7676
t.Fatalf("PipelineRun status not initialized correctly")
7777
}
7878

79+
if p.Status.StartTime.IsZero() {
80+
t.Fatalf("PipelineRun StartTime not initialized correctly")
81+
}
82+
7983
p.Status.TaskRuns["fooTask"] = TaskRunStatus{}
8084

8185
p.Status.InitializeConditions()

pkg/apis/pipeline/v1alpha1/taskrun_types.go

+4
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package v1alpha1
1818

1919
import (
2020
"fmt"
21+
"time"
2122

2223
"github.com/knative/pkg/apis"
2324
duckv1alpha1 "github.com/knative/pkg/apis/duck/v1alpha1"
@@ -136,6 +137,9 @@ func (tr *TaskRunStatus) GetCondition(t duckv1alpha1.ConditionType) *duckv1alpha
136137
return taskRunCondSet.Manage(tr).GetCondition(t)
137138
}
138139
func (tr *TaskRunStatus) InitializeConditions() {
140+
if tr.StartTime.IsZero() {
141+
tr.StartTime = &metav1.Time{time.Now()}
142+
}
139143
taskRunCondSet.Manage(tr).InitializeConditions()
140144
}
141145

pkg/apis/pipeline/v1alpha1/taskrun_validation.go

+1
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ func (ts *TaskRunSpec) Validate() *apis.FieldError {
6969
return err
7070
}
7171
}
72+
7273
return nil
7374
}
7475

pkg/apis/pipeline/v1alpha1/zz_generated.deepcopy.go

+27
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/reconciler/v1alpha1/pipelinerun/pipelinerun.go

+21-8
Original file line numberDiff line numberDiff line change
@@ -294,15 +294,16 @@ func (c *Reconciler) reconcile(ctx context.Context, pr *v1alpha1.PipelineRun) er
294294

295295
if rprt != nil {
296296
c.Logger.Infof("Creating a new TaskRun object %s", rprt.TaskRunName)
297-
rprt.TaskRun, err = c.createTaskRun(c.Logger, rprt, pr, serviceAccount)
297+
rprt.TaskRun, err = c.createTaskRun(c.Logger, rprt, pr, serviceAccount, p.Spec.Timeout)
298298
if err != nil {
299299
c.Recorder.Eventf(pr, corev1.EventTypeWarning, "TaskRunCreationFailed", "Failed to create TaskRun %q: %v", rprt.TaskRunName, err)
300300
return fmt.Errorf("error creating TaskRun called %s for PipelineTask %s from PipelineRun %s: %s", rprt.TaskRunName, rprt.PipelineTask.Name, pr.Name, err)
301301
}
302302
}
303303

304304
before := pr.Status.GetCondition(duckv1alpha1.ConditionSucceeded)
305-
after := resources.GetPipelineConditionStatus(pr.Name, pipelineState, c.Logger)
305+
after := resources.GetPipelineConditionStatus(pr.Name, pipelineState, c.Logger, pr.Status.StartTime, p.Spec.Timeout)
306+
306307
pr.Status.SetCondition(after)
307308

308309
reconciler.EmitEvent(c.Recorder, before, after, pr)
@@ -321,7 +322,21 @@ func updateTaskRunsStatus(pr *v1alpha1.PipelineRun, pipelineState []*resources.R
321322
}
322323
}
323324

324-
func (c *Reconciler) createTaskRun(logger *zap.SugaredLogger, rprt *resources.ResolvedPipelineRunTask, pr *v1alpha1.PipelineRun, sa string) (*v1alpha1.TaskRun, error) {
325+
func (c *Reconciler) createTaskRun(logger *zap.SugaredLogger, rprt *resources.ResolvedPipelineRunTask, pr *v1alpha1.PipelineRun, sa string, pipelineTimeout *metav1.Duration) (*v1alpha1.TaskRun, error) {
326+
ts := rprt.ResolvedTaskResources.TaskSpec.DeepCopy()
327+
if pipelineTimeout != nil {
328+
pTimeoutTime := pr.Status.StartTime.Add(pipelineTimeout.Duration)
329+
if ts.Timeout == nil || time.Now().Add(ts.Timeout.Duration).After(pTimeoutTime) {
330+
// Just in case something goes awry and we're creating the TaskRun after it should have already timed out,
331+
// set a timeout of 0.
332+
taskRunTimeout := pTimeoutTime.Sub(time.Now())
333+
if taskRunTimeout < 0 {
334+
taskRunTimeout = 0
335+
}
336+
ts.Timeout = &metav1.Duration{Duration: taskRunTimeout}
337+
}
338+
}
339+
325340
tr := &v1alpha1.TaskRun{
326341
ObjectMeta: metav1.ObjectMeta{
327342
Name: rprt.TaskRunName,
@@ -333,15 +348,13 @@ func (c *Reconciler) createTaskRun(logger *zap.SugaredLogger, rprt *resources.Re
333348
},
334349
},
335350
Spec: v1alpha1.TaskRunSpec{
336-
TaskRef: &v1alpha1.TaskRef{
337-
Name: rprt.ResolvedTaskResources.TaskName,
338-
},
351+
TaskSpec: ts,
339352
Inputs: v1alpha1.TaskRunInputs{
340353
Params: rprt.PipelineTask.Params,
341354
},
342355
ServiceAccount: sa,
343-
},
344-
}
356+
}}
357+
345358
resources.WrapSteps(&tr.Spec, rprt.PipelineTask, rprt.ResolvedTaskResources.Inputs, rprt.ResolvedTaskResources.Outputs)
346359

347360
return c.PipelineClientSet.PipelineV1alpha1().TaskRuns(pr.Namespace).Create(tr)

pkg/reconciler/v1alpha1/pipelinerun/pipelinerun_test.go

+71-6
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,15 @@ func TestReconcile(t *testing.T) {
184184
),
185185
tb.TaskRunLabel("pipeline.knative.dev/pipeline", "test-pipeline"),
186186
tb.TaskRunLabel("pipeline.knative.dev/pipelineRun", "test-pipeline-run-success"),
187-
tb.TaskRunSpec(tb.TaskRunTaskRef("unit-test-task"),
187+
tb.TaskRunSpec(tb.TaskRunTaskSpec(
188+
tb.TaskInputs(
189+
tb.InputsResource("workspace", v1alpha1.PipelineResourceTypeGit),
190+
tb.InputsParam("foo"), tb.InputsParam("bar"), tb.InputsParam("templatedparam"),
191+
),
192+
tb.TaskOutputs(
193+
tb.OutputsResource("image-to-use", v1alpha1.PipelineResourceTypeImage),
194+
tb.OutputsResource("workspace", v1alpha1.PipelineResourceTypeGit),
195+
)),
188196
tb.TaskRunServiceAccount("test-sa"),
189197
tb.TaskRunInputs(
190198
tb.TaskRunInputsParam("foo", "somethingfun"),
@@ -425,8 +433,7 @@ func TestReconcileOnCompletedPipelineRun(t *testing.T) {
425433
})),
426434
)}
427435
ps := []*v1alpha1.Pipeline{tb.Pipeline("test-pipeline", "foo", tb.PipelineSpec(
428-
tb.PipelineTask("hello-world-1", "hellow-world"),
429-
))}
436+
tb.PipelineTask("hello-world-1", "hellow-world")))}
430437
ts := []*v1alpha1.Task{tb.Task("hello-world", "foo")}
431438
d := test.Data{
432439
PipelineRuns: prs,
@@ -496,6 +503,7 @@ func TestReconcileOnCancelledPipelineRun(t *testing.T) {
496503
),
497504
),
498505
}
506+
499507
d := test.Data{
500508
PipelineRuns: prs,
501509
Pipelines: ps,
@@ -517,12 +525,69 @@ func TestReconcileOnCancelledPipelineRun(t *testing.T) {
517525

518526
// Check that the PipelineRun was reconciled correctly
519527
reconciledRun, err := clients.Pipeline.Pipeline().PipelineRuns("foo").Get("test-pipeline-run-cancelled", metav1.GetOptions{})
520-
if err != nil {
521-
t.Fatalf("Somehow had error getting completed reconciled run out of fake client: %s", err)
522-
}
523528

524529
// This PipelineRun should still be complete and false, and the status should reflect that
525530
if !reconciledRun.Status.GetCondition(duckv1alpha1.ConditionSucceeded).IsFalse() {
526531
t.Errorf("Expected PipelineRun status to be complete and false, but was %v", reconciledRun.Status.GetCondition(duckv1alpha1.ConditionSucceeded))
527532
}
528533
}
534+
535+
func TestReconcileWithTimeout(t *testing.T) {
536+
ps := []*v1alpha1.Pipeline{tb.Pipeline("test-pipeline", "foo", tb.PipelineSpec(
537+
tb.PipelineTask("hello-world-1", "hello-world"),
538+
tb.PipelineTimeout(&metav1.Duration{Duration: 12 * time.Hour}),
539+
))}
540+
prs := []*v1alpha1.PipelineRun{tb.PipelineRun("test-pipeline-run-with-timeout", "foo",
541+
tb.PipelineRunSpec("test-pipeline",
542+
tb.PipelineRunServiceAccount("test-sa"),
543+
),
544+
tb.PipelineRunStatus(
545+
tb.PipelineRunStartTime(time.Now().AddDate(0, 0, -1))),
546+
)}
547+
ts := []*v1alpha1.Task{tb.Task("hello-world", "foo")}
548+
549+
d := test.Data{
550+
PipelineRuns: prs,
551+
Pipelines: ps,
552+
Tasks: ts,
553+
}
554+
555+
// create fake recorder for testing
556+
fr := record.NewFakeRecorder(2)
557+
558+
testAssets := getPipelineRunController(d, fr)
559+
c := testAssets.Controller
560+
clients := testAssets.Clients
561+
562+
err := c.Reconciler.Reconcile(context.Background(), "foo/test-pipeline-run-with-timeout")
563+
if err != nil {
564+
t.Errorf("Did not expect to see error when reconciling completed PipelineRun but saw %s", err)
565+
}
566+
567+
// Check that the PipelineRun was reconciled correctly
568+
reconciledRun, err := clients.Pipeline.Pipeline().PipelineRuns("foo").Get("test-pipeline-run-with-timeout", metav1.GetOptions{})
569+
if err != nil {
570+
t.Fatalf("Somehow had error getting completed reconciled run out of fake client: %s", err)
571+
}
572+
573+
// The PipelineRun should be timed out.
574+
if reconciledRun.Status.GetCondition(duckv1alpha1.ConditionSucceeded).Reason != resources.ReasonTimedOut {
575+
t.Errorf("Expected PipelineRun to be timed out, but condition reason is %s", reconciledRun.Status.GetCondition(duckv1alpha1.ConditionSucceeded))
576+
}
577+
578+
// Check that the expected TaskRun was created
579+
actual := clients.Pipeline.Actions()[0].(ktesting.CreateAction).GetObject().(*v1alpha1.TaskRun)
580+
if actual == nil {
581+
t.Errorf("Expected a TaskRun to be created, but it wasn't.")
582+
}
583+
584+
// There should be a time out for the TaskRun
585+
if actual.Spec.TaskSpec.Timeout == nil {
586+
t.Fatalf("TaskSpec.Timeout shouldn't be nil")
587+
}
588+
589+
// The TaskRun timeout should be less than or equal to the PipelineRun timeout.
590+
if actual.Spec.TaskSpec.Timeout.Duration > ps[0].Spec.Timeout.Duration {
591+
t.Errorf("TaskRun timeout %s should be less than or equal to PipelineRun timeout %s", actual.Spec.TaskSpec.Timeout.Duration.String(), ps[0].Spec.Timeout.Duration.String())
592+
}
593+
}

0 commit comments

Comments
 (0)