diff --git a/apis/event/v1beta1/event.go b/apis/event/v1beta1/event.go index b6664c20..61621e73 100644 --- a/apis/event/v1beta1/event.go +++ b/apis/event/v1beta1/event.go @@ -21,6 +21,9 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) +// Group is the API Group for the Event API. +const Group = "event.toolkit.fluxcd.io" + // These constants define valid event severity values. const ( // EventSeverityTrace represents a trace event, usually diff --git a/runtime/events/recorder.go b/runtime/events/recorder.go index 36804037..7383b9ae 100644 --- a/runtime/events/recorder.go +++ b/runtime/events/recorder.go @@ -21,9 +21,11 @@ import ( "encoding/json" "errors" "fmt" + "maps" "net/http" "net/url" "os" + "strings" "time" "github.com/go-logr/logr" @@ -145,7 +147,7 @@ func (r *Recorder) Eventf(object runtime.Object, eventtype, reason, messageFmt s // It also logs the event if debug logs are enabled in the logger. func (r *Recorder) AnnotatedEventf( object runtime.Object, - annotations map[string]string, + inputAnnotations map[string]string, eventtype, reason string, messageFmt string, args ...interface{}) { @@ -154,6 +156,16 @@ func (r *Recorder) AnnotatedEventf( r.Log.Error(err, "failed to get object reference") } + // Add object annotations to the annotations. + annotations := maps.Clone(inputAnnotations) + if annotatedObject, ok := object.(interface{ GetAnnotations() map[string]string }); ok { + for k, v := range annotatedObject.GetAnnotations() { + if strings.HasPrefix(k, eventv1.Group+"/") { + annotations[k] = v + } + } + } + // Add object info in the logger. log := r.Log.WithValues("name", ref.Name, "namespace", ref.Namespace, "reconciler kind", ref.Kind) diff --git a/runtime/events/recorder_test.go b/runtime/events/recorder_test.go index 58246298..5e4849bb 100644 --- a/runtime/events/recorder_test.go +++ b/runtime/events/recorder_test.go @@ -25,48 +25,91 @@ import ( "github.com/stretchr/testify/require" corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" ctrl "sigs.k8s.io/controller-runtime" eventv1 "github.com/fluxcd/pkg/apis/event/v1beta1" ) func TestEventRecorder_AnnotatedEventf(t *testing.T) { - requestCount := 0 - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - requestCount++ - b, err := io.ReadAll(r.Body) - require.NoError(t, err) - - var payload eventv1.Event - err = json.Unmarshal(b, &payload) - require.NoError(t, err) - - require.Equal(t, "ConfigMap", payload.InvolvedObject.Kind) - require.Equal(t, "webapp", payload.InvolvedObject.Name) - require.Equal(t, "gitops-system", payload.InvolvedObject.Namespace) - require.Equal(t, "true", payload.Metadata["test"]) - require.Equal(t, "sync", payload.Reason) - - })) - defer ts.Close() - - eventRecorder, err := NewRecorder(env, ctrl.Log, ts.URL, "test-controller") - require.NoError(t, err) - - obj := &corev1.ConfigMap{} - obj.Namespace = "gitops-system" - obj.Name = "webapp" - - meta := map[string]string{ - "test": "true", + for _, tt := range []struct { + name string + object runtime.Object + expectedMetadata map[string]string + }{ + { + name: "event with ConfigMap", + object: &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "webapp", + Namespace: "gitops-system", + Annotations: map[string]string{ + "event.toolkit.fluxcd.io/deploymentID": "e076e315-5a48-41c3-81c8-8d8bdee7d74d", + "event.toolkit.fluxcd.io/image": "ghcr.io/stefanprodan/podinfo:6.5.0", + }, + }, + }, + expectedMetadata: map[string]string{ + "test": "true", + "event.toolkit.fluxcd.io/deploymentID": "e076e315-5a48-41c3-81c8-8d8bdee7d74d", + "event.toolkit.fluxcd.io/image": "ghcr.io/stefanprodan/podinfo:6.5.0", + }, + }, + { + name: "event with ObjectReference for ConfigMap (does not panic with runtime.Object without annotations)", + object: &corev1.ObjectReference{ + Name: "webapp", + Namespace: "gitops-system", + Kind: "ConfigMap", + }, + expectedMetadata: map[string]string{ + "test": "true", + }, + }, + } { + t.Run(tt.name, func(t *testing.T) { + requestCount := 0 + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + requestCount++ + b, err := io.ReadAll(r.Body) + require.NoError(t, err) + + var payload eventv1.Event + err = json.Unmarshal(b, &payload) + require.NoError(t, err) + + require.Equal(t, "ConfigMap", payload.InvolvedObject.Kind) + require.Equal(t, "webapp", payload.InvolvedObject.Name) + require.Equal(t, "gitops-system", payload.InvolvedObject.Namespace) + require.Equal(t, "sync", payload.Reason) + require.Equal(t, "sync object", payload.Message) + + for k, v := range tt.expectedMetadata { + require.Equal(t, v, payload.Metadata[k]) + } + })) + defer ts.Close() + + eventRecorder, err := NewRecorder(env, ctrl.Log, ts.URL, "test-controller") + require.NoError(t, err) + + obj := tt.object + + meta := map[string]string{ + "test": "true", + } + + const msg = "sync object" + + eventRecorder.AnnotatedEventf(obj, meta, corev1.EventTypeNormal, "sync", "%s", msg) + require.Equal(t, 2, requestCount) + + // When a trace event is sent, it's dropped, no new request. + eventRecorder.AnnotatedEventf(obj, meta, eventv1.EventTypeTrace, "sync", "%s", msg) + require.Equal(t, 2, requestCount) + }) } - - eventRecorder.AnnotatedEventf(obj, meta, corev1.EventTypeNormal, "sync", "sync %s", obj.Name) - require.Equal(t, 2, requestCount) - - // When a trace event is sent, it's dropped, no new request. - eventRecorder.AnnotatedEventf(obj, meta, eventv1.EventTypeTrace, "sync", "sync %s", obj.Name) - require.Equal(t, 2, requestCount) } func TestEventRecorder_AnnotatedEventf_Retry(t *testing.T) {