This is a work base to introduce more features like build time
dockerfile optimisations, dependency analysis and parallel build, as
well as a first step to go from a dispatch-inline process to a
frontend+backend process.
Signed-off-by: Simon Ferquel <simon.ferquel@docker.com>
1 | 1 |
deleted file mode 100644 |
... | ... |
@@ -1,183 +0,0 @@ |
1 |
-package dockerfile |
|
2 |
- |
|
3 |
-import ( |
|
4 |
- "fmt" |
|
5 |
- "strings" |
|
6 |
-) |
|
7 |
- |
|
8 |
-// FlagType is the type of the build flag |
|
9 |
-type FlagType int |
|
10 |
- |
|
11 |
-const ( |
|
12 |
- boolType FlagType = iota |
|
13 |
- stringType |
|
14 |
-) |
|
15 |
- |
|
16 |
-// BFlags contains all flags information for the builder |
|
17 |
-type BFlags struct { |
|
18 |
- Args []string // actual flags/args from cmd line |
|
19 |
- flags map[string]*Flag |
|
20 |
- used map[string]*Flag |
|
21 |
- Err error |
|
22 |
-} |
|
23 |
- |
|
24 |
-// Flag contains all information for a flag |
|
25 |
-type Flag struct { |
|
26 |
- bf *BFlags |
|
27 |
- name string |
|
28 |
- flagType FlagType |
|
29 |
- Value string |
|
30 |
-} |
|
31 |
- |
|
32 |
-// NewBFlags returns the new BFlags struct |
|
33 |
-func NewBFlags() *BFlags { |
|
34 |
- return &BFlags{ |
|
35 |
- flags: make(map[string]*Flag), |
|
36 |
- used: make(map[string]*Flag), |
|
37 |
- } |
|
38 |
-} |
|
39 |
- |
|
40 |
-// NewBFlagsWithArgs returns the new BFlags struct with Args set to args |
|
41 |
-func NewBFlagsWithArgs(args []string) *BFlags { |
|
42 |
- flags := NewBFlags() |
|
43 |
- flags.Args = args |
|
44 |
- return flags |
|
45 |
-} |
|
46 |
- |
|
47 |
-// AddBool adds a bool flag to BFlags |
|
48 |
-// Note, any error will be generated when Parse() is called (see Parse). |
|
49 |
-func (bf *BFlags) AddBool(name string, def bool) *Flag { |
|
50 |
- flag := bf.addFlag(name, boolType) |
|
51 |
- if flag == nil { |
|
52 |
- return nil |
|
53 |
- } |
|
54 |
- if def { |
|
55 |
- flag.Value = "true" |
|
56 |
- } else { |
|
57 |
- flag.Value = "false" |
|
58 |
- } |
|
59 |
- return flag |
|
60 |
-} |
|
61 |
- |
|
62 |
-// AddString adds a string flag to BFlags |
|
63 |
-// Note, any error will be generated when Parse() is called (see Parse). |
|
64 |
-func (bf *BFlags) AddString(name string, def string) *Flag { |
|
65 |
- flag := bf.addFlag(name, stringType) |
|
66 |
- if flag == nil { |
|
67 |
- return nil |
|
68 |
- } |
|
69 |
- flag.Value = def |
|
70 |
- return flag |
|
71 |
-} |
|
72 |
- |
|
73 |
-// addFlag is a generic func used by the other AddXXX() func |
|
74 |
-// to add a new flag to the BFlags struct. |
|
75 |
-// Note, any error will be generated when Parse() is called (see Parse). |
|
76 |
-func (bf *BFlags) addFlag(name string, flagType FlagType) *Flag { |
|
77 |
- if _, ok := bf.flags[name]; ok { |
|
78 |
- bf.Err = fmt.Errorf("Duplicate flag defined: %s", name) |
|
79 |
- return nil |
|
80 |
- } |
|
81 |
- |
|
82 |
- newFlag := &Flag{ |
|
83 |
- bf: bf, |
|
84 |
- name: name, |
|
85 |
- flagType: flagType, |
|
86 |
- } |
|
87 |
- bf.flags[name] = newFlag |
|
88 |
- |
|
89 |
- return newFlag |
|
90 |
-} |
|
91 |
- |
|
92 |
-// IsUsed checks if the flag is used |
|
93 |
-func (fl *Flag) IsUsed() bool { |
|
94 |
- if _, ok := fl.bf.used[fl.name]; ok { |
|
95 |
- return true |
|
96 |
- } |
|
97 |
- return false |
|
98 |
-} |
|
99 |
- |
|
100 |
-// IsTrue checks if a bool flag is true |
|
101 |
-func (fl *Flag) IsTrue() bool { |
|
102 |
- if fl.flagType != boolType { |
|
103 |
- // Should never get here |
|
104 |
- panic(fmt.Errorf("Trying to use IsTrue on a non-boolean: %s", fl.name)) |
|
105 |
- } |
|
106 |
- return fl.Value == "true" |
|
107 |
-} |
|
108 |
- |
|
109 |
-// Parse parses and checks if the BFlags is valid. |
|
110 |
-// Any error noticed during the AddXXX() funcs will be generated/returned |
|
111 |
-// here. We do this because an error during AddXXX() is more like a |
|
112 |
-// compile time error so it doesn't matter too much when we stop our |
|
113 |
-// processing as long as we do stop it, so this allows the code |
|
114 |
-// around AddXXX() to be just: |
|
115 |
-// defFlag := AddString("description", "") |
|
116 |
-// w/o needing to add an if-statement around each one. |
|
117 |
-func (bf *BFlags) Parse() error { |
|
118 |
- // If there was an error while defining the possible flags |
|
119 |
- // go ahead and bubble it back up here since we didn't do it |
|
120 |
- // earlier in the processing |
|
121 |
- if bf.Err != nil { |
|
122 |
- return fmt.Errorf("Error setting up flags: %s", bf.Err) |
|
123 |
- } |
|
124 |
- |
|
125 |
- for _, arg := range bf.Args { |
|
126 |
- if !strings.HasPrefix(arg, "--") { |
|
127 |
- return fmt.Errorf("Arg should start with -- : %s", arg) |
|
128 |
- } |
|
129 |
- |
|
130 |
- if arg == "--" { |
|
131 |
- return nil |
|
132 |
- } |
|
133 |
- |
|
134 |
- arg = arg[2:] |
|
135 |
- value := "" |
|
136 |
- |
|
137 |
- index := strings.Index(arg, "=") |
|
138 |
- if index >= 0 { |
|
139 |
- value = arg[index+1:] |
|
140 |
- arg = arg[:index] |
|
141 |
- } |
|
142 |
- |
|
143 |
- flag, ok := bf.flags[arg] |
|
144 |
- if !ok { |
|
145 |
- return fmt.Errorf("Unknown flag: %s", arg) |
|
146 |
- } |
|
147 |
- |
|
148 |
- if _, ok = bf.used[arg]; ok { |
|
149 |
- return fmt.Errorf("Duplicate flag specified: %s", arg) |
|
150 |
- } |
|
151 |
- |
|
152 |
- bf.used[arg] = flag |
|
153 |
- |
|
154 |
- switch flag.flagType { |
|
155 |
- case boolType: |
|
156 |
- // value == "" is only ok if no "=" was specified |
|
157 |
- if index >= 0 && value == "" { |
|
158 |
- return fmt.Errorf("Missing a value on flag: %s", arg) |
|
159 |
- } |
|
160 |
- |
|
161 |
- lower := strings.ToLower(value) |
|
162 |
- if lower == "" { |
|
163 |
- flag.Value = "true" |
|
164 |
- } else if lower == "true" || lower == "false" { |
|
165 |
- flag.Value = lower |
|
166 |
- } else { |
|
167 |
- return fmt.Errorf("Expecting boolean value for flag %s, not: %s", arg, value) |
|
168 |
- } |
|
169 |
- |
|
170 |
- case stringType: |
|
171 |
- if index < 0 { |
|
172 |
- return fmt.Errorf("Missing a value on flag: %s", arg) |
|
173 |
- } |
|
174 |
- flag.Value = value |
|
175 |
- |
|
176 |
- default: |
|
177 |
- panic("No idea what kind of flag we have! Should never get here!") |
|
178 |
- } |
|
179 |
- |
|
180 |
- } |
|
181 |
- |
|
182 |
- return nil |
|
183 |
-} |
184 | 1 |
deleted file mode 100644 |
... | ... |
@@ -1,187 +0,0 @@ |
1 |
-package dockerfile |
|
2 |
- |
|
3 |
-import ( |
|
4 |
- "testing" |
|
5 |
-) |
|
6 |
- |
|
7 |
-func TestBuilderFlags(t *testing.T) { |
|
8 |
- var expected string |
|
9 |
- var err error |
|
10 |
- |
|
11 |
- // --- |
|
12 |
- |
|
13 |
- bf := NewBFlags() |
|
14 |
- bf.Args = []string{} |
|
15 |
- if err := bf.Parse(); err != nil { |
|
16 |
- t.Fatalf("Test1 of %q was supposed to work: %s", bf.Args, err) |
|
17 |
- } |
|
18 |
- |
|
19 |
- // --- |
|
20 |
- |
|
21 |
- bf = NewBFlags() |
|
22 |
- bf.Args = []string{"--"} |
|
23 |
- if err := bf.Parse(); err != nil { |
|
24 |
- t.Fatalf("Test2 of %q was supposed to work: %s", bf.Args, err) |
|
25 |
- } |
|
26 |
- |
|
27 |
- // --- |
|
28 |
- |
|
29 |
- bf = NewBFlags() |
|
30 |
- flStr1 := bf.AddString("str1", "") |
|
31 |
- flBool1 := bf.AddBool("bool1", false) |
|
32 |
- bf.Args = []string{} |
|
33 |
- if err = bf.Parse(); err != nil { |
|
34 |
- t.Fatalf("Test3 of %q was supposed to work: %s", bf.Args, err) |
|
35 |
- } |
|
36 |
- |
|
37 |
- if flStr1.IsUsed() { |
|
38 |
- t.Fatal("Test3 - str1 was not used!") |
|
39 |
- } |
|
40 |
- if flBool1.IsUsed() { |
|
41 |
- t.Fatal("Test3 - bool1 was not used!") |
|
42 |
- } |
|
43 |
- |
|
44 |
- // --- |
|
45 |
- |
|
46 |
- bf = NewBFlags() |
|
47 |
- flStr1 = bf.AddString("str1", "HI") |
|
48 |
- flBool1 = bf.AddBool("bool1", false) |
|
49 |
- bf.Args = []string{} |
|
50 |
- |
|
51 |
- if err = bf.Parse(); err != nil { |
|
52 |
- t.Fatalf("Test4 of %q was supposed to work: %s", bf.Args, err) |
|
53 |
- } |
|
54 |
- |
|
55 |
- if flStr1.Value != "HI" { |
|
56 |
- t.Fatal("Str1 was supposed to default to: HI") |
|
57 |
- } |
|
58 |
- if flBool1.IsTrue() { |
|
59 |
- t.Fatal("Bool1 was supposed to default to: false") |
|
60 |
- } |
|
61 |
- if flStr1.IsUsed() { |
|
62 |
- t.Fatal("Str1 was not used!") |
|
63 |
- } |
|
64 |
- if flBool1.IsUsed() { |
|
65 |
- t.Fatal("Bool1 was not used!") |
|
66 |
- } |
|
67 |
- |
|
68 |
- // --- |
|
69 |
- |
|
70 |
- bf = NewBFlags() |
|
71 |
- flStr1 = bf.AddString("str1", "HI") |
|
72 |
- bf.Args = []string{"--str1"} |
|
73 |
- |
|
74 |
- if err = bf.Parse(); err == nil { |
|
75 |
- t.Fatalf("Test %q was supposed to fail", bf.Args) |
|
76 |
- } |
|
77 |
- |
|
78 |
- // --- |
|
79 |
- |
|
80 |
- bf = NewBFlags() |
|
81 |
- flStr1 = bf.AddString("str1", "HI") |
|
82 |
- bf.Args = []string{"--str1="} |
|
83 |
- |
|
84 |
- if err = bf.Parse(); err != nil { |
|
85 |
- t.Fatalf("Test %q was supposed to work: %s", bf.Args, err) |
|
86 |
- } |
|
87 |
- |
|
88 |
- expected = "" |
|
89 |
- if flStr1.Value != expected { |
|
90 |
- t.Fatalf("Str1 (%q) should be: %q", flStr1.Value, expected) |
|
91 |
- } |
|
92 |
- |
|
93 |
- // --- |
|
94 |
- |
|
95 |
- bf = NewBFlags() |
|
96 |
- flStr1 = bf.AddString("str1", "HI") |
|
97 |
- bf.Args = []string{"--str1=BYE"} |
|
98 |
- |
|
99 |
- if err = bf.Parse(); err != nil { |
|
100 |
- t.Fatalf("Test %q was supposed to work: %s", bf.Args, err) |
|
101 |
- } |
|
102 |
- |
|
103 |
- expected = "BYE" |
|
104 |
- if flStr1.Value != expected { |
|
105 |
- t.Fatalf("Str1 (%q) should be: %q", flStr1.Value, expected) |
|
106 |
- } |
|
107 |
- |
|
108 |
- // --- |
|
109 |
- |
|
110 |
- bf = NewBFlags() |
|
111 |
- flBool1 = bf.AddBool("bool1", false) |
|
112 |
- bf.Args = []string{"--bool1"} |
|
113 |
- |
|
114 |
- if err = bf.Parse(); err != nil { |
|
115 |
- t.Fatalf("Test %q was supposed to work: %s", bf.Args, err) |
|
116 |
- } |
|
117 |
- |
|
118 |
- if !flBool1.IsTrue() { |
|
119 |
- t.Fatal("Test-b1 Bool1 was supposed to be true") |
|
120 |
- } |
|
121 |
- |
|
122 |
- // --- |
|
123 |
- |
|
124 |
- bf = NewBFlags() |
|
125 |
- flBool1 = bf.AddBool("bool1", false) |
|
126 |
- bf.Args = []string{"--bool1=true"} |
|
127 |
- |
|
128 |
- if err = bf.Parse(); err != nil { |
|
129 |
- t.Fatalf("Test %q was supposed to work: %s", bf.Args, err) |
|
130 |
- } |
|
131 |
- |
|
132 |
- if !flBool1.IsTrue() { |
|
133 |
- t.Fatal("Test-b2 Bool1 was supposed to be true") |
|
134 |
- } |
|
135 |
- |
|
136 |
- // --- |
|
137 |
- |
|
138 |
- bf = NewBFlags() |
|
139 |
- flBool1 = bf.AddBool("bool1", false) |
|
140 |
- bf.Args = []string{"--bool1=false"} |
|
141 |
- |
|
142 |
- if err = bf.Parse(); err != nil { |
|
143 |
- t.Fatalf("Test %q was supposed to work: %s", bf.Args, err) |
|
144 |
- } |
|
145 |
- |
|
146 |
- if flBool1.IsTrue() { |
|
147 |
- t.Fatal("Test-b3 Bool1 was supposed to be false") |
|
148 |
- } |
|
149 |
- |
|
150 |
- // --- |
|
151 |
- |
|
152 |
- bf = NewBFlags() |
|
153 |
- flBool1 = bf.AddBool("bool1", false) |
|
154 |
- bf.Args = []string{"--bool1=false1"} |
|
155 |
- |
|
156 |
- if err = bf.Parse(); err == nil { |
|
157 |
- t.Fatalf("Test %q was supposed to fail", bf.Args) |
|
158 |
- } |
|
159 |
- |
|
160 |
- // --- |
|
161 |
- |
|
162 |
- bf = NewBFlags() |
|
163 |
- flBool1 = bf.AddBool("bool1", false) |
|
164 |
- bf.Args = []string{"--bool2"} |
|
165 |
- |
|
166 |
- if err = bf.Parse(); err == nil { |
|
167 |
- t.Fatalf("Test %q was supposed to fail", bf.Args) |
|
168 |
- } |
|
169 |
- |
|
170 |
- // --- |
|
171 |
- |
|
172 |
- bf = NewBFlags() |
|
173 |
- flStr1 = bf.AddString("str1", "HI") |
|
174 |
- flBool1 = bf.AddBool("bool1", false) |
|
175 |
- bf.Args = []string{"--bool1", "--str1=BYE"} |
|
176 |
- |
|
177 |
- if err = bf.Parse(); err != nil { |
|
178 |
- t.Fatalf("Test %q was supposed to work: %s", bf.Args, err) |
|
179 |
- } |
|
180 |
- |
|
181 |
- if flStr1.Value != "BYE" { |
|
182 |
- t.Fatalf("Test %s, str1 should be BYE", bf.Args) |
|
183 |
- } |
|
184 |
- if !flBool1.IsTrue() { |
|
185 |
- t.Fatalf("Test %s, bool1 should be true", bf.Args) |
|
186 |
- } |
|
187 |
-} |
... | ... |
@@ -42,6 +42,26 @@ func newBuildArgs(argsFromOptions map[string]*string) *buildArgs { |
42 | 42 |
} |
43 | 43 |
} |
44 | 44 |
|
45 |
+func (b *buildArgs) Clone() *buildArgs { |
|
46 |
+ result := newBuildArgs(b.argsFromOptions) |
|
47 |
+ for k, v := range b.allowedBuildArgs { |
|
48 |
+ result.allowedBuildArgs[k] = v |
|
49 |
+ } |
|
50 |
+ for k, v := range b.allowedMetaArgs { |
|
51 |
+ result.allowedMetaArgs[k] = v |
|
52 |
+ } |
|
53 |
+ for k := range b.referencedArgs { |
|
54 |
+ result.referencedArgs[k] = struct{}{} |
|
55 |
+ } |
|
56 |
+ return result |
|
57 |
+} |
|
58 |
+ |
|
59 |
+func (b *buildArgs) MergeReferencedArgs(other *buildArgs) { |
|
60 |
+ for k := range other.referencedArgs { |
|
61 |
+ b.referencedArgs[k] = struct{}{} |
|
62 |
+ } |
|
63 |
+} |
|
64 |
+ |
|
45 | 65 |
// WarnOnUnusedBuildArgs checks if there are any leftover build-args that were |
46 | 66 |
// passed but not consumed during build. Print a warning, if there are any. |
47 | 67 |
func (b *buildArgs) WarnOnUnusedBuildArgs(out io.Writer) { |
... | ... |
@@ -13,7 +13,7 @@ import ( |
13 | 13 |
"github.com/docker/docker/api/types/backend" |
14 | 14 |
"github.com/docker/docker/api/types/container" |
15 | 15 |
"github.com/docker/docker/builder" |
16 |
- "github.com/docker/docker/builder/dockerfile/command" |
|
16 |
+ "github.com/docker/docker/builder/dockerfile/instructions" |
|
17 | 17 |
"github.com/docker/docker/builder/dockerfile/parser" |
18 | 18 |
"github.com/docker/docker/builder/fscache" |
19 | 19 |
"github.com/docker/docker/builder/remotecontext" |
... | ... |
@@ -41,6 +41,10 @@ var validCommitCommands = map[string]bool{ |
41 | 41 |
"workdir": true, |
42 | 42 |
} |
43 | 43 |
|
44 |
+const ( |
|
45 |
+ stepFormat = "Step %d/%d : %v" |
|
46 |
+) |
|
47 |
+ |
|
44 | 48 |
// SessionGetter is object used to get access to a session by uuid |
45 | 49 |
type SessionGetter interface { |
46 | 50 |
Get(ctx context.Context, uuid string) (session.Caller, error) |
... | ... |
@@ -176,9 +180,7 @@ type Builder struct { |
176 | 176 |
clientCtx context.Context |
177 | 177 |
|
178 | 178 |
idMappings *idtools.IDMappings |
179 |
- buildStages *buildStages |
|
180 | 179 |
disableCommit bool |
181 |
- buildArgs *buildArgs |
|
182 | 180 |
imageSources *imageSources |
183 | 181 |
pathCache pathCache |
184 | 182 |
containerManager *containerManager |
... | ... |
@@ -218,8 +220,6 @@ func newBuilder(clientCtx context.Context, options builderOptions) *Builder { |
218 | 218 |
Output: options.ProgressWriter.Output, |
219 | 219 |
docker: options.Backend, |
220 | 220 |
idMappings: options.IDMappings, |
221 |
- buildArgs: newBuildArgs(config.BuildArgs), |
|
222 |
- buildStages: newBuildStages(), |
|
223 | 221 |
imageSources: newImageSources(clientCtx, options), |
224 | 222 |
pathCache: options.PathCache, |
225 | 223 |
imageProber: newImageProber(options.Backend, config.CacheFrom, options.Platform, config.NoCache), |
... | ... |
@@ -237,24 +237,27 @@ func (b *Builder) build(source builder.Source, dockerfile *parser.Result) (*buil |
237 | 237 |
|
238 | 238 |
addNodesForLabelOption(dockerfile.AST, b.options.Labels) |
239 | 239 |
|
240 |
- if err := checkDispatchDockerfile(dockerfile.AST); err != nil { |
|
241 |
- buildsFailed.WithValues(metricsDockerfileSyntaxError).Inc() |
|
240 |
+ stages, metaArgs, err := instructions.Parse(dockerfile.AST) |
|
241 |
+ if err != nil { |
|
242 |
+ if instructions.IsUnknownInstruction(err) { |
|
243 |
+ buildsFailed.WithValues(metricsUnknownInstructionError).Inc() |
|
244 |
+ } |
|
242 | 245 |
return nil, validationError{err} |
243 | 246 |
} |
247 |
+ if b.options.Target != "" { |
|
248 |
+ targetIx, found := instructions.HasStage(stages, b.options.Target) |
|
249 |
+ if !found { |
|
250 |
+ buildsFailed.WithValues(metricsBuildTargetNotReachableError).Inc() |
|
251 |
+ return nil, errors.Errorf("failed to reach build target %s in Dockerfile", b.options.Target) |
|
252 |
+ } |
|
253 |
+ stages = stages[:targetIx+1] |
|
254 |
+ } |
|
244 | 255 |
|
245 |
- dispatchState, err := b.dispatchDockerfileWithCancellation(dockerfile, source) |
|
256 |
+ dockerfile.PrintWarnings(b.Stderr) |
|
257 |
+ dispatchState, err := b.dispatchDockerfileWithCancellation(stages, metaArgs, dockerfile.EscapeToken, source) |
|
246 | 258 |
if err != nil { |
247 | 259 |
return nil, err |
248 | 260 |
} |
249 |
- |
|
250 |
- if b.options.Target != "" && !dispatchState.isCurrentStage(b.options.Target) { |
|
251 |
- buildsFailed.WithValues(metricsBuildTargetNotReachableError).Inc() |
|
252 |
- return nil, errors.Errorf("failed to reach build target %s in Dockerfile", b.options.Target) |
|
253 |
- } |
|
254 |
- |
|
255 |
- dockerfile.PrintWarnings(b.Stderr) |
|
256 |
- b.buildArgs.WarnOnUnusedBuildArgs(b.Stderr) |
|
257 |
- |
|
258 | 261 |
if dispatchState.imageID == "" { |
259 | 262 |
buildsFailed.WithValues(metricsDockerfileEmptyError).Inc() |
260 | 263 |
return nil, errors.New("No image was generated. Is your Dockerfile empty?") |
... | ... |
@@ -269,61 +272,91 @@ func emitImageID(aux *streamformatter.AuxFormatter, state *dispatchState) error |
269 | 269 |
return aux.Emit(types.BuildResult{ID: state.imageID}) |
270 | 270 |
} |
271 | 271 |
|
272 |
-func (b *Builder) dispatchDockerfileWithCancellation(dockerfile *parser.Result, source builder.Source) (*dispatchState, error) { |
|
273 |
- shlex := NewShellLex(dockerfile.EscapeToken) |
|
274 |
- state := newDispatchState() |
|
275 |
- total := len(dockerfile.AST.Children) |
|
276 |
- var err error |
|
277 |
- for i, n := range dockerfile.AST.Children { |
|
278 |
- select { |
|
279 |
- case <-b.clientCtx.Done(): |
|
280 |
- logrus.Debug("Builder: build cancelled!") |
|
281 |
- fmt.Fprint(b.Stdout, "Build cancelled") |
|
282 |
- buildsFailed.WithValues(metricsBuildCanceled).Inc() |
|
283 |
- return nil, errors.New("Build cancelled") |
|
284 |
- default: |
|
285 |
- // Not cancelled yet, keep going... |
|
286 |
- } |
|
272 |
+func processMetaArg(meta instructions.ArgCommand, shlex *ShellLex, args *buildArgs) error { |
|
273 |
+ // ShellLex currently only support the concatenated string format |
|
274 |
+ envs := convertMapToEnvList(args.GetAllAllowed()) |
|
275 |
+ if err := meta.Expand(func(word string) (string, error) { |
|
276 |
+ return shlex.ProcessWord(word, envs) |
|
277 |
+ }); err != nil { |
|
278 |
+ return err |
|
279 |
+ } |
|
280 |
+ args.AddArg(meta.Key, meta.Value) |
|
281 |
+ args.AddMetaArg(meta.Key, meta.Value) |
|
282 |
+ return nil |
|
283 |
+} |
|
287 | 284 |
|
288 |
- // If this is a FROM and we have a previous image then |
|
289 |
- // emit an aux message for that image since it is the |
|
290 |
- // end of the previous stage |
|
291 |
- if n.Value == command.From { |
|
292 |
- if err := emitImageID(b.Aux, state); err != nil { |
|
293 |
- return nil, err |
|
294 |
- } |
|
285 |
+func printCommand(out io.Writer, currentCommandIndex int, totalCommands int, cmd interface{}) int { |
|
286 |
+ fmt.Fprintf(out, stepFormat, currentCommandIndex, totalCommands, cmd) |
|
287 |
+ fmt.Fprintln(out) |
|
288 |
+ return currentCommandIndex + 1 |
|
289 |
+} |
|
290 |
+ |
|
291 |
+func (b *Builder) dispatchDockerfileWithCancellation(parseResult []instructions.Stage, metaArgs []instructions.ArgCommand, escapeToken rune, source builder.Source) (*dispatchState, error) { |
|
292 |
+ dispatchRequest := dispatchRequest{} |
|
293 |
+ buildArgs := newBuildArgs(b.options.BuildArgs) |
|
294 |
+ totalCommands := len(metaArgs) + len(parseResult) |
|
295 |
+ currentCommandIndex := 1 |
|
296 |
+ for _, stage := range parseResult { |
|
297 |
+ totalCommands += len(stage.Commands) |
|
298 |
+ } |
|
299 |
+ shlex := NewShellLex(escapeToken) |
|
300 |
+ for _, meta := range metaArgs { |
|
301 |
+ currentCommandIndex = printCommand(b.Stdout, currentCommandIndex, totalCommands, &meta) |
|
302 |
+ |
|
303 |
+ err := processMetaArg(meta, shlex, buildArgs) |
|
304 |
+ if err != nil { |
|
305 |
+ return nil, err |
|
295 | 306 |
} |
307 |
+ } |
|
308 |
+ |
|
309 |
+ stagesResults := newStagesBuildResults() |
|
296 | 310 |
|
297 |
- if n.Value == command.From && state.isCurrentStage(b.options.Target) { |
|
298 |
- break |
|
311 |
+ for _, stage := range parseResult { |
|
312 |
+ if err := stagesResults.checkStageNameAvailable(stage.Name); err != nil { |
|
313 |
+ return nil, err |
|
299 | 314 |
} |
315 |
+ dispatchRequest = newDispatchRequest(b, escapeToken, source, buildArgs, stagesResults) |
|
300 | 316 |
|
301 |
- opts := dispatchOptions{ |
|
302 |
- state: state, |
|
303 |
- stepMsg: formatStep(i, total), |
|
304 |
- node: n, |
|
305 |
- shlex: shlex, |
|
306 |
- source: source, |
|
317 |
+ currentCommandIndex = printCommand(b.Stdout, currentCommandIndex, totalCommands, stage.SourceCode) |
|
318 |
+ if err := initializeStage(dispatchRequest, &stage); err != nil { |
|
319 |
+ return nil, err |
|
307 | 320 |
} |
308 |
- if state, err = b.dispatch(opts); err != nil { |
|
309 |
- if b.options.ForceRemove { |
|
310 |
- b.containerManager.RemoveAll(b.Stdout) |
|
321 |
+ dispatchRequest.state.updateRunConfig() |
|
322 |
+ fmt.Fprintf(b.Stdout, " ---> %s\n", stringid.TruncateID(dispatchRequest.state.imageID)) |
|
323 |
+ for _, cmd := range stage.Commands { |
|
324 |
+ select { |
|
325 |
+ case <-b.clientCtx.Done(): |
|
326 |
+ logrus.Debug("Builder: build cancelled!") |
|
327 |
+ fmt.Fprint(b.Stdout, "Build cancelled\n") |
|
328 |
+ buildsFailed.WithValues(metricsBuildCanceled).Inc() |
|
329 |
+ return nil, errors.New("Build cancelled") |
|
330 |
+ default: |
|
331 |
+ // Not cancelled yet, keep going... |
|
311 | 332 |
} |
333 |
+ |
|
334 |
+ currentCommandIndex = printCommand(b.Stdout, currentCommandIndex, totalCommands, cmd) |
|
335 |
+ |
|
336 |
+ if err := dispatch(dispatchRequest, cmd); err != nil { |
|
337 |
+ return nil, err |
|
338 |
+ } |
|
339 |
+ |
|
340 |
+ dispatchRequest.state.updateRunConfig() |
|
341 |
+ fmt.Fprintf(b.Stdout, " ---> %s\n", stringid.TruncateID(dispatchRequest.state.imageID)) |
|
342 |
+ |
|
343 |
+ } |
|
344 |
+ if err := emitImageID(b.Aux, dispatchRequest.state); err != nil { |
|
312 | 345 |
return nil, err |
313 | 346 |
} |
314 |
- |
|
315 |
- fmt.Fprintf(b.Stdout, " ---> %s\n", stringid.TruncateID(state.imageID)) |
|
316 |
- if b.options.Remove { |
|
317 |
- b.containerManager.RemoveAll(b.Stdout) |
|
347 |
+ buildArgs.MergeReferencedArgs(dispatchRequest.state.buildArgs) |
|
348 |
+ if err := commitStage(dispatchRequest.state, stagesResults); err != nil { |
|
349 |
+ return nil, err |
|
318 | 350 |
} |
319 | 351 |
} |
320 |
- |
|
321 |
- // Emit a final aux message for the final image |
|
322 |
- if err := emitImageID(b.Aux, state); err != nil { |
|
323 |
- return nil, err |
|
352 |
+ if b.options.Remove { |
|
353 |
+ b.containerManager.RemoveAll(b.Stdout) |
|
324 | 354 |
} |
325 |
- |
|
326 |
- return state, nil |
|
355 |
+ buildArgs.WarnOnUnusedBuildArgs(b.Stdout) |
|
356 |
+ return dispatchRequest.state, nil |
|
327 | 357 |
} |
328 | 358 |
|
329 | 359 |
func addNodesForLabelOption(dockerfile *parser.Node, labels map[string]string) { |
... | ... |
@@ -380,39 +413,33 @@ func BuildFromConfig(config *container.Config, changes []string) (*container.Con |
380 | 380 |
b.Stderr = ioutil.Discard |
381 | 381 |
b.disableCommit = true |
382 | 382 |
|
383 |
- if err := checkDispatchDockerfile(dockerfile.AST); err != nil { |
|
384 |
- return nil, validationError{err} |
|
383 |
+ commands := []instructions.Command{} |
|
384 |
+ for _, n := range dockerfile.AST.Children { |
|
385 |
+ cmd, err := instructions.ParseCommand(n) |
|
386 |
+ if err != nil { |
|
387 |
+ return nil, validationError{err} |
|
388 |
+ } |
|
389 |
+ commands = append(commands, cmd) |
|
385 | 390 |
} |
386 |
- dispatchState := newDispatchState() |
|
387 |
- dispatchState.runConfig = config |
|
388 |
- return dispatchFromDockerfile(b, dockerfile, dispatchState, nil) |
|
389 |
-} |
|
390 | 391 |
|
391 |
-func checkDispatchDockerfile(dockerfile *parser.Node) error { |
|
392 |
- for _, n := range dockerfile.Children { |
|
393 |
- if err := checkDispatch(n); err != nil { |
|
394 |
- return errors.Wrapf(err, "Dockerfile parse error line %d", n.StartLine) |
|
392 |
+ dispatchRequest := newDispatchRequest(b, dockerfile.EscapeToken, nil, newBuildArgs(b.options.BuildArgs), newStagesBuildResults()) |
|
393 |
+ dispatchRequest.state.runConfig = config |
|
394 |
+ dispatchRequest.state.imageID = config.Image |
|
395 |
+ for _, cmd := range commands { |
|
396 |
+ err := dispatch(dispatchRequest, cmd) |
|
397 |
+ if err != nil { |
|
398 |
+ return nil, validationError{err} |
|
395 | 399 |
} |
400 |
+ dispatchRequest.state.updateRunConfig() |
|
396 | 401 |
} |
397 |
- return nil |
|
402 |
+ |
|
403 |
+ return dispatchRequest.state.runConfig, nil |
|
398 | 404 |
} |
399 | 405 |
|
400 |
-func dispatchFromDockerfile(b *Builder, result *parser.Result, dispatchState *dispatchState, source builder.Source) (*container.Config, error) { |
|
401 |
- shlex := NewShellLex(result.EscapeToken) |
|
402 |
- ast := result.AST |
|
403 |
- total := len(ast.Children) |
|
404 |
- |
|
405 |
- for i, n := range ast.Children { |
|
406 |
- opts := dispatchOptions{ |
|
407 |
- state: dispatchState, |
|
408 |
- stepMsg: formatStep(i, total), |
|
409 |
- node: n, |
|
410 |
- shlex: shlex, |
|
411 |
- source: source, |
|
412 |
- } |
|
413 |
- if _, err := b.dispatch(opts); err != nil { |
|
414 |
- return nil, err |
|
415 |
- } |
|
406 |
+func convertMapToEnvList(m map[string]string) []string { |
|
407 |
+ result := []string{} |
|
408 |
+ for k, v := range m { |
|
409 |
+ result = append(result, k+"="+v) |
|
416 | 410 |
} |
417 |
- return dispatchState.runConfig, nil |
|
411 |
+ return result |
|
418 | 412 |
} |
... | ... |
@@ -10,17 +10,15 @@ package dockerfile |
10 | 10 |
import ( |
11 | 11 |
"bytes" |
12 | 12 |
"fmt" |
13 |
- "regexp" |
|
14 | 13 |
"runtime" |
15 | 14 |
"sort" |
16 |
- "strconv" |
|
17 | 15 |
"strings" |
18 |
- "time" |
|
19 | 16 |
|
20 | 17 |
"github.com/docker/docker/api" |
21 | 18 |
"github.com/docker/docker/api/types/container" |
22 | 19 |
"github.com/docker/docker/api/types/strslice" |
23 | 20 |
"github.com/docker/docker/builder" |
21 |
+ "github.com/docker/docker/builder/dockerfile/instructions" |
|
24 | 22 |
"github.com/docker/docker/builder/dockerfile/parser" |
25 | 23 |
"github.com/docker/docker/image" |
26 | 24 |
"github.com/docker/docker/pkg/jsonmessage" |
... | ... |
@@ -36,32 +34,14 @@ import ( |
36 | 36 |
// Sets the environment variable foo to bar, also makes interpolation |
37 | 37 |
// in the dockerfile available from the next statement on via ${foo}. |
38 | 38 |
// |
39 |
-func env(req dispatchRequest) error { |
|
40 |
- if len(req.args) == 0 { |
|
41 |
- return errAtLeastOneArgument("ENV") |
|
42 |
- } |
|
43 |
- |
|
44 |
- if len(req.args)%2 != 0 { |
|
45 |
- // should never get here, but just in case |
|
46 |
- return errTooManyArguments("ENV") |
|
47 |
- } |
|
48 |
- |
|
49 |
- if err := req.flags.Parse(); err != nil { |
|
50 |
- return err |
|
51 |
- } |
|
52 |
- |
|
53 |
- runConfig := req.state.runConfig |
|
39 |
+func dispatchEnv(d dispatchRequest, c *instructions.EnvCommand) error { |
|
40 |
+ runConfig := d.state.runConfig |
|
54 | 41 |
commitMessage := bytes.NewBufferString("ENV") |
42 |
+ for _, e := range c.Env { |
|
43 |
+ name := e.Key |
|
44 |
+ newVar := e.String() |
|
55 | 45 |
|
56 |
- for j := 0; j < len(req.args); j += 2 { |
|
57 |
- if len(req.args[j]) == 0 { |
|
58 |
- return errBlankCommandNames("ENV") |
|
59 |
- } |
|
60 |
- name := req.args[j] |
|
61 |
- value := req.args[j+1] |
|
62 |
- newVar := name + "=" + value |
|
63 | 46 |
commitMessage.WriteString(" " + newVar) |
64 |
- |
|
65 | 47 |
gotOne := false |
66 | 48 |
for i, envVar := range runConfig.Env { |
67 | 49 |
envParts := strings.SplitN(envVar, "=", 2) |
... | ... |
@@ -76,64 +56,32 @@ func env(req dispatchRequest) error { |
76 | 76 |
runConfig.Env = append(runConfig.Env, newVar) |
77 | 77 |
} |
78 | 78 |
} |
79 |
- |
|
80 |
- return req.builder.commit(req.state, commitMessage.String()) |
|
79 |
+ return d.builder.commit(d.state, commitMessage.String()) |
|
81 | 80 |
} |
82 | 81 |
|
83 | 82 |
// MAINTAINER some text <maybe@an.email.address> |
84 | 83 |
// |
85 | 84 |
// Sets the maintainer metadata. |
86 |
-func maintainer(req dispatchRequest) error { |
|
87 |
- if len(req.args) != 1 { |
|
88 |
- return errExactlyOneArgument("MAINTAINER") |
|
89 |
- } |
|
85 |
+func dispatchMaintainer(d dispatchRequest, c *instructions.MaintainerCommand) error { |
|
90 | 86 |
|
91 |
- if err := req.flags.Parse(); err != nil { |
|
92 |
- return err |
|
93 |
- } |
|
94 |
- |
|
95 |
- maintainer := req.args[0] |
|
96 |
- req.state.maintainer = maintainer |
|
97 |
- return req.builder.commit(req.state, "MAINTAINER "+maintainer) |
|
87 |
+ d.state.maintainer = c.Maintainer |
|
88 |
+ return d.builder.commit(d.state, "MAINTAINER "+c.Maintainer) |
|
98 | 89 |
} |
99 | 90 |
|
100 | 91 |
// LABEL some json data describing the image |
101 | 92 |
// |
102 | 93 |
// Sets the Label variable foo to bar, |
103 | 94 |
// |
104 |
-func label(req dispatchRequest) error { |
|
105 |
- if len(req.args) == 0 { |
|
106 |
- return errAtLeastOneArgument("LABEL") |
|
107 |
- } |
|
108 |
- if len(req.args)%2 != 0 { |
|
109 |
- // should never get here, but just in case |
|
110 |
- return errTooManyArguments("LABEL") |
|
111 |
- } |
|
112 |
- |
|
113 |
- if err := req.flags.Parse(); err != nil { |
|
114 |
- return err |
|
95 |
+func dispatchLabel(d dispatchRequest, c *instructions.LabelCommand) error { |
|
96 |
+ if d.state.runConfig.Labels == nil { |
|
97 |
+ d.state.runConfig.Labels = make(map[string]string) |
|
115 | 98 |
} |
116 |
- |
|
117 | 99 |
commitStr := "LABEL" |
118 |
- runConfig := req.state.runConfig |
|
119 |
- |
|
120 |
- if runConfig.Labels == nil { |
|
121 |
- runConfig.Labels = map[string]string{} |
|
122 |
- } |
|
123 |
- |
|
124 |
- for j := 0; j < len(req.args); j++ { |
|
125 |
- name := req.args[j] |
|
126 |
- if name == "" { |
|
127 |
- return errBlankCommandNames("LABEL") |
|
128 |
- } |
|
129 |
- |
|
130 |
- value := req.args[j+1] |
|
131 |
- commitStr += " " + name + "=" + value |
|
132 |
- |
|
133 |
- runConfig.Labels[name] = value |
|
134 |
- j++ |
|
100 |
+ for _, v := range c.Labels { |
|
101 |
+ d.state.runConfig.Labels[v.Key] = v.Value |
|
102 |
+ commitStr += " " + v.String() |
|
135 | 103 |
} |
136 |
- return req.builder.commit(req.state, commitStr) |
|
104 |
+ return d.builder.commit(d.state, commitStr) |
|
137 | 105 |
} |
138 | 106 |
|
139 | 107 |
// ADD foo /path |
... | ... |
@@ -141,257 +89,172 @@ func label(req dispatchRequest) error { |
141 | 141 |
// Add the file 'foo' to '/path'. Tarball and Remote URL (git, http) handling |
142 | 142 |
// exist here. If you do not wish to have this automatic handling, use COPY. |
143 | 143 |
// |
144 |
-func add(req dispatchRequest) error { |
|
145 |
- if len(req.args) < 2 { |
|
146 |
- return errAtLeastTwoArguments("ADD") |
|
147 |
- } |
|
148 |
- |
|
149 |
- flChown := req.flags.AddString("chown", "") |
|
150 |
- if err := req.flags.Parse(); err != nil { |
|
151 |
- return err |
|
152 |
- } |
|
153 |
- |
|
154 |
- downloader := newRemoteSourceDownloader(req.builder.Output, req.builder.Stdout) |
|
155 |
- copier := copierFromDispatchRequest(req, downloader, nil) |
|
144 |
+func dispatchAdd(d dispatchRequest, c *instructions.AddCommand) error { |
|
145 |
+ downloader := newRemoteSourceDownloader(d.builder.Output, d.builder.Stdout) |
|
146 |
+ copier := copierFromDispatchRequest(d, downloader, nil) |
|
156 | 147 |
defer copier.Cleanup() |
157 |
- copyInstruction, err := copier.createCopyInstruction(req.args, "ADD") |
|
148 |
+ |
|
149 |
+ copyInstruction, err := copier.createCopyInstruction(c.SourcesAndDest, "ADD") |
|
158 | 150 |
if err != nil { |
159 | 151 |
return err |
160 | 152 |
} |
161 |
- copyInstruction.chownStr = flChown.Value |
|
153 |
+ copyInstruction.chownStr = c.Chown |
|
162 | 154 |
copyInstruction.allowLocalDecompression = true |
163 | 155 |
|
164 |
- return req.builder.performCopy(req.state, copyInstruction) |
|
156 |
+ return d.builder.performCopy(d.state, copyInstruction) |
|
165 | 157 |
} |
166 | 158 |
|
167 | 159 |
// COPY foo /path |
168 | 160 |
// |
169 | 161 |
// Same as 'ADD' but without the tar and remote url handling. |
170 | 162 |
// |
171 |
-func dispatchCopy(req dispatchRequest) error { |
|
172 |
- if len(req.args) < 2 { |
|
173 |
- return errAtLeastTwoArguments("COPY") |
|
174 |
- } |
|
175 |
- |
|
176 |
- flFrom := req.flags.AddString("from", "") |
|
177 |
- flChown := req.flags.AddString("chown", "") |
|
178 |
- if err := req.flags.Parse(); err != nil { |
|
179 |
- return err |
|
180 |
- } |
|
181 |
- |
|
182 |
- im, err := req.builder.getImageMount(flFrom) |
|
183 |
- if err != nil { |
|
184 |
- return errors.Wrapf(err, "invalid from flag value %s", flFrom.Value) |
|
163 |
+func dispatchCopy(d dispatchRequest, c *instructions.CopyCommand) error { |
|
164 |
+ var im *imageMount |
|
165 |
+ var err error |
|
166 |
+ if c.From != "" { |
|
167 |
+ im, err = d.getImageMount(c.From) |
|
168 |
+ if err != nil { |
|
169 |
+ return errors.Wrapf(err, "invalid from flag value %s", c.From) |
|
170 |
+ } |
|
185 | 171 |
} |
186 |
- |
|
187 |
- copier := copierFromDispatchRequest(req, errOnSourceDownload, im) |
|
172 |
+ copier := copierFromDispatchRequest(d, errOnSourceDownload, im) |
|
188 | 173 |
defer copier.Cleanup() |
189 |
- copyInstruction, err := copier.createCopyInstruction(req.args, "COPY") |
|
174 |
+ copyInstruction, err := copier.createCopyInstruction(c.SourcesAndDest, "COPY") |
|
190 | 175 |
if err != nil { |
191 | 176 |
return err |
192 | 177 |
} |
193 |
- copyInstruction.chownStr = flChown.Value |
|
178 |
+ copyInstruction.chownStr = c.Chown |
|
194 | 179 |
|
195 |
- return req.builder.performCopy(req.state, copyInstruction) |
|
180 |
+ return d.builder.performCopy(d.state, copyInstruction) |
|
196 | 181 |
} |
197 | 182 |
|
198 |
-func (b *Builder) getImageMount(fromFlag *Flag) (*imageMount, error) { |
|
199 |
- if !fromFlag.IsUsed() { |
|
183 |
+func (d *dispatchRequest) getImageMount(imageRefOrID string) (*imageMount, error) { |
|
184 |
+ if imageRefOrID == "" { |
|
200 | 185 |
// TODO: this could return the source in the default case as well? |
201 | 186 |
return nil, nil |
202 | 187 |
} |
203 | 188 |
|
204 | 189 |
var localOnly bool |
205 |
- imageRefOrID := fromFlag.Value |
|
206 |
- stage, err := b.buildStages.get(fromFlag.Value) |
|
190 |
+ stage, err := d.stages.get(imageRefOrID) |
|
207 | 191 |
if err != nil { |
208 | 192 |
return nil, err |
209 | 193 |
} |
210 | 194 |
if stage != nil { |
211 |
- imageRefOrID = stage.ImageID() |
|
195 |
+ imageRefOrID = stage.Image |
|
212 | 196 |
localOnly = true |
213 | 197 |
} |
214 |
- return b.imageSources.Get(imageRefOrID, localOnly) |
|
198 |
+ return d.builder.imageSources.Get(imageRefOrID, localOnly) |
|
215 | 199 |
} |
216 | 200 |
|
217 | 201 |
// FROM imagename[:tag | @digest] [AS build-stage-name] |
218 | 202 |
// |
219 |
-func from(req dispatchRequest) error { |
|
220 |
- stageName, err := parseBuildStageName(req.args) |
|
221 |
- if err != nil { |
|
222 |
- return err |
|
223 |
- } |
|
224 |
- |
|
225 |
- if err := req.flags.Parse(); err != nil { |
|
226 |
- return err |
|
227 |
- } |
|
228 |
- |
|
229 |
- req.builder.imageProber.Reset() |
|
230 |
- image, err := req.builder.getFromImage(req.shlex, req.args[0]) |
|
203 |
+func initializeStage(d dispatchRequest, cmd *instructions.Stage) error { |
|
204 |
+ d.builder.imageProber.Reset() |
|
205 |
+ image, err := d.getFromImage(d.shlex, cmd.BaseName) |
|
231 | 206 |
if err != nil { |
232 | 207 |
return err |
233 | 208 |
} |
234 |
- if err := req.builder.buildStages.add(stageName, image); err != nil { |
|
235 |
- return err |
|
236 |
- } |
|
237 |
- req.state.beginStage(stageName, image) |
|
238 |
- req.builder.buildArgs.ResetAllowed() |
|
239 |
- if image.ImageID() == "" { |
|
240 |
- // Typically this means they used "FROM scratch" |
|
241 |
- return nil |
|
209 |
+ state := d.state |
|
210 |
+ state.beginStage(cmd.Name, image) |
|
211 |
+ if len(state.runConfig.OnBuild) > 0 { |
|
212 |
+ triggers := state.runConfig.OnBuild |
|
213 |
+ state.runConfig.OnBuild = nil |
|
214 |
+ return dispatchTriggeredOnBuild(d, triggers) |
|
242 | 215 |
} |
243 |
- |
|
244 |
- return processOnBuild(req) |
|
216 |
+ return nil |
|
245 | 217 |
} |
246 | 218 |
|
247 |
-func parseBuildStageName(args []string) (string, error) { |
|
248 |
- stageName := "" |
|
249 |
- switch { |
|
250 |
- case len(args) == 3 && strings.EqualFold(args[1], "as"): |
|
251 |
- stageName = strings.ToLower(args[2]) |
|
252 |
- if ok, _ := regexp.MatchString("^[a-z][a-z0-9-_\\.]*$", stageName); !ok { |
|
253 |
- return "", errors.Errorf("invalid name for build stage: %q, name can't start with a number or contain symbols", stageName) |
|
219 |
+func dispatchTriggeredOnBuild(d dispatchRequest, triggers []string) error { |
|
220 |
+ fmt.Fprintf(d.builder.Stdout, "# Executing %d build trigger", len(triggers)) |
|
221 |
+ if len(triggers) > 1 { |
|
222 |
+ fmt.Fprint(d.builder.Stdout, "s") |
|
223 |
+ } |
|
224 |
+ fmt.Fprintln(d.builder.Stdout) |
|
225 |
+ for _, trigger := range triggers { |
|
226 |
+ d.state.updateRunConfig() |
|
227 |
+ ast, err := parser.Parse(strings.NewReader(trigger)) |
|
228 |
+ if err != nil { |
|
229 |
+ return err |
|
230 |
+ } |
|
231 |
+ if len(ast.AST.Children) != 1 { |
|
232 |
+ return errors.New("onbuild trigger should be a single expression") |
|
233 |
+ } |
|
234 |
+ cmd, err := instructions.ParseCommand(ast.AST.Children[0]) |
|
235 |
+ if err != nil { |
|
236 |
+ if instructions.IsUnknownInstruction(err) { |
|
237 |
+ buildsFailed.WithValues(metricsUnknownInstructionError).Inc() |
|
238 |
+ } |
|
239 |
+ return err |
|
240 |
+ } |
|
241 |
+ err = dispatch(d, cmd) |
|
242 |
+ if err != nil { |
|
243 |
+ return err |
|
254 | 244 |
} |
255 |
- case len(args) != 1: |
|
256 |
- return "", errors.New("FROM requires either one or three arguments") |
|
257 | 245 |
} |
258 |
- |
|
259 |
- return stageName, nil |
|
246 |
+ return nil |
|
260 | 247 |
} |
261 | 248 |
|
262 |
-// scratchImage is used as a token for the empty base image. |
|
249 |
+// scratchImage is used as a token for the empty base image. It uses buildStage |
|
250 |
+// as a convenient implementation of builder.Image, but is not actually a |
|
251 |
+// buildStage. |
|
263 | 252 |
var scratchImage builder.Image = &image.Image{} |
264 | 253 |
|
265 |
-func (b *Builder) getFromImage(shlex *ShellLex, name string) (builder.Image, error) { |
|
254 |
+func (d *dispatchRequest) getExpandedImageName(shlex *ShellLex, name string) (string, error) { |
|
266 | 255 |
substitutionArgs := []string{} |
267 |
- for key, value := range b.buildArgs.GetAllMeta() { |
|
256 |
+ for key, value := range d.state.buildArgs.GetAllMeta() { |
|
268 | 257 |
substitutionArgs = append(substitutionArgs, key+"="+value) |
269 | 258 |
} |
270 | 259 |
|
271 | 260 |
name, err := shlex.ProcessWord(name, substitutionArgs) |
272 | 261 |
if err != nil { |
273 |
- return nil, err |
|
262 |
+ return "", err |
|
274 | 263 |
} |
275 |
- |
|
264 |
+ return name, nil |
|
265 |
+} |
|
266 |
+func (d *dispatchRequest) getImageOrStage(name string) (builder.Image, error) { |
|
276 | 267 |
var localOnly bool |
277 |
- if stage, ok := b.buildStages.getByName(name); ok { |
|
278 |
- name = stage.ImageID() |
|
268 |
+ if im, ok := d.stages.getByName(name); ok { |
|
269 |
+ name = im.Image |
|
279 | 270 |
localOnly = true |
280 | 271 |
} |
281 | 272 |
|
282 | 273 |
// Windows cannot support a container with no base image unless it is LCOW. |
283 | 274 |
if name == api.NoBaseImageSpecifier { |
284 | 275 |
if runtime.GOOS == "windows" { |
285 |
- if b.platform == "windows" || (b.platform != "windows" && !system.LCOWSupported()) { |
|
276 |
+ if d.builder.platform == "windows" || (d.builder.platform != "windows" && !system.LCOWSupported()) { |
|
286 | 277 |
return nil, errors.New("Windows does not support FROM scratch") |
287 | 278 |
} |
288 | 279 |
} |
289 | 280 |
return scratchImage, nil |
290 | 281 |
} |
291 |
- imageMount, err := b.imageSources.Get(name, localOnly) |
|
282 |
+ imageMount, err := d.builder.imageSources.Get(name, localOnly) |
|
292 | 283 |
if err != nil { |
293 | 284 |
return nil, err |
294 | 285 |
} |
295 | 286 |
return imageMount.Image(), nil |
296 | 287 |
} |
297 |
- |
|
298 |
-func processOnBuild(req dispatchRequest) error { |
|
299 |
- dispatchState := req.state |
|
300 |
- // Process ONBUILD triggers if they exist |
|
301 |
- if nTriggers := len(dispatchState.runConfig.OnBuild); nTriggers != 0 { |
|
302 |
- word := "trigger" |
|
303 |
- if nTriggers > 1 { |
|
304 |
- word = "triggers" |
|
305 |
- } |
|
306 |
- fmt.Fprintf(req.builder.Stderr, "# Executing %d build %s...\n", nTriggers, word) |
|
307 |
- } |
|
308 |
- |
|
309 |
- // Copy the ONBUILD triggers, and remove them from the config, since the config will be committed. |
|
310 |
- onBuildTriggers := dispatchState.runConfig.OnBuild |
|
311 |
- dispatchState.runConfig.OnBuild = []string{} |
|
312 |
- |
|
313 |
- // Reset stdin settings as all build actions run without stdin |
|
314 |
- dispatchState.runConfig.OpenStdin = false |
|
315 |
- dispatchState.runConfig.StdinOnce = false |
|
316 |
- |
|
317 |
- // parse the ONBUILD triggers by invoking the parser |
|
318 |
- for _, step := range onBuildTriggers { |
|
319 |
- dockerfile, err := parser.Parse(strings.NewReader(step)) |
|
320 |
- if err != nil { |
|
321 |
- return err |
|
322 |
- } |
|
323 |
- |
|
324 |
- for _, n := range dockerfile.AST.Children { |
|
325 |
- if err := checkDispatch(n); err != nil { |
|
326 |
- return err |
|
327 |
- } |
|
328 |
- |
|
329 |
- upperCasedCmd := strings.ToUpper(n.Value) |
|
330 |
- switch upperCasedCmd { |
|
331 |
- case "ONBUILD": |
|
332 |
- return errors.New("Chaining ONBUILD via `ONBUILD ONBUILD` isn't allowed") |
|
333 |
- case "MAINTAINER", "FROM": |
|
334 |
- return errors.Errorf("%s isn't allowed as an ONBUILD trigger", upperCasedCmd) |
|
335 |
- } |
|
336 |
- } |
|
337 |
- |
|
338 |
- if _, err := dispatchFromDockerfile(req.builder, dockerfile, dispatchState, req.source); err != nil { |
|
339 |
- return err |
|
340 |
- } |
|
288 |
+func (d *dispatchRequest) getFromImage(shlex *ShellLex, name string) (builder.Image, error) { |
|
289 |
+ name, err := d.getExpandedImageName(shlex, name) |
|
290 |
+ if err != nil { |
|
291 |
+ return nil, err |
|
341 | 292 |
} |
342 |
- return nil |
|
293 |
+ return d.getImageOrStage(name) |
|
343 | 294 |
} |
344 | 295 |
|
345 |
-// ONBUILD RUN echo yo |
|
346 |
-// |
|
347 |
-// ONBUILD triggers run when the image is used in a FROM statement. |
|
348 |
-// |
|
349 |
-// ONBUILD handling has a lot of special-case functionality, the heading in |
|
350 |
-// evaluator.go and comments around dispatch() in the same file explain the |
|
351 |
-// special cases. search for 'OnBuild' in internals.go for additional special |
|
352 |
-// cases. |
|
353 |
-// |
|
354 |
-func onbuild(req dispatchRequest) error { |
|
355 |
- if len(req.args) == 0 { |
|
356 |
- return errAtLeastOneArgument("ONBUILD") |
|
357 |
- } |
|
358 |
- |
|
359 |
- if err := req.flags.Parse(); err != nil { |
|
360 |
- return err |
|
361 |
- } |
|
362 |
- |
|
363 |
- triggerInstruction := strings.ToUpper(strings.TrimSpace(req.args[0])) |
|
364 |
- switch triggerInstruction { |
|
365 |
- case "ONBUILD": |
|
366 |
- return errors.New("Chaining ONBUILD via `ONBUILD ONBUILD` isn't allowed") |
|
367 |
- case "MAINTAINER", "FROM": |
|
368 |
- return fmt.Errorf("%s isn't allowed as an ONBUILD trigger", triggerInstruction) |
|
369 |
- } |
|
296 |
+func dispatchOnbuild(d dispatchRequest, c *instructions.OnbuildCommand) error { |
|
370 | 297 |
|
371 |
- runConfig := req.state.runConfig |
|
372 |
- original := regexp.MustCompile(`(?i)^\s*ONBUILD\s*`).ReplaceAllString(req.original, "") |
|
373 |
- runConfig.OnBuild = append(runConfig.OnBuild, original) |
|
374 |
- return req.builder.commit(req.state, "ONBUILD "+original) |
|
298 |
+ d.state.runConfig.OnBuild = append(d.state.runConfig.OnBuild, c.Expression) |
|
299 |
+ return d.builder.commit(d.state, "ONBUILD "+c.Expression) |
|
375 | 300 |
} |
376 | 301 |
|
377 | 302 |
// WORKDIR /tmp |
378 | 303 |
// |
379 | 304 |
// Set the working directory for future RUN/CMD/etc statements. |
380 | 305 |
// |
381 |
-func workdir(req dispatchRequest) error { |
|
382 |
- if len(req.args) != 1 { |
|
383 |
- return errExactlyOneArgument("WORKDIR") |
|
384 |
- } |
|
385 |
- |
|
386 |
- err := req.flags.Parse() |
|
387 |
- if err != nil { |
|
388 |
- return err |
|
389 |
- } |
|
390 |
- |
|
391 |
- runConfig := req.state.runConfig |
|
392 |
- // This is from the Dockerfile and will not necessarily be in platform |
|
393 |
- // specific semantics, hence ensure it is converted. |
|
394 |
- runConfig.WorkingDir, err = normalizeWorkdir(req.builder.platform, runConfig.WorkingDir, req.args[0]) |
|
306 |
+func dispatchWorkdir(d dispatchRequest, c *instructions.WorkdirCommand) error { |
|
307 |
+ runConfig := d.state.runConfig |
|
308 |
+ var err error |
|
309 |
+ runConfig.WorkingDir, err = normalizeWorkdir(d.builder.platform, runConfig.WorkingDir, c.Path) |
|
395 | 310 |
if err != nil { |
396 | 311 |
return err |
397 | 312 |
} |
... | ... |
@@ -400,23 +263,31 @@ func workdir(req dispatchRequest) error { |
400 | 400 |
// This avoids having an unnecessary expensive mount/unmount calls |
401 | 401 |
// (on Windows in particular) during each container create. |
402 | 402 |
// Prior to 1.13, the mkdir was deferred and not executed at this step. |
403 |
- if req.builder.disableCommit { |
|
403 |
+ if d.builder.disableCommit { |
|
404 | 404 |
// Don't call back into the daemon if we're going through docker commit --change "WORKDIR /foo". |
405 | 405 |
// We've already updated the runConfig and that's enough. |
406 | 406 |
return nil |
407 | 407 |
} |
408 | 408 |
|
409 | 409 |
comment := "WORKDIR " + runConfig.WorkingDir |
410 |
- runConfigWithCommentCmd := copyRunConfig(runConfig, withCmdCommentString(comment, req.builder.platform)) |
|
411 |
- containerID, err := req.builder.probeAndCreate(req.state, runConfigWithCommentCmd) |
|
410 |
+ runConfigWithCommentCmd := copyRunConfig(runConfig, withCmdCommentString(comment, d.builder.platform)) |
|
411 |
+ containerID, err := d.builder.probeAndCreate(d.state, runConfigWithCommentCmd) |
|
412 | 412 |
if err != nil || containerID == "" { |
413 | 413 |
return err |
414 | 414 |
} |
415 |
- if err := req.builder.docker.ContainerCreateWorkdir(containerID); err != nil { |
|
415 |
+ if err := d.builder.docker.ContainerCreateWorkdir(containerID); err != nil { |
|
416 | 416 |
return err |
417 | 417 |
} |
418 | 418 |
|
419 |
- return req.builder.commitContainer(req.state, containerID, runConfigWithCommentCmd) |
|
419 |
+ return d.builder.commitContainer(d.state, containerID, runConfigWithCommentCmd) |
|
420 |
+} |
|
421 |
+ |
|
422 |
+func resolveCmdLine(cmd instructions.ShellDependantCmdLine, runConfig *container.Config, platform string) []string { |
|
423 |
+ result := cmd.CmdLine |
|
424 |
+ if cmd.PrependShell && result != nil { |
|
425 |
+ result = append(getShell(runConfig, platform), result...) |
|
426 |
+ } |
|
427 |
+ return result |
|
420 | 428 |
} |
421 | 429 |
|
422 | 430 |
// RUN some command yo |
... | ... |
@@ -429,32 +300,21 @@ func workdir(req dispatchRequest) error { |
429 | 429 |
// RUN echo hi # cmd /S /C echo hi (Windows) |
430 | 430 |
// RUN [ "echo", "hi" ] # echo hi |
431 | 431 |
// |
432 |
-func run(req dispatchRequest) error { |
|
433 |
- if !req.state.hasFromImage() { |
|
434 |
- return errors.New("Please provide a source image with `from` prior to run") |
|
435 |
- } |
|
432 |
+func dispatchRun(d dispatchRequest, c *instructions.RunCommand) error { |
|
436 | 433 |
|
437 |
- if err := req.flags.Parse(); err != nil { |
|
438 |
- return err |
|
439 |
- } |
|
440 |
- |
|
441 |
- stateRunConfig := req.state.runConfig |
|
442 |
- args := handleJSONArgs(req.args, req.attributes) |
|
443 |
- if !req.attributes["json"] { |
|
444 |
- args = append(getShell(stateRunConfig, req.builder.platform), args...) |
|
445 |
- } |
|
446 |
- cmdFromArgs := strslice.StrSlice(args) |
|
447 |
- buildArgs := req.builder.buildArgs.FilterAllowed(stateRunConfig.Env) |
|
434 |
+ stateRunConfig := d.state.runConfig |
|
435 |
+ cmdFromArgs := resolveCmdLine(c.ShellDependantCmdLine, stateRunConfig, d.builder.platform) |
|
436 |
+ buildArgs := d.state.buildArgs.FilterAllowed(stateRunConfig.Env) |
|
448 | 437 |
|
449 | 438 |
saveCmd := cmdFromArgs |
450 | 439 |
if len(buildArgs) > 0 { |
451 |
- saveCmd = prependEnvOnCmd(req.builder.buildArgs, buildArgs, cmdFromArgs) |
|
440 |
+ saveCmd = prependEnvOnCmd(d.state.buildArgs, buildArgs, cmdFromArgs) |
|
452 | 441 |
} |
453 | 442 |
|
454 | 443 |
runConfigForCacheProbe := copyRunConfig(stateRunConfig, |
455 | 444 |
withCmd(saveCmd), |
456 | 445 |
withEntrypointOverride(saveCmd, nil)) |
457 |
- hit, err := req.builder.probeCache(req.state, runConfigForCacheProbe) |
|
446 |
+ hit, err := d.builder.probeCache(d.state, runConfigForCacheProbe) |
|
458 | 447 |
if err != nil || hit { |
459 | 448 |
return err |
460 | 449 |
} |
... | ... |
@@ -468,11 +328,11 @@ func run(req dispatchRequest) error { |
468 | 468 |
runConfig.ArgsEscaped = true |
469 | 469 |
|
470 | 470 |
logrus.Debugf("[BUILDER] Command to be executed: %v", runConfig.Cmd) |
471 |
- cID, err := req.builder.create(runConfig) |
|
471 |
+ cID, err := d.builder.create(runConfig) |
|
472 | 472 |
if err != nil { |
473 | 473 |
return err |
474 | 474 |
} |
475 |
- if err := req.builder.containerManager.Run(req.builder.clientCtx, cID, req.builder.Stdout, req.builder.Stderr); err != nil { |
|
475 |
+ if err := d.builder.containerManager.Run(d.builder.clientCtx, cID, d.builder.Stdout, d.builder.Stderr); err != nil { |
|
476 | 476 |
if err, ok := err.(*statusCodeError); ok { |
477 | 477 |
// TODO: change error type, because jsonmessage.JSONError assumes HTTP |
478 | 478 |
return &jsonmessage.JSONError{ |
... | ... |
@@ -485,7 +345,7 @@ func run(req dispatchRequest) error { |
485 | 485 |
return err |
486 | 486 |
} |
487 | 487 |
|
488 |
- return req.builder.commitContainer(req.state, cID, runConfigForCacheProbe) |
|
488 |
+ return d.builder.commitContainer(d.state, cID, runConfigForCacheProbe) |
|
489 | 489 |
} |
490 | 490 |
|
491 | 491 |
// Derive the command to use for probeCache() and to commit in this container. |
... | ... |
@@ -518,139 +378,39 @@ func prependEnvOnCmd(buildArgs *buildArgs, buildArgVars []string, cmd strslice.S |
518 | 518 |
// Set the default command to run in the container (which may be empty). |
519 | 519 |
// Argument handling is the same as RUN. |
520 | 520 |
// |
521 |
-func cmd(req dispatchRequest) error { |
|
522 |
- if err := req.flags.Parse(); err != nil { |
|
523 |
- return err |
|
524 |
- } |
|
525 |
- |
|
526 |
- runConfig := req.state.runConfig |
|
527 |
- cmdSlice := handleJSONArgs(req.args, req.attributes) |
|
528 |
- if !req.attributes["json"] { |
|
529 |
- cmdSlice = append(getShell(runConfig, req.builder.platform), cmdSlice...) |
|
530 |
- } |
|
531 |
- |
|
532 |
- runConfig.Cmd = strslice.StrSlice(cmdSlice) |
|
521 |
+func dispatchCmd(d dispatchRequest, c *instructions.CmdCommand) error { |
|
522 |
+ runConfig := d.state.runConfig |
|
523 |
+ cmd := resolveCmdLine(c.ShellDependantCmdLine, runConfig, d.builder.platform) |
|
524 |
+ runConfig.Cmd = cmd |
|
533 | 525 |
// set config as already being escaped, this prevents double escaping on windows |
534 | 526 |
runConfig.ArgsEscaped = true |
535 | 527 |
|
536 |
- if err := req.builder.commit(req.state, fmt.Sprintf("CMD %q", cmdSlice)); err != nil { |
|
528 |
+ if err := d.builder.commit(d.state, fmt.Sprintf("CMD %q", cmd)); err != nil { |
|
537 | 529 |
return err |
538 | 530 |
} |
539 | 531 |
|
540 |
- if len(req.args) != 0 { |
|
541 |
- req.state.cmdSet = true |
|
532 |
+ if len(c.ShellDependantCmdLine.CmdLine) != 0 { |
|
533 |
+ d.state.cmdSet = true |
|
542 | 534 |
} |
543 | 535 |
|
544 | 536 |
return nil |
545 | 537 |
} |
546 | 538 |
|
547 |
-// parseOptInterval(flag) is the duration of flag.Value, or 0 if |
|
548 |
-// empty. An error is reported if the value is given and less than minimum duration. |
|
549 |
-func parseOptInterval(f *Flag) (time.Duration, error) { |
|
550 |
- s := f.Value |
|
551 |
- if s == "" { |
|
552 |
- return 0, nil |
|
553 |
- } |
|
554 |
- d, err := time.ParseDuration(s) |
|
555 |
- if err != nil { |
|
556 |
- return 0, err |
|
557 |
- } |
|
558 |
- if d < container.MinimumDuration { |
|
559 |
- return 0, fmt.Errorf("Interval %#v cannot be less than %s", f.name, container.MinimumDuration) |
|
560 |
- } |
|
561 |
- return d, nil |
|
562 |
-} |
|
563 |
- |
|
564 | 539 |
// HEALTHCHECK foo |
565 | 540 |
// |
566 | 541 |
// Set the default healthcheck command to run in the container (which may be empty). |
567 | 542 |
// Argument handling is the same as RUN. |
568 | 543 |
// |
569 |
-func healthcheck(req dispatchRequest) error { |
|
570 |
- if len(req.args) == 0 { |
|
571 |
- return errAtLeastOneArgument("HEALTHCHECK") |
|
572 |
- } |
|
573 |
- runConfig := req.state.runConfig |
|
574 |
- typ := strings.ToUpper(req.args[0]) |
|
575 |
- args := req.args[1:] |
|
576 |
- if typ == "NONE" { |
|
577 |
- if len(args) != 0 { |
|
578 |
- return errors.New("HEALTHCHECK NONE takes no arguments") |
|
579 |
- } |
|
580 |
- test := strslice.StrSlice{typ} |
|
581 |
- runConfig.Healthcheck = &container.HealthConfig{ |
|
582 |
- Test: test, |
|
583 |
- } |
|
584 |
- } else { |
|
585 |
- if runConfig.Healthcheck != nil { |
|
586 |
- oldCmd := runConfig.Healthcheck.Test |
|
587 |
- if len(oldCmd) > 0 && oldCmd[0] != "NONE" { |
|
588 |
- fmt.Fprintf(req.builder.Stdout, "Note: overriding previous HEALTHCHECK: %v\n", oldCmd) |
|
589 |
- } |
|
590 |
- } |
|
591 |
- |
|
592 |
- healthcheck := container.HealthConfig{} |
|
593 |
- |
|
594 |
- flInterval := req.flags.AddString("interval", "") |
|
595 |
- flTimeout := req.flags.AddString("timeout", "") |
|
596 |
- flStartPeriod := req.flags.AddString("start-period", "") |
|
597 |
- flRetries := req.flags.AddString("retries", "") |
|
598 |
- |
|
599 |
- if err := req.flags.Parse(); err != nil { |
|
600 |
- return err |
|
601 |
- } |
|
602 |
- |
|
603 |
- switch typ { |
|
604 |
- case "CMD": |
|
605 |
- cmdSlice := handleJSONArgs(args, req.attributes) |
|
606 |
- if len(cmdSlice) == 0 { |
|
607 |
- return errors.New("Missing command after HEALTHCHECK CMD") |
|
608 |
- } |
|
609 |
- |
|
610 |
- if !req.attributes["json"] { |
|
611 |
- typ = "CMD-SHELL" |
|
612 |
- } |
|
613 |
- |
|
614 |
- healthcheck.Test = strslice.StrSlice(append([]string{typ}, cmdSlice...)) |
|
615 |
- default: |
|
616 |
- return fmt.Errorf("Unknown type %#v in HEALTHCHECK (try CMD)", typ) |
|
617 |
- } |
|
618 |
- |
|
619 |
- interval, err := parseOptInterval(flInterval) |
|
620 |
- if err != nil { |
|
621 |
- return err |
|
622 |
- } |
|
623 |
- healthcheck.Interval = interval |
|
624 |
- |
|
625 |
- timeout, err := parseOptInterval(flTimeout) |
|
626 |
- if err != nil { |
|
627 |
- return err |
|
628 |
- } |
|
629 |
- healthcheck.Timeout = timeout |
|
630 |
- |
|
631 |
- startPeriod, err := parseOptInterval(flStartPeriod) |
|
632 |
- if err != nil { |
|
633 |
- return err |
|
634 |
- } |
|
635 |
- healthcheck.StartPeriod = startPeriod |
|
636 |
- |
|
637 |
- if flRetries.Value != "" { |
|
638 |
- retries, err := strconv.ParseInt(flRetries.Value, 10, 32) |
|
639 |
- if err != nil { |
|
640 |
- return err |
|
641 |
- } |
|
642 |
- if retries < 1 { |
|
643 |
- return fmt.Errorf("--retries must be at least 1 (not %d)", retries) |
|
644 |
- } |
|
645 |
- healthcheck.Retries = int(retries) |
|
646 |
- } else { |
|
647 |
- healthcheck.Retries = 0 |
|
544 |
+func dispatchHealthcheck(d dispatchRequest, c *instructions.HealthCheckCommand) error { |
|
545 |
+ runConfig := d.state.runConfig |
|
546 |
+ if runConfig.Healthcheck != nil { |
|
547 |
+ oldCmd := runConfig.Healthcheck.Test |
|
548 |
+ if len(oldCmd) > 0 && oldCmd[0] != "NONE" { |
|
549 |
+ fmt.Fprintf(d.builder.Stdout, "Note: overriding previous HEALTHCHECK: %v\n", oldCmd) |
|
648 | 550 |
} |
649 |
- |
|
650 |
- runConfig.Healthcheck = &healthcheck |
|
651 | 551 |
} |
652 |
- |
|
653 |
- return req.builder.commit(req.state, fmt.Sprintf("HEALTHCHECK %q", runConfig.Healthcheck)) |
|
552 |
+ runConfig.Healthcheck = c.Health |
|
553 |
+ return d.builder.commit(d.state, fmt.Sprintf("HEALTHCHECK %q", runConfig.Healthcheck)) |
|
654 | 554 |
} |
655 | 555 |
|
656 | 556 |
// ENTRYPOINT /usr/sbin/nginx |
... | ... |
@@ -661,33 +421,15 @@ func healthcheck(req dispatchRequest) error { |
661 | 661 |
// Handles command processing similar to CMD and RUN, only req.runConfig.Entrypoint |
662 | 662 |
// is initialized at newBuilder time instead of through argument parsing. |
663 | 663 |
// |
664 |
-func entrypoint(req dispatchRequest) error { |
|
665 |
- if err := req.flags.Parse(); err != nil { |
|
666 |
- return err |
|
667 |
- } |
|
668 |
- |
|
669 |
- runConfig := req.state.runConfig |
|
670 |
- parsed := handleJSONArgs(req.args, req.attributes) |
|
671 |
- |
|
672 |
- switch { |
|
673 |
- case req.attributes["json"]: |
|
674 |
- // ENTRYPOINT ["echo", "hi"] |
|
675 |
- runConfig.Entrypoint = strslice.StrSlice(parsed) |
|
676 |
- case len(parsed) == 0: |
|
677 |
- // ENTRYPOINT [] |
|
678 |
- runConfig.Entrypoint = nil |
|
679 |
- default: |
|
680 |
- // ENTRYPOINT echo hi |
|
681 |
- runConfig.Entrypoint = strslice.StrSlice(append(getShell(runConfig, req.builder.platform), parsed[0])) |
|
682 |
- } |
|
683 |
- |
|
684 |
- // when setting the entrypoint if a CMD was not explicitly set then |
|
685 |
- // set the command to nil |
|
686 |
- if !req.state.cmdSet { |
|
664 |
+func dispatchEntrypoint(d dispatchRequest, c *instructions.EntrypointCommand) error { |
|
665 |
+ runConfig := d.state.runConfig |
|
666 |
+ cmd := resolveCmdLine(c.ShellDependantCmdLine, runConfig, d.builder.platform) |
|
667 |
+ runConfig.Entrypoint = cmd |
|
668 |
+ if !d.state.cmdSet { |
|
687 | 669 |
runConfig.Cmd = nil |
688 | 670 |
} |
689 | 671 |
|
690 |
- return req.builder.commit(req.state, fmt.Sprintf("ENTRYPOINT %q", runConfig.Entrypoint)) |
|
672 |
+ return d.builder.commit(d.state, fmt.Sprintf("ENTRYPOINT %q", runConfig.Entrypoint)) |
|
691 | 673 |
} |
692 | 674 |
|
693 | 675 |
// EXPOSE 6667/tcp 7000/tcp |
... | ... |
@@ -695,41 +437,33 @@ func entrypoint(req dispatchRequest) error { |
695 | 695 |
// Expose ports for links and port mappings. This all ends up in |
696 | 696 |
// req.runConfig.ExposedPorts for runconfig. |
697 | 697 |
// |
698 |
-func expose(req dispatchRequest) error { |
|
699 |
- portsTab := req.args |
|
700 |
- |
|
701 |
- if len(req.args) == 0 { |
|
702 |
- return errAtLeastOneArgument("EXPOSE") |
|
698 |
+func dispatchExpose(d dispatchRequest, c *instructions.ExposeCommand, envs []string) error { |
|
699 |
+ // custom multi word expansion |
|
700 |
+ // expose $FOO with FOO="80 443" is expanded as EXPOSE [80,443]. This is the only command supporting word to words expansion |
|
701 |
+ // so the word processing has been de-generalized |
|
702 |
+ ports := []string{} |
|
703 |
+ for _, p := range c.Ports { |
|
704 |
+ ps, err := d.shlex.ProcessWords(p, envs) |
|
705 |
+ if err != nil { |
|
706 |
+ return err |
|
707 |
+ } |
|
708 |
+ ports = append(ports, ps...) |
|
703 | 709 |
} |
710 |
+ c.Ports = ports |
|
704 | 711 |
|
705 |
- if err := req.flags.Parse(); err != nil { |
|
712 |
+ ps, _, err := nat.ParsePortSpecs(ports) |
|
713 |
+ if err != nil { |
|
706 | 714 |
return err |
707 | 715 |
} |
708 | 716 |
|
709 |
- runConfig := req.state.runConfig |
|
710 |
- if runConfig.ExposedPorts == nil { |
|
711 |
- runConfig.ExposedPorts = make(nat.PortSet) |
|
717 |
+ if d.state.runConfig.ExposedPorts == nil { |
|
718 |
+ d.state.runConfig.ExposedPorts = make(nat.PortSet) |
|
712 | 719 |
} |
713 |
- |
|
714 |
- ports, _, err := nat.ParsePortSpecs(portsTab) |
|
715 |
- if err != nil { |
|
716 |
- return err |
|
720 |
+ for p := range ps { |
|
721 |
+ d.state.runConfig.ExposedPorts[p] = struct{}{} |
|
717 | 722 |
} |
718 | 723 |
|
719 |
- // instead of using ports directly, we build a list of ports and sort it so |
|
720 |
- // the order is consistent. This prevents cache burst where map ordering |
|
721 |
- // changes between builds |
|
722 |
- portList := make([]string, len(ports)) |
|
723 |
- var i int |
|
724 |
- for port := range ports { |
|
725 |
- if _, exists := runConfig.ExposedPorts[port]; !exists { |
|
726 |
- runConfig.ExposedPorts[port] = struct{}{} |
|
727 |
- } |
|
728 |
- portList[i] = string(port) |
|
729 |
- i++ |
|
730 |
- } |
|
731 |
- sort.Strings(portList) |
|
732 |
- return req.builder.commit(req.state, "EXPOSE "+strings.Join(portList, " ")) |
|
724 |
+ return d.builder.commit(d.state, "EXPOSE "+strings.Join(c.Ports, " ")) |
|
733 | 725 |
} |
734 | 726 |
|
735 | 727 |
// USER foo |
... | ... |
@@ -737,62 +471,39 @@ func expose(req dispatchRequest) error { |
737 | 737 |
// Set the user to 'foo' for future commands and when running the |
738 | 738 |
// ENTRYPOINT/CMD at container run time. |
739 | 739 |
// |
740 |
-func user(req dispatchRequest) error { |
|
741 |
- if len(req.args) != 1 { |
|
742 |
- return errExactlyOneArgument("USER") |
|
743 |
- } |
|
744 |
- |
|
745 |
- if err := req.flags.Parse(); err != nil { |
|
746 |
- return err |
|
747 |
- } |
|
748 |
- |
|
749 |
- req.state.runConfig.User = req.args[0] |
|
750 |
- return req.builder.commit(req.state, fmt.Sprintf("USER %v", req.args)) |
|
740 |
+func dispatchUser(d dispatchRequest, c *instructions.UserCommand) error { |
|
741 |
+ d.state.runConfig.User = c.User |
|
742 |
+ return d.builder.commit(d.state, fmt.Sprintf("USER %v", c.User)) |
|
751 | 743 |
} |
752 | 744 |
|
753 | 745 |
// VOLUME /foo |
754 | 746 |
// |
755 | 747 |
// Expose the volume /foo for use. Will also accept the JSON array form. |
756 | 748 |
// |
757 |
-func volume(req dispatchRequest) error { |
|
758 |
- if len(req.args) == 0 { |
|
759 |
- return errAtLeastOneArgument("VOLUME") |
|
760 |
- } |
|
761 |
- |
|
762 |
- if err := req.flags.Parse(); err != nil { |
|
763 |
- return err |
|
749 |
+func dispatchVolume(d dispatchRequest, c *instructions.VolumeCommand) error { |
|
750 |
+ if d.state.runConfig.Volumes == nil { |
|
751 |
+ d.state.runConfig.Volumes = map[string]struct{}{} |
|
764 | 752 |
} |
765 |
- |
|
766 |
- runConfig := req.state.runConfig |
|
767 |
- if runConfig.Volumes == nil { |
|
768 |
- runConfig.Volumes = map[string]struct{}{} |
|
769 |
- } |
|
770 |
- for _, v := range req.args { |
|
771 |
- v = strings.TrimSpace(v) |
|
753 |
+ for _, v := range c.Volumes { |
|
772 | 754 |
if v == "" { |
773 | 755 |
return errors.New("VOLUME specified can not be an empty string") |
774 | 756 |
} |
775 |
- runConfig.Volumes[v] = struct{}{} |
|
757 |
+ d.state.runConfig.Volumes[v] = struct{}{} |
|
776 | 758 |
} |
777 |
- return req.builder.commit(req.state, fmt.Sprintf("VOLUME %v", req.args)) |
|
759 |
+ return d.builder.commit(d.state, fmt.Sprintf("VOLUME %v", c.Volumes)) |
|
778 | 760 |
} |
779 | 761 |
|
780 | 762 |
// STOPSIGNAL signal |
781 | 763 |
// |
782 | 764 |
// Set the signal that will be used to kill the container. |
783 |
-func stopSignal(req dispatchRequest) error { |
|
784 |
- if len(req.args) != 1 { |
|
785 |
- return errExactlyOneArgument("STOPSIGNAL") |
|
786 |
- } |
|
765 |
+func dispatchStopSignal(d dispatchRequest, c *instructions.StopSignalCommand) error { |
|
787 | 766 |
|
788 |
- sig := req.args[0] |
|
789 |
- _, err := signal.ParseSignal(sig) |
|
767 |
+ _, err := signal.ParseSignal(c.Signal) |
|
790 | 768 |
if err != nil { |
791 | 769 |
return validationError{err} |
792 | 770 |
} |
793 |
- |
|
794 |
- req.state.runConfig.StopSignal = sig |
|
795 |
- return req.builder.commit(req.state, fmt.Sprintf("STOPSIGNAL %v", req.args)) |
|
771 |
+ d.state.runConfig.StopSignal = c.Signal |
|
772 |
+ return d.builder.commit(d.state, fmt.Sprintf("STOPSIGNAL %v", c.Signal)) |
|
796 | 773 |
} |
797 | 774 |
|
798 | 775 |
// ARG name[=value] |
... | ... |
@@ -800,89 +511,21 @@ func stopSignal(req dispatchRequest) error { |
800 | 800 |
// Adds the variable foo to the trusted list of variables that can be passed |
801 | 801 |
// to builder using the --build-arg flag for expansion/substitution or passing to 'run'. |
802 | 802 |
// Dockerfile author may optionally set a default value of this variable. |
803 |
-func arg(req dispatchRequest) error { |
|
804 |
- if len(req.args) != 1 { |
|
805 |
- return errExactlyOneArgument("ARG") |
|
806 |
- } |
|
807 |
- |
|
808 |
- var ( |
|
809 |
- name string |
|
810 |
- newValue string |
|
811 |
- hasDefault bool |
|
812 |
- ) |
|
813 |
- |
|
814 |
- arg := req.args[0] |
|
815 |
- // 'arg' can just be a name or name-value pair. Note that this is different |
|
816 |
- // from 'env' that handles the split of name and value at the parser level. |
|
817 |
- // The reason for doing it differently for 'arg' is that we support just |
|
818 |
- // defining an arg and not assign it a value (while 'env' always expects a |
|
819 |
- // name-value pair). If possible, it will be good to harmonize the two. |
|
820 |
- if strings.Contains(arg, "=") { |
|
821 |
- parts := strings.SplitN(arg, "=", 2) |
|
822 |
- if len(parts[0]) == 0 { |
|
823 |
- return errBlankCommandNames("ARG") |
|
824 |
- } |
|
825 |
- |
|
826 |
- name = parts[0] |
|
827 |
- newValue = parts[1] |
|
828 |
- hasDefault = true |
|
829 |
- } else { |
|
830 |
- name = arg |
|
831 |
- hasDefault = false |
|
832 |
- } |
|
803 |
+func dispatchArg(d dispatchRequest, c *instructions.ArgCommand) error { |
|
833 | 804 |
|
834 |
- var value *string |
|
835 |
- if hasDefault { |
|
836 |
- value = &newValue |
|
805 |
+ commitStr := "ARG " + c.Key |
|
806 |
+ if c.Value != nil { |
|
807 |
+ commitStr += "=" + *c.Value |
|
837 | 808 |
} |
838 |
- req.builder.buildArgs.AddArg(name, value) |
|
839 | 809 |
|
840 |
- // Arg before FROM doesn't add a layer |
|
841 |
- if !req.state.hasFromImage() { |
|
842 |
- req.builder.buildArgs.AddMetaArg(name, value) |
|
843 |
- return nil |
|
844 |
- } |
|
845 |
- return req.builder.commit(req.state, "ARG "+arg) |
|
810 |
+ d.state.buildArgs.AddArg(c.Key, c.Value) |
|
811 |
+ return d.builder.commit(d.state, commitStr) |
|
846 | 812 |
} |
847 | 813 |
|
848 | 814 |
// SHELL powershell -command |
849 | 815 |
// |
850 | 816 |
// Set the non-default shell to use. |
851 |
-func shell(req dispatchRequest) error { |
|
852 |
- if err := req.flags.Parse(); err != nil { |
|
853 |
- return err |
|
854 |
- } |
|
855 |
- shellSlice := handleJSONArgs(req.args, req.attributes) |
|
856 |
- switch { |
|
857 |
- case len(shellSlice) == 0: |
|
858 |
- // SHELL [] |
|
859 |
- return errAtLeastOneArgument("SHELL") |
|
860 |
- case req.attributes["json"]: |
|
861 |
- // SHELL ["powershell", "-command"] |
|
862 |
- req.state.runConfig.Shell = strslice.StrSlice(shellSlice) |
|
863 |
- default: |
|
864 |
- // SHELL powershell -command - not JSON |
|
865 |
- return errNotJSON("SHELL", req.original) |
|
866 |
- } |
|
867 |
- return req.builder.commit(req.state, fmt.Sprintf("SHELL %v", shellSlice)) |
|
868 |
-} |
|
869 |
- |
|
870 |
-func errAtLeastOneArgument(command string) error { |
|
871 |
- return fmt.Errorf("%s requires at least one argument", command) |
|
872 |
-} |
|
873 |
- |
|
874 |
-func errExactlyOneArgument(command string) error { |
|
875 |
- return fmt.Errorf("%s requires exactly one argument", command) |
|
876 |
-} |
|
877 |
- |
|
878 |
-func errAtLeastTwoArguments(command string) error { |
|
879 |
- return fmt.Errorf("%s requires at least two arguments", command) |
|
880 |
-} |
|
881 |
- |
|
882 |
-func errBlankCommandNames(command string) error { |
|
883 |
- return fmt.Errorf("%s names can not be blank", command) |
|
884 |
-} |
|
885 |
- |
|
886 |
-func errTooManyArguments(command string) error { |
|
887 |
- return fmt.Errorf("Bad input to %s, too many arguments", command) |
|
817 |
+func dispatchShell(d dispatchRequest, c *instructions.ShellCommand) error { |
|
818 |
+ d.state.runConfig.Shell = c.Shell |
|
819 |
+ return d.builder.commit(d.state, fmt.Sprintf("SHELL %v", d.state.runConfig.Shell)) |
|
888 | 820 |
} |
... | ... |
@@ -1,60 +1,29 @@ |
1 | 1 |
package dockerfile |
2 | 2 |
|
3 | 3 |
import ( |
4 |
- "fmt" |
|
5 |
- "runtime" |
|
6 |
- "testing" |
|
7 |
- |
|
8 | 4 |
"bytes" |
9 | 5 |
"context" |
6 |
+ "runtime" |
|
7 |
+ "testing" |
|
10 | 8 |
|
11 | 9 |
"github.com/docker/docker/api/types" |
12 | 10 |
"github.com/docker/docker/api/types/backend" |
13 | 11 |
"github.com/docker/docker/api/types/container" |
14 | 12 |
"github.com/docker/docker/api/types/strslice" |
15 | 13 |
"github.com/docker/docker/builder" |
16 |
- "github.com/docker/docker/builder/dockerfile/parser" |
|
17 |
- "github.com/docker/docker/internal/testutil" |
|
14 |
+ "github.com/docker/docker/builder/dockerfile/instructions" |
|
18 | 15 |
"github.com/docker/docker/pkg/system" |
19 | 16 |
"github.com/docker/go-connections/nat" |
20 | 17 |
"github.com/stretchr/testify/assert" |
21 | 18 |
"github.com/stretchr/testify/require" |
22 | 19 |
) |
23 | 20 |
|
24 |
-type commandWithFunction struct { |
|
25 |
- name string |
|
26 |
- function func(args []string) error |
|
27 |
-} |
|
28 |
- |
|
29 |
-func withArgs(f dispatcher) func([]string) error { |
|
30 |
- return func(args []string) error { |
|
31 |
- return f(dispatchRequest{args: args}) |
|
32 |
- } |
|
33 |
-} |
|
34 |
- |
|
35 |
-func withBuilderAndArgs(builder *Builder, f dispatcher) func([]string) error { |
|
36 |
- return func(args []string) error { |
|
37 |
- return f(defaultDispatchReq(builder, args...)) |
|
38 |
- } |
|
39 |
-} |
|
40 |
- |
|
41 |
-func defaultDispatchReq(builder *Builder, args ...string) dispatchRequest { |
|
42 |
- return dispatchRequest{ |
|
43 |
- builder: builder, |
|
44 |
- args: args, |
|
45 |
- flags: NewBFlags(), |
|
46 |
- shlex: NewShellLex(parser.DefaultEscapeToken), |
|
47 |
- state: &dispatchState{runConfig: &container.Config{}}, |
|
48 |
- } |
|
49 |
-} |
|
50 |
- |
|
51 | 21 |
func newBuilderWithMockBackend() *Builder { |
52 | 22 |
mockBackend := &MockBackend{} |
53 | 23 |
ctx := context.Background() |
54 | 24 |
b := &Builder{ |
55 | 25 |
options: &types.ImageBuildOptions{}, |
56 | 26 |
docker: mockBackend, |
57 |
- buildArgs: newBuildArgs(make(map[string]*string)), |
|
58 | 27 |
Stdout: new(bytes.Buffer), |
59 | 28 |
clientCtx: ctx, |
60 | 29 |
disableCommit: true, |
... | ... |
@@ -62,137 +31,84 @@ func newBuilderWithMockBackend() *Builder { |
62 | 62 |
Options: &types.ImageBuildOptions{}, |
63 | 63 |
Backend: mockBackend, |
64 | 64 |
}), |
65 |
- buildStages: newBuildStages(), |
|
66 | 65 |
imageProber: newImageProber(mockBackend, nil, runtime.GOOS, false), |
67 | 66 |
containerManager: newContainerManager(mockBackend), |
68 | 67 |
} |
69 | 68 |
return b |
70 | 69 |
} |
71 | 70 |
|
72 |
-func TestCommandsExactlyOneArgument(t *testing.T) { |
|
73 |
- commands := []commandWithFunction{ |
|
74 |
- {"MAINTAINER", withArgs(maintainer)}, |
|
75 |
- {"WORKDIR", withArgs(workdir)}, |
|
76 |
- {"USER", withArgs(user)}, |
|
77 |
- {"STOPSIGNAL", withArgs(stopSignal)}, |
|
78 |
- } |
|
79 |
- |
|
80 |
- for _, command := range commands { |
|
81 |
- err := command.function([]string{}) |
|
82 |
- assert.EqualError(t, err, errExactlyOneArgument(command.name).Error()) |
|
83 |
- } |
|
84 |
-} |
|
85 |
- |
|
86 |
-func TestCommandsAtLeastOneArgument(t *testing.T) { |
|
87 |
- commands := []commandWithFunction{ |
|
88 |
- {"ENV", withArgs(env)}, |
|
89 |
- {"LABEL", withArgs(label)}, |
|
90 |
- {"ONBUILD", withArgs(onbuild)}, |
|
91 |
- {"HEALTHCHECK", withArgs(healthcheck)}, |
|
92 |
- {"EXPOSE", withArgs(expose)}, |
|
93 |
- {"VOLUME", withArgs(volume)}, |
|
94 |
- } |
|
95 |
- |
|
96 |
- for _, command := range commands { |
|
97 |
- err := command.function([]string{}) |
|
98 |
- assert.EqualError(t, err, errAtLeastOneArgument(command.name).Error()) |
|
99 |
- } |
|
100 |
-} |
|
101 |
- |
|
102 |
-func TestCommandsAtLeastTwoArguments(t *testing.T) { |
|
103 |
- commands := []commandWithFunction{ |
|
104 |
- {"ADD", withArgs(add)}, |
|
105 |
- {"COPY", withArgs(dispatchCopy)}} |
|
106 |
- |
|
107 |
- for _, command := range commands { |
|
108 |
- err := command.function([]string{"arg1"}) |
|
109 |
- assert.EqualError(t, err, errAtLeastTwoArguments(command.name).Error()) |
|
110 |
- } |
|
111 |
-} |
|
112 |
- |
|
113 |
-func TestCommandsTooManyArguments(t *testing.T) { |
|
114 |
- commands := []commandWithFunction{ |
|
115 |
- {"ENV", withArgs(env)}, |
|
116 |
- {"LABEL", withArgs(label)}} |
|
117 |
- |
|
118 |
- for _, command := range commands { |
|
119 |
- err := command.function([]string{"arg1", "arg2", "arg3"}) |
|
120 |
- assert.EqualError(t, err, errTooManyArguments(command.name).Error()) |
|
121 |
- } |
|
122 |
-} |
|
123 |
- |
|
124 |
-func TestCommandsBlankNames(t *testing.T) { |
|
125 |
- builder := newBuilderWithMockBackend() |
|
126 |
- commands := []commandWithFunction{ |
|
127 |
- {"ENV", withBuilderAndArgs(builder, env)}, |
|
128 |
- {"LABEL", withBuilderAndArgs(builder, label)}, |
|
129 |
- } |
|
130 |
- |
|
131 |
- for _, command := range commands { |
|
132 |
- err := command.function([]string{"", ""}) |
|
133 |
- assert.EqualError(t, err, errBlankCommandNames(command.name).Error()) |
|
134 |
- } |
|
135 |
-} |
|
136 |
- |
|
137 | 71 |
func TestEnv2Variables(t *testing.T) { |
138 | 72 |
b := newBuilderWithMockBackend() |
139 |
- |
|
140 |
- args := []string{"var1", "val1", "var2", "val2"} |
|
141 |
- req := defaultDispatchReq(b, args...) |
|
142 |
- err := env(req) |
|
73 |
+ sb := newDispatchRequest(b, '\\', nil, newBuildArgs(make(map[string]*string)), newStagesBuildResults()) |
|
74 |
+ envCommand := &instructions.EnvCommand{ |
|
75 |
+ Env: instructions.KeyValuePairs{ |
|
76 |
+ instructions.KeyValuePair{Key: "var1", Value: "val1"}, |
|
77 |
+ instructions.KeyValuePair{Key: "var2", Value: "val2"}, |
|
78 |
+ }, |
|
79 |
+ } |
|
80 |
+ err := dispatch(sb, envCommand) |
|
143 | 81 |
require.NoError(t, err) |
144 | 82 |
|
145 | 83 |
expected := []string{ |
146 |
- fmt.Sprintf("%s=%s", args[0], args[1]), |
|
147 |
- fmt.Sprintf("%s=%s", args[2], args[3]), |
|
84 |
+ "var1=val1", |
|
85 |
+ "var2=val2", |
|
148 | 86 |
} |
149 |
- assert.Equal(t, expected, req.state.runConfig.Env) |
|
87 |
+ assert.Equal(t, expected, sb.state.runConfig.Env) |
|
150 | 88 |
} |
151 | 89 |
|
152 | 90 |
func TestEnvValueWithExistingRunConfigEnv(t *testing.T) { |
153 | 91 |
b := newBuilderWithMockBackend() |
154 |
- |
|
155 |
- args := []string{"var1", "val1"} |
|
156 |
- req := defaultDispatchReq(b, args...) |
|
157 |
- req.state.runConfig.Env = []string{"var1=old", "var2=fromenv"} |
|
158 |
- err := env(req) |
|
92 |
+ sb := newDispatchRequest(b, '\\', nil, newBuildArgs(make(map[string]*string)), newStagesBuildResults()) |
|
93 |
+ sb.state.runConfig.Env = []string{"var1=old", "var2=fromenv"} |
|
94 |
+ envCommand := &instructions.EnvCommand{ |
|
95 |
+ Env: instructions.KeyValuePairs{ |
|
96 |
+ instructions.KeyValuePair{Key: "var1", Value: "val1"}, |
|
97 |
+ }, |
|
98 |
+ } |
|
99 |
+ err := dispatch(sb, envCommand) |
|
159 | 100 |
require.NoError(t, err) |
160 |
- |
|
161 | 101 |
expected := []string{ |
162 |
- fmt.Sprintf("%s=%s", args[0], args[1]), |
|
102 |
+ "var1=val1", |
|
163 | 103 |
"var2=fromenv", |
164 | 104 |
} |
165 |
- assert.Equal(t, expected, req.state.runConfig.Env) |
|
105 |
+ assert.Equal(t, expected, sb.state.runConfig.Env) |
|
166 | 106 |
} |
167 | 107 |
|
168 | 108 |
func TestMaintainer(t *testing.T) { |
169 | 109 |
maintainerEntry := "Some Maintainer <maintainer@example.com>" |
170 |
- |
|
171 | 110 |
b := newBuilderWithMockBackend() |
172 |
- req := defaultDispatchReq(b, maintainerEntry) |
|
173 |
- err := maintainer(req) |
|
111 |
+ sb := newDispatchRequest(b, '\\', nil, newBuildArgs(make(map[string]*string)), newStagesBuildResults()) |
|
112 |
+ cmd := &instructions.MaintainerCommand{Maintainer: maintainerEntry} |
|
113 |
+ err := dispatch(sb, cmd) |
|
174 | 114 |
require.NoError(t, err) |
175 |
- assert.Equal(t, maintainerEntry, req.state.maintainer) |
|
115 |
+ assert.Equal(t, maintainerEntry, sb.state.maintainer) |
|
176 | 116 |
} |
177 | 117 |
|
178 | 118 |
func TestLabel(t *testing.T) { |
179 | 119 |
labelName := "label" |
180 | 120 |
labelValue := "value" |
181 | 121 |
|
182 |
- labelEntry := []string{labelName, labelValue} |
|
183 | 122 |
b := newBuilderWithMockBackend() |
184 |
- req := defaultDispatchReq(b, labelEntry...) |
|
185 |
- err := label(req) |
|
123 |
+ sb := newDispatchRequest(b, '\\', nil, newBuildArgs(make(map[string]*string)), newStagesBuildResults()) |
|
124 |
+ cmd := &instructions.LabelCommand{ |
|
125 |
+ Labels: instructions.KeyValuePairs{ |
|
126 |
+ instructions.KeyValuePair{Key: labelName, Value: labelValue}, |
|
127 |
+ }, |
|
128 |
+ } |
|
129 |
+ err := dispatch(sb, cmd) |
|
186 | 130 |
require.NoError(t, err) |
187 | 131 |
|
188 |
- require.Contains(t, req.state.runConfig.Labels, labelName) |
|
189 |
- assert.Equal(t, req.state.runConfig.Labels[labelName], labelValue) |
|
132 |
+ require.Contains(t, sb.state.runConfig.Labels, labelName) |
|
133 |
+ assert.Equal(t, sb.state.runConfig.Labels[labelName], labelValue) |
|
190 | 134 |
} |
191 | 135 |
|
192 | 136 |
func TestFromScratch(t *testing.T) { |
193 | 137 |
b := newBuilderWithMockBackend() |
194 |
- req := defaultDispatchReq(b, "scratch") |
|
195 |
- err := from(req) |
|
138 |
+ sb := newDispatchRequest(b, '\\', nil, newBuildArgs(make(map[string]*string)), newStagesBuildResults()) |
|
139 |
+ cmd := &instructions.Stage{ |
|
140 |
+ BaseName: "scratch", |
|
141 |
+ } |
|
142 |
+ err := initializeStage(sb, cmd) |
|
196 | 143 |
|
197 | 144 |
if runtime.GOOS == "windows" && !system.LCOWSupported() { |
198 | 145 |
assert.EqualError(t, err, "Windows does not support FROM scratch") |
... | ... |
@@ -200,14 +116,14 @@ func TestFromScratch(t *testing.T) { |
200 | 200 |
} |
201 | 201 |
|
202 | 202 |
require.NoError(t, err) |
203 |
- assert.True(t, req.state.hasFromImage()) |
|
204 |
- assert.Equal(t, "", req.state.imageID) |
|
203 |
+ assert.True(t, sb.state.hasFromImage()) |
|
204 |
+ assert.Equal(t, "", sb.state.imageID) |
|
205 | 205 |
// Windows does not set the default path. TODO @jhowardmsft LCOW support. This will need revisiting as we get further into the implementation |
206 | 206 |
expected := "PATH=" + system.DefaultPathEnv(runtime.GOOS) |
207 | 207 |
if runtime.GOOS == "windows" { |
208 | 208 |
expected = "" |
209 | 209 |
} |
210 |
- assert.Equal(t, []string{expected}, req.state.runConfig.Env) |
|
210 |
+ assert.Equal(t, []string{expected}, sb.state.runConfig.Env) |
|
211 | 211 |
} |
212 | 212 |
|
213 | 213 |
func TestFromWithArg(t *testing.T) { |
... | ... |
@@ -219,16 +135,27 @@ func TestFromWithArg(t *testing.T) { |
219 | 219 |
} |
220 | 220 |
b := newBuilderWithMockBackend() |
221 | 221 |
b.docker.(*MockBackend).getImageFunc = getImage |
222 |
+ args := newBuildArgs(make(map[string]*string)) |
|
222 | 223 |
|
223 |
- require.NoError(t, arg(defaultDispatchReq(b, "THETAG="+tag))) |
|
224 |
- req := defaultDispatchReq(b, "alpine${THETAG}") |
|
225 |
- err := from(req) |
|
224 |
+ val := "sometag" |
|
225 |
+ metaArg := instructions.ArgCommand{ |
|
226 |
+ Key: "THETAG", |
|
227 |
+ Value: &val, |
|
228 |
+ } |
|
229 |
+ cmd := &instructions.Stage{ |
|
230 |
+ BaseName: "alpine:${THETAG}", |
|
231 |
+ } |
|
232 |
+ err := processMetaArg(metaArg, NewShellLex('\\'), args) |
|
226 | 233 |
|
234 |
+ sb := newDispatchRequest(b, '\\', nil, args, newStagesBuildResults()) |
|
227 | 235 |
require.NoError(t, err) |
228 |
- assert.Equal(t, expected, req.state.imageID) |
|
229 |
- assert.Equal(t, expected, req.state.baseImage.ImageID()) |
|
230 |
- assert.Len(t, b.buildArgs.GetAllAllowed(), 0) |
|
231 |
- assert.Len(t, b.buildArgs.GetAllMeta(), 1) |
|
236 |
+ err = initializeStage(sb, cmd) |
|
237 |
+ require.NoError(t, err) |
|
238 |
+ |
|
239 |
+ assert.Equal(t, expected, sb.state.imageID) |
|
240 |
+ assert.Equal(t, expected, sb.state.baseImage.ImageID()) |
|
241 |
+ assert.Len(t, sb.state.buildArgs.GetAllAllowed(), 0) |
|
242 |
+ assert.Len(t, sb.state.buildArgs.GetAllMeta(), 1) |
|
232 | 243 |
} |
233 | 244 |
|
234 | 245 |
func TestFromWithUndefinedArg(t *testing.T) { |
... | ... |
@@ -240,74 +167,74 @@ func TestFromWithUndefinedArg(t *testing.T) { |
240 | 240 |
} |
241 | 241 |
b := newBuilderWithMockBackend() |
242 | 242 |
b.docker.(*MockBackend).getImageFunc = getImage |
243 |
+ sb := newDispatchRequest(b, '\\', nil, newBuildArgs(make(map[string]*string)), newStagesBuildResults()) |
|
244 |
+ |
|
243 | 245 |
b.options.BuildArgs = map[string]*string{"THETAG": &tag} |
244 | 246 |
|
245 |
- req := defaultDispatchReq(b, "alpine${THETAG}") |
|
246 |
- err := from(req) |
|
247 |
+ cmd := &instructions.Stage{ |
|
248 |
+ BaseName: "alpine${THETAG}", |
|
249 |
+ } |
|
250 |
+ err := initializeStage(sb, cmd) |
|
247 | 251 |
require.NoError(t, err) |
248 |
- assert.Equal(t, expected, req.state.imageID) |
|
252 |
+ assert.Equal(t, expected, sb.state.imageID) |
|
249 | 253 |
} |
250 | 254 |
|
251 |
-func TestFromMultiStageWithScratchNamedStage(t *testing.T) { |
|
252 |
- if runtime.GOOS == "windows" { |
|
253 |
- t.Skip("Windows does not support scratch") |
|
254 |
- } |
|
255 |
+func TestFromMultiStageWithNamedStage(t *testing.T) { |
|
255 | 256 |
b := newBuilderWithMockBackend() |
256 |
- req := defaultDispatchReq(b, "scratch", "AS", "base") |
|
257 |
- |
|
258 |
- require.NoError(t, from(req)) |
|
259 |
- assert.True(t, req.state.hasFromImage()) |
|
260 |
- |
|
261 |
- req.args = []string{"base"} |
|
262 |
- require.NoError(t, from(req)) |
|
263 |
- assert.True(t, req.state.hasFromImage()) |
|
264 |
-} |
|
265 |
- |
|
266 |
-func TestOnbuildIllegalTriggers(t *testing.T) { |
|
267 |
- triggers := []struct{ command, expectedError string }{ |
|
268 |
- {"ONBUILD", "Chaining ONBUILD via `ONBUILD ONBUILD` isn't allowed"}, |
|
269 |
- {"MAINTAINER", "MAINTAINER isn't allowed as an ONBUILD trigger"}, |
|
270 |
- {"FROM", "FROM isn't allowed as an ONBUILD trigger"}} |
|
271 |
- |
|
272 |
- for _, trigger := range triggers { |
|
273 |
- b := newBuilderWithMockBackend() |
|
274 |
- |
|
275 |
- err := onbuild(defaultDispatchReq(b, trigger.command)) |
|
276 |
- testutil.ErrorContains(t, err, trigger.expectedError) |
|
277 |
- } |
|
257 |
+ firstFrom := &instructions.Stage{BaseName: "someimg", Name: "base"} |
|
258 |
+ secondFrom := &instructions.Stage{BaseName: "base"} |
|
259 |
+ previousResults := newStagesBuildResults() |
|
260 |
+ firstSB := newDispatchRequest(b, '\\', nil, newBuildArgs(make(map[string]*string)), previousResults) |
|
261 |
+ secondSB := newDispatchRequest(b, '\\', nil, newBuildArgs(make(map[string]*string)), previousResults) |
|
262 |
+ err := initializeStage(firstSB, firstFrom) |
|
263 |
+ require.NoError(t, err) |
|
264 |
+ assert.True(t, firstSB.state.hasFromImage()) |
|
265 |
+ previousResults.indexed["base"] = firstSB.state.runConfig |
|
266 |
+ previousResults.flat = append(previousResults.flat, firstSB.state.runConfig) |
|
267 |
+ err = initializeStage(secondSB, secondFrom) |
|
268 |
+ require.NoError(t, err) |
|
269 |
+ assert.True(t, secondSB.state.hasFromImage()) |
|
278 | 270 |
} |
279 | 271 |
|
280 | 272 |
func TestOnbuild(t *testing.T) { |
281 | 273 |
b := newBuilderWithMockBackend() |
282 |
- |
|
283 |
- req := defaultDispatchReq(b, "ADD", ".", "/app/src") |
|
284 |
- req.original = "ONBUILD ADD . /app/src" |
|
285 |
- req.state.runConfig = &container.Config{} |
|
286 |
- |
|
287 |
- err := onbuild(req) |
|
274 |
+ sb := newDispatchRequest(b, '\\', nil, newBuildArgs(make(map[string]*string)), newStagesBuildResults()) |
|
275 |
+ cmd := &instructions.OnbuildCommand{ |
|
276 |
+ Expression: "ADD . /app/src", |
|
277 |
+ } |
|
278 |
+ err := dispatch(sb, cmd) |
|
288 | 279 |
require.NoError(t, err) |
289 |
- assert.Equal(t, "ADD . /app/src", req.state.runConfig.OnBuild[0]) |
|
280 |
+ assert.Equal(t, "ADD . /app/src", sb.state.runConfig.OnBuild[0]) |
|
290 | 281 |
} |
291 | 282 |
|
292 | 283 |
func TestWorkdir(t *testing.T) { |
293 | 284 |
b := newBuilderWithMockBackend() |
285 |
+ sb := newDispatchRequest(b, '`', nil, newBuildArgs(make(map[string]*string)), newStagesBuildResults()) |
|
294 | 286 |
workingDir := "/app" |
295 | 287 |
if runtime.GOOS == "windows" { |
296 |
- workingDir = "C:\app" |
|
288 |
+ workingDir = "C:\\app" |
|
289 |
+ } |
|
290 |
+ cmd := &instructions.WorkdirCommand{ |
|
291 |
+ Path: workingDir, |
|
297 | 292 |
} |
298 | 293 |
|
299 |
- req := defaultDispatchReq(b, workingDir) |
|
300 |
- err := workdir(req) |
|
294 |
+ err := dispatch(sb, cmd) |
|
301 | 295 |
require.NoError(t, err) |
302 |
- assert.Equal(t, workingDir, req.state.runConfig.WorkingDir) |
|
296 |
+ assert.Equal(t, workingDir, sb.state.runConfig.WorkingDir) |
|
303 | 297 |
} |
304 | 298 |
|
305 | 299 |
func TestCmd(t *testing.T) { |
306 | 300 |
b := newBuilderWithMockBackend() |
301 |
+ sb := newDispatchRequest(b, '`', nil, newBuildArgs(make(map[string]*string)), newStagesBuildResults()) |
|
307 | 302 |
command := "./executable" |
308 | 303 |
|
309 |
- req := defaultDispatchReq(b, command) |
|
310 |
- err := cmd(req) |
|
304 |
+ cmd := &instructions.CmdCommand{ |
|
305 |
+ ShellDependantCmdLine: instructions.ShellDependantCmdLine{ |
|
306 |
+ CmdLine: strslice.StrSlice{command}, |
|
307 |
+ PrependShell: true, |
|
308 |
+ }, |
|
309 |
+ } |
|
310 |
+ err := dispatch(sb, cmd) |
|
311 | 311 |
require.NoError(t, err) |
312 | 312 |
|
313 | 313 |
var expectedCommand strslice.StrSlice |
... | ... |
@@ -317,42 +244,56 @@ func TestCmd(t *testing.T) { |
317 | 317 |
expectedCommand = strslice.StrSlice(append([]string{"/bin/sh"}, "-c", command)) |
318 | 318 |
} |
319 | 319 |
|
320 |
- assert.Equal(t, expectedCommand, req.state.runConfig.Cmd) |
|
321 |
- assert.True(t, req.state.cmdSet) |
|
320 |
+ assert.Equal(t, expectedCommand, sb.state.runConfig.Cmd) |
|
321 |
+ assert.True(t, sb.state.cmdSet) |
|
322 | 322 |
} |
323 | 323 |
|
324 | 324 |
func TestHealthcheckNone(t *testing.T) { |
325 | 325 |
b := newBuilderWithMockBackend() |
326 |
- |
|
327 |
- req := defaultDispatchReq(b, "NONE") |
|
328 |
- err := healthcheck(req) |
|
326 |
+ sb := newDispatchRequest(b, '`', nil, newBuildArgs(make(map[string]*string)), newStagesBuildResults()) |
|
327 |
+ cmd := &instructions.HealthCheckCommand{ |
|
328 |
+ Health: &container.HealthConfig{ |
|
329 |
+ Test: []string{"NONE"}, |
|
330 |
+ }, |
|
331 |
+ } |
|
332 |
+ err := dispatch(sb, cmd) |
|
329 | 333 |
require.NoError(t, err) |
330 | 334 |
|
331 |
- require.NotNil(t, req.state.runConfig.Healthcheck) |
|
332 |
- assert.Equal(t, []string{"NONE"}, req.state.runConfig.Healthcheck.Test) |
|
335 |
+ require.NotNil(t, sb.state.runConfig.Healthcheck) |
|
336 |
+ assert.Equal(t, []string{"NONE"}, sb.state.runConfig.Healthcheck.Test) |
|
333 | 337 |
} |
334 | 338 |
|
335 | 339 |
func TestHealthcheckCmd(t *testing.T) { |
336 |
- b := newBuilderWithMockBackend() |
|
337 | 340 |
|
338 |
- args := []string{"CMD", "curl", "-f", "http://localhost/", "||", "exit", "1"} |
|
339 |
- req := defaultDispatchReq(b, args...) |
|
340 |
- err := healthcheck(req) |
|
341 |
+ b := newBuilderWithMockBackend() |
|
342 |
+ sb := newDispatchRequest(b, '`', nil, newBuildArgs(make(map[string]*string)), newStagesBuildResults()) |
|
343 |
+ expectedTest := []string{"CMD-SHELL", "curl -f http://localhost/ || exit 1"} |
|
344 |
+ cmd := &instructions.HealthCheckCommand{ |
|
345 |
+ Health: &container.HealthConfig{ |
|
346 |
+ Test: expectedTest, |
|
347 |
+ }, |
|
348 |
+ } |
|
349 |
+ err := dispatch(sb, cmd) |
|
341 | 350 |
require.NoError(t, err) |
342 | 351 |
|
343 |
- require.NotNil(t, req.state.runConfig.Healthcheck) |
|
344 |
- expectedTest := []string{"CMD-SHELL", "curl -f http://localhost/ || exit 1"} |
|
345 |
- assert.Equal(t, expectedTest, req.state.runConfig.Healthcheck.Test) |
|
352 |
+ require.NotNil(t, sb.state.runConfig.Healthcheck) |
|
353 |
+ assert.Equal(t, expectedTest, sb.state.runConfig.Healthcheck.Test) |
|
346 | 354 |
} |
347 | 355 |
|
348 | 356 |
func TestEntrypoint(t *testing.T) { |
349 | 357 |
b := newBuilderWithMockBackend() |
358 |
+ sb := newDispatchRequest(b, '`', nil, newBuildArgs(make(map[string]*string)), newStagesBuildResults()) |
|
350 | 359 |
entrypointCmd := "/usr/sbin/nginx" |
351 | 360 |
|
352 |
- req := defaultDispatchReq(b, entrypointCmd) |
|
353 |
- err := entrypoint(req) |
|
361 |
+ cmd := &instructions.EntrypointCommand{ |
|
362 |
+ ShellDependantCmdLine: instructions.ShellDependantCmdLine{ |
|
363 |
+ CmdLine: strslice.StrSlice{entrypointCmd}, |
|
364 |
+ PrependShell: true, |
|
365 |
+ }, |
|
366 |
+ } |
|
367 |
+ err := dispatch(sb, cmd) |
|
354 | 368 |
require.NoError(t, err) |
355 |
- require.NotNil(t, req.state.runConfig.Entrypoint) |
|
369 |
+ require.NotNil(t, sb.state.runConfig.Entrypoint) |
|
356 | 370 |
|
357 | 371 |
var expectedEntrypoint strslice.StrSlice |
358 | 372 |
if runtime.GOOS == "windows" { |
... | ... |
@@ -360,99 +301,99 @@ func TestEntrypoint(t *testing.T) { |
360 | 360 |
} else { |
361 | 361 |
expectedEntrypoint = strslice.StrSlice(append([]string{"/bin/sh"}, "-c", entrypointCmd)) |
362 | 362 |
} |
363 |
- assert.Equal(t, expectedEntrypoint, req.state.runConfig.Entrypoint) |
|
363 |
+ assert.Equal(t, expectedEntrypoint, sb.state.runConfig.Entrypoint) |
|
364 | 364 |
} |
365 | 365 |
|
366 | 366 |
func TestExpose(t *testing.T) { |
367 | 367 |
b := newBuilderWithMockBackend() |
368 |
+ sb := newDispatchRequest(b, '`', nil, newBuildArgs(make(map[string]*string)), newStagesBuildResults()) |
|
368 | 369 |
|
369 | 370 |
exposedPort := "80" |
370 |
- req := defaultDispatchReq(b, exposedPort) |
|
371 |
- err := expose(req) |
|
371 |
+ cmd := &instructions.ExposeCommand{ |
|
372 |
+ Ports: []string{exposedPort}, |
|
373 |
+ } |
|
374 |
+ err := dispatch(sb, cmd) |
|
372 | 375 |
require.NoError(t, err) |
373 | 376 |
|
374 |
- require.NotNil(t, req.state.runConfig.ExposedPorts) |
|
375 |
- require.Len(t, req.state.runConfig.ExposedPorts, 1) |
|
377 |
+ require.NotNil(t, sb.state.runConfig.ExposedPorts) |
|
378 |
+ require.Len(t, sb.state.runConfig.ExposedPorts, 1) |
|
376 | 379 |
|
377 | 380 |
portsMapping, err := nat.ParsePortSpec(exposedPort) |
378 | 381 |
require.NoError(t, err) |
379 |
- assert.Contains(t, req.state.runConfig.ExposedPorts, portsMapping[0].Port) |
|
382 |
+ assert.Contains(t, sb.state.runConfig.ExposedPorts, portsMapping[0].Port) |
|
380 | 383 |
} |
381 | 384 |
|
382 | 385 |
func TestUser(t *testing.T) { |
383 | 386 |
b := newBuilderWithMockBackend() |
384 |
- userCommand := "foo" |
|
387 |
+ sb := newDispatchRequest(b, '`', nil, newBuildArgs(make(map[string]*string)), newStagesBuildResults()) |
|
385 | 388 |
|
386 |
- req := defaultDispatchReq(b, userCommand) |
|
387 |
- err := user(req) |
|
389 |
+ cmd := &instructions.UserCommand{ |
|
390 |
+ User: "test", |
|
391 |
+ } |
|
392 |
+ err := dispatch(sb, cmd) |
|
388 | 393 |
require.NoError(t, err) |
389 |
- assert.Equal(t, userCommand, req.state.runConfig.User) |
|
394 |
+ assert.Equal(t, "test", sb.state.runConfig.User) |
|
390 | 395 |
} |
391 | 396 |
|
392 | 397 |
func TestVolume(t *testing.T) { |
393 | 398 |
b := newBuilderWithMockBackend() |
399 |
+ sb := newDispatchRequest(b, '`', nil, newBuildArgs(make(map[string]*string)), newStagesBuildResults()) |
|
394 | 400 |
|
395 | 401 |
exposedVolume := "/foo" |
396 | 402 |
|
397 |
- req := defaultDispatchReq(b, exposedVolume) |
|
398 |
- err := volume(req) |
|
403 |
+ cmd := &instructions.VolumeCommand{ |
|
404 |
+ Volumes: []string{exposedVolume}, |
|
405 |
+ } |
|
406 |
+ err := dispatch(sb, cmd) |
|
399 | 407 |
require.NoError(t, err) |
400 |
- |
|
401 |
- require.NotNil(t, req.state.runConfig.Volumes) |
|
402 |
- assert.Len(t, req.state.runConfig.Volumes, 1) |
|
403 |
- assert.Contains(t, req.state.runConfig.Volumes, exposedVolume) |
|
408 |
+ require.NotNil(t, sb.state.runConfig.Volumes) |
|
409 |
+ assert.Len(t, sb.state.runConfig.Volumes, 1) |
|
410 |
+ assert.Contains(t, sb.state.runConfig.Volumes, exposedVolume) |
|
404 | 411 |
} |
405 | 412 |
|
406 | 413 |
func TestStopSignal(t *testing.T) { |
414 |
+ if runtime.GOOS == "windows" { |
|
415 |
+ t.Skip("Windows does not support stopsignal") |
|
416 |
+ return |
|
417 |
+ } |
|
407 | 418 |
b := newBuilderWithMockBackend() |
419 |
+ sb := newDispatchRequest(b, '`', nil, newBuildArgs(make(map[string]*string)), newStagesBuildResults()) |
|
408 | 420 |
signal := "SIGKILL" |
409 | 421 |
|
410 |
- req := defaultDispatchReq(b, signal) |
|
411 |
- err := stopSignal(req) |
|
422 |
+ cmd := &instructions.StopSignalCommand{ |
|
423 |
+ Signal: signal, |
|
424 |
+ } |
|
425 |
+ err := dispatch(sb, cmd) |
|
412 | 426 |
require.NoError(t, err) |
413 |
- assert.Equal(t, signal, req.state.runConfig.StopSignal) |
|
427 |
+ assert.Equal(t, signal, sb.state.runConfig.StopSignal) |
|
414 | 428 |
} |
415 | 429 |
|
416 | 430 |
func TestArg(t *testing.T) { |
417 | 431 |
b := newBuilderWithMockBackend() |
432 |
+ sb := newDispatchRequest(b, '`', nil, newBuildArgs(make(map[string]*string)), newStagesBuildResults()) |
|
418 | 433 |
|
419 | 434 |
argName := "foo" |
420 | 435 |
argVal := "bar" |
421 |
- argDef := fmt.Sprintf("%s=%s", argName, argVal) |
|
422 |
- |
|
423 |
- err := arg(defaultDispatchReq(b, argDef)) |
|
436 |
+ cmd := &instructions.ArgCommand{Key: argName, Value: &argVal} |
|
437 |
+ err := dispatch(sb, cmd) |
|
424 | 438 |
require.NoError(t, err) |
425 | 439 |
|
426 | 440 |
expected := map[string]string{argName: argVal} |
427 |
- assert.Equal(t, expected, b.buildArgs.GetAllAllowed()) |
|
441 |
+ assert.Equal(t, expected, sb.state.buildArgs.GetAllAllowed()) |
|
428 | 442 |
} |
429 | 443 |
|
430 | 444 |
func TestShell(t *testing.T) { |
431 | 445 |
b := newBuilderWithMockBackend() |
446 |
+ sb := newDispatchRequest(b, '`', nil, newBuildArgs(make(map[string]*string)), newStagesBuildResults()) |
|
432 | 447 |
|
433 | 448 |
shellCmd := "powershell" |
434 |
- req := defaultDispatchReq(b, shellCmd) |
|
435 |
- req.attributes = map[string]bool{"json": true} |
|
449 |
+ cmd := &instructions.ShellCommand{Shell: strslice.StrSlice{shellCmd}} |
|
436 | 450 |
|
437 |
- err := shell(req) |
|
451 |
+ err := dispatch(sb, cmd) |
|
438 | 452 |
require.NoError(t, err) |
439 | 453 |
|
440 | 454 |
expectedShell := strslice.StrSlice([]string{shellCmd}) |
441 |
- assert.Equal(t, expectedShell, req.state.runConfig.Shell) |
|
442 |
-} |
|
443 |
- |
|
444 |
-func TestParseOptInterval(t *testing.T) { |
|
445 |
- flInterval := &Flag{ |
|
446 |
- name: "interval", |
|
447 |
- flagType: stringType, |
|
448 |
- Value: "50ns", |
|
449 |
- } |
|
450 |
- _, err := parseOptInterval(flInterval) |
|
451 |
- testutil.ErrorContains(t, err, "cannot be less than 1ms") |
|
452 |
- |
|
453 |
- flInterval.Value = "1ms" |
|
454 |
- _, err = parseOptInterval(flInterval) |
|
455 |
- require.NoError(t, err) |
|
455 |
+ assert.Equal(t, expectedShell, sb.state.runConfig.Shell) |
|
456 | 456 |
} |
457 | 457 |
|
458 | 458 |
func TestPrependEnvOnCmd(t *testing.T) { |
... | ... |
@@ -469,8 +410,10 @@ func TestPrependEnvOnCmd(t *testing.T) { |
469 | 469 |
|
470 | 470 |
func TestRunWithBuildArgs(t *testing.T) { |
471 | 471 |
b := newBuilderWithMockBackend() |
472 |
- b.buildArgs.argsFromOptions["HTTP_PROXY"] = strPtr("FOO") |
|
472 |
+ args := newBuildArgs(make(map[string]*string)) |
|
473 |
+ args.argsFromOptions["HTTP_PROXY"] = strPtr("FOO") |
|
473 | 474 |
b.disableCommit = false |
475 |
+ sb := newDispatchRequest(b, '`', nil, args, newStagesBuildResults()) |
|
474 | 476 |
|
475 | 477 |
runConfig := &container.Config{} |
476 | 478 |
origCmd := strslice.StrSlice([]string{"cmd", "in", "from", "image"}) |
... | ... |
@@ -512,14 +455,18 @@ func TestRunWithBuildArgs(t *testing.T) { |
512 | 512 |
assert.Equal(t, strslice.StrSlice(nil), cfg.Config.Entrypoint) |
513 | 513 |
return "", nil |
514 | 514 |
} |
515 |
- |
|
516 |
- req := defaultDispatchReq(b, "abcdef") |
|
517 |
- require.NoError(t, from(req)) |
|
518 |
- b.buildArgs.AddArg("one", strPtr("two")) |
|
519 |
- |
|
520 |
- req.args = []string{"echo foo"} |
|
521 |
- require.NoError(t, run(req)) |
|
515 |
+ from := &instructions.Stage{BaseName: "abcdef"} |
|
516 |
+ err := initializeStage(sb, from) |
|
517 |
+ require.NoError(t, err) |
|
518 |
+ sb.state.buildArgs.AddArg("one", strPtr("two")) |
|
519 |
+ run := &instructions.RunCommand{ |
|
520 |
+ ShellDependantCmdLine: instructions.ShellDependantCmdLine{ |
|
521 |
+ CmdLine: strslice.StrSlice{"echo foo"}, |
|
522 |
+ PrependShell: true, |
|
523 |
+ }, |
|
524 |
+ } |
|
525 |
+ require.NoError(t, dispatch(sb, run)) |
|
522 | 526 |
|
523 | 527 |
// Check that runConfig.Cmd has not been modified by run |
524 |
- assert.Equal(t, origCmd, req.state.runConfig.Cmd) |
|
528 |
+ assert.Equal(t, origCmd, sb.state.runConfig.Cmd) |
|
525 | 529 |
} |
... | ... |
@@ -4,7 +4,6 @@ package dockerfile |
4 | 4 |
|
5 | 5 |
import ( |
6 | 6 |
"errors" |
7 |
- "fmt" |
|
8 | 7 |
"os" |
9 | 8 |
"path/filepath" |
10 | 9 |
) |
... | ... |
@@ -23,10 +22,6 @@ func normalizeWorkdir(_ string, current string, requested string) (string, error |
23 | 23 |
return requested, nil |
24 | 24 |
} |
25 | 25 |
|
26 |
-func errNotJSON(command, _ string) error { |
|
27 |
- return fmt.Errorf("%s requires the arguments to be in JSON form", command) |
|
28 |
-} |
|
29 |
- |
|
30 | 26 |
// equalEnvKeys compare two strings and returns true if they are equal. On |
31 | 27 |
// Windows this comparison is case insensitive. |
32 | 28 |
func equalEnvKeys(from, to string) bool { |
... | ... |
@@ -94,25 +94,6 @@ func normalizeWorkdirWindows(current string, requested string) (string, error) { |
94 | 94 |
return (strings.ToUpper(string(requested[0])) + requested[1:]), nil |
95 | 95 |
} |
96 | 96 |
|
97 |
-func errNotJSON(command, original string) error { |
|
98 |
- // For Windows users, give a hint if it looks like it might contain |
|
99 |
- // a path which hasn't been escaped such as ["c:\windows\system32\prog.exe", "-param"], |
|
100 |
- // as JSON must be escaped. Unfortunate... |
|
101 |
- // |
|
102 |
- // Specifically looking for quote-driveletter-colon-backslash, there's no |
|
103 |
- // double backslash and a [] pair. No, this is not perfect, but it doesn't |
|
104 |
- // have to be. It's simply a hint to make life a little easier. |
|
105 |
- extra := "" |
|
106 |
- original = filepath.FromSlash(strings.ToLower(strings.Replace(strings.ToLower(original), strings.ToLower(command)+" ", "", -1))) |
|
107 |
- if len(regexp.MustCompile(`"[a-z]:\\.*`).FindStringSubmatch(original)) > 0 && |
|
108 |
- !strings.Contains(original, `\\`) && |
|
109 |
- strings.Contains(original, "[") && |
|
110 |
- strings.Contains(original, "]") { |
|
111 |
- extra = fmt.Sprintf(`. It looks like '%s' includes a file path without an escaped back-slash. JSON requires back-slashes to be escaped such as ["c:\\path\\to\\file.exe", "/parameter"]`, original) |
|
112 |
- } |
|
113 |
- return fmt.Errorf("%s requires the arguments to be in JSON form%s", command, extra) |
|
114 |
-} |
|
115 |
- |
|
116 | 97 |
// equalEnvKeys compare two strings and returns true if they are equal. On |
117 | 98 |
// Windows this comparison is case insensitive. |
118 | 99 |
func equalEnvKeys(from, to string) bool { |
... | ... |
@@ -20,183 +20,178 @@ |
20 | 20 |
package dockerfile |
21 | 21 |
|
22 | 22 |
import ( |
23 |
- "bytes" |
|
24 |
- "fmt" |
|
23 |
+ "reflect" |
|
25 | 24 |
"runtime" |
25 |
+ "strconv" |
|
26 | 26 |
"strings" |
27 | 27 |
|
28 | 28 |
"github.com/docker/docker/api/types/container" |
29 | 29 |
"github.com/docker/docker/builder" |
30 |
- "github.com/docker/docker/builder/dockerfile/command" |
|
31 |
- "github.com/docker/docker/builder/dockerfile/parser" |
|
30 |
+ "github.com/docker/docker/builder/dockerfile/instructions" |
|
32 | 31 |
"github.com/docker/docker/pkg/system" |
33 | 32 |
"github.com/docker/docker/runconfig/opts" |
34 | 33 |
"github.com/pkg/errors" |
35 | 34 |
) |
36 | 35 |
|
37 |
-// Environment variable interpolation will happen on these statements only. |
|
38 |
-var replaceEnvAllowed = map[string]bool{ |
|
39 |
- command.Env: true, |
|
40 |
- command.Label: true, |
|
41 |
- command.Add: true, |
|
42 |
- command.Copy: true, |
|
43 |
- command.Workdir: true, |
|
44 |
- command.Expose: true, |
|
45 |
- command.Volume: true, |
|
46 |
- command.User: true, |
|
47 |
- command.StopSignal: true, |
|
48 |
- command.Arg: true, |
|
49 |
-} |
|
36 |
+func dispatch(d dispatchRequest, cmd instructions.Command) error { |
|
37 |
+ if c, ok := cmd.(instructions.PlatformSpecific); ok { |
|
38 |
+ err := c.CheckPlatform(d.builder.platform) |
|
39 |
+ if err != nil { |
|
40 |
+ return validationError{err} |
|
41 |
+ } |
|
42 |
+ } |
|
43 |
+ runConfigEnv := d.state.runConfig.Env |
|
44 |
+ envs := append(runConfigEnv, d.state.buildArgs.FilterAllowed(runConfigEnv)...) |
|
50 | 45 |
|
51 |
-// Certain commands are allowed to have their args split into more |
|
52 |
-// words after env var replacements. Meaning: |
|
53 |
-// ENV foo="123 456" |
|
54 |
-// EXPOSE $foo |
|
55 |
-// should result in the same thing as: |
|
56 |
-// EXPOSE 123 456 |
|
57 |
-// and not treat "123 456" as a single word. |
|
58 |
-// Note that: EXPOSE "$foo" and EXPOSE $foo are not the same thing. |
|
59 |
-// Quotes will cause it to still be treated as single word. |
|
60 |
-var allowWordExpansion = map[string]bool{ |
|
61 |
- command.Expose: true, |
|
62 |
-} |
|
46 |
+ if ex, ok := cmd.(instructions.SupportsSingleWordExpansion); ok { |
|
47 |
+ err := ex.Expand(func(word string) (string, error) { |
|
48 |
+ return d.shlex.ProcessWord(word, envs) |
|
49 |
+ }) |
|
50 |
+ if err != nil { |
|
51 |
+ return validationError{err} |
|
52 |
+ } |
|
53 |
+ } |
|
63 | 54 |
|
64 |
-type dispatchRequest struct { |
|
65 |
- builder *Builder // TODO: replace this with a smaller interface |
|
66 |
- args []string |
|
67 |
- attributes map[string]bool |
|
68 |
- flags *BFlags |
|
69 |
- original string |
|
70 |
- shlex *ShellLex |
|
71 |
- state *dispatchState |
|
72 |
- source builder.Source |
|
55 |
+ if d.builder.options.ForceRemove { |
|
56 |
+ defer d.builder.containerManager.RemoveAll(d.builder.Stdout) |
|
57 |
+ } |
|
58 |
+ |
|
59 |
+ switch c := cmd.(type) { |
|
60 |
+ case *instructions.EnvCommand: |
|
61 |
+ return dispatchEnv(d, c) |
|
62 |
+ case *instructions.MaintainerCommand: |
|
63 |
+ return dispatchMaintainer(d, c) |
|
64 |
+ case *instructions.LabelCommand: |
|
65 |
+ return dispatchLabel(d, c) |
|
66 |
+ case *instructions.AddCommand: |
|
67 |
+ return dispatchAdd(d, c) |
|
68 |
+ case *instructions.CopyCommand: |
|
69 |
+ return dispatchCopy(d, c) |
|
70 |
+ case *instructions.OnbuildCommand: |
|
71 |
+ return dispatchOnbuild(d, c) |
|
72 |
+ case *instructions.WorkdirCommand: |
|
73 |
+ return dispatchWorkdir(d, c) |
|
74 |
+ case *instructions.RunCommand: |
|
75 |
+ return dispatchRun(d, c) |
|
76 |
+ case *instructions.CmdCommand: |
|
77 |
+ return dispatchCmd(d, c) |
|
78 |
+ case *instructions.HealthCheckCommand: |
|
79 |
+ return dispatchHealthcheck(d, c) |
|
80 |
+ case *instructions.EntrypointCommand: |
|
81 |
+ return dispatchEntrypoint(d, c) |
|
82 |
+ case *instructions.ExposeCommand: |
|
83 |
+ return dispatchExpose(d, c, envs) |
|
84 |
+ case *instructions.UserCommand: |
|
85 |
+ return dispatchUser(d, c) |
|
86 |
+ case *instructions.VolumeCommand: |
|
87 |
+ return dispatchVolume(d, c) |
|
88 |
+ case *instructions.StopSignalCommand: |
|
89 |
+ return dispatchStopSignal(d, c) |
|
90 |
+ case *instructions.ArgCommand: |
|
91 |
+ return dispatchArg(d, c) |
|
92 |
+ case *instructions.ShellCommand: |
|
93 |
+ return dispatchShell(d, c) |
|
94 |
+ } |
|
95 |
+ return errors.Errorf("unsupported command type: %v", reflect.TypeOf(cmd)) |
|
73 | 96 |
} |
74 | 97 |
|
75 |
-func newDispatchRequestFromOptions(options dispatchOptions, builder *Builder, args []string) dispatchRequest { |
|
76 |
- return dispatchRequest{ |
|
77 |
- builder: builder, |
|
78 |
- args: args, |
|
79 |
- attributes: options.node.Attributes, |
|
80 |
- original: options.node.Original, |
|
81 |
- flags: NewBFlagsWithArgs(options.node.Flags), |
|
82 |
- shlex: options.shlex, |
|
83 |
- state: options.state, |
|
84 |
- source: options.source, |
|
85 |
- } |
|
98 |
+// dispatchState is a data object which is modified by dispatchers |
|
99 |
+type dispatchState struct { |
|
100 |
+ runConfig *container.Config |
|
101 |
+ maintainer string |
|
102 |
+ cmdSet bool |
|
103 |
+ imageID string |
|
104 |
+ baseImage builder.Image |
|
105 |
+ stageName string |
|
106 |
+ buildArgs *buildArgs |
|
86 | 107 |
} |
87 | 108 |
|
88 |
-type dispatcher func(dispatchRequest) error |
|
109 |
+func newDispatchState(baseArgs *buildArgs) *dispatchState { |
|
110 |
+ args := baseArgs.Clone() |
|
111 |
+ args.ResetAllowed() |
|
112 |
+ return &dispatchState{runConfig: &container.Config{}, buildArgs: args} |
|
113 |
+} |
|
89 | 114 |
|
90 |
-var evaluateTable map[string]dispatcher |
|
115 |
+type stagesBuildResults struct { |
|
116 |
+ flat []*container.Config |
|
117 |
+ indexed map[string]*container.Config |
|
118 |
+} |
|
91 | 119 |
|
92 |
-func init() { |
|
93 |
- evaluateTable = map[string]dispatcher{ |
|
94 |
- command.Add: add, |
|
95 |
- command.Arg: arg, |
|
96 |
- command.Cmd: cmd, |
|
97 |
- command.Copy: dispatchCopy, // copy() is a go builtin |
|
98 |
- command.Entrypoint: entrypoint, |
|
99 |
- command.Env: env, |
|
100 |
- command.Expose: expose, |
|
101 |
- command.From: from, |
|
102 |
- command.Healthcheck: healthcheck, |
|
103 |
- command.Label: label, |
|
104 |
- command.Maintainer: maintainer, |
|
105 |
- command.Onbuild: onbuild, |
|
106 |
- command.Run: run, |
|
107 |
- command.Shell: shell, |
|
108 |
- command.StopSignal: stopSignal, |
|
109 |
- command.User: user, |
|
110 |
- command.Volume: volume, |
|
111 |
- command.Workdir: workdir, |
|
120 |
+func newStagesBuildResults() *stagesBuildResults { |
|
121 |
+ return &stagesBuildResults{ |
|
122 |
+ indexed: make(map[string]*container.Config), |
|
112 | 123 |
} |
113 | 124 |
} |
114 | 125 |
|
115 |
-func formatStep(stepN int, stepTotal int) string { |
|
116 |
- return fmt.Sprintf("%d/%d", stepN+1, stepTotal) |
|
126 |
+func (r *stagesBuildResults) getByName(name string) (*container.Config, bool) { |
|
127 |
+ c, ok := r.indexed[strings.ToLower(name)] |
|
128 |
+ return c, ok |
|
117 | 129 |
} |
118 | 130 |
|
119 |
-// This method is the entrypoint to all statement handling routines. |
|
120 |
-// |
|
121 |
-// Almost all nodes will have this structure: |
|
122 |
-// Child[Node, Node, Node] where Child is from parser.Node.Children and each |
|
123 |
-// node comes from parser.Node.Next. This forms a "line" with a statement and |
|
124 |
-// arguments and we process them in this normalized form by hitting |
|
125 |
-// evaluateTable with the leaf nodes of the command and the Builder object. |
|
126 |
-// |
|
127 |
-// ONBUILD is a special case; in this case the parser will emit: |
|
128 |
-// Child[Node, Child[Node, Node...]] where the first node is the literal |
|
129 |
-// "onbuild" and the child entrypoint is the command of the ONBUILD statement, |
|
130 |
-// such as `RUN` in ONBUILD RUN foo. There is special case logic in here to |
|
131 |
-// deal with that, at least until it becomes more of a general concern with new |
|
132 |
-// features. |
|
133 |
-func (b *Builder) dispatch(options dispatchOptions) (*dispatchState, error) { |
|
134 |
- node := options.node |
|
135 |
- cmd := node.Value |
|
136 |
- upperCasedCmd := strings.ToUpper(cmd) |
|
137 |
- |
|
138 |
- // To ensure the user is given a decent error message if the platform |
|
139 |
- // on which the daemon is running does not support a builder command. |
|
140 |
- if err := platformSupports(strings.ToLower(cmd)); err != nil { |
|
141 |
- buildsFailed.WithValues(metricsCommandNotSupportedError).Inc() |
|
142 |
- return nil, validationError{err} |
|
131 |
+func (r *stagesBuildResults) validateIndex(i int) error { |
|
132 |
+ if i == len(r.flat) { |
|
133 |
+ return errors.New("refers to current build stage") |
|
143 | 134 |
} |
144 |
- |
|
145 |
- msg := bytes.NewBufferString(fmt.Sprintf("Step %s : %s%s", |
|
146 |
- options.stepMsg, upperCasedCmd, formatFlags(node.Flags))) |
|
147 |
- |
|
148 |
- args := []string{} |
|
149 |
- ast := node |
|
150 |
- if cmd == command.Onbuild { |
|
151 |
- var err error |
|
152 |
- ast, args, err = handleOnBuildNode(node, msg) |
|
153 |
- if err != nil { |
|
154 |
- return nil, validationError{err} |
|
155 |
- } |
|
135 |
+ if i < 0 || i > len(r.flat) { |
|
136 |
+ return errors.New("index out of bounds") |
|
156 | 137 |
} |
138 |
+ return nil |
|
139 |
+} |
|
157 | 140 |
|
158 |
- runConfigEnv := options.state.runConfig.Env |
|
159 |
- envs := append(runConfigEnv, b.buildArgs.FilterAllowed(runConfigEnv)...) |
|
160 |
- processFunc := createProcessWordFunc(options.shlex, cmd, envs) |
|
161 |
- words, err := getDispatchArgsFromNode(ast, processFunc, msg) |
|
141 |
+func (r *stagesBuildResults) get(nameOrIndex string) (*container.Config, error) { |
|
142 |
+ if c, ok := r.getByName(nameOrIndex); ok { |
|
143 |
+ return c, nil |
|
144 |
+ } |
|
145 |
+ ix, err := strconv.ParseInt(nameOrIndex, 10, 0) |
|
162 | 146 |
if err != nil { |
163 |
- buildsFailed.WithValues(metricsErrorProcessingCommandsError).Inc() |
|
164 |
- return nil, validationError{err} |
|
147 |
+ return nil, nil |
|
165 | 148 |
} |
166 |
- args = append(args, words...) |
|
149 |
+ if err := r.validateIndex(int(ix)); err != nil { |
|
150 |
+ return nil, err |
|
151 |
+ } |
|
152 |
+ return r.flat[ix], nil |
|
153 |
+} |
|
167 | 154 |
|
168 |
- fmt.Fprintln(b.Stdout, msg.String()) |
|
155 |
+func (r *stagesBuildResults) checkStageNameAvailable(name string) error { |
|
156 |
+ if name != "" { |
|
157 |
+ if _, ok := r.getByName(name); ok { |
|
158 |
+ return errors.Errorf("%s stage name already used", name) |
|
159 |
+ } |
|
160 |
+ } |
|
161 |
+ return nil |
|
162 |
+} |
|
169 | 163 |
|
170 |
- f, ok := evaluateTable[cmd] |
|
171 |
- if !ok { |
|
172 |
- buildsFailed.WithValues(metricsUnknownInstructionError).Inc() |
|
173 |
- return nil, validationError{errors.Errorf("unknown instruction: %s", upperCasedCmd)} |
|
164 |
+func (r *stagesBuildResults) commitStage(name string, config *container.Config) error { |
|
165 |
+ if name != "" { |
|
166 |
+ if _, ok := r.getByName(name); ok { |
|
167 |
+ return errors.Errorf("%s stage name already used", name) |
|
168 |
+ } |
|
169 |
+ r.indexed[strings.ToLower(name)] = config |
|
174 | 170 |
} |
175 |
- options.state.updateRunConfig() |
|
176 |
- err = f(newDispatchRequestFromOptions(options, b, args)) |
|
177 |
- return options.state, err |
|
171 |
+ r.flat = append(r.flat, config) |
|
172 |
+ return nil |
|
173 |
+} |
|
174 |
+ |
|
175 |
+func commitStage(state *dispatchState, stages *stagesBuildResults) error { |
|
176 |
+ return stages.commitStage(state.stageName, state.runConfig) |
|
178 | 177 |
} |
179 | 178 |
|
180 |
-type dispatchOptions struct { |
|
179 |
+type dispatchRequest struct { |
|
181 | 180 |
state *dispatchState |
182 |
- stepMsg string |
|
183 |
- node *parser.Node |
|
184 | 181 |
shlex *ShellLex |
182 |
+ builder *Builder |
|
185 | 183 |
source builder.Source |
184 |
+ stages *stagesBuildResults |
|
186 | 185 |
} |
187 | 186 |
|
188 |
-// dispatchState is a data object which is modified by dispatchers |
|
189 |
-type dispatchState struct { |
|
190 |
- runConfig *container.Config |
|
191 |
- maintainer string |
|
192 |
- cmdSet bool |
|
193 |
- imageID string |
|
194 |
- baseImage builder.Image |
|
195 |
- stageName string |
|
196 |
-} |
|
197 |
- |
|
198 |
-func newDispatchState() *dispatchState { |
|
199 |
- return &dispatchState{runConfig: &container.Config{}} |
|
187 |
+func newDispatchRequest(builder *Builder, escapeToken rune, source builder.Source, buildArgs *buildArgs, stages *stagesBuildResults) dispatchRequest { |
|
188 |
+ return dispatchRequest{ |
|
189 |
+ state: newDispatchState(buildArgs), |
|
190 |
+ shlex: NewShellLex(escapeToken), |
|
191 |
+ builder: builder, |
|
192 |
+ source: source, |
|
193 |
+ stages: stages, |
|
194 |
+ } |
|
200 | 195 |
} |
201 | 196 |
|
202 | 197 |
func (s *dispatchState) updateRunConfig() { |
... | ... |
@@ -220,12 +215,14 @@ func (s *dispatchState) beginStage(stageName string, image builder.Image) { |
220 | 220 |
s.imageID = image.ImageID() |
221 | 221 |
|
222 | 222 |
if image.RunConfig() != nil { |
223 |
- s.runConfig = image.RunConfig() |
|
223 |
+ s.runConfig = copyRunConfig(image.RunConfig()) // copy avoids referencing the same instance when 2 stages have the same base |
|
224 | 224 |
} else { |
225 | 225 |
s.runConfig = &container.Config{} |
226 | 226 |
} |
227 | 227 |
s.baseImage = image |
228 | 228 |
s.setDefaultPath() |
229 |
+ s.runConfig.OpenStdin = false |
|
230 |
+ s.runConfig.StdinOnce = false |
|
229 | 231 |
} |
230 | 232 |
|
231 | 233 |
// Add the default PATH to runConfig.ENV if one exists for the platform and there |
... | ... |
@@ -244,84 +241,3 @@ func (s *dispatchState) setDefaultPath() { |
244 | 244 |
s.runConfig.Env = append(s.runConfig.Env, "PATH="+system.DefaultPathEnv(platform)) |
245 | 245 |
} |
246 | 246 |
} |
247 |
- |
|
248 |
-func handleOnBuildNode(ast *parser.Node, msg *bytes.Buffer) (*parser.Node, []string, error) { |
|
249 |
- if ast.Next == nil { |
|
250 |
- return nil, nil, validationError{errors.New("ONBUILD requires at least one argument")} |
|
251 |
- } |
|
252 |
- ast = ast.Next.Children[0] |
|
253 |
- msg.WriteString(" " + ast.Value + formatFlags(ast.Flags)) |
|
254 |
- return ast, []string{ast.Value}, nil |
|
255 |
-} |
|
256 |
- |
|
257 |
-func formatFlags(flags []string) string { |
|
258 |
- if len(flags) > 0 { |
|
259 |
- return " " + strings.Join(flags, " ") |
|
260 |
- } |
|
261 |
- return "" |
|
262 |
-} |
|
263 |
- |
|
264 |
-func getDispatchArgsFromNode(ast *parser.Node, processFunc processWordFunc, msg *bytes.Buffer) ([]string, error) { |
|
265 |
- args := []string{} |
|
266 |
- for i := 0; ast.Next != nil; i++ { |
|
267 |
- ast = ast.Next |
|
268 |
- words, err := processFunc(ast.Value) |
|
269 |
- if err != nil { |
|
270 |
- return nil, err |
|
271 |
- } |
|
272 |
- args = append(args, words...) |
|
273 |
- msg.WriteString(" " + ast.Value) |
|
274 |
- } |
|
275 |
- return args, nil |
|
276 |
-} |
|
277 |
- |
|
278 |
-type processWordFunc func(string) ([]string, error) |
|
279 |
- |
|
280 |
-func createProcessWordFunc(shlex *ShellLex, cmd string, envs []string) processWordFunc { |
|
281 |
- switch { |
|
282 |
- case !replaceEnvAllowed[cmd]: |
|
283 |
- return func(word string) ([]string, error) { |
|
284 |
- return []string{word}, nil |
|
285 |
- } |
|
286 |
- case allowWordExpansion[cmd]: |
|
287 |
- return func(word string) ([]string, error) { |
|
288 |
- return shlex.ProcessWords(word, envs) |
|
289 |
- } |
|
290 |
- default: |
|
291 |
- return func(word string) ([]string, error) { |
|
292 |
- word, err := shlex.ProcessWord(word, envs) |
|
293 |
- return []string{word}, err |
|
294 |
- } |
|
295 |
- } |
|
296 |
-} |
|
297 |
- |
|
298 |
-// checkDispatch does a simple check for syntax errors of the Dockerfile. |
|
299 |
-// Because some of the instructions can only be validated through runtime, |
|
300 |
-// arg, env, etc., this syntax check will not be complete and could not replace |
|
301 |
-// the runtime check. Instead, this function is only a helper that allows |
|
302 |
-// user to find out the obvious error in Dockerfile earlier on. |
|
303 |
-func checkDispatch(ast *parser.Node) error { |
|
304 |
- cmd := ast.Value |
|
305 |
- upperCasedCmd := strings.ToUpper(cmd) |
|
306 |
- |
|
307 |
- // To ensure the user is given a decent error message if the platform |
|
308 |
- // on which the daemon is running does not support a builder command. |
|
309 |
- if err := platformSupports(strings.ToLower(cmd)); err != nil { |
|
310 |
- return err |
|
311 |
- } |
|
312 |
- |
|
313 |
- // The instruction itself is ONBUILD, we will make sure it follows with at |
|
314 |
- // least one argument |
|
315 |
- if upperCasedCmd == "ONBUILD" { |
|
316 |
- if ast.Next == nil { |
|
317 |
- buildsFailed.WithValues(metricsMissingOnbuildArgumentsError).Inc() |
|
318 |
- return errors.New("ONBUILD requires at least one argument") |
|
319 |
- } |
|
320 |
- } |
|
321 |
- |
|
322 |
- if _, ok := evaluateTable[cmd]; ok { |
|
323 |
- return nil |
|
324 |
- } |
|
325 |
- buildsFailed.WithValues(metricsUnknownInstructionError).Inc() |
|
326 |
- return errors.Errorf("unknown instruction: %s", upperCasedCmd) |
|
327 |
-} |
... | ... |
@@ -1,13 +1,9 @@ |
1 | 1 |
package dockerfile |
2 | 2 |
|
3 | 3 |
import ( |
4 |
- "io/ioutil" |
|
5 |
- "strings" |
|
6 | 4 |
"testing" |
7 | 5 |
|
8 |
- "github.com/docker/docker/api/types" |
|
9 |
- "github.com/docker/docker/api/types/container" |
|
10 |
- "github.com/docker/docker/builder/dockerfile/parser" |
|
6 |
+ "github.com/docker/docker/builder/dockerfile/instructions" |
|
11 | 7 |
"github.com/docker/docker/builder/remotecontext" |
12 | 8 |
"github.com/docker/docker/internal/testutil" |
13 | 9 |
"github.com/docker/docker/pkg/archive" |
... | ... |
@@ -15,8 +11,9 @@ import ( |
15 | 15 |
) |
16 | 16 |
|
17 | 17 |
type dispatchTestCase struct { |
18 |
- name, dockerfile, expectedError string |
|
19 |
- files map[string]string |
|
18 |
+ name, expectedError string |
|
19 |
+ cmd instructions.Command |
|
20 |
+ files map[string]string |
|
20 | 21 |
} |
21 | 22 |
|
22 | 23 |
func init() { |
... | ... |
@@ -24,108 +21,73 @@ func init() { |
24 | 24 |
} |
25 | 25 |
|
26 | 26 |
func initDispatchTestCases() []dispatchTestCase { |
27 |
- dispatchTestCases := []dispatchTestCase{{ |
|
28 |
- name: "copyEmptyWhitespace", |
|
29 |
- dockerfile: `COPY |
|
30 |
- quux \ |
|
31 |
- bar`, |
|
32 |
- expectedError: "COPY requires at least two arguments", |
|
33 |
- }, |
|
34 |
- { |
|
35 |
- name: "ONBUILD forbidden FROM", |
|
36 |
- dockerfile: "ONBUILD FROM scratch", |
|
37 |
- expectedError: "FROM isn't allowed as an ONBUILD trigger", |
|
38 |
- files: nil, |
|
39 |
- }, |
|
40 |
- { |
|
41 |
- name: "ONBUILD forbidden MAINTAINER", |
|
42 |
- dockerfile: "ONBUILD MAINTAINER docker.io", |
|
43 |
- expectedError: "MAINTAINER isn't allowed as an ONBUILD trigger", |
|
44 |
- files: nil, |
|
45 |
- }, |
|
46 |
- { |
|
47 |
- name: "ARG two arguments", |
|
48 |
- dockerfile: "ARG foo bar", |
|
49 |
- expectedError: "ARG requires exactly one argument", |
|
50 |
- files: nil, |
|
51 |
- }, |
|
52 |
- { |
|
53 |
- name: "MAINTAINER unknown flag", |
|
54 |
- dockerfile: "MAINTAINER --boo joe@example.com", |
|
55 |
- expectedError: "Unknown flag: boo", |
|
56 |
- files: nil, |
|
57 |
- }, |
|
58 |
- { |
|
59 |
- name: "ADD multiple files to file", |
|
60 |
- dockerfile: "ADD file1.txt file2.txt test", |
|
61 |
- expectedError: "When using ADD with more than one source file, the destination must be a directory and end with a /", |
|
62 |
- files: map[string]string{"file1.txt": "test1", "file2.txt": "test2"}, |
|
63 |
- }, |
|
64 |
- { |
|
65 |
- name: "JSON ADD multiple files to file", |
|
66 |
- dockerfile: `ADD ["file1.txt", "file2.txt", "test"]`, |
|
67 |
- expectedError: "When using ADD with more than one source file, the destination must be a directory and end with a /", |
|
68 |
- files: map[string]string{"file1.txt": "test1", "file2.txt": "test2"}, |
|
69 |
- }, |
|
70 |
- { |
|
71 |
- name: "Wildcard ADD multiple files to file", |
|
72 |
- dockerfile: "ADD file*.txt test", |
|
27 |
+ dispatchTestCases := []dispatchTestCase{ |
|
28 |
+ { |
|
29 |
+ name: "ADD multiple files to file", |
|
30 |
+ cmd: &instructions.AddCommand{SourcesAndDest: instructions.SourcesAndDest{ |
|
31 |
+ "file1.txt", |
|
32 |
+ "file2.txt", |
|
33 |
+ "test", |
|
34 |
+ }}, |
|
73 | 35 |
expectedError: "When using ADD with more than one source file, the destination must be a directory and end with a /", |
74 | 36 |
files: map[string]string{"file1.txt": "test1", "file2.txt": "test2"}, |
75 | 37 |
}, |
76 | 38 |
{ |
77 |
- name: "Wildcard JSON ADD multiple files to file", |
|
78 |
- dockerfile: `ADD ["file*.txt", "test"]`, |
|
39 |
+ name: "Wildcard ADD multiple files to file", |
|
40 |
+ cmd: &instructions.AddCommand{SourcesAndDest: instructions.SourcesAndDest{ |
|
41 |
+ "file*.txt", |
|
42 |
+ "test", |
|
43 |
+ }}, |
|
79 | 44 |
expectedError: "When using ADD with more than one source file, the destination must be a directory and end with a /", |
80 | 45 |
files: map[string]string{"file1.txt": "test1", "file2.txt": "test2"}, |
81 | 46 |
}, |
82 | 47 |
{ |
83 |
- name: "COPY multiple files to file", |
|
84 |
- dockerfile: "COPY file1.txt file2.txt test", |
|
85 |
- expectedError: "When using COPY with more than one source file, the destination must be a directory and end with a /", |
|
86 |
- files: map[string]string{"file1.txt": "test1", "file2.txt": "test2"}, |
|
87 |
- }, |
|
88 |
- { |
|
89 |
- name: "JSON COPY multiple files to file", |
|
90 |
- dockerfile: `COPY ["file1.txt", "file2.txt", "test"]`, |
|
48 |
+ name: "COPY multiple files to file", |
|
49 |
+ cmd: &instructions.CopyCommand{SourcesAndDest: instructions.SourcesAndDest{ |
|
50 |
+ "file1.txt", |
|
51 |
+ "file2.txt", |
|
52 |
+ "test", |
|
53 |
+ }}, |
|
91 | 54 |
expectedError: "When using COPY with more than one source file, the destination must be a directory and end with a /", |
92 | 55 |
files: map[string]string{"file1.txt": "test1", "file2.txt": "test2"}, |
93 | 56 |
}, |
94 | 57 |
{ |
95 |
- name: "ADD multiple files to file with whitespace", |
|
96 |
- dockerfile: `ADD [ "test file1.txt", "test file2.txt", "test" ]`, |
|
58 |
+ name: "ADD multiple files to file with whitespace", |
|
59 |
+ cmd: &instructions.AddCommand{SourcesAndDest: instructions.SourcesAndDest{ |
|
60 |
+ "test file1.txt", |
|
61 |
+ "test file2.txt", |
|
62 |
+ "test", |
|
63 |
+ }}, |
|
97 | 64 |
expectedError: "When using ADD with more than one source file, the destination must be a directory and end with a /", |
98 | 65 |
files: map[string]string{"test file1.txt": "test1", "test file2.txt": "test2"}, |
99 | 66 |
}, |
100 | 67 |
{ |
101 |
- name: "COPY multiple files to file with whitespace", |
|
102 |
- dockerfile: `COPY [ "test file1.txt", "test file2.txt", "test" ]`, |
|
68 |
+ name: "COPY multiple files to file with whitespace", |
|
69 |
+ cmd: &instructions.CopyCommand{SourcesAndDest: instructions.SourcesAndDest{ |
|
70 |
+ "test file1.txt", |
|
71 |
+ "test file2.txt", |
|
72 |
+ "test", |
|
73 |
+ }}, |
|
103 | 74 |
expectedError: "When using COPY with more than one source file, the destination must be a directory and end with a /", |
104 | 75 |
files: map[string]string{"test file1.txt": "test1", "test file2.txt": "test2"}, |
105 | 76 |
}, |
106 | 77 |
{ |
107 |
- name: "COPY wildcard no files", |
|
108 |
- dockerfile: `COPY file*.txt /tmp/`, |
|
78 |
+ name: "COPY wildcard no files", |
|
79 |
+ cmd: &instructions.CopyCommand{SourcesAndDest: instructions.SourcesAndDest{ |
|
80 |
+ "file*.txt", |
|
81 |
+ "/tmp/", |
|
82 |
+ }}, |
|
109 | 83 |
expectedError: "COPY failed: no source files were specified", |
110 | 84 |
files: nil, |
111 | 85 |
}, |
112 | 86 |
{ |
113 |
- name: "COPY url", |
|
114 |
- dockerfile: `COPY https://index.docker.io/robots.txt /`, |
|
87 |
+ name: "COPY url", |
|
88 |
+ cmd: &instructions.CopyCommand{SourcesAndDest: instructions.SourcesAndDest{ |
|
89 |
+ "https://index.docker.io/robots.txt", |
|
90 |
+ "/", |
|
91 |
+ }}, |
|
115 | 92 |
expectedError: "source can't be a URL for COPY", |
116 | 93 |
files: nil, |
117 |
- }, |
|
118 |
- { |
|
119 |
- name: "Chaining ONBUILD", |
|
120 |
- dockerfile: `ONBUILD ONBUILD RUN touch foobar`, |
|
121 |
- expectedError: "Chaining ONBUILD via `ONBUILD ONBUILD` isn't allowed", |
|
122 |
- files: nil, |
|
123 |
- }, |
|
124 |
- { |
|
125 |
- name: "Invalid instruction", |
|
126 |
- dockerfile: `foo bar`, |
|
127 |
- expectedError: "unknown instruction: FOO", |
|
128 |
- files: nil, |
|
129 | 94 |
}} |
130 | 95 |
|
131 | 96 |
return dispatchTestCases |
... | ... |
@@ -171,33 +133,8 @@ func executeTestCase(t *testing.T, testCase dispatchTestCase) { |
171 | 171 |
} |
172 | 172 |
}() |
173 | 173 |
|
174 |
- r := strings.NewReader(testCase.dockerfile) |
|
175 |
- result, err := parser.Parse(r) |
|
176 |
- |
|
177 |
- if err != nil { |
|
178 |
- t.Fatalf("Error when parsing Dockerfile: %s", err) |
|
179 |
- } |
|
180 |
- |
|
181 |
- options := &types.ImageBuildOptions{ |
|
182 |
- BuildArgs: make(map[string]*string), |
|
183 |
- } |
|
184 |
- |
|
185 |
- b := &Builder{ |
|
186 |
- options: options, |
|
187 |
- Stdout: ioutil.Discard, |
|
188 |
- buildArgs: newBuildArgs(options.BuildArgs), |
|
189 |
- } |
|
190 |
- |
|
191 |
- shlex := NewShellLex(parser.DefaultEscapeToken) |
|
192 |
- n := result.AST |
|
193 |
- state := &dispatchState{runConfig: &container.Config{}} |
|
194 |
- opts := dispatchOptions{ |
|
195 |
- state: state, |
|
196 |
- stepMsg: formatStep(0, len(n.Children)), |
|
197 |
- node: n.Children[0], |
|
198 |
- shlex: shlex, |
|
199 |
- source: context, |
|
200 |
- } |
|
201 |
- _, err = b.dispatch(opts) |
|
174 |
+ b := newBuilderWithMockBackend() |
|
175 |
+ sb := newDispatchRequest(b, '`', context, newBuildArgs(make(map[string]*string)), newStagesBuildResults()) |
|
176 |
+ err = dispatch(sb, testCase.cmd) |
|
202 | 177 |
testutil.ErrorContains(t, err, testCase.expectedError) |
203 | 178 |
} |
204 | 179 |
deleted file mode 100644 |
... | ... |
@@ -1,9 +0,0 @@ |
1 |
-// +build !windows |
|
2 |
- |
|
3 |
-package dockerfile |
|
4 |
- |
|
5 |
-// platformSupports is a short-term function to give users a quality error |
|
6 |
-// message if a Dockerfile uses a command not supported on the platform. |
|
7 |
-func platformSupports(command string) error { |
|
8 |
- return nil |
|
9 |
-} |
10 | 1 |
deleted file mode 100644 |
... | ... |
@@ -1,13 +0,0 @@ |
1 |
-package dockerfile |
|
2 |
- |
|
3 |
-import "fmt" |
|
4 |
- |
|
5 |
-// platformSupports is gives users a quality error message if a Dockerfile uses |
|
6 |
-// a command not supported on the platform. |
|
7 |
-func platformSupports(command string) error { |
|
8 |
- switch command { |
|
9 |
- case "stopsignal": |
|
10 |
- return fmt.Errorf("The daemon on this platform does not support the command '%s'", command) |
|
11 |
- } |
|
12 |
- return nil |
|
13 |
-} |
... | ... |
@@ -1,9 +1,6 @@ |
1 | 1 |
package dockerfile |
2 | 2 |
|
3 | 3 |
import ( |
4 |
- "strconv" |
|
5 |
- "strings" |
|
6 |
- |
|
7 | 4 |
"github.com/docker/docker/api/types/backend" |
8 | 5 |
"github.com/docker/docker/builder" |
9 | 6 |
"github.com/docker/docker/builder/remotecontext" |
... | ... |
@@ -13,79 +10,6 @@ import ( |
13 | 13 |
"golang.org/x/net/context" |
14 | 14 |
) |
15 | 15 |
|
16 |
-type buildStage struct { |
|
17 |
- id string |
|
18 |
-} |
|
19 |
- |
|
20 |
-func newBuildStage(imageID string) *buildStage { |
|
21 |
- return &buildStage{id: imageID} |
|
22 |
-} |
|
23 |
- |
|
24 |
-func (b *buildStage) ImageID() string { |
|
25 |
- return b.id |
|
26 |
-} |
|
27 |
- |
|
28 |
-func (b *buildStage) update(imageID string) { |
|
29 |
- b.id = imageID |
|
30 |
-} |
|
31 |
- |
|
32 |
-// buildStages tracks each stage of a build so they can be retrieved by index |
|
33 |
-// or by name. |
|
34 |
-type buildStages struct { |
|
35 |
- sequence []*buildStage |
|
36 |
- byName map[string]*buildStage |
|
37 |
-} |
|
38 |
- |
|
39 |
-func newBuildStages() *buildStages { |
|
40 |
- return &buildStages{byName: make(map[string]*buildStage)} |
|
41 |
-} |
|
42 |
- |
|
43 |
-func (s *buildStages) getByName(name string) (*buildStage, bool) { |
|
44 |
- stage, ok := s.byName[strings.ToLower(name)] |
|
45 |
- return stage, ok |
|
46 |
-} |
|
47 |
- |
|
48 |
-func (s *buildStages) get(indexOrName string) (*buildStage, error) { |
|
49 |
- index, err := strconv.Atoi(indexOrName) |
|
50 |
- if err == nil { |
|
51 |
- if err := s.validateIndex(index); err != nil { |
|
52 |
- return nil, err |
|
53 |
- } |
|
54 |
- return s.sequence[index], nil |
|
55 |
- } |
|
56 |
- if im, ok := s.byName[strings.ToLower(indexOrName)]; ok { |
|
57 |
- return im, nil |
|
58 |
- } |
|
59 |
- return nil, nil |
|
60 |
-} |
|
61 |
- |
|
62 |
-func (s *buildStages) validateIndex(i int) error { |
|
63 |
- if i < 0 || i >= len(s.sequence)-1 { |
|
64 |
- if i == len(s.sequence)-1 { |
|
65 |
- return errors.New("refers to current build stage") |
|
66 |
- } |
|
67 |
- return errors.New("index out of bounds") |
|
68 |
- } |
|
69 |
- return nil |
|
70 |
-} |
|
71 |
- |
|
72 |
-func (s *buildStages) add(name string, image builder.Image) error { |
|
73 |
- stage := newBuildStage(image.ImageID()) |
|
74 |
- name = strings.ToLower(name) |
|
75 |
- if len(name) > 0 { |
|
76 |
- if _, ok := s.byName[name]; ok { |
|
77 |
- return errors.Errorf("duplicate name %s", name) |
|
78 |
- } |
|
79 |
- s.byName[name] = stage |
|
80 |
- } |
|
81 |
- s.sequence = append(s.sequence, stage) |
|
82 |
- return nil |
|
83 |
-} |
|
84 |
- |
|
85 |
-func (s *buildStages) update(imageID string) { |
|
86 |
- s.sequence[len(s.sequence)-1].update(imageID) |
|
87 |
-} |
|
88 |
- |
|
89 | 16 |
type getAndMountFunc func(string, bool) (builder.Image, builder.ReleaseableLayer, error) |
90 | 17 |
|
91 | 18 |
// imageSources mounts images and provides a cache for mounted images. It tracks |
92 | 19 |
new file mode 100644 |
... | ... |
@@ -0,0 +1,183 @@ |
0 |
+package instructions |
|
1 |
+ |
|
2 |
+import ( |
|
3 |
+ "fmt" |
|
4 |
+ "strings" |
|
5 |
+) |
|
6 |
+ |
|
7 |
+// FlagType is the type of the build flag |
|
8 |
+type FlagType int |
|
9 |
+ |
|
10 |
+const ( |
|
11 |
+ boolType FlagType = iota |
|
12 |
+ stringType |
|
13 |
+) |
|
14 |
+ |
|
15 |
+// BFlags contains all flags information for the builder |
|
16 |
+type BFlags struct { |
|
17 |
+ Args []string // actual flags/args from cmd line |
|
18 |
+ flags map[string]*Flag |
|
19 |
+ used map[string]*Flag |
|
20 |
+ Err error |
|
21 |
+} |
|
22 |
+ |
|
23 |
+// Flag contains all information for a flag |
|
24 |
+type Flag struct { |
|
25 |
+ bf *BFlags |
|
26 |
+ name string |
|
27 |
+ flagType FlagType |
|
28 |
+ Value string |
|
29 |
+} |
|
30 |
+ |
|
31 |
+// NewBFlags returns the new BFlags struct |
|
32 |
+func NewBFlags() *BFlags { |
|
33 |
+ return &BFlags{ |
|
34 |
+ flags: make(map[string]*Flag), |
|
35 |
+ used: make(map[string]*Flag), |
|
36 |
+ } |
|
37 |
+} |
|
38 |
+ |
|
39 |
+// NewBFlagsWithArgs returns the new BFlags struct with Args set to args |
|
40 |
+func NewBFlagsWithArgs(args []string) *BFlags { |
|
41 |
+ flags := NewBFlags() |
|
42 |
+ flags.Args = args |
|
43 |
+ return flags |
|
44 |
+} |
|
45 |
+ |
|
46 |
+// AddBool adds a bool flag to BFlags |
|
47 |
+// Note, any error will be generated when Parse() is called (see Parse). |
|
48 |
+func (bf *BFlags) AddBool(name string, def bool) *Flag { |
|
49 |
+ flag := bf.addFlag(name, boolType) |
|
50 |
+ if flag == nil { |
|
51 |
+ return nil |
|
52 |
+ } |
|
53 |
+ if def { |
|
54 |
+ flag.Value = "true" |
|
55 |
+ } else { |
|
56 |
+ flag.Value = "false" |
|
57 |
+ } |
|
58 |
+ return flag |
|
59 |
+} |
|
60 |
+ |
|
61 |
+// AddString adds a string flag to BFlags |
|
62 |
+// Note, any error will be generated when Parse() is called (see Parse). |
|
63 |
+func (bf *BFlags) AddString(name string, def string) *Flag { |
|
64 |
+ flag := bf.addFlag(name, stringType) |
|
65 |
+ if flag == nil { |
|
66 |
+ return nil |
|
67 |
+ } |
|
68 |
+ flag.Value = def |
|
69 |
+ return flag |
|
70 |
+} |
|
71 |
+ |
|
72 |
+// addFlag is a generic func used by the other AddXXX() func |
|
73 |
+// to add a new flag to the BFlags struct. |
|
74 |
+// Note, any error will be generated when Parse() is called (see Parse). |
|
75 |
+func (bf *BFlags) addFlag(name string, flagType FlagType) *Flag { |
|
76 |
+ if _, ok := bf.flags[name]; ok { |
|
77 |
+ bf.Err = fmt.Errorf("Duplicate flag defined: %s", name) |
|
78 |
+ return nil |
|
79 |
+ } |
|
80 |
+ |
|
81 |
+ newFlag := &Flag{ |
|
82 |
+ bf: bf, |
|
83 |
+ name: name, |
|
84 |
+ flagType: flagType, |
|
85 |
+ } |
|
86 |
+ bf.flags[name] = newFlag |
|
87 |
+ |
|
88 |
+ return newFlag |
|
89 |
+} |
|
90 |
+ |
|
91 |
+// IsUsed checks if the flag is used |
|
92 |
+func (fl *Flag) IsUsed() bool { |
|
93 |
+ if _, ok := fl.bf.used[fl.name]; ok { |
|
94 |
+ return true |
|
95 |
+ } |
|
96 |
+ return false |
|
97 |
+} |
|
98 |
+ |
|
99 |
+// IsTrue checks if a bool flag is true |
|
100 |
+func (fl *Flag) IsTrue() bool { |
|
101 |
+ if fl.flagType != boolType { |
|
102 |
+ // Should never get here |
|
103 |
+ panic(fmt.Errorf("Trying to use IsTrue on a non-boolean: %s", fl.name)) |
|
104 |
+ } |
|
105 |
+ return fl.Value == "true" |
|
106 |
+} |
|
107 |
+ |
|
108 |
+// Parse parses and checks if the BFlags is valid. |
|
109 |
+// Any error noticed during the AddXXX() funcs will be generated/returned |
|
110 |
+// here. We do this because an error during AddXXX() is more like a |
|
111 |
+// compile time error so it doesn't matter too much when we stop our |
|
112 |
+// processing as long as we do stop it, so this allows the code |
|
113 |
+// around AddXXX() to be just: |
|
114 |
+// defFlag := AddString("description", "") |
|
115 |
+// w/o needing to add an if-statement around each one. |
|
116 |
+func (bf *BFlags) Parse() error { |
|
117 |
+ // If there was an error while defining the possible flags |
|
118 |
+ // go ahead and bubble it back up here since we didn't do it |
|
119 |
+ // earlier in the processing |
|
120 |
+ if bf.Err != nil { |
|
121 |
+ return fmt.Errorf("Error setting up flags: %s", bf.Err) |
|
122 |
+ } |
|
123 |
+ |
|
124 |
+ for _, arg := range bf.Args { |
|
125 |
+ if !strings.HasPrefix(arg, "--") { |
|
126 |
+ return fmt.Errorf("Arg should start with -- : %s", arg) |
|
127 |
+ } |
|
128 |
+ |
|
129 |
+ if arg == "--" { |
|
130 |
+ return nil |
|
131 |
+ } |
|
132 |
+ |
|
133 |
+ arg = arg[2:] |
|
134 |
+ value := "" |
|
135 |
+ |
|
136 |
+ index := strings.Index(arg, "=") |
|
137 |
+ if index >= 0 { |
|
138 |
+ value = arg[index+1:] |
|
139 |
+ arg = arg[:index] |
|
140 |
+ } |
|
141 |
+ |
|
142 |
+ flag, ok := bf.flags[arg] |
|
143 |
+ if !ok { |
|
144 |
+ return fmt.Errorf("Unknown flag: %s", arg) |
|
145 |
+ } |
|
146 |
+ |
|
147 |
+ if _, ok = bf.used[arg]; ok { |
|
148 |
+ return fmt.Errorf("Duplicate flag specified: %s", arg) |
|
149 |
+ } |
|
150 |
+ |
|
151 |
+ bf.used[arg] = flag |
|
152 |
+ |
|
153 |
+ switch flag.flagType { |
|
154 |
+ case boolType: |
|
155 |
+ // value == "" is only ok if no "=" was specified |
|
156 |
+ if index >= 0 && value == "" { |
|
157 |
+ return fmt.Errorf("Missing a value on flag: %s", arg) |
|
158 |
+ } |
|
159 |
+ |
|
160 |
+ lower := strings.ToLower(value) |
|
161 |
+ if lower == "" { |
|
162 |
+ flag.Value = "true" |
|
163 |
+ } else if lower == "true" || lower == "false" { |
|
164 |
+ flag.Value = lower |
|
165 |
+ } else { |
|
166 |
+ return fmt.Errorf("Expecting boolean value for flag %s, not: %s", arg, value) |
|
167 |
+ } |
|
168 |
+ |
|
169 |
+ case stringType: |
|
170 |
+ if index < 0 { |
|
171 |
+ return fmt.Errorf("Missing a value on flag: %s", arg) |
|
172 |
+ } |
|
173 |
+ flag.Value = value |
|
174 |
+ |
|
175 |
+ default: |
|
176 |
+ panic("No idea what kind of flag we have! Should never get here!") |
|
177 |
+ } |
|
178 |
+ |
|
179 |
+ } |
|
180 |
+ |
|
181 |
+ return nil |
|
182 |
+} |
0 | 183 |
new file mode 100644 |
... | ... |
@@ -0,0 +1,187 @@ |
0 |
+package instructions |
|
1 |
+ |
|
2 |
+import ( |
|
3 |
+ "testing" |
|
4 |
+) |
|
5 |
+ |
|
6 |
+func TestBuilderFlags(t *testing.T) { |
|
7 |
+ var expected string |
|
8 |
+ var err error |
|
9 |
+ |
|
10 |
+ // --- |
|
11 |
+ |
|
12 |
+ bf := NewBFlags() |
|
13 |
+ bf.Args = []string{} |
|
14 |
+ if err := bf.Parse(); err != nil { |
|
15 |
+ t.Fatalf("Test1 of %q was supposed to work: %s", bf.Args, err) |
|
16 |
+ } |
|
17 |
+ |
|
18 |
+ // --- |
|
19 |
+ |
|
20 |
+ bf = NewBFlags() |
|
21 |
+ bf.Args = []string{"--"} |
|
22 |
+ if err := bf.Parse(); err != nil { |
|
23 |
+ t.Fatalf("Test2 of %q was supposed to work: %s", bf.Args, err) |
|
24 |
+ } |
|
25 |
+ |
|
26 |
+ // --- |
|
27 |
+ |
|
28 |
+ bf = NewBFlags() |
|
29 |
+ flStr1 := bf.AddString("str1", "") |
|
30 |
+ flBool1 := bf.AddBool("bool1", false) |
|
31 |
+ bf.Args = []string{} |
|
32 |
+ if err = bf.Parse(); err != nil { |
|
33 |
+ t.Fatalf("Test3 of %q was supposed to work: %s", bf.Args, err) |
|
34 |
+ } |
|
35 |
+ |
|
36 |
+ if flStr1.IsUsed() { |
|
37 |
+ t.Fatal("Test3 - str1 was not used!") |
|
38 |
+ } |
|
39 |
+ if flBool1.IsUsed() { |
|
40 |
+ t.Fatal("Test3 - bool1 was not used!") |
|
41 |
+ } |
|
42 |
+ |
|
43 |
+ // --- |
|
44 |
+ |
|
45 |
+ bf = NewBFlags() |
|
46 |
+ flStr1 = bf.AddString("str1", "HI") |
|
47 |
+ flBool1 = bf.AddBool("bool1", false) |
|
48 |
+ bf.Args = []string{} |
|
49 |
+ |
|
50 |
+ if err = bf.Parse(); err != nil { |
|
51 |
+ t.Fatalf("Test4 of %q was supposed to work: %s", bf.Args, err) |
|
52 |
+ } |
|
53 |
+ |
|
54 |
+ if flStr1.Value != "HI" { |
|
55 |
+ t.Fatal("Str1 was supposed to default to: HI") |
|
56 |
+ } |
|
57 |
+ if flBool1.IsTrue() { |
|
58 |
+ t.Fatal("Bool1 was supposed to default to: false") |
|
59 |
+ } |
|
60 |
+ if flStr1.IsUsed() { |
|
61 |
+ t.Fatal("Str1 was not used!") |
|
62 |
+ } |
|
63 |
+ if flBool1.IsUsed() { |
|
64 |
+ t.Fatal("Bool1 was not used!") |
|
65 |
+ } |
|
66 |
+ |
|
67 |
+ // --- |
|
68 |
+ |
|
69 |
+ bf = NewBFlags() |
|
70 |
+ flStr1 = bf.AddString("str1", "HI") |
|
71 |
+ bf.Args = []string{"--str1"} |
|
72 |
+ |
|
73 |
+ if err = bf.Parse(); err == nil { |
|
74 |
+ t.Fatalf("Test %q was supposed to fail", bf.Args) |
|
75 |
+ } |
|
76 |
+ |
|
77 |
+ // --- |
|
78 |
+ |
|
79 |
+ bf = NewBFlags() |
|
80 |
+ flStr1 = bf.AddString("str1", "HI") |
|
81 |
+ bf.Args = []string{"--str1="} |
|
82 |
+ |
|
83 |
+ if err = bf.Parse(); err != nil { |
|
84 |
+ t.Fatalf("Test %q was supposed to work: %s", bf.Args, err) |
|
85 |
+ } |
|
86 |
+ |
|
87 |
+ expected = "" |
|
88 |
+ if flStr1.Value != expected { |
|
89 |
+ t.Fatalf("Str1 (%q) should be: %q", flStr1.Value, expected) |
|
90 |
+ } |
|
91 |
+ |
|
92 |
+ // --- |
|
93 |
+ |
|
94 |
+ bf = NewBFlags() |
|
95 |
+ flStr1 = bf.AddString("str1", "HI") |
|
96 |
+ bf.Args = []string{"--str1=BYE"} |
|
97 |
+ |
|
98 |
+ if err = bf.Parse(); err != nil { |
|
99 |
+ t.Fatalf("Test %q was supposed to work: %s", bf.Args, err) |
|
100 |
+ } |
|
101 |
+ |
|
102 |
+ expected = "BYE" |
|
103 |
+ if flStr1.Value != expected { |
|
104 |
+ t.Fatalf("Str1 (%q) should be: %q", flStr1.Value, expected) |
|
105 |
+ } |
|
106 |
+ |
|
107 |
+ // --- |
|
108 |
+ |
|
109 |
+ bf = NewBFlags() |
|
110 |
+ flBool1 = bf.AddBool("bool1", false) |
|
111 |
+ bf.Args = []string{"--bool1"} |
|
112 |
+ |
|
113 |
+ if err = bf.Parse(); err != nil { |
|
114 |
+ t.Fatalf("Test %q was supposed to work: %s", bf.Args, err) |
|
115 |
+ } |
|
116 |
+ |
|
117 |
+ if !flBool1.IsTrue() { |
|
118 |
+ t.Fatal("Test-b1 Bool1 was supposed to be true") |
|
119 |
+ } |
|
120 |
+ |
|
121 |
+ // --- |
|
122 |
+ |
|
123 |
+ bf = NewBFlags() |
|
124 |
+ flBool1 = bf.AddBool("bool1", false) |
|
125 |
+ bf.Args = []string{"--bool1=true"} |
|
126 |
+ |
|
127 |
+ if err = bf.Parse(); err != nil { |
|
128 |
+ t.Fatalf("Test %q was supposed to work: %s", bf.Args, err) |
|
129 |
+ } |
|
130 |
+ |
|
131 |
+ if !flBool1.IsTrue() { |
|
132 |
+ t.Fatal("Test-b2 Bool1 was supposed to be true") |
|
133 |
+ } |
|
134 |
+ |
|
135 |
+ // --- |
|
136 |
+ |
|
137 |
+ bf = NewBFlags() |
|
138 |
+ flBool1 = bf.AddBool("bool1", false) |
|
139 |
+ bf.Args = []string{"--bool1=false"} |
|
140 |
+ |
|
141 |
+ if err = bf.Parse(); err != nil { |
|
142 |
+ t.Fatalf("Test %q was supposed to work: %s", bf.Args, err) |
|
143 |
+ } |
|
144 |
+ |
|
145 |
+ if flBool1.IsTrue() { |
|
146 |
+ t.Fatal("Test-b3 Bool1 was supposed to be false") |
|
147 |
+ } |
|
148 |
+ |
|
149 |
+ // --- |
|
150 |
+ |
|
151 |
+ bf = NewBFlags() |
|
152 |
+ flBool1 = bf.AddBool("bool1", false) |
|
153 |
+ bf.Args = []string{"--bool1=false1"} |
|
154 |
+ |
|
155 |
+ if err = bf.Parse(); err == nil { |
|
156 |
+ t.Fatalf("Test %q was supposed to fail", bf.Args) |
|
157 |
+ } |
|
158 |
+ |
|
159 |
+ // --- |
|
160 |
+ |
|
161 |
+ bf = NewBFlags() |
|
162 |
+ flBool1 = bf.AddBool("bool1", false) |
|
163 |
+ bf.Args = []string{"--bool2"} |
|
164 |
+ |
|
165 |
+ if err = bf.Parse(); err == nil { |
|
166 |
+ t.Fatalf("Test %q was supposed to fail", bf.Args) |
|
167 |
+ } |
|
168 |
+ |
|
169 |
+ // --- |
|
170 |
+ |
|
171 |
+ bf = NewBFlags() |
|
172 |
+ flStr1 = bf.AddString("str1", "HI") |
|
173 |
+ flBool1 = bf.AddBool("bool1", false) |
|
174 |
+ bf.Args = []string{"--bool1", "--str1=BYE"} |
|
175 |
+ |
|
176 |
+ if err = bf.Parse(); err != nil { |
|
177 |
+ t.Fatalf("Test %q was supposed to work: %s", bf.Args, err) |
|
178 |
+ } |
|
179 |
+ |
|
180 |
+ if flStr1.Value != "BYE" { |
|
181 |
+ t.Fatalf("Test %s, str1 should be BYE", bf.Args) |
|
182 |
+ } |
|
183 |
+ if !flBool1.IsTrue() { |
|
184 |
+ t.Fatalf("Test %s, bool1 should be true", bf.Args) |
|
185 |
+ } |
|
186 |
+} |
0 | 187 |
new file mode 100644 |
... | ... |
@@ -0,0 +1,396 @@ |
0 |
+package instructions |
|
1 |
+ |
|
2 |
+import ( |
|
3 |
+ "errors" |
|
4 |
+ |
|
5 |
+ "strings" |
|
6 |
+ |
|
7 |
+ "github.com/docker/docker/api/types/container" |
|
8 |
+ "github.com/docker/docker/api/types/strslice" |
|
9 |
+) |
|
10 |
+ |
|
11 |
+// KeyValuePair represent an arbitrary named value (usefull in slice insted of map[string] string to preserve ordering) |
|
12 |
+type KeyValuePair struct { |
|
13 |
+ Key string |
|
14 |
+ Value string |
|
15 |
+} |
|
16 |
+ |
|
17 |
+func (kvp *KeyValuePair) String() string { |
|
18 |
+ return kvp.Key + "=" + kvp.Value |
|
19 |
+} |
|
20 |
+ |
|
21 |
+// Command is implemented by every command present in a dockerfile |
|
22 |
+type Command interface { |
|
23 |
+ Name() string |
|
24 |
+} |
|
25 |
+ |
|
26 |
+// KeyValuePairs is a slice of KeyValuePair |
|
27 |
+type KeyValuePairs []KeyValuePair |
|
28 |
+ |
|
29 |
+// withNameAndCode is the base of every command in a Dockerfile (String() returns its source code) |
|
30 |
+type withNameAndCode struct { |
|
31 |
+ code string |
|
32 |
+ name string |
|
33 |
+} |
|
34 |
+ |
|
35 |
+func (c *withNameAndCode) String() string { |
|
36 |
+ return c.code |
|
37 |
+} |
|
38 |
+ |
|
39 |
+// Name of the command |
|
40 |
+func (c *withNameAndCode) Name() string { |
|
41 |
+ return c.name |
|
42 |
+} |
|
43 |
+ |
|
44 |
+func newWithNameAndCode(req parseRequest) withNameAndCode { |
|
45 |
+ return withNameAndCode{code: strings.TrimSpace(req.original), name: req.command} |
|
46 |
+} |
|
47 |
+ |
|
48 |
+// SingleWordExpander is a provider for variable expansion where 1 word => 1 output |
|
49 |
+type SingleWordExpander func(word string) (string, error) |
|
50 |
+ |
|
51 |
+// SupportsSingleWordExpansion interface marks a command as supporting variable expansion |
|
52 |
+type SupportsSingleWordExpansion interface { |
|
53 |
+ Expand(expander SingleWordExpander) error |
|
54 |
+} |
|
55 |
+ |
|
56 |
+// PlatformSpecific adds platform checks to a command |
|
57 |
+type PlatformSpecific interface { |
|
58 |
+ CheckPlatform(platform string) error |
|
59 |
+} |
|
60 |
+ |
|
61 |
+func expandKvp(kvp KeyValuePair, expander SingleWordExpander) (KeyValuePair, error) { |
|
62 |
+ key, err := expander(kvp.Key) |
|
63 |
+ if err != nil { |
|
64 |
+ return KeyValuePair{}, err |
|
65 |
+ } |
|
66 |
+ value, err := expander(kvp.Value) |
|
67 |
+ if err != nil { |
|
68 |
+ return KeyValuePair{}, err |
|
69 |
+ } |
|
70 |
+ return KeyValuePair{Key: key, Value: value}, nil |
|
71 |
+} |
|
72 |
+func expandKvpsInPlace(kvps KeyValuePairs, expander SingleWordExpander) error { |
|
73 |
+ for i, kvp := range kvps { |
|
74 |
+ newKvp, err := expandKvp(kvp, expander) |
|
75 |
+ if err != nil { |
|
76 |
+ return err |
|
77 |
+ } |
|
78 |
+ kvps[i] = newKvp |
|
79 |
+ } |
|
80 |
+ return nil |
|
81 |
+} |
|
82 |
+ |
|
83 |
+func expandSliceInPlace(values []string, expander SingleWordExpander) error { |
|
84 |
+ for i, v := range values { |
|
85 |
+ newValue, err := expander(v) |
|
86 |
+ if err != nil { |
|
87 |
+ return err |
|
88 |
+ } |
|
89 |
+ values[i] = newValue |
|
90 |
+ } |
|
91 |
+ return nil |
|
92 |
+} |
|
93 |
+ |
|
94 |
+// EnvCommand : ENV key1 value1 [keyN valueN...] |
|
95 |
+type EnvCommand struct { |
|
96 |
+ withNameAndCode |
|
97 |
+ Env KeyValuePairs // kvp slice instead of map to preserve ordering |
|
98 |
+} |
|
99 |
+ |
|
100 |
+// Expand variables |
|
101 |
+func (c *EnvCommand) Expand(expander SingleWordExpander) error { |
|
102 |
+ return expandKvpsInPlace(c.Env, expander) |
|
103 |
+} |
|
104 |
+ |
|
105 |
+// MaintainerCommand : MAINTAINER maintainer_name |
|
106 |
+type MaintainerCommand struct { |
|
107 |
+ withNameAndCode |
|
108 |
+ Maintainer string |
|
109 |
+} |
|
110 |
+ |
|
111 |
+// LabelCommand : LABEL some json data describing the image |
|
112 |
+// |
|
113 |
+// Sets the Label variable foo to bar, |
|
114 |
+// |
|
115 |
+type LabelCommand struct { |
|
116 |
+ withNameAndCode |
|
117 |
+ Labels KeyValuePairs // kvp slice instead of map to preserve ordering |
|
118 |
+} |
|
119 |
+ |
|
120 |
+// Expand variables |
|
121 |
+func (c *LabelCommand) Expand(expander SingleWordExpander) error { |
|
122 |
+ return expandKvpsInPlace(c.Labels, expander) |
|
123 |
+} |
|
124 |
+ |
|
125 |
+// SourcesAndDest represent a list of source files and a destination |
|
126 |
+type SourcesAndDest []string |
|
127 |
+ |
|
128 |
+// Sources list the source paths |
|
129 |
+func (s SourcesAndDest) Sources() []string { |
|
130 |
+ res := make([]string, len(s)-1) |
|
131 |
+ copy(res, s[:len(s)-1]) |
|
132 |
+ return res |
|
133 |
+} |
|
134 |
+ |
|
135 |
+// Dest path of the operation |
|
136 |
+func (s SourcesAndDest) Dest() string { |
|
137 |
+ return s[len(s)-1] |
|
138 |
+} |
|
139 |
+ |
|
140 |
+// AddCommand : ADD foo /path |
|
141 |
+// |
|
142 |
+// Add the file 'foo' to '/path'. Tarball and Remote URL (git, http) handling |
|
143 |
+// exist here. If you do not wish to have this automatic handling, use COPY. |
|
144 |
+// |
|
145 |
+type AddCommand struct { |
|
146 |
+ withNameAndCode |
|
147 |
+ SourcesAndDest |
|
148 |
+ Chown string |
|
149 |
+} |
|
150 |
+ |
|
151 |
+// Expand variables |
|
152 |
+func (c *AddCommand) Expand(expander SingleWordExpander) error { |
|
153 |
+ return expandSliceInPlace(c.SourcesAndDest, expander) |
|
154 |
+} |
|
155 |
+ |
|
156 |
+// CopyCommand : COPY foo /path |
|
157 |
+// |
|
158 |
+// Same as 'ADD' but without the tar and remote url handling. |
|
159 |
+// |
|
160 |
+type CopyCommand struct { |
|
161 |
+ withNameAndCode |
|
162 |
+ SourcesAndDest |
|
163 |
+ From string |
|
164 |
+ Chown string |
|
165 |
+} |
|
166 |
+ |
|
167 |
+// Expand variables |
|
168 |
+func (c *CopyCommand) Expand(expander SingleWordExpander) error { |
|
169 |
+ return expandSliceInPlace(c.SourcesAndDest, expander) |
|
170 |
+} |
|
171 |
+ |
|
172 |
+// OnbuildCommand : ONBUILD <some other command> |
|
173 |
+type OnbuildCommand struct { |
|
174 |
+ withNameAndCode |
|
175 |
+ Expression string |
|
176 |
+} |
|
177 |
+ |
|
178 |
+// WorkdirCommand : WORKDIR /tmp |
|
179 |
+// |
|
180 |
+// Set the working directory for future RUN/CMD/etc statements. |
|
181 |
+// |
|
182 |
+type WorkdirCommand struct { |
|
183 |
+ withNameAndCode |
|
184 |
+ Path string |
|
185 |
+} |
|
186 |
+ |
|
187 |
+// Expand variables |
|
188 |
+func (c *WorkdirCommand) Expand(expander SingleWordExpander) error { |
|
189 |
+ p, err := expander(c.Path) |
|
190 |
+ if err != nil { |
|
191 |
+ return err |
|
192 |
+ } |
|
193 |
+ c.Path = p |
|
194 |
+ return nil |
|
195 |
+} |
|
196 |
+ |
|
197 |
+// ShellDependantCmdLine represents a cmdline optionaly prepended with the shell |
|
198 |
+type ShellDependantCmdLine struct { |
|
199 |
+ CmdLine strslice.StrSlice |
|
200 |
+ PrependShell bool |
|
201 |
+} |
|
202 |
+ |
|
203 |
+// RunCommand : RUN some command yo |
|
204 |
+// |
|
205 |
+// run a command and commit the image. Args are automatically prepended with |
|
206 |
+// the current SHELL which defaults to 'sh -c' under linux or 'cmd /S /C' under |
|
207 |
+// Windows, in the event there is only one argument The difference in processing: |
|
208 |
+// |
|
209 |
+// RUN echo hi # sh -c echo hi (Linux) |
|
210 |
+// RUN echo hi # cmd /S /C echo hi (Windows) |
|
211 |
+// RUN [ "echo", "hi" ] # echo hi |
|
212 |
+// |
|
213 |
+type RunCommand struct { |
|
214 |
+ withNameAndCode |
|
215 |
+ ShellDependantCmdLine |
|
216 |
+} |
|
217 |
+ |
|
218 |
+// CmdCommand : CMD foo |
|
219 |
+// |
|
220 |
+// Set the default command to run in the container (which may be empty). |
|
221 |
+// Argument handling is the same as RUN. |
|
222 |
+// |
|
223 |
+type CmdCommand struct { |
|
224 |
+ withNameAndCode |
|
225 |
+ ShellDependantCmdLine |
|
226 |
+} |
|
227 |
+ |
|
228 |
+// HealthCheckCommand : HEALTHCHECK foo |
|
229 |
+// |
|
230 |
+// Set the default healthcheck command to run in the container (which may be empty). |
|
231 |
+// Argument handling is the same as RUN. |
|
232 |
+// |
|
233 |
+type HealthCheckCommand struct { |
|
234 |
+ withNameAndCode |
|
235 |
+ Health *container.HealthConfig |
|
236 |
+} |
|
237 |
+ |
|
238 |
+// EntrypointCommand : ENTRYPOINT /usr/sbin/nginx |
|
239 |
+// |
|
240 |
+// Set the entrypoint to /usr/sbin/nginx. Will accept the CMD as the arguments |
|
241 |
+// to /usr/sbin/nginx. Uses the default shell if not in JSON format. |
|
242 |
+// |
|
243 |
+// Handles command processing similar to CMD and RUN, only req.runConfig.Entrypoint |
|
244 |
+// is initialized at newBuilder time instead of through argument parsing. |
|
245 |
+// |
|
246 |
+type EntrypointCommand struct { |
|
247 |
+ withNameAndCode |
|
248 |
+ ShellDependantCmdLine |
|
249 |
+} |
|
250 |
+ |
|
251 |
+// ExposeCommand : EXPOSE 6667/tcp 7000/tcp |
|
252 |
+// |
|
253 |
+// Expose ports for links and port mappings. This all ends up in |
|
254 |
+// req.runConfig.ExposedPorts for runconfig. |
|
255 |
+// |
|
256 |
+type ExposeCommand struct { |
|
257 |
+ withNameAndCode |
|
258 |
+ Ports []string |
|
259 |
+} |
|
260 |
+ |
|
261 |
+// UserCommand : USER foo |
|
262 |
+// |
|
263 |
+// Set the user to 'foo' for future commands and when running the |
|
264 |
+// ENTRYPOINT/CMD at container run time. |
|
265 |
+// |
|
266 |
+type UserCommand struct { |
|
267 |
+ withNameAndCode |
|
268 |
+ User string |
|
269 |
+} |
|
270 |
+ |
|
271 |
+// Expand variables |
|
272 |
+func (c *UserCommand) Expand(expander SingleWordExpander) error { |
|
273 |
+ p, err := expander(c.User) |
|
274 |
+ if err != nil { |
|
275 |
+ return err |
|
276 |
+ } |
|
277 |
+ c.User = p |
|
278 |
+ return nil |
|
279 |
+} |
|
280 |
+ |
|
281 |
+// VolumeCommand : VOLUME /foo |
|
282 |
+// |
|
283 |
+// Expose the volume /foo for use. Will also accept the JSON array form. |
|
284 |
+// |
|
285 |
+type VolumeCommand struct { |
|
286 |
+ withNameAndCode |
|
287 |
+ Volumes []string |
|
288 |
+} |
|
289 |
+ |
|
290 |
+// Expand variables |
|
291 |
+func (c *VolumeCommand) Expand(expander SingleWordExpander) error { |
|
292 |
+ return expandSliceInPlace(c.Volumes, expander) |
|
293 |
+} |
|
294 |
+ |
|
295 |
+// StopSignalCommand : STOPSIGNAL signal |
|
296 |
+// |
|
297 |
+// Set the signal that will be used to kill the container. |
|
298 |
+type StopSignalCommand struct { |
|
299 |
+ withNameAndCode |
|
300 |
+ Signal string |
|
301 |
+} |
|
302 |
+ |
|
303 |
+// Expand variables |
|
304 |
+func (c *StopSignalCommand) Expand(expander SingleWordExpander) error { |
|
305 |
+ p, err := expander(c.Signal) |
|
306 |
+ if err != nil { |
|
307 |
+ return err |
|
308 |
+ } |
|
309 |
+ c.Signal = p |
|
310 |
+ return nil |
|
311 |
+} |
|
312 |
+ |
|
313 |
+// CheckPlatform checks that the command is supported in the target platform |
|
314 |
+func (c *StopSignalCommand) CheckPlatform(platform string) error { |
|
315 |
+ if platform == "windows" { |
|
316 |
+ return errors.New("The daemon on this platform does not support the command stopsignal") |
|
317 |
+ } |
|
318 |
+ return nil |
|
319 |
+} |
|
320 |
+ |
|
321 |
+// ArgCommand : ARG name[=value] |
|
322 |
+// |
|
323 |
+// Adds the variable foo to the trusted list of variables that can be passed |
|
324 |
+// to builder using the --build-arg flag for expansion/substitution or passing to 'run'. |
|
325 |
+// Dockerfile author may optionally set a default value of this variable. |
|
326 |
+type ArgCommand struct { |
|
327 |
+ withNameAndCode |
|
328 |
+ Key string |
|
329 |
+ Value *string |
|
330 |
+} |
|
331 |
+ |
|
332 |
+// Expand variables |
|
333 |
+func (c *ArgCommand) Expand(expander SingleWordExpander) error { |
|
334 |
+ p, err := expander(c.Key) |
|
335 |
+ if err != nil { |
|
336 |
+ return err |
|
337 |
+ } |
|
338 |
+ c.Key = p |
|
339 |
+ if c.Value != nil { |
|
340 |
+ p, err = expander(*c.Value) |
|
341 |
+ if err != nil { |
|
342 |
+ return err |
|
343 |
+ } |
|
344 |
+ c.Value = &p |
|
345 |
+ } |
|
346 |
+ return nil |
|
347 |
+} |
|
348 |
+ |
|
349 |
+// ShellCommand : SHELL powershell -command |
|
350 |
+// |
|
351 |
+// Set the non-default shell to use. |
|
352 |
+type ShellCommand struct { |
|
353 |
+ withNameAndCode |
|
354 |
+ Shell strslice.StrSlice |
|
355 |
+} |
|
356 |
+ |
|
357 |
+// Stage represents a single stage in a multi-stage build |
|
358 |
+type Stage struct { |
|
359 |
+ Name string |
|
360 |
+ Commands []Command |
|
361 |
+ BaseName string |
|
362 |
+ SourceCode string |
|
363 |
+} |
|
364 |
+ |
|
365 |
+// AddCommand to the stage |
|
366 |
+func (s *Stage) AddCommand(cmd Command) { |
|
367 |
+ // todo: validate cmd type |
|
368 |
+ s.Commands = append(s.Commands, cmd) |
|
369 |
+} |
|
370 |
+ |
|
371 |
+// IsCurrentStage check if the stage name is the current stage |
|
372 |
+func IsCurrentStage(s []Stage, name string) bool { |
|
373 |
+ if len(s) == 0 { |
|
374 |
+ return false |
|
375 |
+ } |
|
376 |
+ return s[len(s)-1].Name == name |
|
377 |
+} |
|
378 |
+ |
|
379 |
+// CurrentStage return the last stage in a slice |
|
380 |
+func CurrentStage(s []Stage) (*Stage, error) { |
|
381 |
+ if len(s) == 0 { |
|
382 |
+ return nil, errors.New("No build stage in current context") |
|
383 |
+ } |
|
384 |
+ return &s[len(s)-1], nil |
|
385 |
+} |
|
386 |
+ |
|
387 |
+// HasStage looks for the presence of a given stage name |
|
388 |
+func HasStage(s []Stage, name string) (int, bool) { |
|
389 |
+ for i, stage := range s { |
|
390 |
+ if stage.Name == name { |
|
391 |
+ return i, true |
|
392 |
+ } |
|
393 |
+ } |
|
394 |
+ return -1, false |
|
395 |
+} |
0 | 9 |
new file mode 100644 |
... | ... |
@@ -0,0 +1,27 @@ |
0 |
+package instructions |
|
1 |
+ |
|
2 |
+import ( |
|
3 |
+ "fmt" |
|
4 |
+ "path/filepath" |
|
5 |
+ "regexp" |
|
6 |
+ "strings" |
|
7 |
+) |
|
8 |
+ |
|
9 |
+func errNotJSON(command, original string) error { |
|
10 |
+ // For Windows users, give a hint if it looks like it might contain |
|
11 |
+ // a path which hasn't been escaped such as ["c:\windows\system32\prog.exe", "-param"], |
|
12 |
+ // as JSON must be escaped. Unfortunate... |
|
13 |
+ // |
|
14 |
+ // Specifically looking for quote-driveletter-colon-backslash, there's no |
|
15 |
+ // double backslash and a [] pair. No, this is not perfect, but it doesn't |
|
16 |
+ // have to be. It's simply a hint to make life a little easier. |
|
17 |
+ extra := "" |
|
18 |
+ original = filepath.FromSlash(strings.ToLower(strings.Replace(strings.ToLower(original), strings.ToLower(command)+" ", "", -1))) |
|
19 |
+ if len(regexp.MustCompile(`"[a-z]:\\.*`).FindStringSubmatch(original)) > 0 && |
|
20 |
+ !strings.Contains(original, `\\`) && |
|
21 |
+ strings.Contains(original, "[") && |
|
22 |
+ strings.Contains(original, "]") { |
|
23 |
+ extra = fmt.Sprintf(`. It looks like '%s' includes a file path without an escaped back-slash. JSON requires back-slashes to be escaped such as ["c:\\path\\to\\file.exe", "/parameter"]`, original) |
|
24 |
+ } |
|
25 |
+ return fmt.Errorf("%s requires the arguments to be in JSON form%s", command, extra) |
|
26 |
+} |
0 | 27 |
new file mode 100644 |
... | ... |
@@ -0,0 +1,635 @@ |
0 |
+package instructions |
|
1 |
+ |
|
2 |
+import ( |
|
3 |
+ "fmt" |
|
4 |
+ "regexp" |
|
5 |
+ "sort" |
|
6 |
+ "strconv" |
|
7 |
+ "strings" |
|
8 |
+ "time" |
|
9 |
+ |
|
10 |
+ "github.com/docker/docker/api/types/container" |
|
11 |
+ "github.com/docker/docker/api/types/strslice" |
|
12 |
+ "github.com/docker/docker/builder/dockerfile/command" |
|
13 |
+ "github.com/docker/docker/builder/dockerfile/parser" |
|
14 |
+ "github.com/pkg/errors" |
|
15 |
+) |
|
16 |
+ |
|
17 |
+type parseRequest struct { |
|
18 |
+ command string |
|
19 |
+ args []string |
|
20 |
+ attributes map[string]bool |
|
21 |
+ flags *BFlags |
|
22 |
+ original string |
|
23 |
+} |
|
24 |
+ |
|
25 |
+func nodeArgs(node *parser.Node) []string { |
|
26 |
+ result := []string{} |
|
27 |
+ for ; node.Next != nil; node = node.Next { |
|
28 |
+ arg := node.Next |
|
29 |
+ if len(arg.Children) == 0 { |
|
30 |
+ result = append(result, arg.Value) |
|
31 |
+ } else if len(arg.Children) == 1 { |
|
32 |
+ //sub command |
|
33 |
+ result = append(result, arg.Children[0].Value) |
|
34 |
+ result = append(result, nodeArgs(arg.Children[0])...) |
|
35 |
+ } |
|
36 |
+ } |
|
37 |
+ return result |
|
38 |
+} |
|
39 |
+ |
|
40 |
+func newParseRequestFromNode(node *parser.Node) parseRequest { |
|
41 |
+ return parseRequest{ |
|
42 |
+ command: node.Value, |
|
43 |
+ args: nodeArgs(node), |
|
44 |
+ attributes: node.Attributes, |
|
45 |
+ original: node.Original, |
|
46 |
+ flags: NewBFlagsWithArgs(node.Flags), |
|
47 |
+ } |
|
48 |
+} |
|
49 |
+ |
|
50 |
+// ParseInstruction converts an AST to a typed instruction (either a command or a build stage beginning when encountering a `FROM` statement) |
|
51 |
+func ParseInstruction(node *parser.Node) (interface{}, error) { |
|
52 |
+ req := newParseRequestFromNode(node) |
|
53 |
+ switch node.Value { |
|
54 |
+ case command.Env: |
|
55 |
+ return parseEnv(req) |
|
56 |
+ case command.Maintainer: |
|
57 |
+ return parseMaintainer(req) |
|
58 |
+ case command.Label: |
|
59 |
+ return parseLabel(req) |
|
60 |
+ case command.Add: |
|
61 |
+ return parseAdd(req) |
|
62 |
+ case command.Copy: |
|
63 |
+ return parseCopy(req) |
|
64 |
+ case command.From: |
|
65 |
+ return parseFrom(req) |
|
66 |
+ case command.Onbuild: |
|
67 |
+ return parseOnBuild(req) |
|
68 |
+ case command.Workdir: |
|
69 |
+ return parseWorkdir(req) |
|
70 |
+ case command.Run: |
|
71 |
+ return parseRun(req) |
|
72 |
+ case command.Cmd: |
|
73 |
+ return parseCmd(req) |
|
74 |
+ case command.Healthcheck: |
|
75 |
+ return parseHealthcheck(req) |
|
76 |
+ case command.Entrypoint: |
|
77 |
+ return parseEntrypoint(req) |
|
78 |
+ case command.Expose: |
|
79 |
+ return parseExpose(req) |
|
80 |
+ case command.User: |
|
81 |
+ return parseUser(req) |
|
82 |
+ case command.Volume: |
|
83 |
+ return parseVolume(req) |
|
84 |
+ case command.StopSignal: |
|
85 |
+ return parseStopSignal(req) |
|
86 |
+ case command.Arg: |
|
87 |
+ return parseArg(req) |
|
88 |
+ case command.Shell: |
|
89 |
+ return parseShell(req) |
|
90 |
+ } |
|
91 |
+ |
|
92 |
+ return nil, &UnknownInstruction{Instruction: node.Value, Line: node.StartLine} |
|
93 |
+} |
|
94 |
+ |
|
95 |
+// ParseCommand converts an AST to a typed Command |
|
96 |
+func ParseCommand(node *parser.Node) (Command, error) { |
|
97 |
+ s, err := ParseInstruction(node) |
|
98 |
+ if err != nil { |
|
99 |
+ return nil, err |
|
100 |
+ } |
|
101 |
+ if c, ok := s.(Command); ok { |
|
102 |
+ return c, nil |
|
103 |
+ } |
|
104 |
+ return nil, errors.Errorf("%T is not a command type", s) |
|
105 |
+} |
|
106 |
+ |
|
107 |
+// UnknownInstruction represents an error occuring when a command is unresolvable |
|
108 |
+type UnknownInstruction struct { |
|
109 |
+ Line int |
|
110 |
+ Instruction string |
|
111 |
+} |
|
112 |
+ |
|
113 |
+func (e *UnknownInstruction) Error() string { |
|
114 |
+ return fmt.Sprintf("unknown instruction: %s", strings.ToUpper(e.Instruction)) |
|
115 |
+} |
|
116 |
+ |
|
117 |
+// IsUnknownInstruction checks if the error is an UnknownInstruction or a parseError containing an UnknownInstruction |
|
118 |
+func IsUnknownInstruction(err error) bool { |
|
119 |
+ _, ok := err.(*UnknownInstruction) |
|
120 |
+ if !ok { |
|
121 |
+ var pe *parseError |
|
122 |
+ if pe, ok = err.(*parseError); ok { |
|
123 |
+ _, ok = pe.inner.(*UnknownInstruction) |
|
124 |
+ } |
|
125 |
+ } |
|
126 |
+ return ok |
|
127 |
+} |
|
128 |
+ |
|
129 |
+type parseError struct { |
|
130 |
+ inner error |
|
131 |
+ node *parser.Node |
|
132 |
+} |
|
133 |
+ |
|
134 |
+func (e *parseError) Error() string { |
|
135 |
+ return fmt.Sprintf("Dockerfile parse error line %d: %v", e.node.StartLine, e.inner.Error()) |
|
136 |
+} |
|
137 |
+ |
|
138 |
+// Parse a docker file into a collection of buildable stages |
|
139 |
+func Parse(ast *parser.Node) (stages []Stage, metaArgs []ArgCommand, err error) { |
|
140 |
+ for _, n := range ast.Children { |
|
141 |
+ cmd, err := ParseInstruction(n) |
|
142 |
+ if err != nil { |
|
143 |
+ return nil, nil, &parseError{inner: err, node: n} |
|
144 |
+ } |
|
145 |
+ if len(stages) == 0 { |
|
146 |
+ // meta arg case |
|
147 |
+ if a, isArg := cmd.(*ArgCommand); isArg { |
|
148 |
+ metaArgs = append(metaArgs, *a) |
|
149 |
+ continue |
|
150 |
+ } |
|
151 |
+ } |
|
152 |
+ switch c := cmd.(type) { |
|
153 |
+ case *Stage: |
|
154 |
+ stages = append(stages, *c) |
|
155 |
+ case Command: |
|
156 |
+ stage, err := CurrentStage(stages) |
|
157 |
+ if err != nil { |
|
158 |
+ return nil, nil, err |
|
159 |
+ } |
|
160 |
+ stage.AddCommand(c) |
|
161 |
+ default: |
|
162 |
+ return nil, nil, errors.Errorf("%T is not a command type", cmd) |
|
163 |
+ } |
|
164 |
+ |
|
165 |
+ } |
|
166 |
+ return stages, metaArgs, nil |
|
167 |
+} |
|
168 |
+ |
|
169 |
+func parseKvps(args []string, cmdName string) (KeyValuePairs, error) { |
|
170 |
+ if len(args) == 0 { |
|
171 |
+ return nil, errAtLeastOneArgument(cmdName) |
|
172 |
+ } |
|
173 |
+ if len(args)%2 != 0 { |
|
174 |
+ // should never get here, but just in case |
|
175 |
+ return nil, errTooManyArguments(cmdName) |
|
176 |
+ } |
|
177 |
+ var res KeyValuePairs |
|
178 |
+ for j := 0; j < len(args); j += 2 { |
|
179 |
+ if len(args[j]) == 0 { |
|
180 |
+ return nil, errBlankCommandNames(cmdName) |
|
181 |
+ } |
|
182 |
+ name := args[j] |
|
183 |
+ value := args[j+1] |
|
184 |
+ res = append(res, KeyValuePair{Key: name, Value: value}) |
|
185 |
+ } |
|
186 |
+ return res, nil |
|
187 |
+} |
|
188 |
+ |
|
189 |
+func parseEnv(req parseRequest) (*EnvCommand, error) { |
|
190 |
+ |
|
191 |
+ if err := req.flags.Parse(); err != nil { |
|
192 |
+ return nil, err |
|
193 |
+ } |
|
194 |
+ envs, err := parseKvps(req.args, "ENV") |
|
195 |
+ if err != nil { |
|
196 |
+ return nil, err |
|
197 |
+ } |
|
198 |
+ return &EnvCommand{ |
|
199 |
+ Env: envs, |
|
200 |
+ withNameAndCode: newWithNameAndCode(req), |
|
201 |
+ }, nil |
|
202 |
+} |
|
203 |
+ |
|
204 |
+func parseMaintainer(req parseRequest) (*MaintainerCommand, error) { |
|
205 |
+ if len(req.args) != 1 { |
|
206 |
+ return nil, errExactlyOneArgument("MAINTAINER") |
|
207 |
+ } |
|
208 |
+ |
|
209 |
+ if err := req.flags.Parse(); err != nil { |
|
210 |
+ return nil, err |
|
211 |
+ } |
|
212 |
+ return &MaintainerCommand{ |
|
213 |
+ Maintainer: req.args[0], |
|
214 |
+ withNameAndCode: newWithNameAndCode(req), |
|
215 |
+ }, nil |
|
216 |
+} |
|
217 |
+ |
|
218 |
+func parseLabel(req parseRequest) (*LabelCommand, error) { |
|
219 |
+ |
|
220 |
+ if err := req.flags.Parse(); err != nil { |
|
221 |
+ return nil, err |
|
222 |
+ } |
|
223 |
+ |
|
224 |
+ labels, err := parseKvps(req.args, "LABEL") |
|
225 |
+ if err != nil { |
|
226 |
+ return nil, err |
|
227 |
+ } |
|
228 |
+ |
|
229 |
+ return &LabelCommand{ |
|
230 |
+ Labels: labels, |
|
231 |
+ withNameAndCode: newWithNameAndCode(req), |
|
232 |
+ }, nil |
|
233 |
+} |
|
234 |
+ |
|
235 |
+func parseAdd(req parseRequest) (*AddCommand, error) { |
|
236 |
+ if len(req.args) < 2 { |
|
237 |
+ return nil, errAtLeastTwoArguments("ADD") |
|
238 |
+ } |
|
239 |
+ flChown := req.flags.AddString("chown", "") |
|
240 |
+ if err := req.flags.Parse(); err != nil { |
|
241 |
+ return nil, err |
|
242 |
+ } |
|
243 |
+ return &AddCommand{ |
|
244 |
+ SourcesAndDest: SourcesAndDest(req.args), |
|
245 |
+ withNameAndCode: newWithNameAndCode(req), |
|
246 |
+ Chown: flChown.Value, |
|
247 |
+ }, nil |
|
248 |
+} |
|
249 |
+ |
|
250 |
+func parseCopy(req parseRequest) (*CopyCommand, error) { |
|
251 |
+ if len(req.args) < 2 { |
|
252 |
+ return nil, errAtLeastTwoArguments("COPY") |
|
253 |
+ } |
|
254 |
+ flChown := req.flags.AddString("chown", "") |
|
255 |
+ flFrom := req.flags.AddString("from", "") |
|
256 |
+ if err := req.flags.Parse(); err != nil { |
|
257 |
+ return nil, err |
|
258 |
+ } |
|
259 |
+ return &CopyCommand{ |
|
260 |
+ SourcesAndDest: SourcesAndDest(req.args), |
|
261 |
+ From: flFrom.Value, |
|
262 |
+ withNameAndCode: newWithNameAndCode(req), |
|
263 |
+ Chown: flChown.Value, |
|
264 |
+ }, nil |
|
265 |
+} |
|
266 |
+ |
|
267 |
+func parseFrom(req parseRequest) (*Stage, error) { |
|
268 |
+ stageName, err := parseBuildStageName(req.args) |
|
269 |
+ if err != nil { |
|
270 |
+ return nil, err |
|
271 |
+ } |
|
272 |
+ |
|
273 |
+ if err := req.flags.Parse(); err != nil { |
|
274 |
+ return nil, err |
|
275 |
+ } |
|
276 |
+ code := strings.TrimSpace(req.original) |
|
277 |
+ |
|
278 |
+ return &Stage{ |
|
279 |
+ BaseName: req.args[0], |
|
280 |
+ Name: stageName, |
|
281 |
+ SourceCode: code, |
|
282 |
+ Commands: []Command{}, |
|
283 |
+ }, nil |
|
284 |
+ |
|
285 |
+} |
|
286 |
+ |
|
287 |
+func parseBuildStageName(args []string) (string, error) { |
|
288 |
+ stageName := "" |
|
289 |
+ switch { |
|
290 |
+ case len(args) == 3 && strings.EqualFold(args[1], "as"): |
|
291 |
+ stageName = strings.ToLower(args[2]) |
|
292 |
+ if ok, _ := regexp.MatchString("^[a-z][a-z0-9-_\\.]*$", stageName); !ok { |
|
293 |
+ return "", errors.Errorf("invalid name for build stage: %q, name can't start with a number or contain symbols", stageName) |
|
294 |
+ } |
|
295 |
+ case len(args) != 1: |
|
296 |
+ return "", errors.New("FROM requires either one or three arguments") |
|
297 |
+ } |
|
298 |
+ |
|
299 |
+ return stageName, nil |
|
300 |
+} |
|
301 |
+ |
|
302 |
+func parseOnBuild(req parseRequest) (*OnbuildCommand, error) { |
|
303 |
+ if len(req.args) == 0 { |
|
304 |
+ return nil, errAtLeastOneArgument("ONBUILD") |
|
305 |
+ } |
|
306 |
+ if err := req.flags.Parse(); err != nil { |
|
307 |
+ return nil, err |
|
308 |
+ } |
|
309 |
+ |
|
310 |
+ triggerInstruction := strings.ToUpper(strings.TrimSpace(req.args[0])) |
|
311 |
+ switch strings.ToUpper(triggerInstruction) { |
|
312 |
+ case "ONBUILD": |
|
313 |
+ return nil, errors.New("Chaining ONBUILD via `ONBUILD ONBUILD` isn't allowed") |
|
314 |
+ case "MAINTAINER", "FROM": |
|
315 |
+ return nil, fmt.Errorf("%s isn't allowed as an ONBUILD trigger", triggerInstruction) |
|
316 |
+ } |
|
317 |
+ |
|
318 |
+ original := regexp.MustCompile(`(?i)^\s*ONBUILD\s*`).ReplaceAllString(req.original, "") |
|
319 |
+ return &OnbuildCommand{ |
|
320 |
+ Expression: original, |
|
321 |
+ withNameAndCode: newWithNameAndCode(req), |
|
322 |
+ }, nil |
|
323 |
+ |
|
324 |
+} |
|
325 |
+ |
|
326 |
+func parseWorkdir(req parseRequest) (*WorkdirCommand, error) { |
|
327 |
+ if len(req.args) != 1 { |
|
328 |
+ return nil, errExactlyOneArgument("WORKDIR") |
|
329 |
+ } |
|
330 |
+ |
|
331 |
+ err := req.flags.Parse() |
|
332 |
+ if err != nil { |
|
333 |
+ return nil, err |
|
334 |
+ } |
|
335 |
+ return &WorkdirCommand{ |
|
336 |
+ Path: req.args[0], |
|
337 |
+ withNameAndCode: newWithNameAndCode(req), |
|
338 |
+ }, nil |
|
339 |
+ |
|
340 |
+} |
|
341 |
+ |
|
342 |
+func parseShellDependentCommand(req parseRequest, emptyAsNil bool) ShellDependantCmdLine { |
|
343 |
+ args := handleJSONArgs(req.args, req.attributes) |
|
344 |
+ cmd := strslice.StrSlice(args) |
|
345 |
+ if emptyAsNil && len(cmd) == 0 { |
|
346 |
+ cmd = nil |
|
347 |
+ } |
|
348 |
+ return ShellDependantCmdLine{ |
|
349 |
+ CmdLine: cmd, |
|
350 |
+ PrependShell: !req.attributes["json"], |
|
351 |
+ } |
|
352 |
+} |
|
353 |
+ |
|
354 |
+func parseRun(req parseRequest) (*RunCommand, error) { |
|
355 |
+ |
|
356 |
+ if err := req.flags.Parse(); err != nil { |
|
357 |
+ return nil, err |
|
358 |
+ } |
|
359 |
+ return &RunCommand{ |
|
360 |
+ ShellDependantCmdLine: parseShellDependentCommand(req, false), |
|
361 |
+ withNameAndCode: newWithNameAndCode(req), |
|
362 |
+ }, nil |
|
363 |
+ |
|
364 |
+} |
|
365 |
+ |
|
366 |
+func parseCmd(req parseRequest) (*CmdCommand, error) { |
|
367 |
+ if err := req.flags.Parse(); err != nil { |
|
368 |
+ return nil, err |
|
369 |
+ } |
|
370 |
+ return &CmdCommand{ |
|
371 |
+ ShellDependantCmdLine: parseShellDependentCommand(req, false), |
|
372 |
+ withNameAndCode: newWithNameAndCode(req), |
|
373 |
+ }, nil |
|
374 |
+ |
|
375 |
+} |
|
376 |
+ |
|
377 |
+func parseEntrypoint(req parseRequest) (*EntrypointCommand, error) { |
|
378 |
+ if err := req.flags.Parse(); err != nil { |
|
379 |
+ return nil, err |
|
380 |
+ } |
|
381 |
+ |
|
382 |
+ cmd := &EntrypointCommand{ |
|
383 |
+ ShellDependantCmdLine: parseShellDependentCommand(req, true), |
|
384 |
+ withNameAndCode: newWithNameAndCode(req), |
|
385 |
+ } |
|
386 |
+ |
|
387 |
+ return cmd, nil |
|
388 |
+} |
|
389 |
+ |
|
390 |
+// parseOptInterval(flag) is the duration of flag.Value, or 0 if |
|
391 |
+// empty. An error is reported if the value is given and less than minimum duration. |
|
392 |
+func parseOptInterval(f *Flag) (time.Duration, error) { |
|
393 |
+ s := f.Value |
|
394 |
+ if s == "" { |
|
395 |
+ return 0, nil |
|
396 |
+ } |
|
397 |
+ d, err := time.ParseDuration(s) |
|
398 |
+ if err != nil { |
|
399 |
+ return 0, err |
|
400 |
+ } |
|
401 |
+ if d < container.MinimumDuration { |
|
402 |
+ return 0, fmt.Errorf("Interval %#v cannot be less than %s", f.name, container.MinimumDuration) |
|
403 |
+ } |
|
404 |
+ return d, nil |
|
405 |
+} |
|
406 |
+func parseHealthcheck(req parseRequest) (*HealthCheckCommand, error) { |
|
407 |
+ if len(req.args) == 0 { |
|
408 |
+ return nil, errAtLeastOneArgument("HEALTHCHECK") |
|
409 |
+ } |
|
410 |
+ cmd := &HealthCheckCommand{ |
|
411 |
+ withNameAndCode: newWithNameAndCode(req), |
|
412 |
+ } |
|
413 |
+ |
|
414 |
+ typ := strings.ToUpper(req.args[0]) |
|
415 |
+ args := req.args[1:] |
|
416 |
+ if typ == "NONE" { |
|
417 |
+ if len(args) != 0 { |
|
418 |
+ return nil, errors.New("HEALTHCHECK NONE takes no arguments") |
|
419 |
+ } |
|
420 |
+ test := strslice.StrSlice{typ} |
|
421 |
+ cmd.Health = &container.HealthConfig{ |
|
422 |
+ Test: test, |
|
423 |
+ } |
|
424 |
+ } else { |
|
425 |
+ |
|
426 |
+ healthcheck := container.HealthConfig{} |
|
427 |
+ |
|
428 |
+ flInterval := req.flags.AddString("interval", "") |
|
429 |
+ flTimeout := req.flags.AddString("timeout", "") |
|
430 |
+ flStartPeriod := req.flags.AddString("start-period", "") |
|
431 |
+ flRetries := req.flags.AddString("retries", "") |
|
432 |
+ |
|
433 |
+ if err := req.flags.Parse(); err != nil { |
|
434 |
+ return nil, err |
|
435 |
+ } |
|
436 |
+ |
|
437 |
+ switch typ { |
|
438 |
+ case "CMD": |
|
439 |
+ cmdSlice := handleJSONArgs(args, req.attributes) |
|
440 |
+ if len(cmdSlice) == 0 { |
|
441 |
+ return nil, errors.New("Missing command after HEALTHCHECK CMD") |
|
442 |
+ } |
|
443 |
+ |
|
444 |
+ if !req.attributes["json"] { |
|
445 |
+ typ = "CMD-SHELL" |
|
446 |
+ } |
|
447 |
+ |
|
448 |
+ healthcheck.Test = strslice.StrSlice(append([]string{typ}, cmdSlice...)) |
|
449 |
+ default: |
|
450 |
+ return nil, fmt.Errorf("Unknown type %#v in HEALTHCHECK (try CMD)", typ) |
|
451 |
+ } |
|
452 |
+ |
|
453 |
+ interval, err := parseOptInterval(flInterval) |
|
454 |
+ if err != nil { |
|
455 |
+ return nil, err |
|
456 |
+ } |
|
457 |
+ healthcheck.Interval = interval |
|
458 |
+ |
|
459 |
+ timeout, err := parseOptInterval(flTimeout) |
|
460 |
+ if err != nil { |
|
461 |
+ return nil, err |
|
462 |
+ } |
|
463 |
+ healthcheck.Timeout = timeout |
|
464 |
+ |
|
465 |
+ startPeriod, err := parseOptInterval(flStartPeriod) |
|
466 |
+ if err != nil { |
|
467 |
+ return nil, err |
|
468 |
+ } |
|
469 |
+ healthcheck.StartPeriod = startPeriod |
|
470 |
+ |
|
471 |
+ if flRetries.Value != "" { |
|
472 |
+ retries, err := strconv.ParseInt(flRetries.Value, 10, 32) |
|
473 |
+ if err != nil { |
|
474 |
+ return nil, err |
|
475 |
+ } |
|
476 |
+ if retries < 1 { |
|
477 |
+ return nil, fmt.Errorf("--retries must be at least 1 (not %d)", retries) |
|
478 |
+ } |
|
479 |
+ healthcheck.Retries = int(retries) |
|
480 |
+ } else { |
|
481 |
+ healthcheck.Retries = 0 |
|
482 |
+ } |
|
483 |
+ |
|
484 |
+ cmd.Health = &healthcheck |
|
485 |
+ } |
|
486 |
+ return cmd, nil |
|
487 |
+} |
|
488 |
+ |
|
489 |
+func parseExpose(req parseRequest) (*ExposeCommand, error) { |
|
490 |
+ portsTab := req.args |
|
491 |
+ |
|
492 |
+ if len(req.args) == 0 { |
|
493 |
+ return nil, errAtLeastOneArgument("EXPOSE") |
|
494 |
+ } |
|
495 |
+ |
|
496 |
+ if err := req.flags.Parse(); err != nil { |
|
497 |
+ return nil, err |
|
498 |
+ } |
|
499 |
+ |
|
500 |
+ sort.Strings(portsTab) |
|
501 |
+ return &ExposeCommand{ |
|
502 |
+ Ports: portsTab, |
|
503 |
+ withNameAndCode: newWithNameAndCode(req), |
|
504 |
+ }, nil |
|
505 |
+} |
|
506 |
+ |
|
507 |
+func parseUser(req parseRequest) (*UserCommand, error) { |
|
508 |
+ if len(req.args) != 1 { |
|
509 |
+ return nil, errExactlyOneArgument("USER") |
|
510 |
+ } |
|
511 |
+ |
|
512 |
+ if err := req.flags.Parse(); err != nil { |
|
513 |
+ return nil, err |
|
514 |
+ } |
|
515 |
+ return &UserCommand{ |
|
516 |
+ User: req.args[0], |
|
517 |
+ withNameAndCode: newWithNameAndCode(req), |
|
518 |
+ }, nil |
|
519 |
+} |
|
520 |
+ |
|
521 |
+func parseVolume(req parseRequest) (*VolumeCommand, error) { |
|
522 |
+ if len(req.args) == 0 { |
|
523 |
+ return nil, errAtLeastOneArgument("VOLUME") |
|
524 |
+ } |
|
525 |
+ |
|
526 |
+ if err := req.flags.Parse(); err != nil { |
|
527 |
+ return nil, err |
|
528 |
+ } |
|
529 |
+ |
|
530 |
+ cmd := &VolumeCommand{ |
|
531 |
+ withNameAndCode: newWithNameAndCode(req), |
|
532 |
+ } |
|
533 |
+ |
|
534 |
+ for _, v := range req.args { |
|
535 |
+ v = strings.TrimSpace(v) |
|
536 |
+ if v == "" { |
|
537 |
+ return nil, errors.New("VOLUME specified can not be an empty string") |
|
538 |
+ } |
|
539 |
+ cmd.Volumes = append(cmd.Volumes, v) |
|
540 |
+ } |
|
541 |
+ return cmd, nil |
|
542 |
+ |
|
543 |
+} |
|
544 |
+ |
|
545 |
+func parseStopSignal(req parseRequest) (*StopSignalCommand, error) { |
|
546 |
+ if len(req.args) != 1 { |
|
547 |
+ return nil, errExactlyOneArgument("STOPSIGNAL") |
|
548 |
+ } |
|
549 |
+ sig := req.args[0] |
|
550 |
+ |
|
551 |
+ cmd := &StopSignalCommand{ |
|
552 |
+ Signal: sig, |
|
553 |
+ withNameAndCode: newWithNameAndCode(req), |
|
554 |
+ } |
|
555 |
+ return cmd, nil |
|
556 |
+ |
|
557 |
+} |
|
558 |
+ |
|
559 |
+func parseArg(req parseRequest) (*ArgCommand, error) { |
|
560 |
+ if len(req.args) != 1 { |
|
561 |
+ return nil, errExactlyOneArgument("ARG") |
|
562 |
+ } |
|
563 |
+ |
|
564 |
+ var ( |
|
565 |
+ name string |
|
566 |
+ newValue *string |
|
567 |
+ ) |
|
568 |
+ |
|
569 |
+ arg := req.args[0] |
|
570 |
+ // 'arg' can just be a name or name-value pair. Note that this is different |
|
571 |
+ // from 'env' that handles the split of name and value at the parser level. |
|
572 |
+ // The reason for doing it differently for 'arg' is that we support just |
|
573 |
+ // defining an arg and not assign it a value (while 'env' always expects a |
|
574 |
+ // name-value pair). If possible, it will be good to harmonize the two. |
|
575 |
+ if strings.Contains(arg, "=") { |
|
576 |
+ parts := strings.SplitN(arg, "=", 2) |
|
577 |
+ if len(parts[0]) == 0 { |
|
578 |
+ return nil, errBlankCommandNames("ARG") |
|
579 |
+ } |
|
580 |
+ |
|
581 |
+ name = parts[0] |
|
582 |
+ newValue = &parts[1] |
|
583 |
+ } else { |
|
584 |
+ name = arg |
|
585 |
+ } |
|
586 |
+ |
|
587 |
+ return &ArgCommand{ |
|
588 |
+ Key: name, |
|
589 |
+ Value: newValue, |
|
590 |
+ withNameAndCode: newWithNameAndCode(req), |
|
591 |
+ }, nil |
|
592 |
+} |
|
593 |
+ |
|
594 |
+func parseShell(req parseRequest) (*ShellCommand, error) { |
|
595 |
+ if err := req.flags.Parse(); err != nil { |
|
596 |
+ return nil, err |
|
597 |
+ } |
|
598 |
+ shellSlice := handleJSONArgs(req.args, req.attributes) |
|
599 |
+ switch { |
|
600 |
+ case len(shellSlice) == 0: |
|
601 |
+ // SHELL [] |
|
602 |
+ return nil, errAtLeastOneArgument("SHELL") |
|
603 |
+ case req.attributes["json"]: |
|
604 |
+ // SHELL ["powershell", "-command"] |
|
605 |
+ |
|
606 |
+ return &ShellCommand{ |
|
607 |
+ Shell: strslice.StrSlice(shellSlice), |
|
608 |
+ withNameAndCode: newWithNameAndCode(req), |
|
609 |
+ }, nil |
|
610 |
+ default: |
|
611 |
+ // SHELL powershell -command - not JSON |
|
612 |
+ return nil, errNotJSON("SHELL", req.original) |
|
613 |
+ } |
|
614 |
+} |
|
615 |
+ |
|
616 |
+func errAtLeastOneArgument(command string) error { |
|
617 |
+ return errors.Errorf("%s requires at least one argument", command) |
|
618 |
+} |
|
619 |
+ |
|
620 |
+func errExactlyOneArgument(command string) error { |
|
621 |
+ return errors.Errorf("%s requires exactly one argument", command) |
|
622 |
+} |
|
623 |
+ |
|
624 |
+func errAtLeastTwoArguments(command string) error { |
|
625 |
+ return errors.Errorf("%s requires at least two arguments", command) |
|
626 |
+} |
|
627 |
+ |
|
628 |
+func errBlankCommandNames(command string) error { |
|
629 |
+ return errors.Errorf("%s names can not be blank", command) |
|
630 |
+} |
|
631 |
+ |
|
632 |
+func errTooManyArguments(command string) error { |
|
633 |
+ return errors.Errorf("Bad input to %s, too many arguments", command) |
|
634 |
+} |
0 | 635 |
new file mode 100644 |
... | ... |
@@ -0,0 +1,204 @@ |
0 |
+package instructions |
|
1 |
+ |
|
2 |
+import ( |
|
3 |
+ "strings" |
|
4 |
+ "testing" |
|
5 |
+ |
|
6 |
+ "github.com/docker/docker/builder/dockerfile/command" |
|
7 |
+ "github.com/docker/docker/builder/dockerfile/parser" |
|
8 |
+ "github.com/docker/docker/internal/testutil" |
|
9 |
+ "github.com/stretchr/testify/assert" |
|
10 |
+ "github.com/stretchr/testify/require" |
|
11 |
+) |
|
12 |
+ |
|
13 |
+func TestCommandsExactlyOneArgument(t *testing.T) { |
|
14 |
+ commands := []string{ |
|
15 |
+ "MAINTAINER", |
|
16 |
+ "WORKDIR", |
|
17 |
+ "USER", |
|
18 |
+ "STOPSIGNAL", |
|
19 |
+ } |
|
20 |
+ |
|
21 |
+ for _, command := range commands { |
|
22 |
+ ast, err := parser.Parse(strings.NewReader(command)) |
|
23 |
+ require.NoError(t, err) |
|
24 |
+ _, err = ParseInstruction(ast.AST.Children[0]) |
|
25 |
+ assert.EqualError(t, err, errExactlyOneArgument(command).Error()) |
|
26 |
+ } |
|
27 |
+} |
|
28 |
+ |
|
29 |
+func TestCommandsAtLeastOneArgument(t *testing.T) { |
|
30 |
+ commands := []string{ |
|
31 |
+ "ENV", |
|
32 |
+ "LABEL", |
|
33 |
+ "ONBUILD", |
|
34 |
+ "HEALTHCHECK", |
|
35 |
+ "EXPOSE", |
|
36 |
+ "VOLUME", |
|
37 |
+ } |
|
38 |
+ |
|
39 |
+ for _, command := range commands { |
|
40 |
+ ast, err := parser.Parse(strings.NewReader(command)) |
|
41 |
+ require.NoError(t, err) |
|
42 |
+ _, err = ParseInstruction(ast.AST.Children[0]) |
|
43 |
+ assert.EqualError(t, err, errAtLeastOneArgument(command).Error()) |
|
44 |
+ } |
|
45 |
+} |
|
46 |
+ |
|
47 |
+func TestCommandsAtLeastTwoArgument(t *testing.T) { |
|
48 |
+ commands := []string{ |
|
49 |
+ "ADD", |
|
50 |
+ "COPY", |
|
51 |
+ } |
|
52 |
+ |
|
53 |
+ for _, command := range commands { |
|
54 |
+ ast, err := parser.Parse(strings.NewReader(command + " arg1")) |
|
55 |
+ require.NoError(t, err) |
|
56 |
+ _, err = ParseInstruction(ast.AST.Children[0]) |
|
57 |
+ assert.EqualError(t, err, errAtLeastTwoArguments(command).Error()) |
|
58 |
+ } |
|
59 |
+} |
|
60 |
+ |
|
61 |
+func TestCommandsTooManyArguments(t *testing.T) { |
|
62 |
+ commands := []string{ |
|
63 |
+ "ENV", |
|
64 |
+ "LABEL", |
|
65 |
+ } |
|
66 |
+ |
|
67 |
+ for _, command := range commands { |
|
68 |
+ node := &parser.Node{ |
|
69 |
+ Original: command + "arg1 arg2 arg3", |
|
70 |
+ Value: strings.ToLower(command), |
|
71 |
+ Next: &parser.Node{ |
|
72 |
+ Value: "arg1", |
|
73 |
+ Next: &parser.Node{ |
|
74 |
+ Value: "arg2", |
|
75 |
+ Next: &parser.Node{ |
|
76 |
+ Value: "arg3", |
|
77 |
+ }, |
|
78 |
+ }, |
|
79 |
+ }, |
|
80 |
+ } |
|
81 |
+ _, err := ParseInstruction(node) |
|
82 |
+ assert.EqualError(t, err, errTooManyArguments(command).Error()) |
|
83 |
+ } |
|
84 |
+} |
|
85 |
+ |
|
86 |
+func TestCommandsBlankNames(t *testing.T) { |
|
87 |
+ commands := []string{ |
|
88 |
+ "ENV", |
|
89 |
+ "LABEL", |
|
90 |
+ } |
|
91 |
+ |
|
92 |
+ for _, command := range commands { |
|
93 |
+ node := &parser.Node{ |
|
94 |
+ Original: command + " =arg2", |
|
95 |
+ Value: strings.ToLower(command), |
|
96 |
+ Next: &parser.Node{ |
|
97 |
+ Value: "", |
|
98 |
+ Next: &parser.Node{ |
|
99 |
+ Value: "arg2", |
|
100 |
+ }, |
|
101 |
+ }, |
|
102 |
+ } |
|
103 |
+ _, err := ParseInstruction(node) |
|
104 |
+ assert.EqualError(t, err, errBlankCommandNames(command).Error()) |
|
105 |
+ } |
|
106 |
+} |
|
107 |
+ |
|
108 |
+func TestHealthCheckCmd(t *testing.T) { |
|
109 |
+ node := &parser.Node{ |
|
110 |
+ Value: command.Healthcheck, |
|
111 |
+ Next: &parser.Node{ |
|
112 |
+ Value: "CMD", |
|
113 |
+ Next: &parser.Node{ |
|
114 |
+ Value: "hello", |
|
115 |
+ Next: &parser.Node{ |
|
116 |
+ Value: "world", |
|
117 |
+ }, |
|
118 |
+ }, |
|
119 |
+ }, |
|
120 |
+ } |
|
121 |
+ cmd, err := ParseInstruction(node) |
|
122 |
+ assert.NoError(t, err) |
|
123 |
+ hc, ok := cmd.(*HealthCheckCommand) |
|
124 |
+ assert.True(t, ok) |
|
125 |
+ expected := []string{"CMD-SHELL", "hello world"} |
|
126 |
+ assert.Equal(t, expected, hc.Health.Test) |
|
127 |
+} |
|
128 |
+ |
|
129 |
+func TestParseOptInterval(t *testing.T) { |
|
130 |
+ flInterval := &Flag{ |
|
131 |
+ name: "interval", |
|
132 |
+ flagType: stringType, |
|
133 |
+ Value: "50ns", |
|
134 |
+ } |
|
135 |
+ _, err := parseOptInterval(flInterval) |
|
136 |
+ testutil.ErrorContains(t, err, "cannot be less than 1ms") |
|
137 |
+ |
|
138 |
+ flInterval.Value = "1ms" |
|
139 |
+ _, err = parseOptInterval(flInterval) |
|
140 |
+ require.NoError(t, err) |
|
141 |
+} |
|
142 |
+ |
|
143 |
+func TestErrorCases(t *testing.T) { |
|
144 |
+ cases := []struct { |
|
145 |
+ name string |
|
146 |
+ dockerfile string |
|
147 |
+ expectedError string |
|
148 |
+ }{ |
|
149 |
+ { |
|
150 |
+ name: "copyEmptyWhitespace", |
|
151 |
+ dockerfile: `COPY |
|
152 |
+ quux \ |
|
153 |
+ bar`, |
|
154 |
+ expectedError: "COPY requires at least two arguments", |
|
155 |
+ }, |
|
156 |
+ { |
|
157 |
+ name: "ONBUILD forbidden FROM", |
|
158 |
+ dockerfile: "ONBUILD FROM scratch", |
|
159 |
+ expectedError: "FROM isn't allowed as an ONBUILD trigger", |
|
160 |
+ }, |
|
161 |
+ { |
|
162 |
+ name: "ONBUILD forbidden MAINTAINER", |
|
163 |
+ dockerfile: "ONBUILD MAINTAINER docker.io", |
|
164 |
+ expectedError: "MAINTAINER isn't allowed as an ONBUILD trigger", |
|
165 |
+ }, |
|
166 |
+ { |
|
167 |
+ name: "ARG two arguments", |
|
168 |
+ dockerfile: "ARG foo bar", |
|
169 |
+ expectedError: "ARG requires exactly one argument", |
|
170 |
+ }, |
|
171 |
+ { |
|
172 |
+ name: "MAINTAINER unknown flag", |
|
173 |
+ dockerfile: "MAINTAINER --boo joe@example.com", |
|
174 |
+ expectedError: "Unknown flag: boo", |
|
175 |
+ }, |
|
176 |
+ { |
|
177 |
+ name: "Chaining ONBUILD", |
|
178 |
+ dockerfile: `ONBUILD ONBUILD RUN touch foobar`, |
|
179 |
+ expectedError: "Chaining ONBUILD via `ONBUILD ONBUILD` isn't allowed", |
|
180 |
+ }, |
|
181 |
+ { |
|
182 |
+ name: "Invalid instruction", |
|
183 |
+ dockerfile: `foo bar`, |
|
184 |
+ expectedError: "unknown instruction: FOO", |
|
185 |
+ }, |
|
186 |
+ } |
|
187 |
+ for _, c := range cases { |
|
188 |
+ r := strings.NewReader(c.dockerfile) |
|
189 |
+ ast, err := parser.Parse(r) |
|
190 |
+ |
|
191 |
+ if err != nil { |
|
192 |
+ t.Fatalf("Error when parsing Dockerfile: %s", err) |
|
193 |
+ } |
|
194 |
+ n := ast.AST.Children[0] |
|
195 |
+ _, err = ParseInstruction(n) |
|
196 |
+ if err != nil { |
|
197 |
+ testutil.ErrorContains(t, err, c.expectedError) |
|
198 |
+ return |
|
199 |
+ } |
|
200 |
+ t.Fatalf("No error when executing test %s", c.name) |
|
201 |
+ } |
|
202 |
+ |
|
203 |
+} |
0 | 204 |
new file mode 100644 |
... | ... |
@@ -0,0 +1,19 @@ |
0 |
+package instructions |
|
1 |
+ |
|
2 |
+import "strings" |
|
3 |
+ |
|
4 |
+// handleJSONArgs parses command passed to CMD, ENTRYPOINT, RUN and SHELL instruction in Dockerfile |
|
5 |
+// for exec form it returns untouched args slice |
|
6 |
+// for shell form it returns concatenated args as the first element of a slice |
|
7 |
+func handleJSONArgs(args []string, attributes map[string]bool) []string { |
|
8 |
+ if len(args) == 0 { |
|
9 |
+ return []string{} |
|
10 |
+ } |
|
11 |
+ |
|
12 |
+ if attributes != nil && attributes["json"] { |
|
13 |
+ return args |
|
14 |
+ } |
|
15 |
+ |
|
16 |
+ // literal string command, not an exec array |
|
17 |
+ return []string{strings.Join(args, " ")} |
|
18 |
+} |
0 | 19 |
new file mode 100644 |
... | ... |
@@ -0,0 +1,65 @@ |
0 |
+package instructions |
|
1 |
+ |
|
2 |
+import "testing" |
|
3 |
+ |
|
4 |
+type testCase struct { |
|
5 |
+ name string |
|
6 |
+ args []string |
|
7 |
+ attributes map[string]bool |
|
8 |
+ expected []string |
|
9 |
+} |
|
10 |
+ |
|
11 |
+func initTestCases() []testCase { |
|
12 |
+ testCases := []testCase{} |
|
13 |
+ |
|
14 |
+ testCases = append(testCases, testCase{ |
|
15 |
+ name: "empty args", |
|
16 |
+ args: []string{}, |
|
17 |
+ attributes: make(map[string]bool), |
|
18 |
+ expected: []string{}, |
|
19 |
+ }) |
|
20 |
+ |
|
21 |
+ jsonAttributes := make(map[string]bool) |
|
22 |
+ jsonAttributes["json"] = true |
|
23 |
+ |
|
24 |
+ testCases = append(testCases, testCase{ |
|
25 |
+ name: "json attribute with one element", |
|
26 |
+ args: []string{"foo"}, |
|
27 |
+ attributes: jsonAttributes, |
|
28 |
+ expected: []string{"foo"}, |
|
29 |
+ }) |
|
30 |
+ |
|
31 |
+ testCases = append(testCases, testCase{ |
|
32 |
+ name: "json attribute with two elements", |
|
33 |
+ args: []string{"foo", "bar"}, |
|
34 |
+ attributes: jsonAttributes, |
|
35 |
+ expected: []string{"foo", "bar"}, |
|
36 |
+ }) |
|
37 |
+ |
|
38 |
+ testCases = append(testCases, testCase{ |
|
39 |
+ name: "no attributes", |
|
40 |
+ args: []string{"foo", "bar"}, |
|
41 |
+ attributes: nil, |
|
42 |
+ expected: []string{"foo bar"}, |
|
43 |
+ }) |
|
44 |
+ |
|
45 |
+ return testCases |
|
46 |
+} |
|
47 |
+ |
|
48 |
+func TestHandleJSONArgs(t *testing.T) { |
|
49 |
+ testCases := initTestCases() |
|
50 |
+ |
|
51 |
+ for _, test := range testCases { |
|
52 |
+ arguments := handleJSONArgs(test.args, test.attributes) |
|
53 |
+ |
|
54 |
+ if len(arguments) != len(test.expected) { |
|
55 |
+ t.Fatalf("In test \"%s\": length of returned slice is incorrect. Expected: %d, got: %d", test.name, len(test.expected), len(arguments)) |
|
56 |
+ } |
|
57 |
+ |
|
58 |
+ for i := range test.expected { |
|
59 |
+ if arguments[i] != test.expected[i] { |
|
60 |
+ t.Fatalf("In test \"%s\": element as position %d is incorrect. Expected: %s, got: %s", test.name, i, test.expected[i], arguments[i]) |
|
61 |
+ } |
|
62 |
+ } |
|
63 |
+ } |
|
64 |
+} |
... | ... |
@@ -124,7 +124,6 @@ func (b *Builder) commitContainer(dispatchState *dispatchState, id string, conta |
124 | 124 |
} |
125 | 125 |
|
126 | 126 |
dispatchState.imageID = imageID |
127 |
- b.buildStages.update(imageID) |
|
128 | 127 |
return nil |
129 | 128 |
} |
130 | 129 |
|
... | ... |
@@ -164,7 +163,6 @@ func (b *Builder) exportImage(state *dispatchState, imageMount *imageMount, runC |
164 | 164 |
|
165 | 165 |
state.imageID = exportedImage.ImageID() |
166 | 166 |
b.imageSources.Add(newImageMount(exportedImage, newLayer)) |
167 |
- b.buildStages.update(state.imageID) |
|
168 | 167 |
return nil |
169 | 168 |
} |
170 | 169 |
|
... | ... |
@@ -460,7 +458,6 @@ func (b *Builder) probeCache(dispatchState *dispatchState, runConfig *container. |
460 | 460 |
fmt.Fprint(b.Stdout, " ---> Using cache\n") |
461 | 461 |
|
462 | 462 |
dispatchState.imageID = cachedID |
463 |
- b.buildStages.update(dispatchState.imageID) |
|
464 | 463 |
return true, nil |
465 | 464 |
} |
466 | 465 |
|
467 | 466 |
deleted file mode 100644 |
... | ... |
@@ -1,19 +0,0 @@ |
1 |
-package dockerfile |
|
2 |
- |
|
3 |
-import "strings" |
|
4 |
- |
|
5 |
-// handleJSONArgs parses command passed to CMD, ENTRYPOINT, RUN and SHELL instruction in Dockerfile |
|
6 |
-// for exec form it returns untouched args slice |
|
7 |
-// for shell form it returns concatenated args as the first element of a slice |
|
8 |
-func handleJSONArgs(args []string, attributes map[string]bool) []string { |
|
9 |
- if len(args) == 0 { |
|
10 |
- return []string{} |
|
11 |
- } |
|
12 |
- |
|
13 |
- if attributes != nil && attributes["json"] { |
|
14 |
- return args |
|
15 |
- } |
|
16 |
- |
|
17 |
- // literal string command, not an exec array |
|
18 |
- return []string{strings.Join(args, " ")} |
|
19 |
-} |
20 | 1 |
deleted file mode 100644 |
... | ... |
@@ -1,65 +0,0 @@ |
1 |
-package dockerfile |
|
2 |
- |
|
3 |
-import "testing" |
|
4 |
- |
|
5 |
-type testCase struct { |
|
6 |
- name string |
|
7 |
- args []string |
|
8 |
- attributes map[string]bool |
|
9 |
- expected []string |
|
10 |
-} |
|
11 |
- |
|
12 |
-func initTestCases() []testCase { |
|
13 |
- testCases := []testCase{} |
|
14 |
- |
|
15 |
- testCases = append(testCases, testCase{ |
|
16 |
- name: "empty args", |
|
17 |
- args: []string{}, |
|
18 |
- attributes: make(map[string]bool), |
|
19 |
- expected: []string{}, |
|
20 |
- }) |
|
21 |
- |
|
22 |
- jsonAttributes := make(map[string]bool) |
|
23 |
- jsonAttributes["json"] = true |
|
24 |
- |
|
25 |
- testCases = append(testCases, testCase{ |
|
26 |
- name: "json attribute with one element", |
|
27 |
- args: []string{"foo"}, |
|
28 |
- attributes: jsonAttributes, |
|
29 |
- expected: []string{"foo"}, |
|
30 |
- }) |
|
31 |
- |
|
32 |
- testCases = append(testCases, testCase{ |
|
33 |
- name: "json attribute with two elements", |
|
34 |
- args: []string{"foo", "bar"}, |
|
35 |
- attributes: jsonAttributes, |
|
36 |
- expected: []string{"foo", "bar"}, |
|
37 |
- }) |
|
38 |
- |
|
39 |
- testCases = append(testCases, testCase{ |
|
40 |
- name: "no attributes", |
|
41 |
- args: []string{"foo", "bar"}, |
|
42 |
- attributes: nil, |
|
43 |
- expected: []string{"foo bar"}, |
|
44 |
- }) |
|
45 |
- |
|
46 |
- return testCases |
|
47 |
-} |
|
48 |
- |
|
49 |
-func TestHandleJSONArgs(t *testing.T) { |
|
50 |
- testCases := initTestCases() |
|
51 |
- |
|
52 |
- for _, test := range testCases { |
|
53 |
- arguments := handleJSONArgs(test.args, test.attributes) |
|
54 |
- |
|
55 |
- if len(arguments) != len(test.expected) { |
|
56 |
- t.Fatalf("In test \"%s\": length of returned slice is incorrect. Expected: %d, got: %d", test.name, len(test.expected), len(arguments)) |
|
57 |
- } |
|
58 |
- |
|
59 |
- for i := range test.expected { |
|
60 |
- if arguments[i] != test.expected[i] { |
|
61 |
- t.Fatalf("In test \"%s\": element as position %d is incorrect. Expected: %s, got: %s", test.name, i, test.expected[i], arguments[i]) |
|
62 |
- } |
|
63 |
- } |
|
64 |
- } |
|
65 |
-} |
... | ... |
@@ -438,6 +438,82 @@ func (s *DockerSuite) TestBuildChownOnCopy(c *check.C) { |
438 | 438 |
assert.Contains(c, string(out), "Successfully built") |
439 | 439 |
} |
440 | 440 |
|
441 |
+func (s *DockerSuite) TestBuildCopyCacheOnFileChange(c *check.C) { |
|
442 |
+ |
|
443 |
+ dockerfile := `FROM busybox |
|
444 |
+COPY file /file` |
|
445 |
+ |
|
446 |
+ ctx1 := fakecontext.New(c, "", |
|
447 |
+ fakecontext.WithDockerfile(dockerfile), |
|
448 |
+ fakecontext.WithFile("file", "foo")) |
|
449 |
+ ctx2 := fakecontext.New(c, "", |
|
450 |
+ fakecontext.WithDockerfile(dockerfile), |
|
451 |
+ fakecontext.WithFile("file", "bar")) |
|
452 |
+ |
|
453 |
+ var build = func(ctx *fakecontext.Fake) string { |
|
454 |
+ res, body, err := request.Post("/build", |
|
455 |
+ request.RawContent(ctx.AsTarReader(c)), |
|
456 |
+ request.ContentType("application/x-tar")) |
|
457 |
+ |
|
458 |
+ require.NoError(c, err) |
|
459 |
+ assert.Equal(c, http.StatusOK, res.StatusCode) |
|
460 |
+ |
|
461 |
+ out, err := request.ReadBody(body) |
|
462 |
+ |
|
463 |
+ ids := getImageIDsFromBuild(c, out) |
|
464 |
+ return ids[len(ids)-1] |
|
465 |
+ } |
|
466 |
+ |
|
467 |
+ id1 := build(ctx1) |
|
468 |
+ id2 := build(ctx1) |
|
469 |
+ id3 := build(ctx2) |
|
470 |
+ |
|
471 |
+ if id1 != id2 { |
|
472 |
+ c.Fatal("didn't use the cache") |
|
473 |
+ } |
|
474 |
+ if id1 == id3 { |
|
475 |
+ c.Fatal("COPY With different source file should not share same cache") |
|
476 |
+ } |
|
477 |
+} |
|
478 |
+ |
|
479 |
+func (s *DockerSuite) TestBuildAddCacheOnFileChange(c *check.C) { |
|
480 |
+ |
|
481 |
+ dockerfile := `FROM busybox |
|
482 |
+ADD file /file` |
|
483 |
+ |
|
484 |
+ ctx1 := fakecontext.New(c, "", |
|
485 |
+ fakecontext.WithDockerfile(dockerfile), |
|
486 |
+ fakecontext.WithFile("file", "foo")) |
|
487 |
+ ctx2 := fakecontext.New(c, "", |
|
488 |
+ fakecontext.WithDockerfile(dockerfile), |
|
489 |
+ fakecontext.WithFile("file", "bar")) |
|
490 |
+ |
|
491 |
+ var build = func(ctx *fakecontext.Fake) string { |
|
492 |
+ res, body, err := request.Post("/build", |
|
493 |
+ request.RawContent(ctx.AsTarReader(c)), |
|
494 |
+ request.ContentType("application/x-tar")) |
|
495 |
+ |
|
496 |
+ require.NoError(c, err) |
|
497 |
+ assert.Equal(c, http.StatusOK, res.StatusCode) |
|
498 |
+ |
|
499 |
+ out, err := request.ReadBody(body) |
|
500 |
+ |
|
501 |
+ ids := getImageIDsFromBuild(c, out) |
|
502 |
+ return ids[len(ids)-1] |
|
503 |
+ } |
|
504 |
+ |
|
505 |
+ id1 := build(ctx1) |
|
506 |
+ id2 := build(ctx1) |
|
507 |
+ id3 := build(ctx2) |
|
508 |
+ |
|
509 |
+ if id1 != id2 { |
|
510 |
+ c.Fatal("didn't use the cache") |
|
511 |
+ } |
|
512 |
+ if id1 == id3 { |
|
513 |
+ c.Fatal("COPY With different source file should not share same cache") |
|
514 |
+ } |
|
515 |
+} |
|
516 |
+ |
|
441 | 517 |
func (s *DockerSuite) TestBuildWithSession(c *check.C) { |
442 | 518 |
testRequires(c, ExperimentalDaemon) |
443 | 519 |
|
... | ... |
@@ -1173,12 +1173,13 @@ func (s *DockerSuite) TestBuildForceRm(c *check.C) { |
1173 | 1173 |
containerCountBefore := getContainerCount(c) |
1174 | 1174 |
name := "testbuildforcerm" |
1175 | 1175 |
|
1176 |
- buildImage(name, cli.WithFlags("--force-rm"), build.WithBuildContext(c, |
|
1177 |
- build.WithFile("Dockerfile", `FROM `+minimalBaseImage()+` |
|
1176 |
+ r := buildImage(name, cli.WithFlags("--force-rm"), build.WithBuildContext(c, |
|
1177 |
+ build.WithFile("Dockerfile", `FROM busybox |
|
1178 | 1178 |
RUN true |
1179 |
- RUN thiswillfail`))).Assert(c, icmd.Expected{ |
|
1180 |
- ExitCode: 1, |
|
1181 |
- }) |
|
1179 |
+ RUN thiswillfail`))) |
|
1180 |
+ if r.ExitCode != 1 && r.ExitCode != 127 { // different on Linux / Windows |
|
1181 |
+ c.Fatalf("Wrong exit code") |
|
1182 |
+ } |
|
1182 | 1183 |
|
1183 | 1184 |
containerCountAfter := getContainerCount(c) |
1184 | 1185 |
if containerCountBefore != containerCountAfter { |
... | ... |
@@ -4542,7 +4543,6 @@ func (s *DockerSuite) TestBuildBuildTimeArgOverrideEnvDefinedBeforeArg(c *check. |
4542 | 4542 |
} |
4543 | 4543 |
|
4544 | 4544 |
func (s *DockerSuite) TestBuildBuildTimeArgExpansion(c *check.C) { |
4545 |
- testRequires(c, DaemonIsLinux) // Windows does not support ARG |
|
4546 | 4545 |
imgName := "bldvarstest" |
4547 | 4546 |
|
4548 | 4547 |
wdVar := "WDIR" |
... | ... |
@@ -4559,6 +4559,10 @@ func (s *DockerSuite) TestBuildBuildTimeArgExpansion(c *check.C) { |
4559 | 4559 |
userVal := "testUser" |
4560 | 4560 |
volVar := "VOL" |
4561 | 4561 |
volVal := "/testVol/" |
4562 |
+ if DaemonIsWindows() { |
|
4563 |
+ volVal = "C:\\testVol" |
|
4564 |
+ wdVal = "C:\\tmp" |
|
4565 |
+ } |
|
4562 | 4566 |
|
4563 | 4567 |
buildImageSuccessfully(c, imgName, |
4564 | 4568 |
cli.WithFlags( |
... | ... |
@@ -4594,7 +4598,7 @@ func (s *DockerSuite) TestBuildBuildTimeArgExpansion(c *check.C) { |
4594 | 4594 |
) |
4595 | 4595 |
|
4596 | 4596 |
res := inspectField(c, imgName, "Config.WorkingDir") |
4597 |
- c.Check(res, check.Equals, filepath.ToSlash(wdVal)) |
|
4597 |
+ c.Check(filepath.ToSlash(res), check.Equals, filepath.ToSlash(wdVal)) |
|
4598 | 4598 |
|
4599 | 4599 |
var resArr []string |
4600 | 4600 |
inspectFieldAndUnmarshall(c, imgName, "Config.Env", &resArr) |