github.com/GoogleContainerTools/skaffold@v1.39.18/pkg/skaffold/build/docker/docker_test.go (about)

     1  /*
     2  Copyright 2020 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 docker
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"io/ioutil"
    23  	"path/filepath"
    24  	"runtime"
    25  	"strings"
    26  	"testing"
    27  
    28  	"github.com/docker/docker/api/types"
    29  	"github.com/docker/docker/errdefs"
    30  	"google.golang.org/protobuf/testing/protocmp"
    31  
    32  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/config"
    33  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/docker"
    34  	sErrors "github.com/GoogleContainerTools/skaffold/pkg/skaffold/errors"
    35  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/platform"
    36  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/schema/latest"
    37  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/util"
    38  	"github.com/GoogleContainerTools/skaffold/proto/v1"
    39  	"github.com/GoogleContainerTools/skaffold/testutil"
    40  )
    41  
    42  func TestDockerCLIBuild(t *testing.T) {
    43  	tests := []struct {
    44  		description     string
    45  		localBuild      latest.LocalBuild
    46  		cliFlags        []string
    47  		cfg             mockConfig
    48  		extraEnv        []string
    49  		expectedEnv     []string
    50  		err             error
    51  		expectedErr     error
    52  		wantDockerCLI   bool
    53  		expectedErrCode proto.StatusCode
    54  	}{
    55  		{
    56  			description: "docker build",
    57  			localBuild:  latest.LocalBuild{},
    58  			cfg:         mockConfig{runMode: config.RunModes.Dev},
    59  			expectedEnv: []string{"KEY=VALUE"},
    60  		},
    61  		{
    62  			description: "extra env",
    63  			localBuild:  latest.LocalBuild{},
    64  			extraEnv:    []string{"OTHER=VALUE"},
    65  			expectedEnv: []string{"KEY=VALUE", "OTHER=VALUE"},
    66  		},
    67  		{
    68  			description:   "buildkit",
    69  			localBuild:    latest.LocalBuild{UseBuildkit: util.BoolPtr(true)},
    70  			wantDockerCLI: true,
    71  			expectedEnv:   []string{"KEY=VALUE", "DOCKER_BUILDKIT=1"},
    72  		},
    73  		{
    74  			description:   "cliFlags",
    75  			cliFlags:      []string{"--platform", "linux/amd64"},
    76  			localBuild:    latest.LocalBuild{},
    77  			wantDockerCLI: true,
    78  			expectedEnv:   []string{"KEY=VALUE"},
    79  		},
    80  		{
    81  			description:   "buildkit and extra env",
    82  			localBuild:    latest.LocalBuild{UseBuildkit: util.BoolPtr(true)},
    83  			wantDockerCLI: true,
    84  			extraEnv:      []string{"OTHER=VALUE"},
    85  			expectedEnv:   []string{"KEY=VALUE", "OTHER=VALUE", "DOCKER_BUILDKIT=1"},
    86  		},
    87  		{
    88  			description:   "env var collisions",
    89  			localBuild:    latest.LocalBuild{UseBuildkit: util.BoolPtr(true)},
    90  			wantDockerCLI: true,
    91  			extraEnv:      []string{"KEY=OTHER_VALUE", "DOCKER_BUILDKIT=0"},
    92  			// DOCKER_BUILDKIT should be overridden
    93  			expectedEnv: []string{"KEY=OTHER_VALUE", "DOCKER_BUILDKIT=1"},
    94  		},
    95  		{
    96  			description: "docker build internal error",
    97  			localBuild:  latest.LocalBuild{UseDockerCLI: true},
    98  			err:         errdefs.Cancelled(fmt.Errorf("cancelled")),
    99  			expectedErr: newBuildError(errdefs.Cancelled(fmt.Errorf("cancelled")), mockConfig{runMode: config.RunModes.Dev}),
   100  		},
   101  		{
   102  			description:     "docker build no space left error with prune for dev",
   103  			localBuild:      latest.LocalBuild{UseDockerCLI: true},
   104  			cfg:             mockConfig{runMode: config.RunModes.Dev, prune: false},
   105  			err:             errdefs.System(fmt.Errorf("no space left")),
   106  			expectedErr:     fmt.Errorf("Docker ran out of memory. Please run 'docker system prune' to removed unused docker data or Run skaffold dev with --cleanup=true to clean up images built by skaffold"),
   107  			expectedErrCode: proto.StatusCode_BUILD_DOCKER_NO_SPACE_ERR,
   108  		},
   109  		{
   110  			description:     "docker build no space left error with prune for build",
   111  			localBuild:      latest.LocalBuild{UseDockerCLI: true},
   112  			cfg:             mockConfig{runMode: config.RunModes.Build, prune: false},
   113  			err:             errdefs.System(fmt.Errorf("no space left")),
   114  			expectedErr:     fmt.Errorf("no space left. Docker ran out of memory. Please run 'docker system prune' to removed unused docker data"),
   115  			expectedErrCode: proto.StatusCode_BUILD_DOCKER_NO_SPACE_ERR,
   116  		},
   117  		{
   118  			description:     "docker build no space left error with prune true",
   119  			localBuild:      latest.LocalBuild{UseDockerCLI: true},
   120  			cfg:             mockConfig{prune: true},
   121  			err:             errdefs.System(fmt.Errorf("no space left")),
   122  			expectedErr:     fmt.Errorf("no space left. Docker ran out of memory. Please run 'docker system prune' to removed unused docker data"),
   123  			expectedErrCode: proto.StatusCode_BUILD_DOCKER_NO_SPACE_ERR,
   124  		},
   125  		{
   126  			description:     "docker build system error",
   127  			localBuild:      latest.LocalBuild{UseDockerCLI: true},
   128  			err:             errdefs.System(fmt.Errorf("something else")),
   129  			expectedErr:     fmt.Errorf("something else"),
   130  			expectedErrCode: proto.StatusCode_BUILD_DOCKER_SYSTEM_ERR,
   131  		},
   132  	}
   133  
   134  	for _, test := range tests {
   135  		testutil.Run(t, test.description, func(t *testutil.T) {
   136  			t.NewTempDir().Touch("Dockerfile").Chdir()
   137  			dockerfilePath, _ := filepath.Abs("Dockerfile")
   138  			t.Override(&docker.EvalBuildArgs, func(_ config.RunMode, _ string, _ string, args map[string]*string, _ map[string]*string) (map[string]*string, error) {
   139  				return args, nil
   140  			})
   141  			t.Override(&docker.DefaultAuthHelper, stubAuth{})
   142  			var mockCmd *testutil.FakeCmd
   143  
   144  			if test.err != nil {
   145  				var pruneFlag string
   146  				if test.cfg.Prune() {
   147  					pruneFlag = " --force-rm"
   148  				}
   149  				mockCmd = testutil.CmdRunErr(
   150  					"docker build . --file "+dockerfilePath+" -t tag"+pruneFlag,
   151  					test.err,
   152  				)
   153  				t.Override(&util.DefaultExecCommand, mockCmd)
   154  			}
   155  			if test.wantDockerCLI {
   156  				cmdLine := "docker build . --file " + dockerfilePath + " -t tag"
   157  				if len(test.cliFlags) > 0 {
   158  					cmdLine += " " + strings.Join(test.cliFlags, " ")
   159  				}
   160  				mockCmd = testutil.CmdRunEnv(cmdLine, test.expectedEnv)
   161  				t.Override(&util.DefaultExecCommand, mockCmd)
   162  			}
   163  			t.Override(&util.OSEnviron, func() []string { return []string{"KEY=VALUE"} })
   164  
   165  			builder := NewArtifactBuilder(fakeLocalDaemonWithExtraEnv(test.extraEnv), test.cfg, test.localBuild.UseDockerCLI, test.localBuild.UseBuildkit, false, mockArtifactResolver{make(map[string]string)}, nil)
   166  
   167  			artifact := &latest.Artifact{
   168  				Workspace: ".",
   169  				ArtifactType: latest.ArtifactType{
   170  					DockerArtifact: &latest.DockerArtifact{
   171  						DockerfilePath: "Dockerfile",
   172  						CliFlags:       test.cliFlags,
   173  					},
   174  				},
   175  			}
   176  
   177  			_, err := builder.Build(context.Background(), ioutil.Discard, artifact, "tag", platform.Matcher{})
   178  			t.CheckError(test.err != nil, err)
   179  			if mockCmd != nil {
   180  				t.CheckDeepEqual(1, mockCmd.TimesCalled())
   181  			}
   182  			if test.err != nil && test.expectedErrCode != 0 {
   183  				if ae, ok := err.(*sErrors.ErrDef); ok {
   184  					t.CheckDeepEqual(test.expectedErrCode, ae.StatusCode(), protocmp.Transform())
   185  					t.CheckErrorContains(test.expectedErr.Error(), ae)
   186  				} else {
   187  					t.Fatalf("expected to find an actionable error. not found")
   188  				}
   189  			}
   190  		})
   191  	}
   192  }
   193  
   194  func TestDockerCLICheckCacheFromArgs(t *testing.T) {
   195  	tests := []struct {
   196  		description       string
   197  		artifact          *latest.Artifact
   198  		tag               string
   199  		expectedCacheFrom []string
   200  	}{
   201  		{
   202  			description: "multiple cache-from images",
   203  			artifact: &latest.Artifact{
   204  				ImageName: "gcr.io/k8s-skaffold/test",
   205  				ArtifactType: latest.ArtifactType{
   206  					DockerArtifact: &latest.DockerArtifact{
   207  						CacheFrom: []string{"from/image1", "from/image2"},
   208  					},
   209  				},
   210  			},
   211  			tag:               "tag",
   212  			expectedCacheFrom: []string{"from/image1", "from/image2"},
   213  		},
   214  		{
   215  			description: "cache-from self uses tagged image",
   216  			artifact: &latest.Artifact{
   217  				ImageName: "gcr.io/k8s-skaffold/test",
   218  				ArtifactType: latest.ArtifactType{
   219  					DockerArtifact: &latest.DockerArtifact{
   220  						CacheFrom: []string{"gcr.io/k8s-skaffold/test"},
   221  					},
   222  				},
   223  			},
   224  			tag:               "gcr.io/k8s-skaffold/test:tagged",
   225  			expectedCacheFrom: []string{"gcr.io/k8s-skaffold/test:tagged"},
   226  		},
   227  	}
   228  
   229  	for _, test := range tests {
   230  		testutil.Run(t, test.description, func(t *testutil.T) {
   231  			if runtime.GOOS == "windows" {
   232  				t.Skip("not working on windows")
   233  			}
   234  			t.NewTempDir().Touch("Dockerfile").Chdir()
   235  			dockerfilePath, _ := filepath.Abs("Dockerfile")
   236  			a := *test.artifact
   237  			a.Workspace = "."
   238  			a.DockerArtifact.DockerfilePath = dockerfilePath
   239  			t.Override(&docker.DefaultAuthHelper, stubAuth{})
   240  			t.Override(&docker.EvalBuildArgs, func(_ config.RunMode, _ string, _ string, args map[string]*string, _ map[string]*string) (map[string]*string, error) {
   241  				return args, nil
   242  			})
   243  
   244  			mockCmd := testutil.CmdRun(
   245  				"docker build . --file " + dockerfilePath + " -t " + test.tag + " --cache-from " + strings.Join(test.expectedCacheFrom, " --cache-from "),
   246  			)
   247  			t.Override(&util.DefaultExecCommand, mockCmd)
   248  
   249  			builder := NewArtifactBuilder(fakeLocalDaemonWithExtraEnv([]string{}), mockConfig{}, true, util.BoolPtr(false), false, mockArtifactResolver{make(map[string]string)}, nil)
   250  			_, err := builder.Build(context.Background(), ioutil.Discard, &a, test.tag, platform.Matcher{})
   251  			t.CheckNoError(err)
   252  		})
   253  	}
   254  }
   255  
   256  func fakeLocalDaemonWithExtraEnv(extraEnv []string) docker.LocalDaemon {
   257  	return docker.NewLocalDaemon(&testutil.FakeAPIClient{}, extraEnv, false, nil)
   258  }
   259  
   260  type mockArtifactResolver struct {
   261  	m map[string]string
   262  }
   263  
   264  func (r mockArtifactResolver) GetImageTag(imageName string) (string, bool) {
   265  	if r.m == nil {
   266  		return "", false
   267  	}
   268  	val, found := r.m[imageName]
   269  	return val, found
   270  }
   271  
   272  type stubAuth struct{}
   273  
   274  func (t stubAuth) GetAuthConfig(string) (types.AuthConfig, error) {
   275  	return types.AuthConfig{}, nil
   276  }
   277  func (t stubAuth) GetAllAuthConfigs(context.Context) (map[string]types.AuthConfig, error) {
   278  	return nil, nil
   279  }
   280  
   281  type mockConfig struct {
   282  	runMode config.RunMode
   283  	prune   bool
   284  }
   285  
   286  func (m mockConfig) GetKubeContext() string {
   287  	return ""
   288  }
   289  
   290  func (m mockConfig) GlobalConfig() string {
   291  	return ""
   292  }
   293  
   294  func (m mockConfig) MinikubeProfile() string {
   295  	return ""
   296  }
   297  
   298  func (m mockConfig) GetInsecureRegistries() map[string]bool {
   299  	return map[string]bool{}
   300  }
   301  
   302  func (m mockConfig) Mode() config.RunMode {
   303  	return m.runMode
   304  }
   305  
   306  func (m mockConfig) Prune() bool {
   307  	return m.prune
   308  }
   309  
   310  func (m mockConfig) ContainerDebugging() bool {
   311  	return false
   312  }