Merge branch 'master' into log-timestamp-v0.19.0

This commit is contained in:
Tejal Desai
2020-05-03 20:58:34 -07:00
committed by GitHub
1197 changed files with 195315 additions and 89291 deletions
+19 -17
View File
@@ -38,25 +38,27 @@ type BuildContext interface {
// parser
func GetBuildContext(srcContext string) (BuildContext, error) {
split := strings.SplitAfter(srcContext, "://")
prefix := split[0]
context := split[1]
if len(split) > 1 {
prefix := split[0]
context := split[1]
switch prefix {
case constants.GCSBuildContextPrefix:
return &GCS{context: context}, nil
case constants.S3BuildContextPrefix:
return &S3{context: context}, nil
case constants.LocalDirBuildContextPrefix:
return &Dir{context: context}, nil
case constants.GitBuildContextPrefix:
return &Git{context: context}, nil
case constants.HTTPSBuildContextPrefix:
if util.ValidAzureBlobStorageHost(srcContext) {
return &AzureBlob{context: srcContext}, nil
switch prefix {
case constants.GCSBuildContextPrefix:
return &GCS{context: context}, nil
case constants.S3BuildContextPrefix:
return &S3{context: context}, nil
case constants.LocalDirBuildContextPrefix:
return &Dir{context: context}, nil
case constants.GitBuildContextPrefix:
return &Git{context: context}, nil
case constants.HTTPSBuildContextPrefix:
if util.ValidAzureBlobStorageHost(srcContext) {
return &AzureBlob{context: srcContext}, nil
}
return nil, errors.New("url provided for https context is not in a supported format, please use the https url for Azure Blob Storage")
case TarBuildContextPrefix:
return &Tar{context: context}, nil
}
return nil, errors.New("url provided for https context is not in a supported format, please use the https url for Azure Blob Storage")
case TarBuildContextPrefix:
return &Tar{context: context}, nil
}
return nil, errors.New("unknown build context prefix provided, please use one of the following: gs://, dir://, tar://, s3://, git://, https://")
}
+19 -1
View File
@@ -25,6 +25,16 @@ import (
"gopkg.in/src-d/go-git.v4/plumbing"
)
const (
gitPullMethodEnvKey = "GIT_PULL_METHOD"
gitPullMethodHTTPS = "https"
gitPullMethodHTTP = "http"
)
var (
supportedGitPullMethods = map[string]bool{gitPullMethodHTTPS: true, gitPullMethodHTTP: true}
)
// Git unifies calls to download and unpack the build context.
type Git struct {
context string
@@ -35,7 +45,7 @@ func (g *Git) UnpackTarFromBuildContext() (string, error) {
directory := constants.BuildContextDir
parts := strings.Split(g.context, "#")
options := git.CloneOptions{
URL: "https://" + parts[0],
URL: getGitPullMethod() + "://" + parts[0],
Progress: os.Stdout,
}
if len(parts) > 1 {
@@ -44,3 +54,11 @@ func (g *Git) UnpackTarFromBuildContext() (string, error) {
_, err := git.PlainClone(directory, false, &options)
return directory, err
}
func getGitPullMethod() string {
gitPullMethod := os.Getenv(gitPullMethodEnvKey)
if ok := supportedGitPullMethods[gitPullMethod]; !ok {
gitPullMethod = gitPullMethodHTTPS
}
return gitPullMethod
}
+82
View File
@@ -0,0 +1,82 @@
/*
Copyright 2020 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 buildcontext
import (
"os"
"testing"
"github.com/GoogleContainerTools/kaniko/testutil"
)
func TestGetGitPullMethod(t *testing.T) {
tests := []struct {
testName string
setEnv func() (expectedValue string)
}{
{
testName: "noEnv",
setEnv: func() (expectedValue string) {
expectedValue = gitPullMethodHTTPS
return
},
},
{
testName: "emptyEnv",
setEnv: func() (expectedValue string) {
_ = os.Setenv(gitPullMethodEnvKey, "")
expectedValue = gitPullMethodHTTPS
return
},
},
{
testName: "httpEnv",
setEnv: func() (expectedValue string) {
err := os.Setenv(gitPullMethodEnvKey, gitPullMethodHTTP)
if nil != err {
expectedValue = gitPullMethodHTTPS
} else {
expectedValue = gitPullMethodHTTP
}
return
},
},
{
testName: "httpsEnv",
setEnv: func() (expectedValue string) {
_ = os.Setenv(gitPullMethodEnvKey, gitPullMethodHTTPS)
expectedValue = gitPullMethodHTTPS
return
},
},
{
testName: "unknownEnv",
setEnv: func() (expectedValue string) {
_ = os.Setenv(gitPullMethodEnvKey, "unknown")
expectedValue = gitPullMethodHTTPS
return
},
},
}
for _, tt := range tests {
t.Run(tt.testName, func(t *testing.T) {
expectedValue := tt.setEnv()
testutil.CheckDeepEqual(t, expectedValue, getGitPullMethod())
})
}
}
+21
View File
@@ -17,11 +17,15 @@ limitations under the License.
package buildcontext
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"github.com/GoogleContainerTools/kaniko/pkg/constants"
"github.com/GoogleContainerTools/kaniko/pkg/util"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
// Tar unifies calls to download and unpack the build context.
@@ -35,6 +39,23 @@ func (t *Tar) UnpackTarFromBuildContext() (string, error) {
if err := os.MkdirAll(directory, 0750); err != nil {
return "", errors.Wrap(err, "unpacking tar from build context")
}
if t.context == "stdin" {
fi, _ := os.Stdin.Stat()
if (fi.Mode() & os.ModeCharDevice) != 0 {
return "", fmt.Errorf("no data found.. don't forget to add the '--interactive, -i' flag")
}
logrus.Infof("To simulate EOF and exit, press 'Ctrl+D'")
// if launched through docker in interactive mode and without piped data
// process will be stuck here until EOF is sent
data, err := util.GetInputFrom(os.Stdin)
if err != nil {
return "", errors.Wrap(err, "fail to get standard input")
}
t.context = filepath.Join(directory, constants.ContextTar)
if err := ioutil.WriteFile(t.context, data, 0644); err != nil {
return "", errors.Wrap(err, "fail to redirect standard input into compressed tar file")
}
}
return directory, util.UnpackCompressedTar(t.context, directory)
}
-7
View File
@@ -17,7 +17,6 @@ limitations under the License.
package commands
import (
"github.com/GoogleContainerTools/kaniko/pkg/constants"
"github.com/GoogleContainerTools/kaniko/pkg/dockerfile"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/moby/buildkit/frontend/dockerfile/instructions"
@@ -25,12 +24,6 @@ import (
"github.com/sirupsen/logrus"
)
var RootDir string
func init() {
RootDir = constants.RootDir
}
type CurrentCacheKey func() (string, error)
type DockerCommand interface {
+6 -5
View File
@@ -22,7 +22,7 @@ import (
"path/filepath"
"strings"
"github.com/GoogleContainerTools/kaniko/pkg/constants"
kConfig "github.com/GoogleContainerTools/kaniko/pkg/config"
"github.com/moby/buildkit/frontend/dockerfile/instructions"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
@@ -47,7 +47,7 @@ type CopyCommand struct {
func (c *CopyCommand) ExecuteCommand(config *v1.Config, buildArgs *dockerfile.BuildArgs) error {
// Resolve from
if c.cmd.From != "" {
c.buildcontext = filepath.Join(constants.KanikoDir, c.cmd.From)
c.buildcontext = filepath.Join(kConfig.KanikoDir, c.cmd.From)
}
replacementEnvs := buildArgs.ReplacementEnvs(config.Env)
@@ -74,7 +74,7 @@ func (c *CopyCommand) ExecuteCommand(config *v1.Config, buildArgs *dockerfile.Bu
}
cwd := config.WorkingDir
if cwd == "" {
cwd = constants.RootDir
cwd = kConfig.RootDir
}
destPath, err := util.DestinationFilepath(fullPath, dest, cwd)
@@ -191,7 +191,7 @@ func (cr *CachingCopyCommand) ExecuteCommand(config *v1.Config, buildArgs *docke
cr.layer = layers[0]
cr.readSuccess = true
cr.extractedFiles, err = util.GetFSFromLayers(RootDir, layers, util.ExtractFunc(cr.extractFn), util.IncludeWhiteout())
cr.extractedFiles, err = util.GetFSFromLayers(kConfig.RootDir, layers, util.ExtractFunc(cr.extractFn), util.IncludeWhiteout())
logrus.Debugf("extractedFiles: %s", cr.extractedFiles)
if err != nil {
@@ -207,7 +207,8 @@ func (cr *CachingCopyCommand) FilesUsedFromContext(config *v1.Config, buildArgs
func (cr *CachingCopyCommand) FilesToSnapshot() []string {
f := cr.extractedFiles
logrus.Debugf("files extracted by caching copy command %s", f)
logrus.Debugf("%d files extracted by caching copy command", len(f))
logrus.Tracef("Extracted files: %s", f)
return f
}
+39 -1
View File
@@ -405,7 +405,6 @@ func TestCopyCommand_ExecuteCommand_Extended(t *testing.T) {
if err := os.MkdirAll(dir, 0777); err != nil {
t.Fatal(err)
}
file := filepath.Join(dir, "bam.txt")
if err := ioutil.WriteFile(file, []byte("meow"), 0777); err != nil {
@@ -418,6 +417,7 @@ func TestCopyCommand_ExecuteCommand_Extended(t *testing.T) {
if err := os.Symlink("dam.txt", filepath.Join(dir, "sym.link")); err != nil {
t.Fatal(err)
}
return testDir, filepath.Base(dir)
}
@@ -922,4 +922,42 @@ func TestCopyCommand_ExecuteCommand_Extended(t *testing.T) {
testutil.CheckNoError(t, err)
}
})
t.Run("copy src dir with relative symlinks in a dir", func(t *testing.T) {
testDir, srcDir := setupDirs(t)
defer os.RemoveAll(testDir)
// Make another dir inside bar with a relative symlink
dir := filepath.Join(testDir, srcDir, "another")
if err := os.MkdirAll(dir, 0777); err != nil {
t.Fatal(err)
}
os.Symlink("../bam.txt", filepath.Join(dir, "bam_relative.txt"))
dest := filepath.Join(testDir, "copy")
cmd := CopyCommand{
cmd: &instructions.CopyCommand{
SourcesAndDest: []string{srcDir, dest},
},
buildcontext: testDir,
}
cfg := &v1.Config{
Cmd: nil,
Env: []string{},
WorkingDir: testDir,
}
err := cmd.ExecuteCommand(cfg, dockerfile.NewBuildArgs([]string{}))
testutil.CheckNoError(t, err)
actual, err := ioutil.ReadDir(filepath.Join(dest, "another"))
if err != nil {
t.Fatal(err)
}
testutil.CheckDeepEqual(t, "bam_relative.txt", actual[0].Name())
linkName, err := os.Readlink(filepath.Join(dest, "another", "bam_relative.txt"))
if err != nil {
t.Fatal(err)
}
testutil.CheckDeepEqual(t, "../bam.txt", linkName)
})
}
+4 -2
View File
@@ -24,6 +24,7 @@ import (
"strings"
"syscall"
kConfig "github.com/GoogleContainerTools/kaniko/pkg/config"
"github.com/GoogleContainerTools/kaniko/pkg/constants"
"github.com/GoogleContainerTools/kaniko/pkg/dockerfile"
"github.com/GoogleContainerTools/kaniko/pkg/util"
@@ -202,7 +203,7 @@ func (cr *CachingRunCommand) ExecuteCommand(config *v1.Config, buildArgs *docker
cr.readSuccess = true
cr.extractedFiles, err = util.GetFSFromLayers(
constants.RootDir,
kConfig.RootDir,
layers,
util.ExtractFunc(cr.extractFn),
util.IncludeWhiteout(),
@@ -216,7 +217,8 @@ func (cr *CachingRunCommand) ExecuteCommand(config *v1.Config, buildArgs *docker
func (cr *CachingRunCommand) FilesToSnapshot() []string {
f := cr.extractedFiles
logrus.Debugf("files extracted from caching run command %s", f)
logrus.Debugf("%d files extracted by caching run command", len(f))
logrus.Tracef("Extracted files: %s", f)
return f
}
+31
View File
@@ -0,0 +1,31 @@
/*
Copyright 2020 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 config
import (
"github.com/GoogleContainerTools/kaniko/pkg/constants"
)
var RootDir string
var KanikoDir string
var WhitelistPath string
func init() {
RootDir = constants.RootDir
KanikoDir = constants.KanikoDir
WhitelistPath = constants.WhitelistPath
}
+1
View File
@@ -56,6 +56,7 @@ type KanikoOptions struct {
Cache bool
Cleanup bool
WhitelistVarRun bool
SkipUnusedStages bool
}
// WarmerOptions are options that are set by command line arguments to the cache warmer.
+158 -56
View File
@@ -25,6 +25,7 @@ import (
"strconv"
"strings"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/sirupsen/logrus"
"github.com/GoogleContainerTools/kaniko/pkg/config"
@@ -34,15 +35,14 @@ import (
"github.com/pkg/errors"
)
// Stages parses a Dockerfile and returns an array of KanikoStage
func Stages(opts *config.KanikoOptions) ([]config.KanikoStage, error) {
func ParseStages(opts *config.KanikoOptions) ([]instructions.Stage, []instructions.ArgCommand, error) {
var err error
var d []uint8
match, _ := regexp.MatchString("^https?://", opts.DockerfilePath)
if match {
response, e := http.Get(opts.DockerfilePath)
if e != nil {
return nil, e
return nil, nil, e
}
d, err = ioutil.ReadAll(response.Body)
} else {
@@ -50,41 +50,15 @@ func Stages(opts *config.KanikoOptions) ([]config.KanikoStage, error) {
}
if err != nil {
return nil, errors.Wrap(err, fmt.Sprintf("reading dockerfile at path %s", opts.DockerfilePath))
return nil, nil, errors.Wrap(err, fmt.Sprintf("reading dockerfile at path %s", opts.DockerfilePath))
}
stages, metaArgs, err := Parse(d)
if err != nil {
return nil, errors.Wrap(err, "parsing dockerfile")
}
targetStage, err := targetStage(stages, opts.Target)
if err != nil {
return nil, err
}
resolveStages(stages)
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")
}
stage.Name = resolvedBaseName
logrus.Infof("Resolved base name %s to %s", stage.BaseName, stage.Name)
kanikoStages = append(kanikoStages, config.KanikoStage{
Stage: stage,
BaseImageIndex: baseImageIndex(index, stages),
BaseImageStoredLocally: (baseImageIndex(index, stages) != -1),
SaveStage: saveStage(index, stages),
Final: index == targetStage,
MetaArgs: metaArgs,
Index: index,
})
if index == targetStage {
break
}
return nil, nil, errors.Wrap(err, "parsing dockerfile")
}
return kanikoStages, nil
return stages, metaArgs, nil
}
// baseImageIndex returns the index of the stage the current stage is built off
@@ -204,30 +178,6 @@ func targetStage(stages []instructions.Stage, target string) (int, error) {
return -1, fmt.Errorf("%s is not a valid target build stage", target)
}
// resolveStages resolves any calls to previous stages with names to indices
// Ex. --from=second_stage should be --from=1 for easier processing later on
// As third party library lowers stage name in FROM instruction, this function resolves stage case insensitively.
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[strings.ToLower(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
@@ -264,3 +214,155 @@ func saveStage(index int, stages []instructions.Stage) bool {
return false
}
// ResolveCrossStageCommands resolves any calls to previous stages with names to indices
// Ex. --from=secondStage should be --from=1 for easier processing later on
// As third party library lowers stage name in FROM instruction, this function resolves stage case insensitively.
func ResolveCrossStageCommands(cmds []instructions.Command, stageNameToIdx map[string]string) {
for _, cmd := range cmds {
switch c := cmd.(type) {
case *instructions.CopyCommand:
if c.From != "" {
if val, ok := stageNameToIdx[strings.ToLower(c.From)]; ok {
c.From = val
}
}
}
}
}
// 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")
}
if opts.SkipUnusedStages {
stages = skipUnusedStages(stages, &targetStage, opts.Target)
}
var kanikoStages []config.KanikoStage
for index, stage := range stages {
if len(stage.Name) > 0 {
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,
BaseImageStoredLocally: (baseImageIndex != -1),
SaveStage: saveStage(index, stages),
Final: index == targetStage,
MetaArgs: metaArgs,
Index: index,
})
if index == targetStage {
break
}
}
return kanikoStages, nil
}
func GetOnBuildInstructions(config *v1.Config, stageNameToIdx map[string]string) ([]instructions.Command, error) {
if config.OnBuild == nil || len(config.OnBuild) == 0 {
return nil, nil
}
cmds, err := ParseCommands(config.OnBuild)
if err != nil {
return nil, err
}
// Iterate over commands and replace references to other stages with their index
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
}
// skipUnusedStages returns the list of used stages without the unnecessaries ones
func skipUnusedStages(stages []instructions.Stage, lastStageIndex *int, target string) []instructions.Stage {
stagesDependencies := make(map[string]bool)
var onlyUsedStages []instructions.Stage
idx := *lastStageIndex
lastStageBaseName := stages[idx].BaseName
for i := idx; i >= 0; i-- {
s := stages[i]
if (s.Name != "" && stagesDependencies[s.Name]) || s.Name == lastStageBaseName || i == idx {
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 != idx {
stagesDependencies[s.BaseName] = true
}
}
}
dependenciesLen := len(stagesDependencies)
if target == "" && dependenciesLen == 0 {
return stages
} else if dependenciesLen > 0 {
for i := 0; i < idx; i++ {
if stages[i].Name == "" {
continue
}
s := stages[i]
if stagesDependencies[s.Name] || s.Name == lastStageBaseName {
onlyUsedStages = append(onlyUsedStages, s)
}
}
}
onlyUsedStages = append(onlyUsedStages, stages[idx])
if idx > len(onlyUsedStages)-1 {
*lastStageIndex = len(onlyUsedStages) - 1
}
return onlyUsedStages
}
+329 -47
View File
@@ -17,25 +17,28 @@ limitations under the License.
package dockerfile
import (
"fmt"
"io/ioutil"
"os"
"strconv"
"reflect"
"testing"
"github.com/GoogleContainerTools/kaniko/pkg/config"
"github.com/GoogleContainerTools/kaniko/testutil"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/moby/buildkit/frontend/dockerfile/instructions"
)
func Test_Stages_ArgValueWithQuotes(t *testing.T) {
func Test_ParseStages_ArgValueWithQuotes(t *testing.T) {
dockerfile := `
ARG IMAGE="ubuntu:16.04"
ARG FOO=bar
FROM ${IMAGE}
RUN echo hi > /hi
FROM scratch AS second
COPY --from=0 /hi /hi2
FROM scratch
COPY --from=second /hi2 /hi3
`
@@ -53,25 +56,23 @@ func Test_Stages_ArgValueWithQuotes(t *testing.T) {
t.Fatal(err)
}
stages, err := Stages(&config.KanikoOptions{DockerfilePath: tmpfile.Name()})
stages, metaArgs, err := ParseStages(&config.KanikoOptions{DockerfilePath: tmpfile.Name()})
if err != nil {
t.Fatal(err)
}
if len(stages) == 0 {
t.Fatal("length of stages expected to be greater than zero, but was zero")
}
if len(stages[0].MetaArgs) == 0 {
t.Fatal("length of stage[0] meta args expected to be greater than zero, but was zero")
if len(metaArgs) != 2 {
t.Fatalf("length of stage meta args expected to be 2, but was %d", len(metaArgs))
}
expectedVal := "ubuntu:16.04"
arg := stages[0].MetaArgs[0]
if arg.ValueString() != expectedVal {
t.Fatalf("expected stages[0].MetaArgs[0] val to be %s but was %s", expectedVal, arg.ValueString())
for i, expectedVal := range []string{"ubuntu:16.04", "bar"} {
if metaArgs[i].ValueString() != expectedVal {
t.Fatalf("expected metaArg %d val to be %s but was %s", i, expectedVal, metaArgs[i].ValueString())
}
}
}
@@ -189,40 +190,63 @@ func Test_stripEnclosingQuotes(t *testing.T) {
}
}
func Test_resolveStages(t *testing.T) {
dockerfile := `
FROM scratch
RUN echo hi > /hi
FROM scratch AS second
COPY --from=0 /hi /hi2
FROM scratch AS tHiRd
COPY --from=second /hi2 /hi3
COPY --from=1 /hi2 /hi3
FROM scratch
COPY --from=thIrD /hi3 /hi4
COPY --from=third /hi3 /hi4
COPY --from=2 /hi3 /hi4
`
stages, _, err := Parse([]byte(dockerfile))
if err != nil {
t.Fatal(err)
func Test_GetOnBuildInstructions(t *testing.T) {
type testCase struct {
name string
cfg *v1.Config
stageToIdx map[string]string
expCommands []instructions.Command
}
resolveStages(stages)
for index, stage := range stages {
if index == 0 {
continue
}
expectedStage := strconv.Itoa(index - 1)
for _, command := range stage.Commands {
copyCmd := command.(*instructions.CopyCommand)
if copyCmd.From != expectedStage {
t.Fatalf("unexpected copy command: %s resolved to stage %s, expected %s", copyCmd.String(), copyCmd.From, expectedStage)
}
}
tests := []testCase{
{name: "no on-build on config",
cfg: &v1.Config{},
stageToIdx: map[string]string{"builder": "0"},
expCommands: nil,
},
{name: "onBuild on config, nothing to resolve",
cfg: &v1.Config{OnBuild: []string{"WORKDIR /app"}},
stageToIdx: map[string]string{"builder": "0", "temp": "1"},
expCommands: []instructions.Command{&instructions.WorkdirCommand{Path: "/app"}},
},
{name: "onBuild on config, resolve multiple stages",
cfg: &v1.Config{OnBuild: []string{"COPY --from=builder a.txt b.txt", "COPY --from=temp /app /app"}},
stageToIdx: map[string]string{"builder": "0", "temp": "1"},
expCommands: []instructions.Command{
&instructions.CopyCommand{
SourcesAndDest: []string{"a.txt b.txt"},
From: "0",
},
&instructions.CopyCommand{
SourcesAndDest: []string{"/app /app"},
From: "1",
},
}},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
cmds, err := GetOnBuildInstructions(test.cfg, test.stageToIdx)
if err != nil {
t.Fatalf("Failed to parse config for on-build instructions")
}
if len(cmds) != len(test.expCommands) {
t.Fatalf("Expected %d commands, got %d", len(test.expCommands), len(cmds))
}
for i, cmd := range cmds {
if reflect.TypeOf(cmd) != reflect.TypeOf(test.expCommands[i]) {
t.Fatalf("Got command %s, expected %s", cmd, test.expCommands[i])
}
switch c := cmd.(type) {
case *instructions.CopyCommand:
{
exp := test.expCommands[i].(*instructions.CopyCommand)
testutil.CheckDeepEqual(t, exp.From, c.From)
}
}
}
})
}
}
@@ -230,10 +254,10 @@ func Test_targetStage(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
`
@@ -364,3 +388,261 @@ 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)
})
}
}
}
}
func Test_SkipingUnusedStages(t *testing.T) {
tests := []struct {
description string
dockerfile string
targets []string
expectedSourceCodes map[string][]string
expectedTargetIndexBeforeSkip map[string]int
expectedTargetIndexAfterSkip map[string]int
}{
{
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"},
},
expectedTargetIndexBeforeSkip: map[string]int{
"base-dev": 0,
"base-prod": 1,
"": 2,
},
expectedTargetIndexAfterSkip: map[string]int{
"base-dev": 0,
"base-prod": 0,
"": 1,
},
},
{
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"},
},
expectedTargetIndexBeforeSkip: map[string]int{
"base-dev": 0,
"base-prod": 1,
"": 2,
},
expectedTargetIndexAfterSkip: map[string]int{
"base-dev": 0,
"base-prod": 0,
"": 1,
},
},
{
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"},
},
expectedTargetIndexBeforeSkip: map[string]int{
"base-dev": 0,
"base-prod": 1,
"": 2,
},
expectedTargetIndexAfterSkip: map[string]int{
"base-dev": 0,
"base-prod": 0,
"": 2,
},
},
{
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{"base", ""},
expectedSourceCodes: map[string][]string{
"base": {"FROM debian:9.11 as base"},
"second": {"FROM debian:9.11 as base", "FROM scratch as second"},
"": {"FROM debian:9.11 as base", "FROM scratch as second", "FROM base as fourth", "FROM fourth"},
},
expectedTargetIndexBeforeSkip: map[string]int{
"base": 0,
"second": 1,
"": 5,
},
expectedTargetIndexAfterSkip: map[string]int{
"base": 0,
"second": 1,
"": 3,
},
},
{
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"},
},
expectedTargetIndexBeforeSkip: map[string]int{
"final": 4,
"buzz": 3,
"fizz": 2,
"": 4,
},
expectedTargetIndexAfterSkip: map[string]int{
"final": 0,
"buzz": 0,
"fizz": 1,
"": 4,
},
},
}
for _, test := range tests {
stages, _, err := Parse([]byte(test.dockerfile))
testutil.CheckError(t, false, err)
actualSourceCodes := make(map[string][]string)
for _, target := range test.targets {
targetIndex, err := targetStage(stages, target)
testutil.CheckError(t, false, err)
targetIndexBeforeSkip := targetIndex
onlyUsedStages := skipUnusedStages(stages, &targetIndex, target)
for _, s := range onlyUsedStages {
actualSourceCodes[target] = append(actualSourceCodes[target], s.SourceCode)
}
t.Run(test.description, func(t *testing.T) {
testutil.CheckDeepEqual(t, test.expectedSourceCodes[target], actualSourceCodes[target])
testutil.CheckDeepEqual(t, test.expectedTargetIndexBeforeSkip[target], targetIndexBeforeSkip)
testutil.CheckDeepEqual(t, test.expectedTargetIndexAfterSkip[target], targetIndex)
})
}
}
}
+115 -56
View File
@@ -20,6 +20,7 @@ import (
"fmt"
"os"
"path/filepath"
"runtime"
"strconv"
"strings"
"time"
@@ -51,6 +52,11 @@ import (
// This is the size of an empty tar in Go
const emptyTarSize = 1024
// for testing
var (
initializeConfig = initConfig
)
type cachePusher func(*config.KanikoOptions, string, string, string) error
type snapShotter interface {
Init() error
@@ -77,7 +83,7 @@ type stageBuilder struct {
}
// newStageBuilder returns a new type stageBuilder which contains all the information required to build the stage
func newStageBuilder(opts *config.KanikoOptions, stage config.KanikoStage, crossStageDeps map[int][]string, dcm map[string]string, sid map[string]string) (*stageBuilder, error) {
func newStageBuilder(opts *config.KanikoOptions, stage config.KanikoStage, crossStageDeps map[int][]string, dcm map[string]string, sid map[string]string, stageNameToIdx map[string]string) (*stageBuilder, error) {
sourceImage, err := util.RetrieveSourceImage(stage, opts)
if err != nil {
return nil, err
@@ -88,7 +94,7 @@ func newStageBuilder(opts *config.KanikoOptions, stage config.KanikoStage, cross
return nil, err
}
if err := resolveOnBuild(&stage, &imageConfig.Config); err != nil {
if err := resolveOnBuild(&stage, &imageConfig.Config, stageNameToIdx); err != nil {
return nil, err
}
@@ -97,7 +103,7 @@ func newStageBuilder(opts *config.KanikoOptions, stage config.KanikoStage, cross
return nil, err
}
l := snapshot.NewLayeredMap(hasher, util.CacheHasher())
snapshotter := snapshot.NewSnapshotter(l, constants.RootDir)
snapshotter := snapshot.NewSnapshotter(l, config.RootDir)
digest, err := sourceImage.Digest()
if err != nil {
@@ -135,7 +141,7 @@ func newStageBuilder(opts *config.KanikoOptions, stage config.KanikoStage, cross
return s, nil
}
func initializeConfig(img partial.WithConfigFile, opts *config.KanikoOptions) (*v1.ConfigFile, error) {
func initConfig(img partial.WithConfigFile, opts *config.KanikoOptions) (*v1.ConfigFile, error) {
imageConfig, err := img.ConfigFile()
if err != nil {
return nil, err
@@ -298,7 +304,7 @@ func (s *stageBuilder) build() error {
if shouldUnpack {
t := timing.Start("FS Unpacking")
if _, err := util.GetFSFromImage(constants.RootDir, s.image, util.ExtractFile); err != nil {
if _, err := util.GetFSFromImage(config.RootDir, s.image, util.ExtractFile); err != nil {
return errors.Wrap(err, "failed to get filesystem from image")
}
@@ -307,7 +313,7 @@ func (s *stageBuilder) build() error {
logrus.Info("Skipping unpacking as no commands require it.")
}
if err := util.DetectFilesystemWhitelist(constants.WhitelistPath); err != nil {
if err := util.DetectFilesystemWhitelist(config.WhitelistPath); err != nil {
return errors.Wrap(err, "failed to check filesystem whitelist")
}
@@ -333,9 +339,11 @@ func (s *stageBuilder) build() error {
return errors.Wrap(err, "failed to get files used from context")
}
*compositeKey, err = s.populateCompositeKey(command, files, *compositeKey, s.args, s.cf.Config.Env)
if err != nil {
return err
if s.opts.Cache {
*compositeKey, err = s.populateCompositeKey(command, files, *compositeKey, s.args, s.cf.Config.Env)
if err != nil && s.opts.Cache {
return err
}
}
logrus.Info(command.String())
@@ -371,19 +379,21 @@ func (s *stageBuilder) build() error {
return errors.Wrap(err, "failed to take snapshot")
}
logrus.Debugf("build: composite key for command %v %v", command.String(), compositeKey)
ck, err := compositeKey.Hash()
if err != nil {
return errors.Wrap(err, "failed to hash composite key")
}
if s.opts.Cache {
logrus.Debugf("build: composite key for command %v %v", command.String(), compositeKey)
ck, err := compositeKey.Hash()
if err != nil {
return errors.Wrap(err, "failed to hash composite key")
}
logrus.Debugf("build: cache key for command %v %v", command.String(), ck)
logrus.Debugf("build: cache key for command %v %v", command.String(), ck)
// Push layer to cache (in parallel) now along with new config file
if s.opts.Cache && command.ShouldCacheOutput() {
cacheGroup.Go(func() error {
return s.pushLayerToCache(s.opts, ck, tarPath, command.String())
})
// Push layer to cache (in parallel) now along with new config file
if command.ShouldCacheOutput() {
cacheGroup.Go(func() error {
return s.pushLayerToCache(s.opts, ck, tarPath, command.String())
})
}
}
if err := s.saveSnapshotToImage(command.String(), tarPath); err != nil {
return errors.Wrap(err, "failed to save snapshot to image")
@@ -414,7 +424,7 @@ func (s *stageBuilder) takeSnapshot(files []string) (string, error) {
}
func (s *stageBuilder) shouldTakeSnapshot(index int, files []string) bool {
isLastCommand := index == len(s.stage.Commands)-1
isLastCommand := index == len(s.cmds)-1
// We only snapshot the very end with single snapshot mode on.
if s.opts.SingleSnapshot {
@@ -486,11 +496,7 @@ func (s *stageBuilder) saveLayerToImage(layer v1.Layer, createdBy string) error
return err
}
func CalculateDependencies(opts *config.KanikoOptions) (map[int][]string, error) {
stages, err := dockerfile.Stages(opts)
if err != nil {
return nil, err
}
func CalculateDependencies(stages []config.KanikoStage, opts *config.KanikoOptions, stageNameToIdx map[string]string) (map[int][]string, error) {
images := []v1.Image{}
depGraph := map[int][]string{}
for _, s := range stages {
@@ -512,7 +518,11 @@ func CalculateDependencies(opts *config.KanikoOptions) (map[int][]string, error)
if err != nil {
return nil, err
}
for _, c := range s.Commands {
cmds, err := dockerfile.GetOnBuildInstructions(&cfg.Config, stageNameToIdx)
cmds = append(cmds, s.Commands...)
for _, c := range cmds {
switch cmd := c.(type) {
case *instructions.CopyCommand:
if cmd.From != "" {
@@ -524,7 +534,6 @@ func CalculateDependencies(opts *config.KanikoOptions) (map[int][]string, error)
if err != nil {
return nil, err
}
depGraph[i] = append(depGraph[i], resolved[0:len(resolved)-1]...)
}
case *instructions.EnvCommand:
@@ -554,27 +563,33 @@ func DoBuild(opts *config.KanikoOptions) (v1.Image, error) {
digestToCacheKey := make(map[string]string)
stageIdxToDigest := make(map[string]string)
// Parse dockerfile
stages, err := dockerfile.Stages(opts)
stages, metaArgs, err := dockerfile.ParseStages(opts)
if err != nil {
return nil, err
}
if err := util.GetExcludedFiles(opts.DockerfilePath, opts.SrcContext); err != nil {
return nil, err
}
// Some stages may refer to other random images, not previous stages
if err := fetchExtraStages(stages, opts); err != nil {
stageNameToIdx := ResolveCrossStageInstructions(stages)
kanikoStages, err := dockerfile.MakeKanikoStages(opts, stages, metaArgs)
if err != nil {
return nil, err
}
crossStageDependencies, err := CalculateDependencies(opts)
if err := util.GetExcludedFiles(opts.DockerfilePath, opts.SrcContext); err != nil {
return nil, err
}
// Some stages may refer to other random images, not previous stages
if err := fetchExtraStages(kanikoStages, opts); err != nil {
return nil, err
}
crossStageDependencies, err := CalculateDependencies(kanikoStages, opts, stageNameToIdx)
if err != nil {
return nil, err
}
logrus.Infof("Built cross stage deps: %v", crossStageDependencies)
for index, stage := range stages {
sb, err := newStageBuilder(opts, stage, crossStageDependencies, digestToCacheKey, stageIdxToDigest)
for index, stage := range kanikoStages {
sb, err := newStageBuilder(opts, stage, crossStageDependencies, digestToCacheKey, stageIdxToDigest, stageNameToIdx)
if err != nil {
return nil, err
}
@@ -589,6 +604,17 @@ func DoBuild(opts *config.KanikoOptions) (v1.Image, error) {
return nil, err
}
configFile, err := sourceImage.ConfigFile()
if err != nil {
return nil, err
}
configFile.OS = runtime.GOOS
configFile.Architecture = runtime.GOARCH
sourceImage, err = mutate.ConfigFile(sourceImage, configFile)
if err != nil {
return nil, err
}
d, err := sourceImage.Digest()
if err != nil {
return nil, err
@@ -629,20 +655,23 @@ func DoBuild(opts *config.KanikoOptions) (v1.Image, error) {
if err != nil {
return nil, err
}
dstDir := filepath.Join(constants.KanikoDir, strconv.Itoa(index))
dstDir := filepath.Join(config.KanikoDir, strconv.Itoa(index))
if err := os.MkdirAll(dstDir, 0644); err != nil {
return nil, err
return nil, errors.Wrap(err,
fmt.Sprintf("to create workspace for stage %s",
stageIdxToDigest[strconv.Itoa(index)],
))
}
for _, p := range filesToSave {
logrus.Infof("Saving file %s for later use", p)
if err := util.CopyFileOrSymlink(p, dstDir); err != nil {
return nil, err
if err := util.CopyFileOrSymlink(p, dstDir, config.RootDir); err != nil {
return nil, errors.Wrap(err, "could not save file")
}
}
// Delete the filesystem
if err := util.DeleteFilesystem(); err != nil {
return nil, err
return nil, errors.Wrap(err, fmt.Sprintf("deleting file system after satge %d", index))
}
}
@@ -654,14 +683,22 @@ func DoBuild(opts *config.KanikoOptions) (v1.Image, error) {
func filesToSave(deps []string) ([]string, error) {
srcFiles := []string{}
for _, src := range deps {
srcs, err := filepath.Glob(src)
srcs, err := filepath.Glob(filepath.Join(config.RootDir, src))
if err != nil {
return nil, err
}
for _, f := range srcs {
if link, err := util.EvalSymLink(f); err == nil {
link, err = filepath.Rel(config.RootDir, link)
if err != nil {
return nil, errors.Wrap(err, fmt.Sprintf("could not find relative path to %s", config.RootDir))
}
srcFiles = append(srcFiles, link)
}
f, err = filepath.Rel(config.RootDir, f)
if err != nil {
return nil, errors.Wrap(err, fmt.Sprintf("could not find relative path to %s", config.RootDir))
}
srcFiles = append(srcFiles, f)
}
}
@@ -672,7 +709,7 @@ func fetchExtraStages(stages []config.KanikoStage, opts *config.KanikoOptions) e
t := timing.Start("Fetching Extra Stages")
defer timing.DefaultRun.Stop(t)
var names = []string{}
var names []string
for stageIndex, s := range stages {
for _, cmd := range s.Commands {
@@ -689,11 +726,10 @@ func fetchExtraStages(stages []config.KanikoStage, opts *config.KanikoOptions) e
continue
}
// Check if the name is the alias of a previous stage
for _, name := range names {
if name == c.From {
continue
}
if fromPreviousStage(c, names) {
continue
}
// This must be an image name, fetch it.
logrus.Debugf("Found extra base image stage %s", c.From)
sourceImage, err := util.RetrieveRemoteImage(c.From, opts)
@@ -714,10 +750,20 @@ func fetchExtraStages(stages []config.KanikoStage, opts *config.KanikoOptions) e
}
return nil
}
func fromPreviousStage(copyCommand *instructions.CopyCommand, previousStageNames []string) bool {
for _, previousStageName := range previousStageNames {
if previousStageName == copyCommand.From {
return true
}
}
return false
}
func extractImageToDependencyDir(name string, image v1.Image) error {
t := timing.Start("Extracting Image to Dependency Dir")
defer timing.DefaultRun.Stop(t)
dependencyDir := filepath.Join(constants.KanikoDir, name)
dependencyDir := filepath.Join(config.KanikoDir, name)
if err := os.MkdirAll(dependencyDir, 0755); err != nil {
return err
}
@@ -752,15 +798,12 @@ func getHasher(snapshotMode string) (func(string) (string, error), error) {
return nil, fmt.Errorf("%s is not a valid snapshot mode", snapshotMode)
}
func resolveOnBuild(stage *config.KanikoStage, config *v1.Config) error {
if config.OnBuild == nil || len(config.OnBuild) == 0 {
return nil
}
// Otherwise, parse into commands
cmds, err := dockerfile.ParseCommands(config.OnBuild)
func resolveOnBuild(stage *config.KanikoStage, config *v1.Config, stageNameToIdx map[string]string) error {
cmds, err := dockerfile.GetOnBuildInstructions(config, stageNameToIdx)
if err != nil {
return err
}
// Append to the beginning of the commands in the stage
stage.Commands = append(cmds, stage.Commands...)
logrus.Infof("Executing %v build triggers", len(cmds))
@@ -789,3 +832,19 @@ func reviewConfig(stage config.KanikoStage, config *v1.Config) {
config.Cmd = nil
}
}
// iterates over a list of stages and resolves instructions referring to earlier stages
// returns a mapping of stage name to stage id, f.e - ["first": "0", "second": "1", "target": "2"]
func ResolveCrossStageInstructions(stages []instructions.Stage) map[string]string {
nameToIndex := make(map[string]string)
for i, stage := range stages {
index := strconv.Itoa(i)
if stage.Name != "" {
nameToIndex[stage.Name] = index
}
dockerfile.ResolveCrossStageCommands(stage.Commands, nameToIndex)
}
logrus.Debugf("Built stage name to index map: %v", nameToIndex)
return nameToIndex
}
+126 -46
View File
@@ -25,6 +25,7 @@ import (
"path/filepath"
"reflect"
"sort"
"strconv"
"testing"
"github.com/GoogleContainerTools/kaniko/pkg/commands"
@@ -35,6 +36,7 @@ import (
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/empty"
"github.com/google/go-containerregistry/pkg/v1/mutate"
"github.com/google/go-containerregistry/pkg/v1/partial"
"github.com/moby/buildkit/frontend/dockerfile/instructions"
)
@@ -88,28 +90,17 @@ func stage(t *testing.T, d string) config.KanikoStage {
}
}
type MockCommand struct {
name string
}
func (m *MockCommand) Name() string {
return m.name
}
func Test_stageBuilder_shouldTakeSnapshot(t *testing.T) {
commands := []instructions.Command{
&MockCommand{name: "command1"},
&MockCommand{name: "command2"},
&MockCommand{name: "command3"},
}
stage := instructions.Stage{
Commands: commands,
cmds := []commands.DockerCommand{
&MockDockerCommand{command: "command1"},
&MockDockerCommand{command: "command2"},
&MockDockerCommand{command: "command3"},
}
type fields struct {
stage config.KanikoStage
opts *config.KanikoOptions
cmds []commands.DockerCommand
}
type args struct {
index int
@@ -126,8 +117,8 @@ func Test_stageBuilder_shouldTakeSnapshot(t *testing.T) {
fields: fields{
stage: config.KanikoStage{
Final: true,
Stage: stage,
},
cmds: cmds,
},
args: args{
index: 1,
@@ -139,11 +130,11 @@ func Test_stageBuilder_shouldTakeSnapshot(t *testing.T) {
fields: fields{
stage: config.KanikoStage{
Final: false,
Stage: stage,
},
cmds: cmds,
},
args: args{
index: len(commands) - 1,
index: len(cmds) - 1,
},
want: true,
},
@@ -152,8 +143,8 @@ func Test_stageBuilder_shouldTakeSnapshot(t *testing.T) {
fields: fields{
stage: config.KanikoStage{
Final: false,
Stage: stage,
},
cmds: cmds,
},
args: args{
index: 0,
@@ -165,9 +156,9 @@ func Test_stageBuilder_shouldTakeSnapshot(t *testing.T) {
fields: fields{
stage: config.KanikoStage{
Final: false,
Stage: stage,
},
opts: &config.KanikoOptions{Cache: true},
cmds: cmds,
},
args: args{
index: 0,
@@ -184,6 +175,7 @@ func Test_stageBuilder_shouldTakeSnapshot(t *testing.T) {
s := &stageBuilder{
stage: tt.fields.stage,
opts: tt.fields.opts,
cmds: tt.fields.cmds,
}
if got := s.shouldTakeSnapshot(tt.args.index, tt.args.files); got != tt.want {
t.Errorf("stageBuilder.shouldTakeSnapshot() = %v, want %v", got, tt.want)
@@ -194,7 +186,8 @@ func Test_stageBuilder_shouldTakeSnapshot(t *testing.T) {
func TestCalculateDependencies(t *testing.T) {
type args struct {
dockerfile string
dockerfile string
mockInitConfig func(partial.WithConfigFile, *config.KanikoOptions) (*v1.ConfigFile, error)
}
tests := []struct {
name string
@@ -314,16 +307,58 @@ COPY --from=stage2 /bar /bat
1: {"/bar"},
},
},
{
name: "one image has onbuild config",
args: args{
mockInitConfig: func(img partial.WithConfigFile, opts *config.KanikoOptions) (*v1.ConfigFile, error) {
cfg, err := img.ConfigFile()
// if image is "alpine" then add ONBUILD to its config
if cfg != nil && cfg.Architecture != "" {
cfg.Config.OnBuild = []string{"COPY --from=builder /app /app"}
}
return cfg, err
},
dockerfile: `
FROM scratch as builder
RUN foo
FROM alpine as second
# This image has an ONBUILD command so it will be executed
COPY --from=builder /foo /bar
FROM scratch as target
COPY --from=second /bar /bat
`,
},
want: map[int][]string{
0: {"/app", "/foo"},
1: {"/bar"},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.args.mockInitConfig != nil {
original := initializeConfig
defer func() { initializeConfig = original }()
initializeConfig = tt.args.mockInitConfig
}
f, _ := ioutil.TempFile("", "")
ioutil.WriteFile(f.Name(), []byte(tt.args.dockerfile), 0755)
opts := &config.KanikoOptions{
DockerfilePath: f.Name(),
}
testStages, metaArgs, err := dockerfile.ParseStages(opts)
if err != nil {
t.Errorf("Failed to parse test dockerfile to stages: %s", err)
}
got, err := CalculateDependencies(opts)
stageNameToIdx := ResolveCrossStageInstructions(testStages)
kanikoStages, err := dockerfile.MakeKanikoStages(opts, testStages, metaArgs)
if err != nil {
t.Errorf("Failed to parse stages to Kaniko Stages: %s", err)
}
got, err := CalculateDependencies(kanikoStages, opts, stageNameToIdx)
if err != nil {
t.Errorf("got error: %s,", err)
}
@@ -371,10 +406,15 @@ func Test_filesToSave(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tmpDir, err := ioutil.TempDir("", "")
original := config.RootDir
config.RootDir = tmpDir
if err != nil {
t.Errorf("error creating tmpdir: %s", err)
}
defer os.RemoveAll(tmpDir)
defer func() {
config.RootDir = original
os.RemoveAll(tmpDir)
}()
for _, f := range tt.files {
p := filepath.Join(tmpDir, f)
@@ -391,22 +431,14 @@ func Test_filesToSave(t *testing.T) {
fp.Close()
}
args := []string{}
for _, arg := range tt.args {
args = append(args, filepath.Join(tmpDir, arg))
}
got, err := filesToSave(args)
got, err := filesToSave(tt.args)
if err != nil {
t.Errorf("got err: %s", err)
}
want := []string{}
for _, w := range tt.want {
want = append(want, filepath.Join(tmpDir, w))
}
sort.Strings(want)
sort.Strings(tt.want)
sort.Strings(got)
if !reflect.DeepEqual(got, want) {
t.Errorf("filesToSave() = %v, want %v", got, want)
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("filesToSave() = %v, want %v", got, tt.want)
}
})
}
@@ -870,12 +902,16 @@ COPY %s bar.txt
DockerfilePath: f.Name(),
}
stages, err := dockerfile.Stages(opts)
testStages, metaArgs, err := dockerfile.ParseStages(opts)
if err != nil {
t.Errorf("could not parse test dockerfile")
t.Errorf("Failed to parse test dockerfile to stages: %s", err)
}
stage := stages[0]
_ = ResolveCrossStageInstructions(testStages)
kanikoStages, err := dockerfile.MakeKanikoStages(opts, testStages, metaArgs)
if err != nil {
t.Errorf("Failed to parse stages to Kaniko Stages: %s", err)
}
stage := kanikoStages[0]
cmds := stage.Commands
return testcase{
@@ -941,12 +977,17 @@ COPY %s bar.txt
DockerfilePath: f.Name(),
}
stages, err := dockerfile.Stages(opts)
testStages, metaArgs, err := dockerfile.ParseStages(opts)
if err != nil {
t.Errorf("could not parse test dockerfile")
t.Errorf("Failed to parse test dockerfile to stages: %s", err)
}
_ = ResolveCrossStageInstructions(testStages)
kanikoStages, err := dockerfile.MakeKanikoStages(opts, testStages, metaArgs)
if err != nil {
t.Errorf("Failed to parse stages to Kaniko Stages: %s", err)
}
stage := stages[0]
stage := kanikoStages[0]
cmds := stage.Commands
return testcase{
@@ -1129,9 +1170,9 @@ COPY %s bar.txt
for key, value := range tc.args {
sb.args.AddArg(key, &value)
}
tmp := commands.RootDir
tmp := config.RootDir
if tc.rootDir != "" {
commands.RootDir = tc.rootDir
config.RootDir = tc.rootDir
}
err := sb.build()
if err != nil {
@@ -1141,7 +1182,7 @@ COPY %s bar.txt
assertCacheKeys(t, tc.expectedCacheKeys, lc.receivedKeys, "receive")
assertCacheKeys(t, tc.pushedCacheKeys, keys, "push")
commands.RootDir = tmp
config.RootDir = tmp
})
}
@@ -1247,3 +1288,42 @@ func hashCompositeKeys(t *testing.T, ck1 CompositeCache, ck2 CompositeCache) (st
}
return key1, key2
}
func Test_ResolveCrossStageInstructions(t *testing.T) {
df := `
FROM scratch
RUN echo hi > /hi
FROM scratch AS second
COPY --from=0 /hi /hi2
FROM scratch AS tHiRd
COPY --from=second /hi2 /hi3
COPY --from=1 /hi2 /hi3
FROM scratch
COPY --from=thIrD /hi3 /hi4
COPY --from=third /hi3 /hi4
COPY --from=2 /hi3 /hi4
`
stages, _, err := dockerfile.Parse([]byte(df))
if err != nil {
t.Fatal(err)
}
stageToIdx := ResolveCrossStageInstructions(stages)
for index, stage := range stages {
if index == 0 {
continue
}
expectedStage := strconv.Itoa(index - 1)
for _, command := range stage.Commands {
copyCmd := command.(*instructions.CopyCommand)
if copyCmd.From != expectedStage {
t.Fatalf("unexpected copy command: %s resolved to stage %s, expected %s", copyCmd.String(), copyCmd.From, expectedStage)
}
}
expectedMap := map[string]string{"second": "1", "third": "2"}
testutil.CheckDeepEqual(t, expectedMap, stageToIdx)
}
}
+187
View File
@@ -0,0 +1,187 @@
/*
Copyright 2020 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 executor
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"testing"
"github.com/GoogleContainerTools/kaniko/pkg/config"
"github.com/GoogleContainerTools/kaniko/pkg/constants"
"github.com/GoogleContainerTools/kaniko/testutil"
)
func TestCopyCommand_Multistage(t *testing.T) {
t.Run("copy a file across multistage", func(t *testing.T) {
testDir, fn := setupMultistageTests(t)
defer fn()
dockerFile := fmt.Sprintf(`
FROM scratch as first
COPY foo/bam.txt copied/
ENV test test
From scratch as second
COPY --from=first copied/bam.txt output/bam.txt`)
ioutil.WriteFile(filepath.Join(testDir, "workspace", "Dockerfile"), []byte(dockerFile), 0755)
opts := &config.KanikoOptions{
DockerfilePath: filepath.Join(testDir, "workspace", "Dockerfile"),
SrcContext: filepath.Join(testDir, "workspace"),
SnapshotMode: constants.SnapshotModeFull,
}
_, err := DoBuild(opts)
testutil.CheckNoError(t, err)
// Check Image has one layer bam.txt
files, err := ioutil.ReadDir(filepath.Join(testDir, "output"))
if err != nil {
t.Fatal(err)
}
testutil.CheckDeepEqual(t, 1, len(files))
testutil.CheckDeepEqual(t, files[0].Name(), "bam.txt")
})
t.Run("copy a file across multistage into a directory", func(t *testing.T) {
testDir, fn := setupMultistageTests(t)
defer fn()
dockerFile := fmt.Sprintf(`
FROM scratch as first
COPY foo/bam.txt copied/
ENV test test
From scratch as second
COPY --from=first copied/bam.txt output/`)
ioutil.WriteFile(filepath.Join(testDir, "workspace", "Dockerfile"), []byte(dockerFile), 0755)
opts := &config.KanikoOptions{
DockerfilePath: filepath.Join(testDir, "workspace", "Dockerfile"),
SrcContext: filepath.Join(testDir, "workspace"),
SnapshotMode: constants.SnapshotModeFull,
}
_, err := DoBuild(opts)
testutil.CheckNoError(t, err)
files, err := ioutil.ReadDir(filepath.Join(testDir, "output"))
if err != nil {
t.Fatal(err)
}
testutil.CheckDeepEqual(t, 1, len(files))
testutil.CheckDeepEqual(t, files[0].Name(), "bam.txt")
})
t.Run("copy directory across multistage into a directory", func(t *testing.T) {
testDir, fn := setupMultistageTests(t)
defer fn()
dockerFile := fmt.Sprintf(`
FROM scratch as first
COPY foo copied
ENV test test
From scratch as second
COPY --from=first copied another`)
ioutil.WriteFile(filepath.Join(testDir, "workspace", "Dockerfile"), []byte(dockerFile), 0755)
opts := &config.KanikoOptions{
DockerfilePath: filepath.Join(testDir, "workspace", "Dockerfile"),
SrcContext: filepath.Join(testDir, "workspace"),
SnapshotMode: constants.SnapshotModeFull,
}
_, err := DoBuild(opts)
testutil.CheckNoError(t, err)
// Check Image has one layer bam.txt
files, err := ioutil.ReadDir(filepath.Join(testDir, "another"))
if err != nil {
t.Fatal(err)
}
testutil.CheckDeepEqual(t, 2, len(files))
testutil.CheckDeepEqual(t, files[0].Name(), "bam.link")
testutil.CheckDeepEqual(t, files[1].Name(), "bam.txt")
// TODO fix this
// path := filepath.Join(testDir, "output/another", "bam.link")
//linkName, err := os.Readlink(path)
//if err != nil {
// t.Fatal(err)
//}
//testutil.CheckDeepEqual(t, linkName, "bam.txt")
})
}
func setupMultistageTests(t *testing.T) (string, func()) {
testDir, err := ioutil.TempDir("", "")
if err != nil {
t.Fatal(err)
}
// Create workspace with files, dirs, and symlinks
// workspace tree:
// /root
// /kaniko
// /workspace
// - /foo
// - bam.txt
// - bam.link -> bam.txt
// - /bin
// - exec.link -> ../exec
// exec
// Make directory for stage or else the executor will create with permissions 0664
// and we will run into issue https://github.com/golang/go/issues/22323
if err := os.MkdirAll(filepath.Join(testDir, "kaniko/0"), 0755); err != nil {
t.Fatal(err)
}
workspace := filepath.Join(testDir, "workspace")
// Make foo
if err := os.MkdirAll(filepath.Join(workspace, "foo"), 0755); err != nil {
t.Fatal(err)
}
file := filepath.Join(workspace, "foo", "bam.txt")
if err := ioutil.WriteFile(file, []byte("meow"), 0755); err != nil {
t.Fatal(err)
}
os.Symlink("bam.txt", filepath.Join(workspace, "foo", "bam.link"))
// Make a file with contents link
file = filepath.Join(workspace, "exec")
if err := ioutil.WriteFile(file, []byte("woof"), 0755); err != nil {
t.Fatal(err)
}
// Make bin
if err := os.MkdirAll(filepath.Join(workspace, "bin"), 0755); err != nil {
t.Fatal(err)
}
os.Symlink("../exec", filepath.Join(workspace, "bin", "exec.link"))
// set up config
config.RootDir = testDir
config.KanikoDir = fmt.Sprintf("%s/%s", testDir, "kaniko")
// Write a whitelist path
if err := os.MkdirAll(filepath.Join(testDir, "proc"), 0755); err != nil {
t.Fatal(err)
}
mFile := filepath.Join(testDir, "proc/mountinfo")
mountInfo := fmt.Sprintf(
`36 35 98:0 /kaniko %s/kaniko rw,noatime master:1 - ext3 /dev/root rw,errors=continue
36 35 98:0 /proc %s/proc rw,noatime master:1 - ext3 /dev/root rw,errors=continue
`, testDir, testDir)
if err := ioutil.WriteFile(mFile, []byte(mountInfo), 0644); err != nil {
t.Fatal(err)
}
config.WhitelistPath = mFile
return testDir, func() {
config.KanikoDir = constants.KanikoDir
config.RootDir = constants.RootDir
config.WhitelistPath = constants.WhitelistPath
}
}
+22 -3
View File
@@ -24,6 +24,7 @@ import (
"io/ioutil"
"net/http"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
@@ -52,6 +53,7 @@ type withUserAgent struct {
const (
UpstreamClientUaKey = "UPSTREAM_CLIENT_TYPE"
DockerConfLocation = "/kaniko/.docker/config.json"
)
func (w *withUserAgent) RoundTrip(r *http.Request) (*http.Response, error) {
@@ -98,6 +100,13 @@ var defaultX509Handler systemCertLoader = func() CertPool {
}
}
// for testing
var (
fs = afero.NewOsFs()
execCommand = exec.Command
checkRemotePushPermission = remote.CheckPushPermission
)
// CheckPushPermissions checks that the configured credentials can be used to
// push to every specified destination.
func CheckPushPermissions(opts *config.KanikoOptions) error {
@@ -115,6 +124,18 @@ func CheckPushPermissions(opts *config.KanikoOptions) error {
continue
}
// Historically kaniko was pre-configured by default with gcr credential helper,
// in here we keep the backwards compatibility by enabling the GCR helper only
// when gcr.io is in one of the destinations.
if strings.Contains(destRef.RegistryStr(), "gcr.io") {
// Checking for existence of docker.config as it's normally required for
// authenticated registries and prevent overwriting user provided docker conf
if _, err := fs.Stat(DockerConfLocation); os.IsNotExist(err) {
if err := execCommand("docker-credential-gcr", "configure-docker").Run(); err != nil {
return errors.Wrap(err, "error while configuring docker-credential-gcr helper")
}
}
}
registryName := destRef.Repository.Registry.Name()
if opts.Insecure || opts.InsecureRegistries.Contains(registryName) {
newReg, err := name.NewRegistry(registryName, name.WeakValidation, name.Insecure)
@@ -124,7 +145,7 @@ func CheckPushPermissions(opts *config.KanikoOptions) error {
destRef.Repository.Registry = newReg
}
tr := makeTransport(opts, registryName, defaultX509Handler)
if err := remote.CheckPushPermission(destRef, creds.GetKeychain(), tr); err != nil {
if err := checkRemotePushPermission(destRef, creds.GetKeychain(), tr); err != nil {
return errors.Wrapf(err, "checking push permission for %q", destRef)
}
checked[destRef.Context().RepositoryStr()] = true
@@ -231,8 +252,6 @@ func DoPush(image v1.Image, opts *config.KanikoOptions) error {
return writeImageOutputs(image, destRefs)
}
var fs = afero.NewOsFs()
func writeImageOutputs(image v1.Image, destRefs []name.Tag) error {
dir := os.Getenv("BUILDER_OUTPUT")
if dir == "" {
+67
View File
@@ -24,11 +24,13 @@ import (
"io/ioutil"
"net/http"
"os"
"os/exec"
"path/filepath"
"testing"
"github.com/GoogleContainerTools/kaniko/pkg/config"
"github.com/GoogleContainerTools/kaniko/testutil"
"github.com/google/go-containerregistry/pkg/authn"
"github.com/google/go-containerregistry/pkg/name"
"github.com/google/go-containerregistry/pkg/v1/layout"
"github.com/google/go-containerregistry/pkg/v1/random"
@@ -307,3 +309,68 @@ func Test_makeTransport(t *testing.T) {
})
}
}
var calledExecCommand = false
var calledCheckPushPermission = false
func setCalledFalse() {
calledExecCommand = false
calledCheckPushPermission = false
}
func fakeExecCommand(command string, args ...string) *exec.Cmd {
calledExecCommand = true
cs := []string{"-test.run=TestHelperProcess", "--", command}
cs = append(cs, args...)
cmd := exec.Command(os.Args[0], cs...)
cmd.Env = []string{"GO_WANT_HELPER_PROCESS=1"}
return cmd
}
func fakeCheckPushPermission(ref name.Reference, kc authn.Keychain, t http.RoundTripper) error {
calledCheckPushPermission = true
return nil
}
func TestCheckPushPermissions(t *testing.T) {
tests := []struct {
Destination string
ShouldCallExecCommand bool
ExistingConfig bool
}{
{"gcr.io/test-image", true, false},
{"gcr.io/test-image", false, true},
{"localhost:5000/test-image", false, false},
{"localhost:5000/test-image", false, true},
}
execCommand = fakeExecCommand
checkRemotePushPermission = fakeCheckPushPermission
for _, test := range tests {
testName := fmt.Sprintf("%s_ExistingDockerConf_%v", test.Destination, test.ExistingConfig)
t.Run(testName, func(t *testing.T) {
fs = afero.NewMemMapFs()
opts := config.KanikoOptions{
Destinations: []string{test.Destination},
}
if test.ExistingConfig {
afero.WriteFile(fs, DockerConfLocation, []byte(""), os.FileMode(0644))
defer fs.Remove(DockerConfLocation)
}
CheckPushPermissions(&opts)
if test.ShouldCallExecCommand != calledExecCommand {
t.Errorf("Expected calledExecCommand to be %v however it was %v",
calledExecCommand, test.ShouldCallExecCommand)
}
setCalledFalse()
})
}
}
func TestHelperProcess(t *testing.T) {
if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" {
return
}
fmt.Fprintf(os.Stdout, "fake result")
os.Exit(0)
}
+5 -4
View File
@@ -20,6 +20,7 @@ import (
"os"
"path/filepath"
"github.com/GoogleContainerTools/kaniko/pkg/config"
"github.com/GoogleContainerTools/kaniko/pkg/util"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
@@ -34,8 +35,8 @@ import (
// output set.
// * Add all ancestors of each path to the output set.
func ResolvePaths(paths []string, wl []util.WhitelistEntry) (pathsToAdd []string, err error) {
logrus.Info("Resolving paths")
logrus.Debugf("Resolving paths %s", paths)
logrus.Infof("Resolving %d paths", len(paths))
logrus.Tracef("Resolving paths %s", paths)
fileSet := make(map[string]bool)
@@ -72,6 +73,7 @@ func ResolvePaths(paths []string, wl []util.WhitelistEntry) (pathsToAdd []string
}
logrus.Debugf("symlink path %s, target does not exist", f)
continue
}
// If the given path is a symlink and the target is part of the whitelist
@@ -89,7 +91,6 @@ func ResolvePaths(paths []string, wl []util.WhitelistEntry) (pathsToAdd []string
// Also add parent directories to keep the permission of them correctly.
pathsToAdd = filesWithParentDirs(pathsToAdd)
return
}
@@ -130,7 +131,7 @@ func resolveSymlinkAncestor(path string) (string, error) {
newPath := filepath.Clean(path)
loop:
for newPath != "/" {
for newPath != config.RootDir {
fi, err := os.Lstat(newPath)
if err != nil {
return "", errors.Wrap(err, "resolvePaths: failed to lstat")
+21 -3
View File
@@ -28,14 +28,14 @@ import (
"github.com/GoogleContainerTools/kaniko/pkg/timing"
"github.com/karrick/godirwalk"
"github.com/GoogleContainerTools/kaniko/pkg/constants"
"github.com/GoogleContainerTools/kaniko/pkg/config"
"github.com/GoogleContainerTools/kaniko/pkg/util"
"github.com/sirupsen/logrus"
)
// For testing
var snapshotPathPrefix = constants.KanikoDir
var snapshotPathPrefix = config.KanikoDir
// Snapshotter holds the root directory from which to take snapshots, and a list of snapshots taken
type Snapshotter struct {
@@ -63,7 +63,7 @@ func (s *Snapshotter) Key() (string, error) {
// TakeSnapshot takes a snapshot of the specified files, avoiding directories in the whitelist, and creates
// a tarball of the changed files. Return contents of the tarball, and whether or not any files were changed
func (s *Snapshotter) TakeSnapshot(files []string) (string, error) {
f, err := ioutil.TempFile(snapshotPathPrefix, "")
f, err := ioutil.TempFile(config.KanikoDir, "")
if err != nil {
return "", err
}
@@ -226,10 +226,28 @@ func writeToTar(t util.Tar, files, whiteouts []string) error {
return err
}
}
addedPaths := make(map[string]bool)
for _, path := range files {
if _, fileExists := addedPaths[path]; fileExists {
continue
}
for _, parentPath := range util.ParentDirectories(path) {
if parentPath == "/" {
continue
}
if _, dirExists := addedPaths[parentPath]; dirExists {
continue
}
if err := t.AddFileToTar(parentPath); err != nil {
return err
}
addedPaths[parentPath] = true
}
if err := t.AddFileToTar(path); err != nil {
return err
}
addedPaths[path] = true
}
return nil
}
+25 -2
View File
@@ -25,6 +25,7 @@ import (
"strings"
"testing"
"github.com/GoogleContainerTools/kaniko/pkg/config"
"github.com/GoogleContainerTools/kaniko/pkg/util"
"github.com/GoogleContainerTools/kaniko/testutil"
"github.com/pkg/errors"
@@ -63,6 +64,12 @@ func TestSnapshotFSFileChange(t *testing.T) {
fooPath: "newbaz1",
batPath: "baz",
}
for _, path := range util.ParentDirectoriesWithoutLeadingSlash(batPath) {
if path == "/" {
continue
}
snapshotFiles[path+"/"] = ""
}
actualFiles := []string{}
for {
@@ -76,6 +83,9 @@ func TestSnapshotFSFileChange(t *testing.T) {
if _, isFile := snapshotFiles[hdr.Name]; !isFile {
t.Fatalf("File %s unexpectedly in tar", hdr.Name)
}
if hdr.Typeflag == tar.TypeDir {
continue
}
contents, _ := ioutil.ReadAll(tr)
if string(contents) != snapshotFiles[hdr.Name] {
t.Fatalf("Contents of %s incorrect, expected: %s, actual: %s", hdr.Name, snapshotFiles[hdr.Name], string(contents))
@@ -152,6 +162,12 @@ func TestSnapshotFSChangePermissions(t *testing.T) {
snapshotFiles := map[string]string{
batPathWithoutLeadingSlash: "baz2",
}
for _, path := range util.ParentDirectoriesWithoutLeadingSlash(batPathWithoutLeadingSlash) {
if path == "/" {
continue
}
snapshotFiles[path+"/"] = ""
}
foundFiles := []string{}
for {
@@ -159,11 +175,13 @@ func TestSnapshotFSChangePermissions(t *testing.T) {
if err == io.EOF {
break
}
t.Logf("Info %s in tar", hdr.Name)
foundFiles = append(foundFiles, hdr.Name)
if _, isFile := snapshotFiles[hdr.Name]; !isFile {
t.Fatalf("File %s unexpectedly in tar", hdr.Name)
}
if hdr.Typeflag == tar.TypeDir {
continue
}
contents, _ := ioutil.ReadAll(tr)
if string(contents) != snapshotFiles[hdr.Name] {
t.Fatalf("Contents of %s incorrect, expected: %s, actual: %s", hdr.Name, snapshotFiles[hdr.Name], string(contents))
@@ -203,7 +221,9 @@ func TestSnapshotFiles(t *testing.T) {
expectedFiles := []string{
filepath.Join(testDirWithoutLeadingSlash, "foo"),
}
expectedFiles = append(expectedFiles, util.ParentDirectoriesWithoutLeadingSlash(filepath.Join(testDir, "foo"))...)
for _, path := range util.ParentDirectoriesWithoutLeadingSlash(filepath.Join(testDir, "foo")) {
expectedFiles = append(expectedFiles, strings.TrimRight(path, "/")+"/")
}
f, err := os.Open(tarPath)
if err != nil {
@@ -458,8 +478,11 @@ func setUpTest() (string, *Snapshotter, func(), error) {
return "", nil, nil, errors.Wrap(err, "initializing snapshotter")
}
original := config.KanikoDir
config.KanikoDir = testDir
cleanup := func() {
os.RemoveAll(snapshotPath)
config.KanikoDir = original
dirCleanUp()
}
+6 -5
View File
@@ -26,13 +26,14 @@ import (
"strconv"
"strings"
"github.com/GoogleContainerTools/kaniko/pkg/constants"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/moby/buildkit/frontend/dockerfile/instructions"
"github.com/moby/buildkit/frontend/dockerfile/parser"
"github.com/moby/buildkit/frontend/dockerfile/shell"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/GoogleContainerTools/kaniko/pkg/config"
)
// for testing
@@ -130,7 +131,6 @@ func ResolveSources(srcs []string, root string) ([]string, error) {
return nil, errors.Wrap(err, "matching sources")
}
logrus.Debugf("Resolved sources to %v", resolved)
fmt.Println("end of resolve sources")
return resolved, nil
}
@@ -145,7 +145,7 @@ func matchSources(srcs, files []string) ([]string, error) {
src = filepath.Clean(src)
for _, file := range files {
if filepath.IsAbs(src) {
file = filepath.Join(constants.RootDir, file)
file = filepath.Join(config.RootDir, file)
}
matched, err := filepath.Match(src, file)
if err != nil {
@@ -290,8 +290,9 @@ func IsSrcRemoteFileURL(rawurl string) bool {
return err == nil
}
func UpdateConfigEnv(newEnvs []instructions.KeyValuePair, config *v1.Config, replacementEnvs []string) error {
for index, pair := range newEnvs {
func UpdateConfigEnv(envVars []instructions.KeyValuePair, config *v1.Config, replacementEnvs []string) error {
newEnvs := make([]instructions.KeyValuePair, len(envVars))
for index, pair := range envVars {
expandedKey, err := ResolveEnvironmentReplacement(pair.Key, replacementEnvs, false)
if err != nil {
return err
+69
View File
@@ -25,6 +25,8 @@ import (
"testing"
"github.com/GoogleContainerTools/kaniko/testutil"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/moby/buildkit/frontend/dockerfile/instructions"
)
var testURL = "https://github.com/GoogleContainerTools/runtimes-common/blob/master/LICENSE"
@@ -275,6 +277,73 @@ func Test_MatchSources(t *testing.T) {
}
}
var updateConfigEnvTests = []struct {
name string
envVars []instructions.KeyValuePair
config *v1.Config
replacementEnvs []string
expectedEnv []string
}{
{
name: "test env config update",
envVars: []instructions.KeyValuePair{
{
Key: "key",
Value: "var",
},
{
Key: "foo",
Value: "baz",
}},
config: &v1.Config{},
replacementEnvs: []string{},
expectedEnv: []string{"key=var", "foo=baz"},
}, {
name: "test env config update with replacmenets",
envVars: []instructions.KeyValuePair{
{
Key: "key",
Value: "/var/run",
},
{
Key: "env",
Value: "$var",
},
{
Key: "foo",
Value: "$argarg",
}},
config: &v1.Config{},
replacementEnvs: []string{"var=/test/with'chars'/", "not=used", "argarg=\"a\"b\""},
expectedEnv: []string{"key=/var/run", "env=/test/with'chars'/", "foo=\"a\"b\""},
}, {
name: "test env config update replacing existing variable",
envVars: []instructions.KeyValuePair{
{
Key: "alice",
Value: "nice",
},
{
Key: "bob",
Value: "cool",
}},
config: &v1.Config{Env: []string{"bob=used", "more=test"}},
replacementEnvs: []string{},
expectedEnv: []string{"bob=cool", "more=test", "alice=nice"},
},
}
func Test_UpdateConfigEnvTests(t *testing.T) {
for _, test := range updateConfigEnvTests {
t.Run(test.name, func(t *testing.T) {
if err := UpdateConfigEnv(test.envVars, test.config, test.replacementEnvs); err != nil {
t.Fatalf("error updating config with env vars: %s", err)
}
testutil.CheckDeepEqual(t, test.expectedEnv, test.config.Env)
})
}
}
var isSrcValidTests = []struct {
name string
srcsAndDest []string
+28 -28
View File
@@ -33,7 +33,7 @@ import (
otiai10Cpy "github.com/otiai10/copy"
"github.com/GoogleContainerTools/kaniko/pkg/constants"
"github.com/GoogleContainerTools/kaniko/pkg/config"
"github.com/docker/docker/builder/dockerignore"
"github.com/docker/docker/pkg/fileutils"
v1 "github.com/google/go-containerregistry/pkg/v1"
@@ -51,7 +51,7 @@ type WhitelistEntry struct {
var initialWhitelist = []WhitelistEntry{
{
Path: "/kaniko",
Path: config.KanikoDir,
PrefixMatchOnly: false,
},
{
@@ -125,7 +125,7 @@ func GetFSFromLayers(root string, layers []v1.Layer, opts ...FSOpt) ([]string, e
return nil, errors.New("must supply an extract function")
}
if err := DetectFilesystemWhitelist(constants.WhitelistPath); err != nil {
if err := DetectFilesystemWhitelist(config.WhitelistPath); err != nil {
return nil, err
}
@@ -161,18 +161,15 @@ func GetFSFromLayers(root string, layers []v1.Layer, opts ...FSOpt) ([]string, e
dir := filepath.Dir(path)
if strings.HasPrefix(base, ".wh.") {
logrus.Debugf("Whiting out %s", path)
name := strings.TrimPrefix(base, ".wh.")
if err := os.RemoveAll(filepath.Join(dir, name)); err != nil {
return nil, errors.Wrapf(err, "removing whiteout %s", hdr.Name)
}
if !cfg.includeWhiteout {
logrus.Debug("not including whiteout files")
continue
}
logrus.Debugf("Whiting out %s", path)
name := strings.TrimPrefix(base, ".wh.")
if err := os.RemoveAll(filepath.Join(dir, name)); err != nil {
return nil, errors.Wrapf(err, "removing whiteout %s", hdr.Name)
}
}
if err := cfg.extractFunc(root, hdr, tr); err != nil {
@@ -188,7 +185,7 @@ func GetFSFromLayers(root string, layers []v1.Layer, opts ...FSOpt) ([]string, e
// DeleteFilesystem deletes the extracted image file system
func DeleteFilesystem() error {
logrus.Info("Deleting filesystem...")
return filepath.Walk(constants.RootDir, func(path string, info os.FileInfo, err error) error {
return filepath.Walk(config.RootDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
// ignore errors when deleting.
return nil
@@ -209,7 +206,7 @@ func DeleteFilesystem() error {
logrus.Debugf("Not deleting %s, as it contains a whitelisted path", path)
return nil
}
if path == constants.RootDir {
if path == config.RootDir {
return nil
}
return os.RemoveAll(path)
@@ -388,7 +385,7 @@ func CheckWhitelist(path string) bool {
}
func checkWhitelistRoot(root string) bool {
if root == constants.RootDir {
if root == config.RootDir {
return false
}
return CheckWhitelist(root)
@@ -423,7 +420,7 @@ func DetectFilesystemWhitelist(path string) error {
}
continue
}
if lineArr[4] != constants.RootDir {
if lineArr[4] != config.RootDir {
logrus.Tracef("Appending %s from line: %s", lineArr[4], line)
whitelist = append(whitelist, WhitelistEntry{
Path: lineArr[4],
@@ -463,16 +460,18 @@ func RelativeFiles(fp string, root string) ([]string, error) {
// ParentDirectories returns a list of paths to all parent directories
// Ex. /some/temp/dir -> [/, /some, /some/temp, /some/temp/dir]
func ParentDirectories(path string) []string {
path = filepath.Clean(path)
dirs := strings.Split(path, "/")
dirPath := constants.RootDir
paths := []string{constants.RootDir}
for index, dir := range dirs {
if dir == "" || index == (len(dirs)-1) {
continue
dir := filepath.Clean(path)
var paths []string
for {
if dir == filepath.Clean(config.RootDir) || dir == "" || dir == "." {
break
}
dirPath = filepath.Join(dirPath, dir)
paths = append(paths, dirPath)
dir, _ = filepath.Split(dir)
dir = filepath.Clean(dir)
paths = append([]string{dir}, paths...)
}
if len(paths) == 0 {
paths = []string{config.RootDir}
}
return paths
}
@@ -484,7 +483,7 @@ func ParentDirectoriesWithoutLeadingSlash(path string) []string {
path = filepath.Clean(path)
dirs := strings.Split(path, "/")
dirPath := ""
paths := []string{constants.RootDir}
paths := []string{config.RootDir}
for index, dir := range dirs {
if dir == "" || index == (len(dirs)-1) {
continue
@@ -728,7 +727,7 @@ func mkdirAllWithPermissions(path string, mode os.FileMode, uid, gid int64) erro
}
if uid > math.MaxUint32 || gid > math.MaxUint32 {
// due to https://github.com/golang/go/issues/8537
return errors.New(fmt.Sprintf("Numeric User-ID or Group-ID greater than %v are not properly supported.", math.MaxUint32))
return errors.New(fmt.Sprintf("Numeric User-ID or Group-ID greater than %v are not properly supported.", uint64(math.MaxUint32)))
}
if err := os.Chown(path, int(uid), int(gid)); err != nil {
return err
@@ -824,12 +823,13 @@ func getSymlink(path string) error {
// For cross stage dependencies kaniko must persist the referenced path so that it can be used in
// the dependent stage. For symlinks we copy the target path because copying the symlink would
// result in a dead link
func CopyFileOrSymlink(src string, destDir string) error {
func CopyFileOrSymlink(src string, destDir string, root string) error {
destFile := filepath.Join(destDir, src)
src = filepath.Join(root, src)
if fi, _ := os.Lstat(src); IsSymlink(fi) {
link, err := os.Readlink(src)
if err != nil {
return err
return errors.Wrap(err, "copying file or symlink")
}
if err := createParentDirectory(destFile); err != nil {
return err
+38 -4
View File
@@ -30,6 +30,7 @@ import (
"testing"
"time"
"github.com/GoogleContainerTools/kaniko/pkg/config"
"github.com/GoogleContainerTools/kaniko/pkg/mocks/go-containerregistry/mockv1"
"github.com/GoogleContainerTools/kaniko/testutil"
"github.com/golang/mock/gomock"
@@ -157,11 +158,13 @@ func Test_ParentDirectories(t *testing.T) {
tests := []struct {
name string
path string
rootDir string
expected []string
}{
{
name: "regular path",
path: "/path/to/dir",
name: "regular path",
path: "/path/to/dir",
rootDir: "/",
expected: []string{
"/",
"/path",
@@ -169,17 +172,48 @@ func Test_ParentDirectories(t *testing.T) {
},
},
{
name: "current directory",
path: ".",
name: "current directory",
path: ".",
rootDir: "/",
expected: []string{
"/",
},
},
{
name: "non / root directory",
path: "/tmp/kaniko/test/another/dir",
rootDir: "/tmp/kaniko/",
expected: []string{
"/tmp/kaniko",
"/tmp/kaniko/test",
"/tmp/kaniko/test/another",
},
},
{
name: "non / root director same path",
path: "/tmp/123",
rootDir: "/tmp/123",
expected: []string{
"/tmp/123",
},
},
{
name: "non / root directory path",
path: "/tmp/120162240/kaniko",
rootDir: "/tmp/120162240",
expected: []string{
"/tmp/120162240",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
original := config.RootDir
defer func() { config.RootDir = original }()
config.RootDir = tt.rootDir
actual := ParentDirectories(tt.path)
testutil.CheckErrorAndDeepEqual(t, false, nil, tt.expected, actual)
})
}
+10 -6
View File
@@ -28,6 +28,7 @@ import (
"strings"
"syscall"
"github.com/GoogleContainerTools/kaniko/pkg/config"
"github.com/docker/docker/pkg/archive"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
@@ -76,14 +77,17 @@ func (t *Tar) AddFileToTar(p string) error {
return err
}
if p != "/" {
// Docker uses no leading / in the tarball
hdr.Name = strings.TrimLeft(p, "/")
} else {
if p == config.RootDir {
// allow entry for / to preserve permission changes etc. (currently ignored anyway by Docker runtime)
hdr.Name = p
hdr.Name = "/"
} else {
// Docker uses no leading / in the tarball
hdr.Name = strings.TrimPrefix(p, config.RootDir)
hdr.Name = strings.TrimLeft(hdr.Name, "/")
}
if hdr.Typeflag == tar.TypeDir && !strings.HasSuffix(hdr.Name, "/") {
hdr.Name = hdr.Name + "/"
}
// rootfs may not have been extracted when using cache, preventing uname/gname from resolving
// this makes this layer unnecessarily differ from a cached layer which does contain this information
hdr.Uname = ""
+10
View File
@@ -21,6 +21,7 @@ import (
"crypto/sha256"
"encoding/hex"
"io"
"io/ioutil"
"os"
"runtime"
"strconv"
@@ -134,3 +135,12 @@ func currentPlatform() v1.Platform {
Architecture: runtime.GOARCH,
}
}
// GetInputFrom returns Reader content
func GetInputFrom(r io.Reader) ([]byte, error) {
output, err := ioutil.ReadAll(r)
if err != nil {
return nil, err
}
return output, nil
}
+32
View File
@@ -0,0 +1,32 @@
/*
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 util
import (
"bufio"
"bytes"
"testing"
"github.com/GoogleContainerTools/kaniko/testutil"
)
func TestGetInputFrom(t *testing.T) {
validInput := []byte("Valid\n")
validReader := bufio.NewReader(bytes.NewReader((validInput)))
validValue, err := GetInputFrom(validReader)
testutil.CheckErrorAndDeepEqual(t, false, err, validInput, validValue)
}