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  }