diff --git a/cmd/events/main.go b/cmd/events/main.go
index 8636eed1663..7b4a20e9712 100644
--- a/cmd/events/main.go
+++ b/cmd/events/main.go
@@ -47,7 +47,8 @@ func main() {
}()
// start the events controller
- sharedmain.Main(eventsControllerName, customrun.NewController())
+ sharedmain.Main(eventsControllerName,
+ customrun.NewController())
}
func handler(w http.ResponseWriter, r *http.Request) {
diff --git a/docs/pipeline-api.md b/docs/pipeline-api.md
index 4f7bba7bbea..74573b4464f 100644
--- a/docs/pipeline-api.md
+++ b/docs/pipeline-api.md
@@ -11445,7 +11445,12 @@ this ResultsType.
RunObject
-
RunObject is implemented by CustomRun and Run
+
RunObject is implemented by Run, CustomRun, TaskRun and PipelineRun
+
+RunObjectWithRetries
+
+
+
RunObject is implemented by Run and CustomRun
Sidecar
diff --git a/pkg/apis/config/store.go b/pkg/apis/config/store.go
index 76307196936..75e40d85fe9 100644
--- a/pkg/apis/config/store.go
+++ b/pkg/apis/config/store.go
@@ -76,7 +76,7 @@ type Store struct {
func NewStore(logger configmap.Logger, onAfterStore ...func(name string, value interface{})) *Store {
store := &Store{
UntypedStore: configmap.NewUntypedStore(
- "defaults/features/artifacts",
+ "defaults/features/metrics/spire/events",
logger,
configmap.Constructors{
GetDefaultsConfigName(): NewDefaultsFromConfigMap,
diff --git a/pkg/apis/pipeline/v1/pipelinerun_types.go b/pkg/apis/pipeline/v1/pipelinerun_types.go
index 88120472ea8..2cfddefef0a 100644
--- a/pkg/apis/pipeline/v1/pipelinerun_types.go
+++ b/pkg/apis/pipeline/v1/pipelinerun_types.go
@@ -83,6 +83,11 @@ func (pr *PipelineRun) HasStarted() bool {
return pr.Status.StartTime != nil && !pr.Status.StartTime.IsZero()
}
+// IsSuccessful returns true if the TaskRun's status indicates that it has succeeded.
+func (tr *PipelineRun) IsSuccessful() bool {
+ return tr != nil && tr.Status.GetCondition(apis.ConditionSucceeded).IsTrue()
+}
+
// IsCancelled returns true if the PipelineRun's spec status is set to Cancelled state
func (pr *PipelineRun) IsCancelled() bool {
return pr.Spec.Status == PipelineRunSpecStatusCancelled
diff --git a/pkg/apis/pipeline/v1beta1/pipelinerun_types.go b/pkg/apis/pipeline/v1beta1/pipelinerun_types.go
index 53c1f738c0f..766530c821a 100644
--- a/pkg/apis/pipeline/v1beta1/pipelinerun_types.go
+++ b/pkg/apis/pipeline/v1beta1/pipelinerun_types.go
@@ -82,6 +82,11 @@ func (pr *PipelineRun) HasStarted() bool {
return pr.Status.StartTime != nil && !pr.Status.StartTime.IsZero()
}
+// IsSuccessful returns true if the TaskRun's status indicates that it has succeeded.
+func (tr *PipelineRun) IsSuccessful() bool {
+ return tr != nil && tr.Status.GetCondition(apis.ConditionSucceeded).IsTrue()
+}
+
// IsCancelled returns true if the PipelineRun's spec status is set to Cancelled state
func (pr *PipelineRun) IsCancelled() bool {
return pr.Spec.Status == PipelineRunSpecStatusCancelled
diff --git a/pkg/apis/pipeline/v1beta1/run_interface.go b/pkg/apis/pipeline/v1beta1/run_interface.go
index 2a0ada19d68..107affe304e 100644
--- a/pkg/apis/pipeline/v1beta1/run_interface.go
+++ b/pkg/apis/pipeline/v1beta1/run_interface.go
@@ -23,7 +23,7 @@ import (
"knative.dev/pkg/apis"
)
-// RunObject is implemented by CustomRun and Run
+// RunObject is implemented by Run, CustomRun, TaskRun and PipelineRun
type RunObject interface {
// Object requires GetObjectKind() and DeepCopyObject()
runtime.Object
@@ -38,6 +38,11 @@ type RunObject interface {
IsCancelled() bool
HasStarted() bool
IsDone() bool
+}
+
+// RunObject is implemented by Run and CustomRun
+type RunObjectWithRetries interface {
+ RunObject
GetRetryCount() int
}
diff --git a/pkg/reconciler/notifications/controller.go b/pkg/reconciler/notifications/controller.go
new file mode 100644
index 00000000000..f1fce17f520
--- /dev/null
+++ b/pkg/reconciler/notifications/controller.go
@@ -0,0 +1,53 @@
+/*
+Copyright 2023 The Tekton Authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package notifications
+
+import (
+ "context"
+
+ "github.com/tektoncd/pipeline/pkg/apis/config"
+ cacheclient "github.com/tektoncd/pipeline/pkg/reconciler/events/cache"
+ cloudeventclient "github.com/tektoncd/pipeline/pkg/reconciler/events/cloudevent"
+ "knative.dev/pkg/configmap"
+ "knative.dev/pkg/controller"
+ "knative.dev/pkg/logging"
+)
+
+// ConfigStoreFromContext initialise the config store from the context
+func ConfigStoreFromContext(ctx context.Context, cmw configmap.Watcher) *config.Store {
+ logger := logging.FromContext(ctx)
+ configStore := config.NewStore(logger.Named("config-store"))
+ configStore.WatchConfigs(cmw)
+ return configStore
+}
+
+// ReconcilerFromContext initialises a Reconciler from the context
+func ReconcilerFromContext(ctx context.Context, c Reconciler) {
+ c.SetCloudEventsClient(cloudeventclient.Get(ctx))
+ c.SetCacheClient(cacheclient.Get(ctx))
+}
+
+// ControllerOptions returns a function that returns options for a controller implementation
+func ControllerOptions(name string, store *config.Store) func(impl *controller.Impl) controller.Options {
+ return func(impl *controller.Impl) controller.Options {
+ return controller.Options{
+ AgentName: name,
+ ConfigStore: store,
+ SkipStatusUpdates: true,
+ }
+ }
+}
diff --git a/pkg/reconciler/notifications/customrun/controller.go b/pkg/reconciler/notifications/customrun/controller.go
index 5e198c9cf54..35cb11a5780 100644
--- a/pkg/reconciler/notifications/customrun/controller.go
+++ b/pkg/reconciler/notifications/customrun/controller.go
@@ -19,39 +19,27 @@ package customrun
import (
"context"
- "github.com/tektoncd/pipeline/pkg/apis/config"
- "github.com/tektoncd/pipeline/pkg/apis/pipeline"
customruninformer "github.com/tektoncd/pipeline/pkg/client/injection/informers/pipeline/v1beta1/customrun"
customrunreconciler "github.com/tektoncd/pipeline/pkg/client/injection/reconciler/pipeline/v1beta1/customrun"
- cacheclient "github.com/tektoncd/pipeline/pkg/reconciler/events/cache"
- cloudeventclient "github.com/tektoncd/pipeline/pkg/reconciler/events/cloudevent"
+ "github.com/tektoncd/pipeline/pkg/reconciler/notifications"
"knative.dev/pkg/configmap"
"knative.dev/pkg/controller"
- "knative.dev/pkg/logging"
)
+const ControllerName = "CustomRunEvents"
+
// NewController instantiates a new controller.Impl from knative.dev/pkg/controller
// This is a read-only controller, hence the SkipStatusUpdates set to true
func NewController() func(context.Context, configmap.Watcher) *controller.Impl {
return func(ctx context.Context, cmw configmap.Watcher) *controller.Impl {
- logger := logging.FromContext(ctx)
- customRunInformer := customruninformer.Get(ctx)
+ configStore := notifications.ConfigStoreFromContext(ctx, cmw)
- configStore := config.NewStore(logger.Named("config-store"))
- configStore.WatchConfigs(cmw)
-
- c := &Reconciler{
- cloudEventClient: cloudeventclient.Get(ctx),
- cacheClient: cacheclient.Get(ctx),
- }
- impl := customrunreconciler.NewImpl(ctx, c, func(impl *controller.Impl) controller.Options {
- return controller.Options{
- AgentName: pipeline.CustomRunControllerName,
- ConfigStore: configStore,
- SkipStatusUpdates: true,
- }
- })
+ c := &Reconciler{}
+ notifications.ReconcilerFromContext(ctx, c)
+ impl := customrunreconciler.NewImpl(ctx, c, notifications.ControllerOptions(ControllerName, configStore))
+
+ customRunInformer := customruninformer.Get(ctx)
customRunInformer.Informer().AddEventHandler(controller.HandleAll(impl.Enqueue))
return impl
diff --git a/pkg/reconciler/notifications/customrun/controller_test.go b/pkg/reconciler/notifications/customrun/controller_test.go
new file mode 100644
index 00000000000..4b69afbec14
--- /dev/null
+++ b/pkg/reconciler/notifications/customrun/controller_test.go
@@ -0,0 +1,141 @@
+/*
+Copyright 2019 The Tekton Authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package customrun_test
+
+import (
+ "testing"
+
+ "github.com/google/go-cmp/cmp"
+ "github.com/google/go-cmp/cmp/cmpopts"
+ "github.com/tektoncd/pipeline/pkg/apis/config"
+ "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1"
+ "github.com/tektoncd/pipeline/pkg/reconciler/events/cloudevent"
+ "github.com/tektoncd/pipeline/pkg/reconciler/notifications/customrun"
+ ntesting "github.com/tektoncd/pipeline/pkg/reconciler/notifications/testing"
+ "github.com/tektoncd/pipeline/test"
+ corev1 "k8s.io/api/core/v1"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/types"
+ "knative.dev/pkg/apis"
+ duckv1 "knative.dev/pkg/apis/duck/v1"
+ cminformer "knative.dev/pkg/configmap/informer"
+ pkgreconciler "knative.dev/pkg/reconciler"
+ "knative.dev/pkg/system"
+ _ "knative.dev/pkg/system/testing" // Setup system.Namespace()
+)
+
+func InitializeTestController(t *testing.T, d test.Data, a test.Assets) test.Assets {
+ t.Helper()
+ configMapWatcher := cminformer.NewInformedWatcher(a.Clients.Kube, system.Namespace())
+ ctl := customrun.NewController()(a.Ctx, configMapWatcher)
+ if err := configMapWatcher.Start(a.Ctx.Done()); err != nil {
+ t.Fatalf("error starting configmap watcher: %v", err)
+ }
+
+ if la, ok := ctl.Reconciler.(pkgreconciler.LeaderAware); ok {
+ la.Promote(pkgreconciler.UniversalBucket(), func(pkgreconciler.Bucket, types.NamespacedName) {})
+ }
+ a.Controller = ctl
+ return a
+}
+
+// TestReconcileNewController runs reconcile with a cloud event sink configured
+// to ensure that events are sent in different cases
+func TestReconcileNewController(t *testing.T) {
+ ignoreResourceVersion := cmpopts.IgnoreFields(v1beta1.CustomRun{}, "ObjectMeta.ResourceVersion")
+
+ cms := []*corev1.ConfigMap{
+ {
+ ObjectMeta: metav1.ObjectMeta{Name: config.GetEventsConfigName(), Namespace: system.Namespace()},
+ Data: map[string]string{
+ "sink": "http://synk:8080",
+ },
+ }, {
+ ObjectMeta: metav1.ObjectMeta{Name: config.GetFeatureFlagsConfigName(), Namespace: system.Namespace()},
+ Data: map[string]string{
+ "send-cloudevents-for-runs": "true",
+ },
+ },
+ }
+
+ condition := &apis.Condition{
+ Type: apis.ConditionSucceeded,
+ Status: corev1.ConditionTrue,
+ Reason: v1beta1.CustomRunReasonSuccessful.String(),
+ }
+ objectStatus := duckv1.Status{
+ Conditions: []apis.Condition{},
+ }
+ crStatusFields := v1beta1.CustomRunStatusFields{}
+ objectStatus.Conditions = append(objectStatus.Conditions, *condition)
+ customRun := v1beta1.CustomRun{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test-customRun",
+ Namespace: "foo",
+ },
+ Spec: v1beta1.CustomRunSpec{},
+ Status: v1beta1.CustomRunStatus{
+ Status: objectStatus,
+ CustomRunStatusFields: crStatusFields,
+ },
+ }
+ customRuns := []*v1beta1.CustomRun{&customRun}
+ wantCloudEvents := []string{`(?s)dev.tekton.event.customrun.successful.v1.*test-customRun`}
+
+ d := test.Data{
+ CustomRuns: customRuns,
+ ConfigMaps: cms,
+ ExpectedCloudEventCount: len(wantCloudEvents),
+ }
+ testAssets, cancel := ntesting.InitializeTestAssets(t, &d)
+ defer cancel()
+ clients := testAssets.Clients
+
+ // Initialise the controller.
+ // Verify that the config map watcher and reconciler setup works well
+ testAssets = InitializeTestController(t, d, testAssets)
+ c := testAssets.Controller
+
+ if err := c.Reconciler.Reconcile(testAssets.Ctx, ntesting.GetTestResourceName(&customRun)); err != nil {
+ t.Errorf("didn't expect an error, but got one: %v", err)
+ }
+
+ for _, a := range clients.Kube.Actions() {
+ aVerb := a.GetVerb()
+ if aVerb != "get" && aVerb != "list" && aVerb != "watch" {
+ t.Errorf("Expected only read actions to be logged in the kubeclient, got %s", aVerb)
+ }
+ }
+
+ crAfter, err := clients.Pipeline.TektonV1beta1().CustomRuns(customRun.Namespace).Get(testAssets.Ctx, customRun.Name, metav1.GetOptions{})
+ if err != nil {
+ t.Fatalf("getting updated customRun: %v", err)
+ }
+
+ if d := cmp.Diff(&customRun, crAfter, ignoreResourceVersion); d != "" {
+ t.Fatalf("CustomRun should not have changed, got %v instead", d)
+ }
+
+ ceClient := clients.CloudEvents.(cloudevent.FakeClient)
+ ceClient.CheckCloudEventsUnordered(t, "controller test", wantCloudEvents)
+
+ // Try and reconcile again - expect no event
+ if err := c.Reconciler.Reconcile(testAssets.Ctx, ntesting.GetTestResourceName(&customRun)); err != nil {
+ t.Errorf("didn't expect an error, but got one: %v", err)
+ }
+ ceClient.CheckCloudEventsUnordered(t, "controller test", []string{})
+}
diff --git a/pkg/reconciler/notifications/customrun/customrun.go b/pkg/reconciler/notifications/customrun/reconciler.go
similarity index 65%
rename from pkg/reconciler/notifications/customrun/customrun.go
rename to pkg/reconciler/notifications/customrun/reconciler.go
index 9ec495f5855..7290e2aa44d 100644
--- a/pkg/reconciler/notifications/customrun/customrun.go
+++ b/pkg/reconciler/notifications/customrun/reconciler.go
@@ -23,11 +23,8 @@ import (
"github.com/tektoncd/pipeline/pkg/apis/config"
"github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1"
customrunreconciler "github.com/tektoncd/pipeline/pkg/client/injection/reconciler/pipeline/v1beta1/customrun"
- "github.com/tektoncd/pipeline/pkg/reconciler/events"
- "github.com/tektoncd/pipeline/pkg/reconciler/events/cache"
"github.com/tektoncd/pipeline/pkg/reconciler/events/cloudevent"
- "knative.dev/pkg/apis"
- "knative.dev/pkg/logging"
+ "github.com/tektoncd/pipeline/pkg/reconciler/notifications"
pkgreconciler "knative.dev/pkg/reconciler"
)
@@ -37,35 +34,36 @@ type Reconciler struct {
cacheClient *lru.Cache
}
+func (c *Reconciler) GetCloudEventsClient() cloudevent.CEClient {
+ return c.cloudEventClient
+}
+
+func (c *Reconciler) GetCacheClient() *lru.Cache {
+ return c.cacheClient
+}
+
+func (c *Reconciler) SetCloudEventsClient(client cloudevent.CEClient) {
+ c.cloudEventClient = client
+}
+
+func (c *Reconciler) SetCacheClient(client *lru.Cache) {
+ c.cacheClient = client
+}
+
// Check that our Reconciler implements customrunreconciler.Interface
var (
_ customrunreconciler.Interface = (*Reconciler)(nil)
)
-// ReconcileKind compares the actual state with the desired, and attempts to
-// converge the two. It then updates the Status block of the CustomRun
-// resource with the current status of the resource.
+// ReconcileKind oberves the resource conditions and triggers notifications accordingly
func (c *Reconciler) ReconcileKind(ctx context.Context, customRun *v1beta1.CustomRun) pkgreconciler.Event {
- logger := logging.FromContext(ctx)
configs := config.FromContextOrDefaults(ctx)
- ctx = cloudevent.ToContext(ctx, c.cloudEventClient)
- ctx = cache.ToContext(ctx, c.cacheClient)
- logger.Infof("Reconciling %s", customRun.Name)
-
- // Create a copy of the CustomRun object, just in case, to avoid sync'ing changes
- customRunEvents := *customRun.DeepCopy()
-
if configs.FeatureFlags.SendCloudEventsForRuns {
// Custom task controllers may be sending events for "CustomRuns" associated
// to the custom tasks they control. To avoid sending duplicate events,
// CloudEvents for "CustomRuns" are only sent when enabled
- // Read and log the condition
- condition := customRunEvents.Status.GetCondition(apis.ConditionSucceeded)
- logger.Debugf("Emitting cloudevent for %s, condition: %s", customRunEvents.Name, condition)
-
- events.EmitCloudEvents(ctx, &customRunEvents)
+ return notifications.ReconcileRuntimeObject(ctx, c, customRun)
}
-
return nil
}
diff --git a/pkg/reconciler/notifications/customrun/customrun_test.go b/pkg/reconciler/notifications/customrun/reconciler_test.go
similarity index 60%
rename from pkg/reconciler/notifications/customrun/customrun_test.go
rename to pkg/reconciler/notifications/customrun/reconciler_test.go
index 38333e0deac..fd1af60c78a 100644
--- a/pkg/reconciler/notifications/customrun/customrun_test.go
+++ b/pkg/reconciler/notifications/customrun/reconciler_test.go
@@ -17,8 +17,6 @@ limitations under the License.
package customrun_test
import (
- "context"
- "strings"
"testing"
"github.com/google/go-cmp/cmp"
@@ -26,73 +24,27 @@ import (
"github.com/tektoncd/pipeline/pkg/apis/config"
"github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1"
"github.com/tektoncd/pipeline/pkg/reconciler/events/cloudevent"
- "github.com/tektoncd/pipeline/pkg/reconciler/notifications/customrun"
- ttesting "github.com/tektoncd/pipeline/pkg/reconciler/testing"
+ "github.com/tektoncd/pipeline/pkg/reconciler/notifications"
+ ntesting "github.com/tektoncd/pipeline/pkg/reconciler/notifications/testing"
"github.com/tektoncd/pipeline/test"
- "github.com/tektoncd/pipeline/test/names"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
- "k8s.io/apimachinery/pkg/types"
- "k8s.io/client-go/tools/record"
"knative.dev/pkg/apis"
duckv1 "knative.dev/pkg/apis/duck/v1"
- cminformer "knative.dev/pkg/configmap/informer"
- "knative.dev/pkg/controller"
- "knative.dev/pkg/logging"
- pkgreconciler "knative.dev/pkg/reconciler"
"knative.dev/pkg/system"
- _ "knative.dev/pkg/system/testing" // Setup system.Namespace()
+ // _ "knative.dev/pkg/system/testing" // Setup system.Namespace()
)
-func initializeCustomRunControllerAssets(t *testing.T, d test.Data) (test.Assets, func()) {
- t.Helper()
- ctx, _ := ttesting.SetupFakeContext(t)
- ctx = ttesting.SetupFakeCloudClientContext(ctx, d.ExpectedCloudEventCount)
- ctx, cancel := context.WithCancel(ctx)
- test.EnsureConfigurationConfigMapsExist(&d)
- c, informers := test.SeedTestData(t, ctx, d)
- configMapWatcher := cminformer.NewInformedWatcher(c.Kube, system.Namespace())
- ctl := customrun.NewController()(ctx, configMapWatcher)
- if err := configMapWatcher.Start(ctx.Done()); err != nil {
- t.Fatalf("error starting configmap watcher: %v", err)
- }
-
- if la, ok := ctl.Reconciler.(pkgreconciler.LeaderAware); ok {
- la.Promote(pkgreconciler.UniversalBucket(), func(pkgreconciler.Bucket, types.NamespacedName) {})
- }
-
- return test.Assets{
- Logger: logging.FromContext(ctx),
- Controller: ctl,
- Clients: c,
- Informers: informers,
- Recorder: controller.GetEventRecorder(ctx).(*record.FakeRecorder),
- Ctx: ctx,
- }, cancel
-}
-
-func getCustomRunName(customRun v1beta1.CustomRun) string {
- return strings.Join([]string{customRun.Namespace, customRun.Name}, "/")
-}
-
-// getCustomRunController returns an instance of the CustomRun controller/reconciler that has been seeded with
-// d, where d represents the state of the system (existing resources) needed for the test.
-func getCustomRunController(t *testing.T, d test.Data) (test.Assets, func()) {
- t.Helper()
- names.TestingSeed()
- return initializeCustomRunControllerAssets(t, d)
-}
-
-// TestReconcile_CloudEvents runs reconcile with a cloud event sink configured
+// TestReconcileKind_CloudEvents runs reconcile with a cloud event sink configured
// to ensure that events are sent in different cases
-func TestReconcile_CloudEvents(t *testing.T) {
+func TestReconcileKind_CloudEvents(t *testing.T) {
ignoreResourceVersion := cmpopts.IgnoreFields(v1beta1.CustomRun{}, "ObjectMeta.ResourceVersion")
cms := []*corev1.ConfigMap{
{
- ObjectMeta: metav1.ObjectMeta{Name: config.GetDefaultsConfigName(), Namespace: system.Namespace()},
+ ObjectMeta: metav1.ObjectMeta{Name: config.GetEventsConfigName(), Namespace: system.Namespace()},
Data: map[string]string{
- "default-cloud-events-sink": "http://synk:8080",
+ "sink": "http://synk:8080",
},
}, {
ObjectMeta: metav1.ObjectMeta{Name: config.GetFeatureFlagsConfigName(), Namespace: system.Namespace()},
@@ -101,6 +53,7 @@ func TestReconcile_CloudEvents(t *testing.T) {
},
},
}
+
testcases := []struct {
name string
condition *apis.Condition
@@ -162,13 +115,14 @@ func TestReconcile_CloudEvents(t *testing.T) {
ConfigMaps: cms,
ExpectedCloudEventCount: len(tc.wantCloudEvents),
}
- testAssets, cancel := getCustomRunController(t, d)
+ testAssets, cancel := ntesting.InitializeTestAssets(t, &d)
defer cancel()
- c := testAssets.Controller
clients := testAssets.Clients
+ reconciler := &ntesting.FakeReconciler{}
+ notifications.ReconcilerFromContext(testAssets.Ctx, reconciler)
- if err := c.Reconciler.Reconcile(testAssets.Ctx, getCustomRunName(customRun)); err != nil {
- t.Fatal("Didn't expect an error, but got one.")
+ if err := notifications.ReconcileRuntimeObject(testAssets.Ctx, reconciler, &customRun); err != nil {
+ t.Errorf("didn't expect an error, but got one: %v", err)
}
for _, a := range clients.Kube.Actions() {
@@ -178,21 +132,21 @@ func TestReconcile_CloudEvents(t *testing.T) {
}
}
- updatedCR, err := clients.Pipeline.TektonV1beta1().CustomRuns(customRun.Namespace).Get(testAssets.Ctx, customRun.Name, metav1.GetOptions{})
+ crAfter, err := clients.Pipeline.TektonV1beta1().CustomRuns(customRun.Namespace).Get(testAssets.Ctx, customRun.Name, metav1.GetOptions{})
if err != nil {
- t.Fatalf("getting updated customRun: %v", err)
+ t.Errorf("getting updated customRun: %v", err)
}
- if d := cmp.Diff(&customRun, updatedCR, ignoreResourceVersion); d != "" {
- t.Fatalf("CustomRun should not have changed, got %v instead", d)
+ if d := cmp.Diff(&customRun, crAfter, ignoreResourceVersion); d != "" {
+ t.Errorf("CustomRun should not have changed, got %v instead", d)
}
ceClient := clients.CloudEvents.(cloudevent.FakeClient)
ceClient.CheckCloudEventsUnordered(t, tc.name, tc.wantCloudEvents)
// Try and reconcile again - expect no event
- if err := c.Reconciler.Reconcile(testAssets.Ctx, getCustomRunName(customRun)); err != nil {
- t.Fatal("Didn't expect an error, but got one.")
+ if err := notifications.ReconcileRuntimeObject(testAssets.Ctx, reconciler, &customRun); err != nil {
+ t.Errorf("didn't expect an error, but got one: %v", err)
}
ceClient.CheckCloudEventsUnordered(t, tc.name, []string{})
})
@@ -201,15 +155,15 @@ func TestReconcile_CloudEvents(t *testing.T) {
func TestReconcile_CloudEvents_Disabled(t *testing.T) {
cmSinkOn := &corev1.ConfigMap{
- ObjectMeta: metav1.ObjectMeta{Name: config.GetDefaultsConfigName(), Namespace: system.Namespace()},
+ ObjectMeta: metav1.ObjectMeta{Name: config.GetEventsConfigName(), Namespace: system.Namespace()},
Data: map[string]string{
- "default-cloud-events-sink": "http://synk:8080",
+ "sink": "http://synk:8080",
},
}
cmSinkOff := &corev1.ConfigMap{
- ObjectMeta: metav1.ObjectMeta{Name: config.GetDefaultsConfigName(), Namespace: system.Namespace()},
+ ObjectMeta: metav1.ObjectMeta{Name: config.GetEventsConfigName(), Namespace: system.Namespace()},
Data: map[string]string{
- "default-cloud-events-sink": "",
+ "sink": "",
},
}
cmRunsOn := &corev1.ConfigMap{
@@ -224,7 +178,7 @@ func TestReconcile_CloudEvents_Disabled(t *testing.T) {
"send-cloudevents-for-runs": "false",
},
}
- testcases := []struct {
+ for _, tc := range []struct {
name string
cms []*corev1.ConfigMap
}{{
@@ -236,9 +190,7 @@ func TestReconcile_CloudEvents_Disabled(t *testing.T) {
}, {
name: "CustomRuns Disabled",
cms: []*corev1.ConfigMap{cmSinkOn, cmRunsOff},
- }}
-
- for _, tc := range testcases {
+ }} {
t.Run(tc.name, func(t *testing.T) {
objectStatus := duckv1.Status{
Conditions: []apis.Condition{
@@ -267,24 +219,22 @@ func TestReconcile_CloudEvents_Disabled(t *testing.T) {
CustomRuns: customRuns,
ConfigMaps: tc.cms,
}
- testAssets, cancel := getCustomRunController(t, d)
+ testAssets, cancel := ntesting.InitializeTestAssets(t, &d)
defer cancel()
- c := testAssets.Controller
clients := testAssets.Clients
+ reconciler := &ntesting.FakeReconciler{}
+ notifications.ReconcilerFromContext(testAssets.Ctx, reconciler)
- if err := c.Reconciler.Reconcile(testAssets.Ctx, getCustomRunName(customRun)); err != nil {
- t.Fatal("Didn't expect an error, but got one.")
- }
- if len(clients.Kube.Actions()) == 0 {
- t.Errorf("Expected actions to be logged in the kubeclient, got none")
+ if err := notifications.ReconcileRuntimeObject(testAssets.Ctx, reconciler, &customRun); err != nil {
+ t.Fatalf("didn't expect an error, but got one: %v", err)
}
- updatedCR, err := clients.Pipeline.TektonV1beta1().CustomRuns(customRun.Namespace).Get(testAssets.Ctx, customRun.Name, metav1.GetOptions{})
+ crAfter, err := clients.Pipeline.TektonV1beta1().CustomRuns(customRun.Namespace).Get(testAssets.Ctx, customRun.Name, metav1.GetOptions{})
if err != nil {
t.Fatalf("getting updated customRun: %v", err)
}
- if d := cmp.Diff(customRun.Status, updatedCR.Status); d != "" {
+ if d := cmp.Diff(customRun.Status, crAfter.Status); d != "" {
t.Fatalf("CustomRun should not have changed, got %v instead", d)
}
diff --git a/pkg/reconciler/notifications/runtimeobject.go b/pkg/reconciler/notifications/runtimeobject.go
new file mode 100644
index 00000000000..6a3f6033065
--- /dev/null
+++ b/pkg/reconciler/notifications/runtimeobject.go
@@ -0,0 +1,53 @@
+/*
+Copyright 2023 The Tekton Authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package notifications
+
+import (
+ "context"
+
+ lru "github.com/hashicorp/golang-lru"
+ "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1"
+ "github.com/tektoncd/pipeline/pkg/reconciler/events"
+ "github.com/tektoncd/pipeline/pkg/reconciler/events/cache"
+ "github.com/tektoncd/pipeline/pkg/reconciler/events/cloudevent"
+ "knative.dev/pkg/apis"
+ "knative.dev/pkg/logging"
+ pkgreconciler "knative.dev/pkg/reconciler"
+)
+
+// Reconciler implements controller.Reconciler for Configuration resources.
+type Reconciler interface {
+ GetCloudEventsClient() cloudevent.CEClient
+ GetCacheClient() *lru.Cache
+ SetCloudEventsClient(cloudevent.CEClient)
+ SetCacheClient(*lru.Cache)
+}
+
+// ReconcileRuntimeObject observes a v1beta1.RunObject and triggers notifications
+func ReconcileRuntimeObject(ctx context.Context, c Reconciler, readOnlyRun v1beta1.RunObject) pkgreconciler.Event {
+ logger := logging.FromContext(ctx)
+ ctx = cloudevent.ToContext(ctx, c.GetCloudEventsClient())
+ ctx = cache.ToContext(ctx, c.GetCacheClient())
+
+ logger.Infof("reconciling %s", readOnlyRun.GetObjectMeta().GetName())
+
+ condition := readOnlyRun.GetStatusCondition().GetCondition(apis.ConditionSucceeded)
+ logger.Debugf("customRun %s, condition: %s", readOnlyRun.GetObjectMeta().GetName(), condition)
+
+ events.EmitCloudEvents(ctx, readOnlyRun)
+ return nil
+}
diff --git a/pkg/reconciler/notifications/runtimeobject_test.go b/pkg/reconciler/notifications/runtimeobject_test.go
new file mode 100644
index 00000000000..48c0ca6df8b
--- /dev/null
+++ b/pkg/reconciler/notifications/runtimeobject_test.go
@@ -0,0 +1,125 @@
+/*
+Copyright 2023 The Tekton Authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package notifications_test
+
+import (
+ "context"
+ "testing"
+
+ "github.com/tektoncd/pipeline/pkg/apis/config"
+ v1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1"
+ "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1"
+ "github.com/tektoncd/pipeline/pkg/reconciler/events"
+ "github.com/tektoncd/pipeline/pkg/reconciler/events/cache"
+ "github.com/tektoncd/pipeline/pkg/reconciler/events/cloudevent"
+ "github.com/tektoncd/pipeline/pkg/reconciler/notifications"
+ ntesting "github.com/tektoncd/pipeline/pkg/reconciler/notifications/testing"
+ "github.com/tektoncd/pipeline/test"
+ corev1 "k8s.io/api/core/v1"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/runtime"
+ "knative.dev/pkg/system"
+ _ "knative.dev/pkg/system/testing" // Setup system.Namespace()
+)
+
+// TestReconcileRuntimeObject runs reconcile with a cloud event sink configured
+// and ensures that the event logic is correctly invoked for all supported types
+func TestReconcileRuntimeObject(t *testing.T) {
+ cms := []*corev1.ConfigMap{
+ {
+ ObjectMeta: metav1.ObjectMeta{Name: config.GetEventsConfigName(), Namespace: system.Namespace()},
+ Data: map[string]string{
+ "sink": "http://synk:8080",
+ },
+ }, {
+ ObjectMeta: metav1.ObjectMeta{Name: config.GetFeatureFlagsConfigName(), Namespace: system.Namespace()},
+ Data: map[string]string{
+ "send-cloudevents-for-runs": "true",
+ },
+ },
+ }
+
+ for _, tc := range []struct {
+ name string
+ runObject v1beta1.RunObject
+ }{{
+ name: "v1 TaskRun",
+ runObject: &v1.TaskRun{},
+ }, {
+ name: "v1 PipelineRun",
+ runObject: &v1.PipelineRun{},
+ }, {
+ name: "v1beta1 TaskRun",
+ runObject: &v1beta1.TaskRun{},
+ }, {
+ name: "v1beta1 PipelineRun",
+ runObject: &v1beta1.PipelineRun{},
+ }, {
+ name: "v1beta1 CustomRun",
+ runObject: &v1beta1.CustomRun{},
+ }} {
+ t.Run(tc.name, func(t *testing.T) {
+ // Setup mock EmitCloudEvents
+ calls := []ntesting.TestEmitCloudEventsParams{}
+ events.EmitCloudEvents = func(ctx context.Context, object runtime.Object) {
+ calls = append(calls, ntesting.TestEmitCloudEventsParams{
+ Ctx: ctx,
+ Object: object,
+ })
+ }
+
+ d := test.Data{
+ ConfigMaps: cms,
+ }
+ testAssets, cancel := ntesting.InitializeTestAssets(t, &d)
+ defer cancel()
+ clients := testAssets.Clients
+ reconciler := &ntesting.FakeReconciler{}
+ notifications.ReconcilerFromContext(testAssets.Ctx, reconciler)
+
+ if err := notifications.ReconcileRuntimeObject(testAssets.Ctx, reconciler, tc.runObject); err != nil {
+ t.Errorf("didn't expect an error, but got one: %v", err)
+ }
+
+ if len(calls) != 1 {
+ t.Errorf("expected one call to EmitCloudEvents, got: %d", len(calls))
+ }
+
+ // Check the context
+ ctx := calls[0].Ctx
+ if ceClient := cloudevent.Get(ctx); ceClient == nil {
+ t.Error("expected the cloudevents client in the context, but got none")
+ }
+ if cacheClient := cache.Get(ctx); cacheClient == nil {
+ t.Error("expected the cache client in the context, but got none")
+ }
+
+ for _, a := range clients.Kube.Actions() {
+ aVerb := a.GetVerb()
+ if aVerb != "get" && aVerb != "list" && aVerb != "watch" {
+ t.Errorf("Expected only read actions to be logged in the kubeclient, got %s", aVerb)
+ }
+ }
+
+ // Check that the object is the same passed to reconcile
+ runObject := calls[0].Object
+ if runObject != tc.runObject {
+ t.Error("expected EmitCloudEvents to receive exactly the same object from the reconcile")
+ }
+ })
+ }
+}
diff --git a/pkg/reconciler/notifications/testing/assets.go b/pkg/reconciler/notifications/testing/assets.go
new file mode 100644
index 00000000000..5db925421f7
--- /dev/null
+++ b/pkg/reconciler/notifications/testing/assets.go
@@ -0,0 +1,114 @@
+/*
+Copyright 2023 The Tekton Authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package testing
+
+import (
+ "context"
+ "strings"
+ "testing"
+
+ lru "github.com/hashicorp/golang-lru"
+ "github.com/tektoncd/pipeline/pkg/apis/config"
+ "github.com/tektoncd/pipeline/pkg/reconciler/events/cloudevent"
+ rtesting "github.com/tektoncd/pipeline/pkg/reconciler/testing"
+ "github.com/tektoncd/pipeline/test"
+ "github.com/tektoncd/pipeline/test/names"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/runtime"
+ "k8s.io/client-go/tools/record"
+ "knative.dev/pkg/controller"
+ "knative.dev/pkg/logging"
+)
+
+// TestEmitCloudEventsParams matches the parameter of the `EmitCloudEvents`
+// function, it is used to record invocations of the function
+type TestEmitCloudEventsParams struct {
+ Ctx context.Context //nolint:containedctx
+ Object runtime.Object
+}
+
+// Reconciler implements the Reconciler interface from the notification package
+type FakeReconciler struct {
+ cloudEventClient cloudevent.CEClient
+ cacheClient *lru.Cache
+}
+
+func (c *FakeReconciler) GetCloudEventsClient() cloudevent.CEClient {
+ return c.cloudEventClient
+}
+
+func (c *FakeReconciler) GetCacheClient() *lru.Cache {
+ return c.cacheClient
+}
+
+func (c *FakeReconciler) SetCloudEventsClient(client cloudevent.CEClient) {
+ c.cloudEventClient = client
+}
+
+func (c *FakeReconciler) SetCacheClient(client *lru.Cache) {
+ c.cacheClient = client
+}
+
+func configFromConfigMap(d test.Data) config.Config {
+ testConfig := config.Config{}
+ for _, cm := range d.ConfigMaps {
+ switch cm.Name {
+ case config.GetDefaultsConfigName():
+ testConfig.Defaults, _ = config.NewDefaultsFromConfigMap(cm)
+ case config.GetFeatureFlagsConfigName():
+ testConfig.FeatureFlags, _ = config.NewFeatureFlagsFromConfigMap(cm)
+ case config.GetEventsConfigName():
+ testConfig.Events, _ = config.NewEventsFromConfigMap(cm)
+ }
+ }
+ return testConfig
+}
+
+// InitializeTestAssets sets up test assets to be used for direct testing
+// of the ReconcileKind method (i.e. with no controller object)
+// Config maps are loaded into the context and no config map watcher is setup
+//
+// Example usage:
+//
+// testAssets, cancel := rtesting.InitializeTestAssets(t, &d)
+// defer cancel()
+// reconciler := &rtesting.FakeReconciler{}
+// notifications.ReconcilerFromContext(testAssets.Ctx, reconciler)
+func InitializeTestAssets(t *testing.T, d *test.Data) (test.Assets, func()) {
+ t.Helper()
+ names.TestingSeed()
+ ctx, _ := rtesting.SetupFakeContext(t)
+ ctx = rtesting.SetupFakeCloudClientContext(ctx, d.ExpectedCloudEventCount)
+ ctx, cancel := context.WithCancel(ctx)
+ // Ensure all cm exists before seeding the data
+ test.EnsureConfigurationConfigMapsExist(d)
+ c, informers := test.SeedTestData(t, ctx, *d)
+ testConfig := configFromConfigMap(*d)
+ ctx = config.ToContext(ctx, &testConfig)
+
+ return test.Assets{
+ Logger: logging.FromContext(ctx),
+ Clients: c,
+ Informers: informers,
+ Recorder: controller.GetEventRecorder(ctx).(*record.FakeRecorder),
+ Ctx: ctx,
+ }, cancel
+}
+
+func GetTestResourceName(run metav1.ObjectMetaAccessor) string {
+ return strings.Join([]string{run.GetObjectMeta().GetNamespace(), run.GetObjectMeta().GetName()}, "/")
+}