Skip to content

Commit d569093

Browse files
committed
Make imagebuilder's argument handling mirror "docker build"
There were a few inconsistencies in the way imagebuilder handled arguments vs the way that docker handles them. This commit addresses those gaps. Specifically: - Subsequent ARG commands with default values override previous ones, but they don't override the values passed on the command line. - Heading args (ARG commands before the first FROM) are applied only to FROM commands unless a matching ARG command exists in a particular stage in which case the value from the heading arg carries forward into that stage. This was accomplished by creating a dedicated Builder struct field for user args and heading args so that they can be tracked separately from args declared with ARG commands in individual stages. Tracking these all separately allows us to apply these priority rules as the build progresses. Signed-off-by: Nick Carboni <[email protected]>
1 parent 5c94679 commit d569093

File tree

6 files changed

+294
-22
lines changed

6 files changed

+294
-22
lines changed

builder.go

Lines changed: 39 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -209,12 +209,8 @@ func NewStages(node *parser.Node, b *Builder) (Stages, error) {
209209
stages = append(stages, Stage{
210210
Position: i,
211211
Name: name,
212-
Builder: &Builder{
213-
Args: b.Args,
214-
AllowedArgs: b.AllowedArgs,
215-
Env: b.Env,
216-
},
217-
Node: root,
212+
Builder: b.builderForStage(),
213+
Node: root,
218214
})
219215
}
220216
return stages, nil
@@ -235,17 +231,30 @@ func (b *Builder) extractHeadingArgsFromNode(node *parser.Node) error {
235231
}
236232
}
237233

234+
// Set children equal to everything except the leading ARG nodes
235+
node.Children = children
236+
237+
// Use a separate builder to evaluate the heading args
238+
tempBuilder := NewBuilder(b.UserArgs)
239+
240+
// Evaluate all the heading arg commands
238241
for _, c := range args {
239-
step := b.Step()
242+
step := tempBuilder.Step()
240243
if err := step.Resolve(c); err != nil {
241244
return err
242245
}
243-
if err := b.Run(step, NoopExecutor, false); err != nil {
246+
if err := tempBuilder.Run(step, NoopExecutor, false); err != nil {
244247
return err
245248
}
246249
}
247250

248-
node.Children = children
251+
// Add all of the defined heading args to the original builder's HeadingArgs map
252+
for k, v := range tempBuilder.Args {
253+
if _, ok := tempBuilder.AllowedArgs[k]; ok {
254+
b.HeadingArgs[k] = v
255+
}
256+
}
257+
249258
return nil
250259
}
251260

@@ -264,13 +273,23 @@ func extractNameFromNode(node *parser.Node) (string, bool) {
264273
return n.Next.Value, true
265274
}
266275

