@@ -17,6 +17,7 @@ package argocd
1717import (
1818 "context"
1919 "fmt"
20+ "maps"
2021 "strings"
2122 "testing"
2223 "time"
@@ -600,3 +601,213 @@ func TestReconcileArgoCD_Cleanup_RBACs_When_NamespaceManagement_Disabled(t *test
600601 assert .NoError (t , err )
601602 assert .Equal (t , "" , string (updatedSecret .Data ["namespaces" ]))
602603}
604+
605+ func Test_restoreTrackingLabelsForOrphanedNamespaces (t * testing.T ) {
606+ ctx := context .Background ()
607+ // Test setup
608+ argocd := & argoproj.ArgoCD {
609+ ObjectMeta : metav1.ObjectMeta {
610+ Name : "argocd" ,
611+ Namespace : "argocd" ,
612+ },
613+ }
614+
615+ nsWithoutAppsetLabel := & corev1.Namespace {
616+ ObjectMeta : metav1.ObjectMeta {
617+ Name : "ns-without-appset-label" ,
618+ Labels : map [string ]string {
619+ // intentionally missing appset tracking label
620+ common .ArgoCDManagedByClusterArgoCDLabel : argocd .Namespace ,
621+ },
622+ },
623+ }
624+
625+ nsWithoutAppsetLabelButWildcard := & corev1.Namespace {
626+ ObjectMeta : metav1.ObjectMeta {
627+ Name : "ns-without-appset-label-but-wildcard" ,
628+ Labels : map [string ]string {
629+ // intentionally missing appset tracking label
630+ common .ArgoCDManagedByClusterArgoCDLabel : argocd .Namespace ,
631+ },
632+ },
633+ }
634+
635+ nsWithAppsetLabel := & corev1.Namespace {
636+ ObjectMeta : metav1.ObjectMeta {
637+ Name : "ns-with-appset-label" ,
638+ Labels : map [string ]string {
639+ common .ArgoCDManagedByClusterArgoCDLabel : argocd .Namespace ,
640+ common .ArgoCDApplicationSetManagedByClusterArgoCDLabel : argocd .Namespace ,
641+ },
642+ },
643+ }
644+
645+ nsRandom := & corev1.Namespace {
646+ ObjectMeta : metav1.ObjectMeta {
647+ Name : "random-ns" ,
648+ Labels : map [string ]string {
649+ common .ArgoCDManagedByClusterArgoCDLabel : argocd .Namespace ,
650+ common .ArgoCDApplicationSetManagedByClusterArgoCDLabel : argocd .Namespace ,
651+ },
652+ },
653+ }
654+
655+ // ArgoCD instance namespace (must never be mutated)
656+ argocdNamespace := & corev1.Namespace {
657+ ObjectMeta : metav1.ObjectMeta {
658+ Name : argocd .Namespace ,
659+ },
660+ }
661+
662+ // RBAC rules required for orphan validation
663+ appScopedRules := []rbacv1.PolicyRule {
664+ {
665+ APIGroups : []string {"argoproj.io" },
666+ Resources : []string {"applicationsets" },
667+ Verbs : []string {"get" , "list" },
668+ },
669+ }
670+
671+ // Roles
672+ appsetRoleName := getResourceNameForApplicationSetSourceNamespaces (argocd )
673+
674+ // AppSet role in namespace that already has the label
675+ appsetRoleInLabelledNS := & rbacv1.Role {
676+ ObjectMeta : metav1.ObjectMeta {
677+ Name : appsetRoleName ,
678+ Namespace : nsWithAppsetLabel .Name ,
679+ Labels : map [string ]string {
680+ common .ArgoCDKeyManagedBy : argocd .Name ,
681+ common .ArgoCDKeyPartOf : common .ArgoCDAppName ,
682+ },
683+ },
684+ Rules : appScopedRules ,
685+ }
686+
687+ // AppSet role in namespace missing the label (should trigger restore)
688+ appsetRoleInUnlabelledNS := & rbacv1.Role {
689+ ObjectMeta : metav1.ObjectMeta {
690+ Name : appsetRoleName ,
691+ Namespace : nsWithoutAppsetLabel .Name ,
692+ Labels : map [string ]string {
693+ common .ArgoCDKeyManagedBy : argocd .Name ,
694+ common .ArgoCDKeyPartOf : common .ArgoCDAppName ,
695+ },
696+ },
697+ Rules : appScopedRules ,
698+ }
699+
700+ // AppSet role in namespace missing the label (should trigger restore) but having resources has *
701+ appSetRoleInUnlabelledNSWithWildcard := & rbacv1.Role {
702+ ObjectMeta : metav1.ObjectMeta {
703+ Name : appsetRoleName ,
704+ Namespace : nsWithoutAppsetLabelButWildcard .Name ,
705+ Labels : map [string ]string {
706+ common .ArgoCDKeyManagedBy : argocd .Name ,
707+ common .ArgoCDKeyPartOf : common .ArgoCDAppName ,
708+ },
709+ },
710+ Rules : []rbacv1.PolicyRule {
711+ {
712+ APIGroups : []string {"argoproj.io" },
713+ Resources : []string {"*" },
714+ Verbs : []string {"get" , "list" },
715+ },
716+ },
717+ }
718+
719+ // Same role name but in ArgoCD namespace (must be ignored)
720+ roleInArgoCDNS := & rbacv1.Role {
721+ ObjectMeta : metav1.ObjectMeta {
722+ Name : appsetRoleName ,
723+ Namespace : argocd .Namespace ,
724+ Labels : map [string ]string {
725+ common .ArgoCDKeyManagedBy : argocd .Name ,
726+ common .ArgoCDKeyPartOf : common .ArgoCDAppName ,
727+ },
728+ },
729+ Rules : appScopedRules ,
730+ }
731+
732+ // Completely unrelated role (no rules, should be ignored)
733+ randomRole := & rbacv1.Role {
734+ ObjectMeta : metav1.ObjectMeta {
735+ Name : "random-role" ,
736+ Namespace : nsRandom .Name ,
737+ Labels : map [string ]string {
738+ common .ArgoCDKeyManagedBy : argocd .Name ,
739+ common .ArgoCDKeyPartOf : common .ArgoCDAppName ,
740+ },
741+ },
742+ }
743+
744+ // Fake client + reconciler
745+ resObjs := []client.Object {
746+ argocd ,
747+ nsWithoutAppsetLabel ,
748+ nsWithoutAppsetLabelButWildcard ,
749+ nsWithAppsetLabel ,
750+ nsRandom ,
751+ argocdNamespace ,
752+ appsetRoleInLabelledNS ,
753+ appsetRoleInUnlabelledNS ,
754+ appSetRoleInUnlabelledNSWithWildcard ,
755+ roleInArgoCDNS ,
756+ randomRole ,
757+ }
758+
759+ subresObjs := append ([]client.Object {}, resObjs ... )
760+ runtimeObjs := []runtime.Object {}
761+
762+ scheme := makeTestReconcilerScheme (argoproj .AddToScheme )
763+ cl := makeTestReconcilerClient (scheme , resObjs , subresObjs , runtimeObjs )
764+ kubeClient := testclient .NewSimpleClientset ()
765+
766+ reconciler := makeTestReconciler (cl , scheme , kubeClient )
767+
768+ // Capture original labels for immutability assertions
769+ origWithout := maps .Clone (nsWithoutAppsetLabel .Labels )
770+ origWithoutButWildcard := maps .Clone (nsWithoutAppsetLabelButWildcard .Labels )
771+ origWith := maps .Clone (nsWithAppsetLabel .Labels )
772+ origRandom := maps .Clone (nsRandom .Labels )
773+
774+ // Execute
775+ err := reconciler .restoreTrackingLabelsForOrphanedNamespaces (ctx , argocd )
776+ assert .NoError (t , err )
777+
778+ // Assertions
779+ // 1) Namespace with AppSet resources but missing label -> label restored
780+ updatedWithout := & corev1.Namespace {}
781+ assert .NoError (t , cl .Get (ctx , types.NamespacedName {Name : nsWithoutAppsetLabel .Name }, updatedWithout ))
782+
783+ assert .Equal (t , origWithout [common .ArgoCDManagedByClusterArgoCDLabel ], updatedWithout .Labels [common .ArgoCDManagedByClusterArgoCDLabel ])
784+
785+ assert .Equal (t , argocd .Namespace , updatedWithout .Labels [common .ArgoCDApplicationSetManagedByClusterArgoCDLabel ], "expected appset tracking label to be restored" )
786+
787+ // 1b) Namespace with wildcard resources but missing label -> label restored
788+ updatedWithoutButWildcard := & corev1.Namespace {}
789+ assert .NoError (t , cl .Get (ctx , types.NamespacedName {Name : nsWithoutAppsetLabelButWildcard .Name }, updatedWithoutButWildcard ))
790+
791+ assert .Equal (t , origWithoutButWildcard [common .ArgoCDManagedByClusterArgoCDLabel ], updatedWithoutButWildcard .Labels [common .ArgoCDManagedByClusterArgoCDLabel ])
792+
793+ assert .Equal (t , argocd .Namespace , updatedWithoutButWildcard .Labels [common .ArgoCDApplicationSetManagedByClusterArgoCDLabel ], "expected appset tracking label to be restored" )
794+
795+ // 2) Namespace already labelled -> unchanged
796+ updatedWith := & corev1.Namespace {}
797+ assert .NoError (t , cl .Get (ctx , types.NamespacedName {Name : nsWithAppsetLabel .Name }, updatedWith ))
798+ assert .Equal (t , origWith , updatedWith .Labels )
799+
800+ // 3) Namespace with unrelated role -> unchanged
801+ updatedRandom := & corev1.Namespace {}
802+ assert .NoError (t , cl .Get (ctx , types.NamespacedName {Name : nsRandom .Name }, updatedRandom ))
803+ assert .Equal (t , origRandom , updatedRandom .Labels )
804+
805+ // 4) ArgoCD instance namespace -> never labeled
806+ updatedArgoCDNS := & corev1.Namespace {}
807+ assert .NoError (t , cl .Get (ctx , types.NamespacedName {Name : argocd .Namespace }, updatedArgoCDNS ))
808+
809+ if updatedArgoCDNS .Labels != nil {
810+ assert .NotContains (t , updatedArgoCDNS .Labels , common .ArgoCDApplicationSetManagedByClusterArgoCDLabel )
811+ assert .NotContains (t , updatedArgoCDNS .Labels , common .ArgoCDManagedByClusterArgoCDLabel )
812+ }
813+ }
0 commit comments