Skip to content

Commit

Permalink
feat: multistages is now built without unusued stages
Browse files Browse the repository at this point in the history
  • Loading branch information
JordanGoasdoue authored and goasdoue committed Mar 31, 2020
1 parent eb4abb4 commit 2b45013
Show file tree
Hide file tree
Showing 3 changed files with 189 additions and 13 deletions.
65 changes: 55 additions & 10 deletions pkg/dockerfile/dockerfile.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,38 +57,83 @@ func Stages(opts *config.KanikoOptions) ([]config.KanikoStage, error) {
if err != nil {
return nil, errors.Wrap(err, "parsing dockerfile")
}
args := unifyArgs(metaArgs, opts.BuildArgs)
if err := resolveStagesArgs(stages, args); err != nil {
return nil, errors.Wrap(err, "resolving args")
}
targetStage, err := targetStage(stages, opts.Target)
if err != nil {
return nil, err
}
resolveStages(stages)
args := unifyArgs(metaArgs, opts.BuildArgs)
if err := resolveStagesArgs(stages, args); err != nil {
return nil, errors.Wrap(err, "resolving args")
usedStages := getUsedStages(stages, targetStage, opts.Target)
if targetStage > len(usedStages)-1 {
targetStage = len(usedStages) - 1
}
resolveStages(usedStages)
var kanikoStages []config.KanikoStage
for index, stage := range stages {
for index, stage := range usedStages {
if len(stage.Name) > 0 {
logrus.Infof("Resolved base name %s to %s", stage.BaseName, stage.Name)
}
baseImageIndex := baseImageIndex(index, stages)
baseImageIndex := baseImageIndex(index, usedStages)
kanikoStages = append(kanikoStages, config.KanikoStage{
Stage: stage,
BaseImageIndex: baseImageIndex,
BaseImageStoredLocally: (baseImageIndex != -1),
SaveStage: saveStage(index, stages),
SaveStage: saveStage(index, usedStages),
Final: index == targetStage,
MetaArgs: metaArgs,
Index: index,
})
if index == targetStage {
break
}
}

return kanikoStages, nil
}

// getUsedStages returns the list of used stages calculated from dependencies
func getUsedStages(stages []instructions.Stage, lastStageIndex int, target string) []instructions.Stage {
stagesDependencies := make(map[string]bool)
var usedStages []instructions.Stage

lastStageBaseName := stages[lastStageIndex].BaseName

for i := lastStageIndex; i >= 0; i-- {
s := stages[i]
if (s.Name != "" && stagesDependencies[s.Name]) || s.Name == lastStageBaseName || i == lastStageIndex {
for _, c := range s.Commands {
switch cmd := c.(type) {
case *instructions.CopyCommand:
stageName := cmd.From
if copyFromIndex, err := strconv.Atoi(stageName); err == nil {
stageName = stages[copyFromIndex].Name
}
if !stagesDependencies[stageName] {
stagesDependencies[stageName] = true
}
}
}
if i != lastStageIndex {
stagesDependencies[s.BaseName] = true
}
}
}
if target == "" && len(stagesDependencies) == 0 {
return stages
}
for i := 0; i < lastStageIndex; i++ {
s := stages[i]
if s.Name == "" {
continue
}
if stagesDependencies[s.Name] || s.Name == lastStageBaseName {
usedStages = append(usedStages, s)
}
}
usedStages = append(usedStages, stages[lastStageIndex])

return usedStages
}

// 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 {
Expand Down
132 changes: 132 additions & 0 deletions pkg/dockerfile/dockerfile_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -435,3 +435,135 @@ func Test_ResolveStagesArgs(t *testing.T) {
}
}
}

