github.com/argoproj/argo-cd/v3@v3.2.1/applicationset/controllers/applicationset_controller.go (about) 1 /* 2 Licensed under the Apache License, Version 2.0 (the "License"); 3 you may not use this file except in compliance with the License. 4 You may obtain a copy of the License at 5 6 http://www.apache.org/licenses/LICENSE-2.0 7 8 Unless required by applicable law or agreed to in writing, software 9 distributed under the License is distributed on an "AS IS" BASIS, 10 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 See the License for the specific language governing permissions and 12 limitations under the License. 13 */ 14 15 package controllers 16 17 import ( 18 "context" 19 "errors" 20 "fmt" 21 "reflect" 22 "runtime/debug" 23 "sort" 24 "strconv" 25 "strings" 26 "time" 27 28 "github.com/google/go-cmp/cmp" 29 "github.com/google/go-cmp/cmp/cmpopts" 30 log "github.com/sirupsen/logrus" 31 corev1 "k8s.io/api/core/v1" 32 apierrors "k8s.io/apimachinery/pkg/api/errors" 33 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 34 "k8s.io/apimachinery/pkg/runtime" 35 "k8s.io/apimachinery/pkg/types" 36 "k8s.io/apimachinery/pkg/util/intstr" 37 "k8s.io/client-go/kubernetes" 38 "k8s.io/client-go/tools/record" 39 "k8s.io/client-go/util/retry" 40 ctrl "sigs.k8s.io/controller-runtime" 41 "sigs.k8s.io/controller-runtime/pkg/builder" 42 "sigs.k8s.io/controller-runtime/pkg/client" 43 "sigs.k8s.io/controller-runtime/pkg/controller" 44 "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" 45 "sigs.k8s.io/controller-runtime/pkg/event" 46 "sigs.k8s.io/controller-runtime/pkg/handler" 47 "sigs.k8s.io/controller-runtime/pkg/predicate" 48 49 "github.com/argoproj/argo-cd/v3/applicationset/controllers/template" 50 "github.com/argoproj/argo-cd/v3/applicationset/generators" 51 "github.com/argoproj/argo-cd/v3/applicationset/metrics" 52 "github.com/argoproj/argo-cd/v3/applicationset/status" 53 "github.com/argoproj/argo-cd/v3/applicationset/utils" 54 "github.com/argoproj/argo-cd/v3/common" 55 applog "github.com/argoproj/argo-cd/v3/util/app/log" 56 "github.com/argoproj/argo-cd/v3/util/db" 57 58 argov1alpha1 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1" 59 argoutil "github.com/argoproj/argo-cd/v3/util/argo" 60 "github.com/argoproj/argo-cd/v3/util/argo/normalizers" 61 62 "github.com/argoproj/argo-cd/v3/pkg/apis/application" 63 ) 64 65 const ( 66 // Rather than importing the whole argocd-notifications controller, just copying the const here 67 // https://github.com/argoproj-labs/argocd-notifications/blob/33d345fa838829bb50fca5c08523aba380d2c12b/pkg/controller/subscriptions.go#L12 68 // https://github.com/argoproj-labs/argocd-notifications/blob/33d345fa838829bb50fca5c08523aba380d2c12b/pkg/controller/state.go#L17 69 NotifiedAnnotationKey = "notified.notifications.argoproj.io" 70 ReconcileRequeueOnValidationError = time.Minute * 3 71 ReverseDeletionOrder = "Reverse" 72 AllAtOnceDeletionOrder = "AllAtOnce" 73 ) 74 75 var defaultPreservedAnnotations = []string{ 76 NotifiedAnnotationKey, 77 argov1alpha1.AnnotationKeyRefresh, 78 } 79 80 type deleteInOrder struct { 81 AppName string 82 Step int 83 } 84 85 // ApplicationSetReconciler reconciles a ApplicationSet object 86 type ApplicationSetReconciler struct { 87 client.Client 88 Scheme *runtime.Scheme 89 Recorder record.EventRecorder 90 Generators map[string]generators.Generator 91 ArgoDB db.ArgoDB 92 KubeClientset kubernetes.Interface 93 Policy argov1alpha1.ApplicationsSyncPolicy 94 EnablePolicyOverride bool 95 utils.Renderer 96 ArgoCDNamespace string 97 ApplicationSetNamespaces []string 98 EnableProgressiveSyncs bool 99 SCMRootCAPath string 100 GlobalPreservedAnnotations []string 101 GlobalPreservedLabels []string 102 Metrics *metrics.ApplicationsetMetrics 103 MaxResourcesStatusCount int 104 } 105 106 // +kubebuilder:rbac:groups=argoproj.io,resources=applicationsets,verbs=get;list;watch;create;update;patch;delete 107 // +kubebuilder:rbac:groups=argoproj.io,resources=applicationsets/status,verbs=get;update;patch 108 109 func (r *ApplicationSetReconciler) Reconcile(ctx context.Context, req ctrl.Request) (result ctrl.Result, err error) { 110 startReconcile := time.Now() 111 logCtx := log.WithField("applicationset", req.NamespacedName) 112 113 defer func() { 114 if rec := recover(); rec != nil { 115 logCtx.Errorf("Recovered from panic: %+v\n%s", rec, debug.Stack()) 116 result = ctrl.Result{} 117 var ok bool 118 err, ok = rec.(error) 119 if !ok { 120 err = fmt.Errorf("%v", r) 121 } 122 } 123 }() 124 125 var applicationSetInfo argov1alpha1.ApplicationSet 126 parametersGenerated := false 127 startTime := time.Now() 128 if err := r.Get(ctx, req.NamespacedName, &applicationSetInfo); err != nil { 129 if client.IgnoreNotFound(err) != nil { 130 logCtx.WithError(err).Infof("unable to get ApplicationSet: '%v' ", err) 131 } 132 return ctrl.Result{}, client.IgnoreNotFound(err) 133 } 134 135 defer func() { 136 r.Metrics.ObserveReconcile(&applicationSetInfo, time.Since(startTime)) 137 }() 138 139 // Do not attempt to further reconcile the ApplicationSet if it is being deleted. 140 if applicationSetInfo.DeletionTimestamp != nil { 141 appsetName := applicationSetInfo.Name 142 logCtx.Debugf("DeletionTimestamp is set on %s", appsetName) 143 deleteAllowed := utils.DefaultPolicy(applicationSetInfo.Spec.SyncPolicy, r.Policy, r.EnablePolicyOverride).AllowDelete() 144 if !deleteAllowed { 145 logCtx.Debugf("ApplicationSet policy does not allow to delete") 146 if err := r.removeOwnerReferencesOnDeleteAppSet(ctx, applicationSetInfo); err != nil { 147 return ctrl.Result{}, err 148 } 149 logCtx.Debugf("ownerReferences referring %s is deleted from generated applications", appsetName) 150 } 151 if isProgressiveSyncDeletionOrderReversed(&applicationSetInfo) { 152 logCtx.Debugf("DeletionOrder is set as Reverse on %s", appsetName) 153 currentApplications, err := r.getCurrentApplications(ctx, applicationSetInfo) 154 if err != nil { 155 return ctrl.Result{}, err 156 } 157 requeueTime, err := r.performReverseDeletion(ctx, logCtx, applicationSetInfo, currentApplications) 158 if err != nil { 159 return ctrl.Result{}, err 160 } else if requeueTime > 0 { 161 return ctrl.Result{RequeueAfter: requeueTime}, err 162 } 163 } 164 controllerutil.RemoveFinalizer(&applicationSetInfo, argov1alpha1.ResourcesFinalizerName) 165 if err := r.Update(ctx, &applicationSetInfo); err != nil { 166 return ctrl.Result{}, err 167 } 168 return ctrl.Result{}, nil 169 } 170 171 if err := r.migrateStatus(ctx, &applicationSetInfo); err != nil { 172 logCtx.Errorf("failed to migrate status subresource %v", err) 173 return ctrl.Result{}, err 174 } 175 176 // Log a warning if there are unrecognized generators 177 _ = utils.CheckInvalidGenerators(&applicationSetInfo) 178 // desiredApplications is the main list of all expected Applications from all generators in this appset. 179 generatedApplications, applicationSetReason, err := template.GenerateApplications(logCtx, applicationSetInfo, r.Generators, r.Renderer, r.Client) 180 if err != nil { 181 logCtx.Errorf("unable to generate applications: %v", err) 182 _ = r.setApplicationSetStatusCondition(ctx, 183 &applicationSetInfo, 184 argov1alpha1.ApplicationSetCondition{ 185 Type: argov1alpha1.ApplicationSetConditionErrorOccurred, 186 Message: err.Error(), 187 Reason: string(applicationSetReason), 188 Status: argov1alpha1.ApplicationSetConditionStatusTrue, 189 }, parametersGenerated, 190 ) 191 // In order for the controller SDK to respect RequeueAfter, the error must be nil 192 return ctrl.Result{RequeueAfter: ReconcileRequeueOnValidationError}, nil 193 } 194 195 parametersGenerated = true 196 197 validateErrors, err := r.validateGeneratedApplications(ctx, generatedApplications, applicationSetInfo) 198 if err != nil { 199 // While some generators may return an error that requires user intervention, 200 // other generators reference external resources that may change to cause 201 // the error to no longer occur. We thus log the error and requeue 202 // with a timeout to give this another shot at a later time. 203 // 204 // Changes to watched resources will cause this to be reconciled sooner than 205 // the RequeueAfter time. 206 logCtx.Errorf("error occurred during application validation: %s", err.Error()) 207 208 _ = r.setApplicationSetStatusCondition(ctx, 209 &applicationSetInfo, 210 argov1alpha1.ApplicationSetCondition{ 211 Type: argov1alpha1.ApplicationSetConditionErrorOccurred, 212 Message: err.Error(), 213 Reason: argov1alpha1.ApplicationSetReasonApplicationValidationError, 214 Status: argov1alpha1.ApplicationSetConditionStatusTrue, 215 }, parametersGenerated, 216 ) 217 return ctrl.Result{RequeueAfter: ReconcileRequeueOnValidationError}, nil 218 } 219 220 currentApplications, err := r.getCurrentApplications(ctx, applicationSetInfo) 221 if err != nil { 222 return ctrl.Result{}, fmt.Errorf("failed to get current applications for application set: %w", err) 223 } 224 225 err = r.updateResourcesStatus(ctx, logCtx, &applicationSetInfo, currentApplications) 226 if err != nil { 227 return ctrl.Result{}, fmt.Errorf("failed to get update resources status for application set: %w", err) 228 } 229 230 // appMap is a name->app collection of Applications in this ApplicationSet. 231 appMap := map[string]argov1alpha1.Application{} 232 // appSyncMap tracks which apps will be synced during this reconciliation. 233 appSyncMap := map[string]bool{} 234 235 if r.EnableProgressiveSyncs { 236 if !isRollingSyncStrategy(&applicationSetInfo) && len(applicationSetInfo.Status.ApplicationStatus) > 0 { 237 // If an appset was previously syncing with a `RollingSync` strategy but it has switched to the default strategy, clean up the progressive sync application statuses 238 logCtx.Infof("Removing %v unnecessary AppStatus entries from ApplicationSet %v", len(applicationSetInfo.Status.ApplicationStatus), applicationSetInfo.Name) 239 240 err := r.setAppSetApplicationStatus(ctx, logCtx, &applicationSetInfo, []argov1alpha1.ApplicationSetApplicationStatus{}) 241 if err != nil { 242 return ctrl.Result{}, fmt.Errorf("failed to clear previous AppSet application statuses for %v: %w", applicationSetInfo.Name, err) 243 } 244 } else if isRollingSyncStrategy(&applicationSetInfo) { 245 // The appset uses progressive sync with `RollingSync` strategy 246 for _, app := range currentApplications { 247 appMap[app.Name] = app 248 } 249 250 appSyncMap, err = r.performProgressiveSyncs(ctx, logCtx, applicationSetInfo, currentApplications, generatedApplications, appMap) 251 if err != nil { 252 return ctrl.Result{}, fmt.Errorf("failed to perform progressive sync reconciliation for application set: %w", err) 253 } 254 } 255 } else { 256 // Progressive Sync is disabled, clear any existing applicationStatus to prevent stale data 257 if len(applicationSetInfo.Status.ApplicationStatus) > 0 { 258 logCtx.Infof("Progressive Sync disabled, removing %v AppStatus entries from ApplicationSet %v", len(applicationSetInfo.Status.ApplicationStatus), applicationSetInfo.Name) 259 260 err := r.setAppSetApplicationStatus(ctx, logCtx, &applicationSetInfo, []argov1alpha1.ApplicationSetApplicationStatus{}) 261 if err != nil { 262 return ctrl.Result{}, fmt.Errorf("failed to clear AppSet application statuses when Progressive Sync is disabled for %v: %w", applicationSetInfo.Name, err) 263 } 264 } 265 } 266 267 var validApps []argov1alpha1.Application 268 for i := range generatedApplications { 269 if validateErrors[generatedApplications[i].QualifiedName()] == nil { 270 validApps = append(validApps, generatedApplications[i]) 271 } 272 } 273 274 if len(validateErrors) > 0 { 275 errorApps := make([]string, 0, len(validateErrors)) 276 for key := range validateErrors { 277 errorApps = append(errorApps, key) 278 } 279 sort.Strings(errorApps) 280 281 var message string 282 for _, appName := range errorApps { 283 message = validateErrors[appName].Error() 284 logCtx.WithField("application", appName).Errorf("validation error found during application validation: %s", message) 285 } 286 if len(validateErrors) > 1 { 287 // Only the last message gets added to the appset status, to keep the size reasonable. 288 message = fmt.Sprintf("%s (and %d more)", message, len(validateErrors)-1) 289 } 290 _ = r.setApplicationSetStatusCondition(ctx, 291 &applicationSetInfo, 292 argov1alpha1.ApplicationSetCondition{ 293 Type: argov1alpha1.ApplicationSetConditionErrorOccurred, 294 Message: message, 295 Reason: argov1alpha1.ApplicationSetReasonApplicationValidationError, 296 Status: argov1alpha1.ApplicationSetConditionStatusTrue, 297 }, parametersGenerated, 298 ) 299 } 300 301 if r.EnableProgressiveSyncs { 302 // trigger appropriate application syncs if RollingSync strategy is enabled 303 if progressiveSyncsRollingSyncStrategyEnabled(&applicationSetInfo) { 304 validApps = r.syncValidApplications(logCtx, &applicationSetInfo, appSyncMap, appMap, validApps) 305 } 306 } 307 308 if utils.DefaultPolicy(applicationSetInfo.Spec.SyncPolicy, r.Policy, r.EnablePolicyOverride).AllowUpdate() { 309 err = r.createOrUpdateInCluster(ctx, logCtx, applicationSetInfo, validApps) 310 if err != nil { 311 _ = r.setApplicationSetStatusCondition(ctx, 312 &applicationSetInfo, 313 argov1alpha1.ApplicationSetCondition{ 314 Type: argov1alpha1.ApplicationSetConditionErrorOccurred, 315 Message: err.Error(), 316 Reason: argov1alpha1.ApplicationSetReasonUpdateApplicationError, 317 Status: argov1alpha1.ApplicationSetConditionStatusTrue, 318 }, parametersGenerated, 319 ) 320 return ctrl.Result{}, err 321 } 322 } else { 323 err = r.createInCluster(ctx, logCtx, applicationSetInfo, validApps) 324 if err != nil { 325 _ = r.setApplicationSetStatusCondition(ctx, 326 &applicationSetInfo, 327 argov1alpha1.ApplicationSetCondition{ 328 Type: argov1alpha1.ApplicationSetConditionErrorOccurred, 329 Message: err.Error(), 330 Reason: argov1alpha1.ApplicationSetReasonCreateApplicationError, 331 Status: argov1alpha1.ApplicationSetConditionStatusTrue, 332 }, parametersGenerated, 333 ) 334 return ctrl.Result{}, err 335 } 336 } 337 338 if utils.DefaultPolicy(applicationSetInfo.Spec.SyncPolicy, r.Policy, r.EnablePolicyOverride).AllowDelete() { 339 err = r.deleteInCluster(ctx, logCtx, applicationSetInfo, generatedApplications) 340 if err != nil { 341 _ = r.setApplicationSetStatusCondition(ctx, 342 &applicationSetInfo, 343 argov1alpha1.ApplicationSetCondition{ 344 Type: argov1alpha1.ApplicationSetConditionErrorOccurred, 345 Message: err.Error(), 346 Reason: argov1alpha1.ApplicationSetReasonDeleteApplicationError, 347 Status: argov1alpha1.ApplicationSetConditionStatusTrue, 348 }, parametersGenerated, 349 ) 350 return ctrl.Result{}, err 351 } 352 } 353 354 if applicationSetInfo.RefreshRequired() { 355 delete(applicationSetInfo.Annotations, common.AnnotationApplicationSetRefresh) 356 err := r.Update(ctx, &applicationSetInfo) 357 if err != nil { 358 logCtx.Warnf("error occurred while updating ApplicationSet: %v", err) 359 _ = r.setApplicationSetStatusCondition(ctx, 360 &applicationSetInfo, 361 argov1alpha1.ApplicationSetCondition{ 362 Type: argov1alpha1.ApplicationSetConditionErrorOccurred, 363 Message: err.Error(), 364 Reason: argov1alpha1.ApplicationSetReasonRefreshApplicationError, 365 Status: argov1alpha1.ApplicationSetConditionStatusTrue, 366 }, parametersGenerated, 367 ) 368 return ctrl.Result{}, err 369 } 370 } 371 372 requeueAfter := r.getMinRequeueAfter(&applicationSetInfo) 373 374 if len(validateErrors) == 0 { 375 if err := r.setApplicationSetStatusCondition(ctx, 376 &applicationSetInfo, 377 argov1alpha1.ApplicationSetCondition{ 378 Type: argov1alpha1.ApplicationSetConditionResourcesUpToDate, 379 Message: "All applications have been generated successfully", 380 Reason: argov1alpha1.ApplicationSetReasonApplicationSetUpToDate, 381 Status: argov1alpha1.ApplicationSetConditionStatusTrue, 382 }, parametersGenerated, 383 ); err != nil { 384 return ctrl.Result{}, err 385 } 386 } else if requeueAfter == time.Duration(0) { 387 // Ensure that the request is requeued if there are validation errors. 388 requeueAfter = ReconcileRequeueOnValidationError 389 } 390 391 logCtx.WithField("requeueAfter", requeueAfter).Info("end reconcile in ", time.Since(startReconcile)) 392 393 return ctrl.Result{ 394 RequeueAfter: requeueAfter, 395 }, nil 396 } 397 398 func (r *ApplicationSetReconciler) performReverseDeletion(ctx context.Context, logCtx *log.Entry, appset argov1alpha1.ApplicationSet, currentApps []argov1alpha1.Application) (time.Duration, error) { 399 requeueTime := 10 * time.Second 400 stepLength := len(appset.Spec.Strategy.RollingSync.Steps) 401 402 // map applications by name using current applications 403 appMap := make(map[string]*argov1alpha1.Application) 404 for _, app := range currentApps { 405 appMap[app.Name] = &app 406 } 407 408 // Get Rolling Sync Step Maps 409 _, appStepMap := r.buildAppDependencyList(logCtx, appset, currentApps) 410 // reverse the AppStepMap to perform deletion 411 var reverseDeleteAppSteps []deleteInOrder 412 for appName, appStep := range appStepMap { 413 reverseDeleteAppSteps = append(reverseDeleteAppSteps, deleteInOrder{appName, stepLength - appStep - 1}) 414 } 415 416 sort.Slice(reverseDeleteAppSteps, func(i, j int) bool { 417 return reverseDeleteAppSteps[i].Step < reverseDeleteAppSteps[j].Step 418 }) 419 420 for _, step := range reverseDeleteAppSteps { 421 logCtx.Infof("step %v : app %v", step.Step, step.AppName) 422 app := appMap[step.AppName] 423 retrievedApp := argov1alpha1.Application{} 424 if err := r.Get(ctx, types.NamespacedName{Name: app.Name, Namespace: app.Namespace}, &retrievedApp); err != nil { 425 if apierrors.IsNotFound(err) { 426 logCtx.Infof("application %s successfully deleted", step.AppName) 427 continue 428 } 429 } 430 // Check if the application is already being deleted 431 if retrievedApp.DeletionTimestamp != nil { 432 logCtx.Infof("application %s has been marked for deletion, but object not removed yet", step.AppName) 433 if time.Since(retrievedApp.DeletionTimestamp.Time) > 2*time.Minute { 434 return 0, errors.New("application has not been deleted in over 2 minutes") 435 } 436 } 437 // The application has not been deleted yet, trigger its deletion 438 if err := r.Delete(ctx, &retrievedApp); err != nil { 439 return 0, err 440 } 441 return requeueTime, nil 442 } 443 logCtx.Infof("completed reverse deletion for ApplicationSet %v", appset.Name) 444 return 0, nil 445 } 446 447 func getParametersGeneratedCondition(parametersGenerated bool, message string) argov1alpha1.ApplicationSetCondition { 448 var parametersGeneratedCondition argov1alpha1.ApplicationSetCondition 449 if parametersGenerated { 450 parametersGeneratedCondition = argov1alpha1.ApplicationSetCondition{ 451 Type: argov1alpha1.ApplicationSetConditionParametersGenerated, 452 Message: "Successfully generated parameters for all Applications", 453 Reason: argov1alpha1.ApplicationSetReasonParametersGenerated, 454 Status: argov1alpha1.ApplicationSetConditionStatusTrue, 455 } 456 } else { 457 parametersGeneratedCondition = argov1alpha1.ApplicationSetCondition{ 458 Type: argov1alpha1.ApplicationSetConditionParametersGenerated, 459 Message: message, 460 Reason: argov1alpha1.ApplicationSetReasonErrorOccurred, 461 Status: argov1alpha1.ApplicationSetConditionStatusFalse, 462 } 463 } 464 return parametersGeneratedCondition 465 } 466 467 func (r *ApplicationSetReconciler) setApplicationSetStatusCondition(ctx context.Context, applicationSet *argov1alpha1.ApplicationSet, condition argov1alpha1.ApplicationSetCondition, parametersGenerated bool) error { 468 // Initialize the default condition types that this method evaluates 469 evaluatedTypes := map[argov1alpha1.ApplicationSetConditionType]bool{ 470 argov1alpha1.ApplicationSetConditionParametersGenerated: true, 471 argov1alpha1.ApplicationSetConditionErrorOccurred: false, 472 argov1alpha1.ApplicationSetConditionResourcesUpToDate: false, 473 argov1alpha1.ApplicationSetConditionRolloutProgressing: false, 474 } 475 // Evaluate current condition 476 evaluatedTypes[condition.Type] = true 477 newConditions := []argov1alpha1.ApplicationSetCondition{condition} 478 479 if !isRollingSyncStrategy(applicationSet) { 480 // Progressing sync is always evaluated so conditions are removed when it is not enabled 481 evaluatedTypes[argov1alpha1.ApplicationSetConditionRolloutProgressing] = true 482 } 483 484 // Evaluate ParametersGenerated since it is always provided 485 if condition.Type != argov1alpha1.ApplicationSetConditionParametersGenerated { 486 newConditions = append(newConditions, getParametersGeneratedCondition(parametersGenerated, condition.Message)) 487 } 488 489 // Evaluate dependencies between conditions. 490 switch condition.Type { 491 case argov1alpha1.ApplicationSetConditionResourcesUpToDate: 492 if condition.Status == argov1alpha1.ApplicationSetConditionStatusTrue { 493 // If the resources are up to date, we know there was no errors 494 evaluatedTypes[argov1alpha1.ApplicationSetConditionErrorOccurred] = true 495 newConditions = append(newConditions, argov1alpha1.ApplicationSetCondition{ 496 Type: argov1alpha1.ApplicationSetConditionErrorOccurred, 497 Status: argov1alpha1.ApplicationSetConditionStatusFalse, 498 Reason: condition.Reason, 499 Message: condition.Message, 500 }) 501 } 502 case argov1alpha1.ApplicationSetConditionErrorOccurred: 503 if condition.Status == argov1alpha1.ApplicationSetConditionStatusTrue { 504 // If there is an error anywhere in the reconciliation, we cannot consider the resources up to date 505 evaluatedTypes[argov1alpha1.ApplicationSetConditionResourcesUpToDate] = true 506 newConditions = append(newConditions, argov1alpha1.ApplicationSetCondition{ 507 Type: argov1alpha1.ApplicationSetConditionResourcesUpToDate, 508 Status: argov1alpha1.ApplicationSetConditionStatusFalse, 509 Reason: argov1alpha1.ApplicationSetReasonErrorOccurred, 510 Message: condition.Message, 511 }) 512 } 513 case argov1alpha1.ApplicationSetConditionRolloutProgressing: 514 if !isRollingSyncStrategy(applicationSet) { 515 // if the condition is a rolling sync and it is disabled, ignore it 516 evaluatedTypes[condition.Type] = false 517 } 518 } 519 520 // Update the applicationSet conditions 521 previousConditions := applicationSet.Status.Conditions 522 applicationSet.Status.SetConditions(newConditions, evaluatedTypes) 523 524 // Try to not call get/update if nothing has changed 525 needToUpdateConditions := len(applicationSet.Status.Conditions) != len(previousConditions) 526 if !needToUpdateConditions { 527 for i, c := range applicationSet.Status.Conditions { 528 previous := previousConditions[i] 529 if c.Type != previous.Type || c.Reason != previous.Reason || c.Status != previous.Status || c.Message != previous.Message { 530 needToUpdateConditions = true 531 break 532 } 533 } 534 } 535 536 if !needToUpdateConditions { 537 return nil 538 } 539 // DefaultRetry will retry 5 times with a backoff factor of 1, jitter of 0.1 and a duration of 10ms 540 err := retry.RetryOnConflict(retry.DefaultRetry, func() error { 541 updatedAppset := &argov1alpha1.ApplicationSet{} 542 if err := r.Get(ctx, types.NamespacedName{Namespace: applicationSet.Namespace, Name: applicationSet.Name}, updatedAppset); err != nil { 543 if client.IgnoreNotFound(err) != nil { 544 return nil 545 } 546 return fmt.Errorf("error fetching updated application set: %w", err) 547 } 548 549 updatedAppset.Status.SetConditions(newConditions, evaluatedTypes) 550 551 // Update the newly fetched object with new set of conditions 552 err := r.Client.Status().Update(ctx, updatedAppset) 553 if err != nil { 554 return err 555 } 556 updatedAppset.DeepCopyInto(applicationSet) 557 return nil 558 }) 559 if err != nil && !apierrors.IsNotFound(err) { 560 return fmt.Errorf("unable to set application set condition: %w", err) 561 } 562 563 return nil 564 } 565 566 // validateGeneratedApplications uses the Argo CD validation functions to verify the correctness of the 567 // generated applications. 568 func (r *ApplicationSetReconciler) validateGeneratedApplications(ctx context.Context, desiredApplications []argov1alpha1.Application, applicationSetInfo argov1alpha1.ApplicationSet) (map[string]error, error) { 569 errorsByApp := map[string]error{} 570 namesSet := map[string]bool{} 571 for i := range desiredApplications { 572 app := &desiredApplications[i] 573 if namesSet[app.Name] { 574 errorsByApp[app.QualifiedName()] = fmt.Errorf("ApplicationSet %s contains applications with duplicate name: %s", applicationSetInfo.Name, app.Name) 575 continue 576 } 577 namesSet[app.Name] = true 578 appProject := &argov1alpha1.AppProject{} 579 err := r.Get(ctx, types.NamespacedName{Name: app.Spec.Project, Namespace: r.ArgoCDNamespace}, appProject) 580 if err != nil { 581 if apierrors.IsNotFound(err) { 582 errorsByApp[app.QualifiedName()] = fmt.Errorf("application references project %s which does not exist", app.Spec.Project) 583 continue 584 } 585 return nil, err 586 } 587 588 if _, err = argoutil.GetDestinationCluster(ctx, app.Spec.Destination, r.ArgoDB); err != nil { 589 errorsByApp[app.QualifiedName()] = fmt.Errorf("application destination spec is invalid: %s", err.Error()) 590 continue 591 } 592 } 593 594 return errorsByApp, nil 595 } 596 597 func (r *ApplicationSetReconciler) getMinRequeueAfter(applicationSetInfo *argov1alpha1.ApplicationSet) time.Duration { 598 var res time.Duration 599 for _, requestedGenerator := range applicationSetInfo.Spec.Generators { 600 relevantGenerators := generators.GetRelevantGenerators(&requestedGenerator, r.Generators) 601 602 for _, g := range relevantGenerators { 603 t := g.GetRequeueAfter(&requestedGenerator) 604 605 if res == 0 { 606 res = t 607 } else if t != 0 && t < res { 608 res = t 609 } 610 } 611 } 612 613 return res 614 } 615 616 func ignoreNotAllowedNamespaces(namespaces []string) predicate.Predicate { 617 return predicate.NewPredicateFuncs(func(object client.Object) bool { 618 return utils.IsNamespaceAllowed(namespaces, object.GetNamespace()) 619 }) 620 } 621 622 func appControllerIndexer(rawObj client.Object) []string { 623 // grab the job object, extract the owner... 624 app := rawObj.(*argov1alpha1.Application) 625 owner := metav1.GetControllerOf(app) 626 if owner == nil { 627 return nil 628 } 629 // ...make sure it's a application set... 630 if owner.APIVersion != argov1alpha1.SchemeGroupVersion.String() || owner.Kind != "ApplicationSet" { 631 return nil 632 } 633 634 // ...and if so, return it 635 return []string{owner.Name} 636 } 637 638 func (r *ApplicationSetReconciler) SetupWithManager(mgr ctrl.Manager, enableProgressiveSyncs bool, maxConcurrentReconciliations int) error { 639 if err := mgr.GetFieldIndexer().IndexField(context.TODO(), &argov1alpha1.Application{}, ".metadata.controller", appControllerIndexer); err != nil { 640 return fmt.Errorf("error setting up with manager: %w", err) 641 } 642 643 appOwnsHandler := getApplicationOwnsHandler(enableProgressiveSyncs) 644 appSetOwnsHandler := getApplicationSetOwnsHandler(enableProgressiveSyncs) 645 646 return ctrl.NewControllerManagedBy(mgr).WithOptions(controller.Options{ 647 MaxConcurrentReconciles: maxConcurrentReconciliations, 648 }).For(&argov1alpha1.ApplicationSet{}, builder.WithPredicates(appSetOwnsHandler)). 649 Owns(&argov1alpha1.Application{}, builder.WithPredicates(appOwnsHandler)). 650 WithEventFilter(ignoreNotAllowedNamespaces(r.ApplicationSetNamespaces)). 651 Watches( 652 &corev1.Secret{}, 653 &clusterSecretEventHandler{ 654 Client: mgr.GetClient(), 655 Log: log.WithField("type", "createSecretEventHandler"), 656 }). 657 Complete(r) 658 } 659 660 // createOrUpdateInCluster will create / update application resources in the cluster. 661 // - For new applications, it will call create 662 // - For existing application, it will call update 663 // The function also adds owner reference to all applications, and uses it to delete them. 664 func (r *ApplicationSetReconciler) createOrUpdateInCluster(ctx context.Context, logCtx *log.Entry, applicationSet argov1alpha1.ApplicationSet, desiredApplications []argov1alpha1.Application) error { 665 var firstError error 666 // Creates or updates the application in appList 667 for _, generatedApp := range desiredApplications { 668 appLog := logCtx.WithFields(applog.GetAppLogFields(&generatedApp)) 669 670 // Normalize to avoid fighting with the application controller. 671 generatedApp.Spec = *argoutil.NormalizeApplicationSpec(&generatedApp.Spec) 672 673 found := &argov1alpha1.Application{ 674 ObjectMeta: metav1.ObjectMeta{ 675 Name: generatedApp.Name, 676 Namespace: generatedApp.Namespace, 677 }, 678 TypeMeta: metav1.TypeMeta{ 679 Kind: application.ApplicationKind, 680 APIVersion: "argoproj.io/v1alpha1", 681 }, 682 } 683 684 action, err := utils.CreateOrUpdate(ctx, appLog, r.Client, applicationSet.Spec.IgnoreApplicationDifferences, normalizers.IgnoreNormalizerOpts{}, found, func() error { 685 // Copy only the Application/ObjectMeta fields that are significant, from the generatedApp 686 found.Spec = generatedApp.Spec 687 688 // allow setting the Operation field to trigger a sync operation on an Application 689 if generatedApp.Operation != nil { 690 found.Operation = generatedApp.Operation 691 } 692 693 preservedAnnotations := make([]string, 0) 694 preservedLabels := make([]string, 0) 695 696 if applicationSet.Spec.PreservedFields != nil { 697 preservedAnnotations = append(preservedAnnotations, applicationSet.Spec.PreservedFields.Annotations...) 698 preservedLabels = append(preservedLabels, applicationSet.Spec.PreservedFields.Labels...) 699 } 700 701 if len(r.GlobalPreservedAnnotations) > 0 { 702 preservedAnnotations = append(preservedAnnotations, r.GlobalPreservedAnnotations...) 703 } 704 705 if len(r.GlobalPreservedLabels) > 0 { 706 preservedLabels = append(preservedLabels, r.GlobalPreservedLabels...) 707 } 708 709 // Preserve specially treated argo cd annotations: 710 // * https://github.com/argoproj/applicationset/issues/180 711 // * https://github.com/argoproj/argo-cd/issues/10500 712 preservedAnnotations = append(preservedAnnotations, defaultPreservedAnnotations...) 713 714 for _, key := range preservedAnnotations { 715 if state, exists := found.Annotations[key]; exists { 716 if generatedApp.Annotations == nil { 717 generatedApp.Annotations = map[string]string{} 718 } 719 generatedApp.Annotations[key] = state 720 } 721 } 722 723 for _, key := range preservedLabels { 724 if state, exists := found.Labels[key]; exists { 725 if generatedApp.Labels == nil { 726 generatedApp.Labels = map[string]string{} 727 } 728 generatedApp.Labels[key] = state 729 } 730 } 731 732 // Preserve post-delete finalizers: 733 // https://github.com/argoproj/argo-cd/issues/17181 734 for _, finalizer := range found.Finalizers { 735 if strings.HasPrefix(finalizer, argov1alpha1.PostDeleteFinalizerName) { 736 if generatedApp.Finalizers == nil { 737 generatedApp.Finalizers = []string{} 738 } 739 generatedApp.Finalizers = append(generatedApp.Finalizers, finalizer) 740 } 741 } 742 743 found.Annotations = generatedApp.Annotations 744 745 found.Finalizers = generatedApp.Finalizers 746 found.Labels = generatedApp.Labels 747 748 return controllerutil.SetControllerReference(&applicationSet, found, r.Scheme) 749 }) 750 if err != nil { 751 appLog.WithError(err).WithField("action", action).Errorf("failed to %s Application", action) 752 if firstError == nil { 753 firstError = err 754 } 755 continue 756 } 757 758 if action != controllerutil.OperationResultNone { 759 // Don't pollute etcd with "unchanged Application" events 760 r.Recorder.Eventf(&applicationSet, corev1.EventTypeNormal, fmt.Sprint(action), "%s Application %q", action, generatedApp.Name) 761 appLog.Logf(log.InfoLevel, "%s Application", action) 762 } else { 763 // "unchanged Application" can be inferred by Reconcile Complete with no action being listed 764 // Or enable debug logging 765 appLog.Logf(log.DebugLevel, "%s Application", action) 766 } 767 } 768 return firstError 769 } 770 771 // createInCluster will filter from the desiredApplications only the application that needs to be created 772 // Then it will call createOrUpdateInCluster to do the actual create 773 func (r *ApplicationSetReconciler) createInCluster(ctx context.Context, logCtx *log.Entry, applicationSet argov1alpha1.ApplicationSet, desiredApplications []argov1alpha1.Application) error { 774 var createApps []argov1alpha1.Application 775 current, err := r.getCurrentApplications(ctx, applicationSet) 776 if err != nil { 777 return fmt.Errorf("error getting current applications: %w", err) 778 } 779 780 m := make(map[string]bool) // Will holds the app names that are current in the cluster 781 782 for _, app := range current { 783 m[app.Name] = true 784 } 785 786 // filter applications that are not in m[string]bool (new to the cluster) 787 for _, app := range desiredApplications { 788 _, exists := m[app.Name] 789 790 if !exists { 791 createApps = append(createApps, app) 792 } 793 } 794 795 return r.createOrUpdateInCluster(ctx, logCtx, applicationSet, createApps) 796 } 797 798 func (r *ApplicationSetReconciler) getCurrentApplications(ctx context.Context, applicationSet argov1alpha1.ApplicationSet) ([]argov1alpha1.Application, error) { 799 var current argov1alpha1.ApplicationList 800 err := r.List(ctx, ¤t, client.MatchingFields{".metadata.controller": applicationSet.Name}, client.InNamespace(applicationSet.Namespace)) 801 if err != nil { 802 return nil, fmt.Errorf("error retrieving applications: %w", err) 803 } 804 805 return current.Items, nil 806 } 807 808 // deleteInCluster will delete Applications that are currently on the cluster, but not in appList. 809 // The function must be called after all generators had been called and generated applications 810 func (r *ApplicationSetReconciler) deleteInCluster(ctx context.Context, logCtx *log.Entry, applicationSet argov1alpha1.ApplicationSet, desiredApplications []argov1alpha1.Application) error { 811 clusterList, err := utils.ListClusters(ctx, r.KubeClientset, r.ArgoCDNamespace) 812 if err != nil { 813 return fmt.Errorf("error listing clusters: %w", err) 814 } 815 816 // Save current applications to be able to delete the ones that are not in appList 817 current, err := r.getCurrentApplications(ctx, applicationSet) 818 if err != nil { 819 return fmt.Errorf("error getting current applications: %w", err) 820 } 821 822 m := make(map[string]bool) // will hold the app names in appList for the deletion process 823 824 for _, app := range desiredApplications { 825 m[app.Name] = true 826 } 827 828 // Delete apps that are not in m[string]bool 829 var firstError error 830 for _, app := range current { 831 logCtx = logCtx.WithFields(applog.GetAppLogFields(&app)) 832 _, exists := m[app.Name] 833 834 if !exists { 835 // Removes the Argo CD resources finalizer if the application contains an invalid target (eg missing cluster) 836 err := r.removeFinalizerOnInvalidDestination(ctx, applicationSet, &app, clusterList, logCtx) 837 if err != nil { 838 logCtx.WithError(err).Error("failed to update Application") 839 if firstError != nil { 840 firstError = err 841 } 842 continue 843 } 844 845 err = r.Delete(ctx, &app) 846 if err != nil { 847 logCtx.WithError(err).Error("failed to delete Application") 848 if firstError != nil { 849 firstError = err 850 } 851 continue 852 } 853 r.Recorder.Eventf(&applicationSet, corev1.EventTypeNormal, "Deleted", "Deleted Application %q", app.Name) 854 logCtx.Log(log.InfoLevel, "Deleted application") 855 } 856 } 857 return firstError 858 } 859 860 // removeFinalizerOnInvalidDestination removes the Argo CD resources finalizer if the application contains an invalid target (eg missing cluster) 861 func (r *ApplicationSetReconciler) removeFinalizerOnInvalidDestination(ctx context.Context, applicationSet argov1alpha1.ApplicationSet, app *argov1alpha1.Application, clusterList []utils.ClusterSpecifier, appLog *log.Entry) error { 862 // Only check if the finalizers need to be removed IF there are finalizers to remove 863 if len(app.Finalizers) == 0 { 864 return nil 865 } 866 867 var validDestination bool 868 869 // Detect if the destination is invalid (name doesn't correspond to a matching cluster) 870 if destCluster, err := argoutil.GetDestinationCluster(ctx, app.Spec.Destination, r.ArgoDB); err != nil { 871 appLog.Warnf("The destination cluster for %s could not be found: %v", app.Name, err) 872 validDestination = false 873 } else { 874 // Detect if the destination's server field does not match an existing cluster 875 matchingCluster := false 876 for _, cluster := range clusterList { 877 if destCluster.Server != cluster.Server { 878 continue 879 } 880 881 if destCluster.Name != cluster.Name { 882 continue 883 } 884 885 matchingCluster = true 886 break 887 } 888 889 if !matchingCluster { 890 appLog.Warnf("A match for the destination cluster for %s, by server url, could not be found", app.Name) 891 } 892 893 validDestination = matchingCluster 894 } 895 // If the destination is invalid (for example the cluster is no longer defined), then remove 896 // the application finalizers to avoid triggering Argo CD bug #5817 897 if !validDestination { 898 // Filter out the Argo CD finalizer from the finalizer list 899 var newFinalizers []string 900 for _, existingFinalizer := range app.Finalizers { 901 if existingFinalizer != argov1alpha1.ResourcesFinalizerName { // only remove this one 902 newFinalizers = append(newFinalizers, existingFinalizer) 903 } 904 } 905 906 // If the finalizer length changed (due to filtering out an Argo finalizer), update the finalizer list on the app 907 if len(newFinalizers) != len(app.Finalizers) { 908 updated := app.DeepCopy() 909 updated.Finalizers = newFinalizers 910 patch := client.MergeFrom(app) 911 if log.IsLevelEnabled(log.DebugLevel) { 912 utils.LogPatch(appLog, patch, updated) 913 } 914 if err := r.Patch(ctx, updated, patch); err != nil { 915 return fmt.Errorf("error updating finalizers: %w", err) 916 } 917 // Application must have updated list of finalizers 918 updated.DeepCopyInto(app) 919 920 r.Recorder.Eventf(&applicationSet, corev1.EventTypeNormal, "Updated", "Updated Application %q finalizer before deletion, because application has an invalid destination", app.Name) 921 appLog.Log(log.InfoLevel, "Updating application finalizer before deletion, because application has an invalid destination") 922 } 923 } 924 925 return nil 926 } 927 928 func (r *ApplicationSetReconciler) removeOwnerReferencesOnDeleteAppSet(ctx context.Context, applicationSet argov1alpha1.ApplicationSet) error { 929 applications, err := r.getCurrentApplications(ctx, applicationSet) 930 if err != nil { 931 return fmt.Errorf("error getting current applications for ApplicationSet: %w", err) 932 } 933 934 for _, app := range applications { 935 app.SetOwnerReferences([]metav1.OwnerReference{}) 936 err := r.Update(ctx, &app) 937 if err != nil { 938 return fmt.Errorf("error updating application: %w", err) 939 } 940 } 941 942 return nil 943 } 944 945 func (r *ApplicationSetReconciler) performProgressiveSyncs(ctx context.Context, logCtx *log.Entry, appset argov1alpha1.ApplicationSet, applications []argov1alpha1.Application, desiredApplications []argov1alpha1.Application, appMap map[string]argov1alpha1.Application) (map[string]bool, error) { 946 appDependencyList, appStepMap := r.buildAppDependencyList(logCtx, appset, desiredApplications) 947 948 _, err := r.updateApplicationSetApplicationStatus(ctx, logCtx, &appset, applications, appStepMap) 949 if err != nil { 950 return nil, fmt.Errorf("failed to update applicationset app status: %w", err) 951 } 952 953 logCtx.Infof("ApplicationSet %v step list:", appset.Name) 954 for i, step := range appDependencyList { 955 logCtx.Infof("step %v: %+v", i+1, step) 956 } 957 958 appSyncMap := r.buildAppSyncMap(appset, appDependencyList, appMap) 959 logCtx.Infof("Application allowed to sync before maxUpdate?: %+v", appSyncMap) 960 961 _, err = r.updateApplicationSetApplicationStatusProgress(ctx, logCtx, &appset, appSyncMap, appStepMap) 962 if err != nil { 963 return nil, fmt.Errorf("failed to update applicationset application status progress: %w", err) 964 } 965 966 _ = r.updateApplicationSetApplicationStatusConditions(ctx, &appset) 967 968 return appSyncMap, nil 969 } 970 971 // this list tracks which Applications belong to each RollingUpdate step 972 func (r *ApplicationSetReconciler) buildAppDependencyList(logCtx *log.Entry, applicationSet argov1alpha1.ApplicationSet, applications []argov1alpha1.Application) ([][]string, map[string]int) { 973 if applicationSet.Spec.Strategy == nil || applicationSet.Spec.Strategy.Type == "" || applicationSet.Spec.Strategy.Type == "AllAtOnce" { 974 return [][]string{}, map[string]int{} 975 } 976 977 steps := []argov1alpha1.ApplicationSetRolloutStep{} 978 if progressiveSyncsRollingSyncStrategyEnabled(&applicationSet) { 979 steps = applicationSet.Spec.Strategy.RollingSync.Steps 980 } 981 982 appDependencyList := make([][]string, 0) 983 for range steps { 984 appDependencyList = append(appDependencyList, make([]string, 0)) 985 } 986 987 appStepMap := map[string]int{} 988 989 // use applicationLabelSelectors to filter generated Applications into steps and status by name 990 for _, app := range applications { 991 for i, step := range steps { 992 selected := true // default to true, assuming the current Application is a match for the given step matchExpression 993 994 for _, matchExpression := range step.MatchExpressions { 995 if val, ok := app.Labels[matchExpression.Key]; ok { 996 valueMatched := labelMatchedExpression(logCtx, val, matchExpression) 997 998 if !valueMatched { // none of the matchExpression values was a match with the Application's labels 999 selected = false 1000 break 1001 } 1002 } else if matchExpression.Operator == "In" { 1003 selected = false // no matching label key with "In" operator means this Application will not be included in the current step 1004 break 1005 } 1006 } 1007 1008 if selected { 1009 appDependencyList[i] = append(appDependencyList[i], app.Name) 1010 if val, ok := appStepMap[app.Name]; ok { 1011 logCtx.Warnf("AppSet '%v' has a invalid matchExpression that selects Application '%v' label twice, in steps %v and %v", applicationSet.Name, app.Name, val+1, i+1) 1012 } else { 1013 appStepMap[app.Name] = i 1014 } 1015 } 1016 } 1017 } 1018 1019 return appDependencyList, appStepMap 1020 } 1021 1022 func labelMatchedExpression(logCtx *log.Entry, val string, matchExpression argov1alpha1.ApplicationMatchExpression) bool { 1023 if matchExpression.Operator != "In" && matchExpression.Operator != "NotIn" { 1024 logCtx.Errorf("skipping AppSet rollingUpdate step Application selection, invalid matchExpression operator provided: %q ", matchExpression.Operator) 1025 return false 1026 } 1027 1028 // if operator == In, default to false 1029 // if operator == NotIn, default to true 1030 valueMatched := matchExpression.Operator == "NotIn" 1031 1032 for _, value := range matchExpression.Values { 1033 if val == value { 1034 // first "In" match returns true 1035 // first "NotIn" match returns false 1036 return matchExpression.Operator == "In" 1037 } 1038 } 1039 return valueMatched 1040 } 1041 1042 // this map is used to determine which stage of Applications are ready to be updated in the reconciler loop 1043 func (r *ApplicationSetReconciler) buildAppSyncMap(applicationSet argov1alpha1.ApplicationSet, appDependencyList [][]string, appMap map[string]argov1alpha1.Application) map[string]bool { 1044 appSyncMap := map[string]bool{} 1045 syncEnabled := true 1046 1047 // healthy stages and the first non-healthy stage should have sync enabled 1048 // every stage after should have sync disabled 1049 1050 for i := range appDependencyList { 1051 // set the syncEnabled boolean for every Application in the current step 1052 for _, appName := range appDependencyList[i] { 1053 appSyncMap[appName] = syncEnabled 1054 } 1055 1056 // detect if we need to halt before progressing to the next step 1057 for _, appName := range appDependencyList[i] { 1058 idx := findApplicationStatusIndex(applicationSet.Status.ApplicationStatus, appName) 1059 if idx == -1 { 1060 // no Application status found, likely because the Application is being newly created 1061 syncEnabled = false 1062 break 1063 } 1064 1065 appStatus := applicationSet.Status.ApplicationStatus[idx] 1066 app, ok := appMap[appName] 1067 if !ok { 1068 // application name not found in the list of applications managed by this ApplicationSet, maybe because it's being deleted 1069 syncEnabled = false 1070 break 1071 } 1072 syncEnabled = appSyncEnabledForNextStep(&applicationSet, app, appStatus) 1073 if !syncEnabled { 1074 break 1075 } 1076 } 1077 } 1078 1079 return appSyncMap 1080 } 1081 1082 func appSyncEnabledForNextStep(appset *argov1alpha1.ApplicationSet, app argov1alpha1.Application, appStatus argov1alpha1.ApplicationSetApplicationStatus) bool { 1083 if progressiveSyncsRollingSyncStrategyEnabled(appset) { 1084 // we still need to complete the current step if the Application is not yet Healthy or there are still pending Application changes 1085 return isApplicationHealthy(app) && appStatus.Status == "Healthy" 1086 } 1087 1088 return true 1089 } 1090 1091 func isRollingSyncStrategy(appset *argov1alpha1.ApplicationSet) bool { 1092 // It's only RollingSync if the type specifically sets it 1093 return appset.Spec.Strategy != nil && appset.Spec.Strategy.Type == "RollingSync" && appset.Spec.Strategy.RollingSync != nil 1094 } 1095 1096 func progressiveSyncsRollingSyncStrategyEnabled(appset *argov1alpha1.ApplicationSet) bool { 1097 // ProgressiveSync is enabled if the strategy is set to `RollingSync` + steps slice is not empty 1098 return isRollingSyncStrategy(appset) && len(appset.Spec.Strategy.RollingSync.Steps) > 0 1099 } 1100 1101 func isProgressiveSyncDeletionOrderReversed(appset *argov1alpha1.ApplicationSet) bool { 1102 // When progressive sync is enabled + deletionOrder is set to Reverse (case-insensitive) 1103 return progressiveSyncsRollingSyncStrategyEnabled(appset) && strings.EqualFold(appset.Spec.Strategy.DeletionOrder, ReverseDeletionOrder) 1104 } 1105 1106 func isApplicationHealthy(app argov1alpha1.Application) bool { 1107 healthStatusString, syncStatusString, operationPhaseString := statusStrings(app) 1108 1109 if healthStatusString == "Healthy" && syncStatusString != "OutOfSync" && (operationPhaseString == "Succeeded" || operationPhaseString == "") { 1110 return true 1111 } 1112 return false 1113 } 1114 1115 func statusStrings(app argov1alpha1.Application) (string, string, string) { 1116 healthStatusString := string(app.Status.Health.Status) 1117 syncStatusString := string(app.Status.Sync.Status) 1118 operationPhaseString := "" 1119 if app.Status.OperationState != nil { 1120 operationPhaseString = string(app.Status.OperationState.Phase) 1121 } 1122 1123 return healthStatusString, syncStatusString, operationPhaseString 1124 } 1125 1126 func getAppStep(appName string, appStepMap map[string]int) int { 1127 // if an application is not selected by any match expression, it defaults to step -1 1128 step := -1 1129 if appStep, ok := appStepMap[appName]; ok { 1130 // 1-based indexing 1131 step = appStep + 1 1132 } 1133 return step 1134 } 1135 1136 // check the status of each Application's status and promote Applications to the next status if needed 1137 func (r *ApplicationSetReconciler) updateApplicationSetApplicationStatus(ctx context.Context, logCtx *log.Entry, applicationSet *argov1alpha1.ApplicationSet, applications []argov1alpha1.Application, appStepMap map[string]int) ([]argov1alpha1.ApplicationSetApplicationStatus, error) { 1138 now := metav1.Now() 1139 appStatuses := make([]argov1alpha1.ApplicationSetApplicationStatus, 0, len(applications)) 1140 1141 for _, app := range applications { 1142 healthStatusString, syncStatusString, operationPhaseString := statusStrings(app) 1143 1144 idx := findApplicationStatusIndex(applicationSet.Status.ApplicationStatus, app.Name) 1145 1146 currentAppStatus := argov1alpha1.ApplicationSetApplicationStatus{} 1147 1148 if idx == -1 { 1149 // AppStatus not found, set default status of "Waiting" 1150 currentAppStatus = argov1alpha1.ApplicationSetApplicationStatus{ 1151 Application: app.Name, 1152 LastTransitionTime: &now, 1153 Message: "No Application status found, defaulting status to Waiting.", 1154 Status: "Waiting", 1155 Step: strconv.Itoa(getAppStep(app.Name, appStepMap)), 1156 } 1157 } else { 1158 // we have an existing AppStatus 1159 currentAppStatus = applicationSet.Status.ApplicationStatus[idx] 1160 if !reflect.DeepEqual(currentAppStatus.TargetRevisions, app.Status.GetRevisions()) { 1161 currentAppStatus.Message = "Application has pending changes, setting status to Waiting." 1162 } 1163 } 1164 if !reflect.DeepEqual(currentAppStatus.TargetRevisions, app.Status.GetRevisions()) { 1165 currentAppStatus.TargetRevisions = app.Status.GetRevisions() 1166 currentAppStatus.Status = "Waiting" 1167 currentAppStatus.LastTransitionTime = &now 1168 currentAppStatus.Step = strconv.Itoa(getAppStep(currentAppStatus.Application, appStepMap)) 1169 } 1170 1171 appOutdated := false 1172 if progressiveSyncsRollingSyncStrategyEnabled(applicationSet) { 1173 appOutdated = syncStatusString == "OutOfSync" 1174 } 1175 1176 if appOutdated && currentAppStatus.Status != "Waiting" && currentAppStatus.Status != "Pending" { 1177 logCtx.Infof("Application %v is outdated, updating its ApplicationSet status to Waiting", app.Name) 1178 currentAppStatus.LastTransitionTime = &now 1179 currentAppStatus.Status = "Waiting" 1180 currentAppStatus.Message = "Application has pending changes, setting status to Waiting." 1181 currentAppStatus.Step = strconv.Itoa(getAppStep(currentAppStatus.Application, appStepMap)) 1182 } 1183 1184 if currentAppStatus.Status == "Pending" { 1185 if !appOutdated && operationPhaseString == "Succeeded" { 1186 logCtx.Infof("Application %v has completed a sync successfully, updating its ApplicationSet status to Progressing", app.Name) 1187 currentAppStatus.LastTransitionTime = &now 1188 currentAppStatus.Status = "Progressing" 1189 currentAppStatus.Message = "Application resource completed a sync successfully, updating status from Pending to Progressing." 1190 currentAppStatus.Step = strconv.Itoa(getAppStep(currentAppStatus.Application, appStepMap)) 1191 } else if operationPhaseString == "Running" || healthStatusString == "Progressing" { 1192 logCtx.Infof("Application %v has entered Progressing status, updating its ApplicationSet status to Progressing", app.Name) 1193 currentAppStatus.LastTransitionTime = &now 1194 currentAppStatus.Status = "Progressing" 1195 currentAppStatus.Message = "Application resource became Progressing, updating status from Pending to Progressing." 1196 currentAppStatus.Step = strconv.Itoa(getAppStep(currentAppStatus.Application, appStepMap)) 1197 } 1198 } 1199 1200 if currentAppStatus.Status == "Waiting" && isApplicationHealthy(app) { 1201 logCtx.Infof("Application %v is already synced and healthy, updating its ApplicationSet status to Healthy", app.Name) 1202 currentAppStatus.LastTransitionTime = &now 1203 currentAppStatus.Status = healthStatusString 1204 currentAppStatus.Message = "Application resource is already Healthy, updating status from Waiting to Healthy." 1205 currentAppStatus.Step = strconv.Itoa(getAppStep(currentAppStatus.Application, appStepMap)) 1206 } 1207 1208 if currentAppStatus.Status == "Progressing" && isApplicationHealthy(app) { 1209 logCtx.Infof("Application %v has completed Progressing status, updating its ApplicationSet status to Healthy", app.Name) 1210 currentAppStatus.LastTransitionTime = &now 1211 currentAppStatus.Status = healthStatusString 1212 currentAppStatus.Message = "Application resource became Healthy, updating status from Progressing to Healthy." 1213 currentAppStatus.Step = strconv.Itoa(getAppStep(currentAppStatus.Application, appStepMap)) 1214 } 1215 1216 appStatuses = append(appStatuses, currentAppStatus) 1217 } 1218 1219 err := r.setAppSetApplicationStatus(ctx, logCtx, applicationSet, appStatuses) 1220 if err != nil { 1221 return nil, fmt.Errorf("failed to set AppSet application statuses: %w", err) 1222 } 1223 1224 return appStatuses, nil 1225 } 1226 1227 // check Applications that are in Waiting status and promote them to Pending if needed 1228 func (r *ApplicationSetReconciler) updateApplicationSetApplicationStatusProgress(ctx context.Context, logCtx *log.Entry, applicationSet *argov1alpha1.ApplicationSet, appSyncMap map[string]bool, appStepMap map[string]int) ([]argov1alpha1.ApplicationSetApplicationStatus, error) { 1229 now := metav1.Now() 1230 1231 appStatuses := make([]argov1alpha1.ApplicationSetApplicationStatus, 0, len(applicationSet.Status.ApplicationStatus)) 1232 1233 // if we have no RollingUpdate steps, clear out the existing ApplicationStatus entries 1234 if progressiveSyncsRollingSyncStrategyEnabled(applicationSet) { 1235 length := len(applicationSet.Spec.Strategy.RollingSync.Steps) 1236 1237 updateCountMap := make([]int, length) 1238 totalCountMap := make([]int, length) 1239 1240 // populate updateCountMap with counts of existing Pending and Progressing Applications 1241 for _, appStatus := range applicationSet.Status.ApplicationStatus { 1242 totalCountMap[appStepMap[appStatus.Application]]++ 1243 1244 if appStatus.Status == "Pending" || appStatus.Status == "Progressing" { 1245 updateCountMap[appStepMap[appStatus.Application]]++ 1246 } 1247 } 1248 1249 for _, appStatus := range applicationSet.Status.ApplicationStatus { 1250 maxUpdateAllowed := true 1251 maxUpdate := &intstr.IntOrString{} 1252 if progressiveSyncsRollingSyncStrategyEnabled(applicationSet) { 1253 maxUpdate = applicationSet.Spec.Strategy.RollingSync.Steps[appStepMap[appStatus.Application]].MaxUpdate 1254 } 1255 1256 // by default allow all applications to update if maxUpdate is unset 1257 if maxUpdate != nil { 1258 maxUpdateVal, err := intstr.GetScaledValueFromIntOrPercent(maxUpdate, totalCountMap[appStepMap[appStatus.Application]], false) 1259 if err != nil { 1260 logCtx.Warnf("AppSet '%v' has a invalid maxUpdate value '%+v', ignoring maxUpdate logic for this step: %v", applicationSet.Name, maxUpdate, err) 1261 } 1262 1263 // ensure that percentage values greater than 0% always result in at least 1 Application being selected 1264 if maxUpdate.Type == intstr.String && maxUpdate.StrVal != "0%" && maxUpdateVal < 1 { 1265 maxUpdateVal = 1 1266 } 1267 1268 if updateCountMap[appStepMap[appStatus.Application]] >= maxUpdateVal { 1269 maxUpdateAllowed = false 1270 logCtx.Infof("Application %v is not allowed to update yet, %v/%v Applications already updating in step %v in AppSet %v", appStatus.Application, updateCountMap[appStepMap[appStatus.Application]], maxUpdateVal, getAppStep(appStatus.Application, appStepMap), applicationSet.Name) 1271 } 1272 } 1273 1274 if appStatus.Status == "Waiting" && appSyncMap[appStatus.Application] && maxUpdateAllowed { 1275 logCtx.Infof("Application %v moved to Pending status, watching for the Application to start Progressing", appStatus.Application) 1276 appStatus.LastTransitionTime = &now 1277 appStatus.Status = "Pending" 1278 appStatus.Message = "Application moved to Pending status, watching for the Application resource to start Progressing." 1279 appStatus.Step = strconv.Itoa(getAppStep(appStatus.Application, appStepMap)) 1280 1281 updateCountMap[appStepMap[appStatus.Application]]++ 1282 } 1283 1284 appStatuses = append(appStatuses, appStatus) 1285 } 1286 } 1287 1288 err := r.setAppSetApplicationStatus(ctx, logCtx, applicationSet, appStatuses) 1289 if err != nil { 1290 return nil, fmt.Errorf("failed to set AppSet app status: %w", err) 1291 } 1292 1293 return appStatuses, nil 1294 } 1295 1296 func (r *ApplicationSetReconciler) updateApplicationSetApplicationStatusConditions(ctx context.Context, applicationSet *argov1alpha1.ApplicationSet) []argov1alpha1.ApplicationSetCondition { 1297 if !isRollingSyncStrategy(applicationSet) { 1298 return applicationSet.Status.Conditions 1299 } 1300 1301 completedWaves := map[string]bool{} 1302 for _, appStatus := range applicationSet.Status.ApplicationStatus { 1303 if v, ok := completedWaves[appStatus.Step]; !ok { 1304 completedWaves[appStatus.Step] = appStatus.Status == "Healthy" 1305 } else { 1306 completedWaves[appStatus.Step] = v && appStatus.Status == "Healthy" 1307 } 1308 } 1309 1310 isProgressing := false 1311 progressingStep := "" 1312 for i := range applicationSet.Spec.Strategy.RollingSync.Steps { 1313 step := strconv.Itoa(i + 1) 1314 isCompleted, ok := completedWaves[step] 1315 if !ok { 1316 // Step has no applications, so it is completed 1317 continue 1318 } 1319 if !isCompleted { 1320 isProgressing = true 1321 progressingStep = step 1322 break 1323 } 1324 } 1325 1326 if isProgressing { 1327 _ = r.setApplicationSetStatusCondition(ctx, 1328 applicationSet, 1329 argov1alpha1.ApplicationSetCondition{ 1330 Type: argov1alpha1.ApplicationSetConditionRolloutProgressing, 1331 Message: "ApplicationSet is performing rollout of step " + progressingStep, 1332 Reason: argov1alpha1.ApplicationSetReasonApplicationSetModified, 1333 Status: argov1alpha1.ApplicationSetConditionStatusTrue, 1334 }, true, 1335 ) 1336 } else { 1337 _ = r.setApplicationSetStatusCondition(ctx, 1338 applicationSet, 1339 argov1alpha1.ApplicationSetCondition{ 1340 Type: argov1alpha1.ApplicationSetConditionRolloutProgressing, 1341 Message: "ApplicationSet Rollout has completed", 1342 Reason: argov1alpha1.ApplicationSetReasonApplicationSetRolloutComplete, 1343 Status: argov1alpha1.ApplicationSetConditionStatusFalse, 1344 }, true, 1345 ) 1346 } 1347 return applicationSet.Status.Conditions 1348 } 1349 1350 func findApplicationStatusIndex(appStatuses []argov1alpha1.ApplicationSetApplicationStatus, application string) int { 1351 for i := range appStatuses { 1352 if appStatuses[i].Application == application { 1353 return i 1354 } 1355 } 1356 return -1 1357 } 1358 1359 // migrateStatus run migrations on the status subresource of ApplicationSet early during the run of ApplicationSetReconciler.Reconcile 1360 // this handles any defaulting of values - which would otherwise cause the references to r.Client.Status().Update to fail given missing required fields. 1361 func (r *ApplicationSetReconciler) migrateStatus(ctx context.Context, appset *argov1alpha1.ApplicationSet) error { 1362 update := false 1363 if statusList := appset.Status.ApplicationStatus; statusList != nil { 1364 for idx := range statusList { 1365 if statusList[idx].TargetRevisions == nil { 1366 statusList[idx].TargetRevisions = []string{} 1367 update = true 1368 } 1369 } 1370 } 1371 1372 if update { 1373 // DefaultRetry will retry 5 times with a backoff factor of 1, jitter of 0.1 and a duration of 10ms 1374 err := retry.RetryOnConflict(retry.DefaultRetry, func() error { 1375 namespacedName := types.NamespacedName{Namespace: appset.Namespace, Name: appset.Name} 1376 updatedAppset := &argov1alpha1.ApplicationSet{} 1377 if err := r.Get(ctx, namespacedName, updatedAppset); err != nil { 1378 if client.IgnoreNotFound(err) != nil { 1379 return nil 1380 } 1381 return fmt.Errorf("error fetching updated application set: %w", err) 1382 } 1383 1384 updatedAppset.Status.ApplicationStatus = appset.Status.ApplicationStatus 1385 1386 // Update the newly fetched object with new set of ApplicationStatus 1387 err := r.Client.Status().Update(ctx, updatedAppset) 1388 if err != nil { 1389 return err 1390 } 1391 updatedAppset.DeepCopyInto(appset) 1392 return nil 1393 }) 1394 if err != nil && !apierrors.IsNotFound(err) { 1395 return fmt.Errorf("unable to set application set condition: %w", err) 1396 } 1397 } 1398 return nil 1399 } 1400 1401 func (r *ApplicationSetReconciler) updateResourcesStatus(ctx context.Context, logCtx *log.Entry, appset *argov1alpha1.ApplicationSet, apps []argov1alpha1.Application) error { 1402 statusMap := status.GetResourceStatusMap(appset) 1403 statusMap = status.BuildResourceStatus(statusMap, apps) 1404 1405 statuses := []argov1alpha1.ResourceStatus{} 1406 for _, status := range statusMap { 1407 statuses = append(statuses, status) 1408 } 1409 sort.Slice(statuses, func(i, j int) bool { 1410 return statuses[i].Name < statuses[j].Name 1411 }) 1412 resourcesCount := int64(len(statuses)) 1413 if r.MaxResourcesStatusCount > 0 && len(statuses) > r.MaxResourcesStatusCount { 1414 logCtx.Warnf("Truncating ApplicationSet %s resource status from %d to max allowed %d entries", appset.Name, len(statuses), r.MaxResourcesStatusCount) 1415 statuses = statuses[:r.MaxResourcesStatusCount] 1416 } 1417 appset.Status.Resources = statuses 1418 appset.Status.ResourcesCount = resourcesCount 1419 // DefaultRetry will retry 5 times with a backoff factor of 1, jitter of 0.1 and a duration of 10ms 1420 err := retry.RetryOnConflict(retry.DefaultRetry, func() error { 1421 namespacedName := types.NamespacedName{Namespace: appset.Namespace, Name: appset.Name} 1422 updatedAppset := &argov1alpha1.ApplicationSet{} 1423 if err := r.Get(ctx, namespacedName, updatedAppset); err != nil { 1424 if client.IgnoreNotFound(err) != nil { 1425 return nil 1426 } 1427 return fmt.Errorf("error fetching updated application set: %w", err) 1428 } 1429 1430 updatedAppset.Status.Resources = appset.Status.Resources 1431 updatedAppset.Status.ResourcesCount = resourcesCount 1432 1433 // Update the newly fetched object with new status resources 1434 err := r.Client.Status().Update(ctx, updatedAppset) 1435 if err != nil { 1436 return err 1437 } 1438 updatedAppset.DeepCopyInto(appset) 1439 return nil 1440 }) 1441 if err != nil { 1442 logCtx.Errorf("unable to set application set status: %v", err) 1443 return fmt.Errorf("unable to set application set status: %w", err) 1444 } 1445 return nil 1446 } 1447 1448 // setAppSetApplicationStatus updates the ApplicationSet's status field 1449 // with any new/changed Application statuses. 1450 func (r *ApplicationSetReconciler) setAppSetApplicationStatus(ctx context.Context, logCtx *log.Entry, applicationSet *argov1alpha1.ApplicationSet, applicationStatuses []argov1alpha1.ApplicationSetApplicationStatus) error { 1451 needToUpdateStatus := false 1452 1453 if len(applicationStatuses) != len(applicationSet.Status.ApplicationStatus) { 1454 logCtx.WithFields(log.Fields{ 1455 "current_count": len(applicationSet.Status.ApplicationStatus), 1456 "expected_count": len(applicationStatuses), 1457 }).Debug("application status count changed") 1458 needToUpdateStatus = true 1459 } else { 1460 for i := range applicationStatuses { 1461 appStatus := applicationStatuses[i] 1462 idx := findApplicationStatusIndex(applicationSet.Status.ApplicationStatus, appStatus.Application) 1463 if idx == -1 { 1464 logCtx.WithFields(log.Fields{"application": appStatus.Application}).Debug("application not found in current status") 1465 needToUpdateStatus = true 1466 break 1467 } 1468 currentStatus := applicationSet.Status.ApplicationStatus[idx] 1469 statusChanged := currentStatus.Status != appStatus.Status 1470 stepChanged := currentStatus.Step != appStatus.Step 1471 messageChanged := currentStatus.Message != appStatus.Message 1472 1473 if statusChanged || stepChanged || messageChanged { 1474 if statusChanged { 1475 logCtx.WithFields(log.Fields{"application": appStatus.Application, "previous_status": currentStatus.Status, "new_status": appStatus.Status}). 1476 Debug("application status changed") 1477 } 1478 if stepChanged { 1479 logCtx.WithFields(log.Fields{"application": appStatus.Application, "previous_step": currentStatus.Step, "new_step": appStatus.Step}). 1480 Debug("application step changed") 1481 } 1482 if messageChanged { 1483 logCtx.WithFields(log.Fields{"application": appStatus.Application}).Debug("application message changed") 1484 } 1485 needToUpdateStatus = true 1486 break 1487 } 1488 } 1489 } 1490 1491 if needToUpdateStatus { 1492 // sort to make sure the array is always in the same order 1493 applicationSet.Status.ApplicationStatus = make([]argov1alpha1.ApplicationSetApplicationStatus, len(applicationStatuses)) 1494 copy(applicationSet.Status.ApplicationStatus, applicationStatuses) 1495 sort.Slice(applicationSet.Status.ApplicationStatus, func(i, j int) bool { 1496 return applicationSet.Status.ApplicationStatus[i].Application < applicationSet.Status.ApplicationStatus[j].Application 1497 }) 1498 1499 // DefaultRetry will retry 5 times with a backoff factor of 1, jitter of 0.1 and a duration of 10ms 1500 err := retry.RetryOnConflict(retry.DefaultRetry, func() error { 1501 updatedAppset := &argov1alpha1.ApplicationSet{} 1502 if err := r.Get(ctx, types.NamespacedName{Namespace: applicationSet.Namespace, Name: applicationSet.Name}, updatedAppset); err != nil { 1503 if client.IgnoreNotFound(err) != nil { 1504 return nil 1505 } 1506 return fmt.Errorf("error fetching updated application set: %w", err) 1507 } 1508 1509 updatedAppset.Status.ApplicationStatus = applicationSet.Status.ApplicationStatus 1510 1511 // Update the newly fetched object with new set of ApplicationStatus 1512 err := r.Client.Status().Update(ctx, updatedAppset) 1513 if err != nil { 1514 return err 1515 } 1516 updatedAppset.DeepCopyInto(applicationSet) 1517 return nil 1518 }) 1519 if err != nil { 1520 logCtx.Errorf("unable to set application set status: %v", err) 1521 return fmt.Errorf("unable to set application set status: %w", err) 1522 } 1523 } 1524 1525 return nil 1526 } 1527 1528 func (r *ApplicationSetReconciler) syncValidApplications(logCtx *log.Entry, applicationSet *argov1alpha1.ApplicationSet, appSyncMap map[string]bool, appMap map[string]argov1alpha1.Application, validApps []argov1alpha1.Application) []argov1alpha1.Application { 1529 rolloutApps := []argov1alpha1.Application{} 1530 for i := range validApps { 1531 pruneEnabled := false 1532 1533 // ensure that Applications generated with RollingSync do not have an automated sync policy, since the AppSet controller will handle triggering the sync operation instead 1534 if validApps[i].Spec.SyncPolicy != nil && validApps[i].Spec.SyncPolicy.IsAutomatedSyncEnabled() { 1535 pruneEnabled = validApps[i].Spec.SyncPolicy.Automated.Prune 1536 validApps[i].Spec.SyncPolicy.Automated = nil 1537 } 1538 1539 appSetStatusPending := false 1540 idx := findApplicationStatusIndex(applicationSet.Status.ApplicationStatus, validApps[i].Name) 1541 if idx > -1 && applicationSet.Status.ApplicationStatus[idx].Status == "Pending" { 1542 // only trigger a sync for Applications that are in Pending status, since this is governed by maxUpdate 1543 appSetStatusPending = true 1544 } 1545 1546 // check appSyncMap to determine which Applications are ready to be updated and which should be skipped 1547 if appSyncMap[validApps[i].Name] && appMap[validApps[i].Name].Status.Sync.Status == "OutOfSync" && appSetStatusPending { 1548 logCtx.Infof("triggering sync for application: %v, prune enabled: %v", validApps[i].Name, pruneEnabled) 1549 validApps[i] = syncApplication(validApps[i], pruneEnabled) 1550 } 1551 rolloutApps = append(rolloutApps, validApps[i]) 1552 } 1553 return rolloutApps 1554 } 1555 1556 // used by the RollingSync Progressive Sync strategy to trigger a sync of a particular Application resource 1557 func syncApplication(application argov1alpha1.Application, prune bool) argov1alpha1.Application { 1558 operation := argov1alpha1.Operation{ 1559 InitiatedBy: argov1alpha1.OperationInitiator{ 1560 Username: "applicationset-controller", 1561 Automated: true, 1562 }, 1563 Info: []*argov1alpha1.Info{ 1564 { 1565 Name: "Reason", 1566 Value: "ApplicationSet RollingSync triggered a sync of this Application resource", 1567 }, 1568 }, 1569 Sync: &argov1alpha1.SyncOperation{}, 1570 // Set a retry limit of 5, aligning with the default in Argo CD's appcontroller auto-sync behavior. 1571 // This provides consistency for retry behavior across controllers. 1572 // See: https://github.com/argoproj/argo-cd/blob/af9ebac0bb35dc16eb034c1cefaf7c92d1029927/controller/appcontroller.go#L2126 1573 Retry: argov1alpha1.RetryStrategy{Limit: 5}, 1574 } 1575 1576 if application.Spec.SyncPolicy != nil { 1577 if application.Spec.SyncPolicy.Retry != nil { 1578 operation.Retry = *application.Spec.SyncPolicy.Retry 1579 } 1580 if application.Spec.SyncPolicy.SyncOptions != nil { 1581 operation.Sync.SyncOptions = application.Spec.SyncPolicy.SyncOptions 1582 } 1583 operation.Sync.Prune = prune 1584 } 1585 application.Operation = &operation 1586 1587 return application 1588 } 1589 1590 func getApplicationOwnsHandler(enableProgressiveSyncs bool) predicate.Funcs { 1591 return predicate.Funcs{ 1592 CreateFunc: func(e event.CreateEvent) bool { 1593 // if we are the owner and there is a create event, we most likely created it and do not need to 1594 // re-reconcile 1595 if log.IsLevelEnabled(log.DebugLevel) { 1596 logFields := log.Fields{"app": ""} 1597 app, isApp := e.Object.(*argov1alpha1.Application) 1598 if isApp { 1599 logFields = applog.GetAppLogFields(app) 1600 } 1601 log.WithFields(logFields).Debugln("received create event from owning an application") 1602 } 1603 return false 1604 }, 1605 DeleteFunc: func(e event.DeleteEvent) bool { 1606 if log.IsLevelEnabled(log.DebugLevel) { 1607 logFields := log.Fields{"app": ""} 1608 app, isApp := e.Object.(*argov1alpha1.Application) 1609 if isApp { 1610 logFields = applog.GetAppLogFields(app) 1611 } 1612 log.WithFields(logFields).Debugln("received delete event from owning an application") 1613 } 1614 return true 1615 }, 1616 UpdateFunc: func(e event.UpdateEvent) bool { 1617 appOld, isApp := e.ObjectOld.(*argov1alpha1.Application) 1618 if !isApp { 1619 return false 1620 } 1621 logCtx := log.WithFields(applog.GetAppLogFields(appOld)) 1622 logCtx.Debugln("received update event from owning an application") 1623 appNew, isApp := e.ObjectNew.(*argov1alpha1.Application) 1624 if !isApp { 1625 return false 1626 } 1627 requeue := shouldRequeueForApplication(appOld, appNew, enableProgressiveSyncs) 1628 logCtx.WithField("requeue", requeue).Debugf("requeue caused by application %s", appNew.Name) 1629 return requeue 1630 }, 1631 GenericFunc: func(e event.GenericEvent) bool { 1632 if log.IsLevelEnabled(log.DebugLevel) { 1633 logFields := log.Fields{} 1634 app, isApp := e.Object.(*argov1alpha1.Application) 1635 if isApp { 1636 logFields = applog.GetAppLogFields(app) 1637 } 1638 log.WithFields(logFields).Debugln("received generic event from owning an application") 1639 } 1640 return true 1641 }, 1642 } 1643 } 1644 1645 // shouldRequeueForApplication determines when we want to requeue an ApplicationSet for reconciling based on an owned 1646 // application change 1647 // The applicationset controller owns a subset of the Application CR. 1648 // We do not need to re-reconcile if parts of the application change outside the applicationset's control. 1649 // An example being, Application.ApplicationStatus.ReconciledAt which gets updated by the application controller. 1650 // Additionally, Application.ObjectMeta.ResourceVersion and Application.ObjectMeta.Generation which are set by K8s. 1651 func shouldRequeueForApplication(appOld *argov1alpha1.Application, appNew *argov1alpha1.Application, enableProgressiveSyncs bool) bool { 1652 if appOld == nil || appNew == nil { 1653 return false 1654 } 1655 1656 // the applicationset controller owns the application spec, labels, annotations, and finalizers on the applications 1657 // reflect.DeepEqual considers nil slices/maps not equal to empty slices/maps 1658 // https://pkg.go.dev/reflect#DeepEqual 1659 // ApplicationDestination has an unexported field so we can just use the == for comparison 1660 if !cmp.Equal(appOld.Spec, appNew.Spec, cmpopts.EquateEmpty(), cmpopts.EquateComparable(argov1alpha1.ApplicationDestination{})) || 1661 !cmp.Equal(appOld.GetAnnotations(), appNew.GetAnnotations(), cmpopts.EquateEmpty()) || 1662 !cmp.Equal(appOld.GetLabels(), appNew.GetLabels(), cmpopts.EquateEmpty()) || 1663 !cmp.Equal(appOld.GetFinalizers(), appNew.GetFinalizers(), cmpopts.EquateEmpty()) { 1664 return true 1665 } 1666 1667 // progressive syncs use the application status for updates. if they differ, requeue to trigger the next progression 1668 if enableProgressiveSyncs { 1669 if appOld.Status.Health.Status != appNew.Status.Health.Status || appOld.Status.Sync.Status != appNew.Status.Sync.Status { 1670 return true 1671 } 1672 1673 if appOld.Status.OperationState != nil && appNew.Status.OperationState != nil { 1674 if appOld.Status.OperationState.Phase != appNew.Status.OperationState.Phase || 1675 appOld.Status.OperationState.StartedAt != appNew.Status.OperationState.StartedAt { 1676 return true 1677 } 1678 } 1679 } 1680 1681 return false 1682 } 1683 1684 func getApplicationSetOwnsHandler(enableProgressiveSyncs bool) predicate.Funcs { 1685 return predicate.Funcs{ 1686 CreateFunc: func(e event.CreateEvent) bool { 1687 appSet, isApp := e.Object.(*argov1alpha1.ApplicationSet) 1688 if !isApp { 1689 return false 1690 } 1691 log.WithField("applicationset", appSet.QualifiedName()).Debugln("received create event") 1692 // Always queue a new applicationset 1693 return true 1694 }, 1695 DeleteFunc: func(e event.DeleteEvent) bool { 1696 appSet, isApp := e.Object.(*argov1alpha1.ApplicationSet) 1697 if !isApp { 1698 return false 1699 } 1700 log.WithField("applicationset", appSet.QualifiedName()).Debugln("received delete event") 1701 // Always queue for the deletion of an applicationset 1702 return true 1703 }, 1704 UpdateFunc: func(e event.UpdateEvent) bool { 1705 appSetOld, isAppSet := e.ObjectOld.(*argov1alpha1.ApplicationSet) 1706 if !isAppSet { 1707 return false 1708 } 1709 appSetNew, isAppSet := e.ObjectNew.(*argov1alpha1.ApplicationSet) 1710 if !isAppSet { 1711 return false 1712 } 1713 requeue := shouldRequeueForApplicationSet(appSetOld, appSetNew, enableProgressiveSyncs) 1714 log.WithField("applicationset", appSetNew.QualifiedName()). 1715 WithField("requeue", requeue).Debugln("received update event") 1716 return requeue 1717 }, 1718 GenericFunc: func(e event.GenericEvent) bool { 1719 appSet, isApp := e.Object.(*argov1alpha1.ApplicationSet) 1720 if !isApp { 1721 return false 1722 } 1723 log.WithField("applicationset", appSet.QualifiedName()).Debugln("received generic event") 1724 // Always queue for the generic of an applicationset 1725 return true 1726 }, 1727 } 1728 } 1729 1730 // shouldRequeueForApplicationSet determines when we need to requeue an applicationset 1731 func shouldRequeueForApplicationSet(appSetOld, appSetNew *argov1alpha1.ApplicationSet, enableProgressiveSyncs bool) bool { 1732 if appSetOld == nil || appSetNew == nil { 1733 return false 1734 } 1735 1736 // Requeue if any ApplicationStatus.Status changed for Progressive sync strategy 1737 if enableProgressiveSyncs { 1738 if !cmp.Equal(appSetOld.Status.ApplicationStatus, appSetNew.Status.ApplicationStatus, cmpopts.EquateEmpty()) { 1739 return true 1740 } 1741 } 1742 1743 // only compare the applicationset spec, annotations, labels and finalizers, deletionTimestamp, specifically avoiding 1744 // the status field. status is owned by the applicationset controller, 1745 // and we do not need to requeue when it does bookkeeping 1746 // NB: the ApplicationDestination comes from the ApplicationSpec being embedded 1747 // in the ApplicationSetTemplate from the generators 1748 if !cmp.Equal(appSetOld.Spec, appSetNew.Spec, cmpopts.EquateEmpty(), cmpopts.EquateComparable(argov1alpha1.ApplicationDestination{})) || 1749 !cmp.Equal(appSetOld.GetLabels(), appSetNew.GetLabels(), cmpopts.EquateEmpty()) || 1750 !cmp.Equal(appSetOld.GetFinalizers(), appSetNew.GetFinalizers(), cmpopts.EquateEmpty()) || 1751 !cmp.Equal(appSetOld.DeletionTimestamp, appSetNew.DeletionTimestamp, cmpopts.EquateEmpty()) { 1752 return true 1753 } 1754 1755 // Requeue only when the refresh annotation is newly added to the ApplicationSet. 1756 // Changes to other annotations made simultaneously might be missed, but such cases are rare. 1757 if !cmp.Equal(appSetOld.GetAnnotations(), appSetNew.GetAnnotations(), cmpopts.EquateEmpty()) { 1758 _, oldHasRefreshAnnotation := appSetOld.Annotations[common.AnnotationApplicationSetRefresh] 1759 _, newHasRefreshAnnotation := appSetNew.Annotations[common.AnnotationApplicationSetRefresh] 1760 1761 if oldHasRefreshAnnotation && !newHasRefreshAnnotation { 1762 return false 1763 } 1764 return true 1765 } 1766 1767 return false 1768 } 1769 1770 var _ handler.EventHandler = &clusterSecretEventHandler{}