This commit is contained in:
Prashant Arya
2019-12-19 03:20:56 +00:00
28 changed files with 1996 additions and 216 deletions
+191
View File
@@ -0,0 +1,191 @@
# This file contains all available configuration options
# with their default values.
# options for analysis running
run:
# default concurrency is a available CPU number
concurrency: 4
# timeout for analysis, e.g. 30s, 5m, default is 1m
deadline: 1m
# exit code when at least one issue was found, default is 1
issues-exit-code: 1
# include test files or not, default is true
tests: true
# list of build tags, all linters use it. Default is empty list.
build-tags:
# which dirs to skip: they won't be analyzed;
# can use regexp here: generated.*, regexp is applied on full path;
# default value is empty list, but next dirs are always skipped independently
# from this option's value:
# vendor$, third_party$, testdata$, examples$, Godeps$, builtin$
skip-dirs:
# which files to skip: they will be analyzed, but issues from them
# won't be reported. Default value is empty list, but there is
# no need to include all autogenerated files, we confidently recognize
# autogenerated files. If it's not please let us know.
skip-files:
# output configuration options
output:
# colored-line-number|line-number|json|tab|checkstyle, default is "colored-line-number"
format: colored-line-number
# print lines of code with issue, default is true
print-issued-lines: true
# print linter name in the end of issue text, default is true
print-linter-name: true
# all available settings of specific linters
linters-settings:
errcheck:
# report about not checking of errors in type assetions: `a := b.(MyStruct)`;
# default is false: such cases aren't reported by default.
check-type-assertions: false
# report about assignment of errors to blank identifier: `num, _ := strconv.Atoi(numStr)`;
# default is false: such cases aren't reported by default.
check-blank: false
govet:
# report about shadowed variables
#check-shadowing: true
# Obtain type information from installed (to $GOPATH/pkg) package files:
# golangci-lint will execute `go install -i` and `go test -i` for analyzed packages
# before analyzing them.
# By default this option is disabled and govet gets type information by loader from source code.
# Loading from source code is slow, but it's done only once for all linters.
# Go-installing of packages first time is much slower than loading them from source code,
# therefore this option is disabled by default.
# But repeated installation is fast in go >= 1.10 because of build caching.
# Enable this option only if all conditions are met:
# 1. you use only "fast" linters (--fast e.g.): no program loading occurs
# 2. you use go >= 1.10
# 3. you do repeated runs (false for CI) or cache $GOPATH/pkg or `go env GOCACHE` dir in CI.
#use-installed-packages: false
golint:
# minimal confidence for issues, default is 0.8
min-confidence: 0.8
gofmt:
# simplify code: gofmt with `-s` option, true by default
simplify: true
#gocyclo:
# # minimal code complexity to report, 30 by default (but we recommend 10-20)
# min-complexity: 10
maligned:
# print struct with more effective memory layout or not, false by default
suggest-new: true
#dupl:
# # tokens count to trigger issue, 150 by default
# threshold: 100
goconst:
# minimal length of string constant, 3 by default
min-len: 3
# minimal occurrences count to trigger, 3 by default
min-occurrences: 3
#depguard:
# list-type: blacklist
# include-go-root: false
# packages:
# - github.com/davecgh/go-spew/spew
misspell:
# Correct spellings using locale preferences for US or UK.
# Default is to use a neutral variety of English.
# Setting locale to US will correct the British spelling of 'colour' to 'color'.
locale: US
#lll:
# # max line length, lines longer will be reported. Default is 120.
# # '\t' is counted as 1 character by default, and can be changed with the tab-width option
# line-length: 120
# # tab width in spaces. Default to 1.
# tab-width: 1
unused:
# treat code as a program (not a library) and report unused exported identifiers; default is false.
# XXX: if you enable this setting, unused will report a lot of false-positives in text editors:
# if it's called for subdir of a project it can't find funcs usages. All text editor integrations
# with golangci-lint call it on a directory with the changed file.
check-exported: false
unparam:
# call graph construction algorithm (cha, rta). In general, use cha for libraries,
# and rta for programs with main packages. Default is cha.
algo: cha
# Inspect exported functions, default is false. Set to true if no external program/library imports your code.
# XXX: if you enable this setting, unparam will report a lot of false-positives in text editors:
# if it's called for subdir of a project it can't find external interfaces. All text editor integrations
# with golangci-lint call it on a directory with the changed file.
check-exported: false
#nakedret:
# # make an issue if func has more lines of code than this setting and it has naked returns; default is 30
# max-func-lines: 30
#prealloc:
# # XXX: we don't recommend using this linter before doing performance profiling.
# # For most programs usage of prealloc will be a premature optimization.
# # Report preallocation suggestions only on simple loops that have no returns/breaks/continues/gotos in them.
# # True by default.
# simple: true
# range-loops: true # Report preallocation suggestions on range loops, true by default
# for-loops: false # Report preallocation suggestions on for loops, false by default
linters:
enable:
- goconst
- goimports
- golint
- interfacer
- maligned
- misspell
- unconvert
- unparam
enable-all: false
disable:
- errcheck
- gas
disable-all: false
presets:
- bugs
- unused
fast: false
issues:
# List of regexps of issue texts to exclude, empty list by default.
# But independently from this option we use default exclude patterns,
# it can be disabled by `exclude-use-default: false`. To list all
# excluded by default patterns execute `golangci-lint run --help`
exclude:
# Independently from option `exclude` we use default exclude patterns,
# it can be disabled by this option. To list all
# excluded by default patterns execute `golangci-lint run --help`.
# Default value for this option is true.
exclude-use-default: true
# Maximum issues count per one linter. Set to 0 to disable. Default is 50.
max-per-linter: 50
# Maximum count of issues with the same text. Set to 0 to disable. Default is 3.
max-same: 3
# Show only new issues: if there are unstaged changes or untracked files,
# only those changes are analyzed, else only changes in HEAD~ are analyzed.
# It's a super-useful option for integration of golangci-lint into existing
# large codebase. It's not practical to fix all existing issues at the moment
# of integration: much better don't allow issues in new code.
# Default is false.
new: false
## Show only new issues created after git revision `REV`
#new-from-rev: REV
## Show only new issues created in git patch with set file path.
#new-from-patch: path/to/patch/file
+2 -2
View File
@@ -41,7 +41,7 @@ specifically:
- Start with a subject line
- Contain a body that explains _why_ you're making the change you're making
- Reference an issue number one exists, closing it if applicable (with text such as
- Reference an issue number if one exists, closing it if applicable (with text such as
["Fixes #245" or "Closes #111"](https://help.github.com/articles/closing-issues-using-keywords/))
Aim for [2 paragraphs in the body](https://www.youtube.com/watch?v=PJjmw9TRB7s).
@@ -67,4 +67,4 @@ interesting to work on:
- To find issues that we particularly would like contributors to tackle, look for
[issues with the "help wanted" label](https://github.com/GoogleContainerTools/kaniko/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22).
- Issues that are good for new folks will additionally be marked with
["good first issue"](https://github.com/GoogleContainerTools/kaniko/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22).
["good first issue"](https://github.com/GoogleContainerTools/kaniko/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22).
+29 -26
View File
@@ -46,26 +46,26 @@ _If you are interested in contributing to kaniko, see [DEVELOPMENT.md](DEVELOPME
- [Pushing to Docker Hub](#pushing-to-docker-hub)
- [Pushing to Amazon ECR](#pushing-to-amazon-ecr)
- [Additional Flags](#additional-flags)
- [--build-arg](#build-arg)
- [--cache](#cache)
- [--cache-dir](#cache-dir)
- [--cache-repo](#cache-repo)
- [--digest-file](#digest-file)
- [--oci-layout-path](#oci-layout-path)
- [--insecure-registry](#insecure-registry)
- [--skip-tls-verify-registry](#skip-tls-verify-registry)
- [--cleanup](#cleanup)
- [--insecure](#insecure)
- [--insecure-pull](#insecure-pull)
- [--no-push](#no-push)
- [--reproducible](#reproducible)
- [--single-snapshot](#single-snapshot)
- [--skip-tls-verify](#skip-tls-verify)
- [--skip-tls-verify-pull](#skip-tls-verify-pull)
- [--snapshotMode](#snapshotmode)
- [--target](#target)
- [--tarPath](#tarpath)
- [--verbosity](#verbosity)
- [--build-arg](#--build-arg)
- [--cache](#--cache)
- [--cache-dir](#--cache-dir)
- [--cache-repo](#--cache-repo)
- [--digest-file](#--digest-file)
- [--oci-layout-path](#--oci-layout-path)
- [--insecure-registry](#--insecure-registry)
- [--skip-tls-verify-registry](#--skip-tls-verify-registry)
- [--cleanup](#--cleanup)
- [--insecure](#--insecure)
- [--insecure-pull](#--insecure-pull)
- [--no-push](#--no-push)
- [--reproducible](#--reproducible)
- [--single-snapshot](#--single-snapshot)
- [--skip-tls-verify](#--skip-tls-verify)
- [--skip-tls-verify-pull](#--skip-tls-verify-pull)
- [--snapshotMode](#--snapshotmode)
- [--target](#--target)
- [--tarPath](#--tarpath)
- [--verbosity](#--verbosity)
- [Debug Image](#debug-image)
- [Security](#security)
- [Comparison with Other Tools](#comparison-with-other-tools)
@@ -87,7 +87,10 @@ After each command, we append a layer of changed files to the base image (if the
## Known Issues
kaniko does not support building Windows containers.
* kaniko does not support building Windows containers.
* Running kaniko in any Docker image other than the official kaniko image is not supported (ie YMMV).
* This includes copying the kaniko executables from the official image into another image.
* kaniko does not support the v1 Registry API ([Registry v1 API Deprecation](https://engineering.docker.com/2019/03/registry-v1-api-deprecation/))
## Demo
@@ -284,7 +287,7 @@ as a remote image destination:
### Caching
#### Caching Layers
kaniko currently can cache layers created by `RUN` commands in a remote repository.
kaniko can cache layers created by `RUN` commands in a remote repository.
Before executing a command, kaniko checks the cache for the layer.
If it exists, kaniko will pull and extract the cached layer instead of executing the command.
If not, kaniko will execute the command and then push the newly created layer to the cache.
@@ -295,7 +298,7 @@ If this flag isn't provided, a cached repo will be inferred from the `--destinat
#### Caching Base Images
kaniko can cache images in a local directory that can be volume mounted into the kaniko image.
kaniko can cache images in a local directory that can be volume mounted into the kaniko pod.
To do so, the cache must first be populated, as it is read-only. We provide a kaniko cache warming
image at `gcr.io/kaniko-project/warmer`:
@@ -306,7 +309,7 @@ docker run -v $(pwd):/workspace gcr.io/kaniko-project/warmer:latest --cache-dir=
`--image` can be specified for any number of desired images.
This command will cache those images by digest in a local directory named `cache`.
Once the cache is populated, caching is opted into with the same `--cache=true` flag as above.
The location of the local cache is provided via the `--cache-dir` flag, defaulting at `/cache` as with the cache warmer.
The location of the local cache is provided via the `--cache-dir` flag, defaulting to `/cache` as with the cache warmer.
See the `examples` directory for how to use with kubernetes clusters and persistent cache volumes.
### Pushing to Different Registries
@@ -319,7 +322,7 @@ kaniko comes with support for GCR, Docker `config.json` and Amazon ECR, but conf
Get your docker registry user and password encoded in base64
echo USER:PASSWORD | base64
echo -n USER:PASSWORD | base64
Create a `config.json` file with your Docker registry url and the previous generated base64 string
@@ -342,7 +345,7 @@ Run kaniko with the `config.json` inside `/kaniko/.docker/config.json`
The Amazon ECR [credential helper](https://github.com/awslabs/amazon-ecr-credential-helper) is built in to the kaniko executor image.
To configure credentials, you will need to do the following:
1. Update the `credHelpers` section of [config.json](https://github.com/GoogleContainerTools/kaniko/blob/master/files/config.json) with the specific URI of your ECR registry:
1. Update the `credHelpers` section of [config.json](https://github.com/awslabs/amazon-ecr-credential-helper#configuration) with the specific URI of your ECR registry:
```json
{
+1 -12
View File
@@ -23,15 +23,4 @@ if ! [ -x "$(command -v golangci-lint)" ]; then
${DIR}/install_golint.sh -b $GOPATH/bin v1.9.3
fi
golangci-lint run \
--no-config \
-E goconst \
-E goimports \
-E golint \
-E interfacer \
-E maligned \
-E misspell \
-E unconvert \
-E unparam \
-D errcheck \
-D gas
golangci-lint run
@@ -0,0 +1,2 @@
FROM phusion/baseimage:0.11
ADD context/foo /etc/service/foo
+69 -59
View File
@@ -32,7 +32,6 @@ import (
"github.com/google/go-containerregistry/pkg/name"
"github.com/google/go-containerregistry/pkg/v1/daemon"
"golang.org/x/sync/errgroup"
"github.com/GoogleContainerTools/kaniko/pkg/timing"
"github.com/GoogleContainerTools/kaniko/pkg/util"
@@ -42,40 +41,6 @@ import (
var config = initGCPConfig()
var imageBuilder *DockerFileBuilder
type gcpConfig struct {
gcsBucket string
imageRepo string
onbuildBaseImage string
hardlinkBaseImage string
}
type imageDetails struct {
name string
numLayers int
digest string
}
func (i imageDetails) String() string {
return fmt.Sprintf("Image: [%s] Digest: [%s] Number of Layers: [%d]", i.name, i.digest, i.numLayers)
}
func initGCPConfig() *gcpConfig {
var c gcpConfig
flag.StringVar(&c.gcsBucket, "bucket", "gs://kaniko-test-bucket", "The gcs bucket argument to uploaded the tar-ed contents of the `integration` dir to.")
flag.StringVar(&c.imageRepo, "repo", "gcr.io/kaniko-test", "The (docker) image repo to build and push images to during the test. `gcloud` must be authenticated with this repo.")
flag.Parse()
if c.gcsBucket == "" || c.imageRepo == "" {
log.Fatalf("You must provide a gcs bucket (\"%s\" was provided) and a docker repo (\"%s\" was provided)", c.gcsBucket, c.imageRepo)
}
if !strings.HasSuffix(c.imageRepo, "/") {
c.imageRepo = c.imageRepo + "/"
}
c.onbuildBaseImage = c.imageRepo + "onbuild-base:latest"
c.hardlinkBaseImage = c.imageRepo + "hardlink-base:latest"
return &c
}
const (
daemonPrefix = "daemon://"
dockerfilesPath = "dockerfiles"
@@ -102,19 +67,6 @@ const (
]`
)
func meetsRequirements() bool {
requiredTools := []string{"container-diff", "gsutil"}
hasRequirements := true
for _, tool := range requiredTools {
_, err := exec.LookPath(tool)
if err != nil {
fmt.Printf("You must have %s installed and on your PATH\n", tool)
hasRequirements = false
}
}
return hasRequirements
}
func TestMain(m *testing.M) {
if !meetsRequirements() {
fmt.Println("Missing required tools")
@@ -188,19 +140,9 @@ func TestMain(m *testing.M) {
}
imageBuilder = NewDockerFileBuilder(dockerfiles)
g := errgroup.Group{}
for dockerfile := range imageBuilder.FilesBuilt {
df := dockerfile
g.Go(func() error {
return imageBuilder.BuildImage(config.imageRepo, config.gcsBucket, dockerfilesPath, df)
})
}
if err := g.Wait(); err != nil {
fmt.Printf("Error building images: %s", err)
os.Exit(1)
}
os.Exit(m.Run())
}
func TestRun(t *testing.T) {
for dockerfile := range imageBuilder.FilesBuilt {
t.Run("test_"+dockerfile, func(t *testing.T) {
@@ -212,6 +154,9 @@ func TestRun(t *testing.T) {
if _, ok := imageBuilder.TestCacheDockerfiles[dockerfile]; ok {
t.SkipNow()
}
imageBuilder.FilesBuilt[dockerfile] = buildImage(t, dockerfile, imageBuilder)
dockerImage := GetDockerImage(config.imageRepo, dockerfile)
kanikoImage := GetKanikoImage(config.imageRepo, dockerfile)
@@ -329,10 +274,14 @@ func TestLayers(t *testing.T) {
for dockerfile := range imageBuilder.FilesBuilt {
t.Run("test_layer_"+dockerfile, func(t *testing.T) {
dockerfile := dockerfile
t.Parallel()
if _, ok := imageBuilder.DockerfilesToIgnore[dockerfile]; ok {
t.SkipNow()
}
imageBuilder.FilesBuilt[dockerfile] = buildImage(t, dockerfile, imageBuilder)
// Pull the kaniko image
dockerImage := GetDockerImage(config.imageRepo, dockerfile)
kanikoImage := GetKanikoImage(config.imageRepo, dockerfile)
@@ -348,6 +297,20 @@ func TestLayers(t *testing.T) {
}
}
func buildImage(t *testing.T, dockerfile string, imageBuilder *DockerFileBuilder) bool {
if imageBuilder.FilesBuilt[dockerfile] {
return true
}
if err := imageBuilder.BuildImage(
config.imageRepo, config.gcsBucket, dockerfilesPath, dockerfile,
); err != nil {
t.Errorf("Error building image: %s", err)
t.FailNow()
}
return true
}
// Build each image with kaniko twice, and then make sure they're exactly the same
func TestCache(t *testing.T) {
populateVolumeCache()
@@ -522,3 +485,50 @@ func logBenchmarks(benchmark string) error {
}
return nil
}
type gcpConfig struct {
gcsBucket string
imageRepo string
onbuildBaseImage string
hardlinkBaseImage string
}
type imageDetails struct {
name string
numLayers int
digest string
}
func (i imageDetails) String() string {
return fmt.Sprintf("Image: [%s] Digest: [%s] Number of Layers: [%d]", i.name, i.digest, i.numLayers)
}
func initGCPConfig() *gcpConfig {
var c gcpConfig
flag.StringVar(&c.gcsBucket, "bucket", "gs://kaniko-test-bucket", "The gcs bucket argument to uploaded the tar-ed contents of the `integration` dir to.")
flag.StringVar(&c.imageRepo, "repo", "gcr.io/kaniko-test", "The (docker) image repo to build and push images to during the test. `gcloud` must be authenticated with this repo.")
flag.Parse()
if c.gcsBucket == "" || c.imageRepo == "" {
log.Fatalf("You must provide a gcs bucket (\"%s\" was provided) and a docker repo (\"%s\" was provided)", c.gcsBucket, c.imageRepo)
}
if !strings.HasSuffix(c.imageRepo, "/") {
c.imageRepo = c.imageRepo + "/"
}
c.onbuildBaseImage = c.imageRepo + "onbuild-base:latest"
c.hardlinkBaseImage = c.imageRepo + "hardlink-base:latest"
return &c
}
func meetsRequirements() bool {
requiredTools := []string{"container-diff", "gsutil"}
hasRequirements := true
for _, tool := range requiredTools {
_, err := exec.LookPath(tool)
if err != nil {
fmt.Printf("You must have %s installed and on your PATH\n", tool)
hasRequirements = false
}
}
return hasRequirements
}
+3 -2
View File
@@ -28,7 +28,7 @@ import (
"github.com/GoogleContainerTools/kaniko/pkg/config"
"github.com/GoogleContainerTools/kaniko/pkg/creds"
"github.com/google/go-containerregistry/pkg/name"
"github.com/google/go-containerregistry/pkg/v1"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/remote"
"github.com/google/go-containerregistry/pkg/v1/tarball"
"github.com/pkg/errors"
@@ -124,7 +124,8 @@ func LocalSource(opts *config.CacheOptions, cacheKey string) (v1.Image, error) {
fi, err := os.Stat(path)
if err != nil {
return nil, errors.Wrap(err, "getting file info")
logrus.Debugf("No file found for cache key %v %v", cacheKey, err)
return nil, nil
}
// A stale cache is a bad cache
+4
View File
@@ -141,3 +141,7 @@ func (a *AddCommand) FilesUsedFromContext(config *v1.Config, buildArgs *dockerfi
func (a *AddCommand) MetadataOnly() bool {
return false
}
func (a *AddCommand) RequiresUnpackedFS() bool {
return true
}
+7
View File
@@ -17,6 +17,7 @@ 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"
@@ -24,6 +25,12 @@ import (
"github.com/sirupsen/logrus"
)
var RootDir string
func init() {
RootDir = constants.RootDir
}
type CurrentCacheKey func() (string, error)
type DockerCommand interface {
+85 -21
View File
@@ -17,6 +17,7 @@ limitations under the License.
package commands
import (
"fmt"
"os"
"path/filepath"
@@ -66,6 +67,14 @@ func (c *CopyCommand) ExecuteCommand(config *v1.Config, buildArgs *dockerfile.Bu
if err != nil {
return err
}
// If the destination dir is a symlink we need to resolve the path and use
// that instead of the symlink path
destPath, err = resolveIfSymlink(destPath)
if err != nil {
return err
}
if fi.IsDir() {
if !filepath.IsAbs(dest) {
// we need to add '/' to the end to indicate the destination is a directory
@@ -112,24 +121,7 @@ func (c *CopyCommand) String() string {
}
func (c *CopyCommand) FilesUsedFromContext(config *v1.Config, buildArgs *dockerfile.BuildArgs) ([]string, error) {
// We don't use the context if we're performing a copy --from.
if c.cmd.From != "" {
return nil, nil
}
replacementEnvs := buildArgs.ReplacementEnvs(config.Env)
srcs, _, err := util.ResolveEnvAndWildcards(c.cmd.SourcesAndDest, c.buildcontext, replacementEnvs)
if err != nil {
return nil, err
}
files := []string{}
for _, src := range srcs {
fullPath := filepath.Join(c.buildcontext, src)
files = append(files, fullPath)
}
logrus.Infof("Using files from context: %v", files)
return files, nil
return copyCmdFilesUsedFromContext(config, buildArgs, c.cmd, c.buildcontext)
}
func (c *CopyCommand) MetadataOnly() bool {
@@ -148,33 +140,105 @@ func (c *CopyCommand) ShouldCacheOutput() bool {
func (c *CopyCommand) CacheCommand(img v1.Image) DockerCommand {
return &CachingCopyCommand{
img: img,
cmd: c.cmd,
img: img,
cmd: c.cmd,
buildcontext: c.buildcontext,
extractFn: util.ExtractFile,
}
}
func (c *CopyCommand) From() string {
return c.cmd.From
}
type CachingCopyCommand struct {
BaseCommand
img v1.Image
extractedFiles []string
cmd *instructions.CopyCommand
buildcontext string
extractFn util.ExtractFunction
}
func (cr *CachingCopyCommand) ExecuteCommand(config *v1.Config, buildArgs *dockerfile.BuildArgs) error {
logrus.Infof("Found cached layer, extracting to filesystem")
var err error
cr.extractedFiles, err = util.GetFSFromImage(constants.RootDir, cr.img)
if cr.img == nil {
return errors.New(fmt.Sprintf("cached command image is nil %v", cr.String()))
}
cr.extractedFiles, err = util.GetFSFromImage(RootDir, cr.img, cr.extractFn)
logrus.Infof("extractedFiles: %s", cr.extractedFiles)
if err != nil {
return errors.Wrap(err, "extracting fs from image")
}
return nil
}
func (cr *CachingCopyCommand) FilesUsedFromContext(config *v1.Config, buildArgs *dockerfile.BuildArgs) ([]string, error) {
return copyCmdFilesUsedFromContext(config, buildArgs, cr.cmd, cr.buildcontext)
}
func (cr *CachingCopyCommand) FilesToSnapshot() []string {
return cr.extractedFiles
}
func (cr *CachingCopyCommand) String() string {
if cr.cmd == nil {
return "nil command"
}
return cr.cmd.String()
}
func (cr *CachingCopyCommand) From() string {
return cr.cmd.From
}
func resolveIfSymlink(destPath string) (string, error) {
baseDir := filepath.Dir(destPath)
if info, err := os.Lstat(baseDir); err == nil {
switch mode := info.Mode(); {
case mode&os.ModeSymlink != 0:
linkPath, err := os.Readlink(baseDir)
if err != nil {
return "", errors.Wrap(err, "error reading symlink")
}
absLinkPath := filepath.Join(filepath.Dir(baseDir), linkPath)
newPath := filepath.Join(absLinkPath, filepath.Base(destPath))
logrus.Tracef("Updating destination path from %v to %v due to symlink", destPath, newPath)
return newPath, nil
}
}
return destPath, nil
}
func copyCmdFilesUsedFromContext(
config *v1.Config, buildArgs *dockerfile.BuildArgs, cmd *instructions.CopyCommand,
buildcontext string,
) ([]string, error) {
// We don't use the context if we're performing a copy --from.
if cmd.From != "" {
return nil, nil
}
replacementEnvs := buildArgs.ReplacementEnvs(config.Env)
srcs, _, err := util.ResolveEnvAndWildcards(
cmd.SourcesAndDest, buildcontext, replacementEnvs,
)
if err != nil {
return nil, err
}
files := []string{}
for _, src := range srcs {
fullPath := filepath.Join(buildcontext, src)
files = append(files, fullPath)
}
logrus.Debugf("Using files from context: %v", files)
return files, nil
}
+199
View File
@@ -16,6 +16,8 @@ limitations under the License.
package commands
import (
"archive/tar"
"fmt"
"io"
"io/ioutil"
"os"
@@ -165,3 +167,200 @@ func copySetUpBuildArgs() *dockerfile.BuildArgs {
buildArgs.AddArg("buildArg2", &d)
return buildArgs
}
func Test_resolveIfSymlink(t *testing.T) {
type testCase struct {
destPath string
expectedPath string
err error
}
tmpDir, err := ioutil.TempDir("", "copy-test")
if err != nil {
t.Error(err)
}
baseDir, err := ioutil.TempDir(tmpDir, "not-linked")
if err != nil {
t.Error(err)
}
path, err := ioutil.TempFile(baseDir, "foo.txt")
if err != nil {
t.Error(err)
}
thepath, err := filepath.Abs(filepath.Dir(path.Name()))
if err != nil {
t.Error(err)
}
cases := []testCase{{destPath: thepath, expectedPath: thepath, err: nil}}
baseDir = tmpDir
symLink := filepath.Join(baseDir, "symlink")
if err := os.Symlink(filepath.Base(thepath), symLink); err != nil {
t.Error(err)
}
cases = append(cases, testCase{filepath.Join(symLink, "foo.txt"), filepath.Join(thepath, "foo.txt"), nil})
for i, c := range cases {
t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
res, e := resolveIfSymlink(c.destPath)
if e != c.err {
t.Errorf("%s: expected %v but got %v", c.destPath, c.err, e)
}
if res != c.expectedPath {
t.Errorf("%s: expected %v but got %v", c.destPath, c.expectedPath, res)
}
})
}
}
func Test_CachingCopyCommand_ExecuteCommand(t *testing.T) {
tempDir := setupTestTemp()
tarContent, err := prepareTarFixture([]string{"foo.txt"})
if err != nil {
t.Errorf("couldn't prepare tar fixture %v", err)
}
config := &v1.Config{}
buildArgs := &dockerfile.BuildArgs{}
type testCase struct {
desctiption string
expectLayer bool
expectErr bool
count *int
expectedCount int
command *CachingCopyCommand
extractedFiles []string
contextFiles []string
}
testCases := []testCase{
func() testCase {
err = ioutil.WriteFile(filepath.Join(tempDir, "foo.txt"), []byte("meow"), 0644)
if err != nil {
t.Errorf("couldn't write tempfile %v", err)
t.FailNow()
}
c := &CachingCopyCommand{
img: fakeImage{
ImageLayers: []v1.Layer{
fakeLayer{TarContent: tarContent},
},
},
buildcontext: tempDir,
cmd: &instructions.CopyCommand{
SourcesAndDest: []string{
"foo.txt", "foo.txt",
},
},
}
count := 0
tc := testCase{
desctiption: "with valid image and valid layer",
count: &count,
expectedCount: 1,
expectLayer: true,
extractedFiles: []string{"/foo.txt"},
contextFiles: []string{"foo.txt"},
}
c.extractFn = func(_ string, _ *tar.Header, _ io.Reader) error {
*tc.count++
return nil
}
tc.command = c
return tc
}(),
func() testCase {
c := &CachingCopyCommand{}
tc := testCase{
desctiption: "with no image",
expectErr: true,
}
c.extractFn = func(_ string, _ *tar.Header, _ io.Reader) error {
return nil
}
tc.command = c
return tc
}(),
func() testCase {
c := &CachingCopyCommand{
img: fakeImage{},
}
tc := testCase{
desctiption: "with image containing no layers",
}
c.extractFn = func(_ string, _ *tar.Header, _ io.Reader) error {
return nil
}
tc.command = c
return tc
}(),
func() testCase {
c := &CachingCopyCommand{
img: fakeImage{
ImageLayers: []v1.Layer{
fakeLayer{},
},
},
}
c.extractFn = func(_ string, _ *tar.Header, _ io.Reader) error {
return nil
}
tc := testCase{
desctiption: "with image one layer which has no tar content",
expectErr: false, // this one probably should fail but doesn't because of how ExecuteCommand and util.GetFSFromLayers are implemented - cvgw- 2019-11-25
expectLayer: true,
}
tc.command = c
return tc
}(),
}
for _, tc := range testCases {
t.Run(tc.desctiption, func(t *testing.T) {
c := tc.command
err := c.ExecuteCommand(config, buildArgs)
if !tc.expectErr && err != nil {
t.Errorf("Expected err to be nil but was %v", err)
} else if tc.expectErr && err == nil {
t.Error("Expected err but was nil")
}
if tc.count != nil {
if *tc.count != tc.expectedCount {
t.Errorf("Expected extractFn to be called %v times but was called %v times", tc.expectedCount, *tc.count)
}
for _, file := range tc.extractedFiles {
match := false
cFiles := c.FilesToSnapshot()
for _, cFile := range cFiles {
if file == cFile {
match = true
break
}
}
if !match {
t.Errorf("Expected extracted files to include %v but did not %v", file, cFiles)
}
}
cmdFiles, err := c.FilesUsedFromContext(
config, buildArgs,
)
if err != nil {
t.Errorf("failed to get files used from context from command %v", err)
}
if len(cmdFiles) != len(tc.contextFiles) {
t.Errorf("expected files used from context to equal %v but was %v", tc.contextFiles, cmdFiles)
}
}
})
}
}
+88
View File
@@ -0,0 +1,88 @@
/*
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.
*/
// used for testing in the commands package
package commands
import (
"bytes"
"io"
"io/ioutil"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/types"
)
type fakeLayer struct {
TarContent []byte
}
func (f fakeLayer) Digest() (v1.Hash, error) {
return v1.Hash{}, nil
}
func (f fakeLayer) DiffID() (v1.Hash, error) {
return v1.Hash{}, nil
}
func (f fakeLayer) Compressed() (io.ReadCloser, error) {
return nil, nil
}
func (f fakeLayer) Uncompressed() (io.ReadCloser, error) {
return ioutil.NopCloser(bytes.NewReader(f.TarContent)), nil
}
func (f fakeLayer) Size() (int64, error) {
return 0, nil
}
func (f fakeLayer) MediaType() (types.MediaType, error) {
return "", nil
}
type fakeImage struct {
ImageLayers []v1.Layer
}
func (f fakeImage) Layers() ([]v1.Layer, error) {
return f.ImageLayers, nil
}
func (f fakeImage) MediaType() (types.MediaType, error) {
return "", nil
}
func (f fakeImage) Size() (int64, error) {
return 0, nil
}
func (f fakeImage) ConfigName() (v1.Hash, error) {
return v1.Hash{}, nil
}
func (f fakeImage) ConfigFile() (*v1.ConfigFile, error) {
return &v1.ConfigFile{}, nil
}
func (f fakeImage) RawConfigFile() ([]byte, error) {
return []byte{}, nil
}
func (f fakeImage) Digest() (v1.Hash, error) {
return v1.Hash{}, nil
}
func (f fakeImage) Manifest() (*v1.Manifest, error) {
return &v1.Manifest{}, nil
}
func (f fakeImage) RawManifest() ([]byte, error) {
return []byte{}, nil
}
func (f fakeImage) LayerByDigest(v1.Hash) (v1.Layer, error) {
return fakeLayer{}, nil
}
func (f fakeImage) LayerByDiffID(v1.Hash) (v1.Layer, error) {
return fakeLayer{}, nil
}
+13 -3
View File
@@ -164,8 +164,9 @@ func (r *RunCommand) FilesToSnapshot() []string {
func (r *RunCommand) CacheCommand(img v1.Image) DockerCommand {
return &CachingRunCommand{
img: img,
cmd: r.cmd,
img: img,
cmd: r.cmd,
extractFn: util.ExtractFile,
}
}
@@ -186,15 +187,21 @@ type CachingRunCommand struct {
img v1.Image
extractedFiles []string
cmd *instructions.RunCommand
extractFn util.ExtractFunction
}
func (cr *CachingRunCommand) ExecuteCommand(config *v1.Config, buildArgs *dockerfile.BuildArgs) error {
logrus.Infof("Found cached layer, extracting to filesystem")
var err error
cr.extractedFiles, err = util.GetFSFromImage(constants.RootDir, cr.img)
if cr.img == nil {
return errors.New(fmt.Sprintf("command image is nil %v", cr.String()))
}
cr.extractedFiles, err = util.GetFSFromImage(constants.RootDir, cr.img, cr.extractFn)
if err != nil {
return errors.Wrap(err, "extracting fs from image")
}
return nil
}
@@ -203,5 +210,8 @@ func (cr *CachingRunCommand) FilesToSnapshot() []string {
}
func (cr *CachingRunCommand) String() string {
if cr.cmd == nil {
return "nil command"
}
return cr.cmd.String()
}
+193
View File
@@ -16,10 +16,19 @@ limitations under the License.
package commands
import (
"archive/tar"
"bytes"
"io"
"io/ioutil"
"log"
"os"
"os/user"
"path/filepath"
"testing"
"github.com/GoogleContainerTools/kaniko/pkg/dockerfile"
"github.com/GoogleContainerTools/kaniko/testutil"
v1 "github.com/google/go-containerregistry/pkg/v1"
)
func Test_addDefaultHOME(t *testing.T) {
@@ -120,3 +129,187 @@ func Test_addDefaultHOME(t *testing.T) {
})
}
}
func prepareTarFixture(fileNames []string) ([]byte, error) {
dir, err := ioutil.TempDir("/tmp", "tar-fixture")
if err != nil {
return nil, err
}
content := `
Meow meow meow meow
meow meow meow meow
`
for _, name := range fileNames {
if err := ioutil.WriteFile(filepath.Join(dir, name), []byte(content), 0777); err != nil {
return nil, err
}
}
writer := bytes.NewBuffer([]byte{})
tw := tar.NewWriter(writer)
defer tw.Close()
filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() {
return nil
}
hdr, err := tar.FileInfoHeader(info, "")
if err != nil {
return err
}
if err := tw.WriteHeader(hdr); err != nil {
log.Fatal(err)
}
body, err := ioutil.ReadFile(path)
if err != nil {
return err
}
if _, err := tw.Write(body); err != nil {
log.Fatal(err)
}
return nil
})
return writer.Bytes(), nil
}
func Test_CachingRunCommand_ExecuteCommand(t *testing.T) {
tarContent, err := prepareTarFixture([]string{"foo.txt"})
if err != nil {
t.Errorf("couldn't prepare tar fixture %v", err)
}
config := &v1.Config{}
buildArgs := &dockerfile.BuildArgs{}
type testCase struct {
desctiption string
expectLayer bool
expectErr bool
count *int
expectedCount int
command *CachingRunCommand
extractedFiles []string
contextFiles []string
}
testCases := []testCase{
func() testCase {
c := &CachingRunCommand{
img: fakeImage{
ImageLayers: []v1.Layer{
fakeLayer{TarContent: tarContent},
},
},
}
count := 0
tc := testCase{
desctiption: "with valid image and valid layer",
count: &count,
expectedCount: 1,
expectLayer: true,
extractedFiles: []string{"/foo.txt"},
contextFiles: []string{"foo.txt"},
}
c.extractFn = func(_ string, _ *tar.Header, _ io.Reader) error {
*tc.count++
return nil
}
tc.command = c
return tc
}(),
func() testCase {
c := &CachingRunCommand{}
tc := testCase{
desctiption: "with no image",
expectErr: true,
}
c.extractFn = func(_ string, _ *tar.Header, _ io.Reader) error {
return nil
}
tc.command = c
return tc
}(),
func() testCase {
c := &CachingRunCommand{
img: fakeImage{},
}
tc := testCase{
desctiption: "with image containing no layers",
}
c.extractFn = func(_ string, _ *tar.Header, _ io.Reader) error {
return nil
}
tc.command = c
return tc
}(),
func() testCase {
c := &CachingRunCommand{
img: fakeImage{
ImageLayers: []v1.Layer{
fakeLayer{},
},
},
}
c.extractFn = func(_ string, _ *tar.Header, _ io.Reader) error {
return nil
}
tc := testCase{
desctiption: "with image one layer which has no tar content",
expectErr: false, // this one probably should fail but doesn't because of how ExecuteCommand and util.GetFSFromLayers are implemented - cvgw- 2019-11-25
expectLayer: true,
}
tc.command = c
return tc
}(),
}
for _, tc := range testCases {
t.Run(tc.desctiption, func(t *testing.T) {
c := tc.command
err := c.ExecuteCommand(config, buildArgs)
if !tc.expectErr && err != nil {
t.Errorf("Expected err to be nil but was %v", err)
} else if tc.expectErr && err == nil {
t.Error("Expected err but was nil")
}
if tc.count != nil {
if *tc.count != tc.expectedCount {
t.Errorf("Expected extractFn to be called %v times but was called %v times", 1, *tc.count)
}
for _, file := range tc.extractedFiles {
match := false
cmdFiles := c.extractedFiles
for _, f := range cmdFiles {
if file == f {
match = true
break
}
}
if !match {
t.Errorf("Expected extracted files to include %v but did not %v", file, cmdFiles)
}
}
// CachingRunCommand does not override BaseCommand
// FilesUseFromContext so this will always return an empty slice and no error
// This seems like it might be a bug as it results in RunCommands and CachingRunCommands generating different cache keys - cvgw - 2019-11-27
cmdFiles, err := c.FilesUsedFromContext(
config, buildArgs,
)
if err != nil {
t.Errorf("failed to get files used from context from command")
}
if len(cmdFiles) != 0 {
t.Errorf("expected files used from context to be empty but was not")
}
}
})
}
}
+3 -1
View File
@@ -16,7 +16,9 @@ limitations under the License.
package config
import "github.com/moby/buildkit/frontend/dockerfile/instructions"
import (
"github.com/moby/buildkit/frontend/dockerfile/instructions"
)
// KanikoStage wraps a stage of the Dockerfile and provides extra information
type KanikoStage struct {
+2 -1
View File
@@ -203,6 +203,7 @@ func targetStage(stages []instructions.Stage, target string) (int, error) {
// 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 {
@@ -214,7 +215,7 @@ func resolveStages(stages []instructions.Stage) {
switch c := cmd.(type) {
case *instructions.CopyCommand:
if c.From != "" {
if val, ok := nameToIndex[c.From]; ok {
if val, ok := nameToIndex[strings.ToLower(c.From)]; ok {
c.From = val
}
+13 -4
View File
@@ -197,8 +197,14 @@ func Test_resolveStages(t *testing.T) {
FROM scratch AS second
COPY --from=0 /hi /hi2
FROM scratch
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 {
@@ -209,11 +215,14 @@ func Test_resolveStages(t *testing.T) {
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)
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)
}
}
}
}
+142 -56
View File
@@ -23,7 +23,7 @@ import (
"strconv"
"time"
"github.com/otiai10/copy"
otiai10Cpy "github.com/otiai10/copy"
"github.com/google/go-containerregistry/pkg/v1/partial"
@@ -52,21 +52,33 @@ import (
// This is the size of an empty tar in Go
const emptyTarSize = 1024
type cachePusher func(*config.KanikoOptions, string, string, string) error
type snapShotter interface {
Init() error
TakeSnapshotFS() (string, error)
TakeSnapshot([]string) (string, error)
}
// stageBuilder contains all fields necessary to build one stage of a Dockerfile
type stageBuilder struct {
stage config.KanikoStage
image v1.Image
cf *v1.ConfigFile
snapshotter *snapshot.Snapshotter
baseImageDigest string
opts *config.KanikoOptions
cmds []commands.DockerCommand
args *dockerfile.BuildArgs
crossStageDeps map[int][]string
stage config.KanikoStage
image v1.Image
cf *v1.ConfigFile
baseImageDigest string
finalCacheKey string
opts *config.KanikoOptions
cmds []commands.DockerCommand
args *dockerfile.BuildArgs
crossStageDeps map[int][]string
digestToCacheKey map[string]string
stageIdxToDigest map[string]string
snapshotter snapShotter
layerCache cache.LayerCache
pushCache cachePusher
}
// 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) (*stageBuilder, error) {
func newStageBuilder(opts *config.KanikoOptions, stage config.KanikoStage, crossStageDeps map[int][]string, dcm map[string]string, sid map[string]string) (*stageBuilder, error) {
sourceImage, err := util.RetrieveSourceImage(stage, opts)
if err != nil {
return nil, err
@@ -93,13 +105,19 @@ func newStageBuilder(opts *config.KanikoOptions, stage config.KanikoStage, cross
return nil, err
}
s := &stageBuilder{
stage: stage,
image: sourceImage,
cf: imageConfig,
snapshotter: snapshotter,
baseImageDigest: digest.String(),
opts: opts,
crossStageDeps: crossStageDeps,
stage: stage,
image: sourceImage,
cf: imageConfig,
snapshotter: snapshotter,
baseImageDigest: digest.String(),
opts: opts,
crossStageDeps: crossStageDeps,
digestToCacheKey: dcm,
stageIdxToDigest: sid,
layerCache: &cache.RegistryCache{
Opts: opts,
},
pushCache: pushLayerToCache,
}
for _, cmd := range s.stage.Commands {
@@ -130,15 +148,46 @@ func initializeConfig(img partial.WithConfigFile) (*v1.ConfigFile, error) {
return imageConfig, nil
}
func (s *stageBuilder) populateCompositeKey(command fmt.Stringer, files []string, compositeKey CompositeCache) (CompositeCache, error) {
// Add the next command to the cache key.
compositeKey.AddKey(command.String())
switch v := command.(type) {
case *commands.CopyCommand:
compositeKey = s.populateCopyCmdCompositeKey(command, v.From(), compositeKey)
case *commands.CachingCopyCommand:
compositeKey = s.populateCopyCmdCompositeKey(command, v.From(), compositeKey)
}
for _, f := range files {
if err := compositeKey.AddPath(f); err != nil {
return compositeKey, err
}
}
return compositeKey, nil
}
func (s *stageBuilder) populateCopyCmdCompositeKey(command fmt.Stringer, from string, compositeKey CompositeCache) CompositeCache {
if from != "" {
digest, ok := s.stageIdxToDigest[from]
if ok {
ds := digest
cacheKey, ok := s.digestToCacheKey[ds]
if ok {
logrus.Debugf("adding digest %v from previous stage to composite key for %v", ds, command.String())
compositeKey.AddKey(cacheKey)
}
}
}
return compositeKey
}
func (s *stageBuilder) optimize(compositeKey CompositeCache, cfg v1.Config) error {
if !s.opts.Cache {
return nil
}
layerCache := &cache.RegistryCache{
Opts: s.opts,
}
stopCache := false
// Possibly replace commands with their cached implementations.
// We walk through all the commands, running any commands that only operate on metadata.
// We throw the metadata away after, but we need it to properly track command dependencies
@@ -147,29 +196,34 @@ func (s *stageBuilder) optimize(compositeKey CompositeCache, cfg v1.Config) erro
if command == nil {
continue
}
compositeKey.AddKey(command.String())
// If the command uses files from the context, add them.
files, err := command.FilesUsedFromContext(&cfg, s.args)
if err != nil {
return err
}
for _, f := range files {
if err := compositeKey.AddPath(f); err != nil {
return err
}
return errors.Wrap(err, "failed to get files used from context")
}
ck, err := compositeKey.Hash()
compositeKey, err = s.populateCompositeKey(command, files, compositeKey)
if err != nil {
return err
}
if command.ShouldCacheOutput() {
img, err := layerCache.RetrieveLayer(ck)
logrus.Debugf("optimize: 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("optimize: cache key for command %v %v", command.String(), ck)
s.finalCacheKey = ck
if command.ShouldCacheOutput() && !stopCache {
img, err := s.layerCache.RetrieveLayer(ck)
if err != nil {
logrus.Debugf("Failed to retrieve layer: %s", err)
logrus.Infof("No cached layer found for cmd %s", command.String())
logrus.Debugf("Key missing was: %s", compositeKey.Key())
break
stopCache = true
continue
}
if cacheCmd := command.CacheCommand(img); cacheCmd != nil {
@@ -190,12 +244,18 @@ func (s *stageBuilder) optimize(compositeKey CompositeCache, cfg v1.Config) erro
func (s *stageBuilder) build() error {
// Set the initial cache key to be the base image digest, the build args and the SrcContext.
compositeKey := NewCompositeCache(s.baseImageDigest)
var compositeKey *CompositeCache
if cacheKey, ok := s.digestToCacheKey[s.baseImageDigest]; ok {
compositeKey = NewCompositeCache(cacheKey)
} else {
compositeKey = NewCompositeCache(s.baseImageDigest)
}
compositeKey.AddKey(s.opts.BuildArgs...)
// Apply optimizations to the instructions.
if err := s.optimize(*compositeKey, s.cf.Config); err != nil {
return err
return errors.Wrap(err, "failed to optimize instructions")
}
// Unpack file system to root if we need to.
@@ -213,21 +273,26 @@ func (s *stageBuilder) build() error {
if shouldUnpack {
t := timing.Start("FS Unpacking")
if _, err := util.GetFSFromImage(constants.RootDir, s.image); err != nil {
return err
if _, err := util.GetFSFromImage(constants.RootDir, s.image, util.ExtractFile); err != nil {
return errors.Wrap(err, "failed to get filesystem from image")
}
timing.DefaultRun.Stop(t)
} else {
logrus.Info("Skipping unpacking as no commands require it.")
}
if err := util.DetectFilesystemWhitelist(constants.WhitelistPath); err != nil {
return err
return errors.Wrap(err, "failed to check filesystem whitelist")
}
// Take initial snapshot
t := timing.Start("Initial FS snapshot")
if err := s.snapshotter.Init(); err != nil {
return err
}
timing.DefaultRun.Stop(t)
cacheGroup := errgroup.Group{}
@@ -236,23 +301,23 @@ func (s *stageBuilder) build() error {
continue
}
// Add the next command to the cache key.
compositeKey.AddKey(command.String())
t := timing.Start("Command: " + command.String())
// If the command uses files from the context, add them.
files, err := command.FilesUsedFromContext(&s.cf.Config, s.args)
if err != nil {
return errors.Wrap(err, "failed to get files used from context")
}
*compositeKey, err = s.populateCompositeKey(command, files, *compositeKey)
if err != nil {
return err
}
for _, f := range files {
if err := compositeKey.AddPath(f); err != nil {
return err
}
}
logrus.Info(command.String())
if err := command.ExecuteCommand(&s.cf.Config, s.args); err != nil {
return err
return errors.Wrap(err, "failed to execute command")
}
files = command.FilesToSnapshot()
timing.DefaultRun.Stop(t)
@@ -263,26 +328,31 @@ func (s *stageBuilder) build() error {
tarPath, err := s.takeSnapshot(files)
if err != nil {
return err
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 err
return errors.Wrap(err, "failed to hash composite key")
}
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 pushLayerToCache(s.opts, ck, tarPath, command.String())
return s.pushCache(s.opts, ck, tarPath, command.String())
})
}
if err := s.saveSnapshotToImage(command.String(), tarPath); err != nil {
return err
return errors.Wrap(err, "failed to save snapshot to image")
}
}
if err := cacheGroup.Wait(); err != nil {
logrus.Warnf("error uploading layer to cache: %s", err)
}
return nil
}
@@ -333,7 +403,7 @@ func (s *stageBuilder) saveSnapshotToImage(createdBy string, tarPath string) err
}
fi, err := os.Stat(tarPath)
if err != nil {
return err
return errors.Wrap(err, "tar file path does not exist")
}
if fi.Size() <= emptyTarSize {
logrus.Info("No files were changed, appending empty layer to config. No layer added to image.")
@@ -354,7 +424,6 @@ func (s *stageBuilder) saveSnapshotToImage(createdBy string, tarPath string) err
},
)
return err
}
func CalculateDependencies(opts *config.KanikoOptions) (map[int][]string, error) {
@@ -422,6 +491,9 @@ func CalculateDependencies(opts *config.KanikoOptions) (map[int][]string, error)
// DoBuild executes building the Dockerfile
func DoBuild(opts *config.KanikoOptions) (v1.Image, error) {
t := timing.Start("Total Build Time")
digestToCacheKey := make(map[string]string)
stageIdxToDigest := make(map[string]string)
// Parse dockerfile and unpack base image to root
stages, err := dockerfile.Stages(opts)
if err != nil {
@@ -442,18 +514,32 @@ func DoBuild(opts *config.KanikoOptions) (v1.Image, error) {
logrus.Infof("Built cross stage deps: %v", crossStageDependencies)
for index, stage := range stages {
sb, err := newStageBuilder(opts, stage, crossStageDependencies)
sb, err := newStageBuilder(opts, stage, crossStageDependencies, digestToCacheKey, stageIdxToDigest)
if err != nil {
return nil, err
}
if err := sb.build(); err != nil {
return nil, errors.Wrap(err, "error building stage")
}
reviewConfig(stage, &sb.cf.Config)
sourceImage, err := mutate.Config(sb.image, sb.cf.Config)
if err != nil {
return nil, err
}
d, err := sourceImage.Digest()
if err != nil {
return nil, err
}
stageIdxToDigest[fmt.Sprintf("%d", sb.stage.Index)] = d.String()
logrus.Debugf("mapping stage idx %v to digest %v", sb.stage.Index, d.String())
digestToCacheKey[d.String()] = sb.finalCacheKey
logrus.Debugf("mapping digest %v to cachekey %v", d.String(), sb.finalCacheKey)
if stage.Final {
sourceImage, err = mutate.CreatedAt(sourceImage, v1.Time{Time: time.Now()})
if err != nil {
@@ -489,7 +575,7 @@ func DoBuild(opts *config.KanikoOptions) (v1.Image, error) {
}
for _, p := range filesToSave {
logrus.Infof("Saving file %s for later use.", p)
copy.Copy(p, filepath.Join(dstDir, p))
otiai10Cpy.Copy(p, filepath.Join(dstDir, p))
}
// Delete the filesystem
@@ -567,7 +653,7 @@ func extractImageToDependencyDir(name string, image v1.Image) error {
return err
}
logrus.Debugf("trying to extract to %s", dependencyDir)
_, err := util.GetFSFromImage(dependencyDir, image)
_, err := util.GetFSFromImage(dependencyDir, image, util.ExtractFile)
return err
}
+531
View File
@@ -17,6 +17,9 @@ limitations under the License.
package executor
import (
"archive/tar"
"bytes"
"fmt"
"io/ioutil"
"os"
"path/filepath"
@@ -24,6 +27,7 @@ import (
"sort"
"testing"
"github.com/GoogleContainerTools/kaniko/pkg/commands"
"github.com/GoogleContainerTools/kaniko/pkg/config"
"github.com/GoogleContainerTools/kaniko/pkg/dockerfile"
"github.com/GoogleContainerTools/kaniko/testutil"
@@ -462,3 +466,530 @@ func TestInitializeConfig(t *testing.T) {
testutil.CheckDeepEqual(t, tt.expected, actual.Config)
}
}
func Test_stageBuilder_optimize(t *testing.T) {
testCases := []struct {
opts *config.KanikoOptions
retrieve bool
name string
}{
{
name: "cache enabled and layer not present in cache",
opts: &config.KanikoOptions{Cache: true},
},
{
name: "cache enabled and layer present in cache",
opts: &config.KanikoOptions{Cache: true},
retrieve: true,
},
{
name: "cache disabled and layer not present in cache",
opts: &config.KanikoOptions{Cache: false},
},
{
name: "cache disabled and layer present in cache",
opts: &config.KanikoOptions{Cache: false},
retrieve: true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
cf := &v1.ConfigFile{}
snap := fakeSnapShotter{}
lc := &fakeLayerCache{retrieve: tc.retrieve}
sb := &stageBuilder{opts: tc.opts, cf: cf, snapshotter: snap, layerCache: lc}
ck := CompositeCache{}
file, err := ioutil.TempFile("", "foo")
if err != nil {
t.Error(err)
}
command := MockDockerCommand{
contextFiles: []string{file.Name()},
cacheCommand: MockCachedDockerCommand{},
}
sb.cmds = []commands.DockerCommand{command}
err = sb.optimize(ck, cf.Config)
if err != nil {
t.Errorf("Expected error to be nil but was %v", err)
}
})
}
}
func Test_stageBuilder_build(t *testing.T) {
type testcase struct {
description string
opts *config.KanikoOptions
layerCache *fakeLayerCache
expectedCacheKeys []string
pushedCacheKeys []string
commands []commands.DockerCommand
fileName string
rootDir string
image v1.Image
config *v1.ConfigFile
}
testCases := []testcase{
func() testcase {
dir, files := tempDirAndFile(t)
file := files[0]
filePath := filepath.Join(dir, file)
ch := NewCompositeCache("", "meow")
ch.AddPath(filePath)
hash, err := ch.Hash()
if err != nil {
t.Errorf("couldn't create hash %v", err)
}
command := MockDockerCommand{
contextFiles: []string{filePath},
cacheCommand: MockCachedDockerCommand{
contextFiles: []string{filePath},
},
}
destDir, err := ioutil.TempDir("", "baz")
if err != nil {
t.Errorf("could not create temp dir %v", err)
}
return testcase{
description: "fake command cache enabled but key not in cache",
config: &v1.ConfigFile{Config: v1.Config{WorkingDir: destDir}},
opts: &config.KanikoOptions{Cache: true},
expectedCacheKeys: []string{hash},
pushedCacheKeys: []string{hash},
commands: []commands.DockerCommand{command},
rootDir: dir,
}
}(),
func() testcase {
dir, files := tempDirAndFile(t)
file := files[0]
filePath := filepath.Join(dir, file)
ch := NewCompositeCache("", "meow")
ch.AddPath(filePath)
hash, err := ch.Hash()
if err != nil {
t.Errorf("couldn't create hash %v", err)
}
command := MockDockerCommand{
contextFiles: []string{filePath},
cacheCommand: MockCachedDockerCommand{
contextFiles: []string{filePath},
},
}
destDir, err := ioutil.TempDir("", "baz")
if err != nil {
t.Errorf("could not create temp dir %v", err)
}
return testcase{
description: "fake command cache enabled and key in cache",
opts: &config.KanikoOptions{Cache: true},
config: &v1.ConfigFile{Config: v1.Config{WorkingDir: destDir}},
layerCache: &fakeLayerCache{
retrieve: true,
},
expectedCacheKeys: []string{hash},
pushedCacheKeys: []string{},
commands: []commands.DockerCommand{command},
rootDir: dir,
}
}(),
{
description: "fake command cache disabled and key not in cache",
opts: &config.KanikoOptions{Cache: false},
},
{
description: "fake command cache disabled and key in cache",
opts: &config.KanikoOptions{Cache: false},
layerCache: &fakeLayerCache{
retrieve: true,
},
},
func() testcase {
dir, filenames := tempDirAndFile(t)
filename := filenames[0]
filepath := filepath.Join(dir, filename)
tarContent := generateTar(t, dir, filename)
ch := NewCompositeCache("", "")
ch.AddPath(filepath)
hash, err := ch.Hash()
if err != nil {
t.Errorf("couldn't create hash %v", err)
}
copyCommandCacheKey := hash
return testcase{
description: "copy command cache enabled and key in cache",
opts: &config.KanikoOptions{Cache: true},
layerCache: &fakeLayerCache{
retrieve: true,
img: fakeImage{
ImageLayers: []v1.Layer{
fakeLayer{
TarContent: tarContent,
},
},
},
},
rootDir: dir,
expectedCacheKeys: []string{copyCommandCacheKey},
// CachingCopyCommand is not pushed to the cache
pushedCacheKeys: []string{},
commands: getCommands(dir, []instructions.Command{
&instructions.CopyCommand{
SourcesAndDest: []string{
filename, "foo.txt",
},
},
}),
fileName: filename,
}
}(),
func() testcase {
dir, filenames := tempDirAndFile(t)
filename := filenames[0]
tarContent := []byte{}
destDir, err := ioutil.TempDir("", "baz")
if err != nil {
t.Errorf("could not create temp dir %v", err)
}
filePath := filepath.Join(dir, filename)
ch := NewCompositeCache("", "")
ch.AddPath(filePath)
hash, err := ch.Hash()
if err != nil {
t.Errorf("couldn't create hash %v", err)
}
return testcase{
description: "copy command cache enabled and key is not in cache",
opts: &config.KanikoOptions{Cache: true},
config: &v1.ConfigFile{Config: v1.Config{WorkingDir: destDir}},
layerCache: &fakeLayerCache{},
image: fakeImage{
ImageLayers: []v1.Layer{
fakeLayer{
TarContent: tarContent,
},
},
},
rootDir: dir,
expectedCacheKeys: []string{hash},
pushedCacheKeys: []string{hash},
commands: getCommands(dir, []instructions.Command{
&instructions.CopyCommand{
SourcesAndDest: []string{
filename, "foo.txt",
},
},
}),
fileName: filename,
}
}(),
func() testcase {
dir, filenames := tempDirAndFile(t)
filename := filenames[0]
tarContent := generateTar(t, filename)
destDir, err := ioutil.TempDir("", "baz")
if err != nil {
t.Errorf("could not create temp dir %v", err)
}
filePath := filepath.Join(dir, filename)
ch := NewCompositeCache("", fmt.Sprintf("RUN foobar"))
hash1, err := ch.Hash()
if err != nil {
t.Errorf("couldn't create hash %v", err)
}
ch.AddKey(fmt.Sprintf("COPY %s bar.txt", filename))
ch.AddPath(filePath)
hash2, err := ch.Hash()
if err != nil {
t.Errorf("couldn't create hash %v", err)
}
ch = NewCompositeCache("", fmt.Sprintf("COPY %s foo.txt", filename))
ch.AddKey(fmt.Sprintf("COPY %s bar.txt", filename))
ch.AddPath(filePath)
image := fakeImage{
ImageLayers: []v1.Layer{
fakeLayer{
TarContent: tarContent,
},
},
}
dockerFile := fmt.Sprintf(`
FROM ubuntu:16.04
RUN foobar
COPY %s bar.txt
`, filename)
f, _ := ioutil.TempFile("", "")
ioutil.WriteFile(f.Name(), []byte(dockerFile), 0755)
opts := &config.KanikoOptions{
DockerfilePath: f.Name(),
}
stages, err := dockerfile.Stages(opts)
if err != nil {
t.Errorf("could not parse test dockerfile")
}
stage := stages[0]
cmds := stage.Commands
return testcase{
description: "cached run command followed by uncached copy command result in consistent read and write hashes",
opts: &config.KanikoOptions{Cache: true},
rootDir: dir,
config: &v1.ConfigFile{Config: v1.Config{WorkingDir: destDir}},
layerCache: &fakeLayerCache{
keySequence: []string{hash1},
img: image,
},
image: image,
// hash1 is the read cachekey for the first layer
// hash2 is the read cachekey for the second layer
expectedCacheKeys: []string{hash1, hash2},
pushedCacheKeys: []string{hash2},
commands: getCommands(dir, cmds),
}
}(),
func() testcase {
dir, filenames := tempDirAndFile(t)
filename := filenames[0]
tarContent := generateTar(t, filename)
destDir, err := ioutil.TempDir("", "baz")
if err != nil {
t.Errorf("could not create temp dir %v", err)
}
filePath := filepath.Join(dir, filename)
ch := NewCompositeCache("", fmt.Sprintf("COPY %s foo.txt", filename))
ch.AddPath(filePath)
hash1, err := ch.Hash()
if err != nil {
t.Errorf("couldn't create hash %v", err)
}
ch.AddKey(fmt.Sprintf("COPY %s bar.txt", filename))
ch.AddPath(filePath)
hash2, err := ch.Hash()
if err != nil {
t.Errorf("couldn't create hash %v", err)
}
ch = NewCompositeCache("", fmt.Sprintf("COPY %s foo.txt", filename))
ch.AddKey(fmt.Sprintf("COPY %s bar.txt", filename))
ch.AddPath(filePath)
image := fakeImage{
ImageLayers: []v1.Layer{
fakeLayer{
TarContent: tarContent,
},
},
}
dockerFile := fmt.Sprintf(`
FROM ubuntu:16.04
COPY %s foo.txt
COPY %s bar.txt
`, filename, filename)
f, _ := ioutil.TempFile("", "")
ioutil.WriteFile(f.Name(), []byte(dockerFile), 0755)
opts := &config.KanikoOptions{
DockerfilePath: f.Name(),
}
stages, err := dockerfile.Stages(opts)
if err != nil {
t.Errorf("could not parse test dockerfile")
}
stage := stages[0]
cmds := stage.Commands
return testcase{
description: "cached copy command followed by uncached copy command result in consistent read and write hashes",
opts: &config.KanikoOptions{Cache: true},
rootDir: dir,
config: &v1.ConfigFile{Config: v1.Config{WorkingDir: destDir}},
layerCache: &fakeLayerCache{
keySequence: []string{hash1},
img: image,
},
image: image,
// hash1 is the read cachekey for the first layer
// hash2 is the read cachekey for the second layer
expectedCacheKeys: []string{hash1, hash2},
pushedCacheKeys: []string{hash2},
commands: getCommands(dir, cmds),
}
}(),
}
for _, tc := range testCases {
t.Run(tc.description, func(t *testing.T) {
var fileName string
if tc.commands == nil {
file, err := ioutil.TempFile("", "foo")
if err != nil {
t.Error(err)
}
command := MockDockerCommand{
contextFiles: []string{file.Name()},
cacheCommand: MockCachedDockerCommand{
contextFiles: []string{file.Name()},
},
}
tc.commands = []commands.DockerCommand{command}
fileName = file.Name()
} else {
fileName = tc.fileName
}
cf := tc.config
if cf == nil {
cf = &v1.ConfigFile{
Config: v1.Config{
Env: make([]string, 0),
},
}
}
snap := fakeSnapShotter{file: fileName}
lc := tc.layerCache
if lc == nil {
lc = &fakeLayerCache{}
}
keys := []string{}
sb := &stageBuilder{
args: &dockerfile.BuildArgs{}, //required or code will panic
image: tc.image,
opts: tc.opts,
cf: cf,
snapshotter: snap,
layerCache: lc,
pushCache: func(_ *config.KanikoOptions, cacheKey, _, _ string) error {
keys = append(keys, cacheKey)
return nil
},
}
sb.cmds = tc.commands
tmp := commands.RootDir
if tc.rootDir != "" {
commands.RootDir = tc.rootDir
}
err := sb.build()
if err != nil {
t.Errorf("Expected error to be nil but was %v", err)
}
assertCacheKeys(t, tc.expectedCacheKeys, lc.receivedKeys, "receive")
assertCacheKeys(t, tc.pushedCacheKeys, keys, "push")
commands.RootDir = tmp
})
}
}
func assertCacheKeys(t *testing.T, expectedCacheKeys, actualCacheKeys []string, description string) {
if len(expectedCacheKeys) != len(actualCacheKeys) {
t.Errorf("expected to %v %v keys but was %v", description, len(expectedCacheKeys), len(actualCacheKeys))
}
sort.Slice(expectedCacheKeys, func(x, y int) bool {
return expectedCacheKeys[x] > expectedCacheKeys[y]
})
sort.Slice(actualCacheKeys, func(x, y int) bool {
return actualCacheKeys[x] > actualCacheKeys[y]
})
if len(expectedCacheKeys) != len(actualCacheKeys) {
t.Errorf("expected %v to equal %v", actualCacheKeys, expectedCacheKeys)
}
for i, key := range expectedCacheKeys {
if key != actualCacheKeys[i] {
t.Errorf("expected to %v keys %d to be %v but was %v %v", description, i, key, actualCacheKeys[i], actualCacheKeys)
}
}
}
func getCommands(dir string, cmds []instructions.Command) []commands.DockerCommand {
outCommands := make([]commands.DockerCommand, 0)
for _, c := range cmds {
cmd, err := commands.GetCommand(
c,
dir,
)
if err != nil {
panic(err)
}
outCommands = append(outCommands, cmd)
}
return outCommands
}
func tempDirAndFile(t *testing.T) (string, []string) {
filenames := []string{"bar.txt"}
dir, err := ioutil.TempDir("", "foo")
if err != nil {
t.Errorf("could not create temp dir %v", err)
}
for _, filename := range filenames {
filepath := filepath.Join(dir, filename)
err = ioutil.WriteFile(filepath, []byte(`meow`), 0777)
if err != nil {
t.Errorf("could not create temp file %v", err)
}
}
return dir, filenames
}
func generateTar(t *testing.T, dir string, fileNames ...string) []byte {
buf := bytes.NewBuffer([]byte{})
writer := tar.NewWriter(buf)
defer writer.Close()
for _, filename := range fileNames {
filePath := filepath.Join(dir, filename)
info, err := os.Stat(filePath)
if err != nil {
t.Errorf("could not get file info for temp file %v", err)
}
hdr, err := tar.FileInfoHeader(info, filename)
if err != nil {
t.Errorf("could not get tar header for temp file %v", err)
}
if err := writer.WriteHeader(hdr); err != nil {
t.Errorf("could not write tar header %v", err)
}
content, err := ioutil.ReadFile(filePath)
if err != nil {
t.Errorf("could not read tempfile %v", err)
}
if _, err := writer.Write(content); err != nil {
t.Errorf("could not write file contents to tar")
}
}
return buf.Bytes()
}
+3 -2
View File
@@ -18,6 +18,7 @@ package executor
import (
"crypto/sha256"
"fmt"
"os"
"path/filepath"
"strings"
@@ -75,7 +76,7 @@ func (s *CompositeCache) AddPath(p string) error {
return err
}
s.keys = append(s.keys, string(sha.Sum(nil)))
s.keys = append(s.keys, fmt.Sprintf("%x", sha.Sum(nil)))
return nil
}
@@ -98,5 +99,5 @@ func HashDir(p string) (string, error) {
return "", err
}
return string(sha.Sum(nil)), nil
return fmt.Sprintf("%x", sha.Sum(nil)), nil
}
+137
View File
@@ -0,0 +1,137 @@
/*
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 executor
import (
"io/ioutil"
"os"
"path/filepath"
"reflect"
"testing"
)
func Test_NewCompositeCache(t *testing.T) {
r := NewCompositeCache()
if reflect.TypeOf(r).String() != "*executor.CompositeCache" {
t.Errorf("expected return to be *executor.CompositeCache but was %v", reflect.TypeOf(r).String())
}
}
func Test_CompositeCache_AddKey(t *testing.T) {
keys := []string{
"meow",
"purr",
}
r := NewCompositeCache()
r.AddKey(keys...)
if len(r.keys) != 2 {
t.Errorf("expected keys to have length 2 but was %v", len(r.keys))
}
}
func Test_CompositeCache_Key(t *testing.T) {
r := NewCompositeCache("meow", "purr")
k := r.Key()
if k != "meow-purr" {
t.Errorf("expected result to equal meow-purr but was %v", k)
}
}
func Test_CompositeCache_Hash(t *testing.T) {
r := NewCompositeCache("meow", "purr")
h, err := r.Hash()
if err != nil {
t.Errorf("expected error to be nil but was %v", err)
}
expectedHash := "b4fd5a11af812a11a79d794007c842794cc668c8e7ebaba6d1e6d021b8e06c71"
if h != expectedHash {
t.Errorf("expected result to equal %v but was %v", expectedHash, h)
}
}
func Test_CompositeCache_AddPath_dir(t *testing.T) {
tmpDir, err := ioutil.TempDir("/tmp", "foo")
if err != nil {
t.Errorf("got error setting up test %v", err)
}
content := `meow meow meow`
if err := ioutil.WriteFile(filepath.Join(tmpDir, "foo.txt"), []byte(content), 0777); err != nil {
t.Errorf("got error writing temp file %v", err)
}
fn := func() string {
r := NewCompositeCache()
if err := r.AddPath(tmpDir); err != nil {
t.Errorf("expected error to be nil but was %v", err)
}
if len(r.keys) != 1 {
t.Errorf("expected len of keys to be 1 but was %v", len(r.keys))
}
hash, err := r.Hash()
if err != nil {
t.Errorf("couldnt generate hash from test cache")
}
return hash
}
hash1 := fn()
hash2 := fn()
if hash1 != hash2 {
t.Errorf("expected hash %v to equal hash %v", hash1, hash2)
}
}
func Test_CompositeCache_AddPath_file(t *testing.T) {
tmpfile, err := ioutil.TempFile("/tmp", "foo.txt")
if err != nil {
t.Errorf("got error setting up test %v", err)
}
defer os.Remove(tmpfile.Name()) // clean up
content := `meow meow meow`
if _, err := tmpfile.Write([]byte(content)); err != nil {
t.Errorf("got error writing temp file %v", err)
}
if err := tmpfile.Close(); err != nil {
t.Errorf("got error closing temp file %v", err)
}
p := tmpfile.Name()
fn := func() string {
r := NewCompositeCache()
if err := r.AddPath(p); err != nil {
t.Errorf("expected error to be nil but was %v", err)
}
if len(r.keys) != 1 {
t.Errorf("expected len of keys to be 1 but was %v", len(r.keys))
}
hash, err := r.Hash()
if err != nil {
t.Errorf("couldnt generate hash from test cache")
}
return hash
}
hash1 := fn()
hash2 := fn()
if hash1 != hash2 {
t.Errorf("expected hash %v to equal hash %v", hash1, hash2)
}
}
+184
View File
@@ -0,0 +1,184 @@
/*
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.
*/
// for use in tests
package executor
import (
"bytes"
"errors"
"io"
"io/ioutil"
"github.com/GoogleContainerTools/kaniko/pkg/commands"
"github.com/GoogleContainerTools/kaniko/pkg/dockerfile"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/types"
)
type fakeSnapShotter struct {
file string
tarPath string
}
func (f fakeSnapShotter) Init() error { return nil }
func (f fakeSnapShotter) TakeSnapshotFS() (string, error) {
return f.tarPath, nil
}
func (f fakeSnapShotter) TakeSnapshot(_ []string) (string, error) {
return f.tarPath, nil
}
type MockDockerCommand struct {
contextFiles []string
cacheCommand commands.DockerCommand
}
func (m MockDockerCommand) ExecuteCommand(c *v1.Config, args *dockerfile.BuildArgs) error { return nil }
func (m MockDockerCommand) String() string {
return "meow"
}
func (m MockDockerCommand) FilesToSnapshot() []string {
return []string{"meow-snapshot-no-cache"}
}
func (m MockDockerCommand) CacheCommand(image v1.Image) commands.DockerCommand {
return m.cacheCommand
}
func (m MockDockerCommand) FilesUsedFromContext(c *v1.Config, args *dockerfile.BuildArgs) ([]string, error) {
return m.contextFiles, nil
}
func (m MockDockerCommand) MetadataOnly() bool {
return false
}
func (m MockDockerCommand) RequiresUnpackedFS() bool {
return false
}
func (m MockDockerCommand) ShouldCacheOutput() bool {
return true
}
type MockCachedDockerCommand struct {
contextFiles []string
}
func (m MockCachedDockerCommand) ExecuteCommand(c *v1.Config, args *dockerfile.BuildArgs) error {
return nil
}
func (m MockCachedDockerCommand) String() string {
return "meow"
}
func (m MockCachedDockerCommand) FilesToSnapshot() []string {
return []string{"meow-snapshot"}
}
func (m MockCachedDockerCommand) CacheCommand(image v1.Image) commands.DockerCommand {
return nil
}
func (m MockCachedDockerCommand) FilesUsedFromContext(c *v1.Config, args *dockerfile.BuildArgs) ([]string, error) {
return m.contextFiles, nil
}
func (m MockCachedDockerCommand) MetadataOnly() bool {
return false
}
func (m MockCachedDockerCommand) RequiresUnpackedFS() bool {
return false
}
func (m MockCachedDockerCommand) ShouldCacheOutput() bool {
return false
}
type fakeLayerCache struct {
retrieve bool
receivedKeys []string
img v1.Image
keySequence []string
}
func (f *fakeLayerCache) RetrieveLayer(key string) (v1.Image, error) {
f.receivedKeys = append(f.receivedKeys, key)
if len(f.keySequence) > 0 {
if f.keySequence[0] == key {
f.keySequence = f.keySequence[1:]
return f.img, nil
}
return f.img, errors.New("could not find layer")
}
if !f.retrieve {
return nil, errors.New("could not find layer")
}
return f.img, nil
}
type fakeLayer struct {
TarContent []byte
}
func (f fakeLayer) Digest() (v1.Hash, error) {
return v1.Hash{}, nil
}
func (f fakeLayer) DiffID() (v1.Hash, error) {
return v1.Hash{}, nil
}
func (f fakeLayer) Compressed() (io.ReadCloser, error) {
return nil, nil
}
func (f fakeLayer) Uncompressed() (io.ReadCloser, error) {
return ioutil.NopCloser(bytes.NewReader(f.TarContent)), nil
}
func (f fakeLayer) Size() (int64, error) {
return 0, nil
}
func (f fakeLayer) MediaType() (types.MediaType, error) {
return "", nil
}
type fakeImage struct {
ImageLayers []v1.Layer
}
func (f fakeImage) Layers() ([]v1.Layer, error) {
return f.ImageLayers, nil
}
func (f fakeImage) MediaType() (types.MediaType, error) {
return "", nil
}
func (f fakeImage) Size() (int64, error) {
return 0, nil
}
func (f fakeImage) ConfigName() (v1.Hash, error) {
return v1.Hash{}, nil
}
func (f fakeImage) ConfigFile() (*v1.ConfigFile, error) {
return &v1.ConfigFile{}, nil
}
func (f fakeImage) RawConfigFile() ([]byte, error) {
return []byte{}, nil
}
func (f fakeImage) Digest() (v1.Hash, error) {
return v1.Hash{}, nil
}
func (f fakeImage) Manifest() (*v1.Manifest, error) {
return &v1.Manifest{}, nil
}
func (f fakeImage) RawManifest() ([]byte, error) {
return []byte{}, nil
}
func (f fakeImage) LayerByDigest(v1.Hash) (v1.Layer, error) {
return fakeLayer{}, nil
}
func (f fakeImage) LayerByDiffID(v1.Hash) (v1.Layer, error) {
return fakeLayer{}, nil
}
+1 -1
View File
@@ -151,7 +151,7 @@ func DoPush(image v1.Image, opts *config.KanikoOptions) error {
if opts.ImageNameDigestFile != "" {
err := ioutil.WriteFile(opts.ImageNameDigestFile, []byte(builder.String()), 0644)
if err != nil {
return errors.Wrap(err, "writing digest to file failed")
return errors.Wrap(err, "writing image name with digest to file failed")
}
}
+1 -1
View File
@@ -176,7 +176,7 @@ func (s *Snapshotter) scanFullFilesystem() ([]string, []string, error) {
// Only add changed files.
fileChanged, err := s.l.CheckFileChange(path)
if err != nil {
return nil, nil, err
return nil, nil, fmt.Errorf("could not check if file has changed %s %s", path, err)
}
if fileChanged {
logrus.Tracef("Adding %s to layer, because it was changed.", path)
+11 -6
View File
@@ -17,6 +17,7 @@ limitations under the License.
package util
import (
"fmt"
"net/http"
"net/url"
"os"
@@ -25,7 +26,7 @@ import (
"strings"
"github.com/GoogleContainerTools/kaniko/pkg/constants"
"github.com/google/go-containerregistry/pkg/v1"
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"
@@ -77,13 +78,16 @@ func ResolveEnvAndWildcards(sd instructions.SourcesAndDest, buildcontext string,
// First, resolve any environment replacement
resolvedEnvs, err := ResolveEnvironmentReplacementList(sd, envs, true)
if err != nil {
return nil, "", err
return nil, "", errors.Wrap(err, "failed to resolve environment")
}
if len(resolvedEnvs) == 0 {
return nil, "", errors.New("resolved envs is empty")
}
dest := resolvedEnvs[len(resolvedEnvs)-1]
// Resolve wildcards and get a list of resolved sources
srcs, err := ResolveSources(resolvedEnvs[0:len(resolvedEnvs)-1], buildcontext)
if err != nil {
return nil, "", err
return nil, "", errors.Wrap(err, "failed to resolve sources")
}
err = IsSrcsValid(sd, srcs, buildcontext)
return srcs, dest, err
@@ -219,9 +223,10 @@ func IsSrcsValid(srcsAndDest instructions.SourcesAndDest, resolvedSources []stri
if IsSrcRemoteFileURL(resolvedSources[0]) {
return nil
}
fi, err := os.Lstat(filepath.Join(root, resolvedSources[0]))
path := filepath.Join(root, resolvedSources[0])
fi, err := os.Lstat(path)
if err != nil {
return err
return errors.Wrap(err, fmt.Sprintf("failed to get fileinfo for %v", path))
}
if fi.IsDir() {
return nil
@@ -237,7 +242,7 @@ func IsSrcsValid(srcsAndDest instructions.SourcesAndDest, resolvedSources []stri
src = filepath.Clean(src)
files, err := RelativeFiles(src, root)
if err != nil {
return err
return errors.Wrap(err, "failed to get relative files")
}
for _, file := range files {
if excludeFile(file, root) {
+25 -5
View File
@@ -69,9 +69,17 @@ var volumes = []string{}
var excluded []string
type ExtractFunction func(string, *tar.Header, io.Reader) error
// GetFSFromImage extracts the layers of img to root
// It returns a list of all files extracted
func GetFSFromImage(root string, img v1.Image) ([]string, error) {
func GetFSFromImage(root string, img v1.Image, extract ExtractFunction) ([]string, error) {
if extract == nil {
return nil, errors.New("must supply an extract function")
}
if img == nil {
return nil, errors.New("image cannot be nil")
}
if err := DetectFilesystemWhitelist(constants.WhitelistPath); err != nil {
return nil, err
}
@@ -114,7 +122,7 @@ func GetFSFromImage(root string, img v1.Image) ([]string, error) {
}
continue
}
if err := extractFile(root, hdr, tr); err != nil {
if err := extract(root, hdr, tr); err != nil {
return nil, err
}
extractedFiles = append(extractedFiles, filepath.Join(root, filepath.Clean(hdr.Name)))
@@ -179,7 +187,7 @@ func unTar(r io.Reader, dest string) ([]string, error) {
if err != nil {
return nil, err
}
if err := extractFile(dest, hdr, tr); err != nil {
if err := ExtractFile(dest, hdr, tr); err != nil {
return nil, err
}
extractedFiles = append(extractedFiles, dest)
@@ -187,7 +195,7 @@ func unTar(r io.Reader, dest string) ([]string, error) {
return extractedFiles, nil
}
func extractFile(dest string, hdr *tar.Header, tr io.Reader) error {
func ExtractFile(dest string, hdr *tar.Header, tr io.Reader) error {
path := filepath.Join(dest, filepath.Clean(hdr.Name))
base := filepath.Base(path)
dir := filepath.Dir(path)
@@ -424,11 +432,17 @@ func FilepathExists(path string) bool {
func CreateFile(path string, reader io.Reader, perm os.FileMode, uid uint32, gid uint32) error {
// Create directory path if it doesn't exist
baseDir := filepath.Dir(path)
if _, err := os.Lstat(baseDir); os.IsNotExist(err) {
if info, err := os.Lstat(baseDir); os.IsNotExist(err) {
logrus.Tracef("baseDir %s for file %s does not exist. Creating.", baseDir, path)
if err := os.MkdirAll(baseDir, 0755); err != nil {
return err
}
} else {
switch mode := info.Mode(); {
case mode&os.ModeSymlink != 0:
logrus.Infof("destination cannot be a symlink %v", baseDir)
return errors.New("destination cannot be a symlink")
}
}
dest, err := os.Create(path)
if err != nil {
@@ -545,6 +559,12 @@ func CopyFile(src, dest, buildcontext string) (bool, error) {
logrus.Debugf("%s found in .dockerignore, ignoring", src)
return true, nil
}
if src == dest {
// This is a no-op. Move on, but don't list it as ignored.
// We have to make sure we do this so we don't overwrite our own file.
// See iusse #904 for an example.
return false, nil
}
fi, err := os.Stat(src)
if err != nil {
return false, err
+39 -1
View File
@@ -659,7 +659,7 @@ func TestExtractFile(t *testing.T) {
defer os.RemoveAll(r)
for _, hdr := range tc.hdrs {
if err := extractFile(r, hdr, bytes.NewReader(tc.contents)); err != nil {
if err := ExtractFile(r, hdr, bytes.NewReader(tc.contents)); err != nil {
t.Fatal(err)
}
}
@@ -825,3 +825,41 @@ func Test_correctDockerignoreFileIsUsed(t *testing.T) {
}
}
}
func Test_CopyFile_skips_self(t *testing.T) {
t.Parallel()
tempDir, err := ioutil.TempDir("", "kaniko_test")
if err != nil {
t.Fatal(err)
}
tempFile := filepath.Join(tempDir, "foo")
expected := "bar"
if err := ioutil.WriteFile(
tempFile,
[]byte(expected),
0755,
); err != nil {
t.Fatal(err)
}
ignored, err := CopyFile(tempFile, tempFile, "")
if err != nil {
t.Fatal(err)
}
if ignored {
t.Fatal("expected file to NOT be ignored")
}
// Ensure file has expected contents
actualData, err := ioutil.ReadFile(tempFile)
if err != nil {
t.Fatal(err)
}
if actual := string(actualData); actual != expected {
t.Fatalf("expected file contents to be %q, but got %q", expected, actual)
}
}
+18 -13
View File
@@ -74,15 +74,13 @@ func RetrieveSourceImage(stage config.KanikoStage, opts *config.KanikoOptions) (
// If so, look in the local cache before trying the remote registry
if opts.CacheDir != "" {
cachedImage, err := cachedImage(opts, currentBaseName)
if cachedImage != nil {
if err != nil {
logrus.Errorf("Error while retrieving image from cache: %v %v", currentBaseName, err)
} else if cachedImage != nil {
return cachedImage, nil
}
if err != nil {
logrus.Infof("Error while retrieving image from cache: %v", err)
}
}
logrus.Infof("Image %v not found in cache", currentBaseName)
// Otherwise, initialize image as usual
return RetrieveRemoteImage(currentBaseName, opts)
}
@@ -93,13 +91,23 @@ func tarballImage(index int) (v1.Image, error) {
return tarball.ImageFromPath(tarPath, nil)
}
// Retrieves the manifest for the specified image from the specified registry
func remoteImage(image string, opts *config.KanikoOptions) (v1.Image, error) {
logrus.Infof("Downloading base image %s", image)
logrus.Infof("Retrieving image manifest %s", image)
ref, err := name.ParseReference(image, name.WeakValidation)
if err != nil {
return nil, err
}
rOpts, err := prepareRemoteRequest(ref, opts)
if err != nil {
return nil, err
}
return remote.Image(ref, rOpts...)
}
func prepareRemoteRequest(ref name.Reference, opts *config.KanikoOptions) ([]remote.Option, error) {
registryName := ref.Context().RegistryStr()
if opts.InsecurePull || opts.InsecureRegistries.Contains(registryName) {
newReg, err := name.NewRegistry(registryName, name.WeakValidation, name.Insecure)
@@ -122,8 +130,7 @@ func remoteImage(image string, opts *config.KanikoOptions) (v1.Image, error) {
InsecureSkipVerify: true,
}
}
return remote.Image(ref, remote.WithTransport(tr), remote.WithAuthFromKeychain(creds.GetKeychain()))
return []remote.Option{remote.WithTransport(tr), remote.WithAuthFromKeychain(creds.GetKeychain())}, nil
}
func cachedImage(opts *config.KanikoOptions, image string) (v1.Image, error) {
@@ -136,18 +143,16 @@ func cachedImage(opts *config.KanikoOptions, image string) (v1.Image, error) {
if d, ok := ref.(name.Digest); ok {
cacheKey = d.DigestStr()
} else {
img, err := remoteImage(image, opts)
image, err := remoteImage(image, opts)
if err != nil {
return nil, err
}
d, err := img.Digest()
d, err := image.Digest()
if err != nil {
return nil, err
}
cacheKey = d.String()
}
return cache.LocalSource(&opts.CacheOptions, cacheKey)
}