276+
func (b *Builder) builderForStage() *Builder {
277+
stageBuilder := NewBuilder(b.UserArgs)
278+
for k, v := range b.HeadingArgs {
279+
stageBuilder.HeadingArgs[k] = v
280+
}
281+
return stageBuilder
282+
}
283+
267284
type Builder struct {
268285
RunConfig docker.Config
269286

270-
Env []string
271-
Args map[string]string
272-
CmdSet bool
273-
Author string
287+
Env []string
288+
Args map[string]string
289+
HeadingArgs map[string]string
290+
UserArgs map[string]string
291+
CmdSet bool
292+
Author string
274293

275294
AllowedArgs map[string]bool
276295
Volumes VolumeSet
@@ -288,12 +307,16 @@ func NewBuilder(args map[string]string) *Builder {
288307
for k, v := range builtinAllowedBuildArgs {
289308
allowed[k] = v
290309
}
291-
provided := make(map[string]string)
310+
userArgs := make(map[string]string)
311+
initialArgs := make(map[string]string)
292312
for k, v := range args {
293-
provided[k] = v
313+
userArgs[k] = v
314+
initialArgs[k] = v
294315
}
295316
return &Builder{
296-
Args: provided,
317+
Args: initialArgs,
318+
UserArgs: userArgs,
319+
HeadingArgs: make(map[string]string),
297320
AllowedArgs: allowed,
298321
}
299322
}

builder_test.go

Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,14 @@ import (
77
"io/ioutil"
88
"os"
99
"reflect"
10+
"regexp"
1011
"strings"
1112
"testing"
1213
"time"
1314

1415
docker "github.com/fsouza/go-dockerclient"
16+
17+
"github.com/openshift/imagebuilder/dockerfile/parser"
1518
)
1619

1720
func TestVolumeSet(t *testing.T) {
@@ -159,6 +162,19 @@ func TestMultiStageParseHeadingArg(t *testing.T) {
159162
if len(stages) != 3 {
160163
t.Fatalf("expected 3 stages, got %d", len(stages))
161164
}
165+
166+
fromImages := []string{"golang:1.9", "busybox:latest", "golang:1.9"}
167+
for i, stage := range stages {
168+
from, err := stage.Builder.From(stage.Node)
169+
if err != nil {
170+
t.Fatal(err)
171+
}
172+
173+
if expected := fromImages[i]; from != expected {
174+
t.Fatalf("expected %s, got %s", expected, from)
175+
}
176+
}
177+
162178
t.Logf("stages: %#v", stages)
163179
}
164180

@@ -192,6 +208,216 @@ RUN echo $FOO $BAR`))
192208
}
193209
}
194210

211+
func resolveNodeArgs(b *Builder, node *parser.Node) error {
212+
for _, c := range node.Children {
213+
if c.Value != "arg" {
214+
continue
215+
}
216+
step := b.Step()
217+
if err := step.Resolve(c); err != nil {
218+
return err
219+
}
220+
if err := b.Run(step, NoopExecutor, false); err != nil {
221+
return err
222+
}
223+
}
224+
return nil
225+
}
226+
227+
func builderHasArgument(b *Builder, argString string) bool {
228+
for _, arg := range b.Arguments() {
229+
if arg == argString {
230+
return true
231+
}
232+
}
233+
return false
234+
}
235+
236+
func TestMultiStageHeadingArgRedefine(t *testing.T) {
237+
n, err := ParseFile("dockerclient/testdata/multistage/Dockerfile.heading-redefine")
238+
if err != nil {
239+
t.Fatal(err)
240+
}
241+
stages, err := NewStages(n, NewBuilder(map[string]string{}))
242+
if err != nil {
243+
t.Fatal(err)
244+
}
245+
if len(stages) != 2 {
246+
t.Fatalf("expected 2 stages, got %d", len(stages))
247+
}
248+
249+
for _, stage := range stages {
250+
if err := resolveNodeArgs(stage.Builder, stage.Node); err != nil {
251+
t.Fatal(err)
252+
}
253+
}
254+
255+
firstStageHasArg := false
256+
for _, arg := range stages[0].Builder.Arguments() {
257+
if match, err := regexp.MatchString(`FOO=.*`, arg); err == nil && match {
258+
firstStageHasArg = true
259+
break
260+
} else if err != nil {
261+
t.Fatal(err)
262+
}
263+
}
264+
if firstStageHasArg {
265+
t.Fatalf("expected FOO to not be present in first stage")
266+
}
267+
268+
if !builderHasArgument(stages[1].Builder, "FOO=latest") {
269+
t.Fatalf("expected FOO=latest in second stage arguments list, got %v", stages[1].Builder.Arguments())
270+
}
271+
}
272+
273+
func TestMultiStageHeadingArgRedefineOverride(t *testing.T) {
274+
n, err := ParseFile("dockerclient/testdata/multistage/Dockerfile.heading-redefine")
275+
if err != nil {
276+
t.Fatal(err)
277+
}
278+
stages, err := NewStages(n, NewBuilder(map[string]string{"FOO": "7"}))
279+
if err != nil {
280+
t.Fatal(err)
281+
}
282+
if len(stages) != 2 {
283+
t.Fatalf("expected 2 stages, got %d", len(stages))
284+
}
285+
286+
for _, stage := range stages {
287+
if err := resolveNodeArgs(stage.Builder, stage.Node); err != nil {
288+
t.Fatal(err)
289+
}
290+
}
291+
292+
firstStageHasArg := false
293+
for _, arg := range stages[0].Builder.Arguments() {
294+
if match, err := regexp.MatchString(`FOO=.*`, arg); err == nil && match {
295+
firstStageHasArg = true
296+
break
297+
} else if err != nil {
298+
t.Fatal(err)
299+
}
300+
}
301+
if firstStageHasArg {
302+
t.Fatalf("expected FOO to not be present in first stage")
303+
}
304+
305+
if !builderHasArgument(stages[1].Builder, "FOO=7") {
306+
t.Fatalf("expected FOO=7 in second stage arguments list, got %v", stages[1].Builder.Arguments())
307+
}
308+
}
309+
310+
func TestArgs(t *testing.T) {
311+
for _, tc := range []struct {
312+
name string
313+
dockerfile string
314+
args map[string]string
315+
expectedValue string
316+
}{
317+
{
318+
name: "argOverride",
319+
dockerfile: "FROM centos\nARG FOO=stuff\nARG FOO=things\n",
320+
args: map[string]string{},
321+
expectedValue: "FOO=things",
322+
},
323+
{
324+
name: "argOverrideWithBuildArgs",
325+
dockerfile: "FROM centos\nARG FOO=stuff\nARG FOO=things\n",
326+
args: map[string]string{"FOO": "bar"},
327+
expectedValue: "FOO=bar",
328+
},
329+
{
330+
name: "headingArgRedefine",
331+
dockerfile: "ARG FOO=stuff\nFROM centos\nARG FOO\n",
332+
args: map[string]string{},
333+
expectedValue: "FOO=stuff",
334+
},
335+
{
336+
name: "headingArgRedefineWithBuildArgs",
337+
dockerfile: "ARG FOO=stuff\nFROM centos\nARG FOO\n",
338+
args: map[string]string{"FOO": "bar"},
339+
expectedValue: "FOO=bar",
340+
},
341+
{
342+
name: "headingArgRedefineDefault",
343+
dockerfile: "ARG FOO=stuff\nFROM centos\nARG FOO=defaultfoovalue\n",
344+
args: map[string]string{},
345+
expectedValue: "FOO=defaultfoovalue",
346+
},
347+
{
348+
name: "headingArgRedefineDefaultWithBuildArgs",
349+
dockerfile: "ARG FOO=stuff\nFROM centos\nARG FOO=defaultfoovalue\n",
350+
args: map[string]string{"FOO": "bar"},
351+
expectedValue: "FOO=bar",
352+
},
353+
} {
354+
t.Run(tc.name, func(t *testing.T) {
355+
node, err := ParseDockerfile(strings.NewReader(tc.dockerfile))
356+
if err != nil {
357+
t.Fatal(err)
358+
}
359+
360+
b := NewBuilder(tc.args)
361+
if err := resolveNodeArgs(b, node); err != nil {
362+
t.Fatal(err)
363+
}
364+
365+
if !builderHasArgument(b, tc.expectedValue) {
366+
t.Fatalf("expected %s to be contained in arguments list: %v", tc.expectedValue, b.Arguments())
367+
}
368+
})
369+
}
370+
}
371+
372+
func TestMultiStageArgScope(t *testing.T) {
373+
n, err := ParseFile("dockerclient/testdata/multistage/Dockerfile.arg-scope")
374+
if err != nil {
375+
t.Fatal(err)
376+
}
377+
args := map[string]string{
378+
"SECRET": "secretthings",
379+
"BAR": "notsecretthings",
380+
}
381+
stages, err := NewStages(n, NewBuilder(args))
382+
if err != nil {
383+
t.Fatal(err)
384+
}
385+
if len(stages) != 2 {
386+
t.Fatalf("expected 2 stages, got %d", len(stages))
387+
}
388+
389+
for _, stage := range stages {
390+
if err := resolveNodeArgs(stage.Builder, stage.Node); err != nil {
391+
t.Fatal(err)
392+
}
393+
}
394+
395+
if !builderHasArgument(stages[0].Builder, "SECRET=secretthings") {
396+
t.Fatalf("expected SECRET=secretthings to be contained in first stage arguments list: %v", stages[0].Builder.Arguments())
397+
}
398+
399+
secondStageArguments := stages[1].Builder.Arguments()
400+
secretInSecondStage := false
401+
for _, arg := range secondStageArguments {
402+
if match, err := regexp.MatchString(`SECRET=.*`, arg); err == nil && match {
403+
secretInSecondStage = true
404+
break
405+
} else if err != nil {
406+
t.Fatal(err)
407+
}
408+
}
409+
if secretInSecondStage {
410+
t.Fatalf("expected SECRET to not be present in second stage")
411+
}
412+
413+
if !builderHasArgument(stages[1].Builder, "FOO=test") {
414+
t.Fatalf("expected FOO=test to be present in second stage arguments list: %v", secondStageArguments)
415+
}
416+
if !builderHasArgument(stages[1].Builder, "BAR=notsecretthings") {
417+
t.Fatalf("expected BAR=notsecretthings to be present in second stage arguments list: %v", secondStageArguments)
418+
}
419+
}
420+
195421
func TestRun(t *testing.T) {
196422
f, err := os.Open("dockerclient/testdata/Dockerfile.add")
197423
if err != nil {

dispatchers.go

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,7 @@ func from(b *Builder, args []string, attributes map[string]bool, flagArgs []stri
216216

217217
// Support ARG before from
218218
argStrs := []string{}
219-
for n, v := range b.Args {
219+
for n, v := range b.HeadingArgs {
220220
argStrs = append(argStrs, n+"="+v)
221221
}
222222
var err error
@@ -598,10 +598,16 @@ func arg(b *Builder, args []string, attributes map[string]bool, flagArgs []strin
598598
// add the arg to allowed list of build-time args from this step on.
599599
b.AllowedArgs[name] = true
600600

601+
// If there is still no default value, a value can be assigned from the heading args
602+
if val, ok := b.HeadingArgs[name]; ok && !hasDefault {
603+
b.Args[name] = val
604+
}
605+
601606
// If there is a default value associated with this arg then add it to the
602-
// b.buildArgs if one is not already passed to the builder. The args passed
603-
// to builder override the default value of 'arg'.
604-
if _, ok := b.Args[name]; !ok && hasDefault {
607+
// b.buildArgs, later default values for the same arg override earlier ones.
608+
// The args passed to builder (UserArgs) override the default value of 'arg'
609+
// Don't add them here as they were already set in NewBuilder.
610+
if _, ok := b.UserArgs[name]; !ok && hasDefault {
605611
b.Args[name] = value
606612
}
607613

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
FROM alpine
2+
ARG SECRET
3+
RUN echo "$SECRET"
4+
5+
FROM alpine
6+
ARG FOO=test
7+
ARG BAR=bartest
8+
RUN echo "$FOO:$BAR"
9+
RUN echo "$SECRET"

0 commit comments

Comments
 (0)