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

     1  package scm_provider
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"net/http"
     7  	"os"
     8  
     9  	"github.com/google/go-github/v69/github"
    10  
    11  	appsetutils "github.com/argoproj/argo-cd/v3/applicationset/utils"
    12  )
    13  
    14  type GithubProvider struct {
    15  	client       *github.Client
    16  	organization string
    17  	allBranches  bool
    18  }
    19  
    20  var _ SCMProviderService = &GithubProvider{}
    21  
    22  func NewGithubProvider(organization string, token string, url string, allBranches bool, optionalHTTPClient ...*http.Client) (*GithubProvider, error) {
    23  	// Undocumented environment variable to set a default token, to be used in testing to dodge anonymous rate limits.
    24  	if token == "" {
    25  		token = os.Getenv("GITHUB_TOKEN")
    26  	}
    27  
    28  	var client *github.Client
    29  	httpClient := appsetutils.GetOptionalHTTPClient(optionalHTTPClient...)
    30  
    31  	if url == "" {
    32  		if token == "" {
    33  			client = github.NewClient(httpClient)
    34  		} else {
    35  			client = github.NewClient(httpClient).WithAuthToken(token)
    36  		}
    37  	} else {
    38  		var err error
    39  		if token == "" {
    40  			client, err = github.NewClient(httpClient).WithEnterpriseURLs(url, url)
    41  		} else {
    42  			client, err = github.NewClient(httpClient).WithAuthToken(token).WithEnterpriseURLs(url, url)
    43  		}
    44  		if err != nil {
    45  			return nil, err
    46  		}
    47  	}
    48  	return &GithubProvider{client: client, organization: organization, allBranches: allBranches}, nil
    49  }
    50  
    51  func (g *GithubProvider) GetBranches(ctx context.Context, repo *Repository) ([]*Repository, error) {
    52  	repos := []*Repository{}
    53  	branches, err := g.listBranches(ctx, repo)
    54  	if err != nil {
    55  		return nil, fmt.Errorf("error listing branches for %s/%s: %w", repo.Organization, repo.Repository, err)
    56  	}
    57  
    58  	for _, branch := range branches {
    59  		repos = append(repos, &Repository{
    60  			Organization: repo.Organization,
    61  			Repository:   repo.Repository,
    62  			URL:          repo.URL,
    63  			Branch:       branch.GetName(),
    64  			SHA:          branch.GetCommit().GetSHA(),
    65  			Labels:       repo.Labels,
    66  			RepositoryId: repo.RepositoryId,
    67  		})
    68  	}
    69  	return repos, nil
    70  }
    71  
    72  func (g *GithubProvider) ListRepos(ctx context.Context, cloneProtocol string) ([]*Repository, error) {
    73  	opt := &github.RepositoryListByOrgOptions{
    74  		ListOptions: github.ListOptions{PerPage: 100},
    75  	}
    76  	repos := []*Repository{}
    77  	for {
    78  		githubRepos, resp, err := g.client.Repositories.ListByOrg(ctx, g.organization, opt)
    79  		if err != nil {
    80  			return nil, fmt.Errorf("error listing repositories for %s: %w", g.organization, err)
    81  		}
    82  		for _, githubRepo := range githubRepos {
    83  			var url string
    84  			switch cloneProtocol {
    85  			// Default to SSH if unspecified (i.e. if "").
    86  			case "", "ssh":
    87  				url = githubRepo.GetSSHURL()
    88  			case "https":
    89  				url = githubRepo.GetCloneURL()
    90  			default:
    91  				return nil, fmt.Errorf("unknown clone protocol for GitHub %v", cloneProtocol)
    92  			}
    93  			repos = append(repos, &Repository{
    94  				Organization: githubRepo.Owner.GetLogin(),
    95  				Repository:   githubRepo.GetName(),
    96  				Branch:       githubRepo.GetDefaultBranch(),
    97  				URL:          url,
    98  				Labels:       githubRepo.Topics,
    99  				RepositoryId: githubRepo.ID,
   100  			})
   101  		}
   102  		if resp.NextPage == 0 {
   103  			break
   104  		}
   105  		opt.Page = resp.NextPage
   106  	}
   107  	return repos, nil
   108  }
   109  
   110  func (g *GithubProvider) RepoHasPath(ctx context.Context, repo *Repository, path string) (bool, error) {
   111  	_, _, resp, err := g.client.Repositories.GetContents(ctx, repo.Organization, repo.Repository, path, &github.RepositoryContentGetOptions{
   112  		Ref: repo.Branch,
   113  	})
   114  	// 404s are not an error here, just a normal false.
   115  	if resp != nil && resp.StatusCode == http.StatusNotFound {
   116  		return false, nil
   117  	}
   118  	if err != nil {
   119  		return false, err
   120  	}
   121  	return true, nil
   122  }
   123  
   124  func (g *GithubProvider) listBranches(ctx context.Context, repo *Repository) ([]github.Branch, error) {
   125  	// If we don't specifically want to query for all branches, just use the default branch and call it a day.
   126  	if !g.allBranches {
   127  		defaultBranch, resp, err := g.client.Repositories.GetBranch(ctx, repo.Organization, repo.Repository, repo.Branch, 0)
   128  		if err != nil {
   129  			if resp.StatusCode == http.StatusNotFound {
   130  				// Default branch doesn't exist, so the repo is empty.
   131  				return []github.Branch{}, nil
   132  			}
   133  			return nil, err
   134  		}
   135  		return []github.Branch{*defaultBranch}, nil
   136  	}
   137  	// Otherwise, scrape the ListBranches API.
   138  	opt := &github.BranchListOptions{
   139  		ListOptions: github.ListOptions{PerPage: 100},
   140  	}
   141  	branches := []github.Branch{}
   142  	for {
   143  		githubBranches, resp, err := g.client.Repositories.ListBranches(ctx, repo.Organization, repo.Repository, opt)
   144  		if err != nil {
   145  			return nil, err
   146  		}
   147  		for _, githubBranch := range githubBranches {
   148  			branches = append(branches, *githubBranch)
   149  		}
   150  
   151  		if resp.NextPage == 0 {
   152  			break
   153  		}
   154  		opt.Page = resp.NextPage
   155  	}
   156  	return branches, nil
   157  }