github.com/argoproj/argo-cd/v3@v3.2.1/applicationset/webhook/webhook.go (about)

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