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 }