Skip to content

Commit 11b52c3

Browse files
committed
use a deletion tracker to revert invalid deletions
Signed-off-by: Chetan Banavikalmutt <[email protected]> Assisted-by: Cursor
1 parent 4093f17 commit 11b52c3

File tree

10 files changed

+124
-55
lines changed

10 files changed

+124
-55
lines changed

agent/agent.go

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,10 @@ type Agent struct {
107107

108108
// sourceCache is a cache of resources from the source. We use it to revert any changes made to the local resources.
109109
sourceCache *cache.SourceCache
110+
111+
// deletions tracks valid deletions from the source.
112+
// This is used to differentiate between valid and invalid deletions
113+
deletions *manager.DeletionTracker
110114
}
111115

112116
const defaultQueueName = "default"
@@ -132,7 +136,9 @@ type AgentOption func(*Agent) error
132136
// options.
133137
func NewAgent(ctx context.Context, client *kube.KubernetesClient, namespace string, opts ...AgentOption) (*Agent, error) {
134138
a := &Agent{
135-
version: version.New("argocd-agent"),
139+
version: version.New("argocd-agent"),
140+
deletions: manager.NewDeletionTracker(),
141+
sourceCache: cache.NewSourceCache(),
136142
}
137143
a.infStopCh = make(chan struct{})
138144
a.namespace = namespace
@@ -200,6 +206,7 @@ func NewAgent(ctx context.Context, client *kube.KubernetesClient, namespace stri
200206
appManagerOpts := []application.ApplicationManagerOption{
201207
application.WithRole(manager.ManagerRoleAgent),
202208
application.WithMode(managerMode),
209+
application.WithDeletionTracker(a.deletions),
203210
}
204211

205212
if a.options.metricsPort > 0 {
@@ -313,8 +320,6 @@ func NewAgent(ctx context.Context, client *kube.KubernetesClient, namespace stri
313320
}
314321
a.clusterCache = clusterCache
315322

316-
a.sourceCache = cache.NewSourceCache()
317-
318323
return a, nil
319324
}
320325

agent/inbound.go

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,9 @@ func (a *Agent) processIncomingApplication(ev *event.Event) error {
175175
logCtx.Errorf("Error updating application: %v", err)
176176
}
177177
case event.Delete:
178+
if a.mode == types.AgentModeManaged {
179+
a.deletions.MarkExpected(incomingApp.UID)
180+
}
178181
err = a.deleteApplication(incomingApp)
179182
if err != nil {
180183
logCtx.Errorf("Error deleting application: %v", err)
@@ -255,6 +258,9 @@ func (a *Agent) processIncomingAppProject(ev *event.Event) error {
255258
logCtx.Errorf("Error updating appproject: %v", err)
256259
}
257260
case event.Delete:
261+
if a.mode == types.AgentModeManaged {
262+
a.deletions.MarkExpected(incomingAppProject.UID)
263+
}
258264
err = a.deleteAppProject(incomingAppProject)
259265
if err != nil {
260266
logCtx.Errorf("Error deleting appproject: %v", err)
@@ -337,6 +343,9 @@ func (a *Agent) processIncomingRepository(ev *event.Event) error {
337343
}
338344

339345
case event.Delete:
346+
if a.mode == types.AgentModeManaged {
347+
a.deletions.MarkExpected(incomingRepo.UID)
348+
}
340349
err = a.deleteRepository(incomingRepo)
341350
if err != nil {
342351
logCtx.Errorf("Error deleting repository: %v", err)
@@ -528,24 +537,23 @@ func (a *Agent) deleteApplication(app *v1alpha1.Application) error {
528537

529538
logCtx.Infof("Deleting application")
530539

531-
if a.mode == types.AgentModeManaged {
532-
a.sourceCache.Application.Delete(app.UID)
533-
}
534-
535540
deletionPropagation := backend.DeletePropagationBackground
536541
err := a.appManager.Delete(a.context, a.namespace, app, &deletionPropagation)
537542
if err != nil {
538543
if apierrors.IsNotFound(err) {
539544
logCtx.Debug("application is not found, perhaps it is already deleted")
545+
if a.mode == types.AgentModeManaged {
546+
a.sourceCache.Application.Delete(app.UID)
547+
}
540548
return nil
541549
}
542-
// Restore the cache if the deletion fails
543-
if a.mode == types.AgentModeManaged {
544-
a.sourceCache.Application.Set(app.UID, app.Spec)
545-
}
546550
return err
547551
}
548552

553+
if a.mode == types.AgentModeManaged {
554+
a.sourceCache.Application.Delete(app.UID)
555+
}
556+
549557
err = a.appManager.Unmanage(app.QualifiedName())
550558
if err != nil {
551559
log().Warnf("Could not unmanage app %s: %v", app.QualifiedName(), err)
@@ -636,8 +644,6 @@ func (a *Agent) deleteAppProject(project *v1alpha1.AppProject) error {
636644

637645
logCtx.Infof("Deleting appProject")
638646

639-
a.sourceCache.AppProject.Delete(project.UID)
640-
641647
deletionPropagation := backend.DeletePropagationBackground
642648
err := a.projectManager.Delete(a.context, project, &deletionPropagation)
643649
if err != nil {
@@ -646,11 +652,11 @@ func (a *Agent) deleteAppProject(project *v1alpha1.AppProject) error {
646652
a.sourceCache.AppProject.Delete(project.UID)
647653
return nil
648654
}
649-
// Restore the cache if the deletion fails
650-
a.sourceCache.AppProject.Set(project.UID, project.Spec)
651655
return err
652656
}
653657

658+
a.sourceCache.AppProject.Delete(project.UID)
659+
654660
err = a.projectManager.Unmanage(project.Name)
655661
if err != nil {
656662
log().Warnf("Could not unmanage appProject %s: %v", project.Name, err)
@@ -739,20 +745,19 @@ func (a *Agent) deleteRepository(repo *corev1.Secret) error {
739745

740746
logCtx.Infof("Deleting repository")
741747

742-
a.sourceCache.Repository.Delete(repo.UID)
743-
744748
deletionPropagation := backend.DeletePropagationBackground
745749
err := a.repoManager.Delete(a.context, repo.Name, repo.Namespace, &deletionPropagation)
746750
if err != nil {
747751
if apierrors.IsNotFound(err) {
748752
logCtx.Debug("repository is not found, perhaps it is already deleted")
753+
a.sourceCache.Repository.Delete(repo.UID)
749754
return nil
750755
}
751-
// Restore the cache if the deletion fails
752-
a.sourceCache.Repository.Set(repo.UID, repo.Data)
753756
return err
754757
}
755758

759+
a.sourceCache.Repository.Delete(repo.UID)
760+
756761
err = a.repoManager.Unmanage(repo.Name)
757762
if err != nil {
758763
log().Warnf("Could not unmanage repository %s: %v", repo.Name, err)

agent/outbound.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ func (a *Agent) addAppDeletionToQueue(app *v1alpha1.Application) {
119119
logCtx.Debugf("Delete app event")
120120

121121
if isResourceFromPrincipal(app) {
122-
if manager.RevertUserInitiatedDeletion(a.context, app, a.sourceCache.Application, a.appManager, logCtx) {
122+
if manager.RevertUserInitiatedDeletion(a.context, app, a.deletions, a.appManager, logCtx) {
123123
logCtx.Trace("Deleted app is recreated")
124124
return
125125
}
@@ -258,7 +258,7 @@ func (a *Agent) addAppProjectDeletionToQueue(appProject *v1alpha1.AppProject) {
258258
logCtx.Debugf("Delete appProject event")
259259

260260
if isResourceFromPrincipal(appProject) {
261-
if manager.RevertUserInitiatedDeletion(a.context, appProject, a.sourceCache.AppProject, a.projectManager, logCtx) {
261+
if manager.RevertUserInitiatedDeletion(a.context, appProject, a.deletions, a.projectManager, logCtx) {
262262
logCtx.Trace("Deleted appProject is recreated")
263263
return
264264
}
@@ -392,7 +392,7 @@ func (a *Agent) handleRepositoryDeletion(repo *corev1.Secret) {
392392

393393
logCtx.Debugf("Delete repository event")
394394

395-
if manager.RevertUserInitiatedDeletion(a.context, repo, a.sourceCache.Repository, a.repoManager, logCtx) {
395+
if manager.RevertUserInitiatedDeletion(a.context, repo, a.deletions, a.repoManager, logCtx) {
396396
logCtx.Trace("Deleted repository is recreated")
397397
return
398398
}

internal/manager/application/application.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ type ApplicationManager struct {
5858
manager.ManagedResources
5959
// ObservedResources, key is qualified name of the application, value is the Application's .metadata.resourceValue field
6060
manager.ObservedResources
61+
// deletions tracks valid deletions from the source.
62+
deletions *manager.DeletionTracker
6163
}
6264

6365
// ApplicationManagerOption is a callback function to set an option to the Application
@@ -85,6 +87,13 @@ func WithMode(mode manager.ManagerMode) ApplicationManagerOption {
8587
}
8688
}
8789

90+
// WithDeletionTracker is used to track valid deletions from the source.
91+
func WithDeletionTracker(d *manager.DeletionTracker) ApplicationManagerOption {
92+
return func(m *ApplicationManager) {
93+
m.deletions = d
94+
}
95+
}
96+
8897
// NewApplicationManager initializes and returns a new Manager with the given backend and
8998
// options.
9099
func NewApplicationManager(be backend.Application, namespace string, opts ...ApplicationManagerOption) (*ApplicationManager, error) {
@@ -258,6 +267,13 @@ func (m *ApplicationManager) UpdateManagedApp(ctx context.Context, incoming *v1a
258267

259268
if deletionTimestampChanged {
260269
logCtx.Infof("deletionTimestamp of managed agent changed from nil to non-nil, so deleting Application")
270+
// Mark this as a valid deletion so the callback does not treat it as a user-initiated deletion.
271+
if m.deletions != nil {
272+
if v, ok := updated.Annotations[manager.SourceUIDAnnotation]; ok {
273+
m.deletions.MarkExpected(ty.UID(v))
274+
}
275+
}
276+
261277
if err := m.applicationBackend.Delete(ctx, incoming.Name, incoming.Namespace, ptr.To(backend.DeletePropagationForeground)); err != nil {
262278
return nil, err
263279
}

internal/manager/manager.go

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,7 @@ type resourceManager[R kubeResource] interface {
211211
// Returns true if the resource was recreated, false otherwise.
212212
func RevertUserInitiatedDeletion[R kubeResource](ctx context.Context,
213213
outbound R,
214-
resCache resourceCache[R],
214+
deletions *DeletionTracker,
215215
mgr resourceManager[R],
216216
logCtx *logrus.Entry,
217217
) bool {
@@ -227,9 +227,8 @@ func RevertUserInitiatedDeletion[R kubeResource](ctx context.Context,
227227
return false
228228
}
229229

230-
// If the resource is not in the cache, it means it was deleted by an incoming delete event.
231-
// So no need to recreate it.
232-
if !resCache.Contains(types.UID(sourceUID)) {
230+
// Check if this deletion is coming from the source
231+
if deletions.IsExpected(types.UID(sourceUID)) {
233232
logCtx.Debugf("Expected deletion detected - allowing it to proceed")
234233
return false
235234
}
@@ -249,3 +248,37 @@ func RevertUserInitiatedDeletion[R kubeResource](ctx context.Context,
249248

250249
return true
251250
}
251+
252+
// DeletionTracker tracks expected deletions from the source.
253+
type DeletionTracker struct {
254+
mu sync.RWMutex
255+
expected map[types.UID]bool
256+
}
257+
258+
func NewDeletionTracker() *DeletionTracker {
259+
return &DeletionTracker{
260+
expected: make(map[types.UID]bool),
261+
}
262+
}
263+
264+
func (d *DeletionTracker) MarkExpected(uid types.UID) {
265+
d.mu.Lock()
266+
defer d.mu.Unlock()
267+
d.expected[uid] = true
268+
}
269+
270+
func (d *DeletionTracker) IsExpected(uid types.UID) bool {
271+
d.mu.Lock()
272+
defer d.mu.Unlock()
273+
_, exists := d.expected[uid]
274+
if exists {
275+
delete(d.expected, uid)
276+
}
277+
return exists
278+
}
279+
280+
func (d *DeletionTracker) Unmark(uid types.UID) {
281+
d.mu.Lock()
282+
defer d.mu.Unlock()
283+
delete(d.expected, uid)
284+
}

internal/manager/manager_test.go

Lines changed: 17 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ import (
1818
"context"
1919
"testing"
2020

21-
"github.com/argoproj-labs/argocd-agent/internal/cache"
2221
argoapp "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
2322
"github.com/sirupsen/logrus"
2423
"github.com/stretchr/testify/assert"
@@ -79,23 +78,23 @@ func TestRevertUserInitiatedDeletion(t *testing.T) {
7978
},
8079
}
8180
mgr := &fakeManager[*argoapp.Application]{}
82-
sc := cache.NewSourceCache()
83-
ok := RevertUserInitiatedDeletion(context.Background(), app, sc.Application, mgr, newLogger())
81+
deletions := NewDeletionTracker()
82+
ok := RevertUserInitiatedDeletion(context.Background(), app, deletions, mgr, newLogger())
8483
requires.False(ok)
8584
requires.Nil(mgr.created)
8685

87-
// With annotation but cache miss -> no recreate
86+
// With annotation but valid deletion -> no recreate
8887
app.Annotations = map[string]string{SourceUIDAnnotation: string(types.UID("u1"))}
8988
mgr = &fakeManager[*argoapp.Application]{}
90-
ok = RevertUserInitiatedDeletion(context.Background(), app, sc.Application, mgr, newLogger())
89+
deletions.MarkExpected(types.UID("u1"))
90+
ok = RevertUserInitiatedDeletion(context.Background(), app, deletions, mgr, newLogger())
9191
requires.False(ok)
9292
requires.Nil(mgr.created)
9393

94-
// With annotation and cache hit -> recreate
94+
// With annotation and invalid deletion -> recreate
9595
app.Annotations = map[string]string{SourceUIDAnnotation: string(types.UID("u2"))}
96-
sc.Application.Set(types.UID("u2"), argoapp.ApplicationSpec{})
9796
mgr = &fakeManager[*argoapp.Application]{}
98-
ok = RevertUserInitiatedDeletion(context.Background(), app, sc.Application, mgr, newLogger())
97+
ok = RevertUserInitiatedDeletion(context.Background(), app, deletions, mgr, newLogger())
9998
requires.True(ok)
10099
requires.NotNil(mgr.created)
101100
requires.Equal(types.UID("u2"), mgr.created.GetUID())
@@ -112,22 +111,22 @@ func TestRevertUserInitiatedDeletion(t *testing.T) {
112111
Namespace: "argocd",
113112
},
114113
}
115-
sc := cache.NewSourceCache()
114+
deletions := NewDeletionTracker()
116115
mgr := &fakeManager[*argoapp.AppProject]{}
117-
ok := RevertUserInitiatedDeletion(context.Background(), proj, sc.AppProject, mgr, newLogger())
116+
ok := RevertUserInitiatedDeletion(context.Background(), proj, deletions, mgr, newLogger())
118117
requires.False(ok)
119118
requires.Nil(mgr.created)
120119

121120
proj.Annotations = map[string]string{SourceUIDAnnotation: string(types.UID("p1"))}
122121
mgr = &fakeManager[*argoapp.AppProject]{}
123-
ok = RevertUserInitiatedDeletion(context.Background(), proj, sc.AppProject, mgr, newLogger())
122+
deletions.MarkExpected(types.UID("p1"))
123+
ok = RevertUserInitiatedDeletion(context.Background(), proj, deletions, mgr, newLogger())
124124
requires.False(ok)
125125
requires.Nil(mgr.created)
126126

127127
proj.Annotations = map[string]string{SourceUIDAnnotation: string(types.UID("p2"))}
128-
sc.AppProject.Set(types.UID("p2"), argoapp.AppProjectSpec{})
129128
mgr = &fakeManager[*argoapp.AppProject]{}
130-
ok = RevertUserInitiatedDeletion(context.Background(), proj, sc.AppProject, mgr, newLogger())
129+
ok = RevertUserInitiatedDeletion(context.Background(), proj, deletions, mgr, newLogger())
131130
requires.True(ok)
132131
requires.NotNil(mgr.created)
133132
requires.Equal(types.UID("p2"), mgr.created.GetUID())
@@ -144,22 +143,22 @@ func TestRevertUserInitiatedDeletion(t *testing.T) {
144143
Namespace: "argocd",
145144
},
146145
}
147-
sc := cache.NewSourceCache()
146+
deletions := NewDeletionTracker()
148147
mgr := &fakeManager[*corev1.Secret]{}
149-
ok := RevertUserInitiatedDeletion(context.Background(), repo, sc.Repository, mgr, newLogger())
148+
ok := RevertUserInitiatedDeletion(context.Background(), repo, deletions, mgr, newLogger())
150149
requires.False(ok)
151150
requires.Nil(mgr.created)
152151

153152
repo.Annotations = map[string]string{SourceUIDAnnotation: string(types.UID("r1"))}
154153
mgr = &fakeManager[*corev1.Secret]{}
155-
ok = RevertUserInitiatedDeletion(context.Background(), repo, sc.Repository, mgr, newLogger())
154+
deletions.MarkExpected(types.UID("r1"))
155+
ok = RevertUserInitiatedDeletion(context.Background(), repo, deletions, mgr, newLogger())
156156
requires.False(ok)
157157
requires.Nil(mgr.created)
158158

159159
repo.Annotations = map[string]string{SourceUIDAnnotation: string(types.UID("r2"))}
160-
sc.Repository.Set(types.UID("r2"), map[string][]byte{"k": {}})
161160
mgr = &fakeManager[*corev1.Secret]{}
162-
ok = RevertUserInitiatedDeletion(context.Background(), repo, sc.Repository, mgr, newLogger())
161+
ok = RevertUserInitiatedDeletion(context.Background(), repo, deletions, mgr, newLogger())
163162
requires.True(ok)
164163
requires.NotNil(mgr.created)
165164
requires.Equal(types.UID("r2"), mgr.created.GetUID())

principal/callbacks.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ func (s *Server) deleteAppCallback(outbound *v1alpha1.Application) {
125125

126126
// Revert user-initiated deletion on autonomous agent applications
127127
if isResourceFromAutonomousAgent(outbound) {
128-
if manager.RevertUserInitiatedDeletion(s.ctx, outbound, s.sourceCache.Application, s.appManager, logCtx) {
128+
if manager.RevertUserInitiatedDeletion(s.ctx, outbound, s.deletions, s.appManager, logCtx) {
129129
logCtx.Trace("Deleted application is recreated")
130130
return
131131
}
@@ -264,7 +264,7 @@ func (s *Server) deleteAppProjectCallback(outbound *v1alpha1.AppProject) {
264264

265265
// Revert user-initiated deletion on autonomous agent applications
266266
if isResourceFromAutonomousAgent(outbound) {
267-
if manager.RevertUserInitiatedDeletion(s.ctx, outbound, s.sourceCache.AppProject, s.projectManager, logCtx) {
267+
if manager.RevertUserInitiatedDeletion(s.ctx, outbound, s.deletions, s.projectManager, logCtx) {
268268
logCtx.Trace("Deleted appProject is recreated")
269269
return
270270
}

0 commit comments

Comments
 (0)