Skip to content

Commit

Permalink
dockerfile: implement hooks for RUN instructions
Browse files Browse the repository at this point in the history
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 <akihiro.suda.cz@hco.ntt.co.jp>
  • Loading branch information
AkihiroSuda committed Jul 26, 2024
1 parent bd53d71 commit f4d9ead
Show file tree
Hide file tree
Showing 12 changed files with 235 additions and 28 deletions.
39 changes: 39 additions & 0 deletions docs/reference/buildctl.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,45 @@ $ buildctl build --frontend dockerfile.v0 --local context=. --local dockerfile=.
$ buildctl build --frontend dockerfile.v0 --local context=. --local dockerfile=. --oci-layout foo2=/home/dir/oci --opt context:alpine=oci-layout://foo2@sha256:bd04a5b26dec16579cd1d7322e949c5905c4742269663fcbc84dcb2e9f4592fb
```

##### Instruction hooks
<!-- TODO: s/master/v0.16/ -->
In the master branch, the Dockerfile frontend also supports "instruction hooks".

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`.

<!--
TODO: add example hook images to show concrete use-cases
https://github.com/moby/buildkit/issues/4576
-->

#### gateway-specific options

The `gateway.v0` frontend passes all of its `--opt` options on to the OCI image that is called to convert the
Expand Down
23 changes: 19 additions & 4 deletions frontend/dockerfile/dockerfile2llb/convert.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import (
"github.com/moby/buildkit/frontend/dockerfile/parser"
"github.com/moby/buildkit/frontend/dockerfile/shell"
"github.com/moby/buildkit/frontend/dockerui"
"github.com/moby/buildkit/frontend/dockerui/types"
"github.com/moby/buildkit/frontend/subrequests/lint"
"github.com/moby/buildkit/frontend/subrequests/outline"
"github.com/moby/buildkit/frontend/subrequests/targets"
Expand Down Expand Up @@ -155,7 +156,7 @@ func ListTargets(ctx context.Context, dt []byte) (*targets.List, error) {
return nil, err
}

stages, _, err := instructions.Parse(dockerfile.AST, nil)
stages, _, err := instructions.Parse(dockerfile.AST, nil, instructions.ParseOpts{})
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -255,7 +256,10 @@ func toDispatchState(ctx context.Context, dt []byte, opt ConvertOpt) (*dispatchS

proxyEnv := proxyEnvFromBuildArgs(opt.BuildArgs)

stages, metaArgs, err := instructions.Parse(dockerfile.AST, lint)
parseOpts := instructions.ParseOpts{
InstructionHook: opt.InstructionHook,
}
stages, metaArgs, err := instructions.Parse(dockerfile.AST, lint, parseOpts)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -670,6 +674,7 @@ func toDispatchState(ctx context.Context, dt []byte, opt ConvertOpt) (*dispatchS
sourceMap: opt.SourceMap,
lint: lint,
dockerIgnoreMatcher: dockerIgnoreMatcher,
instHook: opt.InstructionHook,
}

if err = dispatchOnBuildTriggers(d, d.image.Config.OnBuild, opt); err != nil {
Expand Down Expand Up @@ -845,6 +850,7 @@ type dispatchOpt struct {
sourceMap *llb.SourceMap
lint *linter.Linter
dockerIgnoreMatcher *patternmatcher.PatternMatcher
instHook *types.InstructionHook
}

func getEnv(state llb.State) shell.EnvGetter {
Expand Down Expand Up @@ -1114,6 +1120,9 @@ type command struct {
}

func dispatchOnBuildTriggers(d *dispatchState, triggers []string, opt dispatchOpt) error {
parseOpts := instructions.ParseOpts{
InstructionHook: opt.instHook,
}
for _, trigger := range triggers {
ast, err := parser.Parse(strings.NewReader(trigger))
if err != nil {
Expand All @@ -1122,7 +1131,7 @@ func dispatchOnBuildTriggers(d *dispatchState, triggers []string, opt dispatchOp
if len(ast.AST.Children) != 1 {
return errors.New("onbuild trigger should be a single expression")
}
ic, err := instructions.ParseCommand(ast.AST.Children[0])
ic, err := instructions.ParseCommand(ast.AST.Children[0], parseOpts)
if err != nil {
return err
}
Expand Down Expand Up @@ -1219,6 +1228,12 @@ func dispatchRun(d *dispatchState, c *instructions.RunCommand, proxy *llb.ProxyE
args = withShell(d.image, args)
}

argsForHistory := args
if dopt.instHook != nil && dopt.instHook.Run != nil {
args = append(dopt.instHook.Run.Entrypoint, args...)
// leave argsForHistory unmodified
}

opt = append(opt, llb.Args(args), dfCmd(c), location(dopt.sourceMap, c.Location()))
if d.ignoreCache {
opt = append(opt, llb.IgnoreCache)
Expand Down Expand Up @@ -1282,7 +1297,7 @@ func dispatchRun(d *dispatchState, c *instructions.RunCommand, proxy *llb.ProxyE
}

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

func dispatchWorkdir(d *dispatchState, c *instructions.WorkdirCommand, commit bool, opt *dispatchOpt) error {
Expand Down
76 changes: 76 additions & 0 deletions frontend/dockerfile/dockerfile_insthook_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package dockerfile

import (
"os"
"path/filepath"
"strings"
"testing"

"github.com/containerd/continuity/fs/fstest"
"github.com/moby/buildkit/client"
"github.com/moby/buildkit/frontend/dockerui"
"github.com/moby/buildkit/util/testutil/integration"
"github.com/stretchr/testify/require"
"github.com/tonistiigi/fsutil"
)

var instHookTests = integration.TestFuncs(
testInstructionHook,
)

func testInstructionHook(t *testing.T, sb integration.Sandbox) {
integration.SkipOnPlatform(t, "windows")
f := getFrontend(t, sb)

dockerfile := []byte(`
FROM busybox AS base
RUN echo "$FOO" >/foo
FROM scratch
COPY --from=base /foo /foo
`)

dir := integration.Tmpdir(
t,
fstest.CreateFile("Dockerfile", dockerfile, 0600),
)
destDir := t.TempDir()

c, err := client.New(sb.Context(), sb.Address())
require.NoError(t, err)
defer c.Close()

build := func(attrs map[string]string) string {
_, err = f.Solve(sb.Context(), c, client.SolveOpt{
FrontendAttrs: attrs,
Exports: []client.ExportEntry{
{
Type: client.ExporterLocal,
OutputDir: destDir,
},
},
LocalMounts: map[string]fsutil.FS{
dockerui.DefaultLocalNameDockerfile: dir,
dockerui.DefaultLocalNameContext: dir,
},
}, nil)
require.NoError(t, err)
p := filepath.Join(destDir, "foo")
b, err := os.ReadFile(p)
require.NoError(t, err)
return strings.TrimSpace(string(b))
}

require.Equal(t, "", build(nil))

const hook = `
{
"RUN": {
"entrypoint": ["/dev/.dfhook/bin/busybox", "env", "FOO=BAR"],
"mounts": [
{"from": "busybox:uclibc", "target": "/dev/.dfhook"}
]
}
}`
require.Equal(t, "BAR", build(map[string]string{"hook": hook}))
}
2 changes: 2 additions & 0 deletions frontend/dockerfile/dockerfile_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,8 @@ func TestIntegration(t *testing.T) {
"granted": networkHostGranted,
"denied": networkHostDenied,
}))...)

integration.Run(t, instHookTests, opts...)
}

func testEmptyStringArgInEnv(t *testing.T, sb integration.Sandbox) {
Expand Down
21 changes: 20 additions & 1 deletion frontend/dockerfile/instructions/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"strings"

"github.com/moby/buildkit/frontend/dockerfile/parser"
"github.com/moby/buildkit/frontend/dockerui/types"
dockerspec "github.com/moby/docker-image-spec/specs-go/v1"
"github.com/pkg/errors"
)
Expand Down Expand Up @@ -340,7 +341,7 @@ type ShellDependantCmdLine struct {
// RUN ["echo", "hi"] # echo hi
type RunCommand struct {
withNameAndCode
withExternalData
WithInstructionHook
ShellDependantCmdLine
FlagsUsed []string
}
Expand Down Expand Up @@ -551,3 +552,21 @@ func (c *withExternalData) setExternalValue(k, v interface{}) {
}
c.m[k] = v
}

type WithInstructionHook struct {
withExternalData
}

const instHookKey = "dockerfile/run/instruction-hook"

func (c *WithInstructionHook) GetInstructionHook() *types.InstructionHook {
x := c.getExternalValue(instHookKey)
if x == nil {
return nil
}
return x.(*types.InstructionHook)
}

func (c *WithInstructionHook) SetInstructionHook(h *types.InstructionHook) {
c.setExternalValue(instHookKey, h)
}
15 changes: 12 additions & 3 deletions frontend/dockerfile/instructions/commands_runmount.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,13 +84,22 @@ func setMountState(cmd *RunCommand, expander SingleWordExpander) error {
if st == nil {
return errors.Errorf("no mount state")
}
mounts := make([]*Mount, len(st.flag.StringValues))
for i, str := range st.flag.StringValues {
var mounts []*Mount
if hook := cmd.GetInstructionHook(); hook != nil && hook.Run != nil {
for _, m := range hook.Run.Mounts {
m := m
if err := validateMount(&m, false); err != nil {
return err
}
mounts = append(mounts, &m)
}
}
for _, str := range st.flag.StringValues {
m, err := parseMount(str, expander)
if err != nil {
return err
}
mounts[i] = m
mounts = append(mounts, m)
}
st.mounts = mounts
return nil
Expand Down
22 changes: 15 additions & 7 deletions frontend/dockerfile/instructions/parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,18 @@ import (
"github.com/moby/buildkit/frontend/dockerfile/command"
"github.com/moby/buildkit/frontend/dockerfile/linter"
"github.com/moby/buildkit/frontend/dockerfile/parser"
"github.com/moby/buildkit/frontend/dockerui/types"
"github.com/moby/buildkit/util/suggest"
dockerspec "github.com/moby/docker-image-spec/specs-go/v1"
"github.com/pkg/errors"
)

var excludePatternsEnabled = false

type ParseOpts struct {
InstructionHook *types.InstructionHook
}

type parseRequest struct {
command string
args []string
Expand All @@ -31,6 +36,7 @@ type parseRequest struct {
original string
location []parser.Range
comments []string
opts ParseOpts
}

var parseRunPreHooks []func(*RunCommand, parseRequest) error
Expand Down Expand Up @@ -66,18 +72,19 @@ func newParseRequestFromNode(node *parser.Node) parseRequest {
}
}

func ParseInstruction(node *parser.Node) (v interface{}, err error) {
return ParseInstructionWithLinter(node, nil)
func ParseInstruction(node *parser.Node, opts ParseOpts) (v interface{}, err error) {
return ParseInstructionWithLinter(node, nil, opts)
}

// ParseInstruction converts an AST to a typed instruction (either a command or a build stage beginning when encountering a `FROM` statement)
func ParseInstructionWithLinter(node *parser.Node, lint *linter.Linter) (v interface{}, err error) {
func ParseInstructionWithLinter(node *parser.Node, lint *linter.Linter, opts ParseOpts) (v interface{}, err error) {
defer func() {
if err != nil {
err = parser.WithLocation(err, node.Location())
}
}()
req := newParseRequestFromNode(node)
req.opts = opts
switch strings.ToLower(node.Value) {
case command.Env:
return parseEnv(req)
Expand Down Expand Up @@ -130,8 +137,8 @@ func ParseInstructionWithLinter(node *parser.Node, lint *linter.Linter) (v inter
}

// ParseCommand converts an AST to a typed Command
func ParseCommand(node *parser.Node) (Command, error) {
s, err := ParseInstruction(node)
func ParseCommand(node *parser.Node, opts ParseOpts) (Command, error) {
s, err := ParseInstruction(node, opts)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -166,9 +173,9 @@ func (e *parseError) Unwrap() error {

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

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

for _, fn := range parseRunPreHooks {
if err := fn(cmd, req); err != nil {
Expand Down
6 changes: 3 additions & 3 deletions frontend/dockerfile/instructions/parse_heredoc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ func TestErrorCasesHeredoc(t *testing.T) {
t.Fatalf("Error when parsing Dockerfile: %s", err)
}
n := ast.AST.Children[0]
_, err = ParseInstruction(n)
_, err = ParseInstruction(n, ParseOpts{})
require.Error(t, err)
require.Contains(t, err.Error(), c.expectedError)
}
Expand Down Expand Up @@ -166,7 +166,7 @@ EOF`,
require.NoError(t, err)

n := ast.AST.Children[0]
comm, err := ParseInstruction(n)
comm, err := ParseInstruction(n, ParseOpts{})
require.NoError(t, err)

sd := comm.(*CopyCommand).SourcesAndDest
Expand Down Expand Up @@ -248,7 +248,7 @@ EOF`,
require.NoError(t, err)

n := ast.AST.Children[0]
comm, err := ParseInstruction(n)
comm, err := ParseInstruction(n, ParseOpts{})
require.NoError(t, err)
require.Equal(t, c.shell, comm.(*RunCommand).PrependShell)
require.Equal(t, c.command, comm.(*RunCommand).CmdLine)
Expand Down
Loading

0 comments on commit f4d9ead

Please sign in to comment.