Skip to content

Commit

Permalink
Support reading service bindings from VCAP_SERVICES env var (#566)
Browse files Browse the repository at this point in the history
Co-authored-by: Johannes Dillmann <j.dillmann@sap.com>
  • Loading branch information
pbusko and modulo11 committed May 2, 2024
1 parent 35d8f76 commit 13393ec
Show file tree
Hide file tree
Showing 5 changed files with 228 additions and 15 deletions.
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:examplepass@postgres.example.com: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": []
}
]
}

0 comments on commit 13393ec

Please sign in to comment.