pkg/git/gogithub/gogithub.go (204 lines of code) (raw):
package gogithub
import (
"context"
"errors"
"fmt"
"net/http"
"strings"
"time"
goGithub "github.com/google/go-github/v35/github"
"golang.org/x/oauth2"
"github.com/aws/eks-anywhere/pkg/git"
"github.com/aws/eks-anywhere/pkg/logger"
"github.com/aws/eks-anywhere/pkg/retrier"
)
type GoGithub struct {
Opts Options
Client Client
}
type Options struct {
Auth git.TokenAuth
}
func New(ctx context.Context, opts Options) *GoGithub {
return &GoGithub{
Opts: opts,
Client: newClient(ctx, opts),
}
}
type HTTPClient interface {
Do(req *http.Request) (*http.Response, error)
}
type Client interface {
CreateRepo(ctx context.Context, org string, repo *goGithub.Repository) (*goGithub.Repository, *goGithub.Response, error)
AddDeployKeyToRepo(ctx context.Context, owner, repo string, key *goGithub.Key) error
Repo(ctx context.Context, owner, repo string) (*goGithub.Repository, *goGithub.Response, error)
User(ctx context.Context, user string) (*goGithub.User, *goGithub.Response, error)
Organization(ctx context.Context, org string) (*goGithub.Organization, *goGithub.Response, error)
GetContents(ctx context.Context, owner, repo, path string, opt *goGithub.RepositoryContentGetOptions) (
fileContent *goGithub.RepositoryContent, directoryContent []*goGithub.RepositoryContent, resp *goGithub.Response, err error,
)
DeleteRepo(ctx context.Context, owner, repo string) (*goGithub.Response, error)
}
type githubClient struct {
client *goGithub.Client
}
var HttpClient HTTPClient
func init() {
HttpClient = &http.Client{}
}
func (ggc *githubClient) CreateRepo(ctx context.Context, org string, repo *goGithub.Repository) (*goGithub.Repository, *goGithub.Response, error) {
return ggc.client.Repositories.Create(ctx, org, repo)
}
func (ggc *githubClient) Repo(ctx context.Context, owner, repo string) (*goGithub.Repository, *goGithub.Response, error) {
return ggc.client.Repositories.Get(ctx, owner, repo)
}
func (ggc *githubClient) User(ctx context.Context, user string) (*goGithub.User, *goGithub.Response, error) {
return ggc.client.Users.Get(ctx, user)
}
func (ggc *githubClient) Organization(ctx context.Context, org string) (*goGithub.Organization, *goGithub.Response, error) {
return ggc.client.Organizations.Get(ctx, org)
}
func (ggc *githubClient) GetContents(ctx context.Context, owner, repo, path string, opt *goGithub.RepositoryContentGetOptions) (fileContent *goGithub.RepositoryContent, directoryContent []*goGithub.RepositoryContent, resp *goGithub.Response, err error) {
return ggc.client.Repositories.GetContents(ctx, owner, repo, path, opt)
}
func (ggc *githubClient) DeleteRepo(ctx context.Context, owner, repo string) (*goGithub.Response, error) {
return ggc.client.Repositories.Delete(ctx, owner, repo)
}
func (ggc *githubClient) AddDeployKeyToRepo(ctx context.Context, owner, repo string, key *goGithub.Key) error {
_, resp, err := ggc.client.Repositories.CreateKey(ctx, owner, repo, key)
if err != nil {
logger.Info("createKey response", "resp", resp)
return fmt.Errorf("adding deploy key to repo: %v", err)
}
return err
}
// CreateRepo creates an empty Github Repository. The repository must be initialized locally or
// file must be added to it via the github api before it can be successfully cloned.
func (g *GoGithub) CreateRepo(ctx context.Context, opts git.CreateRepoOpts) (repository *git.Repository, err error) {
logger.V(3).Info("Attempting to create new Github repo", "repo", opts.Name, "owner", opts.Owner)
r := &goGithub.Repository{
Name: &opts.Name,
Private: &opts.Privacy,
Description: &opts.Description,
AutoInit: &opts.AutoInit,
}
org := ""
if !opts.Personal {
org = opts.Owner
logger.V(4).Info("Not a personal repository; using repository Owner as Org", "org", org, "owner", opts.Owner)
}
repo, _, err := g.Client.CreateRepo(ctx, org, r)
if err != nil {
return nil, fmt.Errorf("failed to create new Github repo %s: %v", opts.Name, err)
}
logger.V(3).Info("Successfully created new Github repo", "repo", repo.GetName(), "owner", opts.Owner)
return &git.Repository{
Name: repo.GetName(),
CloneUrl: repo.GetCloneURL(),
Owner: repo.GetOwner().GetName(),
Organization: repo.GetOrganization().GetName(),
}, err
}
func (g *GoGithub) GetAccessTokenPermissions(accessToken string) (string, error) {
req, err := http.NewRequest("HEAD", "https://api.github.com/users/codertocat", nil)
if err != nil {
return "", err
}
req.Header.Set("Authorization", "token "+accessToken)
var resp *http.Response
r := retrier.New(3 * time.Minute)
err = r.Retry(func() error {
resp, err = HttpClient.Do(req)
if err != nil {
return fmt.Errorf("getting Github Personal Access Token permissions %v", err)
}
return nil
})
permissionsScopes := resp.Header.Get("X-Oauth-Scopes")
defer resp.Body.Close()
return permissionsScopes, nil
}
func (g *GoGithub) CheckAccessTokenPermissions(checkPATPermission string, allPermissionScopes string) error {
logger.Info("Checking Github Access Token permissions")
allPermissions := strings.Split(allPermissionScopes, ", ")
for _, permission := range allPermissions {
if permission == checkPATPermission {
return nil
}
}
return errors.New("github access token does not have repo permissions")
}
// GetRepo describes a remote repository, return the repo name if it exists.
// If the repo does not exist, resulting in a 404 exception, it returns a `RepoDoesNotExist` error.
func (g *GoGithub) GetRepo(ctx context.Context, opts git.GetRepoOpts) (*git.Repository, error) {
r := opts.Repository
o := opts.Owner
logger.V(3).Info("Describing Github repository", "name", r, "owner", o)
repo, _, err := g.Client.Repo(ctx, o, r)
if err != nil {
if isNotFound(err) {
return nil, &git.RepositoryDoesNotExistError{Err: err}
}
return nil, fmt.Errorf("unexpected error when describing repository %s: %w", r, err)
}
return &git.Repository{
Name: repo.GetName(),
CloneUrl: repo.GetCloneURL(),
Owner: repo.GetOwner().GetName(),
Organization: repo.GetOrganization().GetName(),
}, err
}
func (g *GoGithub) AuthenticatedUser(ctx context.Context) (*goGithub.User, error) {
githubUser, _, err := g.Client.User(ctx, "") // passing the empty string will fetch the authenticated
if err != nil {
return nil, fmt.Errorf("failed while getting the authenticated github user %v", err)
}
return githubUser, nil
}
func (g *GoGithub) Organization(ctx context.Context, org string) (*goGithub.Organization, error) {
organization, _, err := g.Client.Organization(ctx, org)
if err != nil {
return nil, fmt.Errorf("failed while getting github organization %s details %v", org, err)
}
return organization, nil
}
// PathExists checks if a path exists in the remote repository. If the owner, repository or branch doesn't exist,
// it returns false and no error.
func (g *GoGithub) PathExists(ctx context.Context, owner, repo, branch, path string) (bool, error) {
_, _, _, err := g.Client.GetContents(
ctx,
owner,
repo,
path,
&goGithub.RepositoryContentGetOptions{Ref: branch},
)
if isNotFound(err) {
return false, nil
}
if err != nil {
return false, fmt.Errorf("failed checking if path %s exists in remote github repository: %v", path, err)
}
return true, nil
}
func (g *GoGithub) AddDeployKeyToRepo(ctx context.Context, opts git.AddDeployKeyOpts) error {
logger.V(3).Info("Adding deploy key to repository", "repository", opts.Repository, "owner", opts.Owner)
k := &goGithub.Key{
Key: &opts.Key,
Title: &opts.Title,
ReadOnly: &opts.ReadOnly,
}
return g.Client.AddDeployKeyToRepo(ctx, opts.Owner, opts.Repository, k)
}
// DeleteRepo deletes a Github repository.
func (g *GoGithub) DeleteRepo(ctx context.Context, opts git.DeleteRepoOpts) error {
r := opts.Repository
o := opts.Owner
logger.V(3).Info("Deleting Github repository", "name", r, "owner", o)
_, err := g.Client.DeleteRepo(ctx, o, r)
if err != nil {
return fmt.Errorf("deleting repository %s: %v", r, err)
}
return nil
}
func newClient(ctx context.Context, opts Options) Client {
ts := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: opts.Auth.Token})
tc := oauth2.NewClient(ctx, ts)
return &githubClient{goGithub.NewClient(tc)}
}
func isNotFound(err error) bool {
var e *goGithub.ErrorResponse
return errors.As(err, &e) && e.Response.StatusCode == http.StatusNotFound
}