Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support reading service bindings from VCAP_SERVICES env var #566

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 42 additions & 9 deletions servicebindings/entry.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
package servicebindings

import (
"bytes"
"io"
"os"
)

// Entry represents the read-only content of a binding entry.
type Entry struct {
path string
file *os.File
path string
file *os.File
value *bytes.Reader
}

// NewEntry returns a new Entry whose content is given by the file at the provided path.
Expand All @@ -17,25 +20,50 @@ func NewEntry(path string) *Entry {
}
}

// NewWithValue returns a new Entry with predefined value.
func NewWithValue(value []byte) *Entry {
return &Entry{
value: bytes.NewReader(value),
}
}

// 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) {
if e.value != nil {
return io.ReadAll(e.value)
}
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
var bytes []byte
var err error

if e.value != nil {
bytes, err = io.ReadAll(e.value)
if err != nil {
return "", err
}
} else {
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.value != nil {
return e.value.Read(b)
}

if e.file == nil {
file, err := os.Open(e.path)
if err != nil {
Expand All @@ -49,11 +77,16 @@ func (e *Entry) Read(b []byte) (int, error) {
// 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()

if e.value != nil {
_, err := e.value.Seek(0, io.SeekStart)
return err
} else if e.file == nil {
return nil
} else {
return e.file.Close()
}
}
26 changes: 23 additions & 3 deletions servicebindings/entry_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,10 @@ import (

func testEntry(t *testing.T, context spec.G, it spec.S) {
var (
Expect = NewWithT(t).Expect
entry *servicebindings.Entry
tmpDir string
Expect = NewWithT(t).Expect
entry *servicebindings.Entry
entryWithValue *servicebindings.Entry
tmpDir string
)

it.Before(func() {
Expand All @@ -26,6 +27,7 @@ func testEntry(t *testing.T, context spec.G, it spec.S) {
entryPath := filepath.Join(tmpDir, "entry")
Expect(os.WriteFile(entryPath, []byte("some data"), os.ModePerm)).To(Succeed())
entry = servicebindings.NewEntry(entryPath)
entryWithValue = servicebindings.NewWithValue([]byte("value from env"))
})

it.After(func() {
Expand All @@ -35,12 +37,14 @@ func testEntry(t *testing.T, context spec.G, it spec.S) {
context("ReadBytes", func() {
it("returns the raw bytes of the entry", func() {
Expect(entry.ReadBytes()).To(Equal([]byte("some data")))
Expect(entryWithValue.ReadBytes()).To(Equal([]byte("value from env")))
})
})

context("ReadString", func() {
it("returns the string value of the entry", func() {
Expect(entry.ReadString()).To(Equal("some data"))
Expect(entryWithValue.ReadString()).To(Equal("value from env"))
})
})

Expand All @@ -59,17 +63,33 @@ func testEntry(t *testing.T, context spec.G, it spec.S) {
Expect(err).NotTo(HaveOccurred())
Expect(entry.Close()).To(Succeed())
Expect(data).To(Equal([]byte("some data")))

data, err = io.ReadAll(entryWithValue)
Expect(err).NotTo(HaveOccurred())
Expect(entryWithValue.Close()).To(Succeed())
Expect(data).To(Equal([]byte("value from env")))

data, err = io.ReadAll(entryWithValue)
Expect(err).NotTo(HaveOccurred())
Expect(entryWithValue.Close()).To(Succeed())
Expect(data).To(Equal([]byte("value from env")))
})

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

_, err = io.ReadAll(entryWithValue)
Expect(err).NotTo(HaveOccurred())
Expect(entryWithValue.Close()).To(Succeed())
Expect(entryWithValue.Close()).To(Succeed())
})

it("can be closed if never read from", func() {
Expect(entry.Close()).To(Succeed())
Expect(entryWithValue.Close()).To(Succeed())
})
})
}
60 changes: 57 additions & 3 deletions servicebindings/resolver.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package servicebindings

import (
"encoding/json"
"errors"
"fmt"
"os"
Expand Down Expand Up @@ -47,9 +48,9 @@ func NewResolver() *Resolver {
//
// 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. `<platformDir>/bindings`, if both above are not set
// 1. SERVICE_BINDING_ROOT environment variable
// 2. CNB_BINDINGS environment variable, if above is not set
// 3. `<platformDir>/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
Expand Down Expand Up @@ -92,6 +93,10 @@ func (r *Resolver) ResolveOne(typ, provider, platformDir string) (Binding, error
}

func loadBindings(bindingRoot string) ([]Binding, error) {
if vcapEnv, ok := os.LookupEnv("VCAP_SERVICES"); ok {
return loadvcapservicesbinding(vcapEnv)
}

files, err := os.ReadDir(bindingRoot)
if os.IsNotExist(err) {
return nil, nil
Expand Down Expand Up @@ -233,6 +238,55 @@ func loadLegacyBinding(bindingRoot, name string) (Binding, error) {
return binding, nil
}

func loadvcapservicesbinding(content string) ([]Binding, error) {
var contentTyped map[string][]vcapServicesBinding

err := json.Unmarshal([]byte(content), &contentTyped)
if err != nil {
return []Binding{}, err
}

bindings := []Binding{}
for p, bArray := range contentTyped {
for _, b := range bArray {
entries := map[string]*Entry{}
for k, v := range b.Credentials {
entries[k], err = toJSONString(v)
if err != nil {
return nil, err
}
}
bindings = append(bindings, Binding{
Name: b.Name,
Type: b.Label,
Provider: p,
Entries: entries,
})
}
}

return bindings, nil
}

type vcapServicesBinding struct {
Name string `json:"name"`
Label string `json:"label"`
Credentials map[string]interface{} `json:"credentials"`
}

func toJSONString(input interface{}) (*Entry, error) {
switch in := input.(type) {
case string:
return NewWithValue([]byte(in)), nil
default:
jsonProperty, err := json.Marshal(in)
if err != nil {
return nil, err
}
return NewWithValue(jsonProperty), nil
}
}

func loadEntries(path string) (map[string]*Entry, error) {
entries := map[string]*Entry{}
files, err := os.ReadDir(path)
Expand Down
37 changes: 37 additions & 0 deletions servicebindings/resolver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,43 @@ func testResolver(t *testing.T, context spec.G, it spec.S) {
})
})
})

context("VCAP_SERVICES env var is set", func() {
it.Before(func() {
content, err := os.ReadFile("testdata/vcap_services.json")
Expect(err).NotTo(HaveOccurred())
Expect(os.Setenv("VCAP_SERVICES", string(content))).To(Succeed())
})

it.After(func() {
Expect(os.Unsetenv("VCAP_SERVICES")).To(Succeed())
})

context("SERVICE_BINDING_ROOT env var is set", func() {
it.Before(func() {
Expect(os.Setenv("SERVICE_BINDING_ROOT", bindingRootK8s)).To(Succeed())
})

it("resolves bindings from VCAP_SERVICES", func() {
resolver := servicebindings.NewResolver()
bindings, err := resolver.Resolve("postgres", "", platformDir)
Expect(err).NotTo(HaveOccurred())
Expect(bindings).To(ConsistOf(
servicebindings.Binding{
Name: "postgres",
Path: "",
Type: "postgres",
Provider: "postgres",
Entries: map[string]*servicebindings.Entry{
"username": servicebindings.NewWithValue([]byte("foo")),
"password": servicebindings.NewWithValue([]byte("bar")),
"urls": servicebindings.NewWithValue([]byte("{\"example\":\"http://example.com\"}")),
},
},
))
})
})
})
})

context("resolving bindings", func() {
Expand Down
69 changes: 69 additions & 0 deletions servicebindings/testdata/vcap_services.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
{
"elephantsql-provider": [
{
"name": "elephantsql-binding-c6c60",
"binding_guid": "44ceb72f-100b-4f50-87a2-7809c8b42b8d",
"binding_name": "elephantsql-binding-c6c60",
"instance_guid": "391308e8-8586-4c42-b464-c7831aa2ad22",
"instance_name": "elephantsql-c6c60",
"label": "elephantsql-type",
"tags": [
"postgres",
"postgresql",
"relational"
],
"plan": "turtle",
"credentials": {
"uri": "postgres://exampleuser:[email protected]:5432/exampleuser",
"int": 1,
"bool": true
},
"syslog_drain_url": null,
"volume_mounts": []
}
],
"sendgrid-provider": [
{
"name": "mysendgrid",
"binding_guid": "6533b1b6-7916-488d-b286-ca33d3fa0081",
"binding_name": null,
"instance_guid": "8c907d0f-ec0f-44e4-87cf-e23c9ba3925d",
"instance_name": "mysendgrid",
"label": "sendgrid-type",
"tags": [
"smtp"
],
"plan": "free",
"credentials": {
"hostname": "smtp.example.com",
"username": "QvsXMbJ3rK",
"password": "HCHMOYluTv"
},
"syslog_drain_url": null,
"volume_mounts": []
}
],
"postgres": [
{
"name": "postgres",
"label": "postgres",
"plan": "default",
"tags": [
"postgres"
],
"binding_guid": "6533b1b6-7916-488d-b286-ca33d3fa0081",
"binding_name": null,
"instance_guid": "8c907d0f-ec0f-44e4-87cf-e23c9ba3925d",
"credentials": {
"username": "foo",
"password": "bar",
"urls": {
"example": "http://example.com"
}
},
"syslog_drain_url": null,
"volume_mounts": []
}
]
}

Loading