From f9083e4b04f5b9f018f5181df1edd0d06a4032f4 Mon Sep 17 00:00:00 2001 From: Andrew Meyer Date: Mon, 27 Sep 2021 11:24:19 -0700 Subject: [PATCH] Adds service binding resolver Signed-off-by: Brayan Henao --- .gitignore | 1 + postal/fakes/mapping_resolver.go | 10 +- postal/internal/dependency_mappings.go | 55 +-- postal/internal/dependency_mappings_test.go | 111 ++--- postal/internal/fakes/binding_resolver.go | 37 ++ postal/service.go | 16 +- postal/service_test.go | 103 ++--- servicebindings/entry.go | 59 +++ servicebindings/entry_test.go | 75 ++++ servicebindings/init_test.go | 15 + servicebindings/resolver.go | 241 +++++++++++ servicebindings/resolver_test.go | 441 ++++++++++++++++++++ 12 files changed, 1001 insertions(+), 163 deletions(-) create mode 100644 postal/internal/fakes/binding_resolver.go create mode 100644 servicebindings/entry.go create mode 100644 servicebindings/entry_test.go create mode 100644 servicebindings/init_test.go create mode 100644 servicebindings/resolver.go create mode 100644 servicebindings/resolver_test.go diff --git a/.gitignore b/.gitignore index 4befed30..ec05de3b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ .DS_Store .idea +coverage.out diff --git a/postal/fakes/mapping_resolver.go b/postal/fakes/mapping_resolver.go index d25b6875..09a8b0a5 100644 --- a/postal/fakes/mapping_resolver.go +++ b/postal/fakes/mapping_resolver.go @@ -4,11 +4,11 @@ import "sync" type MappingResolver struct { FindDependencyMappingCall struct { - sync.Mutex + mutex sync.Mutex CallCount int Receives struct { SHA256 string - BindingPath string + PlatformDir string } Returns struct { String string @@ -19,11 +19,11 @@ type MappingResolver struct { } func (f *MappingResolver) FindDependencyMapping(param1 string, param2 string) (string, error) { - f.FindDependencyMappingCall.Lock() - defer f.FindDependencyMappingCall.Unlock() + f.FindDependencyMappingCall.mutex.Lock() + defer f.FindDependencyMappingCall.mutex.Unlock() f.FindDependencyMappingCall.CallCount++ f.FindDependencyMappingCall.Receives.SHA256 = param1 - f.FindDependencyMappingCall.Receives.BindingPath = param2 + f.FindDependencyMappingCall.Receives.PlatformDir = param2 if f.FindDependencyMappingCall.Stub != nil { return f.FindDependencyMappingCall.Stub(param1, param2) } diff --git a/postal/internal/dependency_mappings.go b/postal/internal/dependency_mappings.go index 6a09c825..f54e71b5 100644 --- a/postal/internal/dependency_mappings.go +++ b/postal/internal/dependency_mappings.go @@ -2,51 +2,36 @@ package internal import ( "fmt" - "os" - "path/filepath" - "strings" + "github.com/paketo-buildpacks/packit/servicebindings" ) -type DependencyMappingResolver struct{} +//go:generate faux --interface BindingResolver --output fakes/binding_resolver.go +type BindingResolver interface { + Resolve(typ, provider, platformDir string) ([]servicebindings.Binding, error) +} -func NewDependencyMappingResolver() DependencyMappingResolver { - return DependencyMappingResolver{} +type DependencyMappingResolver struct { + bindingResolver BindingResolver } -// Reference file structure for bindings directory -// - bindings -// - some-binding -// - type -> dependency-mapping -// - some-sha -> some-uri -// - other-sha -> other-uri +func NewDependencyMappingResolver(bindingResolver BindingResolver) DependencyMappingResolver { + return DependencyMappingResolver{ + bindingResolver: bindingResolver, + } +} -// Given a target dependency, look up if there is a matching dependency mapping at the given binding path -func (d DependencyMappingResolver) FindDependencyMapping(sha256, bindingPath string) (string, error) { - allBindings, err := filepath.Glob(filepath.Join(bindingPath, "*")) +// FindDependencyMapping looks up if there is a matching dependency mapping +func (d DependencyMappingResolver) FindDependencyMapping(sha256, platformDir string) (string, error) { + bindings, err := d.bindingResolver.Resolve("dependency-mapping", "", platformDir) if err != nil { - return "", err + return "", fmt.Errorf("failed to resolve 'dependency-mapping' binding: %w", err) } - for _, binding := range allBindings { - bindType, err := os.ReadFile(filepath.Join(binding, "type")) - if err != nil { - return "", fmt.Errorf("couldn't read binding type: %w", err) - } - - if strings.TrimSpace(string(bindType)) == "dependency-mapping" { - if _, err := os.Stat(filepath.Join(binding, sha256)); err != nil { - if !os.IsNotExist(err) { - return "", err - } - continue - } - - uri, err := os.ReadFile(filepath.Join(binding, sha256)) - if err != nil { - return "", err - } - return strings.TrimSpace(string(uri)), nil + for _, binding := range bindings { + if uri, ok := binding.Entries[sha256]; ok { + return uri.ReadString() } } + return "", nil } diff --git a/postal/internal/dependency_mappings_test.go b/postal/internal/dependency_mappings_test.go index ed0cea6e..dc7ae033 100644 --- a/postal/internal/dependency_mappings_test.go +++ b/postal/internal/dependency_mappings_test.go @@ -6,6 +6,8 @@ import ( "testing" "github.com/paketo-buildpacks/packit/postal/internal" + "github.com/paketo-buildpacks/packit/postal/internal/fakes" + "github.com/paketo-buildpacks/packit/servicebindings" "github.com/sclevine/spec" . "github.com/onsi/gomega" @@ -13,99 +15,72 @@ import ( func testDependencyMappings(t *testing.T, context spec.G, it spec.S) { var ( - Expect = NewWithT(t).Expect - path string - resolver internal.DependencyMappingResolver - bindingPath string - err error + Expect = NewWithT(t).Expect + tmpDir string + resolver internal.DependencyMappingResolver + bindingResolver *fakes.BindingResolver + err error ) it.Before(func() { - resolver = internal.NewDependencyMappingResolver() - bindingPath, err = os.MkdirTemp("", "bindings") + tmpDir, err = os.MkdirTemp("", "dependency-mappings") + Expect(err).NotTo(HaveOccurred()) + Expect(os.WriteFile(filepath.Join(tmpDir, "entry-data"), []byte("dependency-mapping-entry.tgz"), os.ModePerm)) + + bindingResolver = &fakes.BindingResolver{} + resolver = internal.NewDependencyMappingResolver(bindingResolver) Expect(err).NotTo(HaveOccurred()) }) it.After(func() { - Expect(os.RemoveAll(path)).To(Succeed()) + Expect(os.RemoveAll(tmpDir)).To(Succeed()) }) context("FindDependencyMapping", func() { it.Before(func() { - Expect(os.MkdirAll(filepath.Join(bindingPath, "some-binding"), 0700)).To(Succeed()) - Expect(os.WriteFile(filepath.Join(bindingPath, "some-binding", "type"), []byte("dependency-mapping"), 0600)).To(Succeed()) - Expect(os.WriteFile(filepath.Join(bindingPath, "some-binding", "some-sha"), []byte("dependency-mapping-entry.tgz"), 0600)).To(Succeed()) - - Expect(os.MkdirAll(filepath.Join(bindingPath, "other-binding"), 0700)).To(Succeed()) - Expect(os.WriteFile(filepath.Join(bindingPath, "other-binding", "type"), []byte("dependency-mapping"), 0600)).To(Succeed()) - Expect(os.WriteFile(filepath.Join(bindingPath, "other-binding", "other-sha"), []byte("dependency-mapping-entry.tgz"), 0600)).To(Succeed()) - - Expect(os.MkdirAll(filepath.Join(bindingPath, "another-binding"), 0700)).To(Succeed()) - Expect(os.WriteFile(filepath.Join(bindingPath, "another-binding", "type"), []byte("another type"), 0600)).To(Succeed()) - Expect(os.WriteFile(filepath.Join(bindingPath, "another-binding", "some-sha"), []byte("entry.tgz"), 0600)).To(Succeed()) + bindingResolver.ResolveCall.Returns.BindingSlice = []servicebindings.Binding{ + { + Name: "some-binding", + Path: "some-path", + Type: "dependency-mapping", + Entries: map[string]*servicebindings.Entry{ + "some-sha": servicebindings.NewEntry(filepath.Join(tmpDir, "entry-data")), + }, + }, + { + Name: "other-binding", + Path: "other-path", + Type: "dependency-mapping", + Entries: map[string]*servicebindings.Entry{ + "other-sha": servicebindings.NewEntry("some-entry-path"), + }, + }, + { + Name: "another-binding", + Path: "another-path", + Type: "another-type", + Entries: map[string]*servicebindings.Entry{}, + }, + } }) context("given a set of bindings and a dependency", func() { it("finds a matching dependency mappings in the platform bindings if there is one", func() { - boundDependency, err := resolver.FindDependencyMapping("some-sha", bindingPath) + boundDependency, err := resolver.FindDependencyMapping("some-sha", "some-platform-dir") Expect(err).ToNot(HaveOccurred()) + Expect(bindingResolver.ResolveCall.Receives.Typ).To(Equal("dependency-mapping")) + Expect(bindingResolver.ResolveCall.Receives.Provider).To(BeEmpty()) + Expect(bindingResolver.ResolveCall.Receives.PlatformDir).To(Equal("some-platform-dir")) Expect(boundDependency).To(Equal("dependency-mapping-entry.tgz")) }) }) context("given a set of bindings and a dependency", func() { it("returns an empty DependencyMapping if there is no match", func() { - boundDependency, err := resolver.FindDependencyMapping("unmatched-sha", bindingPath) + boundDependency, err := resolver.FindDependencyMapping("unmatched-sha", "") Expect(err).ToNot(HaveOccurred()) Expect(boundDependency).To(Equal("")) }) }) }) - - context("failure cases", func() { - context("when the binding path is a bad pattern", func() { - it("errors", func() { - _, err := resolver.FindDependencyMapping("some-sha", "///") - Expect(err).To(HaveOccurred()) - }) - }) - - context("when type file cannot be opened", func() { - it.Before(func() { - Expect(os.MkdirAll(filepath.Join(bindingPath, "some-binding"), 0700)).To(Succeed()) - Expect(os.WriteFile(filepath.Join(bindingPath, "some-binding", "type"), []byte("dependency-mapping"), 0000)).To(Succeed()) - Expect(os.WriteFile(filepath.Join(bindingPath, "some-binding", "some-sha"), []byte("dependency-mapping-entry.tgz"), 0600)).To(Succeed()) - }) - it("errors", func() { - _, err := resolver.FindDependencyMapping("some-sha", bindingPath) - Expect(err).To(HaveOccurred()) - Expect(err).To(MatchError(ContainSubstring("couldn't read binding type"))) - }) - }) - - context("when SHA256 file cannot be stat", func() { - it.Before(func() { - Expect(os.MkdirAll(filepath.Join(bindingPath, "new-binding"), 0700)).To(Succeed()) - Expect(os.WriteFile(filepath.Join(bindingPath, "new-binding", "type"), []byte("dependency-mapping"), 0644)).To(Succeed()) - Expect(os.WriteFile(filepath.Join(bindingPath, "new-binding", "some-sha"), []byte("dependency-mapping-entry.tgz"), 0644)).To(Succeed()) - Expect(os.Chmod(filepath.Join(bindingPath, "new-binding", "some-sha"), 0000)).To(Succeed()) - }) - it("errors", func() { - _, err := resolver.FindDependencyMapping("some-sha", bindingPath) - Expect(err).To(HaveOccurred()) - }) - }) - - context("when SHA256 contents cannot be opened", func() { - it.Before(func() { - Expect(os.MkdirAll(filepath.Join(bindingPath, "some-binding"), 0700)).To(Succeed()) - Expect(os.WriteFile(filepath.Join(bindingPath, "some-binding", "type"), []byte("dependency-mapping"), 0600)).To(Succeed()) - Expect(os.WriteFile(filepath.Join(bindingPath, "some-binding", "some-sha"), []byte("dependency-mapping-entry.tgz"), 0000)).To(Succeed()) - }) - it("errors", func() { - _, err := resolver.FindDependencyMapping("some-sha", bindingPath) - Expect(err).To(HaveOccurred()) - }) - }) - }) } diff --git a/postal/internal/fakes/binding_resolver.go b/postal/internal/fakes/binding_resolver.go new file mode 100644 index 00000000..8edce015 --- /dev/null +++ b/postal/internal/fakes/binding_resolver.go @@ -0,0 +1,37 @@ +package fakes + +import ( + "sync" + + "github.com/paketo-buildpacks/packit/servicebindings" +) + +type BindingResolver struct { + ResolveCall struct { + mutex sync.Mutex + CallCount int + Receives struct { + Typ string + Provider string + PlatformDir string + } + Returns struct { + BindingSlice []servicebindings.Binding + Error error + } + Stub func(string, string, string) ([]servicebindings.Binding, error) + } +} + +func (f *BindingResolver) Resolve(param1 string, param2 string, param3 string) ([]servicebindings.Binding, error) { + f.ResolveCall.mutex.Lock() + defer f.ResolveCall.mutex.Unlock() + f.ResolveCall.CallCount++ + f.ResolveCall.Receives.Typ = param1 + f.ResolveCall.Receives.Provider = param2 + f.ResolveCall.Receives.PlatformDir = param3 + if f.ResolveCall.Stub != nil { + return f.ResolveCall.Stub(param1, param2, param3) + } + return f.ResolveCall.Returns.BindingSlice, f.ResolveCall.Returns.Error +} diff --git a/postal/service.go b/postal/service.go index 37859a13..3083dfdd 100644 --- a/postal/service.go +++ b/postal/service.go @@ -10,9 +10,11 @@ import ( "time" "github.com/Masterminds/semver/v3" + "github.com/paketo-buildpacks/packit" "github.com/paketo-buildpacks/packit/cargo" "github.com/paketo-buildpacks/packit/postal/internal" + "github.com/paketo-buildpacks/packit/servicebindings" "github.com/paketo-buildpacks/packit/vacation" ) @@ -26,9 +28,9 @@ type Transport interface { //go:generate faux --interface MappingResolver --output fakes/mapping_resolver.go // MappingResolver serves as the interface that looks up platform binding provided -// dependency mappings given a SHA256 and a path to search for bindings +// dependency mappings given a SHA256 type MappingResolver interface { - FindDependencyMapping(SHA256, bindingPath string) (string, error) + FindDependencyMapping(SHA256, platformDir string) (string, error) } // Service provides a mechanism for resolving and installing dependencies given @@ -38,11 +40,13 @@ type Service struct { mappingResolver MappingResolver } -// NewService creates an instance of a Servicel given a Transport. +// NewService creates an instance of a Service given a Transport. func NewService(transport Transport) Service { return Service{ - transport: transport, - mappingResolver: internal.NewDependencyMappingResolver(), + transport: transport, + mappingResolver: internal.NewDependencyMappingResolver( + servicebindings.NewResolver(), + ), } } @@ -140,7 +144,7 @@ func (s Service) Resolve(path, id, version, stack string) (Dependency, error) { // validated against the checksum value provided on the Dependency and will // error if there are inconsistencies in the fetched result. func (s Service) Deliver(dependency Dependency, cnbPath, layerPath, platformPath string) error { - dependencyMappingURI, err := s.mappingResolver.FindDependencyMapping(dependency.SHA256, filepath.Join(platformPath, "bindings")) + dependencyMappingURI, err := s.mappingResolver.FindDependencyMapping(dependency.SHA256, platformPath) if err != nil { return fmt.Errorf("failure checking for dependency mappings: %s", err) } diff --git a/postal/service_test.go b/postal/service_test.go index f8f3a0c1..33cccb80 100644 --- a/postal/service_test.go +++ b/postal/service_test.go @@ -92,9 +92,7 @@ strip-components = 1 mappingResolver = &fakes.MappingResolver{} - service = postal.NewService(transport) - - service = service.WithDependencyMappingResolver(mappingResolver) + service = postal.NewService(transport).WithDependencyMappingResolver(mappingResolver) }) context("Resolve", func() { @@ -319,7 +317,6 @@ version = "this is super not semver" var ( dependencySHA string layerPath string - platformPath string deliver func() error ) @@ -328,9 +325,6 @@ version = "this is super not semver" layerPath, err = os.MkdirTemp("", "layer") Expect(err).NotTo(HaveOccurred()) - platformPath, err = os.MkdirTemp("", "platform") - Expect(err).NotTo(HaveOccurred()) - buffer := bytes.NewBuffer(nil) zw := gzip.NewWriter(buffer) tw := tar.NewWriter(zw) @@ -365,15 +359,17 @@ version = "this is super not semver" transport.DropCall.Returns.ReadCloser = io.NopCloser(buffer) deliver = func() error { - return service.Deliver(postal.Dependency{ - ID: "some-entry", - Stacks: []string{"some-stack"}, - URI: "some-entry.tgz", - SHA256: dependencySHA, - Version: "1.2.3", - }, "some-cnb-path", + return service.Deliver( + postal.Dependency{ + ID: "some-entry", + Stacks: []string{"some-stack"}, + URI: "some-entry.tgz", + SHA256: dependencySHA, + Version: "1.2.3", + }, + "some-cnb-path", layerPath, - platformPath, + "some-platform-dir", ) } }) @@ -389,6 +385,7 @@ version = "this is super not semver" Expect(transport.DropCall.Receives.Root).To(Equal("some-cnb-path")) Expect(transport.DropCall.Receives.Uri).To(Equal("some-entry.tgz")) + Expect(mappingResolver.FindDependencyMappingCall.Receives.PlatformDir).To(Equal("some-platform-dir")) files, err := filepath.Glob(fmt.Sprintf("%s/*", layerPath)) Expect(err).NotTo(HaveOccurred()) @@ -445,16 +442,18 @@ version = "this is super not semver" transport.DropCall.Returns.ReadCloser = io.NopCloser(buffer) deliver = func() error { - return service.Deliver(postal.Dependency{ - ID: "some-entry", - Stacks: []string{"some-stack"}, - URI: "some-entry.tgz", - SHA256: dependencySHA, - Version: "1.2.3", - StripComponents: 1, - }, "some-cnb-path", + return service.Deliver( + postal.Dependency{ + ID: "some-entry", + Stacks: []string{"some-stack"}, + URI: "some-entry.tgz", + SHA256: dependencySHA, + Version: "1.2.3", + StripComponents: 1, + }, + "some-cnb-path", layerPath, - platformPath, + "", ) } }) @@ -502,15 +501,17 @@ version = "this is super not semver" transport.DropCall.Returns.ReadCloser = io.NopCloser(buffer) deliver = func() error { - return service.Deliver(postal.Dependency{ - ID: "some-entry", - Stacks: []string{"some-stack"}, - URI: "https://dependencies.example.com/dependencies/some-file-name.txt", - SHA256: dependencySHA, - Version: "1.2.3", - }, "some-cnb-path", + return service.Deliver( + postal.Dependency{ + ID: "some-entry", + Stacks: []string{"some-stack"}, + URI: "https://dependencies.example.com/dependencies/some-file-name.txt", + SHA256: dependencySHA, + Version: "1.2.3", + }, + "some-cnb-path", layerPath, - platformPath, + "some-platform-dir", ) } }) @@ -547,7 +548,7 @@ version = "this is super not semver" Expect(err).NotTo(HaveOccurred()) Expect(mappingResolver.FindDependencyMappingCall.Receives.SHA256).To(Equal(dependencySHA)) - Expect(mappingResolver.FindDependencyMappingCall.Receives.BindingPath).To(Equal(filepath.Join(platformPath, "bindings"))) + 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")) @@ -622,15 +623,17 @@ version = "this is super not semver" context("when the file checksum does not match", func() { it("fails to create a tar reader", func() { - err := service.Deliver(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", + err := service.Deliver( + 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, - platformPath, + "", ) Expect(err).To(MatchError(ContainSubstring("checksum does not match"))) @@ -760,13 +763,15 @@ version = "this is super not semver" 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", + 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, ) } @@ -810,7 +815,7 @@ version = "this is super not semver" Expect(err).NotTo(HaveOccurred()) Expect(mappingResolver.FindDependencyMappingCall.Receives.SHA256).To(Equal(dependencySHA)) - Expect(mappingResolver.FindDependencyMappingCall.Receives.BindingPath).To(Equal("/platform/bindings")) + 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")) diff --git a/servicebindings/entry.go b/servicebindings/entry.go new file mode 100644 index 00000000..6df41045 --- /dev/null +++ b/servicebindings/entry.go @@ -0,0 +1,59 @@ +package servicebindings + +import ( + "os" +) + +// Entry represents the read-only content of a binding entry. +type Entry struct { + path string + file *os.File +} + +// NewEntry returns a new Entry whose content is given by the file at the provided path. +func NewEntry(path string) *Entry { + return &Entry{ + path: path, + } +} + +// ReadBytes reads the entire raw content of the entry. There is no need to call Close after calling ReadBytes. +func (e *Entry) ReadBytes() ([]byte, error) { + return os.ReadFile(e.path) +} + +// ReadString reads the entire content of the entry as a string. There is no need to call Close after calling +// ReadString. +func (e *Entry) ReadString() (string, error) { + bytes, err := e.ReadBytes() + if err != nil { + return "", err + } + return string(bytes), nil +} + +// Read reads up to len(b) bytes from the entry. It returns the number of bytes read and any error encountered. At end +// of entry data, Read returns 0, io.EOF. +// Close must be called when all read operations are complete. +func (e *Entry) Read(b []byte) (int, error) { + if e.file == nil { + file, err := os.Open(e.path) + if err != nil { + return 0, err + } + e.file = file + } + return e.file.Read(b) +} + +// Close closes the entry and resets it for reading. After calling Close, any subsequent calls to Read will read entry +// data from the beginning. Close may be called on a closed entry without error. +func (e *Entry) Close() error { + if e.file == nil { + return nil + } + defer func() { + e.file = nil + }() + return e.file.Close() +} diff --git a/servicebindings/entry_test.go b/servicebindings/entry_test.go new file mode 100644 index 00000000..83fb990d --- /dev/null +++ b/servicebindings/entry_test.go @@ -0,0 +1,75 @@ +package servicebindings_test + +import ( + "io" + "os" + "path/filepath" + "testing" + + "github.com/paketo-buildpacks/packit/servicebindings" + "github.com/sclevine/spec" + + . "github.com/onsi/gomega" +) + +func testEntry(t *testing.T, context spec.G, it spec.S) { + var ( + Expect = NewWithT(t).Expect + entry *servicebindings.Entry + tmpDir string + ) + + it.Before(func() { + var err error + tmpDir, err = os.MkdirTemp("", "entry") + Expect(err).NotTo(HaveOccurred()) + entryPath := filepath.Join(tmpDir, "entry") + Expect(os.WriteFile(entryPath, []byte("some data"), os.ModePerm)).To(Succeed()) + entry = servicebindings.NewEntry(entryPath) + }) + + it.After(func() { + Expect(os.RemoveAll(tmpDir)).To(Succeed()) + }) + + context("ReadBytes", func() { + it("returns the raw bytes of the entry", func() { + Expect(entry.ReadBytes()).To(Equal([]byte("some data"))) + }) + }) + + context("ReadString", func() { + it("returns the string value of the entry", func() { + Expect(entry.ReadString()).To(Equal("some data")) + }) + }) + + context("usage as an io.ReadCloser", func() { + it("is assignable to io.ReadCloser", func() { + var _ io.ReadCloser = entry + }) + + it("can be read again after closing", func() { + data, err := io.ReadAll(entry) + Expect(err).NotTo(HaveOccurred()) + Expect(entry.Close()).To(Succeed()) + Expect(data).To(Equal([]byte("some data"))) + + data, err = io.ReadAll(entry) + Expect(err).NotTo(HaveOccurred()) + Expect(entry.Close()).To(Succeed()) + Expect(data).To(Equal([]byte("some data"))) + }) + + it("can be closed multiple times in a row", func() { + _, err := io.ReadAll(entry) + Expect(err).NotTo(HaveOccurred()) + Expect(entry.Close()).To(Succeed()) + Expect(entry.Close()).To(Succeed()) + }) + + it("can be closed if never read from", func() { + Expect(entry.Close()).To(Succeed()) + }) + }) +} diff --git a/servicebindings/init_test.go b/servicebindings/init_test.go new file mode 100644 index 00000000..6d1c64d8 --- /dev/null +++ b/servicebindings/init_test.go @@ -0,0 +1,15 @@ +package servicebindings_test + +import ( + "testing" + + "github.com/sclevine/spec" + "github.com/sclevine/spec/report" +) + +func TestUnitServiceBindings(t *testing.T) { + suite := spec.New("packit/servicebindings", spec.Report(report.Terminal{})) + suite("Resolver", testResolver) + suite("Entry", testEntry) + suite.Run(t) +} diff --git a/servicebindings/resolver.go b/servicebindings/resolver.go new file mode 100644 index 00000000..af0e5043 --- /dev/null +++ b/servicebindings/resolver.go @@ -0,0 +1,241 @@ +package servicebindings + +import ( + "errors" + "fmt" + "os" + "path/filepath" + "strings" +) + +// Binding represents metadata related to an external service. +type Binding struct { + + // Name is the name of the binding. + Name string + + // Path is the path to the binding directory. + Path string + + // Type is the type of the binding. + Type string + + // Provider is the provider of the binding. + Provider string + + // Entries is the set of entries that make up the binding. + Entries map[string]*Entry +} + +// Resolver resolves service bindings according to the kubernetes binding spec: +// https://github.com/k8s-service-bindings/spec#workload-projection. +// +// It also supports backwards compatibility with the legacy service binding spec: +// https://github.com/buildpacks/spec/blob/main/extensions/bindings.md +type Resolver struct { + bindingRoot string + bindings []Binding +} + +// NewResolver returns a new service binding resolver. +func NewResolver() *Resolver { + return &Resolver{} +} + +// Resolve returns all bindings matching the given type and optional provider (case-insensitive). To match on type only, +// provider may be an empty string. Returns an error if there are problems loading bindings from the file system. +// +// The location of bindings is given by one of the following, in order of precedence: +// +// 1. SERVICE_BINDING_ROOT environment variable +// 2. CNB_BINDINGS environment variable, if above is not set +// 3. `/bindings`, if both above are not set +func (r *Resolver) Resolve(typ, provider, platformDir string) ([]Binding, error) { + if newRoot := bindingRoot(platformDir); r.bindingRoot != newRoot { + r.bindingRoot = newRoot + bindings, err := loadBindings(r.bindingRoot) + if err != nil { + return nil, fmt.Errorf("failed to load bindings from '%s': %w", r.bindingRoot, err) + } + r.bindings = bindings + } + + var resolved []Binding + for _, binding := range r.bindings { + if (strings.EqualFold(binding.Type, typ)) && + (provider == "" || strings.EqualFold(binding.Provider, provider)) { + resolved = append(resolved, binding) + } + } + return resolved, nil +} + +// ResolveOne returns a single binding matching the given type and optional provider (case-insensitive). To match on +// type only, provider may be an empty string. Returns an error if the number of matched bindings is not exactly one, or +// if there are problems loading bindings from the file system. +// +// The location of bindings is given by one of the following, in order of precedence: +// +// 1. SERVICE_BINDING_ROOT environment variable +// 2. CNB_BINDINGS environment variable, if above is not set +// 3. `/bindings`, if both above are not set + +func (r *Resolver) ResolveOne(typ, provider, platformDir string) (Binding, error) { + bindings, err := r.Resolve(typ, provider, platformDir) + if err != nil { + return Binding{}, err + } + if len(bindings) != 1 { + return Binding{}, fmt.Errorf("found %d bindings for type '%s' and provider '%s' but expected exactly 1", len(bindings), typ, provider) + } + return bindings[0], nil +} + +func loadBindings(bindingRoot string) ([]Binding, error) { + files, err := os.ReadDir(bindingRoot) + if err != nil { + return nil, err + } + + var bindings []Binding + for _, file := range files { + isLegacy, err := isLegacyBinding(bindingRoot, file.Name()) + if err != nil { + return nil, err + } + + var binding Binding + if isLegacy { + binding, err = loadLegacyBinding(bindingRoot, file.Name()) + } else { + binding, err = loadBinding(bindingRoot, file.Name()) + } + if err != nil { + return nil, fmt.Errorf("failed to read binding '%s': %w", file.Name(), err) + } + bindings = append(bindings, binding) + } + return bindings, nil +} + +func bindingRoot(platformDir string) string { + root := os.Getenv("SERVICE_BINDING_ROOT") + if root == "" { + root = os.Getenv("CNB_BINDINGS") + } + + if root == "" { + root = filepath.Join(platformDir, "bindings") + } + return root +} + +// According to the legacy spec (https://github.com/buildpacks/spec/blob/main/extensions/bindings.md), a legacy binding +// has a `metadata` directory within the binding path. +func isLegacyBinding(bindingRoot, name string) (bool, error) { + info, err := os.Stat(filepath.Join(bindingRoot, name, "metadata")) + if err == nil { + return info.IsDir(), nil + } else if os.IsNotExist(err) { + return false, nil + } + return false, err +} + +// See: https://github.com/k8s-service-bindings/spec#workload-projection +func loadBinding(bindingRoot, name string) (Binding, error) { + binding := Binding{ + Name: name, + Path: filepath.Join(bindingRoot, name), + Entries: map[string]*Entry{}, + } + + entries, err := loadEntries(filepath.Join(binding.Path)) + if err != nil { + return Binding{}, err + } + + typ, ok := entries["type"] + if !ok { + return Binding{}, errors.New("missing 'type'") + } + binding.Type, err = typ.ReadString() + if err != nil { + return Binding{}, err + } + delete(entries, "type") + + provider, ok := entries["provider"] + if ok { + binding.Provider, err = provider.ReadString() + if err != nil { + return Binding{}, err + } + delete(entries, "provider") + } + + binding.Entries = entries + + return binding, nil +} + +// See: https://github.com/buildpacks/spec/blob/main/extensions/bindings.md +func loadLegacyBinding(bindingRoot, name string) (Binding, error) { + binding := Binding{ + Name: name, + Path: filepath.Join(bindingRoot, name), + Entries: map[string]*Entry{}, + } + + metadata, err := loadEntries(filepath.Join(binding.Path, "metadata")) + if err != nil { + return Binding{}, err + } + + typ, ok := metadata["kind"] + if !ok { + return Binding{}, errors.New("missing 'kind'") + } + binding.Type, err = typ.ReadString() + if err != nil { + return Binding{}, err + } + delete(metadata, "kind") + + provider, ok := metadata["provider"] + if !ok { + return Binding{}, errors.New("missing 'provider'") + } + binding.Provider, err = provider.ReadString() + if err != nil { + return Binding{}, err + } + delete(metadata, "provider") + + binding.Entries = metadata + + secrets, err := loadEntries(filepath.Join(binding.Path, "secret")) + if err != nil && !os.IsNotExist(err) { + return Binding{}, err + } + if err == nil { + for k, v := range secrets { + binding.Entries[k] = v + } + } + + return binding, nil +} + +func loadEntries(path string) (map[string]*Entry, error) { + entries := map[string]*Entry{} + files, err := os.ReadDir(path) + if err != nil { + return nil, err + } + + for _, file := range files { + entries[file.Name()] = NewEntry(filepath.Join(path, file.Name())) + } + return entries, nil +} diff --git a/servicebindings/resolver_test.go b/servicebindings/resolver_test.go new file mode 100644 index 00000000..9e9123b3 --- /dev/null +++ b/servicebindings/resolver_test.go @@ -0,0 +1,441 @@ +package servicebindings_test + +import ( + "os" + "path/filepath" + "testing" + + . "github.com/onsi/gomega" + "github.com/sclevine/spec" + + "github.com/paketo-buildpacks/packit/servicebindings" +) + +func testResolver(t *testing.T, context spec.G, it spec.S) { + var Expect = NewWithT(t).Expect + + context("binding root precedence", func() { + var ( + bindingRootK8s string + bindingRootCNB string + platformDir string + ) + + it.Before(func() { + var err error + + bindingRootK8s, err = os.MkdirTemp("", "bindings-k8s") + Expect(err).NotTo(HaveOccurred()) + + err = os.MkdirAll(filepath.Join(bindingRootK8s, "some-binding"), os.ModePerm) + Expect(err).NotTo(HaveOccurred()) + + err = os.WriteFile(filepath.Join(bindingRootK8s, "some-binding", "type"), []byte("some-type"), os.ModePerm) + Expect(err).NotTo(HaveOccurred()) + + bindingRootCNB, err = os.MkdirTemp("", "bindings-cnb") + Expect(err).NotTo(HaveOccurred()) + + err = os.MkdirAll(filepath.Join(bindingRootCNB, "some-binding"), os.ModePerm) + Expect(err).NotTo(HaveOccurred()) + + err = os.WriteFile(filepath.Join(bindingRootCNB, "some-binding", "type"), []byte("some-type"), os.ModePerm) + Expect(err).NotTo(HaveOccurred()) + + platformDir, err = os.MkdirTemp("", "bindings-platform") + Expect(err).NotTo(HaveOccurred()) + + err = os.MkdirAll(filepath.Join(platformDir, "bindings", "some-binding"), os.ModePerm) + Expect(err).NotTo(HaveOccurred()) + + err = os.WriteFile(filepath.Join(platformDir, "bindings", "some-binding", "type"), []byte("some-type"), os.ModePerm) + Expect(err).NotTo(HaveOccurred()) + }) + + context("SERVICE_BINDING_ROOT env var is set", func() { + it.Before(func() { + Expect(os.Setenv("SERVICE_BINDING_ROOT", bindingRootK8s)).To(Succeed()) + }) + + context("CNB_BINDINGS env var is set", func() { + it.Before(func() { + Expect(os.Setenv("CNB_BINDINGS", bindingRootCNB)).To(Succeed()) + }) + + it("resolves bindings from SERVICE_BINDING_ROOT", func() { + resolver := servicebindings.NewResolver() + + bindings, err := resolver.Resolve("some-type", "", platformDir) + Expect(err).NotTo(HaveOccurred()) + Expect(bindings).To(ConsistOf( + servicebindings.Binding{ + Name: "some-binding", + Path: filepath.Join(bindingRootK8s, "some-binding"), + Type: "some-type", + Entries: map[string]*servicebindings.Entry{}, + }, + )) + }) + }) + + context("CNB_BINDINGS env var is not set", func() { + it.Before(func() { + Expect(os.Unsetenv("CNB_BINDINGS")).To(Succeed()) + }) + + it("resolves bindings from SERVICE_BINDING_ROOT", func() { + resolver := servicebindings.NewResolver() + + bindings, err := resolver.Resolve("some-type", "", platformDir) + Expect(err).NotTo(HaveOccurred()) + Expect(bindings).To(ConsistOf( + servicebindings.Binding{ + Name: "some-binding", + Path: filepath.Join(bindingRootK8s, "some-binding"), + Type: "some-type", + Entries: map[string]*servicebindings.Entry{}, + }, + )) + }) + }) + }) + + context("SERVICE_BINDING_ROOT env var is not set", func() { + it.Before(func() { + Expect(os.Unsetenv("SERVICE_BINDING_ROOT")).To(Succeed()) + }) + + context("CNB_BINDINGS env var is set", func() { + it.Before(func() { + Expect(os.Setenv("CNB_BINDINGS", bindingRootCNB)).To(Succeed()) + }) + + it("resolves bindings from CNB_BINDINGS", func() { + resolver := servicebindings.NewResolver() + + bindings, err := resolver.Resolve("some-type", "", platformDir) + Expect(err).NotTo(HaveOccurred()) + Expect(bindings).To(ConsistOf( + servicebindings.Binding{ + Name: "some-binding", + Path: filepath.Join(bindingRootCNB, "some-binding"), + Type: "some-type", + Entries: map[string]*servicebindings.Entry{}, + }, + )) + }) + }) + + context("CNB_BINDINGS env var is not set", func() { + it.Before(func() { + Expect(os.Unsetenv("CNB_BINDINGS")).To(Succeed()) + }) + + it("resolves bindings from platform dir", func() { + resolver := servicebindings.NewResolver() + + bindings, err := resolver.Resolve("some-type", "", platformDir) + Expect(err).NotTo(HaveOccurred()) + Expect(bindings).To(ConsistOf( + servicebindings.Binding{ + Name: "some-binding", + Path: filepath.Join(platformDir, "bindings", "some-binding"), + Type: "some-type", + Entries: map[string]*servicebindings.Entry{}, + }, + )) + }) + }) + }) + }) + + context("resolving bindings", func() { + var bindingRoot string + var resolver *servicebindings.Resolver + + it.Before(func() { + var err error + bindingRoot, err = os.MkdirTemp("", "bindings") + Expect(err).NotTo(HaveOccurred()) + Expect(os.Setenv("SERVICE_BINDING_ROOT", bindingRoot)).To(Succeed()) + + resolver = servicebindings.NewResolver() + + err = os.MkdirAll(filepath.Join(bindingRoot, "binding-1A"), os.ModePerm) + Expect(err).NotTo(HaveOccurred()) + + err = os.WriteFile(filepath.Join(bindingRoot, "binding-1A", "type"), []byte("type-1"), os.ModePerm) + Expect(err).NotTo(HaveOccurred()) + + err = os.WriteFile(filepath.Join(bindingRoot, "binding-1A", "provider"), []byte("provider-1A"), os.ModePerm) + Expect(err).NotTo(HaveOccurred()) + + err = os.WriteFile(filepath.Join(bindingRoot, "binding-1A", "username"), nil, os.ModePerm) + Expect(err).NotTo(HaveOccurred()) + + err = os.WriteFile(filepath.Join(bindingRoot, "binding-1A", "password"), nil, os.ModePerm) + Expect(err).NotTo(HaveOccurred()) + + err = os.MkdirAll(filepath.Join(bindingRoot, "binding-1B"), os.ModePerm) + Expect(err).NotTo(HaveOccurred()) + + err = os.WriteFile(filepath.Join(bindingRoot, "binding-1B", "type"), []byte("type-1"), os.ModePerm) + Expect(err).NotTo(HaveOccurred()) + + err = os.WriteFile(filepath.Join(bindingRoot, "binding-1B", "provider"), []byte("provider-1B"), os.ModePerm) + Expect(err).NotTo(HaveOccurred()) + + err = os.WriteFile(filepath.Join(bindingRoot, "binding-1B", "username"), nil, os.ModePerm) + Expect(err).NotTo(HaveOccurred()) + + err = os.WriteFile(filepath.Join(bindingRoot, "binding-1B", "password"), nil, os.ModePerm) + Expect(err).NotTo(HaveOccurred()) + + err = os.MkdirAll(filepath.Join(bindingRoot, "binding-2"), os.ModePerm) + Expect(err).NotTo(HaveOccurred()) + + err = os.WriteFile(filepath.Join(bindingRoot, "binding-2", "type"), []byte("type-2"), os.ModePerm) + Expect(err).NotTo(HaveOccurred()) + + err = os.WriteFile(filepath.Join(bindingRoot, "binding-2", "provider"), []byte("provider-2"), os.ModePerm) + Expect(err).NotTo(HaveOccurred()) + + err = os.WriteFile(filepath.Join(bindingRoot, "binding-2", "username"), nil, os.ModePerm) + Expect(err).NotTo(HaveOccurred()) + + err = os.WriteFile(filepath.Join(bindingRoot, "binding-2", "password"), nil, os.ModePerm) + Expect(err).NotTo(HaveOccurred()) + }) + + it.After(func() { + Expect(os.RemoveAll(bindingRoot)).To(Succeed()) + Expect(os.Unsetenv("SERVICE_BINDING_ROOT")).To(Succeed()) + }) + + context("Resolve", func() { + it("resolves by type only (case-insensitive)", func() { + bindings, err := resolver.Resolve("TyPe-1", "", "") + Expect(err).NotTo(HaveOccurred()) + + Expect(bindings).To(ConsistOf( + servicebindings.Binding{ + Name: "binding-1A", + Path: filepath.Join(bindingRoot, "binding-1A"), + Type: "type-1", + Provider: "provider-1A", + Entries: map[string]*servicebindings.Entry{ + "username": servicebindings.NewEntry(filepath.Join(bindingRoot, "binding-1A", "username")), + "password": servicebindings.NewEntry(filepath.Join(bindingRoot, "binding-1A", "password")), + }, + }, + servicebindings.Binding{ + Name: "binding-1B", + Path: filepath.Join(bindingRoot, "binding-1B"), + Type: "type-1", + Provider: "provider-1B", + Entries: map[string]*servicebindings.Entry{ + "username": servicebindings.NewEntry(filepath.Join(bindingRoot, "binding-1B", "username")), + "password": servicebindings.NewEntry(filepath.Join(bindingRoot, "binding-1B", "password")), + }, + }, + )) + }) + + it("resolves by type and provider (case-insensitive)", func() { + bindings, err := resolver.Resolve("TyPe-1", "PrOvIdEr-1B", "") + Expect(err).NotTo(HaveOccurred()) + + Expect(bindings).To(ConsistOf( + servicebindings.Binding{ + Name: "binding-1B", + Path: filepath.Join(bindingRoot, "binding-1B"), + Type: "type-1", + Provider: "provider-1B", + Entries: map[string]*servicebindings.Entry{ + "username": servicebindings.NewEntry(filepath.Join(bindingRoot, "binding-1B", "username")), + "password": servicebindings.NewEntry(filepath.Join(bindingRoot, "binding-1B", "password")), + }, + }, + )) + }) + + it("allows 'metadata' as an entry name", func() { + err := os.MkdirAll(filepath.Join(bindingRoot, "binding-metadata"), os.ModePerm) + Expect(err).NotTo(HaveOccurred()) + + err = os.WriteFile(filepath.Join(bindingRoot, "binding-metadata", "type"), []byte("type-metadata"), os.ModePerm) + Expect(err).NotTo(HaveOccurred()) + + err = os.WriteFile(filepath.Join(bindingRoot, "binding-metadata", "metadata"), nil, os.ModePerm) + Expect(err).NotTo(HaveOccurred()) + + bindings, err := resolver.Resolve("type-metadata", "", "") + Expect(err).NotTo(HaveOccurred()) + + Expect(bindings).To(ConsistOf( + servicebindings.Binding{ + Name: "binding-metadata", + Path: filepath.Join(bindingRoot, "binding-metadata"), + Type: "type-metadata", + Entries: map[string]*servicebindings.Entry{ + "metadata": servicebindings.NewEntry(filepath.Join(bindingRoot, "binding-metadata", "metadata")), + }, + }, + )) + }) + + it("returns an error if type is missing", func() { + err := os.MkdirAll(filepath.Join(bindingRoot, "bad-binding"), os.ModePerm) + Expect(err).NotTo(HaveOccurred()) + + _, err = resolver.Resolve("bad-type", "", "") + Expect(err).To(MatchError(HavePrefix("failed to load bindings from '%s': failed to read binding 'bad-binding': missing 'type'", bindingRoot))) + }) + + it("allows provider to be omitted", func() { + err := os.MkdirAll(filepath.Join(bindingRoot, "some-binding"), os.ModePerm) + Expect(err).NotTo(HaveOccurred()) + + err = os.WriteFile(filepath.Join(bindingRoot, "some-binding", "type"), []byte("some-type"), os.ModePerm) + Expect(err).NotTo(HaveOccurred()) + + bindings, err := resolver.Resolve("some-type", "", "") + Expect(err).NotTo(HaveOccurred()) + + Expect(bindings).To(ConsistOf( + servicebindings.Binding{ + Name: "some-binding", + Path: filepath.Join(bindingRoot, "some-binding"), + Type: "some-type", + Provider: "", + Entries: map[string]*servicebindings.Entry{}, + }, + )) + }) + + it("returns errors encountered reading files", func() { + err := os.MkdirAll(filepath.Join(bindingRoot, "bad-binding"), os.ModePerm) + Expect(err).NotTo(HaveOccurred()) + + err = os.WriteFile(filepath.Join(bindingRoot, "bad-binding", "type"), []byte("bad-type"), 000) + Expect(err).NotTo(HaveOccurred()) + + _, err = resolver.Resolve("bad-type", "", "") + Expect(err).To(MatchError(HavePrefix("failed to load bindings from '%s': failed to read binding 'bad-binding': open %s: permission denied", bindingRoot, filepath.Join(bindingRoot, "bad-binding", "type")))) + }) + }) + + context("ResolveOne", func() { + it("resolves one binding (case-insensitive)", func() { + binding, err := resolver.ResolveOne("TyPe-2", "", "") + Expect(err).NotTo(HaveOccurred()) + Expect(binding).To(Equal(servicebindings.Binding{ + Name: "binding-2", + Path: filepath.Join(bindingRoot, "binding-2"), + Type: "type-2", + Provider: "provider-2", + Entries: map[string]*servicebindings.Entry{ + "username": servicebindings.NewEntry(filepath.Join(bindingRoot, "binding-2", "username")), + "password": servicebindings.NewEntry(filepath.Join(bindingRoot, "binding-2", "password")), + }, + })) + }) + + it("returns an error if no matches", func() { + _, err := resolver.ResolveOne("non-existent-type", "non-existent-provider", "") + Expect(err).To(MatchError("found 0 bindings for type 'non-existent-type' and provider 'non-existent-provider' but expected exactly 1")) + }) + + it("returns an error if more than one match", func() { + _, err := resolver.ResolveOne("TyPe-1", "", "") + Expect(err).To(MatchError("found 2 bindings for type 'TyPe-1' and provider '' but expected exactly 1")) + }) + }) + + context("legacy bindings", func() { + it("resolves legacy bindings", func() { + err := os.MkdirAll(filepath.Join(bindingRoot, "binding-legacy", "metadata"), os.ModePerm) + Expect(err).NotTo(HaveOccurred()) + + err = os.MkdirAll(filepath.Join(bindingRoot, "binding-legacy", "secret"), os.ModePerm) + Expect(err).NotTo(HaveOccurred()) + + err = os.WriteFile(filepath.Join(bindingRoot, "binding-legacy", "metadata", "kind"), []byte("type-legacy"), os.ModePerm) + Expect(err).NotTo(HaveOccurred()) + + err = os.WriteFile(filepath.Join(bindingRoot, "binding-legacy", "metadata", "provider"), []byte("provider-legacy"), os.ModePerm) + Expect(err).NotTo(HaveOccurred()) + + err = os.WriteFile(filepath.Join(bindingRoot, "binding-legacy", "metadata", "username"), nil, os.ModePerm) + Expect(err).NotTo(HaveOccurred()) + + err = os.WriteFile(filepath.Join(bindingRoot, "binding-legacy", "secret", "password"), nil, os.ModePerm) + Expect(err).NotTo(HaveOccurred()) + + bindings, err := resolver.Resolve("type-legacy", "", "") + Expect(err).NotTo(HaveOccurred()) + + Expect(bindings).To(ConsistOf( + servicebindings.Binding{ + Name: "binding-legacy", + Path: filepath.Join(bindingRoot, "binding-legacy"), + Type: "type-legacy", + Provider: "provider-legacy", + Entries: map[string]*servicebindings.Entry{ + "username": servicebindings.NewEntry(filepath.Join(bindingRoot, "binding-legacy", "metadata", "username")), + "password": servicebindings.NewEntry(filepath.Join(bindingRoot, "binding-legacy", "secret", "password")), + }, + }, + )) + }) + + it("allows 'secret' directory to be omitted", func() { + err := os.MkdirAll(filepath.Join(bindingRoot, "binding-legacy", "metadata"), os.ModePerm) + Expect(err).NotTo(HaveOccurred()) + + err = os.WriteFile(filepath.Join(bindingRoot, "binding-legacy", "metadata", "kind"), []byte("type-legacy"), os.ModePerm) + Expect(err).NotTo(HaveOccurred()) + + err = os.WriteFile(filepath.Join(bindingRoot, "binding-legacy", "metadata", "provider"), []byte("provider-legacy"), os.ModePerm) + Expect(err).NotTo(HaveOccurred()) + + err = os.WriteFile(filepath.Join(bindingRoot, "binding-legacy", "metadata", "some-key"), nil, os.ModePerm) + Expect(err).NotTo(HaveOccurred()) + + bindings, err := resolver.Resolve("type-legacy", "", "") + Expect(err).NotTo(HaveOccurred()) + + Expect(bindings).To(ConsistOf( + servicebindings.Binding{ + Name: "binding-legacy", + Path: filepath.Join(bindingRoot, "binding-legacy"), + Type: "type-legacy", + Provider: "provider-legacy", + Entries: map[string]*servicebindings.Entry{ + "some-key": servicebindings.NewEntry(filepath.Join(bindingRoot, "binding-legacy", "metadata", "some-key")), + }, + }, + )) + }) + + it("returns an error if kind is missing", func() { + err := os.MkdirAll(filepath.Join(bindingRoot, "bad-binding", "metadata"), os.ModePerm) + Expect(err).NotTo(HaveOccurred()) + + _, err = resolver.Resolve("bad-type", "", "") + Expect(err).To(MatchError(HavePrefix("failed to load bindings from '%s': failed to read binding 'bad-binding': missing 'kind'", bindingRoot))) + }) + + it("returns an error if provider is missing", func() { + err := os.MkdirAll(filepath.Join(bindingRoot, "bad-binding", "metadata"), os.ModePerm) + Expect(err).NotTo(HaveOccurred()) + + err = os.WriteFile(filepath.Join(bindingRoot, "bad-binding", "metadata", "kind"), []byte("bad-type"), os.ModePerm) + Expect(err).NotTo(HaveOccurred()) + + _, err = resolver.Resolve("bad-type", "", "") + Expect(err).To(MatchError(HavePrefix("failed to load bindings from '%s': failed to read binding 'bad-binding': missing 'provider'", bindingRoot))) + }) + }) + }) +}