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  }