mirror of
https://github.com/GoogleContainerTools/kaniko
synced 2026-09-30 20:24:24 +02:00
* Correctly handle platforms that include CPU variants Prior to this change, Kaniko would only select the platform-specific image from a multi-platform image using the OS and architecture to select the image. This leads to problems when there are two platforms that are indistinguishable using only this information (e.g., linux/arm/v5 and linux/arm/v7). This change more explicitly selects the right image from a multi-platform image, taking CPU variant into account (v5 vs v7), using containerd's CPU variant detection logic. This also moves platform defaulting up as early as possible in execution as it can go, so that malformed platform values are detected as soon as possible. * set platform in unit test
1529 lines
38 KiB
Go
1529 lines
38 KiB
Go
/*
|
|
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 (
|
|
"archive/tar"
|
|
"bytes"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"sort"
|
|
"strconv"
|
|
"testing"
|
|
|
|
"github.com/GoogleContainerTools/kaniko/pkg/commands"
|
|
"github.com/GoogleContainerTools/kaniko/pkg/config"
|
|
"github.com/GoogleContainerTools/kaniko/pkg/dockerfile"
|
|
"github.com/GoogleContainerTools/kaniko/pkg/util"
|
|
"github.com/GoogleContainerTools/kaniko/testutil"
|
|
"github.com/containerd/containerd/platforms"
|
|
"github.com/google/go-cmp/cmp"
|
|
v1 "github.com/google/go-containerregistry/pkg/v1"
|
|
"github.com/google/go-containerregistry/pkg/v1/empty"
|
|
"github.com/google/go-containerregistry/pkg/v1/mutate"
|
|
"github.com/google/go-containerregistry/pkg/v1/partial"
|
|
"github.com/moby/buildkit/frontend/dockerfile/instructions"
|
|
)
|
|
|
|
func Test_reviewConfig(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
dockerfile string
|
|
originalCmd []string
|
|
originalEntrypoint []string
|
|
expectedCmd []string
|
|
}{
|
|
{
|
|
name: "entrypoint and cmd declared",
|
|
dockerfile: `
|
|
FROM scratch
|
|
CMD ["mycmd"]
|
|
ENTRYPOINT ["myentrypoint"]`,
|
|
originalEntrypoint: []string{"myentrypoint"},
|
|
originalCmd: []string{"mycmd"},
|
|
expectedCmd: []string{"mycmd"},
|
|
},
|
|
{
|
|
name: "only entrypoint declared",
|
|
dockerfile: `
|
|
FROM scratch
|
|
ENTRYPOINT ["myentrypoint"]`,
|
|
originalEntrypoint: []string{"myentrypoint"},
|
|
originalCmd: []string{"mycmd"},
|
|
expectedCmd: nil,
|
|
},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
config := &v1.Config{
|
|
Cmd: test.originalCmd,
|
|
Entrypoint: test.originalEntrypoint,
|
|
}
|
|
reviewConfig(stage(t, test.dockerfile), config)
|
|
testutil.CheckErrorAndDeepEqual(t, false, nil, test.expectedCmd, config.Cmd)
|
|
})
|
|
}
|
|
}
|
|
|
|
func stage(t *testing.T, d string) config.KanikoStage {
|
|
stages, _, err := dockerfile.Parse([]byte(d))
|
|
if err != nil {
|
|
t.Fatalf("error parsing dockerfile: %v", err)
|
|
}
|
|
return config.KanikoStage{
|
|
Stage: stages[0],
|
|
}
|
|
}
|
|
|
|
func Test_stageBuilder_shouldTakeSnapshot(t *testing.T) {
|
|
cmds := []commands.DockerCommand{
|
|
&MockDockerCommand{command: "command1"},
|
|
&MockDockerCommand{command: "command2"},
|
|
&MockDockerCommand{command: "command3"},
|
|
}
|
|
|
|
type fields struct {
|
|
stage config.KanikoStage
|
|
opts *config.KanikoOptions
|
|
cmds []commands.DockerCommand
|
|
}
|
|
type args struct {
|
|
index int
|
|
metadataOnly bool
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
fields fields
|
|
args args
|
|
want bool
|
|
}{
|
|
{
|
|
name: "final stage not last command",
|
|
fields: fields{
|
|
stage: config.KanikoStage{
|
|
Final: true,
|
|
},
|
|
cmds: cmds,
|
|
},
|
|
args: args{
|
|
index: 1,
|
|
},
|
|
want: true,
|
|
},
|
|
{
|
|
name: "not final stage last command",
|
|
fields: fields{
|
|
stage: config.KanikoStage{
|
|
Final: false,
|
|
},
|
|
cmds: cmds,
|
|
},
|
|
args: args{
|
|
index: len(cmds) - 1,
|
|
},
|
|
want: true,
|
|
},
|
|
{
|
|
name: "not final stage not last command",
|
|
fields: fields{
|
|
stage: config.KanikoStage{
|
|
Final: false,
|
|
},
|
|
cmds: cmds,
|
|
},
|
|
args: args{
|
|
index: 0,
|
|
},
|
|
want: true,
|
|
},
|
|
{
|
|
name: "not final stage not last command but empty list of files",
|
|
fields: fields{
|
|
stage: config.KanikoStage{},
|
|
},
|
|
args: args{
|
|
index: 0,
|
|
metadataOnly: true,
|
|
},
|
|
want: false,
|
|
},
|
|
{
|
|
name: "not final stage not last command no files provided",
|
|
fields: fields{
|
|
stage: config.KanikoStage{
|
|
Final: false,
|
|
},
|
|
},
|
|
args: args{
|
|
index: 0,
|
|
metadataOnly: false,
|
|
},
|
|
want: true,
|
|
},
|
|
{
|
|
name: "caching enabled intermediate container",
|
|
fields: fields{
|
|
stage: config.KanikoStage{
|
|
Final: false,
|
|
},
|
|
opts: &config.KanikoOptions{Cache: true},
|
|
cmds: cmds,
|
|
},
|
|
args: args{
|
|
index: 0,
|
|
},
|
|
want: true,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
|
|
if tt.fields.opts == nil {
|
|
tt.fields.opts = &config.KanikoOptions{}
|
|
}
|
|
s := &stageBuilder{
|
|
stage: tt.fields.stage,
|
|
opts: tt.fields.opts,
|
|
cmds: tt.fields.cmds,
|
|
}
|
|
if got := s.shouldTakeSnapshot(tt.args.index, tt.args.metadataOnly); got != tt.want {
|
|
t.Errorf("stageBuilder.shouldTakeSnapshot() = %v, want %v", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCalculateDependencies(t *testing.T) {
|
|
type args struct {
|
|
dockerfile string
|
|
mockInitConfig func(partial.WithConfigFile, *config.KanikoOptions) (*v1.ConfigFile, error)
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
args args
|
|
want map[int][]string
|
|
}{
|
|
{
|
|
name: "no deps",
|
|
args: args{
|
|
dockerfile: `
|
|
FROM debian as stage1
|
|
RUN foo
|
|
FROM stage1
|
|
RUN bar
|
|
`,
|
|
},
|
|
want: map[int][]string{},
|
|
},
|
|
{
|
|
name: "args",
|
|
args: args{
|
|
dockerfile: `
|
|
ARG myFile=foo
|
|
FROM debian as stage1
|
|
RUN foo
|
|
FROM stage1
|
|
ARG myFile
|
|
COPY --from=stage1 /tmp/$myFile.txt .
|
|
RUN bar
|
|
`,
|
|
},
|
|
want: map[int][]string{
|
|
0: {"/tmp/foo.txt"},
|
|
},
|
|
},
|
|
{
|
|
name: "simple deps",
|
|
args: args{
|
|
dockerfile: `
|
|
FROM debian as stage1
|
|
FROM alpine
|
|
COPY --from=stage1 /foo /bar
|
|
`,
|
|
},
|
|
want: map[int][]string{
|
|
0: {"/foo"},
|
|
},
|
|
},
|
|
{
|
|
name: "two sets deps",
|
|
args: args{
|
|
dockerfile: `
|
|
FROM debian as stage1
|
|
FROM ubuntu as stage2
|
|
RUN foo
|
|
COPY --from=stage1 /foo /bar
|
|
FROM alpine
|
|
COPY --from=stage2 /bar /bat
|
|
`,
|
|
},
|
|
want: map[int][]string{
|
|
0: {"/foo"},
|
|
1: {"/bar"},
|
|
},
|
|
},
|
|
{
|
|
name: "double deps",
|
|
args: args{
|
|
dockerfile: `
|
|
FROM debian as stage1
|
|
FROM ubuntu as stage2
|
|
RUN foo
|
|
COPY --from=stage1 /foo /bar
|
|
FROM alpine
|
|
COPY --from=stage1 /baz /bat
|
|
`,
|
|
},
|
|
want: map[int][]string{
|
|
0: {"/foo", "/baz"},
|
|
},
|
|
},
|
|
{
|
|
name: "envs in deps",
|
|
args: args{
|
|
dockerfile: `
|
|
FROM debian as stage1
|
|
FROM ubuntu as stage2
|
|
RUN foo
|
|
ENV key1 val1
|
|
ENV key2 val2
|
|
COPY --from=stage1 /foo/$key1 /foo/$key2 /bar
|
|
FROM alpine
|
|
COPY --from=stage2 /bar /bat
|
|
`,
|
|
},
|
|
want: map[int][]string{
|
|
0: {"/foo/val1", "/foo/val2"},
|
|
1: {"/bar"},
|
|
},
|
|
},
|
|
{
|
|
name: "envs from base image in deps",
|
|
args: args{
|
|
dockerfile: `
|
|
FROM debian as stage1
|
|
ENV key1 baseval1
|
|
FROM stage1 as stage2
|
|
RUN foo
|
|
ENV key2 val2
|
|
COPY --from=stage1 /foo/$key1 /foo/$key2 /bar
|
|
FROM alpine
|
|
COPY --from=stage2 /bar /bat
|
|
`,
|
|
},
|
|
want: map[int][]string{
|
|
0: {"/foo/baseval1", "/foo/val2"},
|
|
1: {"/bar"},
|
|
},
|
|
},
|
|
{
|
|
name: "one image has onbuild config",
|
|
args: args{
|
|
mockInitConfig: func(img partial.WithConfigFile, opts *config.KanikoOptions) (*v1.ConfigFile, error) {
|
|
cfg, err := img.ConfigFile()
|
|
// if image is "alpine" then add ONBUILD to its config
|
|
if cfg != nil && cfg.Architecture != "" {
|
|
cfg.Config.OnBuild = []string{"COPY --from=builder /app /app"}
|
|
}
|
|
return cfg, err
|
|
},
|
|
dockerfile: `
|
|
FROM scratch as builder
|
|
RUN foo
|
|
FROM alpine as second
|
|
# This image has an ONBUILD command so it will be executed
|
|
COPY --from=builder /foo /bar
|
|
FROM scratch as target
|
|
COPY --from=second /bar /bat
|
|
`,
|
|
},
|
|
want: map[int][]string{
|
|
0: {"/app", "/foo"},
|
|
1: {"/bar"},
|
|
},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
if tt.args.mockInitConfig != nil {
|
|
original := initializeConfig
|
|
defer func() { initializeConfig = original }()
|
|
initializeConfig = tt.args.mockInitConfig
|
|
}
|
|
|
|
f, _ := ioutil.TempFile("", "")
|
|
ioutil.WriteFile(f.Name(), []byte(tt.args.dockerfile), 0755)
|
|
opts := &config.KanikoOptions{
|
|
DockerfilePath: f.Name(),
|
|
CustomPlatform: platforms.DefaultString(),
|
|
}
|
|
testStages, metaArgs, err := dockerfile.ParseStages(opts)
|
|
if err != nil {
|
|
t.Errorf("Failed to parse test dockerfile to stages: %s", err)
|
|
}
|
|
|
|
kanikoStages, err := dockerfile.MakeKanikoStages(opts, testStages, metaArgs)
|
|
if err != nil {
|
|
t.Errorf("Failed to parse stages to Kaniko Stages: %s", err)
|
|
}
|
|
stageNameToIdx := ResolveCrossStageInstructions(kanikoStages)
|
|
|
|
got, err := CalculateDependencies(kanikoStages, opts, stageNameToIdx)
|
|
if err != nil {
|
|
t.Errorf("got error: %s,", err)
|
|
}
|
|
|
|
if !reflect.DeepEqual(got, tt.want) {
|
|
diff := cmp.Diff(got, tt.want)
|
|
t.Errorf("CalculateDependencies() = %v, want %v, diff %v", got, tt.want, diff)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_filesToSave(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
args []string
|
|
want []string
|
|
files []string
|
|
}{
|
|
{
|
|
name: "simple",
|
|
args: []string{"foo"},
|
|
files: []string{"foo"},
|
|
want: []string{"foo"},
|
|
},
|
|
{
|
|
name: "glob",
|
|
args: []string{"foo*"},
|
|
files: []string{"foo", "foo2", "fooooo", "bar"},
|
|
want: []string{"foo", "foo2", "fooooo"},
|
|
},
|
|
{
|
|
name: "complex glob",
|
|
args: []string{"foo*", "bar?"},
|
|
files: []string{"foo", "foo2", "fooooo", "bar", "bar1", "bar2", "bar33"},
|
|
want: []string{"foo", "foo2", "fooooo", "bar1", "bar2"},
|
|
},
|
|
{
|
|
name: "dir",
|
|
args: []string{"foo"},
|
|
files: []string{"foo/bar", "foo/baz", "foo/bat/baz"},
|
|
want: []string{"foo"},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
original := config.RootDir
|
|
config.RootDir = tmpDir
|
|
defer func() {
|
|
config.RootDir = original
|
|
}()
|
|
|
|
for _, f := range tt.files {
|
|
p := filepath.Join(tmpDir, f)
|
|
dir := filepath.Dir(p)
|
|
if dir != "." {
|
|
if err := os.MkdirAll(dir, 0755); err != nil {
|
|
t.Errorf("error making dir: %s", err)
|
|
}
|
|
}
|
|
fp, err := os.Create(p)
|
|
if err != nil {
|
|
t.Errorf("error making file: %s", err)
|
|
}
|
|
fp.Close()
|
|
}
|
|
|
|
got, err := filesToSave(tt.args)
|
|
if err != nil {
|
|
t.Errorf("got err: %s", err)
|
|
}
|
|
sort.Strings(tt.want)
|
|
sort.Strings(got)
|
|
if !reflect.DeepEqual(got, tt.want) {
|
|
t.Errorf("filesToSave() = %v, want %v", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestInitializeConfig(t *testing.T) {
|
|
tests := []struct {
|
|
description string
|
|
cfg v1.ConfigFile
|
|
expected v1.Config
|
|
}{
|
|
{
|
|
description: "env is not set in the image",
|
|
cfg: v1.ConfigFile{
|
|
Config: v1.Config{
|
|
Image: "test",
|
|
},
|
|
},
|
|
expected: v1.Config{
|
|
Image: "test",
|
|
Env: []string{
|
|
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
description: "env is set in the image",
|
|
cfg: v1.ConfigFile{
|
|
Config: v1.Config{
|
|
Env: []string{
|
|
"PATH=/usr/local/something",
|
|
},
|
|
},
|
|
},
|
|
expected: v1.Config{
|
|
Env: []string{
|
|
"PATH=/usr/local/something",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
description: "image is empty",
|
|
expected: v1.Config{
|
|
Env: []string{
|
|
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
img, err := mutate.ConfigFile(empty.Image, &tt.cfg)
|
|
if err != nil {
|
|
t.Errorf("error seen when running test %s", err)
|
|
t.Fail()
|
|
}
|
|
actual, _ := initializeConfig(img, nil)
|
|
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,
|
|
args: dockerfile.NewBuildArgs([]string{})}
|
|
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)
|
|
}
|
|
|
|
})
|
|
}
|
|
}
|
|
|
|
type stageContext struct {
|
|
command fmt.Stringer
|
|
args *dockerfile.BuildArgs
|
|
env []string
|
|
}
|
|
|
|
func newStageContext(command string, args map[string]string, env []string) stageContext {
|
|
dockerArgs := dockerfile.NewBuildArgs([]string{})
|
|
for k, v := range args {
|
|
dockerArgs.AddArg(k, &v)
|
|
}
|
|
return stageContext{MockDockerCommand{command: command}, dockerArgs, env}
|
|
}
|
|
|
|
func Test_stageBuilder_populateCompositeKey(t *testing.T) {
|
|
testCases := []struct {
|
|
description string
|
|
cmd1 stageContext
|
|
cmd2 stageContext
|
|
shdEqual bool
|
|
}{
|
|
{
|
|
description: "cache key for same command with same build args",
|
|
cmd1: newStageContext(
|
|
"RUN echo $ARG > test",
|
|
map[string]string{"ARG": "foo"},
|
|
[]string{},
|
|
),
|
|
cmd2: newStageContext(
|
|
"RUN echo $ARG > test",
|
|
map[string]string{"ARG": "foo"},
|
|
[]string{},
|
|
),
|
|
shdEqual: true,
|
|
},
|
|
{
|
|
description: "cache key for same command with same env and args",
|
|
cmd1: newStageContext(
|
|
"RUN echo $ENV > test",
|
|
map[string]string{"ARG": "foo"},
|
|
[]string{"ENV=same"},
|
|
),
|
|
cmd2: newStageContext(
|
|
"RUN echo $ENV > test",
|
|
map[string]string{"ARG": "foo"},
|
|
[]string{"ENV=same"},
|
|
),
|
|
shdEqual: true,
|
|
},
|
|
{
|
|
description: "cache key for same command with same env but different args",
|
|
cmd1: newStageContext(
|
|
"RUN echo $ENV > test",
|
|
map[string]string{"ARG": "foo"},
|
|
[]string{"ENV=same"},
|
|
),
|
|
cmd2: newStageContext(
|
|
"RUN echo $ENV > test",
|
|
map[string]string{"ARG": "bar"},
|
|
[]string{"ENV=same"},
|
|
),
|
|
},
|
|
{
|
|
description: "cache key for same command, different buildargs, args not used in command",
|
|
cmd1: newStageContext(
|
|
"RUN echo const > test",
|
|
map[string]string{"ARG": "foo"},
|
|
[]string{"ENV=foo1"},
|
|
),
|
|
cmd2: newStageContext(
|
|
"RUN echo const > test",
|
|
map[string]string{"ARG": "bar"},
|
|
[]string{"ENV=bar1"},
|
|
),
|
|
},
|
|
{
|
|
description: "cache key for same command, different buildargs, args used in script",
|
|
// test.sh
|
|
// #!/bin/sh
|
|
// echo ${ARG}
|
|
cmd1: newStageContext(
|
|
"RUN ./test.sh",
|
|
map[string]string{"ARG": "foo"},
|
|
[]string{"ENV=foo1"},
|
|
),
|
|
cmd2: newStageContext(
|
|
"RUN ./test.sh",
|
|
map[string]string{"ARG": "bar"},
|
|
[]string{"ENV=bar1"},
|
|
),
|
|
},
|
|
{
|
|
description: "cache key for same command with a build arg values",
|
|
cmd1: newStageContext(
|
|
"RUN echo $ARG > test",
|
|
map[string]string{"ARG": "foo"},
|
|
[]string{},
|
|
),
|
|
cmd2: newStageContext(
|
|
"RUN echo $ARG > test",
|
|
map[string]string{"ARG": "bar"},
|
|
[]string{},
|
|
),
|
|
},
|
|
{
|
|
description: "cache key for same command with different env values",
|
|
cmd1: newStageContext(
|
|
"RUN echo $ENV > test",
|
|
map[string]string{"ARG": "foo"},
|
|
[]string{"ENV=1"},
|
|
),
|
|
cmd2: newStageContext(
|
|
"RUN echo $ENV > test",
|
|
map[string]string{"ARG": "foo"},
|
|
[]string{"ENV=2"},
|
|
),
|
|
},
|
|
{
|
|
description: "cache key for different command same context",
|
|
cmd1: newStageContext(
|
|
"RUN echo other > test",
|
|
map[string]string{"ARG": "foo"},
|
|
[]string{"ENV=1"},
|
|
),
|
|
cmd2: newStageContext(
|
|
"RUN echo another > test",
|
|
map[string]string{"ARG": "foo"},
|
|
[]string{"ENV=1"},
|
|
),
|
|
},
|
|
}
|
|
for _, tc := range testCases {
|
|
t.Run(tc.description, func(t *testing.T) {
|
|
sb := &stageBuilder{fileContext: util.FileContext{Root: "workspace"}}
|
|
ck := CompositeCache{}
|
|
|
|
ck1, err := sb.populateCompositeKey(tc.cmd1.command, []string{}, ck, tc.cmd1.args, tc.cmd1.env)
|
|
if err != nil {
|
|
t.Errorf("Expected error to be nil but was %v", err)
|
|
}
|
|
ck2, err := sb.populateCompositeKey(tc.cmd2.command, []string{}, ck, tc.cmd2.args, tc.cmd2.env)
|
|
if err != nil {
|
|
t.Errorf("Expected error to be nil but was %v", err)
|
|
}
|
|
key1, key2 := hashCompositeKeys(t, ck1, ck2)
|
|
if b := key1 == key2; b != tc.shdEqual {
|
|
t.Errorf("expected keys to be equal as %t but found %t", tc.shdEqual, !tc.shdEqual)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_stageBuilder_build(t *testing.T) {
|
|
type testcase struct {
|
|
description string
|
|
opts *config.KanikoOptions
|
|
args map[string]string
|
|
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, util.FileContext{})
|
|
hash, err := ch.Hash()
|
|
if err != nil {
|
|
t.Errorf("couldn't create hash %v", err)
|
|
}
|
|
command := MockDockerCommand{
|
|
command: "meow",
|
|
contextFiles: []string{filePath},
|
|
cacheCommand: MockCachedDockerCommand{
|
|
contextFiles: []string{filePath},
|
|
},
|
|
}
|
|
|
|
destDir := t.TempDir()
|
|
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, util.FileContext{})
|
|
hash, err := ch.Hash()
|
|
if err != nil {
|
|
t.Errorf("couldn't create hash %v", err)
|
|
}
|
|
command := MockDockerCommand{
|
|
command: "meow",
|
|
contextFiles: []string{filePath},
|
|
cacheCommand: MockCachedDockerCommand{
|
|
contextFiles: []string{filePath},
|
|
},
|
|
}
|
|
|
|
destDir := t.TempDir()
|
|
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,
|
|
}
|
|
}(),
|
|
func() testcase {
|
|
dir, files := tempDirAndFile(t)
|
|
file := files[0]
|
|
filePath := filepath.Join(dir, file)
|
|
ch := NewCompositeCache("", "meow")
|
|
|
|
ch.AddPath(filePath, util.FileContext{})
|
|
hash, err := ch.Hash()
|
|
if err != nil {
|
|
t.Errorf("couldn't create hash %v", err)
|
|
}
|
|
command := MockDockerCommand{
|
|
command: "meow",
|
|
contextFiles: []string{filePath},
|
|
cacheCommand: MockCachedDockerCommand{
|
|
contextFiles: []string{filePath},
|
|
},
|
|
}
|
|
|
|
destDir := t.TempDir()
|
|
return testcase{
|
|
description: "fake command cache enabled with tar compression disabled and key in cache",
|
|
opts: &config.KanikoOptions{Cache: true, CompressedCaching: false},
|
|
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("", fmt.Sprintf("COPY %s foo.txt", filename))
|
|
ch.AddPath(filepath, util.FileContext{})
|
|
|
|
hash, err := ch.Hash()
|
|
if err != nil {
|
|
t.Errorf("couldn't create hash %v", err)
|
|
}
|
|
copyCommandCacheKey := hash
|
|
dockerFile := fmt.Sprintf(`
|
|
FROM ubuntu:16.04
|
|
COPY %s foo.txt
|
|
`, filename)
|
|
f, _ := ioutil.TempFile("", "")
|
|
ioutil.WriteFile(f.Name(), []byte(dockerFile), 0755)
|
|
opts := &config.KanikoOptions{
|
|
DockerfilePath: f.Name(),
|
|
Cache: true,
|
|
CacheCopyLayers: true,
|
|
}
|
|
|
|
testStages, metaArgs, err := dockerfile.ParseStages(opts)
|
|
if err != nil {
|
|
t.Errorf("Failed to parse test dockerfile to stages: %s", err)
|
|
}
|
|
|
|
kanikoStages, err := dockerfile.MakeKanikoStages(opts, testStages, metaArgs)
|
|
if err != nil {
|
|
t.Errorf("Failed to parse stages to Kaniko Stages: %s", err)
|
|
}
|
|
_ = ResolveCrossStageInstructions(kanikoStages)
|
|
stage := kanikoStages[0]
|
|
|
|
cmds := stage.Commands
|
|
|
|
return testcase{
|
|
description: "copy command cache enabled and key in cache",
|
|
opts: opts,
|
|
image: fakeImage{
|
|
ImageLayers: []v1.Layer{
|
|
fakeLayer{
|
|
TarContent: tarContent,
|
|
},
|
|
},
|
|
},
|
|
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(util.FileContext{Root: dir}, cmds, true),
|
|
fileName: filename,
|
|
}
|
|
}(),
|
|
func() testcase {
|
|
dir, filenames := tempDirAndFile(t)
|
|
filename := filenames[0]
|
|
tarContent := []byte{}
|
|
destDir := t.TempDir()
|
|
filePath := filepath.Join(dir, filename)
|
|
ch := NewCompositeCache("", fmt.Sprintf("COPY %s foo.txt", filename))
|
|
ch.AddPath(filePath, util.FileContext{})
|
|
|
|
hash, err := ch.Hash()
|
|
if err != nil {
|
|
t.Errorf("couldn't create hash %v", err)
|
|
}
|
|
dockerFile := fmt.Sprintf(`
|
|
FROM ubuntu:16.04
|
|
COPY %s foo.txt
|
|
`, filename)
|
|
f, _ := ioutil.TempFile("", "")
|
|
ioutil.WriteFile(f.Name(), []byte(dockerFile), 0755)
|
|
opts := &config.KanikoOptions{
|
|
DockerfilePath: f.Name(),
|
|
Cache: true,
|
|
CacheCopyLayers: true,
|
|
}
|
|
|
|
testStages, metaArgs, err := dockerfile.ParseStages(opts)
|
|
if err != nil {
|
|
t.Errorf("Failed to parse test dockerfile to stages: %s", err)
|
|
}
|
|
|
|
kanikoStages, err := dockerfile.MakeKanikoStages(opts, testStages, metaArgs)
|
|
if err != nil {
|
|
t.Errorf("Failed to parse stages to Kaniko Stages: %s", err)
|
|
}
|
|
_ = ResolveCrossStageInstructions(kanikoStages)
|
|
stage := kanikoStages[0]
|
|
|
|
cmds := stage.Commands
|
|
return testcase{
|
|
description: "copy command cache enabled and key is not in cache",
|
|
opts: opts,
|
|
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(util.FileContext{Root: dir}, cmds, true),
|
|
fileName: filename,
|
|
}
|
|
}(),
|
|
func() testcase {
|
|
dir, filenames := tempDirAndFile(t)
|
|
filename := filenames[0]
|
|
tarContent := generateTar(t, filename)
|
|
|
|
destDir := t.TempDir()
|
|
filePath := filepath.Join(dir, filename)
|
|
|
|
ch := NewCompositeCache("", "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, util.FileContext{})
|
|
|
|
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, util.FileContext{})
|
|
|
|
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(),
|
|
}
|
|
|
|
testStages, metaArgs, err := dockerfile.ParseStages(opts)
|
|
if err != nil {
|
|
t.Errorf("Failed to parse test dockerfile to stages: %s", err)
|
|
}
|
|
|
|
kanikoStages, err := dockerfile.MakeKanikoStages(opts, testStages, metaArgs)
|
|
if err != nil {
|
|
t.Errorf("Failed to parse stages to Kaniko Stages: %s", err)
|
|
}
|
|
_ = ResolveCrossStageInstructions(kanikoStages)
|
|
stage := kanikoStages[0]
|
|
|
|
cmds := stage.Commands
|
|
return testcase{
|
|
description: "cached run command followed by uncached copy command results in consistent read and write hashes",
|
|
opts: &config.KanikoOptions{Cache: true, CacheCopyLayers: 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
|
|
expectedCacheKeys: []string{hash1, hash2},
|
|
pushedCacheKeys: []string{hash2},
|
|
commands: getCommands(util.FileContext{Root: dir}, cmds, true),
|
|
}
|
|
}(),
|
|
func() testcase {
|
|
dir, filenames := tempDirAndFile(t)
|
|
filename := filenames[0]
|
|
tarContent := generateTar(t, filename)
|
|
|
|
destDir := t.TempDir()
|
|
|
|
filePath := filepath.Join(dir, filename)
|
|
|
|
ch := NewCompositeCache("", fmt.Sprintf("COPY %s bar.txt", filename))
|
|
ch.AddPath(filePath, util.FileContext{})
|
|
|
|
// copy hash
|
|
_, err := ch.Hash()
|
|
if err != nil {
|
|
t.Errorf("couldn't create hash %v", err)
|
|
}
|
|
|
|
ch.AddKey(fmt.Sprintf("RUN foobar"))
|
|
|
|
// run hash
|
|
runHash, err := ch.Hash()
|
|
if err != nil {
|
|
t.Errorf("couldn't create hash %v", err)
|
|
}
|
|
|
|
image := fakeImage{
|
|
ImageLayers: []v1.Layer{
|
|
fakeLayer{
|
|
TarContent: tarContent,
|
|
},
|
|
},
|
|
}
|
|
|
|
dockerFile := fmt.Sprintf(`
|
|
FROM ubuntu:16.04
|
|
COPY %s bar.txt
|
|
RUN foobar
|
|
`, filename)
|
|
f, _ := ioutil.TempFile("", "")
|
|
ioutil.WriteFile(f.Name(), []byte(dockerFile), 0755)
|
|
opts := &config.KanikoOptions{
|
|
DockerfilePath: f.Name(),
|
|
}
|
|
|
|
testStages, metaArgs, err := dockerfile.ParseStages(opts)
|
|
if err != nil {
|
|
t.Errorf("Failed to parse test dockerfile to stages: %s", err)
|
|
}
|
|
|
|
kanikoStages, err := dockerfile.MakeKanikoStages(opts, testStages, metaArgs)
|
|
if err != nil {
|
|
t.Errorf("Failed to parse stages to Kaniko Stages: %s", err)
|
|
}
|
|
_ = ResolveCrossStageInstructions(kanikoStages)
|
|
stage := kanikoStages[0]
|
|
|
|
cmds := stage.Commands
|
|
return testcase{
|
|
description: "copy command followed by cached run command results 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{runHash},
|
|
img: image,
|
|
},
|
|
image: image,
|
|
expectedCacheKeys: []string{runHash},
|
|
pushedCacheKeys: []string{},
|
|
commands: getCommands(util.FileContext{Root: dir}, cmds, false),
|
|
}
|
|
}(),
|
|
func() testcase {
|
|
dir, _ := tempDirAndFile(t)
|
|
ch := NewCompositeCache("")
|
|
ch.AddKey("|1")
|
|
ch.AddKey("test=value")
|
|
ch.AddKey("RUN foobar")
|
|
hash, err := ch.Hash()
|
|
if err != nil {
|
|
t.Errorf("couldn't create hash %v", err)
|
|
}
|
|
|
|
command := MockDockerCommand{
|
|
command: "RUN foobar",
|
|
contextFiles: []string{},
|
|
cacheCommand: MockCachedDockerCommand{
|
|
contextFiles: []string{},
|
|
},
|
|
}
|
|
|
|
return testcase{
|
|
description: "cached run command with no build arg value used uses cached layer and does not push anything",
|
|
config: &v1.ConfigFile{Config: v1.Config{WorkingDir: dir}},
|
|
opts: &config.KanikoOptions{Cache: true},
|
|
args: map[string]string{
|
|
"test": "value",
|
|
},
|
|
expectedCacheKeys: []string{hash},
|
|
commands: []commands.DockerCommand{command},
|
|
// layer key needs to be read.
|
|
layerCache: &fakeLayerCache{
|
|
img: &fakeImage{ImageLayers: []v1.Layer{fakeLayer{}}},
|
|
keySequence: []string{hash},
|
|
},
|
|
rootDir: dir,
|
|
}
|
|
}(),
|
|
func() testcase {
|
|
dir, _ := tempDirAndFile(t)
|
|
|
|
ch := NewCompositeCache("")
|
|
ch.AddKey("|1")
|
|
ch.AddKey("arg=value")
|
|
ch.AddKey("RUN value")
|
|
hash, err := ch.Hash()
|
|
if err != nil {
|
|
t.Errorf("couldn't create hash %v", err)
|
|
}
|
|
|
|
command := MockDockerCommand{
|
|
command: "RUN $arg",
|
|
contextFiles: []string{},
|
|
cacheCommand: MockCachedDockerCommand{
|
|
contextFiles: []string{},
|
|
},
|
|
}
|
|
|
|
return testcase{
|
|
description: "cached run command with same build arg does not push layer",
|
|
config: &v1.ConfigFile{Config: v1.Config{WorkingDir: dir}},
|
|
opts: &config.KanikoOptions{Cache: true},
|
|
args: map[string]string{
|
|
"arg": "value",
|
|
},
|
|
// layer key that exists
|
|
layerCache: &fakeLayerCache{
|
|
img: &fakeImage{ImageLayers: []v1.Layer{fakeLayer{}}},
|
|
keySequence: []string{hash},
|
|
},
|
|
expectedCacheKeys: []string{hash},
|
|
commands: []commands.DockerCommand{command},
|
|
rootDir: dir,
|
|
}
|
|
}(),
|
|
func() testcase {
|
|
dir, _ := tempDirAndFile(t)
|
|
|
|
ch1 := NewCompositeCache("")
|
|
ch1.AddKey("RUN value")
|
|
hash1, err := ch1.Hash()
|
|
if err != nil {
|
|
t.Errorf("couldn't create hash %v", err)
|
|
}
|
|
|
|
ch2 := NewCompositeCache("")
|
|
ch2.AddKey("|1")
|
|
ch2.AddKey("arg=anotherValue")
|
|
ch2.AddKey("RUN anotherValue")
|
|
hash2, err := ch2.Hash()
|
|
if err != nil {
|
|
t.Errorf("couldn't create hash %v", err)
|
|
}
|
|
command := MockDockerCommand{
|
|
command: "RUN $arg",
|
|
contextFiles: []string{},
|
|
cacheCommand: MockCachedDockerCommand{
|
|
contextFiles: []string{},
|
|
},
|
|
}
|
|
|
|
return testcase{
|
|
description: "cached run command with another build arg pushes layer",
|
|
config: &v1.ConfigFile{Config: v1.Config{WorkingDir: dir}},
|
|
opts: &config.KanikoOptions{Cache: true},
|
|
args: map[string]string{
|
|
"arg": "anotherValue",
|
|
},
|
|
// layer for arg=value already exists
|
|
layerCache: &fakeLayerCache{
|
|
img: &fakeImage{ImageLayers: []v1.Layer{fakeLayer{}}},
|
|
keySequence: []string{hash1},
|
|
},
|
|
expectedCacheKeys: []string{hash2},
|
|
pushedCacheKeys: []string{hash2},
|
|
commands: []commands.DockerCommand{command},
|
|
rootDir: dir,
|
|
}
|
|
}(),
|
|
}
|
|
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.NewBuildArgs([]string{}), //required or code will panic
|
|
image: tc.image,
|
|
opts: tc.opts,
|
|
cf: cf,
|
|
snapshotter: snap,
|
|
layerCache: lc,
|
|
pushLayerToCache: func(_ *config.KanikoOptions, cacheKey, _, _ string) error {
|
|
keys = append(keys, cacheKey)
|
|
return nil
|
|
},
|
|
}
|
|
sb.cmds = tc.commands
|
|
for key, value := range tc.args {
|
|
sb.args.AddArg(key, &value)
|
|
}
|
|
tmp := config.RootDir
|
|
if tc.rootDir != "" {
|
|
config.RootDir = tc.rootDir
|
|
}
|
|
err := sb.build()
|
|
if err != nil {
|
|
t.Errorf("Expected error to be nil but was %v", err)
|
|
}
|
|
fmt.Println(lc.receivedKeys)
|
|
assertCacheKeys(t, tc.expectedCacheKeys, lc.receivedKeys, "receive")
|
|
assertCacheKeys(t, tc.pushedCacheKeys, keys, "push")
|
|
|
|
config.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(fileContext util.FileContext, cmds []instructions.Command, cacheCopy bool) []commands.DockerCommand {
|
|
outCommands := make([]commands.DockerCommand, 0)
|
|
for _, c := range cmds {
|
|
cmd, err := commands.GetCommand(
|
|
c,
|
|
fileContext,
|
|
false,
|
|
cacheCopy,
|
|
)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
outCommands = append(outCommands, cmd)
|
|
}
|
|
return outCommands
|
|
|
|
}
|
|
|
|
func tempDirAndFile(t *testing.T) (string, []string) {
|
|
filenames := []string{"bar.txt"}
|
|
|
|
dir := t.TempDir()
|
|
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()
|
|
}
|
|
|
|
func hashCompositeKeys(t *testing.T, ck1 CompositeCache, ck2 CompositeCache) (string, string) {
|
|
key1, err := ck1.Hash()
|
|
if err != nil {
|
|
t.Errorf("could not hash composite key due to %s", err)
|
|
}
|
|
key2, err := ck2.Hash()
|
|
if err != nil {
|
|
t.Errorf("could not hash composite key due to %s", err)
|
|
}
|
|
return key1, key2
|
|
}
|
|
|
|
func Test_stageBuild_populateCompositeKeyForCopyCommand(t *testing.T) {
|
|
// See https://github.com/GoogleContainerTools/kaniko/issues/589
|
|
|
|
for _, tc := range []struct {
|
|
description string
|
|
command string
|
|
expectedCacheKey string
|
|
}{
|
|
{
|
|
description: "multi-stage copy command",
|
|
command: "COPY --from=0 foo.txt bar.txt",
|
|
expectedCacheKey: "COPY --from=0 foo.txt bar.txt-some-cache-key",
|
|
},
|
|
{
|
|
description: "copy command",
|
|
command: "COPY foo.txt bar.txt",
|
|
expectedCacheKey: "COPY foo.txt bar.txt",
|
|
},
|
|
} {
|
|
t.Run(tc.description, func(t *testing.T) {
|
|
instructions, err := dockerfile.ParseCommands([]string{tc.command})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
fc := util.FileContext{Root: "workspace"}
|
|
copyCommand, err := commands.GetCommand(instructions[0], fc, false, true)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
for _, useCacheCommand := range []bool{false, true} {
|
|
t.Run(fmt.Sprintf("CacheCommand=%t", useCacheCommand), func(t *testing.T) {
|
|
var cmd fmt.Stringer = copyCommand
|
|
if useCacheCommand {
|
|
cmd = copyCommand.(*commands.CopyCommand).CacheCommand(nil)
|
|
}
|
|
|
|
sb := &stageBuilder{
|
|
fileContext: fc,
|
|
stageIdxToDigest: map[string]string{
|
|
"0": "some-digest",
|
|
},
|
|
digestToCacheKey: map[string]string{
|
|
"some-digest": "some-cache-key",
|
|
},
|
|
}
|
|
|
|
ck := CompositeCache{}
|
|
ck, err = sb.populateCompositeKey(
|
|
cmd,
|
|
[]string{},
|
|
ck,
|
|
dockerfile.NewBuildArgs([]string{}),
|
|
[]string{},
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
actualCacheKey := ck.Key()
|
|
if tc.expectedCacheKey != actualCacheKey {
|
|
t.Errorf(
|
|
"Expected cache key to be %s, was %s",
|
|
tc.expectedCacheKey,
|
|
actualCacheKey,
|
|
)
|
|
}
|
|
|
|
})
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_ResolveCrossStageInstructions(t *testing.T) {
|
|
df := `
|
|
FROM scratch
|
|
RUN echo hi > /hi
|
|
|
|
FROM scratch AS second
|
|
COPY --from=0 /hi /hi2
|
|
|
|
FROM scratch AS tHiRd
|
|
COPY --from=second /hi2 /hi3
|
|
COPY --from=1 /hi2 /hi3
|
|
|
|
FROM scratch
|
|
COPY --from=thIrD /hi3 /hi4
|
|
COPY --from=third /hi3 /hi4
|
|
COPY --from=2 /hi3 /hi4
|
|
`
|
|
stages, metaArgs, err := dockerfile.Parse([]byte(df))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
opts := &config.KanikoOptions{}
|
|
kanikoStages, err := dockerfile.MakeKanikoStages(opts, stages, metaArgs)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
stageToIdx := ResolveCrossStageInstructions(kanikoStages)
|
|
for index, stage := range stages {
|
|
if index == 0 {
|
|
continue
|
|
}
|
|
expectedStage := strconv.Itoa(index - 1)
|
|
for _, command := range stage.Commands {
|
|
copyCmd := command.(*instructions.CopyCommand)
|
|
if copyCmd.From != expectedStage {
|
|
t.Fatalf("unexpected copy command: %s resolved to stage %s, expected %s", copyCmd.String(), copyCmd.From, expectedStage)
|
|
}
|
|
}
|
|
|
|
expectedMap := map[string]string{"second": "1", "third": "2"}
|
|
testutil.CheckDeepEqual(t, expectedMap, stageToIdx)
|
|
}
|
|
}
|