-
Notifications
You must be signed in to change notification settings - Fork 26
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
9dbcc2e
commit f996636
Showing
8 changed files
with
413 additions
and
64 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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")))) | ||
}) | ||
|
||
}) | ||
}) | ||
} |
Oops, something went wrong.