github.com/GoogleContainerTools/skaffold/v2@v2.13.2/integration/build_test.go (about)

     1  /*
     2  Copyright 2019 The Skaffold Authors
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package integration
    18  
    19  import (
    20  	"context"
    21  	"os"
    22  	"os/exec"
    23  	"strings"
    24  	"testing"
    25  	"time"
    26  
    27  	"4d63.com/tz"
    28  	"github.com/google/go-cmp/cmp"
    29  	"github.com/google/go-cmp/cmp/cmpopts"
    30  	v1 "github.com/opencontainers/image-spec/specs-go/v1"
    31  
    32  	"github.com/GoogleContainerTools/skaffold/v2/cmd/skaffold/app/flags"
    33  	"github.com/GoogleContainerTools/skaffold/v2/integration/skaffold"
    34  	"github.com/GoogleContainerTools/skaffold/v2/pkg/skaffold/build/jib"
    35  	"github.com/GoogleContainerTools/skaffold/v2/pkg/skaffold/docker"
    36  	kubectx "github.com/GoogleContainerTools/skaffold/v2/pkg/skaffold/kubernetes/context"
    37  	"github.com/GoogleContainerTools/skaffold/v2/pkg/skaffold/runner/runcontext"
    38  	"github.com/GoogleContainerTools/skaffold/v2/pkg/skaffold/util"
    39  	"github.com/GoogleContainerTools/skaffold/v2/testutil"
    40  )
    41  
    42  const imageName = "us-central1-docker.pkg.dev/k8s-skaffold/testing/simple-build:"
    43  
    44  func TestBuild(t *testing.T) {
    45  	tests := []struct {
    46  		description string
    47  		dir         string
    48  		args        []string
    49  		expectImage string
    50  		setup       func(t *testing.T, workdir string)
    51  	}{
    52  		{
    53  			description: "docker build",
    54  			dir:         "testdata/build",
    55  		},
    56  		{
    57  			description: "git tagger",
    58  			dir:         "testdata/tagPolicy",
    59  			args:        []string{"-p", "gitCommit"},
    60  			setup:       setupGitRepo,
    61  			expectImage: imageName + "v1",
    62  		},
    63  		{
    64  			description: "sha256 tagger",
    65  			dir:         "testdata/tagPolicy",
    66  			args:        []string{"-p", "sha256"},
    67  			expectImage: imageName + "latest",
    68  		},
    69  		{
    70  			description: "dateTime tagger",
    71  			dir:         "testdata/tagPolicy",
    72  			args:        []string{"-p", "dateTime"},
    73  			// around midnight this test might fail, if the tests above run slowly
    74  			expectImage: imageName + nowInChicago(),
    75  		},
    76  		{
    77  			description: "envTemplate tagger",
    78  			dir:         "testdata/tagPolicy",
    79  			args:        []string{"-p", "envTemplate"},
    80  			expectImage: imageName + "tag",
    81  		},
    82  		{
    83  			description: "custom",
    84  			dir:         "examples/custom",
    85  		},
    86  		{
    87  			description: "--tag arg",
    88  			dir:         "testdata/tagPolicy",
    89  			args:        []string{"-p", "args", "-t", "foo"},
    90  			expectImage: imageName + "foo",
    91  		},
    92  		{
    93  			description: "envTemplate command tagger",
    94  			dir:         "testdata/tagPolicy",
    95  			args:        []string{"-p", "envTemplateCmd"},
    96  			expectImage: imageName + "1.0.0",
    97  		},
    98  		{
    99  			description: "envTemplate default tagger",
   100  			dir:         "testdata/tagPolicy",
   101  			args:        []string{"-p", "envTemplateDefault"},
   102  			expectImage: imageName + "bar",
   103  		},
   104  	}
   105  	for _, test := range tests {
   106  		t.Run(test.description, func(t *testing.T) {
   107  			MarkIntegrationTest(t, CanRunWithoutGcp)
   108  			if test.setup != nil {
   109  				test.setup(t, test.dir)
   110  			}
   111  
   112  			// Run without artifact caching
   113  			skaffold.Build(append(test.args, "--cache-artifacts=false")...).InDir(test.dir).RunOrFail(t)
   114  
   115  			// Run with artifact caching
   116  			skaffold.Build(append(test.args, "--cache-artifacts=true")...).InDir(test.dir).RunOrFail(t)
   117  
   118  			// Run a second time with artifact caching
   119  			out := skaffold.Build(append(test.args, "--cache-artifacts=true")...).InDir(test.dir).RunOrFailOutput(t)
   120  			if strings.Contains(string(out), "Not found. Building") {
   121  				t.Errorf("images were expected to be found in cache: %s", out)
   122  			}
   123  			checkImageExists(t, test.expectImage)
   124  		})
   125  	}
   126  }
   127  
   128  func TestBuildWithWithPlatform(t *testing.T) {
   129  	tests := []struct {
   130  		description       string
   131  		dir               string
   132  		args              []string
   133  		image             string
   134  		expectedPlatforms []v1.Platform
   135  	}{
   136  		{
   137  			description:       "docker build linux/amd64",
   138  			dir:               "testdata/build/docker-with-platform-amd",
   139  			args:              []string{"--platform", "linux/amd64"},
   140  			expectedPlatforms: []v1.Platform{{OS: "linux", Architecture: "amd64"}},
   141  		},
   142  		{
   143  			description:       "docker build linux/arm64",
   144  			dir:               "testdata/build/docker-with-platform-arm",
   145  			args:              []string{"--platform", "linux/arm64"},
   146  			expectedPlatforms: []v1.Platform{{OS: "linux", Architecture: "arm64"}},
   147  		},
   148  	}
   149  
   150  	for _, test := range tests {
   151  		t.Run(test.description, func(t *testing.T) {
   152  			MarkIntegrationTest(t, CanRunWithoutGcp)
   153  			tmpfile := testutil.TempFile(t, "", []byte{})
   154  			args := append(test.args, "--file-output", tmpfile)
   155  			skaffold.Build(args...).InDir(test.dir).RunOrFail(t)
   156  			bytes, err := os.ReadFile(tmpfile)
   157  			failNowIfError(t, err)
   158  			buildArtifacts, err := flags.ParseBuildOutput(bytes)
   159  			failNowIfError(t, err)
   160  			checkLocalImagePlatforms(t, buildArtifacts.Builds[0].Tag, test.expectedPlatforms)
   161  		})
   162  	}
   163  }
   164  
   165  func TestBuildWithMultiPlatforms(t *testing.T) {
   166  	tests := []struct {
   167  		description       string
   168  		dir               string
   169  		args              []string
   170  		image             string
   171  		expectedPlatforms []v1.Platform
   172  	}{
   173  		{
   174  			description:       "build cross platform images with gcb",
   175  			dir:               "testdata/build/gcb-with-platform",
   176  			args:              []string{"--platform", "linux/arm64,linux/amd64"},
   177  			expectedPlatforms: []v1.Platform{{OS: "linux", Architecture: "arm64"}, {OS: "linux", Architecture: "amd64"}},
   178  		},
   179  	}
   180  
   181  	for _, test := range tests {
   182  		t.Run(test.description, func(t *testing.T) {
   183  			MarkIntegrationTest(t, NeedsGcp)
   184  			tmpfile := testutil.TempFile(t, "", []byte{})
   185  			args := append(test.args, "--file-output", tmpfile)
   186  			skaffold.Build(args...).InDir(test.dir).RunOrFail(t)
   187  			bytes, err := os.ReadFile(tmpfile)
   188  			failNowIfError(t, err)
   189  			buildArtifacts, err := flags.ParseBuildOutput(bytes)
   190  			failNowIfError(t, err)
   191  			checkRemoteImagePlatforms(t, buildArtifacts.Builds[0].Tag, test.expectedPlatforms)
   192  		})
   193  	}
   194  }
   195  
   196  // TestExpectedBuildFailures verifies that `skaffold build` fails in expected ways
   197  func TestExpectedBuildFailures(t *testing.T) {
   198  	if !jib.JVMFound(context.Background()) {
   199  		t.Fatal("test requires Java VM")
   200  	}
   201  
   202  	tests := []struct {
   203  		description string
   204  		dir         string
   205  		args        []string
   206  		expected    string
   207  	}{
   208  		{
   209  			description: "jib is too old",
   210  			dir:         "testdata/jib",
   211  			args:        []string{"-p", "old-jib"},
   212  			expected:    "Could not find goal '_skaffold-fail-if-jib-out-of-date' in plugin com.google.cloud.tools:jib-maven-plugin:1.3.0",
   213  			// test string will need to be updated for the jib.requiredVersion error text when moving to Jib > 1.4.0
   214  		},
   215  	}
   216  	for _, test := range tests {
   217  		t.Run(test.description, func(t *testing.T) {
   218  			MarkIntegrationTest(t, NeedsGcp)
   219  			if out, err := skaffold.Build(test.args...).InDir(test.dir).RunWithCombinedOutput(t); err == nil {
   220  				t.Fatal("expected build to fail")
   221  			} else if !strings.Contains(string(out), test.expected) {
   222  				t.Log("build output: ", string(out))
   223  				t.Fatalf("build failed but for wrong reason")
   224  			}
   225  		})
   226  	}
   227  }
   228  
   229  func checkLocalImagePlatforms(t *testing.T, image string, expected []v1.Platform) {
   230  	if expected == nil {
   231  		return
   232  	}
   233  	t.Helper()
   234  
   235  	cfg, err := kubectx.CurrentConfig()
   236  	failNowIfError(t, err)
   237  
   238  	client, err := docker.NewAPIClient(context.Background(), &runcontext.RunContext{
   239  		KubeContext: cfg.CurrentContext,
   240  	})
   241  	failNowIfError(t, err)
   242  	inspect, _, err := client.ImageInspectWithRaw(context.Background(), image)
   243  	failNowIfError(t, err)
   244  
   245  	actual := []v1.Platform{{Architecture: inspect.Architecture, OS: inspect.Os}}
   246  	checkPlatformsEqual(t, actual, expected)
   247  }
   248  
   249  func checkRemoteImagePlatforms(t *testing.T, image string, expected []v1.Platform) {
   250  	if expected == nil {
   251  		return
   252  	}
   253  	t.Helper()
   254  	actual, err := docker.GetPlatforms(image)
   255  	if err != nil {
   256  		t.Error(err)
   257  	}
   258  	checkPlatformsEqual(t, actual, expected)
   259  }
   260  
   261  func checkPlatformsEqual(t *testing.T, actual, expected []v1.Platform) {
   262  	platLess := func(a, b v1.Platform) bool {
   263  		return a.OS < b.OS || (a.OS == b.OS && a.Architecture < b.Architecture)
   264  	}
   265  	if diff := cmp.Diff(expected, actual, cmpopts.SortSlices(platLess)); diff != "" {
   266  		t.Fatalf("Platforms differ (-got,+want):\n%s", diff)
   267  	}
   268  }
   269  
   270  // checkImageExists asserts that the given image is present
   271  func checkImageExists(t *testing.T, image string) {
   272  	t.Helper()
   273  
   274  	if image == "" {
   275  		return
   276  	}
   277  
   278  	cfg, err := kubectx.CurrentConfig()
   279  	failNowIfError(t, err)
   280  
   281  	// TODO: use the proper RunContext
   282  	client, err := docker.NewAPIClient(context.Background(), &runcontext.RunContext{
   283  		KubeContext: cfg.CurrentContext,
   284  	})
   285  	failNowIfError(t, err)
   286  
   287  	ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(5*time.Second))
   288  	defer cancel()
   289  	if !client.ImageExists(ctx, image) {
   290  		t.Errorf("expected image '%s' not present", image)
   291  	}
   292  }
   293  
   294  // setupGitRepo sets up a clean repo with tag v1
   295  func setupGitRepo(t *testing.T, dir string) {
   296  	t.Cleanup(func() { os.RemoveAll(dir + "/.git") })
   297  
   298  	gitArgs := [][]string{
   299  		{"init"},
   300  		{"config", "user.email", "john@doe.org"},
   301  		{"config", "user.name", "John Doe"},
   302  		{"add", "."},
   303  		{"commit", "-m", "Initial commit"},
   304  		{"tag", "v1"},
   305  	}
   306  
   307  	for _, args := range gitArgs {
   308  		cmd := exec.Command("git", args...)
   309  		cmd.Dir = dir
   310  		if buf, err := util.RunCmdOut(context.Background(), cmd); err != nil {
   311  			t.Logf(string(buf))
   312  			t.Fatal(err)
   313  		}
   314  	}
   315  }
   316  
   317  // nowInChicago returns the dateTime string as generated by the dateTime tagger
   318  func nowInChicago() string {
   319  	loc, _ := tz.LoadLocation("America/Chicago")
   320  	return time.Now().In(loc).Format("2006-01-02")
   321  }
   322  
   323  type Fataler interface {
   324  	Fatal(args ...interface{})
   325  	Helper()
   326  }
   327  
   328  func failNowIfError(t Fataler, err error) {
   329  	t.Helper()
   330  	if err != nil {
   331  		t.Fatal(err)
   332  	}
   333  }