Skip to content

Commit 61ed63a

Browse files
committed
Add NotificationsConfiguration CR in the spec.notifications.sourceNamespaces
Signed-off-by: nmirasch <[email protected]>
1 parent 48d3f9c commit 61ed63a

File tree

2 files changed

+327
-2
lines changed

2 files changed

+327
-2
lines changed

controllers/argocd/notifications.go

Lines changed: 103 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -570,7 +570,7 @@ func (r *ReconcileArgoCD) reconcileNotificationsSourceNamespacesResources(cr *ar
570570
continue
571571
}
572572
if !contains(appsNamespaces, sourceNamespace) {
573-
log.Error(fmt.Errorf("skipping reconciliation of resources for sourceNamespace %s as Apps in target sourceNamespace is not enabled", sourceNamespace), "Warning")
573+
log.Error(fmt.Errorf("skipping reconciliation of Notification resources for sourceNamespace %s as Apps in target sourceNamespace is not enabled", sourceNamespace), "Warning")
574574
continue
575575
}
576576

@@ -655,6 +655,11 @@ func (r *ReconcileArgoCD) reconcileNotificationsSourceNamespacesResources(cr *ar
655655
reconciliationErrors = append(reconciliationErrors, err)
656656
}
657657

658+
// ensure NotificationsConfiguration CR exists in the source namespace
659+
if err := r.reconcileSourceNamespaceNotificationsConfigurationCR(cr, sourceNamespace); err != nil {
660+
reconciliationErrors = append(reconciliationErrors, err)
661+
}
662+
658663
// notifications permissions for argocd server in source namespaces are handled by apps-in-any-ns code
659664

