github.com/argoproj/argo-cd/v3@v3.2.1/applicationset/services/scm_provider/bitbucket_server.go (about) 1 package scm_provider 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "io" 8 "net/http" 9 10 bitbucketv1 "github.com/gfleury/go-bitbucket-v1" 11 log "github.com/sirupsen/logrus" 12 13 "github.com/argoproj/argo-cd/v3/applicationset/services" 14 ) 15 16 type BitbucketServerProvider struct { 17 client *bitbucketv1.APIClient 18 projectKey string 19 allBranches bool 20 } 21 22 var _ SCMProviderService = &BitbucketServerProvider{} 23 24 func NewBitbucketServerProviderBasicAuth(ctx context.Context, username, password, url, projectKey string, allBranches bool, scmRootCAPath string, insecure bool, caCerts []byte) (*BitbucketServerProvider, error) { 25 bitbucketConfig := bitbucketv1.NewConfiguration(url) 26 // Avoid the XSRF check 27 bitbucketConfig.AddDefaultHeader("x-atlassian-token", "no-check") 28 bitbucketConfig.AddDefaultHeader("x-requested-with", "XMLHttpRequest") 29 30 ctx = context.WithValue(ctx, bitbucketv1.ContextBasicAuth, bitbucketv1.BasicAuth{ 31 UserName: username, 32 Password: password, 33 }) 34 return newBitbucketServerProvider(ctx, bitbucketConfig, projectKey, allBranches, scmRootCAPath, insecure, caCerts) 35 } 36 37 func NewBitbucketServerProviderBearerToken(ctx context.Context, bearerToken, url, projectKey string, allBranches bool, scmRootCAPath string, insecure bool, caCerts []byte) (*BitbucketServerProvider, error) { 38 bitbucketConfig := bitbucketv1.NewConfiguration(url) 39 // Avoid the XSRF check 40 bitbucketConfig.AddDefaultHeader("x-atlassian-token", "no-check") 41 bitbucketConfig.AddDefaultHeader("x-requested-with", "XMLHttpRequest") 42 43 ctx = context.WithValue(ctx, bitbucketv1.ContextAccessToken, bearerToken) 44 return newBitbucketServerProvider(ctx, bitbucketConfig, projectKey, allBranches, scmRootCAPath, insecure, caCerts) 45 } 46 47 func NewBitbucketServerProviderNoAuth(ctx context.Context, url, projectKey string, allBranches bool, scmRootCAPath string, insecure bool, caCerts []byte) (*BitbucketServerProvider, error) { 48 return newBitbucketServerProvider(ctx, bitbucketv1.NewConfiguration(url), projectKey, allBranches, scmRootCAPath, insecure, caCerts) 49 } 50 51 func newBitbucketServerProvider(ctx context.Context, bitbucketConfig *bitbucketv1.Configuration, projectKey string, allBranches bool, scmRootCAPath string, insecure bool, caCerts []byte) (*BitbucketServerProvider, error) { 52 bbClient := services.SetupBitbucketClient(ctx, bitbucketConfig, scmRootCAPath, insecure, caCerts) 53 54 return &BitbucketServerProvider{ 55 client: bbClient, 56 projectKey: projectKey, 57 allBranches: allBranches, 58 }, nil 59 } 60 61 func (b *BitbucketServerProvider) ListRepos(_ context.Context, cloneProtocol string) ([]*Repository, error) { 62 paged := map[string]any{ 63 "limit": 100, 64 } 65 repos := []*Repository{} 66 for { 67 response, err := b.client.DefaultApi.GetRepositoriesWithOptions(b.projectKey, paged) 68 if err != nil { 69 return nil, fmt.Errorf("error listing repositories for %s: %w", b.projectKey, err) 70 } 71 repositories, err := bitbucketv1.GetRepositoriesResponse(response) 72 if err != nil { 73 log.Errorf("error parsing repositories response '%v'", response.Values) 74 return nil, fmt.Errorf("error parsing repositories response %s: %w", b.projectKey, err) 75 } 76 for _, bitbucketRepo := range repositories { 77 var url string 78 switch cloneProtocol { 79 // Default to SSH if unspecified (i.e. if ""). 80 case "", "ssh": 81 url = getCloneURLFromLinks(bitbucketRepo.Links.Clone, "ssh") 82 case "https": 83 url = getCloneURLFromLinks(bitbucketRepo.Links.Clone, "http") 84 default: 85 return nil, fmt.Errorf("unknown clone protocol for Bitbucket Server %v", cloneProtocol) 86 } 87 88 org := bitbucketRepo.Project.Key 89 repo := bitbucketRepo.Name 90 // Bitbucket doesn't return the default branch in the repo query, fetch it here 91 branch, err := b.getDefaultBranch(org, repo) 92 if err != nil { 93 return nil, err 94 } 95 if branch == nil { 96 log.Debugf("%s/%s does not have a default branch, skipping", org, repo) 97 continue 98 } 99 100 repos = append(repos, &Repository{ 101 Organization: org, 102 Repository: repo, 103 URL: url, 104 Branch: branch.DisplayID, 105 SHA: branch.LatestCommit, 106 Labels: []string{}, // Not supported by library 107 RepositoryId: bitbucketRepo.ID, 108 }) 109 } 110 hasNextPage, nextPageStart := bitbucketv1.HasNextPage(response) 111 if !hasNextPage { 112 break 113 } 114 paged["start"] = nextPageStart 115 } 116 return repos, nil 117 } 118 119 func (b *BitbucketServerProvider) RepoHasPath(_ context.Context, repo *Repository, path string) (bool, error) { 120 opts := map[string]any{ 121 "limit": 100, 122 "at": repo.Branch, 123 "type_": true, 124 } 125 // No need to query for all pages here 126 response, err := b.client.DefaultApi.GetContent_0(repo.Organization, repo.Repository, path, opts) 127 if response != nil && response.StatusCode == http.StatusNotFound { 128 // File/directory not found 129 return false, nil 130 } 131 if err != nil { 132 return false, err 133 } 134 return true, nil 135 } 136 137 func (b *BitbucketServerProvider) GetBranches(_ context.Context, repo *Repository) ([]*Repository, error) { 138 repos := []*Repository{} 139 branches, err := b.listBranches(repo) 140 if err != nil { 141 return nil, fmt.Errorf("error listing branches for %s/%s: %w", repo.Organization, repo.Repository, err) 142 } 143 144 for _, branch := range branches { 145 repos = append(repos, &Repository{ 146 Organization: repo.Organization, 147 Repository: repo.Repository, 148 URL: repo.URL, 149 Branch: branch.DisplayID, 150 SHA: branch.LatestCommit, 151 Labels: repo.Labels, 152 RepositoryId: repo.RepositoryId, 153 }) 154 } 155 return repos, nil 156 } 157 158 func (b *BitbucketServerProvider) listBranches(repo *Repository) ([]bitbucketv1.Branch, error) { 159 // If we don't specifically want to query for all branches, just use the default branch and call it a day. 160 if !b.allBranches { 161 branch, err := b.getDefaultBranch(repo.Organization, repo.Repository) 162 if err != nil { 163 return nil, err 164 } 165 if branch == nil { 166 return []bitbucketv1.Branch{}, nil 167 } 168 return []bitbucketv1.Branch{*branch}, nil 169 } 170 // Otherwise, scrape the GetBranches API. 171 branches := []bitbucketv1.Branch{} 172 paged := map[string]any{ 173 "limit": 100, 174 } 175 for { 176 response, err := b.client.DefaultApi.GetBranches(repo.Organization, repo.Repository, paged) 177 if err != nil { 178 return nil, fmt.Errorf("error listing branches for %s/%s: %w", repo.Organization, repo.Repository, err) 179 } 180 bitbucketBranches, err := bitbucketv1.GetBranchesResponse(response) 181 if err != nil { 182 log.Errorf("error parsing branches response '%v'", response.Values) 183 return nil, fmt.Errorf("error parsing branches response for %s/%s: %w", repo.Organization, repo.Repository, err) 184 } 185 186 branches = append(branches, bitbucketBranches...) 187 188 hasNextPage, nextPageStart := bitbucketv1.HasNextPage(response) 189 if !hasNextPage { 190 break 191 } 192 paged["start"] = nextPageStart 193 } 194 return branches, nil 195 } 196 197 func (b *BitbucketServerProvider) getDefaultBranch(org string, repo string) (*bitbucketv1.Branch, error) { 198 response, err := b.client.DefaultApi.GetDefaultBranch(org, repo) 199 // The API will return 404 if a default branch is set but doesn't exist. In case the repo is empty and default branch is unset, 200 // we will get an EOF and a nil response. 201 if (response != nil && response.StatusCode == http.StatusNotFound) || (response == nil && err != nil && errors.Is(err, io.EOF)) { 202 return nil, nil 203 } 204 if err != nil { 205 return nil, err 206 } 207 branch, err := bitbucketv1.GetBranchResponse(response) 208 if err != nil { 209 return nil, err 210 } 211 return &branch, nil 212 } 213 214 func getCloneURLFromLinks(links []bitbucketv1.CloneLink, name string) string { 215 for _, link := range links { 216 if link.Name == name { 217 return link.Href 218 } 219 } 220 return "" 221 }