From e01714590d951587b70df058ff02a13413f40374 Mon Sep 17 00:00:00 2001 From: Papapetrou Patroklos <1743100+ppapapetrou76@users.noreply.github.com> Date: Wed, 4 Mar 2026 15:51:48 +0200 Subject: [PATCH] fix: create events with argocd namespace (#26667) Signed-off-by: Patroklos Papapetrou --- controller/appcontroller.go | 2 +- server/application/application.go | 2 +- server/applicationset/applicationset.go | 2 +- server/project/project.go | 2 +- util/argo/audit_logger.go | 24 ++- util/argo/audit_logger_test.go | 258 ++++++++++++++++++++---- 6 files changed, 249 insertions(+), 41 deletions(-) diff --git a/controller/appcontroller.go b/controller/appcontroller.go index d2aa8c5fdd..48bd055e1c 100644 --- a/controller/appcontroller.go +++ b/controller/appcontroller.go @@ -205,7 +205,7 @@ func NewApplicationController( statusRefreshJitter: appResyncJitter, refreshRequestedApps: make(map[string]CompareWith), refreshRequestedAppsMutex: &sync.Mutex{}, - auditLogger: argo.NewAuditLogger(kubeClientset, common.CommandApplicationController, enableK8sEvent), + auditLogger: argo.NewAuditLogger(kubeClientset, namespace, common.CommandApplicationController, enableK8sEvent), settingsMgr: settingsMgr, selfHealTimeout: selfHealTimeout, selfHealBackoff: selfHealBackoff, diff --git a/server/application/application.go b/server/application/application.go index 255b3fc6a6..c373cb4698 100644 --- a/server/application/application.go +++ b/server/application/application.go @@ -152,7 +152,7 @@ func NewServer( kubectl: kubectl, enf: enf, projectLock: projectLock, - auditLogger: argo.NewAuditLogger(kubeclientset, "argocd-server", enableK8sEvent), + auditLogger: argo.NewAuditLogger(kubeclientset, namespace, "argocd-server", enableK8sEvent), settingsMgr: settingsMgr, projInformer: projInformer, enabledNamespaces: enabledNamespaces, diff --git a/server/applicationset/applicationset.go b/server/applicationset/applicationset.go index e603f01de8..018f056c30 100644 --- a/server/applicationset/applicationset.go +++ b/server/applicationset/applicationset.go @@ -215,7 +215,7 @@ func NewServer( appsetLister: appsetLister, appSetBroadcaster: appSetBroadcaster, projectLock: projectLock, - auditLogger: argo.NewAuditLogger(kubeclientset, "argocd-server", enableK8sEvent), + auditLogger: argo.NewAuditLogger(kubeclientset, namespace, "argocd-server", enableK8sEvent), enabledNamespaces: enabledNamespaces, clusterInformer: clusterInformer, GitSubmoduleEnabled: gitSubmoduleEnabled, diff --git a/server/project/project.go b/server/project/project.go index b534e738df..a205ecf63c 100644 --- a/server/project/project.go +++ b/server/project/project.go @@ -60,7 +60,7 @@ type Server struct { func NewServer(ns string, kubeclientset kubernetes.Interface, appclientset appclientset.Interface, enf *rbac.Enforcer, projectLock sync.KeyLock, sessionMgr *session.SessionManager, policyEnf *rbacpolicy.RBACPolicyEnforcer, projInformer cache.SharedIndexInformer, settingsMgr *settings.SettingsManager, db db.ArgoDB, enableK8sEvent []string, ) *Server { - auditLogger := argo.NewAuditLogger(kubeclientset, "argocd-server", enableK8sEvent) + auditLogger := argo.NewAuditLogger(kubeclientset, ns, "argocd-server", enableK8sEvent) return &Server{ enf: enf, policyEnf: policyEnf, appclientset: appclientset, kubeclientset: kubeclientset, ns: ns, projectLock: projectLock, auditLogger: auditLogger, sessionMgr: sessionMgr, projInformer: projInformer, settingsMgr: settingsMgr, db: db, diff --git a/util/argo/audit_logger.go b/util/argo/audit_logger.go index 7d01c604e2..8b290ff261 100644 --- a/util/argo/audit_logger.go +++ b/util/argo/audit_logger.go @@ -20,6 +20,7 @@ import ( type AuditLogger struct { kIf kubernetes.Interface component string + namespace string enableEventLog map[string]bool } @@ -63,6 +64,22 @@ func (l *AuditLogger) logEvent(objMeta ObjectRef, gvk schema.GroupVersionKind, i logCtx = logCtx.WithField("name", objMeta.Name) } t := metav1.Time{Time: time.Now()} + + // Determine which namespace to create the event in + eventNamespace := objMeta.Namespace + // For resource events (non-Application, non-AppProject, non-ApplicationSet), + // create events in the ArgoCD namespace to support multi-cluster + if gvk.Kind != application.ApplicationKind && + gvk.Kind != application.AppProjectKind && + gvk.Kind != application.ApplicationSetKind { + eventNamespace = l.namespace + // Add the original namespace to annotations for reference + if logFields == nil { + logFields = make(map[string]string) + } + logFields["resource-namespace"] = objMeta.Namespace + } + event := corev1.Event{ ObjectMeta: metav1.ObjectMeta{ Name: fmt.Sprintf("%v.%x", objMeta.Name, t.UnixNano()), @@ -75,7 +92,7 @@ func (l *AuditLogger) logEvent(objMeta ObjectRef, gvk schema.GroupVersionKind, i InvolvedObject: corev1.ObjectReference{ Kind: gvk.Kind, Name: objMeta.Name, - Namespace: objMeta.Namespace, + Namespace: objMeta.Namespace, // Keep the original namespace in InvolvedObject ResourceVersion: objMeta.ResourceVersion, APIVersion: gvk.GroupVersion().String(), UID: objMeta.UID, @@ -88,7 +105,7 @@ func (l *AuditLogger) logEvent(objMeta ObjectRef, gvk schema.GroupVersionKind, i Reason: info.Reason, } logCtx.Info(message) - _, err := l.kIf.CoreV1().Events(objMeta.Namespace).Create(context.Background(), &event, metav1.CreateOptions{}) + _, err := l.kIf.CoreV1().Events(eventNamespace).Create(context.Background(), &event, metav1.CreateOptions{}) if err != nil { logCtx.Errorf("Unable to create audit event: %v", err) return @@ -178,10 +195,11 @@ func (l *AuditLogger) LogAppProjEvent(proj *v1alpha1.AppProject, info EventInfo, l.logEvent(objectMeta, v1alpha1.AppProjectSchemaGroupVersionKind, info, message, nil, nil) } -func NewAuditLogger(kIf kubernetes.Interface, component string, enableK8sEvent []string) *AuditLogger { +func NewAuditLogger(kIf kubernetes.Interface, namespace, component string, enableK8sEvent []string) *AuditLogger { return &AuditLogger{ kIf: kIf, component: component, + namespace: namespace, enableEventLog: setK8sEventList(enableK8sEvent), } } diff --git a/util/argo/audit_logger_test.go b/util/argo/audit_logger_test.go index c5672d9918..769a6b573f 100644 --- a/util/argo/audit_logger_test.go +++ b/util/argo/audit_logger_test.go @@ -2,12 +2,15 @@ package argo import ( "bytes" + "context" "sync" "testing" log "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes/fake" @@ -17,6 +20,8 @@ import ( const ( _somecomponent = "somecomponent" _test = "test" + _argocdNs = "argocd" + _targetNs = "target-namespace" ) var testEnableEventLog = []string{_somecomponent, _test} @@ -63,18 +68,21 @@ func captureLogEntries(run func()) string { } func TestNewAuditLogger(t *testing.T) { - logger := NewAuditLogger(fake.NewClientset(), _somecomponent, testEnableEventLog) + logger := NewAuditLogger(fake.NewClientset(), _argocdNs, _somecomponent, testEnableEventLog) assert.NotNil(t, logger) + assert.Equal(t, _argocdNs, logger.namespace) + assert.Equal(t, _somecomponent, logger.component) } func TestLogAppProjEvent(t *testing.T) { - logger := NewAuditLogger(fake.NewClientset(), _somecomponent, testEnableEventLog) + fakeClient := fake.NewClientset() + logger := NewAuditLogger(fakeClient, _argocdNs, _somecomponent, testEnableEventLog) assert.NotNil(t, logger) proj := argoappv1.AppProject{ ObjectMeta: metav1.ObjectMeta{ Name: "default", - Namespace: "argocd", + Namespace: _argocdNs, ResourceVersion: "1", UID: "a-b-c-d-e", }, @@ -88,34 +96,33 @@ func TestLogAppProjEvent(t *testing.T) { Type: "info", } - output := captureLogEntries(func() { - logger.LogAppProjEvent(&proj, ei, "This is a test message", "") + captureLogEntries(func() { + logger.LogAppProjEvent(&proj, ei, "This is a test message", "admin") }) - assert.Contains(t, output, "level=info") - assert.Contains(t, output, "project=default") - assert.Contains(t, output, "reason=test") - assert.Contains(t, output, "type=info") - assert.Contains(t, output, "msg=\"This is a test message\"") + // Verify event was created in the AppProject's namespace (argocd) + events, err := fakeClient.CoreV1().Events(_argocdNs).List(context.Background(), metav1.ListOptions{}) + require.NoError(t, err) + require.Len(t, events.Items, 1) - ei.Reason = "Unknown" - - // If K8s Event Disable Log - output = captureLogEntries(func() { - logger.LogAppProjEvent(&proj, ei, "This is a test message", "") - }) - - assert.Empty(t, output) + event := events.Items[0] + assert.Equal(t, "default", event.InvolvedObject.Name) + assert.Equal(t, _argocdNs, event.InvolvedObject.Namespace) + assert.Equal(t, _argocdNs, event.Namespace) // Event created in argocd namespace + assert.Equal(t, "This is a test message", event.Message) + assert.Equal(t, "test", event.Reason) + assert.Equal(t, "info", event.Type) } func TestLogAppEvent(t *testing.T) { - logger := NewAuditLogger(fake.NewClientset(), _somecomponent, testEnableEventLog) + fakeClient := fake.NewClientset() + logger := NewAuditLogger(fakeClient, _argocdNs, _somecomponent, testEnableEventLog) assert.NotNil(t, logger) app := argoappv1.Application{ ObjectMeta: metav1.ObjectMeta{ Name: "testapp", - Namespace: "argocd", + Namespace: _argocdNs, ResourceVersion: "1", UID: "a-b-c-d-e", }, @@ -131,23 +138,29 @@ func TestLogAppEvent(t *testing.T) { Reason: _test, Type: "info", } - - output := captureLogEntries(func() { - logger.LogAppEvent(&app, ei, "This is a test message", "", nil) + captureLogEntries(func() { + logger.LogAppEvent(&app, ei, "This is a test message", "admin", nil) }) - assert.Contains(t, output, "level=info") - assert.Contains(t, output, "application=testapp") - assert.Contains(t, output, "dest-namespace=testns") - assert.Contains(t, output, "dest-server=\"https://127.0.0.1:6443\"") - assert.Contains(t, output, "reason=test") - assert.Contains(t, output, "type=info") - assert.Contains(t, output, "msg=\"This is a test message\"") + // Verify event was created in the Application's namespace (argocd) + events, err := fakeClient.CoreV1().Events(_argocdNs).List(context.Background(), metav1.ListOptions{}) + require.NoError(t, err) + require.Len(t, events.Items, 1) - ei.Reason = "Unknown" + event := events.Items[0] + assert.Equal(t, "testapp", event.InvolvedObject.Name) + assert.Equal(t, _argocdNs, event.InvolvedObject.Namespace) + assert.Equal(t, _argocdNs, event.Namespace) // Event created in argocd namespace + assert.Equal(t, "This is a test message", event.Message) + assert.Equal(t, "test", event.Reason) + assert.Equal(t, "info", event.Type) + assert.Equal(t, "admin", event.Annotations["user"]) + assert.Equal(t, "https://127.0.0.1:6443", event.Annotations["dest-server"]) + assert.Equal(t, "testns", event.Annotations["dest-namespace"]) // If K8s Event Disable Log - output = captureLogEntries(func() { + ei.Reason = "Unknown" + output := captureLogEntries(func() { logger.LogAppEvent(&app, ei, "This is a test message", "", nil) }) @@ -155,7 +168,7 @@ func TestLogAppEvent(t *testing.T) { } func TestLogResourceEvent(t *testing.T) { - logger := NewAuditLogger(fake.NewClientset(), _somecomponent, testEnableEventLog) + logger := NewAuditLogger(fake.NewClientset(), _argocdNs, _somecomponent, testEnableEventLog) assert.NotNil(t, logger) res := argoappv1.ResourceNode{ @@ -164,7 +177,7 @@ func TestLogResourceEvent(t *testing.T) { Version: "v1alpha1", Kind: "SignatureKey", Name: "testapp", - Namespace: "argocd", + Namespace: _argocdNs, UID: "a-b-c-d-e", }, } @@ -193,3 +206,180 @@ func TestLogResourceEvent(t *testing.T) { assert.Empty(t, output) } + +func TestLogResourceEvent_MultiCluster_CreatesEventInArgocdNamespace(t *testing.T) { + fakeClient := fake.NewClientset() + logger := NewAuditLogger(fakeClient, _argocdNs, _somecomponent, []string{EventReasonResourceActionRan}) + + res := argoappv1.ResourceNode{ + ResourceRef: argoappv1.ResourceRef{ + Group: "apps", + Version: "v1", + Kind: "Deployment", + Name: "my-deployment", + Namespace: _targetNs, // Resource is in a different namespace/cluster + UID: "deploy-uid-123", + }, + } + + ei := EventInfo{ + Reason: EventReasonResourceActionRan, + Type: corev1.EventTypeNormal, + } + captureLogEntries(func() { + logger.LogResourceEvent(&res, ei, "Resource action executed", "admin") + }) + + events, err := fakeClient.CoreV1().Events(_targetNs).List(context.Background(), metav1.ListOptions{}) + require.NoError(t, err) + assert.Empty(t, events.Items, "No event should be created in target namespace") + + events, err = fakeClient.CoreV1().Events(_argocdNs).List(context.Background(), metav1.ListOptions{}) + require.NoError(t, err) + require.Len(t, events.Items, 1, "Event should be created in ArgoCD namespace") + + event := events.Items[0] + assert.Equal(t, "my-deployment", event.InvolvedObject.Name) + assert.Equal(t, _targetNs, event.InvolvedObject.Namespace, "InvolvedObject should preserve original namespace") + assert.Equal(t, _argocdNs, event.Namespace, "Event itself should be in ArgoCD namespace") + assert.Equal(t, "Resource action executed", event.Message) + + assert.Equal(t, _targetNs, event.Annotations["resource-namespace"]) +} + +func TestLogAppSetEvent_CreatesEventInAppSetNamespace(t *testing.T) { + fakeClient := fake.NewClientset() + logger := NewAuditLogger(fakeClient, _argocdNs, _somecomponent, testEnableEventLog) + + appset := argoappv1.ApplicationSet{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-appset", + Namespace: _argocdNs, + ResourceVersion: "1", + UID: "appset-uid-123", + }, + } + + ei := EventInfo{ + Reason: _test, + Type: corev1.EventTypeNormal, + } + captureLogEntries(func() { + logger.LogAppSetEvent(&appset, ei, "ApplicationSet event test", "admin") + }) + + // Verify event was created in the ApplicationSet's namespace (argocd) + events, err := fakeClient.CoreV1().Events(_argocdNs).List(context.Background(), metav1.ListOptions{}) + require.NoError(t, err) + require.Len(t, events.Items, 1) + + event := events.Items[0] + assert.Equal(t, "my-appset", event.InvolvedObject.Name) + assert.Equal(t, _argocdNs, event.InvolvedObject.Namespace) + assert.Equal(t, _argocdNs, event.Namespace) // Event created in argocd namespace + assert.Equal(t, "ApplicationSet event test", event.Message) +} + +func TestLogResourceEvent_DifferentKinds_AllInArgocdNamespace(t *testing.T) { + testCases := []struct { + name string + resourceKind string + resourceGroup string + resourceNs string + }{ + { + name: "Pod in remote namespace", + resourceKind: "Pod", + resourceGroup: "", + resourceNs: "production", + }, + { + name: "Service in remote namespace", + resourceKind: "Service", + resourceGroup: "", + resourceNs: "staging", + }, + { + name: "Deployment in remote namespace", + resourceKind: "Deployment", + resourceGroup: "apps", + resourceNs: "default", + }, + { + name: "ConfigMap in remote namespace", + resourceKind: "ConfigMap", + resourceGroup: "", + resourceNs: "kube-system", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + fakeClient := fake.NewClientset() + logger := NewAuditLogger(fakeClient, _argocdNs, _somecomponent, []string{EventReasonResourceActionRan}) + + res := argoappv1.ResourceNode{ + ResourceRef: argoappv1.ResourceRef{ + Group: tc.resourceGroup, + Version: "v1", + Kind: tc.resourceKind, + Name: "test-resource", + Namespace: tc.resourceNs, + UID: "test-uid", + }, + } + + ei := EventInfo{ + Reason: EventReasonResourceActionRan, + Type: corev1.EventTypeNormal, + } + captureLogEntries(func() { + logger.LogResourceEvent(&res, ei, "Action ran", "admin") + }) + + events, err := fakeClient.CoreV1().Events(_argocdNs).List(context.Background(), metav1.ListOptions{}) + require.NoError(t, err) + require.Len(t, events.Items, 1) + + event := events.Items[0] + assert.Equal(t, _argocdNs, event.Namespace, "Event should be in ArgoCD namespace for %s", tc.resourceKind) + assert.Equal(t, tc.resourceNs, event.InvolvedObject.Namespace, "InvolvedObject should preserve original namespace") + assert.Equal(t, tc.resourceNs, event.Annotations["resource-namespace"]) + }) + } +} + +func TestLogResourceEvent_EmptyNamespace(t *testing.T) { + fakeClient := fake.NewClientset() + logger := NewAuditLogger(fakeClient, _argocdNs, _somecomponent, []string{EventReasonResourceActionRan}) + + // Cluster-scoped resource (no namespace) + res := argoappv1.ResourceNode{ + ResourceRef: argoappv1.ResourceRef{ + Group: "rbac.authorization.k8s.io", + Version: "v1", + Kind: "ClusterRole", + Name: "cluster-admin", + Namespace: "", // Cluster-scoped resource + UID: "clusterrole-uid", + }, + } + + ei := EventInfo{ + Reason: EventReasonResourceActionRan, + Type: corev1.EventTypeNormal, + } + captureLogEntries(func() { + logger.LogResourceEvent(&res, ei, "Cluster role action", "admin") + }) + + // Event should be created in ArgoCD namespace (not empty namespace) + events, err := fakeClient.CoreV1().Events(_argocdNs).List(context.Background(), metav1.ListOptions{}) + require.NoError(t, err) + require.Len(t, events.Items, 1) + + event := events.Items[0] + assert.Equal(t, _argocdNs, event.Namespace) + assert.Empty(t, event.InvolvedObject.Namespace) // ClusterRole has no namespace + assert.Empty(t, event.Annotations["resource-namespace"]) +}