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

     1  package generators
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"strconv"
     7  	"time"
     8  
     9  	corev1 "k8s.io/api/core/v1"
    10  	"sigs.k8s.io/controller-runtime/pkg/client"
    11  
    12  	"github.com/gosimple/slug"
    13  
    14  	pullrequest "github.com/argoproj/argo-cd/v2/applicationset/services/pull_request"
    15  	argoprojiov1alpha1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
    16  )
    17  
    18  var _ Generator = (*PullRequestGenerator)(nil)
    19  
    20  const (
    21  	DefaultPullRequestRequeueAfterSeconds = 30 * time.Minute
    22  )
    23  
    24  type PullRequestGenerator struct {
    25  	client                    client.Client
    26  	selectServiceProviderFunc func(context.Context, *argoprojiov1alpha1.PullRequestGenerator, *argoprojiov1alpha1.ApplicationSet) (pullrequest.PullRequestService, error)
    27  	auth                      SCMAuthProviders
    28  	scmRootCAPath             string
    29  	allowedSCMProviders       []string
    30  	enableSCMProviders        bool
    31  }
    32  
    33  func NewPullRequestGenerator(client client.Client, auth SCMAuthProviders, scmRootCAPath string, allowedScmProviders []string, enableSCMProviders bool) Generator {
    34  	g := &PullRequestGenerator{
    35  		client:              client,
    36  		auth:                auth,
    37  		scmRootCAPath:       scmRootCAPath,
    38  		allowedSCMProviders: allowedScmProviders,
    39  		enableSCMProviders:  enableSCMProviders,
    40  	}
    41  	g.selectServiceProviderFunc = g.selectServiceProvider
    42  	return g
    43  }
    44  
    45  func (g *PullRequestGenerator) GetRequeueAfter(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator) time.Duration {
    46  	// Return a requeue default of 30 minutes, if no default is specified.
    47  
    48  	if appSetGenerator.PullRequest.RequeueAfterSeconds != nil {
    49  		return time.Duration(*appSetGenerator.PullRequest.RequeueAfterSeconds) * time.Second
    50  	}
    51  
    52  	return DefaultPullRequestRequeueAfterSeconds
    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) ([]map[string]interface{}, error) {
    60  	if appSetGenerator == nil {
    61  		return nil, EmptyAppSetGeneratorError
    62  	}
    63  
    64  	if appSetGenerator.PullRequest == nil {
    65  		return nil, EmptyAppSetGeneratorError
    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  	if err != nil {
    76  		return nil, fmt.Errorf("error listing repos: %v", err)
    77  	}
    78  	params := make([]map[string]interface{}, 0, len(pulls))
    79  
    80  	// In order to follow the DNS label standard as defined in RFC 1123,
    81  	// we need to limit the 'branch' to 50 to give room to append/suffix-ing it
    82  	// with 13 more characters. Also, there is the need to clean it as recommended
    83  	// here https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#dns-label-names
    84  	slug.MaxLength = 50
    85  
    86  	// Converting underscores to dashes
    87  	slug.CustomSub = map[string]string{
    88  		"_": "-",
    89  	}
    90  
    91  	var shortSHALength int
    92  	var shortSHALength7 int
    93  	for _, pull := range pulls {
    94  		shortSHALength = 8
    95  		if len(pull.HeadSHA) < 8 {
    96  			shortSHALength = len(pull.HeadSHA)
    97  		}
    98  
    99  		shortSHALength7 = 7
   100  		if len(pull.HeadSHA) < 7 {
   101  			shortSHALength7 = len(pull.HeadSHA)
   102  		}
   103  
   104  		paramMap := map[string]interface{}{
   105  			"number":             strconv.Itoa(pull.Number),
   106  			"branch":             pull.Branch,
   107  			"branch_slug":        slug.Make(pull.Branch),
   108  			"target_branch":      pull.TargetBranch,
   109  			"target_branch_slug": slug.Make(pull.TargetBranch),
   110  			"head_sha":           pull.HeadSHA,
   111  			"head_short_sha":     pull.HeadSHA[:shortSHALength],
   112  			"head_short_sha_7":   pull.HeadSHA[:shortSHALength7],
   113  		}
   114  
   115  		// PR lables will only be supported for Go Template appsets, since fasttemplate will be deprecated.
   116  		if applicationSetInfo != nil && applicationSetInfo.Spec.GoTemplate {
   117  			paramMap["labels"] = pull.Labels
   118  		}
   119  		params = append(params, paramMap)
   120  	}
   121  	return params, nil
   122  }
   123  
   124  // selectServiceProvider selects the provider to get pull requests from the configuration
   125  func (g *PullRequestGenerator) selectServiceProvider(ctx context.Context, generatorConfig *argoprojiov1alpha1.PullRequestGenerator, applicationSetInfo *argoprojiov1alpha1.ApplicationSet) (pullrequest.PullRequestService, error) {
   126  	if !g.enableSCMProviders {
   127  		return nil, ErrSCMProvidersDisabled
   128  	}
   129  	if err := ScmProviderAllowed(applicationSetInfo, generatorConfig, g.allowedSCMProviders); err != nil {
   130  		return nil, fmt.Errorf("scm provider not allowed: %w", err)
   131  	}
   132  
   133  	if generatorConfig.Github != nil {
   134  		return g.github(ctx, generatorConfig.Github, applicationSetInfo)
   135  	}
   136  	if generatorConfig.GitLab != nil {
   137  		providerConfig := generatorConfig.GitLab
   138  		token, err := g.getSecretRef(ctx, providerConfig.TokenRef, applicationSetInfo.Namespace)
   139  		if err != nil {
   140  			return nil, fmt.Errorf("error fetching Secret token: %v", err)
   141  		}
   142  		return pullrequest.NewGitLabService(ctx, token, providerConfig.API, providerConfig.Project, providerConfig.Labels, providerConfig.PullRequestState, g.scmRootCAPath, providerConfig.Insecure)
   143  	}
   144  	if generatorConfig.Gitea != nil {
   145  		providerConfig := generatorConfig.Gitea
   146  		token, err := g.getSecretRef(ctx, providerConfig.TokenRef, applicationSetInfo.Namespace)
   147  		if err != nil {
   148  			return nil, fmt.Errorf("error fetching Secret token: %v", err)
   149  		}
   150  		return pullrequest.NewGiteaService(ctx, token, providerConfig.API, providerConfig.Owner, providerConfig.Repo, providerConfig.Insecure)
   151  	}
   152  	if generatorConfig.BitbucketServer != nil {
   153  		providerConfig := generatorConfig.BitbucketServer
   154  		if providerConfig.BasicAuth != nil {
   155  			password, err := g.getSecretRef(ctx, providerConfig.BasicAuth.PasswordRef, applicationSetInfo.Namespace)
   156  			if err != nil {
   157  				return nil, fmt.Errorf("error fetching Secret token: %v", err)
   158  			}
   159  			return pullrequest.NewBitbucketServiceBasicAuth(ctx, providerConfig.BasicAuth.Username, password, providerConfig.API, providerConfig.Project, providerConfig.Repo)
   160  		} else {
   161  			return pullrequest.NewBitbucketServiceNoAuth(ctx, providerConfig.API, providerConfig.Project, providerConfig.Repo)
   162  		}
   163  	}
   164  	if generatorConfig.Bitbucket != nil {
   165  		providerConfig := generatorConfig.Bitbucket
   166  		if providerConfig.BearerToken != nil {
   167  			appToken, err := g.getSecretRef(ctx, providerConfig.BearerToken.TokenRef, applicationSetInfo.Namespace)
   168  			if err != nil {
   169  				return nil, fmt.Errorf("error fetching Secret Bearer token: %v", err)
   170  			}
   171  			return pullrequest.NewBitbucketCloudServiceBearerToken(providerConfig.API, appToken, providerConfig.Owner, providerConfig.Repo)
   172  		} else if providerConfig.BasicAuth != nil {
   173  			password, err := g.getSecretRef(ctx, providerConfig.BasicAuth.PasswordRef, applicationSetInfo.Namespace)
   174  			if err != nil {
   175  				return nil, fmt.Errorf("error fetching Secret token: %v", err)
   176  			}
   177  			return pullrequest.NewBitbucketCloudServiceBasicAuth(providerConfig.API, providerConfig.BasicAuth.Username, password, providerConfig.Owner, providerConfig.Repo)
   178  		} else {
   179  			return pullrequest.NewBitbucketCloudServiceNoAuth(providerConfig.API, providerConfig.Owner, providerConfig.Repo)
   180  		}
   181  	}
   182  	if generatorConfig.AzureDevOps != nil {
   183  		providerConfig := generatorConfig.AzureDevOps
   184  		token, err := g.getSecretRef(ctx, providerConfig.TokenRef, applicationSetInfo.Namespace)
   185  		if err != nil {
   186  			return nil, fmt.Errorf("error fetching Secret token: %v", err)
   187  		}
   188  		return pullrequest.NewAzureDevOpsService(ctx, token, providerConfig.API, providerConfig.Organization, providerConfig.Project, providerConfig.Repo, providerConfig.Labels)
   189  	}
   190  	return nil, fmt.Errorf("no Pull Request provider implementation configured")
   191  }
   192  
   193  func (g *PullRequestGenerator) github(ctx context.Context, cfg *argoprojiov1alpha1.PullRequestGeneratorGithub, applicationSetInfo *argoprojiov1alpha1.ApplicationSet) (pullrequest.PullRequestService, error) {
   194  	// use an app if it was configured
   195  	if cfg.AppSecretName != "" {
   196  		auth, err := g.auth.GitHubApps.GetAuthSecret(ctx, cfg.AppSecretName)
   197  		if err != nil {
   198  			return nil, fmt.Errorf("error getting GitHub App secret: %v", err)
   199  		}
   200  		return pullrequest.NewGithubAppService(*auth, cfg.API, cfg.Owner, cfg.Repo, cfg.Labels)
   201  	}
   202  
   203  	// always default to token, even if not set (public access)
   204  	token, err := g.getSecretRef(ctx, cfg.TokenRef, applicationSetInfo.Namespace)
   205  	if err != nil {
   206  		return nil, fmt.Errorf("error fetching Secret token: %v", err)
   207  	}
   208  	return pullrequest.NewGithubService(ctx, token, cfg.API, cfg.Owner, cfg.Repo, cfg.Labels)
   209  }
   210  
   211  // getSecretRef gets the value of the key for the specified Secret resource.
   212  func (g *PullRequestGenerator) getSecretRef(ctx context.Context, ref *argoprojiov1alpha1.SecretRef, namespace string) (string, error) {
   213  	if ref == nil {
   214  		return "", nil
   215  	}
   216  
   217  	secret := &corev1.Secret{}
   218  	err := g.client.Get(
   219  		ctx,
   220  		client.ObjectKey{
   221  			Name:      ref.SecretName,
   222  			Namespace: namespace,
   223  		},
   224  		secret)
   225  	if err != nil {
   226  		return "", fmt.Errorf("error fetching secret %s/%s: %v", namespace, ref.SecretName, err)
   227  	}
   228  	tokenBytes, ok := secret.Data[ref.Key]
   229  	if !ok {
   230  		return "", fmt.Errorf("key %q in secret %s/%s not found", ref.Key, namespace, ref.SecretName)
   231  	}
   232  	return string(tokenBytes), nil
   233  }