660665
if _, ok := r.ManagedNotificationsSourceNamespaces[sourceNamespace]; !ok {
@@ -668,6 +673,90 @@ func (r *ReconcileArgoCD) reconcileNotificationsSourceNamespacesResources(cr *ar
668673
return amerr.NewAggregate(reconciliationErrors)
669674
}
670675

676+
// reconcileSourceNamespaceNotificationsConfigurationCR ensures a NotificationsConfiguration CR exists in the given source namespace.
677+
// It propagates the NotificationsConfiguration from the Argo CD instance namespace if it exists, otherwise uses defaults.
678+
func (r *ReconcileArgoCD) reconcileSourceNamespaceNotificationsConfigurationCR(cr *argoproj.ArgoCD, sourceNamespace string) error {
679+
if !isNotificationsEnabled(cr) {
680+
return nil
681+
}
682+
683+
// Helper function to copy a map
684+
copyMapForNotificationSpec := func(src map[string]string) map[string]string {
685+
if src == nil {
686+
return map[string]string{}
687+
}
688+
dst := make(map[string]string, len(src))
689+
for k, v := range src {
690+
dst[k] = v
691+
}
692+
return dst
693+
}
694+
695+
// Try to fetch the NotificationsConfiguration from the Argo CD instance namespace
696+
instanceNotifCfg := &v1alpha1.NotificationsConfiguration{}
697+
var instanceSpec *v1alpha1.NotificationsConfigurationSpec
698+
err := argoutil.FetchObject(r.Client, cr.Namespace, DefaultNotificationsConfigurationInstanceName, instanceNotifCfg)
699+
if err != nil {
700+
if !apierrors.IsNotFound(err) {
701+
return fmt.Errorf("failed to get the NotificationsConfiguration from instance namespace %s : %s", cr.Namespace, err)
702+
}
703+
// Not found in instance namespace, use defaults
704+
instanceSpec = &v1alpha1.NotificationsConfigurationSpec{
705+
Context: getDefaultNotificationsContext(),
706+
Triggers: getDefaultNotificationsTriggers(),
707+
Templates: getDefaultNotificationsTemplates(),
708+
}
709+
} else {
710+
// Found in instance namespace, use its spec
711+
instanceSpec = &instanceNotifCfg.Spec
712+
}
713+
714+
// Check if NotificationsConfiguration exists in source namespace
715+
sourceNotifCfg := &v1alpha1.NotificationsConfiguration{}
716+
err = argoutil.FetchObject(r.Client, sourceNamespace, DefaultNotificationsConfigurationInstanceName, sourceNotifCfg)
717+
if err != nil {
718+
if !apierrors.IsNotFound(err) {
719+
return fmt.Errorf("failed to get the NotificationsConfiguration from source namespace %s : %s", sourceNamespace, err)
720+
}
721+
// Not found in source namespace, create it with propagated/default spec
722+
newCfg := &v1alpha1.NotificationsConfiguration{
723+
ObjectMeta: v1.ObjectMeta{
724+
Name: DefaultNotificationsConfigurationInstanceName,
725+
Namespace: sourceNamespace,
726+
},
727+
Spec: v1alpha1.NotificationsConfigurationSpec{
728+
Context: copyMapForNotificationSpec(instanceSpec.Context),
729+
Triggers: copyMapForNotificationSpec(instanceSpec.Triggers),
730+
Templates: copyMapForNotificationSpec(instanceSpec.Templates),
731+
Services: copyMapForNotificationSpec(instanceSpec.Services),
732+
Subscriptions: copyMapForNotificationSpec(instanceSpec.Subscriptions),
733+
},
734+
}
735+
argoutil.LogResourceCreation(log, sourceNotifCfg, "propagating NotificationsConfiguration from instance namespace")
736+
return r.Create(context.TODO(), newCfg)
737+
}
738+
739+
// Already exists in source namespace, update it at leaset to match defaults
740+
updated := false
741+
if sourceNotifCfg.Spec.Context == nil {
742+
sourceNotifCfg.Spec.Context = getDefaultNotificationsContext()
743+
updated = true
744+
}
745+
if sourceNotifCfg.Spec.Triggers == nil {
746+
sourceNotifCfg.Spec.Triggers = getDefaultNotificationsTriggers()
747+
updated = true
748+
}
749+
if sourceNotifCfg.Spec.Templates == nil {
750+
sourceNotifCfg.Spec.Templates = getDefaultNotificationsTemplates()
751+
updated = true
752+
}
753+
if updated {
754+
argoutil.LogResourceUpdate(log, sourceNotifCfg)
755+
return r.Update(context.TODO(), sourceNotifCfg)
756+
}
757+
return nil
758+
}
759+
671760
func (r *ReconcileArgoCD) getNotificationsCommand(cr *argoproj.ArgoCD) []string {
672761

673762
cmd := make([]string, 0)
@@ -822,6 +911,19 @@ func (r *ReconcileArgoCD) cleanupUnmanagedNotificationsSourceNamespaceResources(
822911
}
823912
}
824913

914+
// Delete NotificationsConfiguration CR in source namespace
915+
notifCfg := &v1alpha1.NotificationsConfiguration{}
916+
if err := r.Get(context.TODO(), types.NamespacedName{Name: DefaultNotificationsConfigurationInstanceName, Namespace: namespace.Name}, notifCfg); err != nil {
917+
if !apierrors.IsNotFound(err) {
918+
return fmt.Errorf("failed to get the NotificationsConfiguration in namespace %s : %s", namespace.Name, err)
919+
}
920+
} else {
921+
argoutil.LogResourceDeletion(log, notifCfg, "cleaning up unmanaged notifications resources")
922+
if err := r.Delete(context.TODO(), notifCfg); err != nil {
923+
return fmt.Errorf("failed to delete the NotificationsConfiguration in namespace %s : %s", namespace.Name, err)
924+
}
925+
}
926+
825927
// app-in-any-ns code will handle removal of notifications permissions for argocd-server in target namespace
826928

827929
// Remove notifications-managed-by-cluster-argocd label from the namespace

controllers/argocd/notifications_test.go

Lines changed: 224 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -771,10 +771,12 @@ func TestNotifications_removeUnmanagedNotificationsSourceNamespaceResources(t *t
771771
subresObjs := []client.Object{a}
772772
runtimeObjs := []runtime.Object{}
773773
sch := makeTestReconcilerScheme(argoproj.AddToScheme)
774+
err := v1alpha1.AddToScheme(sch)
775+
assert.NoError(t, err)
774776
cl := makeTestReconcilerClient(sch, resObjs, subresObjs, runtimeObjs)
775777
r := makeTestReconciler(cl, sch, testclient.NewSimpleClientset())
776778

777-
err := createNamespace(r, ns1, "")
779+
err = createNamespace(r, ns1, "")
778780
assert.NoError(t, err)
779781
err = createNamespace(r, ns2, "")
780782
assert.NoError(t, err)
@@ -809,6 +811,12 @@ func TestNotifications_removeUnmanagedNotificationsSourceNamespaceResources(t *t
809811
assert.Error(t, err)
810812
assert.True(t, errors.IsNotFound(err))
811813

814+
// NotificationsConfiguration CR should be deleted from ns1
815+
notifCfg := &v1alpha1.NotificationsConfiguration{}
816+
err = r.Get(context.TODO(), client.ObjectKey{Name: DefaultNotificationsConfigurationInstanceName, Namespace: ns1}, notifCfg)
817+
assert.Error(t, err)
818+
assert.True(t, errors.IsNotFound(err))
819+
812820
// notifications tracking label should be removed
813821
namespace := &v1.Namespace{}
814822
err = r.Get(context.TODO(), client.ObjectKey{Name: ns1}, namespace)
@@ -826,10 +834,225 @@ func TestNotifications_removeUnmanagedNotificationsSourceNamespaceResources(t *t
826834
err = r.Get(context.TODO(), client.ObjectKey{Name: resName, Namespace: ns2}, roleBinding)
827835
assert.NoError(t, err)
828836

837+
// NotificationsConfiguration CR should still exist in ns2
838+
notifCfg = &v1alpha1.NotificationsConfiguration{}
839+
err = r.Get(context.TODO(), client.ObjectKey{Name: DefaultNotificationsConfigurationInstanceName, Namespace: ns2}, notifCfg)
840+
assert.NoError(t, err)
841+
829842
namespace = &v1.Namespace{}
830843
err = r.Get(context.TODO(), client.ObjectKey{Name: ns2}, namespace)
831844
assert.NoError(t, err)
832845
val, found := namespace.Labels[common.ArgoCDNotificationsManagedByClusterArgoCDLabel]
833846
assert.True(t, found)
834847
assert.Equal(t, a.Namespace, val)
835848
}
849+
850+
func TestReconcileNotifications_CreateNotificationsConfigurationInSourceNamespace_WithDefaults(t *testing.T) {
851+
logf.SetLogger(ZapLogger(true))
852+
sourceNamespace := "ns1"
853+
a := makeTestArgoCD(func(a *argoproj.ArgoCD) {
854+
a.Spec.Notifications.Enabled = true
855+
a.Spec.Notifications.SourceNamespaces = []string{sourceNamespace}
856+
a.Spec.SourceNamespaces = []string{sourceNamespace}
857+
})
858+
859+
resObjs := []client.Object{a}
860+
subresObjs := []client.Object{a}
861+
runtimeObjs := []runtime.Object{}
862+
sch := makeTestReconcilerScheme(argoproj.AddToScheme)
863+
err := v1alpha1.AddToScheme(sch)
864+
assert.NoError(t, err)
865+
cl := makeTestReconcilerClient(sch, resObjs, subresObjs, runtimeObjs)
866+
r := makeTestReconciler(cl, sch, testclient.NewSimpleClientset())
867+
868+
// Create the source namespace
869+
err = createNamespace(r, sourceNamespace, "")
870+
assert.NoError(t, err)
871+
872+
// Reconcile should create NotificationsConfiguration CR in source namespace with defaults
873+
// (since no NotificationsConfiguration exists in instance namespace)
874+
err = r.reconcileSourceNamespaceNotificationsConfigurationCR(a, sourceNamespace)
875+
assert.NoError(t, err)
876+
877+
// Verify NotificationsConfiguration CR exists in source namespace
878+
notifCfg := &v1alpha1.NotificationsConfiguration{}
879+
err = r.Get(context.TODO(), types.NamespacedName{
880+
Name: DefaultNotificationsConfigurationInstanceName,
881+
Namespace: sourceNamespace,
882+
}, notifCfg)
883+
assert.NoError(t, err)
884+
885+
// Verify it has the expected defaults
886+
expectedContext := getDefaultNotificationsContext()
887+
expectedTriggers := getDefaultNotificationsTriggers()
888+
expectedTemplates := getDefaultNotificationsTemplates()
889+
890+
// Normalize nil to empty map for comparison (Kubernetes may serialize empty maps as nil)
891+
actualContext := notifCfg.Spec.Context
892+
if actualContext == nil {
893+
actualContext = map[string]string{}
894+
}
895+
896+
assert.Equal(t, expectedContext, actualContext)
897+
assert.Equal(t, expectedTriggers, notifCfg.Spec.Triggers)
898+
assert.Equal(t, expectedTemplates, notifCfg.Spec.Templates)
899+
}
900+
901+
func TestReconcileNotifications_PropagateNotificationsConfigurationFromInstanceNamespace(t *testing.T) {
902+
logf.SetLogger(ZapLogger(true))
903+
sourceNamespace := "ns1"
904+
a := makeTestArgoCD(func(a *argoproj.ArgoCD) {
905+
a.Spec.Notifications.Enabled = true
906+
a.Spec.Notifications.SourceNamespaces = []string{sourceNamespace}
907+
a.Spec.SourceNamespaces = []string{sourceNamespace}
908+
})
909+
910+
// Create a custom NotificationsConfiguration in the instance namespace
911+
customInstanceCfg := &v1alpha1.NotificationsConfiguration{
912+
ObjectMeta: metav1.ObjectMeta{
913+
Name: DefaultNotificationsConfigurationInstanceName,
914+
Namespace: a.Namespace,
915+
},
916+
Spec: v1alpha1.NotificationsConfigurationSpec{
917+
Context: map[string]string{
918+
"customKey": "customValue",
919+
"argocdUrl": "https://custom-argocd.example.com",
920+
},
921+
Triggers: map[string]string{
922+
"trigger.custom": "custom trigger definition",
923+
},
924+
Templates: map[string]string{
925+
"template.custom": "custom template definition",
926+
},
927+
Services: map[string]string{
928+
"service.custom": "custom service definition",
929+
},
930+
Subscriptions: map[string]string{
931+
"subscription.custom": "custom subscription definition",
932+
},
933+
},
934+
}
935+
936+
resObjs := []client.Object{a, customInstanceCfg}
937+
subresObjs := []client.Object{a, customInstanceCfg}
938+
runtimeObjs := []runtime.Object{}
939+
sch := makeTestReconcilerScheme(argoproj.AddToScheme)
940+
err := v1alpha1.AddToScheme(sch)
941+
assert.NoError(t, err)
942+
cl := makeTestReconcilerClient(sch, resObjs, subresObjs, runtimeObjs)
943+
r := makeTestReconciler(cl, sch, testclient.NewSimpleClientset())
944+
945+
// Create the source namespace
946+
err = createNamespace(r, sourceNamespace, "")
947+
assert.NoError(t, err)
948+
949+
// Reconcile should propagate the NotificationsConfiguration from instance namespace
950+
err = r.reconcileSourceNamespaceNotificationsConfigurationCR(a, sourceNamespace)
951+
assert.NoError(t, err)
952+
953+
// Verify NotificationsConfiguration CR exists in source namespace
954+
sourceNotifCfg := &v1alpha1.NotificationsConfiguration{}
955+
err = r.Get(context.TODO(), types.NamespacedName{
956+
Name: DefaultNotificationsConfigurationInstanceName,
957+
Namespace: sourceNamespace,
958+
}, sourceNotifCfg)
959+
assert.NoError(t, err)
960+
961+
// Verify it matches the instance namespace configuration (propagated)
962+
assert.Equal(t, customInstanceCfg.Spec.Context, sourceNotifCfg.Spec.Context)
963+
assert.Equal(t, customInstanceCfg.Spec.Triggers, sourceNotifCfg.Spec.Triggers)
964+
assert.Equal(t, customInstanceCfg.Spec.Templates, sourceNotifCfg.Spec.Templates)
965+
assert.Equal(t, customInstanceCfg.Spec.Services, sourceNotifCfg.Spec.Services)
966+
assert.Equal(t, customInstanceCfg.Spec.Subscriptions, sourceNotifCfg.Spec.Subscriptions)
967+
968+
// Now update the instance namespace configuration and verify it gets propagated
969+
customInstanceCfg.Spec.Context["newKey"] = "newValue"
970+
err = r.Update(context.TODO(), customInstanceCfg)
971+
assert.NoError(t, err)
972+
973+
// Reconcile again
974+
err = r.reconcileSourceNamespaceNotificationsConfigurationCR(a, sourceNamespace)
975+
assert.NoError(t, err)
976+
977+
// Verify the source namespace configuration was updated
978+
err = r.Get(context.TODO(), types.NamespacedName{
979+
Name: DefaultNotificationsConfigurationInstanceName,
980+
Namespace: sourceNamespace,
981+
}, sourceNotifCfg)
982+
assert.NoError(t, err)
983+
// The source namespace config should reflect the updated instance config different from sourceNotifCfg
984+
assert.NotEqual(t, customInstanceCfg.Spec.Context, sourceNotifCfg.Spec.Context)
985+
}
986+
987+
func TestReconcileNotifications_NotificationsConfigurationInSourceNamespaceWhenDisabled(t *testing.T) {
988+
logf.SetLogger(ZapLogger(true))
989+
sourceNamespace := "ns1"
990+
a := makeTestArgoCD(func(a *argoproj.ArgoCD) {
991+
a.Spec.Notifications.Enabled = false
992+
a.Spec.Notifications.SourceNamespaces = []string{sourceNamespace}
993+
a.Spec.SourceNamespaces = []string{sourceNamespace}
994+
})
995+
996+
resObjs := []client.Object{a}
997+
subresObjs := []client.Object{a}
998+
runtimeObjs := []runtime.Object{}
999+
sch := makeTestReconcilerScheme(argoproj.AddToScheme)
1000+
err := v1alpha1.AddToScheme(sch)
1001+
assert.NoError(t, err)
1002+
cl := makeTestReconcilerClient(sch, resObjs, subresObjs, runtimeObjs)
1003+
r := makeTestReconciler(cl, sch, testclient.NewSimpleClientset())
1004+
1005+
// Create the source namespace
1006+
err = createNamespace(r, sourceNamespace, "")
1007+
assert.NoError(t, err)
1008+
1009+
// Reconcile should not create NotificationsConfiguration CR when notifications are disabled
1010+
err = r.reconcileSourceNamespaceNotificationsConfigurationCR(a, sourceNamespace)
1011+
assert.NoError(t, err)
1012+
1013+
// Verify NotificationsConfiguration CR does not exist
1014+
notifCfg := &v1alpha1.NotificationsConfiguration{}
1015+
err = r.Get(context.TODO(), types.NamespacedName{
1016+
Name: DefaultNotificationsConfigurationInstanceName,
1017+
Namespace: sourceNamespace,
1018+
}, notifCfg)
1019+
assert.Error(t, err)
1020+
assert.True(t, errors.IsNotFound(err))
1021+
}
1022+
1023+
func TestReconcileNotifications_SourceNamespaceResourcesIncludeNotificationsConfiguration(t *testing.T) {
1024+
logf.SetLogger(ZapLogger(true))
1025+
sourceNamespace := "ns1"
1026+
a := makeTestArgoCD(func(a *argoproj.ArgoCD) {
1027+
a.Spec.Notifications.Enabled = true
1028+
a.Spec.Notifications.SourceNamespaces = []string{sourceNamespace}
1029+
a.Spec.SourceNamespaces = []string{sourceNamespace}
1030+
})
1031+
1032+
resObjs := []client.Object{a}
1033+
subresObjs := []client.Object{a}
1034+
runtimeObjs := []runtime.Object{}
1035+
sch := makeTestReconcilerScheme(argoproj.AddToScheme)
1036+
err := v1alpha1.AddToScheme(sch)
1037+
assert.NoError(t, err)
1038+
cl := makeTestReconcilerClient(sch, resObjs, subresObjs, runtimeObjs)
1039+
r := makeTestReconciler(cl, sch, testclient.NewSimpleClientset())
1040+
1041+
// Create the source namespace
1042+
err = createNamespace(r, sourceNamespace, "")
1043+
assert.NoError(t, err)
1044+
1045+
// Reconcile source namespace resources (this should create NotificationsConfiguration CR)
1046+
err = r.reconcileNotificationsSourceNamespacesResources(a)
1047+
assert.NoError(t, err)
1048+
1049+
// Verify NotificationsConfiguration CR was created in source namespace
1050+
notifCfg := &v1alpha1.NotificationsConfiguration{}
1051+
err = r.Get(context.TODO(), types.NamespacedName{
1052+
Name: DefaultNotificationsConfigurationInstanceName,
1053+
Namespace: sourceNamespace,
1054+
}, notifCfg)
1055+
assert.NoError(t, err)
1056+
assert.Equal(t, DefaultNotificationsConfigurationInstanceName, notifCfg.Name)
1057+
assert.Equal(t, sourceNamespace, notifCfg.Namespace)
1058+
}

0 commit comments

Comments
 (0)