Skip to content

Commit

Permalink
Add CloudEvents for Run definitions
Browse files Browse the repository at this point in the history
Add the definitions for new CloudEvents for Runs.
The events are not sent yet. The event types are documented and
the cloudevent package supports creating events for Runs.

Partially addresses #3862

Signed-off-by: Andrea Frittoli <[email protected]>
  • Loading branch information
afrittoli committed Mar 11, 2022
1 parent 4116bdd commit bfa77be
Show file tree
Hide file tree
Showing 3 changed files with 144 additions and 5 deletions.
13 changes: 10 additions & 3 deletions docs/events.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@ weight: 700
-->
# Events in Tekton

Tekton's task controller emits [Kubernetes events](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#event-v1-core)
Tekton's controllers emits [Kubernetes events](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#event-v1-core)
when `TaskRuns` and `PipelineRuns` execute. This allows you to monitor and react to what's happening during execution by
retrieving those events using the `kubectl describe` command. Tekton can also emit [CloudEvents](https://github.com/cloudevents/spec).

**Note:** `Conditions` [do not yet emit events](https://github.com/tektoncd/pipeline/issues/2461).
**Note:** `Conditions` [do not emit events](https://github.com/tektoncd/pipeline/issues/2461)
but the underlying `TaskRun` do.

## Events in `TaskRuns`

Expand Down Expand Up @@ -53,7 +54,7 @@ events as described in the table below.

Tekton sends cloud events in a parallel routine to allow for retries without blocking the
reconciler. A routine is started every time the `Succeeded` condition changes - either state,
reason or message. Retries are sent using an exponential back-off strategy.
reason or message. Retries are sent using an exponential back-off strategy.
Because of retries, events are not guaranteed to be sent to the target sink in the order they happened.

Resource |Event |Event Type
Expand All @@ -68,6 +69,12 @@ Resource |Event |Event Type
`PipelineRun` | `Condition Change while Running` | `dev.tekton.event.pipelinerun.unknown.v1`
`PipelineRun` | `Succeed` | `dev.tekton.event.pipelinerun.successful.v1`
`PipelineRun` | `Failed` | `dev.tekton.event.pipelinerun.failed.v1`
`Run` | `Started` | `dev.tekton.event.run.started.v1`
`Run` | `Running` | `dev.tekton.event.run.running.v1`
`Run` | `Succeed` | `dev.tekton.event.run.successful.v1`
`Run` | `Failed` | `dev.tekton.event.run.failed.v1`

`CloudEvents` for `Runs` are defined but not sent yet.

## Format of `CloudEvents`

Expand Down
46 changes: 44 additions & 2 deletions pkg/reconciler/events/cloudevent/cloudevent.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import (
"github.com/google/go-cmp/cmp/cmpopts"
"knative.dev/pkg/apis"

"github.com/tektoncd/pipeline/pkg/apis/pipeline/v1alpha1"
"github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1"
)

Expand Down Expand Up @@ -61,6 +62,16 @@ const (
PipelineRunSuccessfulEventV1 TektonEventType = "dev.tekton.event.pipelinerun.successful.v1"
// PipelineRunFailedEventV1 is sent for PipelineRuns with "ConditionSucceeded" "False"
PipelineRunFailedEventV1 TektonEventType = "dev.tekton.event.pipelinerun.failed.v1"
// RunStartedEventV1 is sent for Runs with "ConditionSucceeded" "Unknown"
// the first time they are picked up by the reconciler
RunStartedEventV1 TektonEventType = "dev.tekton.event.run.started.v1"
// RunRunningEventV1 is sent for Runs with "ConditionSucceeded" "Unknown"
// once the Run is validated and Pod created
RunRunningEventV1 TektonEventType = "dev.tekton.event.run.running.v1"
// RunSuccessfulEventV1 is sent for Runs with "ConditionSucceeded" "True"
RunSuccessfulEventV1 TektonEventType = "dev.tekton.event.run.successful.v1"
// RunFailedEventV1 is sent for Runs with "ConditionSucceeded" "False"
RunFailedEventV1 TektonEventType = "dev.tekton.event.run.failed.v1"
)

func (t TektonEventType) String() string {
Expand All @@ -75,6 +86,7 @@ type CEClient cloudevents.Client
type TektonCloudEventData struct {
TaskRun *v1beta1.TaskRun `json:"taskRun,omitempty"`
PipelineRun *v1beta1.PipelineRun `json:"pipelineRun,omitempty"`
Run *v1alpha1.Run `json:"run,omitempty"`
}

// newTektonCloudEventData returns a new instance of TektonCloudEventData
Expand All @@ -85,6 +97,8 @@ func newTektonCloudEventData(runObject objectWithCondition) TektonCloudEventData
tektonCloudEventData.TaskRun = v
case *v1beta1.PipelineRun:
tektonCloudEventData.PipelineRun = v
case *v1alpha1.Run:
tektonCloudEventData.Run = v
}
return tektonCloudEventData
}
Expand Down Expand Up @@ -142,12 +156,31 @@ func eventForPipelineRun(pipelineRun *v1beta1.PipelineRun) (*cloudevents.Event,
return eventForObjectWithCondition(pipelineRun)
}

// eventForPipelineRun will create a new event based on a PipelineRun,
// or return an error if not possible.
func eventForRun(run *v1alpha1.Run) (*cloudevents.Event, error) {
// Check if the TaskRun is defined
if run == nil {
return nil, errors.New("Cannot send an event for an empty Run")
}
return eventForObjectWithCondition(run)
}

func getEventType(runObject objectWithCondition) (*TektonEventType, error) {
var eventType TektonEventType
c := runObject.GetStatusCondition().GetCondition(apis.ConditionSucceeded)
if c == nil {
return nil, fmt.Errorf("no condition for ConditionSucceeded in %T", runObject)
// When the `Run` is created, it may not have any condition until it's
// picked up by the `Run` reconciler. In that case we consider the run
// as started. In all other cases, conditions have to be initialised
switch runObject.(type) {
case *v1alpha1.Run:
eventType = RunStartedEventV1
return &eventType, nil
default:
return nil, fmt.Errorf("no condition for ConditionSucceeded in %T", runObject)
}
}
var eventType TektonEventType
switch {
case c.IsUnknown():
switch runObject.(type) {
Expand All @@ -169,20 +202,29 @@ func getEventType(runObject objectWithCondition) (*TektonEventType, error) {
default:
eventType = PipelineRunUnknownEventV1
}
case *v1alpha1.Run:
// Run controller have the freedom of setting reasons as they wish
// so we cannot make many assumptions here. If a condition is set
// to unknown (not finished), we sent the running event
eventType = RunRunningEventV1
}
case c.IsFalse():
switch runObject.(type) {
case *v1beta1.TaskRun:
eventType = TaskRunFailedEventV1
case *v1beta1.PipelineRun:
eventType = PipelineRunFailedEventV1
case *v1alpha1.Run:
eventType = RunFailedEventV1
}
case c.IsTrue():
switch runObject.(type) {
case *v1beta1.TaskRun:
eventType = TaskRunSuccessfulEventV1
case *v1beta1.PipelineRun:
eventType = PipelineRunSuccessfulEventV1
case *v1alpha1.Run:
eventType = RunSuccessfulEventV1
}
default:
return nil, fmt.Errorf("unknown condition for in %T.Status %s", runObject, c.Status)
Expand Down
90 changes: 90 additions & 0 deletions pkg/reconciler/events/cloudevent/cloudevent_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,19 +20,22 @@ import (
"testing"

"github.com/google/go-cmp/cmp"
"github.com/tektoncd/pipeline/pkg/apis/pipeline/v1alpha1"
"github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1"
"github.com/tektoncd/pipeline/test/diff"
"github.com/tektoncd/pipeline/test/names"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"knative.dev/pkg/apis"
duckv1 "knative.dev/pkg/apis/duck/v1"
duckv1beta1 "knative.dev/pkg/apis/duck/v1beta1"
)

const (
defaultEventSourceURI = "/runtocompletion/1234"
taskRunName = "faketaskrunname"
pipelineRunName = "fakepipelinerunname"
runName = "fakerunname"
)

func getTaskRunByCondition(status corev1.ConditionStatus, reason string) *v1beta1.TaskRun {
Expand Down Expand Up @@ -83,6 +86,34 @@ func getPipelineRunByCondition(status corev1.ConditionStatus, reason string) *v1
}
}

func getRunByCondition(status corev1.ConditionStatus, reason string) *v1alpha1.Run {
myrun := &v1alpha1.Run{
TypeMeta: metav1.TypeMeta{
Kind: "Run",
APIVersion: "v1alpha1",
},
ObjectMeta: metav1.ObjectMeta{
Name: runName,
Namespace: "marshmallow",
SelfLink: defaultEventSourceURI,
},
Spec: v1alpha1.RunSpec{},
}
switch status {
case corev1.ConditionFalse, corev1.ConditionUnknown, corev1.ConditionTrue:
myrun.Status = v1alpha1.RunStatus{
Status: duckv1.Status{
Conditions: []apis.Condition{{
Type: apis.ConditionSucceeded,
Status: status,
Reason: reason,
}},
},
}
}
return myrun
}

func TestEventForTaskRun(t *testing.T) {
taskRunTests := []struct {
desc string
Expand Down Expand Up @@ -209,3 +240,62 @@ func TestEventForPipelineRun(t *testing.T) {
})
}
}

func TestEventForRun(t *testing.T) {
runTests := []struct {
desc string
run *v1alpha1.Run
wantEventType TektonEventType
}{{
desc: "send a cloud event with unset condition, just started",
run: getRunByCondition("", ""),
wantEventType: RunStartedEventV1,
}, {
desc: "send a cloud event with unknown status pipelinerun, empty reason",
run: getRunByCondition(corev1.ConditionUnknown, ""),
wantEventType: RunRunningEventV1,
}, {
desc: "send a cloud event with unknown status pipelinerun, some reason set",
run: getRunByCondition(corev1.ConditionUnknown, "custom controller reason"),
wantEventType: RunRunningEventV1,
}, {
desc: "send a cloud event with successful status pipelinerun",
run: getRunByCondition(corev1.ConditionTrue, "yay"),
wantEventType: RunSuccessfulEventV1,
}, {
desc: "send a cloud event with unknown status pipelinerun",
run: getRunByCondition(corev1.ConditionFalse, "meh"),
wantEventType: RunFailedEventV1,
}}

for _, c := range runTests {
t.Run(c.desc, func(t *testing.T) {
names.TestingSeed()

got, err := eventForRun(c.run)
if err != nil {
t.Fatalf("I did not expect an error but I got %s", err)
} else {
wantSubject := runName
if d := cmp.Diff(wantSubject, got.Subject()); d != "" {
t.Errorf("Wrong Event ID %s", diff.PrintWantGot(d))
}
if d := cmp.Diff(string(c.wantEventType), got.Type()); d != "" {
t.Errorf("Wrong Event Type %s", diff.PrintWantGot(d))
}
wantData := newTektonCloudEventData(c.run)
gotData := TektonCloudEventData{}
if err := got.DataAs(&gotData); err != nil {
t.Errorf("Unexpected error from DataAsl; %s", err)
}
if d := cmp.Diff(wantData, gotData); d != "" {
t.Errorf("Wrong Event data %s", diff.PrintWantGot(d))
}

if err := got.Validate(); err != nil {
t.Errorf("Expected event to be valid; %s", err)
}
}
})
}
}

0 comments on commit bfa77be

Please sign in to comment.