github.com/paketo-buildpacks/libpak@v1.70.0/dependency_cache_test.go (about)

     1  /*
     2   * Copyright 2018-2020 the original author or 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   *      https://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 libpak_test
    18  
    19  import (
    20  	"bytes"
    21  	"fmt"
    22  	"io"
    23  	"net/http"
    24  	"net/url"
    25  	"os"
    26  	"path/filepath"
    27  	"testing"
    28  	"time"
    29  
    30  	"github.com/BurntSushi/toml"
    31  	"github.com/buildpacks/libcnb"
    32  	. "github.com/onsi/gomega"
    33  	"github.com/onsi/gomega/ghttp"
    34  	"github.com/sclevine/spec"
    35  
    36  	"github.com/paketo-buildpacks/libpak"
    37  	"github.com/paketo-buildpacks/libpak/bard"
    38  )
    39  
    40  func testDependencyCache(t *testing.T, context spec.G, it spec.S) {
    41  	var (
    42  		Expect = NewWithT(t).Expect
    43  	)
    44  
    45  	context("NewDependencyCache", func() {
    46  		var ctx libcnb.BuildContext
    47  
    48  		it.Before(func() {
    49  			ctx = libcnb.BuildContext{
    50  				Buildpack: libcnb.Buildpack{
    51  					Info: libcnb.BuildpackInfo{
    52  						ID:      "some-buildpack-id",
    53  						Version: "some-buildpack-version",
    54  					},
    55  					Path: "some/path",
    56  				},
    57  			}
    58  		})
    59  
    60  		it("set default CachePath and UserAgent", func() {
    61  			dependencyCache, err := libpak.NewDependencyCache(ctx)
    62  			Expect(err).NotTo(HaveOccurred())
    63  			Expect(dependencyCache.CachePath).To(Equal(filepath.Join("some/path/dependencies")))
    64  			Expect(dependencyCache.UserAgent).To(Equal("some-buildpack-id/some-buildpack-version"))
    65  			Expect(dependencyCache.Mappings).To(Equal(map[string]string{}))
    66  		})
    67  
    68  		it("uses default timeout values", func() {
    69  			dependencyCache, err := libpak.NewDependencyCache(ctx)
    70  			Expect(err).NotTo(HaveOccurred())
    71  			Expect(dependencyCache.HttpClientTimeouts.DialerTimeout).To(Equal(6 * time.Second))
    72  			Expect(dependencyCache.HttpClientTimeouts.DialerKeepAlive).To(Equal(60 * time.Second))
    73  			Expect(dependencyCache.HttpClientTimeouts.TLSHandshakeTimeout).To(Equal(5 * time.Second))
    74  			Expect(dependencyCache.HttpClientTimeouts.ResponseHeaderTimeout).To(Equal(5 * time.Second))
    75  			Expect(dependencyCache.HttpClientTimeouts.ExpectContinueTimeout).To(Equal(1 * time.Second))
    76  		})
    77  
    78  		context("custom timeout setttings", func() {
    79  			it.Before(func() {
    80  				t.Setenv("BP_DIALER_TIMEOUT", "7")
    81  				t.Setenv("BP_DIALER_KEEP_ALIVE", "50")
    82  				t.Setenv("BP_TLS_HANDSHAKE_TIMEOUT", "2")
    83  				t.Setenv("BP_RESPONSE_HEADER_TIMEOUT", "3")
    84  				t.Setenv("BP_EXPECT_CONTINUE_TIMEOUT", "2")
    85  			})
    86  
    87  			it("uses custom timeout values", func() {
    88  				dependencyCache, err := libpak.NewDependencyCache(ctx)
    89  				Expect(err).NotTo(HaveOccurred())
    90  				Expect(dependencyCache.HttpClientTimeouts.DialerTimeout).To(Equal(7 * time.Second))
    91  				Expect(dependencyCache.HttpClientTimeouts.DialerKeepAlive).To(Equal(50 * time.Second))
    92  				Expect(dependencyCache.HttpClientTimeouts.TLSHandshakeTimeout).To(Equal(2 * time.Second))
    93  				Expect(dependencyCache.HttpClientTimeouts.ResponseHeaderTimeout).To(Equal(3 * time.Second))
    94  				Expect(dependencyCache.HttpClientTimeouts.ExpectContinueTimeout).To(Equal(2 * time.Second))
    95  			})
    96  		})
    97  
    98  		context("bindings with type dependencies exist", func() {
    99  			it.Before(func() {
   100  				ctx.Platform.Bindings = libcnb.Bindings{
   101  					{
   102  						Type: "dependency-mapping",
   103  						Secret: map[string]string{
   104  							"some-digest1": "some-uri1",
   105  							"some-digest2": "some-uri2",
   106  						},
   107  					},
   108  					{
   109  						Type: "not-dependency-mapping",
   110  						Secret: map[string]string{
   111  							"some-thing": "other-thing",
   112  						},
   113  					},
   114  					{
   115  						Type: "dependency-mapping",
   116  						Secret: map[string]string{
   117  							"some-digest3": "some-uri3",
   118  							"some-digest4": "some-uri4",
   119  						},
   120  					},
   121  				}
   122  			})
   123  
   124  			it("sets Mappings", func() {
   125  				dependencyCache, err := libpak.NewDependencyCache(ctx)
   126  				Expect(err).NotTo(HaveOccurred())
   127  				Expect(dependencyCache.Mappings).To(Equal(
   128  					map[string]string{
   129  						"some-digest1": "some-uri1",
   130  						"some-digest2": "some-uri2",
   131  						"some-digest3": "some-uri3",
   132  						"some-digest4": "some-uri4",
   133  					},
   134  				))
   135  			})
   136  
   137  			context("multiple bindings map the same digest", func() {
   138  				it.Before(func() {
   139  					ctx.Platform.Bindings = append(ctx.Platform.Bindings, libcnb.Binding{
   140  						Type: "dependency-mapping",
   141  						Secret: map[string]string{
   142  							"some-digest1": "other-uri",
   143  						},
   144  					})
   145  				})
   146  
   147  				it("errors", func() {
   148  					_, err := libpak.NewDependencyCache(ctx)
   149  					Expect(err).To(HaveOccurred())
   150  				})
   151  			})
   152  		})
   153  
   154  		context("dependency mirror from environment variable", func() {
   155  			it.Before(func() {
   156  				t.Setenv("BP_DEPENDENCY_MIRROR", "https://env-var-mirror.acme.com")
   157  				t.Setenv("BP_DEPENDENCY_MIRROR_EXAMP__LE_COM", "https://examp-le.com")
   158  			})
   159  
   160  			it("uses BP_DEPENDENCY_MIRROR environment variable", func() {
   161  				dependencyCache, err := libpak.NewDependencyCache(ctx)
   162  				Expect(err).NotTo(HaveOccurred())
   163  				Expect(dependencyCache.DependencyMirrors["default"]).To(Equal("https://env-var-mirror.acme.com"))
   164  				Expect(dependencyCache.DependencyMirrors["examp-le.com"]).To(Equal("https://examp-le.com"))
   165  			})
   166  		})
   167  
   168  		context("dependency mirror from binding and environment variable", func() {
   169  			it.Before(func() {
   170  				t.Setenv("BP_DEPENDENCY_MIRROR_EXAMP__LE_COM", "https://examp-le.com")
   171  				ctx.Platform.Bindings = append(ctx.Platform.Bindings, libcnb.Binding{
   172  					Type: "dependency-mirror",
   173  					Secret: map[string]string{
   174  						"default":      "https://bindings-mirror.acme.com",
   175  						"examp-le.com": "https://invalid.com",
   176  					},
   177  				})
   178  			})
   179  
   180  			it("uses dependency-mirror binding", func() {
   181  				dependencyCache, err := libpak.NewDependencyCache(ctx)
   182  				Expect(err).NotTo(HaveOccurred())
   183  				Expect(dependencyCache.DependencyMirrors["default"]).To(Equal("https://bindings-mirror.acme.com"))
   184  			})
   185  
   186  			it("environment variable overrides binding", func() {
   187  				dependencyCache, err := libpak.NewDependencyCache(ctx)
   188  				Expect(err).NotTo(HaveOccurred())
   189  				Expect(dependencyCache.DependencyMirrors["examp-le.com"]).To(Equal("https://examp-le.com"))
   190  			})
   191  		})
   192  	})
   193  
   194  	context("artifacts", func() {
   195  		var (
   196  			cachePath       string
   197  			downloadPath    string
   198  			dependency      libpak.BuildpackDependency
   199  			dependencyCache libpak.DependencyCache
   200  			server          *ghttp.Server
   201  		)
   202  
   203  		it.Before(func() {
   204  			var err error
   205  
   206  			cachePath = t.TempDir()
   207  			Expect(err).NotTo(HaveOccurred())
   208  
   209  			downloadPath = t.TempDir()
   210  			Expect(err).NotTo(HaveOccurred())
   211  
   212  			RegisterTestingT(t)
   213  			server = ghttp.NewServer()
   214  
   215  			dependency = libpak.BuildpackDependency{
   216  				ID:              "test-id",
   217  				Name:            "test-name",
   218  				Version:         "1.1.1",
   219  				URI:             fmt.Sprintf("%s/test-path", server.URL()),
   220  				SHA256:          "576dd8416de5619ea001d9662291d62444d1292a38e96956bc4651c01f14bca1",
   221  				Stacks:          []string{"test-stack"},
   222  				DeprecationDate: time.Now(),
   223  				Licenses: []libpak.BuildpackDependencyLicense{
   224  					{
   225  						Type: "test-type",
   226  						URI:  "test-uri",
   227  					},
   228  				},
   229  				CPEs: []string{"cpe:2.3:a:some:jre:11.0.2:*:*:*:*:*:*:*"},
   230  				PURL: "pkg:generic/some-java11@11.0.2?arch=amd64",
   231  			}
   232  
   233  			dependencyCache = libpak.DependencyCache{
   234  				CachePath:    cachePath,
   235  				DownloadPath: downloadPath,
   236  				UserAgent:    "test-user-agent",
   237  			}
   238  		})
   239  
   240  		it.After(func() {
   241  			Expect(os.RemoveAll(cachePath)).To(Succeed())
   242  			Expect(os.RemoveAll(downloadPath)).To(Succeed())
   243  			server.Close()
   244  		})
   245  
   246  		copyFile := func(source string, destination string) {
   247  			in, err := os.Open(source)
   248  			Expect(err).NotTo(HaveOccurred())
   249  			defer in.Close()
   250  
   251  			Expect(os.MkdirAll(filepath.Dir(destination), 0755)).To(Succeed())
   252  			out, err := os.OpenFile(destination, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644)
   253  			Expect(err).NotTo(HaveOccurred())
   254  			defer out.Close()
   255  
   256  			_, err = io.Copy(out, in)
   257  			Expect(err).NotTo(HaveOccurred())
   258  		}
   259  
   260  		writeTOML := func(destination string, v interface{}) {
   261  			Expect(os.MkdirAll(filepath.Dir(destination), 0755)).To(Succeed())
   262  			out, err := os.OpenFile(destination, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644)
   263  			Expect(err).NotTo(HaveOccurred())
   264  			defer out.Close()
   265  
   266  			Expect(toml.NewEncoder(out).Encode(v)).To(Succeed())
   267  		}
   268  
   269  		it("returns from cache path", func() {
   270  			copyFile(filepath.Join("testdata", "test-file"), filepath.Join(cachePath, dependency.SHA256, "test-path"))
   271  			writeTOML(filepath.Join(cachePath, fmt.Sprintf("%s.toml", dependency.SHA256)), dependency)
   272  
   273  			a, err := dependencyCache.Artifact(dependency)
   274  			Expect(err).NotTo(HaveOccurred())
   275  
   276  			Expect(io.ReadAll(a)).To(Equal([]byte("test-fixture")))
   277  		})
   278  
   279  		it("returns from download path", func() {
   280  			copyFile(filepath.Join("testdata", "test-file"), filepath.Join(downloadPath, dependency.SHA256, "test-path"))
   281  			writeTOML(filepath.Join(downloadPath, fmt.Sprintf("%s.toml", dependency.SHA256)), dependency)
   282  
   283  			a, err := dependencyCache.Artifact(dependency)
   284  			Expect(err).NotTo(HaveOccurred())
   285  
   286  			Expect(io.ReadAll(a)).To(Equal([]byte("test-fixture")))
   287  		})
   288  
   289  		it("downloads", func() {
   290  			server.AppendHandlers(ghttp.CombineHandlers(
   291  				ghttp.VerifyRequest(http.MethodGet, "/test-path", ""),
   292  				ghttp.RespondWith(http.StatusOK, "test-fixture"),
   293  			))
   294  
   295  			a, err := dependencyCache.Artifact(dependency)
   296  			Expect(err).NotTo(HaveOccurred())
   297  
   298  			Expect(io.ReadAll(a)).To(Equal([]byte("test-fixture")))
   299  		})
   300  
   301  		context("uri is overridden HTTP", func() {
   302  			it.Before(func() {
   303  				dependencyCache.Mappings = map[string]string{
   304  					dependency.SHA256: fmt.Sprintf("%s/override-path", server.URL()),
   305  				}
   306  			})
   307  
   308  			it("downloads from override uri", func() {
   309  				server.AppendHandlers(ghttp.CombineHandlers(
   310  					ghttp.VerifyRequest(http.MethodGet, "/override-path", ""),
   311  					ghttp.RespondWith(http.StatusOK, "test-fixture"),
   312  				))
   313  
   314  				a, err := dependencyCache.Artifact(dependency)
   315  				Expect(err).NotTo(HaveOccurred())
   316  
   317  				Expect(io.ReadAll(a)).To(Equal([]byte("test-fixture")))
   318  			})
   319  		})
   320  
   321  		context("uri is overridden FILE", func() {
   322  			it.Before(func() {
   323  				sourcePath := t.TempDir()
   324  				sourceFile := filepath.Join(sourcePath, "source-file")
   325  				Expect(os.WriteFile(sourceFile, []byte("test-fixture"), 0644)).ToNot(HaveOccurred())
   326  
   327  				dependencyCache.Mappings = map[string]string{
   328  					dependency.SHA256: fmt.Sprintf("file://%s", sourceFile),
   329  				}
   330  			})
   331  
   332  			it("downloads from override filesystem", func() {
   333  				a, err := dependencyCache.Artifact(dependency)
   334  				Expect(err).NotTo(HaveOccurred())
   335  
   336  				Expect(io.ReadAll(a)).To(Equal([]byte("test-fixture")))
   337  			})
   338  		})
   339  
   340  		context("dependency mirror is used https", func() {
   341  			var mirrorServer *ghttp.Server
   342  
   343  			it.Before(func() {
   344  				mirrorServer = ghttp.NewTLSServer()
   345  				dependencyCache.DependencyMirrors = map[string]string{}
   346  			})
   347  
   348  			it.After(func() {
   349  				mirrorServer.Close()
   350  			})
   351  
   352  			it("downloads from https mirror", func() {
   353  				url, err := url.Parse(mirrorServer.URL())
   354  				Expect(err).NotTo(HaveOccurred())
   355  				mirrorServer.AppendHandlers(ghttp.CombineHandlers(
   356  					ghttp.VerifyBasicAuth("username", "password"),
   357  					ghttp.VerifyRequest(http.MethodGet, "/foo/bar/test-path", ""),
   358  					ghttp.RespondWith(http.StatusOK, "test-fixture"),
   359  				))
   360  
   361  				dependencyCache.DependencyMirrors["default"] = url.Scheme + "://" + "username:password@" + url.Host + "/foo/bar"
   362  				a, err := dependencyCache.Artifact(dependency)
   363  				Expect(err).NotTo(HaveOccurred())
   364  
   365  				Expect(io.ReadAll(a)).To(Equal([]byte("test-fixture")))
   366  			})
   367  
   368  			it("downloads from https mirror preserving hostname", func() {
   369  				url, err := url.Parse(mirrorServer.URL())
   370  				Expect(err).NotTo(HaveOccurred())
   371  				mirrorServer.AppendHandlers(ghttp.CombineHandlers(
   372  					ghttp.VerifyRequest(http.MethodGet, "/"+url.Hostname()+"/test-path", ""),
   373  					ghttp.RespondWith(http.StatusOK, "test-fixture"),
   374  				))
   375  
   376  				dependencyCache.DependencyMirrors["default"] = url.Scheme + "://" + url.Host + "/{originalHost}"
   377  				a, err := dependencyCache.Artifact(dependency)
   378  				Expect(err).NotTo(HaveOccurred())
   379  
   380  				Expect(io.ReadAll(a)).To(Equal([]byte("test-fixture")))
   381  			})
   382  
   383  			it("downloads from https mirror host specific", func() {
   384  				url, err := url.Parse(mirrorServer.URL())
   385  				Expect(err).NotTo(HaveOccurred())
   386  				mirrorServer.AppendHandlers(ghttp.CombineHandlers(
   387  					ghttp.VerifyRequest(http.MethodGet, "/host-specific/test-path", ""),
   388  					ghttp.RespondWith(http.StatusOK, "test-fixture"),
   389  				))
   390  
   391  				dependencyCache.DependencyMirrors["127.0.0.1"] = url.Scheme + "://" + url.Host + "/host-specific"
   392  				a, err := dependencyCache.Artifact(dependency)
   393  				Expect(err).NotTo(HaveOccurred())
   394  
   395  				Expect(io.ReadAll(a)).To(Equal([]byte("test-fixture")))
   396  			})
   397  		})
   398  
   399  		context("dependency mirror is used file", func() {
   400  			var (
   401  				mirrorPath              string
   402  				mirrorPathPreservedHost string
   403  			)
   404  
   405  			it.Before(func() {
   406  				var err error
   407  				mirrorPath, err = os.MkdirTemp("", "mirror-path")
   408  				Expect(err).NotTo(HaveOccurred())
   409  				originalUrl, err := url.Parse(dependency.URI)
   410  				Expect(err).NotTo(HaveOccurred())
   411  				mirrorPathPreservedHost = filepath.Join(mirrorPath, originalUrl.Hostname(), "prefix")
   412  				Expect(os.MkdirAll(mirrorPathPreservedHost, os.ModePerm)).NotTo(HaveOccurred())
   413  				dependencyCache.DependencyMirrors = map[string]string{}
   414  			})
   415  
   416  			it.After(func() {
   417  				Expect(os.RemoveAll(mirrorPath)).To(Succeed())
   418  			})
   419  
   420  			it("downloads from file mirror", func() {
   421  				mirrorFile := filepath.Join(mirrorPath, "test-path")
   422  				Expect(os.WriteFile(mirrorFile, []byte("test-fixture"), 0644)).ToNot(HaveOccurred())
   423  
   424  				dependencyCache.DependencyMirrors["default"] = "file://" + mirrorPath
   425  				a, err := dependencyCache.Artifact(dependency)
   426  				Expect(err).NotTo(HaveOccurred())
   427  
   428  				Expect(io.ReadAll(a)).To(Equal([]byte("test-fixture")))
   429  			})
   430  
   431  			it("downloads from file mirror preserving hostname", func() {
   432  				mirrorFilePreservedHost := filepath.Join(mirrorPathPreservedHost, "test-path")
   433  				Expect(os.WriteFile(mirrorFilePreservedHost, []byte("test-fixture"), 0644)).ToNot(HaveOccurred())
   434  
   435  				dependencyCache.DependencyMirrors["default"] = "file://" + mirrorPath + "/{originalHost}" + "/prefix"
   436  				a, err := dependencyCache.Artifact(dependency)
   437  				Expect(err).NotTo(HaveOccurred())
   438  
   439  				Expect(io.ReadAll(a)).To(Equal([]byte("test-fixture")))
   440  			})
   441  		})
   442  
   443  		it("fails with invalid SHA256", func() {
   444  			server.AppendHandlers(ghttp.RespondWith(http.StatusOK, "invalid-fixture"))
   445  
   446  			_, err := dependencyCache.Artifact(dependency)
   447  			Expect(err).To(HaveOccurred())
   448  		})
   449  
   450  		it("skips cache with empty SHA256", func() {
   451  			copyFile(filepath.Join("testdata", "test-file"), filepath.Join(cachePath, dependency.SHA256, "test-path"))
   452  			writeTOML(filepath.Join(cachePath, fmt.Sprintf("%s.toml", dependency.SHA256)), dependency)
   453  			copyFile(filepath.Join("testdata", "test-file"), filepath.Join(downloadPath, dependency.SHA256, "test-path"))
   454  			writeTOML(filepath.Join(downloadPath, fmt.Sprintf("%s.toml", dependency.SHA256)), dependency)
   455  
   456  			dependency.SHA256 = ""
   457  			server.AppendHandlers(ghttp.RespondWith(http.StatusOK, "alternate-fixture"))
   458  
   459  			a, err := dependencyCache.Artifact(dependency)
   460  			Expect(err).NotTo(HaveOccurred())
   461  
   462  			Expect(io.ReadAll(a)).To(Equal([]byte("alternate-fixture")))
   463  		})
   464  
   465  		it("sets User-Agent", func() {
   466  			server.AppendHandlers(ghttp.CombineHandlers(
   467  				ghttp.VerifyHeaderKV("User-Agent", "test-user-agent"),
   468  				ghttp.RespondWith(http.StatusOK, "test-fixture"),
   469  			))
   470  
   471  			a, err := dependencyCache.Artifact(dependency)
   472  			Expect(err).NotTo(HaveOccurred())
   473  
   474  			Expect(io.ReadAll(a)).To(Equal([]byte("test-fixture")))
   475  		})
   476  
   477  		it("modifies request", func() {
   478  			server.AppendHandlers(ghttp.CombineHandlers(
   479  				ghttp.VerifyHeaderKV("User-Agent", "test-user-agent"),
   480  				ghttp.VerifyHeaderKV("Test-Key", "test-value"),
   481  				ghttp.RespondWith(http.StatusOK, "test-fixture"),
   482  			))
   483  
   484  			a, err := dependencyCache.Artifact(dependency, func(request *http.Request) (*http.Request, error) {
   485  				request.Header.Add("Test-Key", "test-value")
   486  				return request, nil
   487  			})
   488  			Expect(err).NotTo(HaveOccurred())
   489  
   490  			Expect(io.ReadAll(a)).To(Equal([]byte("test-fixture")))
   491  		})
   492  
   493  		it("hide uri credentials from log", func() {
   494  			server.AppendHandlers(ghttp.CombineHandlers(
   495  				ghttp.RespondWith(http.StatusOK, "test-fixture"),
   496  			))
   497  
   498  			url, err := url.ParseRequestURI(dependency.URI)
   499  			Expect(err).NotTo(HaveOccurred())
   500  			credentials := "username:password"
   501  			uriWithBasicCreds := url.Scheme + "://" + credentials + "@" + url.Hostname() + ":" + url.Port() + url.Path
   502  			dependency.URI = uriWithBasicCreds
   503  
   504  			var logBuffer bytes.Buffer
   505  			dependencyCache.Logger = bard.NewLogger(&logBuffer)
   506  
   507  			// Make sure the password is not part of the log output.
   508  			a, errA := dependencyCache.Artifact(dependency)
   509  			Expect(errA).NotTo(HaveOccurred())
   510  			Expect(a).NotTo(BeNil())
   511  			Expect(logBuffer.String()).NotTo(ContainSubstring("password"))
   512  			logBuffer.Reset()
   513  
   514  			// Make sure the password is not part of the log output when an error occurs.
   515  			dependency.URI = "foo://username:password@acme.com"
   516  			b, errB := dependencyCache.Artifact(dependency)
   517  			Expect(errB).To(HaveOccurred())
   518  			Expect(b).To(BeNil())
   519  			Expect(logBuffer.String()).NotTo(ContainSubstring("password"))
   520  		})
   521  	})
   522  }