Skip to content

Commit 2c71774

Browse files
committed
fix:recreate application in managed mode upon deletion from spoke
Assisted-by: Cursor Signed-off-by: Rizwana777 <[email protected]>
1 parent 427206b commit 2c71774

File tree

3 files changed

+164
-0
lines changed

3 files changed

+164
-0
lines changed

principal/event.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,21 @@ func (s *Server) processApplicationEvent(ctx context.Context, agentName string,
257257
// When we receive an 'application deleted' event from a managed agent, it should only cause the principal application to be deleted IF the principal Application is ALSO in the deletion state (deletionTimestamp is non-nil)
258258
if app.DeletionTimestamp == nil {
259259
logCtx.Warn("application was detected as deleted from managed agent, even though principal application is not in deletion state")
260+
// In managed mode, if the application was deleted from the agent but the principal
261+
// application is not in deletion state, we should recreate it by sending the
262+
// application spec back to the managed agent
263+
logCtx.Info("Recreating application in managed agent to maintain desired state")
264+
265+
// Send the application spec to the managed agent to recreate it
266+
ev := s.events.ApplicationEvent(event.SpecUpdate, app)
267+
q := s.queues.SendQ(agentName)
268+
if q == nil {
269+
logCtx.Error("Send queue not found for agent, cannot recreate application")
270+
return fmt.Errorf("send queue not found for agent %s", agentName)
271+
}
272+
q.Add(ev)
273+
logCtx.Infof("Sent application spec to managed agent %s to recreate application %s", agentName, app.Name)
274+
260275
return nil
261276
}
262277

principal/event_test.go

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -446,6 +446,9 @@ func Test_DeleteEvents_ManagedMode(t *testing.T) {
446446
wq.On("Done", &ev)
447447
s, err := NewServer(context.Background(), fac, "argocd", WithGeneratedTokenSigningKey())
448448
require.NoError(t, err)
449+
s.events = event.NewEventSource("test")
450+
err = s.queues.Create("foo")
451+
require.NoError(t, err)
449452
s.setAgentMode("foo", types.AgentModeManaged)
450453

451454
_, err = fac.ApplicationsClientset.ArgoprojV1alpha1().Applications(delApp.Namespace).Create(context.Background(), delApp, v1.CreateOptions{})
@@ -920,3 +923,95 @@ func Test_processClusterCacheInfoUpdateEvent(t *testing.T) {
920923
assert.ErrorContains(t, err, "not mapped to any cluster")
921924
})
922925
}
926+
927+
func Test_ManagedAppRecreationLogic(t *testing.T) {
928+
929+
type test struct {
930+
name string
931+
deletionTimestampSetOnPrincipal bool
932+
expectRecreation bool
933+
}
934+
935+
tests := []test{
936+
{
937+
name: "Delete event from managed agent should recreate app when principal is not in deletion state",
938+
deletionTimestampSetOnPrincipal: false,
939+
expectRecreation: true,
940+
},
941+
{
942+
name: "Delete event from managed agent should not recreate app when principal is in deletion state",
943+
deletionTimestampSetOnPrincipal: true,
944+
expectRecreation: false,
945+
},
946+
}
947+
948+
for _, test := range tests {
949+
950+
t.Run(test.name, func(t *testing.T) {
951+
delApp := &v1alpha1.Application{
952+
ObjectMeta: v1.ObjectMeta{
953+
Name: "test-app",
954+
Namespace: "agent-managed",
955+
Finalizers: []string{"test-finalizer"},
956+
},
957+
Spec: v1alpha1.ApplicationSpec{
958+
Project: "default",
959+
Source: &v1alpha1.ApplicationSource{
960+
RepoURL: "https://github.com/argoproj/argocd-example-apps",
961+
TargetRevision: "HEAD",
962+
Path: "kustomize-guestbook",
963+
},
964+
Destination: v1alpha1.ApplicationDestination{
965+
Server: "https://kubernetes.default.svc",
966+
Namespace: "guestbook",
967+
},
968+
},
969+
Status: v1alpha1.ApplicationStatus{
970+
Sync: v1alpha1.SyncStatus{Status: v1alpha1.SyncStatusCodeSynced},
971+
},
972+
}
973+
974+
if test.deletionTimestampSetOnPrincipal {
975+
delApp.DeletionTimestamp = ptr.To(v1.Time{Time: time.Now()})
976+
}
977+
978+
fac := kube.NewKubernetesFakeClientWithApps("argocd")
979+
ev := cloudevents.NewEvent()
980+
ev.SetDataSchema("application")
981+
ev.SetType(event.Delete.String())
982+
ev.SetData(cloudevents.ApplicationJSON, delApp)
983+
wq := wqmock.NewTypedRateLimitingInterface[*cloudevents.Event](t)
984+
wq.On("Get").Return(&ev, false)
985+
wq.On("Done", &ev)
986+
s, err := NewServer(context.Background(), fac, "argocd", WithGeneratedTokenSigningKey())
987+
require.NoError(t, err)
988+
s.setAgentMode("agent-managed", types.AgentModeManaged)
989+
s.events = event.NewEventSource("test")
990+
991+
// Set up the send queue for normal operation
992+
err = s.queues.Create("agent-managed")
993+
require.NoError(t, err)
994+
995+
// Create the application in the principal
996+
_, err = fac.ApplicationsClientset.ArgoprojV1alpha1().Applications(delApp.Namespace).Create(context.Background(), delApp, v1.CreateOptions{})
997+
require.NoError(t, err)
998+
999+
got, err := s.processRecvQueue(context.Background(), "agent-managed", wq)
1000+
require.NoError(t, err)
1001+
require.Equal(t, ev, *got)
1002+
1003+
if test.expectRecreation {
1004+
// When recreation is expected, the application should still exist in the principal
1005+
// because the deletion was prevented and a recreation event was sent
1006+
_, err = fac.ApplicationsClientset.ArgoprojV1alpha1().Applications(delApp.Namespace).Get(context.Background(), delApp.Name, v1.GetOptions{})
1007+
require.NoError(t, err, "Application should still exist in principal when recreation is expected")
1008+
} else {
1009+
// When recreation is not expected, the app should be deleted
1010+
// This happens when the principal app is already in deletion state
1011+
_, err = fac.ApplicationsClientset.ArgoprojV1alpha1().Applications(delApp.Namespace).Get(context.Background(), delApp.Name, v1.GetOptions{})
1012+
require.True(t, apierrors.IsNotFound(err), "Application should be deleted when recreation is not expected")
1013+
}
1014+
})
1015+
}
1016+
1017+
}

