github.com/buildpacks/pack@v0.33.3-0.20240516162812-884dd1837311/pkg/image/fetcher_test.go (about) 1 package image_test 2 3 import ( 4 "bytes" 5 "context" 6 "fmt" 7 "os" 8 "path/filepath" 9 "runtime" 10 "testing" 11 12 "github.com/buildpacks/imgutil" 13 "github.com/buildpacks/imgutil/local" 14 "github.com/buildpacks/imgutil/remote" 15 "github.com/docker/docker/api/types" 16 "github.com/docker/docker/client" 17 "github.com/golang/mock/gomock" 18 "github.com/google/go-containerregistry/pkg/authn" 19 "github.com/heroku/color" 20 "github.com/pkg/errors" 21 "github.com/sclevine/spec" 22 "github.com/sclevine/spec/report" 23 24 "github.com/buildpacks/pack/pkg/image" 25 "github.com/buildpacks/pack/pkg/logging" 26 "github.com/buildpacks/pack/pkg/testmocks" 27 h "github.com/buildpacks/pack/testhelpers" 28 ) 29 30 var docker client.CommonAPIClient 31 var registryConfig *h.TestRegistryConfig 32 33 func TestFetcher(t *testing.T) { 34 color.Disable(true) 35 defer color.Disable(false) 36 37 h.RequireDocker(t) 38 39 registryConfig = h.RunRegistry(t) 40 defer registryConfig.StopRegistry(t) 41 42 // TODO: is there a better solution to the auth problem? 43 os.Setenv("DOCKER_CONFIG", registryConfig.DockerConfigDir) 44 45 var err error 46 docker, err = client.NewClientWithOpts(client.FromEnv, client.WithVersion("1.38")) 47 h.AssertNil(t, err) 48 spec.Run(t, "Fetcher", testFetcher, spec.Parallel(), spec.Report(report.Terminal{})) 49 } 50 51 func testFetcher(t *testing.T, when spec.G, it spec.S) { 52 var ( 53 imageFetcher *image.Fetcher 54 repoName string 55 repo string 56 outBuf bytes.Buffer 57 osType string 58 ) 59 60 it.Before(func() { 61 repo = "some-org/" + h.RandString(10) 62 repoName = registryConfig.RepoName(repo) 63 imageFetcher = image.NewFetcher(logging.NewLogWithWriters(&outBuf, &outBuf, logging.WithVerbose()), docker) 64 65 info, err := docker.Info(context.TODO()) 66 h.AssertNil(t, err) 67 osType = info.OSType 68 }) 69 70 when("#Fetch", func() { 71 when("daemon is false", func() { 72 when("PullAlways", func() { 73 when("there is a remote image", func() { 74 it.Before(func() { 75 img, err := remote.NewImage(repoName, authn.DefaultKeychain) 76 h.AssertNil(t, err) 77 78 h.AssertNil(t, img.Save()) 79 }) 80 81 it("returns the remote image", func() { 82 _, err := imageFetcher.Fetch(context.TODO(), repoName, image.FetchOptions{Daemon: false, PullPolicy: image.PullAlways}) 83 h.AssertNil(t, err) 84 }) 85 }) 86 87 when("there is no remote image", func() { 88 it("returns an error", func() { 89 _, err := imageFetcher.Fetch(context.TODO(), repoName, image.FetchOptions{Daemon: false, PullPolicy: image.PullAlways}) 90 h.AssertError(t, err, fmt.Sprintf("image '%s' does not exist in registry", repoName)) 91 }) 92 }) 93 }) 94 95 when("PullIfNotPresent", func() { 96 when("there is a remote image", func() { 97 it.Before(func() { 98 img, err := remote.NewImage(repoName, authn.DefaultKeychain) 99 h.AssertNil(t, err) 100 101 h.AssertNil(t, img.Save()) 102 }) 103 104 it("returns the remote image", func() { 105 _, err := imageFetcher.Fetch(context.TODO(), repoName, image.FetchOptions{Daemon: false, PullPolicy: image.PullIfNotPresent}) 106 h.AssertNil(t, err) 107 }) 108 }) 109 110 when("there is no remote image", func() { 111 it("returns an error", func() { 112 _, err := imageFetcher.Fetch(context.TODO(), repoName, image.FetchOptions{Daemon: false, PullPolicy: image.PullIfNotPresent}) 113 h.AssertError(t, err, fmt.Sprintf("image '%s' does not exist in registry", repoName)) 114 }) 115 }) 116 }) 117 }) 118 119 when("daemon is true", func() { 120 when("PullNever", func() { 121 when("there is a local image", func() { 122 it.Before(func() { 123 // Make sure the repoName is not a valid remote repo. 124 // This is to verify that no remote check is made 125 // when there's a valid local image. 126 repoName = "invalidhost" + repoName 127 128 img, err := local.NewImage(repoName, docker) 129 h.AssertNil(t, err) 130 131 h.AssertNil(t, img.Save()) 132 }) 133 134 it.After(func() { 135 h.DockerRmi(docker, repoName) 136 }) 137 138 it("returns the local image", func() { 139 _, err := imageFetcher.Fetch(context.TODO(), repoName, image.FetchOptions{Daemon: true, PullPolicy: image.PullNever}) 140 h.AssertNil(t, err) 141 }) 142 }) 143 144 when("there is no local image", func() { 145 it("returns an error", func() { 146 _, err := imageFetcher.Fetch(context.TODO(), repoName, image.FetchOptions{Daemon: true, PullPolicy: image.PullNever}) 147 h.AssertError(t, err, fmt.Sprintf("image '%s' does not exist on the daemon", repoName)) 148 }) 149 }) 150 }) 151 152 when("PullAlways", func() { 153 when("there is a remote image", func() { 154 var ( 155 logger *logging.LogWithWriters 156 output func() string 157 ) 158 159 it.Before(func() { 160 // Instantiate a pull-able local image 161 // as opposed to a remote image so that the image 162 // is created with the OS of the docker daemon 163 img, err := local.NewImage(repoName, docker) 164 h.AssertNil(t, err) 165 defer h.DockerRmi(docker, repoName) 166 167 h.AssertNil(t, img.Save()) 168 169 h.AssertNil(t, h.PushImage(docker, img.Name(), registryConfig)) 170 171 var outCons *color.Console 172 outCons, output = h.MockWriterAndOutput() 173 logger = logging.NewLogWithWriters(outCons, outCons) 174 imageFetcher = image.NewFetcher(logger, docker) 175 }) 176 177 it.After(func() { 178 h.DockerRmi(docker, repoName) 179 }) 180 181 it("pull the image and return the local copy", func() { 182 _, err := imageFetcher.Fetch(context.TODO(), repoName, image.FetchOptions{Daemon: true, PullPolicy: image.PullAlways}) 183 h.AssertNil(t, err) 184 h.AssertNotEq(t, output(), "") 185 }) 186 187 it("doesn't log anything in quiet mode", func() { 188 logger.WantQuiet(true) 189 _, err := imageFetcher.Fetch(context.TODO(), repoName, image.FetchOptions{Daemon: true, PullPolicy: image.PullAlways}) 190 h.AssertNil(t, err) 191 h.AssertEq(t, output(), "") 192 }) 193 }) 194 195 when("there is no remote image", func() { 196 when("there is a local image", func() { 197 it.Before(func() { 198 img, err := local.NewImage(repoName, docker) 199 h.AssertNil(t, err) 200 201 h.AssertNil(t, img.Save()) 202 }) 203 204 it.After(func() { 205 h.DockerRmi(docker, repoName) 206 }) 207 208 it("returns the local image", func() { 209 _, err := imageFetcher.Fetch(context.TODO(), repoName, image.FetchOptions{Daemon: true, PullPolicy: image.PullAlways}) 210 h.AssertNil(t, err) 211 }) 212 }) 213 214 when("there is no local image", func() { 215 it("returns an error", func() { 216 _, err := imageFetcher.Fetch(context.TODO(), repoName, image.FetchOptions{Daemon: true, PullPolicy: image.PullAlways}) 217 h.AssertError(t, err, fmt.Sprintf("image '%s' does not exist on the daemon", repoName)) 218 }) 219 }) 220 }) 221 222 when("image platform is specified", func() { 223 it("passes the platform argument to the daemon", func() { 224 _, err := imageFetcher.Fetch(context.TODO(), repoName, image.FetchOptions{Daemon: true, PullPolicy: image.PullAlways, Platform: "some-unsupported-platform"}) 225 h.AssertError(t, err, "unknown operating system or architecture") 226 }) 227 228 when("remote platform does not match", func() { 229 it.Before(func() { 230 img, err := remote.NewImage(repoName, authn.DefaultKeychain, remote.WithDefaultPlatform(imgutil.Platform{OS: osType, Architecture: ""})) 231 h.AssertNil(t, err) 232 h.AssertNil(t, img.Save()) 233 }) 234 235 it("retry without setting platform", func() { 236 _, err := imageFetcher.Fetch(context.TODO(), repoName, image.FetchOptions{Daemon: true, PullPolicy: image.PullAlways, Platform: fmt.Sprintf("%s/%s", osType, runtime.GOARCH)}) 237 h.AssertNil(t, err) 238 }) 239 }) 240 }) 241 }) 242 243 when("PullIfNotPresent", func() { 244 when("there is a remote image", func() { 245 var ( 246 label = "label" 247 remoteImgLabel string 248 ) 249 250 it.Before(func() { 251 // Instantiate a pull-able local image 252 // as opposed to a remote image so that the image 253 // is created with the OS of the docker daemon 254 remoteImg, err := local.NewImage(repoName, docker) 255 h.AssertNil(t, err) 256 defer h.DockerRmi(docker, repoName) 257 258 h.AssertNil(t, remoteImg.SetLabel(label, "1")) 259 h.AssertNil(t, remoteImg.Save()) 260 261 h.AssertNil(t, h.PushImage(docker, remoteImg.Name(), registryConfig)) 262 263 remoteImgLabel, err = remoteImg.Label(label) 264 h.AssertNil(t, err) 265 }) 266 267 it.After(func() { 268 h.DockerRmi(docker, repoName) 269 }) 270 271 when("there is a local image", func() { 272 var localImgLabel string 273 274 it.Before(func() { 275 localImg, err := local.NewImage(repoName, docker) 276 h.AssertNil(t, localImg.SetLabel(label, "2")) 277 h.AssertNil(t, err) 278 279 h.AssertNil(t, localImg.Save()) 280 281 localImgLabel, err = localImg.Label(label) 282 h.AssertNil(t, err) 283 }) 284 285 it.After(func() { 286 h.DockerRmi(docker, repoName) 287 }) 288 289 it("returns the local image", func() { 290 fetchedImg, err := imageFetcher.Fetch(context.TODO(), repoName, image.FetchOptions{Daemon: true, PullPolicy: image.PullIfNotPresent}) 291 h.AssertNil(t, err) 292 h.AssertNotContains(t, outBuf.String(), "Pulling image") 293 294 fetchedImgLabel, err := fetchedImg.Label(label) 295 h.AssertNil(t, err) 296 297 h.AssertEq(t, fetchedImgLabel, localImgLabel) 298 h.AssertNotEq(t, fetchedImgLabel, remoteImgLabel) 299 }) 300 }) 301 302 when("there is no local image", func() { 303 it("returns the remote image", func() { 304 fetchedImg, err := imageFetcher.Fetch(context.TODO(), repoName, image.FetchOptions{Daemon: true, PullPolicy: image.PullIfNotPresent}) 305 h.AssertNil(t, err) 306 307 fetchedImgLabel, err := fetchedImg.Label(label) 308 h.AssertNil(t, err) 309 h.AssertEq(t, fetchedImgLabel, remoteImgLabel) 310 }) 311 }) 312 }) 313 314 when("there is no remote image", func() { 315 when("there is a local image", func() { 316 it.Before(func() { 317 img, err := local.NewImage(repoName, docker) 318 h.AssertNil(t, err) 319 320 h.AssertNil(t, img.Save()) 321 }) 322 323 it.After(func() { 324 h.DockerRmi(docker, repoName) 325 }) 326 327 it("returns the local image", func() { 328 _, err := imageFetcher.Fetch(context.TODO(), repoName, image.FetchOptions{Daemon: true, PullPolicy: image.PullIfNotPresent}) 329 h.AssertNil(t, err) 330 }) 331 }) 332 333 when("there is no local image", func() { 334 it("returns an error", func() { 335 _, err := imageFetcher.Fetch(context.TODO(), repoName, image.FetchOptions{Daemon: true, PullPolicy: image.PullIfNotPresent}) 336 h.AssertError(t, err, fmt.Sprintf("image '%s' does not exist on the daemon", repoName)) 337 }) 338 }) 339 }) 340 341 when("image platform is specified", func() { 342 it("passes the platform argument to the daemon", func() { 343 _, err := imageFetcher.Fetch(context.TODO(), repoName, image.FetchOptions{Daemon: true, PullPolicy: image.PullIfNotPresent, Platform: "some-unsupported-platform"}) 344 h.AssertError(t, err, "unknown operating system or architecture") 345 }) 346 }) 347 }) 348 }) 349 350 when("layout option is provided", func() { 351 var ( 352 layoutOption image.LayoutOption 353 imagePath string 354 tmpDir string 355 err error 356 ) 357 358 it.Before(func() { 359 // set up local layout repo 360 tmpDir, err = os.MkdirTemp("", "pack.fetcher.test") 361 h.AssertNil(t, err) 362 363 // dummy layer to validate sparse behavior 364 tarDir := filepath.Join(tmpDir, "layer") 365 err = os.MkdirAll(tarDir, os.ModePerm) 366 h.AssertNil(t, err) 367 layerPath := h.CreateTAR(t, tarDir, ".", -1) 368 369 // set up the remote image to be used 370 img, err := remote.NewImage(repoName, authn.DefaultKeychain) 371 img.AddLayer(layerPath) 372 h.AssertNil(t, err) 373 h.AssertNil(t, img.Save()) 374 375 // set up layout options for the tests 376 imagePath = filepath.Join(tmpDir, repo) 377 layoutOption = image.LayoutOption{ 378 Path: imagePath, 379 Sparse: false, 380 } 381 }) 382 383 it.After(func() { 384 err = os.RemoveAll(tmpDir) 385 h.AssertNil(t, err) 386 }) 387 388 when("sparse is false", func() { 389 it("returns and layout image on disk", func() { 390 _, err := imageFetcher.Fetch(context.TODO(), repoName, image.FetchOptions{LayoutOption: layoutOption}) 391 h.AssertNil(t, err) 392 393 // all layers were written 394 h.AssertBlobsLen(t, imagePath, 3) 395 }) 396 }) 397 398 when("sparse is true", func() { 399 it("returns and layout image on disk", func() { 400 layoutOption.Sparse = true 401 _, err := imageFetcher.Fetch(context.TODO(), repoName, image.FetchOptions{LayoutOption: layoutOption}) 402 h.AssertNil(t, err) 403 404 // only manifest and config was written 405 h.AssertBlobsLen(t, imagePath, 2) 406 }) 407 }) 408 }) 409 }) 410 411 when("#CheckReadAccess", func() { 412 var daemon bool 413 414 when("Daemon is true", func() { 415 it.Before(func() { 416 daemon = true 417 }) 418 419 when("an error is thrown by the daemon", func() { 420 it.Before(func() { 421 mockController := gomock.NewController(t) 422 mockDockerClient := testmocks.NewMockCommonAPIClient(mockController) 423 mockDockerClient.EXPECT().ServerVersion(gomock.Any()).Return(types.Version{}, errors.New("something wrong happened")) 424 imageFetcher = image.NewFetcher(logging.NewLogWithWriters(&outBuf, &outBuf, logging.WithVerbose()), mockDockerClient) 425 }) 426 when("PullNever", func() { 427 it("read access must be false", func() { 428 h.AssertFalse(t, imageFetcher.CheckReadAccess("pack.test/dummy", image.FetchOptions{Daemon: daemon, PullPolicy: image.PullNever})) 429 h.AssertContains(t, outBuf.String(), "failed reading image 'pack.test/dummy' from the daemon") 430 }) 431 }) 432 433 when("PullIfNotPresent", func() { 434 it("read access must be false", func() { 435 h.AssertFalse(t, imageFetcher.CheckReadAccess("pack.test/dummy", image.FetchOptions{Daemon: daemon, PullPolicy: image.PullIfNotPresent})) 436 h.AssertContains(t, outBuf.String(), "failed reading image 'pack.test/dummy' from the daemon") 437 }) 438 }) 439 }) 440 441 when("image exists only in the daemon", func() { 442 it.Before(func() { 443 img, err := local.NewImage("pack.test/dummy", docker) 444 h.AssertNil(t, err) 445 h.AssertNil(t, img.Save()) 446 }) 447 when("PullAlways", func() { 448 it("read access must be false", func() { 449 h.AssertFalse(t, imageFetcher.CheckReadAccess("pack.test/dummy", image.FetchOptions{Daemon: daemon, PullPolicy: image.PullAlways})) 450 }) 451 }) 452 453 when("PullNever", func() { 454 it("read access must be true", func() { 455 h.AssertTrue(t, imageFetcher.CheckReadAccess("pack.test/dummy", image.FetchOptions{Daemon: daemon, PullPolicy: image.PullNever})) 456 }) 457 }) 458 459 when("PullIfNotPresent", func() { 460 it("read access must be true", func() { 461 h.AssertTrue(t, imageFetcher.CheckReadAccess("pack.test/dummy", image.FetchOptions{Daemon: daemon, PullPolicy: image.PullIfNotPresent})) 462 }) 463 }) 464 }) 465 466 when("image doesn't exist in the daemon but in remote", func() { 467 it.Before(func() { 468 img, err := remote.NewImage(repoName, authn.DefaultKeychain) 469 h.AssertNil(t, err) 470 h.AssertNil(t, img.Save()) 471 }) 472 when("PullAlways", func() { 473 it("read access must be true", func() { 474 h.AssertTrue(t, imageFetcher.CheckReadAccess(repoName, image.FetchOptions{Daemon: daemon, PullPolicy: image.PullAlways})) 475 }) 476 }) 477 478 when("PullNever", func() { 479 it("read access must be false", func() { 480 h.AssertFalse(t, imageFetcher.CheckReadAccess(repoName, image.FetchOptions{Daemon: daemon, PullPolicy: image.PullNever})) 481 }) 482 }) 483 484 when("PullIfNotPresent", func() { 485 it("read access must be true", func() { 486 h.AssertTrue(t, imageFetcher.CheckReadAccess(repoName, image.FetchOptions{Daemon: daemon, PullPolicy: image.PullIfNotPresent})) 487 }) 488 }) 489 }) 490 }) 491 492 when("Daemon is false", func() { 493 it.Before(func() { 494 daemon = false 495 }) 496 497 when("remote image doesn't exists", func() { 498 it("fails when checking dummy image", func() { 499 h.AssertFalse(t, imageFetcher.CheckReadAccess("pack.test/dummy", image.FetchOptions{Daemon: daemon})) 500 h.AssertContains(t, outBuf.String(), "CheckReadAccess failed for the run image pack.test/dummy") 501 }) 502 }) 503 504 when("remote image exists", func() { 505 it.Before(func() { 506 img, err := remote.NewImage(repoName, authn.DefaultKeychain) 507 h.AssertNil(t, err) 508 h.AssertNil(t, img.Save()) 509 }) 510 511 it("read access is valid", func() { 512 h.AssertTrue(t, imageFetcher.CheckReadAccess(repoName, image.FetchOptions{Daemon: daemon})) 513 h.AssertContains(t, outBuf.String(), fmt.Sprintf("CheckReadAccess succeeded for the run image %s", repoName)) 514 }) 515 }) 516 }) 517 }) 518 }