Skip to content

Commit

Permalink
feat: Pre/PostDelete hooks support
Browse files Browse the repository at this point in the history
Signed-off-by: Alexander Matyushentsev <AMatyushentsev@gmail.com>
  • Loading branch information
alexmt committed Dec 16, 2023
1 parent a761a49 commit 70c0a24
Show file tree
Hide file tree
Showing 8 changed files with 293 additions and 69 deletions.
151 changes: 99 additions & 52 deletions controller/appcontroller.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import (
"sync"
"time"

"github.com/argoproj/argo-cd/v2/pkg/ratelimiter"
clustercache "github.com/argoproj/gitops-engine/pkg/cache"
"github.com/argoproj/gitops-engine/pkg/diff"
"github.com/argoproj/gitops-engine/pkg/health"
Expand Down Expand Up @@ -59,6 +58,7 @@ import (

kubeerrors "k8s.io/apimachinery/pkg/api/errors"

"github.com/argoproj/argo-cd/v2/pkg/ratelimiter"
appstatecache "github.com/argoproj/argo-cd/v2/util/cache/appstate"
"github.com/argoproj/argo-cd/v2/util/db"
"github.com/argoproj/argo-cd/v2/util/errors"
Expand Down Expand Up @@ -884,11 +884,10 @@ func (ctrl *ApplicationController) processAppOperationQueueItem() (processNext b

if app.Operation != nil {
ctrl.processRequestedAppOperation(app)
} else if app.DeletionTimestamp != nil && app.CascadedDeletion() {
_, err = ctrl.finalizeApplicationDeletion(app, func(project string) ([]*appv1.Cluster, error) {
} else if app.DeletionTimestamp != nil {
if err = ctrl.finalizeApplicationDeletion(app, func(project string) ([]*appv1.Cluster, error) {
return ctrl.db.GetProjectClusters(context.Background(), project)
})
if err != nil {
}); err != nil {
ctrl.setAppCondition(app, appv1.ApplicationCondition{
Type: appv1.ApplicationConditionDeletionError,
Message: err.Error(),
Expand Down Expand Up @@ -1023,66 +1022,70 @@ func (ctrl *ApplicationController) getPermittedAppLiveObjects(app *appv1.Applica
return objsMap, nil
}

func (ctrl *ApplicationController) finalizeApplicationDeletion(app *appv1.Application, projectClusters func(project string) ([]*appv1.Cluster, error)) ([]*unstructured.Unstructured, error) {
func (ctrl *ApplicationController) isValidDestination(app *appv1.Application) (bool, *argov1alpha.Cluster) {
// Validate the cluster using the Application destination's `name` field, if applicable,
// and set the Server field, if needed.
if err := argo.ValidateDestination(context.Background(), &app.Spec.Destination, ctrl.db); err != nil {
log.Warnf("Unable to validate destination of the Application being deleted: %v", err)
return false, nil
}

cluster, err := ctrl.db.GetCluster(context.Background(), app.Spec.Destination.Server)
if err != nil {
log.Warnf("Unable to locate cluster URL for Application being deleted: %v", err)
return false, nil
}
return true, cluster
}

func (ctrl *ApplicationController) finalizeApplicationDeletion(app *appv1.Application, projectClusters func(project string) ([]*appv1.Cluster, error)) error {
logCtx := log.WithField("application", app.QualifiedName())
logCtx.Infof("Deleting resources")
// Get refreshed application info, since informer app copy might be stale
app, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(app.Namespace).Get(context.Background(), app.Name, metav1.GetOptions{})
if err != nil {
if !apierr.IsNotFound(err) {
logCtx.Errorf("Unable to get refreshed application info prior deleting resources: %v", err)
}
return nil, nil
return nil
}
proj, err := ctrl.getAppProj(app)
if err != nil {
return nil, err
}

// validDestination is true if the Application destination points to a cluster that is managed by Argo CD
// (and thus either a cluster secret exists for it, or it's local); validDestination is false otherwise.
validDestination := true

// Validate the cluster using the Application destination's `name` field, if applicable,
// and set the Server field, if needed.
if err := argo.ValidateDestination(context.Background(), &app.Spec.Destination, ctrl.db); err != nil {
log.Warnf("Unable to validate destination of the Application being deleted: %v", err)
validDestination = false
return err
}

objs := make([]*unstructured.Unstructured, 0)
var cluster *appv1.Cluster

// Attempt to validate the destination via its URL
if validDestination {
if cluster, err = ctrl.db.GetCluster(context.Background(), app.Spec.Destination.Server); err != nil {
log.Warnf("Unable to locate cluster URL for Application being deleted: %v", err)
validDestination = false
isValid, cluster := ctrl.isValidDestination(app)
if !isValid {
app.UnSetCascadedDeletion()
app.UnSetPostDeleteFinalizer()
if err := ctrl.updateFinalizers(app); err != nil {
return err
}
logCtx.Infof("Resource entries removed from undefined cluster")
return nil
}
config := metrics.AddMetricsTransportWrapper(ctrl.metricsServer, app, cluster.RESTConfig())

if validDestination {
if app.CascadedDeletion() {
logCtx.Infof("Deleting resources")
// ApplicationDestination points to a valid cluster, so we may clean up the live objects

objs := make([]*unstructured.Unstructured, 0)
objsMap, err := ctrl.getPermittedAppLiveObjects(app, proj, projectClusters)
if err != nil {
return nil, err
return err
}

for k := range objsMap {
// Wait for objects pending deletion to complete before proceeding with next sync wave
if objsMap[k].GetDeletionTimestamp() != nil {
logCtx.Infof("%d objects remaining for deletion", len(objsMap))
return objs, nil
return nil
}

if ctrl.shouldBeDeleted(app, objsMap[k]) {
objs = append(objs, objsMap[k])
}
}

config := metrics.AddMetricsTransportWrapper(ctrl.metricsServer, app, cluster.RESTConfig())

filteredObjs := FilterObjectsForDeletion(objs)

propagationPolicy := metav1.DeletePropagationForeground
Expand All @@ -1096,12 +1099,12 @@ func (ctrl *ApplicationController) finalizeApplicationDeletion(app *appv1.Applic
return ctrl.kubectl.DeleteResource(context.Background(), config, obj.GroupVersionKind(), obj.GetName(), obj.GetNamespace(), metav1.DeleteOptions{PropagationPolicy: &propagationPolicy})
})
if err != nil {
return objs, err
return err
}

objsMap, err = ctrl.getPermittedAppLiveObjects(app, proj, projectClusters)
if err != nil {
return nil, err
return err
}

for k, obj := range objsMap {
Expand All @@ -1111,38 +1114,68 @@ func (ctrl *ApplicationController) finalizeApplicationDeletion(app *appv1.Applic
}
if len(objsMap) > 0 {
logCtx.Infof("%d objects remaining for deletion", len(objsMap))
return objs, nil
return nil
}
logCtx.Infof("Successfully deleted %d resources", len(objs))
logCtx.Infof("Resource entries removed from undefined cluster")
app.UnSetCascadedDeletion()
return ctrl.updateFinalizers(app)
}

if err := ctrl.cache.SetAppManagedResources(app.Name, nil); err != nil {
return objs, err
}
if app.HasPostDeleteFinalizer() {
objsMap, err := ctrl.getPermittedAppLiveObjects(app, proj, projectClusters)
if err != nil {
return err
}

if err := ctrl.cache.SetAppResourcesTree(app.Name, nil); err != nil {
return objs, err
done, err := ctrl.executePostDeleteHooks(app, proj, objsMap, config, logCtx)
if err != nil {
return err
}
if !done {
return nil
}
app.UnSetPostDeleteFinalizer()
return ctrl.updateFinalizers(app)
}

if err := ctrl.removeCascadeFinalizer(app); err != nil {
return objs, err
if app.HasPostDeleteFinalizer("cleanup") {
objsMap, err := ctrl.getPermittedAppLiveObjects(app, proj, projectClusters)
if err != nil {
return err
}

done, err := ctrl.cleanupPostDeleteHooks(objsMap, config, logCtx)
if err != nil {
return err
}
if !done {
return nil
}
app.UnSetPostDeleteFinalizer("cleanup")
return ctrl.updateFinalizers(app)
}

if validDestination {
logCtx.Infof("Successfully deleted %d resources", len(objs))
} else {
logCtx.Infof("Resource entries removed from undefined cluster")
if !app.CascadedDeletion() && !app.HasPostDeleteFinalizer() {
if err := ctrl.cache.SetAppManagedResources(app.Name, nil); err != nil {
return err
}

if err := ctrl.cache.SetAppResourcesTree(app.Name, nil); err != nil {
return err
}
ctrl.projectRefreshQueue.Add(fmt.Sprintf("%s/%s", ctrl.namespace, app.Spec.GetProject()))
}

ctrl.projectRefreshQueue.Add(fmt.Sprintf("%s/%s", ctrl.namespace, app.Spec.GetProject()))
return objs, nil
return nil
}

func (ctrl *ApplicationController) removeCascadeFinalizer(app *appv1.Application) error {
func (ctrl *ApplicationController) updateFinalizers(app *appv1.Application) error {
_, err := ctrl.getAppProj(app)
if err != nil {
return fmt.Errorf("error getting project: %w", err)
}
app.UnSetCascadedDeletion()

var patch []byte
patch, _ = json.Marshal(map[string]interface{}{
"metadata": map[string]interface{}{
Expand Down Expand Up @@ -1566,6 +1599,20 @@ func (ctrl *ApplicationController) processAppRefreshQueueItem() (processNext boo
app.Status.SourceTypes = compareResult.appSourceTypes
app.Status.ControllerNamespace = ctrl.namespace
patchMs = ctrl.persistAppStatus(origApp, &app.Status)
if (compareResult.hasPostDeleteHooks != app.HasPostDeleteFinalizer() || compareResult.hasPostDeleteHooks != app.HasPostDeleteFinalizer("cleanup")) &&
app.GetDeletionTimestamp() == nil {
if compareResult.hasPostDeleteHooks {
app.SetPostDeleteFinalizer()
app.SetPostDeleteFinalizer("cleanup")
} else {
app.UnSetPostDeleteFinalizer()
app.UnSetPostDeleteFinalizer("cleanup")
}

if err := ctrl.updateFinalizers(app); err != nil {
logCtx.Errorf("Failed to update finalizers: %v", err)
}
}
return
}

Expand Down
20 changes: 12 additions & 8 deletions controller/appcontroller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -619,6 +619,7 @@ func TestFinalizeAppDeletion(t *testing.T) {
// Ensure app can be deleted cascading
t.Run("CascadingDelete", func(t *testing.T) {
app := newFakeApp()
app.SetCascadedDeletion(v1alpha1.ResourcesFinalizerName)
app.Spec.Destination.Namespace = test.FakeArgoCDNamespace
appObj := kube.MustToUnstructured(&app)
ctrl := newFakeController(&fakeData{apps: []runtime.Object{app, &defaultProj}, managedLiveObjs: map[kube.ResourceKey]*unstructured.Unstructured{
Expand All @@ -636,7 +637,7 @@ func TestFinalizeAppDeletion(t *testing.T) {
patched = true
return true, &v1alpha1.Application{}, nil
})
_, err := ctrl.finalizeApplicationDeletion(app, func(project string) ([]*v1alpha1.Cluster, error) {
err := ctrl.finalizeApplicationDeletion(app, func(project string) ([]*v1alpha1.Cluster, error) {
return []*v1alpha1.Cluster{}, nil
})
assert.NoError(t, err)
Expand All @@ -662,6 +663,7 @@ func TestFinalizeAppDeletion(t *testing.T) {
},
}
app := newFakeApp()
app.SetCascadedDeletion(v1alpha1.ResourcesFinalizerName)
app.Spec.Destination.Namespace = test.FakeArgoCDNamespace
app.Spec.Project = "restricted"
appObj := kube.MustToUnstructured(&app)
Expand All @@ -686,7 +688,7 @@ func TestFinalizeAppDeletion(t *testing.T) {
patched = true
return true, &v1alpha1.Application{}, nil
})
objs, err := ctrl.finalizeApplicationDeletion(app, func(project string) ([]*v1alpha1.Cluster, error) {
err := ctrl.finalizeApplicationDeletion(app, func(project string) ([]*v1alpha1.Cluster, error) {
return []*v1alpha1.Cluster{}, nil
})
assert.NoError(t, err)
Expand All @@ -697,14 +699,16 @@ func TestFinalizeAppDeletion(t *testing.T) {
}
// Managed objects must be empty
assert.Empty(t, objsMap)
// Loop through all deleted objects, ensure that test-cm is none of them
for _, o := range objs {
assert.NotEqual(t, "test-cm", o.GetName())
}
//// Loop through all deleted objects, ensure that test-cm is none of them

//for _, o := range objs {
// assert.NotEqual(t, "test-cm", o.GetName())
//}
})

t.Run("DeleteWithDestinationClusterName", func(t *testing.T) {
app := newFakeAppWithDestName()
app.SetCascadedDeletion(v1alpha1.ResourcesFinalizerName)
appObj := kube.MustToUnstructured(&app)
ctrl := newFakeController(&fakeData{apps: []runtime.Object{app, &defaultProj}, managedLiveObjs: map[kube.ResourceKey]*unstructured.Unstructured{
kube.GetResourceKey(appObj): appObj,
Expand All @@ -720,7 +724,7 @@ func TestFinalizeAppDeletion(t *testing.T) {
patched = true
return true, &v1alpha1.Application{}, nil
})
_, err := ctrl.finalizeApplicationDeletion(app, func(project string) ([]*v1alpha1.Cluster, error) {
err := ctrl.finalizeApplicationDeletion(app, func(project string) ([]*v1alpha1.Cluster, error) {
return []*v1alpha1.Cluster{}, nil
})
assert.NoError(t, err)
Expand All @@ -745,7 +749,7 @@ func TestFinalizeAppDeletion(t *testing.T) {
fakeAppCs.AddReactor("get", "*", func(action kubetesting.Action) (handled bool, ret runtime.Object, err error) {
return defaultReactor.React(action)
})
_, err := ctrl.finalizeApplicationDeletion(app, func(project string) ([]*v1alpha1.Cluster, error) {
err := ctrl.finalizeApplicationDeletion(app, func(project string) ([]*v1alpha1.Cluster, error) {
return []*v1alpha1.Cluster{}, nil
})
assert.NoError(t, err)
Expand Down
Loading

0 comments on commit 70c0a24

Please sign in to comment.