mirror of
https://github.com/GoogleContainerTools/kaniko
synced 2026-10-17 20:17:11 +02:00
This PR adds support for the dockerignore file. Previously when kaniko had support for the dockerignore file, kaniko first went through the build context and deleted files that were meant to be ignored. This resulted in a really bad bug where files in user mounted volumes would be deleted (my bad). This time around, instead of modifying the build context at all, kaniko will check if a file should be excluded when executing ADD/COPY commands. If a file should be excluded (based on the .dockerignore) it won't be copied over from the buildcontext and shouldn't end up in the final image. I also added a .dockerignore file and Dockerfile as an integration test, which should fail if the dockerignore is not being processed correctly or if files aren't being excluded correctly. Also, I removed all the integration testing from the previous version of the dockerignore support.
599 lines
13 KiB
Go
599 lines
13 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 util
|
|
|
|
import (
|
|
"archive/tar"
|
|
"bytes"
|
|
"io/ioutil"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"sort"
|
|
"testing"
|
|
|
|
"github.com/GoogleContainerTools/kaniko/testutil"
|
|
)
|
|
|
|
func Test_DetectFilesystemWhitelist(t *testing.T) {
|
|
testDir, err := ioutil.TempDir("", "")
|
|
if err != nil {
|
|
t.Fatalf("Error creating tempdir: %s", err)
|
|
}
|
|
fileContents := `
|
|
228 122 0:90 / / rw,relatime - aufs none rw,si=f8e2406af90782bc,dio,dirperm1
|
|
229 228 0:98 / /proc rw,nosuid,nodev,noexec,relatime - proc proc rw
|
|
230 228 0:99 / /dev rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755
|
|
231 230 0:100 / /dev/pts rw,nosuid,noexec,relatime - devpts devpts rw,gid=5,mode=620,ptmxmode=666
|
|
232 228 0:101 / /sys ro,nosuid,nodev,noexec,relatime - sysfs sysfs ro`
|
|
|
|
path := filepath.Join(testDir, "mountinfo")
|
|
if err := os.MkdirAll(filepath.Dir(path), 0750); err != nil {
|
|
t.Fatalf("Error creating tempdir: %s", err)
|
|
}
|
|
if err := ioutil.WriteFile(path, []byte(fileContents), 0644); err != nil {
|
|
t.Fatalf("Error writing file contents to %s: %s", path, err)
|
|
}
|
|
|
|
err = DetectFilesystemWhitelist(path)
|
|
expectedWhitelist := []WhitelistEntry{
|
|
{"/kaniko", false},
|
|
{"/proc", false},
|
|
{"/dev", false},
|
|
{"/dev/pts", false},
|
|
{"/sys", false},
|
|
{"/var/run", false},
|
|
{"/etc/mtab", false},
|
|
}
|
|
actualWhitelist := whitelist
|
|
sort.Slice(actualWhitelist, func(i, j int) bool {
|
|
return actualWhitelist[i].Path < actualWhitelist[j].Path
|
|
})
|
|
sort.Slice(expectedWhitelist, func(i, j int) bool {
|
|
return expectedWhitelist[i].Path < expectedWhitelist[j].Path
|
|
})
|
|
testutil.CheckErrorAndDeepEqual(t, false, err, expectedWhitelist, actualWhitelist)
|
|
}
|
|
|
|
var tests = []struct {
|
|
files map[string]string
|
|
directory string
|
|
expectedFiles []string
|
|
}{
|
|
{
|
|
files: map[string]string{
|
|
"/workspace/foo/a": "baz1",
|
|
"/workspace/foo/b": "baz2",
|
|
"/kaniko/file": "file",
|
|
},
|
|
directory: "/workspace/foo/",
|
|
expectedFiles: []string{
|
|
"workspace/foo/a",
|
|
"workspace/foo/b",
|
|
"workspace/foo",
|
|
},
|
|
},
|
|
{
|
|
files: map[string]string{
|
|
"/workspace/foo/a": "baz1",
|
|
},
|
|
directory: "/workspace/foo/a",
|
|
expectedFiles: []string{
|
|
"workspace/foo/a",
|
|
},
|
|
},
|
|
{
|
|
files: map[string]string{
|
|
"/workspace/foo/a": "baz1",
|
|
"/workspace/foo/b": "baz2",
|
|
"/workspace/baz": "hey",
|
|
"/kaniko/file": "file",
|
|
},
|
|
directory: "/workspace",
|
|
expectedFiles: []string{
|
|
"workspace/foo/a",
|
|
"workspace/foo/b",
|
|
"workspace/baz",
|
|
"workspace",
|
|
"workspace/foo",
|
|
},
|
|
},
|
|
{
|
|
files: map[string]string{
|
|
"/workspace/foo/a": "baz1",
|
|
"/workspace/foo/b": "baz2",
|
|
},
|
|
directory: "",
|
|
expectedFiles: []string{
|
|
"workspace/foo/a",
|
|
"workspace/foo/b",
|
|
"workspace",
|
|
"workspace/foo",
|
|
".",
|
|
},
|
|
},
|
|
}
|
|
|
|
func Test_RelativeFiles(t *testing.T) {
|
|
for _, test := range tests {
|
|
testDir, err := ioutil.TempDir("", "")
|
|
if err != nil {
|
|
t.Fatalf("err setting up temp dir: %v", err)
|
|
}
|
|
defer os.RemoveAll(testDir)
|
|
if err := testutil.SetupFiles(testDir, test.files); err != nil {
|
|
t.Fatalf("err setting up files: %v", err)
|
|
}
|
|
actualFiles, err := RelativeFiles(test.directory, testDir)
|
|
sort.Strings(actualFiles)
|
|
sort.Strings(test.expectedFiles)
|
|
testutil.CheckErrorAndDeepEqual(t, false, err, test.expectedFiles, actualFiles)
|
|
}
|
|
}
|
|
|
|
func Test_ParentDirectories(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
path string
|
|
expected []string
|
|
}{
|
|
{
|
|
name: "regular path",
|
|
path: "/path/to/dir",
|
|
expected: []string{
|
|
"/",
|
|
"/path",
|
|
"/path/to",
|
|
},
|
|
},
|
|
{
|
|
name: "current directory",
|
|
path: ".",
|
|
expected: []string{
|
|
"/",
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
actual := ParentDirectories(tt.path)
|
|
testutil.CheckErrorAndDeepEqual(t, false, nil, tt.expected, actual)
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_CheckWhitelist(t *testing.T) {
|
|
type args struct {
|
|
path string
|
|
whitelist []WhitelistEntry
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
args args
|
|
want bool
|
|
}{
|
|
{
|
|
name: "file whitelisted",
|
|
args: args{
|
|
path: "/foo",
|
|
whitelist: []WhitelistEntry{{"/foo", false}},
|
|
},
|
|
want: true,
|
|
},
|
|
{
|
|
name: "directory whitelisted",
|
|
args: args{
|
|
path: "/foo/bar",
|
|
whitelist: []WhitelistEntry{{"/foo", false}},
|
|
},
|
|
want: true,
|
|
},
|
|
{
|
|
name: "grandparent whitelisted",
|
|
args: args{
|
|
path: "/foo/bar/baz",
|
|
whitelist: []WhitelistEntry{{"/foo", false}},
|
|
},
|
|
want: true,
|
|
},
|
|
{
|
|
name: "sibling whitelisted",
|
|
args: args{
|
|
path: "/foo/bar/baz",
|
|
whitelist: []WhitelistEntry{{"/foo/bat", false}},
|
|
},
|
|
want: false,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
original := whitelist
|
|
defer func() {
|
|
whitelist = original
|
|
}()
|
|
whitelist = tt.args.whitelist
|
|
got, err := CheckWhitelist(tt.args.path)
|
|
if err != nil {
|
|
t.Fatalf("error checking whitelist: %v", err)
|
|
}
|
|
if got != tt.want {
|
|
t.Errorf("CheckWhitelist() = %v, want %v", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestHasFilepathPrefix(t *testing.T) {
|
|
type args struct {
|
|
path string
|
|
prefix string
|
|
prefixMatchOnly bool
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
args args
|
|
want bool
|
|
}{
|
|
{
|
|
name: "parent",
|
|
args: args{
|
|
path: "/foo/bar",
|
|
prefix: "/foo",
|
|
prefixMatchOnly: false,
|
|
},
|
|
want: true,
|
|
},
|
|
{
|
|
name: "nested parent",
|
|
args: args{
|
|
path: "/foo/bar/baz",
|
|
prefix: "/foo/bar",
|
|
prefixMatchOnly: false,
|
|
},
|
|
want: true,
|
|
},
|
|
{
|
|
name: "sibling",
|
|
args: args{
|
|
path: "/foo/bar",
|
|
prefix: "/bar",
|
|
prefixMatchOnly: false,
|
|
},
|
|
want: false,
|
|
},
|
|
{
|
|
name: "nested sibling",
|
|
args: args{
|
|
path: "/foo/bar/baz",
|
|
prefix: "/foo/bar",
|
|
prefixMatchOnly: false,
|
|
},
|
|
want: true,
|
|
},
|
|
{
|
|
name: "name prefix",
|
|
args: args{
|
|
path: "/foo2/bar",
|
|
prefix: "/foo",
|
|
prefixMatchOnly: false,
|
|
},
|
|
want: false,
|
|
},
|
|
{
|
|
name: "prefix match only (volume)",
|
|
args: args{
|
|
path: "/foo",
|
|
prefix: "/foo",
|
|
prefixMatchOnly: true,
|
|
},
|
|
want: false,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
if got := HasFilepathPrefix(tt.args.path, tt.args.prefix, tt.args.prefixMatchOnly); got != tt.want {
|
|
t.Errorf("HasFilepathPrefix() = %v, want %v", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
type checker func(root string, t *testing.T)
|
|
|
|
func fileExists(p string) checker {
|
|
return func(root string, t *testing.T) {
|
|
_, err := os.Stat(filepath.Join(root, p))
|
|
if err != nil {
|
|
t.Fatalf("File %s does not exist", filepath.Join(root, p))
|
|
}
|
|
}
|
|
}
|
|
|
|
func fileMatches(p string, c []byte) checker {
|
|
return func(root string, t *testing.T) {
|
|
actual, err := ioutil.ReadFile(filepath.Join(root, p))
|
|
if err != nil {
|
|
t.Fatalf("error reading file: %s", p)
|
|
}
|
|
if !reflect.DeepEqual(actual, c) {
|
|
t.Errorf("file contents do not match. %v!=%v", actual, c)
|
|
}
|
|
}
|
|
}
|
|
|
|
func permissionsMatch(p string, perms os.FileMode) checker {
|
|
return func(root string, t *testing.T) {
|
|
fi, err := os.Stat(filepath.Join(root, p))
|
|
if err != nil {
|
|
t.Fatalf("error statting file %s", p)
|
|
}
|
|
if fi.Mode() != perms {
|
|
t.Errorf("Permissions do not match. %s != %s", fi.Mode(), perms)
|
|
}
|
|
}
|
|
}
|
|
|
|
func linkPointsTo(src, dst string) checker {
|
|
return func(root string, t *testing.T) {
|
|
link := filepath.Join(root, src)
|
|
got, err := os.Readlink(link)
|
|
if err != nil {
|
|
t.Fatalf("error reading link %s: %s", link, err)
|
|
}
|
|
if got != dst {
|
|
t.Errorf("link destination does not match: %s != %s", got, dst)
|
|
}
|
|
}
|
|
}
|
|
|
|
func filesAreHardlinks(first, second string) checker {
|
|
return func(root string, t *testing.T) {
|
|
fi1, err := os.Stat(filepath.Join(root, first))
|
|
if err != nil {
|
|
t.Fatalf("error getting file %s", first)
|
|
}
|
|
fi2, err := os.Stat(filepath.Join(root, second))
|
|
if err != nil {
|
|
t.Fatalf("error getting file %s", second)
|
|
}
|
|
stat1 := getSyscallStatT(fi1)
|
|
stat2 := getSyscallStatT(fi2)
|
|
if stat1.Ino != stat2.Ino {
|
|
t.Errorf("%s and %s aren't hardlinks as they dont' have the same inode", first, second)
|
|
}
|
|
}
|
|
}
|
|
|
|
func fileHeader(name string, contents string, mode int64) *tar.Header {
|
|
return &tar.Header{
|
|
Name: name,
|
|
Size: int64(len(contents)),
|
|
Mode: mode,
|
|
Typeflag: tar.TypeReg,
|
|
Uid: os.Getuid(),
|
|
Gid: os.Getgid(),
|
|
}
|
|
}
|
|
|
|
func linkHeader(name, linkname string) *tar.Header {
|
|
return &tar.Header{
|
|
Name: name,
|
|
Size: 0,
|
|
Typeflag: tar.TypeSymlink,
|
|
Linkname: linkname,
|
|
}
|
|
}
|
|
|
|
func hardlinkHeader(name, linkname string) *tar.Header {
|
|
return &tar.Header{
|
|
Name: name,
|
|
Size: 0,
|
|
Typeflag: tar.TypeLink,
|
|
Linkname: linkname,
|
|
}
|
|
}
|
|
|
|
func dirHeader(name string, mode int64) *tar.Header {
|
|
return &tar.Header{
|
|
Name: name,
|
|
Size: 0,
|
|
Typeflag: tar.TypeDir,
|
|
Mode: mode,
|
|
Uid: os.Getuid(),
|
|
Gid: os.Getgid(),
|
|
}
|
|
}
|
|
|
|
func TestExtractFile(t *testing.T) {
|
|
type tc struct {
|
|
name string
|
|
hdrs []*tar.Header
|
|
tmpdir string
|
|
contents []byte
|
|
checkers []checker
|
|
}
|
|
|
|
tcs := []tc{
|
|
{
|
|
name: "normal file",
|
|
contents: []byte("helloworld"),
|
|
hdrs: []*tar.Header{fileHeader("./bar", "helloworld", 0644)},
|
|
checkers: []checker{
|
|
fileExists("/bar"),
|
|
fileMatches("/bar", []byte("helloworld")),
|
|
permissionsMatch("/bar", 0644),
|
|
},
|
|
},
|
|
{
|
|
name: "normal file, directory does not exist",
|
|
contents: []byte("helloworld"),
|
|
hdrs: []*tar.Header{fileHeader("./foo/bar", "helloworld", 0644)},
|
|
checkers: []checker{
|
|
fileExists("/foo/bar"),
|
|
fileMatches("/foo/bar", []byte("helloworld")),
|
|
permissionsMatch("/foo/bar", 0644),
|
|
permissionsMatch("/foo", 0755|os.ModeDir),
|
|
},
|
|
},
|
|
{
|
|
name: "normal file, directory is created after",
|
|
contents: []byte("helloworld"),
|
|
hdrs: []*tar.Header{
|
|
fileHeader("./foo/bar", "helloworld", 0644),
|
|
dirHeader("./foo", 0722),
|
|
},
|
|
checkers: []checker{
|
|
fileExists("/foo/bar"),
|
|
fileMatches("/foo/bar", []byte("helloworld")),
|
|
permissionsMatch("/foo/bar", 0644),
|
|
permissionsMatch("/foo", 0722|os.ModeDir),
|
|
},
|
|
},
|
|
{
|
|
name: "symlink",
|
|
hdrs: []*tar.Header{linkHeader("./bar", "bar/bat")},
|
|
checkers: []checker{
|
|
linkPointsTo("/bar", "bar/bat"),
|
|
},
|
|
},
|
|
{
|
|
name: "symlink relative path",
|
|
hdrs: []*tar.Header{linkHeader("./bar", "./foo/bar/baz")},
|
|
checkers: []checker{
|
|
linkPointsTo("/bar", "./foo/bar/baz"),
|
|
},
|
|
},
|
|
{
|
|
name: "symlink parent does not exist",
|
|
hdrs: []*tar.Header{linkHeader("./foo/bar/baz", "../../bat")},
|
|
checkers: []checker{
|
|
linkPointsTo("/foo/bar/baz", "../../bat"),
|
|
},
|
|
},
|
|
{
|
|
name: "symlink parent does not exist 2",
|
|
hdrs: []*tar.Header{linkHeader("./foo/bar/baz", "../../bat")},
|
|
checkers: []checker{
|
|
linkPointsTo("/foo/bar/baz", "../../bat"),
|
|
permissionsMatch("/foo", 0755|os.ModeDir),
|
|
permissionsMatch("/foo/bar", 0755|os.ModeDir),
|
|
},
|
|
},
|
|
{
|
|
name: "hardlink",
|
|
tmpdir: "/tmp/hardlink",
|
|
hdrs: []*tar.Header{
|
|
fileHeader("/bin/gzip", "gzip-binary", 0751),
|
|
hardlinkHeader("/bin/uncompress", "/bin/gzip"),
|
|
},
|
|
checkers: []checker{
|
|
fileExists("/bin/gzip"),
|
|
filesAreHardlinks("/bin/uncompress", "/bin/gzip"),
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range tcs {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
tc := tc
|
|
t.Parallel()
|
|
r := ""
|
|
var err error
|
|
|
|
if tc.tmpdir != "" {
|
|
r = tc.tmpdir
|
|
} else {
|
|
r, err = ioutil.TempDir("", "")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
defer os.RemoveAll(r)
|
|
|
|
for _, hdr := range tc.hdrs {
|
|
if err := extractFile(r, hdr, bytes.NewReader(tc.contents)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
for _, checker := range tc.checkers {
|
|
checker(r, t)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCopySymlink(t *testing.T) {
|
|
type tc struct {
|
|
name string
|
|
linkTarget string
|
|
dest string
|
|
beforeLink func(r string) error
|
|
}
|
|
|
|
tcs := []tc{{
|
|
name: "absolute symlink",
|
|
linkTarget: "/abs/dest",
|
|
}, {
|
|
name: "relative symlink",
|
|
linkTarget: "rel",
|
|
}, {
|
|
name: "symlink copy overwrites existing file",
|
|
linkTarget: "/abs/dest",
|
|
dest: "overwrite_me",
|
|
beforeLink: func(r string) error {
|
|
return ioutil.WriteFile(filepath.Join(r, "overwrite_me"), nil, 0644)
|
|
},
|
|
}}
|
|
|
|
for _, tc := range tcs {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
tc := tc
|
|
t.Parallel()
|
|
r, err := ioutil.TempDir("", "")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer os.RemoveAll(r)
|
|
|
|
if tc.beforeLink != nil {
|
|
if err := tc.beforeLink(r); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
link := filepath.Join(r, "link")
|
|
dest := filepath.Join(r, "copy")
|
|
if tc.dest != "" {
|
|
dest = filepath.Join(r, tc.dest)
|
|
}
|
|
if err := os.Symlink(tc.linkTarget, link); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := CopySymlink(link, dest, ""); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got, err := os.Readlink(dest)
|
|
if err != nil {
|
|
t.Fatalf("error reading link %s: %s", link, err)
|
|
}
|
|
if got != tc.linkTarget {
|
|
t.Errorf("link target does not match: %s != %s", got, tc.linkTarget)
|
|
}
|
|
})
|
|
}
|
|
}
|