github.com/argoproj/argo-cd/v2@v2.10.9/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 "fmt" 20 "reflect" 21 "time" 22 23 log "github.com/sirupsen/logrus" 24 corev1 "k8s.io/api/core/v1" 25 apierr "k8s.io/apimachinery/pkg/api/errors" 26 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 27 "k8s.io/apimachinery/pkg/runtime" 28 "k8s.io/apimachinery/pkg/types" 29 "k8s.io/apimachinery/pkg/util/intstr" 30 "k8s.io/client-go/kubernetes" 31 k8scache "k8s.io/client-go/tools/cache" 32 "k8s.io/client-go/tools/record" 33 ctrl "sigs.k8s.io/controller-runtime" 34 "sigs.k8s.io/controller-runtime/pkg/builder" 35 "sigs.k8s.io/controller-runtime/pkg/cache" 36 "sigs.k8s.io/controller-runtime/pkg/client" 37 "sigs.k8s.io/controller-runtime/pkg/controller" 38 "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" 39 "sigs.k8s.io/controller-runtime/pkg/event" 40 "sigs.k8s.io/controller-runtime/pkg/handler" 41 "sigs.k8s.io/controller-runtime/pkg/predicate" 42 "sigs.k8s.io/controller-runtime/pkg/source" 43 44 "github.com/argoproj/argo-cd/v2/applicationset/generators" 45 "github.com/argoproj/argo-cd/v2/applicationset/utils" 46 "github.com/argoproj/argo-cd/v2/common" 47 "github.com/argoproj/argo-cd/v2/util/db" 48 "github.com/argoproj/argo-cd/v2/util/glob" 49 50 argov1alpha1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1" 51 appclientset "github.com/argoproj/argo-cd/v2/pkg/client/clientset/versioned" 52 argoutil "github.com/argoproj/argo-cd/v2/util/argo" 53 "github.com/argoproj/argo-cd/v2/util/argo/normalizers" 54 55 "github.com/argoproj/argo-cd/v2/pkg/apis/application" 56 ) 57 58 const ( 59 // Rather than importing the whole argocd-notifications controller, just copying the const here 60 // https://github.com/argoproj-labs/argocd-notifications/blob/33d345fa838829bb50fca5c08523aba380d2c12b/pkg/controller/subscriptions.go#L12 61 // https://github.com/argoproj-labs/argocd-notifications/blob/33d345fa838829bb50fca5c08523aba380d2c12b/pkg/controller/state.go#L17 62 NotifiedAnnotationKey = "notified.notifications.argoproj.io" 63 ReconcileRequeueOnValidationError = time.Minute * 3 64 ) 65 66 var ( 67 defaultPreservedAnnotations = []string{ 68 NotifiedAnnotationKey, 69 argov1alpha1.AnnotationKeyRefresh, 70 } 71 ) 72 73 // ApplicationSetReconciler reconciles a ApplicationSet object 74 type ApplicationSetReconciler struct { 75 client.Client 76 Scheme *runtime.Scheme 77 Recorder record.EventRecorder 78 Generators map[string]generators.Generator 79 ArgoDB db.ArgoDB 80 ArgoAppClientset appclientset.Interface 81 KubeClientset kubernetes.Interface 82 Policy argov1alpha1.ApplicationsSyncPolicy 83 EnablePolicyOverride bool 84 utils.Renderer 85 ArgoCDNamespace string 86 ApplicationSetNamespaces []string 87 EnableProgressiveSyncs bool 88 SCMRootCAPath string 89 GlobalPreservedAnnotations []string 90 GlobalPreservedLabels []string 91 Cache cache.Cache 92 } 93 94 // +kubebuilder:rbac:groups=argoproj.io,resources=applicationsets,verbs=get;list;watch;create;update;patch;delete 95 // +kubebuilder:rbac:groups=argoproj.io,resources=applicationsets/status,verbs=get;update;patch 96 97 func (r *ApplicationSetReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { 98 logCtx := log.WithField("applicationset", req.NamespacedName) 99 100 var applicationSetInfo argov1alpha1.ApplicationSet 101 parametersGenerated := false 102 103 if err := r.Get(ctx, req.NamespacedName, &applicationSetInfo); err != nil { 104 if client.IgnoreNotFound(err) != nil { 105 logCtx.WithError(err).Infof("unable to get ApplicationSet: '%v' ", err) 106 } 107 return ctrl.Result{}, client.IgnoreNotFound(err) 108 } 109 110 // Do not attempt to further reconcile the ApplicationSet if it is being deleted. 111 if applicationSetInfo.ObjectMeta.DeletionTimestamp != nil { 112 deleteAllowed := utils.DefaultPolicy(applicationSetInfo.Spec.SyncPolicy, r.Policy, r.EnablePolicyOverride).AllowDelete() 113 if !deleteAllowed { 114 if err := r.removeOwnerReferencesOnDeleteAppSet(ctx, applicationSetInfo); err != nil { 115 return ctrl.Result{}, err 116 } 117 controllerutil.RemoveFinalizer(&applicationSetInfo, argov1alpha1.ResourcesFinalizerName) 118 if err := r.Update(ctx, &applicationSetInfo); err != nil { 119 return ctrl.Result{}, err 120 } 121 } 122 return ctrl.Result{}, nil 123 } 124 125 // Log a warning if there are unrecognized generators 126 _ = utils.CheckInvalidGenerators(&applicationSetInfo) 127 // desiredApplications is the main list of all expected Applications from all generators in this appset. 128 desiredApplications, applicationSetReason, err := r.generateApplications(logCtx, applicationSetInfo) 129 if err != nil { 130 _ = r.setApplicationSetStatusCondition(ctx, 131 &applicationSetInfo, 132 argov1alpha1.ApplicationSetCondition{ 133 Type: argov1alpha1.ApplicationSetConditionErrorOccurred, 134 Message: err.Error(), 135 Reason: string(applicationSetReason), 136 Status: argov1alpha1.ApplicationSetConditionStatusTrue, 137 }, parametersGenerated, 138 ) 139 return ctrl.Result{}, err 140 } 141 142 parametersGenerated = true 143 144 validateErrors, err := r.validateGeneratedApplications(ctx, desiredApplications, applicationSetInfo) 145 if err != nil { 146 // While some generators may return an error that requires user intervention, 147 // other generators reference external resources that may change to cause 148 // the error to no longer occur. We thus log the error and requeue 149 // with a timeout to give this another shot at a later time. 150 // 151 // Changes to watched resources will cause this to be reconciled sooner than 152 // the RequeueAfter time. 153 logCtx.Errorf("error occurred during application validation: %s", err.Error()) 154 155 _ = r.setApplicationSetStatusCondition(ctx, 156 &applicationSetInfo, 157 argov1alpha1.ApplicationSetCondition{ 158 Type: argov1alpha1.ApplicationSetConditionErrorOccurred, 159 Message: err.Error(), 160 Reason: argov1alpha1.ApplicationSetReasonApplicationValidationError, 161 Status: argov1alpha1.ApplicationSetConditionStatusTrue, 162 }, parametersGenerated, 163 ) 164 return ctrl.Result{RequeueAfter: ReconcileRequeueOnValidationError}, nil 165 } 166 167 // appMap is a name->app collection of Applications in this ApplicationSet. 168 appMap := map[string]argov1alpha1.Application{} 169 // appSyncMap tracks which apps will be synced during this reconciliation. 170 appSyncMap := map[string]bool{} 171 172 if r.EnableProgressiveSyncs { 173 if applicationSetInfo.Spec.Strategy == nil && len(applicationSetInfo.Status.ApplicationStatus) > 0 { 174 // If appset used progressive sync but stopped, clean up the progressive sync application statuses 175 logCtx.Infof("Removing %v unnecessary AppStatus entries from ApplicationSet %v", len(applicationSetInfo.Status.ApplicationStatus), applicationSetInfo.Name) 176 177 err := r.setAppSetApplicationStatus(ctx, logCtx, &applicationSetInfo, []argov1alpha1.ApplicationSetApplicationStatus{}) 178 if err != nil { 179 return ctrl.Result{}, fmt.Errorf("failed to clear previous AppSet application statuses for %v: %w", applicationSetInfo.Name, err) 180 } 181 } else if applicationSetInfo.Spec.Strategy != nil { 182 // appset uses progressive sync 183 applications, err := r.getCurrentApplications(ctx, applicationSetInfo) 184 if err != nil { 185 return ctrl.Result{}, fmt.Errorf("failed to get current applications for application set: %w", err) 186 } 187 188 for _, app := range applications { 189 appMap[app.Name] = app 190 } 191 192 appSyncMap, err = r.performProgressiveSyncs(ctx, logCtx, applicationSetInfo, applications, desiredApplications, appMap) 193 if err != nil { 194 return ctrl.Result{}, fmt.Errorf("failed to perform progressive sync reconciliation for application set: %w", err) 195 } 196 } 197 } 198 199 var validApps []argov1alpha1.Application 200 for i := range desiredApplications { 201 if validateErrors[i] == nil { 202 validApps = append(validApps, desiredApplications[i]) 203 } 204 } 205 206 if len(validateErrors) > 0 { 207 var message string 208 for _, v := range validateErrors { 209 message = v.Error() 210 logCtx.Errorf("validation error found during application validation: %s", message) 211 } 212 if len(validateErrors) > 1 { 213 // Only the last message gets added to the appset status, to keep the size reasonable. 214 message = fmt.Sprintf("%s (and %d more)", message, len(validateErrors)-1) 215 } 216 _ = r.setApplicationSetStatusCondition(ctx, 217 &applicationSetInfo, 218 argov1alpha1.ApplicationSetCondition{ 219 Type: argov1alpha1.ApplicationSetConditionErrorOccurred, 220 Message: message, 221 Reason: argov1alpha1.ApplicationSetReasonApplicationValidationError, 222 Status: argov1alpha1.ApplicationSetConditionStatusTrue, 223 }, parametersGenerated, 224 ) 225 } 226 227 if r.EnableProgressiveSyncs { 228 // trigger appropriate application syncs if RollingSync strategy is enabled 229 if progressiveSyncsStrategyEnabled(&applicationSetInfo, "RollingSync") { 230 validApps, err = r.syncValidApplications(logCtx, &applicationSetInfo, appSyncMap, appMap, validApps) 231 232 if err != nil { 233 _ = r.setApplicationSetStatusCondition(ctx, 234 &applicationSetInfo, 235 argov1alpha1.ApplicationSetCondition{ 236 Type: argov1alpha1.ApplicationSetConditionErrorOccurred, 237 Message: err.Error(), 238 Reason: argov1alpha1.ApplicationSetReasonSyncApplicationError, 239 Status: argov1alpha1.ApplicationSetConditionStatusTrue, 240 }, parametersGenerated, 241 ) 242 return ctrl.Result{}, err 243 } 244 } 245 } 246 247 if utils.DefaultPolicy(applicationSetInfo.Spec.SyncPolicy, r.Policy, r.EnablePolicyOverride).AllowUpdate() { 248 err = r.createOrUpdateInCluster(ctx, logCtx, applicationSetInfo, validApps) 249 if err != nil { 250 _ = r.setApplicationSetStatusCondition(ctx, 251 &applicationSetInfo, 252 argov1alpha1.ApplicationSetCondition{ 253 Type: argov1alpha1.ApplicationSetConditionErrorOccurred, 254 Message: err.Error(), 255 Reason: argov1alpha1.ApplicationSetReasonUpdateApplicationError, 256 Status: argov1alpha1.ApplicationSetConditionStatusTrue, 257 }, parametersGenerated, 258 ) 259 return ctrl.Result{}, err 260 } 261 } else { 262 err = r.createInCluster(ctx, logCtx, applicationSetInfo, validApps) 263 if err != nil { 264 _ = r.setApplicationSetStatusCondition(ctx, 265 &applicationSetInfo, 266 argov1alpha1.ApplicationSetCondition{ 267 Type: argov1alpha1.ApplicationSetConditionErrorOccurred, 268 Message: err.Error(), 269 Reason: argov1alpha1.ApplicationSetReasonCreateApplicationError, 270 Status: argov1alpha1.ApplicationSetConditionStatusTrue, 271 }, parametersGenerated, 272 ) 273 return ctrl.Result{}, err 274 } 275 } 276 277 if utils.DefaultPolicy(applicationSetInfo.Spec.SyncPolicy, r.Policy, r.EnablePolicyOverride).AllowDelete() { 278 err = r.deleteInCluster(ctx, logCtx, applicationSetInfo, desiredApplications) 279 if err != nil { 280 _ = r.setApplicationSetStatusCondition(ctx, 281 &applicationSetInfo, 282 argov1alpha1.ApplicationSetCondition{ 283 Type: argov1alpha1.ApplicationSetConditionResourcesUpToDate, 284 Message: err.Error(), 285 Reason: argov1alpha1.ApplicationSetReasonDeleteApplicationError, 286 Status: argov1alpha1.ApplicationSetConditionStatusTrue, 287 }, parametersGenerated, 288 ) 289 return ctrl.Result{}, err 290 } 291 } 292 293 if applicationSetInfo.RefreshRequired() { 294 delete(applicationSetInfo.Annotations, common.AnnotationApplicationSetRefresh) 295 err := r.Client.Update(ctx, &applicationSetInfo) 296 if err != nil { 297 logCtx.Warnf("error occurred while updating ApplicationSet: %v", err) 298 _ = r.setApplicationSetStatusCondition(ctx, 299 &applicationSetInfo, 300 argov1alpha1.ApplicationSetCondition{ 301 Type: argov1alpha1.ApplicationSetConditionErrorOccurred, 302 Message: err.Error(), 303 Reason: argov1alpha1.ApplicationSetReasonRefreshApplicationError, 304 Status: argov1alpha1.ApplicationSetConditionStatusTrue, 305 }, parametersGenerated, 306 ) 307 return ctrl.Result{}, err 308 } 309 } 310 311 requeueAfter := r.getMinRequeueAfter(&applicationSetInfo) 312 313 if len(validateErrors) == 0 { 314 if err := r.setApplicationSetStatusCondition(ctx, 315 &applicationSetInfo, 316 argov1alpha1.ApplicationSetCondition{ 317 Type: argov1alpha1.ApplicationSetConditionResourcesUpToDate, 318 Message: "All applications have been generated successfully", 319 Reason: argov1alpha1.ApplicationSetReasonApplicationSetUpToDate, 320 Status: argov1alpha1.ApplicationSetConditionStatusTrue, 321 }, parametersGenerated, 322 ); err != nil { 323 return ctrl.Result{}, err 324 } 325 } else if requeueAfter == time.Duration(0) { 326 // Ensure that the request is requeued if there are validation errors. 327 requeueAfter = ReconcileRequeueOnValidationError 328 } 329 330 logCtx.WithField("requeueAfter", requeueAfter).Info("end reconcile") 331 332 return ctrl.Result{ 333 RequeueAfter: requeueAfter, 334 }, nil 335 } 336 337 func getParametersGeneratedCondition(parametersGenerated bool, message string) argov1alpha1.ApplicationSetCondition { 338 var paramtersGeneratedCondition argov1alpha1.ApplicationSetCondition 339 if parametersGenerated { 340 paramtersGeneratedCondition = argov1alpha1.ApplicationSetCondition{ 341 Type: argov1alpha1.ApplicationSetConditionParametersGenerated, 342 Message: "Successfully generated parameters for all Applications", 343 Reason: argov1alpha1.ApplicationSetReasonParametersGenerated, 344 Status: argov1alpha1.ApplicationSetConditionStatusTrue, 345 } 346 } else { 347 paramtersGeneratedCondition = argov1alpha1.ApplicationSetCondition{ 348 Type: argov1alpha1.ApplicationSetConditionParametersGenerated, 349 Message: message, 350 Reason: argov1alpha1.ApplicationSetReasonErrorOccurred, 351 Status: argov1alpha1.ApplicationSetConditionStatusFalse, 352 } 353 } 354 return paramtersGeneratedCondition 355 } 356 357 func getResourceUpToDateCondition(errorOccurred bool, message string, reason string) argov1alpha1.ApplicationSetCondition { 358 var resourceUpToDateCondition argov1alpha1.ApplicationSetCondition 359 if errorOccurred { 360 resourceUpToDateCondition = argov1alpha1.ApplicationSetCondition{ 361 Type: argov1alpha1.ApplicationSetConditionResourcesUpToDate, 362 Message: message, 363 Reason: reason, 364 Status: argov1alpha1.ApplicationSetConditionStatusFalse, 365 } 366 } else { 367 resourceUpToDateCondition = argov1alpha1.ApplicationSetCondition{ 368 Type: argov1alpha1.ApplicationSetConditionResourcesUpToDate, 369 Message: "ApplicationSet up to date", 370 Reason: argov1alpha1.ApplicationSetReasonApplicationSetUpToDate, 371 Status: argov1alpha1.ApplicationSetConditionStatusTrue, 372 } 373 } 374 return resourceUpToDateCondition 375 } 376 377 func (r *ApplicationSetReconciler) setApplicationSetStatusCondition(ctx context.Context, applicationSet *argov1alpha1.ApplicationSet, condition argov1alpha1.ApplicationSetCondition, paramtersGenerated bool) error { 378 // check if error occurred during reconcile process 379 errOccurred := condition.Type == argov1alpha1.ApplicationSetConditionErrorOccurred 380 381 var errOccurredCondition argov1alpha1.ApplicationSetCondition 382 383 if errOccurred { 384 errOccurredCondition = condition 385 } else { 386 errOccurredCondition = argov1alpha1.ApplicationSetCondition{ 387 Type: argov1alpha1.ApplicationSetConditionErrorOccurred, 388 Message: "Successfully generated parameters for all Applications", 389 Reason: argov1alpha1.ApplicationSetReasonApplicationSetUpToDate, 390 Status: argov1alpha1.ApplicationSetConditionStatusFalse, 391 } 392 } 393 394 paramtersGeneratedCondition := getParametersGeneratedCondition(paramtersGenerated, condition.Message) 395 resourceUpToDateCondition := getResourceUpToDateCondition(errOccurred, condition.Message, condition.Reason) 396 397 newConditions := []argov1alpha1.ApplicationSetCondition{errOccurredCondition, paramtersGeneratedCondition, resourceUpToDateCondition} 398 399 needToUpdateConditions := false 400 for _, condition := range newConditions { 401 // do nothing if appset already has same condition 402 for _, c := range applicationSet.Status.Conditions { 403 if c.Type == condition.Type && (c.Reason != condition.Reason || c.Status != condition.Status || c.Message != condition.Message) { 404 needToUpdateConditions = true 405 break 406 } 407 } 408 } 409 evaluatedTypes := map[argov1alpha1.ApplicationSetConditionType]bool{ 410 argov1alpha1.ApplicationSetConditionErrorOccurred: true, 411 argov1alpha1.ApplicationSetConditionParametersGenerated: true, 412 argov1alpha1.ApplicationSetConditionResourcesUpToDate: true, 413 } 414 415 if needToUpdateConditions || len(applicationSet.Status.Conditions) < 3 { 416 // fetch updated Application Set object before updating it 417 namespacedName := types.NamespacedName{Namespace: applicationSet.Namespace, Name: applicationSet.Name} 418 if err := r.Get(ctx, namespacedName, applicationSet); err != nil { 419 if client.IgnoreNotFound(err) != nil { 420 return nil 421 } 422 return fmt.Errorf("error fetching updated application set: %v", err) 423 } 424 425 applicationSet.Status.SetConditions( 426 newConditions, evaluatedTypes, 427 ) 428 429 // Update the newly fetched object with new set of conditions 430 err := r.Client.Status().Update(ctx, applicationSet) 431 if err != nil && !apierr.IsNotFound(err) { 432 return fmt.Errorf("unable to set application set condition: %v", err) 433 } 434 } 435 436 return nil 437 } 438 439 // validateGeneratedApplications uses the Argo CD validation functions to verify the correctness of the 440 // generated applications. 441 func (r *ApplicationSetReconciler) validateGeneratedApplications(ctx context.Context, desiredApplications []argov1alpha1.Application, applicationSetInfo argov1alpha1.ApplicationSet) (map[int]error, error) { 442 errorsByIndex := map[int]error{} 443 namesSet := map[string]bool{} 444 for i, app := range desiredApplications { 445 446 if !namesSet[app.Name] { 447 namesSet[app.Name] = true 448 } else { 449 errorsByIndex[i] = fmt.Errorf("ApplicationSet %s contains applications with duplicate name: %s", applicationSetInfo.Name, app.Name) 450 continue 451 } 452 _, err := r.ArgoAppClientset.ArgoprojV1alpha1().AppProjects(r.ArgoCDNamespace).Get(ctx, app.Spec.GetProject(), metav1.GetOptions{}) 453 if err != nil { 454 if apierr.IsNotFound(err) { 455 errorsByIndex[i] = fmt.Errorf("application references project %s which does not exist", app.Spec.Project) 456 continue 457 } 458 return nil, err 459 } 460 461 if err := utils.ValidateDestination(ctx, &app.Spec.Destination, r.KubeClientset, r.ArgoCDNamespace); err != nil { 462 errorsByIndex[i] = fmt.Errorf("application destination spec is invalid: %s", err.Error()) 463 continue 464 } 465 466 } 467 468 return errorsByIndex, nil 469 } 470 471 func (r *ApplicationSetReconciler) getMinRequeueAfter(applicationSetInfo *argov1alpha1.ApplicationSet) time.Duration { 472 var res time.Duration 473 for _, requestedGenerator := range applicationSetInfo.Spec.Generators { 474 475 relevantGenerators := generators.GetRelevantGenerators(&requestedGenerator, r.Generators) 476 477 for _, g := range relevantGenerators { 478 t := g.GetRequeueAfter(&requestedGenerator) 479 480 if res == 0 { 481 res = t 482 } else if t != 0 && t < res { 483 res = t 484 } 485 } 486 } 487 488 return res 489 } 490 491 func getTempApplication(applicationSetTemplate argov1alpha1.ApplicationSetTemplate) *argov1alpha1.Application { 492 var tmplApplication argov1alpha1.Application 493 tmplApplication.Annotations = applicationSetTemplate.Annotations 494 tmplApplication.Labels = applicationSetTemplate.Labels 495 tmplApplication.Namespace = applicationSetTemplate.Namespace 496 tmplApplication.Name = applicationSetTemplate.Name 497 tmplApplication.Spec = applicationSetTemplate.Spec 498 tmplApplication.Finalizers = applicationSetTemplate.Finalizers 499 500 return &tmplApplication 501 } 502 503 func (r *ApplicationSetReconciler) generateApplications(logCtx *log.Entry, applicationSetInfo argov1alpha1.ApplicationSet) ([]argov1alpha1.Application, argov1alpha1.ApplicationSetReasonType, error) { 504 var res []argov1alpha1.Application 505 506 var firstError error 507 var applicationSetReason argov1alpha1.ApplicationSetReasonType 508 509 for _, requestedGenerator := range applicationSetInfo.Spec.Generators { 510 t, err := generators.Transform(requestedGenerator, r.Generators, applicationSetInfo.Spec.Template, &applicationSetInfo, map[string]interface{}{}) 511 if err != nil { 512 logCtx.WithError(err).WithField("generator", requestedGenerator). 513 Error("error generating application from params") 514 if firstError == nil { 515 firstError = err 516 applicationSetReason = argov1alpha1.ApplicationSetReasonApplicationParamsGenerationError 517 } 518 continue 519 } 520 521 for _, a := range t { 522 tmplApplication := getTempApplication(a.Template) 523 524 for _, p := range a.Params { 525 app, err := r.Renderer.RenderTemplateParams(tmplApplication, applicationSetInfo.Spec.SyncPolicy, p, applicationSetInfo.Spec.GoTemplate, applicationSetInfo.Spec.GoTemplateOptions) 526 527 if err != nil { 528 logCtx.WithError(err).WithField("params", a.Params).WithField("generator", requestedGenerator). 529 Error("error generating application from params") 530 531 if firstError == nil { 532 firstError = err 533 applicationSetReason = argov1alpha1.ApplicationSetReasonRenderTemplateParamsError 534 } 535 continue 536 } 537 538 if applicationSetInfo.Spec.TemplatePatch != nil { 539 patchedApplication, err := r.applyTemplatePatch(app, applicationSetInfo, p) 540 541 if err != nil { 542 log.WithError(err).WithField("params", a.Params).WithField("generator", requestedGenerator). 543 Error("error generating application from params") 544 545 if firstError == nil { 546 firstError = err 547 applicationSetReason = argov1alpha1.ApplicationSetReasonRenderTemplateParamsError 548 } 549 continue 550 } 551 552 app = patchedApplication 553 } 554 555 res = append(res, *app) 556 } 557 } 558 559 logCtx.WithField("generator", requestedGenerator).Infof("generated %d applications", len(res)) 560 logCtx.WithField("generator", requestedGenerator).Debugf("apps from generator: %+v", res) 561 } 562 563 return res, applicationSetReason, firstError 564 } 565 566 func (r *ApplicationSetReconciler) applyTemplatePatch(app *argov1alpha1.Application, applicationSetInfo argov1alpha1.ApplicationSet, params map[string]interface{}) (*argov1alpha1.Application, error) { 567 replacedTemplate, err := r.Renderer.Replace(*applicationSetInfo.Spec.TemplatePatch, params, applicationSetInfo.Spec.GoTemplate, applicationSetInfo.Spec.GoTemplateOptions) 568 569 if err != nil { 570 return nil, fmt.Errorf("error replacing values in templatePatch: %w", err) 571 } 572 573 return applyTemplatePatch(app, replacedTemplate) 574 } 575 576 func ignoreNotAllowedNamespaces(namespaces []string) predicate.Predicate { 577 return predicate.Funcs{ 578 CreateFunc: func(e event.CreateEvent) bool { 579 return glob.MatchStringInList(namespaces, e.Object.GetNamespace(), false) 580 }, 581 } 582 } 583 584 func appControllerIndexer(rawObj client.Object) []string { 585 // grab the job object, extract the owner... 586 app := rawObj.(*argov1alpha1.Application) 587 owner := metav1.GetControllerOf(app) 588 if owner == nil { 589 return nil 590 } 591 // ...make sure it's a application set... 592 if owner.APIVersion != argov1alpha1.SchemeGroupVersion.String() || owner.Kind != "ApplicationSet" { 593 return nil 594 } 595 596 // ...and if so, return it 597 return []string{owner.Name} 598 } 599 600 func (r *ApplicationSetReconciler) SetupWithManager(mgr ctrl.Manager, enableProgressiveSyncs bool, maxConcurrentReconciliations int) error { 601 if err := mgr.GetFieldIndexer().IndexField(context.TODO(), &argov1alpha1.Application{}, ".metadata.controller", appControllerIndexer); err != nil { 602 return fmt.Errorf("error setting up with manager: %w", err) 603 } 604 605 ownsHandler := getOwnsHandlerPredicates(enableProgressiveSyncs) 606 607 return ctrl.NewControllerManagedBy(mgr).WithOptions(controller.Options{ 608 MaxConcurrentReconciles: maxConcurrentReconciliations, 609 }).For(&argov1alpha1.ApplicationSet{}). 610 Owns(&argov1alpha1.Application{}, builder.WithPredicates(ownsHandler)). 611 WithEventFilter(ignoreNotAllowedNamespaces(r.ApplicationSetNamespaces)). 612 Watches( 613 &source.Kind{Type: &corev1.Secret{}}, 614 &clusterSecretEventHandler{ 615 Client: mgr.GetClient(), 616 Log: log.WithField("type", "createSecretEventHandler"), 617 }). 618 // TODO: also watch Applications and respond on changes if we own them. 619 Complete(r) 620 } 621 622 func (r *ApplicationSetReconciler) updateCache(ctx context.Context, obj client.Object, logger *log.Entry) { 623 informer, err := r.Cache.GetInformer(ctx, obj) 624 if err != nil { 625 logger.Errorf("failed to get informer: %v", err) 626 return 627 } 628 // The controller runtime abstract away informers creation 629 // so unfortunately could not find any other way to access informer store. 630 k8sInformer, ok := informer.(k8scache.SharedInformer) 631 if !ok { 632 logger.Error("informer is not a kubernetes informer") 633 return 634 } 635 if err := k8sInformer.GetStore().Update(obj); err != nil { 636 logger.Errorf("failed to update cache: %v", err) 637 return 638 } 639 } 640 641 // createOrUpdateInCluster will create / update application resources in the cluster. 642 // - For new applications, it will call create 643 // - For existing application, it will call update 644 // The function also adds owner reference to all applications, and uses it to delete them. 645 func (r *ApplicationSetReconciler) createOrUpdateInCluster(ctx context.Context, logCtx *log.Entry, applicationSet argov1alpha1.ApplicationSet, desiredApplications []argov1alpha1.Application) error { 646 647 var firstError error 648 // Creates or updates the application in appList 649 for _, generatedApp := range desiredApplications { 650 // The app's namespace must be the same as the AppSet's namespace to preserve the appsets-in-any-namespace 651 // security boundary. 652 generatedApp.Namespace = applicationSet.Namespace 653 654 appLog := logCtx.WithFields(log.Fields{"app": generatedApp.QualifiedName()}) 655 656 // Normalize to avoid fighting with the application controller. 657 generatedApp.Spec = *argoutil.NormalizeApplicationSpec(&generatedApp.Spec) 658 659 found := &argov1alpha1.Application{ 660 ObjectMeta: metav1.ObjectMeta{ 661 Name: generatedApp.Name, 662 Namespace: generatedApp.Namespace, 663 }, 664 TypeMeta: metav1.TypeMeta{ 665 Kind: application.ApplicationKind, 666 APIVersion: "argoproj.io/v1alpha1", 667 }, 668 } 669 670 action, err := utils.CreateOrUpdate(ctx, appLog, r.Client, applicationSet.Spec.IgnoreApplicationDifferences, normalizers.IgnoreNormalizerOpts{}, found, func() error { 671 // Copy only the Application/ObjectMeta fields that are significant, from the generatedApp 672 found.Spec = generatedApp.Spec 673 674 // allow setting the Operation field to trigger a sync operation on an Application 675 if generatedApp.Operation != nil { 676 found.Operation = generatedApp.Operation 677 } 678 679 preservedAnnotations := make([]string, 0) 680 preservedLabels := make([]string, 0) 681 682 if applicationSet.Spec.PreservedFields != nil { 683 preservedAnnotations = append(preservedAnnotations, applicationSet.Spec.PreservedFields.Annotations...) 684 preservedLabels = append(preservedLabels, applicationSet.Spec.PreservedFields.Labels...) 685 } 686 687 if len(r.GlobalPreservedAnnotations) > 0 { 688 preservedAnnotations = append(preservedAnnotations, r.GlobalPreservedAnnotations...) 689 } 690 691 if len(r.GlobalPreservedLabels) > 0 { 692 preservedLabels = append(preservedLabels, r.GlobalPreservedLabels...) 693 } 694 695 // Preserve specially treated argo cd annotations: 696 // * https://github.com/argoproj/applicationset/issues/180 697 // * https://github.com/argoproj/argo-cd/issues/10500 698 preservedAnnotations = append(preservedAnnotations, defaultPreservedAnnotations...) 699 700 for _, key := range preservedAnnotations { 701 if state, exists := found.ObjectMeta.Annotations[key]; exists { 702 if generatedApp.Annotations == nil { 703 generatedApp.Annotations = map[string]string{} 704 } 705 generatedApp.Annotations[key] = state 706 } 707 } 708 709 for _, key := range preservedLabels { 710 if state, exists := found.ObjectMeta.Labels[key]; exists { 711 if generatedApp.Labels == nil { 712 generatedApp.Labels = map[string]string{} 713 } 714 generatedApp.Labels[key] = state 715 } 716 } 717 718 found.ObjectMeta.Annotations = generatedApp.Annotations 719 720 found.ObjectMeta.Finalizers = generatedApp.Finalizers 721 found.ObjectMeta.Labels = generatedApp.Labels 722 723 return controllerutil.SetControllerReference(&applicationSet, found, r.Scheme) 724 }) 725 726 if err != nil { 727 appLog.WithError(err).WithField("action", action).Errorf("failed to %s Application", action) 728 if firstError == nil { 729 firstError = err 730 } 731 continue 732 } 733 r.updateCache(ctx, found, appLog) 734 735 if action != controllerutil.OperationResultNone { 736 // Don't pollute etcd with "unchanged Application" events 737 r.Recorder.Eventf(&applicationSet, corev1.EventTypeNormal, fmt.Sprint(action), "%s Application %q", action, generatedApp.Name) 738 appLog.Logf(log.InfoLevel, "%s Application", action) 739 } else { 740 // "unchanged Application" can be inferred by Reconcile Complete with no action being listed 741 // Or enable debug logging 742 appLog.Logf(log.DebugLevel, "%s Application", action) 743 } 744 } 745 return firstError 746 } 747 748 // createInCluster will filter from the desiredApplications only the application that needs to be created 749 // Then it will call createOrUpdateInCluster to do the actual create 750 func (r *ApplicationSetReconciler) createInCluster(ctx context.Context, logCtx *log.Entry, applicationSet argov1alpha1.ApplicationSet, desiredApplications []argov1alpha1.Application) error { 751 752 var createApps []argov1alpha1.Application 753 current, err := r.getCurrentApplications(ctx, applicationSet) 754 if err != nil { 755 return fmt.Errorf("error getting current applications: %w", err) 756 } 757 758 m := make(map[string]bool) // Will holds the app names that are current in the cluster 759 760 for _, app := range current { 761 m[app.Name] = true 762 } 763 764 // filter applications that are not in m[string]bool (new to the cluster) 765 for _, app := range desiredApplications { 766 _, exists := m[app.Name] 767 768 if !exists { 769 createApps = append(createApps, app) 770 } 771 } 772 773 return r.createOrUpdateInCluster(ctx, logCtx, applicationSet, createApps) 774 } 775 776 func (r *ApplicationSetReconciler) getCurrentApplications(ctx context.Context, applicationSet argov1alpha1.ApplicationSet) ([]argov1alpha1.Application, error) { 777 var current argov1alpha1.ApplicationList 778 err := r.Client.List(ctx, ¤t, client.MatchingFields{".metadata.controller": applicationSet.Name}, client.InNamespace(applicationSet.Namespace)) 779 780 if err != nil { 781 return nil, fmt.Errorf("error retrieving applications: %w", err) 782 } 783 784 return current.Items, nil 785 } 786 787 // deleteInCluster will delete Applications that are currently on the cluster, but not in appList. 788 // The function must be called after all generators had been called and generated applications 789 func (r *ApplicationSetReconciler) deleteInCluster(ctx context.Context, logCtx *log.Entry, applicationSet argov1alpha1.ApplicationSet, desiredApplications []argov1alpha1.Application) error { 790 // settingsMgr := settings.NewSettingsManager(context.TODO(), r.KubeClientset, applicationSet.Namespace) 791 // argoDB := db.NewDB(applicationSet.Namespace, settingsMgr, r.KubeClientset) 792 // clusterList, err := argoDB.ListClusters(ctx) 793 clusterList, err := utils.ListClusters(ctx, r.KubeClientset, r.ArgoCDNamespace) 794 if err != nil { 795 return fmt.Errorf("error listing clusters: %w", err) 796 } 797 798 // Save current applications to be able to delete the ones that are not in appList 799 current, err := r.getCurrentApplications(ctx, applicationSet) 800 if err != nil { 801 return fmt.Errorf("error getting current applications: %w", err) 802 } 803 804 m := make(map[string]bool) // Will holds the app names in appList for the deletion process 805 806 for _, app := range desiredApplications { 807 m[app.Name] = true 808 } 809 810 // Delete apps that are not in m[string]bool 811 var firstError error 812 for _, app := range current { 813 logCtx = logCtx.WithField("app", app.QualifiedName()) 814 _, exists := m[app.Name] 815 816 if !exists { 817 818 // Removes the Argo CD resources finalizer if the application contains an invalid target (eg missing cluster) 819 err := r.removeFinalizerOnInvalidDestination(ctx, applicationSet, &app, clusterList, logCtx) 820 if err != nil { 821 logCtx.WithError(err).Error("failed to update Application") 822 if firstError != nil { 823 firstError = err 824 } 825 continue 826 } 827 828 err = r.Client.Delete(ctx, &app) 829 if err != nil { 830 logCtx.WithError(err).Error("failed to delete Application") 831 if firstError != nil { 832 firstError = err 833 } 834 continue 835 } 836 r.Recorder.Eventf(&applicationSet, corev1.EventTypeNormal, "Deleted", "Deleted Application %q", app.Name) 837 logCtx.Log(log.InfoLevel, "Deleted application") 838 } 839 } 840 return firstError 841 } 842 843 // removeFinalizerOnInvalidDestination removes the Argo CD resources finalizer if the application contains an invalid target (eg missing cluster) 844 func (r *ApplicationSetReconciler) removeFinalizerOnInvalidDestination(ctx context.Context, applicationSet argov1alpha1.ApplicationSet, app *argov1alpha1.Application, clusterList *argov1alpha1.ClusterList, appLog *log.Entry) error { 845 846 // Only check if the finalizers need to be removed IF there are finalizers to remove 847 if len(app.Finalizers) == 0 { 848 return nil 849 } 850 851 var validDestination bool 852 853 // Detect if the destination is invalid (name doesn't correspond to a matching cluster) 854 if err := utils.ValidateDestination(ctx, &app.Spec.Destination, r.KubeClientset, r.ArgoCDNamespace); err != nil { 855 appLog.Warnf("The destination cluster for %s couldn't be found: %v", app.Name, err) 856 validDestination = false 857 } else { 858 859 // Detect if the destination's server field does not match an existing cluster 860 861 matchingCluster := false 862 for _, cluster := range clusterList.Items { 863 864 // Server fields must match. Note that ValidateDestination ensures that the server field is set, if applicable. 865 if app.Spec.Destination.Server != cluster.Server { 866 continue 867 } 868 869 // The name must match, if it is not empty 870 if app.Spec.Destination.Name != "" && cluster.Name != app.Spec.Destination.Name { 871 continue 872 } 873 874 matchingCluster = true 875 break 876 } 877 878 if !matchingCluster { 879 appLog.Warnf("A match for the destination cluster for %s, by server url, couldn't be found.", app.Name) 880 } 881 882 validDestination = matchingCluster 883 } 884 // If the destination is invalid (for example the cluster is no longer defined), then remove 885 // the application finalizers to avoid triggering Argo CD bug #5817 886 if !validDestination { 887 888 // Filter out the Argo CD finalizer from the finalizer list 889 var newFinalizers []string 890 for _, existingFinalizer := range app.Finalizers { 891 if existingFinalizer != argov1alpha1.ResourcesFinalizerName { // only remove this one 892 newFinalizers = append(newFinalizers, existingFinalizer) 893 } 894 } 895 896 // If the finalizer length changed (due to filtering out an Argo finalizer), update the finalizer list on the app 897 if len(newFinalizers) != len(app.Finalizers) { 898 updated := app.DeepCopy() 899 updated.Finalizers = newFinalizers 900 patch := client.MergeFrom(app) 901 if log.IsLevelEnabled(log.DebugLevel) { 902 utils.LogPatch(appLog, patch, updated) 903 } 904 if err := r.Client.Patch(ctx, updated, patch); err != nil { 905 return fmt.Errorf("error updating finalizers: %w", err) 906 } 907 r.updateCache(ctx, updated, appLog) 908 // Application must have updated list of finalizers 909 updated.DeepCopyInto(app) 910 911 r.Recorder.Eventf(&applicationSet, corev1.EventTypeNormal, "Updated", "Updated Application %q finalizer before deletion, because application has an invalid destination", app.Name) 912 appLog.Log(log.InfoLevel, "Updating application finalizer before deletion, because application has an invalid destination") 913 } 914 } 915 916 return nil 917 } 918 919 func (r *ApplicationSetReconciler) removeOwnerReferencesOnDeleteAppSet(ctx context.Context, applicationSet argov1alpha1.ApplicationSet) error { 920 applications, err := r.getCurrentApplications(ctx, applicationSet) 921 if err != nil { 922 return err 923 } 924 925 for _, app := range applications { 926 app.SetOwnerReferences([]metav1.OwnerReference{}) 927 err := r.Client.Update(ctx, &app) 928 if err != nil { 929 return err 930 } 931 } 932 933 return nil 934 } 935 936 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) { 937 938 appDependencyList, appStepMap, err := r.buildAppDependencyList(logCtx, appset, desiredApplications) 939 if err != nil { 940 return nil, fmt.Errorf("failed to build app dependency list: %w", err) 941 } 942 943 _, err = r.updateApplicationSetApplicationStatus(ctx, logCtx, &appset, applications, appStepMap) 944 if err != nil { 945 return nil, fmt.Errorf("failed to update applicationset app status: %w", err) 946 } 947 948 logCtx.Infof("ApplicationSet %v step list:", appset.Name) 949 for i, step := range appDependencyList { 950 logCtx.Infof("step %v: %+v", i+1, step) 951 } 952 953 appSyncMap, err := r.buildAppSyncMap(ctx, appset, appDependencyList, appMap) 954 if err != nil { 955 return nil, fmt.Errorf("failed to build app sync map: %w", err) 956 } 957 958 logCtx.Infof("Application allowed to sync before maxUpdate?: %+v", appSyncMap) 959 960 _, err = r.updateApplicationSetApplicationStatusProgress(ctx, logCtx, &appset, appSyncMap, appStepMap, appMap) 961 if err != nil { 962 return nil, fmt.Errorf("failed to update applicationset application status progress: %w", err) 963 } 964 965 _, err = r.updateApplicationSetApplicationStatusConditions(ctx, &appset) 966 if err != nil { 967 return nil, fmt.Errorf("failed to update applicationset application status conditions: %w", err) 968 } 969 970 return appSyncMap, nil 971 } 972 973 // this list tracks which Applications belong to each RollingUpdate step 974 func (r *ApplicationSetReconciler) buildAppDependencyList(logCtx *log.Entry, applicationSet argov1alpha1.ApplicationSet, applications []argov1alpha1.Application) ([][]string, map[string]int, error) { 975 976 if applicationSet.Spec.Strategy == nil || applicationSet.Spec.Strategy.Type == "" || applicationSet.Spec.Strategy.Type == "AllAtOnce" { 977 return [][]string{}, map[string]int{}, nil 978 } 979 980 steps := []argov1alpha1.ApplicationSetRolloutStep{} 981 if progressiveSyncsStrategyEnabled(&applicationSet, "RollingSync") { 982 steps = applicationSet.Spec.Strategy.RollingSync.Steps 983 } 984 985 appDependencyList := make([][]string, 0) 986 for range steps { 987 appDependencyList = append(appDependencyList, make([]string, 0)) 988 } 989 990 appStepMap := map[string]int{} 991 992 // use applicationLabelSelectors to filter generated Applications into steps and status by name 993 for _, app := range applications { 994 for i, step := range steps { 995 996 selected := true // default to true, assuming the current Application is a match for the given step matchExpression 997 998 for _, matchExpression := range step.MatchExpressions { 999 1000 if val, ok := app.Labels[matchExpression.Key]; ok { 1001 valueMatched := labelMatchedExpression(logCtx, val, matchExpression) 1002 1003 if !valueMatched { // none of the matchExpression values was a match with the Application's labels 1004 selected = false 1005 break 1006 } 1007 } else if matchExpression.Operator == "In" { 1008 selected = false // no matching label key with "In" operator means this Application will not be included in the current step 1009 break 1010 } 1011 } 1012 1013 if selected { 1014 appDependencyList[i] = append(appDependencyList[i], app.Name) 1015 if val, ok := appStepMap[app.Name]; ok { 1016 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) 1017 } else { 1018 appStepMap[app.Name] = i 1019 } 1020 } 1021 } 1022 } 1023 1024 return appDependencyList, appStepMap, nil 1025 } 1026 1027 func labelMatchedExpression(logCtx *log.Entry, val string, matchExpression argov1alpha1.ApplicationMatchExpression) bool { 1028 if matchExpression.Operator != "In" && matchExpression.Operator != "NotIn" { 1029 logCtx.Errorf("skipping AppSet rollingUpdate step Application selection, invalid matchExpression operator provided: %q ", matchExpression.Operator) 1030 return false 1031 } 1032 1033 // if operator == In, default to false 1034 // if operator == NotIn, default to true 1035 valueMatched := matchExpression.Operator == "NotIn" 1036 1037 for _, value := range matchExpression.Values { 1038 if val == value { 1039 // first "In" match returns true 1040 // first "NotIn" match returns false 1041 return matchExpression.Operator == "In" 1042 } 1043 } 1044 return valueMatched 1045 } 1046 1047 // this map is used to determine which stage of Applications are ready to be updated in the reconciler loop 1048 func (r *ApplicationSetReconciler) buildAppSyncMap(ctx context.Context, applicationSet argov1alpha1.ApplicationSet, appDependencyList [][]string, appMap map[string]argov1alpha1.Application) (map[string]bool, error) { 1049 appSyncMap := map[string]bool{} 1050 syncEnabled := true 1051 1052 // healthy stages and the first non-healthy stage should have sync enabled 1053 // every stage after should have sync disabled 1054 1055 for i := range appDependencyList { 1056 // set the syncEnabled boolean for every Application in the current step 1057 for _, appName := range appDependencyList[i] { 1058 appSyncMap[appName] = syncEnabled 1059 } 1060 1061 // detect if we need to halt before progressing to the next step 1062 for _, appName := range appDependencyList[i] { 1063 1064 idx := findApplicationStatusIndex(applicationSet.Status.ApplicationStatus, appName) 1065 if idx == -1 { 1066 // no Application status found, likely because the Application is being newly created 1067 syncEnabled = false 1068 break 1069 } 1070 1071 appStatus := applicationSet.Status.ApplicationStatus[idx] 1072 1073 if app, ok := appMap[appName]; ok { 1074 1075 syncEnabled = appSyncEnabledForNextStep(&applicationSet, app, appStatus) 1076 if !syncEnabled { 1077 break 1078 } 1079 } else { 1080 // application name not found in the list of applications managed by this ApplicationSet, maybe because it's being deleted 1081 syncEnabled = false 1082 break 1083 } 1084 } 1085 } 1086 1087 return appSyncMap, nil 1088 } 1089 1090 func appSyncEnabledForNextStep(appset *argov1alpha1.ApplicationSet, app argov1alpha1.Application, appStatus argov1alpha1.ApplicationSetApplicationStatus) bool { 1091 1092 if progressiveSyncsStrategyEnabled(appset, "RollingSync") { 1093 // we still need to complete the current step if the Application is not yet Healthy or there are still pending Application changes 1094 return isApplicationHealthy(app) && appStatus.Status == "Healthy" 1095 } 1096 1097 return true 1098 } 1099 1100 func progressiveSyncsStrategyEnabled(appset *argov1alpha1.ApplicationSet, strategyType string) bool { 1101 if appset.Spec.Strategy == nil || appset.Spec.Strategy.Type != strategyType { 1102 return false 1103 } 1104 1105 if strategyType == "RollingSync" && appset.Spec.Strategy.RollingSync == nil { 1106 return false 1107 } 1108 1109 return true 1110 } 1111 1112 func isApplicationHealthy(app argov1alpha1.Application) bool { 1113 healthStatusString, syncStatusString, operationPhaseString := statusStrings(app) 1114 1115 if healthStatusString == "Healthy" && syncStatusString != "OutOfSync" && (operationPhaseString == "Succeeded" || operationPhaseString == "") { 1116 return true 1117 } 1118 return false 1119 } 1120 1121 func statusStrings(app argov1alpha1.Application) (string, string, string) { 1122 healthStatusString := string(app.Status.Health.Status) 1123 syncStatusString := string(app.Status.Sync.Status) 1124 operationPhaseString := "" 1125 if app.Status.OperationState != nil { 1126 operationPhaseString = string(app.Status.OperationState.Phase) 1127 } 1128 1129 return healthStatusString, syncStatusString, operationPhaseString 1130 } 1131 1132 // check the status of each Application's status and promote Applications to the next status if needed 1133 func (r *ApplicationSetReconciler) updateApplicationSetApplicationStatus(ctx context.Context, logCtx *log.Entry, applicationSet *argov1alpha1.ApplicationSet, applications []argov1alpha1.Application, appStepMap map[string]int) ([]argov1alpha1.ApplicationSetApplicationStatus, error) { 1134 1135 now := metav1.Now() 1136 appStatuses := make([]argov1alpha1.ApplicationSetApplicationStatus, 0, len(applications)) 1137 1138 for _, app := range applications { 1139 1140 healthStatusString, syncStatusString, operationPhaseString := statusStrings(app) 1141 1142 idx := findApplicationStatusIndex(applicationSet.Status.ApplicationStatus, app.Name) 1143 1144 currentAppStatus := argov1alpha1.ApplicationSetApplicationStatus{} 1145 1146 if idx == -1 { 1147 // AppStatus not found, set default status of "Waiting" 1148 currentAppStatus = argov1alpha1.ApplicationSetApplicationStatus{ 1149 Application: app.Name, 1150 LastTransitionTime: &now, 1151 Message: "No Application status found, defaulting status to Waiting.", 1152 Status: "Waiting", 1153 Step: fmt.Sprint(appStepMap[app.Name] + 1), 1154 } 1155 } else { 1156 // we have an existing AppStatus 1157 currentAppStatus = applicationSet.Status.ApplicationStatus[idx] 1158 } 1159 1160 appOutdated := false 1161 if progressiveSyncsStrategyEnabled(applicationSet, "RollingSync") { 1162 appOutdated = syncStatusString == "OutOfSync" 1163 } 1164 1165 if appOutdated && currentAppStatus.Status != "Waiting" && currentAppStatus.Status != "Pending" { 1166 logCtx.Infof("Application %v is outdated, updating its ApplicationSet status to Waiting", app.Name) 1167 currentAppStatus.LastTransitionTime = &now 1168 currentAppStatus.Status = "Waiting" 1169 currentAppStatus.Message = "Application has pending changes, setting status to Waiting." 1170 currentAppStatus.Step = fmt.Sprint(appStepMap[currentAppStatus.Application] + 1) 1171 } 1172 1173 if currentAppStatus.Status == "Pending" { 1174 // check for successful syncs started less than 10s before the Application transitioned to Pending 1175 // this covers race conditions where syncs initiated by RollingSync miraculously have a sync time before the transition to Pending state occurred (could be a few seconds) 1176 if operationPhaseString == "Succeeded" && app.Status.OperationState.StartedAt.Add(time.Duration(10)*time.Second).After(currentAppStatus.LastTransitionTime.Time) { 1177 if !app.Status.OperationState.StartedAt.After(currentAppStatus.LastTransitionTime.Time) { 1178 logCtx.Warnf("Application %v was synced less than 10s prior to entering Pending status, we'll assume the AppSet controller triggered this sync and update its status to Progressing", app.Name) 1179 } 1180 logCtx.Infof("Application %v has completed a sync successfully, updating its ApplicationSet status to Progressing", app.Name) 1181 currentAppStatus.LastTransitionTime = &now 1182 currentAppStatus.Status = "Progressing" 1183 currentAppStatus.Message = "Application resource completed a sync successfully, updating status from Pending to Progressing." 1184 currentAppStatus.Step = fmt.Sprint(appStepMap[currentAppStatus.Application] + 1) 1185 } else if operationPhaseString == "Running" || healthStatusString == "Progressing" { 1186 logCtx.Infof("Application %v has entered Progressing status, updating its ApplicationSet status to Progressing", app.Name) 1187 currentAppStatus.LastTransitionTime = &now 1188 currentAppStatus.Status = "Progressing" 1189 currentAppStatus.Message = "Application resource became Progressing, updating status from Pending to Progressing." 1190 currentAppStatus.Step = fmt.Sprint(appStepMap[currentAppStatus.Application] + 1) 1191 } 1192 } 1193 1194 if currentAppStatus.Status == "Waiting" && isApplicationHealthy(app) { 1195 logCtx.Infof("Application %v is already synced and healthy, updating its ApplicationSet status to Healthy", app.Name) 1196 currentAppStatus.LastTransitionTime = &now 1197 currentAppStatus.Status = healthStatusString 1198 currentAppStatus.Message = "Application resource is already Healthy, updating status from Waiting to Healthy." 1199 currentAppStatus.Step = fmt.Sprint(appStepMap[currentAppStatus.Application] + 1) 1200 } 1201 1202 if currentAppStatus.Status == "Progressing" && isApplicationHealthy(app) { 1203 logCtx.Infof("Application %v has completed Progressing status, updating its ApplicationSet status to Healthy", app.Name) 1204 currentAppStatus.LastTransitionTime = &now 1205 currentAppStatus.Status = healthStatusString 1206 currentAppStatus.Message = "Application resource became Healthy, updating status from Progressing to Healthy." 1207 currentAppStatus.Step = fmt.Sprint(appStepMap[currentAppStatus.Application] + 1) 1208 } 1209 1210 appStatuses = append(appStatuses, currentAppStatus) 1211 } 1212 1213 err := r.setAppSetApplicationStatus(ctx, logCtx, applicationSet, appStatuses) 1214 if err != nil { 1215 return nil, fmt.Errorf("failed to set AppSet application statuses: %w", err) 1216 } 1217 1218 return appStatuses, nil 1219 } 1220 1221 // check Applications that are in Waiting status and promote them to Pending if needed 1222 func (r *ApplicationSetReconciler) updateApplicationSetApplicationStatusProgress(ctx context.Context, logCtx *log.Entry, applicationSet *argov1alpha1.ApplicationSet, appSyncMap map[string]bool, appStepMap map[string]int, appMap map[string]argov1alpha1.Application) ([]argov1alpha1.ApplicationSetApplicationStatus, error) { 1223 now := metav1.Now() 1224 1225 appStatuses := make([]argov1alpha1.ApplicationSetApplicationStatus, 0, len(applicationSet.Status.ApplicationStatus)) 1226 1227 // if we have no RollingUpdate steps, clear out the existing ApplicationStatus entries 1228 if applicationSet.Spec.Strategy != nil && applicationSet.Spec.Strategy.Type != "" && applicationSet.Spec.Strategy.Type != "AllAtOnce" { 1229 updateCountMap := []int{} 1230 totalCountMap := []int{} 1231 1232 length := 0 1233 if progressiveSyncsStrategyEnabled(applicationSet, "RollingSync") { 1234 length = len(applicationSet.Spec.Strategy.RollingSync.Steps) 1235 } 1236 for s := 0; s < length; s++ { 1237 updateCountMap = append(updateCountMap, 0) 1238 totalCountMap = append(totalCountMap, 0) 1239 } 1240 1241 // populate updateCountMap with counts of existing Pending and Progressing Applications 1242 for _, appStatus := range applicationSet.Status.ApplicationStatus { 1243 totalCountMap[appStepMap[appStatus.Application]] += 1 1244 1245 if progressiveSyncsStrategyEnabled(applicationSet, "RollingSync") { 1246 if appStatus.Status == "Pending" || appStatus.Status == "Progressing" { 1247 updateCountMap[appStepMap[appStatus.Application]] += 1 1248 } 1249 } 1250 } 1251 1252 for _, appStatus := range applicationSet.Status.ApplicationStatus { 1253 1254 maxUpdateAllowed := true 1255 maxUpdate := &intstr.IntOrString{} 1256 if progressiveSyncsStrategyEnabled(applicationSet, "RollingSync") { 1257 maxUpdate = applicationSet.Spec.Strategy.RollingSync.Steps[appStepMap[appStatus.Application]].MaxUpdate 1258 } 1259 1260 // by default allow all applications to update if maxUpdate is unset 1261 if maxUpdate != nil { 1262 maxUpdateVal, err := intstr.GetScaledValueFromIntOrPercent(maxUpdate, totalCountMap[appStepMap[appStatus.Application]], false) 1263 if err != nil { 1264 logCtx.Warnf("AppSet '%v' has a invalid maxUpdate value '%+v', ignoring maxUpdate logic for this step: %v", applicationSet.Name, maxUpdate, err) 1265 } 1266 1267 // ensure that percentage values greater than 0% always result in at least 1 Application being selected 1268 if maxUpdate.Type == intstr.String && maxUpdate.StrVal != "0%" && maxUpdateVal < 1 { 1269 maxUpdateVal = 1 1270 } 1271 1272 if updateCountMap[appStepMap[appStatus.Application]] >= maxUpdateVal { 1273 maxUpdateAllowed = false 1274 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, appStepMap[appStatus.Application]+1, applicationSet.Name) 1275 } 1276 1277 } 1278 1279 if appStatus.Status == "Waiting" && appSyncMap[appStatus.Application] && maxUpdateAllowed { 1280 logCtx.Infof("Application %v moved to Pending status, watching for the Application to start Progressing", appStatus.Application) 1281 appStatus.LastTransitionTime = &now 1282 appStatus.Status = "Pending" 1283 appStatus.Message = "Application moved to Pending status, watching for the Application resource to start Progressing." 1284 appStatus.Step = fmt.Sprint(appStepMap[appStatus.Application] + 1) 1285 1286 updateCountMap[appStepMap[appStatus.Application]] += 1 1287 } 1288 1289 appStatuses = append(appStatuses, appStatus) 1290 } 1291 } 1292 1293 err := r.setAppSetApplicationStatus(ctx, logCtx, applicationSet, appStatuses) 1294 if err != nil { 1295 return nil, fmt.Errorf("failed to set AppSet app status: %w", err) 1296 } 1297 1298 return appStatuses, nil 1299 } 1300 1301 func (r *ApplicationSetReconciler) updateApplicationSetApplicationStatusConditions(ctx context.Context, applicationSet *argov1alpha1.ApplicationSet) ([]argov1alpha1.ApplicationSetCondition, error) { 1302 1303 appSetProgressing := false 1304 for _, appStatus := range applicationSet.Status.ApplicationStatus { 1305 if appStatus.Status != "Healthy" { 1306 appSetProgressing = true 1307 break 1308 } 1309 } 1310 1311 appSetConditionProgressing := false 1312 for _, appSetCondition := range applicationSet.Status.Conditions { 1313 if appSetCondition.Type == argov1alpha1.ApplicationSetConditionRolloutProgressing && appSetCondition.Status == argov1alpha1.ApplicationSetConditionStatusTrue { 1314 appSetConditionProgressing = true 1315 break 1316 } 1317 } 1318 1319 if appSetProgressing && !appSetConditionProgressing { 1320 _ = r.setApplicationSetStatusCondition(ctx, 1321 applicationSet, 1322 argov1alpha1.ApplicationSetCondition{ 1323 Type: argov1alpha1.ApplicationSetConditionRolloutProgressing, 1324 Message: "ApplicationSet Rollout Rollout started", 1325 Reason: argov1alpha1.ApplicationSetReasonApplicationSetModified, 1326 Status: argov1alpha1.ApplicationSetConditionStatusTrue, 1327 }, false, 1328 ) 1329 } else if !appSetProgressing && appSetConditionProgressing { 1330 _ = r.setApplicationSetStatusCondition(ctx, 1331 applicationSet, 1332 argov1alpha1.ApplicationSetCondition{ 1333 Type: argov1alpha1.ApplicationSetConditionRolloutProgressing, 1334 Message: "ApplicationSet Rollout Rollout complete", 1335 Reason: argov1alpha1.ApplicationSetReasonApplicationSetRolloutComplete, 1336 Status: argov1alpha1.ApplicationSetConditionStatusFalse, 1337 }, false, 1338 ) 1339 } 1340 1341 return applicationSet.Status.Conditions, nil 1342 } 1343 1344 func findApplicationStatusIndex(appStatuses []argov1alpha1.ApplicationSetApplicationStatus, application string) int { 1345 for i := range appStatuses { 1346 if appStatuses[i].Application == application { 1347 return i 1348 } 1349 } 1350 return -1 1351 } 1352 1353 // setApplicationSetApplicationStatus updates the ApplicatonSet's status field 1354 // with any new/changed Application statuses. 1355 func (r *ApplicationSetReconciler) setAppSetApplicationStatus(ctx context.Context, logCtx *log.Entry, applicationSet *argov1alpha1.ApplicationSet, applicationStatuses []argov1alpha1.ApplicationSetApplicationStatus) error { 1356 needToUpdateStatus := false 1357 1358 if len(applicationStatuses) != len(applicationSet.Status.ApplicationStatus) { 1359 needToUpdateStatus = true 1360 } else { 1361 for i := range applicationStatuses { 1362 appStatus := applicationStatuses[i] 1363 idx := findApplicationStatusIndex(applicationSet.Status.ApplicationStatus, appStatus.Application) 1364 if idx == -1 { 1365 needToUpdateStatus = true 1366 break 1367 } 1368 currentStatus := applicationSet.Status.ApplicationStatus[idx] 1369 if currentStatus.Message != appStatus.Message || currentStatus.Status != appStatus.Status || currentStatus.Step != appStatus.Step { 1370 needToUpdateStatus = true 1371 break 1372 } 1373 } 1374 } 1375 1376 if needToUpdateStatus { 1377 namespacedName := types.NamespacedName{Namespace: applicationSet.Namespace, Name: applicationSet.Name} 1378 1379 // rebuild ApplicationStatus from scratch, we don't need any previous status history 1380 applicationSet.Status.ApplicationStatus = []argov1alpha1.ApplicationSetApplicationStatus{} 1381 for i := range applicationStatuses { 1382 applicationSet.Status.SetApplicationStatus(applicationStatuses[i]) 1383 } 1384 1385 // Update the newly fetched object with new set of ApplicationStatus 1386 err := r.Client.Status().Update(ctx, applicationSet) 1387 if err != nil { 1388 1389 logCtx.Errorf("unable to set application set status: %v", err) 1390 return fmt.Errorf("unable to set application set status: %v", err) 1391 } 1392 1393 if err := r.Get(ctx, namespacedName, applicationSet); err != nil { 1394 if client.IgnoreNotFound(err) != nil { 1395 return nil 1396 } 1397 return fmt.Errorf("error fetching updated application set: %v", err) 1398 } 1399 } 1400 1401 return nil 1402 } 1403 1404 func (r *ApplicationSetReconciler) syncValidApplications(logCtx *log.Entry, applicationSet *argov1alpha1.ApplicationSet, appSyncMap map[string]bool, appMap map[string]argov1alpha1.Application, validApps []argov1alpha1.Application) ([]argov1alpha1.Application, error) { 1405 rolloutApps := []argov1alpha1.Application{} 1406 for i := range validApps { 1407 pruneEnabled := false 1408 1409 // ensure that Applications generated with RollingSync do not have an automated sync policy, since the AppSet controller will handle triggering the sync operation instead 1410 if validApps[i].Spec.SyncPolicy != nil && validApps[i].Spec.SyncPolicy.Automated != nil { 1411 pruneEnabled = validApps[i].Spec.SyncPolicy.Automated.Prune 1412 validApps[i].Spec.SyncPolicy.Automated = nil 1413 } 1414 1415 appSetStatusPending := false 1416 idx := findApplicationStatusIndex(applicationSet.Status.ApplicationStatus, validApps[i].Name) 1417 if idx > -1 && applicationSet.Status.ApplicationStatus[idx].Status == "Pending" { 1418 // only trigger a sync for Applications that are in Pending status, since this is governed by maxUpdate 1419 appSetStatusPending = true 1420 } 1421 1422 // check appSyncMap to determine which Applications are ready to be updated and which should be skipped 1423 if appSyncMap[validApps[i].Name] && appMap[validApps[i].Name].Status.Sync.Status == "OutOfSync" && appSetStatusPending { 1424 logCtx.Infof("triggering sync for application: %v, prune enabled: %v", validApps[i].Name, pruneEnabled) 1425 validApps[i], _ = syncApplication(validApps[i], pruneEnabled) 1426 } 1427 rolloutApps = append(rolloutApps, validApps[i]) 1428 } 1429 return rolloutApps, nil 1430 } 1431 1432 // used by the RollingSync Progressive Sync strategy to trigger a sync of a particular Application resource 1433 func syncApplication(application argov1alpha1.Application, prune bool) (argov1alpha1.Application, error) { 1434 1435 operation := argov1alpha1.Operation{ 1436 InitiatedBy: argov1alpha1.OperationInitiator{ 1437 Username: "applicationset-controller", 1438 Automated: true, 1439 }, 1440 Info: []*argov1alpha1.Info{ 1441 { 1442 Name: "Reason", 1443 Value: "ApplicationSet RollingSync triggered a sync of this Application resource.", 1444 }, 1445 }, 1446 Sync: &argov1alpha1.SyncOperation{}, 1447 } 1448 1449 if application.Spec.SyncPolicy != nil { 1450 if application.Spec.SyncPolicy.Retry != nil { 1451 operation.Retry = *application.Spec.SyncPolicy.Retry 1452 } 1453 if application.Spec.SyncPolicy.SyncOptions != nil { 1454 operation.Sync.SyncOptions = application.Spec.SyncPolicy.SyncOptions 1455 } 1456 operation.Sync.Prune = prune 1457 } 1458 application.Operation = &operation 1459 1460 return application, nil 1461 } 1462 1463 func getOwnsHandlerPredicates(enableProgressiveSyncs bool) predicate.Funcs { 1464 return predicate.Funcs{ 1465 CreateFunc: func(e event.CreateEvent) bool { 1466 // if we are the owner and there is a create event, we most likely created it and do not need to 1467 // re-reconcile 1468 if log.IsLevelEnabled(log.DebugLevel) { 1469 var appName string 1470 app, isApp := e.Object.(*argov1alpha1.Application) 1471 if isApp { 1472 appName = app.QualifiedName() 1473 } 1474 log.WithField("app", appName).Debugln("received create event from owning an application") 1475 } 1476 return false 1477 }, 1478 DeleteFunc: func(e event.DeleteEvent) bool { 1479 if log.IsLevelEnabled(log.DebugLevel) { 1480 var appName string 1481 app, isApp := e.Object.(*argov1alpha1.Application) 1482 if isApp { 1483 appName = app.QualifiedName() 1484 } 1485 log.WithField("app", appName).Debugln("received delete event from owning an application") 1486 } 1487 return true 1488 }, 1489 UpdateFunc: func(e event.UpdateEvent) bool { 1490 appOld, isApp := e.ObjectOld.(*argov1alpha1.Application) 1491 if !isApp { 1492 return false 1493 } 1494 logCtx := log.WithField("app", appOld.QualifiedName()) 1495 logCtx.Debugln("received update event from owning an application") 1496 appNew, isApp := e.ObjectNew.(*argov1alpha1.Application) 1497 if !isApp { 1498 return false 1499 } 1500 requeue := shouldRequeueApplicationSet(appOld, appNew, enableProgressiveSyncs) 1501 logCtx.WithField("requeue", requeue).Debugf("requeue: %t caused by application %s\n", requeue, appNew.Name) 1502 return requeue 1503 }, 1504 GenericFunc: func(e event.GenericEvent) bool { 1505 if log.IsLevelEnabled(log.DebugLevel) { 1506 var appName string 1507 app, isApp := e.Object.(*argov1alpha1.Application) 1508 if isApp { 1509 appName = app.QualifiedName() 1510 } 1511 log.WithField("app", appName).Debugln("received generic event from owning an application") 1512 } 1513 return true 1514 }, 1515 } 1516 } 1517 1518 // shouldRequeueApplicationSet determines when we want to requeue an ApplicationSet for reconciling based on an owned 1519 // application change 1520 // The applicationset controller owns a subset of the Application CR. 1521 // We do not need to re-reconcile if parts of the application change outside the applicationset's control. 1522 // An example being, Application.ApplicationStatus.ReconciledAt which gets updated by the application controller. 1523 // Additionally, Application.ObjectMeta.ResourceVersion and Application.ObjectMeta.Generation which are set by K8s. 1524 func shouldRequeueApplicationSet(appOld *argov1alpha1.Application, appNew *argov1alpha1.Application, enableProgressiveSyncs bool) bool { 1525 if appOld == nil || appNew == nil { 1526 return false 1527 } 1528 1529 // the applicationset controller owns the application spec, labels, annotations, and finalizers on the applications 1530 if !reflect.DeepEqual(appOld.Spec, appNew.Spec) || 1531 !reflect.DeepEqual(appOld.ObjectMeta.GetAnnotations(), appNew.ObjectMeta.GetAnnotations()) || 1532 !reflect.DeepEqual(appOld.ObjectMeta.GetLabels(), appNew.ObjectMeta.GetLabels()) || 1533 !reflect.DeepEqual(appOld.ObjectMeta.GetFinalizers(), appNew.ObjectMeta.GetFinalizers()) { 1534 return true 1535 } 1536 1537 // progressive syncs use the application status for updates. if they differ, requeue to trigger the next progression 1538 if enableProgressiveSyncs { 1539 if appOld.Status.Health.Status != appNew.Status.Health.Status || appOld.Status.Sync.Status != appNew.Status.Sync.Status { 1540 return true 1541 } 1542 1543 if appOld.Status.OperationState != nil && appNew.Status.OperationState != nil { 1544 if appOld.Status.OperationState.Phase != appNew.Status.OperationState.Phase || 1545 appOld.Status.OperationState.StartedAt != appNew.Status.OperationState.StartedAt { 1546 return true 1547 } 1548 } 1549 } 1550 1551 return false 1552 } 1553 1554 var _ handler.EventHandler = &clusterSecretEventHandler{}