Skip to content

Commit

Permalink
Add provisioned service support (#978)
Browse files Browse the repository at this point in the history
Application can be bound to a provisioned service:

https://github.com/k8s-service-bindings/spec#provisioned-service

* collect.ProvisionedService handler detects if referred service contains a reference to secret containing the bindings
* unit and acceptance tests demonstrate the contact and exposed functionality

Signed-off-by: Predrag Knezevic <pknezevi@redhat.com>
  • Loading branch information
pedjak committed Jun 16, 2021
1 parent 278caf8 commit 212651d
Show file tree
Hide file tree
Showing 12 changed files with 876 additions and 82 deletions.
13 changes: 13 additions & 0 deletions pkg/reconcile/pipeline/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
api "github.com/redhat-developer/service-binding-operator/api/v1alpha1"
"github.com/redhat-developer/service-binding-operator/pkg/binding"
"github.com/redhat-developer/service-binding-operator/pkg/client/kubernetes"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)
Expand Down Expand Up @@ -123,6 +124,9 @@ type Context interface {
// Add binding item to the context
AddBindingItem(item *BindingItem)

// Add bindings to the context
AddBindings(bindings Bindings)

// List binding items that should be projected into application containers
BindingItems() BindingItems

Expand Down Expand Up @@ -168,6 +172,15 @@ type BindingItem struct {
Source Service
}

// a collection of bindings
type Bindings interface {
// available bindgins
Items() (BindingItems, error)

// reference to resource holding the bindings, nil if not persisted in a resource
Source() *v1.ObjectReference
}

// Returns map representation of given list of binding items
func (items *BindingItems) AsMap() map[string]string {
result := make(map[string]string)
Expand Down
2 changes: 2 additions & 0 deletions pkg/reconcile/pipeline/builder/pipeline.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ func Builder() *builder {

var defaultFlow = []pipeline.Handler{
pipeline.HandlerFunc(project.Unbind),
pipeline.HandlerFunc(collect.PreFlight),
pipeline.HandlerFunc(collect.ProvisionedService),
pipeline.HandlerFunc(collect.BindingDefinitions),
pipeline.HandlerFunc(collect.BindingItems),
pipeline.HandlerFunc(collect.OwnedResources),
Expand Down
62 changes: 55 additions & 7 deletions pkg/reconcile/pipeline/context/impl.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/redhat-developer/service-binding-operator/pkg/client/kubernetes"
"github.com/redhat-developer/service-binding-operator/pkg/converter"
"github.com/redhat-developer/service-binding-operator/pkg/reconcile/pipeline"
"github.com/redhat-developer/service-binding-operator/pkg/util"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
Expand All @@ -35,6 +36,8 @@ type impl struct {

bindingItems pipeline.BindingItems

bindings []pipeline.Bindings

retry bool
stop bool
err error
Expand Down Expand Up @@ -167,14 +170,36 @@ func (i *impl) AddBindingItem(item *pipeline.BindingItem) {
}

func (i *impl) BindingItems() pipeline.BindingItems {
return i.bindingItems
var allItems pipeline.BindingItems
for _, b := range i.bindings {
items, err := b.Items()
if err != nil {
continue
}
allItems = append(allItems, items...)
}
if len(i.bindingItems) > 0 {
allItems = append(allItems, i.bindingItems...)
}
return allItems
}

func (i *impl) BindingSecretName() string {
name, _ := i.bindingSecretName()
return name
}

func (i *impl) bindingSecretName() (string, bool) {
if i.UnbindRequested() {
return i.serviceBinding.Status.Secret
return i.serviceBinding.Status.Secret, true
}
if i.bindingItems == nil && len(i.bindings) == 1 {
ref := i.bindings[0].Source()
if ref != nil && ref.Namespace == i.serviceBinding.GetNamespace() {
return ref.Name, true
}
}
data := i.bindingItems.AsMap()
data := i.bindingItemMap()
keys := make([]string, 0, len(data))
for k := range data {
keys = append(keys, k)
Expand All @@ -185,7 +210,22 @@ func (i *impl) BindingSecretName() string {
_, _ = hash.Write([]byte(k))
_, _ = hash.Write([]byte(data[k]))
}
return i.serviceBinding.Name + "-" + string(hex.EncodeToString(hash.Sum(nil))[:8])
return i.serviceBinding.Name + "-" + string(hex.EncodeToString(hash.Sum(nil))[:8]), false
}

func (i *impl) bindingItemMap() map[string]string {
data := make(map[string]string)
for _, b := range i.bindings {
items, err := b.Items()
if err != nil {
continue
}
util.MergeMaps(data, items.AsMap())
}
if len(i.bindingItems) > 0 {
util.MergeMaps(data, i.bindingItems.AsMap())
}
return data
}

func (i *impl) NamingTemplate() string {
Expand Down Expand Up @@ -227,16 +267,20 @@ func (i *impl) persistBinding() error {
}

func (i *impl) persistSecret() (string, error) {
if i.bindingItems == nil || len(i.bindingItems) == 0 {
name, secretExist := i.bindingSecretName()
if secretExist {
return name, nil
}
data := i.bindingItemMap()
if len(data) == 0 {
return "", nil
}
name := i.BindingSecretName()
secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Namespace: i.serviceBinding.Namespace,
Name: name,
},
StringData: i.bindingItems.AsMap(),
StringData: data,
}
if i.serviceBinding.UID != "" {
secret.OwnerReferences = []metav1.OwnerReference{i.serviceBinding.AsOwnerReference()}
Expand Down Expand Up @@ -300,6 +344,10 @@ func (i *impl) ReadSecret(namespace string, name string) (*unstructured.Unstruct
return i.client.Resource(schema.GroupVersionResource{Group: "", Version: "v1", Resource: "secrets"}).Namespace(namespace).Get(context.Background(), name, metav1.GetOptions{})
}

func (i *impl) AddBindings(bindings pipeline.Bindings) {
i.bindings = append(i.bindings, bindings)
}

//go:generate mockgen -destination=mocks/mocks.go -package=mocks . K8STypeLookup

type K8STypeLookup interface {
Expand Down
190 changes: 190 additions & 0 deletions pkg/reconcile/pipeline/context/impl_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package context

import (
"context"
"encoding/base64"
e "errors"
"fmt"

Expand All @@ -13,6 +14,7 @@ import (
"github.com/redhat-developer/service-binding-operator/pkg/converter"
"github.com/redhat-developer/service-binding-operator/pkg/reconcile/pipeline"
"github.com/redhat-developer/service-binding-operator/pkg/reconcile/pipeline/context/mocks"
mocks2 "github.com/redhat-developer/service-binding-operator/pkg/reconcile/pipeline/mocks"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
Expand Down Expand Up @@ -459,6 +461,121 @@ var _ = Describe("Context", func() {

Expect(ctx.BindingSecretName()).To(Equal(ctx2.BindingSecretName()))
})

It("should be equal to existing secret if additional binding items exist", func() {
secretName := "foo"
namespace := "ns1"
ctx := &impl{serviceBinding: &v1alpha1.ServiceBinding{
ObjectMeta: metav1.ObjectMeta{
Name: "sb1",
Namespace: namespace,
},
}}
secret := &unstructured.Unstructured{Object: map[string]interface{}{
"data": map[string]interface{}{
"foo1": base64.StdEncoding.EncodeToString([]byte("val1")),
"foo2": base64.StdEncoding.EncodeToString([]byte("val2")),
},
}}
secret.SetName(secretName)
secret.SetNamespace(namespace)
secret.SetAPIVersion("v1")
secret.SetKind("Secret")

ctx.AddBindings(&pipeline.SecretBackedBindings{Secret: secret})

Expect(ctx.BindingSecretName()).To(Equal(secretName))
})

It("should be generated if additional items are added", func() {
secretName := "foo"
namespace := "ns1"
ctx := &impl{serviceBinding: &v1alpha1.ServiceBinding{
ObjectMeta: metav1.ObjectMeta{
Name: "sb1",
Namespace: namespace,
},
}}
secret := &unstructured.Unstructured{Object: map[string]interface{}{
"data": map[string]interface{}{
"foo1": base64.StdEncoding.EncodeToString([]byte("val1")),
"foo2": base64.StdEncoding.EncodeToString([]byte("val2")),
},
}}
secret.SetName(secretName)
secret.SetNamespace(namespace)
secret.SetAPIVersion("v1")
secret.SetKind("Secret")

ctx.AddBindings(&pipeline.SecretBackedBindings{Secret: secret})
ctx.AddBindingItem(&pipeline.BindingItem{Name: "foo", Value: "v1"})

bindingSecretName := ctx.BindingSecretName()
Expect(bindingSecretName).NotTo(BeEmpty())
Expect(bindingSecretName).NotTo(Equal(secretName))
})

It("should be generated if item key is modified", func() {
secretName := "foo"
namespace := "ns1"
ctx := &impl{serviceBinding: &v1alpha1.ServiceBinding{
ObjectMeta: metav1.ObjectMeta{
Name: "sb1",
Namespace: namespace,
},
}}
secret := &unstructured.Unstructured{Object: map[string]interface{}{
"data": map[string]interface{}{
"foo1": base64.StdEncoding.EncodeToString([]byte("val1")),
"foo2": base64.StdEncoding.EncodeToString([]byte("val2")),
},
}}
secret.SetName(secretName)
secret.SetNamespace(namespace)
secret.SetAPIVersion("v1")
secret.SetKind("Secret")

service := mocks2.NewMockService(mockCtrl)
b := &pipeline.SecretBackedBindings{Secret: secret, Service: service}
ctx.AddBindings(b)
items, err := b.Items()
Expect(err).NotTo(HaveOccurred())
items[0].Name = "bla"

bindingSecretName := ctx.BindingSecretName()
Expect(bindingSecretName).NotTo(BeEmpty())
Expect(bindingSecretName).NotTo(Equal(secretName))
})

It("should be generated if two binding secrets are set", func() {
secretNames := []string{"foo", "bar"}
namespace := "ns1"
ctx := &impl{serviceBinding: &v1alpha1.ServiceBinding{
ObjectMeta: metav1.ObjectMeta{
Name: "sb1",
Namespace: namespace,
},
}}
for _, sn := range secretNames {
secret := &unstructured.Unstructured{Object: map[string]interface{}{
"data": map[string]interface{}{
"foo1": base64.StdEncoding.EncodeToString([]byte("val1")),
"foo2": base64.StdEncoding.EncodeToString([]byte("val2")),
},
}}
secret.SetName(sn)
secret.SetNamespace(namespace)
secret.SetAPIVersion("v1")
secret.SetKind("Secret")

ctx.AddBindings(&pipeline.SecretBackedBindings{Secret: secret})
}

bindingSecretName := ctx.BindingSecretName()
Expect(bindingSecretName).NotTo(BeEmpty())
Expect(bindingSecretName).NotTo(Equal(secretNames[0]))
Expect(bindingSecretName).NotTo(Equal(secretNames[1]))
})
})

Describe("Close", func() {
Expand Down Expand Up @@ -665,5 +782,78 @@ var _ = Describe("Context", func() {
Expect(u.Object["Spec"]).To(Equal(specData))

})

It("should reuse existing secret if no other bindings are added", func() {
secret := &unstructured.Unstructured{Object: map[string]interface{}{
"data": map[string]interface{}{
"foo1": base64.StdEncoding.EncodeToString([]byte("val1")),
"foo2": base64.StdEncoding.EncodeToString([]byte("val2")),
},
}}
secret.SetName("foo")
secret.SetNamespace("ns1")
secret.SetAPIVersion("v1")
secret.SetKind("Secret")
service := mocks2.NewMockService(mockCtrl)

ctx.AddBindings(&pipeline.SecretBackedBindings{Secret: secret, Service: service})

err := ctx.Close()
Expect(err).NotTo(HaveOccurred())

u, err := client.Resource(v1alpha1.GroupVersionResource).Namespace(sb.Namespace).Get(context.Background(), sb.Name, metav1.GetOptions{})
Expect(err).NotTo(HaveOccurred())

updatedSB := v1alpha1.ServiceBinding{}
err = runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, &updatedSB)
Expect(err).NotTo(HaveOccurred())

Expect(updatedSB.Status.Secret).Should(Equal(secret.GetName()))

secretList, err := client.Resource(schema.GroupVersionResource{Group: "", Version: "v1", Resource: "secrets"}).List(context.Background(), metav1.ListOptions{})
Expect(err).NotTo(HaveOccurred())
Expect(secretList.Items).Should(BeEmpty())
})

It("Should create intermediate secret if additional bindings are added", func() {
secret := &unstructured.Unstructured{Object: map[string]interface{}{
"data": map[string]interface{}{
"foo1": base64.StdEncoding.EncodeToString([]byte("val1")),
"foo2": base64.StdEncoding.EncodeToString([]byte("val2")),
},
}}
secret.SetName("foo")
secret.SetNamespace("ns1")
secret.SetAPIVersion("v1")
secret.SetKind("Secret")
service := mocks2.NewMockService(mockCtrl)

ctx.AddBindings(&pipeline.SecretBackedBindings{Secret: secret, Service: service})
ctx.AddBindingItem(&pipeline.BindingItem{Name: "foo3", Value: "val3"})

err := ctx.Close()
Expect(err).NotTo(HaveOccurred())

u, err := client.Resource(v1alpha1.GroupVersionResource).Namespace(sb.Namespace).Get(context.Background(), sb.Name, metav1.GetOptions{})
Expect(err).NotTo(HaveOccurred())

updatedSB := v1alpha1.ServiceBinding{}
err = runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, &updatedSB)
Expect(err).NotTo(HaveOccurred())

Expect(updatedSB.Status.Secret).ShouldNot(Equal(secret.GetName()))
Expect(updatedSB.Status.Secret).ShouldNot(BeEmpty())

u, err = ctx.ReadSecret(sb.Namespace, sb.Status.Secret)
Expect(err).NotTo(HaveOccurred())

intermediateSecret := &corev1.Secret{}
err = runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, intermediateSecret)
Expect(err).NotTo(HaveOccurred())
Expect(intermediateSecret.StringData).To(HaveLen(3))
Expect(intermediateSecret.StringData).Should(HaveKeyWithValue("foo1", "val1"))
Expect(intermediateSecret.StringData).Should(HaveKeyWithValue("foo2", "val2"))
Expect(intermediateSecret.StringData).Should(HaveKeyWithValue("foo3", "val3"))
})
})
})

0 comments on commit 212651d

Please sign in to comment.