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 }