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