Skip to content

Commit

Permalink
fix(appset): Fix perpetual appset reconciliation (#19822)
Browse files Browse the repository at this point in the history
Golang maps do not guarantee the order of the application resources
from the applicationset which causes rapid sync activity for the applicationset
as the objects and hence their resourceVersions are updated after each reconcile loop.

This then triggers reconciliation of all objects watching the
ApplicationSet.

In order to prevent this behaviour, ensure that the ApplicationSet
reconciler provides an idempotent list of resources, ensuring objects
are not updated.

Fixes: #19757

Signed-off-by: Thibault Jamet <[email protected]>
Signed-off-by: Fabián Sellés <[email protected]>
Co-authored-by: Fabian Selles <[email protected]>
Co-authored-by: Ariadna Rouco <[email protected]>
  • Loading branch information
3 people committed Sep 18, 2024
1 parent 8590550 commit 300fe3d
Show file tree
Hide file tree
Showing 2 changed files with 103 additions and 0 deletions.
4 changes: 4 additions & 0 deletions applicationset/controllers/applicationset_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"context"
"fmt"
"reflect"
"sort"
"strings"
"time"

Expand Down Expand Up @@ -1365,6 +1366,9 @@ func (r *ApplicationSetReconciler) updateResourcesStatus(ctx context.Context, lo
for _, status := range statusMap {
statuses = append(statuses, status)
}
sort.Slice(statuses, func(i, j int) bool {
return statuses[i].Name < statuses[j].Name
})
appset.Status.Resources = statuses

namespacedName := types.NamespacedName{Namespace: appset.Namespace, Name: appset.Name}
Expand Down
99 changes: 99 additions & 0 deletions applicationset/controllers/applicationset_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"encoding/json"
"fmt"
"reflect"
"strconv"
"strings"
"testing"
"time"
Expand Down Expand Up @@ -6351,6 +6352,104 @@ func TestUpdateResourceStatus(t *testing.T) {
}
}

func generateNAppResourceStatuses(n int) []v1alpha1.ResourceStatus {
var r []v1alpha1.ResourceStatus
for i := 0; i < n; i++ {
r = append(r, v1alpha1.ResourceStatus{
Name: "app" + strconv.Itoa(i),
Status: v1alpha1.SyncStatusCodeSynced,
Health: &v1alpha1.HealthStatus{
Status: health.HealthStatusHealthy,
Message: "OK",
},
},
)
}
return r
}

func generateNHealthyApps(n int) []v1alpha1.Application {
var r []v1alpha1.Application
for i := 0; i < n; i++ {
r = append(r, v1alpha1.Application{
ObjectMeta: metav1.ObjectMeta{
Name: "app" + strconv.Itoa(i),
},
Status: v1alpha1.ApplicationStatus{
Sync: v1alpha1.SyncStatus{
Status: v1alpha1.SyncStatusCodeSynced,
},
Health: v1alpha1.HealthStatus{
Status: health.HealthStatusHealthy,
Message: "OK",
},
},
})
}
return r
}

func TestResourceStatusAreOrdered(t *testing.T) {
scheme := runtime.NewScheme()
err := v1alpha1.AddToScheme(scheme)
require.NoError(t, err)

err = v1alpha1.AddToScheme(scheme)
require.NoError(t, err)
for _, cc := range []struct {
name string
appSet v1alpha1.ApplicationSet
apps []v1alpha1.Application
expectedResources []v1alpha1.ResourceStatus
}{
{
name: "Ensures AppSet is always ordered",
appSet: v1alpha1.ApplicationSet{
ObjectMeta: metav1.ObjectMeta{
Name: "name",
Namespace: "argocd",
},
Status: v1alpha1.ApplicationSetStatus{
Resources: []v1alpha1.ResourceStatus{},
},
},
apps: generateNHealthyApps(10),
expectedResources: generateNAppResourceStatuses(10),
},
} {
t.Run(cc.name, func(t *testing.T) {
kubeclientset := kubefake.NewSimpleClientset([]runtime.Object{}...)
argoDBMock := dbmocks.ArgoDB{}
argoObjs := []runtime.Object{}

client := fake.NewClientBuilder().WithScheme(scheme).WithStatusSubresource(&cc.appSet).WithObjects(&cc.appSet).Build()
metrics := appsetmetrics.NewFakeAppsetMetrics(client)

Check failure on line 6426 in applicationset/controllers/applicationset_controller_test.go

View workflow job for this annotation

GitHub Actions / Lint Go code

undefined: appsetmetrics

Check failure on line 6426 in applicationset/controllers/applicationset_controller_test.go

View workflow job for this annotation

GitHub Actions / Run unit tests for Go packages

undefined: appsetmetrics

Check failure on line 6426 in applicationset/controllers/applicationset_controller_test.go

View workflow job for this annotation

GitHub Actions / Run unit tests for Go packages

undefined: appsetmetrics

Check failure on line 6426 in applicationset/controllers/applicationset_controller_test.go

View workflow job for this annotation

GitHub Actions / Run unit tests with -race for Go packages

undefined: appsetmetrics

Check failure on line 6426 in applicationset/controllers/applicationset_controller_test.go

View workflow job for this annotation

GitHub Actions / Run unit tests with -race for Go packages

undefined: appsetmetrics

r := ApplicationSetReconciler{
Client: client,
Scheme: scheme,
Recorder: record.NewFakeRecorder(1),
Generators: map[string]generators.Generator{},
ArgoDB: &argoDBMock,
ArgoAppClientset: appclientset.NewSimpleClientset(argoObjs...),
KubeClientset: kubeclientset,
Metrics: metrics,

Check failure on line 6436 in applicationset/controllers/applicationset_controller_test.go

View workflow job for this annotation

GitHub Actions / Lint Go code

unknown field Metrics in struct literal of type ApplicationSetReconciler (typecheck)

Check failure on line 6436 in applicationset/controllers/applicationset_controller_test.go

View workflow job for this annotation

GitHub Actions / Run unit tests for Go packages

unknown field Metrics in struct literal of type ApplicationSetReconciler

Check failure on line 6436 in applicationset/controllers/applicationset_controller_test.go

View workflow job for this annotation

GitHub Actions / Run unit tests for Go packages

unknown field Metrics in struct literal of type ApplicationSetReconciler

Check failure on line 6436 in applicationset/controllers/applicationset_controller_test.go

View workflow job for this annotation

GitHub Actions / Run unit tests with -race for Go packages

unknown field Metrics in struct literal of type ApplicationSetReconciler

Check failure on line 6436 in applicationset/controllers/applicationset_controller_test.go

View workflow job for this annotation

GitHub Actions / Run unit tests with -race for Go packages

unknown field Metrics in struct literal of type ApplicationSetReconciler
}

err := r.updateResourcesStatus(context.TODO(), log.NewEntry(log.StandardLogger()), &cc.appSet, cc.apps)
require.NoError(t, err, "expected no errors, but errors occurred")

err = r.updateResourcesStatus(context.TODO(), log.NewEntry(log.StandardLogger()), &cc.appSet, cc.apps)
require.NoError(t, err, "expected no errors, but errors occurred")

err = r.updateResourcesStatus(context.TODO(), log.NewEntry(log.StandardLogger()), &cc.appSet, cc.apps)
require.NoError(t, err, "expected no errors, but errors occurred")

assert.Equal(t, cc.expectedResources, cc.appSet.Status.Resources, "expected resources did not match actual")
})
}
}

func TestOwnsHandler(t *testing.T) {
// progressive syncs do not affect create, delete, or generic
ownsHandler := getOwnsHandlerPredicates(true)
Expand Down

0 comments on commit 300fe3d

Please sign in to comment.