github.com/argoproj/argo-cd/v3@v3.2.1/applicationset/services/scm_provider/gitlab.go (about)

     1  package scm_provider
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"net/http"
     8  	"os"
     9  
    10  	"github.com/hashicorp/go-retryablehttp"
    11  	gitlab "gitlab.com/gitlab-org/api/client-go"
    12  
    13  	"github.com/argoproj/argo-cd/v3/applicationset/utils"
    14  )
    15  
    16  type GitlabProvider struct {
    17  	client                *gitlab.Client
    18  	organization          string
    19  	allBranches           bool
    20  	includeSubgroups      bool
    21  	includeSharedProjects bool
    22  	topic                 string
    23  }
    24  
    25  var _ SCMProviderService = &GitlabProvider{}
    26  
    27  func NewGitlabProvider(organization string, token string, url string, allBranches, includeSubgroups, includeSharedProjects, insecure bool, scmRootCAPath, topic string, caCerts []byte) (*GitlabProvider, error) {
    28  	// Undocumented environment variable to set a default token, to be used in testing to dodge anonymous rate limits.
    29  	if token == "" {
    30  		token = os.Getenv("GITLAB_TOKEN")
    31  	}
    32  	var client *gitlab.Client
    33  
    34  	tr := http.DefaultTransport.(*http.Transport).Clone()
    35  	tr.TLSClientConfig = utils.GetTlsConfig(scmRootCAPath, insecure, caCerts)
    36  
    37  	retryClient := retryablehttp.NewClient()
    38  	retryClient.HTTPClient.Transport = tr
    39  
    40  	if url == "" {
    41  		var err error
    42  		client, err = gitlab.NewClient(token, gitlab.WithHTTPClient(retryClient.HTTPClient))
    43  		if err != nil {
    44  			return nil, err
    45  		}
    46  	} else {
    47  		var err error
    48  		client, err = gitlab.NewClient(token, gitlab.WithBaseURL(url), gitlab.WithHTTPClient(retryClient.HTTPClient))
    49  		if err != nil {
    50  			return nil, err
    51  		}
    52  	}
    53  
    54  	return &GitlabProvider{client: client, organization: organization, allBranches: allBranches, includeSubgroups: includeSubgroups, includeSharedProjects: includeSharedProjects, topic: topic}, nil
    55  }
    56  
    57  func (g *GitlabProvider) GetBranches(ctx context.Context, repo *Repository) ([]*Repository, error) {
    58  	repos := []*Repository{}
    59  	branches, err := g.listBranches(ctx, repo)
    60  	if err != nil {
    61  		return nil, fmt.Errorf("error listing branches for %s/%s: %w", repo.Organization, repo.Repository, err)
    62  	}
    63  
    64  	for _, branch := range branches {
    65  		repos = append(repos, &Repository{
    66  			Organization: repo.Organization,
    67  			Repository:   repo.Repository,
    68  			URL:          repo.URL,
    69  			Branch:       branch.Name,
    70  			SHA:          branch.Commit.ID,
    71  			Labels:       repo.Labels,
    72  			RepositoryId: repo.RepositoryId,
    73  		})
    74  	}
    75  	return repos, nil
    76  }
    77  
    78  func (g *GitlabProvider) ListRepos(_ context.Context, cloneProtocol string) ([]*Repository, error) {
    79  	opt := &gitlab.ListGroupProjectsOptions{
    80  		ListOptions:      gitlab.ListOptions{PerPage: 100},
    81  		IncludeSubGroups: &g.includeSubgroups,
    82  		WithShared:       &g.includeSharedProjects,
    83  		Topic:            &g.topic,
    84  	}
    85  
    86  	repos := []*Repository{}
    87  	for {
    88  		gitlabRepos, resp, err := g.client.Groups.ListGroupProjects(g.organization, opt)
    89  		if err != nil {
    90  			return nil, fmt.Errorf("error listing projects for %s: %w", g.organization, err)
    91  		}
    92  		for _, gitlabRepo := range gitlabRepos {
    93  			var url string
    94  			switch cloneProtocol {
    95  			// Default to SSH if unspecified (i.e. if "").
    96  			case "", "ssh":
    97  				url = gitlabRepo.SSHURLToRepo
    98  			case "https":
    99  				url = gitlabRepo.HTTPURLToRepo
   100  			default:
   101  				return nil, fmt.Errorf("unknown clone protocol for Gitlab %v", cloneProtocol)
   102  			}
   103  
   104  			var repoLabels []string
   105  			if len(gitlabRepo.Topics) == 0 {
   106  				// fallback to for gitlab prior to 14.5
   107  				//nolint:staticcheck
   108  				repoLabels = gitlabRepo.TagList
   109  			} else {
   110  				repoLabels = gitlabRepo.Topics
   111  			}
   112  
   113  			repos = append(repos, &Repository{
   114  				Organization: gitlabRepo.Namespace.FullPath,
   115  				Repository:   gitlabRepo.Path,
   116  				URL:          url,
   117  				Branch:       gitlabRepo.DefaultBranch,
   118  				Labels:       repoLabels,
   119  				RepositoryId: gitlabRepo.ID,
   120  			})
   121  		}
   122  		if resp.CurrentPage >= resp.TotalPages {
   123  			break
   124  		}
   125  		opt.Page = resp.NextPage
   126  	}
   127  	return repos, nil
   128  }
   129  
   130  func (g *GitlabProvider) RepoHasPath(_ context.Context, repo *Repository, path string) (bool, error) {
   131  	p, _, err := g.client.Projects.GetProject(repo.Organization+"/"+repo.Repository, nil)
   132  	if err != nil {
   133  		return false, fmt.Errorf("error getting Project Info: %w", err)
   134  	}
   135  
   136  	// search if the path is a file and exists in the repo
   137  	fileOptions := gitlab.GetFileOptions{Ref: &repo.Branch}
   138  	_, _, err = g.client.RepositoryFiles.GetFile(p.ID, path, &fileOptions)
   139  	if err != nil {
   140  		if errors.Is(err, gitlab.ErrNotFound) {
   141  			// no file found, check for a directory
   142  			options := gitlab.ListTreeOptions{
   143  				Path: &path,
   144  				Ref:  &repo.Branch,
   145  			}
   146  			_, _, err := g.client.Repositories.ListTree(p.ID, &options)
   147  			if err != nil {
   148  				if errors.Is(err, gitlab.ErrNotFound) {
   149  					return false, nil // no file or directory found
   150  				}
   151  				return false, err
   152  			}
   153  			return true, nil // directory found
   154  		}
   155  		return false, err
   156  	}
   157  	return true, nil // file found
   158  }
   159  
   160  func (g *GitlabProvider) listBranches(_ context.Context, repo *Repository) ([]gitlab.Branch, error) {
   161  	branches := []gitlab.Branch{}
   162  	// If we don't specifically want to query for all branches, just use the default branch and call it a day.
   163  	if !g.allBranches {
   164  		gitlabBranch, resp, err := g.client.Branches.GetBranch(repo.RepositoryId, repo.Branch, nil)
   165  		// 404s are not an error here, just a normal false.
   166  		if resp != nil && resp.StatusCode == http.StatusNotFound {
   167  			return []gitlab.Branch{}, nil
   168  		}
   169  		if err != nil {
   170  			return nil, err
   171  		}
   172  		branches = append(branches, *gitlabBranch)
   173  		return branches, nil
   174  	}
   175  	// Otherwise, scrape the ListBranches API.
   176  	opt := &gitlab.ListBranchesOptions{
   177  		ListOptions: gitlab.ListOptions{PerPage: 100},
   178  	}
   179  	for {
   180  		gitlabBranches, resp, err := g.client.Branches.ListBranches(repo.RepositoryId, opt)
   181  		// 404s are not an error here, just a normal false.
   182  		if resp != nil && resp.StatusCode == http.StatusNotFound {
   183  			return []gitlab.Branch{}, nil
   184  		}
   185  		if err != nil {
   186  			return nil, err
   187  		}
   188  		for _, gitlabBranch := range gitlabBranches {
   189  			branches = append(branches, *gitlabBranch)
   190  		}
   191  
   192  		if resp.NextPage == 0 {
   193  			break
   194  		}
   195  		opt.Page = resp.NextPage
   196  	}
   197  	return branches, nil
   198  }