Skip to content

Commit

Permalink
Only build images for tests are we are running
Browse files Browse the repository at this point in the history
If we are running only one individual test, we don't want to build
all of the images, so this commit creates a builder which tracks which
images it has built and can be used by a tests to check if it should
build an image before running, or it will use the images that have
already been built by a previous test.

The name of the context tarball has also been randomized to avoid
potential test flakes if two tests using the same GCS bucket run
simultaneously.
  • Loading branch information
bobcatfish committed Jul 25, 2018
1 parent 2a788a9 commit ab753a6
Show file tree
Hide file tree
Showing 6 changed files with 58 additions and 173 deletions.
8 changes: 7 additions & 1 deletion DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ You can also run tests with `go test`, for example to run tests individually:
```shell
export GCS_BUCKET="gs://<your bucket>"
export IMAGE_REPO="gcr.io/somerepo"
go test -v --bucket $GCS_BUCKET --repo $IMAGE_REPO -run TestLayers/test_layer_dockerfiles/Dockerfile_test_copy
go test -v --bucket $GCS_BUCKET --repo $IMAGE_REPO -run TestLayers/test_layer_Dockerfile_test_copy_bucket
```

Requirements:
Expand All @@ -99,6 +99,12 @@ If you want to run these tests yourself, you must override the project using env

