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 }