Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extract intermediate stages to filesystem #266

Merged
merged 3 commits into from
Aug 2, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion integration/dockerfiles/Dockerfile_test_multistage
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ FROM scratch as second
ENV foopath context/foo
COPY --from=0 $foopath context/b* /foo/

FROM second
COPY --from=base /context/foo /new/foo

FROM base
ARG file
COPY --from=second /foo $file
COPY --from=second /foo ${file}
69 changes: 29 additions & 40 deletions pkg/dockerfile/dockerfile.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,32 @@ package dockerfile
import (
"bytes"
"fmt"
"path/filepath"
"io/ioutil"
"strconv"
"strings"

"github.com/GoogleContainerTools/kaniko/pkg/constants"
"github.com/GoogleContainerTools/kaniko/pkg/util"
"github.com/moby/buildkit/frontend/dockerfile/instructions"
"github.com/moby/buildkit/frontend/dockerfile/parser"
)

// Stages reads the Dockerfile, validates it's contents, and returns stages
func Stages(dockerfilePath, target string) ([]instructions.Stage, error) {
d, err := ioutil.ReadFile(dockerfilePath)
if err != nil {
return nil, err
}

stages, err := Parse(d)
if err != nil {
return nil, err
}
if err := ValidateTarget(stages, target); err != nil {
return nil, err
}
ResolveStages(stages)
return stages, nil
}

