Skip to content

Commit d570ada

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

File tree

3 files changed

+161
-0
lines changed

3 files changed

+161
-0
lines changed

principal/event.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,21 @@ func (s *Server) processApplicationEvent(ctx context.Context, agentName string,
255255
// 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)
256256
if app.DeletionTimestamp == nil {
257257
logCtx.Warn("application was detected as deleted from managed agent, even though principal application is not in deletion state")
258+
// In managed mode, if the application was deleted from the agent but the principal
259+
// application is not in deletion state, we should recreate it by sending the
260+
// application spec back to the managed agent
261+
logCtx.Info("Recreating application in managed agent to maintain desired state")
262+
263+
// Send the application spec to the managed agent to recreate it
264+
ev := s.events.ApplicationEvent(event.SpecUpdate, app)
265+
q := s.queues.SendQ(agentName)
266+
if q == nil {
267+
logCtx.Error("Send queue not found for agent, cannot recreate application")
268+
return fmt.Errorf("send queue not found for agent %s", agentName)
269+
}
270+
q.Add(ev)
271+
logCtx.Infof("Sent application spec to managed agent %s to recreate application %s", agentName, app.Name)
272+
258273
return nil
259274
}
260275

principal/event_test.go

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1172,3 +1172,95 @@ func Test_processClusterCacheInfoUpdateEvent(t *testing.T) {
11721172
assert.ErrorContains(t, err, "not mapped to any cluster")
11731173
})
11741174
}
1175+
1176+
func Test_ManagedAppRecreationLogic(t *testing.T) {
1177+
1178+
type test struct {
1179+
name string
1180+
deletionTimestampSetOnPrincipal bool
1181+
expectRecreation bool
1182+
}
1183+
1184+
tests := []test{
1185+
{
1186+
name: "Delete event from managed agent should recreate app when principal is not in deletion state",
1187+
deletionTimestampSetOnPrincipal: false,
1188+
expectRecreation: true,
1189+
},
1190+
{
1191+
name: "Delete event from managed agent should not recreate app when principal is in deletion state",
1192+
deletionTimestampSetOnPrincipal: true,
1193+
expectRecreation: false,
1194+
},
1195+
}
1196+
1197+
for _, test := range tests {
1198+
1199+
t.Run(test.name, func(t *testing.T) {
1200+
delApp := &v1alpha1.Application{
1201+
ObjectMeta: v1.ObjectMeta{
1202+
Name: "test-app",
1203+
Namespace: "agent-managed",
1204+
Finalizers: []string{"test-finalizer"},
1205+
},
1206+
Spec: v1alpha1.ApplicationSpec{
1207+
Project: "default",
1208+
Source: &v1alpha1.ApplicationSource{
1209+
RepoURL: "https://github.com/argoproj/argocd-example-apps",
1210+
TargetRevision: "HEAD",
1211+
Path: "kustomize-guestbook",
1212+
},
1213+
Destination: v1alpha1.ApplicationDestination{
1214+
Server: "https://kubernetes.default.svc",
1215+
Namespace: "guestbook",
1216+
},
1217+
},
1218+
Status: v1alpha1.ApplicationStatus{
1219+
Sync: v1alpha1.SyncStatus{Status: v1alpha1.SyncStatusCodeSynced},
1220+
},
1221+
}
1222+
1223+
if test.deletionTimestampSetOnPrincipal {
1224+
delApp.DeletionTimestamp = ptr.To(v1.Time{Time: time.Now()})
1225+
}
1226+
1227+
fac := kube.NewKubernetesFakeClientWithApps("argocd")
1228+
ev := cloudevents.NewEvent()
1229+
ev.SetDataSchema("application")
1230+
ev.SetType(event.Delete.String())
1231+
ev.SetData(cloudevents.ApplicationJSON, delApp)
1232+
wq := wqmock.NewTypedRateLimitingInterface[*cloudevents.Event](t)
1233+
wq.On("Get").Return(&ev, false)
1234+
wq.On("Done", &ev)
1235+
s, err := NewServer(context.Background(), fac, "argocd", WithGeneratedTokenSigningKey())
1236+
require.NoError(t, err)
1237+
s.setAgentMode("agent-managed", types.AgentModeManaged)
1238+
s.events = event.NewEventSource("test")
1239+
1240+
// Set up the send queue for normal operation
1241+
err = s.queues.Create("agent-managed")
1242+
require.NoError(t, err)
1243+
1244+
// Create the application in the principal
1245+
_, err = fac.ApplicationsClientset.ArgoprojV1alpha1().Applications(delApp.Namespace).Create(context.Background(), delApp, v1.CreateOptions{})
1246+
require.NoError(t, err)
1247+
1248+
got, err := s.processRecvQueue(context.Background(), "agent-managed", wq)
1249+
require.NoError(t, err)
1250+
require.Equal(t, ev, *got)
1251+
1252+
if test.expectRecreation {
1253+
// When recreation is expected, the application should still exist in the principal
1254+
// because the deletion was prevented and a recreation event was sent
1255+
_, err = fac.ApplicationsClientset.ArgoprojV1alpha1().Applications(delApp.Namespace).Get(context.Background(), delApp.Name, v1.GetOptions{})
1256+
require.NoError(t, err, "Application should still exist in principal when recreation is expected")
1257+
} else {
1258+
// When recreation is not expected, the app should be deleted
1259+
// This happens when the principal app is already in deletion state
1260+
_, err = fac.ApplicationsClientset.ArgoprojV1alpha1().Applications(delApp.Namespace).Get(context.Background(), delApp.Name, v1.GetOptions{})
1261+
require.True(t, apierrors.IsNotFound(err), "Application should be deleted when recreation is not expected")
1262+
}
1263+
})
1264+
}
1265+
1266+
}

test/e2e/resync_test.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1018,6 +1018,60 @@ func sampleRepository() *corev1.Secret {
10181018
}
10191019
}
10201020

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

0 commit comments

Comments
 (0)