Skip to content

Commit bb2ec89

Browse files
committed
generator/gomod: support GOPROXY build arg
Add DALEC_GOMOD_PROXY as a known frontend build arg and carry it through SourceOpts into gomod dependency generation and gomod patch preprocessing. Signed-off-by: Kartik Joshi <kartikjoshi@microsoft.com>
1 parent 91a8ea6 commit bb2ec89

8 files changed

Lines changed: 198 additions & 7 deletions

File tree

determinism_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,7 @@ func TestGomodPatchEnvProducesDeterministicLLB(t *testing.T) {
222222
worker := llb.Scratch()
223223

224224
build := func() llb.State {
225-
st, err := spec.generateGomodPatchStateForSource("src", gen, base, worker, nil)
225+
st, err := spec.generateGomodPatchStateForSource("src", gen, base, worker, nil, "")
226226
assert.NilError(t, err)
227227
assert.Assert(t, st != nil)
228228
return *st

frontend/gateway.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,10 @@ func SourceOptFromUIClient(ctx context.Context, c gwclient.Client, dc *dockerui.
134134
return loadSourceFilterConfig(ctx, c, sOpt.GetContext)
135135
})
136136

137+
if gomodProxy, ok := GetBuildArg(c, dalec.BuildArgDalecGomodProxy); ok {
138+
sOpt.GomodProxy = gomodProxy
139+
}
140+
137141
return sOpt
138142
}
139143

frontend/gateway_test.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package frontend
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/project-dalec/dalec"
8+
)
9+
10+
func TestSourceOptFromUIClientReadsGomodProxyBuildArg(t *testing.T) {
11+
t.Parallel()
12+
13+
const proxy = "http://proxy.example:5000,direct"
14+
client := newStubClient()
15+
client.opts["build-arg:"+dalec.BuildArgDalecGomodProxy] = proxy
16+
17+
sOpt := SourceOptFromUIClient(context.Background(), client, nil, nil)
18+
if sOpt.GomodProxy != proxy {
19+
t.Fatalf("expected GomodProxy %q, got %q", proxy, sOpt.GomodProxy)
20+
}
21+
}

generator_gomod.go

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ const (
1919
// GoModCacheKey is the key used to identify the go module cache in the buildkit cache.
2020
// It is exported only for testing purposes.
2121
GomodCacheKey = "dalec-gomod-proxy-cache"
22+
// BuildArgDalecGomodProxy is the frontend build arg used to override GOPROXY
23+
// for gomod dependency generation.
24+
BuildArgDalecGomodProxy = "DALEC_GOMOD_PROXY"
2225
)
2326

