Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 26 additions & 2 deletions internal/manager/application/application.go
Original file line number Diff line number Diff line change
Expand Up @@ -484,13 +484,13 @@ func (m *ApplicationManager) UpdateStatus(ctx context.Context, namespace string,
existing.Annotations = incoming.Annotations
existing.Labels = incoming.Labels
existing.Status = *incoming.Status.DeepCopy()
existing.Operation = incoming.Operation
existing.Operation = statusOperationToUse(existing, incoming)
}, func(existing, incoming *v1alpha1.Application) (jsondiff.Patch, error) {
refresh, incomingRefresh := incoming.Annotations["argocd.argoproj.io/refresh"]
_, existingRefresh := existing.Annotations["argocd.argoproj.io/refresh"]
target := &v1alpha1.Application{
Status: incoming.Status,
Operation: incoming.Operation,
Operation: statusOperationToUse(existing, incoming),
}
source := &v1alpha1.Application{
Status: existing.Status,
Expand All @@ -510,6 +510,14 @@ func (m *ApplicationManager) UpdateStatus(ctx context.Context, namespace string,
patch = append(patch, jsondiff.Operation{Type: "add", Path: "/metadata/annotations/argocd.argoproj.io~1refresh", Value: refresh})
}

// Stamp the last-updated annotation. If the existing app has no
// annotations map yet we must create it; otherwise add/replace the key.
if existing.Annotations == nil {
patch = append(patch, jsondiff.Operation{Type: "add", Path: "/metadata/annotations", Value: map[string]string{LastUpdatedAnnotation: incoming.Annotations[LastUpdatedAnnotation]}})
} else {
patch = append(patch, jsondiff.Operation{Type: "add", Path: "/metadata/annotations/argocd-agent.argoproj.io~1last-updated", Value: incoming.Annotations[LastUpdatedAnnotation]})
}

// If there is no status yet on our application (this happens when the
// application was just created), we need to make sure to initialize
// it properly.
Expand Down Expand Up @@ -602,6 +610,22 @@ func operationToUse(existing, incoming *v1alpha1.Application) *v1alpha1.Operatio
return incoming.Operation.DeepCopy()
}

func statusOperationToUse(existing, incoming *v1alpha1.Application) *v1alpha1.Operation {
if incoming.Operation != nil {
return operationToUse(existing, incoming)
}

if incoming.Status.OperationState == nil {
return existing.Operation
}

if incoming.Status.OperationState.Phase.Completed() {
return nil
}

return existing.Operation
}

// TerminateOperation aborts a running sync operation by setting .status.operationState.phase to Terminating.
func (m *ApplicationManager) TerminateOperation(ctx context.Context, incoming *v1alpha1.Application) (*v1alpha1.Application, error) {
if !m.destinationBasedMapping {
Expand Down
87 changes: 87 additions & 0 deletions internal/manager/application/application_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,93 @@ func Test_ManagerUpdateStatus(t *testing.T) {
require.NotNil(t, updated.Operation)
require.Equal(t, incoming.Operation, updated.Operation)
})

t.Run("Preserve existing operation when incoming status update has no operation", func(t *testing.T) {
incoming := &v1alpha1.Application{
ObjectMeta: v1.ObjectMeta{
Name: "foobar",
Namespace: "argocd",
Annotations: map[string]string{
"bar": "foo",
},
},
Status: v1alpha1.ApplicationStatus{
Sync: v1alpha1.SyncStatus{
Status: v1alpha1.SyncStatusCodeOutOfSync,
},
},
}
existing := &v1alpha1.Application{
ObjectMeta: v1.ObjectMeta{
Name: "foobar",
Namespace: "cluster-1",
Annotations: map[string]string{
"bar": "foo",
},
},
Operation: &v1alpha1.Operation{
InitiatedBy: v1alpha1.OperationInitiator{Username: "principal-sync"},
},
}

appC, ai := fakeInformer(t, "", existing)
be := application.NewKubernetesBackend(appC, "", ai, true)
mgr, err := NewApplicationManager(be, "argocd")
require.NoError(t, err)
mgr.mode = manager.ManagerModeManaged
mgr.role = manager.ManagerRolePrincipal

updated, err := mgr.UpdateStatus(context.Background(), "cluster-1", incoming)
require.NoError(t, err)
require.NotNil(t, updated)
require.NotNil(t, updated.Operation)
require.Equal(t, "principal-sync", updated.Operation.InitiatedBy.Username)
require.Contains(t, updated.Annotations, LastUpdatedAnnotation)
require.NotEmpty(t, updated.Annotations[LastUpdatedAnnotation])
})

t.Run("Clear existing operation when incoming status update is terminal", func(t *testing.T) {
incoming := &v1alpha1.Application{
ObjectMeta: v1.ObjectMeta{
Name: "foobar",
Namespace: "argocd",
Annotations: map[string]string{
"bar": "foo",
},
},
Status: v1alpha1.ApplicationStatus{
OperationState: &v1alpha1.OperationState{
Phase: synccommon.OperationSucceeded,
},
},
}
existing := &v1alpha1.Application{
ObjectMeta: v1.ObjectMeta{
Name: "foobar",
Namespace: "cluster-1",
Annotations: map[string]string{
"bar": "foo",
},
},
Operation: &v1alpha1.Operation{
InitiatedBy: v1alpha1.OperationInitiator{Username: "principal-sync"},
},
}

appC, ai := fakeInformer(t, "", existing)
be := application.NewKubernetesBackend(appC, "", ai, true)
mgr, err := NewApplicationManager(be, "argocd")
require.NoError(t, err)
mgr.mode = manager.ManagerModeManaged
mgr.role = manager.ManagerRolePrincipal

updated, err := mgr.UpdateStatus(context.Background(), "cluster-1", incoming)
require.NoError(t, err)
require.NotNil(t, updated)
require.Nil(t, updated.Operation)
require.Contains(t, updated.Annotations, LastUpdatedAnnotation)
require.NotEmpty(t, updated.Annotations[LastUpdatedAnnotation])
})
}

func Test_ManagerUpdateAutonomous(t *testing.T) {
Expand Down
Loading