These tests will be kicked off by [reviewers](#reviews) for submitted PRs.

#### Troubleshooting

If you see errors due to `container-diff` failing, try pulling the base images (specified in [the
integration test Dockerfiles](./integration/dockerfiles)). If the base image has been updated,
kaniko may be using a newer base image than your local docker daemon.

## Creating a PR

When you have changes you would like to propose to kaniko, you will need to:
Expand Down
6 changes: 4 additions & 2 deletions integration/cleanup.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,13 @@ import (
"os/signal"
)

func runOnInterrupt(f func()) {
// RunOnInterrupt will execute the function f if execution is interrupted with the
// interrupt signal.
func RunOnInterrupt(f func()) {
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt)
go func() {
for _ = range c {
for range c {
log.Println("Interrupted, cleaning up.")
f()
os.Exit(1)
Expand Down
18 changes: 12 additions & 6 deletions integration/gcs.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ import (
"path/filepath"
)

func createIntegrationTarball() (string, error) {
// CreateIntegrationTarball will take the contents of the integration directory and write
// them to a tarball in a temmporary dir. It will return a path to the tarball.
func CreateIntegrationTarball() (string, error) {
log.Println("Creating tarball of integration test files to use as build context")
dir, err := os.Getwd()
if err != nil {
Expand All @@ -45,19 +47,23 @@ func createIntegrationTarball() (string, error) {
return contextFile, err
}

func uploadBuildContext(gcsBucket string, contextFile string) (string, error) {
log.Printf("Uploading tarball at %s to GCS bucket at %s\n", contextFile, gcsBucket)
// UploadFileToBucket will upload the at filePath to gcsBucket. It will return the path
// of the file in gcsBucket.
func UploadFileToBucket(gcsBucket string, filePath string) (string, error) {
log.Printf("Uploading file at %s to GCS bucket at %s\n", filePath, gcsBucket)

cmd := exec.Command("gsutil", "cp", contextFile, gcsBucket)
cmd := exec.Command("gsutil", "cp", filePath, gcsBucket)
_, err := RunCommandWithoutTest(cmd)
if err != nil {
return "", fmt.Errorf("Failed to copy tarball to GCS bucket %s: %s", gcsBucket, err)
}

return filepath.Join(gcsBucket, contextFile), err
return filepath.Join(gcsBucket, filePath), err
}

func deleteFromGCS(path string) error {
// DeleteFromBucket will remove the content at path. path should be the full path
// to a file in GCS.
func DeleteFromBucket(path string) error {
cmd := exec.Command("gsutil", "rm", path)
_, err := RunCommandWithoutTest(cmd)
if err != nil {
Expand Down
10 changes: 7 additions & 3 deletions integration/images.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,15 @@ var singleSnapshotImages = map[string]bool{
var bucketContextTests = []string{"Dockerfile_test_copy_bucket"}
var reproducibleTests = []string{"Dockerfile_test_env"}

func getDockerImage(imageRepo, dockerfile string) string {
// GetDockerImage constructs the name of the docker image that would be built with
// dockerfile if it was tagged with imageRepo.
func GetDockerImage(imageRepo, dockerfile string) string {
return strings.ToLower(imageRepo + dockerPrefix + dockerfile)
}

func getKanikoImage(imageRepo, dockerfile string) string {
// GetDockerImage constructs the name of the kaniko image that would be built with
// dockerfile if it was tagged with imageRepo.
func GetKanikoImage(imageRepo, dockerfile string) string {
return strings.ToLower(imageRepo + kanikoPrefix + dockerfile)
}

Expand Down Expand Up @@ -156,7 +160,7 @@ func (d *DockerFileBuilder) BuildImage(imageRepo, gcsBucket, dockerfilesPath, do
if singleSnapshotImages[dockerfile] {
buildArgs = append(buildArgs, singleSnapshotFlag)
}
kanikoImage := getKanikoImage(imageRepo, dockerfile)
kanikoImage := GetKanikoImage(imageRepo, dockerfile)
kanikoCmd := exec.Command("docker",
append([]string{"run",
"-v", os.Getenv("HOME") + "/.config/gcloud:/root/.config/gcloud",
Expand Down
188 changes: 28 additions & 160 deletions integration/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,6 @@ import (
"math"
"os"
"os/exec"
"path"
"path/filepath"
"runtime"
"strings"
"testing"

Expand All @@ -37,6 +34,7 @@ import (
)

var config = initGCPConfig()
var imageBuilder *DockerFileBuilder

type gcpConfig struct {
gcsBucket string
Expand All @@ -61,15 +59,9 @@ func initGCPConfig() *gcpConfig {
}

const (
executorImage = "executor-image"
dockerImage = "gcr.io/cloud-builders/docker"
ubuntuImage = "ubuntu"
dockerPrefix = "docker-"
kanikoPrefix = "kaniko-"
daemonPrefix = "daemon://"
dockerfilesPath = "dockerfiles"
buildContextPath = "/workspace"
singleSnapshotFlag = "--single-snapshot"
emptyContainerDiff = `[
{
"Image1": "%s",
Expand All @@ -93,9 +85,6 @@ const (
]`
)

// TODO: remove test_user_run from this when https://github.com/GoogleContainerTools/container-diff/issues/237 is fixed
var testsToIgnore = map[string]bool{"Dockerfile_test_user_run": true}

func meetsRequirements() bool {
requiredTools := []string{"container-diff", "gsutil"}
hasRequirements := true
Expand All @@ -114,13 +103,13 @@ func TestMain(m *testing.M) {
fmt.Println("Missing required tools")
os.Exit(1)
}
contextFile, err := createIntegrationTarball()
contextFile, err := CreateIntegrationTarball()
if err != nil {
fmt.Println("Failed to create tarball of integration files for build context", err)
os.Exit(1)
}

fileInBucket, err := uploadBuildContext(config.gcsBucket, contextFile)
fileInBucket, err := UploadFileToBucket(config.gcsBucket, contextFile)
if err != nil {
fmt.Println("Failed to upload build context", err)
os.Exit(1)
Expand All @@ -131,155 +120,37 @@ func TestMain(m *testing.M) {
err = fmt.Errorf("Failed to remove tarball at %s: %s", contextFile, err)
}

runOnInterrupt(func() { deleteFromGCS(fileInBucket) })
defer deleteFromGCS(fileInBucket)
RunOnInterrupt(func() { DeleteFromBucket(fileInBucket) })
defer DeleteFromBucket(fileInBucket)

fmt.Println("Building kaniko image")
buildKaniko := exec.Command("docker", "build", "-t", executorImage, "-f", "../deploy/Dockerfile", "..")
buildKaniko := exec.Command("docker", "build", "-t", ExecutorImage, "-f", "../deploy/Dockerfile", "..")
err = buildKaniko.Run()
if err != nil {
fmt.Print(err)
fmt.Print("Building kaniko failed.")
os.Exit(1)
}

fmt.Println("Building all test images with both docker and kaniko")
err = buildImages()
os.Exit(m.Run())
}

func getDockerFiles(dockerfilesPath string) ([]string, error) {
dockerfiles, err := filepath.Glob(path.Join(dockerfilesPath, "Dockerfile_test*"))
if err != nil {
return []string{}, fmt.Errorf("Failed to find docker files at %s: %s", dockerfilesPath, err)
}
return dockerfiles, nil
}

func getDockerImage(imageRepo, dockerfile string) string {
return strings.ToLower(imageRepo + dockerPrefix + dockerfile)
}

func getKanikoImage(imageRepo, dockerfile string) string {
return strings.ToLower(config.imageRepo + kanikoPrefix + dockerfile)
}

// buildImages will bulid all dockerfils in ./integration/dockerfiles using both docker
// and Kaniko, so subsequent tests can compare the results.
func buildImages() error {
dockerfiles, err := getDockerFiles(dockerfilesPath)
dockerfiles, err := findDockerFiles(dockerfilesPath)
if err != nil {
return fmt.Errorf("Couldn't build images because files couldn't be found: %s", err)
}

// Maps Dockerfiles to the args that they should be build with
argsMap := map[string][]string{
"Dockerfile_test_run": {"file=/file"},
"Dockerfile_test_workdir": {"workdir=/arg/workdir"},
"Dockerfile_test_add": {"file=context/foo"},
"Dockerfile_test_onbuild": {"file=/tmp/onbuild"},
"Dockerfile_test_scratch": {
"image=scratch",
"hello=hello-value",
"file=context/foo",
"file3=context/b*",
},
"Dockerfile_test_multistage": {"file=/foo2"},
}

// These images will be built via Kaniko with only one layer/snapshot, to test
// the single snapshot functionality.
singleSnapshotImages := map[string]bool{
"Dockerfile_test_add": true,
"Dockerfile_test_scratch": true,
}

bucketContextTests := []string{"Dockerfile_test_copy_bucket"}
reproducibleTests := []string{"Dockerfile_test_env"}

_, ex, _, _ := runtime.Caller(0)
cwd := filepath.Dir(ex)

for _, dockerfile := range dockerfiles {
dockerfile = dockerfile[len("dockerfile/")+1:]
if testsToIgnore[dockerfile] {
continue
}
fmt.Printf("Building images for Dockerfile %s\n", dockerfile)

var buildArgs []string
buildArgFlag := "--build-arg"
for _, arg := range argsMap[dockerfile] {
buildArgs = append(buildArgs, buildArgFlag)
buildArgs = append(buildArgs, arg)
}
// build docker image
dockerImage := strings.ToLower(config.imageRepo + dockerPrefix + dockerfile)
dockerCmd := exec.Command("docker",
append([]string{"build",
"-t", dockerImage,
"-f", path.Join(dockerfilesPath, dockerfile),
"."},
buildArgs...)...,
)
_, err := RunCommandWithoutTest(dockerCmd)
if err != nil {
return fmt.Errorf("Failed to build image %s with docker command \"%s\": %s", dockerImage, dockerCmd.Args, err)
}

contextFlag := "-c"
contextPath := buildContextPath
for _, d := range bucketContextTests {
if d == dockerfile {
contextFlag = "-b"
contextPath = config.gcsBucket
break
}
}

reproducibleFlag := ""
for _, d := range reproducibleTests {
if d == dockerfile {
reproducibleFlag = "--reproducible"
break
}
}

// build kaniko image
if singleSnapshotImages[dockerfile] {
buildArgs = append(buildArgs, singleSnapshotFlag)
}
kanikoImage := getKanikoImage(config.imageRepo, dockerfile)
kanikoCmd := exec.Command("docker",
append([]string{"run",
"-v", os.Getenv("HOME") + "/.config/gcloud:/root/.config/gcloud",
"-v", cwd + ":/workspace",
executorImage,
"-f", path.Join(buildContextPath, dockerfilesPath, dockerfile),
"-d", kanikoImage, reproducibleFlag,
contextFlag, contextPath},
buildArgs...)...,
)

_, err = RunCommandWithoutTest(kanikoCmd)
if err != nil {
return fmt.Errorf("Failed to build image %s with kaniko command \"%s\": %s", dockerImage, kanikoCmd.Args, err)
}
fmt.Printf("Coudn't create map of dockerfiles: %s", err)
os.Exit(1)
}
return nil
imageBuilder = NewDockerFileBuilder(dockerfiles)
os.Exit(m.Run())
}

func TestRun(t *testing.T) {
dockerfiles, err := getDockerFiles(dockerfilesPath)
if err != nil {
t.Fatalf("Couldn't run tests because files couldn't be found: %s", err)
}
for _, dockerfile := range dockerfiles {
for dockerfile, built := range imageBuilder.FilesBuilt {
t.Run("test_"+dockerfile, func(t *testing.T) {
if testsToIgnore[dockerfile] {
t.SkipNow()
if !built {
err := imageBuilder.BuildImage(config.imageRepo, config.gcsBucket, dockerfilesPath, dockerfile)
if err != nil {
t.Fatalf("Failed to build kaniko and docker images for %s: %s", dockerfile, err)
}
}
kanikoImage := getKanikoImage(config.imageRepo, dockerfile)
dockerImage := GetDockerImage(config.imageRepo, dockerfile)
kanikoImage := GetKanikoImage(config.imageRepo, dockerfile)

// container-diff
daemonDockerImage := daemonPrefix + dockerImage
Expand All @@ -296,7 +167,7 @@ func TestRun(t *testing.T) {
var diffInt interface{}
var expectedInt interface{}

err = json.Unmarshal(diff, &diffInt)
err := json.Unmarshal(diff, &diffInt)
if err != nil {
t.Error(err)
t.Fail()
Expand All @@ -314,27 +185,24 @@ func TestRun(t *testing.T) {
}

func TestLayers(t *testing.T) {
dockerfiles, err := filepath.Glob(path.Join(dockerfilesPath, "Dockerfile_test*"))
if err != nil {
t.Error(err)
t.FailNow()
}
offset := map[string]int{
"Dockerfile_test_add": 9,
"Dockerfile_test_scratch": 3,
// the Docker built image combined some of the dirs defined by separate VOLUME commands into one layer
// which is why this offset exists
"Dockerfile_test_volume": 1,
}
for _, dockerfile := range dockerfiles {
for dockerfile, built := range imageBuilder.FilesBuilt {
t.Run("test_layer_"+dockerfile, func(t *testing.T) {
dockerfile = dockerfile[len("dockerfile/")+1:]
if testsToIgnore[dockerfile] {
t.SkipNow()
if !built {
err := imageBuilder.BuildImage(config.imageRepo, config.gcsBucket, dockerfilesPath, dockerfile)
if err != nil {
t.Fatalf("Failed to build kaniko and docker images for %s: %s", dockerfile, err)
}
}
// Pull the kaniko image
dockerImage := getDockerImage(config.imageRepo, dockerfile)
kanikoImage := getKanikoImage(config.imageRepo, dockerfile)
dockerImage := GetDockerImage(config.imageRepo, dockerfile)
kanikoImage := GetKanikoImage(config.imageRepo, dockerfile)
pullCmd := exec.Command("docker", "pull", kanikoImage)
RunCommand(pullCmd, t)
if err := checkLayers(t, dockerImage, kanikoImage, offset[dockerfile]); err != nil {
Expand Down
1 change: 0 additions & 1 deletion integration/randomstring.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ import (

// r is used by randomString to generate a random string. It is seeded with the time
// at import so the strings will be different between test runs.

var r *rand.Rand

// once is used to initialize r
Expand Down

0 comments on commit ab753a6

Please sign in to comment.