Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add CloudEvents for Run definitions #4659

Merged
merged 1 commit into from
Mar 14, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
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
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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just some grammar nits: controllers emits -> controllers emit and the underlying TaskRun do -> the underlying TaskRuns do

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice catch, thank you!

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`
Copy link
Member

@lbernick lbernick Mar 11, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what's the reason not to send a condition change while running event? is this because we don't know what the run's previous status was, and can't make assumptions based on what it currently is? Is this a feature users might want?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We cannot make assumptions about the Run and it will use its Conditions beyond what we specify in the custom task interface. So we can see that the resource has been created (start), we can see that an initial condition was set (running) and we can see when it's finished (successfully or not).

`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)
}

// eventForRun will create a new event based on a Run, or return an error if
// not possible.
func eventForRun(run *v1alpha1.Run) (*cloudevents.Event, error) {
afrittoli marked this conversation as resolved.
Show resolved Hide resolved
// Check if the Run 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
92 changes: 89 additions & 3 deletions pkg/reconciler/events/cloudevent/cloudevent_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,19 +20,21 @@ 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 +85,34 @@ func getPipelineRunByCondition(status corev1.ConditionStatus, reason string) *v1
}
}

func createRunWithCondition(status corev1.ConditionStatus, reason string) *v1alpha1.Run {
myrun := &v1alpha1.Run{
TypeMeta: metav1.TypeMeta{
Kind: "Run",
APIVersion: "v1alpha1",
},
ObjectMeta: metav1.ObjectMeta{
Name: runName,
Namespace: "marshmallow",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:)

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 @@ -121,7 +151,6 @@ func TestEventForTaskRun(t *testing.T) {

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

got, err := eventForTaskRun(c.taskRun)
if err != nil {
Expand Down Expand Up @@ -180,7 +209,6 @@ func TestEventForPipelineRun(t *testing.T) {

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

got, err := eventForPipelineRun(c.pipelineRun)
if err != nil {
Expand Down Expand Up @@ -209,3 +237,61 @@ 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: createRunWithCondition("", ""),
wantEventType: RunStartedEventV1,
}, {
desc: "send a cloud event with unknown status run, empty reason",
run: createRunWithCondition(corev1.ConditionUnknown, ""),
wantEventType: RunRunningEventV1,
}, {
desc: "send a cloud event with unknown status run, some reason set",
run: createRunWithCondition(corev1.ConditionUnknown, "custom controller reason"),
wantEventType: RunRunningEventV1,
}, {
desc: "send a cloud event with successful status run",
run: createRunWithCondition(corev1.ConditionTrue, "yay"),
wantEventType: RunSuccessfulEventV1,
}, {
desc: "send a cloud event with unknown status run",
run: createRunWithCondition(corev1.ConditionFalse, "meh"),
wantEventType: RunFailedEventV1,
}}

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

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)
}
}
})
}
}