diff --git a/integration/dockerfiles/Dockerfile_test_arg_blank_with_quotes b/integration/dockerfiles/Dockerfile_test_arg_blank_with_quotes new file mode 100644 index 0000000000..a82f92601a --- /dev/null +++ b/integration/dockerfiles/Dockerfile_test_arg_blank_with_quotes @@ -0,0 +1,12 @@ +ARG FILE_NAME="" + +FROM busybox:latest AS builder +ARG FILE_NAME + +RUN echo $FILE_NAME && touch /$FILE_NAME.txt && stat /$FILE_NAME.txt; + +FROM busybox:latest +ARG FILE_NAME + +RUN echo $FILE_NAME && touch /$FILE_NAME.txt && stat /$FILE_NAME.txt; +COPY --from=builder /$FILE_NAME.txt / diff --git a/integration/dockerfiles/Dockerfile_test_arg_from_quotes b/integration/dockerfiles/Dockerfile_test_arg_from_quotes new file mode 100644 index 0000000000..80846cb92e --- /dev/null +++ b/integration/dockerfiles/Dockerfile_test_arg_from_quotes @@ -0,0 +1,3 @@ +ARG IMAGE_NAME="busybox:latest" + +FROM $IMAGE_NAME diff --git a/integration/dockerfiles/Dockerfile_test_arg_from_single_quotes b/integration/dockerfiles/Dockerfile_test_arg_from_single_quotes new file mode 100644 index 0000000000..5ce248f388 --- /dev/null +++ b/integration/dockerfiles/Dockerfile_test_arg_from_single_quotes @@ -0,0 +1,3 @@ +ARG IMAGE_NAME='busybox:latest' + +FROM $IMAGE_NAME diff --git a/integration/dockerfiles/Dockerfile_test_arg_multi_with_quotes b/integration/dockerfiles/Dockerfile_test_arg_multi_with_quotes new file mode 100644 index 0000000000..8bf2524eb8 --- /dev/null +++ b/integration/dockerfiles/Dockerfile_test_arg_multi_with_quotes @@ -0,0 +1,12 @@ +ARG FILE_NAME="myFile" + +FROM busybox:latest AS builder +ARG FILE_NAME + +RUN echo $FILE_NAME && touch /$FILE_NAME.txt && stat /$FILE_NAME.txt; + +FROM busybox:latest +ARG FILE_NAME + +RUN echo $FILE_NAME && touch /$FILE_NAME.txt && stat /$FILE_NAME.txt; +COPY --from=builder /$FILE_NAME.txt / diff --git a/pkg/dockerfile/dockerfile.go b/pkg/dockerfile/dockerfile.go index 8865b17d00..ea3f5a392b 100644 --- a/pkg/dockerfile/dockerfile.go +++ b/pkg/dockerfile/dockerfile.go @@ -111,9 +111,44 @@ func Parse(b []byte) ([]instructions.Stage, []instructions.ArgCommand, error) { if err != nil { return nil, nil, err } + + metaArgs, err = stripEnclosingQuotes(metaArgs) + if err != nil { + return nil, nil, err + } + return stages, metaArgs, nil } +// stripEnclosingQuotes removes quotes enclosing the value of each instructions.ArgCommand in a slice +// if the quotes are escaped it leaves them +func stripEnclosingQuotes(metaArgs []instructions.ArgCommand) ([]instructions.ArgCommand, error) { + dbl := byte('"') + sgl := byte('\'') + + for i := range metaArgs { + arg := metaArgs[i] + v := arg.Value + if v != nil { + val := *v + fmt.Printf("val %s\n", val) + if (val[0] == dbl && val[len(val)-1] == dbl) || (val[0] == sgl && val[len(val)-1] == sgl) { + val = val[1 : len(val)-1] + } else if val[:2] == `\"` && val[len(val)-2:] == `\"` { + continue + } else if val[:2] == `\'` && val[len(val)-2:] == `\'` { + continue + } else if val[0] == dbl || val[0] == sgl || val[len(val)-1] == dbl || val[len(val)-1] == sgl { + return nil, errors.New("quotes wrapping arg values must be matched") + } + + arg.Value = &val + metaArgs[i] = arg + } + } + return metaArgs, nil +} + // targetStage returns the index of the target stage kaniko is trying to build func targetStage(stages []instructions.Stage, target string) (int, error) { if target == "" { diff --git a/pkg/dockerfile/dockerfile_test.go b/pkg/dockerfile/dockerfile_test.go index 1fa68890c6..52974bd27a 100644 --- a/pkg/dockerfile/dockerfile_test.go +++ b/pkg/dockerfile/dockerfile_test.go @@ -17,13 +17,170 @@ limitations under the License. package dockerfile import ( + "io/ioutil" + "os" "strconv" "testing" + "github.com/GoogleContainerTools/kaniko/pkg/config" "github.com/GoogleContainerTools/kaniko/testutil" "github.com/moby/buildkit/frontend/dockerfile/instructions" ) +func Test_Stages_ArgValueWithQuotes(t *testing.T) { + dockerfile := ` + ARG IMAGE="ubuntu:16.04" + FROM ${IMAGE} + RUN echo hi > /hi + + FROM scratch AS second + COPY --from=0 /hi /hi2 + + FROM scratch + COPY --from=second /hi2 /hi3 + ` + tmpfile, err := ioutil.TempFile("", "Dockerfile.test") + if err != nil { + t.Fatal(err) + } + + defer os.Remove(tmpfile.Name()) + + if _, err := tmpfile.Write([]byte(dockerfile)); err != nil { + t.Fatal(err) + } + if err := tmpfile.Close(); err != nil { + t.Fatal(err) + } + + stages, err := Stages(&config.KanikoOptions{DockerfilePath: tmpfile.Name()}) + if err != nil { + t.Fatal(err) + } + + if len(stages) == 0 { + t.Fatal("length of stages expected to be greater than zero, but was zero") + + } + + if len(stages[0].MetaArgs) == 0 { + t.Fatal("length of stage[0] meta args expected to be greater than zero, but was zero") + } + + expectedVal := "ubuntu:16.04" + + arg := stages[0].MetaArgs[0] + if arg.ValueString() != expectedVal { + t.Fatalf("expected stages[0].MetaArgs[0] val to be %s but was %s", expectedVal, arg.ValueString()) + } +} + +func Test_stripEnclosingQuotes(t *testing.T) { + type testCase struct { + name string + inArgs []instructions.ArgCommand + expected []string + success bool + } + + newArgCommand := func(key, val string) instructions.ArgCommand { + return instructions.ArgCommand{ + KeyValuePairOptional: instructions.KeyValuePairOptional{Key: key, Value: &val}, + } + } + + cases := []testCase{{ + name: "value with no enclosing quotes", + inArgs: []instructions.ArgCommand{newArgCommand("MEOW", "Purr")}, + expected: []string{"Purr"}, + success: true, + }, { + name: "value with unmatched leading double quote", + inArgs: []instructions.ArgCommand{newArgCommand("MEOW", "\"Purr")}, + }, { + name: "value with unmatched trailing double quote", + inArgs: []instructions.ArgCommand{newArgCommand("MEOW", "Purr\"")}, + }, { + name: "value with enclosing double quotes", + inArgs: []instructions.ArgCommand{newArgCommand("MEOW", "\"mrow\"")}, + expected: []string{"mrow"}, + success: true, + }, { + name: "value with unmatched leading single quote", + inArgs: []instructions.ArgCommand{newArgCommand("MEOW", "'Purr")}, + }, { + name: "value with unmatched trailing single quote", + inArgs: []instructions.ArgCommand{newArgCommand("MEOW", "Purr'")}, + }, { + name: "value with enclosing single quotes", + inArgs: []instructions.ArgCommand{newArgCommand("MEOW", "'mrow'")}, + expected: []string{"mrow"}, + success: true, + }, { + name: "blank value with enclosing double quotes", + inArgs: []instructions.ArgCommand{newArgCommand("MEOW", "\"\"")}, + expected: []string{""}, + success: true, + }, { + name: "blank value with enclosing single quotes", + inArgs: []instructions.ArgCommand{newArgCommand("MEOW", "''")}, + expected: []string{""}, + success: true, + }, { + name: "value with escaped, enclosing double quotes", + inArgs: []instructions.ArgCommand{newArgCommand("MEOW", `\"Purr\"`)}, + expected: []string{`\"Purr\"`}, + success: true, + }, { + name: "value with escaped, enclosing single quotes", + inArgs: []instructions.ArgCommand{newArgCommand("MEOW", `\'Purr\'`)}, + expected: []string{`\'Purr\'`}, + success: true, + }, { + name: "multiple values enclosed with single quotes", + inArgs: []instructions.ArgCommand{ + newArgCommand("MEOW", `'Purr'`), + newArgCommand("MEW", `'Mrow'`), + }, + expected: []string{"Purr", "Mrow"}, + success: true, + }, { + name: "no values", + success: true, + }} + + for _, test := range cases { + t.Run(test.name, func(t *testing.T) { + inArgs := test.inArgs + expected := test.expected + success := test.success + + out, err := stripEnclosingQuotes(inArgs) + if success && err != nil { + t.Fatal(err) + } + + if !success && err == nil { + t.Fatal("expected an error but none received") + } + + if len(expected) != len(out) { + t.Fatalf("Expected %d args but got %d", len(expected), len(out)) + } + + for i := range out { + if expected[i] != out[i].ValueString() { + t.Errorf( + "Expected arg at index %d to equal %v but instead equaled %v", + i, + expected[i], + out[i].ValueString()) + } + } + }) + } +} + func Test_resolveStages(t *testing.T) { dockerfile := ` FROM scratch