diff --git a/pkg/dockerfile/dockerfile.go b/pkg/dockerfile/dockerfile.go index 8e3f1b6e1..7279b7b3b 100644 --- a/pkg/dockerfile/dockerfile.go +++ b/pkg/dockerfile/dockerfile.go @@ -230,23 +230,40 @@ func ResolveCrossStageCommands(cmds []instructions.Command, stageNameToIdx map[s } } +// resolveStagesArgs resolves all the args from list of stages +func resolveStagesArgs(stages []instructions.Stage, args []string) error { + for i, s := range stages { + resolvedBaseName, err := util.ResolveEnvironmentReplacement(s.BaseName, args, false) + if err != nil { + return errors.Wrap(err, fmt.Sprintf("resolving base name %s", s.BaseName)) + } + if s.BaseName != resolvedBaseName { + stages[i].BaseName = resolvedBaseName + } + } + return nil +} + func MakeKanikoStages(opts *config.KanikoOptions, stages []instructions.Stage, metaArgs []instructions.ArgCommand) ([]config.KanikoStage, error) { targetStage, err := targetStage(stages, opts.Target) if err != nil { return nil, errors.Wrap(err, "Error finding target stage") } + args := unifyArgs(metaArgs, opts.BuildArgs) + if err := resolveStagesArgs(stages, args); err != nil { + return nil, errors.Wrap(err, "resolving args") + } var kanikoStages []config.KanikoStage for index, stage := range stages { - resolvedBaseName, err := util.ResolveEnvironmentReplacement(stage.BaseName, opts.BuildArgs, false) - if err != nil { - return nil, errors.Wrap(err, "resolving base name") + if len(stage.Name) > 0 { + logrus.Infof("Resolved base name %s to %s", stage.BaseName, stage.Name) } - stage.Name = resolvedBaseName - logrus.Infof("Resolved base name %s to %s", stage.BaseName, stage.Name) + baseImageIndex := baseImageIndex(index, stages) + kanikoStages = append(kanikoStages, config.KanikoStage{ Stage: stage, - BaseImageIndex: baseImageIndex(index, stages), - BaseImageStoredLocally: (baseImageIndex(index, stages) != -1), + BaseImageIndex: baseImageIndex, + BaseImageStoredLocally: (baseImageIndex != -1), SaveStage: saveStage(index, stages), Final: index == targetStage, MetaArgs: metaArgs, @@ -273,3 +290,26 @@ func GetOnBuildInstructions(config *v1.Config, stageNameToIdx map[string]string) ResolveCrossStageCommands(cmds, stageNameToIdx) return cmds, nil } + +// unifyArgs returns the unified args between metaArgs and --build-arg +// by default --build-arg overrides metaArgs except when --build-arg is empty +func unifyArgs(metaArgs []instructions.ArgCommand, buildArgs []string) []string { + argsMap := make(map[string]string) + for _, a := range metaArgs { + if a.Value != nil { + argsMap[a.Key] = *a.Value + } + } + splitter := "=" + for _, a := range buildArgs { + s := strings.Split(a, splitter) + if len(s) > 1 && s[1] != "" { + argsMap[s[0]] = s[1] + } + } + var args []string + for k, v := range argsMap { + args = append(args, fmt.Sprintf("%s=%s", k, v)) + } + return args +} diff --git a/pkg/dockerfile/dockerfile_test.go b/pkg/dockerfile/dockerfile_test.go index 334b8008b..7055eaf00 100644 --- a/pkg/dockerfile/dockerfile_test.go +++ b/pkg/dockerfile/dockerfile_test.go @@ -17,6 +17,7 @@ limitations under the License. package dockerfile import ( + "fmt" "io/ioutil" "os" "reflect" @@ -427,3 +428,72 @@ func Test_baseImageIndex(t *testing.T) { }) } } + +func Test_ResolveStagesArgs(t *testing.T) { + dockerfile := ` + ARG IMAGE="ubuntu:16.04" + ARG LAST_STAGE_VARIANT + FROM ${IMAGE} as base + RUN echo hi > /hi + FROM base AS base-dev + RUN echo dev >> /hi + FROM base AS base-prod + RUN echo prod >> /hi + FROM base-${LAST_STAGE_VARIANT} + RUN cat /hi + ` + + buildArgLastVariants := []string{"dev", "prod"} + buildArgImages := []string{"alpine:3.11", ""} + var expectedImage string + + for _, buildArgLastVariant := range buildArgLastVariants { + for _, buildArgImage := range buildArgImages { + if buildArgImage != "" { + expectedImage = buildArgImage + } else { + expectedImage = "ubuntu:16.04" + } + buildArgs := []string{fmt.Sprintf("IMAGE=%s", buildArgImage), fmt.Sprintf("LAST_STAGE_VARIANT=%s", buildArgLastVariant)} + + stages, metaArgs, err := Parse([]byte(dockerfile)) + if err != nil { + t.Fatal(err) + } + stagesLen := len(stages) + + args := unifyArgs(metaArgs, buildArgs) + if err := resolveStagesArgs(stages, args); err != nil { + t.Fatalf("fail to resolves args %v: %v", buildArgs, err) + } + tests := []struct { + name string + actualSourceCode string + actualBaseName string + expectedSourceCode string + expectedBaseName string + }{ + { + name: "Test_BuildArg_From_First_Stage", + actualSourceCode: stages[0].SourceCode, + actualBaseName: stages[0].BaseName, + expectedSourceCode: "FROM ${IMAGE} as base", + expectedBaseName: expectedImage, + }, + { + name: "Test_BuildArg_From_Last_Stage", + actualSourceCode: stages[stagesLen-1].SourceCode, + actualBaseName: stages[stagesLen-1].BaseName, + expectedSourceCode: "FROM base-${LAST_STAGE_VARIANT}", + expectedBaseName: fmt.Sprintf("base-%s", buildArgLastVariant), + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + testutil.CheckDeepEqual(t, test.expectedSourceCode, test.actualSourceCode) + testutil.CheckDeepEqual(t, test.expectedBaseName, test.actualBaseName) + }) + } + } + } +}