2427
func (g *GeneratorGomod) processBuildArgs(args map[string]string, allowArg func(key string) bool) error {
@@ -63,7 +66,7 @@ func (s *Spec) HasGomods() bool {
6366
return false
6467
}
6568

66-
func withGomod(g *SourceGenerator, srcSt, worker llb.State, subPath string, credHelper llb.RunOption, opts ...llb.ConstraintsOpt) func(llb.State) llb.State {
69+
func withGomod(g *SourceGenerator, srcSt, worker llb.State, subPath string, credHelper llb.RunOption, gomodProxy string, opts ...llb.ConstraintsOpt) func(llb.State) llb.State {
6770
return func(in llb.State) llb.State {
6871
const (
6972
workDir = "/work/src"
@@ -86,12 +89,12 @@ func withGomod(g *SourceGenerator, srcSt, worker llb.State, subPath string, cred
8689
scriptPath := filepath.Join(scriptMountpoint, gomodDownloadWrapperBasename)
8790

8891
for _, path := range paths {
89-
in = worker.Run(
92+
runOpts := []llb.RunOption{
9093
// First download the go module deps to our persistent cache
9194
// Then set the GOPROXY to the cache dir so that we can extract just the deps we need
9295
// This allows us to persist the module cache across builds and avoid downloading
9396
// the same deps over and over again.
94-
ShArgs(`set -e; GOMODCACHE="${TMP_GOMODCACHE}" `+scriptPath+`; GOPROXY="file://${TMP_GOMODCACHE}/cache/download" `+scriptPath),
97+
ShArgs(`set -e; GOMODCACHE="${TMP_GOMODCACHE}" ` + scriptPath + `; GOPROXY="file://${TMP_GOMODCACHE}/cache/download" ` + scriptPath),
9598
g.withGomodSecretsAndSockets(),
9699
llb.AddMount(scriptMountpoint, script),
97100
llb.AddEnv("GOPATH", "/go"),
@@ -103,6 +106,13 @@ func withGomod(g *SourceGenerator, srcSt, worker llb.State, subPath string, cred
103106
llb.AddMount(proxyPath, llb.Scratch(), llb.AsPersistentCacheDir(GomodCacheKey, llb.CacheMountShared)),
104107
WithConstraints(opts...),
105108
g.Gomod._sourceMap.GetLocation(in),
109+
}
110+
if gomodProxy != "" {
111+
runOpts = append(runOpts, llb.AddEnv("GOPROXY", gomodProxy))
112+
}
113+
114+
in = worker.Run(
115+
runOpts...,
106116
).AddMount(gomodCacheDir, in)
107117
}
108118

@@ -238,7 +248,7 @@ func (s *Spec) GomodDeps(sOpt SourceOpts, worker llb.State, opts ...llb.Constrai
238248
deps = deps.With(func(in llb.State) llb.State {
239249
for _, gen := range src.Generate {
240250
if gen.Gomod != nil {
241-
in = in.With(withGomod(gen, patched[key], worker, key, credHelperRunOpt, opts...))
251+
in = in.With(withGomod(gen, patched[key], worker, key, credHelperRunOpt, sOpt.GomodProxy, opts...))
242252
}
243253
}
244254
return in

generator_gomod_test.go

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
package dalec
22

33
import (
4+
"context"
45
"encoding/json"
6+
"slices"
7+
"strings"
58
"testing"
69

710
"github.com/goccy/go-yaml"
11+
"github.com/moby/buildkit/client/llb"
812
)
913

1014
func TestGomodReplaceUnmarshal(t *testing.T) {
@@ -210,3 +214,145 @@ func TestGomodReplaceGoModEditArg(t *testing.T) {
210214
})
211215
}
212216
}
217+
218+
func TestGomodDepsUsesGomodProxy(t *testing.T) {
219+
t.Parallel()
220+
221+
const proxy = "http://proxy.example:5000,direct"
222+
spec := testGomodProxySpec()
223+
st := spec.GomodDeps(testGomodProxySourceOpts(proxy), llb.Scratch())
224+
if st == nil {
225+
t.Fatal("gomod generator succeeded but returned nil state")
226+
}
227+
228+
env := gomodDownloadExecEnv(context.Background(), t, *st)
229+
if !slices.Contains(env, "GOPROXY="+proxy) {
230+
t.Fatalf("expected gomod exec env to include GOPROXY=%q, got %v", proxy, env)
231+
}
232+
}
233+
234+
func TestGomodDepsSkipsEmptyGomodProxy(t *testing.T) {
235+
t.Parallel()
236+
237+
spec := testGomodProxySpec()
238+
st := spec.GomodDeps(testGomodProxySourceOpts(""), llb.Scratch())
239+
if st == nil {
240+
t.Fatal("gomod generator succeeded but returned nil state")
241+
}
242+
243+
env := gomodDownloadExecEnv(context.Background(), t, *st)
244+
for _, item := range env {
245+
if strings.HasPrefix(item, "GOPROXY=") {
246+
t.Fatalf("expected empty gomod proxy to omit GOPROXY, got %v", env)
247+
}
248+
}
249+
}
250+
251+
func TestGomodProxyBuildArgIsKnown(t *testing.T) {
252+
t.Parallel()
253+
254+
spec := &Spec{}
255+
err := spec.SubstituteArgs(map[string]string{
256+
BuildArgDalecGomodProxy: "http://proxy.example:5000",
257+
})
258+
if err != nil {
259+
t.Fatal(err)
260+
}
261+
}
262+
263+
func TestGomodPatchUsesGomodProxy(t *testing.T) {
264+
t.Parallel()
265+
266+
const proxy = "http://proxy.example:5000,direct"
267+
gen := &SourceGenerator{
268+
Gomod: &GeneratorGomod{
269+
Edits: &GomodEdits{
270+
Replace: []GomodReplace{
271+
{Original: "example.com/old", Update: "example.com/new v1.2.3"},
272+
},
273+
},
274+
},
275+
}
276+
277+
st, err := (&Spec{}).generateGomodPatchStateForSource("src", gen, llb.Scratch(), llb.Scratch(), nil, proxy)
278+
if err != nil {
279+
t.Fatal(err)
280+
}
281+
if st == nil {
282+
t.Fatal("gomod patch generation succeeded but returned nil state")
283+
}
284+
285+
env := gomodPatchExecEnv(context.Background(), t, *st)
286+
if !slices.Contains(env, "GOPROXY="+proxy) {
287+
t.Fatalf("expected gomod patch exec env to include GOPROXY=%q, got %v", proxy, env)
288+
}
289+
}
290+
291+
func testGomodProxySpec() *Spec {
292+
return &Spec{
293+
Sources: map[string]Source{
294+
"src": {
295+
Git: &SourceGit{
296+
URL: "https://example.com/repo.git",
297+
Commit: "0123456789abcdef",
298+
},
299+
Generate: []*SourceGenerator{
300+
{Gomod: &GeneratorGomod{}},
301+
},
302+
},
303+
},
304+
}
305+
}
306+
307+
func testGomodProxySourceOpts(proxy string) SourceOpts {
308+
return SourceOpts{
309+
GomodProxy: proxy,
310+
GetContext: func(name string, opts ...llb.LocalOption) (*llb.State, error) {
311+
st := llb.Local(name, opts...)
312+
return &st, nil
313+
},
314+
GitCredHelperOpt: func() (llb.RunOption, error) {
315+
st := llb.Scratch().File(llb.Mkfile("/frontend", 0o755, []byte("#!/usr/bin/env bash\nexit 0\n")))
316+
return RunOptFunc(func(ei *llb.ExecInfo) {
317+
llb.AddMount("/usr/local/bin/frontend", st, llb.SourcePath("/frontend")).SetRunOption(ei)
318+
}), nil
319+
},
320+
}
321+
}
322+
323+
func gomodDownloadExecEnv(ctx context.Context, t *testing.T, st llb.State) []string {
324+
t.Helper()
325+
326+
for _, op := range sourceOpsFromState(ctx, t, st) {
327+
exec := op.GetExec()
328+
if exec == nil {
329+
continue
330+
}
331+
332+
env := exec.Meta.Env
333+
if slices.Contains(env, "GOPATH=/go") && slices.Contains(env, "TMP_GOMODCACHE=/tmp/dalec/gomod-proxy-cache") {
334+
return env
335+
}
336+
}
337+
338+
t.Fatal("expected gomod download exec")
339+
return nil
340+
}
341+
342+
func gomodPatchExecEnv(ctx context.Context, t *testing.T, st llb.State) []string {
343+
t.Helper()
344+
345+
for _, op := range sourceOpsFromState(ctx, t, st) {
346+
exec := op.GetExec()
347+
if exec == nil {
348+
continue
349+
}
350+
351+
if slices.Contains(exec.Meta.Args, "/gomod-patch.sh") {
352+
return exec.Meta.Env
353+
}
354+
}
355+
356+
t.Fatal("expected gomod patch exec")
357+
return nil
358+
}

load.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ func knownArg(key string) bool {
5151
return true
5252
case "DALEC_SOURCE_FILTER_CONFIG_CONTEXT_NAME":
5353
return true
54+
case BuildArgDalecGomodProxy:
55+
return true
5456
case KeyDalecTarget:
5557
return true
5658
}

preprocess.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ func (s *Spec) preprocessGomodEdits(sOpt SourceOpts, worker llb.State, opts ...l
6868
}
6969

7070
// Generate patch state (LLB state, not solved bytes)
71-
patchSt, err := s.generateGomodPatchStateForSource(sourceName, gen, baseState, worker, credHelper, opts...)
71+
patchSt, err := s.generateGomodPatchStateForSource(sourceName, gen, baseState, worker, credHelper, sOpt.GomodProxy, opts...)
7272
if err != nil {
7373
return errors.Wrapf(err, "failed to generate gomod patch state for source %s", sourceName)
7474
}
@@ -220,7 +220,7 @@ func buildGomodPatchEnv(editArgs string, paths []string, gen *SourceGenerator, s
220220
// generateGomodPatchStateForSource generates a single merged patch LLB state for all paths
221221
// in a gomod generator by running go mod edit + tidy and capturing the diff.
222222
// Returns the LLB state containing the patch file, or nil if no changes are needed.
223-
func (s *Spec) generateGomodPatchStateForSource(sourceName string, gen *SourceGenerator, baseState llb.State, worker llb.State, credHelper llb.RunOption, opts ...llb.ConstraintsOpt) (*llb.State, error) {
223+
func (s *Spec) generateGomodPatchStateForSource(sourceName string, gen *SourceGenerator, baseState llb.State, worker llb.State, credHelper llb.RunOption, gomodProxy string, opts ...llb.ConstraintsOpt) (*llb.State, error) {
224224
editArgs, err := gomodEditArgs(gen.Gomod)
225225
if err != nil {
226226
return nil, err
@@ -274,6 +274,10 @@ func (s *Spec) generateGomodPatchStateForSource(sourceName string, gen *SourceGe
274274
WithConstraints(opts...),
275275
}
276276

277+
if gomodProxy != "" {
278+
runOpts = append(runOpts, llb.AddEnv("GOPROXY", gomodProxy))
279+
}
280+
277281
// Add environment variables from the script
278282
for key, value := range SortedMapIter(envVars) {
279283
runOpts = append(runOpts, llb.AddEnv(key, value))

source.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,10 @@ type SourceOpts struct {
155155
TargetPlatform *ocispecs.Platform
156156
GitCredHelperOpt func() (llb.RunOption, error)
157157
SourceFilter func() (SourceFilterConfig, error)
158+
// GomodProxy, when set, overrides GOPROXY while gomod generators download
159+
// modules from the network. The generator still uses its local file proxy
160+
// for the cache extraction pass.
161+
GomodProxy string
158162
}
159163

160164
var errInvalidMountConfig = errors.New("invalid mount config")

0 commit comments

Comments
 (0)