summaryrefslogtreecommitdiff
path: root/evaluator.go
blob: b05f6c647b8f9ddb59d0f8b43bedbc1422d3180d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
package imagebuilder

import (
	"fmt"
	"io"
	"strings"

	"github.com/openshift/imagebuilder/dockerfile/command"
	"github.com/openshift/imagebuilder/dockerfile/parser"
)

// ParseDockerfile parses the provided stream as a canonical Dockerfile
func ParseDockerfile(r io.Reader) (*parser.Node, error) {
	result, err := parser.Parse(r)
	if err != nil {
		return nil, err
	}
	return result.AST, nil
}

// Environment variable interpolation will happen on these statements only.
var replaceEnvAllowed = map[string]bool{
	command.Env:        true,
	command.Label:      true,
	command.Add:        true,
	command.Copy:       true,
	command.Workdir:    true,
	command.Expose:     true,
	command.Volume:     true,
	command.User:       true,
	command.StopSignal: true,
	command.Arg:        true,
}

// Certain commands are allowed to have their args split into more
// words after env var replacements. Meaning:
//
//	ENV foo="123 456"
//	EXPOSE $foo
//
// should result in the same thing as:
//
//	EXPOSE 123 456
//
// and not treat "123 456" as a single word.
// Note that: EXPOSE "$foo" and EXPOSE $foo are not the same thing.
// Quotes will cause it to still be treated as single word.
var allowWordExpansion = map[string]bool{
	command.Expose: true,
}

// Step represents the input Env and the output command after all
// post processing of the command arguments is done.
type Step struct {
	Env []string

	Command  string
	Args     []string
	Flags    []string
	Attrs    map[string]bool
	Message  string
	Original string
}

// Resolve transforms a parsed Dockerfile line into a command to execute,
// resolving any arguments.
//
// Almost all nodes will have this structure:
// Child[Node, Node, Node] where Child is from parser.Node.Children and each
// node comes from parser.Node.Next. This forms a "line" with a statement and
// arguments and we process them in this normalized form by hitting
// evaluateTable with the leaf nodes of the command and the Builder object.
//
// ONBUILD is a special case; in this case the parser will emit:
// Child[Node, Child[Node, Node...]] where the first node is the literal
// "onbuild" and the child entrypoint is the command of the ONBUILD statement,
// such as `RUN` in ONBUILD RUN foo. There is special case logic in here to
// deal with that, at least until it becomes more of a general concern with new
// features.
func (b *Step) Resolve(ast *parser.Node) error {
	cmd := ast.Value
	upperCasedCmd := strings.ToUpper(cmd)

	// To ensure the user is given a decent error message if the platform
	// on which the daemon is running does not support a builder command.
	if err := platformSupports(strings.ToLower(cmd)); err != nil {
		return err
	}

	attrs := ast.Attributes
	original := ast.Original
	flags := ast.Flags
	strList := []string{}
	msg := upperCasedCmd

	if len(ast.Flags) > 0 {
		msg += " " + strings.Join(ast.Flags, " ")
	}

	if cmd == "onbuild" {
		if ast.Next == nil {
			return fmt.Errorf("ONBUILD requires at least one argument")
		}
		ast = ast.Next.Children[0]
		strList = append(strList, ast.Value)
		msg += " " + ast.Value

		if len(ast.Flags) > 0 {
			msg += " " + strings.Join(ast.Flags, " ")
		}

	}

	// count the number of nodes that we are going to traverse first
	// so we can pre-create the argument and message array. This speeds up the
	// allocation of those list a lot when they have a lot of arguments
	cursor := ast
	var n int
	for cursor.Next != nil {
		cursor = cursor.Next
		n++
	}
	msgList := make([]string, n)

	var i int
	envs := b.Env
	for ast.Next != nil {
		ast = ast.Next
		str := ast.Value
		if replaceEnvAllowed[cmd] {
			var err error
			var words []string

			if allowWordExpansion[cmd] {
				words, err = ProcessWords(str, envs)
				if err != nil {
					return err
				}
				strList = append(strList, words...)
			} else {
				str, err = ProcessWord(str, envs)
				if err != nil {
					return err
				}
				strList = append(strList, str)
			}
		} else {
			strList = append(strList, str)
		}
		msgList[i] = ast.Value
		i++
	}

	msg += " " + strings.Join(msgList, " ")

	b.Message = msg
	b.Command = cmd
	b.Args = strList
	b.Original = original
	b.Attrs = attrs
	b.Flags = flags
	return nil
}