Merge pull request #141 from priyawadhwa/multistage
Support multi stage builds
This commit is contained in:
commit
cde277c374
|
|
@ -21,11 +21,10 @@ import (
|
|||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/GoogleContainerTools/kaniko/pkg/executor"
|
||||
"github.com/genuinetools/amicontained/container"
|
||||
|
||||
"github.com/GoogleContainerTools/kaniko/pkg/constants"
|
||||
"github.com/GoogleContainerTools/kaniko/pkg/executor"
|
||||
"github.com/GoogleContainerTools/kaniko/pkg/util"
|
||||
"github.com/genuinetools/amicontained/container"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
|
@ -78,7 +77,12 @@ var RootCmd = &cobra.Command{
|
|||
logrus.Error(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if err := executor.DoBuild(dockerfilePath, srcContext, destination, snapshotMode, dockerInsecureSkipTLSVerify, buildArgs); err != nil {
|
||||
ref, image, err := executor.DoBuild(dockerfilePath, srcContext, snapshotMode, buildArgs)
|
||||
if err != nil {
|
||||
logrus.Error(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if err := executor.DoPush(ref, image, destination); err != nil {
|
||||
logrus.Error(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
FROM gcr.io/distroless/base:latest
|
||||
COPY . .
|
||||
|
||||
FROM scratch as second
|
||||
ENV foopath context/foo
|
||||
COPY --from=0 $foopath context/b* /foo/
|
||||
|
||||
FROM gcr.io/distroless/base:latest
|
||||
ARG file
|
||||
COPY --from=second /foo $file
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
[
|
||||
{
|
||||
"Image1": "gcr.io/kaniko-test/docker-test-multistage:latest",
|
||||
"Image2": "gcr.io/kaniko-test/kaniko-test-multistage:latest",
|
||||
"DiffType": "File",
|
||||
"Diff": {
|
||||
"Adds": null,
|
||||
"Dels": null,
|
||||
"Mods": null
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
@ -166,6 +166,17 @@ var fileTests = []struct {
|
|||
"file3=context/b*",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "test multistage",
|
||||
dockerfilePath: "/workspace/integration_tests/dockerfiles/Dockerfile_test_multistage",
|
||||
configPath: "/workspace/integration_tests/dockerfiles/config_test_multistage.json",
|
||||
dockerContext: buildcontextPath,
|
||||
kanikoContext: buildcontextPath,
|
||||
repo: "test-multistage",
|
||||
args: []string{
|
||||
"file=/foo2",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var structureTests = []struct {
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ func (a *AddCommand) ExecuteCommand(config *v1.Config, buildArgs *dockerfile.Bui
|
|||
logrus.Infof("dest: %s", dest)
|
||||
|
||||
// First, resolve any environment replacement
|
||||
replacementEnvs := util.ReplacementEnvs(config, buildArgs)
|
||||
replacementEnvs := buildArgs.ReplacementEnvs(config.Env)
|
||||
resolvedEnvs, err := util.ResolveEnvironmentReplacementList(a.cmd.SourcesAndDest, replacementEnvs, true)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
|
|||
|
|
@ -42,7 +42,11 @@ func (c *CopyCommand) ExecuteCommand(config *v1.Config, buildArgs *dockerfile.Bu
|
|||
logrus.Infof("cmd: copy %s", srcs)
|
||||
logrus.Infof("dest: %s", dest)
|
||||
|
||||
replacementEnvs := util.ReplacementEnvs(config, buildArgs)
|
||||
// Resolve from
|
||||
if c.cmd.From != "" {
|
||||
c.buildcontext = filepath.Join(constants.KanikoDir, c.cmd.From)
|
||||
}
|
||||
replacementEnvs := buildArgs.ReplacementEnvs(config.Env)
|
||||
// First, resolve any environment replacement
|
||||
resolvedEnvs, err := util.ResolveEnvironmentReplacementList(c.cmd.SourcesAndDest, replacementEnvs, true)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -33,57 +33,8 @@ type EnvCommand struct {
|
|||
func (e *EnvCommand) ExecuteCommand(config *v1.Config, buildArgs *dockerfile.BuildArgs) error {
|
||||
logrus.Info("cmd: ENV")
|
||||
newEnvs := e.cmd.Env
|
||||
replacementEnvs := util.ReplacementEnvs(config, buildArgs)
|
||||
for index, pair := range newEnvs {
|
||||
expandedKey, err := util.ResolveEnvironmentReplacement(pair.Key, replacementEnvs, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
expandedValue, err := util.ResolveEnvironmentReplacement(pair.Value, replacementEnvs, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
newEnvs[index] = instructions.KeyValuePair{
|
||||
Key: expandedKey,
|
||||
Value: expandedValue,
|
||||
}
|
||||
}
|
||||
return updateConfigEnv(newEnvs, config)
|
||||
}
|
||||
|
||||
func updateConfigEnv(newEnvs []instructions.KeyValuePair, config *v1.Config) error {
|
||||
// First, convert config.Env array to []instruction.KeyValuePair
|
||||
var kvps []instructions.KeyValuePair
|
||||
for _, env := range config.Env {
|
||||
entry := strings.Split(env, "=")
|
||||
kvps = append(kvps, instructions.KeyValuePair{
|
||||
Key: entry[0],
|
||||
Value: entry[1],
|
||||
})
|
||||
}
|
||||
// Iterate through new environment variables, and replace existing keys
|
||||
// We can't use a map because we need to preserve the order of the environment variables
|
||||
Loop:
|
||||
for _, newEnv := range newEnvs {
|
||||
for index, kvp := range kvps {
|
||||
// If key exists, replace the KeyValuePair...
|
||||
if kvp.Key == newEnv.Key {
|
||||
logrus.Debugf("Replacing environment variable %v with %v in config", kvp, newEnv)
|
||||
kvps[index] = newEnv
|
||||
continue Loop
|
||||
}
|
||||
}
|
||||
// ... Else, append it as a new env variable
|
||||
kvps = append(kvps, newEnv)
|
||||
}
|
||||
// Convert back to array and set in config
|
||||
envArray := []string{}
|
||||
for _, kvp := range kvps {
|
||||
entry := kvp.Key + "=" + kvp.Value
|
||||
envArray = append(envArray, entry)
|
||||
}
|
||||
config.Env = envArray
|
||||
return nil
|
||||
replacementEnvs := buildArgs.ReplacementEnvs(config.Env)
|
||||
return util.UpdateConfigEnv(newEnvs, config, replacementEnvs)
|
||||
}
|
||||
|
||||
// We know that no files have changed, so return an empty array
|
||||
|
|
|
|||
|
|
@ -23,37 +23,6 @@ import (
|
|||
"testing"
|
||||
)
|
||||
|
||||
func TestUpdateEnvConfig(t *testing.T) {
|
||||
cfg := &v1.Config{
|
||||
Env: []string{
|
||||
"PATH=/path/to/dir",
|
||||
"hey=hey",
|
||||
},
|
||||
}
|
||||
|
||||
newEnvs := []instructions.KeyValuePair{
|
||||
{
|
||||
Key: "foo",
|
||||
Value: "foo2",
|
||||
},
|
||||
{
|
||||
Key: "PATH",
|
||||
Value: "/new/path/",
|
||||
},
|
||||
{
|
||||
Key: "foo",
|
||||
Value: "newfoo",
|
||||
},
|
||||
}
|
||||
|
||||
expectedEnvArray := []string{
|
||||
"PATH=/new/path/",
|
||||
"hey=hey",
|
||||
"foo=newfoo",
|
||||
}
|
||||
updateConfigEnv(newEnvs, cfg)
|
||||
testutil.CheckErrorAndDeepEqual(t, false, nil, expectedEnvArray, cfg.Env)
|
||||
}
|
||||
func Test_EnvExecute(t *testing.T) {
|
||||
cfg := &v1.Config{
|
||||
Env: []string{
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ func (r *ExposeCommand) ExecuteCommand(config *v1.Config, buildArgs *dockerfile.
|
|||
if existingPorts == nil {
|
||||
existingPorts = make(map[string]struct{})
|
||||
}
|
||||
replacementEnvs := util.ReplacementEnvs(config, buildArgs)
|
||||
replacementEnvs := buildArgs.ReplacementEnvs(config.Env)
|
||||
// Add any new ones in
|
||||
for _, p := range r.cmd.Ports {
|
||||
// Resolve any environment variables
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ func updateLabels(labels []instructions.KeyValuePair, config *v1.Config, buildAr
|
|||
existingLabels = make(map[string]string)
|
||||
}
|
||||
// Let's unescape values before setting the label
|
||||
replacementEnvs := util.ReplacementEnvs(config, buildArgs)
|
||||
replacementEnvs := buildArgs.ReplacementEnvs(config.Env)
|
||||
for index, kvp := range labels {
|
||||
key, err := util.ResolveEnvironmentReplacement(kvp.Key, replacementEnvs, false)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ type OnBuildCommand struct {
|
|||
func (o *OnBuildCommand) ExecuteCommand(config *v1.Config, buildArgs *dockerfile.BuildArgs) error {
|
||||
logrus.Info("cmd: ONBUILD")
|
||||
logrus.Infof("args: %s", o.cmd.Expression)
|
||||
replacementEnvs := util.ReplacementEnvs(config, buildArgs)
|
||||
replacementEnvs := buildArgs.ReplacementEnvs(config.Env)
|
||||
resolvedExpression, err := util.ResolveEnvironmentReplacement(o.cmd.Expression, replacementEnvs, false)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ package commands
|
|||
|
||||
import (
|
||||
"github.com/GoogleContainerTools/kaniko/pkg/dockerfile"
|
||||
"github.com/GoogleContainerTools/kaniko/pkg/util"
|
||||
"github.com/docker/docker/builder/dockerfile/instructions"
|
||||
"github.com/google/go-containerregistry/v1"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
|
@ -55,7 +54,7 @@ func (r *RunCommand) ExecuteCommand(config *v1.Config, buildArgs *dockerfile.Bui
|
|||
cmd := exec.Command(newCommand[0], newCommand[1:]...)
|
||||
cmd.Dir = config.WorkingDir
|
||||
cmd.Stdout = os.Stdout
|
||||
replacementEnvs := util.ReplacementEnvs(config, buildArgs)
|
||||
replacementEnvs := buildArgs.ReplacementEnvs(config.Env)
|
||||
cmd.Env = replacementEnvs
|
||||
|
||||
// If specified, run the command as a specific user
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ func (s *StopSignalCommand) ExecuteCommand(config *v1.Config, buildArgs *dockerf
|
|||
logrus.Info("cmd: STOPSIGNAL")
|
||||
|
||||
// resolve possible environment variables
|
||||
replacementEnvs := util.ReplacementEnvs(config, buildArgs)
|
||||
replacementEnvs := buildArgs.ReplacementEnvs(config.Env)
|
||||
resolvedEnvs, err := util.ResolveEnvironmentReplacementList([]string{s.cmd.Signal}, replacementEnvs, false)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ func (r *UserCommand) ExecuteCommand(config *v1.Config, buildArgs *dockerfile.Bu
|
|||
logrus.Info("cmd: USER")
|
||||
u := r.cmd.User
|
||||
userAndGroup := strings.Split(u, ":")
|
||||
replacementEnvs := util.ReplacementEnvs(config, buildArgs)
|
||||
replacementEnvs := buildArgs.ReplacementEnvs(config.Env)
|
||||
userStr, err := util.ResolveEnvironmentReplacement(userAndGroup[0], replacementEnvs, false)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ type VolumeCommand struct {
|
|||
func (v *VolumeCommand) ExecuteCommand(config *v1.Config, buildArgs *dockerfile.BuildArgs) error {
|
||||
logrus.Info("cmd: VOLUME")
|
||||
volumes := v.cmd.Volumes
|
||||
replacementEnvs := util.ReplacementEnvs(config, buildArgs)
|
||||
replacementEnvs := buildArgs.ReplacementEnvs(config.Env)
|
||||
resolvedVolumes, err := util.ResolveEnvironmentReplacementList(volumes, replacementEnvs, true)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ type WorkdirCommand struct {
|
|||
func (w *WorkdirCommand) ExecuteCommand(config *v1.Config, buildArgs *dockerfile.BuildArgs) error {
|
||||
logrus.Info("cmd: workdir")
|
||||
workdirPath := w.cmd.Path
|
||||
replacementEnvs := util.ReplacementEnvs(config, buildArgs)
|
||||
replacementEnvs := buildArgs.ReplacementEnvs(config.Env)
|
||||
resolvedWorkingDir, err := util.ResolveEnvironmentReplacement(workdirPath, replacementEnvs, true)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
|
|||
|
|
@ -39,3 +39,9 @@ func NewBuildArgs(args []string) *BuildArgs {
|
|||
*d.NewBuildArgs(argsFromOptions),
|
||||
}
|
||||
}
|
||||
|
||||
// ReplacementEnvs returns a list of filtered environment variables
|
||||
func (b *BuildArgs) ReplacementEnvs(envs []string) []string {
|
||||
filtered := b.FilterAllowed(envs)
|
||||
return append(envs, filtered...)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,10 +18,19 @@ package dockerfile
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"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/authn"
|
||||
"github.com/google/go-containerregistry/name"
|
||||
"github.com/google/go-containerregistry/v1"
|
||||
"github.com/google/go-containerregistry/v1/empty"
|
||||
"github.com/google/go-containerregistry/v1/remote"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Parse parses the contents of a Dockerfile and returns a list of commands
|
||||
|
|
@ -37,6 +46,28 @@ func Parse(b []byte) ([]instructions.Stage, error) {
|
|||
return stages, err
|
||||
}
|
||||
|
||||
// ResolveStages resolves any calls to previous stages with names to indices
|
||||
// Ex. --from=second_stage should be --from=1 for easier processing later on
|
||||
func ResolveStages(stages []instructions.Stage) {
|
||||
nameToIndex := make(map[string]string)
|
||||
for i, stage := range stages {
|
||||
index := strconv.Itoa(i)
|
||||
if stage.Name != index {
|
||||
nameToIndex[stage.Name] = index
|
||||
}
|
||||
for _, cmd := range stage.Commands {
|
||||
switch c := cmd.(type) {
|
||||
case *instructions.CopyCommand:
|
||||
if c.From != "" {
|
||||
if val, ok := nameToIndex[c.From]; ok {
|
||||
c.From = val
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ParseCommands parses an array of commands into an array of instructions.Command; used for onbuild
|
||||
func ParseCommands(cmdArray []string) ([]instructions.Command, error) {
|
||||
var cmds []instructions.Command
|
||||
|
|
@ -54,3 +85,69 @@ 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) {
|
||||
var 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, auth, http.DefaultTransport)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
imageConfig, err := sourceImage.ConfigFile()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
dependencies = append(dependencies, srcs...)
|
||||
}
|
||||
}
|
||||
}
|
||||
return dependencies, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,136 @@
|
|||
/*
|
||||
Copyright 2018 Google LLC
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package dockerfile
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/GoogleContainerTools/kaniko/testutil"
|
||||
"github.com/docker/docker/builder/dockerfile/instructions"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test_ResolveStages(t *testing.T) {
|
||||
dockerfile := `
|
||||
FROM scratch
|
||||
RUN echo hi > /hi
|
||||
|
||||
FROM scratch AS second
|
||||
COPY --from=0 /hi /hi2
|
||||
|
||||
FROM scratch
|
||||
COPY --from=second /hi2 /hi3
|
||||
`
|
||||
stages, err := Parse([]byte(dockerfile))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ResolveStages(stages)
|
||||
for index, stage := range stages {
|
||||
if index == 0 {
|
||||
continue
|
||||
}
|
||||
copyCmd := stage.Commands[0].(*instructions.CopyCommand)
|
||||
expectedStage := strconv.Itoa(index - 1)
|
||||
if copyCmd.From != expectedStage {
|
||||
t.Fatalf("unexpected copy command: %s resolved to stage %s, expected %s", copyCmd.String(), copyCmd.From, expectedStage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Test_Dependencies(t *testing.T) {
|
||||
testDir, 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)
|
||||
}
|
||||
|
||||
dockerfile := fmt.Sprintf(`
|
||||
FROM scratch
|
||||
COPY %s %s
|
||||
|
||||
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,
|
||||
},
|
||||
nil,
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
helloPath := filepath.Join(testDir, "hello")
|
||||
if err := os.Mkdir(helloPath, 0755); err != nil {
|
||||
t.Fatal(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))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
expectedDependencies := [][]string{
|
||||
{
|
||||
helloPath,
|
||||
testDir,
|
||||
},
|
||||
nil,
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
@ -19,10 +19,12 @@ package executor
|
|||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/GoogleContainerTools/kaniko/pkg/snapshot"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
|
||||
"github.com/google/go-containerregistry/v1/empty"
|
||||
|
||||
|
|
@ -37,99 +39,90 @@ import (
|
|||
"github.com/GoogleContainerTools/kaniko/pkg/commands"
|
||||
"github.com/GoogleContainerTools/kaniko/pkg/constants"
|
||||
"github.com/GoogleContainerTools/kaniko/pkg/dockerfile"
|
||||
"github.com/GoogleContainerTools/kaniko/pkg/image"
|
||||
"github.com/GoogleContainerTools/kaniko/pkg/snapshot"
|
||||
"github.com/GoogleContainerTools/kaniko/pkg/util"
|
||||
"github.com/docker/docker/builder/dockerfile/instructions"
|
||||
"github.com/sirupsen/logrus"
|
||||
"io/ioutil"
|
||||
)
|
||||
|
||||
func DoBuild(dockerfilePath, srcContext, destination, snapshotMode string, dockerInsecureSkipTLSVerify bool, args []string) error {
|
||||
func DoBuild(dockerfilePath, srcContext, snapshotMode string, args []string) (name.Reference, v1.Image, error) {
|
||||
// Parse dockerfile and unpack base image to root
|
||||
d, err := ioutil.ReadFile(dockerfilePath)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
stages, err := dockerfile.Parse(d)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
baseImage := stages[0].BaseName
|
||||
|
||||
// Unpack file system to root
|
||||
var sourceImage v1.Image
|
||||
var ref name.Reference
|
||||
logrus.Infof("Unpacking filesystem of %s...", baseImage)
|
||||
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 err
|
||||
}
|
||||
auth, err := authn.DefaultKeychain.Resolve(ref.Context().Registry)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sourceImage, err = remote.Image(ref, auth, http.DefaultTransport)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := util.GetFSFromImage(sourceImage); err != nil {
|
||||
return err
|
||||
return nil, nil, err
|
||||
}
|
||||
dockerfile.ResolveStages(stages)
|
||||
|
||||
hasher, err := getHasher(snapshotMode)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, nil, err
|
||||
}
|
||||
l := snapshot.NewLayeredMap(hasher)
|
||||
snapshotter := snapshot.NewSnapshotter(l, constants.RootDir)
|
||||
|
||||
// Take initial snapshot
|
||||
if err := snapshotter.Init(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
destRef, err := name.ParseReference(destination, name.WeakValidation)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Set environment variables within the image
|
||||
if err := image.SetEnvVariables(sourceImage); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
imageConfig, err := sourceImage.ConfigFile()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
buildArgs := dockerfile.NewBuildArgs(args)
|
||||
// Currently only supports single stage builds
|
||||
for _, stage := range stages {
|
||||
if err := resolveOnBuild(&stage, &imageConfig.Config); err != nil {
|
||||
return err
|
||||
for index, stage := range stages {
|
||||
baseImage := stage.BaseName
|
||||
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, auth, http.DefaultTransport)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
if err := util.GetFSFromImage(sourceImage); err != nil {
|
||||
return nil, 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
|
||||
}
|
||||
imageConfig, err := sourceImage.ConfigFile()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if err := resolveOnBuild(&stage, &imageConfig.Config); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
buildArgs := dockerfile.NewBuildArgs(args)
|
||||
for _, cmd := range stage.Commands {
|
||||
dockerCommand, err := commands.GetCommand(cmd, srcContext)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, nil, err
|
||||
}
|
||||
if dockerCommand == nil {
|
||||
continue
|
||||
}
|
||||
if err := dockerCommand.ExecuteCommand(&imageConfig.Config, buildArgs); err != nil {
|
||||
return err
|
||||
return nil, nil, err
|
||||
}
|
||||
if !finalStage {
|
||||
continue
|
||||
}
|
||||
// Now, we get the files to snapshot from this command and take the snapshot
|
||||
snapshotFiles := dockerCommand.FilesToSnapshot()
|
||||
contents, err := snapshotter.TakeSnapshot(snapshotFiles)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, nil, err
|
||||
}
|
||||
util.MoveVolumeWhitelistToWhitelist()
|
||||
if contents == nil {
|
||||
|
|
@ -142,7 +135,7 @@ func DoBuild(dockerfilePath, srcContext, destination, snapshotMode string, docke
|
|||
}
|
||||
layer, err := tarball.LayerFromOpener(opener)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, nil, err
|
||||
}
|
||||
sourceImage, err = mutate.Append(sourceImage,
|
||||
mutate.Addendum{
|
||||
|
|
@ -154,15 +147,33 @@ func DoBuild(dockerfilePath, srcContext, destination, snapshotMode string, docke
|
|||
},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
if finalStage {
|
||||
sourceImage, err = mutate.Config(sourceImage, imageConfig.Config)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return ref, sourceImage, nil
|
||||
}
|
||||
if err := saveStageDependencies(index, stages, buildArgs); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
// Delete the filesystem
|
||||
if err := util.DeleteFilesystem(); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
func DoPush(ref name.Reference, image v1.Image, destination string) error {
|
||||
// Push the image
|
||||
if err := setDefaultEnv(); err != nil {
|
||||
destRef, err := name.ParseReference(destination, name.WeakValidation)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
wo := remote.WriteOptions{}
|
||||
if ref != nil {
|
||||
wo.MountPaths = []name.Repository{ref.Context()}
|
||||
|
|
@ -171,12 +182,43 @@ func DoBuild(dockerfilePath, srcContext, destination, snapshotMode string, docke
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sourceImage, err = mutate.Config(sourceImage, imageConfig.Config)
|
||||
return remote.Write(destRef, image, pushAuth, http.DefaultTransport, wo)
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
return remote.Write(destRef, sourceImage, pushAuth, http.DefaultTransport, wo)
|
||||
// Then, create the directory they will exist in
|
||||
i := strconv.Itoa(index)
|
||||
dependencyDir := filepath.Join(constants.KanikoDir, i)
|
||||
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
|
||||
}
|
||||
|
||||
func getHasher(snapshotMode string) (func(string) (string, error), error) {
|
||||
|
|
@ -204,18 +246,3 @@ func resolveOnBuild(stage *instructions.Stage, config *v1.Config) error {
|
|||
logrus.Infof("Executing %v build triggers", len(cmds))
|
||||
return nil
|
||||
}
|
||||
|
||||
// setDefaultEnv sets default values for HOME and PATH so that
|
||||
// config.json and docker-credential-gcr can be accessed
|
||||
func setDefaultEnv() error {
|
||||
defaultEnvs := map[string]string{
|
||||
"HOME": "/root",
|
||||
"PATH": "/usr/local/bin/",
|
||||
}
|
||||
for key, val := range defaultEnvs {
|
||||
if err := os.Setenv(key, val); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@ limitations under the License.
|
|||
package util
|
||||
|
||||
import (
|
||||
"github.com/GoogleContainerTools/kaniko/pkg/dockerfile"
|
||||
"github.com/docker/docker/builder/dockerfile/instructions"
|
||||
"github.com/docker/docker/builder/dockerfile/parser"
|
||||
"github.com/docker/docker/builder/dockerfile/shell"
|
||||
|
|
@ -180,7 +179,7 @@ func IsSrcsValid(srcsAndDest instructions.SourcesAndDest, resolvedSources []stri
|
|||
}
|
||||
|
||||
if len(resolvedSources) == 1 {
|
||||
fi, err := os.Stat(filepath.Join(root, resolvedSources[0]))
|
||||
fi, err := os.Lstat(filepath.Join(root, resolvedSources[0]))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -225,9 +224,52 @@ func IsSrcRemoteFileURL(rawurl string) bool {
|
|||
return true
|
||||
}
|
||||
|
||||
// ReplacementEnvs returns a list of all variables that can be used for
|
||||
// environment replacement
|
||||
func ReplacementEnvs(config *v1.Config, buildArgs *dockerfile.BuildArgs) []string {
|
||||
filtered := buildArgs.FilterAllowed(config.Env)
|
||||
return append(config.Env, filtered...)
|
||||
func UpdateConfigEnv(newEnvs []instructions.KeyValuePair, config *v1.Config, replacementEnvs []string) error {
|
||||
for index, pair := range newEnvs {
|
||||
expandedKey, err := ResolveEnvironmentReplacement(pair.Key, replacementEnvs, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
expandedValue, err := ResolveEnvironmentReplacement(pair.Value, replacementEnvs, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
newEnvs[index] = instructions.KeyValuePair{
|
||||
Key: expandedKey,
|
||||
Value: expandedValue,
|
||||
}
|
||||
}
|
||||
|
||||
// First, convert config.Env array to []instruction.KeyValuePair
|
||||
var kvps []instructions.KeyValuePair
|
||||
for _, env := range config.Env {
|
||||
entry := strings.Split(env, "=")
|
||||
kvps = append(kvps, instructions.KeyValuePair{
|
||||
Key: entry[0],
|
||||
Value: entry[1],
|
||||
})
|
||||
}
|
||||
// Iterate through new environment variables, and replace existing keys
|
||||
// We can't use a map because we need to preserve the order of the environment variables
|
||||
Loop:
|
||||
for _, newEnv := range newEnvs {
|
||||
for index, kvp := range kvps {
|
||||
// If key exists, replace the KeyValuePair...
|
||||
if kvp.Key == newEnv.Key {
|
||||
logrus.Debugf("Replacing environment variable %v with %v in config", kvp, newEnv)
|
||||
kvps[index] = newEnv
|
||||
continue Loop
|
||||
}
|
||||
}
|
||||
// ... Else, append it as a new env variable
|
||||
kvps = append(kvps, newEnv)
|
||||
}
|
||||
// Convert back to array and set in config
|
||||
envArray := []string{}
|
||||
for _, kvp := range kvps {
|
||||
entry := kvp.Key + "=" + kvp.Value
|
||||
envArray = append(envArray, entry)
|
||||
}
|
||||
config.Env = envArray
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -110,6 +110,33 @@ func GetFSFromImage(img v1.Image) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// DeleteFilesystem deletes the extracted image file system
|
||||
func DeleteFilesystem() error {
|
||||
logrus.Info("Deleting filesystem...")
|
||||
err := filepath.Walk(constants.RootDir, func(path string, info os.FileInfo, err error) error {
|
||||
if PathInWhitelist(path, constants.RootDir) || ChildDirInWhitelist(path, constants.RootDir) {
|
||||
logrus.Debugf("Not deleting %s, as it's whitelisted", path)
|
||||
return nil
|
||||
}
|
||||
if path == constants.RootDir {
|
||||
return nil
|
||||
}
|
||||
return os.RemoveAll(path)
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// ChildDirInWhitelist returns true if there is a child file or directory of the path in the whitelist
|
||||
func ChildDirInWhitelist(path, directory string) bool {
|
||||
for _, d := range whitelist {
|
||||
dirPath := filepath.Join(directory, d)
|
||||
if HasFilepathPrefix(dirPath, path) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func unTar(r io.Reader, dest string) error {
|
||||
tr := tar.NewReader(r)
|
||||
for {
|
||||
|
|
@ -272,9 +299,15 @@ func RelativeFiles(fp string, root string) ([]string, error) {
|
|||
fullPath := filepath.Join(root, fp)
|
||||
logrus.Debugf("Getting files and contents at root %s", fullPath)
|
||||
err := filepath.Walk(fullPath, func(path string, info os.FileInfo, err error) error {
|
||||
if PathInWhitelist(path, root) {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if PathInWhitelist(path, root) {
|
||||
return nil
|
||||
}
|
||||
relPath, err := filepath.Rel(root, path)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -290,6 +323,9 @@ func Files(root string) ([]string, error) {
|
|||
var files []string
|
||||
logrus.Debugf("Getting files and contents at root %s", root)
|
||||
err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
|
||||
if PathInWhitelist(path, root) {
|
||||
return nil
|
||||
}
|
||||
files = append(files, path)
|
||||
return err
|
||||
})
|
||||
|
|
@ -391,7 +427,7 @@ func CopyDir(src, dest string) error {
|
|||
}
|
||||
for _, file := range files {
|
||||
fullPath := filepath.Join(src, file)
|
||||
fi, err := os.Stat(fullPath)
|
||||
fi, err := os.Lstat(fullPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -103,16 +103,13 @@ var tests = []struct {
|
|||
files: map[string]string{
|
||||
"/workspace/foo/a": "baz1",
|
||||
"/workspace/foo/b": "baz2",
|
||||
"/kaniko/file": "file",
|
||||
},
|
||||
directory: "",
|
||||
expectedFiles: []string{
|
||||
"workspace/foo/a",
|
||||
"workspace/foo/b",
|
||||
"kaniko/file",
|
||||
"workspace",
|
||||
"workspace/foo",
|
||||
"kaniko",
|
||||
".",
|
||||
},
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in New Issue