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

     1  package pull_request
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"strings"
     7  
     8  	"github.com/microsoft/azure-devops-go-api/azuredevops/v7"
     9  	"github.com/microsoft/azure-devops-go-api/azuredevops/v7/core"
    10  	"github.com/microsoft/azure-devops-go-api/azuredevops/v7/git"
    11  )
    12  
    13  const (
    14  	AZURE_DEVOPS_DEFAULT_URL             = "https://dev.azure.com"
    15  	AZURE_DEVOPS_PROJECT_NOT_FOUND_ERROR = "The following project does not exist"
    16  )
    17  
    18  type AzureDevOpsClientFactory interface {
    19  	// Returns an Azure Devops Client interface.
    20  	GetClient(ctx context.Context) (git.Client, error)
    21  }
    22  
    23  type devopsFactoryImpl struct {
    24  	connection *azuredevops.Connection
    25  }
    26  
    27  func (factory *devopsFactoryImpl) GetClient(ctx context.Context) (git.Client, error) {
    28  	gitClient, err := git.NewClient(ctx, factory.connection)
    29  	if err != nil {
    30  		return nil, fmt.Errorf("failed to get new Azure DevOps git client for pull request generator: %w", err)
    31  	}
    32  	return gitClient, nil
    33  }
    34  
    35  type AzureDevOpsService struct {
    36  	clientFactory AzureDevOpsClientFactory
    37  	project       string
    38  	repo          string
    39  	labels        []string
    40  }
    41  
    42  var (
    43  	_ PullRequestService       = (*AzureDevOpsService)(nil)
    44  	_ AzureDevOpsClientFactory = &devopsFactoryImpl{}
    45  )
    46  
    47  func NewAzureDevOpsService(token, url, organization, project, repo string, labels []string) (PullRequestService, error) {
    48  	organizationURL := buildURL(url, organization)
    49  
    50  	var connection *azuredevops.Connection
    51  	if token == "" {
    52  		connection = azuredevops.NewAnonymousConnection(organizationURL)
    53  	} else {
    54  		connection = azuredevops.NewPatConnection(organizationURL, token)
    55  	}
    56  
    57  	return &AzureDevOpsService{
    58  		clientFactory: &devopsFactoryImpl{connection: connection},
    59  		project:       project,
    60  		repo:          repo,
    61  		labels:        labels,
    62  	}, nil
    63  }
    64  
    65  func (a *AzureDevOpsService) List(ctx context.Context) ([]*PullRequest, error) {
    66  	client, err := a.clientFactory.GetClient(ctx)
    67  	if err != nil {
    68  		return nil, fmt.Errorf("failed to get Azure DevOps client: %w", err)
    69  	}
    70  
    71  	args := git.GetPullRequestsByProjectArgs{
    72  		Project:        &a.project,
    73  		SearchCriteria: &git.GitPullRequestSearchCriteria{},
    74  	}
    75  
    76  	pullRequests := []*PullRequest{}
    77  
    78  	azurePullRequests, err := client.GetPullRequestsByProject(ctx, args)
    79  	if err != nil {
    80  		// A standard Http 404 error is not returned for Azure DevOps,
    81  		// so checking the error message for a specific pattern.
    82  		// NOTE: Since the repos are filtered later, only existence of the project
    83  		// is relevant for AzureDevOps
    84  		if strings.Contains(err.Error(), AZURE_DEVOPS_PROJECT_NOT_FOUND_ERROR) {
    85  			// return a custom error indicating that the repository is not found,
    86  			// but also return the empty result since the decision to continue or not in this case is made by the caller
    87  			return pullRequests, NewRepositoryNotFoundError(err)
    88  		}
    89  		return nil, fmt.Errorf("failed to get pull requests by project: %w", err)
    90  	}
    91  
    92  	for _, pr := range *azurePullRequests {
    93  		if pr.Repository == nil ||
    94  			pr.Repository.Name == nil ||
    95  			pr.PullRequestId == nil ||
    96  			pr.SourceRefName == nil ||
    97  			pr.TargetRefName == nil ||
    98  			pr.LastMergeSourceCommit == nil ||
    99  			pr.LastMergeSourceCommit.CommitId == nil {
   100  			continue
   101  		}
   102  
   103  		azureDevOpsLabels := convertLabels(pr.Labels)
   104  		if !containAzureDevOpsLabels(a.labels, azureDevOpsLabels) {
   105  			continue
   106  		}
   107  
   108  		if *pr.Repository.Name == a.repo {
   109  			pullRequests = append(pullRequests, &PullRequest{
   110  				Number:       *pr.PullRequestId,
   111  				Title:        *pr.Title,
   112  				Branch:       strings.Replace(*pr.SourceRefName, "refs/heads/", "", 1),
   113  				TargetBranch: strings.Replace(*pr.TargetRefName, "refs/heads/", "", 1),
   114  				HeadSHA:      *pr.LastMergeSourceCommit.CommitId,
   115  				Labels:       azureDevOpsLabels,
   116  				Author:       strings.Split(*pr.CreatedBy.UniqueName, "@")[0], // Get the part before the @ in the email-address
   117  			})
   118  		}
   119  	}
   120  
   121  	return pullRequests, nil
   122  }
   123  
   124  // convertLabels converts WebApiTagDefinitions to strings
   125  func convertLabels(tags *[]core.WebApiTagDefinition) []string {
   126  	if tags == nil {
   127  		return []string{}
   128  	}
   129  	labelStrings := make([]string, len(*tags))
   130  	for i, label := range *tags {
   131  		labelStrings[i] = *label.Name
   132  	}
   133  	return labelStrings
   134  }
   135  
   136  // containAzureDevOpsLabels returns true if gotLabels contains expectedLabels
   137  func containAzureDevOpsLabels(expectedLabels []string, gotLabels []string) bool {
   138  	for _, expected := range expectedLabels {
   139  		found := false
   140  		for _, got := range gotLabels {
   141  			if expected == got {
   142  				found = true
   143  				break
   144  			}
   145  		}
   146  		if !found {
   147  			return false
   148  		}
   149  	}
   150  	return true
   151  }
   152  
   153  func buildURL(url, organization string) string {
   154  	if url == "" {
   155  		url = AZURE_DEVOPS_DEFAULT_URL
   156  	}
   157  	separator := ""
   158  	if !strings.HasSuffix(url, "/") {
   159  		separator = "/"
   160  	}
   161  	devOpsURL := fmt.Sprintf("%s%s%s", url, separator, organization)
   162  	return devOpsURL
   163  }