Skip to content

Commit cfd7ef2

Browse files
committed
fix: preserve pending sync operations during managed status updates
Managed-agent status updates can arrive on the principal with a nil .operation even while a principal-initiated sync request is still pending. The principal UpdateStatus path was previously copying incoming.Operation verbatim, which meant an early status update could clear the sync operation before the Argo CD application controller ever observed it. That race leaves newly created applications stuck OutOfSync/Missing with no .operation or .status.operationState on either side, even though the deploy flow requested a sync. Fix this by reusing operationToUse() in the principal UpdateStatus path so an existing operation is preserved when the incoming status payload does not carry one. The operation is still updated when the agent explicitly reports one, and terminating operations keep their existing behavior. Also add a regression test covering the nil-operation status update case so this remains safe to cherry-pick independently.
1 parent 7e2d8ed commit cfd7ef2

2 files changed

Lines changed: 49 additions & 2 deletions

File tree

internal/manager/application/application.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -484,13 +484,16 @@ func (m *ApplicationManager) UpdateStatus(ctx context.Context, namespace string,
484484
existing.Annotations = incoming.Annotations
485485
existing.Labels = incoming.Labels
486486
existing.Status = *incoming.Status.DeepCopy()
487-
existing.Operation = incoming.Operation
487+
// Managed-agent status updates frequently arrive without .operation.
488+
// Preserve any principal-initiated sync operation until the agent
489+
// explicitly reports an updated operation payload.
490+
existing.Operation = operationToUse(existing, incoming)
488491
}, func(existing, incoming *v1alpha1.Application) (jsondiff.Patch, error) {
489492
refresh, incomingRefresh := incoming.Annotations["argocd.argoproj.io/refresh"]
490493
_, existingRefresh := existing.Annotations["argocd.argoproj.io/refresh"]
491494
target := &v1alpha1.Application{
492495
Status: incoming.Status,
493-
Operation: incoming.Operation,
496+
Operation: operationToUse(existing, incoming),
494497
}
495498
source := &v1alpha1.Application{
496499
Status: existing.Status,

internal/manager/application/application_test.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,50 @@ func Test_ManagerUpdateStatus(t *testing.T) {
342342
require.NotNil(t, updated.Operation)
343343
require.Equal(t, incoming.Operation, updated.Operation)
344344
})
345+
346+
t.Run("Preserve existing operation when incoming status update has no operation", func(t *testing.T) {
347+
incoming := &v1alpha1.Application{
348+
ObjectMeta: v1.ObjectMeta{
349+
Name: "foobar",
350+
Namespace: "argocd",
351+
Annotations: map[string]string{
352+
"bar": "foo",
353+
},
354+
},
355+
Status: v1alpha1.ApplicationStatus{
356+
Sync: v1alpha1.SyncStatus{
357+
Status: v1alpha1.SyncStatusCodeOutOfSync,
358+
},
359+
},
360+
}
361+
existing := &v1alpha1.Application{
362+
ObjectMeta: v1.ObjectMeta{
363+
Name: "foobar",
364+
Namespace: "cluster-1",
365+
Annotations: map[string]string{
366+
"bar": "foo",
367+
},
368+
},
369+
Operation: &v1alpha1.Operation{
370+
InitiatedBy: v1alpha1.OperationInitiator{Username: "principal-sync"},
371+
},
372+
}
373+
374+
appC, ai := fakeInformer(t, "", existing)
375+
be := application.NewKubernetesBackend(appC, "", ai, true)
376+
mgr, err := NewApplicationManager(be, "argocd")
377+
require.NoError(t, err)
378+
mgr.mode = manager.ManagerModeManaged
379+
mgr.role = manager.ManagerRolePrincipal
380+
381+
updated, err := mgr.UpdateStatus(context.Background(), "cluster-1", incoming)
382+
require.NoError(t, err)
383+
require.NotNil(t, updated)
384+
require.NotNil(t, updated.Operation)
385+
require.Equal(t, "principal-sync", updated.Operation.InitiatedBy.Username)
386+
require.Contains(t, updated.Annotations, LastUpdatedAnnotation)
387+
require.NotEmpty(t, updated.Annotations[LastUpdatedAnnotation])
388+
})
345389
}
346390

347391
func Test_ManagerUpdateAutonomous(t *testing.T) {

0 commit comments

Comments
 (0)