Docker-DCO-1.1-Signed-off-by: Erik Hollensbe <github@hollensbe.org> (github: erikh)
Erik Hollensbe authored on 2014/08/07 14:56:44... | ... |
@@ -1,5 +1,12 @@ |
1 | 1 |
package evaluator |
2 | 2 |
|
3 |
+// This file contains the dispatchers for each command. Note that |
|
4 |
+// `nullDispatch` is not actually a command, but support for commands we parse |
|
5 |
+// but do nothing with. |
|
6 |
+// |
|
7 |
+// See evaluator.go for a higher level discussion of the whole evaluator |
|
8 |
+// package. |
|
9 |
+ |
|
3 | 10 |
import ( |
4 | 11 |
"fmt" |
5 | 12 |
"path/filepath" |
... | ... |
@@ -10,11 +17,16 @@ import ( |
10 | 10 |
"github.com/docker/docker/utils" |
11 | 11 |
) |
12 | 12 |
|
13 |
-// dispatch with no layer / parsing. |
|
13 |
+// dispatch with no layer / parsing. This is effectively not a command. |
|
14 | 14 |
func nullDispatch(b *buildFile, args []string) error { |
15 | 15 |
return nil |
16 | 16 |
} |
17 | 17 |
|
18 |
+// ENV foo bar |
|
19 |
+// |
|
20 |
+// Sets the environment variable foo to bar, also makes interpolation |
|
21 |
+// in the dockerfile available from the next statement on via ${foo}. |
|
22 |
+// |
|
18 | 23 |
func env(b *buildFile, args []string) error { |
19 | 24 |
if len(args) != 2 { |
20 | 25 |
return fmt.Errorf("ENV accepts two arguments") |
... | ... |
@@ -29,6 +41,9 @@ func env(b *buildFile, args []string) error { |
29 | 29 |
return b.commit("", b.config.Cmd, fmt.Sprintf("ENV %s=%s", key, b.env[key])) |
30 | 30 |
} |
31 | 31 |
|
32 |
+// MAINTAINER some text <maybe@an.email.address> |
|
33 |
+// |
|
34 |
+// Sets the maintainer metadata. |
|
32 | 35 |
func maintainer(b *buildFile, args []string) error { |
33 | 36 |
if len(args) != 1 { |
34 | 37 |
return fmt.Errorf("MAINTAINER requires only one argument") |
... | ... |
@@ -38,6 +53,11 @@ func maintainer(b *buildFile, args []string) error { |
38 | 38 |
return b.commit("", b.config.Cmd, fmt.Sprintf("MAINTAINER %s", b.maintainer)) |
39 | 39 |
} |
40 | 40 |
|
41 |
+// ADD foo /path |
|
42 |
+// |
|
43 |
+// Add the file 'foo' to '/path'. Tarball and Remote URL (git, http) handling |
|
44 |
+// exist here. If you do not wish to have this automatic handling, use COPY. |
|
45 |
+// |
|
41 | 46 |
func add(b *buildFile, args []string) error { |
42 | 47 |
if len(args) != 2 { |
43 | 48 |
return fmt.Errorf("ADD requires two arguments") |
... | ... |
@@ -46,6 +66,10 @@ func add(b *buildFile, args []string) error { |
46 | 46 |
return b.runContextCommand(args, true, true, "ADD") |
47 | 47 |
} |
48 | 48 |
|
49 |
+// COPY foo /path |
|
50 |
+// |
|
51 |
+// Same as 'ADD' but without the tar and remote url handling. |
|
52 |
+// |
|
49 | 53 |
func dispatchCopy(b *buildFile, args []string) error { |
50 | 54 |
if len(args) != 2 { |
51 | 55 |
return fmt.Errorf("COPY requires two arguments") |
... | ... |
@@ -54,6 +78,10 @@ func dispatchCopy(b *buildFile, args []string) error { |
54 | 54 |
return b.runContextCommand(args, false, false, "COPY") |
55 | 55 |
} |
56 | 56 |
|
57 |
+// FROM imagename |
|
58 |
+// |
|
59 |
+// This sets the image the dockerfile will build on top of. |
|
60 |
+// |
|
57 | 61 |
func from(b *buildFile, args []string) error { |
58 | 62 |
if len(args) != 1 { |
59 | 63 |
return fmt.Errorf("FROM requires one argument") |
... | ... |
@@ -77,6 +105,15 @@ func from(b *buildFile, args []string) error { |
77 | 77 |
return b.processImageFrom(image) |
78 | 78 |
} |
79 | 79 |
|
80 |
+// ONBUILD RUN echo yo |
|
81 |
+// |
|
82 |
+// ONBUILD triggers run when the image is used in a FROM statement. |
|
83 |
+// |
|
84 |
+// ONBUILD handling has a lot of special-case functionality, the heading in |
|
85 |
+// evaluator.go and comments around dispatch() in the same file explain the |
|
86 |
+// special cases. search for 'OnBuild' in internals.go for additional special |
|
87 |
+// cases. |
|
88 |
+// |
|
80 | 89 |
func onbuild(b *buildFile, args []string) error { |
81 | 90 |
triggerInstruction := strings.ToUpper(strings.TrimSpace(args[0])) |
82 | 91 |
switch triggerInstruction { |
... | ... |
@@ -92,6 +129,10 @@ func onbuild(b *buildFile, args []string) error { |
92 | 92 |
return b.commit("", b.config.Cmd, fmt.Sprintf("ONBUILD %s", trigger)) |
93 | 93 |
} |
94 | 94 |
|
95 |
+// WORKDIR /tmp |
|
96 |
+// |
|
97 |
+// Set the working directory for future RUN/CMD/etc statements. |
|
98 |
+// |
|
95 | 99 |
func workdir(b *buildFile, args []string) error { |
96 | 100 |
if len(args) != 1 { |
97 | 101 |
return fmt.Errorf("WORKDIR requires exactly one argument") |
... | ... |
@@ -111,6 +152,15 @@ func workdir(b *buildFile, args []string) error { |
111 | 111 |
return b.commit("", b.config.Cmd, fmt.Sprintf("WORKDIR %v", workdir)) |
112 | 112 |
} |
113 | 113 |
|
114 |
+// RUN some command yo |
|
115 |
+// |
|
116 |
+// run a command and commit the image. Args are automatically prepended with |
|
117 |
+// 'sh -c' in the event there is only one argument. The difference in |
|
118 |
+// processing: |
|
119 |
+// |
|
120 |
+// RUN echo hi # sh -c echo hi |
|
121 |
+// RUN [ "echo", "hi" ] # echo hi |
|
122 |
+// |
|
114 | 123 |
func run(b *buildFile, args []string) error { |
115 | 124 |
if len(args) == 1 { // literal string command, not an exec array |
116 | 125 |
args = append([]string{"/bin/sh", "-c"}, args[0]) |
... | ... |
@@ -162,6 +212,11 @@ func run(b *buildFile, args []string) error { |
162 | 162 |
return nil |
163 | 163 |
} |
164 | 164 |
|
165 |
+// CMD foo |
|
166 |
+// |
|
167 |
+// Set the default command to run in the container (which may be empty). |
|
168 |
+// Argument handling is the same as RUN. |
|
169 |
+// |
|
165 | 170 |
func cmd(b *buildFile, args []string) error { |
166 | 171 |
if len(args) < 2 { |
167 | 172 |
args = append([]string{"/bin/sh", "-c"}, args...) |
... | ... |
@@ -176,6 +231,14 @@ func cmd(b *buildFile, args []string) error { |
176 | 176 |
return nil |
177 | 177 |
} |
178 | 178 |
|
179 |
+// ENTRYPOINT /usr/sbin/nginx |
|
180 |
+// |
|
181 |
+// Set the entrypoint (which defaults to sh -c) to /usr/sbin/nginx. Will |
|
182 |
+// accept the CMD as the arguments to /usr/sbin/nginx. |
|
183 |
+// |
|
184 |
+// Handles command processing similar to CMD and RUN, only b.config.Entrypoint |
|
185 |
+// is initialized at NewBuilder time instead of through argument parsing. |
|
186 |
+// |
|
179 | 187 |
func entrypoint(b *buildFile, args []string) error { |
180 | 188 |
b.config.Entrypoint = args |
181 | 189 |
|
... | ... |
@@ -189,6 +252,11 @@ func entrypoint(b *buildFile, args []string) error { |
189 | 189 |
return nil |
190 | 190 |
} |
191 | 191 |
|
192 |
+// EXPOSE 6667/tcp 7000/tcp |
|
193 |
+// |
|
194 |
+// Expose ports for links and port mappings. This all ends up in |
|
195 |
+// b.config.ExposedPorts for runconfig. |
|
196 |
+// |
|
192 | 197 |
func expose(b *buildFile, args []string) error { |
193 | 198 |
portsTab := args |
194 | 199 |
|
... | ... |
@@ -211,6 +279,11 @@ func expose(b *buildFile, args []string) error { |
211 | 211 |
return b.commit("", b.config.Cmd, fmt.Sprintf("EXPOSE %v", ports)) |
212 | 212 |
} |
213 | 213 |
|
214 |
+// USER foo |
|
215 |
+// |
|
216 |
+// Set the user to 'foo' for future commands and when running the |
|
217 |
+// ENTRYPOINT/CMD at container run time. |
|
218 |
+// |
|
214 | 219 |
func user(b *buildFile, args []string) error { |
215 | 220 |
if len(args) != 1 { |
216 | 221 |
return fmt.Errorf("USER requires exactly one argument") |
... | ... |
@@ -220,6 +293,11 @@ func user(b *buildFile, args []string) error { |
220 | 220 |
return b.commit("", b.config.Cmd, fmt.Sprintf("USER %v", args)) |
221 | 221 |
} |
222 | 222 |
|
223 |
+// VOLUME /foo |
|
224 |
+// |
|
225 |
+// Expose the volume /foo for use. Will also accept the JSON form, but either |
|
226 |
+// way requires exactly one argument. |
|
227 |
+// |
|
223 | 228 |
func volume(b *buildFile, args []string) error { |
224 | 229 |
if len(args) != 1 { |
225 | 230 |
return fmt.Errorf("Volume cannot be empty") |
... | ... |
@@ -239,6 +317,7 @@ func volume(b *buildFile, args []string) error { |
239 | 239 |
return nil |
240 | 240 |
} |
241 | 241 |
|
242 |
+// INSERT is no longer accepted, but we still parse it. |
|
242 | 243 |
func insert(b *buildFile, args []string) error { |
243 | 244 |
return fmt.Errorf("INSERT has been deprecated. Please use ADD instead") |
244 | 245 |
} |
... | ... |
@@ -1,3 +1,22 @@ |
1 |
+// evaluator is the evaluation step in the Dockerfile parse/evaluate pipeline. |
|
2 |
+// |
|
3 |
+// It incorporates a dispatch table based on the parser.Node values (see the |
|
4 |
+// parser package for more information) that are yielded from the parser itself. |
|
5 |
+// Calling NewBuilder with the BuildOpts struct can be used to customize the |
|
6 |
+// experience for execution purposes only. Parsing is controlled in the parser |
|
7 |
+// package, and this division of resposibility should be respected. |
|
8 |
+// |
|
9 |
+// Please see the jump table targets for the actual invocations, most of which |
|
10 |
+// will call out to the functions in internals.go to deal with their tasks. |
|
11 |
+// |
|
12 |
+// ONBUILD is a special case, which is covered in the onbuild() func in |
|
13 |
+// dispatchers.go. |
|
14 |
+// |
|
15 |
+// The evaluator uses the concept of "steps", which are usually each processable |
|
16 |
+// line in the Dockerfile. Each step is numbered and certain actions are taken |
|
17 |
+// before and after each step, such as creating an image ID and removing temporary |
|
18 |
+// containers and images. Note that ONBUILD creates a kinda-sorta "sub run" which |
|
19 |
+// includes its own set of steps (usually only one of them). |
|
1 | 20 |
package evaluator |
2 | 21 |
|
3 | 22 |
import ( |
... | ... |
@@ -49,32 +68,40 @@ func init() { |
49 | 49 |
type envMap map[string]string |
50 | 50 |
type uniqueMap map[string]struct{} |
51 | 51 |
|
52 |
+// internal struct, used to maintain configuration of the Dockerfile's |
|
53 |
+// processing as it evaluates the parsing result. |
|
52 | 54 |
type buildFile struct { |
53 |
- dockerfile *parser.Node |
|
54 |
- env envMap |
|
55 |
- image string |
|
56 |
- config *runconfig.Config |
|
57 |
- options *BuildOpts |
|
58 |
- maintainer string |
|
59 |
- |
|
60 |
- // cmdSet indicates is CMD was set in current Dockerfile |
|
61 |
- cmdSet bool |
|
62 |
- |
|
63 |
- context *tarsum.TarSum |
|
64 |
- contextPath string |
|
65 |
- tmpContainers uniqueMap |
|
66 |
- tmpImages uniqueMap |
|
55 |
+ dockerfile *parser.Node // the syntax tree of the dockerfile |
|
56 |
+ env envMap // map of environment variables |
|
57 |
+ image string // image name for commit processing |
|
58 |
+ config *runconfig.Config // runconfig for cmd, run, entrypoint etc. |
|
59 |
+ options *BuildOpts // see below |
|
60 |
+ maintainer string // maintainer name. could probably be removed. |
|
61 |
+ cmdSet bool // indicates is CMD was set in current Dockerfile |
|
62 |
+ context *tarsum.TarSum // the context is a tarball that is uploaded by the client |
|
63 |
+ contextPath string // the path of the temporary directory the local context is unpacked to (server side) |
|
64 |
+ |
|
65 |
+ // both of these are controlled by the Remove and ForceRemove options in BuildOpts |
|
66 |
+ tmpContainers uniqueMap // a map of containers used for removes |
|
67 |
+ tmpImages uniqueMap // a map of images used for removes |
|
67 | 68 |
} |
68 | 69 |
|
69 | 70 |
type BuildOpts struct { |
70 |
- Daemon *daemon.Daemon |
|
71 |
- Engine *engine.Engine |
|
72 |
- OutStream io.Writer |
|
73 |
- ErrStream io.Writer |
|
74 |
- Verbose bool |
|
75 |
- UtilizeCache bool |
|
76 |
- Remove bool |
|
77 |
- ForceRemove bool |
|
71 |
+ Daemon *daemon.Daemon |
|
72 |
+ Engine *engine.Engine |
|
73 |
+ |
|
74 |
+ // effectively stdio for the run. Because it is not stdio, I said |
|
75 |
+ // "Effectively". Do not use stdio anywhere in this package for any reason. |
|
76 |
+ OutStream io.Writer |
|
77 |
+ ErrStream io.Writer |
|
78 |
+ |
|
79 |
+ Verbose bool |
|
80 |
+ UtilizeCache bool |
|
81 |
+ |
|
82 |
+ // controls how images and containers are handled between steps. |
|
83 |
+ Remove bool |
|
84 |
+ ForceRemove bool |
|
85 |
+ |
|
78 | 86 |
AuthConfig *registry.AuthConfig |
79 | 87 |
AuthConfigFile *registry.ConfigFile |
80 | 88 |
|
... | ... |
@@ -83,6 +110,7 @@ type BuildOpts struct { |
83 | 83 |
StreamFormatter *utils.StreamFormatter |
84 | 84 |
} |
85 | 85 |
|
86 |
+// Create a new builder. |
|
86 | 87 |
func NewBuilder(opts *BuildOpts) (*buildFile, error) { |
87 | 88 |
return &buildFile{ |
88 | 89 |
dockerfile: nil, |
... | ... |
@@ -94,10 +122,20 @@ func NewBuilder(opts *BuildOpts) (*buildFile, error) { |
94 | 94 |
}, nil |
95 | 95 |
} |
96 | 96 |
|
97 |
+// Run the builder with the context. This is the lynchpin of this package. This |
|
98 |
+// will (barring errors): |
|
99 |
+// |
|
100 |
+// * call readContext() which will set up the temporary directory and unpack |
|
101 |
+// the context into it. |
|
102 |
+// * read the dockerfile |
|
103 |
+// * parse the dockerfile |
|
104 |
+// * walk the parse tree and execute it by dispatching to handlers. If Remove |
|
105 |
+// or ForceRemove is set, additional cleanup around containers happens after |
|
106 |
+// processing. |
|
107 |
+// * Print a happy message and return the image ID. |
|
108 |
+// |
|
97 | 109 |
func (b *buildFile) Run(context io.Reader) (string, error) { |
98 |
- err := b.readContext(context) |
|
99 |
- |
|
100 |
- if err != nil { |
|
110 |
+ if err := b.readContext(context); err != nil { |
|
101 | 111 |
return "", err |
102 | 112 |
} |
103 | 113 |
|
... | ... |
@@ -131,7 +169,7 @@ func (b *buildFile) Run(context io.Reader) (string, error) { |
131 | 131 |
} |
132 | 132 |
|
133 | 133 |
if b.image == "" { |
134 |
- return "", fmt.Errorf("No image was generated. This may be because the Dockerfile does not, like, do anything.\n") |
|
134 |
+ return "", fmt.Errorf("No image was generated. Is your Dockerfile empty?\n") |
|
135 | 135 |
} |
136 | 136 |
|
137 | 137 |
fmt.Fprintf(b.options.OutStream, "Successfully built %s\n", utils.TruncateID(b.image)) |
... | ... |
@@ -153,6 +191,20 @@ func initRunConfig() *runconfig.Config { |
153 | 153 |
} |
154 | 154 |
} |
155 | 155 |
|
156 |
+// This method is the entrypoint to all statement handling routines. |
|
157 |
+// |
|
158 |
+// Almost all nodes will have this structure: |
|
159 |
+// Child[Node, Node, Node] where Child is from parser.Node.Children and each |
|
160 |
+// node comes from parser.Node.Next. This forms a "line" with a statement and |
|
161 |
+// arguments and we process them in this normalized form by hitting |
|
162 |
+// evaluateTable with the leaf nodes of the command and the buildFile object. |
|
163 |
+// |
|
164 |
+// ONBUILD is a special case; in this case the parser will emit: |
|
165 |
+// Child[Node, Child[Node, Node...]] where the first node is the literal |
|
166 |
+// "onbuild" and the child entrypoint is the command of the ONBUILD statmeent, |
|
167 |
+// such as `RUN` in ONBUILD RUN foo. There is special case logic in here to |
|
168 |
+// deal with that, at least until it becomes more of a general concern with new |
|
169 |
+// features. |
|
156 | 170 |
func (b *buildFile) dispatch(stepN int, ast *parser.Node) error { |
157 | 171 |
cmd := ast.Value |
158 | 172 |
strs := []string{} |
... | ... |
@@ -9,6 +9,7 @@ var ( |
9 | 9 |
TOKEN_ENV_INTERPOLATION = regexp.MustCompile("(\\\\\\\\+|[^\\\\]|\\b|\\A)\\$({?)([[:alnum:]_]+)(}?)") |
10 | 10 |
) |
11 | 11 |
|
12 |
+// handle environment replacement. Used in dispatcher. |
|
12 | 13 |
func replaceEnv(b *buildFile, str string) string { |
13 | 14 |
for _, match := range TOKEN_ENV_INTERPOLATION.FindAllString(str, -1) { |
14 | 15 |
match = match[strings.Index(match, "$"):] |
... | ... |
@@ -1,5 +1,11 @@ |
1 | 1 |
package parser |
2 | 2 |
|
3 |
+// line parsers are dispatch calls that parse a single unit of text into a |
|
4 |
+// Node object which contains the whole statement. Dockerfiles have varied |
|
5 |
+// (but not usually unique, see ONBUILD for a unique example) parsing rules |
|
6 |
+// per-command, and these unify the processing in a way that makes it |
|
7 |
+// manageable. |
|
8 |
+ |
|
3 | 9 |
import ( |
4 | 10 |
"encoding/json" |
5 | 11 |
"strconv" |
... | ... |
@@ -12,6 +18,11 @@ func parseIgnore(rest string) (*Node, error) { |
12 | 12 |
return blankNode(), nil |
13 | 13 |
} |
14 | 14 |
|
15 |
+// used for onbuild. Could potentially be used for anything that represents a |
|
16 |
+// statement with sub-statements. |
|
17 |
+// |
|
18 |
+// ONBUILD RUN foo bar -> (onbuild (run foo bar)) |
|
19 |
+// |
|
15 | 20 |
func parseSubCommand(rest string) (*Node, error) { |
16 | 21 |
_, child, err := parseLine(rest) |
17 | 22 |
if err != nil { |
... | ... |
@@ -8,32 +8,17 @@ import ( |
8 | 8 |
"strings" |
9 | 9 |
) |
10 | 10 |
|
11 |
-// Node is the building block of the AST this package will create. |
|
11 |
+// Node is a structure used to represent a parse tree. |
|
12 | 12 |
// |
13 |
-// Nodes are structured to have a value, next, and child, the latter two of |
|
14 |
-// which are Nodes themselves. |
|
13 |
+// In the node there are three fields, Value, Next, and Children. Value is the |
|
14 |
+// current token's string value. Next is always the next non-child token, and |
|
15 |
+// children contains all the children. Here's an example: |
|
15 | 16 |
// |
16 |
-// This terminology is unfortunately rather confusing, so here's a diagram. |
|
17 |
-// Anything after the ; is a comment. |
|
17 |
+// (value next (child child-next child-next-next) next-next) |
|
18 | 18 |
// |
19 |
-// ( |
|
20 |
-// (run "foo") ; value run, and next is a value foo. |
|
21 |
-// (run "1" "2" "3") ; |
|
22 |
-// (something (really cool)) |
|
23 |
-// ) |
|
24 |
-// |
|
25 |
-// Will give you something like this: |
|
26 |
-// |
|
27 |
-// &Node{ |
|
28 |
-// Value:"", |
|
29 |
-// Child: &Node{Value: "run", Next: &Node{Value: "foo"}, Child: nil}, |
|
30 |
-// Next: &Node{Value:"", Child: &Node{Value:"run", Next: &Node{Value:`"1"`.... |
|
31 |
-// |
|
32 |
-// ... and so on. |
|
33 |
-// |
|
34 |
-// The short and fast rule is that anything that starts with ( is a child of |
|
35 |
-// something. Anything which follows a previous statement is a next of |
|
36 |
-// something. |
|
19 |
+// This data structure is frankly pretty lousy for handling complex languages, |
|
20 |
+// but lucky for us the Dockerfile isn't very complicated. This structure |
|
21 |
+// works a little more effectively than a "proper" parse tree for our needs. |
|
37 | 22 |
// |
38 | 23 |
type Node struct { |
39 | 24 |
Value string // actual content |
... | ... |
@@ -79,6 +64,7 @@ func blankNode() *Node { |
79 | 79 |
return &Node{"", nil, []*Node{}} |
80 | 80 |
} |
81 | 81 |
|
82 |
+// parse a line and return the remainder. |
|
82 | 83 |
func parseLine(line string) (string, *Node, error) { |
83 | 84 |
if line = stripComments(line); line == "" { |
84 | 85 |
return "", nil, nil |