Files
athens/pkg/module/go_get_fetcher.go
Rohan Chakravarthy c7d0c0a8e6 Ensure diskRef cleans up everything in it's own GOPATH (#354)
* Ensure diskRef cleans up everything in it's own GOPATH

* cleanup diskref opens. Fix err return in go_get_fetcher

* Remove Noop ref from go_get_fetcher.Fetch()

* handle temp dir cleanup and return nil diskref
2018-07-30 19:14:06 -04:00

143 lines
4.5 KiB
Go

package module
import (
"bytes"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"github.com/gomods/athens/pkg/errors"
pkgerrors "github.com/pkg/errors"
"github.com/spf13/afero"
)
type goGetFetcher struct {
fs afero.Fs
goBinaryName string
}
// NewGoGetFetcher creates fetcher which uses go get tool to fetch modules
func NewGoGetFetcher(goBinaryName string, fs afero.Fs) Fetcher {
return &goGetFetcher{
fs: fs,
goBinaryName: goBinaryName,
}
}
// Fetch downloads the sources and returns path where it can be found. Make sure to call Clear
// on the returned Ref when you are done with it
func (g *goGetFetcher) Fetch(mod, ver string) (Ref, error) {
// setup the GOPATH
goPathRoot, err := afero.TempDir(g.fs, "", "athens")
if err != nil {
return nil, err
}
sourcePath := filepath.Join(goPathRoot, "src")
modPath := filepath.Join(sourcePath, getRepoDirName(mod, ver))
if err := g.fs.MkdirAll(modPath, os.ModeDir|os.ModePerm); err != nil {
diskRef := newDiskRef(g.fs, goPathRoot, "", "")
diskRef.Clear()
return nil, err
}
// setup the module with barebones stuff
if err := Dummy(g.fs, modPath); err != nil {
diskRef := newDiskRef(g.fs, goPathRoot, "", "")
diskRef.Clear()
return nil, err
}
err = getSources(g.goBinaryName, g.fs, goPathRoot, modPath, mod, ver)
if err != nil {
diskRef := newDiskRef(g.fs, goPathRoot, "", "")
diskRef.Clear()
return nil, err
}
return newDiskRef(g.fs, goPathRoot, mod, ver), nil
}
// Dummy Hacky thing makes vgo not to complain
func Dummy(fs afero.Fs, repoRoot string) error {
const op errors.Op = "module.Dummy"
// vgo expects go.mod file present with module statement or .go file with import comment
gomodPath := filepath.Join(repoRoot, "go.mod")
gomodContent := []byte("module mod")
if err := afero.WriteFile(fs, gomodPath, gomodContent, 0666); err != nil {
return errors.E(op, err)
}
sourcePath := filepath.Join(repoRoot, "mod.go")
sourceContent := []byte("package mod")
if err := afero.WriteFile(fs, sourcePath, sourceContent, 0666); err != nil {
return errors.E(op, err)
}
return nil
}
// given a filesystem, gopath, repository root, module and version, runs 'vgo get'
// on module@version from the repoRoot with GOPATH=gopath, and returns a non-nil error if anything went wrong.
func getSources(goBinaryName string, fs afero.Fs, gopath, repoRoot, module, version string) error {
uri := strings.TrimSuffix(module, "/")
fullURI := fmt.Sprintf("%s@%s", uri, version)
gopathEnv := fmt.Sprintf("GOPATH=%s", gopath)
cacheEnv := fmt.Sprintf("GOCACHE=%s", filepath.Join(gopath, "cache"))
disableCgo := "CGO_ENABLED=0"
enableGoModules := "GO111MODULE=on"
cmd := exec.Command(goBinaryName, "get", fullURI)
// PATH is needed for vgo to recognize vcs binaries
// this breaks windows.
cmd.Env = []string{"PATH=" + os.Getenv("PATH"), gopathEnv, cacheEnv, disableCgo, enableGoModules}
cmd.Dir = repoRoot
o, err := cmd.CombinedOutput()
if err != nil {
errMsg := fmt.Sprintf("%v : %s", err, o)
// github quota exceeded
if isLimitHit(o) {
return errors.E("module.getSources", errMsg, errors.KindRateLimit)
}
// another error in the output
return errors.E("module.getSources", errMsg)
}
// make sure the expected files exist
packagePath := getPackagePath(gopath, module)
return checkFiles(fs, packagePath, version)
}
func checkFiles(fs afero.Fs, path, version string) error {
if _, err := fs.Stat(filepath.Join(path, version+".mod")); err != nil {
return pkgerrors.WithMessage(err, fmt.Sprintf("%s.mod not found in %s", version, path))
}
if _, err := fs.Stat(filepath.Join(path, version+".zip")); err != nil {
return pkgerrors.WithMessage(err, fmt.Sprintf("%s.zip not found in %s", version, path))
}
if _, err := fs.Stat(filepath.Join(path, version+".info")); err != nil {
return pkgerrors.WithMessage(err, fmt.Sprintf("%s.info not found in %s", version, path))
}
return nil
}
func isLimitHit(o []byte) bool {
return bytes.Contains(o, []byte("403 response from api.github.com"))
}
// getRepoDirName takes a raw repository URI and a version and creates a directory name that the
// repository contents can be put into
func getRepoDirName(repoURI, version string) string {
escapedURI := strings.Replace(repoURI, "/", "-", -1)
return fmt.Sprintf("%s-%s", escapedURI, version)
}
// getPackagePath returns the path to the module cache given the gopath and module name
func getPackagePath(gopath, module string) string {
return filepath.Join(gopath, "src", "mod", "cache", "download", module, "@v")
}