cmd/pkgsite: serve modules from the build list
Instead of serving just the module at a given path, serve all the
modules on its build list too (the result of go list -m all).
Change-Id: Ibd899d0a7bf23abd3e81882c2a1a62f2f63570a2
Reviewed-on: https://go-review.googlesource.com/c/pkgsite/+/386194
Trust: Jonathan Amsterdam <jba@google.com>
Run-TryBot: Jonathan Amsterdam <jba@google.com>
TryBot-Result: kokoro <noreply+kokoro@google.com>
Reviewed-by: Jamal Carvalho <jamal@golang.org>
diff --git a/cmd/pkgsite/main.go b/cmd/pkgsite/main.go
index af8ebba..43e2786 100644
--- a/cmd/pkgsite/main.go
+++ b/cmd/pkgsite/main.go
@@ -13,6 +13,10 @@
//
// cd ~/repos/cue && pkgsite
//
+// This form will also serve all of the module's required modules at their
+// required versions. You can disable serving the required modules by passing
+// -list=false.
+//
// You can also serve docs from your module cache, directly from the proxy
// (it uses the GOPROXY environment variable), or both:
//
@@ -31,12 +35,15 @@
package main
import (
+ "bytes"
"context"
+ "encoding/json"
"flag"
"fmt"
"net/http"
"os"
"os/exec"
+ "path/filepath"
"strings"
"time"
@@ -57,12 +64,13 @@
const defaultAddr = "localhost:8080" // default webserver address
var (
- gopathMode = flag.Bool("gopath_mode", false, "assume that local modules' paths are relative to GOPATH/src")
- httpAddr = flag.String("http", defaultAddr, "HTTP service address to listen for incoming requests on")
- useCache = flag.Bool("cache", false, "fetch from the module cache")
- cacheDir = flag.String("cachedir", "", "module cache directory (defaults to `go env GOMODCACHE`)")
- useProxy = flag.Bool("proxy", false, "fetch from GOPROXY if not found locally")
- goRepoPath = flag.String("gorepo", "", "path to Go repo on local filesystem")
+ gopathMode = flag.Bool("gopath_mode", false, "assume that local modules' paths are relative to GOPATH/src")
+ httpAddr = flag.String("http", defaultAddr, "HTTP service address to listen for incoming requests on")
+ useCache = flag.Bool("cache", false, "fetch from the module cache")
+ cacheDir = flag.String("cachedir", "", "module cache directory (defaults to `go env GOMODCACHE`)")
+ useProxy = flag.Bool("proxy", false, "fetch from GOPROXY if not found locally")
+ goRepoPath = flag.String("gorepo", "", "path to Go repo on local filesystem")
+ useListedMods = flag.Bool("list", true, "for each path, serve all modules in build list")
)
func main() {
@@ -82,7 +90,7 @@
}
var modCacheDir string
- if *useCache {
+ if *useCache || *useListedMods {
modCacheDir = *cacheDir
if modCacheDir == "" {
var err error
@@ -99,6 +107,7 @@
if *useCache || *useProxy {
fmt.Fprintf(os.Stderr, "BYPASSING LICENSE CHECKING: MAY DISPLAY NON-REDISTRIBUTABLE INFORMATION\n")
}
+
var prox *proxy.Client
if *useProxy {
url := os.Getenv("GOPROXY")
@@ -116,7 +125,16 @@
stdlib.SetGoRepoPath(*goRepoPath)
}
- server, err := newServer(ctx, paths, *gopathMode, modCacheDir, prox)
+ var cacheMods []internal.Modver
+ if *useListedMods && !*useCache {
+ var err error
+ paths, cacheMods, err = listModsForPaths(paths, modCacheDir)
+ if err != nil {
+ die("listing mods (consider passing -list=false): %v", err)
+ }
+ }
+
+ server, err := newServer(ctx, paths, *gopathMode, modCacheDir, cacheMods, prox)
if err != nil {
die("%s", err)
}
@@ -141,10 +159,10 @@
return paths
}
-func newServer(ctx context.Context, paths []string, gopathMode bool, downloadDir string, prox *proxy.Client) (*frontend.Server, error) {
+func newServer(ctx context.Context, paths []string, gopathMode bool, downloadDir string, cacheMods []internal.Modver, prox *proxy.Client) (*frontend.Server, error) {
getters := buildGetters(ctx, paths, gopathMode)
if downloadDir != "" {
- g, err := fetch.NewFSProxyModuleGetter(downloadDir)
+ g, err := fetch.NewFSProxyModuleGetter(downloadDir, cacheMods)
if err != nil {
return nil, err
}
@@ -204,9 +222,63 @@
}
func defaultCacheDir() (string, error) {
- out, err := exec.Command("go", "env", "GOMODCACHE").CombinedOutput()
+ out, err := runGo("", "env", "GOMODCACHE")
if err != nil {
- return "", fmt.Errorf("running 'go env GOMODCACHE': %v: %s", err, out)
+ return "", err
}
return strings.TrimSpace(string(out)), nil
}
+
+// listedMod has a subset of the fields written by `go list -m`.
+type listedMod struct {
+ internal.Modver
+ GoMod string // absolute path to go.mod file; in download cache or replaced
+}
+
+var listModules = _listModules
+
+func _listModules(dir string) ([]listedMod, error) {
+ out, err := runGo(dir, "list", "-json", "-m", "all")
+ if err != nil {
+ return nil, err
+ }
+ d := json.NewDecoder(bytes.NewReader(out))
+ var ms []listedMod
+ for d.More() {
+ var m listedMod
+ if err := d.Decode(&m); err != nil {
+ return nil, err
+ }
+ ms = append(ms, m)
+ }
+ return ms, nil
+}
+
+func runGo(dir string, args ...string) ([]byte, error) {
+ cmd := exec.Command("go", args...)
+ cmd.Dir = dir
+ out, err := cmd.Output()
+ if err != nil {
+ return nil, fmt.Errorf("running go with %q: %v: %s", args, err, out)
+ }
+ return out, nil
+}
+
+func listModsForPaths(paths []string, cacheDir string) ([]string, []internal.Modver, error) {
+ var outPaths []string
+ var cacheMods []internal.Modver
+ for _, p := range paths {
+ lms, err := listModules(p)
+ if err != nil {
+ return nil, nil, err
+ }
+ for _, lm := range lms {
+ if strings.HasPrefix(lm.GoMod, cacheDir) {
+ cacheMods = append(cacheMods, lm.Modver)
+ } else { // probably the result of a replace directive
+ outPaths = append(outPaths, filepath.Dir(lm.GoMod))
+ }
+ }
+ }
+ return outPaths, cacheMods, nil
+}
diff --git a/cmd/pkgsite/main_test.go b/cmd/pkgsite/main_test.go
index 9a84ee4..86d5cab 100644
--- a/cmd/pkgsite/main_test.go
+++ b/cmd/pkgsite/main_test.go
@@ -16,6 +16,7 @@
"github.com/google/go-cmp/cmp"
"golang.org/x/net/html"
+ "golang.org/x/pkgsite/internal"
"golang.org/x/pkgsite/internal/proxy/proxytest"
"golang.org/x/pkgsite/internal/testing/htmlcheck"
)
@@ -48,7 +49,7 @@
prox, teardown := proxytest.SetupTestClient(t, testModules)
defer teardown()
- server, err := newServer(context.Background(), []string{localModule}, false, cacheDir, prox)
+ server, err := newServer(context.Background(), []string{localModule}, false, cacheDir, nil, prox)
if err != nil {
t.Fatal(err)
}
@@ -122,3 +123,29 @@
t.Errorf("got %v, want %v", got, want)
}
}
+
+func TestListModsForPaths(t *testing.T) {
+ listModules = func(string) ([]listedMod, error) {
+ return []listedMod{
+ {
+ internal.Modver{Path: "m1", Version: "v1.2.3"},
+ "/dir/cache/download/m1/@v/v1.2.3.mod",
+ },
+ {
+ internal.Modver{Path: "m2", Version: "v1.0.0"},
+ "/repos/m2/go.mod",
+ },
+ }, nil
+ }
+ defer func() { listModules = _listModules }()
+
+ gotPaths, gotCacheMods, err := listModsForPaths([]string{"m1"}, "/dir")
+ if err != nil {
+ t.Fatal(err)
+ }
+ wantPaths := []string{"/repos/m2"}
+ wantCacheMods := []internal.Modver{{Path: "m1", Version: "v1.2.3"}}
+ if !cmp.Equal(gotPaths, wantPaths) || !cmp.Equal(gotCacheMods, wantCacheMods) {
+ t.Errorf("got\n%v, %v\nwant\n%v, %v", gotPaths, gotCacheMods, wantPaths, wantCacheMods)
+ }
+}
diff --git a/internal/fetch/getters.go b/internal/fetch/getters.go
index e02d900..4fb74ef 100644
--- a/internal/fetch/getters.go
+++ b/internal/fetch/getters.go
@@ -23,6 +23,7 @@
"golang.org/x/mod/modfile"
"golang.org/x/mod/module"
+ "golang.org/x/pkgsite/internal"
"golang.org/x/pkgsite/internal/derrors"
"golang.org/x/pkgsite/internal/proxy"
"golang.org/x/pkgsite/internal/source"
@@ -194,19 +195,29 @@
// paths that correspond to proxy URLs. An example of such a directory is
// $(go env GOMODCACHE).
type fsProxyModuleGetter struct {
- dir string
+ dir string
+ allowed map[internal.Modver]bool
}
// NewFSModuleGetter return a ModuleGetter that reads modules from a filesystem
// directory organized like the proxy.
-func NewFSProxyModuleGetter(dir string) (_ *fsProxyModuleGetter, err error) {
+// If allowed is non-empty, only module@versions in allowed are served; others
+// result in NotFound errors.
+func NewFSProxyModuleGetter(dir string, allowed []internal.Modver) (_ *fsProxyModuleGetter, err error) {
defer derrors.Wrap(&err, "NewFSProxyModuleGetter(%q)", dir)
abs, err := filepath.Abs(dir)
if err != nil {
return nil, err
}
- return &fsProxyModuleGetter{dir: abs}, nil
+ g := &fsProxyModuleGetter{dir: abs}
+ if len(allowed) > 0 {
+ g.allowed = map[internal.Modver]bool{}
+ for _, a := range allowed {
+ g.allowed[a] = true
+ }
+ }
+ return g, nil
}
// Info returns basic information about the module.
@@ -248,7 +259,7 @@
}
}
- // Check that .zip is readable first.
+ // Check that .zip is readable.
f, err := g.openFile(path, vers, "zip")
if err != nil {
return nil, err
@@ -267,6 +278,7 @@
return nil, err
}
}
+
data, err := g.readFile(path, vers, "zip")
if err != nil {
return nil, err
@@ -307,14 +319,29 @@
}
var versions []string
for _, z := range zips {
- versions = append(versions, strings.TrimSuffix(filepath.Base(z), ".zip"))
+ vers := strings.TrimSuffix(filepath.Base(z), ".zip")
+ if g.allow(modulePath, vers) {
+ versions = append(versions, vers)
+ }
+ }
+ if len(versions) == 0 {
+ return "", fmt.Errorf("no allowed versions form module %q: %w", modulePath, derrors.NotFound)
}
return version.LatestOf(versions), nil
}
+// allow reports whether the module path and version may be served.
+func (g *fsProxyModuleGetter) allow(path, vers string) bool {
+ return g.allowed == nil || g.allowed[internal.Modver{Path: path, Version: vers}]
+}
+
func (g *fsProxyModuleGetter) readFile(path, version, suffix string) (_ []byte, err error) {
defer derrors.Wrap(&err, "fsProxyModuleGetter.readFile(%q, %q, %q)", path, version, suffix)
+ if !g.allow(path, version) {
+ return nil, fmt.Errorf("%s@%s not allowed: %w", path, version, derrors.NotFound)
+ }
+
f, err := g.openFile(path, version, suffix)
if err != nil {
return nil, err
diff --git a/internal/fetch/getters_test.go b/internal/fetch/getters_test.go
index 92c47ff..5d3dbb8 100644
--- a/internal/fetch/getters_test.go
+++ b/internal/fetch/getters_test.go
@@ -13,6 +13,7 @@
"time"
"github.com/google/go-cmp/cmp"
+ "golang.org/x/pkgsite/internal"
"golang.org/x/pkgsite/internal/derrors"
"golang.org/x/pkgsite/internal/proxy"
"golang.org/x/pkgsite/internal/version"
@@ -47,7 +48,7 @@
"dir/cache/download/github.com/a!bc/@v/v2.3.4.zip",
},
} {
- g, err := NewFSProxyModuleGetter("dir")
+ g, err := NewFSProxyModuleGetter("dir", nil)
if err != nil {
t.Fatal(err)
}
@@ -76,7 +77,7 @@
if err != nil {
t.Fatal(err)
}
- g, err := NewFSProxyModuleGetter("testdata/modcache")
+ g, err := NewFSProxyModuleGetter("testdata/modcache", nil)
if err != nil {
t.Fatal(err)
}
@@ -142,3 +143,37 @@
}
})
}
+
+func TestFSProxyGetterAllowed(t *testing.T) {
+ ctx := context.Background()
+ // The cache contains:
+ // github.com/jackc/pgio@v1.0.0
+ // modcache.com@v1.0.0
+ // Allow only the latter.
+ allow := []internal.Modver{{Path: "modcache.com", Version: "v1.0.0"}}
+ g, err := NewFSProxyModuleGetter("testdata/modcache", allow)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ check := func(path, vers string, wantOK bool) {
+ t.Helper()
+ _, err := g.Info(ctx, path, vers)
+ if (err == nil) != wantOK {
+ s := "a"
+ if wantOK {
+ s = "no"
+ }
+ t.Errorf("got %v; wanted %s error", err, s)
+ } else if err != nil && !errors.Is(err, derrors.NotFound) {
+ t.Errorf("got %v, wanted NotFound", err)
+ }
+ }
+
+ // We can get modcache.com.
+ check("modcache.com", "v1.0.0", true)
+ check("modcache.com", "latest", true)
+ // We get NotFound for jackc.
+ check("github.com/jackc/pgio", "v1.0.0", false)
+ check("github.com/jackc/pgio", "latest", false)
+}