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 }