Merge branch 'master' into fix-toc-in-readme
This commit is contained in:
commit
c54d1a5e72
|
|
@ -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
|
||||||
13
README.md
13
README.md
|
|
@ -88,7 +88,10 @@ After each command, we append a layer of changed files to the base image (if the
|
||||||
|
|
||||||
## Known Issues
|
## 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
|
## Demo
|
||||||
|
|
||||||
|
|
@ -285,7 +288,7 @@ as a remote image destination:
|
||||||
### Caching
|
### Caching
|
||||||
|
|
||||||
#### Caching Layers
|
#### 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.
|
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 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.
|
If not, kaniko will execute the command and then push the newly created layer to the cache.
|
||||||
|
|
@ -296,7 +299,7 @@ If this flag isn't provided, a cached repo will be inferred from the `--destinat
|
||||||
|
|
||||||
#### Caching Base Images
|
#### 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
|
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`:
|
image at `gcr.io/kaniko-project/warmer`:
|
||||||
|
|
||||||
|
|
@ -307,7 +310,7 @@ docker run -v $(pwd):/workspace gcr.io/kaniko-project/warmer:latest --cache-dir=
|
||||||
`--image` can be specified for any number of desired images.
|
`--image` can be specified for any number of desired images.
|
||||||
This command will cache those images by digest in a local directory named `cache`.
|
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.
|
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.
|
See the `examples` directory for how to use with kubernetes clusters and persistent cache volumes.
|
||||||
|
|
||||||
### Pushing to Different Registries
|
### Pushing to Different Registries
|
||||||
|
|
@ -343,7 +346,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.
|
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:
|
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
|
```json
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -23,15 +23,4 @@ if ! [ -x "$(command -v golangci-lint)" ]; then
|
||||||
${DIR}/install_golint.sh -b $GOPATH/bin v1.9.3
|
${DIR}/install_golint.sh -b $GOPATH/bin v1.9.3
|
||||||
fi
|
fi
|
||||||
|
|
||||||
golangci-lint run \
|
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
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
FROM phusion/baseimage:0.11
|
||||||
|
ADD context/foo /etc/service/foo
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
FROM busybox:1.31
|
||||||
|
|
||||||
|
ARG REPO=kaniko
|
||||||
|
ARG VERSION=v0.14.0
|
||||||
|
|
||||||
|
ADD https://github.com/GoogleContainerTools/$REPO/archive/$VERSION.tar.gz /tmp/release.tar.gz
|
||||||
|
|
@ -28,7 +28,7 @@ import (
|
||||||
"github.com/GoogleContainerTools/kaniko/pkg/config"
|
"github.com/GoogleContainerTools/kaniko/pkg/config"
|
||||||
"github.com/GoogleContainerTools/kaniko/pkg/creds"
|
"github.com/GoogleContainerTools/kaniko/pkg/creds"
|
||||||
"github.com/google/go-containerregistry/pkg/name"
|
"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/remote"
|
||||||
"github.com/google/go-containerregistry/pkg/v1/tarball"
|
"github.com/google/go-containerregistry/pkg/v1/tarball"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
|
@ -124,7 +124,8 @@ func LocalSource(opts *config.CacheOptions, cacheKey string) (v1.Image, error) {
|
||||||
|
|
||||||
fi, err := os.Stat(path)
|
fi, err := os.Stat(path)
|
||||||
if err != nil {
|
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
|
// A stale cache is a bad cache
|
||||||
|
|
|
||||||
|
|
@ -141,3 +141,7 @@ func (a *AddCommand) FilesUsedFromContext(config *v1.Config, buildArgs *dockerfi
|
||||||
func (a *AddCommand) MetadataOnly() bool {
|
func (a *AddCommand) MetadataOnly() bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *AddCommand) RequiresUnpackedFS() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ limitations under the License.
|
||||||
package commands
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/GoogleContainerTools/kaniko/pkg/constants"
|
||||||
"github.com/GoogleContainerTools/kaniko/pkg/dockerfile"
|
"github.com/GoogleContainerTools/kaniko/pkg/dockerfile"
|
||||||
v1 "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/instructions"
|
||||||
|
|
@ -24,6 +25,12 @@ import (
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var RootDir string
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
RootDir = constants.RootDir
|
||||||
|
}
|
||||||
|
|
||||||
type CurrentCacheKey func() (string, error)
|
type CurrentCacheKey func() (string, error)
|
||||||
|
|
||||||
type DockerCommand interface {
|
type DockerCommand interface {
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,14 @@ func (c *CopyCommand) ExecuteCommand(config *v1.Config, buildArgs *dockerfile.Bu
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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 fi.IsDir() {
|
||||||
if !filepath.IsAbs(dest) {
|
if !filepath.IsAbs(dest) {
|
||||||
// we need to add '/' to the end to indicate the destination is a directory
|
// we need to add '/' to the end to indicate the destination is a directory
|
||||||
|
|
@ -163,7 +171,7 @@ type CachingCopyCommand struct {
|
||||||
func (cr *CachingCopyCommand) ExecuteCommand(config *v1.Config, buildArgs *dockerfile.BuildArgs) error {
|
func (cr *CachingCopyCommand) ExecuteCommand(config *v1.Config, buildArgs *dockerfile.BuildArgs) error {
|
||||||
logrus.Infof("Found cached layer, extracting to filesystem")
|
logrus.Infof("Found cached layer, extracting to filesystem")
|
||||||
var err error
|
var err error
|
||||||
cr.extractedFiles, err = util.GetFSFromImage(constants.RootDir, cr.img)
|
cr.extractedFiles, err = util.GetFSFromImage(RootDir, cr.img)
|
||||||
logrus.Infof("extractedFiles: %s", cr.extractedFiles)
|
logrus.Infof("extractedFiles: %s", cr.extractedFiles)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "extracting fs from image")
|
return errors.Wrap(err, "extracting fs from image")
|
||||||
|
|
@ -178,3 +186,21 @@ func (cr *CachingCopyCommand) FilesToSnapshot() []string {
|
||||||
func (cr *CachingCopyCommand) String() string {
|
func (cr *CachingCopyCommand) String() string {
|
||||||
return cr.cmd.String()
|
return cr.cmd.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ limitations under the License.
|
||||||
package commands
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
|
|
@ -165,3 +166,52 @@ func copySetUpBuildArgs() *dockerfile.BuildArgs {
|
||||||
buildArgs.AddArg("buildArg2", &d)
|
buildArgs.AddArg("buildArg2", &d)
|
||||||
return buildArgs
|
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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ import (
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/otiai10/copy"
|
otiai10Cpy "github.com/otiai10/copy"
|
||||||
|
|
||||||
"github.com/google/go-containerregistry/pkg/v1/partial"
|
"github.com/google/go-containerregistry/pkg/v1/partial"
|
||||||
|
|
||||||
|
|
@ -52,21 +52,32 @@ import (
|
||||||
// This is the size of an empty tar in Go
|
// This is the size of an empty tar in Go
|
||||||
const emptyTarSize = 1024
|
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
|
// stageBuilder contains all fields necessary to build one stage of a Dockerfile
|
||||||
type stageBuilder struct {
|
type stageBuilder struct {
|
||||||
stage config.KanikoStage
|
stage config.KanikoStage
|
||||||
image v1.Image
|
image v1.Image
|
||||||
cf *v1.ConfigFile
|
cf *v1.ConfigFile
|
||||||
snapshotter *snapshot.Snapshotter
|
snapshotter snapShotter
|
||||||
baseImageDigest string
|
layerCache cache.LayerCache
|
||||||
opts *config.KanikoOptions
|
pushCache cachePusher
|
||||||
cmds []commands.DockerCommand
|
baseImageDigest string
|
||||||
args *dockerfile.BuildArgs
|
finalCacheKey string
|
||||||
crossStageDeps map[int][]string
|
opts *config.KanikoOptions
|
||||||
|
cmds []commands.DockerCommand
|
||||||
|
args *dockerfile.BuildArgs
|
||||||
|
crossStageDeps map[int][]string
|
||||||
|
digestToCacheKeyMap map[string]string
|
||||||
}
|
}
|
||||||
|
|
||||||
// newStageBuilder returns a new type stageBuilder which contains all the information required to build the stage
|
// 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) (*stageBuilder, error) {
|
||||||
sourceImage, err := util.RetrieveSourceImage(stage, opts)
|
sourceImage, err := util.RetrieveSourceImage(stage, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
@ -93,13 +104,18 @@ func newStageBuilder(opts *config.KanikoOptions, stage config.KanikoStage, cross
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
s := &stageBuilder{
|
s := &stageBuilder{
|
||||||
stage: stage,
|
stage: stage,
|
||||||
image: sourceImage,
|
image: sourceImage,
|
||||||
cf: imageConfig,
|
cf: imageConfig,
|
||||||
snapshotter: snapshotter,
|
snapshotter: snapshotter,
|
||||||
baseImageDigest: digest.String(),
|
baseImageDigest: digest.String(),
|
||||||
opts: opts,
|
opts: opts,
|
||||||
crossStageDeps: crossStageDeps,
|
crossStageDeps: crossStageDeps,
|
||||||
|
digestToCacheKeyMap: dcm,
|
||||||
|
layerCache: &cache.RegistryCache{
|
||||||
|
Opts: opts,
|
||||||
|
},
|
||||||
|
pushCache: pushLayerToCache,
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, cmd := range s.stage.Commands {
|
for _, cmd := range s.stage.Commands {
|
||||||
|
|
@ -135,10 +151,7 @@ func (s *stageBuilder) optimize(compositeKey CompositeCache, cfg v1.Config) erro
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
layerCache := &cache.RegistryCache{
|
stopCache := false
|
||||||
Opts: s.opts,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Possibly replace commands with their cached implementations.
|
// Possibly replace commands with their cached implementations.
|
||||||
// We walk through all the commands, running any commands that only operate on metadata.
|
// 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
|
// We throw the metadata away after, but we need it to properly track command dependencies
|
||||||
|
|
@ -151,25 +164,27 @@ func (s *stageBuilder) optimize(compositeKey CompositeCache, cfg v1.Config) erro
|
||||||
// If the command uses files from the context, add them.
|
// If the command uses files from the context, add them.
|
||||||
files, err := command.FilesUsedFromContext(&cfg, s.args)
|
files, err := command.FilesUsedFromContext(&cfg, s.args)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return errors.Wrap(err, "failed to get files used from context")
|
||||||
}
|
}
|
||||||
for _, f := range files {
|
for _, f := range files {
|
||||||
if err := compositeKey.AddPath(f); err != nil {
|
if err := compositeKey.AddPath(f); err != nil {
|
||||||
return err
|
return errors.Wrap(err, "failed to add path to composite key")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ck, err := compositeKey.Hash()
|
ck, err := compositeKey.Hash()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return errors.Wrap(err, "failed to hash composite key")
|
||||||
}
|
}
|
||||||
if command.ShouldCacheOutput() {
|
s.finalCacheKey = ck
|
||||||
img, err := layerCache.RetrieveLayer(ck)
|
if command.ShouldCacheOutput() && !stopCache {
|
||||||
|
img, err := s.layerCache.RetrieveLayer(ck)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Debugf("Failed to retrieve layer: %s", err)
|
logrus.Debugf("Failed to retrieve layer: %s", err)
|
||||||
logrus.Infof("No cached layer found for cmd %s", command.String())
|
logrus.Infof("No cached layer found for cmd %s", command.String())
|
||||||
logrus.Debugf("Key missing was: %s", compositeKey.Key())
|
logrus.Debugf("Key missing was: %s", compositeKey.Key())
|
||||||
break
|
stopCache = true
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if cacheCmd := command.CacheCommand(img); cacheCmd != nil {
|
if cacheCmd := command.CacheCommand(img); cacheCmd != nil {
|
||||||
|
|
@ -190,12 +205,17 @@ func (s *stageBuilder) optimize(compositeKey CompositeCache, cfg v1.Config) erro
|
||||||
|
|
||||||
func (s *stageBuilder) build() error {
|
func (s *stageBuilder) build() error {
|
||||||
// Set the initial cache key to be the base image digest, the build args and the SrcContext.
|
// 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.digestToCacheKeyMap[s.baseImageDigest]; ok {
|
||||||
|
compositeKey = NewCompositeCache(cacheKey)
|
||||||
|
} else {
|
||||||
|
compositeKey = NewCompositeCache(s.baseImageDigest)
|
||||||
|
}
|
||||||
compositeKey.AddKey(s.opts.BuildArgs...)
|
compositeKey.AddKey(s.opts.BuildArgs...)
|
||||||
|
|
||||||
// Apply optimizations to the instructions.
|
// Apply optimizations to the instructions.
|
||||||
if err := s.optimize(*compositeKey, s.cf.Config); err != nil {
|
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.
|
// Unpack file system to root if we need to.
|
||||||
|
|
@ -214,14 +234,14 @@ func (s *stageBuilder) build() error {
|
||||||
if shouldUnpack {
|
if shouldUnpack {
|
||||||
t := timing.Start("FS Unpacking")
|
t := timing.Start("FS Unpacking")
|
||||||
if _, err := util.GetFSFromImage(constants.RootDir, s.image); err != nil {
|
if _, err := util.GetFSFromImage(constants.RootDir, s.image); err != nil {
|
||||||
return err
|
return errors.Wrap(err, "failed to get filesystem from image")
|
||||||
}
|
}
|
||||||
timing.DefaultRun.Stop(t)
|
timing.DefaultRun.Stop(t)
|
||||||
} else {
|
} else {
|
||||||
logrus.Info("Skipping unpacking as no commands require it.")
|
logrus.Info("Skipping unpacking as no commands require it.")
|
||||||
}
|
}
|
||||||
if err := util.DetectFilesystemWhitelist(constants.WhitelistPath); err != nil {
|
if err := util.DetectFilesystemWhitelist(constants.WhitelistPath); err != nil {
|
||||||
return err
|
return errors.Wrap(err, "failed to check filesystem whitelist")
|
||||||
}
|
}
|
||||||
// Take initial snapshot
|
// Take initial snapshot
|
||||||
t := timing.Start("Initial FS snapshot")
|
t := timing.Start("Initial FS snapshot")
|
||||||
|
|
@ -242,17 +262,17 @@ func (s *stageBuilder) build() error {
|
||||||
// If the command uses files from the context, add them.
|
// If the command uses files from the context, add them.
|
||||||
files, err := command.FilesUsedFromContext(&s.cf.Config, s.args)
|
files, err := command.FilesUsedFromContext(&s.cf.Config, s.args)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return errors.Wrap(err, "failed to get files used from context")
|
||||||
}
|
}
|
||||||
for _, f := range files {
|
for _, f := range files {
|
||||||
if err := compositeKey.AddPath(f); err != nil {
|
if err := compositeKey.AddPath(f); err != nil {
|
||||||
return err
|
return errors.Wrap(err, fmt.Sprintf("failed to add path to composite key %v", f))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
logrus.Info(command.String())
|
logrus.Info(command.String())
|
||||||
|
|
||||||
if err := command.ExecuteCommand(&s.cf.Config, s.args); err != nil {
|
if err := command.ExecuteCommand(&s.cf.Config, s.args); err != nil {
|
||||||
return err
|
return errors.Wrap(err, "failed to execute command")
|
||||||
}
|
}
|
||||||
files = command.FilesToSnapshot()
|
files = command.FilesToSnapshot()
|
||||||
timing.DefaultRun.Stop(t)
|
timing.DefaultRun.Stop(t)
|
||||||
|
|
@ -263,21 +283,21 @@ func (s *stageBuilder) build() error {
|
||||||
|
|
||||||
tarPath, err := s.takeSnapshot(files)
|
tarPath, err := s.takeSnapshot(files)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return errors.Wrap(err, "failed to take snapshot")
|
||||||
}
|
}
|
||||||
|
|
||||||
ck, err := compositeKey.Hash()
|
ck, err := compositeKey.Hash()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return errors.Wrap(err, "failed to hash composite key")
|
||||||
}
|
}
|
||||||
// Push layer to cache (in parallel) now along with new config file
|
// Push layer to cache (in parallel) now along with new config file
|
||||||
if s.opts.Cache && command.ShouldCacheOutput() {
|
if s.opts.Cache && command.ShouldCacheOutput() {
|
||||||
cacheGroup.Go(func() error {
|
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 {
|
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 {
|
if err := cacheGroup.Wait(); err != nil {
|
||||||
|
|
@ -333,7 +353,7 @@ func (s *stageBuilder) saveSnapshotToImage(createdBy string, tarPath string) err
|
||||||
}
|
}
|
||||||
fi, err := os.Stat(tarPath)
|
fi, err := os.Stat(tarPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return errors.Wrap(err, "tar file path does not exist")
|
||||||
}
|
}
|
||||||
if fi.Size() <= emptyTarSize {
|
if fi.Size() <= emptyTarSize {
|
||||||
logrus.Info("No files were changed, appending empty layer to config. No layer added to image.")
|
logrus.Info("No files were changed, appending empty layer to config. No layer added to image.")
|
||||||
|
|
@ -422,6 +442,7 @@ func CalculateDependencies(opts *config.KanikoOptions) (map[int][]string, error)
|
||||||
// DoBuild executes building the Dockerfile
|
// DoBuild executes building the Dockerfile
|
||||||
func DoBuild(opts *config.KanikoOptions) (v1.Image, error) {
|
func DoBuild(opts *config.KanikoOptions) (v1.Image, error) {
|
||||||
t := timing.Start("Total Build Time")
|
t := timing.Start("Total Build Time")
|
||||||
|
digestToCacheKeyMap := make(map[string]string)
|
||||||
// Parse dockerfile and unpack base image to root
|
// Parse dockerfile and unpack base image to root
|
||||||
stages, err := dockerfile.Stages(opts)
|
stages, err := dockerfile.Stages(opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -442,7 +463,7 @@ func DoBuild(opts *config.KanikoOptions) (v1.Image, error) {
|
||||||
logrus.Infof("Built cross stage deps: %v", crossStageDependencies)
|
logrus.Infof("Built cross stage deps: %v", crossStageDependencies)
|
||||||
|
|
||||||
for index, stage := range stages {
|
for index, stage := range stages {
|
||||||
sb, err := newStageBuilder(opts, stage, crossStageDependencies)
|
sb, err := newStageBuilder(opts, stage, crossStageDependencies, digestToCacheKeyMap)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -454,6 +475,11 @@ func DoBuild(opts *config.KanikoOptions) (v1.Image, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
d, err := sourceImage.Digest()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
digestToCacheKeyMap[d.String()] = sb.finalCacheKey
|
||||||
if stage.Final {
|
if stage.Final {
|
||||||
sourceImage, err = mutate.CreatedAt(sourceImage, v1.Time{Time: time.Now()})
|
sourceImage, err = mutate.CreatedAt(sourceImage, v1.Time{Time: time.Now()})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -489,7 +515,7 @@ func DoBuild(opts *config.KanikoOptions) (v1.Image, error) {
|
||||||
}
|
}
|
||||||
for _, p := range filesToSave {
|
for _, p := range filesToSave {
|
||||||
logrus.Infof("Saving file %s for later use.", p)
|
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
|
// Delete the filesystem
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,9 @@ limitations under the License.
|
||||||
package executor
|
package executor
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"archive/tar"
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
@ -24,6 +27,7 @@ import (
|
||||||
"sort"
|
"sort"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/GoogleContainerTools/kaniko/pkg/commands"
|
||||||
"github.com/GoogleContainerTools/kaniko/pkg/config"
|
"github.com/GoogleContainerTools/kaniko/pkg/config"
|
||||||
"github.com/GoogleContainerTools/kaniko/pkg/dockerfile"
|
"github.com/GoogleContainerTools/kaniko/pkg/dockerfile"
|
||||||
"github.com/GoogleContainerTools/kaniko/testutil"
|
"github.com/GoogleContainerTools/kaniko/testutil"
|
||||||
|
|
@ -32,6 +36,7 @@ import (
|
||||||
"github.com/google/go-containerregistry/pkg/v1/empty"
|
"github.com/google/go-containerregistry/pkg/v1/empty"
|
||||||
"github.com/google/go-containerregistry/pkg/v1/mutate"
|
"github.com/google/go-containerregistry/pkg/v1/mutate"
|
||||||
"github.com/moby/buildkit/frontend/dockerfile/instructions"
|
"github.com/moby/buildkit/frontend/dockerfile/instructions"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Test_reviewConfig(t *testing.T) {
|
func Test_reviewConfig(t *testing.T) {
|
||||||
|
|
@ -462,3 +467,455 @@ func TestInitializeConfig(t *testing.T) {
|
||||||
testutil.CheckDeepEqual(t, tt.expected, actual.Config)
|
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)
|
||||||
|
logrus.SetLevel(logrus.DebugLevel)
|
||||||
|
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)
|
||||||
|
logrus.SetLevel(logrus.DebugLevel)
|
||||||
|
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("COPY %s foo.txt", filename))
|
||||||
|
ch.AddPath(filePath)
|
||||||
|
logrus.SetLevel(logrus.DebugLevel)
|
||||||
|
logrus.Infof("test composite key %v", ch)
|
||||||
|
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)
|
||||||
|
logrus.Infof("test composite key %v", ch)
|
||||||
|
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)
|
||||||
|
logrus.Infof("test composite key %v", ch)
|
||||||
|
hash3, err := ch.Hash()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("couldn't create hash %v", err)
|
||||||
|
}
|
||||||
|
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 different 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},
|
||||||
|
// Due to CachingCopyCommand and CopyCommand returning different values the write cache key for the second copy command will never match the read cache key
|
||||||
|
// hash3 is the cachekey used to write to the cache for layer 2
|
||||||
|
pushedCacheKeys: []string{hash3},
|
||||||
|
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]
|
||||||
|
})
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -176,7 +176,7 @@ func (s *Snapshotter) scanFullFilesystem() ([]string, []string, error) {
|
||||||
// Only add changed files.
|
// Only add changed files.
|
||||||
fileChanged, err := s.l.CheckFileChange(path)
|
fileChanged, err := s.l.CheckFileChange(path)
|
||||||
if err != nil {
|
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 {
|
if fileChanged {
|
||||||
logrus.Debugf("Adding %s to layer, because it was changed.", path)
|
logrus.Debugf("Adding %s to layer, because it was changed.", path)
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ limitations under the License.
|
||||||
package util
|
package util
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
|
|
@ -25,7 +26,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/GoogleContainerTools/kaniko/pkg/constants"
|
"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/instructions"
|
||||||
"github.com/moby/buildkit/frontend/dockerfile/parser"
|
"github.com/moby/buildkit/frontend/dockerfile/parser"
|
||||||
"github.com/moby/buildkit/frontend/dockerfile/shell"
|
"github.com/moby/buildkit/frontend/dockerfile/shell"
|
||||||
|
|
@ -77,13 +78,16 @@ func ResolveEnvAndWildcards(sd instructions.SourcesAndDest, buildcontext string,
|
||||||
// First, resolve any environment replacement
|
// First, resolve any environment replacement
|
||||||
resolvedEnvs, err := ResolveEnvironmentReplacementList(sd, envs, true)
|
resolvedEnvs, err := ResolveEnvironmentReplacementList(sd, envs, true)
|
||||||
if err != nil {
|
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]
|
dest := resolvedEnvs[len(resolvedEnvs)-1]
|
||||||
// Resolve wildcards and get a list of resolved sources
|
// Resolve wildcards and get a list of resolved sources
|
||||||
srcs, err := ResolveSources(resolvedEnvs[0:len(resolvedEnvs)-1], buildcontext)
|
srcs, err := ResolveSources(resolvedEnvs[0:len(resolvedEnvs)-1], buildcontext)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, "", err
|
return nil, "", errors.Wrap(err, "failed to resolve sources")
|
||||||
}
|
}
|
||||||
err = IsSrcsValid(sd, srcs, buildcontext)
|
err = IsSrcsValid(sd, srcs, buildcontext)
|
||||||
return srcs, dest, err
|
return srcs, dest, err
|
||||||
|
|
@ -219,9 +223,10 @@ func IsSrcsValid(srcsAndDest instructions.SourcesAndDest, resolvedSources []stri
|
||||||
if IsSrcRemoteFileURL(resolvedSources[0]) {
|
if IsSrcRemoteFileURL(resolvedSources[0]) {
|
||||||
return nil
|
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 {
|
if err != nil {
|
||||||
return err
|
return errors.Wrap(err, fmt.Sprintf("failed to get fileinfo for %v", path))
|
||||||
}
|
}
|
||||||
if fi.IsDir() {
|
if fi.IsDir() {
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -237,7 +242,7 @@ func IsSrcsValid(srcsAndDest instructions.SourcesAndDest, resolvedSources []stri
|
||||||
src = filepath.Clean(src)
|
src = filepath.Clean(src)
|
||||||
files, err := RelativeFiles(src, root)
|
files, err := RelativeFiles(src, root)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return errors.Wrap(err, "failed to get relative files")
|
||||||
}
|
}
|
||||||
for _, file := range files {
|
for _, file := range files {
|
||||||
if excludeFile(file, root) {
|
if excludeFile(file, root) {
|
||||||
|
|
|
||||||
|
|
@ -424,11 +424,17 @@ func FilepathExists(path string) bool {
|
||||||
func CreateFile(path string, reader io.Reader, perm os.FileMode, uid uint32, gid uint32) error {
|
func CreateFile(path string, reader io.Reader, perm os.FileMode, uid uint32, gid uint32) error {
|
||||||
// Create directory path if it doesn't exist
|
// Create directory path if it doesn't exist
|
||||||
baseDir := filepath.Dir(path)
|
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)
|
logrus.Tracef("baseDir %s for file %s does not exist. Creating.", baseDir, path)
|
||||||
if err := os.MkdirAll(baseDir, 0755); err != nil {
|
if err := os.MkdirAll(baseDir, 0755); err != nil {
|
||||||
return err
|
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)
|
dest, err := os.Create(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -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 so, look in the local cache before trying the remote registry
|
||||||
if opts.CacheDir != "" {
|
if opts.CacheDir != "" {
|
||||||
cachedImage, err := cachedImage(opts, currentBaseName)
|
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
|
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
|
// Otherwise, initialize image as usual
|
||||||
return RetrieveRemoteImage(currentBaseName, opts)
|
return RetrieveRemoteImage(currentBaseName, opts)
|
||||||
}
|
}
|
||||||
|
|
@ -93,13 +91,23 @@ func tarballImage(index int) (v1.Image, error) {
|
||||||
return tarball.ImageFromPath(tarPath, nil)
|
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) {
|
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)
|
ref, err := name.ParseReference(image, name.WeakValidation)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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()
|
registryName := ref.Context().RegistryStr()
|
||||||
if opts.InsecurePull || opts.InsecureRegistries.Contains(registryName) {
|
if opts.InsecurePull || opts.InsecureRegistries.Contains(registryName) {
|
||||||
newReg, err := name.NewRegistry(registryName, name.WeakValidation, name.Insecure)
|
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,
|
InsecureSkipVerify: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return []remote.Option{remote.WithTransport(tr), remote.WithAuthFromKeychain(creds.GetKeychain())}, nil
|
||||||
return remote.Image(ref, remote.WithTransport(tr), remote.WithAuthFromKeychain(creds.GetKeychain()))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func cachedImage(opts *config.KanikoOptions, image string) (v1.Image, error) {
|
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 {
|
if d, ok := ref.(name.Digest); ok {
|
||||||
cacheKey = d.DigestStr()
|
cacheKey = d.DigestStr()
|
||||||
} else {
|
} else {
|
||||||
img, err := remoteImage(image, opts)
|
image, err := remoteImage(image, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
d, err := img.Digest()
|
d, err := image.Digest()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
cacheKey = d.String()
|
cacheKey = d.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
return cache.LocalSource(&opts.CacheOptions, cacheKey)
|
return cache.LocalSource(&opts.CacheOptions, cacheKey)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue