Skip to content

Commit

Permalink
Support managed binding parameters
Browse files Browse the repository at this point in the history
* Introduce the optional `CFServiceBinding.Spec.Parameters` to reference a secret to
  store the binding parameters
* The parameters secret is created by the binding repository. `kubectl`
  users should create it themselves if they want to provide such
  parameters
* The binding parameters are sent to the broker on `bind`. If
  `Spec.Parameters` is not set, no parameters are sent to the broker

fixes ##3549

Co-authored-by: Danail Branekov <danailster@gmail.com>
  • Loading branch information
uzabanov and danail-branekov committed Jan 9, 2025
1 parent 651ff42 commit dbe6fbf
Show file tree
Hide file tree
Showing 13 changed files with 539 additions and 182 deletions.
2 changes: 2 additions & 0 deletions api/payloads/service_binding.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ type ServiceBindingCreate struct {
Relationships *ServiceBindingRelationships `json:"relationships"`
Type string `json:"type"`
Name *string `json:"name"`
Parameters map[string]any `json:"parameters"`
}

func (p ServiceBindingCreate) ToMessage(spaceGUID string) repositories.CreateServiceBindingMessage {
Expand All @@ -21,6 +22,7 @@ func (p ServiceBindingCreate) ToMessage(spaceGUID string) repositories.CreateSer
ServiceInstanceGUID: p.Relationships.ServiceInstance.Data.GUID,
AppGUID: p.Relationships.App.Data.GUID,
SpaceGUID: spaceGUID,
Parameters: p.Parameters,
}
}

Expand Down
147 changes: 89 additions & 58 deletions api/payloads/service_binding_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ import (
"code.cloudfoundry.org/korifi/api/payloads"
"code.cloudfoundry.org/korifi/api/repositories"
"code.cloudfoundry.org/korifi/tools"
"github.com/google/uuid"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/onsi/gomega/gstruct"
. "github.com/onsi/gomega/gstruct"
)

var _ = Describe("ServiceBindingList", func() {
Expand Down Expand Up @@ -57,16 +58,11 @@ var _ = Describe("ServiceBindingList", func() {
})

var _ = Describe("ServiceBindingCreate", func() {
var (
createPayload payloads.ServiceBindingCreate
serviceBindingCreate *payloads.ServiceBindingCreate
validatorErr error
apiError errors.ApiError
)
var createPayload payloads.ServiceBindingCreate

BeforeEach(func() {
serviceBindingCreate = new(payloads.ServiceBindingCreate)
createPayload = payloads.ServiceBindingCreate{
Name: tools.PtrTo(uuid.NewString()),
Relationships: &payloads.ServiceBindingRelationships{
App: &payloads.Relationship{
Data: &payloads.RelationshipData{
Expand All @@ -80,82 +76,117 @@ var _ = Describe("ServiceBindingCreate", func() {
},
},
Type: "app",
Parameters: map[string]any{
"p1": "p1-value",
},
}
})

JustBeforeEach(func() {
validatorErr = validator.DecodeAndValidateJSONPayload(createJSONRequest(createPayload), serviceBindingCreate)
apiError, _ = validatorErr.(errors.ApiError)
})

It("succeeds", func() {
Expect(validatorErr).NotTo(HaveOccurred())
Expect(serviceBindingCreate).To(gstruct.PointTo(Equal(createPayload)))
})
Describe("Validation", func() {
var (
serviceBindingCreate *payloads.ServiceBindingCreate
validatorErr error
apiError errors.ApiError
)

When(`the type is "key"`, func() {
BeforeEach(func() {
createPayload.Type = "key"
serviceBindingCreate = new(payloads.ServiceBindingCreate)
})

It("fails", func() {
Expect(apiError).To(HaveOccurred())
Expect(apiError.Detail()).To(ContainSubstring("type value must be one of: app"))
JustBeforeEach(func() {
validatorErr = validator.DecodeAndValidateJSONPayload(createJSONRequest(createPayload), serviceBindingCreate)
apiError, _ = validatorErr.(errors.ApiError)
})
})

When("all relationships are missing", func() {
BeforeEach(func() {
createPayload.Relationships = nil
It("succeeds", func() {
Expect(validatorErr).NotTo(HaveOccurred())
Expect(serviceBindingCreate).To(PointTo(Equal(createPayload)))
})

It("fails", func() {
Expect(apiError).To(HaveOccurred())
Expect(apiError.Detail()).To(ContainSubstring("relationships is required"))
})
})
When(`the type is "key"`, func() {
BeforeEach(func() {
createPayload.Type = "key"
})

When("app relationship is missing", func() {
BeforeEach(func() {
createPayload.Relationships.App = nil
It("fails", func() {
Expect(apiError).To(HaveOccurred())
Expect(apiError.Detail()).To(ContainSubstring("type value must be one of: app"))
})
})

It("fails", func() {
Expect(apiError).To(HaveOccurred())
Expect(apiError.Detail()).To(ContainSubstring("relationships.app is required"))
When("all relationships are missing", func() {
BeforeEach(func() {
createPayload.Relationships = nil
})

It("fails", func() {
Expect(apiError).To(HaveOccurred())
Expect(apiError.Detail()).To(ContainSubstring("relationships is required"))
})
})
})

When("the app GUID is blank", func() {
BeforeEach(func() {
createPayload.Relationships.App.Data.GUID = ""
When("app relationship is missing", func() {
BeforeEach(func() {
createPayload.Relationships.App = nil
})

It("fails", func() {
Expect(apiError).To(HaveOccurred())
Expect(apiError.Detail()).To(ContainSubstring("relationships.app is required"))
})
})

It("fails", func() {
Expect(apiError).To(HaveOccurred())
Expect(apiError.Detail()).To(ContainSubstring("app.data.guid cannot be blank"))
When("the app GUID is blank", func() {
BeforeEach(func() {
createPayload.Relationships.App.Data.GUID = ""
})

It("fails", func() {
Expect(apiError).To(HaveOccurred())
Expect(apiError.Detail()).To(ContainSubstring("app.data.guid cannot be blank"))
})
})
})

When("service instance relationship is missing", func() {
BeforeEach(func() {
createPayload.Relationships.ServiceInstance = nil
When("service instance relationship is missing", func() {
BeforeEach(func() {
createPayload.Relationships.ServiceInstance = nil
})

It("fails", func() {
Expect(apiError).To(HaveOccurred())
Expect(apiError.Detail()).To(ContainSubstring("relationships.service_instance is required"))
})
})

It("fails", func() {
Expect(apiError).To(HaveOccurred())
Expect(apiError.Detail()).To(ContainSubstring("relationships.service_instance is required"))
When("the service instance GUID is blank", func() {
BeforeEach(func() {
createPayload.Relationships.ServiceInstance.Data.GUID = ""
})

It("fails", func() {
Expect(apiError).To(HaveOccurred())
Expect(apiError.Detail()).To(ContainSubstring("relationships.service_instance.data.guid cannot be blank"))
})
})
})

When("the service instance GUID is blank", func() {
BeforeEach(func() {
createPayload.Relationships.ServiceInstance.Data.GUID = ""
Describe("ToMessage", func() {
var createMessage repositories.CreateServiceBindingMessage

JustBeforeEach(func() {
createMessage = createPayload.ToMessage("space-guid")
})

It("fails", func() {
Expect(apiError).To(HaveOccurred())
Expect(apiError.Detail()).To(ContainSubstring("relationships.service_instance.data.guid cannot be blank"))
It("creates the message", func() {
Expect(createMessage).To(Equal(repositories.CreateServiceBindingMessage{
Name: tools.PtrTo(*createPayload.Name),
ServiceInstanceGUID: createPayload.Relationships.ServiceInstance.Data.GUID,
AppGUID: createPayload.Relationships.App.Data.GUID,
SpaceGUID: "space-guid",
Parameters: map[string]any{
"p1": "p1-value",
},
}))
})
})
})
Expand Down Expand Up @@ -185,7 +216,7 @@ var _ = Describe("ServiceBindingUpdate", func() {

It("succeeds", func() {
Expect(validatorErr).NotTo(HaveOccurred())
Expect(serviceBindingPatch).To(gstruct.PointTo(Equal(patchPayload)))
Expect(serviceBindingPatch).To(PointTo(Equal(patchPayload)))
})

When("metadata uses the cloudfoundry domain", func() {
Expand Down
56 changes: 48 additions & 8 deletions api/repositories/service_binding_repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (

"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/kubernetes/scheme"

"code.cloudfoundry.org/korifi/api/authorization"
apierrors "code.cloudfoundry.org/korifi/api/errors"
Expand All @@ -25,6 +26,7 @@ import (
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
)

const (
Expand Down Expand Up @@ -87,6 +89,7 @@ type CreateServiceBindingMessage struct {
ServiceInstanceGUID string
AppGUID string
SpaceGUID string
Parameters map[string]any
}

type DeleteServiceBindingMessage struct {
Expand All @@ -106,8 +109,8 @@ func (m *ListServiceBindingsMessage) matches(serviceBinding korifiv1alpha1.CFSer
tools.EmptyOrContains(m.PlanGUIDs, serviceBinding.Labels[korifiv1alpha1.PlanGUIDLabelKey])
}

func (m CreateServiceBindingMessage) toCFServiceBinding() *korifiv1alpha1.CFServiceBinding {
return &korifiv1alpha1.CFServiceBinding{
func (m CreateServiceBindingMessage) toCFServiceBinding(instanceType korifiv1alpha1.InstanceType) *korifiv1alpha1.CFServiceBinding {
binding := &korifiv1alpha1.CFServiceBinding{
ObjectMeta: metav1.ObjectMeta{
Name: uuid.NewString(),
Namespace: m.SpaceGUID,
Expand All @@ -123,6 +126,12 @@ func (m CreateServiceBindingMessage) toCFServiceBinding() *korifiv1alpha1.CFServ
AppRef: corev1.LocalObjectReference{Name: m.AppGUID},
},
}

if instanceType == korifiv1alpha1.ManagedType {
binding.Spec.Parameters.Name = uuid.NewString()
}

return binding
}

type UpdateServiceBindingMessage struct {
Expand All @@ -136,10 +145,20 @@ func (r *ServiceBindingRepo) CreateServiceBinding(ctx context.Context, authInfo
return ServiceBindingRecord{}, fmt.Errorf("failed to build user client: %w", err)
}

cfServiceBinding := message.toCFServiceBinding()
cfServiceInstance := new(korifiv1alpha1.CFServiceInstance)
err = userClient.Get(ctx, types.NamespacedName{Name: message.ServiceInstanceGUID, Namespace: message.SpaceGUID}, cfServiceInstance)
if err != nil {
return ServiceBindingRecord{},
apierrors.AsUnprocessableEntity(
apierrors.FromK8sError(err, ServiceBindingResourceType),
"Unable to bind to instance. Ensure that the instance exists and you have access to it.",
apierrors.ForbiddenError{},
apierrors.NotFoundError{},
)
}

cfApp := new(korifiv1alpha1.CFApp)
err = userClient.Get(ctx, types.NamespacedName{Name: cfServiceBinding.Spec.AppRef.Name, Namespace: cfServiceBinding.Namespace}, cfApp)
err = userClient.Get(ctx, types.NamespacedName{Name: message.AppGUID, Namespace: message.SpaceGUID}, cfApp)
if err != nil {
return ServiceBindingRecord{},
apierrors.AsUnprocessableEntity(
Expand All @@ -150,6 +169,7 @@ func (r *ServiceBindingRepo) CreateServiceBinding(ctx context.Context, authInfo
)
}

cfServiceBinding := message.toCFServiceBinding(cfServiceInstance.Spec.Type)
err = userClient.Create(ctx, cfServiceBinding)
if err != nil {
if validationError, ok := validation.WebhookErrorToValidationError(err); ok {
Expand All @@ -161,10 +181,11 @@ func (r *ServiceBindingRepo) CreateServiceBinding(ctx context.Context, authInfo
return ServiceBindingRecord{}, apierrors.FromK8sError(err, ServiceBindingResourceType)
}

cfServiceInstance := new(korifiv1alpha1.CFServiceInstance)
err = userClient.Get(ctx, types.NamespacedName{Name: cfServiceBinding.Spec.Service.Name, Namespace: cfServiceBinding.Namespace}, cfServiceInstance)
if err != nil {
return ServiceBindingRecord{}, fmt.Errorf("failed to get service instance: %w", err)
if cfServiceInstance.Spec.Type == korifiv1alpha1.ManagedType {
err = r.createParametersSecret(ctx, userClient, cfServiceBinding, message.Parameters)
if err != nil {
return ServiceBindingRecord{}, apierrors.FromK8sError(err, ServiceBindingResourceType)
}
}

if cfServiceInstance.Spec.Type == korifiv1alpha1.UserProvidedType {
Expand All @@ -177,6 +198,25 @@ func (r *ServiceBindingRepo) CreateServiceBinding(ctx context.Context, authInfo
return serviceBindingToRecord(*cfServiceBinding), nil
}

func (r *ServiceBindingRepo) createParametersSecret(ctx context.Context, userClient client.Client, cfServiceBinding *korifiv1alpha1.CFServiceBinding, parameters map[string]any) error {
parametersData, err := tools.ToParametersSecretData(parameters)
if err != nil {
return err
}

paramsSecret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Namespace: cfServiceBinding.Namespace,
Name: cfServiceBinding.Spec.Parameters.Name,
},
Data: parametersData,
}

_ = controllerutil.SetOwnerReference(cfServiceBinding, paramsSecret, scheme.Scheme)

return userClient.Create(ctx, paramsSecret)
}

func (r *ServiceBindingRepo) DeleteServiceBinding(ctx context.Context, authInfo authorization.Info, guid string) error {
userClient, err := r.userClientFactory.BuildClient(authInfo)
if err != nil {
Expand Down
Loading

0 comments on commit dbe6fbf

Please sign in to comment.