Skip to content

Conversation

@alkakumari016
Copy link
Contributor

@alkakumari016 alkakumari016 commented Nov 10, 2025

What type of PR is this?

What does this PR do / why we need it:
Delete custom labels from live object when deleted from CR
Have you updated the necessary documentation?

  • Documentation update is required by this PR.
  • Documentation has been updated.

Which issue(s) this PR fixes:

Fixes #?
https://issues.redhat.com/browse/GITOPS-7405
How to test changes / Special notes to the reviewer:

Summary by CodeRabbit

Release Notes

  • New Features

    • Automatic cleanup of labels and annotations removed from ArgoCD resource specifications
  • Bug Fixes

    • Improved metadata synchronization to use incremental updates instead of wholesale overwrites, better preserving operator-managed labels and annotations
    • Enhanced label and annotation handling across deployments and stateful sets for more efficient resource management
  • Tests

    • Added test coverage for label and annotation removal and preservation scenarios across ArgoCD components

alkakumari016 and others added 11 commits November 7, 2025 11:49
…rator labels (argoproj-labs#1737)

* GITOPS-6285: reconcile operator owned labels for configMap and ignore non operator labels

Signed-off-by: Alka Kumari <[email protected]>

* GITOPS-6285: added reconcilialtion logic for labels

Signed-off-by: Alka Kumari <[email protected]>

* Removed a typo that resulted in test case failures

Signed-off-by: Alka Kumari <[email protected]>

* GITOPS-6285: error handling for r.Client.Update

Signed-off-by: Alka Kumari <[email protected]>

* GITOPS-6285: use r.Update

Signed-off-by: Alka Kumari <[email protected]>

* GITOPS-6285: changed func name to UpdateMapValues and added test cases

Signed-off-by: Alka Kumari <[email protected]>

* GITOPS-6285: removed addKubernetesData and replaced the references with UpdateMapValues

Signed-off-by: Alka Kumari <[email protected]>

GITOPS-6285: removed addKubernetesData and replaced the references with UpdateMapValues

* GITOPS-6285: removed labels update logic from applicationset and statefulset

Signed-off-by: Alka Kumari <[email protected]>

* GITOPS-6285: changed comments

Signed-off-by: Alka Kumari <[email protected]>

---------

Signed-off-by: Alka Kumari <[email protected]>
Signed-off-by: Alka Kumari <[email protected]>
…o ginkgo tests for checking custom labels and annotations

Signed-off-by: Alka Kumari <[email protected]>
@coderabbitai
Copy link

coderabbitai bot commented Nov 10, 2025

Walkthrough

The PR refactors metadata synchronization across ArgoCD reconciliation by replacing wholesale map replacements with incremental updates via a new UpdateMapValues helper. It introduces a centralized, thread-safe mechanism to track and clean up removed labels/annotations from resources, with enhanced ConfigMap label reconciliation and improved test coverage for metadata management.

Changes

Cohort / File(s) Summary
Core Metadata Update Infrastructure
controllers/argocd/util.go
Introduced new DropMetadata type, thread-safe DropMetadataStore with mutex, and cleanup functions (addDropMetadata, getDropMetadataForNamespace, removeDropMetadataForNamespace, processDropMetadataForCleanup). Added UpdateMapValues helper to incrementally update maps and detect changes. Implemented per-resource cleanup logic for Deployments and StatefulSets. Added argoCDSpecLabelAndAnnotationCleanupPredicate to watch ArgoCD spec changes and calculateRemovedSpecLabelsAndAnnotations to track removed metadata keys.
Metadata Synchronization Refactoring
controllers/argocd/applicationset.go, controllers/argocd/deployment.go, controllers/argocd/repo_server.go, controllers/argocd/role.go, controllers/argocd/statefulset.go
Replaced direct metadata copying and DeepEqual checks with UpdateMapValues-based incremental updates for labels and annotations. Changes remove wholesale map assignments in favor of targeted value updates that preserve existing custom metadata.
ConfigMap Label Reconciliation
controllers/argocd/configmap.go
Added pre-check logic in reconcileCAConfigMap and reconcileArgoConfigMap to detect and update ConfigMaps when labels differ from desired state.
Controller Integration
controllers/argocd/argocd_controller.go
Inserted two calls to processDropMetadataForCleanup within internalReconcile to execute metadata cleanup processing around reconciliation paths.
Test Updates
controllers/argocd/util_test.go
Renamed TestRetainKubernetesData to TestUpdateMapValue. Updated test table field from live to existing and changed helper invocation from addKubernetesData to UpdateMapValues.
Integration Tests
tests/ginkgo/parallel/1-043_validate_custom_labels_annotations_test.go
Enhanced test to validate selective removal and verification of labels/annotations across multiple ArgoCD components. Modified expectLabelsAndAnnotationsRemovedFromDepl signature to accept component name for per-component assertions. Added test for concurrent multi-instance isolation.

Sequence Diagram(s)

sequenceDiagram
    participant AC as ArgoCD Controller
    participant Reconcile as Reconcile Pipeline
    participant Cleanup as processDropMetadataForCleanup
    participant KubeAPI as Kubernetes API

    AC->>Reconcile: internalReconcile()
    Reconcile->>Cleanup: processDropMetadataForCleanup (call 1)
    Cleanup->>KubeAPI: Query DropMetadataStore
    Cleanup->>KubeAPI: Remove labels/annotations from Deployments
    Cleanup->>KubeAPI: Remove labels/annotations from StatefulSets
    
    Reconcile->>Reconcile: Namespace Management cleanup
    Reconcile->>Cleanup: processDropMetadataForCleanup (call 2)
    Cleanup->>KubeAPI: Query DropMetadataStore
    Cleanup->>KubeAPI: Remove labels/annotations from resources
    
    Reconcile->>Reconcile: reconcileResources()
    Note over Reconcile: UpdateMapValues used for incremental<br/>label/annotation updates
    Reconcile->>KubeAPI: Update Deployments/StatefulSets with<br/>merged metadata changes
Loading
sequenceDiagram
    participant ArgoCD as ArgoCD Spec Change
    participant Watcher as Resource Watches
    participant Predicate as argoCDSpecLabelAndAnnotationCleanupPredicate
    participant Calculator as calculateRemovedSpecLabelsAndAnnotations
    participant Store as DropMetadataStore

    ArgoCD->>Watcher: Spec updated (labels/annotations removed)
    Watcher->>Predicate: Evaluate update event
    Predicate->>Calculator: Compare old vs new spec
    Calculator->>Store: addDropMetadata() for each removed key
    Store->>Store: Thread-safe store update (mutex locked)
    Note over Store: Keys queued for cleanup in next<br/>reconcile cycle
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~35 minutes

  • Metadata update pattern consistency: Verify UpdateMapValues is correctly applied across all 5 controller files with proper change-detection logic and logging
  • Thread-safety of DropMetadata mechanism: Review mutex locking patterns in DropMetadataStore access, especially in concurrent cleanup scenarios and predicate evaluation
  • Dual cleanup calls in controller: Confirm the two processDropMetadataForCleanup calls in internalReconcile do not cause redundant cleanup operations or race conditions
  • ConfigMap label update logic: Ensure pre-check comparisons and update paths align with existing reconciliation patterns
  • Test coverage alignment: Verify test updates in util_test.go and integration tests correctly validate the new incremental update semantics

Poem

🐰 Hop and bounce, metadata's refined,
No more wholesale copies left behind,
Incremental updates, clean and tight,
Labels dance right, cleanup done right,
Thread-safe with care, the store's in sight!

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly and clearly describes the main functional change: removing custom labels from live Kubernetes objects when they are deleted from the ArgoCD CR, which aligns with the primary objective of this pull request.
Docstring Coverage ✅ Passed Docstring coverage is 90.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Comment @coderabbitai help to get the list of available commands and usage tips.

@alkakumari016
Copy link
Contributor Author

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Nov 10, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
controllers/argocd/applicationset.go (1)

279-299: Bug: annotation/label changes are not persisted

You mutate existing.Spec.Template.Annotations/Labels via UpdateMapValues but never set changed or call Update when these are the only diffs. identifyDeploymentDifference runs after the mutation, so it won’t detect that change either.

Apply this diff to track and persist these changes:

@@
-    //Check if annotations have changed
-    UpdateMapValues(&existing.Spec.Template.Annotations, deploy.Spec.Template.Annotations)
+    // Check annotation/label changes up-front
+    annChanged := UpdateMapValues(&existing.Spec.Template.Annotations, deploy.Spec.Template.Annotations)
@@
-    deploymentsDifferent := identifyDeploymentDifference(*existing, *deploy)
-    if len(deploymentsDifferent) > 0 {
+    deploymentsDifferent := identifyDeploymentDifference(*existing, *deploy)
+    if len(deploymentsDifferent) > 0 {
         existing.Spec.Template.Spec.Containers = podSpec.Containers
         existing.Spec.Template.Spec.Volumes = podSpec.Volumes
         existing.Spec.Template.Spec.ServiceAccountName = podSpec.ServiceAccountName
-        UpdateMapValues(&existing.Labels, deploy.Labels)
-        UpdateMapValues(&existing.Spec.Template.Labels, deploy.Spec.Template.Labels)
+        lblChanged1 := UpdateMapValues(&existing.Labels, deploy.Labels)
+        lblChanged2 := UpdateMapValues(&existing.Spec.Template.Labels, deploy.Spec.Template.Labels)
@@
-        argoutil.LogResourceUpdate(log, existing, "due to difference in", deploymentsDifferent)
-        return r.Update(context.TODO(), existing)
-    }
-    return nil // Deployment found with nothing to do, move along...
+        argoutil.LogResourceUpdate(log, existing, "due to difference in", deploymentsDifferent)
+        return r.Update(context.TODO(), existing)
+    }
+    // No structural diffs, but annotations/labels may have changed
+    lblChanged1 := UpdateMapValues(&existing.Labels, deploy.Labels)
+    lblChanged2 := UpdateMapValues(&existing.Spec.Template.Labels, deploy.Spec.Template.Labels)
+    if annChanged || lblChanged1 || lblChanged2 {
+        explanation := "annotations"
+        if lblChanged1 || lblChanged2 {
+            explanation = explanation + ", labels"
+        }
+        argoutil.LogResourceUpdate(log, existing, "updating", explanation)
+        return r.Update(context.TODO(), existing)
+    }
+    return nil // nothing to do
🧹 Nitpick comments (1)
controllers/argocd/configmap.go (1)

346-364: Minor: simplify and fix log message

The nested changed check is redundant, and the error says “service object”. Use “configmap object”.

Apply this diff:

@@
-    if exists {
-        changed := false
-        //Check if labels have changed
-        if UpdateMapValues(&existingCM.Labels, cm.GetLabels()) {
-            argoutil.LogResourceUpdate(log, existingCM, "updating", "CAConfigMap labels")
-            changed = true
-            if changed {
-                if err := r.Update(context.TODO(), existingCM); err != nil {
-                    log.Error(err, "failed to update service object")
-                }
-            }
-        }
-    }
+    if exists {
+        if UpdateMapValues(&existingCM.Labels, cm.GetLabels()) {
+            argoutil.LogResourceUpdate(log, existingCM, "updating", "CAConfigMap labels")
+            if err := r.Update(context.TODO(), existingCM); err != nil {
+                log.Error(err, "failed to update configmap object")
+            }
+        }
+    }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2a10f87 and 93af23d.

📒 Files selected for processing (10)
  • controllers/argocd/applicationset.go (1 hunks)
  • controllers/argocd/argocd_controller.go (1 hunks)
  • controllers/argocd/configmap.go (2 hunks)
  • controllers/argocd/deployment.go (1 hunks)
  • controllers/argocd/repo_server.go (1 hunks)
  • controllers/argocd/role.go (2 hunks)
  • controllers/argocd/statefulset.go (1 hunks)
  • controllers/argocd/util.go (5 hunks)
  • controllers/argocd/util_test.go (2 hunks)
  • tests/ginkgo/parallel/1-043_validate_custom_labels_annotations_test.go (4 hunks)
🔇 Additional comments (13)
controllers/argocd/argocd_controller.go (1)

351-353: Wiring looks good

Invoking metadata cleanup once per reconcile before sub-resource reconciliation is reasonable and side‑effect free when there’s nothing queued.

controllers/argocd/role.go (1)

517-524: Good switch to incremental label updates

Using UpdateMapValues prevents wholesale map replacement and avoids clobbering foreign labels.

Also applies to: 537-544

controllers/argocd/util_test.go (1)

1172-1256: Solid coverage for UpdateMapValues behavior

Tests exercise retain/override/empty cases and match the new semantics.

controllers/argocd/util.go (1)

1948-1962: LGTM: helper is simple and safe

Map is initialized defensively and updates are idempotent.

controllers/argocd/repo_server.go (1)

516-531: Good: incremental metadata updates

Switching to UpdateMapValues for annotations and labels avoids clobbering unrelated keys.

controllers/argocd/configmap.go (1)

556-561: LGTM: labels reconciled incrementally on argocd-cm

Using UpdateMapValues avoids destructive replacement.

controllers/argocd/statefulset.go (1)

987-1002: LGTM! Consistent refactoring across StatefulSet reconciliation.

The changes mirror the refactoring in deployment.go, applying the same incremental metadata update pattern to StatefulSet resources. This ensures consistent behavior across Deployment and StatefulSet reconciliation paths.

tests/ginkgo/parallel/1-043_validate_custom_labels_annotations_test.go (5)

146-294: Excellent test coverage for selective label removal.

The test thoroughly validates:

  • Selective removal of labels from individual components (server, repo, controller, appset)
  • Cross-component isolation (changes to one component don't affect others)
  • Preservation of remaining labels after selective removal

The use of Eventually with detailed GinkgoWriter output provides good debugging information if tests fail.


295-389: LGTM! Comprehensive annotation removal testing.

The annotation removal tests mirror the structure of the label removal tests, providing consistent coverage across both metadata types. This validates that UpdateMapValues works correctly for both annotations and labels.


414-448: Good defensive testing with operator-managed label verification.

The addition of the componentName parameter improves error messages, and the verification that operator-managed labels (like app.kubernetes.io/name) are preserved is an important safeguard. This ensures that UpdateMapValues correctly distinguishes between custom and operator-managed metadata.


461-478: LGTM! Tests label re-addition after removal.

This test validates the complete lifecycle (add → remove → add) and ensures that the removal mechanism doesn't leave the system in a state where labels cannot be added back.


480-744: Thread-safe DropMetadataStore implementation is correctly protected with proper mutex patterns.

Verification confirms the DropMetadataStore has complete synchronization:

  • Write operations (add/delete): Protected by Lock()
  • Read operations: Protected by RLock()
  • All three access functions (addDropMetadata, getDropMetadataForNamespace, removeDropMetadataForNamespace) properly defer unlock

The test itself appropriately uses Eventually for retry logic to accommodate goroutine timing. The concurrent test at lines 480-744 effectively validates per-namespace isolation with the alternating removal pattern (even vs odd instances), and the verification confirms operator-managed labels remain intact under concurrent operations.

No additional synchronization concerns identified in the test or implementation.

controllers/argocd/deployment.go (1)

1252-1267: Incorrect characterization of UpdateMapValues behavior.

The review comment claims the code "allows removal of custom labels/annotations from the live object when they are deleted from the CR spec." However, UpdateMapValues (controllers/argocd/util.go:1950–1962) is additive only—it updates and adds keys from the source map but does not remove keys from the existing map that are absent in the source.

Keys existing in the live deployment but missing from the CR spec will be preserved, not removed. This aligns with the existing code comment at line 1260: "Preserve non-operator labels in the existing deployment."

The thread-safety concern in the review is valid: UpdateMapValues lacks synchronization primitives and is unsafe for concurrent reconciliation loops.

Likely an incorrect or invalid review comment.

Comment on lines +1329 to +1357
func getDropMetadataForNamespace(namespace string) []DropMetadata {
DropMetadataStoreMutex.RLock()
defer DropMetadataStoreMutex.RUnlock()
return DropMetadataStore[namespace]
}

// removeDropMetadataForNamespace removes all DropMetadata for a specific namespace
func removeDropMetadataForNamespace(namespace string) {
DropMetadataStoreMutex.Lock()
defer DropMetadataStoreMutex.Unlock()
delete(DropMetadataStore, namespace)
}

// processDropMetadataForCleanup processes the DropMetadata store and cleans up removed labels and annotations from resources
func (r *ReconcileArgoCD) processDropMetadataForCleanup(argocd *argoproj.ArgoCD) {
namespace := argocd.GetNamespace()
dropMetadataList := getDropMetadataForNamespace(namespace)

for _, dropMeta := range dropMetadataList {
if err := r.cleanupLabelsAndAnnotationsFromResourceType(argocd, dropMeta); err != nil {
log.Error(err, "failed to cleanup labels and annotations from resource type",
"resource", dropMeta.Resource)
continue
}
}

// Clear the DropMetadata for this namespace after cleanup
removeDropMetadataForNamespace(namespace)
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Fix data race and event loss in DropMetadata processing

getDropMetadataForNamespace returns the backing slice without copying, then processDropMetadataForCleanup iterates after releasing the lock and finally deletes the entry. Writers can mutate the same slice concurrently, and new entries added during processing can be dropped when delete runs. Drain the namespace atomically under write lock and iterate over a copy.

Apply this diff:

@@
-func getDropMetadataForNamespace(namespace string) []DropMetadata {
-	DropMetadataStoreMutex.RLock()
-	defer DropMetadataStoreMutex.RUnlock()
-	return DropMetadataStore[namespace]
-}
+func getDropMetadataForNamespace(namespace string) []DropMetadata {
+	DropMetadataStoreMutex.RLock()
+	defer DropMetadataStoreMutex.RUnlock()
+	// Return a shallow copy to avoid callers observing concurrent writes
+	src := DropMetadataStore[namespace]
+	dst := make([]DropMetadata, len(src))
+	copy(dst, src)
+	return dst
+}
 
 // removeDropMetadataForNamespace removes all DropMetadata for a specific namespace
 func removeDropMetadataForNamespace(namespace string) {
 	DropMetadataStoreMutex.Lock()
 	defer DropMetadataStoreMutex.Unlock()
 	delete(DropMetadataStore, namespace)
 }
 
+// drainDropMetadataForNamespace atomically retrieves and clears the namespace entry.
+func drainDropMetadataForNamespace(namespace string) []DropMetadata {
+	DropMetadataStoreMutex.Lock()
+	defer DropMetadataStoreMutex.Unlock()
+	src := DropMetadataStore[namespace]
+	dst := make([]DropMetadata, len(src))
+	copy(dst, src)
+	delete(DropMetadataStore, namespace)
+	return dst
+}
@@
 func (r *ReconcileArgoCD) processDropMetadataForCleanup(argocd *argoproj.ArgoCD) {
 	namespace := argocd.GetNamespace()
-	dropMetadataList := getDropMetadataForNamespace(namespace)
+	// Atomically drain the queue for this namespace to avoid races and event loss
+	dropMetadataList := drainDropMetadataForNamespace(namespace)
 
 	for _, dropMeta := range dropMetadataList {
 		if err := r.cleanupLabelsAndAnnotationsFromResourceType(argocd, dropMeta); err != nil {
 			log.Error(err, "failed to cleanup labels and annotations from resource type",
 				"resource", dropMeta.Resource)
 			continue
 		}
 	}
-
-	// Clear the DropMetadata for this namespace after cleanup
-	removeDropMetadataForNamespace(namespace)
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func getDropMetadataForNamespace(namespace string) []DropMetadata {
DropMetadataStoreMutex.RLock()
defer DropMetadataStoreMutex.RUnlock()
return DropMetadataStore[namespace]
}
// removeDropMetadataForNamespace removes all DropMetadata for a specific namespace
func removeDropMetadataForNamespace(namespace string) {
DropMetadataStoreMutex.Lock()
defer DropMetadataStoreMutex.Unlock()
delete(DropMetadataStore, namespace)
}
// processDropMetadataForCleanup processes the DropMetadata store and cleans up removed labels and annotations from resources
func (r *ReconcileArgoCD) processDropMetadataForCleanup(argocd *argoproj.ArgoCD) {
namespace := argocd.GetNamespace()
dropMetadataList := getDropMetadataForNamespace(namespace)
for _, dropMeta := range dropMetadataList {
if err := r.cleanupLabelsAndAnnotationsFromResourceType(argocd, dropMeta); err != nil {
log.Error(err, "failed to cleanup labels and annotations from resource type",
"resource", dropMeta.Resource)
continue
}
}
// Clear the DropMetadata for this namespace after cleanup
removeDropMetadataForNamespace(namespace)
}
func getDropMetadataForNamespace(namespace string) []DropMetadata {
DropMetadataStoreMutex.RLock()
defer DropMetadataStoreMutex.RUnlock()
// Return a shallow copy to avoid callers observing concurrent writes
src := DropMetadataStore[namespace]
dst := make([]DropMetadata, len(src))
copy(dst, src)
return dst
}
// removeDropMetadataForNamespace removes all DropMetadata for a specific namespace
func removeDropMetadataForNamespace(namespace string) {
DropMetadataStoreMutex.Lock()
defer DropMetadataStoreMutex.Unlock()
delete(DropMetadataStore, namespace)
}
// drainDropMetadataForNamespace atomically retrieves and clears the namespace entry.
func drainDropMetadataForNamespace(namespace string) []DropMetadata {
DropMetadataStoreMutex.Lock()
defer DropMetadataStoreMutex.Unlock()
src := DropMetadataStore[namespace]
dst := make([]DropMetadata, len(src))
copy(dst, src)
delete(DropMetadataStore, namespace)
return dst
}
// processDropMetadataForCleanup processes the DropMetadata store and cleans up removed labels and annotations from resources
func (r *ReconcileArgoCD) processDropMetadataForCleanup(argocd *argoproj.ArgoCD) {
namespace := argocd.GetNamespace()
// Atomically drain the queue for this namespace to avoid races and event loss
dropMetadataList := drainDropMetadataForNamespace(namespace)
for _, dropMeta := range dropMetadataList {
if err := r.cleanupLabelsAndAnnotationsFromResourceType(argocd, dropMeta); err != nil {
log.Error(err, "failed to cleanup labels and annotations from resource type",
"resource", dropMeta.Resource)
continue
}
}
}

Comment on lines +1362 to +1405
switch dropMeta.Resource {
case "server-deployment":
// Clean up from ArgoCD server deployment only
deploymentName := fmt.Sprintf("%s-server", argocd.Name)
if err := r.cleanupLabelsAndAnnotationsFromDeployment(argocd, deploymentName, dropMeta.LabelKeys, dropMeta.AnnotationKeys); err != nil {
return fmt.Errorf("failed to cleanup labels from server deployment: %w", err)
}

case "repo-deployment":
// Clean up from ArgoCD repo server deployment only
deploymentName := fmt.Sprintf("%s-repo-server", argocd.Name)
if err := r.cleanupLabelsAndAnnotationsFromDeployment(argocd, deploymentName, dropMeta.LabelKeys, dropMeta.AnnotationKeys); err != nil {
return fmt.Errorf("failed to cleanup labels from repo deployment: %w", err)
}

case "applicationset-deployment":
// Clean up from ArgoCD applicationset controller deployment only
deploymentName := fmt.Sprintf("%s-applicationset-controller", argocd.Name)
if err := r.cleanupLabelsAndAnnotationsFromDeployment(argocd, deploymentName, dropMeta.LabelKeys, dropMeta.AnnotationKeys); err != nil {
return fmt.Errorf("failed to cleanup labels from applicationset deployment: %w", err)
}

case "notifications-deployment":
// Clean up from ArgoCD notifications controller deployment only
if argocd.Spec.Notifications.Enabled {
deploymentName := fmt.Sprintf("%s-notifications-controller", argocd.Name)
if err := r.cleanupLabelsAndAnnotationsFromDeployment(argocd, deploymentName, dropMeta.LabelKeys, dropMeta.AnnotationKeys); err != nil {
return fmt.Errorf("failed to cleanup labels from notifications deployment: %w", err)
}
}

case "controller-statefulset":
// Clean up from ArgoCD application controller StatefulSet
statefulSetName := fmt.Sprintf("%s-application-controller", argocd.Name)
if err := r.cleanupLabelsAndAnnotationsFromStatefulSet(argocd, statefulSetName, dropMeta.LabelKeys, dropMeta.AnnotationKeys); err != nil {
return fmt.Errorf("failed to cleanup labels from controller statefulset: %w", err)
}

default:
log.Info("Unknown resource type for cleanup", "resourceType", dropMeta.Resource)
}

return nil
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Use nameWithSuffix(...) for resource names to avoid truncation mismatches

Using fmt.Sprintf("%s-...") with cr.Name breaks when resource names are derived via nameWithSuffix (truncated CR name). Cleanup will miss targets for long CR names.

Apply this diff:

@@
-	case "server-deployment":
-		deploymentName := fmt.Sprintf("%s-server", argocd.Name)
+	case "server-deployment":
+		deploymentName := nameWithSuffix("server", argocd)
@@
-	case "repo-deployment":
-		deploymentName := fmt.Sprintf("%s-repo-server", argocd.Name)
+	case "repo-deployment":
+		deploymentName := nameWithSuffix("repo-server", argocd)
@@
-	case "applicationset-deployment":
-		deploymentName := fmt.Sprintf("%s-applicationset-controller", argocd.Name)
+	case "applicationset-deployment":
+		deploymentName := nameWithSuffix("applicationset-controller", argocd)
@@
-	case "notifications-deployment":
+	case "notifications-deployment":
 		// Clean up from ArgoCD notifications controller deployment only
 		if argocd.Spec.Notifications.Enabled {
-			deploymentName := fmt.Sprintf("%s-notifications-controller", argocd.Name)
+			deploymentName := nameWithSuffix("notifications-controller", argocd)
 			if err := r.cleanupLabelsAndAnnotationsFromDeployment(argocd, deploymentName, dropMeta.LabelKeys, dropMeta.AnnotationKeys); err != nil {
 				return fmt.Errorf("failed to cleanup labels from notifications deployment: %w", err)
 			}
 		}
@@
-	case "controller-statefulset":
-		statefulSetName := fmt.Sprintf("%s-application-controller", argocd.Name)
+	case "controller-statefulset":
+		statefulSetName := nameWithSuffix("application-controller", argocd)

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +1407 to +1458
// cleanupLabelsFromDeployment removes labels from a specific deployment
func (r *ReconcileArgoCD) cleanupLabelsAndAnnotationsFromDeployment(argocd *argoproj.ArgoCD, deploymentName string, labelsToRemove, annotationsToRemove []string) error {
namespace := argocd.GetNamespace()
deployment := &appsv1.Deployment{}
key := types.NamespacedName{Namespace: namespace, Name: deploymentName}

if err := r.Get(context.TODO(), key, deployment); err != nil {
if apierrors.IsNotFound(err) {
log.Info("Deployment not found, skipping label cleanup", "name", deploymentName)
return nil // Deployment doesn't exist, skip
}
return fmt.Errorf("failed to get deployment %s: %w", deploymentName, err)
}

// Work on pod template labels and annotations directly, not deployment metadata labels
currentLabels := deployment.Spec.Template.Labels
currentAnnotations := deployment.Spec.Template.Annotations
if currentLabels == nil && currentAnnotations == nil {
return nil // No labels or annotations to remove
}

modifiedLabels := false
modifiedAnnotations := false
for _, labelKey := range labelsToRemove {
if _, exists := currentLabels[labelKey]; exists {
delete(currentLabels, labelKey)
modifiedLabels = true
}
}

for _, annotationKey := range annotationsToRemove {
if _, exists := currentAnnotations[annotationKey]; exists {
delete(currentAnnotations, annotationKey)
modifiedAnnotations = true
}
}

if modifiedLabels {
deployment.Spec.Template.Labels = currentLabels
}
if modifiedAnnotations {
deployment.Spec.Template.Annotations = currentAnnotations
}

if modifiedLabels || modifiedAnnotations {
if err := r.Update(context.TODO(), deployment); err != nil {
log.Error(err, "failed to update deployment after removing labels and annotations", "name", deploymentName)
return err
}
}
return nil
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Also remove labels/annotations from Deployment metadata, not only Pod template

Stale keys can persist on deployment.ObjectMeta.{Labels,Annotations}. Clean both places.

Apply this diff:

@@
-	// Work on pod template labels and annotations directly, not deployment metadata labels
-	currentLabels := deployment.Spec.Template.Labels
-	currentAnnotations := deployment.Spec.Template.Annotations
+	// Clean both deployment metadata and pod template metadata
+	currentDepLabels := deployment.Labels
+	currentDepAnnotations := deployment.Annotations
+	currentLabels := deployment.Spec.Template.Labels
+	currentAnnotations := deployment.Spec.Template.Annotations
 	if currentLabels == nil && currentAnnotations == nil {
 		return nil // No labels or annotations to remove
 	}
 
-	modifiedLabels := false
-	modifiedAnnotations := false
+	modifiedDepLabels := false
+	modifiedDepAnnotations := false
+	modifiedLabels := false
+	modifiedAnnotations := false
 	for _, labelKey := range labelsToRemove {
+		if currentDepLabels != nil {
+			if _, exists := currentDepLabels[labelKey]; exists {
+				delete(currentDepLabels, labelKey)
+				modifiedDepLabels = true
+			}
+		}
 		if _, exists := currentLabels[labelKey]; exists {
 			delete(currentLabels, labelKey)
 			modifiedLabels = true
 		}
 	}
 
 	for _, annotationKey := range annotationsToRemove {
+		if currentDepAnnotations != nil {
+			if _, exists := currentDepAnnotations[annotationKey]; exists {
+				delete(currentDepAnnotations, annotationKey)
+				modifiedDepAnnotations = true
+			}
+		}
 		if _, exists := currentAnnotations[annotationKey]; exists {
 			delete(currentAnnotations, annotationKey)
 			modifiedAnnotations = true
 		}
 	}
 
+	if modifiedDepLabels {
+		deployment.Labels = currentDepLabels
+	}
+	if modifiedDepAnnotations {
+		deployment.Annotations = currentDepAnnotations
+	}
 	if modifiedLabels {
 		deployment.Spec.Template.Labels = currentLabels
 	}
 	if modifiedAnnotations {
 		deployment.Spec.Template.Annotations = currentAnnotations
 	}
 
-	if modifiedLabels || modifiedAnnotations {
+	if modifiedDepLabels || modifiedDepAnnotations || modifiedLabels || modifiedAnnotations {
 		if err := r.Update(context.TODO(), deployment); err != nil {
 			log.Error(err, "failed to update deployment after removing labels and annotations", "name", deploymentName)
 			return err
 		}
 	}

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In controllers/argocd/util.go around lines 1407 to 1458, the function only
removes labels/annotations from deployment.Spec.Template but not from the
Deployment's metadata, so stale keys can remain; update the function to also
check and delete the same keys from deployment.ObjectMeta.Labels and
deployment.ObjectMeta.Annotations, track modifications for metadata separately
(or reuse the existing modified flags), guard against nil maps before
checking/deleting, set the deployment.ObjectMeta.Labels/Annotations back if
modified, and keep the existing update call so the Deployment is persisted when
either template or metadata changes.

Comment on lines +1460 to +1511
// cleanupLabelsFromStatefulSet removes labels from a specific statefulset
func (r *ReconcileArgoCD) cleanupLabelsAndAnnotationsFromStatefulSet(argocd *argoproj.ArgoCD, statefulSetName string, labelsToRemove, annotationsToRemove []string) error {
namespace := argocd.GetNamespace()
statefulSet := &appsv1.StatefulSet{}
key := types.NamespacedName{Namespace: namespace, Name: statefulSetName}

if err := r.Get(context.TODO(), key, statefulSet); err != nil {
if apierrors.IsNotFound(err) {
log.Info("StatefulSet not found, skipping label and annotation cleanup", "name", statefulSetName)
return nil // StatefulSet doesn't exist, nothing to clean
}
return fmt.Errorf("failed to get statefulset %s: %w", statefulSetName, err)
}

// Work on pod template labels directly, not StatefulSet metadata labels
currentLabels := statefulSet.Spec.Template.Labels
currentAnnotations := statefulSet.Spec.Template.Annotations
if currentLabels == nil && currentAnnotations == nil {
return nil // No labels or annotations to remove
}

modifiedLabels := false
modifiedAnnotations := false
for _, labelKey := range labelsToRemove {
if _, exists := currentLabels[labelKey]; exists {
delete(currentLabels, labelKey)
modifiedLabels = true
}
}
for _, annotationKey := range annotationsToRemove {
if _, exists := currentAnnotations[annotationKey]; exists {
delete(currentAnnotations, annotationKey)
modifiedAnnotations = true
}
}

if modifiedLabels {
statefulSet.Spec.Template.Labels = currentLabels
}
if modifiedAnnotations {
statefulSet.Spec.Template.Annotations = currentAnnotations
}

if modifiedLabels || modifiedAnnotations {
if err := r.Update(context.TODO(), statefulSet); err != nil {
log.Error(err, "failed to update statefulset after removing labels and annotations", "name", statefulSetName)
return err
}
}

return nil
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Mirror cleanup on StatefulSet metadata as well

Same stale key risk on statefulSet.ObjectMeta.{Labels,Annotations}.

Apply this diff:

@@
-	// Work on pod template labels directly, not StatefulSet metadata labels
-	currentLabels := statefulSet.Spec.Template.Labels
-	currentAnnotations := statefulSet.Spec.Template.Annotations
+	// Clean both statefulset metadata and pod template metadata
+	currentStsLabels := statefulSet.Labels
+	currentStsAnnotations := statefulSet.Annotations
+	currentLabels := statefulSet.Spec.Template.Labels
+	currentAnnotations := statefulSet.Spec.Template.Annotations
@@
-	modifiedLabels := false
-	modifiedAnnotations := false
+	modifiedStsLabels := false
+	modifiedStsAnnotations := false
+	modifiedLabels := false
+	modifiedAnnotations := false
 	for _, labelKey := range labelsToRemove {
+		if currentStsLabels != nil {
+			if _, exists := currentStsLabels[labelKey]; exists {
+				delete(currentStsLabels, labelKey)
+				modifiedStsLabels = true
+			}
+		}
 		if _, exists := currentLabels[labelKey]; exists {
 			delete(currentLabels, labelKey)
 			modifiedLabels = true
 		}
 	}
 	for _, annotationKey := range annotationsToRemove {
+		if currentStsAnnotations != nil {
+			if _, exists := currentStsAnnotations[annotationKey]; exists {
+				delete(currentStsAnnotations, annotationKey)
+				modifiedStsAnnotations = true
+			}
+		}
 		if _, exists := currentAnnotations[annotationKey]; exists {
 			delete(currentAnnotations, annotationKey)
 			modifiedAnnotations = true
 		}
 	}
 
+	if modifiedStsLabels {
+		statefulSet.Labels = currentStsLabels
+	}
+	if modifiedStsAnnotations {
+		statefulSet.Annotations = currentStsAnnotations
+	}
 	if modifiedLabels {
 		statefulSet.Spec.Template.Labels = currentLabels
 	}
 	if modifiedAnnotations {
 		statefulSet.Spec.Template.Annotations = currentAnnotations
 	}
 
-	if modifiedLabels || modifiedAnnotations {
+	if modifiedStsLabels || modifiedStsAnnotations || modifiedLabels || modifiedAnnotations {
 		if err := r.Update(context.TODO(), statefulSet); err != nil {
 			log.Error(err, "failed to update statefulset after removing labels and annotations", "name", statefulSetName)
 			return err
 		}
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// cleanupLabelsFromStatefulSet removes labels from a specific statefulset
func (r *ReconcileArgoCD) cleanupLabelsAndAnnotationsFromStatefulSet(argocd *argoproj.ArgoCD, statefulSetName string, labelsToRemove, annotationsToRemove []string) error {
namespace := argocd.GetNamespace()
statefulSet := &appsv1.StatefulSet{}
key := types.NamespacedName{Namespace: namespace, Name: statefulSetName}
if err := r.Get(context.TODO(), key, statefulSet); err != nil {
if apierrors.IsNotFound(err) {
log.Info("StatefulSet not found, skipping label and annotation cleanup", "name", statefulSetName)
return nil // StatefulSet doesn't exist, nothing to clean
}
return fmt.Errorf("failed to get statefulset %s: %w", statefulSetName, err)
}
// Work on pod template labels directly, not StatefulSet metadata labels
currentLabels := statefulSet.Spec.Template.Labels
currentAnnotations := statefulSet.Spec.Template.Annotations
if currentLabels == nil && currentAnnotations == nil {
return nil // No labels or annotations to remove
}
modifiedLabels := false
modifiedAnnotations := false
for _, labelKey := range labelsToRemove {
if _, exists := currentLabels[labelKey]; exists {
delete(currentLabels, labelKey)
modifiedLabels = true
}
}
for _, annotationKey := range annotationsToRemove {
if _, exists := currentAnnotations[annotationKey]; exists {
delete(currentAnnotations, annotationKey)
modifiedAnnotations = true
}
}
if modifiedLabels {
statefulSet.Spec.Template.Labels = currentLabels
}
if modifiedAnnotations {
statefulSet.Spec.Template.Annotations = currentAnnotations
}
if modifiedLabels || modifiedAnnotations {
if err := r.Update(context.TODO(), statefulSet); err != nil {
log.Error(err, "failed to update statefulset after removing labels and annotations", "name", statefulSetName)
return err
}
}
return nil
}
// cleanupLabelsFromStatefulSet removes labels from a specific statefulset
func (r *ReconcileArgoCD) cleanupLabelsAndAnnotationsFromStatefulSet(argocd *argoproj.ArgoCD, statefulSetName string, labelsToRemove, annotationsToRemove []string) error {
namespace := argocd.GetNamespace()
statefulSet := &appsv1.StatefulSet{}
key := types.NamespacedName{Namespace: namespace, Name: statefulSetName}
if err := r.Get(context.TODO(), key, statefulSet); err != nil {
if apierrors.IsNotFound(err) {
log.Info("StatefulSet not found, skipping label and annotation cleanup", "name", statefulSetName)
return nil // StatefulSet doesn't exist, nothing to clean
}
return fmt.Errorf("failed to get statefulset %s: %w", statefulSetName, err)
}
// Clean both statefulset metadata and pod template metadata
currentStsLabels := statefulSet.Labels
currentStsAnnotations := statefulSet.Annotations
currentLabels := statefulSet.Spec.Template.Labels
currentAnnotations := statefulSet.Spec.Template.Annotations
if currentLabels == nil && currentAnnotations == nil {
return nil // No labels or annotations to remove
}
modifiedStsLabels := false
modifiedStsAnnotations := false
modifiedLabels := false
modifiedAnnotations := false
for _, labelKey := range labelsToRemove {
if currentStsLabels != nil {
if _, exists := currentStsLabels[labelKey]; exists {
delete(currentStsLabels, labelKey)
modifiedStsLabels = true
}
}
if _, exists := currentLabels[labelKey]; exists {
delete(currentLabels, labelKey)
modifiedLabels = true
}
}
for _, annotationKey := range annotationsToRemove {
if currentStsAnnotations != nil {
if _, exists := currentStsAnnotations[annotationKey]; exists {
delete(currentStsAnnotations, annotationKey)
modifiedStsAnnotations = true
}
}
if _, exists := currentAnnotations[annotationKey]; exists {
delete(currentAnnotations, annotationKey)
modifiedAnnotations = true
}
}
if modifiedStsLabels {
statefulSet.Labels = currentStsLabels
}
if modifiedStsAnnotations {
statefulSet.Annotations = currentStsAnnotations
}
if modifiedLabels {
statefulSet.Spec.Template.Labels = currentLabels
}
if modifiedAnnotations {
statefulSet.Spec.Template.Annotations = currentAnnotations
}
if modifiedStsLabels || modifiedStsAnnotations || modifiedLabels || modifiedAnnotations {
if err := r.Update(context.TODO(), statefulSet); err != nil {
log.Error(err, "failed to update statefulset after removing labels and annotations", "name", statefulSetName)
return err
}
}
return nil
}
🤖 Prompt for AI Agents
In controllers/argocd/util.go around lines 1460 to 1511, the cleanup only
removes labels/annotations from statefulSet.Spec.Template but not from
statefulSet.ObjectMeta, leaving stale keys on the StatefulSet metadata; modify
the function to also check and remove each labelsToRemove/annotationsToRemove
key from statefulSet.ObjectMeta.Labels and statefulSet.ObjectMeta.Annotations
(ensure you initialize nil maps or guard nils), set
modifiedLabels/modifiedAnnotations if any metadata keys were deleted, assign
back to statefulSet.ObjectMeta.Labels/Annotations when changed, and then call
r.Update once if either spec/template or metadata were modified (preserving
existing error logging).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant