bug: manual force-sync hard-replaces shared cluster-scoped resources via tracking-ID conflict, ignoring Prune=false
Checklist:
- I've searched in the docs and FAQ for my answer: https://bit.ly/argocd-faq.
- I've included steps to reproduce the bug.
- I've pasted the output of
argocd version.
Describe the bug
A manually-triggered sync with force: true performs a hard replace
(kubectl replace --force) on a shared, typically cluster-scoped resource
(e.g. a Namespace) when that resource's tracking label
(app.kubernetes.io/instance) points to a different application than the one
being synced. The resource is deleted and recreated, which for a Namespace means
it enters Terminating and takes every workload inside it down.
Crucially, argocd.argoproj.io/sync-options: Prune=false does not protect
against this: it only guards the prune code path, not the force-replace path.
This is not restart- or cache-timing specific. Unlike autoSync — which is
gated behind an OutOfSync check in controller/appcontroller.go — a manual
force-sync is not gated by that check, so the race can occur any time two apps
declare the same resource and the non-owning app is force-synced.
Note: this issue supersedes and re-scopes #28630. The "partial cache after controller restart" framing in that issue was inaccurate — Path A (auto-sync + prune) is correctly guarded (
autoSynconly runs onOutOfSync; a failed live-state load yieldsUnknown, andEnsureSynced()holds the cache lock across the whole resync). The genuine gap is the force-sync + tracking-ID conflict described here. Thanks to @ivomarino for the code-level analysis.
To Reproduce
- Create App-A and App-B that both declare the same shared resource
(e.g.
Namespace/foo, or any cluster-scoped resource). - Sync App-A first so the resource's
app.kubernetes.io/instancelabel points to App-A (App-A owns the tracking ID). - On App-B, set
argocd.argoproj.io/sync-options: Prune=falseon the shared resource. - Trigger a sync for App-B with
force: true(UI "Force" orargocd app sync App-B --force). - Observe: ArgoCD issues a force replace on the shared resource because of the
tracking-ID conflict. The resource is deleted and recreated (Namespace →
Terminating), despitePrune=false.
No controller restart and no partial cache are required to reproduce.
Expected behavior
- A force-sync must not hard-replace a resource owned (by tracking ID) by another
application; a tracking-ID conflict should surface as a
SharedResourceWarning/ sync error instead of a destructive replace. argocd.argoproj.io/sync-options: Prune=falseshould also protect against force-replace-triggered deletion — or the docs should explicitly state thatPrune=falsedoes not cover the force-replace path.
Version
argocd version: v3.3.9
argocd-server: v3.3.9 (chart argo-cd-9.5.11)
Kubernetes: GKE 1.30.x
Controller mode: Deployment (sharded, multiple replicas)
Related: #28630 (original report, re-scoped by this issue)Source: argoproj/argo-cd