@@ -19,6 +19,7 @@ import (
1919 "context"
2020 "crypto/rand"
2121 "encoding/base64"
22+ "encoding/json"
2223 "fmt"
2324 "os"
2425 "reflect"
@@ -1089,7 +1090,7 @@ func (r *ReconcileArgoCD) setResourceWatches(bldr *builder.Builder, clusterResou
10891090 }
10901091
10911092 // Watch for changes to primary resource ArgoCD
1092- bldr .For (& argoproj.ArgoCD {}, builder .WithPredicates (deleteSSOPred , deleteNotificationsPred , r .argoCDNamespaceManagementFilterPredicate ()))
1093+ bldr .For (& argoproj.ArgoCD {}, builder .WithPredicates (deleteSSOPred , deleteNotificationsPred , r .argoCDNamespaceManagementFilterPredicate (), r . argoCDSpecLabelCleanupPredicate () ))
10931094
10941095 // Watch for changes to ConfigMap sub-resources owned by ArgoCD instances.
10951096 bldr .Owns (& corev1.ConfigMap {})
@@ -1310,6 +1311,288 @@ func (r *ReconcileArgoCD) namespaceFilterPredicate() predicate.Predicate {
13101311 }
13111312}
13121313
1314+ // argoCDSpecLabelCleanupPredicate tracks label changes in ArgoCD CR specs and cleans up removed labels from managed resources
1315+ func (r * ReconcileArgoCD ) argoCDSpecLabelCleanupPredicate () predicate.Predicate {
1316+ return predicate.Funcs {
1317+ UpdateFunc : func (e event.UpdateEvent ) bool {
1318+ newCR , ok := e .ObjectNew .(* argoproj.ArgoCD )
1319+ if ! ok {
1320+ return false
1321+ }
1322+ oldCR , ok := e .ObjectOld .(* argoproj.ArgoCD )
1323+ if ! ok {
1324+ return false
1325+ }
1326+
1327+ // Track removed labels from each component spec
1328+ removedLabels := r .calculateRemovedSpecLabels (oldCR , newCR )
1329+
1330+ if len (removedLabels ) > 0 {
1331+ log .Info ("Detected removed labels from ArgoCD spec" ,
1332+ "argocd" , fmt .Sprintf ("%s/%s" , newCR .Namespace , newCR .Name ),
1333+ "removedLabels" , removedLabels )
1334+
1335+ // Store removed labels for cleanup during reconciliation
1336+ if err := r .storeRemovedLabelsForCleanup (newCR , removedLabels ); err != nil {
1337+ log .Error (err , "failed to store removed labels for cleanup" )
1338+ }
1339+ }
1340+
1341+ return true // Trigger reconciliation for ArgoCD updates
1342+ },
1343+ }
1344+ }
1345+
1346+ const RemovedLabelsAnnotation = "argocd.argoproj.io/removed-labels"
1347+
1348+ // calculateRemovedSpecLabels compares old and new ArgoCD specs and returns labels that were removed
1349+ func (r * ReconcileArgoCD ) calculateRemovedSpecLabels (oldCR , newCR * argoproj.ArgoCD ) map [string ]string {
1350+ // Add nil checks to prevent panic
1351+ if r == nil || oldCR == nil || newCR == nil {
1352+ return map [string ]string {}
1353+ }
1354+ removedLabels := make (map [string ]string )
1355+
1356+ // Check Server labels
1357+ oldServerLabels := oldCR .Spec .Server .Labels
1358+ newServerLabels := newCR .Spec .Server .Labels
1359+ for key , value := range oldServerLabels {
1360+ if newServerLabels == nil || newServerLabels [key ] == "" {
1361+ removedLabels [key ] = value
1362+ }
1363+ }
1364+
1365+ // Check Repo Server labels
1366+ oldRepoLabels := oldCR .Spec .Repo .Labels
1367+ newRepoLabels := newCR .Spec .Repo .Labels
1368+ for key , value := range oldRepoLabels {
1369+ if newRepoLabels == nil || newRepoLabels [key ] == "" {
1370+ removedLabels [key ] = value
1371+ }
1372+ }
1373+
1374+ // Check Controller labels
1375+ oldControllerLabels := oldCR .Spec .Controller .Labels
1376+ newControllerLabels := newCR .Spec .Controller .Labels
1377+ for key , value := range oldControllerLabels {
1378+ if newControllerLabels == nil || newControllerLabels [key ] == "" {
1379+ removedLabels [key ] = value
1380+ }
1381+ }
1382+
1383+ // Check ApplicationSet labels
1384+ if oldCR .Spec .ApplicationSet != nil && newCR .Spec .ApplicationSet != nil {
1385+ oldAppSetLabels := oldCR .Spec .ApplicationSet .Labels
1386+ newAppSetLabels := newCR .Spec .ApplicationSet .Labels
1387+ for key , value := range oldAppSetLabels {
1388+ if newAppSetLabels == nil || newAppSetLabels [key ] == "" {
1389+ removedLabels [key ] = value
1390+ }
1391+ }
1392+ }
1393+
1394+ // Note: Notifications and Prometheus specs don't have Labels fields currently
1395+ // If they are added in the future, the logic can be uncommented and updated
1396+
1397+ return removedLabels
1398+ }
1399+
1400+ func (r * ReconcileArgoCD ) storeRemovedLabelsForCleanup (obj client.Object , removedLabels map [string ]string ) error {
1401+ if len (removedLabels ) == 0 {
1402+ return nil
1403+ }
1404+
1405+ // Serialize removed labels to JSON
1406+ removedLabelsJSON , err := json .Marshal (removedLabels )
1407+ if err != nil {
1408+ return fmt .Errorf ("failed to marshal removed labels: %w" , err )
1409+ }
1410+
1411+ // Add annotation with removed labels
1412+ annotations := obj .GetAnnotations ()
1413+ if annotations == nil {
1414+ annotations = make (map [string ]string )
1415+ }
1416+ annotations [RemovedLabelsAnnotation ] = string (removedLabelsJSON )
1417+ obj .SetAnnotations (annotations )
1418+
1419+ // Update the object
1420+ return r .Client .Update (context .TODO (), obj )
1421+ }
1422+
1423+ func (r * ReconcileArgoCD ) processRemovedLabels (argocd * argoproj.ArgoCD ) error {
1424+ annotations := argocd .GetAnnotations ()
1425+ if annotations == nil {
1426+ return nil
1427+ }
1428+
1429+ removedLabelsJSON , exists := annotations [RemovedLabelsAnnotation ]
1430+ if ! exists {
1431+ return nil
1432+ }
1433+
1434+ var removedLabels map [string ]string
1435+ if err := json .Unmarshal ([]byte (removedLabelsJSON ), & removedLabels ); err != nil {
1436+ return fmt .Errorf ("failed to unmarshal removed labels: %w" , err )
1437+ }
1438+
1439+ if len (removedLabels ) > 0 {
1440+ // Clean up from managed resources
1441+ if err := r .cleanupLabelsFromManagedResources (argocd , removedLabels ); err != nil {
1442+ return err
1443+ }
1444+
1445+ // Clear the annotation
1446+ delete (annotations , RemovedLabelsAnnotation )
1447+ argocd .SetAnnotations (annotations )
1448+ return r .Client .Update (context .TODO (), argocd )
1449+ }
1450+
1451+ return nil
1452+ }
1453+
1454+ func (r * ReconcileArgoCD ) cleanupLabelsFromManagedResources (argocd * argoproj.ArgoCD , labelsToRemove map [string ]string ) error {
1455+ log .Info ("Cleaning up removed labels from managed resources" ,
1456+ "argocd" , fmt .Sprintf ("%s/%s" , argocd .Namespace , argocd .Name ),
1457+ "labelsToRemove" , labelsToRemove )
1458+
1459+ // Clean up labels from ArgoCD Server Deployment
1460+ if err := r .cleanupLabelsFromComponent (argocd , labelsToRemove , "server" , & appsv1.Deployment {}); err != nil {
1461+ log .Error (err , "failed to cleanup labels from server deployment" )
1462+ }
1463+
1464+ // Clean up labels from ArgoCD Repo Server Deployment
1465+ if err := r .cleanupLabelsFromComponent (argocd , labelsToRemove , "repo-server" , & appsv1.Deployment {}); err != nil {
1466+ log .Error (err , "failed to cleanup labels from repo-server deployment" )
1467+ }
1468+
1469+ // Clean up labels from ArgoCD Application Controller StatefulSet
1470+ if err := r .cleanupLabelsFromComponent (argocd , labelsToRemove , "application-controller" , & appsv1.StatefulSet {}); err != nil {
1471+ log .Error (err , "failed to cleanup labels from application-controller statefulset" )
1472+ }
1473+
1474+ // Clean up labels from ArgoCD ApplicationSet Controller Deployment
1475+ if err := r .cleanupLabelsFromComponent (argocd , labelsToRemove , "applicationset-controller" , & appsv1.Deployment {}); err != nil {
1476+ log .Error (err , "failed to cleanup labels from applicationset-controller deployment" )
1477+ }
1478+
1479+ // Clean up labels from ArgoCD Notifications Controller Deployment (if enabled)
1480+ if argocd .Spec .Notifications .Enabled {
1481+ if err := r .cleanupLabelsFromComponent (argocd , labelsToRemove , "notifications-controller" , & appsv1.Deployment {}); err != nil {
1482+ log .Error (err , "failed to cleanup labels from notifications-controller deployment" )
1483+ }
1484+ }
1485+
1486+ // Use existing pattern for other managed resources
1487+ selector , err := argocdInstanceSelector (argocd .Name )
1488+ if err != nil {
1489+ return err
1490+ }
1491+
1492+ // Clean up ConfigMaps
1493+ configMapList := & corev1.ConfigMapList {}
1494+ if err := filterObjectsBySelector (r .Client , configMapList , selector ); err == nil {
1495+ for _ , cm := range configMapList .Items {
1496+ if err := r .removeLabelsFromObject (& cm , labelsToRemove ); err != nil {
1497+ log .Error (err , "failed to remove labels from ConfigMap" , "name" , cm .Name )
1498+ }
1499+ }
1500+ }
1501+
1502+ // Add other resource types as needed...
1503+ return nil
1504+ }
1505+
1506+ // cleanupLabelsFromComponent removes specified labels from a specific ArgoCD component
1507+ func (r * ReconcileArgoCD ) cleanupLabelsFromComponent (argocd * argoproj.ArgoCD , labelsToRemove map [string ]string , componentName string , obj client.Object ) error {
1508+ resourceName := fmt .Sprintf ("%s-%s" , argocd .Name , componentName )
1509+ key := types.NamespacedName {
1510+ Namespace : argocd .Namespace ,
1511+ Name : resourceName ,
1512+ }
1513+
1514+ if err := r .Client .Get (context .TODO (), key , obj ); err != nil {
1515+ if apierrors .IsNotFound (err ) {
1516+ // Component doesn't exist, nothing to clean up
1517+ return nil
1518+ }
1519+ return fmt .Errorf ("failed to get %s: %w" , componentName , err )
1520+ }
1521+
1522+ // Remove labels from the resource itself
1523+ if err := r .removeLabelsFromObject (obj , labelsToRemove ); err != nil {
1524+ return fmt .Errorf ("failed to remove labels from %s: %w" , componentName , err )
1525+ }
1526+
1527+ // For Deployments and StatefulSets, also clean labels from pod template
1528+ switch resource := obj .(type ) {
1529+ case * appsv1.Deployment :
1530+ podTemplateLabels := resource .Spec .Template .Labels
1531+ if podTemplateLabels != nil {
1532+ modified := false
1533+ for labelKey := range labelsToRemove {
1534+ if _ , exists := podTemplateLabels [labelKey ]; exists {
1535+ delete (podTemplateLabels , labelKey )
1536+ modified = true
1537+ }
1538+ }
1539+ if modified {
1540+ resource .Spec .Template .Labels = podTemplateLabels
1541+ if err := r .Client .Update (context .TODO (), resource ); err != nil {
1542+ return fmt .Errorf ("failed to update pod template labels for %s deployment: %w" , componentName , err )
1543+ }
1544+ log .Info ("Removed labels from pod template" ,
1545+ "component" , componentName ,
1546+ "removedLabels" , labelsToRemove )
1547+ }
1548+ }
1549+ case * appsv1.StatefulSet :
1550+ podTemplateLabels := resource .Spec .Template .Labels
1551+ if podTemplateLabels != nil {
1552+ modified := false
1553+ for labelKey := range labelsToRemove {
1554+ if _ , exists := podTemplateLabels [labelKey ]; exists {
1555+ delete (podTemplateLabels , labelKey )
1556+ modified = true
1557+ }
1558+ }
1559+ if modified {
1560+ resource .Spec .Template .Labels = podTemplateLabels
1561+ if err := r .Client .Update (context .TODO (), resource ); err != nil {
1562+ return fmt .Errorf ("failed to update pod template labels for %s statefulset: %w" , componentName , err )
1563+ }
1564+ log .Info ("Removed labels from pod template" ,
1565+ "component" , componentName ,
1566+ "removedLabels" , labelsToRemove )
1567+ }
1568+ }
1569+ }
1570+
1571+ return nil
1572+ }
1573+
1574+ func (r * ReconcileArgoCD ) removeLabelsFromObject (obj client.Object , labelsToRemove map [string ]string ) error {
1575+ currentLabels := obj .GetLabels ()
1576+ if currentLabels == nil {
1577+ return nil
1578+ }
1579+
1580+ modified := false
1581+ for labelKey := range labelsToRemove {
1582+ if _ , exists := currentLabels [labelKey ]; exists {
1583+ delete (currentLabels , labelKey )
1584+ modified = true
1585+ }
1586+ }
1587+
1588+ if modified {
1589+ obj .SetLabels (currentLabels )
1590+ return r .Client .Update (context .TODO (), obj )
1591+ }
1592+
1593+ return nil
1594+ }
1595+
13131596// deleteRBACsForNamespace deletes the RBACs when the label from the namespace is removed.
13141597func deleteRBACsForNamespace (sourceNS string , k8sClient kubernetes.Interface ) error {
13151598 log .Info (fmt .Sprintf ("Removing the RBACs created for the namespace: %s" , sourceNS ))
0 commit comments