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

     1  package scm_provider
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	netUrl "net/url"
     8  	"strings"
     9  
    10  	"github.com/google/uuid"
    11  	"github.com/microsoft/azure-devops-go-api/azuredevops/v7"
    12  	azureGit "github.com/microsoft/azure-devops-go-api/azuredevops/v7/git"
    13  )
    14  
    15  const AZURE_DEVOPS_DEFAULT_URL = "https://dev.azure.com"
    16  
    17  type azureDevOpsErrorTypeKeyValuesType struct {
    18  	GitRepositoryNotFound string
    19  	GitItemNotFound       string
    20  }
    21  
    22  var AzureDevOpsErrorsTypeKeyValues = azureDevOpsErrorTypeKeyValuesType{
    23  	GitRepositoryNotFound: "GitRepositoryNotFoundException",
    24  	GitItemNotFound:       "GitItemNotFoundException",
    25  }
    26  
    27  type AzureDevOpsClientFactory interface {
    28  	// Returns an Azure Devops Client interface.
    29  	GetClient(ctx context.Context) (azureGit.Client, error)
    30  }
    31  
    32  type devopsFactoryImpl struct {
    33  	connection *azuredevops.Connection
    34  }
    35  
    36  func (factory *devopsFactoryImpl) GetClient(ctx context.Context) (azureGit.Client, error) {
    37  	gitClient, err := azureGit.NewClient(ctx, factory.connection)
    38  	if err != nil {
    39  		return nil, fmt.Errorf("failed to get new Azure DevOps git client for SCM generator: %w", err)
    40  	}
    41  	return gitClient, nil
    42  }
    43  
    44  // Contains Azure Devops REST API implementation of SCMProviderService.
    45  // See https://docs.microsoft.com/en-us/rest/api/azure/devops
    46  
    47  type AzureDevOpsProvider struct {
    48  	organization  string
    49  	teamProject   string
    50  	clientFactory AzureDevOpsClientFactory
    51  	allBranches   bool
    52  }
    53  
    54  var (
    55  	_ SCMProviderService       = &AzureDevOpsProvider{}
    56  	_ AzureDevOpsClientFactory = &devopsFactoryImpl{}
    57  )
    58  
    59  func NewAzureDevOpsProvider(accessToken string, org string, url string, project string, allBranches bool) (*AzureDevOpsProvider, error) {
    60  	if accessToken == "" {
    61  		return nil, errors.New("no access token provided")
    62  	}
    63  
    64  	devOpsURL, err := getValidDevOpsURL(url, org)
    65  	if err != nil {
    66  		return nil, err
    67  	}
    68  
    69  	connection := azuredevops.NewPatConnection(devOpsURL, accessToken)
    70  
    71  	return &AzureDevOpsProvider{organization: org, teamProject: project, clientFactory: &devopsFactoryImpl{connection: connection}, allBranches: allBranches}, nil
    72  }
    73  
    74  func (g *AzureDevOpsProvider) ListRepos(ctx context.Context, _ string) ([]*Repository, error) {
    75  	gitClient, err := g.clientFactory.GetClient(ctx)
    76  	if err != nil {
    77  		return nil, fmt.Errorf("failed to get Azure DevOps client: %w", err)
    78  	}
    79  	getRepoArgs := azureGit.GetRepositoriesArgs{Project: &g.teamProject}
    80  	azureRepos, err := gitClient.GetRepositories(ctx, getRepoArgs)
    81  	if err != nil {
    82  		return nil, err
    83  	}
    84  	repos := []*Repository{}
    85  	for _, azureRepo := range *azureRepos {
    86  		if azureRepo.Name == nil || azureRepo.DefaultBranch == nil || azureRepo.RemoteUrl == nil || azureRepo.Id == nil {
    87  			continue
    88  		}
    89  		repos = append(repos, &Repository{
    90  			Organization: g.organization,
    91  			Repository:   *azureRepo.Name,
    92  			URL:          *azureRepo.RemoteUrl,
    93  			Branch:       *azureRepo.DefaultBranch,
    94  			Labels:       []string{},
    95  			RepositoryId: *azureRepo.Id,
    96  		})
    97  	}
    98  
    99  	return repos, nil
   100  }
   101  
   102  func (g *AzureDevOpsProvider) RepoHasPath(ctx context.Context, repo *Repository, path string) (bool, error) {
   103  	gitClient, err := g.clientFactory.GetClient(ctx)
   104  	if err != nil {
   105  		return false, fmt.Errorf("failed to get Azure DevOps client: %w", err)
   106  	}
   107  
   108  	var repoId string
   109  	if uuid, isUUID := repo.RepositoryId.(uuid.UUID); isUUID { // most likely an UUID, but do type-safe check anyway. Do %v fallback if not expected type.
   110  		repoId = uuid.String()
   111  	} else {
   112  		repoId = fmt.Sprintf("%v", repo.RepositoryId)
   113  	}
   114  
   115  	branchName := repo.Branch
   116  	getItemArgs := azureGit.GetItemArgs{RepositoryId: &repoId, Project: &g.teamProject, Path: &path, VersionDescriptor: &azureGit.GitVersionDescriptor{Version: &branchName}}
   117  	_, err = gitClient.GetItem(ctx, getItemArgs)
   118  	if err != nil {
   119  		var wrappedError azuredevops.WrappedError
   120  		if errors.As(err, &wrappedError) && wrappedError.TypeKey != nil {
   121  			if *wrappedError.TypeKey == AzureDevOpsErrorsTypeKeyValues.GitItemNotFound {
   122  				return false, nil
   123  			}
   124  		}
   125  
   126  		return false, fmt.Errorf("failed to check for path existence in Azure DevOps: %w", err)
   127  	}
   128  
   129  	return true, nil
   130  }
   131  
   132  func (g *AzureDevOpsProvider) GetBranches(ctx context.Context, repo *Repository) ([]*Repository, error) {
   133  	gitClient, err := g.clientFactory.GetClient(ctx)
   134  	if err != nil {
   135  		return nil, fmt.Errorf("failed to get Azure DevOps client: %w", err)
   136  	}
   137  
   138  	repos := []*Repository{}
   139  
   140  	if !g.allBranches {
   141  		defaultBranchName := strings.Replace(repo.Branch, "refs/heads/", "", 1) // Azure DevOps returns default branch info like 'refs/heads/main', but does not support branch lookup of this format.
   142  		getBranchArgs := azureGit.GetBranchArgs{RepositoryId: &repo.Repository, Project: &g.teamProject, Name: &defaultBranchName}
   143  		branchResult, err := gitClient.GetBranch(ctx, getBranchArgs)
   144  		if err != nil {
   145  			var wrappedError azuredevops.WrappedError
   146  			if errors.As(err, &wrappedError) && wrappedError.TypeKey != nil {
   147  				if *wrappedError.TypeKey == AzureDevOpsErrorsTypeKeyValues.GitRepositoryNotFound {
   148  					return repos, nil
   149  				}
   150  			}
   151  			return nil, fmt.Errorf("could not get default branch %v (%v) from repository %v: %w", defaultBranchName, repo.Branch, repo.Repository, err)
   152  		}
   153  
   154  		if branchResult.Name == nil || branchResult.Commit == nil {
   155  			return nil, fmt.Errorf("invalid branch result after requesting branch %v from repository %v", repo.Branch, repo.Repository)
   156  		}
   157  
   158  		repos = append(repos, &Repository{
   159  			Branch:       *branchResult.Name,
   160  			SHA:          *branchResult.Commit.CommitId,
   161  			Organization: repo.Organization,
   162  			Repository:   repo.Repository,
   163  			URL:          repo.URL,
   164  			Labels:       []string{},
   165  			RepositoryId: repo.RepositoryId,
   166  		})
   167  
   168  		return repos, nil
   169  	}
   170  
   171  	getBranchesRequest := azureGit.GetBranchesArgs{RepositoryId: &repo.Repository, Project: &g.teamProject}
   172  	branches, err := gitClient.GetBranches(ctx, getBranchesRequest)
   173  	if err != nil {
   174  		var wrappedError azuredevops.WrappedError
   175  		if errors.As(err, &wrappedError) && wrappedError.TypeKey != nil {
   176  			if *wrappedError.TypeKey == AzureDevOpsErrorsTypeKeyValues.GitRepositoryNotFound {
   177  				return repos, nil
   178  			}
   179  		}
   180  		return nil, fmt.Errorf("failed getting branches from repository %v, project %v: %w", repo.Repository, g.teamProject, err)
   181  	}
   182  
   183  	if branches == nil {
   184  		return nil, fmt.Errorf("got empty branch result from repository %v, project %v: %w", repo.Repository, g.teamProject, err)
   185  	}
   186  
   187  	for _, azureBranch := range *branches {
   188  		repos = append(repos, &Repository{
   189  			Branch:       *azureBranch.Name,
   190  			SHA:          *azureBranch.Commit.CommitId,
   191  			Organization: repo.Organization,
   192  			Repository:   repo.Repository,
   193  			URL:          repo.URL,
   194  			Labels:       []string{},
   195  			RepositoryId: repo.RepositoryId,
   196  		})
   197  	}
   198  
   199  	return repos, nil
   200  }
   201  
   202  func getValidDevOpsURL(url string, org string) (string, error) {
   203  	if url == "" {
   204  		url = AZURE_DEVOPS_DEFAULT_URL
   205  	}
   206  	separator := ""
   207  	if !strings.HasSuffix(url, "/") {
   208  		separator = "/"
   209  	}
   210  
   211  	devOpsURL := fmt.Sprintf("%s%s%s", url, separator, org)
   212  
   213  	urlCheck, err := netUrl.ParseRequestURI(devOpsURL)
   214  	if err != nil {
   215  		return "", fmt.Errorf("got an invalid URL for the Azure SCM generator: %w", err)
   216  	}
   217  
   218  	ret := urlCheck.String()
   219  	return ret, nil
   220  }