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

     1  package generators
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"net/http"
     8  	"strconv"
     9  	"time"
    10  
    11  	"sigs.k8s.io/controller-runtime/pkg/client"
    12  
    13  	"github.com/gosimple/slug"
    14  	log "github.com/sirupsen/logrus"
    15  
    16  	"github.com/argoproj/argo-cd/v3/applicationset/services"
    17  	pullrequest "github.com/argoproj/argo-cd/v3/applicationset/services/pull_request"
    18  	"github.com/argoproj/argo-cd/v3/applicationset/utils"
    19  	argoprojiov1alpha1 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
    20  )
    21  
    22  const (
    23  	DefaultPullRequestRequeueAfter = 30 * time.Minute
    24  )
    25  
    26  type PullRequestGenerator struct {
    27  	client                    client.Client
    28  	selectServiceProviderFunc func(context.Context, *argoprojiov1alpha1.PullRequestGenerator, *argoprojiov1alpha1.ApplicationSet) (pullrequest.PullRequestService, error)
    29  	SCMConfig
    30  }
    31  
    32  func NewPullRequestGenerator(client client.Client, scmConfig SCMConfig) Generator {
    33  	g := &PullRequestGenerator{
    34  		client:    client,
    35  		SCMConfig: scmConfig,
    36  	}
    37  	g.selectServiceProviderFunc = g.selectServiceProvider
    38  	return g
    39  }
    40  
    41  func (g *PullRequestGenerator) GetRequeueAfter(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator) time.Duration {
    42  	// Return a requeue default of 30 minutes, if no default is specified.
    43  
    44  	if appSetGenerator.PullRequest.RequeueAfterSeconds != nil {
    45  		return time.Duration(*appSetGenerator.PullRequest.RequeueAfterSeconds) * time.Second
    46  	}
    47  
    48  	return DefaultPullRequestRequeueAfter
    49  }
    50  
    51  func (g *PullRequestGenerator) GetContinueOnRepoNotFoundError(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator) bool {
    52  	return appSetGenerator.PullRequest.ContinueOnRepoNotFoundError
    53  }
    54  
    55  func (g *PullRequestGenerator) GetTemplate(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator) *argoprojiov1alpha1.ApplicationSetTemplate {
    56  	return &appSetGenerator.PullRequest.Template
    57  }
    58  
    59  func (g *PullRequestGenerator) GenerateParams(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator, applicationSetInfo *argoprojiov1alpha1.ApplicationSet, _ client.Client) ([]map[string]any, error) {
    60  	if appSetGenerator == nil {
    61  		return nil, ErrEmptyAppSetGenerator
    62  	}
    63  
    64  	if appSetGenerator.PullRequest == nil {
    65  		return nil, ErrEmptyAppSetGenerator
    66  	}
    67  
    68  	ctx := context.Background()
    69  	svc, err := g.selectServiceProviderFunc(ctx, appSetGenerator.PullRequest, applicationSetInfo)
    70  	if err != nil {
    71  		return nil, fmt.Errorf("failed to select pull request service provider: %w", err)
    72  	}
    73  
    74  	pulls, err := pullrequest.ListPullRequests(ctx, svc, appSetGenerator.PullRequest.Filters)
    75  	params := make([]map[string]any, 0, len(pulls))
    76  	if err != nil {
    77  		if pullrequest.IsRepositoryNotFoundError(err) && g.GetContinueOnRepoNotFoundError(appSetGenerator) {
    78  			log.WithError(err).WithField("generator", g).
    79  				Warn("Skipping params generation for this repository since it was not found.")
    80  			return params, nil
    81  		}
    82  		return nil, fmt.Errorf("error listing repos: %w", err)
    83  	}
    84  
    85  	// In order to follow the DNS label standard as defined in RFC 1123,
    86  	// we need to limit the 'branch' to 50 to give room to append/suffix-ing it
    87  	// with 13 more characters. Also, there is the need to clean it as recommended
    88  	// here https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#dns-label-names
    89  	slug.MaxLength = 50
    90  
    91  	// Converting underscores to dashes
    92  	slug.CustomSub = map[string]string{
    93  		"_": "-",
    94  	}
    95  
    96  	var shortSHALength int
    97  	var shortSHALength7 int
    98  	for _, pull := range pulls {
    99  		shortSHALength = 8
   100  		if len(pull.HeadSHA) < 8 {
   101  			shortSHALength = len(pull.HeadSHA)
   102  		}
   103  
   104  		shortSHALength7 = 7
   105  		if len(pull.HeadSHA) < 7 {
   106  			shortSHALength7 = len(pull.HeadSHA)
   107  		}
   108  
   109  		paramMap := map[string]any{
   110  			"number":             strconv.Itoa(pull.Number),
   111  			"title":              pull.Title,
   112  			"branch":             pull.Branch,
   113  			"branch_slug":        slug.Make(pull.Branch),
   114  			"target_branch":      pull.TargetBranch,
   115  			"target_branch_slug": slug.Make(pull.TargetBranch),
   116  			"head_sha":           pull.HeadSHA,
   117  			"head_short_sha":     pull.HeadSHA[:shortSHALength],
   118  			"head_short_sha_7":   pull.HeadSHA[:shortSHALength7],
   119  			"author":             pull.Author,
   120  		}
   121  
   122  		// PR lables will only be supported for Go Template appsets, since fasttemplate will be deprecated.
   123  		if applicationSetInfo != nil && applicationSetInfo.Spec.GoTemplate {
   124  			paramMap["labels"] = pull.Labels
   125  		}
   126  
   127  		err := appendTemplatedValues(appSetGenerator.PullRequest.Values, paramMap, applicationSetInfo.Spec.GoTemplate, applicationSetInfo.Spec.GoTemplateOptions)
   128  		if err != nil {
   129  			return nil, fmt.Errorf("failed to append templated values: %w", err)
   130  		}
   131  		params = append(params, paramMap)
   132  	}
   133  	return params, nil
   134  }
   135  
   136  // selectServiceProvider selects the provider to get pull requests from the configuration
   137  func (g *PullRequestGenerator) selectServiceProvider(ctx context.Context, generatorConfig *argoprojiov1alpha1.PullRequestGenerator, applicationSetInfo *argoprojiov1alpha1.ApplicationSet) (pullrequest.PullRequestService, error) {
   138  	if !g.enableSCMProviders {
   139  		return nil, ErrSCMProvidersDisabled
   140  	}
   141  	if err := ScmProviderAllowed(applicationSetInfo, generatorConfig, g.allowedSCMProviders); err != nil {
   142  		return nil, fmt.Errorf("scm provider not allowed: %w", err)
   143  	}
   144  
   145  	if generatorConfig.Github != nil {
   146  		return g.github(ctx, generatorConfig.Github, applicationSetInfo)
   147  	}
   148  	if generatorConfig.GitLab != nil {
   149  		providerConfig := generatorConfig.GitLab
   150  		var caCerts []byte
   151  		var prErr error
   152  		if providerConfig.CARef != nil {
   153  			caCerts, prErr = utils.GetConfigMapData(ctx, g.client, providerConfig.CARef, applicationSetInfo.Namespace)
   154  			if prErr != nil {
   155  				return nil, fmt.Errorf("error fetching CA certificates from ConfigMap: %w", prErr)
   156  			}
   157  		}
   158  		token, err := utils.GetSecretRef(ctx, g.client, providerConfig.TokenRef, applicationSetInfo.Namespace, g.tokenRefStrictMode)
   159  		if err != nil {
   160  			return nil, fmt.Errorf("error fetching Secret token: %w", err)
   161  		}
   162  		return pullrequest.NewGitLabService(token, providerConfig.API, providerConfig.Project, providerConfig.Labels, providerConfig.PullRequestState, g.scmRootCAPath, providerConfig.Insecure, caCerts)
   163  	}
   164  	if generatorConfig.Gitea != nil {
   165  		providerConfig := generatorConfig.Gitea
   166  		token, err := utils.GetSecretRef(ctx, g.client, providerConfig.TokenRef, applicationSetInfo.Namespace, g.tokenRefStrictMode)
   167  		if err != nil {
   168  			return nil, fmt.Errorf("error fetching Secret token: %w", err)
   169  		}
   170  
   171  		return pullrequest.NewGiteaService(token, providerConfig.API, providerConfig.Owner, providerConfig.Repo, providerConfig.Labels, providerConfig.Insecure)
   172  	}
   173  	if generatorConfig.BitbucketServer != nil {
   174  		providerConfig := generatorConfig.BitbucketServer
   175  		var caCerts []byte
   176  		var prErr error
   177  		if providerConfig.CARef != nil {
   178  			caCerts, prErr = utils.GetConfigMapData(ctx, g.client, providerConfig.CARef, applicationSetInfo.Namespace)
   179  			if prErr != nil {
   180  				return nil, fmt.Errorf("error fetching CA certificates from ConfigMap: %w", prErr)
   181  			}
   182  		}
   183  		if providerConfig.BearerToken != nil {
   184  			appToken, err := utils.GetSecretRef(ctx, g.client, providerConfig.BearerToken.TokenRef, applicationSetInfo.Namespace, g.tokenRefStrictMode)
   185  			if err != nil {
   186  				return nil, fmt.Errorf("error fetching Secret Bearer token: %w", err)
   187  			}
   188  			return pullrequest.NewBitbucketServiceBearerToken(ctx, appToken, providerConfig.API, providerConfig.Project, providerConfig.Repo, g.scmRootCAPath, providerConfig.Insecure, caCerts)
   189  		} else if providerConfig.BasicAuth != nil {
   190  			password, err := utils.GetSecretRef(ctx, g.client, providerConfig.BasicAuth.PasswordRef, applicationSetInfo.Namespace, g.tokenRefStrictMode)
   191  			if err != nil {
   192  				return nil, fmt.Errorf("error fetching Secret token: %w", err)
   193  			}
   194  			return pullrequest.NewBitbucketServiceBasicAuth(ctx, providerConfig.BasicAuth.Username, password, providerConfig.API, providerConfig.Project, providerConfig.Repo, g.scmRootCAPath, providerConfig.Insecure, caCerts)
   195  		}
   196  		return pullrequest.NewBitbucketServiceNoAuth(ctx, providerConfig.API, providerConfig.Project, providerConfig.Repo, g.scmRootCAPath, providerConfig.Insecure, caCerts)
   197  	}
   198  	if generatorConfig.Bitbucket != nil {
   199  		providerConfig := generatorConfig.Bitbucket
   200  		if providerConfig.BearerToken != nil {
   201  			appToken, err := utils.GetSecretRef(ctx, g.client, providerConfig.BearerToken.TokenRef, applicationSetInfo.Namespace, g.tokenRefStrictMode)
   202  			if err != nil {
   203  				return nil, fmt.Errorf("error fetching Secret Bearer token: %w", err)
   204  			}
   205  			return pullrequest.NewBitbucketCloudServiceBearerToken(providerConfig.API, appToken, providerConfig.Owner, providerConfig.Repo)
   206  		} else if providerConfig.BasicAuth != nil {
   207  			password, err := utils.GetSecretRef(ctx, g.client, providerConfig.BasicAuth.PasswordRef, applicationSetInfo.Namespace, g.tokenRefStrictMode)
   208  			if err != nil {
   209  				return nil, fmt.Errorf("error fetching Secret token: %w", err)
   210  			}
   211  			return pullrequest.NewBitbucketCloudServiceBasicAuth(providerConfig.API, providerConfig.BasicAuth.Username, password, providerConfig.Owner, providerConfig.Repo)
   212  		}
   213  		return pullrequest.NewBitbucketCloudServiceNoAuth(providerConfig.API, providerConfig.Owner, providerConfig.Repo)
   214  	}
   215  	if generatorConfig.AzureDevOps != nil {
   216  		providerConfig := generatorConfig.AzureDevOps
   217  		token, err := utils.GetSecretRef(ctx, g.client, providerConfig.TokenRef, applicationSetInfo.Namespace, g.tokenRefStrictMode)
   218  		if err != nil {
   219  			return nil, fmt.Errorf("error fetching Secret token: %w", err)
   220  		}
   221  		return pullrequest.NewAzureDevOpsService(token, providerConfig.API, providerConfig.Organization, providerConfig.Project, providerConfig.Repo, providerConfig.Labels)
   222  	}
   223  	return nil, errors.New("no Pull Request provider implementation configured")
   224  }
   225  
   226  func (g *PullRequestGenerator) github(ctx context.Context, cfg *argoprojiov1alpha1.PullRequestGeneratorGithub, applicationSetInfo *argoprojiov1alpha1.ApplicationSet) (pullrequest.PullRequestService, error) {
   227  	var metricsCtx *services.MetricsContext
   228  	var httpClient *http.Client
   229  
   230  	if g.enableGitHubAPIMetrics {
   231  		metricsCtx = &services.MetricsContext{
   232  			AppSetNamespace: applicationSetInfo.Namespace,
   233  			AppSetName:      applicationSetInfo.Name,
   234  		}
   235  		httpClient = services.NewGitHubMetricsClient(metricsCtx)
   236  	}
   237  
   238  	// use an app if it was configured
   239  	if cfg.AppSecretName != "" {
   240  		auth, err := g.GitHubApps.GetAuthSecret(ctx, cfg.AppSecretName)
   241  		if err != nil {
   242  			return nil, fmt.Errorf("error getting GitHub App secret: %w", err)
   243  		}
   244  
   245  		if g.enableGitHubAPIMetrics {
   246  			return pullrequest.NewGithubAppService(*auth, cfg.API, cfg.Owner, cfg.Repo, cfg.Labels, httpClient)
   247  		}
   248  		return pullrequest.NewGithubAppService(*auth, cfg.API, cfg.Owner, cfg.Repo, cfg.Labels)
   249  	}
   250  
   251  	// always default to token, even if not set (public access)
   252  	token, err := utils.GetSecretRef(ctx, g.client, cfg.TokenRef, applicationSetInfo.Namespace, g.tokenRefStrictMode)
   253  	if err != nil {
   254  		return nil, fmt.Errorf("error fetching Secret token: %w", err)
   255  	}
   256  
   257  	if g.enableGitHubAPIMetrics {
   258  		return pullrequest.NewGithubService(token, cfg.API, cfg.Owner, cfg.Repo, cfg.Labels, httpClient)
   259  	}
   260  	return pullrequest.NewGithubService(token, cfg.API, cfg.Owner, cfg.Repo, cfg.Labels)
   261  }