Skip to content

Commit 8de7174

Browse files
Merge pull request #151 from carbonin/scope_args_to_stage
Treat args the same way `docker build` does
2 parents edcddd8 + d569093 commit 8de7174

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)