From 6fea4183c988222b05bdf26201f66e38f5db6a01 Mon Sep 17 00:00:00 2001 From: Andrea Frittoli Date: Thu, 10 Mar 2022 11:34:41 +0000 Subject: [PATCH] Add CloudEvents for Run definitions 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 --- docs/events.md | 13 ++- .../events/cloudevent/cloudevent.go | 46 +++++++++- .../events/cloudevent/cloudevent_test.go | 92 ++++++++++++++++++- 3 files changed, 143 insertions(+), 8 deletions(-) diff --git a/docs/events.md b/docs/events.md index 40b9cc8fd3d..adc16c9e83d 100644 --- a/docs/events.md +++ b/docs/events.md @@ -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` @@ -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 @@ -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` diff --git a/pkg/reconciler/events/cloudevent/cloudevent.go b/pkg/reconciler/events/cloudevent/cloudevent.go index 1982dd72720..997391e7fe7 100644 --- a/pkg/reconciler/events/cloudevent/cloudevent.go +++ b/pkg/reconciler/events/cloudevent/cloudevent.go @@ -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" ) @@ -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 { @@ -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 @@ -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 } @@ -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) { + // 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) { @@ -169,6 +202,11 @@ 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) { @@ -176,6 +214,8 @@ func getEventType(runObject objectWithCondition) (*TektonEventType, error) { eventType = TaskRunFailedEventV1 case *v1beta1.PipelineRun: eventType = PipelineRunFailedEventV1 + case *v1alpha1.Run: + eventType = RunFailedEventV1 } case c.IsTrue(): switch runObject.(type) { @@ -183,6 +223,8 @@ func getEventType(runObject objectWithCondition) (*TektonEventType, error) { 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) diff --git a/pkg/reconciler/events/cloudevent/cloudevent_test.go b/pkg/reconciler/events/cloudevent/cloudevent_test.go index 610285d4efe..cda155a5268 100644 --- a/pkg/reconciler/events/cloudevent/cloudevent_test.go +++ b/pkg/reconciler/events/cloudevent/cloudevent_test.go @@ -20,12 +20,13 @@ 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" ) @@ -33,6 +34,7 @@ const ( defaultEventSourceURI = "/runtocompletion/1234" taskRunName = "faketaskrunname" pipelineRunName = "fakepipelinerunname" + runName = "fakerunname" ) func getTaskRunByCondition(status corev1.ConditionStatus, reason string) *v1beta1.TaskRun { @@ -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", + 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 @@ -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 { @@ -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 { @@ -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) + } + } + }) + } +}