Skip to content

Commit 584acc8

Browse files
committed
dockerfile: implement hooks for RUN instructions
Close issue 4576 - - - e.g., ```bash buildctl build \ --frontend dockerfile.v0 \ --opt hook="$(cat hook.json)" ``` with `hook.json` as follows: ```json { "RUN": { "entrypoint": ["/dev/.dfhook/entrypoint"], "mounts": [ {"from": "example.com/hook", "target": "/dev/.dfhook"}, {"type": "secret", "source": "something", "target": "/etc/something"} ] } } ``` This will let the frontend treat `RUN foo` as: ```dockerfile RUN \ --mount=from=example.com/hook,target=/dev/.dfhook \ --mount=type=secret,source=something,target=/etc/something \ /dev/.dfhook/entrypoint foo ``` `docker history` will still show this as `RUN foo`. Signed-off-by: Akihiro Suda <[email protected]>
1 parent b04cea9 commit 584acc8

File tree

12 files changed

+234
-27
lines changed

12 files changed

+234
-27
lines changed

docs/reference/buildctl.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,45 @@ $ buildctl build --frontend dockerfile.v0 --local context=. --local dockerfile=.
181181
$ buildctl build --frontend dockerfile.v0 --local context=. --local dockerfile=. --oci-layout foo2=/home/dir/oci --opt context:alpine=oci-layout://foo2@sha256:bd04a5b26dec16579cd1d7322e949c5905c4742269663fcbc84dcb2e9f4592fb
182182
```
183183

184+
##### Instruction hooks
185+
<!-- TODO: s/master/v0.15/ -->
186+
In the master branch, the Dockerfile frontend also supports "instruction hooks".
187+
188+
e.g.,
189+
190+
```bash
191+
buildctl build \
192+
--frontend dockerfile.v0 \
193+
--opt hook="$(cat hook.json)"
194+
```
195+
with `hook.json` as follows:
196+
```json
197+
{
198+
"RUN": {
199+
"entrypoint": ["/dev/.dfhook/entrypoint"],
200+
"mounts": [
201+
{"from": "example.com/hook", "target": "/dev/.dfhook"},
202+
{"type": "secret", "source": "something", "target": "/etc/something"}
203+
]
204+
}
205+
}
206+
```
207+
208+
This will let the frontend treat `RUN foo` as:
209+
```dockerfile
210+
RUN \
211+
--mount=from=example.com/hook,target=/dev/.dfhook \
212+
--mount=type=secret,source=something,target=/etc/something \
213+
/dev/.dfhook/entrypoint foo
214+
```
215+
216+
`docker history` will still show this as `RUN foo`.
217+
218+
<!--
219+
TODO: add example hook images to show concrete use-cases
220+
https://github.com/moby/buildkit/issues/4576
221+
-->
222+
184223
#### gateway-specific options
185224

186225
The `gateway.v0` frontend passes all of its `--opt` options on to the OCI image that is called to convert the

frontend/dockerfile/dockerfile2llb/convert.go

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import (
2929
"github.com/moby/buildkit/frontend/dockerfile/parser"
3030
"github.com/moby/buildkit/frontend/dockerfile/shell"
3131
"github.com/moby/buildkit/frontend/dockerui"
32+
"github.com/moby/buildkit/frontend/dockerui/types"
3233
"github.com/moby/buildkit/frontend/subrequests/lint"
3334
"github.com/moby/buildkit/frontend/subrequests/outline"
3435
"github.com/moby/buildkit/frontend/subrequests/targets"
@@ -148,7 +149,7 @@ func ListTargets(ctx context.Context, dt []byte) (*targets.List, error) {
148149
return nil, err
149150
}
150151

151-
stages, _, err := instructions.Parse(dockerfile.AST, nil)
152+
stages, _, err := instructions.Parse(dockerfile.AST, nil, instructions.ParseOpts{})
152153
if err != nil {
153154
return nil, err
154155
}
@@ -248,7 +249,10 @@ func toDispatchState(ctx context.Context, dt []byte, opt ConvertOpt) (*dispatchS
248249

249250
proxyEnv := proxyEnvFromBuildArgs(opt.BuildArgs)
250251

251-
stages, metaArgs, err := instructions.Parse(dockerfile.AST, lint)
252+
parseOpts := instructions.ParseOpts{
253+
InstructionHook: opt.InstructionHook,
254+
}
255+
stages, metaArgs, err := instructions.Parse(dockerfile.AST, lint, parseOpts)
252256
if err != nil {
253257
return nil, err
254258
}
@@ -651,6 +655,7 @@ func toDispatchState(ctx context.Context, dt []byte, opt ConvertOpt) (*dispatchS
651655
llbCaps: opt.LLBCaps,
652656
sourceMap: opt.SourceMap,
653657
lint: lint,
658+
instHook: opt.InstructionHook,
654659
}
655660

656661
if err = dispatchOnBuildTriggers(d, d.image.Config.OnBuild, opt); err != nil {
@@ -825,6 +830,7 @@ type dispatchOpt struct {
825830
llbCaps *apicaps.CapSet
826831
sourceMap *llb.SourceMap
827832
lint *linter.Linter
833+
instHook *types.InstructionHook
828834
}
829835

830836
func getEnv(state llb.State) shell.EnvGetter {
@@ -1089,6 +1095,9 @@ type command struct {
10891095
}
10901096

10911097
func dispatchOnBuildTriggers(d *dispatchState, triggers []string, opt dispatchOpt) error {
1098+
parseOpts := instructions.ParseOpts{
1099+
InstructionHook: opt.instHook,
1100+
}
10921101
for _, trigger := range triggers {
10931102
ast, err := parser.Parse(strings.NewReader(trigger))
10941103
if err != nil {
@@ -1097,7 +1106,7 @@ func dispatchOnBuildTriggers(d *dispatchState, triggers []string, opt dispatchOp
10971106
if len(ast.AST.Children) != 1 {
10981107
return errors.New("onbuild trigger should be a single expression")
10991108
}
1100-
ic, err := instructions.ParseCommand(ast.AST.Children[0])
1109+
ic, err := instructions.ParseCommand(ast.AST.Children[0], parseOpts)
11011110
if err != nil {
11021111
return err
11031112
}
@@ -1193,6 +1202,12 @@ func dispatchRun(d *dispatchState, c *instructions.RunCommand, proxy *llb.ProxyE
11931202
args = withShell(d.image, args)
11941203
}
11951204

1205+
argsForHistory := args
1206+
if dopt.instHook != nil && dopt.instHook.Run != nil {
1207+
args = append(dopt.instHook.Run.Entrypoint, args...)
1208+
// leave argsForHistory unmodified
1209+
}
1210+
11961211
opt = append(opt, llb.Args(args), dfCmd(c), location(dopt.sourceMap, c.Location()))
11971212
if d.ignoreCache {
11981213
opt = append(opt, llb.IgnoreCache)
@@ -1256,7 +1271,7 @@ func dispatchRun(d *dispatchState, c *instructions.RunCommand, proxy *llb.ProxyE
12561271
}
12571272

12581273
d.state = d.state.Run(opt...).Root()
1259-
return commitToHistory(&d.image, "RUN "+runCommandString(args, d.buildArgs, env), true, &d.state, d.epoch)
1274+
return commitToHistory(&d.image, "RUN "+runCommandString(argsForHistory, d.buildArgs, env), true, &d.state, d.epoch)
12601275
}
12611276

12621277
func dispatchWorkdir(d *dispatchState, c *instructions.WorkdirCommand, commit bool, opt *dispatchOpt) error {
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package dockerfile
2+
3+
import (
4+
"os"
5+
"path/filepath"
6+
"strings"
7+
"testing"
8+
9+
"github.com/containerd/continuity/fs/fstest"
10+
"github.com/moby/buildkit/client"
11+
"github.com/moby/buildkit/frontend/dockerui"
12+
"github.com/moby/buildkit/util/testutil/integration"
13+
"github.com/stretchr/testify/require"
14+
"github.com/tonistiigi/fsutil"
15+
)
16+
17+
var instHookTests = integration.TestFuncs(
18+
testInstructionHook,
19+
)
20+
21+
func testInstructionHook(t *testing.T, sb integration.Sandbox) {
22+
integration.SkipOnPlatform(t, "windows")
23+
f := getFrontend(t, sb)
24+
25+
dockerfile := []byte(`
26+
FROM busybox AS base
27+
RUN echo "$FOO" >/foo
28+
29+
FROM scratch
30+
COPY --from=base /foo /foo
31+
`)
32+
33+
dir := integration.Tmpdir(
34+
t,
35+
fstest.CreateFile("Dockerfile", dockerfile, 0600),
36+
)
37+
destDir := t.TempDir()
38+
39+
c, err := client.New(sb.Context(), sb.Address())
40+
require.NoError(t, err)
41+
defer c.Close()
42+
43+
build := func(attrs map[string]string) string {
44+
_, err = f.Solve(sb.Context(), c, client.SolveOpt{
45+
FrontendAttrs: attrs,
46+
Exports: []client.ExportEntry{
47+
{
48+
Type: client.ExporterLocal,
49+
OutputDir: destDir,
50+
},
51+
},
52+
LocalMounts: map[string]fsutil.FS{
53+
dockerui.DefaultLocalNameDockerfile: dir,
54+
dockerui.DefaultLocalNameContext: dir,
55+
},
56+
}, nil)
57+
require.NoError(t, err)
58+
p := filepath.Join(destDir, "foo")
59+
b, err := os.ReadFile(p)
60+
require.NoError(t, err)
61+
return strings.TrimSpace(string(b))
62+
}
63+
64+
require.Equal(t, "", build(nil))
65+
66+
const hook = `
67+
{
68+
"RUN": {
69+
"entrypoint": ["/dev/.dfhook/bin/busybox", "env", "FOO=BAR"],
70+
"mounts": [
71+
{"from": "busybox:uclibc", "target": "/dev/.dfhook"}
72+
]
73+
}
74+
}`
75+
require.Equal(t, "BAR", build(map[string]string{"hook": hook}))
76+
}

frontend/dockerfile/dockerfile_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,7 @@ func TestIntegration(t *testing.T) {
257257
"amd64/bullseye-20230109-slim:latest": "docker.io/amd64/debian:bullseye-20230109-slim@sha256:1acb06a0c31fb467eb8327ad361f1091ab265e0bf26d452dea45dcb0c0ea5e75",
258258
}),
259259
)...)
260+
integration.Run(t, instHookTests, opts...)
260261
}
261262

262263
func testDefaultEnvWithArgs(t *testing.T, sb integration.Sandbox) {

frontend/dockerfile/instructions/commands.go

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"strings"
55

66
"github.com/moby/buildkit/frontend/dockerfile/parser"
7+
"github.com/moby/buildkit/frontend/dockerui/types"
78
dockerspec "github.com/moby/docker-image-spec/specs-go/v1"
89
"github.com/pkg/errors"
910
)
@@ -340,7 +341,7 @@ type ShellDependantCmdLine struct {
340341
// RUN ["echo", "hi"] # echo hi
341342
type RunCommand struct {
342343
withNameAndCode
343-
withExternalData
344+
WithInstructionHook
344345
ShellDependantCmdLine
345346
FlagsUsed []string
346347
}
@@ -551,3 +552,21 @@ func (c *withExternalData) setExternalValue(k, v interface{}) {
551552
}
552553
c.m[k] = v
553554
}
555+
556+
type WithInstructionHook struct {
557+
withExternalData
558+
}
559+
560+
const instHookKey = "dockerfile/run/instruction-hook"
561+
562+
func (c *WithInstructionHook) GetInstructionHook() *types.InstructionHook {
563+
x := c.getExternalValue(instHookKey)
564+
if x == nil {
565+
return nil
566+
}
567+
return x.(*types.InstructionHook)
568+
}
569+
570+
func (c *WithInstructionHook) SetInstructionHook(h *types.InstructionHook) {
571+
c.setExternalValue(instHookKey, h)
572+
}

frontend/dockerfile/instructions/commands_runmount.go

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,12 +85,21 @@ func setMountState(cmd *RunCommand, expander SingleWordExpander) error {
8585
return errors.Errorf("no mount state")
8686
}
8787
mounts := make([]*Mount, len(st.flag.StringValues))
88-
for i, str := range st.flag.StringValues {
88+
if hook := cmd.GetInstructionHook(); hook != nil && hook.Run != nil {
89+
for _, m := range hook.Run.Mounts {
90+
m := m
91+
if err := validateMount(&m, false); err != nil {
92+
return err
93+
}
94+
mounts = append(mounts, &m)
95+
}
96+
}
97+
for _, str := range st.flag.StringValues {
8998
m, err := parseMount(str, expander)
9099
if err != nil {
91100
return err
92101
}
93-
mounts[i] = m
102+
mounts = append(mounts, m)
94103
}
95104
st.mounts = mounts
96105
return nil

frontend/dockerfile/instructions/parse.go

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,18 @@ import (
1515
"github.com/moby/buildkit/frontend/dockerfile/command"
1616
"github.com/moby/buildkit/frontend/dockerfile/linter"
1717
"github.com/moby/buildkit/frontend/dockerfile/parser"
18+
"github.com/moby/buildkit/frontend/dockerui/types"
1819
"github.com/moby/buildkit/util/suggest"
1920
dockerspec "github.com/moby/docker-image-spec/specs-go/v1"
2021
"github.com/pkg/errors"
2122
)
2223

2324
var excludePatternsEnabled = false
2425

26+
type ParseOpts struct {
27+
InstructionHook *types.InstructionHook
28+
}
29+
2530
type parseRequest struct {
2631
command string
2732
args []string
@@ -31,6 +36,7 @@ type parseRequest struct {
3136
original string
3237
location []parser.Range
3338
comments []string
39+
opts ParseOpts
3440
}
3541

3642
var parseRunPreHooks []func(*RunCommand, parseRequest) error
@@ -66,18 +72,19 @@ func newParseRequestFromNode(node *parser.Node) parseRequest {
6672
}
6773
}
6874

69-
func ParseInstruction(node *parser.Node) (v interface{}, err error) {
70-
return ParseInstructionWithLinter(node, nil)
75+
func ParseInstruction(node *parser.Node, opts ParseOpts) (v interface{}, err error) {
76+
return ParseInstructionWithLinter(node, nil, opts)
7177
}
7278

7379
// ParseInstruction converts an AST to a typed instruction (either a command or a build stage beginning when encountering a `FROM` statement)
74-
func ParseInstructionWithLinter(node *parser.Node, lint *linter.Linter) (v interface{}, err error) {
80+
func ParseInstructionWithLinter(node *parser.Node, lint *linter.Linter, opts ParseOpts) (v interface{}, err error) {
7581
defer func() {
7682
if err != nil {
7783
err = parser.WithLocation(err, node.Location())
7884
}
7985
}()
8086
req := newParseRequestFromNode(node)
87+
req.opts = opts
8188
switch strings.ToLower(node.Value) {
8289
case command.Env:
8390
return parseEnv(req)
@@ -130,8 +137,8 @@ func ParseInstructionWithLinter(node *parser.Node, lint *linter.Linter) (v inter
130137
}
131138

132139
// ParseCommand converts an AST to a typed Command
133-
func ParseCommand(node *parser.Node) (Command, error) {
134-
s, err := ParseInstruction(node)
140+
func ParseCommand(node *parser.Node, opts ParseOpts) (Command, error) {
141+
s, err := ParseInstruction(node, opts)
135142
if err != nil {
136143
return nil, err
137144
}
@@ -166,9 +173,9 @@ func (e *parseError) Unwrap() error {
166173

167174
// Parse a Dockerfile into a collection of buildable stages.
168175
// metaArgs is a collection of ARG instructions that occur before the first FROM.
169-
func Parse(ast *parser.Node, lint *linter.Linter) (stages []Stage, metaArgs []ArgCommand, err error) {
176+
func Parse(ast *parser.Node, lint *linter.Linter, opts ParseOpts) (stages []Stage, metaArgs []ArgCommand, err error) {
170177
for _, n := range ast.Children {
171-
cmd, err := ParseInstructionWithLinter(n, lint)
178+
cmd, err := ParseInstructionWithLinter(n, lint, opts)
172179
if err != nil {
173180
return nil, nil, &parseError{inner: err, node: n}
174181
}
@@ -489,6 +496,7 @@ func parseShellDependentCommand(req parseRequest, emptyAsNil bool) (ShellDependa
489496

490497
func parseRun(req parseRequest) (*RunCommand, error) {
491498
cmd := &RunCommand{}
499+
cmd.SetInstructionHook(req.opts.InstructionHook)
492500

493501
for _, fn := range parseRunPreHooks {
494502
if err := fn(cmd, req); err != nil {

frontend/dockerfile/instructions/parse_heredoc_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ func TestErrorCasesHeredoc(t *testing.T) {
2828
t.Fatalf("Error when parsing Dockerfile: %s", err)
2929
}
3030
n := ast.AST.Children[0]
31-
_, err = ParseInstruction(n)
31+
_, err = ParseInstruction(n, ParseOpts{})
3232
require.Error(t, err)
3333
require.Contains(t, err.Error(), c.expectedError)
3434
}
@@ -166,7 +166,7 @@ EOF`,
166166
require.NoError(t, err)
167167

168168
n := ast.AST.Children[0]
169-
comm, err := ParseInstruction(n)
169+
comm, err := ParseInstruction(n, ParseOpts{})
170170
require.NoError(t, err)
171171

172172
sd := comm.(*CopyCommand).SourcesAndDest
@@ -248,7 +248,7 @@ EOF`,
248248
require.NoError(t, err)
249249

250250
n := ast.AST.Children[0]
251-
comm, err := ParseInstruction(n)
251+
comm, err := ParseInstruction(n, ParseOpts{})
252252
require.NoError(t, err)
253253
require.Equal(t, c.shell, comm.(*RunCommand).PrependShell)
254254
require.Equal(t, c.command, comm.(*RunCommand).CmdLine)

0 commit comments

Comments
 (0)