Skip to content

Commit

Permalink
Adds service binding resolver
Browse files Browse the repository at this point in the history
  • Loading branch information
menehune23 committed Sep 15, 2021
1 parent 9dbcc2e commit c05d818
Show file tree
Hide file tree
Showing 8 changed files with 413 additions and 64 deletions.
14 changes: 14 additions & 0 deletions bindings/init_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package bindings_test

import (
"testing"

"github.com/sclevine/spec"
"github.com/sclevine/spec/report"
)

func TestUnitBindings(t *testing.T) {
suite := spec.New("packit/bindings", spec.Report(report.Terminal{}))
suite("Resolver", testResolver)
suite.Run(t)
}
133 changes: 133 additions & 0 deletions bindings/resolver.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
package bindings

import (
"fmt"
"github.com/pkg/errors"
"io/ioutil"
"os"
"path/filepath"
"strings"
)

// Binding represents metadata related to an external service.
type Binding struct {

// Name is the name of the binding, given by the binding directory name.
Name string

// Path is the path to the binding directory.
Path string

// Type is the type of the binding, given by the content of the 'type' file within the binding directory.
Type string

// Provider is the provider of the binding, given by the content of the 'provider' file within the binding
// directory.
Provider string

// Secret is the primary content of the binding. Keys are given by each file name within the binding directory
// (other than 'type' or 'provider'), and corresponding values are given by the content of each file.
Secret map[string][]byte // TODO: place in custom buffer class
}

// Resolver resolves service bindings.
type Resolver struct {
bindingRoot string
bindings []Binding
}

// NewResolver returns a new service binding resolver. If the SERVICE_BINDING_ROOT environment variable is not set, uses
// the provided platform directory to resolve bindings at `<platformDir>/bindings`.
func NewResolver(platformDir string) *Resolver {
root := os.Getenv("SERVICE_BINDING_ROOT")
if root == "" {
root = filepath.Join(platformDir, "bindings")
}

return &Resolver{
bindingRoot: root,
}
}

