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

     1  package scm_provider
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	netUrl "net/url"
     7  	"strings"
     8  
     9  	"github.com/google/uuid"
    10  	"github.com/microsoft/azure-devops-go-api/azuredevops"
    11  	azureGit "github.com/microsoft/azure-devops-go-api/azuredevops/git"
    12  )
    13  
    14  const AZURE_DEVOPS_DEFAULT_URL = "https://dev.azure.com"
    15  
    16  type azureDevOpsErrorTypeKeyValuesType struct {
    17  	GitRepositoryNotFound string
    18  	GitItemNotFound       string
    19  }
    20  
    21  var AzureDevOpsErrorsTypeKeyValues = azureDevOpsErrorTypeKeyValuesType{
    22  	GitRepositoryNotFound: "GitRepositoryNotFoundException",
    23  	GitItemNotFound:       "GitItemNotFoundException",
    24  }
    25  
    26  type AzureDevOpsClientFactory interface {
    27  	// Returns an Azure Devops Client interface.
    28  	GetClient(ctx context.Context) (azureGit.Client, error)
    29  }
    30  
    31  type devopsFactoryImpl struct {
    32  	connection *azuredevops.Connection
    33  }
    34  
    35  func (factory *devopsFactoryImpl) GetClient(ctx context.Context) (azureGit.Client, error) {
    36  	gitClient, err := azureGit.NewClient(ctx, factory.connection)
    37  	if err != nil {
    38  		return nil, fmt.Errorf("failed to get new Azure DevOps git client for SCM generator: %w", err)
    39  	}
    40  	return gitClient, nil
    41  }
    42  
    43  // Contains Azure Devops REST API implementation of SCMProviderService.
    44  // See https://docs.microsoft.com/en-us/rest/api/azure/devops
    45  
    46  type AzureDevOpsProvider struct {
    47  	organization  string
    48  	teamProject   string
    49  	accessToken   string
    50  	clientFactory AzureDevOpsClientFactory
    51  	allBranches   bool
    52  }
    53  
    54  var _ SCMProviderService = &AzureDevOpsProvider{}
    55  var _ AzureDevOpsClientFactory = &devopsFactoryImpl{}
    56  
    57  func NewAzureDevOpsProvider(ctx context.Context, accessToken string, org string, url string, project string, allBranches bool) (*AzureDevOpsProvider, error) {
    58  	if accessToken == "" {
    59  		return nil, fmt.Errorf("no access token provided")
    60  	}
    61  
    62  	devOpsURL, err := getValidDevOpsURL(url, org)
    63  
    64  	if err != nil {
    65  		return nil, err
    66  	}
    67  
    68  	connection := azuredevops.NewPatConnection(devOpsURL, accessToken)
    69  
    70  	return &AzureDevOpsProvider{organization: org, teamProject: project, accessToken: accessToken, clientFactory: &devopsFactoryImpl{connection: connection}, allBranches: allBranches}, nil
    71  }
    72  
    73  func (g *AzureDevOpsProvider) ListRepos(ctx context.Context, cloneProtocol string) ([]*Repository, error) {
    74  	gitClient, err := g.clientFactory.GetClient(ctx)
    75  	if err != nil {
    76  		return nil, fmt.Errorf("failed to get Azure DevOps client: %w", err)
    77  	}
    78  	getRepoArgs := azureGit.GetRepositoriesArgs{Project: &g.teamProject}
    79  	azureRepos, err := gitClient.GetRepositories(ctx, getRepoArgs)
    80  
    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  
   119  	if err != nil {
   120  		if wrappedError, isWrappedError := err.(azuredevops.WrappedError); isWrappedError && 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  			if wrappedError, isWrappedError := err.(azuredevops.WrappedError); isWrappedError && wrappedError.TypeKey != nil {
   146  				if *wrappedError.TypeKey == AzureDevOpsErrorsTypeKeyValues.GitRepositoryNotFound {
   147  					return repos, nil
   148  				}
   149  			}
   150  			return nil, fmt.Errorf("could not get default branch %v (%v) from repository %v: %w", defaultBranchName, repo.Branch, repo.Repository, err)
   151  		}
   152  
   153  		if branchResult.Name == nil || branchResult.Commit == nil {
   154  			return nil, fmt.Errorf("invalid branch result after requesting branch %v from repository %v", repo.Branch, repo.Repository)
   155  		}
   156  
   157  		repos = append(repos, &Repository{
   158  			Branch:       *branchResult.Name,
   159  			SHA:          *branchResult.Commit.CommitId,
   160  			Organization: repo.Organization,
   161  			Repository:   repo.Repository,
   162  			URL:          repo.URL,
   163  			Labels:       []string{},
   164  			RepositoryId: repo.RepositoryId,
   165  		})
   166  
   167  		return repos, nil
   168  	}
   169  
   170  	getBranchesRequest := azureGit.GetBranchesArgs{RepositoryId: &repo.Repository, Project: &g.teamProject}
   171  	branches, err := gitClient.GetBranches(ctx, getBranchesRequest)
   172  	if err != nil {
   173  		if wrappedError, isWrappedError := err.(azuredevops.WrappedError); isWrappedError && wrappedError.TypeKey != nil {
   174  			if *wrappedError.TypeKey == AzureDevOpsErrorsTypeKeyValues.GitRepositoryNotFound {
   175  				return repos, nil
   176  			}
   177  		}
   178  		return nil, fmt.Errorf("failed getting branches from repository %v, project %v: %w", repo.Repository, g.teamProject, err)
   179  	}
   180  
   181  	if branches == nil {
   182  		return nil, fmt.Errorf("got empty branch result from repository %v, project %v: %w", repo.Repository, g.teamProject, err)
   183  	}
   184  
   185  	for _, azureBranch := range *branches {
   186  		repos = append(repos, &Repository{
   187  			Branch:       *azureBranch.Name,
   188  			SHA:          *azureBranch.Commit.CommitId,
   189  			Organization: repo.Organization,
   190  			Repository:   repo.Repository,
   191  			URL:          repo.URL,
   192  			Labels:       []string{},
   193  			RepositoryId: repo.RepositoryId,
   194  		})
   195  	}
   196  
   197  	return repos, nil
   198  }
   199  
   200  func getValidDevOpsURL(url string, org string) (string, error) {
   201  	if url == "" {
   202  		url = AZURE_DEVOPS_DEFAULT_URL
   203  	}
   204  	separator := ""
   205  	if !strings.HasSuffix(url, "/") {
   206  		separator = "/"
   207  	}
   208  
   209  	devOpsURL := fmt.Sprintf("%s%s%s", url, separator, org)
   210  
   211  	urlCheck, err := netUrl.ParseRequestURI(devOpsURL)
   212  
   213  	if err != nil {
   214  		return "", fmt.Errorf("got an invalid URL for the Azure SCM generator: %w", err)
   215  	}
   216  
   217  	ret := urlCheck.String()
   218  	return ret, nil
   219  }