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 }