github.com/GoogleContainerTools/skaffold@v1.39.18/pkg/skaffold/build/cache/hash_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 cache 18 19 import ( 20 "context" 21 "os" 22 "testing" 23 24 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/config" 25 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/graph" 26 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/platform" 27 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/schema/latest" 28 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/util" 29 "github.com/GoogleContainerTools/skaffold/testutil" 30 ) 31 32 func stubDependencyLister(dependencies []string) DependencyLister { 33 return func(context.Context, *latest.Artifact) ([]string, error) { 34 return dependencies, nil 35 } 36 } 37 38 var mockCacheHasher = func(s string) (string, error) { 39 if s == "not-found" { 40 return "", os.ErrNotExist 41 } 42 return s, nil 43 } 44 45 var fakeArtifactConfig = func(a *latest.Artifact) (string, error) { 46 if a.ArtifactType.DockerArtifact != nil { 47 return "docker/target=" + a.ArtifactType.DockerArtifact.Target, nil 48 } 49 return "", nil 50 } 51 52 const Dockerfile = "Dockerfile" 53 54 func TestGetHashForArtifact(t *testing.T) { 55 tests := []struct { 56 description string 57 dependencies []string 58 artifact *latest.Artifact 59 mode config.RunMode 60 platforms platform.Resolver 61 expected string 62 }{ 63 { 64 description: "hash for artifact", 65 dependencies: []string{"a", "b"}, 66 artifact: &latest.Artifact{}, 67 mode: config.RunModes.Dev, 68 expected: "d99ab295a682897269b4db0fe7c136ea1ecd542150fa224ee03155b4e3e995d9", 69 }, 70 { 71 description: "ignore file not found", 72 dependencies: []string{"a", "b", "not-found"}, 73 artifact: &latest.Artifact{}, 74 mode: config.RunModes.Dev, 75 expected: "d99ab295a682897269b4db0fe7c136ea1ecd542150fa224ee03155b4e3e995d9", 76 }, 77 { 78 description: "dependencies in different orders", 79 dependencies: []string{"b", "a"}, 80 artifact: &latest.Artifact{}, 81 mode: config.RunModes.Dev, 82 expected: "d99ab295a682897269b4db0fe7c136ea1ecd542150fa224ee03155b4e3e995d9", 83 }, 84 { 85 description: "no dependencies", 86 artifact: &latest.Artifact{}, 87 mode: config.RunModes.Dev, 88 expected: "7c077ca2308714493d07163e1033c4282bd869ff6d477b3e77408587f95e2930", 89 }, 90 { 91 description: "docker target", 92 artifact: &latest.Artifact{ 93 ArtifactType: latest.ArtifactType{ 94 DockerArtifact: &latest.DockerArtifact{ 95 Target: "target", 96 }, 97 }, 98 }, 99 mode: config.RunModes.Dev, 100 expected: "f947b5aad32734914aa2dea0ec95bceff257037e6c2a529007183c3f21547eae", 101 }, 102 { 103 description: "different docker target", 104 artifact: &latest.Artifact{ 105 ArtifactType: latest.ArtifactType{ 106 DockerArtifact: &latest.DockerArtifact{ 107 Target: "other", 108 }, 109 }, 110 }, 111 mode: config.RunModes.Dev, 112 expected: "09b366c764d0e39f942283cc081d5522b9dde52e725376661808054e3ed0177f", 113 }, 114 { 115 description: "build args", 116 dependencies: []string{"a", "b"}, 117 artifact: &latest.Artifact{ 118 ArtifactType: latest.ArtifactType{ 119 DockerArtifact: &latest.DockerArtifact{ 120 BuildArgs: map[string]*string{ 121 "key": util.StringPtr("value"), 122 }, 123 }, 124 }, 125 }, 126 mode: config.RunModes.Dev, 127 expected: "f3f710a4ec1d1bfb2a9b8ef2b4b7cc5f254102d17095a71872821b396953a4ce", 128 }, 129 { 130 description: "buildpack in dev mode", 131 dependencies: []string{"a", "b"}, 132 artifact: &latest.Artifact{ 133 ArtifactType: latest.ArtifactType{ 134 BuildpackArtifact: &latest.BuildpackArtifact{}, 135 }, 136 }, 137 mode: config.RunModes.Dev, 138 expected: "d99ab295a682897269b4db0fe7c136ea1ecd542150fa224ee03155b4e3e995d9", 139 }, 140 { 141 description: "buildpack in debug mode", 142 dependencies: []string{"a", "b"}, 143 artifact: &latest.Artifact{ 144 ArtifactType: latest.ArtifactType{ 145 BuildpackArtifact: &latest.BuildpackArtifact{}, 146 }, 147 }, 148 mode: config.RunModes.Debug, 149 expected: "c3a878f799b2a6532db71683a09771af4f9d20ef5884c57642a272934e5c93ea", 150 }, 151 } 152 for _, test := range tests { 153 testutil.Run(t, test.description, func(t *testutil.T) { 154 t.Override(&fileHasherFunc, mockCacheHasher) 155 t.Override(&artifactConfigFunc, fakeArtifactConfig) 156 if test.artifact.DockerArtifact != nil { 157 tmpDir := t.NewTempDir() 158 tmpDir.Write("./Dockerfile", "ARG SKAFFOLD_GO_GCFLAGS\nFROM foo") 159 test.artifact.Workspace = tmpDir.Path(".") 160 test.artifact.DockerArtifact.DockerfilePath = Dockerfile 161 } 162 163 depLister := stubDependencyLister(test.dependencies) 164 actual, err := newArtifactHasher(nil, depLister, test.mode).hash(context.Background(), test.artifact, test.platforms) 165 166 t.CheckNoError(err) 167 t.CheckDeepEqual(test.expected, actual) 168 }) 169 } 170 } 171 172 func TestGetHashForArtifactWithDependencies(t *testing.T) { 173 tests := []struct { 174 description string 175 artifacts []*latest.Artifact 176 fileDeps map[string][]string // keyed on artifact ImageName, returns a list of mock file dependencies. 177 mode config.RunMode 178 expected string 179 }{ 180 { 181 description: "hash for artifact with two dependencies", 182 artifacts: []*latest.Artifact{ 183 {ImageName: "img1", Dependencies: []*latest.ArtifactDependency{{ImageName: "img2"}, {ImageName: "img3"}}}, 184 {ImageName: "img2", Dependencies: []*latest.ArtifactDependency{{ImageName: "img4"}}, ArtifactType: latest.ArtifactType{DockerArtifact: &latest.DockerArtifact{Target: "target2"}}}, 185 {ImageName: "img3", ArtifactType: latest.ArtifactType{DockerArtifact: &latest.DockerArtifact{Target: "target3"}}}, 186 {ImageName: "img4", ArtifactType: latest.ArtifactType{DockerArtifact: &latest.DockerArtifact{Target: "target4"}}}, 187 }, 188 fileDeps: map[string][]string{"img1": {"a"}, "img2": {"b"}, "img3": {"c"}, "img4": {"d"}}, 189 mode: config.RunModes.Dev, 190 expected: "ccd159a9a50853f89ab6784530b58d658a0b349c92828eba335f1074f9a63bb3", 191 }, 192 { 193 description: "hash for artifact with two dependencies in different order", 194 artifacts: []*latest.Artifact{ 195 {ImageName: "img1", Dependencies: []*latest.ArtifactDependency{{ImageName: "img3"}, {ImageName: "img2"}}}, 196 {ImageName: "img2", Dependencies: []*latest.ArtifactDependency{{ImageName: "img4"}}, ArtifactType: latest.ArtifactType{DockerArtifact: &latest.DockerArtifact{Target: "target2"}}}, 197 {ImageName: "img3", ArtifactType: latest.ArtifactType{DockerArtifact: &latest.DockerArtifact{Target: "target3"}}}, 198 {ImageName: "img4", ArtifactType: latest.ArtifactType{DockerArtifact: &latest.DockerArtifact{Target: "target4"}}}, 199 }, 200 fileDeps: map[string][]string{"img1": {"a"}, "img2": {"b"}, "img3": {"c"}, "img4": {"d"}}, 201 mode: config.RunModes.Dev, 202 expected: "ccd159a9a50853f89ab6784530b58d658a0b349c92828eba335f1074f9a63bb3", 203 }, 204 { 205 description: "hash for artifact with different dependencies (img4 builder changed)", 206 artifacts: []*latest.Artifact{ 207 {ImageName: "img1", Dependencies: []*latest.ArtifactDependency{{ImageName: "img2"}, {ImageName: "img3"}}}, 208 {ImageName: "img2", Dependencies: []*latest.ArtifactDependency{{ImageName: "img4"}}, ArtifactType: latest.ArtifactType{DockerArtifact: &latest.DockerArtifact{Target: "target2"}}}, 209 {ImageName: "img3", ArtifactType: latest.ArtifactType{DockerArtifact: &latest.DockerArtifact{Target: "target3"}}}, 210 {ImageName: "img4", ArtifactType: latest.ArtifactType{BuildpackArtifact: &latest.BuildpackArtifact{Builder: "builder"}}}, 211 }, 212 fileDeps: map[string][]string{"img1": {"a"}, "img2": {"b"}, "img3": {"c"}, "img4": {"d"}}, 213 mode: config.RunModes.Dev, 214 expected: "26defaa1291289f40b756b83824f0549a3a9c03cca5471bd268f0ac6e499aba6", 215 }, 216 { 217 description: "hash for artifact with different dependencies (img4 files changed)", 218 artifacts: []*latest.Artifact{ 219 {ImageName: "img1", Dependencies: []*latest.ArtifactDependency{{ImageName: "img2"}, {ImageName: "img3"}}}, 220 {ImageName: "img2", Dependencies: []*latest.ArtifactDependency{{ImageName: "img4"}}, ArtifactType: latest.ArtifactType{DockerArtifact: &latest.DockerArtifact{Target: "target2"}}}, 221 {ImageName: "img3", ArtifactType: latest.ArtifactType{DockerArtifact: &latest.DockerArtifact{Target: "target3"}}}, 222 {ImageName: "img4", ArtifactType: latest.ArtifactType{BuildpackArtifact: &latest.BuildpackArtifact{}}}, 223 }, 224 fileDeps: map[string][]string{"img1": {"a"}, "img2": {"b"}, "img3": {"c"}, "img4": {"e"}}, 225 mode: config.RunModes.Dev, 226 expected: "bab56a88d483fa97ae072b027a46681177628156839b7e390842e6243b1ac6aa", 227 }, 228 } 229 for _, test := range tests { 230 testutil.Run(t, test.description, func(t *testutil.T) { 231 t.Override(&fileHasherFunc, mockCacheHasher) 232 t.Override(&artifactConfigFunc, fakeArtifactConfig) 233 g := graph.ToArtifactGraph(test.artifacts) 234 235 for _, a := range test.artifacts { 236 if a.DockerArtifact != nil { 237 tmpDir := t.NewTempDir() 238 tmpDir.Write("./Dockerfile", "ARG SKAFFOLD_GO_GCFLAGS\nFROM foo") 239 a.Workspace = tmpDir.Path(".") 240 a.DockerArtifact.DockerfilePath = Dockerfile 241 } 242 } 243 244 depLister := func(_ context.Context, a *latest.Artifact) ([]string, error) { 245 return test.fileDeps[a.ImageName], nil 246 } 247 248 actual, err := newArtifactHasher(g, depLister, test.mode).hash(context.Background(), test.artifacts[0], platform.Resolver{}) 249 250 t.CheckNoError(err) 251 t.CheckDeepEqual(test.expected, actual) 252 }) 253 } 254 } 255 256 func TestArtifactConfig(t *testing.T) { 257 testutil.Run(t, "", func(t *testutil.T) { 258 config1, err := artifactConfig(&latest.Artifact{ 259 ArtifactType: latest.ArtifactType{ 260 DockerArtifact: &latest.DockerArtifact{ 261 Target: "target", 262 }, 263 }, 264 }) 265 t.CheckNoError(err) 266 267 config2, err := artifactConfig(&latest.Artifact{ 268 ArtifactType: latest.ArtifactType{ 269 DockerArtifact: &latest.DockerArtifact{ 270 Target: "other", 271 }, 272 }, 273 }) 274 t.CheckNoError(err) 275 276 if config1 == config2 { 277 t.Errorf("configs should be different: [%s] [%s]", config1, config2) 278 } 279 }) 280 } 281 282 func TestBuildArgs(t *testing.T) { 283 tests := []struct { 284 mode config.RunMode 285 expected string 286 }{ 287 { 288 mode: config.RunModes.Debug, 289 expected: "a8544410acafce64325abfffcb21e75efdcd575bd9f8d3be2a516125ec547651", 290 }, 291 { 292 mode: config.RunModes.Dev, 293 expected: "f5b610f4fea07461411b2ea0e2cddfd2ffc28d1baed49180f5d3acee5a18f9e7", 294 }, 295 } 296 for _, test := range tests { 297 testutil.Run(t, "", func(t *testutil.T) { 298 tmpDir := t.NewTempDir() 299 tmpDir.Write("./Dockerfile", "ARG SKAFFOLD_GO_GCFLAGS\nFROM foo") 300 artifact := &latest.Artifact{ 301 Workspace: tmpDir.Path("."), 302 ArtifactType: latest.ArtifactType{ 303 DockerArtifact: &latest.DockerArtifact{ 304 DockerfilePath: Dockerfile, 305 BuildArgs: map[string]*string{"one": util.StringPtr("1"), "two": util.StringPtr("2")}, 306 }, 307 }, 308 } 309 t.Override(&fileHasherFunc, mockCacheHasher) 310 t.Override(&artifactConfigFunc, fakeArtifactConfig) 311 actual, err := newArtifactHasher(nil, stubDependencyLister(nil), test.mode).hash(context.Background(), artifact, platform.Resolver{}) 312 313 t.CheckNoError(err) 314 t.CheckDeepEqual(test.expected, actual) 315 316 // Change order of buildargs 317 artifact.ArtifactType.DockerArtifact.BuildArgs = map[string]*string{"two": util.StringPtr("2"), "one": util.StringPtr("1")} 318 actual, err = newArtifactHasher(nil, stubDependencyLister(nil), test.mode).hash(context.Background(), artifact, platform.Resolver{}) 319 320 t.CheckNoError(err) 321 t.CheckDeepEqual(test.expected, actual) 322 323 // Change build args, get different hash 324 artifact.ArtifactType.DockerArtifact.BuildArgs = map[string]*string{"one": util.StringPtr("1")} 325 actual, err = newArtifactHasher(nil, stubDependencyLister(nil), test.mode).hash(context.Background(), artifact, platform.Resolver{}) 326 327 t.CheckNoError(err) 328 if actual == test.expected { 329 t.Fatal("got same hash as different artifact; expected different hashes.") 330 } 331 }) 332 } 333 } 334 335 func TestBuildArgsEnvSubstitution(t *testing.T) { 336 testutil.Run(t, "", func(t *testutil.T) { 337 original := util.OSEnviron 338 defer func() { util.OSEnviron = original }() 339 util.OSEnviron = func() []string { 340 return []string{"FOO=bar"} 341 } 342 tmpDir := t.NewTempDir() 343 tmpDir.Write("./Dockerfile", "ARG SKAFFOLD_GO_GCFLAGS\nFROM foo") 344 artifact := &latest.Artifact{ 345 Workspace: tmpDir.Path("."), 346 ArtifactType: latest.ArtifactType{ 347 DockerArtifact: &latest.DockerArtifact{ 348 BuildArgs: map[string]*string{"env": util.StringPtr("${{.FOO}}")}, 349 DockerfilePath: Dockerfile, 350 }, 351 }, 352 } 353 354 t.Override(&fileHasherFunc, mockCacheHasher) 355 t.Override(&artifactConfigFunc, fakeArtifactConfig) 356 357 depLister := stubDependencyLister([]string{"graph"}) 358 hash1, err := newArtifactHasher(nil, depLister, config.RunModes.Build).hash(context.Background(), artifact, platform.Resolver{}) 359 360 t.CheckNoError(err) 361 362 // Make sure hash is different with a new env 363 364 util.OSEnviron = func() []string { 365 return []string{"FOO=baz"} 366 } 367 368 hash2, err := newArtifactHasher(nil, depLister, config.RunModes.Build).hash(context.Background(), artifact, platform.Resolver{}) 369 370 t.CheckNoError(err) 371 if hash1 == hash2 { 372 t.Fatal("hashes are the same even though build arg changed") 373 } 374 }) 375 } 376 377 func TestCacheHasher(t *testing.T) { 378 tests := []struct { 379 description string 380 differentHash bool 381 newFilename string 382 update func(oldFile string, folder *testutil.TempDir) 383 }{ 384 { 385 description: "change filename", 386 differentHash: true, 387 newFilename: "newfoo", 388 update: func(oldFile string, folder *testutil.TempDir) { 389 folder.Rename(oldFile, "newfoo") 390 }, 391 }, 392 { 393 description: "change file contents", 394 differentHash: true, 395 update: func(oldFile string, folder *testutil.TempDir) { 396 folder.Write(oldFile, "newcontents") 397 }, 398 }, 399 { 400 description: "change both", 401 differentHash: true, 402 newFilename: "newfoo", 403 update: func(oldFile string, folder *testutil.TempDir) { 404 folder.Rename(oldFile, "newfoo") 405 folder.Write(oldFile, "newcontents") 406 }, 407 }, 408 { 409 description: "change nothing", 410 differentHash: false, 411 update: func(oldFile string, folder *testutil.TempDir) {}, 412 }, 413 } 414 for _, test := range tests { 415 testutil.Run(t, test.description, func(t *testutil.T) { 416 originalFile := "foo" 417 originalContents := "contents" 418 419 tmpDir := t.NewTempDir(). 420 Write(originalFile, originalContents) 421 422 path := originalFile 423 depLister := stubDependencyLister([]string{tmpDir.Path(originalFile)}) 424 425 oldHash, err := newArtifactHasher(nil, depLister, config.RunModes.Build).hash(context.Background(), &latest.Artifact{}, platform.Resolver{}) 426 t.CheckNoError(err) 427 428 test.update(originalFile, tmpDir) 429 if test.newFilename != "" { 430 path = test.newFilename 431 } 432 433 depLister = stubDependencyLister([]string{tmpDir.Path(path)}) 434 newHash, err := newArtifactHasher(nil, depLister, config.RunModes.Build).hash(context.Background(), &latest.Artifact{}, platform.Resolver{}) 435 436 t.CheckNoError(err) 437 t.CheckFalse(test.differentHash && oldHash == newHash) 438 t.CheckFalse(!test.differentHash && oldHash != newHash) 439 }) 440 } 441 } 442 443 func TestHashBuildArgs(t *testing.T) { 444 tests := []struct { 445 description string 446 artifactType latest.ArtifactType 447 expected []string 448 mode config.RunMode 449 }{ 450 { 451 description: "docker artifact with build args for dev", 452 artifactType: latest.ArtifactType{ 453 DockerArtifact: &latest.DockerArtifact{ 454 BuildArgs: map[string]*string{ 455 "foo": util.StringPtr("bar"), 456 }, 457 }, 458 }, 459 mode: config.RunModes.Dev, 460 expected: []string{"foo=bar"}, 461 }, { 462 description: "docker artifact with build args for debug", 463 artifactType: latest.ArtifactType{ 464 DockerArtifact: &latest.DockerArtifact{ 465 BuildArgs: map[string]*string{ 466 "foo": util.StringPtr("bar"), 467 }, 468 }, 469 }, 470 mode: config.RunModes.Debug, 471 expected: []string{"SKAFFOLD_GO_GCFLAGS=all=-N -l", "foo=bar"}, 472 }, { 473 description: "docker artifact without build args for debug", 474 artifactType: latest.ArtifactType{ 475 DockerArtifact: &latest.DockerArtifact{}, 476 }, 477 mode: config.RunModes.Debug, 478 expected: []string{"SKAFFOLD_GO_GCFLAGS=all=-N -l"}, 479 }, { 480 description: "docker artifact without build args for dev", 481 artifactType: latest.ArtifactType{ 482 DockerArtifact: &latest.DockerArtifact{}, 483 }, 484 mode: config.RunModes.Dev, 485 }, { 486 description: "kaniko artifact with build args", 487 artifactType: latest.ArtifactType{ 488 KanikoArtifact: &latest.KanikoArtifact{ 489 BuildArgs: map[string]*string{}, 490 }, 491 }, 492 expected: nil, 493 }, { 494 description: "kaniko artifact without build args", 495 artifactType: latest.ArtifactType{ 496 KanikoArtifact: &latest.KanikoArtifact{}, 497 }, 498 }, { 499 description: "buildpacks artifact with env for dev", 500 artifactType: latest.ArtifactType{ 501 BuildpackArtifact: &latest.BuildpackArtifact{ 502 Env: []string{"foo=bar"}, 503 }, 504 }, 505 mode: config.RunModes.Dev, 506 expected: []string{"foo=bar"}, 507 }, { 508 description: "buildpacks artifact without env for dev", 509 artifactType: latest.ArtifactType{ 510 BuildpackArtifact: &latest.BuildpackArtifact{}, 511 }, 512 mode: config.RunModes.Dev, 513 }, { 514 description: "buildpacks artifact with env for debug", 515 artifactType: latest.ArtifactType{ 516 BuildpackArtifact: &latest.BuildpackArtifact{ 517 Env: []string{"foo=bar"}, 518 }, 519 }, 520 mode: config.RunModes.Debug, 521 expected: []string{"GOOGLE_GOGCFLAGS=all=-N -l", "foo=bar"}, 522 }, { 523 description: "buildpacks artifact without env for debug", 524 artifactType: latest.ArtifactType{ 525 BuildpackArtifact: &latest.BuildpackArtifact{}, 526 }, 527 mode: config.RunModes.Debug, 528 expected: []string{"GOOGLE_GOGCFLAGS=all=-N -l"}, 529 }, { 530 description: "custom artifact, dockerfile dependency, with build args", 531 artifactType: latest.ArtifactType{ 532 CustomArtifact: &latest.CustomArtifact{ 533 Dependencies: &latest.CustomDependencies{ 534 Dockerfile: &latest.DockerfileDependency{ 535 BuildArgs: map[string]*string{}, 536 }, 537 }, 538 }, 539 }, 540 expected: nil, 541 }, { 542 description: "custom artifact, no dockerfile dependency", 543 artifactType: latest.ArtifactType{ 544 CustomArtifact: &latest.CustomArtifact{ 545 Dependencies: &latest.CustomDependencies{}, 546 }, 547 }, 548 }, 549 } 550 551 for _, test := range tests { 552 testutil.Run(t, test.description, func(t *testutil.T) { 553 a := &latest.Artifact{ 554 ArtifactType: test.artifactType, 555 } 556 if test.artifactType.DockerArtifact != nil { 557 tmpDir := t.NewTempDir() 558 tmpDir.Write("./Dockerfile", "ARG SKAFFOLD_GO_GCFLAGS\nFROM foo") 559 a.Workspace = tmpDir.Path(".") 560 a.ArtifactType.DockerArtifact.DockerfilePath = Dockerfile 561 } 562 if test.artifactType.KanikoArtifact != nil { 563 tmpDir := t.NewTempDir() 564 tmpDir.Write("./Dockerfile", "ARG SKAFFOLD_GO_GCFLAGS\nFROM foo") 565 a.Workspace = tmpDir.Path(".") 566 a.ArtifactType.KanikoArtifact.DockerfilePath = Dockerfile 567 } 568 actual, err := hashBuildArgs(a, test.mode) 569 t.CheckNoError(err) 570 t.CheckDeepEqual(test.expected, actual) 571 }) 572 } 573 }