github.com/argoproj/argo-cd/v2@v2.10.9/applicationset/webhook/webhook.go (about) 1 package webhook 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "html" 8 "net/http" 9 "net/url" 10 "regexp" 11 "strconv" 12 "strings" 13 14 "k8s.io/apimachinery/pkg/types" 15 "k8s.io/client-go/util/retry" 16 "sigs.k8s.io/controller-runtime/pkg/client" 17 18 "github.com/argoproj/argo-cd/v2/applicationset/generators" 19 "github.com/argoproj/argo-cd/v2/common" 20 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1" 21 argosettings "github.com/argoproj/argo-cd/v2/util/settings" 22 23 "github.com/go-playground/webhooks/v6/azuredevops" 24 "github.com/go-playground/webhooks/v6/github" 25 "github.com/go-playground/webhooks/v6/gitlab" 26 log "github.com/sirupsen/logrus" 27 ) 28 29 var ( 30 errBasicAuthVerificationFailed = errors.New("basic auth verification failed") 31 ) 32 33 type WebhookHandler struct { 34 namespace string 35 github *github.Webhook 36 gitlab *gitlab.Webhook 37 azuredevops *azuredevops.Webhook 38 azuredevopsAuthHandler func(r *http.Request) error 39 client client.Client 40 generators map[string]generators.Generator 41 } 42 43 type gitGeneratorInfo struct { 44 Revision string 45 TouchedHead bool 46 RepoRegexp *regexp.Regexp 47 } 48 49 type prGeneratorInfo struct { 50 Azuredevops *prGeneratorAzuredevopsInfo 51 Github *prGeneratorGithubInfo 52 Gitlab *prGeneratorGitlabInfo 53 } 54 55 type prGeneratorAzuredevopsInfo struct { 56 Repo string 57 Project string 58 } 59 60 type prGeneratorGithubInfo struct { 61 Repo string 62 Owner string 63 APIRegexp *regexp.Regexp 64 } 65 66 type prGeneratorGitlabInfo struct { 67 Project string 68 APIHostname string 69 } 70 71 func NewWebhookHandler(namespace string, argocdSettingsMgr *argosettings.SettingsManager, client client.Client, generators map[string]generators.Generator) (*WebhookHandler, error) { 72 // register the webhook secrets stored under "argocd-secret" for verifying incoming payloads 73 argocdSettings, err := argocdSettingsMgr.GetSettings() 74 if err != nil { 75 return nil, fmt.Errorf("Failed to get argocd settings: %v", err) 76 } 77 githubHandler, err := github.New(github.Options.Secret(argocdSettings.WebhookGitHubSecret)) 78 if err != nil { 79 return nil, fmt.Errorf("Unable to init GitHub webhook: %v", err) 80 } 81 gitlabHandler, err := gitlab.New(gitlab.Options.Secret(argocdSettings.WebhookGitLabSecret)) 82 if err != nil { 83 return nil, fmt.Errorf("Unable to init GitLab webhook: %v", err) 84 } 85 azuredevopsHandler, err := azuredevops.New() 86 if err != nil { 87 return nil, fmt.Errorf("Unable to init Azure DevOps webhook: %v", err) 88 } 89 azuredevopsAuthHandler := func(r *http.Request) error { 90 if argocdSettings.WebhookAzureDevOpsUsername != "" && argocdSettings.WebhookAzureDevOpsPassword != "" { 91 username, password, ok := r.BasicAuth() 92 if !ok || username != argocdSettings.WebhookAzureDevOpsUsername || password != argocdSettings.WebhookAzureDevOpsPassword { 93 return errBasicAuthVerificationFailed 94 } 95 } 96 return nil 97 } 98 99 return &WebhookHandler{ 100 namespace: namespace, 101 github: githubHandler, 102 gitlab: gitlabHandler, 103 azuredevops: azuredevopsHandler, 104 azuredevopsAuthHandler: azuredevopsAuthHandler, 105 client: client, 106 generators: generators, 107 }, nil 108 } 109 110 func (h *WebhookHandler) HandleEvent(payload interface{}) { 111 gitGenInfo := getGitGeneratorInfo(payload) 112 prGenInfo := getPRGeneratorInfo(payload) 113 if gitGenInfo == nil && prGenInfo == nil { 114 return 115 } 116 117 appSetList := &v1alpha1.ApplicationSetList{} 118 err := h.client.List(context.Background(), appSetList, &client.ListOptions{}) 119 if err != nil { 120 log.Errorf("Failed to list applicationsets: %v", err) 121 return 122 } 123 124 for _, appSet := range appSetList.Items { 125 shouldRefresh := false 126 for _, gen := range appSet.Spec.Generators { 127 // check if the ApplicationSet uses any generator that is relevant to the payload 128 shouldRefresh = shouldRefreshGitGenerator(gen.Git, gitGenInfo) || 129 shouldRefreshPRGenerator(gen.PullRequest, prGenInfo) || 130 shouldRefreshPluginGenerator(gen.Plugin) || 131 h.shouldRefreshMatrixGenerator(gen.Matrix, &appSet, gitGenInfo, prGenInfo) || 132 h.shouldRefreshMergeGenerator(gen.Merge, &appSet, gitGenInfo, prGenInfo) 133 if shouldRefresh { 134 break 135 } 136 } 137 if shouldRefresh { 138 err := refreshApplicationSet(h.client, &appSet) 139 if err != nil { 140 log.Errorf("Failed to refresh ApplicationSet '%s' for controller reprocessing", appSet.Name) 141 continue 142 } 143 log.Infof("refresh ApplicationSet %v/%v from webhook", appSet.Namespace, appSet.Name) 144 } 145 } 146 } 147 148 func (h *WebhookHandler) Handler(w http.ResponseWriter, r *http.Request) { 149 var payload interface{} 150 var err error 151 152 switch { 153 case r.Header.Get("X-GitHub-Event") != "": 154 payload, err = h.github.Parse(r, github.PushEvent, github.PullRequestEvent, github.PingEvent) 155 case r.Header.Get("X-Gitlab-Event") != "": 156 payload, err = h.gitlab.Parse(r, gitlab.PushEvents, gitlab.TagEvents, gitlab.MergeRequestEvents) 157 case r.Header.Get("X-Vss-Activityid") != "": 158 if err = h.azuredevopsAuthHandler(r); err != nil { 159 if errors.Is(err, errBasicAuthVerificationFailed) { 160 log.WithField(common.SecurityField, common.SecurityHigh).Infof("Azure DevOps webhook basic auth verification failed") 161 } 162 } else { 163 payload, err = h.azuredevops.Parse(r, azuredevops.GitPushEventType, azuredevops.GitPullRequestCreatedEventType, azuredevops.GitPullRequestUpdatedEventType, azuredevops.GitPullRequestMergedEventType) 164 } 165 default: 166 log.Debug("Ignoring unknown webhook event") 167 http.Error(w, "Unknown webhook event", http.StatusBadRequest) 168 return 169 } 170 171 if err != nil { 172 log.Infof("Webhook processing failed: %s", err) 173 status := http.StatusBadRequest 174 if r.Method != http.MethodPost { 175 status = http.StatusMethodNotAllowed 176 } 177 http.Error(w, fmt.Sprintf("Webhook processing failed: %s", html.EscapeString(err.Error())), status) 178 return 179 } 180 181 h.HandleEvent(payload) 182 } 183 184 func parseRevision(ref string) string { 185 refParts := strings.SplitN(ref, "/", 3) 186 return refParts[len(refParts)-1] 187 } 188 189 func getGitGeneratorInfo(payload interface{}) *gitGeneratorInfo { 190 var ( 191 webURL string 192 revision string 193 touchedHead bool 194 ) 195 switch payload := payload.(type) { 196 case github.PushPayload: 197 webURL = payload.Repository.HTMLURL 198 revision = parseRevision(payload.Ref) 199 touchedHead = payload.Repository.DefaultBranch == revision 200 case gitlab.PushEventPayload: 201 webURL = payload.Project.WebURL 202 revision = parseRevision(payload.Ref) 203 touchedHead = payload.Project.DefaultBranch == revision 204 case azuredevops.GitPushEvent: 205 // See: https://learn.microsoft.com/en-us/azure/devops/service-hooks/events?view=azure-devops#git.push 206 webURL = payload.Resource.Repository.RemoteURL 207 revision = parseRevision(payload.Resource.RefUpdates[0].Name) 208 touchedHead = payload.Resource.RefUpdates[0].Name == payload.Resource.Repository.DefaultBranch 209 // unfortunately, Azure DevOps doesn't provide a list of changed files 210 default: 211 return nil 212 } 213 214 log.Infof("Received push event repo: %s, revision: %s, touchedHead: %v", webURL, revision, touchedHead) 215 urlObj, err := url.Parse(webURL) 216 if err != nil { 217 log.Errorf("Failed to parse repoURL '%s'", webURL) 218 return nil 219 } 220 regexpStr := `(?i)(http://|https://|\w+@|ssh://(\w+@)?)` + urlObj.Hostname() + "(:[0-9]+|)[:/]" + urlObj.Path[1:] + "(\\.git)?" 221 repoRegexp, err := regexp.Compile(regexpStr) 222 if err != nil { 223 log.Errorf("Failed to compile regexp for repoURL '%s'", webURL) 224 return nil 225 } 226 227 return &gitGeneratorInfo{ 228 RepoRegexp: repoRegexp, 229 TouchedHead: touchedHead, 230 Revision: revision, 231 } 232 } 233 234 func getPRGeneratorInfo(payload interface{}) *prGeneratorInfo { 235 var info prGeneratorInfo 236 switch payload := payload.(type) { 237 case github.PullRequestPayload: 238 if !isAllowedGithubPullRequestAction(payload.Action) { 239 return nil 240 } 241 242 apiURL := payload.Repository.URL 243 urlObj, err := url.Parse(apiURL) 244 if err != nil { 245 log.Errorf("Failed to parse repoURL '%s'", apiURL) 246 return nil 247 } 248 regexpStr := `(?i)(http://|https://|\w+@|ssh://(\w+@)?)` + urlObj.Hostname() + "(:[0-9]+|)[:/]" 249 apiRegexp, err := regexp.Compile(regexpStr) 250 if err != nil { 251 log.Errorf("Failed to compile regexp for repoURL '%s'", apiURL) 252 return nil 253 } 254 info.Github = &prGeneratorGithubInfo{ 255 Repo: payload.Repository.Name, 256 Owner: payload.Repository.Owner.Login, 257 APIRegexp: apiRegexp, 258 } 259 case gitlab.MergeRequestEventPayload: 260 if !isAllowedGitlabPullRequestAction(payload.ObjectAttributes.Action) { 261 return nil 262 } 263 264 apiURL := payload.Project.WebURL 265 urlObj, err := url.Parse(apiURL) 266 if err != nil { 267 log.Errorf("Failed to parse repoURL '%s'", apiURL) 268 return nil 269 } 270 271 info.Gitlab = &prGeneratorGitlabInfo{ 272 Project: strconv.FormatInt(payload.ObjectAttributes.TargetProjectID, 10), 273 APIHostname: urlObj.Hostname(), 274 } 275 case azuredevops.GitPullRequestEvent: 276 if !isAllowedAzureDevOpsPullRequestAction(string(payload.EventType)) { 277 return nil 278 } 279 280 repo := payload.Resource.Repository.Name 281 project := payload.Resource.Repository.Project.Name 282 283 info.Azuredevops = &prGeneratorAzuredevopsInfo{ 284 Repo: repo, 285 Project: project, 286 } 287 default: 288 return nil 289 } 290 291 return &info 292 } 293 294 // githubAllowedPullRequestActions is a list of github actions that allow refresh 295 var githubAllowedPullRequestActions = []string{ 296 "opened", 297 "closed", 298 "synchronize", 299 "labeled", 300 "reopened", 301 "unlabeled", 302 } 303 304 // gitlabAllowedPullRequestActions is a list of gitlab actions that allow refresh 305 // https://docs.gitlab.com/ee/user/project/integrations/webhook_events.html#merge-request-events 306 var gitlabAllowedPullRequestActions = []string{ 307 "open", 308 "close", 309 "reopen", 310 "update", 311 "merge", 312 } 313 314 // azuredevopsAllowedPullRequestActions is a list of Azure DevOps actions that allow refresh 315 var azuredevopsAllowedPullRequestActions = []string{ 316 "git.pullrequest.created", 317 "git.pullrequest.merged", 318 "git.pullrequest.updated", 319 } 320 321 func isAllowedGithubPullRequestAction(action string) bool { 322 for _, allow := range githubAllowedPullRequestActions { 323 if allow == action { 324 return true 325 } 326 } 327 return false 328 } 329 330 func isAllowedGitlabPullRequestAction(action string) bool { 331 for _, allow := range gitlabAllowedPullRequestActions { 332 if allow == action { 333 return true 334 } 335 } 336 return false 337 } 338 339 func isAllowedAzureDevOpsPullRequestAction(action string) bool { 340 for _, allow := range azuredevopsAllowedPullRequestActions { 341 if allow == action { 342 return true 343 } 344 } 345 return false 346 } 347 348 func shouldRefreshGitGenerator(gen *v1alpha1.GitGenerator, info *gitGeneratorInfo) bool { 349 if gen == nil || info == nil { 350 return false 351 } 352 353 if !gitGeneratorUsesURL(gen, info.Revision, info.RepoRegexp) { 354 return false 355 } 356 if !genRevisionHasChanged(gen, info.Revision, info.TouchedHead) { 357 return false 358 } 359 return true 360 } 361 362 func shouldRefreshPluginGenerator(gen *v1alpha1.PluginGenerator) bool { 363 return gen != nil 364 } 365 366 func genRevisionHasChanged(gen *v1alpha1.GitGenerator, revision string, touchedHead bool) bool { 367 targetRev := parseRevision(gen.Revision) 368 if targetRev == "HEAD" || targetRev == "" { // revision is head 369 return touchedHead 370 } 371 372 return targetRev == revision 373 } 374 375 func gitGeneratorUsesURL(gen *v1alpha1.GitGenerator, webURL string, repoRegexp *regexp.Regexp) bool { 376 if !repoRegexp.MatchString(gen.RepoURL) { 377 log.Debugf("%s does not match %s", gen.RepoURL, repoRegexp.String()) 378 return false 379 } 380 381 log.Debugf("%s uses repoURL %s", gen.RepoURL, webURL) 382 return true 383 } 384 385 func shouldRefreshPRGenerator(gen *v1alpha1.PullRequestGenerator, info *prGeneratorInfo) bool { 386 if gen == nil || info == nil { 387 return false 388 } 389 390 if gen.GitLab != nil && info.Gitlab != nil { 391 if gen.GitLab.Project != info.Gitlab.Project { 392 return false 393 } 394 395 api := gen.GitLab.API 396 if api == "" { 397 api = "https://gitlab.com/" 398 } 399 400 urlObj, err := url.Parse(api) 401 if err != nil { 402 log.Errorf("Failed to parse repoURL '%s'", api) 403 return false 404 } 405 406 if urlObj.Hostname() != info.Gitlab.APIHostname { 407 log.Debugf("%s does not match %s", api, info.Gitlab.APIHostname) 408 return false 409 } 410 411 return true 412 } 413 414 if gen.Github != nil && info.Github != nil { 415 // repository owner and name are case-insensitive 416 // See https://docs.github.com/en/rest/pulls/pulls?apiVersion=2022-11-28#list-pull-requests 417 if !strings.EqualFold(gen.Github.Owner, info.Github.Owner) { 418 return false 419 } 420 if !strings.EqualFold(gen.Github.Repo, info.Github.Repo) { 421 return false 422 } 423 api := gen.Github.API 424 if api == "" { 425 api = "https://api.github.com/" 426 } 427 if !info.Github.APIRegexp.MatchString(api) { 428 log.Debugf("%s does not match %s", api, info.Github.APIRegexp.String()) 429 return false 430 } 431 432 return true 433 } 434 435 if gen.AzureDevOps != nil && info.Azuredevops != nil { 436 if gen.AzureDevOps.Project != info.Azuredevops.Project { 437 return false 438 } 439 if gen.AzureDevOps.Repo != info.Azuredevops.Repo { 440 return false 441 } 442 return true 443 } 444 445 return false 446 } 447 448 func (h *WebhookHandler) shouldRefreshMatrixGenerator(gen *v1alpha1.MatrixGenerator, appSet *v1alpha1.ApplicationSet, gitGenInfo *gitGeneratorInfo, prGenInfo *prGeneratorInfo) bool { 449 if gen == nil { 450 return false 451 } 452 453 // Silently ignore, the ApplicationSetReconciler will log the error as part of the reconcile 454 if len(gen.Generators) < 2 || len(gen.Generators) > 2 { 455 return false 456 } 457 458 g0 := gen.Generators[0] 459 460 // Check first child generator for Git or Pull Request Generator 461 if shouldRefreshGitGenerator(g0.Git, gitGenInfo) || 462 shouldRefreshPRGenerator(g0.PullRequest, prGenInfo) { 463 return true 464 } 465 466 // Check first child generator for nested Matrix generator 467 var matrixGenerator0 *v1alpha1.MatrixGenerator 468 if g0.Matrix != nil { 469 // Since nested matrix generator is represented as a JSON object in the CRD, we unmarshall it back to a Go struct here. 470 nestedMatrix, err := v1alpha1.ToNestedMatrixGenerator(g0.Matrix) 471 if err != nil { 472 log.Errorf("Failed to unmarshall nested matrix generator: %v", err) 473 return false 474 } 475 if nestedMatrix != nil { 476 matrixGenerator0 = nestedMatrix.ToMatrixGenerator() 477 if h.shouldRefreshMatrixGenerator(matrixGenerator0, appSet, gitGenInfo, prGenInfo) { 478 return true 479 } 480 } 481 } 482 483 // Check first child generator for nested Merge generator 484 var mergeGenerator0 *v1alpha1.MergeGenerator 485 if g0.Merge != nil { 486 // Since nested merge generator is represented as a JSON object in the CRD, we unmarshall it back to a Go struct here. 487 nestedMerge, err := v1alpha1.ToNestedMergeGenerator(g0.Merge) 488 if err != nil { 489 log.Errorf("Failed to unmarshall nested merge generator: %v", err) 490 return false 491 } 492 if nestedMerge != nil { 493 mergeGenerator0 = nestedMerge.ToMergeGenerator() 494 if h.shouldRefreshMergeGenerator(mergeGenerator0, appSet, gitGenInfo, prGenInfo) { 495 return true 496 } 497 } 498 } 499 500 // Create ApplicationSetGenerator for first child generator from its ApplicationSetNestedGenerator 501 requestedGenerator0 := &v1alpha1.ApplicationSetGenerator{ 502 List: g0.List, 503 Clusters: g0.Clusters, 504 Git: g0.Git, 505 SCMProvider: g0.SCMProvider, 506 ClusterDecisionResource: g0.ClusterDecisionResource, 507 PullRequest: g0.PullRequest, 508 Plugin: g0.Plugin, 509 Matrix: matrixGenerator0, 510 Merge: mergeGenerator0, 511 } 512 513 // Generate params for first child generator 514 relGenerators := generators.GetRelevantGenerators(requestedGenerator0, h.generators) 515 params := []map[string]interface{}{} 516 for _, g := range relGenerators { 517 p, err := g.GenerateParams(requestedGenerator0, appSet) 518 if err != nil { 519 log.Error(err) 520 return false 521 } 522 params = append(params, p...) 523 } 524 525 g1 := gen.Generators[1] 526 527 // Create Matrix generator for nested Matrix generator as second child generator 528 var matrixGenerator1 *v1alpha1.MatrixGenerator 529 if g1.Matrix != nil { 530 // Since nested matrix generator is represented as a JSON object in the CRD, we unmarshall it back to a Go struct here. 531 nestedMatrix, err := v1alpha1.ToNestedMatrixGenerator(g1.Matrix) 532 if err != nil { 533 log.Errorf("Failed to unmarshall nested matrix generator: %v", err) 534 return false 535 } 536 if nestedMatrix != nil { 537 matrixGenerator1 = nestedMatrix.ToMatrixGenerator() 538 } 539 } 540 541 // Create Merge generator for nested Merge generator as second child generator 542 var mergeGenerator1 *v1alpha1.MergeGenerator 543 if g1.Merge != nil { 544 // Since nested merge generator is represented as a JSON object in the CRD, we unmarshall it back to a Go struct here. 545 nestedMerge, err := v1alpha1.ToNestedMergeGenerator(g1.Merge) 546 if err != nil { 547 log.Errorf("Failed to unmarshall nested merge generator: %v", err) 548 return false 549 } 550 if nestedMerge != nil { 551 mergeGenerator1 = nestedMerge.ToMergeGenerator() 552 } 553 } 554 555 // Create ApplicationSetGenerator for second child generator from its ApplicationSetNestedGenerator 556 requestedGenerator1 := &v1alpha1.ApplicationSetGenerator{ 557 List: g1.List, 558 Clusters: g1.Clusters, 559 Git: g1.Git, 560 SCMProvider: g1.SCMProvider, 561 ClusterDecisionResource: g1.ClusterDecisionResource, 562 PullRequest: g1.PullRequest, 563 Plugin: g1.Plugin, 564 Matrix: matrixGenerator1, 565 Merge: mergeGenerator1, 566 } 567 568 // Interpolate second child generator with params from first child generator, if there are any params 569 if len(params) != 0 { 570 for _, p := range params { 571 tempInterpolatedGenerator, err := generators.InterpolateGenerator(requestedGenerator1, p, appSet.Spec.GoTemplate, appSet.Spec.GoTemplateOptions) 572 interpolatedGenerator := &tempInterpolatedGenerator 573 if err != nil { 574 log.Error(err) 575 return false 576 } 577 578 // Check all interpolated child generators 579 if shouldRefreshGitGenerator(interpolatedGenerator.Git, gitGenInfo) || 580 shouldRefreshPRGenerator(interpolatedGenerator.PullRequest, prGenInfo) || 581 shouldRefreshPluginGenerator(interpolatedGenerator.Plugin) || 582 h.shouldRefreshMatrixGenerator(interpolatedGenerator.Matrix, appSet, gitGenInfo, prGenInfo) || 583 h.shouldRefreshMergeGenerator(requestedGenerator1.Merge, appSet, gitGenInfo, prGenInfo) { 584 return true 585 } 586 } 587 } 588 589 // First child generator didn't return any params, just check the second child generator 590 return shouldRefreshGitGenerator(requestedGenerator1.Git, gitGenInfo) || 591 shouldRefreshPRGenerator(requestedGenerator1.PullRequest, prGenInfo) || 592 shouldRefreshPluginGenerator(requestedGenerator1.Plugin) || 593 h.shouldRefreshMatrixGenerator(requestedGenerator1.Matrix, appSet, gitGenInfo, prGenInfo) || 594 h.shouldRefreshMergeGenerator(requestedGenerator1.Merge, appSet, gitGenInfo, prGenInfo) 595 } 596 597 func (h *WebhookHandler) shouldRefreshMergeGenerator(gen *v1alpha1.MergeGenerator, appSet *v1alpha1.ApplicationSet, gitGenInfo *gitGeneratorInfo, prGenInfo *prGeneratorInfo) bool { 598 if gen == nil { 599 return false 600 } 601 602 for _, g := range gen.Generators { 603 // Check Git or Pull Request generator 604 if shouldRefreshGitGenerator(g.Git, gitGenInfo) || 605 shouldRefreshPRGenerator(g.PullRequest, prGenInfo) { 606 return true 607 } 608 609 // Check nested Matrix generator 610 if g.Matrix != nil { 611 // Since nested matrix generator is represented as a JSON object in the CRD, we unmarshall it back to a Go struct here. 612 nestedMatrix, err := v1alpha1.ToNestedMatrixGenerator(g.Matrix) 613 if err != nil { 614 log.Errorf("Failed to unmarshall nested matrix generator: %v", err) 615 return false 616 } 617 if nestedMatrix != nil { 618 if h.shouldRefreshMatrixGenerator(nestedMatrix.ToMatrixGenerator(), appSet, gitGenInfo, prGenInfo) { 619 return true 620 } 621 } 622 } 623 624 // Check nested Merge generator 625 if g.Merge != nil { 626 // Since nested merge generator is represented as a JSON object in the CRD, we unmarshall it back to a Go struct here. 627 nestedMerge, err := v1alpha1.ToNestedMergeGenerator(g.Merge) 628 if err != nil { 629 log.Errorf("Failed to unmarshall nested merge generator: %v", err) 630 return false 631 } 632 if nestedMerge != nil { 633 if h.shouldRefreshMergeGenerator(nestedMerge.ToMergeGenerator(), appSet, gitGenInfo, prGenInfo) { 634 return true 635 } 636 } 637 } 638 } 639 640 return false 641 } 642 643 func refreshApplicationSet(c client.Client, appSet *v1alpha1.ApplicationSet) error { 644 // patch the ApplicationSet with the refresh annotation to reconcile 645 return retry.RetryOnConflict(retry.DefaultBackoff, func() error { 646 err := c.Get(context.Background(), types.NamespacedName{Name: appSet.Name, Namespace: appSet.Namespace}, appSet) 647 if err != nil { 648 return fmt.Errorf("error getting ApplicationSet: %w", err) 649 } 650 if appSet.Annotations == nil { 651 appSet.Annotations = map[string]string{} 652 } 653 appSet.Annotations[common.AnnotationApplicationSetRefresh] = "true" 654 return c.Patch(context.Background(), appSet, client.Merge) 655 }) 656 }