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