Skip to content

Commit 2195d2e

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 fd583c1 commit 2195d2e

8 files changed

Lines changed: 264 additions & 15 deletions

File tree

determinism_test.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,12 @@ 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(gomodGeneratorOpts{
226+
sourceName: "src",
227+
gen: gen,
228+
sourceState: base,
229+
worker: worker,
230+
})
226231
assert.NilError(t, err)
227232
assert.Assert(t, st != nil)
228233
return *st

frontend/gateway.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,13 @@ 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+
if sOpt.ExtraEnvs == nil {
139+
sOpt.ExtraEnvs = map[string]string{}
140+
}
141+
sOpt.ExtraEnvs["GOPROXY"] = gomodProxy
142+
}
143+
137144
return sOpt
138145
}
139146

frontend/gateway_test.go

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

generator_gomod.go

Lines changed: 44 additions & 7 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,16 +66,35 @@ 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+
type gomodGeneratorOpts struct {
70+
sourceName string
71+
gen *SourceGenerator
72+
sourceState llb.State
73+
worker llb.State
74+
credHelper llb.RunOption
75+
extraEnvs map[string]string
76+
constraints []llb.ConstraintsOpt
77+
}
78+
79+
func (opts gomodGeneratorOpts) gomodProxy() string {
80+
if opts.extraEnvs == nil {
81+
return ""
82+
}
83+
return opts.extraEnvs["GOPROXY"]
84+
}
85+
86+
func withGomod(gomodOpts gomodGeneratorOpts) func(llb.State) llb.State {
6787
return func(in llb.State) llb.State {
6888
const (
6989
workDir = "/work/src"
7090
scriptMountpoint = "/tmp/dalec/internal/gomod"
7191
gomodDownloadWrapperBasename = "go_mod_download.sh"
7292
)
7393

74-
joinedWorkDir := filepath.Join(workDir, subPath, g.Subpath)
75-
srcMount := llb.AddMount(workDir, srcSt)
94+
g := gomodOpts.gen
95+
opts := gomodOpts.constraints
96+
joinedWorkDir := filepath.Join(workDir, gomodOpts.sourceName, g.Subpath)
97+
srcMount := llb.AddMount(workDir, gomodOpts.sourceState)
7698

7799
paths := g.Gomod.Paths
78100
if g.Gomod.Paths == nil {
@@ -86,23 +108,30 @@ func withGomod(g *SourceGenerator, srcSt, worker llb.State, subPath string, cred
86108
scriptPath := filepath.Join(scriptMountpoint, gomodDownloadWrapperBasename)
87109

88110
for _, path := range paths {
89-
in = worker.Run(
111+
runOpts := []llb.RunOption{
90112
// First download the go module deps to our persistent cache
91113
// Then set the GOPROXY to the cache dir so that we can extract just the deps we need
92114
// This allows us to persist the module cache across builds and avoid downloading
93115
// the same deps over and over again.
94-
ShArgs(`set -e; GOMODCACHE="${TMP_GOMODCACHE}" `+scriptPath+`; GOPROXY="file://${TMP_GOMODCACHE}/cache/download" `+scriptPath),
116+
ShArgs(`set -e; GOMODCACHE="${TMP_GOMODCACHE}" ` + scriptPath + `; GOPROXY="file://${TMP_GOMODCACHE}/cache/download" ` + scriptPath),
95117
g.withGomodSecretsAndSockets(),
96118
llb.AddMount(scriptMountpoint, script),
97119
llb.AddEnv("GOPATH", "/go"),
98-
credHelper,
120+
gomodOpts.credHelper,
99121
llb.AddEnv("TMP_GOMODCACHE", proxyPath),
100122
llb.AddEnv("GIT_SSH_COMMAND", "ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no"),
101123
llb.Dir(filepath.Join(joinedWorkDir, path)),
102124
srcMount,
103125
llb.AddMount(proxyPath, llb.Scratch(), llb.AsPersistentCacheDir(GomodCacheKey, llb.CacheMountShared)),
104126
WithConstraints(opts...),
105127
g.Gomod._sourceMap.GetLocation(in),
128+
}
129+
if gomodProxy := gomodOpts.gomodProxy(); gomodProxy != "" {
130+
runOpts = append(runOpts, llb.AddEnv("GOPROXY", gomodProxy))
131+
}
132+
133+
in = gomodOpts.worker.Run(
134+
runOpts...,
106135
).AddMount(gomodCacheDir, in)
107136
}
108137

@@ -238,7 +267,15 @@ func (s *Spec) GomodDeps(sOpt SourceOpts, worker llb.State, opts ...llb.Constrai
238267
deps = deps.With(func(in llb.State) llb.State {
239268
for _, gen := range src.Generate {
240269
if gen.Gomod != nil {
241-
in = in.With(withGomod(gen, patched[key], worker, key, credHelperRunOpt, opts...))
270+
in = in.With(withGomod(gomodGeneratorOpts{
271+
sourceName: key,
272+
gen: gen,
273+
sourceState: patched[key],
274+
worker: worker,
275+
credHelper: credHelperRunOpt,
276+
extraEnvs: sOpt.ExtraEnvs,
277+
constraints: opts,
278+
}))
242279
}
243280
}
244281
return in

generator_gomod_test.go

Lines changed: 159 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) {
@@ -296,3 +300,158 @@ func TestGomodEditArgs_DropRequire(t *testing.T) {
296300
})
297301
}
298302
}
303+
304+
func TestGomodDepsUsesGomodProxy(t *testing.T) {
305+
t.Parallel()
306+
307+
const proxy = "http://proxy.example:5000,direct"
308+
spec := testGomodProxySpec()
309+
st := spec.GomodDeps(testGomodProxySourceOpts(proxy), llb.Scratch())
310+
if st == nil {
311+
t.Fatal("gomod generator succeeded but returned nil state")
312+
}
313+
314+
env := gomodDownloadExecEnv(context.Background(), t, *st)
315+
if !slices.Contains(env, "GOPROXY="+proxy) {
316+
t.Fatalf("expected gomod exec env to include GOPROXY=%q, got %v", proxy, env)
317+
}
318+
}
319+
320+
func TestGomodDepsSkipsEmptyGomodProxy(t *testing.T) {
321+
t.Parallel()
322+
323+
spec := testGomodProxySpec()
324+
st := spec.GomodDeps(testGomodProxySourceOpts(""), llb.Scratch())
325+
if st == nil {
326+
t.Fatal("gomod generator succeeded but returned nil state")
327+
}
328+
329+
env := gomodDownloadExecEnv(context.Background(), t, *st)
330+
for _, item := range env {
331+
if strings.HasPrefix(item, "GOPROXY=") {
332+
t.Fatalf("expected empty gomod proxy to omit GOPROXY, got %v", env)
333+
}
334+
}
335+
}
336+
337+
func TestGomodProxyBuildArgIsKnown(t *testing.T) {
338+
t.Parallel()
339+
340+
spec := &Spec{}
341+
err := spec.SubstituteArgs(map[string]string{
342+
BuildArgDalecGomodProxy: "http://proxy.example:5000",
343+
})
344+
if err != nil {
345+
t.Fatal(err)
346+
}
347+
}
348+
349+
func TestGomodPatchUsesGomodProxy(t *testing.T) {
350+
t.Parallel()
351+
352+
const proxy = "http://proxy.example:5000,direct"
353+
gen := &SourceGenerator{
354+
Gomod: &GeneratorGomod{
355+
Edits: &GomodEdits{
356+
Replace: []GomodReplace{
357+
{Original: "example.com/old", Update: "example.com/new v1.2.3"},
358+
},
359+
},
360+
},
361+
}
362+
363+
st, err := (&Spec{}).generateGomodPatchStateForSource(gomodGeneratorOpts{
364+
sourceName: "src",
365+
gen: gen,
366+
sourceState: llb.Scratch(),
367+
worker: llb.Scratch(),
368+
extraEnvs: map[string]string{
369+
"GOPROXY": proxy,
370+
},
371+
})
372+
if err != nil {
373+
t.Fatal(err)
374+
}
375+
if st == nil {
376+
t.Fatal("gomod patch generation succeeded but returned nil state")
377+
}
378+
379+
env := gomodPatchExecEnv(context.Background(), t, *st)
380+
if !slices.Contains(env, "GOPROXY="+proxy) {
381+
t.Fatalf("expected gomod patch exec env to include GOPROXY=%q, got %v", proxy, env)
382+
}
383+
}
384+
385+
func testGomodProxySpec() *Spec {
386+
return &Spec{
387+
Sources: map[string]Source{
388+
"src": {
389+
Git: &SourceGit{
390+
URL: "https://example.com/repo.git",
391+
Commit: "0123456789abcdef",
392+
},
393+
Generate: []*SourceGenerator{
394+
{Gomod: &GeneratorGomod{}},
395+
},
396+
},
397+
},
398+
}
399+
}
400+
401+
func testGomodProxySourceOpts(proxy string) SourceOpts {
402+
sOpt := SourceOpts{
403+
GetContext: func(name string, opts ...llb.LocalOption) (*llb.State, error) {
404+
st := llb.Local(name, opts...)
405+
return &st, nil
406+
},
407+
GitCredHelperOpt: func() (llb.RunOption, error) {
408+
st := llb.Scratch().File(llb.Mkfile("/frontend", 0o755, []byte("#!/usr/bin/env bash\nexit 0\n")))
409+
return RunOptFunc(func(ei *llb.ExecInfo) {
410+
llb.AddMount("/usr/local/bin/frontend", st, llb.SourcePath("/frontend")).SetRunOption(ei)
411+
}), nil
412+
},
413+
}
414+
if proxy != "" {
415+
sOpt.ExtraEnvs = map[string]string{
416+
"GOPROXY": proxy,
417+
}
418+
}
419+
return sOpt
420+
}
421+
422+
func gomodDownloadExecEnv(ctx context.Context, t *testing.T, st llb.State) []string {
423+
t.Helper()
424+
425+
for _, op := range sourceOpsFromState(ctx, t, st) {
426+
exec := op.GetExec()
427+
if exec == nil {
428+
continue
429+
}
430+
431+
env := exec.Meta.Env
432+
if slices.Contains(env, "GOPATH=/go") && slices.Contains(env, "TMP_GOMODCACHE=/tmp/dalec/gomod-proxy-cache") {
433+
return env
434+
}
435+
}
436+
437+
t.Fatal("expected gomod download exec")
438+
return nil
439+
}
440+
441+
func gomodPatchExecEnv(ctx context.Context, t *testing.T, st llb.State) []string {
442+
t.Helper()
443+
444+
for _, op := range sourceOpsFromState(ctx, t, st) {
445+
exec := op.GetExec()
446+
if exec == nil {
447+
continue
448+
}
449+
450+
if slices.Contains(exec.Meta.Args, "/gomod-patch.sh") {
451+
return exec.Meta.Env
452+
}
453+
}
454+
455+
t.Fatal("expected gomod patch exec")
456+
return nil
457+
}

load.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ func knownArg(key string) bool {
5353
return true
5454
case "DALEC_SOURCE_FILTER_CONFIG_CONTEXT_NAME":
5555
return true
56+
case BuildArgDalecGomodProxy:
57+
return true
5658
case KeyDalecTarget:
5759
return true
5860
}

0 commit comments

Comments
 (0)