fix(misconf): handle heredocs in dockerfile instructions (#8284)

Signed-off-by: nikpivkin <nikita.pivkin@smartforce.io>
This commit is contained in:
Nikita Pivkin
2025-01-29 07:18:15 +06:00
committed by GitHub
parent 846498dd23
commit 0a3887ca03
2 changed files with 114 additions and 3 deletions

View File

@@ -30,7 +30,7 @@ func Parse(_ context.Context, r io.Reader, path string) (any, error) {
instr, err := instructions.ParseInstruction(child)
if err != nil {
return nil, fmt.Errorf("process dockerfile instructions: %w", err)
return nil, fmt.Errorf("parse dockerfile instruction: %w", err)
}
if _, ok := instr.(*instructions.Stage); ok {
@@ -56,14 +56,27 @@ func Parse(_ context.Context, r io.Reader, path string) (any, error) {
EndLine: child.EndLine,
}
// processing statement with sub-statement
// example: ONBUILD RUN foo bar
// https://github.com/moby/buildkit/blob/master/frontend/dockerfile/docs/reference.md#onbuild
if child.Next != nil && len(child.Next.Children) > 0 {
cmd.SubCmd = child.Next.Children[0].Value
child = child.Next.Children[0]
}
// mark if the instruction is in exec form
// https://github.com/moby/buildkit/blob/master/frontend/dockerfile/docs/reference.md#exec-form
cmd.JSON = child.Attributes["json"]
for n := child.Next; n != nil; n = n.Next {
cmd.Value = append(cmd.Value, n.Value)
// heredoc may contain a script that will be executed in the shell, so we need to process it
// https://github.com/moby/buildkit/blob/master/frontend/dockerfile/docs/reference.md#here-documents
if len(child.Heredocs) > 0 && child.Next != nil {
cmd.Original = originalFromHeredoc(child)
cmd.Value = []string{processHeredoc(child)}
} else {
for n := child.Next; n != nil; n = n.Next {
cmd.Value = append(cmd.Value, n.Value)
}
}
stage.Commands = append(stage.Commands, cmd)
@@ -75,3 +88,44 @@ func Parse(_ context.Context, r io.Reader, path string) (any, error) {
return &parsedFile, nil
}
func originalFromHeredoc(node *parser.Node) string {
var sb strings.Builder
sb.WriteString(node.Original)
sb.WriteRune('\n')
for i, heredoc := range node.Heredocs {
sb.WriteString(heredoc.Content)
sb.WriteString(heredoc.Name)
if i != len(node.Heredocs)-1 {
sb.WriteRune('\n')
}
}
return sb.String()
}
// heredoc processing taken from here
// https://github.com/moby/buildkit/blob/9a39e2c112b7c98353c27e64602bc08f31fe356e/frontend/dockerfile/dockerfile2llb/convert.go#L1200
func processHeredoc(node *parser.Node) string {
if parser.MustParseHeredoc(node.Next.Value) == nil || strings.HasPrefix(node.Heredocs[0].Content, "#!") {
// more complex heredoc is passed to the shell as is
var sb strings.Builder
sb.WriteString(node.Next.Value)
for _, heredoc := range node.Heredocs {
sb.WriteRune('\n')
sb.WriteString(heredoc.Content)
sb.WriteString(heredoc.Name)
}
return sb.String()
}
// simple heredoc and the content is run in a shell
content := node.Heredocs[0].Content
if node.Heredocs[0].Chomp {
content = parser.ChompHeredocContent(content)
}
content = strings.ReplaceAll(content, "\r\n", "\n")
cmds := strings.Split(strings.TrimSuffix(content, "\n"), "\n")
return strings.Join(cmds, " ; ")
}

View File

@@ -59,3 +59,60 @@ CMD python /app/app.py
assert.Equal(t, 4, commands[3].StartLine)
assert.Equal(t, 4, commands[3].EndLine)
}
func Test_ParseHeredocs(t *testing.T) {
tests := []struct {
name string
src string
expected string
}{
{
name: "multi-line script",
src: `RUN <<EOF
apk add curl
apk add git
EOF`,
expected: "apk add curl ; apk add git",
},
{
name: "file redirection and chained command",
src: `RUN cat <<EOF > /tmp/output && echo 'done'
hello
mr
potato
EOF`,
expected: "cat <<EOF > /tmp/output && echo 'done'\nhello\nmr\npotato\nEOF",
},
{
name: "redirect to file",
src: `RUN <<EOF > /etc/config.yaml
key1: value1
key2: value2
EOF`,
expected: "<<EOF > /etc/config.yaml\nkey1: value1\nkey2: value2\nEOF",
},
{
name: "with a shebang",
src: `RUN <<EOF
#!/usr/bin/env python
print("hello world")
EOF`,
expected: "<<EOF\n#!/usr/bin/env python\nprint(\"hello world\")\nEOF",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
res, err := parser.Parse(context.TODO(), strings.NewReader(tt.src), "Dockerfile")
require.NoError(t, err)
df, ok := res.(*dockerfile.Dockerfile)
require.True(t, ok)
cmd := df.Stages[0].Commands[0]
assert.Equal(t, tt.src, cmd.Original)
assert.Equal(t, []string{tt.expected}, cmd.Value)
})
}
}