github.com/GoogleContainerTools/skaffold@v1.39.18/pkg/skaffold/docker/image_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 docker 18 19 import ( 20 "context" 21 "io/ioutil" 22 "sync" 23 "sync/atomic" 24 "testing" 25 26 "github.com/docker/docker/api/types" 27 "github.com/docker/docker/client" 28 29 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/config" 30 sErrors "github.com/GoogleContainerTools/skaffold/pkg/skaffold/errors" 31 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/schema/latest" 32 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/util" 33 "github.com/GoogleContainerTools/skaffold/proto/v1" 34 "github.com/GoogleContainerTools/skaffold/testutil" 35 ) 36 37 func TestPush(t *testing.T) { 38 tests := []struct { 39 description string 40 imageName string 41 api *testutil.FakeAPIClient 42 expectedDigest string 43 shouldErr bool 44 }{ 45 { 46 description: "push", 47 imageName: "gcr.io/scratchman", 48 api: (&testutil.FakeAPIClient{}).Add("gcr.io/scratchman", "sha256:imageIDabcab"), 49 expectedDigest: "sha256:bb1f952848763dd1f8fcf14231d7a4557775abf3c95e588561bc7a478c94e7e0", 50 }, 51 { 52 description: "stream error", 53 imageName: "gcr.io/imthescratchman", 54 api: &testutil.FakeAPIClient{ 55 ErrStream: true, 56 }, 57 shouldErr: true, 58 }, 59 { 60 description: "image push error", 61 imageName: "gcr.io/skibabopbadopbop", 62 api: &testutil.FakeAPIClient{ 63 ErrImagePush: true, 64 }, 65 shouldErr: true, 66 }, 67 } 68 for _, test := range tests { 69 testutil.Run(t, test.description, func(t *testutil.T) { 70 t.Override(&DefaultAuthHelper, testAuthHelper{}) 71 72 localDocker := NewLocalDaemon(test.api, nil, false, nil) 73 digest, err := localDocker.Push(context.Background(), ioutil.Discard, test.imageName) 74 75 t.CheckErrorAndDeepEqual(test.shouldErr, err, test.expectedDigest, digest) 76 }) 77 } 78 } 79 80 func TestDoNotPushAlreadyPushed(t *testing.T) { 81 testutil.Run(t, "", func(t *testutil.T) { 82 t.Override(&DefaultAuthHelper, testAuthHelper{}) 83 84 api := &testutil.FakeAPIClient{} 85 api.Add("image", "sha256:imageIDabcab") 86 localDocker := NewLocalDaemon(api, nil, false, nil) 87 88 digest, err := localDocker.Push(context.Background(), ioutil.Discard, "image") 89 t.CheckNoError(err) 90 t.CheckDeepEqual("sha256:bb1f952848763dd1f8fcf14231d7a4557775abf3c95e588561bc7a478c94e7e0", digest) 91 92 // Images already pushed don't need being pushed. 93 api.ErrImagePush = true 94 95 digest, err = localDocker.Push(context.Background(), ioutil.Discard, "image") 96 t.CheckNoError(err) 97 t.CheckDeepEqual("sha256:bb1f952848763dd1f8fcf14231d7a4557775abf3c95e588561bc7a478c94e7e0", digest) 98 }) 99 } 100 101 func TestBuild(t *testing.T) { 102 tests := []struct { 103 description string 104 env map[string]string 105 api *testutil.FakeAPIClient 106 workspace string 107 artifact *latest.DockerArtifact 108 expected types.ImageBuildOptions 109 mode config.RunMode 110 shouldErr bool 111 expectedError string 112 }{ 113 { 114 description: "build", 115 api: &testutil.FakeAPIClient{}, 116 workspace: ".", 117 artifact: &latest.DockerArtifact{}, 118 expected: types.ImageBuildOptions{ 119 Tags: []string{"finalimage"}, 120 AuthConfigs: allAuthConfig, 121 }, 122 mode: config.RunModes.Dev, 123 }, 124 { 125 description: "build with options", 126 api: &testutil.FakeAPIClient{}, 127 env: map[string]string{ 128 "VALUE3": "value3", 129 }, 130 workspace: ".", 131 artifact: &latest.DockerArtifact{ 132 DockerfilePath: "Dockerfile", 133 BuildArgs: map[string]*string{ 134 "k1": nil, 135 "k2": util.StringPtr("value2"), 136 "k3": util.StringPtr("{{.VALUE3}}"), 137 }, 138 CacheFrom: []string{"from-1"}, 139 Target: "target", 140 NetworkMode: "None", 141 NoCache: true, 142 PullParent: true, 143 }, 144 mode: config.RunModes.Dev, 145 expected: types.ImageBuildOptions{ 146 Tags: []string{"finalimage"}, 147 Dockerfile: "Dockerfile", 148 BuildArgs: map[string]*string{ 149 "k1": nil, 150 "k2": util.StringPtr("value2"), 151 "k3": util.StringPtr("value3"), 152 }, 153 CacheFrom: []string{"from-1"}, 154 AuthConfigs: allAuthConfig, 155 Target: "target", 156 NetworkMode: "none", 157 NoCache: true, 158 PullParent: true, 159 }, 160 }, 161 { 162 description: "bad image build", 163 api: &testutil.FakeAPIClient{ 164 ErrImageBuild: true, 165 }, 166 mode: config.RunModes.Dev, 167 workspace: ".", 168 artifact: &latest.DockerArtifact{}, 169 shouldErr: true, 170 expectedError: "docker build", 171 }, 172 { 173 description: "bad return reader", 174 api: &testutil.FakeAPIClient{ 175 ErrStream: true, 176 }, 177 workspace: ".", 178 mode: config.RunModes.Dev, 179 artifact: &latest.DockerArtifact{}, 180 shouldErr: true, 181 expectedError: "unable to stream build output", 182 }, 183 { 184 description: "bad build arg template", 185 artifact: &latest.DockerArtifact{ 186 BuildArgs: map[string]*string{ 187 "key": util.StringPtr("{{INVALID"), 188 }, 189 }, 190 mode: config.RunModes.Dev, 191 shouldErr: true, 192 expectedError: `function "INVALID" not defined`, 193 }, 194 } 195 for _, test := range tests { 196 testutil.Run(t, test.description, func(t *testutil.T) { 197 t.Override(&DefaultAuthHelper, testAuthHelper{}) 198 t.Override(&EvalBuildArgs, func(_ config.RunMode, _ string, _ string, args map[string]*string, _ map[string]*string) (map[string]*string, error) { 199 return util.EvaluateEnvTemplateMap(args) 200 }) 201 t.SetEnvs(test.env) 202 203 localDocker := NewLocalDaemon(test.api, nil, false, nil) 204 opts := BuildOptions{Tag: "finalimage", Mode: test.mode} 205 _, err := localDocker.Build(context.Background(), ioutil.Discard, test.workspace, "final-image", test.artifact, opts) 206 207 if test.shouldErr { 208 t.CheckErrorContains(test.expectedError, err) 209 } else { 210 t.CheckNoError(err) 211 t.CheckDeepEqual(test.api.Built[0], test.expected) 212 } 213 }) 214 } 215 } 216 217 func TestImageID(t *testing.T) { 218 tests := []struct { 219 description string 220 ref string 221 api *testutil.FakeAPIClient 222 expected string 223 shouldErr bool 224 }{ 225 { 226 description: "find by tag", 227 ref: "identifier:latest", 228 api: (&testutil.FakeAPIClient{}).Add("identifier:latest", "sha256:123abc"), 229 expected: "sha256:123abc", 230 }, 231 { 232 description: "find by imageID", 233 ref: "sha256:123abc", 234 api: (&testutil.FakeAPIClient{}).Add("identifier:latest", "sha256:123abc"), 235 expected: "sha256:123abc", 236 }, 237 { 238 description: "image inspect error", 239 ref: "test", 240 api: &testutil.FakeAPIClient{ 241 ErrImageInspect: true, 242 }, 243 shouldErr: true, 244 }, 245 { 246 description: "not found", 247 ref: "somethingelse", 248 api: &testutil.FakeAPIClient{}, 249 expected: "", 250 }, 251 } 252 for _, test := range tests { 253 testutil.Run(t, test.description, func(t *testutil.T) { 254 localDocker := NewLocalDaemon(test.api, nil, false, nil) 255 256 imageID, err := localDocker.ImageID(context.Background(), test.ref) 257 258 t.CheckErrorAndDeepEqual(test.shouldErr, err, test.expected, imageID) 259 if test.shouldErr { 260 if e, ok := err.(sErrors.Error); ok { 261 t.CheckDeepEqual(e.StatusCode(), proto.StatusCode_BUILD_DOCKER_GET_DIGEST_ERR) 262 } else { 263 t.Error("expected to be of type actionable err not found") 264 } 265 } 266 }) 267 } 268 } 269 270 func TestGetBuildArgs(t *testing.T) { 271 tests := []struct { 272 description string 273 artifact *latest.DockerArtifact 274 env []string 275 want []string 276 shouldErr bool 277 }{ 278 { 279 description: "build args", 280 artifact: &latest.DockerArtifact{ 281 BuildArgs: map[string]*string{ 282 "key1": util.StringPtr("value1"), 283 "key2": nil, 284 "key3": util.StringPtr("{{.FOO}}"), 285 }, 286 }, 287 env: []string{"FOO=bar"}, 288 want: []string{"--build-arg", "key1=value1", "--build-arg", "key2", "--build-arg", "key3=bar"}, 289 }, 290 { 291 description: "invalid build arg", 292 artifact: &latest.DockerArtifact{ 293 BuildArgs: map[string]*string{ 294 "key": util.StringPtr("{{INVALID"), 295 }, 296 }, 297 shouldErr: true, 298 }, 299 { 300 description: "add host", 301 artifact: &latest.DockerArtifact{ 302 AddHost: []string{"1.gcr.io:127.0.0.1", "2.gcr.io:127.0.0.1"}, 303 }, 304 want: []string{"--add-host", "1.gcr.io:127.0.0.1", "--add-host", "2.gcr.io:127.0.0.1"}, 305 }, 306 { 307 description: "cache from", 308 artifact: &latest.DockerArtifact{ 309 CacheFrom: []string{"gcr.io/foo/bar", "baz:latest"}, 310 }, 311 want: []string{"--cache-from", "gcr.io/foo/bar", "--cache-from", "baz:latest"}, 312 }, 313 { 314 description: "additional CLI flags", 315 artifact: &latest.DockerArtifact{ 316 CliFlags: []string{"--foo", "--bar"}, 317 }, 318 want: []string{"--foo", "--bar"}, 319 }, 320 { 321 description: "target", 322 artifact: &latest.DockerArtifact{ 323 Target: "stage1", 324 }, 325 want: []string{"--target", "stage1"}, 326 }, 327 { 328 description: "network mode", 329 artifact: &latest.DockerArtifact{ 330 NetworkMode: "Bridge", 331 }, 332 want: []string{"--network", "bridge"}, 333 }, 334 { 335 description: "no-cache", 336 artifact: &latest.DockerArtifact{ 337 NoCache: true, 338 }, 339 want: []string{"--no-cache"}, 340 }, 341 { 342 description: "pullParent", 343 artifact: &latest.DockerArtifact{ 344 PullParent: true, 345 }, 346 want: []string{"--pull"}, 347 }, 348 { 349 description: "squash", 350 artifact: &latest.DockerArtifact{ 351 Squash: true, 352 }, 353 want: []string{"--squash"}, 354 }, 355 { 356 description: "secret with no source", 357 artifact: &latest.DockerArtifact{ 358 Secrets: []*latest.DockerSecret{ 359 {ID: "mysecret"}, 360 }, 361 }, 362 want: []string{"--secret", "id=mysecret"}, 363 }, 364 { 365 description: "secret with file source", 366 artifact: &latest.DockerArtifact{ 367 Secrets: []*latest.DockerSecret{ 368 {ID: "mysecret", Source: "foo.src"}, 369 }, 370 }, 371 want: []string{"--secret", "id=mysecret,src=foo.src"}, 372 }, 373 { 374 description: "secret with env source", 375 artifact: &latest.DockerArtifact{ 376 Secrets: []*latest.DockerSecret{ 377 {ID: "mysecret", Env: "FOO"}, 378 }, 379 }, 380 want: []string{"--secret", "id=mysecret,env=FOO"}, 381 }, 382 { 383 description: "multiple secrets", 384 artifact: &latest.DockerArtifact{ 385 Secrets: []*latest.DockerSecret{ 386 {ID: "mysecret", Source: "foo.src"}, 387 {ID: "anothersecret", Source: "bar.src"}, 388 }, 389 }, 390 want: []string{"--secret", "id=mysecret,src=foo.src", "--secret", "id=anothersecret,src=bar.src"}, 391 }, 392 { 393 description: "ssh with no source", 394 artifact: &latest.DockerArtifact{ 395 SSH: "default", 396 }, 397 want: []string{"--ssh", "default"}, 398 }, 399 { 400 description: "all", 401 artifact: &latest.DockerArtifact{ 402 BuildArgs: map[string]*string{ 403 "key1": util.StringPtr("value1"), 404 }, 405 CacheFrom: []string{"foo"}, 406 Target: "stage1", 407 NetworkMode: "None", 408 CliFlags: []string{"--foo", "--bar"}, 409 PullParent: true, 410 }, 411 want: []string{"--build-arg", "key1=value1", "--cache-from", "foo", "--foo", "--bar", "--target", "stage1", "--network", "none", "--pull"}, 412 }, 413 } 414 for _, test := range tests { 415 testutil.Run(t, test.description, func(t *testutil.T) { 416 t.Override(&util.OSEnviron, func() []string { return test.env }) 417 args, err := util.EvaluateEnvTemplateMap(test.artifact.BuildArgs) 418 t.CheckError(test.shouldErr, err) 419 if test.shouldErr { 420 return 421 } 422 423 result, err := ToCLIBuildArgs(test.artifact, args) 424 425 t.CheckError(test.shouldErr, err) 426 if !test.shouldErr { 427 t.CheckDeepEqual(test.want, result) 428 } 429 }) 430 } 431 } 432 433 func TestImageExists(t *testing.T) { 434 tests := []struct { 435 description string 436 api *testutil.FakeAPIClient 437 image string 438 expected bool 439 }{ 440 { 441 description: "image exists", 442 image: "image:tag", 443 api: (&testutil.FakeAPIClient{}).Add("image:tag", "imageID"), 444 expected: true, 445 }, { 446 description: "image does not exist", 447 image: "dne", 448 api: (&testutil.FakeAPIClient{ 449 ErrImageInspect: true, 450 }).Add("image:tag", "imageID"), 451 }, { 452 description: "error getting image", 453 api: (&testutil.FakeAPIClient{ 454 ErrImageInspect: true, 455 }).Add("image:tag", "imageID"), 456 }, 457 } 458 for _, test := range tests { 459 testutil.Run(t, test.description, func(t *testutil.T) { 460 localDocker := NewLocalDaemon(test.api, nil, false, nil) 461 462 actual := localDocker.ImageExists(context.Background(), test.image) 463 464 t.CheckDeepEqual(test.expected, actual) 465 }) 466 } 467 } 468 469 func TestConfigFile(t *testing.T) { 470 api := (&testutil.FakeAPIClient{}).Add("gcr.io/image", "sha256:imageIDabcab") 471 472 localDocker := NewLocalDaemon(api, nil, false, nil) 473 cfg, err := localDocker.ConfigFile(context.Background(), "gcr.io/image") 474 475 testutil.CheckErrorAndDeepEqual(t, false, err, "sha256:imageIDabcab", cfg.Config.Image) 476 } 477 478 type APICallsCounter struct { 479 client.CommonAPIClient 480 calls int32 481 } 482 483 func (c *APICallsCounter) ImageInspectWithRaw(ctx context.Context, image string) (types.ImageInspect, []byte, error) { 484 atomic.AddInt32(&c.calls, 1) 485 return c.CommonAPIClient.ImageInspectWithRaw(ctx, image) 486 } 487 488 func TestConfigFileConcurrentCalls(t *testing.T) { 489 api := &APICallsCounter{ 490 CommonAPIClient: (&testutil.FakeAPIClient{}).Add("gcr.io/image", "sha256:imageIDabcab"), 491 } 492 493 localDocker := NewLocalDaemon(api, nil, false, nil) 494 495 var wg sync.WaitGroup 496 for i := 0; i < 100; i++ { 497 wg.Add(1) 498 go func() { 499 localDocker.ConfigFile(context.Background(), "gcr.io/image") 500 wg.Done() 501 }() 502 } 503 wg.Wait() 504 505 // Check that the APIClient was called only once 506 testutil.CheckDeepEqual(t, int32(1), atomic.LoadInt32(&api.calls)) 507 } 508 509 func TestTagWithImageID(t *testing.T) { 510 tests := []struct { 511 description string 512 imageName string 513 imageID string 514 expected string 515 shouldErr bool 516 }{ 517 { 518 description: "success", 519 imageName: "ref", 520 imageID: "sha256:imageID", 521 expected: "ref:imageID", 522 }, 523 { 524 description: "ignore tag", 525 imageName: "ref:tag", 526 imageID: "sha256:imageID", 527 expected: "ref:imageID", 528 }, 529 { 530 description: "not found", 531 imageName: "ref", 532 imageID: "sha256:unknownImageID", 533 shouldErr: true, 534 }, 535 { 536 description: "invalid", 537 imageName: "!!invalid!!", 538 shouldErr: true, 539 }, 540 { 541 description: "empty image id", 542 imageName: "ref", 543 }, 544 } 545 for _, test := range tests { 546 testutil.Run(t, test.description, func(t *testutil.T) { 547 api := (&testutil.FakeAPIClient{}).Add("sha256:imageID", "sha256:imageID") 548 549 localDocker := NewLocalDaemon(api, nil, false, nil) 550 tag, err := localDocker.TagWithImageID(context.Background(), test.imageName, test.imageID) 551 552 t.CheckError(test.shouldErr, err) 553 t.CheckDeepEqual(test.expected, tag) 554 }) 555 } 556 }