fix: trigger app sync on app-set spec change (cherry-pick #26811 for 3.3) (#27130)

Signed-off-by: Patroklos Papapetrou <ppapapetrou76@gmail.com>
Co-authored-by: Papapetrou Patroklos <1743100+ppapapetrou76@users.noreply.github.com>
Co-authored-by: Jann Fischer <jann@mistrust.net>
This commit is contained in:
argo-cd-cherry-pick-bot[bot] 2026-04-05 18:50:09 -04:00 committed by GitHub
parent a77c1501fe
commit 2512512b0c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 230 additions and 8 deletions

View file

@ -73,6 +73,9 @@ const (
ReconcileRequeueOnValidationError = time.Minute * 3
ReverseDeletionOrder = "Reverse"
AllAtOnceDeletionOrder = "AllAtOnce"
revisionAndSpecChangedMsg = "Application has pending changes (revision and spec differ), setting status to Waiting"
revisionChangedMsg = "Application has pending changes, setting status to Waiting"
specChangedMsg = "Application has pending changes (spec differs), setting status to Waiting"
)
var defaultPreservedFinalizers = []string{
@ -960,7 +963,7 @@ func (r *ApplicationSetReconciler) removeOwnerReferencesOnDeleteAppSet(ctx conte
func (r *ApplicationSetReconciler) performProgressiveSyncs(ctx context.Context, logCtx *log.Entry, appset argov1alpha1.ApplicationSet, applications []argov1alpha1.Application, desiredApplications []argov1alpha1.Application) (map[string]bool, error) {
appDependencyList, appStepMap := r.buildAppDependencyList(logCtx, appset, desiredApplications)
_, err := r.updateApplicationSetApplicationStatus(ctx, logCtx, &appset, applications, appStepMap)
_, err := r.updateApplicationSetApplicationStatus(ctx, logCtx, &appset, applications, desiredApplications, appStepMap)
if err != nil {
return nil, fmt.Errorf("failed to update applicationset app status: %w", err)
}
@ -1139,10 +1142,16 @@ func getAppStep(appName string, appStepMap map[string]int) int {
}
// check the status of each Application's status and promote Applications to the next status if needed
func (r *ApplicationSetReconciler) updateApplicationSetApplicationStatus(ctx context.Context, logCtx *log.Entry, applicationSet *argov1alpha1.ApplicationSet, applications []argov1alpha1.Application, appStepMap map[string]int) ([]argov1alpha1.ApplicationSetApplicationStatus, error) {
func (r *ApplicationSetReconciler) updateApplicationSetApplicationStatus(ctx context.Context, logCtx *log.Entry, applicationSet *argov1alpha1.ApplicationSet, applications []argov1alpha1.Application, desiredApplications []argov1alpha1.Application, appStepMap map[string]int) ([]argov1alpha1.ApplicationSetApplicationStatus, error) {
now := metav1.Now()
appStatuses := make([]argov1alpha1.ApplicationSetApplicationStatus, 0, len(applications))
// Build a map of desired applications for quick lookup
desiredAppsMap := make(map[string]*argov1alpha1.Application)
for i := range desiredApplications {
desiredAppsMap[desiredApplications[i].Name] = &desiredApplications[i]
}
for _, app := range applications {
appHealthStatus := app.Status.Health.Status
appSyncStatus := app.Status.Sync.Status
@ -1177,10 +1186,27 @@ func (r *ApplicationSetReconciler) updateApplicationSetApplicationStatus(ctx con
newAppStatus := currentAppStatus.DeepCopy()
newAppStatus.Step = strconv.Itoa(getAppStep(newAppStatus.Application, appStepMap))
if !reflect.DeepEqual(currentAppStatus.TargetRevisions, app.Status.GetRevisions()) {
// A new version is available in the application and we need to re-sync the application
revisionsChanged := !reflect.DeepEqual(currentAppStatus.TargetRevisions, app.Status.GetRevisions())
// Check if the desired Application spec differs from the current Application spec
specChanged := false
if desiredApp, ok := desiredAppsMap[app.Name]; ok {
// Compare the desired spec with the current spec to detect non-Git changes
// This will catch changes to generator parameters like image tags, helm values, etc.
specChanged = !cmp.Equal(desiredApp.Spec, app.Spec, cmpopts.EquateEmpty(), cmpopts.EquateComparable(argov1alpha1.ApplicationDestination{}))
}
if revisionsChanged || specChanged {
newAppStatus.TargetRevisions = app.Status.GetRevisions()
newAppStatus.Message = "Application has pending changes, setting status to Waiting"
switch {
case revisionsChanged && specChanged:
newAppStatus.Message = revisionAndSpecChangedMsg
case revisionsChanged:
newAppStatus.Message = revisionChangedMsg
default:
newAppStatus.Message = specChangedMsg
}
newAppStatus.Status = argov1alpha1.ProgressiveSyncWaiting
newAppStatus.LastTransitionTime = &now
}

View file

@ -4637,6 +4637,12 @@ func TestUpdateApplicationSetApplicationStatus(t *testing.T) {
}
}
newAppWithSpec := func(name string, health health.HealthStatusCode, sync v1alpha1.SyncStatusCode, revision string, opState *v1alpha1.OperationState, spec v1alpha1.ApplicationSpec) v1alpha1.Application {
app := newApp(name, health, sync, revision, opState)
app.Spec = spec
return app
}
newOperationState := func(phase common.OperationPhase) *v1alpha1.OperationState {
finishedAt := &metav1.Time{Time: time.Now().Add(-1 * time.Second)}
if !phase.Completed() {
@ -4653,6 +4659,7 @@ func TestUpdateApplicationSetApplicationStatus(t *testing.T) {
name string
appSet v1alpha1.ApplicationSet
apps []v1alpha1.Application
desiredApps []v1alpha1.Application
appStepMap map[string]int
expectedAppStatus []v1alpha1.ApplicationSetApplicationStatus
}{
@ -4806,14 +4813,14 @@ func TestUpdateApplicationSetApplicationStatus(t *testing.T) {
expectedAppStatus: []v1alpha1.ApplicationSetApplicationStatus{
{
Application: "app1",
Message: "Application has pending changes, setting status to Waiting",
Message: revisionChangedMsg,
Status: v1alpha1.ProgressiveSyncWaiting,
Step: "1",
TargetRevisions: []string{"next"},
},
{
Application: "app2-multisource",
Message: "Application has pending changes, setting status to Waiting",
Message: revisionChangedMsg,
Status: v1alpha1.ProgressiveSyncWaiting,
Step: "1",
TargetRevisions: []string{"next"},
@ -5253,6 +5260,191 @@ func TestUpdateApplicationSetApplicationStatus(t *testing.T) {
},
},
},
{
name: "detects spec changes when image tag changes in generator (same Git revision)",
appSet: newDefaultAppSet(2, []v1alpha1.ApplicationSetApplicationStatus{
{
Application: "app1",
Message: "",
Status: v1alpha1.ProgressiveSyncHealthy,
Step: "1",
TargetRevisions: []string{"abc123"},
},
}),
apps: []v1alpha1.Application{
newAppWithSpec("app1", health.HealthStatusHealthy, v1alpha1.SyncStatusCodeOutOfSync, "abc123", nil, // Changed to OutOfSync
v1alpha1.ApplicationSpec{
Source: &v1alpha1.ApplicationSource{
RepoURL: "https://example.com/repo.git",
TargetRevision: "master",
Helm: &v1alpha1.ApplicationSourceHelm{
Parameters: []v1alpha1.HelmParameter{
{Name: "image.tag", Value: "v1.0.0"},
},
},
},
Destination: v1alpha1.ApplicationDestination{
Server: "https://kubernetes.default.svc",
Namespace: "default",
},
}),
},
desiredApps: []v1alpha1.Application{
newAppWithSpec("app1", health.HealthStatusHealthy, v1alpha1.SyncStatusCodeOutOfSync, "abc123", nil, // Changed to OutOfSync
v1alpha1.ApplicationSpec{
Source: &v1alpha1.ApplicationSource{
RepoURL: "https://example.com/repo.git",
TargetRevision: "master",
Helm: &v1alpha1.ApplicationSourceHelm{
Parameters: []v1alpha1.HelmParameter{
{Name: "image.tag", Value: "v2.0.0"}, // Different value
},
},
},
Destination: v1alpha1.ApplicationDestination{
Server: "https://kubernetes.default.svc",
Namespace: "default",
},
}),
},
appStepMap: map[string]int{
"app1": 0,
},
expectedAppStatus: []v1alpha1.ApplicationSetApplicationStatus{
{
Application: "app1",
Message: specChangedMsg,
Status: v1alpha1.ProgressiveSyncWaiting,
Step: "1",
TargetRevisions: []string{"abc123"},
},
},
},
{
name: "does not detect changes when spec is identical (same Git revision)",
appSet: newDefaultAppSet(2, []v1alpha1.ApplicationSetApplicationStatus{
{
Application: "app1",
Message: "",
Status: v1alpha1.ProgressiveSyncHealthy,
Step: "1",
TargetRevisions: []string{"abc123"},
},
}),
apps: []v1alpha1.Application{
newAppWithSpec("app1", health.HealthStatusHealthy, v1alpha1.SyncStatusCodeSynced, "abc123", nil,
v1alpha1.ApplicationSpec{
Source: &v1alpha1.ApplicationSource{
RepoURL: "https://example.com/repo.git",
TargetRevision: "master",
Helm: &v1alpha1.ApplicationSourceHelm{
Parameters: []v1alpha1.HelmParameter{
{Name: "image.tag", Value: "v1.0.0"},
},
},
},
Destination: v1alpha1.ApplicationDestination{
Server: "https://kubernetes.default.svc",
Namespace: "default",
},
}),
},
appStepMap: map[string]int{
"app1": 0,
},
// Desired apps have identical spec
desiredApps: []v1alpha1.Application{
{
ObjectMeta: metav1.ObjectMeta{
Name: "app1",
},
Spec: v1alpha1.ApplicationSpec{
Source: &v1alpha1.ApplicationSource{
RepoURL: "https://example.com/repo.git",
TargetRevision: "master",
Helm: &v1alpha1.ApplicationSourceHelm{
Parameters: []v1alpha1.HelmParameter{
{Name: "image.tag", Value: "v1.0.0"}, // Same value
},
},
},
Destination: v1alpha1.ApplicationDestination{
Server: "https://kubernetes.default.svc",
Namespace: "default",
},
},
},
},
expectedAppStatus: []v1alpha1.ApplicationSetApplicationStatus{
{
Application: "app1",
Message: "",
Status: v1alpha1.ProgressiveSyncHealthy,
Step: "1",
TargetRevisions: []string{"abc123"},
},
},
},
{
name: "detects both spec and revision changes",
appSet: newDefaultAppSet(2, []v1alpha1.ApplicationSetApplicationStatus{
{
Application: "app1",
Message: "",
Status: v1alpha1.ProgressiveSyncHealthy,
Step: "1",
TargetRevisions: []string{"abc123"}, // OLD revision in status
},
}),
apps: []v1alpha1.Application{
newAppWithSpec("app1", health.HealthStatusHealthy, v1alpha1.SyncStatusCodeOutOfSync, "def456", nil, // NEW revision, but OutOfSync
v1alpha1.ApplicationSpec{
Source: &v1alpha1.ApplicationSource{
RepoURL: "https://example.com/repo.git",
TargetRevision: "master",
Helm: &v1alpha1.ApplicationSourceHelm{
Parameters: []v1alpha1.HelmParameter{
{Name: "image.tag", Value: "v1.0.0"},
},
},
},
Destination: v1alpha1.ApplicationDestination{
Server: "https://kubernetes.default.svc",
Namespace: "default",
},
}),
},
desiredApps: []v1alpha1.Application{
newAppWithSpec("app1", health.HealthStatusHealthy, v1alpha1.SyncStatusCodeOutOfSync, "def456", nil,
v1alpha1.ApplicationSpec{
Source: &v1alpha1.ApplicationSource{
RepoURL: "https://example.com/repo.git",
TargetRevision: "master",
Helm: &v1alpha1.ApplicationSourceHelm{
Parameters: []v1alpha1.HelmParameter{
{Name: "image.tag", Value: "v2.0.0"}, // Changed value
},
},
},
Destination: v1alpha1.ApplicationDestination{
Server: "https://kubernetes.default.svc",
Namespace: "default",
},
}),
},
appStepMap: map[string]int{
"app1": 0,
},
expectedAppStatus: []v1alpha1.ApplicationSetApplicationStatus{
{
Application: "app1",
Message: revisionAndSpecChangedMsg,
Status: v1alpha1.ProgressiveSyncWaiting,
Step: "1",
TargetRevisions: []string{"def456"},
},
},
},
} {
t.Run(cc.name, func(t *testing.T) {
kubeclientset := kubefake.NewSimpleClientset([]runtime.Object{}...)
@ -5272,7 +5464,11 @@ func TestUpdateApplicationSetApplicationStatus(t *testing.T) {
Metrics: metrics,
}
appStatuses, err := r.updateApplicationSetApplicationStatus(t.Context(), log.NewEntry(log.StandardLogger()), &cc.appSet, cc.apps, cc.appStepMap)
desiredApps := cc.desiredApps
if desiredApps == nil {
desiredApps = cc.apps
}
appStatuses, err := r.updateApplicationSetApplicationStatus(t.Context(), log.NewEntry(log.StandardLogger()), &cc.appSet, cc.apps, desiredApps, cc.appStepMap)
// opt out of testing the LastTransitionTime is accurate
for i := range appStatuses {