func Test_MutiStageDependencies(t *testing.T) {
tests := []struct {
description string
dockerfile string
targets []string
expectedSourceCodes map[string][]string
}{
{
description: "dockerfile_without_copyFrom",
dockerfile: `
FROM alpine:3.11 AS base-dev
RUN echo dev > /hi
FROM alpine:3.11 AS base-prod
RUN echo prod > /hi
FROM base-dev as final-stage
RUN cat /hi
`,
targets: []string{"base-dev", "base-prod", ""},
expectedSourceCodes: map[string][]string{
"base-dev": {"FROM alpine:3.11 AS base-dev"},
"base-prod": {"FROM alpine:3.11 AS base-prod"},
"": {"FROM alpine:3.11 AS base-dev", "FROM base-dev as final-stage"},
},
},
{
description: "dockerfile_with_copyFrom",
dockerfile: `
FROM alpine:3.11 AS base-dev
RUN echo dev > /hi
FROM alpine:3.11 AS base-prod
RUN echo prod > /hi
FROM alpine:3.11
COPY --from=base-prod /hi /finalhi
RUN cat /finalhi
`,
targets: []string{"base-dev", "base-prod", ""},
expectedSourceCodes: map[string][]string{
"base-dev": {"FROM alpine:3.11 AS base-dev"},
"base-prod": {"FROM alpine:3.11 AS base-prod"},
"": {"FROM alpine:3.11 AS base-prod", "FROM alpine:3.11"},
},
},
{
description: "dockerfile_with_two_copyFrom",
dockerfile: `
FROM alpine:3.11 AS base-dev
RUN echo dev > /hi
FROM alpine:3.11 AS base-prod
RUN echo prod > /hi
FROM alpine:3.11
COPY --from=base-dev /hi /finalhidev
COPY --from=base-prod /hi /finalhiprod
RUN cat /finalhidev
RUN cat /finalhiprod
`,
targets: []string{"base-dev", "base-prod", ""},
expectedSourceCodes: map[string][]string{
"base-dev": {"FROM alpine:3.11 AS base-dev"},
"base-prod": {"FROM alpine:3.11 AS base-prod"},
"": {"FROM alpine:3.11 AS base-dev", "FROM alpine:3.11 AS base-prod", "FROM alpine:3.11"},
},
},
{
description: "dockerfile_with_two_copyFrom_and_arg",
dockerfile: `
FROM debian:9.11 as base
COPY . .
FROM scratch as second
ENV foopath context/foo
COPY --from=0 $foopath context/b* /foo/
FROM second as third
COPY --from=base /context/foo /new/foo
FROM base as fourth
# Make sure that we snapshot intermediate images correctly
RUN date > /date
ENV foo bar
# This base image contains symlinks with relative paths to whitelisted directories
# We need to test they're extracted correctly
FROM fedora@sha256:c4cc32b09c6ae3f1353e7e33a8dda93dc41676b923d6d89afa996b421cc5aa48
FROM fourth
ARG file=/foo2
COPY --from=second /foo ${file}
COPY --from=debian:9.11 /etc/os-release /new
`,
targets: []string{},
expectedSourceCodes: map[string][]string{
"": {"FROM debian:9.11 as base", "FROM scratch as second", "FROM base as fourth", "FROM fourth"},
},
},
{
description: "dockerfile_without_final_dependencies",
dockerfile: `
FROM alpine:3.11
FROM debian:9.11 as base
RUN echo foo > /foo
FROM debian:9.11 as fizz
RUN echo fizz >> /fizz
COPY --from=base /foo /fizz
FROM alpine:3.11 as buzz
RUN echo buzz > /buzz
FROM alpine:3.11 as final
RUN echo bar > /bar
`,
targets: []string{"final", "buzz", "fizz", ""},
expectedSourceCodes: map[string][]string{
"final": {"FROM alpine:3.11 as final"},
"buzz": {"FROM alpine:3.11 as buzz"},
"fizz": {"FROM debian:9.11 as base", "FROM debian:9.11 as fizz"},
"": {"FROM alpine:3.11", "FROM debian:9.11 as base", "FROM debian:9.11 as fizz", "FROM alpine:3.11 as buzz", "FROM alpine:3.11 as final"},
},
},
}

for _, test := range tests {
stages, _, err := Parse([]byte(test.dockerfile))
if err != nil {
t.Fatal(err)
}
actualSourceCodes := make(map[string][]string)
for _, target := range test.targets {
targetIndex, err := targetStage(stages, target)
usedStages := getUsedStages(stages, targetIndex, target)
for _, s := range usedStages {
actualSourceCodes[target] = append(actualSourceCodes[target], s.SourceCode)
}
t.Run(test.description, func(t *testing.T) {
testutil.CheckErrorAndDeepEqual(t, false, err, test.expectedSourceCodes[target], actualSourceCodes[target])
})
}
}
}
5 changes: 2 additions & 3 deletions pkg/executor/build_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -264,11 +264,10 @@ COPY --from=stage2 /bar /bat
name: "double deps",
args: args{
dockerfile: `
FROM debian as stage1
FROM ubuntu as stage2
FROM ubuntu as stage1
RUN foo
COPY --from=stage1 /foo /bar
FROM alpine
COPY --from=stage1 /foo /bar
COPY --from=stage1 /baz /bat
`,
},
Expand Down

0 comments on commit 2b45013

Please sign in to comment.