Skip to content

Commit 1eb771c

Browse files
frontend/dockerfile: add integration tests for automount feature
Add comprehensive integration tests for the --automount flag that automatically applies mounts to all RUN commands. Tests cover: - Basic secret/cache/bind/tmpfs automounts - Multiple RUN commands and multiple automounts - Coexistence with explicit RUN --mount directives - Multi-stage build support - Error handling for invalid specs and unsupported 'from' option Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent 23283e5 commit 1eb771c

File tree

1 file changed

+379
-0
lines changed

1 file changed

+379
-0
lines changed
Lines changed: 379 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,379 @@
1+
package dockerfile
2+
3+
import (
4+
"testing"
5+
6+
"github.com/containerd/continuity/fs/fstest"
7+
"github.com/moby/buildkit/client"
8+
"github.com/moby/buildkit/frontend/dockerui"
9+
"github.com/moby/buildkit/session"
10+
"github.com/moby/buildkit/session/secrets/secretsprovider"
11+
"github.com/moby/buildkit/util/testutil/integration"
12+
"github.com/stretchr/testify/require"
13+
"github.com/tonistiigi/fsutil"
14+
)
15+
16+
var automountTests = integration.TestFuncs(
17+
testAutomountSecret,
18+
testAutomountSecretMultipleRuns,
19+
testAutomountMultiple,
20+
testAutomountCache,
21+
testAutomountBind,
22+
testAutomountTmpfs,
23+
testAutomountWithExplicitMount,
24+
testAutomountAppliesAllStages,
25+
testAutomountInvalidFrom,
26+
testAutomountInvalidSpec,
27+
)
28+
29+
func init() {
30+
allTests = append(allTests, automountTests...)
31+
}
32+
33+
func testAutomountSecret(t *testing.T, sb integration.Sandbox) {
34+
integration.SkipOnPlatform(t, "windows")
35+
f := getFrontend(t, sb)
36+
37+
dockerfile := []byte(`
38+
FROM busybox
39+
RUN [ "$(cat /run/secrets/mysecret)" = "secret-content" ]
40+
`)
41+
42+
dir := integration.Tmpdir(
43+
t,
44+
fstest.CreateFile("Dockerfile", dockerfile, 0600),
45+
)
46+
47+
c, err := client.New(sb.Context(), sb.Address())
48+
require.NoError(t, err)
49+
defer c.Close()
50+
51+
_, err = f.Solve(sb.Context(), c, client.SolveOpt{
52+
FrontendAttrs: map[string]string{
53+
"automount:0": "type=secret,id=mysecret,target=/run/secrets/mysecret",
54+
},
55+
Session: []session.Attachable{
56+
secretsprovider.FromMap(map[string][]byte{
57+
"mysecret": []byte("secret-content"),
58+
}),
59+
},
60+
LocalMounts: map[string]fsutil.FS{
61+
dockerui.DefaultLocalNameDockerfile: dir,
62+
dockerui.DefaultLocalNameContext: dir,
63+
},
64+
}, nil)
65+
require.NoError(t, err)
66+
}
67+
68+
func testAutomountSecretMultipleRuns(t *testing.T, sb integration.Sandbox) {
69+
integration.SkipOnPlatform(t, "windows")
70+
f := getFrontend(t, sb)
71+
72+
dockerfile := []byte(`
73+
FROM busybox
74+
RUN [ "$(cat /run/secrets/token)" = "my-token" ]
75+
RUN echo "Second RUN" && [ -f /run/secrets/token ]
76+
RUN cat /run/secrets/token > /tmp/verify && [ "$(cat /tmp/verify)" = "my-token" ]
77+
`)
78+
79+
dir := integration.Tmpdir(
80+
t,
81+
fstest.CreateFile("Dockerfile", dockerfile, 0600),
82+
)
83+
84+
c, err := client.New(sb.Context(), sb.Address())
85+
require.NoError(t, err)
86+
defer c.Close()
87+
88+
_, err = f.Solve(sb.Context(), c, client.SolveOpt{
89+
FrontendAttrs: map[string]string{
90+
"automount:0": "type=secret,id=token,target=/run/secrets/token",
91+
},
92+
Session: []session.Attachable{
93+
secretsprovider.FromMap(map[string][]byte{
94+
"token": []byte("my-token"),
95+
}),
96+
},
97+
LocalMounts: map[string]fsutil.FS{
98+
dockerui.DefaultLocalNameDockerfile: dir,
99+
dockerui.DefaultLocalNameContext: dir,
100+
},
101+
}, nil)
102+
require.NoError(t, err)
103+
}
104+
105+
func testAutomountMultiple(t *testing.T, sb integration.Sandbox) {
106+
integration.SkipOnPlatform(t, "windows")
107+
f := getFrontend(t, sb)
108+
109+
dockerfile := []byte(`
110+
FROM busybox
111+
RUN [ "$(cat /secrets/secret1)" = "value1" ]
112+
RUN [ "$(cat /secrets/secret2)" = "value2" ]
113+
RUN echo "cached" > /cache/test.txt
114+
RUN [ "$(cat /cache/test.txt)" = "cached" ]
115+
`)
116+
117+
dir := integration.Tmpdir(
118+
t,
119+
fstest.CreateFile("Dockerfile", dockerfile, 0600),
120+
)
121+
122+
c, err := client.New(sb.Context(), sb.Address())
123+
require.NoError(t, err)
124+
defer c.Close()
125+
126+
_, err = f.Solve(sb.Context(), c, client.SolveOpt{
127+
FrontendAttrs: map[string]string{
128+
"automount:0": "type=secret,id=secret1,target=/secrets/secret1",
129+
"automount:1": "type=secret,id=secret2,target=/secrets/secret2",
130+
"automount:2": "type=cache,target=/cache",
131+
},
132+
Session: []session.Attachable{
133+
secretsprovider.FromMap(map[string][]byte{
134+
"secret1": []byte("value1"),
135+
"secret2": []byte("value2"),
136+
}),
137+
},
138+
LocalMounts: map[string]fsutil.FS{
139+
dockerui.DefaultLocalNameDockerfile: dir,
140+
dockerui.DefaultLocalNameContext: dir,
141+
},
142+
}, nil)
143+
require.NoError(t, err)
144+
}
145+
146+
func testAutomountCache(t *testing.T, sb integration.Sandbox) {
147+
integration.SkipOnPlatform(t, "windows")
148+
f := getFrontend(t, sb)
149+
150+
dockerfile := []byte(`
151+
FROM busybox
152+
RUN echo "first" > /mycache/data.txt
153+
RUN [ "$(cat /mycache/data.txt)" = "first" ]
154+
RUN echo "second" >> /mycache/data.txt
155+
RUN grep -q "first" /mycache/data.txt && grep -q "second" /mycache/data.txt
156+
`)
157+
158+
dir := integration.Tmpdir(
159+
t,
160+
fstest.CreateFile("Dockerfile", dockerfile, 0600),
161+
)
162+
163+
c, err := client.New(sb.Context(), sb.Address())
164+
require.NoError(t, err)
165+
defer c.Close()
166+
167+
_, err = f.Solve(sb.Context(), c, client.SolveOpt{
168+
FrontendAttrs: map[string]string{
169+
"automount:0": "type=cache,target=/mycache,id=testcache",
170+
},
171+
LocalMounts: map[string]fsutil.FS{
172+
dockerui.DefaultLocalNameDockerfile: dir,
173+
dockerui.DefaultLocalNameContext: dir,
174+
},
175+
}, nil)
176+
require.NoError(t, err)
177+
}
178+
179+
func testAutomountBind(t *testing.T, sb integration.Sandbox) {
180+
integration.SkipOnPlatform(t, "windows")
181+
f := getFrontend(t, sb)
182+
183+
dockerfile := []byte(`
184+
FROM busybox
185+
RUN [ "$(cat /context/config)" = "config-data" ]
186+
`)
187+
188+
dir := integration.Tmpdir(
189+
t,
190+
fstest.CreateFile("Dockerfile", dockerfile, 0600),
191+
fstest.CreateFile("config", []byte("config-data"), 0600),
192+
)
193+
194+
c, err := client.New(sb.Context(), sb.Address())
195+
require.NoError(t, err)
196+
defer c.Close()
197+
198+
_, err = f.Solve(sb.Context(), c, client.SolveOpt{
199+
FrontendAttrs: map[string]string{
200+
"automount:0": "type=bind,source=.,target=/context",
201+
},
202+
LocalMounts: map[string]fsutil.FS{
203+
dockerui.DefaultLocalNameDockerfile: dir,
204+
dockerui.DefaultLocalNameContext: dir,
205+
},
206+
}, nil)
207+
require.NoError(t, err)
208+
}
209+
210+
func testAutomountTmpfs(t *testing.T, sb integration.Sandbox) {
211+
integration.SkipOnPlatform(t, "windows")
212+
f := getFrontend(t, sb)
213+
214+
dockerfile := []byte(`
215+
FROM busybox
216+
RUN touch /tmpdata/foo && [ -f /tmpdata/foo ]
217+
RUN [ ! -f /tmpdata/foo ]
218+
`)
219+
220+
dir := integration.Tmpdir(
221+
t,
222+
fstest.CreateFile("Dockerfile", dockerfile, 0600),
223+
)
224+
225+
c, err := client.New(sb.Context(), sb.Address())
226+
require.NoError(t, err)
227+
defer c.Close()
228+
229+
_, err = f.Solve(sb.Context(), c, client.SolveOpt{
230+
FrontendAttrs: map[string]string{
231+
"automount:0": "type=tmpfs,target=/tmpdata",
232+
},
233+
LocalMounts: map[string]fsutil.FS{
234+
dockerui.DefaultLocalNameDockerfile: dir,
235+
dockerui.DefaultLocalNameContext: dir,
236+
},
237+
}, nil)
238+
require.NoError(t, err)
239+
}
240+
241+
func testAutomountWithExplicitMount(t *testing.T, sb integration.Sandbox) {
242+
integration.SkipOnPlatform(t, "windows")
243+
f := getFrontend(t, sb)
244+
245+
dockerfile := []byte(`
246+
FROM busybox
247+
RUN [ "$(cat /run/secrets/auto)" = "auto-secret" ]
248+
RUN --mount=type=secret,id=explicit,target=/run/secrets/explicit [ "$(cat /run/secrets/explicit)" = "explicit-secret" ] && [ "$(cat /run/secrets/auto)" = "auto-secret" ]
249+
`)
250+
251+
dir := integration.Tmpdir(
252+
t,
253+
fstest.CreateFile("Dockerfile", dockerfile, 0600),
254+
)
255+
256+
c, err := client.New(sb.Context(), sb.Address())
257+
require.NoError(t, err)
258+
defer c.Close()
259+
260+
_, err = f.Solve(sb.Context(), c, client.SolveOpt{
261+
FrontendAttrs: map[string]string{
262+
"automount:0": "type=secret,id=auto,target=/run/secrets/auto",
263+
},
264+
Session: []session.Attachable{
265+
secretsprovider.FromMap(map[string][]byte{
266+
"auto": []byte("auto-secret"),
267+
"explicit": []byte("explicit-secret"),
268+
}),
269+
},
270+
LocalMounts: map[string]fsutil.FS{
271+
dockerui.DefaultLocalNameDockerfile: dir,
272+
dockerui.DefaultLocalNameContext: dir,
273+
},
274+
}, nil)
275+
require.NoError(t, err)
276+
}
277+
278+
func testAutomountAppliesAllStages(t *testing.T, sb integration.Sandbox) {
279+
integration.SkipOnPlatform(t, "windows")
280+
f := getFrontend(t, sb)
281+
282+
dockerfile := []byte(`
283+
FROM busybox AS stage1
284+
RUN [ "$(cat /run/secrets/shared)" = "shared-value" ]
285+
286+
FROM busybox AS stage2
287+
RUN [ "$(cat /run/secrets/shared)" = "shared-value" ]
288+
289+
FROM busybox
290+
RUN [ "$(cat /run/secrets/shared)" = "shared-value" ]
291+
`)
292+
293+
dir := integration.Tmpdir(
294+
t,
295+
fstest.CreateFile("Dockerfile", dockerfile, 0600),
296+
)
297+
298+
c, err := client.New(sb.Context(), sb.Address())
299+
require.NoError(t, err)
300+
defer c.Close()
301+
302+
_, err = f.Solve(sb.Context(), c, client.SolveOpt{
303+
FrontendAttrs: map[string]string{
304+
"automount:0": "type=secret,id=shared,target=/run/secrets/shared",
305+
},
306+
Session: []session.Attachable{
307+
secretsprovider.FromMap(map[string][]byte{
308+
"shared": []byte("shared-value"),
309+
}),
310+
},
311+
LocalMounts: map[string]fsutil.FS{
312+
dockerui.DefaultLocalNameDockerfile: dir,
313+
dockerui.DefaultLocalNameContext: dir,
314+
},
315+
}, nil)
316+
require.NoError(t, err)
317+
}
318+
319+
func testAutomountInvalidFrom(t *testing.T, sb integration.Sandbox) {
320+
integration.SkipOnPlatform(t, "windows")
321+
f := getFrontend(t, sb)
322+
323+
dockerfile := []byte(`
324+
FROM busybox
325+
RUN echo "test"
326+
`)
327+
328+
dir := integration.Tmpdir(
329+
t,
330+
fstest.CreateFile("Dockerfile", dockerfile, 0600),
331+
)
332+
333+
c, err := client.New(sb.Context(), sb.Address())
334+
require.NoError(t, err)
335+
defer c.Close()
336+
337+
_, err = f.Solve(sb.Context(), c, client.SolveOpt{
338+
FrontendAttrs: map[string]string{
339+
"automount:0": "type=bind,from=builder,target=/data",
340+
},
341+
LocalMounts: map[string]fsutil.FS{
342+
dockerui.DefaultLocalNameDockerfile: dir,
343+
dockerui.DefaultLocalNameContext: dir,
344+
},
345+
}, nil)
346+
require.Error(t, err)
347+
require.Contains(t, err.Error(), "automount does not support 'from' option")
348+
}
349+
350+
func testAutomountInvalidSpec(t *testing.T, sb integration.Sandbox) {
351+
integration.SkipOnPlatform(t, "windows")
352+
f := getFrontend(t, sb)
353+
354+
dockerfile := []byte(`
355+
FROM busybox
356+
RUN echo "test"
357+
`)
358+
359+
dir := integration.Tmpdir(
360+
t,
361+
fstest.CreateFile("Dockerfile", dockerfile, 0600),
362+
)
363+
364+
c, err := client.New(sb.Context(), sb.Address())
365+
require.NoError(t, err)
366+
defer c.Close()
367+
368+
_, err = f.Solve(sb.Context(), c, client.SolveOpt{
369+
FrontendAttrs: map[string]string{
370+
"automount:0": "invalid-mount-spec",
371+
},
372+
LocalMounts: map[string]fsutil.FS{
373+
dockerui.DefaultLocalNameDockerfile: dir,
374+
dockerui.DefaultLocalNameContext: dir,
375+
},
376+
}, nil)
377+
require.Error(t, err)
378+
require.Contains(t, err.Error(), "failed to parse automount")
379+
}

0 commit comments

Comments
 (0)