github.com/replicatedhq/ship@v0.55.0/pkg/specs/githubclient/client.go (about)

     1  package githubclient
     2  
     3  import (
     4  	"archive/tar"
     5  	"compress/gzip"
     6  	"context"
     7  	"fmt"
     8  	"io"
     9  	"net/http"
    10  	"net/url"
    11  	"os"
    12  	"path"
    13  	"path/filepath"
    14  	"strings"
    15  
    16  	"github.com/go-kit/kit/log"
    17  	"github.com/go-kit/kit/log/level"
    18  	"github.com/google/go-github/v18/github"
    19  	"github.com/pkg/errors"
    20  	errors2 "github.com/replicatedhq/ship/pkg/util/errors"
    21  
    22  	"github.com/spf13/afero"
    23  	"golang.org/x/oauth2"
    24  )
    25  
    26  type GitHubFetcher interface {
    27  	ResolveReleaseNotes(ctx context.Context, upstream string) (string, error)
    28  	ResolveLatestRelease(ctx context.Context, upstream string) (string, error)
    29  }
    30  
    31  var _ GitHubFetcher = &GithubClient{}
    32  
    33  type GithubClient struct {
    34  	Logger log.Logger
    35  	Client *github.Client
    36  	Fs     afero.Afero
    37  }
    38  
    39  func NewGithubClient(fs afero.Afero, logger log.Logger) *GithubClient {
    40  	var httpClient *http.Client
    41  	if accessToken := os.Getenv("GITHUB_TOKEN"); accessToken != "" {
    42  		level.Debug(logger).Log("msg", "using github access token from environment")
    43  		ts := oauth2.StaticTokenSource(
    44  			&oauth2.Token{AccessToken: accessToken},
    45  		)
    46  		httpClient = oauth2.NewClient(context.Background(), ts)
    47  	}
    48  	client := github.NewClient(httpClient)
    49  	return &GithubClient{
    50  		Client: client,
    51  		Fs:     fs,
    52  		Logger: logger,
    53  	}
    54  }
    55  
    56  func (g *GithubClient) GetFiles(
    57  	ctx context.Context,
    58  	upstream string,
    59  	destinationPath string,
    60  ) (string, error) {
    61  	debug := level.Debug(log.With(g.Logger, "method", "getRepoContents"))
    62  
    63  	debug.Log("event", "validateGithubURL")
    64  	validatedUpstreamURL, err := validateGithubURL(upstream)
    65  	if err != nil {
    66  		return "", err
    67  	}
    68  
    69  	debug.Log("event", "decodeGithubURL")
    70  	owner, repo, branch, repoPath, err := decodeGitHubURL(validatedUpstreamURL.Path)
    71  	if err != nil {
    72  		return "", err
    73  	}
    74  
    75  	debug.Log("event", "removeAll", "destinationPath", destinationPath)
    76  	err = g.Fs.RemoveAll(destinationPath)
    77  	if err != nil {
    78  		return "", errors.Wrap(err, "remove chart clone destination")
    79  	}
    80  
    81  	downloadBasePath := ""
    82  	if filepath.Ext(repoPath) != "" {
    83  		downloadBasePath = repoPath
    84  		repoPath = ""
    85  	}
    86  	err = g.downloadAndExtractFiles(ctx, owner, repo, branch, downloadBasePath, destinationPath)
    87  	if err != nil {
    88  		return "", errors2.FetchFilesError{Message: err.Error()}
    89  	}
    90  
    91  	return filepath.Join(destinationPath, repoPath), nil
    92  }
    93  
    94  func (g *GithubClient) downloadAndExtractFiles(
    95  	ctx context.Context,
    96  	owner string,
    97  	repo string,
    98  	branch string,
    99  	basePath string,
   100  	filePath string,
   101  ) error {
   102  	debug := level.Debug(log.With(g.Logger, "method", "downloadAndExtractFiles"))
   103  
   104  	debug.Log("event", "getContents", "path", basePath)
   105  
   106  	archiveOpts := &github.RepositoryContentGetOptions{
   107  		Ref: branch,
   108  	}
   109  	archiveLink, _, err := g.Client.Repositories.GetArchiveLink(ctx, owner, repo, github.Tarball, archiveOpts)
   110  	if err != nil {
   111  		return errors.Wrapf(err, "get archive link for owner - %s repo - %s", owner, repo)
   112  	}
   113  
   114  	resp, err := http.Get(archiveLink.String())
   115  	if err != nil {
   116  		return errors.Wrapf(err, "downloading archive")
   117  	}
   118  	defer resp.Body.Close()
   119  
   120  	uncompressedStream, err := gzip.NewReader(resp.Body)
   121  	if err != nil {
   122  		return errors.Wrapf(err, "create uncompressed stream")
   123  	}
   124  
   125  	tarReader := tar.NewReader(uncompressedStream)
   126  
   127  	basePathFound := false
   128  	for {
   129  		header, err := tarReader.Next()
   130  		if err == io.EOF {
   131  			if !basePathFound {
   132  				branchString := branch
   133  				if branchString == "" {
   134  					branchString = "master"
   135  				}
   136  				return errors.Errorf("Path %s in %s/%s on branch %s not found", basePath, owner, repo, branchString)
   137  			}
   138  			break
   139  		}
   140  
   141  		if err != nil {
   142  			return errors.Wrapf(err, "extract tar gz, next()")
   143  		}
   144  
   145  		switch header.Typeflag {
   146  		case tar.TypeReg:
   147  			// need this in a func because defer in a loop was leaking handles
   148  			err := func() error {
   149  				fileName := strings.Join(strings.Split(header.Name, "/")[1:], "/")
   150  				if !strings.HasPrefix(fileName, basePath) {
   151  					return nil
   152  				}
   153  				basePathFound = true
   154  
   155  				if fileName != basePath {
   156  					fileName = strings.TrimPrefix(fileName, basePath)
   157  				}
   158  				dirPath, _ := path.Split(fileName)
   159  				if err := g.Fs.MkdirAll(filepath.Join(filePath, dirPath), 0755); err != nil {
   160  					return errors.Wrapf(err, "extract tar gz, mkdir")
   161  				}
   162  				outFile, err := g.Fs.Create(filepath.Join(filePath, fileName))
   163  				if err != nil {
   164  					return errors.Wrapf(err, "extract tar gz, create")
   165  				}
   166  				defer outFile.Close()
   167  				if _, err := io.Copy(outFile, tarReader); err != nil {
   168  					return errors.Wrapf(err, "extract tar gz, copy")
   169  				}
   170  				return nil
   171  			}()
   172  			if err != nil {
   173  				return err
   174  			}
   175  		}
   176  	}
   177  
   178  	return nil
   179  }
   180  
   181  func decodeGitHubURL(chartPath string) (owner string, repo string, branch string, path string, err error) {
   182  	splitPath := strings.Split(chartPath, "/")
   183  
   184  	if len(splitPath) < 3 {
   185  		return owner, repo, path, branch, errors.Wrapf(errors.New("unable to decode github url"), chartPath)
   186  	}
   187  
   188  	owner = splitPath[1]
   189  	repo = splitPath[2]
   190  	branch = ""
   191  	path = ""
   192  	if len(splitPath) > 3 {
   193  		if splitPath[3] == "tree" || splitPath[3] == "blob" {
   194  			branch = splitPath[4]
   195  			path = strings.Join(splitPath[5:], "/")
   196  		} else {
   197  			path = strings.Join(splitPath[3:], "/")
   198  		}
   199  	}
   200  
   201  	return owner, repo, branch, path, nil
   202  }
   203  
   204  func validateGithubURL(upstream string) (*url.URL, error) {
   205  	if !strings.HasPrefix(upstream, "http") {
   206  
   207  		upstream = fmt.Sprintf("http://%s", upstream)
   208  	}
   209  
   210  	upstreamURL, err := url.Parse(upstream)
   211  	if err != nil {
   212  		return nil, err
   213  	}
   214  
   215  	if !strings.Contains(upstreamURL.Host, "github.com") {
   216  		return nil, errors.Errorf("%s is not a Github URL", upstream)
   217  	}
   218  
   219  	return upstreamURL, nil
   220  }
   221  
   222  func (g *GithubClient) ResolveReleaseNotes(ctx context.Context, upstream string) (string, error) {
   223  	debug := level.Debug(log.With(g.Logger, "method", "ResolveReleaseNotes"))
   224  
   225  	debug.Log("event", "validateGithubURL")
   226  	validatedUpstreamURL, err := validateGithubURL(upstream)
   227  	if err != nil {
   228  		return "", errors.Wrap(err, "not a valid github url")
   229  	}
   230  
   231  	debug.Log("event", "decodeGithubURL")
   232  	owner, repo, branch, repoPath, err := decodeGitHubURL(validatedUpstreamURL.Path)
   233  	if err != nil {
   234  		return "", err
   235  	}
   236  
   237  	commitList, _, err := g.Client.Repositories.ListCommits(ctx, owner, repo, &github.CommitsListOptions{
   238  		SHA:  branch,
   239  		Path: repoPath,
   240  	})
   241  	if err != nil {
   242  		return "", err
   243  	}
   244  
   245  	if len(commitList) > 0 {
   246  		latestRepoCommit := commitList[0]
   247  		if latestRepoCommit != nil {
   248  			commit := latestRepoCommit.GetCommit()
   249  			if commit != nil {
   250  				return commit.GetMessage(), nil
   251  			}
   252  		}
   253  	}
   254  
   255  	return "", errors.New("No commit available")
   256  }
   257  
   258  func (g *GithubClient) ResolveLatestRelease(ctx context.Context, upstream string) (string, error) {
   259  	validatedUpstreamURL, err := validateGithubURL(upstream)
   260  	if err != nil {
   261  		return "", errors.Wrap(err, "not a valid github url")
   262  	}
   263  
   264  	owner, repo, _, _, err := decodeGitHubURL(validatedUpstreamURL.Path)
   265  	if err != nil {
   266  		return "", err
   267  	}
   268  
   269  	latest, _, err := g.Client.Repositories.GetLatestRelease(ctx, owner, repo)
   270  	if err != nil {
   271  		return "", errors.Wrap(err, "get latest release")
   272  	}
   273  
   274  	return latest.GetTagName(), nil
   275  }