mirror of
https://github.com/GoogleContainerTools/kaniko
synced 2026-10-09 06:55:48 +02:00
Merge branch 'master' of https://github.com/GoogleContainerTools/kaniko into log
This commit is contained in:
+191
@@ -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
@@ -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).
|
||||
|
||||
@@ -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
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Vendored
+3
-2
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user