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

Save each stage in multistage dockerfiles as a tarball #244

Merged
merged 7 commits into from
Jul 19, 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
4 changes: 2 additions & 2 deletions cmd/executor/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ var RootCmd = &cobra.Command{
logrus.Error(err)
os.Exit(1)
}
ref, image, err := executor.DoBuild(executor.KanikoBuildArgs{
image, err := executor.DoBuild(executor.KanikoBuildArgs{
DockerfilePath: absouteDockerfilePath(),
SrcContext: srcContext,
SnapshotMode: snapshotMode,
Expand All @@ -98,7 +98,7 @@ var RootCmd = &cobra.Command{
os.Exit(1)
}

if err := executor.DoPush(ref, image, destinations, tarPath); err != nil {
if err := executor.DoPush(image, destinations, tarPath); err != nil {
logrus.Error(err)
os.Exit(1)
}
Expand Down
4 changes: 2 additions & 2 deletions integration/dockerfiles/Dockerfile_test_multistage
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
FROM gcr.io/distroless/base:latest
FROM gcr.io/distroless/base:latest as base
COPY . .

FROM scratch as second
ENV foopath context/foo
COPY --from=0 $foopath context/b* /foo/

FROM gcr.io/distroless/base:latest
FROM base
ARG file
COPY --from=second /foo $file
4 changes: 4 additions & 0 deletions pkg/constants/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ const (
// for example, a tarball from a GCS bucket will be unpacked here
BuildContextDir = "/kaniko/buildcontext/"

// KanikoIntermediateStagesDir is where we will store intermediate stages
// as tarballs in case they are needed later on
KanikoIntermediateStagesDir = "/kaniko/stages"

// Various snapshot modes:
SnapshotModeTime = "time"
SnapshotModeFull = "full"
Expand Down
36 changes: 7 additions & 29 deletions pkg/dockerfile/dockerfile.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,13 @@ package dockerfile

import (
"bytes"
"net/http"
"path/filepath"
"strconv"
"strings"

"github.com/GoogleContainerTools/kaniko/pkg/constants"
"github.com/GoogleContainerTools/kaniko/pkg/util"
"github.com/docker/docker/builder/dockerfile/instructions"
"github.com/docker/docker/builder/dockerfile/parser"
"github.com/google/go-containerregistry/pkg/authn"
"github.com/google/go-containerregistry/pkg/name"
"github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/empty"
"github.com/google/go-containerregistry/pkg/v1/remote"
"path/filepath"
"strconv"
"strings"
)

// Parse parses the contents of a Dockerfile and returns a list of commands
Expand Down Expand Up @@ -89,29 +82,14 @@ func ParseCommands(cmdArray []string) ([]instructions.Command, error) {

// 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) {
var dependencies []string
dependencies := []string{}
for stageIndex, stage := range stages {
if stageIndex <= index {
continue
}
var sourceImage v1.Image
if stage.BaseName == constants.NoBaseImage {
sourceImage = empty.Image
} else {
// Initialize source image
ref, err := name.ParseReference(stage.BaseName, name.WeakValidation)
if err != nil {
return nil, err

}
auth, err := authn.DefaultKeychain.Resolve(ref.Context().Registry)
if err != nil {
return nil, err
}
sourceImage, err = remote.Image(ref, remote.WithAuth(auth), remote.WithTransport(http.DefaultTransport))
if err != nil {
return nil, err
}
sourceImage, err := util.RetrieveSourceImage(stageIndex, buildArgs.ReplacementEnvs(nil), stages)
if err != nil {
return nil, err
}
imageConfig, err := sourceImage.ConfigFile()
if err != nil {
Expand Down
4 changes: 2 additions & 2 deletions pkg/dockerfile/dockerfile_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ func Test_Dependencies(t *testing.T) {
helloPath,
testDir,
},
nil,
{},
}

for index := range stages {
Expand Down Expand Up @@ -125,7 +125,7 @@ func Test_DependenciesWithArg(t *testing.T) {
helloPath,
testDir,
},
nil,
{},
}
buildArgs := NewBuildArgs([]string{fmt.Sprintf("hienv=%s", helloPath)})

Expand Down
100 changes: 47 additions & 53 deletions pkg/executor/executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ import (
"strconv"

"github.com/GoogleContainerTools/kaniko/pkg/snapshot"

"github.com/google/go-containerregistry/pkg/authn"
"github.com/google/go-containerregistry/pkg/name"
"github.com/google/go-containerregistry/pkg/v1"
Expand Down Expand Up @@ -55,94 +54,75 @@ type KanikoBuildArgs struct {
Reproducible bool
}

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

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

hasher, err := getHasher(k.SnapshotMode)
if err != nil {
return nil, nil, err
return nil, err
}
for index, stage := range stages {
baseImage, err := util.ResolveEnvironmentReplacement(stage.BaseName, k.Args, false)
if err != nil {
return nil, nil, err
}
finalStage := index == len(stages)-1
// Unpack file system to root
logrus.Infof("Unpacking filesystem of %s...", baseImage)
var sourceImage v1.Image
var ref name.Reference
if baseImage == constants.NoBaseImage {
logrus.Info("No base image, nothing to extract")
sourceImage = empty.Image
} else {
// Initialize source image
ref, err = name.ParseReference(baseImage, name.WeakValidation)
if err != nil {
return nil, nil, err
}
auth, err := authn.DefaultKeychain.Resolve(ref.Context().Registry)
if err != nil {
return nil, nil, err
}
sourceImage, err = remote.Image(ref, remote.WithAuth(auth), remote.WithTransport(http.DefaultTransport))
if err != nil {
return nil, nil, err
}
sourceImage, err := util.RetrieveSourceImage(index, k.Args, stages)
if err != nil {
return nil, err
}
if err := util.GetFSFromImage(sourceImage); err != nil {
return nil, nil, err
return nil, err
}
l := snapshot.NewLayeredMap(hasher)
snapshotter := snapshot.NewSnapshotter(l, constants.RootDir)
// Take initial snapshot
if err := snapshotter.Init(); err != nil {
return nil, nil, err
return nil, err
}
imageConfig, err := sourceImage.ConfigFile()
if baseImage == constants.NoBaseImage {
if sourceImage == empty.Image {
imageConfig.Config.Env = constants.ScratchEnvVars
}
if err != nil {
return nil, nil, err
return nil, err
}
if err := resolveOnBuild(&stage, &imageConfig.Config); err != nil {
return nil, nil, err
return nil, err
}
buildArgs := dockerfile.NewBuildArgs(k.Args)
for index, cmd := range stage.Commands {
finalCmd := index == len(stage.Commands)-1
dockerCommand, err := commands.GetCommand(cmd, k.SrcContext)
if err != nil {
return nil, nil, err
return nil, err
}
if dockerCommand == nil {
continue
}
if err := dockerCommand.ExecuteCommand(&imageConfig.Config, buildArgs); err != nil {
return nil, nil, err
return nil, err
}
if !finalStage || (k.SingleSnapshot && !finalCmd) {
// Don't snapshot if it's not the final stage and not the final command
// Also don't snapshot if it's the final stage, not the final command, and single snapshot is set
if (!finalStage && !finalCmd) || (finalStage && !finalCmd && k.SingleSnapshot) {
continue
}
// Now, we get the files to snapshot from this command and take the snapshot
snapshotFiles := dockerCommand.FilesToSnapshot()
if k.SingleSnapshot && finalCmd {
if finalCmd {
snapshotFiles = nil
}
contents, err := snapshotter.TakeSnapshot(snapshotFiles)
if err != nil {
return nil, nil, err
return nil, err
}
util.MoveVolumeWhitelistToWhitelist()
if contents == nil {
Expand All @@ -155,7 +135,7 @@ func DoBuild(k KanikoBuildArgs) (name.Reference, v1.Image, error) {
}
layer, err := tarball.LayerFromOpener(opener)
if err != nil {
return nil, nil, err
return nil, err
}
sourceImage, err = mutate.Append(sourceImage,
mutate.Addendum{
Expand All @@ -167,36 +147,37 @@ func DoBuild(k KanikoBuildArgs) (name.Reference, v1.Image, error) {
},
)
if err != nil {
return nil, nil, err
return nil, err
}
}
sourceImage, err = mutate.Config(sourceImage, imageConfig.Config)
if err != nil {
return nil, err
}
if finalStage {
sourceImage, err = mutate.Config(sourceImage, imageConfig.Config)
if err != nil {
return nil, nil, err
}

if k.Reproducible {
sourceImage, err = mutate.Canonical(sourceImage)
if err != nil {
return nil, nil, err
return nil, err
}
}

return ref, sourceImage, nil
return sourceImage, nil
}
if err := saveStageAsTarball(index, sourceImage); err != nil {
return nil, err
}
if err := saveStageDependencies(index, stages, buildArgs.Clone()); err != nil {
return nil, nil, err
return nil, err
}
// Delete the filesystem
if err := util.DeleteFilesystem(); err != nil {
return nil, nil, err
return nil, err
}
}
return nil, nil, err
return nil, err
}

func DoPush(ref name.Reference, image v1.Image, destinations []string, tarPath string) error {
func DoPush(image v1.Image, destinations []string, tarPath string) error {
// continue pushing unless an error occurs
for _, destination := range destinations {
// Push the image
Expand Down Expand Up @@ -263,6 +244,19 @@ func saveStageDependencies(index int, stages []instructions.Stage, buildArgs *do
return nil
}

func saveStageAsTarball(stageIndex int, image v1.Image) error {
destRef, err := name.NewTag("temp/tag", name.WeakValidation)
if err != nil {
return err
}
if err := os.MkdirAll(constants.KanikoIntermediateStagesDir, 0750); err != nil {
return err
}
tarPath := filepath.Join(constants.KanikoIntermediateStagesDir, strconv.Itoa(stageIndex))
logrus.Infof("Storing source image from stage %d at path %s", stageIndex, tarPath)
return tarball.WriteToFile(tarPath, destRef, image, nil)
}

func getHasher(snapshotMode string) (func(string) (string, error), error) {
if snapshotMode == constants.SnapshotModeTime {
logrus.Info("Only file modification time will be considered when snapshotting")
Expand Down
Loading