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  }