diff --git a/postal/service.go b/postal/service.go index d3834bc0..3083dfdd 100644 --- a/postal/service.go +++ b/postal/service.go @@ -179,6 +179,13 @@ func (s Service) Deliver(dependency Dependency, cnbPath, layerPath, platformPath return nil } +// Install will invoke Deliver with a hardcoded value of /platform for the platform path. +// +// Deprecated: Use Deliver instead. +func (s Service) Install(dependency Dependency, cnbPath, layerPath string) error { + return s.Deliver(dependency, cnbPath, layerPath, "/platform") +} + // GenerateBillOfMaterials will generate a list of BOMEntry values given a // collection of Dependency values. func (s Service) GenerateBillOfMaterials(dependencies ...Dependency) []packit.BOMEntry { diff --git a/postal/service_test.go b/postal/service_test.go index d071a259..33cccb80 100644 --- a/postal/service_test.go +++ b/postal/service_test.go @@ -511,7 +511,7 @@ version = "this is super not semver" }, "some-cnb-path", layerPath, - "", + "some-platform-dir", ) } }) @@ -548,6 +548,7 @@ version = "this is super not semver" Expect(err).NotTo(HaveOccurred()) Expect(mappingResolver.FindDependencyMappingCall.Receives.SHA256).To(Equal(dependencySHA)) + Expect(mappingResolver.FindDependencyMappingCall.Receives.PlatformDir).To(Equal("some-platform-dir")) Expect(transport.DropCall.Receives.Root).To(Equal("some-cnb-path")) Expect(transport.DropCall.Receives.Uri).To(Equal("dependency-mapping-entry.tgz")) @@ -716,6 +717,260 @@ version = "this is super not semver" }) }) + context("Install", func() { + var ( + dependencySHA string + layerPath string + install func() error + ) + + it.Before(func() { + var err error + layerPath, err = os.MkdirTemp("", "layer") + Expect(err).NotTo(HaveOccurred()) + + buffer := bytes.NewBuffer(nil) + zw := gzip.NewWriter(buffer) + tw := tar.NewWriter(zw) + + Expect(tw.WriteHeader(&tar.Header{Name: "./some-dir", Mode: 0755, Typeflag: tar.TypeDir})).To(Succeed()) + _, err = tw.Write(nil) + Expect(err).NotTo(HaveOccurred()) + + nestedFile := "./some-dir/some-file" + Expect(tw.WriteHeader(&tar.Header{Name: nestedFile, Mode: 0755, Size: int64(len(nestedFile))})).To(Succeed()) + _, err = tw.Write([]byte(nestedFile)) + Expect(err).NotTo(HaveOccurred()) + + for _, file := range []string{"./first", "./second", "./third"} { + Expect(tw.WriteHeader(&tar.Header{Name: file, Mode: 0755, Size: int64(len(file))})).To(Succeed()) + _, err = tw.Write([]byte(file)) + Expect(err).NotTo(HaveOccurred()) + } + + linkName := "./symlink" + linkDest := "./first" + Expect(tw.WriteHeader(&tar.Header{Name: linkName, Mode: 0777, Size: int64(0), Typeflag: tar.TypeSymlink, Linkname: linkDest})).To(Succeed()) + _, err = tw.Write([]byte{}) + Expect(err).NotTo(HaveOccurred()) + + Expect(tw.Close()).To(Succeed()) + Expect(zw.Close()).To(Succeed()) + + sum := sha256.Sum256(buffer.Bytes()) + dependencySHA = hex.EncodeToString(sum[:]) + + transport.DropCall.Returns.ReadCloser = io.NopCloser(buffer) + + install = func() error { + return service.Install( + postal.Dependency{ + ID: "some-entry", + Stacks: []string{"some-stack"}, + URI: "some-entry.tgz", + SHA256: dependencySHA, + Version: "1.2.3", + }, + "some-cnb-path", + layerPath, + ) + } + }) + + it.After(func() { + Expect(os.RemoveAll(layerPath)).To(Succeed()) + }) + + it("downloads the dependency and unpackages it into the path", func() { + err := install() + + Expect(err).NotTo(HaveOccurred()) + + Expect(transport.DropCall.Receives.Root).To(Equal("some-cnb-path")) + Expect(transport.DropCall.Receives.Uri).To(Equal("some-entry.tgz")) + + files, err := filepath.Glob(fmt.Sprintf("%s/*", layerPath)) + Expect(err).NotTo(HaveOccurred()) + Expect(files).To(ConsistOf([]string{ + filepath.Join(layerPath, "first"), + filepath.Join(layerPath, "second"), + filepath.Join(layerPath, "third"), + filepath.Join(layerPath, "some-dir"), + filepath.Join(layerPath, "symlink"), + })) + + info, err := os.Stat(filepath.Join(layerPath, "first")) + Expect(err).NotTo(HaveOccurred()) + Expect(info.Mode()).To(Equal(os.FileMode(0755))) + }) + + context("when there is a dependency mapping via binding", func() { + it.Before(func() { + mappingResolver.FindDependencyMappingCall.Returns.String = "dependency-mapping-entry.tgz" + }) + + it("looks up the dependency from the platform binding and downloads that instead", func() { + err := install() + + Expect(err).NotTo(HaveOccurred()) + + Expect(mappingResolver.FindDependencyMappingCall.Receives.SHA256).To(Equal(dependencySHA)) + Expect(mappingResolver.FindDependencyMappingCall.Receives.PlatformDir).To(Equal("/platform")) + Expect(transport.DropCall.Receives.Root).To(Equal("some-cnb-path")) + Expect(transport.DropCall.Receives.Uri).To(Equal("dependency-mapping-entry.tgz")) + + files, err := filepath.Glob(fmt.Sprintf("%s/*", layerPath)) + Expect(err).NotTo(HaveOccurred()) + Expect(files).To(ConsistOf([]string{ + filepath.Join(layerPath, "first"), + filepath.Join(layerPath, "second"), + filepath.Join(layerPath, "third"), + filepath.Join(layerPath, "some-dir"), + filepath.Join(layerPath, "symlink"), + })) + + info, err := os.Stat(filepath.Join(layerPath, "first")) + Expect(err).NotTo(HaveOccurred()) + Expect(info.Mode()).To(Equal(os.FileMode(0755))) + }) + }) + + context("failure cases", func() { + context("when the transport cannot fetch a dependency", func() { + it.Before(func() { + transport.DropCall.Returns.Error = errors.New("there was an error") + }) + + it("returns an error", func() { + err := install() + + Expect(err).To(MatchError("failed to fetch dependency: there was an error")) + }) + }) + + context("when the file contents are empty", func() { + it.Before(func() { + // This is a FLAC header + buffer := bytes.NewBuffer([]byte("\x66\x4C\x61\x43\x00\x00\x00\x22")) + transport.DropCall.Returns.ReadCloser = io.NopCloser(buffer) + + sum := sha256.Sum256(buffer.Bytes()) + dependencySHA = hex.EncodeToString(sum[:]) + }) + + it("fails to create a gzip reader", func() { + err := install() + + Expect(err).To(MatchError(ContainSubstring("unsupported archive type"))) + }) + }) + + context("when the file contents are malformed", func() { + it.Before(func() { + buffer := bytes.NewBuffer(nil) + gzipWriter := gzip.NewWriter(buffer) + + _, err := gzipWriter.Write([]byte("something")) + Expect(err).NotTo(HaveOccurred()) + + Expect(gzipWriter.Close()).To(Succeed()) + + transport.DropCall.Returns.ReadCloser = io.NopCloser(buffer) + + sum := sha256.Sum256(buffer.Bytes()) + dependencySHA = hex.EncodeToString(sum[:]) + }) + + it("fails to create a tar reader", func() { + err := install() + + Expect(err).To(MatchError(ContainSubstring("failed to read tar response"))) + }) + }) + + context("when the file checksum does not match", func() { + it("fails to create a tar reader", func() { + err := service.Install(postal.Dependency{ + ID: "some-entry", + Stacks: []string{"some-stack"}, + URI: "some-entry.tgz", + SHA256: "this is not a valid checksum", + Version: "1.2.3", + }, "some-cnb-path", + layerPath, + ) + + Expect(err).To(MatchError(ContainSubstring("checksum does not match"))) + }) + }) + + context("when it does not have permission to write into directory on container", func() { + it.Before(func() { + Expect(os.Chmod(layerPath, 0000)).To(Succeed()) + }) + + it.After(func() { + Expect(os.Chmod(layerPath, 0755)).To(Succeed()) + }) + + it("fails to make a dir", func() { + err := install() + + Expect(err).To(MatchError(ContainSubstring("failed to create archived directory"))) + }) + }) + + context("when it does not have permission to write into directory that it decompressed", func() { + var testDir string + it.Before(func() { + testDir = filepath.Join(layerPath, "some-dir") + Expect(os.MkdirAll(testDir, os.ModePerm)).To(Succeed()) + Expect(os.Chmod(testDir, 0000)).To(Succeed()) + }) + + it.After(func() { + Expect(os.Chmod(testDir, 0755)).To(Succeed()) + }) + + it("fails to make a file", func() { + err := install() + + Expect(err).To(MatchError(ContainSubstring("failed to create archived file"))) + }) + }) + + context("when it is given a broken symlink", func() { + it.Before(func() { + buffer := bytes.NewBuffer(nil) + zw := gzip.NewWriter(buffer) + tw := tar.NewWriter(zw) + + linkName := "symlink" + Expect(tw.WriteHeader(&tar.Header{Name: linkName, Mode: 0777, Size: int64(0), Typeflag: tar.TypeSymlink, Linkname: "some-file"})).To(Succeed()) + _, err := tw.Write([]byte{}) + Expect(err).NotTo(HaveOccurred()) + + Expect(tw.Close()).To(Succeed()) + Expect(zw.Close()).To(Succeed()) + + Expect(os.WriteFile(filepath.Join(layerPath, "some-file"), nil, 0644)).To(Succeed()) + Expect(os.Symlink("some-file", filepath.Join(layerPath, "symlink"))).To(Succeed()) + + sum := sha256.Sum256(buffer.Bytes()) + dependencySHA = hex.EncodeToString(sum[:]) + + transport.DropCall.Returns.ReadCloser = io.NopCloser(buffer) + }) + + it("fails to extract the symlink", func() { + err := install() + + Expect(err).To(MatchError(ContainSubstring("failed to extract symlink"))) + }) + }) + }) + }) + context("GenerateBillOfMaterials", func() { it("returns a list of BOMEntry values", func() {