test/e2e/resync_test.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1006,6 +1006,60 @@ func sampleRepository() *corev1.Secret {
10061006
}
10071007
}
10081008

1009+
// Test_ManagedAppRecreationOnDirectDeletion tests the scenario where:
1010+
// 1. A managed application exists on both principal and managed agent
1011+
// 2. The application is directly deleted from the managed agent (while agent is running)
1012+
// 3. The principal should detect this and recreate the application automatically
1013+
func (suite *ResyncTestSuite) Test_ManagedAppRecreationOnDirectDeletion() {
1014+
requires := suite.Require()
1015+
t := suite.T()
1016+
1017+
t.Log("Create a managed application")
1018+
app := suite.createManagedApp()
1019+
key := types.NamespacedName{Name: app.Name, Namespace: "argocd"}
1020+
1021+
t.Log("Verify application exists on both principal and managed agent")
1022+
// Verify on principal
1023+
principalApp := argoapp.Application{}
1024+
err := suite.PrincipalClient.Get(suite.Ctx, types.NamespacedName{Name: app.Name, Namespace: "agent-managed"}, &principalApp, metav1.GetOptions{})
1025+
requires.NoError(err, "Application should exist on principal")
1026+
1027+
// Verify on managed agent
1028+
managedApp := argoapp.Application{}
1029+
err = suite.ManagedAgentClient.Get(suite.Ctx, key, &managedApp, metav1.GetOptions{})
1030+
requires.NoError(err, "Application should exist on managed agent")
1031+
1032+
t.Log("Delete application directly from managed agent")
1033+
err = suite.ManagedAgentClient.Delete(suite.Ctx, &argoapp.Application{
1034+
ObjectMeta: metav1.ObjectMeta{
1035+
Name: app.Name,
1036+
Namespace: "argocd",
1037+
},
1038+
}, metav1.DeleteOptions{})
1039+
requires.NoError(err)
1040+
1041+
t.Log("Verify application is deleted from managed agent")
1042+
requires.Eventually(func() bool {
1043+
app := argoapp.Application{}
1044+
err := suite.ManagedAgentClient.Get(suite.Ctx, key, &app, metav1.GetOptions{})
1045+
return err != nil
1046+
}, 10*time.Second, 1*time.Second, "Application should be deleted from managed agent")
1047+
1048+
t.Log("Verify application is automatically recreated on managed agent")
1049+
requires.Eventually(func() bool {
1050+
app := argoapp.Application{}
1051+
err := suite.ManagedAgentClient.Get(suite.Ctx, key, &app, metav1.GetOptions{})
1052+
return err == nil
1053+
}, 30*time.Second, 2*time.Second, "Application should be recreated on managed agent")
1054+
1055+
t.Log("Verify recreated application has correct spec")
1056+
recreatedApp := argoapp.Application{}
1057+
err = suite.ManagedAgentClient.Get(suite.Ctx, key, &recreatedApp, metav1.GetOptions{})
1058+
requires.NoError(err)
1059+
requires.Equal(principalApp.Spec.Source.RepoURL, recreatedApp.Spec.Source.RepoURL, "Recreated app should have same source")
1060+
requires.Equal(principalApp.Spec.Destination.Namespace, recreatedApp.Spec.Destination.Namespace, "Recreated app should have same namespace")
1061+
}
1062+
10091063
func TestResyncTestSuite(t *testing.T) {
10101064
suite.Run(t, new(ResyncTestSuite))
10111065
}

0 commit comments

Comments
 (0)