// 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.
func (r *Resolver) Resolve(typ string, provider string) ([]Binding, error) {
if r.bindings == nil {
bindings, err := loadBindings(r.bindingRoot)
if err != nil {
return nil, errors.Wrapf(err, "loading bindings from '%s'", r.bindingRoot)
}
r.bindings = bindings
}

var resolved []Binding
for _, bind := range r.bindings {
if (strings.ToLower(bind.Type) == strings.ToLower(typ)) &&
(provider == "" || strings.ToLower(bind.Provider) == strings.ToLower(provider)) {
resolved = append(resolved, bind)
}
}
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.
func (r *Resolver) ResolveOne(typ string, provider string) (Binding, error) {
binds, err := r.Resolve(typ, provider)
if err != nil {
return Binding{}, err
}
if len(binds) != 1 {
return Binding{}, fmt.Errorf("found %d bindings for type '%s' and provider '%s' but expected exactly 1", len(binds), typ, provider)
}
return binds[0], nil
}

func loadBindings(bindingRoot string) ([]Binding, error) {
files, err := ioutil.ReadDir(bindingRoot)
if err != nil {
return nil, err
}

var bindings []Binding
for _, file := range files {
binding, err := loadBinding(bindingRoot, file.Name())
if err != nil {
return nil, err
}
bindings = append(bindings, binding)
}
return bindings, nil
}

func loadBinding(bindingRoot, name string) (Binding, error) {
binding := Binding{
Name: name,
Path: filepath.Join(bindingRoot, name),
Secret: map[string][]byte{},
}

files, err := ioutil.ReadDir(binding.Path)
if err != nil {
return Binding{}, nil
}

for _, file := range files {
content, err := os.ReadFile(filepath.Join(binding.Path, file.Name()))
if err != nil {
return Binding{}, err
}

switch file.Name() {
case "type":
binding.Type = string(content)
case "provider":
binding.Provider = string(content)
default:
binding.Secret[file.Name()] = content
}
}

return binding, nil
}
249 changes: 249 additions & 0 deletions bindings/resolver_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
package bindings_test

import (
"github.com/paketo-buildpacks/packit/bindings"
"github.com/sclevine/spec"
"os"
"path/filepath"
"testing"

. "github.com/onsi/gomega"
)

func testResolver(t *testing.T, context spec.G, it spec.S) {
var Expect = NewWithT(t).Expect

context("NewResolver", func() {
var (
bindingRoot string
platformDir string
)

it.Before(func() {
var err error

bindingRoot, err = os.MkdirTemp("", "bindings")
Expect(err).NotTo(HaveOccurred())

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())

platformDir, err = os.MkdirTemp("", "bindings")
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 is set", func() {
it.Before(func() {
Expect(os.Setenv("SERVICE_BINDING_ROOT", bindingRoot)).To(Succeed())
})

it("uses env var value for binding root", func() {
resolver := bindings.NewResolver(platformDir)

binds, err := resolver.Resolve("some-type", "")
Expect(err).NotTo(HaveOccurred())
Expect(binds).To(ConsistOf(
bindings.Binding{
Name: "some-binding",
Path: filepath.Join(bindingRoot, "some-binding"),
Type: "some-type",
Secret: map[string][]byte{},
},
))
})

})

context("SERVICE_BINDING_ROOT is unset", func() {
it.Before(func() {
Expect(os.Unsetenv("SERVICE_BINDING_ROOT")).To(Succeed())
})

it("uses '<platform>/bindings' for binding root", func() {
resolver := bindings.NewResolver(platformDir)

binds, err := resolver.Resolve("some-type", "")
Expect(err).NotTo(HaveOccurred())
Expect(binds).To(ConsistOf(
bindings.Binding{
Name: "some-binding",
Path: filepath.Join(platformDir, "bindings", "some-binding"),
Type: "some-type",
Secret: map[string][]byte{},
},
))
})

})

})

context("resolving bindings", func() {
var bindingRoot string
var resolver *bindings.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 = bindings.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"), []byte("username-1A"), os.ModePerm)
Expect(err).NotTo(HaveOccurred())

err = os.WriteFile(filepath.Join(bindingRoot, "binding-1A", "password"), []byte("password-1A"), 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"), []byte("username-1B"), os.ModePerm)
Expect(err).NotTo(HaveOccurred())

err = os.WriteFile(filepath.Join(bindingRoot, "binding-1B", "password"), []byte("password-1B"), 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"), []byte("username-2"), os.ModePerm)
Expect(err).NotTo(HaveOccurred())

err = os.WriteFile(filepath.Join(bindingRoot, "binding-2", "password"), []byte("password-2"), 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() {
binds, err := resolver.Resolve("TyPe-1", "")
Expect(err).NotTo(HaveOccurred())

Expect(binds).To(ConsistOf(
bindings.Binding{
Name: "binding-1A",
Path: filepath.Join(bindingRoot, "binding-1A"),
Type: "type-1",
Provider: "provider-1A",
Secret: map[string][]byte{
"username": []byte("username-1A"),
"password": []byte("password-1A"),
},
},
bindings.Binding{
Name: "binding-1B",
Path: filepath.Join(bindingRoot, "binding-1B"),
Type: "type-1",
Provider: "provider-1B",
Secret: map[string][]byte{
"username": []byte("username-1B"),
"password": []byte("password-1B"),
},
},
))
})

it("resolves by type and provider (case-insensitive)", func() {
binds, err := resolver.Resolve("TyPe-1", "PrOvIdEr-1B")
Expect(err).NotTo(HaveOccurred())

Expect(binds).To(ConsistOf(
bindings.Binding{
Name: "binding-1B",
Path: filepath.Join(bindingRoot, "binding-1B"),
Type: "type-1",
Provider: "provider-1B",
Secret: map[string][]byte{
"username": []byte("username-1B"),
"password": []byte("password-1B"),
},
},
))
})

it("returns errors encountered while reading binding", 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)

_, err = resolver.Resolve("bad-type", "")
Expect(err).To(MatchError(HavePrefix("loading bindings from '%s': open %s: permission denied", bindingRoot, filepath.Join(bindingRoot, "bad-binding", "type"))))
})
})

context("ResolveOne", func() {
it("resolves one binding (case-insensitive)", func() {
bind, err := resolver.ResolveOne("TyPe-2", "")
Expect(err).NotTo(HaveOccurred())
Expect(bind).To(Equal(bindings.Binding{
Name: "binding-2",
Path: filepath.Join(bindingRoot, "binding-2"),
Type: "type-2",
Provider: "provider-2",
Secret: map[string][]byte{
"username": []byte("username-2"),
"password": []byte("password-2"),
},
}))
})

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"))
})

it("returns errors encountered while reading binding", 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)

_, err = resolver.Resolve("bad-type", "")
Expect(err).To(MatchError(HavePrefix("loading bindings from '%s': open %s: permission denied", bindingRoot, filepath.Join(bindingRoot, "bad-binding", "type"))))
})

})
})
}
Loading

0 comments on commit c05d818

Please sign in to comment.