Skip to content

Commit 6074d96

Browse files
committed
cmd/go-cache-plugin: add optional module proxy support
The "serve" command now has flags to enable a --modproxy running as an HTTP server alongside the build cache, which can optionally also proxy for (and cache) Go sum database queries (with --sumdb).
1 parent 43ebf09 commit 6074d96

File tree

2 files changed

+89
-19
lines changed

2 files changed

+89
-19
lines changed

cmd/go-cache-plugin/commands.go

Lines changed: 82 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,17 @@ package main
33
import (
44
"context"
55
"errors"
6+
"expvar"
67
"fmt"
78
"io"
89
"log"
910
"net"
11+
"net/http"
1012
"os"
1113
"os/signal"
14+
"path"
15+
"path/filepath"
16+
"strings"
1217
"syscall"
1318
"time"
1419

@@ -18,7 +23,10 @@ import (
1823
"github.com/creachadair/gocache"
1924
"github.com/creachadair/gocache/cachedir"
2025
"github.com/creachadair/taskgroup"
26+
"github.com/goproxy/goproxy"
2127
"github.com/tailscale/go-cache-plugin/s3cache"
28+
"github.com/tailscale/go-cache-plugin/s3proxy"
29+
"tailscale.com/tsweb"
2230
)
2331

2432
var flags struct {
@@ -35,26 +43,26 @@ var flags struct {
3543
DebugLog bool `flag:"debug,default=$GOCACHE_DEBUG,Enable detailed per-request debug logging (noisy)"`
3644
}
3745

38-
func initCacheServer(env *command.Env) (*gocache.Server, error) {
46+
func initCacheServer(env *command.Env) (*gocache.Server, *s3.Client, error) {
3947
switch {
4048
case flags.CacheDir == "":
41-
return nil, env.Usagef("you must provide a --cache-dir")
49+
return nil, nil, env.Usagef("you must provide a --cache-dir")
4250
case flags.S3Bucket == "":
43-
return nil, env.Usagef("you must provide an S3 --bucket name")
51+
return nil, nil, env.Usagef("you must provide an S3 --bucket name")
4452
}
4553
region, err := getBucketRegion(env.Context(), flags.S3Bucket)
4654
if err != nil {
47-
return nil, env.Usagef("you must provide an S3 --region name")
55+
return nil, nil, env.Usagef("you must provide an S3 --region name")
4856
}
4957

5058
dir, err := cachedir.New(flags.CacheDir)
5159
if err != nil {
52-
return nil, fmt.Errorf("create local cache: %w", err)
60+
return nil, nil, fmt.Errorf("create local cache: %w", err)
5361
}
5462

5563
cfg, err := config.LoadDefaultConfig(env.Context(), config.WithRegion(region))
5664
if err != nil {
57-
return nil, fmt.Errorf("laod AWS config: %w", err)
65+
return nil, nil, fmt.Errorf("laod AWS config: %w", err)
5866
}
5967

6068
vprintf("local cache directory: %s", flags.CacheDir)
@@ -67,6 +75,8 @@ func initCacheServer(env *command.Env) (*gocache.Server, error) {
6775
MinUploadSize: flags.MinUploadSize,
6876
UploadConcurrency: flags.S3Concurrency,
6977
}
78+
cache.SetMetrics(env.Context(), expvar.NewMap("gocache_host"))
79+
7080
close := cache.Close
7181
if flags.Expiration > 0 {
7282
dirClose := dir.Cleanup(flags.Expiration)
@@ -83,13 +93,14 @@ func initCacheServer(env *command.Env) (*gocache.Server, error) {
8393
Logf: vprintf,
8494
LogRequests: flags.DebugLog,
8595
}
86-
return s, nil
96+
expvar.Publish("gocache_server", s.Metrics().Get("server"))
97+
return s, cache.S3Client, nil
8798
}
8899

89100
// runDirect runs a cache communicating on stdin/stdout, for use as a direct
90101
// GOCACHEPROG plugin.
91102
func runDirect(env *command.Env) error {
92-
s, err := initCacheServer(env)
103+
s, _, err := initCacheServer(env)
93104
if err != nil {
94105
return err
95106
}
@@ -102,33 +113,35 @@ func runDirect(env *command.Env) error {
102113
return nil
103114
}
104115

105-
var remoteFlags struct {
106-
Socket string `flag:"socket,default=$GOCACHE_SOCKET,Socket path (required)"`
116+
var serveFlags struct {
117+
Socket string `flag:"socket,default=$GOCACHE_SOCKET,Socket path (required)"`
118+
ModProxy string `flag:"modproxy,default=$GOCACHE_MODPROXY,Module proxy service address ([host]:port)"`
119+
SumDB string `flag:"sumdb,default=$GOCACHE_SUMDB,SumDB servers to proxy for (comma-separated)"`
107120
}
108121

109122
func noopClose(context.Context) error { return nil }
110123

111-
// runRemote runs a cache communicating over a Unix-domain socket.
112-
func runRemote(env *command.Env) error {
113-
if remoteFlags.Socket == "" {
124+
// runServe runs a cache communicating over a Unix-domain socket.
125+
func runServe(env *command.Env) error {
126+
if serveFlags.Socket == "" {
114127
return env.Usagef("you must provide a --socket path")
115128
}
116129

117130
// Initialize the cache server. Unlike a direct server, only close down and
118131
// wait for cache cleanup when the whole process exits.
119-
s, err := initCacheServer(env)
132+
s, s3c, err := initCacheServer(env)
120133
if err != nil {
121134
return err
122135
}
123136
closeHook := s.Close
124137
s.Close = noopClose
125138

126139
// Listen for connections from the Go toolchain on the specified socket.
127-
lst, err := net.Listen("unix", remoteFlags.Socket)
140+
lst, err := net.Listen("unix", serveFlags.Socket)
128141
if err != nil {
129142
return fmt.Errorf("listen: %w", err)
130143
}
131-
defer os.Remove(remoteFlags.Socket) // best-effort
144+
defer os.Remove(serveFlags.Socket) // best-effort
132145

133146
ctx, cancel := signal.NotifyContext(env.Context(), syscall.SIGINT, syscall.SIGTERM)
134147
defer cancel()
@@ -138,7 +151,60 @@ func runRemote(env *command.Env) error {
138151
lst.Close()
139152
}()
140153

154+
// If a module proxy is enabled, start it.
141155
var g taskgroup.Group
156+
if serveFlags.ModProxy != "" {
157+
modCachePath := filepath.Join(flags.CacheDir, "module")
158+
if err := os.MkdirAll(modCachePath, 0700); err != nil {
159+
lst.Close()
160+
return fmt.Errorf("create module cache: %w", err)
161+
}
162+
cacher := &s3proxy.Cacher{
163+
Local: modCachePath,
164+
S3Client: s3c,
165+
S3Bucket: flags.S3Bucket,
166+
KeyPrefix: path.Join(flags.KeyPrefix, "module"),
167+
MaxTasks: flags.S3Concurrency,
168+
LogRequests: flags.DebugLog,
169+
Logf: vprintf,
170+
}
171+
defer func() {
172+
vprintf("close cacher (err=%v)", cacher.Close())
173+
}()
174+
proxy := &goproxy.Goproxy{
175+
Fetcher: &goproxy.GoFetcher{
176+
// As configured, the fetcher should never shell out to the go
177+
// tool. Specifically, because we set GOPROXY and do not set any
178+
// bypass via GONOPROXY, GOPRIVATE, etc., we will only attempt to
179+
// proxy for the specific server(s) listed in Env.
180+
GoBin: "/bin/false",
181+
Env: []string{"GOPROXY=https://proxy.golang.org"},
182+
},
183+
Cacher: cacher,
184+
}
185+
if serveFlags.SumDB != "" {
186+
proxy.ProxiedSumDBs = strings.Split(serveFlags.SumDB, ",")
187+
vprintf("enabling sum DB proxy for %s", strings.Join(proxy.ProxiedSumDBs, ", "))
188+
}
189+
expvar.Publish("modcache", cacher.Metrics())
190+
191+
// Run an HTTP server exporting the proxy and debug metrics.
192+
mux := http.NewServeMux()
193+
mux.Handle("/", proxy)
194+
tsweb.Debugger(mux)
195+
srv := &http.Server{
196+
Addr: serveFlags.ModProxy,
197+
Handler: mux,
198+
}
199+
g.Go(srv.ListenAndServe)
200+
vprintf("started module proxy at %q", serveFlags.ModProxy)
201+
go func() {
202+
<-ctx.Done()
203+
vprintf("signal received, stopping module proxy")
204+
srv.Shutdown(context.Background())
205+
}()
206+
}
207+
142208
for {
143209
conn, err := lst.Accept()
144210
if err != nil {

cmd/go-cache-plugin/go-cache-plugin.go

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,14 @@ the --cache-dir flag or GOCACHE_DIR environment.`,
4545
4646
In this mode, the cache server listens for connections on a socket instead of
4747
serving directly over stdin/stdout. The "connect" command adapts the direct
48-
interface to this one.`,
48+
interface to this one.
4949
50-
SetFlags: command.Flags(flax.MustBind, &remoteFlags),
51-
Run: command.Adapt(runRemote),
50+
By default, only the build cache is exported via the --socket path.
51+
If --modcache is set, the server also exports a caching module proxy at the
52+
specified address.`,
53+
54+
SetFlags: command.Flags(flax.MustBind, &serveFlags),
55+
Run: command.Adapt(runServe),
5256
},
5357
{
5458
Name: "connect",

0 commit comments

Comments
 (0)