// Parse parses the contents of a Dockerfile and returns a list of commands
func Parse(b []byte) ([]instructions.Stage, error) {
p, err := parser.Parse(bytes.NewReader(b))
Expand All @@ -43,6 +59,9 @@ func Parse(b []byte) ([]instructions.Stage, error) {
}

func ValidateTarget(stages []instructions.Stage, target string) error {
if target == "" {
return nil
}
for _, stage := range stages {
if stage.Name == target {
return nil
Expand Down Expand Up @@ -91,53 +110,23 @@ func ParseCommands(cmdArray []string) ([]instructions.Command, error) {
return cmds, nil
}

// Dependencies returns a list of files in this stage that will be needed in later stages
func Dependencies(index int, stages []instructions.Stage, buildArgs *BuildArgs) ([]string, error) {
dependencies := []string{}
// SaveStage returns true if the current stage will be needed later in the Dockerfile
func SaveStage(index int, stages []instructions.Stage) bool {
for stageIndex, stage := range stages {
if stageIndex <= index {
continue
}
sourceImage, err := util.RetrieveSourceImage(stageIndex, buildArgs.ReplacementEnvs(nil), stages)
if err != nil {
return nil, err
}
imageConfig, err := sourceImage.ConfigFile()
if err != nil {
return nil, err
if stage.Name == stages[index].BaseName {
return true
}
for _, cmd := range stage.Commands {
switch c := cmd.(type) {
case *instructions.EnvCommand:
replacementEnvs := buildArgs.ReplacementEnvs(imageConfig.Config.Env)
if err := util.UpdateConfigEnv(c.Env, &imageConfig.Config, replacementEnvs); err != nil {
return nil, err
}
case *instructions.ArgCommand:
buildArgs.AddArg(c.Key, c.Value)
case *instructions.CopyCommand:
if c.From != strconv.Itoa(index) {
continue
}
// First, resolve any environment replacement
replacementEnvs := buildArgs.ReplacementEnvs(imageConfig.Config.Env)
resolvedEnvs, err := util.ResolveEnvironmentReplacementList(c.SourcesAndDest, replacementEnvs, true)
if err != nil {
return nil, err
}
// Resolve wildcards and get a list of resolved sources
srcs, err := util.ResolveSources(resolvedEnvs, constants.RootDir)
if err != nil {
return nil, err
}
for index, src := range srcs {
if !filepath.IsAbs(src) {
srcs[index] = filepath.Join(constants.RootDir, src)
}
if c.From == strconv.Itoa(index) {
return true
}
dependencies = append(dependencies, srcs...)
}
}
}
return dependencies, nil
return false
}
112 changes: 44 additions & 68 deletions pkg/dockerfile/dockerfile_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ limitations under the License.
package dockerfile

import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
Expand Down Expand Up @@ -95,82 +94,59 @@ func Test_ValidateTarget(t *testing.T) {
}
}

func Test_Dependencies(t *testing.T) {
testDir, err := ioutil.TempDir("", "")
func Test_SaveStage(t *testing.T) {
tempDir, err := ioutil.TempDir("", "")
if err != nil {
t.Fatal(err)
}
helloPath := filepath.Join(testDir, "hello")
if err := os.Mkdir(helloPath, 0755); err != nil {
t.Fatal(err)
t.Fatalf("couldn't create temp dir: %v", err)
}

dockerfile := fmt.Sprintf(`
FROM scratch
COPY %s %s
defer os.RemoveAll(tempDir)
files := map[string]string{
"Dockerfile": `
FROM scratch
RUN echo hi > /hi

FROM scratch AS second
COPY --from=0 /hi /hi2

FROM scratch AS second
ENV hienv %s
COPY a b
COPY --from=0 /$hienv %s /hi2/
`, helloPath, helloPath, helloPath, testDir)

stages, err := Parse([]byte(dockerfile))
if err != nil {
t.Fatal(err)
}

expectedDependencies := [][]string{
{
helloPath,
testDir,
},
{},
}

for index := range stages {
buildArgs := NewBuildArgs([]string{})
actualDeps, err := Dependencies(index, stages, buildArgs)
testutil.CheckErrorAndDeepEqual(t, false, err, expectedDependencies[index], actualDeps)
}
}

func Test_DependenciesWithArg(t *testing.T) {
testDir, err := ioutil.TempDir("", "")
if err != nil {
t.Fatal(err)
FROM second
RUN xxx

FROM scratch
COPY --from=second /hi2 /hi3
`,
}
helloPath := filepath.Join(testDir, "hello")
if err := os.Mkdir(helloPath, 0755); err != nil {
t.Fatal(err)
if err := testutil.SetupFiles(tempDir, files); err != nil {
t.Fatalf("couldn't create dockerfile: %v", err)
}

dockerfile := fmt.Sprintf(`
FROM scratch
COPY %s %s

FROM scratch AS second
ARG hienv
COPY a b
COPY --from=0 /$hienv %s /hi2/
`, helloPath, helloPath, testDir)

stages, err := Parse([]byte(dockerfile))
stages, err := Stages(filepath.Join(tempDir, "Dockerfile"), "")
if err != nil {
t.Fatal(err)
t.Fatalf("couldn't retrieve stages from Dockerfile: %v", err)
}

expectedDependencies := [][]string{
tests := []struct {
name string
index int
expected bool
}{
{
name: "reference stage in later copy command",
index: 0,
expected: true,
},
{
name: "reference stage in later from command",
index: 1,
expected: true,
},
{
helloPath,
testDir,
name: "don't reference stage later",
index: 2,
expected: false,
},
{},
}
buildArgs := NewBuildArgs([]string{fmt.Sprintf("hienv=%s", helloPath)})

for index := range stages {
actualDeps, err := Dependencies(index, stages, buildArgs)
testutil.CheckErrorAndDeepEqual(t, false, err, expectedDependencies[index], actualDeps)
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
actual := SaveStage(test.index, stages)
testutil.CheckErrorAndDeepEqual(t, false, nil, test.expected, actual)
})
}
}
73 changes: 23 additions & 50 deletions pkg/executor/executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,32 +58,23 @@ type KanikoBuildArgs struct {

func DoBuild(k KanikoBuildArgs) (v1.Image, error) {
// Parse dockerfile and unpack base image to root
d, err := ioutil.ReadFile(k.DockerfilePath)
stages, err := dockerfile.Stages(k.DockerfilePath, k.Target)
if err != nil {
return nil, err
}

stages, err := dockerfile.Parse(d)
if err != nil {
return nil, err
}
if err := dockerfile.ValidateTarget(stages, k.Target); err != nil {
return nil, err
}
dockerfile.ResolveStages(stages)

hasher, err := getHasher(k.SnapshotMode)
if err != nil {
return nil, err
}
for index, stage := range stages {
finalStage := (index == len(stages)-1) || (k.Target == stage.Name)
finalStage := finalStage(index, k.Target, stages)
// Unpack file system to root
sourceImage, err := util.RetrieveSourceImage(index, k.Args, stages)
if err != nil {
return nil, err
}
if err := util.GetFSFromImage(sourceImage); err != nil {
if err := util.GetFSFromImage(constants.RootDir, sourceImage); err != nil {
return nil, err
}
l := snapshot.NewLayeredMap(hasher)
Expand Down Expand Up @@ -168,11 +159,13 @@ func DoBuild(k KanikoBuildArgs) (v1.Image, error) {
}
return sourceImage, nil
}
if err := saveStageAsTarball(index, sourceImage); err != nil {
return nil, err
}
if err := saveStageDependencies(index, stages, buildArgs.Clone()); err != nil {
return nil, err
if dockerfile.SaveStage(index, stages) {
if err := saveStageAsTarball(index, sourceImage); err != nil {
return nil, err
}
if err := extractImageToDependecyDir(index, sourceImage); err != nil {
return nil, err
}
}
// Delete the filesystem
if err := util.DeleteFilesystem(); err != nil {
Expand Down Expand Up @@ -225,44 +218,24 @@ func DoPush(image v1.Image, destinations []string, tarPath string) error {
}
return nil
}
func saveStageDependencies(index int, stages []instructions.Stage, buildArgs *dockerfile.BuildArgs) error {
// First, get the files in this stage later stages will need
dependencies, err := dockerfile.Dependencies(index, stages, buildArgs)
logrus.Infof("saving dependencies %s", dependencies)
if err != nil {
return err

func finalStage(index int, target string, stages []instructions.Stage) bool {
if index == len(stages)-1 {
return true
}
if len(dependencies) == 0 {
return nil
if target == "" {
return false
}
// Then, create the directory they will exist in
i := strconv.Itoa(index)
dependencyDir := filepath.Join(constants.KanikoDir, i)
return target == stages[index].Name
}

func extractImageToDependecyDir(index int, image v1.Image) error {
dependencyDir := filepath.Join(constants.KanikoDir, strconv.Itoa(index))
if err := os.MkdirAll(dependencyDir, 0755); err != nil {
return err
}
// Now, copy over dependencies to this dir
for _, d := range dependencies {
fi, err := os.Lstat(d)
if err != nil {
return err
}
dest := filepath.Join(dependencyDir, d)
if fi.IsDir() {
if err := util.CopyDir(d, dest); err != nil {
return err
}
} else if fi.Mode()&os.ModeSymlink != 0 {
if err := util.CopySymlink(d, dest); err != nil {
return err
}
} else {
if err := util.CopyFile(d, dest); err != nil {
return err
}
}
}
return nil
logrus.Infof("trying to extract to %s", dependencyDir)
return util.GetFSFromImage(dependencyDir, image)
}

func saveStageAsTarball(stageIndex int, image v1.Image) error {
Expand Down
Loading